Firmware for the Solartron clock

Given that both hardware, will and time was at hand, there was no reason not to go through with finishing up the Solartron conversion project and finally writing firmware to have it work as an actual clock.

Given that it’s been ages since I did slightly more structured programming in C outside Arduino stuff, the code is bit of a trainwreck but it does run. More importantly, diving into projects like this gives plenty of opportunity to think of possible improvements when it comes to both code and knowledge.

Not good, not terrible.

Scope and tools

Now, we’ve seen in the previous part what was implemented in hardware blocks, as shown in the below figure.

clock_block_schematic-Page-1

Given that background we will at least need to implement rudimentary support for LED drivers, RTC and user input/output.

A brief outline of functional cases from an user perspective would be:

  • Display clock with seconds marker
  • Setting time via rotary encoder and/or serial interface
  • The time should be persisted to the RTC and read on power-up

Additionally, probably a few display modes to help debugging and general mischief.

Stack and toolchain

The firmware will be based on LUFA since I’m using an atmega32u4 and I want to use the USB Virtual COM debug/control.

I’ve chosen to go with Microchip Studio to minimize the amount of footwork needed in setting up a build environment and there is support for configuring LUFA. Unfortunately, this also results in something where it isn’t always easy to understand what’s going on and not to mention the extra stuff that ends up in version control.

It doesn’t help that the old Visual Studio interface of Microchip Studio hasn’t aged well, but at least it’s better than the old Netbeans interface of the MPLAB X IDE. If I could easily do so with my current setup, I’d probably go with a makefile or cmake-based system and VS Code or perhaps JetBrains CLion. Unless I’m lazy again and go with something that already exists and is fit for purpose.

Programmer

If you’ve looked in the blog archives, you’ll see that there’s a post on using an Arduino as programmer. That doesn’t seem to work very well with Microchip Studio, but could be used via avrdude. I later ended up getting a Microchip SNAP programmer, as it was cheap and could integrate well with Microchip Studio - but I couldn’t get that working in avrdude instead. MPLAB X IPE might be an option for commandline programming, instead of avrdude.

Honestly I think the biggest mistake made here was to not have routed out the JTAG pins of the atmega32u4 to a connector. That way, it was all blind debugging with sprinkles of fprint.

Microchip SNAP

The Microchip SNAP is switchable between PIC and AVR programming modes, but it took quite a while to actually get it working well. It started out with consistently failing communication, although being detected correctly. Once I did get it working, I couldn’t actually tell what step in the process actually made it work.

Firmware implementation

I won’t go through the full code involved here, but if you want to have a look at the source you can find it on GitLab: https://gitlab.com/atnon/clockcontroller-firmware . I’ll pick out a few parts here and give you some thoughts on them. Note that the details around wrangling the LUFA framework are left out, as that was mostly handled though Microchip Studio. The code is actually based on the VirtualSerial example from the LUFA demo collection, which was very helpful.

Main application

The top level application is contained in ClockController files, so this is also where all application logic resides (mostly anyway). Most of the USB configuration is carried over directly from the LUFA example mentioned, and doesn’t really get in the way.

Outside the initialization and main loop in the ClockController, there are helper functions and variables. I’ve left those out here, but they are available in the repository.

Initialization

The startup is an exercise in datasheet reading for the RTC and LED drivers, so nothing too surprising.

This includes:

  • Watchdog disable
  • SPI and I2C setup
  • Setup LED drivers
    • Activate oscillator
    • Accept multiple register writes in a single access (auto-increment address)
    • Set all outputs to be PWM controlled
  • Enable 1 Hz signal from RTC (tied to INT2 on rising edge)
  • USB initialization (courtesy of LUFA)
  • Setup encoder state machine and switch debouncing
  • Setup serial command interface

Main loop

This goes on to an infinite main loop which executes task workers to handle/generate events and several state machines1. Below code snippet has been heavily edited for brevity but has more comments instead.

for (;;)
    {
        /* USB and serial things. */
        CDC_Device_USBTask(&VirtualSerial_CDC_Interface);
        USB_USBTask();
        ComTask(&VirtualSerial_CDC_Interface, &USBSerialStream);
        
        /*  Read encoder pins and feed into encoder task to 
            determine if there has been a valid rotation. */
        int enc_a = (PINB >> PINB5) & 1;
        int enc_b = (PINB >> PINB6) & 1;
        
        EncoderTask(&enc, enc_a, enc_b);
        
        if (enc.updated) { /* enc.update = true if there has been a valid rotation. */
                /* Adjust minutes/hours count and send to display 
                    if we are in one of the set modes SetHours or SetMinutes */
                /* ... code ... */
            enc.updated = 0;
        }
        
        /* Read encoder switch and feed into debounce task. */
        int enc_sw = (PINB >> ENC_SW) & 1;
        DebounceTask(&sw, enc_sw);
        
        if (sw.updated && sw.state) { /* If switch update is valid and pressed switch. */
            switch(CurrentMode) {
                    /* Set NextMode to a new mode depending on state
                    when the button is pressed.
                    (CurrentState -> Nextstate)
                    ClockHours -> SetHours
                    SetHours -> SetMinutes
                    SetMinutes -> SetTime
                    */
                    /* ... code ... */
            }
            sw.updated = 0;
        }
        
        /* Mode transition actions. */
        if (NextMode != CurrentMode) {
            /* Default all segment backgrounds. Default is red background. */
            
            switch(NextMode) {
                /*  Sometimes we need to carry out actions in conjunction with 
                    mode transitions. That is done here.*/
                /* ... code ... */
            }
            CurrentMode = NextMode; /* Tick the state machine to the next state. */
        }
        
        /* If a background color update has been queued, process it. */
        if (BgUpdate) {
            switch(CurrentMode) {
                /*  Different modes will need different toggles.
                    E.g. in the SetHours state, the background needs to blink.
                    So the 1 Hz interrupt will trigger a BgUpdate = true*/
                /* ... code ... */
            }
            BgUpdate = false;
            SendToDisplay = true;
        }
        
        /*  If a display update has been queued, process it. 
            This only updates the internal display representation.*/
        if (UpdateQueued) {
            switch(CurrentMode) {
                /*  Different modes require different update regimes.
                    E.g. when in a clock mode, we get values from the RTC whereas
                    a simple counter will be different. */
                /* ... code ... */
            }
            SendToDisplay = true;
        }
        
        if (SendToDisplay) {
            /* Correspondingly, this is where actual I2C writes are triggered. */
            /* ... code ... */
            }
            SendToDisplay = false;
        }
    }
}

The state machine implementation is a bit chaotic, it would probably make sense to try implement it as a table-based state machine or similar.

1 Hz interrupt

The RTC is configured to pulse once per second and as already mentioned the INT2 interrupt is triggered accordingly. This is used to make sure the display updates with new time, but also to toggle display background for second indicator and time set modes. The code looks like this:

ISR(INT2_vect) {
	switch(CurrentMode) {
		case Seconds: 		/* Fall-through! */
		case ClockHours:	/* Fall-through! */
		case ClockMinutes:
			UpdateQueued = true;
			BgUpdate = true;
		break;
		case SetMinutes: 	/* Fall-through! */
		case SetHours:
			BgUpdate = true;
	default:
		break;
	}
}

Encoder and debounce

The encoder files contain the implementations related to decoding the rotary encoder and debouncing the switch that is also in the rotary encoder. Both are doing a sort of debounce task, but in completely different ways.

Decoding the encoder

All switches are bouncy to some extent, as is the output of the rotary encoder. However, we can use the fact that there is a grey-coded order of the two phases that it needs to step through in order to be valid. An illustration of the outputs from the rotary encoder can be seen below, the detents of the encoder are marked by a blue dot.

Quadrature output of rotary encoder

Seen in the main loop, we call the encoder task once per iteration

EncoderTask(&enc, enc_a, enc_b);

which in implementations looks like

