一、定时器的三件事:时基、比较、捕获
我学定时器时最有效的一次转变,是不再按“寄存器清单”去背它,而是按三件事去理解:
- 时基:一个计数器按什么频率、朝哪个方向、数到多少回绕。它决定“多久产生一次事件”。
- 比较:把计数值和若干个比较值做比较,相等时产生中断、翻转电平或者改变输出。它决定“在一个周期的哪一刻做事”。
- 捕获:外部或内部事件到来的瞬间,把当前计数值抄下来。它决定“量出两个事件之间的时间”。
这三件事共用同一个计数器,所以它们会互相影响:改了分频,捕获到的时间刻度就变了; 改了计数模式,比较事件的次数就变了。这也是为什么我后来把配置顺序固定下来—— 顺序错了,单项配置都对,合起来就是不对。
| 这三件事 | 典型用途 | 依赖什么 | 我见过的常见误用 |
|---|---|---|---|
| 时基 | 系统节拍、超时计时、喂狗、帧间隔判定 | 时钟源、分频、计数模式、重装值 | 把分频“值”当分频“比”;重装值忘记减一;改了分频却没重新核算上层的时间 |
| 比较 | PWM 输出、定时翻转、定时采样触发 | 比较值、输出模式、极性、影子寄存器 | 中途改比较值出现半个脉冲;比较值取到周期值;极性配反导致上电即导通 |
| 捕获 | 测脉宽、测频率、测外部事件间隔 | 输入通道、边沿选择、溢出处理 | 只算一次差值,没处理计数器回绕;把捕获值当“绝对时间”用 |
顺带说一句,定时器的“用途”是会变的,改用途之前一定要查清谁在依赖它。 我在一个低功耗项目里踩过这个坑:那块板子原本用某个定时器做计时, 后来为了在休眠里也能喂狗,把它的用途改成了“周期性唤醒”, 原本的计时功能迁到另一个定时器上。三个动作是同一次改动完成的, 因为只改前两步的话,所有用那个计时的地方都会算错——而这种错不会报编译错误, 只会表现为“休眠唤醒后设备复位”。定时器的编号在很多代码里是硬编码的, 改配置前先在工程里搜一遍它的名字,比事后排查便宜得多。
二、时基配置四步,以及每一步的常见错位
我现在配时基固定走四步:时钟源 → 预分频 → 计数模式与重装值 → 使能。逐步说。
- 时钟源。先确认这个定时器挂在哪条总线上、总线时钟是多少、有没有倍频。 同一颗芯片上不同定时器挂不同总线,频率可能不一样;有的芯片给定时器单独留了一个倍频位, 不打开的话算出来的时间会整整差一倍。
-
预分频。这里有两个“差一”要记:寄存器里写
N表示 N+1 分频; 写0表示 1 分频(也就是不分频),不是“关闭分频器”。 另一个坑是预分频器通常带缓冲:改完之后要等一次更新事件才真正生效, 所以“设置完立刻读回来核对”往往读到的是旧值。 - 计数模式与重装值。向上计数从 0 数到重装值,一共是“重装值 + 1”个计数; 要 N 个计数就写 N−1。向下计数同理。中心对齐(先上后下)的一个完整周期是两倍重装值, 而且比较事件在一个周期里会出现两次——如果你在比较中断里做“每周期一次”的事情, 它就翻倍了。另外重装值写 0 表示每 1 个计数就回绕一次,不是“停止”。
- 使能。使能前要先把影子寄存器装载好:很多定时器提供一个“手动产生更新事件”的控制位, 写它会把预分频值和重装值搬进真正参与计数的影子寄存器,同时把计数器清零、 并把更新标志置起来。所以顺序必须是“装载 → 清标志 → 开中断 → 启动计数”, 否则一开中断就立刻进一次中断服务,把第一拍的时间算错。
/* 按个人理解重写的最小片段:只保留顺序,所有具体数值都用符号代替 */
timer_clock_enable(TIMx); /* 1. 先给时钟,否则后面写寄存器全部无效 */
timer_set_prescaler(TIMx, PSC_VALUE); /* 2. 寄存器值 = 分频比 - 1 */
timer_set_period(TIMx, ARR_VALUE); /* 3. 计数个数 = 重装值 + 1 */
timer_set_count_mode(TIMx, COUNT_UP); /* 4. 计数模式:边界含义随模式改变 */
timer_generate_update(TIMx); /* 5. 手动产生更新事件,装载影子寄存器 */
timer_clear_update_flag(TIMx); /* 6. 先清标志再开中断,避免立刻进一次 */
timer_enable_update_irq(TIMx);
timer_start(TIMx); /* 7. 最后启动计数 */| 步骤 | 要确认什么 | 常见的错位 | 错了之后的表现 |
|---|---|---|---|
| 1. 时钟源 | 定时器挂哪条总线、总线频率、是否有独立倍频 | 漏开倍频;时钟门控没使能就写寄存器 | 时间整体差一倍;寄存器写进去像“没生效” |
| 2. 预分频 | “值”与“比”差一;改完要等更新事件 | 把分频比直接写进寄存器;改完立刻读回核对 | 频率差一点点;调参时“改了没反应” |
| 3. 计数与重装 | 向上计数是“重装值 + 1”个计数;中心对齐是两倍 | 忘记减一;中心对齐下按单周期算中断次数 | 周期差一个计数;中断频率是预期的一半或两倍 |
| 4. 使能 | 影子寄存器装载、更新标志先清 | 一开中断就进一次;先启动再改重装值 | 第一拍偏短;启动瞬间输出一个异常脉冲 |
这四步里“算”的部分,我在《定时器与单位换算的坑》里专门写过一遍: 那篇讲的是为什么注释里的时间不能信、拿到一个配置怎么倒着核算; 这篇讲的是怎么按顺序把它配出来。两篇一起看, 基本可以覆盖“我配了一个定时器,怎么确认它真的是我想的那样”这个问题。
三、周期中断:最小可靠周期怎么估
配好时基之后,紧接着要回答的是“中断周期能短到什么程度”。我的估法是三项相加:
- 中断服务本身的耗时:进出中断的开销 + 服务程序里真正做的事;
- 最坏阻塞:同优先级和更高优先级中断占用的时间,加上主循环里关中断的临界区;
- 余量:留给“以后还要往里加两行代码”的空间。
第 1 项一定要量,不能靠数指令估。我常用的两个办法: 一是在服务程序入口拉高一个空闲 GPIO、出口拉低,用示波器看高电平宽度, 这个办法最直观,代价是占一个引脚;二是用另一个定时器计数——进中断时读一次计数值、 出中断再读一次,差值乘以计数周期就是耗时。我在一块采集板上就是用后一个办法量出 “一轮 10 次 ADC 采样大约 13 ms”,才敢把采集周期定在 100 ms ——如果不量,很容易按“10 次采样怎么会慢”的主观感觉去排任务。
另外两条经验。第一,能在中断外做的事就不要放进中断:中断里只做“必须在这一刻做”的事 (置标志、搬一个数、更新比较值),滤波、换算、判断、打印统统丢回主循环或者任务里, 这样往往比优化中断服务本身有效得多。第二,能不用中断搬的数据就别用中断搬: 连续采样、批量串口收发这类事情交给 DMA,中断只处理“半满”和“完成”两个节点。
| 数据的搬运方式 | CPU 占用 | 抖动 | 适合什么 |
|---|---|---|---|
| 逐次中断 | 最高,每个数据都要进一次中断 | 受其他中断影响明显 | 低速、事件本身稀疏、需要立即处理的场合 |
| 周期中断里批量处理 | 中等,与数据量无关,与周期有关 | 中等,取决于周期 | 采样、控制节拍、需要和其他任务对齐全局的场合 |
| DMA + 半满/完成中断 | 最低,只在两个节点进中断 | 最小,搬运由硬件完成 | 连续采样、批量收发、缓冲固定的场合 |
还有一个容易忽略的坑:周期中断的频率选得越高,“进中断”本身的开销占比越大。 我见过一个配置,服务程序只有三行,但中断频率高到几乎一半的 CPU 时间花在进出中断上。 这种情况下把周期放宽一倍,比把那三行代码优化十倍更管用。 顺带一提,定时器也常被拿来当“帧间隔”用:不靠串口空闲中断,而是每次收到一个字节就复位定时器, 定时器溢出就认为一帧结束——这是帧同步里的一种做法, 它的可靠性和这个定时器的中断响应直接相关。 而这个帧间隔与方向脚切换之间的相对位置,我写在 《RS485 收发方向控制:硬件自动、外设自动与软件手动》里了。
四、比较输出与 PWM:占空比、极性、空闲电平,以及上电瞬间输出什么
比较输出最常见的用法是 PWM:计数器一路数上去,在到达比较值之前输出一种电平、 之后输出另一种电平,于是“有效电平占的比例”就是占空比。要配的东西比时基多,但真正容易出事的是三样:
- 占空比:等于“有效电平的计数个数 ÷ 一个周期的计数个数”。 因为计数器是从 0 开始的,写比较值时同样要注意“个数”和“值”的关系, 并且要给它留出合法范围——不是任意值都能写。
- 极性:极性位翻转的是“输出有效时引脚是高还是低”, 它不改变占空比,但决定你的驱动电路在上电时看到什么。 极性配反的后果在有功率器件的场合非常严重:本该关断的管子在上电瞬间就导通了。
- 空闲/停止时的输出状态:不少定时器能配置“停止或刹车时输出保持、复位还是高阻”。 这个配置的默认值通常不是你要的那个,但它在正常运行时看不出来, 只在故障和停机时刻才体现——所以必须在初始化里显式写死。
下面这条是我认为最容易出事、也最值得单独说的:上电瞬间输出什么。 引脚一旦被配成复用输出,输出的电平就由外设的当前状态决定,而这时候定时器往往还没启动、 比较值还是复位默认值。如果默认值恰好对应“输出一直有效”,那么你的负载在上电的那几毫秒里 就吃到了满输出。
我在变频驱动那块板子上的做法是把它变成一条固定顺序:
- 先把比较值设成占空比居中的值(也就是上下桥各占一半),而不是让它保持复位默认;
- 把两路的极性按驱动电路的有效电平配好——这一步决定了“上电时管子是关的还是开的”;
- 配好死区(下一节)和停止时的空闲状态;
- 最后才打开主输出使能、启动计数。
这样即使上电就立刻使能,桥臂也停在一个确定的“中间态”,不会出现某一桥上电即全通的局面。 单管驱动的场合(比如用 PWM 模拟电流输出)同理:如果比较值的默认值是 0,某些输出模式下 引脚会一直有效,负载上电就被灌满。把初始比较值写成“零输出对应的值”, 是一条几乎不花成本的保险。
| 配置项 | 它决定什么 | 配错的后果 |
|---|---|---|
| 比较值 | 有效电平的宽度,也就是占空比 | 取到周期值附近时输出异常;无预装载时中途改会出现半个脉冲 |
| 输出模式 | 计数小于比较值时有效,还是大于时有效 | 占空比“反了”:想要 10% 得到 90% |
| 极性 | 输出有效时引脚是高还是低 | 上电瞬间功率管误导通;驱动电路长期反相工作 |
| 空闲/停止状态 | 停止、刹车时引脚保持什么状态 | 只在停机与故障时才暴露,可能让负载停在错误状态 |
| 主输出使能 | 所有输出通道的总闸 | 配置期间就驱动了负载;或忘记打开导致“配好了却没波形” |
| 自动重装预装载 | 重装值何时从影子寄存器生效 | 运行中改周期时出现一个超长或超短的周期 |
最后补一个和“换算”有关的例子。有一类做法是用 PWM 加滤波去模拟 4–20 mA 电流输出, 这时要算的是“工程量到占空比”的映射。我看过的那套实现用的是分段线性: 在量程中点前后各取一段直线,靠两个端点加一个中点三个标定点定下来; 而且第二段的末尾还加了一个很小的经验修正值——也就是说,纯理论换算之后, 实际电路上还差一点点,得靠实测把它补回来。 这类“分段线性 + 单点经验补偿”的写法在模拟输出里很常见, 它比硬套一条公式更接近真实,代价是标定点与被测通道绑定了。 关于标定链路本身,我写在《压力传感器的标定与温度补偿》里了。
五、互补输出与死区
驱动半桥或全桥时,每个桥臂的上管和下管必须严格互补:一个导通,另一个就必须关断。
定时器提供互补输出对(主输出 CHx 与互补输出 CHxN),
用一路 PWM 就能同时驱动上下两管的栅极,不需要外部加反相器。
为什么必须留死区。 功率器件的关断不是瞬时的:栅极电荷要放掉、 驱动电阻决定了放电速度、米勒平台会拖一段时间。如果上管的关断和下管的开通完全对齐, 就会出现一个两个管子都导通的瞬间——母线经上下管直接短路,也就是常说的直通。 死区就是在这两个开关时刻之间插入的“都关断”时间,由定时器的死区发生器在硬件里插入, 每个周期都一致。
死区时间怎么定。 我的算法是分上下限:
- 下限由最坏情况定:上管的关断时间(含驱动电路传输延迟)+ 下管的开通延迟 + 两者的时间差,全部按器件手册与驱动芯片手册里给出的最大值取,再留一点余量。 温度、母线电压、栅极电阻都会影响这几个数,不能只按常温典型值算。
- 上限由波形质量定:死区占载波周期的比例越大,实际加在负载上的有效电压就越偏离你想要的, 表现为低压输出时波形畸变、低速时转矩脉动、电流波形出现平顶。 所以死区不是“越大越安全”,大到一定程度就变成了另一种故障。
死区必须由硬件插入,不能在中断里“先开一个、延时再开另一个”—— 软件做不到每个周期都一样,而且中断一旦被抢占,两个管子就同时导通了。 这一点我认为没有讨论余地:互补输出的死区是硬件的活。
配置互补输出时的顺序,我固定成下面这样:
/* 按个人理解重写的最小片段:重点是顺序,不是取值 */
timer_clock_enable(TIMx);
timer_set_prescaler(TIMx, PSC_VALUE);
timer_set_period(TIMx, ARR_VALUE);
timer_set_compare(TIMx, CH_A, DUTY_CENTER); /* 先给一个确定的初始占空比 */
timer_set_compare(TIMx, CH_B, DUTY_CENTER);
timer_set_polarity(TIMx, CH_A, ACTIVE_LEVEL); /* 极性按驱动电路的有效电平 */
timer_set_polarity(TIMx, CH_B, ACTIVE_LEVEL);
timer_set_deadtime(TIMx, DEADTIME_TICKS); /* 死区按器件手册的最坏值取 */
timer_set_idle_state(TIMx, OUTPUT_SAFE); /* 停机/刹车时引脚停在哪 */
timer_enable_brake_input(TIMx); /* 故障时由硬件把输出拉到安全态 */
timer_generate_update(TIMx); /* 装载影子寄存器 */
timer_clear_update_flag(TIMx);
timer_enable_primary_output(TIMx); /* 总闸:配置完成后才打开 */
timer_start(TIMx); /* 最后启动计数 */
/* 关断顺序反过来:先断输出,再停计数,绝不颠倒 */
timer_disable_primary_output(TIMx); /* 1. 先让引脚进入安全态 */
timer_stop(TIMx); /* 2. 再停计数 */
timer_clear_flags(TIMx); /* 3. 最后清标志 */这段顺序里最值得记的是最后两步的颠倒代价。 先停计数再关输出,会出现一个“计数已经停了、输出还挂在当前电平上”的窗口, 这个窗口里引脚停在什么状态取决于当时的比较结果——可能是导通。 我在脉冲发生小板上遇到过同一类问题的轻量版本:固定个数输出结束后, 如果直接停定时器,最后半个周期还可能多翻转一次,波形就多出一个脉冲; 改法就是先关输出使能、再停定时器。互补输出的场合,这条规律的代价从“多一个脉冲” 变成了“多一次直通”,所以更要在代码里把顺序写死。
| 配置项 | 作用 | 配错的后果 |
|---|---|---|
| 互补通道极性 | 决定主输出与互补输出各自的有效电平 | 上电即直通,这是最严重的一类配置错误 |
| 死区时间 | 插入上下管都关断的间隔 | 太小会直通;太大让低压输出畸变、转矩脉动 |
| 主输出使能 | 所有通道的总闸 | 配置期间就驱动了桥臂;或配完忘记打开 |
| 刹车输入 | 故障时硬件自动把输出拉到安全态 | 靠中断关输出会慢一拍;安全态配错则等于没保护 |
| 空闲状态 | 停止、刹车后引脚的静止状态 | 停机时桥臂停在导通态,下次上电就有风险 |
| 重装值预装载 | 运行中改周期时,新值在更新事件统一生效 | 出现一个超长或超短的载波周期 |
六、载波频率怎么选:损耗、噪声与分辨率的三角
载波频率(也就是 PWM 的开关频率)是最不能拍脑袋的一个参数,因为它同时受三个方向拉扯:
- 往上走:电流纹波变小、电机与电感的可听噪声变小甚至移出人耳范围, 闭环控制的响应也能做得更快。
- 往下走:开关损耗变小、驱动电路发热少、电磁干扰更容易压住, MCU 的中断负担也轻。
- 分辨率:这是最容易被忘掉的一条。一个载波周期里可调的档位数由重装值决定 (向上计数时是“重装值 + 1”档)。档位数不够时,低占空比段每调一档就是一个很大的步进, 表现为“轻载时输出一格一格跳”。
我的核算方法是四步,全部用比例而不是具体数值来推:
- 由器件定上限:按开关器件的开关损耗与驱动能力,先划出载波频率允许的上界。
- 由步进要求定档位数:把“最小可接受的相对步进”换算成档位数—— 要求 1% 的步进,就至少要 100 档;要求 0.5%,就要 200 档。
- 联立检查:计数时钟 = 定时器时钟 ÷(预分频值 + 1);载波周期 =(重装值 + 1)÷ 计数时钟。 把想要的载波频率和需要的档位数代进去,看两者能不能同时满足。
- 不够就二选一:要么降低载波频率换档位数,要么提高定时器时钟(减小分频)换档位数。 两者不能同时最优,必须明确放弃哪一个。
实践中还有一个不在公式里、但很影响体验的因素:可听噪声。 载波落在人耳敏感的频段时,电机和电感会有明显的啸叫;把它推到超声频段虽然听不见了, 但开关损耗、驱动发热和电磁干扰都要付代价。我在变频驱动那个项目里的体会是: “避开听感最敏感的那一段”往往比“越高越好”更实用,因为后者是拿效率和发热换安静。
| 载波频率 | 开关损耗与发热 | 可听噪声 | 电流纹波 | 可用档位数 | 中断负担 |
|---|---|---|---|---|---|
| 往高走 | 上升,驱动电路发热明显 | 移出人耳范围,听感变好 | 变小 | 变少,低速段步进变粗 | 变重,中断频率成正比上升 |
| 往低走 | 下降,效率变好 | 进入可听段,可能出现啸叫 | 变大 | 变多,占空比更细腻 | 变轻 |
一个容易被忽略的连带影响:载波频率一变,“每个载波周期中断一次”的那些逻辑的频率也跟着变。 比如三相 SPWM 的做法是在每个载波更新中断里推进正弦表的索引、 并按“当前目标频率下一个基波周期分多少份”来决定索引步进。 这时候载波频率不只是开关频率,它还是波形更新的时间粒度—— 降载波会让波形的相位推进变粗,升载波会让中断负担线性增长。 改这个参数之前,我一般会把依赖它的地方全部列一遍,再决定动不动。
七、用比较中断“翻转电平”产生任意波形
硬件 PWM 不是万能的。我在一块脉冲发生小板上遇到过这样的需求: 要两路带固定相位差的脉冲,频率很低,而且要能“定量输出 N 个脉冲”。 硬件 PWM 能做到相位差,但“数够多少个就停、还要停在整周期上”很别扭; 最后我用的是另一种做法:让定时器跑一个固定的计数时钟,在比较中断里翻转电平。
具体做法是三步,循环执行:
- 进入比较中断,判断还剩多少个脉冲(定量输出时的剩余计数);
- 需要继续的话,把“下一次翻转的时刻”算出来写进比较值—— 有的定时器还提供“比较匹配时翻转、周期匹配时保持不变”的输出模式, 这样每次中断只翻一次,不用软件去记当前电平;
- 到数了就停:先关输出使能,再让它停在周期的边界上。
相位差也是这么来的:把相位角按比例换算成计数偏置,加到另一路的比较值上, 并且做回绕保护(偏置加过头就减掉一个周期,而不是直接写一个超过周期的值)。 换算之前要先确认计数模式——向上计数和中心对齐的回绕方式不一样, 用错了会出现“相位差看起来对、偶尔又跳一下”的现象。
这个方法的优点是自由:波形形状、两路的相位关系、脉冲个数都由软件决定, 想停就停。代价是抖动:翻转时刻的精度等于中断响应延迟, 而这个延迟受其他中断和临界区影响。所以适用边界很清楚:
| 对比项 | 比较中断翻转电平 | 硬件 PWM |
|---|---|---|
| 波形自由度 | 高,任意波形与任意相位关系 | 受输出模式限制,主要是占空比与相位 |
| 翻转时刻精度 | 等于中断响应延迟,有抖动 | 由计数时钟决定,抖动可忽略 |
| 能到的最高频率 | 低,受中断频率限制 | 高,只受计数时钟与器件限制 |
| CPU 占用 | 每次翻转都要进一次中断 | 几乎为零 |
| 精确控制脉冲个数 | 方便,数够就停 | 较麻烦,通常要配合中断或门控 |
| 适合的场合 | 低频、任意波形、相位关系特殊、要精确个数 | 连续输出、占空比连续可调、对抖动敏感 |
需要说明的是:高频场合必须是硬件输出,但“比较值由谁更新”是另一件事。 三相 SPWM 就是典型的混合做法——引脚上的波形完全由硬件产生,比较值则在每个载波中断里更新, 甚至可以用 DMA 把预先算好的表直接搬到比较寄存器里。 这种写法里中断只负责“喂数”,波形质量不受中断抖动影响。 我在变频驱动那块板子上走的就是这条路:中断里推进频率斜坡、算好三相的占空比写进比较寄存器, 引脚输出交给硬件,死区交给硬件。中断晚来一点,最多让相位推进晚一点, 而不会让某一个脉冲变形。
八、两个真实的边界问题:回绕与多打一个脉冲
这一节是两个我真实踩到、也真实修好的问题。它们都不大,但都属于“想不明白就会反复出现”的那类。
边界一:比较值等于周期值时会发生什么
把比较值写到和重装值一样大,直觉上应该得到“一直有效”(或“一直无效”), 但实际上要看输出模式与计数模式:在“计数值小于比较值有效”的模式下, 由于计数器只数到重装值就回绕,那个“相等”的时刻处在周期的边界上, 得到的往往不是干净的满占空比,而是一个差一档的结果,或者干脆是一个极窄的脉冲。 中心对齐模式下更麻烦:比较事件会在上坡和下坡各出现一次, 边界上的那次可能被压缩成一个很窄的尖峰。
我的处理办法是不把边界值当成普通值用:
- 需要满占空比时,不去写“比较值 = 重装值”,而是切到“强制有效”或者直接把引脚置成常通状态;
- 需要零占空比时同理,切到“强制无效”,而不是指望比较值取 0;
- 做相位偏置或线性换算时,对结果做回绕与钳位:超出周期的减一个周期, 低于下限的抬到下限。我在脉冲小板那类的实现里见过“比较值等于周期时就减一”的写法, 就是这条保护的朴素版本。
/* 错误示范:把换算结果直接写进比较寄存器 */
compare = phase_offset + duty_ticks;
timer_set_compare(TIMx, CH_A, compare); /* 超界时输出会“怪一下” */
/* 正确做法:先钳位再写;需要 0% / 100% 时切换输出模式而不是写边界值 */
compare = phase_offset + duty_ticks;
if (compare >= timer_period(TIMx)) { /* 回绕保护:留出一档余量 */
compare -= timer_period(TIMx);
}
if (compare < MIN_USEFUL_COMPARE) {
compare = MIN_USEFUL_COMPARE; /* 下限钳位,避免极窄脉冲 */
}
timer_set_compare(TIMx, CH_A, compare);边界二:改比较值时会不会多出一个脉冲
这个问题取决于比较寄存器有没有预装载(也就是影子寄存器)。 没有预装载时,写入立刻生效:如果新值落在“已经过去的时刻”上, 这一周期的比较事件就不会发生(少一个边沿);如果落在“马上要到的时刻”上, 就会挤出一个极窄的脉冲(多一个边沿)。 开了预装载之后,新值统一在下一个更新事件生效,周期内部不会被切开, 也就不会出现半个周期的怪脉冲。这也是我在变频驱动里把自动重装预装载打开的原因—— 每个载波周期都要改三次比较值,没有预装载一定会出现畸变。
与之相关的是“停下来的时候多一个脉冲”:这也是我在脉冲发生小板上实际遇到的。 现象是定量输出 N 个脉冲,示波器上偶尔能看到第 N+1 个很窄的脉冲。 原因是在比较中断里改比较值的方案,如果直接停定时器, 最后半个周期可能再翻转一次。我的改法是两条:先关输出使能再停定时器; 以及用“等计数值到达比较点再停”的软停方式,让最后一个脉冲是完整的, 停完再清计数、清标志。这样下游计数的设备既不会多算,也不会少算。
| 边界情形 | 现象 | 原因 | 处理 |
|---|---|---|---|
| 比较值取到周期值 | 满占空比不“满”,或出现极窄脉冲 | 相等时刻落在周期边界,中心对齐下还会出现两次 | 不写边界值:需要 0% / 100% 就切输出模式;换算结果做回绕与钳位 |
| 中途改比较值(无预装载) | 某个周期少一个边沿或多一个窄脉冲 | 写入立即生效,新值可能落在已经过去或马上到来的时刻上 | 打开预装载,让新值在更新事件统一生效 |
| 定量输出结束时直接停定时器 | 最后一个波形不完整,或多出第 N+1 个脉冲 | 停止发生在周期中途,输出状态还在变化 | 先关输出使能,再等周期边界软停,最后清计数与标志 |
九、一份定时器配置自查清单
这份清单按“配之前想清楚 → 配的时候按顺序 → 配完再核对”三段排:
| # | 检查项 | 不通过的典型后果 |
|---|---|---|
| 1 | 这个定时器的时钟源、总线归属、是否有倍频都确认过了吗? | 时间整体差一倍 |
| 2 | 分频比是按“寄存器值 + 1”填的吗? | 频率差一点点,且很难被发现 |
| 3 | 重装值是按“计数个数 − 1”填的吗?中心对齐按两倍核对过吗? | 周期差一个计数;中断频率差一倍 |
| 4 | 改过分频或重装之后,有没有产生一次更新事件让影子寄存器装载? | 改了“没反应”,要复位才生效 |
| 5 | 更新标志是不是在开中断之前先清的? | 一开中断就立刻进一次,第一拍偏短 |
| 6 | 中断服务里做的事,有没有可以挪到主循环的?ISR 耗时量过吗? | 中断占用过高,主循环被拖慢 |
| 7 | 引脚在配置阶段处于安全态吗?主输出使能是不是最后才打开的? | 配置期间负载被误驱动 |
| 8 | 比较值、极性、死区、空闲状态是不是都在开输出之前配好的? | 首个周期出现异常脉冲或误导通 |
| 9 | 互补输出的极性配反会不会导致直通?死区按最坏值算过吗? | 桥臂直通,属于会炸器件的错误 |
| 10 | 改比较值时有没有预装载?会不会在一个周期内出现半个脉冲? | 波形畸变,下游计数出错 |
| 11 | 需要 0% / 100% 占空比时,是钳位还是切到强制有效/无效? | 边界处输出“怪一下” |
| 12 | 关断顺序是不是“先关输出、再停计数”? | 停机瞬间桥臂停在导通态 |
| 13 | 载波频率改变时,依赖它的中断逻辑与分辨率核算都跟着改了吗? | 相位推进变粗、中断负担超标、低速步进变粗 |
| 14 | 这个定时器的“用途”最近有没有被别人改过?依赖它的代码都查过了吗? | 改配置后别处算错,且不报编译错误 |
十、几条我反复用到的经验
- 顺序比数值重要。 我遇到的定时器问题里,多数不是“数算错了”,而是“顺序错了”: 影子寄存器没装载、标志没先清、输出先开、计数先停。数值错了可以算,顺序错了很难查。
- 影子寄存器是“看不见的第三个参与者”。 预分频、重装值、比较值都可能带预装载; 写进寄存器不等于立刻生效。想清楚“什么时候生效”,比记住某个值重要得多。
- 上电与关断时的默认状态要当成功能来设计。 运行时的波形大家都测, 上电那一瞬间和停机那一刻的输出,往往才是真正会出问题的时刻。
- 死区是硬件的活,且不是越大越好。 软件插死区做不到每个周期一致; 而死区过大同样是一种故障,表现为低压输出畸变、低速转矩脉动。
- 载波频率是三角取舍,不是越高越好。 损耗、噪声、分辨率三者互相拉扯, 改它之前先把依赖它的逻辑列一遍。
- 中断耗时一定要实测。 我用过两个办法:入口出口翻转 GPIO 看示波器, 或者用另一个定时器数差值。数指令估出来的值,通常偏差比想象的大。
- 低频任意波形用比较中断,高频连续波形交给硬件。 但“比较值由谁更新”是独立的选择——硬件输出配合中断喂数,是最常见的折中。
- 本篇与《定时器与单位换算的坑》的分工: 那篇回答“注释里的时间为什么不能信、拿到一个配置怎么倒着核算”, 这篇回答“要配一个定时器时按什么顺序做、每一步容易错在哪”。 配之前看这篇,配完之后用那篇的清单核算一遍,两个方向合起来才算闭环。
参考资料与说明
- 各厂商 MCU 参考手册中定时器章节关于时基单元、输出比较、输入捕获、互补输出与死区发生器的说明,属于公开文档。
- 功率器件与栅极驱动芯片数据手册中的开通延迟、关断时间与传输延迟参数,是死区时间下限计算的依据,属于公开文档。
- 关于脉宽调制、载波频率与开关损耗、电流纹波之间关系的公开教材与器件应用笔记。
- 出于对个人项目实现细节的保护,本文只公开方法与思路;涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。
- 文中未给出任何项目的具体载波频率、重装值、分频值、死区时间与引脚分配; 所有做法均来自我个人学习项目中的配置与调试过程,不代表任何产品或交付代码。
- 文中出现的芯片与器件类型仅用于说明技术方案,与相关厂商无隶属或授权关系。