当前位置: 首页 > java >正文

STM32 CAN总线

目录

定时传输CAN简介和硬件电路

CAN简介

主流通信协议对比 

​编辑 CAN硬件电路

​编辑 CAN电平标准

CAN收发器 – TJA1050(高速CAN)

CAN物理层特性 

帧格式 

数据帧

遥控帧 

错误帧 

过载帧 

​编辑 帧间隔

​编辑 位填充

波形实例 

位同步 

位时序

硬同步

再同步 

波特率计算 

仲裁

错误处理

CAN外设

STM32 CAN外设简介

​编辑 CAN收发器电路

CAN框图 

CAN基本结构 

CAN发送过程 

CAN标识符过滤器

CAN测试模式​编辑

CAN工作模式

STM32 CAN位时间特性 

CAN总线单个设备的回环测试和三个设备互相通信 

CAN标准格式-扩展格式-数据帧-遥控帧 (查询接收)

CAN-标识符过滤器-16位列表

CAN-标识符过滤器-16位屏蔽

CAN-标识符过滤器-32位列表

CAN-标识符过滤器-32位屏蔽

CAN-标识符过滤器-只要遥控帧​编辑

CAN-中断式接收

CAN-数据传输策略

定时传输 

触发传输

请求传输


定时传输CAN简介和硬件电路

CAN简介

CAN 总线( Controller Area Network Bus )控制器局域网总线
CAN 总线是由 BOSCH 公司开发的一种简洁易用、传输速度快、易扩展、可靠性高的串行通信总线,广泛应用于汽车、嵌入式、工业控制等领域
CAN 总线特征:
两根通信线( CAN_H CAN_L ),线路少
差分信号通信,抗干扰能力 CAN_H CAN_L 就是一对差分线
高速 CAN ISO11898 ): 125k~1Mbps, <40m
低速 CAN ISO11519 ): 10k~125kbps, <1km
异步,无需时钟线,通信速率由设备各自约定
半双工,可挂载多设备,多设备同时发送数据时通过仲裁判断先后顺序
11 /29 位报文 ID ,用于区分消息功能,同时决定优先级
可配置 1~8 字节的有效载荷
可实现广播式和请求式两种传输方式

应答、CRC校验、位填充、位同步、错误处理等特性

主流通信协议对比 

 CAN硬件电路

每个设备通过 CAN 收发器挂载在 CAN 总线网络上
CAN 控制器引出的 TX RX CAN 收发器相连, CAN 收发器引出的 CAN_H CAN_L 分别与总线的 CAN_H CAN_L 相连
高速 CAN 使用闭环网络, CAN_H CAN_L 两端添加 120Ω 的终端电阻
低速 CAN 使用开环网络, CAN_H CAN_L 其中一端添加 2.2kΩ 的终端电阻

 当某个设备想要发送0时,电阻就会操作总线把总线拉开,使其呈现0状态,当设备想要发送1时,就不去碰总线,总线在终端电阻的收缩下自动归位默认状态1(主要学习高速CAN)

这里为什么要加电阻?  

闭环的设计120欧

1.没有设备工作的时候,将两根差分线 电压收紧,使电压一致,就是设备没有操作总线的时候,将两根 线像弹簧一样收紧,拉倒同一水平,为1的状态,阻值越小,拉力越强,设备操纵总线 时候就拉开,就是 为0的状态

如图:

2.防止回波反射,尤其是高频信号,远距离传输的场景,不加的话,信号回波会在线路中断反射,进而干  

扰原始信号,就是有干扰,如图:

 CAN电平标准

CAN 总线采用差分信号,即两线电压差( V CAN_H -V CAN_L )传输数据位
高速CAN规定:

电压差为0V时表示逻辑1(隐性

电压差为2V时表示逻辑0(显性电平)

    

逻辑电平:高电平表示逻辑1 低电平表示逻辑0

差分电平:当CAN_HCAN_L(相对GND)的电压都为2.5V时,两线电压相等,电压差为0V,表示当前CAN总线处于逻辑1的状态当CAN_H对地电压3.5V CAN_L对地电压1.5V时,两线电压差为2V,表示当前CAN总线处于逻辑0状态

比如CAN总线想要发送101的数据流,那么CAN总线就会程序出两线收紧 两线张开 两线收紧的状态,这里逻辑表示和差分表示完成等效,在CAN总线里,实际传输的是差分电平,而在32的引脚以及常见的时序画法里会出现逻辑电平表示,因为实际画时序波形的时候总是画两根线,难免比较麻烦,况且这两根线实际只有10两种状态,所以我们简化一下画个逻辑表示行了

总线收紧状态称为隐性电平表示逻辑1,张开则相反

线收紧,没有电压差是默认状态所以叫隐性,两线张开,产生电压差,是需要设备干预的状态所以叫显性

显性电平和隐性电平同时出现时,总线会表示出显性电平状态 

低速 CAN 规定:

电压差为-1.5V时表示逻辑1(隐性电平)

电压差为3V时表示逻辑0(显性电平)

              

CAN收发器 – TJA1050(高速CAN

 

首先右边CANHCANL就是CAN总线,左边RECEIVER接收器可以时刻检测电压差,并输出到左边这根线,如果有电压差,就输出1,如果没有电压差,就输出0,这个10通过两个场效应管的输出驱动器输出到RXD引脚,这两个管就当成电子开关,右边为1时上管断开下管导通输出0,右边为0时上管导通下管断开输出1,所以这两个管还有电平反相的功能,最终总体上看,当CAN总线有电压差时,输出RXD引脚为低电平0,表示显性电平,CAN总线没有电压差时,输出RXD引脚为低电平1,表示隐性电平,所以这里RXD是输入部分,上面TXD就是输出部分,当TXD1时后面这个驱动器DRIVER,就会让上下两个管都断开,相当于不对总线进行任何操作,总线在外边终端电阻的收紧作用下呈现默认的隐性电平,当TXD0时后面这个驱动器DRIVER,就会让上下两个管联通,就相对于两只手,上面的手将CANH电压拽高,下面的手将CANL的电压拽低,这样两线就会分开,产生电压差,总线呈现显性电平0的状态,这就是发送0的操作 

CAN物理层特性 

帧格式 

CAN总线帧格式

CAN协议规定了以下5种类型的帧:

数据帧

D是显性电平R是隐性电平

D/R是根据你要发送的数据不同可以选择发D也可以选择发R

应答位特有的,发送方必须发隐性1,接收方必须发显性0,也就是发送方释放总线,接收方拉开总线,表示接收方对发送方的应答

帧起始 

在发送数据帧之前总线必须处于空闲状态,空闲状态总线是隐形电平(逻辑表示1),然后开始,数据帧第一位这里线画的是低电平,颜色是灰色表示此位必须是显性电平0,它的意思的SOF,帧起始,

报文ID

首先发送的是报文ID,标志格式是11位,如果你想要发报文ID是101 0101 0101,那这段时序就是隐显隐 显隐显隐 显隐显隐,报文ID可以表示后面数据的功能(因为总线上各种报文消息都有如果没有ID加以区分,那肯定就搞混了,同时报文ID还用于区分优先级,当多个设备同时发送时,根据仲裁规则,ID小的报文优先发送,ID大的报文等待下一次总线空闲再重试发送,不同功能的数据帧ID都不同),

RTR

后面紧跟着的是RTR,RTR占据1位,在数据帧里必须为显性0,RTR的意思是远程请求标志位,用于区分数据帧还是遥控帧,数据帧必须为显性0,遥控帧必须为隐性1,这里是数据帧所以必须为0

报文ID+RTR位可以被称为仲裁段,仲裁主要靠ID来实现,RTR也加进来的目的是相同ID的数据帧和遥控帧,数据帧的优先级大于遥控帧,数据帧和遥控帧的ID可以相同的

IDE和DLC

进入控制段,首先是IDE,意思是ID扩展标志位,用于区分标志格式还是扩展格式,标准格式为显性0,扩展格式为隐性1,下一位r0必须显性0,r0是保留位,目前还没用到,下一位DLC意思是保留数据段的长度(CAN总线一帧数据可以有1~8个字节有效载荷并且可以灵活指定)比如想发送一个字节就是0001,如果想发送8个就想1000,当然DLC要配合后面的数据段使用,数据段就是有效载荷的数据,根据DLC的指定,DLC指定几个字节,数据段就发送几个字节,要对应起来,Data就是数据的长度 0~64位,就是最大8个字节

CRC校验,首先会对前面所有的数据位进行CRC算法计算从SOF到Data计算得到一个校验码,附在后面,接收方收到数据和校验码之后也会调用CRC算法进行计算,看看计算的校验码是否一致,以判断传输是否有误比如身份证号最后一位,类似I2C奇偶校验

CRC界定符,必须是隐形1因为要给ack应答,有应答了就是0,得区分一下,不然一直是0,有没有应答都不知道

ACK应答槽

 在ACK槽前后操作总线的权力是有一个短暂的交换的,前面所有波形都是发送方操作,在ACK槽变为接收方操作总线,为了给权利交接留出时间,ACK槽前后,就要留两个界定符,在CRC界定符时发送方必须发送隐形1,除了做一个分隔,另一个作用是在ACK槽之前发送方必须释放总线,在ACK槽接收方会拉开总线,ACK槽接收后,接收方不能一直拉着不放,所以在ACK界定符时接收方必须及时释放总线,交出控制权

注意事项

(1)允许多个接收方接收发送方的消息,共同拉开总线,多个拉开显示0,就是拉低显示0,绿色那里没 有问题的 

(2)并不是发送方单方面发送消息的,不是发送完在应答,而是边发送边应答,发送方发送一位,接收 方就收到一位了,所以整个数据帧还没有结束就,接收方就收完了

eof发送7个1,结束  

扩展模式 

 这里只是比标准模式多了srr和18位id srr,就是占位的跟标准模式的RTR类似,但是这里要区别标准模式,标准模式优先级>扩展模式, 这里要用隐性为1, 然后IDE也要为1,表示扩展位置 然后这里因为是扩展模式了,RTR后面的IDE就恢复原状r1了。

遥控帧 

 遥控帧 用途是接收设备主动请求数据和数据帧类似 没有数据段
CAN总线的数据主要靠发送方自觉广播出来,一般发送方会定一个周期,定时广播自己的数据,但如果发送方没有及时发出数据或者这个数据的使用频率太低了,广播太频繁了大家都用不到,浪费总线资源,广播太慢了偶尔有用的话,又及时拿不到,这样我们就可以规定发送方不要主动广播这个数据了,而是如果有设备需要的话,首先接收方发出一个遥控帧,遥控帧包含报文ID,其实遥控帧也是广播出来的,每个设备都能收到遥控帧,然后如果其中的某个设备有这个ID的数据,它就会再通过数据帧广播出来,这样接收方就能及时获取这个数据了

错误帧 

 错误帧可以叠加在数据帧上,也可以破坏数据帧的数据

设备默认是主动错误状态,检测到错误时,会发6个显性位置0,会破坏总线上的数据,就是拉开总线,而被动错误的6个隐性1,并不会影响当前总线上已有的波形,如果别的设备正在发出波形,我发的是被动错误标志,那别设备的波形完成不受影响,发隐性1就是释放总线,自然不会受到干扰别的设备

主动错误频繁了,就会觉得不可靠,就会进入被动状态,发6个隐形位1,就是不碰总线,不会破坏总线 的数据,但是会破坏自己的数据

然后再发8个隐形1,错误界定符

那为什么还要画0~6个错误标志延长位勒? 因为:就是设备1的错误信息最后一位开始,才被设备2检测,设备2才开始发6位干扰数据,就0-6了呗

过载帧 

和错误帧类似

 帧间隔

 位填充

1.如果一直发送8个1,就是FF,可能辨别不出来,会有漏

2.区别错误帧和过载帧,他们都是6位的相同的,这时候位填充5位相同就可以取反,就可以识别出来

3.总线如果11位都是1,那么就是表面是空闲的状态 

波形实例 

标志波形:

11位划分 3/4/4 29位划分1/4/4/4/4/4/4/4

can总线是高位先行,会在波形中显示出来,crc计算的时候要剔除填充位

如果5个相同的,然后后面有个相反的填充位,然后下一位和前5位一样,那是不是就是错误位或者是过 载位

标准波形范围11位:0x000~0x7FF  

 扩展波形:

因为是29位,所以,范围是0x0000 0000~0x1FFF FFFF,最前面的是0(就是第一位)

比如扩展数据帧,报文ID为0x0789ABCD

二进制是:0111 1000 1001 1010 1011 1100 1101,但是只有28位,要补个0 占位

就是:00111 1000 1001 1010 1011 1100 1101  

位同步 

采样点 就像下图 在红线的位置在数据的中心位,但CAN总线也可以配置采样点在数据位中心偏前一点也可以配置在中心偏后一点(实际) 

 第一个问题关键就是采样点的初始位置没有对齐,如果我们能以第一次跳变沿为参考延迟半个数据位左右的时间,进行第一次的数据采样(在数据中间),后续再按照固定的采样间隔进行采样(硬同步)

第二个问题在设计时 考虑到误差补偿,在正常采样过程中发现了偏移并把下一次采样的间隔减少,将采样位置往前提一点这就就能弥补采样太慢的问题,相反采样太快就延长一次采样间隔

(再同步)

在一个数据位的波形中,采样点是属于两次跳变沿之间的,且采样点可以灵活地往前或往后进行调整,这样才能满足我们同步时,调节采样点位置的需求,所以我们要对一个数据位的时间进行进一步的细分,这就是所谓的位时序 

位时序

 Tq这个时间单位,可以直接在呈现中指定,比如我可以确定 一个Tq=0.5us,之后,一位包括四个段,SS段固定1Tq,PTS段可以由自己指定配置为1-8Tq,PBS1段可以由自己指定配置为1-8Tq,PBS2段可以由自己指定配置为2-8Tq,配置好后就会依次排列

如果数据跳变沿正好出现在SS段,那就说明当前设备与波形达成同步,如果数据跳变沿不在SS段,那就要调整当前设备的位时序使跳变沿正好出现在同步段

比如说有个波形的数据跳变沿正好在同步段,又因为我这里定义的位时序,就是一位的时间,所以下次数据如果再跳变它肯定在下一个位时序的同步段,如果每次跳变沿都在同步段,那就说明当前设备的位时序与波形是同步的,即我在当前设备定义的一位时间正好与波形一位的时间重合,这样当前设备如果是接收方,那它直接在PBS1和PBS2直接进行采样

PTS是吸收网络缓冲
PBS1和PBS2通过调节两个PBS1和PBS2端的长度可以改变采样点的位置

接收方如何借助这个位时序来与发送方进行同步的(硬同步 再同步)
 

硬同步

 硬同步:使接收方第一个采样点与波形的第一位对齐

分析: 

你看:红色的线,发送方从高电平到低电平,这就是sof,然后就是,接收方,就是那些格子,ss段没有 和红色的下降沿重合(上图),所以就是自动移位重合了一下(下图)  

sof:第一个下数据跳变的边沿

硬同步只在帧的第一个下降沿(逻辑电平的下降沿,差分电平的张开沿)

发送方出现第一个下降边沿,然后就是ss段,然后就是接收方就是知道了,也调到ss段,就将所有的秒 表同步了

采样是在PBS!和PBS2之间交接处采样

然后就是接收方和发送方都有一个'秒表',当转动到ss就说明同步 了,然后就可以正常传送消息 但是,还是有点误差的,你知道吗,就是一会正常,后面就不正常了,所以就是下面的再同步  

再同步 

就是补偿误差 

注意这里sjw是最大补偿值,而不是每次都补偿规定的数,防止波形中的噪声对位时序造成过大影响 比如SJW=2,这里快了1个位置,还是只补偿一个位置,而不是2个,是要误差格子和SJW共同商量, 如果误差>SJW,都只补偿SJW最大的值

 分析:

上图是接收方快于发送方,下图是接收方慢于发送方

1.上面两个图,就是比如先进行了一次硬同步,然后正常了一会,。然后就是又跑偏了,你看那个红色的 下降沿都没有跟黄色的ss段重合,就是有误差了,要么是接收方的秒表跑快了,要么是发送方跑慢了。 总之现在sahib接收方快于发送方

但是,发送方是固定的速度呀,不可能调整熟读的呀,只能接收方来调整,进行误差补偿,上图就红色那,加了两个延时,数据就往后延时了,采样就会在中间,没有就不在中间了

注意:数据采样是在PBS1和PBS2之间 采样的,

2.你看这里就是接收方慢于发送方,发送方提前了,所以这里减少误差补偿就是2个Tq,然后就是你看左边 绿色的PBS2是不是就是少了两个方格

注意,硬同步只能在第一个下降沿一次,二再同步可以在第一个下降沿的每一个数据段

波特率计算 

每秒传输多少个bit

在二进制调制下,波特率=比特率的值

这里除了SS是1Tq,其他的都我们自己设置的

仲裁

线与:只要有一个设备拉开总线,就是0,所有设备释放了总线,才是1

问题:

上面的那个东西发送方先发了,然后又有一个发送方想发送东西,

这时候要么是不让第二个走,就是方案一

要么是把第二个移到前面去,和第一个发送方是在同一个起始位置然后这里就是方案二, 

方案一: 

方案二: 

 同时有两种情况
第一种是 发送需求确实是同时到来的,原来总线一直处于空闲,而两个或多个设备(很巧)同时想要开始发送
第二个种是 因等待而同时到来,比如设备A在这段时间已经占用了总线,设备BCD等等在A占用的期间陆续有发送的需求,但A现在已经被占用了,A一旦结束BCD就会蜂拥而上

回读机制:我发出1读回的是0说明总线上有别的设备,我感知到了别的设备和我的冲突,所以这时候我就退出不再和别设备抢总线资源了

D号越小优先级越高?
ID号越小 二进制出现的1就越晚
ID号越大 1出现的就越早
ID出现差异,且发出数据1的仲裁失利,所以ID号出现的越早,就越容易仲裁失利,优先级自然也就越低

 执行流程:发送一位,回读一位

你看,这个单元1和单元2前面(红色之前那段)都是一样的,他们发送的消息,然后总线返回的数据跟 他们发送的一样的,那么就会继续,(比如发送的是1,然后总线的也是1,然后设备读取到的数据回读 机制),然后到了红色那,设备1发送1,设备2发送0,然后根据的是设备id优先级仲裁,设备号小的就 优先级大,然后(0>1),还有线与的功能,此时总线是0,然后最后回读,设备1发送的是1,但读到总线 是0,那么失败了,设备2发送0,读取到0,成功

在仲裁里会设置位填充,那么位填充会不会影响优先级的啊,不会影响,

位填充会参与仲裁,但是不会影响,不会改变原有的id号的优先级

 相同ID号(相同设备)的数据帧与数据帧是不允许同时出现的(不能进行仲裁)

 相同ID号(相同设备)的遥控帧与遥控帧是不允许同时出现的(不能进行仲裁) 

  相同ID号(相同设备)的数据帧与遥控帧是不允许同时出现的(不能进行仲裁)

 你看,如果这里是标准遥控帧和扩展数据帧进行仲裁,又该如何操作?

遥控帧RTR是1,扩展数据帧的SRR也是1,然后标准帧就结束了仲裁,但是还有个IDE,数据帧是0,扩展 数据帧因为扩展了就是1,所以线与了,扩展帧出现发1收0的情况,然后就自动退出了,所以标准遥控帧 获胜  

错误处理

当某个设备检测到总线的帧传输出错了,它可以主动发出错误帧,通知每个设备这个帧错了,然后大家都扔掉这个错误的帧

位错误:就是回读机制,设备发送的数据,然后读取总线的数据,然后对比,一样就没有问题,但是, 不一样就是有位错误。 但是排除仲裁段,(只是仲裁失败,不是发送失败),还有就是应答机制(发1收0),他只是应答,不 是位错误

格式错误:就是CRC界定符,ACK界定符,EOF...这些都是1,但是如果被检测到时0,那就是格式错误 

应答错误:发送单元在ACK槽中检测出隐性电平时所检测到的错误,进度来说就是发送方发出一个帧,没有接收方给应答,这时,发送方就认为产生了ACK错误,在ACK槽时,发送方已经释放了总线,如果有接收方,那接收方必须在ACK槽拉开总线使总线变为显性电平0,发送方读到0,就说明有接收方收到了 ,发送方没读到0,就说明没有接收方收到了 ,发送方就认为产生ACK错误

如果发送方或者接收方检测到这些错误,就会发送错误通知,就会停止数据,终止。然后总线就变空 闲,等到下一次数据传输 当然,现在每个设备都可以破坏数据传输,但是设备要是发疯,那么不严谨,一直出错,所以得设置防 止一直出错,然后就是有了设备的错误状态

 一开始是主动错误状态,会拉点为0,有破坏数据的能力,但是太频繁了就会变成被动的错误状态,然后 就是错误的转态,是1,就是释放总线,没有破坏数据的能力,但是要是也频繁了,就会被关闭了。

关闭了,但是想要启动,就得总线上检测到128次11个隐形就可以启动了。  

TEC是发送的错误状态值,如果检测到错误的话,就+1,但是如果发送成功一次的话就-1,

REC是接收的错误状态值,如果检测到错误的话,就+1,但是如果发送成功一次的话就-1,

所有他们反应的是当前设备检测的状态错误的相对频率

正常结束了,eof后面还有3个帧间隔,给过载帧准备的,如果这个数据帧的接收方过载了,想要延迟发送方的数据传送,那么接收方就会在帧间隔的第一位拉开总线产生过载帧

第一个图正常标志:ack1位+7位EOF+3位帧间隔=11位,所以每个设备检测到11位隐形电平就是空 闲状态。

第二个图 因为没有设备应答,这时发送方就检测到了一个应答错误,所以发送方会在ACK槽的下一位发出一个错误帧,因为目前发送方还是主动错误状态,所以它会发出主动错误帧,而且8位错误界定符+3位帧间隔=11位隐形电平也是空闲状态

第2个图,这里因为主动状态从16次后就变成了被动状态,然后就,所以那个过渡帧,就会额外产生8位 延迟 因为127/8=16;  

如果主动错误状态和被动错误状态,同时处于空闲时,那么主动错误状态会直接控制总线,因为被动状 态还有8个延时  

CAN外设

STM32 CAN外设简介

 CAN收发器电路

 

CAN框图 

 

1.核心里面有很多寄存器,我们可以读写这些寄存器来对CAN电路进行配置或者获取电路的各种状态,呈现通过读写寄存器来操作对电路的运行 

2.主发送邮箱,发送邮箱有3个每个邮箱可以存入一个CAN报文,如果我们想发出一个报文,那我们把这个报文写入到一个空置邮箱之后设置寄存器请求发送就行了

3.这是接收部分包括接收过滤器和2FIFO,当CAN总线出现一个数据帧或者遥控帧时,CAN硬件电路都会把这个报文缓存下来,至于是不是能保留这个报文,那得看能不能通过过滤器,过滤器内我们可以设置过滤规则告诉硬件,我们想要什么ID的报文,如果硬件收到了这些报文就可以把它存入FIFO,如果报文无法通过任何过滤器说明这个报文我们不需要,那硬件就直接扔了可以减轻负担,通过过滤器的报文会自动存入主接收FIFO 0或主接收FIFO 1FIFO的意思是先进先出寄存器(队列)这里有俩队伍,每个队伍有3个邮箱,也就是最大存入3个报文,如果接受报文很快,CPU无法及时读走那报文就可以在FIFO 0FIFO 1里排队,可以在一定程度上避免报文丢失

CAN基本结构 

TX对应PA12 RX对应PA11
复用推挽输出  上拉输入
引脚进来之后由这个发送和接收控制器全权管理,你想发什么报文只需把报文内容告诉它,之后就自动给你发出去,同样当接收到报文时,它也会自动和你配置的过滤器进行比对,符号过滤器的报文,它自动帮你存入FIFO的队列之中,CUP直接读取FIFO就行了

发送部分当我们想发出一个报文时,我们只需要把报文的各个参数比如ID Data IDE RTR等写入到一个发送邮箱,然后给一个请求发送的命令,之后这个管理员就会等待总线空闲,然后自动把这个报文广播到总线上,为避免短暂拥堵就设置了3个邮箱

接收当有报文先收下,一直到接收过滤器,接收过滤器可根据ID号对报文进行过滤,如果ID号不是我们想要的那报文就通过不了过滤器,就会直接丢弃,过滤器有14个,我们可以任意对一个进行配置把我们想要接收的报文ID规则写入到过滤器中,这样总线上一旦出现我们想要的报文就会通过过滤器进入FIFO中被CPU读取,先存邮箱0,如果又来报文就存入邮箱1,如果FIFO邮箱都满了,这时,STM32可以配置FIFO的锁定状态,来处理FIFO满之后,新的报文该怎么存,如果配置FIFO锁定,会把满出来的报文直接丢弃,如果配置不锁定,新报文会把邮箱2的数据踢出去自己占据邮箱2的位置

发送,可以配置按id优先级发送,也可以按先来后到发送 接收就只能是队列,先来先到

为什么这里要设置两个队伍呀?

因为就是,就好比是饭堂打饭,就是,学生的窗口和老师的窗口,有重要的和不重要的嘛,当然这里是 相等的优先级

CAN发送过程 

 RQCP:请求完成

TXOK:发送成功

TME:发送邮箱空,=1表示空闲状态

x:表示任意值

TXRQ:发送请求控制位,=1表示产生这个请求

IDLE空闲

ABRQ:终止该发送

NART:禁止自动重传,=0表示使用自动重传,就回到预定转态然后等待总线空闲,然后再次发送,=1表 示禁止自动重传,发送失败后,直接进入空置状态

 FMP:报文数目 0=00,第一个报文就是0先,第二个就是0x10b,第三个就是0x11b,溢出了也是0x11b

FIOVR:FIFO溢出为1,没有溢出就是0

溢出了再次收到消息也都是溢出

RFOM=1,读完一个报文就要释放邮箱

如果是溢出状态,那么就是直接跳转到报文2,而不是从溢出跳转到报文3,再到报文2,溢出时状态三就 是跟状态一样的。

CAN标识符过滤器

 每个过滤器还有如下配置位,来配置功能细节
X代表0-13因为是有14个过滤器
FSCx=1那就工作在上面两种状态(32位位宽的状态)
FSCx=0那就工作在下面两种状态(16位位宽的状态)

 这两种都可以再配置FBM来选择模式
所以组合下来过滤器就可以有四种工作状态
我们先看第二种工作状态2个32位过滤器—标识符列表模式(32位列表模式),R1和R2两个寄存器都写入的是目标ID,我们可以直接把想要的ID号,写入到R1或R2寄存器中,R1和R2寄存器总共可以写两个目标ID,当管理员收到报文时,它就和R1、R2寄存器的目标ID对比,如果有一样的就通过过滤器,如果都不一样就不能通过过滤器

那怎么写入目标ID号呢?首先下面这里指明了存储映像,也就是每一位表示什么意思,其中这个32位寄存器的高11位需要存入的是标准格式的ID号:STID,然后标准ID后面跟着,这18位需要存入是扩展格式的ID号:EXID,我们知道扩展ID总共是29位,所以,如果想完整存入一个扩展ID,则需要占用前面11位标准ID的空间,再外加后面18位扩展ID的空间,如果想写标准ID,后面18位都写0
那过滤器咋知道你写入的是标准ID还是扩展ID呢,这就要看后面几位IDE:1是扩展帧0是标准帧,注意前后要一致,如果IDE写1而前面是标准帧,依旧会按标准帧格式过滤,如果想过滤数据帧RTR位写0,想过滤遥控帧RTR位就写1

如果ID号高位是0X1,则低位无论是什么都直接通过过滤器,这样就能完美实现过滤100个ID的需要了,这就是屏蔽模式,R1写ID号,R2决定ID的哪些位必须一样

假设我们的需求是过滤出0x1开头的所以标准ID号,11位二进制ID号是001 xxxx xxxx,那首先需要把这个ID号写入R1寄存器里,从左到由右依次是001,后面可以随便填一般填 0000 0000,IDE一定写0,我们需要标准ID,RTR写0表示需要数据帧,在R2寄存器里写1,表示R1里面的ID位对应位必须匹配一致,写0,表示R1里的ID位对应位1和0均可,现在我们需要标准ID的高3位必须匹配而剩下的位1或0均可,而IDE必须给1表示R1寄存器对应位必须匹配然后RTR,也是,与R1一样就是给1,都可以就是给0,比如R! 的RTR给了1,那么R2的RTR想一致的话给 1,遥控帧或数据帧都要就给0

 这里为什么要左移5位

因为地址左移5位才可以是id的地址,16位映像是左对齐,赋值语句默认是右对齐,RTR IDE EXID总共占据5位,所以写入ID时,必须得左移5位,这样才能对齐映像,然后低位5位默认给0,如果想接收遥控帧就   | 0x10即可
 

ID: R1[15:0]=0x200<<5//写入0x200左移5,然后就是,RTR,IDE没有写入默认是0
Mask: R1[31:16]=
(0x700<<5)|0x10|0x8

 第二排,屏蔽,为什么是0x700,700:111 0000 0000,左移5位就是,屏蔽,高3位一样了。

然后就是|0x10,(10:1000,就是RTR要一样,然后就是|ide 0x8)

这里或上0x8是表示IDE位和上面的ID的IDE位保持一致,,注意这里屏蔽位置1都是跟上面一样,列表一 样,置0就是随便,注意这里IDE在16位就得一样,防止错误

 CAN测试模式

静默模式:TX引脚始终发送1(啥也没发),然后发送端直接接到接收端(自己发自己接)同时RX引脚接收的报文也可以进入接收端,这个模式除了可以自发自收,还可以默默的监测CAN总线的报文数据
环回模式:RX引脚断开不接受任何报文,TX引脚可以发送报文,同时发送的数据自己可以收回来
环回静默:自发自收,完全不影响

CAN工作模式

正常模式转入睡眠模式,我们要给SLEEP位置1,表示请求进入睡眠,但是一般来说SLEEP置1后,CAN外设不会立刻睡眠,因为有可能此时报文才发一半,立刻睡眠报文数据就破坏了,所以有个等待ACK信号,应答之后,SLAK=1才确认睡眠,SYNC信号也是,等待SYNC信号(等待总线空闲时)

SLAK:睡眠确认状态位,=1表示确认进入睡眠状态位

INAK:初始化确认位,=0表示未确认进入初始化模式

-----不会马上进行,还会等待一下,就像下面的请求

INRQ:请求进入初始化 

SLEEP:请求进入睡眠状态

SYNC:总线空闲

正常模式转入睡眠模式,我们要给SLEEP位置1,表示请求进入睡眠,但是一般来说SLEEP置1后,CAN外设不会立刻睡眠,因为有可能此时报文才发一半,立刻睡眠报文数据就破坏了,所以有个等待ACK信号,应答之后,SLAK=1才确认睡眠,SYNC信号也是,等待SYNC信号(等待总线空闲时) 

STM32 CAN位时间特性 

这里BS1把上面的PTS+PBS1合并起来了
TS1寄存器,用于配置BS1有几个Tq,TS2寄存器,用于配置BS2有几个Tq,BRP寄存器用于配置1个Tq的时长,

左图,

最左边的一列,中断信号源

中间的一列是,中断使能寄存器(是否使能中断信号输出)ITConfig函数配置的就是这些IE寄存器

看手册,CAN_IER寄存器有具体说明 

 

 从离线状态恢复到错误主动状态,CAN协议规定的是,当出现128次11个隐性位自动就恢复到错误主动了,但是STM32在这个恢复的箭头加了个开关ABOM,如果ABOM置1,那进入离线状态后,就自动开启恢复过程,和CAN协议一样,但是ABOM置0,进入离线状态后,设备无法直接恢复位错误主动,而是软件必须先请求进入然后再退出初始化模式,就是INRQ位先置1再清0,之后检测到128次11位隐性位才能恢复到错误主动

CAN总线单个设备的回环测试和三个设备互相通信 

void CAN_DeInit(CAN_TypeDef* CANx);
恢复缺省位置,用于把CAN配置到默认的复位状态uint8_t CAN_Init(CAN_TypeDef* CANx, CAN_InitTypeDef* CAN_InitStruct);
第一个参数指定是CAN1还是CAN2,第二个参数是结构体包含了很多用于初始化CAN外设的参数void CAN_FilterInit(CAN_FilterInitTypeDef* CAN_FilterInitStruct);
对过滤器进行初始化的函数 参数是结构体包含了用于初始化过滤器的参数void CAN_StructInit(CAN_InitTypeDef* CAN_InitStruct);
用于给第二个结构体赋一个默认值void CAN_SlaveStartBank(uint8_t CAN_BankNumber); 
用于配置CAN2的起始滤波器号(互联设备使用)void CAN_DBGFreeze(CAN_TypeDef* CANx, FunctionalState NewState);
用于调试时的冻结模式void CAN_TTComModeCmd(CAN_TypeDef* CANx, FunctionalState NewState);
用于使能TTCM模式中的TGT位以上函数用于初始化CAN外设uint8_t CAN_Transmit(CAN_TypeDef* CANx, CanTxMsg* TxMessage);
发送一个CAN报文,参数是TxMessage结构体,需要提前写入待发送报文的各个数据
,返回值表示报文存入了哪个发送邮箱uint8_t CAN_TransmitStatus(CAN_TypeDef* CANx, uint8_t TransmitMailbox);
获取发送邮箱的状态,参数:写入要检测的邮箱,返回值是此邮箱的状态void CAN_CancelTransmit(CAN_TypeDef* CANx, uint8_t Mailbox);
取消发送 置ABRO位为1以上是发送相关的函数 重点掌握前两个void CAN_Receive(CAN_TypeDef* CANx, uint8_t FIFONumber, CanRxMsg* RxMessage);
读取接收FIFO的数据,第一个参数指定读取哪个参数,第二个参数RxMessage是一个输出参数的结构体,传入的结构体在函数退出后会获得接收到的报文数据void CAN_FIFORelease(CAN_TypeDef* CANx, uint8_t FIFONumber);
释放FIFO 置RFOM为1 释放邮箱uint8_t CAN_MessagePending(CAN_TypeDef* CANx, uint8_t FIFONumber);
获取指定FIFO队列里排队的报文数目,参数是哪个FIFO,返回值返回FIFO中排队报文的数目 (获取队伍长度 FMO寄存器的值)判断FIFO是否有报文排队时,就可以调用这个函数以上是接收相关的函数uint8_t CAN_OperatingModeRequest(CAN_TypeDef* CANx, uint8_t CAN_OperatingMode);
工作模式请求 参数可以指定初始化 正常 睡眠,函数内部自动操作SLEEP位和INRQ位,调用函数指定参数就可以请求进入工作模式了uint8_t CAN_Sleep(CAN_TypeDef* CANx);
函数内部执行INRQ为0 SLEEP为1,调用这个函数,直接指定CAN进入睡眠模式uint8_t CAN_WakeUp(CAN_TypeDef* CANx);
函数内部执行SLEEP为0,调用这个函数,直接指定CAN退出睡眠模式,默认进入正常模式以上是工作模式切换的函数uint8_t CAN_GetLastErrorCode(CAN_TypeDef* CANx);
获取最近一次的错误码,返回值表示最近一次的错误是什么类型uint8_t CAN_GetReceiveErrorCounter(CAN_TypeDef* CANx);
获取接收错误计数器的值(REC计数器的值)uint8_t CAN_GetLSBTransmitErrorCounter(CAN_TypeDef* CANx);
获取发送错误计数器的低8位(TEC计数器的值)
#include "stm32f10x.h"                  // Device headervoid MyCAN_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);CAN_InitTypeDef CAN_InitStructure;CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125KCAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq ;CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;CAN_InitStructure.CAN_NART = DISABLE;CAN_InitStructure.CAN_TXFP = DISABLE;CAN_InitStructure.CAN_RFLM = DISABLE;CAN_InitStructure.CAN_AWUM = DISABLE;CAN_InitStructure.CAN_TTCM = DISABLE;CAN_InitStructure.CAN_ABOM = DISABLE;CAN_Init(CAN1, &CAN_InitStructure);CAN_Init(CAN1,&CAN_InitStructure);CAN_FilterInitTypeDef CAN_FilterInitStructure;CAN_FilterInitStructure.CAN_FilterNumber = 0;//指定第一个过滤器初始化//下面四个函数对应R1和R2寄存器//想要接收所以帧,选择32位屏蔽模式,32位ID随意给,32位屏蔽位全给0CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;//32位位宽CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;//选择屏蔽模式CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;//进FIFO0,还是FIFO1CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;//激活过滤器0CAN_FilterInit(&CAN_FilterInitStructure);}
//封装一个发送报文的函数
void MyCAN_Transmit(uint32_t ID, uint8_t Length, uint8_t *Data)
{CanTxMsg TxMessage;TxMessage.StdId = ID;//标准IDTxMessage.ExtId = ID;//扩展IDTxMessage.IDE = CAN_Id_Standard;//使用标志格式//扩展标志位 TxMessage.RTR = CAN_RTR_Data;//数据帧//遥控标志位TxMessage.DLC = Length;//数据段长度for(uint8_t i =0; i < Length;i ++){TxMessage.Data[i] = Data[i] ;//数据段内容/这里Data是一个数组//这里把结构体Data数组的第i个,赋值为参数传进来的Data第i个}//调用CAN_Transmit,这个结构体指向的报文就会被写入发送邮箱,并由管理员发送int8_t TransmitMailbox = CAN_Transmit(CAN1, &TxMessage);//此函数内部可以对照发送过程PPT理解uint32_t Timeout = 0;while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok){Timeout ++;if (Timeout > 100000){break;}}
}//接收函数,使用查询接收,需要两个函数,一个是判断接收FIFO里是否有报文,另一个是读取接收FIFO,把报文内容取出来//判断队伍里有报文吗
uint8_t MyCAN_ReceiveFlag(void)//判断是否有报文,有报文返回1,没有返回0
{if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)//如果读取FIFO队伍长度大于0,返回1{return 1;//表示队伍里有报文}return 0;//表示队伍没里有报文
}//读取FIFO0报文数据,读取后应该返回三个信息,ID,数据长度,数据内容
void MyCAN_Receive(uint32_t *ID, uint8_t *Length, uint8_t *Data)
{CanRxMsg RxMessage;CAN_Receive(CAN1,CAN_FIFO0,&RxMessage);//先判断IDE是不是标志格式if(RxMessage.IDE == CAN_Id_Standard){//则把RxMessage.StdId赋值给输出参数*ID*ID = RxMessage.StdId;}else{//则当前是扩展模式*ID = RxMessage.ExtId;}//再判断是不是接收到的数据帧if (RxMessage.RTR == CAN_RTR_Data){//首先得到长度*Length = RxMessage.DLC;for (uint8_t i = 0; i < *Length; i ++){Data[i] = RxMessage.Data[i];//依次把RxMessage.Data的第i个取出来放到输出参数Data[i]里}}else{//...}
}
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(2,1,"RxID:");OLED_ShowString(3,1,"RxID:");OLED_ShowString(4,1,"RxID:");while (1){KeyNum = Key_GetNum();if(KeyNum == 1){uint8_t TxData[] = {0x66,0x88};MyCAN_Transmit(0x123,2,TxData);}if(MyCAN_ReceiveFlag()){uint32_t RxID;uint8_t RxLength;uint8_t RxData[8];MyCAN_Receive(&RxID,&RxLength,RxData);OLED_ShowHexNum(2,6,RxID,3);OLED_ShowHexNum(3,6,RxLength,3);OLED_ShowHexNum(4,6,RxData[0],2);OLED_ShowHexNum(4,9,RxData[1],2);}}
}

分别对每个设备烧录,每次烧录一次改变一次TXID号 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;
uint32_t TxID = 0x555;
uint8_t TxLength = 4;
uint8_t TxData[8] = {0x11, 0x22, 0x33, 0x44};uint32_t RxID;
uint8_t RxLength;
uint8_t RxData[8];int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(1, 1, "TxID:");OLED_ShowHexNum(1, 6, TxID, 3);OLED_ShowString(2, 1, "RxID:");OLED_ShowString(3, 1, "Leng:");OLED_ShowString(4, 1, "Data:");while (1){KeyNum = Key_GetNum();if (KeyNum == 1){TxData[0] ++;TxData[1] ++;TxData[2] ++;TxData[3] ++;MyCAN_Transmit(TxID, TxLength, TxData);}if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxID, &RxLength, RxData);OLED_ShowHexNum(2, 6, RxID, 3);OLED_ShowHexNum(3, 6, RxLength, 1);OLED_ShowHexNum(4, 6, RxData[0], 2);OLED_ShowHexNum(4, 9, RxData[1], 2);OLED_ShowHexNum(4, 12, RxData[2], 2);OLED_ShowHexNum(4, 15, RxData[3], 2);}}
}

CAN标准格式-扩展格式-数据帧-遥控帧 (查询接收)

