Skip to content

Instantly share code, notes, and snippets.

@ahmedalhulaibi
Last active October 12, 2021 22:00
Show Gist options
  • Save ahmedalhulaibi/b033cb8d44eaf64d0ffcae77093999cb to your computer and use it in GitHub Desktop.
Save ahmedalhulaibi/b033cb8d44eaf64d0ffcae77093999cb to your computer and use it in GitHub Desktop.
TinyGo Crystal Ball
package main
import (
"errors"
"io"
"machine"
"os"
"time"
)
type tiltSwitch struct {
pin machine.Pin
currentState, previousState bool
}
//
func NewTiltSwitch(pin machine.Pin) *tiltSwitch {
pin.Configure(machine.PinConfig{Mode: machine.PinInput})
return &tiltSwitch{
pin: pin,
}
}
//
func (t *tiltSwitch) Read() bool {
t.previousState = t.currentState
t.currentState = t.pin.Get()
return t.currentState
}
//
func (t *tiltSwitch) Changed() bool {
return t.previousState != t.currentState
}
//
func (t *tiltSwitch) IsOff() bool {
return !t.currentState
}
func main() {
lcd, err := NewGPIO4Bit(
[]machine.Pin{machine.D5, machine.D4, machine.D3, machine.D2},
machine.D11,
machine.D12,
machine.NoPin,
)
if err != nil {
os.Exit(1)
}
lcd.Configure(Config{
Width: int16(16),
Height: int16(2),
})
resetLCD(&lcd)
tiltSwitch := NewTiltSwitch(machine.D6)
led := machine.LED
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
for i := uint(0); ; i++ {
tiltSwitch.Read()
if tiltSwitch.Changed() && tiltSwitch.IsOff() {
reply := i % 7
if reply < 4 {
led.High()
} else {
led.Low()
}
displayAnswer(reply, &lcd, led)
}
if i > 7000 {
i = 0
}
}
}
func displayAnswer(reply uint, lcd *Device, led machine.Pin) {
lcd.ClearDisplay()
lcd.SetCursor(uint8(0), uint8(0))
lcd.Write([]byte("The answer is..."))
lcd.Display()
lcd.SetCursor(uint8(0), uint8(1))
if answer, ok := answers[reply]; ok {
lcd.Write(answer)
} else {
lcd.Write([]byte("No"))
}
lcd.Display()
}
func resetLCD(lcd *Device) {
lcd.ClearDisplay()
lcd.SetCursor(uint8(0), uint8(0))
lcd.Write([]byte("Ask me, I'm a..."))
lcd.Display()
lcd.SetCursor(uint8(0), uint8(1))
lcd.Write([]byte("Crystal Ball!"))
lcd.Display()
}
var answers = map[uint][]byte{
0: []byte("Yes"),
1: []byte("Probably"),
2: []byte("Certainly"),
3: []byte("Outlook good"),
4: []byte("Unsure"),
5: []byte("Ask again"),
6: []byte("No idea"),
7: []byte("No"),
}
// package hd44780 // import "tinygo.org/x/drivers/hd44780"
type GPIO struct {
dataPins []machine.Pin
en machine.Pin
rw machine.Pin
rs machine.Pin
write func(data byte)
read func() byte
}
func newGPIO(dataPins []machine.Pin, en, rs, rw machine.Pin, mode byte) Device {
pins := make([]machine.Pin, len(dataPins))
for i := 0; i < len(dataPins); i++ {
dataPins[i].Configure(machine.PinConfig{Mode: machine.PinOutput})
pins[i] = dataPins[i]
}
en.Configure(machine.PinConfig{Mode: machine.PinOutput})
rs.Configure(machine.PinConfig{Mode: machine.PinOutput})
rw.Configure(machine.PinConfig{Mode: machine.PinOutput})
rw.Low()
gpio := GPIO{
dataPins: pins,
en: en,
rs: rs,
rw: rw,
}
if mode == DATA_LENGTH_4BIT {
gpio.write = gpio.write4BitMode
gpio.read = gpio.read4BitMode
} else {
gpio.write = gpio.write8BitMode
gpio.read = gpio.read8BitMode
}
return Device{
bus: &gpio,
datalength: mode,
}
}
// SetCommandMode sets command/instruction mode
func (g *GPIO) SetCommandMode(set bool) {
if set {
g.rs.Low()
} else {
g.rs.High()
}
}
// WriteOnly is true if you passed rw in as machine.NoPin
func (g *GPIO) WriteOnly() bool {
return g.rw == machine.NoPin
}
// Write writes len(data) bytes from data to display driver
func (g *GPIO) Write(data []byte) (n int, err error) {
if !g.WriteOnly() {
g.rw.Low()
}
for _, d := range data {
g.write(d)
n++
}
return n, nil
}
func (g *GPIO) write8BitMode(data byte) {
g.en.High()
g.setPins(data)
g.en.Low()
}
func (g *GPIO) write4BitMode(data byte) {
g.en.High()
g.setPins(data >> 4)
g.en.Low()
g.en.High()
g.setPins(data)
g.en.Low()
}
// Read reads len(data) bytes from display RAM to data starting from RAM address counter position
// Ram address can be changed by writing address in command mode
func (g *GPIO) Read(data []byte) (n int, err error) {
if len(data) == 0 {
return 0, errors.New("length greater than 0 is required")
}
if g.WriteOnly() {
return 0, errors.New("Read not supported if RW not wired")
}
g.rw.High()
g.reconfigureGPIOMode(machine.PinInput)
for i := 0; i < len(data); i++ {
data[i] = g.read()
n++
}
g.reconfigureGPIOMode(machine.PinInput)
return n, nil
}
func (g *GPIO) read4BitMode() byte {
g.en.High()
data := (g.pins() << 4 & 0xF0)
g.en.Low()
g.en.High()
data |= (g.pins() & 0x0F)
g.en.Low()
return data
}
func (g *GPIO) read8BitMode() byte {
g.en.High()
data := g.pins()
g.en.Low()
return data
}
func (g *GPIO) reconfigureGPIOMode(mode machine.PinMode) {
for i := 0; i < len(g.dataPins); i++ {
g.dataPins[i].Configure(machine.PinConfig{Mode: mode})
}
}
// setPins sets high or low state on all data pins depending on data
func (g *GPIO) setPins(data byte) {
mask := byte(1)
for i := 0; i < len(g.dataPins); i++ {
if (data & mask) != 0 {
g.dataPins[i].High()
} else {
g.dataPins[i].Low()
}
mask = mask << 1
}
}
// pins returns current state of data pins. MSB is D7
func (g *GPIO) pins() byte {
bits := byte(0)
for i := uint8(0); i < uint8(len(g.dataPins)); i++ {
if g.dataPins[i].Get() {
bits |= (1 << i)
}
}
return bits
}
const (
// These are the default execution times for the Clear and
// Home commands and everything else.
//
// These are used if RW is passed as machine.NoPin and ignored
// otherwise.
//
// They are set conservatively here and can be tweaked in the
// Config structure.
DefaultClearHomeTime = 80 * time.Millisecond
DefaultInstrExecTime = 80 * time.Microsecond
)
type Buser interface {
io.ReadWriter
SetCommandMode(set bool)
WriteOnly() bool
}
type Device struct {
bus Buser // fails to compile if used
// bus *GPIO // compiles without issues
width uint8
height uint8
buffer []uint8
bufferLength uint8
rowOffset []uint8 // Row offsets in DDRAM
datalength uint8
cursor cursor
busyStatus []byte
clearHomeTime time.Duration // time clear/home instructions might take
instrExecTime time.Duration // time all other instructions might take
}
type cursor struct {
x, y uint8
}
type Config struct {
Width int16
Height int16
CursorBlink bool
CursorOnOff bool
Font uint8
ClearHomeTime time.Duration // time clear/home instructions might take - use 0 for the default
InstrExecTime time.Duration // time all other instructions might take - use 0 for the default
}
// NewGPIO4Bit returns 4bit data length HD44780 driver. Datapins are LCD DB pins starting from DB4 to DB7
//
// If your device has RW set permanently to ground then pass in rw as machine.NoPin
func NewGPIO4Bit(dataPins []machine.Pin, e, rs, rw machine.Pin) (Device, error) {
const fourBitMode = 4
if len(dataPins) != fourBitMode {
return Device{}, errors.New("4 pins are required in data slice (D4-D7) when HD44780 is used in 4 bit mode")
}
return newGPIO(dataPins, e, rs, rw, DATA_LENGTH_4BIT), nil
}
// NewGPIO8Bit returns 8bit data length HD44780 driver. Datapins are LCD DB pins starting from DB0 to DB7
//
// If your device has RW set permanently to ground then pass in rw as machine.NoPin
func NewGPIO8Bit(dataPins []machine.Pin, e, rs, rw machine.Pin) (Device, error) {
const eightBitMode = 8
if len(dataPins) != eightBitMode {
return Device{}, errors.New("8 pins are required in data slice (D0-D7) when HD44780 is used in 8 bit mode")
}
return newGPIO(dataPins, e, rs, rw, DATA_LENGTH_8BIT), nil
}
// Configure initializes device
func (d *Device) Configure(cfg Config) error {
d.busyStatus = make([]byte, 1)
d.width = uint8(cfg.Width)
d.height = uint8(cfg.Height)
if d.width == 0 || d.height == 0 {
return errors.New("width and height must be set")
}
d.clearHomeTime = cfg.ClearHomeTime
d.instrExecTime = cfg.InstrExecTime
memoryMap := uint8(ONE_LINE)
if d.height > 1 {
memoryMap = TWO_LINE
}
d.setRowOffsets()
d.ClearBuffer()
cursor := CURSOR_OFF
if cfg.CursorOnOff {
cursor = CURSOR_ON
}
cursorBlink := CURSOR_BLINK_OFF
if cfg.CursorBlink {
cursorBlink = CURSOR_BLINK_ON
}
if !(cfg.Font == FONT_5X8 || cfg.Font == FONT_5X10) {
cfg.Font = FONT_5X8
}
//Wait 15ms after Vcc rises to 4.5V
time.Sleep(15 * time.Millisecond)
d.bus.SetCommandMode(true)
d.bus.Write([]byte{DATA_LENGTH_8BIT})
time.Sleep(5 * time.Millisecond)
for i := 0; i < 2; i++ {
d.bus.Write([]byte{DATA_LENGTH_8BIT})
time.Sleep(150 * time.Microsecond)
}
if d.datalength == DATA_LENGTH_4BIT {
d.bus.Write([]byte{DATA_LENGTH_4BIT >> 4})
}
// Busy flag is now accessible
d.SendCommand(memoryMap | cfg.Font | d.datalength)
d.SendCommand(DISPLAY_OFF)
d.SendCommand(DISPLAY_CLEAR)
d.SendCommand(ENTRY_MODE | CURSOR_INCREASE | DISPLAY_NO_SHIFT)
d.SendCommand(DISPLAY_ON | uint8(cursor) | uint8(cursorBlink))
return nil
}
// Write writes data to internal buffer
func (d *Device) Write(data []byte) (n int, err error) {
size := len(data)
if size > len(d.buffer) {
size = len(d.buffer)
}
d.bufferLength = uint8(size)
for i := uint8(0); i < d.bufferLength; i++ {
d.buffer[i] = data[i]
}
return size, nil
}
// Display sends the whole buffer to the screen at cursor position
func (d *Device) Display() error {
// Buffer may contain less characters than its capacity.
// We must be sure that we will not send unassigned characters
// That would result in sending zero values of buffer slice and
// potentialy displaying some character.
var totalDisplayedChars uint8
var bufferPos uint8
for ; d.cursor.y < d.height; d.cursor.y++ {
d.SetCursor(d.cursor.x, d.cursor.y)
for ; d.cursor.x < d.width && totalDisplayedChars < d.bufferLength; d.cursor.x++ {
d.sendData(d.buffer[bufferPos])
bufferPos++
totalDisplayedChars++
}
if d.cursor.x >= d.width {
d.cursor.x = 0
}
if totalDisplayedChars >= d.bufferLength {
break
}
}
return nil
}
// SetCursor moves cursor to position x,y, where (0,0) is top left corner and (width-1, height-1) bottom right
func (d *Device) SetCursor(x, y uint8) {
d.cursor.x = x
d.cursor.y = y
d.SendCommand(DDRAM_SET | (x + (d.rowOffset[y] * y)))
}
// SetRowOffsets sets initial memory addresses coresponding to the display rows
// Each row on display has different starting address in DDRAM. Rows are not mapped in order.
// These addresses tend to differ between the types of the displays (16x2, 16x4, 20x4 etc ..),
// https://web.archive.org/web/20111122175541/http://web.alfredstate.edu/weimandn/lcd/lcd_addressing/lcd_addressing_index.html
func (d *Device) setRowOffsets() {
switch d.height {
case 1:
d.rowOffset = []uint8{}
case 2:
d.rowOffset = []uint8{0x0, 0x40, 0x0, 0x40}
case 4:
d.rowOffset = []uint8{0x0, 0x40, d.width, 0x40 + d.width}
default:
d.rowOffset = []uint8{0x0, 0x40, d.width, 0x40 + d.width}
}
}
// SendCommand sends commands to driver
func (d *Device) SendCommand(command byte) {
d.bus.SetCommandMode(true)
d.bus.Write([]byte{command})
for d.busy(command == DISPLAY_CLEAR || command == CURSOR_HOME) {
}
}
// sendData sends byte data directly to display.
func (d *Device) sendData(data byte) {
d.bus.SetCommandMode(false)
d.bus.Write([]byte{data})
for d.busy(false) {
}
}
// CreateCharacter crates characters using data and stores it under cgram Addr in CGRAM
func (d *Device) CreateCharacter(cgramAddr uint8, data []byte) {
d.SendCommand(CGRAM_SET | cgramAddr)
for _, dd := range data {
d.sendData(dd)
}
}
// busy returns true when hd447890 is busy
// or after the timeout specified
func (d *Device) busy(longDelay bool) bool {
if d.bus.WriteOnly() {
// Can't read busy flag if write only, so sleep a bit then return
if longDelay {
// Note that we sleep like this so the default
// time.Sleep is time.Sleep(constant) as
// time.Sleep(variable) doesn't seem to work on AVR yet
if d.clearHomeTime != 0 {
time.Sleep(d.clearHomeTime)
} else {
time.Sleep(DefaultClearHomeTime)
}
} else {
if d.instrExecTime != 0 {
time.Sleep(d.instrExecTime)
} else {
time.Sleep(DefaultInstrExecTime)
}
}
return false
}
d.bus.SetCommandMode(true)
d.bus.Read(d.busyStatus)
return (d.busyStatus[0] & BUSY) > 0
}
// Busy returns true when hd447890 is busy
func (d *Device) Busy() bool {
return d.busy(false)
}
// Size returns the current size of the display.
func (d *Device) Size() (w, h int16) {
return int16(d.width), int16(d.height)
}
// ClearDisplay clears displayed content and buffer
func (d *Device) ClearDisplay() {
d.SendCommand(DISPLAY_CLEAR)
d.ClearBuffer()
}
// ClearBuffer clears internal buffer
func (d *Device) ClearBuffer() {
d.buffer = make([]uint8, d.width*d.height)
}
const (
DISPLAY_CLEAR = 0x1
CURSOR_HOME = 0x2
ENTRY_MODE = 0x4
CURSOR_DECREASE = ENTRY_MODE | 0x0
CURSOR_INCREASE = ENTRY_MODE | 0x2
DISPLAY_SHIFT = ENTRY_MODE | 0x1
DISPLAY_NO_SHIFT = ENTRY_MODE | 0x0
DISPLAY_ON_OFF = 0x8
DISPLAY_ON = DISPLAY_ON_OFF | 0x4
DISPLAY_OFF = DISPLAY_ON_OFF | 0x0
CURSOR_ON = DISPLAY_ON_OFF | 0x2
CURSOR_OFF = DISPLAY_ON_OFF | 0x0
CURSOR_BLINK_ON = DISPLAY_ON_OFF | 0x1
CURSOR_BLINK_OFF = DISPLAY_ON_OFF | 0x0
CURSOR_DISPLAY_SHIFT = 0x10
CURSOR_SHIFT_RIGHT = CURSOR_DISPLAY_SHIFT | 0x4
CURSOR_SHIFT_LEFT = CURSOR_DISPLAY_SHIFT | 0x0
DISPLAY_SHIFT_RIGHT = CURSOR_DISPLAY_SHIFT | 0xC
DISPLAY_SHIFT_LEFT = CURSOR_DISPLAY_SHIFT | 0x8
FUNCTION_MODE = 0x20
DATA_LENGTH_8BIT = FUNCTION_MODE | 0x10
DATA_LENGTH_4BIT = FUNCTION_MODE | 0x0
TWO_LINE = FUNCTION_MODE | 0x8
ONE_LINE = FUNCTION_MODE | 0x0
FONT_5X10 = FUNCTION_MODE | 0x4
FONT_5X8 = FUNCTION_MODE | 0x0
BUSY = 0x80
CGRAM_SET = 0x40
DDRAM_SET = 0x80
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment