1. 从一次电机驱动炸管说起:为什么要啃 TIM1 这几个寄存器位
前阵子帮朋友调一块三相无刷电机驱动板,主控用的是 STM32F103 的 TIM1 互补 PWM 输出。硬件搭好、代码跑起来,波形用示波器一看,问题来了:上电瞬间,互补通道里本该关断的那一路冒出了一个窄脉冲,直接把半桥的一对管子给顶穿了。查了两天,最后定位到BDTR寄存器里的OSSR和OSSI两个位配置不当,加上CR2里的OISX、OISXN空闲电平没设对,导致定时器在使能前和刹车后的输出状态完全不受控。
这件事让我意识到,STM32 高级定时器 TIM1 的这几个位——OSSR、OSSI、OISX、OISXN——虽然名字看着像天书,但它们直接决定了功率级电路在“非正常 PWM 输出”期间的安全状态。搞电机驱动、逆变器、数字电源、大功率 LED 驱动的朋友,绕不开这几个位。而网上大部分教程只告诉你“置 1 使能”,却不讲清楚置 1 之后到底发生了什么、什么时候该置 1、什么时候必须置 0。
这篇内容就是把这几个位彻底拆开讲透。我会从 TIM1 输出级的内部结构讲起,说明每个位控制的是哪一段逻辑,然后给出互补 PWM、单路 PWM、刹车保护等不同场景下的推荐配置,最后附上我实际踩过的坑和排查方法。适合已经会用 STM32 定时器输出 PWM、但对其安全机制还没吃透的嵌入式开发者,也适合正在做功率电子项目、需要保证上电和故障状态下输出确定性的工程师。看完你至少能做到:拿到一个 TIM1 互补输出需求,能自己推导出BDTR和CR2该怎么配,而不是抄一段代码了事。
2. 先搞懂 TIM1 输出级的内部结构,这几个位才有落脚点
2.1 从计数器到引脚:一条输出链路上的三道“闸门”
很多人配 TIM1 的时候,脑子里只有“ARR 决定频率、CCR 决定占空比”这一层,实际上从计数器比较匹配到最终引脚电平,中间隔了好几级逻辑。我习惯把它画成一条链路来理解:
第一级是比较单元产生的原始参考信号OCxREF,这是定时器核心根据 CNT 和 CCR 比较得出的“理想波形”。第二级是输出使能逻辑,由CCER寄存器里的CCxE、CCxNE控制,决定这个参考信号能不能往引脚送。第三级就是本文的主角——空闲状态与关闭状态管理,由BDTR的OSSR、OSSI和CR2的OISx、OISxN共同决定:当输出被“关掉”或者定时器还没“正式开工”时,引脚到底呈现高电平、低电平还是高阻。
打个生活化的比方:OCxREF是水龙头里流出来的水,CCxE是水龙头开关,而OSSR/OSSI/OISx管的是“停水期间管道里该是满的还是空的、阀门该朝哪边”。你要是只管开关不管停水状态,一开阀就可能喷一脸。
2.2 为什么普通定时器没这么多事,偏偏 TIM1 要搞这么复杂
通用定时器(比如 TIM2、TIM3)的输出相对“单纯”,关了就关了,引脚状态基本跟着CCxE走。但 TIM1 是高级控制定时器,它的设计目标就是驱动功率桥——半桥、全桥、三相逆变。这类场景有两个硬性要求:
其一,互补输出的两路绝不能同时导通,否则就是直通短路,功率管瞬间报废。所以 TIM1 内置了死区插入和互补输出逻辑,而OSSR/OSSI正是这套安全逻辑的一部分。
其二,系统在复位、初始化、刹车、故障等“非运行”状态下,输出必须是确定的。不能出现引脚悬空导致栅极驱动误触发的情况。OISx/OISxN就是用来定义这个“确定状态”的。
换句话说,通用定时器的输出逻辑是“能用就行”,TIM1 的输出逻辑是“出错也不能炸”。理解了这层设计意图,再看那几个位就不会觉得是冗余设计了。
2.3 四个位各管哪一段:一张表先建立全局印象
在深入细节前,先用一张表把四个位的职责范围框定清楚,避免后面混淆:
| 位名称 | 所在寄存器 | 核心作用 | 生效时机 |
|---|---|---|---|
OSSR | BDTRbit 11 | 运行模式下,通道关闭时是否让输出进入空闲态 | 定时器运行中,CCxE/CCxNE被清零时 |
OSSI | BDTRbit 10 | 定时器未使能(CEN=0)时,是否让输出进入空闲态 | 定时器停止、初始化阶段 |
OISx | CR2对应位 | 定义通道 x 主输出(CHx)空闲时的电平 | 空闲态被触发时 |
OISxN | CR2对应位 | 定义通道 x 互补输出(CHxN)空闲时的电平 | 空闲态被触发时 |
这里的关键区分是:OSSR和OSSI是**“要不要进入空闲态”的开关**,而OISx/OISxN是**“进入空闲态后电平是高还是低”的选择器**。前者管“去不去”,后者管“去了站哪边”。很多人配错就是因为把这两组混为一谈,以为设了OISx就自动生效,其实还得看OSSR/OSSI放不放行。
3. OSSR 与 OSSI:一对容易混淆的“运行态”与“停止态”开关
3.1 OSSR:运行中关通道时,输出该不该“体面退场”
OSSR全称是 Off-State Selection for Run mode,翻译过来就是“运行模式下的关闭状态选择”。它的作用场景很具体:定时器正在跑(CEN=1),但某个通道的输出使能位CCxE或CCxNE被清零了。
这时候有两种可能。如果OSSR=0,通道一关,输出就彻底撒手不管,引脚进入高阻或者由外部电路决定,内部输出级不再驱动。如果OSSR=1,通道关闭后输出不会立刻“躺平”,而是进入空闲态,电平由OISx/OISxN决定。
为什么需要这个?还是拿电机驱动举例。假设你在运行中因为某种保护逻辑临时关掉了某一路 PWM,如果OSSR=0,那一路的栅极驱动输入瞬间悬空,可能被干扰误触发;如果OSSR=1且OISx设成低电平,那一路就稳稳地保持关断电平,功率管可靠截止。所以在互补输出、驱动功率管的场景下,OSSR基本都应该置 1。
我个人的经验是:只要你的 TIM1 输出接了栅极驱动器或者任何对输入电平敏感的功率级,OSSR就别犹豫,置 1。只有在纯粹做信号发生、输出接高阻负载、且你确实希望关通道后引脚彻底释放的场合,才考虑置 0。
3.2 OSSI:定时器还没“上班”时,输出该摆什么姿态
OSSI是 Off-State Selection for Idle mode,管的是**定时器根本没使能(CEN=0)**的阶段,也就是芯片刚复位、外设刚初始化、或者你主动停了定时器的时候。
这个场景比OSSR更“危险”,因为这时候你的固件可能还没跑几行,GPIO 和定时器都处于初始状态。如果OSSI=0,定时器不使能时输出级不驱动,引脚状态取决于 GPIO 配置和外部上下拉;如果OSSI=1,输出级会主动把引脚拉到OISx/OISxN定义的空闲电平。
上电瞬间那个炸管问题,根源就在这里。当时OSSI=0,定时器还没使能,互补输出引脚处于不确定状态,栅极驱动芯片输入端浮空,被电机母线的 dv/dt 耦合出一个窄脉冲,半桥直通。后来把OSSI置 1、OISxN设成对应关断电平,上电波形立刻干净了。
注意:
OSSI置 1 后,定时器未使能期间引脚是被内部输出级主动驱动的,这会和 GPIO 的复用配置产生交互。配置顺序上要先配好 GPIO 复用功能,再配定时器,否则可能出现短暂的驱动冲突。这个顺序问题我在第 5 节会详细说。
3.3 两者的本质区别:时间轴上的两个不同区间
把OSSR和OSSI放到时间轴上,区别一目了然:
OSSI管的是“定时器没运行”的区间——上电到CEN置 1 之前,以及CEN清零之后。OSSR管的是“定时器运行中但通道被关”的区间——CEN=1但CCxE/CCxNE=0。
一个是“没上班时的状态”,一个是“上班中途离岗的状态”。两者互不重叠,但共同覆盖了所有“非正常 PWM 输出”的时刻。这也是为什么安全设计里通常两个都置 1——把两个区间都管住,输出才真正全程可控。
有个细节值得强调:当OSSI=1时,即使CEN=0,输出级也在工作,这意味着定时器的输出驱动是“常开”的。有些低功耗场景会介意这点静态驱动,但对功率驱动来说,这点代价换来的是确定性,非常划算。
4. OISX 与 OISXN:空闲态到底该高还是该低,这是个安全问题
4.1 OISx 与 OISxN 的定义边界:主输出与互补输出分开设
OISx和OISxN分别对应通道 x 的主输出 CHx和互补输出 CHxN的空闲电平。注意它们是分开的、独立的,不是简单取反关系。这一点极其重要,因为很多功率级的上管和下管驱动极性并不对称。
比如常见的半桥栅极驱动,高边管和低边管的使能极性可能相反,或者高边采用自举供电、要求关断时明确拉低。这时候你就需要OISx=0、OISxN=1这样的“非互补”空闲配置。如果你想当然地认为“互补输出空闲电平一定相反”,那配出来的结果可能正好让某个管子误导通。
OISx的取值含义很直接:置 0 时空闲电平为低,置 1 时空闲电平为高。OISxN同理,作用于互补引脚。真正需要动脑的是怎么根据你的功率级拓扑推导出该设什么,而不是记住“置 1 还是置 0”。
4.2 从功率管安全关断反推空闲电平:一个推导方法
我总结了一个简单的推导流程,每次做新板子都按这个走一遍:
第一步,确定每个功率管的关断条件。查管子或栅极驱动芯片的手册,明确栅极输入什么电平时管子可靠截止。大多数 N 沟道 MOSFET 配常规驱动是低电平关断,但也有例外。
第二步,确定空闲态发生时,你希望每个管子处于什么状态。安全原则永远是“全部关断”,绝不允许任何管子导通。
第三步,把“希望的引脚电平”映射到OISx/OISxN。如果某路空闲时要低电平关断,对应OISx或OISxN就置 0;要 high 才关断的(比如某些 P 沟道或负压关断拓扑),就置 1。
第四步,交叉验证互补对。确认OISx和OISxN配置下,同一桥臂的上下管不会同时满足导通条件。这一步是防直通的最后一道检查,我每次都会在纸上画一遍电平表。
举个具体例子:某半桥,上管高电平导通、下管高电平导通(各自独立驱动),那么空闲时两路都该是低电平,OISx=0、OISxN=0。如果驱动芯片是反相的(输入低电平才导通),那空闲时两路都该是高电平,OISx=1、OISxN=1。看到没,互补对完全可能同高或同低,取决于驱动极性,绝不能想当然。
4.3 刹车(BKIN)触发后,OIS 系列位同样在起作用
很多人以为OISx/OISxN只在OSSR/OSSI触发的空闲态生效,其实刹车事件发生后,输出同样会进入由OISx/OISxN定义的状态(在刹车配置允许输出进入空闲态的前提下)。刹车是 TIM1 的安全保护机制,一旦 BKIN 引脚检测到有效电平,硬件立刻把输出拉到安全状态,这个“安全状态”就是OISx/OISxN说了算。
这意味着OISx/OISxN的配置直接决定了过流、过压等故障发生时,功率级能不能被可靠关断。这是它最核心的价值,也是为什么我说这几个位是“安全位”而不是“功能位”。功能配错了顶多波形不对,安全位配错了就是炸管。
提示:刹车功能和
OSSR/OSSI是协同工作的。建议在调试阶段用信号发生器人为给 BKIN 一个触发,用示波器观察刹车后各引脚电平是否符合OISx/OISxN的设定,这是验证安全配置最直接的手段。
5. 不同应用场景下的配置组合与实操步骤
5.1 三相互补 PWM 驱动:一套可直接抄的初始化顺序
这是 TIM1 最典型的用法。下面这套顺序是我反复验证过的,重点是先让输出处于安全态,再逐步放开,避免初始化过程中出现毛刺。
// 以 STM32F1 标准外设库风格示意,HAL 库逻辑一致 // 步骤1:配置 GPIO 为复用推挽,先不使能定时器 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // ... 配置 CH1/CH1N/CH2/CH2N/CH3/CH3N 六个引脚 // 步骤2:时基配置,此时 CEN 仍为 0 TIM_TimeBaseStructure.TIM_Prescaler = 71; // 72MHz -> 1MHz 计数 TIM_TimeBaseStructure.TIM_Period = 999; // 1kHz PWM TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 步骤3:输出比较配置,先不使能 CCxE/CCxNE TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Disable; TIM_OCInitStructure.TIM_OutputNState = TIM_OutputNState_Disable; // ... 配置 CH1~CH3 // 步骤4:关键——先配空闲电平,再配 OSSR/OSSI // 假设低电平关断,主输出和互补输出空闲都拉低 TIM1->CR2 &= ~(TIM_CR2_OIS1 | TIM_CR2_OIS1N | TIM_CR2_OIS2 | TIM_CR2_OIS2N | TIM_CR2_OIS3 | TIM_CR2_OIS3N); // 步骤5:使能 OSSR 和 OSSI,让空闲态受控 TIM1->BDTR |= (TIM_BDTR_OSSR | TIM_BDTR_OSSI); // 步骤6:配置死区时间 TIM_BDTRStructure.TIM_DeadTime = 0x18; // 具体值按驱动需求算 TIM_BDTRConfig(TIM1, &TIM_BDTRStructure); // 步骤7:最后才使能主输出 MOE 和各通道 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_OC1Init(...); // 此时 OutputState 设为 Enable // ... TIM_Cmd(TIM1, ENABLE);这套顺序的核心思想是:在MOE(主输出使能)置位之前,把所有安全相关的位都配好。MOE是 TIM1 输出级的“总闸”,它没开之前,即使CCxE置了 1,引脚也不会真正输出 PWM。利用这个特性,可以把MOE当作最后一步的“点火开关”,前面所有配置都在“熄火”状态下完成,最安全。
5.2 单路 PWM 且不关心空闲态:什么时候可以偷懒
如果你的 TIM1 只用了 CH1 单路输出,接的是 LED 或者一个不敏感的负载,那OSSR/OSSI确实可以都置 0,省得配。但我要提醒一句:只要负载对“引脚悬空”敏感,就别偷这个懒。哪怕是个继电器驱动,引脚悬空被干扰误吸合也可能出问题。
还有一种情况是输出接的是纯数字逻辑输入,对方有内部上下拉,那OSSI=0也能接受。判断标准就一条:空闲期间引脚状态不确定,会不会导致下游误动作。会,就必须配;不会,才可以省。
5.3 刹车保护场景:OIS 与 BKIN 的联动配置要点
带刹车保护的配置,除了前面那套,还要额外注意几点。刹车使能位BKE、刹车极性BKP、刹车滤波BKF要和OISx/OISxN一起考虑。特别是BKP设错极性,会导致刹车“该触发时不触发、不该触发时乱触发”,这个坑我见过不止一次。
配置刹车后,务必做一次故障注入测试:手动拉 BKIN 引脚到有效电平,观察输出是否立刻进入OISx/OISxN定义的状态,并且MOE是否被硬件自动清零。如果MOE没清,说明刹车配置有问题,保护形同虚设。这个测试花不了五分钟,但能救命。
6. 踩坑实录与排查速查表
6.1 上电毛刺:OSSI 没配导致的经典事故
前面提过的那次炸管,完整复盘一下。现象是上电瞬间互补通道出现窄脉冲。排查路径:先怀疑 GPIO 初始化顺序,改成先配复用再开时钟,无效;再怀疑死区不够,加大死区,无效;最后用示波器抓MOE和引脚电平的时序,发现MOE置位前引脚就已经有动作了,定位到OSSI=0时输出级不驱动、引脚被外部干扰。
解决就是OSSI=1加OISxN设对。这里有个经验:凡是上电时序相关的异常,优先查OSSI和 GPIO 复用配置的先后。这两个是上电阶段输出状态的唯一决定因素。
6.2 运行中关通道输出乱跳:OSSR 与 CCxE 的时序问题
另一个案例:运行中通过软件清零CCxE来临时关闭某路输出,结果关掉后引脚电平乱跳。原因是OSSR=0,通道一关输出级就撒手,引脚被母线上的干扰带着跑。改成OSSR=1后,关通道即进入空闲态,电平稳定。
这个坑的教训是:运行中动态开关通道的场合,OSSR必须置 1。如果你的保护逻辑里有“临时关某路 PWM”的操作,这条尤其重要。
6.3 排查速查表
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 上电瞬间输出毛刺 | OSSI=0或 GPIO 复用顺序错 | 置OSSI=1,调整初始化顺序 |
| 运行中关通道后电平乱跳 | OSSR=0 | 置OSSR=1 |
| 刹车后输出未到预期电平 | OISx/OISxN配反 | 按功率管关断条件重新推导 |
| 互补对空闲时同时导通 | OISx/OISxN未交叉验证 | 画电平表逐路核对 |
| 刹车不触发 | BKE/BKP配置错 | 故障注入测试验证 |
| 空闲电平与预期相反 | 混淆主输出与互补输出 | 确认OISx对 CHx、OISxN对 CHxN |
6.4 几条不写在手册里的实操心得
第一,调试阶段先把OISx/OISxN全设成“关断电平”,等波形调通了再考虑其他。安全优先,别一上来就追求波形漂亮。
第二,用MOE做总开关,而不是靠CCxE逐个关。MOE是硬件级的总闸,响应快、逻辑简单,比软件逐个清CCxE可靠得多。
第三,每次改BDTR或CR2后,重新做一次故障注入测试。这几个位之间有关联,改一个可能影响另一个的行为,别假设“只改了一点点”。
第四,示波器抓时序时,把MOE、CEN、BKIN 和至少一路输出一起抓。单看输出引脚看不出因果,把控制信号和输出放一起,问题往往一眼就现形。
7. 把这几个位当成安全设计的一部分,而不是配置负担
回到最初那个炸管的教训。OSSR、OSSI、OISx、OISxN这四个位,本质上不是“让 PWM 更好看”的功能位,而是功率级在非运行状态下的最后一道安全防线。OSSR和OSSI决定“什么时候进入安全态”,OISx和OISxN决定“安全态长什么样”。两组配合,才能保证从复位到运行、从运行到故障、从故障到停机的每一个时刻,输出都是确定的、安全的。
我现在的习惯是:拿到任何 TIM1 项目,先把这四个位的配置写在纸上,按功率管的关断条件推导一遍,再动手写代码。这个习惯帮我省下了至少两次可能的炸管损失。如果你也在做带功率级的 TIM1 应用,建议把这个推导流程固定下来,别等到板子冒烟了才回头查寄存器。