news 2026/10/26 6:43:18

STM32F103 CAN总线无刷电机控制:协议、换相与联调实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32F103 CAN总线无刷电机控制:协议、换相与联调实践

简介:面向学习STM32F103与无刷电机控制的开发者整理的实战工程包,聚焦CAN总线指令下发与电机驱动联调,适合已掌握单片机基础、希望打通通信与执行环节的中级学习者参考。压缩包共202个文件,体积约577KB,以h头文件与c源文件为主体,另含Keil工程配置、编译中间文件、hex烧录文件及少量脚本与文档,覆盖CAN外设、定时器PWM、GPIO等底层驱动。配套笔记梳理CAN基础、F103外设配置与六步换相、磁场定向控制思路,源码按电机控制、CAN收发和中断处理分模块组织。已有661人学习。内容围绕CAN报文收发、滤波器与波特率配置、三相逆变桥换相及霍尔位置判断展开,可据此理解命令帧如何映射为转速、方向等控制量,并参考程序分层与中断处理思路排查通信异常,适合作为课程设计、竞赛小车或自制电机的起步模板。

1. 从一根 CAN 线开始:F103 加无刷电机到底能跑出什么

手头一块 STM32F103 最小系统板、一个 CAN 收发器、一台无刷电机,这套组合能干的事比不少人想的多。上位机把命令帧丢到总线上,F103 收下来解析,转成六路 PWM 和换相时序,电机就按给定转速转起来,实际转速、母线电流、故障码再打包回传。它适合两类人:一类是做小车底盘、云台、机械臂关节的,需要一个能挂在 CAN 总线上、带节点地址的电机控制节点;另一类是刚学完 CAN 通信和外设驱动,想找一条从收发器、协议解析一路摸到功率级的完整链路。整套东西不依赖复杂的上位机软件,一个 CAN 分析仪加一台限流直流电源就能把链路跑通,电机堵转、丢帧这类玄学问题也能当场定位。

2. 硬件链路与工程结构:收发器、三相桥和那块 103 怎么摆

先把硬件这条线捋清楚。F103 的 CAN 外设只出逻辑电平,TX 和 RX 两根线接到收发器上,收发器再挂到 CAN_H / CAN_L 差分对上。链路跑不通,八成不是代码问题,而是终端电阻、共地或者供电顺序出的岔子。

2.1 CAN 物理层:终端电阻和位时序是两回事

CAN 总线两端各需要一个 120Ω 终端电阻,中间节点不要再并。总线上只有两个节点时,两个 120Ω 并联成 60Ω,用万用表量 CAN_H 和 CAN_L 之间应该读到 60Ω 左右;如果读到 120Ω,说明有一端电阻没接或虚焊;读到 40Ω 以下,多半是某处又并了一个。这个电阻不是可选项,缺了以后短线上也许还能通信,稍微加长线缆就开始随机报错误帧,属于典型的前期省事后期还债。

位时序按 500 kbps 配。F103 的 CAN 挂在 APB1 上,常见时钟是 36 MHz,分频系数取 6 得到 6 MHz 的 tq 时钟,一个位由 1 个同步段加 BS1 加 BS2 共 12 个 tq 组成,6 MHz / 12 = 500 kHz。BS1 取 8、BS2 取 3,采样点落在 (1+8)/12 ≈ 75%,这是工业现场比较稳的取值。采样点太靠前(比如 BS1 取 2),线一长就采到边沿上;太靠后则对时钟误差不宽容。

提示:调试阶段先把波特率降到 125 kbps 跑通,再切回 500 kbps。低速率下对终端电阻和线缆的容忍度高,能快速区分是协议问题还是物理层问题。

收发器的 RX 引脚一般需要上拉,部分型号内部自带上拉,没带的要在外部补 10kΩ 到 3.3V。TX 侧不要串太大电阻,串 22Ω~33Ω 做阻抗匹配可以,串 1kΩ 会直接把上升沿拖垮。

2.2 功率级:三相半桥、栅极驱动与电流采样

电机侧是一套三相半桥,六个 MOS 管分成三组,每组的上桥接母线正、下桥接母线负,中点接电机三相。栅极驱动用常见的半桥驱动芯片,带自举电容和死区控制;死区时间一般取 0.5~1 μs,具体看 MOS 的开关速度和栅极电阻,取值太小会直通,太大则低速时转矩脉动明显。

