FPGA-05-Verilog语法
本文基于模块化设计,记录PWM变频无源蜂鸣器驱动设计、UART串口回环设计思路与代码实现,引入worken与flag标识的模块设计思路,涉及function查找表、task任务语法。在附录内解释亚稳态与异步单bit输入下打两拍的目的。
蜂鸣器驱动
蜂鸣器、喇叭可做设备音频输出设备,最简单的为无源蜂鸣器,体积小,驱动电平低,点式发声。
驱动原理:
- 声调:输入频率呈现不同声调
- 音量:PWM占空比决定音量
功能需求
使用ZYNQ开发板驱动无源蜂鸣器进行七个基本音调“哆来咪发梭拉西”的循环鸣叫,依靠按键切换下一音阶。频率音调对应表如下:
| 音调 | do | re | mi | fa | so | la | si |
|---|---|---|---|---|---|---|---|
| 频率 | 262 | 294 | 330 | 349 | 392 | 440 | 494 |
详细设计
由按键检测模块负责滤波,状态机负责切换状态,送至Beep控制器输出不同频率PWM波。
蜂鸣器控制器模块图
按键检测和状态机不再赘述,对控制器的波形进行简单绘制。
蜂鸣器控制器波形示意图
将占空比 (音量),频率(音调)通过两个按键设定为可调值。其中,频率参数、占空比参数由前级FSM输入,在蜂鸣器控制块内计算高电平时长。两个关键点:
- 频率输入需要转换为时钟拍数,使用function函数从查找表获取固定值。若使用除法,将耗费大量资源
- 占空比调节基于频率对应拍数,利用乘除法计算出高电平时间。因此,freq变化时需要缓存2拍,第1拍防止亚稳态并且刷新freq_cnt,第2拍计算新的high _cnt。而duty变化时仅需要打1拍(没有其他依赖)。
代码实现
以下时蜂鸣器控制代码,输出不同频率占空比的PWM波。实际上duty_change并不需要打2拍。具体参数可由FSM给入。
详细代码:点击查看
1 | module Beep_Ctrl( |
仿真结果
默认状态 freq = 100Hz, duty = 25%
freq = 100Hz, duty = 50%
freq = 50Hz, duty = 50%
状态切换整体仿真
IP核调用
IP即知识产权,定义为“用于ASIC或FPGA中预先设计好的电路功能模块”。
IP核包括:
- 软核(HDL语言形式):为HDL源文件,不涉及电路元件。通常为加密形式。
- 固核(网表形式):完成了综合的功能块,对时序要求严格的功能块预先分配特定布线资源。
- 硬核(版图形式):经过完全布局布线的网表式,一些FPGA的内置ARM核为硬核。
IP又包括免费、付费IP,无论如何,通常都不知道内部实现。
PLL
PLL全称Phase Locked Loop,实现对时钟信号的分频、倍频、相位调整,占空比调整。在此简述PLL电路结构 ,形式是一个反馈结构:
- D(Divider):分频器
- PFD(Phase & Frequency Detector):鉴频鉴相器,比较频率相位并输出误差值
- CP(Charge Pump):电荷泵
- LF(Loop Filter):环路滤波器,控制噪声带宽,实现平滑滤波
- VCO(Voltage-Controlled Oscillator):压控振荡器,电压越大输出频率越大
PLL结构框图(Xilinx手册UG4772)
通过在Vivado左侧IP Catalog选项卡搜索需要的IP核,例如下方Clock IP,可配置选项:
- 类型:MCMM、PLL
- 时钟特性:频率综合、相位对齐、最小化功耗等
- 输入信息:主副时钟(名称、频率可改)
- 输出时钟:可改名称、频率、占空比、Locked信号(标志稳定输出)等
PLL配置UI
待所有信号配置完之后,OK->generate生成.xci尾缀IP核文件,也可在工程中导入xci调用其他已有IP核。
IP Source中选中XX.veo文件可查看调用接口,使用过程同模块实例化,过直接看最终仿真输出。
4路PLL仿真
Locked信号
ROM
ROM,Read-Only Memory。在嵌入式开发中较为常见,特性就是只能读出,且不会因为电源关闭消失。Vivado中使用.coe文件存数据,该文件可使用Matlab生成。
- Vivado自带的IP核分为单端双端RAM/ROM,可自动对齐32bit地址
Vivado ROM核 opt1
- 配置A口B口位宽,使能脚。其中BitB x DepthB=BitA x DepthA。Load File即从已知.coe文件存储数据,数据不足时,输出File Remain …可填充为固定值
Vivado ROM核 opt2
Vivado ROM核 opt3
Vivado ROM核 opt4
ROM核仿真结果
ROM核仿真结果:未初始化数据填充为0xFF
RAM
RAM核,可读写存储RAM。Vivado中RAM与ROM均使用内部RAM资源,但RAM保留读/写端口,ROM仅读端口。RAM类型包括:
- 单端:共用地址线,读写不能同时进行
- 简单双端:读端口+写端口各1个
- 真双端:读端口+写端口各2个
单端口RAM opt1
- operating mode:操作模式,读/写优先
- write first:同时读写,读出的为新写入的数据
- read:同时读写,读出的为上一时刻的数据
- no change:写入数据时,输出保持写操作前上一时刻(读取时)的输出值
RAM核配置也可加载.coe文件,由于可读写,所以可不加。
简单双端口RAM中,A口为write,B口为read,读写分离。
真双端RAM,A口可同时配置宽度和深度,B口配置位宽(自动计算深度)。
真双端RAM opt1
对单端口RAM IP核进行仿真,使用wr_flag切换读写操作。
单端RAM sim1
单端RAM sim2(write
first模式,数据写入后立即反映到输出)
单端RAM sim3(write first模式,第2次读RAM)
当切换模式为no change时,结果如下:
单端RAM sim3(no change模式,第2次读RAM)
FIFO
FIFO,First In First Out,先进先出,在数据结构一文中也有介绍。
FIFO主要用作缓存,应用在同步时钟系统、异步时钟系统中,比如数据跨时钟域转换,带宽同步等。根据读写时钟是否相同分为
- SCFIFO:同步FIFO,应用在同步时钟系统
- DCFIFO:异步FIFO,应用在异步时钟系统
IP核配置界面与RAM、ROM类似,配置位宽、复位、输出延迟…,可选择是同步时钟块RAM还是异步时钟块RAM。
以下是SCFIFO的仿真结果,pi_flag写请求拉高后pi_data自增,写入新数据,当pi_flag拉高且FIFO数据写满时,full信号立即拉高。同理,当rd_en使能时读出最后一个信号的同时,empty立即拉高。
SCFIFO sim1(fifo写满)
SCFIFO sim2(fifo读空)
以下是DCFIFO的仿真结果,其中读时钟25MHz,16bit位宽;写时钟50MHz,8bit位宽。写入深度256、读深度128,但实际深度分别为255、127,即写入会比读出多8bit(255-127*2)。
DCFIFO cfg1(实际深度与配置差异)
从sim1结果看,当按8bit写满再按16bit读空时将遗留1字节数据。分析如下:
- 【sim2】按8bit写,写入第255个数据(数值0-254)的同时刻full输出拉高,wr_data_cnt延迟1拍wr时钟输出计数值255;其中第255个值为0xfe,第256个值0xff未被写入fifo。rd_data_cnt输出延迟4个rd节拍,当第254个,值0xfd被写入时,rd_data_cnt即达到上限127
- 随后rd_en拉高,按16b读,rd_data_cnt延迟一拍rd时钟减一。
- 【sim3】当rd_data_cnt = 1时,数据已经被读空(延时1个rd时钟后清0),此时fifo内的最后8bit数据0xfe未被读出。再向FIFO新写数据时,将追加在0xfe值后,所以新一轮的值读出是0xfe00。
DCFIFO sim1(循环1次吞1字节)
DCFIFO sim2(写满后full拉高)
DCFIFO sim3(读空后empty拉高)
注意:输出的数据高位在前,低位在后,字节序可能影响设计结果。需要注意读出写入带宽匹配,防止出现FIFO空或满的情况。
从使用角度看,DCFIFO类似双端口ROM,只是ROM可以对指定地址读写,而DCFIFO只能先进先出,多出写入/读出剩余计数输出。
RS232串口驱动
功能需求
设计UART收发模块,并完成loopback回环测试。
详细设计
- 串口发送端:输入为8bit数据,输出串行信号
- 串口接收端:输出为串行信号,输出为8bit数据
串口模块框图
输出一个有效标志信号,辅助后级模块或系统在使用该并行数据时确定该时刻采样的数据有效且稳定。
考虑到信号传输波动,需要设置多个采样点取大值作为采样结果: Clk_Cnt = (1/Baudate)/(1/Clk_Freq) 从而得出单次采样的时钟数: Sample_Clk_Cnt = Clk_Cnt/Samplerate 假设通信波特率为9600,采样次数为32,时钟为50MHz,则可得单个bit需要5208个Clk,单次采样需要162个Clk。
由于RX信号为FPGA异步信号,因此必须对其延后2拍防止亚稳态。
接收模块
设计一
- 检测到下降沿之后拉高使能信号work_en
- 检测到单个bit所在时钟的中间位置,输出sample_flag,此时获取当前bit值,bit_cnt自加
- 采集8个bit后,拉高rx_flag ,输出rx_data数据、有效信号rx_done
串口接收模块波形
此方案的波形依靠work_en使能信号,随后根据波特率计算得出的BPS_TO_CNT进行位计数,按照UART传输层约定,10Byte为一帧,分别在每个bit中间位置采样。
代码实现
代码文件详见:点击查看
此处新增task语法,把固定行为的代码封装为task,只需传入参数。
相对于function,似乎更像C函数
1 | initial begin |
仿真结果
串口接收仿真波形
以上设计似乎有两个问题:
- 为什么要延迟两拍产生下降沿?
- 如果传输环境差,产生干扰怎么处理?
对于问题1,可见异步信号处理。而问题2,则可对采样过程做一些优化,见设计二。
设计二
如何优化?
采用小梅哥教程的设计思路,多次采样取均值,尽量减少干扰影响。对于单个bit的采样,增加采样周期与数值计数器。如下图,可根据采样结果判断有效值 2< 6/2,认为是低电平。
单个bit采样
代码实现
源文件uart_rx_plus.v的主要变动如下:
- 采样信号sample_flag:基于bit_clk_cnt
- 新增采样计数sample_cnt:确定单一bit内何时采样
- 位计数bit_cnt:参照bit_clk_cnt确定bit何时结束
- 新增采样值累计sample_level:确定当前bit均值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45/* ----------------- 采样信号 -------------------*/
//reg sample_flag;
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
sample_flag <= 1'b0;
else if((work_en == 1'b1) && ((bit_clk_cnt%SAMPLE_TO_CNT) == 1'b0)) /* 单次采样 */
sample_flag <= 1'b1;
else
sample_flag <= 1'b0;
/* ----------------- 采样计数 -------------------*/
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
sample_cnt <= 4'd0;
else if(work_en == 1'b0)
sample_cnt <= 4'd0;
else if(bit_clk_cnt == BPS_TO_CNT - 1'b1)
sample_cnt <= 4'd0;
else if(sample_flag == 1'b1)
sample_cnt <= sample_cnt + 1'b1;
else
sample_cnt <= sample_cnt;
/* ----------------- 采样值累计 -------------------*/
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
sample_level <= 5'd0;
else if(work_en == 1'b0)
sample_level <= 5'd0;
else if(bit_clk_cnt == BPS_TO_CNT - 1'b1)
sample_level <= 5'd0;
else if(sample_flag == 1'b1)
sample_level <= sample_level + rxd_d3;
else
sample_level <= sample_level;
/* ----------------- 位值采集 -------------------*/
always@(posedge sys_clk or negedge sys_rst_n)
if(!sys_rst_n)
rx_data <= 8'd0;
else if((bit_cnt < 4'd9 && bit_cnt > 4'd0 )&&(bit_clk_cnt == BPS_TO_CNT - 1'b1))
rx_data <= {sample_level >= (SAM_PER_BIT/2)?1:0 ,rx_data[7:1]};
else if(rx_done == 1'b1)
rx_data <= 8'd0;
else
rx_data <= rx_data;
仿真结果
串口接收仿真波形
发送模块
详细设计
从接口上看,在已知波特率的情况下,只需要在已知tx_en后,间隔指定拍数发送单个bit。
- 输入为使能脉冲,因此使用work_en指向发送状态
- 默认电平拉高,可由接收端检测停止位。
串口接收仿真波形
代码实现
代码文件详见:点击查看
仿真结果
串口接收仿真波形
串口回环
建立顶层文件,并仿真和实测。顶层文件调用上述接收模块和发送模块代码,样例如下:
1 | module uart_loop_top( |
仿真结果如下:
串口回环仿真波形
上板验证正常,如下:
串口回环输出
异常现象
上板异常
现象:仿真时未发现异常,故采用排针引出脚TTL电平直连USB转串口,实测输出异常,当输入多个数据时,仅输出00,后续字节丢失。
逐步排查问题:
- 物理层:更换TTL为DB9串口线–:heavy_multiplication_x:
- 软件层:调用外部串口接收模块–:white_check_mark:
因此定位问题为串口接收模块Bug,仿真如下:
上板异常后的仿真
从上述波形看,几乎没有任何问题。接收到指定数据,即可回送发出。
不稳定的初始状态
再往下,发现rxd输入时延迟3拍的初始状态d1、d2、d3设置为0,按照UART协议,空闲态应为1。但仿真未出错,d2=0,d3=1时正常输出下降沿,程序上板后仅能输出首字节0x00,无论实际是何值。
需要注意,将延迟3拍初值修改为1后,仿真正常,上板正常。但考虑“初始值”并不影响下降沿检测,其并不是根本原因,推测是时序上出现某种巧合。
继续追查,接收模块的work_en信号在单字节接收完成后未置位,其复位条件为:
1 | //... |
上述判断逻辑:当第9bit停止位采样到第8次时,work_en复位。sample_flag置位逻辑如下:
1 | else if((work_en == 1'b1) && ((bit_clk_cnt%SAMPLE_TO_CNT) == 1'b0)) /* 单次采样 */ |
预期上,当时钟计数器bit_clk_cnt为单次采样所需时钟的整数倍时,置位。 但是,bit_clk_cnt复位条件为计满5207(9600波特率1bit所需时钟周期)时复位,导致sample_cnt == SAM_PER_BIT后不再有下一次sample_flag【预期是bit_clk_cnt = 5208】,进而无法复位。
work_en未复位
将判断条件修改后仿真和上板均正常,代价是最后一位打不满5207时钟(也可调整,但需要改动其他代码):
1 | else if((sample_cnt == SAM_PER_BIT) && (bit_cnt == 4'd9)) |
work_en正常复位
至此,问题解决。原因是:波特率对应的时钟计数复位的时机 先于 work_en使能信号复位所用的sample_flag 1拍,导致最后一位采样后不会遇到sample_flag。
仿真异常
此处仿真异常指:模块间信号rx_done传输至tx_en出现为未定值X,导致发送模块无法正常工作。当前已无法复现。
怀疑为Vivado软件异常,导致..\UART.runs\synth_1路径下文件无法更新,出现异常。
附录
导出.coe文件
.coe文件可配置ROM内初始数据,输出指定初始信号。
以下为产生8bitx256深度.coe文件的matlab代码:
1 | clc; |
单bit异步信号处理
触发器通过电容存储电荷来保存数据,而时钟沿是控制电荷进出开关的过程。由于开关过程(充放电)非瞬时,因此划定了 tsu 和 th作为电荷稳定的‘窗口期’。
一旦在此窗口期内扰动电荷(数据变化),就会导致电压处于中间态(亚稳态)。亚稳态状态不会持续,但恢复时间和最终逻辑值不可控,取决于具体的工艺、电压、温度(PVT)以及进入亚稳态的‘深度’,因此无法在设计时被保证,可能导致意料外的结果。
如下图,前半部分tsu和th之外数据稳定时,触发器正常锁存。当数据未稳定时,经过振荡时间tmet后出现非预期值。
亚稳态示意图
亚稳态波形
一个触发器的输出本该在时钟沿之后固定的时间(Tco)内稳定下来,但如果它处于亚稳态,这个时间就会变得不可控。这就导致下一级触发器可能无法满足自身的建立时间或保持时间,从而将不确定性沿着数据路径传播下去,最终导致整个系统的状态机进入一个未定义的非法状态。如下图。
tmet1过长时可能影响reg2的输出
对于实际的FPGA异步信号输入,如果仅打一拍就将其输入后续组合逻辑电路,仍有很大概率引入一连串的亚稳态。在经过连续两拍之后,会减小出现输入组合电路的信号为亚稳态的概率,但结果不一定为预期值。
此处参考:跨时钟域同步,为什么两级寄存器结构能够降低亚稳态? - 知乎
亚稳态的几种状况
a)中,异步data持续2T;FF1 亚稳态,第2 cycle采样到1,FF2第3 cycle sample到正确值。
b)中,异步data持续2T;FF1 亚稳态时,第2、3 cycle采样到正确值,FF2第2 cycle sample到正确值。
c)中,异步data马上结束,FF1 亚稳态时,FF2在第2 cycle sample到了正确值。
d)中,异步data马上结束,FF1 亚稳态时,FF2在第3 cycle sample到了正确值。
打两拍的目的是,尽可能防止单bit信号亚稳态传递。数据正确性不是重点。
————————— End —————————
生如不定流水,夕时难觅朝花。