一年半前我买了一块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)) ; 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 ); 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 ]}) 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口的输出功率不足,我用一根单管电压跟随器对电流进行放大后接入扬声器,电路图如下:
总结记录
Verilog中使用加法器需要注意符号位,在为加法器分配线组的时候注意位宽,不要让进位标志溢出。
注意计算复位后各组件时序。本实现由于音高发生器领先节奏发生器一个音符,需要在编谱时将音高发生器整体向后环移一个音符。
AG1280工程创建时PLL类型应选择PLLX,AGMpill开发板板载的振荡器为24MHz,IP配置应如图:
如果在中途创建IP,需要重新Migrate工程,工程配置如图: