A write to a time register rebuilt a struct tm from the current date, substituted the field being written and folded the result back into the offset. Every byte of a multi-byte transfer was therefore normalised against the fields the guest had not written yet: a date of 31 programmed while the month register still held a 30-day month moved the clock into the following month.
Keep the written value in the register file and derive the offset from the file as a whole. This needs the registers refreshed on a START_SEND, as the hardware does on any START. Signed-off-by: Emmanuel Blot <[email protected]> --- hw/rtc/ds1338.c | 116 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 63 insertions(+), 53 deletions(-) diff --git a/hw/rtc/ds1338.c b/hw/rtc/ds1338.c index 7eaa663113..862e03bd32 100644 --- a/hw/rtc/ds1338.c +++ b/hw/rtc/ds1338.c @@ -8,6 +8,12 @@ * * Contributions after 2012-01-13 are licensed under the terms of the * GNU GPL, version 2 or (at your option) any later version. + * + * Limitations: + * - Writing the seconds register does not reset the internal countdown chain, + * so the sub-second phase of the clock is not modelled. + * - Leap years follow the Gregorian rule, where the parts stop at 2100: they + * derive the leap year from the two-digit year alone. */ #include "qemu/osdep.h" @@ -75,6 +81,41 @@ static const VMStateDescription vmstate_ds1338 = { } }; +/* Reconstruct a struct tm from the BCD time registers in nvram. */ +static void ds1338_time_from_regs(DS1338State *s, struct tm *now) +{ + qemu_get_timedate(now, s->offset); + now->tm_sec = from_bcd(s->nvram[0] & 0x7f); + now->tm_min = from_bcd(s->nvram[1] & 0x7f); + if (s->nvram[2] & HOURS_12) { + int tmp = from_bcd(s->nvram[2] & (HOURS_PM - 1)); + if (s->nvram[2] & HOURS_PM) { + tmp += 12; + } + if (tmp % 12 == 0) { + tmp -= 12; + } + now->tm_hour = tmp; + } else { + now->tm_hour = from_bcd(s->nvram[2] & (HOURS_12 - 1)); + } + now->tm_mday = from_bcd(s->nvram[4] & 0x3f); + now->tm_mon = from_bcd(s->nvram[5] & 0x1f) - 1; + now->tm_year = from_bcd(s->nvram[6]) + 100; +} + +static void ds1338_resync_from_regs(DS1338State *s) +{ + struct tm now; + int user_wday; + + ds1338_time_from_regs(s, &now); + s->offset = qemu_timedate_diff(&now); + qemu_get_timedate(&now, s->offset); + user_wday = (s->nvram[3] & 7) - 1; + s->wday_offset = (user_wday - now.tm_wday + 7) % 7; +} + static void ds1338_capture_current_time(DS1338State *s) { /* Capture the current time into the secondary registers @@ -123,16 +164,16 @@ static int ds1338_event(I2CSlave *i2c, enum i2c_event event) DS1338State *s = DS1338(i2c); switch (event) { - case I2C_START_RECV: - /* In h/w, capture happens on any START condition, not just a - * START_RECV, but there is no need to actually capture on - * START_SEND, because the guest can't get at that data - * without going through a START_RECV which would overwrite it. - */ - ds1338_capture_current_time(s); - break; case I2C_START_SEND: s->addr_byte = true; + /* fall through */ + case I2C_START_RECV: + /* + * In h/w the time is transferred to the user registers on any START + * condition, so a write modifies the running time rather than + * whatever the last read left behind. + */ + ds1338_capture_current_time(s); break; default: break; @@ -167,51 +208,20 @@ static int ds1338_send(I2CSlave *i2c, uint8_t data) return 0; } if (s->ptr < 7) { - /* Time register. */ - struct tm now; - qemu_get_timedate(&now, s->offset); - switch(s->ptr) { - case 0: - /* TODO: Implement CH (stop) bit. */ - now.tm_sec = from_bcd(data & 0x7f); - break; - case 1: - now.tm_min = from_bcd(data & 0x7f); - break; - case 2: - if (data & HOURS_12) { - int tmp = from_bcd(data & (HOURS_PM - 1)); - if (data & HOURS_PM) { - tmp += 12; - } - if (tmp % 12 == 0) { - tmp -= 12; - } - now.tm_hour = tmp; - } else { - now.tm_hour = from_bcd(data & (HOURS_12 - 1)); - } - break; - case 3: - { - /* The day field is supposed to contain a value in - the range 1-7. Otherwise behavior is undefined. - */ - int user_wday = (data & 7) - 1; - s->wday_offset = (user_wday - now.tm_wday + 7) % 7; - } - break; - case 4: - now.tm_mday = from_bcd(data & 0x3f); - break; - case 5: - now.tm_mon = from_bcd(data & 0x1f) - 1; - break; - case 6: - now.tm_year = from_bcd(data) + 100; - break; - } - s->offset = qemu_timedate_diff(&now); + /* + * Time register. The write lands in the register file and the clock + * is then rebuilt from it as a whole: a transfer that programs the + * date before the month it belongs to must not be normalised away + * against the month still held from the previous date. + * + * TODO: Implement CH (stop) bit. + */ + static const uint8_t valid[7] = { + 0x7f, 0x7f, 0x7f, 0x07, 0x3f, 0x1f, 0xff + }; + + s->nvram[s->ptr] = data & valid[s->ptr]; + ds1338_resync_from_regs(s); } else { if (s->ptr == k->ctrl_addr) { /* Control register: reserved bits read back as zero. */ -- 2.50.1
