ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

基于STM32的LCD指针式电子钟与日历设计实战

基于STM32的LCD指针式电子钟与日历设计实战 简介这是一份基于STM32F103ZET6的指针式电子钟与万年历完整工程源码适合正在学习STM32裸机寄存器开发、希望综合运用RTC、LCD、Flash与传感器外设的嵌入式初学者。工程采用寄存器风格编写注释齐全可直接在正点原子战舰V3开发板与3.5寸触摸屏上编译运行。代码实现了指针式时钟、数字时钟、日历、温度显示并支持通过代码配置多组闹钟字库存放于W25Q128 Flash中涉及地址寻址与字库调用思路。压缩包共含96个文件容量5.53MB主要包含.c/.h源码、Keil工程文件.uvproj、编译产物.hex/.axf、字库.dzk及说明文档目录按SYSTEM、USER等模块划分便于快速定位。目前已有3706人学习下载适合作为综合项目练手、毕业设计参考或嵌入式课程配套实战。1. 基于STM32的LCD指针式电子钟为什么比数码管方案难一个数量级一块12864液晶屏放在桌面指针式表盘每秒跳一次日期和星期按大小月自动翻页——很多人在第2周就会把项目改成数码管显示因为指针式LCD电子钟最折腾人的地方不是“显示时间”而是“把秒针转动的角度变成像素”每个刻度、每根指针都要做坐标运算LCD刷新一慢表盘就会出现残影和撕裂再叠加闰年计算、按键调时状态机和RTC时钟漂移整个代码量比数码管方案多出三倍以上。这篇文章围绕“基于STM32设计的LCD指针式电子钟与日历项目”这个原型项目拆解其中的表盘绘制、日历算法、按键交互和校准方法。适合正在做STM32课程设计或毕业设计的同学也适合想把手头LCD屏从“显示字符”升级为“显示复杂图形”的嵌入式开发人员。下面每一节的代码都基于STM32F103C8T6和常用的SPI接口TFT屏换用其他型号只需要改引脚宏。2. LCD表盘绘制原理与STM32驱动选型2.1 指针、刻度和表盘的坐标变换LCD平面的坐标系与数学坐标系不同LCD屏幕原点在左上角x轴向右y轴向下。这意味着在画指针之前必须把“12点方向”定义为参考角度0度再按顺时针方向递增。表盘中心设为(cx, cy)指针末端坐标与角度的关系为x cx r * sin(angle) y cy - r * cos(angle)其中angle的单位是度取值0到360表示从12点方向顺时针转过的角度。c语言库函数sin和cos接收弧度因此角度换算弧度的操作在每次刷新时都会执行。下面给出一个可直接复用的画指针函数void LCD_Draw_Hand(uint16_t cx, uint16_t cy, uint16_t length, float angle_deg, uint16_t width, uint16_t color) { float rad angle_deg * 3.14159265f / 180.0f; int16_t x_end cx (int16_t)(length * sinf(rad)); int16_t y_end cy - (int16_t)(length * cosf(rad)); LCD_Draw_Line(cx, cy, x_end, y_end, width, color); }调用时需要传入圆心坐标、指针长度、当前角度、线宽和颜色。sinf与cosf在开启FPU的Cortex-M4上很快但F103没有硬件浮点频繁调用浮点函数会占用大量CPU时间。一个工程上常见的做法是预计算一个91元素的uint16_t查找表每度对应一个正弦值把sinf全部替换为查表访问。若表盘只有60个刻度加上时针分针秒针三条线每秒钟刷新一次F103在72MHz主频下用查找表方案实测整帧刷新时间可以稳定在40ms以内。不要在这个环节使用double类型STM32的软件浮点库处理double的开销比float大4到8倍在指针式表盘场景中没有任何收益。2.2 自带电阻触控的2.4寸TFT屏走SPI还是并口这类型LCD的核心控制芯片通常是ILI9341或ST7789二者都支持SPI和8080并口两种模式。很多同学买到“2.4寸屏”后直接插到开发板上能亮画线一多就出现花屏原因是SPI模式下的D/CX引脚接错或者驱动程序里的像素格式设置成了RGB444而面板是RGB565。表格列出几种常见驱动的关键差异方便选屏时判断该写哪种初始化序列控制芯片分辨率接口方式常见像素格式注意事项ILI9341240x320SPI / 8080RGB565/666SPI时钟最高约40MHz超过容易花屏ST7789240x320SPI4线RGB565/18bit初始化序列与9341不兼容不能直接套用ST7735128x160SPI4线RGB565/12bit小屏常见注意偏置电压寄存器差异SSD1289240x3208080并口RGB666新设计不建议选用占用引脚过多ILI9341的SPI模式使用CS、SCK、MOSI、MISO、D/CX和RESET六个引脚其中D/CX决定当前传输的是命令还是数据初始化时芯片默认识别为0xCF等命令因此必须先完整发送厂家推荐初始化序列再进入显存写模式。如果只是从其他项目里复制一份“能亮屏”的代码而不检查像素格式指针画出来会明显偏色在白色表盘上尤其明显。2.3 实心圆盘背景的填充顺序画表盘时如果先画边框再填充背景色ILI9341的显存写入是逐行扫描的大面积填充色块会占用较长传输时间。常见做法是先用LCD_Clear填充整个屏幕为表盘底色再画最外圈刻度然后画指针最后画中心圆点。这个顺序能保证即使某一帧因为按键中断被切走屏幕残留也不会出现“半根指针悬空”的视觉错误。刻度线的长度分两级整点刻度比非整点刻度长3到5像素颜色也更深。画刻度时给一个for循环步进6度用取余判断是否为整点for (uint8_t i 0; i 60; i) { float rad i * 6.0f * 3.14159265f / 180.0f; uint8_t is_major (i % 5 0); uint8_t len is_major ? 18 : 12; int16_t x1 cx (int16_t)((r - len) * sinf(rad)); int16_t y1 cy - (int16_t)((r - len) * cosf(rad)); int16_t x2 cx (int16_t)(r * sinf(rad)); int16_t y2 cy - (int16_t)(r * cosf(rad)); LCD_Draw_Line(x1, y1, x2, y2, 1, is_major ? 0x0000 : 0x8410); }这里0x0000是黑色0x8410是灰色。颜色值直接拆分成高低字节传输不调用任何颜色转换函数。每次刷新刻度线时只画本帧需要的部分不要每秒钟把60条刻度全部重画否则CPU大部分时间都消耗在读写LCD控制器上。3. 日历算法与RTC时间基准大小月、闰年与星期的计算3.1 用DS3231还是STM32内部RTC做电子钟项目首先要回答“时间基准从哪来”的问题。STM32F103内部有RTC模块一个32位计数器配合外部32.768kHz晶振即可工作功耗低代码也不复杂。但内部RTC有两个隐患一是F103的RTC没有独立的校准寄存器晶振频率偏差只能在应用层每秒修正一次二是VBAT引脚供电不当时主电源掉电后再上电RTC计数器可能跑飞。因此精度要求一般的桌面台钟可以用内部RTC但若日历需要长期走时准确更稳妥的方案是外接DS3231硬件RTC芯片。DS3231是I2C接口内置温度补偿晶振(TCXO)在-40到85摄氏度范围内年误差通常在2分钟以内。与STM32内部RTC相比它额外提供两个闹钟寄存器和32KHz方波输出这在后面的校准环节非常有用。两种方案的对比直接看表格对比项STM32内部RTCDS3231接口无额外引脚I2CSCL/SDA晶振频率32.768kHz外部晶振内置TCXO校准方式应用层秒级修正数字修调寄存器备份供电VBAT引脚VCC引脚接电池典型月误差10~30秒1~2秒I2C的驱动在STM32上可以直接复用硬件外设初始化时把SCL与SDA配置为开漏输出各接4.7kΩ上拉电阻。读写DS3231的代码量不大关键是寄存器地址不能写错秒寄存器地址是0x00依次是分、时、星期、日、月、年。每次读取时直接连续读7个字节然后逐字节做BCD码到十进制的转换。3.2 闰年判断与每月天数表日历模块必须处理平年闰年、大小月和星期自动修正。公历闰年规则是“四年一闰百年不闰四百年再闰”把这个规则写成C函数比很多同学用的“能被4整除就是闰年”要完整得多uint8_t is_leap_year(uint16_t year) { if ((year % 4 0 year % 100 ! 0) || (year % 400 0)) return 1; return 0; } uint8_t days_in_month(uint16_t year, uint8_t month) { uint8_t days[] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; if (month 2 is_leap_year(year)) return 29; return days[month - 1]; }days_in_month的入参month范围是1到12调用前必须确认从RTC读到的是十进制数而非BCD码。DS3231的日期寄存器默认返回BCD格式例如0x25表示25号转换成十进制需要执行day (bcd 4) * 10 (bcd 0x0F)。RTC芯片在跨月、跨年时不会自动校验2月29日是否合法若直接把非法日期写回寄存器芯片会接受并在运行一段时间后产生逻辑错误。星期部分不要依赖RTC硬件自带的星期寄存器。很多时候星期寄存器的初始值是芯片出厂写入的并不一定正确更可靠的方法是使用蔡勒公式从年月日推算星期。蔡勒公式在1582年10月15日之后的公历日期都有效代码实现如下uint8_t zeller_week(uint16_t year, uint8_t month, uint8_t day) { if (month 3) { month 12; year--; } uint16_t h (day 13 * (month 1) / 5 year year / 4 - year / 100 year / 400) % 7; // 蔡勒公式的返回值0星期六, 1星期日, ... 6星期五 uint8_t week[] {6, 0, 1, 2, 3, 4, 5}; return week[h]; }注意这个公式的返回值与常规习惯不同需要做一次映射。应用层在显示星期时通常直接显示“一二三四五六日”因此建议把映射后的结果再转成数组下标。每次RTC日期有变化时调用一次该函数即可不要在每秒刷新中断里重复计算。3.3 指针角度与日历的联动策略电子钟的日历不需要实时刷新到秒级常见的做法是每分钟检查一次RTC寄存器中的日期字段。如果发现日期与本地缓存值不同则重新调用zeller_week并更新LCD顶部的日期区域。这样可以避免在秒中断里执行I2C读操作因为I2C在中断里耗时较长可能会让指针刷新出现抖动。日期显示区域放在表盘下方用16x16点阵的汉字字库渲染“年 月 日 星期”或者直接使用英文缩写。LCD刷新时日期区域单独做一个局部窗口更新只清除日期所在行不清除整个屏幕。使用ILI9341的窗口命令CASET和RASET将LCD重绘范围限制在日期区域矩形内能显著降低每秒刷新的数据量。4. 按键调时、中断刷新与指针平滑运动4.1 秒针的离散跳跃与连续扫描模式差异模拟表盘有两种刷新方式一种是每秒只画一次针秒针直接跳到下一个位置另一种是让秒针在50ms内从当前角度平滑转到下一角度。后者视觉效果好但需要更高的帧率和三角函数计算量。F103在没有FPU的情况下处理平滑扫秒会导致表盘绘制速度下降因此多数低成本LCD指针钟采用第一种离散跳跃模式。每个秒针跳动角度为6度在6度范围内不做插值直接擦除旧指针并绘制新指针。为了让视觉上干净很多实现者会先调用LCD_FillRect把旧指针所在的矩形区域清成表盘底色再在新位置画指针。这种方式在小半径表盘上可行但大半径表盘上会留下扇形残影原因是矩形区域并不能完全覆盖斜线扫过的范围。更好的做法是记录上一帧指针的端点坐标只擦除从圆心到端点的旧直线再绘制新直线。void update_second_hand(RTC_TimeTypeDef *time) { float old_angle g_second_angle; float new_angle time-Seconds * 6.0f; if (old_angle ! new_angle) { LCD_Draw_Line(cx, cy, g_sec_end_x, g_sec_end_y, 3, 0xFFFF); LCD_Draw_Hand(cx, cy, SECOND_HAND_LEN, new_angle, 3, 0xF800); g_second_angle new_angle; g_sec_end_x cx (int16_t)(SECOND_HAND_LEN * sinf(new_angle * PI / 180.0f)); g_sec_end_y cy - (int16_t)(SECOND_HAND_LEN * cosf(new_angle * PI / 180.0f)); } }使用这种方案时如果上一帧指针与当前帧指针位置相差超过6度说明系统曾被中断卡住这时需要连续擦除并重画两个位置之间的所有角度否则表盘上会残留明显弧线。工程上可以加一个静态变量记录上次跳动的时间戳差值大于2秒则执行强制全清重绘。4.2 按键消抖与调时状态机调时功能需要一个状态机在“正常运行”和“校正”两个模式之间切换。常见的按键设计是三个按键模式切换键、数值加一键、数值减一键。长按加键可以实现连续快速增加。按键检测放在主循环中用定时器中断每隔5ms采样一次GPIO电平连续8次采样值一致才判定为有效按下。这个消抖方案不阻塞main循环也不会在按键按下期间影响指针刷新。调时状态机的实现要点是“当前的调节目标”用枚举类型表示typedef enum { TUNE_MODE_NONE 0, TUNE_MODE_HOUR, TUNE_MODE_MINUTE, TUNE_MODE_SECOND, TUNE_MODE_YEAR, TUNE_MODE_MONTH, TUNE_MODE_DAY } TuneModeEnum; TuneModeEnum g_tune_mode TUNE_MODE_NONE;按下模式键时g_tune_mode依序切换。在TUNE_MODE_HOUR状态下短按加键使小时字段加1并立即写入DS3231同时把新值显示在LCD上。关键细节是调节秒字段时写入RTC后应该把秒清零而不是使用当前值因为人工操作无法精确对齐到秒。调完所有字段后再次按下模式键状态机回到TUNE_MODE_NONE并将RTC的秒寄存器写0保证走时起点整齐。4.3 定时器中断刷新不能只挂在SysTick上很多初学者的指针钟直接在SysTick中断里做秒针刷新SysTick默认配置为1ms中断一次再在中断里累加计数并调用draw函数。这样做的风险是如果绘制的函数执行时间超过1msSysTick中断会被其他更高优先级中断打断或者中断嵌套时间过长导致时间基准漂移。推荐的架构是SysTick只维护一个毫秒计数器每秒产生一次“刷新请求”事件实际绘制工作放在main函数的while循环里完成。中断里只置一个标志位主循环检测到后将标志位清零再调用绘制函数。这样做的好处是无论绘制函数耗时多长时间基准都不会被绘图过程影响按键消抖也不会阻塞时间计数。5. 误差溯源与低功耗优化技巧5.1 用秒脉冲信号校准日误差走时误差的来源主要是晶振频率偏差。内部RTC方案下频率偏差可能达到30ppm折算下来每天误差2.6秒左右。DS3231由于内置TCXO出厂校准后通常维持在±2ppm也就是每天约0.17秒误差。但即便使用DS3231板上I2C上拉电阻的寄生电容和供电电压波动也会引入微小偏差。校准方法如下把DS3231的32KHz引脚连接到STM32的一个输入捕获通道同时用另一块标准时间源比如手机秒表记录1小时内捕获到的32KHz脉冲个数。理论上1小时应捕获到32,768 × 3600 117,964,800个脉冲如果实际少计或多了若干个就能反推晶振的实际频率偏差ppm。若偏差超过10ppm可以使用DS3231的数字修调寄存器(偏移地址0x10)进行调整该寄存器配置范围为-128到127每个LSB对应约0.1ppm的修调量。对大多数桌面电子钟不过度追求0.1ppm精度更实用的做法是连续记录7天的秒差在“年误差小于1分钟”的目标下调整修调寄存器然后重新标定零点。5.2 STOP模式下的RAM保持与唤醒策略如果这个电子钟打算用电池供电或使用USB供电但希望断电后继续计时外接DS3231配合一个纽扣电池即可。但如果需要使用STM32自身的低功耗模式需要知道F103的STOP模式支持RAM保持而且RTC若由外部低速时钟驱动可以继续运行。进入STOP模式前必须关闭LCD背光、停止SPI时钟、将未使用的GPIO设置为模拟输入以降低漏电流然后调用RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);此时系统时钟被关闭按键按下不会立即唤醒需要提前配置EXTI外部中断线并将按键对应的GPIO使能为唤醒源。唤醒之后要立即调用SystemInit重新配置时钟树再把LCD重新初始化和清屏。注意在STOP模式下DS3231的I2C通信会中断唤醒后需要重新读取时间并同步到显示层。一个实用的验证方法是断电前把当前时间写入DS3231接入纽扣电池后断开主供电等5分钟后再上电LCD显示的时间如果不连续说明备份电源回路没有接对。这个步骤能最快排查VBAT引脚与DS3231供电之间的问题。5.3 表盘颜色与背光明暗的肉眼校准最后提供一个调表盘配色的技巧。LCD屏的白色背景在低亮度背光下容易泛黄建议把表盘底色从纯白0xFFFF改为偏冷色调的0xDFFF或0xDF7F这样在长时间显示时视觉疲劳更小。指针颜色使用红色0xF800时对比度最好。如果背光使用的是PWM调光把PWM频率设为1kHz以上低于500Hz时肉眼能观察到明显闪烁尤其在秒针高速运动时产生流动感。在调光倍率上深夜模式可以降到10%亮度但此时LCD的响应时间会变长指针运动时会有拖影需要在代码里加上“低亮度模式每2秒刷新一次表盘”的降载策略。本文还有配套的精品资源点击获取
返回列表