wafer/lib/wafer/registers.ex
James Harton 6123ac8493
All checks were successful
continuous-integration/drone/push Build is passing
feat!: Remove ElixirALE support. (#67)
Reviewed-on: #67
Co-authored-by: James Harton <james@harton.nz>
Co-committed-by: James Harton <james@harton.nz>
2024-04-24 14:20:57 +12:00

254 lines
8.3 KiB
Elixir

defmodule Wafer.Registers do
@moduledoc """
This module provides helpful macros for specifying the registers used to
communicate with your device.
This can be a massive time saver, and means you can basically just copy them
straight out of the datasheet.
See the documentation for `defregister/4` for more information.
"""
alias Wafer.Chip
alias Wafer.Conn
@type register_name :: atom
@type access_mode :: :ro | :rw | :wo
@type bytes :: non_neg_integer
@doc false
defmacro __using__(_opts) do
quote do
import Wafer.Registers
end
end
@doc ~S"""
Define functions for interacting with a device register.
## Parameters
- `name` - name of the register.
- `register_address` - the address of the register.
- `mode` the access mode of the register.
- `bytes` the number of bytes in the register.
## Examples
### Read-only registers
Define a read-only register named `status` at address `0x03` which is a single
byte wide:
iex> defregister(:status, 0x03, :ro, 1)
This will define the following function along with documentation and
typespecs:
```elixir
def read_status(conn), do: Chip.read_register(conn, 0x03, 1)
```
### Write-only registers
Define a write-only register named `int_en` at address `0x02` which is 2 bytes
wide:
iex> defregister(:int_en, 0x02, :wo, 2)
This will define the following functions along with documentation and
typespecs:
```elixir
def write_int_en(conn, data)
when is_binary(data) and byte_size(data) == 2,
do: Chip.write_register(conn, 0x2, data)
def write_int_en(_conn, data),
do: {:error, "Argument error: #{inspect(data)}"}
```
### Read-write registers.
Define a read-write register named `config` at address `0x01`.
iex> defregister(:config, 0x01, :rw, 1)
In addition to defining `read_config/1` and `write_config/2` as per the
examples above it will also generate the following functions along with
documentation and typespecs:
```elixir
def swap_config(conn, data)
when is_binary(data) and byte_size(data) == 1,
do: Chip.swap_register(conn, 0x01, data)
def swap_config(_conn, data),
do: {:error, "Argument error: #{inspect(data)}"}
def update_config(conn, callback)
when is_function(callback, 1) do
with {:ok, data} <- Chip.read_register(conn, 0x01, 1),
new_data when is_binary(new_data) and byte_size(new_data) == 1 <- callback.(data),
{:ok, conn} <- Chip.write_register(conn, 0x01, new_data),
do: {:ok, conn}
end
def update_config(_conn, _callback),
do: {:error, "Argument error: callback should be an arity 1 function"}
```
"""
@spec defregister(atom, non_neg_integer, :ro | :rw | :wo, non_neg_integer) :: Macro.t()
defmacro defregister(name, register_address, :ro, bytes)
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
is_integer(bytes) and bytes >= 0 do
empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)
quote do
@doc """
Read the contents of the `#{unquote(name)}` register.
## Example
iex> read_#{unquote(name)}(conn)
{:ok, <<#{unquote(empty_bytes)}>>}
"""
@spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
def unquote(:"read_#{name}")(conn),
do: Chip.read_register(conn, unquote(register_address), unquote(bytes))
end
end
defmacro defregister(name, register_address, :wo, bytes)
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
is_integer(bytes) and bytes >= 0 do
empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)
quote do
@doc """
Write new contents to the `#{unquote(name)}` register.
## Example
iex> write_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
{:ok, _conn}
"""
@spec unquote(:"write_#{name}")(Conn.t(), data :: binary) ::
{:ok, Conn.t()} | {:error, reason :: any}
def unquote(:"write_#{name}")(conn, data)
when is_binary(data) and byte_size(data) == unquote(bytes),
do: Chip.write_register(conn, unquote(register_address), data)
def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
end
end
# credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
defmacro defregister(name, register_address, :rw, bytes)
when is_atom(name) and is_integer(register_address) and register_address >= 0 and
is_integer(bytes) and bytes >= 0 do
empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)
bits = bytes * 8
quote do
@doc """
Read the contents of the `#{unquote(name)}` register.
## Example
iex> read_#{unquote(name)}(conn)
{:ok, <<#{unquote(empty_bytes)}>>}
"""
@spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
def unquote(:"read_#{name}")(conn),
do: Chip.read_register(conn, unquote(register_address), unquote(bytes))
@doc """
Write new contents to the `#{unquote(name)}` register.
## Example
iex> write_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
{:ok, _conn}
"""
@spec unquote(:"write_#{name}")(Conn.t(), data :: binary) ::
{:ok, Conn.t()} | {:error, reason :: any}
def unquote(:"write_#{name}")(conn, data)
when is_binary(data) and byte_size(data) == unquote(bytes),
do: Chip.write_register(conn, unquote(register_address), data)
def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
@doc """
Swap the contents of the `#{unquote(name)}` register.
Reads the contents of the register, then replaces it, returning the
previous contents. Some drivers may implement this atomically.
## Example
iex> swap_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
{:ok, <<#{unquote(empty_bytes)}>>, _conn}
"""
@spec unquote(:"swap_#{name}")(Conn.t(), data :: binary) ::
{:ok, Conn.t()} | {:error, reason :: any}
def unquote(:"swap_#{name}")(conn, data)
when is_binary(data) and byte_size(data) == unquote(bytes),
do: Chip.swap_register(conn, unquote(register_address), data)
def unquote(:"swap_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
@doc """
Update the contents of the `#{unquote(name)}` register using a
transformation function.
## Example
iex> transform = fn <<data::size(#{unquote(bits)})>> -> <<(data * 2)::size(#{unquote(bits)})>> end
...> update_#{unquote(name)}(conn, transform)
{:ok, _conn}
"""
@spec unquote(:"update_#{name}")(
Conn.t(),
(<<_::_*unquote(bits)>> -> <<_::_*unquote(bits)>>)
) :: {:ok, Conn.t()} | {:error, reason :: any}
def unquote(:"update_#{name}")(conn, callback) when is_function(callback, 1) do
with {:ok, old_data} <-
Chip.read_register(conn, unquote(register_address), unquote(bytes)),
new_data when is_binary(new_data) and byte_size(new_data) == unquote(bytes) <-
callback.(old_data),
do: Chip.write_register(conn, unquote(register_address), new_data)
end
def unquote(:"update_#{name}")(_conn, _callback),
do: {:error, "Argument error: callback should be an arity 1 function"}
end
end
@doc """
Define functions for interacting with a device register with common defaults.
## Examples
When specified with an access mode, assumes a 1 byte register:
iex> defregister(:status, 0x03, :ro)
When specified with a byte size, assumes a `:rw` register:
iex> defregister(:config, 0x02, 2)
"""
@spec defregister(atom, non_neg_integer, :ro | :rw | :wo | non_neg_integer) :: Macro.t()
defmacro defregister(name, register_address, mode) when mode in ~w[ro rw wo]a do
quote do
defregister(unquote(name), unquote(register_address), unquote(mode), 1)
end
end
defmacro defregister(name, register_address, bytes) when is_integer(bytes) and bytes >= 0 do
quote do
defregister(unquote(name), unquote(register_address), :rw, unquote(bytes))
end
end
end