banner
NEWS LETTER

AG1280实现《东方红》播放器

Scroll down

一年半前我买了一块AG1280Q48开发板,想要借此学习Verilog HDL,但是苦于一直在忙学校的事情一直搁置在一边。最近放暑假,时间也稍微宽松了一点,故重新把它捡起来,用它制作了一个《东方红》播放器。本文是我对分析与编写过程的一些记录。

本文所述代码见Github:Github仓库。效果见B站:B站链接

分析

我将将要实现的播放器划分为3个模块:

  • 节奏发生器
  • 音高发生器
  • 波形发生器

节奏发生器由时钟信号驱动,输出演奏音符的脉冲信号,每当需要演奏下一个音符的时候,输出一个方波脉冲。音高发生器由演奏脉冲驱动,输出音高编码,每当收到一个演奏脉冲时,输出下一个需要演奏的音高的编码值。波形发生器将音高发生器的输出编码作为输入,根据编码选择输出波的频率并输出波形。

波形发生器输出一定频率的方波,通过IO引脚引出,作为声音信号传递给扬声器,并由扬声器转化为声波。

波形发生器

波形发生器使用分频器将输入时钟信号频率转变为需要的声波频率。然后其通过数据选择器将音高编码所代表的频率输出至IO引脚。波形发生器的定义位于wave_generator.v

音律

人耳对音高的感知取决于声波的频率。我们通过对输出方波频率的控制来演奏不同音高的音符。根据人耳的对数特性,音符的调值每升高八度,频率就会随之翻一番。而对于音阶的相邻音符,频率随调值的变化有两种划分方法:十二平均律和五度相生律(纯律与五度相生律原理一致,不做赘述)。

十二平均律将每八度音程按照频率的对数平均划分,每一个等分为一个半音音程,即每两个相邻半音之间频率之商相等,为2的1/12次方。

五度相生律将纯五度频率定为3:2,向两侧不断相生出新的音,再将其乘以2的整数幂归一到一个八度内,故每两个音的频率之商皆为2和3的整数幂相乘。

分频器实现

在东方红中总共使用了9种调值的音符,low5,low6,low7,1,2,3,5,6,high1,为了能更方便地采用分频器得到每个音高的频率,我采用了五度相生律,并将中央A设定为432Hz,只要计算得出这9个音频率的最小公倍数,即可通过整数分频系数得到这9个音的频率。

通过计算可得,9个频率的最小公倍数为124416,从low5到high1分频系数分别为648,576,512,486,432,384,324,288,243。

分频器的定义如下,使用一个计数器,每收到一个时钟脉冲时计数器累加1,当累加到设定数值时反转输出极性,得到一个可以整数分频的分频器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module clkdivider #( parameter CNT_MAX = 16'd2 )
(
input clk,
input nRST,
output reg clkout
);
reg [15:0] cnt;

always @( posedge clk, negedge nRST ) begin
if( !nRST ) begin
cnt <= 16'd0;
clkout <= 1'b0;
end else begin
if ( cnt < CNT_MAX/2-1 ) begin
cnt <= cnt + 16'd1;
end else begin
cnt <= 16'd0;
clkout <= ~clkout;
end
end
end

endmodule

其中#( parameter CNT_MAX = 16'd2 )是一个模版语法,可以通过给parameter赋予不同的值得到不同分频系数的分频器。注意parameter是在编译期就已经确定的,会固定在线路里,也就是这个分频器的频分值并不能根据外部信号动态地改变。

所以我们要生成9个不同频率的分频器,根据编码信号选择该当的分频器输出。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
wire CLK_124K,CLK_166K;
wire lowgo,lowao,lowbo,co,do,eo,go,ao,highco;
reg mode,freqclk;

clkdivider #(.CNT_MAX(16'd96)) clk124gen (.clk(clk12M), .nRST(Rst), .clkout(CLK_124K));//96=C1,72=F1
clkdivider #(.CNT_MAX(16'd72)) clk166gen (.clk(clk12M), .nRST(Rst), .clkout(CLK_166K));

clkdivider #(.CNT_MAX(16'd648)) lowg (.clk(freqclk), .nRST(Rst), .clkout(lowgo));
clkdivider #(.CNT_MAX(16'd576)) lowa (.clk(freqclk), .nRST(Rst), .clkout(lowao));
clkdivider #(.CNT_MAX(16'd512)) lowb (.clk(freqclk), .nRST(Rst), .clkout(lowbo));
clkdivider #(.CNT_MAX(16'd486)) c (.clk(freqclk), .nRST(Rst), .clkout(co));
clkdivider #(.CNT_MAX(16'd432)) d (.clk(freqclk), .nRST(Rst), .clkout(do));
clkdivider #(.CNT_MAX(16'd384)) e (.clk(freqclk), .nRST(Rst), .clkout(eo));
clkdivider #(.CNT_MAX(16'd324)) g (.clk(freqclk), .nRST(Rst), .clkout(go));
clkdivider #(.CNT_MAX(16'd288)) a (.clk(freqclk), .nRST(Rst), .clkout(ao));
clkdivider #(.CNT_MAX(16'd243)) highc (.clk(freqclk), .nRST(Rst), .clkout(highco));

我首先生成了两个预分频器,分别生成124kHz和166kHz的待分波,方便变换C调和F调。然后将预分频器的输出作为输入送入9个用于生成音高频率的分频器。

