Arduino MIDI Slider OLED R2R Waveform Generator

This project moves on a step from my Arduino MIDI Slider R2R Waveform Generator and adds a small 0.96″ OLED I2C display.

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

These are the key projects for the main concepts used in this project:

If you are new to Arduino, see the Getting Started pages.

Parts list

  • Arduino Uno
  • R2R shield as described here
  • 0.96″ OLED I2C display (connections VCC, GND, SCL, SDA)
  • proto board shield
  • Amplification and speaker
  • MIDI In interface (such as one of the Ready-Made MIDI Modules)
  • Various headers, jumpers and connecting wires
  • Optional: components for the output stage and filter (will be detailed in a future post)

The Circuit

ArduinoOLEDMuxShield_bb

As I had decided to build a shield for the OLED display I thought it was also worth the effort adding a header that matched the pinouts for the mux too.  Note that I’ve included a header pin for the mux “enable” pin, even though I’m not using it – I’m relying on the jumper on the mux board to pre-set it to “enabled” – i.e. connected to GND.  If you are doing things “by hand” you might need to wire this header pin to GND or an IO pin if you want more control (although I’m rapidly running out of IO pins!).

I’ve had to use different IO pins for addressing the mux as A4 and A5 need to be used by the I2C link to the display, even though I’m wiring the display to the SCL/SDA pins on the digital side – we have to remember these are just breakouts from A4 and A5 on the Uno.

So assuming I’m still using the R2R shield from last time, the full IO usage is now as follows:

  • A0 – Mux analog input.
  • A4/A5 – SDA/SCL for I2C.
  • D13-D10 – Mux addressing pins.
  • D2-D9 – R2R DAC output.
  • D0/D1 – RX/TX for the MIDI link.
  • Plus 5V and GND of course.

If it looks like I’ve wired up the VCC/GND connections of the OLED wrong in the diagram above, you’re right!  The Fritzing part has pins in the order GND-VCC-SCL-SDA, but my board has the pins in the order VCC-GND-SCL-SCA – be sure to check yours!

Here are some photos of me building the OLED shield.

I started with the headers as shown above, opting to link GND to the nearest pin on the digital side of the Arduino.

Then I mounted and wired in the OLED display.  Note that with these cheap displays, the pinouts can vary.  Some have VCC-GND-SCL-SDA (like mine above), some have GND-VCC-SCL-SDA, so remember you need to check yours.  I did wonder about putting a female header here so I could plug in the OLED, but in the end opted to solder it directly to the proto board.

As this is designed to go on top of my R2R shield, and all the digital IO pins are now used, I didn’t bother with stacking headers on that side of the board.  I added stacking headers on the analog side in case I want to hook anything up to A1-A3 in the future.

I did wonder about breaking out D0 and D1 again, but I’ve left that as an option for the future too (I’m using the MIDI link on the R2R shield at present).

IMG_5359

And here is the “full stack” – Arduino Uno at the bottom, then the R2R shield connected to a MIDI module, and finally the OLED display with links out to the mux board.

The Code

This really should have been a fairly straight forward update to the Arduino MIDI Slider R2R Waveform Generator – just include the Adafruit SSD1306 graphics library, initialise the display, add some code to work out how to scale the waveform to fit the display and go…

But I had two problems that just meant I wasted almost a day chasing them down:

  • The Adafruit library dynamically allocates a block of memory to serve as the display memory, but I didn’t have enough memory left for this to work resulting in a failure of the initialisation routine.
  • Something was interfering with the mux IO pin allocations meaning I was getting spurious results.

I’ll come back to the mux/IO pin issue at the end as it turned out to just be an annoying (and hard to find) bug rather than anything affecting design choices.

To solve the memory issue, I tried all sorts of memory optimisations:

  • Moving constant structures into PROGMEM (the note frequency table, the mux pin allocations, the ordering of the potentiometers).
  • Compressing structures down into bits in a single word (the “mirrorpins” array became a single 16-bit word).
  • Changing global counting variables from int to byte.

I even looked up other SSD1306 OLED libraries that claimed better performance and optimisation.  One required the use of a static buffer, so I could see directly the problem and the immediate result of my optimiations every time I compiled the sketch.  The closest I came was being 19 bytes over the 2Kb memory availability on the Uno, so had to drop this train of thought at this point as even if I managed it, having no free RAM for the code to run is no use anyway.

So the only option left was to compromise on something somewhere else.  I figured I had three options here:

  • Drop the wavetable size down from 256 to 128.  To be honest this was probably quite feasible as I’ve already sort-of dropped the resolution down anyway as I’m building up a false 256-value table from just 16 potentiometer readings.
  • Cut some of the accuracy when performing the linear interpolation between pot values to build the wavetable.  It currently requires a 66 bytes of temporary storage for 33 16-bit values. I could half this if I dropped the accuracy.
  • Cut some of the accuracy of pot values.  Again I’m storing 16, 16-bit values of “previous readings” which if I dropped down to 8 bits would save me another 16 bytes.

