2

I was reading a lot how to do the multichannel ADC readings. My first success was with poll for conversion. Then I managed to switch to ADC with DMA but I got stuck with my new goal. I have analog output from image sensor I want to read. I will not bother you with the full loop, but basically most important is the loops for the columns:

  1. Set the 8-bit address of the pixel column (8 GPIO pins with BSRR)
  2. wait 40ns before reading the analog output (time delay due to calculations, if necessary add some NOPs)
  3. read two outputs (simultaneously 2 pixels are read) and do some calculations with them

Doing it with channel selection and poll for conversion is extremely slow and I want to have several frames per sec (the more the better). I tried to do it in Scan mode, 2 channels, contconv disabled, DMA set to normal mode. I still see there is a big overhead due to ADC start each time. Best idea would be to read the whole row of 320 values using ADC+DMA, then stop the ADC, do some calcs and continue with next row. I guess a big issue is the fact I try to check it in the debug mode through Truestudio so any advice how to proceed optimally will be appreciated. First, some clarifying questions:

  1. Would it be correct to set ADCbuffer[320] for the DMA and write HAL_ADC_Start_DMA(&hadc1, ADCBuffer, 320) although I have 2 channels? I expect the ADC to loop the sequence of 2 channels in contconv mode, so DMA would fill the buffer. My idea is to put a check for the column number and once final one is reached to stop the ADC. If this is not feasible, I will stick to ADCbuffer[2] and two readings for the DMA.

While I try to debug the case with contconv ADC with 2 channels and circular DMA, I notice a number of problems (some of them may be due to debugger mode). By the way, System clock is 100MHz (4 WS for Flash for my processor), ADC works at 80MHz:

  1. Regardless of the integration time I chose, the time between two HAL_ADC_ConvHalfCpltCallback remains about 150 ticks. Even in the case 640.5 cycles I get 200 ticks. I find it strange, but it may be due to debug mode.
  2. While HAL_ADC_ConvHalfCpltCallback is regularly called and I can insert the bus change for next pixel, HAL_ADC_ConvCpltCallback is not called or called very rarely randomly. Any idea why would that happen?

Considering the timing issues, I am curious if there is a way to use ADC with DMA, disabled contconv, normal mode DMA, but with some fast ADC start (not HAL) or some interruption for ADC start. Thus, I can be sure I read 2 values, send command to the sensor to read next pixel and so on, but with less overhead. I think my code is set correctly at the moment, but I will post it in case someone can find an issue fixing some of the problems. I am using STM32L4R5ZI:

static void MX_ADC1_Init(void)
{


  ADC_ChannelConfTypeDef sConfig = {0};

  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 2;
  hadc1.Init.NbrOfDiscConversion = 0;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DMAContinuousRequests = ENABLE; 
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = 1;
  sConfig.SamplingTime = ADC_SAMPLETIME_12CYCLES_5; 
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_2;
  sConfig.Rank = 2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }
  HAL_NVIC_SetPriority(ADC1_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(ADC1_IRQn);
}


static void MX_DMA_Init(void)
{

  /* DMA controller clock enable */
  __HAL_RCC_DMAMUX1_CLK_ENABLE();
  __HAL_RCC_DMA2_CLK_ENABLE();
  __DMA2_CLK_ENABLE();

  /* DMA interrupt init */
  hdma_adc1.Instance = DMA2_Channel3;
  hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
  hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
  hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
  hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
  hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
  hdma_adc1.Init.Mode = DMA_CIRCULAR;
  hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH;
  //HAL_DMA_DeInit(&hdma_adc1);
  HAL_DMA_Init(&hdma_adc1);
  __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

  DMAMUX1_Channel9->CCR = 0x5;


  /* DMA interrupt init */
  /* DMA1_Channel1_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMA2_Channel3_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMA2_Channel3_IRQn);
  /* DMAMUX1_OVR_IRQn interrupt configuration */
  HAL_NVIC_SetPriority(DMAMUX1_OVR_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(DMAMUX1_OVR_IRQn);

}


//somewhere in main.c, the read out loop:
uint32_t ADCBuffer[2];

GPIOF -> BSRR = (col) | (((~col) & 0xFF) << (16)); //r_col case, F0:7
HAL_ADC_Start_DMA(&hadc1, ADCBuffer, 2);


//And the callbacks and handlers in STM32L4xx_it.c:

void DMA2_Channel3_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_adc1);
  HAL_ADC_IRQHandler(&hadc1);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    if(vCol==159){
        HAL_ADC_Stop_DMA(hadc);
    }
}

