PUYA CANFD — 经典帧基址 ID 轮询收发 CANFD_Classic_BaseID_polling

PUYA PY32F403_Firmware V1.4.5 CANFD 经典帧基址 ID 轮询收发示例教学

CANFD — 经典帧基址 ID 轮询收发 CANFD_Classic_BaseID_polling

固件库: PY32F403_Firmware V1.4.5
芯片: PY32F403(Cortex-M4)⭐ F403 特色外设
源文件: PY32F403-STK/Example/CANFD/CANFD_Classic_BaseID_polling/Src/main.c
核心外设: CANFD(兼容 CAN 2.0B / CAN-FD),PA11/PA12


1. 功能简介(大白话)

这是 F403 特色外设 CANFD 的入门例子。它演示「经典 CAN 帧(非 FD)+ 轮询方式」的收发:

  • 把 CANFD 配成经典帧、正常模式、波特率 250Kbps
  • 设置过滤器:只收 ID = 0x12F 的报文(标准 11 位 ID);
  • MCU 自动发一帧 ID=0x123、8 字节数据 {1,2,3,4,5,6,7,8} 出去(给 PC 上的 PCAN-View 看);
  • 然后轮询等待接收 FIFO 有数据,一旦从 PCAN-View 收到 ID=0x12F 的 8 字节,就通过串口 printf 把数据打出来。

简单说:MCU 主动发一帧 → PC 收到;PC 手动回一帧 0x12F → MCU 收到并打印。整个过程没有中断,CPU 一直「盯着」接收 FIFO 等数据(轮询)。

2. 硬件准备

物品 说明
PY32F403-STK 开发板 自带 CANFD(PA11/PA12)+ 调试串口
USB 转 CAN 模块 + CAN 转接板 接 STK 的 CAN 差分脚
USB 转 TTL 模块 接 STK 的 USART2 看 printf
PC + PCAN-View 软件 收发 CAN 帧

CAN 接线(STK → CAN 转接板):

1
2
3
4
STK 板              CAN 转接板
PA11 (CAN_RX)  -->  R21
PA12 (CAN_TX)  -->  R20
GND            -->  GND

CAN 转接板 → USB 转 CAN 模块

1
2
CAN-H  -->  CAN-H
CAN-L  -->  CAN-L

串口接线(看 printf 输出):

1
2
3
4
STK 板          USB 转 TTL 模块
PA02 (TX)  -->  RX
PA03 (RX)  -->  TX
GND        -->  GND

PCAN-View 配置:协议 CAN 2.0,Clock Frequency 20MHz,Nominal Bit Rate 250kBit/s,Data Bit Rate 不设置,Standard。

3. 完整代码(带中文注释)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
#include "main.h"

CANFD_HandleTypeDef CanfdHandle;
CANFD_FilterTypeDef CanfdFilter;

CANFD_TxHeaderTypeDef CanfdTxHeader;
uint8_t TxData[] = {0x1,0x2,0x3,0x4,0x5,0x6,0x7,0x8};

CANFD_RxHeaderTypeDef CanfdRxHeader;
uint8_t RxData[8] = {0};

/* DLC(数据长度码) → 实际字节数 查表(FD 帧可达 64)*/
uint8_t DLCtoBytes[] = {0,1,2,3,4,5,6,7,8,12,16,20,24,32,48,64};

int main(void)
{
  uint32_t i = 0;
  HAL_Init();
  APP_SystemClockConfig();   /* 系统时钟:HSE;CAN 时钟在 MSP 里配成 20MHz */
  BSP_USART_Config();       /* 调试串口(printf 用,115200)*/

  /* ---------- CANFD 基本配置 ---------- */
  CanfdHandle.Instance                  = CANFD;
  CanfdHandle.Init.FrameFormat          = CANFD_FRAME_CLASSIC;  /* 经典帧(非 FD)*/
  CanfdHandle.Init.Mode                 = CANFD_MODE_NORMAL;     /* 正常模式 */
  CanfdHandle.Init.Prescaler            = 1U;                    /* 波特率分频 */
  CanfdHandle.Init.NominalSyncJumpWidth = 16U;                   /* 同步跳转宽度 */
  CanfdHandle.Init.NominalTimeSeg1      = 64U;                   /* 时间段1 */
  CanfdHandle.Init.NominalTimeSeg2      = 16U;                   /* 时间段2 */
  CanfdHandle.Init.DataSyncJumpWidth    = 16U;                   /* FD 数据段(此处同值)*/
  CanfdHandle.Init.DataTimeSeg1         = 64U;
  CanfdHandle.Init.DataTimeSeg2         = 16U;
  CanfdHandle.Init.SecondSamplePointOffset = 0U;
  if (HAL_CANFD_Init(&CanfdHandle) != HAL_OK) APP_ErrorHandler();

  /* ---------- 过滤器:只收 ID = 0x12F ---------- */
  CanfdFilter.IdType        = CANFD_STANDARD_ID;     /* 11 位标准 ID */
  CanfdFilter.FilterChannel = CANFD_FILTER_CHANNEL_0;
  CanfdFilter.Rank          = CANFD_FILTER_RANK_CHANNEL_NUMBER;
  CanfdFilter.FilterID      = 0x12F;                 /* 目标 ID */
  CanfdFilter.FilterFormat  = 0xFFFFFFFF;
  CanfdFilter.MaskID        = 0x0;                   /* Mask=0 → 精确匹配 0x12F */
  CanfdFilter.MaskFormat    = 0xFFFFFFFF;
  if (HAL_CANFD_ConfigFilter(&CanfdHandle, &CanfdFilter) != HAL_OK) APP_ErrorHandler();

  /* ---------- 启动 CANFD ---------- */
  if (HAL_CANFD_Start(&CanfdHandle) != HAL_OK) APP_ErrorHandler();

  /* ---------- 组装并发送一帧(PTB 优先缓冲)---------- */
  CanfdTxHeader.Identifier  = 0x123;                /* 发出去的帧 ID */
  CanfdTxHeader.IdType      = CANFD_STANDARD_ID;
  CanfdTxHeader.TxFrameType = CANFD_DATA_FRAME;      /* 数据帧 */
  CanfdTxHeader.FrameFormat = CANFD_FRAME_CLASSIC;
  CanfdTxHeader.Handle      = 0x0;
  CanfdTxHeader.DataLength  = CANFD_DLC_BYTES_8;     /* 8 字节 */
  if (HAL_CANFD_AddMessageToTxFifo(&CanfdHandle, &CanfdTxHeader, TxData,
                                   CANFD_TX_FIFO_PTB) != HAL_OK) APP_ErrorHandler();
  if (HAL_CANFD_ActivateTxRequest(&CanfdHandle, CANFD_TXFIFO_PTB_SEND) != HAL_OK)
    APP_ErrorHandler();

  /* ---------- 轮询等待接收 ---------- */
  while (__HAL_CANFD_GET_RX_FIFO_FILL_LEVEL(&CanfdHandle) == CANFD_RX_FIFO_EMPTY) {}
  if (HAL_CANFD_GetRxMessage(&CanfdHandle, &CanfdRxHeader, RxData) != HAL_OK)
    APP_ErrorHandler();

  /* ---------- 打印收到的数据 ---------- */
  printf("decimal data length: %d\r\n", DLCtoBytes[CanfdRxHeader.DataLength]);
  printf("hexadecimal data: \r\n");
  for (i = 0; i < DLCtoBytes[CanfdRxHeader.DataLength]; i++)
    printf("%x ", RxData[i]);

  while (1) {}
}

/**
  * @brief  系统时钟:HSE 作系统时钟;CAN 时钟在 MSP 中单独配成 20MHz
  */
