Управление цветом с MicroPython на дисплеях Raspberry Pi Pico

Управление цветом с MicroPython на дисплеях Raspberry Pi Pico

У вас есть дисплей Waveshare для Raspberry Pi Pico, но вы не знаете, с чего начать? Попробуйте поэкспериментировать с цветами и фигурами с помощью MicroPython!

В этой статье мы покажем управление цветом на дисплеях Waveshare для Raspberry Pi Pico с использованием MicroPython, включая:

  • Как управляются цвета экрана

  • Базовые инструкции для работы с текстом и графикой — прямоугольники

  • Как считывать значения с потенциометров в заданном диапазоне

  • Как собрать простое приложение для смешивания цветов с дополнительными компонентами

Что вам понадобится

  • Raspberry Pi Pico (с припаянными штырьками)

  • Установленный Thonny на вашем компьютере

  • Кабель Micro-USB — для питания и программирования Pico

  • Умение вводить, редактировать, сохранять и запускать код MicroPython на Pico через Thonny

  • Дисплей Waveshare Pico LCD — варианты см. ниже

  • Опционально:

    • 3 x потенциометра на 10 кОм

    • Макетная плата (breadboard)

    • Соединительные провода (jumper wires)

    • Расширитель пинов для Pico (Pico Pin Expander)

Варианты дисплеев

Существует несколько ярких и красочных дисплеев Pico от Waveshare, различающихся размером, количеством пикселей и функциями. В порядке увеличения размера:

  • 0.96» 160x80 пикселей, с джойстиком и 2 кнопками

  • 1.3» 240x240 пикселей, с джойстиком и 4 кнопками

  • 1.44» 128x128 пикселей с 4 кнопками

  • 1.8» 160x128 пикселей

  • 2.0» 320x240 пикселей с 4 кнопками

Все эти дисплеи напрямую подключаются к пинам Pico и программируются одинаково, но требуют немного разного кода драйвера, предоставляемого Waveshare через их страницы Wiki.

Сравнение дисплеев

Для всех этих дисплеев требуется определённый объём памяти Pico — «буфер» — для хранения данных, отображаемых на экране. По мере увеличения количества пикселей возрастает и требуемый размер буфера, а доступное место для кода уменьшается. Чем меньше пиксели, тем труднее читать базовый текст из-за его мелкого размера.

Размер

0.96»

1.3»

1.44»

1.8»

2.0»

Пиксели

160x80

240x240

128x128

160x128

320x240

Буфер

25600

115200

32768

40960

153600

Драйвер

ST7735S

ST7789

ST7735S

ST7735S

ST7789VW

Джойстик

Да

Да

Нет

Нет

Нет

Кнопки

2

4

4

0

4

GPIO пин

13

15

10

6

10

В этом руководстве мы будем использовать дисплей 1.44» 128x128, так как он обеспечивает хороший компромисс между размером базового текста, количеством пикселей для графики, размером буфера, наличием кнопок и ценой. Код легко адаптируется для работы на других дисплеях.

Подключение оборудования

Всё просто — вставьте пины Pico в разъём на задней стороне дисплея и подключите USB-кабель к компьютеру. Убедитесь, что вставляете правильной стороной — сторона с USB обозначена на нижней части платы. После установки Thonny можно приступать к работе.

Как обычно, Waveshare предоставляет базовый пример программы на MicroPython с включённым драйвером. Документацию и код можно найти здесь.

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

Система координат

При расположении кнопок справа и USB-кабеля слева верхний левый угол является началом координат: x = 0 и y = 0 (0, 0).

На этом экране ширина и высота составляют 128 пикселей, поэтому нижний правый угол будет (127, 127). Пиксели нумеруются от 0 до 127 включительно.

Наиболее полезная часть кода выглядит так:

LCD = LCD_1inch44()
#color BRG
LCD.fill(LCD.BLACK)

LCD.show()

LCD.fill_rect(15,40,75,12,LCD.YELLOW)
LCD.rect(15,40,75,12,LCD.YELLOW)
LCD.text("1in44-LCD",17,42,LCD.WHITE)

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

Следующая инструкция заполняет буфер дисплея числом, представляющим BLACK (в этот момент дисплей ещё не меняется):

LCD.fill(LCD.BLACK)

Третья инструкция отправляет содержимое буфера на дисплей и делает его чёрным (дисплей всё ещё не меняется визуально):

LCD.show()

Очень важно помнить: мы можем продолжать записывать значения в буфер экрана, но экран не обновляется и не показывает изменения до выполнения следующей инструкции:

LCD.show()

Строка ниже рисует сплошной прямоугольник на экране, начиная с позиции (15, 40), шириной 75 пикселей и высотой 12 пикселей, в цвете YELLOW:

LCD.fill_rect(15,40,75,12,LCD.YELLOW)

Следующая строка рисует прямоугольник-контур, также в YELLOW:

LCD.rect(15,40,75,12,LCD.YELLOW)

Эта строка выводит текст 1in44-LCD внутри YELLOW-прямоугольника белыми пикселями:

LCD.text("1in44-LCD",17,42,LCD.WHITE)

Изучение системы кодирования цветов

Все эти дисплеи Waveshare используют 16-битные коды цветов для смешивания цветов путём изменения соотношения яркостей красного, зелёного и синего в каждом пикселе. Поскольку глаза человека более чувствительны к зелёному свету, для зелёной компоненты выделяется дополнительный бит. Этот код называется RGB565: 5 бит для красного и синего, 6 бит для зелёного.

5 бит обеспечивают 32 уровня яркости (от 0 до 31), а 6 бит — 64 уровня (от 0 до 63). Комбинируя их, можно получить 32 × 64 × 32 = 65 536 различных цветов!

Уровни яркости красного и синего будут: ВЫКЛ, или комбинация из 1, 2, 4, 8 и 16. Зелёный будет: ВЫКЛ, или комбинация из 1, 2, 4, 8, 16 и 32.

Каждый из этих уровней цвета-яркости включается или выключается одним битом в 2-байтном, 16-битном числе. Если одновременно включено более одного, яркость суммируется. Если все выключены — экран показывает чёрный цвет. Если все включены — экран показывает белый цвет.

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

Этот код помогает найти ответ:

bits = []
bit = 1
for i in range(16):
    bits.append(bit)
    bit = bit *2
print(bits)

Он создаёт список степеней двойки — от младшего к старшему биту для 16-битного числа управления цветом. bits[0] = 1, bits[15] = 32768. [1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768]

hexx = "0123456789ABCDEF" # Счётчик бит в шестнадцатеричной системе — как строка.

Следующие инструкции очищают экран и выводят заголовок:

clear(0)
lcd.text("Colour",25,5,colour(255,0,0))         # RED — заголовок экрана
lcd.text("Control",25,15,colour(0,200,0))       # GREEN
lcd.text("Bits 0-15",25,25,colour(0,0,255))     # BLUE

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

for i in range(16):
    bit = 15 - i
    lcd.fill_rect(i*8,40,8,90,bits[bit])  # Цветной прямоугольник
    lcd.text(hexx[bit],i*8,50+i*4,colour(150,150,150)) # Шестнадцатеричный символ

lcd.show()   # Делает инструкции видимыми на экране
utime.sleep(7) # Задержка 7 секунд

Выполнение этого кода даёт следующее изображение:

Управление цветом Waveshare LCD — пример 1

Возможно, не то, что вы ожидали!

Синий управляется битами 3, 4, 5, 6 и 7.

Красный управляется битами 8, 9, 10, 11 и 12.

Зелёный управляется битами 13, 14, 15, 0, 1 и 2.

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

  • 11100000 00000111 — максимально яркий зелёный

  • 00011111 00000000 — максимально яркий красный

  • 00000000 11111000 — максимально яркий синий

  • 00000000 00000000 — чёрный = 0x0000

  • 11111111 11111111 — максимально яркий белый => красный, зелёный и синий на полную мощность => 0xFFFF

