Current section
Files
Jump to
Current section
Files
lib/st7789_elixir.ex
defmodule ST7789 do @moduledoc """ ST7789 Elixir driver """ use Bitwise @enforce_keys [:spi, :gpio, :opts] defstruct [:spi, :gpio, :opts] @doc """ New connection to an ST7789 - **port**: SPI port number Default value: `0` - **cs**: SPI chip-select number (0 or 1 for BCM). Default value: `0`. - **dc**: Command/data register selection Default value: `9`. - **backlight**: Pin for controlling backlight Default value: `nil`. - **rst**: Reset pin for ST7789 Default value: `nil`. - **width**: Width of display connected to ST7789 Default value: `240`. - **height**: Height of display connected to ST7789 Default value: `240`. - **offset_top**: Offset to top row Default value: `0`. - **offset_left**: Offset to left column Default value: `0`. - **invert**: Invert display Default value: `true`. - **speed_hz**: SPI speed (in Hz) Default value: `400_0000`. **return**: `%ST7789{}` ## Example ```elixir # default # assuming device at /dev/spidev0.0 # DC connects to BCM 9 # BL not connected # RST not connected # SPI speed: 4MHz disp = ST7789.new() ``` ```elixir # specify init arguments port = 0 # spi bus 0 cs = 0 # BCM 8 / CE 0 dc = 9 # BCM 9 backlight = 17 # BCM 17 speed_hz = 80 * 1000 * 1000 # 80MHz disp = ST7789.new(port: port, cs: cs, dc: dc, backlight: backlight, speed_hz: speed_hz) ``` """ @doc functions: :exported def new(opts \\ []) do port = opts[:port] || 0 cs = opts[:cs] || 0 dc = opts[:dc] || 9 speed_hz = opts[:speed_hz] || 4_000_000 invert = opts[:invert] || true width = opts[:width] || 240 height = opts[:height] || 240 offset_top = opts[:offset_top] || 0 offset_left = opts[:offset_left] || 0 backlight = opts[:backlight] rst = opts[:rst] {:ok, spi} = Circuits.SPI.open("spidev#{port}.#{cs}", speed_hz: speed_hz) # Set DC as output. {:ok, gpio_dc} = Circuits.GPIO.open(dc, :output) # Setup backlight as output (if provided). gpio_backlight = init_backlight(backlight) # Setup reset as output (if provided). gpio_rst = init_reset(rst) %ST7789{ spi: spi, gpio: [ dc: gpio_dc, backlight: gpio_backlight, rst: gpio_rst ], opts: [ port: port, cs: cs, dc: dc, speed_hz: speed_hz, invert: invert, width: width, height: height, offset_top: offset_top, offset_left: offset_left, backlight: backlight, rst: rst ] } |> ST7789.reset() |> init() end @doc """ Reset the display, if reset pin is connected. - **self**: `%ST7789{}` **return**: `self` """ @doc functions: :exported def reset(self = %ST7789{gpio: gpio}) do gpio_rst = gpio[:rst] if gpio_rst != nil do Circuits.GPIO.write(gpio_rst, 1) :timer.sleep(500) Circuits.GPIO.write(gpio_rst, 0) :timer.sleep(500) Circuits.GPIO.write(gpio_rst, 1) :timer.sleep(500) end self end @doc """ Write the provided 16bit RGB565 image to the hardware. - **self**: `%ST7789{}` - **image_data**: Should be 16bit RGB565 format and the same dimensions (width x height x 3) as the display hardware. **return**: `self` """ @doc functions: :exported def display_rgb565(self, image_data) when is_binary(image_data) do display_rgb565(self, :binary.bin_to_list(image_data)) end def display_rgb565(self, image_data) when is_list(image_data) do self |> set_window(x0: 0, y0: 0, x1: nil, y2: nil) |> send(image_data, true, 4096) end @doc """ Write the provided 24bit BGR888/RGB888 image to the hardware. - **self**: `%ST7789{}` - **image_data**: Should be 24bit BGR888/RGB888 format and the same dimensions (width x height x 3) as the display hardware. - **channel_order**: either `:rgb` or `:bgr` **return**: `self` """ @doc functions: :exported def display(self, image_data, channel_order) when is_binary(image_data) and (channel_order == :rgb or channel_order == :bgr) do display_rgb565( self, image_data |> CvtColor.cvt(String.to_atom("#{Atom.to_string(channel_order)}888"), :rgb565) |> :binary.bin_to_list() ) end def display(self, image_data, channel_order) when is_list(image_data) and (channel_order == :rgb or channel_order == :bgr) do display( self, Enum.map(image_data, &Enum.into(&1, <<>>, fn bit -> <<bit::8>> end)), channel_order ) end @doc """ Set backlight status - **self**: `%ST7789{}` - **status**: either `:on` or `:off` **return**: `self` """ @doc functions: :exported def set_backlight(self = %ST7789{gpio: gpio}, :on) do backlight = gpio[:backlight] if backlight != nil do Circuits.GPIO.write(backlight, 1) end self end def set_backlight(self = %ST7789{gpio: gpio}, :off) do backlight = gpio[:backlight] if backlight != nil do Circuits.GPIO.write(backlight, 0) end self end @doc """ Get screen size - **self**: `%ST7789{}` **return**: `%{height: height, width: width}` """ @doc functions: :exported def size(%ST7789{opts: opts}) do %{height: opts[:height], width: opts[:width]} end @doc """ Write a byte to the display as command data. - **self**: `%ST7789{}` - **cmd**: command data **return**: `self` """ @doc functions: :exported def command(self, cmd) when is_integer(cmd) do send(self, cmd, false) end @doc """ Write a byte or array of bytes to the display as display data. - **self**: `%ST7789{}` - **data**: display data **return**: `self` """ @doc functions: :exported def data(self, data) do send(self, data, true) end defp chunk_binary(binary, chunk_size) when is_binary(binary) do total_bytes = byte_size(binary) full_chunks = div(total_bytes, chunk_size) chunks = if full_chunks > 0 do for i <- 0..(full_chunks - 1), reduce: [] do acc -> [:binary.part(binary, chunk_size * i, chunk_size) | acc] end else [] end remaining = rem(total_bytes, chunk_size) chunks = if remaining > 0 do [:binary.part(binary, chunk_size * full_chunks, remaining) | chunks] else chunks end Enum.reverse(chunks) end @doc """ Send bytes to the ST7789 - **self**: `%ST7789{}` - **bytes**: The bytes to be sent to `self` - `when is_integer(bytes)`, `sent` will take the 8 least-significant bits `[band(bytes, 0xFF)]` and send it to `self` - `when is_list(bytes)`, `bytes` will be casting to bitstring and then sent to `self` - **is_data**: - `true`: `bytes` will be sent as data - `false`: `bytes` will be sent as commands - **chunk_size**: Indicates how many bytes will be send in a single write call **return**: `self` """ @doc functions: :exported def send(self, bytes, is_data, chunk_size \\ 4096) def send(self = %ST7789{}, bytes, true, chunk_size) do send(self, bytes, 1, chunk_size) end def send(self = %ST7789{}, bytes, false, chunk_size) do send(self, bytes, 0, chunk_size) end def send(self = %ST7789{}, bytes, is_data, chunk_size) when (is_data == 0 or is_data == 1) and is_integer(bytes) do send(self, [Bitwise.band(bytes, 0xFF)], is_data, chunk_size) end def send(self = %ST7789{}, bytes, is_data, chunk_size) when (is_data == 0 or is_data == 1) and is_list(bytes) do send(self, IO.iodata_to_binary(bytes), is_data, chunk_size) end def send(self = %ST7789{gpio: gpio, spi: spi}, bytes, is_data, chunk_size) when (is_data == 0 or is_data == 1) and is_binary(bytes) do gpio_dc = gpio[:dc] if gpio_dc != nil do Circuits.GPIO.write(gpio_dc, is_data) for xfdata <- chunk_binary(bytes, chunk_size) do {:ok, _ret} = Circuits.SPI.transfer(spi, xfdata) end self else {:error, "gpio[:dc] is nil"} end end defp init(self = %ST7789{opts: board}) do invert = board[:invert] # Initialize the display. # Software reset command(self, kSWRESET()) # delay 150 ms :timer.sleep(150) self |> command(kMADCTL()) |> data(0x70) |> command(kFRMCTR2()) |> data(0x0C) |> data(0x0C) |> data(0x00) |> data(0x33) |> data(0x33) |> command(kCOLMOD()) |> data(0x05) |> command(kGCTRL()) |> data(0x14) |> command(kVCOMS()) |> data(0x37) # Power control |> command(kLCMCTRL()) |> data(0x2C) # Power control |> command(kVDVVRHEN()) |> data(0x01) # Power control |> command(kVRHS()) |> data(0x12) # Power control |> command(kVDVS()) |> data(0x20) |> command(0xD0) |> data(0xA4) |> data(0xA1) |> command(kFRCTRL2()) |> data(0x0F) # Set Gamma |> command(kGMCTRP1()) |> data(0xD0) |> data(0x04) |> data(0x0D) |> data(0x11) |> data(0x13) |> data(0x2B) |> data(0x3F) |> data(0x54) |> data(0x4C) |> data(0x18) |> data(0x0D) |> data(0x0B) |> data(0x1F) |> data(0x23) # Set Gamma |> command(kGMCTRN1()) |> data(0xD0) |> data(0x04) |> data(0x0C) |> data(0x11) |> data(0x13) |> data(0x2C) |> data(0x3F) |> data(0x44) |> data(0x51) |> data(0x2F) |> data(0x1F) |> data(0x1F) |> data(0x20) |> data(0x23) |> init_invert(invert) |> command(kSLPOUT()) |> command(kDISPON()) :timer.sleep(100) self end defp init_backlight(nil), do: nil defp init_backlight(backlight) when backlight >= 0 do {:ok, gpio} = Circuits.GPIO.open(backlight, :output) Circuits.GPIO.write(gpio, 0) :timer.sleep(100) Circuits.GPIO.write(gpio, 1) gpio end defp init_backlight(_), do: nil defp init_reset(nil), do: nil defp init_reset(rst) when rst >= 0 do {:ok, gpio} = Circuits.GPIO.open(rst, :output) gpio end defp init_reset(_), do: nil defp init_invert(self, true) do # Invert display command(self, kINVON()) end defp init_invert(self, _) do # Don't invert display command(self, kINVOFF()) end defp set_window(self = %ST7789{opts: board}, opts = [x0: 0, y0: 0, x1: nil, y2: nil]) do width = board[:width] height = board[:height] offset_top = board[:offset_top] offset_left = board[:offset_left] x0 = opts[:x0] x1 = opts[:x1] x1 = if x1 == nil, do: width - 1 y0 = opts[:y0] y1 = opts[:y1] y1 = if y1 == nil, do: height - 1 y0 = y0 + offset_top y1 = y1 + offset_top x0 = x0 + offset_left x1 = x1 + offset_left self |> command(kCASET()) |> data(bsr(x0, 8)) |> data(band(x0, 0xFF)) |> data(bsr(x1, 8)) |> data(band(x1, 0xFF)) |> command(kRASET()) |> data(bsr(y0, 8)) |> data(band(y0, 0xFF)) |> data(bsr(y1, 8)) |> data(band(y1, 0xFF)) |> command(kRAMWR()) end @doc functions: :constants def kSWRESET, do: 0x01 @doc functions: :constants def kSLPOUT, do: 0x11 @doc functions: :constants def kINVOFF, do: 0x20 @doc functions: :constants def kINVON, do: 0x21 @doc functions: :constants def kDISPON, do: 0x29 @doc functions: :constants def kCASET, do: 0x2A @doc functions: :constants def kRASET, do: 0x2B @doc functions: :constants def kRAMWR, do: 0x2C @doc functions: :constants def kMADCTL, do: 0x36 @doc functions: :constants def kCOLMOD, do: 0x3A @doc functions: :constants def kFRMCTR2, do: 0xB2 @doc functions: :constants def kGCTRL, do: 0xB7 @doc functions: :constants def kVCOMS, do: 0xBB @doc functions: :constants def kLCMCTRL, do: 0xC0 @doc functions: :constants def kVDVVRHEN, do: 0xC2 @doc functions: :constants def kVRHS, do: 0xC3 @doc functions: :constants def kVDVS, do: 0xC4 @doc functions: :constants def kFRCTRL2, do: 0xC6 @doc functions: :constants def kGMCTRP1, do: 0xE0 @doc functions: :constants def kGMCTRN1, do: 0xE1end