Jul 22

Secret of Monkey Island on NodeMCU - ESP8266

Category: Design,electronics   — Published by goeszen on July 22, 2017 at 8:26 pm

Today, we'll have a stab at playing a little chiptune on the NodeMCU ESP8266 dev board, via a passive buzzer. If you've got a soft spot for 90s RTTTL ring-tones, or calculator beeping - you've come to the right place.

Most Arduino sketches found on the Internets which generate sounds (actually all I found) rely on some form of a "busy loop". That means the main part of a program is an endless loop with a number of steps to execute. And each of those steps blocks the loop for as long as it takes to finish the task. In this post we'll explore how this can be avoided, but first things first:

In order to play a sound, on a microcontroller, in general, we need to modulate the current on one of the development board's pins. What exactly is being done can be understood by having a look at an excerpt from this Arduino sketch:

void buzz(int targetPin, long frequency, long length) {
  
  digitalWrite(6, HIGH);
  
  long delayValue = 1000000 / frequency / 2;
  long numCycles = frequency * length / 1000;
  
  for (long i = 0; i < numCycles; i++) {
    digitalWrite(targetPin, HIGH);
    delayMicroseconds(delayValue);
    digitalWrite(targetPin, LOW);
    delayMicroseconds(delayValue);
  }
  
  digitalWrite(6, LOW);
}

Here, a frequency is calculated first and then a target pin alternatingly set to HIGH and LOW, forming a sine wave of a certain frequency.

There are two things to note here. Number on: on the ESP8266, in NodeMCU's lua firmware, we have the option to rely on the pwm. module, which does what the above code does, internally. You just tell it to start emitting a modulation on some pin, and when to stop.

The other thing is: in Arduino C-style sketches, it's mostly expected of you to write a busy loop. But in LUA, and more specific for the ESP8266, you must avoid it at all cost! The manual is quite clear about that, in multiple places, for example here.

So, follwing that paradigm, for our implementation here, I tried to take this to heart and implement a simple music player without a busy loop or any other blocking code. Many other snippets or examples of buzzing a speaker via the ESP8266 don't go this extra mile and are merely 1:1 conversions of Arduino code, including busy loops! If you go back to one of the past posts from the goeszen-series on ESP8266, for example Buzzer and OLED on NodeMCU, then you'notice that we've commited this "sin" as well. Have a look at the code sction where we sound the buzzer, on button press etc., and you'll see what I mean:

function beep(pin, freq, duration)
    print ("beep!")

    pwm.setup(pin, freq, 512)
    pwm.start(pin)
    -- delay in uSeconds
    tmr.delay(duration * 1000)
    pwm.stop(pin)
    --20ms pause
    tmr.wdclr()
    tmr.delay(20000)
end

As you can see, there's a timer.delay() call, and even a tmr.wdclr() call, in a blocking command sequence! Both must be avoided at all cost - as per the docs. Yet, it's there, works, and doesn't crash the WiFi stack (just yet, though). This code snipped is from ubiquitious exmaples on the Net. Everyone does it, we as well here. Arg.

Let's try this again, this time with timers. The implementation I made for a callback-based approach of playing notes splits the note-on and note-off operation in two calls, handing back control to the CPU in-between. The code, a cascade of re-entrant calls is kicked-off by calling position() and flares out into timed calls to note_on() and note_off(). Have a look:

-- play a melody, accompanied by stuff on attached OLED
-- CC BY 3.0, except melody, copyright Michael Land, Lucasfilm, Disney
-- https://creativecommons.org/licenses/by/3.0/

sda = 1 -- oled's SDA pin connected to board Pin 1
scl = 2 -- olde's SCL Pin connected to board Pin 2
buzzer_pin = 5

