Содержание

Начальные сведения о DMA

DMA (Direct Memory Access) необходим для пересылки данных, без использования ядра микроконтроллера. Это позволяет не тратить процессорное время на банальную пересылку данных в цикле. Ядро запускает обмен и занимается своими делами, пока не возникнет прерывание от DMA о том, что заданное количество данных передано.

Как и в любой функции Copy, для запуска необходимы следующие параметры:

После передачи очередного слова данных, адреса, как источника данных, так и приемника могут либо увеличиваться на размер слова данных, либо оставаться неизменными. Инкремент необходим, когда передается/принимается массив данных. Неизменный адрес необходим при считывании/записи в регистр, например, когда выбран адрес АПЦ или ЦАП.

Пересылаемые данные могут быть:

Источники и приемники данных

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

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

Например, можно настроить, чтобы одно слово данных копировалось из адреса источника, по событию готовности АЦП. Тогда, если адресом источника выбрать регистр ADC_RESULT, то DMA может без участия ядра считывать готовые значения из АЦП и складывать их, например, в массив. Затем, когда DMA закончит передавать заданное количество данных (цикл DMA), сгенерируется прерывание от DMA, и ядро может обработать весь массив полученных данных от АЦП. Отличие от обычно копирования здесь в том, что измерение АЦП занимает некоторое количество времени, поэтому считывать результат с АЦП нужно только по событию готовности.

Аналогично, для вывода в ЦАП, адресом назначения DMA выбирается регистр DAC_DATA, а событием, по которому происходит передача слова, выбирается событие таймера CNT == ARR. Тогда с помощью DMA получится равномерный (по таймеру) вывод сигнала в ЦАП. Форма сигнала для вывода в ЦАП в данном случае обычно прописывается массивом, который выбирается в качестве источника данных для DMA. Размер массива указывается в параметре - Количество передаваемых данных. При окончании передачи массива возникает прерывание от DMA - окончание цикла DMA.

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

Каналы DMA

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

Поэтому DMA имеет 32 канала и каждый канал настраивается отдельно на свою задачу. Все что мы рассматривали до этого, относилось к одному каналу DMA. Таким образом, мы можем одновременно запустить и копирование массивов, и считывание АЦП и запись в ЦАП и многое другое. Все это будет работать как бы "параллельно".

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

Для этого, в настройках канала используется параметр R_power - количество передач до процедуры арбитража. Значение этого параметра задает степень 2 - т.е. возможны 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 и 1024 непрерывные передачи, которые не могут прерваться процедурой арбитража.

Например, нам надо передать 100 значений из массива в массив. Если выставить R_power = 6 (ведь 26 = 64), то канал передаст 64-е значения и разрешит провести арбитраж. Далее:

Задавая параметр R_power необходимо учитывать, что, выставляя большое значение, мы ускоряем исполнение одного канала в ущерб исполнению других. Обычно блочная передача (R_power > 0) используется для программной пересылки данных. Например, в SPL DMA используется при работе с FIFO буферами Ethernet. При работе же с такой периферией, как АЦП и ЦАП, нет смысла устанавливать значение R_power больше 0, ведь арбитраж логично отдавать после каждой передачи. В случае же с периферией, где есть FIFO, возможна блочная передача.

Передачу по одному значению будем называть одиночной передачей, а передачу 2R_power количества данных - блочной передачей.

Периферийные блоки могут запрашивать блочные и одиночные передачи, используя внутренние сигналы dma_req[С] и dma_sreq[С]. С - здесь означает канал, т.е. у для каждого канала эти сигналы свои.

Обработку одиночных запросов от dma_sreq[C] можно отключить/включить - для этого используются регистры CHNL_USEBURST_SET/CHNL_USEBURST_CLR. Тогда будут обрабатываться только запросы от dma_req[C].

Предполагаю, что запросы к DMA от периферии с FIFO формируются следующим образом (например UART):

  • Как только освобождается место в FIFO к DMA выставляется сигнал dma_sreq.
  • Как только срабатывает выставленный порог по заполнению в FIFO к DMA выставляется сигнал dma_req.

Таким образом, к DMA будет выставлено несколько запросов по линии dma_sreq, к тому моменту как сработает порог заполненности буфера и выставится запрос dma_req. DMA может ответить на каждый dma_sreq, либо при USEBURST закинет весь блок данных за один раз при dma_req.

Каналы DMA жестко привязаны к сигналам dma_req[С] и dma_sreq[С] от определенных периферийных блоков. Например, для микроконтроллеров серии 1986ВЕ9х каналы DMA могут обрабатывать следующие запросы от периферии.

