这个小项目是在2013年初学FPGA时所做的,现把当时的设计笔记贴出来。
RS232C电气规定
EIA-RS-232C对电气特定、逻辑电平和各信号线功能都做了相关规定。在此部分,只简单介绍相关的电气特性。
对于RS232C标准对逻辑电平的定义,在数据部分,逻辑1的电平低于-3V;逻辑0的电平高于3V。在控制信号部分,信号有效的电平高于+3V;信号无效的电平低于-3V。在介于-3V~3V之间,高于15V,低于-15V的电压没有意义。
所以在实际的工作中,需要完成RS232电平与TTL电平的转换,以保证MCU,FPGA等能匹配RS232电平。最常用的芯片MAX232芯片可以完成TTL到RS232电平的转换。
串行通信数据格式
一帧异步通信的数据格式主要包含以下几个部分:
起始位:以一位低电平开始,表示发送端开始发送一帧数据
数据位:即要传输的数据信息,低位在前,高位在后。数据位长度一般为5-10bit,常使用8bit.
奇偶校验位:用于校验数据的正确性,可以选择为奇校验或者偶校验
停止位:用于向接收端表示一帧数据已经发送完成,长度一般为1~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驱动模块。总体设计框图如下:
在本设计中采用自底向上的设计方法。
波特率产生模块
板级整体输入时钟为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
由此可以得到一个153.6KHz的工作时钟。
发送模块
由于波特率发生器产生的时钟信号bclk为9600b/s的16倍,因此在发送器的设计中,应该每隔16拍发送一个bit.发送数据格式应该按照串口数据帧格式来传输。首先应该发送起始位,即发送端口txd从逻辑1变化到逻辑0,并持续1/9600s.其次是8个有效数据bit,低位在前,高位在后,最后是一位停止位。注意在本设计中没有校验位。
整个发送模块的状态转移图如下:
状态转换说明:
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
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
经功能仿真后测试正确。
接收模块
在涉及到双方通信的系统,接收机的复杂程度总是高于发送机的。对于串口通信也是如此。在接受系统中,起始状态和数据都需要依靠接收端检测得到,为了避免毛刺影响,能够得到正确的其实信号和有效数据,需要完成一个简单的最大似然判断。其方法如下:由于bclk的频率为9600的16倍,则对于每个数据都会有16个采样值,最终的采样结果为出现次数超过8次以上的电平值。这样就能最大限度地保证接收到的数据有最大的概率是正确的。
整个接收模块的状态机包括三个部分。状态转移图如下:
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
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
顶层模块
由以上各个子模块的描述,顶层模块的功能只是完成各个子模块的例化,以及完成与其他外部接口的通讯。在本设计中,通过上位机接收数据,当接收到的数据为0x47的时候,点亮对应的LED灯。
本部分逻辑设计代码略。