STM32HAL库学习——CAN笔记

CAN笔记

虽然CAN协议本身具有一定的复杂度,但实际上使用CAN进行基本的数据收发是非常简单的,因为大部分工作都是硬件帮我们完成的,我们要做的仅仅是使用库函数往FIFO(CAN外设的某几个寄存器)中写入/读取数据而已。

CAN有ISO 11898 和 ISO 11519-2 两种标准,本文只涉及ISO 11898(闭环、高速)

一、基本概念

CAN是一种通讯协议,在汽车、工业、机器人等领域广泛使用。

1、CAN的波形

​ CAN有两根通讯线:CAN H 和 CAN L 。图中黄色的为CAN H,蓝色的为CAN L 。

在这里插入图片描述

可以看到,在总线空闲的时候,这两个信号大约都为2.5V左右,因此电位差几乎为0。此时为隐性电平,代表逻辑“1”。而当数据要开始传输的瞬间,CAN H的电压变为3.5V左右,CAN L的电压变为1.5V左右,此时电位差为2V左右,为显性电平,代表逻辑“0”。

​ 可能这部分会有些绕,为什么隐性电平是逻辑“1”,显性电平是逻辑“0”,记不清怎么办?其实我们只需要看CAN L 这根线,也就是图中黄色的那个信号,它为高(2.5V)就是逻辑“1”,为低(1.5V)就是逻辑“0”。其实STM32的CANTX脚的电平是和CAN L 一样的(当然电压不一样)。

2、CAN的优点

  1. 使用差分线来传输数据,抗干扰能力强。
  2. 能在较高的通信速率下(1Mbps)传输较远的距离(40米)。
  3. 一条总线上可挂载多个节点,且节点之间不分主从。总线空闲时,任何一个节点都可以主动对外发送报文。
  4. 节点之间不通过地址来相互访问,而是通过报文的ID来决定报文的归属。
  5. ID自带优先级属性。当多个节点同时发消息时,ID优先级最高的报文仲裁获胜,发送该报文的节点成功发送完整报文。而仲裁失利的节点则被打断发送,只有等到总线再次空闲时才有机会重发。

最后,关于STM32(其他芯片是怎么样的不清楚,没有用过)的CAN外设还有其他的一些优点:报文的打包、解帧、校验以及重发机制等都是由硬件来完成的,我们要做的也就是发送报文的时候往发送邮箱填数据,接收报文的时候从接受FIFO取数据而已。

3、个人理解

位时序: 这部分可能是最容易把人弄晕的地方。其实初次学习并不需要特别在意这部分的内容,这里主要是配置波特率、采样点以及同步用的。因为CAN属于异步通信,需要配置波特率,传输一个bit的时间等于1/波特率。假如波特率是1M的话,传输1bit需要1us的时间。而这1us又被分成了所谓的SS段、PTS段、PBS1段和PBS2段,这些主要是配置采样点和用于信号同步的,实际上信号同步都是由硬件完成的,并不用太纠结这一块了解即可,包括STM32的位时序的各个段都不是严格按照这些定义的。我们要做的是配置出正确的波特率,以及将采样点放在中间靠后一些的位置。

仲裁: 当多个节点检测到总线空闲,并同时发送数据时。某一时刻,有的节点这一bit要发送隐性电平,而有的节点要发送显性电平,由于显性电平的优先级更高,整个总线的这一位都会被拉成显性电平(不知道大家还记不记得IIC只要有一个设备拉低总线整条总线都为低电平,CAN这里也类似)。而此刻发送隐性电平的节点发现此时CAN总线上的电平与自己期望的不一致,它就知道自己仲裁失利,因而停止继续发送数据,转为接收状态。而CAN总线上已经有数据在发送时(总线不空闲),其他节点是不允许发送数据的。

位填充: 细心的朋友可能会发现,如果通过逻辑分析仪解析的值和波形一位一位对比会有不一样,那是因为CAN有位填充的机制。如果有连续5位一样的电平,那么第6位会强制翻转一下。

​ 总之这里只是做个简单的介绍,如果想要更深入的了解,还是要去阅读专门的CAN协议手册。因为个人不够专业,也不想从手册上大段大段的复制,因而讲得不够全面,但都是个人的理解。

二、配置

​ 配置使用CubeMX,实际上东西并不多,最主要是波特率的配置。如果理解了前面的内容,即使是手写代码来配置也很轻松。

在这里插入图片描述

首先是勾选顶部将外设使能,然后看到共有三块内容:

1、Bit Timings Parameters

这一块主要是配置波特率以及采样点的。因为CAN是异步通信,所以通信各方需要事先约定相同的波特率。

​ 波特率的计算可以下载网上专门的计算器,也可以自己简单的算一下。
时 钟 频 率 ÷ 分 频 系 数 = 波 特 率 × 这 三 个 t i m e 之 和 时钟频率÷分频系数 = 波特率×这三个time之和 ÷=×time
​ 这里的时钟频率一般是APB1外设时钟,当然具体是哪个需要看手册确认一下,我这里用的是主频为168M的F4,APB1外设时钟的频率为42MHz。然后分频系数就是第一个参数,这里我选择7。最后三个time相加等于6。因此波特率等于时钟频率÷分频系数÷三个time之和:
波 特 率 = 42000000 ÷ 7 ÷ 6 = 1000000 波特率 = 42000000÷7÷6 = 1000000 =42000000÷7÷6=1000000
也就是波特率为1M。

在这里插入图片描述

​ 然后解释一下这三个time是干什么的,怎么取值。它们是用来决定采样点的,可以看下手册的这张图:

在这里插入图片描述

这三段是和CubeMX中的三个选项一一对应的,总之采样点在BS1和BS2之间,只要让它在中间偏后一点就行了。

2、Basic Parameters

在这里插入图片描述

这些是配置基本参数用的:

  1. 时间触发模式:会自动为报文生成时间戳,没有研究过,暂时默认关闭。
  2. 自动总线关闭管理:没有研究过,暂时默认关闭。
  3. 自动唤醒模式:使能后,当CAN外设在休眠状态时如果CAN总线有数据,则自动唤醒CAN外设。
  4. 自动重发:使能后,如果因为仲裁失败(总线冲突)或是其他原因导致发送失败,会自动重发。建议使能。
  5. 接收FIFO锁定模式:如果使能,当接收FIFO满时,下一条数据会被丢失。如果不使能,则覆盖前面的数据。
  6. 发送FIFO优先级:当发送邮箱中同时有多个帧,是按照先进先出的顺序发送还是按照ID的优先级发送。如果不使能,则按照ID优先级发送。

3、Advanced Parameters

在这里插入图片描述

这一项主要是用来调试用的,如果只是正常使用,选择 Normal 模式。

  1. 正常模式:CAN外设正常地向CAN总线发送数据并从CAN总线上接收数据。
  2. 回环模式:CAN外设正常向CAN总线发送数据,同时接收自己发送的数据,但不从CAN总线上接收数据。在学习CAN外设的时候非常有用,特别是在没有专门的USB转CAN模块也没有两块开发板的时候。
  3. 静默模式:CAN外设不向CAN总线发送数据,仅从CAN总线上接收数据,但不会应答。一般用于检测CAN总线的流量。
  4. 静默回环模式:CAN外设不会往CAN总线收发数据,仅给自己发送。一般用于自检。

配置完成后生成工程即可,但在进行其他操作之前不要忘了要先调用下面这个函数(只需在初始化过程中调用一次即可):

HAL_StatusTypeDef HAL_CAN_Start(CAN_HandleTypeDef *hcan);

因为CubeMX生成的代码是没有帮我们使能CAN的。

三、发送

​ CAN的发送是最简单的,CAN协议总共定义了5种类型的帧,但我们能人为发送的其实只有数据帧和遥控帧。如果我们要发送数据帧,则还要定义数据的长度。不管是数据帧还是遥控帧,我们可以决定这一帧是使用标准ID还是扩展ID,以及相应ID的内容。

发送用到的结构体如下:

typedef struct
{
  uint32_t StdId;    //标准ID
  uint32_t ExtId;    //扩展ID
  uint32_t IDE;      //用来决定报文是使用标准ID还是扩准ID
  uint32_t RTR;      //用来决定报文是数据帧要是遥控帧
  uint32_t DLC;      //数据长度,取值为0-8
  FunctionalState TransmitGlobalTime; 
//最后这个是时间触发模式用的,开启后会自动把时间戳添加到最后两字节的数据中。目前没有用到,选择 DISABLE 
} CAN_TxHeaderTypeDef;

成员:

  1. StdId :如果将要发送的报文使用标准ID,那么这个成员便记录标准ID的值
    取值: 0x0 ~ 0x7FF

  2. ExtId :如果将要发送的报文使用扩展ID,那么这个成员便记录扩展ID的值
    取值: 0x0 ~ 0x1FFFFFFF

  3. IDE :用来决定报文使用标准ID还是扩准ID
    取值: CAN_ID_STDCAN_ID_EXT

  4. RTR :用来决定报文是数据帧要是遥控帧
    取值: CAN_RTR_DATACAN_RTR_REMOTE

  5. DLC :用来记录数据帧的数据长度,单位字节(如果要发送的是遥控帧,该成员中的内容不起作用)
    取值:0 ~ 8

  6. TransmitGlobalTime :目前没有用到,选择 DISABLE
    取值: ENABLEDISABLE

结构体中的 .IDE 成员是用来决定报文是使用标准ID还是扩准ID,如果这个成员等于 CAN_ID_STD ,也就是使用标准ID,此时 .ExtId 中的内容就不起作用了;反之亦然。

发送用到的函数如下:

HAL_StatusTypeDef HAL_CAN_AddTxMessage(CAN_HandleTypeDef *hcan, CAN_TxHeaderTypeDef *pHeader, uint8_t aData[], uint32_t *pTxMailbox);

参数:

  1. *hcan :can的句柄,由CubeMX自动帮我们定义
  2. *pHeader :发送结构体
  3. aData[] :要发送的数据
  4. *pTxMailbox :发送这条报文用的是哪个邮箱,这个作为输出参数。(因为STM32的CAN外设拥有三个发送邮箱,库函数会帮我们找到空的邮箱并把一帧报文装进去,这个参数便记录用到的邮箱号,具体怎么用目前还不清楚。因为CAN总线可能拥挤或是报文被抢占,因此数据并不是填到FIFO中就能马上发出去的,而是要等到合适的时机才能发出去)

示例:

CAN_TxHeaderTypeDef can_Tx;
uint8_t sendBuf[5] = {"hello"};
uint32_t box;

int main(void)
{
  	HAL_Init();
 	SystemClock_Config();
  	MX_GPIO_Init();
  	MX_CAN_Init();

    HAL_CAN_Start(&hcan1);
  
    can_Tx.StdId = 0x123;
    can_Tx.ExtId = 0;
    can_Tx.IDE = CAN_ID_STD;
    can_Tx.RTR = CAN_RTR_DATA;
    can_Tx.DLC = 5;
    can_Tx.TransmitGlobalTime = DISABLE;

  	while (1)
  	{
      	HAL_CAN_AddTxMessage(&hcan1, &can_Tx, sendBuf, &box);
      	HAL_Delay(100);
  	}
}

效果:每隔100ms发送一条报文

在这里插入图片描述

可以自行看一看相关的寄存器。前面提到的邮箱其实就是一组寄存器,里面可以保存一条报文的全部内容(包括标准ID或是扩展ID的内容、数据帧还是遥控帧、数据帧数据的长度,以及最多8字节的数据)。然后一个CAN外设一共有三组这样的寄存器。

四、筛选器

由于不配置筛选器就无法接收数据,因此先介绍筛选器再讲解数据接收。但这方面的内容可能不容易被初学者接受,因此可以暂且跳过,直接复制示例1的代码即可。

​ CAN总线上可以挂载多个节点,每一个节点都会人为地分配一个或是多个特定的ID,而其他的ID对这个节点是无关的,因此需要忽略掉无关ID的报文。如果靠软件来做,则需要CPU的参与,增大CPU的负担。因此STM32设计了专门的寄存器,用来硬件筛选不同ID的报文,只有通过筛选的报文才会被存入接收FIFO中。(对于CAN1、CAN2一同使用的时候,两个CAN共用28个筛选器组。如果只使用了CAN1,则只有14个筛选器组能够使用。)