Упрощение с помощью RGB!

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

code = colour(r, g, b)

где r, g и b представляют яркость трёх основных цветов в диапазоне от 0 до 255. Например:

  • red = colour(255, 0, 0) — ярко-красный

  • green = colour(0, 255, 0) — ярко-зелёный

  • blue = colour(0, 0, 127) — синий на половину мощности

  • yellow = colour(255, 255, 0) — красный + зелёный

  • cyan = colour(0, 255, 255) — зелёный + синий

  • magenta = colour(255, 0, 255) — красный + синий

  • white = colour(255, 255, 255) — красный + зелёный + синий

  • mid-grey = (127, 127, 127) — половина яркости белого

(Если вы работали с адресуемыми RGB-светодиодами, вам знаком этот формат RGB888 для определения цвета.)

Вот функция:

def colour(R,G,B): # Конвертация RGB888 в RGB565
    return (((G&0b00011100)<<3) +((R&0b11111000)>>3)<<8) + (B&0b11111000)+((G&0b11100000)>>5)

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

7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 (RGB888 = 24 бит -> 3 отдельных байта)

Эти биты преобразуются в следующий вид:

4 3 2 7 6 5 4 3 7 6 5 4 3 7 6 5 (RGB565 = 16 бит -> 2 байта, содержащих 16-битное число)

Вот объяснение для ясности (если вам интересно!):

Младший байт

((G&0b11100000)>>5)Выбрать верхние 3 бита зелёного и сдвинуть на 5 позиций вправо -> 00000 111

(B&0b11111000)Выбрать все биты синего -> 11111 000

Старший байт

((G&0b00011100)<<3)Выбрать нижние 3 бита зелёного и сдвинуть на 3 позиции влево -> 111 0000

((R&0b11111000)>>3)Выбрать все биты красного и сдвинуть на 3 позиции вправо -> 000 11111

<<8Сдвинуть весь байт на 8 позиций влево => более значимый байт

Конечный результат: 1111111111111111

Скачать программы

Вы можете скачать 5 программ проверки цветов здесь:

Демонстрируемые процедуры

Эта процедура очищает экран до заданного цвета:

def clear(c):
    lcd.fill(c)

clear(0) # Чёрный фон
lcd.show()

clear(colour(160, 160, 160)) # Серый фон
lcd.show()

Следующий раздел кода показывает каждый из цветов одного бита на полном экране с номером бита и названием цвета:

srs =["Green 4","Green 5","Green 6","Blue 0","Blue 1","Blue 2","Blue 3","Blue 4","Red 0","Red 1","Red 2","Red 3","Red 4","Green 0","Green 1","Green 2"]
for i in range(16):
    lcd.fill(bits[i])
    lcd.text("Bit "+str(i),20,20,colour(200,200,200))
    lcd.text(srs[i],20,35,colour(200,200,200))
    lcd.show()
    utime.sleep(1.5)

Последний раздел кода демонстрирует использование функции colour() с простыми примерами текста и подсчитывает, сколько памяти ещё доступно для дополнительного кода:

clear(0)
lcd.text("Red",0,0,colour(255,0,0))
lcd.text("Green",0,10,colour(0,255,0))
lcd.text("Blue",0,20,colour(0,0,255))
lcd.text("Yellow",0,30,colour(255,255,0))
lcd.text("Cyan",0,40,colour(0,255,255))
lcd.text("Magenta",0,50,colour(255,0,255))
lcd.text("Dark Orange",0,60,colour(255,165,0))
gc.collect()
lcd.text("Mem free: "+str(gc.mem_free()),0,70,0xFFFF)
lcd.show()
utime.sleep(6)

Затем завершающий код:

pwm.duty_u16(32768)#max 65535 ========= ПОЛОВИНА ЯРКОСТИ
clear(0)
lcd.show()

Примечание: Цветовые константы в драйвере 1.44» заданы не все корректно. Именно поэтому фон YELLOW выглядит розоватым. Рекомендуем использовать функцию colour(R, G, B).

