Продвигаемся по нашему учебному курсу, посвященному работе с Raspberry Pi! Героем сегодняшней статьи будет интерфейс SPI. Разберем его включение, настройку и, конечно же, напишем реальный пример использования. Приступаем...
Как и в статье по работе с шиной I2C, для полноценности примера возьмем какое-нибудь устройство и подключим к Raspberry Pi по SPI. Из имеющегося у меня в наличии идеально подойдет датчик L3GD20. Схема подключения будет такой:
Raspberry будет играть роль ведущего устройства SPI (master), а гироскоп - подчиненного (slave). Здесь мы используем сигналы:
- SPI0_CE0_N - chip select
- SPI0_MOSI - master output slave input - для передачи данных от ведущего к подчиненному
- SPI0_MISO - master input slave output - для приема данных от slave
- SPI0_SCLK - тактовый сигнал - его также генерирует master
И, опять же, использование именно этого датчика не накладывает никаких ограничений - с любым другим устройством можно будет работать точно так же. L3GD20 выбран только для наглядности. Итак, переходим к программным аспектам.
Включение SPI на Raspberry Pi.
Для того, чтобы активировать интерфейс SPI платы Raspberry Pi можно воспользоваться утилитой raspi-config. Вводим команду:
sudo raspi-config
После запуска утилиты идем по пути Interfacing options - SPI и включаем нужный нам интерфейс:
Другим способом активации SPI является непосредственное редактирование файла /boot/config.txt. В нем нужно раскомментировать строку:
dtparam=spi=on
В обоих случаях после изменения конфигурации необходимо перезапустить плату:
sudo reboot
Не важно, каким именно способом пользоваться, результат будет одинаков. Для проверки вводим команду:
ls /dev
И в выводе команды в консоли видим такие строки:
Наличие в списке spidev0.0 и spidev0.1 сигнализирует нам о том, что интерфейс SPI успешно активирован. Первая цифра (0) в наименовании указывает, что используется SPI0, к которому и подключен датчик на нашей схеме.
А вторая цифра (0 или 1) определяет, какой вывод платы будет использован в качестве Chip Select'а. Здесь есть два варианта:
- SPI0_CE0_N
- SPI0_CE1_N
У нас использован SPI0_CE0_N, поэтому нас интересует spidev0.0. Два вывода Chip Select нужны для того, чтобы реализовать возможность подключения двух разных устройств к SPI0, каждому из которых будет соответствовать свой сигнал Chip Select (CS), а линии данных и тактирования (MOSI, MISO, SCLK) будут общими.
При таком подключении, если мы собираемся обмениваться данными с одним из устройств, то следует подать низкий уровень (0) на его вывод CS. А на CS второго устройства - высокий уровень (1). Таким образом и осуществляется переключение между устройствами, использующими одни и те же линии данных.
Работа с SPI на Python.
Здесь нам понадобится модуль python-spidev, который можно установить командой:
sudo apt-get install python-spidev
Если при запуске написанного кода будут возникать ошибки, то поможет принудительная установка версии 3.4 пакета:
pip3 install spidev==3.4 --force-reinstall
Теперь уже можно переходить к написанию программы. Первый делом для использования SPI делаем import:
import spidev
Далее нужно создать объект для работы с SPI и произвести подключение:
spi = spidev.SpiDev() spi.open(0, 0)
Аргументы функции open() как раз и позволяют выбрать использующийся модуль и сигнал Chip Select, который мы обсудили ранее. В нашем примере - SPI0 и SPI0_CE0_N. В целом, после вызова этих функций уже можно начинать работу с SPI, но давайте рассмотрим помимо прочего доступные варианты конфигурации:
- bits_per_word - количество битов в слове.
- cshigh - в активном режиме сигнал на выходе Chip Select может быть либо высокого, либо низкого уровня. Для первого случая ставим cshigh = True, для второго, соответственно - cshigh = False.
- loop - включение/отключение loopback режима.
- no_cs - записываем в параметр True, если не используем Chip Select.
- lsbfirst - выбираем, наиболее или наименее значимый бит будет первым в данных.
- max_speed_hz - максимальная частота интерфейса.
- mode - в этот параметр записываем два бита, например 0b01:
Бит | Сигнал | Описание | Значения |
---|---|---|---|
Старший | CPOL | Исходный уровень сигнала синхронизации | 0: низкий уровень 1: высокий уровень |
Младший | CPHA | Фаза синхронизации | 0: по переднему фронту - выборка данных, по заднему - установка 1: по переднему фронту - установка данных, по заднему - выборка |
К примеру, если mode = 0b10, то исходный уровень сигнала синхронизации - высокий, по переднему фронту происходит выборка данных, по заднему - установка.
- threewire - режим, при котором вместо двух линий MOSI и MISO используется одна, объединяющая в себе их функции.
В коде установка параметров может происходить следующим образом:
spi.lsbfirst = False spi.cshigh = False spi.mode = 0b01 spi.bits_per_word = 8
Пришло время, наконец, рассмотреть и основные функции для приема и передачи данных:
- xfer(list of values[, speed_hz, delay_usec, bits_per_word])
Функция осуществления SPI транзакции. В квадратных скобках указаны необязательные аргументы, среди которых последовательно: частота, величина задержки между данными в микросекундах и количество битов в слове. Обязательный аргумент list of values - непосредственно передаваемые данные. Возвращает же функция данные, принятые от подчиненного устройства.
- xfer2(list of values[, speed_hz, delay_usec, bits_per_word])
Аналогичная функция, отличие от xfer() заключается в том, что сигнал Chip Select остается активным между транзакциями. В случае же с xfer() после каждой отправки CS переходит в неактивное состояние, а затем снова активируется.
- xfer3(list of values[, speed_hz, delay_usec, bits_per_word])
И еще одна функция из этой группы. Ее отличие заключается в том, что если размер передаваемых данных превышает максимально допустимый, то данные будут разбиты на порции и отправлены по частям. Максимальный размер передаваемых данных задается через /sys/module/spidev/parameters/bufsiz.
- readbytes(n)
Функция приема данных, аргумент - количество байт, которые необходимо прочитать.
- writebytes(list of values)
Передача данных, задаваемых аргументом функции.
- writebytes2(list of values)
И похожая на предыдущую функция, отличающаяся тем, что разбивает данные на части в случае превышения максимального размера. В общем, аналогично функции xfer3().
Итак, именно при помощи этих функций и организуется обмен данными по интерфейсу SPI.
А теперь вспомним про наш подключенный датчик L3GD20 и напишем небольшой пример для чтения значений его регистров. В частности, есть один регистр (адрес 0x0F), значение которого строго фиксировано и равно 0xD4. Его значение и прочитаем, а затем сравним с заведомо известной верной величиной 0xD4 (11010100):
Но нужно, в двух словах, особо не углубляясь, рассмотреть процесс обмена данными с L3GD20. В целом выглядит это так:
В рамках рассматриваемой нами сегодня задачи это значит следующее. Для того, чтобы запросить значение регистра с адресом 0x0F, нам нужно отправить подчиненному (датчику) команду 0x8F. В ответ на это гироскоп отправит значение регистра. Нюанс заключается в том, что, отправив команду, ведущий (Raspberry Pi) должен обеспечивать тактирование на линии в то время как идет отправка данных подчиненным. Для того, чтобы это осуществить, отправим 2 байта - 0x8F и 0x00. Второй байт не несет никакой смысловой нагрузки, он нужен только для того, чтобы master продолжал выдавать сигнал тактирования в то время, пока slave отправляет данные.
Создаем файл spi_test.py. Итоговый код будет выглядеть так:
import spidev spi = spidev.SpiDev() spi.open(0, 0) spi.max_speed_hz = 4000000 txData = [0x8F, 0x00] rxData = spi.xfer(txData) for i in range(len(rxData)): print (hex(rxData[i])) spi.close()
Запускаем:
python spi_test.py
И в результате получаем правильное значение:
Отправляем два байта, как и планировали, и в ответе от slave получаем также два байта, первый из которых не несет полезной информации. Он соответствует тому периоду времени, когда master отправлял запрос (0x8F), соответственно на шине было тактирование, а на MISO в этот момент был низкий уровень, который и был прочитан как 0x00.
Вот на успешном запуске программы и закончим сегодняшнюю статью, всем спасибо за внимание и до скорого 🤝