CAN概述:
CAN:控制器局域网(Controller Area Network)属于现场总线的范畴,是一种有效支持分布式控制系统的串行通信网络。
MCU其他通信的对比:
UART:异步通信,无需时钟线。仅用TX和RX两根线即可实现全双工通信,点对点通信。
常用于:mcu与pc的调试,gps模块,蓝牙模块等。
注意:虽然UART只有一个DR,但是DR中实际上有两个寄存器,并且拥有两条完全独立的硬件通道,互不影响。
发送流程:CPU → TDR → 发送移位寄存器 → TX引脚
接收流程:RX引脚 → 接收移位寄存器 → RDR → CPU
IIC:同步通信,仅需SDA(数据线)和SCL(时钟线)两根线,支持多主多从,通过地址寻 址区分设备。
常用于:低速的传感器(温湿度)、EEPROM存储器、RTC实时时钟。
SPI:同步、全双工、主从,模式。使用MOSI、MISO、SCLK、CS四根线,传输速率非常高
常用于:高速数据传输,驱动lcd屏幕,读写Flash存储器,高速ADC/DAC通信。
CAN:同步、串行、多主通信。使用CAN_H和CAN_L两根差分信号线,具有极强的抗干扰能力,支持长距离传输。
常用于:汽车电子控制单元ECU。
RS485:异步串行通信,采用差分信号传输(A线和B线),可实现半工或全双工
常用于:自动化,工业现场
CAN通信标准:
ISO-11898:通信速率为125kbps~1Mbps的高速CAN通信标准,闭环总线,线长<=40米

ISO-11519:通信速率为10kbps~125kbps的低速CAN通信标准,开环总线,线长可以1000米


CAN特点:
1、多主控制与优先级仲裁
总线采用多主架构,无中心主机。当多个节点同时发送信息的时候,通过非破坏性仲裁机制,依据报文标识符逐位比较,ID越小优先级越高,确保高优先级信息无损传输。
2、系统柔软性与高拓展性
节点无物理地址的概念,仅通过ID标识符的内容。
3、灵活的速率与距离平衡
通信距离与速率呈反比关系,短距离<40米速率可达1Mbps;长距离(10km)速率将至5kbps。
4、完备的错误处理机制
具备错误检测、通知与恢复功能。任一节点均可检测错误并广播通知全网;发送节点检测到错误后立即停止发送并自动重发,直至传输成功。
5、故障封闭与系统鲁棒性
能区分暂时性干扰与永久性故障。对于持续错误的故障节点,系统可自动将其隔离(总线关闭),防止个别节点故障导致整个网络瘫痪。
6、多节点连接能力
理论上支持无限节点连接,实际数量受电气负载和信号延迟限制。通过降低通信速率,可显著增加挂载节点的数量。
CAN物理层定义
1、CAN通信中如何定义逻辑1跟逻辑0、逻辑1和逻辑0都是信号特征
显性电平:CAN_H3.5V 和 CAN_L1.5V 的电压差是一个 2V 为逻辑0
隐形电平:CAN_H2.5V 和 CAN_L2.5V 的电压差是一个 0V 为逻辑1
2、总线上有显性电平和隐性电平两种电平
隐性电平(逻辑1) = 保持沉默
弱驱动/高阻态" :节点输出 1 时,内部电路处于高阻态(相当于断路),不输出电流,依靠终端电阻让电压恢复到默认的 0V 差值。这就像一个人松开了手,让门靠弹簧自动关上。
显性电平(逻辑0) = 主动出击
节点输出 0 时,内部电路会主动拉高 CAN_H 电压、拉低 CAN_L 电压,强行在总线上制造 2V 的电压差。这就像一个人用力把门推开。

CAN帧种类
1、CAN通信是以下5种类型的帧进行的:

其中,数据帧和遥控帧有标准格式和拓展格式
标志格式有11个位的标识符(ID),拓展格式有29个位的ID
数据帧的组成

