注:工程代码见Github:多功能电子琴功能源代码
1.1 项目简介
本项目基于Digilent公司的Basys2开发板,利用verilog语言实现一个FPGA电子琴。该电子琴能够通过PS2接口外接键盘输入高、中、低3个音阶21个音符,在弹奏音乐时,在7端数码管上会输出相应音符符号,该电子琴还能自动播放存放在内部的音乐,同时还能通过VGA接口在LCD显示音阶和音符名称,此外,此电子琴还具有录音回放功能。
1.2 项目背景
本项目是华中科技大学2013年电子信息工程系《硬件课程设计》项目之一,基于Digilent公司的Basys2开发板,利用Basys2板的板载扩展接口,自行设计硬件模块连接图,通过verilog语言用FPGA来实现各硬件模块功能以及各模块的逻辑连接。
FPGA(Field-Programmable Gate Array),即现场可编程门阵列,它是在PAL、GAL、CPLD等可编程器件的基础上进一步发展的产物。它是作为专用集成电路(ASIC)领域中的一种半定制电路而出现的,既解决了定制电路的不足,又克服了原有可编程器件门电路数有限的缺点。
目前以硬件描述语言(Verilog 或 VHDL)所完成的电路设计,可以经过简单的综合和布局,快速地烧录至FPGA 上进行测试,是现代 IC 设计验证的技术主流。这些可编辑元件可以被用来实现一些基本的逻辑门电路(比如AND、OR、XOR、NOT)或者更复杂一些的组合功能比如解码器或数学方程式。在大多数的FPGA里面,这些可编辑的元件里也包含记忆元件例如触发器(Flip-flop)或者其他更加完整的记忆块。
目前,电子产品在我们的生活中扮演着越来越重要的作用,电子琴作为一个简单的电子产品,利用FPGA来实现一个简单的多功能电子琴,一方面可以让我们在硬件设计方面能力得到提高,也可以让我们看到硬件设计在我们生活中作用。通过这个多功能电子琴的设计,也让我对音乐有更多的了解。
1.3 开发环境
1.3.1 Basys2开发板
该开发板是围绕着一个Xilinx Spartan-3E FPGA芯片和一个Atmel AT90USB USB控制器搭建的,它提供了完整、随时可以使用的硬件平台,并且它适合于从基本逻辑器件到复杂控制器件的各种主机电路。Basys2板上集成了大量的I/O设备和FPGA所需的支持电路,让您能够构建无数的设计而不需要其他器件。
我们项目中基本用到Basys2基本所有的板载I/O资源,拨码开关、按钮、led、7端数码管基本板载I/O设备,利用PS2接口外接键盘,VGA接口外接液晶显示器,Pmod外接双通道功放音响。
1.3.2 ISE Design Suite14.2集成开放环境
ISE是使用XILINX的FPGA的必备的设计工具,它可以完成FPGA开发的全部流程,包括设计输入、仿真、综合、布局布线、生成BIT文件、配置以及在线调试等,功能非常强大。
由于我们没有烧写IP核,只用到ISE Design Suite14.2的基本功能。基本开发流程就是,设计总体模块连接图,定义好模块完成的功能和输入输出,引脚分配,在ISE中编辑verilog源文件,然后综合,布局布线,生成BIT文件,必要时进行仿真验证。
1.3.3 Adept烧写工具
Adept是Digilent公司为Basys2板子开发的开发板下载程序,通过USB下载线将ISE生成的BIT问价烧写到Basys2板上。
1. 高、中、低3个音阶21个音符通过PS2键盘输入弹奏音乐
2. 自动播放预存歌曲,预存曲目4首
3. 弹奏时七段数码管显示相应音阶和音符
4. 弹奏时VGA连接LCD显示相应音阶和音符,不弹奏时显示一幅钢琴吐
5. 录音和回放功能,提供时长8秒的录音时间
6. 双声道独立输出,可以边弹奏,边伴奏
7. 自行设计了音频输出电路,在面包板上自行通过元器件设计了一个简单的音响功放
我在项目小组中,主要负责PS2键盘输入模块和自动回放功能的实现,同时负责VGA显示的测试工作,协助整体系统负责人赵子平完成一些整体测试和细节的测试工作。
Ø 物理接口
Ø PS2协议一般性描述
PS/2鼠标和键盘履行一种双向同步串行协议。换句话说,每次数据线上发送一位数据并且每在时钟线上发一个脉冲就被读入。键盘/鼠标可以发送数据到主机,而主机也可以发送数据到设备,但主机总是在总线上有优先权,它可以在任何时候抑制来自于键盘/鼠标的通讯,只要把时钟拉低即可。 从键盘/鼠标发送到主机的数据在时钟信号的下降沿(当时钟从高变到低的时候)被读取;从主机发送到键盘/鼠标的数据在上升沿(当时钟从低变到高的时候)被读取。不管通讯的方向怎样,键盘/鼠标总是产生时钟信号。如果主机要发送数据,它必须首先告诉设备开始产生时钟信号(这个过程在后面中详细讲解)。最大的时钟频率是33kHz,而且大多数设备工作在10-20kHz。如果你要制作一个PS/2设备,我推荐你把频率控制在15kHz左右,这就意味着时钟应该是高40us低40us。 所有数据安排在字节中,每个字节为一帧,包含了11-12个位。这些位的含义如下:
● 1个起始位,总是为0
● 8个数据位,低位在前
● 1个校验位,奇校验
● 1个停止位,总是为1
● 1个应答位,仅在主机对设备的通讯中
当主机发送数据给键盘/鼠标时,设备回送一个握手信号来应答数据包已经收到。这个位不会出现在设备发送数据到主机的过程中
Ø 设备到主机的通讯过程
数据和时钟线都是集电极开路结构(正常保持高电平)。当键盘或鼠标等待发送数据时,它首先检查时钟以确认它是否是高电平。如果不是,那么是主机抑制了通讯,设备必须缓冲任何要发送的数据直到重新获得总线的控制权(键盘有16 字节的缓冲区,而鼠标的缓冲区仅存储最后一个要发送的数据包)。如果时钟线是高电平,设备就可以开始传送数据。 如在上一节提及的,键盘和鼠标使用一种每帧包含11位的串行协议。这些位含义是:
● 1个起始位,总是为0
● 8个数据位,低位在前
● 1个校验位,奇校验
● 1个停止位,总是为1
时钟频率为10-16.7kHz。从时钟脉冲的上升沿到一个数据转变的时间至少要有5us,数据变化到时钟脉冲的下降沿的时间至少要有5us并且不大于25us。这个定时必须严格遵循。主机可以在第11个时钟脉冲(停止位)之前把时钟线拉低,导致设备放弃发送当前字节(这是非常罕见的)。在停止位发送后,设备在发送下个包前至少应该等待50us。这将给主机一定时间处理接收到的字节(主机在收到每个包时,通常自动做这个),在处理字节这段时间内,主机应抑制其发送。在主机释放抑制后,设备至少应该在发送任何数据前等50us。
下面为键盘/鼠标发送一个字节给主机的完整过程:
1) 等待Clock线为高电平,即等待主机释放Clock线;
2) 延时50us;3) 判断Clock线是否为高电平?
No――跳到第1步;
4) Data线是否为高电平?
No――放弃(跳到从主机读取字节的程序中) 。
5) 延迟20us,输出起始位(0),然后延迟20us,再拉低Clock线保持40us后释放Clock线,形成一个脉冲;
6) 延时20us,测试Clock线是否为高电平?
No――跳到第1步;
7) 输出第1个数据位,然后延时20us,再拉低Clock线保持40us后释放Clock线,形成一个脉冲;
8) 重复6-7步发送剩下的7个数据位和校验位;
9) 延时20us,测试Clock线是否为高电平?
No――跳到第1步;
10) 输出停止位(1),然后延时30us,再拉低Clock线保持50us后释放Clock线,形成最后一个脉冲。
Ø 键盘扫描码
键盘的处理器花费很多的时间来扫描或监视按键矩阵。如果它发现有键被按下、释放或按住,键盘将发送“扫描码”的信息包到计算机。扫描码有两种不同的类型:“通码”和“断码”。当一个键被按下或按住就发送通码;当一个键被释放就发送断码。每个按键被分配了唯一的通码和断码,这样主机通过查找唯一的扫描码就可以测定是哪个按键。每个键一整套的通断码组成了“扫描码集”。现在有三套标准的扫描码集,分别是第一套、第二套和第三套,所有现代的键盘默认使用第二套扫描码,所以下面的介绍以第二套扫描码为主。
只要一个键被按下,这个键的通码就被发送到计算机。通码只表示键盘上的一个按键,它不表示印刷在按键上的那个字符。这就意味着在通码和ASCII码之间没有已定义好的关联,直到主机把扫描码翻译成一个字符或命令。
正如键按下通码就被发往计算机一样,只要键一释放,断码就会被发送。每个键都有它自己唯一的通码,它们也都有唯一的断码。多数第二套断码有两字节长,它们的第一个字节是F0H, 第二个字节是这个键的通码。
下表为部分键值的通码和断码:
Ø 模块简图
这个模块简图是有ISE View RTL Schematic自动生成的,截取其中的键盘模块图。
其中k_clock,k_data分别连接到PS2接口的时钟线和数据线,sys_clk为系统时钟,record和replay分别为录音和回放的开关,out为模块的输出值,输出值为按下键值的ascii码,供给上层或下层模块使用。
Ø 模块设计思想
根据上面的PS2协议的说明,我们可以知道在当按下键盘的时候,键盘会在时钟线k_clock的同步下通过k_data给主机发送这个键的通码,松下键是发送键盘的断码,我们的模块就是要根据时序将键值的通码提取出来,同时判断何时按键何时松开,所以还要根据收到了断码来判断按键松开,整个过程由系统时钟来驱动。
Ø 流程图
Ø 模块核心代码与说明
module keyboard
( sys_clk,
k_data,
k_clock,
record,
replay,
out
);
input sys_clk; //系统同步时钟
input k_data; //键盘输入数据
input k_clock; //键盘输入时钟
input record; //录音拨码开关
input replay; //回放拨码开关
output [7:0] out; //输出的键值ascii码值
wire [7:0] out;
wire [7:0] data_tongma; //扫描码输出, 通码
wire [7:0] data_duanma; //断码
reg [7:0] ps2_asci;
reg [11:0] tmp = 11'b000_0000_0000; //用来记录一帧信号
reg now_kbclk, pre_kbclk;
reg ZHJS; //扫描码转换结束信号,
reg started = 0;
reg [3:0] counter =4'd0; //一个随时间变化的计数器
wire enable;
wire [7:0] record_asci;
assign out = replay ? record_asci : ps2_asci; //根据拨码开关replay的状态选择输出
recordmode m1(sys_clk, record, replay, ps2_asci, record_asci); //recordmode模块实例化
always @(posedge sys_clk)
begin
pre_kbclk <= now_kbclk; //因为按键容易抖动,这里加个时钟判断就比较稳定
now_kbclk <= k_clock; //上升沿发送数据
if(pre_kbclk > now_kbclk)
begin //下降沿触发
tmp[counter] <= k_data;
//-----------------------------------------------------------------------
if(counter == 4'd10)
begin
ZHJS <= 1'b0; //finish
end
else
begin
ZHJS <= 1'b1; //not finish
end
if(counter == 4'd11)
begin
counter<=4'd1;
end
else
begin
counter <= counter +4'd1;
end
end
if(counter > 4'd1 && counter <4'd10 )
begin //转换中
started <= 1'b1;
end
else
begin
started <= 1'b0; //转换结束
end
end
assign enable = started; //started转换完成
assign data_tongma = enable ? 8'b0000_0000 : tmp[8:1]; //如果enable=0把tmp[8:1]
//赋给data-tongma
/****************************************************
用于判断有没有F0断码的出现,一遇到F0证明键已
松开,按键无效,则置keypressed-D=1(代表键松开)
通过一个锁存器DD,保存Pressed-Q,即断开状态
******************************************************/
reg keypressed_D =0;
wire keypressed_Q;
always @(data_tongma or keypressed_Q ) //only can use electricity level tri,else ERROR
begin
keypressed_D <= (data_tongma == 8'hF0)? 1'b1 : 1'b0;
end
assign data_duanma=(keypressed_Q==1'b0)? data_tongma :8'h00;
always @ (data_duanma)
begin
case (data_duanma)
8'h15: ps2_asci <= 8'h51; //Q
8'h1d: ps2_asci <= 8'h57; //W
8'h24: ps2_asci <= 8'h45; //E
8'h2d: ps2_asci <= 8'h52; //R
8'h2c: ps2_asci <= 8'h54; //T
8'h35: ps2_asci <= 8'h59; //Y
8'h3c: ps2_asci <= 8'h55; //U
8'h43: ps2_asci <= 8'h49; //I
8'h44: ps2_asci <= 8'h4f; //O
8'h4d: ps2_asci <= 8'h50; //P
8'h1c: ps2_asci <= 8'h41; //A
8'h1b: ps2_asci <= 8'h53; //S
8'h23: ps2_asci <= 8'h44; //D
8'h2b: ps2_asci <= 8'h46; //F
8'h34: ps2_asci <= 8'h47; //G
8'h33: ps2_asci <= 8'h48; //H
8'h3b: ps2_asci <= 8'h4a; //J
8'h42: ps2_asci <= 8'h4b; //K
8'h4b: ps2_asci <= 8'h4c; //L
8'h1a: ps2_asci <= 8'h5a; //Z
8'h22: ps2_asci <= 8'h58; //X
8'h21: ps2_asci <= 8'h43; //C
8'h2a: ps2_asci <= 8'h56; //V
8'h32: ps2_asci <= 8'h42; //B
8'h31: ps2_asci <= 8'h4e; //N
8'h3a: ps2_asci <= 8'h4d; //M
default: ps2_asci<=8'h00;
endcase
end
//***例化DD锁存器
DD DD_keypressed(
.DCLK(ZHJS), //ZHJS作为D锁存器的时钟
.D(keypressed_D),
.Q(keypressed_Q)
);
//********************************************************
endmodule
这部分功能是在我们基本完成所有功能后想到的一个功能,然后加入的,这个功能的实现方式虽然不是很优雅,但是好歹能够实现,之前写的比较优雅的方式总是出现各种问题,最终决定就用这个不是很优雅但是功能基本的代码模块。
Ø 模块简图
其中输入ps2_asci为keyboard的输出按键的ascii值,clk_5MHz为驱动时钟,record和replay分别为录音和回放的开关,输出record_asci为采样到的按键的ascii码值。
Ø 基本思想
当拨码开关SW2为“1”时,用16Hz的采样频率对keyborad模块的输出进行采样存入到一块内存区域中(在verilog中用简单的类“二维数组”output [7:0] ram []),然后通过拨码开关SW3来控制的上层模块piano的输入是keyboard,还是recordmode模块的输出(assign out = replay ? record_asci : ps2_asci)。输出也是通过这个16Hz同步信号来输出,采样和输出公用一个计数器count,这个count通过16Hz来同步,这不是一种优雅的实现,之前也尝试很多比较优雅的方式,但是解决不了count计数器的同步问题,具体细节参看代码。
Ø 模块核心代码和说明
由于这部分的实现较为简单,直接给出模块源码和注释,根据基本思想的阐释和代码注释可以清晰的说明问题。
module recordmode(clk_5MHz, record, replay, ps2_asci, record_asci);
input clk_5MHz; //输入驱动时钟信号
input record; //录音开关
input replay; //回放开关
input [7:0] ps2_asci; //输入的键盘按键的asciii码值
output reg [7:0] record_asci; //当replay为“1”时,输出的采样的键值
reg [25:0] count_for_16Hz; //分频计数器
reg clk_16Hz;
reg [7:0] mem [127:0]; //存储采样值的内存区域
reg [6:0] count; //存储计数器
//获得16Hz的采样频率
always@(posedge clk_5MHz)
begin
if(count_for_16Hz==156250)
begin
count_for_16Hz <= 26'd0;
clk_16Hz <= ~clk_16Hz;
end
else
begin
count_for_16Hz <= count_for_16Hz + 26'b1;
end
end
//当record为“1”时,将键盘输出的键值存入到mem内存区域中
always@(posedge clk_16Hz)
begin
if(record == 1'b1)
begin
count <= count + 7'b1;
mem[count] <= ps2_asci;
end
end
//将存储的键值输出
always@(count)
begin
record_asci <= mem[count];
end
endmodule
这部分我负责一部分测试工作,主要代码完成有组员赵子平完成,我主要学习了VGA协议标准,测试了一些将VGA模块单独测试的工作,比如显示分辨率更高的中文字等,虽然最后觉得没有必要,最终决定用音阶和音符来显示,这些测试工作还是让自己有很多收获的。
Ø VGA简介
VGA是IBM在1987年随PS/2机一起推出的一种视频传输标准,具有分辨率高、显示速率快、颜色丰富等优点,在彩色显示器领域得到了广泛的应用。目前VGA技术的应用还主要基亍VGA显示卡的计算机、笔记本等设备。 根据分辨率不同,VGA分为VGA(640x480)、SVGA(800x600)、XGA(1024x768)、SXGA(1280x1024)等。
Ø VGA接口和电气特性
VGA接口是一种D型接口(D-SUB),上面共有15针空,分成三排,每排五个。如下图所示,与之配套的底座则为孔型接口。
VGA引脚的定义为:
引脚1、2、3分别为红绿蓝三基色模拟电压,为0~0.714V peak-peak(峰-峰值),0V代表无色,0.714V代表满色。一些非标准显示器使用的是 1Vpp的满色电平。
HSYNC和VSYNC分别为行数据同步不帧数据同步,为TTL电平。
Ø VGA时序
VGA行数据时序:
VGA帧数据时序:
行数据时序,也就是显示一行数据的时序。从上图可以看出,显示一行数据需要处理两件事情。第一:产生行同步 HSYNC。不难看出,HSYNC是一个脉冲信号,此信号的周期为: e=a+b+c+d,低电平时间为 a。其中 a、b、c、d均为时间信号,这些信号根据需要显示的分辨率不同而不同。第二:产生显示的数据(DATA)信号, 此信号为模拟信号,当在显示有效数据(Active video)内,DATA信号为0~0.714Vpp的模拟电压(R、G、B),根据分辨率的不同,DATA的采样率、点数也皆不相同。
帧数据时序不行时序类似,也就是显示一屏数据的时序。只是这里的基本单位为每行数据,而行数据里面的最基本单位为每个点。
不同的分辨率,时序上的时间是不一样的,下表列出常用分辨率及时间参数。
Ø 模块设计思想
根据上面对VGA的介绍可以看到,最主要的就是根据选择的分辨率来产生有效的行同步信号HSYNC和帧同步信号VSYNC,我们采用的是640*480的分辨率,屏幕刷新率为60Hz的显示模式。在产生正确的同步信号的前提后,就是我们来显示出合适的图像出来,考虑到如果采用太高的分辨率后,会产生大量不是太必要的代码,特别是要显示一幅分辨率很高的图片,我们经过讨论后决定显示简单的音阶和音符在LCD上,这样既可以满足要求,而且会节约很多FPGA资源,所以我们将640*480的屏幕分为24*16个块,然后再网上找到PCtoLCD这个软件生成字模,然后根据按键值决定将那些字显示在LCD上。
注:这部分其他阐释见团队报告或赵子平报告,这部分我只负责了测试工作,最终代码的完成由赵子平负责,所以关于实现的部分我就不赘述了。
我们花了大约10天时间完全投入到硬件课设中,每天都会记录我们的奋斗历程和阶段性成果,还在完成之后,拍了一些暂时视频素材,最终将这些素材交给李娇进行剪辑合成,最终做成了一个视频,上传到网上,具体细节参见团队报告中的说明,这里就不重复说明了。
这里仅仅展示一张自己和最终作品的合影吧:
虽然以前做过硬件的项目,不过那个主要是基于STM32平台来进行开发,我负责的主要是软件部分,主要涉及uC/OS部分多任务和GPRS/GSM模块的相关部分。这次硬件课设是个完全不一样的开发环境,利用FPGA来开发电子琴这个简单电子产品,刚开始时,心里还真没底,借鉴了前人的一些经验后,心里终于有点信心了,虽然开发平台不一样,开发环境也不一样,但只要肯付出努力,肯定能完成,最终我们也终于完成,对于我们组所有人都挺不容易的,感谢队友们一直在这10天内基本都在电工基地一起奋斗。由于我要在7月4日要到深圳实习,队友也很支持我在7月2日前完成的目标,最终在大家的配合和努力下如期完成目标,谢谢可爱的队友了。
说说自己的在硬件上面的收获吧,通过这个硬件课设,还是学到了很多关于硬件的知识的。虽然verilog来写程序很像写软件,但还是有很大区别的,在写verilog程序时,心里一定要有基本的硬件连接图,否则很容易写出逻辑正确却无法生成BIT文件,我开始时一直没认识到这点,后来经历了很多次程序报错,自己才认识到在写verilog程序时心里要有硬件电路,至少要有基本的硬件概念在心中,这样才能写出逻辑正确,在硬件的角度来看也正确的程序,自己心里有这个概念后,每次写verilog时心里总会想这样写从硬件的角度来看是否正确,出错的几率小多了。
通过verilog程序的编写,自己也认识到时序的重要性,可以说,没有时钟的驱动,整个数字电路就基本无法正确的工作,特别是各个硬件模块的逻辑连接与通信,写verilog还有一点让我感到很不错的就是,它可以再硬件上实现任务的并行性,任务间的通信也通过时钟的同步也比较实现,这比在通用CPU上来实现类似的功能轻松多了。
自己在编写PS2键盘模块,测试VGA模块时,也学习到了PS2键盘接口协议、VGA接口协议的相关知识,也具备通过协议的文档,用verilog语言来实现接口的通信功能。这些知识以前有一些接触,以前学习Linux驱动时接触过一些接口协议,那时觉得很难,没怎么认真研究,通过课设,现在再去看就会觉得轻松多了,有信心能搞定。我觉得硬件课设不一定是要做出很牛的东西,只要自己学习到了感兴趣的知识,能够在自己以后的学习中发挥作用就很不错,觉得此次课设还是让我在硬件有更清晰的人,可以让我有能力去研究Linux的设备驱动这个更有挑战性的工作。
硬件课设也让自己在查找资料方面的能力加强,现在是一个信息爆炸的时代,如何在良莠不齐的信息中准确寻找对自己有用的信息真的是很重要的能力,每次做课设,都会提高自己在相关领域的信息检索能力,为以后做好准备,觉得这些才是会给以后带来积极作用的收获。
最后感谢指导老师闵玉堂老师提供开发平台Basys2开发板以及有用的建议,还有最后验收时对我们的鼓励。虽然我们的作品没有创意,没能达到老师心目中评优资格的标准,但学习到有用的知识和老师对我们的作品的肯定也挺让自己高兴的。也期待自己以后在硬件上能有更多的深入研究。