《ESP32-P4开发指南—V1.0》第三十六章 TWAI实验

第三十六章 TWAI实验


       本章,我们将介绍如何使用ESP32-P4自带的TWAI控制器来实现TWAI的收发功能,并将结果显示在LCD屏幕上。

       本章分为如下几个小节:

       36.1 TWAI总线协议介绍

       36.2 硬件设计

       36.3 程序设计

       36.4 下载验证


        36.1 TWAI总线协议介绍

       有人说乐鑫的芯片没有CAN总线控制器,只不过它被叫做双线汽车接口,缩写为TWAI,详称为“two_wire automotive interface”。由于ESP32的CAN控制器在功能上相比STM32稍微逊色一点,同时资料也少一点,所以不被大家所熟知。


       36.1.1 TWAI简介

       双线汽车接口(TWAI)是一种适用于汽车和工业应用的实时串行通信协议,兼容ISO11898-1经典帧(CAN2.0),因此可以支持标准帧格式(11位ID)和扩展帧格式(29位ID)。但是不兼容ISO11898-1 FD格式帧,并会将这些帧解析为错误。

       ESP32-P4包含3个TWAI控制器,分别是TWAI0、TWAI1和TWAI2。 ESP32-P4的TWAI控制器具有以下特性:

       ① 兼容ISO 11898-1协议(CAN规范2.0)

       ② 支持标准帧格式(11位ID)和扩展帧格式(29位ID)

       ③ 比特率从1Kbit/s到1Mbit/s

       ④ 多种操作模式:工作模式、只听模式和自检模式(传输无需确认)

       ⑤ 64字节接收FIFO

       ⑥ 特殊传输:单次传输(错误时不会自动重传)、自发自收(同时发送和接收消息)

       ⑦ 数据接收过滤器(支持单过滤器和双过滤器模式)

       ⑧ 错误检测与处理:错误计数、可配置的错误中断阈值、错误代码记录和仲裁丢失记录

       双线汽车接口通过GPIO交换矩阵可配置使用任意GPIO管脚,非常灵活。

       TWAI通信拓扑图如下图所示:


图36.1.1 TWAI通信拓扑结构图


       从上图可知,TWAI总线呈现的是一个闭环结构,总线是由两根线TWAI_High和TWAI_Low组成,且在总线两端各串联了120Ω的电阻(用于阻抗匹配,减少回波反射),同时总线上可以挂载多个节点。每个节点都有TWAI收发器以及TWAI控制器,TWAI控制器通常是MCU的外设,集成在芯片内部,而TWAI收发器则是需要外加芯片转换电路。

       TWAI类似RS485也是通过差分信号传输数据。根据TWAI总线上两根线的电位差来判断总线电平。总线电平分为显性电平和隐性电平,二者必居其一。这是属于物理层特征,ISO11898物理层特性如图36.1.1.2所示:


图36.1.1.2 ISO11898物理层特性


       从该特性可以看出,显性电平对应逻辑0,CAN_H和CAN_L之差为2 V左右。而隐性电平对应逻辑1,CAN_H和CAN_L之差为0V。在总线上显性电平具有优先权,只要有一个单元输出显性电平,总线上即为显性电平。而隐形电平则具有包容的意味,只有所有的单元都输出隐性电平,总线上才为隐性电平(显性电平比隐性电平更强)。


       36.1.2 TWAI总线协议特点

       TWAI是一种多主机、多播、异步、串行通信协议,该协议还支持错误检测和通报,并具有内置报文优先级。

       多主机:总线上的任何节点都可以发起报文传输。

       多播:节点传输报文时,总线上的所有节点都会接收该报文(即广播),确保所有节点数据一致。但通过接收过滤,某些节点可以选择性地接收报文(多播)。

       异步:总线不包含时钟信号。总线上的所有节点以相同的位速率运行,并使用在总线上传输位的边沿进行同步。

       错误检测和通报:每个节点不断监听总线。节点检测到错误时,通过传输错误帧通报检测到的错误。其他节点会接收错误帧,并传输自己的错误帧作为回应,这样一来检测的错误即可传播到总线上的所有节点。

       故障限制:若一组错误计数依据规定增加/减少时,各节点将维护该组错误计数。当错误计数超过一定阈值时,对应节点将自动关闭以退出网络。

       报文优先级:每个报文包含唯一的ID字段。如果两个或多个节点尝试同时传输,ID小的节点将获得总线的控制权,而其他节点将自动转为接收器,确保无论何时最多只有一个发 射 器。

       发送器与接收器:无论何时,任何TWAI节点都可作为发送器和接收器。产生报文的节点为发送器。所有非发送器的节点都将作为接收器。


       36.1.3 TWAI总线协议

       TWAI节点使用报文发送数据,并在检测到总线上存在错误时向其他节点发送错误信号。报文分为不同的帧类型,某些帧类型将具有不同的帧格式。

       TWAI总线协议是通过以下5种类型的帧进行的:

       l 数据帧

       l 遥控帧

       l 错误帧

       l 过载帧

       l 间隔帧

       另外,数据帧和遥控帧有标准格式和扩展格式两种格式。标准格式有11个位的标识符(ID),扩展格式有29个位的ID。各种帧的用途如表36.1.3.1所示:


表36.1.3.1 TWAI协议各种帧及其用途


       由于篇幅所限,我们这里仅对数据帧进行详细介绍,数据帧一般由7个段构成,即:

       帧起始。表示数据帧开始的段。

       仲裁段。表示该帧优先级的段。

       控制段。表示数据的字节数及保留位的段。

       数据段。数据的内容,一帧可发送0~8个字节的数据。

       CRC段。检查帧的传输错误的段。

       ACK段。表示确认正常接收的段。

       帧结束。表示数据帧结束的段。

       数据帧的构成如图36.1.2.1所示:


图36.1.3.1数据帧的构成


       图中D表示显性电平,R表示隐形电平(下同)。

       帧起始,这个比较简单,标准帧和扩展帧都是由1个位的显性电平表示帧起始。

       仲裁段,表示数据优先级的段,标准帧和扩展帧格式在本段有所区别,如图36.1.3.2所示:


