Инструменты пользователя

Инструменты сайта


prog:dma:dma_spi_txrx

Работа DMA по передаче и приему SPI в 1986ВЕ1Т

Данная статья посвящена развитию примера из статьи "Работа DMA по приему SPI в 1986ВЕ92У", но для разнообразия реализована для МК 1986ВЕ1Т. В новом примере не только данные с SPI считываются по DMA, но и запись в SPI здесь реализована через DMA. Напомню, что в прошлом примере мы сами в цикле писали данные в SPI.

Целиком код проекта представлен на GitHub. (Добавлена реализация для 1986ВЕ3Т и 1986ВЕ92У.)

Реализация Main

Перейдем сразу к коду и комментариям. Для простоты реализации примера, снова используем функции из поддиректории brd_src.

Incudes и определения

Первая часть такая-же как в предыдущем примере:

#include "brdClock.h"   // тактирование CPU
#include "brdLed.h"     // управление светодиодами
#include "brdUtils.h"   // функция Delay()
#include "brdSPI.h"     // управление SPI
#include "brdDMA.h"     // управление DMA

//  Определения для отладочной платы выбранной в "brdSelect.h"
#include "brdDef.h"
//  Выбор SPI и настроек - модифицируется под реализацию
#include "brdSPI_Select.h"
//  Выбор настроек DMA - модифицируется под реализацию
#include "brdDMA_Select.h"

//  Светодиоды для отображения статуса. 
//  BRD_LED_х - определены для демоплаты каждого МК в библиотеке brd_src.
#define LED_OK    BRD_LED_1
#define LED_ERR   BRD_LED_2

//  Задержка для восприятия статуса на светодиодах
#define DELAY_TICKS     4000000

//  Используется режим мастера для SPI
#define SPI_MASTER_MODE 1

Буферы и скаттер-файл

С буферами для работы DMA, в случае 1986ВЕ1Т, дело несколько сложнее. По спецификации только память из региона IRAM2 является исполнимой. Только из этой памяти, начинающейся с адреса 0х2010_0000 возможно исполнение инструкций. По спецификации также, только к этой области имеет доступ контроллер DMA. Поэтому массивы, которые в прошлом примере могли быть расположены линкером где угодно, сейчас необходимо обязательно разместить в памяти IRAM2.

Это делается через скаттер файл. Подробно про это уже писалось на сайте, найти информацию можно поиском. Файл DMA_SPI_TXRX.sct подключен в проекте. В отличие от файла создаваемого по умолчанию, добавлена строка с аттрибутом EXECUTABLE_MEMORY_SECTION.

  RW_IRAM2 0x20100000 0x00004000  {
   *.o (EXECUTABLE_MEMORY_SECTION)
   .ANY (+RW +ZI)
  }

(Для того чтобы в каждом проекте не заниматься модификацией скаттер файла, я скопировал его в директорию brd_src и поменял расширение. Расширение изменено затем, чтобы почтовые службы не ругались на файл с расширением *.sct. В последнее время архивы проектов с данным файлом не проходят через Gmain (и сотоварищей) из-за соображений безопасности. Файл назван ScatDMA_VE1.txt. Файл был добавлен позже, поэтому в данном примере не используется. Но в будущем достаточно выбрать этот файл в настройках проекта, чтобы память с аттрибутом EXECUTABLE_MEMORY_SECTION располагалась в исполнимой области ОЗУ IRAM2.)

//  DMA работает только с ОЗУ IRAM2 (с адреса 0x20100000) - прописать в Objects\DMA_SPI_TXRX.sct 
//  так:
//  RW_IRAM2 0x20100000 0x00004000  {
//   *.o (EXECUTABLE_MEMORY_SECTION)
//   .ANY (+RW +ZI)
//  }
//  В опциях линкера убрать галочку "Use Memory Layout from Debug" !
#define  DATA_COUNT  10
uint32_t DestBuf[DATA_COUNT] __attribute__((section("EXECUTABLE_MEMORY_SECTION")));
uint32_t SrcBuf[DATA_COUNT]  __attribute__((section("EXECUTABLE_MEMORY_SECTION")));

Переменные DMA и доступ к управляющим структурам DMA

Для работы нам потребуется два канала DMA (на прием и передачу) и доступ к управляющей структуре канала. Точнее только к контрольному слову. По этому слову мы будем в обработчике прерывания узнавать, от какого канала возникло прерывание.

// Два канала DMA, принимающие запросы на прием и передачу
#define DMA_CH_SPI_TX  DMA_Channel_REQ_SSP1_TX
#define DMA_CH_SPI_RX  DMA_Channel_REQ_SSP1_RX

//  Структуры инициализации каналов, сюда задаем параметры для настройки обмена DMA 
DMA_CtrlDataInitTypeDef DMA_DATA_TX;
DMA_CtrlDataInitTypeDef DMA_DATA_RX;
DMA_ChannelInitTypeDef  DMA_TX;
DMA_ChannelInitTypeDef  DMA_RX;

//  Доступ к таблице управляющих структур каналов, основных и альтернативных
//  Сама структура объявлена и используется в библиотечном файле MDR32F9Qx_dma.c
extern DMA_CtrlDataTypeDef DMA_ControlTable[DMA_Channels_Number * (1 + DMA_AlternateData)];

//  Глобальные переменные для сообщений между основным циклом и обработчиком прерываний DMA.
uint32_t DMA_Completed = 0;
uint32_t TX_Started = 0;

Начальная инициализация - LED и SPI

Начальная часть инициализации полностью совпадает с предыдущим примером. Используется тестовый режим работы по шлейфу LBМ - блок принимает то, что сам же и передает.

