最近工作用uart用的比较多,为了让自己更好的掌握这个协议,写了这篇文章,解读了uart程序的编写过程(程序参考了米联客的教程)。
最基础的概念
UART是用来让两个设备之间传输数据的协议,毕竟我不能直接给你一串01序列,你肯定不知道我表达的意思是啥。通信涉及到这么几个基本的问题:什么时候开始发,什么时候结束?在接收数据的过程中,接收方以什么速率接收?
不同的协议有不同的解决方式,比如I2C,和SPI,他们会利用一个时钟线来解决这些问题,用数据线确定具体的传输数据,在UART中没有时钟线,那只能靠数据线来解决这些问题了,也就是通过下面这些东西:
波特率:由于UART没有时钟线,那么就通信双方必须规定传输的速度是多少,比如每秒发10个符号,那么波特率就是10。
起始位:UART定义当数据总线由高电平变低电平就是开始了,有人可能会疑惑,假如传数据的时候先发了一个1,下一个发0,不会被误判为起始信号?不会的,谜底就在谜面上,因为这是传数据的时候,要传完才会重新根据(1-0)来判断起始位。
数据位:一个数据占用一个波特率时钟,一般是一次发8个数据位。
停止位:因为起始时是需要通过高电平拉低来判断的,所以很自然传输完一个字节的数据,就得把数据线拉高对吧。所以一般数据传输结束后会保持1/2个高电平,或者没有新的数据了一直高也行。
时序图:
发送端代码解析
开局一根发信号数据线,首先我们试着写一个周期的数据发送。
reg [7:0]data_send; //要发送的数据reg uart_out; //开局的线reg [3:0]count=4'b0;always @(posedge clk)beginif(count==0)uart_out<=1; //最开始是高电平else if(count==1)uart_out<=0; //拉低信号就是起始位else if(count<10)uart_out<=data_send[count-2]; //依次发送8个数据 else if(count==10) uart_out<=1; //最后拉高作为停止位count<=count+1;end
这部分程序能按UART要求的时序把数据发出去。但里面有很多问题。
首先,它是按系统时钟的速率发的,UART要求的速率是一些固定的速率,比如9600,115200等。(当然如果你自己把接收端也写了,两边统一一下那就随便你定速率了)所以我们首先得把时钟信号变为相应波特率的时钟,那么就需要一个分频的系数:
BAUD_DIV=时钟频率/波特率。程序如下:
Interger BAUD_DIV=10416; //这是在100M的时钟下,设置9600波特率的分频系数。wire bps_en;//设置一个使能信号,当计数器到达分频系数就使能。reg[13:0] baud_count;//使能计数器always @(posedge clk)beginif(baud_count<BAUD_DIV)baud_count<=baud_count+1;else baud_count<=0;endassign bps_en= (baud_count==BAUD_DIV);//计数器达到分频系数,这个信号放到发送信号程序的判断条件里面,就可以按这个波特率发信号了。
以上就是最基本的发数据的函数了,按照一定的波特率,一定的发数据的顺序(起始--数据位--终止位)。 除此之外,根据实际的应用,可能要需要加一些别的信号线。比如程序一开始就运行直接开始输出数据了,是不是加一个开启信号,比如是否要记录发了多少bit数据,发了多少组数据了,发送状态是正忙还是空闲等等。
这里的发送状态是否正忙这个信号线还是挺重要的,当你发送多个周期的数据的时候,需要通过这个信号来切换,当它正忙转为不忙的时候,你就知道:奥,一个8bit的数据传输完了,那我输入的数据可以改变了。
这段代码可以这样写:
wire uart_busy;
assign uart_busy==(count>0 &count<10)
这个写的肯定是有问题的,但只是表达一下这个意思,可以通过传输过程的计数器来对这个信号线赋值。
下面是一个比较完整的发送部分程序,除了上面提到的那几个信号线,还有个数据发送请求,也就是告诉它什么时候开始传数据,看完上面的部分应该会比较好理解了。
`timescale 1ns / 1ns//仿真时间刻度/精度
module uiuart_tx#
(
parameter integer BAUD_DIV = 10416 //设置采样系数 (时钟/采样率-1)
)
(
input clk_i, //系统时钟输入
input uart_rstn_i, //系统复位输入
input uart_wreq_i, //发送数据请求
input [7:0] uart_wdata_i, //发送数据
output uart_wbusy_o, //发送状态忙,代表正在发送数据
output uart_tx_o //uart tx 发送总线
);
localparam UART_LEN = 4'd10; //设置 uart 发送的 bit 数量为 10,代表 1bit 起始位,8bits 数据,1bit 停止位
wire bps_en ; //发送使能
reg uart_wreq_r = 1'b0; //寄存一次 uart_wreq_i
reg bps_start_en = 1'b0; //波特率计数器启动使能,也是发送启动使能
reg [13:0] baud_div = 14'd0; //波特率计数器
reg [9 :0] uart_wdata_r = 10'h3ff; //寄存 uart_wreq_i
reg [3 :0] tx_cnt = 4'd0; //计数发送了多少 bits
assign uart_tx_o = uart_wdata_r[0]; //总线上的数据,始终是 uart_wdata_r[0]
assign uart_wbusy_o = bps_start_en; //总线忙标志,即是 bps_start_en 为有效,即当总线忙于发送,总线忙
// 发送使能
assign bps_en = (baud_div == BAUD_DIV); //产生一次发送使能信号,条件是 baud_div == BAUD_DIV,波特率计数
达成
//波特率计数器
always@(posedge clk_i )begin
if((uart_rstn_i== 1'b0) || (uart_wreq_i==1'b1&uart_wreq_r==1'b0))begin
baud_div <= 14'd0;
end
else begin
if(bps_start_en && baud_div < BAUD_DIV) //bps_start_en 的信号拉高,表示开始发送
baud_div <= baud_div + 1'b1; //且 baud_div < BAUD_DIV 波特率计算,未达到波特率
baud_div+1
else
baud_div <= 14'd0; //达到清零
end
end
always@(posedge clk_i)begin
uart_wreq_r <= uart_wreq_i; //寄存一次 uart_wreq_i 信号
end
//当 uart_wreq_i 从低电平变为高电平,启动发送
always@(posedge clk_i)begin
if(uart_rstn_i == 1'b0)
bps_start_en <= 1'b0; //复位,计数清零
else if(uart_wreq_i==1'b1&uart_wreq_r==1'b0) //uart_wreq_i 上升沿激活
bps_start_en <= 1'b1; //激活后将 bps_start_en 拉高,传输开始
else if(tx_cnt == UART_LEN) //tx_cnt 用于计数当前发送的 bits 数量,当达到预定值 UART_LEN
bps_start_en <= 1'b0; //将 bps_start_en 拉低,传输结束
else
bps_start_en <= bps_start_en;
end
//发送 bits 计数器
always@(posedge clk_i)begin
if(((uart_rstn_i== 1'b0) || (uart_wreq_i==1'b1&uart_wreq_r==1'b0))||(tx_cnt == 10))//当复位、启动
发送、发送完成,重置 tx_cnt
tx_cnt <=4'd0;
else if(bps_en && (tx_cnt < UART_LEN)) //tx_cnt 计数器,每发送一个 bit 加 1
tx_cnt <= tx_cnt + 1'b1;
end
//uart 发送并串移位控制器
always@(posedge clk_i)begin
if((uart_wreq_i==1'b1&uart_wreq_r==1'b0)) //当发送请求有效,寄存需要发送的数据到 uart_wdata_r
uart_wdata_r <= {1'b1,uart_wdata_i[7:0],1'b0};//寄存需要发送的数据,包括 1bit 起始位,8bits 数据,
1bit 停止位
else if(bps_en && (tx_cnt < (UART_LEN - 1'b1))) //shift 9 bits
uart_wdata_r <= {uart_wdata_r[0],uart_wdata_r[9:1]}; //并串转换,将并行数据依次传输
else
uart_wdata_r <= uart_wdata_r;
end
endmodule
仿真验证
这段代码用计数器做了一个复位,用状态机控制输入数据的变化,可以根据自己的需要更改状态3的程序。
module top();localparam SYS_TIME = 'd10;//时钟周期,以ns为单位
reg sysclk_i; //系统时钟initial begin //仿真初始化
sysclk_i =0;#2000000 $finish;
endalways #(SYS_TIME/2) sysclk_i = ~sysclk_i; //产生主时钟wire uart_tx_o ; // UART 串口发送总线
wire uart_rstn_i; //内部同步复位
wire uart_wbusy; //UART发送驱动器正忙
reg t1s_dly_en; //1S延迟
reg[1:0] S_UART_TX; //UART 发送状态机
reg[1:0] tx_index; //发送index计数器
reg uart_wreq; //UART发送请求
reg[7:0] uart_wdata; //UART发送数据寄存器
reg[7:0] uart_tx_buf[0:2]; //发送缓存
reg [15:0]rst_cnt = 16'd0; //复位计数器assign uart_rstn_i = rst_cnt[15]; //复位//上电通过计数器计数,实现复位
always @(posedge sysclk_i)beginrst_cnt <= (rst_cnt[15] == 1'b0) ? (rst_cnt + 1'b1) : rst_cnt ;
end//数据发送状态机
always @(posedge sysclk_i)beginif(uart_rstn_i==1'b0)begin //初始化uart_tx_buf,为hello fpga等字符共计12 BYTES,以及其他寄存器uart_tx_buf[0] <=8'b10000001;uart_tx_buf[1] <=8'b10101010;uart_tx_buf[2] <=8'd5;uart_wdata <= 8'd0;uart_wreq <= 1'b0;S_UART_TX <= 2'd0;tx_index <= 2'd0;endelse begincase(S_UART_TX)0:beginif(!uart_wbusy)begin//如果UART发送驱动器不忙uart_wdata <= uart_tx_buf[tx_index];//准备发送数据,发送tx_index所指向的数据uart_wreq <= 1'b1; //设置uart_wreq为高电平,请求发送数据endelse begin //当总线忙uart_wreq <= 1'b0; //重置uart_wreqS_UART_TX <= 2'd1; //进入下一状态endend1:begin//该状态等待总线空闲S_UART_TX <= (uart_wbusy == 1'b0) ? 2'd2: S_UART_TX;end 2:begin//更新tx_index计数器if(tx_index < 3)begin //每一帧发送12个字节tx_index <= tx_index + 1'b1; //tx_index 加计数S_UART_TX <= 2'd0; //进入下一状态endelse begin //如果tx_index==11 代表所有数据发送完毕tx_index <= 2'd0; //重置tx_indexS_UART_TX <= 2'd3; //下一状态end end3: S_UART_TX <= S_UART_TX; endcaseend end//例化UART 发送驱动器模块
uiuart_tx#
(
.BAUD_DIV(100000000/115200-1) //波特率计算 BAUD_DIV = 系统时钟/波特率-1
)
uart_tx_u
(
.clk_i(sysclk_i),//系统时钟输入
.uart_rstn_i(uart_rstn_i), //系统复位输入
.uart_wreq_i(uart_wreq), //UART发送(写)数据请求
.uart_wdata_i(uart_wdata), //UART发送(写)数据
.uart_wbusy_o(uart_wbusy),//UART发送驱动器忙
.uart_tx_o(uart_tx_o) //UART 发送串行总线
);
endmodule
仿真结果
可以看到,那根开局的线就是按我们想要的时序发送数据的。
接收端
说完发送端我们再来看看接收端,它也是靠着一根数据线来接收数据的,时序和发送端一致,速率也一致。所以这里就不一点点的讲解了
module uart_rx#
(parameter integer BAUD_DIV = 10416 //波特率分频参数,BAUD_DIV=系统时钟频率/波特率-1 比如100M系统时钟,波特率115200 BAUD_DIV= 100_000_000/115200-1
)
(
input clk_i, //系统时钟输入
input uart_rx_rstn_i,//系统复位输入
input uart_rx_i,//uart rx 总线信号输入
output [7:0] uart_rdata_o,//uart rx接收到的数据输出
output uart_rvalid_o// uart rx 接收数据有效信号,当为1的时候uart_rdata_o数据有效
);localparam BAUD_DIV_SAMP = (BAUD_DIV/8)-1; //多次采样,按照波特率系数的八分之一进行采样wire bps_en ; //波特率使能信号
wire samp_en ; //采样使能信号
wire bit_cap_done ; //uart rx总线信号采样有效数据完成
wire uart_rx_done ; //uart 1byte 接收完成
wire bit_data ; //接收的1bit数据
wire uart_rx_int ; //uart_rx_int的启动信号检测,当变为低电平,代表可能存在起始位(UART 起始位为低电平)reg [13:0] baud_div = 14'd0;//波特率分频计数器
reg [13:0] samp_cnt = 14'd0;//采样计数器
reg [4 :0] uart_rx_i_r = 5'd0;//异步采集多次寄存
reg [3 :0] bit_cnt=4'd0;//bit 计数器
reg [3 :0] cap_cnt=4'd0;//cap 计数器
reg [4 :0] rx_bit_tmp = 5'd0;//rx_bit_tmp用于多次采样,通过计算采样到高电平次数和低电平次数,判断本次采样是高电平还是低电平
reg [7 :0] rx_data = 8'd0;//数据接收寄存器reg bps_start_en_r = 1'b0;
reg bit_cap_done_r = 1'b0;
reg bps_start_en,start_check_done,start_check_failed;assign bps_en = (baud_div == (BAUD_DIV - 1'b1)); //完成一次波特率传输信号
assign samp_en = (samp_cnt == (BAUD_DIV_SAMP - 1'b1 )); //完成一次波特率采样信号
assign bit_cap_done = (cap_cnt == 3'd7);//采样计数
assign uart_rx_done = (bit_cnt == 9)&&(baud_div == BAUD_DIV >> 1);//当停止位开始,提前半停止位,发送uart_rx_done信号,以便提前准备进入下一个数据的接收assign bit_data = (rx_bit_tmp < 5'd15) ? 0 : 1; //rx_bit_tmp用于多次采样,通过计算采样到高电平次数和低电平次数,判断本次采样是高电平还是低电平,提高抗干扰能力
//连续5次信号拉低,判断开始传输
assign uart_rx_int = uart_rx_i_r[4] | uart_rx_i_r[3] | uart_rx_i_r[2] | uart_rx_i_r[1] | uart_rx_i_r[0];
assign uart_rdata_o = rx_data;
assign uart_rvalid_o = uart_rx_done; //波特率计数器
always@(posedge clk_i)beginif(bps_start_en && baud_div < BAUD_DIV) //baud_div计数,目标值BAUD_DIV baud_div <= baud_div + 1'b1;else baud_div <= 14'd0;
end//8bit采样使能,8倍波特率采样,也就是这个计数器,用于产生8倍过采样
always@(posedge clk_i)beginif(bps_start_en && samp_cnt < BAUD_DIV_SAMP) //bps_start_en高电平有效,开始对bit进行采样,samp_cnt以8倍于波特率速度对每个bit采样samp_cnt <= samp_cnt + 1'b1; //samp_cnt计数+1 else samp_cnt <= 14'd0; //samp_cnt计数清零
end//uart rx bus asynchronous to Synchronous
always@(posedge clk_i)begin uart_rx_i_r <= {uart_rx_i_r[3:0],uart_rx_i}; //uart_rx_i的数据存入uart_rx_i_r进行缓存
end//uart接收启动检查
always@(posedge clk_i)beginif(uart_rx_rstn_i == 1'b0 || uart_rx_done || start_check_failed) //bps_start_en拉低的三种情况,复位、接收完成、校验失败bps_start_en <= 1'b0; //接收结束else if((uart_rx_int == 1'b0)&(bps_start_en==1'b0))//当判断到uart_rx_int == 1'b0,并且总线之前空闲(bps_start_en==1'b0,代表总线空闲)bps_start_en <= 1'b1;//使能波特率计数器使能
end//uart接收启动使能
always@(posedge clk_i)beginbps_start_en_r <= bps_start_en; //bps_start_en信号打一拍,方便后续上升沿捕捉
endalways@(posedge clk_i)beginif(uart_rx_rstn_i == 1'b0 || start_check_failed)begin//当系统复位,或者start_check_failed,重置start_check_done和start_check_failedstart_check_done <= 1'b0;start_check_failed <= 1'b0;end else if(bps_start_en == 1'b1&&bps_start_en_r == 1'b0) begin//当检测到start信号,也重置start_check_done和start_check_failedstart_check_done <= 1'b0;start_check_failed <= 1'b0;endelse if((bit_cap_done&&bit_cap_done_r==1'b0)&&(start_check_done == 1'b0))begin//第一个波特率采样,用于判断是否一个有效的起始位,如果不是有效的,start_check_failed设置为1start_check_failed <= bit_data ? 1'b1 : 1'b0;start_check_done <= 1'b1;//不管是否start_check_failed==1,都会设置start_check_done=1,但是start_check_failed==1,会下一个系统时钟重置start_check_done=0end
end//bits 计数器
always@(posedge clk_i)beginif(uart_rx_rstn_i == 1'b0 || uart_rx_done || bps_start_en == 1'b0)//复位、接收完成、或者总线空闲(bps_start_en == 1'b0),重置bit_cntbit_cnt <= 4'd0; else if(bps_en)//每一个bps_en有效,加1bit_cnt <= bit_cnt + 1'b1; // bit_cnt计数器用于计算当前采样了第几个bit
end//8次过采样,提高抗干扰
always@(posedge clk_i)beginif(uart_rx_rstn_i == 1'b0 || bps_en == 1'b1 || bps_start_en == 1'b0) begin //当uart_rx_rstn_i=0或者bps_en=1或者bps_start_en==0,重置cap_cnt和rx_bit_tmpcap_cnt <= 4'd0;rx_bit_tmp <= 5'd15; endelse if(samp_en)begin//bit采样使能cap_cnt <= cap_cnt + 1'b1;//cap_cnt用于记录了当前是第几次过采样,1个bit采样8次rx_bit_tmp <= uart_rx_i_r[4] ? rx_bit_tmp + 1'b1 : rx_bit_tmp - 1'b1; //多次采样,如果是高电平+1,如果是低电平-1,最终看本次bit采样结束rx_bit_tmp如果小于15代表是低电平end
end//寄存一次bit_cap_done,用于产生高电平触发脉冲下面用到
always@(posedge clk_i)bit_cap_done_r <= bit_cap_done;always@(posedge clk_i)beginif(uart_rx_rstn_i == 1'b0 || bps_start_en == 1'b0)//当复位或者总线空闲,重置rx_datarx_data <= 8'd0; else if(start_check_done&&(bit_cap_done&&bit_cap_done_r==1'b0)&&bit_cnt < 9)//当start_check_done有效,并且bit_cnt<9,每次bit_cap_done有效,完成一次移位寄存rx_data <= {bit_data,rx_data[7:1]}; //串并转换,将数据存入rx_data 中,共8位
endendmodule
仿真结果
仿真代码如下
module uart_top_tb();localparam BPS = 'd115200 ; //波特率
localparam CLK_FRE = 'd100_000_000 ; //系统频率
localparam CLK_TIME = 'd1000_000_000/CLK_FRE; //计算系统时钟周期,以ns为单位
localparam BIT_TIME = 'd1000_000_000/BPS ; //计算出传输每个bit所需要的时间以ns为单位
localparam NUM_BYTES = 3; //需要发送的BYTESreg sysclk_p; //系统时钟
reg uart_rstn; //系统复位
reg bsp_clk ; //波特率时钟
reg uart_tx; //uart 数据总线发送,接到UART接收模块的uart_rx
wire [7:0] uart_rx_data_o; //uart 接收到有效数据
wire uart_rvalid_o; //uart 接收数据有效信号reg [8*NUM_BYTES-1:0] uart_send_data; //需要发送的数据
reg [7:0] uart_send_data_r; //寄存每次需要发送的BYTEinteger i,j;//例化模块
uiuart_rx uart_top_inst
(
.clk_i(sysclk_p),
.uart_rx_rstn_i(uart_rstn),
.uart_rx_i(uart_tx),
.uart_rdata_o(uart_rx_data_o),
.uart_rvalid_o(uart_rvalid_o)
);//仿真初始化
initial begin //初始化REG寄存器
sysclk_p =0;
uart_rstn = 0;
bsp_clk = 0;
uart_tx = 1;
i=0;
j=0;uart_send_data =0;
uart_send_data_r =0;#1000;//延迟1000ns
uart_rstn =1; //复位完成
#1000;//延迟1000nsuart_send_data[(0*8) +: 8] = 8'b1001_0101;//初始化需要发送的第1个BYTE
uart_send_data[(1*8) +: 8] = 8'b0000_0101;//初始化需要发送的第2个BYTE
uart_send_data[(2*8) +: 8] = 8'b1000_0100;//初始化需要发送的第3个BYTE//uart tx 发送数据for(i=0; i<NUM_BYTES;i=i+1)beginuart_send_data_r = uart_send_data[(i*8) +: 8];//寄存需要发送的数据到寄存器$display("uart_send_data : 0x%h",uart_send_data_r);//打印准备发送的数据@(posedge bsp_clk); //发送起始位1bituart_tx = 1'b0;for(j=0;j<8;j=j+1)begin//发送数据8bits@(posedge bsp_clk); //发送uart_tx = uart_send_data_r[j];end@(posedge bsp_clk);//发送停止位1bituart_tx = 1'b1; end@(posedge bsp_clk); #200 $finish;
endalways #(CLK_TIME/2) sysclk_p = ~sysclk_p; //产生主时钟
always #(BIT_TIME/2) bsp_clk = ~bsp_clk; //产生波特率时钟endmodule
写在最后
希望对大家有所帮助,欢迎留言讨论,一起进步~