一、oled屏幕 分辨率: 128 * 64 尺寸:0.96英寸 驱动芯片:ssd1306 电压:3.3~5V 功耗:0.06w 寿命:16000h 显示:双色(黄、蓝) 字库:无内置字库(软件控制显示) 通信: 4线spi/i2c(默认spi,使用i2c需调整相应屏后电阻)
# 针脚 GND ------- 接地 VCC ------- 3.3 ~ 5V D0 ------- 时钟线 D1 ------- 数据线 RES ------- 复位(低电平有效) DC ------- spi 通信 data/command 选择;i2c通信时设置i2c地址 cs ------- 片选(spi通信,低电平有效)
SPI通信 SPI(Serial Peripheral Interface) 串行外设接口。 以主从方式进行工作,一台master主机可以连接多台slave从机,通过CS片选可以指定选中的从机。通常为4线或3线(单向传输数据)
# 4wire SPI SCLK ------ 时钟 MOSI ------ Master Out Slave Input MISO ------ Maset Input Slave Out CS ------ 片选
二、连接raspberrypi 1.启用spi
找到advanced options 下的spi,选择启用
2.连接
三、点亮 在github上查找相关ssd1306的相关js驱动,找到一个可用的驱动,为防止丢失,已Fork https://github.com/yancoding/ssd1306-spi
index.js
const rpio = require ("rpio" );class SSD1306 { constructor ({ width, height, resetPin, dcPin, spiChip, rpio } ) { this .EXTERNAL_VCC = 0x1 this .SWITCH_CAP_VCC = 0x2 this .SET_LOW_COLUMN = 0x00 this .SET_HIGH_COLUMN = 0x10 this .SET_MEMORY_MODE = 0x20 this .SET_COL_ADDRESS = 0x21 this .SET_PAGE_ADDRESS = 0x22 this .RIGHT_HORIZ_SCROLL = 0x26 this .LEFT_HORIZ_SCROLL = 0x27 this .VERT_AND_RIGHT_HORIZ_SCROLL = 0x29 this .VERT_AND_LEFT_HORIZ_SCROLL = 0x2A this .DEACTIVATE_SCROLL = 0x2E this .ACTIVATE_SCROLL = 0x2F this .SET_START_LINE = 0x40 this .SET_CONTRAST = 0x81 this .CHARGE_PUMP = 0x8D this .SEG_REMAP = 0xA0 this .SET_VERT_SCROLL_AREA = 0xA3 this .DISPLAY_ALL_ON_RESUME = 0xA4 this .DISPLAY_ALL_ON = 0xA5 this .NORMAL_DISPLAY = 0xA6 this .INVERT_DISPLAY = 0xA7 this .DISPLAY_OFF = 0xAE this .DISPLAY_ON = 0xAF this .COM_SCAN_INC = 0xC0 this .COM_SCAN_DEC = 0xC8 this .SET_DISPLAY_OFFSET = 0xD3 this .SET_COM_PINS = 0xDA this .SET_VCOM_DETECT = 0xDB this .SET_DISPLAY_CLOCK_DIV = 0xD5 this .SET_PRECHARGE = 0xD9 this .SET_MULTIPLEX = 0xA8 this .MEMORY_MODE_HORIZ = 0x00 this .MEMORY_MODE_VERT = 0x01 this .MEMORY_MODE_PAGE = 0x02 let options = { gpiomem : false , mapping : "gpio" , ...rpio }; this ._screenWidth = width; this ._screenHeight = height; this ._resetPin = resetPin; this ._dcPin = dcPin; this ._spiChip = spiChip; this ._rpioOptions = options; this ._screenBuffer = Buffer .alloc ((width * height) / 8 ).fill (0x00 ); } init ( ) { rpio.init (this ._rpioOptions ); rpio.spiBegin (); rpio.spiChipSelect (this ._spiChip ); rpio.spiSetClockDivider (128 ); rpio.open (this ._resetPin , rpio.OUTPUT , rpio.HIGH ); rpio.open (this ._dcPin , rpio.OUTPUT , rpio.LOW ); rpio.msleep (0.01 ); this .reset (); this .command (Buffer .from ([this .DISPLAY_OFF ])); this .command (Buffer .from ([this .SET_DISPLAY_CLOCK_DIV , 0x80 ])); if (this ._screenHeight === 64 ) { this .command (Buffer .from ([this .SET_MULTIPLEX , 0x3F ])); this .command (Buffer .from ([this .SET_COM_PINS , 0x12 ])); } else { this .command (Buffer .from ([this .SET_MULTIPLEX , 0x1F ])); this .command (Buffer .from ([this .SET_COM_PINS , 0x02 ])); } this .command (Buffer .from ([this .SET_DISPLAY_OFFSET , 0x00 ])); this .command (Buffer .from ([this .SET_START_LINE | 0x00 ])); this .command (Buffer .from ([this .CHARGE_PUMP , 0x14 ])); this .command (Buffer .from ([this .SET_MEMORY_MODE , 0x00 ])); this .command (Buffer .from ([this .SEG_REMAP | 0x01 ])); this .command (Buffer .from ([this .COM_SCAN_DEC ])); this .command (Buffer .from ([this .SET_CONTRAST , 0x8f ])); this .command (Buffer .from ([this .SET_PRECHARGE , 0xf1 ])); this .command (Buffer .from ([this .SET_VCOM_DETECT , 0x40 ])); this .command (Buffer .from ([this .DISPLAY_ALL_ON_RESUME ])); this .command (Buffer .from ([this .NORMAL_DISPLAY ])); this .command (Buffer .from ([this .DISPLAY_ON ])); } clearDisplay ( ) { this ._screenBuffer .fill (0 ); } invertDisplay ( ) { this .command (Buffer .from ([this .INVERT_DISPLAY ])); } normalDisplay ( ) { this .command (Buffer .from ([this .NORMAL_DISPLAY ])); } draw ( ) { this .command (Buffer .from ([this .SET_MEMORY_MODE , this .MEMORY_MODE_HORIZ ])); this .command (Buffer .from ([this .SET_PAGE_ADDRESS , 0x00 , 0x07 ])); this .command (Buffer .from ([this .SET_COL_ADDRESS , 0x00 , 0x7f ])); this .command (Buffer .from ([this .DISPLAY_ON ])); this .data (this ._screenBuffer ); } drawPixel (x, y, on ) { if (x < 0 || x > this ._screenWidth - 1 || y < 0 || y > this ._screenHeight - 1 ) { return ; } let page = Math .floor (y / 8 ); let offset = y % 8 ; if (on) { this ._screenBuffer [page * this ._screenWidth + x] |= (0x1 << offset); } else { this ._screenBuffer [page * this ._screenWidth + x] &= ((0x1 << offset) ^ 0xff ); } } reset ( ) { rpio.write (this ._resetPin , rpio.LOW ); rpio.msleep (10 ); rpio.write (this ._resetPin , rpio.HIGH ); } command (buffer ) { rpio.spiWrite (buffer, buffer.length ); } data (buffer ) { rpio.write (this ._dcPin , rpio.HIGH ); rpio.spiWrite (buffer, buffer.length ); rpio.write (this ._dcPin , rpio.LOW ); } end ( ) { rpio.spiEnd (); rpio.close (this ._resetPin , rpio.PIN_RESET ); rpio.close (this ._dcPin , rpio.PIN_RESET ); } } module .exports = SSD1306 ;
使用该驱动
const SSD1306 = require ('ssd1306' )const rpio = require ("rpio" )const ssd1306 = new SSD1306 ({ width : 128 , height : 64 , resetPin : 24 , dcPin : 23 , spiChip : 0 , rpio, }) ssd1306.init () for (let i = 1 ; i < 128 ; i++) { for (let j = 1 ; j < 64 ; j++) { ssd1306.drawPixel (i, j, true ) ssd1306.draw () } }
如果可以依次点亮屏幕上的每一点,说明oled一切正常,可以尝试做些其他的事情了
四、一个像素点是如何点亮的? 其实oled与大多数的点阵屏幕点亮原理都很类似,具体还要看下使用手册
oled 水平共有64个共阴极,那么如果在某一像素点上加上高电平,即可点亮该点
128 * 64 共有64行,128列,64行在数据层分为8页,在第一页,也就是128 * 8的区域,如果在第一列写入的数据为0xff,即二进制的 11111111
,则会点亮第一页第一列的8个像素点,上为高位D0,下为低位D7。
所以屏幕的显存为128 * 8 byte,通过ssd1306的指令写入显存,即可显示我们要展示的内容
传统做法是对常用的字符生成不通大小的点阵数据,也就是所谓的字库,生成方式称为字体取模,这种方式有一定的局限性,我们可以考虑采用另一种方式来显示我们想要的任何图形
五、借助canvas绘图 oled屏幕的尺寸是固定的128 * 64尺寸,这点非常类似canvas的画布,我们因此可以将屏幕当成canvas的画布
首先创建一个屏幕大小的画布, 然后借助canvas的api可以绘制出我们想要的图案,再取出画布上的像素数据,转换为相应的数据格式,写入oled的显存,即可在屏幕上展示画布上的图案
注意这里由于屏幕每个点只有两个状态,亮或暗,因此我们在画布上绘制时,只使用黑色绘制即可
const rpio = require ("rpio" );const { createCanvas, loadImage } = require ('canvas' ) class SSD1306 { constructor ({ width, height, resetPin, dcPin, spiChip, rpio } ) { this .EXTERNAL_VCC = 0x1 this .SWITCH_CAP_VCC = 0x2 this .SET_LOW_COLUMN = 0x00 this .SET_HIGH_COLUMN = 0x10 this .SET_MEMORY_MODE = 0x20 this .SET_COL_ADDRESS = 0x21 this .SET_PAGE_ADDRESS = 0x22 this .RIGHT_HORIZ_SCROLL = 0x26 this .LEFT_HORIZ_SCROLL = 0x27 this .VERT_AND_RIGHT_HORIZ_SCROLL = 0x29 this .VERT_AND_LEFT_HORIZ_SCROLL = 0x2A this .DEACTIVATE_SCROLL = 0x2E this .ACTIVATE_SCROLL = 0x2F this .SET_START_LINE = 0x40 this .SET_CONTRAST = 0x81 this .CHARGE_PUMP = 0x8D this .SEG_REMAP = 0xA0 this .SET_VERT_SCROLL_AREA = 0xA3 this .DISPLAY_ALL_ON_RESUME = 0xA4 this .DISPLAY_ALL_ON = 0xA5 this .NORMAL_DISPLAY = 0xA6 this .INVERT_DISPLAY = 0xA7 this .DISPLAY_OFF = 0xAE this .DISPLAY_ON = 0xAF this .COM_SCAN_INC = 0xC0 this .COM_SCAN_DEC = 0xC8 this .SET_DISPLAY_OFFSET = 0xD3 this .SET_COM_PINS = 0xDA this .SET_VCOM_DETECT = 0xDB this .SET_DISPLAY_CLOCK_DIV = 0xD5 this .SET_PRECHARGE = 0xD9 this .SET_MULTIPLEX = 0xA8 this .MEMORY_MODE_HORIZ = 0x00 this .MEMORY_MODE_VERT = 0x01 this .MEMORY_MODE_PAGE = 0x02 let options = { gpiomem : false , mapping : "gpio" , ...rpio }; this ._screenWidth = width; this ._screenHeight = height; this ._resetPin = resetPin; this ._dcPin = dcPin; this ._spiChip = spiChip; this ._rpioOptions = options; this ._screenBuffer = Buffer .alloc ((width * height) / 8 ).fill (0x00 ); this ._ctx = createCanvas (128 , 64 ).getContext ('2d' ) } init ( ) { this ._ctx .fillStyle = 'rgba(255, 255, 255, 1)' this ._ctx .fillRect (0 , 0 , 128 , 64 ) rpio.init (this ._rpioOptions ); rpio.spiBegin (); rpio.spiChipSelect (this ._spiChip ); rpio.spiSetClockDivider (128 ); rpio.open (this ._resetPin , rpio.OUTPUT , rpio.HIGH ); rpio.open (this ._dcPin , rpio.OUTPUT , rpio.LOW ); rpio.msleep (0.01 ); this .reset (); this .command (Buffer .from ([this .DISPLAY_OFF ])); this .command (Buffer .from ([this .SET_DISPLAY_CLOCK_DIV , 0x80 ])); if (this ._screenHeight === 64 ) { this .command (Buffer .from ([this .SET_MULTIPLEX , 0x3F ])); this .command (Buffer .from ([this .SET_COM_PINS , 0x12 ])); } else { this .command (Buffer .from ([this .SET_MULTIPLEX , 0x1F ])); this .command (Buffer .from ([this .SET_COM_PINS , 0x02 ])); } this .command (Buffer .from ([this .SET_DISPLAY_OFFSET , 0x00 ])); this .command (Buffer .from ([this .SET_START_LINE | 0x00 ])); this .command (Buffer .from ([this .CHARGE_PUMP , 0x14 ])); this .command (Buffer .from ([this .SET_MEMORY_MODE , 0x00 ])); this .command (Buffer .from ([this .SEG_REMAP | 0x01 ])); this .command (Buffer .from ([this .COM_SCAN_DEC ])); this .command (Buffer .from ([this .SET_CONTRAST , 0x8f ])); this .command (Buffer .from ([this .SET_PRECHARGE , 0xf1 ])); this .command (Buffer .from ([this .SET_VCOM_DETECT , 0x40 ])); this .command (Buffer .from ([this .DISPLAY_ALL_ON_RESUME ])); this .command (Buffer .from ([this .NORMAL_DISPLAY ])); this .command (Buffer .from ([this .DISPLAY_ON ])); } getContext (contextID ) { if (contextID === '2d' ) { return this ._ctx } else { throw new Error ('getConText(contextID)参数错误' ) } } clearDisplay ( ) { this ._screenBuffer .fill (0 ); } invertDisplay ( ) { this .command (Buffer .from ([this .INVERT_DISPLAY ])); } normalDisplay ( ) { this .command (Buffer .from ([this .NORMAL_DISPLAY ])); } draw ( ) { this .command (Buffer .from ([this .SET_MEMORY_MODE , this .MEMORY_MODE_HORIZ ])); this .command (Buffer .from ([this .SET_PAGE_ADDRESS , 0x00 , 0x07 ])); this .command (Buffer .from ([this .SET_COL_ADDRESS , 0x00 , 0x7f ])); this .command (Buffer .from ([this .DISPLAY_ON ])); this .data (this ._screenBuffer ); } drawPixel (x, y, on ) { if (x < 0 || x > this ._screenWidth - 1 || y < 0 || y > this ._screenHeight - 1 ) { return ; } let page = Math .floor (y / 8 ); let offset = y % 8 ; if (on) { this ._screenBuffer [page * this ._screenWidth + x] |= (0x1 << offset); } else { this ._screenBuffer [page * this ._screenWidth + x] &= ((0x1 << offset) ^ 0xff ); } } reset ( ) { rpio.write (this ._resetPin , rpio.LOW ); rpio.msleep (10 ); rpio.write (this ._resetPin , rpio.HIGH ); } command (buffer ) { rpio.spiWrite (buffer, buffer.length ); } data (buffer ) { rpio.write (this ._dcPin , rpio.HIGH ); rpio.spiWrite (buffer, buffer.length ); rpio.write (this ._dcPin , rpio.LOW ); } end ( ) { rpio.spiEnd (); rpio.close (this ._resetPin , rpio.PIN_RESET ); rpio.close (this ._dcPin , rpio.PIN_RESET ); } scroll ( ) { this .command (Buffer .from ([this .RIGHT_HORIZ_SCROLL ])); this .command (Buffer .from ([this .ACTIVATE_SCROLL ])); this .data (this ._screenBuffer ); } drawToDisplay ( ) { let pixelArray = [] for (let page = 0 ; page < 8 ; page++) { for (let col = 0 ; col < 128 ; col++) { const imageData = this ._ctx .getImageData (col, page * 8 , 1 , 8 ) let byte = 0x00 for (let i = 0 , j = 0 ; i < imageData.data .length ; i += 4 , j++) { const r = imageData.data [i] const g = imageData.data [i + 1 ] const b = imageData.data [i + 2 ] const a = imageData.data [i + 3 ] const average = 0.21 * r + 0.72 * g + 0.07 * b const bit = average < 128 ? 1 : 0 byte += bit << j } pixelArray.push (byte) } } this ._screenBuffer = Buffer .from (pixelArray) this .draw () } } module .exports = SSD1306 ;
尝试绘制文字
// 引入
const SSD1306 = require('ssd1306')
const rpio = require("rpio")
// 创建实例
const ssd1306 = new SSD1306({
width: 128,
height: 64,
resetPin: 24,
dcPin: 23,
spiChip: 0,
rpio,
})
// 初始化
ssd1306.init()
// 获取canvas 2d 上下文
const ctx = ssd1306.getContext('2d')
ctx.strokeStyle = '#000'
ctx.strokeRect(0, 0, 128, 64)
ctx.font = '18px arial'
ctx.fillStyle = 'rgba(0, 0, 0, 1)'
ctx.textAlign = 'center'
ctx.fillText(`Hello World`, 64, 32)
ssd1306.drawToDisplay()