zephyr/drivers/counter/counter_cmos.c

215 lines
4.7 KiB
C

/*
* Copyright (c) 2019 Intel Corp.
* SPDX-License-Identifier: Apache-2.0
*
* This barebones driver enables the use of the PC AT-style RTC
* (the so-called "CMOS" clock) as a primitive, 1Hz monotonic counter.
*
* Reading a reliable value from the RTC is a fairly slow process, because
* we use legacy I/O ports and do a lot of iterations with spinlocks to read
* the RTC state. Plus we have to read the state multiple times because we're
* crossing clock domains (no pun intended). Use accordingly.
*/
#define DT_DRV_COMPAT motorola_mc146818
#include <zephyr/drivers/counter.h>
#include <zephyr/device.h>
#include <soc.h>
/* The "CMOS" device is accessed via an address latch and data port. */
#define X86_CMOS_ADDR (DT_INST_REG_ADDR_BY_IDX(0, 0))
#define X86_CMOS_DATA (DT_INST_REG_ADDR_BY_IDX(0, 1))
/*
* A snapshot of the RTC state, or at least the state we're
* interested in. This struct should not be modified without
* serious consideration, for two reasons:
*
* 1. Order of the element is important, and must correlate
* with addrs[] and NR_BCD_VALS (see below), and
* 2. if it doesn't remain exactly 8 bytes long, the
* type-punning to compare states will break.
*/
struct state {
uint8_t second,
minute,
hour,
day,
month,
year,
status_a,
status_b;
};
/*
* If the clock is in BCD mode, the first NR_BCD_VALS
* values in 'struct state' are BCD-encoded.
*/
#define NR_BCD_VALS 6
/*
* Indices into the CMOS address space that correspond to
* the members of 'struct state'.
*/
const uint8_t addrs[] = { 0, 2, 4, 7, 8, 9, 10, 11 };
/*
* Interesting bits in 'struct state'.
*/
#define STATUS_B_24HR 0x02 /* 24-hour (vs 12-hour) mode */
#define STATUS_B_BIN 0x01 /* binary (vs BCD) mode */
#define HOUR_PM 0x80 /* high bit of hour set = PM */
/*
* Read a value from the CMOS. Because of the address latch,
* we have to spinlock to make the access atomic.
*/
static uint8_t read_register(uint8_t addr)
{
static struct k_spinlock lock;
k_spinlock_key_t k;
uint8_t val;
k = k_spin_lock(&lock);
sys_out8(addr, X86_CMOS_ADDR);
val = sys_in8(X86_CMOS_DATA);
k_spin_unlock(&lock, k);
return val;
}
/* Populate 'state' with current RTC state. */
void read_state(struct state *state)
{
int i;
uint8_t *p;
p = (uint8_t *) state;
for (i = 0; i < sizeof(*state); ++i) {
*p++ = read_register(addrs[i]);
}
}
/* Convert 8-bit (2-digit) BCD to binary equivalent. */
static inline uint8_t decode_bcd(uint8_t val)
{
return (((val >> 4) & 0x0F) * 10) + (val & 0x0F);
}
/*
* Hinnant's algorithm to calculate the number of days offset from the epoch.
*/
static uint32_t hinnant(int y, int m, int d)
{
unsigned yoe;
unsigned doy;
unsigned doe;
int era;
y -= (m <= 2);
era = ((y >= 0) ? y : (y - 399)) / 400;
yoe = y - era * 400;
doy = (153 * (m + ((m > 2) ? -3 : 9)) + 2)/5 + d - 1;
doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
return era * 146097 + ((int) doe) - 719468;
}
/*
* Get the Unix epoch time (assuming UTC) read from the CMOS RTC.
* This function is long, but linear and easy to follow.
*/
int get_value(const struct device *dev, uint32_t *ticks)
{
struct state state, state2;
uint64_t *pun = (uint64_t *) &state;
uint64_t *pun2 = (uint64_t *) &state2;
bool pm;
uint32_t epoch;
ARG_UNUSED(dev);
/*
* Read the state until we see the same state twice in a row.
*/
read_state(&state2);
do {
state = state2;
read_state(&state2);
} while (*pun != *pun2);
/*
* Normalize the state; 12hr -> 24hr, BCD -> decimal.
* The order is a bit awkward because we need to interpret
* the HOUR_PM flag before we adjust for BCD.
*/
if ((state.status_b & STATUS_B_24HR) != 0U) {
pm = false;
} else {
pm = ((state.hour & HOUR_PM) == HOUR_PM);
state.hour &= ~HOUR_PM;
}
if ((state.status_b & STATUS_B_BIN) == 0U) {
uint8_t *cp = (uint8_t *) &state;
int i;
for (i = 0; i < NR_BCD_VALS; ++i) {
*cp = decode_bcd(*cp);
++cp;
}
}
if (pm) {
state.hour = (state.hour + 12) % 24;
}
/*
* Convert date/time to epoch time. We don't care about
* timezones here, because we're just creating a mapping
* that results in a monotonic clock; the absolute value
* is irrelevant.
*/
epoch = hinnant(state.year + 2000, state.month, state.day);
epoch *= 86400; /* seconds per day */
epoch += state.hour * 3600; /* seconds per hour */
epoch += state.minute * 60; /* seconds per minute */
epoch += state.second;
*ticks = epoch;
return 0;
}
static int init(const struct device *dev)
{
ARG_UNUSED(dev);
return 0;
}
static const struct counter_config_info info = {
.max_top_value = UINT_MAX,
.freq = 1
};
static const struct counter_driver_api api = {
.get_value = get_value
};
DEVICE_DT_INST_DEFINE(0, &init, NULL, NULL, &info,
POST_KERNEL, CONFIG_COUNTER_INIT_PRIORITY, &api);