前言:

        我正在做两个GD32之间的CAN通讯,软件写好,硬件连接好后一上电发现怎么都发不了接收不了,于是开始了长达五天多的问题排查,我也是第一次学习CAN通讯,为此分享下CAN问题排查的一些思路和经历。

软件配置:

    整体过程:

               GD32F103C8T6是一款体积小,性能不错的国产芯片,是STM32F103的很好平替,价格也有优势,官方的标准库也支持keil5,所以很适合进行学习和开发小项目。

                CAN通讯主要有两根线CANH CANL ,因为CAN通讯是通过两条线的电压差来进行数据的传输,也叫差分电压,这种比较方式比较稳定,不会因为一些电磁影响而数据出错,速度也很快,几百k到1M。主要配置过程为:配置GPIO,CAN ,配置过滤器, 实现收发函数。

  配置GPIO 

   首先要做的就是查看官方给的数据手册,官网就有GD32F103C8T6 | MCU选择器 | 兆易创新 GigaDevice | 官方网站

我们先要知道这款芯片的CAN口 搭载在哪个总线上,然后他有几个CAN口,分别是复用了哪些GPIO口,还要记得开启引脚重映射。

我们可以看到搭载在APB1总线上的,然后分频后是108M/2=54MHz,然后使用的是CAN0,对应PB8,PB9 因此GPIO配置就是

static void CAN_GPIO_Init(void){
		//使能时钟
		rcu_periph_clock_enable(RCU_GPIOB); //GPIOB使能
        rcu_periph_clock_enable(RCU_CAN0);  // CAN0
		rcu_periph_clock_enable(RCU_AF);  // 复用时钟
		//配置GPIO PA8:RX PA9:TX 
		gpio_init(GPIOB,GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, CAN_TX);//复用推挽输出
		gpio_init(GPIOB,GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, CAN_RX);//上拉输入 
		gpio_pin_remap_config(GPIO_CAN_PARTIAL_REMAP,ENABLE); //开启引脚重映射
}

配置CAN参数

这个主要就是通过查看官方给的函数接口,一般都是配置结构体,一般有配置时间段1,2,预分频系数、同步段、以及帧的格式(标准帧还是扩展帧)、数据格式等等。

static void CAN_Struct_Init(void){
			can_flag_clear(CAN0, CAN_FLAG_ERRIF);   // 清除错误中断
			can_flag_clear(CAN0, CAN_FLAG_WERR);    // 清除警告
			can_flag_clear(CAN0, CAN_FLAG_PERR);    // 清除校验错
			can_flag_clear(CAN0, CAN_FLAG_BOERR);   // 清除总线关闭
			can_flag_clear(CAN0, CAN_FLAG_RFF1);    // 强制清锁死的FIFO1满标志
			can_flag_clear(CAN0,CAN_FLAG_SLPIF);    //清除睡眠标志位
        
			can_parameter_struct can_init_struct; //CAN参数结构体初始化
	
			//复位CAN0 初始化配置结构体
			can_deinit(CAN0);
			can_struct_para_init(CAN_INIT_STRUCT,&can_init_struct);

			//核心配置
			can_init_struct.working_mode = CAN_NORMAL_MODE;      // 正常模式(硬件通讯)
			//can_init_struct.working_mode = CAN_LOOPBACK_MODE; // 回环模式(自测用)
			
	
			can_init_struct.prescaler = 24;           // 预分频
			can_init_struct.resync_jump_width = CAN_BT_SJW_1TQ;   // SJW=1
			can_init_struct.time_segment_1 = CAN_BT_BS1_5TQ ;      // BS1=8
			can_init_struct.time_segment_2 = CAN_BT_BS1_3TQ ;      // BS2=3 → 250kbps
            // 波特率 =  APB1时钟(54MHz)  /  [  prescaler × ( 1 + BS1 + BS2 )  ]

			// 自动功能 重传
			can_init_struct.auto_bus_off_recovery = ENABLE;
			can_init_struct.auto_retrans = ENABLE;

			// 其他功能
			can_init_struct.time_triggered = DISABLE;
			can_init_struct.auto_wake_up = ENABLE;
			can_init_struct.rec_fifo_overwrite = DISABLE;
			can_init_struct.trans_fifo_order = DISABLE;
			
			//初始化CAN
			can_init(CAN0,&can_init_struct);
		
			can_interrupt_enable(CAN0, CAN_INT_RFNE1);//这里是中断使能 
			nvic_irq_enable(CAN0_RX1_IRQn,0, 0); //依旧使能中断NVIC 这里使用的是RX1 FIFO1
}

配置过滤器参数

        过滤器的作用就是过滤接收的CAN报文,进行筛选,比如我想要哪些ID的报文,我要从哪个接收邮箱里面取FIFO0 还是FIFO1 怎么检查?掩码模式 多少位16/32 等等 也是结构体的形式

        

static void CAN_Struct_Init(void){
		
			can_filter_parameter_struct can_filter_init_struct;
		
		
			//初始化过滤器结构体
			can_struct_para_init(CAN_FILTER_STRUCT,&can_filter_init_struct);
			// 核心:掩码全0 → 所有ID都放行
			can_filter_init_struct.filter_list_high = 0x0000;
			can_filter_init_struct.filter_list_low  = 0x0000 ;//过滤器ID 即目标ID 全0就是接收所有ID的报文
			can_filter_init_struct.filter_mask_high = 0x0000;
			can_filter_init_struct.filter_mask_low  = 0x0000;// 掩码 该位为1必须一样 该位为0 则不管

			// 固定配置
			can_filter_init_struct.filter_fifo_number = CAN_FIFO1; //从FIFO1邮箱取
			can_filter_init_struct.filter_number = 0; 
			can_filter_init_struct.filter_mode = CAN_FILTERMODE_MASK;   // 掩码模式
			can_filter_init_struct.filter_bits = CAN_FILTERBITS_32BIT; //32位
			can_filter_init_struct.filter_enable = ENABLE;
			
			can_filter_init(&can_filter_init_struct);
		
	
}

收发函数

        发送函数

               msg是需要自己定义的消息结构体,简单定义下就行

typedef struct {
    uint32_t id;         // CAN ID(标准ID:0~0x7FF)
    uint8_t data_len;    // 数据长度(0~8)
    uint8_t data[8];     // 数据内容
} can_msg_t;
uint8_t can_send_msg(can_msg_t* msg){
	

		//获取CAN错误状态
		can_error_enum err = can_error_get(CAN0);
		if(err != CAN_ERROR_NONE){
				printf("CAN ERROR!\r\n");
				printf("%d\r\n",err);
				return 1;
		} //打印什么参数 就从官方给的错误里面找

		uint8_t mail;
		can_trasnmit_message_struct can_msg_struct;
		//初始化默认结构体
		can_struct_para_init(CAN_TX_MESSAGE_STRUCT,&can_msg_struct);
		//核心配置
		can_msg_struct.tx_dlen = msg->data_len;  //数据长度
		can_msg_struct.tx_sfid = msg->id ; // ID
		can_msg_struct.tx_ff =CAN_FF_STANDARD;   // 强制标准帧
		can_msg_struct.tx_ft =CAN_FT_DATA;  // 强制数据帧
		for(uint8_t i=0;i<msg->data_len;i++){
				can_msg_struct.tx_data[i] = msg->data[i];
		}
		
		mail = can_message_transmit(CAN0,&can_msg_struct); //主要是这个函数 返回邮箱的值 
	
		if (mail != CAN_MAILBOX0 && mail != CAN_MAILBOX1 && mail != CAN_MAILBOX2) {
            printf("Transmit request failed\n");
		} 
		else {
        /* 等待发送完成并检查硬件寄存器 */
        //while (can_flag_get(CAN0, CAN_FLAG_MTF0 + mail) == RESET);
        /* 清除标志 */
        can_flag_clear(CAN0, (can_flag_enum)(CAN_FLAG_MTF1));
		}
		printf("SEND FINISH!!!\r\n");

		return 0;
}

        接收函数

        还是大差不差 初始化消息的结构体,然后接收函数 ,记得接收完一定要手动释放FIFO1邮箱,以及清除好标志位

uint8_t can_msg_receive(can_msg_t* msg){
		
		//获取CAN错误状态
		can_error_enum err = can_error_get(CAN0);
		if(err != CAN_ERROR_NONE){
				printf("CAN ERROR!\r\n");
				return 1;
		}
		can_receive_message_struct can_rx_struct;
		//初始化
		can_struct_para_init(CAN_RX_MESSAGE_STRUCT,&can_rx_struct);
	
			
				//接收数据 
				can_message_receive(CAN0,CAN_FIFO1,&can_rx_struct);
				
				//释放
				can_fifo_release(CAN0,CAN_FIFO1);
				can_flag_clear(CAN0, CAN_FLAG_RFF1);
				//将数据给定义的结构体
				msg->data_len = can_rx_struct.rx_dlen ;
				msg->id = can_rx_struct.rx_sfid ;
				for(uint8_t i=0;i<can_rx_struct.rx_dlen;i++){
						 msg->data[i] = can_rx_struct.rx_data[i] ;
						}
			
			
			return 0;
}

   下面是使用了中断接收的方式,通过重写中断服务函数来实现。

void CAN0_RX1_IRQHandler(void){
			
			can_receive_message_struct can_rx_struct1;
			can_struct_para_init(CAN_RX_MESSAGE_STRUCT,&can_rx_struct1);
			can_message_receive(CAN0,CAN_FIFO1,&can_rx_struct1);
			//printf("receive sfid:%d  ff:%d\r\n",can_rx_struct1.rx_sfid ,can_rx_struct1.rx_ff);
			can_fifo_release(CAN0, CAN_FIFO1);
			if(can_rx_struct1.rx_sfid == 0x321 && can_rx_struct1.rx_ff == CAN_FF_STANDARD)
			{
						
						//验证收到了报文
						
			}
			can_fifo_release(CAN0,CAN_FIFO1);
			can_flag_clear(CAN0, CAN_FLAG_RFF1);
			

}

硬件部分

        GD32F103内置了CAN控制器,但是CAN通讯还需要能够将电平转换为差分信号的电路器件,也就是CAN收发器。

        

这里引用其他人的图,可以清晰的看到CAN多主节点通讯的硬件连接结构,在CANH CANL总线的两端分别都需要一个120欧姆的电阻来提供足够的阻抗来防止反射,使得末端能够完全吸收信号,以及提供稳定的差分电压,这个值不是算出来的,是测出来的。我是从网上买的TJA1050 CAN收发器模块,这个芯片耐压好,性能高。

        其中这个TJA1050芯片也是很有讲究,我详读了他的数据手册,它的工作电压一般在4.75-5.出头这样子,它比较敏感,因此最好用独立的外接电源或者芯片的5V引脚来提供给供电,要用万用表测的精确的5V,要不然过大的电压有可能击穿烧毁芯片(当然 这芯片耐压)

        这是官方的手册介绍图,可以看到有八个引脚,TXD RXD就是接GD32的TX RX,注意和UART的串口不一样 是TX - TX RX - RX  GND和GD32共地 VCC接5V 然后就是 Vref 这个是参考输出电压,CANH - CANH  CANL - CANL ,最后是S引脚, 用来选择高速输出还是静默模式,一般来说S引脚都是接GND 为0V ,如果通讯异常,它可能就是一个错误点。

        CANH CANL,在空闲的时候为隐形状态,都为2.5V左右,没有差分电压。在发送接收数据的时候为闲性状态, CANH 3.5V CANL 1.5V 一般有2V左右的差分电压。

        这个是手册里面高速CAN模式下的状态电平参考。可以参考来通过逻辑分析仪来分析波形,看看是不是正确的关系。

        

USB转CAN模块 网上也有很多,我选择了一个支持PCAN驱动的 然后从PCAN官网下载驱动,里面会带有PCAN VIEW工具,也就是上位机软件。

排查错误:

        回环测试

               回环测试是隔离外部硬件,使数据只通过GD32内部的CAN控制器进行收发,主要是查看主控芯片以及CAN控制是否有损坏。用法很简单就是在模式下选择回环即可

		    //can_init_struct.working_mode = CAN_NORMAL_MODE;      // 正常模式(硬件通讯)
			can_init_struct.working_mode = CAN_LOOPBACK_MODE; // 回环模式(自测用)

         寄存器

           我觉得能够高效查看CAN是在哪个环节出问题的重要方式就是直接查看CAN相关的寄存器的值,当然, 官方也已经提供了很多封装好的接口。我这里更加推荐使用keil的debug模式,能够直接查看所有寄存器相关的值,然后就是需要去查看官方的数据手册,看看具体每个寄存器的哪个值代表着什么。

        

比如TSTAT寄存器里面的MTFNERR0 MTF0 

还可以查看FIFO邮箱里面到底有没有进入数据,数据ID到底对不对?

 抓波形

   通过逻辑分析仪或者示波器,可以查看具体引脚是否有正确的波形,学生钱包有限,网上20块的24M速率的就够够的了,一般来说范围应该是需要探测波形的十倍差不多。

        但是好像能看的也很少。。。 而且有些东西还会误判说是错误

        

最开始搞了两三天,一直发不了收不了。。。我去网上查看别人的,看官方的例程,发现自己没有引脚重映射,开启后还是不行。。就买逻辑分析仪,也查不出什么波形。。。 就是TX RX一直没波形,CANH CANL也没有 我起初以为难道是我买的CAN收发器坏的?我回环测试没问题啊,最后在确认工作电压的时候,我用万用表测S引脚,发现怎么是4.8V?欸我去,不应该是0V吗?我一看发现,GND被我接到了5V.。。。 正确接好线后,就成功发送了报文0X123 接收0X321报文

        

所以,我觉得还是要细心。。。 当然,因为这个虽然拖了有一个星期,但是倒逼我去了解了很多CAN协议相关的知识与其他人的经验,也算是学习了。。 为此分享出来

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