南京邮电大学电子电路课程设计可编程音乐自动演奏电路

电工电子实验报告

设计题目: 可编程音乐自动演奏电路
课程名称: 电子电路课程设计

学 院: 通信与信息工程学院
班 级: B200124
学 号: B20012418
姓 名: 张宏宇
指导教师:
开课时间: 2022 年第 09 月13日
至2022年09月23日
目录
摘要 6
1.课程目的 6
2.实现方法 6
3.实现情况 6
关键词 6
一、课题技术指标 8
1.乐曲要求: 8
2.演奏要求 8
3.电气指标 8
4.选作指标 9
二、系统设计 9
1.模块化设计 9
4.顶层模块设计 10
5.程序框图 11
三、源程序设计 11
1.按键消抖模块设计 11
(1)为什么要按键消抖 11
(2)按键消抖的方法 12
(3)按键消抖代码 12
2.状态机模块 13
(1)状态机是什么 13
(2)为什么要用状态机 14
(3)状态机的设计方法 14
(4)状态机的代码设计 14
3.点灯模块 16
(1)发光二极管的工作原理 16
(2)点灯模块代码设计 16
4.单个音符时间存储ROM模块 17
(1)时间ROM IP核中需要存储什么数据 17
5.频率计数值存储模块 18
(1)频率输出是怎么实现的 18
(2)存储频率计数值的模块代码设计 18
6.单个音符频率存储ROM模块 20
(1)频率ROM IP核中需要存储什么数据 20
7.时间计数和频率计数模块 20
(1)时间计数模块代码实现 20
(2)频率计数模块代码实现 21
8.PWM方波产生模块 22
(1)PWM原理 22
(2)PWM产生模块代码实现 22
9.音乐选择模块 23
(1)读ROM地址选择 23
(2)读ROM地址代码实现 23
四、单元电路、整体电路功能测试 25
1.按键消抖仿真测试 25
(1)按键消抖仿真测试模块代码 25
(2)仿真波形图 27
2.状态机仿真测试 27
(1)状态机仿真测试模块代码 27
(2)仿真波形图 29
3.点灯模块 29
(1)LED灯仿真测试模块代码 29
(2)仿真波形图 30
4.音乐播放模块 31
(1)音乐播放仿真测试代码 31
(2)仿真波形图 32
五、喇叭功放电路及振荡电路 33
1.喇叭功放电路的搭建 33
(1)LM386芯片引脚配置和功能图 33
(2)LM386应用电路 33
(3)注意事项 34
2.振荡电路 35
(1)SN74HC132芯片引脚配置及功能图 35
(2)振荡电路电路图 36
(3)振荡电路波形 36
六、输出正弦波形(附加指标) 37
1.为什么要输出正弦波 37
2.正弦波的妙用 37
3.产生正弦波的代码实现 37
(1)使用MATLAB产生波表 37
(2)FPGA对波表操作 39
(3)DDS模块 40
(4)PWM产生模块 41
(5)输出正弦波仿真 42
七、问题与解决 42
八、元件清单 43
九、参考文献 43
十、实验小结及心得体会 43
十一、附录 45
1.最终电路图 45
2.课程设计日志 46

摘要
1.课程目的
① 巩固和深化前期电子电路所学知识。
② 掌握综合性和系统性电子电路设计的原则和方法。
③ 进一步掌握电子电路的装配、调测技术。
④ 培养科研、工程应用能力,自学、查找资料能力。
⑤ 进一步提高科技论文的撰写和文档整理能力。
⑥ 培养学生的创新意识和创新能力。
2.实现方法
使用ISE软件通过Verilog HDL语言编辑程序,使用Xilinx公司的Spartan - 3AN系列的XC30S50AN – 4TQG144I硬件下载程序,同时在面包板上搭建外围电路实现。
3.实现情况
使用按键控制选择预先设置在电路中的乐曲,选中某一乐曲后对应的发光二极管亮,音乐演奏电路反复自动演奏所选的乐曲,经功率放大后由喇叭播出,直至选中下一首位置,系统时钟通过使用有源晶振震荡而出。
关键词
Verilog、现场可编辑逻辑门阵列FPGA、音乐播放、音阶频率、芯片使用。

Summary
• 1. Purpose of the course
① Consolidate and deepen the knowledge of early electronic circuit.
② Master the principles and methods of comprehensive and systematic electronic circuit design.
(3) further grasp the assembly and commissioning technology of electronic circuits.
④ Cultivate the ability of scientific research and engineering application, self-study and data searching.
⑤ Further improve the ability of writing scientific and technological papers and document sorting.
⑥ Cultivate students’ innovative consciousness and ability.

• 2. Implementation method
Using ISE software through Verilog HDL language editing program, using Xilinx Spartan-3AN XC30S50AN-4TQG144I hardware download program, and at the same time build peripheral circuit on bread board.
• 3. Implementation
Use key control to select the music preset in the circuit, select a certain music after the corresponding light-emitting diode bright, music playing circuit repeatedly automatic play the selected music, after power amplification by the horn broadcast, until the next position selected, the system clock through the use of active crystal vibration and out.
• 4. Keyword
Verilog, field editable logic gate array FPGA, music playback, scale frequency, chip use.

一、 课题技术指标

  1. 乐曲要求:
    ① 乐曲数目3首;
    ② 每首乐曲长度在20s~30s之间;
    ③ 所选择的乐曲应在4个8度内,以第六个8度作为最高的8度;
    ④乐曲演奏速度为100拍/min~120拍/min。
  2. 演奏要求
    ①用一个自复键Key选择所需的乐曲,用3个LED表示选中对应乐曲,当3个LED均不亮时,表示没有选中,电路没有乐曲输出。
    ②一旦选中某一首乐曲,电路将自动循环放送所选的乐曲。
  3. 电气指标
    ① 音频功放输入为方波;
    ② 音节频率误差E<=5生;
    ③ 负载(喇叭)阻抗为8Ω,功率为1/8W(也可以用蜂鸣器);
    ④ 输出音量可调。
    4.选作指标
    对于产生声音的波形来说,方波听起来是刺耳的,如果向喇叭中输入正弦波(具有基波和谐波),声音会悦耳很多,并且可以通过改变谐波模仿绝大多数乐器的声音。

