1. 这不是“又一个超声波测距教程”,而是一份能让你真正理解STM32 GPIO时序控制的实操手记
你搜“STM32 HC-SR04”出来的前二十页,八成是复制粘贴的代码+几句“接线如图”+“烧录即可”。我带过三十多个嵌入式方向的毕业设计,也帮上百个新手调试过Keil5工程,最常听到的一句话是:“代码编译过了,但串口打印出来全是0或者乱码,示波器上TRIG信号根本没触发。”——问题从来不在HC-SR04模块本身,而在于你对STM32底层GPIO翻转精度、微秒级延时实现、中断响应窗口的理解,还停留在“库函数调用完就万事大吉”的层面。
这个项目标题里藏着三个关键层级:STM32(不是51单片机,它没有内置us级延时指令)、HC-SR04(不是即插即用的传感器,它本质是个需要严格时序配合的“声学开关”)、新手级(意味着不能直接甩出HAL库+CubeMX一键生成,得从寄存器操作讲起,否则你永远不知道为什么HAL_Delay(10)会卡死整个系统)。我今天不讲“怎么让LED亮”,而是带你亲手用标准外设库(不是HAL)在STM32F103C8T6上,用纯GPIO模拟TRIG脉冲、用输入捕获精确测量ECHO高电平时间,最后把距离换算成厘米值通过串口实时输出。所有代码都经过实测,接线图用面包板实物照片还原,连杜邦线颜色都标清楚——因为新手最容易栽在“以为自己接对了,其实GND虚焊”这种细节上。
适合谁看?如果你刚装好Keil5、能点亮LED、但看到TIM_TimeBaseInitTypeDef结构体就头皮发麻;如果你查过“STM32超声波测距”,却卡在“为什么ECHO引脚始终读不到上升沿”;如果你的毕设题目是“基于STM32的智能小车避障”,而导师说“先搞定单个模块的稳定测距”,那么这篇就是为你写的。它不承诺“5分钟上手”,但保证你做完后,能独立解释:为什么必须用定时器输入捕获而不是普通GPIO读取?为什么SysTick配置成72MHz反而测不准?为什么HC-SR04在金属表面测距误差比木板大30%?这些答案,全藏在接下来的每一个寄存器配置和每一行注释里。
1.1 为什么“新手级”反而最难教?——拆解HC-SR04与STM32的底层矛盾
HC-SR04的官方时序要求非常明确:向TRIG引脚发送一个持续至少10μs的高电平脉冲,模块内部电路才会触发超声波发射。这个“至少10μs”是硬性门槛,少1纳秒都不行。而STM32F103的主频通常是72MHz,一个机器周期是1/72μs ≈ 13.9ns。理论上,执行一条GPIO_SetBits()指令需要几个周期,但实际中,库函数调用、编译器优化、流水线预取都会引入不可控延迟。我见过太多人用Delay_us(10)函数,结果示波器测出来只有7.2μs——因为函数本身开销占了3μs,而他们没意识到Delay_us()的实现依赖于SysTick,而SysTick默认配置是1ms中断,根本无法精准到微秒级。
更隐蔽的问题在ECHO端。HC-SR04返回的是一个宽度与距离成正比的高电平脉冲(每1cm对应约58.8μs),最大距离4m对应23520μs(23.5ms)。这意味着你需要测量一个最长23.5ms的脉宽,精度要达到±10μs(对应±0.17mm误差)。普通while循环读取GPIO状态?CPU忙着跑其他任务,漏采几个周期就失之毫厘谬以千里。必须用硬件定时器的输入捕获功能,让外设自动记录边沿时刻,CPU只负责读取结果。但新手常犯的错是:把ECHO接到任意GPIO,却没查数据手册确认该引脚是否支持输入捕获——比如PA0可以,但PB0在某些封装下就不支持。
还有供电陷阱。HC-SR04工作电流峰值达15mA,而STM32的GPIO驱动能力只有3-5mA。直接用MCU引脚供电?轻则模块不工作,重则烧毁IO口。必须用独立5V电源,且GND必须与STM32共地——这点看似简单,但我在实验室亲眼见过学生用两块电池分别供电,结果ECHO信号浮动在2.3V,既不算高电平也不算低电平,逻辑分析仪都抓不到有效边沿。
1.2 为什么坚持用标准外设库而非HAL?——新手绕不开的“寄存器直视”训练
现在网上主流教程几乎全用HAL库,理由很充分:CubeMX点几下就生成代码,HAL_GPIO_WritePin()一行搞定。但这就埋下了隐患:当你的测距突然不准,HAL库报错HAL_ERROR,你第一反应是“重装CubeMX”还是“打开参考手册查TIMx_CCMR1寄存器位定义”?我带的学生里,90%的人说不清__HAL_TIM_SET_COUNTER(&htim2, 0)和__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1000)的区别,更别说理解输入捕获模式下CCER寄存器的CC1P(捕获极性)和CC1E(捕获使能)位如何协同工作。
标准外设库(SPL)虽然老旧,但它强迫你直面寄存器。比如配置定时器输入捕获,你必须手动写:
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿触发 TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 分频为1 TIM_ICInitStructure.TIM_ICFilter = 0x0; // 滤波器关闭 TIM_ICInit(TIM2, &TIM_ICInitStructure);这段代码背后,是往TIM2->CCMR1写0x0001(IC1映射到TI1,上升沿),往TIM2->CCER写0x0001(CC1E置1使能捕获)。当你亲手敲下这些,再对照《STM32F10xxx参考手册》第14章“通用定时器”逐字阅读,你就建立了“代码-寄存器-硬件行为”的完整映射。而HAL库把这一切封装成HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1),像黑箱一样吞掉所有细节——新手能用,但无法debug。
所以本篇全程使用标准外设库v3.5,目标芯片明确为STM32F103C8T6(俗称“蓝 pill”),开发环境Keil MDK v5.30(兼容性最好,避免新版本对老芯片包的兼容问题)。所有初始化代码都附带寄存器地址注释,比如RCC->APB2ENR |= 1<<2; // 使能GPIOA时钟,地址0x40021018,让你知道每一行在操控哪片硅晶。
2. 核心细节解析:从物理接线到寄存器配置的每一处魔鬼细节
2.1 接线不是“按图接线”,而是电气特性的匹配游戏
HC-SR04有4个引脚:VCC、GND、TRIG、ECHO。VCC必须接稳定的5V电源,绝不能接STM32的3.3V引脚(模块会不工作或误触发)。GND必须与STM32的GND物理短接,最好用粗导线,避免共地阻抗导致信号抖动。TRIG和ECHO都是3.3V逻辑电平兼容的,但这里有个致命误区:很多教程说“TRIG接PA0,ECHO接PA1”,却没说明PA0和PA1必须配置为推挽输出和浮空输入——因为TRIG需要主动驱动高/低电平,而ECHO是模块输出,STM32只需被动读取。
我实测过三种接线方案的误差对比(测试环境:20cm固定距离,重复100次):
| 接线方案 | TRIG驱动方式 | ECHO输入配置 | 平均误差 | 最大跳变 |
|---|---|---|---|---|
| 方案A(错误) | PA0推挽输出 +GPIO_ResetBits() | PA1上拉输入 | ±12.3cm | 35cm |
| 方案B(常见错误) | PA0推挽输出 +GPIO_SetBits()/ResetBits() | PA1浮空输入 | ±4.7cm | 18cm |
| 方案C(本文推荐) | PA0推挽输出 + 精确us延时 | PA1浮空输入 + 定时器输入捕获 | ±0.8cm | 2.1cm |
方案A的误差源于上拉电阻与HC-SR04内部电路形成分压,ECHO信号被拉低;方案B虽改善,但普通GPIO读取无法捕捉微秒级脉宽变化。方案C才是正解——TRIG用精确延时确保10μs脉冲,ECHO交由硬件定时器捕获。
具体接线表(实物验证版):
| HC-SR04引脚 | 连接目标 | 线缆颜色 | 关键说明 |
|---|---|---|---|
| VCC | 外部5V稳压电源正极(如LM7805输出) | 红色 | 严禁接STM32的3.3V! |
| GND | STM32的GND引脚(就近选择,如PA0旁的GND孔) | 黑色 | 必须与MCU共地,长度<5cm |
| TRIG | STM32的PA0引脚 | 黄色 | 配置为推挽输出,速度50MHz |
| ECHO | STM32的PA1引脚 | 蓝色 | 配置为浮空输入,仅用于输入捕获 |
提示:别信“杜邦线随便接”的说法。我拆解过3个故障模块,发现2个是因劣质杜邦线接触电阻过大(>5Ω),导致TRIG脉冲上升沿变缓,HC-SR04未识别。建议用镀金头杜邦线,或焊接连接。
2.2 TRIG脉冲生成:为什么“for循环延时”是新手最大陷阱?
HC-SR04要求TRIG引脚高电平持续≥10μs。新手第一反应是写:
GPIO_SetBits(GPIOA, GPIO_Pin_0); for(i=0; i<10; i++); // 假设1次循环1μs GPIO_ResetBits(GPIOA, GPIO_Pin_0);这完全错误。原因有三:
第一,for循环执行时间受编译器优化等级影响极大。Keil默认O0优化时,i++可能编译成3条指令(加载、加1、存储),耗时约6个周期;O2优化时可能被编译器直接优化掉。同一段代码,在Debug和Release模式下延时天差地别。
第二,ARM Cortex-M3的指令周期不是固定的。访问Flash时有等待周期,访问SRAM则快得多。你无法保证for循环每次都在同一内存区域执行。
第三,最致命的是——中断会打断延时。如果此时有SysTick中断发生,for循环会被挂起,恢复后继续执行,导致TRIG高电平远超10μs(可能达几十ms),HC-SR04会误判为连续触发,返回错误数据。
正确做法是使用NOP指令精确延时。STM32F103在72MHz主频下,一条__ASM volatile("nop");指令耗时1/72μs ≈ 13.9ns。要得到10μs,需执行10 / 0.0139 ≈ 719条NOP。但实际中我们取整为720条,留出余量:
GPIO_SetBits(GPIOA, GPIO_Pin_0); for(volatile int i=0; i<720; i++) { __ASM volatile("nop"); } GPIO_ResetBits(GPIOA, GPIO_Pin_0);注意volatile关键字,强制编译器不优化掉这个循环。实测示波器截图显示,此方案TRIG脉冲宽度稳定在10.02±0.05μs,完全满足要求。
实操心得:别用
Delay_us()函数!即使你写了精准的SysTick延时,一旦系统中有其他中断(如串口接收),SysTick计数会被打断,导致延时不准。微秒级脉冲必须用NOP或定时器PWM输出,前者简单直接,后者更专业但对新手稍难。
2.3 ECHO脉宽捕获:输入捕获模式下的“双沿测量”真相
HC-SR04的ECHO信号是“高电平宽度∝距离”,但新手常忽略一个事实:ECHO信号的起始沿(上升沿)和结束沿(下降沿)都需要捕获,才能计算高电平持续时间。很多教程只捕获上升沿,然后用while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1));等待下降沿——这又回到了CPU忙等的老路,精度惨不忍睹。
正确方案是启用定时器的输入捕获通道,配置为“双边沿捕获”。以TIM2为例,PA1映射到TI1通道,我们设置:
- 第一次捕获上升沿,记录计数值T1;
- 第二次捕获下降沿,记录计数值T2;
- 则高电平时间 = (T2 - T1) × 定时器计数周期。
定时器计数周期由预分频器(PSC)和自动重装载值(ARR)决定。我们希望计数周期为1μs(即1MHz计数频率),以便直接读取微秒值。STM32F103的APB1总线频率为36MHz(经RCC分频),TIM2挂载在APB1上,其时钟源为APB1时钟的2倍(即72MHz)。因此:
- 计数频率 = 72MHz / (PSC + 1)
- 设PSC = 71,则计数频率 = 72MHz / 72 = 1MHz → 计数周期 = 1μs
配置代码:
// 开启TIM2时钟 RCC->APB1ENR |= 1<<0; // RCC_APB1ENR_TIM2EN位置1 // 配置TIM2为1MHz计数频率 TIM2->PSC = 71; // 预分频72分频 TIM2->ARR = 0xFFFF; // 自动重装载最大值,避免溢出(23.5ms < 65535μs) TIM2->CR1 = 0x0001; // CEN=1,启动计数 // 配置输入捕获:上升沿触发 TIM2->CCMR1 |= 0x0001; // CC1S=01,TI1映射到IC1 TIM2->CCER |= 0x0001; // CC1E=1,使能IC1 TIM2->DIER |= 0x0001; // UIE=1,开启更新中断(用于溢出处理)关键点在于如何实现双边沿捕获?标准外设库不支持直接配置,需手动操作CCER寄存器:
// 第一次捕获上升沿后,在中断服务程序中切换极性 void TIM2_IRQHandler(void) { if(TIM2->SR & 0x0001) { // 更新中断标志 TIM2->SR &= ~0x0001; // 清除标志 // 处理溢出(此处省略) } if(TIM2->SR & 0x0020) { // CC1中断标志 static uint16_t cap1 = 0, cap2 = 0; static uint8_t flag = 0; if(flag == 0) { // 第一次捕获(上升沿) cap1 = TIM2->CCR1; flag = 1; TIM2->CCER &= ~0x0001; // 关闭CC1E TIM2->CCER |= 0x0002; // CC1P=1,切换为下降沿捕获 TIM2->CCER |= 0x0001; // 重新使能CC1E } else { // 第二次捕获(下降沿) cap2 = TIM2->CCR1; uint16_t pulse_width = cap2 - cap1; // 计算距离:pulse_width * 0.01715 cm(声速343m/s ÷ 2 ÷ 1000000) distance_cm = pulse_width * 0.01715f; flag = 0; } TIM2->SR &= ~0x0020; // 清除CC1中断标志 } }这段代码揭示了输入捕获的本质:不是“自动测脉宽”,而是通过两次中断,手动记录两个时间戳。新手必须理解cap2 - cap1的结果单位是“定时器计数周期”,而我们的周期是1μs,所以结果直接是微秒值。
3. 实操过程:从Keil5工程创建到串口实时显示的完整链路
3.1 Keil5工程搭建:避开芯片包安装的三大坑
Keil5安装本身不难,但新手常卡在“新建工程后找不到STM32F103C8T6芯片”。这不是Keil问题,而是芯片支持包(Device Family Pack, DFP)没装对。最新版Keil5.37自带DFP,但兼容性差;我实测最稳的是Keil5.30 + STM32F1xx_DFP v2.3.0。
安装步骤(亲测有效):
- 下载Keil5.30安装包(官网archive区可找到),不要装最新版;
- 安装时取消勾选“Install ARM Compiler”,用自带的ARMCC v5.06;
- 安装完成后,打开Keil,点击
Pack Installer(小图标),搜索STM32F1xx,安装Keil.STM32F1xx_DFP.2.3.0.pack; - 关键一步:安装后重启Keil,否则新建工程时芯片列表为空。
新建工程流程:
Project → New uVision Project→ 选择保存路径,命名为HC_SR04_Project;- 弹出芯片选择框,展开
STMicroelectronics → STM32F1 Series → STM32F103C8,双击选中(注意不是C6或CB); - 提示“Copy Startup file?”选Yes;
- 右键
Target→Manage Project Items→ 在Files页添加:startup_stm32f10x_md.s(启动文件)、stm32f10x.h、core_cm3.h等头文件; - 在
Folders/Extensions页,设置Include Paths:添加.\CMSIS\Include、.\STM32F10x_StdPeriph_Driver\inc、.\User。
注意:标准外设库v3.5需手动下载。官网已下架,我整理好的压缩包包含
Libraries文件夹,内含CMSIS和STM32F10x_StdPeriph_Driver。解压后,将Libraries\CMSIS\CM3\CoreSupport和Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x复制到工程目录下对应位置。
3.2 主程序框架:初始化顺序决定成败
STM32初始化有严格顺序,错一步就全盘失败。我的经验是遵循“时钟→GPIO→外设→中断”四步法:
第一步:系统时钟配置(RCC)
必须先配置HSE(外部高速晶振)或HSI(内部RC),否则所有外设时钟无效。蓝 pill板载8MHz晶振,我们启用HSE:
// 启用HSE RCC->CR |= 0x00010000; // HSEON=1 while(!(RCC->CR & 0x00020000)); // 等待HSE就绪 // 配置PLL:HSE*9=72MHz RCC->CFGR |= 0x00000002; // PLLSRC=HSE RCC->CFGR |= 0x00000008; // PLLMUL=9 RCC->CR |= 0x01000000; // PLLON=1 while(!(RCC->CR & 0x02000000)); // 等待PLL就绪 // 切换系统时钟为PLL RCC->CFGR |= 0x00000004; // SW=10,选择PLL while((RCC->CFGR & 0x0000000C) != 0x00000004); // 等待切换完成这段代码直接操作RCC寄存器,比RCC_Configuration()函数更透明。新手常犯错是跳过while等待,导致后续外设时钟未就绪,GPIO初始化失败。
第二步:GPIO初始化(PA0/TRIG, PA1/ECHO, PA9/USART1_TX)
PA0配置为推挽输出,PA1配置为浮空输入,PA9配置为复用推挽输出(USART1):
// 使能GPIOA时钟 RCC->APB2ENR |= 0x00000004; // IOPAEN=1 // PA0: TRIG, 推挽输出, 50MHz GPIOA->CRL &= 0xFFFFFF0F; // 清除CNF0[1:0]和MODE0[1:0] GPIOA->CRL |= 0x00000030; // MODE0=11 (50MHz), CNF0=00 (推挽) // PA1: ECHO, 浮空输入 GPIOA->CRL &= 0xFFFFFFF0; // 清除CNF1[1:0]和MODE1[1:0] GPIOA->CRL |= 0x00000004; // MODE1=01 (输入), CNF1=00 (浮空) // PA9: USART1_TX, 复用推挽输出 RCC->APB2ENR |= 0x00000010; // AFIOEN=1 (使能复用功能) GPIOA->CRH &= 0xFFF0FFFF; // 清除CNF9[1:0]和MODE9[1:0] GPIOA->CRH |= 0x000B0000; // MODE9=11 (50MHz), CNF9=10 (复用推挽)第三步:USART1初始化(波特率115200)
串口是调试生命线,必须优先配好:
// 使能USART1时钟 RCC->APB2ENR |= 0x00004000; // USART1EN=1 // 波特率计算:DIV = (72MHz) / (16 * 115200) = 39.0625 → 整数部分39,小数部分0.0625 USART1->BRR = (39 << 4) | 1; // DIV_Mantissa=39, DIV_Fraction=1 USART1->CR1 = 0x0000200C; // UE=1, TE=1, RE=1 (使能、发送、接收)BRR寄存器计算是重点:DIV = PCLK / (16 * baud),PCLK2为72MHz,115200波特率下DIV=39.0625,整数部分39(左移4位),小数部分0.0625×16=1,所以BRR=0x271。
第四步:TIM2初始化(输入捕获)
如前所述,配置PSC=71,ARR=0xFFFF,使能中断:
RCC->APB1ENR |= 0x00000001; // TIM2EN=1 TIM2->PSC = 71; TIM2->ARR = 0xFFFF; TIM2->CCMR1 |= 0x0001; // CC1S=01 TIM2->CCER |= 0x0001; // CC1E=1 TIM2->DIER |= 0x0001 | 0x0020; // UIE=1, CC1IE=1 TIM2->CR1 = 0x0001; // CEN=1 // 使能TIM2中断 NVIC_EnableIRQ(TIM2_IRQn);3.3 主循环逻辑:测距流程的原子化拆解
主循环不是简单“触发-读取”,而是包含状态机管理:
int main(void) { SystemInit(); // 系统时钟初始化 RCC_Configuration(); // 如上RCC配置 GPIO_Configuration(); // GPIO配置 USART1_Configuration(); // 串口配置 TIM2_Configuration(); // 定时器配置 while(1) { // 步骤1:发送TRIG脉冲 GPIO_SetBits(GPIOA, GPIO_Pin_0); for(volatile int i=0; i<720; i++) __ASM volatile("nop"); GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 步骤2:等待ECHO响应(超时保护) uint32_t timeout = 0; while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_1) == Bit_RESET) { timeout++; if(timeout > 1000000) break; // 超时1秒,退出 } // 步骤3:启动TIM2计数(已在初始化中启动) // 步骤4:等待输入捕获完成(通过全局变量flag) while(flag == 1); // 等待中断处理完毕 // 步骤5:计算并发送距离 if(distance_cm > 0 && distance_cm < 400) { // 有效范围0-400cm printf("Distance: %.2f cm\r\n", distance_cm); } else { printf("Out of range or error\r\n"); } // 步骤6:延时,避免频繁触发(HC-SR04最小周期60ms) Delay_ms(100); } }这里的关键是超时保护。HC-SR04在无反射物时ECHO保持低电平,若不加timeout,while循环会死锁。1000000次循环在72MHz下约13.9ms,足够覆盖4m距离(23.5ms)。
串口printf需重定向fputc:
int fputc(int ch, FILE *f) { while((USART1->SR & 0x00000040) == 0); // 等待TXE标志 USART1->DR = (ch & 0xFF); return ch; }4. 常见问题与排查技巧实录:那些让新手崩溃的“灵异现象”
4.1 串口打印全是0?先查这三个硬件层
问题现象:代码编译下载后,串口助手上只看到“Distance: 0.00 cm”反复刷屏。
排查路径(按优先级):
TRIG脉冲是否存在?
用示波器探头接PA0,触发模式设为上升沿。按下复位键,应看到一个10μs宽的方波。若无信号,检查:GPIO_SetBits()是否写错引脚(如GPIO_Pin_1);- PA0是否配置为推挽输出(
GPIOA->CRL的MODE0位是否为11); - 是否忘记使能GPIOA时钟(
RCC->APB2ENR的IOPAEN位)。
ECHO引脚电平是否变化?
万用表测PA1对地电压。正常时,无物体时为0V,20cm处应跳变至3.3V并维持约1176μs(20cm×58.8μs/cm)。若始终为0V,检查:- HC-SR04的VCC是否真为5V(用电压档实测,别信电源标称值);
- GND是否共地(用万用表通断档测HC-SR04 GND与STM32 GND是否导通);
- ECHO线是否虚焊(轻轻摇晃杜邦线,看电压是否跳变)。
定时器中断是否触发?
在TIM2_IRQHandler开头加一句GPIO_SetBits(GPIOA, GPIO_Pin_2);(假设PA2接LED),观察LED是否闪烁。若不闪,说明中断未进入:- 检查
NVIC_EnableIRQ(TIM2_IRQn)是否执行; - 检查
TIM2->DIER的CC1IE位是否置1; - 检查
TIM2->CCER的CC1E位是否置1; - 检查
TIM2->CR1的CEN位是否为1(计数器是否运行)。
- 检查
实操心得:我帮一个学生调试了3小时,最后发现是HC-SR04模块本身损坏——用另一块模块替换,立刻正常。所以备件很重要,别把所有问题都归咎于代码。
4.2 距离忽大忽小?环境干扰的四大元凶
问题现象:同一距离,串口打印值在15-25cm间跳变。
根源分析:
| 干扰源 | 原理 | 解决方案 |
|---|---|---|
| 多径反射 | 超声波打到桌面后反射到墙壁,再返回,导致ECHO脉宽变长 | 测量时确保前方1m内无大型反射面,或用吸音材料包裹周围 |
| 温度漂移 | 声速随温度变化(343m/s @20°C,每℃变化0.6m/s) | 在代码中加入温度补偿:distance = pulse_width * (0.01715 + (temp-20)*0.00001) |
| 供电波动 | 5V电源纹波>100mV时,HC-SR04内部振荡器频率偏移 | 在HC-SR04 VCC与GND间并联100μF电解电容+0.1μF陶瓷电容 |
| 电磁干扰 | 附近有电机、继电器开关,产生EMI耦合到ECHO线 | ECHO线用屏蔽线,或缩短走线长度<10cm |
我实测过不同材质的反射效果:
- 光滑金属板:回波强,但存在相位干涉,误差±3cm;
- 毛巾布料:吸收大部分能量,回波弱,需增大增益(但HC-SR04无增益调节);
- 书本纸面:最稳定,误差±0.5cm。
所以教学演示时,我总用一本厚字典当靶标。
4.3 Keil5烧录失败?“Flash Download Failed”的七种解法
问题现象:点击Download按钮,弹出“Flash Download failed - Cortex-M3”错误。
这是Keil5新手最高频问题,原因及对策:
ST-Link驱动未安装:
下载STSW-LINK009,运行dpinst_amd64.exe(64位系统)或dpinst_x86.exe(32位),安装后设备管理器中应有“STMicroelectronics STLink Debug”设备。SWD接线错误:
蓝 pill的SWD接口是4针:SWCLK(PA13)、SWDIO(PA14)、GND、3.3V。常见错误:- 把SWDIO接到SWCLK;
- GND未接(导致通信失败);
- 3.3V接到VCC(蓝 pill的VCC是5V输入,ST-Link的3.3V输出只能供小电流,接错会烧ST-Link)。
目标芯片被锁:
执行过FLASH_OB_Lock()或误操作导致选项字节锁死。解决:- Keil中
Options for Target → Utilities → Settings → Debug → Connect,勾选Connect under reset; - 按住蓝 pill的RESET键不放,点击Keil的Download,待提示“Connected”后松开RESET。
- Keil中
Flash算法不匹配:
Keil默认Flash算法可能不支持C8T6。手动指定:Options for Target → Debug → Settings → Flash,点击Add,选择STM32F10x_64k.FLM(C8T6 Flash容量64KB)。
供电不足:
ST-Link通过