STM32F103C8T6 HAL库驱动DHT11:从CubeMX配置到OLED显示的实战解析

STM32F103C8T6 HAL库驱动DHT11:从CubeMX配置到OLED显示的实战解析 1. 项目背景与硬件准备STM32F103C8T6作为经典的Cortex-M3内核微控制器凭借其丰富的外设资源和亲民的价格一直是嵌入式开发者的心头好。这次我们要用它来驱动DHT11温湿度传感器并通过OLED实时显示数据。这个项目特别适合刚接触HAL库的开发者因为整个过程会涉及到单总线通信、精确延时控制、I2C驱动等多个实用技能点。硬件清单里除了主角STM32F103C8T6最小系统板还需要准备这几样东西DHT11模块注意要买带PCB板的那种直接裸露的传感器不方便接线、0.96寸OLED屏幕I2C接口、USB-TTL模块我用的是CH340G芯片的以及ST-Link下载器。特别提醒新手DHT11有方向性凸起面朝向你时从左到右分别是VCC、DATA、NC、GND别接反了。我刚开始玩的时候就把电源接反过幸好这模块有保护电路没烧坏。接线方面有个小技巧虽然CubeMX生成的代码会自动配置引脚但建议先在原理图上确认PB6/PB7用作I2C1PA9/PA10用作USART1。DHT11的数据线我接在PB12这个引脚在最小系统板上容易找到。实际接线时OLED的VCC接3.3VSCL接PB6SDA接PB7DHT11的DATA线需要接10K上拉电阻到3.3V这个细节很多人会忽略导致通信失败。2. CubeMX工程配置详解打开CubeMX新建工程时记得选择STM32F103C8系列具体到C8T6型号。时钟配置是个重点我推荐使用外部8MHz晶振经过PLL倍频到72MHz主频这样后续的微秒级延时才能算得准。在Clock Configuration标签页里把HSE选为Crystal/Ceramic Resonator然后在PLL配置区把MUL设为9倍频注意系统时钟源要切换为PLLCLK。外设配置环节需要开启两个关键外设I2C1和USART1。I2C1模式选择I2C参数保持默认的100kHz就行OLED对速率要求不高。USART1配置为异步模式波特率115200这个速率在串口助手上显示比较舒服。GPIO配置里要把PB12设为GPIO_Output后续代码里会动态切换输入输出模式初始电平设为高。生成代码前有个重要设置在Project Manager标签页里把Toolchain/IDE选为MDK-ARM勾选Generate peripheral initialization as a pair of .c/.h files。这样每个外设都会生成独立的文件方便维护。我第一次用CubeMX时没注意这个选项结果所有初始化代码都堆在main.c里后期维护特别麻烦。3. DHT11驱动开发实战DHT11的通信协议看似简单实际调试时却容易踩坑。它的时序分为三个关键阶段起始信号、响应信号和数据传输。起始信号要求主机拉低总线至少18ms然后拉高20-40us。这里有个细节很多例程用HAL_Delay()实现毫秒延时但微秒级延时需要自己实现。我的方案是用SysTick定时器void Delay_us(uint32_t us) { uint32_t ticks us * (SystemCoreClock / 1000000); uint32_t start DWT-CYCCNT; while((DWT-CYCCNT - start) ticks); }记得在初始化时启用DWT单元CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk;数据读取阶段要注意电平判定的时间窗口。DHT11用高电平持续时间区分0和126-28us表示070us表示1。实测中发现STM32的GPIO读取速度很快但HAL_GPIO_ReadPin()函数有额外开销所以我的做法是直接操作寄存器#define DHT11_PIN_IN() {GPIOB-CRH ~(0xF 16); GPIOB-CRH | (4 16);} #define DHT11_PIN_OUT() {GPIOB-CRH ~(0xF 16); GPIOB-CRH | (3 16);} #define DHT11_READ() (GPIOB-IDR GPIO_PIN_12)校验机制也不能忽视。DHT11传输的5字节数据中前4字节的和应该等于第5字节。我在代码里添加了校验失败重试机制最多尝试5次uint8_t retry 5; do { if(DHT11_Read(data) SUCCESS) { if((data[0]data[1]data[2]data[3]) data[4]) break; } HAL_Delay(200); } while(retry--);4. OLED显示与数据融合OLED驱动我推荐使用u8g2库的简化版只保留SSD1306驱动部分。在CubeMX生成的I2C初始化代码基础上需要添加几个关键函数。写命令和写数据的函数要区分开void OLED_WriteCmd(uint8_t cmd) { uint8_t buf[2] {0x00, cmd}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDRESS, buf, 2, 100); } void OLED_WriteData(uint8_t dat) { uint8_t buf[2] {0x40, dat}; HAL_I2C_Master_Transmit(hi2c1, OLED_ADDRESS, buf, 2, 100); }数据显示部分建议做成两级结构底层是基本绘图函数上层是业务逻辑。比如温度显示可以这样实现void ShowTemp(float temp) { char str[16]; sprintf(str, %.1f℃, temp); OLED_ShowString(60, 1, str); }实际项目中我发现直接频繁刷新整个屏幕会导致闪烁更好的做法是局部刷新。比如温度值只有最后一位变化时只需要重写最后两个字符的位置。这需要维护一个显示缓存区比较前后帧数据差异。串口输出建议采用JSON格式方便上位机解析printf({\temp\:%.1f,\humi\:%.1f}\r\n, temperature, humidity);调试时遇到过I2C总线锁死的情况后来在代码里添加了总线恢复机制void I2C_Recovery() { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin GPIO_PIN_6|GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); for(int i0; i9; i) { HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); Delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET); Delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET); Delay_us(5); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET); } MX_I2C1_Init(); }5. 常见问题排查指南第一个容易出问题的是DHT11无响应。先检查硬件测量VCC和GND之间是否有3.3VDATA线是否接了上拉电阻。然后用逻辑分析仪抓取起始信号波形确认18ms低电平和20-40us高电平是否符合要求。如果没有逻辑分析仪可以临时改成用LED指示各阶段状态。I2C通信失败时先用万用表测量SCL和SDA线电压。正常时应为3.3V上拉后如果始终为低说明总线被锁死。这时候可以调用前面提到的I2C_Recovery()函数。OLED不显示还可能是因为地址不对SSD1306的地址通常是0x78或0x7A可以用I2C扫描程序确认。延时不准是个隐形杀手。建议在调试时输出SysTick的值来校准微秒延时函数。有个小技巧用PWM输出一个1MHz的方波然后用延时函数控制GPIO翻转用示波器测量实际周期。我实测发现72MHz主频下减去函数调用开销后每个nop大约消耗14ns。数据校验经常失败的话可以尝试降低系统时钟频率或者优化GPIO读取速度。DHT11对时序要求严格在while循环里判断电平变化时建议加上超时机制uint32_t timeout 1000; // 1ms超时 while(DHT11_READ() RESET timeout--) Delay_us(1); if(timeout 0) return TIMEOUT_ERROR;最后提醒一个STM32的坑PB3/PB4默认是JTAG功能如果要用作普通GPIO需要在初始化时先禁用JTAG__HAL_AFIO_REMAP_SWJ_NOJTAG();