Mix.install([
{:circuits_gpio, "~> 2.1"},
{:circuits_spi, "~> 2.0"},
{:kino, "~> 0.15"}
])このノートブックでは、Raspberry Pi 上で、3.5インチ SPI LCD(グラフィック用 ILI9486 コントローラ)と抵抗膜式タッチパネル(XPT2046)を動かします。
- Raspberry Pi 本体
- 3.5″ SPI LCD + 抵抗膜タッチパネル
- MicroSD カード(Nerves Livebook イメージを書き込んだもの)
- 電源(5V・2A以上推奨)
動作確認済み
- Raspberry Pi 3 Model B
- Raspberry Pi 4 Model B
その他の機種について
以下の機能を備えていれば、他の Pi ボードでも動作するはずです:
- SPI0(ディスプレイ用)、SPI1(タッチ用)
- GPIO 24(DC)、GPIO 25(RST)
# LCD (A) の場合: false
# LCD (C) の場合: true
is_high_speed = true| LCD 側ピン | 用途 | Pi 側 GPIO | ピン番号 |
|---|---|---|---|
DC |
Data/Command | GPIO 24 | 18 |
RST |
Reset | GPIO 25 | 22 |
CS |
LCD Select | SPI0 CS0 | 24 |
T_CS |
Touch Select | SPI1 CS0 | 26 |
SCLK |
SPI Clock | GPIO 11 | 23 |
MOSI |
SPI Data Out | GPIO 10 | 19 |
MISO |
SPI Data In | GPIO 9 | 21 |
GND |
Ground | GND | 6 |
VCC |
Power (3.3V) | 3.3V | 1 |
VCC |
Power (5V) | 5V | 2 |
LCD を制御するうえで重要な信号を2つ GPIO で扱います:
- DC(Data/Command):コマンドとデータを切り替える信号
- RST(Reset):起動時にディスプレイをリセットする信号
これらのピンは一度だけ開き、その参照をキャッシュして使い回す設計にします。
以下のモジュールは Circuits.GPIO.open/2 で得たピン参照を保持し、重複オープンを防ぎます。
defmodule MyApp.GPIOCache do
def open(pin, direction) do
key = {:gpio_ref, pin}
case :persistent_term.get(key, :not_found) do
%Circuits.GPIO.CDev{} = ref ->
ref
_ ->
case Circuits.GPIO.open(pin, direction) do
{:ok, %Circuits.GPIO.CDev{} = ref} ->
:persistent_term.put(key, ref)
ref
{:error, :already_open} ->
raise """
GPIO #{pin} is already open (not via GPIOCache).
Please restart the notebook runtime and open pins only via GPIOCache.open/2.
"""
{:error, reason} ->
raise "Failed to open GPIO #{pin}: #{inspect(reason)}"
end
end
end
end
alias MyApp.GPIOCache
gpio_dc = GPIOCache.open(24, :output)
gpio_rst = GPIOCache.open(25, :output)
[gpio_dc, gpio_rst]ディスプレイ用とタッチ用の2つの SPI バスを開き、その設定を確認します。
# ディスプレイ用
{:ok, spi_lcd} =
if is_high_speed do
Circuits.SPI.open("spidev0.0", speed_hz: 125_000_000)
else
Circuits.SPI.open("spidev0.0", speed_hz: 16_000_000)
end
# タッチ用, 50kHz
{:ok, spi_touch} = Circuits.SPI.open("spidev0.1", speed_hz: 50_000)
# 設定を表示
{:ok, spi_lcd_cfg} = Circuits.SPI.config(spi_lcd)
{:ok, spi_touch_cfg} = Circuits.SPI.config(spi_touch)
[
Map.put(spi_lcd_cfg, :bus, "spidev0.0"),
Map.put(spi_touch_cfg, :bus, "spidev0.1")
]
|> Kino.DataTable.new()前項で準備した SPI および GPIO バスを用いて、ILI9486 ドライバを初期化し、ディスプレイの制御を行います。
本記事では、ili9486_elixir パッケージを参考にしながら、動作確認に必要な最小限の機能を備えた ILI9486 モジュールを実装してみます。
なお、実際の開発では ili9486_elixir パッケージをそのまま利用すると便利です。
defmodule MyApp.ILI9486 do
use GenServer
import Bitwise
## Public interface
def new(opts \\ []) do
GenServer.start_link(__MODULE__, opts)
end
def reset(display_pid) do
GenServer.call(display_pid, :reset)
end
def size(display_pid) do
GenServer.call(display_pid, :size)
end
def pix_fmt(display_pid) do
GenServer.call(display_pid, :pix_fmt)
end
def set_pix_fmt(display_pid, pix_fmt)
when pix_fmt == :bgr565 or pix_fmt == :rgb565 or pix_fmt == :bgr666 or pix_fmt == :rgb666 do
GenServer.call(display_pid, {:set_pix_fmt, pix_fmt})
end
def set_display(display_pid, status) when status == :on or status == :off do
GenServer.call(display_pid, {:set_display, status})
end
def set_frame_rate(display_pid, frame_rate) do
GenServer.call(display_pid, {:set_frame_rate, frame_rate})
end
def set_display_mode(display_pid, display_mode) do
GenServer.call(display_pid, {:set_display_mode, display_mode})
end
def display_666(display_pid, image_data) when is_binary(image_data) or is_list(image_data) do
GenServer.call(display_pid, {:display_666, image_data})
end
def display(display_pid, image_data, source_color)
when is_binary(image_data) and (source_color == :rgb888 or source_color == :bgr888) do
GenServer.call(display_pid, {:display, image_data, source_color})
end
def display_565(display_pid, image_data) when is_binary(image_data) or is_list(image_data) do
GenServer.call(display_pid, {:display_565, image_data})
end
def command(display_pid, cmd, opts \\ []) when is_integer(cmd) do
GenServer.call(display_pid, {:command, cmd, opts})
end
def data(_self_pid, []), do: :ok
def data(display_pid, data) do
GenServer.call(display_pid, {:data, data})
end
def send(display_pid, bytes, is_data)
when (is_integer(bytes) or is_list(bytes)) and is_boolean(is_data) do
GenServer.call(display_pid, {:send, bytes, is_data})
end
## Callbacks
@impl true
def init(opts) do
Process.flag(:trap_exit, true)
width = opts[:width] || 480
height = opts[:height] || 320
offset_top = opts[:offset_top] || 0
offset_left = opts[:offset_left] || 0
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
spi_lcd = opts[:spi_lcd]
spi_touch = opts[:spi_touch]
gpio_dc = opts[:gpio_dc]
gpio_rst = opts[:gpio_rst]
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
data_bus = if is_high_speed, do: :parallel_16bit, else: :parallel_8bit
display =
%{
spi_lcd: spi_lcd,
spi_touch: spi_touch,
gpio: [dc: gpio_dc, rst: gpio_rst],
opts: [
width: width,
height: height,
offset_top: offset_top,
offset_left: offset_left
],
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.(spi_lcd)
}
|> _reset()
|> _init(is_high_speed)
{:ok, display}
end
@impl true
def terminate(_reason, %{spi_lcd: spi_lcd, spi_touch: spi_touch, gpio: gpio}) do
dc_pin = gpio[:dc]
rst_pin = gpio[:rst]
Circuits.SPI.close(spi_lcd)
if spi_touch, do: Circuits.SPI.close(spi_touch)
Circuits.GPIO.close(dc_pin)
if rst_pin, do: Circuits.GPIO.close(rst_pin)
:ok
end
@impl true
def handle_call(:reset, _from, display) do
{:reply, :ok, _reset(display)}
end
@impl true
def handle_call(:size, _from, %{opts: opts} = display) do
ret = %{height: opts[:height], width: opts[:width]}
{:reply, ret, display}
end
@impl true
def handle_call(:pix_fmt, _from, %{pix_fmt: pix_fmt} = display) do
{:reply, pix_fmt, 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({: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
## Private helpers
defp _reset(display = %{gpio: gpio}) do
gpio_rst = gpio[:rst]
if gpio_rst != nil do
Circuits.GPIO.write(gpio_rst, 1)
Process.sleep(500)
Circuits.GPIO.write(gpio_rst, 0)
Process.sleep(500)
Circuits.GPIO.write(gpio_rst, 1)
Process.sleep(500)
end
display
end
defp _set_pix_fmt(display = %{}, pix_fmt)
when pix_fmt == :bgr565 or pix_fmt == :rgb565 or pix_fmt == :bgr666 or pix_fmt == :rgb666 do
%{display | pix_fmt: pix_fmt}
|> _command(kMADCTL(), cmd_data: _mad_mode(display))
end
defp _set_display(display = %{}, :on) do
_command(display, kDISPON())
end
defp _set_display(display = %{}, :off) do
_command(display, kDISPOFF())
end
defp _set_display_mode(display = %{}, display_mode = :normal) do
%{display | display_mode: display_mode}
|> _command(kNORON())
end
defp _set_display_mode(display = %{}, display_mode = :partial) do
%{display | display_mode: display_mode}
|> _command(kPTLON())
end
defp _set_display_mode(display = %{}, display_mode = :idle) do
%{display | display_mode: display_mode}
|> _command(display, kIDLEON())
end
defp _set_frame_rate( display = %{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)
%{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
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
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
defp _display(display = %{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 = %{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,
Enum.map(image_data, &Enum.into(&1, <<>>, fn bit -> <<bit::8>> end)),
source_color
)
end
defp _command(display, cmd, opts \\ [])
defp _command(display = %{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)
Process.sleep(delay)
display
end
defp _command(display = %{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)
Process.sleep(delay)
display
end
defp _data(display, []), do: display
defp _data(display = %{data_bus: :parallel_8bit}, data) do
_send(display, data, true, false)
end
defp _data(display = %{data_bus: :parallel_16bit}, data) do
_send(display, data, true, true)
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 = %{}, bytes, true, to_be16) do
_send(display, bytes, 1, to_be16)
end
defp _send(display = %{}, bytes, false, to_be16) do
_send(display, bytes, 0, to_be16)
end
defp _send(display = %{}, 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 = %{}, 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 = %{gpio: gpio, spi_lcd: 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
defp _get_channel_order(%{pix_fmt: :rgb565}), do: kMAD_RGB()
defp _get_channel_order(%{pix_fmt: :bgr565}), do: kMAD_BGR()
defp _get_channel_order(%{pix_fmt: :rgb666}), do: kMAD_RGB()
defp _get_channel_order(%{pix_fmt: :bgr666}), do: kMAD_BGR()
defp _get_pix_fmt(%{pix_fmt: :rgb565}), do: k16BIT_PIX()
defp _get_pix_fmt(%{pix_fmt: :bgr565}), do: k16BIT_PIX()
defp _get_pix_fmt(%{pix_fmt: :rgb666}), do: k18BIT_PIX()
defp _get_pix_fmt(%{pix_fmt: :bgr666}), do: k18BIT_PIX()
defp _mad_mode(display = %{rotation: 0, mad_mode: :right_down}) do
display
|> _get_channel_order()
|> bor(kMAD_X_RIGHT())
|> bor(kMAD_Y_DOWN())
end
defp _mad_mode(display = %{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 = %{rotation: 180, mad_mode: :right_down}) do
display
|> _get_channel_order()
|> bor(kMAD_X_LEFT())
|> bor(kMAD_Y_UP())
end
defp _mad_mode(display = %{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 = %{rotation: 0, mad_mode: :right_up}) do
display
|> _get_channel_order()
|> bor(kMAD_X_RIGHT())
|> bor(kMAD_Y_UP())
end
defp _mad_mode(display = %{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 = %{rotation: 180, mad_mode: :right_up}) do
display
|> _get_channel_order()
|> bor(kMAD_X_LEFT())
|> bor(kMAD_Y_DOWN())
end
defp _mad_mode(display = %{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 = %{rotation: 0, mad_mode: :rgb_mode}) do
display
|> _get_channel_order()
|> bor(kMAD_X_LEFT())
|> bor(kMAD_Y_DOWN())
end
defp _mad_mode(display = %{rotation: 90, mad_mode: :rgb_mode}) do
display
|> _get_channel_order()
|> bor(kMAD_X_RIGHT())
|> bor(kMAD_Y_DOWN())
end
defp _mad_mode(display = %{rotation: 180, mad_mode: :rgb_mode}) do
display
|> _get_channel_order()
|> bor(kMAD_X_RIGHT())
|> bor(kMAD_Y_UP())
end
defp _mad_mode(display = %{rotation: 270, mad_mode: :rgb_mode}) do
display
|> _get_channel_order()
|> bor(kMAD_X_LEFT())
|> bor(kMAD_Y_UP())
end
defp _init(display = %{frame_rate: frame_rate}, false) do
display
|> _command(kSWRESET(), delay: 120)
|> _command(kRGB_INTERFACE(), cmd_data: 0x00)
|> _command(kSLPOUT(), delay: 200)
|> _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 = %{frame_rate: frame_rate}, true) do
display
|> _command(kSWRESET(), delay: 120)
|> _command(kRGB_INTERFACE(), cmd_data: 0x00)
|> _command(kSLPOUT(), delay: 250)
|> _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 = %{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
def kNOP, do: 0x00
def kSWRESET, do: 0x01
def kRDDID, do: 0x04
def kRDDST, do: 0x09
def kRDMODE, do: 0x0A
def kRDMADCTL, do: 0x0B
def kRDPIXFMT, do: 0x0C
def kRDIMGFMT, do: 0x0D
def kRDSELFDIAG, do: 0x0F
def kSLPIN, do: 0x10
def kSLPOUT, do: 0x11
def kPTLON, do: 0x12
def kNORON, do: 0x13
def kINVOFF, do: 0x20
def kINVON, do: 0x21
def kGAMMASET, do: 0x26
def kDISPOFF, do: 0x28
def kDISPON, do: 0x29
def kCASET, do: 0x2A
def kPASET, do: 0x2B
def kRAMWR, do: 0x2C
def kRAMRD, do: 0x2E
def kPTLAR, do: 0x30
def kVSCRDEF, do: 0x33
def kMADCTL, do: 0x36
def kVSCRSADD, do: 0x37
def kIDLEOFF, do: 0x38
def kIDLEON, do: 0x39
def kPIXFMT, do: 0x3A
def kRGB_INTERFACE, do: 0xB0
def kFRMCTR1, do: 0xB1
def kFRMCTR2, do: 0xB2
def kFRMCTR3, do: 0xB3
def kINVCTR, do: 0xB4
def kDFUNCTR, do: 0xB6
def kPWCTR1, do: 0xC0
def kPWCTR2, do: 0xC1
def kPWCTR3, do: 0xC2
def kPWCTR4, do: 0xC3
def kPWCTR5, do: 0xC4
def kVMCTR1, do: 0xC5
def kVMCTR2, do: 0xC7
def kRDID1, do: 0xDA
def kRDID2, do: 0xDB
def kRDID3, do: 0xDC
def kRDID4, do: 0xDD
def kGMCTRP1, do: 0xE0
def kGMCTRN1, do: 0xE1
def kDGCTR1, do: 0xE2
def kDGCTR2, do: 0xE3
def kMAD_RGB, do: 0x08
def kMAD_BGR, do: 0x00
def k18BIT_PIX, do: 0x66
def k16BIT_PIX, do: 0x55
def kMAD_VERTICAL, do: 0x20
def kMAD_X_LEFT, do: 0x00
def kMAD_X_RIGHT, do: 0x40
def kMAD_Y_UP, do: 0x80
def kMAD_Y_DOWN, do: 0x00
def kHISPEEDF1, do: 0xF1
def kHISPEEDF2, do: 0xF2
def kHISPEEDF8, do: 0xF8
def kHISPEEDF9, do: 0xF9
end
alias MyApp.ILI9486まず ILI9486 ドライバを起動してディスプレイを初期化します。
続いて、size/1 で解像度、pix_fmt/1 で現在のピクセルフォーマットを確認し、最後に set_display/2 でディスプレイの表示を一時的にオフ→オンに切り替えて動作を確かめます。
display_width = 320
display_height = 480
{:ok, display} =
ILI9486.new(
spi_lcd: spi_lcd,
spi_touch: spi_touch,
gpio_dc: gpio_dc,
gpio_rst: gpio_rst,
width: display_width,
height: display_height,
rotation: 0,
pix_fmt: :rgb565,
is_high_speed: is_high_speed
)
# 表示サイズを確認
ILI9486.size(display) |> IO.inspect(label: "解像度")
# 現在のピクセルフォーマットを確認
ILI9486.pix_fmt(display) |> IO.inspect(label: "ピクセルフォーマット")
# 表示をオフ→オンで切り替え
ILI9486.set_display(display, :off)
Process.sleep(200)
ILI9486.set_display(display, :on)ILI9486 パネルは 16 ビットカラーを採用しており、赤(5 ビット)、緑(6 ビット)、青(5 ビット)をまとめて 1 つの 16 ビット数値(0bRRRRRGGGGGGBBBBB)として扱います。
注意: 多くの 3.5″ SPI TFT はデフォルトで BGR565(赤と青が入れ替わった配線)になっています。その場合は、ドライバの
pix_fmt: :bgr565を指定するか、色変換時に赤/青チャンネルを入れ替えてください。
このモジュールを使うと、8ビット(0~255)の RGB 値を RGB565 整数に変換できます。
defmodule MyApp.Color do
import Bitwise
def rgb565(r, g, b) when r in 0..255 and g in 0..255 and b in 0..255 do
(r &&& 0xF8) <<< 8 ||| (g &&& 0xFC) <<< 3 ||| b >>> 3
end
def rgb565_binary(r, g, b), do: <<rgb565(r, g, b)::16-big>>
def rgb565_hex(r, g, b), do: to_hex(rgb565(r, g, b), 4, "0x")
def rgb565_bits(r, g, b), do: to_bits(rgb565(r, g, b), 16)
defp to_hex(int_value, width, prefix) do
int_value
|> Integer.to_string(16)
|> String.upcase()
|> String.pad_leading(width, "0")
|> (&(prefix <> &1)).()
end
defp to_bits(int_value, width) do
int_value
|> Integer.to_string(2)
|> String.pad_leading(width, "0")
end
end
alias MyApp.Color以下のコードでは、いくつかの基本的な色を RGB565 形式に変換し、 それらの色を ディスプレイ全体 に順番に描画し、500ms ごとに切り替えてみます。
colors = [
red: {255, 0, 0},
green: {0, 255, 0},
blue: {0, 0, 255},
white: {255, 255, 255},
black: {0, 0, 0},
yellow: {255, 255, 0},
cyan: {0, 255, 255},
magenta: {255, 0, 255},
gray: {128, 128, 128}
]
for {color_name, {red, green, blue}} <- colors do
IO.puts("表示中: #{color_name}")
pixel = Color.rgb565_binary(red, green, blue)
buffer = :binary.copy(pixel, display_width * display_height)
ILI9486.display_565(display, buffer)
Process.sleep(500)
end
for {color_name, {red, green, blue}} <- colors do
[
name: Atom.to_string(color_name),
red: red,
green: green,
blue: blue,
rgb565_hex: Color.rgb565_hex(red, green, blue),
rgb565_bits: Color.rgb565_bits(red, green, blue)
]
end
|> Kino.DataTable.new()ASCII 文字(0x20–0x7E)のための最小限の 5×7 ビットマップフォントを定義します。
このフォントを使って、テキストをフレームバッファに描画できるようになります。
defmodule MyApp.Font5x7 do
@font %{
?\s => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
?! => [0b00100, 0b00100, 0b00100, 0b00100, 0b00000, 0b00000, 0b00100],
?" => [0b01010, 0b01010, 0b01010, 0b00000, 0b00000, 0b00000, 0b00000],
?# => [0b01010, 0b01010, 0b11111, 0b01010, 0b11111, 0b01010, 0b01010],
?$ => [0b00100, 0b01111, 0b10100, 0b01110, 0b00101, 0b11110, 0b00100],
?% => [0b11000, 0b11001, 0b00010, 0b00100, 0b01000, 0b10011, 0b00011],
?& => [0b01100, 0b10010, 0b10100, 0b01000, 0b10101, 0b10010, 0b01101],
?' => [0b00100, 0b00100, 0b01000, 0b00000, 0b00000, 0b00000, 0b00000],
?( => [0b00010, 0b00100, 0b01000, 0b01000, 0b01000, 0b00100, 0b00010],
?) => [0b01000, 0b00100, 0b00010, 0b00010, 0b00010, 0b00100, 0b01000],
?* => [0b00000, 0b00100, 0b10101, 0b01110, 0b10101, 0b00100, 0b00000],
?+ => [0b00000, 0b00100, 0b00100, 0b11111, 0b00100, 0b00100, 0b00000],
?, => [0b00000, 0b00000, 0b00000, 0b00000, 0b00110, 0b00100, 0b01000],
?- => [0b00000, 0b00000, 0b00000, 0b11111, 0b00000, 0b00000, 0b00000],
?. => [0b00000, 0b00000, 0b00000, 0b00000, 0b01100, 0b01100, 0b00000],
?/ => [0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b00000, 0b00000],
?0 => [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],
?1 => [0b00100, 0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
?2 => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b01000, 0b11111],
?3 => [0b11111, 0b00010, 0b00100, 0b00010, 0b00001, 0b10001, 0b01110],
?4 => [0b00010, 0b00110, 0b01010, 0b10010, 0b11111, 0b00010, 0b00010],
?5 => [0b11111, 0b10000, 0b11110, 0b00001, 0b00001, 0b10001, 0b01110],
?6 => [0b00110, 0b01000, 0b10000, 0b11110, 0b10001, 0b10001, 0b01110],
?7 => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b01000, 0b01000],
?8 => [0b01110, 0b10001, 0b10001, 0b01110, 0b10001, 0b10001, 0b01110],
?9 => [0b01110, 0b10001, 0b10001, 0b01111, 0b00001, 0b00010, 0b01100],
?: => [0b00000, 0b01100, 0b01100, 0b00000, 0b01100, 0b01100, 0b00000],
?; => [0b00000, 0b01100, 0b01100, 0b00000, 0b01100, 0b00100, 0b01000],
?< => [0b00010, 0b00100, 0b01000, 0b10000, 0b01000, 0b00100, 0b00010],
?= => [0b00000, 0b00000, 0b11111, 0b00000, 0b11111, 0b00000, 0b00000],
?> => [0b01000, 0b00100, 0b00010, 0b00001, 0b00010, 0b00100, 0b01000],
?? => [0b01110, 0b10001, 0b00001, 0b00010, 0b00100, 0b00000, 0b00100],
?@ => [0b01110, 0b10001, 0b00001, 0b01101, 0b10101, 0b10101, 0b01110],
?A => [0b01110, 0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001],
?B => [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
?C => [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
?D => [0b11100, 0b10010, 0b10001, 0b10001, 0b10001, 0b10010, 0b11100],
?E => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
?F => [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
?G => [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01111],
?H => [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
?I => [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
?J => [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
?K => [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
?L => [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
?M => [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
?N => [0b10001, 0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001],
?O => [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
?P => [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
?Q => [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
?R => [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
?S => [0b01111, 0b10000, 0b10000, 0b01110, 0b00001, 0b00001, 0b11110],
?T => [0b11111, 0b10101, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
?U => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
?V => [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
?W => [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b10101, 0b01010],
?X => [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
?Y => [0b10001, 0b10001, 0b10001, 0b01110, 0b00100, 0b00100, 0b00100],
?Z => [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
?[ => [0b01110, 0b01000, 0b01000, 0b01000, 0b01000, 0b01000, 0b01110],
?\\ => [0b10001, 0b01010, 0b11111, 0b00100, 0b11111, 0b00100, 0b00100],
?] => [0b01110, 0b00010, 0b00010, 0b00010, 0b00010, 0b00010, 0b01110],
?^ => [0b00100, 0b01010, 0b10001, 0b00000, 0b00000, 0b00000, 0b00000],
?_ => [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b11111],
?` => [0b01000, 0b00100, 0b00010, 0b00000, 0b00000, 0b00000, 0b00000],
?a => [0b00000, 0b00000, 0b01110, 0b00001, 0b01111, 0b10001, 0b01111],
?b => [0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b11110],
?c => [0b00000, 0b00000, 0b01110, 0b10000, 0b10000, 0b10001, 0b01110],
?d => [0b00001, 0b00001, 0b01101, 0b10011, 0b10001, 0b10001, 0b01111],
?e => [0b00000, 0b00000, 0b01110, 0b10001, 0b11111, 0b10000, 0b01110],
?f => [0b00110, 0b01001, 0b01000, 0b11110, 0b01000, 0b01000, 0b01000],
?g => [0b00000, 0b01111, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110],
?h => [0b10000, 0b10000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001],
?i => [0b00100, 0b00000, 0b01100, 0b00100, 0b00100, 0b00100, 0b01110],
?j => [0b00010, 0b00000, 0b00110, 0b00010, 0b00010, 0b10010, 0b01100],
?k => [0b10000, 0b10000, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010],
?l => [0b01100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
?m => [0b00000, 0b00000, 0b11010, 0b10101, 0b10101, 0b10101, 0b10101],
?n => [0b00000, 0b00000, 0b10110, 0b11001, 0b10001, 0b10001, 0b10001],
?o => [0b00000, 0b00000, 0b01110, 0b10001, 0b10001, 0b10001, 0b01110],
?p => [0b00000, 0b00000, 0b11110, 0b10001, 0b11110, 0b10000, 0b10000],
?q => [0b00000, 0b00000, 0b01101, 0b10011, 0b01111, 0b00001, 0b00001],
?r => [0b00000, 0b00000, 0b10110, 0b11001, 0b10000, 0b10000, 0b10000],
?s => [0b00000, 0b00000, 0b01110, 0b10000, 0b01110, 0b00001, 0b11110],
?t => [0b01000, 0b01000, 0b11111, 0b01000, 0b01000, 0b01001, 0b00110],
?u => [0b00000, 0b00000, 0b10001, 0b10001, 0b10001, 0b10011, 0b01101],
?v => [0b00000, 0b00000, 0b10001, 0b10001, 0b10001, 0b01010, 0b00100],
?w => [0b00000, 0b00000, 0b10001, 0b10001, 0b10101, 0b10101, 0b01010],
?x => [0b00000, 0b00000, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001],
?y => [0b00000, 0b00000, 0b10001, 0b10001, 0b01111, 0b00001, 0b01110],
?z => [0b00000, 0b00000, 0b11111, 0b00010, 0b01000, 0b10000, 0b11111]
}
@doc """
指定した `char` の 5×7 ビットマップを返します。
定義がなければ空白(すべて 0)を返します。
"""
def get(char), do: Map.get(@font, char, default_blank())
defp default_blank, do: List.duplicate(0b00000, 7)
end
alias MyApp.Font5x7個別に描画コマンドを発行する代わりに、Elixir で 320x480 の RGB565 バッファを組み立て、
図形や文字を描画したうえで一度に送信します。
defmodule MyApp.Framebuffer do
import Bitwise
defstruct width: 0, height: 0, buffer: <<>>
@doc """
`width`×`height` のフレームバッファを作成し、`fill_color`(RGB565)で塗りつぶします。
"""
def new(width, height, fill_color \\ 0x0000)
when width > 0 and height > 0 and fill_color in 0..0xFFFF do
pixel = <<fill_color::16-big>>
%__MODULE__{width: width, height: height, buffer: :binary.copy(pixel, width * height)}
end
@doc """
座標 `{x, y}` のピクセルを `color`(RGB565)に設定します。
画面外の座標は無視されます。
"""
def put_pixel(%__MODULE__{width: w, height: h, buffer: buf} = fb, {x, y}, color)
when x >= 0 and x < w and y >= 0 and y < h and color in 0..0xFFFF do
idx = (y * w + x) * 2
pixel = <<color::16-big>>
<<head::binary-size(idx), _::binary-size(2), tail::binary>> = buf
%{fb | buffer: head <> pixel <> tail}
end
def put_pixel(fb, _, _), do: fb
@doc """
左上 `{x0, y0}` から幅 `fw` 高さ `fh` の矩形を `color`(RGB565)で塗りつぶします。
"""
def draw_filled_rect(fb, {x0, y0}, {fw, fh}, color)
when is_integer(x0) and is_integer(y0) and fw > 0 and fh > 0 do
0..(fh - 1)
|> Enum.reduce(fb, fn dy, acc ->
0..(fw - 1)
|> Enum.reduce(acc, fn dx, row_acc ->
put_pixel(row_acc, {x0 + dx, y0 + dy}, color)
end)
end)
end
@doc """
フォントを使ってテキストを描画します。
オプション:`scale`(拡大率、デフォルト 1)、`color`(RGB565、デフォルト 0xFFFF)。
"""
def draw_text(fb, text, {x0, y0}, opts \\ []) when is_binary(text) do
scale = Keyword.get(opts, :scale, 1)
color = Keyword.get(opts, :color, 0xFFFF)
spacing = 1
text
|> String.to_charlist()
|> Enum.with_index()
|> Enum.reduce(fb, fn {ch, i}, acc ->
x = x0 + i * (5 + spacing) * scale
draw_char(acc, ch, {x, y0}, scale, color)
end)
end
defp draw_char(%__MODULE__{} = fb, ch, {x, y}, scale, color)
when scale > 0 and color in 0..0xFFFF do
Font5x7.get(ch)
|> Enum.with_index()
|> Enum.reduce(fb, fn {row, dy}, acc ->
for dx <- 0..4, reduce: acc do
buf_acc ->
if (row &&& 1 <<< (4 - dx)) != 0 do
for sy <- 0..(scale - 1), sx <- 0..(scale - 1), reduce: buf_acc do
b -> put_pixel(b, {x + dx * scale + sx, y + dy * scale + sy}, color)
end
else
buf_acc
end
end
end)
end
end
alias MyApp.Framebufferまず、ディスプレイの幅・高さに合わせたフレームバッファを生成します。
四隅に色付きの四角形を描画し、中央に「Hello, Nerves!」をテキスト表示したあと、
display_565/2 で一括転送します。
# ディスプレイのサイズを取得
%{width: w, height: h} = ILI9486.size(display)
# フレームバッファに図形と文字を描画
frame_buffer =
Framebuffer.new(w, h)
# 左上 {0,0} に 🔴
|> Framebuffer.draw_filled_rect({0, 0}, {50, 50}, Color.rgb565(255, 0, 0))
# 右上 {w-50,0} に 🟢
|> Framebuffer.draw_filled_rect({w - 50, 0}, {50, 50}, Color.rgb565(0, 255, 0))
# 左下 {0,h-50} に 🔵
|> Framebuffer.draw_filled_rect({0, h - 50}, {50, 50}, Color.rgb565(0, 0, 255))
# 右下 {w-50,h-50} に 🟡
|> Framebuffer.draw_filled_rect({w - 50, h - 50}, {50, 50}, Color.rgb565(255, 255, 0))
# 中央にテキスト表示
|> Framebuffer.draw_text(
"Hello, Nerves!",
{round(w / 2) - 80, round(h / 2) - 16},
scale: 2,
color: Color.rgb565(255, 255, 255)
)
# フレームバッファを一括転送
ILI9486.display_565(display, frame_buffer.buffer)このセクションでは、Kino のファイル入力ウィジェットを使ってローカルの .rgb565 スプライトファイルをドラッグ&ドロップまたはクリックで選択し、そのままディスプレイに表示します。
ファイルサイズが画面バッファと異なる場合は自動でパディング/トリミングを行うので、手軽に好きな画像を試すことができます。
# ファイル入力ウィジェットを作成(.rgb565 ファイルのみ受け付け)
file_input = Kino.Input.file("スプライト (.rgb565) をドラッグ&ドロップまたはクリックで選択", accept: [".rgb565"])
# 新しいKinoフレームを作成してウィジェットを表示
kino_frame = Kino.Frame.new()
Kino.Frame.render(kino_frame, file_input)
# ディスプレイ解像度から期待バッファサイズを計算
%{width: w, height: h} = ILI9486.size(display)
expected = w * h * 2
# ファイル選択イベントのハンドリング
Kino.listen(file_input, fn
%{type: :change, value: %{file_ref: file_ref, client_name: name}} ->
# 選択されたファイルを読み込み
path = Kino.Input.file_path(file_ref)
body = File.read!(path)
len = byte_size(body)
# バッファサイズが異なる場合はパディング/トリミング
image_buffer =
cond do
len == expected ->
body
len < expected ->
# 足りない分を黒で埋める
pad_pixel = <<0::16-big>>
body <> :binary.copy(pad_pixel, expected - len)
len > expected ->
# 余分な分は切り捨て
:binary.part(body, 0, expected)
end
# ディスプレイに転送して表示
ILI9486.display_565(display, image_buffer)
IO.puts("➡️ 表示: #{name} (#{len} bytes)")
:ok
_ ->
:ok
end)
# インタラクティブ表示を維持するためフレームを返す
kino_frameこのセクションでは、GenServer を使ってディスプレイ上に現在時刻を表示・定期更新する最小限の「時計サーバー」を作成します。
defmodule MyApp.ClockServer do
use GenServer
@interval_ms 100
## Public interface
def start_link(opts \\ []) do
init_args = {opts[:display], opts[:frame_buffer]}
GenServer.start_link(__MODULE__, init_args, name: __MODULE__)
end
def stop do
GenServer.stop(__MODULE__)
end
## Callbacks
@impl true
def init({display, %{buffer: _} = frame_buffer}) when is_pid(display) do
# 表示をオンにしてサイズ取得
ILI9486.set_display(display, :on)
%{width: w, height: h} = ILI9486.size(display)
# 初回の更新をスケジュール
schedule_tick()
{:ok, %{display: display, width: w, height: h, frame_buffer: frame_buffer}}
end
@impl true
def handle_info(:tick, state) do
# 現在時刻を JST 文字列で取得
timestamp =
DateTime.utc_now()
|> DateTime.shift_zone!("Asia/Tokyo")
|> Calendar.strftime("%H:%M:%S")
# フレームバッファに描画
fb =
state.frame_buffer
|> Framebuffer.draw_text(timestamp, {65, 10}, scale: 4, color: Color.rgb565(255, 255, 255))
# 一括転送
ILI9486.display_565(state.display, fb.buffer)
# 次の更新をスケジュール
schedule_tick()
{:noreply, state}
end
defp schedule_tick do
Process.send_after(self(), :tick, @interval_ms)
end
end
alias MyApp.ClockServer
ClockServer.start_link(display: display, frame_buffer: frame_buffer)時計を止めたくなったら:
ClockServer.stop()XPT2046 タッチパネルから定期的に入力を取得し、その生座標を画面ピクセルに線形マッピングします。マッピング結果はディスプレイ上に赤いドットで表示し、同時に Livebook 上で生座標とマッピング後の座標を確認できます。
defmodule MyApp.TouchServer do
use GenServer
import Bitwise
alias MyApp.ILI9486
alias MyApp.Framebuffer
alias MyApp.Color
@poll_ms 200
@cmd_read_x 0b1001_0000
@cmd_read_y 0b1101_0000
@dot_r 10
def start_link(opts) when is_list(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def stop, do: GenServer.stop(__MODULE__, :normal)
@impl true
def init(opts) do
display = Keyword.fetch!(opts, :display)
frame_buffer = Keyword.fetch!(opts, :frame_buffer)
kino_frame = Keyword.fetch!(opts, :kino_frame)
rotation = Keyword.get(opts, :rotation, 0)
%{spi_touch: spi_touch, spi_lcd: spi_lcd} = :sys.get_state(display)
%{width: w, height: h} = ILI9486.size(display)
state = %{
display: display,
spi_touch: spi_touch,
spi_lcd: spi_lcd,
kino_frame: kino_frame,
frame_buffer: frame_buffer,
w: w,
h: h,
rotation: rotation
}
send(self(), :poll)
{:ok, state}
end
@impl true
def handle_info(:poll, state) do
%{w: w, h: h, rotation: rotation} = state
{raw_x, raw_y} = read_raw(state.spi_touch)
unless raw_x == 0xFFF and raw_y == 0 do
{mx, my} = map_coords(raw_x, raw_y, w, h, rotation)
size = @dot_r * 2 + 1
top_left = {mx - @dot_r, my - @dot_r}
fb =
state.frame_buffer
|> Framebuffer.draw_filled_rect(top_left, {size, size}, Color.rgb565(255, 0, 0))
ILI9486.display_565(state.display, fb.buffer)
Kino.Frame.render(
state.kino_frame,
Kino.Markdown.new("""
🎯 **生座標**: x=#{raw_x}, y=#{raw_y}
🖥 **画面座標**: x=#{mx}, y=#{my}
""")
)
end
Process.send_after(self(), :poll, @poll_ms)
{:noreply, state}
end
defp read_raw(spi) do
{:ok, <<_::8, xh::8, xl::8>>} = Circuits.SPI.transfer(spi, <<@cmd_read_x, 0, 0>>)
{:ok, <<_::8, yh::8, yl::8>>} = Circuits.SPI.transfer(spi, <<@cmd_read_y, 0, 0>>)
raw_x = (xh <<< 8 ||| xl) >>> 3
raw_y = (yh <<< 8 ||| yl) >>> 3
{raw_x, raw_y}
end
defp map_coords(raw_x, raw_y, w, h, rotation) do
inv_x = 4095 - raw_x
inv_y = 4095 - raw_y
sx = div(inv_y * (w - 1), 4095)
sy = div(inv_x * (h - 1), 4095)
case rotation do
0 -> {sx, sy}
90 -> {h - 1 - sy, sx}
180 -> {w - 1 - sx, h - 1 - sy}
270 -> {sy, w - 1 - sx}
end
end
end
alias MyApp.TouchServer
# Kino フレームを作成
kino_frame = Kino.Frame.new()
# TouchServer を起動(タッチ入力のポーリング&表示を開始)
TouchServer.start_link(
spi: spi_touch,
display: display,
frame_buffer: frame_buffer,
kino_frame: kino_frame
)
# インタラクティブ表示を維持
kino_frameTouchServer.stop()
ElixirConf 2021 - Frank Hunleth - Embedded Elixir with Nerves Livebook
https://youtu.be/dZzM-rLu-zo?si=7dKf8q3ueuixm8CN