======Вывод сигнала в DAC по DMA в 1986ВЕ1Т и 1986ВЕ92У====== DMA удобно использовать для вывода сигнала в ЦАП. Для того чтобы управлять шкалой времени выводимого сигнала, значения выходного сигнала должны выводиться по каким-то временным отсчетам. Для этого обычно используется таймер. Без использования таймера, DMA вывело бы весь сигнал очень быстро и остановилось при окончании цикла. Ведь DMA работает на частоте ядра и скорость "копирования" DMA никак не регулируется, она должна быть максимальна. Поэтому таймер крайне необходим, и именно по его событиям DMA будет выводить следующее значение сигнала в выходной регистр ЦАП. Сам сигнал, как правило, задается неким массивом, в котором записаны значения одного периода сигнала. Таким образом, переинициализация цикла DMA будет приводить к выводу в ЦАП следующего периода сигнала. Рассмотрим реализацию данного режима работы, целиком код проекта доступен здесь - [[https://github.com/StartMilandr/6.x-DMA_Projects/tree/master/DMA_ToDAC|GitHub]]. Если с пониманием каких-то моментов в работе DMA возникнут сложности, необходимо обратиться к статье - [[prog:dma:dma_intro|Начальные сведения о DMA]]. Проект реализован для запуска на отладочных платах для 1986ВЕ1Т и 1986ВЕ92У. Для наблюдения сигнала необходим осциллограф, который должен быть подключен к выводу используемого ЦАП (на BNC разъеме платы). Помимо вывода сигнала в ЦАП, индикация работы выведена на светодиод. Если светодиод мигает с периодом порядка секунды, то пример работает штатно. В проекте можно проверить необходимость тактирования SSP при работе DMA - [[prog:dma:dma_intro#ошибка_dma_c_запросами_от_ssp|(Подробнее)]]. В этом случае светодиод мигать не будет, показывая неработоспособность такого варианта. =====Инициализация и тактирование===== В 1986ВЕ92У проект использует DAC2, а в 1986ВЕ1Т используется DAC1. В обоих случаях используется Timer1. В целом код одинаков для обоих МК, различия заданы через макроопределения. Код можно посмотреть в исходниках [[https://github.com/StartMilandr/6.x-DMA_Projects/blob/master/DMA_ToDAC/main.c|main.c]], в этой статье приводить его не буду. Следует обратить внимание на следующие моменты, которые упоминались в статье - [[prog:dma:dma_intro|"Начальные сведения о DMA"]]: * Для 1986ВЕ1Т массив с отсчетами сигнала должен лежать в области доступной для DMA - [[prog:dma:dma_intro#буферы_для_dma_в_1986ве1т_1986е3т|Подробнее]]. * При работе с DMA необходимо включить тактирование всех блоков SSP - [[prog:dma:dma_intro#ошибка_dma_c_запросами_от_ssp|Подробнее]]. Необходимость тактирования блоков SSP как раз и проверяется в этом проекте. Вариант тактирования с блоками SSP и без выбирается макроопределением BUG_SSPx_ACTIVATE. // По умолчанию используется рабочий вариант // При активации определения, SSP не тактируются и DMA не управляем //#define BUG_SSPx_ACTIVATE // В 1986ВЕ1Т на один SSP больше #ifdef USE_MDR1986VE9x #define DMA_CLOCK_FIX (RST_CLK_PCLK_SSP1 | RST_CLK_PCLK_SSP2 | RST_CLK_PCLK_DMA) #elif defined (USE_MDR1986VE1T) #define DMA_CLOCK_FIX (RST_CLK_PCLK_SSP1 | RST_CLK_PCLK_SSP2 | RST_CLK_PCLK_SSP3 | RST_CLK_PCLK_DMA) #endif // Тактирование DMA, с блоками SSP, либо без #ifndef BUG_SSPx_ACTIVATE RST_CLK_PCLKcmd (DMA_CLOCK_FIX, ENABLE); // Рабочий вариант #else RST_CLK_PCLKcmd (RST_CLK_PCLK_DMA, ENABLE); // Не рабочий вариант #endif =====Настройка ЦАП, таймера и DMA===== Для настройки ЦАП требуется настроить вывод GPIO в необходимую функцию и настроить сам ЦАП. Расписывать тут особо нечего. Выбор пина GPIO передается входными параметрами в функцию BRD_DAC_PortInit(), которая является обычной функцией настройки выводов GPIO. Для ЦАП требуется минимальная настройка. void BRD_DAC_PortInit(uint32_t Port_ClockMask, MDR_PORT_TypeDef* PORTx, uint32_t Port_PinsSel) { PORT_InitTypeDef GPIOInitStruct; // Тактирование RST_CLK_PCLKcmd (Port_ClockMask, ENABLE); // Конфигурация линий ввода-вывода PORT_StructInit (&GPIOInitStruct); GPIOInitStruct.PORT_Pin = Port_PinsSel; GPIOInitStruct.PORT_MODE = PORT_MODE_ANALOG; // Инициализация порта PORT_Init (PORTx, &GPIOInitStruct); } void BRD_DACs_Init(void) { // Тактирование RST_CLK_PCLKcmd (RST_CLK_PCLK_DAC, ENABLE); // Деинициализация ЦАП DAC_DeInit(); // Простейшая настройка ЦАП DAC_Init(DAC_SYNC_MODE_Independent, DAC1_AVCC, DAC2_AVCC); } Настройки DMA стандартные, как и в демо-проектах SPL. Разница лишь в том, что серию вызовов функций SPL по настройке DMA я вложил внутрь одной функции с префиксом BRD_. Вызовы этих функций SPL кочуют из проекта в проект и особого смысла не несут. Наличие же нескольких вызовов делает код менее читаемым и рассеивает внимание, поскольку действие набора функций одно - сделать Init, то логично убрать их все в один вызов. Если будет интересно как именно реализован Init, то можно зайти внутрь BRD_ и посмотреть. В итоге, для работы с DMA у меня получились две функции BRD_DMA_Init(void) - для включения блока DMA и BRD_DMA_Init_Channel(DMA_CHANNEL, &DMA_ChanCtrl) для настройки необходимого канала. Эти две функции обычно используются в моих проектах и настраивают DMA по умолчанию, нужным мне образом. Подобное "уплотнение кода" произведено и для блока таймера. В случае с DMA и таймером, необходимая конфигурация для настройки блоков передается во входных параметрах. На мой взгляд, при таком подходе этап настройки в проекте выглядит более лаконично. Функции BRD_ инкапсулируют включение тактирования, DeInit(), настройку портов и прочие необходимые вызовы, на которых заострять внимание при реализации логики приложения смысла нет. uint32_t DMA_ChannelCtrl; ... // Настройка DMA BRD_DMA_Init(); DMA_DataCtrl_Pri.DMA_SourceBaseAddr = (uint32_t)&signal; DMA_DataCtrl_Pri.DMA_DestBaseAddr = (uint32_t)&DAC_REG_DATA; DMA_DataCtrl_Pri.DMA_CycleSize = DATA_COUNT; BRD_DMA_Init_Channel(DMA_CHANNEL, &DMA_ChanCtrl); // Считываем управляющее слово канала, которое получилось после настройки // Будет использовано в прерывании для переинициализации цикла DMA BRD_DMA_Read_ChannelCtrl(DMA_CHANNEL, &DMA_ChannelCtrl); // Настройка Таймера BRD_Timer_InitStructDef(&TimerInitStruct, SIGNAL_FREQ_HZ * DATA_COUNT, 64000); // DAC Out 1KHz BRD_Timer_Init(&brdTimer1, &TimerInitStruct); Настройки блоков, как уже сказал, передаются через входные параметры. Их значения можно посмотреть в исходных кодах проекта на GitHub. В логику примера выведены только самые необходимые, такие как адреса и количество данных для DMA. Ведь эти данные меняются из проекта в проект, в то время как остальные настройки DMA могут быть использованы в других проектах со схожим функционалом, где потребуется копирование 16-битных дынных. После настройки таймера и канала DMA остается лишь разрешить обращения к DMA со стороны таймера и включить таймер. После этого при каждом отсчете периода таймера (событие CNT == ARR) в ЦАП будет выводиться одно значение из массива signal. Это копирование данных из ячеек массива в регистр АЦП будет осуществлять канал DMA. При выводе всех значений, указанных в цикле DMA возникнет прерывание от DMA. #ifdef USE_MDR1986VE9x #define TIMER_CALL_DMA_ENA TIMER_DMACmd (MDR_TIMER1, TIMER_STATUS_CNT_ARR, ENABLE) #elif defined (USE_MDR1986VE1T) #define TIMER_CALL_DMA_ENA TIMER_DMACmd (MDR_TIMER1, TIMER_STATUS_CNT_ARR, TIMER_DMA_Channel0, ENABLE) #endif main() { ... // Разрешение запросов к DMA со стороны Таймера TIMER_CALL_DMA_ENA; // Запуск таймера BRD_Timer_Start(&brdTimer1); } Разрешение запросов к DMA в таймерах используемых микроконтроллеров реализовано по разному, поэтому пришлось использовать макроопределение для этого вызова - TIMER_CALL_DMA_ENA. =====Рабочий цикл===== После запуска таймера будет выведен в ЦАП один период сигнала и вызовется обработчик прерывания по окончанию цикла DMA. void DMA_IRQHandler (void) { // Вариант 1: // Восстановление управляющего слова канала BRD_DMA_Write_ChannelCtrl(DMA_CHANNEL, DMA_ChannelCtrl); // Запуск канала на обработку DMA_Cmd(DMA_CHANNEL, ENABLE); // Вариант 2: // Переинициализация цикла DMA через SPL. // Медленно, дает артефакт сигнала (см. осциллографом) при PLL_MUX < RST_CLK_CPU_PLLmul3 // DMA_Init(DMA_CHANNEL, &DMA_ChanCtrl); // Флаг для главного цикла, для обновления светодиода. IrQ_On = 1; NVIC_ClearPendingIRQ(DMA_IRQn); } При возникновении прерывания DMA необходимо переинициализировать новый цикл DMA, чтобы вывод сигнала не прерывался. На момент прерывания, в управляющем слове канала DMA обнулен счетчик передач и выставлен режим Stop. Поэтому в коде я использую сохраненное на этапе инициализации начальное значение этого слова для восстановления настроек канала DMA. После этого функция DMA_Cmd(DMA_CHANNEL, ENABLE) выставляет "1" в бит канала регистра DMA_ENABLE, что активирует канал для обработки. //Напомню что этот бит сбрасывается аппаратно при окончании цикла DMA.// Здесь следует учесть, что таймер мы не останавливаем чтобы сохранить равномерность выводимого сигнала. Поэтому переинициализация DMA должна произойти раньше, чем потребуется вывести следующий отсчет в ЦАП. Т.е. надо успеть настроить DMA за время периода таймера минус время на обработку прерывания. В коде представлен второй вариант с полной переинициализацией DMA. Если при реализации этого варианта посмотреть на осциллограф, то на выводимом сигнале будут присутствовать различные артефакты. Поэтому переинициализацию DMA в своих проектах я произвожу через восстановление управляющего слова. while (1) { if (IrQ_On) { IrQ_On = 0; if (CheckDoLedSwitch()) BRD_LED_Switch(LED_DMA_CYCLE); } } В основном цикле считается сколько было выполнено циклов DMA и при достижении заданного периода переключается состояние светодиода на отладочной плате. Это демонстрирует внешнему наблюдателю работоспособность программы. =====Баг с тактированием SSP===== По коду видно, что на этапе инициализации светодиодов зажигаются два светодиода. После запуска таймера светодиоды выключаются //(при исправной работе далее будет мигать только один)//. Если раскомментировать BUG_SSPx_ACTIVATE, то при запуске в МК светодиоды зажигаются и остаются в таком состоянии. Это вызвано тем, что начинает бесконечно генерироваться прерывание от DMA которое приводит к бесконечному вызову обработчика. Основной код при этом не выполняется и до функций выключения светодиодов исполнение не доходит. У меня в коде выключение светодиодов выставлено после включения таймеров, но на сколько помню, до включения таймеров исполнение не доходит. Сваливание в прерывание произойдет сразу после инициализации DMA. Объясняется такое поведение [[prog:dma:dma_intro#ошибка_dma_c_запросами_от_ssp|здесь]].