STM32 HAL DCMI gocha

 I've been slowly digging through the OV5640 configuration, trying to fit it to my needs. And it's difficult to do, if you have 3 sets of potential error sources:

  1. Your own code
  2. Target device (camera) driver code
  3. STM HAL!

 Every now and then I've been getting chopped up images, such as this:

 

Eventually they would go away (after 5-10 captures), but meanwhile it's unusable. Especially if you are dealing with an autonomous device, which needs proper imagery.

Of course, first I blame myself, then others. Except this time it's not me, it's ST Micro!

The image is 312 899 bytes long. STM32F4xx DMA accepts maximum of 65365 (0xFFFF) words per transfer. So what HAL does, is splitting the whole dataset into multiple equal-sized transfers, each less than the upper limit. It then sets up double-buffer transfer and starts capturing. Luckily, there is HW support for that. While wiring into one buffer, you can change the pointer of the inactive buffer to a new location in DMA Half-transfer Complete callback. They even do the switching for you in the IRQ service, how convenient. 

Except they don't. Well, at least not always. The image above is captured using buffer size of 38400 words *4 = 153600 bytes. If you would open the above image in hex editor, you would notice, that it starts with 0xffd8 and ends with 0xffd9 as JPEG should. Except after first 153600 bytes there would be the same number of zeroes, followed by the reminding number of bytes. This led me to suspect, that the buffer switching is not happening as it should. Sometimes, but not always. Looking at relevant section in stm32_hal_dcmi.c, we can observe following chunk, that is meant to deal with that:

static void DCMI_DMAXferCplt(DMA_HandleTypeDef *hdma)
{
  uint32_t tmp = 0U;
 
  DCMI_HandleTypeDef* hdcmi = ( DCMI_HandleTypeDef* )((DMA_HandleTypeDef* )hdma)->Parent;
  
  if(hdcmi->XferCount != 0U)
  {
    /* Update memory 0 address location */
    tmp = ((hdcmi->DMA_Handle->Instance->CR) & DMA_SxCR_CT);
    if(((hdcmi->XferCount % 2U) == 0U) && (tmp != 0U))
    {
      tmp = hdcmi->DMA_Handle->Instance->M0AR;
      HAL_DMAEx_ChangeMemory(hdcmi->DMA_Handle, (tmp + (8U*hdcmi->XferSize)), MEMORY0);
      hdcmi->XferCount--;
    }
    /* Update memory 1 address location */
    else if((hdcmi->DMA_Handle->Instance->CR & DMA_SxCR_CT) == 0U)
    {
      tmp = hdcmi->DMA_Handle->Instance->M1AR;
      HAL_DMAEx_ChangeMemory(hdcmi->DMA_Handle, (tmp + (8U*hdcmi->XferSize)), MEMORY1);
      hdcmi->XferCount--;
    }
  }
.. 

So they check the DMA_SxCR register for CT (Current Target) bit. This bit indicates, that ongoing DMA process writes to MEMORY0 or MEMORY1 location. OK, that's fine, fine. But what the fuck with the (XferCont % 2U) check?!? This looks like a hack. A workaround for something. Removing it produces an interesting result - every OTHER image captured would be borked. So, I guess I've managed to reproduce original issue this workaround was for. But why is it happening? Since we don't have access to STM repositories with change logs, it's not that easy finding out reasoning behind it. Except heavy logic. And my logic is as follows - if you have a happy-path, where your image size is even number of DMA transfers, it's all good. If last transfer has been odd, then CT bit remains set. So you would write to second buffer twice (skipping 8*Xfersize bytes) and then to first. And if you are reusing the same DCMI configuration handle as before, it's still set and never gets cleared, unless by hardware. Manually clearing the CT bit either before or after capture fixes it. It probably should've been done in DMA capture start function, which it never is. And this explains why I had so many issues with finding my JPEG start marker before (sometimes it would be in first, sometimes in second buffer). 

Goddammit, ST!

No comments:

Post a Comment