Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pbio/os: Prototype simpler and unified async OS. #298

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open

Conversation

laurensvalk
Copy link
Member

Overall, the goals are to reduce complexity, reduce code size, and make it more cross platform.

In this PR, the new approach still drives the original pbio event loop as well, so we can convert one process at a time without breaking things. The charger process is done here as a simple test to illustrate what it looks like. Once all processes have been converted and everything is working, we could drop the Contiki dependency.

Not everything we need is implemented yet, but it is already easy to see that pbio/os is much easier to get into than the full contiki library. Mainly because we only implement what we need, but also due to the simplifications introduced below.

  • Returning errors from protothreads is currently cumbersome. This modification allows protothreads to return error codes. PBIO_ERROR_AGAIN essentially takes the role of PT_YIELDED / PT_WAITING. Everything else indicates completion with the respective error or success.

  • Contiki partially distinguishes (but not really) between waiting and yielding. In practice, both yield and the latter yields at least once. And then both macros exist separately for processes and protothreads so you end up with four variants that are almost the same but subtly different. I think we can do everything with a single set of AWAIT macros.

  • The contiki event timers are more complicated than we need for our use case. Currently, a timer tick first polls the timer process and then posts N events depending on the number of active timers. This is not be safe if we have more timer events than fit in the event queue. Also, etimers use the global PROCESS_CURRENT which requires hacks such as PROCESS_CONTEXT_BEGIN when accessing them from elsewhere.

  • Instead of using various events and polling individual processes throughout, I think we may be able to work with just a single pbio_os_request_poll() function. Then all processes will check their current wait condition. This should not be that much more computationally expensive than checking the needs_poll flag on each process (and it may be less due to overall simplicity).

  • We currently use broadcasting between processes in a few places, which can be hard to follow and risks overflowing the event queue as already stated in a few existing REVISITs around the code. We have recently eliminated this for the uart process. We should be able to do this for the status change too.

  • We can move some of the logic for handling pending events and entering sleep mode to pbio instead of having it in mphalport. Aside from hooks such as enable_irq it becomes platform agnostic. This also brings the virtual hub a bit closer to the embedded hubs. It also unifies the EV3 and NXT with Powered Up.

The goal is to:
- Add error return value to all protothreads.
- Reduce complexity and code size.
- Avoid risk of overruning event queue.
- Use single poll flag and no broadcasting between threads.
- Cross-platform handling of events during sleep.
- Make stm32, ev3, nxt all work the same way.

For now the new OS also drives the contiki event loop so we can migrate processes one by one instead of breaking everything at once.
Idling with WFI was previously implemented in the MicroPython HAL. Now that we moved it to pbio/os in a platform agnostic way, we can fix this longstanding open REVISIT note.
This is a simple example to show that we can
convert the processes one at a time.
@laurensvalk
Copy link
Member Author

laurensvalk commented Mar 13, 2025

Example usage:

pbio_error_t pbio_os_wait_example(pbio_os_state_t *state, uint32_t duration) {

    static pbio_os_timer_t timer;

    ASYNC_BEGIN(state);

    printf("Going to wait for %d ms\n", duration);

    // We can return errors, parse args etc.
    if (duration > 2000) {
        return PBIO_ERROR_INVALID_ARG;
    }

    // This is a nice convenience wrapper I always missed.
    AWAIT_MS(state, &timer, duration);

    ASYNC_END(PBIO_SUCCESS);
}

pbio_error_t pbio_os_sub_example(pbio_os_state_t *state) {

    static pbio_os_state_t child;

    static uint32_t counter;

    pbio_error_t err;

    ASYNC_BEGIN(state);

    for (counter = 0; counter < 100; counter++) {

        AWAIT(state, &child, err = pbio_os_wait_example(&child, 100 * counter));

        if (err != PBIO_SUCCESS) {
            printf("Something bad happened!\n");
            return err;
        }
    }

    // Return arbitrary error code. Macro is needed to close up the } bracket just like in Contiki.
    ASYNC_END(PBIO_SUCCESS);
}

// Outermost "process thread" looks the same but must only have the state argument
pbio_error_t pbio_os_example_thread(pbio_os_state_t *state) {

    static pbio_os_state_t child;

    pbio_error_t err;

    ASYNC_BEGIN(state);

    // Await and get/use return value of awaited thread.
    AWAIT(state, &child, err = pbio_os_sub_example(&child));
    if (err != PBIO_SUCCESS) {
        return err;
    }

    ASYNC_END(PBIO_SUCCESS);
}

@laurensvalk
Copy link
Member Author

laurensvalk commented Mar 13, 2025

Similarly, the following:

PT_THREAD(pbdrv_uart_write(struct pt *pt, pbdrv_uart_dev_t *uart, uint8_t *msg, uint8_t length, uint32_t timeout, pbio_error_t *err)) {

    PT_BEGIN(pt);

    if (!msg || !length) {
        *err = PBIO_ERROR_INVALID_ARG;
        PT_EXIT(pt);
    }

    if (uart->tx_buf) {
        *err = PBIO_ERROR_BUSY;
        PT_EXIT(pt);
    }

    uart->tx_buf = msg;
    uart->tx_buf_size = length;
    uart->tx_buf_index = 0;

    if (timeout) {
        etimer_set(&uart->tx_timer, timeout);
    }

    uart->USART->CR1 |= USART_CR1_TXEIE;

    // Await completion or timeout.
    PT_WAIT_UNTIL(pt, uart->tx_buf_index == uart->tx_buf_size || (timeout && etimer_expired(&uart->tx_timer)));

    if (timeout && etimer_expired(&uart->tx_timer)) {
        uart->USART->CR1 &= ~(USART_CR1_TXEIE | USART_CR1_TCIE);
        *err = PBIO_ERROR_TIMEDOUT;
    } else {
        etimer_stop(&uart->tx_timer);
        *err = PBIO_SUCCESS;
    }
    uart->tx_buf = NULL;

    PT_END(pt);
}

Becomes:

pbio_error_t pbdrv_uart_write(pbio_os_state_t *state, pbdrv_uart_dev_t *uart, uint8_t *msg, uint8_t length, uint32_t timeout) {

    ASYNC_BEGIN(state);

    if (!msg || !length) {
        return PBIO_ERROR_INVALID_ARG;
    }

    if (uart->tx_buf) {
        return PBIO_ERROR_BUSY;
    }

    uart->tx_buf = msg;
    uart->tx_buf_size = length;
    uart->tx_buf_index = 0;

    if (timeout) {
        pbio_os_timer_set(&uart->tx_timer, timeout);
    }

    uart->USART->CR1 |= USART_CR1_TXEIE;

    // Await completion or timeout.
    AWAIT_UNTIL(state, uart->tx_buf_index == uart->tx_buf_size || (timeout && pbio_os_timer_is_expired(&uart->tx_timer)));

    uart->tx_buf = NULL;

    if (timeout && pbio_os_timer_is_expired(&uart->tx_timer)) {
        uart->USART->CR1 &= ~(USART_CR1_TXEIE | USART_CR1_TCIE);
        return PBIO_ERROR_TIMEDOUT;
    }

    ASYNC_END(PBIO_SUCCESS);
}

Error handling at the start and towards the end looks quite a bit more elegant. Awaiting on the condition remains basically the same.

@coveralls
Copy link

Coverage Status

coverage: 56.458% (-0.01%) from 56.472%
when pulling fb743c8 on os
into 75bb664 on master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants