资讯动态

STM32F103ZET6万年历项目实战:RTC走时、农历算法与TFT局部刷新

发布时间:2026/10/30 3:07:55 来源:尧图企业网站定制
简介这是一份面向STM32初学者与嵌入式课程实践者的万年历项目资料以STM32F103ZET6最小系统为主控通过片内RTC实现日期与时间计时并借助7针SPI接口驱动OLED显示。项目重点在于理解RTC计数器读写、时间日期换算及修改计数器重设当前时间的方法适合用作课程设计、毕业设计原型或外设综合练习。压缩包共205个文件、约5.27MB包含C/H源码、Keil工程与配置、HEX烧录文件、MAP/LST映射列表、SCT分散加载文件及编译中间文件可据此还原完整工程、查看资源占用并复现烧录调试过程。现有1203人学习下载便于对照源码梳理OLED显示、RTC初始化与最小系统搭建思路并积累外设驱动与工程配置经验。1. 抽屉里那块 ZET6除了点灯还能干点正经事F103ZET6 大概是很多人手上第一块 Cortex-M3 的板子跑完按键点灯、串口打印之后就卡在那儿了不知道下一步该拿它练什么。做一块 stm32 万年历是我见过性价比最高的选择一块 TFT 彩屏、一颗 32.768kHz 晶振、两颗按键屏幕上要同时显示公历、农历、星期、节气、温度按键能进设置态改时间、设闹钟拔掉电源再插上日期还在。这个工程把 GPIO、FSMC、RTC、定时器、I2C、SPI、单总线全部串在一起体量不大但每个环节都能较真。它适合刚学完外设想找一个完整项目练手的人也适合想给板子加个桌面时钟、需要一份能直接拿来改的代码的人。下面按我拆包的实际顺序讲。2. 工程骨架与硬件资源ZET6 上跑一个万年历到底要占哪些外设拿到压缩包第一件事不是编译是先看清楚它把什么东西塞进了 512KB Flash 和 64KB SRAM。万年历这个应用看着简单真正吃资源的是字库和界面缓存而不是逻辑代码。先把目录和引脚捋清楚后面调试的时候才知道该往哪儿看。2.1 目录结构与模块划分这套工程用的是标准外设库那一套分层方式不是 CubeMX 自动生成的树。分层思路很朴素底层驱动、板级硬件、业务逻辑三层业务层不直接碰寄存器硬件层不关心农历怎么算。目录关键文件职责USERmain.c、stm32f10x_it.c、system_stm32f10x.c启动流程、主循环、中断向量SYSTEMdelay.c、sys.c、usart.cSysTick 延时、串口重定向到 printfHARDWARElcd.c、key.c、rtc.c、timer.c、24c02.c、w25qxx.c、ds18b20.c板级外设驱动一个外设一对文件APPcalendar.c、ui.c、alarm.c公历转农历、界面刷新、闹钟判断FONTfont.h、ascii.h8x16 ASCII 与 16x16 汉字点阵数组calendar.c是唯一一个纯算法文件里面没有任何寄存器操作只吃结构体吐结构体。这一点在看代码时挺重要因为农历算错了你只需要盯着这一个文件不用怀疑是不是屏幕刷错了。ui.c则是唯一允许调用LCD_*系列函数的地方其他模块想显示东西都得通过它转发不然你改个界面要翻遍整个工程。字库放在FONT目录下是最省事的做法直接编译进内部 Flash。代价是font.h动辄几百 KB如果还想放图片就爆了。所以更常见的做法是ASCII 点阵留在内部 Flash只有几 KB汉字点阵烧到板载 SPI Flash 里上电时按需读取。这套工程两种都支持w25qxx.c就是给后者准备的。2.2 引脚与外设分配ZET6 是 144 脚封装引脚够多但 FSMC 一旦用上就会占掉一大片分配的时候要先把 FSMC 钉死剩下的再分给别的外设。下面是这套工程实际用到的映射不同板子可能略有出入改的时候对着原理图核一遍。外设引脚说明TFT 数据线 D0~D15PD14/PD15/PD0/PD1、PE7~PE15FSMC 数据总线16 位并口TFT 片选 CSPD7FSMC_NE1映射到 0x6C000000TFT 命令/数据 RSPD11FSMC_A16靠地址线区分命令和数据TFT 写/读PD5 / PD4FSMC_NWE / FSMC_NOETFT 背光PB0普通推挽输出也可以走定时器 PWM 调光TFT 复位PG15普通 GPIO上电拉低再拉高按键 KEY0~KEY3PE2~PE5上拉输入低电平有效蜂鸣器PB8三极管驱动高电平响EEPROM 24C02PB6 / PB7I2C1存闹钟和用户设置SPI Flash W25Q64PB12~PB15SPI2存汉字点阵与图片温度 DS18B20PA1单总线需要 4.7k 上拉调试串口PA9 / PA10USART1115200RTC 晶振PC14 / PC15LSE32.768kHz后备电源VBAT纽扣电池走时靠它有两个地方容易翻车。第一PB6/PB7 做 I2C1 的同时如果板子把它们引到了别的外设上I2C 就废了得改用软件模拟。第二FSMC 用了 PD 和 PE 大半个端口如果你还想在这两个口上接别的中断源先查复用表别等焊完了才发现冲突。2.3 上电初始化顺序main.c里的初始化顺序不是随便排的顺序错了会出现各种莫名其妙的症状比如屏幕亮但全是雪花、串口打印不出来、时间每次上电都被重置。这套工程按下面这个次序走。int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 2 位抢占 2 位子优先级 */ delay_init(); /* SysTick 配成 1ms后面所有延时都靠它 */ uart_init(115200); /* 先开串口后面调试全靠它输出 */ LED_Init(); KEY_Init(); Buzzer_Init(); I2C_Init(); /* 24C02读出上次保存的闹钟和亮度 */ W25QXX_Init(); /* SPI Flash字库要从这里读 */ LCD_Init(); /* FSMC 屏初始化序列最耗时的一步 */ DS18B20_Init(); RTC_Init(); /* 内部 RTC依赖 LSE 起振 */ RTC_Get_Calendar(g_now); Calendar_Lunar(g_now, g_lunar); UI_Init(); /* 画静态部分边框、标题、固定文字 */ UI_RefreshAll(g_now, g_lunar); while (1) { Key_Scan(); /* 非阻塞扫描内部做消抖 */ App_Task(); /* 秒标志置位后刷新时间、查闹钟 */ } }这段代码的要点在顺序本身。delay_init()必须排在最前面因为后面LCD_Init()里全是delay_ms()而delay_ms()依赖 SysTick 配置完成。串口排在显示前面是我的习惯屏还没点亮的时候你至少能从串口看到走到哪一步了不用靠猜。RTC_Init()放在LCD_Init()之后是有意为之。LSE 起振有时候要几百毫秒甚至更久如果放在前面用户看到的就是一段时间的黑屏会以为板子坏了。放在屏初始化之后至少屏幕先亮起来然后时间再跳出来。当然代价是屏幕刚亮的时候时间栏是空的需要UI_RefreshAll()补一次全刷。UI_Init()和UI_RefreshAll()分开也是有讲究的。前者只画那些永远不变的东西边框、标签文字、农历这两个字后者画会变的值。如果你的界面在秒跳的时候整屏闪一下八成是把这两步合成一步了。3. 走时与显示链路RTC 秒中断怎么把时间送到屏幕上万年历的心脏是 RTC。这一块如果没搞明白后面所有功能都是空中楼阁。F103 的 RTC 和 F4 完全不是一个东西F4 有独立的 BCD 日历寄存器读出来直接就是年月日时分秒F103 只有一个 32 位的秒计数器年月日全靠软件换算。这个差异决定了整个工程的写法。3.1 RTC 与备份域让时间在断电后活下来F103 的 RTC 挂在备份域上由 VBAT 供电主电源断了它还在跑。但备份域默认是锁着的不解锁你写什么都被忽略而且不会报错——这是新手最容易卡住的地方。/* 返回 0 正常1 首次上电已配置2 LSE 起振失败 */ uint8_t RTC_Init(void) { uint32_t timeout 0; RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); /* 解锁备份域不写这一行后面全白搭 */ if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { /* 备份寄存器被清空说明是第一次上电或电池掉过 */ RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET) { if (timeout 0xFFFFF) { /* 别写死循环起振失败要能退出来 */ return 2; } } RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); /* 等 RSF 置位寄存器同步完成 */ RTC_WaitForLastTask(); RTC_SetPrescaler(32767); /* 32768 / (32767 1) 1Hz */ RTC_WaitForLastTask(); RTC_SetCounter(0); /* 后续由 UI 层写入真实时间 */ RTC_WaitForLastTask(); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); /* 打标记下次上电跳过配置 */ } else { RTC_WaitForSynchro(); RTC_WaitForLastTask(); } return 0; }BKP_DR1里那个0xA5A5是整个掉电保持的关键。没有它每次上电都会重新配置 RTC 并把计数器清零表现出来就是电池装了但时间还是归零。这个标记位只要 VBAT 有电就不会丢所以换主电池不影响只有拔掉纽扣电池才会失效。RTC_SetPrescaler(32767)这个参数也值得说一句。预分频寄存器写的是 32767 而不是 32768因为硬件是计数值 预分频 1。写成 32768 的话时钟会慢一半一天差出好几个小时而且这种错误很难第一眼发现因为秒还是在跳只是跳得慢。3.2 秒中断里只做一件事RTC 配好之后让它每秒产生一次中断中断里绝对不能干重活。原因很直接FSMC 刷一个 16x16 汉字要写 32 次总线一次全屏刷新几十毫秒如果在中断里刷屏主循环里的按键扫描和串口处理全被堵死表现出来就是按键没反应、屏幕一顿一顿。/* RTC 全局中断服务函数stm32f10x_it.c 里 */ void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_SEC) ! RESET) { RTC_ClearITPendingBit(RTC_IT_SEC); RTC_WaitForLastTask(); /* 清中断后必须等写操作完成 */ g_sec_flag 1; /* 只置一个标志立即退出 */ } }中断里就三行有效代码剩下的全交给主循环。主循环那边的处理长这样void App_Task(void) { if (g_sec_flag 0) return; g_sec_flag 0; RTC_Get_Calendar(g_now); /* 秒计数 - 年月日时分秒 */ Calendar_Lunar(g_now, g_lunar); /* 公历 - 农历、节气 */ UI_Refresh_Time(g_now); /* 只刷变化的数字 */ UI_Refresh_Lunar(g_lunar); /* 农历一天只变一次内部有判断 */ if (Alarm_Check(g_now)) { /* 闹钟匹配 */ Buzzer_Beep(3); } }UI_Refresh_Lunar()内部会比对上一次的农历日期只有跨天才重绘。农历那一行有十几二十个汉字每秒刷一次的话屏幕闪得没法看而且完全没有必要。同理温度的刷新周期是 2 秒一次跟秒中断解耦用独立的计数变量控制。3.3 TFT 局部刷新与字库取模界面刷新这块能用局部刷新就别整屏重绘。日期 2025-06-08 一共 10 个字符一秒里真正变的只有最后两位秒数其他八个字符一动不动全刷就是纯浪费。/* 两位数字的局部刷新值没变直接返回变了先擦后写 */ void UI_ShowNum_Update(uint16_t x, uint16_t y, uint8_t oldv, uint8_t newv, uint16_t color, uint16_t back) { if (oldv newv) return; LCD_Fill(x, y, x 16 - 1, y 24 - 1, back); /* 按 16x24 字符宽度擦除 */ LCD_ShowNum(x, y, newv, 2, 24, color); }这个函数看着简单但有两个参数必须严格对齐擦除矩形的宽度要等于两个数字的实际像素宽高度要等于字模高度。宽度算少了会残留上一帧的笔画算多了会把旁边的分隔符一起擦掉。我一般的做法是把字模尺寸定义成宏擦除和绘制都引用同一个宏改字号的时候只改一处。字库这块ASCII 用 8x16 或者 16x24 的点阵数组直接const在 Flash 里。汉字用 16x16 宋体取模取模软件的设置要选逐行式、高位在前跟LCD_ShowChinese()里的移位逻辑对上不然显示出来是乱码或者阴阳字。/* 汉字点阵存放在外部 SPI Flashindex 是字在字库里的序号 */ void LCD_ShowChinese(uint16_t x, uint16_t y, uint8_t index, uint16_t color, uint16_t back) { uint8_t buf[32]; /* 16x16 点阵 32 字节 */ W25QXX_Read(buf, FONT_BASE_ADDR index * 32, 32); for (uint8_t row 0; row 16; row) { uint8_t hi buf[row * 2]; uint8_t lo buf[row * 2 1]; for (uint8_t col 0; col 16; col) { /* 高位在前col 8 取 hi否则取 lo */ uint8_t bit (col 8) ? ((hi (7 - col)) 1) : ((lo (15 - col)) 1); LCD_DrawPoint(x col, y row, bit ? color : back); } } }FONT_BASE_ADDR是字库在 SPI Flash 里的起始地址一般是 0 或者某个扇区边界烧录字库的时候要跟这个地址对上。index是这个字在字库里的第几个不是 GB2312 编码所以需要一个查找表或者区位码换算。用区位码换算的好处是不用维护映射表坏处是字库里必须按 GB2312 顺序排满文件会大不少。逐点绘制 16x16 的汉字要 256 次LCD_DrawPoint()每个点又是一次 FSMC 写速度其实不快。界面上汉字不多的话感觉不出来真要刷一整行就得改成开窗加连续写把LCD_DrawPoint换成一次设置窗口再一次灌 32 字节颜色数据能快好几倍。4. 日历算法公历转农历、节气推算与节假日标注硬件跑通了剩下的就是算法。公历转农历这块网上的代码一大堆但能直接用、边界不出错的并不多。核心是一个两百来号元素的十六进制数组每个 bit 都有含义写错一位就可能在某一年某一个月差一天而且这种 bug 往往几个月才暴露一次。4.1 农历查表法的数据结构农历没法用公式算因为它是观测加人为规定的只能查表。表的结构是从 1900 年到 2100 年每年用一个 32 位整数编码低 4 位是闰哪个月0 表示不闰第 5 到 16 位是 12 个月的大小月1 为大月 30 天第 17 位是闰月的天数。/* 1900 ~ 2100每年一个编码 */ static const uint32_t lunarInfo[201] { 0x04bd8, 0x04ae0, 0x0a570, 0x054d5, 0x0d260, 0x0d950, 0x16554, /* ... 中间省略完整表格 201 项 ... */ 0x0d520 }; /* 这一年闰几月返回 0 表示不闰 */ static uint8_t Lunar_LeapMonth(uint16_t y) { return (uint8_t)(lunarInfo[y - 1900] 0x0f); } /* 这一年闰月有多少天没有闰月返回 0 */ static uint8_t Lunar_LeapDays(uint16_t y) { if (Lunar_LeapMonth(y) 0) return 0; return (lunarInfo[y - 1900] 0x10000) ? 30 : 29; } /* 这一年第 m 月1~12有多少天不含闰月 */ static uint8_t Lunar_MonthDays(uint16_t y, uint8_t m) { return (lunarInfo[y - 1900] (0x10000 m)) ? 30 : 29; } /* 这一农历年总共多少天闰月要额外加 */ static uint16_t Lunar_YearDays(uint16_t y) { uint16_t sum 348; /* 12 个月按 29 天算的底数 */ for (uint8_t i 0x8000; i 0x8; i 1) { if (lunarInfo[y - 1900] i) sum; /* 每个大月补 1 天 */ } return sum Lunar_LeapDays(y); }基准点必须记牢1900 年 1 月 31 日是农历 1900 年正月初一。所有换算都是先算公历日期距这一天的天数再一年一年往下减。int Solar2Lunar(const solar_t *s, lunar_t *l) { if (s-year 1900 || s-year 2100) return -1; /* 超出表范围 */ uint32_t offset DaysFrom1900(s-year, s-month, s-day); uint16_t i, yearDays 0; for (i 1900; i 2101 offset 0; i) { yearDays Lunar_YearDays(i); offset - yearDays; } if (offset 0) { offset yearDays; i--; } /* 减多了退回来一年 */ l-year i; uint8_t leap Lunar_LeapMonth(i); uint8_t isLeap 0; for (uint8_t m 1; m 12 offset 0; m) { /* 到闰月的位置了先处理闰月 */ if (leap 0 m leap 1 isLeap 0) { --m; isLeap 1; yearDays Lunar_LeapDays(i); } else { yearDays Lunar_MonthDays(i, m); } offset - yearDays; if (offset 0) { offset yearDays; break; } } l-month ...; /* 由循环里的 m 和 isLeap 决定 */ l-day offset 1; l-is_leap isLeap; return 0; }这段循环的坑在闰月那一段。--m配合isLeap标志的作用是减完闰月之后m要回到原来的月份继续往下走否则后面所有月份都会错位。我第一次写的时候漏了--m结果 2023 年闰二月之后的所有日期全部错一个月测了好几天才定位到。4.2 节气用通用公式还是查表节气有两种做法查表和公式。查表是把 1900 到 2100 每年 24 个节气的日期全部存下来一共 4824 个字节塞进 Flash 毫无压力绝对不会错。公式是通用算法代码短但个别年份要打补丁。/* 21 世纪 24 节气的 C 值顺序小寒、大寒、立春……冬至 */ static const float sTermC[24] { 5.4055f, 20.12f, 3.87f, 18.73f, 5.63f, 20.646f, 4.81f, 20.1f, 5.52f, 21.04f, 5.678f, 21.37f, 7.108f, 22.83f, 7.5f, 23.13f, 7.646f, 23.042f, 8.318f, 23.438f, 7.438f, 22.36f, 7.18f, 21.94f }; /* 返回某年第 idx 个节气0 起是几号Y 取年份后两位 */ uint8_t SolarTerm_Day(uint16_t year, uint8_t idx) { uint8_t y year % 100; float v y * 0.2422f sTermC[idx] - (y / 4); return (uint8_t)(int)v; }公式的原理是节气在回归年上的位置基本固定每年推进 0.2422 天闰年补回来一天。这个算法在多数年份准确但存在少数例外比如 2026 年的小寒、2084 年的大雪都需要 1 天修正。项目里如果想省事就加一张例外表/* 少数年份需要微调的按 {年份, 节气序号, 修正值} 存 */ static const int8_t sTermFix[][3] { { 2026, 0, 1 }, { 2084, 20, 1 }, /* ... */ };我的一般选择是如果界面只显示当前节气和下一个节气用公式加例外表足够了如果要做节气日历一天不差地列出来那就老老实实查表四千多字节换一份永远不会错的数据很划算。4.3 星期、闰年与界面上的今天星期用蔡勒公式或者基姆拉尔森公式都行一行就能算出来。要注意的是 C 语言里负数取模和数学上的取模不是一回事公式算出来的中间结果可能是负的。/* 返回 0周日1周一 …… 6周六 */ uint8_t Weekday(uint16_t y, uint8_t m, uint8_t d) { if (m 3) { m 12; y - 1; } /* 1、2 月按上一年的 13、14 月算 */ uint16_t c y / 100; uint16_t yy y % 100; int16_t w (c / 4) - (2 * c) yy (yy / 4) (13 * (m 1) / 5) d - 1; w % 7; if (w 0) w 7; /* 处理负数别省这一步 */ return (uint8_t)w; }闰年判断就一行(y % 4 0 y % 100 ! 0) || (y % 400 0)。看着简单但凡是涉及到2 月 29 日的界面都要特别注意尤其是闹钟功能如果用户在 2 月 29 日设了一个每天响的闹钟到了平年这天就没有 2 月 29 日逻辑里要决定是跳过还是顺延到 3 月 1 日。这套工程的做法是跳过界面上加一行小字提示比默默顺延用户体验好。5. 避坑排查不走时、花屏、农历错一天这 5 个现场问题下面这几条是我在实际调试中反复遇到的按出现频率排。每一条都写清楚现象、原因和改法遇到类似症状可以直接对照着查。5.1 装上电池了一断电时间还是归零现象拔掉 USB 再插上屏幕上显示的还是 2000-01-01 00:00:00说明 RTC 被重新配置了。原因有三种按概率排一是 VBAT 引脚根本没接电池或者电池座虚焊万用表量一下 VBAT 对地电压正常应该在 3V 左右二是BKP_DR1的标记没写进去或者写进去了但读的时候没解锁备份域三是备份域被意外复位过比如代码里调用了RCC_DeInit()或者上电时序里电源跌落导致备份域掉电。解决先量电压再看代码。PWR_BackupAccessCmd(ENABLE)必须在读写 BKP 之前调用而且 RTC 配置完之后要确保RTC_WaitForLastTask()返回了再写标记位否则写操作可能被后面的复位打断。PWR_BackupAccessCmd(ENABLE); if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { /* 只有确实没有标记时才重配 RTC */ }还有一种隐蔽情况板子上 VBAT 和 3.3V 之间有个二极管方向焊反了主电源一断 VBAT 也跟着掉。这种只能对着原理图查。5.2 时间跳秒时屏幕闪一下按键也跟着卡现象每到整秒屏幕上的秒数更新时会有一瞬间的白闪同时按按键没那么灵敏了偶尔要按两下才有反应。原因秒中断里直接调用了LCD_ShowNum()甚至UI_RefreshAll()。FSMC 写屏是阻塞的刷一个汉字要几十条总线操作在中断里干这个主循环的按键扫描被拖慢消抖逻辑的计数就对不上了。解决中断里只置标志位所有刷屏动作放到主循环。这一点前面 3.2 已经说了但实际改的时候还要注意一个细节如果主循环里有其他地方耗时很长比如读 DS18B20 要 750ms 转换时间秒标志会堆积。所以g_sec_flag用uint8_t置位而不是递增主循环处理完清一次丢一两帧也不影响显示。5.3 农历日期总是差一天或者闰月年份全乱现象公历显示 6 月 8 日农历显示五月初四但手机上一查是五月初三差一天。原因基准日算错了。DaysFrom1900()里如果按1900 年 1 月 1 日作为起点而表里的数据是按1900 年 1 月 31 日编的就会整体差 30 天表现出来就是差一天因为农历月有 29/30 天30 天的偏移正好错开一个月。另一种情况是闰月处理漏了--m导致闰月之后所有月份错位。解决写一个自检函数把 1900 到 2100 年每一年的春节日期跟已知数据对一遍全部对得上再往下做。我一般的习惯是在calendar.c里留一个Calendar_SelfTest()上电时用串口跑一次对不上就打印出错的年份。提示农历表的最后一位2100 年很容易被截断lunarInfo[200]一定要有值否则 2100 年会读到数组外面的野数据症状是那一年显示成乱七八糟的日期。5.4 屏幕花屏、白屏或者颜色反了现象上电后屏幕全白或者显示内容但颜色是反的该红的地方是蓝再或者内容只在半个屏幕上。原因基本都在 FSMC 配置和地址映射上。A16 接 RS 的情况下命令地址是0x6C000000数据地址是0x6C01FFFE注意是FFFE不是0000——因为 16 位数据宽度下地址线是右移一位对齐的A16 对应到 HADDR 的 bit17所以偏移是0x20000再减 2 得到0x1FFFE。写成0x6C020000的话写数据的时候实际是在写命令屏幕就不动了。#define LCD_REG (*(volatile uint16_t *)0x6C000000) /* RS 0写命令 */ #define LCD_RAM (*(volatile uint16_t *)0x6C01FFFE) /* RS 1写数据 */颜色反了通常是初始化序列里的 MADCTL 或者像素格式设置不对RGB 和 BGR 反了改一个寄存器就行。花屏则多半是 FSMC 时序太快把FSMC_DataSetupTime从 0x03 加到 0x05 试试宁可慢一点也别花屏。5.5 RTC 每天快几秒或者慢几秒现象刚对完时跑一天下来差个三五秒跑一周差半分钟。原因32.768kHz 晶振的负载电容匹配不好或者晶振本身精度不够。开发板上配的通常是 6pF 或者 12.5pF 的晶振电容焊错了频率就偏一天差几秒很正常。解决分两层。硬件层面换精度更高的晶振或者用可调电容调软件层面可以做补偿统计一段时间的偏差然后在 RTC 计数器上做周期性加减。/* 每 N 秒补偿一次慢了就加 1 秒快了就减 1 秒N 由实测偏差算出 */ void RTC_Drift_Compensate(void) { static uint16_t cnt 0; if (cnt DRIFT_PERIOD) return; /* 比如 86400一天补一次 */ cnt 0; if (g_drift_dir 0) { RTC_SetCounter(RTC_GetCounter() 1); /* 慢补一秒 */ } else if (g_drift_dir 0) { RTC_SetCounter(RTC_GetCounter() - 1); /* 快扣一秒 */ } RTC_WaitForLastTask(); }这个补偿是玄学改命治标不治本一天差超过 10 秒还是老老实实换晶振。另外补偿不要在秒中断里做RTC_SetCounter有写保护流程放主循环里更稳。6. 二次开发换屏、加传感器、改界面的改动顺序这套工程最常被改的三个地方是换屏幕、加温度显示、重做界面。改动顺序搞反了会很痛苦我的习惯是从不影响现有功能的地方开始动。换屏最先改的是分辨率宏和 FSMC 地址。如果新屏还是 ILI9341 兼容的只是尺寸从 2.8 寸变成 3.5 寸那就只改LCD_W和LCD_H两个宏再检查一遍界面坐标有没有越界。如果换了驱动 IC初始化序列要整个换掉但LCD_DrawPoint、LCD_Fill、LCD_ShowChar这几个上层接口保持不动界面代码一行都不用改。这就是把驱动和界面分开的价值。加温度显示DS18B20 的驱动本身不长麻烦的是它的 750ms 转换时间不能阻塞主循环。做法是把它做成状态机发起转换、记时间戳、下次进来检查是否到时间、到时间再读。void Temp_Task(void) { static uint p a hrefhttps://download.csdn.net/download/weixin_50921275/20670763 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