Содержание

Работа DMA c каналом таймера в режиме захвата сигнала от ШИМ, 1986ВЕ91Т

В этом примере рассмотрим работу таймеров в режиме генерации сигнала ШИМ и в режиме Захвата с применением DMA. ШИМ (Широтно-Импульсная Модуляция) - простым языком, это вывод сигнала в виде меандра. Захват - это фиксация счетчика таймера (CNT) при обнаружении фронтов и/или спадов входного меандра. Подробная статья про таймеры представлена в разделе справка - "Таймеры общего назначения".

Напомню, что каждый таймер общего назначения состоит из счетчика, который отсчитывает временные отсчеты (периоды времени) и 4-х каналов. Каждый канал имеет два вывода GPIO (прямой и инверсный) через которые может выводить наружу меандр, либо (только с прямого вывода) может отслеживать переключение уровней входного сигнала - т.е. захватывать события изменения логических уровней "0" и "1".

Захват используется, например, для определения периода входного сигнала. В данном примере для этого несколько раз измеряется количество отсчетов таймера CNT между соседними фронтами входного сигнала. При обнаружении фронта на входе канала Х, таймер аппаратно заносит текущее значение CNT в регистр CCRх. Далее, эти "захваченные" значения из CCRх необходимо куда-то скопировать для последующего анализа. Для этих целей отлично подходит DMA.

Итак, пример работает следующим образом:

Регулируя кнопками период ШИМ мы определим какой минимальный период сигнала может быть захвачен.

Полностью пример доступен здесь - GitHub

Подключение на плате и конфигурация выводов

Как уже было сказано, сигнал с канала ШИМ необходимо завести на вход канала захвата. Находим на демо-плате 1986ВЕ91Т выводы PF7 и PA9, соединяем. Выводы находятся на мезонином разъеме.

1986ve91_timcap.jpg

При кодировании я придерживаюсь подхода в разделении данных и методов. В одном месте описывается вся конфигурация (заводится как-бы объект) периферийного блока, а для работы с этой конфигурацией реализованы методы, в которые этот объект конфигурации передается входным параметром. Поэтому весь код сводится к тому, что задается конфигурация, а затем функциями она применяется в аппаратуру.

Настройка GPIO вывода ШИМ

  // Настройка структуры по умолчанию под ШИМ
  PORT_InitTypeDef pinTimerPWM =
  {
    .PORT_Pin       = 0,
    .PORT_OE        = PORT_OE_OUT,         // ВЫХОД
    .PORT_PULL_UP   = PORT_PULL_UP_OFF,    // Без подтяжек
    .PORT_PULL_DOWN = PORT_PULL_DOWN_OFF,
    .PORT_PD_SHM    = PORT_PD_SHM_OFF,     // Триггер Шмитта выключен
    .PORT_PD        = PORT_PD_DRIVER,      // Драйвер
    .PORT_GFEN      = PORT_GFEN_OFF,       // Фильтр выключен
    .PORT_FUNC      = PORT_FUNC_MAIN,      // Функция переопределяется в brdPort_Obj 
    .PORT_SPEED     = PORT_SPEED_MAXFAST,  // Максимальная скорость переключения вывода
    .PORT_MODE      = PORT_MODE_DIGITAL    // Цифровой режим
  };

  //  Выбор PF7 - TMR3_CH1, выход сигнала ШИМ
  brdPort_Obj Port_TimerPinPWM = 
  {
    .PORTx          = MDR_PORTF,
    .Port_ClockMask = RST_CLK_PCLK_PORTF,
    .Port_PinsSel   = PORT_Pin_7,
    .Port_PinsFunc  = PORT_FUNC_OVERRID,
    .Port_PinsFunc_ClearMask = 0,                     //  Здесь не используется
    .pInitStruct    = &pinTimerPWM                    //  Настройки пина для ШИМ по умолчанию
  };
  
  //  Настройка пина GPIO на вывод от канала таймера ШИМ
  BRD_Port_Init(&Port_TimerPinPWM);  

