这是大二时候的选修课课程设计,因为本人没学过音乐,没有任何声乐知识。所以刚拿到这个题目的时候不知道具体如何动手,但是问题的本质还是很清楚的。只需要掌握好音符的频率和演奏间隔就可以了,剩下就是把信号输出到喇叭。
根据百度来的声乐知识,组成乐曲的每个音符的发音频率值及其持续的时间是乐曲能连续演奏所需的两个基本要素,获取这两个要素所对应的数值以及通过纯硬件的手段来利用这些数值实现所希望乐曲的演奏效果是本实验的关键。(PS:用单片机实现简单多了,不过用VHDL别有一番趣味性。)
确定节奏
打算演奏的乐曲是《NEXT TO YOU》片段(本来想找《Solitude》的,但是没找到谱) 。首先,从网上找到简谱,如下:
网上有很多版本的,我找了一个看上去简单点的。这个谱是C调,4/4拍。(这图里第一节怎么有5拍半啊,最后的54应该是16分音符,和第三小节的54一样)
每分钟60拍,每拍就是一秒。四拍四就是:以四分音符为一拍,每小节4拍,也就是说每小节共花4秒。
所以这里的一个四分音符对应的频率需要持续1秒,谱子中最短的是十六分音符,一个四分音符可以分成4个十六分音符,所以为了方便起见,设定的节奏计数器应该周期设定成0.25s,对应4Hz的频率。这样一个四分音符的输出占4个节奏计数器的周期,八分音符的输出占2个周期,十六分音符占1个周期。
由于音符频率多为非整数,而分频系数又不能为小数,故必须将得到的分频数四舍五入取整。若基准频率过低,则由于分频系数过小,四舍五入取整后的误差较大,若基准频率过高,虽然误码差变小,但分频结构将变大。实际的设计应综合考虑两方面的因素,在尽量减小频率误差的前提下取舍合适的基准频率。本设计中选取1MHz的基准频率,而板载时钟是50MHz,所以首先需要一个生成1MHz的基准频率的模块。新建一个模块,并编写VHDL代码如下:
library ieee;
use ieee.std_logic_1164.all;
entity fdvd50 is
port(clk : in std_logic; --输入50MHZ信号
out1 : out std_logic);-- 输出1MHZ
end entity;
architecture one of fdvd50 is
signal T1 : integer range 0 to 25-1;
signal F1M : std_logic;
begin
process(clk)
begin
if(clk'event and clk='1')
then
T1<=T1+1;
if(T1=24) then F1M<= not F1M;T1<=0;end if;
end if;
end process;
out1<=F1M;
end architecture one;
根据此,新建一个fdvd4.vhd
,实现输出4Hz的频率:
library ieee;
use ieee.std_logic_1164.all;
entity fdvd4 is
port(clk : in std_logic; --输入1MHZ信号
F : out std_logic);-- 输出4HZ
end;
architecture one of fdvd4 is
signal T1 : integer range 0 to 124999;
signal F1M : std_logic;
begin
process(clk)
begin
if(clk'event and clk='1')
then
T1<=T1+1;
if(T1=124999) then F1M<= not F1M;T1<=0;end if;
end if;
end process;
F <= F1M;
end;
确定频率
前面确定了基准频率为1MHz,那接下来就是确定所需音符频率的分频数。这里输出音符频率的方法和前文的模块不同,这里的音符频率输出模块的输入时钟是1MHz,输出是连接喇叭的信号spks。音符变化的周期是0.25s,即4Hz。那么在这0.25s内就要输出若干个音符频率对应的周期。这里我们设置一个计数值,在达到这个计数值时将信号spks翻转,以达到一个可变的分频器的功能。因此这个计数值是基准频率除以2倍目标频率。
下表为部分可能能用到的音名与频率的对应关系。其中休止符比较特殊,按理来说应该是停止输出,这里为了统一形式,方便编写代码,做了简化处理,把休止符对应频率设定为人听不到的1Hz(最后由于喇叭的原因还是可以听到一点声音)。由于谱子是C调,那么C4\~B4对应do\~si,B3为降si,C5为升do。
音名 | 编码 | 频率F (HZ) | 计数值(10^6/2F)-1 |
---|---|---|---|
休止符 | 0 | 1 | 49999 |
B3 | 1 | 493.88 | 1011 |
C4 | 2 | 523.25 | 955 |
D4 | 3 | 587.33 | 850 |
E4 | 4 | 659.25 | 757 |
F4 | 5 | 698.46 | 715 |
G4 | 6 | 783.99 | 636 |
A4 | 7 | 880.00 | 567 |
B4 | 8 | 987.77 | 505 |
C5 | 9 | 1046.50 | 447 |
D5 | 10 | 1174.66 | 425 |
A5 | 11 | 1760.00 | 283 |
根据上述频率表,我们就可以进行输出信号模块的搭建了,代码如下。
library ieee;
use ieee.std_logic_1164.all;
use ieee.std_logic_unsigned.all;
entity speaker is
port(clk : in std_logic; --1Mhz
TN : in std_logic_vector(3 downto 0);--乐谱编码
spks : out std_logic);
end;
architecture rts of speaker is
signal FM : std_logic;
signal count : integer range 0 to 49998;
signal temp : integer range 0 to 49999;
begin
process (TN)
begin
case TN is
when "0000" => temp<=49999; -- stop
when "0001" => temp<=1011; -- b3
when "0010" => temp<=955; -- c4
when "0011" => temp<=850; -- d4
when "0100" => temp<=757; -- e4
when "0101" => temp<=715; -- f4
when "0110" => temp<=636; -- g4
when "0111" => temp<=567; -- a4
when "1000" => temp<=505; -- b4
when "1001" => temp<=447; -- c5
when "1010" => temp<=425; -- d5
when "1011" => temp<=283; -- a5
end case;
end process;
process(clk)
begin
if(clk'event and clk='1') then count<=count+1;
if(count=temp) then FM<=not FM;count<=0;end if;
end if;
end process;
spks<=FM;
end;
由于本次我们用到的音最多只有12个,因此使用4位的vector足以区分它们。好了,现在我们已经有了把音符输出成对应频率的方法了,下一步就是建立演奏的音符表。
设置乐谱存储器
我们首先来设计乐谱存储器的地址自加器,它的作用是让其随着时间的流逝,输出对应的音符编号。在本曲中,每0.25秒播放一个音符,所以地址自加的时间间隔应为0.25秒,也就是4Hz。本段乐谱的长度为15小结,也就是60个四分音符,即240个十六分音符。故设计最大计数地址为255。程序如下:
LIBRARY ieee;
USE IEEE.STD_LOGIC_1164.ALL;
USE IEEE.STD_LOGIC_UNSIGNED.ALL;
ENTITY address_cnt IS
PORT (clk : IN STD_LOGIC;
A : BUFFER STD_LOGIC_VECTOR(7 DOWNTO 0));
END;
ARCHITECTURE ONE OF address_cnt IS
BEGIN
PROCESS (clk)
BEGIN
IF (clk'event AND clk = '1')
THEN
A <= A + 1;
END IF;
END PROCESS;
END;
根据简谱我们创建mif (Memory Initialization File)文件,记录对应音符和持续时间。
前两小节的乐谱如下所示:
在补完所有mif文件空位后,接下来我们设计一个ROM来存储乐谱,由于本音乐选段所用到的音高只有12个,故该存储器的位数4位足矣。选段一共240个四分音符,故所选number of words为256 (剩下的用休止符填空)。
然后完成创建并且选择mif文件,并将其加入工程。到此,整个工程的构建就完毕了,接着进行编译、分配引脚后就可以上板子了。外接喇叭后,程序正常运转,喇叭中播放出预设音乐,音量清晰,音色动人。
最后附一张项目结构图。
請問有.bdf的圖片可以提供嗎?