======Работа 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.