跳到主要内容

23. 使能电机但不运动

学习形式安全实验
硬件要求完整实验平台
配套 Demolab_23_cia402_enable
本课动作使能,但不下发运动目标

上一课只读 0x6041。这一课第一次写电机,但只写 0x6040 Controlword,让 CiA 402 状态机按标准路径进入 Operation enabled。

程序不写运行模式、不写目标位置、不写目标速度,也不发送运动 PDO。

当前验证状态:独立工程已重新编译并上板验证,修复后固件大小 210192 字节。电机依次到达 Ready to switch on、Switched on 和 Operation enabled,最终 0x6041=0x0637;本课未写运行模式、目标位置或目标速度。记录见第 23 课上板记录

本课每一步都遵守同一条规则:写入、收到 SDO 成功响应、重新读取状态、再决定是否继续。

写 Controlword 后必须读回 Statusword 的验证闭环

从第 19 课工程开始写

先新建 lab_23_cia402_enable 标准工程,把自己在第 19 课已经验证过的这些文件复制到新工程 main/

  • OD.cOD.h
  • idf_component.yml
  • course_sdo_client.ccourse_sdo_client.h

不要复制 build/managed_components/。先保持第 19 课的只读程序,执行一次:

# 先选择 ESP32-C3,再编译工程;这一步不会烧录开发板。
idf.py set-target esp32c3
idf.py build

这次仍然只能读取电机。确认基础工程没有因为复制路径或 CMake 出错后,再加入 download 接口。这样一旦写功能出错,能够确认问题来自本课新增代码。

Controlword 和 Statusword 的关系

可以把它们看成一问一答:

对象方向作用
0x6040 Controlword控制端写给电机请求状态机执行下一步
0x6041 Statusword控制端从电机读取确认电机实际到达哪里

写入成功只代表 SDO Server 接受了对象写入。必须再读 0x6041,才能证明状态转换成功。

本课使用的三步路径

CiA 402 使能路径

步骤0x6040随后期望的状态
Shutdown0x0006Ready to switch on
Switch on0x0007Switched on
Enable operation0x000FOperation enabled

这些名称描述 CiA 402 状态转换,不是操作系统关机或 ESP32 重启。

在 SDO 模块中加入 16 位写入

第 19 课只有 upload。现在加入 download,但接口故意只允许一个 uint16_t

// 接口把写入值限定为 uint16_t,专门覆盖 0x6040 Controlword 这类 16 位对象。
// 目标 Node-ID、Index、Sub-index 和 Abort Code 都由调用者显式提供,失败原因不会丢失。
bool course_sdo_download_u16(CO_t *co, uint8_t server_node_id,
uint16_t index, uint8_t sub_index,
uint16_t value,
CO_SDO_abortCode_t *abort_code);

uint16_t 按小端序放入两字节缓冲区:

// CANopen 使用小端序:低 8 位先放入 data[0],高 8 位再放入 data[1]。
// 例如 Controlword 0x0006 在线上传输为 06 00,而不是 00 06。
uint8_t data[2] = {
(uint8_t)(value & 0xFFU),
(uint8_t)((value >> 8) & 0xFFU),
};

例如 0x0006 发送的数据字节顺序是 06 00

先在函数开头检查参数并清空上一次错误:

// 协议栈、SDO Client 通道和 Abort 输出指针缺一不可,空指针时绝不能继续写电机。
if ((co == NULL) || (co->SDOclient == NULL) || (abort_code == NULL)) {
return false;
}
// 清除上一笔访问的 Abort Code,确保本次失败日志不会误用旧错误。
*abort_code = CO_SDO_AB_NONE;

接着复用第 19 课的通道选择代码:

// [0] 是本地第一条 SDO Client 通道;远程电机由 server_node_id 决定。
CO_SDOclient_t *client = &co->SDOclient[0];

// 默认 SDO 请求/响应 CAN-ID 分别是 0x600+Node-ID、0x580+Node-ID。
CO_SDO_return_t ret = CO_SDOclient_setup(
client,
CO_CAN_ID_SDO_CLI + server_node_id,
CO_CAN_ID_SDO_SRV + server_node_id,
server_node_id);

接着调用 CANopenNode:

// Initiate 声明对象地址、长度和超时,BufWrite 再把刚才准备的 2 字节交给状态机。
// 两步都完成仍不代表电机已经接受,必须继续运行下面的 Download 循环。
CO_SDOclientDownloadInitiate(client, index, sub_index,
sizeof(data), 1000, false);
CO_SDOclientDownloadBufWrite(client, data, sizeof(data));

再周期调用 CO_SDOclientDownload(),直到成功、Abort 或超时。完整实现见:

examples/lab_23_cia402_enable/main/course_sdo_client.c

状态机循环应由你自己补成下面这样:

// ret>0 表示下载仍在进行;每轮同时推进整个 CANopenNode 和当前 SDO 状态机。
do {
(void)CO_process(co, false, SDO_STEP_US, NULL);
ret = CO_SDOclientDownload(client, SDO_STEP_US, false, false,
abort_code, NULL, NULL);
if (ret > 0) {
// 只有仍在等待时才让出 CPU;成功或 Abort 后立即退出,不靠固定延时猜结果。
vTaskDelay(pdMS_TO_TICKS(SDO_STEP_US / 1000));
}
} while (ret > 0);

// 成功、Abort 或超时都要关闭会话,下一次写入才能重新 setup 同一通道。
CO_SDOclientClose(client);
return ret == CO_SDO_RT_ok_communicationEnd;

写完 download 函数后先执行 idf.py build,但先不要烧录。编译只能确认 API 和类型正确;写电机前还要完成下面的状态检查代码。

把三步写成数据表

不要在 app_main() 中散落三个神秘数字。先定义每一步包含什么:

// 每一行把“要写的 Controlword”和“写后应读回的状态”绑定在一起。
// 程序按表顺序执行,避免把 0x0006/0x0007/0x000F 变成散落且难核对的数字。
typedef struct {
uint16_t controlword;
uint16_t expected_state;
const char *name;
} enable_step_t;

static const enable_step_t ENABLE_STEPS[] = {
{0x0006, 0x0021, "Shutdown"},
{0x0007, 0x0023, "Switch on"},
{0x000F, 0x0027, "Enable operation"},
};

controlword 是准备写入 0x6040 的值;expected_state 是随后从 0x6041 取掩码后应该得到的状态。名称只用于日志,不是电机收到的字符串。

接着写 cia402_state_name(),先计算:

// 完整 Statusword 还包含 Remote、Target reached 等附加位,不能直接与状态常量比较。
// 0x006F 只留下 CiA 402 状态机判断所需的 bit。
uint16_t state = statusword & 0x006FU;

再用 switch0x00400x00210x00230x0027 等结果转换成文字。这里复用了第 22 课已经解释过的状态掩码,不重新发明判断规则。

先读状态再决定是否继续

// 任何控制字写入前先确认通信可读且 Fault bit 为 0。
// 读取失败和电机故障都立即返回,绝不“先写一下看看”。
uint16_t statusword = 0;
if (!read_statusword(co, &statusword)) {
return;
}
if ((statusword & 0x0008U) != 0U) {
ESP_LOGE(TAG, "Motor is in Fault");
return;
}

这两次 return 都是安全行为:通信不明或电机故障时,不继续写控制字。

每一步都读回

// 三个步骤逐项执行:写 0x6040、等待设备状态稳定、重新读 0x6041 验证。
// 任一步 SDO 失败或状态不符都停止,不能跳过后继续发送下一控制字。
for (size_t i = 0; i < sizeof(ENABLE_STEPS) / sizeof(ENABLE_STEPS[0]); i++) {
CO_SDO_abortCode_t abort_code = CO_SDO_AB_NONE;
if (!course_sdo_download_u16(co, MOTOR_NODE_ID, 0x6040, 0x00,
ENABLE_STEPS[i].controlword,
&abort_code)) {
return;
}
// SDO 成功只说明 Controlword 已被接受;留出 200 ms 后再读取实际状态机结果。
vTaskDelay(pdMS_TO_TICKS(200));
if (!read_statusword(co, &statusword)) {
return;
}

// 屏蔽附加状态位后再与本步骤 expected_state 比较。
uint16_t state = statusword & 0x006FU;
if (state != ENABLE_STEPS[i].expected_state) {
ESP_LOGE(TAG, "State transition was not accepted");
return;
}
}

这里的 200 ms 不是“假装协议响应”。SDO 下载函数内部已经等待并确认 0x581 成功响应;这段短暂等待给驱动状态机完成物理状态转换,然后程序通过新的 SDO 读取验证结果。

参考工程还允许电机已经提前处于更后面的安全状态,例如第一步读回 Switched on 或 Operation enabled。自己第一次实现时可以先使用严格判断,理解日志后再对照配套 Demo 中 examples/lab_23_cia402_enable/main/app_main.c 的完整状态判断。不要为了让日志显示 PASS 而无条件忽略状态不匹配。

为什么使能后电机不应主动转动

本课没有改变:

  • 0x6060 Modes of operation;
  • 0x607A Target position;
  • 0x60FF Target velocity;
  • Profile Position 的 new set-point 位;
  • 任何运动 RPDO。

因此状态进入 Operation enabled 只表示驱动准备接受控制。若本课代码导致明显持续运动,应立即断电并检查设备先前保存的模式、目标和厂商行为,不能把它当成正常现象继续实验。

构建和上板

工程位于 examples/lab_23_cia402_enable/

  • examples/lab_23_cia402_enable/main/app_main.c(入口和状态判断)
  • examples/lab_23_cia402_enable/main/course_sdo_client.c(SDO 读写模块)
# 在当前终端加载 ESP-IDF 环境;新开终端后需要重新执行。
cd "$HOME/esp/can-canopen-course/examples/lab_23_cia402_enable"
source "$HOME/esp/esp-idf-v5.5.4/export.sh"
idf.py build
idf.py flash monitor

上板前清空机构周围空间,并准备直接切断 12 V 电机电源。

本课过关条件

真实日志和总线报文应共同证明:

  • 初始 0x6041 可读取且 Fault 位为 0;
  • 三次 0x6040 写入均收到 SDO 下载成功响应;
  • 每次写后都重新读取 0x6041
  • 最终 statusword & 0x006F 等于 0x0027
  • 0x6064 在允许的静止误差内没有出现命令性位置变化;
  • 电机没有非预期运动、报警或复位。