图中D表示显性电平0,R表示隐性电平1
①帧起始:表示数据帧开始的段
②仲裁段:表示该帧优先级的段
③控制段:表示数据的字节数及保留位的段
④数据段:数据的内容,一帧可发送0~8个字节的数据
⑤CRC段:检测帧的传输错误段
⑥ACK段:表示确认正常接收的段
⑦帧结束:表示数据帧结束的段
数据帧的解析:
1、帧起始:标准帧和拓展帧都是由1个位的显性电平表示帧起始
2、仲裁帧:表示数据优先级的段,标志帧和拓展帧格式在本段有所区别:如图所示:

ID:高位在前,低位在后
基本ID:禁止高7位都为隐性,既不能::ID=1111111XXXX。(ID值必须小于2032)
RTR:远程请求位。0 数据帧,1 远程帧(遥控帧)
SRR:替代远程请求位,设置为1(隐性电平)
IDE,标识符选择位,0 标准标识符,1 拓展标识符

ID的作用:
1)总线上的节点通过过滤匹配ID,决定是否接收该报文
2)仲裁:传统CAN总线(非TTCM模式)通过ID二进制大小仲裁总线占用权 --ID越小,优先级越高 。例如:0x01 > 0x10
3)分类:不同业务的报文用不同ID区分(如温度报文 = 0x12,火焰报警 = 0x24)通过不同的ID来区分不同的数据
4)标准ID的最大值为 0x7FF (二进制 11位 全1),若写0x800(超出11位),控制器会自动截断高位,实际发送的是0x000;拓展ID最大值为0x1FFFFFFF,超出部分会被阶段,导致ID不符合预期
5)标准ID(CAN 2.0A),最常用,满足绝大部分场景(如工业控制,单片机组网)
拓展ID (CAN 2.0B) ,仅用于更多ID的复杂场景(如汽车电子)
6)使用标准ID时与拓展ID时,它们在同一个时刻只有一个能有效,但在整个网络周期中是共存的
3、控制段

r0,r1:保留位。必须以显性电平发送,但是可以接收隐性电平。
DLC:数据的长度码。0~8,表示发送 / 接收的数据长度(字节)。
IDE:标识符选择位。0 标准标识符;1 拓展标识符
4、数据段
该段包含0~8个字节数据,从高位MSB先出。标准帧和拓展帧在这个段的格式完全一样。
5、CRC段
CRC段:该段用于检查帧传输错误。由15个位的CRC顺序和1个位的CRC界定符(用于分隔的位)组成,标准帧和拓展帧在这个段的格式完全一样。
CRC的值计算范围包括:帧起始、仲裁段、控制段、数据段
接收方以同样的算法计算 CRC 值并进行比较,不一致会通报错误
6、ACK段

ACK:此段用来确认是否正常接收。由ACK槽(ACK Slot)和 ACK 界定符2个位组成。
标准帧和拓展帧在这个段的格式完全一样。
发送单元ACK段:发送2个隐性位。
接收单元ACK段:接受到正确信息的单元,在ACK槽发送显性位,通知发送单元,正常接收结束。称之为发送ACK/返回ACK
注意:发送ACK的是 既不处于总线关闭态 也不处于休眠太的所有接收单元中,接受到正常消息的单元(发送单元不发送ACK)。正常消息是指不含填充错误、格式错误、CRC错误的消息。
7、帧结束
帧结束:由7个位的隐性位组成。标准帧和拓展帧在这个段格式完全一样。
CAN总线仲裁
在总线空闲态,最开始发送信息的单元获得发送权。
多个单元同时发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送。
1、同时多个单元发送数据时:总线仲裁(优先级)过程:

规律:
1、总线空闲时,最先发送的单元获得发送优先权,一旦发送,其他单元无法抢占
2、如果有多个单元同时发送,则连续输出显性电平多的单元,具有较高优先级从ID开始比较
如果ID相同,还可能会比较RTR和SRR等位
CAN位时序
1、位速率
位速率:由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位一般可以分为如下四段:
①同步段:(SS)
②传播时间段(PTS)
③相位缓冲段1(PBS1)
④相位缓冲段2(PBS2)
这些段又可以称为 Time Quantum(以下称为Tq)的最小时间单位构成
1位分为4个段,每个段又由若干个Tq构成,这称为 位时序。
位时间 = 1 / 波特率 ,因此,知道位时间没我们就可以知道波特率。
1位由多少个Tq构成、每个段又由多少个Tq构成等,可以任意设定时序,通过设定位时序,多个单元可同时采样,也可以任意设定采样点。
CAN总线波特率计算公式:
波特率 = 1 / 位周期
1、计算时间量子(TQ)
TQ = 1 / (CAN控制器时钟频率 / 分频系数(BRP))
2、计算位周期(Tbit)
位周期 = (SYNC_SEG + PROP_SEG + PHASE_SEG1 + PHASE_SEG2) * TQ = 总TQ数 * TQ
3、换算公式
brr = 1 / 总TQ数 / TQ
brr = 1 / 总TQ数 * (CAN控制器时钟频率 / 分频系数(BRP))
= CAN 控制器时钟频率 / 总TQ数 / 分频系数
4、计算

假设 CAN时钟频率为:42Mhz 想要 500k的波特率
通过以上公式可以得出(BRP+1)* (1 + TS1 + TS2) = Fcan / baud = 84
拆分84: 6 * 14 = 84
分频系数 = 6 自己算影响TS1和TS2
同步段 = 1 固定为1
TS1 = 7
TS2 = 6
位时序各段的作用Tq数如下表

在寄存器或者库函数代码配置中 同步段为固定一个Tq 没有给到配置位 传播时间段和相位缓冲段1合并成一个寄存器既相位缓冲段1(TS1位)
一个位的构成

图中采样时间加大或减小量的最大值就是SJW
无论误差有多大,我每次动手调整的时间长度,绝对不会超过这把尺子的长度
*SJW 就是 CAN 控制器的**"容错缓冲器"**。它决定了当总线上的信号出现微小抖动或时钟偏差时,控制器能够自动修正的最大幅度。

在STM32中:
1Mbps速率下,采用点的位置在6tq位置处,BS1=5,BS2=2
500kbps速率下,采用点的位置在8tq位置处,BS1=7,BS2=3
250kbps速率下,采用点的位置在14tq位置处,BS1=13,BS2=2
125k,100k,50k,20k,10k的采用点位置与250K相同。
bxCAN特点
①支持CAN协议2.0A和2.0B主动模式
②波特率最高达1Mbps
③支持时间触发通信
④具有3个发送邮箱
⑤具有3级深度的2个接收FIFO
⑥可变的筛选器组(也称过滤器组,最多28个)
STM32 bxCAN模式
1、工作模式
1)初始化模式(INRQ=1,SLEEP=0)
2)正常模式(INRQ=0,SLEEP=0)
3)睡眠模式(SLEEP=1)
2、测试模式
1)静默模式(LBKM=0,SILM=1)
2)环回模式(LBKM=1,SILM=0)
3)环回静默模式(LBKM=1,SILM=1)
3、调试模式

STM32 bxCAN框图
参考手册:两个CAN分别拥有直接的发送邮箱和接收FIFO,但是他们共用28个筛选器。
bxCAN标识符筛选器
CAN总线过滤器(Filter)是CAN控制器(如STM32的bxCAN)的核心功能模块,核心作用是筛选总线是=上的CAN报文,只让符合条件的报文进入FIFO / 中断 ,过滤无关报文 -- 既减少 CPU的中断响应和数据处理负担,也避免接收FIFO溢出导致的报文丢失,是CAN总线多节点通信中必不可少的配置。
1、CAN的标识符不表示母的地址,接收节点根据标识符的值,来决定是否接收对应消息。
2、STM32 CAN控制器,提供了28个可配置的筛选器组(F1仅互联型才有28个,其他的只有14个),可降低CPU处理CAN通信的开销。
3、STM32 CAN控制器每个筛选器组由2个32位寄存器组成(CAN_FxR1和CAN_FxR2,x=0~27)。根据位宽不同,每个筛选器组可提供:
1)1个32位筛选器,包括:STDID[10:0]、EXTID[17:0]、IDE和RTR位
2)2个16位筛选器,包括:STDID[10:0]、IDE、RTR和EXTID[17:15]位
4、筛选器可配置为:屏蔽位模式和标识符列表模式。在屏蔽位模式下,标识符寄存器和屏蔽寄存器一起。指定报文标识符的任何一位,应该按照"必须匹配"或"不用关心"处理。而在标识符列表模式下,屏蔽寄存器也被当作标识符寄存器用。因此,不是采用一个标识符加一个屏蔽位的方式,而是使用2个标识符寄存器。接收报文标识符的每一位都必须跟筛选器标识符相同。
标识符列表模式很好理解:就是指过滤器寄存器的所有位都用来过滤,也就是说,发送端发送的标识符所有的位必须和接收端的过滤器的寄存器定义的一模一样,有一个为不同,都拒收次消息!
5、通过CAN_FM1R 和 CAN_FS1R 可配置筛选器的位宽和模式:
通过设置CAN_FM1R的FBMx位,可以配置过滤器组为标识符列表模式或屏蔽位模式。
FSCx =0 à 过滤器位宽为2个16位 ;
FSCx =1 à 过滤器位宽为单个32位 。
FBMx=0à 过滤器组x的2个32位寄存器工作在标识符屏蔽位模式;
FBMx=1à 过滤器组x的2个32位寄存器工作在标识符列表模式。
● 1个32位过滤器,包括: STDID[10:0]、 EXTID[17:0]、 IDE和RTR位
● 2个16位过滤器,包括: STDID[10:0]、 IDE、 RTR和EXTID[17:15]位
过滤器优先级规则:
位宽为32位的过滤器,优先级高于位宽为16位的过滤器;
对于位宽相同的过滤器,标识符列表模式的优先级高于屏蔽位模式;
位宽和模式都相同的过滤器,优先级由过滤器号决定,过滤器号小的优先级高。
过滤掩码(Mask):决定 ID 的哪些位需要严格匹配(掩码位 = 1 时,对应 ID 位必须一致;掩码位 = 0 时,对应 ID 位无需匹配);
过滤 ID:目标报文的基准 ID;过滤器 ID 与掩码配合,在硬件层判断帧 ID 是否符合规则:仅匹配的帧存入接收 FIFO,其余直接丢弃,减轻 CPU 负载并提升实时性。

数据帧 ID:发送侧的 "身份证",决定总线优先级与消息类型,全网可见。
过滤器 ID:接收侧的 "门禁卡",与掩码配合实现硬件筛选,仅本地生效。
二者协同工作:发送侧用帧 ID 竞争总线,接收侧用过滤器 ID 筛选所需消息,提升系统效率与实时性。
bxCAN 发送流程

CAN 发送流程为:
- 程序选择1 个空置的邮箱(TME=1 )
- 设置标识符(ID ),数据长度和发送数据
- 设置CAN_TIxR 的TXRQ 位为1 ,请求发送
- 邮箱挂号(等待成为最高优先级)
- 预定发送(等待总线空闲)
- 发送
- 邮箱空置。
bxCAN 接收流程