void EncoderTask(struct EncoderData *enc, int a, int b) {
    
    /* Encoder state machine */
    int dataIn = ((b << 1) | a );
    
    switch(enc->currentState) {
        case 0b11: /* Encoder is in a default detent. */
            /* Always starting here, more or less. */
            if (dataIn == 0b00) {
                enc->currentState = 0b00;
            }
            break;
        case 0b00:
            if (dataIn == 0b01) {
                enc->currentState = 0b01;
            } else if (dataIn == 0b10) {
                enc->currentState = 0b10;
            }
            break;
        case 0b01:
            if (dataIn == 0b11) {
                enc->currentState = 0b11;
                enc->updated = 1; /* Clockwise = increment. */
            } else if (dataIn == 0b00) {
                enc->currentState = 0b00;
            }
            break;
        case 0b10:
            if (dataIn == 0b11) {
                enc->currentState = 0b11;
                enc->updated = -1; /* Counter-clockwise = decrement. */
            } else if (dataIn == 0b00) {
                enc->currentState = 0b00;
            }
            break;
        default:
            break;
    }

struct EncoderData {
    int currentState;
    int8_t updated;
};

This results in the state machine having to step through each of the quadrature phases of the rotary encoder, before an event is triggered.2 Also note that the last state has no path back to the next-to-last state, this is to prevent intermittent values. Seems to work as it should without skipping values.

The event is checked for through the enc->updated flag of the struct, if it’s non-zero it will indicate a clockwise or counter-clockwise rotation.

Button debounce

When creating the debounce mechanism, I didn’t want to use any timers. Argument being that the buttons are pretty slow and low-priority in relation to everything else anyway, so I went with a polling-based method. The basic concept is that the pin has to stay in a new state for a threshold number of polls, if there are intermittent values then the counter is restarted.

A first implementation of such behavior would be something along the lines of the following code, but there is one issue.

void DebounceTask(struct DebounceData *sw, int swInput) {
	/* Switch state machine. */
	if (swInput != sw->prevState) {
		sw->prevState = swInput;
		sw->count = 0;
	}
	if (sw->count > sw->threshold) {
		sw->state = sw->prevState;
		sw->updated = 1;
		sw->count = 0;
	} else {
		sw->count++;
	}
}

What would happen is that the button event sw->updated would be retriggered as soon as the event was cleared while the button in pressed. That could be of interest for some implementations, but I opted to go for a latching mechanism where the counting would not start until the button has changed state again. That means one event per press/release of the button.

void DebounceTask(struct DebounceData *sw, int swInput) {
	/* Switch state machine. */
	if (swInput != sw->prevState) {
		sw->prevState = swInput;
		sw->count = 0;
		sw->latch = 0; /* Switch has changed state, counting may resume. */
	}
	if (sw->count > sw->threshold) {
		sw->state = sw->prevState;
		sw->latch = 1;
		sw->updated = 1;
		sw->count = 0;
	} else if (sw->latch == 0) { /* If the latch has not been reset, no counting! */
		sw->count++;
	}
}

The button press event is checked and cleared in the main loop like if (sw.updated) {/* code */; sw.updated = 0;}.

A general drawback of this implementation is that the struct to save the state and variables in is pretty big with its six words. Additionally, it might vary in time from button press to event being registered depending on how much other stuff there is in the main loop.

Command line

All behavior related to the serial interface and command line interface is placed in the CmdLine files, where three functions are present: ComTask(), processCmd(...) and SetCmdList(...). Additionally, there’s the command buffer used to store data received from the USB serial stream until processing.

Serial processing

One per main loop iteration, the ComTask()function is run, saving characters (if any) to the command buffer. Once a line break has been received, the processCmd()function is called to act on the command data.

void ComTask(USB_ClassInfo_CDC_Device_t *interface, FILE *USBSerialStream) {
    char CharIn;
    if (CDC_Device_BytesReceived(interface)) {
        CharIn = CDC_Device_ReceiveByte(interface);
        if ((CharIn == 0x0A) || (CharIn == 0x0D)) { /* Newline or carriage return */
            *CmdBufferPtr = NULL; /* Add a null termination to mark end of string. */
            CmdBufferPtr = CmdBuffer; /* Reset pointer to start of buffer. */
            processCmd(CmdBuffer, USBSerialStream); /* Evaluate command buffer. */
        } else { /* save characters received */
            *CmdBufferPtr = CharIn;
            fputc(CharIn, USBSerialStream); /* Local echo */
            if (CmdBufferPtr < (CmdBuffer + sizeof(CmdBuffer))) {
                CmdBufferPtr++;
                } else { /* If the buffer is full, we will start over. */
                CmdBufferPtr = CmdBuffer;
                /* This would probably not end nicely, but at least we wouldn't go all
                   segmentation fault. */
            }
        }
    }
}

A ring buffer could be used, but we’re resetting the buffer every time we process a command anyway. It’s unlikely to get 128 char commands in this application and there’s only ASCII text expected.

Command processing

Before any command processing can actually be done, there is a bit of setup to be done. The command processor is based on walking through a list of structs containing the command name, associated callback and help text. This is setup by feeding such a struct list to SetCmdList(...) during intialization, like this:

/* Structs defined in CmdLine.h */
typedef void (*pfnAction)(char **saveptr);

typedef struct {
    char *name;
    pfnAction action;
    char *help;
} cmdItem;

/* Variables specified in CmdLine.c */
cmdItem *cmdList = NULL;
int cmdListLength = 0;

/* Related code from CmdLine.c */
void SetCmdList(cmdItem *cmdDef, int numItems) {
    cmdList = cmdDef;
    cmdListLength = numItems;
}

/* Code used during initialization. */
void CmdSeconds(char **saveptr) {NextMode = Seconds;}
/* ... */
void CmdSetMinutes(char **saveptr) {NextMode = SetMinutes;}

cmdItem cmdList[12] = {
	{"seconds", CmdSeconds, "Seconds counting mode"},
	/* ... */
	{"setminutes", CmdSetMinutes, "Set minutes mode"}
};

SetCmdList(cmdList, 12);

It’s important to note here that all callbacks take the parameter char **saveptr, this is to be able to defer any further command processing of e.g. arguments to the serial commands, if any. Continuing to the actual implementation of the command processor, the code outline looks like:

void processCmd(uint8_t *buffer, FILE *USBSerialStream) {
    char *saveptr;
    
    /* Extract the first part of the string, separated by blankspace. */
    /* the saveptr is saved and later directed to the command callback. */
    char *token = strtok_r(buffer, " ", &saveptr);
    
    fputs("\r\n", USBSerialStream); /* Send linebreak to serial. */
    
    int success = false;
    
    cmdItem *ptr = cmdList;
    for(int i=0; i < cmdListLength; i++, ptr++) {
        /* Step through all the items in the command list. */
        if (strcmp(token, ptr->name) == 0) {
            ptr->action(&saveptr);
            success = true;
            break; /* if there is success. */
        }
    }
    
    /* If we get this far, there was no hit amongst the commands. */
	/* Output a list of all commands and respective helptext. */
    if (success == false) {
        ptr = cmdList;
        fputs("Available commands:\r\n", USBSerialStream);
        for (int i=0; i < cmdListLength; i++, ptr++) {
            fputs("\t", USBSerialStream);
            fputs(ptr->name, USBSerialStream);
            fputs("\t", USBSerialStream);
            fputs(ptr->help, USBSerialStream);
            fputs("\r\n", USBSerialStream);
        }
    }
}

A nice perk here is again that we can defer argument processing to the callback functions. Let’s check the callback for the timeset command to see an example of this. The serial command sent would look like timeset 13,13,13.

void CmdTimeset(char **saveptr) {
    fputs("timeset\r\n", &USBSerialStream);
    int hms[3];
    /* We're expecting 3 arguments separated by a comma. */
    for (int i = 0; i < 3; i++)
    {
        /* The saveptr is used to continue the strtok_r from the processCmd function. */
        char *token = strtok_r(NULL, ",", saveptr);
        if (token != NULL)
        {
            /* As long as we are getting valid data, we can save it. */
            int val = atoi(token);
            hms[i] = val;
        }
        else
        {
            /* Otherwise, we can tell the user that there's a command missing.
            fputs("Too few arguments.\r\n", &USBSerialStream);
            break;
        }
    }
    /* Print time confirmation and set time to RTC. */
}

This implementation will only look at the first three commands and just ignore the rest, which is perfectly fine in this use-case.

Doing things better?

Although the journey of improvement is a personal one, I’d like to share some short thoughts on how I think things could’ve turned out better.

Platform

The 8-bit AVR platform is easily accessible and was a pretty solid choice at the time I build the hardware a few years back. I didn’t think about it at the time, but the atmega32u4 does have JTAG and I didn’t route it out. Probably because I only used the regular ISP at the time, but that also meant no breakpoints. There’s been plenty of instances where I would have liked that, because my pointers were suddenly traversing out in memory it shouldn’t be in.

Today, an ARM-based MCU choice and a proper JTAG probe would’ve probably have been the better choice, at least with enough USB-related example code. LUFA worked great for this example.

IDE and toolchain

I’ll give Microchip Studio that it was very easy to get started, but it’s it’s aged and Microchip is unlikely to continue developing it in favor of their MPLAB X IDE.

In short, both feel very outdated and I feel that the way projects are structured and managed is not for me. I’d like to go back to an either Makefile-based build environment or cmake if I can wrap my head around it.

It’d probably be perfectly possible for me to do so, but there’s the inertia associated with getting into those tools either again or learning.

Code

Since I started out with a LUFA example, quite a lot of code is formatted in the manner of the maker whereas I’ve added parts that do not necessarily comply with those coding standards. This is in no way helped by the fact that I have Python as my main language at work and the PEP8-style associated with it. I should’ve looked up the LUFA code style and made sure to keep everything uniform, that’d reduce the eye strain a lot.

The state machine(s) really could use better organization, that’s something I’m in the process of reading up on in order to structure the flow in a better way. There could actually be a point in going towards UML or similar in order to design separately rather than directly in code.

Splitting the code out to re-useable files with a more complete feature set might save work for the future, or for others. A stepping stone there, is to reduce the use of globals and use the scope-limiting effects of static keywords and what not.

There’s other stuff as well, such as inefficient implementations and actually multiple implementations for the same thing, but it’s not production code at least. I’m happy to have gotten the code done at all so many years after the hardware. 😁

The end

That’s it, at least the parts I think might be remotely interesting. There’s of course more stuff in the source code including some discrepancies in code style and other things, but it’s good enough to get going with embedded C again. Perfect is the enemy of good, I’ve heard said.

Oh, and the thing got 3d-printed legs so that it can stand on its own. Cant’ leave the hardware out completely.

3d-printed legs

  1. Although, in a way, it could be seen as event inputs to a single state machine since they are sharing the same list of states. 

  2. Well, if you look at the implementation that’s only to some extent true. I got a bit lazy and only required a few of them to be in the correct order.