Toy Keyboard Tone Piano – Part 3

This post looks in a little more detail at the keyboard matrix decoding for the toy keyboard to see what is possible with less IO pins.

  • Part 1 provides all the details for adding an Arduino to the original toy keyboard.
  • Part 2 demonstrates the toy keyboard running the Oskitone “Scout” firmware.

IMG_5847

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 Arduino tutorials for the main concepts used in this project:

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

Decoding the Keyboard Decoding

The current method of decoding the keyboard matrix and buttons uses a lot of IO pins.  There are 8 “rows” and 10 “columns” in the “keypad” that is formed from all the keys and buttons (see part 1 for details).  There are a number of ways of scanning a keyboard matrix using fewer IO pins, but first we need to understand how it all works.

Here is a simplified circuit of the keyboard matrix as used in my toy keyboard.

Toy Keyboard Switch Matrix1

The Keypad library configures the ROWs as INPUTs with PULLUP enabled and the COLUMNS as OUTPUTs that will default HIGH unless being “scanned” when they are set to LOW in turn.  Pressing a key will connect the corresponding ROW with a specific COL and will make the ROW read LOW when that COL is scanned.  This is illustrated below.

Toy Keyboard Switch Matrix2

Here you can see that the switch that connects C3 to R2 will make R2 LOW only when C3 is set to LOW.  Otherwise R2 will be reading HIGH due to the internal PULLUP.  Consequently during the scanning, when C3 is active (set LOW) and R2 reads LOW, we know that the corresponding switch has been pressed.  By scanning each column in turn (i.e. sequentially setting C1 to C3 to LOW and checking all the ROWs each time), we can determine which switches are pressed and which are not.

This is all well and good, but what happens when several switches are pressed at the same time?  An ideal keyboard would have a diode on every single key or button to prevent “ghosting” (for a detailed discussion, see more here and a project where I look at this, here).

This keyboard only has diodes on each COL connection, which I guess limits some of times ghosting can occur, but it will not eliminate them.  In the following example, you can see how having several keys pressed in a column that isn’t being scanned causes an erroneous result.

Toy Keyboard Switch Matrix3

In this example, when C3 is being scanned, both R2 and R3 appear LOW due to multiple switches being pressed in C2.  This means the code will believe that the two switches in C3 for R2 and R3 are being pressed, which isn’t the case – it is only the R2 switch in C3. There isn’t really anything that can (trivially) be done in software to solve this – as I say, it really needs a diode on every switch, which I don’t have.  So this is a limitation I’m just going to have to live with.

If we want to reduce the number of IO pins used to scan the matrix, then our options are either:

  • Do something to somehow encode the OUTPUTs a bit better.
  • Do something to somehow multiplex the INPUTs.

Well it turns out there are actually complete keyboard matrix scanning ICs available! One example is the Texas Instruments TCA8418 which from my superficial reading of the datasheet looks like it could support an 8×10 matrix directly and provide the results over I2C to a microcontroller!  That sounds ideal – except I don’t have one and I’ve not found this available in any kind of diy/breakout board friendly way – only as single chips… So this option isn’t a possibility for now, but if you know of an alternative that can do something similar, do drop me a line in the comments.

Multiplexing the inputs is a possibility, but I’ve not seen anyone attempt to do it that way, so I’ve not researched it further myself.

Doing something with the OUTPUTs at the “scanning” side of things seems to be the way others have gone. I’ve seen two answers to this so far:

  • 74HC595 shift registers
  • 74HC138 3 to 8 decoder

A shift register is very commonly used to drive LED displays.  The basic idea is that you send in (“shift”) a value using a serial link, using just a couple of pins – usually a data, clock and “latch” to tell the chip when to apply the value – and the outputs will then reflect this value.  It will then maintain that value on the outputs until a new value comes along.

A key feature of using a shift register is that you can have multiple outputs active at the same time – as you’d need if you were driving something like a 7-segment display for example.

That is a bit overkill in this application though, as I only need each output to be on/active in turn, so that is where the 3 to 8 decoder comes in.

The 74H138 3-to-8 Decoder

This basically takes a 3-bit value (so a value between 0 and 7) and uses that to enable one of 8 outputs at a time.  This is perfect for “scanning” a matrix, so that is what I’ve used.

Ideally, I’d use the 74HC138 on the 8 “rows”, but I have to use it at the “output” stage, so instead I use it for the four columns associated with the keys and four of the columns associated with the buttons.  This gives me a choice: drop two “columns” of buttons, or do something else.  One option is to scan them independently, but this would make the scanning “column specific” – i.e. it would have to know if a column is independent or part of the decoder, which is a bit messy.

Thankfully there is another solution.  There is quite a lot of redundancy in the keyboard matrix when it comes to decoding the buttons.  As an example, there are buttons on BP00 for BP23 to BP27, but none for BP20 to BP22.  Similarly there are buttons on BP06 for BP20 to BP22 but none for BP23 to BP27.  So simply connecting BP00 and BP06 together means all buttons can still be decoded and I’m now one column less.  There are no other columns that fit together as well, but BP02 and BP03 only overlap with two, so combining these means I can still decode six out of the eight buttons, but just have “March” and “Rock” acting as duplicates for “Vol+” and “Vol-“.

This means that if I join BP00 to BP06, and BP02 to BP03, the final arrangement is an 8×8 matrix as follows:

Toy Keyboard Sitch Decoding 8x8

This now means that all eight columns can be driven from a 3-to-8 decoder.  This gives me the following circuit.

Toy Keyboard Sitch Matrix - 74HC138_bb

The pinouts from the Arduino Nano are now as follows:

  • D12-11; D8-D4 = BP20-27
  • D9 = Audio output
  • D13 = LED driver
  • A0-A3 = Address for the 74HC138

This leaves the following IO pins free for other uses: D0-D3; A3-A7.  Of course, playing the same “combine BP00 and BP06; BP02+BP03” trick on the previous setup would also have saved me two IO pins, so that is another option.

IMG_5848

Keypad138

The Keypad Library doesn’t support the 74HC138, but with a couple of minor changes to Keypad.cpp it can.

I actually copied Keypad.cpp and Keypad.h into my project directory and renamed them Keypad138.cpp and Keypad138.h.  Then in these files I renamed every instance of Keypad to Keypad138, creating a new Keypad138 library.  This also means that Keypad138.cpp now has to #include “Keypad138.h” rather than #include <Keypad.h> (note the change from angle brackets to quotes too).

Once that is done, the following actual code changes are required to Keypad138.cpp.

I’ve “pre-coded” things to assume a 3-line decoder, so at the start of the file, I set up the number of pins to be used for addressing the columns.

#define COL_ADDR_PINS 3

We now need a new Keypad138::scanKeys function to replace the original, as follows:

void Keypad138::scanKeys() {
  // Re-intialize the row pins. Allows sharing these pins with other hardware.
  for (byte r=0; r<sizeKpd.rows; r++) {
    pin_mode(rowPins[r],INPUT_PULLUP);
  }

  for (int cp=0; cp<COL_ADDR_PINS; cp++) {
    pin_mode(columnPins[cp],OUTPUT);
  }

  // bitMap stores ALL the keys that are being pressed.
  for (byte c=0; c<sizeKpd.columns; c++) {
    // Set the decoders "address"
    for (int cp=0; cp<COL_ADDR_PINS; cp++) {
      if (c & (1<<cp)) {
        pin_write(columnPins[cp], HIGH);
      } else {
        pin_write(columnPins[cp], LOW);
      }
    }

    for (byte r=0; r<sizeKpd.rows; r++) {
      bitWrite(bitMap[r], c, !pin_read(rowPins[r])); // keypress is active low so invert to high.
    }
  }
}

This has replaced the link between column and individual pins in the columnPins[] array with a “decoding” action, using the three columnPins[] to represent the three bits of the column number (from the for loop for “c”).

There are a few assumptions in this code: it assumes that there will be three address pins, and that these were defined when the Keypad138 constructor was called; and it assumes that the number of columns won’t require more than three pins to handle it, which limits the number of columns to 8.  None of these assumptions are enforced in the code though!

Having said that, there is nothing in the original Keypad code either to ensure that the correct number of pins is given for the specified number of ROWS or COLS!  Basically, don’t get it wrong 🙂

All that remains then is the new initialiser for the 74HC138 version of the Keypad138 code in the core KeyBuffer.Cpp file:

const byte ROWS = 8;
const byte COLS = 8;
char key_indexes[ROWS][COLS] = {
/*          BP02/03 BP04 BP05 BP00/06 BP07 11 12 13 */
/* BP20 */ {B_VOLP, 0, B_SLOWROCK, B_DEMOSEL, 32, 24, 9, 1},
/* BP21 */ {B_VOLN, B_STOP, 0, B_GUITAR, 31, 23, 10, 2},
/* BP22 */ {B_NEWAGE,B_PLAY, B_DISCO, B_TRUMPET, 30, 22, 11, 3},
/* BP23 */ {B_SAMBA, B_REC, B_BLUES, B_SW2, 29, 21, 12, 4},
/* BP24 */ {B_TEMPP, B_DEMO, B_VIOLIN, B_DOG, 28, 20, 13, 5},
/* BP25 */ {B_TEMPN, B_PIANO, B_BELL, B_BIRD, 27, 19, 14, 6},
/* BP26 */ {0, B_ORGAN, B_MUSICBOX, B_FROG, 26, 18, 15, 7},
/* BP27 */ {0, B_WALTZ, B_MANDOLIN, B_DUCK, 25, 17, 16, 8},
};
// BP20,21,22,23,24,25,26,27
byte rowPins[ROWS] = { 4, 5, 6, 7, 8,10,11,12 };
// The cols are driven by the decoder: BP03-07+11-13
byte colPins[COLS] = {A0, A1, A2};

Keypad138 _buttons = Keypad138(makeKeymap(key_indexes), rowPins, colPins, ROWS, COLS);

Everything else is the same and should work the same.  The only real difference is that the buttons B_ROCK and B_MARCH will no longer be recognised in code, but pressing either of these buttons will now trigger B_VOLP or B_VOLN instead.

For an extra 8 IO pins I’m happy to lose two buttons!

The Amplifier Circuit

One thing I also wanted to add was some kind of volume control.  The original was completely digital – the VOL+ and VOL- buttons adjusted the volume, presumably by the chip in the original “blob” adjusting its output, but using Arduino tone() I don’t have that luxury.  Besides, it felt like a simple analog volume control would be fine, which wouldn’t need an IO pin and wouldn’t need microcontroller time.

So I set about trying to see how the simple amplifier circuit built onto the PCB works. It turns out it has a relatively simple transistor inverting amplifier.  I believe it is a common emitter amplifier circuit, but with very limited additional components, and it is using a SS8050 (NPN) transistor.  The relevant parts, and the equivalent “schematic” can be seen below.

Toy Keyboard Amp Circuit

There are some curious properties using this “as is” with a tone() output from the Arduino’s IO pin.  To start with, tone() would normally cause a 0 to 5V square wave to be apparent, but when connected up to this circuit, the waveform is significantly smaller, typically a 0 to 1V square wave.

Also, the voltage at the speaker is 5V with no sound, but then turns into a 0 to 4.75V signal when a tone is playing.

In this configuration, with just a square wave as an input, I’m pretty sure the transistor is just acting as a simple switch rather than an amplifier.  With a square wave it doesn’t really make much difference – the sound will be the same regardless (albeit louder).  But this will be something I’ll need to return to if I start outputting proper waveforms.

The original circuit had a capacitor and resistor between what I think was the audio (I’m assuming PWM, but it might be a genuine DAC) output of the “chip in the blob” and the edge connector output labelled “DAC”.  From some simple measurements it appears that it was outputting a signal within the range 0.5V to 0.8V.  I don’t know if the “biasing” is happening under the “blob” in hardware or within the software somehow by setting the appropriate DAC levels, but I guess this is why there are no bias components on the amplifier itself.

For a great description of how a common emitter transistor small signal amplifier works, take a look at this video from RSD Academy that was recommended to me by @Bornach0 on Twitter.  Having heard some of this theory though, I have no idea why there aren’t more components in the circuit on my PCB…

Anyway, in terms of adding in a simple, passive volume circuit, the simplest thing is to have a potentiometer acting as some attenuation to the connection to the speaker, reducing that 0 to 4.75 down to something less.  Consequently I made a hole in the casing and wired in a pot.

I used a logarithmic pot, but it was a cheap one, so I suspect it might be a “pretend log” pot.  And having it wired “opposite” so that fully clockwise means “no resistance” and fully anti-clockwise means “full resistance” (as it is attenuating) might mean it is working “backwards” too… either way, there isn’t much control – almost all the sensitivity is in the first few degrees, but it will do.

Closing Thoughts

From what I understand the amplifier is designed for use with signals in the 0.5V to 0.9V range (or thereabouts) and turns it into a signal in the 0V to 4.5V (or more) range.  If I’m outputting directly from an Arduino, either as a direct square wave or as PWM, this is probably not necessary, so I may well end up skipping the amplifier part of the circuit eventually.

The use of the 138 decoder though was really interesting.  I think I’ll leave that in as an option as I now have six analog and four digital (including RX/TX) free for use with other things!  That leaves loads of possibilities!

Kevin

Leave a comment