STM32F103开发板入门指南:从硬件认知到项目实战全解析 STM32F103开发板到手那天我相信你和当年的我一样内心是“老子要开始搞嵌入式了”的豪迈。但接下来几天你可能就会经历找不到芯片第一脚、固件包装不上、编译通过却烧录失败、延时函数莫名卡死这一连串组合拳。这篇不整虚的就是把F103开发板从开箱到跑通完整项目的路重新走一遍重点放在那些教程里不会明说的选型逻辑和翻车现场。全文围绕STM32和开发板两个核心词展开覆盖环境搭建、标准库工程模板、时钟树、定时器输入捕获、USB设备实现、CAN通信排查等高频场景适合刚买了板子准备入门、或者已经卡在某个外设调不通的读者。1. 开箱先别急着通电先把板子上的东西认清楚1.1 芯片第一脚怎么确认这是新手第一课所有人拿到板子第一步都会问STM32芯片第一脚到底在哪。别笑这个问题直接影响你后面所有硬件接线判断。绝大多数F103开发板用的是LQFP封装四边有引脚贴片封装芯片表面左下角有一个圆形凹点或者丝印小圆点这个点对应的位置就是第1脚。注意是“芯片正放时左下角”。有凹点的那条边朝下芯片型号丝印朝上且文字是正立的此时左下角是第1脚然后逆时针绕一圈引脚编号依次是1、2、3……直到最后一只脚。我在新板子到手后一定会拿数据手册的引脚图对一遍。以STM32F103RCT6为例LQFP64封装第1脚是VBAT备份电池供电第7脚是PC13第16脚是PB9。你只要把丝印圆点找对然后按这个方向数引脚就不会接错。要是只凭感觉插排线反接一次芯片或者传感器基本就交代了。另外很多板子上的引出排针旁边会印丝印小字比如“PB0”“PA9”之类的这些是对应GPIO端口的引出。如果发现丝印方向和芯片引脚对应不上那就以丝印为准因为板厂布线的时候可能调整了位置。但芯片本身的第1脚一定按数据手册和丝印圆点来判断。1.2 开发板上的硬件单元分别是什么把板子翻过来看F103开发板上一般有几大件核心MCU芯片、稳压芯片、晶振、复位电路、USB转串口芯片、LED和按键、调试接口。STM32F103主芯片是核心常见型号有RCT6256KB Flash64引脚和ZET6512KB Flash144引脚。绝大多数入门板用的RCT6因为引脚少、好布线、价格便宜而且对外设的支撑足够你学完整个入门阶段。稳压芯片一般是AMS1117-3.3把USB的5V或者外部电源的5V降到3.3V给芯片供电。注意AMS1117是线性稳压输入输出压差大时会发热所以不要拿它去驱动大电流负载。晶振主晶振8MHz是系统时钟的来源通过PLL倍频到72MHz。RTC晶振32.768kHz很多板子省略了因为内部的低速RC也能凑合用。复位电路RESET按键加一个10K上拉电阻加一个104电容。按下去拉低NRST脚芯片复位。上电瞬间电容充电也会产生一个复位脉冲。USB转串口芯片最常见的是CH340也有CP2102。它把USB信号转成UART信号连到芯片的PA9USART1_TX和PA10USART1_RX实现电脑和芯片之间串口通信。这就是你printf打印调试信息的通道。LED和按键一般有电源指示灯PWR常亮用户LED接某个GPIO比如PC13或者PB12用户按键接某个GPIO比如PA0或者PB0。具体接哪个脚每个板卡不一样拿到板子先看原理图别想当然。我见过不少人在这一步就跳过了直接看教程里写PC13就去点灯结果板子上的LED根本不亮因为他的板子LED接的是PB12。所以“先认板子再动手”这句话是真的能帮你省下半天排查时间。1.3 上电前的安全检查新板子到手不要直接插USB。先做两件事第一用万用表蜂鸣档量一下板子上VCC和GND之间的阻值如果接近0说明板子有短路可能某个电容焊反或者有焊锡搭桥这种板子插电大概率直接烧东西。第二确认BOOT0跳线和BOOT1跳线的位置。BOOT00是正常运行模式从这个模式启动才能跑你的程序BOOT01时从系统存储器启动主要用于通过串口烧录固件。绝大多数正常使用场景下BOOT0跳线应该接GND。如果板子默认跳线在1那边你插上电程序不会运行现象是“什么都正常但代码不执行”。检查完这两项再插USB板子上的电源灯亮说明3.3V正常芯片在跑。上电后用万用表量一下芯片VDD引脚对GND的电压如果稳定在3.3V左右基础供电就放心了。此时如果你的板子出厂自带测试固件可能会看到LED闪烁或者屏上有点东西没有也无所谓你自己烧一个进去就完了。2. 开发环境搭建与工具链选型2.1 Keil MDK 5的安装新手首选IDE但暗坑不少STM32开发用Keil MDKMicrocontroller Development Kit是历史最悠久、教程最多、生态最成熟的选择。MDK基于uVision IDE针对ARM Cortex-M内核做编译优化调试界面直观能看寄存器、看外设、断点调试。新手我用Keil原因只有一个你遇到任何问题网上搜到的答案90%都是基于Keil的这会大幅降低你入门期的挫败感。安装时的第一个坑是版本分裂。Keil官网有MDK和C51两个完全独立的安装包MDK用来开发ARM单片机C51用来开发8051内核的单片机。如果你的板子是STC89C52这类51单片机你得装C51版本STM32得装MDK版本。两个版本的安装路径必须分开千万别装到同一个目录里否则其中一个的编译器会被覆盖导致工程报“No target device”之类的错。热词里“keil5兼容c51和stm32安装”问的就是这个答案是分开装路径别重叠装完MDK后通过Pack Installer装对应的芯片支持包就能和平共存。第二个坑是芯片包没装。MDK本体只是一个IDE它不知道STM32F103的存在你必须安装设备支持包Device Family Pack。在Pack Installer里搜“STM32F1”装最新的“Keil::STM32F1xx_DFP”包。装完在新建工程时芯片型号列表里才能找到STM32F103RCT6或者对应的型号。如果没装包新建工程会卡在选择芯片那一步根本进不去。第三个坑是License。MDK默认评估版限制代码体积在32KB以内。F103入门程序虽然小但到了USB、CAN这些功能加进去之后很容易超限。解决办法是激活License正版购买或学校提供的license。有些所谓“破解”工具会把编译器版本搞坏建议优先用正版途径很多高校和公司都有正版授权。2.2 VSCode配置STM32开发环境老手的进阶路线Keil是老牌IDE但它的编辑器、代码补全、Git集成体验确实跟不上时代。用VSCode做STM32开发近年也越来越流行体验更现代还能和Clangd、Cortex-Debug这些插件搭配。但VSCode本身不是完整的嵌入式IDE它得靠插件和外部工具链组合。标准方案是“EIDE插件 ARM GCC工具链 OpenOCD调试 ST-Link烧录”。EIDEEmbedded IDE插件帮你在VSCode里管理工程、生成makefile不用手工写复杂的构建脚本。ARM GCC工具链arm-none-eabi-gcc负责编译。OpenOCD负责通过调试器连接芯片并烧录。Cortex-Debug插件则提供调试界面支持断点、变量查看。VSCode方案的优势是编译速度快、代码提示好、git管理方便。缺点是前期配置复杂我见过某些人对着launch.json调了一晚上没跑起来。如果你是纯新手第一次学STM32我仍然推荐Keil。等你在Keil里能把工程建明白、点灯、串口、定时器这些玩一圈了再切换到VSCode也不迟。那时候你对工程结构和编译流程已经有概念配置VSCode的参考价值远高于试错成本。2.3 烧录器选型与“编译成功却烧录不进去”板子上的程序最终要下载到Flash里才能独立运行。F103支持JTAG和SWD两种调试接口实际使用中一律建议用SWD只用4根线SWDIO、SWCLK、GND、3.3V。SWD比JTAG占用的引脚少、接线简单、且在芯片被禁用JTAG后依然能救回来。常见烧录器按价格从低到高ST-Link V2ST官方调试器兼容性好价格便宜二三十块新手首选。注意市面上存在大量“非官方”ST-Link个别版本固件有问题或者驱动异常买之前找评价稳定的店铺。J-LinkSEGGER出的调试器功能最强支持各种内核和Flash算法还有RTT等高级调试功能。但正版贵盗版满天飞新手不建议一上来就用盗版J-Link容易遇到“无法识别盗版克隆”的报错。DAPLinkARM官方开源方案免驱动基于CMSIS-DAP协议价格甚至比ST-Link还便宜也很好用。“编译成功却怎么也烧录不进开发板”是热搜里的高频问题。这套问题我拆开来看大多数就那么几个原因第一驱动没装或者设备没识别。电脑插上调试器后打开设备管理器如果看到一个未知设备或者黄色感叹号说明驱动有问题。ST-Link需要装ST官方驱动DAPLink免驱但个别系统需要手动安装驱动。第二Flash算法Flash Algorithm选择错误。Keil的Options - Debug - Settings - Flash Download里会列出烧录算法。F103的Flash算法要选对容量RCT6256KB选择“STM32F10x Med-density Flash 128K”或者对应的256K算法ZET6512KB选择“STM32F10x High-density Flash 512K”。选错了下载时会出现“Erase Failed”或者“Flash Download failed - Target DLL has been cancelled”的报错。这种问题热搜里那句典型报错“load project.axf error flash download failed”基本就是Flash算法配置错误或者芯片型号选错导致的。第三接线问题。SWDIOPA13、SWCLKPA14、GND、3.3V四根线必须一一对应接反任意一根都可能连不上。特别是GND如果调试器没和板子共地就完全无法通信。第四BOOT0设置导致芯片处于下载模式或者运行模式冲突。正常下载时BOOT0接GND有些板子默认跳线接3.3V芯片启动到内置BootloaderKeil连接时芯片ID读不出来自然就下载失败。把上面四条按顺序排查九成以上“烧录不进”都能解决。剩下那成大概率是芯片已经锁死JTAG被禁用、读保护开启需要通过ST-Link Utility或者按住复位键的同时点下载来解锁这个后面单独说。3. 工程模板与系统架构从点灯到真正理解STM323.1 标准库还是HAL库先讲清楚再选STM32的开发库有两套主流标准外设库Standard Peripheral Library简称标准库和硬件抽象层库HAL库。标准库是ST早期官方提供的库直接封装寄存器的操作比如GPIO_SetBits、TIM_Cmd这种函数层次浅、代码容易看懂、执行效率高、学习价值大。2016年之后ST官方不再更新标准库但F103作为经典芯片标准库的例程和教程存量极其庞大教学资源丰富。HAL库是现在ST主推的库配合STM32CubeMX图形化配置工具可以自动生成初始化代码用起来像是填表而不是写代码。HAL库的上手快、跨芯片迁移方便但代码量大、调用层次深出了问题debug起来更费劲。对新手我的建议是直接学标准库。原因很直白标准库离寄存器更近你点灯时能感受到“我设置了某个寄存器、使能了某个时钟、配置了某个模式”这个过程理解的是芯片本身。HAL库里一个HAL_GPIO_Init函数内部干了什么你不会去追究等问题来了也查不动。等标准库的背景知识建立起来再上手HAL和CubeMX几乎一天就能磨平。相反先学HAL库一年后可能连NVIC中断优先级是啥都说不清。3.2 标准库工程目录结构哪些文件一个都不能少标准库新建工程的方式网上教程很多但很多人跟着做完了还是编译不过问题多半是漏了文件或者选错启动文件。我习惯的工程模板按功能分四个文件夹CORE目录存放启动文件和内核相关文件。startup_stm32f10x_xx.s启动文件负责设置初始栈指针、初始化向量表、调SystemInit、进入main是程序运行的起点。core_cm3.h和core_cm3.c是Cortex-M3内核的寄存器定义和内核操作函数。这两个文件从标准库的CMSIS目录拷过来。SYSTEM目录我自己写的一些基础驱动比如delay延时、usart串口、sys系统初始化。这些文件网上有大量共享版功能都一样拷下来用就行。HARDWARE目录存放你自己写的外设驱动比如led.c、key.c、timer.c每个外设一个.c和一个.h方便管理。USER目录存放main.c、stm32f10x_it.c中断服务函数和stm32f10x_conf.h外设配置头文件等。标准库本体在Libraries目录下包含CMSIS内核相关和STM32F10x_StdPeriph_Driver外设驱动源文件这两个目录也会被包含进工程里。选启动文件的时候要看清楚密度分类低密度LDFlash ≤ 32KB型号如F103C416KB、F103C632KB用startup_stm32f10x_ld.s。中密度MDFlash 64-128KB型号如F103C864KB、F103CB128KB用startup_stm32f10x_md.s。我手头这块最便宜的蓝色开发板就是C8T6属于MD。高密度HDFlash ≥ 256KB型号如F103RCT6256KB、F103ZET6512KB用startup_stm32f10x_hd.s。很多人的“新建工程”教程是照着ZET6的板子写的用的hd启动文件你拿一个C8T6的板子照抄烧录进去程序可能能跑但Flash容量配置和启动文件不匹配在某个临界点就会出现诡异问题。所以新建工程第一步就是确认自己芯片的密度级别选对startup文件。另外不要忘记在工程里加入宏定义“STM32F10X_MD”或“STM32F10X_HD”这个宏决定USB等外设模块的内部资源映射不添或者添错了USB库可能编译出错。C8T6对应的宏是STM32F10X_MDRCT6和ZET6都是STM32F10X_HD配合USE_STDPERIPH_DRIVER宏一起定义。3.3 时钟系统架构STM32的“命脉”STM32F103的系统时钟也就是主频默认最高72MHz。整个时钟树从三个振荡源出发HSI内部8MHz RC振荡器精度一般上电后默认系统时钟就是它内部8MHz经过除以2变成4MHz稳定但慢。HSE外部8MHz晶振精度高配合PLL锁相环倍频到72MHz是绝大多数应用的主时钟来源。LSI内部40kHz低频RC主要用于独立看门狗和RTC。LSE外部32.768kHz晶振主要用于RTC时钟低功耗时钟。“为什么点灯之前必须先配置时钟”这个问题很多新手没想明白。STM32的每一个外设都有独立的时钟开关你使用一个外设前必须先使能它的时钟否则写寄存器不生效。GPIO挂在APB2总线上所以点灯前必须先调用RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOx, ENABLE)。时钟配置的常见路径是上电默认HSI 8MHz - 软件配置切换到HSE外部晶振 - 等待HSE就绪 - 配置PLL倍频通常9倍8MHz×972MHz - 等待PLL锁定 - 把系统时钟切换到PLL - 配置AHB分频器为172MHz、APB1分频器为236MHz因为APB1总线最高只能到36MHz、APB2分频器为172MHz。这段逻辑在标准库的system_stm32f10x.c里已经写好了SystemInit()函数做了基本的RCC配置。但你直接从别的工程把SystemInit拷贝过来时可能它里面对时钟树初始化写得不对比如没有等待PLL锁定就会导致系统跑在8MHz甚至外部晶振起振失败后自动切回HSI。表现就是延时函数时间不对、串口波特率错乱、定时器时间全乱。这类问题没有报错信息全会以“莫名其妙”的形式出现很坑。3.4 点灯实操GPIO配置全解点灯的本质是控制GPIO引脚的输出电平。F103的GPIO每个端口有16个引脚每个引脚可以配置为多种模式输入浮空、输入上拉、输入下拉、模拟输入、开漏输出、推挽输出、复用推挽、复用开漏。点灯用推挽输出。推挽的意思是输出高电平时内部P-MOS导通输出VCC输出低电平时N-MOS导通输出GND。这个结构能提供较大的驱动电流且高低电平都强。开漏输出则只能主动拉低要输出高电平必须外部接上拉电阻适合I2C这种多设备共享总线场景线与逻辑。点灯的标准步骤和代码骨架第一步使能GPIO时钟。LED在哪个端口就使能哪个端口。比如LED接PC13需要调RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE)。第二步配置GPIO引脚模式和速度。用GPIO_InitStructure结构体GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; // 输出速度50MHz GPIO_Init(GPIOC, GPIO_InitStructure);第三步控制电平。置高电平用GPIO_SetBits(GPIOC, GPIO_Pin_13)置低电平用GPIO_ResetBits(GPIOC, GPIO_Pin_13)。然后配合一个延时函数让LED交替亮灭。LED极性问题要注意如果LED一端接GPIO、另一端接VCC那么GPIO输出低电平时LED亮如果另一端接GNDGPIO输出高电平时LED亮。看原理图确定初始电平不然你的“亮灭”是反的。GPIO_Speed这个参数新手容易忽略它不影响功能只影响输出边沿的陡峭程度和电磁干扰。点灯50MHz没问题I2C上拉通信可以设低一点。如果GPIO配置过慢而外设要求高速翻转波形会变形导致通信失败。3.5 延时函数为什么卡死SysTick与HSE起振失败热搜词里“stm32延时函数delay卡死”绝对是最经典的问题。先解释卡死的两种典型原因。第一种SystemInit没有完成时钟切换系统还是8MHz内部RC但delay_init函数你传的参数是72MHz。SysTick定时器的计时基准是系统主频或者主频除以8你按72MHz配置成1ms中断一次实际时钟只有8MHz两者频率不匹配看起来就是延时时间错乱但不至于卡死。真正的卡死往往是因为SysTick中断服务函数根本没有被执行或者中断标志位没有被正确清除。第二种更隐蔽外部晶振起振失败。如果板子上的8MHz晶振虚焊、负载电容没焊、或者晶振损坏SystemInit里等待HSE就绪那步会超时这时库函数会尝试用HSI继续运行有些版本的SystemInit处理不好会卡在RCC_GetFlagStatus(RCC_FLAG_HSERDY)的死循环里直接导致程序连main都进不去现象就是“烧录成功后完全没反应”。这种情况排查方法是Debug在SystemInit的while循环里看到PC指针卡住了就知道是晶振问题。用万用表量晶振两脚电压正常应该是芯片供电电压的一半左右约1.6-1.8V如果量到0V或者等于3.3V说明晶振没有起振。SysTick的实现要点SysTick是一个24位向下计数器时钟源可以选择系统时钟或系统时钟除以8。标准库delay的常见做法是static u8 fac_us 0; static u16 fac_ms 0; void delay_init(u8 SYSCLK) { SysTick-CTRL ~(1 2); // 选择时钟源为HCLK/8 fac_us SYSCLK / 8; fac_ms fac_us * 1000; } void delay_us(u32 nus) { u32 temp; SysTick-LOAD nus * fac_us; // 设置加载值 SysTick-VAL 0; SysTick-CTRL 0x01; // 使能计数器使用SysTick异常 while (1) { temp SysTick-CTRL; if (temp 0x10000) break; // 检查COUNTFLAG } SysTick-CTRL 0x00; // 关闭计数器 }核心逻辑是把目标延时时间换算成SysTick需要计数的周期数加载到LOAD寄存器然后轮询COUNTFLAG标志位。如果你自己写延时务必注意LOAD寄存器的值是24位的最大166666660xFFFFFF延时特别长的毫秒级要分段处理否则计数器溢出产生异常程序就跑飞了。4. 热门应用场景拆解从基础到进阶的项目方向4.1 超声波测距定时器输入捕获的实际玩法超声波测距是F103入门后第一个有“项目感”的实践。HC-SR04这个模块结构简单Trig引脚触发给它一个10us以上的高电平触发脉冲模块内部自动发出8个40kHz的超声波脉冲同时Echo引脚输出一个高电平脉宽这个脉宽的时间就是声波往返的时间往返时间乘声速除以2就是距离。距离公式距离(cm) Echo高电平时间(s) × 声速(约340m/s) / 2 × 100比如Echo高电平时间是580us那么距离是 0.00058 × 340 / 2 × 100 9.86cm。注意声速受温度影响温度每升高1℃声速大约增加0.6m/s。在车库或者室内环境按340算误差不大但如果你做高精度测量建议加温度补偿。标准库实现思路就是用定时器输入捕获功能测量Echo引脚高电平的持续时间。选一个定时器比如TIM2把通道1映射到Echo引脚配置为输入捕获、上升沿触发捕获捕获到CCR1的值后切换为下降沿捕获在下降沿中断里计算脉宽。关键代码流程void TIM2_Cap_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_ICInitTypeDef TIM_ICInitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_0; // PA0作为输入捕获 GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period 0xFFFF; TIM_TimeBaseStructure.TIM_Prescaler 71; // 72MHz/721MHz即每1us计1次 TIM_TimeBaseStructure.TIM_ClockDivision 0; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM2, TIM_TimeBaseStructure); TIM_ICInitStructure.TIM_Channel TIM_Channel_1; TIM_ICInitStructure.TIM_ICPolarity TIM_ICPolarity_Rising; // 上升沿捕获 TIM_ICInitStructure.TIM_ICSelection TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler TIM_ICPSC_DIV1; TIM_ICInitStructure.TIM_ICFilter 0x0F; TIM_ICInit(TIM2, TIM_ICInitStructure); TIM_ClearFlag(TIM2, TIM_FLAG_CC1 | TIM_FLAG_Update); TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); TIM_Cmd(TIM2, ENABLE); }在中断服务函数里根据捕获边沿状态记录第一次捕获值CCR1和第二次捕获值CCR2CCR2-CCR1就是脉宽对应的微秒数。预分频设成71计数频率正好1MHzCCR的值直接就是us计算距离就是脉宽值×声速/2。一个我踩过的坑是上升沿捕获触发的是同一个中断如果不切换捕获边沿第二次也会触发上升沿中断导致两个CCR都是同一个值脉宽算出来是0。所以必须在第一次捕获后立即把捕获极性改为下降沿在第二次捕获后重新改为上升沿。4.2 定时器捕获测频率用输入捕获实现频率计测量一个外部信号的频率更直接的方法是定时器输入捕获模式。原理是如果定时器以已知频率计数外部信号每次上升沿触发捕获连续两次捕获的计数差值就是信号的周期以定时器时钟周期为单位频率就是计时频率除以差值。比如定时器TIM4预分频到1MHz每us计一次数外部信号频率是10kHz那一个周期是100us两次上升沿捕获值之差就是100。频率 1000000 / 100 10000Hz。实现时配置TIM4的输入捕获并且开启捕获中断中断服务函数里把本次捕获值CNT和上一次的值做差。需要处理计数器溢出的情况如果被测频率很低比如几Hz计数差值会超过FFFF这时需要开启定时器更新中断在更新中断里对溢出次数进行累加周期计算时加上65536×溢出次数。这个玩法是标准库定时器模块的综合练习理解了输入捕获后面写PWM捕获、电机测速、红外解码都会顺畅很多。红外遥控解码本质上就是测量38kHz载波上高低电平的脉宽和超声波测距用的套路完全一样。4.3 按键模块电路设计上拉下拉与消抖按键电路在开发板上看着简单但你接传感器或者其他模块时按键设计的问题就暴露了。最常用的接法是按键一端接GND另一端接GPIO同时在GPIO上接一个10K电阻到VCC上拉电阻。按键未按下时GPIO读到高电平按下后GPIO被拉低读低电平。这种接法的好处是GPIO在空闲状态有确定的电平不会因为悬空而在高低电平之间抖动而且抗干扰能力好。反过来按键一端接VCC另一端接GPIOGPIO接下拉电阻到GND未按下读低按下读高也是可以的。真正坑的是抖动。机械按键在按下瞬间和松开瞬间触点会发生几次毫秒级的机械弹跳波形表现为边沿附近的高低振荡。如果在中断里读电平一次按键可能触发好几次中断程序会认为按了多次。解决办法是软件消抖检测到电平变化后延时10-20ms再读一次如果电平状态稳定了才确认按键有效。也可以不用延时在定时器中断里每5ms扫描一次按键状态连续两次读到相同电平才确认这样不阻塞主循环适合按键数量多的场景。按键的GPIO配置带外部上拉的按键GPIO配置为输入浮空或者输入上拉都行如果板子没有外部上拉电阻必须配置为输入上拉GPIO_Mode_IPU否则引脚悬空读到的电平是随机的。这个细节非常基础但我真见过有人按键一直读不对最后发现GPIO配成了下拉而电路是按键接GND接上拉的逻辑完全反了。4.4 如何把STM32做成一个USB设备从HID到CDC热搜里“stm32 如何做usb设备”是个高频需求。STM32F103系列是具有USB Device功能的USB device 2.0 FS可以做成HID键盘鼠标、自定义HID、虚拟串口CDC等常见设备类型。但先泼一盆冷水不是所有F103板子都能直接玩USB。USB功能对时钟有硬性要求它需要一个精确的48MHz时钟必须由外部8MHz晶振通过PLL得到或者由USB专用PLL倍频产生。如果的板子上没有8MHz晶振有些极简板子只靠内部RC时钟做USB就是痴人说梦因为内部HSI的精度±1%左右达不到USB规范要求的±0.25%USB主机无法正确识别设备。所以玩USB前先用眼睛确认板子上有8MHz晶振。USB HID设备最简单特点是免驱动。电脑把它识别成一个“人体学输入设备”可以上报按键、鼠标相对位移等数据。把F103做成一个自定义HID设备就能通过一个普通C#或者Python程序读写USB HID数据做上位机和下位机交互。实现要点是修改USB描述符设备描述符、配置描述符、接口描述符、HID描述符、端点描述符并把报告描述符改成你要的输入输出报表格式。报告描述符是HID最绕的部分它定义了数据包的结构和用途写错了电脑认不出来。USB CDC虚拟串口也是免驱动的Windows 10以上系统自带实现后电脑识别成一个COM口用串口助手就能收发数据传输速度比UART高而且不占用USART外设。CDC的核心是两点配置描述符里需要两个接口一个通信接口一个数据接口以及端点方向要正确使用端点1输入、端点2输入、端点2输出。我刚开始做USB设备时也踩过一个巨坑标准库的USB例程里外部晶振配置是通过PLL倍频到48MHz还是72MHz是有冲突的。USB模块内部有自己的PLL从APB1的时钟域提取时钟很多USB例程要求系统时钟配置成48MHz或者72MHz但PLL配置参数要变。当时我按72MHz配置好系统时钟USB枚举不上后来发现例程usb_pwr.c里对时钟有单独配置要从48MHz的主频参数重新算PLL这属于USB移植中比较隐蔽的一个环节。4.5 CAN通信连不上的排查思路CAN总线在工业领域应用极多F103内置bxCAN控制器支持标准帧和扩展帧速率最高1Mbps。但CAN不是简单把两个芯片的CANH、CANL接一起就能通的它需要总线两端各有一个120Ω终端电阻用于匹配阻抗、减少反射。如果你的板子没有集成这个电阻要在CANH和CANL之间跨接一个120Ω电阻每端的节点都要有。“stm32 can通信突然连不上”这类问题的常见原因按概率排序波特率不匹配。CAN的波特率由位时序决定F103的APB1时钟和预分频、时间段1、时间段2、同步跳转宽度共同决定。如果两个节点配置的波特率不同总线会一直发送错误帧表现为接收不到任何消息。排查时可以先确定两端的APB1时钟频率是否一致再比较位时序参数。过滤器配置错误。CAN接收有自己的验收过滤器Filter会把不匹配的消息ID直接滤掉。新手最容易忘的是初始化了CAN却忘记配置过滤器或者把过滤器设成了屏蔽所有消息的模式ID全部为0Mask全为0导致什么消息都收不到。回环模式没改。bxCAN有Normal模式、回环模式LoopBack和静默模式。回环模式是自发自收不经过总线很多初始化例程默认回环测试做完回环测试后忘了切回Normal模式外部节点自然收不到数据。收发器问题。F103的CAN控制器引脚是CAN_RX和CAN_TX它们是TTL电平的不能直接挂到总线上必须经过CAN收发器比如TJA1050、SN65HVD230转换成差分信号。排查CAN问题我自己的习惯是先设置回环模式在板子上自发自收测试确认控制器和中断链路是通的然后切回Normal模式用示波器或者逻辑分析仪看CANH和CANL之间的差分波形确认有数据在总线上最后再逐项核对波特率参数和过滤器。这样能逐步缩小问题范围。5. 新手最容易踩的坑排错实录5.1 JTAG/SWD引脚被禁用导致无法烧录最常见也最绝望的坑你写了一段代码调用了GPIO_PinRemapConfig(GPIO_Remap_SWJ_Disable, ENABLE)把SWJ调试接口整个关了。理由可能是你想把PA13、PA14、PA15这组引脚解放出来当普通IO用但问题是关掉之后下载器再也连不上芯片程序烧不进去板子变成“砖”。我自己第一次遇到这事时以为板子坏了后来才知道有解救手段。方法一按住板子上的复位键不放在Keil里点击下载瞬间松开复位键让芯片在复位期间运行下载流程这招成功率不低。方法二用ST-Link Utility或者STM32CubeProgrammer以“连接时复位”Hardware Reset模式擦除整个Flash擦完后芯片恢复出厂状态SWD又回来了。方法三如果没有下载器用串口ISPBOOT0拉高复位进入内置Bootloader通过串口擦除Flash也能救回来。结论干任何事之前都别轻易关SWJ除非你百分百确保烧录链路没问题。F103的PA13/PA14/PA15这三只脚在SWD模式下是不能当普通IO用的PA15可以复用为TIM2_CH1等但PA13/PA14是SWDIO和SWCLK复用很麻烦实在缺IO就换大封装芯片不要为省两个引脚惹这种麻烦。5.2 供电不足导致的各种诡异故障开发板插USB供电时USB口的5V一般能提供500mA电流标准USB2.0口。板子本身耗电很少但如果你外接了舵机、电机驱动板、多个传感器模块电流很容易超。舵机转起来瞬间电流能有几百毫安甚至上安培如果直接取板子的5V和3.3V电压瞬间跌落芯片会复位。表现是舵机一动板子重启串口打印中断LED灭一下又亮。这种情况不要死磕代码量一下板子供电端的电压用外部5V电源单独给外设供电并且把外设的GND和板子的GND接在一起共地问题立刻消失。共地这个点要特意强调。外部电源给舵机供电、板子USB供电两个电源如果不共地舵机的控制信号和板子之间没有电压基准PWM控制会乱套甚至烧引脚。不管什么外设只要它和板子之间有信号连接GND必须接一起。5.3 启动文件选错导致Flash容量识别错误前面提到启动文件按密度分这里讲一个真实场景。有个朋友买了一块F103C8T6最小系统板64KB Flash中密度照着网上的ZET6例程高密度建工程把startup_stm32f10x_hd.s拷进去了宏定义也写了STM32F10X_HD。编译烧录一切正常程序能跑但Flash容量识别成了512KB。当他程序写到接近64KB时或者说当Keil按HD的512KB Flash算法去擦除时下载偶尔报错程序也出现诡异跳变。这类问题在项目后期非常难查。所以每次新建工程第一件事就是看芯片型号和密度蓝色经典板STM32F103C8T664KBMD红蓝小板STM32F103RCT6256KBHD大板STM32F103ZET6512KBHD往工程里加代码文件时也要保持team里所有人用的startup和宏一致不然有人编译有人不编译代码还都对不上。5.4 串口调试的基础坑波特率、电平、权限串口是STM32开发调试的生命线。新手最容易犯的错是把串口线接反。板子上USB转串口芯片有TX和RX两个引脚TX是发送RX是接收。板子串口芯片的TX要接板子MCU的RXPA10板子串口芯片的RX要接板子MCU的TXPA9是交叉连接的。但你用杜邦线外接串口模块时模块的TX要接板子的RX模块的RX接板子的TX这也是交叉。很多新手直接TX接TX结果打印全乱码。波特率问题也很普遍。标准库例程的USART_Init结构体里波特率设的是115200但有些教程里的代码是9600开发板出厂固件打印的消息又是115200你换个代码就能在串口助手里看到满屏乱码。看到乱码第一反应先确认波特率不是先怀疑代码。如果你在Linux下用开发板连USB转串口会遇到权限问题打开/dev/ttyUSB0提示Permission denied。解决办法是把当前用户加入dialout组sudo usermod -aG dialout 用户名然后重新登录。临时办法是sudo chmod 777 /dev/ttyUSB0但重启后失效。这个问题热搜里“开发板挂载ubuntu”相关其实在嵌入式Linux开发板上跑Ubuntu系统时串口权限是天天都要面对的事情。5.5 代码编译通过但运行异常时的三板斧如果烧录成功但程序运行结果和你预期不一样不要急着改代码逻辑。先按下面的顺序检查。第一看时钟。在外面接一个LED主循环里翻转电平用示波器或者频率计测这个翻转频率。如果程序里配置的延时是1s实际翻转频率和你预期的差很大那说明系统时钟不对。用内部8MHz和72MHz跑同一个delay_init(72)延时差9倍非常容易暴露时钟配置问题。第二看GPIO模式。有些外设需要开漏你用了推挽波形不对有些输入引脚需要上拉你配置成下拉读到反电平。把每个引脚的复用功能和模式对照数据手册看一眼能解决大量“信号不对”。第三看中断优先级。F103的NVIC可以配置抢占优先级和子优先级。如果你开了两个以上中断却没有合理配置优先级中断互相打断会造成资源竞争。比如串口中断一直打断定时器中断定时器捕获值被覆盖测出来的数据就是乱的。特别是使用printf重定向到串口时如果串口中断优先级高于其他实时性要求更高的中断整个系统时序都会乱。一点个人经验板子买回来只是第一步后面每一步都是坑。我见过太多人在第一周就放弃因为被烧录失败、时钟配置、启动文件这些东西耗完了耐心。但说真的装好环境、点亮第一个LED、能把printf打印到串口上之后后面的路就顺了。树是这么长的先点亮LED然后驱动按键然后驱动定时器和中断然后把串口、I2C、SPI这些常用外设逐个摸一遍最后挑一个超声波测距或者USB HID这种小项目收个尾。你不需要同时买十块板子把这块F103上每一个引脚和每一个外设都玩一遍比换板子有意义得多。每块新板子到手我先花五分钟点亮LED、跑一遍串口回环确认最小系统、烧录链路、调试链路这三条路都通了再开始写业务代码。这个小习惯帮我避免了很多“程序写了半天结果发现板子坏了”的尴尬。接下来挑一个方向动手吧。