Uart通信协议
1.起始位,数据线从高变低,低有效为0,数据传输开始。1位时间低电平
2.数据位,起始位传输之后便是数据位开始,一般为8位,传输时低位(LSB)在前,高位(MSB)在后。
3.校验位,校验位可以认为是一个特殊的数据位,可以简便地校验数据是否出错,但是只知道数据是否出错,却不能纠正数据。奇校验指的是,加上校验位,共有奇数个1,偶校验指的是,加上校验位共有偶数个1。可以把所有数据位加起来,观察结果是奇数还是偶数来确定数据中1的个数,也可以用按位异或的方法快速判断。通常使用的是奇偶校验,奇校验在1的个数是奇数时等于1否则为0,偶校验在1的个数是偶数时等于1否则为0。
4.停止位,停止位高有效为1,他表示这一个个字节传输结束。1位时间高电平
位时间,起始位、数据位、校验位的位宽度是一致的,停止位有0.5位、1位、1.5位格式,一般为1位。持续的高电平是空闲位。从起始位开始到停止位结束的时间间隔称之为一帧。
波特率相关 如果波特率为9600 那么1000000000/9600/1000=105us(传输一位数据需要时间)。uart中tx、rx的时钟可以不同但波特率必须一致
为115200那么1000000000/115200/1000=9us(传输一位数据需要时间)
在50m时钟里,如果波特率为300那么 1000000000/300/20=166666次
uart工作流程
在uart协议中,发送方发出数据不需要等待接收方相应就可以发送下个数据包。在TX发送数据的同时,RX也可以接收数据,并且收发可以以不同的波特率收发数据。
发送UART通过数据总线接收来自控制设备的并行数据。控制设备可以是微处理器或微控制器的CPU,RAM或ROM等存储单元。
UART 为该数据添加起始位、奇偶校验位和停止位,以便将其转换为数据包。然后数据包在移位寄存器的帮助下,将8位并行数据的每个位在不同时刻传输,从并行转换为串行,并从 TX 引脚逐位传输。
接收 UART 在 RX 引脚接收此串行数据,并通过识别开始位和停止位来检测实际数据。奇偶校验位用于检查数据的完整性。停止位不仅仅是表示传输的结束,并且提供计算机校正时钟的机会。停止位个数越多,数据传输越稳定,但是数据传输速度也越慢。
在从数据包中分离起始位、奇偶校验位和停止位之后,在移位寄存器的帮助下将数据转换为并行数据。注意在最后的通信过程中,串并转换的过程以及接口是被封装在串口模块内部的。
连接
UART 通信只有两根信号线,一根是发送数据端口线叫 tx(Transmitter),一根是接收数据端口线叫 rx(Receiver),对于 PC 来说它的 tx 要和对于 FPGA来说的 rx 连接,同样 PC 的 rx 要和 FPGA 的 tx 连接,如果连接两个 tx 或者两个 rx 连接那数据就不能正常被发送出去和接收到。
bps_clk为串口时钟
在实际设计时,可以用起始位为0开始发送,停止位为1停止
设计要求
使用Verilog设计一个UART出来,其中UART具体满足的参数如下
数据位长度可调整、可选1位奇偶校验位;
波特率可调(波特率设置端口)
pc可以通过串口助手读到fpga发出的字符
发端比较简单,发送开始,先发一个0,再发所有数据,最后发个1等待下一个send_start。
收端相对就会比较麻烦了,因为要接收数据时序会更加复杂。
这里先给出uart发送端的代码,开始我尝试按照协议定义直接写,然后发现这样的适用性很不好,于是改为使用状态机的方式。发送端采用按键给出使能信号用于开始接收,至于接收端的使能采用下降沿检测:
状态机分为5个状态,idle、start、trans_data、parity、done
idle是默认初始状态,当数据传输完成后也会跳回这一状态用来保证各个量赋值正确
start:表示开始传输,tx拉低
trans_data:传输有效数据并采用tx_cnt计数,全部发送完后根据是否需要校验决定跳转发送校验和或者终止阶段
parity:传输校验和,默认是奇校验,偶校验需要取反
done:终止信号,tx发送恢复0信号,tx_flag拉低
module uart_tx#(
parameter PARITY_ON = 0, //校验位,1为有校验位,0为无校验位,缺省为0
PARITY_TYPE = 0, //校验类型,0为奇校验,1为偶校验,缺省为奇校验
baud_r = 115200,//波特率设置
clk_fre = 50_000_000,
data_width=8,
bps_cnt=clk_fre/baud_r
)(input clk,input rstn,input[data_width-1:0] data,input en,output reg tx,output busy);
reg en_d0,en_d1,tx_flag,parity_check;
wire en_flag;
assign en_flag=(~en_d1)&en_d0,busy=tx_flag;
always @(posedge clk or negedge rstn) begin// en data reg
if(!rstn)begin
en_d0<='d0;
en_d1<='d0;
end
else begin
en_d0<=en;
en_d1<=en_d0;
end end
reg [17:0]div_cnt;
reg [data_width-1:0]data_in;
reg [2:0] cstate,nstate;
reg [3:0] tx_cnt; //接收数据位计数
localparam idle = 3'b000, //空闲状态
start = 3'b001, //开始状态
trans_data = 3'b011, //数据发送状态
parity = 3'b100, //数据校验计算和发送
done = 3'b101; //结束状态
always @(posedge clk or negedge rstn) begin
if(!rstn)div_cnt<=18'd0;
else if(tx_flag)
begin
if(div_cnt==bps_cnt-1)
div_cnt<=0;
else div_cnt<=div_cnt+1'b1;
end
else div_cnt<=18'd0;
end
//FSM-1
always@(posedge clk or negedge rstn)
begin
if(!rstn)
cstate <= idle;
else if(!tx_flag)
cstate <= idle;
else if(div_cnt=='d0)
cstate <= nstate;
else ;
end
//FSM-2
always@(*)
begin
case(cstate)
idle: if(en_flag) nstate <= start; else ;
start: nstate <= trans_data;
trans_data:
if(tx_cnt == data_width)
begin
if(!PARITY_ON)
nstate <= done;
else
nstate <= parity; //校验位开启时进入校验状态
end
else
begin
nstate <= trans_data;
end
parity: nstate <= done;
done: nstate <= idle;
default: nstate <= idle;
endcase
end
always@(posedge clk or negedge rstn)
begin
if(!rstn)
begin
tx <= 1'b1;
tx_cnt <= 4'd0;
parity_check <= 1'b0;
data_in<={data_width{1'b0}};
tx_flag<='d0;
end
else
case(cstate)
idle:begin
tx <= 1'b1;
tx_cnt <= 4'd0;
parity_check <= 'd0;
if(en_flag)
begin
data_in<=data;
tx_flag<='d1;
end
else ;
end
start:begin
if(div_cnt==bps_cnt-1)
tx <= 1'b0;
// pull down tx to start transfer
end
trans_data:begin
if(div_cnt==bps_cnt-1)
begin
tx_cnt <=tx_cnt + 1'b1;
tx <= data_in[0];
parity_check <=parity_check + data_in[0];
data_in <= {1'b0 ,data_in[data_width-1:1]};
end
end
parity:begin
if(PARITY_TYPE&&div_cnt==bps_cnt-1 )
tx <= parity_check;
else if(div_cnt==bps_cnt-1)
tx <= ~parity_check ;
else ;
end
done:begin
if(div_cnt==bps_cnt-1)
begin
data_in<='d0;
tx <= 1'b1;
tx_flag<=0;
end
end
default:;
endcase
end
endmodule
接收模块的代码,一般引脚会设置一个完成输出端口和正在输出信号端口
状态机分为4个状态,idle、rece_data、parity、done
idle是默认初始状态,当数据接收完成后也会跳回这一状态用来保证各个量初始赋值正确.这里注意因为波特率的问题在收到传输开始信号后不能立刻跳转状态,必须等到有效数据开始传输时再跳转,不然时序错误收到的数据也是错的
rece_data:接收有效数据并采用rx_cnt计数,全部接收完后根据是否需要校验决定跳转接收校验和或者终止阶段
parity:接收校验和,默认是奇校验,偶校验需要取反,注意这里也需要保证波特率正确
done:终止信号,done信号拉高,并行发送有效数据
module uart_rx#(
parameter PARITY_ON = 0,
PARITY_TYPE = 0,
clk_fre = 50_000_000,
baud_r =115200,
data_width=8,
bps_cnt=clk_fre/baud_r
)(input clk,input rstn,input rx,output reg[data_width-1:0] data,output reg done_rx);
reg [17:0]div_cnt;
reg [data_width-1:0] data_d0;
reg[3:0]rx_cnt;
reg rx_flag, parity_check;
reg [2:0] cstate,nstate;
reg rx_d0,rx_d1,parity_flag;
wire en_flag;
assign en_flag=rx_d1&(~rx_d0);
localparam idle = 3'b000,
rece_data = 3'b010,
parity = 3'b011,
done = 3'b100;
always @(posedge clk or negedge rstn) begin// rx data reg
if(!rstn)begin
rx_d0<='d1;
rx_d1<='d1;
end
else begin
rx_d0<=rx;
rx_d1<=rx_d0;
end
end
always @(posedge clk or negedge rstn) begin
if(!rstn)div_cnt<=18'd0;
else if(rx_flag)
begin
if(div_cnt==bps_cnt-1)
div_cnt<=0;
else div_cnt<=div_cnt+1'b1;
end
else div_cnt<=18'd0;
end
//FSM-1
always@(posedge clk or negedge rstn)
begin
if(!rstn)
cstate <= idle;
else if(!rx_flag)
cstate <= idle;
else if(div_cnt==18'd0)
cstate <= nstate;
end
//FSM-2
always@(*)
begin
case(cstate)
idle:if(div_cnt==bps_cnt-1) nstate<=rece_data; else ;
rece_data:
if(rx_cnt == data_width)
begin
if(!PARITY_ON )
nstate <= done;
else
nstate <= parity;
end
else ;
parity: nstate <= done;
done: nstate <= idle;
default: nstate <= idle;
endcase
end
always@(posedge clk or negedge rstn)
begin
if(!rstn)
begin
rx_cnt <= 4'd0;
parity_check <= 1'b0;
data_d0<={data_width{1'b0}};
data<={data_width{1'b0}};
done_rx<=0;
rx_flag <= 'd0;
parity_flag<='d0;
end
else
case(cstate)
idle:begin
rx_cnt <= 4'd0;
parity_check <= 'd0;
if(en_flag)
rx_flag <= 1'b1;
done_rx<=0;
end
rece_data:begin
if(div_cnt==bps_cnt/2)
begin
rx_cnt <=rx_cnt + 1'b1;
data_d0<= {rx_d1, data_d0[data_width-1 : 1]};
parity_check <=parity_check + rx_d1;
end
else ;
end
parity:
if(div_cnt==bps_cnt/2)
begin
if(parity_check + rx_d1 == PARITY_TYPE )
parity_flag<='d1;
end
done:begin
if(div_cnt==bps_cnt/2)
begin
if(~PARITY_ON||parity_flag )
begin
data <= data_d0;
done_rx <= 1'b1;
rx_flag <= 1'b0;
parity_flag<='d0;
end
else
begin
done_rx <= 1'b0;
rx_flag <= 1'b0;
data_d0<='d0;
data<='d0;
parity_flag<='d0;
end
end
else ;
end
default:;
endcase
end
endmodule
我们还需要一个串口环回模块将它们连接起来,完成并串转换
module zynq_uart_loop(input clk,input rstn,input rx_done,
input[7:0]rx_data,input tx_busy,output reg tx_en,
output reg [7:0]tx_data );
reg rx_done_d0,rx_done_d1,tx_ready;
wire rx_done_flag;
assign rx_done_flag=(~rx_done_d1)&rx_done_d0;
always @(posedge clk or negedge rstn) begin
if(!rstn)
begin
tx_ready<='d0;
tx_en<='d0;
tx_data<='d0;
end
else begin
if(rx_done_flag)
begin
tx_ready<='d1;
tx_en<='d0;
tx_data<=rx_data;
end
else if(tx_ready&&(~tx_busy)) begin
tx_ready<='d0;
tx_en<='d1;
end
end
end
always @(posedge clk or negedge rstn) begin
if(!rstn)
begin
rx_done_d0<='d0;
rx_done_d1<='d1;
end
else begin
rx_done_d0<=rx_done;
rx_done_d1<=rx_done_d0;
end
end
endmodule
顶层模块 这里给的是奇校验的代码,偶校验和零校验是一样的
module zynq_uart_top (
input clk,input rstn,input rx,output tx
);
wire rx_done,tx_en,tx_busy;
wire[7:0]rx_data,tx_data;
parameter PARITY_ON = 1,
PARITY_TYPE = 0;
zynq_uart_loop loop_u(
.clk(clk),.rstn(rstn),.rx_done(rx_done),.tx_data(tx_data),
.rx_data(rx_data),.tx_busy(tx_busy),.tx_en(tx_en)
);
uart_tx #(
.PARITY_ON (PARITY_ON),
.PARITY_TYPE (PARITY_TYPE)) tx_u
(
.clk(clk),.rstn(rstn),.en(tx_en),.data(tx_data),
.busy(tx_busy),.tx(tx));
uart_rx #(
.PARITY_ON (PARITY_ON),
.PARITY_TYPE (PARITY_TYPE)) rx_u
(
.clk(clk),.rstn(rstn),.data(rx_data),
.done_rx(rx_done),.rx(rx));
endmodule
板上测试
奇校验:
odd
偶校验:
even
零校验:
none
之前的视频都是通过typec线连接fpga内部的uart芯片传输信息的,最后把输入输出引脚改到一般io口并连接usb转ttl模块:
genral io uart
参考博客
https://blog.csdn.net/qq_38812860/article/details/119940848
https://zhuanlan.zhihu.com/p/549612117
https://www.amobbs.com/thread-5757915-1-5.html