После портирования кода для работы на остальных четырёх дисплеях выяснилось, что они используют немного другую систему — биты синего и красного поменялись местами:

Управление цветом Waveshare LCD — пример 2

Для этих плат необходимо изменить функцию colour следующим образом:

def colour(R,G,B): # Конвертация RGB888 в RGB565
    return (((G&0b00011100)<<3) +((B&0b11111000)>>3)<<8) + (R&0b11111000)+((G&0b11100000)>>5)

Просто поменяйте местами переменные R и B.

Программы быстрого старта

Для удобства мы предоставляем минимальные программы настройки для каждой из 5 плат. Каждая программа включает правильный драйвер экрана, настраивает кнопки и джойстик (при наличии), содержит правильную версию функции colour(R, G, B) и отображает трёхстрочную проверку цветов в начале.

Вы можете скачать базовую программу для своей платы здесь:

Каждая программа содержит код драйвера экрана, настраивает кнопки/джойстик и корректно устанавливает переменные ширины и высоты, загружает необходимые библиотеки, определяет процедуры colour(R, G, B) и clear(c). Затем отображает текст для проверки цветов.

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

Попробуйте сами

1. Нарисуйте 50 прямоугольников случайного размера в случайных цветах на экране. Подсказка: вот как генерировать случайные числа:

import random
for i in range(10):
    print(random.randint(0,255)) #числа в диапазоне 0 - 255

2. Вблизи центра экрана, на тёмно-сером фоне, отобразите своё имя красным цветом и почтовый индекс голубым (cyan). Сдвиньте почтовый индекс на 10 пикселей правее имени.

3. Выровняйте имя и почтовый индекс по центру на своих строках.

Проект: смешивание цветов

На этом этапе нам нужно разделить Pico и дисплей. Нам нужен доступ к некоторым пинам GPIO для подключения трёх потенциометров на 10 кОм. Вы можете вставить Pico в макетную плату или, более удобно, использовать Pico Decker, на котором все пины Pico аккуратно пронумерованы и подписаны. Схема показана на рисунке.

Проект Waveshare Pico LCD — подключение потенциометров на макетной плате

Если у вашего дисплея есть кнопки, дополнительная кнопка не понадобится. Подключите пины SPI, питания и кнопок от вашего дисплея (на странице продукта/Wiki-странице для вашего конкретного дисплея будет показана распиновка).

Подключения к схеме на макетной плате

  • Чёрный — к GND

  • Оранжевый — к 3.3V

  • Красный — к ADC0 = GP26

  • Зелёный — к ADC1 = GP27

  • Синий — к ADC2 = GP28

  • Жёлтый — к GP15 = Key0 на дисплее 1.44»

В Thonny создайте новую копию программы Colour Check для вашего дисплея.

(File => Save as One Pot.py)

В копии удалите код ниже строки # ========== END OF SETUP === MAIN ===========.

Скопируйте следующий код и вставьте в конец программы:

# Настройка потенциометров
rpot=machine.ADC(26)
gpot=machine.ADC(27)
bpot=machine.ADC(28)
c = colour(0,0,0)    # ЧЁРНЫЙ

for i in range(100):
    r = rpot.read_u16()
    print(r)
    clear(r)
    lcd.text(str(r),10,10,c)
    lcd.text(str(hex(r)),10,20,c)
    lcd.show()
    utime.sleep(0.5)

pwm.duty_u16(32768)#max 65535 ========= ПОЛОВИНА ЯРКОСТИ
clear(0)
lcd.show()

Сохраните и запустите программу.

Поверните КРАСНЫЙ потенциометр на GP26. Вы должны увидеть быстрое изменение цветов на экране и изменения в нижней области Thonny (Shell). Числа должны изменяться от нуля до 65535 — полный 16-битный диапазон, используемый для наших кодов цветов. Обычно удаётся получить максимальное значение или близкое к нему, но нулевого получить не удавалось ни разу.

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

