蓝桥杯嵌入式备赛用ADC实现8个按键只占1个引脚附完整代码与滤波算法在嵌入式系统开发中IO口资源往往非常宝贵尤其是在蓝桥杯这类竞赛中如何高效利用有限的硬件资源是每个参赛者必须面对的挑战。传统矩阵按键虽然能节省部分IO口但仍然需要多个引脚。本文将介绍一种更高效的解决方案——通过ADC模数转换器实现8个按键仅占用1个引脚的方案并分享完整的代码实现和滤波算法优化技巧。1. ADC按键原理与硬件设计1.1 电阻分压原理ADC按键的核心思想是利用电阻分压网络将不同按键的按下状态转换为不同的电压值。当每个按键按下时会形成不同的电阻组合从而产生不同的分压比。ADC引脚采集这些电压值后通过软件算法判断具体是哪个按键被按下。典型的分压网络设计如下VCC --- R1 --- R2 --- R3 --- ... --- Rn --- GND | | | | S1 S2 S3 Sn当按下某个开关时ADC引脚将检测到对应节点的电压值。精心选择电阻值可以确保每个按键按下时产生的电压区间互不重叠。1.2 硬件设计要点电阻选择建议使用1%精度的电阻阻值遵循等比数列如1k, 2k, 4k, 8k...可以最大化电压区分度防抖处理硬件上可并联小电容如0.1μF减少抖动ADC参考电压确保稳定必要时使用外部基准源提示实际应用中建议先用万用表测量各按键按下时的实际电压值作为软件判断的基准。2. STM32G4 ADC配置与HAL库实现2.1 ADC初始化配置以下是基于STM32G4系列和HAL库的ADC初始化代码示例ADC_HandleTypeDef hadc2; void MX_ADC2_Init(void) { ADC_ChannelConfTypeDef sConfig {0}; hadc2.Instance ADC2; hadc2.Init.ClockPrescaler ADC_CLOCK_ASYNC_DIV2; hadc2.Init.Resolution ADC_RESOLUTION_12B; hadc2.Init.ScanConvMode ADC_SCAN_DISABLE; hadc2.Init.ContinuousConvMode DISABLE; hadc2.Init.DiscontinuousConvMode DISABLE; hadc2.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc2.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc2.Init.NbrOfConversion 1; hadc2.Init.DMAContinuousRequests DISABLE; hadc2.Init.EOCSelection ADC_EOC_SINGLE_CONV; hadc2.Init.LowPowerAutoWait DISABLE; hadc2.Init.Overrun ADC_OVR_DATA_OVERWRITTEN; if (HAL_ADC_Init(hadc2) ! HAL_OK) { Error_Handler(); } sConfig.Channel ADC_CHANNEL_15; sConfig.Rank ADC_REGULAR_RANK_1; sConfig.SamplingTime ADC_SAMPLETIME_47CYCLES_5; sConfig.SingleDiff ADC_SINGLE_ENDED; sConfig.OffsetNumber ADC_OFFSET_NONE; sConfig.Offset 0; if (HAL_ADC_ConfigChannel(hadc2, sConfig) ! HAL_OK) { Error_Handler(); } }2.2 ADC数据采集函数uint16_t Get_ADC_Value(void) { uint16_t adc_value 0; HAL_ADC_Start(hadc2); if(HAL_ADC_PollForConversion(hadc2, 10) HAL_OK) { adc_value HAL_ADC_GetValue(hadc2); } HAL_ADC_Stop(hadc2); return adc_value; }3. 滤波算法实现与优化3.1 中值滤波算法ADC采集容易受到噪声干扰中值滤波能有效消除突发性干扰#define FILTER_LEN 15 uint16_t Median_Filter(void) { uint16_t buffer[FILTER_LEN]; uint16_t temp; uint8_t i, j; // 采集一组数据 for(i0; iFILTER_LEN; i) { buffer[i] Get_ADC_Value(); HAL_Delay(1); } // 冒泡排序 for(i0; iFILTER_LEN-1; i) { for(j0; jFILTER_LEN-i-1; j) { if(buffer[j] buffer[j1]) { temp buffer[j]; buffer[j] buffer[j1]; buffer[j1] temp; } } } // 返回中值 return buffer[FILTER_LEN/2]; }3.2 滑动平均滤波优化对于实时性要求高的场景可以采用滑动平均滤波#define WINDOW_SIZE 8 uint16_t Moving_Average_Filter(void) { static uint16_t window[WINDOW_SIZE] {0}; static uint8_t index 0; static uint32_t sum 0; sum - window[index]; window[index] Get_ADC_Value(); sum window[index]; index (index 1) % WINDOW_SIZE; return sum / WINDOW_SIZE; }4. 按键识别与处理框架4.1 按键状态判断#define KEY_THRESHOLD 50 // 按键识别阈值 uint8_t Get_Key_Number(void) { uint16_t adc_value Median_Filter(); if(adc_value 100) return 1; else if(adc_value 300) return 2; else if(adc_value 600) return 3; else if(adc_value 900) return 4; else if(adc_value 1200) return 5; else if(adc_value 1800) return 6; else if(adc_value 2500) return 7; else if(adc_value 3500) return 8; else return 0; }4.2 完整的按键处理框架typedef enum { KEY_IDLE, KEY_DOWN, KEY_PRESSED, KEY_UP } Key_State; typedef struct { Key_State state; uint8_t number; uint32_t press_time; } Key_Info; void Key_Process(Key_Info *key) { static uint8_t last_key 0; uint8_t current_key Get_Key_Number(); switch(key-state) { case KEY_IDLE: if(current_key ! 0) { key-number current_key; key-state KEY_DOWN; key-press_time HAL_GetTick(); } break; case KEY_DOWN: key-state KEY_PRESSED; // 触发按键按下事件 break; case KEY_PRESSED: if(current_key ! key-number) { key-state KEY_UP; } break; case KEY_UP: if(current_key 0) { key-state KEY_IDLE; // 触发按键释放事件 } break; } last_key current_key; }5. 实际应用中的优化技巧5.1 动态阈值校准为适应不同硬件环境可以实现动态阈值校准uint16_t key_thresholds[8] {100, 300, 600, 900, 1200, 1800, 2500, 3500}; void Calibrate_Keys(void) { uint8_t i; printf(Press each key in order...\n); for(i0; i8; i) { printf(Press key %d..., i1); while(Get_Key_Number() 0); key_thresholds[i] Get_ADC_Value(); printf(ADC value: %d\n, key_thresholds[i]); HAL_Delay(500); } // 计算中间值作为实际阈值 for(i0; i7; i) { key_thresholds[i] (key_thresholds[i] key_thresholds[i1]) / 2; } }5.2 低功耗优化对于电池供电设备可以优化ADC采样频率void Enter_LowPower_KeyMode(void) { // 降低ADC采样率 hadc2.Init.ClockPrescaler ADC_CLOCK_ASYNC_DIV8; HAL_ADC_Init(hadc2); // 使用中断唤醒 HAL_ADC_Start_IT(hadc2); } void ADC2_IRQHandler(void) { if(__HAL_ADC_GET_FLAG(hadc2, ADC_FLAG_EOC)) { uint16_t adc_val HAL_ADC_GetValue(hadc2); if(adc_val KEY_THRESHOLD) { // 唤醒系统 Exit_LowPower_Mode(); } } HAL_ADC_IRQHandler(hadc2); }6. 与传统矩阵按键的对比分析特性ADC按键方案传统矩阵按键IO占用1个引脚NM个引脚同时按键支持不支持有限支持硬件复杂度中等需电阻网络低软件复杂度高需ADC处理低抗干扰能力依赖滤波算法较强适用场景IO资源紧张的系统需要多键同按的系统在实际蓝桥杯竞赛中ADC按键方案特别适合以下场景需要大量外设导致IO紧张按键数量适中通常4-8个不需要多键同时按下功能对实时性要求不高7. 常见问题与调试技巧7.1 按键识别不准确可能原因及解决方案电阻值选择不当确保相邻按键的电压差足够大建议100LSB电源噪声干扰在VCC和GND之间添加滤波电容如10μF电解0.1μF陶瓷采样时间不足增加ADC采样周期如调整为ADC_SAMPLETIME_92CYCLES_57.2 响应速度慢优化建议减少滤波窗口大小如从15降到7采用更高效的排序算法如插入排序使用DMA进行连续采样7.3 低电压下的不稳定性解决方法// 在ADC初始化后添加校准 HAL_ADCEx_Calibration_Start(hadc2, ADC_SINGLE_ENDED);8. 完整代码模块与移植指南8.1 模块化设计建议将ADC按键功能封装为独立模块// adc_key.h #ifndef __ADC_KEY_H #define __ADC_KEY_H #include stm32g4xx_hal.h void ADC_Key_Init(void); uint8_t ADC_Key_GetState(void); void ADC_Key_Update(void); #endif8.2 主程序集成示例// main.c #include adc_key.h int main(void) { HAL_Init(); SystemClock_Config(); MX_ADC2_Init(); ADC_Key_Init(); while(1) { ADC_Key_Update(); uint8_t key ADC_Key_GetState(); if(key ! 0) { printf(Key %d pressed\n, key); HAL_Delay(200); // 简单防抖 } } }移植到其他STM32系列时主要需要修改ADC初始化配置时钟、通道等根据实际硬件调整电阻网络和阈值可能需要的HAL库版本适配