void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
    vCol++;
    GPIOF -> BSRR = (vCol) | (((~vCol) & 0xFF) << (16)); //r_col case, F0:7
    
    rsum = rsum + ADCBuffer[0] + ADCBuffer[1];
}

void ADC_IRQHandler()
{
    HAL_ADC_IRQHandler(&hadc1);
}

Any help and ideas how to proceed are greatly appreciated!

Clifford
  • 88,407
  • 13
  • 85
  • 165
D. K.
  • 63
  • 1
  • 5
  • 1
    It's pointless to trigger an interrupt for every pixel. The overhead for entering and leaving the interrupt and in particular the HAL overhead are too big. A row-wise approach is certainly the way to go. The approach could be to have fixed timing for setting the address and sampling the values. The sampling side could be achieved with a timer trigger the ADC and the ADC using DMA to store then entire row. I'm less familiar with a timer based mechanism to set the column address and how to synchronize the two parallel actions. – Codo Sep 02 '20 at 18:47
  • 1
    An alternative approach is to go low-level (direct register access). Write a loop that sets the column address, waits 50ns, starts the ADC, waits for the ADC to complete, saves the sampled value and starts over. With the proper configuration, the ADC takes 15 cycles (187.5ns @ 80MHz) for a single conversion to complete. So a pixel rate (without computation) of about 3.5 Mpixels/s seems achievable. – Codo Sep 02 '20 at 19:03
  • Yes, thank you for the comment. This is a feasible loop to proceed. I am reading the manual for my MCU and got some ideas. I will keep the configuration for ADC and DMA as done by HAL. In debugger registers look fine. BUT, I will set DMA in normal mode with 320 words for the whole col. As far as I got it from the manual, ADC start converting once CONT=1 for continuous conversion. Yet, DMA configured in Normal mode (called one-shot mode if I am not mixing things) will generate DMA_EOT interrupt at the end and I will use it to stop the ADC. – D. K. Sep 03 '20 at 13:12
  • For the column address I consider using EOSMP interrupt. So, after 2.5 (or 3, not sure how to count it) cycles, I will have interrupt that will have col++ and BSRR update for the remaining 12 cycles. I see BSRR update currently takes 9 cycles. col++ 5 cycles. Some if statement would be necessary not sure if it can happen. Best would be to make col++ and BSRR change on EOSMP interrupt during first channel reading with the idea to have the BSRR updated after second sampling is finished and there still will be some cycles for the delay before reading the next pixel.... – D. K. Sep 03 '20 at 13:19
  • I like the idea that the column address is already changed while the ADC is still working. However, I still think it's not feasible to trigger an interrupt for each pixel. Your calculation ignores the time needed to enter and leave the interrupt (saving and restoring all registers). A busy loop that runs until the flag in the ADC status register is set will be more efficient. – Codo Sep 03 '20 at 14:56
  • Thinking about some more I notice that your approach (ADC in continuous mode, regular code to increase the address) will only work if you turn off interrupts. Otherwise an interrupt (like systick) will interrupt your regular code, the ADC will continue to sample and you will miss a sample. As a result, the address counter and the ADC are out of sync... – Codo Sep 03 '20 at 17:32

0 Answers0