From 4986d4eafbc4c03616ee270989cf906dc446e06d Mon Sep 17 00:00:00 2001 From: vickash Date: Sat, 7 Sep 2024 11:33:03 -0400 Subject: [PATCH] Add SPI support for SSD1306 and SH1106 --- HARDWARE.md | 4 +- examples/display/sh1106.rb | 36 ++++++++--------- examples/display/ssd1306.rb | 36 ++++++++--------- lib/denko/display/ssd1306.rb | 75 ++++++++++++++++++++++++++++-------- 4 files changed, 93 insertions(+), 58 deletions(-) diff --git a/HARDWARE.md b/HARDWARE.md index 5c30dc2..e0abb05 100644 --- a/HARDWARE.md +++ b/HARDWARE.md @@ -145,8 +145,8 @@ Polling and reading follow a call and response pattern. | Name | Status | Interface | Component Class | Notes | | :--------------- | :------: | :-------- | :--------------- |------ | | HD44780 LCD | :green_heart: | Digital Out, Output Register | `Display::HD44780` | -| SSD1306 OLED | :yellow_heart: | I2C | `Display::SSD1306` | 1 font, some graphics -| SH1106 OLED | :yellow_heart: | I2C | `Display::SH1106` | Similar to SSD1306 +| SSD1306 OLED | :yellow_heart: | I2C or SPI | `Display::SSD1306` | 1 font, some graphics +| SH1106 OLED | :yellow_heart: | I2C or SPI | `Display::SH1106` | Works same as SSD1306 | ST7565R (128x64 Mono) | :heart: | SPI | `Display::ST7565R` | | ST7735S (160x128 RGB) | :heart: | SPI | `Display::ST7735S` | | ILI9341 (240x320 RGB) | :heart: | SPI | `Display::ILI9341` | diff --git a/examples/display/sh1106.rb b/examples/display/sh1106.rb index 8995e66..e661370 100644 --- a/examples/display/sh1106.rb +++ b/examples/display/sh1106.rb @@ -6,31 +6,27 @@ board = Denko::Board.new(Denko::Connection::Serial.new) +# The SH1106 OLED connects to either an I2C or SPI bus, depending on the model you have. +# Bus setup exampels in order: +# I2C Hardware +# I2C Bit-Bang +# SPI Hardware +# SPI Bit-Bang # -# Default pins for the I2C0 (first) interface on most chips: -# -# ATmega 328p: SDA = 'A4' SCL = 'A5' - Arduino Uno, Nano -# ATmega 32u4: SDA = 2 SCL = 3 - Arduino Leonardo, Pro Micro -# ATmega1280 / 2560: SDA = 20 SCL = 21 - Arduino Mega -# SAM3X8E: SDA = 20 SCL = 21 - Arduino Due -# SAMD21G18: SDA = 20 SCL = 21 - Arduino Zero, M0, M0 Pro -# ESP8266: SDA = 4 SCL = 5 -# ESP32: SDA = 21 SCL = 22 -# RP2040: SDA = 4 SCL = 5 - Raspberry Pi Pico (W) -# -# Only give the SDA pin of the I2C bus. SCL (clock) pin must be -# connected for it to work, but we don't need to control it. -# - -# Board's hardware I2C interface on predetermined pins. bus = Denko::I2C::Bus.new(board: board, pin: :SDA) -# Bit-banged I2C on any pins. -# bus = Denko::I2C::BitBang.new(board: board, pins: {scl: 8, sda: 9}) +# bus = Denko::I2C::BitBang.new(board: board, pins: {scl: 4, sda: 5}) +# bus = Denko::SPI::Bus.new(board: board) +# bus = Denko::SPI::BitBang.new(board: board, pins: {clock: 13, output: 11}) -oled = Denko::Display::SH1106.new(bus: bus, rotate: true) -canvas = oled.canvas +# I2C OLED, connected to I2C SDA and SCL only. Default I2C address of 0x3C. +oled = Denko::Display::SH1106.new(bus: bus, address: 0x3C, rotate: true) + +# SPI OLED, connected to SPI CLK and MOSI pins. +# select and dc pins must be given. reset is optional (can be pulled high instead). +# oled = Denko::Display::SH1106.new(bus: bus, pins: {select: 10, dc: 7, reset: 8}, rotate: true) # Draw some text on the OLED's canvas (a Ruby memory buffer). +canvas = oled.canvas canvas.text_cursor = [27,60] canvas.print("Hello World!") diff --git a/examples/display/ssd1306.rb b/examples/display/ssd1306.rb index 55ac4e5..f6c98c6 100644 --- a/examples/display/ssd1306.rb +++ b/examples/display/ssd1306.rb @@ -6,31 +6,27 @@ board = Denko::Board.new(Denko::Connection::Serial.new) +# The SSD1306 OLED connects to either an I2C or SPI bus, depending on the model you have. +# Bus setup exampels in order: +# I2C Hardware +# I2C Bit-Bang +# SPI Hardware +# SPI Bit-Bang # -# Default pins for the I2C0 (first) interface on most chips: -# -# ATmega 328p: SDA = 'A4' SCL = 'A5' - Arduino Uno, Nano -# ATmega 32u4: SDA = 2 SCL = 3 - Arduino Leonardo, Pro Micro -# ATmega1280 / 2560: SDA = 20 SCL = 21 - Arduino Mega -# SAM3X8E: SDA = 20 SCL = 21 - Arduino Due -# SAMD21G18: SDA = 20 SCL = 21 - Arduino Zero, M0, M0 Pro -# ESP8266: SDA = 4 SCL = 5 -# ESP32: SDA = 21 SCL = 22 -# RP2040: SDA = 4 SCL = 5 - Raspberry Pi Pico (W) -# -# Only give the SDA pin of the I2C bus. SCL (clock) pin must be -# connected for it to work, but we don't need to control it. -# - -# Board's hardware I2C interface on predetermined pins. bus = Denko::I2C::Bus.new(board: board, pin: :SDA) -# Bit-banged I2C on any pins. -# bus = Denko::I2C::BitBang.new(board: board, pins: {scl: 8, sda: 9}) +# bus = Denko::I2C::BitBang.new(board: board, pins: {scl: 4, sda: 5}) +# bus = Denko::SPI::Bus.new(board: board) +# bus = Denko::SPI::BitBang.new(board: board, pins: {clock: 13, output: 11}) -oled = Denko::Display::SSD1306.new(bus: bus, rotate: true) -canvas = oled.canvas +# I2C OLED, connected to I2C SDA and SCL only. Default I2C address of 0x3C. +oled = Denko::Display::SSD1306.new(bus: bus, address: 0x3C, rotate: true) + +# SPI OLED, connected to SPI CLK and MOSI pins. +# select and dc pins must be given. reset is optional (can be pulled high instead). +# oled = Denko::Display::SSD1306.new(bus: bus, pins: {select: 10, dc: 7, reset: 8}, rotate: true) # Draw some text on the OLED's canvas (a Ruby memory buffer). +canvas = oled.canvas canvas.text_cursor = [27,60] canvas.print("Hello World!") diff --git a/lib/denko/display/ssd1306.rb b/lib/denko/display/ssd1306.rb index 64af1b7..6a6bf65 100644 --- a/lib/denko/display/ssd1306.rb +++ b/lib/denko/display/ssd1306.rb @@ -3,7 +3,7 @@ module Denko module Display class SSD1306 - include I2C::Peripheral + include Behaviors::BusPeripheral # Fundamental Commands # Single byte (no need to OR with anything) @@ -65,12 +65,6 @@ class SSD1306 WIDTHS = [64,96,128] HEIGHTS = [16,32,48,64] - def before_initialize(options={}) - @i2c_address = 0x3C - @i2c_frequency = 400000 - super(options) - end - def after_initialize(options={}) super(options) @@ -79,8 +73,8 @@ def after_initialize(options={}) @rows = options[:rows] || options[:height] || 64 # Validate known sizes. - raise ArgumentError, "error in SSD1306 width: #{@columns}. Must be in: #{WIDTHS.inspect}" unless WIDTHS.include?(@columns) - raise ArgumentError, "error in SSD1306 height: #{@rows}. Must be in: #{HEIGHTS.inspect}" unless HEIGHTS.include?(@rows) + raise ArgumentError, "error in #{self.class} width: #{@columns}. Must be in: #{WIDTHS.inspect}" unless WIDTHS.include?(@columns) + raise ArgumentError, "error in #{self.class} height: #{@rows}. Must be in: #{HEIGHTS.inspect}" unless HEIGHTS.include?(@rows) # Everything except 96x16 size uses clock 0x80. clock = 0x80 @@ -94,7 +88,8 @@ def after_initialize(options={}) seg_remap = options[:rotate] ? 0x01 : 0x00 com_direction = options[:rotate] ? 0x08 : 0x00 - # Startup sequence + # Startup sequence (SPI doesn't work properly if this isn't sent twice.) + 2.times do command [ MULTIPLEX_RATIO, @rows - 1, DISPLAY_OFFSET, 0x00, @@ -111,6 +106,7 @@ def after_initialize(options={}) ADDRESSING_MODE, self.class::ADDRESSING_MODE_DEFAULT, DISPLAY_ON ] + end # Create a new blank canvas and draw it. self.canvas = Canvas.new(@columns, @rows) @@ -169,14 +165,61 @@ def draw_partial(buffer, x_min, x_max, p_min, p_max) end end - # Commands are I2C messages prefixed with 0x00. - def command(bytes) - i2c_write([0x00] + bytes) + def i2c_setup + singleton_class.include(I2C::Peripheral) + + define_singleton_method(:before_initialize) do |options| + @i2c_address = 0x3C + @i2c_frequency = 400000 + super(options) + end + + # Commands are I2C messages prefixed with 0x00. + define_singleton_method(:command) do |bytes| + i2c_write([0x00] + bytes) + end + + # Data are I2C messages prefixed with 0x40. + define_singleton_method(:data) do |bytes| + i2c_write([0x40] + bytes) + end end - # Data are I2C messages prefixed with 0x40. - def data(bytes) - i2c_write([0x40] + bytes) + def spi_setup + singleton_class.include(SPI::Peripheral::MultiPin) + + define_singleton_method(:initialize_pins) do |options| + super(options) + proxy_pin :dc, DigitalIO::Output, board: bus.board + proxy_pin :reset, DigitalIO::Output, board: bus.board, optional: true + reset.high if reset + end + + # Commands are SPI bytes written while DC pin low. + define_singleton_method(:command) do |bytes| + dc.low + spi_write(bytes) + end + + # Data are SPI SPI bytes written while DC pin high. + define_singleton_method(:data) do |bytes| + dc.high + spi_write(bytes) + end + end + + def initialize(options={}) + bus = options[:bus] || options[:board] + + if bus.class.ancestors.include?(Denko::SPI::Bus) || bus.class.ancestors.include?(Denko::SPI::BitBang) + spi_setup + elsif bus.class.ancestors.include?(Denko::I2C::Bus) || bus.class.ancestors.include?(Denko::I2C::BitBang) + i2c_setup + else + raise ArgumentError, "#{self.class} must be connected to either an I2C or SPI bus" + end + + super(options) end end end