一、写在前面
上一节中,我们详细讨论了UART的协议内容并从设计组件的角度给出了UART协议中所需要的诸多内容,以供读者参考
这一节中,我们自定义如下标准的UART进行设计,需要注意的是,本篇文章中所涉及的UART仅供初学者学习参考,并没有采用实际工业开发中所涉及到的代码标准和思考,也并未进行综合与后仿。
1.1 协议标准
【数字IC】深入浅出理解UART协议
1.2 数字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
用以确定停止位位宽
五、整体结构
六、波特率生成器
6.1 设计文件
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) begin if(!rst_n) begin rx_count <= 'b0; rx_clk_reg <= 1'b0; end else if(rx_count == rx_rate - 1'b1) begin rx_clk_reg <= !rx_clk_reg; rx_count <= 'b0; end else rx_count = rx_count + 1'b1; end //tx_clk分频 always@(posedge clk or negedge rst_n) begin if(!rst_n) begin tx_count <= 'b0; tx_clk_reg <= 1'b0; end else if(tx_count == tx_rate - 1'b1) begin tx_clk_reg = !tx_clk_reg; tx_count <= 'b0; end else tx_count= tx_count +1'b1; end assign rx_clk = rx_clk_reg; assign tx_clk = tx_clk_reg; endmodule
6.2 仿真文件
`timescale 1ns / 1ps module 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 begin rst_n = 1; rst_n = 0; #45 rst_n = 1; #4000000000; $stop; end endmodule
6.3 仿真结果
根据波形图我们发现,设定为9600波特率的输出,即:用于tx模块与rx模块的分频时钟信号,符合预期。
我们希望tx端的采样频率遵循9.6khz,但是我们这里最终分频输出的是9.599kHz,这会影响最终设计的正确结果吗?大家可以在评论区讨论一波。
七、发送模块
7.1 发射模块状态机跳变
IDLE:默认态,无数据传输,输出高电平,当enable信号到来时跳转到S1。
S1:起始位,无数据传输,输出低电平,无条件跳转到S2。
S2:数据位,数据传输发生在S2,根据数据输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4
S3:校验位,根据要求,输出奇数校验或者偶数校验的值,下一个状态无条件跳转到S4。
S4:停止位,根据要求,输出1个或2个周期的高电平,下一个状态无条件跳转到IDLE。
7.2 设计文件
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; else state <= nstate; //状态机第二段 always@(*) begin case(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; endcase end //状态机的输出 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) begin if(!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; endcase else check_bit <= check_bit; end //UART停止位计数器 always@(posedge tx_clk or negedge rst_n) begin if(!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; end endmodule
7.3 仿真文件
`timescale 1ns / 1ps module 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; //数据发送task task 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); end endtask //正式测试 initial begin rst_n=1; enable = 0; #15 rst_n=0; #50 rst_n=1; #30; write_data(8'h0a); write_data(8'h24); write_data(8'h33); write_data(8'h14); end endmodule
7.4 仿真结果
我们这里测试的是8位,奇校验,2位停止位的发送module,可见在S2时,输出位从低位到高位将input的数据逐位发出,校验位和停止位也都符合预期
八、接收模块
8.1 接收模块状态机跳变
IDLE:默认态,不需要接收数据传输
S1:起始位接收,接收起始位数据传输,无条件跳转到S2。
S2:数据位接收,数据接收发生在S2,根据数据接收情况输出高低电平,假如有校验位,跳到S3,假如数据传输不设校验位,跳转到S4
S3:校验位接收,根据要求,接收奇数校验或者偶数校验的值,并进行检验,下一个状态无条件跳转到S4。
S4:停止位接收,根据要求,接收1个或2个周期的高电平,下一个状态无条件跳转到IDLE。
8.2 设计文件
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; else state <= nstate; //三段式状态机的第二段 always@(*) begin case(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; endcase end //下降沿检测电路,输出start信号,激活UART接收 always@(posedge rx_clk or negedge rst_n) if(!rst_n) data_in_reg <= 1'b0; else data_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; else count_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; else filter_reg <= 16'h0000; //存储后的多路选择,结果输出位filter_out always@(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]); else filter_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; endcase else test_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
8.3 仿真文件
`timescale 1ns / 1ps module 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; begin repeat(16)@(negedge clk); data_in = 0; test = ^A; repeat(8) begin repeat(16)@(negedge clk); data_in = A[0]; A = A>>1; end repeat(16)@(negedge clk); data_in = test; repeat(16)@(negedge clk); data_in = 1; repeat(16)@(negedge clk); data_in = 1; #200; end endtask initial begin rst_n=1; data_in = 1; #15 rst_n=0; #50 rst_n=1; #30; receive_data(8'h34); receive_data(8'ha8); receive_data(8'hb4); end endmodule
8.4 仿真结果
在仿真中,我们例化RX的时候,位宽选用的八位,校验形式选择的偶校验,停止位选用的为两位
输入数据位宽八位,停止位两位,校验形式为奇校验的数据34,a8,都正确的传入到了输出端,但因校验方式的错误,fail信号拉高一个周期,符合设计标准
九、TOP模块
9.1 设计文件
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
9.2 仿真文件
`timescale 1ns / 1ps module 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; initial begin rst_n = 1; #2000; rst_n = 0; enable = 1; #2000; rst_n = 1; data_in = 8'h34; #20000000; $stop; end endmodule
9.3 仿真结果
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,引入更多变量
十一、其他数字IC基础协议解读
11.1 UART协议
【数字IC】深入浅出理解UART
【数字IC】从零开始的Verilog UART设计
11.2 SPI协议
【数字IC】深入浅出理解SPI协议
【数字IC】从零开始的Verilog SPI设计
11.3 I2C协议
【数字IC】深入浅出理解I2C协议
11.4 AXI协议
【AXI】解读AXI协议双向握手机制的原理
【AXI】解读AXI协议中的burst突发传输机制
【AXI】解读AXI协议事务属性(Transaction Attributes)
【AXI】解读AXI协议乱序机制
【AXI】解读AXI协议原子化访问
【AXI】解读AXI协议的额外信号
【AXI】解读AXI协议的低功耗设计
【数字IC】深入浅出理解AXI协议
【数字IC】深入浅出理解AXI-lite协议