Настройка GPIO вывода Захвата

  // Настройка структуры по умолчанию под Захват
  PORT_InitTypeDef pinTimerCAP =
  {
    .PORT_Pin       = 0,
    .PORT_OE        = PORT_OE_IN,          // ВХОД
    .PORT_PULL_UP   = PORT_PULL_UP_OFF,
    .PORT_PULL_DOWN = PORT_PULL_DOWN_ON,   // ПОДТЯЖКА к земле
    .PORT_PD_SHM    = PORT_PD_SHM_OFF,     // Триггер Шмитта выключен
    .PORT_PD        = PORT_PD_DRIVER,      // Драйвер
    .PORT_GFEN      = PORT_GFEN_OFF,       // Фильтр выключен
    .PORT_FUNC      = PORT_FUNC_MAIN,      // Функция переопределяется в brdPort_Obj 
    .PORT_SPEED     = PORT_SPEED_MAXFAST,  // Максимальная скорость переключения вывода
    .PORT_MODE      = PORT_MODE_DIGITAL    // Цифровой режим
  };

  //  Вход PA9 - TMR1_CH4, вход захвата для подачи сигнала от ШИМ
  brdPort_Obj Port_TimerPinCAP = 
  {
    .PORTx          = MDR_PORTA,
    .Port_ClockMask = RST_CLK_PCLK_PORTA,
    .Port_PinsSel   = PORT_Pin_9,
    .Port_PinsFunc  = PORT_FUNC_ALTER,
    .Port_PinsFunc_ClearMask = 0,                     //  Здесь не используется
    .pInitStruct    = &pinTimerCAP                    //  Настройки пина для захвата по умолчанию
  };
  
  //  Настройка пина GPIO на вход сигнала для канала таймера захвата
  BRD_Port_Init(&Port_TimerPinCAP);  

В настройке входа захвата я включил внутреннюю подтяжку к земле. Если перемычку на плате снять, то вход получится висящим в воздухе и будет ловить всякие наводки. Подтяжка к земле не дает выводу болтаться и не приводит в ложным срабатываниям захвата. Даже когда пины соединены, субъективно, наличие подтяжки сделало работу захвата более стабильным.

Поскольку мы хотим узнать минимально возможный период захвата, то переключение фронтов я задаю максимально быстрым.

Настройка таймеров на счет

Оба таймера настраиваем на одну временную шкалу, разница будет только в настройке периода. В таймере ШИМ выставляется период выводимого меандра (регистр ARR) и ширина импульса (CCR). CCR в режиме ШИМ задает "время" переключения уровня сигнала, т.е. при CNT==CCR и CNT==ARR выходной сигнал меняет логический уровень.

В режиме захвата период таймера ставим максимальным, потому что не известно заранее когда возникнет фронт на входе. В регистре CCR при этом сохранится значение CNT в момент фиксации фронта.

Выбор таймеров происходит в конфигурационных глобальных структурах:

Таймер ШИМ

  //  Выбор TIMER3 и настройки для его инициализации функцией BRD_Timer_Init()
  Timer_Obj brdTimerPWM =
  {
    .TIMERx    = MDR_TIMER3,
    .ClockMask = RST_CLK_PCLK_TIMER3,
    .ClockBRG  = TIMER_HCLKdiv1,
    .EventIT   = TIMER_STATUS_CNT_ARR,
    .IRQn      = Timer3_IRQn
  };

  //  Выбор канала таймера для вывода ШИМ
  #define TIM_CHANNEL_PWM     TIMER_CHANNEL1          //  1-й канал
  #define TIM_PWM_PERIOD      20                      //  Начальный период ШИМ по умолчанию
  #define TIM_PWM_WIDTH       7                       //  Длительность импульса ШИМ

Таймер захвата

//  Выбор TIMER1 и настройки для его инициализации функцией BRD_Timer_Init()
Timer_Obj brdTimerCap =
{
  .TIMERx    = MDR_TIMER1,
  .ClockMask = RST_CLK_PCLK_TIMER1,
  .ClockBRG  = TIMER_HCLKdiv1,
  .EventIT   = TIMER_STATUS_CNT_ARR,
  .IRQn      = Timer1_IRQn
};

