数字IC设计|十一、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模块也是通过总线与内核进行交互的。
数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)
文章图片

2. 基础知识 (1)UART是指通用异步收发传输。
(2)同步串行通信需要通信双方在同一时钟的控制下,同步传输数据;异步串行通信是指通信双方使用各自的时钟控制数据的发送和接收过程。(在数据传输过程中是不需要时钟的,发送方发送的时间间隔可以不均匀,接受方是在数据的起始位和停止位的帮助下实现信息同步的。)
【数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)】(3)UART在发送或接收过程中的一帧数据由4部分组成,起始位、数据位、奇偶校验位和停止位(本设计不带数据校验位):
数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)
文章图片

  • 起始位标志着一帧数据的开始;
  • 停止位标志着一帧数据的结束;
  • 数据位是一帧数据中的有效数据;
  • 校验位分为奇校验和偶校验,用于检验数据在传输过程中是否出错。奇校验时,发送方应使数据位中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 = https://www.it610.com/article/8'hc; localparam UART_RXDATA = https://www.it610.com/article/8'h10;

Step3:对寄存器进行读写
这部分与其他外设的读写过程大致相同,因此不再赘述。可自行下载看工程代码。
Step4:UART的TX发送
// *************************** 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. 等待下一次发送(即下一次发送数据有效信号);
Step5:UART的RX接收
// *************************** 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 endalways @ (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: beginend // 数据位 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 endendmodule

具体接收过程如下:
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和任何数相或,结果都为这个数本身

具体运算过程如下图:

数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)
文章图片

4.后记(工程代码链接) 经过长达12篇的博文,我们终于将RISC-V SoC工程的所有学习心得分享完毕,希望可以帮助感兴趣的同学从零基础入门学习RISC-V架构的SoC。因水平有限,在博文中如有错误,请各位小伙伴私信或留言指正。
将额外添加了详细注释的工程代码链接分享如下,如有需要可自行下载。
百度云盘链接:
提取码:o3r1
链接:
tinyriscv_soc加注版数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)
文章图片
https://pan.baidu.com/s/104LlvWT37VJL82IN-3N3Kg 阿里云盘链接:
提取码: x60a
链接:
tinyriscv_soc加注版数字IC设计|十一、RISC-V SoC外设注解——UART接口 时序设计 代码讲解(终篇)
文章图片
https://www.aliyundrive.com/s/5MyDp9mcf3k
大鹏一日同风起,扶摇直上九万里。相信在未来十年乃至更长时间内鲜有比RISC-V更优秀的开源处理器架构出现。因此,希望对RISC-V感兴趣的小伙伴不要错过这个属于RISC-V的时代。


    推荐阅读