Данная статья посвящена развитию примера из статьи "Работа DMA по приему SPI в 1986ВЕ92У", но для разнообразия реализована для МК 1986ВЕ1Т. В новом примере не только данные с SPI считываются по DMA, но и запись в SPI здесь реализована через DMA. Напомню, что в прошлом примере мы сами в цикле писали данные в SPI.
Целиком код проекта представлен на GitHub. (Добавлена реализация для 1986ВЕ3Т и 1986ВЕ92У.)
Перейдем сразу к коду и комментариям. Для простоты реализации примера, снова используем функции из поддиректории brd_src.
Первая часть такая-же как в предыдущем примере:
#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, принимающие запросы на прием и передачу #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;
Начальная часть инициализации полностью совпадает с предыдущим примером. Используется тестовый режим работы по шлейфу 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);
При записи в регистр 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:
Если в прерывании канал не выключить, то прерывание будет генерироваться постоянно. Поэтому, в прерывании мы опрашиваем контрольное слово управляющей структуры канала, и если канал закончил работу (выставился режим 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.