ssd1306 using I2C on the esp8266

All ESP8266 boards running MicroPython.
Official boards are the Adafruit Huzzah and Feather boards.
Target audience: MicroPython users with an ESP8266 board.
User avatar
mcauser
Posts: 400
Joined: Mon Jun 15, 2015 8:03 am

Re: ssd1306 using I2C on the esp8266

Post by mcauser » Sat Jul 30, 2016 1:03 pm

@mflmartin I'll try to explain.

I hand crafted the [7,192,24,48...] array by drawing a smily on a 16x16 grid, split into groups of 8 pixels, then converted binary to decimal.
16 pixels wide fits into 2 bytes.
Each 2 bytes in the array is a row.
MSB of byte 1 is the top left pixel. LSB of byte 2 is the top right pixel.
MSB of byte 3 is the 2nd row left most pixel, LSB of byte 4 is the 2nd row right most pixel.

The smiley is actually only 15x15, but I used a 16x15 grid. You may notice below the right most column is all zeros.
There's only 30 bytes instead of 32 as you would expect for a 16x16 image, because I skipped the last two bytes. ie. the last row.

If you convert the array to binary (and squint), you may see the the image.

Code: Select all

[00000111,11000000,            .....11111......
00011000,00110000,             ...11.....11....
00100000,00001000,             ..1.........1...
01000000,00000100,             .1...........1..
01000000,00000100,             .1...........1..
11111111,11111110,             111111111111111.
10100111,10011010,             1.1..1111..11.1.
10101111,10111010,    --->     1.1.11111.111.1.
10011100,01110010,             1..111...111..1.
10000000,00000010,             1.............1.
01000000,00100100,             .1........1..1..
01000011,11000100,             .1....1111...1..
00100000,00001000,             ..1.........1...
00011000,00110000,             ...11.....11....
00000111,11000000]             .....11111......
It was a rather laborious process!

If I had a 9x9 image, it would still require 2 bytes per row, but there would be lots of wasted space.
It would effectively be a 16x9 image. 2 bytes per row.

Now, a more efficient format would be to warp to the next row at exactly 9 bits, but would be more complex to decode.
eg. a 4x4 image currently fits on a 8x4 grid with 4px wasted per row. 32 bits, 4 bytes.
If it were stored more efficiently, with no wasted space, it's 16 bits in total, or 2 bytes.

mflmartin
Posts: 17
Joined: Sat Jul 23, 2016 7:30 pm

Re: ssd1306 using I2C on the esp8266

Post by mflmartin » Sat Jul 30, 2016 1:43 pm

deshipu wrote:I didn't save the snippets that I normally use, but here's one that I recently used to generate data for a 2-bit font:
[/code]
I see, so you were able to substitute the default text font? If so, how do you link the new font as the default?

