FPGA学习系列
接下来介绍串口通信控制,本次就不用串口回环了,像正点原子,野火等fpga教程都会教串口回环,所以本文我将介绍如何串口控制去执行一些操作。之后也将拓展FPGA控制串口屏。
介绍
串口通信一般有异步通信,同步通信。又有全双工通信、半双工通信、单工通信。
按接口又有RS422、RS232、RS485。其区别我就不多赘述。
协议
UART串口通信协议需要两根线,一根RX用于接收数据,一根TX用于发送数据。一般来说一帧有10位。其中第一位为起始位:数据线拉低开始即低电平有效,中间8位数据位,最后加上停止位:数据线拉高 即高电平停止。如下图所示。
发送时序流程图
每个时钟周期发送一位,第一位为启动位即TX线拉低表示开始,中间8位数据位最后一位作校验位。之后再有一位停止位即TX线拉高。所有一共10位即一帧10个bit。
波特率
波特率就是每秒传输多少个bit,常见的有9600,115200.这里我以115200为例子。115200就是每秒传输115200个bit。所以传输一个bit需要1s/115200≈8.68us。而我的板子系统时钟是50MHZ也就是1秒有50000000个时钟周期。现在只要其中8.68us去发送一个bit。也就是 50 * 8.68=434(这里是50_000_000*8.68us 我把里0约掉了)。所以只要定义一个计数器去计数到434然后产生一次上升沿就可以产生115200的波特率了。
tip:为什么我用50 * 8.68呢,因为只要8.68us而1秒有50_000_000个周期所以只要算出8.68us占了几个周期就行也就是8.68 * 50.
发送模块
对于FPGA的串口发送就不用像stm32初始化结构体,清除中断标志等操作。只需按照时序编写即可实现串口发送。我都会添加详细注释。我程序中是直接50000000/115200 结果也是434.通用公式也就是频率除以波特率
module uart_send(
input sys_clk, //系统时钟
input sys_rst_n, //系统复位,低电平有效
input uart_en, //发送使能信号
input [7:0] uart_din, //传入要发送的数据
output uart_tx_busy, //发送忙状态标志
output reg uart_txd //UART发送端口
);
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 115200; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; //为得到指定波特率,对系统时钟计数BPS_CNT次 方便修改
reg uart_en_d0;
reg uart_en_d1;
reg [15:0] clk_cnt; //系统时钟计数器
reg [ 3:0] tx_cnt; //发送数据计数器
reg tx_flag; //发送过程标志信号
reg [ 7:0] tx_data; //寄存发送数据
wire en_flag; //使能信号
//*****************************************************
//** 主函数
//*****************************************************
//在串口发送过程中给出忙状态标志
assign uart_tx_busy = tx_flag;
//捕获uart_en上升沿,得到一个时钟周期的脉冲信号 表示开始
assign en_flag = (~uart_en_d1) & uart_en_d0;//这里是获取按键发送的使能信号
//按键按下 会产生一个按键信号获取其上升沿表示开始发送
//对发送使能信号uart_en延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_en_d0 <= 1'b0;
uart_en_d1 <= 1'b0;
end
else begin
uart_en_d0 <= uart_en;
uart_en_d1 <= uart_en_d0;
end
end
//当脉冲信号en_flag到达时,寄存待发送的数据,并进入发送过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
tx_flag <= 1'b0;
tx_data <= 8'd0;
end
else if (en_flag) begin //检测到发送使能上升沿
tx_flag <= 1'b1; //进入发送过程,标志位tx_flag拉高
tx_data <= uart_din; //寄存待发送的数据
end
//计数到停止位结束时,停止发送过程 计数从0-9也就是10位
else if ((tx_cnt == 4'd9) && (clk_cnt == BPS_CNT -(BPS_CNT/16))) begin
tx_flag <= 1'b0; //发送过程结束,标志位tx_flag拉低
tx_data <= 8'd0;
end
else begin
tx_flag <= tx_flag;
tx_data <= tx_data;
end
end
//进入发送过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_cnt <= 16'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt < BPS_CNT - 1)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
end
else
clk_cnt <= 16'd0; //发送过程结束
end
//进入发送过程后,启动发送数据计数器 统计此时发送了几个bit
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
tx_cnt <= 4'd0;
else if (tx_flag) begin //处于发送过程
if (clk_cnt == BPS_CNT - 1) //对系统时钟计数达一个波特率周期
tx_cnt <= tx_cnt + 1'b1; //此时发送数据计数器加1
else
tx_cnt <= tx_cnt;
end
else
tx_cnt <= 4'd0; //发送过程结束
end
//根据发送数据计数器来给uart发送端口赋值
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
uart_txd <= 1'b1;
else if (tx_flag)
case(tx_cnt) //利用case语句发送一帧的数据
4'd0: uart_txd <= 1'b0; //起始位
4'd1: uart_txd <= tx_data[0]; //数据位最低位
4'd2: uart_txd <= tx_data[1];
4'd3: uart_txd <= tx_data[2];
4'd4: uart_txd <= tx_data[3];
4'd5: uart_txd <= tx_data[4];
4'd6: uart_txd <= tx_data[5];
4'd7: uart_txd <= tx_data[6];
4'd8: uart_txd <= tx_data[7]; //数据位最高位
4'd9: uart_txd <= 1'b1; //停止位
default: ;
endcase
else
uart_txd <= 1'b1; //空闲时发送端口为高电平
end
endmodule
接收模块
module uart_recv
(
input wire sys_clk, //系统时钟
input wire sys_rst_n, //系统复位,低电平有效
input uart_rxd, //UART接收端口
output reg uart_done, //接收一帧数据完成标志
output reg rx_flag, //接收过程标志信号
output reg [ 3: 0] rx_cnt, //接收数据计数器
output reg [ 7: 0] uart_data //接收的数据 然后去判断
);
parameter CLK_FREQ = 50000000; //系统时钟频率
parameter UART_BPS = 115200; //串口波特率
localparam BPS_CNT = CLK_FREQ/UART_BPS; //为得到指定波特率 需要对系统时钟计数BPS_CNT次
reg uart_rxd_d0;
reg uart_rxd_d1;
reg [15:0] clk_cnt; //系统时钟计数器
reg [7:0] rxdata; //接收数据缓存
wire start_flag;
//*****************************************************
//** 主函数
//*****************************************************
//捕获接收端口下降沿(起始位),得到一个时钟周期的脉冲信号
assign start_flag = uart_rxd_d1 & (~uart_rxd_d0);
//对UART接收端口的数据延迟两个时钟周期
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_rxd_d0 <= 1'b0;
uart_rxd_d1 <= 1'b0;
end
else begin
uart_rxd_d0 <= uart_rxd;
uart_rxd_d1 <= uart_rxd_d0;
end
end
//当脉冲信号start_flag到达时,进入接收过程
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rx_flag <= 1'b0;
else begin
if(start_flag) //检测到起始位
rx_flag <= 1'b1; //进入接收过程,标志位rx_flag拉高
//计数到停止位中间时,停止接收过程 如果计数到结束的时候容易出现错误 中间采集最稳定
else if((rx_cnt == 4'd9) && (clk_cnt == BPS_CNT/2))
rx_flag <= 1'b0; //接收过程结束,标志位rx_flag拉低
else
rx_flag <= rx_flag;
end
end
//进入接收过程后,启动系统时钟计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
clk_cnt <= 16'd0;
else if ( rx_flag ) begin //处于接收过程
if (clk_cnt < BPS_CNT - 1)
clk_cnt <= clk_cnt + 1'b1;
else
clk_cnt <= 16'd0; //对系统时钟计数达一个波特率周期后清零
end
else
clk_cnt <= 16'd0; //接收过程结束,计数器清零
end
//进入接收过程后,启动接收数据计数器
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n)
rx_cnt <= 4'd0;
else if ( rx_flag ) begin //处于接收过程
if (clk_cnt == BPS_CNT - 1) //对系统时钟计数达一个波特率周期
rx_cnt <= rx_cnt + 1'b1; //此时接收数据计数器加1
else
rx_cnt <= rx_cnt;
end
else
rx_cnt <= 4'd0; //接收过程结束,计数器清零
end
//根据接收数据计数器来寄存uart接收端口数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if ( !sys_rst_n)
rxdata <= 8'd0;
else if(rx_flag) //系统处于接收过程
if (clk_cnt == BPS_CNT/2) begin //判断系统时钟计数器计数到数据位中间
case ( rx_cnt )
4'd1 : rxdata[0] <= uart_rxd_d1; //寄存数据位最低位
4'd2 : rxdata[1] <= uart_rxd_d1;
4'd3 : rxdata[2] <= uart_rxd_d1;
4'd4 : rxdata[3] <= uart_rxd_d1;
4'd5 : rxdata[4] <= uart_rxd_d1;
4'd6 : rxdata[5] <= uart_rxd_d1;
4'd7 : rxdata[6] <= uart_rxd_d1;
4'd8 : rxdata[7] <= uart_rxd_d1; //寄存数据位最高位
default:;
endcase
end
else
rxdata <= rxdata;
else
rxdata <= 8'd0;
end
//数据接收完毕后给出标志信号并寄存输出接收到的数据
always @(posedge sys_clk or negedge sys_rst_n) begin
if (!sys_rst_n) begin
uart_data <= 8'd0;
uart_done <= 1'b0;
end
else if(rx_cnt == 4'd9) begin //接收数据计数器计数到停止位时
uart_data <= rxdata; //寄存输出接收到的数据
uart_done <= 1'b1; //并将接收完成标志位拉高
end
else begin
uart_data <= 8'd0; //其余时刻一直为0
uart_done <= 1'b0;
end
end
endmodule
串口控制模块
module led_crtl
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [7:0] rx_data , //接收到的数据
input wire led_en , //led控制使能
output reg [3:0] led ,
output reg beep
);
reg led_en_d0;//用来打拍
reg led_en_d1;//用来打拍
parameter CNT_MAX=24'd10_000_000;
//流水灯
reg [23:0] cnt;
reg [1:0] led_crtl_water;
reg led_water;//流水灯使能信号
wire start_en;
assign start_en = led_en_d0 & ~led_en_d1;//获取上升沿
//流水灯延时计数0.2s
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
cnt <= 24'd0;
else if(cnt == CNT_MAX - 1'b1)
cnt <= 24'd0;
else if(led_water)
cnt <= cnt + 1'b1;
else
cnt <= 24'd0;
end
//流水灯
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led_crtl_water <= 2'd0;
else if(cnt == CNT_MAX - 1'b1&&led_water)
led_crtl_water <= led_crtl_water + 1'b1;
else
led_crtl_water <= led_crtl_water;
end
//控制信号获取
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
led_en_d0 <= 1'b0;
led_en_d1 <= 1'b0;
end
else begin
led_en_d0 <= led_en ;
led_en_d1 <= led_en_d0;
end
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
led <= 4'd0;
else if(start_en)
case(rx_data) //根据接收到的数据进行判断
8'h01 : led<=4'b0001;
8'h02 : led<=4'b0010;
8'h03 : led<=4'b0100;
8'h04 : led<=4'b1000;
8'h05 : led_water<=1'b1; //开启流水灯
8'h06 : led_water <= 1'b0; //关闭流水灯
8'hbe : beep<=1'b1;
8'hbf : beep<=1'b0;
8'haa : led<=4'b1111; //a
8'ha0 : led<=4'b0000; //
endcase
else if(led_water)
case(led_crtl_water)
2'b00 : led <= 4'b0001;
2'b01 : led <= 4'b0010;
2'b10 : led <= 4'b0100;
2'b11 : led <= 4'b1000;
default : led<=4'b0000;
endcase
end
endmodule
按键消抖模块
//按键消抖
module key_filter
#(
parameter CNT_MAX = 20'd999_999
)
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire key_in ,
output reg key_flag //1表示按下 0表示没有按下
);
reg [19:0] cnt_20ms; //计数器
always@(posedge sys_clk or negedge sys_rst_n) begin
if(sys_rst_n == 1'b0)
cnt_20ms <= 20'b0;
else if(key_in == 1'b1)
cnt_20ms <= 20'b0;
else if(cnt_20ms == CNT_MAX && key_in == 1'b0)
cnt_20ms <= cnt_20ms;
else
cnt_20ms <= cnt_20ms + 1'b1;
end
//key_flag:当计数满20ms后产生按键有效标志位
//且key_flag在999_999时拉高,维持一个时钟的高电平
always@(posedge sys_clk or negedge sys_rst_n)
if(sys_rst_n == 1'b0)
key_flag <= 1'b0;
else if(cnt_20ms == CNT_MAX - 1'b1)
key_flag <= 1'b1;
else
key_flag <= 1'b0;
endmodule
按键控制模块只用于发送数据
按键控制模块
module key_data
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire [3:0] key ,
output reg [7:0] key_data_out ,
output reg key_done
);
wire K1 ; //四个按键
wire K2 ; //四个按键
wire K3 ; //四个按键
wire K4 ; //四个按键
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n)
key_data_out <= 8'd0;
else if(K1==1'b1)
key_data_out <= 8'h01;
else if(K2==1'b1)
key_data_out <= 8'h02;
else if(K3==1'b1)
key_data_out <= 8'hbe;
else if(K4==1'b1)
key_data_out <= 8'hbf;
end
always@(posedge sys_clk or negedge sys_rst_n) begin
if(!sys_rst_n) begin
key_done <= 1'b0;
end
else if(K1||K2||K3||K4)
key_done <= 1'b1; //按键按下标志
else
key_done <= 1'b0;
end
//按键例化
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_1
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key_in (key[0]) ,
.key_flag (K1) //1表示按下 0表示没有按下
);
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_2
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key_in (key[1]) ,
.key_flag (K2) //1表示按下 0表示没有按下
);
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_3
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key_in (key[2]) ,
.key_flag (K3) //1表示按下 0表示没有按下
);
key_filter
#(
.CNT_MAX (20'd999_999)
)
key_4
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key_in (key[3]) ,
.key_flag (K4) //1表示按下 0表示没有按下
);
endmodule
顶层模块
module top_uart_led
(
input wire sys_clk ,
input wire sys_rst_n ,
input wire uart_rxd ,
input wire [3:0] key ,
output wire [3:0] led ,
output wire beep ,
output wire uart_txd
);
parameter CLK_FREQ = 50_000_000;
parameter UART_BPS = 115200;
//连接
wire [7:0] uart_data;
wire uart_done;
wire [7:0] key_data_out;
wire uart_tx_busy;
wire key_done;
led_crtl led_crtl_inst
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n),
.rx_data (uart_data), //接收到的数据
.led_en (uart_done), //led控制使能
.led (led) ,
.beep (beep)
);
//按键发送数据
key_data key_data_inst
(
.sys_clk (sys_clk) ,
.sys_rst_n (sys_rst_n) ,
.key (key) ,
.key_data_out (key_data_out) ,
.key_done (key_done)
);
//发送数据
uart_send uart_send_inst
(
.sys_clk (sys_clk) , //系统时钟
.sys_rst_n (sys_rst_n) , //系统复位,低电平有效
.uart_en (key_done) , //发送使能信号
.uart_din (key_data_out) , //待发送数据
.uart_tx_busy (uart_tx_busy) , //发送忙状态标志
.uart_txd (uart_txd) //UART发送端口
);
uart_recv uart_recv_inst
(
.sys_clk (sys_clk ) , //系统时钟
.sys_rst_n (sys_rst_n) , //系统复位,低电平有效
.uart_rxd (uart_rxd) , //UART接收端口
.uart_done (uart_done) , //接收一帧数据完成标志信号
.uart_data (uart_data) //接收的数据
);
endmodule
获取信号的上升沿或者下降沿
这里有很多种方法,我介绍一下最常用的方法。比如在获取串口发送开始信号的时候.
start_flag = uart_rxd_d1 & (~uart_rxd_d0);就是获取串口发送端开始发送数据的时候TX拉低的时候的下降沿,表示开始.原理如下图所示.分别定义了两个rxd去对uart_rxd信号打拍两次.
在红线部分,当uart_rxd拉低时,下个时钟周期,取反的uart_rxd_d0信号与uart_rxd_d1信号相与就会得到一个高电平,其余全都是低电平,也就是获取他的下降沿会延迟一个时钟周期.
注意
因为串口发送的时候时ascii码形式,所以要不用16进制发送,要不要看ascii码对应的16进制是否和接收判断那部分一致.比如串口直接发送1(字符串形式)那么对应16进制应该是16’h31.
总结
后续将给出模块的仿真文件以及对应vivado与quartusii工程文件.
vivado工程 使用的是2018.3
链接:https://pan.baidu.com/s/1T10IFlGXi4WTwU2smPmBFg
提取码:gp9w
–来自百度网盘超级会员V5的分享