162 lines
6.3 KiB
Markdown
162 lines
6.3 KiB
Markdown
|
title: HD44780 LCD with OCaml
|
||
|
tags: electronics, ocaml
|
||
|
date: 2019-08-28 00:00
|
||
|
---
|
||
|
|
||
|
Some time ago I've purchased an [Adafruit HD44780
|
||
|
LCD](https://www.adafruit.com/product/181) as a fancy accessory for
|
||
|
playing around with Raspberry Pi. It was lying around in my office for
|
||
|
some time until recently. I finally got around to putting it together
|
||
|
(many thanks to my office mate Carlo for helping me solder and wire the
|
||
|
thing).
|
||
|
|
||
|
It came with a [Python
|
||
|
library](https://github.com/adafruit/Adafruit_CircuitPython_CharLCD)
|
||
|
for controlling it via a high-level API. However, I don't really like
|
||
|
Python, and I really don't like the design of CircuitPython. So I
|
||
|
decided to write an interface to the LCD in OCaml, that I originally
|
||
|
wanted to use on a Raspberry Pi (using the bindings to the WiringPi
|
||
|
library).
|
||
|
|
||
|
The code can be found in [my fork of the ocaml-wiringpi repository](https://github.com/co-dan/ocaml-wiringpi/tree/lcd_lwt/examples) under the `lcd_lwt` branch.
|
||
|
It mainly consists of two parts:
|
||
|
the low-level interface for writing data to the display and invoking
|
||
|
some operations, and a high-level interface powered by a "cursor"
|
||
|
module.
|
||
|
|
||
|
## The basics
|
||
|
|
||
|
The first is a low-level API that controls the display using 4 data
|
||
|
pins (D4-D7) and two special "control" pins. The first control pin is EN
|
||
|
("enable pin") which is used to signal the start of data read/write
|
||
|
operations. The RS ("register select pin") is used as a flag for
|
||
|
selecting which registers to address. The low value denotes the
|
||
|
instruction register, and the high value denotes the data register.
|
||
|
|
||
|
For example, to clear the display (instruction 0x01 = 0b00000001) one would send the following signals
|
||
|
to the LCD:
|
||
|
|
||
|
1. send 0 to RS
|
||
|
2. send 0, 0, 0, 0 to D4, D5, D6, D7, resp -- the four upper bits in reverse order
|
||
|
3. send 0, 1, 0 to EN -- signal that the first four bits have been sent
|
||
|
4. send 1, 0, 0, 0 to D4, D5, D6, D7, resp -- the four lower bits in reverse order
|
||
|
3. send 0, 1, 0 to EN -- signal that the last four bits have been sent
|
||
|
|
||
|
Internally, this sequence of signals stores 0b00000001 in the instruction register IR.
|
||
|
|
||
|
Instead of sending the instructions to the LCD, we can instead send
|
||
|
data, which ends up in the display data RAM (DDRAM). Suppose we want
|
||
|
to display a character 'a' (ascii 97 = 0b1100001). We then do the
|
||
|
following sequence of operations:
|
||
|
|
||
|
1. send 1 to RS -- address the data register
|
||
|
2. send 0, 0, 1, 1 to D4, D5, D6, D7, resp -- the four upper bits in reverse order
|
||
|
3. send 0, 1, 0 to EN -- signal that the first four bits have been sent
|
||
|
4. send 0, 0, 0, 1 to D4, D5, D6, D7, resp -- the four lower bits in reverse order
|
||
|
3. send 0, 1, 0 to EN -- signal that the last four bits have been sent
|
||
|
|
||
|
We implement this operation of writing 8 bits to either the
|
||
|
instruction register or the data register as the following function:
|
||
|
|
||
|
```ocaml
|
||
|
write8 : Lcd.t -> ?char_mode:bool -> char -> unit
|
||
|
```
|
||
|
|
||
|
where `char_mode` determines whether we write the data to the data
|
||
|
register (`true`) or to the instruction register (`false`).
|
||
|
|
||
|
To write out a string to be displayed we can use `Bytes.iter`:
|
||
|
|
||
|
```ocaml
|
||
|
let write_bytes lcd bts =
|
||
|
Bytes.iter (fun c -> write8 lcd c ~char_mode:true) bts
|
||
|
```
|
||
|
|
||
|
The location in which we store the data is determined by the address
|
||
|
counter, which is automatically incremented after each write. This
|
||
|
means that we can just send a sequence of characters sequentially,
|
||
|
without touching the address counter ourselves.
|
||
|
|
||
|
## Operations with arguments
|
||
|
|
||
|
The way that operations with arguments are typically represented is by
|
||
|
using e.g. 4 bits to denote the operation and 4 bits to denote the
|
||
|
arguments. For instance, to activate the 2 line mode, we invoke the
|
||
|
"function set" operation which requires the upper 4 bits to be `0010`,
|
||
|
with the argument "2 line mode" which requires the lower 4 bits to be
|
||
|
`1000`. To obtain the code for the whole operation we just take the
|
||
|
bitwise OR:
|
||
|
|
||
|
```
|
||
|
0b0000 1000 -- 2 line mode
|
||
|
0b0010 0000 -- function set
|
||
|
___________
|
||
|
0b0010 1000
|
||
|
```
|
||
|
|
||
|
This translates to the following mini program in OCaml:
|
||
|
|
||
|
```
|
||
|
let _lcd_2line = (0x08) in
|
||
|
let _lcd_functionset = (0x20) in
|
||
|
write8_unsafe lcd (_lcd_2line lor _lcd_functionset);
|
||
|
```
|
||
|
|
||
|
Let us a consider a simple example: shifting the characters.
|
||
|
By default, the data that is actually displayed on the LCD is taken
|
||
|
from the addresses 0x0..0x7 and 0x40..0x47 for the first and the
|
||
|
second rows, resp. If we want to display further characters we can use
|
||
|
this shift operation. (See FIGURE 5 in the docs).
|
||
|
|
||
|
To do this we invoke the "cursor/display shift" operation settin the
|
||
|
appropriate bits for moving the display and the move direction:
|
||
|
|
||
|
```
|
||
|
let _lcd_cursorshift = (0x10)
|
||
|
(* 00010000 *)
|
||
|
let _lcd_displaymove = (0x08)
|
||
|
(* 00001000 *)
|
||
|
let _lcd_moveright = (0x04)
|
||
|
(* 00000100 *)
|
||
|
(* -------- *)
|
||
|
(* 00011100 *)
|
||
|
|
||
|
let shift_right lcd =
|
||
|
write8_unsafe lcd (_lcd_cursorshift lor _lcd_displaymove lor _lcd_moveright)
|
||
|
```
|
||
|
|
||
|
## Higher-level interface
|
||
|
|
||
|
We can provide a slightly higher-level interface by keeping track of the cursor (and some other settings) in the program.
|
||
|
|
||
|
```ocaml
|
||
|
type Cursor.t = { x: int; y: int; visible: bool; blink: bool; _lcd: mono_lcd }
|
||
|
```
|
||
|
|
||
|
A /cursor/ is a record that combines the underlying `mono_lcd` type (which stores the pin layout), the current position of the cursor `x, y` and some settings (whether the cursor should be visible and blinking).
|
||
|
Then all the operation on the cursor are just functions `Cursor.t -> Cursor.t`.
|
||
|
For example, a function that write out a string takes in a cursor, writes the underlying bytes using `write_bytes` and updates the cursor position.
|
||
|
A function that sets the blinking flag writes out the desired bytes (as descirbed in the "operations with arguments") and updates the boolean flag.
|
||
|
Combination of those operations are just function composition:
|
||
|
|
||
|
```ocaml
|
||
|
let (|>) (m : Cursor.t) (f : Cursor.t -> 'b) = f m
|
||
|
let display_lines lcd l1 l2 =
|
||
|
let col_shift = 3 in
|
||
|
clear lcd;
|
||
|
let cur = Cursor.of_lcd lcd in
|
||
|
let open Cursor in
|
||
|
cur
|
||
|
|> set_visible false
|
||
|
|> set_blink false
|
||
|
|> set_position col_shift 0
|
||
|
|> write_string l1 ~wrap:false
|
||
|
|> set_position col_shift 1
|
||
|
|> write_string l2 ~wrap:false
|
||
|
```
|
||
|
|
||
|
## Concluding
|
||
|
|
||
|
You can find more usage examples in the [lcd_lwt.ml file](https://github.com/co-dan/ocaml-wiringpi/blob/lcd_lwt/examples/lcd_lwt.ml).
|
||
|
Overall, I thought that OCaml was a good fit for this kind of programming, and the type system helps out a bit!
|