> 文档中心 > 【数字IC】从零开始的UART设计

【数字IC】从零开始的UART设计

从零开始的UART协议设计

    • 写在前面
        • 协议标准
        • 数字IC组件代码
    • 设计要求
    • 模块划分
    • 全局参数
    • 整体结构
    • 波特率生成器
        • 设计文件
        • 仿真文件
        • 仿真结果
    • 发送模块
        • 发射模块状态机跳变
        • 设计文件
        • 仿真文件
        • 仿真结果
    • 接收模块
        • 接收模块状态机跳变
        • 设计文件
        • 仿真文件
        • 仿真结果
    • TOP模块
        • 设计文件
        • 仿真文件
        • 仿真结果
    • 本设计与工业级UART的差距

写在前面

上一节中,我们详细讨论了UART的协议内容并从设计组件的角度给出了UART协议中所需要的诸多内容,以供读者参考
这一节中,我们自定义如下标准的UART进行设计,需要注意的是,本篇文章中所涉及的UART仅供初学者学习参考并没有采用实际工业开发中所涉及到的代码标准和思考,也并未进行综合与后仿。

协议标准

【数字IC】深入浅出理解UART协议

数字IC组件代码

【数字IC手撕代码】Verilog边沿检测电路(上升沿,下降沿,双边沿)|题目|原理|设计|仿真
【数字IC手撕代码】Verilog奇偶校验|题目|原理|设计|仿真
【数字IC手撕代码】Verilog奇数分频|题目|原理|设计|仿真
【数字IC手撕代码】Verilog偶数分频|题目|原理|设计|仿真

设计要求

  • UART收/发器固定;
  • 支持5-8位数据位、1/2位停止位、可选1位奇偶校验位;
  • 奇偶校验结果错误的检测能力;
  • 波特率2400、4800、9600可调;

模块划分

从top level来看,整个UART的涉及应该包含三个module
首先是baud_generate module

对于波特率生成器而言,我们需要在这个模块中将全局时钟信号100Mhz进行分频,按照约定的波特率如300,1200,2400,9600等要求进行分频处理,以得到所要求的波特率要求。

其次是tx module

对于发送模块而言,我们要从状态机的角度对其进行划分,按起始位,数据位,校验位,停止位进行状态划分,并区分状态转移的条件。

最终是rx module

对于发送模块而言,我们也从状态机的角度对其进行划分,按起始位,数据位,校验位,停止位进行状态划分,并区分状态转移的条件。不过按照《深入浅出理解UART协议》所言,在rx模块中我们需要使用发送频率的十六倍频进行采样,使用多路选择的方法避免数据传输中可能会出现的错误。

全局参数

time_frequency = 100_000_000
用以确定全局时钟频率
baud_rate = 9_600
用以确定RX,TX的波特率
data_width = 8
用以确定数据位宽
test = 1
用以确定奇偶校验,其中0为无校验位,1为偶校验,2为奇校验
stop_width = 2
用以确定停止位位宽

整体结构

【数字IC】从零开始的UART设计

波特率生成器

设计文件

module baud_generator #(parameter clk_rate = 100_000_000, //全局时钟频率parameter baud_rate = 9_600 //波特率)(input clk,input rst_n,output rx_clk,output tx_clk);localparam tx_rate = clk_rate / (baud_rate * 2); //发送模块分频系数localparam rx_rate = clk_rate / (baud_rate * 2 * 16); //接收模块分频系数reg [$clog2(rx_rate)-1:0] rx_count; reg [$clog2(tx_rate)-1:0] tx_count;reg rx_clk_reg;reg tx_clk_reg;// rx_clk分频always@(posedge clk or negedge rst_n)beginif(!rst_n)beginrx_count <= 'b0;rx_clk_reg <= 1'b0;endelse if(rx_count == rx_rate - 1'b1)beginrx_clk_reg <= !rx_clk_reg;rx_count <= 'b0;endelserx_count = rx_count + 1'b1;end//tx_clk分频always@(posedge clk or negedge rst_n)beginif(!rst_n)begintx_count <= 'b0;tx_clk_reg <= 1'b0;endelse if(tx_count == tx_rate - 1'b1)begintx_clk_reg = !tx_clk_reg;tx_count <= 'b0;endelsetx_count= tx_count +1'b1;endassign rx_clk = rx_clk_reg;assign tx_clk = tx_clk_reg;endmodule

仿真文件

