Программирование STM32. Часть 6: SPI

В этой статье мы научимся работать с модулем SPI в микроконтроллере STM32F103C8 в режиме Master с использованием прерываний и без них. Предыдущая статья здесь, все статьи цикла можно посмотреть тут: http://dimoon.ru/category/obuchalka/stm32f1.

SPI является самым популярным последовательным синхронным интерфейсом передачи данных между микроконтроллером и периферийными устройствами. В составе STM32F103C8 есть два модуля SPI. Этот интерфейс может работать в режиме Master (ведущий шины) или Slave (ведомый шины). Вообще говоря, интерфейс SPI является довольно навороченной штукой. Конкретно в STM32 SPI может подсчитывать контрольную сумму принятых и переданных кадров по заданному полиному, работать в Multimaster mode, аппаратно работать с выводом NSS, а так же обмениваться данными в полудуплексном режиме (MOSI и MISO идут по одному и тому же проводу). Поэтому для правильной настройки SPI нужно внимательно изучить все регистры этого модуля. Ну и еще один момент: SPI в STM32 может работать в режиме I2S (не путать с I2C!!!). I2S — это SPI-подобный интерфейс передачи данных между цифровыми аудиоустройствами. Пусть это вас не смущает, по-умолчанию этот модуль работает в режиме SPI и регистры, которые нужны только для I2S режима мы рассматривать не будем.

Для связи двух или нескольких устройств необходимо 4 провода (+земля, куда же без нее):

  • MOSI (Master Out / Slave In) — по этому проводу данные передаются от ведущего устройства к ведомому
  • MISO (Master In / Slave Out) — а тут наоборот: данные идут от ведомого к ведущему
  • SCK (Serial Clock) — тактовый сигнал, который идет от ведущего устройства к ведомому. На каждом новом периоде тактового сигнала Master шины отправляет новый бит данных Slave-у , а Slave в свою очередь отправляет бит данных Master-у.
  • NSS (Slave select) — необязательный провод, нужен в случае, если у нас на шине SPI висит несколько ведомых устройств. Таким образом, с помощью NSS мы можем выбрать, с каким Slave-ом мы хотим обмениваться данными.

Стоит отметить, что если мы только читаем данные из ведомого устройства, то нам нужны только провода SCK и MISO, а если только пишем в ведомое то SCK и MOSI.

Регистры SPI

Тут приведу описание регистров, которые относятся только к SPI. Все, что касается I2S выкинул.

SPI control register 1 (SPI_CR1)

Регистр SPI_CR1

Рис. 1. Регистр SPI_CR1

BIDIMODE: Разрешение двунаправленного режима работы вывода данных.

  • 0: 2-проводной режим работы с однонаправленной передачей по линиям данных
  • 1: 1-проводной режим работы с двунаправленной передачей по линии данных

BIDIOE: Разрешение выхода в двунаправленном режиме

Этот бит, совместно с битом BIDIMODE, выбирает направление передачи в двунаправленном режиме.  В режиме master для передачи данных используется вывод MOSI, в режиме slave используется вывод MISO.

  • 0: Выход отключен (только прием)
  • 1: Выход включен (только передача)

CRCEN: Включение аппаратного подсчета CRC

  • 0: вычисление CRC отключено
  • 1: вычисление CRC включено

CRCNEXT: Следующая передача данных будет завершаться CRC-кодом.

  • 0: Этап передачи данных
  • 1: Следующая передача завершится передачей RCR

DFF: Формат кадра данных

  • 0: Размер кадра передачи 8 бит
  • 1: Размер кадра передачи 16 бит

RXONLY: Этот бит совместно с BIDIMODE выбирает направление передачи в 2-х проводном (MISO и MISO) режиме.

  • 0: Full duplex — передача и прием
  • 1: Output disabled — только прием

SSM: Программное управление ведомым устройством. Когда бит SSM установлен, сигнал NSS заменяется значением бита SSI.

  • 0: Программное управление ведомым отключено
  • 1: Программное управление ведомым включено

