LiquidCrystal robustness and test (#3369)

* LiquidCrystal I2C 4-bit robustness

- Fix up some formatting
- Initialization is now more conformant with the datasheet.
- Read-backs don't needlessly (or erroneously!) store back

While here, document some unexpected behaviour of read-back commands.

* liquidcrystal i2c 4bit NTest
This commit is contained in:
Nathaniel Wesley Filardo 2021-01-13 02:36:00 +00:00 committed by GitHub
parent 109f500be7
commit c3dd27cf9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 137 additions and 37 deletions

View File

@ -237,6 +237,12 @@ liquidcrystal:blink(true)
## liquidcrystal.busy ## liquidcrystal.busy
Get busy status of the LCD. When using GPIO backend without `rw` argument specification function does nothing. Get busy status of the LCD. When using GPIO backend without `rw` argument specification function does nothing.
!!! note
At least some HD44780s and/or interfaces have been observed to count polling
the busy flag as grounds for incrementing their position in memory. This is
mysterious, but software should restore the position after observing that the
busy flag is clear.
#### Syntax #### Syntax
`liquidcrystal.busy(self)` `liquidcrystal.busy(self)`
@ -429,6 +435,11 @@ liquidcrystal:leftToRight()
## liquidcrystal.position ## liquidcrystal.position
Get current position of the cursor. Position is 0 indexed. When using GPIO backend without `rw` argument specification function does nothing. Get current position of the cursor. Position is 0 indexed. When using GPIO backend without `rw` argument specification function does nothing.
!!! note
At least some HD44780s and/or interfaces have been observed to count reading
the position as grounds for incrementing their position in memory. This is
mysterious, but software likely intends to restore the position anyway.
#### Syntax #### Syntax
`liquidcrystal.position(self)` `liquidcrystal.position(self)`

View File

@ -22,27 +22,27 @@ return function(bus_args)
-- The onus is on us to maintain the backlight state -- The onus is on us to maintain the backlight state
local backlight = true local backlight = true
local function send4bitI2C(value, rs_en, rw_en, read) local function exchange(data, read)
local function exchange(data, unset_read)
local rv = data local rv = data
i2c.start(busid) i2c.start(busid)
i2c.address(busid, busad, i2c.TRANSMITTER) i2c.address(busid, busad, i2c.TRANSMITTER)
i2c.write(busid, bit.set(data, en)) i2c.write(busid, bit.set(data, en)) -- set data with en
if read then if read then
i2c.start(busid) i2c.start(busid) -- read 1 byte and go back to tx mode
i2c.address(busid, busad, i2c.RECEIVER) i2c.address(busid, busad, i2c.RECEIVER)
rv = i2c.read(busid, 1):byte(1) rv = i2c.read(busid, 1):byte(1)
i2c.start(busid) i2c.start(busid)
i2c.address(busid, busad, i2c.TRANSMITTER) i2c.address(busid, busad, i2c.TRANSMITTER)
if unset_read then data = bit.bor(bit.bit(rs),
bit.bit(rw),
backlight and bit.bit(bl) or 0) end
i2c.write(busid, bit.set(data, en))
end end
i2c.write(busid, bit.clear(data, en)) i2c.write(busid, data) -- lower en
i2c.stop(busid) i2c.stop(busid)
return rv return rv
end end
local function send4bitI2C(value, rs_en, rw_en)
local meta = bit.bor(rs_en and bit.bit(rs) or 0,
rw_en and bit.bit(rw) or 0,
backlight and bit.bit(bl) or 0)
local lo = bit.bor(bit.isset(value, 0) and bit.bit(d4) or 0, local lo = bit.bor(bit.isset(value, 0) and bit.bit(d4) or 0,
bit.isset(value, 1) and bit.bit(d5) or 0, bit.isset(value, 1) and bit.bit(d5) or 0,
bit.isset(value, 2) and bit.bit(d6) or 0, bit.isset(value, 2) and bit.bit(d6) or 0,
@ -51,11 +51,8 @@ return function(bus_args)
bit.isset(value, 5) and bit.bit(d5) or 0, bit.isset(value, 5) and bit.bit(d5) or 0,
bit.isset(value, 6) and bit.bit(d6) or 0, bit.isset(value, 6) and bit.bit(d6) or 0,
bit.isset(value, 7) and bit.bit(d7) or 0) bit.isset(value, 7) and bit.bit(d7) or 0)
local cmd = bit.bor(rs_en and bit.bit(rs) or 0, hi = exchange(bit.bor(meta, hi), rw_en)
rw_en and bit.bit(rw) or 0, lo = exchange(bit.bor(meta, lo), rw_en)
backlight and bit.bit(bl) or 0)
hi = exchange(bit.bor(cmd, hi), false)
lo = exchange(bit.bor(cmd, lo), true)
return bit.bor(bit.lshift(bit.isset(lo, d4) and 1 or 0, 0), return bit.bor(bit.lshift(bit.isset(lo, d4) and 1 or 0, 0),
bit.lshift(bit.isset(lo, d5) and 1 or 0, 1), bit.lshift(bit.isset(lo, d5) and 1 or 0, 1),
bit.lshift(bit.isset(lo, d6) and 1 or 0, 2), bit.lshift(bit.isset(lo, d6) and 1 or 0, 2),
@ -66,36 +63,45 @@ return function(bus_args)
bit.lshift(bit.isset(hi, d7) and 1 or 0, 7)) bit.lshift(bit.isset(hi, d7) and 1 or 0, 7))
end end
-- init sequence from datasheet -- init sequence from datasheet (Figure 24)
send4bitI2C(0x33, false, false, false) local function justsend(what)
send4bitI2C(0x32, false, false, false) i2c.start(busid)
i2c.address(busid, busad, i2c.TRANSMITTER)
i2c.write(busid, bit.set(what, en))
i2c.write(busid, what)
i2c.stop(busid)
end
local three = bit.bor(bit.bit(d4), bit.bit(d5))
justsend(three)
tmr.delay(5)
justsend(three)
tmr.delay(1)
justsend(three)
tmr.delay(1)
justsend(bit.bit(d5))
-- we are now primed for the FUNCTIONSET command from the liquidcrystal ctor
-- Return backend object -- Return backend object
return { return {
fourbits = true, fourbits = true,
command = function (_, cmd) command = function (_, cmd)
return send4bitI2C(cmd, false, false, false) return send4bitI2C(cmd, false, false)
end, end,
busy = function(_) busy = function(_)
local rv = send4bitI2C(0xff, false, true, true) return bit.isset(send4bitI2C(0xff, false, true), 7)
send4bitI2C(bit.bor(0x80, bit.clear(rv, 7)), false, false, false)
return bit.isset(rv, 7)
end, end,
position = function(_) position = function(_)
local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7) return bit.clear(send4bitI2C(0xff, false, true), 7)
send4bitI2C(bit.bor(0x80, rv), false, false, false)
return rv
end, end,
write = function(_, value) write = function(_, value)
return send4bitI2C(value, true, false, false) return send4bitI2C(value, true, false)
end, end,
read = function(_) read = function(_)
return send4bitI2C(0xff, true, true, true) return send4bitI2C(0xff, true, true)
end, end,
backlight = function(_, on) backlight = function(_, on)
backlight = on backlight = on
local rv = bit.clear(send4bitI2C(0xff, false, true, true), 7) send4bitI2C(0, false, false) -- No-op
send4bitI2C(bit.bor(0x80, rv), false, false, false)
return on return on
end, end,
} }

