Rocket Ignition System Firmware

Previously, we etched a board and connected switches and buttons to it. Now we’re going to look at the software that reads the buttons and switches in order to determine the correct moment to apply power to the rocket igniter.

Although the rocket system can be used without a microcontroller, you won’t get the button sequence enforcement and buzzer features. You can write your own program and use a microcontroller of your choice, or you can use the firmware at the bottom of this page to program an ATTiny45 with my code.

Source Code

I chose a slightly sophisticated approach to the ignition system code, which is documented throughout to help you follow it. Basically, the ignition circuit board is always in a specific mode. Changes in button/switch state and occasionally timed events cause the mode to change. When the mode changes, the state of the LEDs, buzzer, and relay are set.

#include <CL_TargetHeader.h>

#include <CL_Timers2.h>
#include <CL_Serial.h>
#include <CL_Watchdog.h>

#include "main.h"


/****** SETUP, MAIN LOOP, and MODE ******/

// Buzzer uses the same pin as reset
#define BUZZER TRUE

Mode _mode = MODE_START;

void main(void)
{
    CLI(); //disable all interrupts

    // As early as possible, set the igniter output to low.
    SetupPinToOutputLow(&IGNITER_PORT, &IGNITER_DDR, IGNITER_PIN);

    // To save power, turn off all of the modules that we aren’t using in this project.
    PRR = BIT(PRTIM1|PRUSI|PRADC);
    ACSR = BIT(ACD); // Disable the analog comparator

    // Set up the arm and launch buttons as inputs with pullup resistors
    SetupPinToInputWithPullup(&ARM_PORT, &ARM_DDR, ARM_PIN);
    SetupPinToInputWithPullup(&LAUNCH_PORT, &LAUNCH_DDR, LAUNCH_PIN);

    // The keyswitch has a pulldown resistor on the board, because the keyswitch outputs
    // 5V when unlocked.
    SetupPinToInputNoPullup(&UNLOCK_PORT, &UNLOCK_DDR, UNLOCK_PIN);

#if BUZZER
    // If you have a buzzer replacing the reset pin, then turn the buzzer off at startup.
    SetupPinToOutputLow(&BUZZER_PORT, &BUZZER_DDR, BUZZER_PIN);
#endif

#if SERIAL
    CL_SerialSetup();
    CL_SerialTransmitCRLF();
    CL_SerialTransmitFlashStringCRLF("Rocket Launcher v2");
#else
    // The ready LED indicator can be reused as a serial port for debugging.
    SetupPinToOutputLow(&READY_PORT, &READY_DDR, READY_PIN);
#endif

    // If anything goes wrong with the software, and it fails to pass through
    // the main loop at least once a second, then the watchdog hardware will
    // reset the chip.
    CL_WatchdogEnableOneSecond();

    // This project needs three timers, which are emulated in software based
    // on a single hardware timer.
    // By default, they are set up with one second between callback routines,
    // but the actual timing will be modified as needed.
    CL_TimersSetup();
    // This blinks the ready LED
    CL_TimerSetup(TIMER_READY, TIMERS_ONE_SECOND, TRUE, &ReadyAlarm);
    // This makes the buzzer go on and off (think '10', '9', '8', '7', etc, 'liftoff!')
    CL_TimerSetup(TIMER_BUZZER, TIMERS_ONE_SECOND, TRUE, &BuzzerAlarm);
    // This switches us from one mode (such as waiting for countdown to finish)
    // to another (ignite for one second) to another (stop ignition and wait for button release)
    CL_TimerSetup(TIMER_AUTOINCREMENT_MODE, TIMERS_ONE_SECOND, FALSE, &AutomaticallyIncrementModeAlarm);

    SEI(); //re-enable interrupts

    SetMode(MODE_START);

    while(true)
    {
        // The SetMode method changes modes (and performs all pin and timing changes needed).
        // These are checks that occur while a mode is active.
        switch(_mode)
        {
            default:
            //case MODE_START:
                // Should never happen
                // However, just in case, force a clean start.
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("*** ERROR: Starting again");
#endif
                SetMode(MODE_START);
                break;

            case MODE_WAITING_FOR_ALL_BUTTONS_TO_BE_RELEASED:
                // At power up or after a launch, the launcher must be returned to the locked
                // position and the buttons must be released before being allowed
                // to launch again.
                // This avoids the situation where the user has placed the launcher on the
                // ground and the buttons are mashed in while the user changes the igniter.
                if (   IsUnlockButtonPressed() == false
                    && IsArmedButtonPressed() == false
                    && IsLaunchButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher is locked and buttons are released.");
#endif
                    SetMode(MODE_WAITING_FOR_UNLOCK);
                }
                break;

            case MODE_WAITING_FOR_UNLOCK:
                if (   IsUnlockButtonPressed()
                    && IsArmedButtonPressed() == false
                    && IsLaunchButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher is unlocked and buttons are released.");
#endif
                    // The user has unlocked the launcher while the buttons are released.
                    // Give the user a few seconds to walk away from the launch pad
                    // before allowing a countdown to start.
                    SetMode(MODE_WAITING_FOR_ARMED);
                }
                break;

            case MODE_WAITING_FOR_ARMED:
                // The launcher has been unlocked long enough and we are ready for the user
                // to press and hold the arm button.
                if (IsUnlockButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher was locked again.");
#endif
                    SetMode(MODE_WAITING_FOR_UNLOCK);
                }
                else if (IsArmedButtonPressed() && IsLaunchButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher is unlocked, arm button is pressed, and launch button is released.");
#endif
                    SetMode(MODE_COUNTDOWN);
                }
                break;

            case MODE_COUNTDOWN:
                if (IsUnlockButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher was locked again. Countdown aborted.");
#endif
                    SetMode(MODE_WAITING_FOR_UNLOCK);
                }
                else if (IsArmedButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Arm button released during countdown. Countdown aborted.");
#endif
                    SetMode(MODE_WAITING_FOR_ARMED);
                }
                else if (IsLaunchButtonPressed())
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launch button pressed before countdown finished. Countdown aborted.");
#endif
                    SetMode(MODE_WAITING_FOR_ARMED);
                }

                // Otherwise, the mode will automatically advance after the
                // required amount of time.
                break;

            case MODE_WAITING_FOR_LAUNCH:
                // The countdown is finished and the user is now allowed to
                // press the launch button.
                if (IsUnlockButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher was locked again. Launch avoided.");
#endif
                    SetMode(MODE_WAITING_FOR_UNLOCK);
                }
                else if (IsArmedButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Arm button released. Launch avoided.");
#endif
                    SetMode(MODE_WAITING_FOR_ARMED);
                }
                else if (IsLaunchButtonPressed())
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher unlocked, arm pressed, countdown complete, and launch pressed.");
#endif
                    SetMode(MODE_LAUNCHING);
                }

                // Otherwise, the mode will automatically advance after the
                // required amount of time.
                break;

            case MODE_WAITING_FOR_LAUNCH_TOOK_TOO_LONG:
                // The launcher was in launch mode for too long. Start again.
                // By dropping back wait for unlock, the user needs to let go of
                // the armed and launch buttons to start again.
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("It took too long after countdown for the launch button to be pressed. Launch aborted.");
#endif

                SetMode(MODE_WAITING_FOR_UNLOCK);
                break;

            case MODE_LAUNCHING:
                if (IsUnlockButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Launcher locked during launch. Launch aborted.");
#endif
                    SetMode(MODE_START); // Abort! Don’t even power the igniter
                                         // for the minimum period.
                }

                // Otherwise, the mode will automatically advance after the required amount
                // of time.
                // Unlike other modes, notice the user can let go of the launch button
                // or armed button,
                // and the minimum ignition time will still occur.
                // This prevents button debounces from aborting a launch and possibilty
                // ruining an igniter.
                break;

            case MODE_WAITING_FOR_LAUNCH_RELEASE:
                // The launch has happened and the relay has been triggered for the
                // minimum period.
                if (  IsUnlockButtonPressed() == false
                    || IsArmedButtonPressed() == false
                    || IsLaunchButtonPressed() == false)
                {
#if SERIAL
                    CL_SerialTransmitFlashStringCRLF("Locked or buttons released after launch.");
#endif
                    // Success. Start over.
                    SetMode(MODE_START);
                }
                // Keep powering igniter as long as the user holds down the buttons... until...
                // the mode automatically advances after the maximum amount of time.
                // This prevents heavy-handed operators from killing the board or relay
                // in the unlikely event that the igniter leads are touching (short circuit).
                break;
        }

        // Process any timer alarms by calling those methods
        CL_TimersTask();

        // Let the watchdog know that the software is looping normally
        CL_WatchdogTask();

        // This will only sleep for a maximum of 10 ms because the timer interrupt will happen.
        // Let’s save a tiny bit of power.
        SetBit ( SMCR, SE );	// Allow sleep.
        SLEEP();
        ClrBit ( SMCR, SE );	// Disable sleep.
    }
}

void SetMode(Mode mode)
{
    do
    {
        // Most modes do not have a ready LED blink or buzzer or auto-advance mode.
        // So, shut those off by default
        SetReadyTimer(0, false);
        SetBuzzerTimer(0, false);
        SetAutomaticallyIncrementModeTimer(0);

        // If the desired mode is out of range
        if ( mode < MODE_START || mode >= MODE_TABLE_COUNT)
        {
            // Restart
            mode = MODE_START;
        }

        // If we are not launching, set the igniter pin low by default
        if ( mode != MODE_LAUNCHING && mode != MODE_WAITING_FOR_LAUNCH_RELEASE)
        {
            SetPinLow(&IGNITER_PORT, IGNITER_PIN);
        }

        // Remember the current mode
        _mode = mode;

        // Upon switching to this mode, change any timers
        switch(mode)
        {
            case MODE_START:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Start");
#endif
                // A mode change forces us through the loop in this method again,
                // so that the mode is properly set up.
                mode = MODE_WAITING_FOR_ALL_BUTTONS_TO_BE_RELEASED;
                break;

            case MODE_WAITING_FOR_ALL_BUTTONS_TO_BE_RELEASED:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Waiting for lock and all buttons to be released");
#endif
                SetReadyTimerCustom(TIMERS_ONE_TENTH_SECOND, (TIMERS_ONE_SECOND/2)-TIMERS_ONE_TENTH_SECOND, true);
                break;

            case MODE_WAITING_FOR_UNLOCK:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Waiting for unlock");
#endif
                // Quick ON blink to let people know the microcontroller is running,
                // but not ready.
                SetReadyTimerCustom(TIMERS_ONE_TENTH_SECOND, (TIMERS_ONE_SECOND/2)-TIMERS_ONE_TENTH_SECOND, true);
                break;

            case MODE_WAITING_FOR_ARMED:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Waiting for arm");
#endif
                // Quick OFF blink to let people know the microcontroller is running,
                // and ready.
                SetReadyTimerCustom(TIMERS_ONE_SECOND-TIMERS_ONE_TENTH_SECOND, TIMERS_ONE_TENTH_SECOND, true);
                break;

            case MODE_COUNTDOWN:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Countdown");
#endif
                SetBuzzerTimer(TIMERS_ONE_SECOND/2, true); // Buzz every second (on for half, off for half)
                SetReadyTimer(TIMERS_ONE_SECOND/10, true); // Fast blink

                // At least five seconds in case someone is counting fast
                SetAutomaticallyIncrementModeTimer(TIMERS_ONE_SECOND * 5);
                break;

            case MODE_WAITING_FOR_LAUNCH:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Waiting for launch");
#endif
                SetBuzzerTimer(0, true); // Buzz constantly
                SetReadyTimer(0, true); // Light constantly

                // Thirty second limit after countdown
                SetAutomaticallyIncrementModeTimer(TIMERS_ONE_SECOND * 30);
                break;

            case MODE_WAITING_FOR_LAUNCH_TOOK_TOO_LONG:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Took too long to press launch");
#endif
                // Use default setup
                break;

            case MODE_LAUNCHING:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Launch!");
#endif
                SetReadyTimer(0, true); // Light constantly
                SetBuzzerTimer(0, false); // Don’t use power on buzzer

                // Turn on igniter for 1/4 second
                SetAutomaticallyIncrementModeTimer(TIMERS_ONE_SECOND/4);

                SetPinHigh(&IGNITER_PORT, IGNITER_PIN);
                break;

            case MODE_WAITING_FOR_LAUNCH_RELEASE:
#if SERIAL
                CL_SerialTransmitFlashStringCRLF("Mode: Waiting for launch release");
#endif
                SetReadyTimer(0, true); // Light constantly

                // Ignite for up to 1 second total (1/4 already + 3/4 more)
                SetAutomaticallyIncrementModeTimer(TIMERS_ONE_SECOND/4*3);
                break;
        }

        // A mode change forces us through the loop in this method again,
        // so that the mode is properly set up.
    } while(_mode != mode);

    // Mode has been set up and is stable
}

void SetAutomaticallyIncrementModeTimer(unsigned short amount)
{
    if (amount > 0)
    {
        CL_TimerStart(TIMER_AUTOINCREMENT_MODE, amount);
    }
    else
    {
        CL_TimerStop(TIMER_AUTOINCREMENT_MODE);
    }
}

// Called by alarm in TimerTasks();
void AutomaticallyIncrementModeAlarm(void)
{
    SetMode(_mode + 1);
}


/****** BUTTONS AND SWITCHES ******/

bool IsUnlockButtonPressed(void)
{
    return PinToBool(UNLOCK_PORTIN, UNLOCK_PIN);
}

bool IsArmedButtonPressed(void)
{
    // The armed button sends a ground signal when pressed.
    // Flip that to a true.
    return PinToBool(ARM_PORTIN, ARM_PIN) == false;
}

bool IsLaunchButtonPressed(void)
{
    // The launch button sends a ground signal when pressed.
    // Flip that to a true.
    return PinToBool(LAUNCH_PORTIN, LAUNCH_PIN) == false;
}


/****** READY LED ******/

bool _readyLEDTurnedOn = false;
unsigned short _onTime = 0;
unsigned short _offTime = 0;

void SetReadyTimer(unsigned short amount, bool state)
{
    SetReadyTimerCustom(amount, amount, state);
}

void SetReadyTimerCustom(unsigned short onTime, unsigned short offTime, bool state)
{
    _onTime = onTime;
    _offTime = offTime;

    SetReady(state);
    SetReadyTimerBasedOnState();
}

void SetReadyTimerBasedOnState(void)
{
    int amount;

    if (_readyLEDTurnedOn)
    {
        amount = _onTime;
    }
    else
    {
        amount = _offTime;
    }

    if (amount > 0)
    {
        CL_TimerStart(TIMER_READY, amount);
    }
    else
    {
        CL_TimerStop(TIMER_READY);
    }
}

void SetReady(bool state)
{
    _readyLEDTurnedOn = state;

    if (state)
    {
#if SERIAL
        //CL_SerialTransmitFlashStringCRLF("*LED ON*");
#else
        SetPinHigh(&READY_PORT, READY_PIN);
#endif
    }
    else
    {
#if SERIAL
        //CL_SerialTransmitFlashStringCRLF("*LED OFF*");
#else
        SetPinLow(&READY_PORT, READY_PIN);
#endif
    }
}

// Called by alarm in TimerTasks();
void ReadyAlarm(void)
{
    SetReady(!_readyLEDTurnedOn);

    if (_onTime != _offTime)
    {
        SetReadyTimerBasedOnState();
    }
}


/****** BUZZER ******/

bool _buzzerTurnedOn = false;

void SetBuzzerTimer(unsigned short amount, bool state)
{
    if (amount > 0)
    {
        CL_TimerStart(TIMER_BUZZER, amount);
    }
    else
    {
        CL_TimerStop(TIMER_BUZZER);
    }

    SetBuzzer(state);
}

void SetBuzzer(bool state)
{
#if SERIAL
    bool stateChanged = _buzzerTurnedOn != state;
#endif

    _buzzerTurnedOn = state;

    if (state)
    {
#if BUZZER
        SetPinHigh(&BUZZER_PORT, BUZZER_PIN);
#endif

#if SERIAL
        if ( stateChanged)
        {
            CL_SerialTransmitFlashStringCRLF("*BUZZER ON*");
        }
#endif
    }
    else
    {
#if BUZZER
        SetPinLow(&BUZZER_PORT, BUZZER_PIN);
#endif

#if SERIAL
        if ( stateChanged)
        {
            CL_SerialTransmitFlashStringCRLF("*BUZZER OFF*");
        }
#endif
    }
}

// Called by alarm in TimerTasks();
void BuzzerAlarm(void)
{
    SetBuzzer(!_buzzerTurnedOn);
}

Programming the Chip

The microcontroller is an 8-pin Atmel AVR ATtiny45. To gain use of 6 pins instead of only 5, the reset pin is disabled to provide I/O on PB5. That requires high-voltage programming.

After inserting the chip and setting up the STK500 tool for high-voltage programming, configure and program the chip fuses as follows:

Microcontroller fuses for rocket launcher

Microcontroller fuses for rocket launcher

This will set the microcontroller to 1 MHz, with the watchdog always enabled, the reset pin disabled, and low-voltage protection at a fairly low 2.7V (to prevent brown-outs from causing resets during launch). The chip is now ready to have the firmware installed: Rocket_Launcher_Two.hex.

Finally, let's see a video of the model rocket ignition system being run through its paces.