日期 | 作者 | 版本 | 说明 |
---|---|---|---|
2023.02.03 | Mr.Zheng | V1.1 | CAN通讯个人笔记,初版 |
声明
最近刚刚做完一个较复杂项目,为强化巩固,准备写几篇笔记方便后续回溯,本篇重点是个人对CAN通讯的理解与实现,第一部分为CAN的模型架构,CAN的功能实现是第二部分,需要一定的编程基础,希望能够给初学者带来启发。
一、CAN的模型架构
1.CAN总线的通讯模型
CAN协议有不同的国际标准,设计之初需要确认标准,这里用ISO11898CAN 高速通信标准,它的通信速度为 5kbps至1Mbps。下述为此标准下的CAN总线通信模型
第一层:物理层:规定信号传输媒介、电平、硬件收发等
第二层:数据层:物理层接收有用数据进行报错、响应、通知等职能
第三层:网络层:数据传输地址管理
第四层:传输层:数据传输缓冲、排序与错误回溯
第五层:会话层:通讯环中的收发响应、数据收发
第六层:表示层:传输数据的格式转换
第七层:应用层:协议应用,功能表达,需求实现
2.CAN总线的通讯帧结构
以项目用到的某某协议进行CAN的扩展帧结构说明,提前声明,不同公司的CAN协议可能会有不同,这里用一种举例,后续应用换汤不换药基本用法完全一致
起始帧:1 bit 硬件自动生成
仲裁帧:29 bit 就是常说的CAN的ID!仲裁帧扩展ID结构看下一个表
控制帧:6 bit 这里是控制帧存放位置
数据帧:0-64 bit 这就是协议中数据具体传输位置了,收发协议确定后在这传
校验帧:15+1 bit 硬件自动生成
确认帧:2 bit 硬件自动生成
结束帧:7 bit 硬件自动生成
这里选用的扩展帧的方式通讯,仲裁帧的29个bit将会如下图拆分成五项段码
ID28-ID24 源ID:代表载体终端,比如说这个是电池程序就是电池协议ID
ID23-ID19 目标ID:代表联系终端,比如说电池想给bms发数据就填bmsID
ID18-ID16 控制命令:读/写/应答/错误应答/长起始/长传输/长结束/状态码
ID15-ID8 索引:字典型命令集,靠索引与子索引来定向调动命令集中的数据
ID7-ID0 子索引:同上,可以看成目录集的一级标题和二级标题关系
二、CAN协议通讯实现
这里开始附代码并讲解如何实现,不同的协议有差别但是思路完全一致,比如说我想写一个功能:控制器调用电池的当前实时数据,那么你首先就要去根据你的协议设定的各种功能或者数据的存放集合(后面我叫他字典集),写个对应ID的结构体以便于后面使用。
1.数据结构体
根据协议字典集合将你需要用到的数据先打包成结构体,里面根据需求存放一些数据,这也将会是你后面CAN数据传输的必经中转站,我这里直接敲个小demo:
typedef union
{
struct
{
uint8_t S0 : 8; //数据这里可以拆的更细
uint8_t S1 : 8; //占一位
uint16_t S2 : 16; //占两位
uint8_t S4 : 8;
} Sig;
uint8_t Msg[5];
} CANMsg082A6021Union; //082A6021是协议ID下小节面讲
extern CANMsg082A6021Union CANMsg082A6021;
#define ZMX_mode CANMsg082A6021.Sig.S0 //随便举的例子别在乎命名规范
#define ZMX_cur CANMsg082A6021.Sig.S1 //可以定义下各节点数据
#define ZMX_vol CANMsg082A6021.Sig.S2 //后面用变量名拿数据
#define ZMX_Temp CANMsg082A6021.Sig.S4 //也可以直接调用结构体
2.CAN的ID解析
上述结构体例子,里面数据拆成多少,全看需求,自行更改,现在讲解下较为重点的CAN ID,用082A6021举例,要写的太多我直接写纸上拍下来:
通讯帧结构里标注了各个帧的区间位置,结合手稿看下:
082A6021拆成二进制就是0000/1000/0010/1010/0110/0000/0010/0001
源节点ID 在28-24就是08,要看你协议的定义,假设他是控制器
目标节点ID 在23-19就是05,假设他是电池
控制命令码 在18-16就是02,对应表格就是正常应答
索引 在15-8就是60,假设在协议字典集里面的记录是电池数据
子索引 在7-0就是21,假设在协议字典集里面的记录是当前实时电池数据
那么连起来,这个ID的意思就是控制器正常调用了电池的当前实时数据;
同理,根据字典集,可将所有协议功能打包成结构体,用来实现对应协议功能
这些数据在被软件触发执行之后你的结构体里面就存储了调用的CAN总线上你想调用的某个终端的数据,之后就可以使用这些数据进行需求编写了,直接调用结构体元素或者调用变量都行,举个例子
--在屏上显示电池的当前温度
LCD_ShowNum(27,20,CANMsg082A6021.Sig.S4);
--在屏上显示电池的当前电流
LCD_ShowNum(50,20,ZMX_cur);
--在屏上显示电池的当前电压
LCD_ShowNum(50,20,CANMsg082A6021.Sig.S2);
3.CAN配置
拿兆易创新的举个例子,实际应用根据自己的mcu型号移植对应程序
--CAN模式初始化
u8 CAN_Mode_Init(u8 tsjw,u8 tbs2,u8 tbs1,u16 brp,u8 mode)
{
can_parameter_struct can_parameter;
can_filter_parameter_struct can_filter;
rcu_periph_clock_enable(RCU_CAN0);
rcu_periph_clock_enable(RCU_GPIOB);
gpio_init(GPIOB, GPIO_MODE_AF_PP, GPIO_OSPEED_50MHZ, GPIO_PIN_9);
gpio_init(GPIOB, GPIO_MODE_IPU, GPIO_OSPEED_50MHZ, GPIO_PIN_8);
gpio_pin_remap_config(GPIO_CAN_PARTIAL_REMAP, ENABLE);
/* initialize CAN register */
can_deinit(CAN0);
/* initialize CAN */
can_parameter.time_triggered = DISABLE;
can_parameter.auto_bus_off_recovery = DISABLE;
can_parameter.auto_wake_up = DISABLE;
can_parameter.auto_retrans = DISABLE;
can_parameter.rec_fifo_overwrite = DISABLE;
can_parameter.trans_fifo_order = DISABLE;
can_parameter.working_mode = mode;
can_parameter.resync_jump_width = tsjw;
can_parameter.time_segment_1 = tbs1;
can_parameter.time_segment_2 = tbs2;
can_parameter.prescaler = brp;
can_init(CAN0, &can_parameter);
/* initialize filter */
#ifdef CAN0_USED
/* CAN0 filter number */
can_filter.filter_number = 0;
#else
can_filter.filter_number = 0;
#endif
/* initialize filter */
can_filter.filter_mode = CAN_FILTERMODE_MASK;
can_filter.filter_bits = CAN_FILTERBITS_32BIT;
can_filter.filter_list_high = 0x0000;
can_filter.filter_list_low = 0x0000;
can_filter.filter_mask_high = 0x0000;
can_filter.filter_mask_low = 0x0000;
can_filter.filter_fifo_number = CAN_FIFO0;
can_filter.filter_enable = ENABLE;
can_filter_init(&can_filter);
nvic_irq_enable(USBD_LP_CAN0_RX0_IRQn,1,0);
can_interrupt_enable(CAN0, CAN_INTEN_RFNEIE0);
return 0;
}
--CAN发送
u8 Can_Send_Msg(uint32_t CAN_Id ,u8* msg,u8 len)
{
u8 mbox;
u16 i=0;
can_trasnmit_message_struct TxMessage;
TxMessage.tx_sfid=0x00;
TxMessage.tx_efid=CAN_Id;
TxMessage.tx_ff=CAN_FF_EXTENDED;
TxMessage.tx_ft=CAN_FT_DATA;
TxMessage.tx_dlen=len;
for(i=0;i<len;i++)
TxMessage.tx_data[i]=msg[i];
mbox= can_message_transmit(CAN0, &TxMessage);
i=0;
while((can_transmit_states(CAN0, mbox)== CAN_TRANSMIT_FAILED )&&(i<0XFFF))i++;
if(i>=0XFFF)return 1;
return 0;
}
--CAN接收
void vTaskCanRec(void *pvParameters)
{
static int i = 0;
BaseType_t xResult;
const TickType_t xMaxBlockTime = pdMS_TO_TICKS( 100 );
while(1)
{
xResult = xQueueReceive( xQ_CanRec, ( void* )&CAN_Rec, xMaxBlockTime );
if( pdPASS == xResult )
{
Can_Rec_Val(CAN_Rec);
Can_Send_Val(CAN_Rec);
}
}
}
--将MCU接收到的CAN信息保存进结构体里,软件触发条件根据需求自己写
memcpy( &CANMsg082A6021.Msg[0],&CAN_Rec.Data[0], 5);
三、CAN通讯易错点补充(实时更新)
1.CAN通讯失效,纠错分析-硬件层
1.CAN的匹配电阻为120Ω,硬件设计之初就请确认总线通讯中哪个终端加此匹配电阻(后面我会出一个硬件CAN电路设计范例与原理解析,在硬件栏详解此点)。
2.切记,没有焊接can通讯芯片或者can芯片没有正常工作的时候,mcu与can芯片是无有效can回路的,这个时候示波器测mcu的can引脚是无正常can波形的,别认为mcu故障,焊好can芯片以及外围器件再测试。文章来源:https://www.toymoban.com/news/detail-570870.html
2.CAN通讯失效,纠错分析-软件层
1.ISO11898国际规范下的高速CAN通讯波特率一般使用250kbit/s,市面常见CAN盒子调试默认值也是250kbit/s,如无特殊要求请确保使用此波特率,使用其他波特率请按照相对应的规范标准与设计需求;文章来源地址https://www.toymoban.com/news/detail-570870.html
到了这里,关于单片机编程-CAN通讯-理解与实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!