十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

STM32F103例程深度解析:从最小系统到Bootloader的实战避坑指南

STM32F103例程深度解析:从最小系统到Bootloader的实战避坑指南 简介这是一套基于STM32F103芯片的外设驱动例程合集主要面向嵌入式初学者、高校学生和需要快速搭建功能原型的开发者目标是帮助读者降低Cortex-M3平台入门门槛。压缩包内共548个文件以头文件、C源码和文本说明为主同时带有启动汇编、链接脚本、工程配置文件覆盖从系统启动到外设驱动的完整代码骨架整体约876KB便于下载和离线查阅。例程覆盖GPIO、定时器、ADC/DAC、UART、SPI、I2C、CAN、USB以及PWM等常见外设每类都提供可独立运行的示例能帮助理解外设初始化、中断服务、数据收发和时钟配置等关键步骤。目前已有431人学习下载配合STM32官方参考手册与HAL/LL库说明开发者可以较快上手STM32CubeMX代码生成方式并将例程中的配置思路迁移到实际项目中缩短开发调试周期整体目录结构清晰便于按外设模块快速检索。 网上随便一搜“stm32f103 例程”能扒下来上百个工程包。我见过不少新手下载下来挨个编译、下载、跑通然后就当完成任务了结果自己画板子或者换一个型号程序立刻翻车。我自己的看法是例程的真正价值不在“能跑”而在“能拆”。这篇文章不打算列一个大而全的例程清单而是挑出大家在搜stm32f103例程时最常遇到的几个场景最小系统、内部晶振、GPIO耐压、PWMDMA、多路ADC、低功耗保存、Bootloader和CDC多串口把背后的原理和坑位一次说清楚。1. 最小系统例程跑不起来的锅一半在硬件1.1 最小系统四件套的排查顺序很多人在开发板上跑例程没问题自己做了一块板子程序烧进去就是没反应。这时候别急着怀疑代码先把最小系统四件事查一遍电源、复位、时钟、启动模式。电源方面VDD和VDDA都要接3.3V每个电源引脚旁边加0.1uF去耦电容芯片的VCAP引脚记得接一个1uF到2.2uF的陶瓷电容这个电容漏了芯片会直接启动失败。复位引脚NRST要接10k上拉到3.3V再对地接一个0.1uF电容如果悬空电磁干扰可能让芯片不断复位。时钟方面如果例程默认用的是外部8MHz晶振板上必须有8M晶振和两个15到22pF负载电容晶振虚焊或电容不匹配都会导致系统卡死。启动模式方面BOOT0必须拉低BOOT1可以任意保证芯片从主Flash启动如果BOOT0悬空上电后可能进入不了用户程序。我自己第一次打样最小系统板时贴片晶振没焊牢程序烧进去就是不跑折腾了整整一下午最后用示波器量OSC_IN引脚发现完全没波形才想到是晶振虚焊。所以例程跑不起来的时候先拿万用表量电压、量复位电平再看时钟波形最后才是怀疑代码。1.2 把例程从外部晶振改到内部晶振有些板子为了省成本没有焊外部晶振但例程默认用HSE结果上电就黑屏。这种情况下可以把系统切换到内部HSI晶振工作。标准库里的SystemInit默认走外部HSE改成HSI需要操作RCC寄存器RCC_DeInit(); RCC_HSICmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_HSIRDY) RESET); RCC_PLLConfig(RCC_PLLSource_HSI_Div2, RCC_PLLMul_16); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() ! 0x08); FLASH_SetLatency(FLASH_Latency_2);这里有个关键点使用内部晶振时PLL的输入固定是HSI/2也就是4MHz最大倍频16倍所以系统主频最高只能到64MHz不能像外部晶振那样跑72MHz。想要满血72MHz就必须用外部8MHz晶振。另外HSI精度大约在-1%到2%之间跑115200这类高波特率串口时偶发乱码很常见USB应用更是基本不能用内部晶振必须用外部晶振才能保证48MHz USB时钟的精度。提示如果只是做简单的GPIO点灯可以直接把HSI 8MHz作为系统时钟跳过PLL代码更简单功耗也更低。2. GPIO的5V容忍与输入模式例程里最容易栽跟头的两个点2.1 FT引脚与5V容忍的边界STM32F103不是所有引脚都能直接接5V的数据手册上标注FT的引脚才具备5V容忍能力。很多人看到FT就以为“这个引脚随便接5V”这其实是个误解。FT引脚作为数字输入时确实可以接受5V高电平而不损坏芯片。但如果这个引脚同时被配置为ADC模拟输入输入电压就不能超过VDDA也就是3.3V。把5V信号直接接到ADC引脚上轻则采样值一直顶在4095重则长期使用损伤引脚。我有块板子就是踩了这个坑一个5V输出的传感器直接进PA0读出来的值永远是满量程换了分压电阻之后立刻正常。输出模式下的“5V容忍”又是一回事。推挽输出模式下引脚高电平只有3.3V没法输出5V但如果你把引脚配置为开漏输出外部上拉到5V输出高电平就是5V。这也是I2C总线常用的做法。很多新手在这里分不清以为开漏就是没输出能力其实开漏配合外部上拉正好是实现电平转换最简单的方式。2.2 按键输入、红外接收与UART帧错误按键例程看起来简单但输入模式配置错了照样翻车。按键一端接GND一端接GPIO内部上拉输入是标准做法按下时读到低电平。但如果你外部已经加了下拉电阻内部上拉就别再开否则上下拉分压引脚电平会处于不确定状态读出来的值忽高忽低。红外接收头是另一个高频踩坑点。像VS1838B这类红外接收头输出引脚是开漏结构需要外部上拉到3.3V空闲时为高电平收到红外信号时输出一串低脉冲。正确的解码方式是用外部中断下降沿触发配合定时器输入捕获测量脉宽。我看到有人在网上问红外解码时遇到HAL_UART_ERROR_FE帧错误怎么办实际上红外接收头根本不是UART设备之所以报FE多半是把红外输出接到了UART的RX引脚上或者串口波特率和电平配置跟红外的脉宽完全不匹配。我建议红外解码老老实实走外部中断加输入捕获别去蹭UART真正的串口调试出现FE先查波特率、查信号共地、查线上有没有干扰还要记得在HAL库回调里清掉错误标志否则会一直卡在错误状态。3. PWMDMA从呼吸灯到WS2812B全彩灯带3.1 呼吸灯例程的两种实现效率完全不同呼吸灯是PWM入门必做核心就是让占空比按三角波或正弦波规律变化。很多例程用定时器更新中断在中断里递增或递减比较寄存器CCR的值。这种方式确实直观但有一个问题1kHz的PWM频率中断就是1ms一次如果还要同时处理ADC、串口、按键扫描CPU忙得够呛。更好的做法是用DMA把所有占空比数据自动灌进CCR寄存器。先把一组波形表算好uint16_t breathTable[256]; for (uint16_t i 0; i 256; i) { breathTable[i] (uint16_t)((arm_sin_f32(2 * 3.14159f * i / 256) 1.0f) * ARR / 2); }然后把定时器PWM频率设为1kHz左右72MHz主频下PSC71ARR999配置DMA循环模式外设地址指向TIM的CCR寄存器内存地址就是breathTable数组传输大小256个半字。启动DMA之后CPU只需要在波形表算好的那一刻做一次启动后面完全不占处理时间呼吸灯自己会一直循环呼吸下去。3.2 WS2812B灯带PWMDMA是F103上最稳的方案WS2812B灯带用的是单线协议每一个bit用高低电平的持续时间来表示0码大概是350ns高电平加800ns低电平1码是700ns高电平加600ns低电平整个bit周期是1.25us对应800kbps。在STM32F103上驱动WS2812B最省心的方案就是把定时器频率设成800kHz把每一个bit都用PWM周期来表达。假设主频72MHzARR设为89PWM频率就是800kHz。0码的CCR设为28左右1码的CCR设为57左右这样每个PWM周期的占空比就对应了WS2812B的一个bit。然后把整条灯带所有灯珠的数据都转换成CCR值放到一个发送缓冲区里再用DMA循环搬运到CCR寄存器CPU只负责往缓冲区里填数据时序完全交给硬件。这个方案里最容易翻车的是引脚和DMA通道的映射关系。比如有人想同时用PA1和PA3输出两路PWMPA1对应TIM2_CH2PA3对应TIM2_CH4中间还隔着个PA2TIM2_CH3。配置复用功能时GPIO要设成AF_PP推挽复用同时还要开启AFIO时钟。至于DMA请求到底走哪个通道必须严格对照参考手册里的DMA请求映射表不是随便指定一个通道就能用的。我自己的排查经验是先写一个最简单的寄存器操作程序确认这个引脚能输出PWM波形用逻辑分析仪或示波器验证占空比和频率再往上叠加DMA。这样哪怕后面不亮也能把问题缩小到DMA配置而不是PWM本身。4. 多路ADC与相位差测量模拟量例程的精度陷阱4.1 4路ADC扫描DMA的配置顺序一步都不能错F103的ADC是12位单通道最高采样率大约1MHz。要做4路ADC扫描加DMA搬运配置顺序要按照固定的套路来ADC_InitTypeDef ADC_InitStructure; DMA_InitTypeDef DMA_InitStructure; uint16_t adcBuf[4]; // ADC1配置 ADC_DeInit(ADC1); ADC_InitStructure.ADC_Mode ADC_Mode_Independent; 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 4; 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_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); // DMA配置外设地址固定为ADC1-DR DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)adcBuf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize 4; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); ADC_SoftwareStartConvCmd(ADC1, ENABLE);这里有两个细节容易忽略。一个是采样时间如果信号源内阻大采样时间一定要拉长到239.5个周期否则采样电容还没充满就开始转换读数会系统性偏低。另一个是通道串扰扫描模式下前一个通道的采样电容残留电荷会影响后续通道的读数表面上看是“通道2被通道1带偏了”实际解决办法就是拉长采样时间或者降低转换速率。还要泼一盆冷水在Proteus这类仿真软件里跑4路ADC程序电位器一拖数值稳如泰山看着很理想。但实际板子上如果VREF引脚滤波不好、地线噪声大ADC读数跳几个LSB是家常便饭。做产品的话建议ADC引脚前加RC低通PCB上做好模拟地和数字地分割软件里再做中值平均三重保险。4.2 用输入捕获测相位差比ADC过零检测更省事搜索“stm32f103 测量相位差”的人很多是想测两路工频或音频信号的相位。如果信号是方波最省事的方案是定时器输入捕获而不是ADC采样。用TIM2的两个通道分别接A信号和B信号捕获两路上升沿的时间戳差值除以整个周期就是相位差uint16_t ch1Val TIM_GetCapture1(TIM2); uint16_t ch2Val TIM_GetCapture2(TIM2); uint16_t diff (ch2Val ch1Val) ? (ch2Val - ch1Val) : (ch2Val TIM2-ARR 1 - ch1Val); float phaseAngle (float)diff * 360.0f / (float)(TIM2-ARR 1);用这个方法要注意定时器的时基必须能覆盖整个信号周期ARR要设置成比信号周期对应的计数值大否则会出现捕获值回绕算出来的相位差完全错误。另外输入捕获引脚要配成浮空或上拉输入最好加一个施密特触发器整形不然信号边沿抖动会让测量结果跳变。如果信号是正弦波没法直接用输入捕获再用ADC同步采样两路做互相关或者过零检测。F103的算力做FFT有点吃力但过零检测完全够用两路同时采样分别记录各自从负到正的过零时刻两个过零时刻的时间差就是相位差。5. 停机模式与掉电保存低功耗项目的最后一公里5.1 停机模式的进入与唤醒核心是时钟恢复F103的三低功耗模式里停机模式是性能和功耗平衡得最好的一个CPU停了外设时钟全停但是SRAM和寄存器内容全部保留唤醒速度又比待机模式快得多。进入停机的代码只要一行PWR_EnterSTOPMode(PWR_Regulator_LowPower, PWR_STOPEntry_WFI);但唤醒之后有个大坑系统时钟会自动切回HSI如果你原本用的是PLL输出的64MHz或72MHz必须重新初始化时钟。总之一句话唤醒代码的第一件事就是重新跑一遍时钟配置然后重新初始化用到的外设。还有一个功耗上的细节。直接调用上面的函数进停机电流可能还有好几毫安。原因是你根本没管GPIO。正确的做法是进入停机之前把不用的外设时钟关掉把GPIO全部配置为模拟输入避免引脚上的漏电流路径。我实测过同样一块板子GPIO什么都不做直接停机是2.8mA把GPIO全部设成模拟输入后能降到28uA左右差了整整100倍。5.2 掉电保存的三条路按场景选掉电保存数据F103有三种常见方案备份寄存器、内部Flash、外部EEPROM。备份寄存器最简单F103有10个16位的备份寄存器编号BKP_DR1到BKP_DR10。使用之前要先使能PWR和BKP时钟再调用PWR_BackupAccessCmd(ENABLE)打开备份区访问权限。它的优势是VBAT引脚接电池时主电源掉电数据不丢读写速度也快特别适合保存配置参数、校准值这种小数据。缺点就是容量小存不了多少东西。内部Flash适合保存开机次数、累计工作时长这种低频写入的数据。F103内部Flash的擦写寿命大约是1万次不能拿来频繁记录否则很快报废。写入前要先擦除整个页再执行编程操作。如果确实要频繁写就需要做磨损均衡轮流往多个页里写把整个Flash的寿命平摊开。如果要频繁保存比如掉电瞬间保存多个采样点建议用外部EEPROM比如M24C32I2C接口百万次擦写寿命完全不用担心磨损。配合PVD可编程电压检测器可以在主电源开始跌落时触发中断趁电压还没降到芯片最低工作电压之前把关键数据写进EEPROM。PVD阈值可以选2.8V左右注意EEPROM写入需要时间掉电跌落太快可能写不完所以硬件上要在电源输入端加大电容给写入操作争取几个毫秒的时间窗口。6. 系统级例程进阶Bootloader与CDC多串口6.1 Bootloader分区与跳转中断向量表必须重新定位在线升级是很多项目的刚需做IAP Bootloader最核心的就是分区和跳转。以STM32F103C8T6为例Flash共64KB我习惯把Boot区放在0x08000000到0x08007FFF占32KBApp区从0x08008000开始占剩下的32KB。Boot区代码负责通过串口接收固件写入App区然后跳转。跳转的核心代码很简单#define APP_ADDR 0x08008000 void JumpToApp(void) { uint32_t appStackAddr *(volatile uint32_t *)APP_ADDR; uint32_t appResetAddr *(volatile uint32_t *)(APP_ADDR 4); void (*appResetHandler)(void); // 检查栈顶地址是否合理防止跳到空Flash if ((appStackAddr 0x2FFE0000) ! 0x20000000) { return; } // 关闭全局中断跳转前清掉中断状态 __disable_irq(); NVIC_DeInit(); SysTick-CTRL 0; // 设置主栈指针跳转到App复位函数 __set_MSP(appStackAddr); appResetHandler (void (*)(void))appResetAddr; appResetHandler(); }很多新手在这里栽跟头用烧录器直接往0x08008000地址烧App跳转也成功了但程序一跑就进HardFault。原因多半是App工程的中断向量表还是默认从0x08000000开始中断来了之后CPU去0x08000000找向量表找到的是Boot区的内容自然就乱了。解决办法是在App的main函数最开始把中断向量表偏移量设置到App区SCB-VTOR 0x08008000;同时App工程里IROM1的起始地址要改成0x08008000大小改成0x08008000开始的剩余空间。这两个地方不改无论Boot写得再好App照样跑不起来。6.2 CDC多串口和FreeRTOS并行USB时钟必须准F103的USB是FS设备搞CDC虚拟串口是很多人的进阶目标。如果要做“多串口”有两条路。一条是做USB CDC复合设备通过IAD接口关联描述符把两个CDC接口合成一个复合设备本文还有配套的精品资源点击获取
返回列表