1. CANopen协议与CanFestival协议栈概述
CANopen是一种基于CAN总线的应用层协议,广泛应用于工业自动化领域。它定义了标准化的通信对象和通信机制,使得不同厂商的设备能够实现互操作。CanFestival是一个开源的CANopen协议栈实现,支持主站和从站功能,特别适合嵌入式系统开发。
在工业控制系统中,CANopen协议因其高效可靠的通信特性,成为伺服电机控制等场景的首选方案。STM32F407作为一款高性能ARM Cortex-M4微控制器,内置CAN控制器,是开发CANopen设备的理想平台。
提示:CanFestival协议栈采用对象字典(Object Dictionary)来管理所有通信参数,理解对象字典的结构是开发CANopen设备的关键。
2. 开发环境搭建与基础配置
2.1 硬件准备
- STM32F407开发板(需带CAN接口)
- CAN收发器模块(如TJA1050)
- 伺服电机或其他CANopen从站设备
- 120Ω终端电阻(用于CAN总线两端)
2.2 软件准备
- CanFestival源码(可从官网或GitHub获取)
- STM32CubeMX(用于生成基础工程)
- Keil MDK或IAR Embedded Workbench
- CAN分析仪(如PCAN-USB或USB-CAN适配器)
2.3 CanFestival移植要点
- 将CanFestival源码中的
include、src和drivers目录复制到工程 - 修改
config.h适配STM32硬件 - 实现
timer.c中的定时器相关函数 - 实现
can.c中的CAN驱动接口
c复制// 示例:STM32 CAN初始化
void canInit(void) {
CAN_FilterTypeDef filter;
hcan.Instance = CAN1;
hcan.Init.Prescaler = 6;
hcan.Init.Mode = CAN_MODE_NORMAL;
hcan.Init.SyncJumpWidth = CAN_SJW_1TQ;
hcan.Init.TimeSeg1 = CAN_BS1_13TQ;
hcan.Init.TimeSeg2 = CAN_BS2_2TQ;
hcan.Init.TimeTriggeredMode = DISABLE;
hcan.Init.AutoBusOff = DISABLE;
hcan.Init.AutoWakeUp = DISABLE;
hcan.Init.AutoRetransmission = DISABLE;
hcan.Init.ReceiveFifoLocked = DISABLE;
hcan.Init.TransmitFifoPriority = DISABLE;
HAL_CAN_Init(&hcan);
// 配置过滤器
filter.FilterBank = 0;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
filter.FilterIdHigh = 0x0000;
filter.FilterIdLow = 0x0000;
filter.FilterMaskIdHigh = 0x0000;
filter.FilterMaskIdLow = 0x0000;
filter.FilterFIFOAssignment = CAN_RX_FIFO0;
filter.FilterActivation = ENABLE;
HAL_CAN_ConfigFilter(&hcan, &filter);
HAL_CAN_Start(&hcan);
HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING);
}
3. 主站功能实现详解
3.1 PDO通信实现
PDO(Process Data Object)用于实时数据传输,分为接收PDO(RPDO)和发送PDO(TPDO)。PDO通信可采用同步或异步方式。
3.1.1 PDO映射配置
在对象字典中配置PDO映射参数:
- RPDO映射参数(0x1400-0x15FF)
- TPDO映射参数(0x1800-0x19FF)
- PDO通信参数(0x1600-0x17FF, 0x1A00-0x1BFF)
c复制// 配置TPDO1映射
UNS32 objDict[0x1800][2] = {
{0x00000201, 0x00000008}, // 映射1个对象:索引0x2001,子索引0x01,长度8位
{0x00000000, 0x00000000} // 结束标记
};
// 配置TPDO1通信参数
UNS32 objDict[0x1800][6] = {
0x00000180, // COB-ID:0x180 + 节点ID
0x02, // 传输类型:事件驱动
0x00, // 禁止时间
0x00, // 保留
0x00, // 事件定时器
0x00 // 同步起始值
};
3.1.2 PDO发送与接收实现
c复制// TPDO发送示例
void sendTPDOExample(void) {
UNS8 data[8] = {0};
int16_t position = 1000;
int16_t velocity = 500;
// 填充数据
data[0] = (position >> 8) & 0xFF;
data[1] = position & 0xFF;
data[2] = (velocity >> 8) & 0xFF;
data[3] = velocity & 0xFF;
// 发送TPDO1
CO_TPDO *tpdo = &CO->tPDO[0];
co_sendTPDO(tpdo, data, 4);
}
// RPDO接收回调
void rpdo1Callback(CO_Data *d, UNS8 *m, UNS8 len) {
int16_t position = (m[0] << 8) | m[1];
int16_t velocity = (m[2] << 8) | m[3];
printf("Received PDO - Position: %d, Velocity: %d\n", position, velocity);
}
3.2 SDO通信实现
SDO(Service Data Object)用于参数配置和非周期性数据传输,支持分段传输大数据块。
3.2.1 SDO客户端实现
c复制// SDO读取回调
void sdoReadCallback(CO_SDO *sdo, UNS8 errCode) {
if(errCode == 0) {
UNS32 value = (sdo->sdoRxData[0] << 24) |
(sdo->sdoRxData[1] << 16) |
(sdo->sdoRxData[2] << 8) |
sdo->sdoRxData[3];
printf("SDO Read Success: 0x%08X\n", value);
} else {
printf("SDO Read Error: 0x%02X\n", errCode);
}
}
// SDO写入回调
void sdoWriteCallback(CO_SDO *sdo, UNS8 errCode) {
if(errCode == 0) {
printf("SDO Write Success\n");
} else {
printf("SDO Write Error: 0x%02X\n", errCode);
}
}
// 读取从站参数
void readSlaveParameter(UNS8 nodeId, UNS16 index, UNS8 subIndex) {
CO_SDO *sdo = &CO->SDO[0];
sdo->nodeId = nodeId;
co_SDOclientRead(sdo, index, subIndex, sdoReadCallback);
}
// 写入从站参数
void writeSlaveParameter(UNS8 nodeId, UNS16 index, UNS8 subIndex, UNS32 value) {
CO_SDO *sdo = &CO->SDO[0];
sdo->nodeId = nodeId;
sdo->sdoTxData[0] = (value >> 24) & 0xFF;
sdo->sdoTxData[1] = (value >> 16) & 0xFF;
sdo->sdoTxData[2] = (value >> 8) & 0xFF;
sdo->sdoTxData[3] = value & 0xFF;
co_SDOclientWrite(sdo, index, subIndex, 4, sdoWriteCallback);
}
3.3 NMT状态管理与心跳
NMT(Network Management)用于管理节点状态,心跳机制用于监控节点存活状态。
c复制// 心跳消费者回调
void heartbeatConsumerCallback(CO_NMT *nmt, UNS8 nodeId, UNS8 state) {
const char *stateStr[] = {"Initializing", "Pre-Operational", "Operational", "Stopped"};
printf("Node %d state changed to %s\n", nodeId, stateStr[state]);
}
// 初始化心跳消费者
void initHeartbeatConsumer(void) {
CO_NMT *nmt = &CO->NMT;
nmt->heartbeatTime = 1000; // 心跳超时时间1s
nmt->heartbeatConsumer = heartbeatConsumerCallback;
}
// 控制从站状态
void controlSlaveState(UNS8 nodeId, UNS8 command) {
UNS8 msg[2] = {command, nodeId};
co_sendNMTmessage(&CO->NMT, msg, 2);
}
4. 从站功能实现详解
4.1 从站对象字典配置
从站需要定义完整的对象字典,包含通信参数和应用参数。
c复制// 示例对象字典条目
UNS32 objDict[0x2000] = {
[0x00] = 0x00000008, // 数据类型:UNSIGNED32
[0x01] = 0x00000000, // 初始值
[0x02] = 0x00000000, // 最小值
[0x03] = 0xFFFFFFFF, // 最大值
[0x04] = 0x00000001 // 访问权限:读写
};
4.2 从站PDO配置
从站PDO配置与主站类似,但需要注意COB-ID的设置要匹配主站配置。
c复制// 从站TPDO1配置
UNS32 objDict[0x1800][6] = {
0x00000181, // COB-ID:0x181 (TPDO1)
0xFE, // 传输类型:同步周期传输
0x00, // 禁止时间
0x00, // 保留
0x64, // 事件定时器100ms
0x00 // 同步起始值
};
4.3 从站SDO服务端实现
c复制// SDO写请求处理
UNS8 sdoWriteHandler(CO_SDO *sdo, UNS8 dataType, UNS8 dataSize, UNS8 *data) {
UNS16 index = sdo->index;
UNS8 subIndex = sdo->subIndex;
// 检查索引范围
if(index < 0x2000 || index > 0x5FFF) {
return 0x06020000; // 对象字典不存在
}
// 处理写请求
switch(dataType) {
case 0x02: // UNSIGNED8
objDict[index][subIndex] = data[0];
break;
case 0x04: // UNSIGNED16
objDict[index][subIndex] = (data[0] << 8) | data[1];
break;
case 0x06: // UNSIGNED32
objDict[index][subIndex] = (data[0] << 24) | (data[1] << 16) |
(data[2] << 8) | data[3];
break;
default:
return 0x06070010; // 数据类型不匹配
}
return 0; // 成功
}
// SDO读请求处理
UNS8 sdoReadHandler(CO_SDO *sdo, UNS8 *dataType, UNS8 *dataSize, UNS8 **data) {
UNS16 index = sdo->index;
UNS8 subIndex = sdo->subIndex;
// 检查索引范围
if(index < 0x2000 || index > 0x5FFF) {
return 0x06020000; // 对象字典不存在
}
// 设置返回数据
*dataType = 0x06; // UNSIGNED32
*dataSize = 4;
*data = (UNS8 *)&objDict[index][subIndex];
return 0; // 成功
}
4.4 紧急报文(EMCY)实现
c复制// 紧急错误代码定义
#define EMCY_GENERAL_ERROR 0x1000
#define EMCY_CURRENT_OVERFLOW 0x2310
#define EMCY_VOLTAGE_OVERFLOW 0x3210
// 发送紧急报文
void sendEmergency(UNS16 errorCode, UNS8 errorRegister) {
CO_EMCY *emcy = &CO->EMCY;
co_sendEMCY(emcy, errorCode, errorRegister);
}
// 紧急报文回调
void emcyCallback(CO_EMCY *emcy, UNS16 errorCode, UNS8 errorRegister) {
printf("Emergency received: Code=0x%04X, Register=0x%02X\n",
errorCode, errorRegister);
}
5. 系统集成与调试技巧
5.1 一主多从系统配置
- 为每个从站分配唯一的节点ID(1-127)
- 配置主站的心跳消费者参数
- 设置PDO映射确保主从站匹配
- 配置SDO超时时间(建议500ms-1000ms)
5.2 常见问题排查
-
通信失败:
- 检查CAN总线终端电阻(两端各120Ω)
- 确认波特率设置一致(通常1Mbps)
- 使用CAN分析仪监控原始报文
-
PDO数据不更新:
- 检查PDO映射参数是否正确
- 确认传输类型和事件时间配置
- 验证对象字典条目是否存在
-
SDO超时:
- 检查从站节点ID和SDO服务端实现
- 确认CAN总线负载是否过高
- 增加SDO超时时间或重试机制
5.3 性能优化建议
- 合理设置PDO传输周期,平衡实时性和总线负载
- 对关键PDO使用同步传输确保确定性
- 将频繁访问的参数映射到PDO,减少SDO使用
- 优化对象字典布局,将常用参数放在连续地址
6. 伺服电机控制实战
6.1 伺服电机对象字典配置
典型伺服电机需要配置以下参数:
- 控制字(0x6040)
- 状态字(0x6041)
- 目标位置(0x607A)
- 实际位置(0x6064)
- 目标速度(0x60FF)
- 实际速度(0x606C)
6.2 PDO运动控制实现
c复制// 伺服控制PDO映射
UNS32 objDict[0x1601][4] = {
{0x60400010, 0x60640020, 0x606C0020, 0x00000000} // RPDO1映射控制字、位置、速度
};
UNS32 objDict[0x1A01][4] = {
{0x60410010, 0x60640020, 0x606C0020, 0x00000000} // TPDO1映射状态字、位置、速度
};
// 控制伺服运动
void controlServo(int32_t position, int32_t velocity) {
UNS8 ctrlWord[2] = {0x0F, 0x00}; // 使能+启动
UNS8 data[8];
// 控制字
data[0] = ctrlWord[0];
data[1] = ctrlWord[1];
// 目标位置
data[2] = (position >> 24) & 0xFF;
data[3] = (position >> 16) & 0xFF;
data[4] = (position >> 8) & 0xFF;
data[5] = position & 0xFF;
// 目标速度
data[6] = (velocity >> 8) & 0xFF;
data[7] = velocity & 0xFF;
// 发送RPDO
CO_RPDO *rpdo = &CO->rPDO[0];
co_sendRPDO(rpdo, data, 8);
}
6.3 伺服状态监控
c复制// 伺服状态PDO回调
void servoStatusCallback(CO_Data *d, UNS8 *m, UNS8 len) {
UNS16 statusWord = (m[0] << 8) | m[1];
int32_t actualPos = (m[2] << 24) | (m[3] << 16) | (m[4] << 8) | m[5];
int32_t actualVel = (m[6] << 8) | m[7];
printf("Servo Status: Pos=%d, Vel=%d, State=0x%04X\n",
actualPos, actualVel, statusWord);
// 检查错误状态
if(statusWord & 0x0800) {
printf("Servo Fault Detected!\n");
}
}
在实际项目中,我发现伺服电机的控制响应时间与PDO周期密切相关。通过实验,将PDO周期设置为5ms时,系统既能保证实时性,又不会造成CAN总线过载。此外,建议在每次上电后通过SDO读取伺服参数进行验证,确保通信配置正确。