======Работа 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.