平台:STM32F103ZET6 @ 72MHz · 工程名 pwm_measure(一个工程跑完三个实验)· 串口 115200(UTF-8)
前置知识:模块 3A(时基、输出比较、PWM 呼吸灯)
引言:从“产生信号”到“测量信号”
模块 3A 让定时器输出信号(PWM 点灯),模块 3B 把方向反过来——用定时器测量外部信号:测一个 PWM 波形的周期和占空比、数旋钮转了多少格、给电机驱动补上安全机制。三个实验共用同一个工程 pwm_measure,一个实验叠一个实验,最后三套功能在同一块板上和平共处——这也是真实项目的形态。
一、3B.1 输入捕获:造一台 PWM 测量仪
1.1 实验目标
测一个未知信号的周期和占空比,串口每秒打印结果——这就是一台“PWM 测量仪”。它是很多应用的地基:测转速(编码器脉冲)、超声波回波计时、红外遥控解码、频率计。
1.2 原理:给定时器装“硬件快门”
朴素思路是“中断+软件时间戳”,但 CPU 响应中断有微秒级抖动(可能被别的事耽误),测出来的时刻不准。
输入捕获(IC)= 硬件快门:捕获通道盯着引脚,设定的边沿(上升/下降)一到达,硬件瞬间把当前 CNT 的值拍进 CCR——零延迟、零抖动,然后才置标志、发中断。你在回调里取走的不是“现在几点”,而是“边沿到达那一刻的照片”。
测量公式:
- 周期 = 两次上升沿快照之差
- 高电平时长 = 上升沿快照 − 下降沿快照
- 占空比 = 高电平时长 ÷ 周期
F1 的硬件限制:捕获极性只有上升/下降两档,没有双沿捕获(双沿是 F2/F4 之后新系列才加的,CubeMX 里找不到 Both Edges 不是操作问题,是芯片能力)。想要双沿效果 → 双通道分工:CH2 守上升沿测周期、CH3 守下降沿测脉宽——这本来就是仪器级的标准做法。
16 位回绕:CNT 数到 65535 会归零。只要信号周期小于定时器溢出周期,用 (uint16_t)(now - last) 的 16 位减法就能自动处理回绕(数学上 mod 65536)。
1.3 实验设计:信号源 + 测量仪,自己测自己
TIM3_CH3 ──PB0──┬──→ PA1 (TIM2_CH2, 上升沿捕获) → 测周期
│ 两根杜邦线
└──→ PA2 (TIM2_CH3, 下降沿捕获) → 测高电平时长
三个设计决策:
- 信号源用已知参数:TIM3 输出 1Hz、50% PWM(PSC=7199、ARR=9999、Pulse=5000)。测量仪读数必须精确等于已知答案——这叫自校准:用已知校准未知,是仪器思维的第一课
- 测量仪必须用另一个定时器:TIM3 的 CNT 正被自己的 PWM 用着,相位完全同步——上升沿永远发生在 CNT 同一位置,周期会测成 0(混叠)。信号源和测量仪必须时钟独立
- 分辨率与量程的取舍:测量仪 TIM2 用 PSC=7199(每格 0.1ms)+ ARR=65535(量程 6.55 秒)——覆盖 1 秒信号绰绰有余,回绕减法也成立
1.4 CubeMX 配置
| 项 | 配置 |
|---|---|
| TIM3(信号源) | Clock Source=Internal;Channel3=PWM Generation CH3;PSC=7199、ARR=9999、Pulse=5000 |
| TIM2(测量仪) | Channel2=IC Direct(Rising Edge);Channel3=IC Direct(Falling Edge);PSC=7199、Counter Period=65535 |
| USART1 | Asynchronous,115200(打印结果用) |
| NVIC | TIM2 global interrupt 打勾(TIM3 不勾——PWM 零中断) |
| 硬件 | 杜邦线两根:PB0→PA1、PB0→PA2 |
1.5 核心代码
启动(USER CODE 2):
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3); // 信号源:PB0 输出 1Hz 50% PWM
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_2); // CH2 上升沿:测周期
HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_3); // CH3 下降沿:测高电平时长
捕获回调(USER CODE 4):
void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim){
if(htim->Instance != TIM2) return;
if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_2){ // 上升沿 → 周期
uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_2);
period = (uint16_t)(now - cap_rise); // 两次上升沿之差(16位减法自动处理回绕)
cap_rise = now;
ready = 1; // 一整周期凑齐
}else if(htim->Channel == HAL_TIM_ACTIVE_CHANNEL_3){ // 下降沿 → 高电平时长
uint16_t now = HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_3);
width = (uint16_t)(now - cap_rise); // 上升→下降之差
}
}
主循环每秒打印 周期/高/占空比 三件套(printf 重定向用模块 2 的 __io_putchar 老配方,记得 Includes 区加 #include <stdio.h>)。
1.6 现象与验证
- 串口每秒一行:
周期=1000 ms 高=500 ms 占空比=50%——和信号源已知值精确吻合,自校准通过 - 周期在 999/1000 之间偶尔跳变——末位量化误差:信号边沿恰好落在 0.1ms 刻度线附近,捕获有时差一拍。0.1ms 分辨率测 1 秒信号理论精度 ±0.01%,真实仪器的末位跳变就是这么来的
- 实时跟踪验收:用调试器现场把信号源的 CCR3 从 5000 改成 2500(一条
mww命令,不重新烧录),测量仪下一秒立刻显示高=250 ms 占空比=25%——仪器追上了活的信号变化
1.7 踩坑实录(这节踩得最密集)
- F1 无双沿捕获:CubeMX 里 Polarity 只有 Rising/Falling——换双通道分工
- 主循环代码放错区被吃:代码写在
USER CODE END WHILE和BEGIN 3之间的缝隙(模板自留地),CubeMX 重新生成时被还原——加 USART1 重新生成后 printf 循环凭空消失。正确位置:BEGIN 3之后 - TIM2 PSC 填成 72:tick 变成约 1µs,1 秒信号跨 15 次 16 位回绕,减法只救一次——周期打印成 326ms 的乱数(实测变量 period=3261,铁证)
- CH3 极性忘改 Falling:两个通道都守上升沿,CH2/CH3 在同一个上升沿同时触发,CH2 先算周期更新 cap_rise,CH3 随后算 width=now−cap_rise=0——占空比永远 0%(实测 width=0)
- USART1 没配:
huart1未声明,编译直接报错 - 串口中文乱码:固件 printf 是 UTF-8、串口助手默认 GBK——数字和 % 是 ASCII 两边一样所以能认出,中文全花。解法:助手切 UTF-8
排障方法论:变量不对时用调试器直接读运行中的变量(nm 查地址 + mdw 读值)——period=3261、width=0 两个值一出来,两个配置 bug 当场定位,比瞎猜快一百倍。
二、3B.2 编码器接口:让硬件自己数旋钮
2.1 实验目标
读取旋转编码器的位置(累计计数)、方向(顺/逆时针)、速度(单位时间计数变化),串口实时打印。
2.2 原理:AB 正交 + 硬件解码器
旋转编码器(EC11)转起来输出两路方波,玄机在相位差 90°:
顺时针(A 超前 B 90°):
A: ─┐ ┌─┐ ┌─
└─┘ └─┘ └
B: ──┐ ┌─┐
└─┘ └
逆时针(B 超前 A 90°)
- 方向藏在“谁超前谁”里
- 位置藏在累计计数里(每圈 N 个脉冲,EC11 常见 20 个)
- 速度 = 单位时间的计数变化
软件解码的痛:A、B 各接一个 EXTI 中断数边沿判相位——每圈几百次中断、机械触点抖动产生假边沿、判相位容易错。
硬件编码器接口:定时器内置的正交解码器——A 接 CH1、B 接 CH2,硬件实时盯着两相位关系,每个有效边沿自动让 CNT +1 或 −1(顺时针加、逆时针减,方向判断零成本)。你随时读 CNT 就知道位置,零中断、零 CPU。这是“把数数交给硬件”的第三次登场(时基→PWM→编码器)。
三个关键机制:
- 四倍频(x4):Encoder Mode 选 "TI1 and TI2" = A、B 的每个边沿都计数 → 分辨率 ×4(20 脉冲/圈 = 80 计数/圈),转一格恰好 ±4——“四倍频”名字即来于此
- PSC 失效:编码器模式下 CNT 的心跳来自外部边沿而不是内部时钟——预分频器形同虚设(不用设)
- 16 位双向回绕:顺转加到 65535 回绕到 0,逆转减到 0 回绕到 65535。读位置差用
(int16_t)(now - last)——有符号 16 位减法自动处理双向回绕,还白送一个符号(正=顺、负=逆)
2.3 实验设计
TIM4 编码器模式 + 主循环每 200ms 采样一次 CNT 算速度。引脚重映射实战:TIM4 编码器默认脚是 PB6/PB7,但配置时把引脚点到了 PD12/PD13(TIM4 重映射后的位置)——同一对通道的两个住址,CubeMX 自动生成 __HAL_AFIO_REMAP_TIM4_ENABLE(),和 PB6/PB7 完全等效。这无意中把 F1 的“引脚搬家”(AFIO 重映射)功能用了一遍。
编码器模块五脚(GND/VCC/A/B/C):VCC→3.3V(别接 5V,模块上拉电平要和 STM32 一致)、GND→GND、A→PD12、B→PD13、C 是按键脚(按下导通,本实验不接,留给 3B.3 当刹车按钮)。
2.4 CubeMX 配置
| 项 | 配置 |
|---|---|
| TIM4 | Combined Channels → Encoder Mode |
| Encoder Mode | Encoder Mode TI1 and TI2(x4 四倍频) |
| Input Filter(两通道) | 15(机械触点抖动大,硬件滤波) |
| Counter Period | 65535(满量程,16 位回绕成立) |
| 引脚 GPIO | PD12/PD13 设 Pull-up(编码器是开关输出,必须上拉;带板模块有板载上拉则双上拉无害) |
2.5 核心代码
启动只要一行(无需中断):
HAL_TIM_Encoder_Start(&htim4, TIM_CHANNEL_ALL); // 启动编码器接口: 硬件开始自动计数
主循环每 200ms 采样:
int16_t now = (int16_t)__HAL_TIM_GET_COUNTER(&htim4); // 读当前位置
int16_t speed = (int16_t)(now - enc_last); // 差值: 正=顺 负=逆 0=静止
enc_last = now;
printf("位置=%d 速度=%d %s\r\n", now, speed,
speed>0 ? "顺时针" : (speed<0 ? "逆时针" : "静止"));
2.6 现象与验证
串口每 200ms 一行,转动旋钮时:
位置=-25 速度=1 顺时针
位置=-16 速度=8 顺时针
位置=-8 速度=0 静止
位置=-26 速度=-18 逆时针
调试器协同采样(云端定时读 CNT,用户同时转旋钮):+7 → −13 → +23 → −12 → +21 → −22 → +8 → +17 → +7——双向计数实锤,数值在正负之间摆动对应来回旋转,停下归零。A/B 接反不会坏任何东西,只是方向反过来——对调两根线即可,编码器接线的日常。
2.7 踩坑实录
- 编码器没接就查代码:CNT 六次采样全 0,排查半天——其实是编码器根本还没接线。先确认物理连接再查软件
- 引脚住址困惑:资料说 TIM4 是 PD12/13,教程说 PB6/7——两个都对,一个是重映射后的住址。跟着固件实际配置接(看 CubeMX 引脚分配或生成代码里的 REMAP 宏)
- 打印刷屏:主循环
HAL_Delay(10)没按设计改成 200——每秒 100 行串口刷成瀑布。采样周期决定打印节奏,200ms 才能看清
三、3B.3 高级定时器 TIM1:互补、死区与刹车
3.1 实验目标
输出一对互补 PWM(驱动半桥上下管),插入死区时间(防直通),接上刹车输入(硬件急停)——电机控制的看家本领三件套。
3.2 原理:为半桥而生的安全链
半桥是电机驱动的最小单元:
──┬── 电源
[上管] ← TIM1_CH1 (PE9)
├──── 中点 → 接电机
[下管] ← TIM1_CH1N(PE8)
──┴── GND
上下管绝不能同时导通——同时导通=电源对地短路(直通),MOS 管瞬间烧毁。互补 PWM 保证逻辑上不同时通,但 MOS 管关断比开通慢——切换瞬间上管没关干净、下管已经开通,微秒级直通照样炸管。
三个硬件安全机制:
- 互补输出:每个通道两根引脚(CHx/CHxN),输出极性相反,正好驱动上下两管
- 死区时间(DTG):每次切换插入一段“双管全关”的空档,硬件按 tDTS(72MHz,13.9ns 步进)自动插入,不受 PSC 影响,软件零参与
- 刹车(BKIN):一个引脚触发时硬件直接清 MOE 总闸(主输出使能),所有输出瞬间进安全态,不等软件反应——过流保护、急停按钮的最后一道保险丝
3.3 实验设计:第三次“自己测自己”
TIM1_CH1(PE9) ──┬──→ PA1 (捕获仪CH2, 上升沿→周期)
└──→ PA2 (捕获仪CH3, 下降沿→脉宽)
TIM1_CH1N(PE8) ──→ 空着(调试器采样电平验证互补性)
编码器 C 键 ──→ PE15 (BKIN 刹车)
实验参数 vs 工程参数的取舍(重要思维):真实电机用 20kHz PWM + 几百 ns 死区,但捕获仪最快 1µs 分辨率测不到——所以故意放慢 PWM(100Hz)、加大死区(14µs)让仪器能看见。实验参数为可测性服务,工程参数为效率服务,别混为一谈。
参数计算:
- TIM1:PSC=71(1µs 粒度)、ARR=9999(10ms 周期=100Hz)、Pulse=5000(50%)、DTG=255(0xFF 编码 ≈ 14µs 死区)
- 捕获仪 TIM2:PSC 从 7199 改成 71(分辨率从 0.1ms 提到 1µs)
- 预期读数:周期=10000µs、高=4986µs——理想 5000 被死区啃掉 14 → 死区被直接测出来;占空比 49%(死区吃掉 1% 占空比)
引脚重映射:TIM1 默认脚在板上全被占(CH1N=PA7 是 W25Q16、CH2=PA9 是串口、CH3=PA10 是串口、CH4=PA11 是 USB、BKIN=PA6 是 W25Q16)——全重映射到 PE 口:CH1=PE9、CH1N=PE8、BKIN=PE15(动手前先确认板上排针有引出)。
3.4 CubeMX 配置
| 项 | 配置 |
|---|---|
| TIM1 → Channel1 | PWM Generation CH1 CH1N(互补模式) |
| 引脚(点击分配) | PE9→TIM1_CH1、PE8→TIM1_CH1N、PE15→TIM1_BKIN(先分配 BKIN,刹车选项才出现) |
| TIM1 时基 | PSC=71、ARR=9999、Pulse=5000 |
| Break and Dead Time 分组 | Dead Time=255、BRK Polarity=Low(按下=低=刹车)、Automatic Output State=Enable(松开自动恢复)、OSSI=Enable(刹车时引脚驱动到确定电平) |
| PE15 GPIO | Pull-up(关键!见踩坑) |
| TIM2 | Prescaler 7199→71(捕获仪提分辨率) |
| 接线 | PE9→PA1、PE9→PA2(从 PB0 挪线)、编码器 C 键→PE15 |
刹车选项含义速查:BRK State 刹车电路电源开关;BRK Polarity 什么电平触发(按键按下接地→Low);Automatic Output State 刹车消失后谁合闸(Enable=硬件自动,真实故障场景用 Disable 等软件确认);OSSI/OSSR 输出关闭时引脚浮空还是驱动到确定电平(电机场景必开,防栅极浮空误触发);Lock 给安全配置上锁防篡改。
3.5 核心代码
互补输出要开两个开关(USER CODE 2):
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1); // CH1 主输出(顺带开MOE总闸)
HAL_TIMEx_PWMN_Start(&htim1, TIM_CHANNEL_1); // CH1N 互补输出(注意是Ex扩展函数)
printf 换微秒版(tick 已是 1µs,不用再除):
printf("周期=%u us 高=%u us 占空比=%u%%\r\n",
period, width, width*100/period);
3.6 现象与验证
- 串口:
周期=10000 us 高=4986 us 占空比=49%——5000−4986=14µs 就是死区实测,与 DTG=255 的理论值精确命中 - 互补性铁证(调试器十次采样 PE8/PE9 电平):
PE9=1,PE8=0 → PE8=1,PE9=0 → …10/10 严格单高——两脚永不同时为高,互补+死区成立 - 刹车实测:按住编码器旋钮(C 键把 PE15 拉低)→ PWM 立即停止、打印冻结;松开 → AOE 自动合闸、打印恢复滚动
3.7 踩坑实录:BKIN 悬空引发的“全链路假故障”(本模块最精彩的一案)
现象:烧录后测量值全 0、串口没有测量数据、输出引脚电平乱跳。
排查链(每一步都有寄存器证据):
- 读测量变量:period=0、width=0 → 捕获仪没收到边沿 → PE9 上没有波形
- 读 TIM1_BDTR=0x54FF:死区/刹车/自动恢复配置全对,但 MOE=0——总闸被拉掉了
- 读 PE15 电平:低 → 刹车处于触发状态(配的是低有效)
- 因果链闭合:PE15 低 → 刹车 → 硬件清 MOE → 输出禁 → 无波形 → 测量 0。PE15 电平乱跳还导致刹车反复触发/AOE 反复合闸,MOE 振荡,输出时有时无
- 查生成代码:PE15 是
GPIO_MODE_INPUT + GPIO_NOPULL——悬空! - 修复(配上拉)后仍读低 → 读 CRH(模式=上拉输入 ✓)+ ODR(拉的方向=上 ✓)+ IDR(实际电平=低 ✗)三连——内部上拉生效但读低 = 外部有东西硬拉地
- 判决性实验:拔掉 PE15 上的线 → 立刻变高、MOE=1、测量值 10000/4986 全部出现——实锤是那根线接着地(模块端插错孔)
教训:刹车电路对电平极度敏感,BKIN 必须接确定电平(上拉),绝不悬空——这本身就是刹车威力的最好反面教材。排查手法沉淀:CRH/ODR/IDR 三连读(配置生效了吗+拉对方向了吗+实际电平是多少),内部上拉打不过外部直连地,拔线是最终的二分判决。
另一个坑:frp 调试偶发“全零假象”——openocd 连接后芯片停在复位态没放行,所有寄存器读复位值 0、RAM 是陈旧值,看起来像全面故障。解法:monitor reset halt + monitor resume 序列。读数全零时先怀疑目标没在跑,再怀疑代码。
结语:三个实验串成的一条线
三个实验其实是同一条思路的三次递进:
- 3B.1:硬件快门记时刻 → 测周期测脉宽(测量时间)
- 3B.2:硬件解码器数边沿 → 测位置测方向(测量旋转)
- 3B.3:硬件死区刹车保安全 → 互补输出驱动半桥(输出+保护)
加上 3A 的时基/PWM,F103 的定时器全家桶至此没有死角。调试手法也攒出一套:nm 查变量地址 + mdw 读运行中的值、compare-sections 鉴别固件身份、mww 不重烧录现场改参数、协同采样(人转旋钮+云端读寄存器)——没有示波器的环境下,调试器就是你的眼睛。