图36.1.3.2 数据帧仲裁段构成


       标准格式的ID有11个位。禁止高7位都为隐性(禁止设定:ID=1111111XXXX)。扩展格式的ID有29个位。基本ID从ID28到ID18,扩展ID由ID17到ID0表示。基本ID和标准格式的ID相同。禁止高7位都为隐性(禁止设定:基本ID=1111111XXXX)。

       其中RTR位用于标识是否是远程帧(0,数据帧;1,远程帧),IDE位为标识符选择位(0,使用标准标识符;1,使用扩展标识符),SRR位为代替远程请求位,为隐性位,它代替了标准帧中的RTR位。

       控制段,由6个位构成,表示数据段的字节数。标准帧和扩展帧的控制段稍有不同,如图36.1.3.3所示:


图36.1.3.3 数据帧控制段构成


       上图中,r0和r1为保留位,必须全部以显性电平发送,但是接收端可以接收显性、隐性及任意组合的电平。DLC段为数据长度表示段,高位在前,DLC段有效值为0~8,但是接收方接收到9~15的时候并不认为是错误。

       数据段,该段可包含0~8个字节的数据。从最高位(MSB)开始输出,标准帧和扩展帧在这个段的定义都是一样的。如图36.1.3.4所示:


图36.1.3.4 数据帧数据段构成


       CRC段,该段用于检查帧传输错误。由15个位的CRC顺序和1个位的CRC界定符(用于分隔的位)组成,标准帧和扩展帧在这个段的格式也是相同的。如图36.1.3.5所示:


图36.1.3.5 数据帧CRC段构成


       此段CRC的值计算范围包括:帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算CRC值并进行比较,不一致时会通报错误。

       ACK段,此段用来确认是否正常接收。由ACK槽(ACKSlot)和ACK界定符2个位组成。标准帧和扩展帧在这个段的格式也是相同的。如图36.1.3.6所示:


图36.1.3.6 数据帧CRC段构成


       发送单元的ACK,发送2个位的隐性位,而接收到正确消息的单元在ACK槽(ACKSlot)发送显性位,通知发送单元正常接收结束,这个过程叫发送ACK/返回ACK。发送ACK的是在既不处于总线关闭态也不处于休眠态的所有接收单元中,接收到正常消息的单元(发送单元不发送ACK)。所谓正常消息是指不含填充错误、格式错误、CRC错误的消息。

       帧结束,这个段也比较简单,标准帧和扩展帧在这个段格式一样,由7个位的隐性位组成。至此,数据帧的7个段就介绍完了,其他帧的介绍,请大家参考光盘的《CAN入门书.pdf》相关章节。

       接下来,我们再来看看TWAI的位时序。由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位可分为3段。

       l 同步段(SS)

       l 相位缓冲段1(PBS1)

       l 相位缓冲段2(PBS2)

       这些段又由可称为Time Quanta(以下称为Tq)的最小时间单位构成。

       1位分为3个段,每个段又由若干个Tq构成,这称为位时序。

       1位由多少个Tq构成、每个段又由多少个Tq构成等,可以任意设定位时序。通过设定位时序,多个单元可同时采样,也可任意设定采样点。各段的作用和Tq数如表36.1.3.2所示:


表36.1.3.2 一个位各段及其作用


       1个位的构成如图36.1.3.7所示:


图36.1.2.7 一个位的构成


       上图的采样点,是指读取总线电平,并将读到的电平作为位值的点。位置在PBS1结束处。根据这个位时序,我们就可以计算TWAI通信的波特率了。具体计算方法,我们等下再介绍。

       由于时钟偏移和抖动,同一总线上节点的位时序可能会脱离相位段。因此,位边沿可能会偏移到同步段的前后。针对位边沿偏移的问题,TWAI提供了多种同步方式。设位边沿偏移的Tq数量为相位错误“e”该值与SS相关。

       主动相位错误(e>0):位边沿位于同步段之后采样点之前(边沿向后偏移)。

       被动相位错误(e<0):位边沿位于前个位的采样点之后同步段之前(边沿向前偏移)。

       为了解决相位错误,可进行两种同步方式是硬同步和再同步。硬同步和再同步遵守以下规则:① 单个位时序中仅可发生一次同步;② 同步仅可发生在隐形位到显性位的边沿上。

       硬同步:总线空闲期间,硬同步发生在隐性位到显性位的变化边沿上(如总线空闲后的第一个SOF位)。此时,所有节点都将重启其内部位时序,从而使该变化边沿位于重启位时序的同步段内。

       再同步:非总线空闲期间,再同步发生在隐性位到显性位的变化边沿上。如果边沿上有主动相位错误(e>0),则PBS1长度将增加。如果边沿上有被动相位错误(e<0),PBS2长度将减小。

       PBS1/PBS2具体增加和减小的时间定额取决于相位错误的绝对值,同时也受可配置的同步跳宽(SJW)数值限制。

       当相位错误的绝对值小于等于SJW数值时,PBS1/PBS2将增加/减小e个时间定额。该过程与硬同步具有相同效果。

       当相位错误的绝对值大于SJW数值时,PBS1/PBS2将增加/减小与SJW相同数值的时间定额。这意味着,在完全解决相位错误之前,可能需要多个同步位。

       前面提到的TWAI协议具有仲裁功能,下面我们来看看是如何实现的。

       在总线空闲态,最 先开始发送消息的单元获得发送权。当多个单元同时开始发送时,各发送单元从仲裁段的第一位开始进行仲裁。连续输出显性电平最多的单元可继续发送。实现过程,如图36.1.3.8所示:


图36.1.3.8 TWAI总线仲裁过程


       上图中,单元1和单元2同时开始向总线发送数据,开始部分他们的数据格式是一样的,故无法区分优先级,直到T时刻,单元1输出隐性电平,而单元2输出显性电平,此时单元1仲裁失利,立刻转入接收状态工作,不再与单元2竞争,而单元2则顺利获得总线使用权,继续发送自己的数据。这就实现了仲裁,让连续发送显性电平多的单元获得总线使用权。

       简单来说,有以下规律:① ID值最小的帧将总是获得仲裁;② 如果ID和格式相同,由于数据帧的RTR位为显性位,数据帧将优先于远程帧;③ 如果ID的前11位相同,由于扩展帧的SRR位是隐性,因而标准格式帧将总优先于扩展格式帧。


       36.1.4 TWAI控制器结构

       TWAI控制器的结构框图如下图所示。


