[Home] [Donate!] [Контакты]

SPI. Пример. Передача данных с использованием DMA

Пример очень похож на предыдущий, здесь также управление устройством SPI2 происходит по прерываниям. Но в данном случае, для передачи данных устройством SPI1, используется DMA.

Оглавление
SPI в микроконтроллерах STM32. Основы
SPI в STM32. Работа в ведомом и ведущем режимах
SPI в STM32. Управление передачей данных
Регистры SPI
Использование SPI при работе с микроконтроллерами STM32F100xx
SPI. Пример. Передача данных с использованием DMA
Смотрите также примеры
SPI. Пример. Программный контроль передачи данных
SPI. Пример. Передача данных по прерываниям

Если сравнивать с предыдущим примером, то функция main здесь практически остаётся без изменений. Лишь дополнительно включается тактирование DMA1, а при настройке SPI разрешаются запросы DMA.

Все изменения сосредоточены внутри функции spi_send_receive, которая полностью переписана для осуществления передачи данных с помощью DMA. Реализуются такая передача весьма просто. Нужно лишь указать DMA, что передавать, куда и в каком количестве. После этого включаем используемые каналы DMA и ждём завершения передачи.

Желательно время, необходимое для передачи данных, потратить на какую-то полезную работу. Используемый здесь вариант

while(DMA1_Channel2->CNDTR!=0) {}

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

Далее приведён полный текст программы.

/*
File:   main.cpp

Простейший пример с использованием SPI:
передача данных между SPI1 и SPI2 микроконтроллера.
Использование DMA для передачи данных.
SPI1 настроим как ведущее, SPI2 - ведомое устройство.

MCU: STM32F100RB
SPI1:
    PA4 - NSS;
    PA5 - SCK;
    PA6 - MISO;
    PA7 - MOSI.

SPI2:
    PB12 - NSS;
    PB13 - SCK;
    PB14 - MISO;
    PB15 - MOSI.
Для теста одноимённые выводы обоих устройств SPI соединяем между собой.

DMA1:
    SPI1_RX - channel 2
    SPI1_TX - channel 3
*/

/**
*  IMPORTANT NOTE!
*  The symbol VECT_TAB_SRAM needs to be defined when building the project
*  if code has been located to RAM and interrupts are used...
*/

#include "stm32f10x.h"

// Используемые полярность и фаза тактового сигнала;
// должны быть одинаковыми и для ведущего, и для ведомого устройства.
const uint32_t CPOL=SPI_CR1_CPOL*0;
const uint32_t CPHA=SPI_CR1_CPHA*1;

// Тестовые данные для передачи и
// буферы для хранения полученных данных.

// Сообщение, передаваемое через SPI1.
uint8_t a1[]="Hello!";

// Сообщение, получаемое SPI1.
uint8_t b1[sizeof a1];

// Сообщение, передаваемое через SPI2 (при необходимости будет
// дополнено нулевыми байтами).
uint8_t a2[]="hi!";

// Количество уже переданных устройством SPI2 байт.
volatile unsigned int a2n=0;

// С помощью этой глобальной переменной, сообщаем обработчику
// прерывания от SPI2, сколько байт ему требуется передать.
unsigned int a2n_max=0;

// Буфер для сообщения, получаемого SPI2 (не помещающиеся в буфер
// данные будут отброшены).
const unsigned int b2_size=8;   // Размер буфера.
volatile uint8_t b2[b2_size];   // Буфер.
volatile unsigned int b2n=0;    // Текущее количество данных в буфере.

// Передача/получение заданного количества байт через SPI
// (используется DMA).
// Возвращает false, если предыдущая передача ещё не закончена,
// true в случае успеха.
bool spi_send_receive(
        SPI_TypeDef *spi,       // Используемое устройство SPI.
        const void *tx_buf,     // Буфер с передаваемыми данными.
        void *rx_buf,           // Буфер для принимаемых данных.
        uint16_t n              // Размер буфера (оба одинаковые).
        )
{
    if(n==0)
        return true;

    if(DMA1_Channel2->CNDTR!=0)
        return false;

    DMA1_Channel2->CCR&=0x80000000; // Сброс всех значимых битов.

    DMA1_Channel2->CMAR=(uint32_t)rx_buf;
    DMA1_Channel2->CPAR=(uint32_t)&SPI1->DR;
    DMA1_Channel2->CNDTR=n;

    DMA1_Channel2->CCR|=
            DMA_CCR2_MINC|
            DMA_CCR2_EN|
            (1<<8);             // PSIZE: 16 bits

    DMA1_Channel3->CCR&=0x80000000; // Сброс всех значимых битов.

    DMA1_Channel3->CMAR=(uint32_t)tx_buf;
    DMA1_Channel3->CPAR=(uint32_t)&SPI1->DR;
    DMA1_Channel3->CNDTR=n;

    DMA1_Channel3->CCR|=
            DMA_CCR3_MINC|
            DMA_CCR3_DIR|       // Mem ---> Periph.
            DMA_CCR3_EN|
            (1<<8);             // PSIZE: 16 bits
    return true;
}

// Обработчик прерываний от SPI2:
// выясняем причину прерывания и выполняем требуемые действия.
extern "C" void SPI2_IRQHandler()
{
    if(SPI2->SR&SPI_SR_TXE)     // Если установлен TXE флаг...
    {
        if(a2n<a2n_max)
        {
            SPI2->DR=(a2n<sizeof a2)?a2[a2n]:0;
            a2n++;
        }
        else
            SPI2->CR2&=~SPI_CR2_TXEIE;
    }

    if(SPI2->SR&SPI_SR_RXNE)    // Если установлен RXNE флаг...
    {
        uint8_t d=SPI2->DR;
        if(b2n<b2_size)
            b2[b2n++]=d;
    }
}

int main(void)
{
    // Будем обрабатывать прерывание от SPI2, конфигурируем NVIC.
    // Задаём приоритет (используемая идиома задаёт низший приоритет).
    NVIC_SetPriority(SPI2_IRQn, (1<<__NVIC_PRIO_BITS)-1);
    // Разрешаем обработку этого прерывания.
    NVIC_EnableIRQ(SPI2_IRQn);

    // Включаем тактирование используемых устройств:
    // DMA;
    // SPI1 и порт ввода-вывода GPIOA (SPI1 использует выводы PA4..PA7);
    // SPI2 и порт ввода-вывода GPIOB (SPI2 использует PB12..PB15).
    RCC->AHBENR|=RCC_AHBENR_DMA1EN;
    RCC->APB2ENR|=
            RCC_APB2ENR_SPI1EN|
            RCC_APB2ENR_IOPAEN|
            RCC_APB2ENR_IOPBEN;
    RCC->APB1ENR|=RCC_APB1ENR_SPI2EN;

    // Конфигурируем выводы SPI1, учитывая, что у нас SPI1 - ведущее.
    // PA4, SPI1_NSS: alt. out, push-pull, high speed
    // PA5, SPI1_SCK: alt. out, push-pull, high speed
    // PA6, SPI1_MISO: input, pull up/down
    // PA7, SPI1_MOSI: alt. out, push-pull, high speed
    GPIOA->CRL=
            GPIOA->CRL&~0xFFFF0000|
                        0xB8BB0000;
    // Настраиваем подтяжку входа PA6 (SPI1_MISO) - к высокому уровню
    // (если вход окажется не подключён, SPI будет получать все
    // единичные биты; вообще использование подтяжки необязательно).
    GPIOA->BSRR=GPIO_BSRR_BS6;

    // Конфигурируем выводы SPI2 (SPI2 у нас ведомое).
    // PB12, SPI2_NSS: input, pull up/down
    // PB13, SPI2_SCK: input, pull up/down
    // PB14, SPI2_MISO: alt. out, push-pull, high speed
    // PB15, SPI2_MOSI: input, pull up/down
    GPIOB->CRH=
            GPIOB->CRH&~0xFFFF0000|
                        0x8B880000;
    // Настраиваем подтяжку входов (подтяжка необязательна, но
    // не помешает в случае отсутствия физического подключения,
    // когда вход остаётся "висящим").
    GPIOB->BSRR=
        GPIO_BSRR_BS12|
        GPIO_BSRR_BR13|
        GPIO_BSRR_BS15;

    // Конфигурируем SPI1 (обычный ведущий режим в данном случае).
    // BIDIMODE: 0, включение режима с одной линией данных (отключено,
        // используется обычный режим с двумя линиями для передачи данных);
    // BIDIOE: 0, направление передачи (используется при BIDIMODE=1);
    // CRCEN: 0, включение аппаратного подсчёта CRC (отключено);
    // CRCNEXT:0, бит связан с вычислением CRC, используется при CRCEN=1;
    // DFF: 0, формат фрейма данных (здесь - 8-битовый фрейм);
    // RXONLY: 0, включение режима "только приём" (здесь - полнодуплексная связь);
    // SSM: 0, включение режима программного управления сигналом NSS;
    // SSI: 0, при SSM=1 бит замещает значение со входа NSS (здесь - не используется);
    // LSBFIRST: 0, порядок передачи битов (здесь - первым передаётся старший);
    // SPE: 0, бит включения SPI (здесь разделяем этапы конфигурирования и включения);
    // BR[2:0]: управление скоростью передачи (не влияет, если SPI настроен как подчинённое устройство);
        // здесь задано 0x7, что соотв. макс. делителю /256 (для теста выбираем минимальную скорость);
    // MSTR: 1, бит переключения в ведущий режим.
    SPI1->CR1=
            SPI_CR1_MSTR|   // Ведущее устройство.
            SPI_CR1_BR|     // Минимальная скорость для теста.
            CPOL|           // Полярность и
            CPHA;           // фаза тактового сигнала SPI.

    // С помощью регистра CR2 настраиваем генерацию запросов на
    // прерывание и DMA (если нужно); с помощью бита SSOE запрещаем или
    // разрешаем использовать ведущему устройству вывод NSS как выход.
    SPI1->CR2&=~0xE7;       // Сбрасываем все значимые биты регистра.
    SPI1->CR2|=
            SPI_CR2_SSOE|   // NSS будет выходом.
            SPI_CR2_RXDMAEN|// Установка флага RXNE формирует DMA запрос.
            SPI_CR2_TXDMAEN;// Установка флага TXE формирует DMA запрос.

    // Конфигурируем SPI2 для работы в обычном ведомом режиме.
    SPI2->CR1=          // Сбрасываем все биты регистра и
            CPHA|CPOL;  // задаём полярность и фазу SCK.
    SPI2->CR2&=~0xE7;       // Сбрасываем все значимые биты регистра CR2.
    SPI2->CR2|=             // Разрешаем прерывания при установке флагов
            SPI_CR2_RXNEIE| // RXNE и
            SPI_CR2_TXEIE;  // TXE.

    // Включаем SPI1.
    // Передача не начнётся, пока не запишем что-то в регистр данных,
    // но установится состояние выходов SPI (выходы переходят из Z-
    // состояния в состояние формирования выходного сигнала).
    // После этого можно будет включить ведомое устройство без
    // опасения, что оно получит некоторое количество мусорных данных
    // в процессе включения ведущего.
    SPI1->CR1|=SPI_CR1_SPE;

    // Не слишком изящный способ сообщить обработчику прерывания SPI2,
    // сколько следует передать данных (сколько ожидает ведущее
    // устройство).
    a2n_max=sizeof a1;

    // Включаем SPI2.
    SPI2->CR1|=SPI_CR1_SPE;

    // Даём время ведомому устройству подготовиться к обмену - записать
    // первый отправляемый байт в свой регистр данных (это произойдёт
    // в обработчике прерывания сразу после включения SPI2).
    while(a2n<1) {}

    // Отправляем данные через ведущее устройство, побочным продуктом
    // чего всегда является получение такого же объёма входящих данных.
    spi_send_receive(SPI1, a1, b1, sizeof a1);

    // Происходит передача данных;
    // SPI1 выполняет обмен с помощью DMA;
    // SPI2 - по прерываниям.
    // О завершении обмена можно узнать по обнулению регистра
    // DMA1_Channel2->CNDTR;
    // либо по установленному флагу DMA_ISR_TCIF2 в регистре DMA1->ISR.
    // Кстати, можно разрешить генерацию прерывания на установку флага
    // и тогда о завершении передачи узнаем по вызову обработчика,
    // а до тех пор заняться другими делами.

    // Здесь наиболее примитивный вариант с ожиданием цикле.
    while(DMA1_Channel2->CNDTR!=0) {}

    // Обмен завершён, буфер b1 теперь содержит полученные
    // ведущим устройством данные. Можем анализировать их.

    // Буфер b2 содержит полученные ведомым устройством данные.

    // Если не планируется дальнейший обмен, устройства SPI
    // могут быть отключены.
    // Рекомендуемая Руководством процедура (при работе в обычном
    // полнодуплексном режиме):
    // после получения последнего байта ждём установки флага TXE,
    // затем ждём сброса BSY, после чего отключаем SPI.

    while(!(SPI2->SR&SPI_SR_TXE)) {}
    while(SPI2->SR&SPI_SR_BSY) {}
    SPI2->CR1&=~SPI_CR1_SPE;

    while(!(SPI1->SR&SPI_SR_TXE)) {}
    while(SPI1->SR&SPI_SR_BSY) {}
    SPI1->CR1&=~SPI_CR1_SPE;

    /* Infinite loop */
    while (true) {}
}
hamper, 2020-11-03
  Рейтинг@Mail.ru