用C语言实现工业CANopen主站编程:快速对接工业现场的伺服与IO模块
CANopen主站不是“会发CAN帧”就行,核心是四件事:NMT把从站拉进Operational、SDO配对象字典和PDO映射、PDO跑实时过程数据、心跳做节点保活。伺服和IO模块差异只在对象字典索引,主站框架是同一套。
一、COB-ID预定义连接集
主站按Node-ID拼ID即可,不用动态分配:
NMT命令:0x000
SDO发:0x600+ID,回:0x580+ID
RPDO1:0x200+ID(主站→从站)
TPDO1:0x180+ID(从站→主站)
心跳:0x700+ID
二、NMT状态机:先预操作再运行
从站上电自动进Pre-operational,此时SDO可用、PDO沉默;主站配完参数再发Start_Node切Operational。
void nmt_send(uint8_t cs, uint8_t node){
uint8_t d[2] = {cs, node};
can_tx(0x000, d, 2);
}
/* 启动单个伺服 */
nmt_send(0x01, 3); // Start_Remote_Node
/* 广播启动全部 */
nmt_send(0x01, 0x00);
/* 复位通信(重配前常用) */
nmt_send(0x82, 3);
状态值:0x04 Stopped、0x05 Operational、0x7F Pre-op。
三、SDO配置:快速SDO读写参数
SDO是请求-响应模型,配PDO映射、设伺服模式、读厂商参数都走它。≤4字节用快速SDO。
写控制模式(伺服6040/6060类):
/* 主站→从站 0x600+ID,命令字0x23表示快速写 */
void sdo_write(uint8_t node, uint16_t idx, uint8_t sub,
uint32_t val, uint8_t len){
uint8_t d[8] = {0x23, idx&0xFF, idx>>8, sub,
val&0xFF,(val>>8)&0xFF,(val>>16)&0xFF,(val>>24)&0xFF};
can_tx(0x600+node, d, 8);
}
/* 读响应在0x580+ID,Data[0]=0x43/0x4B/0x4F按长度 */
配置RPDO映射前必须先禁PDO:把0x1400:01的COB-ID最高位置1,再清0x1600:00映射数,写映射项,最后恢复。
sdo_write(node, 0x1400, 1, 0x80000200+node, 4); // 禁RPDO1
sdo_write(node, 0x1600, 0, 0, 1); // 映射项数=0
sdo_write(node, 0x1600, 1, 0x60400010, 4); // 控制字6040h:00 16bit
sdo_write(node, 0x1600, 2, 0x607A0010, 4); // 目标位置607Ah 16bit
sdo_write(node, 0x1600, 0, 2, 1); // 映射项数=2
sdo_write(node, 0x1400, 1, 0x00000200+node, 4); // 重新使能
映射值格式:(index<<16)|(sub<<8)|bits,长度是bit不是byte,16位写0x10。
四、PDO实时控制:不握手
进Operational后,伺服控制字/目标位置走RPDO,主站周期发;状态字/实际位置从TPDO收。
void servo_send_rpdo1(uint8_t node, uint16_t ctrl, int32_t tar_pos){
uint8_t d[8];
d[0]=ctrl&0xFF; d[1]=ctrl>>8;
d[2]=tar_pos&0xFF; d[3]=(tar_pos>>8)&0xFF;
d[4]=(tar_pos>>16)&0xFF; d[5]=(tar_pos>>24)&0xFF;
can_tx(0x200+node, d, 6); // 6字节:控制字+目标位置
}
/* 主循环1ms/2ms定时发 */
void pdo_task(void){
for(int i=0;i<n_servo;i++)
servo_send_rpdo1(sv[i].node, sv[i].ctrl, sv[i].tar_pos);
}
收TPDO直接按偏移解,不查索引:
void on_tpdo(uint8_t node, uint8_t*d, int len){
uint16_t status = d[0]|(d[1]<<8);
int32_t act_pos = d[2]|(d[3]<<8)|(d[4]<<16)|(d[5]<<24);
sv_update(node, status, act_pos);
}
IO模块更简单:RPDO1映射输出线圈,TPDO1映射输入状态,8数字量拼一字节。
五、心跳监控从站存活
从站周期发0x700+ID,主站记录最后时间戳,超时判离线:
#define HB_TO_MS 1500
static uint32_t hb_last[128];
void on_heartbeat(uint8_t node, uint8_t state){
hb_last[node] = sys_ms();
if(state!=0x05) node_fault(node);
}
void hb_check(void){
for(int n=1;n<128;n++){
if(dev_exist[n] && sys_ms()-hb_last[n] > HB_TO_MS)
node_offline(n); // 停该轴RPDO输出,报告警
}
}
六、主站调度结构
上电→CAN初始化
→NMT广播Reset_Comm
→等Boot-up(0x700+ID)
→SDO逐个配PDO映射
→NMT Start_Node
→周期任务发RPDO
→收TPDO/SDO响应/心跳
→异常时SDO读错误码0x603F
SDO配置阶段别发RPDO;Operational后别轮询SDO读实时数据,带宽会被吃光。
七、落坑清单
PDO映射长度写byte不写bit→帧错位
单PDO超8字节→映射拒绝
不禁用PDO直接改映射→写失败
Operational前发RPDO→从站不处理
控制字/状态字方向反(RPDO进、TPDO出)→伺服不动
SDO实时轮询位置→总线拥堵
心跳超时不断RPDO→离线轴还发指令
Node-ID冲突→两站抢同一COB-ID
伺服6040使能顺序错→驱动器报错
TPDO解析按索引查→多此一举还慢
八、落地顺序
CAN驱动打通,NMT 0x000能发能收
等从站Boot-up,SDO读0x1000确认设备类型
SDO禁PDO→配RPDO/TPDO映射→使能
NMT Start切Operational
周期发RPDO(伺服控制字+目标值,IO输出)
解析TPDO更新状态/位置/输入
心跳超时离线保护
故障用SDO读0x603F/0x1003,不占周期带宽
CANopen主站对接伺服和IO,代码骨架是通用的:NMT管状态、SDO只管配置、PDO管实时、心跳管存活。难点不在发帧,在“预操作阶段把映射配对”——控制字进RPDO、状态字出TPDO、长度按bit、单帧不超8字节、改映射先禁用。配完进Operational后主站就只干两件事:周期发RPDO、收TPDO解包。SDO留作上电配置和故障诊断,别拿来跑控制环,总线立马清爽。





