1. CAN协议
• CAN协议,全称为Controller Area Network(控制器局域网络)是一种广泛应用的串行 通信 协议。
1.1 CAN种类
• **低速CAN(**ISO11519)通信速率10~125Kbps,总线长度可达1000 米,抗干扰能力更强。
• **高速CAN(**ISO11898)通信速率125Kbps~1Mbps,总线长度≤40米,抗干扰能力比较弱(对比于低速CAN来说)。
• 当然,CAN的传输速率也和环境,配置,物理线等相关。
1.2 定义
• CAN协议是一种基于差分信号 ( 如果受到影响,那么将会是两根线都会受到影响,然后两条线相减去之后,实际上并没受到影响的,或者说影响没有单线的大,所以 CAN 的抗干扰能力强。 ) 的异步 ( 没有时钟信号 ) 串行通信协议 ,采用双绞线 作为传输介质,具有高性能、高可靠性和独特的设计特点。
1.3 特点
• 多主控制 :支持多主方式 ,即任何一个节点都可以在总线上发送数据,其他节点根据需要进行接收。当两个以上的节点同时开始发送数据时,会根据标识符( ID ) 决定优先级。 补充:IIC也有多主特点,但是对比CAN的多主来说是,IIC的抗干扰能力,冲突解决机制 不如CAN,而且CAN还有错误检测机制等。
• 系统柔软性 :与总线相连的单元没有类似于 " 地址 " 的信息 ,因此在总线上增加单元时,连接在总线上的其他单元的软硬件及应用层都不需要改变。
• 通信速度快、距离远:数据传输速率较高,标准速率为125kbps,扩展速率可达1Mbps,且通信距离远,最远可达1KM(速率低于5Kbps)。
• 错误检测与恢复:具有错误检测、错误通知和错误恢复功能,能够确保数据传输的可靠性。
如果发送单元发出的是错误信息,其他的接收单元会作为类似"裁判"那样,进行错误检测,一旦检测到错误的数据,发送错误帧来破环数据,保证其他数据的完整性。
• 故障封闭功能 :能够判断出错误的类型,并将引起故障的单元从总线上隔离出去。
2. CAN控制层介绍
2.1 CAN网络通常由CAN 控制器、 CAN 收发器和双绞线组成
• CAN控制器 :负责处理数据的收发和协议转换。
• CAN收发器 :负责将控制器的数字信号转换为差分信号 进行传输,同时也负责将总线上的差分信号转换为数字信号供控制器处理。(就是两个方向传输)。
2.2 终端电阻
• 在高速 CAN 总线 的两端 分别连接一个电阻(120欧),称为终端电阻。终端电阻的主要作用是匹配总线阻 抗 ,提高信号质量 ,减少回波反射 ( 可以理解为点电压被挡了一下,然后调转方向,流动 )。一般来说,终端电阻的阻值为120Ω。
2.3 总线拓展图:
2.3.1 高速CAN,如图:

2.3.2 低速CAN,没有终端电阻,但是总线也连着两个电阻(电阻值为2.2k欧),如图:

2.4 电平标准
• CAN使用差分信号 进行数据传输,根据CAN_H和CAN_L上的电位差来判断总线电平 (CAN_H - CAN_L)。
• 显性电平 表示逻辑 0,通常 CAN_H 和 CAN_L 有 2V 的压差。
• 隐性电平 表示逻辑 1,通常 CAN_H 和 CAN_L 有 0V 的压差。
• 显性电平在通信中具有优先权,能够覆盖隐性电平,确保数据的正确传输。
• 隐性电平则作为总线的空闲或监听状态存在,等待有节点发送数据。
• 而低速CAN的显性电平也是逻辑0,但是 CAN_H 和 CAN_L 有 3V 的压差
• 而低速CAN的隐性电平也是逻辑1,但是 CAN_H 和 CAN_L 有 -1.5V 的压差(没有终端电阻),如图:

2.5 CAN的收发器,如图:

• 电路图:

• 注意这里的tx 接 tx , rx 接 rx ,不用交叉接线, vcc 接 5v。
• 上图是一个高速CAN的收发器,其电路图里有120R终端电阻。
3. CAN协议层介绍
• CAN帧种类介绍:帧就是数据包
• 数据帧(Data Frame ) :数据帧是CAN总线上用于传输用户数据 的帧,包括必要的帧头、标识符、控制位、数据长度代码、数据域、CRC校验码和应答域等部分,是CAN通信中最基本和最重要的帧类型。
• 遥控帧(Remote Frame ) :遥控帧用于向总线上的其他节点请求发送 具有相同标识符 的数据帧,它没有数据域,仅通过标识符来指定所需的数据。遥控帧的帧结构与数据帧相似,但缺少数据部分。
• 错误帧(Error Frame ) :当CAN总线上的任何节点检测到通信错误时 ,会发送错误帧来通知其他节点。错误帧包含错误标志和错误界定符,用于指示错误的存在和类型。
• 过载帧(Overload Frame ) :过载帧用于在连续的数据帧或远程帧 之间提供额外的延时,以指示接收节点尚未准备好接收下一个帧。当接收节点因内部条件限制而无法立即接收数据时,会发送过载帧来请求发送节点暂停发送。
• 帧间隔( Interframe Space ) :帧间隔用于隔离数据帧与前面的帧 ,确保它们之间的时间间隔足够长,以避免总线上的冲突和数据丢失。帧间隔包括连续三个隐性位(间隔段)和可能存在的空闲段,用于将数据帧或远程帧与前面的帧分隔开来。
3.1 数据帧与遥控帧的区别
• 数据帧:用于传输数据,包含实际的数据字段,RTR位为显性(0)
• 遥控帧:用于请求数据,不包含数据字段,RTR位为隐性(1)
3.2 CAN数据帧介绍
• 数据帧由7段组成。数据帧又分为标准帧 (CAN2.0A) 和扩展帧 (CAN2.0B) ,主要体现在仲裁段和控制段。
• 如图,灰色是显性(0),白色是隐性(1),紫色是可以是1也可以是0。

- 帧起始
- 功能 :表示数据帧的开始。
- 特点 :由一个显性位构成,此时CAN_H为高电平(如3.5V),CAN_L为低电平(如1.5V),二者之间的电位差形成差分信号。
- 仲裁段
- 功能 :确定发送优先级,并包含标识符(Identifier)用于唯一标识发送者和接收者之间的通信关系。
- 组成:
- 标准数据帧的仲裁段由11位ID和1位RTR位(远程发送请求位)组成。RTR 位用于区分数据帧 ( 显性电平)和遥控帧(隐性电平)。
- 扩展数据帧中,还包含SRR 位( Substituted Remote Request ,替代的远程请求 )和IDE 位( Identifier Extension ,标识符扩展)。SRR位用于指示发送方是否发送了远程请求帧,IDE位用于指示标识符字段是否使用了扩展格式(29位)。
- SRR位的作用,替代RTR位置,用来区别标准帧还是扩展帧,还有就是仲裁过程优先级控制,我们知道标准格式的优先级>扩展格式 ,在仲裁过程中,如果两个帧的标识符前 11 位相同,标准帧的优先级高于扩展帧。这是因为标准帧的 RTR 位可以是显性(逻辑 0),而扩展帧的 SRR 位始终为隐性(逻辑 1)。而显性位(逻辑 0)在仲裁中具有更高的优先级。
- IDE位的作用:区分标准格式(显性)还是扩展格式(隐性)
- 控制段
- 功能:包含数据长度代码(DLC),用于定义数据帧中数据域的长度(有多少个字节)
- 特点:DLC占4位,其取值范围为0到8个字节,表示数据帧中包含的数据字节数。
- R0和r1都是保留位
- 数据段
- 功能:包含要传输的数据,是数据帧的主体部分。
- 特点 :数据域的长度可以根据DLC 字段的值从0到8个字节不等,数据从最高位(MSB)开始传输。
- CRC段
- 功能:用于检测数据帧的传输错误。
- 特点:CRC(循环冗余校验)是一种通过对数据进行计算生成的校验码,发送方在发送数据帧时会根据数据计算出CRC值,并将其添加到数据帧的CRC段中。接收方在接收到数据帧后会重新计算CRC值,并与接收到的CRC值进行比较,以确认数据在传输过程中是否发生错误。
- 应答段
- 功能:用来确认数据帧的正常接收。
- 组成:由ACK槽(ACK Slot)和ACK界定符两个位构成。当接收节点成功解析了数据帧并确认无误后,会在ACK槽中发送一个显性位作为应答信号。
- 在ACK槽前后两个都有界定符 ,其目的是给从机有足够的时间来处理数据,并返回信号。
- 帧结束
- 功能:表示数据帧的结束。
- 特点 :主机发送结束帧,由7个连续的隐性位构成,标志着数据帧的传输完成。
3.3 CAN位时序:固定时间去采样总线的电平是怎样的,确保数据在总线上准确传输。
• CAN总线以**"** 位同步 " 机制,实现对电平的正确采样 。位数据都由四段组成:同步段(SS)、传播时间段(PTS)、相位缓冲段1(PBS1)和相位缓冲段2(PBS2),每段又由多个位时序Tq组成。同时Tq的值也是可以配置的。
• 如图,采样点是在PBS1末尾。其中SS是确定只能占有1Tq,其他三个段的Tq是可以设置的。

