1、串口基础知识
1.1、串口介绍
串口是指外设和处理器之间通过数据信号线、地线和控制线等,按位进行传输数据的一种通讯方式。尽管传输速度比并行传输低。但串口可以在使用一根线发送数据的同时用另一根线接收数据。这种通信方式使用的数据线少,在远距离通信中可以节约通信成本。串口通信最重要的参数是波特率、数据位、停止位和奇偶校验位,这些参数在两个通信端口之间必须一致。
1.2、串口通信参数介绍
串口通信参数包括波特率(Baud Rate)、数据位(Data Bits)、校验位(Parity Bits)、停止位(Stop Bits)等。这些参数描述了传输数据的基本规格。例如,波特率定义了数据传输的速率,数据位确定每个数据字节中包含的位数,校验位用于数据的差错检测,停止位表示数据传输结束的标志等。
波特率: 衡量通信速度的参数,它表示每秒钟传送的 bit 的个数。
数据位: 衡量通信中实际数据位的参数,表示一个信息包里包含的数据位的个数。
停止位: 用于表示单个信息包的最后位,典型值为 1、1.5 和 2 位。由于数据是在传输线上传输的,每个设备都有自己的时钟,很有可能在通信过程中出现不同步,停止位不仅仅表示传输的结束,还能提供校正时钟同步的机会。停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率也越慢。
奇偶检验位: 表示一种简单的检查错误的方式。
关于更为详细的介绍请搜索百度。
1.3、串口工作模式
串口可以工作在异步全双工 、同步半双工 和单线半双工模式下。
异步全双工:
异步:指的是通信双方不需要共享一个共同的时钟信号。数据传输的起始和结束通过特定的帧格式、起始位、停止位或者特定的同步字符来标识,允许发送和接收端各自独立工作,不必严格同步它们的时钟。这使得异步通信更加灵活,但可能引入一些开销,如额外的同步信息。
全双工:表示数据可以同时在两个方向上传输。也就是说,通信双方都能同时发送和接收数据,如同有两个独立的通道,互不影响,提高了通信效率,适合于需要高速、双向实时通信的场景,如以太网通信。 同步半双工:
同步:在同步通信中,发送和接收设备之间需要一个共同的时钟信号,用以同步数据的传输节奏。这通常通过一个时钟引脚或在数据流中嵌入同步位来实现。同步通信可以提供更高的数据传输速率,因为它减少了用于数据包同步的开销。
半双工:半双工模式允许数据在两个方向上传输,但不能同时进行。通信双方需要轮流进行发送和接收。例如,经典的RS-485总线在半双工模式下,一次只能进行发送或接收操作,需要通过控制线切换方向。这种方式适用于对实时性要求不高、双向通信但不需要同时收发的场合。
单线半双工:
- 单线半双工:这种模式通常特指使用一根信号线既用于发送也用于接收数据,但不能同时进行,属于半双工通信的一种特殊形式。
1.4、串口通信协议
串口通信协议定义了在串口上进行数据交换的规则和格式。常见的串口通信协议包括ASCII协议、Modbus协议、RS-232协议等。协议规定了数据的帧结构、数据格式、校验方式等,确保发送和接收双方按照相同的规则进行数据交换,从而实现数据的正确传输和解析。
串口通信是一位一位地传输,每传输一个字符总是以起始位开始,以停止位结束,字符之间没有固定的时间间隔要求。每一个字符的前面都有一位起始位(低电平),后面由 7 位数据位组成,接着是一位校验位,最后是停止位。停止位后面是不定长的空闲位,停止位和空闲位都规定为高电平。帧格式如图所示:
串口通信是一位一位地传输,每传输一个字节总是以起始位开始,以停止位结束,字符之间没有固定的时间间隔要求。每一个字符的前面都有一位起始位(低电平),后面由 8 位数据位组成,如果开启了校验位,则最后一位数据位是校验位,最后是停止位。停止位后面是不定长的空闲位,停止位和空闲位都规定为高电平。
2、串口原理图
逻辑派配套的下载器提供了一个虚拟串口用来于上位机进行数据交互。也可以使用一个TTL转USB来与上位机进行数据交互,一般可以采用 CH340 的方案。
3、实战任务
通过 PC 串口助手向逻辑派 FPGA-G1 开发板发送数据(不靠买板赚钱,以培养中国工程师为己任),然后将接收到的数据经过处理后转发到上位机,以验证我们的串口回环程序是否正确。
4、系统框图
5、时序图
以单字节的 UART 发送时序为例,当 tx_data_ready 为高电平时,表示串口处于空闲状态,此时可以将数据输入到 tx_in_data,例如输入一个数据 0xB2。当 tx_data_ready 被拉低时,表示串口开始发送数据,模块将输入的数据转换为串行信号并输出。此时,data_bit_count 为 0 时输出起始位,当 data_bit_count 从 1 增加到 7 时,依次输出 B2 的每个数据位。最后,当 data_bit_count 达到 8 时,输出停止位,表示数据传输结束。在多字节发送时,串口会重复上述单字节的发送过程,依次处理每个字节,确保数据流的连续性和完整性。
由于数据接收信号是异步的,d1和d2对d3进行了打拍处理,以避免产生亚稳态。(什么是亚稳态:指触发器在某个时间段内无法稳定到正确的状态值)其根本原因是在时钟上升沿的建立时间或保持时间内其输入数据的不稳定,导致寄存器输出数据不稳定。虽然亚稳态过后,数据会在下一个时钟沿到来前稳定,但为了消除亚稳态现象,所有会采用了打拍的方法来解决。如下图所示:
6、程序编写
6.1 顶层模块
我们这里首先写发送的顶层模块(uart_loop.v)
module uart_loop
(
input sys_clk , //全局时钟输入
input sys_rst_n , // 全局复位输入,低电平有效
input rx , //串口接收引脚
output tx //串口发送引脚
);
wire [7:0] tx_data; //FIFO写入到串口发送模块的数据
wire [7:0] rx_data; //串口接收模块写入到FIFO的数据
//FIFO状态变量
wire rx_full; //FIFO满标志位
wire rx_empty; //FIFO空标志位
wire tx_done; //串口发送完成标志
wire rx_done; //串口接收完成标志
//串口发送模块
uart_tx uart_tx_inst
(
.tx_clk (sys_clk ), //串口发送时钟
.tx_rst_n (sys_rst_n ), //串口发送复位信号
.tx_in_data (tx_data ), //串口发送输入数据
.tx_enable (~rx_empty ), //串口发送使能
.tx_out_data (tx ), //串口发送输出数据
.tx_data_ready (tx_done ) //串口发送完成标志
);
//串口接收模块
uart_rx uart_rx_inst
(
.rx_clk (sys_clk ), //串口接收时钟
.rx_rst_n (sys_rst_n ), //串口接收复位信号
.rx_in_data (rx ), //串口接收输入数据
.rx_data (rx_data ), //串口接收数据
.rx_done (rx_done ) //串口接收完成标志
);
//串口接收FIFO
fifo_sc_top rx_fifo_inst
(
.Clk (sys_clk ), //input Clk
.Reset (~sys_rst_n ), //input Reset
.Data (rx_data ), //input [7:0] Data
.WrEn (rx_done ), //input WrEn
.RdEn (tx_done ), //input RdEn
.Q (tx_data ), //output [7:0] Q
.Almost_Empty (rx_empty ), //output Almost_Empty
.Full (rx_full ), //output Full
.Empty ( ) //output Empty
);
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
6.2 串口发送模块
串口发送的模块模块(uart_tx.v)
module uart_tx (
input tx_clk, // 串口发送时钟
input tx_rst_n, // 串口复位,低电平有效
input [7:0] tx_in_data, // 输入的 8 位数据
input tx_enable, // 串口发送使能信号
output reg tx_data_ready, // 发送就绪信号
output reg tx_out_data // 串口发送数据
);
// 参数定义
parameter CLK_FREQ = 26'd50_000_000, // 系统时钟频率 50MHz
UART_BPS = 17'd115200; // UART 波特率 115200
parameter START_BIT = 1'b0; // 起始位
parameter END_BIT = 1'b1; // 停止位
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS;
// reg define
reg [12:0] baud_cnt; // 波特率计数器
reg [3:0] data_bit_count; // 数据位计数器
reg transmission_ready; // 发送工作使能信号
// 发送工作使能信号逻辑
always @(posedge tx_clk or negedge tx_rst_n) begin
if (!tx_rst_n)
transmission_ready <= 1'b0;
else if (tx_enable)
transmission_ready <= 1'b1; // 发送使能时,设置为有效
else if ((baud_cnt == 13'd1) && (data_bit_count == 4'd9))
transmission_ready <= 1'b0; // 发送完成后,清除使能信号
else
transmission_ready <= transmission_ready;
end
// 波特率计数器
always @(posedge tx_clk or negedge tx_rst_n) begin
if (!tx_rst_n)
baud_cnt <= 13'b0;
else if ( (baud_cnt == BAUD_CNT_MAX - 1'd1))
baud_cnt <= 13'b0; // 发送无效或计数到达最大值时清零
else if (transmission_ready)
baud_cnt <= baud_cnt + 1'b1; // 计数器加一
else
baud_cnt <= baud_cnt;
end
always @(posedge tx_clk or negedge tx_rst_n) begin
if (!tx_rst_n)
data_bit_count <= 4'b0;
else if(baud_cnt == 13'd1) begin
if (transmission_ready)
data_bit_count <= data_bit_count + 1'b1; //每波特率计数器的第一个时钟周期,数据位加一
else if(data_bit_count == 4'd9)
data_bit_count <= 4'd0; //发送完时,数据位清零
end
else
data_bit_count <= data_bit_count;
end
// UART 数据发送逻辑
always @(posedge tx_clk or negedge tx_rst_n) begin
if (!tx_rst_n)
tx_out_data <= 1'b1; // 空闲状态为高电平
else if (baud_cnt == 13'd1 ) begin
case (data_bit_count)
4'd0: tx_out_data <= START_BIT; // 起始位
4'd1: tx_out_data <= tx_in_data[0]; // 数据最低位
4'd2: tx_out_data <= tx_in_data[1];
4'd3: tx_out_data <= tx_in_data[2];
4'd4: tx_out_data <= tx_in_data[3];
4'd5: tx_out_data <= tx_in_data[4];
4'd6: tx_out_data <= tx_in_data[5];
4'd7: tx_out_data <= tx_in_data[6];
4'd8: tx_out_data <= tx_in_data[7]; // 数据最高位
4'd9: tx_out_data <= END_BIT; // 停止位
default: tx_out_data <= 1'b1; // 默认状态
endcase
end
end
// 发送就绪信号逻辑
always @(posedge tx_clk or negedge tx_rst_n) begin
if (!tx_rst_n)
tx_data_ready <= 1'b0; // 初始化为未就绪
else if ( baud_cnt == 1'd1 && data_bit_count == 4'd9)
tx_data_ready <= 1'b1; // 发送完成,设置发送就绪信号为有效
else
tx_data_ready <= 1'b0; // 发送中
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
6.3 串口接收模块
串口接收的模块模块(uart_rx.v)
module uart_rx(
input rx_clk , // 系统时钟
input rx_rst_n , // 系统复位,低有效
input rx_in_data , // UART 接收端口
output reg rx_done , // UART 接收完成信号
output reg [7:0] rx_data // UART 接收到的数据
);
// 参数定义
parameter CLK_FREQ = 26'd50_000_000; // 系统时钟频率 50MHz
parameter UART_BPS = 17'd115200; // UART 波特率 115200
localparam BAUD_CNT_MAX = CLK_FREQ / UART_BPS; // 波特率计数最大值
// 寄存器定义
reg rx_flag;
reg rxd_d1, rxd_d2, rxd_d3; // 数据同步寄存器
reg work_en; // 接收数据工作使能信号
reg bit_flag; // 中间采样标志
reg [12:0] baud_cnt; // 波特率计数器
reg [3:0] bit_cnt; // 数据位计数器
reg [7:0] rx_data_buf; // 接收数据
//捕获接收端口下降沿(即起始位的下降沿),得到一个时钟周期的脉冲信号,表示接收开始
assign start_en = ~rxd_d2 && rxd_d3;
// 数据同步寄存器
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n) begin
rxd_d1 <= 1'b1; // 默认高电平
rxd_d2 <= 1'b1;
rxd_d3 <= 1'b1;
end
else begin
rxd_d1 <= rx_in_data; // 接收端口数据同步到寄存器
rxd_d2 <= rxd_d1; // 上一个寄存器的数据传递到下一个寄存器
rxd_d3 <= rxd_d2; // 将接收到的数据同步到三级寄存器
end
end
// 接收数据工作使能信号
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
work_en <= 1'b0;
else if (start_en)
work_en <= 1'b1; // 起始位的下降沿触发时,启动接收工作
else if (((bit_cnt == 4'd8) && bit_flag) || rx_done)
work_en <= 1'b0; // 数据接收完成后,停止接收工作
else
work_en <= work_en;
end
// 波特率计数器
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
baud_cnt <= 13'b0;
else if (work_en == 1'b0 || (baud_cnt == BAUD_CNT_MAX - 1))
baud_cnt <= 13'b0; // 不工作时或者计数器达到最大计数值时重置
else if (work_en)
baud_cnt <= baud_cnt + 1'b1;
else
baud_cnt <= baud_cnt;
end
// 中间采样标志
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
bit_flag <= 1'b0;
else if (baud_cnt == BAUD_CNT_MAX / 2 - 1)
bit_flag <= 1'b1; // 当计数器达到半波特率周期时,设置采样标志为有效,保证稳态
else
bit_flag <= 1'b0; // 在其他情况下,标志为无效
end
// 数据位计数器
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
bit_cnt <= 4'b0;
else if (bit_flag)begin
if(bit_cnt == 4'd8) // 接收到 8 位数据后,重置计数器
bit_cnt <= 4'b0;
else
bit_cnt <= bit_cnt + 4'b1; // 每次采样一个数据位,计数器加一
end
else
bit_cnt <= bit_cnt;
end
// 接收数据移位
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
rx_data_buf <= 8'b0;
else if (bit_flag) begin
case (bit_cnt)
4'd1: rx_data_buf[0] <= rxd_d3; // 接收到的每一位数据
4'd2: rx_data_buf[1] <= rxd_d3;
4'd3: rx_data_buf[2] <= rxd_d3;
4'd4: rx_data_buf[3] <= rxd_d3;
4'd5: rx_data_buf[4] <= rxd_d3;
4'd6: rx_data_buf[5] <= rxd_d3;
4'd7: rx_data_buf[6] <= rxd_d3;
4'd8: rx_data_buf[7] <= rxd_d3;
default : rx_data_buf <= 8'b0; // 默认状态
endcase
end
end
// 数据接收完成标志
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
rx_flag <= 1'b0;
else if (bit_cnt == 4'd8 && bit_flag)
rx_flag <= 1'b1; // 接收到 8 位数据时,设置完成标志
else
rx_flag <= 1'b0;
end
// 输出完整的 8 位有效数据,当数据接收完成后,将接收的数据从缓存传递到输出端
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
rx_data <= 8'b0;
else if (rx_flag)
rx_data <= rx_data_buf; // 完成接收时,将接收到的数据输出
end
// 接收完成信号:当数据接收完成时,设置接收完成标志
always @(posedge rx_clk or negedge rx_rst_n) begin
if (!rx_rst_n)
rx_done <= 1'b0;
else
rx_done <= rx_flag; // 接收完成时,设置为有效
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
6.4 FIFO 配置
由于我们这里用到了一个FIFO来做为缓存,故此我们需要对FIFO进行配置,配置如下图所示:
6.5 仿真文件
`timescale 1ns/1ns //仿真的单位/仿真的精度
module uart_loop_mod();
//reg define
reg sys_clk ; //时钟信号
reg sys_rst_n; //复位信号
reg uart_rxd ; //UART接收端口
//wire define
wire uart_txd ; //UART发送端口
//在对 FIFO 或者 SP IP 核等(或其它在底层转换 GSR 时调用了全局复位的 IP/原语)进行仿真时,在仿真代码中需要添加如下这一段代码,否则联合仿真时就会报错。
GSR GSR(.GSRI(1'b1));
//50Mhz的时钟,周期则为1/50Mhz=20ns,所以每10ns,电平取反一次
always #(10) sys_clk = ~sys_clk;
//发送8'h55 8'b0101_0101 8'hAA 8'b1010_1010
initial begin
sys_clk <= 1'b0;
sys_rst_n <= 1'b0;
uart_rxd <= 1'b1;
#200
sys_rst_n <= 1'b1;
#1000
uart_rxd <= 1'b0; //起始位
#8680
uart_rxd <= 1'b0; //D0
#8680
uart_rxd <= 1'b1; //D1
#8680
uart_rxd <= 1'b0; //D2
#8680
uart_rxd <= 1'b1; //D3
#8680
uart_rxd <= 1'b0; //D4
#8680
uart_rxd <= 1'b1; //D5
#8680
uart_rxd <= 1'b0; //D6
#8680
uart_rxd <= 1'b1; //D7
#8680
uart_rxd <= 1'b1; //停止位
#8680
uart_rxd <= 1'b1; //空闲状态
#1000
uart_rxd <= 1'b0; //起始位
#8680
uart_rxd <= 1'b1; //D0
#8680
uart_rxd <= 1'b0; //D1
#8680
uart_rxd <= 1'b1; //D2
#8680
uart_rxd <= 1'b0; //D3
#8680
uart_rxd <= 1'b1; //D4
#8680
uart_rxd <= 1'b0; //D5
#8680
uart_rxd <= 1'b1; //D6
#8680
uart_rxd <= 1'b0; //D7
#8680
uart_rxd <= 1'b1; //停止位
#8680
uart_rxd <= 1'b1; //空闲状态
end
//例化顶层模块
uart_loop u_uart_loop(
.sys_clk (sys_clk ),
.sys_rst_n (sys_rst_n),
.rx (uart_rxd ),
.tx (uart_txd )
);
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
接下来打开 Modelsim 软件对代码进行仿真,需要添加文件如下图所示:
首先来说接收模块:
接收端口需要捕获一个下降沿(即起始位的下降沿),生成一个时钟周期的脉冲信号,表示接收过程开始,并输出该脉冲信号 start_en。此时,接收模块进入串口接收过程。在接收过程中,当 bit_flag 被拉高时,接收模块开始采样当前输入信号的电平。为了保证数据的稳定性和准确性,采样是在每个数据位的中点进行取样。
补充:
为什么使用中点取样:由于 UART 信号的电平变化可能会受到噪声或不稳定因素的影响,直接在信号的起始点或结束点采样可能会导致错误。通过在每个数据位的中点采样,可以确保信号处于稳定状态,从而提高数据接收的准确性。
为什么要在中点取样:为了确保接收到的数据稳定。接收模块不是在每个数据位的开始处立即采样,而是在每个波特率周期的中点进行采样。这个时点由 bit_flag 信号控制。在 bit_flag 被拉高时,接收模块开始采样输入信号的电平,这种中点取样方式能有效避免由于信号波动或噪声引起的错误,确保接收到的信号平稳可靠。如下图所示:
中点取样,如下图所示:
串口的整个接收过程的仿真图如下所示:
发送模块:
我们是从FIFO里面读取数据然后通过串口进行发送,当data_bit_count = 0时为起始位,1-9 为数据位,第10位为停止位。如下图所示:
7、I/O 引脚绑定
在仿真验证完成后,接下来使用 Gowin 对时钟约束和引脚进行分配并上板验证。本实验中,系统时钟、复位按键、串口的RX和TX 管脚分配如下表所示:
信号 | 方向 | 引脚 | 端口作用 | 电平标准 |
---|---|---|---|---|
sys_clk | input | T7 | 时钟 | LVCMOS33 |
sys_rst_n | input | F10 | 复位 | LVCMOS33 |
TX | output | F12 | tx | LVCMOS33 |
RX | input | F13 | rx | LVCMOS33 |
Gowin 软件中 I/O Constraints 界面如下图所示:
9、程序下载
连接开发板的下载器,将码流文件下载到开发板后,使用下载器上面的虚拟串口进行通讯,在串口输入端 不靠买板赚钱,以培养中国工程师为己任 ,然后在串口软件会接收到什么数据,如下图所示: