TM1638作为集成了数码管、LED和按键的三合一驱动芯片,在嵌入式设备中应用广泛。我最早接触这个芯片是在一个温控器项目上,当时最头疼的就是按键处理部分。传统的轮询扫描方式虽然简单,但实际使用中会遇到各种问题。
先说说基础原理。TM1638的8个按键全部连接到同一个K3引脚上,通过读取4个字节的数据来检测按键状态。每个字节的B0和B4位对应不同按键,具体对应关系如下:
这种设计导致两个主要痛点:一是无法实现组合按键,二是需要处理按键抖动问题。我在初期实现时用的是最简单的延时去抖法,代码大概长这样:
c复制uint8_t TM1638_ReadKey() {
uint8_t key = read_raw_key();
if(key != 0) {
delay_ms(20); // 简单延时去抖
if(key == read_raw_key()) {
return key;
}
}
return 0;
}
这种方法虽然能用,但在实际项目中暴露了明显缺陷:无法区分短按和长按,连发功能实现困难,而且在主循环中阻塞延时会影响其他任务执行。特别是在需要快速响应的场景(比如温度调节),用户体验很不好。
为了解决上述问题,我转向了状态机方案。状态机(State Machine)是嵌入式开发中的利器,特别适合处理这种有明确状态转换的场景。先来看按键操作的典型状态转换:
code复制空闲状态 → 按下检测 → 消抖等待 → 按下确认 → 长按计时 → 连发触发
用C语言实现时,我会定义一个枚举来表示这些状态:
c复制typedef enum {
KEY_STATE_IDLE, // 空闲
KEY_STATE_DOWN, // 按下检测
KEY_STATE_DEBOUNCE, // 消抖等待
KEY_STATE_PRESSED, // 按下确认
KEY_STATE_HOLD, // 长按计时
KEY_STATE_REPEAT // 连发触发
} KeyState;
每个按键需要维护自己的状态信息。我通常用结构体来组织这些数据:
c复制typedef struct {
KeyState state;
uint32_t timestamp;
uint8_t key_code;
uint16_t hold_counter;
} KeyContext;
状态机的核心处理函数大概框架如下。注意这里使用了非阻塞式的设计,通过系统滴答计时器来判断时间间隔:
c复制void Key_Process(KeyContext *ctx) {
uint8_t current_key = TM1638_ReadRawKey();
switch(ctx->state) {
case KEY_STATE_IDLE:
if(current_key != 0) {
ctx->key_code = current_key;
ctx->state = KEY_STATE_DOWN;
ctx->timestamp = HAL_GetTick();
}
break;
case KEY_STATE_DOWN:
if(HAL_GetTick() - ctx->timestamp > DEBOUNCE_TIME) {
if(current_key == ctx->key_code) {
ctx->state = KEY_STATE_PRESSED;
// 触发按键按下事件
} else {
ctx->state = KEY_STATE_IDLE;
}
}
break;
// 其他状态处理...
}
}
长按和连发是提升用户体验的关键功能。在温控器项目中,用户希望能按住"+"键连续调高温度,而不是反复点击。实现这个功能需要注意几个关键参数:
在状态机中,我们新增两个状态处理分支:
c复制case KEY_STATE_PRESSED:
if(current_key != ctx->key_code) {
ctx->state = KEY_STATE_IDLE;
// 触发按键释放事件(短按)
}
else if(HAL_GetTick() - ctx->timestamp > HOLD_THRESHOLD) {
ctx->state = KEY_STATE_HOLD;
ctx->hold_counter = 0;
// 触发长按开始事件
}
break;
case KEY_STATE_HOLD:
if(current_key != ctx->key_code) {
ctx->state = KEY_STATE_IDLE;
// 触发按键释放事件
}
else {
if(++ctx->hold_counter >= REPEAT_INTERVAL) {
ctx->hold_counter = 0;
// 触发连发事件
}
}
break;
实际项目中,我会把这些时间参数做成可配置的,方便不同场景调整:
c复制typedef struct {
uint16_t debounce_time; // 消抖时间(ms)
uint16_t hold_threshold; // 长按阈值(ms)
uint16_t repeat_interval;// 连发间隔(ms)
} KeyConfig;
结合前面内容,下面给出一个完整的TM1638按键驱动框架。这个版本经过多个项目验证,稳定性和响应速度都不错。
首先定义按键事件类型,丰富交互可能性:
c复制typedef enum {
KEY_EVENT_NONE,
KEY_EVENT_DOWN, // 按下瞬间
KEY_EVENT_UP, // 释放瞬间
KEY_EVENT_SHORT, // 短按
KEY_EVENT_HOLD, // 长按开始
KEY_EVENT_REPEAT // 连发触发
} KeyEventType;
按键处理核心代码如下,注意加入了超时保护机制:
c复制void TM1638_Key_Update(void) {
static KeyContext ctx = {0};
uint8_t raw_key = TM1638_ReadRawKey();
uint32_t current_time = HAL_GetTick();
uint32_t elapsed = current_time - ctx.timestamp;
switch(ctx.state) {
case KEY_STATE_IDLE:
if(raw_key && (raw_key == ctx.key_code || ctx.key_code == 0)) {
ctx.key_code = raw_key;
ctx.state = KEY_STATE_DOWN;
ctx.timestamp = current_time;
Send_Key_Event(KEY_EVENT_DOWN, ctx.key_code);
}
break;
case KEY_STATE_DOWN:
if(raw_key != ctx.key_code) {
ctx.state = KEY_STATE_IDLE;
Send_Key_Event(KEY_EVENT_UP, ctx.key_code);
}
else if(elapsed > g_config.debounce_time) {
ctx.state = KEY_STATE_PRESSED;
Send_Key_Event(KEY_EVENT_SHORT, ctx.key_code);
}
break;
case KEY_STATE_PRESSED:
if(raw_key != ctx.key_code) {
ctx.state = KEY_STATE_IDLE;
Send_Key_Event(KEY_EVENT_UP, ctx.key_code);
}
else if(elapsed > g_config.hold_threshold) {
ctx.state = KEY_STATE_HOLD;
ctx.hold_counter = 0;
Send_Key_Event(KEY_EVENT_HOLD, ctx.key_code);
}
break;
case KEY_STATE_HOLD:
if(raw_key != ctx.key_code) {
ctx.state = KEY_STATE_IDLE;
Send_Key_Event(KEY_EVENT_UP, ctx.key_code);
}
else {
if(++ctx.hold_counter >= g_config.repeat_interval) {
ctx.hold_counter = 0;
Send_Key_Event(KEY_EVENT_REPEAT, ctx.key_code);
}
}
break;
}
// 超时保护
if(elapsed > 5000) { // 5秒无操作重置
ctx.state = KEY_STATE_IDLE;
ctx.key_code = 0;
}
}
几个优化技巧分享:
在最近的智能温控器项目中,我应用了这套方案实现了以下交互逻辑:
关键实现代码如下:
c复制void Handle_Key_Event(KeyEventType event, uint8_t key_code) {
static bool setting_mode = false;
switch(key_code) {
case KEY_SET:
if(event == KEY_EVENT_SHORT) {
setting_mode = !setting_mode;
// 切换设置模式显示状态
}
else if(event == KEY_EVENT_HOLD) {
// 切换温度单位
Toggle_Temperature_Unit();
}
break;
case KEY_UP:
if(setting_mode && (event==KEY_EVENT_SHORT || event==KEY_EVENT_REPEAT)) {
Adjust_Setpoint(0.5f);
}
break;
case KEY_DOWN:
if(setting_mode && (event==KEY_EVENT_SHORT || event==KEY_EVENT_REPEAT)) {
Adjust_Setpoint(-0.5f);
}
break;
}
}
在STM32上的资源占用情况:
这套方案经过半年实际运行验证,按键响应灵敏,没有出现误触发或卡死的情况。特别是在工业环境下(有较强电磁干扰),表现依然稳定。