1.7 CAN - 实验篇

这一章开始我们将使用真正的大疆主控板来完成接下来的学习 这一章开始我们将使用真正的大疆主控板来完成接下来的学习

我这里采用的大疆 A 板, 它的参数如下

主控单元为: STM32F427IIH6 12MHz 晶振, 可配置最高主频 180MHz

CAN 发送: 把发送数据放到 CAN 发送邮箱中, 有它自主控制发送, 放进去就不管了. 我们用到的 STM32F427, 每条 CAN 总线上有 3 个发送邮箱 这也意味着, 考虑到发送需要时间, 我们在代码层面上, 最多只能同时朝总线灌3 个 can 帧, 再多就发不出去了

CAN 接收: 从接收队列(FIFO 队列)选择接收, 可配合中断使用 我们用到的 STM32F427, 每条 CAN 总线上有 2 个接收队列


这个实验中, 我们要利用 CAN 信号来点灯.

  • CAN1 自发自收
  • 发送方发送一字节数据指明灯的序号
  • 接收方接收后直接在中断函数中处理信息, 熄灭对应的灯, 直到下次指令重新点灯

用到的有关元件与对应引脚, 详细请参考大疆 A 型开发板用户手册

  • LED1-8: PG1-PG8, 低电平点亮
  • CAN1: PD0, PD1 在 APB1 总线上 Pasted image 20260707172226

Pasted image 20260707180318 Pasted image 20260707182328 Pasted image 20260707184454 ***

CubeMX配置

和之前的一样, 配置完基本的内容(时钟树性能拉满)之后…

  • 配置PG1-8 为 GPIO_Output 作为 LED, 并将其初始电平配置为 high
  • 配置 PD0, PD1 为 CAN1

接下来进入CAN1 的详细配置选项

Pasted image 20260707185346 首先, 根据 ST 公司的 datasheet, CAN1 挂载在 APB1 下, 我们查看 APB1 的频率Pasted image 20260707185525

发现是 45MHz, 根据刚刚原理篇学过的妙妙小公式, 如果我的预分频为 9, Tq 应该是 200ns 也是对的. 说明我们是对的 接下来就要分配相位缓冲段 1 和相位缓冲段 2 的时间片

根据妙妙小公式, 同步段保持 1 个时间片不变, 相位缓冲段 1 和相位缓冲段都各分配 2个时间片, 这样比较合理

但是如果你在上图的情况下直接选择相位缓冲段 1 为 2 的话会报错Pasted image 20260707185933 因为这个 CubeMX 傻傻, 他认为你的相位缓冲段 2 不改了直接就拿错误的数据去算了(CAN 协议规定的波特率最大为 1Mbps). 解决的办法就是相位缓冲段 1 选择一个较大的数字, 然后先把相位缓冲段 2 选好了再去选相位缓冲段 1Pasted image 20260707190300

这样, 我们就美美配置好了

Bit Timings Parameters(位时序)

决定波特率

选项 含义
Prescaler (for Time Quantum) 分频系数,决定一个时间片 Tq 多长。Tq = Prescaler / f_APB1
Time Quantum 一个 Tq 的时长,由 Prescaler 自动算出
Time Quanta in Bit Segment 1 BS1 占几个 Tq(= 传播段 + 相位缓冲段1)
Time Quanta in Bit Segment 2 BS2 占几个 Tq(= 相位缓冲段2);采样点在 BS1 与 BS2 之间
Time for one Bit 一个 bit 总时长 =(1 + BS1 + BS2)× Tq
Baud Rate 波特率 = 1 / 位时间 = 1 Mbps,这组配置的最终产物
ReSynchronization Jump Width 即 SJW,重同步时相位缓冲段最多伸缩几个 Tq

Basic Parameters(基本行为开关)

选项 含义
Time Triggered Communication Mode 时间触发通信(TTCAN),按时间表调度、给帧打时间戳;普通场合用不到
Automatic Bus-Off Management 进入 bus-off 后是否由硬件自动按协议恢复。关=需软件手动恢复
Automatic Wake-Up Mode 睡眠时检测到总线活动是否自动唤醒。关=需软件手动唤醒
Automatic Retransmission 发送失败(仲裁失利/出错)是否自动重发。注意:这里 Disable 表示: 开启自动重传, CubeMX 这项是反逻辑的(对应寄存器 NART),Disable NART = 允许重传
Receive Fifo Locked Mode 接收 FIFO 满时:Disable=新帧覆盖旧帧;Enable=锁住丢弃新帧
Transmit Fifo Priority 发送顺序:Disable=按报文 ID 优先级发;Enable=按进入邮箱的先后(FIFO)发

最后把模式改为静默回环, 再去 NVIC 设置勾上对应的中断就可以生成代码了!

Pasted image 20260707192604

Pasted image 20260707192613

看到这里不妨想一想: 我们如果这样配置分频器和相位缓冲段的时间片的话, 采样点是在 60%. 这有一点靠前了. 但是理想的应该是在 75% 左右. 那么聪明的你想到了怎样的配置呢? 请在评论区写下来吧!


代码

首先我们配置滤波器的宏(macro)定义

总的来说就是这样: 编号[3:] | FIFOx[2:2] | ID类型[1:1] | 帧类型[0:0]

Pasted image 20260708164949

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Private macro -------------------------------------------------------------*/  
/* USER CODE BEGIN PM */
// 滤波器编号
#define CAN_FILTER(x) ((x) << 3)

// 接收队列 FIFO#define CAN_FIFO_0 (0 << 2)
#define CAN_FIFO_1 (1 << 2)

// 标准帧或拓展帧
#define CAN_STDID (0 << 1)
#define CAN_EXTID (1 << 1)

// 数据帧或遥控帧
#define CAN_DATA_TYPE (0 << 0)
#define CAN_REMOTE_TYPE (1 << 0)

然后, 设置一个CAN 启动并且开启中断的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Private user code ---------------------------------------------------------*/  
/* USER CODE BEGIN 0 */

/**
* @brief 初始化 CAN 总线
* * @param hcan CAN 编号
* */void CAN_Init(CAN_HandleTypeDef *hcan)
{
HAL_CAN_Start(hcan); // 开启 CAN 外设
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO0_MSG_PENDING); // 开 FIFO0 收中断
__HAL_CAN_ENABLE_IT(hcan, CAN_IT_RX_FIFO1_MSG_PENDING); // 开 FIFO1 收中断
}

/* USER CODE END 0 */

填HAL结构体写入滤波器