Номер Источник sreq Источник req
0 UART1_TX UART1_TX
1 UART1_RX UART1_RX
2 UART2_TX UART2_TX
3 UART2_RX UART2_RX
4 SSP1_TX SSP1_TX
5 SSP1_RX SSP1_RX
6 SSP2_RX SSP2_TX
7 SSP2_RX SSP2_RX
8 ADC1_EC -
9 ADC1_EC -
10 TIMER1 -
11 TIMER1 -
12 TIMER1 -
13 - -
- -
31 - -

Каналы 13 - 31 чисто програмные. Кстати, запрос на передачу аппаратного канала также можно выставить программно. Делается это через регистр CHNL_SW_REQUEST записью "1" в бит по индексу канала. Регистр доступен только на запись.

У каждого канала есть свой приоритет, и он определяется индексом канала. 0-ой канал самый приоритетный, далее приоритет следует в порядке убывания. Если посмотреть распределение каналов DMA для серии 1986ВЕ9х, то самый приоритетный - это UART1_TX. Приоритет по умолчанию можно перекрыть, задав каналу высший приоритет. Для задания высшего приоритета используется регистр CHNL_PRIORITY_SET, в который необходимо записать "1" в бит по индексу канала. Для возвращения приоритета в обычное состояние (определяемое индексом), используется регистр CHNL_PRIORITY_CLR - запись "1" в бит по индексу канала. Чтение обоих регистров возвращает текущее состояние приоритета канала: 0 - приоритет по индексу, 1 - высший приоритет.

Арбитраж каналов DMA

На картинке ниже я попытался представить все выше сказанное про арбитраж и каналы DMA.

По картинке видно следующее:

По поводу программных каналов DMA стоит отметить, что если не выставить режим "Авто запрос", то необходимо в цикле программно вызывать DMA Request, чтобы канал участвовал в арбитраже и имел возможность передать следующую порцию данных. Ведь в данном случае нет аппаратных сигналов req и sreq, которые бы выставляли DMA Request.

Управляющие данные канала DMA

Каждый канал управляется своей структурой, которая состоит из 4-х 32-разрядных слов и в которой заданы все упомянутые выше параметры:

Параметры цикла DMA канала находятся в слове Control. Это слово меняется в процессе работы канала, и по этому слову можно диагностировать, сколько осталось данных для передачи и закончил ли работу канал. Именно это слово должно быть восстановлено начальными значениями, при перезапуске цикла DMA. Поля слова Control:

биты поле Значения Описание
31..30 dst_inc b00 - байт
b01 - полуслово
b10 - слово
b11 - ноль/нет инкремента
Инкремент адреса приемника
29..28 dst_size Размерность данных приемника, всегда равна src_size!
27..26 src_inc Инкремент адреса источника
25..24 src_size Размерность данных источника
23..21 dst_prot_ctrl b001 - Привилегированый доступ
b010 - Буферизируется
b100 - Кэшируется
Флаги привилегий приемника
20..18 src_prot_ctrl Флаги привилегий источника
17..14 R_power b000 - 1 передача Передач до арбитража
b001 - 2 передачи
b010 - 4 передачи
b1010 - 1024 передачи
13..4 N_minus_1 0 - 1024 Передач в цикле DMA
3 next_useburst b0 - sreq и req Переопределение CHNL_USEBURST_SET[C] в режиме
с изменением конфигурации
b1 - req
2..0 cycle_ctrl b000 - STOP Режим работы
b001 - Основной
b010 - Авто-запрос
b011 - Пинг-понг
b100/b101 - Реконфигурация с памятью
b110/b111 - Реконфигурация с периферией

Контроллер DMA вычисляет адрес текущей передачи из конечного адреса по следующей формуле:

  Addr = data_end_ptr - (n_minus_1 << src_inc), где 
  src_inc - инкремент данных источника
Размерность данных приемника всегда должна быть равна размерности данных источника!
src_inc == dst_inc

Управляющие структуры каналов должны лежать в памяти друг за другом и представлять собой массив структур. Когда DMA будет обрабатывать какой-то канал, например 5-й, то он будет считывать и изменять 5-ю структуру в массиве. Указатель на начало массива структур должен быть прописан в регистре CTRL_BASE_PTR. Адрес этого массива не может быть любым, подробнее см. спецификацию.

При использовании SPL этот массив структур уже определен в MDR32F9Qx_dma.c как DMA_ControlTable, и разбираться с этим нет необходимости. Если посмотреть, то массив DMA_ControlTable имеет вдвое большую длину - 64 структуры, а каналов всего 32. Вторая половина, та что с 32-го индекса, является альтернативным массивом структур, и адрес этого массива можно считать в регистре ALT_CTRL_BASE_PTR. Регистр ALT_CTRL_BASE_PTR доступен только на чтение и устанавливается автоматически при задании регистра CTRL_BASE_PTR.

Предположим, что нам требуется очень быстро передать или принять большой объем информации, многократно превышающий максимальную длину цикла в 1024 значения. Мы настроили управляющую структуру и запустили DMA, дождались прерывания от окончания передачи и теперь нам необходимо время, чтобы перенастроить структуру заново - сместить указатели, восстановить Control. Но бывает, что времени на это нет, данные продолжают поступать и мы будем их терять занимаясь переинициализацией. На этот случай как раз и необходима альтернативная структура.

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

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

Режимы с реконфигурацией являются продолжением режима с ping-pong. Здесь тоже используются обе структуры, но альтернативная структура настраивается не программистом, а считывается из какого-либо источника в цикле DMA по основной структуре. Получается, что программист настраивает только основную структуру канала DMA и осуществляет запуск. DMA считывает 4-ре 32-битных слова, например, откуда-то извне, и записывает их в свою альтернативную структуру. После этого сразу начинается исполнение цикла DMA по альтернативной структуре.

Это может быть полезно, например, при обмене данными между двумя микроконтроллерами через общую память. Например, допустим, МК1 готовит для МК2 структуру доступа к своим данным в общей памяти, которые хочет, чтобы МК2 считал и обработал. МК2 в цикле DMA по основной структуре считает эту структуру и выполнит чтение внешних данных в цикле DMA по данной считанной альтернативной структуре.

Регистры блока DMA

Описание регистров в принципе понятно, как и назначение бит. Остановлюсь только на тех нюансах, на которые не сразу обращаешь внимание. Полное описание регистров - в спецификации. Особое внимание следует уделить параметрам ReadOnly (RO) и WriteOnly (WO). Там где ниже атрибуты не указаны, доступ разрешен и на запись и на чтение - Read-Write (RW).

Большинство регистров блока DMA существую в двух вариантах - с суффиксами SET и CLR. Запись нулей ни в SET, ни в GET никак не влияют на состояние регистра.

  1. Регистр SET: - RW, запись 1-ниц выставляет биты в реальном регистре.
  2. Регистр CLR: - RO, запись 1-ниц стирает биты в реальном регистре.

Чтобы не отвлекаться на SET и GET, в таблице ниже я укажу только название реального регистра. А обозначением (S/C), помечу что доступ обеспечивается через регистры-маски SET и GET.

Регистр Описание
STATUS Отражает состояние контроллера, RO
CFG Бит Master_enable - включает работу блока DMA. Но весь регистр WO, при чтении возвращается 0.
CTRL_BASE_PTR Задает указатель на массив первичных и вторичных управляющих структур каналов.
ALT_CTRL_BASE_PTR Возвращает указатель на массив альтернативных управляющих структур каналов, RO. Значение высчитывается из CTRL_BASE_PTR и количества каналов в блоке DMA.
WAITONREG_STATUS Состояние сигналов запроса на обработку к каналам DMA, RO
CHNL_SW_REQUEST Запись 1 в бит канала вызывает программный запрос на обработку канала, WO.
CHNL_USE_BURTS (S/C) Бит канала: 0 - обработка запросов sreq и req, 1 - только req.
CHNL_REQ_MASK (S/C) Бит канала: 0 - Канал разрешен, 1 - Работа канала запрещена.
CHNL_ENABLE (S/C) Запись 1 в бит канала запускает работу канала. После окончания цикла, бит аппаратно сбрасывается в 0! (Подозреваю, что этот регистр является dma_done[].)
CHNL_PRI_ALT (S/C) Бит канала: 0 - Канал использует первичную структуру, 1 - Альтернативную. Можно указать вручную. В режимах использующих обе структуры бит переключается аппаратно.
CHNL_PRIORITY (S/C) Бит канала: 0 - обычный приоритет, 1 - повышенный.
ERR_CLR Бит 0: Возвращает наличие ошибки при работе DMA. Запись 1 сбрасывает ошибку. В каком канале была ошибка надо искать по dma_done[]. (Не понял как - спецификация "Индикация ошибок")

Пример программного копирования данных в FIFO

Для наглядности приведу пример работы с DMA из библиотечного файла MDR32F9Qx_eth.c. В этом примере пакет данных с помощью DMA записывается в FIFO блока Ethernet.

Инициализация DMA

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

void ETH_DMAPrepare(void)
{
  // Локальная управляющая структура канала DMA
  DMA_CtrlDataInitTypeDef DMA_PriCtrlStr;
  // Структура инициализации DMA
  DMA_ChannelInitTypeDef  DMA_InitStr;

  // Подача тактирования на DMA	
  RST_CLK_PCLKcmd(RST_CLK_PCLK_DMA, ENABLE);
  //  Сброс настроек DMA
  DMA_DeInit();

  //  Инициализация управляющей структуры по умолчанию	
  DMA_StructInit(&DMA_InitStr);

  //  Настройка DMA - выбор основной управляющей структуры
  DMA_InitStr.DMA_PriCtrlData = &DMA_PriCtrlStr;
  DMA_InitStr.DMA_Priority = DMA_Priority_High;
  DMA_InitStr.DMA_UseBurst = DMA_BurstClear;
  DMA_InitStr.DMA_SelectDataStructure = DMA_CTRL_DATA_PRIMARY;
  
  //  Инициализация DMA
  DMA_Init(DMA_Channel_SW2, &DMA_InitStr);
}

Функция записи в FIFO через DMA

Адрес FIFO (адрес назначения), как и адрес массива данных (источника), задается вызывающим кодом вместе с количеством передаваемых данных. Некоторые комментарии по коду:

#define DMA_Channel_SW2           ((uint8_t)(15))

// Доступ к глобальному массиву управляющих данных каналов
extern DMA_CtrlDataTypeDef DMA_ControlTable[DMA_Channels_Number * (1 + DMA_AlternateData)];


void ETH_DMAFrameTx(uint32_t * DstBuf, uint32_t BufferSize, uint32_t * SrcBuf)
{
  __IO uint32_t * ptrControltable;
  uint32_t tmpval;

  // Высший приоритет
  MDR_DMA->CHNL_PRIORITY_SET |= 1 << DMA_Channel_SW2;
  
  // Настройка управляющей структуры
  DMA_ControlTable[DMA_Channel_SW2].DMA_SourceEndAddr = (uint32_t)SrcBuf + 4*(BufferSize-1);
  DMA_ControlTable[DMA_Channel_SW2].DMA_DestEndAddr = (uint32_t)DstBuf;
  DMA_ControlTable[DMA_Channel_SW2].DMA_Control = DMA_DestIncNo
                                  |  DMA_SourceIncWord
				  | DMA_MemoryDataSize_Word
				  | DMA_Mode_AutoRequest
				  | DMA_Transfers_1024
				  | ((BufferSize - 1) << 4);

  //  Разрешение работы канала - DMA ждет Request для запуска
  DMA_Cmd(DMA_Channel_SW2, ENABLE);   //  MDR_DMA->CHNL_ENABLE_SET = (1 << DMA_Channel); 
  //  Подача DMA Request - старт цикла DMA
  DMA_Request(DMA_Channel_SW2);       //  MDR_DMA->CHNL_SW_REQUEST = (1 << DMA_Channel);
  
  //  Получение указателя на поле Control канала
  ptrControltable = (uint32_t *)&DMA_ControlTable[DMA_Channel_SW2].DMA_Control; 
  //  Ожидание окончания цикла
  while( 1 ){
   tmpval = (*ptrControltable) & 0x7;
   if(tmpval == 0)                     //  режим STOP?
     break;
  }
  
  //  Выключение канала DMA
  DMA_Cmd(DMA_Channel_SW2, DISABLE);   //  MDR_DMA->CHNL_ENABLE_CLR = (1 << DMA_Channel);
}

Буферы для DMA в 1986ВЕ1Т/1986Е3Т

Следует обратить внимание, что в отличие от 1986ВЕ9х, в микроконтроллерах 1986ВЕ1Т и 1986ВЕ3Т не вся память ОЗУ доступна для DMA. Необходимо при размещении массивов в ОЗУ указывать их расположение в памяти, начинающейся с адреса 0х2010_0000.

Расположение массива, помеченного каким-либо аттрибутом, указывается в линкере через подключение модифицированного скаттер файла. Скаттер файл создается автоматически при сборке проекта если в опциях стоит галочка Linker - Use Memory Layout from Target Dialog. Файл создается в поддиректории Objects и называется именем проекта с расширением *.sct. (Из-за такого расширения, в частности, не проходят проекты через почтовые службы, даже в архивах.)

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

//  Если буфер объявлен так
  uint32_t DestBuf[DATA_COUNT] __attribute__((section("EXECUTABLE_MEMORY_SECTION")));

//  То секция RW_IRAM2 в *.sct должна выглядеть так
  RW_IRAM2 0x20100000 0x00004000  {
   *.o (EXECUTABLE_MEMORY_SECTION)
   .ANY (+RW +ZI)
  }

Более наглядные картинки про получение и модификацию скаттер файла можно найти в этой статье "Расположение функций в ОЗУ, программирование EEPROM".

Чтобы не заниматься созданием скаттер файла каждый раз, когда потребуется использовать DMA, я вынес готовый файл под 1986ВЕ1Т в подборку src_brd на GitHub, файл назван ScatDMA_VE1.txt. В настройках проекта теперь достаточно его подключить в линкере и DMA будет работать с массивами помеченными аттрибутом EXECUTABLE_MEMORY_SECTION. (Расширение файла изменено чтобы проекты не фильтровались почтовиками.)

У меня этот файл мигрирует из проекта в проект со всей папкой brd_src, но можно подключить его и просто скопировав в свой проект. Поскольку раскладка памяти в 1986ВЕ1Т и 1986ВЕ3Т одинакова, то этот файл подойдет и для 1986ВЕ3Т.

Ошибка DMA c запросами от SSP, зацикливание прерывания

При работе с блоком DMA во всех микроконтроллерах с блоками SSP есть один аппаратный дефект. Баг связан с тем, что при включении МК, со стороны SSP к NVIC стоит активный сигнал запроса прерывания. Поэтому при включении блока DMA начинают непрерывно генерироваться прерывания DMA. Даже если все каналы DMA выключены. Дальнейшее исполнение программы при этом не происходит. Ядро не может выйти из обработчика прерывания - при выходе из обработчика исполнение тут же возвращается обратно в обработчик, поскольку прерывание активно.

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

Пример, показывающий данное поведение представлен здесь - GitHub, статья по этому проекту - "Вывод сигнала в DAC по DMA".

ИСПРАВЛЕНИЕ

Оказалось, что это не ошибка! Если внимательней читать спецификацию о работе DMA, то выясняется следующее:

  • Для того чтобы выключить канал DMA, он должен быть разрешен в регистре CHNL_ENABLE=1, но обработка запросов от него должна быть выключена в регистре CHNL_REQ_MASK=1!
  • При этом сам блок DMA должен быть затактирован и включен в регистре CFG.master_enable! Иначе, например, если сразу после входа в main() включить прерывание DMA в NVIC то исполнение сразу свалится в обработчик прерывания DMA_HandleIRQ().

Тогда становится не важно, есть ли активный запрос к DMA от выключенных SSP или ADC, к генерации прерывания это не приведет!

Дополнительная информация

В качестве дополнительной информации можно почитать какой-нибудь референсный дизайн на DMA от ARM. У ARM их несколько и мне не доводилось искать отличия. Но вот этот, на вскидку, похож на реализацию от Миландр, и судя по поиску в Google встречается у многих прочих производителей микроконтроллеров.

PrimeCell ® µDMAController (PL230) Technical Reference Manual

SReq vs BReq (или я многое понял сегодня)

С SReq - всегда все было понятно, это единичная транзакций блоком DMA. Например если UART принимает один байт, то он активирует запрос Sreq к каналу DMA, и если этот канал настроен с CHNL_USEBURST = 0, то канал заберет данные из DR регистра блока Uart куда-то в массив.

Сигнал BReq активируется блоком UART когда превышен порог заполнения FIFO, который задается регистром IFLS. Например пусть это будет 4 слова, тогда как только UART примет 4 слова в свое FIFO, то он выставит сигнал BReq к каналу DMA. НО DMA понятия не имеет, сколько там в FIFO UART-а подготовлено для него данных. Как не знает о размерах FIFO и настройках прочих периферийных блоков к DMA привязанных. DMA может лишь в ответ на сигнал BReq вычитает количество слов, которое задано в его поле арбитража. Следовательно, это поле необходимо задавать кратно размеру порога FIFO. Т.е. если для FIFO настроен порог в 4 слова, то поле арбитража тоже должно быть настроено в 4. Так-же можно настроить в 2 или в 1. Если же например настроить арбитраж в 1024, то в ответ на BReq DMA вычитает 1024 значения. Хотя очевидно, что в FIFO UART столько данных нет.