Skip to content

Instantly share code, notes, and snippets.

@strayer
Created September 19, 2018 11:28
Show Gist options
  • Save strayer/c2b6c39f6939b4d14b7f66d25e5807eb to your computer and use it in GitHub Desktop.
Save strayer/c2b6c39f6939b4d14b7f66d25e5807eb to your computer and use it in GitHub Desktop.
defmodule ExLCD.HD44780.I2C do
@moduledoc """
**ExLCD.HD44780** is the display driver module for Hitachi
HD44780 type parallel LCD display controller managed display modules.
## Hitachi HD44780 Style Controller (including Sitronix ST7066)
The HD44780 is the most ubiquitous character matrix display controller
but not the only one. It supports a number of standard operations like
moving the cursor, displaying characters and scrolling. It is an 8-bit
parallel interface which can operate in 4-bit mode by sending 2 4-bit
nibbles to make one 8-bit byte.
It supports 208 characters which are compatible with UNICODE single byte
latin characters. The controller character ROM includes a number of non-
standard character glyphs which this driver maps to their multi-byte
UNICODE equivilents automatically. See the character map in this file for
details. There are also 8 user definable character bitmaps mapped to
characters byte codes 0-7.
## Configuration
The start/1 function expects to receive a map of configuration settings
for the display and it's hardware interface. The configuration map is
passed by your application to ExLCD.start_link/1 and then on to this
driver module. Please see ExLCD for details. The following keys are
used by this driver to operate the display:
* *Key* -> **Type(O|R)** -> *Description*
* rs -> integer(R) -> The GPIO pin ID for the RS signal
* en -> integer(R) -> The GPIO pin ID for the EN signal
* d0 -> integer(O) -> The GPIO pin ID for the d0 signal
* d1 -> integer(O) -> The GPIO pin ID for the d1 signal
* d2 -> integer(O) -> The GPIO pin ID for the d2 signal
* d3 -> integer(O) -> The GPIO pin ID for the d3 signal
* d4 -> integer(R) -> The GPIO pin ID for the d4 signal
* d5 -> integer(R) -> The GPIO pin ID for the d5 signal
* d6 -> integer(R) -> The GPIO pin ID for the d6 signal
* d7 -> integer(R) -> The GPIO pin ID for the d7 signal
* rows -> integer(R) -> The number of display rows or lines
* cols -> integer(R) -> The number of display columns
* font_5x10 -> boolean(O) -> Font: true: 5x10, false: 5x8 (default)
O - optional
R - required
Example:
```elixir
config :MyApp, hd44780: %{
rs: 1,
en: 2,
d4: 3,
d5: 4,
d6: 5,
d7: 6,
rows: 2,
cols: 20
}
```
## More Information
For more information about your display and its capabilities here are
a few resources to help you get the most of it:
* Hitachi HD44780 Datasheet
* Wikipedia Entry for HD44780
* ExLCD
* Raspberry Pi Example Application with nerves
"""
use Bitwise
use ExLCD.Driver
use ExLCD.IO
@low 0
@high 1
# Function set flags
@mode_4bit 0x01
@mode_8bit 0x00
@font_5x8 0x00
@font_5x10 0x04
@lines_1 0x00
@lines_2 0x08
# Command flags
@cmd_clear 0x01
@cmd_home 0x02
@cmd_entrymodeset 0x04
@cmd_dispcontrol 0x08
@cmd_cursorshift 0x10
@cmd_functionset 0x20
@cmd_setcgramaddr 0x40
@cmd_setddramaddr 0x80
# Entry mode flags
@entry_left 0x02
@entry_increment 0x01
# Display control flags
@ctl_display 0x04
@ctl_cursor 0x02
@ctl_blink 0x01
# Shift flags
@shift_display 0x08
@shift_right 0x04
# -------------------------------------------------------------------
# CharDisplay.Driver Behaviour
#
@doc false
def start(config) do
init(config)
end
@doc false
def stop(display) do
{:ok, display} = command(display, {:display, :off})
@i2c.release(display[:i2c_pid])
:ok
end
@doc false
def execute do
&command/2
end
# ------------------------------------------------------------------
# Initialization
#
defp init(config) do
# validate and unpack the config
config |> validate_config!()
# I2C is always in 4bit mode
config = %{config | d0: nil}
bits = @mode_4bit
lines =
case config.rows do
1 -> @lines_1
_ -> @lines_2
end
font =
case config[:font_5x10] do
true -> @font_5x10
_ -> @font_5x8
end
starting_function_state = @cmd_functionset ||| bits ||| font ||| lines
display =
Map.merge(config, %{
function_set: starting_function_state,
display_control: @cmd_dispcontrol,
entry_mode: @cmd_entrymodeset,
shift_control: @cmd_cursorshift
})
display
|> start_i2c()
|> poi(bits)
|> set_feature(:function_set)
|> clear()
end
# start ElixirALE.I2C GenServer and store the PID
defp start_i2c(config) do
{:ok, pid} = @i2c.start_link(config[:i2c_bus], config[:i2c_address])
Map.merge(%{i2c_pid: pid}, config)
end
# Software Power On Init (POI) for 4bit operation of HD44780 controller.
# Since the display is initialized more than 50mS after > 4.7V on due to
# OS/BEAM/App boot time this isn't strictly necessary but let's be
# safe and do it anyway.
defp poi(state, @mode_4bit) do
state
|> write_4_bits(0x03)
|> write_4_bits(0x03)
|> write_4_bits(0x03)
|> write_4_bits(0x02)
end
# POI for 8 bit mode
defp poi(state, @mode_8bit) do
state
|> set_feature(:function_set)
|> set_feature(:function_set)
|> set_feature(:function_set)
end
defp validate_config!(config) do
config
end
# -------------------------------------------------------------------
# ExLCD API callback
#
defp command(display, {:clear, _params}) do
clear(display)
{:ok, display}
end
defp command(display, {:home, _params}) do
home(display)
{:ok, display}
end
# translate string to charlist
defp command(display, {:print, content}) do
characters = String.to_charlist(content)
command(display, {:write, characters})
end
defp command(display, {:write, content}) do
content
|> Enum.each(fn x -> write_a_byte(display, x, @high) end)
{:ok, display}
end
defp command(display, {:set_cursor, {row, col}}) do
{:ok, set_cursor(display, {row, col})}
end
defp command(display, {:cursor, :off}) do
{:ok, disable_feature_flag(display, :display_control, @ctl_cursor)}
end
defp command(display, {:cursor, :on}) do
{:ok, enable_feature_flag(display, :display_control, @ctl_cursor)}
end
defp command(display, {:blink, :off}) do
{:ok, disable_feature_flag(display, :display_control, @ctl_blink)}
end
defp command(display, {:blink, :on}) do
{:ok, enable_feature_flag(display, :display_control, @ctl_blink)}
end
defp command(display, {:display, :off}) do
{:ok, disable_feature_flag(display, :display_control, @ctl_display)}
end
defp command(display, {:display, :on}) do
{:ok, enable_feature_flag(display, :display_control, @ctl_display)}
end
defp command(display, {:autoscroll, :off}) do
{:ok, disable_feature_flag(display, :entry_mode, @entry_increment)}
end
defp command(display, {:autoscroll, :on}) do
{:ok, enable_feature_flag(display, :entry_mode, @entry_increment)}
end
defp command(display, {:rtl_text, :on}) do
{:ok, disable_feature_flag(display, :entry_mode, @entry_left)}
end
defp command(display, {:ltr_text, :on}) do
{:ok, enable_feature_flag(display, :entry_mode, @entry_left)}
end
# Scroll the entire display left (-) or right (+)
defp command(display, {:scroll, 0}), do: {:ok, display}
defp command(display, {:scroll, cols}) when cols < 0 do
write_a_byte(display, @cmd_cursorshift ||| @shift_display)
command(display, {:scroll, cols + 1})
end
defp command(display, {:scroll, cols}) do
write_a_byte(display, @cmd_cursorshift ||| @shift_display ||| @shift_right)
command(display, {:scroll, cols - 1})
end
# Scroll(move) cursor right
defp command(display, {:right, 0}), do: {:ok, display}
defp command(display, {:right, cols}) do
write_a_byte(display, @cmd_cursorshift ||| @shift_right)
command(display, {:right, cols - 1})
end
# Scroll(move) cursor left
defp command(display, {:left, 0}), do: {:ok, display}
defp command(display, {:left, cols}) do
write_a_byte(display, @cmd_cursorshift)
command(display, {:left, cols - 1})
end
# Program custom character to CGRAM
defp command(display, {:char, idx, bitmap}) when idx in 0..7 and length(bitmap) === 8 do
write_a_byte(display, @cmd_setcgramaddr ||| idx <<< 3)
for line <- bitmap do
write_a_byte(display, line, @high)
end
{:ok, display}
end
# All other commands are unsupported
defp command(display, _), do: {:unsupported, display}
# -------------------------------------------------------------------
# Low-level device and utility functions
#
defp clear(display) do
display
|> write_a_byte(@cmd_clear)
|> delay(3_000)
end
defp home(display) do
display
|> write_a_byte(@cmd_home)
|> delay(3_000)
end
# DDRAM is organized as two 40 byte rows. In a 2x display the first row
# maps to address 0x00 - 0x27 and the second row maps to 0x40 - 0x67
# in a 4x display rows 0 & 2 are mapped to the first row of DDRAM and
# rows 1 & 3 map to the second row of DDRAM. This means that the rows
# are not contiguous in memory.
#
# row_offsets/1 determines the starting DDRAM address of each display row
# and returns a map for up to 4 rows.
defp row_offsets(cols) do
%{0 => 0x00, 1 => 0x40, 2 => 0x00 + cols, 3 => 0x40 + cols}
end
# Set the DDRAM address corresponding to the {row,col} position
defp set_cursor(display, {row, col}) do
col = min(col, display[:cols] - 1)
row = min(row, display[:rows] - 1)
%{^row => offset} = row_offsets(display[:cols])
write_a_byte(display, @cmd_setddramaddr ||| col + offset)
end
# Switch a register flag bit OFF(0). Return the updated state.
defp disable_feature_flag(state, feature, flag) do
%{state | feature => state[feature] &&& ~~~flag}
|> set_feature(feature)
end
# Switch a register flag bit ON(1). Return the updated state.
defp enable_feature_flag(state, feature, flag) do
%{state | feature => state[feature] ||| flag}
|> set_feature(feature)
end
# Write a feature register to the controller and return the state.
defp set_feature(display, feature) do
display |> write_a_byte(display[feature])
end
# Write a byte to the device
defp write_a_byte(display, byte_to_write, rs_value \\ @low) do
display
|> write_4_bits(rs_value ||| (byte_to_write &&& 0xF0))
|> write_4_bits(rs_value ||| (byte_to_write <<< 4 &&& 0xF0))
end
# Write 4 parallel bits to the device
defp write_4_bits(%{i2c_pid: i2c_pid} = display, bits) do
@i2c.write(i2c_pid, bits)
pulse_en(display, bits)
end
defp pulse_en(%{i2c_pid: i2c_pid} = display, byte) do
@i2c.write(i2c_pid, byte ||| 0x04)
delay(display, 5)
@i2c.write(i2c_pid, byte &&& ~~~0x04)
delay(display, 2)
end
def delay(display, microseconds) do
# Unfortunately, BEAM does not provides microsecond precision
# And if we need waiting, we MUST wait
ms = max(round(microseconds / 1000), 1)
Process.sleep(ms)
display
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment