freebsd-dev/sys/dev/iicbus/ds1307.c
Mathew McBride 2486b446db ds1307: add support for the EPSON RX-8035SA I2C RTC
The EPSON RX-8035SA I2C RTC has a similar time register layout to the
ds1307 family, with some minor differences in bit positions, polarity
and control registers.  Further generalize ds1307 to make it easier
to add more compatible chips and add support for the EPSON RX-8035SA.

MFC after:	6 days
Sponsored by:	Traverse Technologies
Differential Revision: https://reviews.freebsd.org/D35837
2022-09-02 00:51:42 +00:00

586 lines
14 KiB
C

/*-
* Copyright (c) 2015 Luiz Otavio O Souza <loos@FreeBSD.org>
* Copyright (c) 2022 Mathew McBride <matt@traverse.com.au>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
/*
* Driver for Maxim DS1307 I2C real-time clock/calendar.
*/
#include "opt_platform.h"
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/clock.h>
#include <sys/kernel.h>
#include <sys/module.h>
#include <sys/sysctl.h>
#include <dev/iicbus/iicbus.h>
#include <dev/iicbus/iiconf.h>
#ifdef FDT
#include <dev/ofw/openfirm.h>
#include <dev/ofw/ofw_bus.h>
#include <dev/ofw/ofw_bus_subr.h>
#endif
#include <dev/iicbus/ds1307reg.h>
#include "clock_if.h"
#include "iicbus_if.h"
enum {
TYPE_DS1307,
TYPE_MAXIM1307,
TYPE_MICROCHIP_MCP7491X,
TYPE_EPSON_RX8035,
TYPE_COUNT
};
struct ds1307_softc {
device_t sc_dev;
struct intr_config_hook enum_hook;
uint32_t chiptype;
uint8_t sc_ctrl;
bool sc_use_ampm;
};
static void ds1307_start(void *);
#ifdef FDT
static const struct ofw_compat_data ds1307_compat_data[] = {
{"dallas,ds1307", TYPE_DS1307},
{"maxim,ds1307", TYPE_MAXIM1307},
{"microchip,mcp7941x", TYPE_MICROCHIP_MCP7491X},
{"epson,rx8035", TYPE_EPSON_RX8035},
{ NULL, 0 }
};
#endif
static int
ds1307_read1(device_t dev, uint8_t reg, uint8_t *data)
{
return (iicdev_readfrom(dev, reg, data, 1, IIC_INTRWAIT));
}
static int
ds1307_write1(device_t dev, uint8_t reg, uint8_t data)
{
return (iicdev_writeto(dev, reg, &data, 1, IIC_INTRWAIT));
}
static int
ds1307_ctrl_read(struct ds1307_softc *sc)
{
int error;
sc->sc_ctrl = 0;
error = ds1307_read1(sc->sc_dev, DS1307_CONTROL, &sc->sc_ctrl);
if (error) {
device_printf(sc->sc_dev, "%s: cannot read from RTC: %d\n",
__func__, error);
return (error);
}
return (0);
}
static int
ds1307_ctrl_write(struct ds1307_softc *sc)
{
int error;
uint8_t ctrl;
ctrl = sc->sc_ctrl & DS1307_CTRL_MASK;
error = ds1307_write1(sc->sc_dev, DS1307_CONTROL, ctrl);
if (error != 0)
device_printf(sc->sc_dev, "%s: cannot write to RTC: %d\n",
__func__, error);
return (error);
}
static int
ds1307_sqwe_sysctl(SYSCTL_HANDLER_ARGS)
{
int sqwe, error, newv, sqwe_bit;
struct ds1307_softc *sc;
sc = (struct ds1307_softc *)arg1;
error = ds1307_ctrl_read(sc);
if (error != 0)
return (error);
if (sc->chiptype == TYPE_MICROCHIP_MCP7491X)
sqwe_bit = MCP7941X_CTRL_SQWE;
else
sqwe_bit = DS1307_CTRL_SQWE;
sqwe = newv = (sc->sc_ctrl & sqwe_bit) ? 1 : 0;
error = sysctl_handle_int(oidp, &newv, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
if (sqwe != newv) {
sc->sc_ctrl &= ~sqwe_bit;
if (newv)
sc->sc_ctrl |= sqwe_bit;
error = ds1307_ctrl_write(sc);
if (error != 0)
return (error);
}
return (error);
}
static int
ds1307_sqw_freq_sysctl(SYSCTL_HANDLER_ARGS)
{
int ds1307_sqw_freq[] = { 1, 4096, 8192, 32768 };
int error, freq, i, newf, tmp;
struct ds1307_softc *sc;
sc = (struct ds1307_softc *)arg1;
error = ds1307_ctrl_read(sc);
if (error != 0)
return (error);
tmp = (sc->sc_ctrl & DS1307_CTRL_RS_MASK);
if (tmp >= nitems(ds1307_sqw_freq))
tmp = nitems(ds1307_sqw_freq) - 1;
freq = ds1307_sqw_freq[tmp];
error = sysctl_handle_int(oidp, &freq, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
if (freq != ds1307_sqw_freq[tmp]) {
newf = 0;
for (i = 0; i < nitems(ds1307_sqw_freq); i++)
if (freq >= ds1307_sqw_freq[i])
newf = i;
sc->sc_ctrl &= ~DS1307_CTRL_RS_MASK;
sc->sc_ctrl |= newf;
error = ds1307_ctrl_write(sc);
if (error != 0)
return (error);
}
return (error);
}
static int
ds1307_sqw_out_sysctl(SYSCTL_HANDLER_ARGS)
{
int sqwe, error, newv;
struct ds1307_softc *sc;
sc = (struct ds1307_softc *)arg1;
error = ds1307_ctrl_read(sc);
if (error != 0)
return (error);
sqwe = newv = (sc->sc_ctrl & DS1307_CTRL_OUT) ? 1 : 0;
error = sysctl_handle_int(oidp, &newv, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
if (sqwe != newv) {
sc->sc_ctrl &= ~DS1307_CTRL_OUT;
if (newv)
sc->sc_ctrl |= DS1307_CTRL_OUT;
error = ds1307_ctrl_write(sc);
if (error != 0)
return (error);
}
return (error);
}
static int
ds1307_probe(device_t dev)
{
#ifdef FDT
const struct ofw_compat_data *compat;
if (!ofw_bus_status_okay(dev))
return (ENXIO);
compat = ofw_bus_search_compatible(dev, ds1307_compat_data);
if (compat->ocd_str == NULL)
return (ENXIO);
switch(compat->ocd_data) {
case TYPE_DS1307:
device_set_desc(dev, "Dallas DS1307");
break;
case TYPE_MAXIM1307:
device_set_desc(dev, "Maxim DS1307");
break;
case TYPE_MICROCHIP_MCP7491X:
device_set_desc(dev, "Microchip MCP7491X");
break;
case TYPE_EPSON_RX8035:
device_set_desc(dev, "Epson RX-8035");
break;
default:
device_set_desc(dev, "Unknown DS1307-like device");
break;
}
return (BUS_PROBE_DEFAULT);
#endif
device_set_desc(dev, "Maxim DS1307 RTC");
return (BUS_PROBE_NOWILDCARD);
}
static int
ds1307_attach(device_t dev)
{
#ifdef FDT
const struct ofw_compat_data *compat;
#endif
struct ds1307_softc *sc;
sc = device_get_softc(dev);
sc->sc_dev = dev;
sc->enum_hook.ich_func = ds1307_start;
sc->enum_hook.ich_arg = dev;
#ifdef FDT
compat = ofw_bus_search_compatible(dev, ds1307_compat_data);
sc->chiptype = compat->ocd_data;
/* Unify the chiptypes to DS1307 where possible. */
if (sc->chiptype == TYPE_MAXIM1307)
sc->chiptype = TYPE_DS1307;
#else
sc->chiptype = TYPE_DS1307;
#endif
/*
* We have to wait until interrupts are enabled. Usually I2C read
* and write only works when the interrupts are available.
*/
if (config_intrhook_establish(&sc->enum_hook) != 0)
return (ENOMEM);
return (0);
}
static int
ds1307_detach(device_t dev)
{
clock_unregister(dev);
return (0);
}
static bool
is_epson_time_valid(struct ds1307_softc *sc)
{
device_t dev;
int error;
uint8_t ctrl2;
dev = sc->sc_dev;
/*
* The RX-8035 single register read is non-standard
* Refer to section 8.9.5 of the RX-8035 application manual:
* "I2C bus basic transfer format", under "Standard Read Method".
* Basically, register to read goes into the top 4 bits.
*/
error = ds1307_read1(dev, (RX8035_CTRL_2 << 4), &ctrl2);
if (error) {
device_printf(dev, "%s cannot read Control 2 register: %d\n",
__func__, error);
return (false);
}
if (ctrl2 & RX8035_CTRL_2_XSTP) {
device_printf(dev, "Oscillation stop detected (ctrl2=%#02x)\n",
ctrl2);
return (false);
}
/*
* Power on reset (PON) generally implies oscillation stop,
* but catch it as well to be sure.
*/
if (ctrl2 & RX8035_CTRL_2_PON) {
device_printf(dev, "Power-on reset detected (ctrl2=%#02x)\n",
ctrl2);
return (false);
}
return (true);
}
static int
mark_epson_time_valid(struct ds1307_softc *sc)
{
device_t dev;
int error;
uint8_t ctrl2;
uint8_t control_mask;
dev = sc->sc_dev;
error = ds1307_read1(dev, (RX8035_CTRL_2 << 4), &ctrl2);
if (error) {
device_printf(dev, "%s cannot read Control 2 register: %d\n",
__func__, error);
return (false);
}
control_mask = (RX8035_CTRL_2_PON | RX8035_CTRL_2_XSTP | RX8035_CTRL_2_VDET);
ctrl2 = ctrl2 & ~(control_mask);
error = ds1307_write1(dev, (RX8035_CTRL_2 << 4), ctrl2);
if (error) {
device_printf(dev, "%s cannot write to Control 2 register: %d\n",
__func__, error);
return (false);
}
return (true);
}
static bool is_dev_time_valid(struct ds1307_softc *sc)
{
device_t dev;
int error;
uint8_t osc_en;
uint8_t secs;
/* Epson RTCs have different control/status registers. */
if (sc->chiptype == TYPE_EPSON_RX8035)
return (is_epson_time_valid(sc));
dev = sc->sc_dev;
/* Check if the oscillator is disabled. */
error = ds1307_read1(dev, DS1307_SECS, &secs);
if (error) {
device_printf(dev, "%s: cannot read from RTC: %d\n",
__func__, error);
return (false);
}
switch (sc->chiptype) {
case TYPE_MICROCHIP_MCP7491X:
osc_en = 0x80;
break;
default:
osc_en = 0x00;
break;
}
if (((secs & DS1307_SECS_CH) ^ osc_en) != 0)
return (false);
return (true);
}
static void
ds1307_start(void *xdev)
{
device_t dev;
struct ds1307_softc *sc;
struct sysctl_ctx_list *ctx;
struct sysctl_oid *tree_node;
struct sysctl_oid_list *tree;
dev = (device_t)xdev;
sc = device_get_softc(dev);
config_intrhook_disestablish(&sc->enum_hook);
if (!is_dev_time_valid(sc))
device_printf(dev,
"WARNING: RTC clock stopped, check the battery.\n");
/*
* Configuration parameters:
* square wave output cannot be changed or inhibited on the RX-8035,
* so don't present the sysctls there.
*/
if (sc->chiptype == TYPE_EPSON_RX8035)
goto skip_sysctl;
ctx = device_get_sysctl_ctx(dev);
tree_node = device_get_sysctl_tree(dev);
tree = SYSCTL_CHILDREN(tree_node);
SYSCTL_ADD_PROC(ctx, tree, OID_AUTO, "sqwe",
CTLFLAG_RW | CTLTYPE_UINT | CTLFLAG_MPSAFE, sc, 0,
ds1307_sqwe_sysctl, "IU", "DS1307 square-wave enable");
SYSCTL_ADD_PROC(ctx, tree, OID_AUTO, "sqw_freq",
CTLFLAG_RW | CTLTYPE_UINT | CTLFLAG_MPSAFE, sc, 0,
ds1307_sqw_freq_sysctl, "IU",
"DS1307 square-wave output frequency");
SYSCTL_ADD_PROC(ctx, tree, OID_AUTO, "sqw_out",
CTLFLAG_RW | CTLTYPE_UINT | CTLFLAG_MPSAFE, sc, 0,
ds1307_sqw_out_sysctl, "IU", "DS1307 square-wave output state");
skip_sysctl:
/*
* Register as a clock with 1 second resolution. Schedule the
* clock_settime() method to be called just after top-of-second;
* resetting the time resets top-of-second in the hardware.
*/
clock_register_flags(dev, 1000000, CLOCKF_SETTIME_NO_ADJ);
clock_schedule(dev, 1);
}
static int
ds1307_gettime(device_t dev, struct timespec *ts)
{
struct bcd_clocktime bct;
struct ds1307_softc *sc;
int error;
uint8_t data[7], hourmask, ampm_mode;
sc = device_get_softc(dev);
error = iicdev_readfrom(sc->sc_dev, DS1307_SECS, data, sizeof(data),
IIC_INTRWAIT);
if (error != 0) {
device_printf(dev, "%s: cannot read from RTC: %d\n",
__func__, error);
return (error);
}
if (!is_dev_time_valid(sc)) {
device_printf(dev, "Device time not valid.\n");
return (EINVAL);
}
/*
* If the chip is in AM/PM mode remember that.
* The EPSON uses a 1 to signify 24 hour mode, while the DS uses a 0,
* in slighly different positions.
*/
if (sc->chiptype == TYPE_EPSON_RX8035)
ampm_mode = !(data[DS1307_HOUR] & RX8035_HOUR_USE_24);
else
ampm_mode = data[DS1307_HOUR] & DS1307_HOUR_USE_AMPM;
if (ampm_mode) {
sc->sc_use_ampm = true;
hourmask = DS1307_HOUR_MASK_12HR;
} else
hourmask = DS1307_HOUR_MASK_24HR;
bct.nsec = 0;
bct.ispm = (data[DS1307_HOUR] & DS1307_HOUR_IS_PM) != 0;
bct.sec = data[DS1307_SECS] & DS1307_SECS_MASK;
bct.min = data[DS1307_MINS] & DS1307_MINS_MASK;
bct.hour = data[DS1307_HOUR] & hourmask;
bct.day = data[DS1307_DATE] & DS1307_DATE_MASK;
bct.mon = data[DS1307_MONTH] & DS1307_MONTH_MASK;
bct.year = data[DS1307_YEAR] & DS1307_YEAR_MASK;
clock_dbgprint_bcd(sc->sc_dev, CLOCK_DBG_READ, &bct);
return (clock_bcd_to_ts(&bct, ts, sc->sc_use_ampm));
}
static int
ds1307_settime(device_t dev, struct timespec *ts)
{
struct bcd_clocktime bct;
struct ds1307_softc *sc;
int error, year;
uint8_t data[7];
uint8_t pmflags;
sc = device_get_softc(dev);
/*
* We request a timespec with no resolution-adjustment. That also
* disables utc adjustment, so apply that ourselves.
*/
ts->tv_sec -= utc_offset();
clock_ts_to_bcd(ts, &bct, sc->sc_use_ampm);
clock_dbgprint_bcd(sc->sc_dev, CLOCK_DBG_WRITE, &bct);
/*
* If the chip is in AM/PM mode, adjust hour and set flags as needed.
* The AM/PM bit polarity and position is different on the EPSON.
*/
if (sc->sc_use_ampm) {
pmflags = (sc->chiptype != TYPE_EPSON_RX8035) ?
DS1307_HOUR_USE_AMPM : 0;
if (bct.ispm)
pmflags |= DS1307_HOUR_IS_PM;
} else if (sc->chiptype == TYPE_EPSON_RX8035)
pmflags = RX8035_HOUR_USE_24;
else
pmflags = 0;
data[DS1307_SECS] = bct.sec;
data[DS1307_MINS] = bct.min;
data[DS1307_HOUR] = bct.hour | pmflags;
data[DS1307_DATE] = bct.day;
data[DS1307_WEEKDAY] = bct.dow;
data[DS1307_MONTH] = bct.mon;
data[DS1307_YEAR] = bct.year & 0xff;
if (sc->chiptype == TYPE_MICROCHIP_MCP7491X) {
data[DS1307_SECS] |= MCP7941X_SECS_ST;
data[DS1307_WEEKDAY] |= MCP7941X_WEEKDAY_VBATEN;
year = bcd2bin(bct.year >> 8) * 100 + bcd2bin(bct.year & 0xff);
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
data[DS1307_MONTH] |= MCP7941X_MONTH_LPYR;
}
/* Write the time back to RTC. */
error = iicdev_writeto(sc->sc_dev, DS1307_SECS, data, sizeof(data),
IIC_INTRWAIT);
if (error != 0)
device_printf(dev, "%s: cannot write to RTC: %d\n",
__func__, error);
if (sc->chiptype == TYPE_EPSON_RX8035)
error = mark_epson_time_valid(sc);
return (error);
}
static device_method_t ds1307_methods[] = {
DEVMETHOD(device_probe, ds1307_probe),
DEVMETHOD(device_attach, ds1307_attach),
DEVMETHOD(device_detach, ds1307_detach),
DEVMETHOD(clock_gettime, ds1307_gettime),
DEVMETHOD(clock_settime, ds1307_settime),
DEVMETHOD_END
};
static driver_t ds1307_driver = {
"ds1307",
ds1307_methods,
sizeof(struct ds1307_softc),
};
DRIVER_MODULE(ds1307, iicbus, ds1307_driver, NULL, NULL);
MODULE_VERSION(ds1307, 1);
MODULE_DEPEND(ds1307, iicbus, 1, 1, 1);
IICBUS_FDT_PNP_INFO(ds1307_compat_data);