But all of this was compromising the main function of the sketch – that of digital synthesis. In the end I opted to tell the Adafruit library to treat the display as a 128×32 display rather than a 128×64 display.  This instantly halved the required amount of memory from the original 1Kb.  I kept some of the PROGMEM optimisations too so this then gave me some working memory to play with and allow the code to run.

The downside was losing some of the vertical resolution for the display, but the Adafruit library scales 32 pixels across the whole 64 of the display, so you don’t really notice.  This kept things relatively simple in the end.

Having just finished reading “The Computers that Made Britain” (which I can recommend if you grew up with some of these computers, like I did) I have a new found respect for those early computer designers attempting to get full colour, usable gaming displays out of computers with only kilobytes of RAM in total!

In terms of scaling a 256-entry wavetable, with a range of 0 to 255 onto a 128×32 display, I opted for the following design considerations:

  • I didn’t want to take up lots of time updating the display, so I’m doing it in 16 chunks – update 1/16 of the horizontal display from the wavetable on each scan through the loop.
  • On the 17th scan, actually update the display.

There is an updateDisplay() function that is called from the loop() that takes care of all of this.  The basic algorithm is as follows:

IF displayScan < 16 THEN
  map 16 values of the wavetable from displayScan*16 ...
  .. to 8 values of the display from displayScan*8

  FOR xcoord from displayScan*8 to (displayScan*8 + 7):
    Draw a BLACK vertical line to clear previous points.
    Plot the pixel with the y-coord equal to:
       Wavetable value read from displayScan*16 + xcoord*2
       Scale wavetable value from 0-255 to 0-31 by >>3
       Use the scaled value as the y-coord and plot it WHITE.

ELSE if displayScan == 16
  update the display

Increment the displayScan (wrapping around after 16)

So, back to that IO bug.

This really foxed me for quite some time.  The symptoms where that the analog readings coming back we, well, weird.  Sometimes all the same (0 or 1023), sometimes seemingly random.  Some changed when the pots changed, some didn’t, but it wasn’t always the same ones…

In terms of debugging the issue I tried all the following:

  • Downloaded my test mux reading code – all 16 reading fine, so hardware wise it was all fine (although I did this several points during the day, as I kept getting to the point where I didn’t believe it!).
  • Switched the PROGMEM decoding in/out – was I reading the wrong offsets out of program memory for the mux numbers or IO pins?  Additional Serial.print statements confirmed this was all ok.
  • Was the OLED library interfering?  Many of them allow for either I2C or SPI displays, so as I was now using pins 10-13 for the mux, these are the hardware SPI pins… but moving back to A1-3 worked fine, but moving to any digital pins didn’t… And looking through the code for both the Adafruit and other OLED libraries confirmed the logic – they definitely don’t touch the SPI if you’re using I2C (I was pretty sure they wouldn’t).

Eventually I decided I’d have to disable the R2R logic and try some alternative non-SPI digital pins… and then I realised what the problem was.  In the ddsOutput routine I’m using direct PORT IO to talk to the R2R DAC (it is all explained here).

But there is always an inherent issue with mixing direct PORT IO with digitialWrite statements – it is easy to get them out of sync with other as a statement such as PORTD = 0x0A will set some output bits to HIGH and others to LOW regardless of what the previous setting was.  This was the problem.

In ddsOutput, the lines causing the problem are:

uint8_t pd = wave & 0b11111100;
uint8_t pb = wave & 0b00000011;
PORTD = pd;
PORTB = pb;

I knew when I was writing it that I ought to have taken steps to preserve the state of PORT D bits 0 and 1, and PORT B bits 2-7, which correspond to the other digital pins that I’m not using for the DAC.  But I was lazy and the code was simple, and it didn’t matter when I was just playing around with the DAC.

In theory it might have occasionally caused a spurious value on RX/TX but nothing that caused me any issues.

But now I’m using D10-D13 to address the mux, these are bits 3-6 in PORTB which all keep getting set to zero by the above line every time the “TICK” happens – which will often be between the main loop setting up the mux address pins and actually reading the analog value from the mux!  The joys of multi-threaded, embedded programming!

The right way to do it is therefore to read what the current status of the other bits in the port are and merge the DAC values into those. We can get away with doing this here as the “TICK” is the highest priority code running at this point, so we don’t need to worry about anything else coming in and changing the values again between the reading of PORTD and writing back to PORTD. If we were worried about that, then we’d also have to disable other interrupts whilst doing this just to be really, really sure nothing else is running…

The correct code is as follows.

  uint8_t pd = wave & 0b11111100;
  uint8_t pdo = PORTD & 0b00000011;
  uint8_t pb = wave & 0b00000011;
  uint8_t pbo = PORTB & 0b11111100;
  PORTD = pdo | pd;
  PORTB = pbo | pb;

That will teach me to knowingly take shortcuts.  It is bound to catch me out when cutting and pasting code in  the future!

As John Woods (probably) once said “Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live”.  That future person might just be you!

Find it on GitHub here.

Closing Thoughts

I was getting very close to the point where I’d given up trying to add the display, but it was very satisfying to finally work out the issues and get it going.  It really took a lot more effort than I was anticipating, but I’m really pleased with the final result. I really wasn’t keen on giving up on this one!

Kevin

IMG_5360

Leave a comment