From 9d1fd13a2f1415de07b78dff0a247c4b7c55fd05 Mon Sep 17 00:00:00 2001 From: Thomas Kohler Date: Sun, 12 Jun 2022 19:00:09 +0200 Subject: [PATCH] i2c PCF8583 clock and counter driver introduced --- README.md | 1 + drivers/i2c/README.md | 1 + drivers/i2c/pcf8583_driver.go | 360 ++++++++++++++ drivers/i2c/pcf8583_driver_test.go | 612 ++++++++++++++++++++++++ examples/tinkerboard_pcf8583_clock.go | 61 +++ examples/tinkerboard_pcf8583_counter.go | 61 +++ 6 files changed, 1096 insertions(+) create mode 100644 drivers/i2c/pcf8583_driver.go create mode 100644 drivers/i2c/pcf8583_driver_test.go create mode 100644 examples/tinkerboard_pcf8583_clock.go create mode 100644 examples/tinkerboard_pcf8583_counter.go diff --git a/README.md b/README.md index 644114f1..bd545cfb 100644 --- a/README.md +++ b/README.md @@ -288,6 +288,7 @@ drivers provided using the `gobot/drivers/i2c` package: - MPU6050 Accelerometer/Gyroscope - PCA9501 8-bit I/O port with interrupt, 2-kbit EEPROM - PCA9685 16-channel 12-bit PWM/Servo Driver + - PCF8583 clock and calendar or event counter, 240 x 8-bit RAM - PCF8591 8-bit 4xA/D & 1xD/A converter - SHT2x Temperature/Humidity - SHT3x-D Temperature/Humidity diff --git a/drivers/i2c/README.md b/drivers/i2c/README.md index 874ebe1e..04440f2e 100644 --- a/drivers/i2c/README.md +++ b/drivers/i2c/README.md @@ -40,6 +40,7 @@ Gobot has a extensible system for connecting to hardware devices. The following - MPU6050 Accelerometer/Gyroscope - PCA9501 8-bit I/O port with interrupt, 2-kbit EEPROM - PCA9685 16-channel 12-bit PWM/Servo Driver +- PCF8583 clock and calendar or event counter, 240 x 8-bit RAM - PCF8591 8-bit 4xA/D & 1xD/A converter - SHT2x Temperature/Humidity - SHT3x-D Temperature/Humidity diff --git a/drivers/i2c/pcf8583_driver.go b/drivers/i2c/pcf8583_driver.go new file mode 100644 index 00000000..4ae6197a --- /dev/null +++ b/drivers/i2c/pcf8583_driver.go @@ -0,0 +1,360 @@ +package i2c + +import ( + "fmt" + "log" + "time" +) + +const ( + pcf8583Debug = true + + // PCF8583 supports addresses 0x50 and 0x51 + // The default address applies when the address pin is grounded. + pcf8583DefaultAddress = 0x50 + + // default is 0x10, when set to 0 also some free or unused RAM can be accessed + pcf8583RamOffset = 0x10 +) + +// PCF8583Control is used to specify control and status register content +type PCF8583Control uint8 + +const ( + // registers are named according to the datasheet + pcf8583Reg_CTRL = iota // 0x00 + pcf8583Reg_SUBSEC_D0D1 // 0x01 + pcf8583Reg_SEC_D2D3 // 0x02 + pcf8583Reg_MIN_D4D5 // 0x03 + pcf8583Reg_HOUR // 0x04 + pcf8583Reg_YEARDATE // 0x05 + pcf8583Reg_WEEKDAYMONTH // 0x06 + pcf8583Reg_TIMER // 0x07 + pcf8583Reg_ALARMCTRL // 0x08, offset for all alarm registers 0x09 ... 0xF + + pcf8583CtrlTimerFlag PCF8583Control = 0x01 // 50% duty factor, seconds flag if alarm enable bit is 0 + pcf8583CtrlAlarmFlag PCF8583Control = 0x02 // 50% duty factor, minutes flag if alarm enable bit is 0 + pcf8583CtrlAlarmEnable PCF8583Control = 0x04 // if enabled, memory 08h is alarm control register + pcf8583CtrlMask PCF8583Control = 0x08 // 0: read 05h, 06h unmasked, 1: read date and month count directly + PCF8583CtrlModeClock50 PCF8583Control = 0x10 // clock mode with 50 Hz + PCF8583CtrlModeCounter PCF8583Control = 0x20 // event counter mode + PCF8583CtrlModeTest PCF8583Control = 0x30 // test mode + pcf8583CtrlHoldLastCount PCF8583Control = 0x40 // 0: count, 1: store and hold count in capture latches + pcf8583CtrlStopCounting PCF8583Control = 0x80 // 0: count, 1: stop counting, reset divider +) + +// PCF8583Driver is a Gobot Driver for the PCF8583 clock and calendar chip & 240 x 8-bit bit RAM with 1 address program pin. +// please refer to data sheet: https://www.nxp.com/docs/en/data-sheet/PCF8583.pdf +// +// 0 1 0 1 0 0 0 A0|rd +// Lowest bit (rd) is mapped to switch between write(0)/read(1), it is not part of the "real" address. +// +// PCF8583 is mainly compatible to PCF8593, so this driver should also work for PCF8593 except RAM calls +// +// This driver was tested with Tinkerboard. +type PCF8583Driver struct { + *Driver + mode PCF8583Control // clock 32.768kHz (default), clock 50Hz, event counter + yearOffset int + ramOffset byte +} + +// NewPCF8583Driver creates a new driver with specified i2c interface +// Params: +// c Connector - the Adaptor to use with this Driver +// +// Optional params: +// i2c.WithBus(int): bus to use with this driver +// i2c.WithAddress(int): address to use with this driver +// i2c.WithPCF8583Mode(PCF8583Control): mode of this driver +// +func NewPCF8583Driver(c Connector, options ...func(Config)) *PCF8583Driver { + d := &PCF8583Driver{ + Driver: NewDriver(c, "PCF8583", pcf8583DefaultAddress), + ramOffset: pcf8583RamOffset, + } + d.afterStart = d.initialize + + for _, option := range options { + option(d) + } + + // API commands + d.AddCommand("WriteTime", func(params map[string]interface{}) interface{} { + val := params["val"].(time.Time) + err := d.WriteTime(val) + return map[string]interface{}{"err": err} + }) + + d.AddCommand("ReadTime", func(params map[string]interface{}) interface{} { + val, err := d.ReadTime() + return map[string]interface{}{"val": val, "err": err} + }) + + d.AddCommand("WriteCounter", func(params map[string]interface{}) interface{} { + val := params["val"].(int32) + err := d.WriteCounter(val) + return map[string]interface{}{"err": err} + }) + + d.AddCommand("ReadCounter", func(params map[string]interface{}) interface{} { + val, err := d.ReadCounter() + return map[string]interface{}{"val": val, "err": err} + }) + + d.AddCommand("WriteRAM", func(params map[string]interface{}) interface{} { + address := params["address"].(uint8) + val := params["val"].(uint8) + err := d.WriteRAM(address, val) + return map[string]interface{}{"err": err} + }) + + d.AddCommand("ReadRAM", func(params map[string]interface{}) interface{} { + address := params["address"].(uint8) + val, err := d.ReadRAM(address) + return map[string]interface{}{"val": val, "err": err} + }) + return d +} + +// WithPCF8583Mode is used to change the mode between 32.678kHz clock, 50Hz clock, event counter +// Valid settings are of type "PCF8583Control" +func WithPCF8583Mode(mode PCF8583Control) func(Config) { + return func(c Config) { + d, ok := c.(*PCF8583Driver) + if ok { + if !mode.isClockMode() && !mode.isCounterMode() { + panic(fmt.Sprintf("%s: mode 0x%02x is not supported", d.name, mode)) + } + d.mode = mode + } else if pcf8583Debug { + log.Printf("trying to set mode for non-PCF8583Driver %v", c) + } + } +} + +// WriteTime setup the clock registers with the given time +func (d *PCF8583Driver) WriteTime(val time.Time) error { + d.mutex.Lock() + defer d.mutex.Unlock() + + // according to chapter 7.11 of the product data sheet, the stop counting flag of the control/status register + // must be set before, so we read the control byte before and only set/reset the stop + ctrlRegVal, err := d.connection.ReadByteData(uint8(pcf8583Reg_CTRL)) + if err != nil { + return err + } + if !PCF8583Control(ctrlRegVal).isClockMode() { + return fmt.Errorf("%s: can't write time because the device is in wrong mode 0x%02x", d.name, ctrlRegVal) + } + // auto increment feature is used + year, month, day := val.Date() + written, err := d.connection.Write([]byte{ + uint8(pcf8583Reg_CTRL), ctrlRegVal | uint8(pcf8583CtrlStopCounting), + pcf8583encodeBcd(uint8(val.Nanosecond() / 1000000 / 10)), // sub seconds in 1/10th seconds + pcf8583encodeBcd(uint8(val.Second())), + pcf8583encodeBcd(uint8(val.Minute())), + pcf8583encodeBcd(uint8(val.Hour())), + pcf8583encodeBcd(uint8(day)), // year, date (we keep the year counter zero and set the offset) + uint8(val.Weekday())<<5 | pcf8583encodeBcd(uint8(month)), // month, weekday (not BCD): Sunday = 0, Monday = 1 ... + }) + if err != nil { + return err + } + if written != 8 { + return fmt.Errorf("%s: %d bytes written, but %d expected", d.name, written, 8) + } + d.yearOffset = year + return d.run(ctrlRegVal) +} + +// ReadTime reads the clock and returns the value +func (d *PCF8583Driver) ReadTime() (val time.Time, err error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + // according to chapter 7.1 of the product data sheet, the setting of "hold last count" flag + // is not needed when reading with auto increment + ctrlRegVal, err := d.connection.ReadByteData(uint8(pcf8583Reg_CTRL)) + if err != nil { + return + } + if !PCF8583Control(ctrlRegVal).isClockMode() { + return val, fmt.Errorf("%s: can't read time because the device is in wrong mode 0x%02x", d.name, ctrlRegVal) + } + // auto increment feature is used + clockDataSize := 6 + data := make([]byte, clockDataSize) + read, err := d.connection.Read(data) + if err != nil { + return + } + if read != clockDataSize { + return val, fmt.Errorf("%s: %d bytes read, but %d expected", d.name, read, clockDataSize) + } + nanos := int(pcf8583decodeBcd(data[0])) * 1000000 * 10 // sub seconds in 1/10th seconds + seconds := int(pcf8583decodeBcd(data[1])) + minutes := int(pcf8583decodeBcd(data[2])) + hours := int(pcf8583decodeBcd(data[3])) + // year, date (the device can only count 4 years) + year := int(data[4]>>6) + d.yearOffset // use the first two bits, no BCD + date := int(pcf8583decodeBcd(data[4] & 0x3F)) // remove the year-bits for date + // weekday (not used here), month + month := time.Month(pcf8583decodeBcd(data[5] & 0x1F)) // remove the weekday-bits + return time.Date(year, month, date, hours, minutes, seconds, nanos, time.UTC), nil +} + +// WriteCounter writes the counter registers +func (d *PCF8583Driver) WriteCounter(val int32) error { + d.mutex.Lock() + defer d.mutex.Unlock() + + // we don't care of negative values here + // according to chapter 7.11 of the product data sheet, the stop counting flag of the control/status register + // must be set before, so we read the control byte before and only set/reset the stop + ctrlRegVal, err := d.connection.ReadByteData(uint8(pcf8583Reg_CTRL)) + if err != nil { + return err + } + if !PCF8583Control(ctrlRegVal).isCounterMode() { + return fmt.Errorf("%s: can't write counter because the device is in wrong mode 0x%02x", d.name, ctrlRegVal) + } + // auto increment feature is used, PCF8583 not working with WriteBlockData + written, err := d.connection.Write([]byte{ + uint8(pcf8583Reg_CTRL), ctrlRegVal | uint8(pcf8583CtrlStopCounting), // stop + pcf8583encodeBcd(uint8(val % 100)), // 2 lowest digits + pcf8583encodeBcd(uint8((val / 100) % 100)), // 2 middle digits + pcf8583encodeBcd(uint8((val / 10000) % 100)), // 2 highest digits + }) + if err != nil { + return err + } + if written != 5 { + return fmt.Errorf("%s: %d bytes written, but %d expected", d.name, written, 5) + } + return d.run(ctrlRegVal) +} + +// ReadCounter reads the counter registers +func (d *PCF8583Driver) ReadCounter() (val int32, err error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + // according to chapter 7.1 of the product data sheet, the setting of "hold last count" flag + // is not needed when reading with auto increment + ctrlRegVal, err := d.connection.ReadByteData(uint8(pcf8583Reg_CTRL)) + if err != nil { + return + } + if !PCF8583Control(ctrlRegVal).isCounterMode() { + return val, fmt.Errorf("%s: can't read counter because the device is in wrong mode 0x%02x", d.name, ctrlRegVal) + } + // auto increment feature is used + counterDataSize := 3 + data := make([]byte, counterDataSize) + read, err := d.connection.Read(data) + if err != nil { + return + } + if read != counterDataSize { + return val, fmt.Errorf("%s: %d bytes read, but %d expected", d.name, read, counterDataSize) + } + return int32(pcf8583decodeBcd(data[0])) + + int32(pcf8583decodeBcd(data[1]))*100 + + int32(pcf8583decodeBcd(data[2]))*10000, nil +} + +// WriteRAM writes a value to a given address in memory (0x00-0xFF) +func (d *PCF8583Driver) WriteRAM(address uint8, val uint8) error { + d.mutex.Lock() + defer d.mutex.Unlock() + + realAddress := uint16(address) + uint16(d.ramOffset) + if realAddress > 0xFF { + return fmt.Errorf("%s: RAM address overflow %d", d.name, realAddress) + } + return d.connection.WriteByteData(uint8(realAddress), val) +} + +// ReadRAM reads a value from a given address (0x00-0xFF) +func (d *PCF8583Driver) ReadRAM(address uint8) (val uint8, err error) { + d.mutex.Lock() + defer d.mutex.Unlock() + + realAddress := uint16(address) + uint16(d.ramOffset) + if realAddress > 0xFF { + return val, fmt.Errorf("%s: RAM address overflow %d", d.name, realAddress) + } + return d.connection.ReadByteData(uint8(realAddress)) +} + +func (d *PCF8583Driver) run(ctrlRegVal uint8) error { + ctrlRegVal = ctrlRegVal & ^uint8(pcf8583CtrlStopCounting) // reset stop bit + return d.connection.WriteByteData(uint8(pcf8583Reg_CTRL), ctrlRegVal) +} + +func (d *PCF8583Driver) initialize() error { + // switch to configured mode + ctrlRegVal, err := d.connection.ReadByteData(uint8(pcf8583Reg_CTRL)) + if err != nil { + return err + } + if d.mode.isModeDiffer(PCF8583Control(ctrlRegVal)) { + ctrlRegVal = ctrlRegVal&^uint8(PCF8583CtrlModeTest) | uint8(d.mode) + if err = d.connection.WriteByteData(uint8(pcf8583Reg_CTRL), ctrlRegVal); err != nil { + return err + } + if pcf8583Debug { + if PCF8583Control(ctrlRegVal).isCounterMode() { + log.Printf("%s switched to counter mode 0x%02x", d.name, ctrlRegVal) + } else { + log.Printf("%s switched to clock mode 0x%02x", d.name, ctrlRegVal) + } + } + } + return nil +} + +func (c PCF8583Control) isClockMode() bool { + return uint8(c)&uint8(PCF8583CtrlModeCounter) == 0 +} + +func (c PCF8583Control) isCounterMode() bool { + counterModeSet := (uint8(c) & uint8(PCF8583CtrlModeCounter)) != 0 + clockMode50Set := (uint8(c) & uint8(PCF8583CtrlModeClock50)) != 0 + return counterModeSet && !clockMode50Set +} + +func (c PCF8583Control) isModeDiffer(mode PCF8583Control) bool { + return uint8(c)&uint8(PCF8583CtrlModeTest) != uint8(mode)&uint8(PCF8583CtrlModeTest) +} + +func pcf8583encodeBcd(val byte) byte { + // decimal 12 => 0x12 + if val > 99 { + val = 99 + if pcf8583Debug { + log.Printf("PCF8583 BCD value (%d) exceeds limit of 99, now limited.", val) + } + } + hi, lo := byte(val/10), byte(val%10) + return hi<<4 | lo +} + +func pcf8583decodeBcd(bcd byte) byte { + // 0x12 => decimal 12 + hi, lo := byte(bcd>>4), byte(bcd&0x0f) + if hi > 9 { + hi = 9 + if pcf8583Debug { + log.Printf("PCF8583 BCD value (%02x) exceeds limit 0x99 on most significant digit, now limited", bcd) + } + } + if lo > 9 { + lo = 9 + if pcf8583Debug { + log.Printf("PCF8583 BCD value (%02x) exceeds limit 0x99 on least significant digit, now limited", bcd) + } + } + return 10*hi + lo +} diff --git a/drivers/i2c/pcf8583_driver_test.go b/drivers/i2c/pcf8583_driver_test.go new file mode 100644 index 00000000..36aa09ef --- /dev/null +++ b/drivers/i2c/pcf8583_driver_test.go @@ -0,0 +1,612 @@ +package i2c + +import ( + "strings" + "testing" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/gobottest" +) + +// this ensures that the implementation is based on i2c.Driver, which implements the gobot.Driver +// and tests all implementations, so no further tests needed here for gobot.Driver interface +var _ gobot.Driver = (*PCF8583Driver)(nil) + +func initTestPCF8583WithStubbedAdaptor() (*PCF8583Driver, *i2cTestAdaptor) { + a := newI2cTestAdaptor() + d := NewPCF8583Driver(a) + d.Start() + return d, a +} + +func TestNewPCF8583Driver(t *testing.T) { + var di interface{} = NewPCF8583Driver(newI2cTestAdaptor()) + d, ok := di.(*PCF8583Driver) + if !ok { + t.Errorf("NewPCF8583Driver() should have returned a *PCF8583Driver") + } + gobottest.Refute(t, d.Driver, nil) + gobottest.Assert(t, strings.HasPrefix(d.name, "PCF8583"), true) + gobottest.Assert(t, d.mode, PCF8583Control(0x00)) + gobottest.Assert(t, d.yearOffset, 0) + gobottest.Assert(t, d.ramOffset, uint8(0x10)) +} + +func TestPCF8583Options(t *testing.T) { + // This is a general test, that options are applied in constructor by using the common WithBus() option and + // least one of this driver. Further tests for options can also be done by call of "WithOption(val)(d)". + d := NewPCF8583Driver(newI2cTestAdaptor(), WithBus(2), WithPCF8583Mode(PCF8583CtrlModeClock50)) + gobottest.Assert(t, d.GetBusOrDefault(1), 2) + gobottest.Assert(t, d.mode, PCF8583CtrlModeClock50) +} + +func TestPCF8583CommandsWriteTime(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + readCtrlState := uint8(0x10) // clock 50Hz + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + result := d.Command("WriteTime")(map[string]interface{}{"val": time.Now()}) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) +} + +func TestPCF8583CommandsReadTime(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + d.yearOffset = 2019 + milliSec := 550 * time.Millisecond // 0.55 sec = 550 ms + want := time.Date(2021, time.December, 24, 18, 00, 00, int(milliSec), time.UTC) + reg0Val := uint8(0x00) // clock mode 32.768 kHz + reg1Val := uint8(0x55) // BCD: 1/10 and 1/100 sec (55) + reg2Val := uint8(0x00) // BCD: 10 and 1 sec (00) + reg3Val := uint8(0x00) // BCD: 10 and 1 min (00) + reg4Val := uint8(0x18) // BCD: 10 and 1 hour (18) + reg5Val := uint8(0xA4) // year (2) and BCD: date (24) + reg6Val := uint8(0xB2) // weekday 5, bit 5 and bit 7 (0xA0) and BCD: month (0x12) + returnRead := [2][]uint8{ + {reg0Val}, + {reg1Val, reg2Val, reg3Val, reg4Val, reg5Val, reg6Val}, + } + // arrange reads + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + rr := returnRead[numCallsRead-1] + for i := 0; i < len(b); i++ { + b[i] = rr[i] + } + return len(b), nil + } + // act + result := d.Command("ReadTime")(map[string]interface{}{}) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) + gobottest.Assert(t, result.(map[string]interface{})["val"], want) +} + +func TestPCF8583CommandsWriteCounter(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + readCtrlState := uint8(0x20) // counter + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + result := d.Command("WriteCounter")(map[string]interface{}{"val": int32(123456)}) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) +} + +func TestPCF8583CommandsReadCounter(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + want := int32(123456) + reg0Val := uint8(0x20) // counter mode + reg1Val := uint8(0x56) // BCD: 56 + reg2Val := uint8(0x34) // BCD: 34 + reg3Val := uint8(0x12) // BCD: 12 + returnRead := [2][]uint8{ + {reg0Val}, + {reg1Val, reg2Val, reg3Val}, + } + // arrange reads + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + rr := returnRead[numCallsRead-1] + for i := 0; i < len(b); i++ { + b[i] = rr[i] + } + return len(b), nil + } + // act + result := d.Command("ReadCounter")(map[string]interface{}{}) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) + gobottest.Assert(t, result.(map[string]interface{})["val"], want) +} + +func TestPCF8583CommandsWriteRAM(t *testing.T) { + // arrange + d, _ := initTestPCF8583WithStubbedAdaptor() + var addressValue = map[string]interface{}{ + "address": uint8(0x12), + "val": uint8(0x45), + } + // act + result := d.Command("WriteRAM")(addressValue) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) +} + +func TestPCF8583CommandsReadRAM(t *testing.T) { + // arrange + d, _ := initTestPCF8583WithStubbedAdaptor() + var address = map[string]interface{}{ + "address": uint8(0x34), + } + // act + result := d.Command("ReadRAM")(address) + // assert + gobottest.Assert(t, result.(map[string]interface{})["err"], nil) + gobottest.Assert(t, result.(map[string]interface{})["val"], uint8(0)) +} + +func TestPCF8583WriteTime(t *testing.T) { + // sequence to write the time: + // * read control register for get current state and ensure an clock mode is set + // * write the control register (stop counting) + // * create the values for date registers (default is 24h mode) + // * write the clock and calendar registers with auto increment + // * write the control register (start counting) + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x07) // 32.768kHz clock mode + milliSec := 210 * time.Millisecond // 0.21 sec = 210 ms + initDate := time.Date(2022, time.December, 16, 15, 14, 13, int(milliSec), time.UTC) + wantCtrlStop := uint8(0x87) // stop counting bit is set + wantReg1Val := uint8(0x21) // BCD: 1/10 and 1/100 sec (21) + wantReg2Val := uint8(0x13) // BCD: 10 and 1 sec (13) + wantReg3Val := uint8(0x14) // BCD: 10 and 1 min (14) + wantReg4Val := uint8(0x15) // BCD: 10 and 1 hour (15) + wantReg5Val := uint8(0x16) // year (0) and BCD: date (16) + wantReg6Val := uint8(0xB2) // weekday 5, bit 5 and bit 7 (0xA0) and BCD: month (0x12) + wantCrtlStart := uint8(0x07) // stop counting bit is reset + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + err := d.WriteTime(initDate) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, d.yearOffset, initDate.Year()) + gobottest.Assert(t, numCallsRead, 1) + gobottest.Assert(t, len(a.written), 11) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[1], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[2], wantCtrlStop) + gobottest.Assert(t, a.written[3], wantReg1Val) + gobottest.Assert(t, a.written[4], wantReg2Val) + gobottest.Assert(t, a.written[5], wantReg3Val) + gobottest.Assert(t, a.written[6], wantReg4Val) + gobottest.Assert(t, a.written[7], wantReg5Val) + gobottest.Assert(t, a.written[8], wantReg6Val) + gobottest.Assert(t, a.written[9], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[10], wantCrtlStart) +} + +func TestPCF8583WriteTimeNoTimeModeFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x30) // test mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + err := d.WriteTime(time.Now()) + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "wrong mode 0x30"), true) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 1) +} + +func TestPCF8583ReadTime(t *testing.T) { + // sequence to read the time: + // * read the control register to determine mask flag and ensure an clock mode is set + // * read the clock and calendar registers with auto increment + // * create the value out of registers content + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + d.yearOffset = 2020 + milliSec := 210 * time.Millisecond // 0.21 sec = 210 ms + want := time.Date(2022, time.December, 16, 15, 14, 13, int(milliSec), time.UTC) + reg0Val := uint8(0x10) // clock mode 50Hz + reg1Val := uint8(0x21) // BCD: 1/10 and 1/100 sec (21) + reg2Val := uint8(0x13) // BCD: 10 and 1 sec (13) + reg3Val := uint8(0x14) // BCD: 10 and 1 min (14) + reg4Val := uint8(0x15) // BCD: 10 and 1 hour (15) + reg5Val := uint8(0x96) // year (2) and BCD: date (16) + reg6Val := uint8(0xB2) // weekday 5, bit 5 and bit 7 (0xA0) and BCD: month (0x12) + returnRead := [2][]uint8{ + {reg0Val}, + {reg1Val, reg2Val, reg3Val, reg4Val, reg5Val, reg6Val}, + } + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + rr := returnRead[numCallsRead-1] + for i := 0; i < len(b); i++ { + b[i] = rr[i] + } + return len(b), nil + } + // act + got, err := d.ReadTime() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) +} + +func TestPCF8583ReadTimeNoTimeModeFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x20) // counter mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + got, err := d.ReadTime() + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "wrong mode 0x20"), true) + gobottest.Assert(t, got, time.Time{}) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 1) +} + +func TestPCF8583WriteCounter(t *testing.T) { + // sequence to write the counter: + // * read control register for get current state and ensure the event counter mode is set + // * write the control register (stop counting) + // * create the values for counter registers + // * write the counter registers + // * write the control register (start counting) + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x27) // counter mode + initCount := int32(654321) // 6 digits used of 10 possible with int32 + wantCtrlStop := uint8(0xA7) // stop counting bit is set + wantReg1Val := uint8(0x21) // BCD: 21 + wantReg2Val := uint8(0x43) // BCD: 43 + wantReg3Val := uint8(0x65) // BCD: 65 + wantCtrlStart := uint8(0x27) // counter mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + err := d.WriteCounter(initCount) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, numCallsRead, 1) + gobottest.Assert(t, len(a.written), 8) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[1], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[2], wantCtrlStop) + gobottest.Assert(t, a.written[3], wantReg1Val) + gobottest.Assert(t, a.written[4], wantReg2Val) + gobottest.Assert(t, a.written[5], wantReg3Val) + gobottest.Assert(t, a.written[6], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[7], wantCtrlStart) +} + +func TestPCF8583WriteCounterNoCounterModeFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x10) // 50Hz mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + err := d.WriteCounter(123) + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "wrong mode 0x10"), true) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 1) +} + +func TestPCF8583ReadCounter(t *testing.T) { + // sequence to read the counter: + // * read the control register to ensure the event counter mode is set + // * read the counter registers + // * create the value out of registers content + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + want := int32(654321) + reg0Val := uint8(0x20) // counter mode + reg1Val := uint8(0x21) // BCD: 21 + reg2Val := uint8(0x43) // BCD: 43 + reg3Val := uint8(0x65) // BCD: 65 + returnRead := [2][]uint8{ + {reg0Val}, + {reg1Val, reg2Val, reg3Val}, + } + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + rr := returnRead[numCallsRead-1] + for i := 0; i < len(b); i++ { + b[i] = rr[i] + } + return len(b), nil + } + // act + got, err := d.ReadCounter() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 2) + gobottest.Assert(t, got, want) +} + +func TestPCF8583ReadCounterNoCounterModeFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + readCtrlState := uint8(0x30) // test mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act + got, err := d.ReadCounter() + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "wrong mode 0x30"), true) + gobottest.Assert(t, got, int32(0)) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, numCallsRead, 1) +} + +func TestPCF8583WriteRam(t *testing.T) { + // sequence to write the RAM: + // * calculate the RAM address and check for valid range + // * write the given value to the given RAM address + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + wantRamAddress := uint8(0xFF) + wantRamValue := uint8(0xEF) + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // act + err := d.WriteRAM(wantRamAddress-pcf8583RamOffset, wantRamValue) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, len(a.written), 2) + gobottest.Assert(t, a.written[0], wantRamAddress) + gobottest.Assert(t, a.written[1], wantRamValue) +} + +func TestPCF8583WriteRamAddressOverflowFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + // act + err := d.WriteRAM(uint8(0xF0), 15) + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "overflow 256"), true) + gobottest.Assert(t, len(a.written), 0) +} + +func TestPCF8583ReadRam(t *testing.T) { + // sequence to read the RAM: + // * calculate the RAM address and check for valid range + // * read the value from the given RAM address + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + wantRamAddress := uint8(pcf8583RamOffset) + want := uint8(0xAB) + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = want + return len(b), nil + } + // act + got, err := d.ReadRAM(wantRamAddress - pcf8583RamOffset) + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, got, want) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], wantRamAddress) + gobottest.Assert(t, numCallsRead, 1) +} + +func TestPCF8583ReadRamAddressOverflowFails(t *testing.T) { + // arrange + d, a := initTestPCF8583WithStubbedAdaptor() + a.written = []byte{} // reset writes of Start() and former test + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + return len(b), nil + } + // act + got, err := d.ReadRAM(uint8(0xF0)) + // assert + gobottest.Refute(t, err, nil) + gobottest.Assert(t, strings.Contains(err.Error(), "overflow 256"), true) + gobottest.Assert(t, got, uint8(0)) + gobottest.Assert(t, len(a.written), 0) + gobottest.Assert(t, numCallsRead, 0) +} + +func TestPCF8583_initializeNoModeSwitch(t *testing.T) { + // arrange + a := newI2cTestAdaptor() + d := NewPCF8583Driver(a) + a.written = []byte{} // reset writes of former tests + readCtrlState := uint8(0x01) // 32.768kHz clock mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act, assert - initialize() must be called on Start() + err := d.Start() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, numCallsRead, 1) + gobottest.Assert(t, len(a.written), 1) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) +} + +func TestPCF8583_initializeWithModeSwitch(t *testing.T) { + // sequence to change mode: + // * read control register for get current state + // * reset old mode bits and set new mode bit + // * write the control register + // arrange + a := newI2cTestAdaptor() + d := NewPCF8583Driver(a) + d.mode = PCF8583CtrlModeCounter + a.written = []byte{} // reset writes of former tests + readCtrlState := uint8(0x02) // 32.768kHz clock mode + wantReg0Val := uint8(0x22) // event counter mode + // arrange writes + a.i2cWriteImpl = func(b []byte) (int, error) { + return len(b), nil + } + // arrange reads + numCallsRead := 0 + a.i2cReadImpl = func(b []byte) (int, error) { + numCallsRead++ + b[len(b)-1] = readCtrlState + return len(b), nil + } + // act, assert - initialize() must be called on Start() + err := d.Start() + // assert + gobottest.Assert(t, err, nil) + gobottest.Assert(t, numCallsRead, 1) + gobottest.Assert(t, len(a.written), 3) + gobottest.Assert(t, a.written[0], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[1], uint8(pcf8583Reg_CTRL)) + gobottest.Assert(t, a.written[2], uint8(wantReg0Val)) +} diff --git a/examples/tinkerboard_pcf8583_clock.go b/examples/tinkerboard_pcf8583_clock.go new file mode 100644 index 00000000..897a42d9 --- /dev/null +++ b/examples/tinkerboard_pcf8583_clock.go @@ -0,0 +1,61 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/platforms/tinkerboard" +) + +// Wiring +// PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) +// I2C1 Tinkerboard: 3 (SDA), 5 (SCL) +// PCF8583 DIP package: 1 (OSCI, 50Hz), 2 (OSCO, nc), 3 (A0 - GND), 4 (VSS, +3.3V), 5 (SDA), 6 (SCL), 7 (/INT, nc), 8 (VDD, GND) +func main() { + board := tinkerboard.NewAdaptor() + pcf := i2c.NewPCF8583Driver(board, i2c.WithBus(1), i2c.WithPCF8583Mode(i2c.PCF8583CtrlModeClock50)) + + work := func() { + + currentTime := time.Now() + log.Println(currentTime) + + if err := pcf.WriteTime(currentTime); err != nil { + fmt.Println(err) + } + + gobot.Every(10*time.Second, func() { + if val, err := pcf.ReadTime(); err != nil { + fmt.Println(err) + } else { + log.Printf("read Time: %v", val) + } + + ramVal, err := pcf.ReadRAM(uint8(0)) + if err != nil { + fmt.Println(err) + } else { + log.Printf("read RAM: %v", ramVal) + ramVal++ + } + if err := pcf.WriteRAM(uint8(0), ramVal); err != nil { + fmt.Println(err) + } + }) + } + + robot := gobot.NewRobot("pcfBot", + []gobot.Connection{board}, + []gobot.Device{pcf}, + work, + ) + + robot.Start() +} diff --git a/examples/tinkerboard_pcf8583_counter.go b/examples/tinkerboard_pcf8583_counter.go new file mode 100644 index 00000000..2cb53319 --- /dev/null +++ b/examples/tinkerboard_pcf8583_counter.go @@ -0,0 +1,61 @@ +// +build example +// +// Do not build by default. + +package main + +import ( + "fmt" + "log" + "time" + + "gobot.io/x/gobot" + "gobot.io/x/gobot/drivers/i2c" + "gobot.io/x/gobot/platforms/tinkerboard" +) + +// Wiring +// PWR Tinkerboard: 1 (+3.3V, VCC), 6, 9, 14, 20 (GND) +// I2C1 Tinkerboard: 3 (SDA), 5 (SCL) +// PCF8583 DIP package: 1 (OSCI, event), 2 (OSCO, nc), 3 (A0 - GND), 4 (VSS, +3.3V), 5 (SDA), 6 (SCL), 7 (/INT, nc), 8 (VDD, GND) +// Note: event can be created by e.g. an debounced button +func main() { + board := tinkerboard.NewAdaptor() + pcf := i2c.NewPCF8583Driver(board, i2c.WithBus(1), i2c.WithPCF8583Mode(i2c.PCF8583CtrlModeCounter)) + + work := func() { + lastCnt := int32(1234) + + if err := pcf.WriteCounter(lastCnt); err != nil { + fmt.Println(err) + } + + gobot.Every(1000*time.Millisecond, func() { + if val, err := pcf.ReadCounter(); err != nil { + fmt.Println(err) + } else { + log.Printf("read Counter: %d, diff: %d", val, val-lastCnt) + lastCnt = val + } + + ramVal, err := pcf.ReadRAM(uint8(0)) + if err != nil { + fmt.Println(err) + } else { + log.Printf("read RAM: %d", ramVal) + ramVal++ + } + if err := pcf.WriteRAM(uint8(0), ramVal); err != nil { + fmt.Println(err) + } + }) + } + + robot := gobot.NewRobot("pcfBot", + []gobot.Connection{board}, + []gobot.Device{pcf}, + work, + ) + + robot.Start() +}