CAN 接收流程为:
- FIFO 空
- 收到有效报文
- 挂号_1 (存入FIFO 的一个邮箱,这个由硬件控制,我们不需要理会)
- 收到有效报文
- 挂号_2
- 收到有效报文
- 挂号_3
- 收到有效报文
- 溢出。
CAN 收到的有效报文,存储在3 级邮箱深度的FIFO 中。FIFO 接收到的报文数,我们可以通过查询CAN_RFxR 的FMP 寄存器来得到,只要FMP 不为0 ,我们就可以从FIFO 读出收到的报文。
报文FIFO 具有锁定功能( 由CAN_MCR ,RFLM 位控制) ,锁定后,新数据将丢弃,不锁定则新数据将替代老数据。
bxCAN代码流程
初始化代码流程:
1)开启时钟
①使能 GPIOA 的时钟,因为 CAN 的 TX/RX 引脚(PA11/PA12)在 A 端口上。
②使能 CAN1 外设的时钟,这是操作 CAN 控制器的前提。
2)配置 GPIO 引脚
①将 PA11 (RX) 和 PA12 (TX) 配置为复用功能模式 (GPIO_Mode_AF)。
②使用 GPIO_PinAFConfig 函数,将这两个引脚明确地复用到 CAN1 外设上。
3)配置 CAN 核心参数
① 填充 CAN_InitTypeDef 结构体,设置 CAN 的工作模式、波特率(通过 BS1, BS2, Prescaler)、自动离线管理 (ABOM) 等。
② 调用 CAN_Init 函数,将上述配置应用到 CAN1 硬件上。
4)配置CAN筛选器
①填充 CAN_FilterInitTypeDef 结构体。这是 bxCAN 模块的精华部分。
②代码中配置了筛选器组 0 ,采用屏蔽位模式 (CAN_FilterMode_IdMask) 和 32 位位宽。
③关键点 :ID 和掩码都被设置为 0x0000。这意味着接收所有 CAN 报文,不做任何过滤。
④调用 CAN_FilterInit 函数,激活筛选器配置。
具体代码实现:
cs
/*************************
函数名称:Can_Config(void)
函数功能:Can总线配置
返回值:无
形参:无
作者:me
版本:1.0
PA11 -- CAN1RX PA12 -- CAN1TX
*************************/
void Can_Config(u8 BS1,u8 BS2,u8 Mode,u16 psc)
{
//打开时钟
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE);//打开GPIOA
RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);//打开CAN1
GPIO_InitTypeDef GPIO_InitStruct = {0};
CAN_FilterInitTypeDef CAN_FilterInitStruct = {0};
//初始化IO口 复用推挽模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_12;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF;
GPIO_InitStruct.GPIO_Speed = GPIO_High_Speed;
GPIO_Init(GPIOA, &GPIO_InitStruct);
GPIO_PinAFConfig(GPIOA,GPIO_PinSource11,GPIO_AF_CAN1);//PA11复用到CAN1
GPIO_PinAFConfig(GPIOA,GPIO_PinSource12,GPIO_AF_CAN1);//PA12复用到CAN1
//初始化配置CAN
CAN_InitTypeDef CAN_InitStruct = {0};
CAN_InitStruct.CAN_ABOM = ENABLE;//自动关闭总线
CAN_InitStruct.CAN_AWUM = DISABLE;//自动唤醒
CAN_InitStruct.CAN_BS1 = BS1;
CAN_InitStruct.CAN_BS2 = BS2;
CAN_InitStruct.CAN_Mode = Mode;//模式
CAN_InitStruct.CAN_NART = ENABLE;//无论发送的结果如何 消息均只发送一次
CAN_InitStruct.CAN_Prescaler = psc -1;//预分频
CAN_InitStruct.CAN_RFLM = DISABLE;//接收FIFO装满后,下一条传入信息将覆盖前一条信息
CAN_InitStruct.CAN_SJW = CAN_SJW_1tq;
CAN_InitStruct.CAN_TTCM = DISABLE;//时间触发通信模式
CAN_InitStruct.CAN_TXFP = DISABLE;
u8 temp = CAN_Init(CAN1, &CAN_InitStruct);
//初始化配置 CAN的筛选器
CAN_FilterInitStruct.CAN_FilterActivation = ENABLE;//激活或禁用这个筛选器
CAN_FilterInitStruct.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;//筛选后,被存入哪个接收FIFO(先进先出队列)0的优先级高,用来存安全气囊 ,1为日常的信息
CAN_FilterInitStruct.CAN_FilterIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdHigh = 0x0000;
CAN_FilterInitStruct.CAN_FilterMaskIdLow = 0x0000;
CAN_FilterInitStruct.CAN_FilterMode = CAN_FilterMode_IdMask;//设置筛选器的工作模式,使用"ID"和"掩码"来定义筛选规则
CAN_FilterInitStruct.CAN_FilterNumber = 0;//要配置的筛选器编号(也称为筛选器组)
CAN_FilterInitStruct.CAN_FilterScale = CAN_FilterScale_32bit;
CAN_FilterInit(&CAN_FilterInitStruct);
}
CAN发送信息
1)准备发送报文
①定义并清空一个 CanTxMsg 结构体变量 TxMessage。
②填充报文信息
2)提交发送请求
①调用 CAN_Transmit 函数,将填好的 TxMessage 提交给 CAN1 的一个空闲发送邮箱。
②该函数会返回一个邮箱号 (Mailbox),用于后续查询发送状态。
3)等待发送完成
①使用一个 while 循环,通过 CAN_TransmitStatus 函数不断查询指定邮箱的发送状态。
②代码中的逻辑是:如果发送失败 (CAN_TxStatus_Failed) 且未超时,则持续等待。
③如果超时(i >= 0xfff),则返回 1 表示发送失败;否则返回 0 表示发送成功。
具体代码实现:
cs
/*************************
函数名称:Can_Send_Message(u8 *sendmesg)
函数功能:Can发送信息
返回值:无
形参:无
作者:me
版本:1.0
*************************/
u8 Can_Send_Message(u8 *sendmesg,u8 len)
{
u16 i;
CanTxMsg TxMessage = {0};
TxMessage.DLC = len;//数据长度
TxMessage.IDE = CAN_Id_Standard;// 0为标准ID , 1为拓展ID
TxMessage.RTR = CAN_RTR_Data;// 0数据帧 , 摇控帧
TxMessage.StdId = 0x14;
for(i=0;i<len;i++)
{
TxMessage.Data[i]=sendmesg[i];
}
u8 Mailbox = CAN_Transmit(CAN1, &TxMessage);
//判断发送失败和超时的情况下退出
while(CAN_TransmitStatus(CAN1,Mailbox)==CAN_TxStatus_Failed && i<0xfff)
{
i++;
}
if(i>=0xfff)
{
return 1;
}
return 0;
}
CAN接收信息
1)检查数据是否到达
①调用 CAN_MessagePending 函数,查询 CAN_FIFO0 中是否有等待处理的报文。
②如果返回值为 0,表示 FIFO 为空,函数直接返回 0,结束本次接收。
2)读取报文数据
①如果 FIFO 中有数据,则调用 CAN_Receive 函数。
②该函数会从 CAN_FIFO0 中取出最早进入的报文,并将其信息(ID、数据长度、数据内容等)填充到 CanRxMsg 结构体 RxMessage 中。
3)
①通过循环,将 RxMessage.Data 中的数据拷贝到函数参数 resmesg 指向的缓冲区中。
②最后,函数返回 RxMessage.DLC,即实际接收到的数据长度。
具体代码实现:
cs
/*************************
函数名称:Can_Res_Message(u8 *resmesg)
函数功能:Can接收信息
返回值:无
形参:无
作者:me
版本:1.0
*************************/
u8 Can_Res_Message(u8 *resmesg)
{
u8 i;
CanRxMsg RxMessage = {0};
while(CAN_MessagePending(CAN1,CAN_FIFO0)==0)
{
return 0;
}
//第二个参数 FIFONumber 用于指定从哪个接收 FIFO(First-In, First-Out 缓冲区)中读取 CAN 报文。 CAN_FIFO0 -- CAN_FIFO1
CAN_Receive(CAN1, CAN_FIFO0, &RxMessage);
for(i=0;i<RxMessage.DLC;i++)
{
resmesg[i] = RxMessage.Data[i];
}
return RxMessage.DLC;
}
主函数调用代码
这里实现的功能是环回模式
cs
//CAN环回测试:
u8 key,cnt,Mode,i,time;
Can_Config(6,7,Mode,6);
u8 send_buff[8];
u8 rev_buff[8];
while(1)
{
//按键一按下是发送信息 , 按键二是切换模式
key = Key_Scan();
switch(key)
{
case 1:
{
Mode=!Mode;
Can_Config(CAN_BS1_7tq,CAN_BS2_6tq,Mode,6);
if(Mode==0)
{
printf("此时为普通模式\r\n");
}
else
{
printf("此时为环回模式\r\n");
}
}
;break;
case 2:
{
for(i=0;i<8;i++)
{
send_buff[i]=cnt+i;
}
printf("此时发送的数据如下:");
for(i=0;i<8;i++)
{
printf("%d ",send_buff[i]);
}
printf("\r\n");
Can_Send_Message(send_buff,sizeof(send_buff));
}
;break;
default: ;break;
}
key = Can_Res_Message(rev_buff);
if(key)
{
printf("此时接收到的数据如下:");
for(i=0;i<8;i++)
{
printf("%d ",rev_buff[i]);
}
printf("\r\n");
}
if(time == 50)
{
time = 0;
printf("cnt=%d\r\n",cnt);
cnt ++;
}
time ++;
Delay_Ms(10);
}
功能效果:
