STM32F103 打造 可存储式闹钟系统
1. 为什么选择STM32F103做可存储式闹钟STM32F103系列芯片在嵌入式领域被称为国民MCU价格亲民但性能强悍。我当年第一次接触这个芯片时就被它丰富的外设资源惊艳到了。对于闹钟系统来说它内置的RTC实时时钟模块和FLASH存储功能简直就是量身定制。RTC模块自带电池供电引脚即使主电源断开也能持续计时。实测下来配合普通的CR2032纽扣电池可以维持时钟运行长达3年以上。这个特性完美解决了传统闹钟断电后需要重新设置时间的问题。我拆解过市面上几款电子闹钟发现它们大多采用DS1302这类外置RTC芯片而STM32F103直接把这项功能集成在了片内。FLASH存储更是关键所在。普通闹钟每次上电都要重新设置闹铃时间而用STM32的片上FLASH可以保存用户设定的闹钟参数。我做过测试在FLASH的指定地址写入数据后即使断电半年再上电数据依然完好无损。这里有个细节要注意STM32的FLASH写入前需要先擦除整个扇区所以最好把闹钟数据集中存放在同一个扇区内。2. 硬件设计要点解析2.1 最小系统搭建用STM32F103做项目最方便的就是它的最小系统非常简单。我通常会用正点原子的精英版开发板做原型核心只需要8MHz晶振提供主时钟32.768kHz晶振给RTC提供时钟源复位电路10k电阻104电容启动模式选择电路两个10k电阻电源部分要特别注意RTC模块需要单独供电。我的做法是在VBAT引脚接一个纽扣电池座同时通过二极管与主电源隔离。这样当主电源断开时电池会自动接管RTC供电。实测中遇到过电池反接烧毁芯片的情况所以建议在电池座旁边标注正负极。2.2 外设接口设计显示部分推荐使用LCD1602或OLED这两种屏幕我都用过个人更倾向OLED因为它不需要背光电路而且显示效果更清晰。按键设计有讲究我建议至少包含功能键模式切换加/减键数值调整确认键蜂鸣器电路要注意驱动能力。STM32的IO口直接驱动无源蜂鸣器声音很小我通常会用三极管放大驱动信号。曾经有个项目因为蜂鸣器声音太小被客户投诉后来改用S8050三极管驱动音量立刻提升了三倍。3. RTC模块深度配置3.1 时钟源选择STM32F103的RTC支持三种时钟源LSE低速外部晶振最精准误差约±5ppmLSI低速内部RC约±500ppm不需要外接晶振HSE分频不推荐功耗高我强烈建议使用32.768kHz晶振虽然要多焊两个负载电容但精度绝对值得。曾经偷懒用了LSI结果一天能差出几十秒完全达不到闹钟的要求。配置代码很简单RCC_LSEConfig(RCC_LSE_ON); // 开启LSE while(!RCC_GetFlagStatus(RCC_FLAG_LSERDY)); // 等待就绪 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); // 选择时钟源 RCC_RTCCLKCmd(ENABLE); // 使能RTC RTC_WaitForSynchro(); // 等待同步3.2 日历功能实现STM32的RTC本质上是个计数器需要我们自己实现日历转换。正点原子提供的日历转换函数就很好用我稍作修改后用了很多年typedef struct { u16 year; u8 month; u8 day; u8 hour; u8 minute; u8 second; u8 week; } Calendar; void RTC_GetDateTime(Calendar *cal) { u32 counter RTC_GetCounter(); u32 days counter / 86400; cal-hour (counter % 86400) / 3600; cal-minute (counter % 3600) / 60; cal-second counter % 60; // 日期计算略... }有个坑要注意RTC计数器是32位的大约136年后会溢出。不过对闹钟应用来说完全够用了谁会用同一个闹钟上百年呢4. FLASH存储关键技术4.1 存储结构设计STM32F103的FLASH按页组织不同容量型号的页大小不同。以64KB的型号为例每页有1KB空间。我通常这样规划存储结构0x08010000开始存储闹钟时间2字节小时2字节分钟0x08010004开始存储用户设置铃声模式等一定要记住FLASH写入前必须先擦除整个页我曾经因为忘记擦除导致数据写入失败调试了整整一天。擦除操作很简单FLASH_Unlock(); // 解锁FLASH FLASH_ErasePage(0x08010000); // 擦除指定页 FLASH_Lock(); // 重新上锁4.2 数据可靠性保障FLASH有写入次数限制约1万次频繁写入会缩短芯片寿命。我的解决方案是只在用户修改设置时写入采用写入-校验-重试机制添加CRC校验这是我在多个项目中验证过的可靠写法void FLASH_WriteAlarm(u8 hour, u8 minute) { u16 data[2] {hour, minute}; u32 addr 0x08010000; FLASH_Unlock(); FLASH_ErasePage(addr); for(int i0; i2; i) { FLASH_ProgramHalfWord(addr i*2, data[i]); } FLASH_Lock(); // 校验 u16 read_hour *(u16*)addr; u16 read_min *(u16*)(addr2); if(read_hour ! hour || read_min ! minute) { // 写入失败处理 } }5. 闹钟逻辑实现技巧5.1 时间比对算法闹钟触发的核心是比较当前时间和设定时间。我最初用笨办法逐个字段比较if(now.hour alarm.hour now.minute alarm.minute) { RingAlarm(); }后来发现这种方法在跨天比较时会出问题。改进后的算法考虑了各种边界情况u32 TimeToMinutes(u8 h, u8 m) { return h * 60 m; } void CheckAlarm() { static u32 lastAlarmTime 0; u32 now TimeToMinutes(now.hour, now.minute); u32 alarmTime TimeToMinutes(alarm.hour, alarm.minute); if(now alarmTime lastAlarmTime ! alarmTime) { RingAlarm(); } lastAlarmTime now; }5.2 防误触设计实际使用中经常遇到按键误触问题。我的解决方案是按键消抖硬件软件长按/短按区分操作确认机制这是经过实战检验的按键处理代码u8 KeyScan() { static u8 keyState 0; if(KEY0 0) { delay_ms(10); // 消抖 if(KEY0 0) { if(keyState 0) { keyState 1; return KEY_SHORT; } else if(keyState 100) { keyState; } else { keyState 101; return KEY_LONG; } } } else { keyState 0; } return KEY_NONE; }6. 低功耗优化方案6.1 睡眠模式应用闹钟大部分时间处于等待状态完全可以用睡眠模式省电。STM32F103支持多种低功耗模式我推荐使用停止模式Stop mode功耗可降至20μA左右void EnterSleep() { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI); // 唤醒后需要重新配置时钟 SystemInit(); }唤醒源可以配置为RTC闹钟或外部按键中断。实测使用800mAh的锂电池可以连续工作3个月以上。6.2 外设电源管理不用的外设要及时关闭时钟RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, DISABLE);显示设备也要优化LCD启用局部刷新OLED使用休眠命令LED指示灯改为闪烁模式我曾经通过优化外设管理将整机功耗从5mA降到了0.8mA效果非常明显。7. 常见问题解决方案7.1 RTC走时不准可能原因晶振负载电容不匹配PCB布局不合理温度影响解决方法用示波器测32.768kHz波形调整负载电容通常6-12pF远离发热源7.2 FLASH写入失败典型症状数据校验错误程序跑飞排查步骤检查地址是否对齐确认写入前已擦除验证供电稳定检查写保护位7.3 闹钟误触发可能原因按键抖动软件逻辑缺陷电源干扰改进方法增加防抖算法添加状态机控制优化电源滤波电路记得有一次客户反映闹钟半夜自己响后来发现是电源纹波导致误触发在VDD和GND之间加了个100μF电容就解决了。