Pages

10 déc. 2018

OLED - Micropython

Je me suis mis en tête de faire fonctionner un  petit écran oled que j'avais déjà testé sur arduino, sur la carte micropython en SPI, histoire de faire fonctionner un premier hardware sur cette carte.



J'ai pas mal cherché sur le web s'il y avait déjà eu des expériences du genre, mon but étant d'afficher du graphisme et pas seulement du texte.

J'ai quand même trouvé un super tutoriel de chez adafruit de Tonny Dicola qui se rapproche très fort de ce que je veux faire, celui ci fait référence au ESP8266, du coup un bon point pour la librairie en python, mais il existe des différences entre la pyboard 1.1 et l'ESP8266. Il va falloir vérifier quels sont les connectiques à faire entre la carte pyb et l'écran oled. L'écran oled que j'utilises étant différent de celui proposé dans le tutoriel.

Voici le matériel dont je dispose:




Ecran OLED I2C/SPI, 128x64 pixel
série 12864, 0.96 pouces, 
module d'affichage SSD1306
Py Board v1.0, Micropython


Analyse de l'écran OLED:
Il existe plusieurs sortes d'écrans OLEDS avec différents annotations sur les pinouts. Cette version-ci est SPI et I2C. Sur les versions I2C seules, on trouvera 4 pinouts (VCC,GND,SDL,SDA). Les versions SPI ont d'autres sorties, parfois nommées différemment. Voici un tableau comparatifs des différentes nomenclatures des sorties SPI sur ce genre d'écran:


pin OLED > GND VCC D0 D1 RES DC CS
SPI masse 3v3, 5v CLK (clock) MOSI Reset Data Command Cable Select
I2C masse 3v3, 5v SCL SDA - - -

Analyse de la connectique SPI de la carte micropython pyb v 1.1:


On peut observer que cette carte a plusieurs ports SPI disponibles. 


/SS (CS)
SCK
MISO
MOSI
SPI (1)
X5
X6
X7
X8
(cpu name)
A4
A5
A6
A7
SPI (2)
Y5
Y6
Y7
Y8
(cpu name)
B12
B13
B14
B15

Il existe plusieurs notations pour le port SPI (exemple de notation sur le wiki).
On notera ici que l'écran OLED ne reprends pas les notations standard.

J'ai opté pour mon test du port SPI (2), (cela aura de l'importance dans le code lors de l'initialisation du port SPI) ce qui donne ceci pour la connectique:



pin OLED >GNDVCCD0D1RESDCCS
noms SPI
CLK (clock)MOSIResetData CommandCable Select
pin PyBoard >GND3v3Y6 (SCK)Y8Y1Y2Y3 (/SS)

Ce qui est curieux, c'est qu'il semblerait que le D1 ne soit pas le DI (data in), mais plutot le MOSI
Idem pour le CS, Y3 alors que le SS est en Y5 sur la carte, ( software ? à vérifier ).

Code:
J'ai repris deux librairies que j'ai un peu adapté et compilée en une seule, les code originaux de la librarie SSD1306.py et gfx.py de chez adafruit.

Les modifs consistent en la suppression de la partie I2C du code SSD1306. Pour la partie GFX, je n'ai gardé que tout ce qui concerne le remplissage de forme, et j'ai ajouté la notion de polygones à 4 cotés pour mon application.

Le premier code est le main.py a copier dans la pyboard.

On note que dans le code lors de la définition du SPI, j'ai opté pour le Y3 pour le Chip Select(CS) et le Y2 pour le data command (DC).

