From cfa50fd3c2f410e86a0dc5a79b0cb021ebf1bcdd Mon Sep 17 00:00:00 2001 From: Thomas Richner Date: Mon, 18 Sep 2023 22:38:00 +0200 Subject: [PATCH] pcf8523: RTC driver --- examples/pcf8523/main.go | 34 +++++++++++++++ pcf8523/pcf8523.go | 89 +++++++++++++++++++++++++++++++++++++++ pcf8523/pcf8523_test.go | 91 ++++++++++++++++++++++++++++++++++++++++ pcf8523/registers.go | 39 +++++++++++++++++ smoketest.sh | 1 + 5 files changed, 254 insertions(+) create mode 100644 examples/pcf8523/main.go create mode 100644 pcf8523/pcf8523.go create mode 100644 pcf8523/pcf8523_test.go create mode 100644 pcf8523/registers.go diff --git a/examples/pcf8523/main.go b/examples/pcf8523/main.go new file mode 100644 index 0000000..2d416b0 --- /dev/null +++ b/examples/pcf8523/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "machine" + "time" + "tinygo.org/x/drivers/pcf8523" +) + +func main() { + machine.I2C0.Configure(machine.I2CConfig{}) + dev := pcf8523.New(machine.I2C0) + + // make sure the battery takes over if power is lost + err := dev.SetPowerManagement(pcf8523.PowerManagement_SwitchOver_ModeStandard) + if err != nil { + panic(err) + } + + // set RTC once, i.e. from `date -u +"%Y-%m-%dT%H:%M:%SZ"` + now, _ := time.Parse(time.RFC3339, "2023-09-18T20:31:38Z") + err = dev.SetTime(now) + if err != nil { + panic(err) + } + + for { + ts, err := dev.ReadTime() + if err != nil { + panic(err) + } + println("tick-tock, it's: " + ts.String()) + time.Sleep(2 * time.Second) + } +} diff --git a/pcf8523/pcf8523.go b/pcf8523/pcf8523.go new file mode 100644 index 0000000..333c6e4 --- /dev/null +++ b/pcf8523/pcf8523.go @@ -0,0 +1,89 @@ +// Package pcf8523 implements a driver for the PCF8523 CMOS Real-Time Clock (RTC) +// +// Datasheet: https://www.nxp.com/docs/en/data-sheet/PCF8523.pdf +package pcf8523 + +import ( + "time" + "tinygo.org/x/drivers" +) + +type Device struct { + bus drivers.I2C + Address uint8 +} + +func New(i2c drivers.I2C) Device { + return Device{ + bus: i2c, + Address: DefaultAddress, + } +} + +// Reset resets the device according to the datasheet section 8.3 +// This does not wipe the time registers, but resets control registers. +func (d *Device) Reset() (err error) { + return d.bus.Tx(uint16(d.Address), []byte{rControl1, 0x58}, nil) +} + +// SetPowerManagement configures how the device makes use of the backup battery, see +// datasheet section 8.5 +func (d *Device) SetPowerManagement(b PowerManagement) error { + return d.setRegister(rControl3, byte(b)<<5, 0xE0) +} + +func (d *Device) setRegister(reg uint8, value, mask uint8) error { + var buf [1]byte + err := d.bus.Tx(uint16(d.Address), []byte{reg}, buf[:]) + if err != nil { + return err + } + buf[0] = (value & mask) | (buf[0] & (^mask)) + return d.bus.Tx(uint16(d.Address), []byte{reg, buf[0]}, nil) +} + +// SetTime sets the time and date +func (d *Device) SetTime(t time.Time) error { + buf := []byte{ + rSeconds, + bin2bcd(t.Second()), + bin2bcd(t.Minute()), + bin2bcd(t.Hour()), + bin2bcd(t.Day()), + bin2bcd(int(t.Weekday())), + bin2bcd(int(t.Month())), + bin2bcd(t.Year() - 2000), + } + + return d.bus.Tx(uint16(d.Address), buf, nil) +} + +// ReadTime returns the date and time +func (d *Device) ReadTime() (time.Time, error) { + buf := make([]byte, 9) + err := d.bus.Tx(uint16(d.Address), []byte{rSeconds}, buf) + if err != nil { + return time.Time{}, err + } + + seconds := bcd2bin(buf[0] & 0x7F) + minute := bcd2bin(buf[1] & 0x7F) + hour := bcd2bin(buf[2] & 0x3F) + day := bcd2bin(buf[3] & 0x3F) + //skipping weekday buf[4] + month := time.Month(bcd2bin(buf[5] & 0x1F)) + year := int(bcd2bin(buf[6])) + 2000 + + t := time.Date(year, month, day, hour, minute, seconds, 0, time.UTC) + return t, nil +} + +// bin2bcd converts binary to BCD +func bin2bcd(dec int) uint8 { + return uint8(dec + 6*(dec/10)) +} + +// bcd2bin converts BCD to binary +func bcd2bin(bcd uint8) int { + return int(bcd - 6*(bcd>>4)) +} diff --git a/pcf8523/pcf8523_test.go b/pcf8523/pcf8523_test.go new file mode 100644 index 0000000..ef103c4 --- /dev/null +++ b/pcf8523/pcf8523_test.go @@ -0,0 +1,91 @@ +package pcf8523 + +import ( + "encoding/hex" + "testing" + "time" + "tinygo.org/x/drivers/tester" +) + +func TestDecToBcd_RoundTrip(t *testing.T) { + + for i := 0; i < 60; i++ { + a := bcd2bin(bin2bcd(i)) + if a != i { + t.Logf("not equal: %d != %d", a, i) + t.FailNow() + } + } +} + +func TestDevice_Reset(t *testing.T) { + bus := tester.NewI2CBus(t) + fake := bus.NewDevice(DefaultAddress) + + dev := New(bus) + + err := dev.Reset() + assertNoError(t, err) + + assertEquals(t, fake.Registers[rControl1], 0x58) +} + +func TestDevice_SetPowerManagement(t *testing.T) { + bus := tester.NewI2CBus(t) + fake := bus.NewDevice(DefaultAddress) + + dev := New(bus) + + err := dev.SetPowerManagement(PowerManagement_SwitchOver_ModeStandard) + assertNoError(t, err) + + assertEquals(t, fake.Registers[rControl3], 0b100<<5) +} + +func TestDevice_SetTime(t *testing.T) { + bus := tester.NewI2CBus(t) + fake := bus.NewDevice(DefaultAddress) + + dev := New(bus) + + pointInTime, _ := time.Parse(time.RFC3339, "2023-09-12T22:35:50Z") + err := dev.SetTime(pointInTime) + assertNoError(t, err) + + actual := hex.EncodeToString(fake.Registers[rSeconds : rSeconds+7]) + expected := "50352212020923" + assertEquals(t, actual, expected) +} + +func TestDevice_ReadTime(t *testing.T) { + bus := tester.NewI2CBus(t) + fake := bus.NewDevice(DefaultAddress) + + expectedPointInTime := time.Date(2023, 9, 12, 17, 55, 42, 0, time.UTC) + fake.Registers[rSeconds] = 0x42 + fake.Registers[rMinutes] = 0x55 + fake.Registers[rHours] = 0x17 + fake.Registers[rDays] = 0x12 + fake.Registers[rMonths] = 0x9 + fake.Registers[rYears] = 0x23 + + dev := New(bus) + + //when + actualPointInTime, err := dev.ReadTime() + + //then + assertNoError(t, err) + assertEquals(t, actualPointInTime, expectedPointInTime) +} + +func assertNoError(t testing.TB, e error) { + if e != nil { + t.Fatalf("unexpected error: %v", e) + } +} +func assertEquals[T comparable](t testing.TB, a, b T) { + if a != b { + t.Fatalf("%v != %v", a, b) + } +} diff --git a/pcf8523/registers.go b/pcf8523/registers.go new file mode 100644 index 0000000..c77f496 --- /dev/null +++ b/pcf8523/registers.go @@ -0,0 +1,39 @@ +package pcf8523 + +const DefaultAddress = 0x68 + +// datasheet 8.5 Power management functions, table 11 +type PowerManagement byte + +const ( + PowerManagement_SwitchOver_ModeStandard_LowDetection PowerManagement = 0b000 + PowerManagement_SwitchOver_ModeDirect_LowDetection PowerManagement = 0b001 + PowerManagement_VddOnly_LowDetection PowerManagement = 0b010 + PowerManagement_SwitchOver_ModeStandard PowerManagement = 0b100 + PowerManagement_SwitchOver_ModeDirect PowerManagement = 0b101 + PowerManagement_VddOnly PowerManagement = 0b101 +) + +// constants for all internal registers +const ( + rControl1 = 0x00 // Control_1 + rControl2 = 0x01 // Control_2 + rControl3 = 0x02 // Control_3 + rSeconds = 0x03 // Seconds + rMinutes = 0x04 // Minutes + rHours = 0x05 // Hours + rDays = 0x06 // Days + rWeekdays = 0x07 // Weekdays + rMonths = 0x08 // Months + rYears = 0x09 // Years + rMinuteAlarm = 0x0A // Minute_alarm + rHourAlarm = 0x0B // Hour_alarm + rDayAlarm = 0x0C // Day_alarm + rWeekdayAlarm = 0x0D // Weekday_alarm + rOffset = 0x0E // Offset + rTimerClkoutControl = 0x0F // Tmr_CLKOUT_ctrl + rTimerAFrequencyControl = 0x10 // Tmr_A_freq_ctrl + rTimerARegister = 0x11 // Tmr_A_reg + rTimerBFrequencyControl = 0x12 // Tmr_B_freq_ctrl + rTimerBRegister = 0x13 // Tmr_B_reg +) diff --git a/smoketest.sh b/smoketest.sh index deaafe2..e0a9203 100755 --- a/smoketest.sh +++ b/smoketest.sh @@ -103,6 +103,7 @@ tinygo build -size short -o ./build/test.hex -target=circuitplay-express ./examp tinygo build -size short -o ./build/test.hex -target=arduino-nano33 ./examples/max72xx/main.go tinygo build -size short -o ./build/test.hex -target=feather-m0 ./examples/dht/main.go # tinygo build -size short -o ./build/test.hex -target=arduino ./examples/keypad4x4/main.go +tinygo build -size short -o ./build/test.hex -target=feather-rp2040 ./examples/pcf8523/ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/alarm/ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/clkout/ tinygo build -size short -o ./build/test.hex -target=xiao ./examples/pcf8563/time/