roadrunner84 466 Posted January 27, 2013 Share Posted January 27, 2013 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. bluehash, sirri and SirPatrick 3 Quote Link to post Share on other sites
GeekDoc 226 Posted January 28, 2013 Share Posted January 28, 2013 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. ;-) Quote Link to post Share on other sites
roadrunner84 466 Posted January 28, 2013 Author Share Posted January 28, 2013 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 Quote Link to post Share on other sites
GeekDoc 226 Posted January 29, 2013 Share Posted January 29, 2013 ...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. Quote Link to post Share on other sites
roadrunner84 466 Posted January 29, 2013 Author Share Posted January 29, 2013 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? Quote Link to post Share on other sites
roadrunner84 466 Posted February 19, 2013 Author Share Posted February 19, 2013 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 Quote Link to post Share on other sites
GG430 53 Posted February 19, 2013 Share Posted February 19, 2013 Cool project! The idea with using minutes >60 for something else is a cool feature. Do you want to make it a wearable finally? Why did you split the charlieplexing to 2x3 vs. 1x4? Quote Link to post Share on other sites
roadrunner84 466 Posted February 20, 2013 Author Share Posted February 20, 2013 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) Quote Link to post Share on other sites
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.