☰
无需开发板:STM32驱动DS18B20与OLED仿真实战
2026/10/9 4:21:41 网站建设 项目流程

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还是SPI

OLED屏常见的有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驱动调通了,剩下的就是加功能的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询