【双 MCU 架构车载仪表终端(CAN 互联)】STM32F1 CAN 回环收发测试

1. 引言

在进行双机互发之前,必须单机本地进行回环测试,以确保发送和接收数据没有问题。

2. CAN CubeMX 配置

基础参数配置:
image

参数解释:

  • Bit Timings Parameters【位时序参数】
    • Prescaler (for Time Quantum):预分频器(用于时间份额 tq),对 CAN 内核时钟分频,生成基础时间单位 tq(CAN 时钟源为 APB1 外设时钟 36MHz)。选较大值=tq 变长、波特率降低;选较小值=tq 变短、波特率提高。
    • Time Quantum:时间份额 (tq),CAN 位时序最小时间单位。它由内核时钟和预分频决定:预分频越大,tq 越长;预分频越小,tq 越短。
    • Time Quanta in Bit Segment 1:位段 1 的 tq 个数(BS1,包含传播段 + 相位缓冲段 1,决定采样点位置)。选较大值=采样点后移,单 bit 时间变长;选较小值=采样点前移,单 bit 时间变短。
    • Time Quanta in Bit Segment 2:位段 2 的 tq 个数(BS2,相位缓冲段 2)。选较大值=单 bit 时间变长、采样点比例相对前移;选较小值=单 bit 时间变短、采样点比例相对后移。
    • Time for one Bit:单个 bit 总时长(SyncSeg 固定 1 tq + BS1 + BS2)。BS1/BS2 增大则单 bit 时间变长、波特率降低;减小则单 bit 时间变短、波特率提高。
    • Baud Rate:波特率。由 tq 和单 bit 总 tq 数决定:Prescaler、BS1、BS2 增大会降低波特率;减小会提高波特率。
    • ReSynchronization Jump Width:再同步跳转宽度 (SJW),总线同步时单次最多调整的 tq 数量。选较大值=允许更大的相位误差补偿;选较小值=同步更严格、补偿范围更小。
  • Basic Parameters【基础参数】
    • Time Triggered Communication Mode:时间触发通信模式(TTCAN,基于时间调度发送报文,汽车 AUTOSAR 常用)。选 Enable=启用时间触发模式,基于内部时间基准调度发送并记录时间戳;选 Disable=使用标准的基于事件触发的 CAN 通信模式。
    • Automatic Bus-Off Management:自动 Bus-Off 管理。选 Enable=出错进入 Bus-Off 后硬件自动恢复,不需要软件干预;选 Disable=进入 Bus-Off 后需要软件手动清除标志并恢复。
    • Automatic Wake-Up Mode:自动唤醒模式。选 Enable=休眠时检测到总线活动自动唤醒外设;选 Disable=不自动唤醒,需要软件介入或外部触发。
    • Automatic Retransmission:自动重传。选 Enable=仲裁丢失/发送出错时硬件自动重试直至成功(会一直占用某个邮箱进行重发,不会自动释放);选 Disable=只发送一次,然后释放邮箱(单发模式/one-shot),失败不重发,避免阻塞总线。
    • Receive Fifo Locked Mode:接收 Fifo 锁定模式。选 Enable=FIFO 满时锁定,不再覆盖旧报文(保护旧数据);选 Disable=覆盖最早报文(始终接收最新数据)。
    • Transmit Fifo Priority:发送 Fifo 优先级。选 Enable=按 FIFO 入队顺序发送;选 Disable=按 CAN ID 仲裁优先级发送(通常 ID 越小优先级越高)。
  • Advanced Parameters【高级参数】
    • Operating Mode:工作模式。选 Normal=正常收发;选 Loopback=环回自测模式(发送帧在内部回送,不驱动引脚);选 Silent=静默模式(只听,不发送,不干扰总线);选 Silent_Loopback=静默+回环自测模式(环回且不影响总线)。

中断配置:
image

  • USB high priority or CAN TX interrupts:CAN 发送报文完成/中止
  • USB low priority or CAN RX0 interrupts:CAN FIFO0 接收报文
  • CAN RX1 interrupt:CAN FIFO1 接收报文
  • CAN SCE interrupt:状态变化与错误:Bus-Off、错误被动、错误计数超限、唤醒/睡眠事件

3. 代码编写

CubeMX 生成代码后,CAN 的过滤器和中断需要手动开启。

3.1 配置并使能过滤器

在 CAN 初始化后的 /* USER CODE BEGIN CAN_Init 2 */ 进行配置和使能 CAN 的过滤器。

