蓝桥杯嵌入式备赛:用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(i=0; i<FILTER_LEN; i++) { buffer[i] = Get_ADC_Value(); HAL_Delay(1); } // 冒泡排序 for(i=0; i<FILTER_LEN-1; i++) { for(j=0; j<FILTER_LEN-i-1; j++) { if(buffer[j] > buffer[j+1]) { temp = buffer[j]; buffer[j] = buffer[j+1]; buffer[j+1] = 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(i=0; i<8; i++) { printf("Press key %d...", i+1); while(Get_Key_Number() == 0); key_thresholds[i] = Get_ADC_Value(); printf("ADC value: %d\n", key_thresholds[i]); HAL_Delay(500); } // 计算中间值作为实际阈值 for(i=0; i<7; i++) { key_thresholds[i] = (key_thresholds[i] + key_thresholds[i+1]) / 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个引脚 | N+M个引脚 |
| 同时按键支持 | 不支持 | 有限支持 |
| 硬件复杂度 | 中等(需电阻网络) | 低 |
| 软件复杂度 | 高(需ADC处理) | 低 |
| 抗干扰能力 | 依赖滤波算法 | 较强 |
| 适用场景 | IO资源紧张的系统 | 需要多键同按的系统 |
在实际蓝桥杯竞赛中,ADC按键方案特别适合以下场景:
- 需要大量外设导致IO紧张
- 按键数量适中(通常4-8个)
- 不需要多键同时按下功能
- 对实时性要求不高
7. 常见问题与调试技巧
7.1 按键识别不准确
可能原因及解决方案:
- 电阻值选择不当:确保相邻按键的电压差足够大(建议>100LSB)
- 电源噪声干扰:在VCC和GND之间添加滤波电容(如10μF电解+0.1μF陶瓷)
- 采样时间不足:增加ADC采样周期(如调整为ADC_SAMPLETIME_92CYCLES_5)
7.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库版本适配