​ 一个筛选器组共有两个32位的寄存器 (CAN_FiRx) ,可将筛选码填入这两个寄存器中。而一个筛选组具有四种筛选模式,不同模式下的两个筛选寄存器中的内容会有不同的效果:

  1. 32位掩码模式
    此模式下筛选寄存器1 (CAN_FiR1) 用来存放ID,筛选寄存器2 (CAN_FiR2) 用来存放此ID的掩码。掩码为1的位一定要匹配ID所对应的位,掩码为0的位则ID此为为1还是0都可以。如果收到匹配成功的ID,则放入接收FIFO。

  2. 32位列表模式
    列表模式就相当于白名单,只有收到与白名单一模一样的ID时才能通过匹配。此模式下筛选寄存器1存储一条ID,筛选寄存器2存储另一条ID。也就是说此模式可以存放两条32位的白名单。

  3. 16位掩码模式
    此模式与前面的32位掩码模式类似,只不过筛选码从原来的32位变成了16位,对扩展ID筛选能力变弱了。但这种模式可以存放两条ID及两条掩码。

  4. 16位列表模式
    此模式可以存放4条16位的ID。

下面是32位与16位筛选码的格式:

筛选寄存器[32:24][23:16][15:8][7:0]
32位筛选码格式STID[10:3]STID[2:0]+EXID[17:13]EXID[12:5]EXID[4:0]+IDE+RTR+0
16位筛选码格式STID[10:3]STID[2:0]+RTR+IDE+EXID[17:15]STID[10:3]STID[2:0]+RTR+IDE+EXID[17:15]

具体的筛选码需要根据4种模式下的格式来确定,切记要仔细配置不要弄错。

注:STID即标准ID内容、EXID即扩展ID内容、IDE用以决定使用标准ID还是扩展ID(为0即标准ID)、RTR用以决定是数据帧还是遥控帧(为0即数据帧)。

相关配置寄存器说明:

  1. 一个筛选器组使用32位还是16位由筛选器尺度寄存器CAN_FS1R决定

  2. 使用掩码模式还是列表模式由筛选器模式寄存器CAN_FM1R决定

  3. 如果某条报文通过了某个筛选器组,那么这条报文存在FIFO0还是FIFO1由筛选器分配寄存器CAN_FFA1R决定

  4. 还有筛选器激活寄存器CAN_FA1R来使能或失能某个筛选器组

  5. 最后,筛选器主寄存器CAN_FMR中的FINIT位用来切换筛选器的工作模式(相当于总开关)。当进入初始化模式,所有的筛选器都不再工作,但只有在这种模式下,筛选器才可以被配置。一旦进入工作模式,之前配置的所有筛选器组都将生效,且不可配置。

相关结构体说明:

typedef struct
{
  uint32_t FilterIdHigh;          //CAN_FiR1寄存器的高16位
  uint32_t FilterIdLow;           //CAN_FiR1寄存器的低16位
  uint32_t FilterMaskIdHigh;      //CAN_FiR2寄存器的高16位
  uint32_t FilterMaskIdLow;       //CAN_FiR2寄存器的低16位
  uint32_t FilterFIFOAssignment;  //通过筛选器的报文存在FIFO0还是FIFO1中
  uint32_t FilterBank;            //此次配置用的是哪个筛选器。用单CAN的取值为0-13
  uint32_t FilterMode;            //掩码模式或列表模式
  uint32_t FilterScale;           //32位或16位
  uint32_t FilterActivation;      //使能或失能
  uint32_t SlaveStartFilterBank;  //CAN1和CAN2一起用的时候,为CAN2分配筛选器的个数
} CAN_FilterTypeDef;

成员:

  1. FilterIdHighCAN_FiR1寄存器的高16位,用于填写筛选码。具体的格式要根据16位、32位;掩码模式、列表模式来确定。
    取值: 0x0 ~ 0xFFFF

  2. FilterIdLowCAN_FiR1寄存器的低16位

  3. FilterMaskIdHighCAN_FiR2寄存器的高16位

  4. FilterMaskIdLowCAN_FiR2寄存器的低16位

  5. FilterFIFOAssignment :通过筛选器的报文存在FIFO0还是FIFO1中
    取值:CAN_FILTER_FIFO0CAN_FILTER_FIFO1

  6. FilterBank :本次配置的筛选器号
    取值:对于单CAN为 0 ~ 13;对于双CAN为 0 ~ 27

  7. FilterMode :筛选模式,掩码模式或列表模式。
    取值:CAN_FILTERMODE_IDMASKCAN_FILTERMODE_IDMASK

  8. FilterScale :筛选码大小,16位或32位。
    取值:CAN_FILTERSCALE_16BITCAN_FILTERSCALE_32BIT

  9. FilterActivation :使能或失能此筛选器。
    取值:CAN_FILTER_DISABLECAN_FILTER_ENABLE

  10. SlaveStartFilterBank :为从CAN(CAN2)分配的筛选器个数。如果只使用单个CAN,可忽略此成员
    取值:0 ~ 27

