1、实验目的
在逻辑派Z1开发板实现串口回环功能,上位机发送数据给逻辑派Z1开发板,逻辑派Z1开发板能够返回相应的数据。
2、实验原理
串口通信是一种常见的点对点通信方式,通常用于设备之间的数据传输。串口通信通过串行的方式逐位传输数据,即一位一位地将数据按顺序从发送端传输到接收端,具有结构简单、成本低的优点。
2.1串口通讯原理
串行传输:数据按位依次从发送端传输到接收端,而不是同时传输多个数据位。
数据帧格式:数据一般是按帧的方式传输,一个典型的数据帧包括:
- 起始位:用于通知接收端即将有数据到来。
- 数据位:实际传输的数据,通常是8位。
- 校验位(可选):用于检验数据是否在传输过程中出现错误。
- 停止位:用于通知接收端一帧数据已经结束。
2.2 通讯流程
发送方准备好数据并通过TXD发送;
接收方通过RXD接收数据;
数据在传输时,按预设的帧格式进行传输,包括起始位、数据位、校验位和停止位。
3、代码设计
uart_loop顶层代码如下:
module uart_loop(
input clk ,//外部50MHz时钟
input rst_n ,//系外部复位信号,低有效
//UART端口
input uart_rxd ,//UART接收
output uart_txd //UART发送
);
//parameter define
parameter SYS_CLK_FREQ = 50000000; //输入时钟50Mhz
parameter UART_BPS = 115200; //定义串口波特率
//wire define
wire uart_rx_finish ;//UART接收完成信号
wire [ 7: 0] uart_rx_data ;//UART接收数据
wire uart_tx_busy ;
//接收模块
uart_rx #(
.SYS_CLK_FREQ (SYS_CLK_FREQ ),
.UART_BPS (UART_BPS )
)
uart_rx_inst(
.clk (clk ),
.rst_n (rst_n ),
.uart_rxd (uart_rxd ),
.uart_rx_finish (uart_rx_finish ),
.uart_rx_data (uart_rx_data )
);
//发送模块
uart_tx #(
.SYS_CLK_FREQ (SYS_CLK_FREQ ),
.UART_BPS (UART_BPS )
)
uart_tx_inst(
.clk (clk ),
.rst_n (rst_n ),
.uart_tx_req (uart_rx_finish ),
.uart_tx_data (uart_rx_data ),
.uart_txd (uart_txd ),
.uart_tx_busy (uart_tx_busy )
);
endmodule
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
在顶层模块中我们将串口发送模块(uart_tx)与串口接收模块(uart_rx)相连接,串口接收模块负责按照uart数据帧结构将串口数据解析为单个字节的uart_rx_data,同时输出一个单字节接收完成的标志信号uart_rx_finish。串口发送模块根据这些信号将PC端发送的数据再重新发送给PC,实现串口数据回环的效果,串口发送模块(uart_tx)与串口接收模块(uart_rx)我们会在下文详细介绍。
uart_rx串口接收模块代码如下:
module uart_rx(
input clk ,
input rst_n ,
input uart_rxd ,//UART接收端口
output reg uart_rx_finish ,//一帧数据接收完成
output reg [ 7: 0] uart_rx_data //解析后的数据
);
parameter SYS_CLK_FREQ = 50000000;
parameter UART_BPS = 115200;
localparam BAUD_CNT_MAX = SYS_CLK_FREQ/UART_BPS; //根据时钟和波特率计算波特计数器的值
//reg define
reg uart_rxd_d0 ;
reg uart_rxd_d1 ;
reg uart_rxd_d2 ;
reg work_en ;//接收过程标志信号
reg [ 3: 0] rx_data_cnt ;//接收数据计数器
reg [ 15: 0] baud_cnt ;//波特率计数器
reg [ 7: 0] rx_data_reg ;//接收数据寄存器
wire start_flag ;
//检测下降沿(起始位)
assign start_flag = uart_rxd_d2 & (~uart_rxd_d1) & (~work_en);
//多级寄存器缓存消除亚稳态
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
uart_rxd_d2 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
uart_rxd_d2 <= uart_rxd_d1;
end
end
//tx_flag
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
work_en <= 1'b0;
else if(start_flag) //检测到起始位
work_en <= 1'b1;
else if((rx_data_cnt == 4'd9) && (baud_cnt == BAUD_CNT_MAX/2 - 1'b1))
work_en <= 1'b0;
else
work_en <= work_en;
end
//波特率的计数器赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
baud_cnt <= 16'd0;
else if(work_en) begin
if(baud_cnt < BAUD_CNT_MAX - 1'b1)
baud_cnt <= baud_cnt + 16'b1;
else
baud_cnt <= 16'd0;
end
else
baud_cnt <= 16'd0;
end
//接收bit计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_data_cnt <= 4'd0;
else if(work_en) begin
if(baud_cnt == BAUD_CNT_MAX - 1'b1)
rx_data_cnt <= rx_data_cnt + 1'b1;
else
rx_data_cnt <= rx_data_cnt;
end
else
rx_data_cnt <= 4'd0; //接收完成计数器清零
end
//接收端口赋值
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
rx_data_reg <= 8'b0;
else if(work_en) begin
if(baud_cnt == BAUD_CNT_MAX/2 - 1'b1) begin //在波特计数器中间寄存数据,以得到稳定数据
if (rx_data_cnt >= 4'd1 && rx_data_cnt <= 4'd8) begin
rx_data_reg[rx_data_cnt - 1] <= uart_rxd_d2; // 动态索引更新对应位
end
end
else
rx_data_reg <= rx_data_reg;
end
else
rx_data_reg <= 8'b0;
end
//给出数据和有效信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
uart_rx_finish <= 1'b0;
uart_rx_data <= 8'b0;
end
else if(rx_data_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX/2 - 1'b1) begin
uart_rx_finish <= 1'b1 ; //拉高接收完成信号
uart_rx_data <= rx_data_reg; //取出解析完成数据
end
else begin
uart_rx_finish <= 1'b0;
uart_rx_data <= uart_rx_data;
end
end
endmodule
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
第 27 行是一个经典的边沿检测电路,通过检测串口接收端 uart_rxd 的下降沿来捕获起始位。一旦检测到起始位,输出一个时钟周期的脉冲 start_flag,并进入串口接收过程。串口接收状态用 work_en 来标志,work_en为高标志着串口接收过程正在进行,此时启动系统时钟计数器 baud_cnt 与接收数据计数器 rx_data_cnt。这里需要明白,我们不知道uart_rxd 信号何时会跳变,也就是说这是一个异步信号,异步信号会带来亚稳态问题。因此,对于异步信号,我们需要进行异步处理,常用的异步处理方式是打拍处理。uart_rxd_d0 是打第一拍,uart_rxd_d1 打第二拍,uart_rxd_d2 打第三拍。通常打两拍就基本上能避免亚稳态问题,但这里使用了三级寄存器来确保更高的稳定性。
第 12 行的公式 BAUD_CNT_MAX = SYS_CLK_FREQ / UART_BPS 可知,BAUD_CNT_MAX 为当前波特率下,串口传输一位所需要的系统时钟周期数。因此,baud_cnt 从零计数到 BAUD_CNT_MAX - 1 时,串口刚好完成一位数据的传输。由于接收数据计数器 rx_data_cnt 在每次 baud_cnt 计数到 BAUD_CNT_MAX - 1 时加 1,因此可以通过 rx_data_cnt 的值来判断串口当前传输的是第几位数据。
第 53 行到第 113 行,根据 baud_cnt 的值将 uart_rxd 接收端口的数据寄存到接收数据寄存器 rx_data_reg 对应的位,从而实现接收数据的串并转换。具体来说,当 baud_cnt 计数到 BAUD_CNT_MAX / 2 - 1 时寄存接收端口数据,这是因为计数到数据中间时的采样结果最稳定。当rx_data_cnt跳变到9时,一帧串口数据解析完成,此时拉高一个时钟周期的uart_rx_finish,并将数据赋值给uart_rx_data,同时work_en拉低,一帧串口数据解析完成
uart_tx串口发送模块代码如下:
module uart_tx(
input clk ,
input rst_n ,
input uart_tx_req ,//发送使能请求
input [ 7: 0] uart_tx_data ,//UART要发送的数据
output reg uart_txd ,//UART发送端口
output reg uart_tx_busy //发送忙
);
//parameter define
parameter SYS_CLK_FREQ = 50000000;
parameter UART_BPS = 115200;
localparam BAUD_CNT_MAX = SYS_CLK_FREQ/UART_BPS; //根据时钟和波特率计算波特计数器的值
//reg define
reg [ 7: 0] tx_data_reg ;//发送数据寄存器
reg [ 3: 0] tx_data_cnt ;//发送数据计数器
reg [ 15: 0] baud_cnt ;//波特率计数器
//当uart_tx_en为高时,寄存输入的并行数据,并拉高发送忙信号
always @(posedge clk or negedge rst_n) begin
if(!rst_n) begin
tx_data_reg <= 8'b0;
uart_tx_busy <= 1'b0;
end
else if(uart_tx_req == 'd1) begin
tx_data_reg <= uart_tx_data;
uart_tx_busy <= 1'b1;
end
else if(tx_data_cnt == 4'd9 && baud_cnt == BAUD_CNT_MAX - 1) begin
tx_data_reg <= 8'b0;
uart_tx_busy <= 1'b0;
end
else begin
tx_data_reg <= tx_data_reg;
uart_tx_busy <= uart_tx_busy;
end
end
//波特率计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
baud_cnt <= 16'd0;
else if(uart_tx_req)
baud_cnt <= 16'd0;
else if(uart_tx_busy) begin
if(baud_cnt < BAUD_CNT_MAX - 1'b1)
baud_cnt <= baud_cnt + 16'b1;
else
baud_cnt <= 16'd0;
end
else
baud_cnt <= 16'd0;
end
//发送位计数器
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
tx_data_cnt <= 4'd0;
else if(uart_tx_req)
tx_data_cnt <= 16'd0;
else if(uart_tx_busy) begin
if(baud_cnt == BAUD_CNT_MAX - 1'b1)
tx_data_cnt <= tx_data_cnt + 1'b1;
else
tx_data_cnt <= tx_data_cnt;
end
else
tx_data_cnt <= 4'd0; //发送完成清零
end
//发送端口赋值
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
uart_txd <= 1'b1; // 空闲
end else if (uart_tx_busy) begin
if (tx_data_cnt == 4'd0)
uart_txd <= 1'b0; // 发送起始位
else if (tx_data_cnt >= 4'd1 && tx_data_cnt <= 4'd8)
uart_txd <= tx_data_reg[tx_data_cnt - 1]; // 发送数据位
else if (tx_data_cnt == 4'd9)
uart_txd <= 1'b1; // 停止位
else
uart_txd <= 1'b1; // 空闲
end else begin
uart_txd <= 1'b1;
end
end
endmodule
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
串口发送模块与串口接收模块实现思想大致相同。相信读者在理解了uart协议的接收过程后,也能够看懂发送过程的逻辑。我们这里对代码中的关键信号进行说明。
在代码中,uart_tx_req 信号用于触发数据发送过程。当检测到 uart_tx_req 的上升沿后,模块将 uart_tx_data 端口上的待发送数据寄存到 tx_data_reg 中,并同时将 uart_tx_busy 信号拉高,表示发送器正处于繁忙状态。这意味着外部模块需要等待当前发送过程结束后,即 uart_tx_busy 信号再次变为低电平,才能启动新的发送请求。
baud_cnt 用于计算一个波特率周期内的时钟脉冲。当发送使能信号 uart_tx_req 为高时,波特率计数器会被清零,以开始一个新的数据帧的发送。在 uart_tx_busy 信号为高电平期间,波特率计数器会在每个时钟周期递增,直到达到一个完整的波特率周期(BAUD_CNT_MAX - 1),然后清零,准备发送下一个数据位。
tx_data_cnt 用于跟踪当前正在发送的数据位。当发送使能信号 uart_tx_req 为高时,发送位计数器会被清零。在 uart_tx_busy 信号为高电平期间,当波特率计数器达到 BAUD_CNT_MAX - 1 时,发送位计数器递增,表示下一个数据位将要发送。
uart_txd 是串口发送的输出信号。在复位时,uart_txd 被设置为高电平,表示空闲状态。当 uart_tx_busy 信号为高电平时,根据 tx_data_cnt 的值,uart_txd 会依次输出起始位(低电平)、数据位和停止位(高电平)。具体而言:当tx_data_cnt = 0:输出起始位,高电平拉低(低电平)。当tx_data_cnt = 1 至 8:依次输出数据位,这些位从 tx_data_reg 中读取。当tx_data_cnt = 9:输出停止位,低电平拉高(高电平)。
为了确保发送过程的可靠性,当 tx_data_cnt 计数到 9 并且 baud_cnt 也达到 BAUD_CNT_MAX - 1 时,表示一个完整的数据帧发送完成,uart_tx_busy 信号被拉低,模块进入空闲状态。
在整个发送过程中,uart_tx_busy 信号保持高电平,表示发送器正在忙碌。当发送完成时,uart_tx_busy 信号被拉低,模块进入空闲状态,准备接收下一个发送请求。
接下来我们直接将其下载进入板卡观察其实验现象。
4、实验现象
逻辑派 Z1 开发板搭载了 CH747T 芯片, CH374T 是一款功能强大的接口芯片, 集成了 JTAG 和 UART 两种重要通信方式, 分别用于调试、 以及数据传输。 根据原理图所示, 其 TXD1 与 RXD1 引脚分别与 PGC4KD-6ILPG144 的 32、33 号引脚相连。 我们根据原理图, 设置以下 io 约束:
create_clock -name {clk} [get_ports {clk}] -period {20.000} -waveform {0.000 10.000}
define_attribute {p:uart_txd} {PAP_IO_DIRECTION} {OUTPUT}
define_attribute {p:uart_txd} {PAP_IO_LOC} {33}
define_attribute {p:uart_txd} {PAP_IO_VCCIO} {1.2}
define_attribute {p:uart_txd} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:uart_txd} {PAP_IO_DRIVE} {2}
define_attribute {p:uart_txd} {PAP_IO_NONE} {TRUE}
define_attribute {p:uart_txd} {PAP_IO_SLEW} {SLOW}
define_attribute {p:clk} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:clk} {PAP_IO_LOC} {5}
define_attribute {p:clk} {PAP_IO_VCCIO} {1.2}
define_attribute {p:clk} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:clk} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:uart_rxd} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:uart_rxd} {PAP_IO_LOC} {32}
define_attribute {p:uart_rxd} {PAP_IO_VCCIO} {1.2}
define_attribute {p:uart_rxd} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:uart_rxd} {PAP_IO_PULLUP} {TRUE}
define_attribute {p:rst_n} {PAP_IO_DIRECTION} {INPUT}
define_attribute {p:rst_n} {PAP_IO_LOC} {20}
define_attribute {p:rst_n} {PAP_IO_VCCIO} {1.2}
define_attribute {p:rst_n} {PAP_IO_STANDARD} {LVCMOS12}
define_attribute {p:rst_n} {PAP_IO_PULLUP} {TRUE}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
将程序编译生成比特流后, 只需要将板卡与电脑使用一根 usb 转 type-c 线材进行连接, 即可实现供电、 调试、 串口通信功能。 接下来将程序下载进入开发板, 打开上位机串口调试助手, 设置波特率为 115200, 打开串口, 发送 1234567890。观察到板卡返回相同数据, 串口回环验证成功。如下图所示: