STM32 定时器主从模式输出精确脉冲数实测:TRGO 触发链拆解、多发一脉冲根因与干净停止时序落地

文章目录

    • 摘要
    • 前言
    • 一、核心原理:一条硬连线串起来的触发链
      • [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 已统一为前者,移植旧工程时注意函数名替换

参考资料

相关推荐
青梅味猪大肠35 分钟前
【操作系统-60】临近适应算法
java·linux·开发语言·嵌入式硬件·算法·操作系统
FakeOccupational42 分钟前
【电路笔记 STM32】RTC配置:晶振选择+闹钟Configuration+中断设置
笔记·stm32·实时音视频
ChaseVLSI1 小时前
FPGA与ASIC的区别详解:内部结构、开发成本与适用场景,项目选型到底该选哪个
嵌入式硬件·fpga开发·硬件架构·硬件工程
东莞市壹星自动化设备有限公司1 小时前
透明圆瓶内部特征怎样定位:折射追踪与近远相位分支
嵌入式硬件·自动化·需求分析·几何学·骨骼绑定
Dev_XH1 小时前
【成品设计】基于物联网的配电箱智能防护系统设计
stm32·单片机·嵌入式硬件·物联网
Dev_XH3 小时前
【成品设计】基于单片机的工厂设备能耗监控与优化系统
stm32·单片机·嵌入式硬件·物联网
不脱发的程序猿3 小时前
STM32制作单手鼠标键盘
stm32·嵌入式硬件·计算机外设·usb·qmk
anguoyang16 小时前
RK3588 上模型「能跑但很慢」:逐算子查 CPU 回退的完整方法与实测
嵌入式
路上有只喵19 小时前
认识元器件 · 单片机(MCU/MPU/SOC) · 实战篇 11
单片机·嵌入式硬件