//  Выбор канала таймера захвата
#define TIM_CHANNEL_CAP     TIMER_CHANNEL4            //  4-й канал
#define TIM_REG_CCR         CCR4                      //  CCR4 - Регистр захвата 4-го канала, отсюда DMA читает данные
#define TIM_DMA_SREQ_CCR    TIMER_STATUS_CCR_CAP_CH4  //  Событие захвата в CCR4 генерирует запрос к DMA
#define DMA_CHANNEL_CAP     DMA_Channel_TIM1          //  Канал DMA обслуживающий TIMER1
#define DMA_IRQ_PRIORITY    1                         //  Приоритет прерывания DMA

Инициализация счетчиков

Функции применения в аппаратуру.

  // Заполнение структуры счетчика таймера по умолчанию
  TIMER_CntInitTypeDef    TimerInitStruct;
  TIMER_CntStructInit (&TimerInitStruct);
  
  //  Настройка счетчика таймера ШИМ
  TimerInitStruct.TIMER_Period = capPeriodPWM;  
  BRD_Timer_Init(&brdTimerPWM, &TimerInitStruct);
  //  Выставление скважности ШИМ
  TIMER_SetChnCompare (brdTimerPWM.TIMERx , TIM_CHANNEL_PWM, TIM_PWM_WIDTH);  
  
  //  Настройка счетчика таймера захвата
  TimerInitStruct.TIMER_Period     = 0xFFFF;  //  Max period
  BRD_Timer_Init(&brdTimerCap, &TimerInitStruct);  

Настройка каналов на ШИМ и Захват

Счетчик таймера настроили и выводы GPIO тоже, осталось настроить каналы таймеров на необходимый режим работы. Для этого необходимо заполнить две структуры:

  1. Структура канала (TIMER_ChnInitTypeDef) - режим ШИМ или Захват.
  2. Структура вывода канала (TIMER_ChnOutInitTypeDef) - конфигурация работы внешнего пина.

Собственно, настройка сводится к заполнению этих структур и вызову функции Apply - BRD_TimerChannel_Apply().

Канал ШИМ

  //  Настройка канала таймера на вывод ШИМ
  BRD_TimerChannel_InitStructPWM(TIM_CHANNEL_PWM, &TimerChanCfg, &TimerChanOutCfg);
  BRD_TimerChannel_Apply(brdTimerPWM.TIMERx, &TimerChanCfg, &TimerChanOutCfg);

где, основные параметры структуры канала на ШИМ настраиваются так:

  // Настройка структур канала таймера на ШИМ по умолчанию
  void BRD_TimerChannel_InitStructPWM(uint16_t channel, TIMER_ChnInitTypeDef* 
                                      pChanCfg, TIMER_ChnOutInitTypeDef* pChanOutCfg)
  {
    //  Настройки PWM
    pChanCfg->TIMER_CH_Number           = channel;
    pChanCfg->TIMER_CH_Mode             = TIMER_CH_MODE_PWM;                // ШИМ
    pChanCfg->TIMER_CH_REF_Format       = TIMER_CH_REF_Format6;             // Режим формирования Ref

    // Настройки прямого вывода канала
    pChanOutCfg->TIMER_CH_Number            = channel;  
    pChanOutCfg->TIMER_CH_DirOut_Source     = TIMER_CH_OutSrc_REF;          //  Выводить сигнал Ref
    pChanOutCfg->TIMER_CH_DirOut_Mode       = TIMER_CH_OutMode_Output;      //  ВЫХОД
    pChanOutCfg->TIMER_CH_DirOut_Polarity   = TIMER_CHOPolarity_NonInverted;      
    
    ...
  } 

Канал захвата

  //  Настройка канала таймера на захват внешнего сигнала (от ШИМ)
  BRD_TimerChannel_InitStructCAP(TIM_CHANNEL_CAP, &TimerChanCfg, &TimerChanOutCfg);
  BRD_TimerChannel_Apply(brdTimerCap.TIMERx, &TimerChanCfg, &TimerChanOutCfg);

где, основные параметры структуры канала на захват настраиваются так:

  // Настройка структур канала таймера на Захват по умолчанию
  void BRD_TimerChannel_InitStructCAP(uint16_t channel, TIMER_ChnInitTypeDef* pChanCfg, 
                                      TIMER_ChnOutInitTypeDef* pChanOutCfg)
  {
    //  Основные настройки захвата
    pChanCfg->TIMER_CH_Number           = channel;
    pChanCfg->TIMER_CH_Mode             = TIMER_CH_MODE_CAPTURE;            // Захват
    pChanCfg->TIMER_CH_FilterConf       = TIMER_Filter_1FF_at_TIMER_CLK;    // Фильтр события
    pChanCfg->TIMER_CH_CCR_UpdateMode   = TIMER_CH_CCR_Update_Immediately;
    
    // Настройки прямого вывода канала
    pChanOutCfg->TIMER_CH_DirOut_Source     = TIMER_CH_OutSrc_Only_0;
    pChanOutCfg->TIMER_CH_DirOut_Mode       = TIMER_CH_OutMode_Input;       // ВХОД
    pChanOutCfg->TIMER_CH_DirOut_Polarity   = TIMER_CHOPolarity_NonInverted;
    
    ... 
  }

Настройка DMA и запуск

Настройка DMA стандартна, все аналогично тому, что делалось в прошлых проектах.

  #define TIM_REG_CCR       CCR4                      //  CCR4 - Регистр захвата 4-го канала
  #define TIM_DMA_SREQ_CCR  TIMER_STATUS_CCR_CAP_CH4  //  Событие захвата в CCR4 генерирует запрос к DMA  
  #define DMA_CHANNEL_CAP   DMA_Channel_TIM1          //  Канал DMA обслуживающий TIMER1  

  //  Настройка канала DMA для передачи данных от регистра захвата - TaskDefs.h
  BRD_DMA_Init();
  DMA_DataCtrl_Pri.DMA_SourceBaseAddr = (uint32_t)&brdTimerCap.TIMERx->TIM_REG_CCR;
  DMA_DataCtrl_Pri.DMA_DestBaseAddr   = (uint32_t)&arrDataCCR;
  DMA_DataCtrl_Pri.DMA_CycleSize      = DATA_COUNT;
  BRD_DMA_Init_Channel(DMA_CHANNEL_CAP, &DMA_ChanCtrl);
  
  BRD_DMA_InitIRQ(DMA_IRQ_PRIORITY);
  
  //  Сохранение управляющего слова канала DMA, для следующих перезапусков
  dmaChanCtrlStart = BRD_DMA_Read_ChannelCtrl(DMA_CHANNEL_CAP);
  
  //  Разрешение запросов sreq к DMA по событию захвата фронта сигнала на входе канала таймера
  TIMER_DMACmd (brdTimerCap.TIMERx, TIM_DMA_SREQ_CCR, ENABLE);

  //  Запуск таймеров. 
  //    Вывод ШИМ выдает сигнал на вход для захватаPA9.
  //    DMA сохраняет отсчеты захвата в массив arrDataCCR.
  //    По окончании цикла DMA возникает прерывание - DMA_IRQHandler(), 
  //    в котором выставляется запрос на обработку данных DoCheckResult = 1.
  BRD_Timer_Start(&brdTimerPWM);
  BRD_Timer_Start(&brdTimerCap);  

Напомню, что управляющее слово необходимо нам для быстрого перезапуска канала DMA на новый цикл. Ведь только это слово меняется в управляющей структуре при окончании цикла - в нем выставляется режим Stop и сбрасывается количество передач.

По окончании настройки DMA разрешается генерация запросов к DMA от канала таймера захвата по событию обновления регистра CCR4. После этого оба таймера запускаются и начинается серия измерений, по окончании которой возникнет прерывание DMA.

Цикл измерений частоты

Чтобы не раздувать статью кодом, основной цикл описывать не буду, в исходных кодах на GitHub думаю все понятно. Опишу только рабочую часть - запуск измерения и остановку в прерывании DMA, поскольку именно с этим возникли некоторые сложности.

После инициализации и запуска таймеров, DMA выполнит заказанное количество передач в цикле и сгенерирует прерывание.

