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

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


prog:dma:dma_spi_rx

Работа DMA по приему SPI в 1986ВЕ92У

Рассмотрим пример считывания принимаемых данных из SPI в свой буфер на примере МК 1986ВЕ92У.

Функционал такой:

  1. Заполняем массив SrcBuf любыми данными, приемный массив DestBuf заполняем нулями.
  2. В цикле выводим массив SrcBuf в линию SPI (запись в регистр SSPx→DR).
  3. Поскольку SPI при выдаче на линию каждого бита (линия TX по фронту CLK) считывает тут же принимаемый бит (линия RX по спаду CLK), то выдача слова приводит к приему слова.
  4. При приеме слова вырабатывается запрос к DMA, который считывает принятое слово и записывает его в массив DestBuf.
  5. DMA настраивается на передачу количества слов, равного количеству слов в массивах SrcBuf и DestBuf. Поэтому при приеме всего массива DestBuf произойдет завершение цикла DMA и вызовется прерывание DMA.
  6. В прерывании DMA мы ставим флаг окончания передачи всех данных. Основной цикл после записи массива в SPI ожидает этот флаг, чтобы сравнить данные в SrcBuf и DestBuf. (Прерывание заканчиваем переинициализацией канала DMA или его выключением - вариант выбирается макроопределением при условной компиляции.)
  7. В основном цикле данные сравниваются и статус выводится на светодиоды:
    1. VD3 - переключается, если данные совпали;
    2. VD4 - включается, если данные не совпали.
  8. После этого выдерживается некоторая задержка для визуального восприятия статуса на светодиодах, и тест передачи повторяется заново. Перед новым циклом светодиод ошибки VD4 выключается.

Таким образом, если передача данных проходит успешно, то мигает светодиод VD3. Если при передаче данные не совпали, то мигает светодиод VD4.

Поскольку мы принимаем то, что сами передаем, то удобно замкнуть линии передатчика и приемника. Либо использовать режим тестирования по шлейфу, когда такое подключение реализуется внутри кристалла. Будем использовать второй вариант.

Целиком код проекта доступен на GitHub.

Библиотека BRD_SRC

При реализации примеров часто приходится писать одно и тоже. Чтобы этого избежать, я сложил часто используемые куски кода в некую "библиотеку", которую копирую в каждую директорию с демо-проектом и подключаю только необходимые части. "Куски кода" лежат в директории src_brd, описание этой библиотеки возможно появится позже.

Подключаемые файлы имеют префикс brd_. Функции, вызываемые из библиотеки src_brd, имеют префикс BRD_ и содержат минимум входных параметров. Надеюсь, что в перспективе внутренняя реализация этих функций может быть заменена на прямые вызовы к регистрам (минуя SPL), что позволит повысить быстродействие примеров. Если оставить оба варианта, то можно будет с помощью условной компиляции выбирать SPL или регистровую реализацию для одного и того же проекта. Для этого конфигурационная часть вынесена в отдельные глобальные переменные (файлы с суффиксом _Select.h), которые подключаются для задания конкретной необходимой конфигурации.

Реализация библиотеки включает в себя "адаптацию" кода под различные микроконтроллеры и отладочные платы. Это позволяет писать один код примера и использовать его в проектах под различные МК. Различия под МК реализованы в функциях, а "ресурсы МК" и демоплат сведены в файлы:

  • brdDef_VE1.h,
  • brdDef_VE3.h,
  • brdDef_VE91.h,
  • brdDef_VE91.h
  • и т.д.

Под ресурсами МК подразумевается выбор портов, пинов, клоков для светодиодов, кнопок, внешней шины, DMA, SPI и т.д.

Конкретный файл подключается через файл brdDef.h, выбор в котором идет через макроопределение МК

#include "brdSelect.h"

#if defined ( USE_BOARD_VE_91 )  || defined ( USE_BOARD_VE_94 )
  #include "brdDef_VE91.h"
   
#elif defined ( USE_BOARD_VE_92 )
  #include "brdDef_VE92.h"
...

#elif defined ( USE_BOARD_VE_1 )
  #include "brdDef_VE1.h"
...

#elif defined ( USE_BOARD_VE_8 )
  #include "brdDef_VE8.h"

#endif

Само же определение микроконтроллера для проекта задается в brdSelect.h.

// Select one of the Demo boards below for project:

// #define  USE_BOARD_VE_91
// #define USE_BOARD_VE_94

 #define USE_BOARD_VE_92 

// #define USE_BOARD_VE_93
// #define USE_BOARD_VE_1 
// #define USE_BOARD_VE_3 
// #define USE_BOARD_VE_4 
// #define USE_BOARD_VE_8 

Для данного проекта файл brdSelect.h лежит в директории проекта и активирует определения ресурсов для 1986ВЕ92У. Для сборки примера, например, для 1986ВЕ1Т, необходимо создать проект под 1986ВЕ1Т и в директорию проекта положить файл с активным #define USE_BOARD_VE_1. В проект затем подключить файл main.c и указать пути компилятору к

  1. файлу main.c
  2. внутренностям brd_src
  3. текущему проекту (для доступа к brdSelect.h)

Реализация Main

В принципе, код говорит сам за себя, приведу лишь некоторые комментарии. Содержимое main.c:

Подключаем функции библиотеки для настройки периферии:

#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
#define DMA_CHANNEL  DMA_Channel_SSP2_RX

//  Массивы для данных
#define  DATA_COUNT  10
uint32_t DestBuf[DATA_COUNT];
uint32_t SrcBuf[DATA_COUNT];

//  Флаг окончания цикла DMA, выставляется в прерывании от DMA. 
//  Ожидается в основном цикле, для начала сравнения данных.
uint32_t DMA_Completed = 0;

Как указывалось ранее в статье "Начальные сведения о DMA", при окончании цикла DMA канал DMA необходимо либо выключить, либо переинициализировать на новый цикл. Выбор варианта в примере задается определением DMA_USE_CASE_DIS. Если определение активно, то выберется вариант с полной переинициализацией. Это долго. В текущем варианте определение DMA_USE_CASE_DIS закомментировано, т.е. используется вариант с переинициализацией цикла.

  //  Выбор варианта работы с DMA
//#define DMA_USE_CASE_DIS

Настройка периферии:

int main(void)
{
  uint32_t i;      //  счетчик для цикла
  uint32_t result; //  результат сравнения данных
  
  //  Включение тактирования ядра 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 --------------------
  // Настройки DMA в глобальной переменной DMA_ChanCtrl, файл brdDMA_Select.h
  // Брать данные из регистра SPI->DR, класть в DestBuf, количество DATA_COUNT
  DMA_DataCtrl_Pri.DMA_SourceBaseAddr = (uint32_t)&pBRD_SPIx->SPIx->DR;
  DMA_DataCtrl_Pri.DMA_DestBaseAddr   = (uint32_t)&DestBuf;
  DMA_DataCtrl_Pri.DMA_CycleSize      = DATA_COUNT;
  
  //  Настройка и включение канала DMA (DMA_Channel_SSP2_RX)
  //  Канал начинает ждать запросы на передачу от SSP2_RX
  BRD_DMA_Init(DMA_CHANNEL, &DMA_ChanCtrl);
  
  //  Включение запросов от SSP2_RX к DMA
  //  Запросы начнут поступать при приеме новых данных
  SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_RXE, ENABLE);

Основной цикл:

  while (1)
  {  
  //---------  Выполнение цикла обмена ----
    // Выключаем статус ошибки на светодиодах если она была
    BRD_LED_Set(LED_ERR, 0);
    
    // Заполняем массив данных на отправку по SPI, зануляем приемный массив
    for (i = 0; i < DATA_COUNT; i++)
    {
      DestBuf[i] = 0;
      SrcBuf[i]  = i + 1; // не важно чем, фантазии хватило на индексы...
    }
    
    //  Выводим массив данных в SPI
    //  Функция BRD_SPI_Master_WR() выводит данные.
    //  SPI_RX вызывает запрос к DMA и считанное значение пересылается каналом DMA в буфер DestBuf.
    for (i = 0; i < DATA_COUNT; i++)
    {
      BRD_SPI_Master_WR(pBRD_SPIx, SrcBuf[i]);
    }

    //  Дожидаемся окончания цикла DMA, флаг выставиться в прерывании от DMA.
    while (!DMA_Completed);
    
    //  Сверяем полученные данные с посылаемыми
    result = 1;
    for (i = 0; i < DATA_COUNT; i++)
      if (DestBuf[i] != SrcBuf[i])
        result = 0;
        
    //  Отображаем статус сравнения
    //  В случае успеха переключается светодиод LED_OK
    //  При возникновении ошибки загорится светодиод LED_ERR
    if (result)  
      BRD_LED_Switch(LED_OK);
    else
      BRD_LED_Set(LED_ERR, 1);      
    
  //---------  Подготовка следующего цикла обмена ----
    //  Сбрасываем статус окончания цикла DMA
    DMA_Completed = 0;

    //  Включение канала DMA, если мы ранее выключили его в прерывании от DMA
#ifdef DMA_USE_CASE_DIS
    DMA_Init(DMA_CHANNEL, &DMA_ChanCtrl);
    DMA_Cmd(DMA_CHANNEL, ENABLE);
    SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_RXE, ENABLE);
#endif    
  
  //---------  Задержка перед следующим циклом ----  
    //  Задержка для наблюдения индикации на светодиодах
    //  Иначе из-за высокой скорости работы было бы не видно, что светодиоды переключаются.
    Delay(DELAY_TICKS);   
  }

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

В коде обработчика прерывания DMA важно настроить следующий цикл DMA либо выключить канал. В текущем примере используется переинициализация канала. Это быстрее, чем выключать - включать канал, но тоже не очень удачно. На самом деле необходимо переинициализировать только управляющее слово в канале (там, где выставился режим STOP). Этот вариант будет рассмотрен в статье "Вывод сигнала в DAC по DMA в 1986ВЕ1Т и 1986ВЕ92У". Там задержка на переинициализацию критична, и необходимо ее сократить.

В данном примере время не критично, поэтому код такой:

void DMA_IRQHandler (void)
{
 // Чтобы повторных прерываний не происходило
#ifdef DMA_USE_CASE_DIS
  // Выключение запросов от SPI к DMA
  // Выключение канала DMA  
  SSP_DMACmd(pBRD_SPIx->SPIx, SSP_DMA_RXE, DISABLE);
  DMA_Cmd(DMA_CHANNEL, DISABLE);
#else  
  //  Переинициализация цикла DMA
  //  Канал ждет запросов на передачу от SPI
  DMA_Init(DMA_CHANNEL, &DMA_ChanCtrl);  
#endif  
  
  //  Выставляем флаг окончания цикла DMA, 
  //  Все данные переданы, можно сравнивать
  DMA_Completed = 1;
  
  //  Сброс отложенных прерываний
  //  если запросы на прерывание защелкнулись пока выполнялся этот обработчик
  NVIC_ClearPendingIRQ (DMA_IRQn);
}	
prog/dma/dma_spi_rx.txt · Последние изменения: 2018/05/07 15:50 — vasco