概述
FPGA实现串口通信
一、基本概念
1、并行通信:指数据的多个位用多条数据线同时传输,同时具备传输速度快的优点和占用引脚资源多的缺点。
2、串行通信:将数据分成一位一位的形式在一条传输线上逐个传输,优缺点与并行通信相反。
3、同步通信:带时钟同步信号的数据传输,发送方和接收方在同一时钟的控制下,同步传输数据。
4、异步通信:不带时钟同步信号的数据传输。发送方与接收方使用各自的时钟控制数据的发送和接收过程。
5、串行通信的传输方向
单工:数据只能沿一个方向传输
半双工:数据传输可以沿两个方向,但同一时刻只能传输一个方向
双工:数据可以同时进行双向传输
6、常见的串行通信接口
通信标准 | 引脚说明 | 通信方式 | 通信方向 |
---|---|---|---|
UART | TXD:发送 RXD:接收 GND:公共地 | 异步 | 双工 |
单总线 | DQ: 发送/接收端 | 异步 | 半双工 |
SPI | SCK:同步时钟 MISO:主机输入,从机输出 MOSI:主机输出,从机输入 | 同步 | 双工 |
IIC | SCL:同步时钟 SDA:数据输入输入端 | 同步 | 半双工 |
二、串口特性
1、串口基本概念:通用异步收发传输器(Universal Asynchronous Receiver/Transmitter,通常称作UART) 是一种串行异步收发协议。
2、通信速率:串口通信的速率用波特率表示,波特率是指从一设备发到另一设备的波特率,即每秒钟可以通信的数据比特个数。UART波特率受发送和接收线对距离(线长度)的影响。常用的波特率有9600、19200、38400、57600、115200等
3、适用场景:一般适用于 传输速度要求不高,距离短的场合。
4、电平标准:TTL、RS-232、RS-485是适用串口的三种电平标准(电信号),常见接口形式如4针杜邦线(TTL),DB9(RS232)。PL2303、CP2102芯片是USB 转 TTL串口 的芯片,用USB来扩展串口(TTL电平)。MAX232芯片是TTL电平与RS232电平的专用双向转换芯片,可以TTL转RS-232,也可以RS-232转TTL。uart是一种异步通信协议。而rs232只是物理层的电气接口要求。uart可以使用rs232物理层来进行通信。而rs232作为物理层也可以用其余不同于uart的协议来做通信。
为什么串口有这么多不同的电气标准?有了UART这个东西之后是不是就可以直接在设备之间用了呢?如果是在一块板子上这里没有太大问题。现在大多数芯片的输出电压都是TTL的,所以电平兼容,而且一块板子上干扰又不会太高。但要是用在设备与设备之间通信,那可能就会有问题了。比如说主控电脑和电机驱动器之间。这种情况下TTL的抗噪能力就比较单薄了。比较简单的做法就是增加逻辑1和逻辑0之间的电压差,比如像RS232这种接口中定义的电平格式。还有用差分信号的方式,比如RS485。没错,RS232只是一种接口,定义了接口的外观、各个引脚的功能、信号0和1对应的电压等。
三、串口通信协议
UART串口通信需要两根信号线来实现,一根用于串口发送,另外一根负责串口接收,要求通信双方配置相同的通信参数。UART在发送或接收过程中的一帧数据由 4 部分组成,起始位、数据位、奇偶校验位和停止位。
空闲位:UART协议规定,当总线处于空闲状态时信号线的状态为‘1’即高电平.
起始位:开始进行数据传输时发送方要先发出一个低电平’0’来表示传输字符的开始。因为空闲位一直是高电平所以开始第一次通讯时先发送一个明显区别于空闲状态的信号即为低电平。
数据位:起始位之后就是要传输的数据,数据可以是5,6,7,8,9位,构成一个字符,一般都是8位。先发送最低位最后发送最高位。
奇偶校验位:数据位传送完成后,要进行奇偶校验,校验位其实是调整个数,串口校验分几种方式:
1.无校验
2.奇校验:如果数据位中’1’的数目是偶数,则校验位为’1’,如果’1’的数目是奇数,校验位为’0’。
3.偶校验:如果数据为中’1’的数目是偶数,则校验位为’0’,如果为奇数,校验位为’1’。
停止位:数据结束标志,可以是1位,1.5位,2位的高电平。
四、逻辑开发
4.1verilog代码
Top模块:
module uart_top(
input clk,//50M
input reset_n,
input uart_rx,
output uart_tx,
output led
);
//确定baud周期
parameter Baud = 3'd0;
reg [15:0]bclk_cnt; //一个串口位占据的时钟个数
always@(posedge clk or negedge reset_n)
if(~reset_n)
bclk_cnt <= 16'd5207;//1s/9600/20ns=5208 9600Bau
else begin
case(Baud)
0:bclk_cnt <= 16'd5207; //9600Bau
1:bclk_cnt <= 16'd2603; //19200Bau
2:bclk_cnt <= 16'd1301; //38400Bau
3:bclk_cnt <= 16'd867; //57600Bau
4:bclk_cnt <= 16'd433; //115200Bau
default:bclk_cnt <= 16'd5207;
endcase
end
//获取数据接收起始位
reg uart_rx_reg1;
reg uart_rx_reg2;
reg uart_rx_reg3;
wire uart_rx_nedge;
always@(posedge clk or negedge reset_n)
if(~reset_n)begin
uart_rx_reg1 <= 1'b0;
uart_rx_reg2 <= 1'b0;
uart_rx_reg3 <= 1'b0;
end
else begin
uart_rx_reg1 <= uart_rx;
uart_rx_reg2 <= uart_rx_reg1;
uart_rx_reg3 <= uart_rx_reg2;
end
//下降沿检测
assign uart_rx_nedge = !uart_rx_reg2 & uart_rx_reg3;
//#####################################3.state machine
reg [2:0] state;
parameter IDLE = 3'd0;
parameter START = 3'd1;
parameter DATA = 3'd2;
parameter STOP = 3'd3;
always@(posedge clk or negedge reset_n)
if(!reset_n)
begin
state<=IDLE;
end
else case(state)
IDLE: begin
state<=uart_rx_nedge? START:IDLE;
end
START:begin
if(start_cnt==bclk_cnt)begin
if(begin_flag<2)
state<=DATA;
else
state<=IDLE;
end else
state<=START;
end
DATA:begin
state <=(data_cnt==4'd8&&start_cnt==bclk_cnt)? STOP :DATA;
end
STOP:begin
state<=IDLE;
end
endcase
//baud周期计数,计满为一个波特周期(时钟分频)
reg [15:0]start_cnt;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
start_cnt<=0;
else if(start_cnt==bclk_cnt)begin
start_cnt<=0;
end else if(state==START||state==DATA)begin
start_cnt<=start_cnt+1;
end
end
//起始信号干扰排除
reg [2:0]begin_flag;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
begin_flag<=3'd0;
else if(state==START)begin
if(start_cnt==16'd1950||start_cnt==16'd2275||start_cnt==16'd2600||start_cnt==16'd2925||
start_cnt==16'd3250||start_cnt==16'd3575)begin
begin_flag<=begin_flag+uart_rx_reg2;
end else
begin_flag<=begin_flag;
end else
begin_flag<=0;
end
//接收数据排序
reg [3:0]data_cnt;
always @(posedge clk or negedge reset_n)begin
if(!reset_n)
data_cnt<=0;
else if(state==DATA)begin
if(start_cnt==16'd1)begin
data_cnt<=data_cnt+1;
end else
data_cnt<=data_cnt;
end
else
data_cnt<=0;
end
//寄存接收到的数据
reg [7:0]cur_data;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)begin
cur_data[0]<=3'd0;
cur_data[1]<=3'd0;
cur_data[2]<=3'd0;
cur_data[3]<=3'd0;
cur_data[4]<=3'd0;
cur_data[5]<=3'd0;
cur_data[6]<=3'd0;
cur_data[7]<=3'd0;
end
else if(state==DATA)begin
if(start_cnt==bclk_cnt/2)begin
case(data_cnt)
4'd1 : cur_data[0] <= uart_rx_reg2; //寄存数据位最低位
4'd2 : cur_data[1] <= uart_rx_reg2;
4'd3 : cur_data[2] <= uart_rx_reg2;
4'd4 : cur_data[3] <= uart_rx_reg2;
4'd5 : cur_data[4] <= uart_rx_reg2;
4'd6 : cur_data[5] <= uart_rx_reg2;
4'd7 : cur_data[6] <= uart_rx_reg2;
4'd8 : cur_data[7] <= uart_rx_reg2;
default:;
endcase
end
end
end
//接收结束,发出发送指令(数据同步信号,50M一个周期)
wire rx_bit_done;
assign rx_bit_done=(state==STOP)? 1'b1:1'b0;
uart_my_tx u1_uart_tx(
.clk(clk),
.reset_n(reset_n),
.data_byte(cur_data),
.send_en(rx_bit_done),
.baud_set(Baud),
.uart_tx(uart_tx),
.tx_flag(led)
);
endmodule
子模块:
module uart_my_tx(
input clk,
input reset_n,
input [7:0]data_byte,
input send_en,
input baud_set,
output reg uart_tx,
output tx_flag
);
assign tx_flag=start_flag;//busy信号相当于
//寄存发送数据
reg [7:0]data_byte_reg;//data_byte寄存后数据
always@(posedge clk or negedge reset_n)
if(!reset_n)
data_byte_reg <= 8'd0;
else if(send_en)
data_byte_reg <= data_byte;
else
data_byte_reg <= data_byte_reg;
//确定baud周期
reg [15:0]bclk_cnt; //一个串口位占据的时钟个数
always@(posedge clk or negedge reset_n)
if(~reset_n)
bclk_cnt <= 16'd5207;//1s/9600/20ns=5208 9600Bau
else begin
case(baud_set)
0:bclk_cnt <= 16'd5207; //9600Bau
1:bclk_cnt <= 16'd2603; //19200Bau
2:bclk_cnt <= 16'd1301; //38400Bau
3:bclk_cnt <= 16'd867; //57600Bau
4:bclk_cnt <= 16'd433; //115200Bau
default:bclk_cnt <= 16'd5207;
endcase
end
//do transfer
reg start_flag;//指示写过程正在进行,可以据此产生busy信号
reg tx_done;//指示写结束
always@(posedge clk or negedge reset_n)
if(~reset_n)
start_flag<=0;
else if(send_en)
start_flag<=1;
else if(pluse_cnt == 4'd10)
start_flag<=0;
else
start_flag<=start_flag;
//波特计数 计满为一个波特周期
reg [15:0]baud_cnt;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
baud_cnt<=0;
else if(start_flag)begin
if(baud_cnt==bclk_cnt)
baud_cnt<=0;
else
baud_cnt<=baud_cnt+1;
end
else
baud_cnt<=0;
end
//产生发送数据脉冲
reg tran_pluse;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
tran_pluse<=0;
else if(baud_cnt==16'd1)begin
tran_pluse<=1;
end
else
tran_pluse<=0;
end
//脉冲计数使数据顺序发送
reg [3:0]pluse_cnt;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
pluse_cnt<=0;
else if(pluse_cnt==4'd10)begin
pluse_cnt<=0;
end else if(tran_pluse)
pluse_cnt<=pluse_cnt+1;
else
pluse_cnt<=pluse_cnt;
end
//产生tx_done数据发送完毕指示
always@(posedge clk or negedge reset_n)
if(!reset_n)
tx_done <= 1'b0;
else if(pluse_cnt == 4'd10)
tx_done <= 1'b1;
else
tx_done <= 1'b0;
localparam START_BIT = 1'b0;
localparam STOP_BIT = 1'b1;
always@(posedge clk or negedge reset_n)
if(!reset_n)
uart_tx <= 1'b1;
else begin
case(pluse_cnt)
0:uart_tx <= 1'b1;
1:uart_tx <= START_BIT;
2:uart_tx <= data_byte_reg[0];
3:uart_tx <= data_byte_reg[1];
4:uart_tx <= data_byte_reg[2];
5:uart_tx <= data_byte_reg[3];
6:uart_tx <= data_byte_reg[4];
7:uart_tx <= data_byte_reg[5];
8:uart_tx <= data_byte_reg[6];
9:uart_tx <= data_byte_reg[7];
// 10:uart_tx <= STOP_BIT;
default:uart_tx <= 1'b1;
endcase
end
reg [3:0]tr_cnt;
always @(posedge clk or negedge reset_n) begin
if(!reset_n)
tr_cnt <= 0;
else if(tx_done)
tr_cnt<=tr_cnt+1;
else
tr_cnt<=tr_cnt;
end
ila_0 your_instance_name (
.clk(clk), // input wire clk
.probe0({tran_pluse,tr_cnt,pluse_cnt,start_flag}), // input wire [0:0] probe0
.probe1(data_byte_reg), // input wire [7:0] probe1
.probe2(tx_done), // input wire [0:0] probe2
.probe3(uart_tx) // input wire [0:0] probe3
);
endmodule
4.2代码解读
1.描述
该串口驱动的基本配置为:9600Baud、1bit停止位,8Bit数据位,无奇偶校验位;
该驱动运行的效果为:接受一个8bit数据,便产生一个send_en指示脉冲,指示发送模块将这8bit数据发送出去。在接收完成之后继续接收,在发送完成之后继续等待发送。
2.步骤解读(从Top开始)
①结合系统时钟以及驱动配置,使用信号bclk_cnt计算出9600Baud情况下,一个“Baud”的时钟个数,并给出易于修改波特率的方法。
②通过经典代码,捕获uart_rx管脚的下降沿(uart_rx_nedge)便于捕获起始信号。
③通过begin_flag信号对起始信号的干扰做一个判断和甄别,在状态机中,将据此信号决定状态机的走向。
④通过data_cnt信号与接收数据对齐,从而便于在start_cnt==bclk_cnt/2(某位数据传输的正中间最稳定的时候)寄存串口数据。
⑤start_cnt信号计满为一个波特周期,便于数据寄存。
⑥最后在top中生成rx_bit_done脉冲信号,用于指示将接收到的数据传送回去。
⑦步骤1-5在top中完成对数据的接收,并产生的指示发送信号,进入到子模块中,收到send_en后,将接收到的8bit数据寄存到data_byte_reg信号。
⑧根据top指示,选择设置的Baud(bclk_cnt信号)用于发送数据。
⑨根据send_en和发送过程未结束产生tx_flag,相当于busy信号,该信号通过top连接至led。
⑩根据子模块中计满波特周期信号baud_cnt结合tran_pluse脉冲信号,生成数据同步信号pluse_cnt,便于数据的发送。
通过给uart_tx赋值完成数据发送,并产生tx_done信号表示一次8bit发送结束。
3.注意事项
①波特率指每秒传输的数据位数,系统使用不同的用户时钟,采用不同的波特率可能会造成,传输每一位数据所需的时钟个数成为小数,但较小偏差的波特率在串口通信是被允许的。
②当数据寄存完成后,可以提前一点结束接收过程,也就提前开始了下一个数据的监听,为下一帧数据的起始位预留时间。
③采样时刻选取在每一位数据(每一个波特周期)的中点,确保数据稳定。
⑤为了防止在传输的过程中数据出现变化,需要将数据做寄存处理。
⑥通信双方的波特率、停止位、数据位、校验位的设置必须一致才能正确通信。
⑦若时钟频率为50Mhz,得到一位数据需要的时钟数,基本的分频计数单元(每一位数据或者起始位停止位)1000000000(1s)/115200=8680ns 。每一位的时间是8680ns 用计数器计数8680/20=434,则计数434-1次即可。
五、效果
如图所示,是与Qt写的上位机进行通信实验的结果,可以看到配置一致,串口工作,将发送的数据全部返回。请查看“C2—Qt实现串口调试助手2021.10.21”讲述使用Qt写串口上位机
最后
以上就是危机雨为你收集整理的D1--FPGA串口通信UART-2022.07.06FPGA实现串口通信的全部内容,希望文章能够帮你解决D1--FPGA串口通信UART-2022.07.06FPGA实现串口通信所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复