ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32+TWAI/CAN驱动CyberGear微电机控制框架实战

ESP32+TWAI/CAN驱动CyberGear微电机控制框架实战 做机器人相关的小项目最绕不开的坑之一就是电机控制。我最近手上正好有一颗CyberGear微电机想用ESP32直接驱动它做一个关节模组结果发现网上关于“ESP32 TWAI/CAN CyberGear”整套流程的资料少得可怜要么是STM32的老代码要么是拿了现成库跑个demo真正从零开始搭一套控制框架的过程几乎没人系统写过。这篇文章就是把我从硬件接线、TWAI外设初始化、CyberGear协议封包到控制框架落地踩过的所有坑完整捋一遍。不管你是手里有CyberGear电机想让ESP32快速跑起来还是单纯想在ESP32上用CAN总线做机器人控制这篇都能给你一条可以直接抄作业的路径。我会把每一步为什么这么做、参数怎么定、代码怎么写都讲清楚少走弯路。1. 整体设计与方案选型解析1.1 为什么不用STM32偏选ESP32这颗物联网芯片很多做电机控制的老手第一反应肯定是STM32毕竟CAN外设、实时性这些东西在STM32上成熟得不能再成熟。但我在这个项目里选ESP32有自己的考虑首先CyberGear微电机本身带FOC驱动器和编码器你不需要在MCU里做高频率的电流环只需要通过CAN总线发位置、速度、力矩指令再读回状态就完事了。这种情况下MCU的实时性压力远没有想象中那么大ESP32跑1Mbps的CAN总线完全够用。其次ESP32自带WiFi和蓝牙这个优势在做机器人调试时太香了。STM32调参数还得接一个USB转CAN盒子或者焊一个串口屏而ESP32我可以直接把调试信息通过WiFi发到手机上或者开一个TCP服务端用电脑无线连上来改PID参数不用反复插拔线。CyberGear又需要24V供电线材本来就多能少一根是一根。再一个就是成本。ESP32模组便宜、开源资料多Arduino和ESP-IDF两条路都能走尤其ESP-IDF的TWAI驱动封装得已经很完善了不用像操作寄存器那样一层层去抠细节写起来舒服很多。对于做原型验证和中小型机器人项目来说这个选型性价比确实高。1.2 TWAI不是“另一种总线”它就是ESP32上的CAN控制器“TWAI”这个缩写全称是Two-Wire Automotive Interface翻译过来就是双线汽车接口听起来很陌生但本质上它就是一个符合CAN 2.0B协议的控制器。ESP32芯片内部集成了TWAI控制器支持标准帧11位ID和扩展帧29位ID最高可以跑1Mbps数据长度最多8字节。这里有个关键点很多人会搞混ESP32芯片里只有CAN协议控制器没有CAN收发器。CAN总线上的物理层信号差分电平需要外接一颗CAN收发器芯片才能实现典型的有TJA1050、SN65HVD230、MCP2562这些。也就是说ESP32引出来的TXD、RXD引脚是数字逻辑电平必须经过收发器转换成CAN_H和CAN_L两根差分线才能接到CyberGear电机的CAN接口上。注意ESP32的TWAI外设不支持CAN FDCyberGear官方通信协议用的也是标准CAN 2.0B所以这里不涉及兼容性问题。但如果你手里有CAN FD设备就得换方案了。还有一个细节ESP32-S3、ESP32-C3这些不同芯片的TWAI控制器数量和外设引脚映射不太一样项目里我用的是经典款ESP32双核那款TWAI0和TWAI1两个控制器都有本文示例代码基于ESP32经典款的TWAI0来写其他型号思路完全一样只需要调整引脚配置。1.3 选CyberGear作为执行器的三个理由CyberGear是市面上比较少见的高性价比一体式FOC微电机它把电机本体、减速箱、编码器和FOC驱动器全部集成在一个小体积里外面只留电源线和CAN总线接口。我选它的原因有三个。第一个是力矩控制和位置控制精度够用。CyberGear内置的驱动器支持位置模式、速度模式、力矩模式和混合模式底层通过FOC算法驱动电机本体的力矩波动小低速运行也足够平稳做机械臂关节或者双足机器人腿关节都合适。第二个是调试成本低。传统方案如果电机不转你得分别排查电机线、编码器线、驱动器固件、上位机通讯好多环节CyberGear只通过CAN总线通讯一根差分线就能把指令和状态全部跑完排查链路短很多。第三个是社区生态逐渐起来了。虽然官方文档写得比较理工科但网上已经有不少人用过、踩过坑协议资料和示例能七拼八凑找得到。这一点在选硬件时特别重要资料少意味着你出问题只能靠自己啃对于做项目来说时间成本还是很现实的。2. 硬件接线与TWAI外设初始化2.1 接线图与元器件清单少一个终端电阻都不行先把硬件清单列一遍都是很常见的物料ESP32开发板我用的是经典的DevKitC30 pin版本CAN收发器模块我选了SN65HVD230模块3.3V供电兼容ESP32电平120Ω终端电阻如果收发器模块上没有集成需要自己准备24V直流电源CyberGear供电若干杜邦线或者干脆自己焊一个小转接板接线逻辑很简单ESP32引脚收发器模块说明GPIO_NUM_5TXDESP32发送数据给收发器GPIO_NUM_4RXD收发器把总线数据传给ESP323.3VVCC给收发器供电GNDGND与电源共地-CAN_H接到CyberGear的CAN_H-CAN_L接到CyberGear的CAN_L这里有两个非常容易踩的坑。第一个是终端电阻CAN总线要求总线两端各接一个120Ω电阻如果总线上只有ESP32这边一个收发器加CyberGear一个节点理论上应该在两个节点上都匹配终端电阻。实际做下来CyberGear内部一般自带终端电阻所以你只需要在收发器模块这边把CAN_H和CAN_L之间跨接一个120Ω电阻。如果懒得焊很多现成的SN65HVD230模块上有跳线帽把跳线帽插上就相当于接上了终端电阻。第二个坑是共地。CAN总线是差分信号理论上抗干扰能力很强但收发器、ESP32和CyberGear的电源地必须连在一起。我一开始偷懒24V电源地和ESP32开发板的地没接结果总线一直报错报文时有时无排查了半天才发现是地电位不一致。这个低级错误大家千万别学。2.2 TWAI驱动初始化一条命令让CAN跑起来在ESP-IDF环境下TWAI驱动的初始化代码非常简洁。重点就三步装驱动、设参数、启动控制器。#include driver/twai.h #define TX_GPIO_NUM GPIO_NUM_5 #define RX_GPIO_NUM GPIO_NUM_4 void twai_can_init(void) { // 常规配置模式、引脚、时钟源等 twai_general_config_t g_config TWAI_GENERAL_CONFIG_DEFAULT( TX_GPIO_NUM, RX_GPIO_NUM, TWAI_MODE_NORMAL); // 时序配置这里直接选1Mbps twai_timing_config_t t_config TWAI_TIMING_CONFIG_1MBITS(); // 过滤器配置先全部接收验证通了再精细化 twai_filter_config_t f_config TWAI_FILTER_CONFIG_ACCEPT_ALL(); if (twai_driver_install(g_config, t_config, f_config) ESP_OK) { twai_start(); } }TWAI_GENERAL_CONFIG_DEFAULT宏里面已经帮你把重要的参数都设好了包括接受过滤器模式、总线关闭恢复等。TWAI_TIMING_CONFIG_1MBITS()是波特率配置ESP-IDF里已经内置了多种常用波特率从125Kbps到1Mbps都有。这里要注意CyberGear默认的CAN波特率是1Mbps如果电机是通过官方上位机软件改过波特率的你需要去核对一下再选择对应的时序配置宏。还有一点TWAI外设的TX和RX引脚不是固定死的可以用任意GPIO引脚。我选GPIO5和GPIO4是因为这两个引脚上没有接开发板上的Flash等外设不会对启动和烧录造成影响。如果你把TWAI映射到GPIO1或者GPIO3也就是UART0的TXD、RXD引脚那每次烧录都会出问题因为下载程序默认走串口0。2.3 回环模式自测先证明代码没问题再上总线接线接好了、代码也编译过了是不是就可以直接发指令给电机了我建议先别急先做一次回环测试。回环测试就是把TWAI控制器设置成回环模式让控制器发送的数据不经过外部收发器直接在芯片内部回到接收缓冲区。这能验证你的发送接收代码和引脚配置是否正确。void twai_can_selftest(void) { twai_general_config_t g_config TWAI_GENERAL_CONFIG_DEFAULT( TX_GPIO_NUM, RX_GPIO_NUM, TWAI_MODE_SELF_TEST); twai_timing_config_t t_config TWAI_TIMING_CONFIG_1MBITS(); twai_filter_config_t f_config TWAI_FILTER_CONFIG_ACCEPT_ALL(); twai_driver_install(g_config, t_config, f_config); twai_start(); twai_message_t tx_msg { .identifier 0x100, .data_length_code 4, }; tx_msg.data[0] 0xAA; tx_msg.data[1] 0xBB; tx_msg.data[2] 0xCC; tx_msg.data[3] 0xDD; twai_transmit(tx_msg, pdMS_TO_TICKS(100)); twai_message_t rx_msg; if (twai_receive(rx_msg, pdMS_TO_TICKS(100)) ESP_OK) { printf(回环测试通过收到 ID0x%lX 数据0x%02X 0x%02X\n, rx_msg.identifier, rx_msg.data[0], rx_msg.data[1]); } }如果回环测试能收到自己发的报文说明你配置的GPIO、波特率、驱动安装都没问题。接下来就可以把模式切换回TWAI_MODE_NORMAL接上CAN收发器模块正式连电机了。3. CyberGear控制协议与报文实现3.1 拆解CyberGear报文CAN ID和8字节数据域如何组织CyberGear微电机遵循的是标准CAN 2.0B协议帧类型是数据帧数据域最多8字节。要实现对它的控制必须搞清楚CAN ID和8字节数据分别代表什么。我手里这个固件版本使用的报文布局大致是这样的CAN ID采用29位扩展帧分三段高8位是主机ID主控端中间8位是电机ID低8位是命令字段。主机ID用于标识谁在发指令电机ID用于标识这条指令发给哪个电机命令字段则告诉电机要执行什么操作。8字节数据域第0字节是命令码或者子命令后面的字节根据具体命令存放参数比如位置目标值、速度值、PID参数等。这里必须强调一句不同固件版本的CyberGear报文格式可能略有不同你拿到电机后第一件事是去官方渠道下载对应固件的通信协议文档我下面给出的命令码和帧结构是我手头固件实际验证过的可以作为参考但具体以你手里的协议文档为准。命令码功能数据说明0x01使能电机无参数0x02失能电机无参数0x03设置运行模式数据[1]模式编号0x04位置控制数据[1..4]位置float0x05速度控制数据[1..4]速度float0x06力矩控制数据[1..4]力矩float0x07读取状态无参数0x08设置PID参数数据[1..6]KD/KP/KI比如要让ID为5的电机使能主机ID是自己定义的0x01那CAN ID就是(0x01 16) | (0x05 8) | 0x01。如果要让ID为5的电机转到90度50度对应的float数值是50.0f转成4字节后放在数据[1]到数据[4]CAN ID就是(0x01 16) | (0x05 8) | 0x04。3.2 命令封装技巧float在CAN总线里其实没有浮点数CAN总线每次只能传8字节本身就是最纯粹的字节流所以float类型不能直接往数据域里放必须拆成4个字节。网上很多代码直接用指针强转不同平台的大小端差异够你喝一壶的。我习惯写两个工具函数一收一发统一处理字节序问题。static void pack_float(uint8_t *buf, float val) { uint32_t u32; memcpy(u32, val, 4); buf[0] (u32 0) 0xFF; buf[1] (u32 8) 0xFF; buf[2] (u32 16) 0xFF; buf[3] (u32 24) 0xFF; } static float unpack_float(const uint8_t *buf) { uint32_t u32 0; u32 | ((uint32_t)buf[0]) 0; u32 | ((uint32_t)buf[1]) 8; u32 | ((uint32_t)buf[2]) 16; u32 | ((uint32_t)buf[3]) 24; float val; memcpy(val, u32, 4); return val; }这里就能看出一个坑很多人直接写memcpy(buf, float_val, 4)如果你的ESP32运行的是小端模式绝大多数ARM和RISC-V都是小端而电机的固件是小端读取数据那确实没问题。但一旦你换了主控比如之后想把框架移植到STM32上字节序一下就乱了。所以我宁可用上面的组合和shift操作把字节序在函数里固定下来后面移植都不用再纠结。再者CyberGear反馈的状态数据里也包含float型的位置、速度、电流用同一个unpack_float函数去解析即可。3.3 状态反馈解析位置、速度和电流是怎么回来的电机不是单向执行指令的哑巴设备它会持续往总线上回传状态帧。CyberGear的状态反馈帧在我这个固件版本里CAN ID高位是电机ID低位是主机ID数据域放着电机当前的位置、速度、力矩/电流等信息。反馈帧的解析逻辑一般是收到帧后先判断电机ID是不是自己关心的再根据数据域内容刷新对应电机的状态。#define MY_HOST_ID 0x01 typedef struct { float position; float velocity; float current; uint8_t online; } motor_state_t; motor_state_t motor_states[8]; void parse_motor_feedback(const twai_message_t *msg) { if (!msg-extd) return; // 扩展帧ID高8位是电机ID低8位是主机ID uint8_t motor_id (msg-identifier 16) 0xFF; uint8_t host_id (msg-identifier 0) 0xFF; if (host_id ! MY_HOST_ID) return; if (motor_id 8) return; // 数据[0]固定是状态标志数据[1..4]是位置数据[5..8]是速度 motor_states[motor_id].online 1; motor_states[motor_id].position unpack_float(msg-data[1]); motor_states[motor_id].velocity unpack_float(msg-data[5]); }这里有个经验分享电机的在线状态建议在应用层做超时判断比如连续200ms没收到某个电机的状态帧就认为它离线了。不要简单依赖一个online标志因为如果电机断电总线并不会主动发一条“我下线了”的消息你只有靠超时和状态帧的缺失去判断。这个逻辑对后面做机器人整机安全保护特别重要。4. 控制框架分层设计与代码落地4.1 框架分四层每层只干一件事代码看起来只要发指令、收状态就够了但如果你只是把CAN报文收发直接写在业务逻辑里项目一复杂就直接崩盘。我实际做下来把代码分成了四层每一层的职责都很单一。底层can_bus.c封装ESP32 TWAI驱动的初始化、发送、接收、错误处理向上层屏蔽硬件细节。协议层cybergear.c把CyberGear报文拼接、解析、命令码和状态帧的组装拆解都封装在函数里。控制层motion_ctrl.c做运动规划、PID计算等只和协议层的接口打交道。应用层app_main.c写业务逻辑比如让机械臂走一个轨迹它只需要调用控制层提供的接口。我见过很多人把FPGA、电机和主控的代码全混在一个文件里项目前期看着方便后面要换电机或者改主控的时候整个人都是崩溃的。分层最大的好处就是可以局部替换比如以后把CyberGear换成别的品牌电机只需要改协议层上层业务逻辑一行都不用动。4.2 接收任务与队列电机反馈再快也不会丢TWAI控制器接收数据是靠中断还是轮询直接在主循环里调用twai_receive阻塞等待会导致主循环卡死而如果在中断里做协议解析代码又容易出bug。ESP-IDF提供的TWAI驱动本身就是支持从FreeRTOS任务里调用的所以我用独立的任务消息队列来处理接收。#define RX_QUEUE_SIZE 16 static QueueHandle_t rx_queue; void can_receive_task(void *arg) { twai_message_t msg; while (1) { if (twai_receive(msg, pdMS_TO_TICKS(50)) ESP_OK) { xQueueSend(rx_queue, msg, 0); } } } void motor_state_task(void *arg) { twai_message_t msg; while (1) { if (xQueueReceive(rx_queue, msg, portMAX_DELAY)) { parse_motor_feedback(msg); } } }接收任务只负责把报文丢进队列解析报文的任务再异步去处理这样电机因为控制频率高而发来大量状态帧时主循环也不会被打断。而且用队列天然起到了缓冲作用即使某一帧处理不太及时也不至于丢数据。发送端也要注意twai_transmit虽然不会阻塞很久但如果总线上有错误或者连接异常它是有可能超时的。所以发送函数最好也加上超时时间别让控制逻辑死等。4.3 梯形速度规划为什么直接给目标位置会抖成帕金森CyberGear位置模式下你直接给一个目标位置电机确实会转过去但如果没有加减速规划电机就像一个猛踩油门的司机起步猛冲、到位急停轻则抖动重则过冲甚至触发驱动器的过流保护。合理的做法是给位置控制加一个梯形速度规划每一小段时间内实际位置增量不超过一个最大速度值接近目标后再线性减速。这样电机的实际运动曲线就是平滑的。void position_plan_to(motor_state_t *state, float target, float max_step) { float delta target - state-position; if (delta max_step) state-cmd_position max_step; else if (delta -max_step) state-cmd_position - max_step; else state-cmd_position target; } // 主循环里以100Hz周期调用 float step 0.5f; // 每个周期最多走0.5度 position_plan_to(motor_states[5], 90.0f, step); send_position_cmd(5, motor_states[5].cmd_position);这里max_step的取值直接关系到运动的最快速度。如果主循环是10ms周期max_step0.5f意味着电机最快50度/秒这个参数要根据你的机械负载去调负载大就调小一点运动太慢了再调大。至于更高级的S型速度规划一般用在高速高精度的工业机器人上对CyberGear这种自带FOC驱动器的微电机来说梯形规划已经能应对绝大多数场景。如果后面发现末端抖动仍明显再上低通滤波或者改S型规划也不迟。4.4 主程序调用示例从0°走到90°的完整链路把前面的内容拼起来让一个ID为5的电机从当前位置转到90度位置完整的代码逻辑是这样void app_main(void) { // 1. 初始化CAN总线 twai_can_init(); // 2. 创建接收任务 rx_queue xQueueCreate(RX_QUEUE_SIZE, sizeof(twai_message_t)); xTaskCreate(can_receive_task, can_rx, 4096, NULL, 10, NULL); xTaskCreate(motor_state_task, motor_state, 4096, NULL, 10, NULL); // 3. 等电机反馈上线 vTaskDelay(pdMS_TO_TICKS(200)); // 4. 配置运行模式并启动 cybergear_set_mode(5, MODE_POSITION); cybergear_enable(5); // 5. 发送位置指令 float target 90.0f; while (1) { position_plan_to(motor_states[5], target, 0.5f); cybergear_set_position(5, motor_states[5].cmd_position); vTaskDelay(pdMS_TO_TICKS(10)); } }这段代码里我故意做了一个细节先延时200ms再发指令是为了等电机上线让motor_states[5]里的初始位置先被反馈刷新一遍。如果在电机位置未知的情况下直接发绝对位置指令电机先猛冲到85度然后才返回90度你会吓一跳的。更稳健的做法是上电后先让电机回到零位或者记录当前编码器位置再开始规划。5. 常见问题与排查技巧实录5.1 接上总线没反应先查这三处如果你的程序烧进去电机一点动静都没有我个人排查顺序是这样的。先查收发器供电和接线。CAN收发器模块的VCC是不是3.3VGND是不是和ESP32共地TX和RX有没有接反。我第一次用TJA1050时就是没注意模块是5V供电接到3.3V上芯片根本不工作看起来像是代码问题实际上硬件就没通电。再查终端电阻。特别是当你手头没有CAN分析仪只有一片收发器和电机在总线上时终端电阻缺失会导致信号反射轻则偶发错误帧重则完全不通信。不少SN65HVD230模块上默认跳线帽是拔掉的你得自己插上或者外接120Ω电阻。最后查波特率。CYberGear默认1Mbps如果你的TWAI时序配置错了理论上总线也能“通”但报文全是错误帧。用逻辑分析仪抓一下波形看看位时间是不是1微秒能立刻定位问题。5.2 电机能转但抖得厉害多半是单位不对位置模式下电机出现了持续的微弱抖动或者到达目标后来回振荡最常见的原因是单位不匹配。CyberGear的位置单位是度速度单位是度/秒电流单位是安培这些在协议文档里写得很清楚但实际用的时候很容易只记住了“float”忘了具体单位。我踩过的一个典型问题是把PID参数和速度规划值填成弧度制的量级导致实际速度比预期大好几倍电机一启动就开始震荡。还有个更隐蔽的问题控制周期不稳定。主循环里如果被其他任务抢占导致位置指令发送周期不是严格的10ms那速度规划里的max_step实际上是不准确的表现出来就是运动时快时慢。解决办法是把控制放到定时器中断里去或者提高控制任务的任务优先级。5.3 多电机同挂一条总线ID规划和仲裁问题当一条总线上挂多台CyberGear电机时每个电机必须设置不同的电机ID。CyberGear可以通过官方上位机或者特定CAN指令修改电机ID改完后就固定存在驱动器里。这里要提一下CAN总线的仲裁机制CAN报文ID本身决定了总线访问优先级ID数值越小优先级越高。在设计主机ID和电机ID时尽量让频繁发送的状态反馈帧和重要控制指令的ID处于合理范围。如果主机ID和电机ID分配混乱可能出现低优先级报文频繁被高优先级帧挤占导致控制延迟增大。我习惯把主机ID固定为0x01电机ID从0x01开始递增这样控制指令帧ID从0x010101往上走数值不算太小跟状态反馈帧ID也能天然错开一个段整体仲裁压力比较均衡。5.4 烧录失败与CAN引脚冲突ESP32下载的隐藏坑这个问题很多人会卡很久CAN通信一切正常但只要一插USB烧录就提示连接失败或者烧到一半卡死。大概率是你选的TWAI引脚和UART或者Flash引脚冲突了。ESP32的下载模式默认使用UART0GPIO1和GPIO3如果你把TWAI的TX/ RX引脚配到这俩脚上收发器会把电平拉来拉去干扰串口下载信号。解决方法是换引脚比如我用GPIO5和GPIO4就不影响下载。另外如果CAN收发器的TXD引脚悬空后输出异常电平也可能反灌到ESP32的GPIO上稳妥起见可以在烧录时把收发器模块的供电断开烧完再插上。5.5 常见问题速查表现象可能原因排查方法完全无响应接线错误、终端电阻缺失测量CAN_H和CAN_L之间电阻应为60Ω左右总线错误帧持续波特率不匹配逻辑分析仪抓波形确认位时间报文能发但收不到TX/RX接反、ID过滤配置错误检查引脚映射过滤器改成ACCEPT_ALL电机抖动PID参数不合适、位置单位错误先把PID调小确认位置/速度单位电机不使能上电时序问题、模式没设置先设置模式并等待200ms再发使能指令烧录失败TWAI引脚占用UART0换引脚烧录时断开收发器供电多电机间歇性延迟ID规划不均衡检查扩展帧ID分布让重要帧ID偏小5.6 调试小工具逻辑分析仪抓CAN波形调CAN总线的时候一个8通道的逻辑分析仪几十块钱的就行加一个CAN解码插件效果堪比几万块的CAN分析仪。把逻辑分析仪的CH0接CAN_H、CH1接CAN_L软件里选择CAN协议解码配置好波特率就能解析出帧ID和数据域内容。这个方法在排查“代码看起来没问题但电机就是不动”的时候特别有用因为你能直接看到总线上到底有没有CAN帧、帧ID是什么、数据对不对而不是瞎猜。我调CyberGear的时候至少有三次是靠着逻辑分析仪发现主机ID拼错了或者float字节序不对这类肉眼盯代码纯靠脑子推很难发现的低级错误。最后再分享一个实用小技巧整套框架跑通之后我最大的体会是先花半小时把CAN总线的自检和回环测试做好比你直接怼到电机上猛调参数效率高得多。CAN总线调起来本来就有点“黑盒”一旦出错你会面临“电机坏了、线坏了、代码坏了”三选一的猜谜环节。把这套分层框架和排查顺序完整走一遍能让你把问题限制在一个很小的范围内。另外一个经验是如果你准备把这个框架用在更复杂的机器人项目上建议尽早把无线调试通道加上去ESP32自带的WiFi可以开一个TCP服务端把电机状态、PID参数、目标位置这些数据全部通过无线发到上位机调机的时候人不用蹲在电机旁边。我后续在这个框架上还扩展了Micro-ROS让ESP32直接跟ROS 2通信用的就是同一套CAN底层改起来非常顺这也是分层设计带来的红利。
返回列表