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

SPI. Пример. Передача данных по прерываниям

Здесь рассматривается более сложный, по сравнению с предыдущим, пример программы, выполняющей передачу данных по интерфейсу SPI. Во-первых, одно из устройств SPI управляется по прерываниям. Во-вторых, хотя другое работает под программным управлением, в данном примере демонстрируется порядок работы при передаче данных произвольного объёма (а не одного байта).

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

По-прежнему, используется микроконтроллер STM32F100xx; передача данных происходит между устройствами SPI1 и SPI2. SPI1 используется как ведущее, SPI2 - ведомое. Передача данных через SPI1 управляется программно, SPI2 управляется по прерываниям (обработчик SPI2_IRQHandler).

Начальные действия программы по инициализации оборудования аналогичны тем, что выполнялись в предыдущем примере (включается тактовый сигнал для используемых устройств, настраиваются выводы используемых SPI, настраиваются сами SPI). Дополнительно к этому выполняется настройка NVIC (разрешается обработка прерываний от SPI2 и устанавливается приоритет прерывания). Прерывания генерируются как при установке флага TXE, так и при установке RXNE (настраивается с помощью регистра CR2 устройства SPI2).

Для передачи данных с программным управлением передачей используется функция spi_send_receive. Особенностью интерфейса SPI является то, что передача данных всегда сопровождается получением такого же количества данных. Поэтому функции в качестве аргументов передаём указатели на два буфера в памяти: один с передаваемыми данными, второй - для получаемых данных. Размеры буферов должны быть одинаковы, размер передаётся функции в качестве аргумента.

Реализована функция spi_send_receive следующим образом.

1. Убедившись, что буферный регистр для передаваемых данных устройства SPI свободен (установлен флаг TXE), функция записывает первый пересылаемый байт в регистр данных устройства SPI.

Из буферного регистра байт вскоре пересылается в сдвигающий регистр SPI и, одновременно с началом процесса побитовой пересылки данных, буферный регистр для передаваемых данных становится свободным (устанавливается флаг TXE) и готовым к записи в него следующего передаваемого байта.

2. Поэтому, не дожидаясь завершения пересылки предыдущего байта по интерфейсу, как только установится флаг TXE, мы загружаем следующий байт пересылаемых данных в регистр данных SPI. Теперь TXE будет оставаться сброшенным до завершения физической передачи предыдущего байта.

3. Одновременно с завершением передачи предыдущего байта, завершается получение байта по интерфейсу, который загружается в буферный регистр принимаемых данных. Об этом мы узнаём по установленному флагу RXNE. Считываем полученный байт и помещаем его в буфер получаемых данных в памяти (адрес которого передан функции в качестве аргумента).

Повторяем последовательность шагов 2, 3, пока все данные не будут записаны в регистр данных SPI.

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

Ведомое устройство обслуживается обработчиком прерывания. Всякий раз, когда обработчик обнаруживает установленный флаг TXE своего устройства SPI, он записывает новые данные в регистр данных SPI. И всякий раз, когда обнаруживается установленный флаг RXNE, обработчик считывает полученные данные и помещает их в буфер памяти.

В примере выбран не слишком элегантный, но простой и эффективный при осторожном применении способ взаимодействия обработчика и основной программы - через глобальные переменные.

Заметим, что прежде чем ведущее устройство инициирует передачу, ведомое должно подготовиться - заблаговременно поместить первый передаваемый байт в свой регистр данных. Здесь это обеспечивается следующим образом: включаются оба SPI, но вызов функции spi_send_receive, который начинает передачу данных ведущим устройством, откладывается до тех пор, пока не будет обнаружено, что ведомое устройство готово. О готовности ведомого устройства мы судим по тому, что изменяется значение счётчика отправленных байт a2n. В свою очередь, в регистр данных ведомого устройства первый передаваемый байт будем помещён обработчиком прерывания - сразу после включения ведомого устройства устанавливается его флаг TXE, а значит, генерируется прерывание и вызывается его обработчик.

Есть одна проблема, связанная с управлением ведомым устройством - оно не знает заранее, сколько данных передаст ему ведущее устройство. И соответственно, сколько данных должно отправить оно. С получаемыми данными проще - что с ними делать можно решить по мере их поступления. С отправляемыми немного сложнее - отправляемый байт должен быть подготовлен заранее, до того как ведущее устройство начнёт передачу очередного фрейма. И чтобы не записать в свой регистр данных "лишнего", ведомое устройство должно знать заранее, сколько байт оно передаст.

Решается проблема разными способами, на уровне протокола. Можно договориться обмениваться блоками данных фиксированной длины, но это не всегда удобно. Более гибкий способ - составлять сообщение из заголовка фиксированной длины, и тела переменной длины. Длина тела сообщается в предшествующем ему заголовке. Здесь, поскольку оба SPI находятся в пределах одного микроконтроллера, используется небольшая уловка. Обработчику прерываний от SPI2 мы сообщаем, сколько ему следует передать данных... с помощью глобальной переменной a2n_max. В реальном устройстве такой фокус не пройдёт, но в данном примере такое упрощение оправдано тем, что излишние детали реализации будут лишь мешать демонстрации основных идей метода. После того, как обработчик передал требуемое количество данных, он просто отключает дальнейшую генерацию прерывания при установке флаг TXE.

Завершение выполнения функции spi_send_receive означает, что передача данных завершена (это гарантируется использованием программного способа управления передачей внутри функции). Теперь можно обработать записанные в буферы в памяти данные и, если SPI больше не нужны, можно отключить их.

/*
File:   main.cpp

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

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

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

/**
*  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, сколько байт должен отправить SPI2
// (т.е., сколько ожидает получить ведущее устройство).
unsigned int a2n_max=0;

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

// Функция, реализующая передачу/получение заданного количества байт
// (используется программное управление передачей).
void spi_send_receive(
        SPI_TypeDef *spi,       // Используемое устройство SPI.
        const void *tx_buf,     // Буфер с передаваемыми данными.
        void *rx_buf,           // Буфер для принимаемых данных.
        unsigned int n          // Размер буфера (оба одинаковые).
        )
{
    if(n==0)
        return;

    const char *ptx=(const char*)tx_buf;
    char *prx=(char*)rx_buf;

    while(!(spi->SR&SPI_SR_TXE)) {}
    spi->DR=*(ptx++);
    n--;

    while(n>0)
    {
        while(!(spi->SR&SPI_SR_TXE)) {}
        spi->DR=*(ptx++);

        while(!(spi->SR&SPI_SR_RXNE)) {}
        *(prx++)=spi->DR;

        n--;
    }

    while(!(spi->SR&SPI_SR_RXNE)) {}
    *(prx++)=spi->DR;
}

// Обработчик прерываний от 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    // Отключаем генерацию прерываний при установке TXE.
            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);

    // Включаем тактирование используемых устройств:
    // SPI1 и порт ввода-вывода GPIOA (SPI1 использует выводы PA4..PA7);
    // SPI2 и порт ввода-вывода GPIOB (SPI2 использует PB12..PB15).
    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 будет выходом.

    // Конфигурируем 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 (сразу же генерируется прерывание, так как
    // изначально флаг TXE установлен).
    SPI2->CR1|=SPI_CR1_SPE;

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

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

    // Обмен завершён, буфер 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