I wanted to do that, and use pixel fonts ( the ones that were very popular in old flash sites: http://www.fontsforflash.com/ )

Thanks!

mflmartin
Posts: 17
Joined: Sat Jul 23, 2016 7:30 pm

Re: ssd1306 using I2C on the esp8266

Post by mflmartin » Sat Jul 30, 2016 1:49 pm

mcauser wrote:@mflmartin I'll try to explain.

I hand crafted the [7,192,24,48...] array by drawing a smily on a 16x16 grid, split into groups of 8 pixels, then converted binary to decimal.
16 pixels wide fits into 2 bytes...
Yeah, it seems labourious... hmmmmm. Thanks for the in depth explanation. It helps!

I wouldn't want to do this by hand, it looks like a waste of time if you have to do a lot of images. But the efficient way you use to store the data is, indeed, very interesting.
Last edited by mflmartin on Sat Jul 30, 2016 2:16 pm, edited 1 time in total.

User avatar
deshipu
Posts: 1347
Joined: Thu May 28, 2015 5:54 pm

Re: ssd1306 using I2C on the esp8266

Post by deshipu » Sat Jul 30, 2016 2:09 pm

mflmartin wrote: I see, so you were able to substitute the default text font? If so, how do you link the new font as the default?
No, I didn't substitute the default font. I used the font I generated on my e-ink display, using custom code to draw it:

Code: Select all

DATA = (...)

def blit(pixel, x, y, char, palette):
    char = ord(char) - 0x20
    if not 0 <= char <= 127:
        char = 127
    for row, byte in enumerate(DATA[char]):
        for col in range(4):
            pixel(x + col, y + row, palette[(byte >> (col * 2)) & 0x03])

def blit2(pixel, x, y, char):
    blit(pixel, x, y, char, (0, 0, 1, 1))

def blit4(pixel, x, y, char):
    blit(pixel, x, y, char, (0, 1, 2, 3))

Code: Select all

from machine import Pin, SPI
import ssd1606
import font4x6

text = """Lorem ipsum dolor sit amet, consectetur
adipiscing elit, sed do eiusmod tempor
incididunt ut labore & dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud
exercitation ullamco laboris nisi ut aliquip
ex ea commodo consequat? Duis aute: irure
dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur.
Excepteur s'int occaecat cupidatat "non"
proident; sunt in culpa qui officia deserunt
mollit anim id est laborum!"""


spi = SPI(miso=Pin(12), mosi=Pin(13, Pin.OUT), sck=Pin(14, Pin.OUT))
display = ssd1606.SSD1306_SPI(177, 72, Pin(5), Pin(4), Pin(15))
display.fill(3)

for y, line in enumerate(text.split("\n")):
    for x, char in enumerate(line):
        font4x6.blit4(display.pixel, x * 4, y * 6, char)
display.show()

The result is:
IMG_20160703_115759.jpg
IMG_20160703_115759.jpg (132.13 KiB) Viewed 3467 times
Last edited by deshipu on Sat Jul 30, 2016 2:15 pm, edited 1 time in total.

mflmartin
Posts: 17
Joined: Sat Jul 23, 2016 7:30 pm

Re: ssd1306 using I2C on the esp8266

Post by mflmartin » Sat Jul 30, 2016 2:14 pm

deshipu wrote:I didn't save the snippets that I normally use, but here's one that I recently used to generate data for a 2-bit font:

Code: Select all


import pygame

colors = {
    (0, 0, 0, 255): 0,
    (102, 102, 102, 255): 1,
    (204, 204, 204, 255): 2,
    (255, 255, 255, 255): 3,
}
image = pygame.image.load("font.png")
images = []

for tile_x in range(0, image.get_size()[0]/4):
    rect = (tile_x * 4, 0, 4, 6)
    images.append(image.subsurface(rect))
    
for image in images:
    print '(%s),' % ', '.join('%d' %
        sum(colors[tuple(image.get_at((x, y)))] << (x * 2)
            for x in range(4))
        for y in range(6))

When I compile this, with python 2.7, and pygame, I get this:

Code: Select all

Traceback (most recent call last):
  File "pytest.py", line 20, in <module>
    for y in range(6))
  File "pytest.py", line 20, in <genexpr>
    for y in range(6))
  File "pytest.py", line 19, in <genexpr>
    for x in range(4))
KeyError: (25, 25, 25, 255)
I guess, there is something wrong with the join not being closed? But I am not used to this intrincated structures in python yet (with the sum followed by the fors, etc...). I tried a bunch of things to make it work, without success.

Also, I have a question about the colors array. What is the relationship with the image (I assumed the image is a pure black over white background usually).

Thanks, and pardon my ignorance in advance :D.

User avatar
deshipu
Posts: 1347
Joined: Thu May 28, 2015 5:54 pm

Re: ssd1306 using I2C on the esp8266

Post by deshipu » Sat Jul 30, 2016 2:17 pm

No, the error you are getting is because the image you are reading has a color that is not included in the colors dictionary, so it doesn't know what to do with it.

mflmartin
Posts: 17
Joined: Sat Jul 23, 2016 7:30 pm

Re: ssd1306 using I2C on the esp8266