Измените счётчик цикла на 10. Поверните потенциометр до минимума и снова запустите программу.

Мы получили следующее:

>>> %Run -c $EDITOR_CONTENT
272
320
288
272
288
304
320
304
272
288

Поэтому мы изменили цикл следующим образом:

t = 0
for i in range(10):
    r = rpot.read_u16()
    print(r)
    t = t + r
    utime.sleep(0.1)
print("Average: ",int(t/10))

Что дало нам:

288
320
320
336
304
288
352
288
720
272
Average: 348

Изменение строки считывания показаний потенциометра на:

r = int((rpot.read_u16() - 348) /255)

Если прогнать цикл сто раз, изредка встречаются несколько значений -1. Это легко исправить, добавив две дополнительные строки:

for i in range(10):
    r = int((rpot.read_u16() - 348) /255)
    if r < 0:
        r = 0
    print(r)
    utime.sleep(0.1)

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

Сохраните текущую программу как Pots Mixing.py и замените цикл на:

for i in range(100):
    r = int((rpot.read_u16() - 348) /255)
    if r < 0:
        r = 0
    g = int((gpot.read_u16() - 348) /255)
    if g < 0:
        g = 0
    b = int((bpot.read_u16() - 348) /255)
    if b < 0:
        b = 0
    print(r, g, b)
    utime.sleep(0.5)

Поверните все три потенциометра поочерёдно и убедитесь, что диапазон каждого составляет от 0 до 255. Для дисплея 1.44» размером 128 × 128 добавьте эти строки перед циклом и обновите сам цикл:

width = 128   # Размер дисплея в пикселях
height = 128  # Настройте для вашего дисплея

w3 = int(width / 3)
h2 = int(height / 2)

while True:
    r = int((rpot.read_u16() - 348) /255)
    if r < 0:
        r = 0
    g = int((gpot.read_u16() - 348) /255)
    if g < 0:
        g = 0
    b = int((bpot.read_u16() - 348) /255)
    if b < 0:
        b = 0
    print(r, g, b)

    lcd.fill_rect(0,0,w3,h2,colour(r,0,0)) # КРАСНЫЙ
    lcd.fill_rect(w3+1,0,w3,h2,colour(0,g,0)) # ЗЕЛЁНЫЙ
    lcd.fill_rect(w3*2+2,0,w3,h2,colour(0,0,b)) # СИНИЙ
    lcd.fill_rect(0,h2,width,h2,colour(r,g,b)) # СМЕШАННЫЙ

    c = colour(255,255,255)
    lcd.text(str(r),10,10,c)
    if g > 200:
        c = 0
    lcd.text(str(g),10+w3,10,c)
    c = colour(255,255,255)
    lcd.text(str(b),10+w3*2,10,c)
    c = colour(255,255,255)
    if r+g+b > 300:
        c = 0
    lcd.text(str(colour(r,g,b)),10,10+h2,c)
    lcd.text(str(hex(colour(r,g,b))),10 + int(width/2),10+h2,c)
    lcd.show()
    utime.sleep(0.5)

Как это работает

  1. Вычисляются значения одной трети ширины и половины высоты

  2. Считываются показания трёх потенциометров и диапазон корректируется до 0–255

  3. Рисуются четыре прямоугольника: красный, зелёный, синий и смешанный

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

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

Вы можете скачать готовый код здесь: WS 1-44 128x128 Pots Mixing.py

Что ещё попробовать

  1. Покрутите потенциометры, чтобы найти коды цветов для: ярко-оранжевого, ярко-розового, тёмно-коричневого, светло-коричневого, фиолетового, светло-жёлтого, лаймово-зелёного. (Может помочь сайт https://htmlcolors.com/.)

  2. Добавьте шестнадцатеричное значение в 3 маленьких прямоугольника

  3. Остановите цикл нажатием кнопки; затем выполните завершающие действия

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

  5. Добавьте заглавный экран в начало с инструкциями

Надеемся, это руководство оказалось для вас полезным и интересным!