package epd7in3e import ( "context" "errors" "fmt" "os" "time" "periph.io/x/conn/v3" "periph.io/x/conn/v3/gpio" "periph.io/x/conn/v3/gpio/gpioreg" "periph.io/x/conn/v3/physic" "periph.io/x/conn/v3/spi" "periph.io/x/conn/v3/spi/spireg" "periph.io/x/host/v3" ) // bus is the seam between the protocol logic in this package and the // hardware. type bus interface { // Cmd sends one command byte (DC low). Cmd(c byte) error // Data sends data bytes (DC high). The implementation must // chunk them to stay within the spidev transfer limit while // keeping DC high across all chunks. Data(b []byte) error // Reset pulses the RST pin: high 20 ms, low 2 ms, high 20 ms. Reset() error // Power drives the PWR pin; it must be HIGH before anything // else. Power(on bool) error // WaitIdle polls BUSY (LOW = busy, HIGH = idle) until it reads // HIGH, the context is done, or the busy timeout elapses; the // last case returns a wrapped ErrBusyTimeout. WaitIdle(ctx context.Context) error // Sleep waits for d. Sleep(d time.Duration) } // maxTxSizeFallback is the default Linux spidev transfer buffer size // in bytes; the kernel rejects larger single transfers. const maxTxSizeFallback = 4096 // spiSpeed is the SPI clock of the HAT. const spiSpeed = 4 * physic.MegaHertz // busyPollInterval is the BUSY polling interval. const busyPollInterval = time.Millisecond // Reset timing. const ( resetHigh1 = 20 * time.Millisecond resetLow = 2 * time.Millisecond resetHigh2 = 20 * time.Millisecond ) type spiBus struct { conn spi.Conn dc gpio.PinIO rst gpio.PinIO pwr gpio.PinIO busy gpio.PinIn log func(format string, args ...any) scratch [1]byte } func (b *spiBus) logf(format string, args ...any) { if b.log != nil { b.log(format, args...) } } // New builds a Dev on top of an already-opened SPI port p (e.g. // spireg.Open("SPI0.0")) and the HAT's GPIO pins: dc, rst and pwr are // outputs, busy is the input. The port is connected at 4 MHz, mode 0, // 8 bits, MSB first. o may be nil. func New(p spi.Port, dc, rst, pwr gpio.PinIO, busy gpio.PinIn, o *Opts) (*Dev, error) { if p == nil { return nil, errors.New("epd7in3e: new: nil SPI port") } if dc == nil || rst == nil || pwr == nil || busy == nil { return nil, errors.New("epd7in3e: new: a GPIO pin is nil") } conn, err := p.Connect(spiSpeed, spi.Mode0, 8) if err != nil { return nil, fmt.Errorf("epd7in3e: new: connect SPI: %w", err) } b := &spiBus{ conn: conn, dc: dc, rst: rst, pwr: pwr, busy: busy, } if o != nil && o.Verbose { b.log = func(format string, args ...any) { fmt.Fprintf(os.Stderr, "epd7in3e: "+format+"\n", args...) } } return newDev(b, o), nil } // NewHAT opens the HAT on the default Waveshare wiring: SPI0.0, RST on // GPIO17, DC on GPIO25, PWR on GPIO27 and BUSY on GPIO24. It calls // host.Init() first, so it must be the first periph call in the // program. o may be nil for the defaults. func NewHAT(o *Opts) (*Dev, error) { if _, err := host.Init(); err != nil { return nil, fmt.Errorf("epd7in3e: new hat: host init: %w", err) } port, err := spireg.Open("SPI0.0") if err != nil { return nil, fmt.Errorf("epd7in3e: new hat: open SPI0.0: %w", err) } // gpioreg names use the BCM GPIO number. pins := []struct { name string pin gpio.PinIO }{ {"dc", gpioreg.ByName("GPIO25")}, {"rst", gpioreg.ByName("GPIO17")}, {"pwr", gpioreg.ByName("GPIO27")}, {"busy", gpioreg.ByName("GPIO24")}, } for _, p := range pins { if p.pin == nil { return nil, fmt.Errorf("epd7in3e: new hat: pin %s not found; check that SPI is enabled and GPIO is accessible", p.name) } } return New(port, pins[0].pin, pins[1].pin, pins[2].pin, pins[3].pin, o) } func (b *spiBus) Cmd(c byte) error { b.logf("cmd 0x%02X", c) if err := b.dc.Out(gpio.Low); err != nil { return fmt.Errorf("epd7in3e: cmd 0x%02X: DC low: %w", c, err) } b.scratch[0] = c if err := b.conn.Tx(b.scratch[:], nil); err != nil { return fmt.Errorf("epd7in3e: cmd 0x%02X: %w", c, err) } return nil } // Data sends data bytes with DC high, chunked to stay within the // spidev transfer limit. DC stays high across all chunks. func (b *spiBus) Data(data []byte) error { if len(data) <= 8 { b.logf("data % X", data) } else { n := maxTxSize(b.conn) b.logf("data %d bytes (%d chunks of <= %d)", len(data), (len(data)+n-1)/n, n) } if err := b.dc.Out(gpio.High); err != nil { return fmt.Errorf("epd7in3e: data: DC high: %w", err) } if err := chunkedTx(b.conn, data); err != nil { return fmt.Errorf("epd7in3e: data: %w", err) } return nil } func maxTxSize(c spi.Conn) int { if l, ok := c.(conn.Limits); ok { if n := l.MaxTxSize(); n > 0 { return n } } return maxTxSizeFallback } func chunkedTx(c spi.Conn, b []byte) error { n := maxTxSize(c) for len(b) > 0 { m := n if m > len(b) { m = len(b) } if err := c.Tx(b[:m], nil); err != nil { return err } b = b[m:] } return nil } func (b *spiBus) Reset() error { b.logf("reset") if err := b.rst.Out(gpio.High); err != nil { return fmt.Errorf("epd7in3e: reset: %w", err) } time.Sleep(resetHigh1) if err := b.rst.Out(gpio.Low); err != nil { return fmt.Errorf("epd7in3e: reset: %w", err) } time.Sleep(resetLow) if err := b.rst.Out(gpio.High); err != nil { return fmt.Errorf("epd7in3e: reset: %w", err) } time.Sleep(resetHigh2) return nil } func (b *spiBus) Power(on bool) error { if on { b.logf("power on") if err := b.pwr.Out(gpio.High); err != nil { return fmt.Errorf("epd7in3e: power on: %w", err) } return nil } b.logf("power off") if err := b.pwr.Out(gpio.Low); err != nil { return fmt.Errorf("epd7in3e: power off: %w", err) } return nil } // WaitIdle polls BUSY until it reads HIGH, the context deadline // elapses (returns a wrapped ErrBusyTimeout) or the context is // cancelled (returns the context error). func (b *spiBus) WaitIdle(ctx context.Context) error { start := time.Now() for { if b.busy.Read() == gpio.High { b.logf("waitIdle: idle after %s", time.Since(start)) return nil } select { case <-ctx.Done(): if errors.Is(ctx.Err(), context.DeadlineExceeded) { b.logf("waitIdle: BUSY still low after %s (busy timeout)", time.Since(start)) return ErrBusyTimeout } err := fmt.Errorf("epd7in3e: wait idle: %w", ctx.Err()) b.logf("waitIdle: aborted after %s: %v", time.Since(start), ctx.Err()) return err case <-time.After(busyPollInterval): } } } func (b *spiBus) Sleep(d time.Duration) { b.logf("sleep %s", d) time.Sleep(d) }