简介:这是一套面向STM32入门学习与嵌入式课程设计的温湿度实时监测工程。以STM32F103C8为主控,通过DHT11传感器采集环境温湿度,并驱动OLED屏完成数据展示,搭配Keil uVision5开发环境,源码组织清晰,适合用来理解GPIO、定时器延时、单总线通信及OLED显示驱动等基础外设的协作流程;既可作为智慧农业、小型气象站、家居环境监测等场景的原型,也适合初学者由零开始跟进一个完整的小型显示项目。压缩包共75个文件,以C源码与头文件为主体,包含硬件驱动、标准外设库、启动文件、Keil工程配置及辅助清理脚本,整体仅306KB;目录按Hardware、System、User、Library等功能模块划分,可快速定位DHT11与OLED驱动、延时函数和main主逻辑,便于二次开发与移植。目前已有7934人学习下载,无论是完成课程设计还是积累STM32嵌入式项目经验,都是一份轻量而完整的参考资料。 自己买过DHT11模块,照着网上代码抄了一遍,结果屏幕乱码、温度不更新、传感器读数卡死——最后查出来是I2C地址搞错了、DHT11时序不满足、OLED复位脚没拉高。这几乎是每个STM32新手都会踩的三连坑。
所以当看到“基于STM32的温湿度传感器+OLED屏显示项目文件压缩包”这个压缩包的名字时,我第一反应是:这玩意儿要是里面代码能直接编译通过、连上硬件就能跑,那真的省掉一大半的排查时间。但光能用还不够,如果不懂背后原理,换个引脚、换个芯片型号,项目又得从头折腾。
这篇文章我就以这个压缩包项目为线索,把从硬件选型、环境搭建、DHT11驱动时序、OLED取模显示到工程管理踩过的坑,一条龙讲清楚。适合刚入门STM32、想做一个完整小项目的同学,也适合那些手上有板子但不知道从哪儿下手的自学者。
1. 项目整体思路与硬件选型
1.1 为什么是STM32+DHT11+OLED这个组合
很多新手一上来就想搞复杂的,比如加WiFi、上云、搞APP控制。但坦白说,能把手头这块STM32F103C8T6用明白,把传感器数据采集准确、把屏幕显示调通,这个基本功比什么都重要。这个项目的核心链路很简单:STM32通过GPIO读取DHT11的温湿度数据,然后通过I2C接口把数据发送给OLED屏幕显示出来。
这个组合经典到什么程度呢?江科大、正点原子、野火,几乎所有STM32开发板的例程里都有这个Demo。原因有三:
- DHT11便宜、易买、单总线协议,逻辑简单,适合理解时序概念。
- OLED(通常是0.96寸,SSD1306控制器)功耗低、显示清晰、I2C接口只占两根线,接线方便。
- STM32F103C8T6性能和外设足够,几十块钱一块板子,坏了不心疼。
所以哪怕你以后要去做更复杂的项目,比如智能家居中控、小型气象站、大棚环境监测,这套框架都能直接复用,只需要把DHT11换成DHT22、SHT30,或者把OLED换成TFT彩屏而已。
1.2 温湿度传感器的选型差异:DHT11、DHT22、SHT30、485工业传感器
很多人看到热搜词里有“恒智微鑫485温湿度传感器原理图”,会疑惑:为什么我的项目用了DHT11,还要扯485工业传感器?因为这是两种完全不同的应用场景。
DHT11是消费级、低成本、单总线数字传感器,精度是±2℃和±5%RH,测量范围0-50℃、20-90%RH,适合室内环境、桌面小玩意、课程设计。它的优势就一个字:便宜,几块钱一个。
DHT22(也叫AM2302)精度高一些,是±0.5℃和±2%RH,测量范围也更宽(-40~80℃),但价格翻好几倍。
SHT30是I2C接口的传感器,精度更高、稳定性更好,而且不用自己写单总线时序,直接用I2C读寄存器就行。缺点是价格更高。
至于485输出的工业传感器,那一般是24V供电、RS485总线传输、Modbus协议,用在工厂、机房、粮库这种长距离、多节点、强干扰的场合。一块钱和一百块钱的传感器,应用场景完全不同。我这个压缩包项目用的DHT11,核心价值在于让你理解时序和协议,而不是追求精度。
1.3 OLED屏的选型:I2C还是SPI,0.96寸还是1.3寸
OLED这块,0.96寸的SSD1306是最常见的,有I2C接口和SPI接口两个版本。我强烈建议新手选I2C版本,因为只接4根线:VCC、GND、SCL、SDA,代码也好写。SPI版本虽然刷新速度快,但要多接好几根线,而且SPI时序比I2C复杂一些,对新手不友好。
这里插一句热搜词里提到的“oled屏的像素点由几层组成”这个问题。其实这个问题挺有意思的,OLED屏幕的像素点从物理结构来说主要是基板、阳极(ITO透明导电层)、有机发光层(空穴传输层、发光层、电子传输层这几层有机薄膜)、阴极(金属反射层),加上封装层。不过从单片机驱动角度来说,你只需要关心屏幕的分辨率和颜色就行了。0.96寸OLED是128x64像素,单色,每个像素只能亮或不亮,通过SSD1306控制器的GRAM来控制。你往GRAM里写1,这个像素就亮,写0就灭。屏幕刷新时控制器会自动把GRAM映射到物理像素上,这个映射关系、扫描方向是可以通过命令配置的,不同的库实现可能会因此出现镜像、反色之类的差异。
2. 开发环境准备与工程搭建
2.1 CubeMX配置:引脚分配与时钟树
拿到这个压缩包项目,第一步不是打开代码看,而是确认你的开发板是不是STM32F103C8T6,以及代码里引脚配置是否和你手上的板子一致。我用STM32CubeMX重新生成一份工程,这是最稳妥的路径。
打开STM32CubeMX,芯片选择STM32F103C8Tx,然后按下面的方式配置:
- RCC:HSE选择Crystal/Ceramic Resonator(外部晶振),这个是8MHz无源晶振常用的配置方式。
- SYS:Debug选择Serial Wire,这一步非常关键,不然你烧录一次程序之后,第二次就连接不上芯片了,因为默认的JTAG引脚被程序占用了,SWD也被关了。这就是为什么热搜词里有“stm32禁用jtag”,踩过坑的都懂。
- I2C1:选择I2C,默认PB6是SCL、PB7是SDA。
- GPIO:DHT11数据脚我一般接PA0,配置为Output Push Pull,速度Low就行。读时序时再手动切换为输入模式。如果使用开漏输出+外部上拉也可以,但DHT11的时序要求比较严格,用推挽输出+切换输入方向这个方式更可控。
- USART1:可选,用于调试打印,接PA9(TX)和PA10(RX)。
时钟树要注意:STM32F103系列最高主频72MHz,一般我们在Clock Configuration里把PLL倍频调到x9,系统时钟选PLLCLK,最终得到72MHz。我见过有人直接在CubeMX里默认配置就跑了,结果系统时钟才8MHz(直接用HSI),I2C通信速率不对,OLED显示就会出问题。
有一点要特别说明:I2C的速率配置为100KHz(Standard Mode)就行,OLED的SSD1306控制器虽然支持400KHz(Fast Mode),但实际走线、上拉电阻和代码实现都可能影响稳定性。我调试的时候遇到过100KHz稳定、400KHz偶发乱码的情况,后来干脆统一用100KHz。这个项目显示内容不多,100KHz完全够用。
2.2 KEIL5工程配置与Pack安装
CubeMX生成代码后,用KEIL5打开MDK-ARM目录下的工程文件。但在这之前,你要确认KEIL5的Pack装了没有。很多新手上来就打开工程,然后报一堆“device not found”或者“cannot open source file xxx.h”,大部分原因就是Pack没装好。
STM32F103C8T6对应的是Keil.STM32F1xx_DFP这个Pack。打开KEIL的Pack Installer,搜索STM32F1xx,安装对应版本就行。这个Pack里面包含了芯片的SVD描述文件、Flash算法、启动文件,没有它KEIL根本不知道你要烧录的芯片是什么。
编译前还有几个设置我要提醒一下:
- C/C++选项卡里,Define一栏加上
USE_HAL_DRIVER,STM32F103C8Tx,这是CubeMX生成代码的标配,如果你用的是现成工程,看看有没有漏掉。 - 勾选Use MicroLIB,这样可以省不少Flash空间,因为标准的C库printf会引入很多浮点格式化代码,MicroLIB是精简版,对单片机完全够用。
- Debug选项卡里,选择ST-Link Debugger(或者其他你手上的下载器,比如DAP-Link),然后在Settings里确认能识别到芯片ID。
如果你看到的是“No Target Connected”或者“Cannot access Target”,先检查接线:SWDIO、SWCLK、GND三根线必须接对,VCC也要接,但不要接下载器的目标供电(除非你确定板子没有独立供电)。还有一个常见坑:下载器质量不好或线太长,导致SWD时钟频率太高连不上,把Flash Download里的下载速度调到100kHz甚至10kHz试试。
2.3 工程文件结构拆解
打开这个压缩包项目,我猜里面大概有这几个文件夹:
- Core:包含main.c、stm32f1xx_it.c、system_stm32f1xx.c
- Drivers:STM32官方HAL库的源码和头文件
- HAL_LIB或Middlewares:一些额外的库文件
- BSP或User:我们自己写的DHT11驱动、OLED驱动
- MDK-ARM:KEIL工程文件
这种分层思路是对的:HAL库是ST官方的,不要动;BSP层是板级支持包,每个外设一个文件;User层是主逻辑。如果你拿到手的压缩包没有这种结构,也没关系,只要main.c是完整的,你完全可以自己把DHT11和OLED的驱动文件整理出来,这也是我下面要重点讲的。
3. 核心实现拆解:DHT11驱动与OLED显示
3.1 DHT11单总线时序:从波形到代码
DHT11的通信协议叫单总线(1-Wire),一根线既做电源又做数据(严格说是数据线,电源是独立的),核心逻辑就是靠不同长度的低电平脉冲来表示0和1。
完整的一次通信流程是这样:主机(STM32)先把数据线拉低至少18ms,然后释放(拉高),DHT11检测到这个起始信号后,会先拉低80us再拉高80us作为响应信号,紧接着发送40bit的数据:8bit湿度整数部分、8bit湿度小数部分、8bit温度整数部分、8bit温度小数部分、8bit校验和。
40bit的数据里面,每一位的编码规则是:先拉低50us,然后拉高,拉高持续26~28us表示0,拉高持续70us表示1。注意,这个时序不是绝对的,DHT11的手册上给的是范围值,不同的批次的传感器可能会有细微差别。所以读时序的代码里一般会有超时判断,防止死等。
下面这段代码是我整理过的DHT11读取逻辑,使用HAL库的微秒延时函数:
// 注意需要自己实现微秒延时,HAL_Delay只支持毫秒 static void DHT11_Delay_US(uint16_t us) { // 使用DWT计数器实现精确延时,或者用SysTick重载 DWT_Delay_Init(); DWT_Delay_US(us); } uint8_t DHT11_Read_Data(uint8_t *humidity, uint8_t *temperature) { uint8_t buf[5] = {0}; uint8_t i, j, temp = 0; // 主机拉低起始信号,至少18ms GPIO_Set_Pin(DHT11_PIN, 0); HAL_Delay(20); // 释放总线,拉高,等待DHT11响应 GPIO_Set_Pin(DHT11_PIN, 1); DHT11_Delay_US(30); // 切换为输入模式,读取数据 GPIO_Set_Mode_Input(DHT11_PIN); // 等待低电平响应信号(80us低+80us高) // 超时保护,防止死循环 uint16_t timeout = 10000; while (GPIO_Read_Pin(DHT11_PIN) == 1) { if (--timeout == 0) return 1; // 超时,传感器未响应 } timeout = 10000; while (GPIO_Read_Pin(DHT11_PIN) == 0) { if (--timeout == 0) return 1; } timeout = 10000; while (GPIO_Read_Pin(DHT11_PIN) == 1) { if (--timeout == 0) return 1; } // 读取40bit数据 for (j = 0; j < 5; j++) { for (i = 0; i < 8; i++) { timeout = 10000; while (GPIO_Read_Pin(DHT11_PIN) == 0) { if (--timeout == 0) return 1; } DHT11_Delay_US(40); // 40us后采样 if (GPIO_Read_Pin(DHT11_PIN) == 1) { temp |= 0x01; // 高电平持续较久,说明是1 } while (GPIO_Read_Pin(DHT11_PIN) == 1) { if (--timeout == 0) return 1; } if (i < 7) temp <<= 1; } buf[j] = temp; temp = 0; } // 校验 if ((buf[0] + buf[1] + buf[2] + buf[3]) == buf[4]) { *humidity = buf[0]; *temperature = buf[2]; return 0; } return 2; // 校验失败 }这里最关键的是那个DHT11_Delay_US(40),在第28~30us的时候采样电平,如果你延时不准确、偏长,读出来的数据会全部是错误的。这也是为什么我强调要用DWT或者SysTick做微秒延时,而不是靠空循环。空循环在不同优化等级下时间变化很大,Debug模式下能跑通,Release模式下就挂了。
另外每次读取完DHT11之后,建议间隔1秒以上再读下一次,DHT11手册上写的最高采样频率是1Hz,也就是每秒最多一次。你如果在一个while循环里连续读取,中间不加延时,传感器会反应不过来,读到的数据永远是旧的或者直接超时。
3.2 SSD1306 OLED驱动:I2C通信与初始化序列
OLED用的SSD1306控制器支持6800/8080并行接口、SPI、I2C三种方式,我们用的是I2C。SSD1306的I2C地址一般是0x3C,如果你的模块是0x3D,多半是因为地址引脚(SA0)被拉高了。这个地址在初始化代码里写死,如果你发现屏幕没反应,先查地址,不要急着改代码。
I2C通信的流程是:主机发送起始信号→发送设备地址+写位(0x78或0x7A)→发送控制字节(0x00表示后续是命令,0x40表示后续是数据)→发送命令或数据。
SSD1306初始化是一长串命令序列,设置显示关闭、电荷泵开启、对比度、扫描方向、行地址模式等。这里我强烈建议用现成的、验证过的初始化序列,不要自己改,除非你仔细读过SSD1306的数据手册。下面是常用的一段:
uint8_t oled_init_cmds[] = { 0xAE, // 关闭显示 0x20, 0x00, // 设置内存寻址模式为水平寻址 0xB0, // 设置页地址(第0页) 0xC8, // 设置扫描方向(从右上到左下,和常见屏幕方向一致) 0x00, 0x10, // 设置列地址低/高四位 0x40, // 设置显示起始行 0x81, 0x7F, // 设置对比度 0xA1, // 设置段重映射(避免镜像) 0xA6, // 正常显示(非反色) 0xA8, 0x3F, // 设置多路复用比 0xD3, 0x00, // 设置显示偏移 0xD5, 0x80, // 设置时钟分频 0xD9, 0xF1, // 设置预充电周期 0xDA, 0x12, // 设置引脚配置 0xDB, 0x40, // 设置VCOMH 0x8D, 0x14, // 开启电荷泵 0xAF, // 开启显示 };有些初始化序列里如果有0xA1(段重映射),而你的屏幕又是镜像的,那就改成0xA0试试。同理,扫描方向0xC8和0xC0互为镜像。OLED这行就是个靠命令调镜像的事情,别怀疑芯片坏了。
I2C读写用HAL库的HAL_I2C_Mem_Write就很方便,因为它能自动处理设备地址、寄存器地址(这里就是控制字节)和数据缓冲区。写一个命令:
void OLED_Write_Cmd(uint8_t cmd) { HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, 0x00, I2C_MEMADD_SIZE_8BIT, &cmd, 1, 100); } void OLED_Write_Data(uint8_t data) { HAL_I2C_Mem_Write(&hi2c1, OLED_ADDR, 0x40, I2C_MEMADD_SIZE_8BIT, &data, 1, 100); }这里有个小坑:HAL_I2C_Mem_Write的第二个参数是设备地址,SSD1306的7位地址是0x3C,但HAL库内部会在发送时自动左移一位并拼接读写位,所以你传的就是0x3C,不用自己移位成0x78。我见过有人在代码里写0x78,结果I2C总线上的地址变成了0xF0,屏幕完全不响应。
3.3 OLED汉字取模与显示逻辑
OLED显示汉字和字符的本质是往GRAM里填像素数据。128x64分辨率,一列8个像素为一个字节,16x16的汉字就是32字节数据。取模工具我用的是PCtoLCD2002,设置如下:
- 取模方式:阴码,点阵内为1表示点亮
- 逐行式取模
- 每行16个点,共16行
取出来的效果是0x00,0x00,0xFC,0x04, ...这种代码,可以直接定义成const数组,节省RAM。
显示时要注意,SSD1306在页寻址模式下,一次可以从某个页(page)的某列开始连续写数据。页地址范围是0~7,每页8行像素,所以64行像素被分成8页。你往某个页写一个字节,这个字节的8个bit就代表该页这8行对应列的亮灭。这在显示16x16汉字时,需要跨两个页写数据:上半部分16行像素,位于第0页和第1页(从page p开始),写两轮,每轮16字节。
显示函数的核心逻辑大概这样:
void OLED_Show_CN(uint8_t x, uint8_t page, const uint8_t *font_data) { // 先写汉字上半部分(前16字节) OLED_Set_Cursor(x, page); for (uint8_t i = 0; i < 16; i++) { OLED_Write_Data(font_data[i]); } // 再写下半部分(后16字节) OLED_Set_Cursor(x, page + 1); for (uint8_t i = 16; i < 32; i++) { OLED_Write_Data(font_data[i]); } }这里有个注意点:汉字库通常是16x16像素,但“温”、“湿”、“度”这些字有些笔画比较复杂,取模出来效果可能差强人意。如果显示效果不好,可以把取模的字号改成16x16标准,或者换成12x12的字体减少空间占用。
还有一个很实用的技巧:显示变量。DHT11读出来的是整数,比如湿度45%、温度25度。我们可以在OLED上显示“Temp: 25 C”和“Hum: 45 %”。但注意DHT11的精度,整数显示就够了。如果你想显示小数,做浮点格式化会很消耗Flash和RAM,建议整数和小数分开算,比如25.3度,就分别取25和3,手动拼字符。
3.4 main.c主循环逻辑设计
主程序逻辑非常简单,但有一些细节值得讲究。我的main函数里,外设初始化完成后,主循环大概是这样:
int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_I2C1_Init(); MX_USART1_UART_Init(); OLED_Init(); OLED_Clear(); OLED_Show_String(16, 0, "Temp:", 16); OLED_Show_String(16, 2, "Hum:", 16); uint8_t hum, temp; char buf[20]; while (1) { if (DHT11_Read_Data(&hum, &temp) == 0) { // 成功读取,更新显示 sprintf(buf, "%d C", temp); OLED_Show_String(64, 0, buf, 16); // 显示温度 sprintf(buf, "%d %%", hum); OLED_Show_String(64, 2, buf, 16); // 显示湿度 } else { // 读取失败,可以显示错误标记 OLED_Show_String(0, 7, "DHT11 ERR", 8); } HAL_Delay(1000); // 每秒读一次 } }这里有个细节:每一轮更新前不用清屏,因为你要更新的区域只是数字部分,而且数字是用固定位置写上去的。如果数字从“25”变成“5”,之前十位的“2”如果不擦掉,显示就成了“52”。最简单的办法是每次先在这个区域填充空白字符(比如写三个空格),再写新数据。
还有一点,OLED_Init之后要做一次OLED_Clear,不然上电屏幕可能是花屏,因为GRAM里的初始数据是不确定的。
4. 常见问题与排查技巧实录
4.1 I2C通信异常的排查思路
I2C通信异常是最让人头痛的问题,因为看起来代码没错、接线没错,但就是没反应。我总结了几个排查方向,按优先级排列:
查硬件接线:SCL和SDA有没有接反?VCC和GND有没有接对?OLED模块的VCC虽然标称3.3V~5V都能工作,但STM32的I2C引脚是3.3V电平,所以最好统一用3.3V供电。如果你把OLED的VCC接到了5V,而SCL/SDA是3.3V的IO口,长期运行还是有风险的。
查I2C地址:前面已经提过,0x3C和0x3D的问题。你可以在初始化OLED之前,用HAL_I2C_IsDeviceReady检测一下设备是否在线:
if (HAL_I2C_IsDeviceReady(&hi2c1, 0x3C, 5, 100) != HAL_OK) { // 设备未响应,这里可以点一个LED提示 }查外部上拉电阻:STM32的I2C引脚是开漏输出,必须要有外部上拉电阻才能输出高电平。大多数开发板和OLED模块上已经带了4.7k或10k上拉电阻,但如果你的OLED是那种裸屏模块,或者你用了杜邦线外接,没有上拉电阻,I2C通信是绝对不工作的。
查I2C速率:如果通信不稳定,把速率从400KHz降到100KHz试试。这个问题在长线连接时特别明显,杜邦线一长,寄生电容就会影响信号边沿,速率越高越容易出错。
4.2 DHT11读取失败和数据恒为0的调试方法
DHT11读取失败最常见的三个原因:引脚配置不对、微秒延时不准、总线竞争(多个设备共用引脚)。调试方法有一个很好用的:用逻辑分析仪抓波形,没有逻辑分析仪也可以用printf打印每个阶段的电平状态。
我分享一段调试思路:把DHT11数据引脚配置为输入模式后,在主循环里只打印引脚电平的翻转时间。如果你看到的是大约20ms低电平→80us低→80us高→数据段,说明时序基本正常。如果你看到引脚一直高电平,说明传感器没供电,或者数据线没接对。如果一上电就是低电平,可能是传感器坏了,或者数据线短路到地。
数据恒为0还有一个可能性:读取成功后,你打印的是buf[0]和buf[2],但实际接收的湿度值是整数部分(buf[0]),如果你看错位了,把buf[1](小数部分)当成湿度打印,那确实经常是0,因为DHT11的湿度小数部分大多数时候就是0。
关于微秒延时,我再补充一下DWT的实现。DWT(Data Watchpoint and Trace)是Cortex-M3内核里的一个调试组件,它的CYCCNT寄存器可以计数CPU周期。通过它实现微秒延时非常精准,而且不受优化等级影响:
void DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; } void DWT_Delay_US(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }这个方案比SysTick更省事,因为SysTick可能被HAL库的HAL_Delay占用。
4.3 OLED显示乱码、花屏和镜像问题
OLED显示乱码的排查:
- 如果屏幕上全是雪花点或者满屏亮块,大概率是初始化序列不对,或者GRAM写入方式不匹配。检查一下你是不是用了SPI接口的OLED,但代码是按I2C写的——这两种接口的屏长得几乎一样,容易拿错模块。
- 如果字符显示出来是上下左右颠倒的,那基本就是段重映射和扫描方向的问题。把初始化序列里的
0xA1改成0xA0,0xC8改成0xC0,排列组合试一遍就能找到正着的方向。 - 如果字符显示正常,但位置不对,比如从中间开始显示,看看页地址和列地址的设置。OLED_Set_Cursor这个函数必须同时设页地址和列地址,有些人只设了列地址没设页地址,所有内容都写在第一页上,就会重叠成一坨。
花屏还有一个特殊原因:I2C通信本身没问题,但OLED的电源电压不稳。OLED的电荷泵开启后电流会增大,如果你用的是电脑USB口供电,电压可能被拉低,导致屏闪或者花屏。换独立5V/3.3V电源,或者在电源引脚旁边加一个10uF的电解电容和一个100nF的瓷片电容,能缓解这个问题。
4.4 STM32烧录失败:SWD连不上和JTAG禁用问题
烧录失败这个坑我必须单独说,因为它太常见了。有两种典型情况:
第一种:第一次烧录没问题,烧完之后第二次就报“No target connected”。这种基本都是程序里配置了GPIO,把PA13/PA14/PA15/PB3/PB4这些调试脚给重新定义了,或者把SWD功能关了。CubeMX里在SYS选项卡设置Debug为Serial Wire,这样SWD引脚就不会被重映射,下载器还能连上。如果已经连不上了,解决办法是把BOOT0引脚拉高,重新上电,让芯片进入ISP模式,这时候SWD是恢复的,擦除一下Flash再重新下载。
第二种:下载器本身驱动有问题。ST-Link/V2在Win10/Win11上有时会因为驱动冲突,导致电脑识别不到。解决办法是在ST官方工具STM32CubeProgrammer里重新安装驱动。如果你用的是盗版ST-Link或者那种9块9的DAP-Link,固件不稳定的情况更多,建议先换一根数据线试试——没错,数据线问题导致下载失败的案例我见过不止一次。
我在实际使用中最推荐下载方式的是ST-Link Utility(STM32 ST-LINK Utility),它可以直接擦除、编程、校验整个Flash,对于排查程序运行异常非常有用。程序跑飞了、芯片锁死了,用它一键全片擦除,干干净净。
4.5 这个压缩包项目的可扩展方向
把这个基础项目吃透之后,后续扩展的空间很大,这里列几个我实际做过的方向:
- 换成DHT22或者SHT30传感器,只需要改驱动层,显示层完全不用动。
- 增加历史温度曲线显示,OLED刷新改成局部刷新,每秒画出温度变化的像素点,就能做一个微型趋势图。
- 增加按键切换到不同显示页面,比如一页显示温湿度,一页显示最大值最小值。
- 把串口打印加上,通过USART把数据发到电脑的串口助手,方便调试和分析。
- 加一个ESP8266或者ESP32模块,通过串口把温湿度数据传到云端——这就是智能家居的雏形了。
- 把OLED换成1.3寸的I2C OLED(SH1106驱动),代码大部分可以复用,只要改初始化序列和窗口设置函数就行。
从底层驱动的角度,你可以深入研究一下CubeMX的HAL库源码,看看I2C的状态机是怎么实现的、DMA模式怎么处理、中断回调里能不能触发数据读取。这些内容才是从一个抄代码的新手变成能独立做项目的人的分水岭。
5. 最后想说的话
这个压缩包项目,说大不大,说小也不小。它没有用到复杂的外设,没有RTOS,没有复杂的算法,但它把嵌入式开发最基本的几个环节全部串起来了:芯片配置、GPIO操作、外设通信协议、时序控制、显示驱动、以及最重要的——排查问题的思路。
我的建议是,拿到任何项目压缩包,不要急着烧录看效果,先用CubeMX打开工程文件(或者自己重建一份),把每一行代码都过一遍,搞清楚时钟怎么配的、引脚怎么选的、I2C速率设了多少、DHT11时序怎么实现的、OLED初始化序列里每一条命令干嘛用的。全部搞清楚之后,再烧录到板子上。这样就算出问题,你也知道往哪儿查。
如果你在调试过程中遇到和我当年一样的坑——OLED不亮、DHT11读不出来、烧录第二次就失败——欢迎回来对照这篇文章,一条条排查。实在解决不了,可以看看板子型号、引脚接线、报错信息这三个信息,很多时候问题就藏在最基础的细节里。
本文还有配套的精品资源,点击获取