先把模式配置成环回模式

之前发送函数只能发送3个参数,其中IDE和RTR写死了,现在我们想正在发送的时候也可以指定IDE和RTR,一种方法是把IDE和RTR也提取成参数,这样做当然可以,这样函数就有五个参数了比较多,而且原来的结构体CanTxMsg本来就是封装函数参数的,再把结构体拆开,用5个参数赋值,这样做,感觉步骤太繁琐了
所以既然结构体里所有的参数都用到了,那就直接用结构体传(之前写好的)递参数吧,用地址传递提高效率,传递结构体指针后,下面这个取地址符号就不用了,这样发送函数就改造完成
函数调用者提供结构体变量,然后函数内部,把这个结构体表示的帧给发出去

接收函数也是直接用CanRxMsg这个结构体来传递参数,内部判断和赋值都不需要了,这里只是重新包装了一下CAN_Receive函数,也可以在主函数中直接调用CAN_Receive函数,包装一下可以少写俩参数
 

这说改造后的发送和接受函数

//封装一个发送报文的函数
void MyCAN_Transmit(CanTxMsg *TxMessage)
{int8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);//此函数内部可以对照发送过程PPT理解uint32_t Timeout = 0;while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok){Timeout ++;if (Timeout > 100000){break;}}
}//接收函数,使用查询接收,需要两个函数,一个是判断接收FIFO里是否有报文,另一个是读取接收FIFO,把报文内容取出来
//判断队伍里有报文吗
uint8_t MyCAN_ReceiveFlag(void)//判断是否有报文,有报文返回1,没有返回0
{if (CAN_MessagePending(CAN1, CAN_FIFO0) > 0)//如果读取FIFO队伍长度大于0,返回1{return 1;//表示队伍里有报文}return 0;//表示队伍没里有报文
}
//读取FIFO0报文数据,读取后应该返回三个信息,ID,数据长度,数据内容
//接收到的数据存入RxMessage里
void MyCAN_Receive(CanRxMsg *RxMessage)
{CAN_Receive(CAN1,CAN_FIFO0,RxMessage);}

 主函数,上面是用单独的变量存储发送和接受的相关数据,现在用结构体来实现CanTxMsg TxMsg;在结构体后写个等于号,按照规定顺序依次给结构体的每一个成员赋初值,成员的顺序可以转到结构体定义

主函数也进行了改造,直接调用结构体里的成员

现在TDE可以修改成扩展格式 RTR可以修改成遥控帧

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;CanTxMsg TxMsg = {
/*	StdID     ExtID          IDE            RTR      DLC            Data[8]     */0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data, 4, {0x11, 0x22, 0x33, 0x44}
};CanRxMsg RxMsg;
int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(1, 1, "TxID:");OLED_ShowHexNum(1, 6, TxMsg.StdId, 3);OLED_ShowString(2, 1, "RxID:");OLED_ShowString(3, 1, "Leng:");OLED_ShowString(4, 1, "Data:");while (1){KeyNum = Key_GetNum();if (KeyNum == 1){TxMsg.Data[0] ++;TxMsg.Data[1] ++;TxMsg.Data[2] ++;TxMsg.Data[3] ++;MyCAN_Transmit(&TxMsg);}if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxMsg);OLED_ShowHexNum(2, 6, RxMsg.StdId, 3);OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);}}
}     

 再根据上面函数进行修改

现在再进行改造,我们现在可以接受标志帧和扩展帧,还有数据帧和遥控帧
所以现在接收时需要进行判断

首先判断标准格式和扩展格式,同时还想在OLED第一行显示一下帧的类型,比如标志格式就显示个Std,扩展格式就显示个Ext

再判断是数据帧还是遥控帧,同时还想在OLED第一行显示一下帧的类型,比如数据帧格式就显示个Data,遥控帧格式就显示个Remote

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;CanTxMsg TxMsg = {
/*	StdID     ExtID          IDE            RTR      DLC            Data[8]     */0x555, 0x12362784, CAN_Id_Extended, CAN_RTR_Remote, 4, {0x11, 0x22, 0x33, 0x44}
};CanRxMsg RxMsg;
int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(1, 1, " Rx :");OLED_ShowString(2, 1, "RxID:");OLED_ShowString(3, 1, "Leng:");OLED_ShowString(4, 1, "Data:");while (1){KeyNum = Key_GetNum();if (KeyNum == 1){TxMsg.Data[0] ++;TxMsg.Data[1] ++;TxMsg.Data[2] ++;TxMsg.Data[3] ++;MyCAN_Transmit(&TxMsg);}if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxMsg);if(RxMsg.IDE == CAN_Id_Standard)//判断是不是收到标志格式{OLED_ShowString(1, 6, "Std");OLED_ShowHexNum(2, 6, RxMsg.StdId, 8);//这时在2行6列显示StdId ID(标准格式ID)}else if(RxMsg.IDE == CAN_Id_Extended)//判断是不是扩展格式{OLED_ShowString(1, 6, "Ext");OLED_ShowHexNum(2, 6, RxMsg.ExtId, 8);//这时在2行6列显示ExtId ID(扩展格式ID)}if(RxMsg.RTR == CAN_RTR_Data)//判断收到的是不是数据帧{OLED_ShowString(1, 10, "Data  ");OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);}else if(RxMsg.RTR == CAN_RTR_Remote)//判断收到的是不是遥控帧{OLED_ShowString(1, 10, "Remote");OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, 0x00, 2);OLED_ShowHexNum(4, 9, 0x00, 2);OLED_ShowHexNum(4, 12, 0x00, 2);OLED_ShowHexNum(4, 15, 0x00, 2);}}}
}     

改为遥控帧 后面的数据其实是无效的 

 

再进行改造 使得按键每次按下依次取出TxMsgArray里的每一项

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;CanTxMsg TxMsgArray[] = {
/*StdId     ExtId           IDE             RTR     DLC      Data[8]           */{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x44}}
};//这个数组的类型是CanTxMsg CanRxMsg RxMsg;//定义一个变量用于指示发到第几帧了
uint8_t pTxMsgArray = 0;int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(1, 1, " Rx :");OLED_ShowString(2, 1, "RxID:");OLED_ShowString(3, 1, "Leng:");OLED_ShowString(4, 1, "Data:");while (1){KeyNum = Key_GetNum();if (KeyNum == 1){MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);pTxMsgArray ++;//每按下一次pTxMsgArray加一,移动数组的下一项//进行越界判断if(pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg)){pTxMsgArray = 0;}}if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxMsg);if(RxMsg.IDE == CAN_Id_Standard)//判断是不是收到标志格式{OLED_ShowString(1, 6, "Std");OLED_ShowHexNum(2, 6, RxMsg.StdId, 8);//这时在2行6列显示StdId ID(标准格式ID)}else if(RxMsg.IDE == CAN_Id_Extended)//判断是不是扩展格式{OLED_ShowString(1, 6, "Ext");OLED_ShowHexNum(2, 6, RxMsg.ExtId, 8);//这时在2行6列显示ExtId ID(扩展格式ID)}if(RxMsg.RTR == CAN_RTR_Data)//判断收到的是不是数据帧{OLED_ShowString(1, 10, "Data  ");OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, RxMsg.Data[0], 2);OLED_ShowHexNum(4, 9, RxMsg.Data[1], 2);OLED_ShowHexNum(4, 12, RxMsg.Data[2], 2);OLED_ShowHexNum(4, 15, RxMsg.Data[3], 2);}else if(RxMsg.RTR == CAN_RTR_Remote)//判断收到的是不是遥控帧{OLED_ShowString(1, 10, "Remote");OLED_ShowHexNum(3, 6, RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, 0x00, 2);OLED_ShowHexNum(4, 9, 0x00, 2);OLED_ShowHexNum(4, 12, 0x00, 2);OLED_ShowHexNum(4, 15, 0x00, 2);}}}
}     

 如果想要不同设备进行收发呢,想要设备1发送不同的帧,然后设备2接收并显示,改一下测试模式就行了,改成Normal

然后把发送部分注释掉,将接收函数烧录到设备2中,把接收部分注释掉,将接收函数烧录到设备1中(设备1不注释也不影响)

CAN-标识符过滤器-16位列表

首先,修改发送报文的列表模拟总线上存在的报文,然后修改过滤器的配置值,让过滤器只接受我们想要的报文,下载程序,实验验证

这里模式调回环回模式

 按第一次按下按键,被过滤器过滤掉了没有收到,第二次按下按键,收到报文,第三次按下按键,也收到报文,第四次按下按键,没收到报文,第五次按下按键,收到报文,第六次按下按键,没收到报文,第七次按下按键,回到第一条没收到报文,,从测试看出,接收方只会收到ID位234、345、567的报文,与过滤器里配置的值一致

我们用设备2,监控一下总线上的所有报文,将模式调为静默模式,过滤器调为全通,

目前设备1是环回模式只接收部分报文,设备2是静默模式接收全部报文

第一次按下第二个按下

第三次按下第四次按下

第五次按下 第六次按下

第七次按下 

也可以配置两个过滤器

如果这里要过滤遥控帧,那就是要加上RTR,

 CAN_FilterInitStructure.CAN_FilterIdHigh = (0x234 << 5)|0x10;

 CAN-标识符过滤器-16位屏蔽

 CAN-标识符过滤器-32位列表

 

 CAN-标识符过滤器-32位屏蔽

 

CAN-标识符过滤器-只要遥控帧

 

 CAN-中断式接收

