devices: add code to access MCP7940N RTC

Signed-off-by: Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
2 files changed, 289 insertions(+), 0 deletions(-)

A => devices/i2c/mcp7940n.c
M devices/i2c/mcp7940n.h
A => devices/i2c/mcp7940n.c +280 -0
@@ 0,0 1,280 @@ 
+/*
+ * Copyright (c) 2024 Josef 'Jeff' Sipek <jeffpc@josefsipek.net>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include "../../arch.h"
+#include "mcp7940n.h"
+
+static inline uint8_t div10(uint8_t v)
+{
+	uint8_t out;
+
+	for (out = 0; v >= 10; out++)
+		v -= 10;
+
+	return out;
+}
+
+static inline uint8_t mod10(uint8_t v)
+{
+	return v - div10(v) * 10;
+}
+
+#define BCD2BIN(val, umask, ushift, lmask, lshift) \
+	((((val) & umask) >> ushift) * 10 + \
+	 (((val) & lmask) >> lshift))
+#define BIN2BCD(val, ushift, lshift) \
+	((div10(val) << ushift) | \
+	 (mod10(val) << lshift))
+
+/*
+ * Returns:
+ *  -1 = error
+ *   0 = success, started RTC
+ *  +1 = success, RTC already running
+ */
+int mcp7940n_init(const struct tm *tm)
+{
+	struct mcp7940n_timekeeping_regs tmp;
+
+	if (!mcp7940n_read(offsetof(struct mcp7940n, timekeeping), &tmp,
+			   sizeof(tmp)))
+		return -1;
+
+	if (tmp.sec & MCP7940N_SEC_ST)
+		return +1; /* already started, don't disturb the time */
+
+	if (!mcp7940n_settime(tm))
+		return -1;
+
+	return 0;
+}
+
+/*
+ * write a number of bytes to the specified RTC register address
+ *
+ * Example I2C traffic:
+ *
+ * [ 0xde 0x00 0x80 0x41 0x00 0x02 0x25 0x12 ... ]
+ * ^ ^    ^    ^    ^    ^    ^    ^    ^        ^
+ * | |    |    |    |    |    |    |    |        `- stop condition
+ * | |    |    `----`----`----`----`----`- data written
+ * | |    `- register address
+ * | `- device address
+ * `- start condition
+ */
+bool mcp7940n_write(uint8_t addr, const void *_buf, size_t len)
+{
+	const uint8_t *buf = _buf;
+	size_t i;
+
+	i2c_start();
+	if (!i2c_sync(I2C_STATUS_START))
+		goto err;
+
+	i2c_tx_byte(MCP7940N_ADDR_W);
+	if (!i2c_sync(I2C_STATUS_MT_SLA_ACK))
+		goto err;
+
+	i2c_tx_byte(addr);
+	if (!i2c_sync(I2C_STATUS_MT_DATA_ACK))
+		goto err;
+
+	for (i = 0; i < len; i++) {
+		i2c_tx_byte(buf[i]);
+		if (!i2c_sync(I2C_STATUS_MT_DATA_ACK))
+			goto err;
+	}
+
+	i2c_stop();
+
+	return true;
+
+err:
+	i2c_stop();
+	i2c_stop();
+
+	return false;
+}
+
+/*
+ * read a number of bytes from the specified RTC register address
+ *
+ * Example I2C traffic:
+ *
+ * [ 0xde 0x00 [ 0xdf 0xAA 0xBB 0xCC ... ]
+ * ^ ^    ^    ^ ^    ^    ^    ^        ^
+ * | |    |    | |    |    |    |        `- stop condition
+ * | |    |    | |    `----`----`- read data
+ * | |    |    | `- device address
+ * | |    |    `- repeated start condition
+ * | |    `- register address
+ * | `- device address
+ * `- start condition
+ */
+bool mcp7940n_read(uint8_t addr, void *_buf, size_t len)
+{
+	uint8_t *buf = _buf;
+	size_t i;
+
+	i2c_start();
+	if (!i2c_sync(I2C_STATUS_START))
+		goto err;
+
+	i2c_tx_byte(MCP7940N_ADDR_W);
+	if (!i2c_sync(I2C_STATUS_MT_SLA_ACK))
+		goto err;
+
+	i2c_tx_byte(addr);
+	if (!i2c_sync(I2C_STATUS_MT_DATA_ACK))
+		goto err;
+
+	i2c_start();
+	if (!i2c_sync(I2C_STATUS_REP_START))
+		goto err;
+
+	i2c_tx_byte(MCP7940N_ADDR_R);
+	if (!i2c_sync(I2C_STATUS_MR_SLA_ACK))
+		goto err;
+
+	for (i = 0; i < len; i++)
+		buf[i] = i2c_rx_byte((i + 1) == len, NULL);
+
+	i2c_stop();
+
+	return true;
+
+err:
+	i2c_stop();
+	i2c_stop();
+
+	return false;
+}
+
+bool mcp7940n_gettime(struct tm *tm)
+{
+	struct mcp7940n_timekeeping_regs tmp;
+
+	if (!mcp7940n_read(offsetof(struct mcp7940n, timekeeping), &tmp,
+			   sizeof(tmp)))
+		return false;
+
+	if (!(tmp.sec & MCP7940N_SEC_ST) ||
+	    !(tmp.wkday & MCP7940N_WKDAY_OSCRUN))
+		return false;
+
+	tm->sec = BCD2BIN(tmp.sec, 0x70, 4, 0x0f, 0);
+	tm->min = BCD2BIN(tmp.min, 0x70, 4, 0x0f, 0);
+	tm->hour = BCD2BIN(tmp.hour, 0x10, 4, 0x0f, 0);
+	if ((tmp.hour & MCP7940N_HOUR_FMT) == MCP7940N_HOUR_FMT_12H)
+		tm->hour += (tmp.hour & 0x20) ? 12 /* pm */ : 0 /* am */;
+	else
+		tm->hour += (tmp.hour & 0x20) ? 20 : 0;
+	tm->wkday = BCD2BIN(tmp.wkday, 0, 0, 0x07, 0);
+	tm->day = BCD2BIN(tmp.date, 0x30, 4, 0x0f, 0);
+	tm->month = BCD2BIN(tmp.month, 0x10, 4, 0x0f, 0);
+	tm->year = 2000 + BCD2BIN(tmp.year, 0xf0, 4, 0x0f, 0);
+
+	return true;
+}
+
+bool mcp7940n_settime(const struct tm *tm)
+{
+	struct mcp7940n_timekeeping_regs tmp = { };
+
+	/* stop the oscillator */
+	tmp.sec = 0;
+	if (!mcp7940n_write(offsetof(struct mcp7940n, timekeeping.sec),
+			    &tmp.sec, sizeof(tmp.sec)))
+		return false;
+
+	tmp.sec = BIN2BCD(tm->sec, 4, 0) & 0x7f;
+	tmp.min = BIN2BCD(tm->min, 4, 0) & 0x7f;
+	tmp.hour = MCP7940N_HOUR_FMT_24H | (BIN2BCD(tm->hour, 4, 0) & 0x3f);
+	tmp.wkday = MCP7940N_WKDAY_VBATEN | (BIN2BCD(0 /* FIXME */, 8, 0) & 0x7);
+	tmp.date = BIN2BCD(tm->day, 4, 0) & 0x3f;
+	tmp.month = BIN2BCD(tm->month, 4, 0) & 0x1f;
+	tmp.year = BIN2BCD(tm->year - 2000, 4, 0);
+
+	if (!mcp7940n_write(offsetof(struct mcp7940n, timekeeping), &tmp,
+			    sizeof(tmp)))
+		return false;
+
+	/* start the oscillator */
+	tmp.sec |= MCP7940N_SEC_ST;
+	if (!mcp7940n_write(offsetof(struct mcp7940n, timekeeping.sec),
+			    &tmp.sec, sizeof(tmp.sec)))
+		return false;
+
+	/* TODO: check OSCRUN in RTCWKDAY */
+
+	return true;
+}
+
+bool mcp7940n_gettrim(int8_t *v)
+{
+	uint8_t tmp;
+
+	if (!mcp7940n_read(offsetof(struct mcp7940n, timekeeping.trim), &tmp,
+			   sizeof(tmp)))
+		return false;
+
+	if (tmp & 0x80)
+		*v = -((int8_t) (tmp & 0x7f));
+	else
+		*v = tmp;
+
+	return true;
+}
+
+bool mcp7940n_settrim(int8_t v)
+{
+	uint8_t tmp;
+
+	if (v < 0)
+		tmp = 0x80 | ((uint8_t) -v);
+	else
+		tmp = v;
+
+	return mcp7940n_write(offsetof(struct mcp7940n, timekeeping.trim),
+			      &tmp, sizeof(tmp));
+}
+
+bool mcp7940n_adjtrim(int8_t delta)
+{
+	int16_t tmp;
+	int8_t v;
+
+	if (!mcp7940n_gettrim(&v))
+		return false;
+
+	tmp = v + delta;
+
+	/* clamp the new trim value */
+	if (tmp > 127)
+		v = 127;
+	else if (tmp < -127)
+		v = - -127;
+	else
+		v = tmp;
+
+	return mcp7940n_settrim(v);
+}

          
M devices/i2c/mcp7940n.h +9 -0
@@ 90,6 90,15 @@ struct mcp7940n {
 };
 STATIC_ASSERT(sizeof(struct mcp7940n) == 32);
 
+extern int mcp7940n_init(const struct tm *tm);
+extern bool mcp7940n_write(uint8_t addr, const void *_buf, size_t len);
+extern bool mcp7940n_read(uint8_t addr, void *_buf, size_t len);
+extern bool mcp7940n_gettime(struct tm *tm);
+extern bool mcp7940n_settime(const struct tm *tm);
+extern bool mcp7940n_gettrim(int8_t *v);
+extern bool mcp7940n_settrim(int8_t v);
+extern bool mcp7940n_adjtrim(int8_t delta);
+
 #endif
 
 #endif