Skip to content

Code Snippets and Tricks

Marco Migliorini edited this page Oct 21, 2024 · 26 revisions

This page lists the necessary informations to explain the working logic of the proposed setup.

This system is designed to remain in an ultra low-power state while the device is at rest. The MCU stays in standby mode, and the IMU monitors for motion, generating an interrupt when movement is detected. Once motion is detected, the device is reconfigured: the IMU switches to measuring mode and stores all measurements in its internal FIFO buffer, while the MCU transitions to stop mode 2. This enables the MCU to quickly perform a burst read of the stored measurements.

Measurements are initially logged into an SRAM region on the MCU, providing a local buffer that minimizes writing operations to external permanent memory (to be added in future work). The MPU6050 also generates a zero-motion interrupt when movement ends, allowing the device to return to standby mode when measurements are no longer required.

MPU6050 features

Motion Interrupt

During motion interrupt the MPU6050 ticks the accelerometer axes at a configurable frequency (down to 1.25 Hz) while keeping all the other components into a low power state, reducing the necessary current to ~10 uA (plus 70-80 uA required by the voltage regulator).
The correct procedure to setup the motion interrupt is implemented by the corresponding function located in the source file Core/Src/mpu6050.c .

void MPU6050_setupMotionInt(I2C_HandleTypeDef *I2Cx, uint8_t duration, uint8_t threshold, int16_t* offsets)

The motion duration (1 LSB = 1 ms) and motion threshold (1 LSB = 2 mg) can be configured by the user. From an experimental perspective, I recommend keeping the duration short (1-2 ms) and increasing the threshold parameter to around 30-40 mg for better results.

Zero Motion Interrupt

Zero motion interrupt is used to generate an interrupt to revert the device operation when motion is no longer detected.
The setup procedure is similar to "Motion Interrupt", see the source file Core/Src/mpu6050.c.

void MPU6050_setupZeroMotionInt(I2C_HandleTypeDef *I2Cx, uint8_t duration, uint8_t threshold, int16_t* offsets)

In this case zero_motion_duration (1 LSB = 64 ms) and zero_motion_threshold (1 LSB = 2 mg) should be configured for longer duration (few seconds) and higher threshold to avoid entering sleep state while the device is still moving. The main difference between these two motion interrupts is related to the DHPF (digital high pass filter) configuration: while the motion interrupt uses "hold-setting", meaning that the DHPF output is given by the difference between the current sample and the sample stored in the MPU6050 before entering sleep state, the zero motion interrupt is based on "High pass filtering" to remove the bias due to gravity and avoid erroneous trigger signals.

In this application zero motion trigger was redirected to a GPIO pin configured as EXTI (External interrupt mode with rising edge trigger detection). The corresponding callback must be implemented to catch the interrupt event:

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  if(GPIO_Pin == IMU_Int1_Pin)
  { 
    // code here...
    zero_mot_trg = true;
  }
}

Internal FIFO buffer

The MPU6050 internal FIFO (1024 bytes) is used to store sensors readings and avoid to continually read the outputs which would require to keep the MCU active. FIFO can be configured using the following code:

void MPU6050_setupFifoBuffer(I2C_HandleTypeDef *I2Cx, uint8_t dlpfMode, uint8_t freqDivider, bool overflowEnabled) {
  // force gyro output rate to 1kHz
   MPU6050_setDLPFMode(I2Cx, dlpfMode);

   // frequency divider:
   MPU6050_setRate(I2Cx, freqDivider);

   // enable sensors writing to FIFO:
   MPU6050_setXGyroFIFOEnabled(I2Cx, true);
   MPU6050_setYGyroFIFOEnabled(I2Cx, true);
   MPU6050_setZGyroFIFOEnabled(I2Cx, true);
   MPU6050_setAccelFIFOEnabled(I2Cx, true);

   // enable interrupt generation when the FIFO overflows:
   MPU6050_setIntFIFOBufferOverflowEnabled(I2Cx, overflowEnabled);

   //set trigger event: Active high until interrupt status register is cleared, push-pull configuration
   MPU6050_setInterruptLatch(I2Cx, 1);
   MPU6050_setInterruptLatchClear(I2Cx, 0);
   MPU6050_setInterruptDrive(I2Cx, 0);
   MPU6050_setInterruptMode(I2Cx, 0);

   // Enable the FIFO here:
   MPU6050_setFIFOEnabled(I2Cx, true);

}

