按鍵及消抖

方法一:延時阻塞

key.c:

#include "key.h"
#include "delay.h"//初始化GPIO
void key_init(void)
{GPIO_InitTypeDef gpio_initstruct;//打開時鐘__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能GPIOA時鐘//調用GPIO初始化函數gpio_initstruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;          // 兩個KEY對應的引腳gpio_initstruct.Mode = GPIO_MODE_INPUT;                 // 輸入gpio_initstruct.Pull = GPIO_PULLUP;                     // 默認上拉,要結合實際電路,如果按下拉低接口,輸入一個低電平,那這里就是上拉輸入gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH;           // 高速HAL_GPIO_Init(GPIOA, &gpio_initstruct);
}//按鍵掃描函數
uint8_t key_scan(void)
{//檢測按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判斷按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET){//如果確實是按下的狀態,等待按鍵松開while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);//返回按鍵值return 1;}}//檢測按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//消抖delay_ms(10);//再次判斷按鍵是否按下if(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET){//如果確實是按下的狀態,等待按鍵松開while(HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET);//返回按鍵值return 2;}}//返回默認值return 0;
}

key_scan() 里用 延時消抖(delay_ms(10))屬于 “時間消抖法”,優點是簡單,缺點是:

  • 延時是阻塞的,CPU 在等待時不能干別的事;

  • 多個按鍵時效率更低;

  • 延時時間設置不好會影響響應速度。

實際上,按鍵消抖方法有很多,除了“延時消抖”,常見的還有:

🔹 1. 狀態機消抖法(非阻塞,推薦)

用一個狀態機記錄按鍵狀態(未按下、按下確認、按住、釋放確認),每次掃描時更新狀態。

  • 掃描周期(比如 5ms 或 10ms)由 定時器中斷RTOS 任務來保證。

  • 只要穩定幾次掃描結果一致,才認為按下/松開。

優點:非阻塞,不用 delay,系統實時性好。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"   // 確保包含 HAL 頭
#include <stdint.h>/* ========================= 配置區域 ========================= */
/* 掃描周期與確認次數:* - 建議每 5~10ms 調用一次 key_scan()(可在主循環里用 HAL_GetTick() 節拍控制,或用定時器/RTOS任務)* - 連續 N 次一致才確認狀態變化:N * SCAN_TICKS_MS 即為有效消抖時間*/
#define SCAN_TICKS_MS        5      // 每次掃描的理想間隔(毫秒)
#define CONFIRM_COUNT        3      // 連續 3 次一致才確認(約 15ms 消抖)/* 按鍵引腳(按你的原工程保持 A0/A1 上拉,低電平=按下) */
#define KEY0_PORT            GPIOA
#define KEY0_PIN             GPIO_PIN_0
#define KEY1_PORT            GPIOA
#define KEY1_PIN             GPIO_PIN_1/* 物理讀取:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ========================= 狀態機定義 =========================* 我們用 4 態模型:*   IDLE(穩定松開)*   PRESS_CHECK(檢測到可能按下,進入確認期)*   PRESSED(穩定按下)*   RELEASE_CHECK(檢測到可能松開,進入確認期)* 時序:IDLE→PRESS_CHECK→PRESSED→RELEASE_CHECK→IDLE* 只有在 “PRESSED → RELEASE_CHECK → IDLE” 完成時,才認為一次完整按鍵發生并返回鍵值。*/
typedef enum {KEY_IDLE = 0,          // 穩定松開KEY_PRESS_CHECK,       // 按下確認中KEY_PRESSED,           // 穩定按下KEY_RELEASE_CHECK      // 松開確認中
} key_state_t;typedef struct {key_state_t state;     // 當前狀態uint8_t     cnt;       // 連續一致計數
} key_fsm_t;/* 兩個按鍵的狀態機 */
static key_fsm_t key0 = { KEY_IDLE, 0 };
static key_fsm_t key1 = { KEY_IDLE, 0 };/* 節拍控制:保證 key_scan() 按 SCAN_TICKS_MS 的節奏運行(防止被過于頻繁調用導致“計時加快”) */
static uint32_t s_last_tick = 0;/* ========================= 用戶接口實現 ========================= *//* 初始化GPIO:與原邏輯一致(上拉輸入,低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();  // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;   // 兩個 KEY 引腳gpio_initstruct.Mode  = GPIO_MODE_INPUT;           // 輸入模式gpio_initstruct.Pull  = GPIO_PULLUP;               // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;       // 輸入模式速度無關,LOW 即可HAL_GPIO_Init(GPIOA, &gpio_initstruct);// 狀態機復位key0.state = KEY_IDLE;  key0.cnt = 0;key1.state = KEY_IDLE;  key1.cnt = 0;s_last_tick = HAL_GetTick();
}/* 內部:推進單個鍵的狀態機;返回1表示“本次完成一次按鍵動作(按下并松開確認)”,否則0 */
static uint8_t step_fsm(key_fsm_t *k, uint8_t raw_press)
{switch (k->state){case KEY_IDLE:  // 穩定松開if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt   = 1;}break;case KEY_PRESS_CHECK:  // 按下確認中if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_PRESSED;   // 確認按下成立k->cnt   = 0;}} else {// 搖回去k->state = KEY_IDLE;k->cnt   = 0;}break;case KEY_PRESSED:  // 穩定按下if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt   = 1;}break;case KEY_RELEASE_CHECK:  // 松開確認中if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE;      // 確認松開成立k->cnt   = 0;return 1;                 // ——一次完整按鍵動作完成(與原版功能一致:松開后返回)}} else {// 又按下了,回到按下穩定k->state = KEY_PRESSED;k->cnt   = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}return 0;
}/* 非阻塞按鍵掃描:* - 需被“周期性地”調用(推薦每 5~10ms 一次)* - 返回:0=無;1=KEY0 完成一次按鍵;2=KEY1 完成一次按鍵* - 行為與原版一致:只有“按下→松開”完整動作完成才返回鍵值*/
uint8_t key_scan(void)
{/* 簡單節拍器:若調用過快則不推進(防止消抖時間被縮短) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {return 0;}s_last_tick = now;/* 讀取原始電平(低電平=按下) */uint8_t k0_raw = key0_raw();uint8_t k1_raw = key1_raw();/* 推進兩個鍵的狀態機;誰先完成誰先返回 */if (step_fsm(&key0, k0_raw)) return 1;if (step_fsm(&key1, k1_raw)) return 2;return 0;
}

使用建議

  • while(1) 主循環里,每次循環都調用一次 key_scan() 即可;內部已按 SCAN_TICKS_MS 自節拍。

  • 若你在定時器中斷/RTOS里有固定 5ms 節拍,也可以去掉內部節拍器邏輯,直接每 5ms 調一次 key_scan()(把頂部節拍相關代碼移除即可)。

  • 調參:

    • SCAN_TICKS_MS:掃描周期;

    • CONFIRM_COUNT:確認次數;有效消抖時間約為 SCAN_TICKS_MS × CONFIRM_COUNT(默認 ≈ 15ms)。

如果還想要按下事件立即上報(而不是等松開),可以在 KEY_PRESSED 的進入處(PRESS_CHECK 確認完成時)返回一個“PRESS 事件”;并在 RELEASE_CHECK 確認完成時返回“RELEASE 事件”。

🔹 2. 定時器中斷消抖

  • 使用定時器中斷(如 1ms)周期性采樣按鍵。

  • 若某引腳狀態連續穩定一段時間(如 20ms),再確認按下或松開。

👉 思路類似狀態機法,但觸發源換成了硬件定時器。

  • key_init():初始化 GPIO + 配置并啟動 TIM3 的 1 ms 中斷采樣。

  • key_scan():非阻塞,讀取由中斷產生的“完整按鍵事件”(按下→松開)并返回 1/2/0

思路:在 TIM3 1 ms中斷里做采樣與消抖的狀態機(PRESS_CHECK / RELEASE_CHECK),當“按下并確認”再“松開并確認”完成時,置一個事件標志。主循環調用 key_scan() 只需讀標志即可,無 delay、無 while 等待

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ===================== 用戶可調參數 ===================== */
/* 采樣周期:1ms(由 TIM3 產生) */
#define SAMPLE_PERIOD_MS      1/* 消抖確認時間:例如 20ms(按下與松開都采用該門限) */
#define STABLE_MS             20
#define CONFIRM_TICKS         (STABLE_MS / SAMPLE_PERIOD_MS)  // =20/* 兩個按鍵:PA0 / PA1,上拉輸入,低電平=按下 */
#define KEY0_PORT             GPIOA
#define KEY0_PIN              GPIO_PIN_0
#define KEY1_PORT             GPIOA
#define KEY1_PIN              GPIO_PIN_1/* 物理采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== 定時器句柄 ===================== */
static TIM_HandleTypeDef htim3;/* ===================== 狀態機與事件 ===================== */
/* 4 態狀態機:*  IDLE(穩定松開) → PRESS_CHECK(按下確認) → PRESSED(穩定按下) → RELEASE_CHECK(松開確認) → IDLE*  只有完成 PRESSED→RELEASE_CHECK→IDLE 時才記為“一次完整按鍵”。*/
typedef enum {KEY_IDLE = 0,        // 穩定松開KEY_PRESS_CHECK,     // 按下確認中KEY_PRESSED,         // 穩定按下KEY_RELEASE_CHECK    // 松開確認中
} key_state_t;typedef struct {key_state_t state;   // 當前狀態uint8_t     cnt;     // 連續一致的計數(用于確認)
} key_fsm_t;/* 兩個鍵的狀態機實例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 中斷里產生的“完整按鍵事件”標志(按下→松開完成) */
static volatile uint8_t key0_event = 0;  // 置1表示有一次完整的 KEY0 事件待取
static volatile uint8_t key1_event = 0;  // 置1表示有一次完整的 KEY1 事件待取/* ===================== 內部:推進一個鍵的狀態機(在中斷里調用) ===================== */
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, volatile uint8_t *event_flag)
{switch (k->state){case KEY_IDLE:  /* 穩定松開 -> 看到“按下”則進入確認 */if (raw_press) {k->state = KEY_PRESS_CHECK;k->cnt   = 1;}break;case KEY_PRESS_CHECK:  /* 按下確認:連續 CONFIRM_TICKS 次都“按下”才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_PRESSED;    // 確認“穩定按下”k->cnt   = 0;}} else {/* 中途又松開:判定失敗,回到 IDLE */k->state = KEY_IDLE;k->cnt   = 0;}break;case KEY_PRESSED:  /* 穩定按下 -> 觀察到“松開”則進入確認 */if (!raw_press) {k->state = KEY_RELEASE_CHECK;k->cnt   = 1;}break;case KEY_RELEASE_CHECK:  /* 松開確認:連續 CONFIRM_TICKS 次都“松開”才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_TICKS) {k->state = KEY_IDLE;       // 確認“穩定松開”k->cnt   = 0;*event_flag = 1;           // ——一次完整按鍵(按下→松開)完成,置事件}} else {/* 中途又按下:回到穩定按下 */k->state = KEY_PRESSED;k->cnt   = 0;}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== TIM3 初始化:1kHz(1ms)中斷 =====================* 典型 F1 時鐘:SYSCLK=72MHz, APB1=36MHz,但定時器時鐘在 APB1 分頻!=1 時倍頻到 72MHz。* 這里配置:Prescaler=7200-1 → 分頻 7200,計數頻率 10kHz;*          Period   =10-1   → 計滿 10 次產生更新 → 1kHz 中斷(1ms)。* 若你的時鐘不同,請按實際修改。* ====================================================================== */
static void TIM3_Init_1ms(void)
{__HAL_RCC_TIM3_CLK_ENABLE();htim3.Instance = TIM3;htim3.Init.Prescaler         = 7200 - 1;   // 72MHz / 7200 = 10kHzhtim3.Init.CounterMode       = TIM_COUNTERMODE_UP;htim3.Init.Period            = 10 - 1;     // 10kHz / 10 = 1kHz → 1mshtim3.Init.ClockDivision     = TIM_CLOCKDIVISION_DIV1;htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;HAL_TIM_Base_Init(&htim3);HAL_TIM_Base_Start_IT(&htim3);/* 使能 NVIC 中斷 */HAL_NVIC_SetPriority(TIM3_IRQn, 1, 0);HAL_NVIC_EnableIRQ(TIM3_IRQn);
}/* ===================== 用戶接口:GPIO + 定時器初始化 ===================== */
void key_init(void)
{/* GPIO:上拉輸入,低電平=按下(保持與原工程一致) */__HAL_RCC_GPIOA_CLK_ENABLE();GPIO_InitTypeDef io = {0};io.Pin   = GPIO_PIN_0 | GPIO_PIN_1;io.Mode  = GPIO_MODE_INPUT;io.Pull  = GPIO_PULLUP;io.Speed = GPIO_SPEED_FREQ_LOW;  // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &io);/* 狀態機與事件清零 */key0_fsm.state = KEY_IDLE;  key0_fsm.cnt = 0;  key0_event = 0;key1_fsm.state = KEY_IDLE;  key1_fsm.cnt = 0;  key1_event = 0;/* 啟動 TIM3 1ms 周期中斷,進行消抖采樣與判定 */TIM3_Init_1ms();
}/* ===================== 中斷服務:1ms 采樣與消抖 ===================== */
/* HAL 的更新回調:由 TIM3_IRQHandler → HAL_TIM_IRQHandler → 觸發此回調 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{if (htim->Instance == TIM3){/* 讀取原始電平(低電平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 推進兩個鍵的狀態機;事件標志在狀態機內部置位 */key_fsm_step(&key0_fsm, r0, &key0_event);key_fsm_step(&key1_fsm, r1, &key1_event);}
}/* TIM3 IRQHandler(放在 stm32f1xx_it.c 里更規范;若沒該文件也可暫放此處) */
void TIM3_IRQHandler(void)
{HAL_TIM_IRQHandler(&htim3);
}/* ===================== 用戶接口:查詢一次事件 ===================== */
/* 行為與原版保持一致:* - 返回 1:KEY0 完成一次“按下→松開”* - 返回 2:KEY1 完成一次“按下→松開”* - 返回 0:無新事件* 非阻塞:不含 delay,不會卡主循環。*/
uint8_t key_scan(void)
{if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

使用提示

  • 保持原來的主循環不變,周期性調用 key_scan() 即可(現在 不會阻塞 主循環)。

  • 如果在工程里使用了 SysTick 1 ms,也可以改為在 SysTick_Handler 里調用一個 Key_ISR_1ms() 來推進狀態機,思路完全一樣;這里用 TIM3 是為了不影響 SysTick

  • 想調整消抖時間:把 STABLE_MS 改為你需要的值(例如 10/15/30 ms),1 ms 采樣會自動按 CONFIRM_TICKS 計算確認門限。

🔹 3. 計數濾波法

  • 每次掃描時對按鍵狀態計數:

    • 檢測到按下 → count++

    • 檢測到松開 → count--

  • count 超過上限(如 >3)時確認“按下”,小于下限(如 <0)時確認“松開”。

👉 優點:實現簡單,兼顧濾波和消抖。

  • key_init():初始化 GPIO(上拉輸入,低電平按下)。

  • key_scan()非阻塞,建議每 5 ms 調用一次;當且僅當完成“一次按下→松開”的完整動作時返回 1/2,否則返回 0(與原代碼保持一致,但不再 delay/while 卡住主循環)。

計數濾波法要點:

  • 每次掃描讀一次原始電平(按下=1,松開=0)。

  • 計數器 cnt:按下則 +1,松開則 -1,并在 [0..CNT_MAX] 內飽和。

  • cnt >= TH_ON 認為“穩定按下”;當 cnt <= TH_OFF 認為“穩定松開”(帶回滯避免抖動來回翻轉)。

  • 只有從穩定按下轉到穩定松開時,才認定完成一次“點擊”,置事件供 key_scan() 返回(與原先“按下后等待松開再返回”的行為一致)。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"
#include <stdint.h>/* ====================== 可調參數 ====================== */
/* 建議每 5 ms 調用一次 key_scan()(主循環或定時器節拍) */
#define SCAN_TICKS_MS   5/* 計數濾波參數(積分 + 回滯):* - CNT_MAX:計數上限(越大濾波越強但響應越慢)* - TH_ON  :達到/超過該值判定“穩定按下”* - TH_OFF :小于/等于該值判定“穩定松開”(回滯閾值,應小于 TH_ON)* 例如:CNT_MAX=10,TH_ON=7,TH_OFF=3 → 近似相當于 ~20~40ms 的抗抖(視抖動形態)*/
#define CNT_MAX   10
#define TH_ON     7
#define TH_OFF    3/* ====================== 硬件引腳 ====================== */
/* 與原工程保持一致:PA0 / PA1 上拉輸入,低電平=按下 */
#define KEY0_PORT   GPIOA
#define KEY0_PIN    GPIO_PIN_0
#define KEY1_PORT   GPIOA
#define KEY1_PIN    GPIO_PIN_1/* 原始采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 計數濾波 + 事件 ====================== */
/* 穩定狀態標記:0=穩定按下,1=穩定松開(用 1 表示松開更直觀) */
typedef struct {uint8_t cnt;       // 0..CNT_MAX 的積分計數uint8_t state;     // 當前穩定狀態:1=松開,0=按下
} key_counter_t;static key_counter_t key0 = { CNT_MAX, 1 };  // 初始偏向“松開”
static key_counter_t key1 = { CNT_MAX, 1 };static volatile uint8_t key0_event = 0;      // 完整“按下→松開”事件
static volatile uint8_t key1_event = 0;static uint32_t s_last_tick = 0;             // 內部節拍器,確保按 SCAN_TICKS_MS 推進/* 推進單鍵的計數濾波與事件判定:* - raw_press: 1=按下,0=松開* - 當從“穩定按下(0)”轉變為“穩定松開(1)”時,置 *click_flag = 1*/
static void key_counter_step(key_counter_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 計數積分:按下(+1) / 松開(-1),并飽和在 [0..CNT_MAX] */if (raw_press) {if (k->cnt < CNT_MAX) k->cnt++;} else {if (k->cnt > 0)       k->cnt--;}/* 2) 回滯閾值判定,更新穩定狀態 */if (k->cnt >= TH_ON) {/* 穩定按下 */if (k->state != 0) {k->state = 0;               // 進入“穩定按下”/* 此處可產生“PRESS 事件”(如果你需要立即上報按下) */}} else if (k->cnt <= TH_OFF) {/* 穩定松開 */if (k->state != 1) {/* 僅當從按下(0)回到松開(1)時,認為一次點擊完成 */if (k->state == 0) {*click_flag = 1;        // ——一次“按下→松開”完成}k->state = 1;               // 進入“穩定松開”}}
}/* ====================== 用戶接口實現 ====================== *//* 初始化GPIO:與原工程一致(上拉輸入,低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();  // 使能 GPIOAGPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;  // 兩個 KEYgpio_initstruct.Mode  = GPIO_MODE_INPUT;          // 輸入gpio_initstruct.Pull  = GPIO_PULLUP;              // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;      // 輸入速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化計數與狀態、事件與節拍 */key0.cnt = CNT_MAX; key0.state = 1; key0_event = 0;key1.cnt = CNT_MAX; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞掃描:* - 建議每 5 ms 調用一次(內部也做了節拍限速,過快調用會直接返回 0)* - 返回:1=KEY0 完成一次“按下→松開”;2=KEY1 完成一次“按下→松開”;0=無事件* - 與原實現“按下后等待松開再返回”的功能一致,但不阻塞。*/
uint8_t key_scan(void)
{/* 簡單節拍器:確保按 SCAN_TICKS_MS 推進一次濾波(防止被高頻多次調用而縮短“有效消抖時間”) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 沒到節拍,不推進濾波 */goto _check_event_and_return;}s_last_tick = now;/* 一次節拍:讀取原始電平并推進兩個鍵的計數濾波 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_counter_step(&key0, r0, &key0_event);key_counter_step(&key1, r1, &key1_event);_check_event_and_return:/* 誰先完成一次點擊誰先返回;保證與原代碼返回語義一致 */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

說明與調參建議

  • 非阻塞:沒有 delay_ms()while(),主循環實時性更好。

  • 調用頻率:建議每 5 ms 調用一次 key_scan();若你已有固定節拍(比如用定時器/RTOS),也可以移除內部節拍器,固定周期調用即可。

  • 消抖強度

    • 增大 CNT_MAXTH_ON 可增強抗抖(響應更慢);

    • 減小 TH_OFF 可增加回滯,避免臨界抖動來回切換;

    • 一般保持 TH_OFF < TH_ON,如示例 TH_ON=7,TH_OFF=3

  • 事件時機:當前代碼僅在從穩定按下→穩定松開時上報事件(與原行為一致)。如果你需要“按下就上報(PRESS)/松開再上報(RELEASE)”,可在 k->state 切換處各自置不同事件枚舉即可。?

🔹 4. 硬件RC電路消抖

在按鍵硬件電路上加:

  • 電阻 + 電容(RC電路),濾掉機械抖動;

  • 或者加 施密特觸發器(如 74HC14)來增強邊沿穩定性。

👉 優點:軟件不需要處理消抖,反應更快。缺點是需要增加硬件成本。

🔹 5. 軟件濾波算法

  • 滑動平均:保存最近 N 次按鍵采樣結果,取多數值作為當前狀態;

  • 數字濾波:如 IIR、FIR 濾波,減少抖動影響。

  • 接口與語義保持不變:key_init() 初始化;key_scan() 只有在一次“按下→松開”完整動作結束時,返回 1/2(否則返回 0),與原阻塞版行為一致;

  • 不再使用 delay_ms() / while(...),建議每 5 ms 調用一次 key_scan()(也可用定時器/RTOS固定節拍調用)。

算法說明(軟件濾波)

  • 維護每個鍵最近 N=8 次采樣的“按下位歷史(1=按下,0=松開)”hist

  • 計算窗口內“按下”的個數 sum = popcount(hist)

  • 多數判決 + 回滯閾值

    • sum >= MAJ_ON(6) → 判為“穩定按下”;

    • sum <= MAJ_OFF(2) → 判為“穩定松開”;

  • 只有當“穩定按下→穩定松開”完成時,置一次點擊事件key_scan() 返回鍵值;

  • 回滯 (MAJ_OFF < MAJ_ON) 可避免在臨界抖動時來回翻轉。

仍然可以把 SCAN_TICKS_MSWIN_BITSMAJ_ONMAJ_OFF 調一下,達到你想要的響應/抗抖平衡。

key.c:

#include "key.h"
#include "stm32f1xx_hal.h"   // HAL 頭文件
#include <stdint.h>
// #include "delay.h"         // 本實現不再使用延時,可刪/* ======================== 可調參數 ======================== */
/* 建議每 5 ms 調用一次 key_scan()(主循環節拍或定時器/RTOS) */
#define SCAN_TICKS_MS   5/* 滑動窗口參數(軟件濾波) */
#define WIN_BITS        8    /* 窗口長度:最近 8 次采樣 */
#define MAJ_ON          6    /* 多數判決:>=6/8 認為“穩定按下” */
#define MAJ_OFF         2    /* 多數判決:<=2/8 認為“穩定松開”(回滯,應當 < MAJ_ON) *//* ======================== 硬件引腳 ======================== */
/* 與原工程保持一致:PA0/PA1 上拉輸入,低電平=按下 */
#define KEY0_PORT   GPIOA
#define KEY0_PIN    GPIO_PIN_0
#define KEY1_PORT   GPIOA
#define KEY1_PIN    GPIO_PIN_1/* 讀取原始電平:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ====================== 位歷史 + 事件 ====================== */
/* 穩定狀態語義:0=穩定按下,1=穩定松開(用 1 表示松開更直觀) */
typedef struct {uint8_t  hist;      // 最近 WIN_BITS 次的“按下=1/松開=0”歷史(這里是 8 位)uint8_t  state;     // 當前穩定狀態:1=松開,0=按下
} key_swf_t;static key_swf_t key0 = { 0x00, 1 };  // 初始歷史按 0(=松開),穩定狀態設為松開
static key_swf_t key1 = { 0x00, 1 };/* 完整“按下→松開”點擊事件(由濾波判定后置位,key_scan() 讀取后清零) */
static volatile uint8_t key0_event = 0;
static volatile uint8_t key1_event = 0;/* 內部節拍器,限制推進頻率(防止被高頻調用導致“有效消抖時間”變短) */
static uint32_t s_last_tick = 0;/* 高效 8 位 popcount(計算 hist 內 1 的個數) */
static inline uint8_t popcount8(uint8_t x)
{x = x - ((x >> 1) & 0x55);x = (x & 0x33) + ((x >> 2) & 0x33);return (uint8_t)(((x + (x >> 4)) & 0x0F));
}/* 推進一個鍵的滑動窗口濾波與事件判定* raw_press: 1=按下,0=松開* 過程:*   1) hist 左移一位,最低位寫入 raw_press(按下=1/松開=0)*   2) 統計 1 的個數 sum = popcount(hist)*   3) 多數判決 + 回滯:sum >= MAJ_ON → 穩定按下;sum <= MAJ_OFF → 穩定松開*   4) 當“穩定按下→穩定松開”發生時,置 click_flag=1(生成一次點擊事件)*/
static void key_swf_step(key_swf_t *k, uint8_t raw_press, volatile uint8_t *click_flag)
{/* 1) 移位引入當前樣本 */k->hist = (uint8_t)((k->hist << 1) | (raw_press ? 1u : 0u));/* 2) 多數判決 */uint8_t sum = popcount8(k->hist);if (sum >= MAJ_ON) {/* 判為“穩定按下” */if (k->state != 0) {k->state = 0;         // 進入穩定按下// 如需“按下立即上報”可在此處產生 PRESS 事件}} else if (sum <= MAJ_OFF) {/* 判為“穩定松開” */if (k->state != 1) {/* 只有從按下(0)回到松開(1)時,認為一次點擊完成 */if (k->state == 0) {*click_flag = 1;  // ——一次“按下→松開”完成}k->state = 1;         // 進入穩定松開}}
}/* ======================== 用戶接口 ======================== *//* 初始化 GPIO:與原始代碼一致(上拉輸入、低電平有效) */
void key_init(void)
{__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;        // 兩個 KEY 引腳gpio_initstruct.Mode  = GPIO_MODE_INPUT;                // 輸入gpio_initstruct.Pull  = GPIO_PULLUP;                    // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;            // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 初始化濾波與事件、節拍 */key0.hist = 0x00; key0.state = 1; key0_event = 0;key1.hist = 0x00; key1.state = 1; key1_event = 0;s_last_tick = HAL_GetTick();
}/* 非阻塞掃描(軟件濾波版本)* - 建議每 5 ms 調用一次;內部也做了 5 ms 的節拍限速* - 返回:1=KEY0 完成一次“按下→松開”;2=KEY1 完成一次“按下→松開”;0=無事件* - 與原代碼“按下后等待松開再返回”的語義一致,但不再阻塞主循環*/
uint8_t key_scan(void)
{/* 節拍器:確保按固定間隔推進一次濾波(防止被過快調用而降低消抖時間) */uint32_t now = HAL_GetTick();if ((now - s_last_tick) < SCAN_TICKS_MS) {/* 未到節拍,僅檢查是否已有事件 */goto _check_event_and_return;}s_last_tick = now;/* 采樣 + 推進兩個鍵的濾波器 */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();key_swf_step(&key0, r0, &key0_event);key_swf_step(&key1, r1, &key1_event);_check_event_and_return:/* 誰先完成一次點擊誰先返回;保證與原代碼返回時機一致(在“松開確認”時返回) */if (key0_event) { key0_event = 0; return 1; }if (key1_event) { key1_event = 0; return 2; }return 0;
}

使用與調參建議

  • 調用頻率:保持每 5 ms 調用一次 key_scan()。若你已有固定節拍,可以去掉內部節拍器,固定周期調用。

  • 窗口與閾值

    • 增大 WIN_BITSMAJ_ON,或減小 MAJ_OFF → 抗抖更強但響應更慢;

    • 減小 WIN_BITS,或降低 MAJ_ON、提高 MAJ_OFF → 響應更快但抗抖變弱;

    • 一般保持 MAJ_OFF < MAJ_ON 形成回滯,避免邊界抖動來回判定。

🔹 6. 操作系統事件消抖

如果在 FreeRTOSRT-Thread 中:

  • 按鍵掃描任務用固定周期(比如 10ms);

  • 信號量/消息隊列通知主任務。
    這樣消抖邏輯集中在一個“鍵盤驅動任務”里。

  • key_init():初始化 GPIO,并創建一個“按鍵掃描任務”,每隔 10ms 采樣,做去抖狀態機;當完成“按下→松開”一次完整點擊時,把鍵值(1 或 2)投遞到消息隊列

  • key_scan()非阻塞,從隊列里取一次事件,有就返回 1/2,沒有返回 0

  • 不再使用 delay_ms()while(...)delay.h 可保留但未使用。

算法:狀態機 + 固定周期采樣(10ms)。當同一狀態連續 N 次(這里 N=2 ? 約 20ms)保持一致,才確認“穩定按下/穩定松開”。當從“穩定按下”過渡到“穩定松開”時,判定一次完整點擊,并將鍵值通過 FreeRTOS 隊列發給主循環。

key.c:

#include "key.h"
#include "delay.h"              // 本實現已不再使用 delay,可保留以兼容舊工程
#include "stm32f1xx_hal.h"/* ===== FreeRTOS 頭文件(事件/任務/定時) ===== */
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdint.h>/* ===================== 用戶可調參數 ===================== */
/* 掃描周期:10ms(按鍵任務每 10ms 運行一次) */
#define SCAN_TICKS_MS        10/* 去抖確認次數:例如 2 次(2*10ms = 20ms),按下與松開都用該門限 */
#define CONFIRM_COUNT        2/* 隊列容量:能緩存這么多個“完整點擊事件” */
#define KEY_EVENT_QUEUE_LEN  8/* 硬件引腳:與原工程保持一致(PA0, PA1 上拉輸入,低電平=按下) */
#define KEY0_PORT            GPIOA
#define KEY0_PIN             GPIO_PIN_0
#define KEY1_PORT            GPIOA
#define KEY1_PIN             GPIO_PIN_1/* 物理采樣:返回 1=按下(低電平),0=松開(高電平) */
static inline uint8_t key0_raw(void) { return (HAL_GPIO_ReadPin(KEY0_PORT, KEY0_PIN) == GPIO_PIN_RESET); }
static inline uint8_t key1_raw(void) { return (HAL_GPIO_ReadPin(KEY1_PORT, KEY1_PIN) == GPIO_PIN_RESET); }/* ===================== OS 對象 ===================== */
static QueueHandle_t s_keyQueue = NULL;    /* 用于把“完整點擊事件”發給主循環 */
static TaskHandle_t  s_keyTask  = NULL;    /* 按鍵掃描任務 *//* ===================== 去抖狀態機 ===================== */
/* 4 態:*   IDLE(穩定松開) → PRESS_CHECK(按下確認) → PRESSED(穩定按下)*        → RELEASE_CHECK(松開確認) → IDLE* 只有通過 PRESSED→RELEASE_CHECK→IDLE,才認為一次“按下→松開”的完整點擊。*/
typedef enum {KEY_IDLE = 0,        // 穩定松開KEY_PRESS_CHECK,     // 按下確認中KEY_PRESSED,         // 穩定按下KEY_RELEASE_CHECK    // 松開確認中
} key_state_t;typedef struct {key_state_t state;   // 當前狀態uint8_t     cnt;     // 連續一致計數(用于確認)
} key_fsm_t;/* 兩個鍵的狀態機實例 */
static key_fsm_t key0_fsm = { KEY_IDLE, 0 };
static key_fsm_t key1_fsm = { KEY_IDLE, 0 };/* 推進單鍵狀態機:* raw_press: 1=按下, 0=松開* 完成一次“按下→松開”時,通過隊列投遞鍵值 event_val(1 或 2)*/
static void key_fsm_step(key_fsm_t *k, uint8_t raw_press, uint8_t event_val)
{switch (k->state){case KEY_IDLE:  /* 穩定松開 → 看到“按下”則進入確認 */if (raw_press) { k->state = KEY_PRESS_CHECK; k->cnt = 1; }break;case KEY_PRESS_CHECK:  /* 按下確認:連續 CONFIRM_COUNT 次均為按下才成立 */if (raw_press) {if (++k->cnt >= CONFIRM_COUNT) { k->state = KEY_PRESSED; k->cnt = 0; }} else {k->state = KEY_IDLE; k->cnt = 0;  /* 回退 */}break;case KEY_PRESSED:  /* 穩定按下 → 觀察到“松開”進入確認 */if (!raw_press) { k->state = KEY_RELEASE_CHECK; k->cnt = 1; }break;case KEY_RELEASE_CHECK:  /* 松開確認:連續 CONFIRM_COUNT 次均為松開才成立 */if (!raw_press) {if (++k->cnt >= CONFIRM_COUNT) {k->state = KEY_IDLE; k->cnt = 0;/* ——一次完整點擊完成:發事件給主循環 —— */if (s_keyQueue) { uint8_t v = event_val; xQueueSend(s_keyQueue, &v, 0); }}} else {k->state = KEY_PRESSED; k->cnt = 0;  /* 回退 */}break;default:k->state = KEY_IDLE; k->cnt = 0;break;}
}/* ===================== 按鍵掃描任務 =====================* 周期:每 10ms 運行一次* 邏輯:讀取原始電平 → 推進兩個鍵的狀態機 → 若形成“完整點擊”,將鍵值投遞到隊列*/
static void KeyScanTask(void *arg)
{(void)arg;for (;;){/* 1) 采樣兩鍵原始電平(低電平=按下) */uint8_t r0 = key0_raw();uint8_t r1 = key1_raw();/* 2) 推進兩路狀態機;完成點擊時由內部發隊列 */key_fsm_step(&key0_fsm, r0, 1);  // 鍵0 → 事件值 1key_fsm_step(&key1_fsm, r1, 2);  // 鍵1 → 事件值 2/* 3) 固定周期休眠(10ms) */vTaskDelay(pdMS_TO_TICKS(SCAN_TICKS_MS));}
}/* ===================== 用戶接口:初始化 ===================== */
void key_init(void)
{/* 1) GPIO 初始化(與原工程保持一致) */__HAL_RCC_GPIOA_CLK_ENABLE();                           // 使能 GPIOA 時鐘GPIO_InitTypeDef gpio_initstruct = {0};gpio_initstruct.Pin   = GPIO_PIN_0 | GPIO_PIN_1;        // 兩個 KEY 引腳gpio_initstruct.Mode  = GPIO_MODE_INPUT;                // 輸入gpio_initstruct.Pull  = GPIO_PULLUP;                    // 上拉gpio_initstruct.Speed = GPIO_SPEED_FREQ_LOW;            // 輸入模式速度無關HAL_GPIO_Init(GPIOA, &gpio_initstruct);/* 2) 狀態機復位 */key0_fsm.state = KEY_IDLE; key0_fsm.cnt = 0;key1_fsm.state = KEY_IDLE; key1_fsm.cnt = 0;/* 3) 創建事件隊列(緩存多個“完整點擊”) */s_keyQueue = xQueueCreate(KEY_EVENT_QUEUE_LEN, sizeof(uint8_t));/* 4) 創建按鍵掃描任務(優先級略高于空閑任務即可) */xTaskCreate(KeyScanTask, "key_scan", 256 /*棧字(4B)*/, NULL,tskIDLE_PRIORITY + 1, &s_keyTask);/* 注意:請確保在 main() 里調用 vTaskStartScheduler() 啟動調度器 */
}/* ===================== 用戶接口:獲取一次事件 ===================== */
/* 行為保持與原代碼一致:* - 返回 1:KEY0 完成一次“按下→松開”* - 返回 2:KEY1 完成一次“按下→松開”* - 返回 0:當前無事件* 非阻塞:0 超時讀取隊列*/
uint8_t key_scan(void)
{if (!s_keyQueue) return 0;uint8_t v = 0;if (xQueueReceive(s_keyQueue, &v, 0) == pdPASS) {return v;       // 1 或 2}return 0;
}

使用說明

  • 在系統初始化流程中調用 key_init(),隨后啟動 RTOS:vTaskStartScheduler()

  • 主循環或你的業務任務里,像以前一樣周期性調用 key_scan()

    • 返回 1/2 代表相應按鍵完成一次按下→松開

    • 返回 0 表示暫無事件;

  • 調參

    • SCAN_TICKS_MS 控制掃描周期;

    • CONFIRM_COUNT 控制按下/松開確認次數(SCAN_TICKS_MS * CONFIRM_COUNT ≈ 去抖時間,默認 ≈ 20ms)。

如果更想用軟件定時器而不是任務,也很容易:把 KeyScanTask 換成 xTimerCreate(..., pdMS_TO_TICKS(10), pdTRUE, ...) 的回調里做同樣的狀態機推進與 xQueueSend 即可,API 保持不變。

? 總結

  • 簡單項目:延時法足夠。

  • 稍復雜的單片機項目:狀態機法 / 計數法(非阻塞)。

  • 追求穩定 + 硬件條件允許:RC 濾波 + 軟件確認最佳。

  • RTOS 系統:定時掃描 + 消息隊列更優雅。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/93705.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/93705.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/93705.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

什么是接口?PHP如何使用 SessionHandlerInterface 接口實現Session自定義會話數據存儲

在面向對象編程中&#xff0c;接口&#xff08;Interface&#xff09;作為類與類之間的契約規范&#xff0c;定義了實現類必須遵守的方法簽名集合&#xff0c;卻不包含具體實現細節。這種抽象機制通過強制統一的方法命名和參數結構&#xff0c;實現了代碼的解耦與多態性&#x…

健身房預約系統SSM+Mybatis-plus實現(二、增刪改查的具體實現)

文章目錄一、環境搭建二、用戶管理頁面&#xff08;純展示無事件操作&#xff09;0.三步走1.查詢表單&#xff08;1&#xff09;書寫頁面代碼 &#xff1a;&#xff08;2&#xff09;對應的js部分創建對象數據模型的綁定部分&#xff1a;&#xff08;3&#xff09;引入需要的庫…

在IAR Embedded Workbench for Arm中實現NXP S32K3安全調試

隨著汽車電子系統變得越來越智能&#xff0c;對功能安全&#xff08;Safety&#xff09;的要求越來越高&#xff0c;同時信息安全&#xff08;Security&#xff09;也越來越被關注&#xff0c;安全調試&#xff08;Secure Debug&#xff09;機制已成為一個重要的信息安全特性。…

Vue實例中的其他屬性【5】

目錄1.計算屬性&#xff1a;1.概述&#xff1a;2.語法特點&#xff1a;3.案例&#xff1a;案例1&#xff1a;案例2&#xff1a;案例3&#xff1a;4.總結&#xff1a;5.get函數什么時候執行&#xff1f;6.注意:2.監視屬性&#xff1a;1.概述&#xff1a; 2.用法&#xff1a;1.監…

C++入門自學Day11-- String, Vector, List 復習

往期內容回顧 List類型的自實現 List類型&#xff08;初識&#xff09; Vector類的自實現 Vector類&#xff08;注意事項&#xff09; 初識Vector String類的自實現 String類的使用&#xff08;續&#xff09; String類&#xff08;續&#xff09; String類&#xff08;初識&…

JavaScript性能優化實戰(三):DOM操作性能優化

想象一下&#xff0c;你正在精心布置一個豪華蛋糕&#xff08;你的網頁&#xff09;&#xff0c;每次添加一顆草莓&#xff08;DOM元素&#xff09;都要把整個蛋糕從冰箱拿出來、放回去&#xff08;重排重繪&#xff09;&#xff0c;來來回回幾十次&#xff0c;不僅效率低下&am…

【力扣】面試經典150題總結02-雙指針、滑動窗口

1.驗證回文串&#xff08;簡單&#xff09;用toLowerCase()轉為小寫字母&#xff0c;然后前后指針向中間進行比對。2.判斷子序列&#xff08;簡單&#xff09;兩個指針一個指向長字符串&#xff0c;另一個指向短字符串。匹配就都1&#xff0c;不匹配就將長字符串指針1。長字符串…

MQ遷移方案

以下是完整的MQ遷移方案設計&#xff0c;涵蓋同構/異構遷移、零丟失保障、灰度切換等關鍵環節&#xff0c;適用于Kafka、RabbitMQ、RocketMQ等主流消息隊列&#xff1a;?一、遷移方案選型矩陣??場景??適用方案??技術實現??優缺點??同集群版本升級?滾動重啟 協議兼…

RAG 分塊中表格填補簡明示例:Markdown、HTML、Excel、Doc

表格填補是RAG分塊中常見的需求&#xff0c;但不同格式的表格處理方式有所不同。本文將對 Markdown、HTML、Excel 的合并單元格進行說明&#xff0c;并給出 Python 示例&#xff0c;演示如何解析和填補。1. Markdown 表格Markdown 只能用空值表示合并單元格。&#xff08;只有列…

IDEA創建一個VUE項目

由于新手學習VUE&#xff0c;所以使用手動初始化項目 步驟&#xff1a; 創建項目文件夾&#xff1a;在 IDEA 中點擊 File > New > Project&#xff0c;選擇 Empty Project&#xff0c;指定項目路徑。初始化 npm&#xff1a;在終端中&#xff1a;npm init -y安裝vue&#…

Chrome插件開發實戰:todoList 插件

以下是一個適合小團隊自用的 Chrome TodoList 插件開發示例&#xff0c;包含基礎功能&#xff08;增刪改查、本地存儲、統計&#xff09;和簡潔的交互設計。代碼結構清晰&#xff0c;適合新手學習或快速上手。 一、項目準備 創建插件項目目錄 todo-list-extension&#xff0c;…

【Redis數據庫開啟SSL加密】【小白指南】【生產環境可用】附帶Docker服務器配置和python連接Redis數據庫代碼(加密通訊版)

【Redis數據庫開啟SSL加密】【填坑指南】附帶服務器配置和python連接測試代碼 本教程轉為小白提供設置Redis安全訪問&#xff0c;自簽名證書進行安全訪問你的Redis數據庫&#xff0c;輕松實現安全訪問和保護數據庫不被非法入侵。 本文原創&#xff0c;轉載請注明出處&#xff0…

筆記本電腦鍵盤失靈【已解決】

配置環境硬件詳情筆記本電腦聯想拯救者y7000 2019 PG0&#xff08;已更新為win11&#xff09;外接鍵盤colorful ckb-p100問題今天筆記本開機后&#xff0c;進入登錄頁面輸入密碼&#xff0c;突然發現筆記本自帶鍵盤&#xff08;我通常不用外接鍵盤&#xff09;的鍵失靈了&#…

postgresql運維問題解決:PG集群備節點狀態異常告警處理

小亦平臺會持續給大家科普一些運維過程中常見的問題解決案例&#xff0c;運維朋友們可以在常見問題及解決方案專欄查看更多案例 問題概述&#xff1a; 故障&#xff1a; pg數據庫備節點狀態異常現象&#xff1a; 一般為集群間心跳超時導致,現象為集群有fail-count失敗數告警&…

Maven 開發實踐

文章目錄1. 搭建私服&#xff08;windows)2.上傳依賴3.多個遠程倉庫配置4.其它1. 搭建私服&#xff08;windows) 軟件下載 https://help.sonatype.com/en/download.html修改端口 etc/nexus-default.properties啟動程序 管理員身份進入進入bin目錄下執行.\nexus.exe /run創建Ma…

設計心得——如何架構選型

一、架構的作用 可能對于很多的公司&#xff0c;其實架構本身的重要性并不大。大家一定明白這回事&#xff0c;架構在實際的開發&#xff0c;在大多數的場景下其實用處并沒有書籍和資料中講的那樣重要&#xff0c;甚至是可有可無。這樣講是不有些可笑&#xff1f;是不是覺得挺意…

vba學習系列(12)--反射率通過率計算復雜度優化25/8/17

系列文章目錄 文章目錄系列文章目錄前言一、反射率通過率1.整體通過率2.整體通過率3.客戶工藝匹配4.機臺通過率分析5.鏡片通過率罩次分析分析1.1分析1.26.鏡片通過率圈數分析分析1.1分析1.28.鏡筒通過率圈數分析分析1.1分析1.29.鏡筒通過率罩次分析分析1.2總結前言 一、反射率通…

Microsoft WebView2

運行效果 代碼如下 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Form…

GitCode 疑難問題診療:全方位指南

一、問題診斷與解決框架1.1 通用問題排查流程在面對 GitCode 問題時&#xff0c;遵循一套科學的排查流程至關重要。首先&#xff0c;詳細記錄問題出現時的具體操作步驟與相關報錯信息&#xff0c;這有助于精準定位問題根源。例如&#xff0c;若在執行git push命令時出現錯誤&am…

AMD Ryzen AI Max+ 395四機并聯:大語言模型集群推理深度測試

本文介紹使用四塊Framework主板構建AI推理集群的完整過程&#xff0c;并對其在大語言模型推理任務中的性能表現進行了系統性評估。該集群基于AMD Ryzen AI Max 395處理器&#xff0c;采用mini ITX規格設計&#xff0c;可部署在10英寸標準機架中。 Jeff Geerling大佬還開發了名…