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Ω的电阻,由原理图可见板载已经接入了该电阻,无需额外接入。两个板子连接的实物图如下。
IMX6开发板上的CAN收发器
双机CAN通信连线示意图
双机CAN通信连接实物

驱动层实现

这里所说的驱动层,是从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 = <&reg_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,如下:
使能NXP自带的flexcan驱动
保存设置后CAN驱动的编译选项就存在了编译配置文件中,再编译内核就能使上面的设备树配置和驱动配置生效了,将生成的Image和dtb文件载入IMX6ULL开发板,即可看到系统中的CAN设备,如下。在/sys/class/net/目录下的can0即为我们配置的外设,查看其控制器设备树的status可见值为okay,和我们设置的一样。
CAN设备
CAN控制器的设备树

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 STM32 send 0x123 send 0x456 send 0x123 send 0x456 ...... IMX6 STM32

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。
STM32打印的接收消息
IMX6进程打印的接收消息

  • 9
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值