The FIFO filling rate depends on the gyroscope output rate, which is configured by setting the DLPF (digital low pass filter, output rate can be 8 kHz or 1 kHz) and the frequency divider (1 - 255). In my application I used the lowest possible frequency: DLPF set to BW = 188 Hz which results in a gyro frequency of 1 kHz, and divider equals to 255, meaning that the sensors sampling frequency was set to ~4 Hz. In my case only accelerometer and gyroscope readings were selected, corresponding to 12 bytes (6 for accel. 6 for gyro.) added to the FIFO each time. The interrupt latch was configured to remain high until the status reg is cleared. This is used to correctly detect the interrupt in the case the MCU is busy while interrupt is generated.

Problem with overflow interrupt

MPU6050 provides the possibility to generate an interrupt when the FIFO overflows. This would be useful to trigger the MCU activation, however there are few problems related to this feature as widely described here. Briefly, the fifo count register updates even if a full packet has not been written in the FIFO yet, potentially resulting in data corruption if the FIFO is read during this "unlucky" cases. The correct fifo sequence should be as follows (if we are storing accel. and gyro. readings):

Expected correct FIFO readings:
| Ax Ay Az Gx Gy Gz Ax Ay Az Gx Gy Gz ... Ax Ay Az Gx Gy Gz Ax Ay Az Gx Gy Gz |

however, sometimes a full 12-byte packet is not written in a single shot, resulting in a potential data corruption:

| Ax Ay Az Gx Gy Gz Ax Ay Az Gx Gy Gz ... Ax Ay Az Gx Gy Gz Ax Ay (Az Gx Gy Gz not written yet) |

If the FIFO overflows with partially written packet:
                    
| Gy Ay Az Gx Gy Gz Ax Ay Az Gx Gy Gz ... Ax Ay Az Gx Gy Gz Ax Ay Az Gx|
  ^                 ^   1st entire  ^     ^ last complete ^                          
Overflow byte              packet               packet
(Gz missing)
(Ax overwritten)

In the last situation the first fifo byte would be Ay. There wouldn't be any way to skip the corrupted packet (Ay Az Gx Gy Gz) and move to the first entire packet if we are not sure that the overflowed packet was entirely written. Notice that fifo count stucks at 1024 even if the buffer has overflowed.

This issue makes FIFO overflow useless in my application, given that there wouldn't be any way to reconstruct the measurements without knowing the exact position of the 1st byte in the FIFO. As an alternative solution I used a time based approach, keeping the MCU into stop mode 2 for the time necessary to completely fill the FIFO using 12-byte long packets and 4 Hz sampling rate. This can be done by configuring the RTC to generate an interrupt event to wake the MCU before the fifo overflows.

Reading FIFO with DMA

DMA (Direct memory access) was used to read the 1024 bytes stored in the FIFO in the fastest possible way. This trick allows to keep the FIFO off for less than 7 ms. For details about how to setup the DMA using CubeIde see the Getting Started section. The DMA can be used by calling the HAL function:

HAL_StatusTypeDef HAL_I2C_Mem_Read_DMA(I2C_HandleTypeDef *hi2c, uint16_t DevAddress, uint16_t MemAddress,
                                       uint16_t MemAddSize, uint8_t *pData, uint16_t Size)

Once the DMA transmission has been completed a callback is executed. Refer to the "..._hal_i2c.c" file (in my case stm32l4xx_hal_i2c.c) for additional details:

*** DMA mode IO MEM operation ***
=================================
[..]
  (+) Write an amount of data in non-blocking mode with DMA to a specific memory address using
      HAL_I2C_Mem_Write_DMA()
  (+) At Memory end of write transfer, HAL_I2C_MemTxCpltCallback() is executed and users can
       add their own code by customization of function pointer HAL_I2C_MemTxCpltCallback()
  (+) Read an amount of data in non-blocking mode with DMA from a specific memory address using
      HAL_I2C_Mem_Read_DMA()
  (+) At Memory end of read transfer, HAL_I2C_MemRxCpltCallback() is executed and users can
       add their own code by customization of function pointer HAL_I2C_MemRxCpltCallback()
  (+) In case of transfer Error, HAL_I2C_ErrorCallback() function is executed and users can
       add their own code by customization of function pointer HAL_I2C_ErrorCallback()

Therefore we must implement these two callbacks.

void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef * hi2c) {
  dmaTxDone = true;
}

void HAL_I2C_ErrorCallback(I2C_HandleTypeDef * hi2c){
  printf("Err DMA!\r\n");
}

STM32 MCU sleep states - tricks

Two different sleep conditions are used in this project: standby mode and stop mode2.

Standby mode

Allows to achieve the lowest possible current consumption. Almost all the peripherals are switched off, the ram contents is lost and a reset is performed once the mcu quits the low power state. In my project an external wake up pin is used to trigger the system activation when motion interrupt is generated by the MPU6050.

The standby mode initialization procedure might look something like this:

/* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART2_UART_Init();
  MX_I2C1_Init();
  MX_RTC_Init();
  /* USER CODE BEGIN 2 */
  
  // Recovering from standby mode, so the initialization was already performed
  if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET){ 

    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_SB);  // clear the StandBy flag

    /** Disable the WWAKEUP PIN **/
    HAL_PWR_DisableWakeUpPin(PWR_WAKEUP_PIN5);

    // Other user code here ...

  } else { // 1st initialization (executed only one time after powering on the device)

    // Initialize buffers stored in RAM2 section: 
    // (only variables in flash memory and in this SRAM region will be preserved)
    // Initialize here only the variables stored in the dedicated RAM2 region (see below for more details)
    memset(mainBuff, 0, sizeof(mainBuff));
    buff_Head = 0;

    // Initialize external sensors before entering sleep state
    // (In the complete code the mpu6050 initialization is performed at this stage)

    // Needed while loading the script (avoid conflict with ST-Link)
    HAL_Delay(500);

    // Other initialization code here...

    /** Now enter the standby mode **/
    
    /* Clear the WU FLAG */
    __HAL_PWR_CLEAR_FLAG(PWR_FLAG_WU);

    // Enable pull down on SYS_WKUP pin
    HAL_PWREx_EnableGPIOPullDown(PWR_GPIO_C, PWR_GPIO_BIT_5);
    HAL_PWREx_EnablePullUpPullDownConfig();

    // Enable RAM2 content retention
    HAL_PWREx_EnableSRAM2ContentRetention();

    /* Enable the WAKEUP PIN */
    HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN5);

    /* Finally enter the standby mode */
    HAL_PWR_EnterSTANDBYMode();


    // Upon waking up, code will be executed from the beginning (like a reset)
  }

Notice that in standby mode the RTC remains active only if sourced by the LSE.
Additionally, while recovering from standby we must skip the RTC initialization code to avoid losing the time reference used to generate the timestamps. This can be done by using the following code inside the MX_RTC_Init() function.

  /* USER CODE BEGIN Check_RTC_BKUP */

  if (__HAL_PWR_GET_FLAG(PWR_FLAG_SB) != RESET) {
    printf("\n\nWaking from StandBy mode, RTC skip!\r\n");
    return; // Exit the function to prevent reinitialize RTC
  } else {
    printf("\n\nInitializing RTC!\r\n");
  }

  /* USER CODE END Check_RTC_BKUP */

Add also this code to initialize the calendar with your own reference:

  /* USER CODE BEGIN RTC_Init 2 */

  // Initialize RTC with user variables:
   sTime.Hours = startHours;
   sTime.Minutes = startMinutes;
   sTime.Seconds = startSeconds;
   sTime.DayLightSaving = RTC_DAYLIGHTSAVING_NONE;
   sTime.StoreOperation = RTC_STOREOPERATION_RESET;
   if (HAL_RTC_SetTime(&hrtc, &sTime, RTC_FORMAT_BCD) != HAL_OK)
   {
     Error_Handler();
   }
   sDate.WeekDay = RTC_WEEKDAY_MONDAY;
   sDate.Month = startMonth;
   sDate.Date = startDate;
   sDate.Year = startYear;

   if (HAL_RTC_SetDate(&hrtc, &sDate, RTC_FORMAT_BCD) != HAL_OK)
   {
     Error_Handler();
   }

  /* USER CODE END RTC_Init 2 */

To allocate a memory region powered during standby mode modify the "..._FLASH.id" file by adding the following lines.


/*Allocate space in RAM2 to store variables between sleep cycles*/
.ram2 (NOLOAD):
{
  _sram2 = .;
  *(.ram2*)
  . = ALIGN(4);
  _eram2 = .;
} >RAM2

After that, variables can be allocated in that memory region by using this code:


char mainBuff[BUFFER_SIZE] __attribute__((section(".ram2"))) __attribute__((aligned(4)));
uint16_t buff_Head __attribute__((section(".ram2"))) __attribute__((aligned(4)));

Stop mode 2

In stop mode 2 the MCU reduces its current to ~ 6 uA while maintaining most of the settings.
Stop mode can be entered by using this code:

  // suspend tick and setup RTC interrupt
  HAL_SuspendTick();
  HAL_RTCEx_SetWakeUpTimer_IT(&hrtc, sleepTime, RTC_WAKEUPCLOCK_RTCCLK_DIV16);

  /* Enter Stop mode 2 */
  HAL_PWREx_EnterSTOP2Mode(PWR_STOPENTRY_WFI);

Tick must be suspended to avoid unintentionally awaking the MCU. In this case we used the internal RTC as wakeup source. The sleep time can be calculated using this general rule:

  • Wake-up Time Base = (RTC_WAKEUPCLOCK_RTCCLK_DIV /(LSI)) ==> WakeUpCounter = Wake-up Time / Wake-up Time Base

  • RTC_WAKEUPCLOCK_RTCCLK_DIV = RTCCLK_Div16 = 16, Wake-up Time Base = 16 /(32KHz) = 0.0005 seconds

    ==> WakeUpCounter = Wake-up Time / Wake-up Time Base = 20 s / 0.0005 s ~= 40000

Upon waking up the MCU functionalities can be restored by using this lines:

    HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);
    
    // Clock must be rehabilitated given that stop mode runs using the internal low power regulator
    // which supplies only the LSE oscillator
    SystemClock_Config();       
    HAL_ResumeTick();

See Getting Started for more details about the RTC setup, and external interrupts management. On the user side, the RTC callback must be used to manage the interrupt event.

void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
  // other code here...
  printf("wake from RTC\r\n");
  rtc_trg = true;
}

Remember also to initially deactivate the RTC wakeup event to prevent randomly waking up your microcontroller. This is achieved by adding the following line to your initialization code:

// Initially deactivate wakeup function:
HAL_RTCEx_DeactivateWakeUpTimer(&hrtc);

Initialize pin as SYSWKUP

A single pin can be used both for resuming system operation from standby mode (SYSWKUP) as well as to catch external interrupt events (EXTI). To do so, I followed this procedure: initialize the pin as EXTI, so CUBEIde will automatically generate all the call back functions to manage the NVIC; when the pin is needed as SYSWKUP, it can be reconfigured using the following code lines:

  // disable IRQ:
  HAL_NVIC_DisableIRQ(EXTI9_5_IRQn);
  
  // initialize pin as simple input:
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  GPIO_InitStruct.Pin  = GPIO_PIN_5;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLDOWN;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /* Enable the WAKEUP PIN */
  HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN5);

Debug using printf

In my project I relied on printf function for debugging purposes. To do so, the printf output can be redirected to the UART connected to the ST-Link. This can be done by adding this function into your main.c file.

// Function to redirect printf output to UART
int _write(int file, char *ptr, int len)
{
  HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY);
  return len;
}