
先说个可能会得罪人的观点STM32F1系列在2024年的今天依然是绝大多数人入门嵌入式最不该绕开的选择。别被各种F4、H7的新品评测带跑了节奏实际去逛一圈招聘要求、开源项目、学生毕设、小家电方案你会发现在大量真实场景里出货量最大的仍然是F103这一代。这篇文章我打算用STM32F1系列为主线把选型、资源、开发环境、寄存器操作这些基本功全部过一遍最后用一个DHT11温湿度传感器做完整实战从时序到代码逐一拆开顺便把那些网上教程里不太会写的坑也整理出来。无论你是刚买到开发板、照着点灯教程一步步来的新手还是已经熟悉Arduino、想切到更接近底层生态的老手这篇文章都值得认真看一遍。尤其是最后DHT11那一段很多人在GitHub上抄了代码却读不到数据问题往往不在代码本身而在对时序、引脚和上拉电阻的细节理解上。1. 先说结论STM32F1到底值不值得继续学1.1 一颗Cortex-M3芯片凭什么还能打STM32F1系列的核心是Cortex-M3内核主频最高72MHzFlash从16KB到512KB不等RAM从4KB到64KB不等。拿现在的眼光看这个性能确实算不上激进但嵌入式开发讲究的是够用就行。我们盘点一下F103C8T6这颗最典型的芯片64KB Flash、20KB RAM、三个USART、两个SPI、两个I2C、一个USB从机、一个CAN、三个16位定时器TIM1可做高级定时器用、一个12位ADC引脚才48个。就这套配置做个环境监测节点、电机驱动板、传感器采集网关甚至带个LCD屏做简单人机界面完全绰绰有余。Cortex-M3的内核架构本身也值得学习。它比Cortex-M0指令集更丰富有硬件除法器、位带操作、优先级可配置的NVIC中断控制器又没有Cortex-M4那套FPU和DSP指令的额外复杂度。对于理解中断、时钟树、寄存器映射这些嵌入式基本功F1是难度刚刚好的教材。我实际带项目时的体会是如果一个方案在F1上跑不动那十有八九是代码结构问题而不是芯片性能问题。先把F1玩透再谈换更高端的芯片这个顺序反了会非常痛苦。1.2 F1与F0、F4、G0系列横评为了不让选F1变成一种固执我把目前ST在低端市场的几个主力系列做了个对比系列内核最高主频典型价格区间C8T6级别核心优势适合人群STM32F0Cortex-M048MHz极低功耗低、价格便宜成本敏感的量产小家电STM32F1Cortex-M372MHz很低生态成熟、教程海量、外设均衡绝大多数学习者和工程师STM32G0Cortex-M064MHz低新一代低端主力、外设现代新设计低功耗产品STM32F4Cortex-M4168MHz中等带FPU和DSP、性能强音频处理、视觉、复杂控制STM32H7Cortex-M7480MHz偏高双核、超强算力高性能边缘计算从学习曲线来看F0资源太紧张外设也砍了不少F4对新手来说CubeMX生成的初始化代码多到让人头皮发麻偏偏硬件上跑的也是同一套外设逻辑。F1处在一个微妙的黄金位置资料多到你不可能搜不到硬件资源又足够你把该踩的坑都踩一遍。1.3 F103之间的型号差异怎么选F1家族内部最常见的几颗料往往让新手迷惑。直接给结论STM32F103C8T648引脚、64KB Flash、20KB RAM。淘宝上几块钱一片蓝板开发板的标配学习首选很多小项目直接用它量产。STM32F103RCT664引脚、256KB Flash、48KB RAM。资源更宽裕适合带屏项目或者跑Modbus协议栈。STM32F103ZET6144引脚、512KB Flash、64KB RAM。正点原子精英板常用外设接口全适合做复杂综合项目。STM32F105/107带USB OTG和以太网MAC适合需要联网或USB Host的场景但引脚复用复杂不建议新手开局就碰。我自己的建议很明确学习阶段闭眼选STM32F103C8T6。它便宜到可以随便焊坏几块不心疼而且绝大多数教程案例都以它为基准抄作业最顺畅。如果后续要做带屏幕的实训项目再升级到RCT6也完全来得及。2. STM32F1核心资源盘点外设、引脚与时钟树2.1 引脚功能复用从根上看懂初学者最容易在引脚上栽跟头。F103C8T6只有48个引脚GPIO本身是通用的但很多引脚除了当普通IO还能复用成USART、I2C、SPI、ADC的通道这就涉及一个叫做引脚复用的概念。引用数据手册里的Table 4引脚定义表你会发现每个引脚都有一个Default、Remap和具体复用功能的说明。比如PB6和PB7默认是I2C1_SCL和I2C1_SDA但也可以做普通GPIO或者复用为TIM4_CH1和TIM4_CH2。PA9和PA10则是USART1_TX和USART1_RX这是串口打印最常见的一对引脚。不查手册直接开干的代价是真实的。有一次我把DHT11接在PB4上怎么读都是超时最后发现PB4是JTAG的NJTRST引脚默认被JTAG功能占着普通GPIO输出根本不生效。解决方式是使用AFIO重映射功能或者干脆换引脚。实操提示F103上PA13、PA14是SWDIO和SWCLKPA15、PB3、PB4是JTAG引脚。如果你不想用JTAG只保留SWD调试可以在初始化代码里调用GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)把JTAG关掉这三个引脚才能当普通GPIO用。我建议直接把引脚选在PA0-PA2、PB0-PB2这种干净的位置省心。2.2 时钟树F1性能的命脉STM32F1的时钟树值得花一个小时认真看。芯片内部有两个时钟源高速外部时钟HSE通常接8MHz晶振和高速内部时钟HSI8MHz RC振荡器。HSI精度差、温漂大正常开发都是优先用HSE。F103最高跑到72MHz是通过PLL把8MHz倍频到72MHz实现的。标准配置是HSE 8MHz除以1PLL倍频9倍产生72MHz的系统时钟SYSCLK然后经过AHB预分频器分给各外设总线。这个倍频过程直接决定了串口波特率准不准、定时器延时准不准。很多人用内部时钟HSI跑代码里却按外部8MHz晶振的倍频来配置结果串口打印出来全是乱码——因为实际的系统时钟是8MHz x 9 72MHz但如果晶振没起振代码走了HSI路径就是8MHz x 9 72MHz不对HSI经过PLL配置也可能是72MHz关键在于HSI本身的精度差长时间运行后波特率偏移明显。一个稳妥的检查方法是看RCC-CR寄存器的HSERDY位确认外部晶振是否就绪。如果这个位一直置不了说明晶振电路有问题或者匹配电容不对程序还在用HSI跑。2.3 告别GPIO点灯瓶颈复用映射与AFIOGPIO在F1里不只是一堆寄存器那么简单。F1的GPIO有四种输入模式浮空、上拉、下拉、模拟和四种输出模式开漏、推挽、复用开漏、复用推挽外加两种速度档位2MHz、10MHz、50MHz。对于普通IO输出LED用推挽输出、50MHz速度就够了。但对于USART的TX引脚必须配置为复用推挽输出GPIO_Mode_AF_PP否则串口发不出数据。这是很多新手在标准外设库阶段最常见的错误管脚模式配错了功能就是不工作。还有AFIO这个容易被忽略的外设。AFIO负责引脚重映射和外部中断引脚选择。比如你想把USART1从PA9/PA10重映射到PB6/PB7就必须操作AFIO寄存器。用CubeMX生成代码时它自动搞定了但如果用标准外设库裸写很多人会漏掉GPIO_PinRemapConfig这一步。2.4 定时器、ADC、串口与DMA的配合思路F1的定时器分三类基本定时器TIM6/TIM7、通用定时器TIM2/TIM3/TIM4、高级定时器TIM1。学习时优先掌握TIM2到TIM4的四种工作模式定时中断、PWM输出、输入捕获、编码器模式。PWM调LED亮度、输入捕获测频率这些都是很经典的应用。ADC方面F103C8T6有10个外部通道12位分辨率采样时间可配置。ADC采集电位器电压几乎是继点灯之后第二个必做实验。使用DMA自动搬运ADC结果到内存可以把CPU解放出来这个技能在以后做连续采样时非常有用。串口配置本身不复杂但中文社区里大量教程用的都是阻塞式发送这对学习没坏处。真正要提高开发效率USART1 DMA 空闲中断这套组合必须学会。它能让你在接收不定长数据时不用一个字符一个字符地去判断帧头帧尾而是等一帧数据安静下来之后统一处理。3. 环境搭建与第一个工程CubeMX生成加Keil编译点灯3.1 工具链选型标准外设库还是HAL库现在STM32F1新手面临一个选择用ST官方的HAL库配合STM32CubeMX还是用已经被官方停更的标准外设库SPL。我的观点可能和很多人不同建议初学者直接用标准外设库入门后期再转HAL。原因有三第一F1时代的SPL文档和教程实在太多你遇到的问题几乎都有人踩过并且留了解法搜答案非常方便。第二SPL直接暴露寄存器操作逻辑对GPIO的速度、模式、时钟使能这些概念的理解来得更直观。第三HAL库虽然用CubeMX生成代码很爽但中间抽象层太多一旦出问题你根本不知道底层在干什么。当然如果是公司量产项目我建议用HAL CubeMX因为代码规范、可维护性强、移植也方便。学习阶段目的是理解底层没必要过早依赖图形化工具。我这篇文章的DHT11实战部分就用标准外设库来写帮助你直接看到寄存器级的操作。3.2 用CubeMX快速生成一个点灯工程虽然我建议学SPL但STM32CubeMX仍然是很好的工程脚手架工具。可以这样分工用CubeMX配置时钟和引脚生成初始化代码然后在main函数里往用户代码区填自己的逻辑。具体步骤打开STM32CubeMX选择芯片型号STM32F103C8Tx。在Pinout视图里点击PC13选择GPIO_Output这就是蓝色开发板上的板载LED。进入Clock Configuration页面把HSE设为Crystal/Ceramic ResonatorPLL倍频设成9x最终SYSCLK显示72MHz。Project Manager里选择Toolchain为MDK-ARM生成工程。用Keil打开工程在main.c的while循环里加一个HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13)再HAL_Delay(500)编译下载。这个流程的重点是理解两个概念时钟配置页面上的每一根连线代表的是真实的分频和倍频关系CubeMX生成的引脚初始化代码对应的是寄存器配置。跑通了第一个工程你对F1的整个运转逻辑就有了初步感觉。3.3 标准外设库的点灯代码逐行拆解CubeMX默认生成的是HAL库但这里把SPL版本的点灯代码写出来方便你对比理解#include stm32f10x.h void LED_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); } void delay_ms(uint32_t ms) { uint32_t i; while (ms--) { for (i 0; i 7200; i); } } int main(void) { LED_Init(); while (1) { GPIO_SetBits(GPIOC, GPIO_Pin_13); delay_ms(500); GPIO_ResetBits(GPIOC, GPIO_Pin_13); delay_ms(500); } }注意代码里的RCC_APB2PeriphClockCmd这一句作用是打开GPIOC的时钟。STM32的外设时钟默认是全关的你不开时钟寄存器写了也白写。这是F1和很多8位单片机最大的区别之一每次初始化外设都要记得开对应的时钟。还有那个延时函数我故意用了不精确的for循环因为点灯不需要精确时序而且能让你体会一下主频对延时的影响。但到了DHT11这种对时序敏感的传感器就不能这么写了后面实战部分我会改用DWT精确计时。3.4 下载调试ST-Link和串口ISPF103支持多种下载方式最常用的是ST-Link/V2通过SWD接口只用PA13和PA14两根线加GND就能调试。接线时SWDIO接PA13、SWCLK接PA14如果目标板供电不稳定最好把3.3V也接上共地。如果手头没有ST-Link也可以用一个USB转TTL模块通过串口ISP下载。把BOOT0引脚拉高、BOOT1拉低复位芯片后进入系统存储器引导模式然后用FlyMcu或STM32CubeProgrammer烧录。但注意这个方式只能下载程序不能在线调试看不了变量调试体验大打折扣。别小看这三根连线。SWD调试不仅用来下载程序还能实时查看寄存器值、内存值设置断点单步执行这是新手排查问题最强大的武器。强烈建议人手一个ST-Link/V2能够避免大量无意义的改了重新烧录、烧录了再看现象的循环。4. DHT11温湿度传感器实战时序解析与完整代码4.1 DHT11能被F1驱动靠的是严格的单总线时序DHT11是国产传感器里的常青树便宜、够用但通信协议比较原始单总线、40位数据、微秒级时序没有I2C那种规范的时钟信号全靠一根数据线的高低电平时序来传输信息。对初学者来说这反而是好事——能用它把时序的概念吃透。DHT11的数据格式很简单一次完整输出40位数据依次是湿度整数、湿度小数、温度整数、温度小数、校验和。每一字节都是8位高位先出校验和等于前四字节之和的低8位。通信开始时主机先把总线拉低至少18ms作为起始信号然后释放总线并延时20到40微秒。DHT11检测到这个起始信号后会先拉低80微秒表示响应再拉高80微秒准备发送数据。之后就是40位数据逐个发送。每一位数据的标准是每一位前有一个50微秒的低电平如果后面跟一个26到28微秒的高电平表示0如果后面跟一个70微秒的高电平表示1这个时序里最关键的技巧是采样时机。诺大的高电平持续时间就是区分0和1的唯一依据所以在低电平结束后延时40微秒再读取引脚电平如果读到的还是高电平那就是1如果已经变成低电平那就是0。4.2 电路接线与引脚选择建议DHT11模块一般有三个引脚或四个引脚VCC、DATA、GND有的带NC。对STM32F103来说我的接线方案是DHT11引脚STM32F103引脚说明VCC3.3V建议统一3.3V供电DATAPB0避开JTAG/SWD复用引脚GNDGND公共地数据线DATA必须接一个上拉电阻4.7k到10k都可以。大部分成品模块上已经焊好了上拉电阻直接接就行。如果你用的是裸DHT11传感器一定要在DATA和VCC之间自己加一个10k电阻否则读到的数据全是乱的。选择PB0还有一个原因它靠近板上GND线可以拉得很短。DHT11对线长和干扰都比较敏感我实测超过30厘米的杜邦线在电机旁边就很容易出现校验错误最佳实践是把传感器和单片机之间的连线控制在20厘米以内。4.3 微秒级延时的正确姿势DWT精确计时刚才点灯的for循环延时到了DHT11这里就不够看了。DHT11要求18ms、20到40us、50us这些微秒级别的延时如果还用循环里估算执行周期的方式误差会大到时序崩溃。Cortex-M3内核有一个叫DWTData Watchpoint and Trace的调试组件里面有一个CYCCNT计数器直接统计CPU时钟周期数。SYSCLK是72MHz时一个周期约13.9纳秒计数器的值每秒增加72000000次。我们只需要通过读取计数器差值来计时就能实现微秒级延时精度极高而且不依赖定时器。下面给出一个适用于F103的DWT延时实现#include core_cm3.h static void DWT_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } static void delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t ticks us * 72; // 72MHz主频下1us 72个时钟周期 while (DWT-CYCCNT - start ticks); }注意DWT-CYCCNT - start这个减法运算是有符号还是无符号的问题。当计数器回绕时无符号减法依然能得到正确的差值所以这段代码在长时间运行后依然可靠。4.4 GPIO输入输出模式切换的要害DHT11的数据线既要作为输出发送起始信号又要作为输入接收数据。标准外设库的GPIO_Init结构体可以反复配置同一引脚但要注意一个问题每次切换模式最好把整个GPIO_InitTypeDef重新赋值一遍。我习惯的做法是把引脚初始化封成两个函数一个设置推挽输出一个设置上拉输入void DHT11_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); } static void DHT11_SetOutput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); } static void DHT11_SetInput(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; // 上拉输入 GPIO_Init(GPIOB, GPIO_InitStructure); }这里有一个细节值得说为什么接收数据时要用上拉输入而不是浮空输入因为DHT11输出高电平时是靠内部上拉电阻把电平拉高的如果外部上拉电阻阻值偏大加上引脚悬空干扰数据线可能被误判为低电平。启用内部上拉相当于给弱上拉又加了一路保险。4.5 完整读取代码带超时保护的DHT11驱动网上能找到很多DHT11驱动代码但大部分都有一个问题如果传感器没接好或者被干扰while循环会一直死等某个电平导致整个程序卡死。我在实际工程里一定会给所有等待环节加超时保护这也是嵌入式开发的基本素养之一。下面是完整的DHT11读取代码我已经在F103C8T6上实际验证过#include stm32f10x.h #include core_cm3.h #define DHT11_PORT GPIOB #define DHT11_PIN GPIO_Pin_0 static void DWT_Init(void); static void delay_us(uint32_t us); static void DHT11_SetOutput(void); static void DHT11_SetInput(void); static uint8_t DHT11_WaitLevel(uint8_t level, uint16_t timeout_us); static uint8_t DHT11_ReadByte(void); uint8_t DHT11_ReadData(uint8_t *humidity, uint8_t *temperature) { uint8_t data[5] {0}; uint8_t i; // 1. 主机发送起始信号 DHT11_SetOutput(); GPIO_ResetBits(DHT11_PORT, DHT11_PIN); delay_us(18000); // 拉低至少18ms GPIO_SetBits(DHT11_PORT, DHT11_PIN); delay_us(30); // 拉高20-40us // 2. 切换输入模式等待DHT11响应 DHT11_SetInput(); if (!DHT11_WaitLevel(RESET, 100)) return 0; // 等待低电平响应信号 if (!DHT11_WaitLevel(SET, 100)) return 0; // 等待高电平结束 // 3. 读取40位数据 for (i 0; i 5; i) { data[i] DHT11_ReadByte(); } // 4. 校验和检查 if ((uint8_t)(data[0] data[1] data[2] data[3]) ! data[4]) { return 0; } *humidity data[0]; *temperature data[2]; return 1; } static uint8_t DHT11_ReadByte(void) { uint8_t i, result 0; for (i 0; i 8; i) { // 每一位都先有一个50us的低电平 if (!DHT11_WaitLevel(RESET, 100)) return 0; // 延时40us后采样0的高电平持续时间短此时已经回落1的高电平还有余量 delay_us(40); if (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) SET) { result | (1 (7 - i)); } // 等待高电平结束 if (!DHT11_WaitLevel(SET, 100)) return 0; } return result; } static uint8_t DHT11_WaitLevel(uint8_t level, uint16_t timeout_us) { while (GPIO_ReadInputDataBit(DHT11_PORT, DHT11_PIN) level) { if (timeout_us 0) return 0; timeout_us--; delay_us(1); } return 1; }这段代码有两个值得学习的设计点一是超时保护。DHT11_WaitLevel以微秒为单位倒计时如果等待超时立即返回0调用方就能快速退出而不是死循环。工程代码里任何while(1)式的电平等待都是隐患。二是采样时机。数据位低电平结束之后延时40us采样0和1能清晰区分开。这个延时采样思想在很多低速传感器都适用比单纯测量高电平时间要简单高效。读取主函数的用法int main(void) { uint8_t hum, temp; DWT_Init(); DHT11_GPIO_Init(); while (1) { if (DHT11_ReadData(hum, temp)) { // 输出温度湿度比如通过串口打印 } delay_ms(1000); // DHT11读取间隔至少1s } }4.6 DHT11的局限和替代方案DHT11的分辨率只有1%相对湿度和1摄氏度温度精度正负2摄氏度湿度精度正负5%。做演示和学习完全够用做数据记录也勉强能接受但如果你要做精确的温室环境监测建议换成SHT30或者AHT20这类I2C接口的传感器。SHT30的精度能到正负0.2摄氏度AHT20更是国产传感器里的明星价格也和DHT11差不了多少。不过从学习通信时序的角度DHT11反而有不可替代的优势因为单总线协议比I2C更原始更能锻炼你对电平、延时、上拉这些基础概念的敏感度。5. 调试中遇到的那些坑排查思路逐个拆5.1 引脚分配的隐形地雷前面提到过JTAG和SWD引脚冲突的问题但还有一类更隐蔽的坑BOOT1引脚PB2。PB2在F103上默认是BOOT1引脚芯片复位时会根据它的电平决定启动模式。你把它当普通GPIO用输出高电平或低电平本身没问题但如果恰好外接了影响BOOT1电平的元件可能导致上电启动模式不对程序跑飞。还有一个是PC13、PC14、PC15这三根引脚默认没有外部时钟源直接用作普通GPIO时会受RTC相关寄存器影响速度也只能跑到2MHz。所以不建议在这些引脚上做对时序敏感的操作DHT11这种就不要接PC13了。排查引脚问题时最有效的方法就是看数据手册的引脚定义表把引脚号、复位功能、复用功能一列一列对清楚。不要凭直觉猜测这颗芯片引脚应该差不多F1和F0、F4的复用映射差别很大。5.2 硬件I2C死锁问题为什么很多人宁愿软件模拟F1系列有一个知名问题硬件I2C外设存在设计缺陷在总线上出现噪声或从机异常时容易进入Busy状态之后不管怎么操作I2C总线都无法恢复。这个问题在ST社区的讨论帖里被抱怨了很多年官方也多次重申建议使用软件I2C。带DHT11虽然不用I2C但做其他I2C传感器比如OLED屏幕SSD1306、AT24C02存储芯片时一定会遇到。解决方案有两种一是引脚复用外部上拉电阻配合硬件I2C并且在通信失败后做总线复位二是彻底放弃硬件I2C用GPIO模拟I2C时序。我个人的工程习惯是F1上OLED和EEPROM全部使用软件I2C虽然代码稍长但稳定性和可移植性都很好。到了G0系列或F4系列硬件I2C性能才比较靠谱。5.3 烧录失败和BOOT0跳线的爱恨情仇STM32F1的BOOT0引脚决定上电后的启动区域。把BOOT0拉低从Flash启动正常运行用户程序把BOOT0拉高进入系统存储器引导通过串口ISP烧录。串口ISP下载的完整流程是这样先把BOOT0跳线帽接到1给板上电使用STM32CubeProgrammer连接串口选择波特率并擦除烧录烧完把BOOT0跳回0按一下复位键程序才能从Flash启动。很多新手烧录不成功就是因为忘了最后一步把跳线帽跳回来。如果用ST-Link下载BOOT0的电平对烧录不敏感不需要反复跳线。这也是我建议新手优先用ST-Link的原因。5.4 串口乱码时钟配置与波特率误差串口打印出现乱码先检查两个东西系统时钟是否真的跑到了72MHz波特率设置是否一致。如果HSE晶振没起振代码走了HSI内部时钟或者干脆用8MHz配置跑串口输出的波特率就会偏离预期值。用示波器测量TX引脚的波形能一眼看出实际波特率但没示波器的话可以先把CubeMX的时钟配置页面截图核对确保HSE、PLL、AHB分频三项都对。还有一个小陷阱C8T6这种48脚封装的芯片PC14和PC15默认是用来接32.768kHz的RTC晶振的。如果你在CubeMX里把PC14/PC15配成了GPIO而没有禁用RTC初始化时可能没问题但代码量大了以后功耗和时钟切换行为会变得诡异。5.5 为什么我的DHT11读数一直是0这个问题的出现频率非常高通常有三个原因一、传感器上电后第一个信号需要缓冲。DHT11上电后要稳定1秒以上才能正常读取如果程序在上电后立刻发起读取大概率得到校验失败或全0。解决方法是初始化后延时2秒再开始读。二、读取间隔太短。DHT11的采样周期是1秒到2秒如果代码里间隔离几百毫秒去读芯片的内部数据处理不过来可能返回上一次的旧值或者校验失败。我建议主循环里至少延时1.5秒。三、引脚没配好。确认代码和实际接线一一对应特别是GPIO模式切换的部分重载Mode时如果把整个结构体遗忘了一个字段GPIO_Init会写进错误配置。如果信息全对但始终读不到用示波器抓DATA引脚的波形看主机起始信号和传感器响应是否存在。5.6 排查实录一次合法代码读不到数据的完整过程有一次给朋友排查DHT11他把代码从GitHub抄下来接线也对上拉电阻也有但串口就是不输出数据。我看他的板子是蓝板F103C8T6代码里配的引脚是PA15立刻意识到问题所在。PA15是JTDI引脚默认被JTAG占用。他用的标准外设库初始化代码里只开了GPIOA的时钟没有禁用JTAG所以PA15根本不受GPIO配置控制DHT11自然没法工作。解决方法是加一行GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);或者干脆把传感器挪到PA0。这两个方案我推荐后一个因为JTAG禁用后还要注意不影响SWD调试功能配置不当会让调试器断开连接。排查的结果虽然简单但过程能总结成一套方法论先确认引脚功能没有冲突再确认时钟是否配置正确然后用示波器或逻辑分析仪看波形最后才怀疑代码逻辑。顺序反过来容易事倍功半。6. 样例扩展思路从DHT11到完整小项目6.1 加LCD屏从裸数据到可视化界面DHT11采集到温湿度后最直接的展示方式是接一块0.96寸I2C OLED屏SSD1306芯片。在F1上驱动这块屏幕主流方案是软件I2C模拟时序然后找一份SSD1306的驱动库把初始化、显示字符、显示数字这几个函数用起来。一个完整的温湿度显示项目结构可以这样分DHT11驱动模块负责数据采集和校验OLED驱动模块负责显示初始化、清屏、打印主循环每1.5秒读一次DHT11刷新OLED显示如果你用的是标准外设库OLED驱动库建议直接移植u8g2或Adafruit SSD1306的纯软件版本不要自己重写绘图函数省下的时间用来调试显示布局更有价值。6.2 加联网模块让数据上报到云平台DHT11加ESP8266模块通过AT指令把温湿度数据上传到国内主流物联网云平台是另一个经典项目方向。具体流程是STM32F1通过USART2控制ESP8266波特率通常设成115200ESP8266连接WiFi使用ATCWJAP指令建立TCP连接或MQTT连接向云平台发送JSON格式数据云平台上配置数据模板就能看到曲线这里有个工程注意点ESP8266的供电要求5V电源且峰值电流可达几百毫安。不要把ESP8266的VCC直接接在STM32F1开发板的3.3V引脚上否则电压跌落会导致模块频繁重启。建议用独立的5V电源给ESP8266供电然后通过串口连接STM32注意两边要共地。6.3 低功耗场景让STM32F1挂电池跑更久如果DHT11项目要做成便携设备比如粮仓温湿度记录仪就得考虑F1的低功耗模式。F103的三种低功耗模式睡眠模式CPU停止外设继续运行电流很低唤醒快停机模式所有时钟停止保留SRAM和寄存器内容外部中断可唤醒待机模式几乎全关只有备份域和唤醒电路工作功耗最低对DHT11采集这种每隔几分钟醒一次的场景用待机模式加RTC闹钟唤醒比较合适。整个流程是进入待机前把DHT11的电源也切掉RTC定时唤醒后重新上电延时2秒让传感器稳定读取数据保存到Flash再进待机。做过一轮这个设计你对功耗管理才有实际感觉。6.4 软件层面的提高状态机、队列与模块化做传感器项目容易但把项目做得规范是另一回事。我建议在DHT11项目的基础上认真实践三个软件工程习惯一是把硬件初始化和业务逻辑分离。DHT11驱动只负责采集和校验主循环只负责调度和显示不要在读取函数里夹杂延时等待之外的复杂逻辑。二是用状态机管理DHT11的读取时序。DHT11单总线时序天然适合拆成几个状态发送起始信号、等待响应、接收数据。用状态机可以避免阻塞式等待把单片机的空闲时间让给其他任务。三是一定要写调试日志。在项目的关键节点通过串口打印状态码比如传感器未响应、校验失败、读取成功三个状态用不同编号输出排查问题时能省出大量时间。我见过太多人在点灯阶段就把代码结构写得一团乱几十个全局变量互相耦合功能函数越写越长。等到项目变大一个传感器好使两个传感器同时跑就崩溃。嵌入式开发写代码宁可多拆几个小函数也别图省事把逻辑堆在一起。7. 一些更实战的体会写到这里不再做什么总结。说说这几年用STM32F1的切身体会吧。F1这颗芯片确实不算年轻了它的许多设计在今天的视角看有些老派从硬件I2C的争议到JTAG引脚默认占用都是F4和G0系列早已经优化过的东西。但恰恰是这些不完美才让它成为特别好的学习材料。你在F1上踩过的每一个坑都对应着对硬件工作机制的一次真正理解你在F1上调通的一段DHT11代码放到任何Cortex-M芯片上都能很快移植过去。如果我只能给一条建议那就是不要满足于让DHT11读出来数据就结束。拆掉代码里的超时保护看看会发生什么换一根一米长的线试试会不会校验失败把供电改成3.3V再把裸传感器和模块版各测一轮。这些主动制造的故障会让你对本系列芯片的理解远远超过看十篇教程。最后分享一个我在实际项目中常用的组合拳F103C8T6做控制器DHT11做采集0.96寸OLED做显示PC上位机留一个USART1调试口。这套配置总成本不超过二十块钱却覆盖了GPIO、定时器、串口、I2C、单总线、功耗管理几乎全部嵌入式基础技能。把它彻底吃透你再去碰ESP32、STM32F4、甚至Zynq这类复杂SoC会发现很多东西其实都是相通的。硬件平台永远在变但底层那套时钟、时序、中断和外设交互的思维方式一旦建立就再也不会丢。