Search
Write a publication
Pull to refresh

Обучающий проект на одноплатнике — изучение embedded-программирования. Проект первый — простая метеостанция

Level of difficultyMedium
Reading time26 min
Views2.2K

Несколько абзацев про одноплатные компьютеры, встроенные системы и про DIY проекты :-) Можно пропустить и читать сразу про проект. Кто же давно собирался открыть для себя мир проектов на одноплатниках, embedded-программирование, работу с Linux или же искал учебные проекты для домашних занятий с ребёнком и открыл эту статью с мыслью "может это знак и пора начать" - то Вам стоит прочитать краткий обзор перед тем, как сделать свой выбор одноплатника, читать эту статью дальше по существу и начать собирать проект.

Если Вы хотите начать свой Путь в изучении Мира автоматики и телеметрии, начать практиковать Embedded-разработку, т.е. разрабатывать встраиваемые программно-аппаратные решения и собственные системы управления и сбора данных или же если Вы в этом дока и хотите найти готовое решение и материал для занятия со своими детьми, то эта статья для Вас. А ещё если Вы искали прикольные практические проекты с которыми хотели бы начать работать с одноплатными компьютерами даже просто как повод для изучения Linux, то этот материал так же Вам подходит. Сейчас в IT отрасли происходят такие масштабные изменения, что Embedded-разработку становится одним из наиболее перспективных и развивающихся направлений, открывающих Путь и в разработку промышленных и встраиваемых систем, а так же является специализацией в рамках которой разработчики получают широкий круг знаний и могут успешнее работать в любых других направлениях.

Небольшое вступление про мир одноплатных компьютеров

Одноплатник - часто используемое разговорное обозначение одноплатных компьютеров. Одноплатный компьютер (SBC, англ. single-board computer) - это такой вполне полноценный компьютер, только собранный на одной печатной плате, на которой установлены и микропроцессор и оперативная память и накопитель данных и всё прочее. Как правило одноплатные компьютеры имеют небольшие размеры и относительно небольшое энергопотребление и при этом меньшую производительность, чем большие и дорогие десктопные компьютеры. Одноплатники бывают разные - в качестве полноценных офисных и домашних персоналок (например, неттопы), тонких клиентов, всяких там демонстрационных систем. Или же в качестве систем для разработчиков или образования. А ещё для выполнения роли промышленных или встраиваемых компьютеров во всякие сложные и крутые устройства (от стиральных машин и умных контроллеров до дронов). И так повелось, что чаще всего одноплатники снабжаются ARM процессорами с RISC архитектурой, а это значит, что в основном для работы на них используется операционная система Linux. Но так уж получилось, что в последние годы этот сегмент процессоров значительно прибавил в производительности. Поэтому сейчас на современных одноплатных компьютерах можно учиться, работать со всякими там браузерами и редакторами, программировать и строить и запускать проекты - уж только для развлечения или и для обучения или вообще для создания какого то продукта и решения - это решать только Вам. Главное, что это стало очень удобно и доступно.

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

В целом одноплатные компьютеры можно разделить на две категории:

  • универсальные - для обучения, разработки, домашнего использования и "малой" автоматизации - они универсальны, доступны по стоимости и широко распространены,

  • специализированные - для промышленного использования - они максимально надёжны в работе, заточены под какие то типы задач и для работы только с пассивным охлаждением, но не очень удобны для широкого спектра задач и для разработки и имеют, как правило, стоимость в несколько раз выше чем универсальные одноплатники,

  • сверхмалые и особо компактные - для установки внутри приборов и дронов, имеют минимальные размеры и вес, меньшую производительность и не очень удобны для разработки и прототипирования.

Какое то время назад (больше десяти лет) известный бренд Raspberry Pi представил форм-фактор универсального одноплатного компьютера, ставший по сути отраслевым стандартом. Назовём его "формфактор raspberry" - в нём совместился компактный размер, удобное подключение стандартных HDMI монитора и USB клавиатуры и мыши, а так же просто огромный - 40 пиновый штырьковый разъём с доступными на нём промышленными интерфейсами и портами ввода вывода. Этот разъём позволяет подключать разные внешние устройства в виде всяких датчиков, исполнительных механизмов и устройств ввода/вывода информации. Этот стандарт "формфактора raspberry" стали использовать другие производители и теперь выбор удобных одноплатниклов большой.