/* CAN init function */
void MX_CAN_Init(void)
{

  /* USER CODE BEGIN CAN_Init 0 */

  /* USER CODE END CAN_Init 0 */

  /* USER CODE BEGIN CAN_Init 1 */

  /* USER CODE END CAN_Init 1 */
  hcan.Instance = CAN1;
  hcan.Init.Prescaler = 3;
  hcan.Init.Mode = CAN_MODE_LOOPBACK;
  hcan.Init.SyncJumpWidth = CAN_SJW_2TQ;
  hcan.Init.TimeSeg1 = CAN_BS1_8TQ;
  hcan.Init.TimeSeg2 = CAN_BS2_3TQ;
  hcan.Init.TimeTriggeredMode = DISABLE;
  hcan.Init.AutoBusOff = ENABLE;
  hcan.Init.AutoWakeUp = ENABLE;
  hcan.Init.AutoRetransmission = ENABLE;
  hcan.Init.ReceiveFifoLocked = DISABLE;
  hcan.Init.TransmitFifoPriority = DISABLE;
  if (HAL_CAN_Init(&hcan) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN CAN_Init 2 */
  CAN_FilterTypeDef CAN_FilterInitStruct;
  CAN_FilterInitStruct.FilterBank = 0;
  /**
   * @brief 四种过滤器
   *        1. 16 位列表模式:四个参数分别存入一组 ID 即可
   *        2. 16 位掩码模式:IdHigh 存入第一组 ID,MaskIdHigh 存入对应掩码;IdLow 存入第二组 ID,MaskIdLow 存入对应掩码
   *        3. 32 位列表模式:IdHigh 和 IdLow 组合到一起,存入第一组 32 位 ID;MaskIdHigh 和 MaskIdLow 组合到一起,存入第二组 32 位 ID
   *        4. 32 位掩码模式:IdHigh 和 IdLow 组合到一起,存入第一组 32 位 ID;MaskIdHigh 和 MaskIdLow 组合到一起,存入 对应掩码位
   *        5. 全通模式:IdHigh 和 IdLow 都存入 0x0000,MaskIdHigh 和 MaskIdLow 都存入 0xFFFF,前提是 32 位掩码模式
   */
  CAN_FilterInitStruct.FilterIdHigh = 0x0000;
  CAN_FilterInitStruct.FilterIdLow = 0x0000;
  CAN_FilterInitStruct.FilterMaskIdHigh = 0x0000;
  CAN_FilterInitStruct.FilterMaskIdLow = 0x0000;
  CAN_FilterInitStruct.FilterScale = CAN_FILTERSCALE_32BIT;
  CAN_FilterInitStruct.FilterMode = CAN_FILTERMODE_IDMASK;
  CAN_FilterInitStruct.FilterFIFOAssignment = CAN_FILTER_FIFO0;
  CAN_FilterInitStruct.FilterActivation = ENABLE;
  if (HAL_CAN_ConfigFilter(&hcan, &CAN_FilterInitStruct) != HAL_OK)
  {
    LOG_ERROR("CAN", "ConfigFilter failed");
    Error_Handler();
  }
  /* USER CODE END CAN_Init 2 */
}

3.1 使能中断

使能过滤器后加上 CAN FIFO0 接收中断的使能:

/* 使能接收 FIFO 0 中断 */
if (HAL_CAN_ActivateNotification(&hcan, CAN_IT_RX_FIFO0_MSG_PENDING) != HAL_OK)
{
  Error_Handler();
}

3.2 启动 CAN

中断使能后才能启动 CAN:

/* 启动 CAN 模块 */
if (HAL_CAN_Start(&hcan) != HAL_OK)
{
  LOG_ERROR("CAN", "Start failed");
  Error_Handler();
}

3.3 发送与接收代码编写

新建 app_can.c/h 文件后进行编写,这里接收部分代码使用了环形缓冲区的方法,后续会进行讲解。

#include "app_can.h"
#include "mylog.h"
#include "can.h"
#include <string.h>

#define CAN_RX_BUF_SIZE 16 // 软件接收缓冲区深度(帧),16 对单帧间隔收发足够

typedef struct
{
    uint32_t ID;     // 报文 ID(标准或扩展)
    uint8_t Length;  // 数据长度
    uint8_t Data[8]; // 数据域
} CAN_FrameTypeDef;

static CAN_FrameTypeDef CAN_RxBuf[CAN_RX_BUF_SIZE];   // 环形缓冲区
static volatile uint8_t CAN_RxHead = 0;               // 写指针,仅中断回调修改
static volatile uint8_t CAN_RxTail = 0;               // 读指针,仅主循环修改

/**
 * @brief 发送 CAN 消息
 * @param ID 标准 ID 号
 * @param Length 数据长度
 * @param TxData 数据指针
 */
void CAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *TxData)
{
    CAN_TxHeaderTypeDef TxMessage; // 发送消息结构体
    uint32_t TxMailbox;            // 接收返回存储 Tx 消息的邮箱号

    /* 填充帧头信息 */
    TxMessage.StdId = ID;         // 标准 ID 号
    TxMessage.ExtId = ID;         // 扩展 ID 号
    TxMessage.IDE = CAN_ID_STD;   // 扩展 ID 标志位,选择标准 ID 类型
    TxMessage.RTR = CAN_RTR_DATA; // 数据帧 & 遥控帧
    TxMessage.DLC = Length;       // 数据长度

    /* 打印当前空闲邮箱数量 */
    LOG_INFO("CAN", "Free Tx mailbox count: %lu", HAL_CAN_GetTxMailboxesFreeLevel(&hcan));

    /* 将发送消息添加到发送邮箱队列并激活相应的传输请求 */
    uint8_t result = HAL_CAN_AddTxMessage(&hcan, &TxMessage, TxData, &TxMailbox);
    if (result != HAL_OK)
    {
        LOG_ERROR("CAN", "Add TxMessage to Mailbox or Activate transmission request Failed, return");
        return;
    }
    LOG_INFO("CAN", "Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x%02X", TxMailbox);
}