电流采样有两种常见做法:一种是母线串一个毫欧级采样电阻,配运放放大后送 ADC,成本低但只能测总电流;另一种是每相下桥各串一个采样电阻,能拿到三相电流做 FOC,硬件复杂一些。这套方案如果只做速度环和六步换相,母线采样就够用,做力矩控制才需要三相采样。

位置反馈看电机类型。带霍尔的电机直接读三路霍尔,换相表按 1~6 的霍尔状态查表输出;无霍尔的靠反电动势过零检测,低速启动要先用开环拖动到一定转速再切闭环。工程上先用霍尔版本把通信链路和速度环跑通,再换无感方案,能省掉大量联调时间。

供电上,逻辑 3.3V、驱动 12V、母线 24V 或 48V 三路要分开做,母线电容靠近半桥摆放。逻辑地和功率地单点连接,不要图省事在板子上大面积铺同一种地,否则 PWM 开关噪声会顺着地线爬进 ADC 和 CAN 收发器,表现为电流采样跳动、CAN 偶发错误帧,这类问题查起来相当费劲。

2.3 工程目录:把通信、控制、驱动切成三层

一份能长期维护的工程,目录大致按下面这样切,通信层不直接操作定时器,控制层不直接碰寄存器,中间靠结构体传数据:

目录 / 文件职责改动频率
app/main.c初始化顺序、1ms 调度、状态机低
app/can_proto.c/.h帧打包解包、命令分发、CRC中
app/speed_loop.c/.h斜坡给定、PI 速度环、限幅高
bsp/can_bsp.cCAN 外设、过滤器、中断低
bsp/pwm_bsp.cTIM1 三路互补 PWM、死区、刹车低
bsp/adc_bsp.c母线电流、温度、母线电压采样低
bsp/hall_bsp.c霍尔读脚、换相表、换相时刻中
cfg/motor_cfg.h极对数、最大转速、限流值、节点 ID高

节点 ID、极对数、最大转速这类参数集中放在一个头文件里,烧录前改一处就能换电机、换地址。把参数散落在各个 .c 文件里是后期最难受的事,改一个极对数要翻五六个文件,还容易漏。

3. CAN 命令协议设计:11 位 ID 怎么分、8 字节怎么塞

协议定得好,后面改需求就是加一个功能码;定得随意,加了两个功能就得推翻重来。F103 的 CAN 只支持标准帧 11 位 ID 或扩展帧 29 位,节点不多的话标准帧足够,仲裁开销也小。

3.1 帧格式:ID 放功能码和节点号,数据域放参数

11 位 ID 拆成两段:高 4 位是功能码,低 7 位是节点号,这样一个 ID 段内最多 16 种命令、127 个节点,做小车底盘或者多关节机械臂够用。数据域固定 8 字节,小端序,最后两个字节留作参数或者去掉,不塞 CRC 也可以,CAN 本身有 15 位 CRC,短帧下再叠一层意义不大;如果命令涉及安全动作(比如使能、急停),在数据域里带一个字节的校验和更稳妥。

功能码ID 示例(节点 0x05)数据域含义
0x1 使能0x085Data[0]:0 失能 / 1 使能
0x2 设转速0x085Data[0..1]:目标转速 rpm(int16);Data[2..3]:加速度 rpm/s
0x3 设电流0x085Data[0..1]:目标电流 mA(int16)
0x4 刹车0x085Data[0]:0 滑行 / 1 短接刹车
0x5 读参数0x085Data[0]:参数索引
0x6 写参数0x085Data[0]:索引;Data[1..2]:值
0x7 急停0x085无

回传帧用另一段 ID,比如0x600 | 节点号,这样上位机按 ID 就能区分上行下行,不用维护方向标志。回传数据域里放实际转速、母线电流、母线电压、温度和故障码,8 字节刚好:

/* 回传帧打包:8 字节,小端序,1ms 发一次 */ void can_send_status(uint8_t node, const motor_status_t *st) { uint8_t d[8]; d[0] = (uint8_t)(st->rpm & 0xFF); d[1] = (uint8_t)((st->rpm >> 8) & 0xFF); /* 实际转速 int16 */ d[2] = (uint8_t)(st->current_ma & 0xFF); d[3] = (uint8_t)((st->current_ma >> 8) & 0xFF); d[4] = st->vbus_100mv; /* 母线电压,单位 100mV */ d[5] = st->temp_c; /* MOS 附近温度 */ d[6] = st->fault_code; /* 过流/过压/堵转/霍尔错 */ d[7] = (uint8_t)(d[0] ^ d[1] ^ d[2] ^ d[3] ^ d[4] ^ d[5] ^ d[6]); /* 异或校验 */ can_send_std(0x600 | node, d, 8); }