这里的Rst信号用于控制分频器是否输出波。我会用它来分隔音符,这样遇到连续两个音高相同的音符就不会连成一个了。

数据选择器实现

数据选择器的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
always @(*)
begin
case (tune)
4'h0: wave=lowgo;
4'h1: wave=lowao;
4'h2: wave=lowbo;
4'h3: wave=co;
4'h4: wave=do;
4'h5: wave=eo;
4'h6: wave=go;
4'h7: wave=ao;
default: wave=highco;
endcase
if(mode==1'b0) freqclk=CLK_124K;
else freqclk=CLK_166K;
end

首先是实现了一个9选一的数据选择器,然后还有一个根据mode值切换预分频波实现音调切换的数据选择器。

音高发生器

音高发生器的代码位于tune_generator.v,用于生成东方红的音高序列,并以音高编码的形式输出。

音高编码

由于只用到了9个音高,故用4位2进制数代表一个音高,即一个音高编码会用到4根线。

由于序列总共包含40个音,故使用一个160线的数组代表音高序列。

音高发生器实现

代码如下:

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
module tune_generator(
input trigger,
input rst,
output reg [3:0] tune_code
);

/**
* Tune Code:
* low G: 0
* low A: 1
* low B: 2
* C: 3
* D: 4
* E: 5
* G: 6
* A: 7
* high C: 8
*/

wire [159:0] music;
reg [8:0] counter;

assign music=160'h0667433146678763314643210645433145434321;

always @(negedge trigger,negedge rst)
begin
tune_code[3]<=music[counter];
tune_code[2]<=music[counter-1];
tune_code[1]<=music[counter-2];
tune_code[0]<=music[counter-3];
if(rst==1'b0) counter=9'd159;
else if(counter>9'd3) begin counter<=counter-9'd4; end
else begin counter <= 9'd159; end

end

endmodule

其中实现了4个160选1数据选择器,根据计数器counter的值选出数组music对应位置的编码值送给输出。数组music代表整张曲谱的音符序列,每4根线代表一个音符。每次收到来自trigger的脉冲时,counter自增4,从而将输出移动到下4根线。

节奏发生器

节奏发生器与音高发生器原理类似,用一张表保存曲谱每一个音的时值,然后用计数器和数据选择器去遍历。

节奏编码

整张曲谱用到了3种时值:8分音符,4分音符和2分音符,我将其分别编码为00,01,10。

我2分音符的时长作8等分作为输入时钟频率,这样8分音符,4分音符和2分音符应当分别占2,4,8个时钟周期。

节奏发生器实现

代码如下:

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
module tick_controller(
input clk4hz,
input rst,
output reg tick
);

reg [8:0] counter;
reg [3:0] pulse_counter;
wire [79:0] music;

assign music=80'h42425004254150400002;

always @(negedge clk4hz,negedge rst) begin

if(rst==1'b0) begin counter=9'd79; pulse_counter=4'd7; tick<=1'b1; end
else if(pulse_counter<4'd7) begin pulse_counter<=pulse_counter+4'd1; tick<=1'b1; end
else begin
case({music[counter],music[counter-1]})//reset the pulse counter
2'b00: pulse_counter<=4'd6;
2'b01: pulse_counter<=4'd4;
2'b10: pulse_counter<=4'd0;
default: pulse_counter<=3'd2;
endcase
tick<=1'b0;
if(counter>9'd1) begin counter<=counter-9'd2; end
else begin counter <= 9'd79; end
end

end

endmodule

与音高发生器类似,counter用于遍历时值序列music。每遍历一个音符时,节奏发生器根据该当的节奏编码决定计数器pulse_counter的重装值,重装值决定了pulse_counter需要花多少个时钟周期才能累加到7,每当累加到7时,输出一个下降沿脉冲,并使counter加1,从而进入到下一个音符。

组装

芯片侧

实现了所有必要的组件后,我们在顶层模块中将它们的信号连接起来:

1
2
3
4
5
   wire[3:0] tune_wire;
wire music_tick;
tick_controller dfh_jiezou(.clk4hz(CLK_4Hz),.rst(KEY[2]),.tick(music_tick));
tune_generator dfh(.trigger(music_tick),.rst(KEY[2]), .tune_code(tune_wire));
wave_generator wav(.tune(tune_wire), .clk12M(CLK_12M), .Rst(music_tick), .switcher(KEY[1]), .wave(BENCH_OUT));

节奏发生器接收4Hz时钟(2分音符时长占2s,8分后即4Hz),并将节奏脉冲给音高发生器和波形发生器。音高发生器按照序列将音高编码依次输出给波形发生器,而波形发生器将音高编码转化为声波信号后输出到声音信号引脚上。

将程序编译、综合、下载到芯片中,即可得到一个输出东方红乐曲波形的电路。

输出侧

为了防止IO口的输出功率不足,我用一根单管电压跟随器对电流进行放大后接入扬声器,电路图如下:
扬声器电路

总结记录

  1. Verilog中使用加法器需要注意符号位,在为加法器分配线组的时候注意位宽,不要让进位标志溢出。
  2. 注意计算复位后各组件时序。本实现由于音高发生器领先节奏发生器一个音符,需要在编谱时将音高发生器整体向后环移一个音符。
  3. AG1280工程创建时PLL类型应选择PLLX,AGMpill开发板板载的振荡器为24MHz,IP配置应如图:
    PLL配置
  4. 如果在中途创建IP,需要重新Migrate工程,工程配置如图:
    工程配置
其他文章