На Российском рынке есть решения в этих формфакторах. Универсальные одноплатники: проект Repka Pi (Репка) и ELTAY (Алтай). Repka Pi на рынке уже три года, выпустила два поколения одноплатников и готовит третье, всё c документацией на русском языке, c разными примерами проектов, широкой экосистемой, доступными конкурентными ценами и с короткими сроками поставки. Его и берём для нашего первого проекта автоматики и телеметрии, так как доступен в продаже и в интернет магазине проекта и почти на всех популярных интернет магазинах, имеет небольшую стоимость, высокое качество, стабильную зрелую операционную систему на базе Linux Ubuntu с утилитами конфигурации и, что особенно важно, библиотеки для программирования управления портами ввода/вывода, что для нашей задачи особенно актуально.

Самое главное в универсальном формфакторе - чаще всего это 40 пиновый разъём - он даёт уникальные возможности всем, кто уже умеет, ещё учится или хочет научится программировать - вроде обычный компьютер и при этом к нему можно подключить всё что только захочется и можно программировать всё это как только вздумается. Получается, что становится доступной для самостоятельной разработки в домашних условиях любая автоматика. Вот такие проекты часто и называют DIY проектами, т.е. сделай сам (англ. DIY — do it yourself). Автоматика, робототехника, программирование и теперь ещё и работа с Linux - это в современном мире почти уже обязательная часть навыков часто уже не только для любого инженера, т.е. это то, чем можно решать кучу задач, чем можно зарабатывать и чем можно развлекаться и то, чему нужно обучать детей в роли сопутствующих навыков и знаний даже если они выберут совсем другие профессии.


Итак, к проекту!

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

У нас под рукой вышедшая в прошлом году вторая модель Repka Pi - это Repka Pi 4 (с номером 4 - видимо для условного сопоставления с модельным рядом Raspberry). Так как недавно проект анонсировал под него новые библиотеки для работы с интерфейсами 40-пинового разъёма - WiringRP и RepkaPi.GPIO и все примеры и документация оказались на очень высоком уровне - то мы выбираем его на роль одноплатника для нашего первого своего DIY проекта по автоматике, а точнее пока телеметрии. В качестве первого проекта будет проект условной метеостанции - он простой и лучше всего подойдёт в роли первого проекта. Для проекта берём датчик температуры, давления и влажности и самый простой экран для вывода цифро-символьной информации:

Содержание

  1. О проекте

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

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

  4. Подготовка макетной монтажной платы, источника питания и IDE для разработки ПО

  5. Сборка и проверка схемы

  6. Запуск

  7. Подробный алгоритм работы и варианты программной реализации

О проекте

Наш первый проект - система для мониторинга основных показателей погодных условий в реальном времени или проект “Метеостанция”. Электрическая принципиальная и монтажная схема, исходные коды и подробное описание алгоритма работы будет раскрыты в данной статье, а так же инструкции для более простого монтажа в домашних условиях.

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

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

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

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

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

Рисунок 2. ЖК дисплей (1602 I2C).
Рисунок 2. ЖК дисплей (1602 I2C).

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

Ссылки на все электронные модули и коммутационные компоненты (провода, шлейф 40 пин, переходник с 40 пин шлейфа на макетную плату) приведены в таблице ниже, на случай если Вам будет интересно посмотреть на их подробные характеристики и приобрести:

Компонент

Ссылка на приобретение

Монтажная/макетная плата

Ссылка

Шлейф

Ссылка

Переходник с шлейфа на макетную плату

Ссылка

Соединительные провода

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

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

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

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

Ссылка

ЖК дисплей (1602 I2C)

Ссылка

