ATtiny85 chirping in response to red key presses on a Liyafy HC-35 keypad

For background on this post, please see “Fun with an ATTiny85, Liyafy HC-35 keypad with eight LEDs, and a serial to parallel shift register”.

Last time, I was just excited to get some lights to blink. Since then, I’ve figured out a few things. For example, if a bit in the output is set, the corresponding LED on the HC-35 is turned off. Therefore, to get the pleasing animated counting effect I wanted, I needed to send the complement of the number to the shift register. I also worked on tidying up some of the wiring to get things slightly more organized on the breadboard to prepare for six more wires coming in from the HC-35 and placing another ATtiny85 on the board. It turned out that I needed to run a separate pair of VCC/GND to the second ATtiny85 which where that power supply really shone.

So, before proceeding further, here is the improved code for flashing the LEDs on the HC-35:

// Adapted sample code in https://www.arduino.cc/en/Tutorial/Foundations/ShiftOut
// Code sample 1: Hello World https://www.arduino.cc/en/Tutorial/ShftOut11

// ATtiny85    SN74HC595
// ----------------------
// PB0 (5) <-> RCLCK (12)
// PB1 (6) <-> SRCLK (11)
// PB2 (7) <-> SER (14)

// ATtiny85
// --------
// 5V  <-> VCC(8)
// GND <-> GND(4)

// SN74HC595
// ---------
// GND <-> GND (8)
// VCC <-> SRCLR (10)
// VCC <-> OE (13)
// VCC <-> VCC (16)

// PB0 (5) <-> RCLK (12)
// ST_CP / latch / Storage register clock pin in tutorial
static const int rclk_out = 0;

// PB1 (6) <-> SRCLK (11)
// SH_CP / Shift register clock pin in tutorial
static const int srclk_out = 1;

// PB2 (7) <-> SER (14)
// DS / Serial data input in tutorial
static const int ser_out = 2;

void setup() {
  delay(250); // let things settle
  pinMode(rclk_out, OUTPUT);
  pinMode(srclk_out, OUTPUT);
  pinMode(ser_out, OUTPUT);
  delay(250);
  digitalWrite(ser_out, 255); // lights off to start
}

void led_out(int b) {
  digitalWrite(rclk_out, LOW); // so LEDs don't change while the bits are being transmitted
  shiftOut(ser_out, srclk_out, LSBFIRST, b); // send the data
  digitalWrite(rclk_out, HIGH); // make the new eight bits available
}

void one_at_a_time() {
  // Light up each LED once
  // Should take about a second
  for (int i = 0; i < 8; ++i) {
    led_out(~(1 << i));
    delay(125);
  }
}

void all_combos() {
  // Count from 0 to 255 in a fraction under a minute
  for (int i = 0; i < 256; ++i) {
    led_out(~i);
    delay(234);
  }
  led_out(255);
}

void blink_all() {
  // Rapid blinking for five seconds
  for (int i = 0; i < 50; ++i) {
    led_out(0);
    delay(50);
    led_out(255);
    delay(50);
  }
  led_out(255);
}

void loop() {
  delay(1000); // wait a little before the show
  one_at_a_time();
  delay(1000);
  all_combos();
  delay(1000);
  blink_all();
}

Satisfied that each LED was being turned on from the LSB to MSB and they were counting from 0 to 255 in binary, I turned my attention to the task of getting key press information.

I took for granted that I could read the keypresses using four wires. That turned out to be a correct assumption. So, four pins for reading keys, one pin for VCC, one pin for GND, the RESET pin which I am not going to disable because I do not want to need a high voltage serial programmer to reprogram the chip. That leaves one pin to get information out. My mistake was assume that I should immediately figure out how use SoftwareSerial or similar to output bytes on that pin. I decided to try to use PB4 because mapping four keys to PB0PB3 just made sense even though I know PB2 has abbreviations associated with serial interfaces next to it. At first, I had been using the pin change interrupts to read key states, but then SoftwareSerial wants to install its own handlers for receive operations. Remember, I am only interested in getting information out. Stuff was just not working out. So, I decided to take a step back and proceed in small, discrete steps.

How about proving that I can read key states using the simplest approach? I decided I wanted audible confirmation that I was able to distinguish which specific key was pressed. So, I just wrote a simple busy loop, wired up the active buzzer to PB4. And, it works:

void
setup() {
  pinMode(0, INPUT_PULLUP);
  pinMode(1, INPUT_PULLUP);
  pinMode(2, INPUT_PULLUP);
  pinMode(3, INPUT_PULLUP);

  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);
  delay(5000);
}

bool
key_pressed(int pin)
{
  return digitalRead(pin) == LOW;
}

void
blink(int times, int duration)
{
  for (int i = 0; i < times; ++i) {
    digitalWrite(4, HIGH);
    delay(duration);
    digitalWrite(4, LOW);
    delay(duration);
  }
}

void
loop() {
  if (key_pressed(0)) blink(50, 50);
  if (key_pressed(1)) blink(20, 125);
  if (key_pressed(2)) blink(10, 250);
  if (key_pressed(3)) blink(5, 500);
}

Here’s a photo of the slightly cleaned up placement and wiring of all the components:

[ One ATtiny85 blinking LEDs, another reading keypresses and chirping an active buzzer in response ]

And here’s a video of the buzzer chirping in response to key presses and LEDs flashing pleasingly. I kept the buzzer covered because otherwise it was getting too loud on the video. In case the chirping is not audible, I added a yellow LED inline to also give a visual indication that different keypresses generate different output.

At this point, I was ready it call it a day, but something else came up. Later, I did a search for using SotwareSerial at low bit rates, and I found a forum post where the possibility of fiddling with the calibration of the oscillator was mentioned. The OP posted a calibration routine which looped through values 0 to 255 for OSCCAL, sending the output on the serial line it was trying to use. Watching on the monitor, you could see at what OSCCAL value you stopped seeing gibberish. My first few attempts were discouraging because after running it or five minutes or so, I would see long periods of nothing punctuated by some gibberish. I decided to fiddle with the calibration routine by instead starting at the mid-point by setting OSCCAL = 128, and then adding Δ ∈ {±1, ±2, ±3, ... ±127}. Yeah, this misses OSCCAL = 0 but we’ve already established I don’t see anthying at that value.

Surprise!

Here’s the calibration routine:

#include <SoftwareSerial.h>

SoftwareSerial comm(-1, 0);

static const int anchor = 128;

void
print_osccal(int v) {
  comm.println(F("********************************"));
  comm.print(F("OSCCAL = "));
  comm.println(v);
  comm.println(F("********************************"));
}

void
setup() {
  delay(5000);
  comm.begin(300);
  OSCCAL = anchor;
  print_osccal(anchor);
  delay(5000);
}

void
loop() {
  int x;
  for (int i = 1; i < 128; ++i) {
    x = anchor + i;
    OSCCAL = x;
    print_osccal(x);
    delay(1000);
    x = anchor - i;
    OSCCAL = x;
    print_osccal(x);
    delay(1000);
  }
}

Now, I have decided I’d rather not use SoftwareSerial in the actual project, but it does the job of helping me discover the OSCCAL value at which serial communication works. In the case, I have a baud rate of 300 mostly because it creates a retro effect in the serial monitor. It might also be the right baud rate to use for this project, but let’s not get ahead of ourselves. I can’t adapt one of those send only serial libraries without knowing the value of OSCCAL which makes serial work reliably.

It is at this point that I decided to get fancy: Why not give me button to tell the ATtiny85 to save the current value in EEPROM so I don’t have to remember to set it manually in every the actual code that uses serial comms? Note that it is not clear I gain much from this as now I have to remember to add EEPROM support to the code that uses the value. But, I am doing this for fun, so why not? My first search landed me on this question and this helpful answer told me I needed to modify boards.txt in Arduino IDE to set “high fuse” to 0xD7 instead o 0xDF for the EEPROM to be preserved when I reprogram the chip.

When you have the tempatation to get fancy … resist. This where things went a bit wrong. First, I shorted the push button on the breadboard. Then, I burned the ATtiny85 by inserting it the wrong way in the programmer. Finally, fancy USB to TTL UART converter became unresponsive (possibly due to me connecting one of the legs of that button to 5V instead of GND). So, took another break from this and came back to it later.

The next ATtiny85 did not seem to have an initial OSCCAL value anywhere near the middle of the 0 … 255 range, so I decided to fiddle with the search to make it asymmetric around the value of OSCCAL at startup. Here is the code (note, I’ve kind a left the saving the EEPROM thing as an exercise for the reader):

#include <EEPROM.h>
#include <SoftwareSerial.h>

#define TX_BAUD 300
#define TX_PIN 0

#define BUTTON_PIN 2
#define LED_PIN 4

#define LONG_WAIT 1000
#define SHORT_WAIT 250

static int initial_osccal;

static void blink_on_button_press();
static void blink_on_done();
static void blink_on_print_osccal();
static void blink_on_startup();
static void do_blink(int, int);
static void print_osccal(int);
static int restore();
static void save(int);
static int search_osccal();
static bool should_save();
static int try_osccal(int);

SoftwareSerial* comm;

static void
print_osccal(int v) {
  comm->println("\r\n*-- OSCCAL -------------------------------");
  comm->print("* At startup = ");
  comm->println(initial_osccal);
  comm->print("* Current = ");
  comm->println(v);
  comm->println("*--- Press button to save in EEPROM ------\r\n");
}

static void
save(int v)
{
  EEPROM.update(0, 'O');
  EEPROM.update(1, 'S');
  EEPROM.update(2, 'C');
  EEPROM.update(3, (byte)(v & 0xff));
}

static int
restore()
{
  // Check signature to avoid bogus values
  if (
    (EEPROM.read(0) == 'O') &&
    (EEPROM.read(1) == 'S') &&
    (EEPROM.read(2) == 'C')
    )
  {
    return EEPROM.read(3);
  }
  return -1;
}

static bool
should_save()
{
  // Figure out what this should do
  return false;
}

static int
try_osccal(int v)
{
  if ((v < 0) || (v > 255)) return -1;

  OSCCAL = v;
  print_osccal(v);
  blink_on_print_osccal();
  delay(SHORT_WAIT);
  
  if (should_save()) return v;

  return -1;
}

static int
search_osccal()
{
  // Maybe the initial value is good.
  if (try_osccal(initial_osccal) >= 0) return initial_osccal;

  int anchor = initial_osccal;
  int limit = max(initial_osccal, 255 - initial_osccal);
  int x;

  // Search up and down
  for (int i = 1; i < limit; ++i) {
    if ((x = try_osccal(anchor + i)) >= 0) return x;
    if ((x = try_osccal(anchor - i)) >= 0) return x;
  }
  return -1;
}

static void
do_blink(int times, int delta)
{
  for (int i = 0; i < times; ++i)
  {
    digitalWrite(LED_PIN, HIGH);
    delay(delta);
    digitalWrite(LED_PIN, LOW);
    delay(delta);
  }
}

static void
blink_on_button_press()
{
  do_blink(25, 20);
}

static void
blink_on_done()
{
  do_blink(50, LONG_WAIT/50);
}

static void
blink_on_print_osccal()
{
  do_blink(8, LONG_WAIT/8);
}

static void
blink_on_startup()
{
  do_blink(75, LONG_WAIT/75);
}

void
setup() {
  blink_on_startup();

  initial_osccal = restore();
  if (initial_osccal < 0)
  {
    initial_osccal = OSCCAL;
  }

  OSCCAL = initial_osccal;

  pinMode(LED_PIN, OUTPUT);
  pinMode(BUTTON_PIN, INPUT);

  comm = new SoftwareSerial(-1, TX_PIN);
  comm->begin(TX_BAUD);

  delay(5000);
  blink_on_startup();
}

void
loop() {
  int good_osccal;

  while ((good_osccal = search_osccal()) < 0)
  {
    // Spin until desired value is determined
  }

  save(good_osccal);
  OSCCAL = good_osccal;
  blink_on_done();
  comm->print("OSCCAL value = ");
  comm->print(good_osccal);
  comm->println("should be saved now.");
}

The screenshot below links to an animated GIF of the serial monitor during the OSCCAL search:

[ Screenshot of the serial monitor during the search for an appropriate OSCCAL ]

The code for this takes up about 3.5 Kb of the 8 Kb flash space and uses something 330 bytes out of the 512 bytes of RAM. So, I have no intention of including any of this in the solution for sending key state to another ATtiny85, but getting reliable serial output was good. I am still going to attempt to put together, using others’ good ideas, obviously, a decent send only serial bit banger to operate at 300 baud and 1 MHz clock speed (SoftwareSerial requires at least 8 MHz clock).

PS: You can discuss this post on r/attiny and HackerNews.