Author Topic: FSMs and cycle frequency: Am I going too fast?  (Read 1003 times)

0 Members and 1 Guest are viewing this topic.

Offline FinweTopic starter

  • Newbie
  • Posts: 1
  • Country: br
FSMs and cycle frequency: Am I going too fast?
« on: July 18, 2022, 03:56:03 am »
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: [Select]
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?
« Last Edit: July 18, 2022, 04:00:12 am by Finwe »
 

Offline ataradov

  • Super Contributor
  • ***
  • Posts: 11269
  • Country: us
    • Personal site
Re: FSMs and cycle frequency: Am I going too fast?
« Reply #1 on: July 18, 2022, 04:06:00 am »
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.

Alex
 

Offline Nominal Animal

  • Super Contributor
  • ***
  • Posts: 6265
  • Country: fi
    • My home page and email address
Re: FSMs and cycle frequency: Am I going too fast?
« Reply #2 on: July 18, 2022, 07:31:26 am »
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: [Select]
/* 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[0])

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 within user_events_process(),
Code: [Select]
        for (int i = 0; i < UI_BUTTONS; i++) {
            struct button *const b = ui_button + i;  /* == &(ui_button[i]) */

            if (!b->deadtime) {
                if (gpio_button_pressed(b->gpio)) {
                    /* Individual button press. */
                    b->deadtime = DEADTIME_FIRST;
                    b->event(b);
                }
            } else
            if (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 the deadtime member 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 in main(), above.
 
The following users thanked this post: Bassman59

Online T3sl4co1l

  • Super Contributor
  • ***
  • Posts: 21693
  • Country: us
  • Expert, Analog Electronics, PCB Layout, EMC
    • Seven Transistor Labs
Re: FSMs and cycle frequency: Am I going too fast?
« Reply #3 on: July 19, 2022, 01:04:24 am »
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
Seven Transistor Labs, LLC
Electronic design, from concept to prototype.
Bringing a project to life?  Send me a message!
 

Offline ebastler

  • Super Contributor
  • ***
  • Posts: 6510
  • Country: de
Re: FSMs and cycle frequency: Am I going too fast?
« Reply #4 on: July 19, 2022, 07:26:25 am »
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.
 

Offline ajb

  • Super Contributor
  • ***
  • Posts: 2608
  • Country: us
Re: FSMs and cycle frequency: Am I going too fast?
« Reply #5 on: July 26, 2022, 07:04:09 pm »
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. 
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf