物联网通信之CAN通讯
CAN通讯的整体架构:
局部网络(10至40米的距离内能够保持最高1Mbps的传输速率),所有设备都挂在一根总线,用广播相互发送信息,没有设备地址,没有主机(区别I2C,SPI)但和USART相似都是点对点通信,每条信息均携带11位或29位的标识符,如温度传感器等设备仅关注其订阅的特定标识符(例如ID=0x123的温度相关话题)跟着的信息,而自动忽略其他信息。
优势:不会因为某个设备崩溃而整个系统瘫痪
CAN通讯物理层介绍:
CAN网络节点:控制器(处理收发逻辑电平,一般由MCU处理,例如STM32内部集成了CAN收发器),收发器(保障长距离传输,和外部进行连接),两者要相互连接。
CAN总线网络:
闭环总线网络:遵循ISO11898标准高速,短距离,最大传输距离40m,通讯速度最高为1Mbps,这个还在发展,具体速度和距离看设备设计
开环总线网络:低速远距离,遵循ISO11519-2标准,最大传输距离1Km,通讯速度125Kbps,l两根总线是独立的,不形成闭环,每根线要求串联2.2千欧的电阻
差分信号:为了传输的距离更远,屏蔽周围干扰,CAN_High和CAN_Low走一对差分信号;传统的单端信号传输是一根信号线一根地线,即和0电位进行差分信号;
底层原理:两根线传输表达同一个信号,振幅相同,相位相反;判断两端的差值来判断电平是1还是0,差分走线必须是等长等宽,精密靠近,且在同一层面的两根线
差分信号优缺点:
电路板距离小,一般不考虑差分传输,但是要实现远距离传输都要用差分信号,比如USB协议,485协议,以太网协议和CAN协议在物理层中,都使用了差分信号
差分信号到底这么传输0和1的:以高速CAN为例子:
CAN通讯的协议层:
其中错误帧,过载帧,间隔帧都是由硬件自动完成的,没有办法用软件来控制,对于我们来说,只需要掌握数据帧和遥控帧就行
CAN的帧(报文)种类:广播类型的报文,所有设备都能够收到数据,并没有办法单独指定接收对象,这样效率过低,因此提供本地过滤功能,每个结点对接收到的报文做有选择的响应,即先检查一下这个报文是否是发给我的,检查时是否要整个报文进行解析才知道是不是我需要的呢?并不用,前人开发了一种在报文前设计一个标识符,对应的结点看编号就知道是不是自己订阅的消息,就像去菜鸟取快递,看编号找到自己的快递,其他快递不是我的,我没法出库拿走。
CAN使用短报文,总长度最大实用负载是94位。报文中没有任何明确的地址,相反,可以认为报文是通过内容寻址,也就是说,报文的内容隐式地确定其地址
CAN总线上有5种不同的报文类型(或帧):数据帧,远程帧,错误帧,过载帧,帧间隔
数据帧:包含了数据的一大包数据包,用于发送单元向接收单元发送数据,平常我们发送和接收的都是数据帧
远程帧(遥控帧):接收单元反过来向发送单元请求发送数据。A发送数据帧给B,如果B需要某种控制信息,B知道自己订阅的信息是什么类型,就可以往总线上发请求,谁有就发给B
错误帧:是一种状态帧,检测到当前发送出现了哪些错误,就立刻发出错误帧,向其他单元通知
过载帧(现在不常见,当前CAN控制器会非常智能化的调控数据发送多少快慢,从而避免使用过载帧):接收方处理能力不足,发送方发太快,发太多就会这样了,那接收方就会发送过载帧,喊发送方慢点
帧间隔:把帧和帧之间分隔开
数据帧介绍:
分成两种类型:标准帧和扩展帧,核心在于标准帧标志符的位数小于扩展帧,11位表示的消息种类个,由于引用领域不断扩展导致不够用了,所以就要开始扩展到29位表示的消息种类
(512M)个
标准数据帧:打包好的各种各样的数据放在一起构成了一个进行识别和控制的数据结构
扩展帧和标准帧的不同在于仲裁帧和控制帧:
仲裁段:SRR:替代远程请求位(RTR),隐性信号;IDE:置1表示当前是扩展帧;
18位扩展ID设计在后面明显就是为了兼容考虑,一开始我们并没有扩展帧如果一上来就ID就跟上扩展ID的话,我们没法确认哪个是扩展ID哪个是RTR表示位;再跟上RTR表示数据帧还是扩展帧;前面的ID改跟为SRR相当于一个占位符,因为RTR是要跟在ID结束后的1位的
控制段:R1,R0都是保留位,用来做扩展。
远程帧介绍:区别在于没有数据帧
CAN的总线仲裁:
总线空闲时,A和B同时都要发送信息,我们该如何平衡?就要判定优先级,就叫总线仲裁,就是通过ID来排位,不同ID对应不同的消息类型,那么我们就按照ID越小优先级越高,仲裁时ID间逐位做比较。我们仲裁的是ID,真正的数据并没有发出去,就算输了仲裁,也只是回归等待下一次空闲再发数据罢了,所以总裁也叫做无破环性仲裁,所以0是显性位有数值就可以盖掉别人的1,1是隐性位,差分信号差值就是0,就更没有一样,挨个发送ID位,帧起始,撞车就要仲裁,同时接收总线上的差分信号,两者比较,如果赢总线仲裁后,那我发什么总线就是什么,如果我发的ID 是1,结果发现总线上电平0,和我发送的不一样,那优先级比我高,那我就不发了,等待检测总线上空闲(1),我们再继续发;
如果ID都一样(上文提到每个结点可以发多个ID消息,但结点间的负责ID不能有相同的,所以我们就考虑远程帧和数据帧的抢占),数据帧优先于远程帧,数据帧都在总线上发数据了,那请求帧是请求被人发送数据给自己,所以就先处理正要发的。
CAN总线的位时序:硬件完成同步,我们软件设置Tq
位同步:因为没有时钟而给出的定义
CAN总线还是串行的半双工,同步和异步的主要区分是否有时钟,因为没有时钟,所以没有同步和异步的区分,发送和接收的波特率要一致,如果收发不一致就会导致错位或吞并数据,采样的时候还要对齐采样波形,那么我们就提出了位同步
CAN中提出了位同步的方式来保持通讯时序,一帧中包含了很多个位
CAN把每1位分成4段(从时间角度划分),区分在数据帧我们说的是一位一位的数据
数据同步:根据同步方式差异分类:
硬同步:针对一帧一帧的起始位的
在理想状态下,当检测到一帧数据的起始位时,接收方就会执行硬同步,使得接收方和发送方的数据传播速率一致。但现实是接收方接收到发送方的数据,是有一定的滞后的,误差后累计,滞后就越来越大了。滞后累积的本质是时钟频率差异的线性放大效应。
再同步(重同步):是针对一位一位的数据的
当接收方检测到发送方跳变沿和接收方SS同步段的不一致的时候,就再同步(增长PBS1或缩短PBS2),让它重新回到SS段
位填充:
每5位0填充1位1(因为连续的5位数据不会对通信产生过大影响),避免因为多为数据都是0,所以长时间没有产生跳变沿,导致同步段无法正常判断是否触发再同步(本质是检查同步段)、
在STM32里用到的CAN外设
bxCAN(basic extened CAN)是基本扩展,支持标准数据帧和扩展数据帧,底层支持2.0ACAN协议(只处理标准帧,接收到扩展数据帧就报错)和2.0BCAN协议(Active :同时处理标准数据帧和扩展数据帧,Passive:处理标准数据帧忽略扩展数据帧)
CAN外设:
支持最高通讯速率1Mbit/s,PD1050S芯片是一款CAN的收发器,引脚连接到CAN控制器上Rx和Tx,差分信号线做电平转换(隐形电平->逻辑1,显性电平->逻辑0)
硬件原理图:
CAN控制器的3种工作模式:
CAN控制器的3种测试模式:必须让控制器进入初始化模式的时候才能配置测试模式
CAN外设的功能框图:CAN不支持DMA进行数据收发
外设具有3个发送邮箱,发送报文的优先级可以配置,还能记录发送时间
具有2个3级别深度(2个队列,每个队列只能排3个人)的接收FIFO(CPU来不及处理的就隔着排着队),接收的时候可以用过滤功能只接收或不接收某些ID号的报文,可以配置成自动重发
接收过滤器的两种过滤方式:
标识符列表模式:接收方将订阅的ID列成表,只接收表内的存在的ID(精确比较到ID的每一位),但列表数量有限
掩码模式(屏蔽位模式):可接收报文ID的某几位作为列表,这几位被称为掩码,可以把它理解成关键字搜索,只要掩码(关键字)相同,就符合要求,报文就会被保存到接收FIFO。
32中CAN的位时序:
把传播时间段和相位缓冲段1做了合并
CAN通讯案例1:环回静默模式测试,做自检观察网络是否好用
这个并不是默认的CAN控制器引脚,如果默认引脚被占用我们就可以用重映射引脚来满足需求
寄存器代码:
配置CAN和32单片机连接的引脚,位时序:(配置Tq,配置位时序每一个段占用多少Tq,再同步跳跃宽度),配工作模式,配测试模式
can.h
#ifndef __CAN_H
#define __CAN_H
#include "stm32f10x.h"
//定义结构体,保存接收到的报文信息
typedef struct
{
uint16_t stdID;
uint8_t data[8];
uint8_t len;
}RxMsg;
void CAN_Init(void);
void CAN_SendMsg(uint16_t stdID,uint8_t *data,uint8_t len);
void CAN_ReceiveMsg(RxMsg rxmsg[],uint8_t *msgCount);
#endif
can.c:
#include "stm32f10x.h"
#include "usart.h"
#include "can.h"
static void CAN_FilterConfig(void);
void CAN_Init(void)
{
//时钟
RCC->APB1ENR |=RCC_APB1ENR_CAN1EN;
RCC->APB2ENR |=RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |=RCC_APB2ENR_AFIOEN;
//2.重映射PB8,PB8
AFIO->MAPR |=AFIO_MAPR_CAN_REMAP_1;
AFIO->MAPR &=~AFIO_MAPR_CAN_REMAP_0;
//3.GPIO工作模式 PB8-浮空输入,PB9复用推挽输出
GPIOB->CRH &=~GPIO_CRH_MODE8;
GPIOB->CRH &=~GPIO_CRH_CNF8_1;
GPIOB->CRH |= GPIO_CRH_CNF8_0;
GPIOB->CRH |=GPIO_CRH_MODE9;
GPIOB->CRH &=~GPIO_CRH_CNF9_0;
GPIOB->CRH |= GPIO_CRH_CNF9_1;
//3.CAN初始化配置
//3.1基本配置
//3.1.1进入初始化模式
CAN1->MCR |=CAN_MCR_INRQ;
while ((CAN1->MSR & CAN_MSR_INAK)==0);
//3.1.2退出睡眠模式
CAN1->MCR &=~CAN_MCR_SLEEP;
while ((CAN1->MSR & CAN_MSR_SLAK)==1);
//3.1.3自动离线管理
CAN1->MCR |=CAN_MCR_ABOM;
//3.1.4自动唤醒管理
CAN1->MCR |=CAN_MCR_AWUM;
//3.1.5配置环回静默测试模式
CAN1->BTR |=CAN_BTR_SILM;
CAN1->BTR |=CAN_BTR_LBKM;
//3.2配置时序,先清零再赋值
//3.2.1配置波特率分频器 36MHz,Tq=1us
CAN1->BTR &=~CAN_BTR_BRP;
CAN1->BTR |=35<<0;
//3.2.2配置BS1和BS2的时间长度
CAN1->BTR &=~(CAN_BTR_TS1|CAN_BTR_TS2);
CAN1->BTR|=2<<16;
CAN1->BTR|=5<<20;
//3.2.3再同步跳跃宽度
CAN1->BTR &=~CAN_BTR_SJW;
CAN1->BTR |=1<<24;
//3.3退出初始化模式,正常工作模式
CAN1->MCR &=~CAN_MCR_INRQ;
while ((CAN1->MSR & CAN_MSR_INAK)==0){}
//4过滤器配置
CAN_FilterConfig();
}
//定义静态函数进行过滤器的配置
static void CAN_FilterConfig(void)
{
//1。进入初始化模式
CAN1->FMR |=CAN_FMR_FINIT;
//2.配置过滤器工作模式:0-屏蔽,1-列表
CAN1->FM1R &=~CAN_FM1R_FBM0;
//3.配置位宽:1-32位
CAN1->FS1R |=CAN_FS1R_FSC0;
//4.设置关联FIFO:FIFO0
CAN1->FFA1R &=~CAN_FFA1R_FFA0;
//底层逻辑:FR2中为1的位表示需要严格匹配的位,对应FR1中的值必须与消息ID完全一致
//5.设置过滤器组0的ID寄存器:FR1
CAN1->sFilterRegister[0].FR1=0x060<<21;
//6.设置过滤器组0的掩码寄存器:FR2=0,表示来者不拒
CAN1->sFilterRegister[0].FR2=0x7f1<<21;
//7.激活过滤器组0
CAN1->FA1R |=CAN_FA1R_FACT0;
//8.退出初始化模式
CAN1->FMR &=~CAN_FMR_FINIT;
}
/// @brief 发送报文,使用发送邮箱0
/// @param stdID ID
/// @param data 数据
/// @param len 长度
void CAN_SendMsg(uint16_t stdID,uint8_t *data,uint8_t len)
{
//1.等待发送邮箱0为空
while ((CAN1->TSR & CAN_TSR_TME0)==0);
//2.包装要发送的数据帧
//2.1设置标准ID,掩码清零,移位写入
CAN1->sTxMailBox[0].TIR &=~CAN_TI0R_STID;
CAN1->sTxMailBox[0].TIR |=stdID<<21;
//2.2数据帧的格式:为标准帧
CAN1->sTxMailBox[0].TIR &=~CAN_TI0R_IDE;
//2.3设置为数据帧
CAN1->sTxMailBox[0].TIR &=~CAN_TI0R_RTR;
//2.4设置数据长度
CAN1->sTxMailBox[0].TDTR &=~CAN_TDT0R_DLC;
CAN1->sTxMailBox[0].TDTR|=len<<0;
//2.5吧数据填写到对应的数据寄存器里
CAN1->sTxMailBox[0].TDLR=0;
CAN1->sTxMailBox[0].TDHR=0;
/*当数据被发送到总线上时,
DATA0是第一个字节,
DATA7是最后一个字节,
符合CAN协议的顺序。*/
for (uint8_t i = 0; i < len; i++)
{
if (i<4)
{
CAN1->sTxMailBox[0].TDLR |=data[i]<<(i*8);
}
else
{
CAN1->sTxMailBox[0].TDHR |=data[i]<<((i-4)*8);
}
}
//3.请求发送数据帧
CAN1->sTxMailBox[0].TIR |=CAN_TI0R_TXRQ;
//4.等待成功发送
while ((CAN1->TSR &CAN_TSR_TXOK0)==0);
}
void CAN_ReceiveMsg(RxMsg rxmsg[],uint8_t *msgCount)
{
//1.判读FIFO是否有数据,获取FIFO0的报文个数,通过指针返回
*msgCount =(CAN1->RF0R & CAN_RF0R_FMP0)>>0;
//2.循环读取每一个报文
for (uint8_t i = 0; i < *msgCount; i++)
{
//定义指针,指向当前保存报文的数据对象
RxMsg *msg=&rxmsg[i];
//2.1读取ID,
msg->stdID=(CAN1->sFIFOMailBox[0].RIR >>21) & 0X7FF;
//2.2读取数据长度,和0x07位与目的是清除寄存器前面多余的数据,只取后3位
msg->len=(CAN1->sFIFOMailBox[0].RDTR>>0) & 0X0f;
//2.3读取数据
uint32_t low=CAN1->sFIFOMailBox[0].RDLR;
uint32_t high=CAN1->sFIFOMailBox[0].RDHR;
for (uint8_t j = 0; j < msg->len; j++)
{
if (j<4)
{
/*右移后位与清除前面多余的数据*/
msg->data[j]=low>>(j*8) &0xff;
}
else
{
msg->data[j]=high>>((j-4)*8 )&0xff;
}
}
//3.释放FIFO0,出队,再读取下一个报文
CAN1->RF0R|=CAN_RF0R_RFOM0;
}
}
main.c
#include "stm32f10x.h"
#include "usart.h"
#include "can.h"
#include "string.h"
int main(void)
{
USART_Init();
CAN_Init();
printf("Loopback Silent Mode Test, Register Version\n");
uint16_t stdID=0x066;
uint8_t *data="abcdefg";
CAN_SendMsg(stdID,data,strlen((char *)data));
stdID=0x066;
data="123";
CAN_SendMsg(stdID,data,strlen((char *)data));
stdID=0x067;
data="xys";
CAN_SendMsg(stdID,data,strlen((char *)data));
RxMsg rsmag[3];
uint8_t msgCount;
CAN_ReceiveMsg(rsmag,&msgCount);
for (uint8_t i = 0; i < msgCount; i++)
{
/* code */
printf("stdID= %#X,len=%d,data=%.*s\n",rsmag[i].stdID,rsmag[i].len,rsmag[i].len,rsmag[i].data);
}
while(1)
{
}
}
HAL库配置+HAL库代码:
can.h
/* USER CODE BEGIN Prototypes */
typedef struct
{
uint16_t stdId;
uint8_t data[8];
uint8_t length;
} RxDataStruct;
void CAN_Filter_Config(void);
void CAN_SendMsg(uint16_t stdId, uint8_t *data, uint8_t length);
void CAN_ReceiveMsg(RxDataStruct rxDataStruct[], uint8_t *msgCount);
/* USER CODE END Prototypes */
can.c
/**
* @description: 配置过滤器
*/
void CAN_Filter_Config()
{
CAN_FilterTypeDef sFilterConfig;
sFilterConfig.FilterBank = 0; // 过滤器编号, CAN1是0-13, CAN2是14-27
sFilterConfig.FilterMode = CAN_FILTERMODE_IDMASK; // 采用掩码模式
sFilterConfig.FilterScale = CAN_FILTERSCALE_32BIT; // 设置筛选器的尺度, 采用32位
sFilterConfig.FilterIdHigh = 0X0000; // 过滤器ID高16位,即CAN_FxR1寄存器的高16位
sFilterConfig.FilterIdLow = 0X0000; // 过滤器ID低16位,即CAN_FxR1寄存器的低16位
sFilterConfig.FilterMaskIdHigh = 0X0000; // 过滤器掩码高16位,即CAN_FxR2寄存器的高16位
sFilterConfig.FilterMaskIdLow = 0X0000; // 过滤器掩码低16位,即CAN_FxR2寄存器的低16位
sFilterConfig.FilterFIFOAssignment = CAN_RX_FIFO0; // 设置经过筛选后数据存储到哪个接收FIFO
sFilterConfig.FilterActivation = ENABLE; // 是否使能本筛选器
sFilterConfig.SlaveStartFilterBank = 14; // 指定为CAN1分配多少个滤波器组
HAL_CAN_ConfigFilter(&hcan, &sFilterConfig);
}
/**
* @description: 发送信息
* @param {uint16_t} stdId
* @param {uint8_t} *data
* @param {uint8_t} length
*/
void CAN_SendMsg(uint16_t stdId,
uint8_t *data,
uint8_t length)
{
/* 1. 检测发送邮箱是否可用 */
while (HAL_CAN_GetTxMailboxesFreeLevel(&hcan) == 0)
;
CAN_TxHeaderTypeDef txHeader;
txHeader.IDE = CAN_ID_STD; // 标准帧还是扩展帧
txHeader.RTR = CAN_RTR_DATA; // 帧的类型: 数据帧还是远程帧
txHeader.StdId = stdId; // 标准帧的id
txHeader.DLC = length; // 发送的数据长度 单位字节
uint32_t txMailBox; // 会把这次使用的邮箱存入到这个变量
/* 2. 发送消息 */
HAL_CAN_AddTxMessage(&hcan, &txHeader, data, &txMailBox);
}
/**
* @description: 接收消息
* @param {RxDataType} *
*/
void CAN_ReceiveMsg(RxDataStruct rxDataStruct[], uint8_t *msgCount)
{
/* 1. 检测FIFO0收到的报文个数 */
*msgCount = HAL_CAN_GetRxFifoFillLevel(&hcan, CAN_RX_FIFO0);
/* 2. 遍历出所有消息 */
uint8_t i;
CAN_RxHeaderTypeDef rxHeader;
for (i = 0; i < *msgCount; i++)
{
HAL_CAN_GetRxMessage(&hcan, CAN_RX_FIFO0, &rxHeader, rxDataStruct[i].data);
rxDataStruct[i].stdId = rxHeader.StdId;
rxDataStruct[i].length = rxHeader.DLC;
}
}
/* USER CODE END 1 */
main.c
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_CAN_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
/* 1. 配置过滤器 */
CAN_Filter_Config();
/* 2. 启动CAN总线 */
HAL_CAN_Start(&hcan);
/* 3. 发送数据 */
uint16_t stdId = 0x011;
uint8_t *tData = "abcdefg";
CAN_SendMsg(stdId, tData, strlen((char *)tData));
printf("发送完毕...\r\n");
tData = "123";
CAN_SendMsg(stdId, tData, strlen((char *)tData));
printf("发送完毕...\r\n");
/* 4. 接收数据 */
RxDataStruct rxDataStruct[8];
uint8_t rxMsgCount;
CAN_ReceiveMsg(rxDataStruct, &rxMsgCount);
printf("接收完毕 rxMsgCount = %d...\r\n", rxMsgCount);
/* 5. 输出消息 */
uint8_t i;
for (i = 0; i < rxMsgCount; i++)
{
RxDataStruct msg = rxDataStruct[i];
printf("stdId = %d, length = %d, msgData = %s\r\n", msg.stdId, msg.length, msg.data);
}
while (1)
{
}
}