Current section
Files
Jump to
Current section
Files
lib/ili9486_elixir.ex
defmodule ILI9486 do @moduledoc """ ILI9486 Elixir driver """ use GenServer import Bitwise @enforce_keys [:gpio, :opts, :lcd_spi, :data_bus, :display_mode, :chunk_size] defstruct [ :gpio, :opts, :lcd_spi, :touch_spi, :touch_pid, :pix_fmt, :rotation, :mad_mode, :data_bus, :display_mode, :frame_rate, :diva, :rtna, :chunk_size ] @typedoc "Supported pixel formats" @type pixel_format :: :rgb565 | :bgr565 | :rgb666 | :bgr666 @typedoc "Screen rotation in degrees" @type rotation :: 0 | 90 | 180 | 270 @typedoc "MADCTL orientation / RGB mode" @type mad_mode :: :right_down | :right_up | :rgb_mode @typedoc "Display mode" @type display_mode :: :normal | :partial | :idle @typedoc "Supported frame rates (Hz)" @type frame_rate :: 28 | 30 | 32 | 34 | 36 | 39 | 42 | 46 | 50 | 56 | 62 | 70 | 81 | 96 | 117 @typedoc "Division ratio for internal clocks (0=focs, 1=focs/2, 2=focs/4, 3=focs/8)" @type diva :: 0..3 @typedoc "RTNA timing value (0b10000=16 .. 0b11111=31)" @type rtna :: 0b10000..0b11111 @typedoc "Parallel data bus mode" @type data_bus :: :parallel_8bit | :parallel_16bit @typedoc "Supported source color formats" @type color_format :: :rgb888 | :bgr888 @typedoc "Display struct" @type t :: %__MODULE__{ gpio: keyword(), opts: keyword(), lcd_spi: any(), touch_spi: any(), touch_pid: pid() | nil, pix_fmt: pixel_format(), rotation: rotation(), mad_mode: mad_mode(), data_bus: data_bus(), display_mode: display_mode(), frame_rate: pos_integer(), diva: diva(), rtna: rtna(), chunk_size: pos_integer() } @typedoc "Image data buffer accepted by display functions" @type image_data :: binary() | [byte() | [byte()]] @typedoc "Callback invoked on touch IRQ" @type touch_callback :: GPIOIRQDevice.irq_callback() @typedoc """ Options for `start_link/1`. - `:port` - SPI port number (default: `0`). - `:lcd_cs` - LCD chip select (default: `0`). - `:touch_cs` - Touch panel chip select, optional (default: `nil`). - `:touch_irq` - Touch panel IRQ pin (active low), optional (default: `nil`). - `:touch_speed_hz` - SPI speed for touch panel (default: `50_000`). - `:dc` - D/C pin (default: `24`). - `:rst` - Reset pin for ILI9486, optional (default: `nil`). - `:width` - Display width in pixels (default: `480`). - `:height` - Display height in pixels (default: `320`). - `:offset_top` - Vertical offset (default: `0`). - `:offset_left` - Horizontal offset (default: `0`). - `:speed_hz` - SPI speed for LCD (default: `16_000_000`). - `:pix_fmt` - Pixel format, one of `:bgr565 | :rgb565 | :bgr666 | :rgb666` (default: `:bgr565`). - `:rotation` - Rotation, one of `0 | 90 | 180 | 270` (default: `90`). - `:mad_mode` - MAD mode, one of `:right_down | :right_up | :rgb_mode` (default: `:right_down`). - `:display_mode` - Display mode, one of `:normal | :partial | :idle` (default: `:normal`). - `:frame_rate` - Frame rate (Hz), one of `28, 30, 32, 34, 36, 39, 42, 46, 50, 56, 62, 70, 81, 96, 117` (default: `70`). - `:diva` - Clock division (`0` = focs, `1` = focs/2, `2` = focs/4, `3` = focs/8) (default: `0`). - `:rtna` - Line period `RTNA[4:0]`, `0b10000` (16) to `0b11111` (31), default `0b10001` (17); each step increases clocks by 1. - `:is_high_speed` - Use high-speed variant (125 MHz SPI) (default: `false`). - `:chunk_size` - Batch transfer size; default `4096` (lo-speed) or `0x8000` (hi-speed). - `:spi_lcd` - Pre-opened SPI handle for LCD; overrides `:port`/`:lcd_cs` when set. - `:spi_touch` - Pre-opened SPI handle for touch; overrides `:port`/`:touch_cs` when set. - `:gpio_dc` - Pre-opened GPIO for D/C; overrides `:dc` when set. - `:gpio_rst` - Pre-opened GPIO for reset; overrides `:rst` when set. - `:name` - Registered name for the GenServer. """ @type ili9486_option :: {:port, non_neg_integer()} | {:lcd_cs, non_neg_integer()} | {:touch_cs, non_neg_integer() | nil} | {:touch_irq, non_neg_integer() | nil} | {:touch_speed_hz, pos_integer()} | {:dc, non_neg_integer()} | {:rst, non_neg_integer() | nil} | {:width, pos_integer()} | {:height, pos_integer()} | {:offset_top, non_neg_integer()} | {:offset_left, non_neg_integer()} | {:speed_hz, pos_integer()} | {:pix_fmt, pixel_format()} | {:rotation, rotation()} | {:mad_mode, mad_mode()} | {:display_mode, display_mode()} | {:frame_rate, frame_rate()} | {:diva, diva()} | {:rtna, rtna()} | {:is_high_speed, boolean()} | {:chunk_size, pos_integer()} | {:spi_lcd, any()} | {:spi_touch, any()} | {:gpio_dc, any()} | {:gpio_rst, any()} | {:name, GenServer.name()} @doc """ Start a new connection to an ILI9486. **return**: `%ILI9486{}` ## Example ```elixir # default # assuming LCD device at /dev/spidev0.0 # DC connects to PIN 24 # RST not connected # SPI speed: 16MHz # Pixel Format: BGR565 {:ok, disp} = ILI9486.start_link() ``` ```elixir # default with touch panel # DC connects to PIN 24 # RST connects to PIN 25 # SPI speed: 16MHz # Pixel Format: RGB666 (for demo only, not necessary) # Touch panel device at /dev/spidev0.1 # Touch panel IRQ PIN 17 {:ok, disp} = ILI9486.start_link( speed_hz: 16_000_000, pix_fmt: :bgr666, rst: 25, touch_cs: 1, touch_irq: 17 ) ``` high-speed variant (125MHz SPI) ```elixir # assuming LCD device at /dev/spidev0.0 # DC connects to PIN 24 # RST connects to PIN 25 (for demo only, not necessary) # SPI speed: 125MHz # Pixel Format: BGR666 (for demo only, not necessary) {:ok, disp} = ILI9486.start_link( is_high_speed: true, speed_hz: 125_000_000, pix_fmt: :bgr666, rst: 25 ) ``` high-speed variant (125MHz SPI) with touch panel ```elixir # assuming LCD device at /dev/spidev0.0 # DC connects to PIN 24 # RST connects to PIN 25 (for demo only, not necessary) # SPI speed: 125MHz # Pixel Format: BGR666 (for demo only, not necessary) # Touch panel device at /dev/spidev0.1 # Touch panel IRQ PIN 17 {:ok, disp} = ILI9486.start_link( is_high_speed: true, speed_hz: 125_000_000, pix_fmt: :bgr666, rst: 25, touch_cs: 1, touch_irq: 17 ) ``` """ @spec start_link([ili9486_option()]) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, opts, name: opts[:name]) end @deprecated "Use start_link/1 instead" @spec new([ili9486_option()]) :: GenServer.on_start() def new(opts \\ []) do GenServer.start_link(__MODULE__, opts) end @deprecated "Use start_link/1 instead" @spec new!([ili9486_option()]) :: pid() def new!(opts \\ []) do {:ok, pid} = GenServer.start_link(__MODULE__, opts) pid end @impl true @spec init(Keyword.t()) :: {:ok, t()} def init(opts) do # Make sure terminate/2 is called on shutdown Process.flag(:trap_exit, true) port = opts[:port] || 0 lcd_cs = opts[:lcd_cs] || 0 touch_cs = opts[:touch_cs] touch_irq = opts[:touch_irq] dc = opts[:dc] || 24 speed_hz = opts[:speed_hz] || 16_000_000 touch_speed_hz = opts[:touch_speed_hz] || 50000 width = opts[:width] || 480 height = opts[:height] || 320 offset_top = opts[:offset_top] || 0 offset_left = opts[:offset_left] || 0 rst = opts[:rst] pix_fmt = opts[:pix_fmt] || :bgr565 rotation = opts[:rotation] || 90 mad_mode = opts[:mad_mode] || :right_down display_mode = opts[:display_mode] || :normal frame_rate = opts[:frame_rate] || 70 diva = opts[:diva] || 0b00 rtna = opts[:rtna] || 0b10001 is_high_speed = opts[:is_high_speed] || false calc_chunk_size = fn spi_bus -> from_opts = opts[:chunk_size] desired_size = cond do is_integer(from_opts) and from_opts > 0 -> from_opts is_high_speed -> 0x8000 true -> 4_096 end driver_limit = cond do function_exported?(Circuits.SPI, :max_transfer_size, 1) -> Circuits.SPI.max_transfer_size(spi_bus) function_exported?(Circuits.SPI, :max_transfer_size, 0) -> Circuits.SPI.max_transfer_size() true -> desired_size end effective_limit = if driver_limit > 0, do: driver_limit, else: desired_size min(desired_size, effective_limit) end # supported data connection # 8-bit parallel MCU interface for low speed ones # - Waveshare RPi 3.5 LCD (A) / Tested # - Waveshare RPi 3.5 LCD (B) # 16-bit parallel MCU interface for the high speed one # - Waveshare RPi 3.5 LCD (C) / Tested # :parallel_16bit and :parallel_8bit supported colors # - 65K colors # - 262K colors data_bus = if is_high_speed, do: :parallel_16bit, else: :parallel_8bit lcd_spi = Keyword.get_lazy(opts, :spi_lcd, fn -> {:ok, bus} = _init_spi(port, lcd_cs, speed_hz) bus end) touch_spi = Keyword.get_lazy(opts, :spi_touch, fn -> if touch_cs do {:ok, bus} = _init_spi(port, touch_cs, touch_speed_hz) bus end end) gpio_dc = Keyword.get_lazy(opts, :gpio_dc, fn -> {:ok, pin} = Circuits.GPIO.open(dc, :output) pin end) gpio_rst = Keyword.get_lazy(opts, :gpio_rst, fn -> if rst, do: _init_reset(rst) end) {:ok, touch_pid} = _init_touch_irq(touch_irq) display = %ILI9486{ lcd_spi: lcd_spi, touch_spi: touch_spi, touch_pid: touch_pid, gpio: [ dc: gpio_dc, rst: gpio_rst ], opts: [ port: port, lcd_cs: lcd_cs, touch_cs: touch_cs, dc: dc, speed_hz: speed_hz, touch_speed_hz: touch_speed_hz, width: width, height: height, offset_top: offset_top, offset_left: offset_left, rst: rst ], pix_fmt: pix_fmt, rotation: rotation, mad_mode: mad_mode, data_bus: data_bus, display_mode: display_mode, frame_rate: frame_rate, diva: diva, rtna: rtna, chunk_size: calc_chunk_size.(lcd_spi) } |> _reset() |> _init(is_high_speed) {:ok, display} end @doc """ Closes all SPI and GPIO resources on shutdown. """ @impl true @spec terminate(any(), t()) :: :ok def terminate(_reason, %{lcd_spi: lcd_spi, touch_spi: touch_spi, gpio: gpio}) do dc_pin = gpio[:dc] rst_pin = gpio[:rst] Circuits.SPI.close(lcd_spi) if touch_spi, do: Circuits.SPI.close(touch_spi) Circuits.GPIO.close(dc_pin) if rst_pin, do: Circuits.GPIO.close(rst_pin) :ok end @doc """ Reset the display, if reset pin is connected. - **display**: `%ILI9486{}` **return**: `display` """ @spec reset(pid()) :: :ok def reset(self_pid) do GenServer.call(self_pid, :reset) end defp _reset(display = %ILI9486{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 display end @doc """ Get screen size - **display**: `%ILI9486{}` **return**: `%{height: height, width: width}` """ @spec size(pid()) :: %{height: pos_integer(), width: pos_integer()} def size(self_pid) do GenServer.call(self_pid, :size) end defp _size(%ILI9486{opts: opts}) do %{height: opts[:height], width: opts[:width]} end @doc """ Get display pixel format - **display**: `%ILI9486{}` **return**: one of `:bgr565`, `:rgb565`, `:bgr666`, `:rgb666` """ @spec pix_fmt(pid()) :: pixel_format() def pix_fmt(self_pid) do GenServer.call(self_pid, :pix_fmt) end defp _pix_fmt(%ILI9486{pix_fmt: pix_fmt}) do pix_fmt end @doc """ Set display pixel format - **display**: `%ILI9486{}` - **pix_fmt**: one of `:bgr565`, `:rgb565`, `:bgr666`, `:rgb666` **return**: `display` """ @spec set_pix_fmt(pid(), pixel_format()) :: :ok def set_pix_fmt(self_pid, pix_fmt) when pix_fmt == :bgr565 or pix_fmt == :rgb565 or pix_fmt == :bgr666 or pix_fmt == :rgb666 do GenServer.call(self_pid, {:set_pix_fmt, pix_fmt}) end defp _set_pix_fmt(display = %ILI9486{}, pix_fmt) when pix_fmt == :bgr565 or pix_fmt == :rgb565 or pix_fmt == :bgr666 or pix_fmt == :rgb666 do %ILI9486{display | pix_fmt: pix_fmt} |> _command(kMADCTL(), cmd_data: _mad_mode(display)) end @doc """ Turn on/off display - **display**: `%ILI9486{}` - **status**: either `:on` or `:off` **return**: `display` """ @spec set_display(pid(), :on | :off) :: :ok def set_display(self_pid, status) when status == :on or status == :off do GenServer.call(self_pid, {:set_display, status}) end defp _set_display(display = %ILI9486{}, :on) do _command(display, kDISPON()) end defp _set_display(display = %ILI9486{}, :off) do _command(display, kDISPOFF()) end @doc """ Set display mode - **display**: `%ILI9486{}` - **display_mode**: Valid values: `:normal`, `:partial`, `:idle` **return**: `display` """ @spec set_display_mode(pid(), display_mode()) :: :ok def set_display_mode(self_pid, display_mode) do GenServer.call(self_pid, {:set_display_mode, display_mode}) end defp _set_display_mode(display = %ILI9486{}, display_mode = :normal) do %ILI9486{display | display_mode: display_mode} |> _command(kNORON()) end defp _set_display_mode(display = %ILI9486{}, display_mode = :partial) do %ILI9486{display | display_mode: display_mode} |> _command(kPTLON()) end defp _set_display_mode(display = %ILI9486{}, display_mode = :idle) do %ILI9486{display | display_mode: display_mode} |> _command(kIDLEON()) end @doc """ Set frame rate - **display**: `%ILI9486{}` - **frame_rate**: Valid value should be one of the following - 28 - 30 - 32 - 34 - 36 - 39 - 42 - 46 - 50 - 56 - 62 - 70 - 81 - 96 - 117 **return**: `:ok` """ @spec set_frame_rate(pid(), frame_rate()) :: :ok def set_frame_rate(self_pid, frame_rate) do GenServer.call(self_pid, {:set_frame_rate, frame_rate}) end defp _set_frame_rate( display = %ILI9486{display_mode: display_mode, diva: diva, rtna: rtna}, frame_rate ) do index = Enum.find_index(_valid_frame_rates(display_mode), fn valid -> valid == frame_rate end) p1 = index |> bsl(4) |> bor(diva) %ILI9486{display | frame_rate: frame_rate} |> _command(kFRMCTR1()) |> _data(p1) |> _data(rtna) end defp _valid_frame_rates(:normal) do [28, 30, 32, 34, 36, 39, 42, 46, 50, 56, 62, 70, 81, 96, 117, 117] end @doc """ Write the provided 16bit BGR565/RGB565 image to the hardware. - **display**: `%ILI9486{}` - **image_data**: Should be 16bit BGR565/RGB565 format (same channel order as in `display`) and the same dimensions (width x height x 3) as the display hardware. **return**: `display` """ @spec display_565(pid(), image_data()) :: :ok def display_565(self_pid, image_data) when is_binary(image_data) or is_list(image_data) do GenServer.call(self_pid, {:display_565, image_data}) end defp _display_565(display, image_data) when is_binary(image_data) do _display_565(display, :binary.bin_to_list(image_data)) end defp _display_565(display, image_data) when is_list(image_data) do display |> _set_window(x0: 0, y0: 0, x1: nil, y2: nil) |> _send(image_data, true, false) end @doc """ Write the provided 18bit BGR666/RGB666 image to the hardware. - **display**: `%ILI9486{}` - **image_data**: Should be 18bit BGR666/RGB666 format (same channel order as in `display`) and the same dimensions (width x height x 3) as the display hardware. **return**: `display` """ @spec display_666(pid(), image_data()) :: :ok def display_666(self_pid, image_data) when is_binary(image_data) or is_list(image_data) do GenServer.call(self_pid, {:display_666, image_data}) end defp _display_666(display, image_data) when is_binary(image_data) do _display_666(display, :binary.bin_to_list(image_data)) end defp _display_666(display, image_data) when is_list(image_data) do display |> _set_window(x0: 0, y0: 0, x1: nil, y2: nil) |> _send(image_data, true, false) end @doc """ Write the provided 24bit BGR888/RGB888 image to the hardware. - **display**: `%ILI9486{}` - **image_data**: Should be 24bit format and the same dimensions (width x height x 3) as the display hardware. - **pix_fmt**: Either `:rgb888` or `:bgr888`. Indicates the channel order of the provided `image_data`. **return**: `display` """ @spec display(pid(), image_data(), color_format()) :: :ok def display(self_pid, image_data, source_color) when is_binary(image_data) and (source_color == :rgb888 or source_color == :bgr888) do GenServer.call(self_pid, {:display, image_data, source_color}) end defp _display(display = %ILI9486{pix_fmt: target_color}, image_data, source_color) when is_binary(image_data) and (source_color == :rgb888 or source_color == :bgr888) and (target_color == :rgb565 or target_color == :bgr565) do _display_565(display, _to_565(image_data, source_color, target_color)) end defp _display(display = %ILI9486{pix_fmt: target_color}, image_data, source_color) when is_binary(image_data) and (source_color == :rgb888 or source_color == :bgr888) and (target_color == :rgb666 or target_color == :bgr666) do _display_666(display, _to_666(image_data, source_color, target_color)) end defp _display(display, image_data, source_color) when is_list(image_data) and (source_color == :rgb888 or source_color == :bgr888) do _display( display, image_data_to_binary(image_data), source_color ) end defp image_data_to_binary(image_data) do image_data |> Enum.map(fn row_of_bytes when is_list(row_of_bytes) -> Enum.map(row_of_bytes, &<<&1::8>>) byte when is_integer(byte) -> <<byte::8>> end) |> IO.iodata_to_binary() end @doc """ Set touch panel callback function - **display**: `%ILI9486{}` - **callback**: callback function. 3 arguments: `pin`, `timestamp`, `status` """ @spec set_touch_callback(pid(), touch_callback()) :: :ok def set_touch_callback(self_pid, callback) when is_function(callback, 3) do GenServer.call(self_pid, {:set_touch_callback, callback}) end defp _set_touch_callback(display = %ILI9486{touch_pid: touch_pid}, callback) when is_function(callback, 3) do GPIOIRQDevice.set_callback(touch_pid, callback) display end @doc """ Write a byte to the display as command data. - **display**: `%ILI9486{}` - **cmd**: command data - **opts**: - **cmd_data**: cmd data to be sent. Default value: `[]`. (no data will be sent) - **delay**: wait `delay` ms after the cmd data is sent Default value: `0`. (no wait) **return**: `display` """ @spec command(pid(), byte(), Keyword.t()) :: :ok def command(self_pid, cmd, opts \\ []) when is_integer(cmd) do GenServer.call(self_pid, {:command, cmd, opts}) end defp _command(display, cmd, opts \\ []) defp _command(display = %ILI9486{data_bus: :parallel_8bit}, cmd, opts) when is_integer(cmd) do cmd_data = opts[:cmd_data] || [] delay = opts[:delay] || 0 display |> _send(cmd, false, false) |> _data(cmd_data) :timer.sleep(delay) display end defp _command(display = %ILI9486{data_bus: :parallel_16bit}, cmd, opts) when is_integer(cmd) do cmd_data = opts[:cmd_data] || [] delay = opts[:delay] || 0 display |> _send(cmd, false, true) |> _data(cmd_data) :timer.sleep(delay) display end @doc """ Write a byte or array of bytes to the display as display data. - **display**: `%ILI9486{}` - **data**: display data **return**: `display` """ @spec data(pid(), iodata()) :: :ok def data(_self_pid, []), do: :ok def data(self_pid, data) do GenServer.call(self_pid, {:data, data}) end defp _data(display, []), do: display defp _data(display = %ILI9486{data_bus: :parallel_8bit}, data) do _send(display, data, true, false) end defp _data(display = %ILI9486{data_bus: :parallel_16bit}, data) do _send(display, data, true, true) end @doc """ Send bytes to the ILI9486 - **display**: `%ILI9486{}` - **bytes**: The bytes to be sent to `display` - `when is_integer(bytes)`, `sent` will take the 8 least-significant bits `[band(bytes, 0xFF)]` and send it to `display` - `when is_list(bytes)`, `bytes` will be casting to bitstring and then sent to `display` - **is_data**: - `true`: `bytes` will be sent as data - `false`: `bytes` will be sent as commands **return**: `display` """ @spec send(pid(), integer() | iodata(), boolean()) :: :ok def send(self_pid, bytes, is_data) when (is_integer(bytes) or is_list(bytes)) and is_boolean(is_data) do GenServer.call(self_pid, {:send, bytes, is_data}) end defp to_be_u16(u8_bytes) do u8_bytes |> Enum.map(fn u8 -> [0x00, u8] end) |> IO.iodata_to_binary() 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 defp _send(display, bytes, is_data, to_be16 \\ false) defp _send(display = %ILI9486{}, bytes, true, to_be16) do _send(display, bytes, 1, to_be16) end defp _send(display = %ILI9486{}, bytes, false, to_be16) do _send(display, bytes, 0, to_be16) end defp _send(display = %ILI9486{}, bytes, is_data, to_be16) when (is_data == 0 or is_data == 1) and is_integer(bytes) do _send(display, <<Bitwise.band(bytes, 0xFF)>>, is_data, to_be16) end defp _send(display = %ILI9486{}, bytes, is_data, to_be16) when (is_data == 0 or is_data == 1) and is_list(bytes) do _send(display, IO.iodata_to_binary(bytes), is_data, to_be16) end defp _send( display = %ILI9486{gpio: gpio, lcd_spi: spi, chunk_size: chunk_size}, bytes, is_data, to_be16 ) when (is_data == 0 or is_data == 1) and is_binary(bytes) do gpio_dc = gpio[:dc] bytes = if to_be16, do: to_be_u16(:binary.bin_to_list(bytes)), else: bytes Circuits.GPIO.write(gpio_dc, is_data) for xfdata <- chunk_binary(bytes, chunk_size) do {:ok, _ret} = Circuits.SPI.transfer(spi, xfdata) end display end @impl true def handle_call(:reset, _from, display) do {:reply, :ok, _reset(display)} end @impl true def handle_call(:size, _from, display) do ret = _size(display) {:reply, ret, display} end @impl true def handle_call(:pix_fmt, _from, display) do ret = _pix_fmt(display) {:reply, ret, display} end @impl true def handle_call({:set_pix_fmt, pix_fmt}, _from, display) do {:reply, :ok, _set_pix_fmt(display, pix_fmt)} end @impl true def handle_call({:set_display, status}, _from, display) do {:reply, :ok, _set_display(display, status)} end @impl true def handle_call({:set_display_mode, display_mode}, _from, display) do {:reply, :ok, _set_display_mode(display, display_mode)} end @impl true def handle_call({:set_frame_rate, frame_rate}, _from, display) do {:reply, :ok, _set_frame_rate(display, frame_rate)} end @impl true def handle_call({:display_565, image_data}, _from, display) do {:reply, :ok, _display_565(display, image_data)} end @impl true def handle_call({:display_666, image_data}, _from, display) do {:reply, :ok, _display_666(display, image_data)} end @impl true def handle_call({:display, image_data, source_color}, _from, display) do {:reply, :ok, _display(display, image_data, source_color)} end @impl true def handle_call({:set_touch_callback, callback}, _from, display) do {:reply, :ok, _set_touch_callback(display, callback)} end @impl true def handle_call({:command, cmd, opts}, _from, display) do {:reply, :ok, _command(display, cmd, opts)} end @impl true def handle_call({:data, data}, _from, display) do {:reply, :ok, _data(display, data)} end @impl true def handle_call({:send, bytes, is_data}, _from, display) do {:reply, :ok, _send(display, bytes, is_data)} end defp _init_spi(_port, nil, _speed_hz), do: {:ok, nil} defp _init_spi(port, cs, speed_hz) when cs >= 0 do Circuits.SPI.open("spidev#{port}.#{cs}", speed_hz: speed_hz) end defp _init_spi(_port, _cs, _speed_hz), do: {:error, :invalid_cs} defp _init_touch_irq(nil), do: {:ok, nil} defp _init_touch_irq(pin) do GenServer.start_link(GPIOIRQDevice, pin) end 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 _get_channel_order(%ILI9486{pix_fmt: :rgb565}), do: kMAD_RGB() defp _get_channel_order(%ILI9486{pix_fmt: :bgr565}), do: kMAD_BGR() defp _get_channel_order(%ILI9486{pix_fmt: :rgb666}), do: kMAD_RGB() defp _get_channel_order(%ILI9486{pix_fmt: :bgr666}), do: kMAD_BGR() defp _get_pix_fmt(%ILI9486{pix_fmt: :rgb565}), do: k16BIT_PIX() defp _get_pix_fmt(%ILI9486{pix_fmt: :bgr565}), do: k16BIT_PIX() defp _get_pix_fmt(%ILI9486{pix_fmt: :rgb666}), do: k18BIT_PIX() defp _get_pix_fmt(%ILI9486{pix_fmt: :bgr666}), do: k18BIT_PIX() defp _mad_mode(display = %ILI9486{rotation: 0, mad_mode: :right_down}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_DOWN()) end defp _mad_mode(display = %ILI9486{rotation: 90, mad_mode: :right_down}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_DOWN()) |> bor(kMAD_VERTICAL()) end defp _mad_mode(display = %ILI9486{rotation: 180, mad_mode: :right_down}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_UP()) end defp _mad_mode(display = %ILI9486{rotation: 270, mad_mode: :right_down}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_UP()) |> bor(kMAD_VERTICAL()) end defp _mad_mode(display = %ILI9486{rotation: 0, mad_mode: :right_up}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_UP()) end defp _mad_mode(display = %ILI9486{rotation: 90, mad_mode: :right_up}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_DOWN()) |> bor(kMAD_VERTICAL()) end defp _mad_mode(display = %ILI9486{rotation: 180, mad_mode: :right_up}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_DOWN()) end defp _mad_mode(display = %ILI9486{rotation: 270, mad_mode: :right_up}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_UP()) |> bor(kMAD_VERTICAL()) end defp _mad_mode(display = %ILI9486{rotation: 0, mad_mode: :rgb_mode}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_DOWN()) end defp _mad_mode(display = %ILI9486{rotation: 90, mad_mode: :rgb_mode}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_DOWN()) end defp _mad_mode(display = %ILI9486{rotation: 180, mad_mode: :rgb_mode}) do display |> _get_channel_order() |> bor(kMAD_X_RIGHT()) |> bor(kMAD_Y_UP()) end defp _mad_mode(display = %ILI9486{rotation: 270, mad_mode: :rgb_mode}) do display |> _get_channel_order() |> bor(kMAD_X_LEFT()) |> bor(kMAD_Y_UP()) end defp _init(display = %ILI9486{frame_rate: frame_rate}, false) do display # software reset |> _command(kSWRESET(), delay: 120) # RGB mode off |> _command(kRGB_INTERFACE(), cmd_data: 0x00) # turn off sleep mode |> _command(kSLPOUT(), delay: 200) # interface format |> _command(kPIXFMT(), cmd_data: _get_pix_fmt(display)) |> _command(kMADCTL(), cmd_data: _mad_mode(display)) |> _command(kPWCTR3(), cmd_data: 0x44) |> _command(kVMCTR1()) |> _data(0x00) |> _data(0x00) |> _data(0x00) |> _data(0x00) |> _command(kGMCTRP1()) |> _data(0x0F) |> _data(0x1F) |> _data(0x1C) |> _data(0x0C) |> _data(0x0F) |> _data(0x08) |> _data(0x48) |> _data(0x98) |> _data(0x37) |> _data(0x0A) |> _data(0x13) |> _data(0x04) |> _data(0x11) |> _data(0x0D) |> _data(0x00) |> _command(kGMCTRN1()) |> _data(0x0F) |> _data(0x32) |> _data(0x2E) |> _data(0x0B) |> _data(0x0D) |> _data(0x05) |> _data(0x47) |> _data(0x75) |> _data(0x37) |> _data(0x06) |> _data(0x10) |> _data(0x03) |> _data(0x24) |> _data(0x20) |> _data(0x00) |> _command(kDGCTR1()) |> _data(0x0F) |> _data(0x32) |> _data(0x2E) |> _data(0x0B) |> _data(0x0D) |> _data(0x05) |> _data(0x47) |> _data(0x75) |> _data(0x37) |> _data(0x06) |> _data(0x10) |> _data(0x03) |> _data(0x24) |> _data(0x20) |> _data(0x00) |> _set_display_mode(:normal) |> _command(kINVOFF()) |> _command(kSLPOUT(), delay: 200) |> _command(kDISPON()) |> _set_frame_rate(frame_rate) end defp _init(display = %ILI9486{frame_rate: frame_rate}, true) do display # software reset |> _command(kSWRESET(), delay: 120) # RGB mode off |> _command(kRGB_INTERFACE(), cmd_data: 0x00) # turn off sleep mode |> _command(kSLPOUT(), delay: 250) # interface format |> _command(kPIXFMT(), cmd_data: _get_pix_fmt(display)) |> _command(kPWCTR3(), cmd_data: 0x44) |> _command(kVMCTR1(), cmd_data: [0x00, 0x00, 0x00, 0x00]) |> _command(kGMCTRP1()) |> _data(0x0F) |> _data(0x1F) |> _data(0x1C) |> _data(0x0C) |> _data(0x0F) |> _data(0x08) |> _data(0x48) |> _data(0x98) |> _data(0x37) |> _data(0x0A) |> _data(0x13) |> _data(0x04) |> _data(0x11) |> _data(0x0D) |> _data(0x00) |> _command(kGMCTRN1()) |> _data(0x0F) |> _data(0x32) |> _data(0x2E) |> _data(0x0B) |> _data(0x0D) |> _data(0x05) |> _data(0x47) |> _data(0x75) |> _data(0x37) |> _data(0x06) |> _data(0x10) |> _data(0x03) |> _data(0x24) |> _data(0x20) |> _data(0x00) |> _command(kDGCTR1()) |> _data(0x0F) |> _data(0x32) |> _data(0x2E) |> _data(0x0B) |> _data(0x0D) |> _data(0x05) |> _data(0x47) |> _data(0x75) |> _data(0x37) |> _data(0x06) |> _data(0x10) |> _data(0x03) |> _data(0x24) |> _data(0x20) |> _data(0x00) |> _set_display_mode(:normal) |> _command(kINVOFF()) |> _command(kDISPON(), delay: 100) |> _command(kMADCTL(), cmd_data: _mad_mode(display)) |> _set_frame_rate(frame_rate) end defp _set_window(display = %ILI9486{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 display |> _command(kCASET()) |> _data(bsr(x0, 8)) |> _data(band(x0, 0xFF)) |> _data(bsr(x1, 8)) |> _data(band(x1, 0xFF)) |> _command(kPASET()) |> _data(bsr(y0, 8)) |> _data(band(y0, 0xFF)) |> _data(bsr(y1, 8)) |> _data(band(y1, 0xFF)) |> _command(kRAMWR()) end defp _to_565(image_data, source_color, target_color) when is_binary(image_data) do image_data |> CvtColor.cvt(source_color, target_color) |> :binary.bin_to_list() end defp _to_666(image_data, :bgr888, :bgr666) when is_binary(image_data) do image_data |> :binary.bin_to_list() end defp _to_666(image_data, source_color, target_color) when is_binary(image_data) do image_data |> CvtColor.cvt(source_color, target_color) |> :binary.bin_to_list() end @doc functions: :constants def kNOP, do: 0x00 @doc functions: :constants def kSWRESET, do: 0x01 @doc functions: :constants def kRDDID, do: 0x04 @doc functions: :constants def kRDDST, do: 0x09 @doc functions: :constants def kRDMODE, do: 0x0A @doc functions: :constants def kRDMADCTL, do: 0x0B @doc functions: :constants def kRDPIXFMT, do: 0x0C @doc functions: :constants def kRDIMGFMT, do: 0x0D @doc functions: :constants def kRDSELFDIAG, do: 0x0F @doc functions: :constants def kSLPIN, do: 0x10 @doc functions: :constants def kSLPOUT, do: 0x11 @doc functions: :constants def kPTLON, do: 0x12 @doc functions: :constants def kNORON, do: 0x13 @doc functions: :constants def kINVOFF, do: 0x20 @doc functions: :constants def kINVON, do: 0x21 @doc functions: :constants def kGAMMASET, do: 0x26 @doc functions: :constants def kDISPOFF, do: 0x28 @doc functions: :constants def kDISPON, do: 0x29 @doc functions: :constants def kCASET, do: 0x2A @doc functions: :constants def kPASET, do: 0x2B @doc functions: :constants def kRAMWR, do: 0x2C @doc functions: :constants def kRAMRD, do: 0x2E @doc functions: :constants def kPTLAR, do: 0x30 @doc functions: :constants def kVSCRDEF, do: 0x33 @doc functions: :constants def kMADCTL, do: 0x36 @doc functions: :constants # Vertical Scrolling Start Address def kVSCRSADD, do: 0x37 @doc functions: :constants def kIDLEOFF, do: 0x38 @doc functions: :constants def kIDLEON, do: 0x39 @doc functions: :constants # COLMOD: Pixel Format Set def kPIXFMT, do: 0x3A @doc functions: :constants # RGB Interface Signal Control def kRGB_INTERFACE, do: 0xB0 @doc functions: :constants def kFRMCTR1, do: 0xB1 @doc functions: :constants def kFRMCTR2, do: 0xB2 @doc functions: :constants def kFRMCTR3, do: 0xB3 @doc functions: :constants def kINVCTR, do: 0xB4 # Display Function Control @doc functions: :constants def kDFUNCTR, do: 0xB6 @doc functions: :constants def kPWCTR1, do: 0xC0 @doc functions: :constants def kPWCTR2, do: 0xC1 @doc functions: :constants def kPWCTR3, do: 0xC2 @doc functions: :constants def kPWCTR4, do: 0xC3 @doc functions: :constants def kPWCTR5, do: 0xC4 @doc functions: :constants def kVMCTR1, do: 0xC5 @doc functions: :constants def kVMCTR2, do: 0xC7 @doc functions: :constants def kRDID1, do: 0xDA @doc functions: :constants def kRDID2, do: 0xDB @doc functions: :constants def kRDID3, do: 0xDC @doc functions: :constants def kRDID4, do: 0xDD @doc functions: :constants def kGMCTRP1, do: 0xE0 @doc functions: :constants def kGMCTRN1, do: 0xE1 @doc functions: :constants def kDGCTR1, do: 0xE2 @doc functions: :constants def kDGCTR2, do: 0xE3 @doc functions: :constants def kMAD_RGB, do: 0x08 @doc functions: :constants def kMAD_BGR, do: 0x00 @doc functions: :constants def k18BIT_PIX, do: 0x66 @doc functions: :constants def k16BIT_PIX, do: 0x55 @doc functions: :constants def kMAD_VERTICAL, do: 0x20 @doc functions: :constants def kMAD_X_LEFT, do: 0x00 @doc functions: :constants def kMAD_X_RIGHT, do: 0x40 @doc functions: :constants def kMAD_Y_UP, do: 0x80 @doc functions: :constants def kMAD_Y_DOWN, do: 0x00 @doc functions: :constants def kHISPEEDF1, do: 0xF1 @doc functions: :constants def kHISPEEDF2, do: 0xF2 @doc functions: :constants def kHISPEEDF8, do: 0xF8 @doc functions: :constants def kHISPEEDF9, do: 0xF9end