Close

Software

A project log for Business Card + Clock + ATtiny3227 Dev Board

A business card with a charlieplexed realtime clock and an ATtiny3227 development board.

cheetahhenrycheetah_henry 06/09/2024 at 08:590 Comments

While waiting for the PCB, it is time to write some actual code for the clock.

As I've explained the ATtiny3227 Power Down mode and the configuration of RTC, so I won't explain it again, I will focus on the rest of the design.

Charlieplexing

Charlieplexing is a multiplexing technique that able to drive n2 - n LEDs with only n GPIO pins, that is, only 4 GPIO pin are required to drive 42 - 4 = 12 LEDs. In the schematic, the LED assignment correspond to the postion of the LED on the clock face, LED D12 is designated as 0, and the PORTB, PIN4-7 of the ATtiny3227 are used as the driving pins for the LED matrix. Charlieplexing is also known as tristate multiplexing, at any giving time, only one LED is light-up based on the states of the driving pins, for example, in order to turn on LED D1, a positive voltage is apply to PB4 and a zero (ground) need to be asserted at PB5, PB6 and PB7 would have to be "disconnected" by putting it in tri-state/high impedence mode. For microcontroller, we could turn those pins to "input" mode which effectively put those pins in high impedence mode.

In software, this is achieve by defining an array of LED matrix, each consists of two states for `pinConfig` and `pinState`. The `pinConfig` represents the value on which the pin should be configured as INPUT or OUTPUT, the 'pinState' decided on whether the pin should be set to HIGH or LOW. The `mux[LEDS]` contains both the values of `pinConfig` and `pinState` for each LED. For example, to turn on the LED D1 as we previously mentioned, the mux[1] have a value of `{0x30, 0x10}` which when expands to binary would look like `{B00110000, B00010000}` with the MSB represents PB7 and LSB represents PB0. We only care for the upper 4-bits that represents PB7 - PB4. What this means is that for the `pinConfig` value of `B00110000`, both PB4 and PB5 would be set as OUTPUT, PB6 and PB7 would be set to INPUT. The `pinState` of `B00010000` means that only PB4 is set to HIGH, which based on the schematic would turn on the LED D1 and the rest of the LEDs would be off. That's what the `turnOnLED()` function do. To turn off all the LEDs, it is simple, just set all the pins to INPUT mode with the `turnOffLED()`.

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

#define LEDS                12

const uint32_t DISPLAY_TIME = 5000;  // 5000ms

// Charlieplexing configuration and state matrix
typedef struct {
  uint8_t pinConfig;
  uint8_t pinState;
} Mux_t;

const Mux_t mux[LEDS] = {
  {0x90, 0x80}, // 0
  {0x30, 0x10}, // 1
  {0x30, 0x20}, // 2
  {0x60, 0x20}, // 3
  {0x60, 0x40}, // 4
  {0xc0, 0x40}, // 5
  {0xc0, 0x80}, // 6
  {0x50, 0x10}, // 7
  {0x50, 0x40}, // 8
  {0xA0, 0x20}, // 9
  {0xA0, 0x80}, // 10
  {0x90, 0x10}  // 11
};

void turnOnLED(uint8_t led) {

  // enable input buffer
  PORTB.PIN4CTRL = 0;
  PORTB.PIN5CTRL = 0;
  PORTB.PIN6CTRL = 0;
  PORTB.PIN7CTRL = 0;

  // rest all pins to input and set it to low
  PORTB.DIRCLR = (PIN4_bm | PIN5_bm | PIN6_bm | PIN7_bm);
  PORTB.OUTCLR = (PIN4_bm | PIN5_bm | PIN6_bm | PIN7_bm);

  // set pin(s) based on mux.pinConfig and mux.pinState value
  PORTB.DIRSET = mux[led].pinConfig;
  PORTB.OUTSET = mux[led].pinState;

}

int main() {

  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_4X_gc)); // set prescaler to 4 for running at 5MHz

  configRTC();
  SLPCTRL.CTRLA |= SLPCTRL_SMODE_PDOWN_gc;    // config sleep controller to PowerDown mode 
  sei();

  while(1) {
    cli();
    uint16_t seconds = timeCount;
    sei();

    uint8_t hours = (seconds / 3600) % 12;
    uint8_t minutes = (seconds / 60) % 60;
    uint8_t fiveMinuteInterval = (minutes / 5) % 12;
    uint8_t flashes = minutes % 5;

    turnOnLED(hours);
    flashLED(fiveMinuteInterval, flashes);
  }

  return 0;
}

