webcc/content/adafruit-lcd.md

162 lines
6.3 KiB
Markdown
Raw Normal View History

2020-08-28 18:21:05 +02:00
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!