Four-key Piano on Fipsy FPGA

The newest addition to yoursunny.com's toy vault is Fipsy FPGA Breakout Board, a tiny circuit board offering a piece of Lattice MachXO2-256 field-programmable gate array (FPGA). After porting an SPI programmer to ESP32, it's time to write some Verilog! Blinky is boring, but I did it anyway. Then, I'm moving on to better stuff: a piano.

The piano is an acoustic music instrument played using a keyboard. When a key is pressed, a hammer strikes a string, causing it to resonate and produce sound at a certain frequency. A normal piano has 88 keys, and each key has a well-defined sound frequency. My "piano", built on Fipsy, has four keys, and uses a passive buzzer to produce sound.

Fipsy FPGA connected to a buzzer and a keypad

Play Tone on Passive Buzzer with FPGA

A passive buzzer plays a tone controlled by an oscillating electronic signal at the desired frequency. In Arduino, the tone() function generates a square wave of a specified frequency, which can be used to control a passive buzzer.

The waveform of square wave looks like this:

waveform of square wave, derived from https://commons.wikimedia.org/wiki/File:Waveforms.svg by Omegatron, CreativeCommons BY-SA 3.0 license

The following Arduino code produces a square wave:

void setup() {
  pinMode(2, OUTPUT);
}
void loop() {
  digitalWrite(0, HIGH);
  delay(500);
  digitalWrite(0, LOW);
  delay(500);
}

Wait, isn't it the blinky? Yes, it is. This code produces a 1Hz square wave, which can blink an LED once a second. However, if you control a passive buzzer with this signal, chances are that you won't hear anything, because 1Hz sound is out of human's hearing range. To produce an audible sound, you'll need a square wave with higher frequency.

The "middle C" on a piano has a frequency of 261.626 Hz. Arduino can't produce this frequency with digitalWrite() and delay(), because the CPU is not fast enough. Fipsy, on the other hand, can do so in the same way as blinky. Replacing the 20-bit counter with a 13-bit counter, you'll get a 253.906 Hz square wave, which is quite close to the "middle C":

wire [12:0] Q;
FreqDiv13Bit d(.Clock(INTERNAL_OSC), .Clk_En(1'b1), .Q(Q)); // Counter module from Tools-IPexpress
assign PIN10 = Q[12];

To obtain a more accurate frequency in the square wave, we'll need a counter and a comparator:

input wire clk; // 2.08MHz clock
output reg spk; // passive buzzer
reg [12:1] cnt; // the counter
initial begin
  cnt <= 0;
  spk <= 0;
end
always @(posedge clk) begin
  // (2.08MHz / 261.626Hz) / 2 = 3975
  if (cnt >= 3975) begin
    cnt <= 0;
    spk <= ~spk; // toggle buzzer signal when counter overflows
  end else begin
    cnt <= cnt + 1; // increment the counter
  end
end

The "magic number" 3975 is calculated from the clock frequency and desired frequency. Fipsy's internal oscillator, by default, runs at 2.08MHz. The frequency of "middle C" is 261.626Hz, or 1/7950 of the oscillator frequency. If we count from 0 to 7949, the Most Significant Bit (MSB) would toggle between 0 and 1 at 261.635Hz frequency. However, the duty cycle of this signal would 48%, because MSB is 0 when the counter reads 0-4095, and becomes 1 when the counter reads 4096-7949. Ideally, a square wave driving a passive buzzer should have a 50% duty cycle, i.e. spend roughly the same time in 0 and 1 states. To achieve that, the code above counts from 0 to 3975 (half of 7950), producing a signal at 523.270Hz. Then, spk <= ~spk line divides this signal by half, generating a square wave of 261.635Hz frequency and 50% duty cycle.

Read the Keypad

I have a "4-key button module" with pullup resistors. Each button has a pin providing an "active low" digital signal. In Arduino, digitalRead(11) is normally 1, and becomes 0 when the button is pressed. In Verilog, the same logic can be written as assign key1 = ~PIN11;, where ~ is the bitwise negation operator.

Each of the four keys plays a different piano note:

key note frequency divisor
1 (red) C4 261.626 Hz 3975
2 (blue) E4 329.628 Hz 3155
3 (yellow) G4 391.995 Hz 2653
4 (green) C5 523.251 Hz 1987

While I could instantiate four counters to generate square waves for these four frequencies and use a multiplexer to select a signal, this solution would consume a lot of logical resources in the FPGA. Instead, I have only one counter, along with a comparator loaded with a "divisor" computed using the method in the previous section:

input wire clk; // 2.08MHz clock
output reg spk; // passive buzzer
reg [12:1] cnt; // the counter
reg [12:1] div; // the current divisor
initial begin
  cnt <= 0;
  spk <= 0;
end
always @(posedge clk) begin
  if (key1) begin
    div <= 3075;
  end else if (key2) begin
    div <= 3155;
  end else if (key2) begin
    div <= 2653;
  end else if (key4) begin
    div <= 1987;
  end

  if (cnt >= div) begin
    cnt <= 0;
    spk <= ~spk; // toggle buzzer signal when counter overflows
  end else begin
    cnt <= cnt + 1; // increment the counter
  end
end

The "enable" Signal

At the moment, the piano still has a big problem: it keeps playing the same tone even after I release the key! To solve this problem, we need an "enable" signal:

assign enable = key1 | key2 | key3 | key4;
assign PIN10 = enable ? spk : 1'bZ;

The square wave goes to the buzzer if any of the keys is pressed. Otherwise, the signal remains in "high-impedance state", so the buzzer does not sound.

Reduce Volume

I live in an apartment building. I don't want my loud buzzer to disturb neighbors. I tried to put a resistor on the buzzer's signal line, but it does not reduce the volume. However, changing the square wave's duty cycle seems to be effective: setting a duty cycle away from 50% reduces the volume, because the buzzer cannot resonate as much.

input wire clk;     // 2.08MHz clock
input wire enable;  // whether to enable output
output wire buzzer; // passive buzzer
reg [10:1] cnt;     // frequency divider
reg [3:1] spk;      // volume reduction counter
initial begin
  cnt <= 0;
  spk <= 0;
end
always @(posedge clk) begin
  // (2.08MHz / 261.626Hz) / 8 = 993
  if (cnt >= 993) begin
    cnt <= 0;
    spk <= spk + 1;
  end else begin
    cnt <= cnt + 1;
  end
end
assign buzzer = enable ? (spk < 1) : 1'bZ;

spk is now a 3-bit counter, so that buzzer's signal is at "1" in 1/8 of the time, i.e. a duty cycle of 12.5%.

Video Demo and Complete Code

View Fipsy 4-key piano code on GitHub. The complete code is divided into multiple modules, and selects a higher internal oscillator frequency for more accurate tone frequency.

Tags: FPGA Fipsy