443 lines
15 KiB
Markdown
443 lines
15 KiB
Markdown
# Подключение 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 |