图36.1.4.1 ESP32-P4的TWAI结构框图


       TWAI控制器主要由寄存器模块(Registers)、位流处理器(Bit Stream Processing)、错误管理逻辑(Error Management Logic)、位时序逻辑(Bit Timing Logic)、接收过滤器(Acceptance Filter)和接收FIFO (Receive FIFO)。

       寄存器模块

       TWAI控制器包括比较多的寄存器,比如:

       ① 配置寄存器,用于存储TWAI控制器的各配置项,如位速率、操作模式、接收过滤器等。

       ② 指令寄存器,CPU通过该寄存器驱动TWAI控制器执行任务,如发送报文或清除接收缓冲器。

       ③ 中断&状态寄存器,用于显示TWAI控制器中发生的事件。

       ④ 错误管理寄存器,包含错误计数和捕捉寄存器。错误计数寄存器存放的是发送错误或接收错误的累积次数,而捕捉寄存器负责记录相关信息,比如何处检测出总线错误,丢失仲裁等。

       ⑤ 发送缓冲寄存器,大小为13字节,用于存储TWAI的待发送报文。

       ⑥ 接收缓冲寄存器,大小为13字节,用于存储单个报文。接收缓冲器是进入接收FIFO的窗口,接收FIFO中的第一个报文将被映射到接收缓冲器中。

       位流处理器

       位流处理器BSP模块负责对发送缓冲器的数据进行帧处理(如位填充和附加CRC域)并为位时序逻辑模块BTL生成位流。同时,BSP模块还负责处理从BTL模块中接收的位流(如去填充和验证CRC),并将处理报文置于接收FIFO。BSP还负责检测TWAI总线上的错误并将此类错误报告给错误管理逻辑EML。

       错误管理逻辑

       错误管理逻辑EML模块负责更新TEC和REC数值,记录错误信息(如错误类型和错误位置),更新控制器的错误状态,确保BSP模块发送正确的错误标志。此外,该模块还负责记录TWAI控制器丢失仲裁时的bit位置。

       位时序逻辑

       位时序逻辑BTL模块负责以预先配置的位速率发送和接收报文。BTL模块还负责同步位时序,确保数据传输的稳定性。位速率由多个可编程的段组成,且用户可设置每个段的Tq长度,来调整传播延迟、控制器处理时间等因素。

       接收过滤器

       接收过滤器是一个可编程的报文过滤单元,允许TWAI控制器根据报文的标识符域接收或拒绝该报文。通过接收滤波器的报文才能被存储到接收FIFO中。用户可配置接收滤波器的模式:单滤波器、双滤波器。

       接收FIFO

       接收FIFO是大小为64字节的缓冲器(位于TWAI控制器内部),负责存储通过接收滤波器的接收报文。接收FIFO中存储的报文大小可以不同。当接收FIFO为满时(或剩余的空间不足以完全存储下一个接收报文),将触发溢出中断,后续的接收报文将丢失,直到接收FIFO中清除出足够的储存空间。接收FIFO中的第一条报文将映射到13字节的接收缓冲器中,直到该报文被清除(通过释放接收缓冲器指令)。清除后,接收缓冲器将继续映射接收FIFO中的下一条报文,接收FIFO中上一条已清除报文的空间将被释放。


       36.1.5 TWAI控制器接收过滤器

       ESP32-P4 TWAI控制器内置硬件接收过滤器,可以过滤特定ID的报文。只有通过过滤的报文才能存储到接收FIFO中。接收过滤器的使用可以一定程度减轻TWAI控制器的运行负荷(如克减少使用接收FIFO和发生接收中断的次数),因为TWAI控制器将只需要操作一小部分过滤后的报文。

       只有当TWAI控制器处于复位模式时,才可以访问接收过滤器的配置寄存器,因为这些配置寄存器和发送/接收缓冲寄存器的地址空间相同。

       接收过滤器的配置寄存器由32bit的Code值(接收码)和32bit的Mask值(接收掩码)组成。Code值将指定一种位排列模式,每条过滤报文中的位都必须匹配该模式,才能使该报文通过过滤。Mask值可屏蔽Code值中的某些位(将屏蔽位设置为“不相关”的位)。为了使报文通过过滤,每条过滤报文ID都必须匹配Code值所设模式或被Mask值屏蔽。过滤过程如下所示。


图36.1.5.1 接收过滤器过滤过程


       TWAI控制器的接收过滤器允许32bit的Code值和Mask值定义单个过滤器(单过滤器模式)或两个过滤器(双过滤器模式)。接收过滤器如何解析32bit的Code值和Mask值,取决于过滤模式以及接收报文的格式。

       单过滤模式使用接收码和接收掩码定义了一个过滤器,支持过滤标准帧的前两个数据字节,或扩展帧的29位ID的全部内容,如下图所示。


图36.1.5.2 单过滤模式


       在单过滤器模式下解析32位接收代码和掩码的方式。黄色代表的是标准帧格式,而蓝色代表的是扩展帧格式。

       双过滤器模式使用接收代码和掩码定义两个单独过滤器,支持接收更多ID,但不支持过滤扩展ID的全部29位,如下图所示。


图36.1.5.3 双过滤模式


       在双过滤器模式下解析32位接收代码和掩码的方式。黄色代表的是标准帧格式,而蓝色代表的是扩展帧格式。


       36.1.6 TWAI控制器模式

       ESP32-P4 TWAI控制器有两种工作模式:复位模式和操作模式。

       复位模式

       要修改TWAI控制器的各种配置寄存器,需进入复位模式。进入复位模式时,TWAI控制器彻底与TWAI总线断开连接。复位模式下,TWAI控制器将无法发送任何报文(包括错误信号)。任何正在进行的报文传输将立即被终止。同样的,TWAI控制器在该模式下也将无法接收任何报文。

       操作模式

       进入操作模式后,TWAI控制器与总线相连,并且写保护各配置寄存器,以确保控制器的配置在运行期间保持一致。操作模式下,TWAI控制器可以发送和接收报文(包括错误信号),但具体取决于TWAI控制器配置于哪种运行子模式。TWAI控制器支持以下三种子模式:

       正常模式:TWAI控制器可以发送和接收包含错误信号在内的报文(如错误帧和过载帧)。发送报文时需要来自另一个节点的应答。

       自测模式(无应答模式):与正常模式类似,但在该模式下,TWAI控制器发送报文时,即使在CRC域之后没有接收到应答信号,也不会产生应答错误。通过在TWAI控制器自测时使用该模式。

       只听模式:TWAI控制器可以接收报文,但在TWAI总线上保持完全被动。因此,TWAI控制器将无法发送任何报文、应答或错误信号。错误计数将保持冻结状态。该模式用于TWAI总线监控。

       注意:退出复位模式后(如进入操作模式时),TWAI控制器需要等待11个连续隐性位出现,才能完全连接上TWAI总线,发送或接收报文。


       36.1.7 TWAI波特率

       TWAI总线上的各个节点只要约定好1个Tq的时间长度以及每一个数据位占据多少个Tq,就可以确认TWAI通信的波特率。

       假如1Tq = 0.1us,而每个数据位由20个Tq组成,则传输一位数据需要时间T1bit = 2us,从而得到每秒钟可以传输的数据位个数:

1 x 10 6/ 2=  500k (bps)

       这个每秒可传输的数据位个数即为通信的波特率。

       在ESP32-P4的TWAI控制器中,Tq时钟是由XTAL时钟分配得到的,XTAL时钟为40MHz。可通过以下公式计算。

       其中,t Tq 为时间定额的时钟周期,tCLK为XTAL时钟周期,而BRP为预分频值。

       然后,得到一个数据位的时间即(PBS1+PBS2+SS)* t Tq ,最终可推导出如下公式:

Baud = 1 / ((PBS1 + PBS2 + SS) * 2 * tCLK * (BRP + 1))


        36.2 硬件设计


       36.2.1 例程功能

       通过KEY1按键选择正常模式;通过KEY2按键选择不应答模式。通过KEY0控制数据发送,接着查询是否有数据接收到。假如接收到数据,就将接收到的数据显示在LCD屏上。如果 是无应答模式,我们不需要2个开发板。如果是正常模式,我们就需要两个ESP32-P4开发板,并将他们的TWAI接口连接起来,然后用一个开发板发送数据,另外一个开发板将接收到的数据显示在LCD屏上。LED0用来指示程序正在运行。


       36.2.2 硬件资源


       1)LED灯

              LED 0 - IO51


       2)RGBLCD / MIPILCD(引脚太多,不罗列出来)


       3)TWAI电路

              CAN_TX - IO27(U1RXD)(跳线帽连接)

              CAN_RX - IO26(U1TXD)(跳线帽连接)


       36.2.3 原理图

       CAN电路相关原理图,如下图所示。


图36.2.3.1 CAN电路原理图


       从上图可以看出:开发板的TWAI电路通过P5的设置,连接到TJA1050/SIT1050T收发芯片,然后通过接线端子(CAN)同外部的CAN总线连接。图中可以看出,在ESP32-P4开发板上是带有120Ω的终端电阻的,如果我们的开发板不是作为CAN的终端的话,需要把这个电阻去掉,以免影响通信。另外,需要注意:TWAI、485公用IO26和IO27,他们不能同时使用。

       这里还需要注意,我们要设置好开发板上的P5排针的连接,通过跳线帽将IO26和IO27分别连接到CAN_RX和CAN_TX上面,如图36.2.3.2所示。


图36.2.3.2 TWAI实验需要跳线连接的位置


       最后,我们用2根导线将两个开发板CAN端子的CAN_L和CAN_L,CAN_H和CAN_H连接起来。这里注意不要接反了(CAN_L接CAN_H),接反了会导致通信异常!!!


        36.3 程序设计


       36.3.1 TWAI的IDF驱动

       使用TWAI相关功能函数,必须先导入以下头文件:

#include "driver/twai.h"

       接下来,作者将介绍一些常用的函数,这些函数的描述及其作用如下:


       1,创建TWAI函数twai_driver_install

       该函数用于创建TWAI,其函数原型如下:

esp_err_t twai_driver_install(const twai_general_config_t *g_config, 
const twai_timing_config_t *t_config, 
const twai_filter_config_t *f_config);

       函数形参:


表36.3.1.1 twai_driver_install函数形参描述


       函数返回值:

       ESP_OK表示创建成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_NO_MEM表示内存不足。

       ESP_ERR_INVALID_STATE表示驱动已经被安装。

       g_config为指向TWAI通用配置结构体指针。接下来,介绍twai_general_config_t结构体中各个成员,如下代码所示:

typedef struct {
    int controller_id;              /* TWAI控制器ID */
    twai_mode_t mode;               /* TWAI控制器模式 */
    gpio_num_t tx_io;               /* 发送引脚 */
    gpio_num_t rx_io;               /* 接收引脚 */
    gpio_num_t clkout_io;           /* 时钟输出引脚 */
    gpio_num_t bus_off_io;          /* 离线通知引脚 */
    uint32_t tx_queue_len;          /* TX队列可容纳的消息数 */
    uint32_t rx_queue_len;          /* RX队列可容纳的消息数 */
    uint32_t alerts_enabled;        /* 要启动的警报位字段 */
    uint32_t clkout_divider;        /* CLKOUT分频器 */
    int intr_flags;                 /* 中断优先级 */
    struct {
        uint32_t sleep_allow_pd;    /* 允许断电 */
    } general_flags;                /* 通用标志 */
} twai_general_config_t;

       twai_general_config_t结构体用于设置TWAI控制器的基本参数,以下对各成员做简单介绍。

       1)controller_id:

       设置TWAI控制器的ID。ESP32-P4有三个TWAI,当选择0时,才使用这个函数。非零时,需要选择twai_driver_install_v2函数进行创建TWAI。

       2)mode:

       设置TWAI操作模式。三个选项分别是TWAI_MODE_NORMAL、TWAI_MODE_NO_ACK和TWAI_MODE_LISTEN_ONLY,对应着36.1.5小节描述的三个操作模式。

       3)tx_io

       TWAI发送引脚,用于与外部收发器通信。

       4)rx_io

       TWAI接收引脚,用于与外部收发器通信。

       5)clkout_io

       CLKOUT信号线属于TWAI控制器的信号线,是可选的,会输出控制器源时钟的分配时钟。

       6)buf_off_io

       BUS-OFF信号线属于TWAI控制器的信号线,是可选的,在TWAI控制器进入离线状态时为低逻辑电平(0V)。否则为高逻辑电平(3.3V)。

       7)tx_queue_len

       用于设置TX队列可容纳的消息数。若设置为0即禁用TX队列。

       8)rx_queue_len

       用于设置RX队列可容纳的消息数。

       9)alerts_enabled

       用于启用的警报位字段,也就是对哪些标志进行使能。

       10)clkout_divider

       用于设置CLKOUT分频器。可以是1或2到14之间的任何偶数。如果不使用可设置为0。

       11)intr_flags

       用于设置中断优先级。

       12)sleep_allow_pd

       用于设置允许断电。当设置此标志时,驱动程序将在进入低功耗模式前,对TWAI寄存器的内容进行备份。驱动程序退出低功耗模式后,对TWAI寄存器内容进行恢复。

       TWAI的IDF驱动提供了通用配置初始化宏,代码如下:

TWAI_GENERAL_CONFIG_DEFAULT_V2(controller_num, tx_io_num, rx_io_num, op_mode)
TWAI_GENERAL_CONFIG_DEFAULT(tx_io_num, rx_io_num, op_mode)

       第一个宏函数是配置TWAI1或TWAI2,而第二个宏函数是配置TWAI0的。使用宏函数就可以不对其结构体成员赋值,比较方便。但为了方便大家了解其成员,例程中还是采用了对各个成员赋值的方式。

       t_config为指向TWAI时序配置结构体指针。介绍twai_timing_config_t结构体中各个成员,如下代码所示:

typedef struct {
    twai_clock_source_t clk_src;    /* TWAI控制器时钟源 */
    uint32_t quanta_resolution_hz;  /* 时间定额的分辨率 */
    uint32_t brp;                   /* 波特率分频器 */
    uint8_t tseg_1;                 /* 时序段1 */
    uint8_t tseg_2;                 /* 时序段2 */
    uint8_t sjw;                    /* 同步跳变宽度 */
    bool triple_sampling;           /* 三重采样 */
} twai_timing_config_t;

       twai_timing_config_t结构体用于设置位速率,以下对各成员做简单介绍。

       1)clk_src:

       用来设置时钟源。设置为0或使用TWAI_CLK_SRC_DEFAULT,使用默认时钟源。

       2)quanta_resolution_hz:

       用来设置时间定额Tq的分辨率,单位为Hz。这里会跟brp的值由该字段反映。

       3)brp:

       用来设置TWAI控制器的源时钟分频,确定每个时间定额Tq的周期。在ESP32-P4上,brp可以是从2到32768的任何偶数。也可将quanta_resulotion_hz设置为非零值,决定时间定额的分辨率,由驱动程序计算出底层brp值。该方法适用于需要设置不同的时钟源,但希望位速率保持不变的情况。

       4)tseg_1:

       用来设置SEG1即时序段1的时间定额数量。

       5)tseg_2:

       用来设置SEG2即时序段2的时间定额数量。

       6)sjw:

       用来设置单位时间可以为了同步而延长或缩短的最大事件定额数。

       7)triple_sampling

       用来设置三重采样,有利于过滤低/中速总线的尖峰信号。启用三重采样会导致每个位采样3个时间定额,而不是1个,额外的采样点位置位于时序段1尾部。

       注意:brp、tseg1、tseg2和sjw的不同组合可以实现相同位速率。用户应该考虑传播延迟、节点信息处理时间和相位误差等因素,根据总线的物理特性进行调整。

       常见的位速率时序可以使用初始化宏。TWAI的IDF驱动提供了以下的初始化宏。

TWAI_TIMING_CONFIG_25KBITS()     /* 波特率25K */
TWAI_TIMING_CONFIG_50KBITS()     /* 波特率50K */
TWAI_TIMING_CONFIG_100KBITS()    /* 波特率100K */
TWAI_TIMING_CONFIG_125KBITS()    /* 波特率125K */
TWAI_TIMING_CONFIG_250KBITS()    /* 波特率250K */
TWAI_TIMING_CONFIG_500KBITS()    /* 波特率500K */
TWAI_TIMING_CONFIG_800KBITS()    /* 波特率800K */
TWAI_TIMING_CONFIG_1MBITS()      /* 波特率1M */

       f_config为指向TWAI过滤器的配置结构体指针。介绍twai_filter_config_t结构体中各个成员,如下代码所示:

typedef struct {
    uint32_t acceptance_code;      /* 接收码 */
    uint32_t acceptance_mask;      /* 接收掩码 */
    bool single_filter;            /* 单过滤器模式 */
} twai_filter_config_t;

       twai_filter_config_t结构体用于设置接收过滤器,以下对各成员做简单介绍。

       1)acceptance_code:

       用来设置接收码。接收码指定报文的ID、RTR和数据字节必须匹配的位序列,使报文可以由TWAI控制器接收。

       2)acceptance_mask:

       用来设置接收掩码。接收掩码是一个位序列,指定接收码中可以忽略的位,从而实现用单个接收码接收不同ID的报文。

       3)single_filter:

       用来设置使用单过滤器模式还是双过滤器模式。单过滤器模式使用接收代码和掩码定义一个过滤器,支持筛选标准帧的前两个数据字节,或扩展帧的29位ID的全部内容。双过滤器模式使用接收代码和掩码定义两个单独的过滤器,支持接收更多ID,但不支持筛选扩展ID的全部29位。

       TWAI的IDF驱动提供了过滤器的初始化宏,可用TWAI_FILTER_CONFIG_ACCEPT_ALL去替代对其成员赋值,去接收所有ID。


       2,启动TWAI函数twai_start

       该函数用于启动TWAI,允许参与TWAI总线活动,比如发送/接收消息,其函数原型如下:

esp_err_t twai_start(void);

       无函数形参

       函数返回值:

       ESP_OK表示TWAI驱动在运行。

       ESP_ERR_INVALID_STATE表示驱动不在停止状态或没有被驱动安装。

       与twai_start函数对应的便是twai_stop,用于停止TWAI。


       3,发送报文函数twai_transmit

       该函数用于向TWAI总线上发送报文信息,其函数原型如下:

esp_err_t twai_transmit(const twai_message_t *message, 
TickType_t ticks_to_wait);

       函数形参:


表36.3.1.2 twai_transmit函数形参描述


       函数返回值:

       ESP_OK表示发送成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_TIMEOUT表示等待TX队列空间超时。

       ESP_FAIL表示TX队列被禁用,或当前正在发送另外一条报文。

       ESP_ERR_INVALID_STATE表示TWAI驱动不在运行状态,或没有安装。

       ESP_ERR_NOT_SUPPORT表示只听模式不支持传输。

       message为指向报文配置结构体指针。介绍twai_message_t结构体中各成员,如下代码所示:

typedef struct {
    union {
        struct {
            uint32_t extd: 1;            /* 标准帧 / 扩展帧格式(29位ID) */
            uint32_t rtr: 1;             /* 数据帧或远程帧 */
            uint32_t ss: 1;              /* 报文是否单次发送 */
            uint32_t self: 1;            /* 报文是否为自收发 */
            uint32_t dlc_non_comp: 1;    /* 数据长度代码,不能大于8 */
            uint32_t reserved: 27;       /* 保留 */
        };
        uint32_t flags;                  /* 标志 */
    };
    uint32_t identifier;                 /* 报文ID */
    uint8_t data_length_code;            /* 报文携带数据的长度 */
    uint8_t data[TWAI_FRAME_MAX_DLC];    /* 报文携带的数据 */
} twai_message_t;

       twai_message_t结构体用于设置接收过滤器,以下对各成员做简单介绍。

       1)extd:

       用来设置报文是标准帧还是扩展帧格式。若要标准帧,这里要设置为0,若要扩展帧,这里要设置为1。

       2)rtr:

       用来设置报文是数据帧还是远程帧。若是数据帧,这里要设置为0,若是远程帧,这里要设置为1。

       3)ss:

       用来设置报文是否单次发送。若是单次发送,这里要设置为1,若是要错误重发,这里要设置为0。

       4)self:

       用来设置报文是否自收发。若要接收自发的报文,这里设置为1即可。

       5)dlc_non_comp:

       用来设置报文的数据长度代码,ISO11898标准下不能超过8。

       6)identifier:

       用来设置报文的ID。若extd为0即标准帧,这里便是11位ID。若extd为1即扩展帧,这里便是29位ID。

       7)data_length_code:

       用来设置报文携带数据的长度。

       8)data[TWAI_FRAME_MAX_DLC]:

       用来存放报文携带的数据。

       注意:这个函数将待发送的报文在TX队列上排队等待传输。如果没有其他报文排队,报文可立马传输。如果TX队列满了,该函数将会阻塞,直到有空间可用或超时。该函数在只听模式下不可用。


       4,接收报文函数twai_receive

       该函数用于从RX队列中接收报文,其函数原型如下:

esp_err_t twai_receive(twai_message_t *message, TickType_t ticks_to_wait);

       函数形参:


表36.3.1.3 twai_receive函数形参描述


       函数返回值:

       ESP_OK表示接收成功。

       ESP_ERR_INVALID_ARG表示错误参数。

       ESP_ERR_TIMEOUT表示等待超时。

       ESP_ERR_INVALID_STATE表示TWAI驱动没有安装。


       36.3.2 程序流程图


图36.3.2.1 TWAI实验程序流程图


       36.3.3 程序解析

       在27_twai例程中,作者在27_twai \components\BSP路径下新增了1个文件夹TWAI,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。


       1. TWAI驱动代码

       这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。TWAI驱动源码包括两个文件:esp_twai.c和esp_twai.h。

       下面先解析esp_twai.h的程序。对TWAI相关引脚以及报文的ID做了相关宏定义。

#define TWAI_TX_GPIO_PIN        GPIO_NUM_27
#define TWAI_RX_GPIO_PIN        GPIO_NUM_26
 
#define MSG_ID                  0x12

       TWAI的发送引脚用到的是IO26,接收引脚用到的是IO27。

       下面我们再解析twai.c的程序,首先先来看一下初始化函数twai_init,代码如下:

/**
 * @brief      TWAI初始化
 * @param      mode: TWAI控制器操作模式
 * @note      TWAI_MODE_NORMAL:正常模式
 * @note        TWAI_MODE_NO_ACK:无应答模式
 * @retval     ESP_OK:表示初始化成功
 */
esp_err_t twai_init(twai_mode_t mode)
{
    if (tawi_state == 0xFF)
    {
        ESP_ERROR_CHECK(twai_stop());
        ESP_ERROR_CHECK(twai_driver_uninstall());
        tawi_state = 0x00;
    }
 
    tawi_mode = mode;
    /* TWAI接口基本配置 */
    twai_general_config_t twai_config = {
        .controller_id  = 0,             /* TWAI控制器编号 */
        .mode           = mode,                  /* TWAI控制器操作模式 */
        .tx_io          = TWAI_TX_GPIO_PIN,      /* 发送引脚 */
        .rx_io          = TWAI_RX_GPIO_PIN,      /* 接收引脚 */
        .clkout_io      = TWAI_IO_UNUSED,        /* 时钟输出引脚 */
        .bus_off_io     = TWAI_IO_UNUSED,        /* 总线离线引脚 */
        .tx_queue_len   = 5,                     /* 发送队列长度 */
        .rx_queue_len   = 5,                     /* 接收队列长度 */
        .alerts_enabled = TWAI_ALERT_NONE,       /* 警告标志 */
        .clkout_divider = 0,                     /* 时钟分频,1to14,0不用 */
        .intr_flags     = ESP_INTR_FLAG_LEVEL1,  /* 中断优先级 */
    };
 
    /* TWAI接口位速率配置(官方提供1K到1Mbps常用配置宏) */
    /* Note 波特率计算公式: baud = Ftwai / (tseg_1 + tseg_2 + tss) / brp */
    twai_timing_config_t timing_config = { 
        .clk_src                = TWAI_CLK_SRC_DEFAULT, /* 时钟源 */   
        .quanta_resolution_hz   = 10000000,             /* 时间单元的分辨率 */
        .brp                    = 0,                    /* 时钟分频器 */
        .tseg_1                 = 15,                   /* 时间段1 */
        .tseg_2                 = 4,                    /* 时间段2 */
        .sjw                    = 3,                    /* 再次同步跳跃宽度*/
        .triple_sampling        = false,                /* 三重采样 */
    };
 
    /* 过滤器配置 */
    twai_filter_config_t filter_config = {
        .acceptance_code = 0,            /* 接收码 */
        .acceptance_mask = 0xFFFFFFFF,     /* 接收掩码 0xFFFFFFFF表示全部接收 */
        .single_filter   = true,         /* true:单过滤器模式;false:双过滤器模式 */
    };
ESP_ERROR_CHECK(twai_driver_install(&twai_config, &timing_config, 
&filter_config));  /* 安装TWAI驱动 */
 
    ESP_ERROR_CHECK(twai_start());        /* 启动TWAI驱动 */
 
    tawi_state = 0xFF;
    return ESP_OK;
}

       在TWAI初始化函数中,首先对twai_config结构体变量成员进行赋值,选择使用TWAI0,TWAI的操作模式由形参mode决定,TWAI的发送引脚选择使用的是IO27,接收引脚选择使用的是IO26,不使用CLKOUT和BUS_OFF引脚,发送和接收队列长度都设置为5。然后对timing_config结构体变量成员进行赋值,通过对clk_src、quanta_resolution_hz、tseg_1和tseg_2成员搭配设置,最终得到500k bps波特率。波特率达到500k bps时,就不采用三重采样。接着对filter_config结构体变量成员进行赋值,这里的配置直接接收所有ID的报文,并没有过滤特定ID的报文。最后调用twai_driver_install函数创建TWAI总线,调用twai_start函数启动TWAI总线,往后便可以通过twai_transmit函数发送报文,twai_receive函数接收报文。

       接下来,看一下TWAI发送报文的函数twai_send_data,代码如下。

