ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

STM32F1嵌入式开发实战:从选型到DHT11温湿度传感器驱动

STM32F1嵌入式开发实战:从选型到DHT11温湿度传感器驱动 掏了这么多年嵌入式很多人问我一个问题现在芯片都卷到Cortex-M7、双核、NPU了STM32F1这种2013年之前就在量产的“老古董”还值得花时间学吗我的回答是如果你要做成本敏感的工控产品、稳定可靠的传感器节点或者刚踏入嵌入式大门准备打基本功STM32F1系列依然是那个不踩雷的选择。2025年的今天用F1搭一颗DHT11做环境监测的项目照样能出活儿而且供应链稳定、资料多到看不完。这篇文章就把F1系列的选型、硬件设计、固件库选择到DHT11温湿度传感器驱动实战一次讲透尤其是DHT11这颗“看着简单、上手就翻车”的传感器手把手捋一遍。1. 为什么2025年了STM32F1依然是性价比之王1.1 先弄清楚F1到底是个什么样的产品线STM32F1是意法半导体基于ARM Cortex-M3内核推出的主流32位微控制器系列主频最高72MHz。对比现在动辄几百兆赫兹的芯片它确实不快但它的外设配置放在今天依然能打最多5个USART、2个I2C、3个SPI、2个高级定时器TIM1/TIM8带刹车功能天然适合电机控制、最多3个12位ADC部分型号还带CAN、USB全速设备F105/F107互联型甚至集成了USB OTG和以太网MAC控制器。做一个小型工业控制器、环境采集模块、简单HMI这套外设绰绰有余。F1内部也分几个子系列很多人没搞清楚就用错了STM32F101基础型主频36MHz外设精简适合纯IO控制、低成本场景。STM32F102在F101基础上增加USB全速从机适合USB小设备。STM32F103增强型主频72MHz外设最齐全出货量最大绝大多数教程和开源项目指的就是它。STM32F105/F107互联型额外增加USB OTG和以太网MAC适合需要组网或高速通信的产品。所以说“F1”和“F103”在白话里基本等同至少F103是F1的代表选型时绝大多数人也默认从这个子系列起步。1.2 F1不可替代的三个理由F1系列能在市场上活十几年绝不是仅仅因为“便宜”。我自己的项目经验里客户换过芯片平台换完又换回来原因无非下面三点。第一个理由是成本与生命周期。工业产品的采购逻辑永远是“谁能在五到十年供货周期里不涨价、不砍料”而F1系列出货量极其庞大晶圆产能稳定单价常年被压得很低。STM32F103C8T6批量拿货能到几块钱人民币做温湿度变送器、电量采集模块、健身器材主控这种毛利不高的产品一颗芯片省两块钱一年几万台就是几十万的利润。第二个理由是学习资源和人才池。从寄存器手册到标准外设库、HAL库到论坛里十几年的历史帖F1系列的资料密度可以说是整个MCU行业最高。招应届毕业生来面试十个有八个在大学实验室碰过F1。硬件排障、软件调试遇到问题搜索引擎一搜就是上百个相同场景的案例这点很多新型号芯片做不到。第三个理由也是最容易忽略的72MHz真的够用。很多研发喜欢把主频当信仰觉得换F4/F7就万事大吉。但实际上做传感器轮询、Modbus通信、继电器动作、数码管显示CPU负载连20%都用不到。我接过一个农业大棚项目12路DHT11温湿度采集加一个RS485上位机通信F103C8T6跑得轻轻松松板子不发烫抗干扰也不错客户用了三年没出过通信问题。主频高不代表产品好稳定、简单、易维护才是现场工程的价值。2. 芯片选型与硬件设计别让原理图埋雷2.1 一张表看懂主流F103型号F103子系列覆盖了从低到高的多种封装和容量很多新手一上来就选大容量ZET6“以绝后患”结果板子面积大了、四层板成本也上去了。选型有明确的递减顺序引脚数够不够Flash够不够外设有没有。先把这三件事想清楚再决定用哪颗。型号封装FlashSRAM主要特点适合场景STM32F103C8T6LQFP4864KB20KB价格最低、资源均衡传感器节点、小控制板、入门学习STM32F103RBT6LQFP64128KB20KBIO数量中等、容量适中小型HMI、电机控制STM32F103RCT6LQFP64256KB48KB大容量、IO多中型控制器、带LCD和通信协议栈STM32F103VET6LQFP100512KB64KB引脚丰富多路采集、扩展功能较多的产品STM32F103ZET6LQFP144512KB64KB旗舰型号复杂控制、学习开发板常用这里多说一句C8T6的数据手册标注Flash是64KB但不少批次实际有128KB很多人在网上说“C8T6可以当128K用”。我个人的建议是学习阶段你随便折腾做产品千万别依赖这个隐藏容量写代码按64KB规划超了就换RCT6。硬件工程师不赌运气这是原则。2.2 硬件设计里三个高频翻车点在自己画F1最小系统板的时候有三个地方最容易出错每个都是真实项目里教过学费的。第一个是复位电路。F1的NRST引脚内部已经有上拉电阻和滤波结构外部只需放一个0.1uF电容到地就能可靠复位。但网上很多老原理图喜欢加“10k电阻104电容”看起来有模有样实际遇到电源慢上电或者外部干扰时反而会出现复位不彻底、程序跑飞的现象。按官方手册推荐的来接别画蛇添足。第二个是BOOT0和BOOT1的处理。正常运行用户程序时BOOT0必须拉低到GNDBOOT1任意。如果需要在板上用串口ISP下载程序则要把BOOT0跳到3.3V复位后进入引导程序。很多新手第一次做板子BOOT0悬空了导致芯片始终进不去用户程序现象是“明明烧录成功上电却不运行”。这个小问题排查起来却非常费时间画原理图时一定要确认。第三个是晶振准备电路。F1的HSE外部高速时钟一般接8MHz晶振配两个15pF到20pF的负载电容误差控制在20ppm以内。这个电路本身很简单但PCB布局一旦离MCU过远或者旁边跑着高速数字信号线晶振就容易不起振。我踩过一次坑板子打样回来程序死活跑不起来用示波器看OSC_OUT波形像一团噪声最后发现是晶振走线绕了很远和一根SPI时钟线并排走了几厘米。后来改成靠近MCU引脚、包地处理问题立刻消失。电源去耦也不能省每个VDD引脚就近放一颗100nF陶瓷电容VDDA引脚单独加一颗1uF和一颗10nF模拟地和数字地单点连接。很多稳定性问题不是芯片不行而是电源纹波直接把ADC精度和通信时序毁了。3. 固件库选择与工程搭建这一步入错后面全是泪3.1 标准外设库与HAL库到底怎么选现在接触F1的新人一上来就会遇到一个选择题用标准外设库SPL还是HAL库我自己的答案是新项目一律用HAL但存量代码和资料里标准库太多必须学会看懂。标准外设库是ST早期的官方固件库把寄存器操作封装成一个个函数比如GPIO_Init、USART_SendData。它的特点是代码直接、执行效率高、初始化逻辑一目了然很适合学习底层原理。缺点也很明显不同系列的API不兼容从F1换到F4所有外设的函数名和参数结构变了移植工作量非常大而且ST早已停止维护。HAL库则把外设抽象成统一的API比如HAL_UART_Transmit、HAL_GPIO_WritePin在F1、F4、F7上长得都差不多。换芯片时重写的主要是时钟配置和引脚映射业务逻辑基本能保留。代价是封装层级多、代码量大极端性能场景下会有额外开销。但对于F1这种72MHz跑传感器采集的应用HAL的性能损耗根本无感。如果你做的是简单裸机项目想省Flash和RAM可以学一下LL库——它像HAL和寄存器操作之间的折中调用轻量且API风格统一。入门阶段建议先用HAL库做通功能再用LL库优化资源循序渐进。3.2 CubeMX初始化工程到底靠不靠谱有人看到图形化配置工具就觉得“不自由”“不专业”我的看法正相反。F1开发里最容易出错的时钟树配置用手写寄存器格式极易翻车而STM32CubeMX能把RCC时钟树自动算好。用CubeMX生成的工程解决了F1开发一半的入门门槛。实际使用CubeMX时有几个细节要注意时钟树页面里检查HSE是否选择Crystal/Ceramic Resonator外部晶振频率填8MHz然后PLL倍频到72MHz。APB1总线时钟最高36MHzAPB2最高72MHz。CubeMX会自动分频但如果你手改代码改乱了USART波特率、定时器分频全都会跟着错。APB1分频为2、APB2分频为1这个配置后检查一下。调试接口务必选择SWD串行调试不然程序第一次烧进去之后第二个工程烧录时连接不上只能靠改BOOT模式救回来。中断优先级在CubeMX里不要随手改NVIC的抢占优先级配置错误会造成中断嵌套混乱DHT11这种时序敏感的外设最容易出幺蛾子。CubeMX生成的代码结构也比较清晰main.c里包含初始化和外设句柄具体业务逻辑可以写在用户代码区USER CODE BEGIN/END之间这样重新生成代码时不会覆盖你的修改。这个习惯从第一天就要养成否则每次重新生成工程都像一次代码迁移。4. 实操用F103驱动DHT11温湿度传感器从协议到代码4.1 DHT11的协议到底难在哪里DHT11是一颗成本非常低的数字温湿度传感器输出的数据走单总线。单总线不是标准通信协议没有时钟线全靠一根数据线的高低电平时序来传递信息对微控制器来说就是“用GPIO模拟时序”。一次完整通信的流程是这样的主机先把数据线拉低至少18ms这就是起始信号让传感器知道主机要读数了。我一般习惯拉低20ms更稳妥。主机释放总线拉高20到40us然后等待传感器应答。DHT11检测到起始信号后会把数据线拉低80us表示“收到”再拉高80us准备输出数据。随后连续输出40bit数据。这40bit的排列是湿度整数8位湿度小数8位温度整数8位温度小数8位最后是8位校验和。每一位数据都以50us低电平开头后面跟着一个高电平。这个高电平持续约26到28us表示逻辑0持续约70us表示逻辑1。知道了这个协议就明白DHT11真正的难点不是协议有多复杂而是对微秒级时间精度的要求。F1主频72MHz一次空循环延迟一点点累计到40个bit上判断就会全部错位。还有一点容易被忽略DHT11本身的采样速度很慢连续两次读取间隔至少1秒建议2秒以上。如果你在while循环里用200ms间隔去读经常读到异常数据。这个坑我遇到太多次了。4.2 GPIO怎么配置最稳妥DHT11的接线很简单VCC接3.3VGND接地DATA接任意GPIO但在DATA引脚和VCC之间必须加一个4.7k到10k的上拉电阻。为什么必须加因为DHT11的数据引脚是开漏输出只能主动拉低拉高靠外部上拉电阻完成。不加这个电阻数据线永远是低电平传感器永远不响应。在F1上我强烈建议把接DATA的GPIO配置成开漏输出模式。初始化的代码像这样GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_0; gpio.Mode GPIO_MODE_OUTPUT_OD; gpio.Pull GPIO_PULLUP; gpio.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, gpio);这里用开漏而不是推挽有一个非常实际的收益开漏模式下向引脚写1时引脚实际上是被释放成高阻态由外部上拉电阻把电平拉高写0时引脚被拉低。这样整段读取过程中完全不用切换GPIO模式读引脚电平随时可以读。推挽模式虽然也能驱动但读取数据前必须重新初始化GPIO为输入模式多一次切换代码繁琐时序边界也多一分不确定性。如果你实在不想加外部上拉可以把GPIO配置成推挽输出然后开启内部上拉来读但没有外部电阻边沿那么陡峭我实测下来抗干扰能力差一些。量产设计还是加上那颗4.7k电阻这是最省心的方案。4.3 微秒延时整个驱动的命门HAL库的HAL_Delay只能做毫秒级延时而且依赖SysTick中断。DHT11需要的是微秒级时序必须自己写延时函数。最常见的错误写法是for循环空转void Delay_us_bad(uint32_t us) { for (uint32_t i 0; i us * 10; i); }这种写法的问题有三个一是不开编译器优化时勉强能用开了-O2优化后空循环可能被直接优化掉延时失效二是延时大小依赖主频、编译器版本、变量类型这些看不见的因素换一颗芯片或换个IDE整个驱动就要重新调三是不具备可移植性。我在项目里用的是基于SysTick硬件计时器的延时函数代码如下void Delay_us(uint32_t us) { if (us 0) return; SysTick-LOAD (uint32_t)(us * (SystemCoreClock / 1000000)) - 1; SysTick-VAL 0; SysTick-CTRL | SysTick_CTRL_ENABLE_Msk; while (!(SysTick-CTRL SysTick_CTRL_COUNTFLAG_Msk)); SysTick-CTRL ~SysTick_CTRL_ENABLE_Msk; }原理是给SysTick加载“需要延时多少个系统时钟周期”的计数值然后等计数器减到0。在72MHz主频下SystemCoreClock/1000000等于72所以延时1us等于让SysTick数72次。这里注意SysTick是一个24位计数器最大计数是16777215按72MHz算一次性延时us不能超过约233016us。DHT11通信里最长延时也就几百微秒完全够用。如果你在CubeMX工程里同时用HAL_Delay作为系统时基SysTick会被打断我建议在读取DHT11数据位的整个流程里暂时关闭全局中断。DHT11一帧数据不超过5ms关中断不会影响系统实时性但能防止高优先级中断把电平采样时机打乱。读取完成后马上恢复中断。4.4 完整驱动代码仔细看注释这些坑我都踩过下面给出一个完整的F1 HAL库的DHT11读取驱动可以直接抄进自己的工程。先写单字节读取函数#define DHT11_PORT GPIOA #define DHT11_PIN GPIO_PIN_0 static uint8_t DHT11_ReadByte(void) { uint8_t data 0; for (uint8_t i 0; i 8; i) { // 每一位数据都以50us低电平开始先等待低电平结束 uint16_t timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (timeout 60000) return 0xFF; } // 延时约40us等到电平稳定后判断高电平是否还在 Delay_us(40); data 1; if (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { data | 1; } // 等待当前位的高电平结束 timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (timeout 60000) return 0xFF; } } return data; }然后写完整的读取函数里面包含起始信号、应答判断和5字节数据校验uint8_t DHT11_ReadData(uint8_t *humi, uint8_t *temp) { uint8_t i, buf[5] {0}; uint16_t timeout 0; // 1. 主机发送起始信号拉低至少18ms HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(DHT11_PORT, DHT11_PIN, GPIO_PIN_SET); Delay_us(30); // 2. 等待应答。如果引脚一直是高说明传感器没响应 timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (timeout 60000) return 1; } // 应答低电平约80us等待结束 timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_RESET) { if (timeout 60000) return 1; } // 应答高电平约80us等待结束 timeout 0; while (HAL_GPIO_ReadPin(DHT11_PORT, DHT11_PIN) GPIO_PIN_SET) { if (timeout 60000) return 1; } // 3. 读取40位数据 for (i 0; i 5; i) { buf[i] DHT11_ReadByte(); } // 4. 校验前4字节之和等于第5字节 if ((buf[0] buf[1] buf[2] buf[3]) ! buf[4]) { return 2; } *humi buf[0]; // 湿度整数DHT11小数位一般为0 *temp buf[2]; // 温度整数 return 0; }这套代码里我最想强调的就是超时处理。很多DHT11例程里用while死等电平变化一旦传感器没接、线断了、或者芯片被干扰拉死程序就卡在循环里出不来。现场产品里这种卡死会让整个系统趴窝。加了超时之后最坏情况是返回一次错误码下一次循环再试系统的健壮性完全不一样。读取周期建议放在主循环里用定时器调度间隔1秒或2秒不要在收到错误后立刻重试否则连续读DHT11极易读到上一次通信的残留数据。我就是这样在产品上稳定跑了几万台。5. 调试实录三个让我差点掀桌的问题5.1 问题一DHT11湿度永远是0温度正常现象程序接上LCD温度显示24度正常湿度却一直显示0。一开始我以为是传感器坏了换了一颗新的还是一样。后来用逻辑分析仪抓波形才发现通信是成功的40bit数据也都收到了问题出在解析上。DHT11的数据格式里第一个字节是湿度整数第二个字节是湿度小数第三字节是温度整数第四字节是温度小数。DHT11本身精度有限湿度小数位几乎总是0。如果我在显示的时候直接打印buf[1]那当然永远是0。正确做法是把buf[0]当湿度整数buf[2]当温度整数。这个问题说出来觉得简单但在现场一旦先入为主认为是硬件故障就会绕很远。从这件事我养成了一个习惯遇到传感器数据异常先问自己“读到的字节和协议对得上吗”再问“是不是校验没过”最后才怀疑硬件。5.2 问题二程序烧进去串口打印全乱码现象用printf重定向到USART串口助手收到一堆乱码波特率明明配置成115200。F1的USART波特率由对应的外设时钟决定USART1挂在APB2总线上最高72MHzUSART2和USART3挂在APB1总线上最高36MHz。如果CubeMX时钟树里的APB1分频没有设在2导致APB1实际频率不是36MHz波特率算出来就翻倍或减半。还有一种情况代码里直接把USART2的波特率寄存器按72MHz的公式计算实际APB1只有36MHz也会乱码。排查方式其实很简单打开CubeMX看时钟树确认HSE是8MHzPLL倍频到72MHzAPB1分频2APB2分频1。把这个截图存下来每个新工程都检查一遍。凡是F1串口乱码十有八九出在这个地方而不是波特率或者引脚配置。5.3 问题三实验室好好的现场偶尔返回校验错误现象样机在办公室桌上跑了一周没问题发到客户现场用了一天就开始报校验错误。我排查了三个方向最后都找到了原因。第一个方向是数据线长度。DHT11手册建议连接线不要超过20米实际上超过2米就容易被周围的继电器、电源线干扰。现场机柜里强电线很多DHT11线在槽里跟着220V走了一段被干扰的概率极高。对策是把传感器线和动力线分开走或者换成屏蔽线。第二个方向是电源纹波。DHT11的VCC如果直接从开关电源取纹波大了会影响内部ADC采样导致数据跳动。给DHT11供电引脚加一颗0.1uF去耦电容问题改善了很多。第三个方向是读取频率。DHT11自身采样慢如果上位机下发指令频繁或者主循环里隔几百毫秒就调用一次读取函数传感器根本反应不过来返回的数据经常校验失败。把读取周期改成2秒单次读取异常时不要立刻补偿读取而是等到下一个周期再说问题彻底消失。我在排查这类问题时最喜欢用逻辑分析仪看DHT11数据线上的完整波形。把起始信号、应答信号、40bit数据的宽度全部展开哪一位的信号宽度不对一眼就能锁定是延时问题还是干扰问题。6. 写在最后个人经验分享作为一个拿STM32F1做了多年产品的工程师我最大的体会是F1也许不是最性感的芯片但它是最能让你学会“原理”的芯片。很多人在F1上理解了外设时钟树的逻辑、中断优先级的嵌套、GPIO开漏与推挽的差异到了F4、H7上只是换个壳继续用这些底层思维是通用的。如果你要开始F1 DHT11这类项目我最后再分享两个实实在在的建议。第一硬件上把上拉电阻和去耦电容加到位这两颗小元件能帮你挡掉一半以上“传感器不工作”的麻烦。第二软件上一定要有超时保护一个读不到数据就卡死的设备送到用户手里口碑直接崩塌。调试DHT11这类单总线设备时手边备一个逻辑分析仪比什么都管用。把波形抓下来数高电平宽度0和1的分布一目了然比盲猜代码快十倍。我第一次写DHT11驱动的时候全靠反复试错后来学会抓波形调试效率直接翻倍。这个习惯比我写过的任何一行F1代码都值钱。希望这篇实战分享能让你少走几步我以前走过的弯路。
返回列表