2025-03-14 5:21 AM
Hi! I have ran into an issue with the HAL that I have yet to find a decent solution to. This issue has been discussed multiple times in different contexts on the forum, and I though I would write a post to gather some of the discussions/proposed solutions
I have labeled this post with the STM32H5 Series as that’s what I’m working on right now, but I believe this might apply to most/all of the STM32 Series.
The issue relates to callbacks in the peripheral drivers of the HAL. Take for example an SPI transaction that I want to initiate on an instance, and where I want the MCU to be notified by an ISR when the transaction is completed. The callback has a type signature of
void (* TxRxCpltCallback)(struct __SPI_HandleTypeDef *hspi);
in the case of a registered callback or simply
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi);
when the callback is overwritten from user code. In the callback I can easily identify which SPI instance triggered the interrupt and act accordingly. However, an issue arises when determining what piece of software has ownership of the handle.
Typically, one would want to write software drivers that handle the gritty details of the transaction and expose a nice and tidy interface to the application code. In this case it would be nice to identify the software driver “owning” the handle as well, to e.g. change some internal state or notify the application code through e.g. an RTOS semaphore.
Several solutions have been proposed:
Wrapping the HandleTypeDef in a wrapper struct.
In user code, one can define a wrapper struct around HandleTypeDef where HandleTypeDef is the first element A void* can hold a pointer to some user defined context. The HandleTypeDef and the wrapper struct will share the same base address, and we can cast the HandleTypeDef back to the wrapper struct inside the callback to access our user defined context. This solution is pretty neat except for the fact that CubeMX automatically generates the declarations of all the handles being used, so that they cannot be placed inside a wrapper struct in this way. Moving the handles will cause CubeMX to mess up your code when the project code is regenerated by CubeMX.
Adding a pUserData to the HandleTypeDef structs
By adding a single pointer (e.g. pUserData) to each HandleTypeDef-like struct, the user defined context can be attached to the HandleTypeDef structs and fetched from the handle inside the callback
A global table/mapping associating HAL handles with user context
Some kind of global array or mapping is kept in user code that relates each handle to its context. The core issue with this solution is that it makes it difficult to write modular code with good abstractions, and is likely not a good solution in the long run
I am currently employed on a small team with the need for rapid prototyping and quick proofs-of-concept. Modifying/rewriting the HAL or building our own application without CubeMX is something we do not have the resources to do, which excludes the two solutions that I find to be acceptable.
I want to open up a discussion on these points and encourage the ST employees to investigate this issue, as this is one of the most repeating issues I have run into with the HAL so far.
I believe adding a user context pointer to the HandleTypeDef structs to be the better solution (which can potentially be enabled and disabled in config in the same way as registrable callbacks to prevent bloat), but I am interested in hearing any other opinions!
2025-03-14 5:59 AM - edited 2025-03-14 6:03 AM
@jostlowe wrote:Hi! I have ran into an issue with the HAL that I have yet to find a decent solution to. This issue has been discussed multiple times in different contexts on the forum, and I though I would write a post to gather some of the discussions/proposed solutions
Can you provide a list of those topics. I only know of one other than this post and that is one I posted in recently:
https://community.st.com/t5/stm32-mcus-embedded-software/custom-context-in-callback-handlers/m-p/781937#M61042
@jostlowe wrote:Moving the handles will cause CubeMX to mess up your code when the project code is regenerated by CubeMX.
Instead of moving the struct, perhaps you can copy the struct and from that point on use the new handle.
example:
/* USER CODE BEGIN 0 */
// custom struct:
typedef struct
{
void* userContext;
// bla bla
I2C_HandleTypeDef originalHandle;
}
I2C_HandleTypeDef_wrapper_t;
I2C_HandleTypeDef_wrapper_t wrapper;
/* USER CODE END 0 */
I2C_HandleTypeDef hi2c1;
/* I2C1 init function */
void MX_I2C1_Init(void)
{
/* USER CODE BEGIN I2C1_Init 0 */
/* USER CODE END I2C1_Init 0 */
/* USER CODE BEGIN I2C1_Init 1 */
/* USER CODE END I2C1_Init 1 */
hi2c1.Instance = I2C1;
hi2c1.Init.Timing = 0x00B03FDB;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.OwnAddress2Masks = I2C_OA2_NOMASK;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
/** Configure Analogue filter
*/
if (HAL_I2CEx_ConfigAnalogFilter(&hi2c1, I2C_ANALOGFILTER_ENABLE) != HAL_OK)
{
Error_Handler();
}
/** Configure Digital filter
*/
if (HAL_I2CEx_ConfigDigitalFilter(&hi2c1, 0) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN I2C1_Init 2 */
wrapper.originalHandle = hi2c1;
/* USER CODE END I2C1_Init 2 */
}
// now use &wrapper.originalHandle instead of &hi2c1
I have not tested this.
@jostlowe wrote:A global table/mapping associating HAL handles with user context
Some kind of global array or mapping is kept in user code that relates each handle to its context. The core issue with this solution is that it makes it difficult to write modular code with good abstractions, and is likely not a good solution in the long run
This was my suggestion. I have used that a few times. How does this not provide modular code? How can you not make abstractions this way? You can even wrap using C++ code and make it as object oriented as you want.
The mapping table is in its own c file. The header would look something like this:
typedef void (*usercallback_t)(void *handlePtr, void* usercontext);
void registerI2cCallback(I2C_HandleTypeDef *handlePtr, type of callback?, usercallback_t cb, void* userContext);
void unregisterI2cCallback(I2C_HandleTypeDef *handlePtr, type of callback?);
And in user code you can have as many callback functions as you want. You can have one callback function for each peripheral instance and each event. Or you can combine the events, instances and even different peripherals!
I have used this with C++ too. You can have static member functions that take a pointer to an object instance as an argument. I use that as callback functions all the time. It works great.
I agree it would be better if ST adds a usercontext pointer to the handle struct.
2025-03-14 6:28 AM
Thanks for the quick reply @unsigned_char_array !
Appreciate the input! Here's a (probably incomplete) list of links to a few threads where similiar ideas were discussed that i found with a quick search:
Seems a ticked was raised on the G4 HAL but it was never deployed
I see your points regarding the mapping table! Excellent input. I think my arguments about modularity/abstractions might not stand :)
I have been leaning towards this solution myself, as it seems like the best option in the absence of a more "fundamental" fix in the HAL
One note though, is that the callback look-up has an O(n) runtime for each interrupt where n is the number of entries in the registry (perhaps it can be made O(log(n)). Shouldn't be too much overhead in most cases, but it is definitely more snappy to just have all your context one pointer indirection away!
Cheers!
2025-03-14 6:28 AM
@jostlowe wrote:This issue has been discussed multiple times in different contexts on the forum, and I though I would write a post to gather some of the discussions/proposed solutions
Would be helpful to give links to those previous discussions!
Here's three for starters:
2025-03-14 6:31 AM
@Andrew Neil you beat me too it! See my previous post for some links (although i suspect they are the same)
I saw you had posted in some of these discussions earlier and was hoping you'd show up :)
2025-03-14 6:35 AM
A custom void* pointer within the handle is probably the most likely solution, gated by a #define which you have to declare. There's unlikely to be a change to HAL which breaks previous code unilaterally.
2025-03-14 7:09 AM
@jostlowe wrote:One note though, is that the callback look-up has an O(n) runtime for each interrupt where n is the number of entries in the registry (perhaps it can be made O(log(n)). Shouldn't be too much overhead in most cases, but it is definitely more snappy to just have all your context one pointer indirection away!
Exactly. Extra overhead in terms of time, stack, flash, etc. And also extra code to write and test. It does work, but it shouldn't be needed. One way to speed it up would be to check which instance of the peripheral is used (spi1, spi2, etc.) when registering and then use a different function and table for each peripheral instance. Once the HAL calls the function there is only one lookup or only a few if different events have different handlers. But this would be more code...