把打包好的 Object_Para 拆开,连同 ID、Mask_ID 一起,填进 HAL 的 CAN_FilterTypeDef 结构体,最后交给 HAL_CAN_ConfigFilter 写进滤波器

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
/**  
* @brief 配置 CAN 的滤波器
* * @param hcan CAN编号
* @param Object_Para 编号[3:] | FIFOx[2:2] | ID类型[1:1] | 帧类型[0:0]
* @param ID CAN ID * @param Mask_ID 掩码
*/void CAN_Filter_Mask_Config(CAN_HandleTypeDef *hcan, uint8_t Object_Para, uint32_t ID, uint32_t Mask_ID)
{
CAN_FilterTypeDef can_filter_init_structure;

// 看第 0 位, 判断是数据帧还是遥控帧, 遥控帧不做处理
if (Object_Para & 0x01)
{
return;
}

// 看第 1 位, 判断是标准帧还是拓展帧, 拓展帧不做处理
if ((Object_Para & 0x02) >> 1)
{
return;
}

// CAN ID 配置, 标准帧的 ID 是 11bit, 按规定放在高 16位 中的[15:5] 位
// CAN ID 放在高 16 位的[15:5]
can_filter_init_structure.FilterIdHigh = (ID & 0x7FF) << 5;
// 低 16位保持 0 can_filter_init_structure.FilterIdLow = 0x0000;
// MASK 放在高 16 位的[15:5]
can_filter_init_structure.FilterMaskIdHigh = (Mask_ID & 0x7FF) << 5;
// 低 16 位保持为 0 can_filter_init_structure.FilterMaskIdLow = 0x0000;

// 滤波器配置
// 滤波器编号
can_filter_init_structure.FilterBank = (Object_Para >> 3) & 0x1F;
// 滤波器模式, 设置为 ID 掩码模式
can_filter_init_structure.FilterMode = CAN_FILTERMODE_IDMASK;
// 32 位滤波器
can_filter_init_structure.FilterScale = CAN_FILTERSCALE_32BIT;
// 最后需要使能滤波器
can_filter_init_structure.FilterActivation = ENABLE;

// 从机模式配置
// 分配 CAN1, CAN2各 14个滤波器
can_filter_init_structure.SlaveStartFilterBank = 14;

// 绑定接收队列 FIFO can_filter_init_structure.FilterFIFOAssignment = (Object_Para >> 2) & 0x01;

// 最后提交
HAL_CAN_ConfigFilter(hcan, &can_filter_init_structure);
}

写发送的标准数据帧的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**  
* @brief 发送数据帧
* * @param hcan CAN 编号
* @param ID CAN ID * @param Data 被发送的数据指针
* @param Length 长度
* @return uint8_t 执行状态
*/uint8_t CAN_Send_Data(CAN_HandleTypeDef *hcan, uint16_t ID, uint8_t *Data, uint16_t Length)
{
CAN_TxHeaderTypeDef tx_header;
uint32_t used_mailbox;

tx_header.StdId = ID;
tx_header.ExtId = 0;
tx_header.IDE = 0;
tx_header.RTR = 0;
tx_header.DLC = Length;

return (HAL_CAN_AddTxMessage(hcan, &tx_header, Data, &used_mailbox));
}

接收数据之后, LED的控制函数

1
2
3
4
5
6
7
8
9
10
11
/**  
* @brief LED 控制
* @param data 收到的数据
*/void LED_Control(uint8_t data)
{
for (int i = 0; i < 8; i++)
{
GPIO_PinState state = (data & (1 << i)) ? GPIO_PIN_RESET : GPIO_PIN_SET;
HAL_GPIO_WritePin(GPIOG, (uint16_t)(GPIO_PIN_1 << i), state);
}
}

写接收中断函数

1
2
3
4
5
6
7
8
9
10
11
/**  
* @brief HAL 库 CAN 接收 FIFO1中断
* @param hcan CAN编号
*/void HAL_CAN_RxFifo1MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
CAN_RxHeaderTypeDef header;
uint8_t data;

HAL_CAN_GetRxMessage(hcan, CAN_FILTER_FIFO1, &header, &data);
LED_Control(data);
}

利用我们之前写好的函数初始化 can 以及配置滤波器

1
2
3
4
5
6
7
8
/* USER CODE BEGIN 2 */  
uint8_t Send_Data = 0;
uint32_t cfg = CAN_FILTER(13) | CAN_FIFO_1 | CAN_STDID | CAN_DATA_TYPE;

CAN_Init(&hcan1);
CAN_Filter_Mask_Config(&hcan1, cfg, 0x114, 0x7ff);

/* USER CODE END 2 */

利用发送数据函数, 开始点灯

1
2
3
4
5
6
7
8
9
/* Infinite loop */  
/* USER CODE BEGIN WHILE */
while (1)
{
Send_Data++;
CAN_Send_Data(&hcan1, 0x114, &Send_Data, 1);

HAL_Delay(250);
/* USER CODE END WHILE */

烧录

记得把之前的 daplink 的 cfg 文件改成 f4 哦

也是美美用上 can 了!