diff --git a/examples/drivers/displays/ssd1306-io/main.js b/examples/drivers/displays/ssd1306-io/main.js new file mode 100644 index 0000000000..ac3d70a31d --- /dev/null +++ b/examples/drivers/displays/ssd1306-io/main.js @@ -0,0 +1,31 @@ +// ssd1306-io exammple +// Copright © 2023 by Thorsten von Eicken +// +// run using a commandline like mcconfig -d -m -p esp32/nodemcu -f gray256 + +import Poco from "commodetto/Poco" +import SSD1306 from "embedded:display/ssd1306" +import parseBMF from "commodetto/parseBMF" +import Resource from "Resource" + +//const font = parseBMF(new Resource("OpenSans-Regular-24.bf4")) +const font = parseBMF(new Resource("OpenSans-Semibold-28.bf4")) + +const screen = new SSD1306({ + io: device.io.I2C, + clock: 10, + data: 8, + height: 32, +}) + +let poco = new Poco(screen) +const white = poco.makeColor(255, 255, 255) +const black = poco.makeColor(0, 0, 0) + +poco.begin() +poco.fillRectangle(white, 0, 0, poco.width, poco.height) +poco.fillRectangle(black, 1, 1, poco.width - 2, poco.height - 2) +poco.fillRectangle(white, poco.height / 2, poco.height / 2, poco.height / 2, poco.height / 2) +//poco.fillRectangle(poco.makeColor(gray, gray, gray), 4, 4, poco.width - 8, poco.height - 8) +poco.drawText("220W 35%", font, white, 0, -7) +poco.end() diff --git a/examples/drivers/displays/ssd1306-io/manifest.json b/examples/drivers/displays/ssd1306-io/manifest.json new file mode 100644 index 0000000000..81bf7d8f4c --- /dev/null +++ b/examples/drivers/displays/ssd1306-io/manifest.json @@ -0,0 +1,18 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json", + "$(MODDABLE)/examples/manifest_commodetto.json", + "$(MODDABLE)/modules/io/manifest.json", + "$(MODDABLE)/modules/data/text/decoder/manifest.json", + "$(MODDABLE)/modules/drivers/displays/ssd1306-io/manifest.json" + ], + "modules": { + "*": "./main" + }, + "resources": { + "*-mask": [ + "$(MODDABLE)/examples/assets/fonts/OpenSans-Regular-24", + "$(MODDABLE)/examples/assets/fonts/OpenSans-Semibold-28" + ] + } +} diff --git a/modules/drivers/displays/ssd1306-io/README.md b/modules/drivers/displays/ssd1306-io/README.md new file mode 100644 index 0000000000..4adca1fd69 --- /dev/null +++ b/modules/drivers/displays/ssd1306-io/README.md @@ -0,0 +1,4 @@ +SSD1306 driver using ECMA-419 +============================= + +This driver is for the SSD1306 OLED display. It is intended to conform to ECMA-419. diff --git a/modules/drivers/displays/ssd1306-io/manifest.json b/modules/drivers/displays/ssd1306-io/manifest.json new file mode 100644 index 0000000000..f9fa42b974 --- /dev/null +++ b/modules/drivers/displays/ssd1306-io/manifest.json @@ -0,0 +1,8 @@ +{ + "include": ["$(MODDABLE)/examples/manifest_typings.json"], + "modules": { + "embedded:display/ssd1306": "./ssd1306", + "commodetto/Bitmap": "$(MODDABLE)/modules/commodetto/commodettoBitmap" + }, + "preload": ["embedded:display/ssd1306", "commodetto/Bitmap"] +} diff --git a/modules/drivers/displays/ssd1306-io/ssd1306.ts b/modules/drivers/displays/ssd1306-io/ssd1306.ts new file mode 100644 index 0000000000..37a61f75b6 --- /dev/null +++ b/modules/drivers/displays/ssd1306-io/ssd1306.ts @@ -0,0 +1,214 @@ +// ssd1306-io - driver for a SSD1306 display using embedded:io +// Copright © 2023 by Thorsten von Eicken +// Based on a driver by Moddable Tech Inc. (had no license, but presumably LGPL) + +import Bitmap from "commodetto/Bitmap" +import I2C, { Options as I2COptions } from "embedded:io/i2c" + +const SSD1306_SETCONTRAST = 0x81 +const SSD1306_DISPLAYALLON_RESUME = 0xa4 +const SSD1306_NORMALDISPLAY = 0xa6 +const SSD1306_DISPLAYOFF = 0xae +const SSD1306_DISPLAYON = 0xaf +const SSD1306_SETDISPLAYOFFSET = 0xd3 +const SSD1306_SETCOMPINS = 0xda +const SSD1306_SETVCOMDETECT = 0xdb +const SSD1306_SETDISPLAYCLOCKDIV = 0xd5 +const SSD1306_SETPRECHARGE = 0xd9 +const SSD1306_SETMULTIPLEX = 0xa8 +const SSD1306_SETSTARTLINE = 0x40 +const SSD1306_MEMORYMODE = 0x20 +const SSD1306_COLUMNADDR = 0x21 +const SSD1306_PAGEADDR = 0x22 +const SSD1306_COMSCANDEC = 0xc8 +const SSD1306_SEGREMAP = 0xa0 +const SSD1306_CHARGEPUMP = 0x8d +const SSD1306_EXTERNALVCC = 0x1 +const SSD1306_SWITCHCAPVCC = 0x2 +const SSD1306_DEACTIVATE_SCROLL = 0x2e +const vccstate: number = SSD1306_SWITCHCAPVCC + +const kBufferSlop = 1 + +function doCmd(ssd: I2C, cmd: number) { + ssd.write(new Uint8Array([0, cmd]).buffer) +} + +export type Options = Omit & { + // we default hz&address + io: typeof I2C + hz?: number + address?: number + height?: number + width?: number +} + +export default class SSD1306 { + ssd1306: I2C + pixel = 0 + out?: Uint8Array + #height = 64 + #width = 128 + + constructor(options: Options) { + let ssd = (this.ssd1306 = new options.io({ + hz: 1000000, + address: 0x3c, + ...options, + })) + + if (options.height) this.#height = options.height + if (options.width) this.#width = options.width + + let repeat = 3 + while (repeat--) { + try { + doCmd(ssd, SSD1306_DISPLAYOFF) // 0xAE + break + } catch (e) { + trace("SSD1306 init failed:\n" + e + "\n") + if (!repeat) throw e + } + } + doCmd(ssd, SSD1306_SETDISPLAYCLOCKDIV) // 0xD5 + doCmd(ssd, 0x80) // the suggested ratio 0x80 + + doCmd(ssd, SSD1306_SETMULTIPLEX) // 0xA8 + doCmd(ssd, this.#height - 1) + + doCmd(ssd, SSD1306_SETDISPLAYOFFSET) // 0xD3 + doCmd(ssd, 0x0) // no offset + doCmd(ssd, SSD1306_SETSTARTLINE | 0x0) // line #0 + doCmd(ssd, SSD1306_CHARGEPUMP) // 0x8D + if (vccstate == SSD1306_EXTERNALVCC) { + doCmd(ssd, 0x10) + } else { + doCmd(ssd, 0x14) + } + doCmd(ssd, SSD1306_MEMORYMODE) // 0x20 + doCmd(ssd, 0x00) // 0x0 act like ks0108 + doCmd(ssd, SSD1306_SEGREMAP | 0x1) + doCmd(ssd, SSD1306_COMSCANDEC) + + if (this.#width == 128 && this.#height == 32) { + doCmd(ssd, SSD1306_SETCOMPINS) // 0xDA + doCmd(ssd, 0x02) + doCmd(ssd, SSD1306_SETCONTRAST) // 0x81 + doCmd(ssd, 0x8f) + } else if (this.#width == 128 && this.#height == 64) { + doCmd(ssd, SSD1306_SETCOMPINS) // 0xDA + doCmd(ssd, 0x12) + doCmd(ssd, SSD1306_SETCONTRAST) // 0x81 + doCmd(ssd, 0xcf) + } else { + throw Error("height/width not supported") + return + } + + doCmd(ssd, SSD1306_SETPRECHARGE) // 0xd9 + if (vccstate == SSD1306_EXTERNALVCC) { + doCmd(ssd, 0x22) + } else { + doCmd(ssd, 0xf1) + } + doCmd(ssd, SSD1306_SETVCOMDETECT) // 0xDB + doCmd(ssd, 0x40) + doCmd(ssd, SSD1306_DISPLAYALLON_RESUME) // 0xA4 + doCmd(ssd, SSD1306_NORMALDISPLAY) // 0xA6 + + doCmd(ssd, SSD1306_DEACTIVATE_SCROLL) + + doCmd(ssd, SSD1306_DISPLAYON) //--turn on oled panel + } + begin(x: number, y: number, width: number, height: number) { + if (0 != x || 0 != y || this.#width != width || this.#height != height) { + trace("partial updates unsupported\n") + return + } else { + let ssd = this.ssd1306 + + doCmd(ssd, SSD1306_COLUMNADDR) + doCmd(ssd, 0) // Column start address (0 = reset) + doCmd(ssd, this.#width - 1) // Column end address (127 = reset) + + doCmd(ssd, SSD1306_PAGEADDR) + doCmd(ssd, 0) // Page start address (0 = reset) + + doCmd(ssd, (this.#height >> 3) - 1) // Page end address + + this.pixel = 1 + this.out = new Uint8Array(this.#width + kBufferSlop) + this.out[kBufferSlop - 1] = 0x40 + this.out.fill(0, kBufferSlop) + } + } + send(data: ArrayBuffer, offset: number, count: number) { + if (undefined !== offset && offset > 0) { + data = data.slice(offset) + if (undefined === count) count = data.byteLength + else count -= offset + } else { + offset = 0 + count = data.byteLength + } + let pixels = new Uint8Array(data) + + let ssd = this.ssd1306 + if (count < 0) count = -count + let pixel = this.pixel + + let out = this.out + if (!out) throw new Error("begin not called") + + let off = 0 + let width = this.#width + while (count > 0) { + let i = width + while (i--) { + if (pixels[off + i] & 128) out[kBufferSlop + i] |= pixel + } + off += width + + pixel <<= 1 + if (256 === pixel) { + // flush this set of 8 lines + ssd.write(out.buffer) + // start new group of 8 rows + out.fill(0, kBufferSlop) + pixel = 1 + } + + count -= width + } + this.pixel = pixel + } + end() {} + continue() {} + pixelsToBytes(count: number) { + return count + } + get pixelFormat() { + return Bitmap.Gray256 + } + get width() { + return this.#width + } + get height() { + return this.#height + } + get c_dispatch() { + return undefined + } + adaptInvalid(r: { x: number; y: number; width: number; height: number }) { + r.x = 0 + r.y = 0 + r.width = this.width + r.height = this.height + } + get async() { + return false + } + get clut() { + return undefined + } +} diff --git a/typings/embedded_io/i2c.d.ts b/typings/embedded_io/i2c.d.ts index 8621d5b3be..7806825898 100644 --- a/typings/embedded_io/i2c.d.ts +++ b/typings/embedded_io/i2c.d.ts @@ -21,16 +21,19 @@ declare module "embedded:io/i2c" { import { Buffer, PinSpecifier, PortSpecifier } from "embedded:io/_common"; + + export interface Options { + data: PinSpecifier; + clock: PinSpecifier; + hz: number; + address: number; + port?: PortSpecifier; + format?: "buffer"; + target?: any; + } + class I2C { - constructor(options: { - data: PinSpecifier; - clock: PinSpecifier; - hz: number; - address: number; - port?: PortSpecifier; - format?: "buffer"; - target?: any; - }); + constructor(options: Options); readonly resolution: number; write(value: Buffer, stop?: boolean): void; diff --git a/typings/embedded_io/spi.d.ts b/typings/embedded_io/spi.d.ts index f3a28adbe0..2989a580c6 100644 --- a/typings/embedded_io/spi.d.ts +++ b/typings/embedded_io/spi.d.ts @@ -20,19 +20,20 @@ declare module "embedded:io/spi" { import { Buffer, PinSpecifier, PortSpecifier } from "embedded:io/_common"; + export interface Options { + out?: PinSpecifier; + in?: PinSpecifier; + clock: PinSpecifier; + select?: PinSpecifier; + active?: 1 | 0; + hz: number; + mode?: number; + port?: PortSpecifier; + format?: "buffer"; + target?: any; + } class SPI { - constructor(options: { - out?: PinSpecifier; - in?: PinSpecifier; - clock: PinSpecifier; - select?: PinSpecifier; - active?: 1 | 0; - hz: number; - mode?: number; - port?: PortSpecifier; - format?: "buffer"; - target?: any; - }) + constructor(options: Options); read(byteLength: number): ArrayBuffer; read(buffer: Buffer): void; write(buffer: Buffer): void;