资讯动态

基于STM32的电机控制实战:PWM死区、采样保护与四类电机驱动

发布时间:2026/10/29 18:38:55 来源:尧图企业网站定制
简介《电机应用开发实战指南—基于STM32》是野火电子于2021年5月发布的一本电机控制实战教程面向已具备一定嵌入式基础、希望系统掌握电机驱动开发的工程师与学习者帮助读者打通从电机原理到STM32实际控制的完整链路。全书先介绍直流电机、步进电机、伺服电机与舵机的工作原理及适用场景再梳理有刷、无刷、步进、伺服四类驱动器的电路特点与控制差异随后深入STM32定时器、PWM输出、编码器反馈等关键外设讲解速度环与位置环的调试思路并结合野火STM32全系列开发板给出可落地的实验方案。资源包为1个PDF文件约35.84MB篇幅完整、目录层级清晰便于按章节检索与打印查阅。目前已有863人学习适合作为课程配套读物或项目开发时的手边参考也可用于查漏补缺、快速建立电机控制知识框架。1. 一套把电机从能转推到转得准的 STM32 实战资料很多人第一次碰电机卡住的地方不是算法是板子焊好了电机一上电就抽搐。这份《电机应用开发实战指南—基于STM32》更像一本工作手册定时器怎么配、PWM 死区怎么给、电流电压温度怎么采、编码器怎么读、PID 怎么整一条链路从头拆到尾。全书按直流有刷、舵机、步进、无刷直流四类电机分章每章都落到硬件连接和软件设计末尾还带一个上位机联调环节。它适合已经会点 STM32 外设、但没真正带过电机的嵌入式工程师也适合课程设计要把实物跑起来的学生。它不解决电机选型采购解决的是拿到一台电机之后怎么用 STM32 把它控住、控稳、控得能复现。2. 定时器是电机控制的地基PWM、互补输出与输入捕获怎么配2.1 为什么电机控制绕不开高级定时器电机控制里 CPU 真正干的活只有两件按时间给出开关信号按时间读回外部信号。前者是 PWM后者是捕获两件事都挂在定时器上。用 GPIO 加软件延时也能凑方波可电机一转起来中断抖动加上其它外设抢占周期立刻飘表现出来就是电机滋滋叫、低速一顿一顿。硬件定时器的计数由时钟直接驱动初始化时把 PSC 和 ARR 定死波形由硬件自己发主循环忙不忙都不影响它。选型上有个硬边界高级定时器TIM1/TIM8 这类带互补输出、死区插入和刹车输入通用定时器只有单路 PWM。直流有刷的 H 桥、无刷直流的半桥都必须靠互补输出加死区来防上下管直通所以只能用高级定时器舵机、步进这种只需要一路方波的通用定时器足够。分不清这条线后面接线时就会莫名其妙地两个管子同时导通、电源瞬间打嗝。2.2 时基与 PWM 初始化PSC/ARR 两个数怎么定PWM 频率由计数时钟除以ARR1得到占空比由 CCR 与 ARR 的比值决定。实做时先把 PSC 固定成能让计数时钟落到 1 MHz 的值再让 ARR 去凑目标频率这样 CCR 的一个 tick 恰好是 1 微秒调占空比、算脉宽都不用再换算。ARR 尽量往大了取分辨率才够细20 kHz 载波配 1 MHz 计数时钟只有 50 级驱动直流有刷电机够用但要精细调速就得把计数时钟提到 10 MHz 以上。/* 高级定时器 TIM1时基 单路 PWM标准库风格 */ static void Motor_TIM1_PWM_Init(uint32_t pwm_freq_hz) { TIM_TimeBaseInitTypeDef tb; TIM_OCInitTypeDef oc; uint32_t tim_clk 168000000U; /* TIM1 在 APB2 定时器时钟上168MHz */ uint32_t psc 168U - 1U; /* 168 分频 → 计数时钟 1MHz1 tick 1us */ uint32_t arr (tim_clk / (psc 1U)) / pwm_freq_hz - 1U; RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE); tb.TIM_Prescaler psc; tb.TIM_CounterMode TIM_CounterMode_Up; /* 向上计数边沿对齐 */ tb.TIM_Period arr; /* 20kHz 时 arr 49 */ tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_RepetitionCounter 0; /* 高级定时器独有必须清 0 */ TIM_TimeBaseInit(TIM1, tb); oc.TIM_OCMode TIM_OCMode_PWM1; /* CNT CCR 时输出有效电平 */ oc.TIM_OutputState TIM_OutputState_Enable; oc.TIM_Pulse 0; /* 上电占空比为 0电机不转 */ oc.TIM_OCPolarity TIM_OCPolarity_High; oc.TIM_OCIdleState TIM_OCIdleState_Reset; /* 刹车时主通道拉低 */ TIM_OC1Init(TIM1, oc); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable);/* CCR 走影子寄存器改值不撕裂波形 */ TIM_CtrlPWMOutputs(TIM1, ENABLE); /* 高级定时器不开这个引脚上一点波形都没有 */ TIM_Cmd(TIM1, ENABLE); }这段里最容易漏的是两句RepetitionCounter不清零会让 PWM 周期莫名其妙变成几倍长TIM_CtrlPWMOutputs不开示波器上就是一条直线很多人卡在这里以为芯片坏了。TIM_OCPreload_Enable也建议默认打开否则主循环里实时改 CCR 时偶尔会在周期中途改值输出一个窄毛刺反映到电机上就是轻微的抖。2.3 互补输出与死区DTG 不是随便填的数互补输出给的是 CH1 和 CH1N 一对反相波形但两个 MOS 管关断都需要时间反相瞬间如果上管还没完全关、下管已经开电源就被直接短路业内叫直通。死区就是在这两个边沿之间硬塞一段都不导通的空白时间。教程里给了死区时间的计算方法落到寄存器上就是 BDTR 的 DTG[7:0] 八位分四段线性区间。DTG 取值区间死区时间公式tDTS 5.95 ns 时的实际范围0 ~ 127DT DTG × tDTS0 ~ 756 ns128 ~ 191DT (64 DTG[5:0]) × 2 × tDTS762 ns ~ 1.51 µs192 ~ 223DT (32 DTG[4:0]) × 8 × tDTS1.52 µs ~ 3.0 µs224 ~ 255DT (32 DTG[4:0]) × 16 × tDTS3.05 µs ~ 6.0 µs/* 在 TIM1 上开互补输出并插入死区 */ TIM_BDTRInitTypeDef bdtr; oc.TIM_OutputNState TIM_OutputNState_Enable; /* CH1N 互补通道打开 */ oc.TIM_OCNPolarity TIM_OCNPolarity_High; oc.TIM_OCNIdleState TIM_OCNIdleState_Reset; /* 刹车时互补通道也拉低 */ TIM_OC1Init(TIM1, oc); bdtr.TIM_OSSRState TIM_OSSRState_Enable; /* 运行模式下关闭时输出由定时器接管 */ bdtr.TIM_OSSIState TIM_OSSIState_Enable; /* 空闲模式下强制无效电平 */ bdtr.TIM_LOCKLevel TIM_LOCKLevel_1; /* 锁住 BDTR 关键位防跑飞改写 */ bdtr.TIM_DeadTime 168; /* 落在 128~191 段约 1.24us */ bdtr.TIM_Break TIM_Break_Enable; /* 刹车输入使能 */ bdtr.TIM_BreakPolarity TIM_BreakPolarity_Low; bdtr.TIM_AutomaticOutput TIM_AutomaticOutput_Enable; TIM_BDTRConfig(TIM1, bdtr);死区取多大看 MOS 管的关断时间和栅极驱动电阻。常见做法是先按驱动器手册给的最短关断时间乘 1.5 倍留余量再用示波器双通道看上下管栅极波形确认两段之间有明显间隔且不重叠。死区给太小会烧管给太大会损失有效占空比低速时表现为给到 5% 占空比电机还是不转。TIM_LOCKLevel_1建议打开它锁住 BDTR 里的死区和极性位程序跑飞时也不至于把死区改成 0。2.4 输入捕获测转速边沿到计数值的换算测转速有测频和测周两条路。高转速用测频固定闸门时间内数脉冲低转速用测周测两个上升沿之间的时间低速时精度更高。教程两套都给实现上都是输入捕获把通道配成上升沿触发进中断后把捕获寄存器的值读出来两次相减就是周期对应的 tick 数。/* TIM3_CH1 输入捕获抓上升沿记录两次边沿之间的 tick 差 */ volatile uint32_t g_cap_period_ticks 0; static uint32_t s_last_capture 0; void Motor_TIM3_Capture_Init(uint16_t psc) { TIM_TimeBaseInitTypeDef tb; TIM_ICInitTypeDef ic; NVIC_InitTypeDef nvic; RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitTypeDef gpio {GPIO_Pin_6, GPIO_Mode_AF, GPIO_Speed_100MHz, GPIO_OType_PP, GPIO_PuPd_NOPULL}; GPIO_PinAFConfig(GPIOA, GPIO_PinSource6, GPIO_AF_TIM3); GPIO_Init(GPIOA, gpio); tb.TIM_Prescaler psc - 1U; /* psc 84 → 84MHz/84 1MHz1 tick 1us */ tb.TIM_CounterMode TIM_CounterMode_Up; tb.TIM_Period 0xFFFFU; /* 16 位满量程靠无符号减法处理回卷 */ tb.TIM_ClockDivision TIM_CKD_DIV1; tb.TIM_RepetitionCounter 0; TIM_TimeBaseInit(TIM3, tb); ic.TIM_Channel TIM_Channel_1; ic.TIM_ICPolarity TIM_ICPolarity_Rising; ic.TIM_ICSelection TIM_ICSelection_DirectTI; ic.TIM_ICPrescaler TIM_ICPSC_DIV1; /* 每个边沿都捕不做预分频 */ ic.TIM_ICFilter 0x0F; /* 最狠的数字滤波专治电机换向干扰 */ TIM_ICInit(TIM3, ic); TIM_ITConfig(TIM3, TIM_IT_CC1, ENABLE); nvic.NVIC_IRQChannel TIM3_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 2; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); TIM_Cmd(TIM3, ENABLE); } void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_CC1) ! RESET) { uint32_t now TIM_GetCapture1(TIM3); /* 无符号相减天然处理 16 位计数器回卷不用额外判溢出 */ g_cap_period_ticks now - s_last_capture; s_last_capture now; TIM_ClearITPendingBit(TIM3, TIM_IT_CC1); } }TIM_ICFilter这一项别省。电机换向时地线上有几十伏的尖峰直接耦合到信号线上滤波关掉就会测出一堆几十微秒的假周期转速显示乱跳。滤波值 0x0F 意味着连续 8 个采样点一致才认一次边沿代价是引入了几个微秒的延迟测低速完全够用。得到 tick 数之后转速公式是60 × 计数时钟 / (tick 数 × 每转脉冲数)每转脉冲数要算上编码器的倍频倍数这一点在编码器那一章有专门讲算错了转速会整体偏四倍。3. 四类电机四条链路从 H 桥到六步换相怎么落地3.1 直流有刷电机H 桥、方向脚与占空比有刷电机的驱动最简单两个半桥组成 H 桥四个开关管对角导通决定转向PWM 加在其中一个管上决定转速。教程里给了完整的原理图分析、驱动芯片选型和软件实现核心就三件事方向脚、使能脚、PWM 占空比。刹车有两种能耗制动是两端同时拉高让电机绕组短路消耗动能反接制动是瞬间加反向电压后者电流冲得很猛一般只在不心疼管子的场合用。typedef enum { MOTOR_STOP 0, MOTOR_FORWARD, MOTOR_REVERSE, MOTOR_BRAKE } motor_dir_t; /* 单路 H 桥两路方向脚 一路 PWM切换方向前必须先停 */ void Motor_SetDir(motor_dir_t dir, uint16_t duty_permille) { uint16_t ccr (uint32_t)duty_permille * (TIM1-ARR 1U) / 1000U; switch (dir) { case MOTOR_FORWARD: /* IN11 IN20PWM 走使能端 */ GPIO_SetBits(GPIOE, GPIO_Pin_9); GPIO_ResetBits(GPIOE, GPIO_Pin_11); TIM_SetCompare1(TIM1, ccr); break; case MOTOR_REVERSE: GPIO_ResetBits(GPIOE, GPIO_Pin_9); GPIO_SetBits(GPIOE, GPIO_Pin_11); TIM_SetCompare1(TIM1, ccr); break; case MOTOR_BRAKE: /* 两端同高绕组短路能耗制动 */ GPIO_SetBits(GPIOE, GPIO_Pin_9); GPIO_SetBits(GPIOE, GPIO_Pin_11); TIM_SetCompare1(TIM1, 0); break; default: /* 停止两端同低绕组悬空 */ GPIO_ResetBits(GPIOE, GPIO_Pin_9); GPIO_ResetBits(GPIOE, GPIO_Pin_11); TIM_SetCompare1(TIM1, 0); break; } }函数里最要紧的是调用方的顺序换向前必须先把占空比降到 0等电机电流衰减下来再切方向脚最后重新给占空比。直接在满占空比下翻转方向脚等于把电源反向接到还在旋转的电机上反电动势叠加上电源电压瞬时电流能到额定值的五倍以上续流二极管和 MOS 管一起遭殃。我的做法是封一个Motor_RampTo(dir, target, ms)用每 10 ms 调一档的方式做斜坡顺带把机械冲击也消掉了。3.2 舵机50 Hz 脉冲与角度线性映射舵机内部自带控制板外面只需要喂一路 50 Hz 的 PWM脉宽 0.5 ms 到 2.5 ms 对应 0 到 180 度。难点不在配置在两点一是有些型号的行程是 1.0 ms 到 2.0 ms直接套 0.5~2.5 会顶到机械限位齿轮发出咔咔声二是上电瞬间如果定时器还没配好引脚处于浮空状态舵机会因为收到乱码脉冲而猛地甩到一端。/* 舵机50Hz 周期0.5ms~2.5ms 映射到 0~180 度 */ #define SERVO_MIN_US 500U #define SERVO_MAX_US 2500U void Servo_SetAngle(uint8_t angle_deg) { uint32_t us; if (angle_deg 180U) angle_deg 180U; /* 线性映射先乘后除避免整数截断把首尾两度吃掉 */ us SERVO_MIN_US (uint32_t)angle_deg * (SERVO_MAX_US - SERVO_MIN_US) / 180U; /* 计数时钟 1MHz 时1 tick 恰好 1usCCR 直接填微秒数 */ TIM_SetCompare2(TIM1, us); }TIM_SetCompare2的入参是 CCR 值前提是定时器的计数时钟已经调成 1 MHz、ARR 调成 19999。上电初始化时先把 CCR 设成中位值1.5 ms对应 90 度再使能定时器输出舵机就会从中间位置开始不会甩。想让舵机动得柔和可以每 20 ms 加一度做插值直接给目标角度舵机会以最大速度冲过去云台类应用里晃得厉害。3.3 步进电机细分、脉冲频率与走多少步步进电机的控制逻辑跟上面两类完全不同它不需要闭环给一个脉冲走一个固定角度。所以软件里最重要的不是占空比是两个量——脉冲频率决定转速和脉冲个数决定走多远。教程里用了独立的细分驱动器细分拨码决定一圈需要多少个脉冲常见 1.8 度电机整步 200 步一圈16 细分就是 3200 步一圈。算角度时先把这个数定死后面所有换算都从它出发。细分设置每圈脉冲数单个脉冲对应角度1 kHz 脉冲频率下的转速整步2001.8°300 r/min4 细分8000.45°75 r/min8 细分16000.225°37.5 r/min16 细分32000.1125°18.75 r/min32 细分64000.05625°9.375 r/min/* 步进电机定时器发方波脉冲计数走完即停 */ static volatile int32_t s_pulse_left 0; void Stepper_MoveSteps(int32_t steps, uint16_t freq_hz) { /* 前提TIM2 计数时钟已设为 1MHz则 f 1MHz / (ARR1) */ uint32_t arr 1000000U / freq_hz - 1U; TIM_SetAutoreload(TIM2, arr); TIM_SetCompare1(TIM2, arr / 2U); /* 50% 方波驱动器识别最稳 */ /* 方向脚必须在没有脉冲输出时切换否则会丢步或反转 */ if (steps 0) GPIO_ResetBits(GPIOD, GPIO_Pin_3); else GPIO_SetBits(GPIOD, GPIO_Pin_3); s_pulse_left (steps 0) ? -steps : steps; TIM_Cmd(TIM2, ENABLE); } void TIM2_IRQHandler(void) /* 更新中断一个完整周期算一步 */ { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { if (s_pulse_left 0 --s_pulse_left 0) { TIM_Cmd(TIM2, DISABLE); /* 发完立刻停防止多走一步 */ } TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }高频启动是步进电机的经典坑。直接给 5 kHz 脉冲频率转子跟不上旋转磁场原地嗡鸣不转。正确做法是加减速曲线从 500 Hz 起每 2 ms 提一档到目标频率再匀速快到位时反过来降。另外arr很小时比如 20 kHz 以上中断频率会高到把 CPU 吃掉用中断计步要盯一下中断里的执行时间。3.4 无刷直流霍尔状态与六步换相表无刷直流的复杂度跳了一档因为它没有电刷换相得由软件根据转子位置来做。霍尔传感器每 60 电角度给出一个三位状态码六个状态对应六种通电组合这张映射表就是六步换相的核心。教程里把换相表、霍尔状态码、PWM 调制方式都列了出来实现上是在霍尔中断里查表把对应的三个半桥上下管打开或关断。/* 三相六步换相每项是三个半桥的开关状态bit 顺序 AH AL BH BL CH CL */ static const uint8_t s_bldc_table[6] { 0x09, /* AH BL- A 相上桥、B 相下桥导通 */ 0x21, /* AH CL- */ 0x24, /* BH CL- */ 0x12, /* BH AL- */ 0x06, /* CH AL- */ 0x18, /* CH BL- */ }; /* 霍尔状态到换相序号的映射必须按实机实测校准 */ static const uint8_t s_hall_to_step[8] { 0, 1, 3, 2, 5, 4, 6, 0 }; void BLDC_Commutate(uint8_t hall_state) { uint8_t step s_hall_to_step[hall_state 0x07]; if (step 0 || step 6) { /* 非法霍尔状态立刻关断所有输出 */ TIM_CtrlPWMOutputs(TIM1, DISABLE); return; } /* 真正调制的是当前导通相的上桥其余两相一个常通一个常闭 */ BLDC_ApplyState(s_bldc_table[step - 1]); }s_hall_to_step这张表不能照抄。霍尔元件装的位置、相线接法、电机是 120 度还是 60 度排列都会让实际顺序不一样。可靠的做法是手动低速转一圈把六个霍尔状态按出现顺序记下来再对照换相表逐个试。顺序错了的表现很典型电机能转但电流特别大、发热快、带不动负载。这也是后面避坑清单里要展开的一条。4. 采样与保护电流、母线电压、温度三路怎么采才不飘4.1 三路采样电路的量程换算电流采样用串在回路里的小阻值采样电阻配合运放放大后送 ADC母线电压用两个电阻分压温度用 NTC 或者内置温度传感器。这三路都要先把ADC 码值 → 物理量的换算关系在纸上写清楚再写代码。跳过这一步直接调 PID 的大有人在结果是 PID 里用的反馈值单位不明参数整定全靠瞎试。通道采样方式换算关系示例12 位 ADC 分辨率电流10 mΩ 采样电阻 20 倍运放I(mA) (adc × 3300 / 4095) / 20 / 0.01约 0.4 mA母线电压100 kΩ / 4.7 kΩ 分压V(mV) (adc × 3300 / 4095) × 22.3约 18 mV温度NTC 10 kΩ 上拉查表或 B 值公式反算取决于工作区间要注意 ADC 参考电压用的到底是 3.3 V 还是外部基准教程里默认板载的 3.3 V实际误差可能有 ±2%用它算出来的电流值天生带系统偏差。做保护够用要做精确计量就得换基准或加校准系数。我的习惯是先在固件里留一个CAL_CURRENT_GAIN常量出厂时用钳形表实测一次电流把比值填进去后面所有换算都乘这个系数。4.2 ADC 扫描 DMA 循环采样的配置三路采样千万别用软件触发、逐个读的方式那样每路的采样时刻不同步算出来的电流已经落后于 PWM 一个周期做限流保护会慢半拍。正确做法是开扫描模式配 DMA让 ADC 自动一轮一轮地刷进缓冲区主循环或定时中断里直接读数组读到的一定是最新的一组。/* ADC1 双通道扫描 DMA2 循环搬运缓冲区自动刷新 */ __IO uint16_t g_adc_buf[2] {0}; /* [0]电流 [1]母线电压 */ void Motor_ADC_Init(void) { ADC_InitTypeDef adc; ADC_CommonInitTypeDef cmon; DMA_InitTypeDef dma; RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); cmon.ADC_Mode ADC_Mode_Independent; cmon.ADC_Prescaler ADC_Prescaler_Div4; /* 84MHz/4 21MHz别超 36MHz */ cmon.ADC_DMAAccessMode ADC_DMAAccessMode_Disabled; cmon.ADC_TwoSamplingDelay ADC_TwoSamplingDelay_5Cycles; ADC_CommonInit(cmon); adc.ADC_Resolution ADC_Resolution_12b; adc.ADC_ScanConvMode ENABLE; /* 多通道扫描 */ adc.ADC_ContinuousConvMode ENABLE; /* 连续转换不需要反复触发 */ adc.ADC_ExternalTrigConvEdge ADC_ExternalTrigConvEdge_None; adc.ADC_DataAlign ADC_DataAlign_Right; adc.ADC_NbrOfConversion 2; ADC_Init(ADC1, adc); /* 采样时间 84 周期源阻抗大的分压电路必须给足采样时间 */ ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_84Cycles); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_84Cycles); dma.DMA_Channel DMA_Channel_0; dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; dma.DMA_Memory0BaseAddr (uint32_t)g_adc_buf; dma.DMA_DIR DMA_DIR_PeripheralToMemory; dma.DMA_BufferSize 2; dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma.DMA_MemoryInc DMA_MemoryInc_Enable; dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; /* 循环模式转完自动重来 */ dma.DMA_Priority DMA_Priority_High; DMA_Init(DMA2_Stream0, dma); DMA_Cmd(DMA2_Stream0, ENABLE); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE); ADC_SoftwareStartConv(ADC1); /* 启动一次之后自动循环 */ }三个参数值得单独盯ADC_Prescaler不能超过手册给的上限超了采样值会整体偏低且不稳定ADC_SampleTime跟分压电阻的阻值直接相关100 kΩ 的分压配最短采样时间读出来永远偏小因为采样电容还没充到电压就关门了DMA_Mode_Circular必须开否则搬完一轮就停缓冲区冻在第一次的值上看起来一切正常其实反馈早就死了。4.3 限流、过压、欠压双阈值与迟滞保护逻辑本身不复杂难的是阈值附近别抖。单一阈值的写法电流在阈值上下浮动时会反复进入和退出保护PWM 一开一关电机发出哒哒声管子也热。教程里的处理方式是给每个保护项配两个阈值超过上限立刻动作跌回一个明显更低的值才恢复中间这段就是迟滞带。#define FAULT_OC 0x01 #define FAULT_OV 0x02 #define FAULT_UV 0x04 typedef struct { int32_t oc_ma, oc_exit_ma; /* 限流动作值 / 恢复值 */ uint16_t ov_mv, ov_exit_mv; /* 过压动作值 / 恢复值 */ uint16_t uv_mv, uv_exit_mv; /* 欠压动作值 / 恢复值 */ uint8_t fault; } motor_prot_t; void Motor_Protect_Update(motor_prot_t *p, int32_t cur_ma, uint16_t bus_mv) { /* 电流超限立刻把 CCR 清零比等主循环处理快得多 */ if (cur_ma p-oc_ma) { p-fault | FAULT_OC; TIM_SetCompare1(TIM1, 0); } else if (cur_ma p-oc_exit_ma) { p-fault ~FAULT_OC; } if (bus_mv p-ov_mv) p-fault | FAULT_OV; else if (bus_mv p-ov_exit_mv) p-fault ~FAULT_OV; if (bus_mv p-uv_mv) p-fault | FAULT_UV; else if (bus_mv p-uv_exit_mv) p-fault ~FAULT_UV; }调用位置比函数本身更重要。这套判断要放在 1 ms 的定时中断里或者干脆放到 ADC 转换完成中断里别塞进主循环。主循环里如果有 LCD 刷新、串口打印这类耗时操作保护响应会被拖到几十毫秒电机堵转时足够把管子烧掉。恢复值我一般取动作值的 80%电流环和电压环都适用带载启动的场景要把欠压恢复值再往下调一点否则大电流一冲、母线电压瞬间跌一下就误判成欠压停机。5. 避坑清单调电机时最容易翻车的五个地方5.1 硬件与驱动级的三次翻车现象一上电第一次给 PWM电源立刻限流MOS 管发烫甚至炸掉。原因互补通道的极性配反了或者死区时间给成了 0。极性配反时 CH1 和 CH1N 会短暂同时为高上下管一起导通电源直接对地短路。另一种情况是初始化顺序不对先使能了 PWM 输出再配 BDTR中间有几十微秒的两路全开窗口。解决上电前先用示波器双通道夹住上下管栅极确认初始状态两路都是低确认TIM_OCNPolarity与硬件驱动电路的本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