阳江城乡建设部网站首页单职业传奇手机手游版
阳江城乡建设部网站首页,单职业传奇手机手游版,书城网站建设规划书,新冠疫苗接种查询1. 开篇#xff1a;为什么我们要啃这块“硬骨头”#xff1f;
大家好#xff0c;我是老张#xff0c;一个在嵌入式领域摸爬滚打了十来年的工程师。最近看到很多朋友对稚晖君开源的dummy机械臂项目特别感兴趣#xff0c;尤其是那个看起来有点神秘的CAN总线通信部分。很多人…1. 开篇为什么我们要啃这块“硬骨头”大家好我是老张一个在嵌入式领域摸爬滚打了十来年的工程师。最近看到很多朋友对稚晖君开源的dummy机械臂项目特别感兴趣尤其是那个看起来有点神秘的CAN总线通信部分。很多人拿到代码打开interface_can.cpp文件看到满屏的十六进制数字、结构体和指针操作可能头一下就大了感觉无从下手。我完全理解这种感觉。当初我第一次接触工业CAN总线协议的时候也是一头雾水。但我想告诉你的是稚晖君的这个CAN通信代码其实是理解整个机械臂运动控制最核心、也最有趣的一把钥匙。它就像机器人的“神经语言”上位机比如你的电脑或主控板通过它向每一个关节电机发送指令“动起来”“转快点”“停在这里”。如果你能看懂这套语言你不仅能玩转dummy机械臂以后自己做四足机器人、智能小车甚至任何基于多电机协同的系统都会感觉豁然开朗。所以这个系列文章我就打算用最“小白”的方式带大家从零开始一行行地解析这份代码。我们不求一下子成为专家但求能真正看懂每一行代码在干什么知道怎么去修改、调试甚至为自己的项目定制通信协议。放心我会尽量避开那些枯燥的理论堆砌多用生活中的例子和实际的代码操作来解释。准备好了吗我们这就开始第一站就是理解CAN消息是如何被接收和处理的。2. 庖丁解牛认识CAN通信的“回调函数”当我们说“解析CAN代码”时我们在说什么在dummy机械臂的项目里电机控制器比如那个精致的FOC驱动板上跑着一段程序它需要时刻竖起耳朵监听CAN总线上传来的消息。这段负责“听”的程序就是一个回调函数Callback Function。你可以把它想象成一个非常尽职的快递驿站管理员。CAN总线就像一条繁忙的公路上面跑着各种数据包快递。这个管理员回调函数一直守在驿站里每当有属于他这个驿站的快递特定ID的CAN消息送达他就立刻被唤醒然后根据快递单号命令字_cmd来决定下一步做什么是签收启用电机还是执行校准或者是设置运动参数。在interface_can.cpp文件中这个核心的管理员函数通常叫做OnCanCmd或者类似的名字。它的函数签名大概长这样void OnCanCmd(uint32_t canId, uint8_t* RxData, uint8_t len) { // 你的代码逻辑在这里 }canId 这是快递单号用来区分这条消息是发给哪个设备的。在复杂的系统中可能有多个电机节点每个都有自己唯一的ID。RxData 这是一个指针指向接收到的“快递包裹”内部——也就是实际的数据内容。比如让电机转到90度这个“90.0”的数值就放在这里面。len 数据长度告诉你这个包裹有多大通常是8个字节这是CAN标准数据帧的最大容量。稚晖君的代码里这个函数的框架非常清晰。它用一个switch (_cmd)语句根据不同的命令字跳转到不同的处理分支。这就像管理员看到快递单上写着“电器”他就知道要轻拿轻放写着“文件”他就知道要送去办公室。接下来我们就深入这个switch语句看看第一个关键命令是怎么工作的。3. 第一个命令让电机“苏醒”0x01我们来看switch (_cmd)里的第一个case通常就是case 0x01:。这个十六进制的0x01代表的就是“启用电机”命令。这是所有控制的起点电机在收到这个命令之前通常处于一种安全待机或失能状态不会响应任何运动指令。代码片段通常是这样case 0x01: // Enable Motor motor.controller-requestMode (*(uint32_t*) (RxData) 1) ? Motor::MODE_COMMAND_VELOCITY : Motor::MODE_STOP; break;这几行代码信息量很大我们拆开揉碎了看命令解析0x01是命令码上位机发送一个CAN帧其数据部分的第一个字节或按协议约定就是这个值告诉驱动器“现在要执行启用命令”。数据提取(*(uint32_t*) (RxData) 1)这一串看起来有点吓人其实是C语言中典型的“指针类型转换解引用”操作。我来翻译一下RxData是uint8_t*类型指向一个字节数组。(uint32_t*) (RxData)意思是“嘿编译器别把RxData当成字节数组看了请你把它当成一个指向uint32_t32位无符号整数的指针吧”。这是一种强制类型转换。最前面的*是解引用操作符意思是“取出这个指针所指向地址的值”。所以*(uint32_t*) (RxData)合起来就是将RxData起始地址的4个字节解释为一个32位整数并取出它的值。最后 1判断这个值是否等于1。逻辑决策这里使用了一个C语言的三目运算符? :。它的逻辑是如果从RxData解析出的整数等于1那么motor.controller-requestMode电机控制器的请求模式就被设置为Motor::MODE_COMMAND_VELOCITY速度控制模式。如果不等于1通常是0则设置为Motor::MODE_STOP停止模式。这在实际操作中意味着什么这意味着上位机发送启用命令时可以附带一个参数。比如发送数据{0x01, 0x00, 0x00, 0x00, 0x01, ...}假设协议规定前4字节是参数驱动器解析出参数为1电机就会进入准备进行速度控制的状态。如果参数是0电机则会进入安全停止状态。这种设计给了控制很大的灵活性你可以用一个命令选择是启动还是急停。注意这里只是设置了requestMode真正的模式切换可能还需要在其他地方比如主循环检查这个标志位并执行实际的模式切换函数。这是一种常见的异步设计模式将“请求”和“执行”分开让代码更清晰。4. 校准与初始化为精准运动奠基0x02第二个关键命令是case 0x02:对应“执行编码器校准”。对于像dummy机械臂使用的伺服电机特别是带绝对值编码器的上电后的第一次校准至关重要。case 0x02: // Do Calibration encoderCalibrator.isTriggered true; break;这行代码非常简单但背后代表的意义非凡校准的目的电机内部的编码器用来告诉控制器“我现在转到了什么位置”。但上电瞬间控制器并不知道机械零位比如机械臂完全伸直的位置对应编码器的哪个数值。校准过程就是让电机转动到一个预设的物理位置比如通过堵转寻找机械限位然后将这个位置记录为编码器的零点。触发标志代码没有直接执行复杂的校准算法而是简单地设置了一个标志位encoderCalibrator.isTriggered true。这同样是异步编程的思想。校准可能是一个需要耗时几秒钟、包含多步动作如缓慢移动、检测扭矩突变的过程不适合在中断回调函数OnCanCmd通常是在CAN接收中断中被调用中长时间执行。后续动作在主循环或某个专门的任务中会不断检查这个isTriggered标志。一旦发现它为true就会启动一套完整的校准流程流程结束后再将标志置为false。为什么校准如此重要想象一下如果你的机械臂每次上电都认为“伸直”是90度而实际物理位置是0度那么所有基于角度的运动控制都会完全错乱甚至导致机械臂撞到自己或别的东西。所以在第一次运行机械臂或者任何可能丢失编码器位置信息的情况下比如更换电池执行编码器校准是必不可少的第一步。稚晖君通过一个简单的CAN命令将这个过程暴露出来使得远程或自动初始化成为可能。5. 运动控制三剑客电流、速度与位置0x03, 0x04, 0x05掌握了启用和校准我们就来到了运动控制的核心区设定目标值。0x03,0x04,0x05这三个命令分别对应电流环、速度环、位置环的控制。这是电机控制从内到外的三个闭环理解它们的关系对调试至关重要。5.1 电流环0x03控制电机的“力气”case 0x03: // Set Current SetPoint if (motor.controller-modeRunning ! Motor::MODE_COMMAND_CURRENT) motor.controller-SetCtrlMode(Motor::MODE_COMMAND_CURRENT); motor.controller-SetCurrentSetPoint((int32_t) (*(float*) RxData * 1000)); break;模式切换首先检查电机当前是否已在电流控制模式如果不是则调用SetCtrlMode切换到该模式。电流环是最内环响应最快它直接控制电机相绕组的电流大小从而控制扭矩力气。参数解析与设置(*(float*) RxData * 1000)再次使用了类型转换。这里假设RxData的前4个字节是一个浮点数例如2.5表示2.5安培。乘以1000是为了将单位“安培”转换为驱动器内部可能使用的“毫安”或某个固定单位的整数值。SetCurrentSetPoint函数将这个最终值设定为电流目标。5.2 速度环0x04控制电机的“转速”速度环建立在电流环之上。它给定一个速度目标控制器会计算需要多大的电流扭矩来达到并维持这个速度。case 0x04: // Set Velocity SetPoint if (motor.controller-modeRunning ! Motor::MODE_COMMAND_VELOCITY) { motor.config.motionParams.ratedVelocity boardConfig.velocityLimit; motor.controller-SetCtrlMode(Motor::MODE_COMMAND_VELOCITY); } motor.controller-SetVelocitySetPoint( (int32_t) (*(float*) RxData * (float) motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS)); break;额外设置在切换到速度模式前代码将电机的额定速度参数更新为配置中的速度限制boardConfig.velocityLimit。这是一个安全措施防止意外设置过高的速度。单位转换motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS这个常量非常关键。它表示电机旋转一圈在控制器内部对应的“步数”或“计数单位”。这通常由编码器的分辨率决定例如17位编码器一圈是131072个计数。所以*(float*) RxData可能是以“圈/秒”或“弧度/秒”为单位的速度乘以这个常量后就转换成了控制器内部理解的速度单位计数/秒。5.3 位置环0x05控制电机的“角度”位置环是最外环也是机械臂最常用的控制模式。它给定一个位置目标控制器会计算需要达到的速度进而计算需要的电流。case 0x05: // Set Position SetPoint if (motor.controller-modeRunning ! Motor::MODE_COMMAND_POSITION) { motor.config.motionParams.ratedVelocity boardConfig.velocityLimit; motor.controller-SetCtrlMode(Motor::MODE_COMMAND_POSITION); } motor.controller-SetPositionSetPoint( (int32_t) (*(float*) RxData * (float) motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS)); if (_data[4]) // Need Position Finished ACK { tmpF motor.controller-GetPosition(); auto* b (unsigned char*) tmpF; for (int i 0; i 4; i) _data[i] *(b i); _data[4] motor.controller-state Motor::STATE_FINISH ? 1 : 0; txHeader.StdId (boardConfig.canNodeId 7) | 0x23; CAN_Send(txHeader, _data); } break;这个case比前两个复杂因为它包含了反馈机制。模式切换与目标设置前半部分和速度环类似切换模式并设置位置目标值。同样进行了单位转换将外部单位如角度、弧度转换为内部计数单位。反馈请求if (_data[4])这一部分是精髓。_data数组在这里扮演了双重角色注意区分接收数据RxData和用于发送的_data。_data[4]这个字节被用作一个标志位。如果上位机在发送位置设定命令时将这个字节设为非零比如1就相当于在命令后附加了一句“喂设置完之后告诉我你现在实际的位置和运动是否完成了。”打包反馈数据tmpF motor.controller-GetPosition();获取电机当前的实际位置内部单位。auto* b (unsigned char*) tmpF;这是一个巧妙的操作。它获取了存储tmpF这个浮点数的内存起始地址并将其视为一个字节数组的指针。因为CAN总线发送的是字节流我们需要把浮点数拆成4个字节。for循环将这4个字节逐一拷贝到准备发送的_data数组的前4个字节。_data[4]则被用来表示运动状态如果电机状态是STATE_FINISH完成则为1否则为0。发送反馈txHeader.StdId (boardConfig.canNodeId 7) | 0x23;这行设置了反馈消息的CAN ID。boardConfig.canNodeId是当前电机节点的ID左移7位再与0x23进行或操作是稚晖君协议里定义的一种地址功能码的封装方式0x23很可能就代表“位置状态反馈”功能。最后调用CAN_Send将包含当前位置和状态的数据发送回总线。这种设计的好处是什么它实现了请求-应答式的通信。上位机可以异步地发送一个位置命令并请求反馈然后去处理别的事情。过一会儿它再来检查CAN总线是否收到了来自指定ID的、带有0x23功能码的反馈帧从而知道命令执行结果。这对于需要精确同步或状态确认的机器人应用非常关键。6. 进阶位置控制带约束的运动0x06, 0x07在基础的位置控制之上稚晖君的代码还提供了两个更高级的指令0x06和0x07它们让位置控制更加平滑和安全。6.1 时间约束位置控制0x06case 0x06: // Set Position with Time这个命令不仅告诉电机“要去哪里”还告诉了它“要用多长时间到达”。motor.controller-SetPositionSetPointWithTime( (int32_t) (*(float*) RxData * (float) motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS), *(float*) (RxData 4));参数解析这个函数接收两个参数。第一个参数和0x05命令一样是目标位置。关键在于第二个参数*(float*) (RxData 4)。这里RxData 4是指针运算意思是取RxData起始地址向后偏移4个字节的地址。通常协议约定前4个字节(RxData[0]~RxData[3])是目标位置浮点数紧接着的4个字节(RxData[4]~RxData[7])就是时间参数浮点数单位可能是秒。内部逻辑控制器拿到这两个参数后会在内部规划一条从当前位置到目标位置的轨迹。最简单的规划是匀速运动根据距离和时间算出一个恒定的速度曲线。更高级的规划器可能会生成S型曲线匀加速-匀速-匀减速使启停更加平滑减少冲击。这对于机械臂这类刚性系统来说能有效避免振动保护齿轮和结构。6.2 速度约束位置控制0x07case 0x07: // Set Position with Velocity-Limit这个命令则是告诉电机“要去哪里”并且“路上最快不能超过某个速度”。motor.config.motionParams.ratedVelocity (int32_t) (*(float*) (RxData 4) * (float) motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS); motor.controller-SetPositionSetPoint( (int32_t) (*(float*) RxData * (float) motor.MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS));动态限速注意看它在设置位置目标前先修改了motor.config.motionParams.ratedVelocity电机额定速度/速度限制。这个值很可能被内部的位置规划器或速度环限制器所使用。*(float*) (RxData 4)解析出来的就是速度上限值。与0x04命令的区别0x04是直接进行速度控制目标是速度本身。而0x07是位置控制速度限制只是一个安全或性能约束。电机仍然会以尽可能快的速度但不超过限制走向目标点并在接近时减速。这个命令后面强制包含了反馈代码中注释为// Always Need Position Finished ACK说明这种带约束的运动通常更需要状态确认。在实际调试dummy机械臂时我常常先用0x05命令做简单的位置点动然后用0x07来测试不同速度限制下的运动平稳性最后用0x06来让多个关节实现时间同步的运动比如让所有关节在2秒内同时到达指定位置这对于实现流畅的轨迹动作至关重要。7. 实战演练动手发送你的第一条CAN指令读懂了代码不实践一下总觉得缺点什么。虽然我们可能没有实体的dummy机械臂硬件但完全可以在PC上模拟CAN通信或者用一块简单的CAN调试工具来验证我们的理解。这里我以使用一款常见的USB-CAN适配器配合上位机软件为例给大家演示一下如何构造并发送一个启用电机0x01的命令。7.1 准备工作硬件准备一个USB转CAN的适配器如PCAN-USB, ZLG的USBCAN系列或者更开源的CANable等。软件安装适配器厂商提供的调试软件或者使用开源的candump,cansend工具Linux环境或者像CANalyzer、CANoe专业等。连接将适配器连接到你的电脑并将CAN_H和CAN_L线连接到你的电机驱动器或另一个CAN适配器做回环测试。7.2 解析协议格式根据我们之前分析的代码我们需要确定dummy机械臂电机驱动器期待的CAN帧格式。从代码中我们可以推断出一些信息但最准确的应该查看项目文档或interface_can.h如果有。假设一个最简单的协议格式CAN ID由目标电机节点ID和功能码组成。例如代码中反馈帧的ID是(boardConfig.canNodeId 7) | 0x23。对于发送给电机的命令可能ID是(nodeId 7) | 0x01或者更简单直接使用扩展ID这需要你查看项目中的具体定义。我们假设命令帧使用标准ID且ID的低位就包含了节点地址和命令类型。例如设置节点1的ID为0x101。数据长度DLC通常是8。数据域第一个字节命令字_cmd如0x01。后续字节命令参数。对于0x01命令参数是一个4字节的uint32_t。1表示启用至速度模式0表示停止。7.3 构造数据帧假设我们要启用节点1的电机进入速度模式。CAN ID:0x101(这是一个示例需根据实际协议调整)DLC:8Data:01 00 00 00 01 00 00 00字节0:0x01- 命令字字节1-4:00 00 00 01- 这是小端字节序Little-Endian表示的0x00000001即十进制1。在内存中低位字节在前。所以数字1的32位表示就是01 00 00 00。7.4 使用工具发送在CAN调试软件中你通常会看到一个发送区。填入上述信息帧类型标准数据帧帧ID101(十六进制)帧格式标准帧数据长度8数据01 00 00 00 01 00 00 00点击发送。如果驱动器在线并且ID匹配你应该能看到驱动器上的状态指示灯发生变化比如从闪烁变为常亮表示电机已启用。7.5 更复杂的例子发送位置指令如果要发送0x05命令让节点1的电机转到90度位置并请求反馈。假设电机一转对应360度MOTOR_ONE_CIRCLE_SUBDIVIDE_STEPS是131072。那么90度对应的内部位置值是(90 / 360) * 131072 32768。作为浮点数发送90.0 的浮点数十六进制表示为0x42B40000按IEEE754标准。请求反馈标志_data[4]设为1。构造数据假设协议为字节0命令字节1-4浮点位置字节5请求标志命令字0x05位置浮点数90.000 00 B4 42(小端字节序)请求标志01其余字节补零。完整数据域可能是05 00 00 B4 42 01 00 00发送这个帧后如果一切正常你应该能很快在总线上收到一个来自ID为(17)|0x23 0xA3假设的反馈帧数据域中包含电机当前的位置4字节浮点和完成状态1字节。通过这样的动手实验屏幕上冰冷的代码就变成了总线上一闪而过的数据包以及电机实实在在的转动。这种成就感正是驱动我们不断深入学习的最大动力。