Статьи

Adafruit WS2812B NeoPixels с платой Freescale FRDM-K64F – Часть 5. DMA

Это  часть 5  мини-серии. В части 4 я описал, как настроить FTM (модуль Kinetis Flex Timer) для генерации необходимых сигналов, используемых для операций DMA (см. « Учебное пособие: NeoPixels Adafruit WS2812B с платой Freescale FRDM-K64F — Часть 4: Таймер »). В этом посте я опишу, как использовать для запуска событий DMA (Direct To Memory). Цель состоит в том, чтобы запустить NeoPixel от Adafruit (WS2812B) с  платой Freescale FRDM-K64F  :

FRDM-K64F с Adafruit NeoPixel

FRDM-K64F с Adafruit NeoPixel

Список учебников Mini Series

  1. Учебник. AdoPruit WS2812B NeoPixels с платой Freescale FRDM-K64F — Часть 1.  Аппаратное обеспечение
  2. Учебник. AdoPruit WS2812B NeoPixels с платой Freescale FRDM-K64F — Часть 2.  Инструменты программного обеспечения
  3. Учебное пособие: Adafruit WS2812B NeoPixels с платой Freescale FRDM-K64F — Часть 3.  Основные понятия
  4. Учебник. AdoPruit WS2812B NeoPixels с платой Freescale FRDM-K64F — Часть 4.  Таймер
  5. Учебник. AdoPruit WS2812B NeoPixels с платой Freescale FRDM-K64F — Часть 5.  DMA

Контур

В этой статье я использую DMA (прямой доступ к памяти) для выполнения операций с памятью в память, чтобы сгенерировать необходимый поток битов для светодиодов WS2812B. В предыдущем уроке я использовал FTM устройства FRDM-K64F для генерации трех сигналов:

Форма волны и время

Форма волны и время

Я буду использовать «задний фронт» сигналов для запуска передач DMA, помеченных как «M» на следующей временной диаграмме:

Водительские биты с DMA

Водительские биты с DMA

В этом посте я использую Kinetis Design Studio v3.0.0 с Kinetis SDK v1.2.

Мы настроим весь этот движок позже в этой статье. Сначала давайте перейдем к простой вещи: сконфигурируем вывод GPIO для DIN светодиодов.

Порт GPIO

Для генерации сигнала на DIN NeoPixel / WS2812 я могу использовать обычный вывод GPIO (вход / выход общего назначения). Если я использую несколько выводов на таком порту GPIO, я могу управлять несколькими «дорожками» пиксельных массивов.

: idea:  Мне нужно 24 бит на каждый светодиод / пиксель (8 бит для красного, зеленого и синего каждый). Из-за особенностей записи байтов в порт GPIO мне нужно 3 байта памяти (обычно ОЗУ) для каждого светодиода. Так что наличие большого количества светодиодов означает много оперативной памяти. Только с одной дорожкой используется только один бит в каждом байте. Но если у меня есть 8 дорожек (скажем, портовые биты от 0 до 7), тогда мне все еще могут понадобиться 3 байта для каждого пикселя, но я могу управлять 8 светодиодами с этими тремя байтами. Поэтому, если у вас много-много светодиодов, используйте несколько полос для их объединения. Это не только уменьшает объем необходимой памяти, но также уменьшает время, необходимое для отправки потока битов.

Чтобы использовать порт GPIO, мне нужно:

  1. Прикрепите булавку  к порту, используемому. В основном это означает направление внутреннего сигнала порта на внешний вывод.
  2. Часы порт  (включить часы). Доступ к регистрам портов без их синхронизации вызовет  серьезную ошибку .
  3. Сконфигурируйте  порт / вывод как  выходной  вывод / порт, используя GPIOx_PDDR (регистр направления данных порта).
  4. Чтобы поставить вывод (ы)  ВЫСОКИЙ , я могу записать 1 бит / значение в GPIOx_PSOR (регистр вывода набора портов)
  5. Чтобы поставить вывод (ы)  НИЗКИМ , я могу записать 1 бит / значение в GPIOx_PCOR (Выходной регистр очистки порта)
  6. Чтобы поставить вывод (ы)  ВЫСОКИЙ или НИЗКИЙ , я могу записать бит / значение в GPIOx_PDOR (регистр вывода данных порта).

На следующей диаграмме показаны записи в регистр выходного порта, необходимые для создания потока битов WS2812:

Регистр GPIO Ouput пишет

Выходной регистр GPIO пишет

