Jump to content
43oh

Recommended Posts

Finally I'll share some of my binary watch project with you guys.

 

 

Some features:

- 11 LEDs charlieplexed on 2x 3 I/O pins (6 LEDs per 3 I/O lines, one LED not placed)

- As low power as can get (900 nA in stand-by)

- 14-pin value lin MSP430

- watch crystal sourced (32768Hz)

- two buttons (of which one is on the reset line)

- well under 2kB code (1.6kB)

 

The "display refresh rate" is 512Hz, which makes the "pixel refresh rate" at 85Hz, a comfortable rate for the eye.

The WDT is used as a minute timer (2 second hardware, 30 iterations in software), sourced from the crystal, I go down to LPM3 then and can run at 900 nanoAmps.

During display I run in LPM1 from which I wake 512 times per second to update the LED display.

The reset line is reused as an input line for one of my two buttons, using the NMI I can use it as a reliable button input pin using some nifty software tricks.

I use the reset line as button input because I ran out of I/O (10 available: 6 for the display, 1 for a button, 2 for the crystal and 1 for a future buzzer)

 

I'm busy desiging a PCB for this too (not a booster, a feature complete board). Using a coincell/buttoncell battery I can fit all my components (except the battery and buzzer) on a 11x39mm PCB, targeting for 20x20mm now.

Link to post
Share on other sites

I'm busy desiging a PCB for this too (not a booster, a feature complete board). Using a coincell/buttoncell battery I can fit all my components (except the battery and buzzer) on a 11x39mm PCB, targeting for 20x20mm now.

Nice! You can panelize four on the standard 50x50mm Seeed/Elecrow size. I'd like to make a binary clock to go with my VFD clock on my desk at work. ;-)

Link to post
Share on other sites

You know, there are 10 types of binary clocks:

- "true" binary

- BCD binary

true binary
3 1
2 6 8 4 2 1
  O O O O O hours
O O O O O O minutes
O O O O O O seconds

BCD binary
8 4 2 1
      O PM/AM (or tens of hours)
O O O O ones of hours
  O O O tens of minutes
O O O O ones of minutes
  O O O tens of seconds
O O O O ones of seconds

The BCD binary clock is more widespread, but is also less challenging to read and way less compact to display.

The clock I made is "true" binary (ofcourse one could also display seconds since midnight or seconds since January 1st 1970, but those are as good as unreadable)

 

If you'd want one to go with your VFD clock, I suggest using neon lights, not LEDs :grin:

 

My main goal was to make a DIY binary watch as cheap as possible, so I ended up with:

- MSP430G2201 (1 timer with 2 CCR, 2kB flash, no other peripheral requirements)

- watch crystal

- two buttons

- one capacitor (reset pull down)

- five resistors (2 for hour display, 2 for minute display, 1 for reset pull up)

- eleven LEDs

 

I wanted to go with an even smaller msp430, but I cannot manage to squeeze my code in under 1280 bytes. The charlieplexing scheme takes quite some space (I estimate about 400 bytes).

 

Instead of panelizing 4 on the 50x50mm PCBs I'd rather go with a bunch of projects on a single PCB and order 10 of those, what on earth am I going to do with 40 watch PCBs?! On the other hand, 10 watches, 10 pong TV games, 10 triple dice and 10 iambic keyer decoders are much more useful ;)

Link to post
Share on other sites

...what on earth am I going to do with 40 watch PCBs?! On the other hand, 10 watches, 10 pong TV games, 10 triple dice and 10 iambic keyer decoders are much more useful

 

Of course, what are you going to do with 10 of each of those?

 

I'm probably going to use blue LEDs to match my blue VFD. Should look pretty cool.

Link to post
Share on other sites

I can't use blue LEDs in my setup, since I aim on powering from a coincell (3V nominal, 2V end-of-life) the forward current cannot exceed 3V minus twice the I/O pin voltage drop.

 

Yeah, 10 watches isn't going to be much use either, but I might reuse some for other projects, as it is basically a crystal, two buttons and 12 (1 not placed) charlieplexed LEDs. Maybe a thermometer (15-27 degrees celcius display, or 18-24 display with half degrees) or a turntable tuner?

Link to post
Share on other sites
  • 3 weeks later...

Alright, I finally feel confident enough to publish my code. Sorry for the short variable names, they don't help ease of reading.

For the time being, consider the code to be published under GPLv2 ;)

/*
       +-----   -----+
   3V3 |     \_/     | 0V
buzzer |             | XTAL
 LEDmA |             | XTAL
 LEDmB | MSP430G2001 | N/C     ]programming
 LEDmC |             | ButtNMI ]programming
 LEDhA |             | ButtP1
 LEDhB |             | LEDhC
       +-------------+

*******************************************************************************
***  WARNING: while programming (over Spy-Bi wire) DO NOT press ButtNMI!!!  ***
***  Doing so will enter JTAG mode, which may damage LED/ButtP1 circuits.   ***
*******************************************************************************

display: B1

display alarm: B2

en Al: hold B1

snooze: B1 or B2

alarm: hold B2
        ++: B2
        next: B1
        save: B1

set time: hold B2+B1
        ++: B2
        next: B1
        save: B1

when setting time/alarm:
        60 minutes will become 0 minutes
        61 minutes will use hours as snooze interval (0 minutes equals 1 day)
        62 minutes will set clock to 24-hour mode
        63 minutes will set clock to 12-hour mode
        61..63 will restart editing mode to set time/alarm resp.
*/
#define __USE_CLIB__
#ifdef __USE_CLIB__
#include <stdint.h>
#include <stdbool.h>
#else
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
#ifdef _Bool
typedef _Bool bool;
#else
typedef uint16_t bool;
#endif /*_Bool*/
#define true 1
#define false 0
#endif /*__USE_CLIB__*/
#include "msp430.h"

/* timer = 32ki / 8 / 8 = 512Hz => ~2msec
 * 512Hz / 6 (charlieplex) = 85Hz (refresh rate), halving this is unacceptable
 * >10msec button debounce => 10m*512 = 5.12 => 6 or 7 "ticks"
 * >2 sec button hold => 2*512 = 1024 => 1025 "ticks"
 */
#define PRESS_T      7
#define HOLD_T    1025
#define BLINK_T    256
#define DISPLAY_T 2049
#define BIP_T       87
#define BEEP_T     216
#define ALARM_EXPIRE_T (512 * 30)
#define ALARM_BIP_1_T    1
#define ALARM_MUTE_1_T  44
#define ALARM_BIP_2_T   87
#define ALARM_MUTE_2_T 130
#define ALARM_BIP_3_T  173
#define ALARM_MUTE_3_T 216
#define ALARM_BIP_4_T  259
#define ALARM_MUTE_4_T 302
#define ALARM_LOOP_MASK 511

#define BUZZER BIT0
#define LEDmA BIT1
#define LEDmB BIT2
#define LEDmC BIT3
#define LEDhA BIT4
#define LEDhB BIT5
#define LEDhC BIT6
#define SWITCH BIT7

#define HM2W(H, M) ((H) + 0x100 * (M)) /* convert hour, minute tuple to short */
///////////////////////////////////////////////////////////////////////////////
///  Type definitions  ////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
typedef enum // to use __even_in_range(), use only even numbers
{
  IDLE = 0,
  DISPLAY_TIME = 2,
  DISPLAY_ALARM = 4,
  EDIT_TIME = 6,
  EDIT_ALARM = 8,
  MAX_STATE = 8
} state_t;

enum
{
  nmi,
  p1,
  MAX_BUTTONS
};

typedef enum // to use __even_in_range(), use only even numbers
{
  MUTE = 0,
  BIP = 2,
  BEEP = 4,
  ALARM = 6,
  MAX_BUZZER_STATE = 6
} buzzer_t;

typedef struct
{
  uint16_t T;
  bool A;
  bool P;
  bool D;
  bool H;
} button_t;

typedef union
{
  uint16_t W;
  #pragma pack(1)
  struct
  {
    uint8_t H;
    uint8_t M;
  };
  #pragma pack()
} time_t;

///////////////////////////////////////////////////////////////////////////////
///  Constant declarations  ///////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
#define LED_CHARLIEPLEX_MAX 6
const uint8_t
  LED_PLEX[LED_CHARLIEPLEX_MAX] =
  {
    LEDmA | LEDmB | LEDhA | LEDhB | BUZZER,
    LEDmA | LEDmB | LEDhA | LEDhB | BUZZER,
    LEDmA | LEDmC | LEDhA | LEDhC | BUZZER,
    LEDmA | LEDmC | LEDhA | LEDhC | BUZZER,
    LEDmC | LEDmB | LEDhC | LEDhB | BUZZER,
    LEDmC | LEDmB | LEDhC | LEDhB | BUZZER
  };

const uint8_t LEDs = LEDmA | LEDmB | LEDmC | LEDhA | LEDhB | LEDhC;

const time_t LED_ALL_ON = {0x1F2F};

///////////////////////////////////////////////////////////////////////////////
///  Variable declarations  ///////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
button_t
  B[MAX_BUTTONS];
uint16_t
  D_T,
  E_b,
  S_T,
  Buzz_T;
time_t
  time,
  alarm,
  A;
uint8_t
  D_state,
  S,
  LED[LED_CHARLIEPLEX_MAX];
time_t*
  E;
state_t
  state;
bool
  PM;
buzzer_t
  Buzz;

///////////////////////////////////////////////////////////////////////////////
///  Function declarations  ///////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
inline void Display(void);
void CalculateTime(time_t time);
inline void HandleState(void);
inline void Buzzer(void);

///////////////////////////////////////////////////////////////////////////////
///  Function definitions  ////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void main(void)
{
  WDTCTL = WDTPW | WDTHOLD;
  DCOCTL = 0; // This is a fix for bug BCL12 as described in errata slaz061b
  BCSCTL1 = CALBC1_1MHZ | DIVA_3; // Set ACLK to LFXT1 / 8 = 32kiHz / 8 = 4kiHz
  DCOCTL = CALDCO_1MHZ; // Last step calibration loading
  //BCSCTL2 = DIVS_3; // SMCLK = DCO / 8 = 1 MiHz / 8 = 128 kiHz
  BCSCTL3 = XCAP_3; // TODO: set XCAP to correct setting, depend on XTAL
  P2SEL = BIT6 | BIT7; // Use P2 pins for crystal

  // Set NMI switch to H->L interrupt
  // Set WDT to 250ms (@32kiHz) timer; actual ACLK /= 8, hence 250ms * 8 = 2 sec
  WDTCTL = WDTNMIES | WDTNMI | WDT_ADLY_250;
  IFG1 = 0;
  IE1 = NMIIE | WDTIE;

  //Set P1 switch to pull-up, H->L interrupt
  P1REN = SWITCH;
  P1OUT = SWITCH;
  P1IES = SWITCH;
  P1IFG = 0;
  P1IE  = SWITCH;

  // Set TimerA to overflow at 2 / (ACLK / 4) = 2 / (4ki / 4) = 512 Hz
  TACTL = TASSEL_1 | ID_2;
  TACCTL0 = CCIE;
  TACCR0 = 1;

  P1DIR = BUZZER; // Set buzzer pin to be output
  P1REN = LEDs | SWITCH;

  __low_power_mode_3();
  __no_operation();
}

// NMI button pressed
#pragma bis_nmi_ie1 = NMIIE /* reenable NMIIE as part of the ISR */
#pragma vector = NMI_VECTOR
__interrupt void NMI_ISR(void)
{
  if (WDTCTL & WDTNMIES)
  { // a H->L (press) was triggered
    B[nmi].A = true;
    TACTL |= MC_1;
  }
  else
  { // a L->H (release) was triggered
    B[nmi].A = false;
    B[nmi].D = false;
  }
  B[nmi].T = 0;
  WDTCTL ^= (WDTPW ^ 0x6900) | WDTNMIES; // invert trigger edge
  IFG1 &= ~NMIIFG;
}

// P1 button pressed
#pragma vector = PORT1_VECTOR
__interrupt void PORT1_ISR(void)
{
  P1IFG = 0;
  B[p1].T = 0;
  B[p1].A = true;
  TACTL |= MC_1;
}

// Timer overflows (1 minute)
#pragma bis_nmi_ie1 = WDTIE
#pragma vector = WDT_VECTOR
__interrupt void WDT_ISR(void)
{
  if (state == EDIT_TIME) return;
  if(!S)
  {
    S = 30;
    // Increment time
    time.M += 1;
    if (time.M >= 60)
    {
      time.M = 0;
      time.H += 1;
    }
    if (time.H >= 24)
    {
      time.H = 0;
    }
    if (state == DISPLAY_TIME) CalculateTime(time);
    if (A.W == time.W)
    {
      Buzz = ALARM;
      TACTL |= MC_1;
    }
  }
  S--;
}

// Timer compare match
#pragma vector = TIMER0_A0_VECTOR
__interrupt void TIMER0_A0_ISR(void)
{
  if (state != IDLE) Display();

  // Parse button debounce and hold code
  if (P1IN & SWITCH){
    B[p1].A = false;
    B[p1].D = false;
  }
  for(int i = 0; i < MAX_BUTTONS; ++i)
  {
    if (B[i].A)
    {
      if (B[i].T == PRESS_T)
      {
        B[i].P = true;
        B[i].D = true;
      }
      if (B[i].T == HOLD_T)
      {
        B[i].A = false; // Stop monitoring button after hold period
        B[i].H = true;
      }
      B[i].T++;
    }
  }

  HandleState();
  Buzzer();

  for(int i = 0; i < MAX_BUTTONS; ++i) B[i].H = B[i].P = false;

  // Check for fast timer activities, shut down fast timer if none
  if (!(B[0].A || B[1].A || (Buzz != MUTE) || (state != IDLE)))
    TACTL &= ~MC_1;
}

void Display(void)
{
  P1OUT &= ~LEDs; // Clear LED pins
  P1DIR = LED_PLEX[D_state];
  P1OUT = LED[D_state];
  P1REN &= ~LEDs;
  D_state++;
  if (D_state >= LED_CHARLIEPLEX_MAX) D_state = 0;
}

void CalculateTime(time_t time)
{
  // Only translate if not in editing mode
  if ((state != EDIT_TIME) && (state != EDIT_ALARM))
  {
    // Translate given time to correct binary presentation
    if (PM) // if using 12-hour clock system
    {
      if (time.H >= 12) time.H += -12 + 0x10; // 12..23 => 0..11 pm
      if ((time.H & 0x0F) == 0) time.H += 12; // 0 (am/pm) => 12 (am/pm)
    }
    else // if using 24-hour clock system
    {
      if ((time.M == 0) && (time.H == 0))
        time.H = 24; // 0.00 => 24.00 (never display "nothing")
    }
  }
  // Transpose given binaries to bitpatterns
  LED[0] = LED[1] = LED[2] = LED[3] = LED[4] = LED[5] = SWITCH;
  if (time.M & 0x01) LED[0] |= LEDmA;
  if (time.M & 0x02) LED[1] |= LEDmB;
  if (time.M & 0x04) LED[2] |= LEDmA;
  if (time.M & 0x08) LED[3] |= LEDmC;
  if (time.M & 0x10) LED[4] |= LEDmB;
  if (time.M & 0x20) LED[5] |= LEDmC;
  if (time.H & 0x01) LED[0] |= LEDhA;
  if (time.H & 0x02) LED[1] |= LEDhB;
  if (time.H & 0x04) LED[2] |= LEDhA;
  if (time.H & 0x08) LED[3] |= LEDhC;
  if (time.H & 0x10) LED[4] |= LEDhB;
  /*if (time.H & 0x20) LED[5] |= LEDhC;*/ //This LED is not placed
}