Post by mflmartin » Sat Jul 30, 2016 11:53 pm

deshipu wrote:No, the error you are getting is because the image you are reading has a color that is not included in the colors dictionary, so it doesn't know what to do with it.
Oupppps, my bad! You are right. The black was set to 191919.

So, I understand it about black and white, but what do this other 2 colors do?

(0, 0, 0, 255): 0,
(102, 102, 102, 255): 1,
(204, 204, 204, 255): 2,

(255, 255, 255, 255): 3,


Also, I tried to use the script on an image. I assume every character of your font is 4 pixels wide, and 6 pixel tall. So, if I feed this image:

Image

I get, for the first 2 characters:

(0, 252, 252, 12, 60, 0),
(0, 60, 0, 60, 60, 60),


But, when I feed this into the drawbitmapdata function of @mcauser ...

i2c = I2C(sda=Pin(2), scl=Pin(0))
display = ssd1306.SSD1306_I2C(128, 64, i2c, 60)

g = [0, 252, 252, 12, 60, 0]
display.draw_bitmap(0, 0, g, 4, 6, 1)
display.show()

I get a different set of pixels drawn, not the G. I am sure I am missing something.

User avatar
deshipu
Posts: 1347
Joined: Thu May 28, 2015 5:54 pm

Re: ssd1306 using I2C on the esp8266

Post by deshipu » Sun Jul 31, 2016 8:25 am

The display I'm using has 4 shades of grey, encoded in pairs of bits: 00 is black, 01 is dark grey, 10 is light grey and 11 is white. So I did the same thing with my font. That's why every 4x6 character is encoded as 6 bytes -- 2 bits for every pixel. Of course you can modify the pygame program to encode pixels as individual bits -- I leave that as the exercise for the reader ;-)

mflmartin
Posts: 17
Joined: Sat Jul 23, 2016 7:30 pm

Re: ssd1306 using I2C on the esp8266

Post by mflmartin » Sun Jul 31, 2016 6:06 pm

deshipu wrote:The display I'm using has 4 shades of grey, encoded in pairs of bits: 00 is black, 01 is dark grey, 10 is light grey and 11 is white. So I did the same thing with my font. That's why every 4x6 character is encoded as 6 bytes -- 2 bits for every pixel. Of course you can modify the pygame program to encode pixels as individual bits -- I leave that as the exercise for the reader ;-)

Thanks for the help!

I got it now. A little bit tricky, and possible my code is not as elegant and compressed as yours (still getting the hang of some python structures, as I come from JAVA/AS3).

This is the code for encoding 8x8 images.

Code: Select all

import pygame

colors = {
    (0, 0, 0, 255): 1,
    (255, 255, 255, 255): 0,
}
image = pygame.image.load("pygame2.png")
images = []

for tile_x in range(0, image.get_size()[0]/8):
    rect = (tile_x * 8, 0, 8, 8)
    images.append(image.subsurface(rect))
    
for image in images:
    
	
	result = ""
	for y in range(8):
		byte = 0
		for x in range(8):
			byte |= colors[tuple(image.get_at((x, y)))] << (7 - x)
			print("{0:b}".format(byte))
		
		result += format(byte)
		if y<7: result +=","
		
	print("({0})".format(result))
    
* (I left the trace of the bytes for checking purposes, useful when I was running into errors)

I think, with a bit of work next week, I can mash it into a little program to read all kinds of 2 color images and sizes and get the corresponding data, now that I learned about bits, masking, and how to set them. I will share the code when finished or put it up in github. :).


I have 2 questions though:
1) I saw that @mcauser used 2 bytes to encode 2 rows. Also you used bytes, with info by pair of bits.
If you have a 16x16 image, why not just use 16 bits all together?
If you have a 16x16 image, the first row and 2 columns of 16 pixels, that now @mcauser encoded as 2 bytes:
00000111,11000000 -----> (7,192)

could be encoded as
0000011111000000 -----> 1984

