一、概述
串口收发时序参考
起始位:拉低数据线,传输一个0作为开始信号
停止位:拉高数据线,传输一个1作为结束信号
波特率:1秒钟传输多少位,例如波特率为115200就是一秒钟可以传输115200位数据
设计中需要注意的点:1、严格按照时序设计,先发送低位再发送高位
2、系统时钟计数,根据波特率来确认发送一位的时钟数。定义一个常量来保存最大的计数值,每次计数到最大值代表一位发送完成,然后接着发送下一位
parameter BAUD_RATE = 115200; //波特率
localparam MAX_CNT = 50_000_000/BAUD_RATE; //得到发一位需要等待多少个系统时钟
3、接收数据时,不能在整个位时钟计数周期内一直采集那一位数据,应该在计数中间的位置采集数据,保证数据的稳定性
4、在接收停止位时,计数到最大计数值的一半时就可以准备下一次的接收了,要是等待全部计数完,有可能会错过起始位的下降沿
二、UART发送
module uart_send(
input sys_clk, //系统时钟 50m
input rst_n, //按键复位
input ena_mo, //模块使能
input [7:0] tx_data, //发送的数据
output reg uart_tx //串口tx
);
parameter BAUD_RATE = 115200; //波特率
localparam MAX_CNT = 50_000_000/BAUD_RATE; //得到发一位需要等待多少个系统时钟
localparam waiten=0; //等待模块使能(等待发送)
localparam start=1; //发送开始信号
localparam send_data=2; //发送数据 注意是从低位到高位
localparam stop=3; //发送停止信号
localparam done=4; //一帧发送完成
reg [14:0] clk_cnt; //系统时钟计数器
reg [2:0] state,next_state; //当前状态和下一个状态
reg [3:0] w_cnt; //位计数器
reg cnt_clr; //计数器清零
//系统时钟计数器计数
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n) begin
clk_cnt <= 15'd0;
end
else begin
if(cnt_clr) //清零信号有效时,系统时钟计数器清零
clk_cnt <= 15'd0;
else //无效时,计数到最大值-1(从0开始的),然后清零
clk_cnt <= (clk_cnt == MAX_CNT-1) ? 15'd0 : clk_cnt + 1'b1;
end
end
//下一个状态确认
always @(*)begin
if(!rst_n)begin
next_state = waiten;
end
else begin
case(state)
waiten: next_state = ena_mo ? start : waiten;
start: next_state = (clk_cnt == MAX_CNT-1) ? send_data : start;
//发送到最后一位的时候要记得等待时钟计数器到最大值
//但是也不能用到8来判断,这样会造成两个时钟沿的延迟,而且导致一个时钟沿的不定状态
send_data: next_state = (w_cnt==4'd7&&clk_cnt == MAX_CNT-1) ? stop : send_data;
stop: next_state = (clk_cnt == MAX_CNT-1) ? done : stop;
done: next_state = waiten;
endcase
end
end
//状态变量赋值
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)begin
cnt_clr <= 1'b1;
uart_tx <= 1'b1;
end
else begin
case(state)
waiten:begin
cnt_clr <= 1'b1; //等待时计数器清零 不计数
uart_tx <= 1'b1; //tx空闲状态为高电平
end
start:begin
cnt_clr <= 1'b0; //开始计数
uart_tx <= 1'b0; //发送开始信号 tx拉低
end
send_data:begin
uart_tx <= tx_data[w_cnt]; //发送数据 从低位到高位
end
stop:begin
uart_tx <= 1'b1; //发送停止信号
end
done:begin
cnt_clr <= 1'b1; //计数器清零
uart_tx <= 1'b1;
end
endcase
end
end
//状态流转
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)
state <= waiten;
else
state <= next_state;
end
//位计数器赋值
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)begin
w_cnt <= 4'd0;
end
else begin
case(state)
stop: w_cnt <= 4'd0; //到停止位的时候位计数器清零
//在发送数据状态时 计数到最大值时位计数器加1
send_data: w_cnt <= (clk_cnt == MAX_CNT-1) ? w_cnt + 1'b1 : w_cnt;
default: w_cnt <= w_cnt;
endcase
end
end
endmodule
三、UART接收
module uart_recv(
input sys_clk, //系统时钟 50m
input rst_n, //按键复位
input ena_mo, //模块使能
input uart_rx, //串口rx
output reg [7:0]rev_data, //串口接收到的数据
output rev_done
);
parameter BAUD_RATE = 115200; //波特率
localparam MAX_CNT = 50_000_000/BAUD_RATE; //发一位的需要等待的系统时钟数
localparam no_data = 0; //没有数据状态 等待数据
localparam wait_start = 1; //等待开始信号结束
localparam get_data = 2; //获取数据
localparam wait_stop = 3; //等待停止信号结束
localparam done = 4; //完成状态
reg [14:0] clk_cnt; //系统时钟计数器
reg [2:0] state,next_state; //当前状态和下一个状态
reg [3:0] w_cnt; //位计数器
reg cnt_clr; //系统时钟计数器清零信号
reg uart_rx_r0; //寄存rx前一个时钟状态
wire start_flag; //开始的标志信号
assign start_flag = ~uart_rx & uart_rx_r0; //rx下降沿检测 开始信号
assign rev_done = (state==done);
//rx信号寄存
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)
uart_rx_r0 <= 1'b1;
else
uart_rx_r0 <= uart_rx; //rx总是比rx_r0提前一个时钟
end
//系统时钟计数器计数
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n) begin
clk_cnt <= 15'd0;
end
else begin
if(cnt_clr)
clk_cnt <= 15'd0;
else
clk_cnt <= (clk_cnt == MAX_CNT-1) ? 15'd0 : clk_cnt + 1'b1;
end
end
//下一个状态确认
always @(*)begin
if(!rst_n)
next_state <= no_data;
else begin
case(state)
no_data: next_state = (start_flag&&ena_mo) ? wait_start : no_data;
wait_start: next_state = (clk_cnt == MAX_CNT-1) ? get_data : wait_start;
get_data: next_state = (w_cnt==3'd7&&clk_cnt == MAX_CNT-1) ? wait_stop : get_data;
wait_stop: next_state = (clk_cnt == (MAX_CNT-1)/2) ? done : wait_stop;//计数到一半一帧接收完毕,进入done状态产生完成信号
done:next_state = no_data;//进入下一帧等待状态
endcase
end
end
//状态变量赋值
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)begin
cnt_clr <= 1'b1;
rev_data <= 8'd0;
end
else begin
case(state)
no_data:begin
cnt_clr <= 1'b1; //计数器清零
end
wait_start:begin
cnt_clr <= 1'b0; //开始计数
end
get_data:begin //每次计数到中间值时获取数据
rev_data[w_cnt] <= (clk_cnt == (MAX_CNT-1)/2) ? uart_rx : rev_data[w_cnt];
end
wait_stop:begin
cnt_clr <= 1'b0;
end
endcase
end
end
//状态流转
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)
state <= no_data;
else
state <= next_state;
end
//位计数器计数
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)
w_cnt <= 4'd0;
else begin
case(state)
no_data: w_cnt <= 4'd0;
get_data: w_cnt <= (clk_cnt == MAX_CNT-1) ? w_cnt + 1'b1 : w_cnt;
default: w_cnt <= w_cnt;
endcase
end
end
endmodule
四、实现效果
发送的数据(电脑端接收)
八位拨码开关输入数据,按键发送(没有消抖,所以一次性就发了很多)
接收数据,使用数码管来显示,所以我发的是一些数字来验证接收模块
五、数码管动态扫描
时钟频率不能太高,太高可能会出现模糊的现象,当然也不能太低,这样视觉上会有停顿感,我选择1KHz的时钟。
module seg_led_show(
input clk_1k, //时钟信号
input rst_n, //按键复位
input [23:0] data, //显示的数据 4位一个
output reg [5:0] seg_sel, //数码管位选 低电平有效
output reg [7:0] seg_led //数码管段选 低电平有效
);
localparam S0=0; //位选状态0
localparam S1=1; //位选状态1
localparam S2=2; //位选状态2
localparam S3=3; //位选状态3
localparam S4=4; //位选状态4
localparam S5=5; //位选状态5
reg [2:0] state,next_state; //当前状态和下一个状态
reg [3:0]num; //当前显示的数字
//下一个状态确认
always @(*)begin
if(!rst_n)begin
next_state = S0;
end
else begin
case(state)
S0:next_state = S1; //循环扫描
S1:next_state = S2;
S2:next_state = S3;
S3:next_state = S4;
S4:next_state = S5;
S5:next_state = S0;
endcase
end
end
//状态变量赋值
always @(posedge clk_1k,negedge rst_n)begin
if(!rst_n)begin
seg_sel <= 6'd0;
num <= 4'd0;
end
else begin
case(state)
S0:begin
seg_sel <= 6'b111110; //选中第0个数码管
num <= data[3:0]; //要显示的数据赋值
end
S1:begin
seg_sel <= 6'b111101; //选中第1个数码管
num <= data[7:4];
end
S2:begin
seg_sel <= 6'b111011; //选中第2个数码管
num <= data[11:8];
end
S3:begin
seg_sel <= 6'b110111; //选中第3个数码管
num <= data[15:12];
end
S4:begin
seg_sel <= 6'b101111; //选中第4个数码管
num <= data[19:16];
end
S5:begin
seg_sel <= 6'b011111; //选中第5个数码管
num <= data[23:20];
end
endcase
end
end
//状态流转
always @(posedge clk_1k,negedge rst_n)begin
if(!rst_n)
state <= S0;
else
state <= next_state;
end
//根据num(要显示的数字)值来输出段选信号
always @(*)begin
if(!rst_n)
seg_led = 8'd0;
else begin
case (num)
4'h0 : seg_led = 8'b1100_0000;
4'h1 : seg_led = 8'b1111_1001;
4'h2 : seg_led = 8'b1010_0100;
4'h3 : seg_led = 8'b1011_0000;
4'h4 : seg_led = 8'b1001_1001;
4'h5 : seg_led = 8'b1001_0010;
4'h6 : seg_led = 8'b1000_0010;
4'h7 : seg_led = 8'b1111_1000;
4'h8 : seg_led = 8'b1000_0000;
4'h9 : seg_led = 8'b1001_0000;
4'ha : seg_led = 8'b1000_1000;
4'hb : seg_led = 8'b1000_0011;
4'hc : seg_led = 8'b1100_0110;
4'hd : seg_led = 8'b1010_0001;
4'he : seg_led = 8'b1000_0110;
4'hf : seg_led = 8'b1000_1110;
default : seg_led = 8'b1100_0000;
endcase
end
end
endmodule
六、顶层模块
这部分和之前spi是混在一起的,所以有点乱。顶层模块就是例化一下其他模块,比较简单,这部分可以选择性跳过。
module oscilloscope(
input sys_clk, //系统时钟 50m
input rst_n, //按键复位
input uart_rx, //串口rx信号
input uart_send_key, //串口发送按键
input [7:0]uart_send_data,//串口发送数据
input spi_miso, //spi 主进从出信号
output spi_mosi, //spi 主出从进信号
output spi_sck, //spi 时钟信号
output spi_nss, //spi 片选信号(使用这个信号可以提高传输的稳定性)
output uart_tx,
output [5:0] seg_sel,
output [7:0] seg_led
);
wire [7:0] spi_rdata; //spi读取到的数据
wire [7:0] spi_tdata; //spi要发送的数据
wire tr_done; //一个字节发送完成信号
wire clk_1m; //1M 时钟信号
wire clk_1k; //1K 时钟信号
wire uart_rev_done; //串口接收完成
wire [23:0]show_data; //要显示的数据
wire [7:0] uart_rdata; //串口收到的数据
wire [3:0] ydata; //将接收到的串口数据处理一下
wire uart_send_flag; //串口发送使能标志信号
reg [7:0] tx_data; //给要发送的数据赋值
reg [23:0]show_data_r; //用来给显示的数据赋值 wire类型主要用来连接模块
reg uart_send_key_r0; //按键前一个时钟的信号
assign spi_tdata = tx_data;//要发送的数据赋值
assign show_data = show_data_r;
assign ydata = uart_rdata-8'h30;//将串口接收到的数据转换为数字
assign uart_send_flag = ~uart_send_key&uart_send_key_r0;//下降沿检测
always @(posedge clk_1m,negedge rst_n)begin
if(!rst_n)
uart_send_key_r0 <= 1'b0;
else
uart_send_key_r0 <= uart_send_key;
end
always @(posedge clk_1m,negedge rst_n)begin
if(!rst_n)
tx_data <= 8'd0;
else begin
tx_data <= tr_done ? tx_data + 1'b1 : tx_data;//每发送完成一个字节 数据的值增加1
end
end
always @(posedge sys_clk,negedge rst_n)begin
if(!rst_n)
show_data_r <= 24'd0;
else
//通过移位操作把串口接收到的数据移到显示的数据
show_data_r <= uart_rev_done ? {show_data_r[19:0],ydata} : show_data_r;
end
//时钟分频模块 产生1MHz时钟
clk_fenpin u_clk_fenpin(
.sys_clk(sys_clk),
.rst_n(rst_n),
.clk_1m(clk_1m),
.clk_1k(clk_1k)
);
//spi 主模式模块
spi_master u_spi_master(
.clk_1m(clk_1m),
.rst_n(rst_n),
.ena_mo(1'b1),
.spi_tdata(tx_data),
.spi_miso(spi_miso),
.spi_mosi(spi_mosi),
.spi_sck(spi_sck),
.spi_nss(spi_nss),
.spi_rdata(spi_rdata),
.tr_done(tr_done)
);
//uart发送模块
uart_send u_uart_send(
.sys_clk(sys_clk),
.rst_n(rst_n),
.ena_mo(uart_send_flag),
.tx_data(uart_send_data),
.uart_tx(uart_tx)
);
//uart接收模块
uart_recv u_uart_recv(
.sys_clk(sys_clk),
.rst_n(rst_n),
.ena_mo(1'b1),
.rev_data(uart_rdata),
.uart_rx(uart_rx),
.rev_done(uart_rev_done)
);
//数码管显示模块
seg_led_show u_seg_led_show(
.clk_1k(clk_1k),
.rst_n(rst_n),
.data(show_data),
.seg_sel(seg_sel),
.seg_led(seg_led)
);
endmodule