verilog串口通信的接收与发送_UART协议

(101) 2024-06-23 10:01:03

【Verilog串口篇3】UART自定义应用层协议SLink实现多字节协议帧收发

本文从应用层出发,讲解自定义的通信协议,暂且命名为 SLink,实现串口通信 多字节协议帧收发

1、帧格式
帧是什么?不知道的我想你来错地方了。一帧数据包括多个字节,每个字节的含义由通信协议规定,即为帧格式。 帧格式描述常用的元素有:帧头、数据长度、有效载荷、校验、帧尾等等,当然,我们大可不必拘泥于这些元素,增删名用(增补、删除、命名、运用),仁者见仁,智者见智。

verilog串口通信的接收与发送_UART协议 (https://mushiming.com/)  第1张

HEAD 帧头,固定值 8’hfa。
MARK 协议标识,固定值 8’hfe。
通常,在消息解析时,我们读到帧头就认为消息开始了,之后就按照格式开始逐字节解析。而这里,我们再加个协议标识判断,只有两者都符合,才认为消息开始。
EXT 扩展信息,内容由用户自定义,长度在参数中设置。
TAG 标签,表示消息类型。
LEN 有效载荷的字节长度
VAL 有效载荷
理论上,我们可配置特定消息类型对应的有效载荷长度固定,这样在解析到长度时,就能知道长度是否有误,若错误,则结束该帧的解析,进入下一帧,实践中,意义不大,留给大家去实现了。

2、消息解析
先说消息解析模块的接口描述,巧妇难为无米之炊,首先得要有数据给你解析,这里采用的是 逐字节解析 方式,所以有一个字节输入端口,还有一个脉冲控制信号输入端口,用于启动解析。消息处理是用户的事情,正所谓众口难调,这里只是将解析到的消息交给用户,管他蒸煮焖炖还是煎烤炒炸,所以还需要消息输出端口。

module SLinkParse #(parameter EXT_NBR = 2, // 扩展信息长度 VAL_SIZE = 8 // 最大有效载荷长度 ) ( input Clock, input nRst, input Parse, // 启动解析,由该脉冲信号控制逐字节解析 input [7:0] Word, // 待解析字节 // 解析到的数据:扩展信息(EXT),标签(TAG),有效载荷长度(LEN),有效载荷(VAL) output reg [(EXT_NBR>0?EXT_NBR:1)*8-1:0] Ext, output reg [7:0] Tag, output reg [7:0] Len, output reg [(VAL_SIZE>0?VAL_SIZE:1)*8-1:0] Val, output reg Cplt // 解析完成,得到完整消息 ); 

接下来是解析过程,又是顺序操作,老套路,三段式状态机。主要来看看是如何得到次态的,对于单个字节的元素,我们以 Parse 启动解析信号 作为状态转移的条件,而对于多字节元素,以 接收到的字节数目 count 作为转移条件。另外,在解析帧头和协议标识时,还需判断数据是否相符。至于是否需要解析扩展信息或有效载荷,我们根据二者的长度进行判断,大于零才解析。

/* 得到次态的组合逻辑,用 = 赋值 */ always @(*) begin case (cstate) S_HEAD: nstate = Parse ? (Word == HEAD ? S_MARK : S_HEAD) : S_HEAD; S_MARK: nstate = Parse ? (Word == MARK ? (EXT_NBR ? S_EXT : S_TAG) : S_HEAD) : S_MARK; S_EXT: nstate = count == EXT_NBR + 'd1 ? S_TAG : S_EXT; S_TAG: nstate = Parse ? S_LEN : S_TAG; S_LEN: nstate = Parse ? (Word ? S_VAL : S_CPLT) : S_LEN; S_VAL: nstate = count == Len + 'd1 ? S_CPLT : S_VAL; S_CPLT: nstate = S_HEAD; default: nstate = S_HEAD; endcase end 

状态输出,形象的说就是各状态所执行的动作,直接贴上代码,前面说过,我们不对消息做任何处理,直接输出给用户,从中可看出主要就是 存储数据到相应的输出端口

/* 状态输出(即相应状态需要执行的动作) */ always @(posedge Clock or negedge nRst) begin if (!nRst) begin count <= 'd0; Ext <= 'd0; Tag <= 'd0; Len <= 'd0; Val <= 'd0; Cplt <= 1'b0; end else begin case (cstate) S_HEAD: Cplt <= 1'b0; S_MARK: count <= 'd1; // 为存储扩展信息做准备 S_EXT: begin // 存储扩展信息,由低位到高位存储,先接收到的存在低位 Ext[count*8-1-:8] <= Parse ? Word : Ext[count*8-1-:8]; count <= Parse ? count + 'd1 : count; end S_TAG: Tag <= Parse ? Word : Tag; // 存储标签 S_LEN: begin Len <= Parse ? Word : Len; // 存储长度 count <= 'd1; // 为存储有效载荷做准备 end S_VAL: begin // 存储有效载荷,由低位到高位存储,先接收到的存在低位 Val[count*8-1-:8] <= Parse ? Word : Val[count*8-1-:8]; count <= Parse ? count + 'd1 : count; end S_CPLT: Cplt <= 1'b1; default: Cplt <= 1'b0; endcase end end 

3、打包发送
看到这里,本协议的核心已经讲完了,打包发送实际就是 串口的多字节发送,对串口多字节发送有问题的小伙伴可以看一看。接口描述涉及到底层的 UART 发送模块,因为我们最终调用它来发送数据。

module SLinkPack #(parameter EXT_NBR = 2, // 扩展信息长度 VAL_SIZE = 8 // 最大有效载荷长度 ) ( input Clock, input nRst, input Pack, // 启动打包发送 // 待发送消息:扩展信息(EXT),标签(TAG),有效载荷长度(LEN),有效载荷(VAL) input [(EXT_NBR>0?EXT_NBR:1)*8-1:0] Ext, input [7:0] Tag, input [7:0] Len, input [(VAL_SIZE>0?VAL_SIZE:1)*8-1:0] Val, input TransCplt, // 由底层串口发送模块反馈的字节传输完成信号 output reg [7:0] Word, // 待发送字节,按照帧格式逐字节发送 output reg Trans, // 输出给底层串口发送模块的启动传输信号 output reg PackCplt // 打包发送完成 ); 

功能实现用的也是三段式状态机,总体思路就是:准备一串数据,发送一个字节,等待发送完成,再发送下一个字节。。。何时发送结束呢?加个计数器记录一下已发送的字节数,计数值到了,就发送完成了。得到次态的组合逻辑:

/* 得到次态的组合逻辑,用 = 赋值 */ always @(*) begin case (cstate) S_IDLE: nstate = Pack ? S_HEAD : S_IDLE; S_HEAD: nstate = S_MARK; S_MARK: nstate = TransCplt ? (EXT_NBR ? S_EXT : S_TAG) : S_MARK; S_EXT: nstate = count == EXT_NBR + 'd1 ? S_TAG : S_EXT; S_TAG: nstate = TransCplt ? S_LEN : S_TAG; S_LEN: nstate = TransCplt ? (Len ? S_VAL : S_CPLT) : S_LEN; S_VAL: nstate = count == Len + 'd1 ? S_CPLT : S_VAL; // 等待最后一个字节发送完成 S_CPLT: nstate = TransCplt ? S_IDLE : S_CPLT; default: nstate = S_IDLE; endcase end 

状态输出是什么呢?当然是根据帧格式,准备数据并启动底层的串口发送模块将数据发送出去。除了第一次传输,以后每次传输,都需要等待前一次传输完成。

/* 状态输出(即相应状态需要执行的动作) */ always @(posedge Clock or negedge nRst) begin if (!nRst) begin count <= 'd0; Word <= 'd0; Trans <= 1'b0; PackCplt <= 1'b0; end else begin case (cstate) S_IDLE: begin Trans <= 1'b0; PackCplt <= 1'b0; end S_HEAD: begin Word <= 'hfa; // 帧头 Trans <= 1'b1; // 启动传输 end S_MARK: begin Word <= TransCplt ? 'hfe : Word; Trans <= TransCplt ? 1'b1 : 1'b0; count <= 'd1; // 为扩展信息计数做准备 end S_EXT: begin Word <= TransCplt ? Ext[count*8-1-:8] : Word; // 发送扩展信息,低位先发,谨记,实践中容易搞反 Trans <= TransCplt ? 1'b1 : 1'b0; count <= TransCplt ? count + 'd1 : count; end S_TAG: begin Word <= TransCplt ? Tag : Word; Trans <= TransCplt ? 1'b1 : 1'b0; end S_LEN: begin Word <= TransCplt ? Len : Word; Trans <= TransCplt ? 1'b1 : 1'b0; count <= 'd1; // 为有效载荷计数做准备 end S_VAL: begin Word <= TransCplt ? Val[count*8-1-:8] : Word; // 发送有效载荷,低位先发,谨记,实践中容易搞反 Trans <= TransCplt ? 1'b1 : 1'b0; count <= TransCplt ? count + 'd1 : count; end S_CPLT: begin Trans <= 1'b0; PackCplt <= TransCplt ? 1'b1 : PackCplt; // 等待最后一个字节发送完成 end default: begin Trans <= 1'b0; PackCplt <= 1'b0; end endcase end end 
THE END

发表回复