24440

[i] Работа DMA c каналом таймера в режиме захвата сигнала от ШИМ. Микроконтроллеры К1986ВЕ91Т и К1986ВЕ1Т

Дата последнего изменения: 19.12.2025 16:22:17
Материал из настоящей статьи, относящийся к микросхемам К1986ВЕ92У и К1986ВЕ1Т, распространяется в том числе на микроконтроллеры К1986ВЕ91Т, К1986ВЕ92QI, К1986ВЕ92У1, К1986ВЕ93У, К1986ВЕ94Т, К1986ВЕ92FI, К1986ВЕ92F1I, К1986ВЕ94GI и К1986ВЕ1QI, К1986ВЕ1АТ, К1986ВЕ1FI, К1986ВЕ1GI

В этой статье будет рассмотрена работа таймеров в режиме генерации сигнала ШИМ и в режиме Захвата с применением DMA на микроконтроллере К1986ВЕ91Т. Проект также может быть запущен на микроконтроллере К1986ВЕ1Т, но в статье основная работа будет вестись с контроллером из серии К1986ВЕ9х.

Про режимы работы таймеров можно прочитать в следующей статье  - "Таймеры общего назначения".

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

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

Пример работает следующим образом:

  • 1-й канал Таймера 3 генерирует выходной сигнал ШИМ на вывод PF7.
  • 4-й канал Таймера 1 захватывает входной сигнал с входа PA9. (PF7 и PA9 на плате соединены)
  • DMA копирует захваченные значения из CCR4 в массив. Количество усреднений определяет цикл DMA и размер массива.
  • После прерывания DMA об окончании цикла, из массива высчитываются дельты и усредняются - это период входного сигнала.
  • Период и прочая служебная информация выводятся на LCD дисплей.
  • После задержки цикл измерения запускается снова.
  • Кнопки UP/DOWN на плате позволяют регулировать период ШИМ.

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

Пример доступен для загрузки после статьи, в подразделе "Файлы для скачивания".

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

Ранее говорилось о том, что сигнал с канала ШИМ необходимо завести на вход канала захвата. Находим на демонстрационной плате К1986ВЕ91Т(94Т) производства компании Миландр выводы PF7 и PA9 и производим соединение. Выводы находятся на мезонином разъеме. Порядок подключения показан на рисунке 1.

Рисунок 1 - Подключение выводов таймера на отладочной плате К1986ВЕ91Т(94Т)

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

Настройка 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);
Фрагмент кода 1

Настройка 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);
Фрагмент кода 2

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

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

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

Оба таймера настраиваются на одну временную шкалу, разница будет только в настройке периода. В таймере ШИМ выставляется период выводимого меандра (регистр 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 // Длительность импульса ШИМ
Фрагмент кода 3

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

// Выбор 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
Фрагмент кода 4

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

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

// Заполнение структуры счетчика таймера по умолчанию
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);
Фрагмент кода 5

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

Счетчик таймера и выводы 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);
Фрагмент кода 6

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

// Настройка структур канала таймера на ШИМ по умолчанию
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;

   ...
}
Фрагмент кода 7

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

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

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

// Настройка структур канала таймера на Захват по умолчанию
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;
   ...
}
Фрагмент кода 9

Настройка 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);
Фрагмент кода 10

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

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

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

Основной цикл описываться не будет. Будет описана только рабочая часть - запуск измерения и остановка в прерывании 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;

   // Выставление флага на обработку результатов
   DoCheckResult = 1;

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

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

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

Таблица 1

Правило Описание
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 отрабатывает этот висящий запрос, и выход из прерывания происходит успешно. 

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

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

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

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

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

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

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

  • UP: +1 к периоду ШИМ
  • DOWN: -1 к периоду ШИМ

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

  • Pw - Период ШИМ (регулируется UP/DOWN).
  • Pc - Высчитанный период из массива захвата.
  • Offs - Пропуск первых значений в массиве захвата при расчете периода (регулируется LEFT/RIGHT).
  • Err - Количество ошибок в массиве захвата.

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

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

  • LEFT : -1 к Offs
  • RIGHT: +1 к Offs

Для полной ясности ниже приведен простой код подсчета периода:

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);
}
Фрагмент кода 13

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

Рисунок 2 - Пример работы проекта на LCD экране отладочной платы

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

Рисунок 3 - Обработки импульсного запроса в тактах

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

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

Вариант для микроконтроллера К1986ВЕ1Т

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

Рисунок 4 - Подключение выводов таймера на отладочной плате К1986ВЕ1Т

Итоговый результат аналогичен результатами на микроконтроллере К1986ВЕ91Т.


Сохранить статью в PDF

Файлы для скачивания

Теги

Была ли статья полезной?