- 所谓采样点是读取总线电平 ,并将读到的电平作为位值的点。位置在 PBS1 结束处。
- CAN总线通过时钟同步机制 来确保各个节点在通信过程中保持同步 。时钟同步机制包括硬同步和再同步两种。
- 硬同步:
- 硬同步只在帧的起始位(SOF)处进行。
- 当接收节点检测到帧起始位的下降沿时,会将其与自身的位时间进行对齐 ,从而实现同步。
-
如图,如果SS不在下降沿,那么会硬同步一次,让SS在下降沿处,然后每隔一段时间检查。
-
再同步
- 再同步在帧的后续数据位中进行
- 如果接收节点检测到数据位的跳变沿不在自身的同步段内,则会通过延长或缩短相位缓冲段的时间来调整自身的位时间,以重新获得同步。
- 再同步时,PBS1和PBS2中增加或者减少的时间被称为"再同步补偿宽度(SJW)",其范围:1~4Tq。
- 如图
- 如图
-
3.4 CAN的仲裁机制
- CAN总线处于空闲状态,最先开始发送消息的单元获得发送权。(先到先得)。
- 多个单元同时开始发送时,从仲裁段 ( 报文 ID) 的第一位开始进行仲裁。仲裁原理如下:
- 标识符优先级:CAN总线中传输的数据帧的起始部分为数据的标识符(ID) 。这个标识符不仅用于区分消息,还 表示消息的优先级 。在CAN 2.0标准中,标识符可以是11位或29位(对于扩展帧)。标识符的数值越小,优先级越高
- 逐位仲裁
- 当两个或两个以上的节点同时开始传送报文时,就会产生总线访问冲突。此时,各节点会按照标识符的位顺序逐位进行仲裁。
- 在仲裁过程中,每个节点都会将自己发送的电平与总线上的电平进行比较。如果电平相同,则节点继续发送下一位;如果电平不同,则优先级低的节点停止发送,而优先级高的节点继续发送。
- 这种仲裁方式是非破坏性的,即优先级低的节点在仲裁过程中不会破坏总线上已经存在的数据。
- 显性电平优先:显性电平(逻辑0)的优先级高于隐性电平(逻辑1)。
4. STM32的CAN控制器介绍
4.1 CAN控制器介绍
- STM32的bxCAN,即基本扩展CAN(Basic Extend CAN),是STM32微控制器系列中集成的CAN 控制器模块。
- 协议支持:支持CAN协议2.0A(只认识标准模式)和2.0B(两个都认识)的主动模式。
- 高波特率 :波特率最高可达1 兆位 / 秒。
- 时间触发通信 :支持时间触发通信功能,CAN的硬件内部定时器可以在TX/RX的帧起始位的采样点位置生成时间戳。
- 发送功能
- 具有3 个发送邮箱 ,发送报文的优先级特性可软件配置
- 记录发送SOF(Start Of Frame,帧起始)时刻的时间
- 接收功能:具有3级深度的2个FIFO(First In First Out,先进先出队列),每个FIFO都可以存放3个完整的报文,完全由硬件管理。
- 共有14个位宽可变的过滤器组(设置过滤规则,接收想要的数据)(部分STM32型号可能支持更多),由整个 CAN 共享,用于筛选有效报文。
- 记录接收SOF时刻的时间。
- 支持禁止自动重传模式。
4.2 CAN控制器模式
- CAN控制器的工作模式有三种:初始化模式、正常模式和睡眠模式。
- 睡眠模式 :在睡眠模式下,CAN 控制器的时钟停止,以降低功耗。但软件仍然可以访问邮箱寄存器。
- 初始化模式 :在初始化模式下,禁止报文的接收和发送 ,并且CANTX引脚输出隐性位(高电平)。此时,可以对CAN控制器的相关寄存器进行配置,如位时间特性(CAN_BTR)和控制(CAN_MCR)等。
- 正常模式:作为总线的正常节点,可以向总线发送或接收数据。
- 如图
- CAN控制器的测试模式 有三种:静默模式、环回模式和环回静默模式,主要用于特定的测试或调试目的,以确保CAN控制器的功能正常。
- 静默模式
- 特点:
- 在静默模式下,CAN控制器可以正常地接收数据帧和远程帧 ,但只能发出隐性位,而不能真正发送报文。发送隐性位不会影响总线上的数据。
- 这意味着,虽然CAN控制器在尝试发送数据,但实际上它并没有在CAN总线上产生任何显性位,因此不会对总线上的其他节点产生影响。
- 应用场景
- 静默模式通常用于分析 CAN 总线的活动,而不会对总线上的其他通信造成干扰
- 开发人员可以使用此模式来观察总线上的数据流,而无需担心他们的测试设备会发送出不必要的报文(隐性位)。
- 特点:
- 环回模式
- 特点:
- 在环回模式下,CAN控制器会把发送的报文当作接收的报文并保存(如果可以通过接收过滤)。
- 这意味着,当CAN控制器发送一个报文时,它会立即在自己的接收缓冲区中看到这个报文,就像它是从总线上接收到的一样。
- 并且不能从总线上收到数据
- 应用场景:
- 环回模式通常用于自测试 ,以验证CAN 控制器的发送和接收功能是否正常。
- 通过发送一个报文并检查它是否被正确接收,开发人员可以确保CAN控制器的硬件和固件都按预期工作。
- 特点:
- 环回静默模式:
-
特点:
- 环回静默模式结合了静默模式和环回模式的特点
- 在该模式下,CANRX 引脚与 CAN 总线断开 ,同时CANTX 引脚被驱动到隐性位状态
- 这意味着,虽然CAN控制器在尝试发送报文,但它实际上并没有在CAN总线上产生任何显性位,并且它会将发送的报文视为接收到的报文。
-
应用模式:
- 环回静默模式通常用于**"** 热自测试 " ,即可以在不影响CANTX 和 CANRX所连接的整个CAN系统的情况下进行测试。
- 这种模式允许开发人员在不干扰总线上的其他通信的情况下,验证CAN控制器的发送和接收功能。
- 例如:假如接收器一开始是能正常工作,但是后面不能了,但又不能影响到总线上的其他设备,可以用这种模式。
-
- 如图
4.3 CAN控制器框图
- CAN控制内核:包含各种控制/状态/配置寄存器,用于配置CAN 控制器的模式、波特率等参数。
- 发送邮箱:用来缓存待发送的 CAN 报文 。STM32等微控制器通常具有多个发送邮箱 (如3个),以支持同时缓存多个报文。
- 接收FIFO**:** 缓存接收到的有效 CAN 报文。CAN控制器通常具有多个接收FIFO(如2个),以提高接收效率。
- 接收过滤器:筛选接收到的 CAN 报文,只将符合特定条件的报文保存到接收FIFO中。这有助于减少CPU的处理负担,提高系统的响应速度。
- 如图,发送调度:可根据邮箱优先级,报文优先级,报文顺序来设置。
4.4 发送的处理过程
-

- 大致过程:选择一个空的邮箱,把报文放进去,然后邮箱挂号,等待最高优先级,预定发送,等待总线的空闲。
- 接收处理过程:

- 大致过程:选择一个空的位置,放进有效报文,如果满了就不再放进,期间报文是可以取出来的。
- 有效报文:有效报文指的是(数据帧直到 EOF 段的最后一位都没有错误 ),且通过过滤器组对标识符过滤。
- 接收过滤器:
- 当总线上报文数据量很大时,总线上的设备会频繁获取报文,占用CPU。过滤器的存在,选择性接收有效报文,减轻系统负担。
- STM32的CAN控制器支持配置过滤器组,每个过滤器组包含2 个 32 位的寄存器 CAN_FxR1 和 CAN_FxR2 ,用于存储要筛选的 ID 或掩码 。对于STM32F103C8T6,如果只有一个CAN控制器,则可以配置14 个过滤器组,对应的编号为 0 到 13。
- 过滤器可以配置为不同的位宽,以适应不同长度的CAN ID。常见的位宽包括16位(用于标准帧)和32位 (用于扩展帧)。
- 选择模式可设置屏蔽位模式 或标识符列表模式 ,寄存器内容的功能就有所区别。屏蔽位模式,可以选择出一组符合条件的报文。寄存器内容功能相当于是否符合条件 。标识符列表模式,可以选择出几个特定ID的报文。寄存器内容功能就是标识符本身。
- 如图
4.5 CAN的位时序:只有三段时间。