int main(void)
{
  uint32_t i;
  uint32_t errCnt;

  //  Включение тактирования ядра 80МГц
  BRD_Clock_Init_HSE_PLL(RST_CLK_CPU_PLLmul10);
  
  //  Инициализация всех светодиодов на плате - тактирование, порты, пины.
  BRD_LEDs_Init();
  
  // ---------------   SPI --------------------
  //  Выбор конкретного SPI, его параметров, портов и пинов в глобальной переменной pBRD_SPIx
  //  см. файл brdSPI_Select.h
  
  //  Настройка выводов SPI - тактирование, порты, пины.
  //  В тестовом режиме SPI выводы не используются, в реальном включении - раскомментировать!
  // BRD_SPI_PortInit(pBRD_SPIx);

  //  Инициализация SPI
  BRD_SPI_Init(pBRD_SPIx, SPI_MASTER_MODE); 

Начальная инициализация - каналы DMA

При записи в регистр SPIx→DR данные попадают в FIFO_TX блока SPI, а при чтении этого же регистра SPIx→DR данные считываются из FIFO_RX блока SPI. Это так называемая двухпортовая память. Поэтому канал DMA_CH_SPI_TX настраивается на запись в регистр SPIx→DR, а канал DMA_CH_SPI_RX - на считывание SPIx→DR.

  // Присваиваем настройки по умолчанию для каналов DMA, определены в brdDMA_Select.h
  DMA_DATA_TX = DMA_DataCtrl_Pri;
  DMA_DATA_RX = DMA_DataCtrl_Pri;
  DMA_TX = DMA_ChanCtrl;
  DMA_RX = DMA_ChanCtrl;  

  //  Общая настройка блока DMA - включение тактирования, DeInit.
  BRD_DMA_Init();
  
  //  ----- TX: Настройка канала на выдачу данных -----
  //    Данные из SrcBuf выводить в регистр SPIx->DR, количество DATA_COUNT
  //    Адрес источника данных инкрементировать словами (следующее значение).
  //    Адрес регистра SPIx->DR постоянен, инкрементировать нельзя
  DMA_DATA_TX.DMA_SourceBaseAddr = (uint32_t)&SrcBuf;  
  DMA_DATA_TX.DMA_DestBaseAddr   = (uint32_t)&pBRD_SPIx->SPIx->DR;
  DMA_DATA_TX.DMA_SourceIncSize  = DMA_SourceIncWord;  
  DMA_DATA_TX.DMA_DestIncSize    = DMA_DestIncNo;    
  DMA_DATA_TX.DMA_CycleSize      = DATA_COUNT;
  
  DMA_TX.DMA_PriCtrlData = &DMA_DATA_TX;
  DMA_TX.DMA_AltCtrlData = &DMA_DATA_TX;
  
  //  Включение канала передатчика
  //  Канал DMA начинает ждать запросы от SPI о наличии свободного места в FIFO_TX.
  BRD_DMA_Init_Channel(DMA_CH_SPI_TX, &DMA_TX);
  
  //  ----- RX: Настройка канала на выдачу данных -----
  //    Данные из регистр SPIx->DR выводить в DestBuf, количество DATA_COUNT
  //    Адрес регистра SPIx->DR постоянен, инкрементировать нельзя
  //    Адрес назначения данных инкрементировать словами (следующее значение).
  DMA_DATA_RX.DMA_SourceBaseAddr = (uint32_t)&pBRD_SPIx->SPIx->DR;  
  DMA_DATA_RX.DMA_DestBaseAddr   = (uint32_t)&DestBuf;
  DMA_DATA_RX.DMA_SourceIncSize  = DMA_SourceIncNo;  
  DMA_DATA_RX.DMA_DestIncSize    = DMA_DestIncWord;      
  DMA_DATA_RX.DMA_CycleSize      = DATA_COUNT;

  DMA_RX.DMA_PriCtrlData = &DMA_DATA_RX;
  DMA_RX.DMA_AltCtrlData = &DMA_DATA_RX;

  //  Включение канала передатчика
  //  Канал DMA начинает ждать запросы от SPI о наличии данных в FIFO_RX.
  BRD_DMA_Init_Channel(DMA_CH_SPI_RX, &DMA_RX);

Начало обмена и основной цикл

В основном цикле, как и в предыдущем примере, будем передавать по SPI массив данных и затем сверять полученные данные с отправленными. Статус обмена будет отражаться на светодиодах.

// ----- Запуск обмена -----
  //  Выставляем флаг начала обмена и сбрасываем флаг окончания приема DMA
  TX_Started = 1;
  DMA_Completed = 0;
  
  //  Подготавливаем массивы, записываем индексы в SrcBuf и зануляем данные в DestBuf.
  PrepareData();
  
  //  Разрешаем SPI формировать запросы к DMA
  //    SPI_TX - обнаружит, что в FIFO_TX есть свободное место 
  //             и будет запрашивать слова от канала DMA_TX, пока место не кончится.
  //    SPI_RX - при каждой пересылке слова обнаружит данные в FIFO_RX 
  //             и будет запрашивать канал DMA_RX их забрать.
  SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_TXE | SSP_DMA_RXE, ENABLE);
  
// ----- Основной цикл -----
  while (1)
  {  
    //  Ждем, пока все данные передадутся и в прерывании выставится флаг об окончании цикла DMA по приему.
    while (!DMA_Completed);
    
    //  Сверяем переданные и принятые данные
    errCnt = 0;
    for (i = 0; i < DATA_COUNT; i++)
      if (DestBuf[i] != SrcBuf[i])
        errCnt++;
    
    //  Если число не совпавших данных равно 0, то выводим статус
    //    LED_OK  зажигается при успехе
    //    LED_ERR зажигается при сбое в данных
    if (!errCnt)  
    {  
      BRD_LED_Switch(LED_OK);
      BRD_LED_Set(LED_ERR, 0);
    }  
    else
      BRD_LED_Set(LED_ERR, 1);
    
    //  Выжидаем паузу для восприятия индикации статуса человеком
    Delay(DELAY_TICKS);    
    
// ----- Запуск следующего цикла -----
    //  Снова выставляем флаги и подготавливаем массивы с данными
    TX_Started = 1;
    DMA_Completed = 0;    
    PrepareData();
        
    //  Разрешаем работу каналов DMA и запросов к ним со стороны SPI
    //    Каналы были выключен в прерывании от DMA.
    //    Вариант с переинициализацией каналов здесь не подходит, 
    //    потому что передатчик тут же начнет передавать следующие данные.
    DMA_Init(DMA_CH_SPI_TX, &DMA_TX);
    DMA_Init(DMA_CH_SPI_RX, &DMA_RX);
    DMA_Cmd(DMA_CH_SPI_TX, ENABLE);
    DMA_Cmd(DMA_CH_SPI_RX, ENABLE);
    SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_TXE | SSP_DMA_RXE, ENABLE);
  }
}

