文章目录
-
- 摘要
- 前言
- 一、核心原理:一条硬连线串起来的触发链
-
- [1.1 整体链路与信号流](#1.1 整体链路与信号流)
- [1.2 为什么 TRGO 选 Update 而不是 OC1REF](#1.2 为什么 TRGO 选 Update 而不是 OC1REF)
- [1.3 从机侧:外部时钟模式 1 与触发映射](#1.3 从机侧:外部时钟模式 1 与触发映射)
- [1.4 三种方案的取舍](#1.4 三种方案的取舍)
- [二、CubeMX 配置与寄存器对照](#二、CubeMX 配置与寄存器对照)
-
- [2.1 主机 TIM1](#2.1 主机 TIM1)
- [2.2 从机 TIM2](#2.2 从机 TIM2)
- [2.3 容易遗漏的四个配置细节](#2.3 容易遗漏的四个配置细节)
- 三、核心代码实现
- 四、测试验证:三个量化视角
-
- [4.1 对照一:PWM 频率理论值 vs 实测值](#4.1 对照一:PWM 频率理论值 vs 实测值)
- [4.2 停止窗口模型:多发一脉冲的定量解释](#4.2 停止窗口模型:多发一脉冲的定量解释)
- [4.3 参数扫描:PWM 频率与 CPU 负载/精确性的权衡](#4.3 参数扫描:PWM 频率与 CPU 负载/精确性的权衡)
- [4.4 长跑统计](#4.4 长跑统计)
- [五、一次翻车实录:200 kHz 下偶发 N+1 的完整排查](#五、一次翻车实录:200 kHz 下偶发 N+1 的完整排查)
- 六、故障排查速查
- 七、扩展与变体
- 八、总结
- 版本备注
- 参考资料
摘要
步进电机和伺服驱动器普遍采用"脉冲+方向"接口定位,脉冲个数直接对应位移量,多发或漏发一个脉冲都会累积成位置误差。本文基于 STM32F103C8T6,用 TIM1 作主定时器输出 PWM(TRGO 选更新事件),TIM2 作从定时器以外部时钟模式 1 硬件计数,实现脉冲数全程 0 CPU 占用的精确输出与自动停发。实测 1 kHz~200 kHz、单次 10000 脉冲 ×200 轮:脉冲数偏差为 0;ISR 直接寄存器停止延迟 0.28 μs,停止窗口模型预测的临界频率 625 kHz 与实测失效边界吻合;并完整复盘 HAL 中断分发路径 2.9 μs 延迟导致多发一脉冲的排查与修复过程。
前言
写这个系列的过程中,PWM 相关的内容已经写过输出异常排查、输入捕获测频、互补 PWM 半桥好几种,但一直没碰"输出固定个数的脉冲"这个需求。直到前段时间帮朋友调一台三轴步进工作台:上位机下发"移动 3.75 mm",换算成脉冲是 750 个(丝杆导程 5 mm、驱动器 16 细分、200 步电机,3.75 × 1600 / 5 = 1200,这里按他的机构是 750),结果每次都多走一小段,定位误差虽然只有零点几毫米,但累积起来批次就不一致了。这类问题的根源很典型------软件用"PWM 输出 + 中断计数"的方式停发,停止时机不受控,最后总会多冒出一两个脉冲。
解决"精确脉冲数"的主流做法有三类:定时中断逐个计数、主从定时器硬件计数、高级定时器重复计数器(RCR)。第一类实现最简单,但每个脉冲进一次中断,频率一高 CPU 就被吃光,而且停止时机依赖中断响应延迟;后两类把"数脉冲"这件事交给硬件,软件只负责启动和收尾。本文聚焦第二类------主从定时器方案,把 TRGO 触发链、寄存器级配置、停止时序这些容易被教程一笔带过的细节拆开讲透,并用 200 kHz 高频场景下的实测翻车案例,给出"多发一脉冲"问题的完整排查链。
阅读本文需要熟悉 STM32 定时器的基本计数原理(PSC/ARR/CCR 三件套)和 PWM 输出模式。软硬件平台:STM32F103C8T6 最小系统板,逻辑分析仪 24 MHz 采样,Keil MDK 5.36 + STM32CubeMX 6.9 + HAL 库 F1 V1.8.5。代码思路对 F103 系列通用,其他 F1 型号只需改触发映射核对。
本文完整工程代码可在 CSDN 下载频道 获取(VIP 免费)。
一、核心原理:一条硬连线串起来的触发链
1.1 整体链路与信号流
主从方案的本质,是让主定时器负责"发脉冲",从定时器负责"数脉冲",两个定时器之间用芯片内部一根硬连线的触发信号(TRGO → ITRx)连接,全程不经过 CPU。整条链路如下图:
#mermaid-svg-1bBRENRYzRuPh3zc{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1bBRENRYzRuPh3zc .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1bBRENRYzRuPh3zc .error-icon{fill:#552222;}#mermaid-svg-1bBRENRYzRuPh3zc .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1bBRENRYzRuPh3zc .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1bBRENRYzRuPh3zc .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1bBRENRYzRuPh3zc .marker.cross{stroke:#333333;}#mermaid-svg-1bBRENRYzRuPh3zc svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1bBRENRYzRuPh3zc p{margin:0;}#mermaid-svg-1bBRENRYzRuPh3zc .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1bBRENRYzRuPh3zc .cluster-label text{fill:#333;}#mermaid-svg-1bBRENRYzRuPh3zc .cluster-label span{color:#333;}#mermaid-svg-1bBRENRYzRuPh3zc .cluster-label span p{background-color:transparent;}#mermaid-svg-1bBRENRYzRuPh3zc .label text,#mermaid-svg-1bBRENRYzRuPh3zc span{fill:#333;color:#333;}#mermaid-svg-1bBRENRYzRuPh3zc .node rect,#mermaid-svg-1bBRENRYzRuPh3zc .node circle,#mermaid-svg-1bBRENRYzRuPh3zc .node ellipse,#mermaid-svg-1bBRENRYzRuPh3zc .node polygon,#mermaid-svg-1bBRENRYzRuPh3zc .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1bBRENRYzRuPh3zc .rough-node .label text,#mermaid-svg-1bBRENRYzRuPh3zc .node .label text,#mermaid-svg-1bBRENRYzRuPh3zc .image-shape .label,#mermaid-svg-1bBRENRYzRuPh3zc .icon-shape .label{text-anchor:middle;}#mermaid-svg-1bBRENRYzRuPh3zc .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1bBRENRYzRuPh3zc .rough-node .label,#mermaid-svg-1bBRENRYzRuPh3zc .node .label,#mermaid-svg-1bBRENRYzRuPh3zc .image-shape .label,#mermaid-svg-1bBRENRYzRuPh3zc .icon-shape .label{text-align:center;}#mermaid-svg-1bBRENRYzRuPh3zc .node.clickable{cursor:pointer;}#mermaid-svg-1bBRENRYzRuPh3zc .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1bBRENRYzRuPh3zc .arrowheadPath{fill:#333333;}#mermaid-svg-1bBRENRYzRuPh3zc .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1bBRENRYzRuPh3zc .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1bBRENRYzRuPh3zc .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1bBRENRYzRuPh3zc .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1bBRENRYzRuPh3zc .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1bBRENRYzRuPh3zc .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1bBRENRYzRuPh3zc .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1bBRENRYzRuPh3zc .cluster text{fill:#333;}#mermaid-svg-1bBRENRYzRuPh3zc .cluster span{color:#333;}#mermaid-svg-1bBRENRYzRuPh3zc div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1bBRENRYzRuPh3zc .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1bBRENRYzRuPh3zc rect.text{fill:none;stroke-width:0;}#mermaid-svg-1bBRENRYzRuPh3zc .icon-shape,#mermaid-svg-1bBRENRYzRuPh3zc .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1bBRENRYzRuPh3zc .icon-shape p,#mermaid-svg-1bBRENRYzRuPh3zc .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1bBRENRYzRuPh3zc .icon-shape .label rect,#mermaid-svg-1bBRENRYzRuPh3zc .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1bBRENRYzRuPh3zc .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1bBRENRYzRuPh3zc .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1bBRENRYzRuPh3zc :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 每个周期溢出一次
ITR0 硬连线
SMCR.TS=000
CNT 计到 ARR+1=N
溢出
TIM1 计数器
PSC=71, ARR=999
1MHz 计数时钟
更新事件
CR2.MMS=010
TRGO 触发输出
每周期一个上升沿
TIM2 从机
SMS=111 外部时钟模式1
更新中断
UIE=1
ISR: 清 CC1E → 清 CEN
精确停发
PA8 输出
1kHz / 50% PWM
步进驱动器 STEP 端
这里有几个关键点值得逐一说明。主定时器每输出一个完整的 PWM 周期,计数器溢出一次,就产生一个更新事件;把更新事件选为 TRGO 输出,从定时器以外部时钟模式 1 把 TRGO 的上升沿当作自己的计数时钟,于是从机 CNT 的每一次自增都精确对应主机输出的一个完整周期------也就是一个脉冲。从机 ARR 设为 N-1,计满 N 个周期时从机溢出、触发更新中断,软件在这个中断里停掉主机,整个 burst 结束。此时恰好 N 个完整脉冲已经输出完毕,误差为零。
1.2 为什么 TRGO 选 Update 而不是 OC1REF
这是本文第一个配置级决策,也是很多教程没讲清楚的地方。TIM1_CR2 的 MMS2:0 位决定 TRGO 输出什么(RM0008 Table 71):000 = Reset(复位事件,CubeMX 默认值)、010 = Update(更新事件)、100 = OC1REF(通道 1 比较参考信号)、其余还有使能事件等选项。OC1REF 是通道内部比较结果------CNT < CCR 时为高,它每个 PWM 周期也会产生一个上升沿,理论上同样能数。但对比之后我仍然选了 Update,理由有三:
| 对比维度 | MMS=010(Update) | MMS=100(OC1REF) |
|---|---|---|
| 每周期边沿数 | 恒为 1(溢出一次) | 占空比 <100% 时为 1 |
| 占空比=100% 时 | 仍正常计数 | REF 恒高,边沿消失,计数停摆 |
| 边沿时刻 | 周期边界(= 脉冲下降沿) | 脉冲上升沿 |
| 改占空比调速 | 计数不受影响 | 计数不受影响 |
| 停止窗口起点 | 周期边界处中断,距下一个上升沿最远 | 边沿处中断,需立刻判断下一 REF 状态 |
第三行是决定性的:从机溢出中断发生的时刻,恰好是主机的周期边界,也就是第 N 个脉冲下降沿刚结束的瞬间。此时距离下一个(多余的)上升沿还有整整一段 CCR 时间,软件有最宽裕的窗口去停发。如果用 OC1REF,中断发生在上升沿附近,窗口判断会复杂得多。至于第二行的极端情况------占空比改到 100% 时 OC1REF 恒高、从机停止计数而软件毫无察觉------虽然属于误配置,但 Update 方案天然免疫,工程上更皮实。
1.3 从机侧:外部时钟模式 1 与触发映射
从机 TIM2 的 SMCR 寄存器承担两个配置:SMS2:0 = 111 选择外部时钟模式 1(计数器以所选触发输入的上升沿作为时钟),TS2:0 = 000 选择 ITR0 作为触发源。TIM2 的 ITR0 在芯片内部固定连接到 TIM1_TRGO,这是 RM0008 Table 78(TIMx internal trigger connection)定义的硬连线映射,不可软件重定向:
| 从机触发输入 | 连接信号 | 本项目使用 |
|---|---|---|
| ITR0(TS=000) | TIM1_TRGO | 是 |
| ITR1(TS=101) | TIM8_TRGO | 否(C8T6 无 TIM8) |
| ITR2(TS=010) | TIM3_TRGO | 否 |
| ITR3(TS=011) | TIM4_TRGO | 否 |
选用 ITR0 的原因很直接:F103C8T6 没有 TIM8,而 TIM2/TIM3/TIM4 的 ITR0 都统一接 TIM1_TRGO,选哪个 GP 定时器做从机都可以;TIM2 资源上通常最富余,就选了它。如果你用的是互联型 F105/F107 或者 F4 系列,ITR 映射表不同,务必查对应型号参考手册的同名表格核对,不能照搬。
还有一条隐性边界:从机对 TRGO 的采样要经过两级同步器(同步到从机自己的计数时钟),触发频率过高会丢边沿。手册没有给出硬性上限,但按同步器需要 2~3 个采样时钟来算,72 MHz 从机时钟下 TRGO 频率建议不超过 1/3,即约 24 MHz------对步进/伺服应用(几十 kHz 量级)完全不是问题,但做高频超声驱动这类应用时要留意识别。
1.4 三种方案的取舍
方案级决策需要放在完整对比里看。三种实现方式我都在同一块板上跑过,关键维度如下:
| 对比维度 | 方式A:PWM中断计数 | 方式B:主从硬件计数(本文) | 方式C:高级定时器RCR |
|---|---|---|---|
| 每脉冲 CPU 占用 | 1 次 ISR(实测 1.4 μs) | 0 | 0 |
| 脉冲数精确性 | 依赖停止时机,易多发 | 硬件保证 | 硬件保证 |
| 定时器资源 | 1 个 | 2 个 | 1 个(仅 TIM1/TIM8) |
| 单次脉冲数上限 | 无硬限制 | 65536(从机 16 位) | 65536(RCR 16 位) |
| 运行中可调频/调占空比 | 可以 | 可以 | 可以 |
| 停止时机确定性 | 差(ISR 延迟) | 好(周期边界触发) | 好(同为周期边界) |
| 适用频率 | ≤50 kHz(实测 70% 负载) | ≤200 kHz(受停止窗口约束) | ≤200 kHz(同上) |
最终选 B 的理由:既不占 CPU,又不受 RCR 的"只能用高级定时器"限制(虽然本文恰好用 TIM1 做主机,但方案对 TIM2 主 + TIM3 从同样成立,见第五节扩展);C 方案作为单定时器变体,在 TIM1 的互补输出被电机控制占用时是很好的退路。A 方案只适合低频、脉冲数几百以内的场合。
相关阅读:STM32主从定时器实现PWM精确输出脉冲数控制电机 --- 与本文同思路的主从方案实现,可对照参考。
二、CubeMX 配置与寄存器对照
2.1 主机 TIM1
时钟树配置为 HSE 8 MHz ×9 = 72 MHz,APB2 不分频(TIM1 时钟 72 MHz),APB2 分频 2(36 MHz,TIM2/3/4 按 ×2 规则同为 72 MHz)。CubeMX 里 TIM1 的关键配置如下,每项都标注了对应的寄存器字段:
| CubeMX 配置项 | 取值 | 对应寄存器/字段 | 说明 |
|---|---|---|---|
| Clock Source | Internal Clock | CR1.CEN 由软件置位 | 内部 72 MHz 计数 |
| Prescaler | 71 | PSC = 71 | 72MHz/72 = 1 MHz 计数时钟 |
| Counter Period | 999 | ARR = 999 | 1MHz/1000 = 1 kHz PWM |
| Repetition Counter | 0 | RCR = 0 | 每次溢出都产生更新事件,链路成立的前提 |
| PWM 模式 | PWM Mode 1 | CCMR1.OC1M = 110 | CNT < CCR 时输出有效 |
| Pulse | 500 | CCR = 500 | 50% 占空比 |
| Output compare preload | Disable | CCMR1.OC1PE = 0 | 见 2.3 节,关键配置 |
| CH1 引脚 | PA8 复用推挽 | GPIOA_CRH | 无 remap 默认映射 |
| Trigger Event Selection | Update Event | CR2.MMS = 010 | TRGO 输出更新事件,本文核心 |
| NVIC | TIM2 更新中断使能,抢占优先级 0 | ISER + TIM2_IPR | 抢占 0 保证停止窗口 |
其中 Repetition Counter 必须保持 0:RCR > 0 时更新事件按 RCR+1 个周期才出一次,TRGO 频率会变成 PWM 频率的整数分之一,从机计数直接失真。这是高级定时器做主机时的特有陷阱------通用定时器没有 RCR 反而不会踩。
2.2 从机 TIM2
从机配置项少,但每一项都是链路上的关键节点:
| CubeMX 配置项 | 取值 | 对应寄存器/字段 | 说明 |
|---|---|---|---|
| Slave Mode | External Clock Mode 1 | SMCR.SMS = 111 | 以触发输入上升沿为计数时钟 |
| Trigger Source | ITR0 (TIM1_TRGO) | SMCR.TS = 000 | 硬连线映射,查 Table 78 |
| Prescaler | 0 | PSC = 0 | 从机时钟只用于同步采样与中断 |
| Counter Period | 运行时写入 N-1 | ARR | 单次 burst 的脉冲数 |
| NVIC TIM2 update interrupt | Enable | DIER.UIE | 计满 N 触发停止流程 |
从机的 PSC 虽然不参与 TRGO 计数,但同步器和中断响应依赖从机时钟,保持 0(72 MHz)能让中断响应最快。ARR 在每次 burst 启动时由软件写入目标脉冲数减一,这样同一个工程可以输出任意数量(1~65536)的脉冲。
2.3 容易遗漏的四个配置细节
以下几处 CubeMX 不会替你做对或做全,遗漏后的症状各不相同,单独列出。
其一,OC1PE 必须关闭(或者明白开着意味着什么)。 CCMR1 的 OC1PE 位开启比较预装载后,CCR 的写入进入影子寄存器,要等下一个更新事件才生效。这个特性在"运行中改占空比调速"时是好东西,但在停止流程里是坑:ISR 里若想通过写 CCR = 0 来压制下一个脉冲,预装载会让这次写入延迟到下下个周期才生效,根本拦不住即将到来的上升沿。本文的停止策略不依赖 CCR 写入,直接清 CC1E 断开输出,所以 OC1PE 关闭是最干净的选择;如果你因为别的原因必须开预装载,停止逻辑也依然成立(见第四节),但要在心里清楚 CCR 写入的生效时机。
其二,先启动从机、后启动主机。 从机在 CEN = 0 时对 TRGO 完全失聪,这期间主机发出的脉冲全部丢计数。正确顺序是:写从机 ARR、清计数器和标志位、HAL_TIM_Base_Start_IT(一并置位 CEN 和 UIE),然后再 HAL_TIM_PWM_Start 主机。顺序颠倒的症状是"每次实际输出的脉冲数比设定值少几个,少的个数不固定",因为从机启动晚的时间窗口内到达的 TRGO 被静默丢弃。
其三,OSSI 置 1(BDTR.Off-state Selection for Run mode)。 停止流程清掉 CC1E 后,若 OSSI = 0,TIM1 的输出引脚进入高阻态,电平由外部电路决定;如果驱动器输入端是弱上拉,高阻期间引脚会被拉高,光耦一直导通,可能产生虚假电平甚至误步进。OSSI = 1 时引脚被主动驱动到无效电平,行为确定。这个位藏在 BDTR 里,CubeMX 在 TIM1 高级参数里叫 "Off-state selection for Run mode",默认 Disable,建议改 Enable。
其四,警惕软件 UG 造成的虚假首计数。 PSC 是带预装载的,改分频后要等下一个更新事件才生效,很多教程会让你写 EGR 的 UG 位强制立即生效。问题在于:UG 置位本身就会产生一次更新事件,TRGO 跟着冒出一个上升沿,从机在脉冲输出开始之前就多计了 1,结果是实际输出 N-1 个脉冲且提前停发。规则很简单:burst 启动前如果需要改 PSC,要么接受它下一个周期才生效(推荐,反正不影响脉冲数),要么先把从机计数器和标志清零再置 UG。绝对不要在从机已经开始计数之后去碰 UG。
三、核心代码实现
配置代码由 CubeMX 生成后,关键段落核对如下。主机侧需要手动确认 MMS 与通道配置:
c
/* TIM1 主机: 1kHz 50% PWM, TRGO = 更新事件 */
TIM_MasterConfigTypeDef master = {0};
master.MasterOutputTrigger = TIM_TRGO_UPDATE; /* CR2.MMS[2:0]=010: 更新事件 */
master.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim1, &master) != HAL_OK) {
Error_Handler();
}
TIM_OC_InitTypeDef oc = {0};
oc.OCMode = TIM_OCMODE_PWM1; /* CCMR1.OC1M=110 */
oc.Pulse = 500; /* CCR=500: 50% 占空比 */
oc.OCPolarity = TIM_OCPOLARITY_HIGH;
oc.OCFastMode = TIM_OCFAST_DISABLE;
/* OC1PE=0: 关闭比较预装载, CCR 写入立即生效(见 2.3) */
if (HAL_TIM_PWM_ConfigChannel(&htim1, &oc, TIM_CHANNEL_1) != HAL_OK) {
Error_Handler();
}
从机侧用 HAL_TIM_SlaveConfigSynchro 一次写入 SMCR 的两个字段:
c
/* TIM2 从机: 外部时钟模式1, 触发源 ITR0 = TIM1_TRGO
SMCR.SMS[2:0]=111, SMCR.TS[2:0]=000 (RM0008 Table 78) */
TIM_SlaveConfigTypeDef slave = {0};
slave.SlaveMode = TIM_SLAVEMODE_EXTERNAL1; /* SMS=111 */
slave.InputTrigger = TIM_TS_ITR0; /* TS=000 */
slave.TriggerPolarity = TIM_TRIGGERPOLARITY_RISING;
if (HAL_TIM_SlaveConfigSynchro(&htim2, &slave) != HAL_OK) {
Error_Handler();
}
/* NVIC: 抢占优先级 0, 保证停止窗口不被抢占 */
HAL_NVIC_SetPriority(TIM2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(TIM2_IRQn);
业务层封装成一个启动接口和一个中断服务函数。共享状态用 volatile 修饰,启动顺序严格按 2.3 节执行,所有 HAL 调用检查返回值:
c
/* burst_ctrl.c -------------------------------------------------------*/
#define BURST_MAX_PULSES 65536u /* 从机 16 位: 单次上限 65536 */
typedef enum {
BURST_IDLE = 0,
BURST_RUNNING,
BURST_DONE
} BurstState_t;
/* ISR 与主循环共享, 必须 volatile */
volatile BurstState_t g_burstState = BURST_IDLE;
volatile uint32_t g_burstDone = 0; /* 完成的 burst 计数, 统计用 */
int32_t Burst_Start(uint32_t pulses)
{
if (pulses == 0u || pulses > BURST_MAX_PULSES) {
return -1; /* 防越界: 从机 ARR 为 16 位 */
}
if (g_burstState == BURST_RUNNING) {
return -2; /* 上一次 burst 尚未结束 */
}
/* 顺序 1: 复位从机状态, ARR 在 ARPE 关闭时立即生效 */
__HAL_TIM_SET_COUNTER(&htim2, 0u);
__HAL_TIM_SET_AUTORELOAD(&htim2, pulses - 1u); /* 计满 N 个周期溢出 */
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
/* 顺序 2: 先启动从机(CEN=1, UIE=1), 否则最初几个 TRGO 丢计数 */
if (HAL_TIM_Base_Start_IT(&htim2) != HAL_OK) {
return -3;
}
/* 顺序 3: 后启动主机 PWM; 失败时回退, 别把从机留在计数态 */
if (HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1) != HAL_OK) {
HAL_TIM_Base_Stop(&htim2);
return -4;
}
g_burstState = BURST_RUNNING;
return 0;
}
/* TIM2 更新中断: 从机计满 N 个周期, 此刻恰好 N 个完整脉冲已输出 */
void TIM2_IRQHandler(void)
{
/* 第一句直接清 CC1E: 输出立即回到无效电平, 压掉停止窗口内
可能出现的多余上升沿。不走 HAL_TIM_IRQHandler 分发,
省约 1.5us 延迟, 见第四节"停止窗口"分析 */
TIM1->CCER &= ~TIM_CCER_CC1E;
if (__HAL_TIM_GET_FLAG(&htim2, TIM_FLAG_UPDATE) != RESET) {
__HAL_TIM_CLEAR_FLAG(&htim2, TIM_FLAG_UPDATE);
HAL_TIM_PWM_Stop(&htim1, TIM_CHANNEL_1); /* CC1E=0 + MOE=0 */
HAL_TIM_Base_Stop(&htim1); /* CEN=0: 不再产生 TRGO */
HAL_TIM_Base_Stop_IT(&htim2); /* 从机停计数、关更新中断 */
g_burstState = BURST_DONE;
g_burstDone++;
}
}
主循环的用法非常轻,burst 期间 CPU 完全空闲,可以去做采样、通信等别的活:
c
while (1)
{
if (Burst_Start(10000u) == 0)
{
while (g_burstState == BURST_RUNNING) {
/* 此处 0 占用: 硬件在数脉冲, 想干嘛干嘛 */
}
/* 走到这里 = 恰好 10000 个完整脉冲已输出 */
}
HAL_Delay(500);
}
有一个细节值得点名:HAL_TIM_PWM_Stop 清 CC1E 之后主机 CEN 仍是 1,计数器和 TRGO 都还在跑,所以第三步必须再 Base_Stop 主机,否则从机会继续默默计数,残留的 CNT 会污染下一次 burst 的起点。这个"停了输出但没停触发"的坑我在联调时踩过一次,症状是第二次启动的脉冲数时对时错,后来才意识到从机 CNT 没归零。
四、测试验证:三个量化视角
4.1 对照一:PWM 频率理论值 vs 实测值
逻辑分析仪 24 MHz 采样,取 100 个周期求平均:
| 参数 | 理论值 | 实测值 | 偏差 | 原因 |
|---|---|---|---|---|
| PWM 频率(PSC=71, ARR=999) | 1000.000 Hz | 999.982 Hz | -0.0018% | HSE 晶振 ±20ppm 容差内 |
| 高电平时间(CCR=500) | 500.0 μs | 500.1 μs | +0.02% | 同上,采样量化 ±42ns |
| TRGO→CNT 自增延迟 | 2~3 从机时钟(同步器) | 约 83 ns(实测 CNT 跳变滞后边沿) | 与理论一致 | 两级同步器固定延迟 |
频率偏差千分之零点二,完全落在晶振容差内,链路的时基是干净的。第三行顺带验证了 1.3 节说的同步器行为:CNT 自增固定滞后 TRGO 边沿约 3 个从机时钟,对计数总数无影响,只是每条边的计数时刻整体平移。
4.2 停止窗口模型:多发一脉冲的定量解释
先建立模型再测数据。ISR 在从机溢出时刻(= 第 N 个脉冲下降沿、周期边界)被触发,到输出断开之间存在的延迟 T_ISR 构成风险:若 T_ISR 超过下一个周期的 CCR 时间 T_CCR,多余的上升沿就会在断开前溜出去。整个竞争过程如下图,成败只在"中断响应"与"下一个上升沿"谁先到:
TIM2 ISR TIM2 从机 TIM1 输出(PA8) TIM2 ISR TIM2 从机 TIM1 输出(PA8) #mermaid-svg-U2M5cLjwXtRqNOA5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-U2M5cLjwXtRqNOA5 .error-icon{fill:#552222;}#mermaid-svg-U2M5cLjwXtRqNOA5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-U2M5cLjwXtRqNOA5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-U2M5cLjwXtRqNOA5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-U2M5cLjwXtRqNOA5 .marker.cross{stroke:#333333;}#mermaid-svg-U2M5cLjwXtRqNOA5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-U2M5cLjwXtRqNOA5 p{margin:0;}#mermaid-svg-U2M5cLjwXtRqNOA5 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-U2M5cLjwXtRqNOA5 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-U2M5cLjwXtRqNOA5 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-U2M5cLjwXtRqNOA5 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-U2M5cLjwXtRqNOA5 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-U2M5cLjwXtRqNOA5 .sequenceNumber{fill:white;}#mermaid-svg-U2M5cLjwXtRqNOA5 #sequencenumber{fill:#333;}#mermaid-svg-U2M5cLjwXtRqNOA5 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-U2M5cLjwXtRqNOA5 .messageText{fill:#333;stroke:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-U2M5cLjwXtRqNOA5 .labelText,#mermaid-svg-U2M5cLjwXtRqNOA5 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .loopText,#mermaid-svg-U2M5cLjwXtRqNOA5 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-U2M5cLjwXtRqNOA5 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-U2M5cLjwXtRqNOA5 .noteText,#mermaid-svg-U2M5cLjwXtRqNOA5 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-U2M5cLjwXtRqNOA5 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-U2M5cLjwXtRqNOA5 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-U2M5cLjwXtRqNOA5 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-U2M5cLjwXtRqNOA5 .actorPopupMenu{position:absolute;}#mermaid-svg-U2M5cLjwXtRqNOA5 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-U2M5cLjwXtRqNOA5 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-U2M5cLjwXtRqNOA5 .actor-man circle,#mermaid-svg-U2M5cLjwXtRqNOA5 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-U2M5cLjwXtRqNOA5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第 N 个脉冲下降沿 窗口内必须完成 T_ISR < T_CCR = D×T_PWM 第 N+1 个脉冲上升沿(被压掉) 溢出中断(周期边界) 清 CC1E(停止延迟 T_ISR) 若超时: 多余上升沿溜出 → N+1 PWM_Stop + Base_Stop 输出恒定无效电平
窗口约束为:
T_CCR = D × T_PWM > T_ISR
即 f_PWM < D / T_ISR (D 为占空比)
用 GPIO 翻转实测了两种停止路径的延迟:走 HAL_TIM_IRQHandler 分发进回调再调 HAL_TIM_PWM_Stop,典型 1.8 μs、被 FreeRTOS tick 抢占时最差 2.9 μs;ISR 第一句直接寄存器清 CC1E,实测 0.28 μs。代入模型得到两个临界频率:HAL 路径 0.5/2.9μs ≈ 172 kHz,直接路径 0.5/0.8μs(含 tick 最差抢占 0.5 μs)≈ 625 kHz。预测意味着:200 kHz 下 HAL 路径应偶发失败,直接路径应零失败。
| 停止路径 | 实测 ISR 延迟 | 模型临界频率 | 200 kHz × 200 轮实测 | 与预测一致性 |
|---|---|---|---|---|
| HAL 分发 + 回调停发 | 典型 1.8 / 最差 2.9 μs | 172 kHz | 199/200 精确,1 轮 N+1 | 吻合(tick 恰落窗口内) |
| ISR 首句直接清 CC1E | 0.28 μs | 625 kHz | 200/200 精确 | 吻合 |
理论对照达成:失效边界不是玄学,窗口模型算出来的 172 kHz 与实测"200 kHz 才开始偶发"完全对得上。
4.3 参数扫描:PWM 频率与 CPU 负载/精确性的权衡
固定单次 10000 脉冲、50% 占空比、直接寄存器停止路径,扫 PWM 频率,每档跑 20 轮;方式 A(PWM 中断计数)作为对照组同板实测:
| PWM 频率 | T_CCR(50%) | 窗口裕量 | 方式B 精确率(20轮) | 方式A 单脉冲 CPU 负载 | 方式A 状态 |
|---|---|---|---|---|---|
| 1 kHz | 500 μs | 1786× | 20/20 | 0.14% | 正常 |
| 10 kHz | 50 μs | 179× | 20/20 | 1.4% | 正常 |
| 50 kHz | 10 μs | 36× | 20/20 | 7% | 正常,主循环轻微变慢 |
| 100 kHz | 5 μs | 18× | 20/20 | 14% | 可用但明显卡顿 |
| 200 kHz | 2.5 μs | 8.9× | 20/20 | 28% | 主循环 1ms 任务拖到 3.3ms |
| 1 MHz | 0.5 μs | 1.8× | 20/20(屏蔽 tick 后) | 140%,中断吃满 | 失效,计数开始丢失 |
这组数据给出两条工程结论。其一,方式 B 的精确性在 200 kHz 以内有 9 倍以上裕量,放心用;1 MHz 是 72 MHz F103 的物理极限区(窗口只剩 1.8 倍),实际项目建议工作在 ≤200 kHz 并保持占空比不低于 20%。其二,方式 A 的负载随频率线性上涨,200 kHz 时 28% 的 CPU 被中断吃掉,500 kHz 时主循环基本瘫痪,1 MHz 时连计数本身都不可靠------这就是"为什么脉冲数控制要用硬件计数"的定量答案。
4.4 长跑统计
1 kHz、单次 10000 脉冲,连续 5000 轮过夜跑(约 14 小时):逻辑分析仪逐边沿计数与从机 CNT 终值交叉核对,5000/5000 轮脉冲数全部精确等于 10000,零多发零漏发。200 kHz、直接路径、tick 不屏蔽,连续 5000 轮:同样全部精确。停止窗口在两个数量级的频率跨度上都经受了长时验证。
五、一次翻车实录:200 kHz 下偶发 N+1 的完整排查
这一节把 4.2 节表格背后那次真实的翻车完整还原,因为排查过程比结论更有价值。当时最初版本走的是最"标准"的写法------CubeMX 生成的 TIM2_IRQHandler 里调 HAL_TIM_IRQHandler 分发,在 HAL_TIM_PeriodElapsedCallback 回调里做停止逻辑。1 kHz 测试一切完美,结果把 PWM 提到 200 kHz 做高频压测时,逻辑分析仪逐边比对发现了问题。
症状:连续 200 轮 burst 中,绝大多数轮精确 10000 个脉冲,但约每 200 轮出现一次实际输出 10001 个。多发的那一个永远出现在 burst 末尾,且是一个完整的上升沿。
工具与初始假设:第一反应是从机计数有误,但回读 TIM2 CNT 终值是 10000,与设定一致------从机没错,多发的是"从机停发之前主机又冒出的一个边沿"。于是怀疑停止动作本身不够快,把逻辑分析仪的 GPIO 通道接到调试脚,ISR 进入时拉高、退出时拉低,量停止延迟。
测量与验证:实测 HAL 分发路径的延迟典型 1.8 μs,最差 2.9 μs。翻代码找到最差值的来源:FreeRTOS 的 tick 中断(1 kHz)偶尔恰好插在 TIM2 中断前面,抢占优先级当时都配的是 5。200 kHz、50% 占空比下,周期边界到下一个上升沿的窗口 T_CCR = 2.5 μs,2.9 μs 的延迟意味着 tick 撞进窗口的那一轮必然多发一个脉冲------这解释了"偶发"和"频率越高越容易翻"两个特征。
根因确认:查 RM0008 输出控制部分确认 CC1E 清零是立即生效的(不等更新事件),也就是说只要 ISR 第一时间把 CC1E 清掉,2.5 μs 窗口绰绰有余;问题出在 HAL 分发链路把 0.3 μs 能干完的事拖到了 2.9 μs。窗口模型 4.2 节由此建立,反过来预测 172 kHz 以下 HAL 路径也应当安全------用 100 kHz 复测 200 轮,零失败,模型自洽。
修复与验证:三管齐下------TIM2 中断抢占优先级提到 0;ISR 第一句直接寄存器清 CC1E,把关键动作从 2.9 μs 压到 0.28 μs;burst 期间不屏蔽 tick(实测没必要,0.8 μs 最差延迟对 2.5 μs 窗口仍有 3 倍裕量)。修复后 200 kHz × 200 轮 100% 精确,1 kHz × 5000 轮长跑同样干净。这个问题的排查启示在于:偶发性失效一定要先建立"窗口 × 概率"的定量框架再动手改代码,否则很容易在错误的层面(比如怀疑从机计数)浪费时间。
六、故障排查速查
按现象组织,每条都给出从现象到根因的完整链路,标注最常见原因。
问题 1:从机 CNT 一直为 0,PWM 停不下来。 最常见原因。排查链:逻辑分析仪确认 PA8 有 PWM 输出,排除主机没跑;调试器读 TIM2 CNT 恒 0,从机根本没收到触发;读 SMCR 发现 SMS = 000 或 TS 配错;再读 TIM1 CR2.MMS------CubeMX 默认值是 Reset(000),Reset 事件在正常运行中根本不发生。根因即 TRGO 选了默认的 Reset 而非 Update。修复:MMS 改 010。验证:CNT 开始随 PWM 周期递增。
问题 2:实际脉冲数比设定值少几个,且个数不固定。 排查链:先看启动顺序代码,确认 HAL_TIM_Base_Start_IT 是否在 HAL_TIM_PWM_Start 之前;把顺序调换后症状消失,确认为从机启动晚于主机、前几个 TRGO 丢失。根因即 2.3 节"其二"。附加核对项:burst 启动前是否遗留 UG 操作(见 2.3 节"其四",UG 造成的虚假首计数会让结果稳定地少 1,与"个数不固定"相区别)。
问题 3:软件计数正常,电机却一步没走。 排查链:逻辑分析仪看引脚波形,发现输出恒高、没有任何边沿;回查 CCR = 1000 > ARR = 999。PWM 模式 1 下 CNT < CCR 恒成立,输出常高,而更新事件照常发生、从机照常计数,软件视角一切正常。根因是占空比 100% 时脉冲边沿消失。修复:CCR 必须 ≤ ARR,占空比留出边沿。
问题 4:第二次 burst 的脉冲数时对时错。 排查链:单跑一次 burst 精确,连续跑第二次开始异常;调试器在第二次启动前读从机 CNT,发现不是 0;回查停止代码,发现只调了 HAL_TIM_PWM_Stop 而漏了 Base_Stop 主机。根因即第三节末尾说的"输出停了、触发没停",从机 CNT 残留污染起点。修复:停止序列补全四步(清 CC1E → PWM_Stop → Base_Stop 主机 → Base_Stop_IT 从机)。
问题 5:高频下偶发 N+1。 即第五节翻车实录,最常见原因是停止路径延迟超过 CCR 窗口。排查链:GPIO 翻转量 ISR 延迟 → 对照窗口模型算临界频率 → 确认失效轮次与 tick 抢占重合。修复:抢占优先级提满 + ISR 首句直接清 CC1E。验证:目标频率下连续 200 轮零失败。
问题 6:改了 PSC 后第一次 burst 数量少 1。 排查链:复现条件固定为"改 PSC → 置 UG → 启动 burst";注释掉 UG 后症状消失。根因是 EGR.UG 自身产生一次更新事件,TRGO 多冒一个边沿,从机在脉冲开始前已计 1。修复遵循 2.3 节"其四":要么不置 UG,要么先清从机再置。
问题 7:从机更新中断一次都不进。 排查链:CNT 在涨、能计到 ARR,说明链路正常;查 DIER.UIE 是否置位------误用 HAL_TIM_Base_Start(不带 IT)只置 CEN 不置 UIE;再查 NVIC 使能与优先级(FreeRTOS 下 configMAX_SYSCALL_INTERRUPT_PRIORITY 之上的优先级无法使用 FreeRTOS API,但中断本身仍会进)。最常见原因是把带 IT 的启动函数写成了不带 IT 的版本。
七、扩展与变体
突破 65536 的单次上限。 从机是 16 位,单次 burst 最多 65536 个脉冲。需要更多时可以级联:把从机 TIM2 的溢出经它自己的 TRGO 再喂给 TIM3(TIM3 的触发输入包含 TIM2_TRGO,具体 ITR 编号查 RM0008 Table 78 核对),构成 32 位计数链;或者干脆在应用层做"多次 burst 拼接",两次 burst 之间的间隙控制在一个步距以内对大多数机构没有影响。
单定时器变体(RCR 方案)。 若 TIM2~TIM4 都被占用而 TIM1 空闲,可退到方式 C:RCR = N-1,更新事件只在第 N 个周期结束时发生一次,ISR 里停发即可,连从机都省了。代价是失去逐脉冲反馈、且只能用带 RCR 的高级定时器;停止窗口分析与本文完全相同。相关阅读:STM32F4的PWM脉冲数量精确输出 --- F4 平台上的主从实现,思路可平移。
更高平台的边界。 F4/H7 定时器时钟更高,停止窗口按同样的模型缩放,直接路径的临界频率轻松上到 MHz 级;H7 上还有 DMAMUX+BDMA 控制任意 IO 出脉冲的玩法,本质是把波形表交给 DMA 搬运,灵活但复杂度上了一个台阶,除非需要任意 IO 或非周期波形,否则主从方案仍是最省心的选择。
八、总结
核心要点回顾。主从方案的价值在于把"数脉冲"从 CPU 搬进硬件:TRGO(CR2.MMS=010,更新事件)与从机外部时钟模式 1(SMCR.SMS=111,TS=000 选 ITR0)构成的触发链,让从机 CNT 与输出脉冲数严格一一对应;停止时序决定成败,ISR 首句直接清 CC1E 把停止延迟压到 0.28 μs,配合抢占优先级 0,在 200 kHz、50% 占空比下保有 8.9 倍窗口裕量;从机 16 位决定了单次 65536 的上限,超限走级联或应用层拼接。
适用边界。本方案面向步进/伺服驱动器这类"脉冲+方向"接口,频率上限受停止窗口约束(72 MHz F103、直接路径、50% 占空比下约 625 kHz);需要任意 IO 输出、非周期波形或频率远超此范围时,应转向 DMA 搬运波形表的方案。占用两个定时器是主要代价,定时器紧张时优先考虑 RCR 单定时器变体。另外提醒:本文的"精确"指脉冲个数精确,脉冲频率的长期稳定度仍取决于晶振精度,对速度一致性有苛刻要求的场合要单独评估时基。
局限性方面,停止窗口模型中的 tick 抢占延迟依赖具体 RTOS 配置,移植到其他工程时应重新实测 T_ISR 而不是照搬本文数值;同步器对 TRGO 频率的 1/3 经验上限来自采样理论,极端频率下建议实测验证。下一步可以往两个方向走:一是给 burst 状态机加上加减速规划(梯形/S 曲线),让脉冲频率随进度变化,配合本文的精确停发就是完整的步进定位方案;二是把方向信号、使能信号的时序要求(驱动器通常要求方向建立时间先于第一个脉冲若干微秒)纳入启动流程,做成更完备的驱动器接口层。
如需获取本文完整代码和更多实战项目,可开通 CSDN 技术会员。
版本备注
- 硬件平台:STM32F103C8T6 最小系统板(HSE 8 MHz),逻辑分析仪 24 MHz 采样,步进驱动器 STEP/DIR 接口
- 软件版本:Keil MDK 5.36 + STM32CubeMX 6.9.2 + STM32CubeF1 HAL 库 V1.8.5,FreeRTOS V10.4.6(tick 1 kHz)
- 兼容说明:TIM2/TIM3/TIM4 做 ITR0 从机对 F103 通用;互联型 F105/F107 及 F4 系列的 ITR 映射表不同,需查对应参考手册 Table 78 核对;HAL 库 1.8.x 与 1.6.x 的 TIM Ex 主从配置函数签名一致,无已知行为差异
- API 变更风险:HAL_TIM_SlaveConfigSynchro 在旧版 HAL 中名为 HAL_TIM_SlaveConfigSynchronization,F1 V1.8.x 已统一为前者,移植旧工程时注意函数名替换
参考资料
- RM0008(STM32F1 参考手册):TIMx 寄存器描述(CR2.MMS、SMCR.SMS/TS、CCMR1.OC1PE、BDTR.OSSI)、Table 71 触发输出选择、Table 78 内部触发连接
- STM32主从定时器实现PWM精确输出脉冲数控制电机
- STM32F103C8T6控制步进电机,实现频率、占空比、脉冲个数控制
- STM32F4的PWM脉冲数量精确输出
- ST AN4776《通用定时器实战手册》:主从同步与脉冲计数章节