相关库函数说明:

有了前面的寄存器基础,现在看这个就不会很困难了。填好筛选器结构体,然后调用下面这个函数即可生效:

HAL_StatusTypeDef HAL_CAN_ConfigFilter(CAN_HandleTypeDef *hcan, CAN_FilterTypeDef *sFilterConfig);

可以多次配置筛选器结构体并多次调用这个函数,因为最多可以有28个筛选器组供我们配置。但如果想要接收CAN总线上的数据,那么最起码也要配置一个筛选器组,否则会收不到任何报文。

示例1:

CAN_FilterTypeDef can_Filter = {0};

can_Filter.FilterIdHigh = 0;
can_Filter.FilterIdLow = 0;
can_Filter.FilterMaskIdHigh = 0;
can_Filter.FilterMaskIdLow = 0;
can_Filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
can_Filter.FilterBank = 0;
can_Filter.FilterMode = CAN_FILTERMODE_IDMASK;
can_Filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_Filter.FilterActivation = CAN_FILTER_ENABLE;

HAL_CAN_ConfigFilter(&hcan1, &can_Filter);

效果:CAN总线上所有的报文都会被接收,并存入FIFO0中。

示例2:

CAN_FilterTypeDef can_Filter = {0};

can_Filter.FilterIdHigh = 0;
can_Filter.FilterIdLow = 0;
can_Filter.FilterMaskIdHigh = 0;
can_Filter.FilterMaskIdLow = 2;
can_Filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
can_Filter.FilterBank = 0;
can_Filter.FilterMode = CAN_FILTERMODE_IDLIST;
can_Filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_Filter.FilterActivation = CAN_FILTER_ENABLE;

HAL_CAN_ConfigFilter(&hcan1, &can_Filter);

效果:仅接收标准ID为0x0的数据帧和遥控帧,并存入FIFO0中。

这部分的内容会有点多,大家可以自己编写代码来尝试验证一下。

五、接收

​ CAN的接收通常是使用中断方式来实现(因为没有DMA,而查询法又难以保证实时性),因此首先要在CubeMX中打开接收的全局中断。

在这里插入图片描述

我们可以看到有两个中断,一个是FIFO0收到数据的RX0中断,另一个是FIFO1收到数据的RX1中断,这里只用到了FIFO0,所以只勾选这个。(这里也说一说自己的理解,由于一个FIFO只能保存3条报文,有了两个FIFO就能保存6条报文。我们可以通过筛选器把不同ID的报文装进不同的FIFO,比如我们可以让FIFO0来接收关键、重要的报文,用FIFO1来接收不那么重要的报文,并且这两个中断是独立的,我们甚至可以给它们配置不一样的中断优先级。)

​ 光打开全局中断还不够,我们还需要打开CAN的FIFO消息挂起中断请求(也就是CAN外设的中断使能位)。

HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);

​ 这样,当CAN收到了符合筛选器的报文时,就会触发这个中断,我们便可以在这个中断回调函数中接收并处理收到的报文。(由于FIFO0和FIFO1用到的中断函数是独立的,因此这里的回调函数也是不一样的,大家要看清楚是FIFO0的还是1的)

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &can_Rx, recvBuf);

	/*下面是你用来处理收到数据的代码,
	可以通过串口把内容发送出来,
	也可以用来控制某些外设*/
}

接收用到的结构体如下:

typedef struct
{
  uint32_t StdId;    
  uint32_t ExtId;    
  uint32_t IDE;      
  uint32_t RTR;      
  uint32_t DLC;      
  uint32_t Timestamp; 
  uint32_t FilterMatchIndex; 
} CAN_RxHeaderTypeDef;

和发送结构体非常类似,不过这个结构体并不需要我们来赋值,而是作为接收函数的输出参数。这里仅介绍发送结构体没有的成员:

  1. Timestamp :只有使能了时间触发模式才有用,记录时间戳
  2. FilterMatchIndex :这条报文被接收是通过哪个筛选器

接收用到的函数如下:

HAL_StatusTypeDef HAL_CAN_GetRxMessage(CAN_HandleTypeDef *hcan, uint32_t RxFifo, CAN_RxHeaderTypeDef *pHeader, uint8_t aData[]);

参数:

  1. *hcan :can的句柄,由CubeMX自动帮我们定义
  2. RxFifo :接收FIFO号。参数: CAN_RX_FIFO0CAN_RX_FIFO1
  3. pHeader :接收结构体,这里作为输出参数
  4. aData[] :接收数组,这里作为输出参数

示例:

#include <stdio.h>

CAN_RxHeaderTypeDef can_Rx;
uint8_t recvBuf[8];

uint8_t uartBuf[64];

int main(void)
{
  	HAL_Init();
 	SystemClock_Config();
  	MX_GPIO_Init();
  	MX_CAN_Init();
    MX_USART1_UART_Init()

    CAN_FilterTypeDef can_Filter = {0};
    
    can_Filter.FilterIdHigh = 0;
    can_Filter.FilterIdLow = 0;
    can_Filter.FilterMaskIdHigh = 0;
    can_Filter.FilterMaskIdLow = 0;
    can_Filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
    can_Filter.FilterBank = 0;
    can_Filter.FilterMode = CAN_FILTERMODE_IDMASK;
    can_Filter.FilterScale = CAN_FILTERSCALE_32BIT;
    can_Filter.FilterActivation = CAN_FILTER_ENABLE;
    HAL_CAN_ConfigFilter(&hcan1, &can_Filter);
    
    HAL_CAN_ActivateNotification(&hcan1, CAN_IT_RX_FIFO0_MSG_PENDING);
    
    HAL_CAN_Start(&hcan1);

  	while (1)
  	{

  	}
}

void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan)
{
    uint16_t len = 0;
    
    HAL_CAN_GetRxMessage(&hcan1, CAN_RX_FIFO0, &can_Rx, recvBuf);

    if(can_Rx.IDE == CAN_ID_STD)
    {
        len += sprintf((char *)&uartBuf[len], "标准ID:%#X; ", can_Rx.StdId);
    }
    else if(can_Rx.IDE == CAN_ID_EXT)
    {
        len += sprintf((char *)&uartBuf[len], "扩展ID:%#X; ", can_Rx.ExtId);
    }
    
    if(can_Rx.RTR == CAN_RTR_DATA)
    {
        len += sprintf((char *)&uartBuf[len], "数据帧; 数据为:");
        
        for(int i = 0; i < can_Rx.DLC; i ++)
        {
            len += sprintf((char *)&uartBuf[len], "%X ", recvBuf[i]);
        }
        
        len += sprintf((char *)&uartBuf[len], "\r\n");
        HAL_UART_Transmit(&huart1, uartBuf, len, 100);        
    }
    else if(can_Rx.RTR == CAN_RTR_REMOTE)
    {
        len += sprintf((char *)&uartBuf[len], "遥控帧\r\n");
        HAL_UART_Transmit(&huart1, uartBuf, len, 100);        
    }    
}

效果:接收CAN总线上所有数据,并将内容通过串口打印出来。

在这里插入图片描述

六、总结

​ 记得第一次打算学CAN大概还是去年的这会,但没想到拖到现在才搞明白CAN的基本操作,个人也分析了一下为什么这么久了才学会CAN的基本操作:

  1. 首先是学习CAN的意愿不强。因为我用到的绝大部分外接外设都是使用SPI、IIC协议通信的(比如OLED、LCD、flash、陀螺仪、NRF24L等),就连用UART的都很少,而用到CAN的外设即使是今天都还没有接触过。因此用不到自然学习意愿就不强,再加上CAN本身就具有一定的复杂度,看一会看不明白就算了不学了。

  2. 其次是某些卖开发板的教程实在是太烂。永远都是按部就班那一套,介绍协议就从协议手册复制一段下来,介绍外设就从STM32手册复制一段下来,然后走马观花地过一遍库函数就直接看代码了。永远都不会介绍某个参数是做什么的、为什么要这么配置,更别提细节部分和深层思考了。

​ STM32的CAN外设看着寄存器很多,实际上大都是重复的,常用的库函数也就三四个,所以开头才说使用CAN进行基本的数据收发是非常简单的。当然目前也仅仅是把CAN这个外设调通,并没有真正的应用,实际上应用层面的东西并不简单,等以后有机会了用到了再分享出来吧。

  • 72
    点赞
  • 316
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值