FPGA学习——串口通信与RAM读写

一、工程简介

串口通信简单来说就是一种以连续的串行数据流形式发送或接收数据的方式。以数据帧为单位,一次传输一帧数据。一般来说,一帧数据有八个数据位,根据传输方式不同会加上不同的起始位以及停止位,所以一般一帧数据有9~11位。传输一帧数据时,每次传输一个二进制位。
SRAM叫做静态随机存取存储器。 静态随机存取存储器是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。相对之下,动态随机存取存储器(DRAM)里面所储存的数据就需要周期性地更新。
该工程就是利用串口通信的方式将上位机发送过来的数据接收后写入到FPGA的RAM中,等待接收到发送命令,就将RAM中的信息读取出来发送到上位机。

本文主要是为了记录项目过程,将模块中的各个步骤都拆开来详细说明,所以会显得有点冗长且乱。

二、功能介绍

该设计具体功能为:
利用串口通信,从PC端接收到数据存储到FPGA的RAM中;等到按下按键,将RAM中的数据通过串口通信一次性全部发送到上位机去。
根据设计功能,可将本项目大体可划分为五个模块:

  • 串口发送
  • 串口接收
  • RAM读写控制
  • 按键消抖
  • RAM核调用

注:隐约记得好像有一个错误是上位机接收到的数据是发送数据的双份,也就是上位机发送出去并经过RAM的读写后再回到上位机是两份重复的数据。因为自那以后没用过这个程序,也就没有找过原因

三、具体实现

1.顶层模块

顶层模块中实例化各个功能模块并调用,根据上述几个部分编写功能模块。(顶层程序的编写一般放到最后,根据最终的各个功能模块的接口进行编写)

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

2.UART接收模块

先定义端口以及变量:

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

3.UART发送模块

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

4.SRAM控制

在写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;    //其余情况发送使能关闭

5.RAM核调用

SRAM的IP核调用只需要注意端口就好,根据调用IP核的端口来编写SRAM的控制模块

ram_ip ram_dual(
		.clock(clk),
		.data(rx_data),
		.rdaddress(rdaddress),
		.wraddress(wraddress),
		.wren(wren),
		.q(tx_data)

6.按键消抖

因为在工程中还用到了按键,所以做了一个按键的消抖模块
原理也简单,利用了状态机对按键产生的毛刺进行判断消除从而产生真正需要的上升沿来对其他信号进行触发

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年年底的几天,当时对串口通信以及内存的理解不是很通透,就在网上找了许多资料,花了一个星期去理解这些东西,然后照葫芦画瓢做了这么一个工程。
不是很精致,但是该有的功能都有,也没什么大错误,就是当时的水平受限写的不是很精美。现在回头在梳理一遍理解更深刻了,但是太懒了,不想重新写一个了,就用之前不太精美的工程做了一个比较细一点的分析,以后有时间了可能会重新写一个精致一点的吧。

【注】个人学习笔记,请不吝赐教!

猜你喜欢

转载自blog.csdn.net/zsisinterested/article/details/118793468