Event Group / Signaling Strategy

hirohiro wrote on Monday, November 04, 2019:

Hey all, I’m building an application based on FreeRTOS and am looking for some advice on how to implement signaling between different threads/modules. I’m using event groups extensively to signal threads but am not sure that the way I have implemented it is the optimal way. Right now what I have is:

  • Separate event group per thread
  • All event groups have global scope so any module/thread can set them
  • Each thread blocks on it’s own event group
  • Each thread may handle the event directly or propagate the event to other threads. (ie. UI Task would set one or more events for other tasks depending on UI state when it receives a button pressed evt)

Does this make sense? Are there glaring flaws with this? Two of the items in particular I have concerns with are:

  1. Should the scope of each event group be confined to the task responsible for it and only be set through a well defined API? For example should a GPIO interrupt handler directly set the BUTTON_PRESSED event or call a UI_Task_Button_Pressed() API which would internally set that flag?
  2. Is there a correct way of propagating events through multiple tasks? For example a SHUTDOWN_EVT would need to propagate through each task to trigger shut down activities. Currently I have a SYS_MGR task which will handle the SHUTDOWN_EVT and as a result set a shutdown event in each of the other tasks.

richard_damon wrote on Monday, November 04, 2019:

Personally, I prefer to put all the FreeRTOS interfaces behind the modules API, so their is a functions call that other modules will make to send the notification, and then internally that gets converted into a set event call. This has the advantage that if you change your mind on how you want to implement that notification (like when event groups were created), the change is very locallized.

Generally, I try to make each task have ONE primary thing it is waiting on to process, this could be an event group, a semaphore, a queue, a message buffer, or what ever. In processing that item it might have some short waits on other things (a mutex for access to something, or a semaphore that is part of some device driver (like I2C).

For something like a Shutdown Event, I create a llist of call backs, that each task that needs to react to the shutdown registers its own callback, that ends the task the appropriate notification so it knows the shutdown is happening, and then when the shutdown request happens, this list is walked and the functions are called and the tasks get their notification (This means the shutdown system doesn’t need to know who all needs notification, tasks know if they need it, and they set themselves up as needed).

hirohiro wrote on Monday, November 04, 2019:

Okay that makes a lot of sense- I like the idea of having each module register itself for events it cares about.

When exposing a module’s API do you keep a separate header just to define public APIs and a separate one for private definitions or is the entire module header just left to be visible by all who include it?

richard_damon wrote on Monday, November 04, 2019:

In C, if the entire module is contained in one source file, you only need to declaire the public API in the header, the private stuff can just be kept static and internal. If you need to use multiple files. I tend to make an internal header and a public header file. You only need to expose private details if the using application needs to allocate private structures.

hirohiro wrote on Tuesday, November 05, 2019:

Thanks again Richard. One follow up. I just discovered the task notification IPC. Is there any reason not to use this instead of eventGroups? It seems like the only limitation is that only one task can consume which is already a part of the above design.

hs2sf wrote on Tuesday, November 05, 2019:

I’d recommend to use TaskNotifications because they’re lean and fast.

richard_damon wrote on Tuesday, November 05, 2019:

Direct to Task Notifications can replace many uses of Event Groups. Event groups are still useful when you have multiple tasks possibly waiting for an event (maybe something different than the Shutdown event). One example would be the driver for a multichannel A-D, where a task wanting a reading calls a function that will block on the bit for that channel after queue up a request for it.