串口通信简单来说就是一种以连续的串行数据流形式发送或接收数据的方式。以数据帧为单位,一次传输一帧数据。一般来说,一帧数据有八个数据位,根据传输方式不同会加上不同的起始位以及停止位,所以一般一帧数据有9~11位。传输一帧数据时,每次传输一个二进制位。
SRAM叫做静态随机存取存储器。 静态随机存取存储器是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,动态随机存取存储器(DRAM)里面所储存的数据就需要周期性地更新。
该工程就是利用串口通信的方式将上位机发送过来的数据接收后写入到FPGA的RAM中,等待接收到发送命令,就将RAM中的信息读取出来发送到上位机。
本文主要是为了记录项目过程,将模块中的各个步骤都拆开来详细说明,所以会显得有点冗长且乱。
该设计具体功能为:
利用串口通信,从PC端接收到数据存储到FPGA的RAM中;等到按下按键,将RAM中的数据通过串口通信一次性全部发送到上位机去。
根据设计功能,可将本项目大体可划分为五个模块:
注:隐约记得好像有一个错误是上位机接收到的数据是发送数据的双份,也就是上位机发送出去并经过RAM的读写后再回到上位机是两份重复的数据。因为自那以后没用过这个程序,也就没有找过原因
顶层模块中实例化各个功能模块并调用,根据上述几个部分编写功能模块。(顶层程序的编写一般放到最后,根据最终的各个功能模块的接口进行编写)
module ram_uart(
input clk,
input rst_n,
input rs232_rx,
input key_in,
output rs232_tx
);
wire [7:0] rx_data;
wire [7:0] tx_data;
wire tx_done;
wire rx_done;
wire key_flag;
wire key_state;
wire [11:0] rdaddress;
wire [11:0] wraddress;
wire wren;
wire send_en;
fsm_key_filter uut_key_filter(
.clk(clk),
.rst_n(rst_n),
.key(key_in),
.key_flag(key_flag),
.key_state(key_state)
);
uart_rx2 uut_uart_rx(
.clk(clk),
.rst_n(rst_n),
.rs232_rx(rs232_rx),
.baud_set(3'd0),
.rx_done(rx_done),
.data_byte(rx_data)
);
uart_tx2 uut_uart_tx(
.clk(clk),
.rst_n(rst_n),
.send_en(send_en),
.baud_set(3'd0),
.data_byte(tx_data),
.rs232_tx(rs232_tx),
.tx_done(tx_done),
.uart_state()
);
ram_ip ram_dual(
.clock(clk),
.data(rx_data),
.rdaddress(rdaddress),
.wraddress(wraddress),
.wren(wren),
.q(tx_data)
);
uart_ctrl uut_controller(
.clk(clk),
.rst_n(rst_n),
.key_flag(key_flag),
.key_state(key_state),
.rx_done(rx_done),
.tx_done(tx_done),
.rdaddress(rdaddress),
.wraddress(wraddress),
.wren(wren),
.send_en(send_en)
);
endmodule
先定义端口以及变量:
module uart_rx2
(
input clk,
input rst_n,
input rs232_rx,
input [2:0] baud_set,
output reg rx_done,
output reg [7:0] data_byte
);
reg rs232_rx_reg1;
reg rs232_rx_reg2;
reg rs232_rx_syn1;
reg rs232_rx_syn2;
reg uart_state;
reg [8:0] div_cnt;
reg [8:0] cnt_max;//div_cnt max value
reg baud_clk;
reg [7:0] baud_cnt;
reg [3:0] reg_data_byte [7:0];
reg [2:0] START;
reg [2:0] STOP;
// wire pos_edge;
wire neg_edge;
对信号延时两个时钟周期:
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
rs232_rx_syn1 <= 0;
rs232_rx_syn2 <= 0;
end else begin
rs232_rx_syn1 <= rs232_rx;
rs232_rx_syn2 <= rs232_rx_syn1;
end
波特率计数器设置(可调):
//------baud_set------------------------
always@(*)
case(baud_set)
3'd0: cnt_max <= 9'd325;
3'd1: cnt_max <= 9'd163;
3'd2: cnt_max <= 9'd81;
3'd3: cnt_max <= 9'd54;
3'd4: cnt_max <= 9'd27;
default: cnt_max <= 9'd325;
endcase
//----frequence div-->baud_clk-----------
always@(posedge clk or negedge rst_n)
if(!rst_n)
div_cnt <= 0;
else if(uart_state)begin
if(div_cnt == cnt_max)
div_cnt <= 0;
else
div_cnt <= div_cnt + 9'd1;
end else
div_cnt <= 0;
always@(posedge clk or negedge rst_n)
if(!rst_n)
baud_clk <= 1'd0;
else if(div_cnt == 9'd1)
baud_clk <= 1'd1;
else
baud_clk <= 1'd0;
产生一个下降沿以此来作为传输信号的起始标志:
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
rs232_rx_reg1 <= 0;
rs232_rx_reg2 <= 0;
end else begin
rs232_rx_reg1 <= rs232_rx_syn2;
rs232_rx_reg2 <= rs232_rx_reg1;
end
assign neg_edge = (!rs232_rx_reg1 & rs232_rx_reg2);
当检测到下降沿时,表明通信开始,即把uart_state置1。:
//----uart_state-------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
uart_state <= 0;
else if(neg_edge)
uart_state <= 1;
else if(rx_done || (baud_cnt==12 && (START > 2)))
uart_state <= 0;
else
uart_state <= uart_state;
波特率控制接收,如果接收完成标志位拉高,则波特计数器清零,即准备开始新一帧的数据接收:
//----baud_cnt--------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
baud_cnt <= 0;
else if(rx_done || (baud_cnt==12 && (START > 2)))
baud_cnt <= 0;
else if(baud_clk)
baud_cnt <= baud_cnt + 8'd1;
else
baud_cnt <= baud_cnt;
如果波特计数器计数完成,则接收完成,接收完成标志位拉高:
//----rx_done----------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
rx_done <= 1'b0;
else if(baud_cnt == 8'd159)
rx_done <= 1'b1;
else
rx_done <= 1'b0;
接收过程中,如果波特计数器计数完成,则将数据临时寄存器中的数据放到端口进行接收
//----data_byte--------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
data_byte <= 8'd0;
else if(baud_cnt == 8'd159)begin
data_byte[0] <= reg_data_byte[0][2];
data_byte[1] <= reg_data_byte[1][2];
data_byte[2] <= reg_data_byte[2][2];
data_byte[3] <= reg_data_byte[3][2];
data_byte[4] <= reg_data_byte[4][2];
data_byte[5] <= reg_data_byte[5][2];
data_byte[6] <= reg_data_byte[6][2];
data_byte[7] <= reg_data_byte[7][2];
end
发送数据寄存器控制:
根据波特计数器的计数进度,来控制 接收数据寄存器中存入数据的进度,等待波特计数完成就将寄存器中的待接收数据送入接收端口等待接收。
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
START <= 3'd0;
reg_data_byte[0] <= 3'd0;
reg_data_byte[1] <= 3'd0;
reg_data_byte[2] <= 3'd0;
reg_data_byte[3] <= 3'd0;
reg_data_byte[4] <= 3'd0;
reg_data_byte[5] <= 3'd0;
reg_data_byte[6] <= 3'd0;
reg_data_byte[7] <= 3'd0;
STOP <= 3'd0;
end
else if(baud_clk)begin
case(baud_cnt)
0: begin
START <= 3'd0;
reg_data_byte[0] <= 3'd0;
reg_data_byte[1] <= 3'd0;
reg_data_byte[2] <= 3'd0;
reg_data_byte[3] <= 3'd0;
reg_data_byte[4] <= 3'd0;
reg_data_byte[5] <= 3'd0;
reg_data_byte[6] <= 3'd0;
reg_data_byte[7] <= 3'd0;
STOP <= 3'd0;
end
6,7,8,9,10,11: START <= START + rs232_rx_reg2;
22,23,24,25,26,27:
reg_data_byte[0] <= reg_data_byte[0] + rs232_rx_reg2;
38,39,40,41,42,43:
reg_data_byte[1] <= reg_data_byte[1] + rs232_rx_reg2;
54,55,56,57,58,59:
reg_data_byte[2] <= reg_data_byte[2] + rs232_rx_reg2;
70,71,72,73,74,75:
reg_data_byte[3] <= reg_data_byte[3] + rs232_rx_reg2;
86,87,88,89,90,91:
reg_data_byte[4] <= reg_data_byte[4] + rs232_rx_reg2;
102,103,104,105,106,107:
reg_data_byte[5] <= reg_data_byte[5] + rs232_rx_reg2;
118,119,120,121,122,123:
reg_data_byte[6] <= reg_data_byte[6] + rs232_rx_reg2;
130,131,132,133,134,135:
reg_data_byte[7] <= reg_data_byte[7] + rs232_rx_reg2;
148,149,150,151,152,153: STOP <= STOP + rs232_rx_reg2;
default begin
START <= START;
reg_data_byte[0] <= reg_data_byte[0];
reg_data_byte[1] <= reg_data_byte[1];
reg_data_byte[2] <= reg_data_byte[2];
reg_data_byte[3] <= reg_data_byte[3];
reg_data_byte[4] <= reg_data_byte[4];
reg_data_byte[5] <= reg_data_byte[5];
reg_data_byte[6] <= reg_data_byte[6];
reg_data_byte[7] <= reg_data_byte[7];
STOP <= STOP;
end
endcase
end
end
endmodule
uart的发送模块与接收模块的思路基本一致,所以没有详细的说明
module uart_tx2(
input clk, //50MHz
input rst_n,
input send_en,
input [2:0] baud_set,
input [7:0] data_byte,
output reg rs232_tx,
output reg tx_done,
output reg uart_state
);
parameter START = 1'b0;
parameter STOP = 1'b1;
//----define--------------------------------------
reg [7:0] reg_data_byte;
reg [15:0] cnt;//divide frequence counter
reg baud_clk;
reg [15:0] cnt_max;//baud value
reg [3:0] cnt_bit;//
//----baud_set--------------------------------------
always@(*)begin
case(baud_set)
0: cnt_max <= 16'd5207;
1: cnt_max <= 16'd2603;
2: cnt_max <= 16'd1301;
3: cnt_max <= 16'd867;
4: cnt_max <= 16'd433;
default: cnt_max <= 5207;
endcase
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
cnt <= 0;
end
else if(uart_state)begin
if(cnt == cnt_max)
cnt <= 0;
else
cnt <= cnt +16'd1;
end else
cnt <= 0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
baud_clk <= 1'b0;
else if(cnt == 16'd1)
baud_clk <= 1'b1;
else
baud_clk <= 1'b0;
end
//----cnt_bit----------------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt_bit <= 0;
else if(cnt_bit == 4'd11)
cnt_bit <= 0;
else if(baud_clk)
cnt_bit <= cnt_bit + 4'd1;
else
cnt_bit <= cnt_bit;
end
//----rs232_tx --------------------------------------
always@(posedge clk or negedge rst_n)begin // asynchronos transimiter, reg_data_byte is used to keep input data stable
if(!rst_n)
reg_data_byte <= 8'd0;
else if(send_en)
reg_data_byte <= data_byte;
else
reg_data_byte <= reg_data_byte;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
rs232_tx <= 0;
end begin
case(cnt_bit)
0: rs232_tx <= 1'b1;
1: rs232_tx <= START; //0
2: rs232_tx <= reg_data_byte[0];
3: rs232_tx <= reg_data_byte[1];
4: rs232_tx <= reg_data_byte[2];
5: rs232_tx <= reg_data_byte[3];
6: rs232_tx <= reg_data_byte[4];
7: rs232_tx <= reg_data_byte[5];
8: rs232_tx <= reg_data_byte[6];
9: rs232_tx <= reg_data_byte[7];
10: rs232_tx <= STOP; //1
default: rs232_tx <= 1;
endcase
end
end
//----tx_done-----------------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
tx_done <= 1'b0;
else if(cnt_bit == 4'd11)
tx_done <= 1'b1;
else
tx_done <= 1'b0;
end
//----uart_state--------------------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
uart_state <= 1'b0;
else if(send_en)
uart_state <= 1'b1;
else if(cnt_bit == 4'd11)
uart_state <= 0;
else
uart_state <= uart_state;
endmodule
在写SRAM以前,先了解一下RAM的基本读写步骤:
SRAM分为单端口RAM和双端口RAM,双端口RAM又分为同步读写和异步读写。其中异步读写不受读写统一读写时钟clk的控制,只和片选,使能等控制信号有关。
在读写时,需要先使能读或写信号,才能使SRAM处于工作状态进行读或写。读写需要提供具体的内存地址,才能把数据准确的存放到RAM中去。
对于SRAM的控制,还是先定义了输入输出端口以及相关变量等。
module uart_ctrl(
input clk,
input rst_n,
input key_flag,
input key_state,
input rx_done, //接收完成标志
input tx_done, //发送完成标志
output reg [11:0] rdaddress, //读地址
output reg [11:0] wraddress, //写地址
output wren, //读写使能
output reg send_en //发送使能
);
reg read_send; //
reg reg1_done;
reg reg2_done;
读写使能要一直与接收使能保持一致,即只要接收完成,就打开读写能使开始存储:
assign wren = rx_done;
对写地址的控制:如果接收标志拉高,即已经从其他地方接收到数据后,开始向SRAM中存储数据,这时提供写入数据要存放的地址,开始写入。接收标志每完成一次,写地址就向下移一位以便下次数据写入
always@(posedge clk or negedge rst_n)
if(!rst_n) //若复位端有效,则将写地址置零
wraddress <= 12'd0;
else if((wraddress==4094)&&(rx_done==1))
wraddress <= 12'd0;
else if(rx_done) //如果接收完成,则将写地址移到下一个
wraddress <= wraddress +12'd1;
else
wraddress <= wraddress; //其余情况,写地址不动
读地址的控制与写地址控制一样,只不过由于设定了读数据的规定,所以读数据的地址应该是从读命令触发开始一直下移,直到读完所有的数据
//------------读使能控制------------
always@(posedge clk or negedge rst_n)
if(!rst_n) //若复位端有效,则将发送置零
read_send <= 1'd0;
else if(key_flag && !key_state) //如果按键按下一次,则发送打开
read_send <=1;
else if((rdaddress==4094)&&(tx_done==1)) //当读地址到4095且最后一帧数据发送完成,关闭发送
read_send <=0;
else
read_send <= read_send; //其余发送使能不变
//------------读地址控制------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
rdaddress <= 12'd0;
else if(read_send && tx_done) //如果读发送有效并且一帧数据发送完成,将读地址移到下一位
rdaddress <= rdaddress +12'd1;
else if((rdaddress==4094)&&(tx_done==1))
rdaddress <=12'd0;
else
rdaddress <= rdaddress; //其余情况读地址不动
如果数据读取并发送完成,就将完成标志延时两个时钟周期
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
reg1_done <= 1'd0;
reg2_done <= 1'd0;
end
else begin
reg1_done <= (read_send && tx_done); //如果发送有效而且一帧数据发送完成,延时两个时钟
reg2_done <= reg1_done;
end
在SRAM中根据发送数据的命令同时触发读RAM使能以及发送使能
always@(posedge clk or negedge rst_n)
if(!rst_n)
send_en <= 1'd0;
else if(key_flag && !key_state) //如果按键按下,则发送使能打开
send_en <= 1'd1;
else if(reg2_done)
send_en <= 1'd1; //或者发送有效而且一帧数据发送完成,发送使能也打开
else
send_en <= 1'd0; //其余情况发送使能关闭
SRAM的IP核调用只需要注意端口就好,根据调用IP核的端口来编写SRAM的控制模块
ram_ip ram_dual(
.clock(clk),
.data(rx_data),
.rdaddress(rdaddress),
.wraddress(wraddress),
.wren(wren),
.q(tx_data)
因为在工程中还用到了按键,所以做了一个按键的消抖模块
原理也简单,利用了状态机对按键产生的毛刺进行判断消除从而产生真正需要的上升沿来对其他信号进行触发
module fsm_key_filter#(
parameter IDLE = 4'b0001,
parameter FILTER1 = 4'b0010,
parameter DOWN = 4'b0100,
parameter FILTER2 = 4'b1000
)
(
input clk, //50MHz 20us
input rst_n,
input key,
output key_flag,
output reg key_state
);
reg cnt_en;
reg cnt_full;
reg [19:0] cnt1;
//reg [19:0] cnt2;
reg [3:0] state;
reg key_syn1;
reg key_syn2;
reg key_reg1;
reg key_reg2;
wire pos_edge;
wire neg_edge;
always@(posedge clk or negedge rst_n)
if(!rst_n)begin
state <= IDLE;
cnt_en <= 1'b0;
end else
begin
case(state)
IDLE:
begin
if(neg_edge)begin
state <= FILTER1;
cnt_en <= 1'b1;
end else
begin
state <= IDLE;
cnt_en <= 1'b0;
end
end
FILTER1:
begin
if(cnt_full)//20ms
begin
state <= DOWN;
cnt_en <= 1'b0;
end
else if(pos_edge)
begin
state <= IDLE;
cnt_en <= 1'b0;
end else
begin
state <= FILTER1;
cnt_en <= cnt_en;
end
end
DOWN:
begin
if(pos_edge)begin
cnt_en <= 1'b1;
state <= FILTER2;
end else
begin
cnt_en <= 1'b0;
state <= DOWN;
end
end
FILTER2:
begin
if(cnt_full)
state <= IDLE;
else if(neg_edge)begin
cnt_en <= 1'b0;
state <= DOWN;
end
else
state <= FILTER2;
end
default: begin
state <= IDLE;
cnt_en <= 1'b0;
end
endcase
end
//----cnt--------------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt1 <= 20'd0;
else if(cnt_en)
cnt1 <= cnt1 + 20'd1;
else
cnt1 <= 20'd0;
end
always@(posedge clk or negedge rst_n)begin
if(!rst_n)
cnt_full <= 1'b0;
else if(cnt1 == 20'd999_999)
cnt_full <= 1'b1;
else
cnt_full <= 1'b0;
end
//----asyn_key-->syn---------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_syn1 <= 1'b0;
key_syn2 <= 1'b0;
end else
begin
key_syn1 <= key;
key_syn2 <= key_syn1;
end
end
//----key edge detect--------------------------------
always@(posedge clk or negedge rst_n)begin
if(!rst_n)begin
key_reg1 <= 1'b0;
key_reg2 <= 1'b0;
end else
begin
key_reg1 <= key_syn2;
key_reg2 <= key_reg1;
end
end
assign neg_edge = (!key_reg1) & key_reg2;
assign pos_edge = key_reg1 & (!key_reg2);
//----key_flag---------------------------------------
assign key_flag = (cnt1 == 20'd999_999)? 1'b1:1'b0;
//----key_state--------------------------------------
always@(posedge clk or negedge rst_n)
if(!rst_n)
key_state <= 1;
else if(cnt_full)
key_state <= ~key_state;
else
key_state <= key_state;
endmodule
这个工程是大半年前写的,也就是在2020年年底的几天,当时对串口通信以及内存的理解不是很通透,就在网上找了许多资料,花了一个星期去理解这些东西,然后照葫芦画瓢做了这么一个工程。
不是很精致,但是该有的功能都有,也没什么大错误,就是当时的水平受限写的不是很精美。现在回头在梳理一遍理解更深刻了,但是太懒了,不想重新写一个了,就用之前不太精美的工程做了一个比较细一点的分析,以后有时间了可能会重新写一个精致一点的吧。
【注】个人学习笔记,请不吝赐教!