1. CANopen从站配置基础
在工业自动化领域,CANopen协议因其高实时性和可靠性被广泛应用于各类设备通信。作为一名嵌入式开发者,我曾多次在STM32平台上实现CANopen从站功能,今天就来分享一个完整的CiA402从站配置实例。
CiA402是CANopen在运动控制领域的标准协议,定义了驱动器、伺服电机等设备的标准接口。一个典型的CANopen从站实现包含三个核心部分:对象字典(Object Dictionary)、通信协议栈和应用层逻辑。对象字典是CANopen设备的核心数据库,存储了所有配置参数和过程数据;协议栈处理底层通信;应用层则实现设备的具体功能。
2. 对象字典配置详解
2.1 对象字典结构设计
对象字典是CANopen设备的核心数据结构,采用16位索引和8位子索引的寻址方式。根据CiA标准,对象字典分为几个关键区域:
c复制// 对象字典区域划分示例
0x1000-0x1FFF: 通信参数区 (所有CANopen设备必备)
0x2000-0x5FFF: 制造商特定参数 (可选)
0x6000-0x9FFF: 设备规范区 (CiA402驱动器专用)
在我的项目中,对象字典通过两个关键文件实现:
- Ds402_Slave.c:定义静态对象字典结构
- Ds402_SlaveCb.c:实现字典访问回调方法
2.2 关键对象字典项配置
2.2.1 通信参数区配置
c复制// 设备类型标识 (0x1000)
UNS32 Ds402_Slave_obj1000 = 0x22; // CiA402驱动器类型代码
// 心跳配置 (0x1017)
UNS32 Ds402_Slave_obj1017 = 1000; // 心跳周期1000ms
// SDO服务器参数 (0x1200)
UNS32 Ds402_Slave_obj1200_COB_ID_ClientToServer = 0x600 + NODE_ID;
UNS32 Ds402_Slave_obj1200_COB_ID_ServerToClient = 0x580 + NODE_ID;
2.2.2 CiA402专用参数
c复制// 控制字和状态字 (CiA402核心)
UNS16 Ds402_Slave_obj6040 = 0x0000; // 控制字初始值
UNS16 Ds402_Slave_obj6041 = 0x0000; // 状态字初始值
// 操作模式配置
UNS8 Ds402_Slave_obj6060 = 0xFF; // 操作模式选择
UNS8 Ds402_Slave_obj6061 = 0xFF; // 当前操作模式显示
// 运动控制参数
UNS32 Ds402_Slave_obj60FF = 0; // 目标速度
UNS32 Ds402_Slave_obj607A = 0; // 目标位置
2.3 PDO映射配置
过程数据对象(PDO)用于实时数据传输,配置时需要特别注意COB-ID和映射关系:
c复制// RPDO1接收参数 (0x1400)
UNS32 Ds402_Slave_obj1400_COB_ID_used_by_PDO = 0x200 + NODE_ID;
UNS8 Ds402_Slave_obj1400_Transmission_Type = 0xFF; // 异步传输
// RPDO1映射 (0x1600)
UNS32 Ds402_Slave_obj1600[] = {
0x60600020, // 控制字(16位)
0x60FF0020 // 目标速度(32位)
};
// TPDO1发送参数 (0x1800)
UNS32 Ds402_Slave_obj1800_COB_ID_used_by_PDO = 0x180 + NODE_ID;
UNS16 Ds402_Slave_obj1800_Event_Timer = 100; // 100ms周期发送
// TPDO1映射 (0x1A00)
UNS32 Ds402_Slave_obj1A00[] = {
0x60610020, // 状态字(16位)
0x606C0020 // 实际速度(32位)
};
注意:PDO映射一旦设置,在运行时不建议修改。如需动态配置,应先在预操作状态下完成所有PDO映射,再进入操作状态。
3. 硬件初始化与协议栈集成
3.1 CAN控制器初始化
在STM32平台上,CAN外设初始化是关键第一步:
c复制void CAN_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct;
CAN_InitTypeDef CAN_InitStruct;
// 1. 时钟使能
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);
// 2. GPIO配置
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. CAN参数配置
CAN_InitStruct.CAN_TTCM = DISABLE;
CAN_InitStruct.CAN_ABOM = ENABLE; // 自动离线管理
CAN_InitStruct.CAN_AWUM = ENABLE; // 自动唤醒
CAN_InitStruct.CAN_NART = DISABLE; // 非自动重传
CAN_InitStruct.CAN_RFLM = DISABLE;
CAN_InitStruct.CAN_TXFP = DISABLE;
CAN_InitStruct.CAN_Mode = CAN_Mode_Normal;
CAN_InitStruct.CAN_SJW = CAN_SJW_1tq;
CAN_InitStruct.CAN_BS1 = CAN_BS1_9tq;
CAN_InitStruct.CAN_BS2 = CAN_BS2_4tq;
CAN_InitStruct.CAN_Prescaler = 6; // 1MHz总线速率
CAN_Init(CAN1, &CAN_InitStruct);
// 4. 过滤器配置
CAN_FilterInitTypeDef CAN_FilterInitStruct;
CAN_FilterInitStruct.CAN_FilterNumber = 0;
CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask;
CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStruct.CAN_FilterIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;
CAN_FilterInitStruct.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStruct);
// 5. 中断使能
CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);
NVIC_EnableIRQ(USB_LP_CAN1_RX0_IRQn);
}
3.2 CAN接收中断处理
CAN接收中断是协议栈与硬件的桥梁,需要高效实现:
c复制void USB_LP_CAN1_RX0_IRQHandler(void)
{
CanRxMsg RxMessage;
Message CanFestivalMsg;
if(CAN_GetITStatus(CAN1, CAN_IT_FMP0) != RESET)
{
CAN_ClearITPendingBit(CAN1, CAN_IT_FMP0);
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
// 转换为CanFestival格式
CanFestivalMsg.cob_id = RxMessage.StdId;
CanFestivalMsg.rtr = (RxMessage.RTR == CAN_RTR_Data) ? 0 : 1;
CanFestivalMsg.len = RxMessage.DLC;
memcpy(CanFestivalMsg.data, RxMessage.Data, RxMessage.DLC);
// 分发到协议栈
canDispatch(&Ds402_Slave_Data, &CanFestivalMsg);
}
}
3.3 定时器配置
协议栈需要精确的定时服务,我使用TIM2提供1ms时基:
c复制void TIM2_Init(void)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseStruct.TIM_Prescaler = 72 - 1; // 1MHz计数频率
TIM_TimeBaseStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseStruct.TIM_Period = 1000 - 1; // 1ms中断
TIM_TimeBaseStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStruct);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_EnableIRQ(TIM2_IRQn);
TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
TimeDispatch(); // 协议栈时间处理
}
}
4. CiA402状态机实现
4.1 状态转换逻辑
CiA402定义了严格的状态转换机制,必须正确实现:
c复制void UpdateStateMachine(void)
{
static UNS8 lastState = 0;
UNS8 currentState = Ds402_Slave_obj6041 & 0x006F;
if(currentState != lastState)
{
switch(currentState)
{
case 0x0000: // Not ready to switch on
// 初始化硬件
break;
case 0x0040: // Switch on disabled
// 允许参数配置
break;
case 0x0021: // Ready to switch on
// 准备使能
break;
case 0x0023: // Switched on
// 电机上电但未使能
break;
case 0x0027: // Operation enabled
// 正常运行状态
break;
case 0x0007: // Fault
// 错误处理
break;
}
lastState = currentState;
}
}
4.2 控制字处理
控制字(0x6040)是状态转换的触发源,需要仔细解析:
c复制void ProcessControlWord(UNS16 cw)
{
static UNS16 lastCW = 0;
if(cw == lastCW) return;
// 状态转换条件判断
if((cw & 0x000F) == 0x0006) // Shutdown
{
// 过渡到"Ready to switch on"
}
else if((cw & 0x000F) == 0x0007) // Switch on
{
// 过渡到"Switched on"
}
else if((cw & 0x000F) == 0x000F) // Enable operation
{
// 过渡到"Operation enabled"
}
else if((cw & 0x000F) == 0x0000) // Disable
{
// 过渡回初始状态
}
lastCW = cw;
}
5. 运动控制功能实现
5.1 速度模式实现
c复制void VelocityModeHandler(void)
{
if(Ds402_Slave_obj6061 != 0x02) return; // 非速度模式
INT32 targetVel = (INT32)Ds402_Slave_obj60FF;
INT32 actualVel = 0;
// 这里实现速度控制算法
// ...
// 更新实际速度反馈
Ds402_Slave_obj606C = actualVel;
}
5.2 位置模式实现
c复制void PositionModeHandler(void)
{
if(Ds402_Slave_obj6061 != 0x01) return; // 非位置模式
INT32 targetPos = (INT32)Ds402_Slave_obj607A;
INT32 actualPos = ReadEncoder(); // 读取编码器
// 位置控制算法实现
// ...
// 更新实际位置反馈
Ds402_Slave_obj6064 = actualPos;
}
5.3 回零功能实现
c复制void HomingProcedure(void)
{
if(Ds402_Slave_obj6061 != 0x06) return; // 非回零模式
static UNS8 homingState = 0;
switch(homingState)
{
case 0: // 启动回零
SetHomingSpeed(Ds402_Slave_obj6099_1);
homingState = 1;
break;
case 1: // 搜索限位开关
if(ReadLimitSwitch())
{
SetHomingSpeed(Ds402_Slave_obj6099_2);
homingState = 2;
}
break;
case 2: // 搜索零脉冲
if(ReadIndexPulse())
{
SetPosition(0); // 重置位置
homingState = 0;
Ds402_Slave_obj6041 |= 0x1000; // 回零完成标志
}
break;
}
}
6. 调试与测试技巧
6.1 常见问题排查
-
CAN通信失败
- 检查终端电阻:总线两端应有120Ω终端电阻
- 验证波特率:确保所有节点波特率一致
- 检查CAN控制器初始化:特别是过滤器配置
-
PDO不更新
- 确认节点处于操作状态
- 检查PDO映射是否正确
- 验证COB-ID计算是否正确
-
状态机不转换
- 检查控制字发送顺序
- 确认状态字更新逻辑
- 验证操作模式设置
6.2 性能优化建议
-
中断优化
- 保持CAN中断处理尽可能短
- 将耗时操作移到主循环
- 使用DMA传输CAN数据
-
协议栈调优
- 调整PDO事件时间
- 合理设置SYNC周期
- 优化对象字典访问
-
实时性保障
- 为CAN中断分配高优先级
- 使用硬件定时器生成SYNC
- 避免在临界区执行耗时操作
7. 项目总结与扩展
通过这个项目,我实现了完整的CiA402从站功能,包括:
- 标准对象字典配置
- PDO/SDO通信
- CiA402状态机
- 多种运动控制模式
在实际应用中,还可以进一步扩展:
- 增加LSS协议支持,实现节点ID动态配置
- 实现SDO块传输,提高大数据量传输效率
- 添加EDS文件支持,方便主站配置
对于资源受限的STM32F103平台,需要注意:
- 合理规划对象字典大小
- 优化协议栈内存使用
- 平衡功能完整性和性能需求
这个实现方案已经成功应用于多个工业伺服驱动项目,稳定运行超过10000小时。关键是要严格遵循CiA402标准,同时根据实际硬件特性进行适当优化。