void DMA_IRQHandler (void)
{
  //  Запрет запросов от событий захвата к каналу DMA
  TIMER_DMACmd (brdTimerCap.TIMERx, TIM_DMA_SREQ_CCR, DISABLE);
  
  //  Переинициализация следующего цикла DMA:
  //    На момент вызова TimerCap_CallDMAEna(DISABLE) со стороны таймера стоит запрос к DMA на передачу из CCR.
  //    После запрета формирования запросов от таймера к DMA, новые не будут формируются, 
  //    но текущий сигнал запроса остается активным.
  //    При запуске нового цикла DMA, запрос сразу же отрабатывается и 
  //    первое значение в массиве arrDataCCR заполняется текущим значеним CCR.
  //    Это значение нельзя рассматривать для вычисления периода - поэтому passDataCap = 1.
  BRD_DMA_Write_ChannelCtrl(DMA_CHANNEL_CAP, dmaChanCtrlStart);
  DMA_Cmd(DMA_CHANNEL_CAP, ENABLE);
  
  //  !!! - Здесь происходит перезапись значения arrDataCCR[0] = CCRx - !!!
  //  При этом снимается активный запрос к DMA и из прерывания выход происходит.
  
  // Если не разрешить следующий цикл DMA, то выйти из прерывания не удастся, 
  // даже если замаскировать и выключить канал! 
  // Висящий запрос на обработку будет постоянно генерировать прерывание!
  // Спецификация подобное описывает как "запросы к ядру от запрещенных каналов" - правила DMA 19-21.
  // Без переинициализации цикла, 
  //  в случае 1986ВЕ9х выйти из прерывания помогает запрет обработки одиночных запросов:
  //    MDR_DMA->CHNL_USEBURST_SET |= 1 << DMA_CHANNEL_CAP;  
  //  В 1986ВЕ1Т такой подход не помог.
  
  //  Выставление флага на обработку результатов
  DoCheckResult = 1;

  // Сброс возможных отложенных прерываний
  NVIC_ClearPendingIRQ (DMA_IRQn); 
}

Здесь стандартный прием не сработал. Обычно достаточно отключить в периферийном блоке разрешение на доступ к DMA - TIMER_DMACmd(…, DISABLE). Можно даже замаскировать канал в регистре CHNL_REQ_MASK_SET, чтобы он не работал. Но в данном случае это не помогло. Прерывание генерируется снова и снова. Полагаю, что это связано с тем, что последний запрос к DMA от таймера остался активным, при выключении формирования запросов. Далее видимо сказываются (оставшиеся для меня не понятными) правила из спецификации:

Правила осуществления DMA передач при «запрещенных» каналах

Правило Описание
19 Если dma_req[C] установлен в 1, то контроллер устанавливает dma_done[C] в 1. Это позволяет контроллеру показать центральному процессору запрос готовности, даже если канал выключен (запрещен)
20 Если dma_sreq[C] установлен в 1, то контроллер устанавливает dma_done[C] в 1 при условии dma_waitonreq[C] в 1 и chnl_useburst_set[C] в состоянии 0. Это позволяет контроллеру показать центральному процессору запрос готовности, даже если канал выключен (запрещен)
21 dma_active[C] всегда удерживается в состоянии 0

Инициализация следующего цикла приводит к тому, что DMA отрабатывает этот висящий запрос и выход из прерывания происходит успешно. (То, что запрос остается активным подтверждает и предыдущий вариант данного проекта, когда для прекращения прерываний помог запрет обрабатывать одиночные запросы sreq в DMA через регистр CHNL_USEBURST_SET.)

Далее по коду выставляется флаг DoCheckResult и происходит выход из обработчика прерывания. По выставленному флагу в основном цикле обрабатываются полученные данные и результат выводится на LCD экран. Затем выдерживается пауза, чтобы экран LCD не обновлялся слишком часто, и производится запуск следующего цикла измерения частоты входного сигнала.

