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

SPI. Пример. Программный контроль передачи данных

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

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

В данном примере предполагается использование микроконтроллера STM32F100RB. Обмен данными осуществляется между устройствами SPI1 и SPI2 микроконтроллера. Чтобы экспериментировать с передачей данных по SPI, нам понадобится соединить между собой соответствующие выводы SPI1 и SPI2:
SPI1_MOSI - SPI2_MOSI;
SPI1_MISO - SPI2_MISO;
SPI1_SCK - SPI2_SCK;
SPI1_NSS - SPI2_NSS.

Соединение может быть непосредственным или через резисторы с небольшим сопротивлением (200 Ом), чтобы застраховать себя от возможных неожиданностей в случае ошибок в процессе отладки.

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

Как обычно, прежде всего, в программе включаем тактовый сигнал используемых устройств: SPI1, SPI2, GPIOA (для работы с выводами SPI1), GPIOB (для работы с выводами SPI2). Затем конфигурируем используемые выводы, с учётом того, что у нас SPI1 будет ведущим устройством, а SPI2 - ведомым. Выходы настраиваем как двухтактные, для выполнения альтернативной функции. Быстродействие - в зависимости от предполагаемой для использования скорости передачи SPI. С одной стороны, SPI способен работать как достаточно высокоскоростной интерфейс и логично выбирать для выходов высокую скорость переключения. С другой стороны, не всегда высокие скорости необходимы, в то время как настройка выхода GPIO для работы в высокоскоростном режиме ведёт к росту потребляемой энергии. Кроме того, в некоторых режимах SPI (ведомый режим при выборе для полярности и фазы тактового сигнала SPI варианта CPOL=0, CPHA=0 или варианта CPOL=1, CPHA=1) наблюдаются проблемы при передаче (искажение данных, нарушение синхронизации), если выходы ведущего устройства сконфигурированы для работы с высокой скоростью переключения.

Конфигурируем SPI1 и SPI2.

Пока SPI ведущее устройство не будет включено, его выходы будут находиться в Z-состоянии. Для активации выхода ведомого устройство оно должно быть не только включено, но и выбрано низким уровнем сигнала NSS (либо программно - в зависимости от настроек).

Сначала в программе включается ведущее устройство SPI1. После его включения, на выходах устанавливаются уровни, соответствующие начальному состоянию. На NSS выходе устанавливается низкий уровень (низким уровнем выбирается подключённое ведомое устройство).

Затем включаем ведомое устройство. Сразу после включения, оно ещё не будет готово к обмену. Сначала надо записать в регистр данных значение, которое будет отправлено ведущему устройству, когда оно инициализирует обмен данными. Перед записью убеждаемся, что установлен флаг TXE. Если требуется отправить несколько байт данных, перед каждой записью дожидаемся установки TXE.

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

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

Если в дальнейшем обмен по интерфейсу SPI не планируются, устройства SPI1, SPI2 могут быть отключены.

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

/*
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;

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

uint8_t a1=0x5A;    // Байт, передаваемый через SPI1.
uint8_t a2=0x03;    // Байт, передаваемый через SPI2.
uint8_t b1=0;       // Байт, получаемый SPI1.
uint8_t b2=0;       // Байт, получаемый SPI2.

int main(void)
{
    // Включаем тактирование используемых устройств:
    // 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|           // Полярность тактового сигнала SPI.
            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.

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

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

    // Готовим ведомое устройство к передаче данных (если ведомое
    // будет передавать данные, они должны быть подготовлены ДО ТОГО,
    // как ведущее начнёт передачу).
    while(!(SPI2->SR&SPI_SR_TXE)) {}
    SPI2->DR=a2;

    // Пишем в регистр данных устройства SPI1,
    // после чего начнётся обмен данными.
    while(!(SPI1->SR&SPI_SR_TXE)) {}
    SPI1->DR=a1;

    // Ждём получения данных устройствами SPI1, SPI2 (порядок
    // в данном случае непринципиален).
    while(!(SPI1->SR&SPI_SR_RXNE)) {}
    b1=SPI1->DR;

    while(!(SPI2->SR&SPI_SR_RXNE)) {}
    b2=SPI2->DR;

    // Здесь, если передача прошла корректно, должно быть
    // b1==a2, b2==a1.

    // Если не планируется дальнейший обмен, устройства 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) {}
}

Смотрите также более интересный пример, в котором происходит обмен сообщениями произвольной длины и демонстрируется как программное управление SPI, так и управление по прерываниям:
SPI. Пример. Передача данных по прерываниям

hamper, 2020-11-03
  Рейтинг@Mail.ru