linjpxt 发表于 2012-2-19 15:55:59

[原创] 用 FPGA 实现 UART

最近在学习FPGA和Verilog,简单的跑了一些范例之后,自己也开始写一点模块.
所以打了一块EP4C的测试板,板子回来后,就开始调试了,最先的点了同个灯,证明了FPGA,JTAG,晶振都正常了.
接着打算写一个串口,以便与电脑通信,方便下一步的调试.
在网上有挺多的关于UART的代码,但是很多都写的太复杂或是比较差,不清楚底细,所以觉得不如自己动手写一个.
在写完之后,感觉也多了一些收获,所以把设计过程分享出来,方便初学的朋友们.

linjpxt 发表于 2012-2-19 16:24:14

我用的是QII V11.0,由于没有自带仿真,所以只能使用MODELSIM,之前没用过,所以在网上找了很久也没完全搞明白,但是能出波形,也就不管它了,一边用一边学:)

首先建立工程,这个比较简单就不用多说了.
顶层是图形文件,然后下层用代码,感觉这样比较直观一点,当然也可以全用代码.
建立了myuart.v,之后就开始写代码了.按照单片机的UART,有发送缓冲寄存器,移位发送寄存器,移位接收寄存器,及接收缓冲寄存器,那就按这个模型来做.
产生是发送部分,感觉这块最好写,所以就从这个开始.

        always @ (negedge sclk)       // data tx
                begin
                        tx_dev_timer <= tx_dev_timer + 1;
                        if(tx_buffer_full && !tx_shift_full)
                        begin                                       
                                load_txb2shb <= 1;
                                tx_shift_buff <= {tx_buffer,{1'b0}};
                                tx_shift_cnt <= DAT_WIDE+2;                       
                        end
                        else
                        begin                       
                                load_txb2shb <= 0;
                                tx_shift_buff <= tx_shift_buff;        //LSB first
                                tx_shift_buff <= 1'b1;
                                if(tx_shift_full) tx_shift_cnt <= (tx_shift_cnt - 1);                               
                        end
                end

有一个移位记数器,用于计数所需移出去的位数,当计数器为0的时候,停止移位.
移位寄存器由于要多一个起始位和一个停止位(暂时按8位1位无校验来做),所以人为的插入了两个位.所以需要移出的总数要再加上两位.

linjpxt 发表于 2012-2-19 17:02:03

接下来考虑输入缓冲寄存器跟移位寄存器间的数据传递,这个做的时候费了比较周折,
当外部 nWR 下降沿来的时候,把数据存进 tx_buffer 并设置缓冲满标志,然后当移位发送寄存器空的时候,把数据取走,同时清除数据满的标志.
但是,nWR 跟移位时钟sclk它们之间没有任何关系,nWR可能是很快,也可能会很慢,所以它们可以看做是一个异步的时钟关系.
所以打算用一个异步复位寄存器来放置 tx_buffer 满的标志,
http://cache.amobbs.com/bbs_upload782111/files_52/ourdev_720122EI2BLJ.png

但会发现,移位寄存器为空的时候,nWR是不能设置标志的.所以这种做法是行不通的.
后面在网上搜了一下,修改后觉得改为下面这种结构是可行的
http://cache.amobbs.com/bbs_upload782111/files_52/ourdev_720176TV19T8.png

这种结构下,异步控制信号可以保存到下一下时钟周斯到来以后再清掉.

        always @ (posedge load_txb2shb or negedge ndat_wr)
        begin
                // Reset whenever the reset signal goes low, regardless of the clock
                if (load_txb2shb)
                begin
                        tx_buffer_full <= 1'b0;
                end
                // If not resetting, update the register output on the clock's falling edge
                else
                begin
                        tx_buffer <= udatin;
                        tx_buffer_full <= 1;
                end
        end

lcofjp 发表于 2012-2-19 17:05:38

顶楼主。

linjpxt 发表于 2012-2-19 17:31:53

接着就是实现接收部分了,类似的,接收部分也采用移位输入寄存器,跟移位计数器来计算移入的位数,
当移位寄存器空闲时,我们就期望从RDX上检测到一个低电平的起始位,然后开始计数.当计数达到最后一个位,也就是停止位时,检查该数据位上的内容,如果是高电平,就产生一个接收有效的信号,把移位寄存器的内容送到接收缓冲区保存.否则认为出错,扔掉该则数据.

但是由于RDX是一个异步数据,它的发送时钟跟我们的接收时钟的相位不是相同的,而且频率也可能存在一定的误差,如果我们启始位刚好采在靠近边缘上的,而有误差的频率会让我们后面的某几个位的采样出错.这就导致了我们采样有问题.

按照UART的时序我们知道有一个起始位,所以我们可以用这个从位来同步我们的接收时钟,现在的想法是,我们把接收时钟提高到指定频率的N倍,然后我们检测到RDX从高变低时,我们就开始计数,这样计N个时钟,刚好就是一位,而我们跟输入数据的最大不同步时间也只有 1/N 位.
那我们就弄一个N倍的时钟,然后一个最大为N的计数器,再有一个寄存器,记下我们同步时刻的时钟值,以后一到达这个数值,我们就认为一位的时间到了.

然后考虑采样的问题,我们可以在中间点采一次,也可以在中间点前后采三次,然后投票法表决一下,还有一个更好的想法是N个采样点采样值取平均,这样可以提高我们的抗干扰能力.决定采用最后一种办法,N次采样的值累计,如果结果达到或超过 N/2则认为输入是1,否则认为输入是0 减少了因为意外干扰产生的误码.

------------修改新增以下内容--------------------

另外,在启始位上,为了防止有扰动错误的引起一个数据帧的开始接收,所以加入了连续 N/2 次检测到低电平时,才认为一个起始位的存在,为什么不是连续检测 N次更可靠呢,因为如果存在时间频率误差,你可能永远都得不到一个N位的连续低电平,所以只检测了 N/2 次.

linjpxt 发表于 2012-2-19 17:41:06

接收缓冲寄存器,也是跟发送缓冲寄存器一样的想法,采用一个异步的寄存器来保存接收区满的信号,当nRD到来的时候,清除该标志.
下面上传完整的代码文件.

代码完全原创仅供各位学习,请不要要用于商业用途,如果用出版,转载,请联系本人.
verilog 代码ourdev_720165THZTSM.zip(文件大小:1K) (原文件名:myuart.zip)

linjpxt 发表于 2012-2-19 17:55:46

接下去就是一点关于modelsim 的仿真,在网上找了很多关于写modelsim的教程,但好像都不管用,只好手工来输命令
在菜单上有每一个命令,完在在下面的输出信息有你刚才输出的命令及参数,所以可以摘起来,存成一个 .do的文件,下次直接do调用这个文件就好了,也挺方便的.现在用到的只以这几条命令.
vsim rtl_work.my_uart//选择这个库进入仿真
view wave            //打开波型显示的窗口
add wave clk_x8      //加入波型的名字
add wave -hex udatin   //以16进制的方式显示这个波形
force -freeze clk_x8 1 0, 0 {12 ps} -r 25 //设置时钟的频率跟占空比,单位ps
run 210//先跑这么长的时间
force -freeze udatin 16#83 150 //强制这个输入为什么值
run 200         // 继续跑
....自己加啦

上传一下这个口的传真时序
<center>http://cache.amobbs.com/bbs_upload782111/files_52/ourdev_720168PT4CM5.png

启始位的细节图
<center>http://cache.amobbs.com/bbs_upload782111/files_52/ourdev_720170XZFC33.png

jielove2003 发表于 2012-2-19 18:00:00

MARK

linjpxt 发表于 2012-2-19 18:00:16

目前采用8倍的时钟,用115200 即 115200*8=0.92160MHz 时钟做为输入,通过FD232TR跟电脑通信,挺稳定的,如果需要用其它的波特率,可以在前面做一个分频器跟选择开关就好了.

llc123 发表于 2012-2-24 22:26:37

回复【楼主位】linjpxt
-----------------------------------------------------------------------

我也用CPLD写了一个UART接口,发现用TTL电平输入时,高低电平中间会出现严重的抖动,所以不能直接采用RX输入来做波特率时序的触发。需要做抖动处理或采用斯密特触发输入。

hymeng98 发表于 2012-2-29 12:36:25

收藏先!!
页: [1]
查看完整版本: [原创] 用 FPGA 实现 UART