diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | src/examples/i2c-target/main.go | 130 | ||||
-rw-r--r-- | src/examples/i2c-target/main_feather_nrf52840.go | 15 | ||||
-rw-r--r-- | src/examples/i2c-target/main_feather_rp2040.go | 15 | ||||
-rw-r--r-- | src/machine/i2c.go | 31 | ||||
-rw-r--r-- | src/machine/machine_nrf.go | 21 | ||||
-rw-r--r-- | src/machine/machine_nrf528xx.go | 120 | ||||
-rw-r--r-- | src/machine/machine_nrf5x.go | 7 | ||||
-rw-r--r-- | src/machine/machine_rp2040_i2c.go | 165 |
9 files changed, 479 insertions, 27 deletions
@@ -482,6 +482,8 @@ smoketest: @$(MD5SUM) test.hex $(TINYGO) build -size short -o test.hex -target=wioterminal examples/hid-keyboard @$(MD5SUM) test.hex + $(TINYGO) build -size short -o test.hex -target=feather-rp2040 examples/i2c-target + @$(MD5SUM) test.hex # test simulated boards on play.tinygo.org ifneq ($(WASM), 0) $(TINYGO) build -size short -o test.wasm -tags=arduino examples/blinky1 diff --git a/src/examples/i2c-target/main.go b/src/examples/i2c-target/main.go new file mode 100644 index 000000000..cd984ba3c --- /dev/null +++ b/src/examples/i2c-target/main.go @@ -0,0 +1,130 @@ +// Example demonstrating I2C controller / target comms. +// +// To use this example, physically connect I2C0 and I2C1. +// I2C0 will be used as the controller and I2C1 used as +// the target. +// +// In this example, the target implements a simple memory +// map, with the controller able to read and write the +// memory. + +package main + +import ( + "machine" + "time" +) + +const ( + targetAddress = 0x11 + maxTxSize = 16 +) + +func main() { + // Delay to enable USB monitor time to attach + time.Sleep(3 * time.Second) + + // Controller uses default I2C pins and controller + // mode is default + err := controller.Configure(machine.I2CConfig{}) + if err != nil { + panic("failed to config I2C0 as controller") + } + + // Target uses alternate pins and target mode is + // explicit + err = target.Configure(machine.I2CConfig{ + Mode: machine.I2CModeTarget, + SCL: TARGET_SCL, + SDA: TARGET_SDA, + }) + if err != nil { + panic("failed to config I2C1 as target") + } + + // Put welcome message directly into target memory + copy(mem[0:], []byte("Hello World!")) + err = target.Listen(targetAddress) + if err != nil { + panic("failed to listen as I2C target") + } + + // Start the target + go targetMain() + + // Read welcome message from target over I2C + buf := make([]byte, 12) + err = controller.Tx(targetAddress, []byte{0}, buf) + if err != nil { + println("failed to read welcome message over I2C") + panic(err) + } + println("message from target:", string(buf)) + + // Write (1,2,3) to the target starting at memory address 3 + println("writing (1,2,3) @ 0x3") + err = controller.Tx(targetAddress, []byte{3, 1, 2, 3}, nil) + if err != nil { + println("failed to write to I2C target") + panic(err) + } + + time.Sleep(100 * time.Millisecond) // Wait for target to process write + println("mem:", mem[0], mem[1], mem[2], mem[3], mem[4], mem[5]) + + // Read memory address 4 from target, which should be the value 2 + buf = make([]byte, 1) + err = controller.Tx(targetAddress, []byte{4}, buf[:1]) + if err != nil { + println("failed to read from I2C target") + panic(err) + } + + if buf[0] != 2 { + panic("read incorrect value from I2C target") + } + + println("all done!") + for { + time.Sleep(1 * time.Second) + } +} + +// -- target --- + +var ( + mem [256]byte +) + +// targetMain implements the 'main loop' for an I2C target +func targetMain() { + buf := make([]byte, maxTxSize) + var ptr uint8 + + for { + evt, n, err := target.WaitForEvent(buf) + if err != nil { + panic(err) + } + + switch evt { + case machine.I2CReceive: + if n > 0 { + ptr = buf[0] + } + + for o := 1; o < n; o++ { + mem[ptr] = buf[o] + ptr++ + } + + case machine.I2CRequest: + target.Reply(mem[ptr:256]) + + case machine.I2CFinish: + // nothing to do + + default: + } + } +} diff --git a/src/examples/i2c-target/main_feather_nrf52840.go b/src/examples/i2c-target/main_feather_nrf52840.go new file mode 100644 index 000000000..6c94cfe58 --- /dev/null +++ b/src/examples/i2c-target/main_feather_nrf52840.go @@ -0,0 +1,15 @@ +//go:build feather_nrf52840 + +package main + +import "machine" + +const ( + TARGET_SCL = machine.A5 + TARGET_SDA = machine.A4 +) + +var ( + controller = machine.I2C0 + target = machine.I2C1 +) diff --git a/src/examples/i2c-target/main_feather_rp2040.go b/src/examples/i2c-target/main_feather_rp2040.go new file mode 100644 index 000000000..6f0581bba --- /dev/null +++ b/src/examples/i2c-target/main_feather_rp2040.go @@ -0,0 +1,15 @@ +//go:build rp2040 + +package main + +import "machine" + +const ( + TARGET_SCL = machine.GPIO25 + TARGET_SDA = machine.GPIO24 +) + +var ( + controller = machine.I2C1 + target = machine.I2C0 +) diff --git a/src/machine/i2c.go b/src/machine/i2c.go index 4e7a630b9..c8bd6d1b4 100644 --- a/src/machine/i2c.go +++ b/src/machine/i2c.go @@ -23,6 +23,37 @@ var ( errI2CSignalStopTimeout = errors.New("I2C timeout on signal stop") errI2CAckExpected = errors.New("I2C error: expected ACK not NACK") errI2CBusError = errors.New("I2C bus error") + errI2COverflow = errors.New("I2C receive buffer overflow") + errI2COverread = errors.New("I2C transmit buffer overflow") +) + +// I2CTargetEvent reflects events on the I2C bus +type I2CTargetEvent uint8 + +const ( + // I2CReceive indicates target has received a message from the controller. + I2CReceive I2CTargetEvent = iota + + // I2CRequest indicates the controller is expecting a message from the target. + I2CRequest + + // I2CFinish indicates the controller has ended the transaction. + // + // I2C controllers can chain multiple receive/request messages without + // relinquishing the bus by doing 'restarts'. I2CFinish indicates the + // bus has been relinquished by an I2C 'stop'. + I2CFinish +) + +// I2CMode determines if an I2C peripheral is in Controller or Target mode. +type I2CMode int + +const ( + // I2CModeController represents an I2C peripheral in controller mode. + I2CModeController I2CMode = iota + + // I2CModeTarget represents an I2C peripheral in target mode. + I2CModeTarget ) // WriteRegister transmits first the register and then the data to the diff --git a/src/machine/machine_nrf.go b/src/machine/machine_nrf.go index e457db9db..979693a10 100644 --- a/src/machine/machine_nrf.go +++ b/src/machine/machine_nrf.go @@ -206,6 +206,7 @@ type I2CConfig struct { Frequency uint32 SCL Pin SDA Pin + Mode I2CMode } // Configure is intended to setup the I2C interface. @@ -238,15 +239,21 @@ func (i2c *I2C) Configure(config I2CConfig) error { (nrf.GPIO_PIN_CNF_DRIVE_S0D1 << nrf.GPIO_PIN_CNF_DRIVE_Pos) | (nrf.GPIO_PIN_CNF_SENSE_Disabled << nrf.GPIO_PIN_CNF_SENSE_Pos)) - if config.Frequency >= 400*KHz { - i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K400) - } else { - i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K100) - } - i2c.setPins(config.SCL, config.SDA) - i2c.enableAsController() + i2c.mode = config.Mode + + if i2c.mode == I2CModeController { + if config.Frequency >= 400*KHz { + i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K400) + } else { + i2c.Bus.FREQUENCY.Set(nrf.TWI_FREQUENCY_FREQUENCY_K100) + } + + i2c.enableAsController() + } else { + i2c.enableAsTarget() + } return nil } diff --git a/src/machine/machine_nrf528xx.go b/src/machine/machine_nrf528xx.go index b3e95a253..019a66cf1 100644 --- a/src/machine/machine_nrf528xx.go +++ b/src/machine/machine_nrf528xx.go @@ -9,19 +9,25 @@ import ( // I2C on the NRF528xx. type I2C struct { - Bus *nrf.TWIM_Type + Bus *nrf.TWIM_Type // Called Bus to align with Bus field in nrf51 + BusT *nrf.TWIS_Type + mode I2CMode } // There are 2 I2C interfaces on the NRF. var ( - I2C0 = &I2C{Bus: nrf.TWIM0} - I2C1 = &I2C{Bus: nrf.TWIM1} + I2C0 = &I2C{Bus: nrf.TWIM0, BusT: nrf.TWIS0} + I2C1 = &I2C{Bus: nrf.TWIM1, BusT: nrf.TWIS1} ) func (i2c *I2C) enableAsController() { i2c.Bus.ENABLE.Set(nrf.TWIM_ENABLE_ENABLE_Enabled) } +func (i2c *I2C) enableAsTarget() { + i2c.BusT.ENABLE.Set(nrf.TWIS_ENABLE_ENABLE_Enabled) +} + func (i2c *I2C) disable() { i2c.Bus.ENABLE.Set(0) } @@ -86,6 +92,99 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) { return } +// Listen starts listening for I2C requests sent to specified address +// +// addr is the address to listen to +func (i2c *I2C) Listen(addr uint8) error { + i2c.BusT.ADDRESS[0].Set(uint32(addr)) + i2c.BusT.CONFIG.Set(nrf.TWIS_CONFIG_ADDRESS0_Enabled) + + i2c.BusT.EVENTS_STOPPED.Set(0) + i2c.BusT.EVENTS_ERROR.Set(0) + i2c.BusT.EVENTS_RXSTARTED.Set(0) + i2c.BusT.EVENTS_TXSTARTED.Set(0) + i2c.BusT.EVENTS_WRITE.Set(0) + i2c.BusT.EVENTS_READ.Set(0) + + return nil +} + +// WaitForEvent blocks the current go-routine until an I2C event is received (when in Target mode). +// +// The passed buffer will be populated for receive events, with the number of bytes +// received returned in count. For other event types, buf is not modified and a count +// of zero is returned. +// +// For request events, the caller MUST call `Reply` to avoid hanging the i2c bus indefinitely. +func (i2c *I2C) WaitForEvent(buf []byte) (evt I2CTargetEvent, count int, err error) { + i2c.BusT.RXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&buf[0])))) + i2c.BusT.RXD.MAXCNT.Set(uint32(len(buf))) + + i2c.BusT.TASKS_PREPARERX.Set(nrf.TWIS_TASKS_PREPARERX_TASKS_PREPARERX_Trigger) + + i2c.Bus.TASKS_RESUME.Set(1) + + for i2c.BusT.EVENTS_STOPPED.Get() == 0 && + i2c.BusT.EVENTS_READ.Get() == 0 { + gosched() + + if i2c.BusT.EVENTS_ERROR.Get() != 0 { + i2c.BusT.EVENTS_ERROR.Set(0) + return I2CReceive, 0, twisError(i2c.BusT.ERRORSRC.Get()) + } + } + + count = 0 + evt = I2CFinish + err = nil + + if i2c.BusT.EVENTS_WRITE.Get() != 0 { + i2c.BusT.EVENTS_WRITE.Set(0) + + // Data was sent to this target. We've waited for + // READ or STOPPED event, so transmission should be + // complete. + count = int(i2c.BusT.RXD.AMOUNT.Get()) + evt = I2CReceive + } else if i2c.BusT.EVENTS_READ.Get() != 0 { + i2c.BusT.EVENTS_READ.Set(0) + + // Data is requested from this target, hw will stretch + // the controller's clock until there is a reply to + // send + evt = I2CRequest + } else if i2c.BusT.EVENTS_STOPPED.Get() != 0 { + i2c.BusT.EVENTS_STOPPED.Set(0) + evt = I2CFinish + } + + return +} + +// Reply supplies the response data the controller. +func (i2c *I2C) Reply(buf []byte) error { + i2c.BusT.TXD.PTR.Set(uint32(uintptr(unsafe.Pointer(&buf[0])))) + i2c.BusT.TXD.MAXCNT.Set(uint32(len(buf))) + + i2c.BusT.EVENTS_STOPPED.Set(0) + + // Trigger Tx + i2c.BusT.TASKS_PREPARETX.Set(nrf.TWIS_TASKS_PREPARETX_TASKS_PREPARETX_Trigger) + + // Block, waiting for Tx to complete + for i2c.BusT.EVENTS_STOPPED.Get() == 0 { + gosched() + + if i2c.BusT.EVENTS_ERROR.Get() != 0 { + return twisError(i2c.BusT.ERRORSRC.Get()) + } + } + + i2c.BusT.EVENTS_STOPPED.Set(0) + + return nil +} + // twiCError converts an I2C controller error to Go func twiCError(val uint32) error { if val == 0 { @@ -100,3 +199,18 @@ func twiCError(val uint32) error { return errI2CBusError } + +// twisError converts an I2C target error to Go +func twisError(val uint32) error { + if val == 0 { + return nil + } else if val&nrf.TWIS_ERRORSRC_OVERFLOW_Msk == nrf.TWIS_ERRORSRC_OVERFLOW { + return errI2COverflow + } else if val&nrf.TWIS_ERRORSRC_DNACK_Msk == nrf.TWIS_ERRORSRC_DNACK { + return errI2CAckExpected + } else if val&nrf.TWIS_ERRORSRC_OVERREAD_Msk == nrf.TWIS_ERRORSRC_OVERREAD { + return errI2COverread + } + + return errI2CBusError +} diff --git a/src/machine/machine_nrf5x.go b/src/machine/machine_nrf5x.go index dbf2e1ccf..f36c7c93c 100644 --- a/src/machine/machine_nrf5x.go +++ b/src/machine/machine_nrf5x.go @@ -6,7 +6,8 @@ import "device/nrf" // I2C on the NRF51 and NRF52. type I2C struct { - Bus *nrf.TWI_Type + Bus *nrf.TWI_Type + mode I2CMode } // There are 2 I2C interfaces on the NRF. @@ -19,6 +20,10 @@ func (i2c *I2C) enableAsController() { i2c.Bus.ENABLE.Set(nrf.TWI_ENABLE_ENABLE_Enabled) } +func (i2c *I2C) enableAsTarget() { + // Not supported on this hardware +} + func (i2c *I2C) disable() { i2c.Bus.ENABLE.Set(0) } diff --git a/src/machine/machine_rp2040_i2c.go b/src/machine/machine_rp2040_i2c.go index 0c5b688ef..4a4867c81 100644 --- a/src/machine/machine_rp2040_i2c.go +++ b/src/machine/machine_rp2040_i2c.go @@ -20,10 +20,13 @@ var ( } ) +// The I2C target implementation is based on the C implementation from +// here: https://github.com/vmilea/pico_i2c_slave + // Features: Taken from datasheet. -// Default master mode, with slave mode available (not simulataneously). -// Default slave address of RP2040: 0x055 -// Supports 10-bit addressing in Master mode +// Default controller mode, with target mode available (not simulataneously). +// Default target address of RP2040: 0x055 +// Supports 10-bit addressing in controller mode // 16-element transmit buffer // 16-element receive buffer // Can be driven from DMA @@ -45,20 +48,26 @@ type I2CConfig struct { // SDA/SCL Serial Data and clock pins. Refer to datasheet to see // which pins match the desired bus. SDA, SCL Pin + Mode I2CMode } type I2C struct { Bus *rp.I2C0_Type restartOnNext bool + mode I2CMode + txInProgress bool } var ( - ErrInvalidI2CBaudrate = errors.New("invalid i2c baudrate") - ErrInvalidTgtAddr = errors.New("invalid target i2c address not in 0..0x80 or is reserved") - ErrI2CGeneric = errors.New("i2c error") - ErrRP2040I2CDisable = errors.New("i2c rp2040 peripheral timeout in disable") - errInvalidI2CSDA = errors.New("invalid I2C SDA pin") - errInvalidI2CSCL = errors.New("invalid I2C SCL pin") + ErrInvalidI2CBaudrate = errors.New("invalid i2c baudrate") + ErrInvalidTgtAddr = errors.New("invalid target i2c address not in 0..0x80 or is reserved") + ErrI2CGeneric = errors.New("i2c error") + ErrRP2040I2CDisable = errors.New("i2c rp2040 peripheral timeout in disable") + errInvalidI2CSDA = errors.New("invalid I2C SDA pin") + errInvalidI2CSCL = errors.New("invalid I2C SCL pin") + ErrI2CAlreadyListening = errors.New("i2c already listening") + ErrI2CWrongMode = errors.New("i2c wrong mode") + ErrI2CUnderflow = errors.New("i2c underflow") ) // Tx performs a write and then a read transfer placing the result in @@ -75,11 +84,26 @@ var ( // // Performs only a write transfer. func (i2c *I2C) Tx(addr uint16, w, r []byte) error { + if i2c.mode != I2CModeController { + return ErrI2CWrongMode + } + // timeout in microseconds. const timeout = 40 * 1000 // 40ms is a reasonable time for a real-time system. return i2c.tx(uint8(addr), w, r, timeout) } +// Listen starts listening for I2C requests sent to specified address +// +// addr is the address to listen to +func (i2c *I2C) Listen(addr uint16) error { + if i2c.mode != I2CModeTarget { + return ErrI2CWrongMode + } + + return i2c.listen(uint8(addr)) +} + // Configure initializes i2c peripheral and configures I2C config's pins passed. // Here's a list of valid SDA and SCL GPIO pins on bus I2C0 of the rp2040: // @@ -213,14 +237,22 @@ func (i2c *I2C) init(config I2CConfig) error { return err } i2c.restartOnNext = false - // Configure as a fast-mode master with RepStart support, 7-bit addresses - i2c.Bus.IC_CON.Set((rp.I2C0_IC_CON_SPEED_FAST << rp.I2C0_IC_CON_SPEED_Pos) | - rp.I2C0_IC_CON_MASTER_MODE | rp.I2C0_IC_CON_IC_SLAVE_DISABLE | - rp.I2C0_IC_CON_IC_RESTART_EN | rp.I2C0_IC_CON_TX_EMPTY_CTRL) // sets TX_EMPTY_CTRL to enable TX_EMPTY interrupt status + + i2c.mode = config.Mode + + // Configure as fast-mode with RepStart support, 7-bit addresses + mode := uint32(rp.I2C0_IC_CON_SPEED_FAST<<rp.I2C0_IC_CON_SPEED_Pos) | + rp.I2C0_IC_CON_IC_RESTART_EN | rp.I2C0_IC_CON_TX_EMPTY_CTRL // sets TX_EMPTY_CTRL to enable TX_EMPTY interrupt status + if config.Mode == I2CModeController { + mode |= rp.I2C0_IC_CON_MASTER_MODE | rp.I2C0_IC_CON_IC_SLAVE_DISABLE + } + i2c.Bus.IC_CON.Set(mode) // Set FIFO watermarks to 1 to make things simpler. This is encoded by a register value of 0. - i2c.Bus.IC_TX_TL.Set(0) - i2c.Bus.IC_RX_TL.Set(0) + if config.Mode == I2CModeController { + i2c.Bus.IC_TX_TL.Set(0) + i2c.Bus.IC_RX_TL.Set(0) + } // Always enable the DREQ signalling -- harmless if DMA isn't listening i2c.Bus.IC_DMA_CR.Set(rp.I2C0_IC_DMA_CR_TDMAE | rp.I2C0_IC_DMA_CR_RDMAE) @@ -300,12 +332,14 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte, timeout_us uint64) (err error) { // IC_ENABLE[0] is set to 0, the TX FIFO is flushed and held // in reset. There the TX FIFO looks like it has no data within // it, so this bit is set to 1, provided there is activity in the - // master or slave state machines. When there is no longer + // controller or target state machines. When there is no longer // any activity, then with ic_en=0, this bit is set to 0. for !i2c.interrupted(rp.I2C0_IC_RAW_INTR_STAT_TX_EMPTY) { if ticks() > deadline { return errI2CWriteTimeout // If there was a timeout, don't attempt to do anything else. } + + gosched() } abortReason = i2c.getAbortReason() @@ -324,6 +358,8 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte, timeout_us uint64) (err error) { if ticks() > deadline { return errI2CWriteTimeout } + + gosched() } i2c.Bus.IC_CLR_STOP_DET.Get() } @@ -334,6 +370,7 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte, timeout_us uint64) (err error) { first := rxCtr == 0 last := rxCtr == rxlen-1 for i2c.writeAvailable() == 0 { + gosched() } i2c.Bus.IC_DATA_CMD.Set( boolToBit(first && i2c.restartOnNext)<<rp.I2C0_IC_DATA_CMD_RESTART_Pos | @@ -349,6 +386,8 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte, timeout_us uint64) (err error) { if ticks() > deadline { return errI2CReadTimeout // If there was a timeout, don't attempt to do anything else. } + + gosched() } if abort { break @@ -374,6 +413,100 @@ func (i2c *I2C) tx(addr uint8, tx, rx []byte, timeout_us uint64) (err error) { return err } +// listen sets up for async handling of requests on the I2C bus. +func (i2c *I2C) listen(addr uint8) error { + if addr >= 0x80 || isReservedI2CAddr(addr) { + return ErrInvalidTgtAddr + } + + err := i2c.disable() + if err != nil { + return err + } + + i2c.Bus.IC_SAR.Set(uint32(addr)) + + i2c.enable() + + return nil +} + +func (i2c *I2C) WaitForEvent(buf []byte) (evt I2CTargetEvent, count int, err error) { + rxPtr := 0 + for { + stat := i2c.Bus.IC_RAW_INTR_STAT.Get() + + if stat&rp.I2C0_IC_INTR_MASK_M_RX_FULL != 0 { + b := uint8(i2c.Bus.IC_DATA_CMD.Get()) + if rxPtr < len(buf) { + buf[rxPtr] = b + rxPtr++ + } + } + + // Stop + if stat&rp.I2C0_IC_INTR_MASK_M_STOP_DET != 0 { + if rxPtr > 0 { + return I2CReceive, rxPtr, nil + } + + i2c.Bus.IC_CLR_STOP_DET.Get() // clear + return I2CFinish, 0, nil + } + + // Start or restart - ignore start, return on restart + if stat&rp.I2C0_IC_INTR_MASK_M_START_DET != 0 { + i2c.Bus.IC_CLR_START_DET.Get() // clear restart + + // Restart + if rxPtr > 0 { + return I2CReceive, rxPtr, nil + } + } + + // Read request - leave flag set until we start to reply. + if stat&rp.I2C0_IC_INTR_MASK_M_RD_REQ != 0 { + return I2CRequest, 0, nil + } + + gosched() + } +} + +func (i2c *I2C) Reply(buf []byte) error { + txPtr := 0 + + stat := i2c.Bus.IC_RAW_INTR_STAT.Get() + + if stat&rp.I2C0_IC_INTR_MASK_M_RD_REQ == 0 { + return ErrI2CWrongMode + } + i2c.Bus.IC_CLR_RD_REQ.Get() // clear restart + + // Clear any dangling TX abort + if stat&rp.I2C0_IC_INTR_MASK_M_TX_ABRT != 0 { + i2c.Bus.IC_CLR_TX_ABRT.Get() + } + + for txPtr < len(buf) { + if stat&rp.I2C0_IC_INTR_MASK_M_TX_EMPTY != 0 { + i2c.Bus.IC_DATA_CMD.Set(uint32(buf[txPtr])) + txPtr++ + } + + // This Tx abort is a normal case - we're sending more + // data than controller wants to receive + if stat&rp.I2C0_IC_INTR_MASK_M_TX_ABRT != 0 { + i2c.Bus.IC_CLR_TX_ABRT.Get() + return nil + } + + gosched() + } + + return nil +} + // writeAvailable determines non-blocking write space available // //go:inline |