1. 为什么我劝你先别急着买开发板
很多人一提到学STM32,第一反应就是先下单一块开发板,然后对着教程点灯、串口打印、跑马灯三件套走一遍。但说实话,我见过太多人开发板买回来吃灰的——焊好的板子往抽屉一扔,教程看了三集就再也没打开过。问题出在哪?不是STM32难,而是学习路径太重了。你得先装Keil、装芯片包、配下载器驱动、搞懂BOOT引脚、处理各种下载失败的问题,光环境搭建就能劝退一半人。
所以当我第一次看到“不用开发板学习STM32”这个思路时,我是很认同的。核心逻辑很简单:用Proteus仿真替代实物硬件,把DS18B20温度采集和OLED显示这两个经典外设跑通,你就能理解STM32的GPIO操作、单总线时序、I2C通信协议这几个核心知识点。而且仿真环境有个天然优势——你可以随时暂停、单步调试、用虚拟示波器看波形,这在实物上反而不好操作。
这篇文章要聊的就是这么一套完整的仿真方案:在Proteus里搭一个STM32最小系统,挂上DS18B20温度传感器和OLED显示屏,用Keil写代码,编译出hex文件加载到仿真里跑。你不需要买任何硬件,一台电脑就够。适合谁看?刚学完C语言、想入门STM32但不想先花钱买板子的学生;或者手头有开发板但想验证某个外设逻辑的工程师;再或者带课的老师,想给学生演示时序但实验室设备不够用。
关键词先摆出来:STM32、DS18B20、OLED、温度采集、Proteus仿真、单总线时序、I2C通信。这些词后面会反复出现,每一个我都会拆开讲清楚。
2. 整体方案设计与选型思路
2.1 为什么选DS18B20而不是LM75或热敏电阻
温度采集的方案太多了,热敏电阻加ADC、LM75走I2C、DHT11单总线、DS18B20单总线,各有各的适用场景。我选DS18B20来配合这个仿真项目,理由有三条。
第一,单总线协议是STM32学习里绕不开的坎。DS18B20只用一根数据线就能完成供电和通信(寄生电源模式),这对理解GPIO的输入输出切换、微秒级延时、时序窗口这些概念特别有帮助。你调通了DS18B20,再去搞DHT11或者单总线EEPROM,基本就是换个命令字的事。
第二,测量范围够用且精度合理。DS18B20的测温范围是-55°C到+125°C,在-10°C到+85°C区间内精度是±0.5°C,分辨率可以通过配置寄存器设成9到12位。对于仿真演示来说,这个精度完全够看,而且它的温度值格式是16位带符号数,处理起来能顺便练一下数据类型的转换。
第三,Proteus里有现成的仿真模型。这一点很关键。LM75在Proteus里也有模型,但DS18B20的模型更成熟,你可以直接在元件库里搜到,还能双击修改它模拟输出的温度值。这意味着你不需要真的拿个打火机去烤传感器,在仿真里改个数字就能看到OLED上的温度变化。
2.2 OLED选I2C接口还是SPI接口
OLED显示屏常见的有两种驱动方式:I2C和SPI。I2C只要两根线(SCL、SDA),SPI要四根线(SCK、MOSI、CS、DC)甚至更多。在Proteus仿真里,我建议用I2C接口的0.96寸OLED,原因很实际。
仿真环境下,接线越少越不容易出错。SPI的OLED在Proteus里需要接CS片选、DC数据命令切换、RES复位,线一多就容易接错,而且SPI的时序在仿真里调试起来比I2C麻烦。I2C虽然速率慢一点,但0.96寸128x64的屏幕刷新一帧也就几十毫秒,温度显示这种应用完全感觉不到延迟。
另外,I2C的协议本身也是STM32学习的重要一环。你可以用硬件I2C外设,也可以用GPIO模拟I2C。我建议先用GPIO模拟I2C,因为这样你能完全掌控时序,遇到问题好排查。等模拟的跑通了,再换成硬件I2C对比一下,理解会更深刻。
2.3 软件工具链的选择
Keil MDK是STM32开发的老牌工具,虽然界面老旧,但资料多、问题好搜。Proteus 8.9以上版本对STM32F103的支持比较稳定,我实测用Proteus 8.13跑STM32F103C6没问题。芯片包需要单独安装,Keil里装STM32F1系列的Device Family Pack,Proteus里确认有STM32F103的仿真模型。
这里有个坑要提前说:Proteus仿真STM32的时钟频率和实际芯片有差异。仿真里CPU的指令执行时间并不是精确的1/72MHz,所以那些依赖精确延时的代码(比如DS18B20的微秒级延时)在仿真里可能会偏慢或偏快。解决办法后面会讲,核心思路是用循环延时配合示波器校准,而不是死磕理论计算值。
3. 核心细节解析与实操要点
3.1 DS18B20的单总线时序到底怎么理解
DS18B20的通信协议说复杂也不复杂,但第一次接触的人容易被各种时间参数搞晕。我用一个生活化的类比来解释:单总线就像两个人用摩尔斯电码聊天,只有一根线,你说的时候我不能说,我说的时候你不能说,而且每个“滴”和“答”的持续时间都有严格规定。
具体到DS18B20,所有通信都由主机(STM32)发起。主机拉低总线一段时间再释放,这个低电平的持续时间决定了是写0还是写1,或者是一个复位脉冲。DS18B20在特定时间窗口内采样总线电平,从而判断主机发的是什么。
关键时序参数我列个表,这些数字必须记住,写代码的时候要对照着调:
| 操作类型 | 时序名称 | 时间要求 | 说明 |
|---|---|---|---|
| 复位 | 复位低电平 | 480-960μs | 主机拉低至少480μs |
| 复位 | 存在脉冲 | 60-240μs | DS18B20拉低回应 |
| 写0 | 低电平时间 | 60-120μs | 整个时隙至少60μs |
| 写1 | 低电平时间 | 1-15μs | 然后释放总线 |
| 读 | 采样窗口 | 15μs内 | 主机拉低后15μs内采样 |
| 读 | 时隙总长 | 至少60μs | 两次读之间间隔至少1μs |
在STM32上实现这些延时,通常用循环空转或者DWT周期计数器。循环空转的好处是不依赖额外外设,坏处是不同编译器优化等级下延时不一样。我的做法是写一个delay_us()函数,然后在Proteus里用虚拟示波器看实际波形,微调循环次数直到时序满足要求。
注意:Proteus仿真STM32时,如果用的是正点原子那种
delay_us()函数(基于SysTick),在仿真里可能会因为时钟配置问题导致延时不准。建议在仿真阶段先用简单的for循环延时,等逻辑跑通了再换成精确延时。
3.2 OLED的I2C驱动要点
OLED的I2C驱动分两层:底层是I2C的字节读写,上层是SSD1306控制器的命令和数据写入。SSD1306的I2C地址通常是0x78(写地址),有些模块是0x7A,这个要看具体模块的手册。在Proteus里,你可以双击OLED元件查看它的I2C地址设置。
初始化SSD1306有一长串命令要发,包括设置对比度、显示模式、扫描方向、时钟分频等等。这些命令不需要你从头记,网上有现成的初始化序列,直接抄就行。但你要理解几个关键命令的作用:
- 0xAE/0xAF:关闭/开启显示。调试的时候可以先关显示,等数据写完了再开,避免看到花屏。
- 0x20:设置内存寻址模式。通常用0x00(水平寻址)或0x02(页寻址)。页寻址适合局部刷新,水平寻址适合整屏刷新。
- 0xB0-0xB7:设置页地址(0-7页,每页8行像素)。
- 0x00-0x0F和0x10-0x1F:设置列地址的低4位和高4位。
显示字符的原理就是取模。你要显示一个字符,得先有这个字符的点阵数据。常用的取模软件是PCtoLCD2002,设置成“阴码、逐列式、顺向”,生成的字模数组直接放到代码里。0.96寸OLED是128x64像素,显示16x16的汉字一行能放8个,显示8x16的ASCII字符一行能放16个。
3.3 GPIO模拟I2C的代码结构
用GPIO模拟I2C,核心就是控制SCL和SDA两根线的电平变化。SDA和SCL都要配置成开漏输出(GPIO_Mode_Out_OD),这样总线才能被外部上拉电阻拉高。Proteus里的OLED模型内部有上拉,所以你在STM32端配置成开漏输出就行。
模拟I2C的基本函数有这几个:
// SDA方向切换:输出模式用于发送,输入模式用于接收 void I2C_SDA_Out(void); void I2C_SDA_In(void); // 起始信号:SCL高时SDA从高变低 void I2C_Start(void); // 停止信号:SCL高时SDA从低变高 void I2C_Stop(void); // 发送一个字节,返回从机应答 uint8_t I2C_SendByte(uint8_t data); // 接收一个字节,发送应答或非应答 uint8_t I2C_RecvByte(uint8_t ack);每个函数的实现都要配合延时。I2C标准模式是100kHz,快速模式是400kHz。在仿真里,我建议先用低速跑通,延时给大一点,比如每个电平变化后延时5-10μs。等逻辑没问题了,再逐步减小延时提高速率。
实操心得:Proteus仿真I2C时,如果发现OLED不亮或者显示乱码,先检查I2C地址对不对,再检查起始信号和停止信号的时序。用Proteus的I2C调试器可以抓包看数据,这个功能比实物调试还方便。
4. 实操过程与核心环节实现
4.1 Proteus工程搭建步骤
打开Proteus,新建工程,按下面的清单把元件找齐:
- STM32F103C6(或C8,R6/R8都行,仿真里差别不大)
- DS18B20(在“Temperature Sensors”分类里)
- OLED 128x64 I2C(在“Displays”分类里,选SSD1306驱动的)
- RES(电阻,用于上拉,虽然OLED模型内部有,但加上更保险)
- CRYSTAL(晶振,8MHz)
- CAP(电容,22pF两个,用于晶振)
- BUTTON(可选,用于复位)
接线方案:
- STM32的PA0接DS18B20的DQ引脚,同时接一个4.7kΩ上拉电阻到3.3V
- STM32的PB6接OLED的SCL,PB7接OLED的SDA
- 晶振接在PD0和PD1(OSC_IN和OSC_OUT)上,配22pF电容到地
- BOOT0接10kΩ下拉到地,BOOT1也下拉
这里有个细节:Proteus里STM32的电源引脚默认是隐藏的,不需要手动接。但你要在STM32的属性里确认“Use ROM/RAM”设置正确,否则程序可能跑不起来。
4.2 Keil工程配置与代码框架
Keil里新建工程,选STM32F103C6,然后添加启动文件(startup_stm32f10x_md.s)和标准外设库文件。如果你用HAL库也行,但标准库在仿真里更轻量,编译出来的hex更小,加载更快。
代码框架分四个模块:
- 延时模块:
delay.c,提供delay_us()和delay_ms() - DS18B20驱动:
ds18b20.c,提供DS18B20_Init()、DS18B20_ReadTemp() - OLED驱动:
oled.c,提供OLED_Init()、OLED_ShowString()、OLED_ShowNum() - 主函数:
main.c,初始化外设,循环读取温度并显示
主循环的逻辑很简单:
int main(void) { float temp; Delay_Init(); OLED_Init(); OLED_ShowString(0, 0, "Temp:", 16); while(1) { temp = DS18B20_ReadTemp(); OLED_ShowString(0, 2, " ", 16); // 清除旧数据 OLED_ShowFloat(0, 2, temp, 16); delay_ms(500); } }4.3 DS18B20温度读取的完整流程
DS18B20读一次温度要经过三个步骤:复位、跳过ROM、启动转换、等待转换、复位、跳过ROM、读暂存器。每一步都有对应的命令字:
- 0xCC:跳过ROM(只有一个传感器时用)
- 0x44:启动温度转换
- 0xBE:读取暂存器(前两个字节就是温度值)
温度值的处理要注意:DS18B20返回的是16位数据,低4位是小数部分,高12位是整数部分(12位分辨率时)。如果温度是负的,高5位是符号位,全是1。处理代码大概长这样:
float DS18B20_ReadTemp(void) { uint8_t tempL, tempH; int16_t raw; float result; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); delay_ms(750); // 等待转换完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); tempL = DS18B20_ReadByte(); tempH = DS18B20_ReadByte(); raw = (tempH << 8) | tempL; if(raw & 0x8000) // 负温度 { raw = ~raw + 1; result = -(raw * 0.0625); } else { result = raw * 0.0625; } return result; }0.0625是12位分辨率下的最小刻度,因为12位分辨率把1°C分成了16份,1/16=0.0625。
4.4 仿真调试与波形验证
代码编译出hex后,在Proteus里双击STM32,把Program File指向hex文件,Clock Frequency设成8MHz(和晶振一致)。点运行,如果OLED亮了并且显示温度,基本就成功了。
如果没显示,按这个顺序排查:
- 看电源:Proteus里STM32的VDD和VSS虽然隐藏,但要在属性里确认电压是3.3V。
- 看复位:NRST引脚有没有被意外拉低。
- 看时钟:用Proteus的虚拟示波器接在OSC_OUT上,看有没有8MHz波形。
- 看I2C:用I2C调试器抓SCL和SDA,看有没有起始信号和地址帧。
- 看单总线:用示波器接PA0,看复位脉冲和存在脉冲是否正常。
我踩过的一个坑是:Proteus里DS18B20的默认温度值是25°C,但如果你在仿真过程中修改了它的温度属性,需要先停止仿真再改,运行中改可能不生效。另外,DS18B20的转换时间在仿真里可能比实际芯片快,delay_ms(750)可以适当缩短,但建议还是保留,养成好习惯。
5. 常见问题与排查技巧实录
5.1 仿真跑不起来或程序不执行
这是最常见的问题,表现是Proteus里STM32的引脚电平没变化,OLED全黑。排查思路:
- 确认hex文件路径没有中文和空格。Proteus对中文路径支持不好,把工程放在纯英文路径下。
- 确认Keil里选的芯片型号和Proteus里的一致。都是STM32F103C6或C8,不能一个C6一个C8。
- 确认启动文件选对了。STM32F103C6是中等容量,启动文件用
startup_stm32f10x_md.s,用hd的会跑飞。 - 确认BOOT0和BOOT1都接地。仿真里虽然不涉及启动模式,但Proteus的模型可能会检查这两个引脚。
5.2 OLED不亮或显示乱码
OLED的问题分几种情况:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 全黑 | I2C地址错 | 改成0x78或0x7A试试 |
| 全黑 | 初始化命令没发完 | 检查初始化序列,加延时 |
| 花屏 | 显存数据错位 | 检查页地址和列地址设置 |
| 部分显示 | 取模方式不对 | 改成逐列式、顺向 |
| 闪烁 | 刷新太频繁 | 降低刷新率,加延时 |
我遇到过一次OLED显示一半正常一半乱码,查了半天发现是列地址设置时高4位和低4位搞反了。SSD1306的列地址是分两次发的,低4位先发,高4位后发,顺序错了就会导致显示偏移。
5.3 DS18B20读出来一直是85°C
85°C是DS18B20的上电默认值,说明温度转换根本没执行。原因通常是:
- 启动转换命令(0x44)没发出去
- 复位脉冲太短,DS18B20没响应
- 延时不够,转换还没完成就去读
解决办法:用示波器看PA0的波形,确认复位脉冲宽度在480μs以上,存在脉冲在60-240μs之间。如果波形不对,调整delay_us()的参数。
5.4 仿真里延时函数不准怎么办
这是Proteus仿真的固有问题。我的经验是:不要依赖理论计算,用示波器实测。比如你要一个500μs的延时,写个循环延时,然后在示波器上看实际宽度,再按比例调整循环次数。虽然麻烦一点,但调一次后面就准了。
另外,如果你用的是正点原子的delay_us()(基于SysTick),在Proteus里需要确认SysTick的时钟源配置正确。有些仿真模型对SysTick的支持不完整,会导致延时偏大或偏小。这种情况下,换成简单的for循环延时反而更可靠。
5.5 常见问题速查表
| 问题 | 排查方向 | 快速验证 |
|---|---|---|
| 程序不跑 | hex路径、芯片型号、启动文件 | 换英文路径,核对型号 |
| OLED黑屏 | I2C地址、初始化、接线 | 用I2C调试器抓包 |
| 温度85°C | 转换命令、复位脉冲、延时 | 示波器看PA0波形 |
| 温度值跳变 | 电源干扰、上拉电阻 | 加4.7k上拉,缩短杜邦线 |
| 显示闪烁 | 刷新频率、清屏方式 | 局部刷新代替整屏刷新 |
| 编译报错 | 头文件路径、库版本 | 检查Include Paths |
独家避坑:Proteus仿真STM32时,不要开太多虚拟仪器。我同时开过示波器、I2C调试器和串口终端,结果仿真速度慢到几乎卡死。需要看哪个就开哪个,看完就关。
6. 从仿真到实物的迁移建议
仿真跑通之后,如果你想转到实物上,有几个地方需要调整。首先是延时函数,实物上可以用SysTick或者DWT做精确微秒延时,比仿真里的循环延时靠谱得多。其次是上拉电阻,实物上DS18B20的DQ引脚必须接4.7kΩ上拉,OLED的SDA和SCL也要接4.7kΩ上拉,很多模块自带上拉,但最好确认一下。最后是电源,实物上STM32和OLED都要3.3V供电,DS18B20可以3.3V也可以5V,但为了电平匹配,统一用3.3V最省事。
还有一点,实物上DS18B20的转换时间是真的750ms,不能缩短。仿真里你可以偷懒,但实物上必须等够时间,否则读出来还是85°C。
这个仿真工程我前后调了大概三个晚上,大部分时间花在I2C的时序和DS18B20的延时上。现在回头看,最难的不是写代码,而是理解时序图上的每一个时间参数对应到代码里是哪一行。一旦你把示波器上的波形和代码里的延时对应起来,后面再调其他单总线器件就是复制粘贴的事了。代码和Proteus工程文件我整理好了,需要的可以拿去直接跑,但建议你先自己照着搭一遍,踩过的坑才是自己的。