STM32F103实战精讲:从工程搭建到DHT11温湿度驱动开发 STM32F1系列这颗芯片我是从大学实验室一路用到现在。当年用它调DHT11温湿度传感器第一次看到串口助手刷出稳定的温度和湿度数据时那种成就感到现在还记得。十几年过去了ARM Cortex-M0/M4/M7满天飞可F1依然是很多人入门嵌入式、做毕业设计、甚至量产小设备的首选。为什么便宜、资料多、外设丰富而且生态已经成熟到“随便一搜都是答案”。这篇文章我不打算念手册而是以我实际折腾F103的经验为主线从选型、工程搭建、核心外设讲到DHT11驱动的完整实战最后把我踩过的坑一并整理给你。不管你是刚开始学STM32的新手还是做项目需要快速上手的工程师按这篇的思路走F1系列你基本能玩转大半。1. STM32F1系列十年不倒的“初学者之王”1.1 这系列芯片到底什么来头STM32F1基于ARM Cortex-M3内核工作主频最高72MHz这个性能放在今天确实不算亮眼但胜在均衡。它不像M0那样精简到有些外设要省也不像M4那样带着DSP和FPU把人吓退M3处在“够用且好学”的甜蜜点。指令集是Thumb-2单周期乘法适合跑裸机逻辑、FreeRTOS复杂点的算法也能勉强扛住。我理解F1的思路是这样它把“通用性”放在了第一位。片上集成的资源非常全——Flash从16KB到1MBRAM从4KB到96KBGPIO、定时器、USART、I2C、SPI、ADC、DAC、DMA、CAN、USB全部覆盖。你做一个温湿度采集、电机控制、CAN通信设备F103一种芯片全搞定不用在几种型号之间反复横跳。另一个隐藏优势是生态。Keil、IAR、GCC都支持标准外设库虽然停止更新但例程满天飞HAL库由CubeMX生成代码规范还自带错误处理。这就意味着你在开发中遇到的问题大概率早有人踩过并在论坛留下了帖子。1.2 热门型号怎么选一张表看清F103家族F1系列的“C”是中等容量“R”是大容量“Z”是超大容量实际引脚数不同。选型号时先看封装引脚数再看Flash、RAM千万别拿错。这里整理几个我实际用过的型号型号封装FlashRAM常用场景STM32F103C8T6LQFP4864KB20KB入门首选、温湿度计、小型控制器STM32F103RBT6LQFP64128KB20KB带显示屏或稍多IO的项目STM32F103RCT6LQFP64256KB48KB跑RTOS、存字库、功能较多的产品STM32F103ZET6LQFP144512KB64KB做开发板、资源富余的教学板我自己买的第一个是C8T6淘宝几块钱一片当时是拿来点LED、跑串口后来升级项目才换RCT6。这里有个教训C8T6的Flash标称64KB实际小容量型号在某些批次可以通过配置读到128KB但你千万别依赖这个“隐藏Buff”产品量产如果用C8T6老老实实按64KB规划代码空间否则编译出来烧进去运行到一半跳飞了排除问题会非常痛苦。选型最终看三点引脚要不要够、Flash/RAM装不装得下固件、封装能不能手焊。如果都要最稳的直接上RCT6大多数项目这个档位都够用。1.3 为什么DHT11偏偏爱搭F103DHT11是单总线数字温湿度传感器数据脚只有一根和F103的任意普通GPIO都能接。这里的“任意”很关键——F103没有强制占用特定引脚你可以把DHT11挂在PA5、PB7或者PC13上代码里改一个GPIO宏就行。加上F103主频够高用GPIO翻转模拟时序完全无压力。实际项目中我还常用DHT11和F103搭配做小气象站、孵化箱、粮仓监控这类场景。F103负责采集、存储、显示、上传DHT11负责感知环境温湿度。这套组合做出来的产品硬件成本不到二十块功能完整度却很高这也是很多创客和电子爱好者愿意用它做原型的原因。2. 开发环境与工程模板从零搭建一个能跑的项目2.1 三种开发方式的选型F103的开发方式大致有三种寄存器、标准外设库、HAL库。我三种都写过感受很直接。寄存器开发直接操作寄存器地址代码最底层执行效率最高但代码可读性差。适合追求极致性能的驱动开发或者你想彻底搞懂芯片内部结构。标准外设库官方封装好的函数库把寄存器操作包装成GPIO_Init()、USART_SendData()这类函数。学习曲线平滑代码利落当年的经典教程基本都是这个风格。唯一问题是官方已经停止维护新芯片不再支持。HAL库配合STM32CubeMX使用配置图形化代码自动生成。它引入了超时机制和抽象层代码量大、执行效率略低但可读性和可移植性很好做项目和做产品都比标准库省心。我不推荐新手上来就死磕寄存器。现在的共识是入门用HAL库进阶后回去读寄存器代码加深理解。CubeMX把时钟、引脚、中断、DMA这些初始化全部可视化完成你省下写配置代码的时间正好用来理解外设的工作原理。2.2 CubeMX生成工程的关键配置CubeMX生成F103工程时最值得关注的有三块RCC时钟配置、调试接口选择、外设初始化。RCC配置要选“Crystal/Ceramic Resonator”这样才会启用外部高速晶振HSE。HCLK我在“Clock Configuration”页面里直接拉到72MHz软件会自动算好PLL倍频和总线分频系数。很多新手不做这一步直接跑默认的8MHz后面驱动外设会因为频率不符导致时序错误DHT11这种传感器尤其明显。调试接口要勾选“Serial Wire”而不是“JTAG”因为JTAG默认占用的PA13、PA14、PA15、PB3、PB4如果你把这些引脚当GPIO用又没关JTAG功能GPIO配置会失效。我见过一个项目把PB3当按键输入死活读不到电平最后发现JTAG还开着。外设初始化在生成代码后会自动出现在main()里但你要留意生成的顺序HAL_Init()、SystemClock_Config()、MX_GPIO_Init()、MX_USARTx_Init()这一套你不要在系统时钟配置之前就去初始化外设否则外设时钟树还是默认的操作的结果完全不可控。2.3 调试器连接与下载的坑下载调试用ST-Link或者J-Link都行。ST-Link便宜和F103兼容性最好。接线就四根SWDIO、SWCLK、GND、3.3V。Keil里在“Debug”设置选ST-Link再在“Flash Download”勾选“Reset and Run”下载完程序能直接跑起来。实际接线时有个细节SWDIO和SWCLK要在板上各放一个10k左右的上拉电阻到3.3V。不焊这两个电阻也能下载但下载速度稍微高一点就容易出错我的板子第一版就是没加上拉每次下载都有几率报RDDI-DAP Error。后来老老实实加上电阻下载一次成功一次。还有一种情况是下载器识别不到芯片先检查芯片供电再用万用表量一下SWCLK引脚是否有信号波形很多时候是杜邦线接触不良或者线序错了。3. 吃透F1核心外设时钟、GPIO、定时器、串口3.1 时钟树72MHz怎么来的F103的时钟系统是我觉得理解整颗芯片的关键。它内部有HSI内部8MHz、HSE外部4-16MHz晶振、PLL锁相环和两级总线分频器APB1、APB2。标准配置下8MHz外部晶振作为HSE输入PLL倍频到72MHz作为系统时钟SYSCLK。之后AHB预分频器把72MHz给到GPIO、DMA等APB2最高72MHz接USART1、ADC、定时器1APB1最高36MHz接USART2/3、I2C、SPI、定时器2/3/4。注意APB1的定时器时钟实际是72MHz因为定时器2/3/4的时钟源会再乘2这是F103一个很容易被忽略的设计。这个“为什么”很重要定时器的PWM频率和串口波特率都依赖这条时钟链。如果你把外部晶振换成其他频率或者CubeMX里改了PLL倍频系数却没同步更新定时器预分频值和串口分频值你得到的外设工作频率一定是错的。我见过一个项目把晶振换成12MHzPLL倍频照旧结果串口输出乱码排查了半天才发现是时钟配置没同步。3.2 GPIO八种模式怎么选不糊弄F103的GPIO有八种配置模式总结下来就是输入四种、输出四种。我经常用这样一张表来做选择模式用途典型场景浮空输入引脚电平不确定外部决定电平读取外部数字信号上拉输入默认高电平外部拉低触发接按键到GND下拉输入默认低电平外部拉高触发接按键到VCC模拟输入不经过施密特触发器ADC采样引脚推挽输出强驱动高/低电平驱动LED、继电器控制信号开漏输出只能拉低高电平靠外部上拉I2C、电平转换复用推挽由外设控制引脚UART TX、PWM输出复用开漏外设控制开漏输出I2C、外部中断触发线新手最容易错的是按键输入模式。很多人初始化成浮空输入外部没接上拉电阻按键松开时引脚电平来回漂移读到的按键状态完全随机。正确做法是输入模式内部上拉按键另一端接GND按下是低电平松开高电平稳得很。还有一点要提醒F103的GPIO翻转速率要配合外设需求设置。输出模式里有两档翻转速度2MHz、10MHz、50MHz。LED这种低频设备用2MHz就够了但你驱动DHT11这种需要快速翻转信号线的话建议选10MHz以上。速度设置过低信号边沿会变缓时序严格时读错数据很正常。3.3 定时器/PWM产生一个精确频率的重点F1系列定时器资源挺丰富高级定时器TIM1/TIM8带互补输出和刹车功能通用定时器TIM2/3/4/5支持PWM和输入捕获。最常用的PWM频率计算公式是PWM频率 定时器时钟频率 / (预分频PSC 1) / (自动重装载ARR 1)以TIM3为例时钟源是72MHz我让PSC71ARR999那么PWM频率 72_000_000 / 72 / 1000 1000Hz也就是1kHz。占空比由比较寄存器CCR决定CCR500时占空比50%。这段计算逻辑不复杂麻烦的总是PSC和ARR边界值——有人把PSC72ARR1000得出来的频率就变成988Hz误差虽然不大但强迫症患者要尽量选整除关系。配置时还要注意一个优先级。HAL库的HAL_TIM_PWM_Start()必须在__HAL_TIM_SET_COMPARE()设置占空比之后执行否则第一轮输出可能是0。另外启动PWM前MX_TIM3_Init()里要把“Pulse”默认值填一个非零数不然你启动后忘了设置CCR引脚会一直低电平怎么调都不出波。3.4 串口DMA收发数据不掉数据的秘诀串口是调试和通信的命根子。F103的USART1挂在APB2总线上最高72MHzUSART2/3挂在APB1上最高36MHz。CubeMX里配置波特率选“115200 Bits/s”字长8位无校验停止位1位。中断要勾选USART1 global interrupt这样HAL_UART_Receive_IT()才能有效。但裸的串口中断收发有个问题高频数据时MCU频繁进中断容易丢失字节。我的经验是用DMA搬运串口数据。DMA是外设和内存之间的搬运工数据到了串口寄存器DMA自动把数据写到内存数组里不需要CPU介入。配置方法不复杂CubeMX的USART设置里把“DMA Settings”区的“Add”分别添加TX和RX通道Mode选循环模式。生成代码后调用HAL_UART_Receive_DMA(huart1, 数据缓冲区, 缓冲区长度)之后收数据在HAL_UART_RxCpltCallback回调函数里处理就行。F103的DMA通道有限USART1_TX对应DMA1通道4USART1_RX对应DMA1通道5别选错。这类配置在调试里有个心得直接用串口空闲中断IDLE处理不定长数据是嵌入式老手的常规操作。F103的USART支持检测总线空闲状态配合DMA可以实现“一帧数据到达后自动触发处理”典型的项目参考流程是这样的使能USART的IDLE中断和DMA接收在中断服务函数里判断IDLE标志根据DMA计数器计算本次接收到的数据长度处理完一帧数据后重新开启接收。我第一次实现这个逻辑后再处理GPS模块的NMEA串口数据就一点不慌了。同样地如果你在做WiFi模块或4G模块的AT指令解析这个技巧能帮你省掉大量阻塞等待时间。4. DHT11温湿度传感器驱动实战单总线时序才是核心4.1 先看数据手册DHT11的数据格式与握手时序DHT11的通信协议叫单总线数据线只需要一根主机和传感器通过电平的时序来交换信息。它上电后有1到2秒的稳定时间这段时间内不能发任何指令否则传感器不响应。数据格式是40位湿度整数8位、湿度小数8位、温度整数8位、温度小数8位、校验和8位。比如收到00110101 00000000 00111000 00000000 01101101湿度十进制是0x3553%温度是0x3856℃校验和0x6D正好等于前面四个字节之和。校验对不上这帧数据就得丢。握手过程分为三步主机把数据线拉低至少18ms再释放并延时20~40us这是起始信号DHT11检测到起始信号后先拉低80us响应再拉高80us准备发送数据随后每个bit的传输是拉低50us表示起始释放后拉高26~28us表示“0”拉高70us表示“1”。这里的关键在第三步读bit时你从数据线变高开始计时高电平持续40us以内判定“0”超过40us判定“1”。实际操作中我用一个循环反复读取引脚同时用延时函数测量高电平宽度。4.2 驱动代码实现逐位解析完整Demo我使用HAL库和GPIOC端口测试过DHT11的数据线接到了PC13上。用一个引脚高低电平切换加延时函数模拟时间宽度。第一步定义延时函数微秒级延时。void delay_us(uint32_t us) { uint32_t n us * 72; // 72MHz约等于1us72个时钟周期 while (n--) { __NOP(); // 空操作 } }us * 72这个数值在不同编译器优化等级下会有偏差充其量只是一个工程近似值真正要求精准的地方要改用定时器或SysTick。做DHT11驱动时这个精度基本够用。第二步主机发送起始信号。void DHT11_Start(void) { GPIO_InitTypeDef gpio_init {0}; // 先配置为输出模式 gpio_init.Pin GPIO_PIN_13; gpio_init.Mode GPIO_MODE_OUTPUT_PP; gpio_init.Pull GPIO_NOPULL; gpio_init.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOC, gpio_init); // 拉低18ms HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET); delay_us(18000); // 释放并延时20~40us HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET); delay_us(30); }第三步切换为输入模式并读取引脚。uint8_t DHT11_ReadByte(void) { uint8_t data 0; GPIO_InitTypeDef gpio_init {0}; // 重新配置为输入模式 gpio_init.Pin GPIO_PIN_13; gpio_init.Mode GPIO_MODE_INPUT; gpio_init.Pull GPIO_PULLUP; HAL_GPIO_Init(GPIOC, gpio_init); for (int i 0; i 8; i) { // 等待低电平结束每bit起始的50us低电平 while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_RESET); // 延时40us后读取电平判断0还是1 delay_us(40); if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_SET) { data (data 1) | 1; // 高电平是“1”等它结束 while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_SET); } else { data (data 1); } } return data; }这个读取函数的重点在于DHT11的每一位数据都是以50us低电平开始的然后才是26~28us逻辑0或70us逻辑1的高电平。我在代码里先“while读低电平”等到低电平结束这样能对齐每一位的起始边界再延时40us这时如果引脚仍为高就说明这一位的高电平宽度超过40us判断为“1”。这个时序逻辑实测可用但每个人的延时函数精度不同建议动手前先看一眼DHT11数据手册里那个时序图。第四步在主函数中调用。uint8_t dht11_data[5] {0}; void DHT11_Read(float *humidity, float *temperature) { uint8_t checksum 0; DHT11_Start(); // 发送起始信号 // 等待DHT11响应先低80us再高80us while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_SET); while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_RESET); while (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) GPIO_PIN_SET); for (int i 0; i 5; i) { dht11_data[i] DHT11_ReadByte(); } checksum dht11_data[0] dht11_data[1] dht11_data[2] dht11_data[3]; if (checksum dht11_data[4]) { *humidity (float)dht11_data[0] (float)dht11_data[1] / 10.0f; *temperature (float)dht11_data[2] (float)dht11_data[3] / 10.0f; } }这段代码里用到两次while等待响应信号的写法相当于我先等引脚变低再等引脚变高确保传感器已经进入发送数据状态。等每个字节时也有一个对齐起始位置的信号所以DHT11_ReadByte里的while循环是完全有必要的不要因为觉得耗时就去掉。去掉后读取的数据经常错位而且错得很随机排查起来特别费劲。4.3 环境监测小应用的整合建议DHT11的单次读取很简单但放到实际环境监测项目中读取频率要控制。DHT11手册明确写了采样周期不能低于1秒也就是最多每秒读一次。你把它放在循环里连续快速读取第二三次以后基本全是超时或者校验错误。正确的是循环里每轮读取后延时500ms或者用定时器保证1秒以上的间隔。整合应用时可以把F103的USART1接电脑调试DHT11挂PC13OLED显示屏挂I2CF103的I2C外设很多人说难用其实加个上拉电阻初始化时序对了就稳。采集到的温湿度刷新在OLED上实时还能把数据通过串口上传到上位机做曲线一套小型环境监测站就成型了。4.4 常见DHT11疑难数据问题的排查代码没问题、接线没问题但还是读不到数据的情况我也遇到过。这边整理成排查清单现象可能原因解决方法数据全为0或全为255数据线没接对 / GPIO模式初始化错重查引脚和GPIO配置确认输入模式下拉无校验恒失败延时精度不匹配用逻辑分析仪看时序调整delay_us的循环参数温度正常、湿度偏差大DHT11靠近发热源 / 传感器老化参考值校准或换传感器后放在合理位置前几次读不到等几秒恢复上电稳定时间不够在DHT11_Start前延迟至少1秒数据偶发跳变线太长或环境干扰数据线尽量短加一个上拉电阻到3.3V最后这条线长问题我之前做样机时DHT11和F103隔了50cm的杜邦线读出来的数据经常飘个一两度、跳几个百分点。把线剪到15cm以后数据就很稳定了。传感器信号线尽量短和远离电源线这一条在几乎所有数字传感器上都适用。5. 调试实录这些坑我替你先踩了5.1 芯片锁死与BOOT0急救F103最常见的一个“事故”是你下载了一个把SWD引脚复用成普通GPIO的程序芯片还能下载但之后重新上电下载器一连接Keil就报No target connected。遇到这种情况不用慌F103设计了BOOT引脚救场。把BOOT0拨到1BOOT1保持0复位上电此时芯片从系统存储器启动不再执行用户程序先用ST-Link连上把Flash全擦除BOOT0拨回0重新复位芯片恢复正常。这个操作我重复了无数次。值得强调的原理是启动方式通过BOOT0和BOOT1引脚选择第三种“从SRAM启动”很少用。物理开关调BOOT0之前先断电防止带电拨码损坏启动配置逻辑。5.2 串口乱码与时钟不匹配串口调试打印乱码我把它分成两类。一类是软件原因CubeMX里配置的时钟源和实际硬件不一致。你板子上焊了8MHz晶振代码里却把LSE当HSE或者时钟配置没选HSE系统用的还是内部8MHz RC振荡器实际频率可能是8.1MHz左右而串口波特率按72MHz系统时钟来算当然会乱。排查方法进调试模式在SystemClock_Config()设断点看RCC相关寄存器的值是否符合预期。另一类是硬件原因USB转串口模块的晶振不准。便宜的CH340模块用无源晶振如果焊接不良或者电容配错实际波特率会偏移调到9600时乱码调到115200时反而不乱。这时候换个好的串口模块或者干脆用示波器量一下TXD引脚上的波形。还有个小问题F103的USART支持的最高波特率不是无限大。APB2上的USART1最高可以到72MHz/164.5Mbps实际能稳定跑多高还与电气环境有关APB1上的USART2/3最高是36MHz/162.25Mbps。盲目开高波特率大概率通信不稳定正常的项目用115200就足够快了。5.3 定时器精度问题与DHT11读数的玄学故障有个很典型的“玄学”故障程序加了HAL_Delay()之后就正常去了就开始乱。表面上看像是DHT11时序问题实际上很可能是你读取过程的延时函数依赖的主循环频率被其他代码影响了。我遇到过一版程序在while(1)里既跑OLED刷新又跑DHT11读取然后OLED刷新是大段阻塞刷屏DHT11读着读着就被卡了一下整个位序全乱了。最终的解决办法是DHT11读取期间严格禁止其他外设操作读取结束再刷新OLED。如果追求更稳定的定时精度使用硬件定时器做微秒延时是正道。配置一个TIM为1us周期读取时打开计数值通过__HAL_TIM_GET_COUNTER()计算时间差配合while循环判断高低电平的时间宽度。这个方案的时序误差比纯软件延时小得多适合对读取稳定性要求更高的场合。F103的SysTick也是一个高精度时间基准但它会被HAL_Delay()占用。所以当你既想用SysTick做调度又想让它提供us级别的延时就得自己实现一个比HAL_Delay更高优先级的底层延时函数。这里的关键是模型清晰一个时间基准多个延时接口不要谁都去改SysTick的计数值。5.4 用逻辑分析仪看时序问题如果你刚开始调试DHT11这类单总线设备我强烈建议你买一个几十块钱的逻辑分析仪。它不需要太多通道8通道就够采样率100MHz以上。把DHT11的数据线接到逻辑分析仪通道抓一段读取过程你就能直观看到主机拉低18ms、释放、传感器响应、数据位的波形。我第一次用逻辑分析仪看DHT11时序时发现自己的延时函数实际延时比预期大了一倍多因为编译器开启-O2优化后NOP循环被部分展开耗时变了。这类问题靠肉眼看串口是发现不了的波形图一拉出来问题一目了然。对于F103这种没有内置调试波形输出的芯片逻辑分析仪几乎是排查时序类问题的最佳工具。在Keil环境里调试时我会把关键引脚在关键时刻拉高拉低用逻辑分析仪同步看F103和DHT11双方的交互这样就能精确定位是哪一端没按协议走。5.5 电源与干扰问题总结F103的3.3V供电很多人直接用USB转串口模块的3.3V但那个电流输出能力有限一旦同时驱动OLED、WiFi模块、蜂鸣器电压会被拖到3V以下芯片直接复位。这不是程序Bug是硬件供电不足。我给F103做小系统板时用的是AMS1117-3.3线性稳压芯片输入5V输出3.3V。AMS1117最大输出1A但实际到800mA就明显发热所以给负载供电时留出裕量。DHT11的工作电流只有0.5mA左右可以忽略但WiFi模块峰值电流能到300mA以上这种负载就要单独供电或加大电容。电源退耦也很重要F103的每个VDD引脚旁边都要放一个100nF陶瓷电容靠近引脚系统总入口再放一个10uF电解电容。我在早期布局时省过这些电容结果系统靠近电机就会复位后来补上电容世界安静了。简单的经验是MCU供电线上没有退耦电容的板子我不做第二次。6. 写在最后的个人经验STM32F1系列陪我走过了很多项目从温度计、环境监测到小型的物联网网关。它虽然不是性能最强的但绝对是让你搞懂嵌入式底层的首选。很多工程师抱怨F1的外设设计老旧、DMA通道少、Flash小但在学习阶段这些“限制”反而迫使你去理解每一行代码背后的硬件逻辑而不是无脑堆API。如果你刚接触F103我的建议是先点亮一个LED跑通串口打印再接一个DHT11跟着这篇文章的流程走一遍。做完这三件事你对GPIO、时钟、延时、单总线协议的理解会远超看十篇教程的收获。等你熟练掌握F1系列之后再往F4或F0系列迁移你会发现大部分外设的配置思路都是相通的只是寄存器和库函数的名字变了而已。顺手多说一句DHT11只是起点等你掌握了F103可以接着试试DHT22、DS18B20、SHT30这些传感器会发现单总线、I2C、SPI这类通信协议的套路大体一致。祝你调试顺利一次通过。