======Работа DMA по передаче и приему SPI в 1986ВЕ1Т====== Данная статья посвящена развитию примера из статьи [[prog:dma:dma_spi_rx|"Работа DMA по приему SPI в 1986ВЕ92У"]], но для разнообразия реализована для МК 1986ВЕ1Т. В новом примере не только данные с SPI считываются по DMA, но и запись в SPI здесь реализована через DMA. Напомню, что в прошлом примере мы сами в цикле писали данные в SPI. Целиком код проекта представлен на [[https://github.com/StartMilandr/6.2-DMA_SPI_TXRX|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: - цикл передачи данных, канал DMA_CH_SPI_TX - цикл приема данных, канал 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.