这段打包函数的关键在两点。转速和电流作为有符号 16 位量,先取低字节再取高字节,接收端按同样的顺序拼回去,端序错了会得到莫名其妙的负数。第 7 字节的异或校验只覆盖前 7 字节,接收端算一遍不一致就丢弃整帧,这个校验在 CAN 自身的 CRC 之外又加了一层,主要防的是软件层拼帧错误,比如 DMA 搬运越界把数据域写花。vbus_100mv用 100mV 做单位,是让 8 位无符号量能表示 0~25.5V,48V 系统要换成 200mV 单位或者占两个字节。

3.2 过滤器:只收自己的节点号,别在中断里做判断

F103 的 CAN 有两组过滤器,每组 14 个,可以配成掩码模式或列表模式。节点号固定的话,掩码模式最省资源:低 7 位必须匹配自己的节点号,高 4 位功能码不关心,这样一条过滤器就能收下所有发往本节点的命令。

/* 过滤器 0:只接收节点号为 NODE_ID 的标准帧,功能码不限定 */ CAN_FilterTypeDef f; f.CAN_FilterIdHigh = (uint16_t)(NODE_ID << 5); /* 标准帧 ID 在寄存器里左移 5 位 */ f.CAN_FilterIdLow = 0x0000; f.CAN_FilterMaskIdHigh = (uint16_t)(0x07F << 5); /* 1 = 必须匹配,低 7 位全部比较 */ f.CAN_FilterMaskIdLow = 0x0000; f.CAN_FilterMode = CAN_FilterMode_IdMask; f.CAN_FilterScale = CAN_FilterScale_32bit; f.CAN_FilterFIFOAssignment = CAN_FILTER_FIFO0; f.CAN_FilterActivation = ENABLE; HAL_CAN_ConfigFilter(&hcan, &f);

标准帧 ID 写进过滤器寄存器时要左移 5 位,这是很多人第一次配过滤器时踩的坑:直接把 0x05 写进去,结果一帧都收不到,或者收到一堆不属于自己的帧。掩码位为 1 表示该位参与比较,为 0 表示不关心,所以0x07F << 5的实际效果是"低 7 位必须等于 NODE_ID,高 4 位随意"。如果总线上节点超过 127 个,就得改用扩展帧,29 位 ID 的空间足够,但过滤器配置和帧头长度都要跟着改。

注意:过滤器没配好时,现象往往是"中断进了但数据不对"或者"根本没中断",用回环模式(Loopback)先自测一遍能省很多时间。回环模式下发出的帧自己收,能验证过滤器和中断链路,跟外部接线无关。

3.3 命令解析:一个状态机把字节变成动作

中断里只做最少的事:把帧从 FIFO 取出来,判断功能码,写进一个全局的命令结构体,置一个标志位,然后立刻退出。真正的动作放到主循环的 1ms 调度里执行,这样即使某条命令处理慢了也不会阻塞总线接收。

typedef struct { volatile int16_t target_rpm; volatile int16_t accel_rpm_s; volatile uint8_t enable; volatile uint8_t brake; volatile uint8_t cmd_pending; /* 有新命令待处理 */ } can_cmd_t; can_cmd_t g_cmd; /* CAN RX0 中断:只解包和置位,不做控制 */ void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *h) { CAN_RxHeaderTypeDef rx; uint8_t d[8]; if (HAL_CAN_GetRxMessage(h, CAN_FILTER_FIFO0, &rx, d) != HAL_OK) return; if (rx.IDE != CAN_ID_STD) return; uint8_t node = rx.StdId & 0x7F; uint8_t func = (rx.StdId >> 7) & 0x0F; if (node != NODE_ID) return; /* 过滤器已挡掉,双保险 */ switch (func) { case 0x1: g_cmd.enable = d[0] ? 1 : 0; break; case 0x2: g_cmd.target_rpm = (int16_t)(d[0] | (d[1] << 8)); g_cmd.accel_rpm_s = (int16_t)(d[2] | (d[3] << 8)); break; case 0x4: g_cmd.brake = d[0] ? 1 : 0; break; case 0x7: g_cmd.enable = 0; g_cmd.target_rpm = 0; break; default: return; } g_cmd.cmd_pending = 1; }

