基于小脚丫FPGA的电子琴设计
本项目完成了基于小脚丫FPGA的电子琴的开发,实现了弹奏音乐,自动播放音乐,切扬声器和蜂鸣器播放等功能,同时,实现了至多8个音调的和弦播放和通过额外按键拓展音域的功能。
标签
FPGA
DDS
2022暑假在家练
2022暑假
smallcracker
更新2022-09-02
哈尔滨工业大学
1050

一、电子琴的工作原理和框图

物体震动产生声音,给无源蜂鸣器通入一定频率的方波信号,或者给扬声器通入一个波动的电信号,都可以发出声音。通入多少频率的电信号,发声装置就能发出多少频率的声音。信号的产生由FPGA来完成。FPGA还接有若干按键,其中13枚控制音调,2个进行音域拓展,1个用于切换扬声器/蜂鸣器播放,一个用于开启自动播放。

本项目基于硬禾的小脚丫FPGA和电子琴拓展板来完成,系统的硬件结构框图表示为:

系统的软件结构框图为:

二、蜂鸣器和模拟喇叭的差别

1. 蜂鸣器是一种一体化的电子讯响器,采用直流电压供电,有压电式蜂鸣器和电磁式蜂鸣器两种类型,它们又各有两种结构:有源型和无源型。这里,源指的是振荡源,有源蜂鸣器只需通入直流信号即可发声,而无源蜂鸣器则需要通入一定频率的方波信号才能发声。

2. 扬声器也是一种换能元件,俗称喇叭,有电动式,压电式,舌簧式几类。扬声器需要交流信号来进行驱动,交流信号的频谱对应发声的频谱。
三、用模拟蜂鸣器和模拟喇叭的实现方法以及音效差别分析

针对蜂鸣器,我们只需使用一个三极管即可通过IO口进行控制,希望得到多少频率的声音,就要给蜂鸣器通入多少频率的方波信号。蜂鸣器发出的声音是方波信号的声音

针对扬声器,需要用FPGA生成PWM波,然后滤波后得到交流音频信号。这样的到的音频信号理论上可以得到扬声器频率范围内的任意频谱

四、模拟放大电路的仿真及分析

FPGA的IO口输出的PWM波,首先经过R16,R17,C3构成的低通滤波器,滤除高频的噪声,然后进入音频放大环节,音频放大环节有一个带通的有源滤波器,可以放大一定频率的信号。8002B是一颗专用的音频放大IC,可以将信号减去二分之一电源电压后输出(相当于去除直流分量)。经过电脑仿真,在20-20kHz频段(即人耳可以听到的频率范围内)的增益和相位如图所示。可以看出在200Hz以下的声音增益要远小于中频增益。


五、主要代码片段及说明

pwm生成

PWM波生成模块借鉴了硬禾的程序,使用一个累加器,将一个脉冲分散在一个周期内,从而充分利用了时钟频率,提高了方波的频率,为之后滤波提供便利。这里我们采用的PWM精度为7位。精度的选择主要考虑了FPGA的时钟频率(12MHz),预期的发声频率范围(130-1050Hz),平衡PWM精度和

module pwm (
    clk,rst_n,pwm_in,pwm_out
);
    input clk;
    input rst_n;
    input [6:0] pwm_in;
    output pwm_out;

    reg [7:0] cnt;

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin
            cnt <= 0;
        end
        else cnt <= cnt[6:0] + pwm_in;
    end
    assign pwm_out = cnt[7];
endmodule

自动播放模块-autoplay

自动播放模块使用了软件提供的ROM ip核来实现。

分频器模块

参考了硬禾的例程,主要为自动播放模块提供一个频率更低的时钟。相比简单的累加器分频,可以实现精准的奇数分频。

module divide(clk, rst_n, clk_out);
    input clk;
    input rst_n;
    output clk_out;

    parameter WIDTH = 16;
    parameter N = 40000;

    reg [WIDTH:0] cnt_n,cnt_p;
    reg           clk_p,clk_n;

    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin
            cnt_p <=0;
        end
        else if(cnt_p==(N-1))begin
            cnt_p <= 0;
        end
        else begin
            cnt_p <= cnt_p + 1;
        end
    end
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_p <= 0;
        end
        else if (cnt_p>(N>>1)) begin
            clk_p <= 1;
        end
        else begin
            clk_p <= 0;
        end
    end
    always @(negedge clk or negedge rst_n) begin
        if(!rst_n)begin
            cnt_n <= 0;
        end
        else if(cnt_n==(N-1))begin
            cnt_n <= 0;
        end
        else begin
            cnt_n <= cnt_n + 1;
        end
    end
    always @(negedge clk or negedge rst_n) begin
        if (!rst_n) begin
            clk_n <= 0;
        end
        else if(cnt_n>(N>>1))begin
            clk_n <= 1;
        end
        else begin
            clk_n <= 0;
        end
    end
    assign clk_out = (N==1)?clk:(N[0]?(clk_p|clk_n):clk_p);
endmodule

 主要功能模块

波表的生成使用c语言生成带有谐波的波形信号。查找资料得到了小提琴的各谐波,然后累加各个正弦波信号得到类似小提琴的声音。在实际测试过程中听感与小提琴具有一定类似程度,但未设计包络,因此声音幅值缺乏变化,同时实际的乐器各个音调的各个谐波占比并不相同,即使是同一音调,在发声初和后期的谐波占比也有所不同,这些变化仍有待实现。

代码中可以看出,我在这刻意加强了二次、三次和五次谐波的强度,这将在频谱图中有所体现。

#include<cmath>
#include<stdio.h>
#include<iostream>
#include<stdlib.h>
using namespace std;
#define STEP 3.1415926*2/128
double am[10]={1.0, 0.286699025, 0.150079537, 0.042909002, 
	0.203797365};
double temp[200];
int main(){
	freopen("out4.mem","w",stdout);
	char string[33];
	char dest[33];
	double max=-1;
	for(int i=0;i<128;i++){
		for(int j=0;j<=4;j++){
			temp[i]+=sin((STEP*i*(j+1)))*am[j];
		}
		max = max<temp[i]?temp[i]:max;
	}
	for(int i=0;i<128;i++){
		itoa((int)(temp[i]/max*63)+64, string, 2);
		sprintf(dest,"%7s", string);
		for(int i=0;i<=6;i++){
			if(dest[i]==' ')dest[i]='0';
		}
		printf("7'b%s,\n",dest);
	}
	fclose(stdout);
}

代码中设计了四次谐波,使用软件phyphox观察发声的频谱如下,在频谱中可以清晰地看见设置的基波和共计4个谐波,其中二三五次谐波较强,同程序中的设置相符。基波的强度反而不如谐波的强度,可能与上文分析到的低频信号增益较低有关。

声音频谱

主要基于system verilog的parameter数组特性来构建。在之前的尝试过程中,曾经尝试使用ROM ip来存储波表,考虑到和弦的需求,需要为每一个按键实例化一个module,共计37个,但是ebr只有十个,不足以实现如此多的ROM,因此尝试使用parameter数组。

    parameter [6:0] ARRAY [127:0] = {
        7'b1000000,
        7'b1001001,
        7'b1010010,
        7'b1011010,
        7'b1100010,
        7'b1101001,
        //这里省略中间部分的波表,具体内容见附件工程文档中的代码
        7'b0110111
    };
    parameter [9:0] FREQ [36:0] = {
        10'd716,
        10'd676,
        10'd638,
        10'd602,
        10'd568,
        10'd536,
        10'd506,
        10'd478,
        10'd451,
        10'd426,
        10'd402,
        10'd379,
        10'd358,
        10'd338,
        10'd319,
        10'd301,
        10'd284,
        10'd268,
        10'd253,
        10'd239,
        10'd225,
        10'd213,
        10'd201,
        10'd189,
        10'd179,
        10'd169,
        10'd159,
        10'd150,
        10'd142,
        10'd134,
        10'd126,
        10'd119,
        10'd112,
        10'd106,
        10'd100,
        10'd94,
        10'd89
    };//各个音调的定时器参数

具体播放时,使用37组如下代码完成音调的生成:

    wire clk_out [36:0];
    reg [6:0] b [36:0];
    reg [9:0] pwm_in_buf;
    reg [9:0] cnt [36:0];
    reg [7:0] idn [36:0];
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin
            cnt[0] <= 0;
            idn[0] <= 0;
        end
        else if(cnt[0] == FREQ[0]) begin
            cnt[0] <= 0;
            idn[0] <= idn[0] + 1;
            b[0] <= ARRAY[idn[0]];
        end
        else cnt[0] <= cnt[0] + 1;
    end
    always @(posedge clk or negedge rst_n) begin
        if(!rst_n)begin
            cnt[1] <= 0;
            idn[1] <= 0;
        end
        else if(cnt[1] == FREQ[1]) begin
            cnt[1] <= 0;
            idn[1] <= idn[1] + 1;
            b[1] <= ARRAY[idn[1]];
        end
        else cnt[1] <= cnt[1] + 1;
    end
//此后省略其他音调的生成代码,具体查看附件工程文件中的代码

自动播放

相关的代码如下,在乐曲的最后加入一个零音调,初始化和播放完成后都停止在这个最后的音调,触发播放之后,乐曲从头播放,最终也会在最后一个音调停下来。pnt是乐谱索引,key_auto是输出的音调。这里自动播放和手动弹奏输出的音调进行与运算,因此可以同时进行。这里我加载了三首音乐,可以通过按键进行切换。

    wire pulse;
    reg [12:0] pnt;
    wire [36:0] key_auto;
    wire [36:0] key_syn;
    divide #(.N(1500000)) w1(.clk(clk),.rst_n(rst_n),.clk_out(pulse));
    always @(posedge pulse or negedge rst_n or negedge auto[0] or negedge auto[1] or negedge auto[2]) begin
        if (!rst_n) begin
            pnt <= 13'd0;
        end
        else if(!auto[0])begin
            pnt <= 1;
        end
        else if(!auto[1])begin
            pnt <= 13'd160;
        end
        else if(!auto[2])begin
            pnt <= 13'd993;
        end
        else if(pnt == 13'd0)begin
            pnt <= 13'd0;
        end
        else if(pnt == 13'd159)begin
            pnt <= 13'd159;
        end
        else if(pnt == 13'd992)begin
            pnt <= 13'd992;
        end 
        else if(pnt == 13'd1736)begin    
            pnt <= 13'd1736;
        end
        else pnt <= pnt + 1;
    end
    autoplay r1(.Address(pnt),.OutClock(clk),.OutClockEn(1'b1),.Reset(1'b0),.Q(key_auto));

按键滤波模块

参考了硬禾的例程,在检测到按键按下之后,触发一个计时器,定时获取按键状态,若按键和之前一段时间的按键状态均为按下,则认为按键确实按下,从而实现了按键的滤波。

module debounce (
    clk,rst_n,key,key_out
);
  parameter N = 2;
  input clk;
  input rst_n;
  input [N-1:0] key;
  output [N-1:0] key_out;
  wire [N-1:0] key_edge;

  reg [N-1:0] key_rst_pre;
  reg [N-1:0] key_rst;

  reg [N-1:0] key_hes_pre;
  reg [N-1:0] key_hes;

  //update key_rst_pre and key_rst
  always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        key_rst <= {N{1'b1}};
        key_rst_pre <= {N{1'b1}};
    end
    else begin
        key_rst <= key;
        key_rst_pre <= key_rst;
    end
  end
  //get key_edge, key_edge give a pulse when key state changes
  assign key_edge = key_rst_pre&(~key_rst);
  //start to count when a key_edge comes
  reg [17:0] cnt;
  always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        cnt <= 0;
    end
    else if(key_edge)begin
        cnt <= 0;
    end
    else if(cnt == 18'd240001)begin
        cnt <= 0;
    end
    else begin
        cnt <= cnt + 1;
    end
  end
  //update key_hes and key_hes_pre
  always @(posedge clk or negedge rst_n) begin
    if(!rst_n)begin
        key_hes <= {N{1'b1}};
        key_hes_pre <= {N{1'b1}};
    end 
    else if (cnt == 18'd240000) begin
        key_hes <= key;
        key_hes_pre <= key_hes;
    end
  end
  //get key_out from key_hes and key_hes_pre
  assign key_out = key_hes&key_hes_pre;
endmodule

顶层模块

完成按键的滤波功能、音域的拓展功能(其实就是把13个按键映射到37个音调上去),以及扬声器和蜂鸣器的切换功能:

module transform(
    clk, rst_n, sw, tr, key, auto,speaker, buzzer
);
    input clk;
    input rst_n;
    input sw;
    input [1:0] tr;
    input [12:0] key;
    input [2:0] auto;

    wire speaker_buf;
    wire buzzer_buf;
    output speaker;
    output buzzer;

    wire [12:0] key_out;
    wire [2:0] auto_out;
    debounce #(.N(13)) de(.clk(clk),.rst_n(rst_n),.key(key),.key_out(key_out));
    debounce #(.N(3)) de1(.clk(clk),.rst_n(rst_n),.key(auto),.key_out(auto_out));
    wire [36:0] key_trans;
    assign key_trans[11:0] = key_out[11:0]|{12{~tr[0]}}|{12{tr[1]}};
    assign key_trans[12] = {key_out[12]|{~tr[0]}|tr[1]}&{key_out[0]|{~tr[0]}|{~tr[1]}};
    assign key_trans[23:13] = key_out[11:1]|{11{~tr[0]}}|{11{~tr[1]}};
    assign key_trans[24] = {key_out[12]|{~tr[0]}|{~tr[1]}}&{key_out[0]|tr[0]|{~tr[1]}};
    assign key_trans[36:25] = key_out[12:1]|{12{tr[0]}}|{12{~tr[1]}};
    svtest u1(
        .clk(clk),
        .rst_n(rst_n),
        .key(key_trans),
        .auto(auto_out),
        .speaker(speaker_buf),
        .buzzer(buzzer_buf)
    );
    assign buzzer = buzzer_buf&sw;
    assign speaker = speaker_buf&(~sw);
endmodule


六、改进建议

1. 更换按键声音更小的按键。当前在弹奏时按键会发出较大声音,影响听感。

2. 之后我会尝试加入包络功能。现在每个音调按下之后发声的幅度是恒定的,而诸如钢琴、吉他等乐器发出的声音是减弱的,在东方红卫星上的播放器上也同样有一个5Hz的低频载波,这些可以令声音听起来更加动听。

七、主要困难和解决方法

在开发过程中,基础模块因为功能简单,加之硬禾提供了大量的例程以供参考,所以开发起来非常顺利。但是将各个模块综合起来较为考验优化能力。在2022年春的FPGA项目中,ICE40UP5K的资源相对更丰富一些,开发的功能也更简单,所以没有体会到优化的重要性。但是在暑假在家一起练中,多次遇到了ebr不足和LUT不足的情况。

1.为每一个音调甚至是每一个按键声明一个ROM module的尝试是不现实的,STEP-MXO2-C只拥有10个ebr,所以改用LUT来保存使用的波表。具体是使用了system verilog的parameter参数数组功能。他可以帮助开发者更方便地使用LUT实现类似数组的功能。代价是要用掉相当多的LUT资源。

2.我保存乐谱的方法是使用一个37位的01串来表示某个音符是否发声,然后依次保存下每四分之一个节拍的发声的音符,我选择的乐曲这样保存下来将占用67,743 字节的空间。如果同样使用LUT资源来实现,将大大超出可使用的LUT资源数量。后来我该用了fpga的ROM来保存乐谱。解决了这个问题。

3.如何将midi文件转化成上述的乐谱文件:我曾尝试寻找解析midi文件的c,c++或者Python库,但是解析得到的数据往往需要经过繁复的处理才能转化成我需要的乐谱(主要工作包括将相对的时间转化成绝对的时间,然后吸附到节拍上),后来参考了群内大佬的解决方案,在网站onlinesequencer中完成了上述工作,同时借用了大佬的Python翻译工具,对其进行修改(主要是修改了音域,对两个相同音符之间添加了换气的停顿,修改了大小端序),实现了将midi文件翻译为可播放的乐谱的功能。

八、参考资料

在开发过程中,除了硬禾的资料外,许多博客、百科也给了我很多帮助,在这里一并列出,希望对其他进行类似开发的同学也能有所帮助。

音高和频率

Lattice ROM软核的使用

system verilog中parameter数组的使用

九、资源使用

Design Summary
   Number of registers:   1218 out of  4635 (26%)
      PFU registers:         1202 out of  4320 (28%)
      PIO registers:           16 out of   315 (5%)
   Number of SLICEs:      2081 out of  2160 (96%)
      SLICEs as Logic/ROM:   2081 out of  2160 (96%)
      SLICEs as RAM:            0 out of  1620 (0%)
      SLICEs as Carry:        551 out of  2160 (26%)
   Number of LUT4s:        4080 out of  4320 (94%)
      Number used as logic LUTs:        2978
      Number used as distributed RAM:     0
      Number used as ripple logic:      1102
      Number used as shift registers:     0
   Number of PIO sites used: 23 + 4(JTAG) out of 105 (26%)
   Number of block RAMs:  10 out of 10 (100%)
   Number of GSRs:  1 out of 1 (100%)
   EFB used :       No
   JTAG used :      No
   Readback used :  No
   Oscillator used :  No
   Startup used :   No
   POR :            On
   Bandgap :        On
   Number of Power Controller:  0 out of 1 (0%)
   Number of Dynamic Bank Controller (BCINRD):  0 out of 6 (0%)
   Number of Dynamic Bank Controller (BCLVDSO):  0 out of 1 (0%)
   Number of DCCA:  0 out of 8 (0%)
   Number of DCMA:  0 out of 2 (0%)
   Number of PLLs:  0 out of 2 (0%)
   Number of DQSDLLs:  0 out of 2 (0%)
   Number of CLKDIVC:  0 out of 4 (0%)
   Number of ECLKSYNCA:  0 out of 4 (0%)
   Number of ECLKBRIDGECS:  0 out of 2 (0%)

 

附件下载
工程文件.7z
内含所有代码,以及可以直接烧录的jed文件
团队介绍
许明辉 哈尔滨工业大学
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号