基于FPGA的串口通讯设计

这个小项目是在2013年初学FPGA时所做的,现把当时的设计笔记贴出来。


RS232C电气规定

EIA-RS-232C对电气特定、逻辑电平和各信号线功能都做了相关规定。在此部分,只简单介绍相关的电气特性。

基于FPGA的串口通讯设计_第1张图片

对于RS232C标准对逻辑电平的定义,在数据部分,逻辑1的电平低于-3V;逻辑0的电平高于3V。在控制信号部分,信号有效的电平高于+3V;信号无效的电平低于-3V。在介于-3V~3V之间,高于15V,低于-15V的电压没有意义。

所以在实际的工作中,需要完成RS232电平与TTL电平的转换,以保证MCU,FPGA等能匹配RS232电平。最常用的芯片MAX232芯片可以完成TTL到RS232电平的转换。


串行通信数据格式

一帧异步通信的数据格式主要包含以下几个部分:

起始位:以一位低电平开始,表示发送端开始发送一帧数据

数据位:即要传输的数据信息,低位在前,高位在后。数据位长度一般为5-10bit,常使用8bit.

奇偶校验位:用于校验数据的正确性,可以选择为奇校验或者偶校验

停止位:用于向接收端表示一帧数据已经发送完成,长度一般为1~2位。

空闲状态:一般用高电平表示,用于通知接收端等待数据传输。

基于FPGA的串口通讯设计_第2张图片


RS232C的串行总线在空闲的时候总保持为逻辑1状态,即串行数据连接线上的电平为-3v~-15v。当需要传输一个字符时,首先会发送一个逻辑0为起始位,表示开始发送数据;之后就逐个发送数据位、奇偶校验位和停止位(逻辑1),每一次传输1个字符(通常为8bit).由于两次传输中的时间间隔是可变的,所以这种传输被称之为异步传输。


RS232C规定了一系列的波特率标准,常用的有9600b/s、115200b/s等。在设置波特率的时候,必须通知通信双方。


具体设计

本设计在KC705的Demo板上,实现了波特率为9600b/s,停止位为1bit,不带奇偶校验位,具有异步复位功能的串口通讯控制器。最终要求和PC上的超级终端完成双向通信。为了测试串口通信是否正常,在顶层设计的时候添加了可供测试验证的部分:当在开发板上按下button时,发送数据每次加1,并且当通过PC发送数据的时候,点亮对应的LED灯。


总体设计方案

RS232串口通信主要由波特率发生模块,串口数据发送模块,串口数据接收模块构成。在顶层设计中还加入了外部按键相应模块和LED驱动模块。总体设计框图如下:

基于FPGA的串口通讯设计_第3张图片

在本设计中采用自底向上的设计方法。


波特率产生模块

板级整体输入时钟为50M,为了要产生9600b/s的波特率,那么必须使用分频器。一般来讲,为了提高系统的容错性处理,经常会要求波特率发生器的输出时钟为实际串口数据波特率的N倍,N一般取值为8、16、32等。由于串口通讯的速率较慢,取N倍后的时钟频率也不是很高,所以能保证设计完成。在本设计中,取N为16,因此波特率发生器的输出信号频率应该为9600×16=153.6kb/s.在波特率倍频后,可以不按照时钟信号的占空比为50%开设计,在本设计中使用占空比为1:325.设计的波特率发生器的verilog代码如下:

module baud_gen(
	input                                 rst,
	input                                 clk50m,
	
	output reg                            bclk,
);

reg  [8:0]                                    cnt;

always @ (posedge clk50m or posedge rst)
begin
	if(rst)
	begin
		cnt  <=  9'd0;
	end
	else
	begin
		//50_000_000 / 153600 = 325.5
		if(cnt > 9'd324)
		begin
			cnt  <=  9'd0;
		end
		else
		begin
			cnt  <=  cnt + 1'b1;
		end
	end
end

always @ (posedge clk50m or posedge rst)
begin
	if(rst)
	begin
		bclk  <=  1'b0;
	end
	else
	begin
		if(cnt > 9'd324)
			bclk  <=  1'b1;
		else
			bclk  <=  1'b0;
	end
end

endmodule

经仿真所得的仿真波形如下:

基于FPGA的串口通讯设计_第4张图片

由此可以得到一个153.6KHz的工作时钟。


发送模块

由于波特率发生器产生的时钟信号bclk为9600b/s的16倍,因此在发送器的设计中,应该每隔16拍发送一个bit.发送数据格式应该按照串口数据帧格式来传输。首先应该发送起始位,即发送端口txd从逻辑1变化到逻辑0,并持续1/9600s.其次是8个有效数据bit,低位在前,高位在后,最后是一位停止位。注意在本设计中没有校验位。


整个发送模块的状态转移图如下:

基于FPGA的串口通讯设计_第5张图片

状态转换说明:

s_idle为空闲状态,当复位信号有效或发送任务完成时,发送模块就处于s_idle状态,等待下一个发送指令(tx_cmd)到来。在s_idle中,发送完成指示tx_ready为高电平,表示随时可以接受外部的发送指令。tx_cmd信号高有效,且持续一个bclk周期。当tx_cmd有效时,发送模块的下一个状态为s_start.

s_start为发送模块的起始状态,拉底tx_ready信号,表明发送模块正在发送中,并拉底发送线txd,给出起始位,然后跳转到s_wait状态。需要注意的是,s_start状态仅持续一个bclk周期,完成起始位的发送后,转入s_wait状态。

s_wait为发送模块的等待状态,保持所有信号值不变,当发送模块处于这一状态的时候,等待计数满16个bclk后,判断8个有效数据位是否发送完成,如果发送完成,则跳转到s_stop准备发送停止位。否则,跳转到s+shift状态,发送下一个有效数据位。

s_shift为数据移位状态,发送模块在这一状态将下一个要发送的数据移动到发送端口上,然后直接跳转到s_wait状态。

s_stop状态完成停止位的发送,当有效数据发送完成以后,发送模块进入该状态,发送一个停止位后,自动进入s_idle状态,并且将tx_ready信号拉高。

verilog代码设计如下:

module uart_tx (
	input                                 bclk,
	input                                 rst,
	
	input                                 tx_cmd,
	input  [7:0]                          tx_din,
	
	output reg                            tx_ready,
	output                                txd
);

parameter S_IDLE                        = 3'b000;
parameter S_START                       = 3'b001;
parameter S_WAIT                        = 3'b010;
parameter S_SHIFT                       = 3'b011;
parameter S_STOP                        = 3'b100;

parameter LFRAME                        = 8;

reg                                     txd_t;
reg  [3:0]                              cnt;
reg  [3:0]                              dcnt;

assign txd                              = txd_t;

reg  [2:0]                              st_current;
reg  [2:0]                              st_next;

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		st_current  <=  S_IDLE;
	else
		st_current  <=  st_next;
end

always @ *
begin
	case(st_current)
		S_IDLE :
		begin
			if(tx_cmd == 1'b1)
				st_next  =  S_START;
			else
				st_next  =  S_IDLE;
		end
		
		S_START :
		begin
			st_next  =  S_WAIT;
		end
		
		S_WAIT :
		begin
			if(cnt >= 4'b1110)
			begin
				if(dcnt == LFRAME)
					st_next  =  S_STOP;
				else
					st_next  =  S_SHIFT;
			end
			else
			begin
				st_next  =  S_WAIT;
			end
		end
		
		S_SHIFT :
		begin
			st_next  =  S_WAIT;
		end
		
		S_STOP:
		begin
			if(cnt >= 4'b1110)
				st_next  =  S_IDLE;
			else
				st_next  =  S_STOP;
		end
		
		default : ;
		
	endcase
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		cnt  <=  4'h0;
	else if(st_current == S_WAIT)
	begin
		if(cnt >= 4'b1110)
			cnt  <=  4'h0;
		else
			cnt  <=  cnt + 1'b1;
	end
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		dcnt  <=  4'h0;
	else
	begin
		if(st_current == S_SHIFT)
		begin
			if(dcnt == LFRAME)
				dcnt  <=  4'h0;
			else
				dcnt  <=  dcnt + 1'b1;
		end
	end
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		tx_ready  <=  1'b0;
	else
	begin
		if(st_current == S_IDLE)
			tx_ready  <=  1'b1;
		else if((st_current == S_STOP) && (cnt >= 4'b1110))
			tx_ready  <=  1'b1;
		else
			tx_ready  <=  1'b0;
	end
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		txd_t  <=  1'b1;
	else
	begin
		if(st_current == S_IDLE)
			txd_t  <=  1'b1;
		else if(st_current == S_START)
			txd_t  <=  1'b0;
		else if(st_current == S_WAIT)
		begin
			if((cnt == 4'b1110) && (dcnt == LFRAME))
				txd_t  <=  1'b1;
		end
		else if(st_current == S_SHIFT)
			txd_t  <=  tx_din[dcnt];
	end
end
				
endmodule

对上面逻辑代码编写的testbench如下:

module uart_tx_tb ();

	reg                                   bclk;
	reg                                   rst;
	reg  [7:0]                            tx_din;
	reg                                   tx_cmd;
	
	wire                                  tx_ready;
	wire                                  txd;
	
  parameter RST_PERIOD                  = 100;
  parameter CLK_PERIOD                  = 10;
	
	initial
	begin
		rst = 1;
		bclk = 0;
		tx_din = 0;
		tx_cmd = 0;
		#RST_PERIOD;
		rst = 0;
		#20;
		tx_din = 10;
		tx_cmd = 1;
		#20;
		tx_cmd = 0;
	end
	
	always
		bclk = #(CLK_PERIOD/2.0) !bclk;
	
uart_tx uut(
	.bclk                                 (bclk),
	.rst                                  (rst),
	
	.tx_cmd                               (tx_cmd),
	.tx_din                               (tx_din),
	
	.tx_ready                             (tx_ready),
	.txd                                  (txd)
);
	
endmodule

需要发送的数据为0x0A,仿真模型如下:

基于FPGA的串口通讯设计_第6张图片


经功能仿真后测试正确。


接收模块
在涉及到双方通信的系统,接收机的复杂程度总是高于发送机的。对于串口通信也是如此。在接受系统中,起始状态和数据都需要依靠接收端检测得到,为了避免毛刺影响,能够得到正确的其实信号和有效数据,需要完成一个简单的最大似然判断。其方法如下:由于bclk的频率为9600的16倍,则对于每个数据都会有16个采样值,最终的采样结果为出现次数超过8次以上的电平值。这样就能最大限度地保证接收到的数据有最大的概率是正确的。
整个接收模块的状态机包括三个部分。状态转移图如下:

基于FPGA的串口通讯设计_第7张图片

s_idle状态为空闲状态,在这个状态下,接收机循环接收发送机所发送的起始信号。当接收到有从1->0的起始信号以后,状态机转换到采样状态。在此状态下,tx_ready的值为1.
s_sample状态为采样状态。接口模块连续采样数据,并对每16个采样数据进行最大似然判决,得到相应的逻辑值,这一过程在每次接收到数据的时候都需要重复。然后依次完成串并转换。在接收完8个数据位后,直接进入s_stop状态。在这一状态下,rx_ready信号值为0.
s_stop状态用于检测停止位,在接收到停止为后,直接转入到s_idle状态。如果设计者需要添加校验位的话,还需要在此部分先做数据校验。另外需要注意的是,在第八个数据位后一定是停止位,再因为我所选择的时钟为9600的16倍,所以需要在停止位后有时间补偿。
逻辑设计代码如下:

module uart_rx(
	input                                 bclk,
	input                                 rst,
	input                                 rxd,
	
	output  [7:0]                         rx_dout,
	output  reg                           rx_ready
);

parameter LFRAME                        = 8;
parameter S_IDLE                        = 3'b001;
parameter S_SAMPLE                      = 3'b010;
parameter S_STOP                        = 3'b100;

reg  [2:0]                              st_current;
reg  [2:0]                              st_next;

reg  [3:0]                              cnt;
reg  [3:0]                              dcnt;
reg  [3:0]                              num;
reg  [7:0]                              rx_dout_t;

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		st_current  <=  S_IDLE;
	else
		st_current  <=  st_next;
end

always @ *
begin
	case(st_current)
		S_IDLE :
		begin
			if((cnt == 4'b1111) && (num > 7))
				st_next  =  S_SAMPLE;
			else
				st_next  =  S_IDLE;
		end
		
		S_SAMPLE :
		begin
			if(dcnt == LFRAME)
				st_next  =  S_STOP;
			else
				st_next  =  S_SAMPLE;
		end
		
		S_STOP :
		begin
			if(cnt == 4'b1111)
				st_next  =  S_STOP;
		end
		
		default : ;
	endcase
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		cnt  <=  4'b0000;
	else
		cnt  <=  cnt + 1'b1;
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		dcnt  <=  4'b0000;
	else
	begin
		if(st_current == S_IDLE)
			dcnt  <=  4'h0;
		else if((st_current == S_SAMPLE) && (cnt == 4'b1111))
			dcnt  <=  dcnt + 1'b1;
	end
end

// 最大似然
always @ (posedge bclk or posedge rst)
begin
	if(rst)
		num  <=  4'h0;
	else
	begin
		if((st_current == S_IDLE) && (rxd == 1'b0))
		begin
			if(cnt == 4'b1111)
				num  <=  4'h0;
			else
				num  <=  num + 1'b1;
		end
		else if((st_current == S_SAMPLE) && (rxd == 1'b1))
		begin
			if(cnt == 4'b1111)
				num  <=  4'h0;
			else
				num  <=  num + 1'b1;
		end
	end
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		rx_ready  <=  1'b0;
	else
	begin
		if(st_current == S_SAMPLE)
			rx_ready  <=  1'b0;
		else
			rx_ready  <=  1'b1;
	end
end

always @ (posedge bclk or posedge rst)
begin
	if(rst)
		rx_dout_t  <=  7'h00;
	else
	begin
		if(st_current == S_SAMPLE)
		begin
			if(num > 7)
				rx_dout_t[dcnt]  <=  1'b1;
			else
				rx_dout_t[dcnt]  <=  1'b0;
		end
	end
end

assign rx_dout                          = rx_dout_t;

endmodule

对上逻辑设计编写testbench如下:

module uart_rx_tb();

	reg                                   bclk;
	reg                                   rst;
	reg                                   rxd;
	
	wire                                  rx_ready;
	wire  [7:0]                           rx_dout;
	
	uart_rx
	uut (
	.bclk                                 (bclk),
	.rst                                  (rst),
	.rxd                                  (rxd),
	
	.rx_dout                              (rx_dout),
	.rx_ready                             (rx_ready)
);

	initial
	begin
		bclk = 0;
		rst = 1;
		rxd = 1;
		#100;
		rst = 0;
		rxd = 0;
		#320;
		rxd = 1;
	end
	
	always bclk = #5 !bclk;
endmodule

在testbench中,通过txd发送一个0xFE的反向bit序列(低位在前,高位在后),仿真波形如下:

基于FPGA的串口通讯设计_第8张图片


顶层模块
由以上各个子模块的描述,顶层模块的功能只是完成各个子模块的例化,以及完成与其他外部接口的通讯。在本设计中,通过上位机接收数据,当接收到的数据为0x47的时候,点亮对应的LED灯。
本部分逻辑设计代码略。


你可能感兴趣的:(Interface)