原理部分
- UART (Universal Asynchronous Receiver/Transmitter)是一种串行通信协议,广泛应用于计算机和外围设备之间的数据传输。它通过将数据分成一系列的位(bit)进行传输,从而实现设备间的通信。
- 在发送数据的时候将并行数据转换成串行数据来传输,在数据接受的时候将接受到的串行数据转换成并行数据,可以实现全双工传输和接收。
- 包括了RS232,RS449,RS423,RS422和RS485等标准。
- 我们使用的是最常见的RS232标准,包括9个引脚,如图所以:
- 数据位: 发送的数据位数,通常为5到8位。
- 波特率: 数据传输的速度,单位为比特每秒(bps)。常见的波特率有9600、19200、115200等。
- 奇偶校验: 用于检测数据传输中的错误,可以是奇校验、偶校验或无校验。
- 停止位: 用于标识数据包的结束,通常为1或2位。
uart 发送数据格式:
1个起始位 + 5-8个数据位 + 可选的奇偶校验位 + 1或2个停止位
起始位为低电平,停止位为高电平。
- 可以想到之前的FPGA点灯实验中8个LED灯的基础时间单元和波特率其实是一个东西,只不过点灯实验中是直接控制LED的亮灭时长,而UART通信中则是通过这个时间单元来控制数据的发送和接收速度。而用户自定义8种亮暗恰好对应8个数据位。
实验部分
- 串口通信模块设计的目的是用来发送数据的,因此需要有一个数据输入端口
- 串口通信,支持不同的波特率,所以需要一个波特率设置端口。
- 串口通信的本质就是将8位的并行数据通过一根信号线,在不同的时刻传输并行数据的不同位,通过多个时刻,最终将8位并行数据全部传出。
- 以1位低电平的起始位开始,接着是8位数据位,最后是1位高电平的停止位,总共10位数据。
- 控制信号,需要一个信号来告诉模块可以发送数据了(when to send),还需要一个信号来告诉模块当前的一批数据已经成功发送了。
Verilog代码实现,切记这里坑比较多,推荐自己写一遍
1
2
3
4
5
6
7
8
9
10
11
|
`timescale 1ns / 1ns
module uart_byte_tx(
input Clk,
input Reset_n,
input [7:0] Data,
input [2:0] Baud_set, // 波特率设置,8种波特率
input Send_en,
output uart_tx,
output Tx_done
)
|
首先计算一下计数器的最大值,已知我们的FPGA时钟频率是125MHz,假设我们选择的波特率是115200bps,那么每一位对应的时钟周期数就是125000000/115200=1085.069,遵从宁可多花时间也不丢数据的原则,向上取整为1086个时钟周期(实际波特率会略低于115200bps),需要11位的计数器来存储。
假设我们选择的波特率是300bps,那么每一位对应的时钟周期数就是125000000/300=416666.67,向上取整为416667个时钟周期,需要19位的计数器来存储。
- 使用一个19位的bps_DR寄存器来存储当前波特率对应的时钟周期数,[18:0]bps_DR
- 可以使用一个译码器将3位的波特率设置转换成对应的时钟周期数
1
2
3
4
5
6
7
8
9
10
|
reg [18:0] bps_DR;
always@(*)
case(Baud_set)
0: bps_DR = 125000000/ 9600;
1: bps_DR = 125000000/ 19200;
2: bps_DR = 125000000/ 38400;
3: bps_DR = 125000000/ 57600;
4: bps_DR = 125000000/ 115200;
default: bps_DR = 125000000/ 9600;
endcase
|
- 由于最大的bps_DR是19位,所以可以使用一个19位的计数器来进行计数[18:0] div_cnt,需要考虑到只有在Send_en状态时才进行计数。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
reg [18:0] div_cnt;
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
div_cnt <= 0;
else if(Send_en)
begin
if(div_cnt == bps_DR -1)
div_cnt <= 0;
else
div_cnt <= div_cnt + 1;
end
else
div_cnt <= 0;
|
- 由于一共有10位数据需要发送,所以可以使用一个4位的计数器来记录当前发送的数据是第几位[3:0] bps_cnt
1
2
3
4
5
6
7
8
|
reg [3:0] bps_cnt;
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
bps_cnt <= 10; // 初始状态设置为10而不是0,后面解释🙋♂️
else if(div_cnt == bps_DR -1)
bps_cnt <= bps_cnt + 1;
else if(bps_cnt == 10 && div_cnt == 2) // 如果写成Send_en呢,如果写成div_cnt == 1呢,后面再说🙋♂️
bps_cnt <= 0;
|
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
|
always@(posedge Clk or negedge Reset_n)
if(!Reset_n)
begin
uart_tx <= 1; // 空闲状态为高电平
Tx_done <= 1;
end
else begin
case(bps_cnt)
0: uart_tx <= 0;
1: uart_tx <= Data[0];
2: uart_tx <= Data[1];
3: uart_tx <= Data[2];
4: uart_tx <= Data[3];
5: uart_tx <= Data[4];
6: uart_tx <= Data[5];
7: uart_tx <= Data[6];
8: uart_tx <= Data[7];
9: uart_tx <= 1;
10: begin
uart_tx <= 1;
Tx_done <= 1;
end
default: uart_tx <= 1;
endcase
end
|
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
|
`timescale 1ns / 1ns
module uart_byte_tx_tb();
reg Clk;
reg Reset_n;
reg [7:0] Data;
reg Send_en;
reg [2:0] Baud_set;
wire uart_tx;
wire Tx_done;
uart_byte_tx uart_byte_tx(
.Clk(Clk),
.Reset_n(Reset_n),
.Data(Data),
.Baud_set(Baud_set),
.Send_en(Send_en),
.uart_tx(uart_tx),
.Tx_done(Tx_done)
)
initial Clk = 1;
always #4 Clk = !Clk; // notice that we use 125MHz clock
initial begin
Reset_n = 0;
Data = 0;
Send_en = 0;
Baud_set = 4; // 115200bps
# 201; // 我的理解是比周期正好多一点,防止不稳定
Reset_n = 1;
# 100;
Data = 8'h57;
Send_en = 1;
# 20; // 我的理解是保证Tx_done有充分的时间可以变成0,方便上升沿检测
@(posedge Tx_done);
Send_en = 0;
# 20000;
Data = 8'h75;
Send_en = 1;
# 20;
@(posedge Tx_done);
Send_en = 0;
# 20000;
$stop;
endmodule
|
- 经过仿真可以测得一个小字节的间隔是8.68us,接近理论值1000000/115200=8.68us,符合预期。
- 上述两个疑问的回答:
1.bps_cnt初始值设置为10是为了在reset的时候让uart_tx和Tx_done都处于空闲状态(高电平),即此时并没有数据发送。
2. 为什么不可以使用Send_en来控制bps_cnt变成0,因为在实际仿真的过程中,在Tx_done上升时,看似Send_en=0,实际使用的Send_en信号并不是0而是1,导致bps_cnt变成0,进入了发送状态,然而我们要求bsp_cnt保持10的状态直到下一次Send_en变成1才开始发送数据。
3. 为什么不可以写成div_cnt == 1,看似使用计数保证了有一个稳定的时间间隔,但是在实际仿真过程中,在Tx_done上升瞬间,和2一样,Send_en并不是0而是1,导致div_cnt并没有归零,而是继续计数,div_cnt=1,而在下一个时钟周期上升时,div_cnt恰好是1导致bps_cnt变成0,进入了发送状态,就和2一样了。
- 总结:深入分析仿真中实际的信号变化情况,考虑到信号的竞争与冒险问题,设计出合理的代码是关键。还有就是善用仿真。