Особенности использования прерывания Half transfer при работе с DMA

Если обрабатывается большой массив данных например от АЦП, то удобно применить для этого прямой доступ к памяти(ПДП). С использованием ПДП данные складываются в массив и потом могут быть обработаны. Если необходим непрерывный сбор данных и их обработка, то сбор можно вести с использованием ПДП, а обрабатывать их в прерывании ПДП. Данные пишутся в буфер циклически. По заполнении данными больше половины буфера, выставляется Half Transfer Interrupt Flag (HTIFx), По заполнении всего буфера выставляется Transfer Complete Interrupt Flag (TCIFx). Прерывания генерируются по установке любого из этих флагов (если разрешены). В обработчике прерывания эти флаги могут быть прочитаны и выяснено, насколько заполнен буфер — наполовину или целиком. Если буфер заполнен наполовину — то обрабатывается первая половина буфера. Данные в это время пишутся во вторую половину. И наоборот. Таким образом можно осуществить непрерывную обработку данных.
Примерно такая идея была у меня когда я собирался усреднять результаты измерений АЦП. При отладке обнаружил, что прерывание вызывается не два раза на весь буфер, а чаще. После разбора причины оказалось, что флаг Half transfer выставляется, не один раз, а всякий раз, когда DMA канал работает со второй половиной буфера. Пришлось прямо в обработчике запрещать прерывания по HTIF после прихода первого из прерываний. Прерывания разрешаются снова в прерывании, возникшем по заполнении буфера (по флагу TCIF). Ниже — пример кода непрерывного усреднения по двум каналам в обработчике прерывания DMA. В моем случае усреднение проводилось по 2048 точкам каждого из каналов.

void DMAChannel1_IRQHandler(void) /* Read ADC values */
{
  if (DMA1->ISR & DMA_ISR_HTIF1 && /*   Half Transfer complete */
       (DMA1_Channel1->CCR & DMA_CCR1_HTIE) /* Half traisfer interrupt enabled */
      ) 
  {
    int i;
    /* HTIF is set every time when transfer interrupt > half !!! */
    DMA1_Channel1->CCR &= ~DMA_CCR1_HTIE; /* disable Half traisfer interrupt !!!! */

    for (i = 0; i < ADC_ARRAY_SIZE/2; i = i+2)
    {
      ADCVoltageSum += AdcOutArray[i];
      ADCCurrentSum += AdcOutArray[i+1];
    }
  } else if (DMA1->ISR & DMA_ISR_TCIF1 ) /* transfer complete */
  {
    int i;

    /* transfer complete */
    DMA1->IFCR |= DMA_IFCR_CGIF1; /* Clear all interrupt flags */
    DMA1_Channel1->CCR |= DMA_CCR1_HTIE; /* enable Half traisfer interrupt  */

    for (i=ADC_ARRAY_SIZE/2; i< ADC_ARRAY_SIZE; i=i+2)
    {
      ADCVoltageSum += AdcOutArray[i];
      ADCCurrentSum += AdcOutArray[i+1];
    }

    DMACounter++;
    if (DMACounter == 16) /* 2Mhz/195/ADC_ARRAY_SIZE*2/16 - refresh frenq = 5 Hz */
    {
      DMACounter = 0;
      ADCVoltage = ADCVoltageSum;
      ADCCurrent = ADCCurrentSum;
      ADCVoltageSum = 0;
      ADCCurrentSum = 0;
      EndFlag = 1;
    }
  }
  DMA1->IFCR |= DMA_IFCR_CGIF1; /* Clear all interrupt flags */
}

p.s. Этот код я проверял в симуляторе Keil, потом перенес без изменений в железо. Так что может быть повторное прерывание — это особенность симулятора.

Вот код проекта radiokot.ru/circuit/power/supply/22/01.rar, где все это применилось
  • +3
  • 12 мая 2011, 13:41
  • OlegG

Комментарии (24)

RSS свернуть / развернуть
все гораздо проще:

void DMA1_Channel1_Handler(void)
{
if (DMA_GetITStatus(DMA1_IT_HT1))
{
обрабытываем первую половину
}
if (DMA_GetITStatus(DMA1_IT_TC1))
{
обрабатываем вторую половину
}

DMA1->IFCR = DMA1_IT_GL1;
}

и никаких повторных вхождений.
0
  • avatar
  • ZiB
  • 12 мая 2011, 14:19
чет я запарился, если на основе либ от стм, то нужно сбрасывать вот так
DMA_ClearITPendingBit(DMA1_IT_GL1);

обратите внимание на различие между режимами пересылки
память-периферия
периферия-память
память-память

они влияют время вызова прерывания.
0
точнее не на время, а на момент вызова.
0
Ну и слава богу что у Вас так работает.
-1
ну почему же у меня, у всех так работает :)
+1
ваша ошибка в том что вы «читаете» регистр IFCR, а он доступен только для записи, т.е. нужно просто писать нужный бит для сброса соотв флага.
0
т.е. Вместо DMA1->IFCR |= DMA_IFCR_CGIF1;
нужно писать
DMA1->IFCR = DMA_IFCR_CGIF1;
Я предполагаю, что код отлаживался в симуляторе keil и это его особенность.
0
не много не поля, про отладку…
я пишу на gcc, keil-ом ни разу не пользовался.
приведенный фрагмент это рабочий код.

кстати заметил одну особенность в работе АЦП при запуске от таймера. при нечетных значениях деления модуль ацп пропускает «старт» от таймера (зависит от момента разрешения тактирования модуля АЦП).
0
у меня аналогичная задача, цифрую два канала и обрабатываю
вот такой обработчик:

void DMA1_Channel1_Handler(void)
{
if (DMA_GetITStatus(DMA1_IT_HT1))
{
data_filter(&adc_buffer[0]);
}
if (DMA_GetITStatus(DMA1_IT_TC1))
{
data_filter(&adc_buffer[ARRAY_LENGHT(adc_buffer) / 2]);
}

DMA_ClearITPendingBit(DMA1_IT_GL1);
}
0
я сам только учусь :)

тут не понятно как более правильно делать,
можно ещё вот так обрабатывать:

if (DMA_GetITStatus(DMA1_IT_HT1))
{
DMA_ClearITPendingBit(DMA1_IT_HT1);
data_filter(&adc_buffer[0]);
}
if (DMA_GetITStatus(DMA1_IT_TC1))
{
DMA_ClearITPendingBit(DMA1_IT_TC1);
data_filter(&adc_buffer[ARRAY_LENGHT(adc_buffer) / 2]);
}
0
Я тоже учусь. Отличие кода во времени сброса флага прерывания. Во втором варианте еще не сбрасывается флаг ошибки. Если данные гарантированно успевают обрабатыватсья — то оба варианта должны быть эквивалентны. Второй вариант может допустить большую задержку в обработке — оба флага сохранятся и этот обработчик вызовется 2 раза после другого долгого прерывания.
0
это понятно, пока у меня нет обработки ошибки поэтому и не проверяю его, так как прерывание по ошибке запрещено.
0
А DMA во время работы полностью шину занимает? Есть возможность для дальнейшего исполнения программы?
0
На 1/3 — 1/4 примерно. Ради этого все и затевалось.
0
Назрел вопрос. Кратко и по пунктам для лучшего понимания:
1. пытаюсь сохранять данные из 2 каналов АЦП в массиве из двух элементов.
2. преобразования АЦП запускаются по таймеру
3. первый канал ДМА настроен на передачу данных а АЦП в этот самый массив из 2 элементов
4. каким либо образом я читаю массивчик и смотрю что же там наацепировалось

теперь хочу уточнить некоторые места логики работы АЦП в связке с ДМА:
если настроено
-одиночное регулярное преобразование АЦП (Multichannel (scan), single conversion mode по терминологии AN3116 )
-выбраны два канала
-число преобразований — два
-в регистры последовательности забиты нужные номера каналов для 1ого и 2ого преобразования
-дма настроен на получение 2 байтов, циклическая передача, указаны адреса источника и приемника.

// если выбрать только один канал АЦП, то все работает

