From 2cec480fb89947491c70f8e66d2c125b653f3b50 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Sat, 18 Jul 2020 20:36:32 -0400 Subject: [PATCH] p1am: initial driver for the base controller All the driver can currently do is initialize the base controller and enumerate the connected modules. --- Makefile | 2 +- p1am/p1am.go | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 p1am/p1am.go diff --git a/Makefile b/Makefile index f073712..6a69e1f 100644 --- a/Makefile +++ b/Makefile @@ -175,7 +175,7 @@ endif DRIVERS = $(wildcard */) NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \ hcsr04 ssd1331 ws2812 thermistor apa102 easystepper ssd1351 ili9341 wifinina shifter hub75 \ - hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x dht keypad4x4 max72xx + hd44780 buzzer ssd1306 espat l9110x st7735 bmi160 l293x dht keypad4x4 max72xx p1am TESTS = $(filter-out $(addsuffix /%,$(NOTESTS)),$(DRIVERS)) unit-test: diff --git a/p1am/p1am.go b/p1am/p1am.go new file mode 100644 index 0000000..8cfb75a --- /dev/null +++ b/p1am/p1am.go @@ -0,0 +1,286 @@ +// Driver for the P1AM-100 base controller. +// +// This is an embedded device on the P1AM-100 board. +// Based on v1.0.1 of the Arduino library: https://github.com/facts-engineering/P1AM/tree/1.0.1 + +package p1am + +import ( + "encoding/binary" + "errors" + "fmt" + "machine" + "time" +) + +type P1AM struct { + bus machine.SPI + slaveSelectPin, slaveAckPin, baseEnablePin machine.Pin + + // SkipAutoConfig will skip loading a default configuration into each module. + SkipAutoConfig bool + + Slots int + // Access slots via Slot() + slots []Slot +} + +var Controller = P1AM{ + bus: machine.SPI0, + slaveSelectPin: machine.BASE_SLAVE_SELECT_PIN, + slaveAckPin: machine.BASE_SLAVE_ACK_PIN, + baseEnablePin: machine.BASE_ENABLE_PIN, +} + +type baseSlotConstants struct { + DI, DO, AI, AO, Status, Config, DataSize byte +} + +func (p *P1AM) Initialize() error { + p.slaveSelectPin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + p.slaveAckPin.Configure(machine.PinConfig{Mode: machine.PinInput}) + p.baseEnablePin.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + if err := p.bus.Configure(machine.SPIConfig{ + Frequency: 1000000, + Mode: 2, + LSBFirst: false, + }); err != nil { + return err + } + + p.SetEnabled(true) + time.Sleep(100 * time.Millisecond) + + if err := p.waitAck(5 * time.Second); err != nil { + return errors.New("no base controller activity; check external supply connection") + } + + for i := 0; i < 5; i++ { + if err := p.handleHDR(MOD_HDR); err == nil { + time.Sleep(5 * time.Millisecond) + slots, err := p.spiSendRecvByte(0xFF) + if err == nil && slots > 0 && slots <= 15 { + p.Slots = int(slots) + break + } + } + if i > 2 { + // Try restarting the base controller + p.SetEnabled(false) + time.Sleep(10 * time.Millisecond) + p.SetEnabled(true) + time.Sleep(10 * time.Millisecond) + } + } + if p.Slots <= 0 || p.Slots > 15 { + return errors.New("zero modules in the base") + } + + moduleIDs := make([]uint32, p.Slots) + + p.waitAck(200 * time.Millisecond) + if err := binary.Read(p, binary.LittleEndian, &moduleIDs); err != nil { + return err + } + + baseConstants := make([]baseSlotConstants, p.Slots) + p.slots = make([]Slot, p.Slots) + + for i := 1; i <= p.Slots; i++ { + slot := p.Slot(i) + slot.p = p + slot.slot = byte(i) + slot.ID = moduleIDs[i-1] + // What if 0xFFFFFFFF isn't at position -2? + slot.Props = &modules[len(modules)-2] + for j := 0; j < len(modules); j++ { + if modules[j].ModuleID == slot.ID { + slot.Props = &modules[j] + } + bc := &baseConstants[i-1] + bc.DI = slot.Props.DI + bc.DO = slot.Props.DO + bc.AI = slot.Props.AI + bc.AO = slot.Props.AO + bc.Status = slot.Props.Status + bc.Config = slot.Props.Config + bc.DataSize = slot.Props.DataSize + } + } + + p.waitAck(200 * time.Millisecond) + if err := binary.Write(p, binary.LittleEndian, &baseConstants); err != nil { + return err + } + + if !p.SkipAutoConfig { + for i := 1; i <= p.Slots; i++ { + s := p.Slot(i) + if s.Props.Config > 0 { + cfg := defaultConfig[s.ID] + if cfg != nil { + s.Configure(cfg) + } + } + } + } + + return nil +} + +func (p *P1AM) Active() (bool, error) { + if _, err := p.spiSendRecvByte(ACTIVE_HDR); err != nil { + return false, err + } + if err := p.waitAck(200 * time.Millisecond); err != nil { + return false, err + } + buf, err := p.spiSendRecvByte(DUMMY) + defer p.dataSync() + return buf != 0, err +} + +func (p *P1AM) Slot(i int) *Slot { + if i < 1 || i > p.Slots { + return nil + } + return &p.slots[i-1] +} + +type Slot struct { + p *P1AM + slot byte + ID uint32 + // TODO: Embed this? + Props *ModuleProps +} + +func (s *Slot) Configure(data []byte) error { + if s == nil { + return errors.New("invalid slot") + } + if len(data) != int(s.Props.Config) { + return fmt.Errorf("expected %d config bytes, got %d", s.Props.Config, len(data)) + } + + if len(data) == 0 { + return errors.New("no config bytes") + } + + out := make([]byte, len(data)+2) + out[0] = CFG_HDR + out[1] = s.slot + copy(out[2:], data) + + if err := s.p.spiSendRecvBuf(out, nil); err != nil { + return err + } + time.Sleep(100 * time.Millisecond) + s.p.dataSync() + s.p.dataSync() + return nil +} + +const ackTimeout = 200 * time.Millisecond + +func awaitPin(pin machine.Pin, state bool, timeout time.Duration) bool { + start := time.Now() + for pin.Get() != state { + time.Sleep(100 * time.Microsecond) + if time.Since(start) > timeout { + return false + } + } + return true + // TODO: Use channels when https://github.com/tinygo-org/tinygo/pull/1402 is merged. + // edge := machine.PinRising + // if state { + // edge = machine.PinFalling + // } + // ch := make(chan struct{}, 1) + // defer close(ch) + // pin.SetInterrupt(edge, func(machine.Pin) { + // ch <- struct{}{} + // }) + // defer pin.SetInterrupt(0, nil) + // select { + // case <-ch: + // return true + // case <-time.After(timeout): + // return false + // } +} + +var dataSyncErr = errors.New("base sync timeout") + +func (p *P1AM) dataSync() error { + if !awaitPin(p.slaveAckPin, true, ackTimeout) { + return dataSyncErr + } + time.Sleep(time.Microsecond) + if !awaitPin(p.slaveAckPin, false, ackTimeout) { + return dataSyncErr + } + time.Sleep(time.Microsecond) + if !awaitPin(p.slaveAckPin, true, ackTimeout) { + return dataSyncErr + } + time.Sleep(time.Microsecond) + return nil +} + +func (p *P1AM) handleHDR(HDR byte) error { + for !p.slaveAckPin.Get() { + } + if _, err := p.spiSendRecvByte(HDR); err != nil { + return err + } + return p.spiTimeout(MAX_TIMEOUT*time.Millisecond, HDR, 2*time.Second) +} + +func (p *P1AM) Read(data []byte) (int, error) { + return len(data), p.spiSendRecvBuf(nil, data) +} + +func (p *P1AM) Write(data []byte) (int, error) { + return len(data), p.spiSendRecvBuf(data, nil) +} + +func (p *P1AM) spiSendRecvBuf(w, r []byte) error { + p.slaveSelectPin.Low() + defer p.slaveSelectPin.High() + return p.bus.Tx(w, r) +} + +func (p *P1AM) spiSendRecvByte(data byte) (byte, error) { + p.slaveSelectPin.Low() + defer p.slaveSelectPin.High() + return p.bus.Transfer(data) +} + +func (p *P1AM) waitAck(timeout time.Duration) error { + return p.spiTimeout(timeout, 0, 0) +} + +var timeoutErr = errors.New("timeout") + +func (p *P1AM) spiTimeout(timeout time.Duration, resendMsg byte, retryPeriod time.Duration) error { + end := time.Now().Add(timeout) + retry := time.Now().Add(retryPeriod) + for time.Now().Before(end) { + if p.slaveAckPin.Get() { + time.Sleep(50 * time.Microsecond) + return nil + } + if retryPeriod > 0 && time.Now().After(retry) { + p.spiSendRecvByte(resendMsg) + retry = retry.Add(retryPeriod) + } + } + return timeoutErr +} + +func (p *P1AM) SetEnabled(enabled bool) { + p.baseEnablePin.Set(enabled) +}