#include "stm32f10x.h"                  // Device headerCanRxMsg MyCAN_RxMsg;
uint8_t MyCAN_RxFlag;void MyCAN_Init(void)
{RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);RCC_APB1PeriphClockCmd(RCC_APB1Periph_CAN1, ENABLE);GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure);CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);//开始中断配置CAN_ITConfig(CAN1, CAN_IT_FMP0, ENABLE);//打开外设中断信号的输出,CAN_IT_FMP0是
fif0有报文时触发中断NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;//配置nvicNVIC_InitStructure.NVIC_IRQChannel = USB_LP_CAN1_RX0_IRQn;//USB_HP_CAN1_TX_IRQn       发送中断    // USB_LP_CAN1_RX0_IRQn       接收fifo0中断// CAN1_RX1_IRQn                 接收fif1中断// CAN1_SCE_IRQn               状态改变和错误中断对比其他CAN中断标志//需手动清除的标志://例如 CAN_IT_TME(发送邮箱空)、CAN_IT_FF0(FIFO 0 满)等,需调用                 CAN_ClearITPendingBit()。//无需手动清除的标志://CAN_IT_FMP0 和 CAN_IT_FMP1(FIFO 待处理消息)由硬件自动管理。NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStructure);//收到报文后就会跳转到中断函数CAN_InitTypeDef CAN_InitStructure;CAN_InitStructure.CAN_Mode = CAN_Mode_LoopBack;CAN_InitStructure.CAN_Prescaler = 48;		//波特率 = 36M / 48 / (1 + 2 + 3) = 125KCAN_InitStructure.CAN_BS1 = CAN_BS1_2tq;CAN_InitStructure.CAN_BS2 = CAN_BS2_3tq;CAN_InitStructure.CAN_SJW = CAN_SJW_2tq;CAN_InitStructure.CAN_NART = DISABLE;CAN_InitStructure.CAN_TXFP = DISABLE;CAN_InitStructure.CAN_RFLM = DISABLE;CAN_InitStructure.CAN_AWUM = DISABLE;CAN_InitStructure.CAN_TTCM = DISABLE;CAN_InitStructure.CAN_ABOM = DISABLE;CAN_Init(CAN1, &CAN_InitStructure);CAN_FilterInitTypeDef CAN_FilterInitStructure;CAN_FilterInitStructure.CAN_FilterNumber = 0;CAN_FilterInitStructure.CAN_FilterIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdHigh = 0x0000;CAN_FilterInitStructure.CAN_FilterMaskIdLow = 0x0000;CAN_FilterInitStructure.CAN_FilterScale = CAN_FilterScale_32bit;CAN_FilterInitStructure.CAN_FilterMode = CAN_FilterMode_IdMask;CAN_FilterInitStructure.CAN_FilterFIFOAssignment = CAN_Filter_FIFO0;CAN_FilterInitStructure.CAN_FilterActivation = ENABLE;CAN_FilterInit(&CAN_FilterInitStructure);
}void MyCAN_Transmit(CanTxMsg *TxMessage)
{uint8_t TransmitMailbox = CAN_Transmit(CAN1, TxMessage);uint32_t Timeout = 0;while (CAN_TransmitStatus(CAN1, TransmitMailbox) != CAN_TxStatus_Ok){Timeout ++;if (Timeout > 100000){break;}}
}void USB_LP_CAN1_RX0_IRQHandler(void)
{if (CAN_GetITStatus(CAN1, CAN_IT_FMP0) == SET)//注意这里有2bit(因为要最大存3条数据呀),即0,1,2,3然后就是,大于0就表示有数据了{CAN_Receive(CAN1, CAN_FIFO0, &MyCAN_RxMsg);//接收到了就存到MyCAN_RxMsg结构变量里MyCAN_RxFlag = 1;}//不用再调用清零标志位CAN_cleanITPendingBit,因为CAN_Receive最后会自动释放邮箱,然后还
有FMP0是2bit,特殊不能直接清零
}

TConfig函数配置的就是IE寄存器,TME发送邮箱空,FMP0:FIFO 0 中有报文,FF0:FIFO 0满

FOV0:FIFO 0溢出,下面以1结尾的是与FIFO相关的

这里用FIFO 0接收数据,当接收FIFO 0里有报文时,触发中断,所有选择FMP0这个参数
中断通道是指右边四个箭头,RX0:用接收FIFO 0中断

当FIFO 0收到一个数据后,进中断,此数据存入MyCAN_RxMsg这个全局变量里,然后MyCAN_RxFlag置1;告诉主循环收到数据了,再根据判断,判断刚刚存入数据的MyCAN_RxMsg
 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Key.h"
#include "MyCAN.h"uint8_t KeyNum;CanTxMsg TxMsgArray[] = {
/*   StdId     ExtId         IDE             RTR        DLC         Data[8]          */{0x555, 0x00000000, CAN_Id_Standard, CAN_RTR_Data,   4, {0x11, 0x22, 0x33, 0x44}},{0x000, 0x12345678, CAN_Id_Extended, CAN_RTR_Data,   4, {0xAA, 0xBB, 0xCC, 0xDD}},{0x666, 0x00000000, CAN_Id_Standard, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},{0x000, 0x0789ABCD, CAN_Id_Extended, CAN_RTR_Remote, 0, {0x00, 0x00, 0x00, 0x00}},
};uint8_t pTxMsgArray = 0;int main(void)
{OLED_Init();Key_Init();MyCAN_Init();OLED_ShowString(1, 1, " Rx :");OLED_ShowString(2, 1, "RxID:");OLED_ShowString(3, 1, "Leng:");OLED_ShowString(4, 1, "Data:");while (1){KeyNum = Key_GetNum();if (KeyNum == 1){MyCAN_Transmit(&TxMsgArray[pTxMsgArray]);pTxMsgArray ++;if (pTxMsgArray >= sizeof(TxMsgArray) / sizeof(CanTxMsg)){pTxMsgArray = 0;}}if (MyCAN_RxFlag == 1){MyCAN_RxFlag = 0;if (MyCAN_RxMsg.IDE == CAN_Id_Standard){OLED_ShowString(1, 6, "Std");OLED_ShowHexNum(2, 6, MyCAN_RxMsg.StdId, 8);}else if (MyCAN_RxMsg.IDE == CAN_Id_Extended){OLED_ShowString(1, 6, "Ext");OLED_ShowHexNum(2, 6, MyCAN_RxMsg.ExtId, 8);}if (MyCAN_RxMsg.RTR == CAN_RTR_Data){OLED_ShowString(1, 10, "Data  ");OLED_ShowHexNum(3, 6, MyCAN_RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, MyCAN_RxMsg.Data[0], 2);OLED_ShowHexNum(4, 9, MyCAN_RxMsg.Data[1], 2);OLED_ShowHexNum(4, 12, MyCAN_RxMsg.Data[2], 2);OLED_ShowHexNum(4, 15, MyCAN_RxMsg.Data[3], 2);}else if (MyCAN_RxMsg.RTR == CAN_RTR_Remote){OLED_ShowString(1, 10, "Remote");OLED_ShowHexNum(3, 6, MyCAN_RxMsg.DLC, 1);OLED_ShowHexNum(4, 6, 0x00, 2);OLED_ShowHexNum(4, 9, 0x00, 2);OLED_ShowHexNum(4, 12, 0x00, 2);OLED_ShowHexNum(4, 15, 0x00, 2);}}}
}

CAN-数据传输策略

定时传输,发送方定时发,接收方直接接收(也可以过滤等等操作)

触发发送,发送放内部达到了条件就发送,适合一些警报和通知信息,我们之前演示的按键按下发送代 码就是这类

请求发送,1.接受放接收请求数据,先发送一个遥控帧,如果发送方有这个数据帧,那么就发给他,当然 也可以用数据帧请求数据帧,这更加有优势

                2. 多请求问题,多个节点同时请求一个数据,不能进行仲裁,尽量避免

定时传输 

发送

uint8_t TimingFlag;
CanTxMsg TxMsg_Timing = {.StdId = 0x100,.ExtId = 0x00000000,.IDE = CAN_Id_Standard,.RTR = CAN_RTR_Data,.DLC = 4,.Data = {0x11, 0x22, 0x33, 0x44}
};
/*定时发送*/if (TimingFlag == 1){TimingFlag = 0;TxMsg_Timing.Data[0] ++;TxMsg_Timing.Data[1] ++;TxMsg_Timing.Data[2] ++;TxMsg_Timing.Data[3] ++;MyCAN_Transmit(&TxMsg_Timing);OLED_ShowHexNum(2, 5, TxMsg_Timing.Data[0], 2);OLED_ShowHexNum(2, 8, TxMsg_Timing.Data[1], 2);OLED_ShowHexNum(2, 11, TxMsg_Timing.Data[2], 2);OLED_ShowHexNum(2, 14, TxMsg_Timing.Data[3], 2);//这里没有用delay延时,避免阻塞,用定时器}

定时器 

void Timer_Init(void)
{RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);接收TIM_InternalClockConfig(TIM2);TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;TIM_TimeBaseInitStructure.TIM_Period = 1000 - 1;TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);TIM_ClearFlag(TIM2, TIM_FLAG_Update);TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);NVIC_InitTypeDef NVIC_InitStructure;NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;NVIC_Init(&NVIC_InitStructure);TIM_Cmd(TIM2, ENABLE);
}
void TIM2_IRQHandler(void)
{//每100ms就自动执行定时操作if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET){TimingFlag = 1;
//不要在中断函数里调用CAN的发送函数还有oled的函数,因为会造成资源冲突,中断是快进快出TIM_ClearITPendingBit(TIM2, TIM_IT_Update);}
}

