一、工程简介
使用正点原子达芬奇A7板子,在基于小梅哥课程的基础上完成了关于搭建串口收发与存储双口RAM 简易应用系统拓展学习。与上一次的实践课程相比,这次的项目在16位数据的来源上有所不同,上次往RAM的2048个地址中存储的数据是通过写模块生成的与地址相同的数据。这次将数据通过串口发送到FPGA中,当需要时,按下按键key0,则FPGA 将RAM中存储的数据通过串口发送出去。
同时这次所用到的串口接收和发送模块都是小梅哥的工程实例,相比上次有更加规范的表达和更加详细的原理讲解。
二、功能介绍
将数据通过串口发送到FPGA中,当需要时,按下按键key0,则FPGA 将RAM中存储的数据通过串口发送出去。根据功能实现要求进行功能划分:串口接收发送模块、按键消抖模块、RAM 模块以及控制模块。以下详细介绍一下各个模块工作原理和代码编写。
三、具体实现
1.串口模块
该项目所用的串口接收模块和发送模块都是小梅哥工程实例中的,讲解的非常详细,把工作原理搞懂,在以后的实际应用中只需要根据要求对其中的一些部分进行修改即可。例如对于串口接收模块为追求数据更加可靠而增加的采样次数;对于串口接收模块中串口传输超时的判定不是3.5个字符时间而改成其他时间;对于传输的数据不是2字节而是更高字节;输出字节顺序的改变;工作基频不是50MH时对采样时钟计算和超时时间计数值的改变等等。
1).串口接收模块
要实现多字节(这里是16位两字节)串口接收模块设计,我们可以例化单字节串口接收模块,随后设计一个状态机,对单字节串口接收模块输出的数据进行拼接。同时,基于单字节串口接收模块产生的接收完成信号,计数当前已经接收(拼接)的数据量。当拼接的数据达到指定位宽时,将数据输出。
(1)单字节接收
当对于数据线上的每一位进行采样时,一般情况下认为每一位数据的中间点是最稳定的。同时为避免干扰,所以进行多次采集求概率的方式进行状态判定。
在上图中,将每一位数据再平均分成了16 小段。对于Bit_x 这一位数据,考虑到数据在刚刚发生变化和即将发生变化的这一时期,数据极有可能不稳定的(用深灰色标出的两段),在这两个时间段采集数据,很有可能得到错误的结果,因此判定这两段时间的电平无效,采集时直接忽略。而中间这一时间段(用浅灰色标出),数据本身是比较稳定的,一般都代表了正确的结果。也就是前面提到的中间测量方式,但是也不排除该段数据受强电磁干扰而出现错误的电平脉冲。因此对这一段电平,进行多次采样,并求高低电平发生的概率,6 次采集结果中,取出现次数多的电平作为采样结果。例如,采样6次的结果分别为1/1/1/1/0/1/,则取电平结果为1,若为0/0/1/0/0/0,,则取电平结果为0,当6次采样结果中1和0 各占一半(各3次),则可判断当前通信线路环境非常恶劣,数据不具有可靠性,不进行处理。基于以上原理,串口接收模块整体框图如下图所示。
这里串口接收的信号uart_rx 相对于FPGA 内部信号来说是一个异步信号,如不进行处理直接将其输入使用,容易出现时序违例导致亚稳态。因此这里就需要先将信号同步到FPGA 的时钟域内才可以供后续模块使用,使用两级触发器对信号打两拍的方式进行与系统时钟进行同步。
always@(posedge clk or posedge reset)
if(reset)
begin
uart_rx_sync1 <= 1'b0;
uart_rx_sync2 <= 1'b0;
end
else
begin
uart_rx_sync1 <= uart_rx;
uart_rx_sync2 <= uart_rx_sync1;
end
串口接收模块需要检测下降沿来识别串口接收信号的起始位,对同步后的串行输入数据检测下降沿,下降沿检测的具体代码如下。
always@(posedge clk or posedge reset)
if(reset)
begin
uart_rx_reg1 <= 1'b0;
uart_rx_reg2 <= 1'b0;
end
else
begin
uart_rx_reg1 <= uart_rx_sync2;
uart_rx_reg2 <= uart_rx_reg1;
end
assign uart_rx_nedge = !uart_rx_reg1 & uart_rx_reg2;
波特率时钟生成模块,数据采集的实际采样频率是波特率的 16 倍,得出计数值与波特率之间的关系如下表所示,其中系统时钟周期为clk,这里为 20ns 。
波特率 | 波特率周期 | 采样时钟分频计数值 | clk=20计数值 |
9600 | 104167ns | 104167ns/clk/16 | 325-1 |
19200 | 52083ns | 52083ns/clk/16 | 163-1 |
38400 | 26041ns | 26041ns/clk/16 | 81-1 |
57600 | 17361ns | 17361ns/clk/16 | 54-1 |
115200 | 8680ns | 8680ns/clk/16 | 27-1 |
//下降沿检测
assign uart_rx_nedge = !uart_rx_reg1 & uart_rx_reg2;
always@(posedge clk or posedge reset)
if(reset)
bps_DR <= 16'd324;
else begin
case(baud_set)
0:bps_DR <= 16'd324;
1:bps_DR <= 16'd162;
2:bps_DR <= 16'd80;
3:bps_DR <= 16'd53;
4:bps_DR <= 16'd26;
default:bps_DR <= 16'd324;
endcase
end
//counter
always@(posedge clk or posedge reset)
if(reset)
div_cnt <= 16'd0;
else if(uart_state)begin
if(div_cnt == bps_DR)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 16'd0;
// bps_clk gen
always@(posedge clk or posedge reset)
if(reset)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1)
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
采样时钟计数器,计数器清零条件之一bps_cnt == 8'd159 代表 一个字节接收完毕。
(bps_cnt == 8'd12 && (START_BIT > 2))是实现起始位检测是否出错。这样的一种思路:当数据线上出现了下降沿时,先假设它是起始信号,然后对其进行中间段采样。如果这6次采样值累加结果大于2 ,即6次采样中有至少一半的 状态为高电平 时,那么这显然不符合真正起始信号的持续低电平要求,此时就把刚才到来的下降沿视为干扰信号 ,而不作为起始信号。
//bps counter
always@(posedge clk or posedge reset)
if(reset)
bps_cnt <= 8'd0;
else if(bps_cnt == 8'd159 | (bps_cnt == 8'd12 && (START_BIT > 2)))
bps_cnt <= 8'd0;
else if(bps_clk)
bps_cnt <= bps_cnt + 1'b1;
else
bps_cnt <= bps_cnt;
always@(posedge clk or posedge reset)
if(reset)
rx_done <= 1'b0;
else if(bps_cnt == 8'd159)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
采样数据接收模块设计
always@(posedge clk or posedge reset)
if(reset)begin
START_BIT <= 3'd0;
data_byte_pre[0] <= 3'd0;
data_byte_pre[1] <= 3'd0;
data_byte_pre[2] <= 3'd0;
data_byte_pre[3] <= 3'd0;
data_byte_pre[4] <= 3'd0;
data_byte_pre[5] <= 3'd0;
data_byte_pre[6] <= 3'd0;
data_byte_pre[7] <= 3'd0;
STOP_BIT <= 3'd0;
end
else if(bps_clk)begin
case(bps_cnt)
0:begin
START_BIT <= 3'd0;
data_byte_pre[0] <= 3'd0;
data_byte_pre[1] <= 3'd0;
data_byte_pre[2] <= 3'd0;
data_byte_pre[3] <= 3'd0;
data_byte_pre[4] <= 3'd0;
data_byte_pre[5] <= 3'd0;
data_byte_pre[6] <= 3'd0;
data_byte_pre[7] <= 3'd0;
STOP_BIT <= 3'd0;
end
6 ,7 ,8 ,9 ,10,11:START_BIT <= START_BIT + uart_rx_sync2;
22,23,24,25,26,27:data_byte_pre[0] <= data_byte_pre[0] + uart_rx_sync2;
38,39,40,41,42,43:data_byte_pre[1] <= data_byte_pre[1] + uart_rx_sync2;
54,55,56,57,58,59:data_byte_pre[2] <= data_byte_pre[2] + uart_rx_sync2;
70,71,72,73,74,75:data_byte_pre[3] <= data_byte_pre[3] + uart_rx_sync2;
86,87,88,89,90,91:data_byte_pre[4] <= data_byte_pre[4] + uart_rx_sync2;
102,103,104,105,106,107:data_byte_pre[5] <= data_byte_pre[5] + uart_rx_sync2;
118,119,120,121,122,123:data_byte_pre[6] <= data_byte_pre[6] + uart_rx_sync2;
134,135,136,137,138,139:data_byte_pre[7] <= data_byte_pre[7] + uart_rx_sync2;
150,151,152,153,154,155:STOP_BIT <= STOP_BIT + uart_rx_sync2;
default:
begin
START_BIT <= START_BIT;
data_byte_pre[0] <= data_byte_pre[0];
data_byte_pre[1] <= data_byte_pre[1];
data_byte_pre[2] <= data_byte_pre[2];
data_byte_pre[3] <= data_byte_pre[3];
data_byte_pre[4] <= data_byte_pre[4];
data_byte_pre[5] <= data_byte_pre[5];
data_byte_pre[6] <= data_byte_pre[6];
data_byte_pre[7] <= data_byte_pre[7];
STOP_BIT <= STOP_BIT;
end
endcase
end
在原理部分介绍过,对一位数据需进6次采样,然后取出现次数较多的数据作为采样结果,也就是说, 6 次采样中出现次数多于3次的数据才能作为最终的有效数据。
always@(posedge clk or posedge reset)
if(reset)
data_byte <= 8'd0;
else if(bps_cnt == 8'd159)begin
data_byte[0] <= data_byte_pre[0][2];
data_byte[1] <= data_byte_pre[1][2];
data_byte[2] <= data_byte_pre[2][2];
data_byte[3] <= data_byte_pre[3][2];
data_byte[4] <= data_byte_pre[4][2];
data_byte[5] <= data_byte_pre[5][2];
data_byte[6] <= data_byte_pre[6][2];
data_byte[7] <= data_byte_pre[7][2];
end
(2)多字节接收
为避免某些外部干扰,导致部分数据发生变化无法被识别,我们需要对每次接收进行计时,一旦接收超时,便产生接收超时标志信号,同时通过状态机结束本次串口接收。
首先对单字节接收模块例化
例化时
1、通过修改DATA_WIDTH的值来指定每次发送的数据位宽
2、通过修改MSB_FIRST的值来确定先发高字节还是先发低字节。为1则先发高字节,为0则先发低字节
3、send_en为脉冲触发信号,发送时提供一个时钟周期的高脉冲即可触发一次传输
4、Baud_Set为波特率设置值,0(9600)、1(19200)、2(38400)、3(57600)、4(115200)
5、每次传输完成(指定位宽的数据传输完成),TX-Done产生一个时钟周期的高脉冲
parameter DATA_WIDTH = 16;
parameter MSB_FIRST = 1;
input clk;
input reset_n;
input uart_rx;
output reg [DATA_WIDTH - 1 : 0]data;
input [2:0]Baud_Set;
output reg rx_done;
output reg timeout_flag;
reg [DATA_WIDTH - 1 : 0]data_r;
wire [7:0] data_byte;
wire byte_rx_done;
wire [19:0] TIMEOUT;
uart_byte_rx uart_byte_rx(
.dlk(dlk),
.reset_n(reset_n),
.baud_set(Baud_Set),
.uart_rx(uart_rx),
.data_byte(data_byte),
.rx_done(byte_rx_done)
);
如何判断接收是否是超时?
基于modbus对于串口传输超时的定义,传输一旦超过3.5字符时间就视作是新的报文。基于此,本次设计中,我们认为,在进行串口接收数据时,一旦数据间超过 3.5个字符时间,便是接收超时。例如115200波特率,我们可以计算出,串口正常接收一字节数据所需的时间为
1 /115200 10^9*(1+8+1)=86.805us
那么如果串口超过8 6.805*3.5=300.8175 us 时间内都未接收到数据,便可以认为是接收超时。使用的开发板是50MHz时钟时钟,即每个时钟周期为20 ns 。串口接收一字节数据所花的时间,可以 通过计数两个串口接收完成信号之间的时钟上升沿来确定 。因此就可以求出,当计数值大于
3 00.8175*10 ^3 /20 1 = 15190
接收超时。一旦接收超时,我们就让它产生超时标志信号。
至于计数器该何时计数,我们可以让计数器在每个字节接收开始时工作,在字节接收完成时被清零。
//根据波特率自动设置超时时间
assign TIMEOUT = (Baud_Set == 3'd0) ? 20'd182291:
(Baud_Set == 3'd1) ? 20'd91145:
(Baud_Set == 3'd2) ? 20'd45572:
(Baud_Set == 3'd3) ? 20'd30381:
20'd15190;
reg [31:0]timeout_cnt;
always@(posedge clk or negedge reset_n)
if(!reset_n)
timeout_flag <= 1'd0;
else if(timeout_cnt >= TIMEOUT)
timeout_flag <= 1'd1;
else if(state == S0)
timeout_flag <= 1'd0;
reg to_state;
always@(posedge clk or negedge reset_n)
if(!reset_n)
to_state <= 0;
else if(!uart_rx)
to_state <= 1;
else if(byte_rx_done)
to_state <= 0;
always@(posedge clk or negedge reset_n)
if(!reset_n)
timeout_cnt <= 32'd0;
else if(to_state)begin
if(byte_rx_done)
timeout_cnt <= 32'd0;
else if(timeout_cnt >= TIMEOUT)
timeout_cnt <= TIMEOUT;
else
timeout_cnt <= timeout_cnt + 1'd1;
end
状态机
1. 等待单字节接收模块的接收完成信号,缓存第一个字节数据,对缓存的数据进行计数
2. 开始超时计数,如果下一次传输完成信号到来前接收超时,产生 Rx _D one 信号,传输结束。 如果未超时则对新一字节数据进行拼接,并对已缓存数据计数。
3. 对比已缓存的数据与输出数据位宽,检查所有数据是否接收完成。如果完成,产生Rx _D one 信号,传输结束。如果未完成则继续步骤 2
reg [8:0]cnt;
reg [1:0]state;
localparam S0 = 0; //等待单字节接收完成信号
localparam S1 = 1; //判断接收是否超时
localparam S2 = 2; //检查所有数据是否接收完成
always@(posedge clk or negedge reset_n)
if(!reset_n)begin
data_r <= 0;
state <= S0;
cnt <= 0;
data <= 0;
end
else begin
case(state)
S0:
begin
rx_done <= 0;
data_r <= 0;
if(DATA_WIDTH == 8)begin
data <= data_byte;
rx_done <= byte_rx_done;
end
else if(byte_rx_done)begin
state <= S1;
cnt <= cnt + 9'd8;
if(MSB_FIRST == 1)
data_r <= {data_r[DATA_WIDTH - 1 - 8 : 0], data_byte};
else
data_r <= {data_byte, data_r[DATA_WIDTH - 1 : 8]};
end
end
S1:
if(timeout_flag)begin
state <= S0;
rx_done <= 1;
end
else if(byte_rx_done)begin
state <= S2;
cnt <= cnt + 9'd8;
if(MSB_FIRST == 1)
data_r <= {data_r[DATA_WIDTH - 1 - 8 : 0], data_byte};
else
data_r <= {data_byte, data_r[DATA_WIDTH - 1 : 8]};
end
S2:
if(cnt >= DATA_WIDTH)begin
state <= S0;
cnt <= 0;
data <= data_r;
rx_done <= 1;
end
else begin
state <= S1;
rx_done <= 0;
end
default:state <= S0;
endcase
end
2).串口发送模块
(1)单字节发送
单字节发送模块比较简单,对此上一篇已经介绍的差不多了,这里就不过多赘述了。详细工程实例代码如下:
module uart_byte_tx(
clk,
reset_n,
data_byte,
send_en,
Baud_Set,
uart_tx,
tx_done,
uart_state
);
input clk ; //模块全局时钟输入,50M
input reset_n; //复位信号输入,低有效
input [7:0]data_byte; //待传输8bit数据
input send_en; //发送使能
input [2:0]Baud_Set; //波特率设置
output reg uart_tx; //串口输出信号
output reg tx_done; //1byte数据发送完成标志
output reg uart_state; //发送数据状态
localparam START_BIT = 1'b0;
localparam STOP_BIT = 1'b1;
reg bps_clk; //波特率时钟
reg [15:0]div_cnt; //分频计数器
reg [15:0]bps_DR; //分频计数最大值
reg [3:0]bps_cnt; //波特率时钟计数器
reg [7:0]data_byte_reg;//data_byte寄存后数据
always@(posedge clk or negedge reset_n)
if(!reset_n)
uart_state <= 1'b0;
else if(send_en)
uart_state <= 1'b1;
else if(bps_cnt == 4'd11)
uart_state <= 1'b0;
else
uart_state <= uart_state;
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;
always@(posedge clk or negedge reset_n)
if(!reset_n)
bps_DR <= 16'd5207;
else begin
case(Baud_Set)
0:bps_DR <= 16'd5207;
1:bps_DR <= 16'd2603;
2:bps_DR <= 16'd1301;
3:bps_DR <= 16'd867;
4:bps_DR <= 16'd433;
default:bps_DR <= 16'd5207;
endcase
end
//counter
always@(posedge clk or negedge reset_n)
if(!reset_n)
div_cnt <= 16'd0;
else if(uart_state)begin
if(div_cnt == bps_DR)
div_cnt <= 16'd0;
else
div_cnt <= div_cnt + 1'b1;
end
else
div_cnt <= 16'd0;
// bps_clk gen
always@(posedge clk or negedge reset_n)
if(!reset_n)
bps_clk <= 1'b0;
else if(div_cnt == 16'd1)
bps_clk <= 1'b1;
else
bps_clk <= 1'b0;
//bps counter
always@(posedge clk or negedge reset_n)
if(!reset_n)
bps_cnt <= 4'd0;
else if(bps_cnt == 4'd11)
bps_cnt <= 4'd0;
else if(bps_clk)
bps_cnt <= bps_cnt + 1'b1;
else
bps_cnt <= bps_cnt;
always@(posedge clk or negedge reset_n)
if(!reset_n)
tx_done <= 1'b0;
else if(bps_cnt == 4'd11)
tx_done <= 1'b1;
else
tx_done <= 1'b0;
always@(posedge clk or negedge reset_n)
if(!reset_n)
uart_tx <= 1'b1;
else begin
case(bps_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
endmodule
(2)多字节发送
对于多字节发送,上次写的只是115200波特率2字节的数据发送,字节高低位顺序也不容易直接改变。这里详细介绍一下工作原理,并且完善上次的程序使其具有更加的灵活性和通用性。
为了保证数据的连续性,我们可以使用单字节串口发送模块的单字节发送完成信号
(byte_tx_done)作为条件信号。当该信号有效时,说明一字节数据发送完成,此时就需要输出下一字节数据给单字节串口发送模块。一次多字节串口发送,所需发送的数据量是确定的,串口不可能一直发送下去。因此在每发送完一字节数据后,我们还需要判断发送是否完成。根据byte_tx_done 信号计算当前已发送数据,再与接收到的数据位宽做对比。如果发送未完成,则继续下一字节的发送,一旦判断发送完成,产生串口发送完成信号,本次传输结束。
单字节串口发送模块的例化
input clk;
input reset_n;
input [2:0]Baud_Set;
output uart_tx;
output uart_state;
reg [7:0] data_byte;
reg byte_send_en;
wire byte_tx_done;
uart_byte_tx uart_byte_tx(
.clk(clk),
.reset_n(reset_n),
.data_byte(data_byte),
.send_en(byte_send_en),
.Baud_Set(Baud_Set),
.uart_tx(uart_tx),
.tx_done(byte_tx_done),
.uart_state(uart_state)
);
状态机
对于多字节串口发送模块而言,当输入的send _en 有效时,表明传输开始,状态机产生一字节的发送数据以及发送使能信号给单字节串口发送模块。随后,状态机需要等待byte_tx_done 信号,确认当前字节数据已经发送完毕。同时,状态机需要基于该信号计数,判断传输是否完成。如果未传输完成,便继续下一字节数据和使能信号的发送。如果传输完成,状态机便需要产生发送完成信号 tx_done ,标志本次多字节发送结束。
- 状态S0,该状态需要等待发送请求信号 send _en,在 send_en 有效时,接收传入的数据,并使状态跳转到S1。
- S1状态,该状态需要产生单字节发送使能信号以及单字节数据,并使状态跳转到S2。考虑到在不同应用场景中,数据的字节序会有不同需求,可以通过使用parameter 定义一个常量MSB_FIRST 控制数据的大小端字节序。
- S2状态,该状态需要等待单字节数据发送完成,也就是等待单字节串口发送模块的 byte_tx_done 信号。当该信号有效时,代表单字节数据发送完成,我们可以基于该信号计数当前已发送的数据量,并将状态跳转到 S3。
- S3状态,我们可以通过对比已发送数据量和输入位宽量来产生 Tx_ Done 信号。当已发送数据量大于等于输入位宽时,说明数据全部发送完毕,此时可以产生 T x_Done信号并将状态跳转到S0,等待下次传输开始。当已发送数据量小于输入位宽时,说明传输
还未结束,此时将状态跳转到S1,准备下一字节的数据传输。代码如下
reg [8:0]cnt;
reg [1:0]state;
localparam S0 = 0; //等待发送请求
localparam S1 = 1; //发起单字节数据发送
localparam S2 = 2; //等待单字节数据发送完成
localparam S3 = 3; //检查所有数据是否发送完成
always@(posedge clk or negedge reset_n)
if(!reset_n)begin
data_byte <= 0;
byte_send_en <= 0;
state <= S0;
cnt <= 0;
end
else begin
case(state)
S0:
begin
data_byte <= 0;
cnt <= 0;
tx_done <= 0;
if(send_en)begin
state <= S1;
data_r <= data;
end
else begin
state <= S0;
data_r <= data_r;
end
end
S1:
begin
byte_send_en <= 1;
if(MSB_FIRST == 1)begin
data_byte <= data_r[DATA_WIDTH-1:DATA_WIDTH - 8];
data_r <= data_r << 8;
end
else begin
data_byte <= data_r[7:0];
data_r <= data_r >> 8;
end
state <= S2;
end
S2:
begin
byte_send_en <= 0;
if(byte_tx_done)begin
state <= S3;
cnt <= cnt + 9'd8;
end
else
state <= S2;
end
S3:
if(cnt >= DATA_WIDTH)begin
state <= S0;
cnt <= 0;
tx_done <= 1;
end
else begin
state <= S1;
tx_done <= 0;
end
default:state <= S0;
endcase
end
2.摁键消抖模块
摁键消抖模块也是可以直接调用的一项工程实例,所以就简单介绍一下。对于单按键的消抖模块,其接口如下图所示:
按键消抖状态转移条件:
module key_filter(
clk,
reset_n,
key_in,
key_flag,
key_state
);
wire reset=~reset_n;
input clk; //模块全局时钟输入,50M
input reset_n; //复位信号输入,低有效
input key_in; //按键输入
output key_flag ; //按键标志信号
output key_state; //按键状态信号
reg key_flag ;
reg key_state;
localparam
IDLE= 4'b0001,
FILTER0= 4'b0010,
DOWN= 4'b0100,
FILTER1= 4'b1000;
reg [3:0]state; //状态机状态
reg [19:0]cnt; //计数器
reg en_cnt; //使能计数寄存器
reg cnt_full;
reg key_in_sync1;
reg key_in_sync2;
reg key_in_reg1;
reg key_in_reg2;
wire key_in_pedge;
wire key_in_nedge;
//对外部输入的异步信号进行同步处理
always@(posedge clk or posedge reset)
if(reset)begin
key_in_sync1 <= 1'b0;
key_in_sync2 <= 1'b0;
end
else begin
key_in_sync1 <= key_in;
key_in_sync2 <= key_in_sync1;
end
//使用D触发器存储两个相邻时钟上升沿时外部输入信号(已经同步到系统时钟域中)的电平状态
always@(posedge clk or posedge reset)
if(reset)begin
key_in_reg1 <= 1'b0;
key_in_reg2 <= 1'b0;
end
else begin
key_in_reg1 <= key_in_sync2;
key_in_reg2 <= key_in_reg1;
end
//产生跳变沿信号
assign key_in_nedge = !key_in_reg1 & key_in_reg2;
assign key_in_pedge = key_in_reg1 & (!key_in_reg2);
//按键消抖状态机
always@(posedge clk or posedge reset)
if(reset)begin
en_cnt <= 1'b0;
state <= IDLE;
key_flag <= 1'b0;
key_state <= 1'b1;
end
else begin
case(state)
IDLE :begin
key_flag <= 1'b0;
if(key_in_nedge)begin
state <= FILTER0;
en_cnt <= 1'b1;
end
else
state <= IDLE;
end
FILTER0:begin
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b0;
en_cnt <= 1'b0;
state <= DOWN;
end
else if(key_in_pedge)begin
state <= IDLE;
en_cnt <= 1'b0;
end
else
state <= FILTER0;
end
DOWN:begin
key_flag <= 1'b0;
if(key_in_pedge)begin
state <= FILTER1;
en_cnt <= 1'b1;
end
else
state <= DOWN;
end
FILTER1:begin
if(cnt_full)begin
key_flag <= 1'b1;
key_state <= 1'b1;
state <= IDLE;
en_cnt <= 1'b0;
end
else if(key_in_nedge)begin
en_cnt <= 1'b0;
state <= DOWN;
end
else
state <= FILTER1;
end
default:begin
state <= IDLE;
en_cnt <= 1'b0;
key_flag <= 1'b0;
key_state <= 1'b1;
end
endcase
end
always@(posedge clk or posedge reset)
if(reset)
cnt <= 20'd0;
else if(en_cnt)
cnt <= cnt + 1'b1;
else
cnt <= 20'd0;
always@(posedge clk or posedge reset)
if(reset)
cnt_full <= 1'b0;
else if(cnt == 20'd999_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
endmodule
3.RAM IP核的创建
这里还是使用伪双端口RAM ,16位位宽和2048深度。创建和上次的一样,这里就不多说了。
4.控制模块
为了实现 FPGA接收到数据后将数据存储在双口 ram 的一段连续空间中,这样就需要设计一个写地址自加的控制部分,且其控制写使能信号为串口接收模块输出的 rx_done 信号。每来一个 rx_done也就是每接收成功一字节数,地址数进行加一。
为了实现当按下按键0 FPGA 将 RAM 中存储的数据通过串口发送出去 。 也就是实现按键按下即启动连续读操作,再次按下即可暂停读操作 。
编写代码如下:
module ctrl(
clk,
reset_n,
key_flag,
key_state,
rx_done,
tx_done,
addra,
addrb,
wea,
send_en
);
wire reset=~reset_n;
input clk; //时钟输入50M
input reset_n; //模块复位,低有效
input key_flag; //按键标志信号
input key_state; //按键状态信号
input rx_done; //串口一次数据接收完成标志
input tx_done; //串口一次数据发送完成标志
output [7:0]addrb; //dpram读地址
output [7:0]addra; //dpram写地址
output wea; //dpram写使能
output send_en; //串口发送使能
reg [7:0]addrb;
reg [7:0]addra;
reg send_en;
reg send_state;
reg send_1st_en;
reg tx_done_dly1;
reg tx_done_dly2;
reg tx_done_dly3;
wire send_en_pre;
assign wea = rx_done;
always@(posedge clk or posedge reset)
if(reset)
addra <= 8'd0;
else if(rx_done)
addra <= addra + 1'b1;
else
addra <= addra;
//按键控制数据发送的状态,
//为1表示从dpram读出数据从串口发出
always@(posedge clk or posedge reset)
if(reset)
send_state <= 1'b0;
else if(key_flag && !key_state)
send_state <= ~send_state;
else
send_state <= send_state;
always@(posedge clk or posedge reset)
if(reset)
addrb <= 8'd0;
else if(tx_done && send_state == 1'b1)
addrb <= addrb + 8'd1;
else
addrb <= addrb;
//切换到发送状态,读取第一个数据发送使能
always@(posedge clk or posedge reset)
if(reset)
send_1st_en <= 1'b0;
else if(key_flag && !key_state && send_state == 1'b0)
send_1st_en <= 1'b1;
else
send_1st_en <= 1'b0;
always@(posedge clk or posedge reset)
if(reset)
begin
tx_done_dly1 <= 1'b0;
tx_done_dly2 <= 1'b0;
tx_done_dly3 <= 1'b0;
end
else
begin
tx_done_dly1 <= tx_done;
tx_done_dly2 <= tx_done_dly1;
tx_done_dly3 <= tx_done_dly2;
end
assign send_en_pre = send_1st_en | (tx_done_dly3 && send_state == 1'b1);
always@(posedge clk or posedge reset)
if(reset)
send_en <= 1'b0;
else
send_en <= send_en_pre;
endmodule
5.顶层模块
设计完ctrl 模块后,然后合入顶层,对顶层实现以上各个模块的例化,代码如下:
module uart_dpram(
clk,
reset_n,
key_in,
uart_rx,
uart_tx
);
input clk;
input reset_n;
input key_in;
input uart_rx;
output uart_tx;
wire key_flag;
wire key_state;
wire [7:0]rx_data;
wire rx_done;
wire send_en;
wire [7:0]tx_data;
wire tx_done;
wire [7:0]addra;
wire [7:0]addrb;
wire wea;
key_filter key_filter_inst(
.clk(clk),
.reset_n(reset_n),
.key_in(key_in),
.key_flag(key_flag),
.key_state(key_state )
);
uart_byte_rx uart_byte_rx_inst(
.clk(clk),
.reset_n(reset_n),
.baud_set(3'd0),
.uart_rx(uart_rx),
.data_byte(rx_data),
.rx_done(rx_done)
);
uart_byte_tx uart_byte_tx_inst(
.clk(clk),
.reset_n(reset_n),
.data_byte(tx_data),
.send_en(send_en),
.baud_set(3'd0),
.uart_tx(uart_tx),
.tx_done(tx_done),
.uart_state()
);
ctrl ctrl_inst(
.clk(clk),
.reset_n(reset_n),
.key_flag(key_flag),
.key_state(key_state),
.rx_done(rx_done),
.tx_done(tx_done),
.addra(addra),
.addrb(addrb),
.wea(wea),
.send_en(send_en)
);
dpram_blk dpram_blk_inst (
.clka(clk), // input wire clka
.wea(wea), // input wire [0 : 0] wea
.addra(addra), // input wire [7 : 0] addra
.dina(rx_data), // input wire [7 : 0] dina
.clkb (clk), // input wire clkb
.addrb(addrb), // input wire [7 : 0] addrb
.doutb(tx_data) // output wire [7 : 0] doutb
);
endmodule
对顶层进行分析综合后
四、板级验证
进行引脚约束
生成bit文件下载到板子里,连接好上位机,使用上位机向FPGA发送以下数据,摁下key0摁键开始读出数据再摁一下停止读出,结果如下: