在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 代码:
`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 代码如下:
`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 代码:
`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 代码:
`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 代码:
`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]