mirror of
https://github.com/tinygo-org/drivers.git
synced 2026-07-26 10:38:41 +00:00
f57b5ecee9
sharpmem: add implementation of sharpmem display driver * Implement Configure, Clear and ClearBuffer, add some tests, add documentation/comments * Reverse white * Inverted bits, fix and improve ClearBuffer, cleanup * Refine doc comment * Driver refactor, optimizations toggle, additional tests & support for all SKUs * Fix address overflow padding, add wire-level tests for assumed address encoding * Minor rename * Cleanup and doc fixes * Add device configs * Bounds check * Add example and smoketest entry * Use uf2 output file format * Refine example
375 lines
8.7 KiB
Go
375 lines
8.7 KiB
Go
package sharpmem
|
|
|
|
import (
|
|
"errors"
|
|
"image/color"
|
|
|
|
"tinygo.org/x/drivers"
|
|
)
|
|
|
|
const (
|
|
bitWriteCmd uint8 = 0b00000001
|
|
bitVcom uint8 = 0b00000010
|
|
bitClear uint8 = 0b00000100
|
|
)
|
|
|
|
var (
|
|
ConfigLS010B7DH04 = Config{Width: 128, Height: 128}
|
|
ConfigLS011B7DH03 = Config{Width: 160, Height: 68}
|
|
ConfigLS012B7DD01 = Config{Width: 184, Height: 38}
|
|
ConfigLS013B7DH03 = ConfigLS010B7DH04
|
|
ConfigLS013B7DH05 = Config{Width: 144, Height: 168}
|
|
ConfigLS018B7DH02 = Config{Width: 230, Height: 303}
|
|
ConfigLS027B7DH01 = Config{Width: 400, Height: 240}
|
|
ConfigLS027B7DH01A = ConfigLS027B7DH01
|
|
ConfigLS032B7DD02 = Config{Width: 336, Height: 536}
|
|
ConfigLS044Q7DH01 = Config{Width: 320, Height: 240}
|
|
)
|
|
|
|
type Pin interface {
|
|
High()
|
|
Low()
|
|
}
|
|
|
|
// Device represents a Sharp Memory Display device. This driver implementation
|
|
// concerns the 1-bit color versions only (black and white memory displays).
|
|
//
|
|
// Supported SKUs include:
|
|
// LS010B7DH04, LS011B7DH03, LS012B7DD01, LS013B7DH03, LS013B7DH05,
|
|
// LS018B7DH02, LS027B7DH01, LS027B7DH01A, LS032B7DD02, LS044Q7DH01
|
|
//
|
|
// Note: Only SKU LS011B7DH03 (160x68) has been tested as of writing.
|
|
//
|
|
// The driver includes optimizations (frame and per-line invalidation) that
|
|
// only transmit the changed lines to the display. These optimizations are on
|
|
// by default, and they can be disabled with the respective config option.
|
|
type Device struct {
|
|
bus drivers.SPI
|
|
csPin Pin
|
|
buffer []byte
|
|
txBuf []byte
|
|
lineDiff []byte
|
|
width int16
|
|
height int16
|
|
bufferSize int16
|
|
bytesPerLine int16
|
|
vcom uint8
|
|
diffing bool
|
|
}
|
|
|
|
type Config struct {
|
|
Width int16
|
|
Height int16
|
|
|
|
// DisableOptimizations disables frame and line invalidation optimizations.
|
|
// Useful if constant frame times are desired.
|
|
DisableOptimizations bool
|
|
}
|
|
|
|
// New creates a new device connection.
|
|
// The SPI bus must have already been configured.
|
|
func New(bus drivers.SPI, csPin Pin) Device {
|
|
d := Device{
|
|
bus: bus,
|
|
csPin: csPin,
|
|
}
|
|
return d
|
|
}
|
|
|
|
// Configure initializes the display with specified configuration. It can be
|
|
// called multiple times on the same display, resetting its internal state.
|
|
func (d *Device) Configure(cfg Config) {
|
|
if cfg.Width == 0 {
|
|
cfg.Width = 160
|
|
}
|
|
if cfg.Height == 0 {
|
|
cfg.Height = 68
|
|
}
|
|
|
|
d.width = cfg.Width
|
|
d.height = cfg.Height
|
|
d.diffing = !cfg.DisableOptimizations
|
|
|
|
d.initialize()
|
|
}
|
|
|
|
// initialize properly initializes the display and the in-memory image buffers.
|
|
func (d *Device) initialize() {
|
|
d.csPin.Low()
|
|
|
|
// initialize VCOM as high
|
|
d.vcom = bitVcom
|
|
|
|
// bytesPerLine has to be 16-bit aligned, as some resolutions require
|
|
// padding to the nearest 2nd byte.
|
|
d.bytesPerLine = ceilDiv(d.width, 16) * 2
|
|
|
|
// preallocate a contiguous byte buffer for all lines, including
|
|
// protocol-required padding for each line apriori (easier to transfer).
|
|
d.bufferSize = d.bytesPerLine * d.height
|
|
d.buffer = make([]byte, d.bufferSize)
|
|
// A bit being 1 is white (reflective), 0 is black (less reflective).
|
|
for i := range d.buffer {
|
|
d.buffer[i] = 0xff
|
|
}
|
|
|
|
// auxiliary buffer for SPI transfers to avoid dynamic allocations
|
|
d.txBuf = make([]byte, 2)
|
|
|
|
if d.diffing {
|
|
// buffer to store the changed lines. First bit is whether any line has
|
|
// changed at all (i.e. the frame is invalid), followed by N bits,
|
|
// one for each line.
|
|
d.lineDiff = make([]byte, bitfieldBufLen(1+d.height))
|
|
}
|
|
}
|
|
|
|
// SetPixel enables or disables a pixel in the buffer.
|
|
// color.RGBA{0, 0, 0, 255} is considered transparent (reflective, white),
|
|
// anything else will enable a pixel on the screen (make it appear less
|
|
// reflective, black).
|
|
func (d *Device) SetPixel(x, y int16, c color.RGBA) {
|
|
if d.width == 0 {
|
|
return
|
|
}
|
|
|
|
// bounds check
|
|
if x < 0 || x >= d.width || y < 0 || y >= d.height {
|
|
return
|
|
}
|
|
|
|
offset := y * d.bytesPerLine
|
|
|
|
div := offset + x/8
|
|
mod := uint8(x % 8)
|
|
|
|
prev := hasBit(d.buffer[div], mod)
|
|
curr := c.R == 0 && c.G == 0 && c.B == 0 && c.A == 255
|
|
|
|
if prev == curr {
|
|
return
|
|
}
|
|
|
|
if curr {
|
|
d.buffer[div] = setBit(d.buffer[div], mod)
|
|
} else {
|
|
d.buffer[div] = unsetBit(d.buffer[div], mod)
|
|
}
|
|
|
|
if d.diffing {
|
|
d.invalidateLine(y)
|
|
}
|
|
}
|
|
|
|
// Size returns the current size of the display.
|
|
func (d *Device) Size() (x, y int16) {
|
|
return d.width, d.height
|
|
}
|
|
|
|
// Display renders the buffer to the screen. It only transmits changed lines if
|
|
// optimizations are enabled. It should be called at >=1hz, even if the
|
|
// buffer hasn't been modified.
|
|
func (d *Device) Display() error {
|
|
if d.width == 0 {
|
|
return errors.New("display not configured")
|
|
}
|
|
|
|
if d.diffing {
|
|
if !hasBit(d.lineDiff[0], 0) {
|
|
// no pixels have been modified, simply toggle VCOM
|
|
return d.holdDisplay()
|
|
}
|
|
|
|
defer func() {
|
|
for i := 0; i < len(d.lineDiff); i++ {
|
|
d.lineDiff[i] = 0x00
|
|
}
|
|
}()
|
|
}
|
|
|
|
cmd := bitWriteCmd | d.vcom
|
|
|
|
d.toggleVcom()
|
|
|
|
// Padding to use for high bits of line numbers that overflow 8 bits.
|
|
var hiPad = uint8(0)
|
|
if d.height >= 512 {
|
|
hiPad = 3 + 3 // 3 mode bits + 3 low bits
|
|
} else if d.height >= 256 {
|
|
hiPad = 3 + 4 // 3 mode bits + 4 low bits
|
|
}
|
|
|
|
// start transfer
|
|
d.csPin.High()
|
|
|
|
for i := int16(0); i < d.height; i++ {
|
|
if d.diffing {
|
|
// Skip rendering lines that haven't changed.
|
|
linediv := (i + 1) / 8
|
|
linemod := uint8((i + 1) % 8)
|
|
if !hasBit(d.lineDiff[linediv], linemod) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// The first 5 bits are either dummy or part of the current line
|
|
// (1-indexed) if it overflows 8-bits.
|
|
// The last 3 bits are the command for the first line and dummy bits
|
|
// for subsequent lines (set as command for simplicity)
|
|
hi := uint8((i + 1) >> 8)
|
|
hi = hi << hiPad
|
|
d.txBuf[0] = cmd | hi
|
|
|
|
// The second byte is the low bits of the current line (1-indexed).
|
|
// for <8 bits cases, the high bits are dummy, so we leave them as 0.
|
|
d.txBuf[1] = uint8(i + 1)
|
|
|
|
// send the first two bytes
|
|
err := d.bus.Tx(d.txBuf, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// send the line data
|
|
err = d.bus.Tx(d.buffer[i*d.bytesPerLine:(i+1)*d.bytesPerLine], nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Trailer 16 bits (low)
|
|
d.txBuf[0] = 0x00
|
|
d.txBuf[1] = 0x00
|
|
err := d.bus.Tx(d.txBuf, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// end transfer
|
|
d.csPin.Low()
|
|
|
|
return nil
|
|
}
|
|
|
|
// holdDisplay simply toggles VCOM without updating any lines.
|
|
func (d *Device) holdDisplay() error {
|
|
d.txBuf[0] = d.vcom
|
|
d.txBuf[1] = 0x00
|
|
|
|
d.toggleVcom()
|
|
|
|
// begin transaction
|
|
d.csPin.High()
|
|
|
|
err := d.bus.Tx(d.txBuf, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// end transaction
|
|
d.csPin.Low()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Clear clears both the in-memory buffer and the display.
|
|
func (d *Device) Clear() error {
|
|
if d.width == 0 {
|
|
return errors.New("display not configured")
|
|
}
|
|
|
|
d.ClearBuffer()
|
|
return d.ClearDisplay()
|
|
}
|
|
|
|
// ClearBuffer clears the in-memory buffer. The display is not updated.
|
|
func (d *Device) ClearBuffer() {
|
|
if d.width == 0 {
|
|
return
|
|
}
|
|
|
|
if d.diffing {
|
|
// detect what rows need to be reset on the next render
|
|
d.invalidateModifiedLines()
|
|
}
|
|
|
|
// reset the in-memory buffer
|
|
for i := 0; i < len(d.buffer); i++ {
|
|
d.buffer[i] = 0xff
|
|
}
|
|
}
|
|
|
|
// invalidateModifiedLines marks any line that has at least a single black pixel
|
|
// as invalidated. Padding bits, if any, are always 1.
|
|
func (d *Device) invalidateModifiedLines() {
|
|
for y := int16(0); y < d.height; y++ {
|
|
offset := y * d.bytesPerLine
|
|
|
|
updateLine := false
|
|
for x := int16(0); x < d.width; x++ {
|
|
div := offset + x/8
|
|
mod := uint8(x % 8)
|
|
|
|
if !hasBit(d.buffer[div], mod) {
|
|
updateLine = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if updateLine {
|
|
d.invalidateLine(y)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ClearDisplay clears the display. The in-memory buffer is not updated. A
|
|
// subsequent call to Display() will re-render the content as it was before
|
|
// clearing.
|
|
func (d *Device) ClearDisplay() error {
|
|
if d.width == 0 {
|
|
return errors.New("display not configured")
|
|
}
|
|
|
|
d.txBuf[0] = d.vcom | bitClear
|
|
d.txBuf[1] = 0x00
|
|
|
|
d.toggleVcom()
|
|
|
|
// begin transaction
|
|
d.csPin.High()
|
|
|
|
err := d.bus.Tx(d.txBuf, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// end transaction
|
|
d.csPin.Low()
|
|
|
|
return nil
|
|
}
|
|
|
|
// invalidateLine marks a line and the frame itself as invalidated.
|
|
func (d *Device) invalidateLine(line int16) {
|
|
// mark the frame as invalidated
|
|
d.lineDiff[0] = setBit(d.lineDiff[0], 0)
|
|
|
|
// mark the line as invalidated
|
|
linediv := (line + 1) / 8
|
|
linemod := uint8((line + 1) % 8)
|
|
d.lineDiff[linediv] = setBit(d.lineDiff[linediv], linemod)
|
|
}
|
|
|
|
// toggleVcom toggles the VCOM, as is instructed by the datasheet.
|
|
// Toggling VCOM can help maintain the display's longevity. It should ideally
|
|
// be called at least once per second, preferably at 4-100 Hz.
|
|
// Toggling VCOM causes a tiny bit of flicker, but without it the pixels can
|
|
// be permanently damaged by the DC bias accumulating over time.
|
|
func (d *Device) toggleVcom() {
|
|
if d.vcom != 0 {
|
|
d.vcom = 0x00
|
|
} else {
|
|
d.vcom = bitVcom
|
|
}
|
|
}
|