中断里没有浮点运算、没有除法、没有等待,最坏执行时间可控。target_rpm和accel_rpm_s都声明成volatile,因为它们在中断里写、在主循环里读,不加编译器可能把它优化进寄存器导致读到旧值。cmd_pending置位后由主循环清掉,这个标志也可以换成 RTOS 的事件组,但裸机下一个字节变量足够。急停命令把目标和使能一起清掉,不依赖上位机再补一帧,这是安全动作该有的样子。

4. 核心代码拆解:从一帧命令到六路 PWM 的完整链路

命令进了结构体,接下来要变成电机轴上的转矩。这条链路是:命令 → 斜坡给定 → 速度环 → 占空比 → 换相表 → PWM 比较值。每一段都有可以调参数的地方,也都可能成为抖动和啸叫的来源。

4.1 初始化顺序:时钟、CAN、PWM、ADC 谁先谁后

上电初始化的顺序会影响故障表现。比较稳的顺序是:系统时钟 → GPIO → CAN → PWM(先关闭输出)→ ADC → 霍尔 → 使能中断 → 最后打开 PWM 输出。PWM 一定要在参数都准备好之后才使能,否则上电瞬间 MOS 可能收到一个随机占空比。

void app_init(void) { clock_init(); /* HSE 8M -> PLL 72M,APB1 36M */ gpio_init(); /* CAN_TX/RX、PWM 六路、霍尔三路、采样脚 */ can_bsp_init(); /* 500kbps,过滤器,开 RX0 中断 */ pwm_bsp_init(); /* TIM1 三路互补,死区 800ns,输出默认关闭 */ adc_bsp_init(); /* 母线电流、电压、温度,DMA 循环采样 */ hall_bsp_init(); /* 三路输入,上升沿中断或 1ms 轮询 */ motor_state = STATE_IDLE; pwm_enable(0); /* 显式关闭,等使能命令 */ }

clock_init里 APB1 必须是 36 MHz,因为 CAN 的波特率是从 APB1 分出来的,改了主频忘了改分频系数,CAN 就会以错误的速率通信,表现是分析仪上看得到帧但全是错误帧。pwm_bsp_init里把 TIM1 的 BDTR 寄存器 MOE 位先清掉,等收到使能命令再置位,这一步能让上电测试安全很多。

4.2 斜坡给定与速度环:别让目标值直接跳变

上位机发来的目标转速可能是 0 直接跳到 3000 rpm,如果直接把这个值送进 PI,输出会瞬间打满,电机猛冲一下,电流冲到限流值,电源都可能被拉跌。中间加一层斜坡,限制加速度,是最省事的办法。

/* 1ms 调用一次:斜坡 + PI + 限幅,返回 -1000~1000 的占空比 */ int16_t speed_ctrl_step(int16_t target, int16_t actual, int16_t accel_rpm_s) { static int32_t ramp = 0; static int32_t integ = 0; const int32_t step = accel_rpm_s / 1000; /* 每 ms 允许变化量 */ /* 斜坡:每个周期最多靠近目标 step,step 为 0 时按最大加速度走 */ if (ramp < target) ramp = (ramp + (step > 0 ? step : 50) > target) ? target : ramp + (step > 0 ? step : 50); else if (ramp > target) ramp = (ramp - (step > 0 ? step : 50) < target) ? target : ramp - (step > 0 ? step : 50); int32_t err = ramp - actual; integ += err * 3; /* Ki 的定点等效值,实际按电机调 */ if (integ > 8000) integ = 8000; if (integ < -8000) integ = -8000; int32_t out = err / 4 + integ / 16; /* Kp、Ki 的定点缩放 */ if (out > 1000) out = 1000; if (out < -1000) out = -1000; return (int16_t)out; }

斜坡那段代码写得有点绕,实际就是把ramp每个周期往target挪一步,步长由加速度决定,accel_rpm_s除以 1000 换成每毫秒的量。step为 0 时给一个默认的 50 rpm/ms,防止上位机忘了填加速度导致电机永远不动。积分项限幅到 ±8000,是为了防止长时间堵转把积分撑爆,松手后要很久才能退出来,表现出来就是"堵转一下之后电机疯转几秒"。Kp、Ki用整数缩放而不是浮点,是因为这段代码在 1ms 定时中断里跑,F103 没有 FPU,浮点运算会带来不确定的执行时间。

参数怎么调:先把Ki置零,只留Kp,从小到大加,直到转速阶跃响应出现轻微超调;再把Ki加进去消除静差,加到响应不再变快为止。空载和带载的临界值差很多,调参要在实际负载下做。

4.3 换相与 PWM:霍尔状态到六路占空比

带霍尔的电机,三个霍尔引脚组合出 6 个有效状态,每个状态对应一组上下桥导通。用一张表把霍尔状态映射到三相的"上桥导通 / 下桥导通 / 关闭",PWM 只加在需要调制的那个管子上,另一相常通,这就是六步换相的基本做法。

/* 霍尔状态 1~6 对应的三相输出:0=关,1=上桥常通,2=下桥常通,3=上桥PWM */ static const uint8_t hall_map[8][3] = { {0, 0, 0}, /* 0:无效 */ {3, 2, 0}, /* 1:A 上桥 PWM,B 下桥常通 */ {3, 0, 2}, /* 2:A 上桥 PWM,C 下桥常通 */ {0, 3, 2}, /* 3:B 上桥 PWM,C 下桥常通 */ {2, 3, 0}, /* 4:B 上桥 PWM,A 下桥常通 */ {2, 0, 3}, /* 5:C 上桥 PWM,A 下桥常通 */ {0, 2, 3}, /* 6:C 上桥 PWM,B 下桥常通 */ {0, 0, 0} }; /* duty: 0~1000,对应 0~100% 占空比 */ void apply_commutation(uint8_t hall, int16_t duty) { if (duty < 0) duty = 0; if (duty > 1000) duty = 1000; const uint8_t *m = hall_map[hall & 0x07]; for (int ph = 0; ph < 3; ph++) { switch (m[ph]) { case 1: pwm_set_high(ph, 1000); pwm_set_low(ph, 0); break; case 2: pwm_set_high(ph, 0); pwm_set_low(ph, 1000); break; case 3: pwm_set_high(ph, duty); pwm_set_low(ph, 0); break; default: pwm_set_high(ph, 0); pwm_set_low(ph, 0); break; } } }

这张表的方向要和电机相线接法对应。相线任意对调两根,转向就反了;霍尔线对调,换相顺序也会乱。第一次接线时先不接电机,用示波器看三路 PWM 的输出顺序是否符合 1→2→3→4→5→6 循环,接上电机后如果转一下就停或者电流很大但不转,八成是表里的顺序和实际霍尔安装角度对不上,把表里任意两行交换一下再试。

死区时间由 TIM1 的 BDTR 寄存器配置,800ns 左右是个不错的起点。死区太小会直通,一上电就炸管;太大则低速时有效占空比损失明显,电机发热。这个参数没有万能值,跟 MOS 的栅极电荷和驱动电阻直接相关。

4.4 状态回传与故障处理:把黑匣子留出来

回传帧不只是给上位机看转速,更重要的是故障码。过流、过压、欠压、堵转、霍尔异常、CAN 超时,每个故障占一位或者一个枚举值,出问题时能直接定位到哪一类。调试阶段还可以把最近一次的故障发生时的电流、转速、占空比存进一个结构体,通过"读参数"命令读出来,相当于给电机装了个黑匣子。

typedef enum { FAULT_NONE = 0, FAULT_OVERCURRENT = 1, FAULT_OVERVOLT = 2, FAULT_UNDERVOLT = 3, FAULT_STALL = 4, FAULT_HALL = 5, FAULT_CAN_TIMEOUT = 6, } fault_code_t; /* 1ms 调度里调用:故障检测与保护 */ void fault_check(void) { if (adc_current_ma > CFG_MAX_CURRENT_MA) { pwm_enable(0); g_status.fault_code = FAULT_OVERCURRENT; g_cmd.enable = 0; } if (hall_state == 0 || hall_state == 7) { g_status.fault_code = FAULT_HALL; } /* CAN 超时:1s 没收到任何有效命令,主动停机 */ if (++can_silent_ms > 1000) { g_cmd.enable = 0; g_status.fault_code = FAULT_CAN_TIMEOUT; } }

过流保护里直接关 PWM 而不是只降占空比,是因为电流环的响应可能跟不上短路级别的变化。CAN 超时保护是很多人会漏掉的一条:上位机死机或者线断了,电机还在按最后一条命令一直转,这在移动底盘上是危险工况。1 秒超时是个折中值,太短会被总线拥塞误触发,太长则失去保护意义。

5. 联调与避坑:从静默 CAN 到电机稳定转动的排查清单

代码编译通过只是开始,真正花时间的是联调。下面这条顺序是我一般会走的:先让 CAN 通,再让 PWM 出波形,最后才接电机。

5.1 分阶段联调:每一步只验证一件事

第一步,不接电机、不接功率级,只给控制板供电,用 CAN 分析仪发一帧使能命令,看板子有没有回传帧。回传帧的内容对不对先不管,能收到就说明收发器、终端电阻、过滤器、中断这条链路是通的。收不到就按 5.2 的第一条查。

第二步,用回环模式自发自收,验证软件解析逻辑。这一步把外部接线排除掉,如果回环能收到、正常模式收不到,问题一定在物理层。

第三步,接功率级但不接电机,示波器看三路 PWM 的高低侧波形,确认死区存在、互补关系正确、占空比随命令变化。用一个 10% 的固定占空比命令就能看出来。

第四步,接电机,用限流电源把电流限制在额定值的一半,先发一个很低的转速(比如 100 rpm),看电机能不能平稳起转、转向对不对、回传的实际转速是否合理。这一步如果电机抖或者不转,直接跳到 5.2 的第二条。

第五步,逐步升速、加载、测试加减速和急停,观察母线电流和温度。急停要单独测一次,确认收到命令后 PWM 立刻关闭而不是慢慢降速。

5.2 常见故障:现象、原因、解决

现象一:分析仪显示错误帧,或者一帧都收不到。先量 CAN_H 和 CAN_L 之间的电阻,正常应该是 60Ω 左右。读到 120Ω 说明只有一端有终端电阻,读到无穷大说明线没接对或者收发器没供电。电阻正常还收不到,检查收发器的 TX/RX 有没有接反,很多模块的丝印方向和数据手册不一致。再不行就用回环模式确认 MCU 侧的 CAN 外设本身是好的。

现象二:电机通电后抖动、转两下就停,电流明显偏大。这是换相顺序和霍尔状态对不上,最常见的原因是相线接错或者霍尔线序错。解决办法是把三相线中任意两根对调看转向是否改变,如果不转只抖,把换相表里相邻两行交换再试。还有一种情况是死区时间不够导致上下桥瞬间直通,用示波器看互补的两路 PWM 是否有重叠区,有重叠就把死区加大到 1.2μs 再试。

现象三:上电瞬间炸 MOS。优先查三件事:PWM 初始化时输出有没有默认关闭、自举电容有没有先充电、母线电容是不是离半桥太远。初始化顺序里如果先开 PWM 再配参数,上电瞬间可能输出一个满占空比。自举电容没充电就开上桥,上管会处于线性区发热。另外限流电源在调试阶段一定要用,把电流限制在 2A 以内,炸管概率会低很多。

现象四:偶发丢帧,电机偶尔不响应,但大部分时间正常。先看总线负载,1ms 发一帧回传、命令帧不密集的话负载很低,不至于丢帧。重点查地和终端电阻:节点之间地电位差超过 1V 就可能干扰差分信号,长距离走线要加共模电感或者改用隔离收发器。软件侧检查接收 FIFO 有没有溢出,中断里耗时太长会导致后续帧被覆盖,把中断里的浮点运算和打印全删掉。

现象五:转速上不去,或者一到某个转速就开始啸叫、振荡。这是速度环参数或者 PWM 频率的问题。PWM 频率一般取 16~20 kHz,低于 10 kHz 电机会有明显啸叫,高于 25 kHz 开关损耗又上去了。速度环振荡就把 Kp 减半再试,如果减了还振,检查斜坡给定是不是没生效导致目标值阶跃。还有一种隐蔽情况是霍尔信号有毛刺,换相时刻被误触发,加一个 1μs 的 RC 滤波或者软件消抖就能解决。

6. 二次开发:改协议、加闭环、做多机同步

协议和代码结构理顺之后,改动就有了着力点。最常见的三个方向是:把速度环换成电流环或位置环、把单节点扩成多节点同步、把参数烧死在 Flash 里做在线整定。前两个改控制层,第三个改通信层,互不干扰。

