简介:一套基于STM32F103的无刷电机CAN通信控制工程,面向嵌入式开发者与电机控制初学者,解决通过CAN总线远程下达转速、方向等命令并获取电机状态的问题。压缩包共202个文件,约577KB,以h/c源文件为主,兼顾Keil工程配置、链接脚本、调试信息与说明文档,可对照工程了解CAN模块波特率与滤波器配置、六步换相和FOC控制策略。已有661人学习下载,内容从CAN报文帧结构、STM32F103内置CAN控制器初始化,到霍尔信号采集、PWM驱动三相逆变器均有覆盖,并附加开发者学习笔记,便于理解模块化程序结构与排错思路。在此基础上修改代码即可快速验证电机控制效果,适合需要CAN通信与BLDC实战的人群。
1. 一套能跑通全链路的方案,比任何零散代码都省时间
做过无刷电机调试的人都有过这种经历:电机转了,但转速不受控;PWM输出正常,但CAN报文发过去没反应;霍尔换相一抖一抖的,电流大得吓人。这套基于STM32F103 + CAN通信命令控制的无刷电机资源,解决的就是从“电机能转”到“上位机发一条转速命令电机就稳定转起来”的最后一公里问题。它不只是一份代码,而是一个完整的控制链路设计:CAN报文协议、F103的bxCAN外设配置、六步换相PWM输出、霍尔信号处理、PID转速闭环和主机端命令解析。适合正在做AGV小车底盘、小型机械臂关节、无刷风机或实验室调速台的开发者参考。反直觉的是,在这个场景里CAN总线最大的价值不是速度,而是多节点确定性通信和故障隔离能力。
2. 系统架构与协议设计:从拓扑、帧结构到F103时序预算
2.1 为什么选CAN而不是串口,F103又是如何被选中的
无刷电机控制链路里,主控和驱动器之间的通信方式直接影响整个系统的实时边界。串口UART虽然简单,但天生是点对点,主机要同时控制四五个独立电机轴时,要么用USB转多串口芯片,要么用RS485总线。RS485在半双工模式下需要控制收发方向,波特率越高,方向切换的时序问题越明显。CAN总线是真正的多主多从总线,报文带ID优先级仲裁,出现碰撞时不丢数据只是低优先级退让,而且出错后硬件自动重发或恢复总线。CAN的差分双线结构在电机这种强干扰环境下比单端信号可靠得多。
STM32F103被选中不是因为它算力最强,而是因为F103的bxCAN控制器和高级定时器TIM1的搭配刚好覆盖这个需求。F103的主频72MHz,Flash从64KB到512KB可选,针对典型的小型无刷电机控制场景(500W以下、额定转速3000rpm左右)完全够用。关键是F103内部集成了CAN控制器,无需外挂SPI接口的独立CAN收发芯片,搭配一个TJA1050或SN65HVD230收发器就能上总线。而TIM1是带互补输出和刹车功能的16位高级定时器,可以直接生成六路带死区的PWM信号驱动三相全桥,这是F103相对F0和G0系列最顺手的地方。
2.2 三段式命令帧结构:ID分配、DLC定义与数据段布局
先梳理清楚这套方案里“命令控制”到底是什么粒度。不是PWM占空比直接下发——那样主机还要关心电机磁场角度,而是把F103当作电机驱动器节点,主机只发抽象转速命令,由F103完成换相、调速和状态上报。这种主从架构的好处是主机不用知道底层换相细节,新增电机轴时完全不用改主机逻辑。
建议的帧划分方式如下:标准帧(11位ID)分成两个区间,0x101到0x110是下行命令区,0x181到0x190是上行状态区。每个ID对应一个轴编号,例如0x101是轴1转速设定命令,0x181是轴1状态回报。数据段固定为8字节,便于统一解析:
| 字节偏移 | 含义 | 说明 |
|---|---|---|
| 0 | 命令字 | 0x00停止、0x01转速控制、0x02位置清零、0x03读取状态 |
| 1-2 | 目标转速 | 有符号16位,单位rpm,负数表示反转 |
| 3-4 | 附加参数 | 比如加速度限幅或位置目标值 |
| 5-6 | 保留 | 置0 |
| 7 | CRC8 | 对前7字节做校验,多项式0x31 |
主机发0x01 + 目标转速值,F103收到后先做CRC校验,通过后进入转速闭环计算,每20ms回传一帧0x181状态帧,包含当前转速、母线电压、霍尔错误标识和过流状态位。这套设计后续扩展多轴时只需要增加ID映射,数据段结构完全不需要改动。
2.3 F103在500kbps总线上的时序预算到底紧不紧
很多人担心F103又要跑换相中断又要跑PID又要处理CAN收发,会不会跑不动。先算一笔账:CAN波特率设置为500kbps,一帧16字节的完整报文传输时间约180-200us。F103的CAN外设自带三个发送邮箱和两个接收FIFO,报文收发其实不需要CPU逐比特处理,而是在中断里读/写寄存器即可。以1000rpm的无刷电机为例,极对数通常是2对级,电气频率约33Hz,一个电角度周期约30ms,霍尔换相每60°电角度触发一次,大约每5ms产生一次换相中断。在每个换相中断里要做的工作是读霍尔电平状态、查换相表、更新PWM输出寄存器,耗时远小于20us。PID转速环如果放在1kHz定时器中断里,每1ms执行一次PID运算和CAN状态发送,CPU负载率约在10%以内。
所以这个方案的时序余量相当充足,真正紧迫的反而是中断优先级分配——换相中断必须设为最高优先级,其次才是CAN接收中断,PID定时器中断放最后。一旦把顺序搞反,CAN接收中断频繁打断换相过程,电机就会出现明显的顿挫感。
3. 硬件初始化落地:bxCAN外设配置、TIM1六路PWM与霍尔换相表
3.1 CAN外设初始化:滤波器、位时序与收发完整过程
F103的CAN1外设挂载在APB1总线上,APB1最高时钟为36MHz。需要特别注意这个时钟基准,因为CAN波特率计算完全围绕它展开。下面是完整的初始化代码,配置单滤波器和500kbps波特率:
void MX_CAN1_Init(void) { CAN_InitTypeDef CAN_InitStruct; CAN_FilterInitTypeDef CAN_FilterInitStruct; GPIO_InitTypeDef GPIO_InitStruct; // 使能CAN1与GPIOA时钟,CAN1默认引脚为PA11(RX)/PA12(TX) RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // PA11: CAN_RX,配置为上拉输入 GPIO_InitStruct.Pin = GPIO_Pin_11; GPIO_InitStruct.Mode = GPIO_Mode_IPU; GPIO_Init(GPIOA, &GPIO_InitStruct); // PA12: CAN_TX,配置为复用推挽输出 GPIO_InitStruct.Pin = GPIO_Pin_12; GPIO_InitStruct.Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); CAN_DeInit(CAN1); CAN_StructInit(&CAN_InitStruct); // 自动离线恢复:总线故障恢复后无需手动复位CAN CAN_InitStruct.CAN_ABOM = ENABLE; // 睡眠模式自动唤醒 CAN_InitStruct.CAN_AWUM = ENABLE; // 禁止自动重传:适合命令控制,发送失败立即上报错误 CAN_InitStruct.CAN_NART = DISABLE; // 正常模式,非回环模式 CAN_InitStruct.CAN_Mode = CAN_Mode_Normal; // 时间戳禁止 CAN_InitStruct.CAN_TTCM = DISABLE; // 接收FIFO锁定禁止 CAN_InitStruct.CAN_RFLM = DISABLE; // 发送优先级:ID小的先发送 CAN_InitStruct.CAN_TXFP = DISABLE; // 500kbps位时序:APB1=36MHz,预分频8得到4.5MHz // 位时间=1TSJ+6TS1+2TS2=9TQ,4.5MHz/9=500kHz CAN_InitStruct.CAN_SJW = CAN_SJW_1tq; CAN_InitStruct.CAN_BS1 = CAN_BS1_6tq; CAN_InitStruct.CAN_BS2 = CAN_BS2_2tq; CAN_InitStruct.CAN_Prescaler = 8; CAN_Init(CAN1, &CAN_InitStruct); // 滤波器0:只接收标准帧,屏蔽位配置为不过滤任何标准帧 CAN_FilterInitStruct.CAN_FilterNumber = 0; CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask; CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32Bit; CAN_FilterInitStruct.CAN_FilterIdHigh = 0x0000; CAN_FilterInitStruct.CAN_FilterIdLow = 0x0000; CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0x0000; CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0x0000; CAN_FilterInitStruct.CAN_FilterFIFOAssignment = CAN_FIFO0; CAN_FilterInitStruct.CAN_FilterActivation = ENABLE; CAN_FilterInit(&CAN_FilterInitStruct); // 使能CAN接收中断(FIFO0有报文时触发) CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE); }这段代码里值得关注的是CAN_BS1和CAN_BS2的取值。同样一组预分频值在不同时钟下得到的波特率完全不同,如果APB1不是36MHz而是改成了其他倍频,波特率就会漂移。CAN_ABOM置位也很重要,无刷电机启动瞬间母线电压跌落,可能导致CAN收发器进入总线关闭状态,没有自动恢复功能就必须人工复位整个CAN外设,这在现场调试时是非常常见的白屏现场。
3.2 TIM1六路PWM输出:互补模式、死区时间与刹车保护
F103的TIM1和TIM8是高级定时器,六路输出CC1/CC2/CC3加上互补通道CC1N/CC2N/CC3N刚好对应三相全桥的六个MOS管。無刷电机方波控制的要点是:任意时刻只有两相导通(120°导通方式),但上下桥臂绝对不能同时导通,否则直接炸管。死区时间就是防止一个桥臂关断还没完全截止时另一个桥臂提前导通的保险丝。
void MX_TIM1_Init_WDG(void) { TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct; TIM_OCInitTypeDef TIM_OCInitStruct; TIM_BDTRInitTypeDef TIM_BDTRInitStruct; GPIO_InitTypeDef GPIO_InitStruct; // TIM1时钟来自APB2,72MHz RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB, ENABLE); // 输出引脚:PA8=TIM1_CH1, PA9=CH2, PA10=CH3 // 互补输出:PB13=CH1N, PB14=CH2N, PB15=CH3N GPIO_InitStruct.Pin = GPIO_Pin_8 | GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStruct.Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15; GPIO_InitStruct.Mode = GPIO_Mode_AF_PP; GPIO_InitStruct.Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); // PWM频率20kHz,周期=72MHz/(72-1)/( (1000+1)-1 )约20kHz? // 此处PSC=71,ARR=99,得到72MHz/(72*100)=10kHz,要20kHz则ARR=49 TIM_TimeBaseStruct.TIM_Prescaler = 71; // 72MHz / 72 = 1MHz TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseStruct.TIM_Period = 49; // 1MHz / 50 = 20kHz TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStruct); // PWM模式1,输出极性高电平有效 TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1; TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable; TIM_OCInitStruct.TIM_OutputNState = TIM_OutputNState_Enable; TIM_OCInitStruct.TIM_Pulse = 0; TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High; TIM_OCInitStruct.TIM_OCNPolarity = TIM_OCNPolarity_High; TIM_OCInitStruct.TIM_OCIdleState = TIM_OCIdleState_Reset; TIM_OCInitStruct.TIM_OCNIdleState = TIM_OCNIdleState_Reset; TIM_OC1Init(TIM1, &TIM_OCInitStruct); TIM_OC2Init(TIM1, &TIM_OCInitStruct); TIM_OC3Init(TIM1, &TIM_OCInitStruct); // 死区时间设为2us,换算关系见参考手册BDTR寄存器DTG段 TIM_BDTRInitStruct.TIM_OSSRState = TIM_OSSRState_Disable; TIM_BDTRInitStruct.TIM_OSSIState = TIM_OSSIState_Enable; TIM_BDTRInitStruct.TIM_LOCKLevel = TIM_LOCKLevel_1; TIM_BDTRInitStruct.TIM_DeadTime = 200; // 按寄存器步进计算,实际死区约2us TIM_BDTRInitStruct.TIM_Break = TIM_Break_Enable; TIM_BDTRInitStruct.TIM_BreakPolarity = TIM_BreakPolarity_High; TIM_BDTRInitStruct.TIM_AutomaticOutput = TIM_AutomaticOutput_Disable; TIM_BDTRConfig(TIM1, &TIM_BDTRInitStruct); TIM_OC1PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC2PreloadConfig(TIM1, TIM_OCPreload_Enable); TIM_OC3PreloadConfig(TIM1, TIM_OCPreload_Enable); // 高级定时器必须使能主输出,否则六路PWM全部无输出 TIM_CtrlPWMOutputs(TIM1, ENABLE); TIM_Cmd(TIM1, ENABLE); }3.3 霍尔传感器输入捕获与六步换相逻辑
无刷电机带霍尔传感器时,三个霍尔信号以120°或60°间隔分布,每个电机机械周期内产生6组不同的霍尔编码组合。F103采集霍尔信号最直接的方式是接三路外部中断(EXTI),每个霍尔信号跳变都触发一次中断,在中断里读取全部三个霍尔引脚的电平状态并执行换相。需要避免的写法是轮询方式去读霍尔电平,那样换相时刻完全不可控,电机运行噪音大且效率低。
// 霍尔引脚:PB6霍尔A,PB7霍尔B,PB8霍尔C,全部配置为上拉输入 void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { uint8_t hall_code; const uint8_t comm_table[8] = { 0x00, // 000 非法状态 0x01, // 001 导通管组合 0x02, // 010 0x03, // 011 0x04, // 100 0x05, // 101 0x06, // 110 0x00 // 111 非法状态 }; // 读取最新霍尔编码,执行换相 hall_code = (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_6) << 0) | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_7) << 1) | (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_8) << 2); comm_set_output(comm_table[hall_code]); }实际工程里directly mapping霍尔编码到输出管的表需要根据电机线序调整,不同厂家的霍尔传感器相位与电机相线的关系并不统一。一个拉伸调试点是把电动机最大反向电动势波形用示波器抓出来,再与霍尔波形对比,确定正确的换相表项。如果霍尔表某一项配错,表现通常是某个换相点电流尖峰特别大,甚至电机在那个扇区附近卡死。
4. 命令解析与控制逻辑:转速闭环代码、CRC校验与状态回传
4.1 命令状态机:超时保护、错误帧过滤与主循环设计
F103的CAN接收中断收到报文后,不直接在下文解析。更稳妥的做法是把CAN ID、DLC和八个数据字节整体搬进一个环形缓冲区,主循环再从缓冲区取帧解析,避免在中断里做耗时的CRC计算和PID运算。
typedef struct { uint32_t id; uint8_t data[8]; uint8_t dlc; } CanPacket_t; volatile CanPacket_t rx_queue[16]; volatile uint8_t rx_head = 0; volatile uint8_t rx_tail = 0; // CAN接收中断服务函数 void USB_LP_CAN1_RX0_IRQHandler(void) { CanPacket_t *pkt; if (CAN_GetITStatus(CAN1, CAN_IT_FMP0)) { if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0) { CAN_Receive(CAN1, CAN_FIFO0, &rx_msg); // 复制到环形缓冲区,只保留标准帧 if ((rx_msg.IDE == CAN_Id_Standard) && (rx_msg.DLC <= 8)) { // 注意以下代码在中断中尽量简洁,禁止printf } } CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0); } }主循环中解析到的0x01转速命令后,先做两个判断:目标转速绝对值是否超过上限,以及命令字和CRC是否匹配。CRC计算用查表法最合适,F103按位计算CRC开销太大。在接收中断中如果发现CRC错误,丢弃该帧并递增错误计数器,连续收到三帧CRC错误就进入“命令超时保护”状态,认为总线已不可信,强制电机减速停机。
4.2 PID转速闭环:增量式PID与防止积分饱和
转速环的反馈信号来自霍尔换相频率。每检测到一个霍尔扇区变化,对应的电角度推进60°,结合极对数可以换算出机械转速。更精确的方法是使用定时器输入捕获测量霍尔信号的周期,叫测频法或测周法——低转速用测周法,高转速用测频法,这里使用最简单的M法测速,即统计固定时间窗口内的霍尔跳变次数。
#define PID_KP 1.2f #define PID_KI 0.08f #define PID_KD 0.0f #define TARGET_MAX 3000 // 允许最大转速 3000rpm float pid_update(float target_rpm, float current_rpm) { static float integral = 0.0f; static float prev_error = 0.0f; float error = target_rpm - current_rpm; // 积分分离:误差过大时禁用积分,防止积分饱和拖慢响应 if (fabs(error) < 300.0f) { integral += error * 0.001f; } else { integral = 0.0f; } // 积分限幅 if (integral > 100.0f) integral = 100.0f; if (integral < -100.0f) integral = -100.0f; float output = PID_KP * error + PID_KI * integral + PID_KD * (error - prev_error); prev_error = error; // 输出限幅,PWM占空比限制在10%~90% if (output > 90.0f) output = 90.0f; if (output < 10.0f) output = 10.0f; return output; }PID三个参数的整定没什么捷径,先令KI和KD为零,KP从0.5往上加,观察转速阶跃响应是否振荡;出现等幅振荡时把KP回退到临界值的60%,再逐步加KI消除稳态误差。特别提醒的是,这里的输出限幅值10%~90%不是随意给定的,10%以下六步换相处PWM死区效应明显,90%以上MOS管导通压降增大还容易发烫。
4.3 状态回传帧的组装与发送时序
F103作为CAN从节点,状态回传尽量做到周期性、低冗余。每20ms发送一帧状态帧就足够让主机监视电机健康度,把母线电压、当前转速、温度(如有NTC)和错误字打包进8字节。发送时检查发送邮箱是否可用,邮箱满时就丢弃本次发送,而不是堵在循环里等邮箱清空:
void send_status_frame(void) { CanTxMsg tx_msg; uint8_t buf[8]; tx_msg.ExtId = 0x01; // 标准帧ID tx_msg.IDE = CAN_Id_Standard; tx_msg.RTR = CAN_RTR_Data; tx_msg.DLC = 8; buf[0] = MOTOR_STATUS_RUN; buf[1] = (current_rpm >> 8) & 0xFF; buf[2] = current_rpm & 0xFF; buf[3] = bus_voltage_adc >> 8; // 母线电压采样值 buf[4] = bus_voltage_adc & 0xFF; buf[5] = fault_flags; // 位0过流 位1过温 位2霍尔故障 buf[6] = temperature_value; buf[7] = crc8_compute(buf, 7); for (int i = 0; i < 8; i++) { tx_msg.Data[i] = buf[i]; } // 检查TME发送邮箱空标志,避免阻塞 if (CAN_TransmitStatus(CAN1, 0) == CAN_TxStatus_Failed || CAN_TransmitStatus(CAN1, 1) == CAN_TxStatus_Failed) { // 发送失败时直接返回,等待下一周期 return; } CAN_Transmit(CAN1, &tx_msg); }一个容易忽略的细节:F103的发送邮箱只有三个,CAN_Transmit函数本身有阻塞等待选项,但在中断里调用或者高频调用时,必须使用非阻塞方式。如果总线另一端没有接收节点或者终端电阻配置错误,发送邮箱很快就会被堆满,状态帧丢失率过高,主机端就会判断从设备掉线。
5. 常见翻车点排查:CAN无声、换相抖动、电流尖峰等七个实战避坑记录
5.1 CAN报文发出去对方完全收不到
现象:逻辑分析仪接CAN_H和CAN_L测不到显性电平,或者能测到波形但对面节点一直收不到任何帧。
原因:最常见的是两个问题叠加。第一,波特率配置和总线上的其他节点不一致,一个假定500kbps,另一个却配成250kbps,双方都会报错。第二,缺少终端电阻匹配。我见过不少开发者的调试现场,两个CAN节点中间没有接120欧终端电阻,低速短距可能能跑通,一旦拉到1米以上线缆或者总线节点增多,信号反射就会导致大量ACK错误。
解决:先用CAN分析仪或带CAN的示波器抓总线波形,对比一个显性位的宽度确认波特率。再确认总线两端各并联一个120欧终端电阻,测CAN_H和CAN_L之间的直流电阻应在60欧左右(两个120欧并联)。另外检查CAN收发器的Rs引脚是否接对了模式,有的开发板默认是静音模式,只收不发。
5.2 波特率计算错导致所有报文都报错
现象:初始化代码和正常节点一样,但自己的节点一上总线,总线上的其它节点立刻进入错误状态,错误计数器飙升。
原因:STM32的bxCAN波特率计算和很多别的单片机不一样,它先把APB1时钟分频得到TQ,再由SJW、BS1、BS2三段时间加起来构成一个位时间。很多人只改了CAN_Prescaler,没有同步调整BS1/BS2,以为改预分频就能得到目标波特率。
解决:严格按照36MHz APB1的基准来计算。500kbps时,一个位周期是2us,如果选择9TQ方案,每个TQ约222ns,那么CAN_SJW=1TQ、CAN_BS1=6TQ、CAN_BS2=2TQ,预分频=36MHz/(位时间频率×9)=8。改波特率时要么保持TQ数量不变只改预分频,要么保持预分频不变只改BS段长度,不要两边同时瞎调。
5.3 换相瞬间电流尖峰大,甚至烧MOS管
现象:电机转速稍微拉高,母线电流波形的换相点附近出现非常尖锐的毛刺,伴随明显顿挫,严重时直接炸管。
原因:换相时刻上下桥臂切换没有做到“先关后开”。虽然TIM1配置了死区,但死区时间设定过短,只有几百纳秒,而MOS管实际关断延迟(含驱动芯片传输延迟)在1us左右,导致直通短路。
解决:把死区时间调到2us到3us再实测。另外检查刹车功能(TIM1的BRK输入)必须接到硬件过流保护输出上,而不是只靠代码判断。如果过流信号到TIM1的BRK引脚路径有一个RC滤波,延迟会变得不可控,最好用比较器直接输出,不经过滤波。
5.4 连续发转速命令后电机反而停转
现象:主机按10ms周期发转速命令,刚发的前几帧电机响应正常,持续几秒后电机无响应,停止转动。
原因:发送频率超过了F103的处理能力,或者命令帧在F103的接收FIFO里堆积。F103的CAN接收FIFO只有两级(FIFO0深度为3个邮箱),主机10ms发一帧,理论上完全能处理,但如果在CAN接收中断中执行了重负载操作(比如printf、浮点运算),中断服务时间超过了一个帧间隔,FIFO溢出就会丢弃后续报文。
解决:接收中断只做数据搬移,把CRC校验和命令解析全部移出中断。把CAN接收中断优先级设为2(不能是0,0要留给换相中断),保证处理时间压缩在几十微秒内。另外主机端增加响应超时机制:F103如果隔500ms没收到新的转速命令,自动进入待机状态,防止总线粘滞。
5.5 霍尔换相时电机抖动并伴随反转现象
现象:电机能转,但声音发闷,转速提不上去,偶尔还倒退一小段后再往前冲。
原因:霍尔换相表和电机实际绕组-霍尔排列不匹配。无刷电机有三种常见的霍尔排列方式(120°间隔和60°间隔),同一电机型号下霍尔传感器磁极方向可能相反,导致换相表的若干项完全反过来。某个扇区换相错误时,该扇区的电磁力方向与转子运动方向相反,就会产生抖动和反转感。
解决:先把六步换相表补全,用低速开环PWM启动,仔细测试每个霍尔扇区对应的正确输出管。标准做法是锁住转子,手动旋转电机轴,记录6个霍尔编码对应的反电动势相位,再推出正确的换相表。这个工作耗时但必须做,网上流传的换相表不一定匹配你的电机。
5.6 PWM频率设置不当导致电机啸叫
现象:电机运行时有明显的高频啸叫,声音频率约1kHz到5kHz,而不是正常的电磁噪声。
原因:PWM频率太低,进入人耳听觉范围。有人把TIM1的ARR设得过大,比如50Hz或100Hz,电机能转但啸叫严重。或PWM频率漂移到20kHz以下,叠加上电机固有频率产生可闻共振。
解决:PWM频率设置在16kHz到25kHz之间,配合霍尔换相无明显延迟。修改方式是把TIM_Prescaler调小、TIM_Period调小,例如PSC=71,ARR=49得到20kHz。示波器确认波形频率后,再用耳朵听噪声,20kHz以上基本听不到。
5.7 霍尔信号在换相边界跳变时产生毛刺,导致误换相
现象:电机高速运转时电流频繁波动,转速读数跳变,逻辑分析仪抓霍尔信号看到边界处有连续多个高-低-高的脉冲。
原因:霍尔传感器的数字输出本质上是模拟比较器结果,在磁场方向翻转点附近,输出会有一段不稳定的抖动区间。F103的EXTI中断会被这些抖动反复触发,导致换相逻辑在同一个电角度边界反复切换,换相状态机进入混乱状态。
解决:硬件上在霍尔输出和MCU输入之间加一个RC低通滤波器,时间常数约5-10us;软件上加入“信号稳定确认”逻辑,检测到霍尔边沿后延时10us再读取一次,两次读值一致才确认换相。注意这个软件滤波会导致换相时刻滞后约10us,在20kHz PWM周期内影响很小,可以接受。
6. 验证手段与一个进阶技巧:用CAN自测模式做单板调试,再加一套S曲线加减速
拿到代码后不要直接上电机,先做一次安静的通路验证。F103的bxCAN支持回环模式,把CAN_Mode改为CAN_Mode_LoopBack后,发送的报文会直接回到接收FIFO,不需要任何外部CAN节点,不需要终端电阻,把一个不接电机的F103板子单独就能验证收发通路。具体做法:初始化模式改成LoopBack,程序定时发送一帧0x101,并在接收中断中打一个翻转GPIO的调试信号,用示波器或逻辑分析仪看GPIO翻转,确认数据链路是通的。这一步把“CAN外设配置问题”和“电机控制问题”隔离开,以后电机不转时不用怀疑CAN部分。
确认CAN收发正常后再进入联动调试,建议顺序是:先跑开环PWM,锁住电机轴旋转,验证六步换相表;再让电机空载运行,整定转速环PID;最后再接CAN总线命令。整个流程下来,必要的验证工具是逻辑分析仪(抓霍尔和PWM互相同步)、带示波器通道和CAN解码(看报文时序与霍尔时序的相对关系)、以及一台能模拟主机发包的上位机。没有CAN分析仪的时候,用另一块F103开发板写几行简单代码也能当主机用——PA12发,PA11收,连两根线就够。
最后一个我认为值得加的进阶技巧是S曲线加减速,而不是直接阶跃给定转速。无刷电机的机械系统和大惯量负载对加速度变化率非常敏感,阶跃转速命令在启动瞬间产生极大冲击电流,长期运行容易加速轴承磨损。在命令解析模块中增加一阶低通滤波或真正的S曲线规划器,把阶跃命令变成约束加速度变化率的平滑曲线,实现方法是在PID闭环前加一段速度规划代码,每次更新时目标转速在上一周期基础上最多变化一个固定的步长:
#define RAMP_STEP 20 // 每次PID周期目标转速最大变化20rpm void ramp_target_rpm(float new_target) { static float current_command = 0; if (new_target > current_command) { current_command += RAMP_STEP; if (current_command > new_target) current_command = new_target; } else { current_command -= RAMP_STEP; if (current_command < new_target) current_command = new_target; } pid_setpoint = current_command; }这个简单的斜坡函数每1ms执行一次,1000rpm的转速变化需要50ms完成,对大多数应用足够平顺。从那以后我每次调试新电机节点,都强制先走一遍回环自测,确认M CU和总线无问题后再去碰电机,这个习惯帮我少烧了不少管子。整套方案里最该沉住气调的就是霍尔换相表,这一步做对了后面全是水到渠成的事,希望帮到你。
本文还有配套的精品资源,点击获取