二、系统设计
1.模块化设计
(1)按键消抖模块
为了消除按键的机械抖动,防止按键误判,设置当只有低电平超过20ms时检测到按键按下。
(2)状态机模块
一个按键控制四种状态,需要使用状态机,按键按下即进行一次状态机跳转,分别是不放音乐NONE(不亮灯),放第一首音乐LED0(亮第一个灯),放第二首音乐LED1(亮第二个灯),放第三首音乐LED2(亮第三个灯)。
(3)点灯模块
通过读取状态机的现态,控制三个灯的亮灭。
(4)单个音符时间存储ROM模块
设置一拍时间为 1 秒,那么四分音符也就是 1 秒,三十二分音符也就是 1/8 秒,可以看到最短的时值为三十二分音符,按 1/8 秒作为最小时间单位,时间 ROM 里读出的时长数据,就是有多少个时间单位,比如第一个简谱名中音 3,为四分音符,也就是 8 个时间单位,ROM中存储的值为 8,也就是 1 秒。
(5)单个音符频率存储ROM模块
每个简谱名对应不同的频率,可以用系统时钟生成对应的频率,对每个简谱名进行编码,hz_sel 为频率选择信号,cycle 为每个简谱名对应的计数值。
(6)频率计数值存储模块
将二十八个音符的频率计数值写在频率存储模块中,供hz_sel选择信号选择调用。
(7)时间计数和频率计数模块
通过两个ROM中读取的数据,经过计算后送入对应计数器中计数,就可以实现产生对应的频率和音符时值。
(8)PWM方波产生模块
设置占空比,在一个音符的时钟周期中占空比对应数值之前的时间置1,之后的时间置0,就可以产生对应的PWM波。
(9)音乐选择
分别在两个ROM存储时间和频率对应值,三首歌存在同一个ROM中,在状态机跳转后读取不同的ROM的地址位,即可实现不同音乐的播放。
4. 顶层模块设计
顶层模块包含了按键消抖、状态机跳转、led灯控制、音乐播放的底层模块。
通过按键消抖模块读取按键输入,按键输入信号进行状态机跳转,在不同状态下控制led灯的亮灭情况和音乐选曲播放的情况。

  1. 程序框图
    在这里插入图片描述

图2-1 系统程序框图设计

三、源程序设计
1.按键消抖模块设计
(1)为什么要按键消抖
按键开关位机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合式不会马上稳定的接通,在断开时也不会立即断开,因此在按键闭合和断开的瞬间伴随有一连串的抖动,为了不产生这种现象就需要进行按键消抖。抖动时间的长短由按键的机械特性决定,一般为5ms~10ms。按键稳定闭合时间的长短则是由按键动作决定的,一般为零点几秒至数秒。
按键抖动会导致按键被误读多次,并且即便没有抖动,在按键按下后会读取很多次时钟上升沿,这就会导致按键被检测到多次按下,不符合我们想达成的目的,因此需要进行按键消抖。
(2)按键消抖的方法
按键消抖有硬件和软件的两种方式。
硬件消抖方法:可以用两个与非门构成一个RS触发器进行硬件消抖。
软件消抖方法:软件消抖的原则就是检测出按键闭合后执行一个延时程序,当检测到低电平的时候就开始计时,计数为10ms以内并检测到了高电平信号,就代表着是一次按键抖动,当低电平持续时间达到20ms及以上的时候,将按键按下标志信号拉高一个时钟周期,代表着按键按下了一次。
(3)按键消抖代码