SSI: Внутренний выбор ведомого. Этот бит работает только когда бит SSM установлен. Значение этого бита принудительно подается на NSS, а значение IO вывода NSS игнорируется.

LSBFIRST: Формат кадра

  • 0: MSB передается первым
  • 1: LSB передается первым

SPE: Включить SPI

  • 0: SPI отключен
  • 1: SPI включен

BR[2:0]: Выбор скорости передачи

  • 000: fPCLK/2
  • 001: fPCLK/4
  • 010: fPCLK/8
  • 011: fPCLK/16
  • 100: fPCLK/32
  • 101: fPCLK/64
  • 110: fPCLK/128
  • 111: fPCLK/256

MSTR: Выбор режима работы SPI: Master/Slave

  • 0: Режим Slave (ведомый)
  • 1: Режим Master (ведущий)

CPOL: Полярность тактового сигнала

  • 0: CK в 0 при простое
  • 1: CK в 1 при простое

CPHA: Фаза тактового сигнала

  • 0: Первый переход тактового сигнала является краем захвата данных
  • 1: Второй переход тактового сигнала является краем захвата данных

 

SPI control register 2 (SPI_CR2)

Регистр SPI_CR2

Рис. 2. Регистр SPI_CR2

TXEIE: Прерывание опустошения буфера передачи данных Tx

  • 0: Прерывание TXE запрещено
  • 1: Прерывание TXE разрешено. Используется для генерации прерывания когда устанавливается флаг TXE

RXNEIE: Прерывание не пустого буфера приема Rx

  • 0: Прерывание RXNE запрещено
  • 1: Прерывание RXNE разрешено. Используется для генерации прерывания когда устанавливается флаг RXNE.

ERRIE: Прерывание при возникновении ошибок передачи. Этот бит контролирует генерацию прерывания при возникновении одной из ошибок интерфейса SPI (CRCERR, OVR, MODF).

  • 0: Прерывание при возникновении ошибок запрещено
  • 1: Прерывание при возникновении ошибок разрешено

SSOE: Разрешить выход SS

  • 0: Выход SS отключен в режиме master (ведущий) и есть возможность работать в multimaster режиме
  • 1: Выход SS включен в режиме master (ведущий), при этом нет возможности работать в multimaster режиме

TXDMAEN: Когда этот бит установлен, запрос DMA возникает при установке флага TXE

RXDMAEN: Когда этот бит установлен, запрос DMA возникает при установке флага RXNE

 

SPI status register (SPI_SR)

Регистр SPI_SR

Рис. 3. Регистр SPI_SR

BSY: Флаг занятости. Этот флаг устанавливается и сбрасывается аппаратно

  • 0: SPI не занят
  • 1: SPI занят обменом данными или буфер передачи Tx не пуст

OVR: Флаг переполнения.

  • 0: переполнение не произошло
  • 1: произошло переполнение

CRCERR: Флаг ошибки контрольной суммы CRC. Этот флаг устанавливается аппаратно и сбрасывается программно записью нуля.

  • 0: Принятое значение CRC совпало со значением регистра SPI_RXCRCR
  • 1: Принятое значение CRC не совпало со значением регистра SPI_RXCRCR

TXE: Буфер передатчика пуст

  • 0: Tx буфер не пуст
  • 1: Tx буфер пуст

RXNE: Буфер приемника не пуст

  • 0: Rx буфер пуст
  • 1: Rx буфер не пуст

 

SPI data register (SPI_DR)

Регистр SPI_DR

Рис. 4. Регистр SPI_DR

DR[15:0]: Регистр данных. Этот регистр разделен на два буфера: первый для записи (буфер передатчика), второй для чтения (буфер приемника). Операция записи в регистр SPI_DR записывает данные в буфер передатчика, а операция чтения из SPI_DR возвращает значение из буфера приемника.

SPI CRC polynomial register (SPI_CRCPR)

Регистр SPI_CRCPR

Рис. 5. Регистр SPI_CRCPR

CRCPOLY[15:0]: Регистр CRC полинома, по-умолчанию 0007h

 

SPI RX CRC register (SPI_RXCRCR)

