You are correct with #2: two "threads" will be required.
Typically, your interrupt routine will save characters into a buffer. Your main program will continuously read the buffer, call detectString and implement your logic.
You can use hardware flow control if you're concerned about missing characters. One the buffer is full, you can make the sender wait until you're ready again.
Your FSM might be a switch statement inside your main loop.
EDIT: Some code:
/* UART0 Receive Interrupt Vector */
ISR(USART0_RX_vect)
{
static uint8_t tmp;
// calc next insert index & store character
tmp = (uart0_rx_insert_idx + 1) % UART0_RX_BUFFER_SIZE;
uart0_rx_buffer[uart0_rx_insert_idx] = UDR0;
// check for more room in queue
if (tmp != uart0_rx_extract_idx)
uart0_rx_insert_idx = tmp; // update insert index
}
enum fsm {
IDLE,
MAGIC,
};
int main(void) {
// setup
enum fsm state = IDLE;
uint8_t wordbuf[10];
uint8_t wordbuf_index = 0x00u;
setLed(0);
uartSetup();
//activate interrupts
sei();
// mainloop
while (1) {
switch (state) {
case IDLE:
if (characterInBuffer()) {
// if buffer is full; word still not found
// shift all characters by one
if (wordbuf_index == sizeof(wordbuf) - 1)
memcpy(&wordbuf[1], &wordbuf[0], sizeof(wordbuf));
// save current character
wordbuf[wordbuf_index] = readCharacterFromBuffer();
// increment index if not at end of buffer
if (wordbuf_index != sizeof(wordbuf) - 1)
wordbuf_index++;
if (detectString(wordbuf))
state = MAGIC;
}
if (getCharacter())
break;
case MAGIC:
setLed(1);
if (buttonPressed()) {
setLed(0);
state = IDLE;
}
break;
} /* switch */
} /* while */
return 0;
} /* main */
This would be for an Atmel AVR ATmega microcontroller. Many microcontrollers have static vector tables that store the addresses of interrupt service routines (ISRs for short) that are present in your code.
When a peripheral interrupt is activatde, such as the UART Receive interrupt in the example above, and global interrupts are enabled (sei() call), the microcontroller will stop executing the main code and start executing the ISR code. When the ISR exits, execution will resume in the main code.
There are microcontrollers that have a programmable vectored interrupt controller (VIC). With these, you can freely assign an ISR at runtime instead of compile time (and even switch at runtime!). The idea remains the same though:
- hardware notices interrupt condition,
- main program flow stops
- ISR is executed
- main program flow resumes
You can have mutex locks and spin locks in your code. You just have to be careful about dead locks and use proper types, i.e. atomic types.
There are some flavors of microcontrollers that have a hiararchical interrupt system: one type of interrupt can interrupt another interrupt. With these, you have to be especially mindful of using mutual exclusion and proper typing.
In general, ISRs are supposed to be very slim and should not have long pieces of code or loops in them. The reason is simple: While your ISR is being executed, the main program is not. Since the main program usually does something very useful, you wouldn't want it to be interrupt for a prolonged amount of time.
Having an operating system (OS) on microcontrollers is usually optional. You can certainly write one-off C code and be done with it. If you care about re-usability of your code or being able to use pre-existing code, you might want to look into ChibiOS, FreeRTOS, etc.
OSes will typically encapsulate the idea of interrupts (or even vectored interrupts) and present a common interface for your higher-level program. The workings "under the hood" are the ones described above though.
If you're looking to do many things in parallel and use threaded applications, you should definitely look into popular code. They usually have mutexes, spin locks, etc. pre-made and ready-to-use.