1. 项目背景与核心目标
去年用STM32F103C8T6开发板做的恐龙小游戏项目收获了不少关注,这次趁着春节假期,我又基于相同的工程模板开发了一款功能更完整的智能手表系统。选择延续之前的工程框架有两个主要原因:一是可以验证这套模板在多任务复杂场景下的可靠性,二是通过实际项目积累FreeRTOS在资源受限MCU上的实战经验。
这个智能手表项目实现了以下核心功能模块:
- 多应用切换(时钟、日历、温湿度监测、游戏等)
- 旋转编码器+按键组合交互
- OLED多任务刷新管理
- 低功耗运行模式
在开发过程中,我重点解决了三个关键技术问题:
- 通过任务通知机制实现高效的事件路由
- 采用互斥量保护共享硬件资源
- 优化内存分配策略应对多任务需求
2. 系统架构设计
2.1 硬件平台选型
主控芯片依然选用STM32F103C8T6(Cortex-M3内核),主要外设包括:
- 0.96寸OLED(SSD1306驱动,I2C接口)
- 旋转编码器(EC11,带按键功能)
- 四个独立按键(轻触开关)
- DHT11温湿度传感器
- DS1302实时时钟模块
选择这些元件主要基于以下考量:
- 总成本控制在50元以内
- 所有器件支持3.3V供电
- 接口不冲突(I2C、GPIO等)
- 社区资源丰富便于调试
2.2 软件架构设计
系统采用分层架构设计:
code复制应用层
├── 时钟任务
├── 日历任务
├── 温湿度监测
└── 恐龙游戏
中间件层
├── 按键路由服务
├── 显示管理服务
└── 电源管理
硬件抽象层
├── OLED驱动
├── 编码器驱动
└── 传感器驱动
FreeRTOS任务划分如下:
- KeyRouterTask(优先级3):处理所有输入事件
- DisplayManager(优先级2):协调屏幕刷新
- AppTasks(优先级1):各个应用任务
- IdleTask(优先级0):处理低功耗模式
3. 关键技术实现
3.1 输入事件路由系统
3.1.1 传统方案的缺陷
在之前的恐龙游戏项目中,我采用全局变量传递按键事件,这种方式存在明显问题:
- 强耦合:驱动层需要了解应用层逻辑
- 可维护性差:新增功能需要修改多处代码
- 资源竞争:多个任务可能同时修改标志位
3.1.2 基于任务通知的改进方案
FreeRTOS的任务通知机制提供了轻量级的任务间通信方式,相比消息队列有以下优势:
- 不需要创建额外的通信对象
- 每个任务自带通知存储空间
- 支持32位的事件位图管理
具体实现步骤:
- 定义事件枚举类型:
c复制typedef enum {
KEY_EVT_NONE = 0,
KEY_EVT_K1_PRESS,
KEY_EVT_K1_LONG_PRESS,
KEY_EVT_ENC_KEY_PRESS,
KEY_EVT_ENC_LEFT,
KEY_EVT_ENC_RIGHT
} KeyEvent_t;
- 创建路由任务核心逻辑:
c复制void KeyRouterTask(void *params) {
uint32_t notification_bits;
for (;;) {
xTaskNotifyWait(0, 0xFFFFFFFF, ¬ification_bits, portMAX_DELAY);
if (notification_bits & BIT_ENC_LEFT) {
Dispatch(KEY_EVT_ENC_LEFT);
}
// 其他事件处理...
}
}
- 前台任务注册机制:
c复制static KeyEventHandler_t s_handler = NULL;
static void *s_user_ctx = NULL;
void KeyRouter_SetActiveHandler(KeyEventHandler_t h, void *ctx) {
taskENTER_CRITICAL();
s_handler = h;
s_user_ctx = ctx;
taskEXIT_CRITICAL();
}
3.1.3 防抖与长按检测优化
针对机械按键的抖动问题,我实现了双重防抖机制:
- 硬件防抖:10ms采样间隔
- 软件防抖:状态稳定检测
长按检测采用状态机实现:
c复制uint32_t press_start = HAL_GetTick();
while (K1_IsPressedRaw()) {
if (HAL_GetTick() - press_start > LONG_PRESS_MS) {
Dispatch(KEY_EVT_K1_LONG_PRESS);
break;
}
vTaskDelay(pdMS_TO_TICKS(1));
}
if (!K1_IsPressedRaw()) {
Dispatch(KEY_EVT_K1_PRESS);
}
3.2 显示资源管理
3.2.1 I2C总线冲突问题
当多个任务同时调用OLED刷新时,会出现以下现象:
- 屏幕显示乱码
- 系统死锁
- I2C总线挂起
原因分析:
- SSD1306不支持多主机模式
- I2C时序被任务切换打断
- 显示缓冲区数据竞争
3.2.2 互斥量解决方案
采用优先级继承互斥量保护I2C资源:
c复制SemaphoreHandle_t xI2CMutex;
void OLED_Init() {
xI2CMutex = xSemaphoreCreateMutex();
configASSERT(xI2CMutex != NULL);
}
void OLED_Refresh() {
if (xSemaphoreTake(xI2CMutex, pdMS_TO_TICKS(100)) == pdTRUE) {
// 实际刷新操作
xSemaphoreGive(xI2CMutex);
}
}
关键优化点:
- 将互斥量封装在驱动内部
- 设置合理的等待超时
- 使用configASSERT确保创建成功
3.2.3 局部刷新优化
传统全屏刷新方案(128x64分辨率)需要传输1024字节数据,改进后的局部刷新:
c复制void OLED_RefreshArea(uint8_t x, uint8_t y, uint8_t w, uint8_t h) {
uint8_t start_page = y / 8;
uint8_t end_page = (y + h - 1) / 8;
for (uint8_t page = start_page; page <= end_page; page++) {
OLED_SetPageAddr(page);
OLED_SetColAddr(x);
I2C_Write(&oled_buffer[page][x], w);
}
}
实测数据:
- 全屏刷新:12.8ms
- 局部刷新(40x40区域):1.2ms
3.3 内存管理策略
3.3.1 动态分配的问题
在添加恐龙游戏任务时遇到系统崩溃,经排查发现:
- 默认堆空间(configTOTAL_HEAP_SIZE)不足
- 动态分配导致内存碎片
- 栈溢出破坏其他内存区域
3.3.2 静态分配实现
改用静态分配方案:
c复制#define GAME_TASK_STACK_SIZE 512
StackType_t xGameStack[GAME_TASK_STACK_SIZE];
StaticTask_t xGameTaskTCB;
void GameTask_Create() {
xTaskCreateStatic(GameTask_Main,
"Game",
GAME_TASK_STACK_SIZE,
NULL,
2,
xGameStack,
&xGameTaskTCB);
}
优势对比:
| 特性 | 动态分配 | 静态分配 |
|---|---|---|
| 内存来源 | Heap区 | .bss/.data区 |
| 编译期检查 | 无 | 有 |
| 碎片问题 | 严重 | 无 |
| 创建失败处理 | 需要额外判断 | 编译时报错 |
| 调试难度 | 困难 | 容易 |
3.3.3 混合分配策略
最终采用折中方案:
- 核心任务使用静态分配
- 临时对象使用动态分配
- 通过链接脚本控制内存布局
4. 传感器驱动优化
4.1 DHT11通信问题
初始实现遇到的问题:
- 单任务测试正常
- 多任务环境下读取失败
- 数据校验错误率高
原因分析:
- 单总线协议时序严格(微秒级)
- 任务切换导致时序中断
- 没有总线占用管理
4.2 临界区保护方案
改进后的驱动实现:
c复制int DHT11_Read(int *hum, int *temp) {
taskENTER_CRITICAL();
// 启动信号
DHT11_Start();
// 等待应答
if (DHT11_Wait_Ack() != 0) {
taskEXIT_CRITICAL();
return -1;
}
// 读取数据字节
uint8_t data[5];
for (int i = 0; i < 5; i++) {
data[i] = DHT11_ReadByte();
}
taskEXIT_CRITICAL();
// 校验和处理...
}
关键改进点:
- 整个通信过程处于临界区
- 增加超时检测
- 优化GPIO操作时序
4.3 性能优化对比
优化前后指标:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 读取成功率 | 35% | 98% |
| 平均耗时 | 8ms | 5ms |
| CPU占用率 | 高 | 低 |
5. 开发经验总结
5.1 关键收获
-
任务通知的灵活应用:
- 事件位图适合管理多种输入事件
- 比消息队列更节省内存
- 需要注意32位限制
-
资源保护策略:
- 互斥量适合保护硬件资源
- 临界区适合保护时序关键代码
- 优先级继承避免优先级反转
-
内存管理心得:
- 静态分配提高可靠性
- 混合策略平衡灵活性
- 链接脚本控制内存布局
5.2 典型问题排查记录
问题1:游戏任务频繁崩溃
- 现象:进入游戏后随机死机
- 排查:
- 检查栈使用情况(uxTaskGetStackHighWaterMark)
- 发现栈空间不足
- 分析内存映射(map文件)
- 解决:改用静态分配并优化栈大小
问题2:OLED显示残影
- 现象:切换应用时屏幕残留前画面
- 排查:
- 检查刷新逻辑
- 发现未清空显示缓冲区
- 验证互斥量保护范围
- 解决:增加切换时的全屏清空操作
5.3 后续优化方向
-
低功耗优化:
- 动态调整CPU频率
- 外设电源管理
- 任务休眠策略
-
图形界面升级:
- 引入GUI库
- 增加动画效果
- 改进字体渲染
-
无线功能扩展:
- 蓝牙连接
- 数据同步
- 远程控制
这个项目让我深刻体会到,嵌入式开发不仅是写代码,更需要建立系统级的思维。每个设计决策都需要考虑资源限制、实时性和可维护性的平衡。通过实际踩坑解决问题,对FreeRTOS的理解也从文档层面上升到了实践认知。