Регистр SPI_RXCRCR

Рис. 6. Регистр SPI_RXCRCR

RXCRC[15:0]: Значение CRC принятых данных. Когда вычисление CRC включено, RXCRC содержит вычисленное значение CRC принятых данных. Этот регистр сбрасывается в ноль, когда бит CRCEN в регистре SPI_CR1 устанавливается в единицу.

 

SPI TX CRC register (SPI_TXCRCR)

Рис. 7. Регистр SPI_TXCRCR

TXCRC[15:0]: Значение CRC переданных данных. Когда вычисление CRC включено, TXCRC содержит вычисленное значение CRC переданных данных. Этот регистр сбрасывается в ноль, когда бит CRCEN в регистре SPI_CR1 устанавливается в единицу.

 

Настройка SPI в режиме Master (ведущий) без прерываний

После того, как мы изучили регистры SPI, приступим к практике. Задача: настроить SPI1 в режиме Master и запустить непрерывную отправку одного байта без использования прерываний. Ну что, поехали!

Функцию инициализации назовем SPI1_Init():

Теперь надо определить, к каким выводам микроконтроллера подключен SPI1. Открываем Reference manual, идем в раздел про GPIO, находим 9.3.10 SPI1 alternate function remapping. Там есть вот такая таблица:

SPI1 alternate function remapping

Рис. 8 Таблица 56 в Reference manual

Пока не будем возиться с Remap-ом. Из рис. 7 становится понятно, что SPI1 подключен к порту GPIOA к следующим выводам:

  • NSS — PA4
  • SCK — PA5
  • MISO — PA6
  • MOSI — PA7

А как эти выводы правильно настроить для работы с SPI? В 9.1.11 GPIO configurations for device peripherals есть вот такая таблица:

Настройка выводов порта для работы с SPI1

Рис. 9. Настройка выводов порта для работы с SPI1

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

Далее настраиваем GPIO. Про его регистры можно почитать вот в этой статье.

Вывод NSS не трогаем, так как не будем его использовать. Далее, настройка SPI:

Настроили вот так: 8 бит, MSB first, CPOL/CPHA 00. Стоит обратить особое внимание на SSM и SSI. Инициализация модуля SPI в режиме Master возможна только при сигнале SS равном единице. Почему это так объяснять не буду, скажу только, что это идет из режима Multimaster mode. Сигнал SS может быть получен либо с вывода NSS, либо бита SSI регистра CR1. Если SSM установлен в ноль (значение по-умолчанию), то при включении SPI он будет производить проверку состояния NSS, а NSS по-умолчанию настроен как Input floating. Таким образом, если на выводе NSS будет логическая единица, то инициализация завершится успешно, в противном случае ни чего не получится и в регистре SR установится бит MODF, который говорит об ошибке режима. Кроме того, даже после успешной инициализации низкий уровень на NSS отключит SPI и сбросит бит MSTR (из режима master переключится в slave). А если NSS будет просто болтаться в воздухе, то система будет вообще неработоспособной. Поэтому устанавливаем SSM и SSI в единицы.

Осталось теперь только включить SPI1:

Все, инициализация в режиме Master завершена! Вот полный код функции:

Перейдем теперь к обмену данными. Но вначале нужно немного коснуться устройства модуля SPI.

Блок-схема модуля SPI

 

Рис. 10. Блок-схема модуля SPI

У SPI есть сдвиговой регистр (Shift register), буфер передатчика (Tx buffer) и буфер приемника (Rx buffer). В регистре SR есть три очень интересных флага: BSYTXE и RXNE. Флаг TXE устанавливается в том случае, если буфер передатчика (Tx buffer) пуст и в него можно загрузить следующее значение, RXNE устанавливается в единицу, если в буфер приемника (Rx buffer) поступило новое значение и его можно прочитать. BSY устанавливается в том случае, если модуль SPI занят операцией обмена данными либо буфер передатчика не пуст.

