跳到主要内容

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.c 和 OD.h;
  • idf_component.yml;
  • course_sdo_client.c 和 course_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;

再用 switch 把 0x0040、0x0021、0x0023、0x0027 等结果转换成文字。这里复用了第 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 在允许的静止误差内没有出现命令性位置变化;
  • 电机没有非预期运动、报警或复位。