Я правильно считаю что, как только установится флаг конца преобразования ДМА положит значение из регистра данных АЦП в первый элемент массива, затем (раз выбрано число преобразований =2) произойдет преобразование второго выбранного канала АЦП и, по флагу окончания его преобразования, АПЦ положит во 2ой элемент массива значение из регистра данных АЦП, где к тому времени уже будет лежать данные из второго выбранного канала. Затем АЦП останавливается и ждет сигнал от таймера чтобы снова начать преобразования?
Так должно быть? Или я не догоняю как работает АЦП+ДМА в многоканальном режиме? У меня всегда в массиве лежат значения из первого канала. Даже если выбираю хоть 1, хоть 2, хоть 4 преобразования.
0
Не хотел выкладывать код, т.к. у меня один проект и там куча всего-всего наинициализировано. Выложу отдельно функции инициализации АЦП и 1канала ДМА. Надеюсь ниче лишнего, кроме некоторых закомменченных строк не похерил.

void ACD1_init(void)
  { 
    // channel 16 - temp sensor
    RCC->APB2ENR |=RCC_APB2ENR_ADC1EN;// enable adc1 clock
//  ADC1->CR2   |= ADC_CR2_CONT;      // continuous conversion
    
    ADC1->CR2   |= ADC_CR2_TSVREFE;   // temp sensor & Vref enable
    ADC1->CR2   |= ADC_CR2_ADON;      // enable adc1    
  
    // 101: Timer 4 CC4 event
    ADC1->CR2&=~ADC_CR2_EXTSEL_1; ADC1->CR2|= ADC_CR2_EXTSEL_2 |ADC_CR2_EXTSEL_0;
   
    ADC1->CR2   |= ADC_CR2_EXTTRIG;   // ext triggering en
    ADC1->SQR1  &=~ADC_SQR1_L;        // conversions quantity - 1
    ADC1->SQR3  |= ADC_SQR3_SQ1_4;    // 1conv: 16th channal (0b10000) - temp sensor
    ADC1->SQR3  |= ADC_SQR3_SQ2_3|ADC_SQR3_SQ2_1|ADC_SQR3_SQ2_0;  // 2conv: 11th channal (0b01011) pc1
    ADC1->SMPR1 |= ADC_SMPR1_SMP16_1; // set 71.5 cycles of conversion on 16th ch
    ADC1->SMPR1 |= ADC_SMPR1_SMP16_2;
    ADC1->SMPR1 |= ADC_SMPR1_SMP11_1; // set 71.5 cycles of conversion on 11th ch
    ADC1->SMPR1 |= ADC_SMPR1_SMP11_2;    
    ADC1->CR2 |= ADC_CR2_DMA; //dma request en
  
  }



//DMA for ADC1
void DMA1ch1_init (void) 
 {  DMA1_Channel1->CCR &=~ DMA_CCR1_EN; //turn  DMA1 ch1 off
    RCC->AHBENR |= RCC_AHBENR_DMA1EN; 
    DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;  // 8 bit data to array
    DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;  // 8 bit data from adc 
    DMA1_Channel1->CCR |=  DMA_CCR1_CIRC;   // circular mode on
    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;    // read from peripheral
    //DMA1_Channel1->CCR |= DMA_CCR1_PINC;  // periph inc
    DMA1_Channel1->CCR |=  DMA_CCR1_MINC;   // memory inc
    DMA1_Channel1->CCR &= ~DMA_CCR1_PL;     // low priority
    DMA1_Channel1->CNDTR = 2;               // data quantity
    DMA1_Channel1->CPAR  = (uint32_t)&ADC1->DR; 
    DMA1_Channel1->CMAR  = (uint32_t)&adc_bytes; 
    DMA1_Channel1->CCR  |= DMA_CCR1_EN;     // turn  DMA1 ch1 on
 }

ну и таймер4 еще, который дергает АЦП

void TIM4_init(void)
 {
  RCC->APB1ENR |= RCC_APB1ENR_TIM4EN;    
  TIM4->ARR = 16000;   
  TIM4->PSC = 1000;
  //TIM4->DIER |= TIM_DIER_UIE; 
  TIM4->CCR4  = 8000; 
  TIM4->CCMR2&= ~TIM_CCMR2_CC4S; // capture/compare select - compare
  TIM4->CCMR2&=~TIM_CCMR2_OC4M_0; // PWM mode on channel4
  TIM4->CCMR2|= TIM_CCMR2_OC4M_1;
  TIM4->CCMR2|= TIM_CCMR2_OC4M_2;
  TIM4->CCMR2|= TIM_CCMR2_OC4PE;
  TIM4->CCER |= TIM_CCER_CC4E; // c/c enable register: cc4e - enable , cc4p-polarity
  TIM4->CR1  |= TIM_CR1_CEN;   
  TIM4->CR1  |= TIM_CR1_ARPE; 
 }
0
Вы правильно понимаете как должно работать.
Код как делал я можно найти на радиокоте radiokot.ru/circuit/power/supply/22/01.rar.
Да хотел еще сказать, что массив из 2 элементов использовать не имеет смысла. Есть инжектированные каналы. С попощью их можно делать до 5 преобразований за раз. А потом просто читать данные из 4 регистров инжектированных каналов и 1 обычного.
0
Да хотел еще сказать, что массив из 2 элементов использовать не имеет смысла. Есть инжектированные каналы. С попощью их можно делать до 5 преобразований за раз.
А я специально так, для тренировки). Спасибо за совет.
0
Посмотрел AN3116 STM32™’s ADC modes and their applications, в нем принцип тот же, только АЦП настроен на непрерывное преобразование и запускается один раз при инициации + на все либах сделано. Так и не пойму в чем косяк. Может кто-нибудь хоть наводки какие-нибудь даст или пнёт в нужном направлении?
0
Вот 6 строк моего кода инициализации из той самой ссылки что я вам дал

  ADC1->CR1 |= 
                ADC_CR1_JAUTO|ADC_CR1_SCAN; /* Scan mode + auto injection */
  ADC1->SMPR2 = ADC_SMPR2_SMP7_0|ADC_SMPR2_SMP8_0|ADC_SMPR2_SMP9_0; /* 7.5 sampl
  ADC1->SQR1 =  ADC_SQR1_L_0; /* 2 conversion */^
  ADC1->SQR3 =  ADC_SQR3_SQ1_3| /* 8 channel - voltage*/^
                ADC_SQR3_SQ2_3|ADC_SQR3_SQ2_0; /* 9- channel - current */^
  ADC1->JSQR = ADC_JSQR_JSQ4_2|ADC_JSQR_JSQ4_1|ADC_JSQR_JSQ4_0; /* 1 conversion
  ADC1->CR2 |= ADC_CR2_EXTTRIG|ADC_CR2_DMA; /* External trigger by T1 CC1, DMA *

Включение 2 преобразований за раз отмечено соответствующим комментарием. У вас этот код отсутвует.
0
Надеюсь ниче лишнего, кроме некоторых закомменченных строк не похерил.
Включение 2 преобразований за раз отмечено соответствующим комментарием. У вас этот код отсутвует
Как раз ее и похерил при вставке в комент, видимо когда вставлял, она была закомменчена у меня. Но косяк не в этом был: код ващ просмотрел и заметил я в этом же самом месте вот эту строчку
ADC_CR1_JAUTO|ADC_CR1_SCAN; /* Scan mode + auto injection */

и сразу стало все на свои места. Прописал
ADC1->CR1 |= ADC_CR1_SCAN;
и все заработало. Спасибо за пример. Сберегли мне кучу нервов)
0
Код как делал я можно найти на радиокоте radiokot.ru/circuit/power/supply/22/01.rar

У меня первая мысль была тоже о БП, когда я увидел встроенный ЦАП
0
А этот код я так понимаю очищает половину буфера?
DMACounter++;
    if (DMACounter == 16) /* 2Mhz/195/ADC_ARRAY_SIZE*2/16 - refresh frenq = 5 Hz */
    {
      DMACounter = 0;
      ADCVoltage = ADCVoltageSum;
      ADCCurrent = ADCCurrentSum;
      ADCVoltageSum = 0;
      ADCCurrentSum = 0;
      EndFlag = 1;
    }
  }
  DMA1->IFCR |= DMA_IFCR_CGIF1; /* Clear all interrupt flags */
}
0

Еще один полубуфер обработан
Если посчитали уже 16 полубуферов то
  Обнулить счетчик полубуферов
  Сохранить накопленные суммы
  Обнулить сумматоры
  Выставить флаг готовности результата
Очистить флаг прервывания DMA 
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.