LINUX 主站编译
-
拉取CANopennode代码
bashgit clone https://github.com/CANopenNode/CANopenNode.git cd CANopenNode -
编译
bashmake -
执行canopend
bash./canopend can0 1
移植STM32 从站
1. 工程概述
本工程基于STM32F407VG芯片,使用CANopenNode库实现CANopen从站功能。主要特性:
- 节点ID: 1
- 波特率: 250kbps
- 功能: LED控制、心跳、SDO/PDO通信
- 对象字典: 包含标准设备信息和用户自定义对象
工程结构如下:
STM32_CAN/
├── Core/
│ ├── CanOpenSTM32/ # CANopenNode库及STM32驱动
│ │ ├── CANopenNode/ # CANopenNode核心库
│ │ └── CANopenNode_STM32/ # STM32专用驱动
│ ├── Inc/ # 头文件 (main.h, OD.h等)
│ └── Src/ # 源文件 (main.c, OD.c等)
├── Drivers/ # STM32 HAL驱动
├── build/ # 编译输出
├── Makefile # 编译配置
└── CMakeLists.txt # CMake配置
2. 配置STM32F407
2.1 时钟配置 (RCC)
- 系统时钟: 168MHz (HSE 25MHz 根据实际晶振决定, PLL倍频)
- APB1时钟: 42MHz (CAN1使用APB1总线)
- APB2时钟 : 84MHz

2.2 CAN1配置
- 模式: CAN_MODE_NORMAL (正常模式)
- 波特率: 250kbps
- 预分频器: 21 (168MHz / 21 / (1 + 3 + 4) = 250kbps)
- 时间段 :
- SJW: 1 TQ
- BS1: 3 TQ
- BS2: 4 TQ
- 中断 : 使能can1所有中断

2.3 定时器配置 (TIM14)
- 用途: 生成1ms中断用于CANopen时钟
- 预分频器: 42 (42MHz / 42 = 1MHz)
- 周期 : 999 (1MHz / 1000 = 1kHz, 即1ms)