接收

/*接收部分*/if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxMsg);if (RxMsg.RTR == CAN_RTR_Data){/*收到定时数据帧*/if (RxMsg.StdId == 0x100 && RxMsg.IDE == CAN_Id_Standard)//这个地方都用StdId == 0x100了,不就说明它已经是标准帧了吗,//为什么后面还要判断是不是标准帧 //之前的代码讲过,stdid和extid可以都有值的,//真正决定标准格式还是扩展格式还是得靠IDE//扩展帧的std ID也可能是0x100{OLED_ShowHexNum(2, 5, RxMsg.Data[0], 2);OLED_ShowHexNum(2, 8, RxMsg.Data[1], 2); }

触发传输

这个按键触发,可以改成检测到危险信号,触发发出报警帧,检测事件完成,触发发出通知帧

发送

uint8_t TriggerFlag;
CanTxMsg TxMsg_Trigger = {//触发发送结构体.StdId = 0x200,.ExtId = 0x00000000,.IDE = CAN_Id_Standard,.RTR = CAN_RTR_Data,.DLC = 4,.Data = {0x11, 0x22, 0x33, 0x44}
};
/*触发发送*/KeyNum = Key_GetNum();if (KeyNum == 1){TriggerFlag = 1;}if (TriggerFlag == 1){TriggerFlag = 0;TxMsg_Trigger.Data[0] ++;TxMsg_Trigger.Data[1] ++;TxMsg_Trigger.Data[2] ++;TxMsg_Trigger.Data[3] ++;MyCAN_Transmit(&TxMsg_Trigger);OLED_ShowHexNum(3, 5, TxMsg_Trigger.Data[0], 2);OLED_ShowHexNum(3, 8, TxMsg_Trigger.Data[1], 2);OLED_ShowHexNum(3, 11, TxMsg_Trigger.Data[2], 2);OLED_ShowHexNum(3, 14, TxMsg_Trigger.Data[3], 2);}

接收方

/*收到触发数据帧*/if (RxMsg.StdId == 0x200 && RxMsg.IDE == CAN_Id_Standard){OLED_ShowHexNum(3, 5, RxMsg.Data[0], 2);OLED_ShowHexNum(3, 8, RxMsg.Data[1], 2);OLED_ShowHexNum(3, 11, RxMsg.Data[2], 2);OLED_ShowHexNum(3, 14, RxMsg.Data[3], 2);}

请求传输

第一步:接收指定为请求的遥控帧或数据帧,然后置请求标志位

第二步:根据请求标志位,发送对应的数据帧

发送方,

uint8_t RequestFlag;
CanTxMsg TxMsg_Request = {//请求传输结构体.StdId = 0x300,.ExtId = 0x00000000,.IDE = CAN_Id_Standard,.RTR = CAN_RTR_Data,.DLC = 4,.Data = {0x11, 0x22, 0x33, 0x44}
};
CanRxMsg RxMsg;
/*请求发送*/if (MyCAN_ReceiveFlag())//如果不为0,说明收到帧了{MyCAN_Receive(&RxMsg);if (RxMsg.IDE == CAN_Id_Standard &&RxMsg.RTR == CAN_RTR_Remote &&RxMsg.StdId == 0x300)//请求收到标准id300的遥控帧,回复一个他的数据帧,{RequestFlag = 1;//如果三个条件均为真,则收到0x300标准遥控帧帧了,将标志位置1}if (RxMsg.IDE == CAN_Id_Standard &&RxMsg.RTR == CAN_RTR_Data &&RxMsg.StdId == 0x3FF)//收到数据帧也可以的啊{//还可以把RXMsg.Data拿出来作为请求参数,实现不同的子请求RequestFlag = 1;}}//第二步if (RequestFlag == 1){RequestFlag = 0;TxMsg_Request.Data[0] ++;TxMsg_Request.Data[1] ++;接收方TxMsg_Request.Data[2] ++;TxMsg_Request.Data[3] ++;MyCAN_Transmit(&TxMsg_Request);OLED_ShowHexNum(4, 5, TxMsg_Request.Data[0], 2);OLED_ShowHexNum(4, 8, TxMsg_Request.Data[1], 2);OLED_ShowHexNum(4, 11, TxMsg_Request.Data[2], 2);OLED_ShowHexNum(4, 14, TxMsg_Request.Data[3], 2);}}

接收

第一步,发出请求帧(比如按键按下),发出一个请求帧,

第二步,接收请求的数据,并显示

CanTxMsg TxMsg_Request_Remote = {.StdId = 0x300,.ExtId = 0x00000000,.IDE = CAN_Id_Standard,.RTR = CAN_RTR_Remote,.DLC = 0,.Data = {0x00}
};
CanTxMsg TxMsg_Request_Data = {.StdId = 0x3FF,.ExtId = 0x00000000,.IDE = CAN_Id_Standard,.RTR = CAN_RTR_Data,.DLC = 0,.Data = {0x00}
};
CanRxMsg RxMsg;
main:
while (1){/*请求部分*/KeyNum = Key_GetNum();if (KeyNum == 1){MyCAN_Transmit(&TxMsg_Request_Remote);//按键1按下就是用遥控帧进行请求}if (KeyNum == 2){MyCAN_Transmit(&TxMsg_Request_Data);//按键2按下就是用数据帧进行请求}/*接收部分*/if (MyCAN_ReceiveFlag()){MyCAN_Receive(&RxMsg);/*收到请求数据帧*/if (RxMsg.StdId == 0x300 && RxMsg.IDE == CAN_Id_Standard){OLED_ShowHexNum(4, 5, RxMsg.Data[0], 2);OLED_ShowHexNum(4, 8, RxMsg.Data[1], 2);OLED_ShowHexNum(4, 11, RxMsg.Data[2], 2);OLED_ShowHexNum(4, 14, RxMsg.Data[3], 2);}}

首先接收方按键按下,比如按下按键1,则接收方发出一个0x300的遥控帧MyCAN_Transmit,然后这个数据会到发送方这里来,发送方接收帧MyCAN_Receive,判断收到0x300的遥控帧了,就置RequestFlag = 1,然后继续运行代码,发送方发出TxMsg_Request也就是0x300的数据帧,最后返回接收方,接收方执行接收部分代码,判断,收到0x300的数据帧后,显示出来

感谢江科大up主 

http://www.xdnf.cn/news/4558.html

相关文章:

  • Linux网络编程day6 下午去健身
  • MATLAB导出和导入Excel文件表格数据并处理
  • 大模型范式转移:解码深度学习新纪元
  • 【Day 21】HarmonyOS实战:从智慧医疗到工业物联网
  • 【FreeRTOS-消息队列】
  • PyQt5 实现自定义滑块,效果还不错
  • grpc到底是啥! ! !!
  • shell操作文件上传
  • 第3章 模拟法
  • SDC命令详解:使用get_ports命令进行查询
  • 浅谈广告投放从业者底层思维逻辑
  • C语言 指针(8)
  • 第七章 模板制作工具
  • ubuntu 挂载硬盘
  • 当“信任”遇上“安全”:如何用Curtain Logtrace记录文件操作活动 守护团队与数据的双重底线?
  • 2398.预算内的最多机器人数目 滑动窗口+单调队列
  • springboot集成langchain4j记忆对话
  • 通道注意力-senet
  • HDMI布局布线
  • Loly: 1靶场渗透
  • 大模型 Function Calling 学习路线图
  • Solana批量转账教程:提高代币持有地址和生态用户空投代币
  • 缓存菜品-04.功能测试
  • C++ 静态成员
  • 大模型系列(四)--- GPT2: Language Models are Unsupervised Multitask Learners​
  • Java 多线程编程:从基础到实战!
  • Ceph集群OSD运维手册:基础操作与节点扩缩容实战
  • MSTP 实验拓扑配置(ENSP)
  • 自动化创业机器人:现状、挑战与Y Combinator的启示
  • hadoop中的序列化和反序列化(3)