Autodidact Ambitions 6 – Doubling Down on the Deep Dive

by | May 28, 2024 | LifeSkills, Pastimes, Technology | 41 comments

When the dive into the microcontroller code for my clock turned into a whopper of an article, I had to split it down the middle. I’m going to repeat the complete source code here so you can look at whatever parts might be referenced by not explictly dissected here.

#include <msp430.h>
#include <intrinsics.h>

#define SER_OUT BIT0
#define SR_CLK  BIT1
#define R_CLK   BIT2

volatile int outTime[4] = {0,0,1,0};
volatile int seconds = 0;

void update7(void){
    unsigned int digit;
    unsigned int i;
    /*
     * Control Bits:
     * output order
     * 1 : on or off
     * 2-5 Digit to show
     * 6-12 segments of display
     */
    for(digit=0; digit<4; digit++){
        //right to left
        //colon = seconds & BIT0 - SER_OUt is BIT0  Set P1OUT bit0 to BIT0 of seconds
        if(seconds & SER_OUT){
            P1OUT |= SER_OUT;
        }else{
            P1OUT &= ~SER_OUT;
        }
        P1OUT |= SR_CLK;
        P1OUT &= ~SR_CLK;

        for(i=0; i<4; i++){
            if (i==digit){
                //P1OUT &= ~SER_OUT;
                P1OUT |= SER_OUT;
            }else{
                //P1OUT |= SER_OUT;
                P1OUT &= ~SER_OUT;
            }
            P1OUT |= SR_CLK;
            P1OUT &= ~SR_CLK;
        }

        int curBit = 0x01;

        unsigned int digMask[10] = { 0x77, 0x11, 0x6b, 0x3b, 0x1d, 0x3e, 0x7e, 0x13, 0x7f, 0x3f};
        for(i=0; i<7; i++){
            //bits
            if ((digMask[outTime[digit]] & curBit) == 0){
                P1OUT &= ~SER_OUT;
            }else{
                P1OUT |= SER_OUT;
            }
            curBit *= 2;
            P1OUT |= SR_CLK;
            P1OUT &= ~SR_CLK;
        }

        //cycle serial output to put a 0 in the unused bit of the shift register.
        P1OUT &= ~SER_OUT;
        //P1OUT ^= SR_CLK;
        P1OUT |= SR_CLK;
        P1OUT &= ~SR_CLK;
        //don't hit the R_CLK until all twelve bits are pushed
        P1OUT |= R_CLK;
        P1OUT &= ~R_CLK;
    }
}

void tick(void){
    if(seconds > 59){
        seconds=0;
        outTime[0]++;
    }
    if(outTime[0]>=10){
        outTime[0]=0;
        outTime[1]++;
    }
    if (outTime[1] >= 6){
        outTime[2]++;
        outTime[1] = 0;
    }
    if (outTime[2] >= 10){
        outTime[3]++;
        outTime[2]=0;
    }
    if((outTime[3] * 10) + outTime[2] > 12){
        outTime[2]=1;
        outTime[3]=0;
    }
}

int debounce(int pinno){
    //
    int debo=0x00;
    int d=0;
    for(d=100;d>0;d--){
        debo += P1IN & pinno;
    }
    return (debo);
}

/**
 * main.c
 */
int main(void)
{
    WDTCTL = WDTPW | WDTHOLD;   // stop watchdog timer
    BCSCTL3 |= XCAP_3; //set clock caps to 12.5pF
    TACCTL0=CCIE; //Enable timer interrupts.
    TACCR0=4096; //Counter target for timer to get 1s/interrupt
    /*
     * MC_1 - Count up to TACCR0
     * ID_3 - Divide raw timer by 8
     * TASSEL_1 - Use external clock (ACLK)
     * TACLR - clear interrupt bit so no current active interrupt
     * */
    TACTL = MC_1|ID_3|TASSEL_1|TACLR;
    //Clock starts
    //Setup of clock complete

    //Input/Output configuration
    P1DIR = 0x07;    //Set P1.0-P1.2 & P1.6 to output
    P1REN = 0x38;    //Activate pull up/down resistors on P1.3-P1.5
    P1OUT = 0x00;    //set all to 0
    P1IE |= 0x38;    //Enable interrupts for P1.3 & P1.4
    P1IES = 0x00;    //Trigger input interrupts on rising edge signal
    __enable_interrupt();

    while(1){
       update7();
    }
}

#pragma vector=TIMER0_A0_VECTOR;
__interrupt void TA0_ISR (void){
    seconds++;
    tick();
}
#pragma vector=PORT1_VECTOR;
//__interrupt void button_Press(void){
__interrupt void Port_1(void){
    if (P1IN & 0x20){
            if (debounce(0x08) >= 50){
                outTime[0]++;
            }
            if (debounce(0x10) >= 50){
                outTime[2]++;
            }
        tick();
    }
    P1IFG = 0x00;
}

The program proper starts at “int main(void)”. You’ve got all your global definitions and variables set up, all your functions defined, and when you start up the program, your logic runs from here. So, what is all this nonsense?

It breaks down into three chunks: setting up the clock; setting the input/output configuration, and the update loop.

Setting up the clock:

    WDTCTL = WDTPW | WDTHOLD;   // stop watchdog timer
    BCSCTL3 |= XCAP_3; //set clock caps to 12.5pF
    TACCTL0=CCIE; //Enable timer interrupts.
    TACCR0=4096; //Counter target for timer to get 1s/interrupt
    /*
     * MC_1 - Count up to TACCR0
     * ID_3 - Divide raw timer by 8
     * TASSEL_1 - Use external clock (ACLK)
     * TACLR - clear interrupt bit so no current active interrupt
     * */
    TACTL = MC_1|ID_3|TASSEL_1|TACLR;
    //Clock starts
    //Setup of clock complete

All of these lines are setting hardware registers in the microcontroller to configure the behavior of the timers. When you start a new empty project with a main.c in Code Composer Studio, the template includes two things – the line ‘#include <msp430.h>” and the line “WDTCTL = WDTPW | WDTHOLD”. The MSP430 series of chips includes a timer that will automatically shut it down if it’s running for too long. This watchdog timer is on by default, and most people do not want it to be, so the project template includes the code to disable it. The line after that where we are OR-ing BCSCTL3 with XCAP_3 has to do with using an external crystal oscillator instead of the chip’s internal clock source. If you want real time values, as we do with our clock, an external crystal is more accurate and reliable, however, there are a few things we need to do from a hardware persepctive besides wiring in the crystal. It turns out that the signal from the crystal is impacted by the amount of capacitance on the wire connecting it to the chip. The folks at Texas Instruments were well aware of that and included what I think is a rather neat feature into the MSP430-series: programable capacitors on the chip connected to the clock pins. This line is setting those capacitors to their maximum value of 12.5 picofarads, which is the amount needed to make our common 32 Kilohertz crystal read at its nominal frequency. If not for the onboard capacitors, I’d be futzing around with even more components, and fretting all the way.

“TACCTL0=CCIE” says that Timer A (the one connected to our external crystal) will throw an interrupt every time it finishes counting. Interrupts cause code execution to stop and a different function to run, after which the program resumes wherever it left off. “TACCR0=4096” sets what value we want to use for our counting. The next line warrents a whole block of comments because I had to dig through the header files to find the right values. “TACTL = MC_1|ID_3|TASSEL_1|TACLR” means nothing just looking at it. TACTL is a hardware register that controls the behavior of Timer A. MC_1 says we do our counts by counting from zero up to the value in TACCR0 and then starting over from zero. The other two options here are to count down to zero, or to count up and then down again in an alternating pattern.

ID_3 says we want to divide the clock source by eight. This means we only increment our count by one every eight cycles of the crystal’s occilation. This eases the load on the logic end, since we don’t need the granularity of the higher frequency from the crystal. We just need a count that gives us one interrupt per second. Since the crystal occilates at a rate of 32,768 times per second, dividing that by eight gives us the 4096. Note, this results in a software bug because the chip will actually count to 4097, I should have set TACCR0 to 4095 to avoid a slow drift in clock value.

Oops.

TASSEL_1 says we want to actually use that external crystal for a timer source. And finally TACLR clears any active interrupt that might have been on Timer A. So now, we have it set up so that once every 4097/4096ths of a second the timer throws an interrupt at the chip. But we’re not ready to handle that interrupt yet. First we need to configure the input and output.

