defmodule IS31FL3733 do
@moduledoc """
I2C driver for the IS31FL3733 12x16 dot matrix LED driver.
This driver tries to follow the data-sheet as closely as possible, so it's
recommended to be familiar with it: http://www.issi.com/WW/pdf/IS31FL3733.pdf
**NOTE:** Currently only PWM mode is supported. The auto-breath feature has
not yet been implemented in this driver.
## Example Usage
iex> # each bit is one LED, 24 * 8 == 16 * 12
...> led_state_data = String.duplicate(<<255>>, 24)
...>
...> # each byte is one LED
...> led_pwm_data = String.duplicate(<<255>>, 16 * 12)
...>
...> # turn on all the LEDs in PWM mode
...> ic =
...> "i2c-1"
...> |> IS31FL3733.open(0x50)
...> |> IS31FL3733.set_global_current_control(0x3C)
...> |> IS31FL3733.set_swy_pull_up_resistor(:"32k")
...> |> IS31FL3733.set_csx_pull_down_resistor(:"32k")
...> |> IS31FL3733.set_led_on_off(0x00, led_state_data)
...> |> IS31FL3733.set_led_pwm(0x00, led_pwm_data)
...> |> IS31FL3733.disable_software_shutdown()
...>
...> ic
#IS31FL3733<"i2c-1@0x50">
"""
alias IS31FL3733.I2CBehavior, as: I2C
defstruct ~w(
address
bus
bus_name
page
config
)a
@type sync_mode :: :single | :primary | :secondary
@type led_mode :: :breathing | :pwm
@type current_control_value :: 0x00..0xFF
@type led_on_off_register :: 0x00..0x17
@type led_pwm_register :: 0x00..0xBF
@type resistor :: :none | :"500" | :"1k" | :"2k" | :"4k" | :"8k" | :"16k" | :"32k"
@opaque t :: %__MODULE__{
address: I2C.address(),
bus: I2C.bus(),
bus_name: binary() | charlist(),
config: __MODULE__.Config.t(),
page: 0x00..0x03
}
@i2c Application.compile_env(:is31fl3733, :i2c, IS31FL3733.I2C)
# Defined pages. Use command register to change active page.
@page [
led_on_off: 0x00,
led_pwm: 0x01,
led_auto_breath: 0x02,
function: 0x03
]
# Defined registers.
# command and command_write_lock are always available;
# the rest of the registers are only available when the corresponding page is
# active.
@register [
command: 0xFD,
command_write_lock: 0xFE,
led_on_off: [
on_off: 0x00..0x17,
open: 0x18..0x2F,
short: 0x30..0x47
],
led_pwm: 0x00..0xBF,
led_auto_breath: 0x00..0xBF,
function: [
configuration: 0x00,
global_current_control: 0x01,
swy_pull_up_resistor: 0x0F,
csx_pull_down_resistor: 0x10,
reset: 0x11
]
]
@resistor [
none: 0x00,
"500": 0x01,
"1k": 0x02,
"2k": 0x03,
"4k": 0x04,
"8k": 0x05,
"16k": 0x06,
"32k": 0x07
]
@command_write_lock_disable_once 0xC5
@doc """
Opens an I2C connection and resets the configuration.
The address can be determined by consulting page 9, table 1 in the data-sheet.
## Examples
iex> IS31FL3733.open("i2c-1", 0x50)
#IS31FL3733<"i2c-1@0x50">
"""
@spec open(bus_name :: binary() | charlist(), address :: I2C.address()) :: t()
def open(bus_name, address) do
case @i2c.open(bus_name) do
{:ok, bus} ->
state = %__MODULE__{
address: address,
bus: bus,
bus_name: bus_name,
page: 0,
config: IS31FL3733.Config.default()
}
reset(state)
{:error, reason} ->
raise inspect(reason)
end
end
@doc """
Closes an I2C connection.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.close(ic)
:ok
"""
@spec close(state :: t()) :: :ok
def close(state), do: @i2c.close(state.bus)
@doc """
Sets the sync mode.
The default sync mode is `:single`.
Modes:
* `:single` - A single IC controlling a 12x16 matrix of LEDs
* `:primary` - This IC is the primary among a set of others and supplies the
clock signal.
* `:secondary` - This IC is a secondary and receives its clock signal from
the primary.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_sync_mode(ic, :primary)
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_sync_mode(state :: t(), sync_mode :: sync_mode()) :: t()
def set_sync_mode(state, sync_mode) when sync_mode in ~w(single primary secondary)a do
state = %{state | config: %{state.config | sync_mode: sync_mode}}
case write_config(state) do
:ok -> state
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Sets the LED mode.
The default mode is `:pwm`.
Modes:
* `:breathing` - Use the auto-breath feature
* `:pwm` - Manually set the PWM of each LED
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_led_mode(ic, :pwm)
#IS31FL3733<"i2c-1@0x50">
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_led_mode(ic, :breathing)
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_led_mode(state :: t(), led_mode :: led_mode()) :: t()
def set_led_mode(state, :breathing) do
state = %{state | config: %{state.config | breathing: true}}
case write_config(state) do
:ok -> state
{:error, reason} -> raise inspect(reason)
end
end
def set_led_mode(state, :pwm) do
state = %{state | config: %{state.config | breathing: false}}
case write_config(state) do
:ok -> state
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Enables software shutdown, causing all LEDs to be turned off.
Software shutdown is enable by default.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.enable_software_shutdown(ic)
#IS31FL3733<"i2c-1@0x50">
"""
@spec enable_software_shutdown(state :: t()) :: t()
def enable_software_shutdown(state) do
state = %{state | config: %{state.config | software_shutdown: true}}
case write_config(state) do
:ok -> state
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Disables software shutdown, allowing LEDs to be turned on.
Software shutdown is enable by default.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.disable_software_shutdown(ic)
#IS31FL3733<"i2c-1@0x50">
"""
@spec disable_software_shutdown(state :: t()) :: t()
def disable_software_shutdown(state) do
state = %{state | config: %{state.config | software_shutdown: false}}
case write_config(state) do
:ok -> state
{:error, reason} -> raise inspect(reason)
end
end
defp trigger_open_short_detection(state) do
state = %{state | config: %{state.config | trigger_open_short_detection: true}}
with :ok <- write_config(state) do
# trigger open short detection resets back to off as soon as you use it.
{:ok, %{state | config: %{state.config | trigger_open_short_detection: false}}}
end
end
defp write_config(state) do
with {:ok, state} <- set_page(state, @page[:function]) do
data = IS31FL3733.Config.encode(state.config)
write(state, @register[:function][:configuration], data)
end
end
@doc """
Sets the global current control of the CSx pins.
See page 18, table 14 in the data-sheet for details on how this affects the
current output of the CSx pins.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_global_current_control(ic, 0x3C)
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_global_current_control(state :: t(), value :: current_control_value()) :: t()
def set_global_current_control(state, value) when value in 0x00..0xFF do
with {:ok, state} <- set_page(state, @page[:function]),
:ok <- write(state, @register[:function][:global_current_control], value) do
state
else
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Sets the on/off state of individual LEDs.
Each register controls 8 LEDs (one byte, each bit turns one LED on or off).
E.g.: register 0x00 addresses row SW1, columns CS1 through CS8, and register
0x01 addresses row SW1, columns CS9 through CS16, and so on.
You can send more than one byte to the register, and each subsequent byte will
internally increment the register by one. This allows you to set the on/off
state of the whole matrix with one call.
See page 14, table 6 in the data-sheet for details on how to address each LED.
## Examples
iex> # each bit is one LED, 24 * 8 == 12 * 16
...> led_state_data = String.duplicate(<<255>>, 24)
...> # turns on all LEDs in the matrix:
...> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_led_on_off(ic, 0x00, led_state_data)
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_led_on_off(state :: t(), start_register :: led_on_off_register(), data :: binary()) ::
t()
def set_led_on_off(state, start_register, data) do
with :ok <- validate_register(start_register, @register[:led_on_off][:on_off]),
max_writable_bytes = 0x17 - start_register + 1,
:ok <- validate_byte_size(data, max_writable_bytes),
{:ok, state} <- set_page(state, @page[:led_on_off]),
:ok <- write(state, start_register, data) do
state
else
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Sets the PWM value of individual LEDs.
Each register controls 1 LED (one byte, gives possible values 0..255). E.g.
register 0x00 addresses row SW1, column CS1, and register 0x01 addresses row
SW1, column CS2, and so on.
You can send more than one byte to the register, and each subsequent byte will
internally increment the register by one. This allows you to set the PWM
values of the whole matrix with one call.
See page 15, figure 9 in the data-sheet for details on how to address each
LED.
## Examples
iex> # each byte is one LED
...> led_pwm_data = String.duplicate(<<255>>, 16 * 12)
...> # sets the PWM value to max for all LEDs:
...> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_led_pwm(ic, 0x00, led_pwm_data)
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_led_pwm(state :: t(), start_register :: led_pwm_register(), data :: binary()) :: t()
def set_led_pwm(state, start_register, data) do
with :ok <- validate_register(start_register, @register[:led_pwm]),
max_writable_bytes = 0xBF - start_register + 1,
:ok <- validate_byte_size(data, max_writable_bytes),
{:ok, state} <- set_page(state, @page[:led_pwm]),
:ok <- write(state, start_register, data) do
state
else
{:error, reason} -> raise inspect(reason)
end
end
defp validate_register(register, register_range) do
if register in register_range do
:ok
else
{:error, :invalid_register}
end
end
defp validate_byte_size(led_data, max_writable_bytes) do
if byte_size(led_data) <= max_writable_bytes do
:ok
else
{:error, :too_much_data}
end
end
@doc """
Sets the internal pull-up resistor for the SWy pins.
By default, there is no pull-up resistor set.
Resistor values:
* :none
* :"500"
* :"1k"
* :"2k"
* :"4k"
* :"8k"
* :"16k"
* :"32k"
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_swy_pull_up_resistor(ic, :"32k")
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_swy_pull_up_resistor(state :: t(), resistor :: resistor()) :: t()
def set_swy_pull_up_resistor(state, resistor) do
with {:ok, resistor_value} <- validate_resistor(resistor),
{:ok, state} <- set_page(state, @page[:function]),
:ok <- write(state, @register[:function][:swy_pull_up_resistor], resistor_value) do
state
else
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Sets the internal pull-down resistor for the CSx pins.
By default, there is no pull-down resistor set.
Resistor values:
* :none
* :"500"
* :"1k"
* :"2k"
* :"4k"
* :"8k"
* :"16k"
* :"32k"
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.set_csx_pull_down_resistor(ic, :"32k")
#IS31FL3733<"i2c-1@0x50">
"""
@spec set_csx_pull_down_resistor(state :: t(), resistor :: resistor()) :: t()
def set_csx_pull_down_resistor(state, resistor) do
with {:ok, resistor_value} <- validate_resistor(resistor),
{:ok, state} <- set_page(state, @page[:function]),
:ok <- write(state, @register[:function][:csx_pull_down_resistor], resistor_value) do
state
else
{:error, reason} -> raise inspect(reason)
end
end
defp validate_resistor(resistor) do
case Keyword.get(@resistor, resistor) do
nil -> {:error, :invalid_resistor}
value -> {:ok, value}
end
end
@doc """
Resets the internal state and configuration to defaults.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> IS31FL3733.reset(ic)
#IS31FL3733<"i2c-1@0x50">
"""
@spec reset(state :: t()) :: t()
def reset(state) do
with {:ok, state} <- set_page(state, @page[:function]),
{:ok, _result} <- read(state, @register[:function][:reset], 1) do
# performing a reset sets the page back to the default page
%{state | page: @page[:led_on_off], config: IS31FL3733.Config.default()}
else
{:error, reason} -> raise inspect(reason)
end
end
@doc """
Reports open LEDs.
This will return a report of which LEDs are open in the circuit, i.e. missing
LEDs.
The report is returned as a binary, where each byte represents the open state
of 8 LEDs. The arrangements follow the same structure as the LED on/off
states.
See page 14, table 6 in the data-sheet for details.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> {_ic, report} = IS31FL3733.report_open_leds(ic)
...> report
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
"""
@spec report_open_leds(state :: t()) :: {t(), binary()}
def report_open_leds(state) do
with {:ok, state} <- trigger_open_short_detection(state),
:ok <- :timer.sleep(10),
{:ok, state} <- set_page(state, @page[:led_on_off]),
{:ok, result} <- read(state, @register[:led_on_off][:open].first, 24) do
{state, result}
end
end
@doc """
Reports short LEDs.
This will return a report of which LEDs are shorted in the circuit.
The report is returned as a binary, where each byte represents the short state
of 8 LEDs. The arrangements follow the same structure as the LED on/off
states.
See page 14, table 6 in the data-sheet for details.
## Examples
iex> ic = IS31FL3733.open("i2c-1", 0x50)
...> {_ic, report} = IS31FL3733.report_short_leds(ic)
...> report
<<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0>>
"""
@spec report_short_leds(state :: t()) :: {t(), binary()}
def report_short_leds(state) do
with {:ok, state} <- trigger_open_short_detection(state),
:ok <- :timer.sleep(10),
{:ok, state} <- set_page(state, @page[:led_on_off]),
{:ok, result} <- read(state, @register[:led_on_off][:short].first, 24) do
{state, result}
end
end
defp set_page(%{page: page} = state, page), do: {:ok, state}
defp set_page(state, page) do
with :ok <- unlock_command_register(state),
:ok <- write(state, @register[:command], page) do
{:ok, %{state | page: page}}
end
end
defp unlock_command_register(state),
do: write(state, @register[:command_write_lock], @command_write_lock_disable_once)
defp read(state, register, bytes),
do: @i2c.write_read(state.bus, state.address, <<register>>, bytes)
defp write(state, register, value) when is_integer(value),
do: write(state, register, <<value>>)
defp write(state, register, data) when is_binary(data),
do: @i2c.write(state.bus, state.address, <<register>> <> data)
end