======Работа DMA c каналом таймера в режиме захвата сигнала от ШИМ, 1986ВЕ91Т======
В этом примере рассмотрим работу таймеров в режиме генерации сигнала ШИМ и в режиме Захвата с применением DMA. ШИМ //(Широтно-Импульсная Модуляция)// - простым языком, это вывод сигнала в виде меандра. Захват - это фиксация счетчика таймера (CNT) при обнаружении фронтов и/или спадов входного меандра. Подробная статья про таймеры представлена в разделе справка - [[doc:doclist:timers|"Таймеры общего назначения"]].
Напомню, что каждый таймер общего назначения состоит из счетчика, который отсчитывает временные отсчеты (периоды времени) и 4-х каналов. Каждый канал имеет два вывода GPIO (прямой и инверсный) через которые может выводить наружу меандр, либо (только с прямого вывода) может отслеживать переключение уровней входного сигнала - т.е. захватывать события изменения логических уровней //"0"// и //"1"//.
Захват используется, например, для определения периода входного сигнала. В данном примере для этого несколько раз измеряется количество отсчетов таймера CNT между соседними фронтами входного сигнала. При обнаружении фронта на входе канала //Х//, таймер аппаратно заносит текущее значение CNT в регистр //CCRх//. Далее, эти "захваченные" значения из //CCRх// необходимо куда-то скопировать для последующего анализа. Для этих целей отлично подходит DMA.
Итак, пример работает следующим образом:
* 1-й канал Таймера 3 генерирует выходной сигнал ШИМ на вывод PF7.
* 4-й канал Таймера 1 захватывает входной сигнал с входа PA9. //(PF7 и PA9 на плате соединены)//
* DMA копирует захваченные значения из CCR4 в массив. Количество усреднений определяет цикл DMA и размер массива.
* После прерывания DMA об окончании цикла, из массива высчитываются дельты и усредняются - это период входного сигнала.
* Период и прочая служебная информация выводятся на LCD дисплей.
* После задержки цикл измерения запускается снова.
* Кнопки UP/DOWN на плате позволяют регулировать период ШИМ.
Регулируя кнопками период ШИМ мы определим какой минимальный период сигнала может быть захвачен.
Полностью пример доступен здесь - [[https://github.com/StartMilandr/6.x-DMA_Projects/tree/master/6.3-DMA_TimerCAP|GitHub]]
=====Подключение на плате и конфигурация выводов=====
Как уже было сказано, сигнал с канала ШИМ необходимо завести на вход канала захвата. Находим на демо-плате 1986ВЕ91Т выводы PF7 и PA9, соединяем. Выводы находятся на мезонином разъеме.
{{prog:timer: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 тоже, осталось настроить каналы таймеров на необходимый режим работы. Для этого необходимо заполнить две структуры:
- Структура канала //(TIMER_ChnInitTypeDef)// - режим ШИМ или Захват.
- Структура вывода канала //(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 от блока таймера и цикл измерения начинается вновь.
=====Находим минимальный период захвата=====
Для регулировки периода ШИМ в проекте используются кнопки
* 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);
}
При запуске примера все работает по умолчанию и на экран выводится текущий период ШИМ //Pw = 20// и измеренный период //Pс = 20//. Далее нажимаем кнопку Down и смотрим как ведут себя индикаторы. В итоге удалось добраться до значений периода ШИМ в 11 тактов TIM_CLK.
{{prog:timer:timer_capmin.jpg}}
Это не полноценный тест, а лишь оценочный, поэтому результаты могут быть и иными, если сделать что-то по другому. Полученное значение в 11 тактов вероятно связано с картинкой из спецификации про такты обработки импульсного запроса. Обработка одного запроса занимает как раз 11 тактов.
{{prog:dma:dma_tacts.png}}
Но с другой стороны, эта же картинка показывает, что если новый запрос к DMA формируется уже на такте "Т7", то он тоже будет обработан. Минимальное время на обработку запроса по данной картинке должно занимать порядка 7 тактов. То, что мы не достигли таких показателей, возможно связано с качеством сигналов, либо в какими-то недостатками в коде. Возможно влияет что-то еще, я не стал с этим серьезно разбираться.
//(Для определенности, при задании периода ШИМ в 9 тактов, дельты получаются следующими: 10, 10, 20, ... и далее тоже самое. Скорее всего DMA начинает пропускать события и копирует не каждое значение CCR, поэтому дельты получаются двойными).//
Целью проекта было на одном примере показать, как работать с таймерами в режиме ШИМ и режиме Захват с применением DMA. Надеюсь этот пример будет полезен начинающим осваивать таймеры и DMA.
=====Вариант для 1986ВЕ1Т=====
В проект были добавлены дополнения для запуска на отладочной плате 1986ВЕ1Т. Каналы и таймеры были оставлены прежние, поменялись только выводы GPIO. Изменения в коде незначительны, поскольку разделение конфигураций и функций позволило подправить только конфигурационные структуры для 1986ВЕ1Т.
{{prog:timer:1986ve1_timcap.jpg}}
Итоговый результат получился точно такой-же, захватывается сигнал с периодом вплоть до 11 тиков TIM_CLK.