Files
wikipedia/HomeLab/Hermes/ssd1306-rpi5-guide.md
T

443 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Подключение OLED дисплея SSD1306 128x64 к Raspberry Pi 5
## Содержание
1. [Подключение по I2C](#1-подключение-по-i2c)
2. [Настройка I2C на RPi5](#2-настройка-i2c-на-rpi5)
3. [Установка ПО](#3-установка-по)
4. [Проверка дисплея](#4-проверка-дисплея)
5. [Статическое изображение](#5-статическое-изображение)
6. [CPU/Монитор в реальном времени](#6-cpumонитор-в-реальном-времени)
7. [Systemd-сервис для автостарта](#7-systemd-сервис-для-автостарта)
8. [Решение проблем](#8-решение-проблем)
9. [Приложение: полный код скриптов](#9-приложение-полный-код-скриптов)
---
## 1. Подключение по I2C
SSD1306 подключается по I2C-шине. Пины I2C на Raspberry Pi 5:
| Пин | Название | GPIO | Назначение |
|-----|----------|------|------------|
| 3 | SDA | GPIO2 | I2C Data |
| 5 | SCL | GPIO3 | I2C Clock |
| 1 | 3.3V | — | Питание |
| 6 | GND | — | Земля |
**Схема подключения:**
```
RPi5 OLED SSD1306
GPIO2 (Pin 3) ─────── SDA
GPIO3 (Pin 5) ─────── SCL
3.3V (Pin 1) ─────── VCC
GND (Pin 6) ─────── GND
```
**Важно:** Для I2C нужны подтягивающие резисторы (pull-up) 4.7 кОм на линиях SDA и SCL к 3.3V. На большинстве готовых OLED-модулей они уже впаяны — дополнительно ставить не нужно.
---
## 2. Настройка I2C на RPi5
На Raspberry Pi 5 I2C по умолчанию **отключён** в `/boot/firmware/config.txt`.
### 2.1. Включение I2C
```bash
sudo sed -i 's/#dtparam=i2c_arm=on/dtparam=i2c_arm=on/' /boot/firmware/config.txt
sudo reboot
```
**Проверка после перезагрузки:**
```bash
sudo /usr/sbin/i2cdetect -y 1
```
Вывод (пример — если дисплей подключён):
```
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
...
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
...
```
Адрес **0x3C** — стандартный для SSD1306 (может быть 0x3D, проверяйте по даташиту).
**Заметка:** `i2cdetect` лежит в `/usr/sbin/` — этого каталога нет в PATH обычного пользователя. Используйте полный путь или `sudo`.
### 2.2. Альтернатива: raspi-config
```bash
sudo raspi-config
# → Interface Options → I2C → Enable
sudo reboot
```
---
## 3. Установка ПО
### 3.1. Системные пакеты
```bash
sudo apt-get install -y i2c-tools python3-pip python3-smbus python3-pil
```
### 3.2. Виртуальное окружение Python
На Raspberry Pi OS Bookworm используется `external-managed-environment` — `pip install` напрямую не работает. Обязательно создаём venv:
```bash
python3 -m venv ~/oled-env
source ~/oled-env/bin/activate
```
### 3.3. Установка luma.oled
```bash
pip install luma.oled Pillow
```
Проверка установки:
```bash
python3 -c "from luma.core.interface.serial import i2c; from luma.oled.device import ssd1306; print('OK')"
```
---
## 4. Проверка дисплея
После включения I2C и установки ПО — быстрый тест сканирует шину и зажигает пиксели:
```bash
source ~/oled-env/bin/activate
python3 -c "
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)
img = Image.new('1', device.size, 'black')
draw = ImageDraw.Draw(img)
draw.rectangle((0, 0, 127, 63), outline='white', fill='black')
draw.text((20, 25), 'Hello!', fill='white')
device.display(img)
device.cleanup = lambda: None
"
```
Если дисплей показывает рамку и текст — всё работает.
---
## 5. Статическое изображение
Скрипт `/home/emb/oled_show.py` — выводит статический текст и завершается, оставляя изображение на дисплее.
```python
#!/home/emb/oled-env/bin/python3
import sys, os, signal
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw, ImageFont
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)
img = Image.new('1', device.size, 'black')
draw = ImageDraw.Draw(img)
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 11)
draw.rectangle((0, 0, 127, 63), outline='white', fill='black')
draw.text((2, 2), 'RPi5 Ready!', font=font, fill='white')
draw.text((2, 16), 'I2C 128x64', font=font, fill='white')
draw.text((2, 30), '192.168.1.46', font=font, fill='white')
draw.text((2, 50), 'by Hermes', font=font, fill='white')
device.display(img)
device.cleanup = lambda: None # <-- КЛЮЧЕВОЙ МОМЕНТ
print('OLED ready')
```
### Почему `device.cleanup = lambda: None`?
Библиотека `luma.oled` в деструкторе объекта `device` вызывает команду 0xAE (DISPLAY OFF), что гасит дисплей при завершении скрипта. Замена `cleanup` на пустую функцию предотвращает это.
Если вам нужно, чтобы скрипт работал постоянно (и дисплей не гас _никогда_), добавьте `signal.pause()` в конец — процесс останется висеть.
---
## 6. CPU/Монитор в реальном времени
Скрипт `/home/emb/oled_cpu_mon.py` — отображает загрузку CPU, RAM и температуру с обновлением раз в секунду.
```python
import time, os
from luma.core.interface.serial import i2c
from luma.oled.device import ssd1306
from PIL import Image, ImageDraw, ImageFont
serial = i2c(port=1, address=0x3C)
device = ssd1306(serial)
font = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf', 10)
font_big = ImageFont.truetype('/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf', 16)
device.cleanup = lambda: None
def get_cpu():
with open('/proc/loadavg') as f:
parts = f.read().split()
load = float(parts[0])
cores = os.cpu_count() or 4
percent = (load / cores) * 100
return min(percent, 100), load
def get_ram():
with open('/proc/meminfo') as f:
lines = f.readlines()
mem_total = int([l for l in lines if 'MemTotal' in l][0].split()[1])
mem_avail = int([l for l in lines if 'MemAvailable' in l][0].split()[1])
used = mem_total - mem_avail
return used / mem_total * 100, used // 1024, mem_total // 1024
def get_temp():
try:
with open('/sys/class/thermal/thermal_zone0/temp') as f:
return int(f.read()) / 1000
except:
return 0
while True:
cpu_pct, load = get_cpu()
ram_pct, used_mb, total_mb = get_ram()
temp = get_temp()
img = Image.new('1', device.size, 'black')
draw = ImageDraw.Draw(img)
draw.rectangle((0, 0, 127, 63), outline='white', fill='black')
# CPU bar
bar_w = 90
bar_h = 12
draw.rectangle((32, 4, 32 + bar_w, 4 + bar_h), outline='white', fill='black')
fill_w = int(bar_w * cpu_pct / 100)
if fill_w > 0:
draw.rectangle((32 + 1, 4 + 1, 32 + fill_w, 4 + bar_h - 1), fill='white')
draw.text((2, 5), 'CPU', font=font, fill='white')
draw.text((32 + bar_w + 4, 3), f'{cpu_pct:.0f}%', font=font_big, fill='white')
draw.text((2, 20), f'Load: {load:.2f}', font=font, fill='white')
# RAM bar
draw.rectangle((32, 36, 32 + bar_w, 36 + bar_h), outline='white', fill='black')
fill_w2 = int(bar_w * ram_pct / 100)
if fill_w2 > 0:
draw.rectangle((32 + 1, 36 + 1, 32 + fill_w2, 36 + bar_h - 1), fill='white')
draw.text((2, 38), 'RAM', font=font, fill='white')
draw.text((32 + bar_w + 4, 36), f'{ram_pct:.0f}%', font=font_big, fill='white')
# Temp and PID at bottom
draw.text((2, 52), f'Temp: {temp:.1f}C', font=font, fill='white')
draw.text((72, 52), f'PID: {os.getpid()}', font=font, fill='white')
device.display(img)
time.sleep(1)
```
### Визуальная схема расположения на дисплее:
```
┌─────────────────────────────────────────┐
│ CPU ████████████████░░░░ 45% │
│ Load: 1.80 │
│ RAM ██████████░░░░░░░░ 32% │
│ Temp: 52.3C PID: 2535 │
└─────────────────────────────────────────┘
```
---
## 7. Systemd-сервис для автостарта
Чтобы скрипт запускался автоматически при включении RPi5 и перезапускался при падении — используем systemd.
### 7.1. Создание сервиса
**Предварительно:** убедитесь, что модуль `i2c-dev` загружается при старте:
```bash
echo i2c-dev | sudo tee /etc/modules-load.d/i2c-dev.conf
```
Создать сервис:
```bash
sudo tee /etc/systemd/system/oled-display.service > /dev/null << 'EOF'
[Unit]
Description=OLED Display Service
After=multi-user.target
[Service]
Type=simple
User=emb
ExecStart=/home/emb/oled-env/bin/python3 /home/emb/oled_cpu_mon.py
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
```
| Параметр | Значение | Пояснение |
|----------|----------|-----------|
| `User=emb` | emb | Запуск от вашего пользователя |
| `ExecStart` | venv/bin/python3 + скрипт | Полный путь к интерпретатору |
| `Restart=always` | — | Перезапуск при любом завершении |
| `RestartSec=5` | 5 | Пауза между перезапусками |
### 7.2. Активация и управление
```bash
sudo systemctl daemon-reload # перечитать конфиги
sudo systemctl enable oled-display.service # автостарт при boot
sudo systemctl restart oled-display.service # запустить сейчас
```
### 7.3. Базовые команды
```bash
# Статус
sudo systemctl status oled-display.service
# Остановка
sudo systemctl stop oled-display.service
# Запуск
sudo systemctl start oled-display.service
# Отключение автостарта
sudo systemctl disable oled-display.service
# Логи
sudo journalctl -u oled-display.service -n 20
```
### 7.4. Смена скрипта
Чтобы переключить сервис на другой скрипт (например, со статического на монитор):
```bash
sudo sed -i 's|oled_show.py|oled_cpu_mon.py|' /etc/systemd/system/oled-display.service
sudo systemctl daemon-reload
sudo systemctl restart oled-display.service
```
### 7.5. Обновление скрипта без простоя
Если сервис уже работает:
```bash
sudo systemctl stop oled-display.service
# редактируем /home/emb/oled_cpu_mon.py
sudo systemctl restart oled-display.service
```
---
## 8. Решение проблем
### 8.1. Дисплей пустой после запуска
**Причина:** деструктор luma.oled отключает дисплей.
**Решение:** `device.cleanup = lambda: None` перед выходом из скрипта.
### 8.2. I2C device not found после перезагрузки (стартовая гонка)
**Симптом:** сервис падает в логах с `I2C device not found: /dev/i2c-1`, хотя после ручного запуска всё работает.
**Причина:** systemd запускает сервис до загрузки модуля `i2c-dev`.
**Решение:** добавить ожидание устройства в скрипте:
```python
import os, time
for attempt in range(30):
if os.path.exists('/dev/i2c-1'):
break
time.sleep(1)
else:
raise RuntimeError('I2C device /dev/i2c-1 not found after 30s')
```
Также можно загружать модуль принудительно: `echo i2c-dev | sudo tee /etc/modules-load.d/i2c-dev.conf`
```bash
sudo /usr/sbin/i2cdetect -y 1
```
Если адрес не отображается:
- Проверьте физическое подключение пинов SDA/SCL/3.3V/GND
- Включите I2C в `/boot/firmware/config.txt` и перезагрузите
- Проверьте `ls /dev/i2c*` — если шины нет, загрузите модуль: `sudo modprobe i2c-dev`
### 8.3. pip install не работает
Ошибка: `error: externally-managed-environment`
**Причина:** Raspberry Pi OS Bookworm включает PEP 668.
**Решение:** всегда используйте venv.
```bash
python3 -m venv ~/oled-env
source ~/oled-env/bin/activate
pip install luma.oled Pillow
```
### 8.4. i2cdetect: command not found
**Причина:** `/usr/sbin/` не в PATH обычного пользователя.
**Решение:** `sudo /usr/sbin/i2cdetect -y 1`
### 8.5. systemd-сервис падает
```bash
sudo journalctl -u oled-display.service -n 20
```
Типичные ошибки:
- `SyntaxError` — проверьте скрипт на синтаксис
- `ModuleNotFoundError` — проверьте путь в `ExecStart` (должен вести к python из venv)
- `PermissionError` — файл скрипта должен быть читаемым для пользователя
### 8.6. Дисплей показывает мусор/рябь
- Сделайте полную инициализацию контроллера (последовательность 0xAE ... 0xAF)
- Проверьте питание — 3.3V стабильно?
- Убедитесь что нет конфликта адресов (другие I2C устройства на той же шине)
---
## 9. Приложение: полный код скриптов
### 9.1. static_text.py — статический текст
Полный код доступен в файле `/home/emb/oled_show.py` на целевой системе.
### 9.2. cpu_mon.py — монитор CPU/RAM/Температура
Полный код доступен в файле `/home/emb/oled_cpu_mon.py` на целевой системе.
Создано: Hermes Agent · Raspberry Pi 5 · Debian Bookworm · SSD1306 128×64 I2C