在UART 综合应用-协议与数据包文章中,我们介绍了从FPGA 中通过uart 读取数据,CPU 写入数据到FPGA 外设的过程。在一些中大型项目中,往往会增加数据链路层,用来隔离应用数据和具体的硬件传输方式。 也就是说,应用程序只是负责数据发送,处理,而不关系具体使用什么样的硬件来发送(UART , I2C ,SPI, ETHERNET)。 而phy模块只管数据的接收和发送, 但是接收和发送的数据,phy 模块不再关心。这样,就需要不同的模块来进行分层处理,使得整个工程结构明晰,同时根据不同的项目需要, 模块可以在不同的项目上重复使用。
1 对功模块能重新分层
之前的文章中,uart项目的结构为:

图1
经过分层处理后,重新规划的模块结构为:

图2
从图2 中可以看出,整个工程由2个大模块构成:uart_app, uart_wrapper。这2 个模块直接是通过fifo 相关的数据信号来传递信息的。
- uart_app 模块主要负责对数据的解析,发送等,不再关心具体使用什么样的phy 来进行通讯。
- uart_wrapper 模块主要是接收rx来的数据, 或者是将数据发送到tx上,实现完整的UART phy 功能,而不再关心数据具体是什么含义,也不对数据做任何的解析。
通过这样的分层设计,可以看出, uart_app 模块可以给很多项目使用, 具体使用UART, SPI,I2C 等,都可以正常通讯。 uart_wrapper 模块也是可以给很多项目来重用的。因为这个模块值负责接收,发送数据,不对数据解析。 这样的分层结构,每个模块的功能清晰,重用性高,从而实现了phy层和数据链路层的分离。
2 uart_wrapper 结构
图3
uart_wrapper 封装了完整的uart 模块的接口。 通过数据总线方式 和外部设备通讯。 它是有uart_engine 和uart_phy 两个部分组成。 其中uart_phy部分,在之前的文章中做过详细的介绍。一端是uart_engine 部分负责接收、发送信号到uart_phy,另外一端是通过数据总线和外部设备沟通。uart_engine 主要负责uart 接收来的数据存储到rx fifo中,同时提供包接收指示。 同时,也负责从tx fifo 中得到数据, 按照uart_phy 所需要的时序,发送数据到uart_phy。
uart_wrapper 代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
`timescale 1ns / 1ps module uart_engine# ( parameter DEEP = 256 ) ( input sys_clk, // tx channel output tx_req, output [7:0] tx_data, input tx_ack, input [8:0] tx_fifo_din, input tx_fifo_wr, output [7:0] tx_fifo_cnt, // rx channel input [7:0] rx_data, input rx_rdy, input rx_eop, output [8:0] rx_fifo_dout, input rx_fifo_rd, output rx_pkg_plus, input rst ); //===========================rx side rx_fifo # ( .DEEP ( DEEP ) ) rx_fifo_inst ( .sys_clk ( sys_clk ), .rx_data ( rx_data ), .rx_rdy ( rx_rdy ), .rx_eop ( rx_eop ), .rx_fifo_dout ( rx_fifo_dout ), .rx_fifo_rd ( rx_fifo_rd ), .rx_pkg_plus ( rx_pkg_plus ), .rst ( rst ) ); //===========================tx side tx_fifo # ( .DEEP ( DEEP ) ) tx_fifo_inst ( .sys_clk ( sys_clk ), .tx_ack ( tx_ack ), .tx_req ( tx_req ), .tx_data ( tx_data ), .tx_fifo_din ( tx_fifo_din ), .tx_fifo_wr ( tx_fifo_wr ), .tx_fifo_cnt ( tx_fifo_cnt ), .rst ( rst ) ); endmodule |
uart_engine 模块包括tx_fifo.v 和rx_fifo.v 两个部分。其中
rx_fifo.v 代码如下:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
`timescale 1ns / 1ps module rx_fifo # ( parameter DEEP = 256 ) ( input sys_clk, // write channel input [7:0] rx_data, input rx_rdy, input rx_eop, // read channel output [8:0] rx_fifo_dout, input rx_fifo_rd, output reg rx_pkg_plus = 0, input rst ); //==================== write fifo side reg [2:0] rx_wr_st = 0; reg rx_wrreq = 0; reg [8:0] rx_fifo_din = 0; wire rx_rdempty; wire rx_wrfull ; wire [$clog2(DEEP) - 1:0] wrusedw; always @ (posedge sys_clk ) if(rst) begin rx_wrreq <= 1'b0; rx_fifo_din <= 0; rx_wr_st <= 0; rx_pkg_plus <= 1'b0; end else case(rx_wr_st) 0: begin rx_pkg_plus <= 1'b0; rx_wrreq <= 1'b0; if(wrusedw ==0) rx_wr_st <= 1; end 1: begin rx_wrreq <= 1'b0; rx_pkg_plus <= 1'b0; if(rx_rdy) begin rx_wrreq <= 1'b1; rx_fifo_din <= {1'b1, rx_data}; end else if(rx_eop) begin rx_wrreq <= 1'b1; rx_fifo_din <= 0; rx_pkg_plus <= 1'b1; rx_wr_st <= 0; end end default:rx_wr_st <= 0; endcase //==================== rx side ipcore instantiate fifo rx_fifo_inst ( .wrclk ( sys_clk ), .wrreq ( rx_wrreq ), .data ( rx_fifo_din ), .wrfull ( rx_wrfull ), .wrusedw ( wrusedw ), .rdclk ( sys_clk ), .rdreq ( rx_fifo_rd ), .q ( rx_fifo_dout ), .rdempty ( rx_rdempty ) ); endmodule |
其中fifo IP的定义 适用于 tx_fifo.v 和rx_fifo.v 这个fifo 定义为 读写时钟分离(异步时钟),深度为256,读写数据宽度为9bit , 增加了写counter 属性。
图4
状态机部分比较简单,主要是
负责接收uart模块传递上来的数据有效使能rx_rdy、8bit数据rx_data、包结束信号rx_eop。将数据以一定格式写入rx端的FIFO 中,等待应用层给读使能。程序实现流程如下:
初始化:reset作为复位信号对变量进行初始化操作
状态0:FIFO深度0-255,包长范围0-255,为确保每次能存入一个完整数据包,需要保证剩余空间256。判断wrusedw 为0跳转到状态1
状态1:等待rx_rdy 即数据有效使能信号,来一个信号收8bit数据,给一个1作为控制字信息合并为9bit数据 {1’b1, rx_data},同时给写使能,将数据写入FIFO。如果一直没有rx_rdy信号并且来了包结束信号rx_eop,写9’b0作为包隔断,同时给上层应用一个rx_pkg_plus指示FIFO 里面存入了一个完整的数据包。然后跳转到状态0。(由于rx_eop与rx_rdy至少隔了12个波特率使能信号的时间,所以当回到状态0再去判断wrusedw 时,已经可以准确拿到wrusedw 的值了)
rx_fifo 流程图
tx_fifo.v 代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
`timescale 1 ns / 1ps module tx_fifo # ( parameter DEEP = 256 ) ( input sys_clk, // read channel input tx_ack, output reg tx_req, output reg [ 7: 0 ] tx_data = 0, // write channel input [ 8: 0 ] tx_fifo_din, input tx_fifo_wr, output [ $clog2( DEEP ) - 1:0 ] tx_fifo_cnt, input rst ); //==================== read fifo side wire tx_rdempty; reg [ 2: 0 ] tx_rd_st = 0; reg tx_fifo_rd = 1'b0; wire [ 8: 0 ] tx_fifo_dout; always@( posedge sys_clk ) if ( rst ) begin tx_rd_st <= 0; tx_fifo_rd <= 1'b0; tx_req <= 1'b0; tx_data <= 0; end else case ( tx_rd_st ) 0: begin tx_req <= 1'b0; tx_fifo_rd <= 1'b0; if ( !tx_rdempty ) begin tx_fifo_rd <= 1'b1; tx_rd_st <= 1; end end 1: begin tx_fifo_rd <= 1'b0; tx_rd_st <= 2; end 2: begin if(tx_fifo_dout[8]) begin tx_req <= 1'b1; tx_data <= tx_fifo_dout [ 7: 0 ]; if ( tx_ack ) begin tx_req <= 1'b0; tx_rd_st <= 3; end end else tx_rd_st <= 0; end 3: begin if ( !tx_ack ) tx_rd_st <= 0; end default: tx_rd_st <= 0; endcase //==================== tx side ipcore instantiate fifo tx_fifo_inst ( .wrclk ( sys_clk ), .wrreq ( tx_fifo_wr ), .data ( tx_fifo_din ), .wrfull ( ), .wrusedw ( tx_fifo_cnt ), .rdclk ( sys_clk ), .rdreq ( tx_fifo_rd ), .q ( tx_fifo_dout ), .rdempty ( tx_rdempty ) ); endmodule |
fifo IP 核部分和rx fifo中的定义相同(9bit 数据, 异步时钟, 增加了写counter)。
状态机部分:
当底层uart有空可以发数据时,读出应用层写入FIFO 的数据,并给出数据有效使能信号。具体实现流程如下:
初始化:reset作为复位信号对状态机进行初始化操作
状态0:FIFO有数就到状态1,同时给出读使能
状态1:复位读使能跳转到状态2
状态2:如果tx_fifo_dout[8] == 1,截取低8位发送给uart模块同时给出数据有效使能信号,uart模块反馈收到数据后将使能信号拉低,跳转到状态3,否则跳转到状态0
状态3:等待uart将ack信号拉低,然后跳回状态0(由于ack是一个长电平信号,不等待ack拉低直接到0状态会导致重复给uart发送数据)

tx_fifo 流程图
3 user_app 结构
user_app模块包含:tx_app,rx_app, seg_dev,cmd_decoder 等, 其中seg_dev, cmd_decoder 在之前的文章中有所介绍。这里主要介绍一下tx_app.v 和rx_app.v模块。
- rx_app模块:当底层的FIFO有完整数据包时,读取数据包内容,主要由状态机完成;按照双方约定的协议格式对读出的数据进行解析,最后将正确的读写命令、数据等信号传递给上层应用模块,主要由从状态机完成。具体流程如下:
rx_app.v 代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 |
`timescale 1ns / 1ps module rx_app ( input sys_clk, input rx_pkg_plus, input [8:0] rx_fifo_dout, output reg rx_fifo_rd = 0, output reg [7:0] pkg_type = 0, output reg pkg_op = 0, // write 0 or read 1 output reg [23:0] pkg_data = 0, output reg pkg_valid = 0, input rst ); /* packet format : leng + wr/rd + dev_type + pkg_content.... leng : total length of the packet (1 byte) wr/rd : fpga write or read operation. 0 => wirte op; 1 => read op; (1 byte) dev_type: device index : 01 => segment; 02 => switch; 03 => keypad; pkg_cntent: data was send by pc (ready to write to a device) (n bytes). */ //==================== fifo里面完整包计数 reg rx_pkg_minus = 0; reg [7:0] packets_count = 0; always @ (posedge sys_clk ) if(rst) packets_count <= 0; else begin packets_count <= packets_count + rx_pkg_plus - rx_pkg_minus; end //==================== 读FIFO reg [3:0] rd_st = 0; reg [7:0] rx_len = 0; reg [7:0] rx_cnt = 0; reg rx_error = 0; // 指令发送错误 always @ (posedge sys_clk ) if(rst) begin rx_pkg_minus <= 0; rx_cnt <= 0; pkg_valid <= 0; rx_fifo_rd <= 1'b0; pkg_type <= 8'hff; rx_len <= 0; pkg_op <= 0; pkg_data <= 0; rx_error <= 0; rd_st <= 0; end else case (rd_st) 0: begin rx_pkg_minus <= 0; rx_cnt <= 0; pkg_valid <= 0; rx_fifo_rd <= 1'b0; pkg_type <= 8'hff; pkg_op <= 0; rx_len <= 0; pkg_data <= 0; rx_error <= 0; rd_st <= 0; if(packets_count != 0) begin rx_fifo_rd <= 1'b1; rd_st <= 1; end end 1: begin rx_fifo_rd <= 1'b0; rd_st <= 2; end 2: begin pkg_data <= 0; if(rx_fifo_dout[8]) begin rx_len <= rx_fifo_dout[7:0]; rx_cnt <= 1; if(rx_fifo_dout[7:0] < 3) // packet lenth <= 2; begin rx_pkg_minus <= 1'b1; rx_fifo_rd <= 1'b1; //由于FIFO空间设置的时候256,判定必须FIFO为空才会进行下一次读写,所以必须清空 rd_st <= 11; end else begin rd_st <= 3; rx_fifo_rd <= 1'b1; end end else begin rx_pkg_minus <= 1'b1; rd_st <= 14; end end 3: begin rd_st <= 4; rx_fifo_rd <= 1'b0; end 4: begin if(rx_fifo_dout[8]) begin rx_cnt <= 2; rx_fifo_rd <= 1'b1; pkg_op <= rx_fifo_dout[0]; // get command, 00 => write; 01 => read rd_st <= 5; end else begin rx_pkg_minus <= 1'b1; rd_st <= 14; end end 5: begin rx_fifo_rd <= 1'b0; rd_st <= 6; end 6: begin if(rx_fifo_dout[8]) begin rx_cnt <= 3; pkg_type <= rx_fifo_dout[7:0]; // get type, 01 => seg; 02 => sw rx_fifo_rd <= 1'b1; rd_st <= 7; end else begin rx_pkg_minus <= 1'b1; rd_st <= 14; end end 7: begin rx_fifo_rd <= 1'b0; rd_st <= 8; end 8: begin if(rx_fifo_dout[8]) begin if(rx_len == rx_cnt) //说明实际写入FIFO 数据比lenth 长,命令截断,同时需要将FIFO清空 begin pkg_valid <= 1; rx_pkg_minus <= 1'b1; rx_fifo_rd <= 1'b1; rd_st <= 11; end else begin pkg_data <= {pkg_data[15:0], rx_fifo_dout[7:0]}; rx_cnt <= rx_cnt + 1; rx_fifo_rd <= 1'b1; rd_st <= 7; end end else begin if(rx_len == rx_cnt) //说明实际写入FIFO 数据与lenth相同,命令正确 begin pkg_valid <= 1; rx_pkg_minus <= 1'b1; rd_st <= 15; end else //说明实际写入FIFO 数据比lenth短 begin rd_st <= 14; rx_pkg_minus <= 1'b1; end end end 11: begin pkg_valid <= 0; rx_pkg_minus <= 1'b0; rx_fifo_rd <= 1'b0; rd_st <= 12; end 12: begin if(rx_fifo_dout[8]) begin rx_fifo_rd <= 1'b1; rd_st <= 11; end else rd_st <= 14; end 14: begin rx_error <= 1'b1; rx_pkg_minus <= 1'b0; rd_st <= 0; end 15: begin pkg_valid <= 0; rx_pkg_minus <= 1'b0; rx_error <= 1'b0; rd_st <= 0; //==========等待一个时钟周期,确保packets_count 拿到准确的数据。 end default: rd_st <= 0; endcase endmodule |

端口信号解释:
sys_clk:时钟域模块传递进来的系统复位信号
rx_pkg_plus:rx_fifo传递的单脉冲信号,表示rx_fifo已经写入一个完整数据包
rx_fifo_dout:从rx_fifo读出的数据
rx_fifo_rd :读rx_fifo 使能,当rx_fifo写入一个完整包开始读
pkg_type:从设备地址,表示计算机准备对那个设备进行操作
pkg_op:读/写 控制字
pkg_data:计算机写入到fpga的数据
pkg_valid:传递给应用层的数据和命令有效信号
rst:时钟域模块传递进来的系统复位信号
状态机详解:
初始化:rst信号对整个状态机进行复位
- 状态0:如果有完整数据包写入fifo完成,发读使能,跳转到状态1
- 状态1:等待一个时钟周期,跳转到状态2
- 状态2:判断读出数据最高控制bit是否为高,为高表示数据有效,为高且读出的length小于3,则不满足数据包协议,跳转到状态11,将fifo读空;如果控制bit为高且length 大于等于3,给读使能,跳转到状态3;如果最高控制bit为低,表示fifo读空,表示数据包有问题,跳转到错误指示状态14
- 状态3:等待一个时钟周期,跳转到状态4
- 状态4:判断读出的数据最高位是否为高,为高表示数据有效,根据数据包协议将[7:0]数据存入pkg_op,跳转到状态5;为低,表示数据包错误,跳转到状态14
- 状态5:等待一个时钟周期,跳转到状态6
- 状态6:判断读出的数据最高位是否为高为高,根据数据包协议将[7:0]数据存入pkg_type,跳转到状态7;为低,表示数据包错误,跳转到状态14
- 状态7:等待一个时钟周期,跳转到状态8
- 状态8:判断读出最高控制bit是否为高,为高且此时rx_len == rx_cnt,表示实际写入FIFO 数据比lenth 长,命令错误,同时需要将FIFO清空,跳转到状态11;控制bit为高且rx_len >= rx_cnt,表示数据未读完,返回状态7等待一个时钟。如果控制bit为低,且rx_len == rx_cnt 表示数据包正确,读完,跳转到状态15;如果控制bit为低但是rx_len >= rx_cnt,说明实际写入FIFO 数据比lenth短,跳转到状态14
- 状态11-状态12:将剩余fifo中的数据读完,为下次写入fifo准备空间
- 状态14:数据包错误指示状态
- 状态15:各种数据清0,等待一个时钟,确保packets_count 拿到准确的数据。
tx_app 模块
当有读命令时,将读设备(SW)反馈的并行数据按照一定格式写入tx_fifo。

端口信号解释:
sys_clk:时钟域模块传递进的系统时钟
tx_fifo_cnt:tx_fifo 中写入了多少字节计数,以此判断fifo剩余空间是否足够
tx_fifo_wr:写tx_fifo 的写使能
tx_fifo_din:写tx_fifo的写数据
rd_sw_cmd:计算机发送给fpga的读设备命令
SW:读设备发送的读数据
rst:时钟域模块传递进来的系统复位信号
状态机详解:
在确保FIFO 有足够空间后,如果接收到读命令,就将读设备数据同步到系统时钟,并且将数据按照一定格式存入FIFO,等待底层模块读取。具体流程如下:
步骤1:将读设备输入信号打两拍同步到系统时钟
步骤2:利用状态机实现数据的存储
- 初始化:reset作为复位信号对状态机进行初始化操作
- 状态0:判断FIFO可用空间是否足够,足够就跳转到状态1
- 状态1:收到读命令,给出写FIFO使能,同时给出9bit写数据{1’b1,key_r1}最高位1作为控制字,表示后面的8bit设备数据有效。跳转到状态2
- 状态2:由于本次工程的目的是接收一次命令读一次,给出9bit写数据{1’b0, 8’00},然后跳转到状态3
- 状态3:由于本次工程的目的是接收一次命令读一次,所以需要等待写命令拉低,然后跳转到状态4
- 状态4:由于tx_fifo_cnt 生效需要等待2个时钟,保险期间,状态4作为等待状态。跳转到状态0
tx_app.v 代码:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
`timescale 1ns / 1ps module tx_app # ( parameter DEEP = 256 ) ( input sys_clk, input [$clog2(DEEP) - 1:0] tx_fifo_cnt, output reg tx_fifo_wr, output reg [8:0] tx_fifo_din, input rd_sw_cmd, input [7:0] SW, input rst ); //============================输入信号同步处理 reg [7:0] key_r0 = 0; reg [7:0] key_r1 = 0; always @ (posedge sys_clk or posedge rst) if(rst) begin key_r0 <= 0; key_r1 <= 0; end else begin key_r0 <= SW; key_r1 <= key_r0; end //============================输入信号写入FIFO reg [3:0] wr_st = 0; always @ (posedge sys_clk or posedge rst) if(rst) begin tx_fifo_din <= 0; tx_fifo_wr <= 1'b0; wr_st <= 0; end else case(wr_st) 0: begin tx_fifo_din <= 0; tx_fifo_wr <= 1'b0; if(tx_fifo_cnt == 0) wr_st <= 1; end 1: begin if(rd_sw_cmd) begin tx_fifo_din <= {1'b1,key_r1}; tx_fifo_wr <= 1'b1; wr_st <= 2; end end 2: begin tx_fifo_din <= 0; tx_fifo_wr <= 1'b1; wr_st <= 3; end 3: begin tx_fifo_wr <= 1'b0; wr_st <= 4; end 4: begin wr_st <= 0; //tx_fifo_cnt 生效要晚2个时钟,保险起见,多停留1个时钟 end default:wr_st <= 0; endcase endmodule |
下载调试:
1)08 00 01 aa bb cc ee ff
2)05 00 01 12 34

Quartus 程序代码:
[content_control]
[/content_control]