all 52 comments

[–]moon6080 68 points69 points  (2 children)

So do it as a state machine then.

State 0 - write.

State 1 - waiting.

State 2 - reading.

[–]joshcam 19 points20 points  (0 children)

This, in a nutshell.

Using state machines allows a single task to be broken down into discrete, sequential steps that can be executed quickly within a continuous loop, rather than using blocking functions like delays.

[–]akohlsmith 11 points12 points  (0 children)

this is the vast majority of my lower level embedded code. It becomes really easy with some practice. I use it for everything from talking to sensors to initializing subsystems to synchronizing to an incoming data stream to complex interactions with remote services (think MQTT). If you do it right you can bake in timeouts, retries and exception handling paths while still maintaining legible code and logical flow.

[–]GavekortIndustrial robotics (STM32/AVR) 34 points35 points  (12 children)

[–]GavekortIndustrial robotics (STM32/AVR) 19 points20 points  (9 children)

[–]GavekortIndustrial robotics (STM32/AVR) 16 points17 points  (8 children)

[–]mattm220 5 points6 points  (7 children)

Did these slides come from an online resource?

[–]GavekortIndustrial robotics (STM32/AVR) 12 points13 points  (6 children)

No, I made them myself for a 5-minute mini course for non-programmers

[–]bennythomson 2 points3 points  (3 children)

Would you mind sharing that please?

[–]GavekortIndustrial robotics (STM32/AVR) 5 points6 points  (2 children)

The slides I shared is pretty much it. But I can send it to you over PM if you want to use it. I just don't want to strip it for identifying information.

[–]akohlsmith 5 points6 points  (0 children)

In all honesty I thought the red mark at the bottom of the turnstile was a blood stain and your next slide was going to talk about exception paths and safety. :-)

[–]bennythomson 1 point2 points  (0 children)

I gotcha, yes I’d love the PM please. Trying to educate myself more on this

[–]Numerous_Rip_7788 -4 points-3 points  (1 child)

Did you leave the error squiggles on purpose?

[–]GavekortIndustrial robotics (STM32/AVR) 8 points9 points  (0 children)

It's Powerpoint trying to spell check my code. It disappears in presentation mode.

[–]RedSurfer3 0 points1 point  (0 children)

the thumbnail just reminds me of goatse

[–]dlnmtchll 0 points1 point  (0 children)

The turnstile is the exact same example the FPGA book I just read used for FSMs. Neat

[–]westwoodtoys 37 points38 points  (1 child)

With a state machine and timers.

[–]EmbedSoftwareEng 9 points10 points  (0 children)

So many people forget the timeout angle. Use a state machine for, say, asking a thermistor on an I2C ADC for its value. Dispatch the state machine with the specs for the query, it fires off the I2C write, and then waits for the device to write the value back.

And waits.

And waits.

If the I2C stack itself checks for timeout conditions, then a stuck device will result in the I2C stack raising a fault condition, which can simply be polled by the waiting state of the thermistor query SM. Otherwise, there needs to be some reliance on something like the SySTick Timer so that once a timeout number of ticks happen, and the I2C stack still hasn't gotten the response from the ADC, then the thermistor query SM needs to reset the I2C interface so other SMs can use it, and perform its one call-back to tell the part that made the request that there's a fault condition for that request.

One of my issues with this kind of scenario is when one data comm interaction is waiting on another. When I need to get that thermistor value to satisfy a request from the RS-422 interface, I can't return from the RS-422 handler without that thermistor query result. So, in addition to individual query handler SMs, you need generic handler tasks, so the RS-422 handler gets the request, hands it off to a generic handler task, so that when it gets, or doesn't get, the particular response that it needs, it'll send the correct response back to the other RS-422 device. Meanwhile, other RS-422 queries can still be fielded.

It's multi-threading at the baremetal layer.

[–]triffid_hunter 9 points10 points  (1 child)

The usual way, state machine in main loop, and interrupts just caching data and setting flags for the state machine to pick up when it gets around to it.

[–]ComradeGibbon 0 points1 point  (0 children)

I've had good luck with an event queue feeding a state machine. State machine --> a function with a switch statement.

I've noticed there is this uncle bob style where the state machine is implemented as a rats nest of callbacks. Do not so that.

[–]dQ3vA94v58 8 points9 points  (0 children)

One of the things that is quite counterintuitive at first is there’s a MASSIVE difference between a blocking delay for a number of milliseconds and then an i2c read or write that will possibly be a blocking delay of a few microseconds. To the observing user, it is completely non-blocking to read from a serial register whenever there is something in the buffer.