So as each of the LED represents the hour of the current time, by passing an value of the `hours` to the function `turnOnLED(hours)` would turn on the LED correspondent to the current hour.
Each LED on the clock face will also repsent the five-minute interval past the hour (e.g. LED D2 means 10 minutes past the hour), in order to differentiate from the solid ON that represent the hour, I developed a scheme of flashing the LED for showing the number of minutes past the hour.

State Machine for LED Flashing

To display the five-minute interval, a state machine is used as it is not only need to turn the LED on and off in a non-blocking manner for the need of multiplexing the LEDs, there is also several iterations within a given display time. I decided to set the total display time to 5 seconds, each flash of showing the minutes last for 500 ms, a long flash that represents 0 minute will be turning LED on for 450ms, and off for 50ms, and for short flash that represents 1 minute after the five-minute interval would be 50ms on and 450 ms off. The `flashLED(fiveMinuteInteral, flashes)` simply moving from one state to next state based on the time passed-by. The `fiveMinuteInterval` value indicates which LED to be activated, and the `flashes` determines the number of flashes the LED need to do to represent the exact minute in current time. Here is the number of flashes and pattern, say, for LED D2 that represent 10 minutes past the current hour. 

LED D2 flash patternTime in minute
ON 450ms, OFF 50ms10 minutes past the current hour
ON 50ms, OFF 450ms11 minutes past the current hour
ON-OFF, ON-OFF12 minutes past the current hour
ON-OFF, ON-OFF, ON-OFF13 minutes past the current hour
ON-OFF, ON-OFF, ON-OFF, ON-OFF14 minutes past the current hour
// state machine for LED blinking states
enum States {BEGIN, LED_ON, LED_OFF, END};

volatile uint16_t timeCount = 0;
volatile uint32_t t_millis = 0;

void flashLED(uint8_t theLED, uint8_t flashes) {

  static uint8_t flashState = BEGIN;
  static uint8_t cycle = 0;
  static uint32_t onTimer = 0;
  static uint32_t offTimer = 0;
  static uint32_t intervalTimer = 0;

  switch (flashState) {
    case BEGIN:
      onTimer = millis();
      flashState = LED_ON;
      break;
    case LED_ON:
      {
        turnOnLED(theLED);
        if (flashes == 0) {  // flash once for 450ms On/50ms Off
          if (millis() - onTimer > 450) {
            flashState = LED_OFF;
          }
        }
        else {
          if (millis() - onTimer > 50) {  // flash once for 50ms On
            flashState = LED_OFF;
            offTimer = millis();
          }
        }
      }
      break;
    case LED_OFF:
      {
        turnOffLED();
        if (flashes == 0) {
          if (millis() - offTimer > 50) {
            intervalTimer = millis();
            flashState = END;
          }
        }
        else {
          if (millis() - offTimer > (500UL - 50 * flashes) / flashes) { // Off varies based on number of flashes
            if (++cycle < flashes) {
              flashState = BEGIN;
            }
            else {
              flashState = END;
              intervalTimer = millis();
            }
          }
        }
      }
      break;
    case END:
      if (millis() - intervalTimer > 1000UL) {
        flashState = BEGIN;
        cycle = 0;
      }
      break;
    default:
      break;
  }

}

As the `flashLED()` requires a non-blocking execution for several iterations, and I'm using bare metal programming instead of relying on Arduino framework, so in order to generate a millisecond interval to emulate the `millis()` function like what Arduino framework did, I set up TCA timer interrupt to increment a counter `t_millis` at every 1ms. The TCA is only enabled during the active mode and disabled prior going into Power Down Mode.

const uint16_t TWELVE_HOUR = 43200;    // 12 * 3600 seconds

volatile uint32_t t_millis = 0;

uint32_t millis() {
    while (TCA0.SINGLE.INTFLAGS & TCA_SINGLE_OVF_bm);
    return t_millis;
}

// Generate a 1ms output for millis()
void configTCA() {
    TCA0.SINGLE.CTRLB = TCA_SINGLE_WGMODE_NORMAL_gc;
    TCA0.SINGLE.PER = 625 - 1;  // (1ms * F_CPU ) / 8 -1 (i.e. (0.001 * 5000000 / 8) - 1 )
    TCA0.SINGLE.INTCTRL = TCA_SINGLE_OVF_bm;
    TCA0.SINGLE.CTRLA = TCA_SINGLE_CLKSEL_DIV8_gc | TCA_SINGLE_ENABLE_bm;
}

Config Time

The code so far will contstantly showing the time based on the value in `timeCount` which is started at 0 and incremented once in every second. We need to have a mechanism to set the `timeCount` to the current real time.
Everytime when the compiler compile the code, it generates a time stamp based on your PC's local time and can be retrieved from the `__TIME__` variable, since I'm using bare metal programming without any framework overhead and the UPDI programmer is quite fast in flashing the code to the MCU, so from compiler to compile the code to the completion of uploading the code to the ATtiny3227, it takes a little bit of 1 seconds, so the value in `__TIME__` provide accuracy to be nearest second of the actual time. The `__TIME__` is a string and need to be parsed to get the value, the value will be used to set the initial `timeCount` as the current time.

void configTime() {
    char timeStr[10];
    strcpy(timeStr, __TIME__);

    uint16_t h = atoi(strtok(timeStr, ":"));
    uint16_t m = atoi(strtok(NULL, ":"));
    uint16_t s = atoi(strtok(NULL, ":"));
    timeCount = ( h * 3600 +  m * 60 + s ) % TWELVE_HOUR; //round it to 12-hour
}

Using `__TIME__` to set the time make the design of time configuration easy as there is no user interactive required, it is however not perfect, the most obvious problem is that next time when you replace the battery, the previous compilation time `__TIME__` is going to be used again unless you hook up the UPDI programmer and re-compile the code and upload again. So having a way to manually adjust the time is inevitable, but for time being, this is good enough for the project, and I will develop the code for interacting with the clock later as an enhancement.

Buttons Configuration and Show Time

As discussed in battery life calculation, I could configure the clock in such a way that it will automatically show the time in every minute with the trade-off of shorter battery time, or I could have more than 3 years battery life if only show the time when user press a button. The schematic has two buttons connected to PORTC, PIN4(sw2) and PIN5(sw1), as I imagine that in order to adjust time, I will probably need two buttons, one for confirmation and another for adjustment, and either of the two buttons could be programmed to wake-up the ATtiny3227 to show the time. For now I program both buttons to behave the same for waking up the ATtiny3227.

const uint32_t DISPLAY_TIME = 5000;  // 5000ms

volatile uint8_t sw1Pressed = 0;
volatile uint8_t sw2Pressed = 0;

ISR(PORTC_PORT_vect) {
 if (PORTC.INTFLAGS & PORT_INT5_bm) {  // PC5 (SW1) for show time
    PORTC.PIN5CTRL = 0;                // disable trigger
    PORTC.INTFLAGS = PORT_INT5_bm;     // Clear PC5 interrupt flag
    sw1Pressed = 1;
  }
  if (PORTC.INTFLAGS & PORT_INT4_bm) { // PC4 (SW2) for show time
    PORTC.PIN4CTRL = 0;                // disable trigger
    PORTC.INTFLAGS = PORT_INT4_bm;     // Clear PC4 interrupt flag
    sw2Pressed = 1;
  }
  displayStart = millis();
}

void configButtons() {
    PORTC.DIRCLR = PIN4_bm;
    PORTC.PIN4CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;  // Enable PC4(SW2) PULLUP and interrupt trigger
    PORTC.DIRCLR = PIN5_bm;
    PORTC.PIN5CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;  // Enable PC5(SW1) PULLUP and interrupt trigger
}

Each button is configured as INPUT with internal pull-up resistor enabled, and interrupt to be triggered at FALLING edge. The interrupt function will temperately disable the interrupt trigger and set either `sw1Pressed` or `sw2Pressed` flag and the `displayStart` flag will be set to the current `millis()` value. The LEDs will show the time for 5 seconds (configurable by changing the value used in `DISPLAY_TIME`). Once the 5 seconds is elapsed, all LEDs will be turned off, and `sw1Pressed` or `sw2Pressed` flag will be reset, pin pull-up resistor and interrupt trigger mode will be re-activated, waiting for next time a user press the button.

      if (millis() - displayStart >= DISPLAY_TIME) {
              turnOffLED();
              if (sw1Pressed) {
                sw1Pressed = 0;
                PORTC.PIN5CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
              }
              if (sw2Pressed) {
                sw2Pressed = 0;
                PORTC.PIN4CTRL = PORT_PULLUPEN_bm | PORT_ISC_FALLING_gc;
              }
              showTime = 0;
              displayStart = millis();
          }
      }

The complete source code is available at my GitHub

Discussions