/**
 * @brief 检查软件接收缓冲区是否为空
 * @param None
 * @return uint8_t 1: 为空, 0: 不为空
 */
uint8_t CAN_RxFIFO_isEmpty(void)
{
    if (CAN_RxHead == CAN_RxTail) // 软件缓冲区为空
    {
        LOG_WARN("CAN", "CAN RxFIFO is empty");
        return 1;
    }
    LOG_INFO("CAN", "CAN RxFIFO not empty");
    return 0;
}

/**
 * @brief 接收 CAN 消息
 * @param ID 标准 ID 号指针
 * @param Length 数据长度指针
 * @param RxData 数据指针
 */
void CAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *RxData)
{
    CAN_FrameTypeDef *frame = &CAN_RxBuf[CAN_RxTail];

    *ID = frame->ID;
    *Length = frame->Length;
    memcpy(RxData, frame->Data, frame->Length);

    CAN_RxTail = (CAN_RxTail + 1) % CAN_RX_BUF_SIZE;
}

/**
 * @brief FIFO0 收到报文的中断回调(中断上下文,禁止调用 mylog)
 */
void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcanPtr)
{
    CAN_RxHeaderTypeDef RxMessage;
    CAN_FrameTypeDef frame;

    /* 从 FIFO FIFO0 读取消息 */
    if (HAL_CAN_GetRxMessage(hcanPtr, CAN_RX_FIFO0, &RxMessage, frame.Data) == HAL_OK)
    {
        frame.ID = (RxMessage.IDE == CAN_ID_STD) ? RxMessage.StdId : RxMessage.ExtId; // 读出标准 ID 号或扩展 ID 号
        frame.Length = RxMessage.DLC;                                                 // 数据长度

        uint8_t next = (CAN_RxHead + 1) % CAN_RX_BUF_SIZE;
        if (next != CAN_RxTail) // 缓冲区未满才入队,满了丢弃 newest
        {
            CAN_RxBuf[CAN_RxHead] = frame;
            CAN_RxHead = next;
        }
    }
}

3.4 环形缓冲区入队原理

基本设定:

生产者 = CAN 接收中断,只推进 H(写指针)      ← 调用入队代码
消费者 = 主循环 CAN_Receive(),只推进 T(读指针) ← 调用出队代码
一格   = 一条完整 CAN 帧(不是 1 字节)
·      = 空格子        F1、F2… = 已存入的帧      F1* = 已被取走、逻辑作废

流程图:

入队代码就三步,后面每张图都对应它:
next = (H + 1) % 8;        // ① 探路:算出 H 的下一格(绕回就是它实现的)
if (next != T) {           // ② 判满:下一格没顶到 T 才能写
    buf[H] = frame;        // ③a 先写数据
    H = next;              // ③b 后推指针(宣告这格就绪)
}
第 0 步:开机,缓冲区空
       0     1     2     3     4     5     6     7
     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │  ·  │  ·  │  ·  │  ·  │  ·  │  ·  │  ·  │  ·  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
        ▲
      T=H=0                          ← H == T,判定"空"
第 1 步:中断收到 F1(走一遍入队三步)
next = (0+1)%8 = 1    1 ≠ T(0) → 未满
写入 buf[0] → H=1

     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F1  │  ·  │  ·  │  ·  │  ·  │  ·  │  ·  │  ·  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
        ▲     ▲
       T=0   H=1
第 2 步:又连收 F2、F3(每次重复三步,H 每次走一格)
     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F1  │ F2  │ F3  │  ·  │  ·  │  ·  │  ·  │  ·  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
        ▲                    ▲
       T=0                  H=3

缓冲区现有帧数 = 从 T 数到 H 的前一格 = 3 帧
第 3 步:主循环取走 F1(消费者的动作,和入队互为镜像)
读 buf[0] 得到 F1 → T=1

     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F1* │ F2  │ F3  │  ·  │  ·  │  ·  │  ·  │  ·  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
              ▲          ▲
             T=1        H=3

F1 的字节还躺在内存里,但逻辑上已作废,[0] 号格可在下次写入时直接覆盖
第 4 步:中断连续收 F4~F8,写到末尾后绕回
依次写 buf[3]=F4 … buf[7]=F8,写完 buf[7] 时:
next = (7+1)%8 = 0    ← % 运算把指针接回数组开头,"环形"生效 → H=0

     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F1* │ F2  │ F3  │ F4  │ F5  │ F6  │ F7  │ F8  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
        ▲     ▲
       H=0   T=1
此时存了 7 帧 = 8−1,已达上限。检查:next=(0+1)%8=1 == T(1) → 满。
第 5 步:再来 F9 —— 撞上满条件,丢弃
next = (0+1)%8 = 1 == T(1) → if 不成立 → F9 整帧丢弃

     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F1* │ F2  │ F3  │ F4  │ F5  │ F6  │ F7  │ F8  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
        ▲     ▲
       H=0   T=1          ← H、T、格子内容全部原封不动
旧帧 F2~F8 全部保住,被扔掉的是最新的 F9。
第 6 步:主循环取走 F2,空位出现,写入恢复
读 buf[1] 得到 F2 → T=2
此时 next=(0+1)%8=1 ≠ T(2) → 未满 → F10 可以入队:写 buf[0],H=1

     ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
     │ F10 │ F2* │ F3  │ F4  │ F5  │ F6  │ F7  │ F8  │
     └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
              ▲     ▲
             H=1   T=2
注意 F10 恰好覆盖了最早作废的 F1* 那一格——空位就是这么复用的。

3.5 主函数调用

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration--------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  MX_CAN_Init();
  /* USER CODE BEGIN 2 */
  LOG_INFO("APP", "APP Boot ============================================================================");
  LOG_INFO("SYSTEM", "main() user init OK");
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    HAL_Delay(1000);
    LOG_INFO("SYSTEM", "running");
    CAN_Transmit(0x123, 8, (uint8_t *)"Hello!");
    if (!CAN_RxFIFO_isEmpty())
    {
      uint32_t ID = 0;
      uint8_t Length = 0;
      uint8_t RxData[8] = {0};
      CAN_Receive(&ID, &Length, RxData);
      LOG_INFO("CAN", "ID: %d, Length: %d, Data: %02X %02X %02X %02X %02X %02X %02X %02X", 
        ID, 
        Length, 
        RxData[0], 
        RxData[1], 
        RxData[2], 
        RxData[3], 
        RxData[4], 
        RxData[5], 
        RxData[6], 
        RxData[7]);
    }
    CAN_ProcessEvents();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

4. 串口打印消息

[I][APP] APP Boot ============================================================================
[I][SYSTEM] main() user init OK
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
[I][SYSTEM] running
[I][CAN] Free Tx mailbox count: 3
[I][CAN] Add TxMessage to Mailbox and Activate transmission request Succeed, Mailbox: 0x01
[I][CAN] CAN RxFIFO not empty
[I][CAN] ID: 291, Length: 8, Data: 48 65 6C 6C 6F 21 00 00
...

博客导航

博客导航

posted @ 2026-11-01 00:31  膝盖中箭卫兵  阅读(5)  评论(0)    收藏  举报
ORCID iD icon https://orcid.org/0000-0001-5102-772X