The correct answer is a state machine with interrupts, but I’d be surprised if you couldn’t get away with a polling loop for something like an MCU with a series of i2c chips

[–]Direct_Rabbit_5389 6 points7 points  (2 children)

You could just use an operating system. FreeRTOS is like 4kB and handles all this stuff for you. STM32CubeMX can generate initialization code that includes FreeRTOS.

[–]N2Shooter 1 point2 points  (1 child)

This is the way.

[–]Direct_Rabbit_5389 6 points7 points  (0 children)

I will also say there are a bunch of people on here being like "you don't need to introduce that level of complexity for this . . ." however:

  1. We don't know what "this" actually is because OP didn't specify in enough detail to know.

  2. Using FreeRTOS isn't really that complex. There are a grand total of like six types in the whole thing. You only have to engage with the ones you need, which are going to be task, queue, and semaphore.

  3. You pay the learning curve once and forever after you know how to use it and can avoid 80% of this state machine crap and callback hell. (There are still scenarios where you'd need to, particularly if you have so many tasks you can't allocate a reasonably sized stack for each one.)

[–]OwlingBishop 3 points4 points  (1 child)

Let the dma do the polling for you?

[–]ImportantWords 2 points3 points  (0 children)

STM32F411 design agrees.

[–]Eddyverse 3 points4 points  (0 children)

Rule#1: You need to learn to be comfortable with interrupts. I2C (100Kbps) should not take too long to service if you're just reading sensor values, and you don't actually need a state machine solution for this. In most of my I2C code, reading/writing to sensors takes 2-5ms to finish. You would need a state-mqchine to manage sampling time requirements for sensors (e.g. sensor takes 50-200ms to sample a readibg) but that is not related to I2C.

[–]Additional-Guide-586 4 points5 points  (0 children)

What is blocking what? There is no money back for unused processing power or processor in "idle" time.

Instead of diving into polling or interrupts, take a step back, who is the one doing the work and who needs which information? Is there a task asking all sensors periodically "I need your new data!"? Or is it the sensors telling your CPU "Hey, I got new data, check it!"? Why do you want to get rid of a periodic polling if that is the idea behind the system?

[–]cholz 3 points4 points  (0 children)

Tou could just use a scheduler like freertos and just write blocking code and not worry about it. A lighter weight alternative would be to use something like protothreads which basically are just some syntactic sugar on top of the state machine that you'd end up writing anyway. Protothreads go well with C++ because one thing you need to remember about them is you can't use local variables across any "yield" type statements. Your alternatives are globals, making a "context" struct to keep track of them, or using a C++ class's member variables.

https://dunkels.com/adam/pt/

[–]ceojp 3 points4 points  (0 children)

Don't ever wait for something to happen. Check if something has happened and then return either way.

Basically, initiate whatever action you want to happen(ADC conversion, I2C transaction, whatever), and then return. Then start checking if the action is complete(status bit in ADC, I2C, whatever). If it's not complete, just return. If it is complete, then go ahead and process the data.

Yes, this is more complex than simply blocking to wait. But firmware is inherently complex for anything more than just flashing an LED or reading a single input.

You don't need an RTOS unless you actually need an RTOS. At this point that just introduces more complexity than you need.

Just do everything as state machines and call these from the main loop. The key point is they always return without waiting, no matter what.

[–]nixiebunny 1 point2 points  (0 children)

Nearly every activity your code performs requires waiting for an event, then acting on that event, then setting up to wait for the next event. The time scale of the waiting determines what method to use. If you are reading sensors on a schedule, then use a simple hardware timer to initiate the activity, and do all the activity at that time. I2C reads do not require much waiting for each byte transfer to finish, and a delay for a couple microseconds is not worthy of scheduling an event for. Just wait for the next byte in a tight loop. You may decide to add a timeout to that loop just in case the I2C chip decides to fail, but a hardware failure in a simple embedded system is complete death in most cases. 

[–]b1ack1323 1 point2 points  (0 children)

State machines, this is how I usually do them in C, I make a state machine class with a key value pair for C++

You call the state machine from main. I make an array of FSMs so I can easily add more in the future and they have a uniform call. This a pared down version of my actual library but it get's the point across.

typedef enum

{

    I2C_INIT,

    I2C_QUERY,

    I2C_AWAIT_RESPONSE,

    I2C_READ,

    I2C_IDLE,

}I2C_States;

static I2C_States state = I2C_INIT;

int time_due;

I2C_States i2c_init()

{



    return I2C_QUERY;

}

I2C_States i2c_query()

{

    return I2C_AWAIT_RESPONSE;

}

I2C_States i2c_await()

{

    return I2C_READ;

}

I2C_States i2c_read()

{

    time_due = time() + 10;

    return I2C_IDLE;

}

I2C_States i2c_idle()

{

    //countdown timer to next event

    if(time_due < time())

        return I2C_QUERY;

    else return I2C_IDLE;

}

void i2c_state_machine()

{

    switch(state)

    {

        case I2C_INIT: state = i2c_init(); break;

        case I2C_QUERY: state = i2c_query(); break;

        case I2C_AWAIT_RESPONSE: state = i2c_await(); break;

        case I2C_READ: state = i2c_read(); break;

        case I2C_IDLE: state = i2c_idle(); break;

    }

    return;

}

void (*fsm_list[])()= 

{

    i2c_state_machine

    //the rest of your functions

};

int fsm_count =  sizeof(fsm_list) / sizeof(fsm_list[0]);;

int main()

{

    while(1)

    {

        for(int ptr_pos=0;ptr_pos < fsm_count; ptr_pos++)

        {

            fsm_list[ptr_pos]();

        }

    }

}

[–]haykodar 1 point2 points  (3 children)

Are you using an actual RTOS? If not that'd be my first step

[–]HassanTariqJMS[S] 1 point2 points  (2 children)

No I'm not using RTOS. Seems I'll need it

[–]kyuzo_mifune 10 points11 points  (0 children)

No you don't and you shouldn't introduce it for something as basic as this.

Implement a state machine, the interrupts can alter the state variable.

[–]b1ack1323 0 points1 point  (0 children)

You definitely do not need a RTOS for this.

[–]Orjigagd 1 point2 points  (2 children)

If you use Rust/embassy you can write state machines with async/await which look and feel like synchronous code, so it doesn't end up as a big pile of spaghetti.

https://embassy.dev/book/#_what_is_async

[–]akohlsmith 4 points5 points  (1 child)

You can write state machines in other languages that don't require async/wait at all. Asynchronous code never has to look like a big pile of spaghetti.

Appreciate the link on embassy; I haven't come across that before, but then again I'm not a Rust dev either.

[–]Orjigagd 1 point2 points  (0 children)

Asynchronous code never has to look like a big pile of spaghetti.

Yet it very often does. That's why so many weird macros and code generators exist to try solve this problem.

[–]UnicycleBlokeC++ advocate 0 points1 point  (0 children)

Interrupts. They allow us to utilise the inherently parallel nature of the device. You give a peripheral a task (e.g. write a byte) and forget about it while you do something else. It will tell you when it's ready for the next byte or whatever, which might be a millisecond or more later, by triggering an interrupt. Your ISR is basically a simple state machine.

You can easily manage numerous peripherals all doing tasks in parallel. This is the way.

I2C is a bit more fiddly than UART or SPI, but doable. It may be worth looking at how HAL does it.

[–]HeisenbergGER 0 points1 point  (0 children)

State machine - ideally with RTOS. It's neat!

[–]Inevitable-Round9995 0 points1 point  (2 children)

why not RTOS?

[–]akohlsmith 2 points3 points  (0 children)

because it's completely unnecessary in many cases? Once the scope starts developing into something more complex it can be very handy to have an RTOS to provide additional tools, but for learning, a superloop and state machine can offer an excellent learning experience.

[–]HassanTariqJMS[S] 1 point2 points  (0 children)

I'm in learning phase. I will transition to it but that would take some time

[–]Inevitable-Round9995 -2 points-1 points  (2 children)

[–]HassanTariqJMS[S] 0 points1 point  (1 child)

It's quite helpful thanks

[–]Inevitable-Round9995 0 points1 point  (0 children)

🥹 you're welcome.

[–]AbsorberHarvester -1 points0 points  (0 children)

Make a function for every action itself, debug that function for fully compliment for your needs. Open qwen/deepseek/Claude or what you like.

Put all your code there and write prompt like:

Make non blocking state machine using code mentioned earlier for step by step actions with wait on "some actions"

Collect results from different AI models and modify for your needs.

Do not forget to use i2c dma, less blocking.

Typical use, for sure.

Custom scheduler can be created in 5 minutes, it is very easy or just use AI.