IMX6ULL与STM32F103的CAN通信实现
在上一篇博文中,我们利用USBCAN设备及其上位机软件,测试了一块以IMX6ULL为核心芯片的开发板的CAN通信收发功能,了解了在Linux应用层基于套接字实现CAN网络应用的基本方法。本次我们将USBCAN替换为一块以STM32F103ZET6为核心芯片的开发板,尝试实现双机之间的CAN通信基本功能。
硬件连接
在硬件上,两个板子各自均只留出1路CAN接口,只需将两个CAN接口的CAN_H与CAN_L对应连接起来即可。由于CAN信号是两根线的差分信号,故不需要再连接其它。两板的CAN收发器均采用经典的TJA1050,器件连接的原理图如下。用线连好后双机的CAN通信连接示意图如下。芯片内部集成的CAN控制器与外接的CAN收发器交换CAN报文数据,收发器是控制器与物理总线的中间媒介,负责在控制器的逻辑电平和总线的差分信号间做转换。而CAN控制器的具体行为由相关寄存器的状态决定,需要我们编写程序来控制。两个收发器之间的CAN_H和CAN_L两条线的两段之间,需要各自接一个120Ω的电阻,由原理图可见板载已经接入了该电阻,无需额外接入。两个板子连接的实物图如下。
驱动层实现
这里所说的驱动层,是从Linux系统体系的角度而言的。Linux具有明显的内核与应用层之分,对底层硬件的驱动程序是内核的组成部分,在应用层控制硬件工作的流程中充当了一种“中间件”的作用,对上提供相关接口供应用层调用,对下通过操作寄存器等真正操控硬件的行为。但对于STM32裸机的开发方式来说,程序并不需要明确划分哪些部分属于app、哪些属于driver,所有的代码是可以放在一起的。我们本次的开发基于ST官方提供的标准库,所以这里把基于标准库封装开发所得的用于实现STM32的CAN外设基本功能的接口,称为STM32的CAN“驱动层”。
IMX6ULL
为驱动IMX6ULL上的CAN外设,首先要找到描述该设备硬件信息的设备树,其中外设引脚信息和控制器描述信息是必不可少的。我们在内核源码/arch/arm/boot/dts/目录下找到有关该CAN外设的设备树代码如下:
// 引脚配置信息
pinctrl_flexcan1: flexcan1grp {
fsl,pins = <
MX6UL_PAD_UART3_RTS_B__FLEXCAN1_RX 0x1b020 // CAN的RX引脚复用UART3_RTS
MX6UL_PAD_UART3_CTS_B__FLEXCAN1_TX 0x1b020 // CAN的TX引脚复用UART3_CTS
>;
};
// CAN1控制器
flexcan1: can@02090000 {
compatible = "fsl,imx6ul-flexcan", "fsl,imx6q-flexcan"; // 匹配NXP原厂CAN驱动
reg = <0x02090000 0x4000>;
interrupts = <GIC_SPI 110 IRQ_TYPE_LEVEL_HIGH>;
clocks = <&clks IMX6UL_CLK_CAN1_IPG>,
<&clks IMX6UL_CLK_CAN1_SERIAL>;
clock-names = "ipg", "per";
stop-mode = <&gpr 0x10 1 0x10 17>;
status = "disabled";
};
上面CAN控制器的status为disabled,表示不可用,需要改成okay才能正常使用。一般我们不会直接去修改原控制器的默认设置,而是采用“追加”的方式更改属性值,如下为对flexcan1硬件信息的追加补充:
&flexcan1 {
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_flexcan1>; // 指定引脚
xceiver-supply = <®_can_3v3>; // 指定CAN收发器电压为3.3V
status = "okay"; // 状态为可用
};
这样设备树的配置基本就完成了,然后需要使能IMX6ULL的flexcan驱动。一般而言,NXP在官方提供的内核里已经集成了多种类型外设的驱动,如果仅仅使用基础通信等基本功能而不涉及复杂需求的实现,那么大多数情况下直接使能内核自带驱动后就可以了。在内核源码顶层目录下执行内核配置make menuconfig,依次进入Networking support、CAN bus subsystem support、CAN Device Driver,找到Support for Freescale FLEXCAN based chips选项,编译类型选成编译为内核一部分即built-in,如下:
保存设置后CAN驱动的编译选项就存在了编译配置文件中,再编译内核就能使上面的设备树配置和驱动配置生效了,将生成的Image和dtb文件载入IMX6ULL开发板,即可看到系统中的CAN设备,如下。在/sys/class/net/目录下的can0即为我们配置的外设,查看其控制器设备树的status可见值为okay,和我们设置的一样。
STM32F103ZET6
要使用STM32F103的CAN外设,我们需要考虑这些方面:①引脚配置,要考虑CAN_RX和CAN_TX所在引脚号、GPIO的时钟使能、GPIO参数如何配置等;②CAN控制器配置,包括CAN外设时钟使能、CAN工作模式、波特率、接收FIFO属性等;③CAN过滤器配置,主要决定CAN的接收策略;④CAN外设的接收和发送接口;⑤在需要利用CAN中断的场合,还应该配置NVIC和CAN中断类型。后面和IMX6ULL交互时需要用到CAN接收中断,所以我们本次需要配置中断。基于ST提供的标准库,可以将上述几方面开发成自己的接口,即为所谓的CAN“驱动层”。我们开发的接口主要代码如下:
#define CANx CAN1 // 使用CAN1
#define CAN_CLK RCC_APB1Periph_CAN1 // CAN1的时钟
#define CAN_RX_IRQ USB_LP_CAN1_RX0_IRQn // CAN接收中断号
#define CAN_RX_IRQHandler USB_LP_CAN1_RX0_IRQHandler // CAN接收中断服务函数
#define CAN_RX_PIN GPIO_Pin_11 // Rx:PA11
#define CAN_TX_PIN GPIO_Pin_12 // Tx:PA12
#define CAN_TX_GPIO_PORT GPIOA
#define CAN_RX_GPIO_PORT GPIOA
#define CAN_TX_GPIO_CLK RCC_APB2Periph_GPIOA // GPIOA的时钟
#define CAN_RX_GPIO_CLK RCC_APB2Periph_GPIOA
typedef enum{
STANDARD = 0,
EXTENDED
} CAN_ID_TYPE; // 报文ID类型
typedef enum{
DATA = 0,
REMOTE
} CAN_DATA_TYPE; // 帧类型
/* GPIO配置 */
static void CAN_GPIO_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(CAN_TX_GPIO_CLK|CAN_RX_GPIO_CLK, ENABLE); // 使能Rx和Tx对应GPIO的时钟
GPIO_InitStructure.GPIO_Pin = CAN_TX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // Tx配置为复用推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CAN_TX_GPIO_PORT, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = CAN_RX_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // Rx配置为上拉输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(CAN_RX_GPIO_PORT, &GPIO_InitStructure);
}
/* NVIC配置 */
static void CAN_NVIC_Config(void)
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); // 设置中断优先级分组1
NVIC_InitStructure.NVIC_IRQChannel = CAN_RX_IRQ; // CAN接收中断号
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; // 子优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
/* CAN控制器配置 */
static void CAN_Mode_Config(void)
{
CAN_InitTypeDef CAN_InitStructure;
RCC_APB1PeriphClockCmd(CAN_CLK, ENABLE); // 使能CAN时钟
CAN_DeInit(CANx);
CAN_StructInit(&CAN_InitStructure);
CAN_InitStructure.CAN_TTCM = DISABLE;
CAN_InitStructure.CAN_ABOM = ENABLE;
CAN_InitStructure.CAN_AWUM = ENABLE;
CAN_InitStructure.CAN_NART = DISABLE;
CAN_InitStructure.CAN_RFLM = DISABLE; // 禁止FIFO锁定模式
CAN_InitStructure.CAN_TXFP = DISABLE;
CAN_InitStructure.CAN_Mode = CAN_Mode_Normal;
CAN_InitStructure.CAN_SJW = CAN_SJW_1tq; // 同步跳跃宽度
CAN_InitStructure.CAN_BS1 = CAN_BS1_5tq; // 位段1
CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq; // 位段2
CAN_InitStructure.CAN_Prescaler = 8; // 时钟分频
CAN_Init(CANx, &CAN_InitStructure);
}
/* CAN过滤器配置 */
static void CAN_Filter_Config(void)
{
CAN_FilterInitTypeDef CAN_FilterInitStructure;
u16 FilterId = 0x123; // 可接收的报文ID
u16 FilterMask = 0xFFFF; // 掩码
CAN_FilterInitStructure.CAN_FilterNumber = 0; // 过滤器号
CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask; // 掩码模式
CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInitStructure.CAN_FilterIdHigh = FilterId<<5; // 标准标识符
CAN_FilterInitStructure.CAN_FilterIdLow = (u16)(CAN_ID_STD|CAN_RTR_DATA); // 数据帧
CAN_FilterInitStructure.CAN_FilterMaskIdHigh = FilterMask; // 掩码
CAN_FilterInitStructure.CAN_FilterMaskIdLow = FilterMask;
CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0; // 关联FIFO0
CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;
CAN_FilterInit(&CAN_FilterInitStructure);
CAN_ITConfig(CANx, CAN_IT_FMP0, ENABLE); // 使能FIFO0的消息挂起中断
}
/* CAN配置 */
void CAN_Config(void)
{
CAN_GPIO_Config();
CAN_NVIC_Config();
CAN_Mode_Config();
CAN_Filter_Config();
}
/* 接收消息初始化 */
void CAN_InitRxMessage(CanRxMsg *RxMessage, CAN_ID_TYPE idtype, CAN_DATA_TYPE datatype, u8 dlc, u8 fmi)
{
RxMessage->StdId = 0;
RxMessage->ExtId = 0;
if(idtype == STANDARD)
RxMessage->IDE = CAN_ID_STD;
else if(idtype == EXTENDED)
RxMessage->IDE = CAN_ID_EXT;
if(datatype == DATA)
RxMessage->RTR = CAN_RTR_DATA;
else if(datatype == REMOTE)
RxMessage->RTR = CAN_RTR_REMOTE;
RxMessage->DLC = dlc;
memset(RxMessage->Data, 0x00, 8);
RxMessage->FMI = fmi;
}
/* 设置发送消息 */
void CAN_SetTxMessage(CanTxMsg *TxMessage, u32 id, CAN_ID_TYPE idtype, CAN_DATA_TYPE datatype, u8 dlc, unsigned char *data)
{
TxMessage->StdId = id;
if(idtype == STANDARD)
TxMessage->IDE = CAN_ID_STD;
else if(idtype == EXTENDED)
TxMessage->IDE = CAN_ID_EXT;
if(datatype == DATA)
TxMessage->RTR = CAN_RTR_DATA;
else if(datatype == REMOTE)
TxMessage->RTR = CAN_RTR_REMOTE;
TxMessage->DLC = dlc;
memcpy(TxMessage->Data, data, dlc);
}
/* CAN发送消息 */
u8 CAN_SendTxMsg(CAN_TypeDef *CANxx, CanTxMsg *TxMessage)
{
u8 mailbox = 3;
if((mailbox = CAN_Transmit(CANxx, TxMessage)) != CAN_TxStatus_NoMailBox) // 调用库函数CAN_Transmit实现发送
{
while((CAN_TransmitStatus(CANxx, mailbox)) != CAN_TxStatus_Ok); // 检查发送状态是否为完成
return 0;
}
else
{
printf("CAN_SendTxMsg error and no mail box\r\n");
return 1;
}
}
上面代码中,CAN接收中断号定义在文件stm32f10x.h中的枚举类型IRQn_Type,该类型规定了STM32F10x的所有中断号。CAN接收中断服务函数的名称定义在启动文件start_up_stm32f10x_hd.s中。在CAN控制器配置函数中,设定了禁止FIFO锁定模式,意味着FIFO溢出时新接收的报文会覆盖旧的报文而不会被丢弃。由时间参数的设定可以得出,CAN波特率即为36MHz/(8*(1+5+3))=500kbps。在CAN过滤器的配置中,采用了32位掩码模式,根据ID为0x123和掩码为0xFFFF可知,这里设定了仅接受ID为0x123的报文,并且为标准ID和数据帧。使能了FIFO0的消息挂起中断,即该邮箱中接收到消息就进入CAN接收服务函数。在CAN_SendTxMsg接口中,调用了库函数CAN_Transmit进行发送,该函数返回有效的邮箱号或表示无可用邮箱的标志。利用库函数CAN_TransmitStatus检查发送状态是否为完成。
应用层实现
我们设计一个非常简单的双机交互流程,如下:
IMX6这边首先发送ID为x0123的CAN报文,STM32接收到后回复ID为0x456的CAN报文,IMX6收到后继续发送0x123,然后重复这个过程。
IMX6ULL
关于IMX6ULL的CAN通信应用层的基本知识,可参考之前的博文《imx6ull开发板的CAN通信》,这里不详细说明了。我们设置2个线程分别处理发送和接收任务,发送线程在收到报文后延时2秒发送,两个线程之间使用简单的标志位同步。应用层代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/can.h>
#include <linux/can/raw.h>
#include <net/if.h>
#include <pthread.h>
struct can_frame receiveframe = {0};
struct can_frame sendframe = {0};
struct can_filter filters[3];
int sockfd;
int flag = 1;
void CAN_Config(void){
struct ifreq ifr = {0};
struct sockaddr_can can_addr = {0};
int i;
int ret;
if((sockfd = socket(PF_CAN, SOCK_RAW, CAN_RAW)) < 0){
perror("socket error");
exit(EXIT_FAILURE);
}
strcpy(ifr.ifr_name, "can0");
if((ioctl(sockfd, SIOCGIFINDEX, &ifr)) != 0){
perror("ioctl error");
exit(EXIT_FAILURE);
}
can_addr.can_family = AF_CAN;
can_addr.can_ifindex = ifr.ifr_ifindex;
if((ret = bind(sockfd, (struct sockaddr *)&can_addr, sizeof(can_addr))) < 0){
perror("bind error");
close(sockfd);
exit(EXIT_FAILURE);
}
filters[0].can_id = 0x123;
filters[1].can_id = 0x234;
filters[2].can_id = 0x456;
for(i = 0;i < 3;i++)
filters[i].can_mask = 0x7FF; // ID掩码
setsockopt(sockfd, SOL_CAN_RAW, CAN_RAW_FILTER, &filters, sizeof(filters)); // 设置接收过滤
sendframe.can_id = 0x123; // 帧ID为0x123(标准帧)
sendframe.can_dlc = 8; // 一次发送8个字节
sendframe.data[0] = 0x01;
sendframe.data[1] = 0x02;
sendframe.data[2] = 0x03;
sendframe.data[3] = 0x04;
sendframe.data[4] = 0x05;
sendframe.data[5] = 0x06;
sendframe.data[6] = 0x07;
sendframe.data[7] = 0x08;
}
void *canReceive(void *arg){
while(1){
int i = 0;
if((read(sockfd, &receiveframe, sizeof(struct can_frame))) < 0){
perror("read error");
break;
}
if(receiveframe.can_id & CAN_ERR_FLAG){ // 校验错误帧
printf("Error frame!\n");
break;
}
printf("Received CAN data from STM32F103ZET6\n");
if(receiveframe.can_id & CAN_EFF_FLAG) // 校验扩展帧
printf("扩展帧 <0x%08x> \n", receiveframe.can_id & CAN_EFF_MASK);
else
printf("标准帧 <0x%03x> \n", receiveframe.can_id & CAN_SFF_MASK);
if(receiveframe.can_id & CAN_RTR_FLAG){ // 校验远程帧
printf("remote request\n");
continue;
}
printf("can_dlc: [%d] \ndata: ", receiveframe.can_dlc);
for(i;i < receiveframe.can_dlc;i++)
printf("%02x ", receiveframe.data[i]);
printf("\n\n");
//usleep(5000);
flag = 1;
}
}
void *canSend(void *arg){
while(1){
sleep(2);
if(flag == 1){
if((write(sockfd, &sendframe, sizeof(sendframe))) != sizeof(sendframe)){ // 发送
perror("write error");
break;
}
flag = 0;
}
}
}
void threadCreate(void){
pthread_t t1;
pthread_t t2;
int ret;
if(ret = pthread_create(&t1, NULL, canReceive, NULL)){
printf("canReceive thread create fail\r\n");
return;
}
pthread_detach(t1);
if(ret = pthread_create(&t2, NULL, canSend, NULL)){
printf("canSend thread create fail\r\n");
return;
}
pthread_detach(t2);
}
int main(void){
CAN_Config();
//printf("CAN_Config end\n");
threadCreate();
//printf("threadCreate end\n");
while(1){
sleep(10000);
}
return 0;
}
STM32F103ZET6
STM32侧的接收是在CAN接收中断服务函数中完成的,在文件stm32f10x_it.c中添加如下代码:
extern CanRxMsg RxMessage; // 接收缓冲区
extern u8 flag; // 接收中断通知主函数的标志
void CAN_RX_IRQHandler(void)
{
CAN_Receive(CANx, CAN_FIFO0, &RxMessage); // 调用库函数实现报文接收
flag = 1; // 通知主函数报文已收到
}
有报文到来则FIFO0的消息挂起中断触发,调用库函数CAN_Receive实现接收,并且通过简单的标志位通知主函数消息收到。接收缓冲区和通知标志均在主函数中声明。主函数循环检测该标志,如检测到被置1则通过串口把收到的数据打印出来,并立即发送报文给IMX6ULL。主函数的主要代码如下:
CanTxMsg TxMessage; // 发送缓冲区
CanRxMsg RxMessage; // 接收缓冲区
unsigned char senddata[] = {0x78, 0x89, 0x9A, 0xAB, 0xBC, 0xCD, 0xDE, 0xEF}; // 报文数据内容
u8 flag = 0; // 接收中断通知主函数的标志
int main(void) {
uartInit(115200); // 串口初始化
CAN_Config(); // CAN配置
CAN_SetTxMessage(&TxMessage, 0x456, STANDARD, DATA, 8, senddata); // 组织发送报文
CAN_InitRxMessage(&RxMessage, STANDARD, DATA, 8, 0); // 初始化接收缓冲区
while(1) {
if(flag == 1) // 有消息到来
{
u8 i = 0;
printf("[Received CAN data from IMX6ULL] ID[0x%X] IDE[0x%X] RTR[0x%X] DLC[0x%X]\r\n",
RxMessage.StdId, RxMessage.IDE, RxMessage.RTR, RxMessage.DLC);
for(i;i < RxMessage.DLC;i++)
printf("data[%d]:0x%X ", i, RxMessage.Data[i]);
printf("\r\n");
printf("\n");
if(CAN_SendTxMsg(CANx, &TxMessage)) // 发送报文
break;
//printf("send from mailbox:%d\r\n", ret);
flag = 0; // 等待下一次消息到来
}
}
}
结果
两边编译成功后烧写到各自板子中,启动IMX6侧的应用进程,可以看到STM32侧打印的串口消息和IMX6侧进程打印的接收消息如下。可以看出STM32成功接收到了ID为0x123的标准数据帧,每次接收8字节0x01到0x08,IMX6ULL侧成功接收到了ID为0x456的标准数据帧,每次接收8字节:0x78、0x89、0x9a、0xab、0xbc、0xcd、0xde和0xef。