
简介基于STM32F407与DS18B20构建的温度传感报警项目配套4针0.96寸OLED屏能够实时显示环境温度与日期并在温度超出设定范围时给出声光报警。代码围绕STM32F4系列编写模块划分清晰可移植性好既适合嵌入式初学者学习GPIO、定时器、RTC及传感器驱动也适合作为课程设计或小型温度监控方案的基础进行二次扩展。资源压缩包共210个文件约7.05MB以C源码51个.h与49个.c为核心包含Keil工程配置文件、编译生成的.o/.axf/.hex/.map等中间与最终产物另有清理脚本、备份文件和说明文本可从源码完整追踪到固件输出。目前已吸引2844人学习下载经多次测试运行稳定包内保留了可直接烧录的hex文件便于快速验证对照源码可深入理解DS18B20单总线时序、OLED显示驱动与RTC日期处理逻辑在此基础上修改报警阈值或增加继电器控制都很方便。1. 这套温度报警系统我为什么建议你拆开重新写一遍拿到这个工程文件的时候我先扫了一眼目录Template.uvguix、stm32f4xx_tim.c、stm32f4xx_rtc.c、keilkilll.bat标准外设库的工程骨架不是HAL库。这种“老菜”反而是好东西。DS18B20读取温度、0.96寸OLED显示、RTC日期刷新、蜂鸣器声光报警四个功能单独看都不难但真正把它们在一个主循环里稳定跑起来新手至少要踩三四个坑单总线时序被中断打断、OLED像素坐标算错、RTC备份域没使能导致时间初始化失败。适合谁看正在做STM32F407课程设计、想把传感器驱动搞扎实的嵌入式学生以及想快速拿一块F4开发板拼一个温度监测小仪表的工程师。这套代码的价值不在“能跑”而在于每个模块怎么从数据手册翻译成可执行的时序逻辑。2. DS18B20单总线时序与STM32F407 GPIO驱动2.1 单总线的物理层与GPIO模式切换DS18B20只用一根数据线既能传命令又能传数据。这根线空闲时被4.7kΩ电阻拉高到3.3V主机要发送数据就得把线拉低。所以STM32F407的GPIO必须工作在开漏输出模式这样才能实现“谁都能把线拉低”释放后自动回到高电平。常见的错误是用推挽输出一旦DS18B20拉低总线主机同时输出高电平轻则读数据全FF重则烧传感器。在F407上GPIO初始化时要设置开漏输出和50MHz速度。我用的是PD6作为DQ引脚可以直接改宏定义换成PB9或PA8。切换输入输出模式时需要重新调用GPIO_Init不能只改ODR寄存器因为F407的输入和输出配置在MODER寄存器里必须整个GPIO_Init配置一次。我封装了一个模式切换函数方便时序里反复切换。#define DS18B20_PORT GPIOD #define DS18B20_PIN GPIO_Pin_6 #define DS18B20_RCC RCC_AHB1Periph_GPIOD void DS18B20_Set_Pin_Mode(GPIOMode_TypeDef mode) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin DS18B20_PIN; gpio.GPIO_Mode mode; gpio.GPIO_Speed GPIO_Speed_50MHz; if (mode GPIO_Mode_OUT) { gpio.GPIO_OType GPIO_OType_OD; // 开漏输出必须配合外部上拉 gpio.GPIO_PuPd GPIO_PuPd_NOPULL; } else { gpio.GPIO_PuPd GPIO_PuPd_NOPULL; // 输入模式浮空输入 } GPIO_Init(DS18B20_PORT, gpio); }这个函数的核心在于输出模式下强制使用开漏输出输入模式下不使能内部上下拉。DS18B20的时序非常依赖外部上拉电阻STM32F407内部上拉太弱不建议依赖内部上拉。注意初始化时先使能GPIOD时钟否则GPIO_Init会卡在等待寄存器写入上。另外如果你的板子跟传感器之间用长杜邦线连接上拉电阻建议改成2.2kΩ否则边沿会变缓时序容限变小。2.2 复位、读时隙、写时隙的时序实现单总线时序压在微秒级任何中断插入都会导致通信失败所以我在读写字节的函数里直接用__disable_irq()关掉全局中断函数结束后再打开。有人担心关中断影响系统调度但整个读写流程最多几百微秒对报警系统来说完全可以接受。复位序列是主机拉低480μs释放后等待60~240μsDS18B20会拉低总线60~240μs作为存在脉冲。主机在释放后70μs左右采样读到低电平说明设备在线。注意采样点必须在60μs之后拉低脉冲结束之前否则会漏掉存在脉冲。uint8_t DS18B20_Reset(void) { uint8_t ret; __disable_irq(); DS18B20_Set_Pin_Mode(GPIO_Mode_OUT); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(480); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); DS18B20_Set_Pin_Mode(GPIO_Mode_IN); delay_us(70); ret GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN); delay_us(410); __enable_irq(); return (ret Bit_RESET) ? 1 : 0; }返回值1表示检测到设备0表示通信失败。这里有个容易忽略的细节释放总线后要先把GPIO切回输入模式再延时因为开漏模式下GPIO_SetBits只是把输出寄存器置1真正把线拉高需要靠外部上拉电阻如果上拉电阻没焊或焊错位置采样时读不到高电平复位就会失败。写时隙和读时隙的原理类似。写1时隙拉低总线2μs后释放DS18B20会在15~60μs内采样写0时隙拉低总线60μs以上再释放。读时隙主机拉低2μs释放在释放后10μs内采样因为DS18B20读到上升沿后会控制总线输出数据位数据保持到时隙结束。void DS18B20_Write_Bit(uint8_t bit) { DS18B20_Set_Pin_Mode(GPIO_Mode_OUT); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(2); if (bit) { GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); } delay_us(60); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); delay_us(2); } uint8_t DS18B20_Read_Bit(void) { uint8_t bit; DS18B20_Set_Pin_Mode(GPIO_Mode_OUT); GPIO_ResetBits(DS18B20_PORT, DS18B20_PIN); delay_us(2); GPIO_SetBits(DS18B20_PORT, DS18B20_PIN); DS18B20_Set_Pin_Mode(GPIO_Mode_IN); delay_us(10); bit GPIO_ReadInputDataBit(DS18B20_PORT, DS18B20_PIN); delay_us(45); return bit; }注意读时序的采样点是释放总线后10μs这个时间点必须在DS18B20拉低总线之后、时隙结束之前。DS18B20手册里写的读时隙最短60μs我的释放后延时45μs用来凑满时隙。如果在F407主频168MHz下delay_us的循环次数需要按实际编译优化等级校准推荐先用逻辑分析仪看波形再微调延时循环。2.3 跳过ROM命令与温度读取流程单总线上只有一颗DS18B20时使用跳过ROM命令0xCC免去匹配64位序列号。读取温度分为两步先发转换命令等待转换完成再发读暂存器命令。常用命令字如下命令指令码说明跳过ROM0xCC单设备时直接操作不发送序列号启动温度转换0x44转换结果存入暂存器第0/1字节读暂存器0xBE读取9字节暂存器温度数据在前两字节温度数据是16位有符号数低4位为小数部分12位分辨率下每个LSB代表0.0625℃。读取后要拼接高低字节负数在数据手册里用补码表示好在定义int16_t类型后直接相乘就能得到正确负温。float DS18B20_Get_Temperature(void) { uint8_t low, high; int16_t raw; DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0x44); delay_ms(750); // 12位分辨率最大转换时间 DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0xBE); low DS18B20_Read_Byte(); high DS18B20_Read_Byte(); raw (int16_t)((high 8) | low); return raw * 0.0625f; }这里的750ms延时是12位分辨率下的最大转换时间。如果你的系统要求刷新快过1Hz可以把DS18B20配置为9位分辨率转换时间只需要93.75ms代价是温度分辨率降到0.5℃。报警系统通常不需要这么高分辨率12位足够。写完温度读取函数后在main开头先调用一次DS18B20_Reset如果没有返回1直接显示传感器错误避免后续一直读到垃圾数据。3. 4针0.96寸OLED的I2C驱动与字符/汉字显示3.1 用模拟I2C还是硬件I2C网上关于STM32F407硬件I2C的讨论很多原厂标准库的硬件I2C确实存在总线锁死风险特别是在I2C速率较高或者从设备刚上电时。4针OLED模块用的是SSD1306控制器I2C接口的速率只有100kHz~400kHz完全没必要复杂化。我选择用模拟I2CGPIO直接翻转代码在F1、F4、H7上都能原样移植不依赖外设寄存器差异。OLED的I2C通信需要四个引脚VCC、GND、SCL、SDA。这块0.96寸的4针屏默认地址是0x78写地址对应7位地址0x3C。模拟I2C时SCL和SDA都配置成推挽输出SDA在读应答时切换成输入模式。因为OLED只有拉低总线表示ACK拉高就是NACK所以用推挽输出跟开漏效果一样只要在读取应答位之前把SDA切回输入就行。#define OLED_SCL_PORT GPIOB #define OLED_SDA_PORT GPIOB #define OLED_SCL_PIN GPIO_Pin_8 #define OLED_SDA_PIN GPIO_Pin_9 void OLED_GPIO_Init(void) { GPIO_InitTypeDef gpio; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE); gpio.GPIO_Pin OLED_SCL_PIN | OLED_SDA_PIN; gpio.GPIO_Mode GPIO_Mode_OUT; gpio.GPIO_OType GPIO_OType_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; gpio.GPIO_PuPd GPIO_PuPd_NOPULL; GPIO_Init(OLED_SCL_PORT, gpio); GPIO_SetBits(OLED_SCL_PORT, OLED_SCL_PIN | OLED_SDA_PIN); }读写函数的关键是起始信号SCL高电平期间SDA产生一个下降沿停止信号是SCL高电平期间SDA产生上升沿。字节传输始终是高位在前。每发送完一个字节主机要释放SDA总线输入模式读取从设备的ACK。这里的模拟I2C不校验ACK因为OLED时序要求没那么严真出错时写命令也不会得到肉眼可见的反馈。void OLED_I2C_Start(void) { OLED_SDA_HIGH(); OLED_SCL_HIGH(); delay_us(4); OLED_SDA_LOW(); delay_us(4); OLED_SCL_LOW(); } void OLED_I2C_Stop(void) { OLED_SDA_LOW(); OLED_SCL_HIGH(); delay_us(4); OLED_SDA_HIGH(); delay_us(4); } void OLED_I2C_SendByte(uint8_t dat) { uint8_t i; for (i 0; i 8; i) { if (dat 0x80) { OLED_SDA_HIGH(); } else { OLED_SDA_LOW(); } dat 1; OLED_SCL_HIGH(); delay_us(2); OLED_SCL_LOW(); delay_us(2); } }延时2μs是为了把SCL频率控制在200kHz以下。如果你用400kHz模式可以把延时改成1μs但OLED初始化序列和显示刷新时太高的I2C速率容易在长线上出干扰。我一般控制在100kHz刷新128x64单色屏幕完全够用肉眼也不会看出来刷新变慢。3.2 SSD1306初始化序列与命令写入SSD1306在上电后必须发送一长串初始化命令不能上来就送显示数据。这些命令配置了显示时钟分频、多路复用比例、对比度、内存寻址模式。下面的初始化序列是经过多次验证的直接抄就行。void OLED_Init(void) { OLED_GPIO_Init(); delay_ms(100); OLED_Write_Cmd(0xAE); // 关显示 OLED_Write_Cmd(0x20); // 设置内存寻址模式 OLED_Write_Cmd(0x10); // 页面寻址模式 OLED_Write_Cmd(0xB0); // 第0页起始 OLED_Write_Cmd(0xC8); // COM输出方向反显 OLED_Write_Cmd(0x00); // 列低地址 OLED_Write_Cmd(0x10); // 列高地址 OLED_Write_Cmd(0x40); // 起始行0 OLED_Write_Cmd(0x81); // 对比度设置 OLED_Write_Cmd(0x7F); // 默认值127 OLED_Write_Cmd(0xA1); // 段重映射 OLED_Write_Cmd(0xA6); // 正常显示 OLED_Write_Cmd(0xA8); // 多路复用 OLED_Write_Cmd(0x3F); // 1/64占空比 OLED_Write_Cmd(0xA4); // 恢复RAM显示 OLED_Write_Cmd(0xD3); // 显示偏移 OLED_Write_Cmd(0x00); // 偏移0 OLED_Write_Cmd(0xD5); // 时钟分频 OLED_Write_Cmd(0x80); // 分频值 OLED_Write_Cmd(0xD9); // 预充电周期 OLED_Write_Cmd(0xF1); OLED_Write_Cmd(0xDA); // COM硬件配置 OLED_Write_Cmd(0x12); OLED_Write_Cmd(0xDB); // VCOMH电平 OLED_Write_Cmd(0x40); OLED_Write_Cmd(0x8D); // 电荷泵 OLED_Write_Cmd(0x14); // 开启电荷泵 OLED_Write_Cmd(0xAF); // 开显示 OLED_Clear(); }命令写入函数要区分命令和数据。0x00是命令控制字节0x40是数据控制字节。在SSD1306的I2C协议里发送完设备地址后紧接着的字节如果是0x00后面的字节都解释为命令如果是0x40后面的都解释为数据。如果你把命令写错最常见的现象是屏幕全暗或者出现乱码但芯片不会报错。void OLED_Write_Cmd(uint8_t cmd) { OLED_I2C_Start(); OLED_I2C_SendByte(OLED_ADDR); // 0x78 OLED_I2C_SendByte(0x00); // Co1, D/C0 OLED_I2C_SendByte(cmd); OLED_I2C_Stop(); } void OLED_Write_Data(uint8_t dat) { OLED_I2C_Start(); OLED_I2C_SendByte(OLED_ADDR); OLED_I2C_SendByte(0x40); // D/C1 OLED_I2C_SendByte(dat); OLED_I2C_Stop(); }每次写一个字节都重新发送起始和停止信号效率不高但足够稳定。批量写显示数据时可以在一个起始信号后连续发送多个数据字节但0.96寸OLED屏缓存只有1024字节刷新整个屏幕也就一眨眼的事没必要优化。3.3 字符与汉字的显示原理OLED的GRAM是128x64位分成8页每页对应8行像素。页面寻址模式下先指定页地址0xB0~0xB7和列地址然后连续写数据数据会自动换列但不会自动换页。写满一页128列后要继续写下一页必须重新设置坐标。显示ASCII字符时我直接用取模好的8x16点阵字体。常用的显示函数套路是循环按列把字模数据写入控制字节。如果你要显示汉字比如“温度”两个字需要先取16x16点阵字模。示例字模16x16宋体“温”的一部分如下实际项目中用PCtoLCD2002取模设置为阴码、列行式、字节正序const uint8_t font_temp[] { 0x04, 0x04, 0xF4, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x94, 0x04, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x44, 0x84, 0x7E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00 };显示函数的核心是坐标计算汉字左上角在列x、页y那么上半屏写x到x15页y下半屏写同样的列范围页y1。因为16x16字模分上下两部分每部分16字节对应16列。void OLED_Show_Chinese(uint8_t x, uint8_t y, const uint8_t *font) { uint8_t i; OLED_Set_Pos(x, y); for (i 0; i 16; i) { OLED_Write_Data(font[i]); } OLED_Set_Pos(x, y 1); for (i 0; i 16; i) { OLED_Write_Data(font[i 16]); } }设置坐标时要同时写列地址低字节和高字节。列地址范围是0~127低字节是列的低4位高字节是列的高4位。比如列地址为100需要先发0x00再发0x10错误地把列号直接发进去会显示错位。页地址直接加0xB0得到命令字节。3.4 温度显示的刷新策略温度值每秒变化不超过0.5℃时OLED显示刷新频率不需要太高。我一般用一个500ms左右的周期来更新温度和日期。刷新太快会看到屏幕闪烁因为OLED在写GRAM时不会像LCD那样有残影但SSD1306内部没有帧缓冲复用反复全屏重写会有可感知的刷屏感。正确做法是只更新数值所在的区域而不是整屏重绘。比如温度值显示在(20, 3)位置每次先画一个空白矩形覆盖旧值再写新值这样屏幕不闪代码也更高效。4. RTC日期实时显示与报警逻辑实现4.1 RTC使用LSE晶振与备份域配置STM32F407的RTC在备份域里由VBAT引脚供电。当主电源掉电时只要VBAT接上3V电池RTC还能继续走。RTC的时钟源一般选择LSE外部32.768kHz晶振比内部LSI稳定得多。标准库中使能RTC前必须先使能PWR和BKP外设时钟然后调用PWR_BackupAccessCmd(ENABLE)打开备份域的写入权限否则后续操作会卡死。备份域还有一个BKP寄存器可以用来判断RTC是否已经初始化过。每次上电后读取BKP_DR0里的值如果等于魔数0xA5A5说明RTC已经在之前的运行中完成过初始化直接等待同步然后读取时间如果不是就执行完整的初始化流程并写入魔数。这样即使代码复位也不会把时间重置回初始值。void RTC_Init(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); if (RTC_ReadBackupRegister(RTC_BKP_DR0) ! 0xA5A5) { RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) RESET) { // 等待LSE起振 } RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_WriteProtectionCmd(DISABLE); RTC_TimeTypeDef time; time.RTC_H12 RTC_H12_AM; time.RTC_Hour 8; time.RTC_Minute 30; time.RTC_Seconds 0; RTC_SetTime(time); RTC_DateTypeDef date; date.RTC_Year 24; date.RTC_Month 1; date.RTC_Date 1; date.RTC_WeekDay 1; RTC_SetDate(date); RTC_WriteProtectionCmd(ENABLE); RTC_WriteBackupRegister(RTC_BKP_DR0, 0xA5A5); } else { RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); } }注意RTC_WriteProtectionCmd的调用必须在RTC_WaitForSynchro之后。如果你在初始化流程里漏掉写保护解除RTC_SetTime和RTC_SetDate都不会生效但不会报错。另外RTC的日期结构体里year存的是相对于2000年的偏移所以2024年要填24不是2024。这个细节写错会导致日期显示成3924年。4.2 读取时间日期并格式化输出标准库中读取RTC时间用RTC_GetTime和RTC_GetDate。读取日期时函数会自动处理日期进位比如跨月、闰年。我写一个格式化函数把日期和时间拼成字符串方便直接传给OLED显示char time_buf[20]; void RTC_Get_Display_String(char *buf) { RTC_TimeTypeDef time; RTC_DateTypeDef date; RTC_GetTime(RTC_Format_BIN, time); RTC_GetDate(RTC_Format_BIN, date); sprintf(buf, %04d-%02d-%02d\n%02d:%02d:%02d, 2000 date.RTC_Year, date.RTC_Month, date.RTC_Date, time.RTC_Hours, time.RTC_Minutes, time.RTC_Seconds); }注意RTC_GetTime和RTC_GetDate必须成对调用。在标准库实现里RTC_GetDate会从RTC_DR寄存器读取日期同时内部还维护一个日期缓存如果你只调用RTC_GetTime日期缓存不更新后续读取日期会拿到上一次的值。我犯过这个错最后显示的时间时分秒一直在跳日期永远停在上电那天。4.3 报警阈值比较与连续去抖报警逻辑本身很简单温度大于上限或小于下限就触发。但温度传感器数据偶尔会有毛刺比如接触不良时DS18B20会读到-55℃或125℃这种异常数据如果直接触发报警系统就成“狼来了”。我做了连续三次确认机制只有连续三次数值越限才真正报警期间只要有一次正常就清零计数器。#define ALERT_HIGH 40.0f #define ALERT_LOW 10.0f uint8_t alert_count 0; void Alert_Process(float temp) { if ((temp ALERT_HIGH) || (temp ALERT_LOW)) { if (alert_count 3) { alert_count; } else { GPIO_SetBits(GPIOE, GPIO_Pin_0); // LED亮 GPIO_SetBits(GPIOE, GPIO_Pin_1); // 蜂鸣器响 } } else { alert_count 0; GPIO_ResetBits(GPIOE, GPIO_Pin_0); GPIO_ResetBits(GPIOE, GPIO_Pin_1); } }我这里把LED接在PE0蜂鸣器接在PE1。不同板卡的蜂鸣器驱动方式不一样有些通过三极管放大有些直接由MOS管驱动有源蜂鸣器要给高电平才响无源蜂鸣器需要PWM方波。这套代码里用简单的GPIO_SetBits驱动有源蜂鸣器如果你的是无源蜂鸣器需要改成输出50%占空比的方波否则只会听到“咔哒”一声。报警阈值写死在宏定义里开发调试时可以直接改宏重编。但我更建议把阈值放到Flash的最后一个扇区或备份寄存器里这样产品上电后可以保留用户设置不重新编译也能改报警范围。4.4 主循环整合流程主循环的逻辑顺序是读取温度、更新OLED、读取RTC日期、报警处理。由于DS18B20转换一次要750ms如果每次都等转换完成RTC秒更新也会被拖到同一节奏。我把温度读取放在一个状态机里利用等待转换的750ms去刷新日期和最开始的温度显示。static uint8_t convert_started 0; while (1) { if (!convert_started) { DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0x44); convert_started 1; } if (get_tick_ms() - last_convert_ms 750) { float temp DS18B20_Read_Temp_Result(); OLED_Show_Temperature(temp); Alert_Process(temp); convert_started 0; last_convert_ms get_tick_ms(); } if (get_tick_ms() - last_time_ms 1000) { RTC_Get_Display_String(time_buf); OLED_Show_Time(time_buf); last_time_ms get_tick_ms(); } }主循环里不要用delay_ms(750)死等温度转换否则RTC日期会严重滞后。用非阻塞的时间片方式把温度转换等待期让给OLED刷新和按键扫描。这套代码里没有按键报警阈值是写死的如果你想加按键调阈值需要在主循环里增加按键扫描时间片并把阈值变量改成可修改的ram变量。5. 移植到其他F4芯片与调试技巧把工程从STM32F407移植到另一颗F4芯片比如STM32F411或STM32F427其实只改三处设备型号、C/C宏定义里的STM32F40_41xxx、启动文件startup_stm32f40xx.s。外部晶振频率如果不一致还需要修改SystemCoreClock配置F407裸机工程默认25MHz外部晶振换到8MHz晶振的板卡上SystemCoreClock会被自动算成168MHz吗不会因为标准库的SystemInit函数会读取HSE_VALUE宏不支持运行时切换。DS18B20和OLED的GPIO引脚如果跟新板卡冲突改宏定义里的端口和引脚号就行。RTC的初始化代码也不需要变因为LSE时钟源和备份域在F4全系列都相同。唯一要注意的是F411和F401没有RTC电池供电还是有的但RTC_BKP寄存器数量不同如果你用到了后面的备份寄存器需要查一下新芯片的BKP数量。调试这套系统时我通常会先用串口打印而不是OLED。把温度和RTC时间通过printf打到串口助手这样能看到连续的数据趋势比盯着屏幕猜问题快得多。如果你的工程里没有printf重定向加一个fputc函数重写int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }常见问题排查看这个表格现象可能原因排查步骤DS18B20一直返回0xFF上拉电阻缺失、时序延时不准、引脚接触不良测数据线静态电平正常为高检查延时循环OLED没反应或花屏I2C地址错误、初始化序列不完整、SDA/SCL接反用逻辑分析仪看I2C波形确认从机地址RTC时间不走LSE晶振没起振、备份域未使能检查RCC_FLAG_LSERDY标志更换晶振温度显示恒为某个数DS18B20配置成寄生供电模式但3脚悬空检查VDD是否接3.3V或改为寄生供电最后一个技巧给报警阈值加上Flash存储。F4系列自带Flash模拟EEPROM的方案已经成熟你可以直接把阈值写到最后一个扇区上电读取。这样这个报警系统就不只是课程设计而是一个能改参数的小产品。移植时记得把阈值读取函数放在AD转换之后、DS18B20读取之前避免加载阈值时阻塞主循环。本文还有配套的精品资源点击获取