The Main Loop

2018

Asynchronous programming in an embedded environment

What does a CPU do when it has nothing to do?

The Arduino Way

void setup()
{
     pinMode(LED_BUILTIN, OUTPUT);
}
void loop()
{
    digitalWrite(LED_BUILTIN, HIGH);
    delay(1000);
    
    digitalWrite(LED_BUILTIN, LOW);
    delay(1000);
}

The Arduino Way

void delay(double __ms)
{
    double __tmp = ((F_CPU) / 4e3) * __ms;
    uint16_t __ticks;
    
    if (__tmp < 1.0)
    {
        __ticks = 1;
    }
    else if (__tmp > 65535)
    {
        __ticks = (uint16_t) (__ms * 10.0);
        while(__ticks)
        {
            _delay_loop_2(((F_CPU) / 4e3) / 10);
            __ticks --;
        }
        return;
    }
    else
    {
        __ticks = (uint16_t)__tmp;
    }
    
    _delay_loop_2(__ticks);
}
void _delay_loop_2(uint16_t c)
{
    __asm__ volatile (
        "1: sbiw %0,1" "\n\t"
        "brne 1b"
        : "=w" (c)
        : "0" (c)
    );
}
void _delay_loop_2(uint16_t c)
{
    // more or less
    while (c--);
}

The FreeRTOS Way

void vTaskDelay(uint32_t ticks)
{
    while (ticks > 0)
    {
        vTaskSuspendAll();
        prvAddCurrentTaskToDelayedList(ticks);
        vTaskResumeAll();
    }
}

The FreeRTOS Way

void xPortSysTickHandler(void)
{
    if (xTaskIncrementTick())
    {
        SCB->ICSR = 
            SCB_ICSR_PENDSVSET_Msk;
        __SEV();
    }
}
bool xTaskIncrementTick(void)
{
    if(xConstTickCount >=
       xNextTaskUnblockTime)
    {
        pxTCB = 
            listGET_HEAD(pxDelayedTaskList);
	prvAddTaskToReadyList(pxTCB);
	return true;
    }
}
void xPortPendSVHandler(void)
{
    asm volatile
    (
        "   stmdb r0!, {r4-r11, r14} \n"
        "   str r0, [r2]             \n"
        "bl vTaskSwitchContext       \n"
        "   ldr r1, [r3]             \n"
        "   ldr r0, [r1]             \n"
        "   ldmia r0!, {r4-r11, r14} \n"
    )
}
void vTaskSwitchContext(void)
{
    pxCurrentTCB = 
        taskSELECT_HIGHEST_PRIORITY_TASK();
}
    

The FreeRTOS Way

void prvIdleTask(void *)
{
    while (true)
    {
	ticks = prvGetExpectedIdleTime();

        vPortSuppressTicksAndSleep(ticks);
    }
}
void vPortSuppressTicksAndSleep(uint32_t ticks)
{
    do {
        __WFE();
    } while (!(NVIC->ISPR[0] | NVIC->ISPR[1]));
}

CPU vs I/O

The official 4.2BSD release came in August 1983

select(2) first  appeared in 4.2BSD

The UNIX Way

int connection = socket(...);

fd_set descriptors;

while (true)
{
    FD_ZERO(&descriptors);

    FD_SET(connection, &descriptors);

    select(&descriptors);

    if (FD_ISSET(connection, &descriptors))
        process_connection();
}

What if...

void some_task(void)
{
    int i = 0;

    while (true)
    {
        printf("%d\n", i++);
        return sleep_ms(100);
    }
}

???

State machine

void some_task(void)
{
    static enum state state = STATE_IDLE;
    static int i = 0;

    switch (state)
    {
        case STATE_IDLE:
            printf("%d\n", i++);
            state = STATE_WAITING;
            sleep_ms(100);
            break;

        case STATE_WAITING:           
            state = STATE_IDLE;
            break;
    }
}
void some_task(struct event *event)
{
    static enum state state = STATE_IDLE;
    static int i = 0;

    switch (state)
    {
        case STATE_IDLE:
            printf("%d\n", i++);
            state = STATE_WAITING;
            sleep_ms(100);
            break;

        case STATE_WAITING:
            if (event->id == EVENT_TIMEOUT)
                state = STATE_IDLE;
            break;
    }
}

Events

void sleep_ms(
        uint32_t timeout)
{
    // configure TIMER0
}




void TIMER0_Handler(void)
{
    struct event ev = {
        .id = EVENT_TIMEOUT,
        .data = NULL,
    };

    queue_put(&ev);
}
NRF_SDH_BLE_OBSERVER(
    m_ble_observer,
    0,
    ble_evt_handler,
    NULL);


void ble_evt_handler(
        ble_evt_t const * p_ble_evt,
        void * p_context)
{
    struct event ev = {
        .id = EVENT_BLE,
        .data = ...
    };

    queue_put(&ev);
}

Sources

bool queue_pop(struct queue *q, struct event *e);
void queue_push(struct queue *q, struct event *e);

struct queue queue;

while (true)
{
    struct event;

    if (!queue_pop(&queue, &event))
    {
        __WFE();
        continue;
    }

    some_task(&event);
    another_task(&event);
}

Queues

bool queue_pop(struct queue *q, struct event *e);
void queue_push_back(struct queue *q, struct event *e);
void queue_push_front(struct queue *q, struct event *e);

struct queue queue;

while (true)
{
    struct event;

    if (!queue_pop(&queue, &event))
    {
        __WFE();
        continue;
    }

    some_task(&event);
    another_task(&event);
}

Priorities

Blocking

void some_task(struct event *event)
{
    static enum state state = STATE_IDLE;
 
    switch (state)
    {
        case STATE_IDLE:
            state = STATE_WAITING;
            long_computation();
            break;

        // ...
    }
}

Blocking: worker thread

struct work
{
    void (*callback)(void *);
    void *data;
};

void worker(void)
{
    struct work work;

    while (true)
    {
        if (queue_pop(&queue, &work))
            work.callback(work.data);
    }
}
  • no task synchronisation

  • trivial unit testing

  • lower resource usage

  • well-defined behaviour

  • enforced task isolation

  • somewhat harder to read

  • state explosion

Pros

Cons

hierarchical state machines

pushdown automata

coroutines

The Main Loop

Thank you

Questions?

Main Loop vol 0, embedded @ Silvair

By Michał Lowas-Rzechonek

Main Loop vol 0, embedded @ Silvair

Asynchronous programming in an embedded environment

  • 835