资讯动态

STM32F1嵌入式开发实战:时钟、GPIO、定时器与DHT11驱动详解

发布时间:2026/10/27 18:14:59 来源:尧图企业网站定制
1. 为什么STM32F1至今仍是嵌入式入门的首选平台如果你在电子论坛或者技术群里问一句“新手学单片机选什么”十有八九会有人回你“STM32F1”。这个答案从十年前一直延续到现在哪怕Cortex-M0、M3、M4甚至M33的新品层出不穷STM32F103系列依然牢牢占据着入门和中小型项目的主力位置。我自己从最早用寄存器操作点亮第一颗LED到后来用标准外设库做工业采集板再到现在偶尔拿它做快速原型验证前前后后经手过几十个项目对这颗芯片的脾气算是摸得比较透。STM32F1系列是意法半导体基于ARM Cortex-M3内核推出的32位微控制器家族主打型号包括STM32F103C8T6、STM32F103RCT6、STM32F103ZET6等。它的核心频率最高72MHz内置Flash从16KB到1MB不等SRAM从6KB到96KB外设覆盖GPIO、USART、SPI、I2C、ADC、DAC、定时器、CAN、USB等几乎你能想到的常用接口它都有。关键是价格便宜一片STM32F103C8T6核心板不到十块钱资料铺天盖地社区活跃度极高出了问题随便一搜就有答案。但“资料多”这件事本身也是双刃剑。网上关于STM32F1的内容质量参差不齐很多教程停留在“复制粘贴代码能跑就行”的层面真正讲清楚配置逻辑、时钟树关系、中断优先级分组、DMA与外设配合的内容反而被淹没。更麻烦的是很多新手直接拿别人的工程改遇到问题不知道从哪查起最后卡在某个看似简单的环节上耗掉好几天。所以这篇文章不打算再写一份“从零开始点灯”的流水账而是围绕STM32F1在实际项目中最容易踩坑的几个核心环节把原理、配置逻辑和实操经验串起来讲清楚。这篇文章适合三类人第一类是刚接触STM32F1、能看懂C语言但还没建立起完整工程概念的新手第二类是用过F1但一直停留在“改改例程”层面、想真正搞懂底层机制的中级开发者第三类是需要快速选型和搭建采集类项目的工程师尤其是涉及DHT11温湿度传感器这类经典外设的场景。我会从时钟系统这个最容易被忽视但最关键的环节讲起然后依次展开GPIO配置、定时器与延时、串口通信、DHT11单总线驱动、ADC采集以及低功耗与调试技巧每个部分都给出可复现的配置思路和实测中总结的注意事项。2. 时钟树没搞懂后面全是玄学问题2.1 为什么你的串口波特率总是对不上很多人第一次用STM32F1串口通信时会遇到一个诡异现象代码里明明写的波特率是9600但串口助手收到的全是乱码换个波特率偶尔能好但换个芯片又不行。这个问题的根源几乎都指向同一个地方——时钟配置。STM32F1的时钟系统比51单片机复杂得多它有多条时钟源和分频路径最终送到各个外设的时钟频率决定了该外设的工作节奏。串口的波特率是由APB总线时钟经过分频得到的如果APB时钟和你计算时假设的不一致波特率自然就偏了。STM32F1的时钟源主要有四个HSI内部高速RC8MHz、HSE外部高速晶振通常8MHz、LSI内部低速RC约40kHz、LSE外部低速晶振32.768kHz。系统时钟SYSCLK可以来自HSI、HSE或者PLL倍频输出。以最常见的STM32F103C8T6最小系统板为例板子上焊的是8MHz晶振标准做法是HSE经PLL九倍频得到72MHz作为SYSCLK。然后SYSCLK经过AHB分频器通常不分频得到HCLK72MHzHCLK再经过APB1分频器通常2分频得到PCLK136MHz经过APB2分频器通常不分频得到PCLK272MHz。这里有个关键细节APB1上挂的是USART2/3/4/5、TIM2/3/4/5/6/7、I2C1/2、SPI2等低速外设APB2上挂的是USART1、TIM1、TIM8、SPI1、ADC1/2、GPIO等高速外设。如果你用USART1它的时钟是PCLK272MHz如果用USART2时钟是PCLK136MHz。很多例程默认用USART1你改成USART2时如果没注意这个差异波特率寄存器算出来的值就会错一倍。注意STM32F1的定时器还有一个特殊规则——当APB分频系数不为1时定时器的时钟会是APB时钟的两倍。也就是说TIM2挂在APB1上PCLK136MHz但TIM2实际计数时钟是72MHz。这个规则在计算定时器预分频值时必须考虑否则定时时间会差一倍。2.2 用标准库配置72MHz时钟的完整逻辑虽然现在HAL库和LL库很流行但STM32F1的标准外设库依然是很多老项目的首选而且标准库的时钟配置代码更直观地展示了时钟树的运作过程。下面这段代码是SystemInit函数的核心逻辑我把它拆开解释每一步在做什么。// 使能HSE并等待就绪 RCC-CR | RCC_CR_HSEON; while(!(RCC-CR RCC_CR_HSERDY)); // 配置Flash预取和等待周期 FLASH-ACR | FLASH_ACR_PRFTBE; FLASH-ACR ~FLASH_ACR_LATENCY; FLASH-ACR | FLASH_ACR_LATENCY_2; // 72MHz需要2个等待周期 // 配置PLLHSE作为输入9倍频 RCC-CFGR ~RCC_CFGR_PLLSRC; RCC-CFGR | RCC_CFGR_PLLSRC; RCC-CFGR ~RCC_CFGR_PLLMULL; RCC-CFGR | RCC_CFGR_PLLMULL9; // 使能PLL并等待锁定 RCC-CR | RCC_CR_PLLON; while(!(RCC-CR RCC_CR_PLLRDY)); // 配置AHB、APB1、APB2分频 RCC-CFGR ~RCC_CFGR_HPRE; RCC-CFGR | RCC_CFGR_HPRE_DIV1; // AHB SYSCLK RCC-CFGR ~RCC_CFGR_PPRE1; RCC-CFGR | RCC_CFGR_PPRE1_DIV2; // APB1 HCLK/2 RCC-CFGR ~RCC_CFGR_PPRE2; RCC-CFGR | RCC_CFGR_PPRE2_DIV1; // APB2 HCLK // 切换系统时钟到PLL RCC-CFGR ~RCC_CFGR_SW; RCC-CFGR | RCC_CFGR_SW_PLL; while((RCC-CFGR RCC_CFGR_SWS) ! RCC_CFGR_SWS_PLL);这段代码里有两个地方新手容易忽略。第一是Flash等待周期72MHz下必须设置2个等待周期否则取指会出错表现为程序跑飞或者偶尔死机。第二是切换系统时钟前必须确保目标时钟源已经稳定代码里用while循环等待HSERDY和PLLRDY就是干这个的。如果你自己写时钟初始化漏掉任何一个等待环节都可能出现“大部分时候正常、偶尔启动失败”的玄学问题。2.3 时钟安全系统与实测中的异常排查STM32F1有一个时钟安全系统CSS当HSE失效时会自动切换到HSI并产生中断。这个功能在工业现场很有用因为晶振受震动或温度影响可能停振。但CSS默认是关闭的需要手动使能。我遇到过一块板子在电机启动瞬间程序跑飞后来查出来是电机干扰导致晶振短暂停振使能CSS后至少能保证系统不死机可以在中断里做安全处理。排查时钟问题有一个很实用的方法在系统启动后把各个时钟频率通过串口打印出来。标准库提供了RCC_GetClocksFreq函数可以获取SYSCLK、HCLK、PCLK1、PCLK2的实际值。如果你怀疑时钟配置有问题先打印这几个值和预期对比能快速定位是哪个分频环节出了偏差。另外用示波器测量MCO引脚PA8输出的时钟信号也是验证系统时钟的直观手段通过配置RCC_CFGR的MCO位可以把SYSCLK或HSE分频后输出到PA8。3. GPIO的八种模式到底该怎么选3.1 推挽、开漏、上拉、下拉的实际区别STM32F1的每个GPIO都有八种可配置模式输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、开漏复用、推挽复用。很多教程只列个表格就过去了但实际项目中选错模式会导致各种奇怪现象。我用几个具体场景来说明。推挽输出是最常用的模式它可以主动输出高电平和低电平驱动能力强适合直接驱动LED、继电器控制脚等。开漏输出只能主动拉低高电平需要外部上拉电阻提供它的好处是可以做电平转换和线与逻辑。比如你用3.3V的STM32去驱动5V的逻辑电路开漏输出加上拉到5V就能安全实现推挽输出直接接5V可能会损坏芯片。输入浮空模式下的引脚阻抗很高如果外部没有明确的驱动源引脚电平会随机漂移。我见过一个案例按键接在浮空输入引脚上没按的时候读到的值一直在跳程序里做按键检测时误触发不断。改成输入上拉后问题消失。所以按键输入一般用上拉或下拉模式具体取决于按键另一端接的是GND还是VCC。模拟输入模式用于ADC采集此时引脚的数字输入功能被关闭信号直接进入模拟通道。如果你用ADC采集但GPIO配成了浮空输入读出来的值会不准因为输入施密特触发器还在工作会对模拟信号产生干扰。3.2 复用功能与重映射的配置顺序STM32F1的很多外设引脚可以通过AFIO的重映射功能切换到其他引脚。比如USART1默认在PA9/PA10但可以重映射到PB6/PB7。重映射的配置有一个严格的顺序要求必须先使能AFIO时钟再配置重映射寄存器最后配置GPIO模式。如果顺序反了重映射不生效。// 正确顺序先开AFIO时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE); // 再配置重映射 GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE); // 最后配置GPIO RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_Init(...);还有一个容易踩的坑JTAG引脚复用。STM32F1的PA13、PA14、PA15、PB3、PB4默认是JTAG调试接口如果你想把它们当普通GPIO用需要先关闭JTAG功能。标准库提供了GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE)来只保留SWD调试。这个操作在引脚紧张的项目里很常见但忘记关闭的话配置GPIO时怎么都不生效。3.3 输出速度设置对EMI的影响GPIO输出速度有2MHz、10MHz、50MHz三档。速度越高引脚翻转时的边沿越陡EMI辐射也越大。很多新手随手就选50MHz结果产品做EMC测试时辐射超标。正确的做法是根据实际信号频率选择LED控制、继电器驱动这类低速信号用2MHz就够了SPI、USART等中速信号用10MHz只有需要高速翻转的场合才用50MHz。我在一个工业采集板上把所有GPIO默认设成50MHz后来整改EMC时逐个降到10MHz和2MHz辐射余量立刻上来了。4. 定时器与延时的正确打开方式4.1 SysTick和通用定时器的分工STM32F1有一个SysTick系统定时器属于Cortex-M3内核的一部分通常用来做系统时基。标准库的delay函数就是基于SysTick实现的毫秒级延时。但SysTick只有一个如果项目中需要多个不同精度的延时或者需要PWM输出就得用通用定时器TIM2到TIM5。SysTick的配置很简单它有一个24位的递减计数器时钟源可以是HCLK或HCLK/8。标准库默认用HCLK/8也就是72MHz/89MHz然后根据要延时的毫秒数计算重装载值。这里有一个细节SysTick的重装载值最大是2的24次方减一约1677万在9MHz时钟下最大延时约1.86秒。如果你需要更长的延时得在delay函数里循环调用。通用定时器的功能就丰富多了除了基本定时还支持输入捕获、输出比较、PWM、编码器接口等。TIM2到TIM5是通用定时器TIM1和TIM8是高级定时器支持互补输出和死区插入适合驱动电机。TIM6和TIM7是基本定时器只能做定时没有输入输出通道。4.2 定时器预分频和重装载值的计算定时器的定时时间由三个参数决定时钟频率、预分频系数PSC、重装载值ARR。公式是定时时间 (PSC1) × (ARR1) / 定时器时钟频率。注意PSC和ARR都要加一因为寄存器值是从0开始计数的。假设TIM2的时钟是72MHz我想定时1毫秒。先选一个合适的预分频系数比如72-1这样定时器计数时钟变成72MHz/721MHz每个计数周期1微秒。然后ARR设为1000-1总共计数1000次就是1毫秒。这个计算过程看起来简单但实际配置时容易忘记定时器时钟是APB时钟的两倍这个规则。如果你以为TIM2的时钟是36MHz算出来的定时时间就会差一倍。void TIM2_Init(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); TIM_TimeBaseStructure.TIM_Period 1000 - 1; // ARR TIM_TimeBaseStructure.TIM_Prescaler 72 - 1; // PSC TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); TIM_Cmd(TIM2, ENABLE); }4.3 中断优先级分组对实时性的影响Cortex-M3的中断优先级分为抢占优先级和响应优先级STM32F1支持5种分组方式。分组决定了多少个位用于抢占优先级、多少个位用于响应优先级。抢占优先级高的中断可以打断正在执行的低抢占优先级中断响应优先级只在同时挂起时决定谁先执行不能打断。很多项目里中断响应不及时根源就是优先级分组没设对。比如串口接收中断和定时器中断同时存在如果串口中断的抢占优先级低于定时器中断而定时器中断服务程序又比较长串口数据就会丢失。我的习惯是在系统初始化时统一设置分组为NVIC_PriorityGroup_2即2位抢占优先级、2位响应优先级这样抢占优先级有0到3共4级响应优先级也有4级对大多数项目够用了。提示中断服务程序里尽量不要做耗时操作比如浮点运算、长循环、串口打印。如果确实需要处理大量数据用标志位在主循环里处理中断里只做数据搬运和标志置位。5. 串口通信从能用到好用5.1 波特率误差的来源与容忍范围串口通信对波特率误差有一定容忍度一般要求在2%以内超过这个范围就可能出现误码。STM32F1的USART波特率寄存器是一个16位的整数部分加4位小数部分计算公式是USARTDIV 外设时钟 / (16 × 波特率)。以USART1、PCLK272MHz、波特率115200为例USARTDIV 72000000 / (16 × 115200) 39.0625。整数部分是39小数部分是0.0625×161所以寄存器值应该是39.1。实际写入时整数部分39小数部分1。如果外设时钟不是72MHz而是36MHzUSARTDIV 36000000 / (16 × 115200) 19.53125整数19小数0.53125×168.5取整后误差就大了。所以用USART2时如果没注意时钟差异115200的波特率误差可能超过容忍范围导致通信不稳定。5.2 接收中断与空闲中断的配合使用串口接收不定长数据是一个经典问题。常见做法是用接收中断每收到一个字节进一次中断把数据存入缓冲区。但这样每字节一次中断在高波特率下CPU开销很大。更好的方案是配合DMA和空闲中断DMA负责把接收到的数据自动搬到缓冲区空闲中断在总线空闲时触发表示一帧数据接收完毕。STM32F1的USART支持空闲中断当RX线上检测到一个字节时间的空闲电平时触发。配置步骤是使能USART的DMA接收请求配置DMA通道从USART数据寄存器搬到内存缓冲区然后使能空闲中断。在空闲中断服务程序里读取DMA剩余传输数量就能知道这一帧收了多少字节处理完后重新设置DMA计数器准备下一帧。void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART1); // 读DR清空闲标志 u16 len BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // len就是本帧接收到的字节数 ProcessFrame(rxBuffer, len); // 重新配置DMA准备下一帧 DMA_Cmd(DMA1_Channel5, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel5, BUFFER_SIZE); DMA_Cmd(DMA1_Channel5, ENABLE); } }这个方案我在多个工业采集项目里用过115200波特率下连续接收几百字节不丢数据CPU占用率极低。5.3 printf重定向与调试输出把printf重定向到串口是调试利器但标准库的printf默认输出到调试器需要重写fputc函数。在Keil MDK环境下还需要勾选Use MicroLIB否则printf可能无法正常工作。重定向代码如下int fputc(int ch, FILE *f) { while(USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }这里用TC标志而不是TXE标志是因为TXE只表示发送数据寄存器空但移位寄存器可能还在发送。用TC能确保上一个字节完全发完再发下一个避免输出乱序。不过用TC会稍微降低吞吐量如果对速度要求高可以用TXE加适当延时。6. DHT11温湿度传感器的单总线驱动实战6.1 单总线时序的微秒级要求DHT11是一款经典的温湿度传感器它用一根数据线完成通信协议是单总线。虽然接线简单但时序要求比较严格特别是微秒级的延时必须准确。DHT11的通信过程是主机拉低总线至少18毫秒作为起始信号然后释放总线DHT11响应拉低80微秒、再拉高80微秒之后开始传输40位数据。每一位数据以50微秒低电平开始然后高电平持续26到28微秒表示0持续70微秒表示1。问题来了STM32F1用SysTick做微秒级延时时如果系统主频是72MHz一个微秒是72个时钟周期。用循环做微秒延时需要精确计算循环次数而且编译器优化等级不同循环被优化掉的风险很大。我的做法是用定时器做微秒延时或者用DWT数据观察点与跟踪单元里的CYCCNT计数器它能精确计数CPU周期不受中断影响。// 用DWT实现微秒延时 void DWT_Delay_us(uint32_t us) { uint32_t start DWT-CYCCNT; uint32_t cycles us * (SystemCoreClock / 1000000); while((DWT-CYCCNT - start) cycles); }DWT需要先使能在CoreDebug-DEMCR寄存器里置位TRCENA然后清零DWT-CYCCNT最后使能DWT-CTRL的CYCCNTENA位。这个延时方法精度很高而且不受编译器优化影响。6.2 读取DHT11的完整流程与校验DHT11的数据格式是8位湿度整数、8位湿度小数通常为0、8位温度整数、8位温度小数通常为0、8位校验和。校验和等于前四个字节相加的低8位。读取流程如下主机拉低总线至少18ms然后拉高20-40us切换到输入模式等待DHT11响应。等待DHT11拉低80us再拉高80us之后进入数据传输。循环40次每次等待50us低电平结束然后延时30us左右采样总线电平高电平为1低电平为0。收完40位后释放总线DHT11会自动拉低50us再释放。校验数据如果校验和不匹配则丢弃本次数据。实际调试时最容易出问题的地方是等待响应信号。如果DHT11没有接上拉电阻总线在释放后可能无法回到高电平导致一直等待超时。DHT11的数据线需要接一个4.7k到10k的上拉电阻到VCC。另外DHT11的采样周期不能太快两次读取之间至少间隔1秒否则读出来的数据可能不更新。6.3 在FreeRTOS任务中读取DHT11的注意事项如果项目用了FreeRTOS在任务里读取DHT11要特别注意临界区保护。因为DHT11的时序是微秒级的如果读取过程中被任务调度打断时序就乱了。解决办法是在读取DHT11的整个过程中关闭任务调度器或者把DHT11读取放在高优先级任务里并确保没有其他高优先级任务会抢占。void DHT11_Task(void *pvParameters) { while(1) { taskENTER_CRITICAL(); // 进入临界区关闭中断 DHT11_Read(temp, humi); taskEXIT_CRITICAL(); // 退出临界区 vTaskDelay(pdMS_TO_TICKS(2000)); } }不过关闭中断会影响系统实时性如果DHT11读取耗时较长完整读取约4-5毫秒可以考虑只关闭任务调度而不关中断用vTaskSuspendAll和xTaskResumeAll。但这样如果中断服务程序里有时序敏感操作还是可能干扰。最稳妥的方案是用硬件定时器配合状态机来解码DHT11把微秒级时序交给硬件处理CPU只负责状态切换。7. ADC采集中的精度陷阱与软件滤波7.1 参考电压与采样时间的选择STM32F1的ADC是12位逐次逼近型参考电压通常接VDDA也就是3.3V。理论分辨率是3.3V/4096≈0.8mV。但实际精度受很多因素影响其中采样时间是一个关键参数。ADC的采样时间可配置为1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5个ADC时钟周期。采样时间越长采样电容充电越充分转换结果越准确但转换速度越慢。如果被测信号源内阻较大比如用电阻分压测量电池电压分压电阻取值较大时必须用较长的采样时间。我见过一个案例用100k和100k分压测12V电池ADC配置成1.5周期采样读出来的值一直偏低且跳动。后来把采样时间改成239.5周期读数立刻稳定了。原因是分压电阻和ADC采样电容构成了RC充电电路时间常数太大短采样时间下电容还没充到稳定值就开始转换了。7.2 多重采样与中值滤波的代码实现即使采样时间设置合理ADC读数仍然会有几个LSB的跳动这是正常现象。在工业采集场景里通常需要做软件滤波。最简单的是多次采样取平均但平均对脉冲干扰的抑制效果不好。更好的方案是中值滤波加平均连续采样N次去掉最大值和最小值剩下的取平均。#define SAMPLE_COUNT 11 u16 ADC_GetFilteredValue(u8 channel) { u16 samples[SAMPLE_COUNT]; u16 temp; u32 sum 0; u8 i, j; for(i 0; i SAMPLE_COUNT; i) { samples[i] ADC_GetConversionValue(channel); } // 冒泡排序 for(i 0; i SAMPLE_COUNT - 1; i) { for(j 0; j SAMPLE_COUNT - 1 - i; j) { if(samples[j] samples[j1]) { temp samples[j]; samples[j] samples[j1]; samples[j1] temp; } } } // 去掉最大最小各两个中间7个取平均 for(i 2; i SAMPLE_COUNT - 2; i) { sum samples[i]; } return sum / (SAMPLE_COUNT - 4); }这个滤波方案在温度采集、电压监测等场景里效果很好能把跳动从十几个LSB压到两三个LSB以内。代价是需要采样11次转换时间乘以11对采样率要求高的场合要权衡。7.3 ADC与DMA配合实现自动扫描当需要同时采集多个通道时用DMA自动搬运ADC数据可以大大减轻CPU负担。配置步骤是把ADC设置成扫描模式配置规则组的通道顺序和采样时间使能ADC的DMA请求配置DMA通道从ADC数据寄存器搬到内存数组。这样ADC按顺序转换完所有通道后DMA自动把结果存到数组里CPU只需要在需要的时候读数组就行。// ADC多通道DMA配置核心代码 ADC_InitStructure.ADC_ScanConvMode ENABLE; ADC_InitStructure.ADC_ContinuousConvMode ENABLE; ADC_InitStructure.ADC_ExternalTrigConv ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel 3; ADC_Init(ADC1, ADC_InitStructure); ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE);DMA配置时要注意数据宽度匹配ADC数据寄存器是16位的内存数组也要定义成u16类型DMA外设数据宽度和内存数据宽度都设成HalfWord。另外DMA模式要设成循环模式这样ADC连续转换时DMA会自动覆盖旧数据不会因为传输完成而停止。8. 低功耗设计与调试接口的取舍8.1 睡眠、停止、待机三种模式的唤醒源STM32F1支持三种低功耗模式睡眠、停止、待机。睡眠模式只关闭CPU时钟外设继续运行任何中断都能唤醒功耗降低有限。停止模式关闭所有时钟保留SRAM和寄存器内容功耗降到几十微安可以通过外部中断、RTC闹钟、独立看门狗等唤醒。待机模式功耗最低只有几微安但SRAM内容丢失唤醒后相当于复位只有WKUP引脚、RTC闹钟、独立看门狗能唤醒。选择哪种模式取决于应用场景。如果是电池供电的采集节点每隔几分钟采集一次数据然后休眠用停止模式比较合适因为唤醒后不需要重新初始化外设程序从休眠处继续执行。如果是长期待机的设备偶尔被按键唤醒可以用待机模式但唤醒后要重新走完整初始化流程。进入停止模式的代码很简单调用PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI)。唤醒后系统时钟会自动切回HSI需要重新配置时钟到72MHz。这个细节很容易被忽略导致唤醒后串口波特率不对、定时器变慢。我的做法是在唤醒后立即调用SystemInit重新配置时钟然后再继续执行。8.2 调试接口对低功耗的影响在调试低功耗项目时SWD调试接口本身会消耗电流。如果测量到的休眠电流比数据手册标称值大很多先检查调试器是否还连着。断开调试器后电流通常会降下来。另外GPIO在休眠前要配置成合适的状态悬空的输入引脚会导致输入级振荡耗电应该配置成模拟输入或者带上拉/下拉的输入输出引脚要根据外部电路决定输出高还是低避免形成电流通路。我做过一个温湿度采集节点用DHT11加STM32F103C8T6两节AA电池供电。最初休眠电流测出来有2mA远高于预期的几十微安。排查后发现三个问题一是调试器没拔二是DHT11数据引脚在休眠时仍被驱动为高电平三是有一个LED指示灯忘了关。逐个解决后休眠电流降到30微安左右两节电池理论上能撑一年多。8.3 看门狗与低功耗的配合独立看门狗IWDG由LSI驱动在停止模式下仍然运行可以用来定时唤醒或者作为系统保护。但IWDG一旦启动就不能关闭只能通过复位来停止。如果项目需要在低功耗和看门狗之间灵活切换可以用窗口看门狗WWDG它由APB1时钟驱动停止模式下会停止计数。在停止模式下用IWDG唤醒时要注意IWDG的超时时间由LSI频率决定而LSI频率在30kHz到60kHz之间波动精度不高。如果对唤醒时间精度要求高还是用RTC闹钟更靠谱。RTC可以由LSE驱动32.768kHz晶振精度高得多配合预分频器可以实现精确的秒级、分钟级定时。9. 从工程模板到量产固件的几个经验点9.1 启动文件与链接脚本的匹配STM32F1的启动文件有多个版本对应不同容量的芯片。startup_stm32f10x_ld.s对应小容量16-32KB Flashmd对应中容量64-128KBhd对应大容量256-512KBxl对应超大容量768KB-1MB。选错启动文件会导致栈顶地址和中断向量表位置不对程序可能下载后不运行或者一进中断就死。链接脚本里的ROM和RAM大小也要和实际芯片匹配。STM32F103C8T6有64KB Flash和20KB RAM如果链接脚本里写的RAM是64KB编译能过但运行时变量会跑到不存在的地址表现为随机死机。在Keil里可以在Target选项卡里设置ROM和RAM的起始地址和大小IAR则在.icf文件里配置。9.2 固件升级方案的选择量产产品通常需要预留固件升级接口。STM32F1支持在应用编程IAP通过串口、CAN、USB等接口接收新固件并写入Flash。实现IAP需要把Flash分成Bootloader区和应用程序区Bootloader负责接收和烧写应用程序通过修改中断向量表偏移来运行。中断向量表偏移通过SCB-VTOR寄存器设置应用程序在main函数开头调用NVIC_SetVectorTable(NVIC_VectTab_FLASH, 0x8000)把向量表指向自己的起始地址。这个偏移量必须是0x200的整数倍因为Cortex-M3要求向量表对齐。Bootloader跳转到应用程序前要关闭所有中断和外设设置主栈指针为应用程序向量表的第一个字然后跳转到第二个字指向的复位处理函数。9.3 批量生产时的选项字节配置STM32F1的选项字节控制着读写保护、硬件看门狗、复位后启动模式等。批量生产时通常需要设置读保护防止固件被读取但读保护一旦设置再次烧写就需要先解除保护而解除保护会自动擦除整个Flash。所以产线流程要设计好先烧写固件再设置读保护后续如果要返修升级得先解除保护再重新烧写。写保护可以按页设置把Bootloader所在的页设为写保护防止应用程序意外擦除Bootloader。选项字节的编程可以通过ST-Link Utility或者在自己的Bootloader里用Flash编程接口实现。需要注意的是选项字节的修改需要先解锁Flash修改后系统复位才生效。10. 个人在实际项目中的几点体会STM32F1这颗芯片我用了很多年从学生时代的课程设计到后来的工业产品它几乎陪伴了我整个嵌入式开发生涯。如果说有什么最深的体会那就是“基础不牢地动山摇”。时钟树、中断优先级、GPIO模式这些看似基础的东西一旦理解不到位后面做再复杂的功能都是在沙子上盖楼。我见过太多人急着跑通例程、急着做项目结果遇到问题只能靠猜和试效率极低。另一个体会是不要迷信“最新”的库。HAL库抽象程度高跨系列移植方便但在STM32F1这种资源有限的芯片上标准库的代码效率和直观性反而更好。很多老工程师坚持用标准库不是守旧而是因为标准库的每一行代码你都能看到它在操作哪个寄存器出了问题能直接定位到硬件层面。当然新项目用LL库也是不错的选择它在抽象和效率之间取得了比较好的平衡。最后说一个调试习惯手边常备一个逻辑分析仪。串口通信、I2C、SPI、单总线这些时序相关的问题用逻辑分析仪抓一下波形比盯着代码看半天有效得多。一个几十块钱的八通道逻辑分析仪配合开源软件能解决嵌入式开发中大半的通信问题。我甚至用它抓过DHT11的时序一眼就看出是延时不够导致采样点落在了电平跳变沿上。工具不贵但能省下的时间远超它的价格。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