1. 为什么我劝你先别急着买开发板很多人一提到学STM32第一反应就是先下单一块开发板然后对着教程点灯、串口打印、跑马灯三件套走一遍。但说实话如果你手头暂时没有开发板或者你只是想快速验证一个温度采集加显示的小方案完全没必要等快递。一台电脑装好Keil或者STM32CubeIDE再配一个Proteus就能把DS18B20温度采集和OLED显示这条链路完整跑通。这个项目的核心就是两件事第一用STM32的GPIO去驱动DS18B20这个单总线温度传感器把温度值读出来第二把读到的温度数据通过I2C或者SPI送到OLED屏幕上显示。听起来简单但里面藏着不少坑尤其是DS18B20的时序问题很多人第一次写出来的代码读出来永远是85度或者-127度这就是典型的时序没对齐。我写这篇东西的目的很明确给那些手头没有开发板、但又想动手实践STM32外设驱动的人一条可复现的路径。不管你是刚学完GPIO和延时函数的新手还是已经用过HAL库但没碰过单总线协议的老手这套流程都能直接抄作业。文末我会把代码和工程文件的获取方式说清楚你照着搭一遍基本能避开我当年踩过的那些坑。2. 整体方案设计与核心思路拆解2.1 为什么选DS18B20而不是LM75或者DHT11温度传感器的选型其实挺多的I2C接口的LM75、单总线的DS18B20、还有DHT11这种温湿度一体的。我选DS18B20的理由有三个第一它只需要一根数据线就能通信对GPIO资源紧张的STM32来说很友好第二它的测温范围是-55到125摄氏度精度在-10到85度范围内是±0.5度做室温采集完全够用第三它的时序虽然严格但一旦调通稳定性非常好不会像DHT11那样偶尔抽风。LM75是I2C接口接线更简单但它的精度和DS18B20差不多价格却贵一些。DHT11虽然便宜但它的湿度精度只有±5%温度精度±2度而且采样率只有1Hz读一次要等一秒以上。如果你只是测温度DS18B20是性价比最高的选择。2.2 OLED选I2C还是SPIOLED屏常见的有0.96寸和1.3寸两种接口分I2C和SPI。I2C只需要两根线SCL和SDA接线少代码也简单适合引脚不多的场景。SPI速度快刷屏流畅但需要四根线而且不同厂家的驱动芯片初始化序列可能不一样。我这个方案用的是I2C接口的0.96寸OLED驱动芯片是SSD1306。为什么选它因为SSD1306的驱动资料最全HAL库的I2C驱动也很成熟你只要把从机地址设对基本不会出大问题。从机地址一般是0x78或者0x7A具体看你模块背面的电阻跳线。我手上这块是0x78也就是7位地址0x3C左移一位。2.3 不用开发板的仿真方案怎么搭Proteus里可以仿真STM32F103系列配合DS18B20和OLED的仿真模型能完整跑通整个流程。但要注意Proteus里的DS18B20模型和真实器件有时序差异尤其是复位脉冲和读写时隙的延时如果你直接用正点原子或者野火例程里的延时函数可能会因为仿真速度的问题导致时序错乱。我的做法是在Proteus里把STM32的主频设成72MHz然后用SysTick或者DWT来做微秒级延时。DWT的好处是不占用定时器资源而且精度高在仿真和真实硬件上表现一致。如果你用HAL库的HAL_Delay最小单位是毫秒根本没法做DS18B20的微秒级时序所以必须自己写一个us延时的函数。3. 核心细节解析与实操要点3.1 DS18B20的时序到底难在哪DS18B20用的是单总线协议所有通信都靠一根线完成包括复位、存在脉冲、读写时隙。它的时序要求非常严格比如复位脉冲需要拉低至少480微秒然后释放等待15到60微秒后如果器件在线它会拉低60到240微秒作为存在脉冲。如果你拉低的时间不够或者释放后采样太早就会读不到存在脉冲。写时隙分写0和写1。写0是拉低60到120微秒然后释放写1是拉低1到15微秒然后释放再拉低至少60微秒。读时隙是主机拉低1到15微秒然后释放在15微秒内采样总线电平。这些时间窗口都很窄所以延时函数的精度直接决定了通信成败。我实测下来用DWT延时在72MHz主频下微秒级延时的误差可以控制在1微秒以内完全满足DS18B20的要求。如果你用for循环做空指令延时在不同优化等级下延时时间会变很容易翻车。3.2 GPIO模式切换的坑DS18B20的数据线是双向的主机需要输出的时候配置成推挽输出需要输入的时候配置成浮空输入或者上拉输入。很多人忘了切换模式结果读出来的数据永远是0或者1。我的做法是写两个宏一个把引脚设成输出一个设成输入在复位、写时隙、读时隙之前分别调用。还有一个细节DS18B20的数据线需要接一个4.7k到10k的上拉电阻。如果你用的是STM32的最小系统板很多板子上的GPIO内部上拉电阻大概在40k左右太弱了可能导致通信不稳定。所以最好在数据线和3.3V之间外接一个4.7k的电阻。Proteus仿真里也要记得加上拉电阻否则仿真也会失败。3.3 OLED显示的温度格式处理DS18B20读出来的原始数据是16位的低4位是小数部分高12位是整数部分。比如读出来0x0191转换成十进制是401乘以0.0625就是25.0625度。如果你直接把这个数除以16得到的是整数部分小数部分会被截断。我的做法是先把原始值转成浮点数然后乘以0.0625再用sprintf格式化成字符串显示。但sprintf在STM32上默认是不支持浮点的你需要在Keil的工程设置里勾选“Use MicroLIB”或者在链接选项里加上“-u _printf_float”。如果你不想用浮点也可以把原始值拆成整数和小数两部分整数部分右移4位小数部分与0x0F相与再乘以0.0625然后分别显示。4. 实操过程与核心环节实现4.1 工程搭建与基础配置我用的开发环境是Keil MDK 5芯片选STM32F103C8T6时钟树配置成72MHz。如果你用STM32CubeIDE流程也差不多只是界面不一样。新建工程后先使能外部晶振配置好时钟树然后打开I2C1模式选I2C速度设成400kHz。GPIO方面选一个引脚作为DS18B20的数据线我用的PB12配置成推挽输出初始电平高。接下来写DWT延时的初始化代码。DWT是Cortex-M3内核自带的一个调试单元可以用来做精确延时。初始化步骤是先使能DWT的CYCCNT计数器然后写一个us延时的函数原理就是读取当前CYCCNT的值加上主频乘以微秒数然后循环等待直到CYCCNT超过这个值。// DWT延时初始化 void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } // 微秒级延时 void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000); while ((DWT-CYCCNT - start) ticks); }这段代码在72MHz下delay_us(1)大概延时1微秒实测误差在0.5微秒以内。注意DWT-CYCCNT是32位的如果延时时间太长会溢出但DS18B20的时序最长也就几百微秒完全够用。4.2 DS18B20的复位与存在脉冲检测复位脉冲的流程是主机拉低数据线至少480微秒然后释放切换到输入模式等待15到60微秒然后采样数据线。如果器件在线它会拉低60到240微秒所以采样到低电平就说明存在脉冲有效。uint8_t DS18B20_Reset(void) { uint8_t presence; DS18B20_Output_Mode(); DS18B20_Low(); delay_us(500); DS18B20_High(); DS18B20_Input_Mode(); delay_us(30); presence DS18B20_Read_Pin(); delay_us(400); return presence; }这里有个细节释放数据线后要等30微秒再采样因为存在脉冲是在15到60微秒之间出现的。如果你等的时间太短可能还没等到器件拉低等太久存在脉冲可能已经结束了。我试过等20微秒和40微秒都能读到但30微秒是最稳的。4.3 读写时隙的实现写时隙的代码逻辑是拉低数据线如果是写1延时1到15微秒后释放如果是写0延时60到120微秒后释放。读时隙是拉低1到15微秒然后释放在15微秒内采样。void DS18B20_Write_Byte(uint8_t data) { for (int i 0; i 8; i) { DS18B20_Output_Mode(); DS18B20_Low(); delay_us(2); if (data 0x01) DS18B20_High(); else DS18B20_Low(); delay_us(60); DS18B20_High(); data 1; } } uint8_t DS18B20_Read_Byte(void) { uint8_t data 0; for (int i 0; i 8; i) { DS18B20_Output_Mode(); DS18B20_Low(); delay_us(2); DS18B20_High(); DS18B20_Input_Mode(); delay_us(10); data 1; if (DS18B20_Read_Pin()) data | 0x80; delay_us(50); } return data; }写时隙里拉低2微秒后设置电平然后保持60微秒这样写0的时候总拉低时间是62微秒写1的时候拉低2微秒后释放符合时序要求。读时隙里拉低2微秒后释放等10微秒采样采样窗口在15微秒以内没问题。4.4 温度转换与读取流程DS18B20的温度转换需要先发一条0x44命令然后等待转换完成。12位精度的转换时间最长是750毫秒所以如果你用HAL_Delay(750)也可以但为了不阻塞其他任务我一般用状态机或者定时器来轮询。读取温度的时候先发0xCC跳过ROM再发0xBE读暂存器然后连续读两个字节低字节在前高字节在后。float DS18B20_Get_Temp(void) { uint8_t temp_l, temp_h; int16_t raw; float temp; DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0x44); HAL_Delay(750); DS18B20_Reset(); DS18B20_Write_Byte(0xCC); DS18B20_Write_Byte(0xBE); temp_l DS18B20_Read_Byte(); temp_h DS18B20_Read_Byte(); raw (temp_h 8) | temp_l; temp raw * 0.0625f; return temp; }这里要注意raw是有符号的如果温度是负的高字节的高5位是1直接转成int16_t就能正确处理。我见过有人用uint16_t结果负温度读出来是六千多度这就是没考虑符号位。4.5 OLED驱动与显示刷新OLED的驱动我用的是一份精简版的SSD1306驱动只保留了I2C写命令和写数据两个底层函数然后封装了清屏、显示字符串、显示浮点数几个上层函数。I2C的从机地址是0x78写命令是0x00写数据是0x40。void OLED_Write_Cmd(uint8_t cmd) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x00, I2C_MEMADD_SIZE_8BIT, cmd, 1, 100); } void OLED_Write_Data(uint8_t data) { HAL_I2C_Mem_Write(hi2c1, 0x78, 0x40, I2C_MEMADD_SIZE_8BIT, data, 1, 100); }显示温度的时候我用sprintf把浮点数格式化成字符串然后调用OLED_ShowString显示。注意sprintf的缓冲区要够大我一般给32字节。如果你不想用浮点可以把温度拆成整数和小数两部分分别用%d和%d显示中间加个小数点。5. 常见问题与排查技巧实录5.1 读出来永远是85度或者-127度这是最经典的问题。85度是DS18B20上电后的默认值说明你的复位或者读写时序有问题器件根本没响应。-127度是读到的原始值是0xFFFF说明数据线一直是高电平可能是上拉电阻没接或者GPIO模式没切换。排查步骤先用示波器或者逻辑分析仪看复位脉冲的宽度确认拉低时间够480微秒。如果没有仪器就在代码里加个LED复位成功后点亮看LED有没有反应。如果LED不亮说明存在脉冲没检测到检查上拉电阻和GPIO模式。5.2 Proteus仿真时DS18B20不响应Proteus里的DS18B20模型对时序的要求比真实器件更严格尤其是延时函数的精度。如果你用正点原子的delay_us那个函数是基于SysTick的在Proteus里可能因为仿真速度的问题导致延时不准。我的建议是换成DWT延时或者在Proteus里把STM32的主频调低一点比如设成8MHz这样延时函数的误差会小一些。还有一个坑Proteus里的DS18B20模型默认温度是25度但如果你不发送温度转换命令它不会更新。所以你必须先发0x44等一段时间再发0xBE读。如果你直接读读到的可能是默认值。5.3 OLED不亮或者显示乱码OLED不亮的原因通常有三个一是I2C地址不对二是初始化序列没发对三是供电不足。先确认地址用I2C扫描程序扫一下看看能不能找到0x78。如果找不到检查SDA和SCL有没有接反上拉电阻有没有接。初始化序列要严格按照SSD1306的数据手册来尤其是对比度设置和显示模式设置。显示乱码一般是字库问题。如果你用的字库是GBK编码的而你的代码是UTF-8的显示出来就是乱码。解决办法是把字库转成UTF-8或者把代码转成GBK。我一般用UTF-8然后在Keil的工程设置里把编码改成UTF-8。5.4 常见问题速查表现象可能原因解决方法温度恒为85度复位时序错误检查拉低时间是否够480us温度恒为-127度数据线一直高电平检查上拉电阻和GPIO模式OLED不亮I2C地址错误用扫描程序确认地址OLED显示乱码字库编码不匹配统一用UTF-8或GBK仿真时通信失败延时函数精度不够改用DWT延时温度跳变严重电源噪声加滤波电容缩短导线6. 实操心得与避坑经验6.1 延时函数的选择决定成败我试过三种延时方案for循环空指令、SysTick、DWT。for循环在O0优化下还行但O2优化下会被编译器优化掉延时时间完全不对。SysTick在真实硬件上没问题但在Proteus里因为仿真速度的问题延时误差比较大。DWT是最稳的不管在真实硬件还是仿真里精度都能保证。如果你用HAL库HAL_Delay的最小单位是毫秒做DS18B20的微秒级时序根本不够用。所以你必须自己写一个us延时函数。DWT的代码很简单就几行但效果立竿见影。6.2 上拉电阻不能省DS18B20的数据线是开漏输出的必须接上拉电阻才能输出高电平。STM32的内部上拉电阻大概在40k左右太弱了可能导致上升沿太慢通信失败。我一般外接一个4.7k的电阻在Proteus里也要加上否则仿真也会失败。6.3 温度转换时间不能省DS18B20的12位精度转换时间最长是750毫秒如果你不等够时间就去读读到的可能是上一次的值或者默认值。我一般用HAL_Delay(750)但这样会阻塞CPU。如果你用RTOS可以把这个延时改成任务挂起让其他任务先跑。6.4 代码和工程文件的获取代码和工程文件我已经打包好了包括Keil工程、Proteus仿真文件、DS18B20和OLED的驱动源码。你可以在评论区留下邮箱或者私信我我看到后会统一发。如果你在搭建过程中遇到问题也可以把报错信息发给我我帮你看看。这个方案后续还可以扩展比如加一个按键切换温度单位或者把温度数据通过串口传到上位机显示曲线。如果你手头有ESP8266或者蓝牙模块还可以把温度数据传到手机上看。这些扩展都不难核心的DS18B20和OLED驱动调通了剩下的就是加功能的事。