Не обязательно использовать шлейф-удлинитель и Т-образный переходник, просто с ними намного удобнее выполнять монтаж, особенно если Вы планируете на этом проекте попробовать свои силы и Вам понравится и Вы решите расширить этот проект своими Идеями или же собрать другие проекты. Так удобнее вести прототипирование и разработку, особенно когда проект носит образовательный характер. Можно не использовать не только шлейф и переходник, а даже и макетную монтажную плату и просто всё коммутировать проводами напрямую. Но мы рассмотрим более громоздкий и при этом более удобный вариант с макетной платой.

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

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

Рисунок 3. Электрическая принципиальная схема проекта “Метеостанция”.
Рисунок 3. Электрическая принципиальная схема проекта “Метеостанция”.

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

Вариант подключения напрямую без переходников и удлинительных шлейфов

Рисунок 4. Монтажная схема проекта “Метеостанция” без переходников и дополнительных источников питания.
Рисунок 4. Монтажная схема проекта “Метеостанция” без переходников и дополнительных источников питания.

Вариант монтажа с использованием переходников, удлинителей и дополнительных источников питания:

Рисунок 4.2. Монтажная схема проекта “Метеостанция” с переходником и дополнительным источником питания.
Рисунок 4.2. Монтажная схема проекта “Метеостанция” с переходником и дополнительным источником питания.

В этом случае используемый переходник в виде Т-образная коммутационной платы полностью повторяет конфигурацию распиновки 40-pin разъёма Repka Pi:

Рисунок 4.1. Расширительная плата наложенная на распиновку Repka Pi 4.
Рисунок 4.1. Расширительная плата наложенная на распиновку Repka Pi 4.

Подготовка макетной монтажной платы, источника питания и IDE для разработки ПО

Если Вы выберете вариант использования дополнительного источника питания, то в качестве источника питания можно просто приобрести ещё один блок питания для Repka Pi 4 c Type-C разъёмом, т.е. один штатный блок питания для самого одноплатника, а второй для питания схем автоматики и телеметрии. И у шнура второго блока питания просто отрезать разъём со стороны подключения к одноплатнику.

В идеале нужно затем провода зачистить и залудить или даже поставить и обжать монтажные разъёмы - мы за такой вариант.

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

Еще вариант - использовать лабораторный блок питания, если он у Вас есть.

И конечно можно использовать аккумуляторы или батарейки.

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

Сборка и проверка

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

Как видно из схем на рисунках 3 и 4, датчик подключается через интерфейс I2C и питается от 3.3V. 3.3 вольта мы берём с пина 40 пинового разъёма Репки, там есть и 3.3 и 5 вольт, питание датчика не требует большого потребления, так что можно взять с одноплатника.

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

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

BME280

3.3V

VIN

GND

GND

SCL1

SCL

SDA1

SDA

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

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

Рисунок 5.1. Подключенный BME280 к макетной плате.
Рисунок 5.1. Подключенный BME280 к макетной плате.
Рисунок 5.2. Подключенный BME280 к макетной плате.
Рисунок 5.2. Подключенный BME280 к макетной плате.

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. Выполним установку зависимостей:

make setup-bme280

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 видим, что скрипт успешно выполнился, нам удалось считать показания с датчика.

Рисунок 6. Считывание показаний с датчика BME280.
Рисунок 6. Считывание показаний с датчика BME280.

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.1 для варианта с переходником, удлинителем и дополнительным источником питания и рисунок 7.2 для варианта без переходника, удлинителя и дополнительного источника питания:

Рисунок 7.1. Подключенный ЖК дисплея (1602 I2C) к макетной плате для варианта с переходником, удлинителем и дополнительным источником питания.
Рисунок 7.1. Подключенный ЖК дисплея (1602 I2C) к макетной плате для варианта с переходником, удлинителем и дополнительным источником питания.
Рисунок 7.2. Подключенный ЖК дисплея (1602 I2C) к макетной плате для варианта БЕЗ переходника, удлинителя и БЕЗ дополнительного источника питания.
Рисунок 7.2. Подключенный ЖК дисплея (1602 I2C) к макетной плате для варианта БЕЗ переходника, удлинителя и БЕЗ дополнительного источника питания.

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

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

make setup-display-1602-i2c

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

make display-1602-i2c

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

Рисунок 8. Успешно подключенный ЖК дисплея (1602 I2C) к макетной плате.
Рисунок 8. Успешно подключенный ЖК дисплея (1602 I2C) к макетной плате.
Вы можете собрать более бюджетную версию данного проекта.
Рисунок 12. Бюджетный набор компонентов.
Рисунок 13. Распиновка портов на 40 pin разъёме на Repka Pi 4.
Рисунок 13.1. Монтажная схема бюджетной версии проекта “Метеостанция”.
Рисунок 15. Подключенный BME280 к макетной плате.
Рисунок 16. Подключенный ЖК дисплея (1602 I2C) к макетной плате.
Рисунок 16.1. Подключенный ЖК дисплея (1602 I2C) к макетной плате.
Рисунок 16.2. Подключенный ЖК дисплея (1602 I2C) к макетной плате.

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

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

make weather-station

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

Рисунок 9. Запущенный проект "Метеостанция".
Рисунок 9. Запущенный проект "Метеостанция".
Рисунок 10. Вывод в консоль данных проекта “Метеостанция”.
Рисунок 10. Вывод в консоль данных проекта “Метеостанция”.
Рисунок 11. Вывод в консоль данных проекта “Метеостанция”.
Рисунок 11. Вывод в консоль данных проекта “Метеостанция”.

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

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

Исходные коды проекта можно найти в репозитории на платформе Gitflic.

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

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

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

  1. Старт и инициализация: При запуске программа первым делом инициализирует все необходимые аппаратные компоненты: устанавливает соединение по шине I2C с датчиком BME280 и LCD-дисплеем.

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

  3. Сбор данных: На каждой итерации цикла происходит опрос датчика BME280 для получения актуальных значений температуры, атмосферного давления и влажности.

  4. Вывод в консоль: Полученные данные форматируются и выводятся в консоль для отладки и мониторинга.

  5. Вывод на дисплей: Экран LCD-дисплея очищается, после чего на него выводятся те же метеоданные в компактном и читаемом формате.

  6. Пауза: Программа делает паузу на 4 секунды. Это необходимо, чтобы данные на экране не менялись слишком быстро и были удобны для восприятия, а также для снижения нагрузки на процессор.

  7. Повторение: После паузы цикл начинается заново с пункта 3.

  8. Завершение: Цикл прерывается пользователем (например, нажатием Ctrl+C). При этом программа корректно завершает работу, очищая дисплей.

Варианты программной реализации алгоритма

Рассмотрим реализации алгоритма на языках Python и С++. А так же два подхода к программированию - а) функциональный и б) объектно ориентированный (ООП).

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

Функциональный подход на Python

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

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

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

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), если они отличаются от стандартных.

  • smbus2rpi_bme280RPLCD.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, позволяя программе завершиться корректно: очистить экран дисплея и вывести прощальное сообщение.

Объектно-ориентированный подход (ООП) на Python

Этот подход инкапсулирует всю логику работы метеостанции в единый класс 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. Он выполняет ту же инициализацию, что и в функциональном подходе, но сохраняет объекты bussensor и 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++ c использованием библиотеки WiringRP

Язык C в связке с библиотекой 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_Tadc_Padc_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]);
    }
}

Гибридный подход к реализации (Python + WiringRP)

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

Вместо использования Python-библиотек для I2C (smbus2periphery), мы будем вызывать функции 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, через который можно обращаться к ее функциям.

  • Определение прототипов (argtypesrestype): Это критически важный шаг. Мы явно указываем Python, какие типы данных (c_intc_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(). Все методы, такие как i2cwrite, теперь напрямую вызывают функции из 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 и WiringRPWiringRP и C становятся незаменимы, когда требуется работа в реальном времени или генерация высокочастотных сигналов. Например:

    • Программная реализация протоколов связи (например, "bit-banging" I2C или SPI).

    • Управление устройствами, требующими очень точных и коротких импульсов, недостижимых с помощью time.sleep().

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

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

Итоги

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

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

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

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

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

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

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

Tags:
Hubs:
+7
Comments15

Articles