View File

@ -0,0 +1,83 @@
-- Run LiquidCrystal through some basic tests. Requires `liquidcrystal.lua`
-- and `l2-i2c4bit.lua` available available to `require`.
--
-- This file ought to be named "NTest_liquidcrystal_i2c4bit" or something,
-- but it has its current name due to our default SPIFFS filename length limit.
local N = ...
N = (N or require "NTest")("liquidcrystal-i2c4bit")
local metalcd
local metaback
local backend
local lcd
collectgarbage()
print("HEAP init", node.heap())
metalcd = require "liquidcrystal"
collectgarbage() print("HEAP constructor imported ", node.heap())
metaback = require "lc-i2c4bit"
collectgarbage() print("HEAP backend imported ", node.heap())
backend = metaback({
address = 0x27,
id = 0,
speed = i2c.SLOW,
sda = 2,
scl = 1,
})
collectgarbage() print("HEAP backend built", node.heap())
lcd = metalcd(backend, false, true, 20)
collectgarbage() print("HEAP lcd built", node.heap())
print("waiting for LCD to be unbusy before testing...")
while lcd:busy() do end
N.test("custom character", function()
local glyph = { 0x1F, 0x15, 0x1B, 0x15, 0x1F, 0x10, 0x10, 0x0 }
lcd:customChar(0, glyph)
ok(eq(glyph,lcd:readCustom(0)), "read back")
end)
N.test("draw and readback", function()
lcd:cursorMove(0)
lcd:write("abc")
lcd:cursorMove(10,1)
lcd:write("de")
lcd:cursorMove(10,2)
lcd:write("fg")
lcd:cursorMove(12,3)
lcd:write("hi\000")
lcd:cursorMove(18,4)
lcd:write("jk")
lcd:home() ok(eq(0x61, lcd:read()), "read back 'a'")
ok(eq(0x62, lcd:read()), "read back 'b'")
lcd:cursorMove(11,1) ok(eq(0x65, lcd:read()), "read back 'e'")
lcd:cursorMove(11,2) ok(eq(0x67, lcd:read()), "read back 'g'")
lcd:cursorMove(13,3) ok(eq(0x69, lcd:read()), "read back 'i'")
lcd:cursorMove(14,3) ok(eq(0x00, lcd:read()), "read back 0" )
lcd:cursorMove(19,4) ok(eq(0x6B, lcd:read()), "read back 'k'")
end)
N.test("update home", function()
lcd:home() lcd:write("l")
lcd:home() ok(eq(0x6C, lcd:read()))
end)
N.testasync("clear", function(next)
-- clear and poll busy
lcd:clear()
tmr.create():alarm(5, tmr.ALARM_SEMI, function(tp)
if lcd:busy() then tp:start() else next() end
end)
lcd:home() -- work around busy polling incrementing position (XXX)
ok(eq(0x20, lcd:read()), "is space")
ok(eq(1, lcd:position())) -- having just read 1 from home, we should be at 1
end)