2.4 堆栈配置
- 堆大小: 0x2000 (8KB)
- 栈大小: 0x800 (2KB)
2.5 UART配置 (用于调试输出)
- UART1: 波特率115200, 8N1, 用于printf重定向
3. 生成工程
- 使用STM32CubeMX配置上述参数
- 生成Makefile工程
- 生成代码
4. 移植CanOpenSTM32库
4.1 获取CANopenNode库
bash
# 在Core目录下克隆CanOpenSTM32库
cd Core
git clone https://github.com/hamedjafarzadeh/CanOpenSTM32.git
cd CanOpenSTM32
git submodule update --init --recursive
4.2 配置Makefile
在Makefile中添加CANopenNode源文件:
makefile
# CANopenNode sources
C_SOURCES += \
Core/CanOpenSTM32/CANopenNode_STM32/CO_storageBlank.c \
Core/CanOpenSTM32/CANopenNode_STM32/CO_app_STM32.c \
Core/CanOpenSTM32/CANopenNode_STM32/CO_driver_STM32.c \
Core/CanOpenSTM32/CANopenNode/301/CO_ODinterface.c \
Core/CanOpenSTM32/CANopenNode/301/CO_NMT_Heartbeat.c \
Core/CanOpenSTM32/CANopenNode/301/CO_HBconsumer.c \
Core/CanOpenSTM32/CANopenNode/301/CO_Emergency.c \
Core/CanOpenSTM32/CANopenNode/301/CO_SDOserver.c \
Core/CanOpenSTM32/CANopenNode/301/CO_TIME.c \
Core/CanOpenSTM32/CANopenNode/301/CO_SYNC.c \
Core/CanOpenSTM32/CANopenNode/301/CO_PDO.c \
Core/CanOpenSTM32/CANopenNode/303/CO_LEDs.c \
Core/CanOpenSTM32/CANopenNode/305/CO_LSSslave.c \
Core/CanOpenSTM32/CANopenNode/storage/CO_storage.c \
Core/CanOpenSTM32/CANopenNode/CANopen.c \
Core/Src/OD.c \
Core/CanOpenSTM32/CANopenNode/extra/CO_trace.c
4.3 配置包含路径
在Makefile中添加头文件包含路径:
makefile
C_INCLUDES = \
-ICore/Inc \
-ICore/CanOpenSTM32/CANopenNode_STM32 \
-ICore/CanOpenSTM32/CANopenNode \
-ICore/CanOpenSTM32/CANopenNode/301 \
-ICore/CanOpenSTM32/CANopenNode/303 \
-ICore/CanOpenSTM32/CANopenNode/305 \
-ICore/CanOpenSTM32/CANopenNode/storage \
-ICore/CanOpenSTM32/CANopenNode/extra \
-IDrivers/CMSIS/Device/ST/STM32F4xx/Include \
-IDrivers/CMSIS/Include \
-IDrivers/STM32F4xx_HAL_Driver/Inc \
-IDrivers/STM32F4xx_HAL_Driver/Inc/Legacy
4.4 对象字典配置
CANopen对象字典由CANopenEditor生成,文件位于:
Core/Inc/OD.h- 对象字典头文件Core/Src/OD.c- 对象字典实现
对象字典包含以下主要对象:
- 0x1000-0x1018: 设备信息
- 0x1019: 节点ID
- 0x1020: 时间对象
- 0x1021: 存储参数
- 0x6000: 用户定义对象 (LED控制)
5. 主程序集成
5.1 main.c中的CANopen初始化
在main()函数的USER CODE BEGIN 2区域添加CANopen初始化代码:
c
/* USER CODE BEGIN 2 */
// 配置系统滴答定时器
HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/1000);
// 初始化CANopen节点结构体
CANopenNodeSTM32 canOpenNodeSTM32;
canOpenNodeSTM32.CANHandle = &hcan1;
canOpenNodeSTM32.HWInitFunction = MX_CAN1_Init;
canOpenNodeSTM32.timerHandle = &htim14;
canOpenNodeSTM32.desiredNodeID = 1; // 节点ID
canOpenNodeSTM32.baudrate = 250; // 波特率250kbps
// 输出初始化信息
printf("Starting CANopen initialization...\n");
printf("CAN1 Instance: 0x%08lX\n", (uint32_t)hcan1.Instance);
printf("CAN1 State: %d\n", hcan1.State);
// 初始化CANopen栈
canopen_app_init(&canOpenNodeSTM32);
/* USER CODE END 2 */
5.2 主循环中的CANopen处理
在while(1)循环中添加CANopen处理函数:
c
/* USER CODE BEGIN WHILE */
while (1)
{
// CANopen主处理函数
canopen_app_process();
// 检查对象字典中的LED控制值
static uint32_t last_led_value = 0xFFFFFFFF;
if (OD_RAM.x6001_LEDCtrl != last_led_value) {
// LED控制值变化
uint8_t led_state = (uint8_t)(OD_RAM.x6001_LEDCtrl & 0xff);
LED_Control(led_state);
last_led_value = OD_RAM.x6001_LEDCtrl;
}
// 其他应用代码...
}
/* USER CODE END WHILE */
5.3 定时器中断处理
在stm32f4xx_it.c或main.c中添加定时器中断回调函数:
c
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef* htim) {
if (htim == &htim14) {
// CANopen时钟中断处理
canopen_app_interrupt();
}
}
5.4 添加CAN过滤
在MX_CAN1_Init中/* USER CODE BEGIN CAN1_Init 2 */下添加:
c
/* USER CODE BEGIN CAN1_Init 2 */
CAN_FilterTypeDef filter;
filter.FilterActivation = ENABLE;
filter.FilterBank = 0;
filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
filter.FilterIdLow = 0;
filter.FilterMaskIdLow = 0x08;
filter.FilterMode = CAN_FILTERMODE_IDMASK;
filter.FilterScale = CAN_FILTERSCALE_32BIT;
HAL_CAN_ConfigFilter(&hcan1, &filter);
/* USER CODE END CAN1_Init 2 *
5.5 printf重定向
在main.c中重定向printf到UART1:
c
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif
PUTCHAR_PROTOTYPE
{
if (ch == '\n') {
HAL_UART_Transmit(&huart1, (uint8_t *)"\r", 1, 0xffff);
}
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
return ch;
}
5.6 头文件包含
在main.c中添加必要的头文件:
c
/* USER CODE BEGIN Includes */
#include "stdio.h"
#include "stm32f407xx.h"
#include "stm32f4xx_hal.h"
#include "stm32f4xx_hal_cortex.h"
#include "stm32f4xx_hal_gpio.h"
#include "stm32f4xx_hal_uart.h"
#include "CO_app_STM32.h"
#include "OD.h"
#include <ctype.h>
#include <stdint.h>
#include <string.h>
/* USER CODE END Includes */
5.7 LED控制功能(可选)
添加LED控制函数,用于通过对象字典控制LED:
c
void LED_Control(uint16_t state) {
int i = 0;
// Define LED pins array (PE6-PE0)
struct GPIO_PinAttributes led_pins[] = {
{GPIOE, GPIO_PIN_6}, // LED0
{GPIOE, GPIO_PIN_5}, // LED1
{GPIOE, GPIO_PIN_4}, // LED2
{GPIOE, GPIO_PIN_4}, // LED3
{GPIOE, GPIO_PIN_3}, // LED4
{GPIOE, GPIO_PIN_2}, // LED5
{GPIOE, GPIO_PIN_2}, // LED6
{GPIOE, GPIO_PIN_1}, // LED7
{GPIOE, GPIO_PIN_0}, // LED8
{GPIOB, GPIO_PIN_9}, // LED9
{GPIOC, GPIO_PIN_3}, // LED10
{GPIOF, GPIO_PIN_9}, // LED11
{GPIOF, GPIO_PIN_8}, // LED12
{GPIOF, GPIO_PIN_7}, // LED13
{GPIOF, GPIO_PIN_6}, // LED14
{GPIOC, GPIO_PIN_15}, // LED15
{GPIOC, GPIO_PIN_14}, // LED16
{GPIOC, GPIO_PIN_13}, // LED17
};
for(i = 0; i < 16; i++) {
HAL_GPIO_WritePin(led_pins[i].GPIOX, led_pins[i].GPIO_Pin, (state & (1 << i)) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
}
注:LED GPIO自行配置
6. 编译和烧录
6.1 编译工程
使用Makefile编译工程:
bash
make GCC_PATH=xxxx
6.2 烧录程序
使用OpenOCD烧录程序:
bash
<openocd-dir>/bin/openocd \
-f <openocd-dir>/openocd/scripts/interface/stlink.cfg \
-f <openocd-dir>/openocd/scripts/target/stm32f4x.cfg \
-c "program build/STM32_CAN.elf verify exit"
6.3 清理编译文件
bash
make clean
7. 调试和测试
7.1 验证移植成功
- 编译无错误 : 运行
make命令,确保没有编译错误 - 烧录成功: 程序能够成功烧录到STM32F407
- 串口输出: 连接UART1到串口调试工具,波特率115200,看到初始化输出
- LED控制: 通过串口命令或SDO能够控制LED状态
- CAN通信: 能够发送和接收CAN消息
7.2 串口调试输出
连接UART1 (PA9/PA10) 到串口调试工具,波特率115200,可以看到以下输出:
Starting CANopen initialization...
CAN1 Instance: 0x40006400
CAN1 State: 0
Allocated XXXX bytes for CANopen objects
CANopen initialized successfully
如果看到"CANopen initialized successfully",说明移植基本成功。
7.2 CAN总线测试
-
连接CAN总线到CAN分析仪或主站设备
-
使用CAN分析仪发送NMT命令:
- 启动节点: ID 0x000, 数据 0x01 0x01 (启动节点1)
- 进入预操作状态: ID 0x000, 数据 0x02 0x01
- 停止节点: ID 0x000, 数据 0x00 0x01
-
检查节点状态:
- 发送SDO读取请求读取对象字典
- 观察LED状态变化
7.3 对象字典访问
通过SDO读取/写入对象字典:
- 读取LED状态: SDO请求读取对象 0x6001
- 写入LED控制: SDO写入对象 0x6001
8. 通信命令测试
8.1 CAN总线配置
在Linux主机上配置CAN接口:
bash
# 设置CAN接口(假设使用can0)
ip link set can0 up type can bitrate 250000 fd off
# 验证CAN接口状态
ip link show can0
8.2 启动CANopen主站
使用canopend启动CANopen主站:
bash
# 启动canopend,节点ID为4,使用标准IO接口
canopend can0 -i 4 -c "stdio"
预期输出:
canopend[4611]: CANopen device, Node ID = 0x04, starting
canopend[4611]: CANopen command interface on "standard IO" started
canopend[4611]: CAN Interface "can0" RX buffer set to 477 messages (212992 Bytes)
canopend[4611]: CANopen NMT state changed to: "initializing" (0)
canopend[4611]: CANopen device, Node ID = 0x04, communication reset
canopend[4611]: CANopen device, Node ID = 0x04, running ...
canopend[4611]: CANopen NMT state changed to: "pre-operational" (127)
canopend[4611]: CANopen Emergency message from node 0x04: errorCode=0x5000, errorRegister=0x01, errorBit=0x2F, infoCode=0xFFFFFFF3
8.3 SDO命令测试
通过SDO读写对象字典:
写入LED控制值(U8类型):
bash
1 w 0x6001 0 U8 0xff
[0] OK
读取LED控制值:
bash
1 r 0x6001 0 U8
[0] OK 0xff
注意: 如果出现数据类型不匹配错误:
bash
1 w 0x6001 0 U16 0xff
[0] ERROR:0x06070012 #Data type does not match, length of service parameter too high.
这是因为对象0x6001定义为U8类型,不能使用U16写入。
8.4 NMT命令测试
通过NMT命令控制从站节点:
启动节点1:
bash
# 发送NMT启动命令到节点1
cansend can0 000#0101
进入预操作状态:
bash
# 发送NMT预操作命令到节点1
cansend can0 000#0201
停止节点:
bash
# 发送NMT停止命令到节点1
cansend can0 000#0001
8.5 PDO测试
如果配置了PDO,可以测试PDO通信:
发送RPDO(接收PDO):
bash
# 假设RPDO1映射到对象0x6001
cansend can0 181#01020304
监控TPDO(发送PDO):
bash
# 使用candump监控TPDO
candump can0 281
8.6 完整测试流程
- 启动canopend主站
- 启动STM32从站节点(通过NMT命令)
- 使用SDO读取设备信息(对象0x1000-0x1018)
- 使用SDO写入LED控制值
- 使用SDO读取LED状态验证
- 监控心跳消息(对象0x1017)
- 测试PDO通信(如果配置)
8.7 测试结果验证
成功标准:
- ✅ canopend启动正常,无错误消息
- ✅ 能够通过SDO读写对象字典
- ✅ LED状态随SDO写入正确变化
- ✅ 能够接收心跳消息
- ✅ CAN总线无错误帧
9. 常见问题和解决方案
9.1 CAN初始化失败
- 问题 :
HAL_CAN_Init()返回错误 - 解决方案 :
- 检查CAN时钟配置
- 确认CAN引脚配置正确
- 检查波特率计算公式
9.2 定时器中断不触发
- 问题: CANopen时钟不更新
- 解决方案 :
- 确认TIM14中断已使能
- 检查中断优先级配置
- 确认
HAL_TIM_Base_Start_IT(&htim14)已调用
9.3 串口无输出
- 问题: printf无输出
- 解决方案 :
- 检查UART引脚配置
- 确认重定向函数已正确实现
- 检查波特率设置
9.4 CAN通信失败
- 问题: 无法接收/发送CAN消息
- 解决方案 :
- 检查CAN过滤器配置
- 确认波特率匹配
- 检查CAN总线终端电阻
10. 性能优化建议
- 中断优先级: 将TIM14中断设置为较高优先级
- 缓冲区大小: 根据应用需求调整CAN缓冲区大小
- 电源管理: 在空闲时降低功耗
- 错误处理: 添加完善的错误处理和恢复机制
11. 参考文档
优化建议
- 中断优先级: 将TIM14中断设置为较高优先级
- 缓冲区大小: 根据应用需求调整CAN缓冲区大小
- 电源管理: 在空闲时降低功耗
- 错误处理: 添加完善的错误处理和恢复机制