Мы могли бы сделать это от прерываний по таймеру, но опять-таки это было бы слишком медленно. Вместо этого эти записи в регистр выходного порта должны запускаться DMA.

Настройте порт GPIO

На моей плате я использую только одну дорожку / штырь к DIN WS2812B. Я собираюсь использовать PTD0  (PORT D, контакт 0) для него:

Использование PTD0 для DIN

Использование PTD0 для DIN

Три других белых провода — это контакты трех каналов FTM, подключенных к логическому анализатору.

Поэтому мне нужно расширить инициализацию оборудования, как показано ниже:

  1. Строка 4: включить синхронизирующий вентиль для порта D
  2. Строка 11: Mux PTD0 в качестве GPIO
  3. Строка 12: запишите в PDDR (регистр направления данных порта) 1 бит, чтобы использовать PTD0 в качестве выходного контакта.

static void InitHardware(void) {
  /* Enable clock for PORTs */
  SIM_HAL_EnableClock(SIM, kSimClockGatePortC);
  SIM_HAL_EnableClock(SIM, kSimClockGatePortD);

  /* Setup board clock source. */
  g_xtal0ClkFreq = 50000000U;           /* Value of the external crystal or oscillator clock frequency of the system oscillator (OSC) in Hz */
  g_xtalRtcClkFreq = 32768U;            /* Value of the external 32k crystal or oscillator clock frequency of the RTC in Hz */

  /* Use PTD0 as DIN to the Neopixels: mux it as GPIO and output pin */
  PORT_HAL_SetMuxMode(PORTD, 0UL, kPortMuxAsGpio); /* PTD0: DIN to NeoPixels */
  GPIO_PDDR_REG(PTD_BASE_PTR) |= (1<<0); /* PTD0 as output */

  /* FTM and FTM Muxing */
  InitFlexTimer(FTM0_IDX);
  PORT_HAL_SetMuxMode(PORTC,1UL,kPortMuxAlt4); /* use PTC1 for channel 0 of FTM0 */
  PORT_HAL_SetMuxMode(PORTC,2UL,kPortMuxAlt4); /* use PTC2 for channel 1 of FTM0 */
  PORT_HAL_SetMuxMode(PORTC,3UL,kPortMuxAlt4); /* use PTC3 for channel 2 of FTM0 */
}

Вы можете заметить, что я использую разные API для этого.

PORT_HAL_SetMuxMode(PORTD, 0UL, kPortMuxAsGpio); /* PTD0: DIN to NeoPixels */

является методом Kinetis SDK. Однако

GPIO_PDDR_REG(PTD_BASE_PTR) |= (1<<0); /* PTD0 as output */

использует прямую запись в регистр стиля CMSIS-Core. Muxing прямо вперед. Однако для настройки вывода в качестве выходного вывода требуются дополнительные слои в SDK с дескрипторами выводов. Для меня использование слоев Kinetis SDK GPIO в этом примере слишком сложное, поэтому я просто использую макросы регистра CMSIS.

: idea:  Я также хочу показать здесь, что сочетание SDK с CMSIS — это, на мой взгляд, хорошая вещь, чтобы сбалансировать простоту использования и сложность.

После этого у меня настроен вывод GPIO. Теперь мне нужно написать регистры портов с DMA.

Прямой доступ к памяти

Как объясняется в  посте Concepts  , мне нужно что-то очень быстрое, чтобы написать регистр порта GPIO. Поскольку время составляет около 0,3 мкс, определенно слишком быстро, чтобы использовать процессор для этого, особенно если я хочу, чтобы процессор тоже делал что-то еще. С DMA доступ к памяти будет осуществляться без участия процессора, именно то, что мне нужно.

Я использую DMA на плате FRDM-KL25Z для таких вещей, как чтение портов в  DIY Logic Analyzer или  проецирование WS2812 пикселей . Микроконтроллер ARM Cortex-M4F на плате FRDM-K64F имеет контроллер eDMA (улучшенный DMA). Он может использовать до 16 независимых каналов DMA для операций DMA с расширенными вычислениями адреса источника и назначения. Этот контроллер eDMA описан в  Справочном руководстве K64F .

Блок-схема eDMA

Блок-схема eDMA (Источник: Справочное руководство по Freescale K64F)

  • Путь к данным : контроллер может читать / записывать данные с / на коммутатор. Перекладина обеспечивает доступ к памяти и периферии.
  • Путь к адресу : этот блок вычисляет адрес источника и получателя. Это делает вычисление плюс любое увеличение или уменьшение адреса. Для этого он использует дескрипторы управления передачей (TCD).
  • Управление и арбитраж канала : этот блок отвечает за получение запросов DMA от поддерживаемых источников запросов (например, от модуля таймера) и флагов обратной записи в него (например, информирование модуля таймера о том, что операция DMA выполнена).
  • Дескриптор управления передачей : дескриптор используется для описания того, что должно быть сделано в операциях DMA: сколько байтов для чтения / записи, адрес источника и назначения, что делать после передачи, сколько циклов (внутренний и внешний циклы).

Основной поток DMA следующий: когда поступает запрос периферийного устройства DMA, он устанавливает адрес источника и назначения, используя TCD:

Работа с eDMA, часть 1

Работа с eDMA, часть 1 (Источник: Справочное руководство по Freescale K64F)

Используя адрес источника и адресата, контроллер выполнит операцию чтения / записи. В зависимости от конфигурации в TCD, это может быть несколько операций чтения / записи источника / назначения с помощью счетчиков циклов ‘minor’ и ‘major’:

Работа с eDMA, часть 2

Работа с eDMA, часть 2 (Источник: Справочное руководство по Freescale K64F)

На последнем этапе TCD обновляется, например, меняются значения адреса и устанавливаются флаги. Кроме того, периферийное устройство, которое запросило передачу DMA, получает информацию о том, что операция выполнена:

Операция eDMA, часть 3

Операция eDMA, часть 3

Соображения памяти

Помните, у меня есть три канала FTM. Каждый канал должен запускать работу порта GPIO:

  1. FTM0 канал 0 : запишите ‘1’ в  PSOR,  чтобы установить DIN на ВЫСОКИЙ.
  2. FTM0 Канал 1 : Записать бит данных в  PDOR  для поддержания DIN HIGH («1» WS2812 bit) или для установки DIN LOW («0» WS2812 bit).
  3. FTM0 Канал 2 : Запишите «1» в  PCOR,  чтобы установить DIN на НИЗКИЙ.

Это необходимо сделать для каждого бита WS2812, а количество бит определяется количеством светодиодов WS2812 (24 бита для каждого), а биты сохраняются в буфере:

#define NEO_NOF_PIXEL       (8*8) /* Adafruit 8x8 matrix */
#define NEO_NOF_BITS_PIXEL   (24) /* 24 bits for pixel */
static uint8_t transmitBuf[NEO_NOF_PIXEL*NEO_NOF_BITS_PIXEL];

Помните, что в каждом байте используется только младший значащий бит, поскольку я использую только одну дорожку WS2812.

: idea:  Если бы я использовал 8 линий (например, 8 дисплеев NeoPixel Matrix, каждый из которых подключен к одному выводу порта, от PTD0 до PTD7), то я бы использовал каждый бит байта. Мне нужно 3 байта памяти на каждый пиксель WS2812.

Запуск DMA-запросов

Чтобы включить запросы DMA от моих каналов FTM, мне нужно внимательно прочитать справочное руководство:

FTM DMA Запрос

FTM DMA Запрос

Что меня смущает, так это то, что две настройки (DMA = 0 | CHnIE = 0 и DMA = 1 | CHnIE = 0) делают то же самое? Сначала я подумал, что это должна быть ошибка копирования-вставки в руководстве. Но без включения бита «Interrupt Enable» (CHnIE) DMA не работал  🙁 . Похоже, что на самом деле должны быть установлены оба бита. И это было то, что я должен был сделать в моей процедуре инициализации / сброса FTM:

static void ResetFTM(uint32_t instance) {
  FTM_Type *ftmBase = g_ftmBase[instance];
  uint8_t channel;

  /* reset all values */
  FTM_HAL_SetCounter(ftmBase, 0); /* reset FTM counter */
  FTM_HAL_ClearTimerOverflow(ftmBase); /* clear timer overflow flag (if any) */
  for(channel=0; channel&amp;lt;NOF_FTM_CHANNELS; channel++) {
    FTM_HAL_ClearChnEventFlag(ftmBase, channel); /* clear channel flag */
    FTM_HAL_SetChnDmaCmd(ftmBase, channel, true); /* enable DMA request */
    FTM_HAL_EnableChnInt(ftmBase, channel); /* enable channel interrupt: need to have both DMA and CHnIE set for DMA transfers! See RM 40.4.23 */
  }
}

Инициализация драйвера DMA

Время инициализировать драйвер DMA SDK. Из-за сложности eDMA я снова использую смесь Kinetis SDK API и Kinetis SDK HAL API. Инициализацию DMA я делаю с помощью API SDK:

static void InitDMADriver(void) {
  edma_user_config_t  edmaUserConfig;
  static edma_state_t edmaState;
  uint8_t res, channel;

  /* Initialize eDMA modules. */
  edmaUserConfig.chnArbitration = kEDMAChnArbitrationRoundrobin; /* use round-robin arbitration */
  edmaUserConfig.notHaltOnError = false; /* do not halt in case of errors */
  EDMA_DRV_Init(&amp;amp;edmaState, &amp;amp;edmaUserConfig); /* initialize DMA with configuration */
}

Инициализация довольно проста: я установил арбитраж канала DMA (планирование приоритетов) на Round-Robin. Это означает, что DMA будет выполнять один канал один за другим и не будет использовать механизм приоритета канала DMA. Поскольку у меня есть фиксированная последовательность событий канала таймера, я сохраняю ее простой и использую циклический перебор. С noHaltOnError я указываю, что устройство не должно останавливаться в случае ошибок, это опять-таки для простоты.

Я инициализирую Драйвер DMA как часть моей аппаратной инициализации:

static void InitHardware(void) {
  /* Enable clock for PORTs */
  SIM_HAL_EnableClock(SIM, kSimClockGatePortC);
  SIM_HAL_EnableClock(SIM, kSimClockGatePortD);

  /* Setup board clock source. */
  g_xtal0ClkFreq = 50000000U;           /* Value of the external crystal or oscillator clock frequency of the system oscillator (OSC) in Hz */
  g_xtalRtcClkFreq = 32768U;            /* Value of the external 32k crystal or oscillator clock frequency of the RTC in Hz */

  /* Use PTD0 as DIN to the Neopixels: mux it as GPIO and output pin */
  PORT_HAL_SetMuxMode(PORTD, 0UL, kPortMuxAsGpio); /* PTD0: DIN to NeoPixels */
  GPIO_PDDR_REG(PTD_BASE_PTR) |= (1&amp;lt;&amp;lt;0); /* PTD0 as output */

  /* FTM and FTM Muxing */
  InitFlexTimer(FTM0_IDX);
  PORT_HAL_SetMuxMode(PORTC,1UL,kPortMuxAlt4); /* use PTC1 for channel 0 of FTM0 */
  PORT_HAL_SetMuxMode(PORTC,2UL,kPortMuxAlt4); /* use PTC2 for channel 1 of FTM0 */
  PORT_HAL_SetMuxMode(PORTC,3UL,kPortMuxAlt4); /* use PTC3 for channel 2 of FTM0 */

  InitDMADriver(); /* initialize DMA driver */
}

Передача Биты DMA

Пока у меня все настроено

  • Таймер FTM генерирует необходимые сигналы с включенным запуском DMA
  • GPIO для DIN на светодиод готов
  • Драйвер eDMA инициализирован

Теперь я могу начать передачу DMA и использую следующий метод:

void DMA_Transfer(uint8_t *transmitBuf, uint32_t nofBytes);

Помните, что у меня есть буфер с битами для светодиодов WS2812. Чтобы отправить биты в PTD0, я могу использовать

DMA_Transfer(transmitBuf, sizeof(transmitBuf));

DMA Transfer

Я собираюсь использовать три канала DMA, по одному на каждый канал таймера. Чтобы передать биты с DMA в DMA_Transfer (), я делаю следующее:

  1. Сброс FTM : сброс регистров таймера. FTM не синхронизируется в этот момент.
  2. DMA Muxing : запрос трех каналов DMA для каналов FTM0 1, 2 и 3
  3. Установить обратный вызов : установить обработчик прерывания «Конец передачи» для канала DMA 3. Таким образом, я получаю уведомление, когда передача всех битов закончена.
  4. Настройка DMA TCD : Настройка дескриптора управления передачей с источником / назначением для канала DMA.
  5. Запуск / включение всех каналов DMA : это включает / включает каналы DMA.
  6. Запустите FTM : инициализируйте флаг ‘dmaDone’ и включите часы для FTM, позволяя таймеру работать.
  7. Дождитесь завершения DMA : «прерывание конца передачи» установит флаг «dmaDone».
  8. Выключить FTM : убрать часы из таймера FTM.
  9. Отключить / остановить все  каналы DMA .
  10. Де-Mux  и де-установить каналы DMA.

: idea:  Вы можете задаться вопросом, почему я делаю Muxing и De-Muxing для каждой передачи (шаг 2 и 10)? Ответ (я полагаю), что есть внутренние задержки распространения внутри контроллера DMA. Смешивание и демультиплексирование DMA гарантирует, что контроллер DMA сбрасывает свои внутренние регистры. Мне пришлось усвоить этот трудный путь: DMA прекрасно работал на более низкой скорости (скажем, частоты DMA 1 мс), так как было достаточно времени и тактов внутри модуля, чтобы привести его в правильное состояние. Но использование DMA во временной области sub μs, поскольку я использую его здесь, определенно показало некоторое странное поведение DMA с «призрачными» передачами DMA. У меня уже были такие странные вещи на FRDM-KL25Z, см. « NeoShield: WS2812 RGB LED Shield с DMA и nRF24L01 + ».

Ниже приводится полная рутина, я буду обсуждать некоторые детали

/* DMA related */
#define NOF_EDMA_CHANNELS  3 /* using three DMA channels */
static edma_chn_state_t chnStates[NOF_EDMA_CHANNELS]; /* array of DMA channel states */
static volatile bool dmaDone = false; /* set by DMA complete interrupt on DMA channel 3 */
static const uint8_t OneValue = 0xFF; /* value to clear or set the port bits */

void DMA_Transfer(uint8_t *transmitBuf, uint32_t nofBytes) {
  edma_transfer_config_t config;
  uint8_t channel;
  uint8_t res;

  ResetFTM(FTM0_IDX); /* clear FTFM and prepare for DMA */

  /* DMA Muxing: Allocate EDMA channel request trough DMAMUX */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    res = EDMA_DRV_RequestChannel(channel, kDmaRequestMux0FTM0Channel0+channel, &amp;amp;chnStates[channel]);
    if (res==kEDMAInvalidChannel) { /* check error code */
      for(;;); /* ups!?! */
    }
  }
  /* Install callback for eDMA handler on last channel which is channel 2 */
  EDMA_DRV_InstallCallback(&amp;amp;chnStates[NOF_EDMA_CHANNELS-1], EDMA_Callback, NULL);

  /* prepare DMA configuration */
  config.srcLastAddrAdjust = 0; /* no address adjustment needed after last transfer */
  config.destLastAddrAdjust = 0; /* no address adjustment needed after last transfer */
  config.srcModulo = kEDMAModuloDisable; /* no address modulo (no ring buffer) */
  config.destModulo = kEDMAModuloDisable; /* no address modulo (no ring buffer) */
  config.srcTransferSize = kEDMATransferSize_1Bytes; /* transmitting one byte in each DMA transfer */
  config.destTransferSize = kEDMATransferSize_1Bytes; /* transmitting one byte in each DMA transfer */
  config.minorLoopCount = 1; /* one byte transmitted for each request */
  config.majorLoopCount = nofBytes; /* total number of bytes to send */
  config.destOffset = 0; /* do not increment destination address */

  config.srcAddr = (uint32_t)&amp;amp;OneValue; /* Bit set */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PSOR_REG(PTD_BASE_PTR); /* Port Set Output register */
  config.srcOffset = 0; /* do not increment source address */
  PushDMADescriptor(&amp;amp;config, &amp;amp;chnStates[0], false); /* write configuration to DMA channel 0 */

  config.srcAddr = (uint32_t)transmitBuf; /* pointer to data */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PDOR_REG(PTD_BASE_PTR); /* Port Data Output register */
  config.srcOffset = 1; /* do not increment source address */
  PushDMADescriptor(&amp;amp;config, &amp;amp;chnStates[1], false); /* write configuration to DMA channel 1 */

  config.srcAddr = (uint32_t)&amp;amp;OneValue; /* Bit set */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PCOR_REG(PTD_BASE_PTR); /* Port Clear Output register */
  config.srcOffset = 0; /* do not increment source address */
  PushDMADescriptor(&amp;amp;config, &amp;amp;chnStates[2], true); /* write configuration to DMA channel 1 */

  /* enable the DMA channels */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    EDMA_DRV_StartChannel(&amp;amp;chnStates[channel]); /* enable DMA */
  }
  dmaDone = false; /* reset done flag */
  StartStopFTM(FTM0_IDX, true); /* start FTM timer to fire sequence of DMA transfers */
  do {
    /* wait until transfer is complete */
  } while(!dmaDone);
  StopFTMDMA(FTM0_IDX); /* stop FTM DMA tranfers */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    EDMA_DRV_StopChannel(&amp;amp;chnStates[channel]); /* stop DMA channel */
  }
  /* Release EDMA channel request trough DMAMUX, otherwise events might still be latched! */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    res = EDMA_DRV_ReleaseChannel(&amp;amp;chnStates[channel]);
    if (res!=kStatus_EDMA_Success) { /* check error code */
      for(;;); /* ups!?! */
    }
  }
}

Одной из важных частей является конфигурация TCD (дескриптор управления передачей). Я установил три дескриптора, по одному для каждого канала DMA:

  1. Канал 0: запись «1» в регистр PSOR (выход набора портов).
  2. Канал 1: Запись бита данных в регистр PDOR (вывод данных порта).
  3. Канал 2: Запись «1» в регистр PCOR (Port Clear Output).

Дескрипторы имеют несколько полей для настройки передачи DMA. В основном, что я описываю для передач DMA, это «взять этот байт с этого адреса источника и записать его по этому адресу назначения». Кроме того, я указываю «сколько байтов для чтения / записи» и нужно ли выполнять некоторые вычисления адреса для адреса источника и назначения. В следующих разделах я объясню различные настройки:

В eDMA можно сделать специальную настройку в конце последней передачи: поскольку мне это не нужно для WS2812, эта настройка имеет нулевое смещение:

config.srcLastAddrAdjust = 0; /* no address adjustment needed after last transfer */
config.destLastAddrAdjust = 0; /* no address adjustment needed after last transfer */

Вычисление адреса DMA может быть сконфигурировано для «обтекания», например, если используется кольцевой буфер: у меня он отключен, так как мне не нужны эти функции:

config.srcModulo = kEDMAModuloDisable; /* no address modulo (no ring buffer) */
config.destModulo = kEDMAModuloDisable; /* no address modulo (no ring buffer) */

Следующий параметр должен указать, сколько байтов должно быть передано за одну передачу DMA: мне нужно только записать один байт в порт GPIO:

config.srcTransferSize = kEDMATransferSize_1Bytes; /* transmitting one byte in each DMA transfer */
config.destTransferSize = kEDMATransferSize_1Bytes; /* transmitting one byte in each DMA transfer */

В следующей настройке я могу указать циклы ‘minor’ и ‘major’: таким образом я могу ‘вкладывать’ операции DMA:

Многоцикловая интеграция eDMA

Многоцикловая интеграция eDMA

В моем случае мне нужно только написать один байт для каждого запроса DMA, поэтому второстепенный счетчик цикла равен «1». Тем не менее, мне нужно записать несколько байтов для операции DMA (чтобы записать все байты для forwardBuf [], поэтому majorLoopCount — это общее количество байтов:

  config.minorLoopCount = 1; /* one byte transmitted for each request */
  config.majorLoopCount = nofBytes; /* total number of bytes to send */

Следующая настройка — указать, что должно происходить с адресом назначения. Адрес назначения будет адресом порта GPIO, поэтому менять его не нужно.

  config.destOffset = 0; /* do not increment destination address */

Вышеуказанные настройки одинаковы для всех трех каналов DMA. Ниже приведены специальные настройки, которые будут использоваться для каждого канала DMA.

Нулевой канал ДНК создаст передний фронт сигнала DIN WS2812. Чтобы быть выполненным процессором, я написал бы это так:

static const uint8_t OneValue = 0x01; /* value to clear or set the port bits */

GPIO_PSOR_REG(PTD_BASE_PTR) = OneValue:

В переводе на дескриптор DMA это так:

  config.srcAddr = (uint32_t)&amp;amp;OneValue; /* Bit set */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PSOR_REG(PTD_BASE_PTR); /* Port Set Output register */
  config.srcOffset = 0; /* do not increment source address */

Далее идет канал DMA 1, который запишет бит данных. В «нормальном» коде это было бы так:

static const uint8_t OneValue = 0x01; /* value to clear or set the port bits */

GPIO_PDOR_REG(PTD_BASE_PTR) = *transmitBuf; transmitBuf++;

В «языке DMA» это так:

  config.srcAddr = (uint32_t)transmitBuf; /* pointer to data */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PDOR_REG(PTD_BASE_PTR); /* Port Data Output register */
  config.srcOffset = 1; /* increment source address */

Наконец, как и для канала DMA 0, канал 2 записывает единицу в регистр GPIO:

  config.srcAddr = (uint32_t)&amp;amp;OneValue; /* Bit set */
  config.destAddr = (uint32_t)&amp;amp;GPIO_PCOR_REG(PTD_BASE_PTR); /* Port Clear Output register */
  config.srcOffset = 0; /* do not increment source address */

Каждый из дескрипторов записывается в аппаратные регистры с помощью этой пользовательской процедуры:

Вы заметили эту переменную temp [2]? Это необходимо для выравнивания TCD с 32-байтовой границей. Если адрес TCD не выровнен по этой границе, произойдет серьезная ошибка

static void PushDMADescriptor(edma_transfer_config_t *config, edma_chn_state_t *chn, bool enableInt) {
  /* If only one TCD is required, only hardware TCD is required and user
   * is not required to prepare the software TCD memory. */
  edma_software_tcd_t temp[2]; /* make it larger so we can have a 32byte aligned address into it */
  edma_software_tcd_t *tempTCD = STCD_ADDR(temp); /* ensure that we have a 32byte aligned address */

  memset((void*) tempTCD, 0, sizeof(edma_software_tcd_t)); /* initialize temporary descriptor with zeros */
  EDMA_DRV_PrepareDescriptorTransfer(chn, tempTCD, config, enableInt, true); /* prepare and copy descriptor into temporary one */
  EDMA_DRV_PushDescriptorToReg(chn, tempTCD); /* write EDMA registers */
}

: idea:  ПРЕДУПРЕЖДЕНИЕ: функция EDMA_DRV_ConfigLoopTransfer () в Kinetis SDK v1.2 может создать серьезную ошибку, поскольку она не выполняет это специальное выравнивание.

Каналы DMA 0 и 1 настроены так, чтобы не создавать никаких прерываний. Только канал 2 настроен с третьим параметром для создания прерывания в конце «основной» итерации (когда передаются все байты):

  PushDMADescriptor(&amp;amp;config, &amp;amp;chnStates[2], true); /* write configuration to DMA channel 1, and enable 'end' interrupt for it */

Поэтому я должен добавить обработчик прерывания DMA на канале 2, иначе мое приложение окажется в необработанном прерывании. DMA2_IRQHandler () является обработчиком прерываний, а EDMA_DRV_IRQHandler () будет вызывать обратный вызов EDMA_Callback ():

/*! @brief Dma channel 2 ISR */
void DMA2_IRQHandler(void){
   EDMA_DRV_IRQHandler(2U); /* call SDK EDMA IRQ handler, this will call EDMA_Callback() */
}

void EDMA_Callback(void *param, edma_chn_status_t chanStatus) {
  (void)param; /* not used */
  (void)chanStatus; /* not used */
  dmaDone = true; /* set 'done' flag at the end of the major loop */
}

Этот обработчик я должен установить с

  /* Install callback for eDMA handler on last channel which is channel 2 */
  EDMA_DRV_InstallCallback(&amp;amp;chnStates[NOF_EDMA_CHANNELS-1], EDMA_Callback, NULL);

Со всеми настройками TCD, переданными на каналы DMA, пришло время включить все каналы:

  /* enable the DMA channels */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    EDMA_DRV_StartChannel(&amp;amp;chnStates[channel]); /* enable DMA */
  }

Затем я сбрасываю флаг «Готово», запускаю таймер FTM и жду, пока передача не будет завершена:

  dmaDone = false; /* reset done flag */
  StartStopFTM(FTM0_IDX, true); /* start FTM timer to fire sequence of DMA transfers */
  do {
    /* wait until transfer is complete */
  } while(!dmaDone);

После того, как все байты отправлены, я останавливаю таймер FTM, отключаю каналы и освобождаю каналы DMA:

  StopFTMDMA(FTM0_IDX); /* stop FTM DMA transfers */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    EDMA_DRV_StopChannel(&amp;amp;chnStates[channel]); /* stop DMA channel */
  }
  /* Release EDMA channel request trough DMAMUX, otherwise events might still be latched! */
  for (channel=0; channel&amp;lt;NOF_EDMA_CHANNELS; channel++) {
    res = EDMA_DRV_ReleaseChannel(&amp;amp;chnStates[channel]);
    if (res!=kStatus_EDMA_Success) { /* check error code */
      for(;;); /* ups!?! */
    }
  }

Это завершает передачу DMA, и все может начаться заново со следующей передачей.

«Замечательные и красочные вещи»

Время попробовать. Следующая программа записывает три пикселя WS2812B: зеленый, красный и синий:

#include &amp;quot;fsl_device_registers.h&amp;quot;
#include &amp;quot;DMAPixel.h&amp;quot;

#define NEO_NOF_PIXEL       3
#define NEO_NOF_BITS_PIXEL 24
static uint8_t transmitBuf[NEO_NOF_PIXEL*NEO_NOF_BITS_PIXEL] =
    {
        /* pixel 0: */
        1, 1, 1, 1, 1, 1, 1, 1, /* green */
        0, 0, 0, 0, 0, 0, 0, 0, /* red */
        0, 0, 0, 0, 0, 0, 0, 0, /* blue */
        /* pixel 1: */
        0, 0, 0, 0, 0, 0, 0, 0, /* green */
        1, 1, 1, 1, 1, 1, 1, 1, /* red */
        0, 0, 0, 0, 0, 0, 0, 0,  /* blue */
        /* pixel 0: */
        0, 0, 0, 0, 0, 0, 0, 0, /* green */
        0, 0, 0, 0, 0, 0, 0, 0, /* red */
        1, 1, 1, 1, 1, 1, 1, 1  /* blue */
    };

int main(void) {
  uint8_t red, green, blue;

  DMA_Init();
  for (;;) {
    DMA_Transfer(transmitBuf, sizeof(transmitBuf));
  }
  /* Never leave main */
  return 0;
}

Проверяя с помощью логического анализатора, я вижу, что для отправки данных требуется 91,1 мкс:

Сроки передачи данных для трех WS2812

Сроки передачи данных для трех WS2812

Следующие увеличения в первые 8 отправленных битов (зеленого цвета):

первые 8 зеленых битов

первые 8 зеленых битов

Я также вижу задержку между событием таймера / DMA и временем, пока бит порта фактически не изменился: он составляет около 0,2 мкс:

Задержка DMA в GPIO

Задержка DMA в GPIO

Но сроки для битов «1» и «0» находятся в пределах спецификации :-):

WS2812 Бит 1 Сроки

WS2812 Бит 1 Сроки

WS2812 Бит 0 Сроки

WS2812 Бит 0 Сроки

И вуаля, это то, что я получаю на матрице NeoPixel: первые три светодиода: зеленый, красный и синий :-):

Красные, зеленые и синие цветные пиксели

Красные, зеленые и синие цветные пиксели

Резюме

Теперь у меня есть FTM с работающей DMA, и он немного отрывается от порта GPIO в одну или несколько линий. Сейчас я использую только одну полосу, но она работает одинаково с несколькими полосами. Благодаря 128 Кбайт оперативной памяти количество WS2812 пикселей, которые я могу использовать, огромно: мне нужно 24 байта на пиксель, если я использую одну дорожку. Таким образом, для матрицы 8 × 8 мне нужно 1536 байт, но если я использую восемь плат 8 × 8 с 8 дорожками (PTD0 до PTD7), мне нужно только 3 байта на пиксель: тоже 1536 байт  🙂

: idea:  Я мог бы упаковать все 24 бита для пикселя в три байта, а затем сделать многоступенчатую передачу DMA: распаковать биты и отправить их в порт. Я не думал об этом, но, возможно, это было бы чем-то выполнимым, чтобы уменьшить объем оперативной памяти, необходимой для конфигурации с одной полосой.

В этом проекте используется DMA на устройстве Freescale Kinetis, и я изо всех сил пытался объяснить используемый здесь подход. Тем не менее, есть гораздо больше возможностей и возможностей с DMA. Требуется некоторое время, чтобы ознакомиться с DMA, но возможности потрясающие  🙂 .

Мне пришлось использовать смесь макросов доступа Freescale Kinetis SDK API, SDK HAL API и CMSIS. Freescale продвигает Kinetis SDK, но этот проект еще раз подтвердил мне, что один SDK не покрывает все потребности разработки встроенных приложений: мне все еще нужен API доступа к реестру CMSIS. С другой стороны: в SDK есть несколько приятных подпрограмм и особенно уровень HAL, который облегчает использование. Но опять же, как и во всем: требуется время, чтобы изучить все эти вещи. И я надеюсь, что эта серия статей поможет вам в этом процессе обучения.

Источники проекта находятся на GitHub здесь:
https://github.com/ErichStyger/mcuoneclipse/tree/master/Examples/KDS/FRDM-K64F120M/FRDM-K64F_NeoPixel_SDK

Итак, что может быть дальше? Я мог бы описать / разработать «графический» драйвер для пикселей WS2812? Или, может быть, это то, что я оставляю Мане? Оставляйте комментарии и дайте мне знать, что вы думаете  🙂 .

Счастливого DMAing  🙂