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

Использование SPI в STM32F100xx. Примеры

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

Таким образом, управление процессом передачи данных оказывается достаточно сложным. Для управления передачей данных могут быть предложены три основных варианта: программное управление передачей; управление по прерываниям; передача с использованием DMA.

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

Примеры использования SPI

Укажем характерные особенности каждого из вариантов управления передачей данных при работе с SPI и приведём простые примеры, иллюстрирующие основные идеи по реализации каждого подхода.

  1. Программное управление передачей: данные передаются/принимаются по одному байту (или фрейму). Моменты, когда в SPI можно записывать очередной элемент данных или когда есть готовые для считывания (полученные) данные, определяются по флагам SPI, которые непрерывно контролируются в программном цикле. Способ имеет крайне простую программную реализацию, но процесс передачи сопровождается высокой бесполезной вычислительной нагрузкой на микроконтроллер. Причём, передача на низких скоростях не уменьшает эту нагрузку, а даже увеличивает (на пересылку данных требуется больше времени, большую часть которого процессор проводит в циклах контроля за флагами). Способ годится для эпизодических обменов небольшими порциями данных, но плохо подходит для интенсивного обмена информацией.
  2. Управление по прерываниям. Необязательно в процессе пересылки фрейма постоянно проверять флаги SPI; в это время можно заняться более полезной работой или уйти в состояние с пониженным потреблением энергии. А чтобы своевременно узнать об изменении состояния SPI, нужно просто разрешить прерывания при установке интересующих нас флагов. Действия по чтению полученных данных и записи очередных данных для пересылки, выполняются обработчиком прерывания. Способ более сложен с точки зрения программной реализации, но значительно более эффективен. Однако, с ростом скорости передачи, вычислительная нагрузка на микроконтроллер растёт (так как передача и приём каждого фрейма сопровождается обслуживанием этого события обработчиком прерывания, на вызов и выполнение которого затрачиваются определённые вычислительные ресурсы).
  3. Передача с использованием DMA. Процессор только лишь осуществляет конфигурирование SPI и DMA, после чего передача данных из буфера в памяти и приём данных в буфер происходят под управлением системы DMA. Вычислительная нагрузка на микроконтроллер минимальна (намного меньше, чем при передаче по прерываниям), особенно если передаются большие объёмы данных. В отличие от предыдущего варианта, передача с высокими скоростями не приводит к существенным накладным расходам. С точки зрения программной реализации, способ достаточно прост. Правда, количество каналов DMA в микроконтроллерах STM32F100xx весьма ограничено, что делает этот ресурс очень ценным. Кроме того, передача каждого блока данных требует выполнения ряда действий по подготовке DMA, что снижает общую эффективность метода при передаче данных небольшими порциями (не исключено, что в таких случаях более выгодным окажется второй или даже первый метод). Но если передаются большие объёмы данных на больших скоростях, то вариант с DMA оптимален.

Соответственно, рассмотрим следующие три примера, демонстрирующие программную реализацию каждого из описанных подходов. Во всех примерах происходит передача данных между двумя устройствами SPI одного микроконтроллера (SPI1, SPI2). Это существенно упрощает отладку. Для определённости, в каждом примере будем настраивать SPI1 как ведущее, а SPI2 - как ведомое устройство.

Пример 1. Простейший пример, SPI1 и SPI2 обмениваются данными объёмом 1 байт под программным управлением передачей.

Пример 2. Более сложный пример, решаемая задача уже ближе к задачам из реальной жизни. SPI1 и SPI2 обмениваются сообщениями произвольной длины. Устройство SPI1 находится под программным управлением передачей. SPI2 управляется по прерываниям.

Пример 3. SPI1 передаёт данные с помощью DMA. SPI2 управляется по прерываниям.

Общие замечания

Рассмотрим некоторые моменты, касающиеся всех предложенных выше примеров.

Используемое оборудование. Все приведённые примеры предполагают использование микроконтроллера STM32F100RB (запускались и проверялись на этом устройстве), однако, не должно возникнуть проблем, если заменить его на другой микроконтроллер из семейства STM32F100xx, имеющий 2 устройства SPI. Без труда примеры можно адаптировать под любой микроконтроллер STM32 благодаря высокой программной совместимости периферийных устройств в микроконтроллерах STM разных моделей.

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

SPI1 настраивается как ведущее устройство, а SPI2 - как ведомое.

Конфигурационный код. Примеры содержат значительный объём сходного кода, служащего для инициализации устройств микроконтроллера: включение тактового сигнала задействованных в работе периферийных устройств микроконтроллера; конфигурирование выводов, используемых в качестве выводов SPI; настройка самих SPI.

Тактовый сигнал включаем для SPI1, SPI2, GPIOA (чтобы использовать выводы SPI1), GPIOB (для использования выводов SPI2). Если требуются дополнительные устройства (допустим, DMA) - то и для них.

Затем следует сконфигурировать выводы, используемые как выводы SPI. Так как договорились, что SPI1 - ведущее, SPI2 - ведомое, то во всех примерах выводы конфигурируются одинаково:
выходы (MOSI, SCK, NSS для SPI1; MISO для SPI2) - как двухтактные выходы для выполнения альтернативной функции;
входы (MISO для SPI1; MOSI, SCK, NSS для SPI2) - как входы с подтяжкой.

Поскольку SPI - интерфейс, способный работать на достаточно высоких скоростях, то и скорость переключения для выходов SPI в примерах задаётся высокой. Если высокая скорость передачи для SPI не нужна, то стоит снизить скорость переключения выходов. Это полезно для снижения энергопотребления. Кроме того, при работе в качестве ведомого устройства возникают проблемы в некоторых режимах (а именно, когда CPOL = 0, CPHA = 0 или CPOL = 1, CPHA = 1), если ведомое устройство подключено к ведущему устройству, для выходов которого задана высокая скорость переключения. В процессе тестирования при выполнении указанных условий, наблюдались сбои в передаче данных и сбой синхронизации ведомого устройства. Поэтому, следует подумать о снижении скорости в проблемных режимах, либо использовать менее проблемные режимы (CPOL = 0, CPHA = 1 или CPOL = 1, CPHA = 0). В процессе тестирования, SPI в качестве ведущего устройства работало без проблем во всех режимах.

Входы сконфигурированы как входы с подтяжкой. Это совершенно необходимо для входа NSS ведомого устройства - подтягивая вход к высокому уровню, мы гарантируем, что устройство не будет случайно активировано наведённой на входе помехой, если вход NSS ведомого устройства остался физически неподключённым, либо подключён к выходу другого SPI устройства, которое пока не включено и его NSS выход находится в Z-состоянии.

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

Подтяжка для входов данных необязательна, но и не помешает.

После того, как настроены выводы, следует выполнить настройку SPI. Устройства SPI в предлагаемых примерах конфигурируются сходным образом, отличия касаются лишь регистра CR2, инициализация которого зависит от того, используются ли прерывания и запросы DMA. Регистр CR1 инициализируется везде одинаково, с его помощью: для SPI1 устанавливаем бит режима ведущего устройства; задаётся скорость (в целях тестирования здесь устанавливается максимальный делитель для получения тактового сигнала SPI, соответственно скорость будет минимальной); задаём полярность и фазу тактового сигнала SPI. Для SPI2 только задаём полярность и фазу тактового сигнала SPI, все остальные биты регистра CR1 устанавливаем нулевыми.

Полярность и фаза тактового сигнала SPI могут быть заданы любыми, но для "общающихся" между собой SPI, они должны быть заданы одинаково. Чтобы было удобнее экспериментировать с разными режимами тактирования, параметры тактового сигнала определены в начале текста программы в виде констант CPOL и CPHA, которые могут иметь значения 0 или SPI_CR1_CPOL и 0 или SPI_CR1_CPHA соответственно.

На этом конфигурирование завершается, теперь можно включить устройства SPI1, SPI2 и приступить к передаче данных:

Пример 1. Программное управление передачей.

Пример 2. Управление по прерываниям.

Пример 3. Передача с помощью DMA.

Простейший пример

И ещё один пример. Во всех рассмотренных ранее программах происходила полноценная передача данных между двумя устройствами SPI. Но существует ещё более простой вариант: передача устройством SPI данных самому себе. Это возможно, если SPI настроено как ведущее устройство. Тогда, если соединить выводы MOSI и MISO между собой, то отправляя байт через SPI, должны его же и получить. Практической пользы от такой передачи немного. Тем не менее, как тест работоспособности оборудования, даже столь примитивный пример представляет определённый интерес.

Пусть для работы выбран микроконтроллер STM32F100RB (подойдут и другие STM32F100xx без необходимости внесения изменений в программу и практически любые STM32, но, возможно, с внесением небольших изменений).

Будем экспериментировать с SPI1, SPI использует 4 вывода микроконтроллера:
PA4 - NSS;
PA5 - SCK;
PA6 - MISO;
PA7 - MOSI.

Из них нам, на самом деле, здесь потребуется только MOSI и MISO (которые в данном случае мы соединяем вместе), но чтобы этот простейший пример был пригоден в качестве основы для более сложных, настроим правильным образом все выводы.

Несколько пояснений к тексту программы. В начале программы можно увидеть определение двух констант: CPOL (по желанию можно определить её как 0 или SPI_CR1_CPOL) и CPHA (0 или SPI_CR1_CPHA). Задав желаемое значение, можно выбрать полярность и фазу тактового сигнала, используемые в тесте.

Далее определяется функция main, в которой и находится основной код. Функция main в процессе выполнения: включает тактовый сигнал для SPI1 и для GPIOA (GPIOA требуется, поскольку к этому порту относятся выводы SPI1); выполняет конфигурирование выводов. Так как SPI1 будет ведущим устройством, то PA4 (SPI1_NSS), PA5 (SPI1_SCK) и PA7 (SPI1_MOSI) настраиваем как выходы (двухтактные выходы для выполнения альтернативной функции; так как SPI - высокоскоростной интерфейс, выбираем высокую скорость переключения выходов). Вывод PA6 (SPI1_MISO) - вход. Здесь выбран режим с подтяжкой, на тот случай, если вывод окажется неподключённым. Используется подтяжка к высокому уровню, тогда, если выход останется неподключённым, в процессе обмена все принимаемые биты будут единичными.

После конфигурирования выводов приступаем к конфигурированию SPI (регистры CR1, CR2). Для обычного режима работы большинство битов оставляются нулевыми. Устанавливаются в 1 биты SPI_CR1_MSTR (так как SPI1 будет ведущим), все биты в битовом поле SPI_CR1_BR управления скоростью передачи (используем максимальный делитель тактовой частоты, 256 и, соответственно - минимальную скорость - для целей отладки здесь не нужна высокая скорость). В регистре CR2 устанавливаем только бит SPI_CR2_SSOE, что означает, что вывод NSS будет выходом (обычный вариант использования вывода NSS в случае ведущего устройства).

Теперь включаем SPI1. До включения SPI, выходы находились в Z-состоянии, после включения, на них выводится соответствующий начальному состоянию уровень. Например, на NSS устанавливается низкий уровень, на SCK - уровень, определяемый значением CPOL.

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

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

Если мы не собираемся больше работать с SPI, устройство может быть отключено. Для используемого здесь режима полнодуплексной связи, руководство требует перед отключением SPI дождаться сначала установки флага TXE, а затем - сброса флага BSY.

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

/*
File:   main.cpp

Простейший пример с использованием SPI: передача байта самому себе.

MCU: STM32F100RB
SPI1:
    PA4 - NSS;
    PA5 - SCK;
    PA6 - MISO;
    PA7 - MOSI.
Для теста MISO и MOSI соединяем вместе.
*/

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

// Указываем полярность и фазу тактового сигнала SPI.
const uint32_t CPOL=SPI_CR1_CPOL*0;
const uint32_t CPHA=SPI_CR1_CPHA*0;

int main(void)
{
    // Включаем тактирование используемых устройств:
    // SPI1 и порт ввода-вывода GPIOA (SPI1 использует выводы PA4..PA7).
    RCC->APB2ENR|=
            RCC_APB2ENR_SPI1EN|
            RCC_APB2ENR_IOPAEN;

    // Конфигурирование выводов 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) к высокому уровню
    // (использование подтяжки необязательно).
    GPIOA->BSRR=GPIO_BSRR_BS6;

    // Конфигурируем 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 (значение бита используется вместо сигнала NSS при SSM=1);
    // LSBFIRST: 0 (порядок передачи битов, здесь - первым передаётся старший);
    // SPE: 0 (бит включения SPI, здесь разделяем конфигурирование и включение);
    // BR[2:0] (управление скоростью передачи, только для ведущего;
            // здесь задаём 0x7, что соотв. макс. делителю /256 и мин. скорости);
    // MSTR: 1 (бит переключения в ведущий режим).
    SPI1->CR1=              // Большинство битов задаём нулевыми.
            SPI_CR1_MSTR|   // Ведущий режим.
            SPI_CR1_BR|     // BR[2:0]=0x7 - минимальная скорость для теста.
            CPOL|           // Полярность тактового сигнала.
            CPHA;           // Фаза тактового сигнала.

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

    // Включаем SPI1.
    // Передача не начнётся, пока не запишем что-то в его регистр,
    // данных, но установится состояние выходов.
    SPI1->CR1|=SPI_CR1_SPE;

    char d_out=0x5A;    // Передаваемый байт.
    char d_in=0;        // Сюда запишем полученный байт.

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

    // Ждём получения данных, читаем их.
    while(!(SPI1->SR&SPI_SR_RXNE)) {}
    d_in=SPI1->DR;

    // Можем проверить правильность полученных данных, здесь должно быть
    // d_in==d_out;

    // Если больше не планируется передавать данные по SPI,
    // он может быть отключён.
    while(!(SPI1->SR&SPI_SR_TXE)) {}
    while(SPI1->SR&SPI_SR_BSY) {}

    SPI1->CR1&=~SPI_CR1_SPE;

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