加电流环的话,在速度环外面套一层,速度环输出作为电流环给定,电流环以 10 kHz 跑在定时器中断里,母线采样电阻的 ADC 值先做一个滑动平均滤波。速度环保持 1ms 周期,两个环的周期差一个数量级,参数不会互相打架。位置环再往外套一层,周期可以放到 5ms,用编码器或者霍尔计数做反馈。改完一定要用阶跃响应验证:给一个位置阶跃,看超调量和稳定时间,超调超过 20% 就把外环 Kp 往下调。

多节点同步的关键是让命令帧被所有节点在同一时刻生效。常见做法是广播一帧带时间戳的同步命令,各节点收到后把动作挂到一个基于自身定时器的延时队列,在约定的时刻统一执行。F103 的定时器精度足够做毫秒级同步,但要注意各节点的晶振误差会累积,长时间运行需要周期性重新对齐。同步帧的 ID 用一个所有节点都收的广播地址,过滤器那边把掩码放开一点即可。

参数在线整定建议单独划一段 Flash 扇区,写参数走"命令 → 校验 → 擦写 → 回读确认"四步,写完回读不一致就回一个错误码。Flash 擦写期间要关中断,否则 CAN 接收会丢帧,这一步的时序必须在文档里写清楚,不然以后自己都会忘。

我之前有个项目,改完换相表直接烧录上电,电机猛地反转撞到限位,把联轴器顶弯了。从那以后我每次改动换相表或者相线接法,都强制先用限流电源、低占空比、电机悬空跑一遍,确认转向和霍尔顺序都对,再装回机构上。这个习惯看着笨,但省下来的返工时间远超那几分钟。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 6:43:17

Python六边形世界地图生成:fBm噪声与生物群系

简介&#xff1a;这是一份基于 Python 的六角形世界地图生成器源码包&#xff0c;适合独立游戏开发者、跑团主持人及对程序化地图生成感兴趣的学习者&#xff0c;用于快速生成随机行星表面&#xff0c;并将地图切分为任意大小的“领土”&#xff0c;可作为浏览器游戏或桌游场景…

作者头像 李华
网站建设 2026/10/26 6:43:03

C++类内声明与类外定义:从编译模型到工程实践

1. 类内声明到底是什么&#xff1a;先把概念掰开揉碎“类内声明”这四个字&#xff0c;我在带新人的时候被问过不下十次。刚上手写 C 的朋友经常一脸困惑地指着代码问&#xff1a;这行写在 class 大括号里的void draw();后面跟个分号&#xff0c;到底是啥&#xff1f;它和下面那…

作者头像 李华
网站建设 2026/10/26 6:43:03

STM32F103+CAN无刷电机控制链路:协议、PWM映射与调试避坑

简介&#xff1a;面向STM32F103与无刷电机控制学习者的CAN通信命令控制实践包&#xff0c;适合已具备单片机基础、希望打通通信协议与电机驱动环节的开发者。内容围绕CAN报文帧结构、标识符与数据字段配置&#xff0c;结合STM32内置CAN控制器的收发编程&#xff0c;并延伸到三相…

作者头像 李华
网站建设 2026/10/26 6:41:55

DeepSeek Harness 插件架构解析:Cordis 上下文、服务与依赖注入实战

1. 从一次插件加载失败说起&#xff1a;Cordis 架构到底解决了什么问题我第一次接触 DeepSeek Harness 的插件体系&#xff0c;是因为一个很具体的需求&#xff1a;团队内部有一个自研的代码审查流程&#xff0c;想在 Harness 每次生成补丁之后自动跑一遍静态检查&#xff0c;再…

作者头像 李华
网站建设 2026/10/26 6:41:51

校园生活服务系统全栈开发:PHP+UniApp实战解析

1. 项目概述&#xff1a;校园生活服务系统的全栈开发实践这个基于微信小程序的校园生活服务系统&#xff0c;是我去年为某高校开发的一个实战项目。系统采用PHPUniApp的全栈架构&#xff0c;整合了二手交易、自习室预约、会议室管理和失物招领四大核心功能模块。在实际运行中&a…

作者头像 李华
网站建设 2026/10/26 6:41:37

C++工业视觉模板匹配:度量选型、NMS与性能优化实战

工业视觉项目里&#xff0c;模板匹配大概是被调用次数最多、也最容易被低估的一个算子。产线上相机一拍&#xff0c;你要知道料框里那个零件在什么位置、角度偏了多少&#xff0c;第一步往往就是把事先截好的一小块模板拿去大图里做一次全局搜索——这个动作就是模板匹配。用C来…

作者头像 李华