||
基于FPGA的串口驱动设计
1设计需求
本设计预完成基于FPGA的串口驱动程序的编写,可实现FPGA与串口设备的数据传输。本设计拟以3实验完成串口驱动的编写、测试以及使用实例。
2设计内容2.1设计原理2.1.1串口通信串口传输是一种通用的两线制全双工通信方式,其协议如下:
其传输与接收都不时钟同步,为异步通信方式,在通信之前收发双方必须确认通信的频率(常称为比特率),其具体时序传输过程已发送举例如下时序:
图1 串口时序
总线常态为逻辑“1”高电平,在有数据传输时发送端将信号线拉低,接收检测到下降沿即开始接收数据,电平的改变频率受预先定义的比特率控制,传输8位数据(串口数据位数也有5、6、7等不同类,常用8位)后传输1位奇(偶)校验位亦可依据协议省略不发,最后输出上升沿停止位将总线置高,设为常态,等待下次下降沿起始位的触发。接收则是依据发送时序进行采样。
2.1.2FIFO存储器FIFO常用于通信缓冲器,与通常RAM不同的是,其没有地址线数据,输出方式采用先入先出的方式(FIFO--First In First Out),正常的工作模式是在输入使能条件下,存储器每一个时钟边沿触发就会存储一个数据,其读指针同时也会发生偏移,在输出使能的条件下每个时钟边沿触发就出读出一个数据,其写指针同时也会发生偏移,多用在数据的缓冲作用。其模型如图2所示,初始状态下读写指针都指定在0x00上,此时FIFO状态为空,当读使能信号使能,并保持一个时钟(FIFO读写时钟)周期,数据将通过数据输入端存储到0x00读指针也偏移到0x01,依次类推。当FIFO中有数据(非空)时,写使能有效并保持一个时钟,FIFO将会输出写指针所指的存储空间的数据,完成先入先出的效果。
图2 FIFO示意图
2.2参考文档在撰写该文档并实验之前,笔者先拜读了特权、小梅哥以及黑金原创的教程。
2.2.1特权FPGA实验实现与理解特权同学的串口代码目前是网上流传最为广泛的版本。在拿到特权同学的代码,首先打开工程编译了一下,通过了!由于习惯问题,先看看其RTL视图如图3所示。特权果然大神,其代码结构十分明显,主要哟四个模块组成分别是发送波特率发生器、发送模块、接收波特率发生器以及接受模块。其中发送波特率发生器与接收波特率发生器的逻辑完全一样,采用同一个模块文件例化而成(特权同学特别说明:不是资源共享,和软件中的同一个子程序调用不能混为一谈,可称之为逻辑复制)。
图3 特权UART驱动RTL视图
特权的代码经过综合后,下载到我的板子上测试,果然可以完成单字节的收发(其功能是接收串口发送一个字节的数据后并返回)。特权同学的各模块接口定义如表1所示:
speed_select模块接口:
位宽 | 描述 | |
clk | 1 | 工作时钟 |
rst_n | 1 | |
bps_start | 1 | 波特率同步使能信号 |
clk_bps | 1 | 波特率脉冲信号输出口 |
my_uart_rx模块接口:
位宽 | 描述 | |
clk | 1 | 工作时钟 |
rst_n | 1 | 异步复位信号,低电平有效 |
rs232_rx | 1 | 串口接收信号线 |
rx_data | 8 | 接收数据8位并行输出接口 |
rx_int | 1 | 接收数据中断,接收到数据期间始终为高电平 |
clk_bps | 1 | 波特率脉冲信号输入 |
bps_start | 1 | 波特率发生器使能,在接收过程中始终为高电平 |
my_uart_tx模块接口:
接口名称 | 位宽 | 描述 |
clk | 1 | 工作时钟 |
rst_n | 1 | 异步复位信号,低电平有效 |
rs232_tx | 1 | 串口接收信号线 |
tx_data | 8 | 发送8为数据并行输入接口 |
tx_int | 1 | 发送中断请求,下降沿开始发送串口数据 |
clk_bps | 1 | 波特率脉冲信号输入 |
bps_start | 1 | 波特率发生器使能,在接收过程中始终为高电平 |
(说明:在特权的原本代码中tx_int、tx_data名称为rx_int、rx_data为了与上发送模块的接口区分,稍作了修改。)
在了解了各模块接口之后,我们基本上可以使用该驱动了。返回top层的RTL视图观察该实验的连接关系,笔者发现了一个问题。笔者将每个模块视为一个个黑匣子,只根据以上的接口功能对实验进行分析。首先上电之后,接收模块接收数据,该段时间my_uart_rx中的rx_int、bps_start会为高电平,波特率发生器产生信号,接收正常;此时由于my_uart_tx模块没有接收到其接口tx_int的下降沿,处于等待状态。当数据接收完毕后,my_uart_rx中的rx_int、bps_start会被拉低,而tx_int与rx_int的连接关系会触发my_uart_tx发送数据,而后my_uart_rx可以继续等待并接收数据。理论上,在my_uart_rx接收了第二个字符后,my_uart_tx也发送完了第一个字符。这样就形成了一个数据通路,以这种状态足以完成多字节的传输与返回。这种状态需要个条件,就是发送速度大于或者等于接收速度(在两者相等时,发送结束到接收到发送请求必须要有足够的响应时间)。
指出了上面的问题,查看模块内部的具体代码:
波特率发生器代码:
module speed_select(
clk,rst_n,
bps_start,clk_bps
);
input clk; // 50MHz主时钟
input rst_n; //低电平复位信号
input bps_start; //接收到数据后,波特率时钟启动信号置位
output clk_bps; // clk_bps的高电平为接收或者发送数据位的中间采样点
/*
parameter bps9600 = 5207, //波特率为9600bps
bps19200 = 2603, //波特率为19200bps
bps38400 = 1301, //波特率为38400bps
bps57600 = 867, //波特率为57600bps
bps115200 = 433; //波特率为115200bps
parameter bps9600_2 = 2603,
bps19200_2 = 1301,
bps38400_2 = 650,
bps57600_2 = 433,
bps115200_2 = 216;
*/
//以下波特率分频计数值可参照上面的参数进行更改
`define BPS_PARA 5207 //波特率为9600时的分频计数值
`define BPS_PARA_2 2603 //波特率为9600时的分频计数值的一半,用于数据采样
reg[12:0] cnt; //分频计数
reg clk_bps_r; //波特率时钟寄存器
//----------------------------------------------------------
reg[2:0] uart_ctrl; // uart波特率选择寄存器
//----------------------------------------------------------
always @ (posedge clk or negedge rst_n)
if(!rst_n) cnt <= 13'd0;
else if((cnt == `BPS_PARA) || !bps_start) cnt <= 13'd0; //波特率计数清零
else cnt <= cnt+1'b1; //波特率时钟计数启动
always @ (posedge clk or negedge rst_n)
if(!rst_n) clk_bps_r <= 1'b0;
else if(cnt == `BPS_PARA_2) clk_bps_r <= 1'b1; // clk_bps_r高电平为接收数据位的中间采样点,同时也作为发送数据的数据改变点
else clk_bps_r <= 1'b0;
assign clk_bps = clk_bps_r;
endmodule
1~9行:为模块的接口定义,个接口功能在上文已说明,不再赘述;
12~27行:以注释的方式给我们说明了各个不同波特率的计数值,该值只在50Mhz的时钟平率下有效,具体计算以9600bps举例为何为5207,详情阅读下面代码说明。
37~41行:异步清零同步使能计数器的典型设计,在50Mhz时钟驱动下,计数器会在使能情况下从0计数到达5207后清零再次计数,这样0~5207计数时间为5208*20ns=104160ns;计算一下9600Hz的周期1000,000,000ns/9600=104166ns。两者基本相同。
43~47行:在0~5207计数到中间值2603时,输出一个脉冲信号,其频率依然是9600。这里为何要在其中间值产生一个脉冲,而不是在开始计数时,也不是在计数之后产生呢?特权同学是有其道理的!!!下文会对其进一步分析。
发送模块代码:
module my_uart_tx(
clk,rst_n,
rx_data,rx_int,rs232_tx,
clk_bps,bps_start
);
input clk; // 50MHz主时钟
input rst_n; //低电平复位信号
input clk_bps; // clk_bps_r高电平为接收数据位的中间采样点,同时也作为发送数据的数据改变点
input[7:0] rx_data; //接收数据寄存器
input rx_int; //接收数据中断信号,接收到数据期间始终为高电平,在该模块中利用它的下降沿来启动串口发送数据
output rs232_tx; // RS232发送数据信号
output bps_start; //接收或者要发送数据,波特率时钟启动信号置位
//---------------------------------------------------------
reg rx_int0,rx_int1,rx_int2; //rx_int信号寄存器,捕捉下降沿滤波用
wire neg_rx_int; // rx_int下降沿标志位
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rx_int0 <= 1'b0;
rx_int1 <= 1'b0;
rx_int2 <= 1'b0;
end
else begin
rx_int0 <= rx_int;
rx_int1 <= rx_int0;
rx_int2 <= rx_int1;
end
end
assign neg_rx_int = ~rx_int1 & rx_int2; //捕捉到下降沿后,neg_rx_int拉高保持一个主时钟周期
//---------------------------------------------------------
reg[7:0] tx_data; //待发送数据的寄存器
//---------------------------------------------------------
reg bps_start_r;
reg tx_en; //发送数据使能信号,高有效
reg[3:0] num;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
bps_start_r <= 1'bz;
tx_en <= 1'b0;
tx_data <= 8'd0;
end
else if(neg_rx_int) begin //接收数据完毕,准备把接收到的数据发回去
bps_start_r <= 1'b1;
tx_data <= rx_data; //把接收到的数据存入发送数据寄存器
tx_en <= 1'b1; //进入发送数据状态中
end
else if(num==4'd11) begin //数据发送完成,复位
bps_start_r <= 1'b0;
tx_en <= 1'b0;
end
end
assign bps_start = bps_start_r;
//---------------------------------------------------------
reg rs232_tx_r;
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
num <= 4'd0;
rs232_tx_r <= 1'b1;
end
else if(tx_en) begin
if(clk_bps) begin
num <= num+1'b1;
case (num)
4'd0: rs232_tx_r <= 1'b0; //发送起始位
4'd1: rs232_tx_r <= tx_data[0]; //发送bit0
4'd2: rs232_tx_r <= tx_data[1]; //发送bit1
4'd3: rs232_tx_r <= tx_data[2]; //发送bit2
4'd4: rs232_tx_r <= tx_data[3]; //发送bit3
4'd5: rs232_tx_r <= tx_data[4]; //发送bit4
4'd6: rs232_tx_r <= tx_data[5]; //发送bit5
4'd7: rs232_tx_r <= tx_data[6]; //发送bit6
4'd8: rs232_tx_r <= tx_data[7]; //发送bit7
4'd9: rs232_tx_r <= 1'b1; //发送结束位
default: rs232_tx_r <= 1'b1;
endcase
end
else if(num==4'd11) num <= 4'd0; //复位
end
end
assign rs232_tx = rs232_tx_r;
endmodule
相较于接收,发送总是更为简单,因为串口的发送只是按一定频率将数据按协议的模式一步一步发出去就完事了,而接收则需要判断什么时候采样,需要去考虑发送方的时序。
特权同学串口发送模块
1~15行:为接口的定义说明,上文已作说明,不再赘述;
17~34行:为一个电平检测电路,通过4个寄存器rx_int0,rx_int1,rx_int2对rx_int的电平进行缓存,根据缓存的状态判断边沿。
如:assign neg_rx_int =~rx_int0 & ~rx_int1 & rx_int2 & rx_int3; //捕捉下降沿,neg_rx_int=1
assign neg_rx_int = rx_int0 & rx_int1 & ~rx_int2 &~rx_int3; //捕捉到上升沿,neg_rx_int=1
当然特权同学只是用2个寄存器也是同样的原理,可以有一定的灵明度提升。采用多个寄存器缓存判断边沿触发是一种时间换稳定性的方法。
37~59行:控制波特率发生器使能语句,在检测到rx_int的下降沿后,使能波特率发生器、将tx_en设置为1以及锁存外部输入的传输数据。只有在寄存器num计数到11时才停止波特率和数据的发送。
64~92行:此段代码为本模块的核心内容,其就是一个有限状态机(小梅哥称之为有限序列机,小弟无法理解两者区别,姑且称之为状态机)。该状态机受控于两个信号tx_en和clk_bps,其中tx_en将会在接收发送请求后被置1,clk_bps也会随着脉冲发生器被使能后,输出9600hz的脉冲信号。当发送开始后每一次脉冲num都会自增1,且
num为0时,输出起始位;
num为1时输出数据0位;
... ...
num为8时输出数据7位;
num为9时输出停止位;
计数到11时停止发送总共会产生11个脉冲信号,这里就是问题了,1位起始位+8位数据位+1位停止位总共10位数据为何需要11个脉冲信号操纵呢!这里就是导致为什么该驱动只能接收但个字节而不能接受多个字节。由于其在发送后有多等待了一个周期,拖慢了传输的速率,影响到整个信道的流畅性。修改55行和88行代码为
55 else if(num==4'd10) begin //数据发送完成,复位
88 else if(num==4'd11) num <= 4'd0; //复位
将11改为10在测试能否进行多字节的收发,测试发现还是无法达到目标,继续看看接收部分。
接收模块代码:
module my_uart_rx(
clk,rst_n,
rs232_rx,rx_data,rx_int,
clk_bps,bps_start
);
input clk; // 50MHz主时钟
input rst_n; //低电平复位信号
input rs232_rx; // RS232接收数据信号
input clk_bps; // clk_bps的高电平为接收或者发送数据位的中间采样点
output bps_start; //接收到数据后,波特率时钟启动信号置位
output[7:0] rx_data; //接收数据寄存器,保存直至下一个数据来到
output rx_int; //接收数据中断信号,接收到数据期间始终为高电平
//----------------------------------------------------------------
reg rs232_rx0,rs232_rx1,rs232_rx2,rs232_rx3; //接收数据寄存器,滤波用
wire neg_rs232_rx; //表示数据线接收到下降沿
always @ (posedge clk or negedge rst_n) begin
if(!rst_n) begin
rs232_rx0 <= 1'b0;
rs232_rx1 <= 1'b0;
rs232_rx2 <= 1'b0;
rs232_rx3 <= 1'b0;
end
else begin
rs232_rx0 <= rs232_rx;
rs232_rx1 <= rs232_rx0;
rs232_rx2 <= rs232_rx1;
rs232_rx3 <= rs232_rx2;
end
end
//下面的下降沿检测可以滤掉<20ns-40ns的毛刺(包括高脉冲和低脉冲毛刺),
//这里就是用资源换稳定(前提是我们对时间要求不是那么苛刻,因为输入信号打了好几拍)
//(当然我们的有效低脉冲信号肯定是远远大于40ns的)
assign neg_rs232_rx = rs232_rx3 & rs232_rx2 & ~rs232_rx1 & ~rs232_rx0; //接收到下降沿后neg_rs232_rx置高一个时钟周期
//----------------------------------------------------------------
reg bps_start_r;
reg[3:0] num; //移位次数
reg rx_int; //接收数据中断信号,接收到数据期间始终为高电平
always @ (posedge clk or negedge rst_n)
if(!rst_n) begin
bps_start_r <= 1'bz;
rx_int <= 1'b0;
end
else if(neg_rs232_rx) begin //接收到串口接收线rs232_rx的下降沿标志信号
bps_start_r <= 1'b1; //启动串口准备数据接收
rx_int <= 1'b1; //接收数据中断信号使能
end
else if(num==4'd12) begin //接收完有用数据信息
bps_start_r <= 1'b0; //数据接收完毕,释放波特率启动信号
rx_int <= 1'b0; //接收数据中断信号关闭
end
assign bps_start = bps_start_r;
//----------------------------------------------------------------
reg[7:0] rx_data_r; //串口接收数据寄存器,保存直至下一个数据来到
//----------------------------------------------------------------
reg[7:0] rx_temp_data; //当前接收数据寄存器
always @ (posedge clk or negedge rst_n)
if(!rst_n) begin
rx_temp_data <= 8'd0;
num <= 4'd0;
rx_data_r <= 8'd0;
end
else if(rx_int) begin //接收数据处理
if(clk_bps) begin //读取并保存数据,接收数据为一个起始位,8bit数据,1或2个结束位
num <= num+1'b1;
case (num)
4'd1: rx_temp_data[0] <= rs232_rx; //锁存第0bit
4'd2: rx_temp_data[1] <= rs232_rx; //锁存第1bit
4'd3: rx_temp_data[2] <= rs232_rx; //锁存第2bit
4'd4: rx_temp_data[3] <= rs232_rx; //锁存第3bit
4'd5: rx_temp_data[4] <= rs232_rx; //锁存第4bit
4'd6: rx_temp_data[5] <= rs232_rx; //锁存第5bit
4'd7: rx_temp_data[6] <= rs232_rx; //锁存第6bit
4'd8: rx_temp_data[7] <= rs232_rx; //锁存第7bit
default: ;
endcase
end
else if(num == 4'd12) begin //我们的标准接收模式下只有1+8+1(2)=11bit的有效数据
num <= 4'd0; //接收到STOP位后结束,num清零
rx_data_r <= rx_temp_data; //把数据锁存到数据寄存器rx_data中
end
end
assign rx_data = rx_data_r;
endmodule
1~15行:为接口的定义说明,上文已作说明,不再赘述;
17~36行:为一个电平检测电路,通过4个寄存器rx_int0,rx_int1,rx_int2对rx_int的电平进行缓存,根据缓存的状态判断边沿。原理与上文相同,只是换了一个信号的输入口而已,这里不再赘述。
40~60行:以串口的起始位作为信号,开启波特率发生器和接收中断信号。与前文略有相同,可对比理解,不在赘述。
62~100行:此段代码为本模块的核心内容,其就是一个有限状态机。其结构大体与上文的发送代码极其相似,只有79~95有所不同,只对其进行分析。仔细看79行代码,与一般的状态机不同,不是从0开始的!分析串口时序图:
图4 三中不同脉冲触发方式时序图
虽说UART是异步传输,但是其还是有一定时钟节拍,只是没有通过信号输出来同步而已,在发送与接收双方都会产生一个时钟,作为基准。图4所示,比如发送端在上升沿进行数据的改变,在第一个时钟的上升沿产生起始信号,FPGA开始使能波特率发生器产生一定频率的脉冲,三种不同的脉冲产生形式对应如图4。只有在中值时产生的脉冲正巧对应着发送时钟的下降沿,且处于两个数据改变边沿正中央,是个绝佳的采样点。而第一个时钟是起始位、最后一个时钟是停止位,故在代码中num=0与num=9多没有对信号进行采样。但问题又来了,与前文相同90行代码,num=12时才结束接收,多等待了两个时钟,这段等待的时间在连续发送的多字节时,肯定会错过下一个字节的起始位和第0位数据。这就解释了上面代码更改了之后还是无法进行多字节的收发。用事实说话,做个小实验。
首先我们不改变接收代码只改变发送代码,采用16进制发送0x11,0x00,0x22,0x00, 0x33,0x00,0x44,0x00,0x55,0x00,0x66,0x00。如果笔者分析正确,驱动一定会丢失偶数位的代码(由于没有接收到起始位,不会启动接收),偶数位发送0x00是为了让1~7位的数据变化被驱动错认为是启动信号,当然0xff也是可行的,只有不出现下降沿,实验依然可行。
图5 测试图
测试结果与预期相同,进一步证明分析正确。修改55行和90行代码为
55 else if(num==4'd10) begin //接收完有用数据信息
90 else if(num == 4'd10) begin //我们的标准接收模式下只有
图6特权代码实现多字节传输实验图
发送字节数与接收字节数完全相同,没有丢码和误码现象。到此特权同学的代码分析与实现到此结束。在此感谢特权同学的无私分享!
2.2.2小梅哥教程实验小梅哥的代码是以特权同学的代码为基础,在上文的2个重点上进行了修改,并且在内容的一些细节上加以完善,代码可直接综合跑通,串口的字节接收并发送的实验。其代码的大体结构与框架并未进行大的改变,不予细读了。在小梅哥的教程中利用了强大的逻辑仿真软件modlesim对代码进行了测试,再进行板级调试。这是大多数教程中并没有涉及到的东西,当接触后才发觉,FPGA编程根本离不开逻辑仿真工具。详情请参阅《小梅哥和你一起深入学习FPGA之串口调试》。
2.2.3黑金教程黑金的教程在网上的一些FPGA教程中是比较系统的。其中《verilog那些事儿-驱动篇I》中实验12~15都是简述FPGA实现串口通信的教程。
实验十二 | |
实验十三 | 串口模块2--接收 |
实验十四 | 存储模块 |
实验十五 | FIFO存储模块(同步) |
实验十二 串口模块1--发送:
驱动代码主要分文两个模块,一个是串口的发送模块另一个是顶层模块。在黑金的文档中其建模将两个模块分为功能模块和控制模块,这样的结构分类可以让整个勾结更为清晰,相信在较为复杂的设计中会体现其优势(本人臆测尚未有待证实)。图7所示为功能模块RTL视图:
图7功能模块RTL视图
接口定义:
接口名称 | 位宽 | 描述 |
CLOCK | 1 | 工作时钟入口 |
RESET | 1 | 异步复位信号,低电平有效 |
iCall | 1 | 请求发送数据信号,可视为同步使能 |
iData[7:0] | 8 | 8位数据传输输入模块 |
TXD | 1 | 串口发送数据通信线 |
oDone | 1 | 发送结束信号 |
在了解接口定义后,可直接调用该模块进行串口数据的发送了,其控制流程为:iData输入待传输数据,而后将iCall信号线拉高,模块即进入发送模式,在oDone返回一个脉冲之后,表明发送结束,拉低iCall完成一次传输过程。
控制模块代码
module tx_demo
(
input CLOCK, RESET,
output TXD
);
wire DoneU1;
tx_funcmod U1
(
.CLOCK( CLOCK ),
.RESET( RESET ),
.TXD( TXD ),
.iCall( isTX ),
.oDone( DoneU1 ),
.iData( D1 )
);
reg [3:0]i;
reg [7:0]D1;
reg isTX;
always @ ( posedge CLOCK or negedge RESET )
if( !RESET )
begin
i <= 4'd0;
D1 <= 8'd0;
isTX <= 1'b0;
end
else
case( i )
0:
if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end
else begin isTX <= 1'b1; D1 <= 8'hA1; end
1:
if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end
else begin isTX <= 1'b1; D1 <= 8'hA2; end
2:
if( DoneU1 ) begin isTX <= 1'b0; i <= i + 1'b1; end
else begin isTX <= 1'b1; D1 <= 8'hA3; end
3: // Stop
i <= i;
endcase
endmodule
对比特权串口的代码,该模块类似于特权的顶层模块。两者有一定的区别,黑金的代码思想更接近于实践的控制,顶层同样采用一种状态机的控制逻辑完成实验目的,在实际的串口使用控制模式同样是可以使用这样的模式,只是更改状态机的逻辑而已,而特权同学的代码则更注重于测试目的。
22~46行:完成3个字节的发送,而后停止控制。
从控制模块的控制来看,逻辑简单就可完成串口驱动的控制,同样表明了给接口定义的科学性。
更能模块:
module tx_funcmod
(
input CLOCK, RESET,
output TXD,
input iCall,
output oDone,
input [7:0]iData
);
parameter B115K2 = 9'd434; // formula : ( 1/115200 )/( 1/50E+6 )
reg [3:0]i;
reg [8:0]C1;
reg [10:0]D1;
reg rTXD;
reg isDone;
always @( posedge CLOCK or negedge RESET )
if( !RESET )
begin
i <= 4'd0;
C1 <= 9'd0;
D1 <= 11'd0;
rTXD <= 1'b1;
isDone <= 1'b0;
end
else if( iCall )
case( i )
0:
begin D1 <= { 2'b11 , iData , 1'b0 }; i <= i + 1'b1; end
1,2,3,4,5,6,7,8,9,10,11:
if( C1 == B115K2 -1 ) begin C1 <= 8'd0; i <= i + 1'b1; end
else begin rTXD <= D1[i - 1]; C1 <= C1 + 1'b1; end
12:
begin isDone <= 1'b1; i <= i + 1'b1; end
13:
begin isDone <= 1'b0; i <= 4'd0; end
endcase
assign TXD = rTXD;
assign oDone = isDone;
endmodule
发送模块总过只需48行代码,而且一个所有逻辑融合在一个状态机中。
1~15行:接口定义,不再赘述。
11~46行:一个状态机完成了串口驱动的逻辑功能。状态机仅受控于一个同步使能信号iCall,同样也是控制模块的控制信号。
状态寄存器i=0:完成发送数据的锁存,发送的模式为1+8+2的11位数据,加上了校验位,但是校验位并没有使用,所有的校验位均只默认发送1,这样在发送端会降低发送速率,在多字节发送时有可能出现与特权同学相同的问题。
状态寄存器i=1~11:发送11位数据,采用计数方式让每个数据保持B115K2个时钟周期时间,该数值具体需更具波特率计算。与特权的代码思想上略有不同,前者采用的时序触发的思维模式,在时序图上可以清晰的看出逻辑,后者从电平的保持时间来作为切入点,从时序逻辑上看,特权的思维感觉更为容易理解。
状态寄存器i=12~13:完成一个结束指令输出。
分析完代码,采用逻辑仿真工具看一下时序逻辑,编写基础的testbench文件,由于发送模块的一些控制指令在,控制模块上已经给出了控制信号,在testbench中只需提供复位和时钟信号即可。
关于modelsim的操作,参阅《小梅哥和你一起深入学习FPGA之串口调试》解释的极为详尽!
testbench代码:
`timescale 1ns/1ns
module testbench;
reg clk;
reg rst_n;
wire TXD;
initial
begin
clk <= 1'b0;
rst_n <= 1'b0;
#200 rst_n <= 1'b1;
end
always #10 clk <= ~clk;
tx_demo tx_demo(
.CLOCK( clk ),
.RESET( rst_n ),
.TXD( TXD )
);
endmodule
该部分代码极为简单,只是在16行提供了一个时钟操作和11~12行进行了一个复位操作。需要说明的是,在modelsim中其实一个软件模拟方式,故在导入硬件描述语言的时候有可能会出现类似于c语言中报错没有定义的情况,需要调整寄存器的定义位置,比如在tx_demo中19、20行定义了D1和isTX但是在之前13、15行已经使用了这两个变量,在Quartus中编译综合都没有问题,但在modelsim中就可能会报错无法编译。需要将其定义移动到使用之前即可。
图8 modelsim未定义报错
图9 串口发送
图9红色方框中的每个11个周期时间,对应的TXD电平依次是11'b01000010111、11'b00100010111、11'b01100010111;与发送逻辑符合(下降沿起始信号+0xa1(0xa2、0xa3)+1(校验位)+上升沿停止位)。
逻辑仿真查看逻辑正常后,在进行板级测试,基本上若是在仿真器上的逻辑跑通了,板级测试不会有太大问题(低速应用)。图10显示,在FPGA上电后,发送了0xa1、0xa2、0xa3三个十六进制数给上位机,测试成功。
图10 板级测试图
实验十三 串口模块2--接收
此模块代码在黑金的文档中有详尽的解析,其中对串口接收中时钟跟踪的理解方法很是到位。详情参阅黑金verilog那些事儿-驱动篇I《驱动篇I-实验13》。
代码阅读之后,进行逻辑仿真更为直观的观察逻辑是否正确。由于要测试接收模块,所以除了必要的时钟激励和复位信号以外,还要有串口输入信号。在编写testbench之前,采用不可综合逻辑语言写出串口模块的模型用于测试,代码如下:
`timescale 1ns/1ns
module UART_module(
RX,
TX
);
input RX;
output TX;
reg TX;
reg [7:0]data_buf;
task transmission;
input [7:0]tdata;
begin
#8680 TX <= 1'b0;
#8680 TX <= tdata[0];
#8680 TX <= tdata[1];
#8680 TX <= tdata[2];
#8680 TX <= tdata[3];
#8680 TX <= tdata[4];
#8680 TX <= tdata[5];
#8680 TX <= tdata[6];
#8680 TX <= tdata[7];
#8680 TX <= 1'b1;
#8680 TX <= 1'b1;
end
endtask
task receive;
output [7:0]rdata;
begin
@( negedge RX ) #8680;
#4340 rdata[0] = RX;#4340;
#4340 rdata[1] = RX;#4340;
#4340 rdata[2] = RX;#4340;
#4340 rdata[3] = RX;#4340;
#4340 rdata[4] = RX;#4340;
#4340 rdata[5] = RX;#4340;
#4340 rdata[6] = RX;#4340;
#4340 rdata[7] = RX;#4340;
#8680;
#8680;
end
endtask
initial
begin
TX = 1'b1;
data_buf = 8'h55;
transmission( 8'h0f );
receive( data_buf );
transmission( 8'hf0 );
receive( data_buf );
transmission( 8'haa );
receive( data_buf );
transmission( 8'h55 );
receive( data_buf );
end
endmodule
对于可综合Verilog HDL而言,不可综合的Verilog HDL语言书写更加随意,并且可以采用顺序语言的思想进行书写。由于习惯于C语言的代码思想,在进行串口的建模时,采用的顺序思想进行编写。
11~26行:表示发送任务,每延时8680ns后输出信号电平;
28~42行:表示接收任务,31行代码表示等待RX下降沿的触发,而后便是依据115200的比特率进行采样信号。
在初始化语句中(45~47行),调用了任务并初始化了一些信号线,该部分的代码顺序执行。先发送8'h0f再等待接受,而后发送8'hf0等待接受1字节数据,再发送8'haa等待接收1字节数据,最后发送8'h55等待接收1字节数据。
顶层testbench例化待测试模块以及串口模块的模型:
`timescale 1ns/1ns
module testbench;
reg clk;
reg rst_n;
initial
begin
clk <= 1'b0;
rst_n <= 1'b0;
#10 rst_n <= 1'b1;
end
always #10 clk = ~clk;
rx_demo rx_demo(
.CLOCK( clk ),
.RESET( rst_n ),
.RXD( TX ),
.TXD( RX )
);
UART_module UART_module(
.RX( RX ),
.TX( TX )
);
endmodule
testbench提供时钟并例化模块,仿真逻辑图如图11:
图11 串口接收测试模型
有仿真结果可知,串口收发4组数据,逻辑并未出错。但是这种收发室单字节,每个传输字节之间有一个字节传输的等待时间,意思就是作为全双工通信的总线的串口只是工作在单工的模式。修改黑金的代码,看能否像特权的驱动一般实现在多字节传输。首先要修改合金的顶层例化文件,将发送模块与接收模块分开例化,使其可以完成同时工作状态,:
module rx_demo
(
input CLOCK, RESET,
input RXD,
output TXD
);
wire DoneU1 , DoneUT;
wire [7:0]DataU1;
reg [7:0]TData;
reg [4:0]i;
reg [8:0]C1;
reg [10:0]D1;
reg isRX , isTX;
/*------------------------------------------------------*/
rx_funcmod U1
(
.CLOCK( CLOCK ),
.RESET( RESET ),
.RXD( RXD ), // < top
.iCall( isRX ), // < core
.oDone( DoneU1 ), // > core
.oData( DataU1 ) // > core
);
/*------------------------------------------------------*/
tx_funcmod UT
(
.CLOCK( CLOCK ),
.RESET( RESET ),
.TXD( TXD ),
.iCall( isTX ),
.oDone( oDoneUT ),
.iData( TData )
);
/*------------------------------------------------------*/
parameter B115K2 = 9'd434, TXFUNC = 5'd16;
always @ ( posedge CLOCK or negedge RESET )
if( !RESET )
begin
i <= 5'd0;
C1 <= 9'd0;
D1 <= 11'd0;
isRX<= 1'b1;
end
else
case( i )
0 :
if( DoneU1 ) begin i <= i + 1'b1; TData <= DataU1; end
1 :
begin i <= i + 1'b1;isTX <= 1'b1;end
2 :
if( oDoneUT ) begin i <= 5'd0; isTX <= 1'b0;end
endcase
endmodule
26~34行:例化了串口发送驱动,使用实验十二 串口发送代码。
40~60行:修改了控制模块的状态机串口的接收使能控制isRX始终置高,只有有数据传入就可接受,在每次接收完一个数据后,启动发送指令发送一个数据。其基本的思想和特权代码的例化顶层大同小异,仿真测试。
修改串口模型:
`timescale 1ns/1ns
module UART_module(
RX,
TX
);
input RX;
output TX;
reg TX;
reg [7:0]data_buf;
task transmission;
input [7:0]tdata;
begin
#8680 TX <= 1'b0;
#8680 TX <= tdata[0];
#8680 TX <= tdata[1];
#8680 TX <= tdata[2];
#8680 TX <= tdata[3];
#8680 TX <= tdata[4];
#8680 TX <= tdata[5];
#8680 TX <= tdata[6];
#8680 TX <= tdata[7];
#8680 TX <= 1'b1;
#8680 TX <= 1'b1;
end
endtask
task receive;
output [7:0]rdata;
begin
@( negedge RX ) #8680;
#4340 rdata[0] = RX;#4340;
#4340 rdata[1] = RX;#4340;
#4340 rdata[2] = RX;#4340;
#4340 rdata[3] = RX;#4340;
#4340 rdata[4] = RX;#4340;
#4340 rdata[5] = RX;#4340;
#4340 rdata[6] = RX;#4340;
#4340 rdata[7] = RX;#4340;
#8680;
#8680;
end
endtask
initial
begin
TX = 1'b1;
data_buf = 8'h55;
transmission( 8'h0f );
transmission( 8'hf0 );
transmission( 8'haa );
transmission( 8'h55 );
end
initial
begin
receive( data_buf );
receive( data_buf );
receive( data_buf );
receive( data_buf );
end
endmodule
查看测试结果:
图12 黑金实验多字节收发逻辑图
分析黑金的代码与特权的代码的细微差别!
黑金文档中对其代码时序有这样一个时序图:
其根据时序的理解和逻辑操作十分严格,整个发送和读取均消耗了11个通讯时钟周期,而且在最后一个时钟操作和读取完成之后,还在继续计数延时,直至整个时序严格完整的执行完毕。读取和发送的速率是完全相同的,但在连续字节发送中中间没有充足的间隔时间让控制器反应启动数据的发送,导致部分数据没有发送数据,但其已经接收到数据。
图13 黑金连续多字节连续发送逻辑分析
而对于特权的代码为什么可以完成这样的实验呢?
在特权的代码中在最后一个数据的读取或者是发送完成之后,其并没有等待半个周期而是立即结束了发送和接收,这个半个发送周期对于50Mhz的时钟来说,有(波特率为115200)217个时钟脉冲,足以让其启动发送命令。
解决黑金的代码完成多字节传输方法:1、如黑金所说,添加缓冲器2、提高控制模块时钟3、修改驱动最后一步,提前结束时序传输。
实验十五 FIFO存储模块(同步)
该实验自己设计了一个FIFO模块,作为收发串口模块之间的缓冲器,可完成多字节的收发功能。
2.3开发环境本次设计采用Altera公司提供的FPGA开发环境QuartusII14.1,并安装modelsim-Altera作为FPGA开发过程中的逻辑仿真工具。
2.4设计思路2.4.1串口发送实验串口发送部分驱动代码,在功能上预设分为两部分:1.波特率发生器;2串口输出总线控制模块。
波特率发生器部分为一个同步使能脉冲发生器,在同步使能控制下产生一定频率的脉冲信号供串口输出总线进行数据的节拍。
串口输出总线控制模块采用一个有限状态机的的方式,状态转移图如图1
图14 状态转移图
图1所示为串口的发送状态机状态转移图,图中第二位到第七位的数据发送状态以省略号替代。
设定状态寄存器i,初值为0,则
i=0:检测启动信号是否为高电平,启动信号被置高,立即开始发送数据进入下个状态;
i=1:将波特率发生器同步使能信号拉高,进入下个状态;
i=2:等待波特率发生器的脉冲信号,以其脉冲为发送时钟发送起始位,进入下个状态;
i=3、4、5、6··· ···、10:等待波特率发生器的脉冲信号,以其脉冲为发送时钟发送数据0位、1位、2位、3位··· ···、7位,进入下个状态;
i=11:等待波特率发生器的脉冲信号,以其脉冲为发送时钟发送停止位,进入下个状态;
i=12:发送结束,拉高结束信号done并拉低波特率发生器同步使能信号停止产生波特率,进入下个状态;
i=13:拉低结束信号done,进入下个状态;
i=14:返回检测启动信号状态,进入i=0状态;
串口发送接口定义,如表1:
接口名称 | 位宽 | 功能说明 |
Clk | 1 | 提供串口工作时钟 |
Rst_n | 1 | 异步复位信号,低电平有效 |
Start | 1 | 发送数据启动信号,高电平有效 |
Sdata | 8 | 发送数据并行输入接口 |
Tx | 1 | 串口输出线 |
Done | 1 | 发送完成反馈信号线 |
串口接收驱动代码的编写,笔者认为思路上与串口的发送并不相同,即使两者使用着同一种通信协议。对于串口发送驱动,由于是发送数据方,发送时钟的节拍可根据协议十分清楚的产生,而对于串口数据的接收,驱动必须产生更为合适的时钟进行采样才能保证数据的稳定性。对于串口的接收驱动,编程采用完全被动模式,即驱动随时等候响应外部起始信号进行串口数据接收。
串口接收驱动,在功能上可分为两部分:1.波特率发生器;2串口接收模块。在波特率发生器部分与串口的发送机制相同,为一个简单的同步时钟脉冲发生器。
所说串口接收驱动的波特率发生器与发送驱动完全相同,但在状态机如何利用其脉冲节拍上却稍有不同,具体可由本文第三章进行细节描述。串口接收驱动主题思想依然是个有限状态机,状态转移图如图2所示:
图15 串口接收驱动状态转移图
设定状态寄存器i,初值为0,则
i=0:检测串口总线上的电平,检测到低电平立即开始数据的接收;
i=1:将波特率发生器同步使能信号拉高,进入下个状态;
i=2:与发送状态机不同的是,当波特率发生器被使能后的第一个时钟脉冲需要被跳过,检测到第一个时钟脉冲不做任何处理,进入下个状态;
i=3、4、5、6··· ···、10:等待波特率发生器的脉冲信号,接收RX总线上的数据0位、1位、2位、3位··· ···、7位存储到buffer各位中,进入下个状态;
i=11:等待波特率发生器的脉冲信号,缓存此时RX电平信号,进入下个状态;
i=12:校验停止位缓存是否为逻辑1,否则跳入i=0全部此次接收失败,校验正确则进入下个状态;
i=13:发送结束,拉高结束信号done并拉低波特率发生器同步使能信号停止产生波特率,进入下个状态;
i=14:拉低结束信号done,进入下个状态;
i=15:返回检测串口总线上的电平状态,进入i=0状态;
串口接收接口定义:
接口名称 | 位宽 | 功能说明 |
Clk | 1 | 提供串口工作时钟 |
Rst_n | 1 | 异步复位信号,低电平有效 |
Rdata | 8 | 接收数据并行输出接口 |
Rx | 1 | 串口输入线 |
Done | 1 | 接收完成反馈信号线 |
相较于串口的发送接口,串口的接收更为独立,完全有外部串口数据决定,只有一有数据就可以立刻响应,不需要接收FPGA端控制,接收完成后产生一个结束信号,提醒FPGA提取数据即可。
2.4.3串口收发(FIFO)实验本实验重点在如何控制FIFO存储器对串口收发驱动进行缓冲。整个实验的模块构架框图如图所示,串口驱动采用前两个实验驱动代码,FIFO调用QuartusII内部IP核FIFO存储器,采用状态机控制串口接收数据存储在FIFO中,核心模块只要FIFO中有数据就立刻取数据读出通过串口传输出去。
图16 模块构架框图
FIFO存储控制状态机:
图17 FIFO存储状态机
FIFO存储状态机功能简单,要串口接收到数据,立刻响应读取数据存入FIFO中。
核心模块状态机:
图18核心模块状态机状态转移图
3设计的实现3.1串口发送实验串口的发送驱动基本采用特权同学的代码思想,控制信号的外部建模方式采用黑金的建模方式。发送驱动代码:
module UART_send(
clk,
rst_n,
TX,
Sdata,
done,
Start
);
parameter clk_frq = 50_000_000,
UART_bsp = 115200;
input clk;
input rst_n;
input Start;
input [7:0]Sdata;
output done;
output TX;
reg UART_plus;
reg [31:0]cnt;
reg [3:0]i;
reg done;
reg TX;
reg enable;
reg [7:0]Sdatabuf;
//FSM
always@( posedge clk , negedge rst_n )begin
if( !rst_n )begin
i <= 4'd0;
done <= 1'b0;
TX <= 1'b1;
enable <= 1'b0;
Sdatabuf <= 8'h00;
end else
begin
case ( i )
4'd0 : if( Start ) begin i <= i + 1'b1;Sdatabuf <= Sdata;end
4'd1 : begin i <= i + 1'b1;enable <= 1'b1;end
4'd2 : if( UART_plus ) begin i <= i + 1'b1; TX <= 1'b0;end
4'd3 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[0];end
4'd4 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[1];end
4'd5 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[2];end
4'd6 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[3];end
4'd7 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[4];end
4'd8 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[5];end
4'd9 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[6];end
4'd10 : if( UART_plus ) begin i <= i + 1'b1; TX <= Sdatabuf[7];end
4'd11 : if( UART_plus ) begin i <= i + 1'b1; TX <= 1'b1;end
4'd12 : begin i <= i + 1'b1; done <= 1'b1; enable <= 1'b0;end
4'd13 : begin i <= i + 1'b1; done <= 1'b0;end
4'd14 : i <= 4'd0;
default :i <= 4'd0;
endcase
end
end
//generate clock
always@(posedge clk , negedge rst_n)begin
if( !rst_n )begin
UART_plus <= 1'b0;
cnt <= 32'd0;
end else
begin
if( enable )begin
if( cnt == clk_frq/UART_bsp) cnt <= 32'd0;
else if( cnt == clk_frq/UART_bsp/2) begin UART_plus <= 1'b1;cnt <= cnt + 1'd1;end
else begin cnt <= cnt + 1'd1; UART_plus <= 1'b0; end
end else
begin cnt <= 32'd0; UART_plus <= 1'b0;end
end
end
endmodule
外部控制模块,状态机编程:
module UARTop(
clk,
rst_n,
TX
);
input clk;
input rst_n;
output TX;
reg Start;
reg [7:0]Sdata;
UART_send UART_send(
.clk(clk),
.rst_n(rst_n),
.TX(TX),
.Sdata(Sdata),
.done(done),
.Start(Start)
);
//FSM
reg [4:0]i;
always@(posedge clk , negedge rst_n )begin
if( !rst_n )begin
Sdata <= 8'd0;
Start <= 1'b0;
i <= 5'd0;
end else
begin
case( i )
5'd0 : begin i <= i + 1'b1;Sdata <= 8'haa;end
5'd1 : begin i <= i + 1'b1;Start <= 1'b1;end
5'd2 : begin i <= i + 1'b1;Start <= 1'b0;end
5'd3 : if( done == 1'b1 )begin i <= i + 1'b1;end
5'd4 : begin i <= i + 1'b1;Sdata <= 8'hf0;end
5'd5 : begin i <= i + 1'b1;Start <= 1'b1;end
5'd6 : begin i <= i + 1'b1;Start <= 1'b0;end
5'd7 : if( done == 1'b1 )begin i <= i + 1'b1;end
5'd8 : begin i <= i + 1'b1;Sdata <= 8'h0f;end
5'd9 : begin i <= i + 1'b1;Start <= 1'b1;end
5'd10 : begin i <= i + 1'b1;Start <= 1'b0;end
5'd11 : if( done == 1'b1 )begin i <= i + 1'b1;end
5'd12 : begin i <= i + 1'b1;Sdata <= 8'h55;end
5'd13 : begin i <= i + 1'b1;Start <= 1'b1;end
5'd14 : begin i <= i + 1'b1;Start <= 1'b0;end
5'd15 : if( done == 1'b1 )begin i <= i + 1'b1;end
5'd16 : i <= 5'd16;
default :;
endcase
end
end
endmodule
其功能是发送连续8'haa、8'hf0、8'h0f、8'h55四个字节数据。而后状态机将滞留在i=16状态,整个工作停止。
testbench文件,只需提供时钟与复位信号:
`timescale 1ns/1ns
module testbench;
reg clk;
reg rst_n;
wire TX;
initial
begin
clk <= 1'b1;
rst_n <= 1'b0;
#200 rst_n <= 1'b1;
#400000 $stop;
end
always #10 clk <= ~clk;
UARTop UARTop(
.clk(clk),
.rst_n(rst_n),
.TX(TX)
);
endmodule
modelsim测试逻辑图:
图19 modelsim测试逻辑图
3.2串口接收实验串口接收设计备用完全独立试的设计,有考虑到串口的接收时异步模式,外部随时可能有信号传入,串口只要在非接收状态,都可以随时响应起始信号,开始接收数据,不受外部使能信号控制,接收完成反馈接收结束信号。
建模图:
图20 串口接收模型
module UARTreceive(
clk,
rst_n,
RX,
Rdata,
done
);
parameter clk_frq = 50_000_000,
bsp = 115200;
input clk;
input rst_n;
input RX;
output [7:0]Rdata;
output done;
reg [3:0]i;
reg Rclk_pulse;
reg [31:0]cnt;
reg [7:0]Rdatabuf;
reg stopbuf;
reg enable;
reg [7:0]Rdata;
reg done;
//FSM
always@(posedge clk , negedge rst_n)begin
if( !rst_n )begin
i <= 4'd0;
stopbuf <= 1'b0;
Rdatabuf <= 8'h00;
Rdata <= 8'h00;
enable <= 1'b0;
done <= 1'b0;
end else
begin
case( i )
4'd0 : if( RX == 1'b0 ) i <= i + 1'b1;
4'd1 : begin i <= i + 1'b1; enable <= 1'b1; end
4'd2 : if( Rclk_pulse ) i <= i + 1'b1;
4'd3 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[0] <= RX; end
4'd4 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[1] <= RX; end
4'd5 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[2] <= RX; end
4'd6 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[3] <= RX; end
4'd7 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[4] <= RX; end
4'd8 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[5] <= RX; end
4'd9 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[6] <= RX; end
4'd10 : if( Rclk_pulse ) begin i <= i + 1'b1; Rdatabuf[7] <= RX; end
4'd11 : if( Rclk_pulse ) begin i <= i + 1'b1; stopbuf <= RX;end
4'd12 : if( stopbuf == 1 ) begin i <= i + 1'b1;stopbuf <= 1'b0;end
else i <= 4'd0;
4'd13 : begin i <= i + 1'b1; Rdata <= Rdatabuf; enable <= 1'b0;end
4'd14 : begin i <= i + 1'b1; done <= 1'b1;end
4'd15 : begin i <= i + 1'b1; done <= 1'b0;end
default :i <= 4'd0;
endcase
end
end
//generate UART clock
always@( posedge clk , negedge rst_n )begin
if( !rst_n )begin
Rclk_pulse <= 1'b0;
cnt <= 32'd0;
end else
begin
if( enable )
if( cnt == clk_frq/bsp) cnt <= 32'd0;
else if( cnt == clk_frq/bsp/2) begin cnt <= cnt + 1'b1; Rclk_pulse <= 1'b1;end
else begin cnt <= cnt + 1'b1; Rclk_pulse <= 1'b0;end
else
cnt <= 32'd0;
end
end
endmodule
串口接收需要外部输入信号,该实验采用了上个实验验证正确的串口发送代码来作为串口接收的信号来源,也可编写串口模型测试。由于笔者测试过串口接收逻辑正确,直接进行顶层连线,完成串口的收发实验目的。
顶层控制模块:
module UARTTop(
clk,
rst_n,
RX,
TX
);
input clk;
input rst_n;
input RX;
output TX;
wire Rdone,Tdone;
wire [7:0]Rdata;
reg [7:0]Sdata;
reg Start;
UART_send UART_send(
.clk( clk ),
.rst_n( rst_n ),
.TX( TX ),
.Sdata( Sdata ),
.done( Tdone ),
.Start( Start )
);
UARTreceive UARTreceive(
.clk( clk ),
.rst_n( rst_n ),
.RX( RX ),
.Rdata( Rdata ),
.done( Rdone )
);
//FSM
reg [3:0]transfer_i;
always@(posedge clk , negedge rst_n )begin
if( !rst_n )begin
transfer_i <= 4'd0;
Start <= 1'd0;
Sdata <= 8'hf0;
end else
begin
case( transfer_i )
4'd0 : begin transfer_i <= transfer_i + 1'b1; Start <= 1'b1;end
4'd1 : begin transfer_i <= transfer_i + 1'b1; Start <= 1'b0;end
4'd2 : if( Tdone ) transfer_i <= transfer_i + 1'b1;
4'd3 : if( Rdone ) begin transfer_i <= transfer_i + 1'b1; Sdata <= Rdata;end
4'd4 : begin transfer_i <= transfer_i + 1'b1; Start <= 1'b1;end
4'd5 : begin transfer_i <= transfer_i + 1'b1; Start <= 1'b0;end
4'd6 : if( Tdone ) transfer_i <= transfer_i + 1'b1;
4'd7 : transfer_i <= 4'd3;
default :transfer_i <= 4'd3;
endcase
end
end
endmodule
3.3串口多字节接收实验(FIFO)
首先整合前两个实验的串口收发代码,例化为一个模块UART_driver:
图21 UART_driver RTL视图
端口说明:
端口名称 | 属性(位宽) | 描述 |
clk, | input(1) | 工作时钟 |
rst_n, | input(1) | 异步复位信号,低电平有效 |
TX_data, | input(8) | 传输数据8为并口输入 |
TX_request, | input(1) | 发送请求信号,电平启动发送 |
TX_done, | output(1) | 发送完成信号 |
TX, | output(1) | 串口输出端口 |
RX_data, | output(8) | 接收数据8为并口输出 |
RX_EN, | input(1) | 接收使能信号 |
RX_done, | output(1) | 接收结束信号 |
RX | input(1) | 串口接收端口 |
例化出串口模块之后,开始生成FIFO模块。调用QuartusII内部集成的FIFO存储器IP核:
图22 Quartus II FIFO IP核调用界面
IP核调用使用同源读写时钟,FIFO的位宽为8位栈深度为4个字节,添加一个异步清零端口,其余默认设置:
图23 FIFO设置界面
图24 FIFO设置界面
FIFO端口说明:
端口名称 | 属性(位宽) | 描述 |
aclr | input(1) | 异步清零位,高电平清楚FIFO内部数据 |
clock | input(1) | FIFO数据时钟,上升沿锁存端口信息 |
data | input(8) | 8为并口存储数据位,在wrreq=1时,时钟上升沿存储数据 |
rdreq | input(1) | 读取请求,高电平在时钟上升沿有效 |
wrreq | input(1) | 写入请求,高电平在时钟上升沿有效 |
empty | output(1) | FIFO空栈状态标志位 |
full | output(1) | FIFO满栈态标志位 |
q | output(8) | 读取数据8位数据输出端口 |
usedw | output(2) | FIFO内部数据数 |
调用官方IP核不需要了解其内部逻辑,直接进行功能测试,先观察各个接口功能时序功能,使用modelsim测试时序图:
图25 FIFO 逻辑功能测试
完成了FIFO的功能测试,编写顶层控制模块以及由收数据到FIFO的存储控制:
module UART_fifotop(
clk,
rst_n,
RX,
TX
);
input clk;
input rst_n;
input RX;
output TX;
//---------------------------------------------------------------
reg [7:0]WR_data;
reg WR_request;
wire [7:0]RD_data;
reg RD_request;
wire empty;
wire full;
wire [1:0]usedw_sig;
//---------------------------------------------------------------
reg [7:0]TX_data;
reg TX_request;
wire TX_done;
wire [7:0]RX_data;
reg RX_EN;
wire RX_done;
//---------------------------------------------------------------
reg [3:0]uart2fifo_i;
reg [4:0]i;
//---------------------------------------------------------------
//UART driver module
//---------------------------------------------------------------
UART_driver UART_driver(
.clk( clk ),
.rst_n( rst_n ),
.TX_data( TX_data ),
.TX_request( TX_request ),
.TX_done( TX_done ),
.TX( TX ),
.RX_data( RX_data ),
.RX_EN( RX_EN ),
.RX_done( RX_done ),
.RX( RX )
);
//---------------------------------------------------------------
//FIFO module
//---------------------------------------------------------------
fifo fifo_inst (
.aclr ( ~rst_n ),
.clock ( clk ),
.data ( WR_data ),
.rdreq ( RD_request ),
.wrreq ( WR_request ),
.empty ( empty ),
.full ( full ),
.q ( RD_data ),
.usedw ( usedw_sig )
);
//---------------------------------------------------------------
always@(negedge clk , negedge rst_n)begin
if(!rst_n)begin
uart2fifo_i <= 4'd0;
RX_EN <= 1'b1;
WR_data <= 8'h00;
WR_request <= 1'b0;
end else
begin
case( uart2fifo_i )
4'd0 : if( RX_done )begin uart2fifo_i <= uart2fifo_i + 1'b1;WR_data <= RX_data;end
4'd1 : if( full == 1'b0)begin uart2fifo_i <= uart2fifo_i + 1'b1;WR_request <= 1'b1;end
else uart2fifo_i <= 4'd1;
4'd2 : begin uart2fifo_i <= uart2fifo_i + 1'b1;WR_request <= 1'b0;end
4'd3 : uart2fifo_i <= 4'd0;
default :uart2fifo_i <= 4'd0;
endcase
end
end
always@(negedge clk , negedge rst_n)begin
if(!rst_n)begin
i <= 5'd0;
RD_request <= 1'b0;
TX_data <= 8'h00;
TX_request <= 1'b0;
end else
begin
case( i )
5'd0 : if( empty == 1'b0 )begin i <= i + 1'b1; RD_request <= 1'b1;end
5'd1 : begin i <= i + 1'b1; RD_request <= 1'b0;TX_data <= RD_data;end
5'd2 : begin i <= i + 1'b1; TX_request <= 1'b1;end
5'd3 : begin i <= i + 1'b1; TX_request <= 1'b0;end
5'd4 : if( TX_done )i <= 1'b0;
endcase
end
end
endmodule
在测试的时候调用在,黑金实验中编写的多字节发送的串口模型做为测试文件,在testbench中只提供了时钟信号,仿真逻辑图如下图26:
图26 串口多字节收发FIFO缓存逻辑仿真
4设计的验证4.1串口发送板级测试结果由于状态机发送4个数据后停止工作,故板级测试时,需按下复位键后,打开串口,松开复位键后观察串口助手,结果如图:
4.2串口接收板级测试结果
测试串口发送任何数据,FPGA接收后立刻返回原有数据,测试结果如图:
4.3串口多字节接收FIFO板级测试结果
5设计总结5.1思考问题
本次实验对FPGA的控制语言有了一定练习,同时也产生了许多疑惑:
1、在FPGA编写底层代码之后,在顶层调用时,采用状态机的方式,在时钟上许多多要工作在统一时钟的环境下,控制模块的时钟过快,有时的触发响应信号底层可能无法反应,过慢又有可能错过底层驱动的反馈信号。当在目前逻辑比较少的时候可能时钟控制还比较简单,都是用一个时钟也没什么致命的缺陷。但在以后的复杂设计中,这时钟的管理分析应如何解决?
2、写一些总线驱动的目的在于以后的方便使用,而这些驱动的控制接口有没有一种固定的格式或者规范?
5.2编程过程报错汇总<1>Error (10028): Can't resolve multiple constant drivers for net "Rdatabuf[7]" at UARTreceive.v(41)
一个变量无法在多个always块语句中进行操作
<2>BEGIN - END required around task/function statements
task与function语法中,功能语句必须为一个块语句(前后用begin ......end)
<3>Error (10663): Verilog HDL Port Connection error at UART_fifotop.v(31): output or inout port "TX" must be connected to a structural net expression输出必须连接wire变量,连接reg型报错。