- 这就是相当于:

- 例如:设TS1=8、TS2=7、BRP=3,波特率 = 36000 / [( 9 + 8 + 1 ) * 4] = 500Kbps。
- 注意:通信双方波特率需要一致才能通信成功。
- 注意:是APB时钟的时钟周期...36M的倒数。
5. CAN基本驱动
- CAN参数初始化(工作模式,波特率,功能设置)
- msp初始化(时钟,引脚,中断等)
- 过滤器设置,
- 发送,接收函数编写
- 中断服务函数编写(可选)
- 在can.c
cpp
#include "can.h"
#include "stdio.h"
CAN_HandleTypeDef can_handle = {0};
void can_init(){
can_handle.Instance = CAN1;
can_handle.Init.Prescaler = 4;
can_handle.Init.Mode = CAN_MODE_LOOPBACK;
can_handle.Init.SyncJumpWidth = CAN_SJW_1TQ;//同步段
can_handle.Init.TimeSeg1 = CAN_BS1_9TQ;//相位缓冲段1
can_handle.Init.TimeSeg2 = CAN_BS2_8TQ;//相位缓冲段2
can_handle.Init.AutoBusOff = DISABLE; /* 禁止自动离线管理 */
can_handle.Init.AutoRetransmission = DISABLE; /* 禁止自动重发 */
can_handle.Init.AutoWakeUp = DISABLE; /* 禁止自动唤醒 */
can_handle.Init.ReceiveFifoLocked = DISABLE; /* 禁止接收FIFO锁定 */
can_handle.Init.TimeTriggeredMode = DISABLE; /* 禁止时间触发通信模式 */
can_handle.Init.TransmitFifoPriority = DISABLE; /* 禁止发送FIFO优先级 */
HAL_CAN_Init(&can_handle);
filter_config();
HAL_CAN_Start(&can_handle);
}
void filter_config(){
CAN_FilterTypeDef can_filter = {0};
can_filter.FilterActivation = CAN_FILTER_ENABLE;
can_filter.FilterBank = 0;
can_filter.FilterFIFOAssignment = CAN_FILTER_FIFO0;
can_filter.FilterIdHigh = 0;
can_filter.FilterIdLow = 0;
can_filter.FilterMaskIdHigh = 0;
can_filter.FilterMaskIdLow = 0;
//上述的标识符和掩码的配置,说明can所有数据都能接收
can_filter.FilterMode = CAN_FILTERMODE_IDMASK;
can_filter.FilterScale = CAN_FILTERSCALE_32BIT;
can_filter.SlaveStartFilterBank = 14;
/*将 SlaveStartFilterBank 设置为 14,意味着从 CAN 实例将使用从 14 开始的过滤器组。
这样可以避免主从 CAN 实例之间的过滤器组冲突,确保每个实例都能正确地接收和处理自己的消息
主 CAN 实例(CAN1):使用过滤器组 0 到 13。
从 CAN 实例(CAN2):使用过滤器组 14 到 27
*/
HAL_CAN_ConfigFilter(&can_handle,&can_filter);//配置过滤器
}
void HAL_CAN_MspInit(CAN_HandleTypeDef *hcan){//这里的tx rx不用交叉接线
if(hcan->Instance == CAN1){
__HAL_RCC_CAN1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
GPIO_InitTypeDef gpio_init = {0};
gpio_init.Mode = GPIO_MODE_AF_PP;
gpio_init.Pin = GPIO_PIN_12;//推挽输出
gpio_init.Pull = GPIO_PULLUP;//默认上拉
gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
gpio_init.Mode = GPIO_MODE_INPUT;
gpio_init.Pin = GPIO_PIN_11;//推挽输出
gpio_init.Pull = GPIO_PULLUP;//默认上拉
gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
}
}
void can_send_data(uint32_t id,uint8_t* data,uint8_t len){
uint32_t mail = CAN_TX_MAILBOX0;
CAN_TxHeaderTypeDef tx_header = {0};
tx_header.ExtId = id;
tx_header.DLC = len;
tx_header.IDE = CAN_ID_EXT;
tx_header.RTR = CAN_RTR_DATA;
//TransmitGlobalTime 时间戳
HAL_CAN_AddTxMessage(&can_handle,&tx_header,data,&mail);
while(HAL_CAN_GetTxMailboxesFreeLevel(&can_handle) != 3);//等着发完(所有邮箱)
printf("发送数据:");
uint8_t i = 0;
for(i = 0;i < len;i++)
printf("%X ",data[i]);
printf("\r\n");
}
uint8_t can_recv_data(uint8_t* data){
CAN_RxHeaderTypeDef rx_header = {0};
if(HAL_CAN_GetRxFifoFillLevel(&can_handle,CAN_RX_FIFO0) == 0)//判断有没有数据
return 0;
HAL_CAN_GetRxMessage(&can_handle,CAN_RX_FIFO0,&rx_header,data);
printf("接收数据:");
uint8_t i = 0;
for(i = 0;i < rx_header.DLC;i++)
printf("%X ",data[i]);
printf("\r\n");
return rx_header.DLC;
}
- 在key.c
cpp
#include "key.h"
#include "delay.h"
void key_init(){
GPIO_InitTypeDef gpio_init = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();//使能GPIO时钟
gpio_init.Mode = GPIO_MODE_INPUT;
gpio_init.Pin = GPIO_PIN_0 | GPIO_PIN_1;//推挽输出
gpio_init.Pull = GPIO_PULLUP;//默认上拉
gpio_init.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA,&gpio_init);//初始化gpio
}
uint8_t key_scan(){
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET){//判断按键是否被按下
//延时一段时间,在判断按键是否被按下,达到软件消抖作用
delay_ms(10);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET){//在判断按键是否被按下
while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0) == GPIO_PIN_RESET);//如果被按下,等待松开按键
return 1;//返回按键值
}
}
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == GPIO_PIN_RESET){//判断按键是否被按下
//延时一段时间,在判断按键是否被按下,达到软件消抖作用
delay_ms(10);
if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == GPIO_PIN_RESET){//在判断按键是否被按下
while(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_1) == GPIO_PIN_RESET);//如果被按下,等待松开按键
return 2;//返回按键值
}
}
return 0;
}
- 在mian.c
cpp
#include "sys.h"
#include "uart1.h"
#include "delay.h"
#include "led.h"
#include "can.h"
#include "key.h"
uint8_t send_data[8] = {0x11,0x22,0x33,0x44,0x55,0x66,0x77,0x88};
uint8_t recv_data[8];
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
led_init(); /* LED初始化 */
uart1_init(115200);
key_init();
can_init();
printf("hello world\r\n");
uint8_t i = 0;
while(1)//流水灯实验
{
if(key_scan() == 1){
can_send_data(0x12345678,send_data,8);
for(i = 0;i < 8;i++)
send_data[i]++;
}
can_recv_data(recv_data);
delay_ms(100);
}
}





