Введение
Помимо простых цифровых GPIO-пинов, Raspberry Pi поддерживает шины I2C и SPI — те же протоколы, что разбирались в Arduino-цикле для подключения дисплеев, RTC-модулей и датчиков (статьи 282-283, 299). В статье разберём, как обращаться к устройствам на этих шинах непосредственно из Qt-приложения на Linux, без посредничества микроконтроллера.
Концепция
Linux предоставляет доступ к шинам I2C через файлы устройств /dev/i2c-N и к SPI через /dev/spidevN.N — взаимодействие происходит через стандартные системные вызовы ioctl()/read()/write(), для которых существуют удобные библиотеки-обёртки (libi2c-dev для I2C). В отличие от Arduino, где Wire.h/SPI.h дают высокоуровневый интерфейс прямо в составе ядра прошивки, на Linux взаимодействие с шинами идёт через файловую систему /dev, что требует прав доступа (аналогично GPIO из статьи 343) и явного указания I2C-адреса устройства при каждой операции.
Пример кода: I2C и SPI устройства
# Включение I2C/SPI через raspi-config или напрямую в конфигурации загрузки
sudo raspi-config # Interface Options -> I2C / SPI -> Enable
# Проверка доступных устройств на шине I2C
sudo apt install i2c-tools
i2cdetect -y 1 # сканирование шины 1 (стандартная для большинства моделей Raspberry Pi)
// i2csensor.h — чтение данных с I2C-датчика (например, BMP280 для температуры/давления)
#pragma once
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/i2c-dev.h>
#include <stdexcept>
class I2cSensor
{
public:
I2cSensor(const std::string &devicePath, int deviceAddress)
{
m_fd = open(devicePath.c_str(), O_RDWR);
if (m_fd < 0) {
throw std::runtime_error("Не удалось открыть устройство I2C: " + devicePath);
}
if (ioctl(m_fd, I2C_SLAVE, deviceAddress) < 0) {
close(m_fd);
throw std::runtime_error("Не удалось установить адрес устройства I2C");
}
}
~I2cSensor() { close(m_fd); }
uint8_t readRegister(uint8_t reg)
{
if (write(m_fd, ®, 1) != 1) {
throw std::runtime_error("Ошибка записи адреса регистра");
}
uint8_t value;
if (read(m_fd, &value, 1) != 1) {
throw std::runtime_error("Ошибка чтения значения регистра");
}
return value;
}
void writeRegister(uint8_t reg, uint8_t value)
{
uint8_t buffer[2] = {reg, value};
if (write(m_fd, buffer, 2) != 2) {
throw std::runtime_error("Ошибка записи в регистр");
}
}
private:
int m_fd;
};
// Использование — чтение идентификатора чипа BMP280 (регистр 0xD0, ожидаемое значение 0x58)
I2cSensor bmp280("/dev/i2c-1", 0x76);
uint8_t chipId = bmp280.readRegister(0xD0);
if (chipId == 0x58) {
qDebug() << "Обнаружен датчик BMP280";
} else {
qWarning() << "Неожиданный идентификатор чипа:" << Qt::hex << chipId;
}
// SPI-дисплей — аналогичный подход через /dev/spidevN.N
#include <linux/spi/spidev.h>
class SpiDevice
{
public:
SpiDevice(const std::string &devicePath, uint32_t speedHz)
{
m_fd = open(devicePath.c_str(), O_RDWR);
uint8_t mode = SPI_MODE_0;
ioctl(m_fd, SPI_IOC_WR_MODE, &mode);
ioctl(m_fd, SPI_IOC_WR_MAX_SPEED_HZ, &speedHz);
}
void transfer(const uint8_t *txBuffer, uint8_t *rxBuffer, size_t length)
{
spi_ioc_transfer transfer{};
transfer.tx_buf = reinterpret_cast<unsigned long>(txBuffer);
transfer.rx_buf = reinterpret_cast<unsigned long>(rxBuffer);
transfer.len = length;
ioctl(m_fd, SPI_IOC_MESSAGE(1), &transfer);
}
private:
int m_fd;
};
Пояснения к коду
I2cSensor показывает типичный паттерн работы с I2C на Linux — ioctl(m_fd, I2C_SLAVE, deviceAddress) устанавливает адрес устройства на шине для всех последующих операций с этим файловым дескриптором, после чего readRegister()/writeRegister() реализуют стандартный протокол доступа к регистрам устройства (запись адреса интересующего регистра, затем чтение значения) — концептуально идентично тому, что Wire.h делает на Arduino, но через более низкоуровневый интерфейс файловых операций Linux. i2cdetect -y 1 — практичный диагностический инструмент, показывающий, какие адреса устройств реально отвечают на шине, что помогает быстро проверить физическое подключение и правильность адреса устройства до написания какого-либо кода.
Подводные камни
- Не включённая в конфигурации Raspberry Pi поддержка I2C/SPI (через
raspi-configили прямое редактирование/boot/config.txt) — без явного включения соответствующего интерфейса файлы устройств/dev/i2c-1//dev/spidev0.0просто не будут существовать, и попытка их открыть завершится понятной, но легко упускаемой при первой настройке ошибкой «файл не найден». - Использование неправильного номера шины I2C — разные модели и ревизии Raspberry Pi могут иметь разное соответствие физических пинов и номеров шин (
/dev/i2c-0против/dev/i2c-1), и код, написанный для одной модели, может не работать на другой без проверки актуальной разводки конкретной используемой модели. - Отсутствие проверки прав доступа к файлам
/dev/i2c-N//dev/spidevN.Nдля запуска не от root — аналогично GPIO из статьи 343, эти устройства обычно принадлежат специальным группам (i2c,spi), и пользователь, под которым работает Qt-приложение, должен быть явно добавлен в соответствующую группу для доступа без повышенных привилегий. - Игнорирование задержек, требуемых датчиком между операциями записи и чтения (например, время на выполнение измерения внутри самого датчика) — в отличие от простого протокола из примера, многие реальные датчики требуют выждать определённое время после команды «начать измерение» перед попыткой чтения результата, и слишком быстрое чтение результата вернёт устаревшее или некорректное значение — даталист (datasheet) конкретного используемого датчика всегда должен быть проверен на наличие таких требований к тайминга.