В данной статье мы рассмотрим подключение OLED дисплея SSD1306 к плате Raspberry Pi Pico по интерфейсу I2C. Программирование платы Raspberry Pi Pico будет осуществляться на языке Micropython. Настройку данной платы для работы с языком Micropython мы рассматривали в этой статье на нашем сайте. Также можете посмотреть статью про подключение OLED дисплея SSD1306 к плате Arduino и к плате Raspberry Pi – в них рассмотрены основные принципы работы с данным дисплеем.
Необходимые компоненты
- Плата Raspberry Pi Pico (купить на AliExpress).
- Модуль OLED дисплея SSD1306 128×64 с диагональю 1.3 дюйма и интерфейсом I2C (купить на AliExpress).
- Макетная плата.
- Соединительные провода.
Схема проекта
Схема подключения OLED дисплея SSD1306 к плате Raspberry Pi Pico представлена на следующем рисунке.
Контакт SDA модуля OLED дисплея подключен к контакту GPIO16 (Pin21) платы, а контакт SCL модуля OLED дисплея – к контакту GPIO17 (Pin22) платы. Контакт Vcc модуля OLED дисплея подключен к контакту 3.3v (Pin36) платы Raspberry Pi Pico. Контакт земли (Ground pin) дисплея подключен к контакту земли (Pin38) платы.
Библиотека для работы с OLED дисплеем на MicroPython
Для начала вам необходимо создать новую папку, в которую вы будете сохранять файлы программы. Далее необходимо скачать папку с программами с GitHub, в которой содержатся файлы программ для платы Raspberry Pi Pico на языке MicroPython. В этой скачанной папке с программами откройте папку с названием “T3_Interfacing_An_OLED”. Из этой папки вам необходимо скопировать файлы “main.py” и “ssd1306.py” в папку, которую вы создали. Файл “main.py” содержит код программы для отображения текста и изображений. Файл “ssd1306.py” – это библиотека для работы с модулем OLED дисплея. Но прежде чем приступать к рассмотрению кода программы вам необходимо выполнить преобразование изображения, которое вы хотите отобразить на экране OLED дисплея, в формат bmp. С этой целью в указанной папке “T3_Interfacing_An_OLED” вы можете найти файл “img_to_bmp.py”. Для запуска этой программы на выполнение вам необходимо окружение для работы с языком python. Автор данного проекта (ссылка на оригинал приведена в конце статьи) использовал для этого Python 3.6.
Объяснение программы для Raspberry Pi Pico
Полный код программы приведен в конце статьи, здесь же мы кратко рассмотрим его основные фрагменты. Мы рассмотрим код программы, содержащийся в файле “img_to_bmp.py”.
Сначала вам необходимо установить библиотеку “PIL” с помощью установщика пакетов “pip”. Параметр input_filename_with_path содержит путь к файлу без указания его расширения. В нашем случае файл с изображением и файл с программой img_to_bmp.py находятся в одной и той же папке. Поэтому мы будем упоминать только имя файла с изображением. Команда “Image.open(file_in)” будет открывать указанный файл с изображением, после чего мы сможем выполнять другие операции над данным изображением – сжимать, конвертировать и т.д.
Функция save() используется для сохранения конвертированного изображения в виде выходного файла с сохранением “.bmp”. После этого функция Image.open(file_out,mode='r') используется для открытия “bmp” файла, а функция io.BytesIO() используется считывания битового массива (bitmaparray) изображения. Массив img_bytes используется для хранения этого массива. С помощью функции print(img_bytes) производится печать массива нашего изображения. Нам необходимо скопировать этот байтовый массив для его последующего использования в файле “main.py”. Функция print('Image Resolution: ({0},{1})'.format(img.width,img.height)) выводит на печать разрешение изображения. Нам это разрешение будет необходимо в файле “main.py”. Убедитесь в том, что разрешение изображения не очень большое. Для уменьшения разрешения изображения вы можете использовать любой удобный вам графический редактор.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import io from PIL import Image input_filename_with_path = "rpilogo1" file_in = input_filename_with_path + ".png" file_out = input_filename_with_path+".bmp" img = Image.open(file_in) img = img.transpose(Image.FLIP_LEFT_RIGHT) threshold = 65 func = lambda x : 255 if x > threshold else 0 img = img.convert('L').point(func,mode='1') img.save(file_out) img = Image.open(file_out,mode='r') img_bytes = io.BytesIO() img.save(img_bytes,format='BMP') img_bytes = img_bytes.getvalue() print("Copy this bitmap array:") print('\n') print(img_bytes) print('\n') print('Image Resolution: ({0},{1})'.format(img.width,img.height)) |
Вам необходимо будет скопировать следующий массив:
На представленном рисунке показан результат работы программы из файла img_to_bmp.py. В нашем случае мы использовали изображение, конвертированное в разрешение 32x32.
Итак, у нас есть байтовый массив (byte array) изображения. Теперь рассмотрим код файла “main.py”. В этом файле мы используем библиотеку “machine” для выполнения основных операций с аппаратным обеспечением ("железом") с помощью языка MicroPython. Также мы используем библиотеку “ssd1306”, которую мы ранее добавили в плату Raspberry Pi Pico. Библиотека “utime” используется для доступа к встроенным функциям времени платы Raspberry Pi Pico, а библиотека framebuf,sys – для выполнения операций с массивами.
1 2 3 4 5 6 |
from machine import Pin, I2C from ssd1306 import SSD1306_I2C import utime import framebuf,sys WIDTH = 128 # oled display width HEIGHT = 64 |
В массиве с именем image_byte_arr содержится байтовый массив (byte array) изображения, которое мы будем отображать на экране дисплея. В него необходимо вставить массив, который мы получили в результате исполнения программы из файла img_to_bmp.py. Его размерность точно такая же, как и выходного массива, сформированного в результате выполнения программы img_to_bmp.py.
1 2 3 |
image_byte_arr = b'BM\xbe\x00\x00\x00\x00\x00\x00\x00>\x00\x00\x00(\x00\x00\x00 \x00\x00\x00 \x00\x00\x00\x01\x00\x01\x00\x00\x00\x00\x00\x80\x00\x00\x00\xc4\x0e\x00\x00\xc4\x0e\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xfc?\xff\xff\xfb\xcf\xff\xff\xe7\xf7\xff\xff\xc7\xf1\xff\xffs\xc6\x7f\xfe\xf1\xcf\xbf\xfd\xf7\xef\xbf\xfd\xf7\xf7\xdf\xff\xff\xf7\xdf\xfb\xef\xf3\xdf\xf9\xc7\xf1\x8f\xf6\x13\xeco\xf7|>w\xf7~\x7fw\xf7\xfe\x7fw\xf7\xff\x7fo\xfb\x7f\x7fo\xfc~~\x1f\xfd\xbc=\xff\xff\xe3\xe3\xbf\xfe\xf7\xf7\xbf\xff\x7f\xff\x7f\xff3\xe4\xff\xffx\x1f\x7f\xfe\xfc?\xbf\xfd\xfe?\xff\xfb\xff\x7f\xdf\xfb\xff\x7f\xdf\xfb\xfe\x7f\xef\xfb\xff\xbf\xef\xf9\xfb\xdf\x9f\xff\x0f\xf0\xff' image_width = 32 image_height = 32 |
Далее в программе функция I2C() используется для инициализации связи по интерфейсу I2C. В функцию необходимо передать номер используемого канала I2C поскольку в плате Raspberry Pi Pico таких каналов 2. В нашем случае мы используем канал I2C0. Если вы будете использовать канал I2C1, то тогда вам необходимо изменить первый параметр в данной функции с 0 на 1. Следующие два параметра при вызове данной функции используются для указания контактов SCL и SDL, используемых для осуществления связи по интерфейсу I2C. В нашем случае для этих целей мы используем контакты 17 и 16. Следующий параметр функции I2C используется для указания рабочей частоты, используемой для связи по интерфейсу I2C. Функция i2c.scan() используется для определения адреса устройства.
1 2 3 4 |
#OLED I2C Configuration i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000) #Init I2C using pinsGP17&GP16 (defaultI2C0 pins) print("I2C Address : "+hex(i2c.scan()[0]).upper()) # Display device address print("I2C Configuration: "+str(i2c)) # Display I2C config |
Функция SSD1306_I2C() используется для инициализации OLED дисплея и формирования “oled” объекта. В качестве входных параметров в данной функции используются ширина и высота экрана дисплея, а также режим его работы. Затем мы используем функции из библиотеки ssd1306, вызывая их при помощи созданного нами “oled” объекта. Например, функция oled.fill(0) используется для очистки экрана дисплея.
1 |
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c) # Init oled display |
Функция displayText() используется для отображения текста на экране OLED дисплея. В качестве параметров данной функции необходимо указать выводимый текст, позицию вывода, параметры clear_oled и the show_text. Позиция для вывода текста по умолчанию равна (0,0). Но вы можете изменить ее по своему желанию – ее необходимо указывать в формате (x,y). По умолчанию значение параметров clear_oled и show_text установлено в True.
1 2 3 4 5 6 7 |
#OLED Text display Function def displayText(text, position=(0,0),clear_oled=True,show_text=True): if clear_oled: oled.fill(0) # Clear the oled display in case it has junk on it. oled.text(text,position[0],position[1]) # dispaying text if show_text: oled.show() # Updating the display |
Функция displayImage() используется для отображения изображения на экране OLED дисплея. В качестве входных параметров в данную функцию передаются параметры image_byte_array, image_resolution, position, clear_oled и show_img. Параметр image_resolution содержит ширину и высоту изображения. Позиция для вывода изображения указывается в формате (x,y), по умолчанию она равна (0,0). В нашем случае параметры clear_oled и show_img устанавливаются в False и True соответственно. Функция bytearray() используется для получения растра изображения. Далее функция framebuf.FrameBuffer() используется для сохранения каждого бита этого растрового изображения. Буфер кадров (frame buffer) может работать в нескольких различных режимах, мы будем использовать режим MONO_HMSB. После этого мы используем функцию oled.blit() для отображения кадра изображения на экране OLED дисплея.
1 2 3 4 5 6 7 8 9 10 11 12 |
#OLED Image display function def displayImage(image_byte_array, image_resolution,position=(0,0),clear_oled=False,show_img=True): img = bytearray(image_byte_array) img = bytearray([img[i] for i in range(len(img)-1,-1,-1)]) frame = framebuf.FrameBuffer(img, image_resolution[0], image_resolution[1], framebuf.MONO_HMSB) # frame buffer types: MONO_HLSB, MONO_VLSB, MONO_HMSB if clear_oled: oled.fill(0) # clear the OLED print("clear") if show_img: oled.blit(frame, position[0],position[1]) # show the image at location (x=0,y=0) oled.show() print("display") |
Следующий фрагмент кода используется для отображения текста на экране OLED и его скроллинга. Мы вызываем функцию displayText() два раза в цикле loop и передаем в нее в качестве параметров строки text1 и text2. Мы установили clear_oled в False и show_text в True для отображения строки text1. Для отображения строки text2 мы установили clear_oled в True и show_text в True. Это позволит нам отображать строки текста на экране дисплея при каждой итерации. Когда значение переменной “x” достигнет конца текущей итерации (это будет соответствовать значению ширины дисплея), будет происходить прерывание работы цикла (break).
1 2 3 4 5 6 7 8 9 10 |
#Scrolling Text on OLED text1 = "Welcome to" text2 = "CircuitDigest" for x in range(0, WIDTH): displayText(text1,(x,0),clear_oled=False,show_text=True) displayText(text2,(WIDTH-x,20),clear_oled=True, show_text=True) if x == WIDTH: break else: x+=5 |
Далее в цикле while мы будем использовать анимацию текста и изображения. Для отображения изображения будет использоваться функция displayImage, в которую будут передаваться параметры image_byte_arr, (image_width, image_height), (x,y), и clear_oled. (x,y) – это позиция для вывода изображения, image_width – ширина изображения, image_height – высота изображения. clear_oled устанавливаем в False. Значение переменной show_img мы не устанавливаем – оно по умолчанию установлено в True. Таким образом, вы можете использовать функции displayImage() и displayText() для отображения изображения и текста на экране OLED дисплея соответственно.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
while True: y=0 text = "Interfacing OLED" oled.fill(0) #Animating Text And Image Horizontaly for x in range(0,WIDTH-image_width): displayImage(image_byte_arr,(image_width,image_height),(x,y),clear_oled=False) displayText(text,(x,y+40),clear_oled=False,show_text=True) if x == (WIDTH-image_width)-1: break else: x+=2 oled.fill(0) for x in range(WIDTH-image_width,-1,-1): displayImage(image_byte_arr,(image_width,image_height),(x,y),clear_oled=True) displayText(text,(x,y+40),clear_oled=False,show_text=True) if x == 0: break else: x-=2 |
Теперь откройте файлы “main.py” и “ssd1306.py” в Thonny IDE. Сначала вам необходимо сохранить файл “ssd1306.py” в плате Raspberry Pi Pico при помощи нажатия комбинации клавиш “ctrl+shift+s” на клавиатуре. Убедитесь в том, что ваша плата подключена к компьютеру в то время, когда вы производите эти операции. Когда вы начнете процесс сохранения файла вы увидите всплывающее окно, показанное на следующем рисунке. В этом окне вам необходимо выбрать плату Raspberry Pi Pico, ввести имя файла и нажать на кнопку save. Далее аналогичную процедуру необходимо проделать для файла “main.py”. Эта процедура позволит вам запускать программу в то время, когда на плату подано питание.
Исходный код программы на MicroPython
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 |
# отображение изображения и текста на OLED дисплее ssd1306 с интерфейсом I2C from machine import Pin, I2C from ssd1306 import SSD1306_I2C import utime WIDTH = 128 # ширина oled дисплея HEIGHT = 64 # высота oled дисплея i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000) # инициализация I2C используя контакты GP8 & GP9 (default I2C0 pins) print("I2C Address : "+hex(i2c.scan()[0]).upper()) # отображение адреса устройства print("I2C Configuration: "+str(i2c)) # отображение конфигурации I2C oled = SSD1306_I2C(WIDTH, HEIGHT, i2c) # инициализация oled дисплея # очистка экрана дисплея oled.fill(0) # Add some text oled.text("Welcome",5,8) oled.text("To",15,8) oled.text("CIRCUIT DIGEST",8,25) # обновляем состояние oled дисплея чтобы изображение и текст отобразились на нем oled.show() utime.sleep(1) counter = 0 while True: oled.fill(0) utime.sleep(0.2) oled.text("Counter:",5,8) oled.text(str(counter),15,8) oled.show() utime.sleep(2) counter+=1 SSD1306: # MicroPython SSD1306 OLED driver, I2C and SPI interfaces from micropython import const import framebuf # register definitions SET_CONTRAST = const(0x81) SET_ENTIRE_ON = const(0xA4) SET_NORM_INV = const(0xA6) SET_DISP = const(0xAE) SET_MEM_ADDR = const(0x20) SET_COL_ADDR = const(0x21) SET_PAGE_ADDR = const(0x22) SET_DISP_START_LINE = const(0x40) SET_SEG_REMAP = const(0xA0) SET_MUX_RATIO = const(0xA8) SET_COM_OUT_DIR = const(0xC0) SET_DISP_OFFSET = const(0xD3) SET_COM_PIN_CFG = const(0xDA) SET_DISP_CLK_DIV = const(0xD5) SET_PRECHARGE = const(0xD9) SET_VCOM_DESEL = const(0xDB) SET_CHARGE_PUMP = const(0x8D) # Subclassing FrameBuffer provides support for graphics primitives # http://docs.micropython.org/en/latest/pyboard/library/framebuf.html class SSD1306(framebuf.FrameBuffer): def __init__(self, width, height, external_vcc): self.width = width self.height = height self.external_vcc = external_vcc self.pages = self.height // 8 self.buffer = bytearray(self.pages * self.width) super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB) self.init_display() def init_display(self): for cmd in ( SET_DISP | 0x00, # off # установка адреса SET_MEM_ADDR, 0x00, # horizontal # resolution and layout SET_DISP_START_LINE | 0x00, SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 SET_MUX_RATIO, self.height - 1, SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 SET_DISP_OFFSET, 0x00, SET_COM_PIN_CFG, 0x02 if self.width > 2 * self.height else 0x12, # timing and driving scheme SET_DISP_CLK_DIV, 0x80, SET_PRECHARGE, 0x22 if self.external_vcc else 0xF1, SET_VCOM_DESEL, 0x30, # 0.83*Vcc # display SET_CONTRAST, 0xFF, # maximum SET_ENTIRE_ON, # output follows RAM contents SET_NORM_INV, # not inverted # charge pump SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14, SET_DISP | 0x01, ): # on self.write_cmd(cmd) self.fill(0) self.show() def poweroff(self): self.write_cmd(SET_DISP | 0x00) def poweron(self): self.write_cmd(SET_DISP | 0x01) def contrast(self, contrast): self.write_cmd(SET_CONTRAST) self.write_cmd(contrast) def invert(self, invert): self.write_cmd(SET_NORM_INV | (invert & 1)) def show(self): x0 = 0 x1 = self.width - 1 if self.width == 64: # displays with width of 64 pixels are shifted by 32 x0 += 32 x1 += 32 self.write_cmd(SET_COL_ADDR) self.write_cmd(x0) self.write_cmd(x1) self.write_cmd(SET_PAGE_ADDR) self.write_cmd(0) self.write_cmd(self.pages - 1) self.write_data(self.buffer) class SSD1306_I2C(SSD1306): def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False): self.i2c = i2c self.addr = addr self.temp = bytearray(2) self.write_list = [b"\x40", None] # Co=0, D/C#=1 super().__init__(width, height, external_vcc) def write_cmd(self, cmd): self.temp[0] = 0x80 # Co=1, D/C#=0 self.temp[1] = cmd self.i2c.writeto(self.addr, self.temp) def write_data(self, buf): self.write_list[1] = buf self.i2c.writevto(self.addr, self.write_list) class SSD1306_SPI(SSD1306): def __init__(self, width, height, spi, dc, res, cs, external_vcc=False): self.rate = 10 * 1024 * 1024 dc.init(dc.OUT, value=0) res.init(res.OUT, value=0) cs.init(cs.OUT, value=1) self.spi = spi self.dc = dc self.res = res self.cs = cs import time self.res(1) time.sleep_ms(1) self.res(0) time.sleep_ms(10) self.res(1) super().__init__(width, height, external_vcc) def write_cmd(self, cmd): self.spi.init(baudrate=self.rate, polarity=0, phase=0) self.cs(1) self.dc(0) self.cs(0) self.spi.write(bytearray([cmd])) self.cs(1) def write_data(self, buf): self.spi.init(baudrate=self.rate, polarity=0, phase=0) self.cs(1) self.dc(1) self.cs(0) self.spi.write(buf) self.cs(1) OLED: # Display Image & text on I2C driven ssd1306 OLED display from machine import Pin, I2C from ssd1306 import SSD1306_I2C import utime WIDTH = 128 # oled display width HEIGHT = 64 # oled display height i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000) # Init I2C using pins GP8 & GP9 (default I2C0 pins) print("I2C Address : "+hex(i2c.scan()[0]).upper()) # Display device address print("I2C Configuration: "+str(i2c)) # Display I2C config oled = SSD1306_I2C(WIDTH, HEIGHT, i2c) # Init oled display # Clear the oled display in case it has junk on it. oled.fill(0) # Add some text oled.text("Welcome",5,8) oled.text("To",15,8) oled.text("CIRCUIT DIGEST",8,25) # Finally update the oled display so the image & text is displayed oled.show() utime.sleep(1) counter = 0 while True: oled.fill(0) utime.sleep(0.2) oled.text("Counter:",5,8) oled.text(str(counter),15,8) oled.show() utime.sleep(2) counter+=1 OLED: # Display Image & text on I2C driven ssd1306 OLED display from machine import Pin, I2C from ssd1306 import SSD1306_I2C import utime WIDTH = 128 # oled display width HEIGHT = 64 # oled display height i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000) # Init I2C using pins GP8 & GP9 (default I2C0 pins) print("I2C Address : "+hex(i2c.scan()[0]).upper()) # Display device address print("I2C Configuration: "+str(i2c)) # Display I2C config oled = SSD1306_I2C(WIDTH, HEIGHT, i2c) # Init oled display # Clear the oled display in case it has junk on it. oled.fill(0) # Add some text oled.text("Welcome",5,8) oled.text("To",15,8) oled.text("CIRCUIT DIGEST",8,25) # Finally update the oled display so the image & text is displayed oled.show() utime.sleep(1) counter = 0 while True: oled.fill(0) utime.sleep(0.2) oled.text("Counter:",5,8) oled.text(str(counter),15,8) oled.show() utime.sleep(2) counter+=1 OLED: # Display Image & text on I2C driven ssd1306 OLED display from machine import Pin, I2C from ssd1306 import SSD1306_I2C import utime WIDTH = 128 # oled display width HEIGHT = 64 # oled display height i2c = I2C(0, scl=Pin(17), sda=Pin(16), freq=200000) # Init I2C using pins GP8 & GP9 (default I2C0 pins) print("I2C Address : "+hex(i2c.scan()[0]).upper()) # Display device address print("I2C Configuration: "+str(i2c)) # Display I2C config oled = SSD1306_I2C(WIDTH, HEIGHT, i2c) # Init oled display # Clear the oled display in case it has junk on it. oled.fill(0) # Add some text oled.text("Welcome",5,8) oled.text("To",15,8) oled.text("CIRCUIT DIGEST",8,25) # Finally update the oled display so the image & text is displayed oled.show() utime.sleep(1) counter = 0 while True: oled.fill(0) utime.sleep(0.2) oled.text("Counter:",5,8) oled.text(str(counter),15,8) oled.show() utime.sleep(2) counter+=1 |