`timescale 1ns / 1psmodule baud_generator_tb ();reg clk;reg rst_n;wire rx_clk;wire tx_clk;baud_generator #(10_000_000,9600) u1 (clk,rst_n,rx_clk,tx_clk);initial clk = 0;always #5 clk = !clk; //生成10ns全局时钟initial beginrst_n = 1;rst_n = 0;#45rst_n = 1;#4000000000;$stop;endendmodule

仿真结果

【数字IC】从零开始的UART设计
根据波形图我们发现,设定为9600波特率的输出,即:用于tx模块与rx模块的分频时钟信号,符合预期。

我们希望tx端的采样频率遵循9.6khz,但是我们这里最终分频输出的是9.599kHz,这会影响最终设计的正确结果吗?大家可以在评论区讨论一波。

发送模块

发射模块状态机跳变

【数字IC】从零开始的UART设计
IDLE:默认态,无数据传输,输出高电平,当enable信号到来时跳转到S1。
S1:起始位,无数据传输,输出低电平,无条件跳转到S2。
S2:数据位,数据传输发生在S2,根据数据输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4
S3:校验位,根据要求,输出奇数校验或者偶数校验的值,下一个状态无条件跳转到S4。
S4:停止位,根据要求,输出1个或2个周期的高电平,下一个状态无条件跳转到IDLE。

设计文件

module tx #(parameter data_width = 8,parameter test = 2,parameter stop_width = 1)(input tx_clk,input rst_n,input [data_width-1:0] data_in,input enable,output reg tx_out);localparam IDLE = 3'b000;localparam S1  = 3'b001;localparam S2 = 3'b010;localparam S3  = 3'b011;localparam S4   = 3'b100;reg [3:0] state, nstate;reg [3:0] count_data;reg [1:0] count_stop;reg check_bit;//状态机第一段always@(posedge tx_clk or negedge rst_n)if(!rst_n)state <= IDLE;elsestate <= nstate;//状态机第二段always@(*)begincase(state)IDLE: nstate = (enable ? S1 : IDLE);S1:nstate = S2;S2:nstate = (test == 'b0 && count_data == (data_width - 1)? S4 : count_data == (data_width - 1)? S3 : S2); S3:nstate =  S4 ;S4:nstate = count_stop == (stop_width - 1) ? IDLE : S4 ;default: nstate = IDLE;endcaseend//状态机的输出always@(*)case(state)IDLE : tx_out = 1'b1;S1 : tx_out = 1'b0;S2 : tx_out = data_in[count_data]; //由低位到高位依次输出S3 : tx_out = check_bit;S4 : tx_out = 1'b1;default : tx_out = 1'b1;endcase//S2状态,输出数据位的计数器always@(posedge tx_clk or negedge rst_n)if(!rst_n)count_data <= 4'b0000;else if (count_data < data_width && state == S2)count_data <= count_data + 1'b1;else count_data <= 4'b0000;//UART的奇偶校验位的生成always@(posedge tx_clk or negedge rst_n)beginif(!rst_n)check_bit <= 1'b0;else if (state == S2)case(test)2'b00 : check_bit <= 1'b0;2'b01 : check_bit <= ^data_in;2'b10 : check_bit <= !(^data_in);default:check_bit <= 1'b0;endcaseelse check_bit <= check_bit;end//UART停止位计数器always@(posedge tx_clk or negedge rst_n)beginif(!rst_n)count_stop <= 2'b00;else if( state == S4 && count_stop <stop_width)count_stop <= count_stop + 1'b1;else count_stop <= 2'b00;endendmodule

仿真文件

`timescale 1ns / 1psmodule tx_tb();reg clk;reg rst_n;reg enable;reg [7:0] data_in;wire tx_out;//端口例化与数据传输parameter A=8;parameter B=1;parameter C=2;tx #(A,B,C) u1 (clk,rst_n,data_in,enable,tx_out);//时钟生成initial clk = 0;always #5 clk = !clk;//数据发送tasktask write_data;input [7:0] task_data_in;begin@(negedge clk);enable = 1;@(negedge clk) ;data_in = task_data_in;enable =0;repeat(12)@(negedge clk);endendtask//正式测试initialbeginrst_n=1;enable = 0;#15rst_n=0;#50rst_n=1;#30;write_data(8'h0a);write_data(8'h24);write_data(8'h33);write_data(8'h14);endendmodule

仿真结果

【数字IC】从零开始的UART设计
我们这里测试的是8位,奇校验,2位停止位的发送module,可见在S2时,输出位从低位到高位将input的数据逐位发出,校验位和停止位也都符合预期

接收模块

接收模块状态机跳变

【数字IC】从零开始的UART设计
IDLE:默认态,不需要接收数据传输
S1:起始位接收,接收起始位数据传输,无条件跳转到S2。
S2:数据位接收,数据接收发生在S2,根据数据接收情况输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4
S3:校验位接收,根据要求,接收奇数校验或者偶数校验的值,并进行检验,下一个状态无条件跳转到S4。
S4:停止位接收,根据要求,接收1个或2个周期的高电平,下一个状态无条件跳转到IDLE。

设计文件

module rx #(parameter data_width = 8,parameter test = 2,parameter stop_width = 1)(input rx_clk,input rst_n,input data_in,output [data_width-1:0]rx_out,output fail);//状态机定义localparam IDLE = 3'b000;localparam S1 = 3'b001;localparam S2  = 3'b010;localparam S3 = 3'b011;localparam S4   = 3'b100;reg [2:0] state, nstate;//reg [3:0] frq_6;reg data_in_reg;reg [15:0] filter_reg;reg filter_out;reg [data_width-1:0] rx_out_reg;reg test_reg;wire start;//????????????reg [3:0] count_data;reg [1:0] count_stop;//三段式状态机的第一段always@(posedge rx_clk or negedge rst_n)if(!rst_n)state <= IDLE;elsestate <= nstate;//三段式状态机的第二段always@(*)begincase(state)IDLE: nstate = (start ? S1 : IDLE);S1:nstate = (frq_6 == 4'b1111 ? S2 : S1);S2:nstate = (test == 'b0 && frq_6 == 4'b1111 && count_data == data_width - 1) ? S4 : (frq_6 == 4'b1111 && count_data == data_width - 1)? S3 : S2 ;S3:nstate = (frq_6 == 4'b1111 ? S4 : S3);S4:nstate = (frq_6 == 4'b1111 && (count_stop == stop_width - 1)? IDLE : S4);default:nstate = IDLE;endcaseend//下降沿检测电路,输出start信号,激活UART接收always@(posedge rx_clk or negedge rst_n)if(!rst_n)data_in_reg <= 1'b0;elsedata_in_reg <= data_in;assign start = data_in_reg & !data_in;//数据接收个数的计数器always@(posedge rx_clk or negedge rst_n)if(!rst_n || state !== S2) count_data <= 4'b0000;else if (count_data == data_width - 1 && frq_6 ==4'b1111)count_data <= 4'b0000;else if (frq_6 == 4'b1111)count_data <= count_data + 1'b1;elsecount_data <= count_data;//停止位接受个数的计数器always@(posedge rx_clk or negedge rst_n)if(!rst_n || state !== S4) count_stop <= 2'b00;else if (count_stop == stop_width - 1 && frq_6 ==4'b1111)count_stop <= 2'b00;else if(frq_6 == 4'b1111)count_stop <= count_stop + 1'b1;else count_stop <= count_stop;//16倍的采样计数器always@(posedge rx_clk or negedge rst_n)if(!rst_n)frq_6 <= 4'b0000;else if(frq_6 == 4'b1111 && state == IDLE)frq_6 <= 4'b0000;else if(state == S1 || state == S2 || state == S3 || state == S4)frq_6 <= frq_6 + 1'b1;else frq_6 <= frq_6;//16倍频的采样结果存储在filter_reg中always@(posedge rx_clk or negedge rst_n)if(!rst_n)filter_reg <= 16'h0000;else if(state == S1 || state == S2 || state == S3 || state == S4)filter_reg[frq_6] <= data_in;elsefilter_reg <= 16'h0000;//存储后的多路选择,结果输出位filter_outalways@(posedge rx_clk or negedge rst_n)if(!rst_n || state == IDLE)filter_out <= 1'b0;else if ( frq_6 == 4'b1100)filter_out <= (filter_reg[7] & filter_reg[8]) ^ (filter_reg[7] & filter_reg[9]) ^ (filter_reg[8] & filter_reg[9]);elsefilter_out <= filter_out;//S2状态时将数据依次存入寄存器always@(posedge rx_clk or negedge rst_n)if(!rst_n || state == IDLE)rx_out_reg <= 'b0;else if (state == S2 && frq_6 == 4'b1111)rx_out_reg[count_data] <= filter_out;else rx_out_reg <= rx_out_reg;//S3状态时判断校验位是否正确always@(posedge rx_clk or negedge rst_n)if(!rst_n)test_reg <= 1'b0;else if(state == S3)case(test)2'b00 : test_reg <= 1'b0;2'b01 : test_reg <= !(^rx_out_reg);2'b10 : test_reg <= (^rx_out_reg);default : test_reg <= 1'b0;endcaseelsetest_reg <= 1'b0;assign fail = (state == S3 && frq_6 == 4'b1110 && filter_out !== test_reg) ? 1 : 0;//数据接受完毕,输出传入RX的值assign rx_out =(state == S4 && count_stop == stop_width - 1) ? rx_out_reg : 'b0 ;endmodule