Функцию подготовки массивов данных PrepareData() опять используем простейшую. Приемный массив обнуляется, передаваемый массив заполняется индексами. При желании, можно внести разнообразие.

void PrepareData(void)
{
  uint32_t i;
  
  for (i = 0; i < DATA_COUNT; i++)
  {
    DestBuf[i] = 0;
    SrcBuf[i]  = i + 1;
  }  
} 

Обработчик прерывания DMA

Обработчик прерывания DMA сложнее, чем в прошлом примере. Теперь прерывание будет возникать при окончании двух циклов DMA:

  1. цикл передачи данных, канал DMA_CH_SPI_TX
  2. цикл приема данных, канал DMA_CH_SPI_RX

Если в прерывании канал не выключить, то прерывание будет генерироваться постоянно. Поэтому, в прерывании мы опрашиваем контрольное слово управляющей структуры канала, и если канал закончил работу (выставился режим STOP), то выключаем канал. Режим в контрольном слове занимает первые три бита, поэтому мы проверяем их на равенство нулю - т.е. значению STOP.

void DMA_IRQHandler (void)
{
  // Считываем контрольные слова управляющих структур каналов
  uint32_t DMA_Ctrl_Tx = DMA_ControlTable[DMA_CH_SPI_TX].DMA_Control;
  uint32_t DMA_Ctrl_Rx = DMA_ControlTable[DMA_CH_SPI_RX].DMA_Control;

  // Перывание от TX приходит первым - все данные переданы в FIFO TX
  // Выключаем DMA TX
  if (((DMA_Ctrl_Tx & 7) == 0) && TX_Started)   
  {
    //  Выключаем генерацию запросов к DMA со стороны SPI
    //  и выключаем канал DMA
    SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_TXE, DISABLE);
    DMA_Cmd(DMA_CH_SPI_TX, DISABLE);
    
    //  Сбрасываем флаг, чтобы в следующем прерывании не выполнять выключение канала снова.
    TX_Started = 0;
  }  

  // Перывание от RX приходит вторым - все данные приняты
  // Выключаем DMA RX 
  if ((DMA_Ctrl_Rx & 7) == 0)
  {
    //  Выключаем генерацию запросов к DMA со стороны SPI
    //  и выключаем канал DMA
    SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_RXE, DISABLE);
    DMA_Cmd(DMA_CH_SPI_RX, DISABLE);
    
    //  Выставляем флаг окончания обмена, 
    //  код в основном цикле начнет сравнивать данные и выводить статус
    DMA_Completed = 1;
  }    
  
  //  Сбрасываем отложенные прерывания, если запрос возник за время выполнения этого обработчика.    
  NVIC_ClearPendingIRQ (DMA_IRQn);
}	

В прошлом примере мы обсуждали, что вместо выключения канала можно переинициализировать его на выполнение следующего цикла. Для канала DMA_CH_SPI_RX это возможно, потому что запросы к DMA не начнутся, пока не стартует передача данных и не появятся данные в приемном FIFO_RX.

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

prog/dma/dma_spi_txrx.txt · Последнее изменение: 2022/04/03 23:09 (внешнее изменение)