1.	//定义常量为计数器最大值 
2.	parameter CNT_MAX=1_999;  
3.	  
4.	//按键按下标志信号,作为按键消抖后的有效判断信号  
5.	reg key_flag;  
6.	//按键按下后的延时电路的计数器,用来计数是否到达20ms,计数最大值为1999,位宽为11位。  
7.	reg [10:0]cnt_20;  
8.	  
9.	/*按键消抖计数器*/
10.	always @(posedge clk or negedge rst_n)  
11.	    if(!rst_n)  
12.	        cnt_20<=1'b0;  
13.	    else if(key_in==1'b1)  
14.	        cnt_20<=1'b0;  
15.	    else if(cnt_20==CNT_MAX &&  key_in==1'b0)  
16.	        cnt_20<=cnt_20;  
17.	    else  
18.	        cnt_20<=cnt_20+1'b1;  
19.	  
20.	/*按键有效标志信号key_flag*/  
21.	always @(posedge clk or negedge rst_n)  
22.	    if(!rst_n)  
23.	        key_flag<=1'b0;  
24.	    else if(cnt_20==CNT_MAX-1'b1)  
25.	        key_flag<=1'b1;  
26.	    else   
27.	        key_flag<=1'b0; 

CNT_MAX:系统时钟输入100KHz,一个时钟周期是0.00001s计数器计数计数20ms的计数值CNT_MAX为1_999次。
cnt_20:计数器初始值为0,按键端口没有按下时的输入是拉高状态,因此在kye_in等于高电平的时候,计数器始终为0,当key_in等于低电平的时候,计数器开始工作,每一个时钟上升沿计数器加一,在记到最大值CNT_MAX并且此时key_in为低电平时,计数器始终保持最高值,直到按键结束。
key_flag:初始值为0,在计数器达到最大值的前一个状态时,key_flag拉高,并且在下一个时钟上升沿到来的时候拉低,这样就保证了一次按键按下只会被检测到一次。

2.状态机模块
(1)状态机是什么
状态机是一个具有有限个状态以及状态转移的数学模型,通过状态机,可以实现不同判定条件下的准确的状态判定,从而达到控制及选择的目的。状态机的编码方式有多种,这里采用最简单的二进制码来控制四个状态的判定,对于更多状态和更高精度要求的状态机,要通过格雷码、独热码等编码方式实现。状态机有一段式、二段式、三段式之分,这里使用二段式状态机。
(2)为什么要用状态机
题目要求使用一个按键来控制四个状态的切换,用一个信号的高低状态来控制四个状态显然是不可能的,这个时候就需要使用到状态机来进行状态跳转。
(3)状态机的设计方法
可以通过绘制状态状态转移图的方式来设计状态机的状态跳转情况,下面给出本实验用到的状态转移图
在这里插入图片描述

图3-1 状态机原理图
(4)状态机的代码设计

1.	/*将状态机的四个状态定义为常量*/  
2.	parameter       NONE = 2'b00,  
3.	                LED0 = 2'b01,  
4.	                LED1 = 2'b10,  
5.	                LED2 = 2'b11;  
6.	  
7.	  
8.	/*定义一个二位的次态和一个二位的现态*/  
9.	reg [1:0]       nstate;  
10.	reg [1:0]       cstate;  
11.	  
12.	/*使用时序逻辑在第一个always块中将次态赋给现态*/  
13.	always @(posedge clk or negedge rst_n)  
14.	        if(!rst_n)  
15.	        cstate <= NONE;  
16.	        else   
17.	        cstate <= nstate;  
18.	  
19.	/*使用组合逻辑在第二个always块中进行次态的跳转*/  
20.	always @(*)  
21.	        case(cstate)  
22.	            NONE :  
23.	                if(key_flag)  
24.	                    nstate <= LED0;  
25.	                else   
26.	                    nstate <= NONE;  
27.	            LED0 :  
28.	                if(key_flag)  
29.	                    nstate <= LED1;  
30.	                else  
31.	                    nstate <= LED0;  
32.	            LED1 :  
33.	                if(key_flag)  
34.	                    nstate <= LED2;  
35.	                else  
36.	                    nstate <= LED1;  
37.	            LED2 :  
38.	                if(key_flag)  
39.	                    nstate <= NONE;  
40.	                else  
41.	                    nstate <= LED2;  
42.	            default :  
43.	                nstate <= NONE;  
44.	        endcase 

状态跳转实现:次态,通过组合逻辑实现,当现态在NONE状态时,如果检测到一个按键信号,次态跳转到LED0,如果没有检测到,次态依然保持NONE,当下一个时钟上升沿到来时,在时序逻辑里现态cstate等于nstate,实现了状态跳转,同理,接下来的几个状态也是如此跳转。当现态为LED2时,检测到按键信号后,次态会立即跳转到NONE,如此进行了一次状态循环。

3.点灯模块
(1)发光二极管的工作原理
如图所示,向发光二极管的阳极输入低电平时不亮,输入高电平时点亮了LED灯
在这里插入图片描述

图3-2 发光二极管原理图
(2)点灯模块代码设计

1.	module led_ctrl(  
2.	    /*输入系统时钟,频率为100KHz*/  
3.	    input wire clk,  
4.	    /*复位信号,低电平有效*/  
5.	    input wire rst_n,  
6.	    /*在顶层模块向led_ctrl输入的状态机状态*/  
7.	    input wire [1:0]nstate,  
8.	    /*三个LED灯输出*/  
9.	    output reg [2:0]led  
10.	);  
11.	    /*将输入的状态机状态定义为常量,命名方式和顶层保持一致,便	于操作*/  
12.	    parameter       NONE = 2'b00,  
13.	                    LED0 = 2'b01,  
14.	                    LED1 = 2'b10,  
15.	                    LED2 = 2'b11;  
16.	  
17.	    /*LED灯控制*/                  
18.	    always @(posedge clk or negedge rst_n)  
19.	    if(!rst_n)  
20.	        led <= 1'b0;  
21.	    else if(nstate == NONE)  
22.	        led <= 3'b000;  
23.	    else if(nstate == LED0)  
24.	        led <= 3'b001;  
25.	    else if(nstate == LED1)  
26.	        led <= 3'b010;  
27.	    else if(nstate == LED2)  
28.	        led <= 3'b100;  
29.	    else   
30.	        led <= led;  
31.	  
32.	endmodule 

LED灯控制:当输入状态为NONE时,led输出为3’b000,全为低电平,led不亮,当输入状态为LED0时,led输出为3’b001,第一位led拉高,其余为低电平,对应的第一位的led点亮,其余熄灭;当输入状态为LED1时,led输出为3’b010,第二位led点亮,其余熄灭;当输入状态为LED2时,led输出为3’b100,对应的第三位led点亮,其余熄灭。

4.单个音符时间存储ROM模块
(1)时间ROM IP核中需要存储什么数据
配置位宽为8位,深度为512的ROM,进行乐谱单个音符时间的存储。
设置一拍时间为 1 秒,那么四分音符也就是 1 秒,三十二分音符也就是 1/8 秒,可以看到最短的时值为三十二分音符,按 1/8 秒作为最小时间单位,时间 ROM 里读出的时长数据,就是有多少个时间单位,
在这里插入图片描述

图3-3 乐谱
以这段简谱为例,第一行的第十五个音符高音re,是一整拍,在COE文件中存储的第一个数便是十进制的08;第一行的第三个音符高音do,在下面有两条横杠,是四分之一拍,在COE文件中存储的数便是十进制的02;第一行的第六个音符高音fa,是二分之一拍,在COE文件中存储的数据便是十进制的04;而第一行的第一个音符高音do,是两拍,持续两拍的时间,在COE文件中存储的数据便是十进制的16。
5.频率计数值存储模块
(1)频率输出是怎么实现的
系统输入时钟为100KHz,记为fclk,以中音do为例,频率为523Hz,记为fout,时钟对应的周期为Tclk=1/fclk,中音do对应的周期为Tout=1/fout,频率的输出通过计数器来实现,在系统时钟中,计数器计满Tout/Tclk,即fclk/fout,便得到了一个音符频率的计数器最大值。
(2)存储频率计数值的模块代码设计

1.	module hz(  
2.	  
3.	    /*频率选择输入信号*/  
4.	    input wire [7:0]hz_sel,  
5.	      
6.	    /*输出频率计数值*/  
7.	    output reg [19:0]cycle  
8.	);  
9.	    /*定义系统时钟为常数*/  
10.	    parameter CLK_FRE = 1 ;  
11.	  
12.	    /*使用case语句进行频率计数值选择*/  
13.	always @(*)begin  
14.	case(hz_sel)  
15.	    8'h01 : cycle <= CLK_FRE*100000/261 ; //low 1 261Hz  
16.	    8'h02 : cycle <= CLK_FRE*100000/293 ; //low 2 293Hz  
17.	    8'h03 : cycle <= CLK_FRE*100000/329 ; //low 3 329Hz  
18.	    8'h04 : cycle <= CLK_FRE*100000/349 ; //low 4 349Hz  
19.	    8'h05 : cycle <= CLK_FRE*100000/392 ; //low 5 392Hz  
20.	    8'h06 : cycle <= CLK_FRE*100000/440 ; //low 6 440Hz  
21.	    8'h07 : cycle <= CLK_FRE*100000/499 ; //low 7 499Hz  
22.	    8'h11 : cycle <= CLK_FRE*100000/523 ; //middle 1 523Hz 
23.	    8'h12 : cycle <= CLK_FRE*100000/587 ; //middle 2 587Hz 
24.	    8'h13 : cycle <= CLK_FRE*100000/659 ; //middle 3 659Hz 
25.	    8'h14 : cycle <= CLK_FRE*100000/698 ; //middle 4 698Hz 
26.	    8'h15 : cycle <= CLK_FRE*100000/784 ; //middle 5 784Hz 
27.	    8'h16 : cycle <= CLK_FRE*100000/880 ; //middle 6 880Hz 
28.	    8'h17 : cycle <= CLK_FRE*100000/998 ; //middle 7 998Hz 
29.	    8'h21 : cycle <= CLK_FRE*100000/1046 ; //high 1 1046Hz 
30.	    8'h22 : cycle <= CLK_FRE*100000/1174 ; //high 2 1174Hz 
31.	    8'h23 : cycle <= CLK_FRE*100000/1318 ; //high 3 1318Hz 
32.	    8'h24 : cycle <= CLK_FRE*100000/1396 ; //high 4 1396Hz 
33.	    8'h25 : cycle <= CLK_FRE*100000/1568 ; //high 5 1568Hz 
34.	    8'h26 : cycle <= CLK_FRE*100000/1760 ; //high 6 1760Hz 
35.	    8'h27 : cycle <= CLK_FRE*100000/1976 ; //high 7 1976Hz 
36.	    8'h31 : cycle <= CLK_FRE*100000/2093 ; //super high 1 2093Hz  
37.	    8'h32 : cycle <= CLK_FRE*100000/2349 ; //super high 2 2349Hz  
38.	    8'h33 : cycle <= CLK_FRE*100000/2637 ; //super high 3 2637Hz  
39.	    8'h34 : cycle <= CLK_FRE*100000/2794 ; //super high 4 2794Hz  
40.	    8'h35 : cycle <= CLK_FRE*100000/3136 ; //super high 5 3136Hz  
41.	    8'h36 : cycle <= CLK_FRE*100000/3520 ; //super high 6 3520Hz  
42.	    8'h37 : cycle <= CLK_FRE*100000/3951 ; //super high 7 3951Hz  
43.	    default:cycle<=20'd0;  
44.	endcase  
45.	end  
46.	  
47.	endmodule 

可以看到,当输入hz_sel信号后,就可以对各种频率的计数值进行选择,hz_sel信号的数值,便是在单个音符频率存储的ROM中存储的数据。

6.单个音符频率存储ROM模块
(1)频率ROM IP核中需要存储什么数据
在上一个模块中,实现了对每个音符频率对应计数值的编写,在频率ROM模块中,就可以写入每个音符频率对应的选择值
在这里插入图片描述

图3-4 乐谱
依然以这段简谱为例,乐谱中高音do的音符对应存储在COE文件中的数据为十六进制的21,ROM会输出频率为1046的计数值;高音fa的音符对应存储在COE文件中的数据为十六进制的24,ROM会输出频率为1396的计数值,以此类推。
7.时间计数和频率计数模块
(1)时间计数模块代码实现

1.	/*时间计数器*/  
2.	reg [31:0]cnt;  
3.	  
4.	/*单个音符时间值*/  
5.	reg [31:0]time_cycle;  
6.	  
7.	/*单个音符时间计数值*/  
8.	always @(posedge clk or negedge rst_n)  
9.	if(!rst_n)  
10.	    time_cycle<=32'd0;  
11.	else  
12.	    time_cycle<=time_music*(CLK_FRE*100000/8) ;   
13.	      
14.	/*单个音符时间计数器计数,最大值为time_cycle*/  
15.	always @(posedge clk or negedge rst_n)  
16.	if(!rst_n)  
17.	    cnt<=1'b0;  
18.	else if(cnt==time_cycle)  
19.	    cnt<=1'b0;  
20.	else  
21.	    cnt<=cnt+1'b1;  

time_cycle:定义32位的寄存器来存放单个音符时间的计数值,计算公式为time_cycle<=time_music*(CLK_FRE*100000/8),其中CLK_FRE为定义的常数1,其值根据输入系统的不同的时钟而改变。
cnt:单个音符时间计数器,在计满一个time_cycle的时间时,就代表着一个音符的时间读取完毕,再进入下一个音符时间。

(2)频率计数模块代码实现

1.		/*在hz模块输出的cycle值*/  
2.	wire [19:0]cycle;  
3.	  
4.	/*频率计数器*/  
5.	reg [19:0]cnt_cycle;  
6.	
7.	/*定义占空比*/
8.	wire [19:0]duty_data;
9.	  
10.	//频率计数器  
11.	always @(posedge clk or negedge rst_n)  
12.	if(!rst_n)  
13.	    cnt_cycle<=1'b0;  
14.	else if(cnt_cycle==cycle || cnt==time_cycle)  
15.	    cnt_cycle<=1'b0;  
16.	else  
17.	    cnt_cycle<=cnt_cycle+1'b1;  
18.	      
19.	//占空比  
20.	assign duty_data=cycle/8;  

cycle:单个音符的频率计数值,由hz模块中输入得来。
cnt_cycle:频率计数器,用来计算输出PWM波的频率,在一个音符的时间内,循环加计数器,计满为cnt_cycle与cycle相等时归零,重新开始计数,直到这个音符的时间结束,也就是cnt等于time_cycle时,接下来进入下一个音符的频率计数。
duty_data:PWM占空比,占空比控制着声音的强度,为了声音柔和不刺耳,将占空比调整为了20%,一般情况下的占空比可调整为50%。占空比的计算公式为duty_data=cycle/8。

8.PWM方波产生模块
(1)PWM原理
PWM方波的频率可以控制音色,占空比可以控制声音强度,在一个周期里,占空比数值前的时间输出高电平,占空比数值后的时间输出低电平。
(2)PWM产生模块代码实现

1.	//蜂鸣器输出PWM  
2.	always @(posedge clk or negedge rst_n)  
3.	if(!rst_n)  
4.	    beep=1'b0;  
5.	else if(cnt_cycle>=duty_data)  
6.	    beep=1'b1;  
7.	else  
8.	    beep=1'b0;  

beep:喇叭输出,初始值为低电平,在低电平时刻喇叭不响,在高电平时刻喇叭响,因此在占空比之数值之前输出高电平,在占空比数值之后输出低电平,就实现了使用PWM波输出对应的频率的波形到喇叭中,可以播放对应的音符。

9.音乐选择模块
(1)读ROM地址选择
音乐的播放需要用到两个ROM,将这两个ROM的地址同时控制,可以实现时间和频率的一致输出。
三首歌是同时存储在一个ROM中,每首歌曲的地址不同,因此只需要在不同的状态时选择不同的初始地址循环播放就可以实现三首歌的选择。
(2)读ROM地址代码实现

1.	/*输出的真正地址,在不同的状态选择不同的值*/  
2.	reg [9:0]address;  
3.	  
4.	/*歌曲一的地址选择*/  
5.	reg [9:0]address_1;  
6.	  
7.	/*歌曲二的地址选择*/  
8.	reg [9:0]address_2;  
9.	  
10.	/*歌曲三的地址选择*/  
11.	reg [9:0]address_3;  
12.	  
13.	/*歌曲一的地址,从0-111*/  
14.	always @(posedge clk or negedge rst_n)  
15.	if(!rst_n)  
16.	    address_1 <= 1'b0;  
17.	else if(address_1 == 10'd111 && cnt == time_cycle && nstate == LED0)  
18.	    address_1 <= 1'b0;  
19.	else if(cnt == time_cycle && nstate == LED0)  
20.	    address_1 <= address_1 + 1'b1;  
21.	  
22.	/*歌曲二的地址,从112-177*/  
23.	always @(posedge clk or negedge rst_n)  
24.	if(!rst_n)  
25.	    address_2 <= 10'd112;  
26.	else if(address_2 == 10'd177 && cnt == time_cycle && nstate == LED1)  
27.	    address_2 <= 10'd112;  
28.	else if(cnt == time_cycle && nstate == LED1)  
29.	    address_2 <= address_2 + 1'b1;  
30.	  
31.	/*歌曲三的地址,从178-323*/  
32.	always @(posedge clk or negedge rst_n)  
33.	if(!rst_n)  
34.	    address_3 <= 10'd178;  
35.	else if(address_1 == 10'd323 && cnt == time_cycle && nstate == LED2)  
36.	    address_3 <= 10'd178;  
37.	else if(cnt == time_cycle && nstate == LED2)  
38.	    address_3 <= address_3 + 1'b1;  
39.	
40.	/*真正输出的地址,通过状态选择不同的歌曲地址*/  
41.	always @(posedge clk or negedge rst_n)  
42.	if(!rst_n)    
43.	    address <= 1'b0;  
44.	else if(nstate == NONE)  
45.	    address <= 1'b0;  
46.	else if(nstate == LED0)  
47.	    address <= address_1;  
48.	else if(nstate == LED1)  
49.	    address <= address_2;  
50.	else if(nstate == LED2)  
51.	    address <= address_3;  

三首歌曲的首地址不同,结束的地址也不同,因此将address_1的首地址初始值设为0,尾地址设为111,address_2的首地址初始值设为112,尾地址设为177,address_3的首地址初始值设为178,尾地址设为323。
当状态为NONE时,不播放音乐,address为0,当状态为LED0时,播放第一段音乐,address等于address_1,播放第一段音乐;当状态为LED1时,播放第二段音乐,address等于address_2,播放第二段音乐;当状态为LED2时,播放第三段音乐,address等于address_3,播放第三段音乐。

四、单元电路、整体电路功能测试
1.按键消抖仿真测试
(1)按键消抖仿真测试模块代码

1.	/*设置时间步进为1ns,精度为1ns*/
2.	`timescale 1ns/1ns 
3.	/*定义时钟周期为10000ns,时钟频率就为1KHz*/
4.	`define clk_period 10000
5.	
6.	module  key_filter_tb;  
7.	  
8.	    /*定义仿真的不同时间*/  
9.	    parameter   CNT_1MS  = 20'd19   ,  
10.	                CNT_11MS = 21'd69   ,  
11.	                CNT_41MS = 22'd149  ,  
12.	                CNT_51MS = 22'd199  ,  
13.	                CNT_60MS = 22'd249  ;  
14.	  
15.	    //wire  define  
16.	    wire            key_flag        ;   //消抖后按键信号  
17.	  
18.	    //reg   define  
19.	    reg             clk         ;   //仿真时钟信号  
20.	    reg             rst_n       ;   //仿真复位信号  
21.	    reg             key_in          ;   //模拟按键输入  
22.	    reg     [21:0]  tb_cnt          ;  //模拟按键抖动计数器  
23.	  
24.	    /*初始化输入信号*/  
25.	    initial begin  
26.	        clk    = 1'b1;  
27.	        rst_n <= 1'b0;  
28.	        key_in    <= 1'b0;  
29.	        #20           
30.	        rst_n <= 1'b1;  
31.	    end  
32.	  
33.	    //时钟信号  
34.	    always #10 clk = ~clk;  
35.	  
36.	    /*tb_cnt:按键过程计数器,通过该计数器的计数时间来模拟按键的抖动过程*/  
37.	    always@(posedge clk or negedge rst_n)  
38.	    if(rst_n == 1'b0)  
39.	        tb_cnt <= 22'b0;  
40.	    else    if(tb_cnt == CNT_60MS)  
41.	        tb_cnt <= 22'b0;  
42.	    else      
43.	        tb_cnt <= tb_cnt + 1'b1;  
44.	  
45.	    /*key_in:产生输入随机数,模拟按键的输入情况*/  
46.	    always@(posedge clk or negedge rst_n)  
47.	    if(rst_n == 1'b0)  
48.	        key_in <= 1'b1;       
49.	    else    if((tb_cnt >= CNT_1MS && tb_cnt <= CNT_11MS)  
50.	             || (tb_cnt >= CNT_41MS && tb_cnt <= CNT_51MS))  
51.	    /*在该计数区间内产生非负随机数0、1来模拟10ms的前抖动和10ms的后抖动*/  
52.	        key_in <= {$random} % 2;      
53.	    else    if(tb_cnt >= CNT_11MS && tb_cnt <= CNT_41MS)  
54.	        key_in <= 1'b0;  
55.	    /*按键经过10ms的前抖动后稳定在低电平,持续时间需大于CNT_MAX*/  
56.	    else  
57.	        key_in <= 1'b1;  
58.	  
59.	    key_filter  
60.	    #(  
61.	        .CNT_MAX    (20'd24     )  
62.	    )  
63.	    key_filter_inst  
64.	    (  
65.	        .clk    (clk    ),   
66.	        .rst_n  (rst_n  ),   
67.	        .key_in     (key_in     ),    
68.	                              
69.	        .key_flag   (key_flag   )   
70.	    );  
71.	  
72.	endmodule 

为了节约仿真时间,对计数器的计数值做了相应的调整,可以使我们更加清晰地观测到按键消抖的效果,后面提到的20ms均为模拟的值,不是真实时间。
使用随机数产生代码{$random} % 2来模拟按键的按下和松开,通过使用tb_cnt来模拟每一个key_in的持续时间,当持续时间大于设置的20ms后,按键被检测到有效,这时key_flag拉高一个时钟周期的高电平,代表着按键按下有效。

(2)仿真波形图
在这里插入图片描述

图4-1 按键消抖仿真
观察波形图,可以看到,当key_in的低电平时间不足设置的20ms时,key_flag无效,当key_in的低电平时间到了20ms后,key_flag拉高一个时钟周期,并且立即变为低电平,这样就达到了只检测到一个按键按下的效果。
2.状态机仿真测试
(1)状态机仿真测试模块代码

1.	/*设置时间步进为1ns,精度为1ns*/
2.	`timescale 1ns/1ns 
3.	/*定义时钟周期为10000ns,时钟频率就为1KHz*/
4.	`define clk_period 10000 
5.	  
6.	module state_test_tb;  
7.	  
8.	    /*定义系统时钟*/  
9.	    reg clk;  
10.	    /*复位输入,低电平有效*/  
11.	    reg rst_n;  
12.	    /*输入按键*/  
13.	    reg key_flag;  
14.	  
15.	    /*输出现态*/  
16.	    wire [1:0]cstate;  
17.	  
18.	    /*模拟时钟输入*/  
19.	    initial clk = 1'b0;  
20.	    always #(`clk_period/2) clk = ~clk;  
21.	  
22.	    initial begin  
23.	        /*初始化输入*/  
24.	        rst_n = 1'b0;  
25.	        key_flag = 1'b0;      
26.	        #200;  
27.	        /*复位置1,进行四次按键输入,可以模拟整个状态转移*/  
28.	        rst_n = 1'b1;  
29.	        key_flag = 1'b1;  
30.	        #20;  
31.	        key_flag = 1'b0;  
32.	        #200;  
33.	        key_flag = 1'b1;  
34.	        #20;  
35.	        key_flag = 1'b0;  
36.	        #200;  
37.	        key_flag = 1'b1;  
38.	        #20;  
39.	        key_flag = 1'b0;  
40.	        #200;  
41.	        key_flag = 1'b1;  
42.	        #20;  
43.	        key_flag = 1'b0;  
44.	        #500;  
45.	        $stop;  
46.	    end  
47.	  
48.	    /*实例化状态机模块代码*/  
49.	    state_test state_test_inst(  
50.	        .clk        (clk),  
51.	        .rst_n      (rst_n),  
52.	        .key_flag     (key_flag),  
53.	  
54.	        .cstate     (cstate)  
55.	    );  
56.	  
57.	endmodule   

(2)仿真波形图
在这里插入图片描述

图4-2 状态机仿真
观察波形图,可以看到,当按键四次按下时,现态进行了四次跳转,且每次跳转均正确,仿真成功。
3.点灯模块
(1)LED灯仿真测试模块代码

1.	/*设置时间步进为1ns,精度为1ns*/
2.	`timescale 1ns/1ns  
3.	/*定义时钟周期为10000ns,时钟频率就为1KHz*/
4.	`define clk_period 10000
5.	  
6.	module led_ctrl_tb;  
7.	  
8.	    reg clk;  
9.	    reg rst_n;  
10.	    reg [1:0]nstate;  
11.	  
12.	    wire [2:0]led;  
13.	  
14.	    /*设置模拟仿真时钟*/  
15.	    initial clk = 1'b0;  
16.	    always #(`clk_period/2) clk = ~clk;  
17.	  
18.	    initial begin  
19.	        rst_n = 1'b0;  
20.	        nstate = 2'b00;  
21.	        #23;  
22.	        rst_n = 1'b1;  
23.	        #200;  
24.	        nstate = 2'b01;  
25.	        #200;  
26.	        nstate = 2'b10;  
27.	        #200;  
28.	        nstate = 2'b11;  
29.	        #200;  
30.	        $stop;  
31.	    end  
32.	  
33.	    /*实例化LED模块*/  
34.	    led_ctrl led_ctrl_inst(  
35.	        /*输入系统时钟,频率为100KHz*/  
36.	        .clk        (clk),  
37.	        /*复位信号,低电平有效*/  
38.	        .rst_n      (rst_n),  
39.	        /*在顶层模块向led_ctrl输入的状态机状态*/  
40.	        .nstate     (nstate),  
41.	        /*三个LED灯输出*/  
42.	        .led        (led)  
43.	    );  
44.	  
45.	  
46.	endmodule 

将LED模块例化在仿真激励测试文件中,通过状态机输入的状态值来进行灯的控制。

(2)仿真波形图
在这里插入图片描述

图4-3 LED模块仿真
通过波形图,可以看到,在状态为2’b00时,LED全灭,在状态2’b01时,LED0亮,在状态2’b10时,LED1亮,在状态2’b11时,LED2亮。仿真通过。
4.音乐播放模块
(1)音乐播放仿真测试代码

1.	/*设置时间步进为1ns,精度为1ns*/  
2.	`timescale 1ns/1ns  
3.	/*定义时钟周期为10000ns,时钟频率就为1KHz*/  
4.	`define clk_period 10000  
5.	  
6.	module beep_music_tb;  
7.	  
8.	    /*定义时钟*/  
9.	    reg clk;  
10.	    /*异步复位*/  
11.	    reg rst_n;  
12.	      
13.	    /*输出PWM波*/  
14.	    wire beep;  
15.	      
16.	    /*模拟输入系统时钟,时钟频率为100KHz*/  
17.	    initial clk=1'b1;  
18.	    always #(`clk_period/2) clk=~clk;  
19.	      
20.	    /*初始化端口变量*/  
21.	    initial begin  
22.	        rst_n=1'b0;  
23.	        #(`clk_period*20+1);  
24.	        rst_n=1'b1;  
25.	    end  
26.	  
27.	    /*实例化音乐播放模块*/  
28.	    beep_music   
29.	    #(  
30.	        .CLK_FRE(100_000)  
31.	    )  
32.	    beep_music(  
33.	  
34.	        .clk(clk),  
35.	        .rst_n(rst_n),  
36.	          
37.	        .beep(beep)  
38.	  
39.	    );  
40.	  
41.	endmodule

音乐自动播放,直接在初始化之后将复位信号拉高,地址自动增加。

(2)仿真波形图
在这里插入图片描述

图4-4 音乐播放模块仿真
地址依次加一。
在这里插入图片描述

图4-5 频率仿真图
单个音符的频率显示,此音符显示为839Hz。
五、喇叭功放电路及振荡电路
1.喇叭功放电路的搭建
(1)LM386芯片引脚配置和功能图
在这里插入图片描述

图5-1 LM386引脚图
在这里插入图片描述

图5-2 LM386功能图
(2)LM386应用电路
设置增益为20的应用电路:
在这里插入图片描述

图5-3 LM386电路图
(3)注意事项
电源输入电压信号会伴有噪声,在喇叭发出声音时会有较大的干扰,所以要在LM386芯片的电源脚上接一个1nF的滤波电容,这样就可以滤掉电源带来的噪声。
2.振荡电路
(1)SN74HC132芯片引脚配置及功能图
在这里插入图片描述

图5-4 SN74HC132引脚图
在这里插入图片描述

图5-5 SN74HC132功能图
(2)振荡电路电路图
在这里插入图片描述

图5-5 振荡电路图
其中,R为10K欧姆电阻,C为471、数值为470PF的电容。
(3)振荡电路波形
在这里插入图片描述

图5-6 时钟波形图
六、输出正弦波形(附加指标)
1.为什么要输出正弦波
对于声音来说,方波和正弦波输出声音,但是方波发出的声音的波形捕获下来是一段不规则的正弦波叠加而成,声音不会很悦耳。但是如果输出正弦波,包括基波和一至多个谐波,是很柔和且可控的声音。
2.正弦波的妙用
各种声音,尤其是乐器的声音听起来不同的原因是声音的基波和谐波不同,电子乐器就是通过模拟各种不同的乐器的每个声音的基波和谐波来达到发出对应的乐器的声音的,因此,只要知道了某一种声音的波形,就可以用代码来模拟并且输出出来。
3.产生正弦波的代码实现
(1)使用MATLAB产生波表
使用MATLAB数学工具可以产生任意形状的波形,FPGA通过在ROM中存储波表再输出可以实现波形发生器(DDS)。
MATLAB产生一个基波加一个一次谐波的代码

1.	F1=1;       %信号频率  
2.	F2=2;  
3.	P1=0;       %信号初始相位  
4.	P2=0;  
5.	Fs=248;     %采样频率  
6.	N=248;      %采样点数  
7.	t=[0:1/Fs:(N-1)/Fs];    %采样时刻  
8.	ADC=2^7+35;  
9.	A=2^7;      %信号幅度  
10.	   
11.	%生成信号  
12.	s1=A*sin(2*pi*F1*t+pi*P1/180)+ADC;  
13.	s2=A/2*sin(2*pi*F2*t+pi*P2/180)+ADC;  
14.	s=s1+s2-ADC;  
15.	   
16.	plot(t,s1,'-',t,s2,'*',t,s,'-');  
17.	legend('s1','s2','s3');  
18.	grid;  
19.	   
20.	fild = fopen('sin_wave_2.txt','wt');  
21.	for i = 1:N  
22.	    s0(i) = round(s(i));    %对小数四舍五入以取整  
23.	    if s0(i)<0  
24.	        s0(i)=0  
25.	    end  
26.	    fprintf(fild, '\t%g\t',i-1);    %地址编码  
27.	    fprintf(fild, '%s\t',':');      %冒号  
28.	    fprintf(fild, '%d',s0(i));      %数据写入  
29.	    fprintf(fild, '%s\n',';');      %分号,换行  
30.	end  
31.	fprintf(fild, '%s\n','END;');       %结束  
32.	fclose(fild);  

波形图
在这里插入图片描述

图6-1 带有一次谐波的波形图
(2)FPGA对波表操作
取产生波表的一半,作为数据输入,在输入完半个波形周期后,进行反向输入,在节省存储空间的同时还可以正确的输出波形,这样就获得了一个完整的带有一次谐波的正弦波, 代码如下:

1.	module lookup_tables(  
2.	    input wire [7:0]phase,  
3.	    output wire [9:0]sin_out  
4.	);  
5.	    reg [6:0]address;  
6.	    wire sel;  
7.	    wire [8:0]sine_table_out;  
8.	      
9.	    reg [9:0]sin_onecycle_amp;  
10.	  
11.	    sin_table sin_table_inst(  
12.	        .address(address),  
13.	        .sin(sine_table_out)  
14.	    );  
15.	  
16.		 /*输出十位的正弦波信号*/
17.	    assign sin_out = sin_onecycle_amp[9:0];  
18.	    /*在波表地址读满128个之后对波形进行翻转输出,这样就实现了只
19.		 用半个波表就输出了整个波形*/
20.		 assign sel = phase[7];  
21.	      
22.		 /*读取波表*/
23.	    always @(*)  
24.	        case(sel)  
25.	            1'b0:  begin  
26.	                    sin_onecycle_amp =  9'h1ff + sine_table_out[8:0];  
27.	                    address = phase[6:0];  
28.	                    end  
29.	            1'b1:  begin  
30.	                    sin_onecycle_amp = 9'h1ff - sine_table_out[8:0];  
31.	                    address = ~phase[6:0];  
32.	                    end  
33.	        endcase  
34.	  
35.	endmodule   

(3)DDS模块
其中 fre_add 表示相位累加器输出值,位宽为 32 位,系统上电后,fre_add 信号一直执行自加操作,每个时钟周期自加参数 FREQ_CTRL,参数FREQ_CTRL 的计算方法为:FREQ_CTRL= 2N * fOUT / fCLK 。

1.	module dds  
2.	#(    
3.	    parameter FREQ_CTRL = 24'd731  
4.	)  
5.	(  
6.	    input wire clk,  
7.	    input wire rst_n,  
8.	    /*波形输出*/
9.			output wire [9:0]data_out  
10.	);  
11.	  
12.	    reg [23:0]fre_add;  
13.	      
14.		 /*通过fre_add的自加操作实现了不同频率的输出*/
15.	    always @(posedge clk or negedge rst_n)  
16.	    if(!rst_n)  
17.	        fre_add <= 1'b0;  
18.	    else  
19.	        fre_add <= fre_add + FREQ_CTRL;  
20.	  
21.	    lookup_tables lookup_tables_inst(  
22.	        .phase(fre_add[23:16]),  
23.	        .sin_out(data_out)  
24.	    );  
25.	  
26.	endmodule  

通过对定义的常量FREQ_CTRL的改变,可以调整不同声音的频率。

(4)PWM产生模块

1.	module speaker_pwm(  
2.	    input wire clk,  
3.	    input wire rst_n,  
4.	    input wire [9:0]duty,  
5.	      
6.	    output wire pwm_out  
7.	);  
8.	    reg [10:0]pwm_acc;  
9.	 
10.	    always @(posedge clk or negedge rst_n)  
11.	    if(!rst_n)  
12.	        pwm_acc <= 1'b0;  
13.	    else      
14.	        pwm_acc <= pwm_acc[9:0] + duty;  
15.	    
16.		 /*PWM输出*/
17.	    assign pwm_out = pwm_acc[10];  
18.	  
19.	endmodule  

PWM产生的原理是:通过pwm_acc和波形数据duty进行累加,duty是低十位的数据,pwm_out是最高位的数据,这样就实现了一个以duty为分频点的分频器,在波形输出的不同时间里可以有不同的PWM输出,就可以发出正弦波波形的声音了。

(5)输出正弦波仿真
在这里插入图片描述

图6-2 带有一次谐波的波形仿真图
根据波形图,可以看到信号发生器产生的波形和我们输入波表中的波形是一致的,并且正弦波的频率是531Hz,为中音Do,仿真通过。
七、问题与解决
1.声音经过放大电路后有很大的噪声
解决方法:在LM386芯片的电源脚上加上一个1nF的滤波电容,就可以有效地去除电源噪声。
2.使用103电容和104电容振荡不出需要的高频的频率。
解决方法:根据公式T=2RC,使用更小的511的电容,获得更小的周期,便可以振荡出更大的频率。
3.在对一个ROM操作时改变选取
解决方法:根据FPGA上电自复位,将三个歌曲的首地址设为复位时的状态,便可以自由选曲。
八、元件清单
1、Spartan - 3AN XC30S50AN – 4TQG144I
2、LM386功放芯片 1个
3、SN74HC132有源晶振 1个
4、1/8W喇叭 1个
5、LED发光二极管 3只
6、1K电阻 4个
7、103电容、104电容、511电容 各1只
8、10K电位器 1个
9、导线若干

九、参考文献
1、LM386芯片手册
2、SN74HC132芯片手册

十、实验小结及心得体会
本次实验我使用的是Verilog语言编写整个系统程序,通过工程实践,我发现了很多之前没有注意到的不好的代码习惯,也注意到了程序框图的重要性,在大型的系统设计中,系统框图可以对工程代码的编写起到引导性的作用。同时仿真也是必不可少的一环,正确编写仿真测试激励文件和如何观察仿真波形是一项必备的技巧性技能。
在此之前,我总是对全英文的芯片手册束手无策,但是这次我自己对照着芯片手册,一步一步搭建起了两个芯片对应的外围电路,对于这一方面的技能有了长足的进步。
在后面的大学生活中,我也会继续进行更深入的FPGA设计实现,将这一项技能变成我的优势能力,完善自身,力争上游。
十一、附录
1.最终电路图

图11-1 最终电路图
2.课程设计日志
9月12日,我对整个系统电路进行了系统框图的设计,对每一个模块的作用做了初步的计划。
9月13日,编写按键消抖模块的代码,并通过仿真。
9月14日,编写LED模块代码并且搭配按键消抖模块实现了3个LED灯的控制,完成了这一项目指标。
9月15日,编写了音乐播放模块代码并通过了仿真。
9月16日,搭建了功放的外围电路并且实现了声音的输出。
9月19日,搭建了SN74HC132的外围电路并且达成了需要的输出频率。
9月20日,实现了整个工程,并通过了验收。
9月21日-23日,编写了额外指标的输出正弦波代码设计并通过了仿真。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值