AnsweredAssumed Answered

Questions surrounding __HAL_LOCK

Question asked by Matt Kline on Apr 26, 2016
Latest reply on May 15, 2018 by Clive One
I’m an engineer at Fluke, and we’re using an STM32F4xx series microcontroller (together with its HAL drivers) as the basis for a new product. The HAL contains a “locking” mechanism, where each subsystem—I²C, USB, UART, and so on—has a “locking object”. My team has been working on the assumption that this mechanism’s goal is to provide some mutual exclusion for memory-mapped hardware registers and related state so that, for example, an interrupt doesn’t modify the same hardware state being manipulated in the main “thread” of exectution.

What’s confused us, however, is how this is done. The driver code found in stm32f4xx_hal_def.h defines the locking mechanism as follows:[1]

typedef enum
{
  HAL_UNLOCKED = 0x00U,
  HAL_LOCKED   = 0x01U
} HAL_LockTypeDef;
 
 
#if (USE_RTOS == 1)
  /* Reserved for future use */
  #error "USE_RTOS should be 0 in the current HAL release"
#else
  #define __HAL_LOCK(__HANDLE__)                       \
            do{                                        \
                if((__HANDLE__)->Lock == HAL_LOCKED)   \
                {                                      \
                   return HAL_BUSY;                    \
                }                                      \
                else                                   \
                {                                      \
                   (__HANDLE__)->Lock = HAL_LOCKED;    \
                }                                      \
              }while (0)
 
 
  #define __HAL_UNLOCK(__HANDLE__)                     \
             do{                                       \
                 (__HANDLE__)->Lock = HAL_UNLOCKED;    \
               }while (0)
#endif /* USE_RTOS */



In summary, “locking” consists of setting a flag, or returning an error (indicating the system is busy) when the flag is already set. “Unlocking” consists of unconditionally clearing the flag.

What throws us is the immediate return of HAL_BUSY in the case of contention. If a HAL lock is providing mutual exclusion between the main thread and an interrupt handler, what should be done if the interrupt is the second caller of __HAL_LOCK and gets HAL_BUSY? An interrupt cannot pause its execution and start again when the lock is no longer contended, like a userspace thread could in a multitasking OS. (The best one can do is have the interrupt schedule the work for some later time, but this is not always feasible.) Additionally, many read-modify-write sequences (such as ORing bits) are performed on hardware registers without any additional protection besides these "locks". What is to keep an interrupt (or, if one is running an RTOS, another task) from executing in the middle of one of these sequences?

Based on these observations, we tried redefining __HAL_LOCK and __HAL_UNLOCK to be a global “interrupt lock”, i.e., the former disables interrupts and the latter re-enables them. For cases where several calls to these functions nest (whether intentionally or unintentionally), we keep track of a nesting count. Unfortunately this doesn’t work well with the I²C driver, since it contains several code paths where, on a timeout, __HAL_UNLOCK is called twice for a single call to __HAL_LOCK, unbalancing our nesting count to disastrous effect.

How is the __HAL_LOCK/__HAL_UNLOCK system meant to work? It would seem we’re not using it according to its design goals.

[1] All code is taken from STM32CubeF4, version 1.11.0

Outcomes