Most of the time it is good to handle the various things the processor needs to do with a combination of interrupts, tasks, and events. In this case, I'd use a periodic 1 ms interrupt for checking and debouncing the switch, a separate task to handle the USB stream from the host, and either events or another task to manage the SPI communication. USB requires some ongoing support even when you're not actually sending or receiving anything at the application layer. I'd make that a separate task or call the USB background processing routine regularly from the main event loop.
My usual architecture for such things contains the following constructs:
- 1 ms periodic interrupt. 1 ms is a "long" time for most microcontrollers, so this takes only a small fraction of the CPU. This is where the debouncing logic for mechanical switches is performed. A switch signal needs to be seen in a state for 50 consecutive interrupts to be officially declared to be in that state. The rest of the system only sees the flag bits representing the debounced state of each switch input.
This interrupt also increments one or more clock tick counters. Sometimes powers of 10 are useful, like 1 ms, 10 ms, 100 ms, and seconds. The interrupt routine increments each of these as appropriate, and the rest of the system just sees them change automatically.
- Interrupts for other things that must be handled with low latency, like A/D readings. The A/D interrupt routine grabs the new reading from the A/D, possibly applies some low pass filtering, maybe applies scaling and offset, and leaves the final results in global variables for the rest of the system to read whenever it wants to.
- A separate task to handle the reception of each communications stream. The information in serial communication streams is usually quite state-dependent. It therefore makes sense to process such streams using state machines. A task is basically a state machine where the PC is used as the state variable, except that its more maintainable, less obfuscated, and much more intuitive than a traditional state machine implemented as a computed GOTO on the state variable. Since the state for processing each communication stream is independent and asynchronous to other system state, it makes sense to give each stream its own task.
Each task runs as a inifinite loop, except that it calls TASK_YIELD whenever it has nothing immediate to do or after any significant chunk of work. For receiving a byte stream, it is more convenient to write the code as if it goes out and gets the next byte, even though in reality bytes come in when they come in. The GET_BYTE routine usually starts in a loop that calls TASK_YIELD, checks for a new byte being available, and jumps back to the start of the loop if not. When a byte is available, it then returns with the new byte. This allows GET_BYTE to be called by the main task code at many places that are each context-dependent. For example, at the strart of the main task loop, it calls GET_BYTE to get the opcode of the next packet. The code than dispatches to the routine for that opcode, which calls GET_BYTE to get the first data byte, etc. This way the PC is automatically used as the state variable for parsing the stream.
- One task, usually the original one so that nothing special needs to be done if there are no other tasks, runs the main event loop. This checks and handles all the little things that need to be handled occasionally. It calls TASK_YIELD at the top of the loop, then sequentially checks for events to handle. If a event is not pending, it jumps to the end of that section, which then proceeds to check the next event. If a pending event is found, it handles that event and jumps back to the top of the loop. If no pending events are found, then it hits the end of the loop, which simply jumps back to the start.
This mechanism effectively prioritizes events with them checked in high to low priority order. Each event should be something that is relatively simple to handle and doesn't require waiting on anything else. This keeps the main event loop calling TASK_YIELD regularly. Break up more complicated proceedures into separate individual immediately-executable steps if necessary, usually by using a sequence of 1-bit flags to indicate ready for the next phase of processing. For example, one event might start a new A/D reading. Instead of waiting for that to be done, the A/D interrupt routine sets a flag when a new reading is available. That is then a separate event handled by the main event routine.
Some events are time-based. Instead of setting flags for them in the clock interrupt routine, I usually have the main event routine check the global clock tick counters near the start of the loop to decide if any relevant time-based events need to be triggered. This has several advantages:
- It keeps the clock interrupt routine clean. It is better for maintenence to not have main event logic hidden in other modules, like the clock module. This also allows for more easily using a generic clock module with minimum customization.
- It keeps the logic local to the main event module. Someone doing maintenence later can see the whole logic in one place.
- It doesn't lose ticks in case some processing takes longer than one tick interval. I keep the last known clock value in the main event routine for each timed event. These values are compared to the current clocks to determine whether there is a new tick or not. If so, the last known clock value is only incremented by one and the tick handled. If for some reason the system gets behind, it will catch up automatically with several of these events in succession every time they are checked, which will eventually get the last known clocks back in sync with the live clocks. If you get consistantly behind, then the overall system was badly designed and something else needs to be fixed.
A large number of template modules and a few examples are available as part of my PIC development environment. This includes nice coorperative multi-tasking managers for PIC 18 and the 16 bit PICs, templates for clock modules, the main event loop, and lots more.