note = {
	["b0"] = 246,
	["c"] = 261,
	["c"] = 261,
	["d"] = 294,
	["e"] = 329,
	["f"] = 349,
	["fS"] = 369,
	["g"] = 392,
	["gS"]= 415,
	["a"] = 440,
	["aS"]= 455,
	["b"] = 493,
	["C"] = 523,
	["CS"]= 554,
	["D"]= 587,
	["DS"]= 622,
	["E"]= 659,
	["F"]= 698,
	["FS"]= 740,
	["G"]= 784,
	["GS"]= 830,
	["A"]= 880
}

function init_OLED(sda,scl) --Set up the u8glib lib
     sla = 0x3C
     i2c.setup(0, sda, scl, i2c.SLOW)
     disp = u8g.ssd1306_128x64_i2c(sla)
     disp:setFont(u8g.font_6x10)
     disp:setFontRefHeightExtendedText()
     disp:setDefaultForegroundColor()
     disp:setFontPosTop()
     disp:setRot180()           -- Rotate Display if needed (we do)
end

function write_OLED(value) -- Write Display
   disp:firstPage()

	local pixel = (value - 200) / 3
	if pixel > 64 then
		pixel = 64
	end

        print (" oled pixel ".. pixel)
   repeat
     disp:drawLine(0, pixel, 128, pixel)
   until disp:nextPage() == false
end

function clear_OLED()
   disp:firstPage()
   repeat
   until disp:nextPage() == false
end

function note_on(freq)
	pwm.setup(buzzer_pin, freq, 512)
	pwm.start(buzzer_pin)
end

function note_off()
        print (" note_off")
	pwm.stop(buzzer_pin)

	tmr.stop(1)
	collectgarbage() -- why????

	index = index + 1

        print (" pos next if".. index)
	position(index)
end

function position(index)
   print ("position, index ".. index)

   if index <= #melody_notes then
	note_duration = tempo_base / melody_duration[index]

	if melody_notes[index] == "r" then
	        print (" melody_note: REST for".. note_duration .."ms")

		tmr.alarm(1, note_duration, 1, note_off)
	else
	        print (" melody_note: ".. melody_notes[index] .." is ".. note[melody_notes[index]] .."Hz for ".. note_duration .."ms")
		note_on( note[melody_notes[index]] )

		tmr.alarm(1, note_duration, 1, note_off)

		write_OLED( note[melody_notes[index]] )
	end
   else
	print("STOP")

	clear_OLED()
   end

end


-- Main Program

-- init GPIO pins properly
gpio.mode(buzzer_pin, gpio.OUTPUT)

init_OLED(sda,scl)

tempo = 120 -- at 120BPM, a 4's (quarter note) duration is 500ms
tempo_base = (60 / tempo) * 4000
print("tempo_base (a whole note) is ".. tempo_base .."ms long")
melody_notes    = { "e", "r", "e", "g", "fS", "e", "d", "e", "r", "d", "r", "d", "c", "b0", "d", "c", "c", "b0"}
melody_duration = { 16,  8,    4,   8,   8,    8,   8,   4,   4,   16,  8,   8,   8,   8,    8,   4,   4,   2 }

index = 1

position(index)

The first part is more or less the same as in all "buzzing the buzzer" Arduino scripts: declaring notes and their frequencies. This is followed by some eye-candy code to trigger something on the OLED. Followed by the timer callbacks, and finalised with the actual main part, where we start the playback and some efficient code to calculate musically correct tempo based on BPM. Two arrays hold the melody, separated into notes and duration data.

And while the script behaves nicely by adhering to the no-busy-loops paradigm, it does something else very wrong. It works, but only once. There's some memory leak in there I'm overseeing. The fact that I had to call collectgarbage() manually, to prevent it from crashing was a first evidence. The, running it multiple times without a hard reset is not possible - "out of memory"!

But as this is meant as a quick hack only, I haven't invested the time to actually stare at the code for long enough to find what's wrong here. Comments welcome.

I'm tinkering here with a NodeMCU ESP8266 board.
I'm using the ESP8266 Java IDE ESPlorer.
The firmware is version 0.9.6 build 20150704 powered by Lua 5.1.4

Leave a Reply

=