由于篇幅较大,我在这里就不介绍CAN通信的基础知识了,感兴趣可以自己去学一下。本实验使用STM32F103ZET6 的 CAN 外设实现两个设备之间的通讯,该实验中使用了两个实验板,是在MDK编译环境下调试。可以参考STM32F103的官方参考手册和官方数据手册。
#ifndef __CAN_H
#define __CAN_H
#include "stm32f10x.h"
#include "./usart/bsp_debug_usart.h"
#define CANx CAN1
#define CAN_CLK RCC_APB1Periph_CAN1
#define CAN_RX_IRQ USB_LP_CAN1_RX0_IRQn
#define CAN_RX_IRQHandler USB_LP_CAN1_RX0_IRQHandler
#define CAN_RX_PIN GPIO_Pin_8
#define CAN_TX_PIN GPIO_Pin_9
#define CAN_TX_GPIO_PORT GPIOB
#define CAN_RX_GPIO_PORT GPIOB
#define CAN_TX_GPIO_CLK (RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB)
#define CAN_RX_GPIO_CLK RCC_APB2Periph_GPIOB
/*debug*/
#define CAN_DEBUG_ON 1
#define CAN_DEBUG_ARRAY_ON 1
#define CAN_DEBUG_FUNC_ON 1
// Log define
#define CAN_INFO(fmt,arg...) printf("<<-CAN-INFO->> "fmt"\n",##arg)
#define CAN_ERROR(fmt,arg...) printf("<<-CAN-ERROR->> "fmt"\n",##arg)
#define CAN_DEBUG(fmt,arg...) do{\
if(CAN_DEBUG_ON)\
printf("<<-CAN-DEBUG->> [%d]"fmt"\n",__LINE__, ##arg);\
}while(0)
#define CAN_DEBUG_ARRAY(array, num) do{\
int32_t i;\
uint8_t* a = array;\
if(CAN_DEBUG_ARRAY_ON)\
{\
printf("<<-CAN-DEBUG-ARRAY->>\n");\
for (i = 0; i < (num); i++)\
{\
printf("%02x ", (a)[i]);\
if ((i + 1 ) %10 == 0)\
{\
printf("\n");\
}\
}\
printf("\n");\
}\
}while(0)
#define CAN_DEBUG_FUNC() do{\
if(CAN_DEBUG_FUNC_ON)\
printf("<<-CAN-FUNC->> Func:%s@Line:%d\n",__func__,__LINE__);\
}while(0)
static void CAN_GPIO_Config(void);
static void CAN_NVIC_Config(void);
static void CAN_Mode_Config(void);
static void CAN_Filter_Config(void);
void CAN_Config(void);
void CAN_SetMsg(CanTxMsg *TxMessage);
void Init_RxMes(CanRxMsg *RxMessage);
#endif
以上代码根据硬件连接,把与 CAN 通讯使用的 CAN 号 、引脚号以及时钟都以宏封装
起来,并且定义了接收中断的中断向量和中断服务函数,我们通过中断来获知接收 FIFO
的信息。注意在 GPIO 时钟部分我们还加入了 AFIO 时钟,这是为下面 CAN 进行复用功能重映射而设置的,当使用复用功能重映射时,必须开启 AFIO 时钟。
/*
* 函数名:CAN_GPIO_Config
* 描述 :CAN的GPIO 配置
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
static void CAN_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/* Enable GPIO clock */
RCC_APB2PeriphClockCmd(CAN_TX_GPIO_CLK|CAN_RX_GPIO_CLK, ENABLE);
//重映射引脚
GPIO_PinRemapConfig(GPIO_Remap1_CAN1, ENABLE);
/* Configure CAN TX pins */
GPIO_InitStructure.GPIO_Pin = CAN_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CAN_TX_GPIO_PORT, &GPIO_InitStructure);
/* Configure CAN RX pins */
GPIO_InitStructure.GPIO_Pin = CAN_RX_PIN ;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);
}
与所有使用到 GPIO 的外设一样,都要先把使用到的 GPIO 引脚模式初始化,配置好
复用功能,CAN 的两个引脚都配置成通用推挽输出模式即可。根据 CAN 外设的要求,TX引脚要被配置成推挽复用输出、RX 引脚要被设置成浮空输入或上拉输入。另外,由于本实验板中的 CAN 使用的 PB8、PB9 的默认复用功能不是 CAN 外设的引脚,需要使用外设引脚的复用功能重映射才能正常使用,代码中使用 GPIO_PinRemapConfig 函数把 CAN 映射至 PB8、PB9。
接下来我们配置 CAN 的工作模式,由于我们是自己用的两个板子之间进行通讯,波
特率之类的配置只要两个板子一致即可。如果您要使实验板与某个 CAN 总线网络的通讯
的节点通讯,那么实验板的 CAN 配置必须要与该总线一致。
/*
* 函数名:CAN_Mode_Config
* 描述 :CAN的模式 配置
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
static void CAN_Mode_Config(void)
{
CAN_InitTypeDef CAN_InitStructure;
/************************CAN通信参数设置**********************************/
/* Enable CAN clock */
RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE);
/*CAN寄存器初始化*/
CAN_DeInit(CANx);
CAN_StructInit(&CAN_InitStructure);
/*CAN单元初始化*/
CAN_InitStructure.CAN_TTCM=DISABLE; //MCR-TTCM 关闭时间触发通信模式使能
CAN_InitStructure.CAN_ABOM=ENABLE; //MCR-ABOM 自动离线管理
CAN_InitStructure.CAN_AWUM=ENABLE; //MCR-AWUM 使用自动唤醒模式
CAN_InitStructure.CAN_NART=DISABLE; //MCR-NART 禁止报文自动重传 DISABLE-自动重传
CAN_InitStructure.CAN_RFLM=DISABLE; //MCR-RFLM 接收FIFO 锁定模式 DISABLE-溢出时新报文会覆盖原有报文
CAN_InitStructure.CAN_TXFP=DISABLE; //MCR-TXFP 发送FIFO优先级 DISABLE-优先级取决于报文标示符
CAN_InitStructure.CAN_Mode = CAN_Mode_Normal; //正常工作模式
CAN_InitStructure.CAN_SJW=CAN_SJW_2tq; //BTR-SJW 重新同步跳跃宽度 2个时间单元
/* ss=1 bs1=5 bs2=3 位时间宽度为(1+5+3) 波特率即为时钟周期tq*(1+3+5) */
CAN_InitStructure.CAN_BS1=CAN_BS1_5tq; //BTR-TS1 时间段1 占用了5个时间单元
CAN_InitStructure.CAN_BS2=CAN_BS2_3tq; //BTR-TS1 时间段2 占用了3个时间单元
/* CAN Baudrate = 1 MBps (1MBps已为stm32的CAN最高速率) (CAN 时钟频率为 APB1 = 36 MHz) */
CAN_InitStructure.CAN_Prescaler =4; ////BTR-BRP 波特率分频器 定义了时间单元的时间长度 36/(1+5+3)/4=1 Mbps
CAN_Init(CANx, &CAN_InitStructure);
}
这段代码主要是把 CAN 的模式设置成了正常工作模式,如果您阅读的是“CAN—回环测试”的工程,这里是被配置成回环模式的,除此之外,两个工程就没有其它差别了。代码中还把位时序中的 BS1 和 BS2 段分别设置成了 5Tq 和 3Tq,再加上 SYNC_SEG段,一个 CAN 数据位就是 9Tq 了,加上 CAN 外设的分频配置为 4 分频,CAN 所使用的总线时钟 fAPB1 = 36MHz,于是我们可计算出它的波特率:
1Tq = 1/(36M/4)=1/9 us
T1bit=(5+3+1) x Tq =1us
波特率=1/T1bit =1Mbps
/*
* 函数名:CAN_Filter_Config
* 描述 :CAN的过滤器 配置
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
static void CAN_Filter_Config(void)
{
CAN_FilterInitTypeDef CAN_FilterInitStructure;
/*CAN筛选器初始化*/
CAN_FilterInitStructure.CAN_FilterNumber=0; //筛选器组0
CAN_FilterInitStructure.CAN_FilterMode=CAN_FilterMode_IdMask; //工作在掩码模式
CAN_FilterInitStructure.CAN_FilterScale=CAN_FilterScale_32bit; //筛选器位宽为单个32位。
/* 使能筛选器,按照标志的内容进行比对筛选,扩展ID不是如下的就抛弃掉,是的话,会存入FIFO0。 */
CAN_FilterInitStructure.CAN_FilterIdHigh= ((((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF0000)>>16; //要筛选的ID高位
CAN_FilterInitStructure.CAN_FilterIdLow= (((u32)0x1314<<3)|CAN_ID_EXT|CAN_RTR_DATA)&0xFFFF; //要筛选的ID低位
CAN_FilterInitStructure.CAN_FilterMaskIdHigh= 0xFFFF; //筛选器高16位每位必须匹配
CAN_FilterInitStructure.CAN_FilterMaskIdLow= 0xFFFF; //筛选器低16位每位必须匹配
CAN_FilterInitStructure.CAN_FilterFIFOAssignment=CAN_Filter_FIFO0 ; //筛选器被关联到FIFO0
CAN_FilterInitStructure.CAN_FilterActivation=ENABLE; //使能筛选器
CAN_FilterInit(&CAN_FilterInitStructure);
/*CAN通信中断使能*/
CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE);
}
这段代码把筛选器第 0 组配置成了 32 位的掩码模式,并且把它的输出连接到接收
FIFO0,若通过了筛选器的匹配,报文会被存储到接收 FIFO0。筛选器配置的重点是配置 ID 和掩码,根据我们的配置,这个筛选器工作在下图的模式。
在该配置中,结构体成员 CAN_FilterIdHigh 和 CAN_FilterIdLow 存储的是要筛选的 ID, 而 CAN_FilterMaskIdHigh 和 CAN_FilterMaskIdLow 存储的是相应的掩码。在赋值时,要注意寄存器位的映射,在 32 位的 ID 中,第 0 位是保留位,第 1 位是 RTR 标志,第 2 位是IDE 标志,从第 3 位起才是报文的 ID(扩展 ID)。因此在上述代码中我们先把扩展ID“0x1314”、IDE 位标志“宏 CAN_ID_EXT”以及RTR 位标志“宏 CAN_RTR_DATA”根据寄存器位映射组成一个 32 位的数据,然后再把它的高 16 位和低 16 位分别赋值给结构体成员 CAN_FilterIdHigh 和 CAN_FilterIdLow。而在掩码部分,为简单起见我们直接对所有位赋值为 1,表示上述所有标志都完全一样的报文才能经过筛选,所以我们这个配置相当于单个 ID 列表的模式,只筛选了一个 ID号,而不是筛选一组 ID 号。这里只是为了演示方便,实际使用中一般会对不要求相等的数据位赋值为 0,从而过滤一组 ID,如果有需要,还可以继续配置多个筛选器组,最多可以配置 28 个,代码中只是配置了筛选器组 0。
在配置筛选器代码的最后部分我们还调用库函数 CAN_ITConfig 使能了 CAN 的中断,
该函数使用的输入参数宏 CAN_IT_FMP0 表示当 FIFO0 接收到数据时会引起中断,该接收中断的优先级配置如下
/*
* 函数名:CAN_NVIC_Config
* 描述 :CAN的NVIC 配置,第1优先级组,0,0优先级
* 输入 :无
* 输出 : 无
* 调用 :内部调用
*/
static void CAN_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
/* Configure one bit for preemption priority */
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1);
/*中断设置*/
NVIC_InitStructure.NVIC_IRQChannel = CAN_RX_IRQ; //CAN1 RX0中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; //抢占优先级0
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //子优先级为0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
这部分与我们配置其它中断的优先级无异,都是配置 NVIC 结构体,优先级可根据自己的需要配置,最主要的是中断向量,上述代码中把中断向量配置成了 CAN 的接收中断。
要使用 CAN 发送报文时,我们需要先定义一个发送报文结构体并向它赋值
/*
* 函数名:CAN_SetMsg
* 描述 :CAN通信报文内容设置,设置一个数据内容为0-7的数据包
* 输入 :发送报文结构体
* 输出 : 无
* 调用 :外部调用
*/
void CAN_SetMsg(CanTxMsg *TxMessage)
{
uint8_t ubCounter = 0;
//TxMessage.StdId=0x00;
TxMessage->ExtId=0x1314; //使用的扩展ID
TxMessage->IDE=CAN_ID_EXT; //扩展模式
TxMessage->RTR=CAN_RTR_DATA; //发送的是数据
TxMessage->DLC=8; //数据长度为8字节
/*设置要发送的数据0-7*/
for (ubCounter = 0; ubCounter < 8; ubCounter++)
{
TxMessage->Data[ubCounter] = ubCounter;
}
}
这段代码是我们为了方便演示而自己定义的设置报文内容的函数,它把报文设置成了扩展模式的数据帧,扩展 ID 为 0x1314,数据段的长度为 8,且数据内容分别为 0-7,实际应用中您可根据自己的需求发设置报文内容。当我们设置好报文内容后,调用库函数CAN_Transmit 即可把该报文存储到发送邮箱,然后 CAN 外设会把它发送出去:
CAN_Transmit(CANx, &TxMessage);
extern __IO uint32_t flag ; //用于标志是否接收到数据,在中断函数中赋值
extern CanRxMsg RxMessage; //接收缓冲区
/*
* 函数名:USB_LP_CAN1_RX0_IRQHandler
* 描述 :USB中断和CAN接收中断服务程序,USB跟CAN公用I/O,这里只用到CAN的中断。
* 输入 :无
* 输出 : 无
* 调用 :无
*/
void CAN_RX_IRQHandler(void)
{
/*从邮箱中读出报文*/
CAN_Receive(CANx, CAN_FIFO0, &RxMessage);
/* 比较ID是否为0x1314 */
if((RxMessage.ExtId==0x1314) && (RxMessage.IDE==CAN_ID_EXT) && (RxMessage.DLC==8) )
{
flag = 1; //接收成功
}
else
{
flag = 0; //接收失败
}
}
根据我们前面的配置,若 CAN 接收的报文经过筛选器匹配后会被存储到 FIFO0 中,并引起中断进入到这个中断服务函数中,在这个函数里我们调用了库函数 CAN_Receive 把报文从 FIFO 复制到自定义的接收报文结构体 RxMessage 中,并且比较了接收到的报文 ID是否与我们希望接收的一致,若一致就设置标志 flag=1,否则为 0,通过 flag 标志通知主程序流程获知是否接收到数据。
要注意如果设置了接收报文中断,必须要在中断内调用 CAN_Receive 函数读取接收FIFO 的内容,因为只有这样才能清除该 FIFO 的接收中断标志,如果不在中断内调用它清除标志的话,一旦接收到报文,STM32 会不断进入中断服务函数,导致程序卡死。
#ifndef __KEY_H
#define __KEY_H
#include "stm32f10x.h"
// 引脚定义
#define KEY1_GPIO_CLK RCC_APB2Periph_GPIOA
#define KEY1_GPIO_PORT GPIOA
#define KEY1_GPIO_PIN GPIO_Pin_0
#define KEY2_GPIO_CLK RCC_APB2Periph_GPIOC
#define KEY2_GPIO_PORT GPIOC
#define KEY2_GPIO_PIN GPIO_Pin_13
/** 按键按下标置宏
* 按键按下为高电平,设置 KEY_ON=1, KEY_OFF=0
* 若按键按下为低电平,把宏设置成KEY_ON=0 ,KEY_OFF=1 即可
*/
#define KEY_ON 1
#define KEY_OFF 0
void Key_GPIO_Config(void);
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin);
#endif /* __KEY_H */
/**
* @brief 配置按键用到的I/O口
* @param 无
* @retval 无
*/
void Key_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
/*开启按键端口的时钟*/
RCC_APB2PeriphClockCmd(KEY1_GPIO_CLK|KEY2_GPIO_CLK,ENABLE);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY1_GPIO_PIN;
// 设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY1_GPIO_PORT, &GPIO_InitStructure);
//选择按键的引脚
GPIO_InitStructure.GPIO_Pin = KEY2_GPIO_PIN;
//设置按键的引脚为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
//使用结构体初始化按键
GPIO_Init(KEY2_GPIO_PORT, &GPIO_InitStructure);
}
/*
* 函数名:Key_Scan
* 描述 :检测是否有按键按下
* 输入 :GPIOx:x 可以是 A,B,C,D或者 E
* GPIO_Pin:待读取的端口位
* 输出 :KEY_OFF(没按下按键)、KEY_ON(按下按键)
*/
uint8_t Key_Scan(GPIO_TypeDef* GPIOx,uint16_t GPIO_Pin)
{
/*检测是否有按键按下 */
if(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON )
{
/*等待按键释放 */
while(GPIO_ReadInputDataBit(GPIOx,GPIO_Pin) == KEY_ON);
return KEY_ON;
}
else
return KEY_OFF;
}
#ifndef __DEBUG_USART_H
#define __DEBUG_USART_H
#include "stm32f10x.h"
#include <stdio.h>
/**
* 串口宏定义,不同的串口挂载的总线不一样,移植时需要修改这几个宏
*/
#define DEBUG_USART USART1
#define DEBUG_USART_CLK RCC_APB2Periph_USART1
#define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_BAUDRATE 115200
// USART GPIO 引脚宏定义
#define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10
void Debug_USART_Config(void);
//int fputc(int ch, FILE *f);
#endif /* __USART1_H */
/**
* @brief USART1 GPIO 配置,工作模式配置。115200 8-N-1
* @param 无
* @retval 无
*/
void Debug_USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
DEBUG_USART_GPIO_APBxClkCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
DEBUG_USART_APBxClkCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No ;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(DEBUG_USART, &USART_InitStructure);
// 使能串口
USART_Cmd(DEBUG_USART, ENABLE);
}
///重定向c库函数printf到USART1
int fputc(int ch, FILE *f)
{
/* 发送一个字节数据到USART1 */
USART_SendData(DEBUG_USART, (uint8_t) ch);
/* 等待发送完毕 */
while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_TXE) == RESET);
return (ch);
}
///重定向c库函数scanf到USART1
int fgetc(FILE *f)
{
/* 等待串口1输入数据 */
while (USART_GetFlagStatus(DEBUG_USART, USART_FLAG_RXNE) == RESET);
return (int)USART_ReceiveData(DEBUG_USART);
}
__IO uint32_t flag = 0; //用于标志是否接收到数据,在中断函数中赋值
CanTxMsg TxMessage; //发送缓冲区
CanRxMsg RxMessage; //接收缓冲区
/// 不精确的延时
static void can_delay(__IO u32 nCount)
{
for(; nCount != 0; nCount--);
}
/**
* @brief 主函数
* @param 无
* @retval 无
*/
int main(void)
{
LED_GPIO_Config();
/*初始化USART1*/
Debug_USART_Config();
/*初始化按键*/
Key_GPIO_Config();
/*初始化can,在中断接收CAN数据包*/
CAN_Config();
while(1)
{
/*按一次按键发送一次数据*/
if( Key_Scan(KEY1_GPIO_PORT,KEY1_GPIO_PIN) == KEY_ON)
{
LED_BLUE;
/*设置要发送的报文*/
CAN_SetMsg(&TxMessage);
/*把报文存储到发送邮箱,发送*/
CAN_Transmit(CANx, &TxMessage);
can_delay(10000);//等待发送完毕,可使用CAN_TransmitStatus查看状态
LED_GREEN;
printf("\r\n已使用CAN发送数据包!\r\n");
printf("\r\n发送的报文内容为:\r\n");
printf("\r\n 扩展ID号ExtId:0x%x \r\n",TxMessage.ExtId);
CAN_DEBUG_ARRAY(TxMessage.Data,8);
}
if(flag==1)
{
LED_GREEN;
printf("\r\nCAN接收到数据:\r\n");
CAN_DEBUG_ARRAY(RxMessage.Data,8);
flag=0;
}
}
}
在 main 函数里,我们调用了 CAN_Config 函数初始化 CAN 外设,它包含我们前面解
说的 GPIO 初始化函数 CAN_GPIO_Config、中断优先级设置函数 CAN_NVIC_Config、工作模式设置函数 CAN_Mode_Config 以及筛选器配置函数 CAN_Filter_Config。
初始化完成后,我们在 while 循环里检测按键,当按下实验板的按键 1 时,它就调用CAN_SetMsg 函数设置要发送的报文,然后调用 CAN_Transmit 函数把该报文存储到发送邮箱,等待 CAN 外设把它发送出去。代码中并没有检测发送状态,如果需要,您可以调用库函数 CAN_TransmitStatus 检查发送状态。while 循环中在其它时间一直检查 flag 标志,当接收到报文时,我们的中断服务函数会把它置 1,所以我们可以通过它获知接收状态,当接收到报文时,我们把它使用宏CAN_DEBUG_ARRAY 输出到串口。