Le second est la lib oled.
# this is main.py file for test library and conenct to spi on pyboard
# init SPI & oled
import pyb
from pyb import SPI
from pyb import Pin
from pyb import delay
import oled_spi
spi = SPI('Y', mode=SPI.MASTER, baudrate=8000000, polarity=0, phase=0)
display = oled_spi.OLED_SPI(128, 64, spi, dc=Pin('Y2'), res=Pin('Y1'), cs=Pin('Y3'))
dirX = 1
youpiX = 15
dirY = 1
x = 64
y = 32
while True:
display.framebuf.fill(0)
x = x + dirX
y = y + dirY
x0 = x - 10
x1 = x + 10
y0 = y - 10
y1 = y + 10
if x0 < 0 or x1 > 128:
dirX = -dirX
#display.invert()
if y0 < 0 or y1 > 64:
dirY = -dirY
if x+15 > 64:
youpiX = -55
elif x-55 < 0:
youpiX = 15
display.quad(x0, y0, x1, y0, x0, y1, x1, y1, 1)
display.text("youpi",x+youpiX,y)
pyb.delay(10)
display.show()
view raw main.py hosted with ❤ by GitHub
# this library is come from adafruit library, adapted to my use.
# it is only spi on this version, from SSD1306.py
# gfx.py from adafruit if partially implemented here
# It is added quad filled polygons on this lib, adn remoce contour version of primitives
# MicroPython SSD1306 OLED driver, SPI interfaces
import time
import framebuf
from pyb import delay
# 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)
class OLED_SPI:
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
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
self.buffer = bytearray((height // 8) * width)
self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
self.poweron()
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00, # off
# address setting
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.height == 32 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.framebuf.fill(0)
self.show()
def write_cmd(self, cmd):
self.cs.high()
self.dc.low()
self.cs.low()
self.spi.send(bytearray([cmd]))
self.cs.high()
def write_framebuf(self):
self.cs.high()
self.dc.high()
self.cs.low()
self.spi.send(self.buffer)
self.cs.high()
def poweron(self):
self.res.high()
time.sleep_ms(1)
self.res.low()
time.sleep_ms(10)
self.res.high()
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
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_framebuf()
def line(self, x0, y0, x1, y1, col):
steep = abs(y1 - y0) > abs(x1 - x0)
if steep:
x0, y0 = y0, x0
x1, y1 = y1, x1
if x0 > x1:
x0, x1 = x1, x0
y0, y1 = y1, y0
dx = x1 - x0
dy = abs(y1 - y0)
err = dx // 2
ystep = 0
if y0 < y1:
ystep = 1
else:
ystep = -1
while x0 <= x1:
if steep:
self.framebuf.pixel(y0, x0, col)
else:
self.framebuf.pixel(x0, y0, col)
err -= dy
if err < 0:
y0 += ystep
err += dx
x0 += 1
def triangle_Flat(self, x0, y0, x1, y1, x2, y2, col):
'''flat triangle, Top parameter to define, TopFlat, else BotFlat'''
xT, xL, xR, yT, yB = x0, x1, x2, y0, y1
# define top, sides
if y0 == y1:
xT, xL, xR, yT, yB = x2, x0, x1, y2, y0
elif y0 == y2:
xT, xL, xR, yT, yB = x1, x0, x2, y1, y0
#print ("Flat tri top-sides xT, xL, xR, yT, yB:", xT, xL, xR, yT, yB)
if xL > xR: # line begin from left to right
#print ("invert x sides")
xL,xR = xR,xL
if yB < yT: # write line from top to bottom
#print ("Top Flat case")
yT,yB = yB,yT
curxL = xL
curxR = xR
slopeL = -(xL - xT) / abs(yB - yT);
slopeR = -(xR - xT) / abs(yB - yT);
else:
#print ("Base Flat case")
curxL = xT
curxR = xT
slopeL = (xL - xT) / abs(yB - yT);
slopeR = (xR - xT) / abs(yB - yT);
#print ("xT, xL, xR, yT, yB, slopeL ,slopeR:", xT, xL, xR, yT, yB, slopeL ,slopeR)
for y in range (yT,yB):
#print("hLine curxL, curxR, y:", int(curxL), int(curxR),y)
#self.hline(int(curxL), int(curxR), y, col) # secure pour le clipping
x = int(curxL)
length = int(curxR)-x
self.framebuf.hline(x,y,length,col) # perf max
curxL = curxL + slopeL # * dir
curxR = curxR + slopeR # * dir
#delay(50)
#self.show()
def triangle(self, x0, y0, x1, y1, x2, y2, col):
# Filled triangle drawing function. Will draw a filled triangle around the points (x0, y0), (x1, y1), and (x2, y2).
# at first sort the three vertices by y-coordinate ascending so y0 is the topmost vertice
# here we know that y0 <= y1 <= y2
#print ("tri datas orig v0,v1,v2 ", x0, y0, x1, y1, x2, y2)
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
if y1 > y2:
y2, y1 = y1, y2
x2, x1 = x1, x2
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
#print ("tri datas sorts v0,v1,v2 ", x0, y0, x1, y1, x2, y2)
if y0 == y1 and y1 == y2: # check for trivial: line
#print ("Case: hline")
#print ("x0, y0, x1, y1, x2, y2:", x0, y0, x1, y1, x2, y2)
self.framebuf.hline(x0,y0,x2-x0,col)
elif y0 == y1 or y1 == y2: # check for trivial case of top/bottom-flat triangle
#print ("Case: trivial flat tri")
self.triangle_Flat(x0, y0, x1, y1, x2, y2, col)
else: # general case - split the triangle in a topflat and bottom-flat one
#print ("Case: general tri")
xCut = x0 + float(y1 - y0) / float(y2 - y0) * (x2 - x0)
#print ("tri one half")
self.triangle_Flat(x0, y0, x1, y1, xCut, y1, col)
#print ("tri second half")
self.triangle_Flat(x2, y2, x1, y1, xCut, y1, col)
def quad(self, x0, y0, x1, y1, x2, y2, x3, y3, col):
# trivial case of regular rectangle
if x0 == x2 and x1 == x3 and y0 == y1 and y2 == y3:
self.rectangle(x0, y0, x3, y3, col)
#print ("rectangle")
return
# irregular rectangle
#print ("quad tri top")
self.triangle(int(x0), int(y0), int(x1), int(y1), int(x2), int(y2), col)
#print ("quad tri bot")
self.triangle(int(x2), int(y2), int(x3), int(y3), int(x1), int(y1), col)
def rectangle(self, x0, y0, x1, y1, col):
if y0>y1:
y0,y1 = y1,y0
width = x1-x0
for y in range(y0,y1):
self.framebuf.hline(x0,y,width,col)
def scroll(self, dx, dy):
self.framebuf.scroll(dx, dy)
def text(self, string, x, y, col=1):
self.framebuf.text(string, x, y, col)
view raw oled_spi.py hosted with ❤ by GitHub





Vous devriez voir un petit carré qui se balade sur l'écran avec un mot qui le suit a droite ou a gauche.



Notez bien:

spi = SPI('Y', mode=SPI.MASTER, baudrate=8000000, polarity=0, phase=0)

Le 'Y', correspond au second port SPI, comme décrit dans la doc pour la définition du constructeur du port.





# this is main.py file for test library and conenct to spi on pyboard
# init SPI & oled
import pyb
from pyb import SPI
from pyb import Pin
from pyb import delay
import oled_spi
spi = SPI('Y', mode=SPI.MASTER, baudrate=8000000, polarity=0, phase=0)
display = oled_spi.OLED_SPI(128, 64, spi, dc=Pin('Y2'), res=Pin('Y1'), cs=Pin('Y3'))
dirX = 1
youpiX = 15
dirY = 1
x = 64
y = 32
while True:
display.framebuf.fill(0)
x = x + dirX
y = y + dirY
x0 = x - 10
x1 = x + 10
y0 = y - 10
y1 = y + 10
if x0 < 0 or x1 > 128:
dirX = -dirX
#display.invert()
if y0 < 0 or y1 > 64:
dirY = -dirY
if x+15 > 64:
youpiX = -55
elif x-55 < 0:
youpiX = 15
display.quad(x0, y0, x1, y0, x0, y1, x1, y1, 1)
display.text("youpi",x+youpiX,y)
pyb.delay(10)
display.show()
view raw main.py hosted with ❤ by GitHub
# this library is come from adafruit library, adapted to my use.
# it is only spi on this version, from SSD1306.py
# gfx.py from adafruit if partially implemented here
# It is added quad filled polygons on this lib, adn remoce contour version of primitives
# MicroPython SSD1306 OLED driver, SPI interfaces
import time
import framebuf
from pyb import delay
# 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)
class OLED_SPI:
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
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
self.buffer = bytearray((height // 8) * width)
self.framebuf = framebuf.FrameBuffer1(self.buffer, width, height)
self.poweron()
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00, # off
# address setting
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.height == 32 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.framebuf.fill(0)
self.show()
def write_cmd(self, cmd):
self.cs.high()
self.dc.low()
self.cs.low()
self.spi.send(bytearray([cmd]))
self.cs.high()
def write_framebuf(self):
self.cs.high()
self.dc.high()
self.cs.low()
self.spi.send(self.buffer)
self.cs.high()
def poweron(self):
self.res.high()
time.sleep_ms(1)
self.res.low()
time.sleep_ms(10)
self.res.high()
def poweroff(self):
self.write_cmd(SET_DISP | 0x00)
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_framebuf()
def line(self, x0, y0, x1, y1, col):
steep = abs(y1 - y0) > abs(x1 - x0)
if steep:
x0, y0 = y0, x0
x1, y1 = y1, x1
if x0 > x1:
x0, x1 = x1, x0
y0, y1 = y1, y0
dx = x1 - x0
dy = abs(y1 - y0)
err = dx // 2
ystep = 0
if y0 < y1:
ystep = 1
else:
ystep = -1
while x0 <= x1:
if steep:
self.framebuf.pixel(y0, x0, col)
else:
self.framebuf.pixel(x0, y0, col)
err -= dy
if err < 0:
y0 += ystep
err += dx
x0 += 1
def triangle_Flat(self, x0, y0, x1, y1, x2, y2, col):
'''flat triangle, Top parameter to define, TopFlat, else BotFlat'''
xT, xL, xR, yT, yB = x0, x1, x2, y0, y1
# define top, sides
if y0 == y1:
xT, xL, xR, yT, yB = x2, x0, x1, y2, y0
elif y0 == y2:
xT, xL, xR, yT, yB = x1, x0, x2, y1, y0
#print ("Flat tri top-sides xT, xL, xR, yT, yB:", xT, xL, xR, yT, yB)
if xL > xR: # line begin from left to right
#print ("invert x sides")
xL,xR = xR,xL
if yB < yT: # write line from top to bottom
#print ("Top Flat case")
yT,yB = yB,yT
curxL = xL
curxR = xR
slopeL = -(xL - xT) / abs(yB - yT);
slopeR = -(xR - xT) / abs(yB - yT);
else:
#print ("Base Flat case")
curxL = xT
curxR = xT
slopeL = (xL - xT) / abs(yB - yT);
slopeR = (xR - xT) / abs(yB - yT);
#print ("xT, xL, xR, yT, yB, slopeL ,slopeR:", xT, xL, xR, yT, yB, slopeL ,slopeR)
for y in range (yT,yB):
#print("hLine curxL, curxR, y:", int(curxL), int(curxR),y)
#self.hline(int(curxL), int(curxR), y, col) # secure pour le clipping
x = int(curxL)
length = int(curxR)-x
self.framebuf.hline(x,y,length,col) # perf max
curxL = curxL + slopeL # * dir
curxR = curxR + slopeR # * dir
#delay(50)
#self.show()
def triangle(self, x0, y0, x1, y1, x2, y2, col):
# Filled triangle drawing function. Will draw a filled triangle around the points (x0, y0), (x1, y1), and (x2, y2).
# at first sort the three vertices by y-coordinate ascending so y0 is the topmost vertice
# here we know that y0 <= y1 <= y2
#print ("tri datas orig v0,v1,v2 ", x0, y0, x1, y1, x2, y2)
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
if y1 > y2:
y2, y1 = y1, y2
x2, x1 = x1, x2
if y0 > y1:
y0, y1 = y1, y0
x0, x1 = x1, x0
#print ("tri datas sorts v0,v1,v2 ", x0, y0, x1, y1, x2, y2)
if y0 == y1 and y1 == y2: # check for trivial: line
#print ("Case: hline")
#print ("x0, y0, x1, y1, x2, y2:", x0, y0, x1, y1, x2, y2)
self.framebuf.hline(x0,y0,x2-x0,col)
elif y0 == y1 or y1 == y2: # check for trivial case of top/bottom-flat triangle
#print ("Case: trivial flat tri")
self.triangle_Flat(x0, y0, x1, y1, x2, y2, col)
else: # general case - split the triangle in a topflat and bottom-flat one
#print ("Case: general tri")
xCut = x0 + float(y1 - y0) / float(y2 - y0) * (x2 - x0)
#print ("tri one half")
self.triangle_Flat(x0, y0, x1, y1, xCut, y1, col)
#print ("tri second half")
self.triangle_Flat(x2, y2, x1, y1, xCut, y1, col)
def quad(self, x0, y0, x1, y1, x2, y2, x3, y3, col):
# trivial case of regular rectangle
if x0 == x2 and x1 == x3 and y0 == y1 and y2 == y3:
self.rectangle(x0, y0, x3, y3, col)
#print ("rectangle")
return
# irregular rectangle
#print ("quad tri top")
self.triangle(int(x0), int(y0), int(x1), int(y1), int(x2), int(y2), col)
#print ("quad tri bot")
self.triangle(int(x2), int(y2), int(x3), int(y3), int(x1), int(y1), col)
def rectangle(self, x0, y0, x1, y1, col):
if y0>y1:
y0,y1 = y1,y0
width = x1-x0
for y in range(y0,y1):
self.framebuf.hline(x0,y,width,col)
def scroll(self, dx, dy):
self.framebuf.scroll(dx, dy)
def text(self, string, x, y, col=1):
self.framebuf.text(string, x, y, col)
view raw oled_spi.py hosted with ❤ by GitHub

Aucun commentaire: