在上一章中 UART 组装与实验,我们学习了UART 数据的仿真和真实的板载通讯。其中仿真程序由FPGA 发送数据包,在tb 仿真文件中回环,在FPGA 接收模块中检查数据结果。在板载实验中, 计算机发送数据给FPGA ,FPGA 得到数据后,将数据原封不动的发送给计算机;在计算机上,我们可以通过检查发送的数据和接收的数据来验证真实板载实验中FPGA 接收和发送模块。在实际工作中, 往往不是回环测试。 发送的数据和接收到的数据完成不同的功能。这样,UART通讯往往是以包为单位完成某些特定的行为操作。比如查询FPGA 开发板上的开关状态, 按键状态,或者是计算机发送字符串在FPGA 开发板上显示等等,尤其在开发中如何利用UART实现与计算机联合调试也是目前UART应用最广泛的场景之一。带有结构的数据包,数据包中按照功能与交换等特点需要分层设计,因此协议的概念就会自然产生和使用。分层和协议在通信中广泛存在,如网络通信中ISO/OSI的7层协议,TCP/IP的4层协议,4G/5G无线通信中的3层协议等,这些知名的协议已经形成标准,称为从事该行业的工程师和设计者共同遵循的标准。但也还有大量的子网、异构网通信中,由于成本,通信效率,程序复杂度等因素的考量,在点到点以及小规模的通信网中采用自定义的私有协议。
私有协议是一个双方约定好的,具有分层协议的数据包。一个约定好的数据包格式, 可以让收发双方快速理解对方的意图,完成莫一个具体操作,同时又可以检查发送的数据是否被正确的接收到,这不仅使通信程序可以实现,也是开发工程师共同遵守的约定。如图1所示在终端设备中由于数据量小,通信简单,但的确又有数据需要搜集,并能远程执行特定的命令,因此在设备(device)和网关(GateWay)之间往往由私有协议实现。

图1
为了突出重点,本文将以仅以FPGA与PC机的串口通信为例介绍具有分层协议的数据包形式,并实例介绍UART在综合开发中的应用。
1. 设计要求和内容
- 通信方式采用主从模式,即以FPGA或计算机为主, 本设计以计算机为主,FPGA为从。
- 计算机发送命令(含数据包内)即数据给FPGA , 让FPGA 实现某些具体行为操作。计算机以数据包的形式发送给FPGA ,数据包格式是由双方(FPGA ,计算机)事先约定的。用于FPGA与计算机相互理解。如:
- 计算机发送写命令,在多个数码管上显示0~f 16进制数,每次最多只能显示 6 位。
- 计算机发送读命令,读取FPGA 开发板上的拨动开关(switch)信息。
- 程序框架是可以扩展的, 方便计算机完成不同的写操作,读操作等。
2.自定义分层协议及通讯数据包格式
分层的概念如图2所示,从应用数据开始逐渐向下一层一层打包,每层都添加包头,直到交给物理层。由此可见在之前的开发中无论使UART的tx程序,rx程序都是物理层实现的一个部分。为了使用数据包机制,在发送和接收端都介绍了初始阶段长IDLE的使用,为本文分层,协议,包的使用打下了基础。

图2
按照图2分层结果,将设计要求和内容重新组织如图3所示的数据包结构,

图3
图3中,按照设计要求和内容定义了计算机发送数据包的格式:
- 数据包长度(length):第一字节,代表当前发送的数据包总长度,包括长度字节。
- 读写操作码(rd/wr):第二字节:读写操作。 0 表示 写操作; 1 表示读操作,其他的数值(2~255)无定义。
- 设备类型(dev_type) :第三字节:表示计算机要对FPGA 上的哪一个设备进行读写操作。 8’h01 代表数码管,8’h02 代表swtich, 8’h03 代表按键等,如表1所示,
表1
| 设备代码 | 设备说明 |
| 8’h01 | 数码管设备 |
| 8’h02 | 拨动开关设备 |
| 8’h03 | 按键设备 |
- 用户数据:第四字节到第n字节:由用户定义的数据,也可以是用户定义的包格式。
由于协议是由通讯双方共同约定的,所以FPGA 可以根据数据包里面的内容,迅速理解计算机的行为,作出相应的操作(读写操作)。开发者也可以根据上面的协议设计出在小范围内可以通用的通信程序。
注意:在制定协议时,既要考虑简介易用,又要兼顾可扩展性的特点。
3. FPGA 程序设计框架
根据设计内容及要求以及协议格式,程序组织框架如下:

图4
注:以上的设计只起到抛砖引玉的作用,在实际工作中可以根据需要定义合适的协议层及数据包的格式,以及每层的头部内容及格式。对数据的准确性有要求的还应添加各种FCS(frame check sequence)
在图4中,uart_pll, baud_rate_en,tx,rx_more_one 这4个模块和之前的设计相同,没有更改。在UART模块中增加了rx_eop 信号,该信号指示接收数据包的结束。rx_eop 信号的产生及握手如下:
reg [11:0] rx_eop_r = 0;
always @ (posedge sys_clk or posedge rst)
if(rst) rx_eop_r <= 0;
else
begin
if(rx_rdy) rx_eop_r <= 12'hfff;
else if(tx_bd_en) rx_eop_r <= rx_eop_r >> 1;
else if (rx_eop_r == 12'h001) rx_eop_r <= 0;
end
assign rx_eop = (rx_eop_r == 12'h001) ? 1'b1 : 1'b0;
上面的程序描述如下:
- rx_eop 信号产生机制
如果rx_rdy 信号产生, 表示接收到一个byte的数据,这时启动rx_eop_r = 12’hfff; 之后,每隔tx_bd_en 周期, rx_eop_r 右移一位。 如果期间有新的rx_rdy 信号出现(即接收到一个新的数据),rx_eop_r 重新置位12’hfff。 如果一直都没有rx_rdy 信号(12个 tx_band_en周期), 那么代表当前的数据包结束并 产生rx_eop 信号。
- rx_app: 模块是用来解析数据包格式的模块。解析后产生 pkg_data数据, pkg_type,pkg_op, pkg_valid 信号。用来指示当前需要执行那些操作(读FPGA ,还是写FPGA )。
- cmd decode : 是一个译码逻辑,产生每个设备的选通信号。
- seg_dev:是一个设备,当设备得到wr_seg_cmd 信号后,执行相关的操作。
- tx_app: 是计算机读命令需要处理的相关操作, 当得到read_sw_cmd信号后, 读取sw pin 的信息,然后发送数据到UART模块中。
4. 解析数据包
本设计中使用pkg_len,pkg_op,pkg_type, pkg_data分别代表数据包长度,操作类型,设备类型和用户数据。程序流程图如图5所示,

图5
根据流程图,编写Verilog RTL 代码如下:
`timescale 1ns / 1ps
module rx_app
(
input sys_clk,
input [7:0] rx_data,
input rx_rdy,
input rx_eop,
output reg [7:0] pkg_type = 8'hff,
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).
*/
reg [2:0] rx_st = 0;
reg [7:0] rx_len = 0;
reg [7:0] rx_cnt = 0;
reg rx_error = 0;
always @ (posedge sys_clk )
if(rx_eop | rst)
begin
pkg_type <= 8'hff;
pkg_valid <= 0;
rx_error <= 0;
rx_st <= 0;
end
else case (rx_st)
0:
begin
pkg_type <= 8'hff;
pkg_valid <= 0;
rx_error <= 0;
if(rx_rdy)
begin
pkg_data <= 0;
rx_len <= rx_data; // get packet length
rx_cnt <= 1;
if(rx_data[7:1] == 0) // packet lenth <= 2;
begin
rx_error <= 1;
rx_st <= 7;
end
else rx_st <= 1;
end
end
1: // read or write ?
begin
if(rx_rdy)
begin
rx_cnt <= 2;
pkg_op <= rx_data[0]; // get command, 00 => write; 01 => read
rx_st <= 2;
end
end
2:
begin
if(rx_rdy)
begin
rx_cnt <= 3;
pkg_type <= rx_data;
rx_st <= 3;
end
else if(rx_len == rx_cnt)
begin
pkg_valid <= 0;
rx_st <= 7;
end
end
3:
begin
if(rx_rdy)
begin
pkg_data <= {pkg_data[15:0], rx_data};
rx_cnt <= rx_cnt + 1;
end
else if(rx_len == rx_cnt)
begin
pkg_valid <= 1;
rx_st <= 7;
end
end
7:
begin
pkg_valid <= 0;
rx_st <= 7;
end
default: rx_st <= 0;
endcase
endmodule
5. 命令选通 cmd decoder (uart_app.v)
- 命令选通变量定义及编译码描述如下:
- 拨动开关设备选通: wire read_sw_cmd = (pkg_type == 8’h02) && (pkg_op == 1’b1) && (pkg_valid); 程序的含义如下:
设备号8’h02 ,读操作, 命令有效。如read_sw_cmd==1,则在tx_app 模块中,将拨动开关输入变量发送给计算机。
-
- 数码管设备选通:wire wr_seg_cmd = (pkg_type == 8’h01) && (pkg_op == 1’b0) && (pkg_valid); 程序的含义如下:
设备号8’h01 ,写操作, 命令有效。将计算机发送过来的数据(pkg_data)发送给数码管模块。
-
根据以上描述,编写RTL代码如下:
`timescale 1ns / 1ps
module uart_app #
(
parameter SIM_DEBUG = "FALSE" // "TRUE" "FALSE"
)
(
input inclk,
output [6:0] SEVEN_SEG,
output DP,
output [5:0] SEAT,
input [7:0] SW,
output tx,
input rx
);
parameter PARITY = "NONE";
parameter STOP_BIT = 1;
wire rst;
wire sys_clk;
wire tx_ack;
wire [7:0] tx_data;
wire tx_req;
wire [7:0] rx_data;
wire rx_rdy;
wire rx_eop;
uart
#(
.STOP_BIT (STOP_BIT),
.PARITY (PARITY)
)
uart_inst
(
.inclk (inclk),
.tx_req (tx_req),
.tx_data (tx_data),
.tx_ack (tx_ack),
.tx (tx),
.rx (rx),
.rx_data (rx_data),
.rx_rdy (rx_rdy),
.rx_eop (rx_eop),
.sys_clk (sys_clk),
.rst (rst)
);
wire [7:0] pkg_type;
wire pkg_op;
wire [23:0] pkg_data;
wire pkg_valid;
rx_app rx_app_inst
(
.sys_clk (sys_clk),
.rx_data (rx_data),
.rx_rdy (rx_rdy),
.rx_eop (rx_eop),
.pkg_type (pkg_type),
.pkg_op (pkg_op),
.pkg_data (pkg_data),
.pkg_valid (pkg_valid),
.rst (rst)
);
wire read_sw_cmd = (pkg_type == 8'h02) && (pkg_op == 1'b1) && (pkg_valid);
tx_app tx_app_inst
(
.sys_clk (sys_clk),
.tx_data (tx_data),
.tx_req (tx_req),
.tx_ack (tx_ack),
.read_sw_cmd(read_sw_cmd), //read data from device
.SW (SW),
.rst (rst)
);
wire wr_seg_cmd = (pkg_type == 8'h01) && (pkg_op == 1'b0) && (pkg_valid);
seg_dev seg_dev_inst
(
.sys_clk (sys_clk),
.wr_seg_data (pkg_data),
.wr_seg_cmd (wr_seg_cmd), // write data to device
.SEVEN_SEG (SEVEN_SEG),
.DP (DP),
.SEAT (SEAT),
.rst (rst)
);
endmodule
6. 拨动开关数据发送给计算机
本程序的目的是得到读取拨动开关的命令后, 将当前的拨动开关输入变量,由UART发送给计算机。
`timescale 1ns / 1ps
module tx_app
(
input sys_clk,
output reg [7:0] tx_data = 0,
output reg tx_req = 0,
input tx_ack,
input read_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
reg [2:0] tx_st = 0;
always @ (posedge sys_clk or posedge rst)
if(rst)
begin
tx_data <= 0;
tx_req <= 0;
tx_st <= 0;
end
else case(tx_st)
0:
begin
if(read_sw_cmd)
begin
tx_data <= key_r1;
tx_req <= 1;
tx_st <= 1;
end
end
1:
begin
if(tx_ack)
begin
tx_req <= 0;
tx_st <= 2;
end
end
2:
begin
if(!tx_ack)
begin
tx_st <= 0;
end
end
default: tx_st <= 0;
endcase
endmodule
工程中还有7段数码管的译码等程序,这里没有一一罗列,详细的程序可以下载工程代码并阅读。
7 . 实验与验证
- 在Quartus下编译整个UART_APP工程, 下载到pra006 开发板上。在Vivado下编译,并在PRX100D上运行,过程类似,这里不再累述。
- 打开串口调试工具,选择正确的串口号,并将计算机串口配置为115200,8,1,n 。
- 发送数据包,并观察实验现象,理解数据包的含义,例如:
- 发送数据包06 00 01 ab cd ef 。 其中 06 代表当前的包长度为6;00 代表写 操作;01 代表设备1 (数码管);ab cd ef 为发送到数码管上的数据。

图6
根据以上操作步骤,改变数据内容重新实验。

图5
-
- 计算机发送数据 03 01 02 。 其中03 代表当前的包长;01代表从FPGA 中读数据到计算机; 02 代表设备为拨动开关的状态。

图7
从图7 中可以看出,当计算机发送 03 01 02 后,FPGA 返回数据为03 。 代表当前的拨动开关最低2位为1。
8 总结
通过命令选通过程, 我们可以增加不同的模块,实现FPGA 和CPU 之间的通讯。 由于我们定义了数据包的格式, 使得整个代码可以比较容易的增加读写设备,实现多机通信,多人合作开发的特点,也使得应用极易扩展。
思考:如果想保留上次数码管的值,代码如何改动?
比如:
第1次发送: 06 00 01 ab cd ef
第2次发送: 04 00 01 01
第3次发送: 04 00 01 02
第4次发送: 04 00 01 03
希望在数码管上显示
ab cd ef
cd ef 01
ef 01 02
01 02 03
Quartus 代码:
[content_control]
[/content_control]