在嵌入式开发领域,基于STM32的智能手表项目一直是个经典而实用的练手项目。最近我在重构一个开源手表项目时,对主程序框架和关键外设初始化有了些新体会。这个项目主要涉及STM32F103C8T6最小系统板、0.96寸OLED屏幕和RTC时钟模块,通过合理的主函数设计和定时器配置,实现了时间显示、菜单切换等基础功能。
提示:STM32F1系列虽然性能不如新款,但其丰富的外设和成熟的生态,依然是入门嵌入式开发的绝佳选择。
主函数作为程序入口,需要统筹全局资源初始化并启动各功能模块。而Timer的精准配置则直接影响系统节拍和显示刷新率,OLED_Clear()这样的基础函数虽然简单,但在实际使用中却有不少优化空间。下面我就从这三个关键点展开,分享一些你可能在官方例程里看不到的实战经验。
主函数的第一要务是完成芯片基础配置。不同于简单的"点亮LED"项目,智能手表需要更严谨的初始化顺序:
c复制int main(void) {
// 阶段1:关键外设初始化
HAL_Init(); // 必须最先执行
SystemClock_Config(); // 时钟树配置
MX_GPIO_Init();
MX_TIM2_Init(); // 基础定时器
MX_I2C1_Init(); // OLED通信接口
// 阶段2:应用层初始化
OLED_Init();
RTC_Init();
Button_Init();
// 阶段3:主循环
while (1) {
UI_Update();
Power_Manage();
}
}
这里容易踩的坑是忽略初始化顺序依赖。比如我曾遇到过I2C初始化在GPIO之前导致通信失败的情况。经过示波器抓取波形才发现,SCL/SDA线未被正确配置为复用模式。
对于没有RTOS的裸机系统,主循环的任务调度需要特别注意:
实测发现,当主循环中加入以下优化后,系统响应明显改善:
c复制void UI_Update(void) {
static uint32_t lastTick = 0;
if (HAL_GetTick() - lastTick >= 20) { // 50Hz刷新
Display_Process();
lastTick = HAL_GetTick();
}
}
使用TIM2作为系统时基时,CubeMX生成的代码通常需要调整。以1ms中断为例:
c复制void MX_TIM2_Init(void) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 7200 - 1; // 72MHz/7200=10kHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 10 - 1; // 10kHz/10=1kHz(1ms)
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
HAL_TIM_Base_Start_IT(&htim2);
}
注意:APB1总线时钟默认是72MHz的一半,但TIM2的时钟会倍频回72MHz,这点在手册中容易忽略。
当系统中存在多个中断源时,合理的优先级设置至关重要:
| 中断源 | 抢占优先级 | 子优先级 | 说明 |
|---|---|---|---|
| SysTick | 0 | 0 | 系统心跳最高优先级 |
| TIM2 | 1 | 0 | 时基中断 |
| EXTI | 2 | 0 | 按键中断 |
| I2C | 3 | 0 | 通信中断 |
通过NVIC_SetPriority()函数调整后,按键响应延迟从原来的15ms降低到了5ms以内。
常见的OLED_Clear()有三种实现方式:
c复制// 方法1:逐点清屏(最慢)
void OLED_Clear(void) {
for(uint8_t i=0;i<8;i++)
for(uint8_t j=0;j<128;j++)
OLED_WR_Byte(0x00,OLED_DATA);
}
// 方法2:页模式清屏(推荐)
void OLED_Clear(void) {
uint8_t zero[128] = {0};
for(uint8_t i=0;i<8;i++) {
OLED_SetPos(0,i);
OLED_WriteData(zero,128);
}
}
// 方法3:DMA传输(最快但实现复杂)
实测数据对比(128x64屏幕):
| 方法 | 执行时间(72MHz) | 适用场景 |
|---|---|---|
| 逐点 | 12.8ms | 不推荐 |
| 页模式 | 1.2ms | 通用方案 |
| DMA | 0.3ms | 高性能需求 |
采用双缓冲机制可以显著减少闪烁:
c复制uint8_t oled_buf[2][8][128]; // 双缓冲
uint8_t buf_index = 0;
void OLED_Refresh(void) {
buf_index ^= 1; // 切换缓冲
OLED_DisplayBuf(oled_buf[buf_index^1]);
// 新内容写入oled_buf[buf_index]
}
配合定时器中断实现60Hz刷新率时,CPU占用率从35%降到了12%。
现象:1秒实际耗时1.2秒
排查步骤:
典型故障处理流程:
通过以下措施可将待机电流从15mA降到3mA:
完成基础功能后,可以考虑以下扩展:
在移植lvgl等GUI库时,需要注意:
这个项目最让我意外的是,原本以为简单的定时器配置,在实际应用中竟有如此多细节需要考虑。比如当系统需要同时处理按键、显示刷新和传感器数据时,中断优先级的细微差别就会导致完全不同的用户体验。