FSMs and cycle frequency: Am I going too fast? - Page 1
Forum section: electronics / general

author: Finwe
author_rank: Newbie
timestamp: July 18, 2022, 03:56:03 am
content: Folks,I need some feedback on a design choice for a toy project. I plan to run some signal processing code in a STM32 that needs to read analog inputs, update a PWM based on them, read user inputs to navigate in a menu, and update a LCD. My main loop is a classic FSM, basically:Code:void main(void){init_sys();filters_init();user_events_init();lcd_init();while (1) {filters_process();user_events_process();lcd_process();}}I don't want to run too much code in interrupt handlers because I think that would create me a nightmare in the future. So my ISRs just put the data in a queue that my "filters_process()" will consume in a predicable way. Every "_process()" function checks a timer at its beginning then skips if it's not ready to proceed. Also seems pretty common.To not fall behind, the main loop will need to run at least at 48 kHz (I'm processing audio) and currently my *average* loop frequency is ~240 kHz... well, I'm not lacking CPU power at least!Question: is it common to have these loops going that fast? Most examples that I saw run at 100 Hz or 1 kHz, some with explicit delays at the end...Is the design for my sound processing code... err... *sound*?Is there some best practice for this? Or some kind of industry standard?

author: ataradov
author_rank: Super Contributor
timestamp: July 18, 2022, 04:06:00 am
content: You often see a delay as a way to yield time in a preemptive multitasking system. This does make sense. But why would you want a delay in a simple loop like this? It has no benefits at all. On MPUs with power scaling it might make sense to conserve the power, but on MCUs simple blocking delay will not do anything. You can go into some shallow sleep mode if you have power concerns, but usually this is not a problem.

author: Nominal Animal
author_rank: Super Contributor
timestamp: July 18, 2022, 07:31:26 am
content: As long as you use a timer counter, say the internal CPU cycle counter (ARM_DWT_CYCCNT) to determine the intervals related to button presses and so on, I do not see any problem in having the FSM loop repeat that often.For example, let's say you have an "up" button. How exactly do you determine it is time to perform the "up" action? You surely don't want to do it each iteration of the FSM, because that'd lead right now to ~250,000 up goings per second. What I recommend, is using an unsigned integer counter to implement the delay, or "dead time". Whenever you detect that "up" button is being pressed, you check the counter. If it is zero, you do the action, and set it to the inital press delay (in timer units; CPU cycles if using ARM_DWT_CYCCNT). If it is nonzero, you decrement the counter by the duration of the last iteration, and only do the action if it brings the counter to zero.Example:Code:/* Interval, in milliseconds, after initial button press */#define DEADTIME_FIRST_MS 300/* Autorepeat interval, in milliseconds */#define DEADTIME_REPEAT_MS 200/* Deadtime intervals in CPU cycles, using ARM_DWT_CYCCNT */#define DEADTIME_FIRST ((uint32_t)(FCPU * DEADTIME_FIRST_MS / 1000.0))#define DEADTIME_REPEAT ((uint32_t)(FCPU * DEADTIME_REPEAT_MS / 1000.0))struct button {void (*event)(struct button *); /* Parameter is a pointer to this structure */int gpio; /* GPIO pin, perhaps? */uint32_t deadtime; /* Current deadtime */};struct button ui_button[] = {{ .event = FUNCTION_TO_QUEUE_THE_EVENT, .gpio = GPIO_TO_CHECK, .deadtime = 0 },/* All UI buttons listed here like above */};#define UI_BUTTONS (sizeof ui_button / sizeof ui_button)void main(void){uint32_t last_cyccnt, curr_cyccnt, duration;init_sys();filters_init();user_events_init();lcd_init();curr_cyccnt = ARM_DWT_CYCCNT;while (1) {last_cyccnt = curr_cyccnt;curr_cyccnt = ARM_DWT_CYCCNT;duration = (uint32_t)(curr_cyccnt - last_cyccnt); /* Reminder: wraparound math! */filters_process();user_events_process(duration);lcd_process();}}and withinuser_events_process(),Code:for (int i = 0; i < UI_BUTTONS; i++) {struct button *const b = ui_button + i; /* == &(ui_button) */if (!b->deadtime) {if (gpio_button_pressed(b->gpio)) {/* Individual button press. */b->deadtime = DEADTIME_FIRST;b->event(b);}} elseif (b->deadtime <= duration) {if (gpio_button_pressed(b->gpio)) {/* Autorepeat. */b->deadtime = DEADTIME_REPEAT;b->event(b);} else {/* Button no longer pressed. */b->deadtime -= duration;}} else {/* Button is in deadtime after an event. */b->deadtime -= duration;}}This gives you immediate button response, but fixed initial delay and autorepeat intervals. It will react to spurious noise on the GPIO, but given sufficient dead time, is immune to switch bounce. There are other patterns as well, but all the good ones rely on some sort of a reliable counter to determine exactly when to act on the current button state.Note how the button loop doesn't even check the GPIO state unless the dead time has elapsed? This way you don't waste checking the GPIO state, unless it is something we can act upon. Technically, only thedeadtimemember needs to be in RAM, and the rest could be left in Flash/ROM, but meh, this is just an example.Written this way, button response is not at all dependent on the FSM cycle interval; it is dictated by the timer counter, here the cycle counter,ARM_DWT_CYCCNT, with the duration of the previous iteration used for the dead time counters. It is not cycle-accurate, but it does generate the events in the correct iteration of the loop, plus or minus one iteration, and should definitely be precise enough for UI stuff. Other things might wish to keep track of the cycle counter on their own, but given thousands of iterations per second, I'd just use the same mechanism; that's why I did it inmain(), above.

author: T3sl4co1l
author_rank: Super Contributor
timestamp: July 19, 2022, 01:04:24 am
content: If audio is the only thing going on, I might just do it all in the ISR. Actually I did, albeit on a probably somewhat simpler project:https://github.com/T3sl4co1l/Reverb/(Highlights: main.c, dsp.c)This doesn't have anything else as hard real-time as the audio, and what's left can be serviced almost whenever (serial takes a couple audio samples before another character comes in/out). Interestingly it's oversampled, then a counter counts sampling events and copies the accumulator down periodically. Overkill really for the ADC on the XMEGA, it's good enough by itself, but ended up not taking many CPU cycles, no biggie.The copy-accumulator and process interrupts are separate; this even allows serial to interrupt it anyway, without missing a sample event (which would cause popping noise as the gain is momentarily in error -- or maybe even overflows if near full scale!). A jitter error is less noticeable. Or maybe serial is still lower priority, I forget how both "low priority" interrupts are ordered.Anyway, interaction is only on an as-needed basis, to set values, navigate menus, so even if it pauses audio, that's not a big deal.Meanwhile, for main(), I like to use a fixed sample rate for state machine stuff. Like here for example, pushbuttons are checked and debounced periodically, and the display updated as a result. Serial console is updated more often, but it just passes if no data are available, so it's a small difference. Having most everything sampled, means I can get a measure for spare CPU cycles as loop counts per whatever period, for example.Putting state machine stuff on a timer vs. unlimited loops, doesn't make any difference to the logic itself, but it does use less CPU cycles waiting for something to happen, and you may find it helpful to e.g. increment timers every step, rather than testing for a timer event and then advancing logic. But that's not a big deal, to pass along such a signal, either. And if you don't have anything else to do, anyway, yeah, doesn't matter.And as you can see, I like to do the pattern where a timer interrupt sets a flag, and the flag is read and reset asynchronously by main (generator-consumer pattern). This clears the flag so the event needs to be passed along to any main functions that might also need it.Tim

author: ebastler
author_rank: Super Contributor
timestamp: July 19, 2022, 07:26:25 am
content: You have not told us what is actually happening in the three routines in your main loop. Just going by their names, I would assume that filters_process() is actually the only routine which can never "miss a beat", since it does real-time audio processing.In contrast, user_events_process() and lcd_process() sound like they interact with the user, who is not operating on a strict 48 kHz clock.So these probably have much more relaxed timing constraints?If that is the case, it seems undesirable to impose the strict 48 kHz timing constraints on the UI routines by including them in the same main loop as the time-critical filter processing. I would move all time-critical filtering into the ISR, and have the main loop operate asynchronously and with uncritical timing.

author: ajb
author_rank: Super Contributor
timestamp: July 26, 2022, 07:04:09 pm
content: Remember also that the ARM NVIC allows multiple levels of interrupts with varying priority, which opens up several possibilities for dealing with tasks of different timing sensitivities. For example, if your filters need to be serviced on a strict schedule you can do all of that in a timer interrupt set to a relatively low priority, which will still allow other hardware-based interrupts that need prompt handling but not very much processing (setting/clearing flags or advancing buffers or whatever) to interrupt the filter processing.You also have the PendSV and SVCall interrupts available--these are normally used to support RTOS implementations for task switching, system services, privilege level escalation, etc, but can also be used as general-purpose software interrupts in situations where you need a bit more flexibility in how various tasks are scheduled but don't need the full complexity/overhead of a complete RTOS. For example, if you have a communications interface that receives data that needs immediate but time-consuming processing, you could run the comms ISR at a high priority, and have it pend the PendSV interrupt when it has data ready to be processed. If the PendSV IRQ is set to a lower priority, its ISR will be invoked when the current higher-priority comms ISR (and any other pending/active higher priority irqs) exits. This essentially allows you to have a separate, higher-priority, thread of execution that is independent of your main loop and hardware-servicing ISRs.