十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)

上一篇博文中注释了TIMER模块,现在来介绍UART模块。

终于迎来了最后一篇关于RISC-V SoC软核注解的博文,在本篇的最后会上传额外添加详细注释的工程代码,完全开源,如有需要可自行下载。

目录

0 RISC-V SoC注解系列文章目录

1. 结构

2. 基础知识

3. UART模块

3.1 输入和输出端口

3.2 程序注解

4.后记(工程代码链接)


0 RISC-V SoC注解系列文章目录

零、RISC-V SoC软核笔记详解——前言

 一、RISC-V SoC内核注解——取指

 二、RISC-V SoC内核注解——译码

三、RISC-V SoC内核注解——执行

四、RISC-V SoC内核注解——除法(试商法)

五、RISC-V SoC内核注解——中断

六、RISC-V SoC内核注解——通用寄存器

七、RISC-V SoC内核注解——总线

八、RISC-V SoC外设注解——GPIO

九、RISC-V SoC外设注解——SPI接口

十、RISC-V SoC外设注解——timer定时器

十一、RISC-V SoC外设注解——UART模块(终篇)

1. 结构

如下图,UART模块也是通过总线与内核进行交互的。

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_15,color_FFFFFF,t_70,g_se,x_16

2. 基础知识

(1)UART是指通用异步收发传输。

(2)同步串行通信需要通信双方在同一时钟的控制下,同步传输数据;异步串行通信是指通信双方使用各自的时钟控制数据的发送和接收过程。(在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方是在数据的起始位和停止位的帮助下实现信息同步的。)

(3)UART在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位(本设计不带数据校验位):

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_20,color_FFFFFF,t_70,g_se,x_16

  • 起始位标志着一帧数据的开始;
  • 停止位标志着一帧数据的结束;
  • 数据位是一帧数据中的有效数据;
  • 校验位分为奇校验和偶校验,用于检验数据在传输过程中是否出错。奇校验时,发送方应使数据位中1的个数与校验位中1的个数之和为奇数;接收方在接收数据时,对1的个数进行检查,若不为奇数,则说明数据在传输过程中出了差错。同样,偶校验则检查1的个数是否为偶数。

(4)串口通信的速率用波特率表示,它表示每秒传输二进制数据的位数,单位是bps(位/秒)。

(5)拓展知识:由于FPGA串口输入输出引脚为TTL电平,用3.3V代表逻辑“1”,0V代表逻辑“0”;而计算机串口采用RS-232电平,它是负逻辑电平,即-15V~-5V代表逻辑“1”,+5V~+15V代表逻辑“0”。因此当计算机与FPGA通信时,需要加电平转换芯片SP3232,实现RS232电平与TTL电平的转换。

3. UART模块

3.1 输入和输出端口

    input wire clk,  
    input wire rst,  
//内核给外设  
    input wire we_i,  
    input wire[31:0] addr_i,  
    input wire[31:0] data_i,  
//外设给内核  
    output reg[31:0] data_o,//通过总线,将uart模块中的寄存器数据输出到内核中(根据外设寄存器地址,C语言通过总线读寄存器;)  
//UART模块与其外设的通信线  
    output wire tx_pin,  
    input wire rx_pin  

3.2 程序注解

Step1:定义寄存器

// addr: 0x00  
// rw. bit[0]: tx enable, 1 = enable, 0 = disable  
// rw. bit[1]: rx enable, 1 = enable, 0 = disable  
reg[31:0] uart_ctrl;  
  
// addr: 0x04  
// ro. bit[0]: tx busy, 1 = busy, 0 = idle  发送状态(只读)  
// rw. bit[1]: rx over, 1 = over, 0 = receiving 接收状态(读写)  
// must check this bit before tx data  
reg[31:0] uart_status;  
  
// addr: 0x08  
// rw. clk div  
reg[31:0] uart_baud;  
  
// addr: 0x10  
// ro. rx data  
reg[31:0] uart_rx;  

Step2:给寄存器规划地址

localparam UART_CTRL = 8'h0;  
localparam UART_STATUS = 8'h4;  
localparam UART_BAUD = 8'h8;  
localparam UART_TXDATA = 8'hc;  
localparam UART_RXDATA = 8'h10; 

Step3:对寄存器进行读写

这部分与其他外设的读写过程大致相同,因此不再赘述。可自行下载看工程代码。

Step4UARTTX发送

// *************************** TX发送 ****************************  
  
always @ (posedge clk) begin  
    if (rst == 1'b0) begin  
        state <= S_IDLE;  
        cycle_cnt <= 16'd0;  
        tx_reg <= 1'b0;  
        bit_cnt <= 4'd0;  
        tx_data_ready <= 1'b0;  
    end else begin  
        if (state == S_IDLE) begin   
            tx_reg <= 1'b1;   //空闲状态下发送数据置1  
            tx_data_ready <= 1'b0;  
            if (tx_data_valid == 1'b1) begin //发送数据有效时 下一个时钟周期开始发送数据  
                state <= S_START;  
                cycle_cnt <= 16'd0;  
                bit_cnt <= 4'd0;  
                tx_reg <= 1'b0;  
            end  
        end else begin  
            cycle_cnt <= cycle_cnt + 16'd1;  //波特率115200bps对应的分频系数  
            if (cycle_cnt == uart_baud[15:0]) begin  
                cycle_cnt <= 16'd0;  
                case (state)  
                    S_START: begin  
                        tx_reg <= tx_data[bit_cnt];  
                        state <= S_SEND_BYTE;  
                        bit_cnt <= bit_cnt + 4'd1;  
                    end  
                    S_SEND_BYTE: begin  
                        if (bit_cnt == 4'd8) begin  //发送完8bit 结束本次发送  
                            state <= S_STOP;  
                            tx_reg <= 1'b1;  
                        end else begin                  
                            tx_reg <= tx_data[bit_cnt];  
                        end  
            bit_cnt <= bit_cnt + 4'd1;  
                    end  
                    S_STOP: begin  
                        tx_reg <= 1'b1; //数据发送结束后 发送1  
                        state <= S_IDLE;  
                        tx_data_ready <= 1'b1;  
                    end  
                endcase  
            end  
        end  
    end  
end 

具体发送过程如下:

a. 发送空闲时(即不发送数据),(根据协议)将发送端保持置1;当发送数据有效时(C语言将要发送的数据写入寄存器UART_TXDATA),发送端发送 起始位0(一个计数周期)

b. 根据约定的发送速率(波特率)控制时钟分频计数器的计数阈值,发送数据,先发送低位在发送高位,发送完数据,将发送端置1,对应时序中的停止位;并更新接收发送状态寄存器相应的位UART_STATUS[0] <= 0;

c. 等待下一次发送(即下一次发送数据有效信号);

Step5UARTRX接收

 // *************************** RX接收 ****************************  
  
    // 下降沿检测(检测起始信号)  
    assign rx_negedge = rx_q1 && ~rx_q0;  
  
  
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_q0 <= 1'b0;  
            rx_q1 <= 1'b0;     
        end else begin  
            rx_q0 <= rx_pin;  
            rx_q1 <= rx_q0;  
        end  
    end  
  
    // 开始接收数据信号rx_start,接收期间一直有效  
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_start <= 1'b0;  
        end else begin  
            if (uart_ctrl[1]) begin //uart_ctrl[1]置1为 接收使能  
                if (rx_negedge) begin //检测到下降沿 起始信号,开始接收数据  
                    rx_start <= 1'b1;  
                end else if (rx_clk_edge_cnt == 4'd9) begin  
                    rx_start <= 1'b0;  
                end  
            end else begin  
                rx_start <= 1'b0;  
            end  
        end  
    end  
      
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_div_cnt <= 16'h0;  
        end else begin  
            // 第一个时钟沿只需波特率分频系数的一半  
            if (rx_start == 1'b1 && rx_clk_edge_cnt == 4'h0) begin  
                rx_div_cnt <= {1'b0, uart_baud[15:1]};  
            end else begin  
                rx_div_cnt <= uart_baud[15:0];  
            end  
        end  
    end  
  
    // 对时钟进行计数  
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_clk_cnt <= 16'h0;  
        end else if (rx_start == 1'b1) begin  
            // 计数达到分频值  
            if (rx_clk_cnt == rx_div_cnt) begin  
                rx_clk_cnt <= 16'h0;  
            end else begin  
                rx_clk_cnt <= rx_clk_cnt + 1'b1;  
            end  
        end else begin  
            rx_clk_cnt <= 16'h0;  
        end  
    end  
  
    // 每当时钟计数达到分频值时产生一个上升沿脉冲  
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_clk_edge_cnt <= 4'h0;  
            rx_clk_edge_level <= 1'b0;  
        end else if (rx_start == 1'b1) begin  
            // 计数达到分频值  
            if (rx_clk_cnt == rx_div_cnt) begin  
                // 时钟沿个数达到最大值  
                if (rx_clk_edge_cnt == 4'd9) begin  
                    rx_clk_edge_cnt <= 4'h0;  
                    rx_clk_edge_level <= 1'b0;  
                end else begin  
                    // 时钟沿个数加1  
                    rx_clk_edge_cnt <= rx_clk_edge_cnt + 1'b1;  
                    // 产生上升沿脉冲  
                    rx_clk_edge_level <= 1'b1;  
                end  
            end else begin  
                rx_clk_edge_level <= 1'b0;  
            end  
        end else begin  
            rx_clk_edge_cnt <= 4'h0;  
            rx_clk_edge_level <= 1'b0;  
        end  
    end  
  
    // bit序列  
    always @ (posedge clk) begin  
        if (rst == 1'b0) begin  
            rx_data <= 8'h0;  
            rx_over <= 1'b0;  
        end else begin  
            if (rx_start == 1'b1) begin  
                // 上升沿  
                if (rx_clk_edge_level == 1'b1) begin  
                    case (rx_clk_edge_cnt)  
                        // 起始位  
                        1: begin  
  
                        end  
                        // 数据位  
                        2, 3, 4, 5, 6, 7, 8, 9: begin  
                            rx_data <= rx_data | (rx_pin << (rx_clk_edge_cnt - 2)); //1和任何数相或,结果都为这个数本身  
                              
                            // 最后一位接收完成,置位接收完成标志  
                            if (rx_clk_edge_cnt == 4'h9) begin  
                                rx_over <= 1'b1;  
                            end  
                        end  
                    endcase  
                end  
            end else begin  
                rx_data <= 8'h0;  
                rx_over <= 1'b0;  
            end  
        end  
    end  
  
endmodule 

具体接收过程如下:

a. 检测接收信号的下降沿,即空闲和起始位的下降沿;(为什么要检测沿,而不是直接检测电平:因为如果出现毛刺,有一个短暂的低电平,就会出错。而下降沿的时间非常短,就检测不到)

b. 当使能接收,且检测当接收信号将下降沿时,开始接收数据;

       i. 需对接收时钟沿进行计数,以判断数据是否接收完毕(默认一次接收8bit数据);

       ii. 第一个时钟沿只需波特率分频系数的一半,这样之后就可以在被接收数据的中间进行采样,保证采样数据的稳定;

       iii. 第一个时钟沿是起始位数据,不需要接收,之后需要连续接受8bit数据,先接收低位数据;

c. 接收数据时钟沿计数到9,8bit数据接收完毕,将数据接收完信号rx_over置1,并更新接收发送状态寄存器相应的位UART_STATUS[1] <= 1;

这里将其中较难理解的几句代码进行注解代码如下:(将串行的8位数据,变成8位的并行数据),

// 数据位  
2, 3, 4, 5, 6, 7, 8, 9: begin  
    rx_data <= rx_data | (rx_pin << (rx_clk_edge_cnt - 2)); //1和任何数相或,结果都为这个数本身

具体运算过程如下图:

watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAQ29kZV9jb3B5MQ==,size_20,color_FFFFFF,t_70,g_se,x_16

4.后记(工程代码链接)

经过长达12篇的博文,我们终于将RISC-V SoC工程的所有学习心得分享完毕,希望可以帮助感兴趣的同学从零基础入门学习RISC-V架构的SoC。因水平有限,在博文中如有错误,请各位小伙伴私信或留言指正。

将额外添加了详细注释的工程代码链接分享如下,如有需要可自行下载。

百度云盘链接:

提取码:o3r1

链接:

tinyriscv_soc加注版https://pan.baidu.com/s/104LlvWT37VJL82IN-3N3Kg 阿里云盘链接:

提取码: x60a

链接:

tinyriscv_soc加注版https://www.aliyundrive.com/s/5MyDp9mcf3k

大鹏一日同风起,扶摇直上九万里。相信在未来十年乃至更长时间内鲜有比RISC-V更优秀的开源处理器架构出现。因此,希望对RISC-V感兴趣的小伙伴不要错过这个属于RISC-V的时代。

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小wang的IC自习室

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值