仿真文件

`timescale 1ns / 1psmodule rx_tb();reg clk;reg rst_n;reg data_in;wire rx_out;reg test;parameter A=8;parameter B=1;parameter C=2;rx #(A,B,C) u1 (clk,rst_n,data_in,rx_out);initial clk = 0;always #5 clk = !clk;task receive_data;input [7:0] A;beginrepeat(16)@(negedge clk);data_in = 0;  test = ^A;repeat(8) beginrepeat(16)@(negedge clk);data_in = A[0];A = A>>1;endrepeat(16)@(negedge clk);data_in = test;repeat(16)@(negedge clk);data_in = 1;repeat(16)@(negedge clk);data_in = 1;#200;endendtaskinitialbeginrst_n=1;data_in = 1;#15rst_n=0;#50rst_n=1;#30;receive_data(8'h34);receive_data(8'ha8);receive_data(8'hb4);endendmodule

仿真结果

【数字IC】从零开始的UART设计
在仿真中,我们例化RX的时候,位宽选用的八位,校验形式选择的偶校验,停止位选用的为两位
输入数据位宽八位,停止位两位,校验形式为奇校验的数据34,a8,都正确的传入到了输出端,但因校验方式的错误,fail信号拉高一个周期,符合设计标准

TOP模块

设计文件

module uart_top#(parameter time_frequency = 100_000_000,parameter baud_rate = 9_600,parameter data_width = 8,parameter test = 1,parameter stop_width = 2)(input clk,input rst_n,input enable,input [data_width-1:0] data_in,output [data_width-1:0] rx_out);wire rx_clk;wire tx_clk;wire tx_out;baud_generator #(time_frequency,baud_rate)   u1 (.clk(clk),.rst_n(rst_n),.rx_clk(rx_clk),.tx_clk(tx_clk));tx #(data_width,test,stop_width) u2(.tx_clk(tx_clk),.rst_n(rst_n),.data_in(data_in),.enable(enable),.tx_out(tx_out));rx #(data_width,test,stop_width) u3(.rx_clk(rx_clk),.rst_n(rst_n),.data_in(tx_out),.rx_out(rx_out),.fail());endmodule

仿真文件

`timescale 1ns / 1psmodule uart_top_tb();reg clk;reg rst_n;reg enable;reg [7:0] data_in;wire [7:0] rx_out;uart_top  u4(clk,rst_n,enable,data_in,rx_out);initial clk = 0;always #5 clk = !clk;initialbeginrst_n = 1;#2000;rst_n = 0;enable = 1;#2000;rst_n = 1;data_in = 8'h34;#20000000;$stop;endendmodule

仿真结果

【数字IC】从零开始的UART设计
TX输入8’h34之后,经过1.2ms,RX输出相同的数值,设计符合要求

本设计与工业级UART的差距

最后再讨论一下本设计与实际工程中的UART的差异性在哪,以供读者补充参考。

  • 仅存在校验位检测,不存在帧格式检测
  • 中断控制缺失
  • 单向TX,RX固定,而非双向RX,TX可选
  • 输入输出缺少FIFO做缓冲

不过本设计仅为学习参考使用,配合【数字IC】深入浅出理解UART协议使读者对于URAT的协议理解和电路实现有基本的认识才是本篇博文的目的所在。

  • 帧格式的检测,与校验位的检测其实大同小异。
  • 双向TX,RX也不过是在单向TX,RX的基础上使用状态机进行更多的状态跳转
  • FIFO做数据缓冲的功能也不过是在RX的data_in与TX的data_out处例化FIFO,引入更多变量

收藏并关注作者,获取最新的更新动态