本文档详细说明如何将 keyflow 按键模块移植到不同硬件平台,包括 STM32、51 单片机、ESP32、Linux 用户态以及裸机环境。遵循同样的设计哲学:硬件抽象层(HAL)与逻辑层完全解耦,移植只需适配 3 个回调函数。
1. 移植架构总览
1.1 keyflow 的分层结构
┌─────────────────────────────────────────────┐
│ 应用层 (Application) │
│ 主循环 / 按键注册 / 业务逻辑 │
└────────────────────┬────────────────────────┘
│
┌────────────────────▼────────────────────────┐
│ 逻辑层 (keyflow / button_*.c) │
│ 六状态机 / 消抖 / 事件分发 / 队列 │
│ ──────────────────────────────────────── │
│ ⚠ 这一层完全不涉及硬件 │
└────────────────────┬────────────────────────┘
│ 仅通过函数指针耦合
┌────────────────────▼────────────────────────┐
│ 硬件抽象层 (HAL / Platform) │
│ GPIO 读取 / 时间源 / 中断处理 │
│ ──────────────────────────────────────── │
│ ✅ 这一层是移植的唯一改动点 │
└─────────────────────────────────────────────┘
1.2 移植的三要素
| 要素 |
函数签名 |
说明 |
| GPIO 读取 |
bool read_pin(uint16_t pin) |
返回引脚当前电平(0/1) |
| 时间获取 |
uint32_t get_tick(void) |
返回系统运行时间(单位 ms) |
| 中断处理 |
void exti_isr(uint8_t pin, uint8_t level, uint32_t tick) |
仅在使用中断驱动模式时需要 |
1.3 零成本抽象
未启用的扩展功能(矩阵键盘、事件队列、组合键等)在编译时完全不产生代码。以下是各模块的编译特性:
| 模块 |
未启用时 |
启用后 |
button.c 核心 |
状态机 + 消抖 (~3KB) |
--- |
button_queue.c |
0 bytes |
环形队列操作 (~0.5KB) |
button_matrix.c |
0 bytes |
行列扫描 (~1KB) |
button_combo.c |
0 bytes |
组合键/序列键 (~1.5KB) |
button_exti.c |
0 bytes |
中断分发 (~0.8KB) |
2. STM32 平台移植
2.1 平台特性
| 项目 |
说明 |
| 架构 |
ARM Cortex-M (STM32F1/F4/F7/H7 等) |
| 库 |
STM32Cube HAL / LL 库 |
| 时间源 |
HAL_GetTick() --- SysTick 中断,1ms 递增 |
| 中断 |
EXTI (外部中断) 支持任意 GPIO 引脚 |
| 编译器 |
arm-none-eabi-gcc (MDK/IAR/GD32 亦可) |
2.2 完整移植代码
/**
* @file platform_stm32.c
* @brief keyflow 在 STM32 HAL 上的移植实现
*/
#include "button/button.h"
#include "button/button_exti.h" /* 如使用中断模式 */
#include "stm32f4xx_hal.h" /* 根据实际芯片修改 */
/* ================================================================
* 硬件抽象层实现
* ================================================================ */
/* GPIO 读取回调
* @param pin 用户自定义的引脚索引 (0, 1, 2, ...)
* @return 0=低电平, 1=高电平
*
* 注意:这里将 pin 视为 GPIO_Pin 编号的 2^N 次方 (即 HAL_GPIO_PinSource0 = 1<<0)
* 实际项目中建议用 pin_map 数组映射,详见"引脚编号映射策略"章节
*/
bool hal_read_pin(uint16_t pin)
{
/* 将 pin (0,1,2...) 映射到具体 GPIO 和引脚号 */
GPIO_TypeDef *gpio_port = GPIOG; /* 根据实际修改 */
/* pin == 0 → GPIO_PIN_0, pin == 1 → GPIO_PIN_1 ... */
uint16_t gpio_pin = (uint16_t)(1U << pin);
return HAL_GPIO_ReadPin(gpio_port, gpio_pin) == GPIO_PIN_SET;
}
/* 时间源回调 */
uint32_t hal_get_tick(void)
{
return HAL_GetTick(); /* 1ms 分辨率,由 SysTick 提供 */
}
/* ================================================================
* 中断驱动模式(可选)
* ================================================================ */
/* 全局中断驱动管理器(定义在 main.c 或 app.c 中)*/
static ExtiButtonManager g_exti_mgr;
/* GPIO EXTI 中断回调
* 此函数由 HAL 库在 GPIO 引脚边沿变化时自动调用
*
* 注意:在真实项目中,需要区分不同引脚对应的 GPIO 端口
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
/* 获取触发时间 */
uint32_t tick = HAL_GetTick();
/* 将 GPIO_Pin 编号转换为用户定义的 pin 索引
* 例: GPIO_PIN_0 → 0, GPIO_PIN_5 → 5
* 使用 __builtin_ctz (Count Trailing Zeros) 高效计算
*/
uint8_t pin_idx = (uint8_t)__builtin_ctz(GPIO_Pin);
/* 读取当前电平 */
/* 实际项目中应根据 GPIO_Pin 找到对应的 GPIO 端口 */
uint8_t level = (HAL_GPIO_ReadPin(GPIOG, GPIO_Pin) == GPIO_PIN_SET) ? 1 : 0;
/* 记录中断事件 */
ExtiButton_OnInterrupt(&g_exti_mgr, pin_idx, level, tick);
}
/* ================================================================
* 应用层
* ================================================================ */
static ButtonManager g_btn_mgr;
void app_button_init(void)
{
/* 初始化按键管理器 */
ButtonManager_Init(&g_btn_mgr);
/* 设置时间源 */
ButtonManager_SetTimeSource(&g_btn_mgr, hal_get_tick);
/* 配置按键参数 */
ButtonConfig cfg = {
.pin = 0, /* GPIO_PIN_0 */
.active_level = 1, /* 低电平有效 */
.debounce_ms = 20,
.release_debounce_ms = 20,
.long_press_ms = 1000,
.double_click_ms = 300,
.stable_cnt_required = 2,
.long_press_repeat_ms = 0,
};
/* 注册按键 */
ButtonManager_AddButton(&g_btn_mgr, cfg,
hal_read_pin,
on_key_event, /* 用户定义的回调 */
NULL);
/* 如使用中断驱动模式 */
static ExtiButtonRecord rec_buf[8];
ExtiButton_Init(&g_exti_mgr, &g_btn_mgr, rec_buf, 8);
/* 配置 GPIO 为输入(上拉)--- 由 CubeMX 自动生成,此处仅供参考 */
/*
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2;
GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOG, &GPIO_InitStruct);
*/
}
/* 主循环 */
void app_main_loop(void)
{
while (1) {
/* 轮询模式 */
ButtonManager_UpdateAuto(&g_btn_mgr); /* 自动获取时间 */
/* 或中断驱动模式 */
/* ExtiButton_Dispatch(&g_exti_mgr, HAL_GetTick(), 10); */
/* 其他业务逻辑 */
HAL_Delay(10);
}
}
/* ================================================================
* 按键事件回调示例
* ================================================================ */
static void on_key_event(Button *btn, ButtonEventType event, void *ud)
{
(void)ud;
switch (event) {
case BUTTON_EVENT_PRESSED:
printf("[BTN %d] 按下\n", btn->index);
break;
case BUTTON_EVENT_RELEASED:
printf("[BTN %d] 释放\n", btn->index);
break;
case BUTTON_EVENT_CLICKED:
printf("[BTN %d] 单击\n", btn->index);
break;
case BUTTON_EVENT_LONG_PRESS:
printf("[BTN %d] 长按\n", btn->index);
break;
default:
break;
}
}
2.3 CubeMX 配置步骤
- 使能 GPIO 时钟 :
__HAL_RCC_GPIOG_CLK_ENABLE()
- 配置引脚为输入模式,上拉电阻使能
- 使能 EXTI 中断:在 NVIC 设置中开启对应引脚的外部中断
- 配置 SysTick:HAL 库自动使用 SysTick 作为 1ms 时钟源
3. 51 单片机平台移植
3.1 平台特性
| 项目 |
说明 |
| 架构 |
Intel 8051 (STC15/AT89C51/ISD51 等) |
| 编译器 |
SDCC / Keil C51 |
| 时间源 |
定时器 T0 中断,1ms 溢出 |
| GPIO |
P1.0~P1.7 共 8 个引脚(可扩展到 P0/P2/P3) |
| RAM |
128B~1KB,代码量极小 |
3.2 完整移植代码
/**
* @file platform_8051.c
* @brief keyflow 在 8051 (STC15) 上的移植实现
*/
#include <reg52.h> /* STC15 头文件 */
#include "button/button.h"
#include <stdint.h>
/* ================================================================
* 硬件抽象层实现
* ================================================================ */
/* GPIO 读取回调
* @param pin 0=P1.0, 1=P1.1, 2=P1.2 ...
*/
bool hal_read_pin(uint16_t pin)
{
if (pin >= 8) return 0; /* 仅 P1 有 8 个引脚 */
switch (pin) {
case 0: return (P1 & 0x01) != 0;
case 1: return (P1 & 0x02) != 0;
case 2: return (P1 & 0x04) != 0;
case 3: return (P1 & 0x08) != 0;
case 4: return (P1 & 0x10) != 0;
case 5: return (P1 & 0x20) != 0;
case 6: return (P1 & 0x40) != 0;
case 7: return (P1 & 0x80) != 0;
default: return 0;
}
}
/* 时间源 --- 使用定时器 T0 中断
*
* STC15 @ 11.0592MHz 晶振:
* 定时器模式 1 (16-bit), 定时 1ms
* 初值 = 65536 - (11059200 / 12 / 1000) ≈ 65536 - 920 = 64616
* 64616 = 0xFC68 → TH0=0xFC, TL0=0x68
*/
/* 定时器初值计算宏 */
#define TICK_MS_11M 64616 /* 1ms @ 11.0592MHz, 12T 模式 */
static volatile uint32_t g_tick = 0; /* 注意: volatile 防止编译器优化 */
/* 定时器 T0 中断服务程序 */
void timer0_isr(void) interrupt 1 /* 中断号 1 (8051 标准) */
{
TH0 = (uint8_t)(TICK_MS_11M >> 8);
TL0 = (uint8_t)(TICK_MS_11M & 0xFF);
g_tick++;
}
uint32_t hal_get_tick(void)
{
return g_tick;
}
/* 定时器初始化 */
void timer0_init(void)
{
TMOD &= 0xF0; /* 清零 T0 模式位 */
TMOD |= 0x01; /* 设置 T0 为模式 1 (16-bit 定时器) */
TH0 = (uint8_t)(TICK_MS_11M >> 8);
TL0 = (uint8_t)(TICK_MS_11M & 0xFF);
ET0 = 1; /* 使能 T0 中断 */
TR0 = 1; /* 启动 T0 */
EA = 1; /* 使能全局中断 */
}
/* ================================================================
* 应用层
* ================================================================ */
static ButtonManager g_btn_mgr;
/* 按键回调 */
static void on_key(Button *btn, ButtonEventType event, void *ud)
{
(void)ud;
if (event == BUTTON_EVENT_CLICKED) {
/* 根据 btn->index 执行不同功能 */
if (btn->index == 0) { /* P1.0 */ }
if (btn->index == 1) { /* P1.1 */ }
}
}
/* 应用初始化 */
void app_button_init(void)
{
/* 初始化定时器 */
timer0_init();
/* 初始化按键管理器 */
ButtonManager_Init(&g_btn_mgr);
ButtonManager_SetTimeSource(&g_btn_mgr, hal_get_tick);
/* 配置并注册 4 个按键 */
ButtonConfig cfg = {
.active_level = 0, /* 低电平有效 */
.debounce_ms = 20,
.release_debounce_ms = 20,
.long_press_ms = 800,
.double_click_ms = 250,
.stable_cnt_required = 2,
.long_press_repeat_ms = 0,
};
for (uint8_t i = 0; i < 4; i++) {
cfg.pin = i;
ButtonManager_AddButton(&g_btn_mgr, cfg, hal_read_pin, on_key, NULL);
}
}
/* 主循环 --- 8051 必须在 main 中循环调用 */
void main(void)
{
app_button_init();
while (1) {
/* 更新按键状态机(建议 5~10ms 周期)*/
ButtonManager_Update(&g_btn_mgr, hal_get_tick());
/* 其他业务... */
}
}
4. ESP32 平台移植
4.1 平台特性
| 项目 |
说明 |
| 架构 |
Tensilica Xtensa LX6 (Dual Core) |
| SDK |
ESP-IDF |
| 时间源 |
xTaskGetTickCount() * portTICK_PERIOD_MS (FreeRTOS) |
| 中断 |
GPIO 中断,支持任意引脚(除某些特殊引脚) |
| RAM |
200KB+,非常充裕 |
4.2 完整移植代码
/**
* @file platform_esp32.c
* @brief keyflow 在 ESP32 (ESP-IDF) 上的移植实现
*/
#include <driver/gpio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "button/button.h"
#include "button/button_exti.h"
#include <stdint.h>
/* ================================================================
* 硬件抽象层实现
* ================================================================ */
/* GPIO 读取回调
* @param pin ESP32 GPIO 编号 (0-39)
*/
bool hal_read_pin(uint16_t pin)
{
return gpio_get_level(pin) == 1;
}
/* 时间源回调 --- FreeRTOS 时钟
* portTICK_PERIOD_MS 在 ESP-IDF 中通常为 10 (configTICK_RATE_HZ=100)
*/
uint32_t hal_get_tick(void)
{
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
/* ================================================================
* 中断驱动模式
* ================================================================ */
static ExtiButtonManager g_exti_mgr;
/* GPIO 中断处理函数
* IRAM_ATTR 保证中断处理函数在 IRAM 中执行,减少中断延迟
*/
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
uint32_t gpio_num = (uint32_t)arg;
uint32_t tick = xTaskGetTickCount() * portTICK_PERIOD_MS;
uint8_t level = gpio_get_level(gpio_num);
ExtiButton_OnInterrupt(&g_exti_mgr, gpio_num, level, tick);
}
/* GPIO 初始化 */
static void gpio_init(void)
{
/* GPIO 配置: 输入 + 上拉 + 双边沿中断 */
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << GPIO_NUM_0) |
(1ULL << GPIO_NUM_4) |
(1ULL << GPIO_NUM_5),
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ONLY,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_ANYEDGE, /* 上升沿和下降沿均触发 */
};
gpio_config(&io_conf);
/* 安装 GPIO 中断服务 */
gpio_install_isr_service(ESP_INTR_FLAG_IRAM);
/* 绑定中断处理函数到各引脚 */
gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void *)GPIO_NUM_0);
gpio_isr_handler_add(GPIO_NUM_4, gpio_isr_handler, (void *)GPIO_NUM_4);
gpio_isr_handler_add(GPIO_NUM_5, gpio_isr_handler, (void *)GPIO_NUM_5);
}
/* ================================================================
* 按键事件回调
* ================================================================ */
static const char *key_names[] = { "GPIO0", "GPIO4", "GPIO5" };
static void on_key_event(Button *btn, ButtonEventType event, void *ud)
{
(void)ud;
const char *name = (btn->index < 3) ? key_names[btn->index] : "?";
switch (event) {
case BUTTON_EVENT_PRESSED:
printf("[%s] 按下\n", name);
break;
case BUTTON_EVENT_RELEASED:
printf("[%s] 释放\n", name);
break;
case BUTTON_EVENT_CLICKED:
printf("[%s] 单击\n", name);
break;
case BUTTON_EVENT_LONG_PRESS:
printf("[%s] 长按\n", name);
break;
default:
break;
}
}
/* ================================================================
* 应用层 --- RTOS 任务方式
* ================================================================ */
static ButtonManager g_btn_mgr;
static TaskHandle_t g_key_task_handle;
static void key_task(void *arg)
{
(void)arg;
/* 中断驱动模式:等待中断通知 */
while (1) {
/* 等待中断服务发送通知 (ulTaskNotifyTake 为阻塞调用) */
ulTaskNotifyTake(pdTRUE, portMAX_DELAY);
/* 分发中断事件 */
ExtiButton_Dispatch(&g_exti_mgr,
xTaskGetTickCount() * portTICK_PERIOD_MS,
10); /* 10ms 最小分发间隔 */
}
}
void app_main(void)
{
/* 初始化 GPIO 和中断 */
gpio_init();
/* 初始化按键管理器 */
ButtonManager_Init(&g_btn_mgr);
ButtonManager_SetTimeSource(&g_btn_mgr, hal_get_tick);
/* 注册 3 个按键 */
ButtonConfig cfg = {
.active_level = 0,
.debounce_ms = 20,
.release_debounce_ms = 20,
.long_press_ms = 800,
.double_click_ms = 300,
.stable_cnt_required = 2,
.long_press_repeat_ms = 0,
};
uint8_t pins[] = { GPIO_NUM_0, GPIO_NUM_4, GPIO_NUM_5 };
for (uint8_t i = 0; i < 3; i++) {
cfg.pin = pins[i];
ButtonManager_AddButton(&g_btn_mgr, cfg,
hal_read_pin,
on_key_event, NULL);
}
/* 初始化中断驱动管理器 */
static ExtiButtonRecord rec_buf[16];
ExtiButton_Init(&g_exti_mgr, &g_btn_mgr, rec_buf, 16);
/* 创建按键处理任务 (优先级较低,让主任务优先) */
xTaskCreatePinnedToCore(key_task, "key_task",
2048, /* 栈大小 */
NULL,
3, /* 优先级 */
&g_key_task_handle,
1); /* 绑定到 Core 1 (另一核心可运行 WiFi/BT) */
printf("ESP32 按键模块已初始化\n");
}
/* app_main 需在 app_main.c 中被 app_main() 调用 */
5. Linux 用户态平台移植
5.1 平台特性
| 项目 |
说明 |
| 环境 |
Linux 用户态,无 GPIO 访问权限的通用场景 |
| 输入设备 |
/dev/input/event* (evdev 接口) |
| 时间源 |
clock_gettime(CLOCK_MONOTONIC) |
| 适用场景 |
嵌入式 Linux 开发板 / 模拟器测试 / 按键回放测试 |
5.2 完整移植代码
/**
* @file platform_linux.c
* @brief keyflow 在 Linux 用户态的移植(使用 /dev/input)
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <linux/input.h>
#include <time.h>
#include <string.h>
#include <errno.h>
#include "button/button.h"
/* ================================================================
* 硬件抽象层实现
* ================================================================ */
static int g_fd = -1; /* /dev/input/event 文件描述符 */
static uint32_t g_tick_base = 0;/* 时间基准 */
/* GPIO 读取回调
* Linux 下无真实 GPIO,这里用 /dev/input/event 模拟
* 将 pin 视为 KEY_xxx 事件码
*/
bool hal_read_pin(uint16_t pin)
{
if (g_fd < 0) return 0;
struct input_event ev;
/* 读取所有待处理的事件(无阻塞)*/
while (read(g_fd, &ev, sizeof(ev)) > 0) {
if (ev.type == EV_KEY && ev.code == pin) {
/* 找到目标按键的事件,更新内部状态
* Linux input 子系统会报告按下和释放
* 这里用全局变量缓存当前电平
*/
return ev.value == 1;
}
}
return 0;
}
/* 时间源回调 --- 单调时钟 (CLOCK_MONOTONIC) */
uint32_t hal_get_tick(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
uint64_t ms = (uint64_t)ts.tv_sec * 1000ULL
+ (uint64_t)ts.tv_nsec / 1000000ULL;
/* 记录首次调用时的时间基准 */
if (g_tick_base == 0) {
g_tick_base = (uint32_t)ms;
}
return (uint32_t)(ms - g_tick_base);
}
/* ================================================================
* 应用层
* ================================================================ */
static ButtonManager g_btn_mgr;
static void on_key_event(Button *btn, ButtonEventType event, void *ud)
{
(void)ud;
printf("[%s] ", (char *)btn->user_data);
switch (event) {
case BUTTON_EVENT_PRESSED: printf("按下\n"); break;
case BUTTON_EVENT_RELEASED: printf("释放\n"); break;
case BUTTON_EVENT_CLICKED: printf("单击\n"); break;
case BUTTON_EVENT_LONG_PRESS: printf("长按\n"); break;
default: break;
}
}
/**
* 从 /proc/bus/input/devices 查找第一个 event 设备
* 或使用环境变量 INPUT_DEV 指定
*/
static int find_input_device(void)
{
char *dev = getenv("INPUT_DEV");
if (dev) {
return open(dev, O_RDONLY);
}
/* 遍历 /dev/input/event* 找第一个可用设备 */
for (int i = 0; i < 16; i++) {
char path[64];
snprintf(path, sizeof(path), "/dev/input/event%d", i);
int fd = open(path, O_RDONLY | O_NONBLOCK);
if (fd >= 0) {
printf("使用输入设备: %s\n", path);
return fd;
}
}
return -1;
}
int main(int argc, char *argv[])
{
printf("=== keyflow Linux 平台演示 ===\n");
/* 打开输入设备 */
g_fd = find_input_device();
if (g_fd < 0) {
fprintf(stderr, "无法打开 /dev/input 设备: %s\n",
strerror(errno));
fprintf(stderr, "提示: 可用 evtest 查看可用设备,或设置 INPUT_DEV 环境变量\n");
return 1;
}
/* 初始化按键管理器 */
ButtonManager_Init(&g_btn_mgr);
ButtonManager_SetTimeSource(&g_btn_mgr, hal_get_tick);
/* 定义按键映射表 (Linux KEY_* → keyflow 索引)
* 这里用 3 个按键演示: KEY_W=17, KEY_A=30, KEY_S=31
*/
struct { uint16_t pin; const char *name; } key_map[] = {
{ KEY_W, "W (上)" },
{ KEY_A, "A (左)" },
{ KEY_S, "S (下)" },
};
ButtonConfig cfg = {
.active_level = 1,
.debounce_ms = 50, /* Linux 输入设备已有消抖,设大一些 */
.release_debounce_ms = 50,
.long_press_ms = 1000,
.double_click_ms = 300,
.stable_cnt_required = 2,
.long_press_repeat_ms = 200,
};
for (size_t i = 0; i < sizeof(key_map)/sizeof(key_map[0]); i++) {
cfg.pin = key_map[i].pin;
ButtonManager_AddButton(&g_btn_mgr, cfg,
hal_read_pin,
on_key_event,
(void *)key_map[i].name);
}
printf("按 KEY_W / KEY_A / KEY_S 测试...\n\n");
/* 主循环 --- 读取 input 事件并更新按键 */
while (1) {
/* 读取 /dev/input 事件(会填充内部按键状态)*/
struct input_event ev;
fd_set rfds;
struct timeval tv;
FD_ZERO(&rfds);
FD_SET(g_fd, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 10000; /* 10ms 超时 */
int ret = select(g_fd + 1, &rfds, NULL, NULL, &tv);
if (ret > 0) {
while (read(g_fd, &ev, sizeof(ev)) > 0) {
if (ev.type == EV_KEY) {
/* 将事件传递给按键管理器
* 实际项目中这里需要根据 ev.code 找到对应按键
* 并更新其 GPIO 状态
*/
(void)ev;
}
}
}
/* 更新按键状态机 */
ButtonManager_Update(&g_btn_mgr, hal_get_tick());
}
close(g_fd);
return 0;
}
5.3 编译与运行
# 编译
gcc -Wall -Wextra -O2 \
-I ../include \
-o keyflow_linux \
platform_linux.c ../src/button/button.c
# 运行
INPUT_DEV=/dev/input/event0 ./keyflow_linux
# 或直接运行(自动查找设备)
./keyflow_linux
6.1 平台特性
| 项目 |
说明 |
| 环境 |
无操作系统,无标准库 |
| 编译器 |
GCC (arm-none-eabi-gcc / riscv-none-embed-gcc) |
| 时间源 |
SysTick 或硬件定时器 |
| 中断 |
EXTI / GPIO 中断(如果有) |
| 关键约束 |
不能使用 malloc/printf,避免浮点运算 |
6.2 完整移植代码
/**
* @file platform_baremetal.c
* @brief keyflow 在裸机环境(任何 MCU)上的最小移植
*
* 适用于: ARM Cortex-M / RISC-V / MSP430 / AVR 等
* 不依赖任何标准库和操作系统
*/
/* ================================================================
* 最小移植:只需实现这 3 个函数
* ================================================================ */
/* GPIO 读取 --- 适用于任何 MCU
*
* 示例: 假设按键连接在 PORTA 的 bit 0~3
* PORTB 的 bit 0~3 用于 LED (仅示例)
*
* 寄存器映射 (由 MCU 头文件提供):
* #define PORTA_IN (*(volatile uint8_t *)0x40020000)
* #define PORTA_DIR (*(volatile uint8_t *)0x40020004)
*/
/* 读取 PORTA 第 pin 位的电平 */
bool hal_read_pin(uint16_t pin)
{
extern volatile uint8_t PORTA_IN; /* 由链接器或头文件提供 */
extern volatile uint8_t PORTA_DIR;
if (pin >= 8) return 0;
return (PORTA_IN & (1U << pin)) != 0;
}
/* 时间源 --- 使用 SysTick 或定时器
*
* 裸机中必须有一个周期性中断源来递增 tick。
* 这里假设已配置好 SysTick 或定时器,使其每 1ms 产生一次中断,
* 中断服务程序中递增 g_tick 变量。
*
* g_tick 必须声明为 volatile,防止被编译器优化掉
*/
extern volatile uint32_t g_tick; /* 由启动文件或定时器驱动提供 */
uint32_t hal_get_tick(void)
{
return g_tick;
}
/* ================================================================
* 中断处理 (仅中断驱动模式需要)
* ================================================================ */
/* ExtiButton_OnInterrupt 的平台绑定
*
* 在真实裸机项目中,GPIO 中断服务程序(ISR)大致如下:
*
* void PORTA_IRQHandler(void)
* {
* uint32_t pending = PORTA_PENDING; // 读取中断挂起寄存器
* uint32_t tick = g_tick;
*
* if (pending & (1U << 0)) { // PORTA Pin 0
* uint8_t level = (PORTA_IN & 0x01) ? 1 : 0;
* ExtiButton_OnInterrupt(&g_exti, 0, level, tick);
* PORTA_PENDING = (1U << 0); // 清除中断标志
* }
* // ... 其他引脚类似
* }
*/
/* ================================================================
* 关键约束与注意事项
* ================================================================
*
* 1. 不要使用 malloc/free
* → 所有缓冲区使用静态分配或栈上数组
* → 事件队列、按键缓冲等均使用固定大小数组
*
* 2. 不要使用 printf (除非实现了 syscall)
* → 使用 UART 轮询发送字符
* → 或重定向 putchar 到串口
*
* 3. 避免浮点运算
* → 按键模块中没有浮点运算,但应用层可能有
* → 如需浮点,使用 -float-abi 等编译选项
*
* 4. 栈大小估算
* → 每个按键约需 64 字节栈 (状态机递归深度浅)
* → 主循环栈 + 按键处理栈 < 1KB (典型)
* → 估算公式: 128 + (BTN_COUNT * 64) + (额外业务栈)
*
* 5. 编译优化
* → 禁止使用 -O0(状态机会计时不准)
* → 推荐 -O1 或 -O2
* → 中断处理函数使用 -fno-optimize-sibling-calls
*/
/* ================================================================
* 应用层模板
* ================================================================ */
static ButtonManager g_btn_mgr;
/* 无操作的时间空转(代替 HAL_Delay) */
static void delay_ms(uint32_t ms)
{
uint32_t start = g_tick;
while ((g_tick - start) < ms) { __WFI(); }
}
int main(void)
{
/* 平台初始化(时钟、GPIO、定时器)*/
/* platform_init(); */
/* 初始化按键模块 */
ButtonManager_Init(&g_btn_mgr);
ButtonManager_SetTimeSource(&g_btn_mgr, hal_get_tick);
/* 注册按键 */
ButtonConfig cfg = {
.active_level = 0,
.debounce_ms = 20,
.release_debounce_ms = 20,
.long_press_ms = 800,
.double_click_ms = 250,
.stable_cnt_required = 2,
.long_press_repeat_ms = 0,
};
for (uint8_t i = 0; i < 4; i++) {
cfg.pin = i;
ButtonManager_AddButton(&g_btn_mgr, cfg,
hal_read_pin, NULL, NULL);
}
/* 主循环 */
while (1) {
ButtonManager_UpdateAuto(&g_btn_mgr);
delay_ms(10); /* 10ms 扫描周期 */
}
}
7. 引脚编号映射策略
在真实项目中,GPIO 引脚编号通常是离散的(不一定是连续的 0, 1, 2...),建议使用枚举+映射表统一管理。
7.1 映射表方式
/**
* @file pin_mapping.h
* @brief 统一的引脚编号映射 --- 推荐在实际项目中使用
*/
/* 用户自定义的逻辑按键编号 */
typedef enum {
KEY_MENU = 0,
KEY_UP = 1,
KEY_DOWN = 2,
KEY_LEFT = 3,
KEY_RIGHT = 4,
KEY_OK = 5,
KEY_BACK = 6,
KEY_PWR = 7,
KEY_VOL_UP = 8,
KEY_VOL_DN = 9,
BTN_COUNT /* 必须是最后一个 */
} AppKeyIndex;
/* 按键编号 → GPIO 引脚映射
* 适用于: STM32 (GPIO_Pin), 51 (P1.x), ESP32 (gpio_num_t)
*/
typedef struct {
AppKeyIndex key_id;
uint8_t gpio_port; /* GPIO 端口: 0=GPIOA, 1=GPIOB, ... */
uint16_t gpio_pin; /* 具体引脚编号 */
} KeyPinMap;
/* 引脚映射表 --- 根据原理图填写 */
static const KeyPinMap g_key_map[BTN_COUNT] = {
[KEY_MENU] = { KEY_MENU, 0, GPIO_PIN_1 }, /* PG1 */
[KEY_UP] = { KEY_UP, 0, GPIO_PIN_2 }, /* PG2 */
[KEY_DOWN] = { KEY_DOWN, 0, GPIO_PIN_3 }, /* PG3 */
[KEY_LEFT] = { KEY_LEFT, 0, GPIO_PIN_4 }, /* PG4 */
[KEY_RIGHT] = { KEY_RIGHT, 0, GPIO_PIN_5 }, /* PG5 */
[KEY_OK] = { KEY_OK, 0, GPIO_PIN_0 }, /* PG0 */
[KEY_BACK] = { KEY_BACK, 1, GPIO_PIN_0 }, /* PH0 */
[KEY_PWR] = { KEY_PWR, 1, GPIO_PIN_1 }, /* PH1 */
[KEY_VOL_UP] = { KEY_VOL_UP, 1, GPIO_PIN_2 }, /* PH2 */
[KEY_VOL_DN] = { KEY_VOL_DN, 1, GPIO_PIN_3 }, /* PH3 */
};
/* GPIO 读取函数 --- 使用映射表 */
bool hal_read_pin(uint16_t logical_index)
{
if (logical_index >= BTN_COUNT) return 0;
const KeyPinMap *m = &g_key_map[logical_index];
/* 根据端口选择 GPIO 寄存器 */
GPIO_TypeDef *gpio_ports[] = { GPIOA, GPIOB, /* ... */ };
GPIO_TypeDef *port = gpio_ports[m->gpio_port];
return HAL_GPIO_ReadPin(port, m->gpio_pin) == GPIO_PIN_SET;
}
8. 移植检查清单
完成移植后,按以下清单逐项验证:
8.1 GPIO 读取验证
| 检查项 |
操作 |
预期结果 |
| 上拉/下拉 |
测量引脚电压 |
按键未按时为高/低(取决于电路) |
| 读取返回值 |
用万用表对比 hal_read_pin() |
返回值与实际电平一致 |
| 多按键独立性 |
同时按下两个按键 |
两个 hal_read_pin() 均返回正确值 |
| 引脚复用 |
检查无其他外设复用同一引脚 |
无冲突 |
8.2 时间源验证
| 检查项 |
操作 |
预期结果 |
| tick 递增 |
打印 hal_get_tick() 两次,间隔 100ms |
差值 ≈ 100 |
| 精度 |
连续读取 10 次 tick(无延时) |
差值均为 0 或 1(无累积误差) |
| 中断重入 |
在定时器中断中读取 tick |
读取正确(无竞态) |
8.3 消抖验证
| 检查项 |
操作 |
预期结果 |
| 快速轻触 |
用螺丝刀快速碰触按键引脚 |
无事件产生 |
| 正常按压 |
正常速度按下并释放 |
仅产生一次单击事件 |
| 长按 |
按住按键 1.5s 不释放 |
500ms 时产生长按事件 |
8.4 内存与性能
| 检查项 |
方法 |
标准 |
| 代码大小 |
编译后 .text 段大小 |
核心模块 < 5KB |
| RAM 使用 |
统计全局变量 + 栈 |
按键数×16 + 队列大小 < 1KB |
| 中断延迟 |
用示波器测量 EXT 中断响应时间 |
< 50μs (裸机) |
| 扫描周期 |
在主循环中计时 ButtonManager_Update() |
< 1ms / 按键数 |
9. 常见移植问题与解决
| 问题 |
原因 |
解决方法 |
| 按键一直显示"按下" |
上拉/下拉配置错误 |
检查电路图,确认引脚模式 |
| 长按无法触发 |
tick 未递增或分辨率不够 |
用示波器/调试器确认 tick 每 1ms 变化 |
| 消抖无效,按一下出多个事件 |
消抖时间 < 实际抖动时长 |
增加 debounce_ms 到 30~50ms |
| 多按键同时按下时事件丢失 |
中断处理函数中操作了被中断的代码 |
在中断中只记录标志,在主循环中处理 |
| 长时间运行后按键无响应 |
定时器溢出 (16-bit tick 到 65536ms) |
使用 32-bit tick,或在溢出时重置按键状态 |
| 按键在低功耗模式下失效 |
低功耗模式关闭了 GPIO 时钟 |
在进入休眠前切换到中断模式 |
| 编译后代码太大 |
未使用 -Os 优化或链接了不需要的模块 |
在 Makefile 中加 -Os,确认未启用的 *_c 文件未被编译 |
10. 快速参考表
10.1 平台对比
| 平台 |
GPIO 读取 |
时间源 |
中断支持 |
难度 |
| STM32 HAL |
HAL_GPIO_ReadPin() |
HAL_GetTick() |
EXTI |
★☆☆ |
| 51 单片机 |
直接读 P1 寄存器 |
定时器 T0 中断 |
外中断 INT0/1 |
★★☆ |
| ESP32 |
gpio_get_level() |
xTaskGetTickCount() |
GPIO 中断 |
★★☆ |
| Linux |
/dev/input/event* |
clock_gettime() |
epoll |
★★★ |
| 裸机 |
直接操作寄存器 |
SysTick |
EXTI |
★★☆ |
10.2 最小改动量
| 平台 |
需要修改的代码行数 |
主要工作 |
| STM32 |
~50 行 |
GPIO 配置 + 3 个回调函数 |
| 51 单片机 |
~40 行 |
定时器初始化 + 3 个回调 |
| ESP32 |
~60 行 |
GPIO 中断配置 + FreeRTOS 任务 |
| Linux |
~80 行 |
select 事件循环 + input 解析 |
| 裸机 |
~30 行 |
寄存器映射 + 定时器驱动 |
本文档为 keyflow 多平台移植指南,所有代码均为参考实现。实际项目中请根据具体芯片手册和电路原理图调整寄存器配置和引脚映射。
项目仓库
免责声明
本文内容仅作为技术研究与学习交流 之用,不构成任何形式的产品设计建议、电子工程建议或商业推荐。文中涉及的代码片段、状态机模型、消抖策略等技术方案,基于特定嵌入式场景与硬件条件设计,直接用于生产环境前请务必进行充分的测试与验证。
使用本文内容所导致的任何直接或间接后果(包括但不限于设备损坏、数据丢失、商业损失等),作者及 AZE-BlackCore 不承担任何责任。 请根据你的实际项目需求,结合硬件手册、行业规范与最佳实践进行独立判断和决策。
版权声明:本文版权归 AZE-BlackCore 所有,转载请注明出处。封面与示意图由 AI 生成,仅供示意参考。