From bd42656644a7a020d853b1fa85ab7d9504c90c72 Mon Sep 17 00:00:00 2001 From: sago35 Date: Wed, 22 Sep 2021 23:31:02 +0900 Subject: [PATCH] i2csoft: add support for software I2C --- Makefile | 4 +- README.md | 1 + examples/i2csoft/adt7410/main.go | 27 +++ i2csoft/i2csoft.go | 297 +++++++++++++++++++++++++++++++ i2csoft/i2csoft_atsamd51.go | 23 +++ i2csoft/i2csoft_esp32.go | 23 +++ i2csoft/i2csoft_other.go | 22 +++ 7 files changed, 396 insertions(+), 1 deletion(-) create mode 100644 examples/i2csoft/adt7410/main.go create mode 100644 i2csoft/i2csoft.go create mode 100644 i2csoft/i2csoft_atsamd51.go create mode 100644 i2csoft/i2csoft_esp32.go create mode 100644 i2csoft/i2csoft_other.go diff --git a/Makefile b/Makefile index f4526f2..185374b 100644 --- a/Makefile +++ b/Makefile @@ -205,12 +205,14 @@ endif @md5sum ./build/test.hex tinygo build -size short -o ./build/test.hex -target=wioterminal ./examples/rtl8720dn/mqttsub/ @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.hex -target=feather-m4 ./examples/i2csoft/adt7410/ + @md5sum ./build/test.hex 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 p1am tone tm1637 \ - pcf8563 mcp2515 servo sdcard rtl8720dn image cmd + pcf8563 mcp2515 servo sdcard rtl8720dn image cmd i2csoft TESTS = $(filter-out $(addsuffix /%,$(NOTESTS)),$(DRIVERS)) unit-test: diff --git a/README.md b/README.md index 1bea7d9..f07076c 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,7 @@ The following 67 devices are supported. | [HC-SR04 Ultrasonic distance sensor](https://cdn.sparkfun.com/datasheets/Sensors/Proximity/HCSR04.pdf) | GPIO | | [HD44780 LCD controller](https://www.sparkfun.com/datasheets/LCD/HD44780.pdf) | GPIO/I2C | | [HUB75 RGB led matrix](https://cdn-learn.adafruit.com/downloads/pdf/32x16-32x32-rgb-led-matrix.pdf) | SPI | +| [software I2C driver](https://www.ti.com/lit/an/slva704/slva704.pdf) | GPIO | | [ILI9341 TFT color display](https://cdn-shop.adafruit.com/datasheets/ILI9341.pdf) | SPI | | [INA260 Volt/Amp/Power meter](https://www.ti.com/lit/ds/symlink/ina260.pdf) | I2C | | [4x4 Membrane Keypad](https://cdn.sparkfun.com/assets/f/f/a/5/0/DS-16038.pdf) | GPIO | diff --git a/examples/i2csoft/adt7410/main.go b/examples/i2csoft/adt7410/main.go new file mode 100644 index 0000000..30b294d --- /dev/null +++ b/examples/i2csoft/adt7410/main.go @@ -0,0 +1,27 @@ +package main + +import ( + "fmt" + "machine" + "time" + + "tinygo.org/x/drivers/adt7410" + "tinygo.org/x/drivers/i2csoft" +) + +func main() { + i2c := i2csoft.New(machine.SCL_PIN, machine.SDA_PIN) + i2c.Configure(i2csoft.I2CConfig{ + Frequency: 400e3, + }) + + sensor := adt7410.New(i2c) + sensor.Configure() + + for { + temp := sensor.ReadTempF() + fmt.Printf("temperature: %f\r\n", temp) + time.Sleep(time.Second) + } + +} diff --git a/i2csoft/i2csoft.go b/i2csoft/i2csoft.go new file mode 100644 index 0000000..3c0c595 --- /dev/null +++ b/i2csoft/i2csoft.go @@ -0,0 +1,297 @@ +package i2csoft + +import ( + "errors" + "machine" +) + +// I2C is an I2C implementation by Software. Since it is implemented by +// software, it can be used with microcontrollers that do not have I2C +// function. This is not efficient but works around broken or missing drivers. +type I2C struct { + scl machine.Pin + sda machine.Pin + nack bool + baudrate uint32 +} + +// I2CConfig is used to store config info for I2C. +type I2CConfig struct { + Frequency uint32 + SCL machine.Pin + SDA machine.Pin +} + +var ( + errSI2CAckExpected = errors.New("I2C error: expected ACK not NACK") +) + +// New returns the i2csoft driver. For the arguments, specify the pins to be +// used as SCL and SDA. As I2C is implemented in software, any GPIO pin can be +// specified. +func New(sclPin, sdaPin machine.Pin) *I2C { + return &I2C{ + scl: sclPin, + sda: sdaPin, + baudrate: 100 * 1e3, + } +} + +// Configure is intended to setup the I2C interface. +func (i2c *I2C) Configure(config I2CConfig) error { + // Default I2C bus speed is 100 kHz. + if config.Frequency != 0 { + i2c.baudrate = config.Frequency + } + + // This exists for compatibility with machine.I2CConfig. SCL and SDA must + // be set at the same time. Because Pin(0) is sometimes set, it is not + // checked for 0. + if config.SCL != config.SDA { + i2c.scl = config.SCL + i2c.sda = config.SDA + } + + i2c.SetBaudRate(config.Frequency) + + // enable pins + i2c.sda.High() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinOutput}) + i2c.scl.High() + i2c.scl.Configure(machine.PinConfig{Mode: machine.PinOutput}) + + return nil +} + +// SetBaudRate sets the communication speed for the I2C. +func (i2c *I2C) SetBaudRate(br uint32) { + i2c.baudrate = br +} + +// Tx does a single I2C transaction at the specified address. +// It clocks out the given address, writes the bytes in w, reads back len(r) +// bytes and stores them in r, and generates a stop condition on the bus. +func (i2c *I2C) Tx(addr uint16, w, r []byte) error { + var err error + i2c.nack = false + if len(w) != 0 { + // send start/address for write + i2c.sendAddress(addr, true) + + // wait until transmission complete + + // ACK received (0: ACK, 1: NACK) + if i2c.nack { + i2c.signalStop() + return errSI2CAckExpected + } + + // write data + for _, b := range w { + err = i2c.WriteByte(b) + if err != nil { + return err + } + } + + err = i2c.signalStop() + if err != nil { + return err + } + } + if len(r) != 0 { + // send start/address for read + i2c.sendAddress(addr, false) + + // wait transmission complete + + // ACK received (0: ACK, 1: NACK) + if i2c.nack { + i2c.signalStop() + return errSI2CAckExpected + } + + // read first byte + r[0] = i2c.readByte() + for i := 1; i < len(r); i++ { + // Send an ACK + + i2c.signalRead() + + // Read data and send the ACK + r[i] = i2c.readByte() + } + + // Send NACK to end transmission + i2c.sendNack() + + err = i2c.signalStop() + if err != nil { + return err + } + } + + return nil +} + +// WriteByte writes a single byte to the I2C bus. +func (i2c *I2C) WriteByte(data byte) error { + // Send data byte + i2c.scl.Low() + i2c.sda.High() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinOutput}) + i2c.wait() + + for i := 0; i < 8; i++ { + i2c.scl.Low() + if ((data >> (7 - i)) & 1) == 1 { + i2c.sda.High() + } else { + i2c.sda.Low() + } + i2c.wait() + i2c.wait() + i2c.scl.High() + i2c.wait() + i2c.wait() + } + + i2c.scl.Low() + i2c.wait() + i2c.wait() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinInput}) + i2c.scl.High() + i2c.wait() + + i2c.nack = i2c.sda.Get() + + i2c.wait() + + // wait until transmission successful + + return nil +} + +// sendAddress sends the address and start signal +func (i2c *I2C) sendAddress(address uint16, write bool) error { + data := (address << 1) + if !write { + data |= 1 // set read flag + } + + i2c.scl.High() + i2c.sda.Low() + i2c.wait() + i2c.wait() + for i := 0; i < 8; i++ { + i2c.scl.Low() + if ((data >> (7 - i)) & 1) == 1 { + i2c.sda.High() + } else { + i2c.sda.Low() + } + i2c.wait() + i2c.wait() + i2c.scl.High() + i2c.wait() + i2c.wait() + } + + i2c.scl.Low() + i2c.wait() + i2c.wait() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinInput}) + i2c.scl.High() + i2c.wait() + + i2c.nack = i2c.sda.Get() + + i2c.wait() + + // wait until bus ready + + return nil +} + +func (i2c *I2C) signalStop() error { + i2c.scl.Low() + i2c.sda.Low() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinOutput}) + i2c.wait() + i2c.wait() + i2c.scl.High() + i2c.wait() + i2c.wait() + i2c.sda.High() + i2c.wait() + i2c.wait() + return nil +} + +func (i2c *I2C) signalRead() error { + i2c.wait() + i2c.wait() + i2c.scl.Low() + i2c.sda.Low() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinOutput}) + i2c.wait() + i2c.wait() + i2c.scl.High() + i2c.wait() + i2c.wait() + return nil +} + +func (i2c *I2C) readByte() byte { + var data byte + for i := 0; i < 8; i++ { + i2c.scl.Low() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinInput}) + i2c.wait() + i2c.wait() + i2c.scl.High() + if i2c.sda.Get() { + data |= 1 << (7 - i) + } + i2c.wait() + i2c.wait() + } + return data +} + +func (i2c *I2C) sendNack() error { + i2c.wait() + i2c.wait() + i2c.scl.Low() + i2c.sda.High() + i2c.sda.Configure(machine.PinConfig{Mode: machine.PinOutput}) + i2c.wait() + i2c.wait() + i2c.scl.High() + i2c.wait() + i2c.wait() + return nil +} + +// WriteRegister transmits first the register and then the data to the +// peripheral device. +// +// Many I2C-compatible devices are organized in terms of registers. This method +// is a shortcut to easily write to such registers. Also, it only works for +// devices with 7-bit addresses, which is the vast majority. +func (i2c *I2C) WriteRegister(address uint8, register uint8, data []byte) error { + buf := make([]uint8, len(data)+1) + buf[0] = register + copy(buf[1:], data) + return i2c.Tx(uint16(address), buf, nil) +} + +// ReadRegister transmits the register, restarts the connection as a read +// operation, and reads the response. +// +// Many I2C-compatible devices are organized in terms of registers. This method +// is a shortcut to easily read such registers. Also, it only works for devices +// with 7-bit addresses, which is the vast majority. +func (i2c *I2C) ReadRegister(address uint8, register uint8, data []byte) error { + return i2c.Tx(uint16(address), []byte{register}, data) +} diff --git a/i2csoft/i2csoft_atsamd51.go b/i2csoft/i2csoft_atsamd51.go new file mode 100644 index 0000000..8b8b384 --- /dev/null +++ b/i2csoft/i2csoft_atsamd51.go @@ -0,0 +1,23 @@ +//go:build atsamd51 || atsame5x +// +build atsamd51 atsame5x + +package i2csoft + +import ( + "device/arm" +) + +func (i2c *I2C) wait() { + // atsamd51 + // 1 : about 388kHz + // 17 : about 97kHz + + wait := 1 + if i2c.baudrate < 400*1e3 { + wait = 17 + } + + for i := 0; i < wait; i++ { + arm.Asm(`nop`) + } +} diff --git a/i2csoft/i2csoft_esp32.go b/i2csoft/i2csoft_esp32.go new file mode 100644 index 0000000..b9bee10 --- /dev/null +++ b/i2csoft/i2csoft_esp32.go @@ -0,0 +1,23 @@ +//go:build esp32 +// +build esp32 + +package i2csoft + +import ( + "device" +) + +func (i2c *I2C) wait() { + // atsamd51 + // 10 : about 387kHz + // 56 : about 99kHz + + wait := 10 + if i2c.baudrate < 400*1e3 { + wait = 56 + } + + for i := 0; i < wait; i++ { + device.Asm(`nop`) + } +} diff --git a/i2csoft/i2csoft_other.go b/i2csoft/i2csoft_other.go new file mode 100644 index 0000000..f305533 --- /dev/null +++ b/i2csoft/i2csoft_other.go @@ -0,0 +1,22 @@ +//go:build !esp32 && !atsamd51 && !atsame5x +// +build !esp32,!atsamd51,!atsame5x + +package i2csoft + +import ( + "device" +) + +func (i2c *I2C) wait() { + // atsamd21 @ 48MHz + // 1 : about 360kHz + // 24 : about 96kHz + wait := 1 + if i2c.baudrate < 400*1e3 { + wait = 19 + } + + for i := 0; i < wait; i++ { + device.Asm(`nop`) + } +}