
1. 为什么STM32F1到今天还值得花时间学1.1 一颗“老芯片”的生存逻辑如果你最近在选型或者准备入门嵌入式大概率会刷到一堆推荐什么国产替代、什么Cortex-M4、什么RTOS加WiFi6。但只要你翻一翻淘宝销量、看看各大论坛的新手提问区就会发现一个反常识的现象——STM32F1系列依然是出货量最大、教程最多、踩坑记录最全的32位MCU之一。尤其是搭配DHT11温湿度传感器做环境监测这个组合几乎成了电子类专业学生和转行嵌入式的人绕不开的“新手村任务”。我自己第一次接触STM32F1是在一块最小系统板上芯片型号是STM32F103C8T648个引脚64KB Flash20KB RAM主频72MHz。当时觉得这参数放到今天简直寒酸但真正用起来才发现对于绝大多数控制类、采集类、显示类的项目它完全够用而且便宜、好买、资料多到令人发指。一块核心板不到十块钱DHT11模块两三块加起来一顿早餐钱就能跑通一个完整的“采集-处理-显示”链路。这篇文章不打算写成数据手册的复读机而是想从一个实际做过多个F1项目的人的角度把STM32F1的核心架构、开发方式选择、DHT11驱动细节、常见翻车现场这几个事情讲透。不管你是刚学完51单片机想往上走一步还是已经工作但一直用库函数“糊代码”想补补底层应该都能找到对你有用的东西。1.2 谁适合看这篇能解决什么问题先说清楚受众。如果你是完全零基础连C语言指针和结构体都还没搞明白那建议先补一下C语言否则看寄存器操作会很痛苦。但如果你满足下面任意一条这篇内容就是为你准备的学过51或者Arduino想系统性地掌握一款32位MCU工作中需要用到STM32F1做产品但一直用CubeMX生成代码想搞清楚底层到底发生了什么做课设或者项目要用DHT11采集温湿度结果时序调不通、数据一直显示85或者0想从标准库迁移到HAL库或者反过来不知道两者到底差在哪。我会从芯片选型、开发环境搭建、GPIO与时钟体系、DHT11单总线时序、串口打印调试、常见故障排查这几个维度展开。每个部分都会给出可以直接复现的操作步骤和参数计算过程而不是只丢一句“配置一下就行”。尤其是DHT11的时序部分我会把微秒级延时的计算方法、中断干扰的处理、校验失败的排查思路全部写出来这些都是我在实际项目里踩过坑之后总结的。2. STM32F1的芯片家族与选型思路2.1 型号命名规则拆解STM32F1不是一个芯片而是一个系列。拿最常见的型号STM32F103C8T6来拆字段含义具体到本例STM32产品家族意法半导体32位MCUF产品类型基础型103子系列增强型带USB和CANC引脚数48引脚8Flash容量64KBT封装LQFP6温度范围-40到85摄氏度同系列里还有STM32F103RCT664引脚、256KB Flash、STM32F103ZET6144引脚、512KB Flash等等。对于DHT11这种单总线传感器项目C8T6完全足够甚至用STM32F101这种基础型也行只是101没有USB和CAN主频也低一些。选型的时候有个经验先看引脚数够不够再看Flash和RAM最后看外设。很多人一上来就买ZET6这种大板子结果焊起来麻烦、体积大、成本高实际项目里根本用不到那么多IO。我个人的建议是新手入门买C8T6最小系统板做产品再根据实际外设需求往上选。2.2 核心资源与时钟树STM32F103C8T6的核心资源如下内核ARM Cortex-M3最高72MHzFlash64KBSRAM20KBGPIO37个可用IO定时器3个通用定时器TIM2/3/4、1个高级定时器TIM1通信接口2个SPI、2个I2C、3个USART、1个USB、1个CANADC2个12位ADC共10个通道。时钟树是F1的精华也是难点。简单说外部晶振一般是8MHz经过PLL倍频到72MHz作为系统时钟。具体路径是8MHz → 除以1 → 乘以9 → 72MHz。这个72MHz再分频给AHB、APB1、APB2。APB1最高36MHzAPB2最高72MHz所以挂载在APB1上的外设如TIM2/3/4、USART2/3时钟频率要注意配置波特率的时候如果算错了串口就会出乱码。我见过太多人串口打印乱码最后发现是把APB1的时钟当成72MHz去算波特率了。记住一句话USART1挂在APB2上USART2和USART3挂在APB1上前者72MHz后者36MHz。3. 开发方式的选择标准库、HAL库还是寄存器3.1 三种方式的真实对比这是每个学F1的人都会纠结的问题。我用一个表格把三种方式说清楚对比项寄存器标准库HAL库代码量最少中等最多可读性差好好移植性差差好学习曲线陡平缓平缓调试难度高中低官方支持无已停止持续更新标准库SPL是意法半导体早期推出的代码直观比如GPIO_SetBits(GPIOA, GPIO_Pin_0)一眼就能看懂。但官方从2015年左右就停止更新了新芯片不再支持。HAL库是现在的主流配合CubeMX可以图形化配置生成代码快但封装层次多出了问题不好查。我的建议是新手先用标准库把GPIO、定时器、串口、中断这几个基础外设摸一遍理解寄存器是怎么被操作的然后再转HAL库做项目。如果一上来就用CubeMX点几下生成代码很容易变成“只会点鼠标”的状态遇到时序问题就懵了。3.2 开发环境搭建的实操步骤以Keil MDK为例搭建一个F1工程需要这几步安装Keil MDK版本建议5.30以上太老的版本对Win11兼容性差安装器件支持包去Keil官网下载Keil.STM32F1xx_DFP双击安装下载标准库从ST官网找STM32F10x_StdPeriph_Lib_V3.5.0这是最后一个标准库版本新建工程选择芯片型号STM32F103C8添加启动文件从库里的startup/arm目录复制startup_stm32f10x_md.smd表示中等容量配置头文件路径把Library和User目录加进去定义宏在C/C选项卡里加上USE_STDPERIPH_DRIVER, STM32F10X_MD。这七步里最容易出错的是第5步和第7步。启动文件选错了比如选了hd高容量版本编译能过但运行会跑飞宏定义没加会报一堆assert_param相关的错误。我建议把这几步截图保存下次建工程直接照着做能省很多时间。提示如果你用的是CubeMX加HAL库步骤会简化很多但生成的工程默认用内部晶振需要手动在Clock Configuration里改成外部晶振并配置PLL否则主频只有8MHzDHT11的微秒延时算出来会差很多。4. DHT11温湿度传感器的驱动细节4.1 单总线协议的时序本质DHT11用的是单根数据线完成双向通信这种协议叫单总线。它的核心难点在于所有时序都是微秒级的而且对时间精度要求比较高。一次完整的读取流程是这样的主机拉低总线至少18ms然后拉高20-40us作为起始信号DHT11检测到起始信号后拉低总线80us再拉高80us作为响应之后DHT11连续发送40位数据每一位以50us低电平开始高电平的持续时间决定是0还是1高电平持续26-28us表示0持续70us表示1最后一位数据发完后DHT11拉低总线50us然后释放。这里的关键是区分0和1的阈值。理论上26-28us是070us是1但实际测量会有波动。我一般用40us作为分界高电平持续时间大于40us判为1小于40us判为0。这个阈值在大多数DHT11上都能稳定工作。4.2 微秒延时的实现与误差控制在72MHz的F1上用SysTick或者定时器做微秒延时都可以。用SysTick的话配置成1us中断一次但中断本身有开销实际延时会有偏差。更可靠的做法是用定时器计数或者空循环。空循环的写法需要根据主频计算循环次数。比如72MHz下一个指令周期约13.9ns但C语言一条语句编译后可能对应多条指令。我一般用示波器实测校准先写一个大概的循环然后测量实际延时再调整循环次数。下面是一个经过校准的微秒延时函数示例void delay_us(uint32_t us) { uint32_t i; while(us--) { for(i 0; i 8; i) // 72MHz下约1us需实测校准 { __NOP(); } } }注意这个8不是固定的不同编译器优化等级下结果不同。建议用示波器或者逻辑分析仪测一下实际波形把循环次数调到延时准确为止。如果没有仪器可以用串口打印时间戳来粗略验证。4.3 完整的读取函数与校验逻辑DHT11一次返回40位数据格式是湿度整数8位 湿度小数8位 温度整数8位 温度小数8位 校验和8位。校验和等于前四个字节相加的低8位。下面是一个完整的读取函数框架uint8_t DHT11_Read(uint8_t *temp, uint8_t *humi) { uint8_t buf[5]; uint8_t i, j; // 主机发送起始信号 DHT11_Out(); DHT11_Low(); delay_ms(20); DHT11_High(); delay_us(30); DHT11_In(); // 等待DHT11响应 if(DHT11_WaitLow(100) ! 0) return 1; if(DHT11_WaitHigh(100) ! 0) return 2; // 读取40位数据 for(i 0; i 5; i) { buf[i] 0; for(j 0; j 8; j) { if(DHT11_WaitHigh(100) ! 0) return 3; delay_us(40); buf[i] 1; if(DHT11_ReadPin() 1) { buf[i] | 1; if(DHT11_WaitLow(100) ! 0) return 4; } } } // 校验 if(buf[4] ! (buf[0] buf[1] buf[2] buf[3])) return 5; *humi buf[0]; *temp buf[2]; return 0; }这个函数里每个return对应一种错误1是响应超时2是响应高电平超时3是数据位高电平超时4是数据位低电平超时5是校验失败。实际调试时把返回值通过串口打印出来能快速定位问题出在哪一步。5. 串口调试与数据可视化5.1 串口配置的关键参数用USART1做调试输出配置如下波特率115200数据位8停止位1校验位无硬件流控无。在标准库里的配置代码USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_Cmd(USART1, ENABLE);波特率的计算依赖于APB2的时钟频率。如果系统时钟是72MHzUSART1挂在APB2上那么USARTDIV 72000000 / (16 * 115200) 39.0625实际寄存器值取39小数部分0.0625乘以16等于1所以BRR寄存器写入0x271。这些计算标准库的USART_Init函数会自动完成但你要确保RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE)被调用了否则串口没时钟什么也发不出来。5.2 用printf重定向到串口标准库工程里用printf需要重定向fputc函数int fputc(int ch, FILE *f) { while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }然后在Keil的Target选项里勾选Use MicroLIB。如果不勾选需要自己实现_sys_exit等底层函数比较麻烦。MicroLIB是Keil提供的一个精简C库专门用于嵌入式勾上就行。重定向之后就可以用printf(Temp: %d C, Humi: %d %%\r\n, temp, humi)这样的语句输出数据了。注意DHT11的湿度是整数温度也是整数小数部分在DHT11上通常是0所以直接用%d就行。5.3 数据上云与本地显示的扩展思路串口打印只是最基础的调试手段。实际项目里你可能需要把数据传到上位机或者显示在屏幕上。几个常见的扩展方向OLED显示用I2C接口的0.96寸OLED把温湿度实时显示出来适合做桌面气象站上位机曲线用Python的pyserial库读取串口数据配合matplotlib画实时曲线数据记录加一个SD卡模块用SPI接口把数据按时间戳写入CSV文件无线传输加一个蓝牙或者WiFi模块把数据发到手机或者云平台。这些扩展里OLED显示是最适合练手的因为I2C协议本身也是F1的必修课。我建议先把DHT11读通再加OLED一步一步来不要一上来就搞无线传输否则出了问题不知道是传感器的问题还是通信模块的问题。6. 常见问题与排查技巧实录6.1 DHT11数据一直显示85或0这是最经典的问题。85这个值其实是DHT11上电后的默认值说明根本没有读到有效数据。排查顺序如下检查供电DHT11的工作电压是3.3V到5.5V但很多模块是5V供电的如果MCU是3.3V数据线电平可能不匹配。建议用3.3V给DHT11供电或者在数据线上加电平转换检查上拉电阻DHT11的数据线需要4.7K到10K的上拉电阻很多模块自带但如果自己接线一定要加上检查延时精度用示波器看起始信号的低电平时间是不是18ms以上响应信号的80us低和80us高是不是准确检查引脚配置读取数据前要把引脚从输出模式切换到输入模式而且输入模式要配置成浮空输入或者上拉输入不能是模拟输入。我遇到过最坑的一次是上拉电阻没接数据线一直悬空读出来全是0。后来加了一个4.7K电阻到3.3V立刻正常。6.2 串口打印乱码乱码的原因基本只有一个波特率不对。排查步骤确认系统时钟是不是72MHz用RCC_GetClocksFreq函数打印出来看看确认USART挂在哪个总线上USART1是APB2USART2/3是APB1确认外部晶振是不是8MHz有些板子用的是12MHz晶振PLL倍频参数要改确认串口助手的波特率和代码里一致。还有一种情况是晶振没起振MCU自动切换到内部8MHz RC振荡器这时候系统时钟不是72MHz波特率自然就错了。用示波器测一下晶振引脚有没有波形或者读RCC_GetFlagStatus(RCC_FLAG_HSERDY)看看高速外部晶振有没有就绪。6.3 程序跑飞或进入HardFaultHardFault是Cortex-M3的异常通常由以下几种原因引起原因排查方法数组越界检查数组下标尤其是字符串操作空指针解引用检查指针是否初始化堆栈溢出增大启动文件里的Stack_Size中断优先级配置错误检查NVIC_PriorityGroupConfig访问了未使能时钟的外设检查RCC_APBxPeriphClockCmd我调试HardFault的常用方法是在HardFault_Handler里加一个死循环然后看LR寄存器的值通过LR判断是从哪个函数跳进来的。Keil的调试模式下可以在Registers窗口看到LR和PC的值结合反汇编定位问题代码。6.4 常见问题速查表现象可能原因解决方法DHT11返回85未读到数据检查上拉电阻和延时DHT11返回0时序错误用示波器校准延时校验失败数据位读取错误调整0/1判定阈值串口乱码波特率错误检查时钟配置程序不运行启动文件选错换成md版本编译报错assert_param宏定义缺失加USE_STDPERIPH_DRIVER中断不触发NVIC未配置检查优先级和使能定时器不准时钟频率算错确认APB1/APB2频率提示DHT11的采样率不能太高官方建议间隔不小于1秒。如果连续快速读取传感器可能不响应或者返回上一次的数据。我在代码里一般加一个delay_ms(1500)确保两次读取之间有足够的间隔。7. 从F1出发的进阶路线7.1 什么时候该换芯片STM32F1虽然够用但有些场景确实力不从心需要浮点运算F1没有FPU做PID或者滤波算法时浮点计算很慢这时候可以考虑F4系列需要更大RAMF1最多20KB RAM跑RTOS加网络协议栈会很吃力需要高速ADC或DACF1的ADC采样率1MHzF4可以到2.4MHz需要以太网F1没有内置MACF4和F7有。但如果你只是做温湿度采集、继电器控制、串口通信这类任务F1完全够用而且成本优势明显。不要为了“先进”而换芯片要根据实际需求来。7.2 从裸机到RTOS的过渡裸机程序写多了会遇到一个问题多个任务互相阻塞。比如DHT11读取需要20ms这期间如果还要处理按键和显示就会卡顿。这时候可以引入RTOS比如FreeRTOS。在F1上跑FreeRTOS是可行的20KB RAM够创建几个任务。移植步骤下载FreeRTOS源码把Source目录下的文件加入工程复制FreeRTOSConfig.h到User目录根据F1的资源修改配置实现vApplicationStackOverflowHook和vApplicationMallocFailedHook配置SysTick和PendSV中断优先级为最低创建任务启动调度器。我建议先把裸机程序写稳定再上RTOS。如果裸机都调不通上RTOS只会让问题更复杂。而且F1的资源有限任务不要建太多一般3到5个就够了。7.3 项目实战桌面温湿度监测站最后给一个完整的项目思路把前面讲的东西串起来硬件STM32F103C8T6最小系统板 DHT11 0.96寸OLED 蜂鸣器功能每2秒读取一次温湿度显示在OLED上温度超过30度蜂鸣器报警软件架构主循环轮询DHT11定时器中断刷新OLED蜂鸣器用GPIO控制调试手段串口打印每次读取的原始数据和校验结果。这个项目麻雀虽小五脏俱全涵盖了GPIO、定时器、中断、I2C、单总线、串口这几个核心外设。做完之后你对F1的掌握程度会有一个质的飞跃。我在带新人的时候基本都用这个项目作为考核能独立做出来并且稳定运行一周不出问题的就可以放手去做更复杂的任务了。我个人在实际操作中的体会是STM32F1就像一把用了很多年的螺丝刀看起来不起眼但顺手、可靠、坏了不心疼。DHT11虽然精度一般但胜在便宜和简单适合用来验证想法。真正把这两个东西吃透比盲目追新芯片要有价值得多。