CANOpen 移植+调试 LINUX(主站)+STM32(从站)

LINUX 主站编译

  1. 拉取CANopennode代码

    bash 复制代码
    git clone https://github.com/CANopenNode/CANopenNode.git
    cd CANopenNode
  2. 编译

    bash 复制代码
    make
  3. 执行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. 生成工程

  1. 使用STM32CubeMX配置上述参数
  2. 生成Makefile工程
  3. 生成代码

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.cmain.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 验证移植成功
  1. 编译无错误 : 运行make命令,确保没有编译错误
  2. 烧录成功: 程序能够成功烧录到STM32F407
  3. 串口输出: 连接UART1到串口调试工具,波特率115200,看到初始化输出
  4. LED控制: 通过串口命令或SDO能够控制LED状态
  5. 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总线测试
  1. 连接CAN总线到CAN分析仪或主站设备

  2. 使用CAN分析仪发送NMT命令:

    • 启动节点: ID 0x000, 数据 0x01 0x01 (启动节点1)
    • 进入预操作状态: ID 0x000, 数据 0x02 0x01
    • 停止节点: ID 0x000, 数据 0x00 0x01
  3. 检查节点状态:

    • 发送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 完整测试流程
  1. 启动canopend主站
  2. 启动STM32从站节点(通过NMT命令)
  3. 使用SDO读取设备信息(对象0x1000-0x1018)
  4. 使用SDO写入LED控制值
  5. 使用SDO读取LED状态验证
  6. 监控心跳消息(对象0x1017)
  7. 测试PDO通信(如果配置)
8.7 测试结果验证

成功标准:

  • ✅ canopend启动正常,无错误消息
  • ✅ 能够通过SDO读写对象字典
  • ✅ LED状态随SDO写入正确变化
  • ✅ 能够接收心跳消息
  • ✅ CAN总线无错误帧

9. 常见问题和解决方案

9.1 CAN初始化失败
  • 问题 : HAL_CAN_Init()返回错误
  • 解决方案 :
    1. 检查CAN时钟配置
    2. 确认CAN引脚配置正确
    3. 检查波特率计算公式
9.2 定时器中断不触发
  • 问题: CANopen时钟不更新
  • 解决方案 :
    1. 确认TIM14中断已使能
    2. 检查中断优先级配置
    3. 确认HAL_TIM_Base_Start_IT(&htim14)已调用
9.3 串口无输出
  • 问题: printf无输出
  • 解决方案 :
    1. 检查UART引脚配置
    2. 确认重定向函数已正确实现
    3. 检查波特率设置
9.4 CAN通信失败
  • 问题: 无法接收/发送CAN消息
  • 解决方案 :
    1. 检查CAN过滤器配置
    2. 确认波特率匹配
    3. 检查CAN总线终端电阻

10. 性能优化建议

  1. 中断优先级: 将TIM14中断设置为较高优先级
  2. 缓冲区大小: 根据应用需求调整CAN缓冲区大小
  3. 电源管理: 在空闲时降低功耗
  4. 错误处理: 添加完善的错误处理和恢复机制

11. 参考文档

优化建议

  1. 中断优先级: 将TIM14中断设置为较高优先级
  2. 缓冲区大小: 根据应用需求调整CAN缓冲区大小
  3. 电源管理: 在空闲时降低功耗
  4. 错误处理: 添加完善的错误处理和恢复机制

11. 参考文档

相关推荐
文静小土豆1 小时前
Linux 进程终止指南:理解 kill 与 kill -9 的核心区别与正确用法
linux·运维·服务器
不懒不懒1 小时前
安装python3.9.7和pycharm-community-2022.3.2.exe以及linux
linux·ide·python·pycharm
IMPYLH1 小时前
Linux 的 df 命令
linux·运维·服务器
Xueqian E1 小时前
驱动策略和效率的整理
stm32·单片机·嵌入式硬件
wefg11 小时前
【Linux】会话、终端、前后台进程
linux·运维·服务器
zhixingheyi_tian1 小时前
Linux/Windows 免密登录
linux·运维·服务器
尤老师FPGA2 小时前
petalinux制作linux系统flash+sd卡启动
linux·运维·服务器
蓝天居士2 小时前
Linux实用功能代码集(4) —— 线程间消息队列(2)
linux
Name_NaN_None2 小时前
Linux 使用 Remmina 连接 Windows 远程桌面 ——「小白教程」
linux·网络·电脑·远程工作
shepherd1112 小时前
别再无脑 cat 了!后端排查 GB 级生产日志的实战命令
linux·后端