/**
 * @brief      TWAI发送一帧数据
 * @note       发送格式固定为:标准ID,数据帧
 * @param     id:标准ID (11位)
 * @param      msg:数据指针,最大8个字节
 * @param      len:数据长度(最大8)
 * @retval    ESP_OK成功; ESP_FAIL失败
 */
esp_err_t twai_send_data(uint32_t id, uint8_t msg[], uint8_t len)
{
    twai_message_t tx_msg = {   /* 设置报文类型及格式 */
        .extd   = 0,            /* 0标准帧; 1扩展帧 */
        .rtr    = 0,            /* 0数据帧; 1远程帧 */
        .ss     = 1,            /* 0错误重发; 1单次发送(仲裁或丢失时消息不会被重发) */
        .self   = 0,            /* 报文是否为自收发(回环) */
    };
 
    if (tawi_mode == TWAI_MODE_NO_ACK) /* 无应答模式 + (self = 1) 实现自发自收功能 */
    {
        tx_msg.self = 1; /* 1把消息发送到总线上且接收自己发送的消息也接收总线上消息 */
    }  
    
    /* 正常模式+(self=0)实现把消息发送到总线上,但不接收自己发送的消息且不接收总线上的消息 */
 
    tx_msg.dlc_non_comp     = 0;  /* 0数据长度不大于8; 1数据长度大于8(非标) */
    tx_msg.identifier       = id;  /* 标准帧格式(11位ID)/扩展帧格式(29位ID) */
    tx_msg.data_length_code = len; /* 数据长度代码(字节为单位),数据帧数据符号大小 */
    memset(&tx_msg.data, 0, sizeof(tx_msg.data));   /* 发送数据,对远程帧无效 */
 
    for (int i = 0; i < len; i++)                   /* 复制数据到消息结构体 */
    {
        tx_msg.data[i] = msg[i]; /* 数据(最多8个字节,跟data_length_code匹配) */
    }
    
    /* 发送消息 */
    ESP_ERROR_CHECK(twai_transmit(&tx_msg, pdMS_TO_TICKS(1000)));
 
    return ESP_OK;
}

       该函数的实现,主要调用twai_transmit函数。首先对tx_msg结构体变量成员进行赋值,把报文格式设置好。报文格式具体如下:标准帧、数据帧、单次发送。报文的自收发设置通过twai_mode全局变量决定,若TWAI操作模式为正常模式,就不接收自己报文,若是非应答模式可接收自己报文。报文ID和内容则是通过函数参数决定。

       继续看一下TWAI查询接收数据的函数twai_receive_data,代码如下。

/**
 * @brief       TWAI接收数据查询
 * @note        接收数据格式固定为:标准ID,数据帧
 * @param       id:要查询的 标准ID(11位)
 * @param       buf:数据缓存区
 * @retval     ESP_OK成功; ESP_FAIL失败
 */
esp_err_t twai_receive_data(uint32_t id, uint8_t buf[])
{
    twai_message_t rx_message;
    ESP_ERROR_CHECK(twai_receive(&rx_message, portMAX_DELAY));
  
    if (rx_message.identifier == id)
    {
        for (int i = 0; i < rx_message.data_length_code; i++)
        {
            buf[i] = rx_message.data[i];
        }
    }
 
    return ESP_OK;
}

       该函数的实现主要调用twai_receive函数。定义一个rx_message结构体变量,后面把它的地址作为twai_receive函数的参数,若接收到报文,后续便可以通过rx_message变量获取到。


       2. CMakeLists.txt文件

       本例程的功能实现主要依靠QMI8658A驱动。要在main函数中,成功调用QMI8658A文件中的内容,就得需要修改BSP文件夹下的CMakeLists.txt文件,修改如下:

set(src_dirs
            LED
KEY
LCD
MYIIC
XL9555
           TWAI)
 
set(include_dirs
           LED
KEY
LCD
MYIIC
XL9555
           TWAI)
 
set(requires
            driver
            esp_lcd
            esp_common)
 
idf_component_register( SRC_DIRS ${src_dirs} INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
 
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)


       3. main.c驱动代码

       在main.c里面编写如下代码。

/**
 * @brief       CAN接收任务句柄
 * @param     arg: 传入参数(未用到)
 * @retval   无
 */
static void twai_receive_task(void *arg)    
{
    arg = arg;
    uint8_t ret = 0;
    uint8_t rx_canbuf[8] = {0};
 
    while (1)
    {
        ret = twai_receive_data(MSG_ID, rx_canbuf); /* 接收数据查询0x12报文 */
        if (ret == ESP_OK)            /* 接收到有数据 */
        {
            lcd_fill(30, 270, 130, 310, WHITE);         
 
            for (int i = 0; i < 8; i++)
            {
                if (i < 4)
                {
                    lcd_show_xnum(30+i*32,270,rx_canbuf[i],3,16,0X80,BLUE);
                }
                else
                {
                    lcd_show_xnum(30+(i-4)*32,290,rx_canbuf[i],3,16,0X80,BLUE);
                }
            }
            memset(rx_canbuf, 0, sizeof(rx_canbuf));
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}
 
void app_main(void)
{
    esp_err_t ret;
    uint8_t key;
    uint8_t i = 0, t = 0;
    uint8_t cnt = 0;
    uint8_t canbuf[8] = {0};
    uint8_t twai_mode = 1;
 
    ret = nvs_flash_init();      /* 初始化NVS */
    if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ESP_ERROR_CHECK(nvs_flash_init());
    }
 
    led_init();                     /* LED初始化 */
    key_init();                     /* KEY初始化 */
    myiic_init();                   /* MYIIC初始化 */
    xl9555_init();                  /* XL9555初始化 */
    lcd_init();                     /* LCD屏初始化 */
    twai_init(TWAI_MODE_NO_ACK);    /* TWAI初始化 */
    twai_mode = TWAI_MODE_NO_ACK;   /* 标记TWAI模式 */
 
    lcd_show_string(30, 50,  200, 16, 16, "ESP32-P4", RED);
    lcd_show_string(30, 70,  200, 16, 16, "TWAI TEST", RED);
    lcd_show_string(30, 90,  200, 16, 16, "ATOM@ALIENTEK", RED);
    lcd_show_string(30, 110, 200, 16, 16, "NO_ACK Mode     ", RED);
    lcd_show_string(30, 130, 200, 16, 16, "K0:Send   K1:NORMAL", RED);
    lcd_show_string(30, 150, 200, 16, 16, "K2:NO_ACK ", RED);
 
    lcd_show_string(30, 170, 200, 16, 16, "Count:", BLUE);
    lcd_show_string(30, 190, 200, 16, 16, "Send Data:", BLUE);
    lcd_show_string(30, 250, 200, 16, 16, "Receive Data:", BLUE);
 
xTaskCreatePinnedToCore(twai_receive_task, "TWAI_RX_TASK", 4096, NULL, 9,
&twai_receive_handle, tskNO_AFFINITY);
 
    while (1)
    {
        key = xl9555_key_scan(0);
        if (key == KEY0_PRES)    
        {
            for (i = 0; i < 8; i++)
            {
                canbuf[i] = cnt + i;
 
                if (i < 4)
                {
                    lcd_show_xnum(30+i*32, 210, canbuf[i], 3, 16, 0x80, BLUE);
                }
                else
                {
                    lcd_show_xnum(30+(i-4)*32,230,canbuf[i], 3, 16, 0x80, BLUE);
                }
            }
 
            ret = twai_send_data(MSG_ID, canbuf, 8); /* ID=0x12, 发送8个字节 */
            if (ret)
            {
                lcd_show_string(30 + 80, 190, 200, 16, 16, "Failed", BLUE);
            }
            else
            {
                lcd_show_string(30 + 80, 190, 200, 16, 16, "OK    ", BLUE);
            }
        }
        else if (key == KEY1_PRES)
        {
            if (twai_mode == TWAI_MODE_NO_ACK)      /* 若当前是无应答模式 */
            {
                vTaskDelete(twai_receive_handle);   /* 删除任务 */
                twai_init(TWAI_MODE_NORMAL);        /* 切换到正常模式 */
                lcd_show_string(30, 110, 200, 16, 16, "NORMAL Mode     ", RED);
                twai_mode = TWAI_MODE_NORMAL;
                xTaskCreatePinnedToCore(twai_receive_task, "TWAI_RX_TASK", 4096,
                NULL, 9, &twai_receive_handle, tskNO_AFFINITY);    
            }
        }
        else if (key == KEY2_PRES)
        {
            if (twai_mode == TWAI_MODE_NORMAL)      /* 若当前是正常模式 */
            {
                vTaskDelete(twai_receive_handle);   /* 删除任务 */
                twai_init(TWAI_MODE_NO_ACK);        /* 切换到无应答模式 */
                lcd_show_string(30, 110, 200, 16, 16, "NO_ACK Mode     ", RED);
                twai_mode = TWAI_MODE_NO_ACK;
                xTaskCreatePinnedToCore(twai_receive_task, "TWAI_RX_TASK", 4096,
                NULL, 9, &twai_receive_handle, tskNO_AFFINITY);    
            }
        }
 
        t++;
        vTaskDelay(pdMS_TO_TICKS(10));
 
        if (t == 20)
        {
            LED0_TOGGLE();
            t = 0;
            cnt++;
            lcd_show_xnum(30 + 48, 170, cnt, 3, 16, 0X80, BLUE);
        }
    }
}

       在app_main函数中,首先初始化所需的外设,注意TWAI此时默认配置为非应答模式,然后创建了TWAI的接收任务,后续在TWAI接收任务中查询是否接收到数据。在while循环中,通过检测按键去选择操作,若KEY0按下,发送报文消息;若KEY1按下,设置TWAI模式为正常模式;若KEY2按下,设置TWAI模式为非应答模式。以上操作会更新LCD屏的显示内容。

       twai_receive_task函数便是TWAI接收任务,在函数中,主要就是调用twai_receive_data函数查询是否接收到数据,若接收到数据,就会在LCD上显示出来。


        36.4 下载验证

       将程序下载到开发板后(注意两个开发板都要下载这个代码),可以看到LED0不停闪烁,提示程序已经在运行了,可看到LCD显示的内容如图36.4.1所示:


图36.4.1 TWAI实验程序运行效果图


       默认设置的是非应答模式,按下KEY0可以在LCD屏上看到自发自收的数据,如下图所示。


图36.4.2 TWAI非应答模式操作效果


       这时候可以通过按下KEY1切换到正常模式,正常模式下,就必须连接两个开发板的CAN接口,然后便可以互发数据了,如图36.4.3和图36.4.4所示。


图36.4.3 TWAI正常模式发送数据


图36.4.4 TWAI正常模式接收数据


       图36.4.3来自开发板A,发送了8个数据,图36.4.4来自开发板B,接收到了来自开发板A的8个数据。


请使用浏览器的分享功能分享到微信等