static void APP_SystemClockConfig(void)
{
  RCC_OscInitTypeDef  OscInitstruct = {0};
  RCC_ClkInitTypeDef  ClkInitstruct = {0};

  OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  OscInitstruct.HSEState       = RCC_HSE_ON;
  OscInitstruct.HSEFreq        = RCC_HSE_16_32MHz;     /* 板载 HSE 24MHz */
  OscInitstruct.HSIState       = RCC_HSI_ON;
  OscInitstruct.HSI48MState    = RCC_HSI48M_OFF;
  OscInitstruct.LSEState       = RCC_LSE_OFF;
  OscInitstruct.LSIState       = RCC_LSI_OFF;
  OscInitstruct.PLL.PLLState   = RCC_PLL_OFF;           /* 系统时钟用 HSE 直驱 */
  if (HAL_RCC_OscConfig(&OscInitstruct) != HAL_OK) APP_ErrorHandler();

  ClkInitstruct.ClockType      = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK |
                                 RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  ClkInitstruct.SYSCLKSource   = RCC_SYSCLKSOURCE_HSE;  /* 24MHz */
  ClkInitstruct.AHBCLKDivider  = RCC_SYSCLK_DIV1;
  ClkInitstruct.APB1CLKDivider = RCC_HCLK_DIV1;
  ClkInitstruct.APB2CLKDivider = RCC_HCLK_DIV2;
  if (HAL_RCC_ClockConfig(&ClkInitstruct, FLASH_LATENCY_0) != HAL_OK) APP_ErrorHandler();
}

void APP_ErrorHandler(void) { while (1) {} }

附:MSP 里的时钟与引脚配置(HAL_CANFD_Init 自动调用)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void HAL_CANFD_MspInit(CANFD_HandleTypeDef *hcan)
{
  RCC_OscInitTypeDef OscInitstruct = {0};
  GPIO_InitTypeDef   GPIO_InitStruct = {0};

  /* PLL 来源 HSE(24M) ×5 = 120MHz */
  OscInitstruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  OscInitstruct.HSEState       = RCC_HSE_ON;
  OscInitstruct.HSEFreq        = RCC_HSE_16_32MHz;
  OscInitstruct.PLL.PLLState   = RCC_PLL_ON;
  OscInitstruct.PLL.PLLSource  = RCC_PLLSOURCE_HSE;
  OscInitstruct.PLL.PLLMUL     = RCC_PLL_MUL5;     /* 24M ×5 = 120MHz */
  HAL_RCC_OscConfig(&OscInitstruct);

  /* CAN 时钟 = PLL /6 = 20MHz(PCAN-View 也要配 20MHz)*/
  __HAL_RCC_CAN_CONFIG(RCC_CANCLKSOURCE_PLL_DIV6);

  __HAL_RCC_CAN_CLK_ENABLE();
  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* PA11=CAN_RX, PA12=CAN_TX(均 AF10,复用推挽)*/
  GPIO_InitStruct.Pin       = GPIO_PIN_11 | GPIO_PIN_12;
  GPIO_InitStruct.Mode      = GPIO_MODE_AF_PP;
  GPIO_InitStruct.Pull      = GPIO_NOPULL;
  GPIO_InitStruct.Speed     = GPIO_SPEED_FREQ_VERY_HIGH;
  GPIO_InitStruct.Alternate = GPIO_AF10_CAN;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

4. 代码分段讲解

4.1 经典帧 vs FD 帧

CANFD_FRAME_CLASSIC = 经典 CAN 2.0,单帧最多 8 字节,仲裁段与数据段同速。若改成 CANFD_FRAME_FD,则数据段可用更高波特率、最大 64 字节(对应 DLCtoBytes 表里 12~64 的值)。

4.2 波特率 250K 是怎么来的?

CAN 时钟 = 20MHz,每个 Tq(时间量子)= Prescaler / 20MHz = 50ns。一位 = (SYNC_SEG=1 + TS1=64 + TS2=16) = 81 个 Tq:

1
位时间 = 81 × 50ns ≈ 4.05µs  →  波特率 ≈ 247kHz ≈ 250Kbps

PUYA 的 SyncJumpWidth/TimeSeg1/TimeSeg2 直接以 Tq 个数为单位,所以数值偏大是正常的。FD 帧的 DataTimeSeg1/2 独立控制数据段更高速率。

4.3 过滤器 MaskID=0 的含义

过滤器只在「收到的 ID 与 FilterID(0x12F) 完全一致」时才放进 RX FIFO(MaskID=0 = 精确匹配)。MCU 自己发出的帧 ID 是 0x123(给 PC 看),而它接收的是 PC 发来的 0x12F——所以 PCAN-View 手动发 0x12F 才会被收下并打印;发别的 ID 会被硬件丢弃。

4.4 发送:PTB + 激活

HAL_CANFD_AddMessageToTxFifo(..., CANFD_TX_FIFO_PTB) 把报文放进优先发送缓冲(PTB)HAL_CANFD_ActivateTxRequest(PTB_SEND) 触发硬件发出。STm32 风格里 TX 要显式「激活请求」。

4.5 接收:轮询 FIFO

__HAL_CANFD_GET_RX_FIFO_FILL_LEVEL 查接收 FIFO 是否为空;非空则 HAL_CANFD_GetRxMessage 取出帧头与数据。因为是轮询,CPU 在 while 里空转等,最简单但占 CPU——想解放 CPU 可换 *_IT(中断)版本。

5. 实验现象

  1. 烧录复位后,PCAN-View 会立即收到 MCU 发出的 ID=0x123、8 字节 1 2 3 4 5 6 7 8
  2. 在 PCAN-View 里手动发送 ID=0x12F、8 字节 数据;
  3. STK 的调试串口(115200)打印:
1
2
3
decimal data length: 8
hexadecimal data:
x1 x2 x3 x4 x5 x6 x7 x8

(x1~x8 为你从 PCAN-View 发出的数值)

6. 常见问题

现象 原因 解决
PCAN-View 收不到 MCU 发的帧 CANH/CANL 接反 / 没终端电阻 确认差分线序;总线两端各 120Ω
MCU 串口不打印 过滤器 ID 不匹配 用 PCAN-View 发 ID=0x12F;不要发别的
报错误帧/波特率不对 CAN 时钟或 Bit Timing 错 确认 CAN 时钟 20MHz,且 PCAN 配 250K
单板想自测 没有第二节点 Mode 改为 CANFD_MODE_LOOPBACK 内部回环
想发超过 8 字节 用了经典帧 CANFD_FRAME_FD 并设置 Data 段更高波特率
printf 无输出 串口接线/波特率 PA2→RX、PA3→TX、115200-8-N-1

7. 小结

  • CANFD 是 F403 特色外设,兼容经典 CAN 2.0B 与 CAN-FD;
  • 引脚固定 PA11(RX)/PA12(TX) AF10,时钟由 PLL 分频而来(本例 20MHz);
  • 流程:Init → ConfigFilter → Start → AddMessageToTxFifo → ActivateTxRequest → 轮询 Rx
  • 经典帧单帧 ≤8 字节;过滤器 MaskID=0 做精确 ID 匹配;
  • 本例 250K,MCU 发 0x123、收 0x12F 并打印,是 CAN 入门最佳样本。

8. 拓展应用(学了能做什么项目)

  • 汽车/工业节点:定义 ID 优先级矩阵,多 ECU 协同;
  • 多板互联:用过滤器分组,主节点轮询各从节点传感器;
  • 升级 CAN-FD:提高数据段波特率、传 64 字节,做固件 OTA / 图像下发;
  • 中断化:参考 CANFD_ExtendedID_IT,收帧即进回调,释放 CPU;
  • 回环自测:参考 CANFD_ExtendedID_LBMI/LBME_polling,无外设也能验证协议栈;
  • BMS / 机器人:多电芯电压、关节状态经 CAN 汇总到主控制器。

返回:CANFD 总览

世界是你们
使用 Hugo 构建
主题 StackJimmy 设计