注意点:如果输入信号在最终没有输出,verilog是不会各这个信号分配资源的。
学习编程就是一个从学会初步,到自己时间发现预期效果和真实情况相背,再到重新学习,再做出来的过程,
由于某些原因,我要开始学习FPGA了。之前一直是接触的单片机,积累了一定的编程知识,以及模电数电知识。现在想要挑战编程FPGA.首先第一个映入眼帘的是verilog这门语言,不能说这门语言和C语言毫不相关,但也可以说是大相径庭。虽然看上去,语法都是差不多的。但是C语言编程的思想完全不能用在verilog这门语言上。C语言面向的是过程,而verilog面向的是硬件。里面有好多东西,以C语言的逻辑是不能理解的。虽然都有模块化的思想,但是相差还是有点多的。
我觉得是这样,想要学习一门技术、语言。那必然是有利可图,也就是有一定的原因、动力去学习。关于这个动力不归我管,自己发现去。比如我的动力就是要完成一个项目,得到的奖励。有了动力,也就有了目的。明确了目的也就明白了,要学什么。
知道了目的,就把目标列出来。列一张思维到图就行。
顺便再把大体的框架列出来
好了,现在需要学习的东西已经列好了。现在要干什么?学习,学什么?怎么用模块吗?不是。
首先,需要了解verilog的语法。俗话说的好,磨刀不误砍柴功。要是练verilog语法都不会,看verilog的例程就像英语考试不会英语单词一样,无法想象直接。
我花了几天的时间来学习verilog语法。也总算是有自信能看懂带有注释的例程。接下来,学习各个相应的模块,先从按键学起。先把简单的学会。
我把串口放在第一个,是因为串口时必不可少的交互工具。开发了串口功能,也就开发了一种人机交互的方法,为以后开发各种模块提供了便利。
接下来加入按键。按键是否成功,用串口发送。
可以通过按下按键,来改变电平的高低。硬件中常常通过电平变化来执行语句。在看了一圈按键的例程后,我的感觉是,既然可以用电平的变化来执行语句,那为什么不用按键线上的变化来执行语句呢?网上有的说法是要消抖,所以依靠时钟来变化的。嗯,好像有点道理。但是,这不是硬件编程语言嘛,所以要贴合硬件。想想硬件上,按键时怎么消抖的。并联一个电容。额,好像在逻辑电路上有点问题。那想想消抖是为了什么?为了防止按键被多次按下,那就是用时钟延时了嘛,延时的话,果然还是要用到时序逻辑电路。下面是小脚丫官网的一个例程,里面的消抖方法感觉还挺常见的。
// ********************************************************************
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// ********************************************************************
// File name : debounce.v
// Module name : debounce
// Author : STEP
// Description :
// Web : www.stepfpga.com
//
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date: |Changes Made:
// V1.0 |2017/03/02 |Initial ver
// --------------------------------------------------------------------
// Module Function:按键消抖
module debounce (clk,rst,key,key_pulse);
parameter N = 1; //要消除的按键的数量
input clk;
input rst;
input [N-1:0] key; //输入的按键
output [N-1:0] key_pulse; //按键动作产生的脉冲
reg [N-1:0] key_rst_pre; //定义一个寄存器型变量存储上一个触发时的按键值
reg [N-1:0] key_rst; //定义一个寄存器变量储存储当前时刻触发的按键值
wire [N-1:0] key_edge; //检测到按键由高到低变化是产生一个高脉冲
//利用非阻塞赋值特点,将两个时钟触发时按键状态存储在两个寄存器变量中
always @(posedge clk or negedge rst)
begin
if (!rst) begin
key_rst <= {N{1'b1}}; //初始化时给key_rst赋值全为1,{}中表示N个1
key_rst_pre <= {N{1'b1}};
end
else begin
key_rst <= key; //第一个时钟上升沿触发之后key的值赋给key_rst,同时key_rst的值赋给key_rst_pre
key_rst_pre <= key_rst; //非阻塞赋值。相当于经过两个时钟触发,key_rst存储的是当前时刻key的值,key_rst_pre存储的是前一个时钟的key的值
end
end
assign key_edge = key_rst_pre & (~key_rst);//脉冲边沿检测。当key检测到下降沿时,key_edge产生一个时钟周期的高电平
reg [17:0] cnt; //产生延时所用的计数器,系统时钟12MHz,要延时20ms左右时间,至少需要18位计数器
//产生20ms延时,当检测到key_edge有效是计数器清零开始计数
always @(posedge clk or negedge rst)
begin
if(!rst)
cnt <= 18'h0;
else if(key_edge)
cnt <= 18'h0;
else
cnt <= cnt + 1'h1;
end
reg [N-1:0] key_sec_pre; //延时后检测电平寄存器变量
reg [N-1:0] key_sec;
//延时后检测key,如果按键状态变低产生一个时钟的高脉冲。如果按键状态是高的话说明按键无效
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec <= {N{1'b1}};
else if (cnt==18'h3ffff)
key_sec <= key;
end
always @(posedge clk or negedge rst)
begin
if (!rst)
key_sec_pre <= {N{1'b1}};
else
key_sec_pre <= key_sec;
end
assign key_pulse = key_sec_pre & (~key_sec);
endmodule
之前在stm32哪个项目上用过了,是无源蜂鸣器。只需要产生一定频率的PWM波,就可以使他响。最简单的方法就是用计数器将时钟分频,然后映射到管脚就行了。这个和单片机上的差不多。
电子森林给了现成的代码。每个接口的作用也都标清楚了。
// --------------------------------------------------------------------
// >>>>>>>>>>>>>>>>>>>>>>>>> COPYRIGHT NOTICE <<<<<<<<<<<<<<<<<<<<<<<<<
// --------------------------------------------------------------------
// Module: Beeper
//
// Author: Step
//
// Description: Beeper
//
// Web: www.stepfapga.com
//
// --------------------------------------------------------------------
// Code Revision History :
// --------------------------------------------------------------------
// Version: |Mod. Date: |Changes Made:
// V1.0 |2016/04/20 |Initial ver
// --------------------------------------------------------------------
module Beeper
(
input clk_in, //系统时钟
input rst_n_in, //系统复位,低有效
input tone_en, //蜂鸣器使能信号
input [4:0] tone, //蜂鸣器音节控制
output reg piano_out //蜂鸣器控制输出
);
/*
无源蜂鸣器可以发出不同的音节,与蜂鸣器震动的频率(等于蜂鸣器控制信号的频率)相关,
为了让蜂鸣器控制信号产生不同的频率,我们使用计数器计数(分频)实现,不同的音节控制对应不同的计数终值(分频系数)
计数器根据计数终值计数并分频,产生蜂鸣器控制信号
*/
reg [15:0] time_end;
//根据不同的音节控制,选择对应的计数终值(分频系数)
//低音1的频率为261.6Hz,蜂鸣器控制信号周期应为12MHz/261.6Hz = 45871.5,
//因为本设计中蜂鸣器控制信号是按计数器周期翻转的,所以几种终值 = 45871.5/2 = 22936
//需要计数22936个,计数范围为0 ~ (22936-1),所以time_end = 22935
always@(tone) begin
case(tone)
5'd1: time_end = 16'd22935; //L1,
5'd2: time_end = 16'd20428; //L2,
5'd3: time_end = 16'd18203; //L3,
5'd4: time_end = 16'd17181; //L4,
5'd5: time_end = 16'd15305; //L5,
5'd6: time_end = 16'd13635; //L6,
5'd7: time_end = 16'd12147; //L7,
5'd8: time_end = 16'd11464; //M1,
5'd9: time_end = 16'd10215; //M2,
5'd10: time_end = 16'd9100; //M3,
5'd11: time_end = 16'd8589; //M4,
5'd12: time_end = 16'd7652; //M5,
5'd13: time_end = 16'd6817; //M6,
5'd14: time_end = 16'd6073; //M7,
5'd15: time_end = 16'd5740; //H1,
5'd16: time_end = 16'd5107; //H2,
5'd17: time_end = 16'd4549; //H3,
5'd18: time_end = 16'd4294; //H4,
5'd19: time_end = 16'd3825; //H5,
5'd20: time_end = 16'd3408; //H6,
5'd21: time_end = 16'd3036; //H7,
default:time_end = 16'd65535;
endcase
end
reg [17:0] time_cnt;
//当蜂鸣器使能时,计数器按照计数终值(分频系数)计数
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
time_cnt <= 1'b0;
end else if(!tone_en) begin
time_cnt <= 1'b0;
end else if(time_cnt>=time_end) begin
time_cnt <= 1'b0;
end else begin
time_cnt <= time_cnt + 1'b1;
end
end
//根据计数器的周期,翻转蜂鸣器控制信号
always@(posedge clk_in or negedge rst_n_in) begin
if(!rst_n_in) begin
piano_out <= 1'b0;
end else if(time_cnt==time_end) begin
piano_out <= ~piano_out; //蜂鸣器控制输出翻转,两次翻转为1Hz
end else begin
piano_out <= piano_out;
end
end
endmodule
现在用的时钟没有弄错的话就是12MHz的时钟频率。那就简单了,想要产生1Hz的波就需要对他经行12*106分频。
由图可得需要24位的计数器,计数到一半,也就是6 * 106-1的时候转为低电平。计数到12 * 106 - 1时转为高电平。记住初始化的时候默认为高电平。
现在需要考虑,具体时间问题。考虑到时间之间的自然进位不相互影响。我打算使用三个寄存器,分别记录小时,分钟和秒钟。方便起见,我使用三个6位的寄存器。在时间转换中,我使用组合逻辑。
DS18B20之前就有听说过,是一款很流行的温度传感器,但是说到具体使用,倒是没怎么使用过。
现在直接找视频学习一下.
需要读取的温度在RAM中
数据格式为补码,低11位的二进制数转化为十进制数后再乘以0.0625得到所测的实际温度值。
写数据的方法。
读时序
读取操作
需要注意的是,18B20是以微妙为单位操作的。所以,我直接给他一微秒的时间。
由于需要按照时序读写DS18B20,所以直接使用状态机读写。状态直接按上图写就行。
读出来的格式为16位的格式,只有后十二位有效,高五位均为同一个值。低十二位的数据格式为补码格式。所以用代码转化。
always @(posedge clk_1us or negedge rst_n) begin
if(!rst_n) begin
sign <= 1'b0;
data1 <= 11'b0;
end
else if(org_data[15] == 1'b0) begin
sign <= 1'b0;
data1 <= org_data[10:0];
end
else if(org_data[15] == 1'b1) begin
sign <= 1'b1;
data1 <= ~org_data[10:0] + 1'b1;
end
end
取到后十一位的真实数据,并且输出符号位。
SPI采样有四种模式。CPOL控制时钟空闲状态电平,CPHA控制时钟在奇偶沿采样。
在前面各个模块都验证完毕后,总算要上手OLED了,之前在网上搜OLED的教程是在是少的可怜,最多也就是LCD的教程。
所以这个OLED的程序我打算自己写。在之前的学习中我累计的经验告诉我。单片机里面的时序,可以由状态机解决。所以我打算使用状态机对OLED进行读写。
像这样的OLED的初始化操作:
在这里插入代码片`
/******************************************************************************
* @file OLED init
* @author zero Team
* @version V1.0
* @date 13-March-2019
* @brief 初始化OLED显示屏幕
******************************************************************************/
void OLED_Init(void)
{
delay_ms(500);//初始化之前的延时 important
//初始化指令 由厂商给定
OLED_Write_cmd(0xAE);//display off
OLED_Write_cmd(0xD5);//set memory addressing Mode
OLED_Write_cmd(0X80);//分频因子
OLED_Write_cmd(0xA8);//设置驱动路数
OLED_Write_cmd(0x1F);//默认0X3f(1/64) 0x1f(1/32)
OLED_Write_cmd(0xD3); //设置显示偏移
OLED_Write_cmd(0x00);//默认值00
OLED_Write_cmd(0x40);//设置开始行 【5:0】,行数
OLED_Write_cmd(0x8D);//电荷泵设置
OLED_Write_cmd(0x14);//bit2,开启/关闭
OLED_Write_cmd(0x20);//设置内存地址模式
OLED_Write_cmd(0x02);//[[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
OLED_Write_cmd(0xA1);//段重定义设置,bit0:0,0->0;1,0->127;
OLED_Write_cmd(0xC8);//设置COM扫描方向
OLED_Write_cmd(0xDA);//设置COM硬件引脚配置
OLED_Write_cmd(0x02);//0.91英寸128*32分辨率
OLED_Write_cmd(0x81);//对比度设置
OLED_Write_cmd(0x8f);//1~255(亮度设置,越大越亮)
OLED_Write_cmd(0xD9);//设置预充电周期
OLED_Write_cmd(0xf1);//[3:0],PHASE 1;[7:4],PHASE 2;
OLED_Write_cmd(0xDB);//设置VCOMH 电压倍率
OLED_Write_cmd(0x40);//[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
OLED_Write_cmd(0xA4);//全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
OLED_Write_cmd(0xA6);//设置显示方式;bit0:1,反相显示;0,正常显示
OLED_Write_cmd(0x2E);//停用滚动条
OLED_Write_cmd(0xAF);//开启显示
delay_ms(100);//延时一段时间
OLED_Clear();//清除显示屏幕,防止屏幕中存在噪点
}`
只需要在状态机的对应位置写入
5'd1 : OLED_data = 8'hae; //关闭显示
5'd2 : OLED_data = 8'hd5; //设置时钟分频因子,震荡频率
5'd3 : OLED_data = 8'h80; //[3:0],分频因子;[7:4],震荡频率
5'd4 : OLED_data = 8'ha8; //设置驱动路数
5'd5 : OLED_data = 8'h3f; //默认0X3f(1/64) 0x1f(1/32)
5'd6 : OLED_data = 8'hd3; //设置显示偏移
5'd7 : OLED_data = 8'h00; //默认为0
5'd8 : OLED_data = 8'h40; //设置显示开始行 [5:0],行数.
5'd9 : OLED_data = 8'h8d; //电荷泵设置
5'd10: OLED_data = 8'h14; //bit2,开启/关闭
5'd11: OLED_data = 8'h20; //设置内存地址模式
5'd12: OLED_data = 8'h02; //[1:0],00,列地址模式;01,行地址模式;10,页地址模式;默认10;
5'd13: OLED_data = 8'ha1; //段重定义设置,bit0:0,0->0;1,0->127;
5'd14: OLED_data = 8'hc0; //设置COM扫描方向
5'd15: OLED_data = 8'hda; //设置COM硬件引脚配置
5'd16: OLED_data = 8'h12; //0.91英寸128*32分辨率
5'd17: OLED_data = 8'h81; //对比度设置
5'd18: OLED_data = 8'hef; //1~255(亮度设置,越大越亮)
5'd19: OLED_data = 8'hd9; //设置预充电周期
5'd20: OLED_data = 8'hf1; //[3:0],PHASE 1;[7:4],PHASE 2;
5'd21: OLED_data = 8'hdb; //设置VCOMH 电压倍率
5'd22: OLED_data = 8'h30; //[6:4] 000,0.65*vcc;001,0.77*vcc;011,0.83*vcc;
5'd23: OLED_data = 8'ha4; //全局显示开启;bit0:1,开启;0,关闭;(白屏/黑屏)
5'd24: OLED_data = 8'ha6; //设置显示方式;bit0:1,反相显示;0,正常显示
5'd25: OLED_data = 8'haf; //开启显示
在经历了四天的奋斗后,总算是把OLED点亮了。(久违的兴奋)就像是经历了一场持久战。从什么都不会,到把底层SPI输出代码代码搭建完成,单方面测试成功,到解决综合输出没有波形,到波形输出成功,输出内容不正确,到波形无输出问题,OLED无法点亮,到最后OLED终于点亮。这一路的艰辛通过言语是无法分享的,只有通过亲身实践,去踩到误区。才能体会。由于这四天(2.1-2.4)我基本上是全天调试内容,没有怎么记录过程。所以在这里分享一些我认为重要的东西,对小白来说一定有用。
复位一定要做!!!
在设计每一个模块的时候复位一定要加上,具体复位要做的内容,根据模块实际的效果来决定。比如状态机的状态转换模块。那就是复位到一个固定的地址。在always调试模块中也要加入复位的下降沿条件。例程如下
always@(posedge w_Master_TX_Ready or negedge rst_n)begin
if(~rst_n)
begin
OLED_SPI_STATE = 1;
end
else
。。。
如果是学习单片机过来的朋友,可以重视一下状态机和verilog中的TASK和function的功能.
其中,function和task的功能和单片机中C语言的功能差不多。可能比C语言差一点,但是总比没有强啊。而状态机则是像汇编里面的规则一样,重视地址的变化,在做完一个任务后,就能获取到下一个的地址。如果是这么理解的话,编程起来思路可能会更清晰一点。
verilog是并行语句
由于我接触单片机比较多,所以编程思维一半都是串行,也就是一句一句写。FPGA中的状态机也是这个调性。但是FPGA的特点就是各个模块并行运行,所以,要用并行的思维去对其编程,才能达到最好的效果,反之,就有可能发生很多意想不到的bug。
OLED的引脚注意点
正常的通讯线路需要接上:MOSI、CLK、D/C这三条先就不多说了。CS需要下拉表示芯片选中。RST引脚需要有一个上升沿,才能点亮。也就是说,需要先下拉,然后再上拉才行。
如果使用task、function之类的内容,需要注意输入信号的位数。
在显示温度数值时使用了BCD码.由于十进制转BCD码需要用到除法,所以占用的资源较多。到后期如果资源不够的话可以改方法,当前使用除余法是最方便的。
现在已经能够成功在oled上动态显示数字,就是温度显示并不是非常准确。现在打算直接把温度数据发送出去,给上位机,燃梦后上位机处理完成,然后发送回来。
首先使将温度信号传送到上位机进行解码。看看温度信息是否正确,然后再发送回FPGA。
传统艺能,上位机修改时间,格式如下:
74 11 30 30
头、时、分、秒
音乐模块
用python写的上位机。格式如下调整数字就能调整音调。空格表示暂停。
sound = "6 6 7 7 8 8 7 7 6 6 3 3 1 1 . . 5 5 4 4 3 3 4 5 4 0 4 4 5 5 6 6 7 7 5 5 2 2 4 4 3 3 2 3 4 3" #声音音调
块使其发声。
首先使将温度信号传送到上位机进行解码。看看温度信息是否正确,然后再发送回FPGA。
传统艺能,上位机修改时间,格式如下:
74 11 30 30
头、时、分、秒
音乐模块
用python写的上位机。格式如下调整数字就能调整音调。空格表示暂停。
sound = "6 6 7 7 8 8 7 7 6 6 3 3 1 1 . . 5 5 4 4 3 3 4 5 4 0 4 4 5 5 6 6 7 7 5 5 2 2 4 4 3 3 2 3 4 3" #声音音调