void HandleState(void)
{
  switch (__even_in_range(state, MAX_STATE))
  {
    case IDLE:
      if (!B[0].P && !B[1].P) break;
      /*no break*/
    case DISPLAY_TIME:
    case DISPLAY_ALARM:
      D_T++;
      if (B[0].P || B[1].P) D_T = 0;
      if (B[0].H || B[1].H)
      {
        Buzz = BEEP;
        if (B[0].D && B[1].D)
        {
          state = EDIT_TIME;
          E = &time;
          E_b = HM2W(0x10, 0x00);
          if (PM) // if using 12-hour clock system
          {
            if (time.H >= 12) time.H += -12 + 0x10; // 12..23 => 0..11 pm
            if ((time.H & 0x0F) == 0) time.H += 12; // 0 (am/pm) => 12 (am/pm)
          }
        }
        else if (B[1].D)
        {
          state = EDIT_ALARM;
          E = &alarm;
          E_b = HM2W(0x10, 0x00);
          if (PM) // if using 12-hour clock system
          {
            if (time.H >= 12) time.H += -12 + 0x10; // 12..23 => 0..11 pm
            if ((time.H & 0x0F) == 0) time.H += 12; // 0 (am/pm) => 12 (am/pm)
          }
        }
        else /*if (B[0].D)*/
        {
          if (A.W != alarm.W)
          {
            A.W = alarm.W;
            CalculateTime(alarm);
            D_T = 0;
          }
          else
          {
            A.W = HM2W(0x40, 0x00); // will never match; alarm off
            D_T = DISPLAY_T; // turn off display to confirm turning off alarm
          }
        }
      }
      else if (BCSCTL3 & LFXT1OF)
      {
        state = DISPLAY_TIME;
        CalculateTime(LED_ALL_ON); // if oscillator fault, set all LEDs on
      }
      else if (B[0].P)
      {
        state = DISPLAY_TIME;
        CalculateTime(time);
      }
      else if (B[1].P && !B[0].D)
      {
        state = DISPLAY_ALARM;
        CalculateTime(alarm);
      }
      if (D_T >= DISPLAY_T)
      {
        P1OUT = SWITCH;
        P1REN = LEDs | SWITCH;
        P1DIR = LEDs | BUZZER;
        state = IDLE;
      }
      /* else; */
      break;
    case EDIT_TIME:
    case EDIT_ALARM:
      if (E_b == HM2W(0x40, 0x00))
      {
        if (B[0].P || B[1].P)
        {
          Buzz = BEEP;
          if (E->M == 61) S_T = E->H;
          else if (E->M == 62) PM = false;
          else if (E->M == 63) PM = true;
          else
          {
            if (PM == true)
            {
              if ((E->H & 0x0F) == 12) E->H ^= 0x10; // "12am"->28, "12pm"->12
              if (E->H >= 16) E->H += 12 - 0x10; // "17..28"->13..24
              // this parsing allows: "13..15am"->13..15, "0pm"->12 "0am"->0
            }
            if (E->H >= 24) E->H = 0; //24->0, also filters invalid entries
            if (E->M >= 60) E->M = 0;
            TAR = 0; // NOW is the start of a minute
            S = 29; // reload seconds timer with a whole minute interval
            P1OUT = SWITCH;
            P1REN = LEDs | SWITCH;
            P1DIR = LEDs | BUZZER;
            state = IDLE;
            break;
          }
          // not a valid value for the time/alarm; start receiving input again
          E_b = HM2W(0x10, 0x00);
          //E->W = 0;
        }
        break;
      }
      D_T++;
      if (B[0].P) E->W |= E_b;
      else if (B[1].P) E->W &= ~E_b;
      else
      {
        if (D_T > BLINK_T)
        {
          D_T = 0;
          E->W ^= E_b;
          CalculateTime(*E);
        }
        break;
      }
      Buzz = BIP;
      E_b >>= 1;
      if (E_b == (HM2W(0x00, 0x01) >> 1)) E_b = HM2W(0x40, 0x00);
      if (E_b == (HM2W(0x01, 0x00) >> 1)) E_b = HM2W(0x00, 0x20);
        // After H, go to M edit point
      CalculateTime(*E);
      break;
  }
}

void Buzzer(void)
{
  switch(__even_in_range(Buzz, MAX_BUZZER_STATE))
  {
    case MUTE:
      break;
    case BIP:
      if (Buzz_T >= BIP_T)
      {
        P1SEL &= ~BUZZER;
        Buzz = MUTE;
        break;
      }
      P1SEL |= BUZZER;
      break;
    case BEEP:
      if (Buzz_T >= BEEP_T)
      {
        P1SEL &= ~BUZZER;
        Buzz = MUTE;
        break;
      }
      P1SEL |= BUZZER;
      break;
    case ALARM:
      if ((Buzz_T >= ALARM_EXPIRE_T) || B[0].P || B[1].P)
      { // Snooze after expiration or button press
        A.M += S_T;
        if (A.M >= 60)
        {
          A.M -= 60;
          A.H++;
          if (A.H >= 24) A.H = 0;
        }
        P1SEL &= ~BUZZER;
        Buzz = MUTE;
        break;
      }
      switch(Buzz_T & ALARM_LOOP_MASK)
      {
        case ALARM_BIP_1_T:
        case ALARM_BIP_2_T:
        case ALARM_BIP_3_T:
        case ALARM_BIP_4_T:
          P1SEL |= BUZZER;
          break;
        case ALARM_MUTE_1_T:
        case ALARM_MUTE_2_T:
        case ALARM_MUTE_3_T:
        case ALARM_MUTE_4_T:
          P1SEL &= ~BUZZER;
          break;
      }
      break;
  }
  Buzz_T++;
  if (Buzz == MUTE) Buzz_T = 0;
}

I use IAR, so you'll find some IAR specifics in this code (like #pragma bis_nmi_ie1 = WDTIE, which reenable the watchdog timer after the ISR).

 

Comments are welcome :)

Link to post
Share on other sites

Yes, I would like to make it wearable, my current design is 11x39mm, so it must be doable.

I chose 2x3 charlieplexing instead of 1x4 because I can have a better refreshrate and te maybe have more current available per LED.

With a 1x4 display, I'd have a 1/11th dutycycle (or 1/4th if using multiple sinks at once), while with a 2x3 display my dutycycle increases to 1/6th (or 1/3rd if using multiple sinks)

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...