int main(void)
{
  ...
  while (1)
  {
    ...
    //  Очистка предыдущих данных, массив заполняется значением 3.
    ClearCaptureData(3);

    //  Сброс счетчика, чтобы не обрабатывать ситуацию переполнения счета с 0xFFFF в 0x0000
    TIMER_SetCounter(brdTimerCap.TIMERx, 0);
        
    //  Разрешение запросов от событий захвата к каналу DMA
    TimerCap_CallDMAEna(ENABLE);
  }

Предварительно очищаем предыдущие данные и счетчик таймера захвата сбрасываем в 0. Ведь когда регистр CNT досчитает до ARR, значение CNT сбросится в 0 и продолжит увеличиваться дальше. Соответственно после сброса CNT в 0, значение в регистре захвата CCR окажется меньше предыдущего значения в массиве, а дельты (периоды сигнала) мы вычисляем простым вычитанием. Период получится отрицательным и надо будет как-то это обрабатывать. Но в данном примере мы ищем минимальный период ШИМ и регистр CNT заведомо не успеет досчитать до ARR, при сбросе CNT в 0 при каждом запуске. Поэтому мы и используем сброс.

Напоследок, разрешаем запросы к DMA от блока таймера и цикл измерения начинается вновь.

Находим минимальный период захвата

Для регулировки периода ШИМ в проекте используются кнопки

По умолчанию период ШИМ задан в 20 тиков частоты таймера, при длительности импульса в 3 тика. Регулировать период необходимо наблюдая за данными на LCD экране. На нем отображается следующая информация:

По умолчанию, при расчете периода мы пропускаем первое, не валидное значение в массиве, поскольку оно перезаписывается в прерывании DMA. Это значение отражается на экране в поле Err. Можно убрать этот пропуск кнопками выставив Offs = 0 и убедиться что период при этом становится неправильным.

Для того чтобы пропустить в расчете периода первые несколько значений используются кнопки

Для полной ясности приведу код подсчета периода, он самый простой:

  uint32_t CalcPeriod(uint32_t passOffset, uint32_t* errCnt)
  {
    uint16_t i;
    uint32_t sum;
	
    *errCnt = 0;
    sum = 0;
    for (i = passOffset; i < (DATA_COUNT - 1); ++i)
    {
     arrPeriod[i] = arrDataCCR[i+1] - arrDataCCR[i];
     sum = sum + arrPeriod[i];
        
      if (arrPeriod[i] == 0)
        ++(*errCnt);
    }
   
    return sum / (DATA_COUNT - passOffset - 1);
  }

При запуске примера все работает по умолчанию и на экран выводится текущий период ШИМ Pw = 20 и измеренный период Pс = 20. Далее нажимаем кнопку Down и смотрим как ведут себя индикаторы. В итоге удалось добраться до значений периода ШИМ в 11 тактов TIM_CLK.

timer_capmin.jpg

Это не полноценный тест, а лишь оценочный, поэтому результаты могут быть и иными, если сделать что-то по другому. Полученное значение в 11 тактов вероятно связано с картинкой из спецификации про такты обработки импульсного запроса. Обработка одного запроса занимает как раз 11 тактов.

Но с другой стороны, эта же картинка показывает, что если новый запрос к DMA формируется уже на такте "Т7", то он тоже будет обработан. Минимальное время на обработку запроса по данной картинке должно занимать порядка 7 тактов. То, что мы не достигли таких показателей, возможно связано с качеством сигналов, либо в какими-то недостатками в коде. Возможно влияет что-то еще, я не стал с этим серьезно разбираться.

(Для определенности, при задании периода ШИМ в 9 тактов, дельты получаются следующими: 10, 10, 20, … и далее тоже самое. Скорее всего DMA начинает пропускать события и копирует не каждое значение CCR, поэтому дельты получаются двойными).

Целью проекта было на одном примере показать, как работать с таймерами в режиме ШИМ и режиме Захват с применением DMA. Надеюсь этот пример будет полезен начинающим осваивать таймеры и DMA.

Вариант для 1986ВЕ1Т

В проект были добавлены дополнения для запуска на отладочной плате 1986ВЕ1Т. Каналы и таймеры были оставлены прежние, поменялись только выводы GPIO. Изменения в коде незначительны, поскольку разделение конфигураций и функций позволило подправить только конфигурационные структуры для 1986ВЕ1Т.

1986ve1_timcap.jpg

Итоговый результат получился точно такой-же, захватывается сигнал с периодом вплоть до 11 тиков TIM_CLK.