Knowing the width and height of the image, I assume, you could just iterate over all those bits once, instead of having to do 2 passes (one per byte). Also, one would not loose bits in bytes as zeros, if not necessary, no?

Let's imagine an image of 9 pixels wide and 2 pixels height with this structure:
011100111
000010000

I would encode it as (231, 16)
I do just 2 rows, 9 passes per row. No loss or irrelevant bits allocated in bytes.

Maybe I am missing something, by I don't see the point of sticking to bytes (bits in groups of 8), if you can get any bit easily in a N row of bits.

What is your input on this matter?


2) About your font, @deshipu:

I understood now how you did the multiple colors (also very useful). I wanted to know what is the structure of the DATA object that you used, and if you encoded each char in HEX (I saw that often). And if you did the pairing of each character manually.

Cheers,

User avatar
deshipu
Posts: 1347
Joined: Thu May 28, 2015 5:54 pm

Re: ssd1306 using I2C on the esp8266

Post by deshipu » Sun Jul 31, 2016 6:20 pm

mflmartin wrote: If you have a 16x16 image, why not just use 16 bits all together?
Yes, you can do it -- if you are using numbers. However, if you want to save space, at some point you will want to save this as raw
strings of bytes.
mflmartin wrote: 2) About your font, @deshipu:

I understood now how you did the multiple colors (also very useful). I wanted to know what is the structure of the DATA object that you used, and if you encoded each char in HEX (I saw that often). And if you did the pairing of each character manually.
The whole font file looks like this:

Code: Select all

DATA = (
    (255, 255, 255, 255, 255, 255),
    (243, 243, 247, 251, 243, 255),
    (204, 221, 238, 255, 255, 255),
    (221, 128, 221, 128, 221, 255),
    (247, 129, 228, 198, 208, 247),
    (204, 219, 243, 249, 204, 255),
    (246, 205, 99, 220, 102, 255),
    (243, 247, 254, 255, 255, 255),
    (246, 253, 252, 253, 246, 255),
    (231, 223, 207, 223, 231, 255),
    (255, 217, 226, 217, 255, 255),
    (255, 243, 192, 243, 255, 255),
    (255, 255, 255, 243, 247, 254),
    (255, 255, 128, 255, 255, 255),
    (255, 255, 255, 255, 243, 255),
    (207, 219, 243, 249, 252, 255),
    (210, 205, 200, 220, 225, 255),
    (247, 241, 243, 243, 226, 255),
    (225, 206, 227, 253, 192, 255),
    (225, 206, 227, 206, 225, 255),
    (243, 249, 220, 192, 207, 255),
    (192, 252, 228, 207, 225, 255),
    (210, 252, 225, 204, 226, 255),
    (192, 219, 243, 249, 252, 255),
    (226, 204, 226, 204, 226, 255),
    (226, 204, 210, 207, 225, 255),
    (255, 243, 255, 243, 255, 255),
    (255, 243, 255, 243, 247, 254),
    (207, 243, 252, 243, 207, 255),
    (255, 192, 255, 192, 255, 255),
    (252, 243, 207, 243, 252, 255),
    (225, 207, 227, 251, 243, 255),
    (226, 205, 196, 212, 189, 210),
    (226, 221, 204, 196, 204, 255),
    (228, 204, 228, 204, 228, 255),
    (226, 205, 252, 205, 226, 255),
    (228, 220, 204, 220, 228, 255),
    (208, 252, 244, 252, 208, 255),
    (208, 252, 252, 244, 252, 255),
    (210, 253, 252, 141, 210, 255),
    (204, 204, 196, 204, 204, 255),
    (209, 243, 243, 243, 209, 255),
    (203, 207, 207, 220, 226, 255),
    (220, 204, 216, 244, 200, 255),
    (252, 252, 252, 236, 192, 255),
    (221, 196, 192, 200, 204, 255),
    (205, 212, 209, 197, 220, 255),
    (226, 221, 204, 221, 226, 255),
    (228, 204, 204, 228, 252, 255),
    (226, 204, 204, 200, 210, 207),
    (228, 204, 204, 224, 204, 255),
    (210, 236, 226, 206, 225, 255),
    (192, 226, 243, 243, 243, 255),
    (204, 204, 204, 221, 226, 255),
    (204, 204, 221, 230, 243, 255),
    (204, 200, 196, 192, 217, 255),
    (204, 217, 226, 217, 204, 255),
    (204, 221, 230, 243, 243, 255),
    (192, 222, 247, 237, 192, 255),
    (208, 252, 252, 252, 208, 255),
    (252, 249, 243, 219, 207, 255),
    (193, 207, 207, 207, 193, 255),
    (243, 217, 238, 255, 255, 255),
    (255, 255, 255, 255, 128, 255),
    (252, 247, 239, 255, 255, 255),
    (255, 210, 205, 204, 134, 255),
    (252, 228, 220, 204, 228, 255),
    (255, 210, 253, 188, 198, 255),
    (207, 198, 205, 204, 134, 255),
    (255, 214, 205, 177, 210, 255),
    (203, 183, 193, 243, 243, 246),
    (255, 226, 204, 210, 223, 225),
    (252, 228, 220, 204, 204, 255),
    (243, 251, 241, 179, 219, 255),
    (207, 239, 199, 207, 221, 226),
    (253, 236, 216, 244, 204, 255),
    (246, 243, 243, 243, 219, 255),
    (255, 217, 196, 200, 204, 255),
    (255, 228, 221, 204, 204, 255),
    (255, 226, 204, 204, 226, 255),
    (255, 228, 220, 204, 228, 252),
    (255, 198, 205, 204, 198, 207),
    (255, 201, 244, 252, 252, 255),
    (255, 210, 248, 203, 225, 255),
    (243, 209, 243, 179, 219, 255),
    (255, 204, 204, 205, 130, 255),
    (255, 204, 221, 230, 243, 255),
    (255, 204, 200, 209, 217, 255),
    (255, 204, 230, 230, 204, 255),
    (255, 220, 205, 210, 207, 225),
    (255, 192, 219, 249, 192, 255),
    (211, 243, 249, 243, 211, 255),
    (243, 243, 247, 243, 243, 255),
    (241, 243, 219, 243, 241, 255),
    (191, 114, 141, 254, 255, 255),
    (102, 153, 102, 153, 102, 153),
)


def blit(pixel, x, y, char, palette):
    char = ord(char) - 0x20
    if not 0 <= char <= 127:
        char = 127
    for row, byte in enumerate(DATA[char]):
        for col in range(4):
            pixel(x + col, y + row, palette[(byte >> (col * 2)) & 0x03])

def blit2(pixel, x, y, char):
    blit(pixel, x, y, char, (0, 0, 1, 1))

def blit4(pixel, x, y, char):
    blit(pixel, x, y, char, (0, 1, 2, 3))
Which, as you can see, is very inoptimal from the point of view of storage, but it was just a quick test of the display, so I didn't care much.
At some point I would probably save this as one large string, something like this:

Code: Select all

DATA = '\xff\xff\xff\xff\xff\xff\xf3\xf3\xf7\xfb\xf3\xff\xcc'
'\xdd\xee\xff\xff\xff\xdd\x80\xdd\x80\xdd\xff\xf7\x81'
'\xe4\xc6\xd0\xf7\xcc\xdb\xf3\xf9\xcc\xff\xf6\xcdc\xdcf\xff'
'\xf3\xf7\xfe\xff\xff\xff\xf6\xfd\xfc\xfd\xf6\xff\xe7\xdf'
'\xcf\xdf\xe7\xff\xff\xd9\xe2\xd9\xff\xff\xff\xf3\xc0\xf3\xff'
'\xff\xff\xff\xff\xf3\xf7\xfe\xff\xff\x80\xff\xff\xff\xff'
'\xff\xff\xff\xf3\xff\xcf\xdb\xf3\xf9\xfc\xff'
and saved as compiled bytecode.

Post Reply