Featured image of post FPGA2 — 基于FPGA的串口发送实验以及verilog实现

FPGA2 — 基于FPGA的串口发送实验以及verilog实现

原理部分

  • UART (Universal Asynchronous Receiver/Transmitter)是一种串行通信协议,广泛应用于计算机和外围设备之间的数据传输。它通过将数据分成一系列的位(bit)进行传输,从而实现设备间的通信。
  • 在发送数据的时候将并行数据转换成串行数据来传输,在数据接受的时候将接受到的串行数据转换成并行数据,可以实现全双工传输和接收。
  • 包括了RS232,RS449,RS423,RS422和RS485等标准。
  • 我们使用的是最常见的RS232标准,包括9个引脚,如图所以:
RS232引脚图
  • 数据位: 发送的数据位数,通常为5到8位。
  • 波特率: 数据传输的速度,单位为比特每秒(bps)。常见的波特率有9600、19200、115200等。
  • 奇偶校验: 用于检测数据传输中的错误,可以是奇校验、偶校验或无校验。
  • 停止位: 用于标识数据包的结束,通常为1或2位。

uart 发送数据格式: 1个起始位 + 5-8个数据位 + 可选的奇偶校验位 + 1或2个停止位 起始位为低电平,停止位为高电平。

UART发送一个字节时序图
  • 可以想到之前的FPGA点灯实验中8个LED灯的基础时间单元和波特率其实是一个东西,只不过点灯实验中是直接控制LED的亮灭时长,而UART通信中则是通过这个时间单元来控制数据的发送和接收速度。而用户自定义8种亮暗恰好对应8个数据位。

实验部分

  1. 串口通信模块设计的目的是用来发送数据的,因此需要有一个数据输入端口
  2. 串口通信,支持不同的波特率,所以需要一个波特率设置端口。
  3. 串口通信的本质就是将8位的并行数据通过一根信号线,在不同的时刻传输并行数据的不同位,通过多个时刻,最终将8位并行数据全部传出。
  4. 以1位低电平的起始位开始,接着是8位数据位,最后是1位高电平的停止位,总共10位数据。
  5. 控制信号,需要一个信号来告诉模块可以发送数据了(when to send),还需要一个信号来告诉模块当前的一批数据已经成功发送了。
UART发送模块设计图

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;
  • 设置uart_tx和Tx_done信号
 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
  • testbench代码
 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一样了。

  • 总结:深入分析仿真中实际的信号变化情况,考虑到信号的竞争与冒险问题,设计出合理的代码是关键。还有就是善用仿真。
Licensed under CC BY-NC-SA 4.0
啊啊啊啊啊啊啊
使用 Hugo 构建
主题 StackJimmy 设计