Логика работы следующая: операцией записи в регистр DR производится заполнение буфера передатчика кадром данных (8 или 16 бит, зависит от настройки), при этом флаг BSY устанавливается, а TXE сбрасывается. После этого значение из буфера передатчика загружается в сдвиговой регистр и запускается процесс передачи данных по SPI, а флаг TXE устанавливается в единицу, что говорит об возможности загрузить новое значение в Tx buffer. Если в Tx buffer загрузить еще одно значение, то TXE сбросится в ноль до момента завершения текущей передачи кадра данных и очередной загрузке значения Tx buffer в сдвиговой регистр.

С каждым новым периодом сигнала синхронизации SCK сдвиговой регистр выплевывает очередной бит в MOSI и в свой хвост заносит новый бит данных с MISO (это справедливо для режима Master, для Slave наоборот). После того, как был принят последний бит, значение сдвигового регистра загружается с буфер приемника (Rx buffer) и устанавливается флаг RXNE. Если в Tx buffer не было загружено новое значение, то передача данных завершается и флаг BSY сбрасывается в ноль.

Отправка данных в SPI будет выглядеть вот так:

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

А вот и прием данных:

Для теста вот такой main():

ClockInit() — инициализация системы тактирования, см. эту статью. Далее инициализация SPI1 и бесконечный цикл с отправкой значения 0x34. В доказательство правильной работы программы приведу осциллограмму:

Рис. 11. Осциллограмма работы программы, нижний график (синий) сигнал SCK, верхний (желтый) сигнал на выводе MOSI

На рис. 11 видно, что данные идут непрерывным потоком без задержек. Отлично! 🙂

Настройка SPI в режиме Master (ведущий) с прерываниями

Теперь давайте сделаем все то же самое, но только на прерываниях. Задача следующая: у нас есть некий массив байт, который надо выплюнуть в SPI с использованием прерываний. Вдаваться в подробности работы прерываний в STM32 в не буду, для этого будет отдельная статья, ограничусь только необходимым минимумом.

У периферийного модуля может быть несколько событий, которые могут вызвать прерывание, для SPI это TXEIERXNEIE и ERRIE (см. рис. 2). Однако, обработчик прерывания в большинстве случаев всего один: SPI1_IRQHandler(). Таким образом, если у нас включено прерывание по нескольким событиям, то чтобы понять, что произошло, в SPI1_IRQHandler() нужно смотреть регистр статуса SR.

Для того, чтобы прерывание сработало, нужно выполнить 3 действия:

  1. Включить прерывание в модуле SPI.
  2. Разрешить прерывание от SPI в NVIC. При возникновении любого разрешенного в SPI прерывания будет вызван обработчик SPI1_IRQHandler().
  3. Разрешить прерывания глобально (по-умолчанию, после сброса микроконтроллера они разрешены).

Функция инициализации почти ни чем не отличается от предыдущего примера, только добавлена строчка NVIC_EnableIRQ(SPI1_IRQn). Вот код инициализации:

Далее, нам понадобятся 3 глобальных переменных:

Затем идет функция запуска передачи данных по SPI. На вход она принимает указатель на массив uint8_t и количество байт для передачи:

Работает это так. В начальном состоянии SPI не ведет ни какую передачу данных и в регистре SR флаг TXE установлен в единицу. Это значит, что если разрешить прерывание TXEIE, то оно тут же сработает. После всех предварительных настроек мы разрешаем прерывание TXEIE, тем самым запускаем процесс отправки данных по SPI. Обработчик прерывания, в котором происходит вся основная работа, выглядит вот так:

Думаю, все ясно из комментариев. Набросаем небольшой main() для демонстрации:

А вот осциллограммы процесса передачи данных. Это для длинны буфера data[] 3 байта:

Рис. 12. Отправка буфера длинной 3 байта с использованием  прерываний

Все работает правильно, сколько сказали — столько и отправили 😉 Байты идут друг за другом без задержек. Отлично 🙂

А вот так выглядит отправка 10-и байт:

Рис. 13. Отправка буфера длинной 10 байт с использованием  прерываний

На этом пока все, статья и так получилась большой. Что будет в следующей части еще не решил, но надо бы сделать статьи про контроллер прерываний NVIC и контроллер прямого доступа к памяти DMA. Ну и SPI в режиме Slave не мешало бы рассмотреть. Всем спасибо за внимание, задавайте вопросы на мыло или мне во Вконтакте, ссылки в разделе «Контакты» вверху страницы.

Все статьи цикла тут: http://dimoon.ru/category/obuchalka/stm32f1

Метки: , , , . Закладка Постоянная ссылка.

8 комментариев: Программирование STM32. Часть 6: SPI

  1. Alex пишет:

    поясните плиз почему
    SPI1->CR1 |= 1<CR1 |= 1<<SPI_CR1_SPE; //Включаем SPI

    что есть _Pos? Where I can read about it?

    • DiMoon пишет:

      Если открыть файл stm32f103xb.h, то там можно найти следующее:

      #define SPI_CR1_SPE_Pos (6U)
      #define SPI_CR1_SPE_Msk (0x1U << SPI_CR1_SPE_Pos)
      #define SPI_CR1_SPE SPI_CR1_SPE_Msk

      Бит SPE является 6-м по счету в регистре CR1. SPI_CR1_SPE_Pos — это позиция бита в регистре. Однако, для того, чтобы этот бит изменить, нам нужна не позиция бита, а битовая маска, которую можно получить вот таким образом:

      (1<<SPI_CR1_SPE_Pos)

      И в коде у нас будет следующее:

      SPI1->CR1 |= (1<<SPI_CR1_SPE_Pos);

      Но в данном случае можно не париться и использовать другой #define, а именно SPI_CR1_SPE_Msk — маска бита, ну или SPI_CR1_SPE, что является синонимом SPI_CR1_SPE_Msk:

      SPI1->CR1 |= SPI_CR1_SPE; //можно и так

      Но надо помнить, что у нас есть параметры, которые занимают не один бит, а несколько, например, SPI_CR1_BR (установка скорости SPI, занимает 3 бита). Вот тут как раз и будет полезен #define с постфиксом _Pos. Для создания битовой маски мы можем сделать следующее:

      (0x04<<SPI_CR1_BR_Pos) — Записать число 0x04 в биты BR.

  2. Олег пишет:

    Подскажите пожалуйста, как сделать так, чтобы после отправки, скажем, трёх байт stm устанавливал высокий уровень на линии NSS как сигнал об окончании команды. NSSP может делать строб после каждого байта, но это не то. Такое необходимо, чтобы обозначит окончание отправки команды, к примеру, в чип eeprom.

    • DiMoon пишет:

      NSS в STM32 очень неудобный, поэтому тут лучше всего реализовать это программным способом. В прерывании об окончании приема (если сработало RXNEIE, то это означает, что данные уже точно были отправлены, и есть порция очередных данных для считывания) увеличивать некий счетчик, и когда он станет равен количеству отправленных байт, то поднимать NSS. Это можно использовать и с DMA, и без него.

  3. Алексей пишет:

    Очень познавательно, вот ещё в режиме Slave было-бы не плохо. Просто не совсем понятно как Slave принимать массив от Master? Сколько не пробовал как-то странно всё получается.

  4. Михаил пишет:

    А будет ли статья на тему двухсторонней связи МК с ПК по Virtual COM Port? Очень хотелось бы увидеть как это все настраивается через регистры.

    • DiMoon пишет:

      Virtual COM Port имеется ввиду тот, который эмулируется по USB? Пока по USB ни чего не планирую, тема очень обширная и сложная. Советую использовать мосты USB-UART типа FT232 и подобные

  5. Евгений пишет:

    я может ошибаюсь, но помоему в самом обработчике прерывания, первой стракой необходимо отключить прерывания по опустошению буфера? Ведь оно может сработать еще в самом обработчике прерывания, если скорость Spi больше чем время затраченное на сложение и т.к. Т.е. как только в обработчике мы запишем очередной байт из массива в dr, spi начнет его передовать и если флаг опустошения tx поднимется раньше чем пройдет операция увелечения tx_index и т.д. то прерывания сработает и начнется сначала?

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *