Close

Hacking on Ted's LED clock software

ken-yapKen Yap wrote 05/21/2020 at 04:42 • 7 min read • Like

@Paul Gallagher recently did a respin, or should that be a re-ring, of @Ted Yapo 's 20 year old clock project that used a PIC16F84. I thought the original and the tribute were fantastic projects and was curious about the software. As expected it's very compact PIC assembler. I wondered whether it could be re-expressed in C, mainly as an excuse for me to get familiar with the PIC16 architecture and the MPLAB X IDE, but also to perhaps implement in future with a different MCU. Hence this quick project.

The original clock.asm is 299 lines, consisting of a main routine, a couple of utility routines and an interrupt routine which is run 32 times per second to update the display and handle the buttons. After setting up the registers, the main routine just loops indefinitely and all the work is done by the interrupt routine. So it's nearly all straight line code.

The assembler was fairly easy to translate but the branches gave me a bit of headache until I defined a handy macro. I ended up with a clock.c of 114 lines, more readable of course.

I wanted to know what the penalty was in terms of code size. According to the map generated by the assembler, 163 words were used. The C version required 261 words. Comparing the assembler code I found several reasons for this. One is that a more general table lookup routine for the hand to port bit translation is used, one that apparently caters to large tables. There is also some C startup code. The compiler generates longer sequences in some cases. For example an increment of a variable, for example tick, was this in the original.

        INCF    tick

but the C compiler generated this:

        movlw   1
        movwf   ??_tc_int
        movf    ??_tc_int,w
        addwf   _tick,f

Not sure why it had to do those extra moves.

Finally the C compiler didn't know that main goes into a loop, so there is no need to save context on entering an interrupt and restore on exit. Of course these days 100 words is nothing in the scheme of thngs for MCUs. Except for the context save and restore, the interrupt routine should run at the same speed. At 32 Hz, it doesn't matter anyway.

A few things worth mentioning. The free xc8 compiler will only do -O2. You need to pay for higher levels. Also the free version won't generate an assembler file, even if you tick the box in the config, but you can observe the xc8-cc command that it runs to generate an object and then substitute -S for -c, or add it in the command. I did not need the register bank switching macros as xc8 knows to insert them when needed.

Microchip recommends that interrupt functions be kept small so that less or no context has to be saved. To achieve this, you have to convert this pattern:

void __interrupt() tc_int(void)
{
    lots of work
    ...
    T0IF = 0;
}
...
void main(void)
{
    startup initialisation
    ...
    while (1)
        ;
}

to:

unsigned char flag;

void __interrupt() tc_int(void)
{
    flag = 1;
    T0IF = 0;
}
...
void main(void)
{
    startup initialisation
    ...
    while (1) {
        flag = 0;
        while (!flag)
            ;
        lots of work
        ...
    }
}

Anyway here is the C code in its entirety for interest. The #pragmas for the configuration bits can be generated with a tool in the IDE. I haven't checked that the branches are correct but they should be easy to fix if I ever build one. The 16C84 is overpriced now for its capability as it is used for repairing old equipment. A more modern member of the family might be cheaper.

// Clock I driver - LED clock
// Copyright (C) 2000, Theodore C. Yapo.  All rights reserved.

// CONFIG
#pragma config FOSC = LP        // Oscillator Selection bits (LP oscillator)
#pragma config WDTE = ON        // Watchdog Timer (WDT enabled)
#pragma config PWRTE = OFF      // Power-up Timer Enable bit (Power-up Timer is disabled)
#pragma config CP = OFF         // Code Protection bit (Code protection disabled)

#include <xc.h>

#define REGBANK_0       RP0 = 0
#define REGBANK_1       RP0 = 1
#define IS_UP(x)        (x == 1)

unsigned char tick, hour, minute_1, minute_5, second_1, second_5, porta_value, portb_value, minutes_button, hours_button;
const unsigned char hand_porta[] = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8, 0x4, 0x2, 0x1, 0x0};
const unsigned char hand_portb[] = {0x40, 0x20, 0x10, 0x08, 0x04, 0x02, 0x01, 0x0, 0x0, 0x0, 0x80};

void inc_hours(void) {
    hour++;
    if (hour < 12)
        return;
    hour = 0;
}

void inc_seconds(void) {
    second_1++;
    if (second_1 < 5)
        return;
    second_1 = 0;
    second_5++;
    if (second_5 < 12)
        return;
    second_5 = 0;
    minute_1++;
    if (minute_1 < 5)
        return;
    minute_1 = 0;
    minute_5++;
    if (minute_5 < 12)
        return;
    inc_hours();
}

void __interrupt() tc_int(void) // interrupt function
{
    tick++;
    if (tick & 32) {
        tick = 0;
        inc_seconds();
    }
    portb_value = porta_value = 0;
    // if minutes are being adjusted, don't display hours
    if (IS_UP(minutes_button)) {
        porta_value = hand_porta[hour];
        portb_value = hand_portb[hour];
    }
    // if hours are being adjusted, don't display minutes
    if ((IS_UP(hours_button) && IS_UP(minutes_button)) || !(tick & 16)) {
        porta_value ^= hand_porta[minute_5];
        portb_value ^= hand_portb[minute_5];
    }
    // if minutes are being adjusted, don't display seconds
    // if hours are being adjusted, don't display seconds
    if (IS_UP(minutes_button) && IS_UP(hours_button) && (tick & 4)) {
        porta_value ^= hand_porta[second_5];
        portb_value ^= hand_portb[second_5];
    }
    // check for button press
    // REGBANK_1;       // compiler makes it happen
    TRISB |= 0x4;       // RB2 input
    TRISA |= 0x8;       // RA3 input
    // REGBANK_0;       // compiler makes it happen
    // check minutes (must be pressed for at least 4 ticks)
    if (PORTB & 0x4)
        minutes_button = 0;
    minutes_button++;
    if (minutes_button & 0x10) {
        minutes_button = 2;
        minute_1 = second_5 = second_1 = 0;
        minute_5++;
        if (minute_5 >= 12)
            minute_5 = 0;
    }
    // check hours (must be pressed for at least 4 ticks)
    if (PORTA & 0x8)
        hours_button = 0;
    hours_button++;
    if (hours_button & 0x10) {
        hours_button = 2;
        inc_hours();
    }
    // REGBANK_1;       // compiler makes it happen
    TRISB &= ~0x4;
    TRISB &= ~0x8;
    // REGBANK_0;       // compiler makes it happen
    PORTA = porta_value;
    PORTB = portb_value;
    T0IF = 0;
}

void main(void) {
    PORTA = 0;
    PORTB = 0;
    // REGBANK_1;       // compiler makes it happen
    TRISA = 0;
    TRISB = 0;
    OPTION_REG = 0xC8;
    // REGBANK_0;       // compiler makes it happen
    INTCON = 0xA0;
    tick = hour = minute_1 = minute_5 = second_1 = second_5 = porta_value = portb_value = minutes_button = hours_button = 0;
    while (1);
}

Addendum: It turns out that SDCC can compile for the 16F84, but you have to use the non-free includes and libraries, which means building them from the source distribution.

I made one small change to the source for SDCC interrupt handler syntax, and compiled it. The result was:

;    code size estimation:
;     242+   27 =   269 instructions (  592 byte)

Note that I had already made the transformation of moving most of the work into the main routine, so there is a little discrepancy, but even so SDCC does better in code space (242 vs 255) so is a suitable alternative to Microchip's compiler.

Like

Discussions

Ted Yapo wrote 05/21/2020 at 12:54 point

hey, fun stuff!

Which version of the XC8 compiler are you using? I think the 2.10 and later versions allow for more optimizations, up to -O2 . If you haven't tried, it might be worth a test to see if it makes any difference.

  Are you sure? yes | no

Ken Yap wrote 05/21/2020 at 13:13 point

About gives this:

Product Version: MPLAB X IDE v5.00
Java: 1.8.0_144; Java HotSpot(TM) 64-Bit Server VM 25.144-b01
Runtime: Java(TM) SE Runtime Environment 1.8.0_144-b01
System: Linux version 4.12.14-lp151.28.48-default running on amd64; UTF-8; en_AU (mplab)
User directory: /home/ken/.mplab_ide/dev/v5.00
Cache directory: /home/ken/.cache/mplab_ide/dev/v5.00/var

Not sure what version that corresponds to. I fetched it in 2018. I'm using the free features when I tried to choose > -O1 it said that required a license.

  Are you sure? yes | no

Ted Yapo wrote 05/21/2020 at 15:09 point

I don't know how to check the XC8 version in Windows; on linux I need to download the compilers separately from MPLABX, so I know which one I have.

It looks like maybe you install the same way in windows? On the download tab on this screen, I see you can get the install files for XC8 for windows.

https://www.microchip.com/mplab/compilers

V2.10 of XC8 which includes the free optimizations was released on 28 July 2019. The latest version is 2.20, released 4/29/2020.

  Are you sure? yes | no

Ken Yap wrote 05/21/2020 at 21:02 point

Ok, found the download page. I was using 2.0. With 2.2 the max free level is -O2 and now the size is:

Memory Summary:
    Program space        used    FFh (   255) of   400h words   ( 24.9%)
    Data space           used     Fh (    15) of    44h bytes   ( 22.1%)
    EEPROM space         used     0h (     0) of    40h bytes   (  0.0%)
    Configuration bits   used     1h (     1) of     1h word    (100.0%)
    ID Location space    used     0h (     0) of     4h bytes   (  0.0%)

Comparing the assembler generated code it's not bad, after all the mapping is pretty much one-to-one. I think the difference is mainly due to the overhead of startup routines. Might be possible to measure that by compiling an empty program. But up to here is all the work I'm willing to put into this quickie.

  Are you sure? yes | no