Содержание

Вывод сигнала в DAC по DMA в 1986ВЕ1Т и 1986ВЕ92У

DMA удобно использовать для вывода сигнала в ЦАП. Для того чтобы управлять шкалой времени выводимого сигнала, значения выходного сигнала должны выводиться по каким-то временным отсчетам. Для этого обычно используется таймер. Без использования таймера, DMA вывело бы весь сигнал очень быстро и остановилось при окончании цикла. Ведь DMA работает на частоте ядра и скорость "копирования" DMA никак не регулируется, она должна быть максимальна. Поэтому таймер крайне необходим, и именно по его событиям DMA будет выводить следующее значение сигнала в выходной регистр ЦАП. Сам сигнал, как правило, задается неким массивом, в котором записаны значения одного периода сигнала. Таким образом, переинициализация цикла DMA будет приводить к выводу в ЦАП следующего периода сигнала.

Рассмотрим реализацию данного режима работы, целиком код проекта доступен здесь - GitHub. Если с пониманием каких-то моментов в работе DMA возникнут сложности, необходимо обратиться к статье - Начальные сведения о DMA.

Проект реализован для запуска на отладочных платах для 1986ВЕ1Т и 1986ВЕ92У. Для наблюдения сигнала необходим осциллограф, который должен быть подключен к выводу используемого ЦАП (на BNC разъеме платы). Помимо вывода сигнала в ЦАП, индикация работы выведена на светодиод. Если светодиод мигает с периодом порядка секунды, то пример работает штатно. В проекте можно проверить необходимость тактирования SSP при работе DMA - (Подробнее). В этом случае светодиод мигать не будет, показывая неработоспособность такого варианта.

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

В 1986ВЕ92У проект использует DAC2, а в 1986ВЕ1Т используется DAC1. В обоих случаях используется Timer1. В целом код одинаков для обоих МК, различия заданы через макроопределения. Код можно посмотреть в исходниках main.c, в этой статье приводить его не буду.

Следует обратить внимание на следующие моменты, которые упоминались в статье - "Начальные сведения о DMA":

Необходимость тактирования блоков 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.

Объясняется такое поведение здесь.