STM32C8T6驱动RTC、双传感器与OLED中文字库实战解析 简介基于STM32F103C8T6单片机实现的多功能万年历工程覆盖OLED显示、实时时钟、闰年自动判断、温湿度采集等完整链路适合电子设计课程设计、嵌入式入门及项目二次开发。工程包含RTC时间校准、DS18B20温度读取、DHT11湿度读取、OLED汉字与数字显示等模块代码结构清晰关键外设驱动均有独立c/h文件组织。从主函数初始化到外设时序均有注释便于理解RTC配置、单总线通信和OLED取模显示逻辑附带的原理接线图可快速搭建实物docx设计文档也可作为课设报告参考。资源包共88个文件以.h头文件与.c源码文件为主另有.s启动文件、Keil工程文件uvprojx/uvoptx、原理接线图pdsprj及设计说明文档整体压缩后仅1.77MB便于快速下载与部署。目前已有581人学习适合需要完整参考实现、电路连接说明和可直接编译烧录代码的单片机开发者。1. 一块C8T6同时跑RTC、双传感器与OLED中文字库先想清楚资源账拿到这个工程时我第一反应是看它怎么把三件事塞进一颗64KB Flash的芯片里RTC走秒、DS18B20测温、DHT11测湿度外加一块128x64的OLED刷中文界面。STM32F103C8T6的RAM只有20KBOLED显存本身要1KB中文字库如果按16x16点阵存一个汉字32字节界面上的每个字都要从Flash里抠。这套代码能跑起来关键不在于某个外设用得多花哨而在于它把标准外设库、BKP寄存器、单总线时序和屏幕刷新安排得各归其位。工程里能看到的文件结构也印证了这一点HARDWARE目录下dht11.c、ds18b20.c、rtc.c、oled.c、beep.c、led.c全部独立成模块main.c只做初始化和主循环调度标准外设库FWLib单独放一层。这种分层对课设或者自己做小项目都合适——调传感器时不用翻RTC代码改显示时不会碰时序。下面按我拆这套代码的路径先从RTC这块硬骨头入手。2. RTC后备域、秒中断与闰年自动调整的实现细节2.1 RTC时钟源选择与BKP寄存器的用途STM32F103的RTC不算一个完整独立的RTC它的核心是一个可编程的分频器和32位计数器时钟源可以是LSE外部32.768kHz晶振、LSI内部约40kHz或HSE分频。工程里用的是LSE这也是绝大多数RTC应用的标准做法。选择原因很直接32.768kHz刚好是2的15次方分频到1Hz只需要设置15位分频系数精度由晶振决定走时偏差可以控制在ppm级。LSI的精度太差温漂也大只能用来做唤醒定时器这类对时间不敏感的场景。BKP寄存器在这套设计里承担了掉电记忆功能。RTC的计数器值在VDD断电后只要VBAT引脚还接着电池或电容就能靠备份域电源维持走时。C8T6有10个16位的BKP寄存器工程里用它来标记RTC是否已经初始化过。判断逻辑通常是这样的if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { RTC_Configuration(); // 首次上电做完整RTC配置 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 写入初始化标志 } else { RTC_WaitForSynchro(); // 已初始化等待RTC时钟同步 }这段代码在工程中对应RTC_Init()函数。BKP_ReadBackupRegister读取的是备份数据寄存器DR10xA5A5这个魔数只是自定义标志位你也可以换成任意值只要保证和写入时一致。RTC_WaitForSynchro()必须在每次读RTC寄存器前调用作用是等待RTC的APB接口与RTC时钟域同步否则读到的可能是旧数据或者中间态数据。注意一个前提访问BKP和RTC寄存器前必须先打开PWR和BKP外设时钟并且使能后备区域访问权限RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE);2.2 秒中断里怎么合法地改时间RTC配置完成后系统会走RTC中断。工程里把RTC中断配置为秒中断也就是每秒触发一次在中断服务函数里更新时间显示。配置的核心代码如下RTC_ITConfig(RTC_IT_SEC, ENABLE); // 使能秒中断 RTC_WaitForLastTask(); // 等待上一次写操作完成 NVIC_InitStructure.NVIC_IRQChannel RTC_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure);RTC_ITConfig使能秒中断后每次RTC计数器递增到设定值产生更新事件就会进入RTC_IRQHandler。中断里做的第一件事是判断中断标志位void RTC_IRQHandler(void) { if (RTC_GetITStatus(RTC_IT_SEC) ! RESET) { RTC_ClearITPendingBit(RTC_IT_SEC); TimeDisplay_Flag 1; // 置位标志主循环刷新 } }这里有一个容易踩的坑清标志位一定要注意先后顺序。要先读中断状态寄存器确认确实是秒中断再清挂起位。不清挂起位会导致中断反复进入程序卡死在中断服务函数里而如果不判断直接清又可能把别的RTC中断标志误清掉。置位TimeDisplay_Flag而不是直接在中断里刷屏是为了减少中断占用时间——OLED走SPI一屏数据要几百微秒放在中断里会严重影响其他外设的时序。2.2.1 主程序判断更新时间与年月日换算RTC计数器存储的是从2000年1月1日0时0分0秒开始经过的秒数要把这个值转换成年-月-日-时-分-秒需要做除法取余和月份天数累加。工程里在main.c中用RTC_Get()读取计数器值然后调用Convert函数换算。换算逻辑分两个步骤先算日数day RTC_Get() / 86400; // 86400 24小时 * 60分 * 60秒这里注意一旦算出day大于365就要进入跨年判断。工程里闰年逻辑放在月份天数表里处理。判断闰年的标准写法是if ((year % 4 0 year % 100 ! 0) || year % 400 0) { month_day[1] 29; // 2月29天 } else { month_day[1] 28; }这个判断放在while(1)主循环里每次换算时间前先执行一次把2月的天数按当前年份更新。因为年份是动态变化的所以不能把month_day数组写成const常量否则到了闰年2月28日之后会直接跳到3月1日白白丢掉2月29日。这种按月表映射的做法比直接用公式计算一年中的第几天更直观调试时也更容易看明白时间推进路径。RTC本身不感知星期。代码里的星期是通过2000年1月1日是星期六这个基准往回推算的公式为week (day 5) % 7; // 2000-01-01是周六对应值为5这个推算每次从基准日累计不需要外部干预就算时间跨过多年也能正确显示星期。3. DS18B20单总线时序与DHT11数据帧解析的配套写法3.1 DS18B20的复位、读ROM与温度转换DS18B20用的是单总线通信一根数据线既传时钟又传数据所有时序都以微秒级延时为基础。工程里把它接到某个普通GPIO口开漏输出加外部上拉。初始化时的复位脉冲是这套时序里最关键的环节代码逻辑如下u8 DS18B20_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_OD; // 开漏输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); DS18B20_Reset(); // 复位脉冲 return DS18B20_Check(); // 等待应答 }GPIO_Mode_Out_OD开漏模式是单总线的必须条件。开漏输出下引脚只能拉低拉高靠外部上拉电阻。如果不设置成开漏而用手推挽输出当器件拉低总线时引脚和器件同时输出高电平会形成瞬间短路表现为通信不稳定温度值跳变。复位应答之后读取温度需要三个命令跳过ROM0xCC、启动温度转换0x44、读暂存器0xBE。工程里的DS18B20_ReadTemp()把这三步串起来short DS18B20_ReadTemp(void) { u8 tempL 0, tempH 0; short temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // Skip ROM只挂一个设备时用 DS18B20_WriteByte(0x44); // 启动温度转换 delay_ms(750); // 12位精度转换时间典型值750ms DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器从byte0开始 tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); temp (tempH 8) | tempL; return temp; }返回的temp是有符号16位数低4位是小数部分。工程main.c里为了显示方便直接取了整数部分temperature DS18B20_Get_Temp();再除以16去掉小数位。如果你要显示小数就要保留低4位换算成0.0625的倍数。这里有个取舍12位分辨率下转换时间最长750ms所以主循环里不能频繁调用ReadTemp()否则界面刷新会明显卡顿。常见的折衷方案是每2秒采一次温度中间穿插DHT11读取和OLED刷新。3.2 DHT11的起始信号、位时序与校验法DHT11和DS18B20虽然都叫单总线但时序规格完全不一样。DHT11的通信由主机主动发起先拉低总线至少18ms作为起始信号然后释放总线等待从机应答。应答信号持续80us低电平加80us高电平之后是40bit数据。工程里的DHT11_Init和读取函数细节如下u8 DHT11_ReadData(u8 *temp, u8 *humi) { u8 buf[5]; u8 i; DHT11_Start(); // 主机起始信号 if (DHT11_Check_Response() 0) return 1; // 无应答返回错误 for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); // 连续读5字节 } if ((buf[0] buf[1] buf[2] buf[3]) buf[4]) { *humi buf[0]; *temp buf[2]; return 0; } return 1; }校验位是DHT11相对DS18B20一个很友好的设计。前4字节分别是湿度整数、湿度小数、温度整数、温度小数第5字节是前四字节之和的低8位。注意buf[1]和buf[3]这两位小数值在DHT11这个型号上恒为0所以实际显示时只取buf[0]和buf[2]。工程里的返回值设计成0为成功、1为失败主循环里要根据返回值决定要不要更新显示避免把错误数据刷到OLED上。一个重要的时序细节是DHT11的数据位读取用循环检测电平跳变的方式而不是固定延时。读每一位时先等待总线变为低电平再等待高电平开始然后计时高电平持续时间。高电平26~28us代表逻辑070us左右代表逻辑1。工程里的DHT11_ReadByte就是这个思路用两个while循环卡边沿再用delay_us测量高电平宽度。调试时如果发现湿度永远显示0%优先用示波器看起始信号后的应答脉冲是否存在而不是怀疑传感器坏了。两个传感器同时挂在一颗MCU上时GPIO配置有冲突需要注意DS18B20要求开漏输出DHT11建议也用开漏两者可以共用同一种GPIO初始化模式但绝对不能在初始化DS18B20时把DHT11的引脚重配置成推挽输出。工程里DS18B20接PA1、DHT11接PA2这类相邻引脚配置时各管各的互不干扰。4. OLED显示层的组织中文字库、数值刷新与界面布局4.1 0.96英寸OLED的SPI初始化和显存刷新工程用的OLED是0.96寸、128x64分辨率、SSD1306控制器、4线SPI接口DC、CS、SCLK、SDIN外加RST。初始化序列比较固定核心是关闭显示、设置显示时钟分频、设置电荷泵、设置对比度、打开显示这几个步骤。工程里OLED_Init()函数前几行如下OLED_WR_Byte(0xAE, OLED_CMD); // 关闭显示 OLED_WR_Byte(0xD5, OLED_CMD); // 设置显示时钟分频/振荡器频率 OLED_WR_Byte(0x80, OLED_CMD); OLED_WR_Byte(0xA8, OLED_CMD); // 设置多路复用比 OLED_WR_Byte(0x3F, OLED_CMD); // 64行 OLED_WR_Byte(0xD3, OLED_CMD); // 设置显示偏移 OLED_WR_Byte(0x00, OLED_CMD); OLED_WR_Byte(0x8D, OLED_CMD); // 电荷泵 OLED_WR_Byte(0x14, OLED_CMD); // 开启电荷泵0x10为关闭OLED_WR_Byte有两个参数第二个参数区分这是命令还是数据。这个函数内部把DC引脚拉低写命令、拉高写数据。SPI时钟频率我习惯设置在2MHz以下SSD1306虽然支持更高频率但C8T6的SPI1如果开到18MHz加上杜邦线较长时容易误码画面会出现随机雪花点。工程里的OLED_Clear和OLED_ShowString都直接操作内部的OLED_GRAM[8][128]缓冲区修改完统一调用OLED_Refresh()一次性刷入屏幕这样能避免反复写SPI造成的闪烁。注意OLED_ColorTurn(0)和OLED_DisplayTurn(0)这两个函数它们在初始化后立即调用。前者控制显示颜色正常或反色后者控制屏幕是否翻转180度。这两个函数通过修改SSD1306的0xA0段重映射和0xC0扫描方向寄存器实现不需要改显示缓冲区内容。如果你把OLED焊反了方向用OLED_DisplayTurn(1)就行不用重新裁PCB。4.2 字模的生成、存储与智能万年历界面摆放界面上要显示智能万年历这五个汉字每个字在16x16点阵下占32字节。字模数据放在oledfont.h里格式是数组。取模方法是用取模软件如PCtoLCD2002设置阴码、逐行式、十六进制输出生成后按数组顺序拷贝进工程。核心写法OLED_ShowChinese(8, 0, 0, 16); // 第一个参数x坐标第二个y坐标 OLED_ShowChinese(33, 0, 1, 16); // 第三个参数是字库中的索引这个函数的存储结构是每个汉字按16行x2字节存放先高字节后低字节。坐标为(8, 0)表示从屏幕第8列、第0页SSD1306把64行分成8页每页8行开始显示。字模数组里的索引顺序要和界面设计一致0对应智、1对应能以此类推。界面布局上工程做了这样的安排第一行显示智能万年历第二行显示日期YYYY-MM-DD第三行显示时间HH:MM:SS第四行显示温湿度。每次刷新先清空对应区域再写入新数据而不是刷新全屏。因为OLED是页面寻址写第二行时只需把页地址设到第二页即可。OLED_ShowNum(15, 2, year, 4, 16); // 在(15, 2)位置显示4位年份 OLED_ShowChar(55, 2, -, 16); OLED_ShowNum(63, 2, month, 2, 16); // 月份补零显示OLED_ShowNum的最后一个参数16是字号OLED_ShowChar显示ASCII字符。数字和日期里的分隔符都按字符处理。这里有个显示上容易犯的毛病月份或日期是一位数时比如3月OLED_ShowNum(63, 2, 3, 2, 16)会自动显示03还是3取决于函数内部有没有补零处理。工程里的实现是直接按位数取模输出所以需要你在更新前手动判断if (month 10) OLED_ShowChar(6316, 2, 0, 16);先把十位补零再显示个位否则日期会左移一位视觉上跳动。温度刷新同理DS18B20读取的原始值是short类型除以16得到实际温度。显示时先判断正负号温度低于0度时要在数值前补一个-字符。0度以上时补空格这样能避免正负号残留导致数字错位。DHT11的温度值是无符号u8类型显示逻辑简单很多但要注意它只能测0~50度范围超出后还是要以DS18B20的数据为准。5. 烧录前必须检查的三个坑位JTAG引脚、VBAT与传感器上拉5.1 ST-Link烧不进程序时先看引脚复用C8T6的PA13、PA14、PA15、PB3、PB4在系统复位后默认是JTAG/SWD调试引脚。如果你把DS18B20或按键接在这几个脚上代码初始化GPIO时抢占了调试口ST-Link就会突然连不上芯片报No target connected。工程里如果传感器用了PA15或PB3、PB4务必要在GPIO初始化前调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);这一行把JTAG功能关掉只保留SWD的PA13和PA14两个脚PB3、PB4、PA15就能当普通GPIO用。调试时先烧一版干净程序不带引脚初始化确保连接正常再把外设代码加回去。5.2 VBAT不接电池RTC上电默认2000年1月1日C8T6的VBAT引脚如果不接任何电源RTC会在系统完全断电后丢失时间重新上电复位到2000年1月1日0点。工程里的BKP标志位0xA5A5写在预留的BKP_DR1里如果VBAT没接BKP寄存器内容掉电即丢每次上电都会重新走RTC_Configuration流程。如果只是做课设演示不想外接电池还有个通用技巧在main.c里检测到初始化标志不存在时不写死在程序里而是用串口接收上位机下发的时间值初始化RTC。毕竟程序里写死RTC_Set(2025, 1, 1, 12, 0, 0)每次上电都回到这个时间不具备实际意义。工程里main.c当前是直接调用RTC_Init()通过BKP判断是否初始化所以只要VBAT接一个1.5V以上的电池或大电容时间就能持续走。5.3 让DS18B20与DHT11同时挂在同一条总线上要避免的时序冲突代码里DS18B20和DHT11分别接了不同GPIO这是稳妥做法。如果后续你想让两个传感器共用同一条数据线就要注意DHT11的起始信号要求主机拉低至少18ms而DS18B20在复位脉冲时只要求拉低480us。这个时间尺度差了几十倍共用一条总线时必须把两者初始化顺序错开并且在读取DHT11数据期间关掉DS18B20的转换任务否则DS18B20的温度转换会因总线被长时间占用而失败。实际验证方法很简单。程序烧录后OLED上同时显示三组数据日期时间、DS18B20温度、DHT11湿度。用手捏住DS18B20温度应该在1秒内开始爬升松开后缓慢回落。对着DHT11哈气湿度数值应该立刻上升10%以上。日期上翻越过2月底时注意观察是否从2月28日直接跳到3月1日——如果跳了说明闰年判断条件没生效检查month_day[1]是否在年份更新时被重新赋值。时间走准与否拿手机秒表对一下连续走24小时误差不超过3秒属于LSE晶振正常工作范围超过10秒就要考虑晶振负载电容是否匹配。本文还有配套的精品资源点击获取