repka-team
repka-team
794 просмотров0 комментариев

Проект “Метеостанция”

#PWM #UART #I2C #SPI #CAN

Это статья №1 цикла статей с примерами проектов на базе Repka Pi 4

Содержание #

  1. Введение
  2. Электрическая принципиальная схема
  3. Монтажная схема
  4. Сборка проекта
  5. Запуск проекта
  6. Программная реализация
  7. Практическая значимость проекта
  8. Расширение проекта
  9. Видеообзор проекта

Введение #

Первый проект нашего цикла - система для мониторинга погодных условий в реальном времени или метеостанция. Это проект “Метеостанция”, разработанный и доступный в рамках учебно-методического комплекса на базе одноплатного компьютера Repka PI 4. Проект будет собираться с использованием компонентов учебно-методический комплекса УМК “REPKA”. Монтажные и принципиальные схемы и подробное описание можно найти в разделе "Примеры готовых проектов" учебного пособия УМК “REPKA”, а в рамках статьи будут даны так же инструкции для более простого монтажа в домашних условиях.

Построим пример такого проекта с использованием датчика BME280, который измеряет атмосферное давление, температуру и влажность, а отображение данных реализуем на ЖК дисплее 1602 I2C и в консоли одноплатника Repka Pi.

Также все необходимые материалы и схемы подключения доступны в репозитории на платформе Gitflic.

Используемые в проекте компоненты

1. Датчик атмосферного давления и температуры (BME280) см. рисунок 1. Измеряет атмосферное давление, температуру и влажность.

2. ЖК дисплей (1602 I2C). Отображает показания с датчика BME280 и позволяет пользователю видеть информацию о текущих погодных условиях.

Вы можете приобрести все необходимые компоненты отдельно от "Учебно-методический комплекс REPKA". Ссылки на модули приведены в таблице ниже.

Компонент Ссылка на приобретение
Монтажная/макетная плата Ссылка
Шлейф Ссылка
Переходник с шлейфа на макетную плату Ссылка
Соединительные провода

Провода соединительные м-п

Провода соединительные п-п

Провода соединительные п-п

Датчик атмосферного давления и температуры (BME280) Ссылка
ЖК дисплей (1602 I2C) Ссылка

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

Во время сборки проекта будем регулярно обращаться к электрической принципиальной схеме и монтажной схеме, представленными в учебном пособии (см. рисунки 3 и 4). Эти схемы будут служить основным ориентиром на всех этапах подключения компонентов, обеспечивая точность и правильность сборки устройства.

Для разработки кода будет использоваться текстовый редактор Geany, который входит в состав стандартного ПО Репка ОС.

Электрическая принципиальная схема

Монтажная схема

Т-образная расширительная плата GPIO полностью повторяет конфигурацию распиновки:

Сборка проекта

1. Подключение датчика BME280.

Как видно из рисунков 3 и 4 датчик подключается через интерфейс I2C и питается от 3.3V.

1.1. Подключаем BME280 к макетной плате согласно таблице 1:

Макетная плата BME280
3.3V VIN
GND GND
SCL1 SCL
SDA1 SDA

Таблица 1. Подключение датчика BME280 к макетной плате.

1.2.  Результат подключения будет выглядеть следующим образом, см. рисунок 5:

2. Для проверки правильности подключения используем Python скрипт из репозитория repka-pi_iot-examples.

2.1. Клонируем репозиторий:

git clone git@gitflic.ru:repka_pi/repka-pi_iot-examples.git

2.2. Переходим в репозиторий:

cd repka-pi_iot-examples/

2.3. Выполним установку зависимостей.

2.3.1. Если хотите установить зависимости только для датчик атмосферного давления и температуры (BME280), выполните:

make setup-bme280

2.3.2. Если хотите установить зависимости для всех датчиков и проектов, выполните:

make setup-all

2.4. Запускаем скрипт для проверки:

make bme280

2.5. Если на этапе 2.4. возникает ошибка, то измените в python скрипте, который находится по пути: /devices/sensors/BME280_example/py, номер шины – bus_number, если ошибок нет, то пропустите данный пункт.

 # Укажите номер шины
bus_number = 1 
# Инициализация I2C-шины
bus = SMBus(bus_number)
sensor = BME280(i2c_dev=bus)  # Инициализация

2.6. Из рисунка 6 видим, что скрипт успешно выполнился, нам удалось считать показания с датчика.

3. Подключение ЖК дисплея (1602 I2C).

Из рисунков 3 и 4 видно, что устройство подключается аналогично датчику BME280 по интерфейсу I2c, но питается уже от 5V.

3.1. Выполним подключение к макетной плате согласно таблице 2.

Макетная плата

1602 I2C

5V

VCC

GND

GND

SCL1

SCL

SDA1

SDA

Таблица 2. Подключение ЖК дисплея (1602 I2C) к макетной плате.

3.2. Результат подключения будет выглядеть следующим образом, см. рисунок 7:

4. Аналогично пункту 2 выполним проверку подключения датчика:

4.1. Установим зависимости для ЖК дисплея (1602 I2C), выполнив:

make setup-display-1602-i2c

4.2. Выполните команду:

make display-1602-i2c

4.3. Как видно из рисунка 8 датчик успешно подключен.

Запуск проекта

Теперь, когда все компоненты подключены, можно запустить проект "Метеостанция". Для этого в репозитории repka-pi_iot-examples выполняем команду:

make weather-station

Данные о температуре, давлении и влажности теперь отображаются на дисплее и выводятся в консоль, см. рисунок 9 и 10.

Вы можете собрать более бюджетную версию данного проекта.

Для более бюджетной реализации проекта достаточно использовать Repka Pi 4 в стандартной комплектации и макетную плату без внешнего источника питания — остальные компоненты остаются неизменными. Макетную плату можно приобрести здесь.

Для подключения нам потребуется “Распиновка портов на 40 pin разъёме на Repka Pi 4“, см. изображение ниже.

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

5. В соответствии с таблицей 1 и рисунком 14, выполним подключение датчика BME280:

Проверка подключения датчика осуществляется аналогично пункту 2.

6. В соответствии с таблицей 2 и рисунком, выполним подключение ЖК дисплея (1602 I2C):

Питание устройств также можно осуществлять с использованием:

Внимание! Подаваемое напряжение не должно превышать 5 В.

А. Лабораторного блока питания:

Б. Аккумуляторного блока питания:

Проверка подключения датчика осуществляется аналогично пункту 4.

Программная реализация #

Программное обеспечение для проекта "Метеостанция" отвечает за инициализацию датчиков, периодическое считывание метеорологических данных и их отображение в удобном для пользователя виде. В этом разделе мы подробно разберем логику работы программы и два подхода к ее реализации на языке Python.

Алгоритм работы #

Логика работы метеостанции очень проста и представляет собой бесконечный цикл опроса и отображения данных. Алгоритм наглядно представлен на блок-схеме.


Описание алгоритма:

  1. Старт и инициализация: При запуске программа первым делом инициализирует все необходимые аппаратные компоненты: устанавливает соединение по шине I2C с датчиком BME280 и LCD-дисплеем.
  2. Начало цикла: Программа входит в бесконечный цикл для обеспечения непрерывной работы.
  3. Сбор данных: На каждой итерации цикла происходит опрос датчика BME280 для получения актуальных значений температуры, атмосферного давления и влажности.
  4. Вывод в консоль: Полученные данные форматируются и выводятся в консоль для отладки и мониторинга.
  5. Вывод на дисплей: Экран LCD-дисплея очищается, после чего на него выводятся те же метеоданные в компактном и читаемом формате.
  6. Пауза: Программа делает паузу на 4 секунды. Это необходимо, чтобы данные на экране не менялись слишком быстро и были удобны для восприятия, а также для снижения нагрузки на процессор.
  7. Повторение: После паузы цикл начинается заново с пункта 3.
  8. Завершение: Цикл прерывается пользователем (например, нажатием Ctrl+C). При этом программа корректно завершает работу, очищая дисплей.

Реализация на Python #

Python идеально подходит для этого проекта, так как существующие библиотеки для датчика BME280 и дисплея LCD1602 берут на себя всю сложность низкоуровневого взаимодействия по протоколу I2C.

1. Функциональный подход #

Этот подход является самым прямолинейным. Вся программа представляет собой один скрипт, который выполняется последовательно сверху вниз.

Импорты и обработка аргументов

В начале скрипта импортируются все необходимые библиотеки и настраивается парсер аргументов командной строки для гибкой конфигурации.

import time
import argparse
from smbus2 import SMBus
from rpi_bme280 import BME280
from RPLCD.i2c import CharLCD

# Используется парсер аргументов командной строки для задания номера I2C-шины и адреса дисплея:
parser = argparse.ArgumentParser(description="BME280 + LCD1602 I2C вывод в консоль и на дисплей")
parser.add_argument('--bus', type=int, default=0, help="I2C шина (по умолчанию 1)")
parser.add_argument('--lcd_addr', type=lambda x: int(x,0), default=0x27, help="Адрес LCD дисплея (например, 0x27)")
args = parser.parse_args()

Объяснение:

  • argparse: Стандартная библиотека Python для создания гибких программ, запускаемых из командной строки. Она позволяет пользователю при запуске указать параметры, например, номер шины I2C (--bus 1) или адрес дисплея (--lcd_addr 0x3f), если они отличаются от стандартных.
  • smbus2, rpi_bme280, RPLCD.i2c: Специализированные библиотеки, которые предоставляют готовые и удобные инструменты для работы с устройствами по шине I2C.

Инициализация и основной цикл

После получения аргументов происходит инициализация устройств и запуск главного цикла работы.

# Инициализация I2C-шины, датчика BME280 и LCD-дисплея:
bus = SMBus(args.bus)
sensor = BME280(i2c_dev=bus)
lcd = CharLCD('PCF8574', args.lcd_addr)

try:
    while True:
        # Считывание значений
        temperature = sensor.get_temperature()
        pressure = sensor.get_pressure()
        humidity = sensor.get_humidity()

        # Вывод в консоль
        print(f"Температура = {temperature:.2f} °C")
        print(f"Давление    = {pressure:.2f} hPa")
        print(f"Влажность   = {humidity:.2f} %")
        print("-------------------------------")

        # Вывод на дисплей
        lcd.clear()
        lcd.write_string(f"T:{temperature:.1f}C P:{pressure:.0f}hPa")
        lcd.crlf() # Переход на новую строку
        lcd.write_string(f"H:{humidity:.1f}%")

        # Пауза
        time.sleep(4)

except KeyboardInterrupt:
    lcd.clear()
    print("Завершение работы")

Объяснение:

  • Инициализация: Создаются экземпляры объектов SMBus (для шины I2C), BME280 (для датчика) и CharLCD (для дисплея). Библиотеки берут на себя всю "магию" настройки связи с устройствами.
  • Основной цикл: Вся логика заключена в бесконечном цикле while True, обернутом в try...except.
    • Чтение: Вызовы sensor.get_temperature(), get_pressure() и get_humidity() обращаются к датчику по I2C и возвращают уже обработанные значения в нужных единицах измерения.
    • Вывод: Данные выводятся в двух местах: в консоль с помощью print() и на дисплей с помощью методов lcd.clear() и lcd.write_string(). Форматирование строк (f-strings) позволяет красиво отображать числа с нужным количеством знаков после запятой.
    • Завершение: Блок except KeyboardInterrupt отлавливает нажатие Ctrl+C, позволяя программе завершиться корректно: очистить экран дисплея и вывести прощальное сообщение.
2. Объектно-ориентированный подход (ООП) #

Этот подход инкапсулирует всю логику работы метеостанции в единый класс WeatherStation. Это делает код более структурированным, удобным для расширения и повторного использования в других проектах.

Определение класса WeatherStation

Класс содержит методы для инициализации, чтения данных и их отображения.

class WeatherStation:
    def __init__(self, bus_num=0, lcd_addr=0x27):
        """
        Конструктор класса. Здесь происходит инициализация всех необходимых устройств.
        """
        self.bus = SMBus(bus_num)
        self.sensor = BME280(i2c_dev=self.bus)
        self.lcd = CharLCD('PCF8574', lcd_addr)

    def read_data(self):
        """
        Считывает данные с датчика BME280.
        Возвращает кортеж: (температура, давление, влажность).
        """
        temperature = self.sensor.get_temperature()
        pressure = self.sensor.get_pressure()
        humidity = self.sensor.get_humidity()
        return temperature, pressure, humidity

    def display_data(self, temperature, pressure, humidity):
        """
        Выводит данные на LCD-дисплей.
        """
        self.lcd.clear()
        self.lcd.write_string(f"T:{temperature:.1f}C P:{pressure:.0f}hPa")
        self.lcd.crlf()
        self.lcd.write_string(f"H:{humidity:.1f}%")

    def print_data(self, temperature, pressure, humidity):
        """
        Выводит данные в консоль.
        """
        print(f"Температура = {temperature:.2f} °C")
        # ... (остальные print'ы)

Объяснение:

  • __init__ (Конструктор): Этот метод вызывается при создании объекта WeatherStation. Он выполняет ту же инициализацию, что и в функциональном подходе, но сохраняет объекты bus, sensor и lcd как атрибуты экземпляра (через self.).
  • Разделение ответственности: Вместо одной большой логики в цикле, мы разделили ее на три метода: read_data отвечает только за чтение с датчика, display_data — только за вывод на экран, а print_data — только за вывод в консоль. Это делает код гораздо чище и проще для понимания.

Основной цикл и точка входа

Метод run() инкапсулирует главный цикл, а блок if __name__ == "__main__" запускает всю систему.

    def run(self):
        """
        Основной цикл работы станции.
        """
        try:
            while True:
                temperature, pressure, humidity = self.read_data()
                self.print_data(temperature, pressure, humidity)
                self.display_data(temperature, pressure, humidity)
                time.sleep(4)
        except KeyboardInterrupt:
            self.lcd.clear()
            print("Завершение работы")

if __name__ == "__main__":
    # Создаём экземпляр класса WeatherStation
    station = WeatherStation(bus_num=1, lcd_addr=0x27)
    # Запускаем основной цикл работы станции
    station.run()

Объяснение: Модуль инкапсулирует в себе протокол управления дисплеем через I2C-расширитель PCF8574. Функции send_command и send_data отправляют на дисплей либо управляющие команды (очистить экран, переместить курсор), либо байты данных (коды символов для отображения). Функция lcd_print просто перебирает символы в строке и отправляет каждый из них на дисплей. Для main.c вся эта сложность скрыта за простыми и понятными вызовами функций. Объяснение:

  • run(): Этот метод является сердцем объекта. Он организует работу, вызывая другие методы класса (self.read_data(), self.print_data() и т.д.) в правильном порядке внутри бесконечного цикла.
  • if __name__ == "__main__": Эта стандартная конструкция Python позволяет файлу работать в двух режимах. Если вы запускаете этот файл напрямую, она выполнится, создаст объект WeatherStation и запустит его. Если же вы импортируете этот файл в другой проект, чтобы, например, использовать класс WeatherStation, этот блок выполнен не будет. Это делает код идеально подходящим для повторного использования.

Реализация на C #

Язык C в связке с библиотекой WiringRP представляет собой классический подход для программирования встраиваемых систем. Он обеспечивает максимальную производительность, низкое потребление ресурсов и полный, прямой контроль над аппаратным обеспечением, что критически важно для надежной и предсказуемой работы устройств.

1. Реализация с WiringRP #

Данный проект демонстрирует профессиональный подход к разработке на C, используя модульную структуру. Для каждого аппаратного компонента создан свой "драйвер" в виде пары файлов: заголовочного (.h) и файла реализации (.c). Это делает код чистым, переиспользуемым и легким для отладки.

Структура проекта:

  • main.c: Содержит основную логику приложения, инициализацию всех модулей и главный цикл работы.
  • bme280_driver/: Драйвер для чтения и обработки данных с датчика BME280.
  • lcd1602_i2c/: Драйвер для управления LCD-дисплеем 1602 по шине I2C.

Основной файл логики (main.c)

Этот файл является точкой входа и "мозгом" всей системы. Он инициализирует все модули и реализует основной цикл опроса и отображения данных.

#include <stdio.h>
#include <stdlib.h>
#include <wiringrp/wiringRP.h>
#include <wiringrp/wire.h>

// Подключение заголовочных файлов всех модулей
#include "bme280_driver/bme280_driver.h"
#include "lcd1602_i2c/lcd1602_i2c.h"

// --- НАСТРОЙКА АДРЕСОВ ---
#define BME280_ADDR   0x76
#define LCD_ADDR      0x27
#define I2C_BUS_ID    I2C1_BUS

void setup() {
    printf("Запуск метеостанции...\n");
    if (setupWiringRP(WRP_MODE_PHYS) < 0) exit(EXIT_FAILURE);
    
    // Инициализация каждого аппаратного модуля с проверкой ошибок
    if (bme280_init(I2C_BUS_ID, BME280_ADDR) < 0) exit(EXIT_FAILURE);
    if (lcd_init(I2C_BUS_ID, LCD_ADDR) < 0) exit(EXIT_FAILURE);
    
    printf("Метеостанция готова.\n");
    lcd_print("Weather Station");
    delay(2000);
}

void loop() {
    BME280_Data sensor_data;
    char line1_buffer[17];
    char line2_buffer[17];
    
    if (bme280_read_data(&sensor_data) == 0) {
        // Вывод в консоль
        printf("Температура: %.2f C, Давление: %.2f hPa, Влажность: %.2f %%\n",
               sensor_data.temperature, sensor_data.pressure, sensor_data.humidity);

        // Форматирование строк и вывод на LCD
        snprintf(line1_buffer, 17, "T:%.1fC P:%.0fhPa", sensor_data.temperature, sensor_data.pressure);
        snprintf(line2_buffer, 17, "H: %.1f %%", sensor_data.humidity);
        
        lcd_clear();
        lcd_print(line1_buffer);
        lcd_set_cursor(0, 1);
        lcd_print(line2_buffer);
    } else {
        lcd_clear();
        lcd_print("Sensor Error!");
    }
    
    delay(4000);
}

ONDESTROY() {
    printf("\nЗавершение работы.\n");
    lcd_clear();
    bme280_release();
    lcd_release();
    releaseWiringRP();
    exit(0);
}

MAIN_WIRINGRP();

Объяснение:

  • Структура setup() и loop(): Библиотека WiringRP предоставляет макросы MAIN_WIRINGRP(), setup() и loop(), которые эмулируют привычную и удобную среду программирования Arduino. Код в setup() выполняется один раз при старте для инициализации всех систем, а loop() — в бесконечном цикле.
  • Инициализация: В setup() последовательно вызываются функции ..._init() из каждого модуля, передавая им необходимые параметры (шину I2C и адрес устройства). При этом результат каждой функции проверяется, и в случае ошибки программа завершается с информативным сообщением.
  • Основной цикл loop(): Здесь реализована вся логика, описанная в блок-схеме. Каждые 4 секунды (delay(4000)) с помощью функции bme280_read_data считываются данные в структуру sensor_data. Затем они форматируются в строки с помощью snprintf и выводятся как в консоль, так и на LCD-дисплей.
  • Безопасное завершение ONDESTROY(): Этот макрос регистрирует функцию, которая будет вызвана при завершении программы (например, по Ctrl+C). Это гарантирует, что дисплей будет очищен, а все аппаратные ресурсы — корректно освобождены.

Модуль датчика BME280 (bme280_driver.c)

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

// Фрагмент из bme280_driver.c
#include "bme280_driver.h"
#include <wiringrp/wiringRP.h>
#include <wiringrp/wire.h>
#include <math.h>

// ... (объявление статических переменных для калибровочных коэффициентов) ...
static int32_t t_fine;

static void read_calibration_data() {
    // ... (чтение ~24 байт калибровочных данных с датчика) ...
}

static float compensate_temperature(int32_t adc_T) {
    // ... (сложные математические расчеты по формулам из документации) ...
}
static float compensate_pressure(int32_t adc_P) {
    // ... (сложные математические расчеты по формулам из документации) ...
}
static float compensate_humidity(int32_t adc_H) {
    // ... (сложные математические расчеты по формулам из документации) ...
}

int bme280_init(int i2c_bus, int addr) {
    // ... (инициализация I2C, проверка Chip ID) ...
    read_calibration_data(); // Считываем уникальные для каждого датчика коэффициенты
    // ... (настройка режимов работы датчика) ...
    return 0;
}

int bme280_read_data(BME280_Data* data) {
    // ... (чтение 8 байт "сырых" данных температуры, давления и влажности) ...
    
    // Передаем сырые данные в функции компенсации
    data->temperature = compensate_temperature(adc_T);
    data->pressure = compensate_pressure(adc_P);
    data->humidity = compensate_humidity(adc_H);
    
    return 0;
}

Объяснение: Датчик BME280 — это сложное устройство. Он не выдает готовые значения в градусах Цельсия или гектопаскалях. Вместо этого он возвращает "сырые" цифровые значения (adc_T, adc_P, adc_H).

  • Калибровка: При производстве в каждый чип BME280 записываются уникальные калибровочные коэффициенты. Функция read_calibration_data считывает их один раз при инициализации и сохраняет в статических переменных модуля.
  • Компенсация: Функции compensate_* реализуют сложные математические формулы, предоставленные производителем в документации (datasheet). Эти формулы используют "сырые" данные и уникальные калибровочные коэффициенты для вычисления точных физических величин.
  • bme280_read_data: Эта функция, вызываемая из main.c, считывает 8 байт сырых данных с датчика и пропускает их через функции компенсации, записывая итоговый результат в переданную по указателю структуру BME280_Data.

Модуль LCD-дисплея (lcd1602_i2c.c)

Этот драйвер предоставляет простой интерфейс для вывода текста на стандартный символьный дисплей 16x2.

// Фрагмент из lcd1602_i2c.c
#include "lcd1602_i2c.h"
#include <wiringrp/wiringRP.h>
#include <wiringrp/wire.h>

static void send_command(int command) { /* ... */ }
static void send_data(int data) { /* ... */ }

int lcd_init(int i2c_bus, int addr) {
    // ... (инициализация I2C) ...
    send_command(0x28); // Установка режима: 4-битный интерфейс, 2 строки
    send_command(0x0C); // Включить дисплей без курсора
    // ... (другие команды инициализации) ...
    return 0;
}

void lcd_print(const char* str) {
    for (size_t i = 0; i < strlen(str); i++) {
        send_data(str[i]);
    }
}
2. Гибридный подход (Python + WiringRP) #

Этот мощный подход сочетает в себе простоту и гибкость Python для написания основной логики и высокую производительность и прямой доступ к аппаратуре C для выполнения низкоуровневых операций.

Вместо использования Python-библиотек для I2C (smbus2, periphery), мы будем вызывать функции I2C напрямую из скомпилированной C-библиотеки libwiringrp.so. Это позволяет полностью контролировать процесс обмена данными и избежать накладных расходов Python-оберток. Для этого мы напишем на Python собственные классы-драйверы, которые "под капотом" будут использовать C-функции.

1. Загрузка C-библиотеки и определение функций

Первым шагом является загрузка общей библиотеки libwiringrp.so в Python-скрипт с помощью стандартного модуля ctypes и описание прототипов C-функций, которые мы будем использовать.

import ctypes

# Загрузка библиотеки
try:
    wiringrp = ctypes.CDLL("libwiringrp.so")
except OSError:
    print("Ошибка: Не удалось найти 'libwiringrp.so'.")
    exit(1)

# Определение прототипов I2C и временных функций
wiringrp.i2cSetup.argtypes = [ctypes.c_int, ctypes.c_int]
wiringrp.i2cSetup.restype = ctypes.c_int
wiringrp.i2cReadReg8.argtypes = [ctypes.c_int, ctypes.c_int]
wiringrp.i2cReadReg8.restype = ctypes.c_int
wiringrp.i2cWriteReg8.argtypes = [ctypes.c_int, ctypes.c_int, ctypes.c_int]
# ... и так далее для всех нужных функций ...

Объяснение:

  • ctypes.CDLL("libwiringrp.so") загружает C-библиотеку в память и предоставляет объект wiringrp, через который можно обращаться к ее функциям.
  • Определение прототипов (argtypes, restype): Это критически важный шаг. Мы явно указываем Python, какие типы данных (c_int, c_uint) ожидает каждая C-функция и какой тип данных она возвращает. Это гарантирует корректную и безопасную передачу данных между двумя языками.

2. Python-драйвер для LCD1602 на базе WiringRP

Мы создаем Python-класс, который полностью повторяет логику C-драйвера, но вместо прямых вызовов i2cWrite использует их эквиваленты из wiringrp, вызванные через ctypes.

class LcdI2c:
    def __init__(self, fd):
        self.fd = fd # файловый дескриптор, полученный от wiringrp.i2cSetup()
        # ... (команды инициализации дисплея) ...

    def _i2c_write(self, value):
        # Прямой вызов C-функции для записи байта по I2C
        wiringrp.i2cWriteReg8(self.fd, 0, value | self.backlight_val)

    def _pulse_enable(self, value):
        # Эмуляция "моргания" пином Enable для отправки данных
        self._i2c_write(value | 0x04)
        time.sleep(0.0005)
        self._i2c_write(value & ~0x04)
        time.sleep(0.0005)

    def write_string(self, text):
        for char in text:
            self._write_char(ord(char))

Объяснение: Этот класс инкапсулирует всю сложность управления дисплеем. Он принимает в конструкторе fd — файловый дескриптор, который является результатом работы C-функции wiringrp.i2cSetup(). Все методы, такие как _i2c_write, теперь напрямую вызывают функции из libwiringrp.so, обеспечивая низкоуровневый контроль над шиной I2C из Python-кода.

3. Python-драйвер для BME280 на базе WiringRP

Аналогично создается драйвер для датчика BME280, который также использует C-функции для чтения и записи регистров.

class BME280:
    def __init__(self, fd):
        self.fd = fd
        self.cal_data = {}
        # ... (инициализация) ...

    def _read_s16_le(self, reg):
        # Чтение 16-битного знакового числа (little-endian)
        lsb = wiringrp.i2cReadReg8(self.fd, reg)
        msb = wiringrp.i2cReadReg8(self.fd, reg + 1)
        val = (msb << 8) | lsb
        return ctypes.c_int16(val).value

    def _read_calibration(self):
        # Чтение калибровочных данных с помощью _read_s16_le и i2cReadReg8
        self.cal_data['dig_T1'] = self._read_u16_le(0x88)
        # ... (чтение остальных коэффициентов) ...

    def compensate_temp(self, raw_t):
        # ... (математика компенсации, полностью на Python) ...

Объяснение: Этот класс также полностью скрывает сложность работы с датчиком. Он использует wiringrp.i2cReadReg8 для чтения байтов с шины I2C, считывает калибровочные данные при инициализации, а затем выполняет математические расчеты для компенсации температуры, давления и влажности уже средствами Python.

4. Основная программа

Главная часть программы теперь использует наши Python-драйверы, которые работают на "движке" из C.

def main(bus_num, lcd_addr, bme_addr):
    try:
        # Инициализируем WiringRP
        if wiringrp.setupWiringRP(0) < 0:
            raise RuntimeError("Ошибка инициализации WiringRP.")

        # Получаем файловые дескрипторы от C-библиотеки
        lcd_fd = wiringrp.i2cSetup(bus_num, lcd_addr)
        bme_fd = wiringrp.i2cSetup(bus_num, bme_addr)

        # Создаем экземпляры наших Python-драйверов
        lcd = LcdI2c(lcd_fd)
        sensor = BME280(bme_fd)
        
        while True:
            # Работаем с объектами, как с обычными Python-объектами
            raw_t, raw_p, raw_h = sensor.read_raw_data()
            temperature = sensor.compensate_temp(raw_t)
            # ...
            lcd.write_string(f"T:{temperature:.1f}C")
            time.sleep(4)

    finally:
        # Освобождаем ресурсы, вызванные в C
        if lcd_fd >= 0: wiringrp.i2cRelease(lcd_fd)
        if bme_fd >= 0: wiringrp.i2cRelease(bme_fd)
        wiringrp.releaseWiringRP()

Объяснение:

  • Инициализация: main сначала инициализирует WiringRP, а затем получает от нее файловые дескрипторы для каждого I2C-устройства.
  • Создание объектов: Эти дескрипторы передаются в конструкторы наших Python-классов LcdI2c и BME280.
  • Основной цикл: Вся логика внутри while True остается высокоуровневой и читаемой. Мы работаем с методами наших классов (sensor.read_raw_data(), lcd.write_string()), даже не задумываясь, что под капотом они вызывают быстрый C-код.
  • Очистка: В блоке finally мы корректно освобождаем все ресурсы, включая дескрипторы, полученные от WiringRP.

Вывод: Этот гибридный подход — самый гибкий. Он позволяет писать драйверы для устройств на Python, используя всю мощь и синтаксический сахар языка, но при этом полагаться на производительность и точность низкоуровневых C-функций для непосредственного взаимодействия с оборудованием.

Сравнение производительности: RepkaPi.GPIO (SysFS) vs WiringRP #

В рамках наших проектов мы использовали два разных подхода для взаимодействия с GPIO-пинами:

  1. Python с библиотекой RepkaPi.GPIO: Высокоуровневый подход, работающий через стандартный интерфейс ядра Linux SysFS.
  2. C с библиотекой WiringRP: Низкоуровневый подход, работающий максимально близко к "железу" через прямой доступ к памяти.

Возникает логичный вопрос: насколько велика разница в производительности и когда какой подход следует выбирать? Для ответа на этот вопрос был проведен объективный тест — бенчмарк.

Методика тестирования #

Чтобы измерить чистую скорость работы с GPIO, была поставлена простая задача: переключать один и тот же GPIO-пин из высокого состояния (HIGH) в низкое (LOW) и обратно так быстро, как это возможно, в течение 5 секунд. Эта операция "включить-выключить" является фундаментальной для любого проекта, работающего с GPIO, и ее скорость напрямую отражает эффективность используемой библиотеки.

Были написаны два минималистичных скрипта, реализующих этот тест.

Код на C с WiringRP

// benchmark_c_counter.c
#include <wiringrp/wiringRP.h>
#include <stdio.h>
#include <time.h>

#define TEST_PIN 7
#define BENCHMARK_DURATION 5

int main(void) {
    unsigned long long counter = 0;
    time_t start_time = time(NULL);

    if (setupWiringRP(WRP_MODE_PHYS) < 0) return 1;
    
    pinMode(TEST_PIN, OUTPUT);
    
    while (1) {
        digitalWrite(TEST_PIN, HIGH);
        digitalWrite(TEST_PIN, LOW);
        counter++;
        
        if (time(NULL) - start_time >= BENCHMARK_DURATION) {
            break;
        }
    }
    
    printf("Операций в секунду: %llu ops/sec\n", counter / BENCHMARK_DURATION);
    return 0;
}

Код на Python с RepkaPi.GPIO

# benchmark_c_counter.py
import RepkaPi.GPIO as GPIO
import time

TEST_PIN = 7
BENCHMARK_DURATION = 5

GPIO.setmode(GPIO.BOARD)
GPIO.setup(TEST_PIN, GPIO.OUT)

counter = 0
start_time = time.time()

try:
    while True:
        GPIO.output(TEST_PIN, GPIO.HIGH)
        GPIO.output(TEST_PIN, GPIO.LOW)
        counter += 1
        
        if time.time() - start_time >= BENCHMARK_DURATION:
            break
finally:
    GPIO.cleanup()

print(f"Операций в секунду: {counter // BENCHMARK_DURATION} ops/sec")

