final project -- about me -- weekly assignments -- fab academy

What Makes the Clock Tick: Software

Given how much tech there is in the clock, it might not be surprising that there is also quite a bit of software involved - it's five microcontroller boards talking to each other.

In the original plans of building a placer, there would have been a separate "brain" board to control everything and look after getting everything synchronized. I built two of those boards, but didn't get to actually doing anything with them - they haven't even been powered yet. I started out with this part of the documentation while still planning on them, and planning on building an NC machine, though, so the structure of this page might still be a bit strange for what things turned out to become.

The Brain

The clock brain is... Not really a brain. It's one of the motor boards. So, it's driving a digit wheel like the others do (the ten hours wheel).

Additionally, it also counts time and tells the other digit wheels to move. The algorithm behind that is pretty simple: Timer 2 (It's not special, I just used timer 2) triggers an interrupt each millisecond. That (rather fast) timer is a remnant of the motor boards being planned for a CNC machine, and the state machine for the motors runs directly from it. Interrupts are counted, and after 60000 interrupts (so, after 1 minute) the variable counting minutes is incremented:

void stateAdvanceMinute(void)
{
  axisState.clockMinutes++;

At this point, an overflow of the minutes is checked - after all, the minutes part showing anything more than 59 would be embarrassing for a clock. If there is a rollover, the hours variable is advanced, too, using a similar function (and again checking for rollovers, but noch counting days):

  if(axisState.clockMinutes >= 60) // hour rollover
  {
    axisState.clockMinutes = 0;
    stateAdvanceHour();
  }

We do now have a valid state of the hours and minutes variables again, and at least one wheel needs to change position, so we send out the new target positions to the other boards via the CAN bus:

  stateSendNewWheelPositions();
}

The exception to that data being sent out via the bus is the (possibly) new position of the ten hours wheel, which is handled via a direct call to the routine that would normally handle the incoming CAN message.

The Muscle

Booting a Microcontroller

In the beginning, there was a Power On Reset. Then, there is getting the microcontroller from it's reset state to where we want to have it. This happens at the beginning of main(), and it all that main does (apart from then spinning in a good old endless loop).

The first part of setting up the microcontroller is, in my case, to get the clock up to speed. When booting, it will run from a slow backup oscillator (which is assumed to always work, and makes for a great fallback). The main clock source is usually multiplied by a PLL, which has to be configured in software. To do that, we first set up the multipliers of the different stages:

PLLFBD = 68; // set PLL divider to 70 (from 4 to 280 MHz)
CLKDIV = 0; // set N1 and N2 to 2 (8 to 4 and 280 to 140 MHz)

Then, we set the clock configuration we want to switch to, and trigger a switch:

__builtin_write_OSCCONH(0x03); // prepare clock switch to external oscillator with PLL
__builtin_write_OSCCONL(OSCCON | 0x01); // initiate clock switch

Now, it's waiting time:

while(OSCCONbits.COSC != 0b011); // wait for oscillator to stabilize
while(OSCCONbits.LOCK != 1); // wait for PLL lock

There is no error handling here, if the oscillator fails or the PLL fails to lock, the microcontroller will stay stuck and wait forever. I could theoretically catch that, of course, but on the other paw, a board with failed hardware will not be able to participate in any fun, anyway.

Next, a lot of I/O-configuration has to be set. For normal I/O-pins, you have to configure them as inputs or outputs, and shut off any ADC inputs you don't need - They are switched on by default (why?!?) and will make the pin read as low at all times. Pins used for all kinds of special purposes (like CAN bus, UART, ...) can mostly be chosen, by mapping the special function to a pin. That has to be done, of course, before using them.

After that, all the peripherals are initialised: PWM, the Quadrature Encoder Interface, the CAN bus and Timer 2. On one module (which doesn't drive a wheel), the UART is initialised, too. I haven't put any functionality in there yet, but it might be good to have a quick way of setting the clock there, or switch it to other functionalities like a countdown.

Communication: CAN Bus

This part is done and documented as part of the Embedded Networking & Communications assignment.

Driving The Motor: PWM

Most of the software to control the PWM output is described in fine detail in the Output Devices assignment. The PIC used on the motorboard differs only in that its PWM module uses the main clock as a clock source, not an additional clock that first has to be configured. For the motor board software, I packed the PWM parts up into a nice, simple, but really not generalised API, so it hides between two function calls - One to initialise the module, the other to set a duty cycle.

In the clock, the motors are simply switched on to a predefined PWM value and left on until the wheel reaches its target, then the motor is switched of. With the wheels having a bit of room, this leads to them sometimes not being exactly at their target position, but doing something about that would take more time than I have right now (put in a real position controller, and give the wheels less room to move on their own).

Knowing Where We Are: Quadrature Decoding

This part is done and documented as part of the Input Devices assignment.

State Affairs: Where Everything Comes Together

There is a simple state machine in the software that mostly controls the initialisation: When the module is switched on, it starts by driving the wheel (at a good pace, because the geared motors tend to stick after some time, and need a good shove to run smoothly again), and waits for an index pulse. When an index pulse comes, the encoder interface is set to a the calibration offset of the magnet in this particular wheel, the motor is switched to its normal speed (mostly to make the change visible for debugging) and the target is set to 0, so the first number all wheels will stop at is 0. The module then falls into its normal mode, and accepts commands for setting a number.

Visualising Our State: LED Output

The RGB-LED is quite a simple thing, and I didn't do anything fancy with it. The pins it's connected to are set to be outputs during initialisation, so what is left is packing LED usage into a simple function:

void setLED(unsigned char led, unsigned char state)
{
  switch(led)
  {
    case RED:
      LATAbits.LATA1 = state;
      break;
    case GREEN:
      LATBbits.LATB0 = state;
      break;
    case BLUE:
      LATBbits.LATB1 = state;
      break;
    case YELLOW:
      LATBbits.LATB0 = state;
      LATAbits.LATA1 = state;
      break;
    case WHITE:
      LATAbits.LATA1 = state;
      LATBbits.LATB0 = state;
      LATBbits.LATB1 = state;
  }
}

The definitions for the colours and states is added, too, of course, in the header file for the LED routine.

Inputs: Setting The Clock

At each motorboard driving a digit wheel, a microswitch is connected to one of the endstop inputs and mounted on top of the clock. While endstops are not needed in any way for the clock, a way to set it is quite an important thing, and having an "increment one" button for each digit is a quick way to do so.

The buttons are read in the timer interrupt each millisecond, and filtered with a simple filter routine to debounce them. When their filtered state changes to pressed, a CAN message is sent of the button being pressed - As with the setting of the wheel positions, this is again done directly in the ten hour wheel. A routine at that board reads those messages from the bus, and increments the time variables accordingly.

final project -- about me -- weekly assignments -- fab academy

Creative Commons License
This work by Christoph Nieß is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.