STM32高级定时器互补PWM输出详解:死区配置与刹车保护

发布时间:2026/9/9 14:28:13
STM32高级定时器互补PWM输出详解:死区配置与刹车保护 简介面向STM32嵌入式开发者的PWM输出实验工程包基于标准外设库完成定时器TIM3与TIM4通道一的PWM输出并重点演示高级定时器TIM8与TIM1的互补PWM配置支持频率与占空比实时调整适用于电机控制、电源管理等需要精确信号控制的场景。压缩包共134个文件容量约400KB代码部分包含33个C源文件与34个H头文件另有Keil工程配置文件以及编译生成的HEX、AXF等文件整体是可直接打开、编译并烧录验证的完整工程。资源已有1542人学习下载。实验代码覆盖定时器工作模式设置、预分频与自动重载计算、比较寄存器调节占空比、高级定时器互补输出、程序初始化流程等关键环节并涉及边沿对齐PWM模式、互补通道死区控制与频率动态调整等知识点可帮助读者深入理解STM32定时器PWM的设计与实现方法快速迁移到电机驱动、开关电源等实际项目中。对于需要在现有工程中快速加入PWM功能或调试互补波形的开发者该工程提供了可直接参考的参数配置与主程序框架可显著减少底层驱动编写时间。 做电机控制和电源类的工程PWM 输出迟早要迈过“互补”这道坎。普通定时器发个呼吸灯、驱动个舵机很容易但一旦接上 H 桥或者三相逆变器你会发现只靠软件去错开上下管的导通时间根本不现实——中断一抖动、代码一跑偏上下管同时导通轻则波形畸形重则直接炸管。我这段时间在做 TIM8 和 TIM1 的双定时器互补 PWM 输出实验频率可调、占空比可调还要带死区把整个配置链路和踩坑过程整理成文。这篇文章适合正在学 STM32 高级定时器、准备做电机驱动或数字电源的开发者尤其是卡在“为什么我配了 PWM 却没有输出”和“死区到底怎么设置”这两个问题上的人。1. 为什么互补PWM必须交给TIM1/TIM81.1 普通定时器到底缺了什么很多初学者习惯用 TIM2、TIM3、TIM4 来产生 PWM这些通用定时器在 F103 上数量多、用起来顺手。但如果你把需求换成“同时输出一对互补信号中间还要留死区”通用定时器就力不从心了。通用定时器可以用 PWM 模式产生单路波形但要让 CH1 和 CH2 输出精确互补、可编程死区、可硬件刹车它根本没有对应的硬件逻辑。高级定时器和通用定时器最本质的差别在于它内部多了三样东西死区生成器可以在互补输出之间自动插入一段延迟时间硬件完成不需要软件干预。刹车输入Break外部故障信号可以直接把输出关断比如过流、过压信号接入后几微秒内就能封锁 PWM。主输出开关MOE也就是 BDTR 寄存器里的主输出使能位它单独控制高级定时器所有 PWM 输出是否真正送到引脚上。如果把普通 PWM 当作一个“能定时翻转的电平发生器”那么高级定时器的互补输出就是一个“自带安全机制的功率驱动核心”。这不只是多几个寄存器那么简单而是硬件层面的驱动保护设计。电机驱动、逆变器、开关电源这类应用恰恰最吃这点。1.2 TIM1/TIM8的资源与引脚布局STM32F103ZET6 这颗芯片上TIM1 和 TIM8 都挂在 APB2 总线上最高时钟 72MHz。每个定时器都有 4 个独立通道其中前 3 个通道带互补输出第 4 通道是单端输出另外还有专门的刹车输入引脚。我这次实验用的是 CH1 和 CH1N但实际上三对互补通道的配置方法完全一样只是引脚不同。下面把两者的引脚对照整理出来方便查表定时器通道主输出引脚互补输出引脚备注TIM1CH1PA8PB13带互补TIM1CH2PA9PB14带互补TIM1CH3PA10PB15带互补TIM1CH4PA11无单端输出TIM1BKINPB12无刹车输入TIM8CH1PC6PA7带互补TIM8CH2PC7PB0带互补TIM8CH3PC8PB1带互补TIM8CH4PC9无单端输出TIM8BKINPA6无刹车输入实际接线下TIM1_CH1 和 TIM1_CH1N 在 PA8 / PB13 上这两脚在最小系统板上都引出来了是调试互补 PWM 最方便的通道。TIM8 的 PC6 和 PA7 使用起来也没问题但要注意 PA7 作为互补输出的时候不能再把它当普通 IO 用复用配置时容易搞混。2. 把频率、占空比、死区的账一次算清2.1 时基公式和频率/占空比高级定时器本质上还是一个 16 位向上计数器配合预分频器、自动重装载寄存器产生周期比较寄存器产生占空比。核心公式还是那条PWM频率 定时器时钟 / ((PSC 1) × (ARR 1))这里的 PSC 是预分频系数ARR 是自动重装载值。在 F103 上TIM1 和 TIM8 的时钟默认 72MHz如果 PSC 0那么频率就完全由 ARR 决定。占空比则通过修改 CCR1 来实现占空比 CCR1 / (ARR 1) × 100%这个方式很简单但实际调频率时有个容易想错的地方ARR 不但影响周期也会影响占空比的百分比。比如 ARR 从 7200 改成 3600周期缩短一半但 CCR1 不变的话绝对导通时间没变占空比却变成了原来的两倍。所以做频率可调功能时必须把 ARR 和 CCR1 一起更新而不是只改一个。2.2 死区 DTG 的实现逻辑死区由 BDTR 寄存器的 DTG[7:0] 位控制它不是线性的 0 到 255 映射而是分成了几个区段目的是在有限的 8 位寄存器里既支持小死区的精细步进又支持大死区的粗略配置。以 72MHz 时钟为基准tCK_INT 约等于 13.9nsDTG[7:5] 0xx死区时间 DTG × tCK_INT步进 13.9ns范围 0 ~ 1763ns。DTG[7:5] 10x死区时间 (64 DTG[6:0]) × 2 × tCK_INT步进 27.8ns。DTG[7:5] 110死区时间 (32 DTG[6:0]) × 8 × tCK_INT步进 111ns。DTG[7:5] 111死区时间 (32 DTG[6:0]) × 16 × tCK_INT步进 222ns。很多人直接往 DT 寄存器里塞一个时间数字却发现实际死区完全不是预期就是因为没搞懂这个分段逻辑。简单记法当需要的死区在 1μs 以内时DTG 写 0 到 127此时死区 DTG × 13.9ns超过 1.7μs 后要用更高的区段。CubeMX 里填死区时间时它会自动帮你算 DTG但直接用寄存器操作就必须自己算。2.3 具体实例20kHz、50%、1μs 死区假设我需要一组 20kHz、占空比 50%、死区 1μs 的互补 PWM定时器时钟 72MHz。先算周期。20kHz 对应的周期是 50μs也就是 50000ns。一个计数周期等于 tCK_INT × (ARR 1)所以 ARR 1 72000000 / 20000 3600也就是 ARR 3599。占空比 50%则 CCR1 1800。死区 1μs因为 1000ns / 13.9ns ≈ 72落在 DTG 第一区段0 到 127所以 DTG 直接写 72实际死区约 999ns误差不到 0.1%完全可用。再验证一下如果要求死区 3μs72MHz 下第一区段最大只能到 127 × 13.9ns ≈ 1763ns不够了就得用第二区段。3μs / (2 × 13.9ns) ≈ 108落在 64 到 126 区间内因此 DTG[6:0] 108 - 64 44DTG 整体写入 (2 5) | 44 108。这样算一次就错不了。3. 寄存器配置链路核心是MOE别搞反3.1 配置步骤拆解这个实验我一开始直接复用通用定时器的 PWM 配置代码结果引脚死活没有输出查了半天才发现高级定时器比普通定时器多了一道工序必须把 BDTR 寄存器的 MOE 位置 1主输出才真正开放。下面把完整初始化流程写出来用的是标准库风格方便对照寄存器手册看void TIM1_ComPWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); // PA8 TIM1_CH1PB13 TIM1_CH1N GPIO_InitStructure.GPIO_Pin GPIO_Pin_8 | GPIO_Pin_13; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_Init(GPIOB, GPIO_InitStructure); // 时基20kHzPSC0ARR3599 TIM_TimeBaseStructure.TIM_Period 3599; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM1, TIM_TimeBaseStructure); // PWM 模式1占空比 50%主输出和互补输出同时使能 TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse 1800; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); // 死区 DTG 72约 1μs TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 72; TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Disable; TIM_BDTRInit(TIM1, TIM_BDTRInitStructure); // 关键MOE 置 1开放主输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }这段代码里TIM_CtrlPWMOutputs(TIM1, ENABLE)就是置位 MOE 的封装。如果漏掉这一步前面的 CCR、ARR 配置得再准引脚上也什么都看不到。这部分我也在 F407 和 F103 上分别验证过行为一致。3.2 为什么 MOE 是所有问题的根源MOE 位是高级定时器单独的“总闸”。普通定时器只要 CR1 的 CEN 置位计数器跑起来输出就自然出现在引脚上。高级定时器不是这样它把 PWM 生成逻辑和外部引脚输出之间加了一道安全隔离MOE 就是这道隔离的开关。这样设计是有道理的系统发生故障时单片机只需要把 MOE 清零就能瞬间切断所有 PWM 输出不需要逐通道去关 CCR、改 CCER这在电机堵转或过流场景下反应速度远快于软件逐通道操作。初学阶段最容易踩的坑是初始化代码顺序不对导致上电瞬间引脚电平不确定。比如先进 BDTR、置 MOE再配 PWM 模式那么在 MOE 开放的一瞬间输出引脚可能还是默认状态在驱动电路上表现为一个短暂的误触发。稳妥的顺序是先把 GPIO 和通道模式都配好最后再置 MOE。如果换成 OSSR 和 OSSI 位还能控制空闲状态的电平这两个位在空闲模式下决定输出是关闭还是有效电平建议实验阶段都配成 Disable即关闭态输出无效电平。3.3 同时配置 TIM8 时的第二次坑TIM1 调通之后我以为 TIM8 复制粘贴改个通道、改个 GPIO 就行了结果实际配置才发现 TIM8 的互补输出在 PA7而 PA7 在同一个最小系统板上还连接着一些其他外设比如某些板子把它引到了 DAC 或者按键上。这就是同时在 TIM1 和 TIM8 上做实验容易出问题的典型原因引脚冲突。TIM8 的配置结构和 TIM1 完全一样只要把 RCC、GPIO、定时器基地址换掉即可。代码如下void TIM8_ComPWM_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; TIM_OCInitTypeDef TIM_OCInitStructure; TIM_BDTRInitTypeDef TIM_BDTRInitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM8, ENABLE); // PC6 TIM8_CH1PA7 TIM8_CH1N GPIO_InitStructure.GPIO_Pin GPIO_Pin_6; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_7; GPIO_Init(GPIOA, GPIO_InitStructure); // 其余时基、PWM、死区配置与 TIM1 相同只是寄存器改为 TIM8 TIM_TimeBaseStructure.TIM_Period 3599; TIM_TimeBaseStructure.TIM_Prescaler 0; TIM_TimeBaseStructure.TIM_ClockDivision TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode TIM_CounterMode_Up; TIM_TimeBaseInit(TIM8, TIM_TimeBaseStructure); TIM_OCInitStructure.TIM_OCMode TIM_OCMode_PWM1; TIM_OCInitStructure.TIM_OutputState TIM_OutputState_Enable; TIM_OCInitStructure.TIM_OutputNState TIM_OutputNState_Enable; TIM_OCInitStructure.TIM_Pulse 1800; TIM_OCInitStructure.TIM_OCPolarity TIM_OCPolarity_High; TIM_OCInitStructure.TIM_OCNPolarity TIM_OCNPolarity_High; TIM_OCInitStructure.TIM_OCIdleState TIM_OCIdleState_Reset; TIM_OCInitStructure.TIM_OCNIdleState TIM_OCNIdleState_Reset; TIM_OC1Init(TIM8, TIM_OCInitStructure); TIM_OC1PreloadConfig(TIM8, TIM_OCPreload_Enable); TIM_BDTRInitStructure.TIM_OSSIState TIM_OSSIState_Disable; TIM_BDTRInitStructure.TIM_OSSRState TIM_OSSRState_Disable; TIM_BDTRInitStructure.TIM_LOCKLevel TIM_LOCKLevel_OFF; TIM_BDTRInitStructure.TIM_DeadTime 72; TIM_BDTRInitStructure.TIM_Break TIM_Break_Disable; TIM_BDTRInitStructure.TIM_BreakPolarity TIM_BreakPolarity_High; TIM_BDTRInitStructure.TIM_AutomaticOutput TIM_AutomaticOutput_Disable; TIM_BDTRInit(TIM8, TIM_BDTRInitStructure); TIM_CtrlPWMOutputs(TIM8, ENABLE); TIM_Cmd(TIM8, ENABLE); }需要留意的是F1 系列 TIM1 和 TIM8 的引脚没有重映射功能必须严格按照上表接线。不像 TIM2 到 TIM4 可以靠 AFIO 重映射调整位置。4. 频率和占空比的动态调节4.1 修改 ARR 与 CCR 的同步问题实验要求“频率可调、占空比可调”最简单的实现方式是用按键去改变 ARR 和 CCR。但这里有一个容易翻车的细节ARR 和 CCR 不是实时生效的它们都有预装载影子寄存器由 ARPE 位和 OC1PE 位控制。ARPE 1 时写入 ARR 的值会先缓存在预装载寄存器里等更新事件到来才真正进入影子寄存器OC1PE 1 时CCR1 同理。这个机制的好处是当同时修改 ARR 和 CCR1并且两者都启用了预装载时它们会在同一个更新事件瞬间一起生效保证频率和占空比的切换是“原子操作”中间不会出现一个周期比例失调的波形。如果关掉预装载写入操作直接作用于当前寄存器那么改 ARR 和改 CCR1 之间输出波形就可能出现一个不正常的周期。所以在初始化里ARPE 和 OC1PE 必须都置 1。标准库中TIM_TimeBaseInit默认会把 ARPE 设为 Disable需要手动打开TIM_ARRPreloadConfig(TIM1, ENABLE)。实际动态调节还有一个常见问题是按键扫描和 PWM 更新之间的时序关系。按键消抖一般有十几毫秒延迟对 20kHz 的 PWM 来说一个周期只有 50μs按键处理完全不影响更新事件。所以不用担心 PWM 周期和按键处理冲突。4.2 按键调节的代码方案我的实验方案是四个按键KEY1 频率增加KEY2 频率减少KEY3 占空比增加KEY4 占空比减少。频率范围设在 1kHz 到 50kHz占空比限制在 5% 到 95%。部分代码如下void SetPWMFreqDuty(TIM_TypeDef* TIMx, uint32_t freq_hz, uint16_t duty_percent) { uint16_t arr, ccr; if (freq_hz 1000) freq_hz 1000; if (freq_hz 50000) freq_hz 50000; if (duty_percent 5) duty_percent 5; if (duty_percent 95) duty_percent 95; arr 72000000UL / freq_hz - 1; ccr (uint32_t)arr * duty_percent / 100; TIMx-ARR arr; TIMx-CCR1 ccr; // 如果希望立即生效可以软件产生一次更新事件 TIMx-EGR | TIM_EGR_UG; }按键事件里只需要维护两个全局变量频率变量和占空比变量然后调用这个函数分别更新 TIM1 和 TIM8void Key_Process(void) { if (KEY1_Pressed()) { freq_hz 1000; if (freq_hz 50000) freq_hz 1000; SetPWMFreqDuty(TIM1, freq_hz, duty); SetPWMFreqDuty(TIM8, freq_hz, duty); } if (KEY3_Pressed()) { duty 5; if (duty 95) duty 5; SetPWMFreqDuty(TIM1, freq_hz, duty); SetPWMFreqDuty(TIM8, freq_hz, duty); } }注意TIMx-EGR | TIM_EGR_UG会产生一次软件更新事件如果此时恰好接近硬件更新事件两者可能重叠虽然实际使用中很少出问题但严谨的做法是在更新中断里做频率调整或者使用影子寄存器的自然更新而不是软件强制更新。实验中我为了简化直接软件触发波形没有发现异常只是逻辑上多做一次无伤大雅的事件。4.3 使用 HAL 的注意点如果用 CubeMX 生成工程高级定时器的配置界面比标准库直观很多时基、输出通道、互补通道、死区时间、刹车模式都是图形化配置。但在调用函数启动输出时有个经典坑很多人都卡在这里。在 HAL 库中启动普通 PWM 用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1)但这只启动了主输出。互补输出还需要单独调用HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIMEx_PWMN_Start(htim1, TIM_CHANNEL_1);如果只调用第一个PA8 上有波形PB13 上一直为低很多人的第一反应是硬件坏了其实只是互补输出没有启动。HAL_TIMEx_PWMN_Start内部会把 MOE 置位同时打开互补通道。调试时如果发现主输出正常、互补无输出十有八九是少了这一句。5. 实测验证、刹车保护与常见陷阱5.1 示波器看死区的正确姿势配置完成后把示波器两个通道分别接 PA8 和 PB13共地夹子都用板子的 GND。此时应该看到两路相位互补、占空比一致的 PWM 波形。把时基放大到 1μs/div 左右仔细看 PA8 下降沿和 PB13 上升沿之间有一段两个都为低电平的间隙这就是死区。我习惯把示波器触发设为 PA8 的下降沿然后显示两路波形这样能稳定观察每次切换瞬间的死区。正常情况下两路信号会有一段短暂的重叠为低电平时间大约 1μs。如果看不出明显的低电平间隙可能的原因有两种一是死区值写得太小比如只写了 DTG 113.9ns 的时间在示波器上几乎看不见二是通道极性配反了两路波形根本不是在预期相位上切换看起来就没有正确的死区间隙。如果你的示波器带有时间测量功能直接把光标放到低电平间隙的两端测出来的时间应该在 0.9μs 到 1.1μs 之间基本对应配置值。没有示波器的话用逻辑分析仪采样也能看但采样率建议至少 50MHz 以上否则死区时间可能被采样间隔吞掉肉眼看不出来。5.2 刹车功能实测实验板的硬件上通常没有真实的过流信号所以我用 GPIO 模拟了一路刹车输入验证高级定时器的故障保护行为。上位操作是把 TIM1 和 TIM8 的刹车功能打开配置 BKIN 引脚为输入然后连接一个按键按下时拉高电平模拟故障。void Break_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // PB12 作为 BKIN 输入下拉 GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPD; GPIO_Init(GPIOB, GPIO_InitStructure); } void Break_Enable(TIM_TypeDef* TIMx) { // 使能刹车功能输入低电平触发并且自动恢复输出 TIMx-BDTR | TIM_BDTR_BKE; // 刹车使能 TIMx-BDTR ~TIM_BDTR_BKP; // 刹车输入有效电平为低 TIMx-BDTR | TIM_BDTR_AOE; // 下个更新事件自动恢复 }故障发生时把 BKIN 引脚拉低MOE 位被硬件清零PA8 和 PB13 立刻被封锁到无效电平。实测下来从引脚电平变化到 PWM 消失的延迟在数百纳秒内这个速度靠软件中断根本做不到。清除故障后由于配置了 AOE经过一个更新事件后输出自动恢复。这个功能在做电机驱动时特别实用。过流比较器输出的信号直接接到 BKIN一旦电枢电流超标硬件自动封锁 PWMMCU 只需要在中断中处理故障记录和状态显示。注意如果不需要自动恢复就把 AOE 清掉故障之后必须软件重新置位 MOE防止故障源未解除时盲目恢复输出导致二次损坏。5.3 调试现场遇到的典型问题这次实验从零调通前后记录的典型问题有五个基本都是初学者或者第一次用高级定时器的人高频踩坑第一引脚上毫无波形排查半天发现是 PA8 和 PB13 的复用时钟没开或者 GPIO 模式配成了推挽输出而不是复用推挽。F103 上必须配GPIO_Mode_AF_PP普通推挽输出不会把定时器信号引到引脚。第二PWM 只有一个通道工作另一个始终低电平。先检查TIM_OutputNState是否 Enable再检查TIM_CtrlPWMOutputs是否执行。HAL 环境下注意双启动函数都要调用。第三死区时间实测和配置不符。排查方向两个DTG 分段计算错误、或者定时器时钟不是 72MHz。如果 APB2 预分频配置不同实际定时器时钟可能是 36MHz 或其他值需要按实际时钟重算。第四调节频率时 PWM 偶发乱跳。这个基本是 ARR 和 CCR1 没有同步更新导致的启用两个预装载位并且在同一个代码路径里同时修改能明显改善。第五上电瞬间输出异常高电平。这是有驱动负载时特别危险的信号处理办法是确保初始化顺序是“GPIO 复用 → 通道模式 → 死区 → 最后置 MOE”并且尽量把负载连接放在配置完成之后。更进一步可以用TIM_OCIdleState设置空闲状态为有效或无效让刹车状态下引脚电平确定。实验阶段先把 MOS 管和电机的连接断开只观察波形等确认无误再挂负载。链路完全调通后的波形表现很稳定TIM1 和 TIM8 输出的两组互补 PWM 在相位和死区上表现一致。按键切换频率时前一个周期的最后一个脉冲可能会略有畸变但下一个周期马上恢复不影响实际使用。如果对切换瞬间的要求高建议改用定时器更新中断在更新事件里读写 ARR 和 CCR1能保证切换点永远发生在周期的起点。最后再分享一个调试经验调高级定时器时不要在代码里一次性写完所有功能我建议分成四步验证。第一步只配普通 PWM 单通道确认频率占空比正确第二步加互补通道确认两路波形完全反相第三步加死区用示波器确认间隙时间第四步加刹车人为触发故障信号观察 MOE 封锁行为。每步都验证通过再做下一步出现问题时定位范围会小很多。这个实验做完之后回头再看电机驱动的底层原理很多之前觉得玄乎的东西其实都是高级定时器里的那点硬件机制。本文还有配套的精品资源点击获取