//Input/Output configuration
P1DIR = 0x07; //Set P1.0-P1.2 & P1.6 to output
P1REN = 0x38; //Activate pull up/down resistors on P1.3-P1.5
P1OUT = 0x00; //set all to 0
P1IE |= 0x38; //Enable interrupts for P1.3 & P1.4
P1IES = 0x00; //Trigger input interrupts on rising edge signal
__enable_interrupt();

So, what is all this? Again, we’re dealing with hardware registers which control the function of the chip. In this case, configuring the general I/O pins. These are grouped into sets of eight referred to as ‘ports’ by Texas Instruments, but we’re not going to need more than six of them. Note – the comments here are actually out of date.

P1DIR sets what direction the first eight I/O pins are to be used in. By default it’s set to all 0s for all input. So we change the first three bits to a 1 so that we can have pins for SER_OUT, SR_CLK, and R_CLK. The number there is a hex value for those binary 1s. The reference fo pin P1.6 was from when I was debugging. The code for it is gone, but the comment remains.

P1REN is more interesting. If the value on a data line is not explicitly controlled, it does something known as ‘floating’, that is, the voltage will meander up and down the middle and sometimes enter into the logical high or logical low regions. This is the result of electrical wires being in close proximity. Signals on other lines will induce current on the uncontrolled lines resulting in this uncontrolled ‘floating’ voltage. We don’t want data lines to float, that noise can result in false inputs being registered. So every I/O pin on an MSP430 is built with both a pull-up and a pull-down resister, and we can programmatically set which is in use. We will either pull the value on the line to a logical high, or pull it down to ground. Why no standard? Because Texas Instruments doesn’t know what form your input signals will take. I am using momentary switches which produce a strong high signal when pressed. On the other hand, integrated circuits are better at sinking to ground than pulling to high. So if we were listening for a signal from another IC, we may want to set the line high and get a clean zero signal when the other chip pulls the line low, rather than letting it fight against us pulling the line low.

So we use the hex equivalent of the bits for our three input pins to activate the resisters on those pins. The next line we set all bits of P1OUT low. This has a dual function. First, it establishes a baseline state for our actual outputs. Second, it tells the chip that we want the input resisters to pull low on our input pins. Since the output register isn’t going to be using those bits when we’re listening for input, the same memory space serves an additional function.

P1IE simply enables interrupts for our input pins. The comment here is also outdated. I had to enable interrupts for P1.5 even though I had no intention on acting on changes to that pin. Why? Because the compiler decides that if I’m not listening for an interrupt on a pin, there won’t be any input there, so anything dependent upon that pin won’t get executed. The switch which enables human input for the clock is connected to pin P1.5, and with the compiler optimizing it out, the interrupts triggered by the buttons did nothing.

P1IES tells the chip whether we want our interrupts to trigger on a ‘rising edge’ or a ‘falling edge’. What this means is whether we want to act when the signal switches from low to high (rising edge) or from high to low (falling edge). For example, the clock signals for our shift registers trigger on a rising edge. Since we’re listening for button presses and that unreliable human might hold the button for irregular amounts of time, the rising edge is probably our best bet. After all, it gives immediate feedback that the button did something when the number changes. So we set P1IES to all zeros. We can choose what to key off of by pin if we want, just like all the other settings.

Lastly, “__enable_interrupt()” says we’re actually going to do something when we get one of these interrupts that I keep talking about. Otherwise, they get ignored.

To round out our main function we have an infinite loop.

    while(1){
       update7();
    }

Infinite loops are normally bad practice, but we need to continually update our display, otherwise, it would only show a single, unchanging digit.

So, it’s time to do something about all those interrupts. Our first one catches the timer whenever it counts down each second.

#pragma vector=TIMER0_A0_VECTOR;
__interrupt void TA0_ISR (void){
    seconds++;
    tick();
}

The vector names are defined within the msp430.h header for each type of interrupt that can be thrown. The name of our interrupt function, that’s up to us. All we need to do here is increment the seconds and tell the program to check if anything rolls over by calling tick(). Note, depending on the display font it might not be clear that those are a pair of parentheses. When calling a function, we have to include the parentheses, which would enclose any arguments we’re sending it. Since tick() doesn’t need any arguments, we don’t give it any.

Our other interrupt has to deal with messy humans.

#pragma vector=PORT1_VECTOR;
//__interrupt void button_Press(void){
__interrupt void Port_1(void){
    if (P1IN & 0x20){
            if (debounce(0x08) >= 50){
                outTime[0]++;
            }
            if (debounce(0x10) >= 50){
                outTime[2]++;
            }
        tick();
    }
    P1IFG = 0x00;
}

The first thing this does is check to see if the switch which says whether we are intentionally setting the time has been turned on. Momentary buttons are easy to press accidentally, so if the switch is off, we ignore them as unintentional If it is on, we do something.

Here, we are calling the debounce() function I skipped over earlier.

int debounce(int pinno){
    int debo=0x00;
    int d=0;
    for(d=100;d>0;d--){
        debo += P1IN & pinno;
    }
    return (debo);
}

Why are we doing all of this? Well, it comes back to the fact that human button presses and the buttons themselves do not reliably send clean signals. The signals are ‘bouncy’ and this can result in multiple ‘presses’ for a single intended button press. There are a number of ways to clean up the signal, some are done in hardware, some in software. This brute force loop and resample technique is one that is not recommended as best “make everything clean” practices. What was recommended was a timer-based polling to see if the button had been held for more than a nominal length of time, indicating a true signal. But that also takes up a lot of code space and requires configuring an additional timer. The MSP430 might have three timers, but the MSP430F2003 only has 1kb of storage for code. So, the loop and poll method was chosen to simplify the code.

The way I have it, for a few hundred to a thousand CPU cycles, no additional interrupts can trigger off of the buttons while it checks to see if you’re holding one of the buttons for at least half of its cycle. If it detects enough of a press, it increments either the minutes or the hours. Then it calls tick() to sort out rollovers and lastly, clears the interrupt register for inputs, so you need to push the button again to get another action. Because resources are so tight, the code is a trade-off. We’re not updating the display for a thousand CPU cycles. Thankfully, the CPU cycles sixteen thousand times a second, so we’ll be back to updating the display before the eye notices the blip. An ideal debounce would sample over a longer span of real time, but if we interrupt that display update too long, it manifests as a flicker. So we had to go with long enough to catch most incidental bounces, but quick enough that it’s still faster than the eye.

Knowing that my clock as written runs a little slow, I might fix the TACC0 value, but I figured I’d leave that in the article, since the quirk that it won’t trigger the actual interrupt after the one where it hits zero is something easily missed. Especially with such small increments of time as 1/4096th of a second. It would take a while to notice the drift from this bug, especially since the display only shows hours and minutes and the drift is 21 seconds per day.

How did I ramble for so many articles about a simple digital clock?

About The Author

UnCivilServant

UnCivilServant

A premature curmudgeon and IT drone at a government agency with a well known dislike of many things popular among the Commentariat. Also fails at shilling Books

41 Comments

  1. UnCivilServant

    I would have a Part 10, but I’m still trying to figure out a line of inquiry to figure out what’s going on.

  2. Richard

    ZZZZZZZZZ

    (just kidding)

    • UnCivilServant

      Muahahaha! I succeeded in putting people to sleep.

      • Gender Traitor

        I’m not asleep – I’ve been triggered by the extended passages in a foreign language! ๐Ÿ˜ต

      • UnCivilServant

        C isn’t foreign, it was Made in America!

      • Gustave Lytton

        Sequel: it’s an alarm clock?

      • DrOtto

        I was just resting my eyes.

      • UnCivilServant

        So the snore was just for emphesis?

    • The Last American Hero

      Every time I see the pic on the main page, I’m reminded of that kid that was dragged out of school for making a “bomb” that he correctly insisted was a home-made clock.

      • kinnath

        College student arrested at airport (as I recall) for wearing a circuit of similar design on a sweatshirt.

      • Bobarian LMD

        It was trolling, a clock clearly made to resemble a bomb. Complete with ready made accusations of “the racisms!”

    • Gustave Lytton

      Same here! He was and is a legend here for leading the Blazers to their first and so far only championship.

      Local pizza joint has a pizza (veggie, of course) named after him and it’s delicious.

  3. The Late P Brooks

    Once upon a time, youtube served me up a very nice simplified animation of a mechanical clockwork. Something I could understand. I can’t find it.

    • Sensei

      Do you want a watch or spring driven clock or something with a pendulum?

  4. Derpetologist

    Festus complained of wish fulfillment in my last installment of the story, and I think he has a fair point. In my defense, it’s my first foray into long fiction. Also, in Full Metal Jacket, there is talk of The Great Homecoming Fuck Fantasy, so I guess I was going for that vibe.

    https://www.youtube.com/watch?v=PmILOL55xP0

    We should all strive to understand things as thoroughly as UnCiv while remaining gainfully employed.

    • UnCivilServant

      The only thing I am learning is how much I don’t know.

    • Gustave Lytton

      But we also know what happens in First Blood. No quite a happy fairy tale life after all.

      • Gustave Lytton

        Or course, could also do the JR thing and he wakes up back in the interrogation cell pumped full of KGB mind control drugs.

    • Mojeaux

      Festus complained of wish fulfillment in my last installment of the story

      I wouldn’t take that to heart. See: James Bond.

  5. The Late P Brooks

    Do you want a watch or spring driven clock or something with a pendulum?

    I’m fascinated by the mechanism. It’s just a tiny transmission.

  6. Gender Traitor

    Tasked by my supposed-to-be-on-vacation boss with finding some sensitive HR docs in the newly-retired CEO’s desk or credenza drawers, I’m finding that the ex-CEO was even worse at filing than I am. ๐Ÿ˜•

    • The Artist Formerly Known as Lackadaisical

      Sounds like the premise to a spy movie or industrial espionage.

      • Gender Traitor

        Well, I found the codes and instructions for notifying the local TV stations if we’re closing or delaying opening due to bad weather. They were in a folder labeled “2014 Budget.” ๐Ÿ™„

      • UnCivilServant

        Any “Beware of Panther” signs?

      • Sensei

        UCS – if that’s the case I believe the credenza and desk needed to have been locked.

      • Gender Traitor

        Should you behold a panther crouch,
        Prepare to say Ouch.
        Better yet, if called by a panther,
        Don’t anther.

        – Ogden Nash

      • Ted S.

        Tinder is going to match her with Werner Brandes.

  7. UnCivilServant

    Today I learned that there are PCIe cards that can hold several M.2 hard drives. I could expand my NAS storage… but M.2 drives are costly per byte because of their higher speed and lower power needs… I don’t need the high speed on the NAS.

  8. Unreconstructed

    There’s always tradeoffs, right? M.2 drives are tiny and sip power, so if those are the qualities you value, it makes perfect sense. But the unit cost is definitely a lot higher. Are you using a commercial NAS, or a homebrew?

  9. The Late P Brooks

    Turn that clown frown upside down

    President Joe Biden’s campaign held a press conference outside the Manhattan courtroom where Donald Trump is on trial in his hush money case, with actor Robert De Niro and two officers who defended the Capitol from the Jan. 6 mob in 2021 warning about the dangers of re-electing the former president.

    “The Twin Towers fell just over here, just over there. This part of the city was like a ghost town, but we vowed we would not allow terrorists to change our way of life. … I love this city. I don’t want to destroy it. Donald Trump wants to destroy not only the city, but the country, and eventually he can destroy the world,” De Niro said.

    “I don’t mean to scare you. No, no, wait โ€” maybe I do mean to scare you,” De Niro continued. “If Trump returns to the White House, you can kiss these freedoms goodbye that we all take for granted. And elections โ€” forget about it. That’s over, that’s done. If he gets in, I can tell you right now, he will never leave.”

    Serious people, doing serious things.

    • Sean

      “The best that Biden can do is roll out a washed-up actor,” Trump campaign senior adviser Jason Miller said.

      Lulz

  10. The Late P Brooks

    “You’re not going to intimidate,” De Niro replied. “That’s what Trump does. … We are going to fight back. We’re trying to be gentlemen in this world, the Democrats. You are gangsters. You are gangsters!”

    “You’re washed up,” a protester yelled.

    “F— you,” De Niro shot back.

    Stick to the script, bozo.

  11. EvilSheldon

    โ€œI donโ€™t mean to scare you. No, no, wait โ€” maybe I do mean to scare you,โ€ De Niro continued. โ€œIf Trump returns to the White House, you can kiss these freedoms goodbye that we all take for granted. And elections โ€” forget about it. Thatโ€™s over, thatโ€™s done. If he gets in, I can tell you right now, he will never leave.โ€

    And the right wing is the paranoid one?