Результаты #

После компиляции C-кода и запуска обоих скриптов на Repka Pi были получены следующие результаты:

Подход Операций в секунду (ops/sec)
Python + RepkaPi.GPIO (SysFS) ~6,679
C + WiringRP (прямой доступ) ~484,638

Анализ результатов #

Как видно из таблицы, разница в производительности колоссальна: подход на C с использованием WiringRP оказался примерно в 72 раза быстрее, чем его аналог на Python. Эта разница обусловлена фундаментальными различиями в том, как эти библиотеки взаимодействуют с оборудованием.

  • WiringRP (C):

    • Компилируемый язык: Код на C преобразуется в нативные машинные инструкции, которые выполняются процессором напрямую без посредников.
    • Прямой доступ к памяти (/dev/mem): WiringRP изменяет состояние GPIO-пина путем прямой записи нужных значений в физические адреса памяти, где расположены регистры управления GPIO. С точки зрения системы, это одна быстрая операция записи в память.
  • RepkaPi.GPIO (Python):

    • Интерпретируемый язык: Код на Python выполняется через интерпретатор, который добавляет свои, хоть и небольшие, накладные расходы.
    • Интерфейс SysFS: Это ключевое отличие. Библиотека RepkaPi.GPIO работает через стандартный для Linux интерфейс SysFS. Для операционной системы GPIO-пины представлены в виде файлов в директории /sys/class/gpio/. Чтобы изменить состояние пина, библиотека выполняет целую последовательность действий:
      1. Отправляет системный вызов на открытие файла (например, /sys/class/gpio/gpio7/value).
      2. Отправляет системный вызов на запись в этот файл символа "1" или "0".
      3. Отправляет системный вызов на закрытие файла.

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

Выводы и рекомендации #

Означают ли эти результаты, что RepkaPi.GPIO — плохая библиотека? Однозначно нет. Выбор инструмента всегда зависит от задачи.

  • Когда использовать Python и RepkaPi.GPIO? Почти всегда. Во всех проектах, которые мы рассмотрели (метеостанция, система полива, парктроник, RFID-сейф), основной цикл программы имеет задержку от сотен миллисекунд до нескольких секунд (time.sleep(1)). На фоне таких задержек разница в скорости выполнения GPIO.output() в несколько микросекунд абсолютно несущественна. Преимущества Python — скорость разработки, простота отладки, читаемость кода и огромное количество готовых библиотек — многократно перевешивают проигрыш в "чистой" производительности GPIO.

  • Когда использовать C и WiringRP? WiringRP и C становятся незаменимы, когда требуется работа в реальном времени или генерация высокочастотных сигналов. Например:

    • Программная реализация протоколов связи (например, "bit-banging" I2C или SPI).
    • Управление устройствами, требующими очень точных и коротких импульсов, недостижимых с помощью time.sleep().
    • Приложения, где критически важна минимальная и предсказуемая задержка реакции на событие.

Итог: Для подавляющего большинства образовательных и хобби-проектов удобство и скорость разработки на Python с RepkaPi.GPIO являются предпочтительным выбором. К мощи и производительности C и WiringRP следует обращаться тогда, когда вы точно знаете, что ваш проект упирается в пределы скорости или точности, которые может предоставить Python.

Практическая значимость проекта

Проект "Метеостанция" имеет широкую практическую значимость в различных областях. В первую очередь, он является отличным образовательным инструментом, позволяющим студентам и учащимся изучать работу с датчиками, интерфейсами и осваивать программирование на Python. Кроме того, проект может быть использован для мониторинга климатических условий, что важно для научных исследований в области экологии, сельского хозяйства и других направлений.

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

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

Расширение проекта

Проект "Метеостанция" можно значительно расширить, добавив новые функциональные возможности. Например, можно интегрировать дополнительные датчики, такие как:

Датчики для измерения уровня осадков — это позволит получить полную картину погодных условий, включая дождевые осадки.

Датчик UV-излучения — для измерения уровня ультрафиолетового излучения, что может быть полезно для контроля солнечной активности.

Сенсоры качества воздуха — для мониторинга загрязнения воздуха в реальном времени.

Глобальная система позиционирования (GPS) — для отслеживания точных координат метеостанции и анализа данных по географическому расположению.

Видеообзор проекта

Для более детального ознакомления с проектом, вы можете посмотреть видеообзор на платформе Rutube:

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

Проект полностью реализован на языке Python. Код для работы с метеостанцией можно найти в репозитории на платформе Gitflic.


Комментарии (0)

Для участия в обсуждении Вы должны быть авторизованным пользователем

Еще посты по теме

Новые посты



Темы

Навигация

ВойтиРегистрация