diff options
Diffstat (limited to 'src/devices/machine/bq48x2.cpp')
-rw-r--r-- | src/devices/machine/bq48x2.cpp | 558 |
1 files changed, 558 insertions, 0 deletions
diff --git a/src/devices/machine/bq48x2.cpp b/src/devices/machine/bq48x2.cpp new file mode 100644 index 00000000000..a4524367d3c --- /dev/null +++ b/src/devices/machine/bq48x2.cpp @@ -0,0 +1,558 @@ +// license:BSD-3-Clause +// copyright-holders:Michael Zapf +/* + Texas Instruments/Benchmarq BQ4842/52 Real-time clock + + Michael Zapf, April 2020 +*/ +#include "emu.h" +#include "bq48x2.h" + +#define LOG_WARN (1U << 1) // Warnings +#define LOG_CLOCK (1U << 2) // Clock operation +#define LOG_REGW (1U << 3) // Register write +#define LOG_WATCHDOG (1U << 4) // Watchdog +#define LOG_SRAM (1U << 5) // SRAM + +#define VERBOSE (LOG_GENERAL | LOG_WARN) +#include "logmacro.h" + +// device type definition +DEFINE_DEVICE_TYPE(BQ4842, bq4842_device, "bq4842", "Benchmarq BQ4842 RTC") +DEFINE_DEVICE_TYPE(BQ4852, bq4852_device, "bq4852", "Benchmarq BQ4852 RTC") + +enum +{ + reg_year = 0, + reg_month, + reg_date, + reg_days, + reg_hours, + reg_minutes, + reg_seconds, + reg_control, + reg_watchdog, + reg_interrupts, + reg_alarmdate, + reg_alarmhours, + reg_alarmminutes, + reg_alarmseconds, + reg_100ths, + reg_flags +}; + +enum +{ + FLAG_FTE = 0x40, + FLAG_OSC = 0x80, + FLAG_W = 0x80, + FLAG_R = 0x40, + FLAG_WDS = 0x80, + FLAG_AIE = 0x80, + FLAG_PIE = 0x10, + FLAG_AF = 0x40, + FLAG_WDF = 0x80, + FLAG_PF = 0x08 +}; + +//------------------------------------------------- +// Constructors for basetype +//------------------------------------------------- + +bq48x2_device::bq48x2_device(const machine_config &mconfig, device_type type, const char *tag, device_t *owner, int memsize) + : device_t(mconfig, type, tag, owner, 0), + device_nvram_interface(mconfig, *this), + device_rtc_interface(mconfig, *this), + m_interrupt_cb(*this), + m_resetout_cb(*this), + m_memsize(memsize) +{ +} + +//------------------------------------------------- +// Constructors for subtypes +//------------------------------------------------- + +// 128 KiB memory (including clock registers) +bq4842_device::bq4842_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) + : bq48x2_device(mconfig, BQ4842, tag, owner, 128*1024) +{ +} + +// 512 KiB memory (including clock registers) +bq4852_device::bq4852_device(const machine_config &mconfig, const char *tag, device_t *owner, uint32_t clock) + : bq48x2_device(mconfig, BQ4852, tag, owner, 512*1024) +{ +} + +/* + Inherited from device_rtc_interface. The date and time is given as integer + and must be converted to BCD. +*/ +void bq48x2_device::rtc_clock_updated(int year, int month, int day, int day_of_week, int hour, int minute, int second) +{ + m_intreg[reg_hours] = convert_to_bcd(hour); + m_intreg[reg_minutes] = convert_to_bcd(minute); + m_intreg[reg_seconds] = convert_to_bcd(second); + m_intreg[reg_year] = convert_to_bcd(year); + m_intreg[reg_month] = convert_to_bcd(month); + m_intreg[reg_date] = convert_to_bcd(day); + m_intreg[reg_days] = convert_to_bcd(day_of_week); +} + +bool bq48x2_device::increment_bcd(uint8_t& bcdnumber, uint8_t limit, uint8_t min) +{ + if (!valid_bcd(bcdnumber, min, limit)) + { + bcdnumber = min; + return false; + } + + if (bcdnumber==limit) + { + bcdnumber = min; + return true; + } + else + { + uint8_t dig0 = bcdnumber & 0x0f; + uint8_t dig1 = bcdnumber & 0xf0; + + if (dig0==9) + { + bcdnumber = dig1 + 0x10; + } + else bcdnumber++; + } + return false; +} + +// TODO: Remove; the real clock cannot verify BCD numbers. +bool bq48x2_device::valid_bcd(uint8_t value, uint8_t min, uint8_t max) +{ + bool valid = ((value>=min) && (value<=max) && ((value&0x0f)<=9)); + if (!valid) LOGMASKED(LOG_WARN, "Invalid BCD number %02x\n", value); + return valid; +} + +// ---------------------------------------------------- + +/* + Update cycle, called every second + The BQ RTCs use BCD representation +*/ +TIMER_CALLBACK_MEMBER(bq48x2_device::rtc_clock_cb) +{ + // Just for debugging + static const char* dow[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + + bool carry = true; + bool newsec = false; + + // Test mode (FTW) or oscillator stop (OSC) + if (get_register(reg_days, FLAG_FTE) || get_register(reg_seconds, FLAG_OSC)) + return; + + // When the timer ticks, the 100ths are 0. + // TODO: Verify this with a real chip + m_intreg[reg_100ths] = 0; + + if (carry) + { + carry = increment_bcd(m_intreg[reg_seconds], 0x59, 0); + newsec = true; + } + if (carry) + carry = increment_bcd(m_intreg[reg_minutes], 0x59, 0); + + if (carry) + { + increment_bcd(m_intreg[reg_hours], 0xff, 0); + if (m_intreg[reg_hours] == 0x24) + { + m_intreg[reg_hours] = 0; + carry = true; + } + } + if (carry) + { + advance_days_bcd(); + } + + LOGMASKED(LOG_CLOCK, "%s 20%02x-%02x-%02x %02x:%02x:%02x\n", + dow[m_intreg[reg_days]-1], m_intreg[reg_year], m_intreg[reg_month], m_intreg[reg_date], + m_intreg[reg_hours], m_intreg[reg_minutes], m_intreg[reg_seconds]); + + // Copy into memory registers if the read bit is reset + if (newsec) + { + if (!is_set(reg_control, FLAG_R | FLAG_W)) + { + // Copy values from internal registers to memory space + transfer_to_access(); + } + + if (check_match(reg_date, reg_alarmdate, 0x3f) && + check_match(reg_hours, reg_alarmhours, 0x3f) && + check_match(reg_minutes, reg_alarmminutes, 0x7f) && + check_match(reg_seconds, reg_alarmseconds, 0x7f)) + { + set_register(reg_flags, FLAG_AF, true); + m_interrupt_cb(intrq_r()); + } + } +} +void bq48x2_device::advance_days_bcd() +{ + bool carry = false; + + // BCD-encoded numbers + static const int days_in_month_table[12] = + { + 0x31, 0x28, 0x31, 0x30, 0x31, 0x30, + 0x31, 0x31, 0x30, 0x31, 0x30, 0x31 + }; + + uint8_t month = bcd_to_integer(m_intreg[reg_month]); + if (month > 12) month = 12; + + // if (!valid_bcd(month, 0x01, 0x12)) month = 1; + uint8_t days = days_in_month_table[month-1]; + + // Leap years are indeed handled (but the year is only 2-digit) + if ((month==2) && ((m_intreg[reg_year]%4)==0)) + days = 0x29; + + increment_bcd(m_intreg[reg_days], 7, 1); // Increment the day-of-week (without carry) + carry = increment_bcd(m_intreg[reg_date], days, 1); + + if (carry) + { + increment_bcd(m_intreg[reg_month], 0xff, 1); + if (m_intreg[reg_month] == 0x13) + { + m_intreg[reg_month] = 0x01; + increment_bcd(m_intreg[reg_year], 0xff, 0); + } + } +} + +uint8_t bq48x2_device::get_register(int number, uint8_t mask) +{ + return m_sram[m_memsize-1-number] & mask; +} + +bool bq48x2_device::is_set(int number, uint8_t flag) +{ + return get_register(number, flag)!=0; +} + +void bq48x2_device::set_register(int number, uint8_t bits, bool set) +{ + int addr = m_memsize-1-number; + + if (set) + m_sram[addr] |= bits; + else + m_sram[addr] &= ~bits; +} + +void bq48x2_device::set_register(int number, uint8_t value) +{ + m_sram[m_memsize-1-number] = value; +} + +// The 0 bits in these masks are the "unused bits" according to the specification; +// they are left unchanged +static const uint8_t regmask[] = { 0xff, 0x1f, 0x3f, 0x07, 0x3f, 0x7f, 0x7f, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xf8 }; + +void bq48x2_device::transfer_to_int() +{ + uint8_t hds = m_intreg[reg_100ths]; + + for (int i=0; i < 16; i++) + m_intreg[i] = get_register(i, regmask[i]); + + // If we set the 100ths not to be 0, the next second will occur earlier + // TODO: Check this with the real chip + if (hds != m_intreg[reg_100ths]) + m_clock_timer->adjust(attotime::from_msec(get_delay()), 0, attotime::from_seconds(1)); +} + +void bq48x2_device::transfer_to_access() +{ + for (int i=0; i < 16; i++) + set_register(i, get_register(i, ~regmask[i]) | (m_intreg[i] & regmask[i])); +} + +bool bq48x2_device::check_match(int now, int alarm, uint8_t mask) +{ + // The ignore feature is active once the alarm has set in + // Will lead to a periodic alarm + bool ignore = (is_set(alarm, 0x80) && is_set(reg_flags, FLAG_AF)); + return ignore || ((m_intreg[now] & mask) == get_register(alarm, mask)); +} + +// ========================================================= + +/* + Read from SRAM or registers +*/ +uint8_t bq48x2_device::read(offs_t address) +{ + address = address & (m_memsize-1); + + uint8_t value = m_sram[address]; + + if ((m_memsize-1-address) == reg_flags) // Read flag register + { + set_register(reg_flags, 0xf8, false); // reset all flags + m_interrupt_cb(intrq_r()); + } + return value; +} + +/* + Write to the SRAM or registers +*/ +void bq48x2_device::write(offs_t address, uint8_t data) +{ + address = address % m_memsize; + + int regmask = (m_memsize - 1) & ~0x0f; + + // Registers + if ((address & regmask) == regmask) + { + int regnum = 15 - (address & 0x0f); + switch (regnum) + { + // No special effect + case reg_year: + case reg_month: + case reg_date: + case reg_hours: + case reg_minutes: + case reg_alarmdate: + case reg_alarmhours: + case reg_alarmminutes: + case reg_alarmseconds: + case reg_100ths: + break; + + case reg_days: + if (data & FLAG_FTE) + // Test mode + m_periodic_timer->adjust(attotime::from_hz(1024), 0, attotime::from_hz(1024)); + else + { + // reset to periodic timing + set_periodic_timer(); + } + break; + case reg_seconds: + // Start oscillator on falling edge + if (is_set(reg_seconds, FLAG_OSC) && ((data & FLAG_OSC) == 0)) + connect_osc(true); + else + { + // Turn off oscillator on raising edge + if (!is_set(reg_seconds, FLAG_OSC) && ((data & FLAG_OSC) != 0)) + connect_osc(false); + } + break; + case reg_control: + // Transfer to internal registers when W set to 0 + if (is_set(reg_control, FLAG_W) && ((data & FLAG_W) == 0)) + transfer_to_int(); + // Calibration bits are ignored, we don't calibrate the + // backing PC clock + break; + case reg_watchdog: + set_register(regnum, data); + set_watchdog_timer(); + break; + + case reg_interrupts: + set_register(regnum, data); + set_periodic_timer(); + return; + case reg_flags: + LOGMASKED(LOG_WARN, "Ignoring write attempt to flag bit register (%02x)\n", data); + return; + } + set_register(regnum, data); + } + else + { + LOGMASKED(LOG_SRAM, "sram %05x <- %02x\n", address, data); + m_sram[address] = data; + } +} + +void bq48x2_device::set_periodic_timer() +{ + uint8_t rateval = get_register(reg_interrupts, 0x0f); + int rate = 0; + + switch (rateval) + { + case 0: + m_periodic_timer->reset(); + break; + case 1: + m_periodic_timer->adjust(attotime::from_msec(10), 0, attotime::from_msec(10)); + break; + case 2: + m_periodic_timer->adjust(attotime::from_msec(100), 0, attotime::from_msec(100)); + break; + default: + rate = 1 << (16-rateval); + m_periodic_timer->adjust(attotime::from_hz(rate), 0, attotime::from_hz(rate)); + break; + } +} + +void bq48x2_device::set_watchdog_timer() +{ + int multi = get_register(reg_watchdog, 0x7c)>>2; + int reso = get_register(reg_watchdog, 0x03); + + // reso = 0 -> 1/16 s (2^-4) = 62500 us + // reso = 1 -> 1/4 s (2^-2) + // reso = 2 -> 1 s (2^0) + // reso = 3 -> 4 s (2^2) + + int time = (1<<(reso*2))*62500 * multi; + m_watchdog_timer->adjust(attotime::from_usec(time)); // single shot +} + +/* + Periodic cycle (called at defined intervals) +*/ +TIMER_CALLBACK_MEMBER(bq48x2_device::rtc_periodic_cb) +{ + // Test mode + if (get_register(reg_days, FLAG_FTE)) + { + // Create a 1:1 on-off signal on the seconds' last bit + set_register(reg_seconds, get_register(reg_seconds, 0xff) ^ 0x01); + } + else + { + set_register(reg_flags, FLAG_PF, true); + // The INT line is only released by reading the flag register + if (intrq_r()) + { + m_interrupt_cb(ASSERT_LINE); + } + } +} + +/* + Watchdog callback +*/ +TIMER_CALLBACK_MEMBER(bq48x2_device::rtc_watchdog_cb) +{ + set_register(reg_flags, FLAG_WDF, true); + if (is_set(reg_watchdog, FLAG_WDS)) + { + LOGMASKED(LOG_WATCHDOG, "Watchdog alarm, reset pulse\n"); + m_resetout_cb(ASSERT_LINE); + // During the reset pulse, the watchdog register is cleared + set_register(reg_watchdog, 0); + m_resetout_cb(CLEAR_LINE); + } + else + { + LOGMASKED(LOG_WATCHDOG, "Watchdog alarm, interrupt\n"); + m_interrupt_cb(intrq_r()); + } +} + +/* + Indicates that there is an interrupt condition. Also used to drive the + outgoing line. +*/ +int bq48x2_device::intrq_r() +{ + bool alarm = (is_set(reg_interrupts, FLAG_AIE) && is_set(reg_flags, FLAG_AF)); + bool period = (is_set(reg_interrupts, FLAG_PIE) && is_set(reg_flags, FLAG_PF)); + + return (alarm || period)? ASSERT_LINE : CLEAR_LINE; +} + +void bq48x2_device::connect_osc(bool conn) +{ + if (conn) + { + // The internal update cycle is 1 sec + m_clock_timer->adjust(attotime::from_msec(get_delay()), 0, attotime::from_seconds(1)); + set_periodic_timer(); + } + else + { + // Turn off completely + m_clock_timer->reset(); + m_watchdog_timer->reset(); + m_periodic_timer->reset(); + } +} + +int bq48x2_device::get_delay() +{ + int hds = ((m_intreg[reg_100ths] & 0xf0)>>16) * 10 + (m_intreg[reg_100ths] & 0x0f); + return 1000 - hds*10; +} + +void bq48x2_device::device_start() +{ + m_clock_timer = timer_alloc(FUNC(bq48x2_device::rtc_clock_cb), this); + + // Periodic timer + m_periodic_timer = timer_alloc(FUNC(bq48x2_device::rtc_periodic_cb), this); + + // Watchdog timer + m_watchdog_timer = timer_alloc(FUNC(bq48x2_device::rtc_watchdog_cb), this); + + m_sram = std::make_unique<u8 []>(m_memsize); + + // Interrupt enables are cleared on powerup + set_register(reg_interrupts, 0xff, false); + + // State save + save_pointer(NAME(m_sram), m_memsize); + save_pointer(NAME(m_intreg), 8); + + // Start clock + connect_osc(true); +} + +// ---------------------------------------------------- + +void bq48x2_device::nvram_default() +{ + std::fill_n(m_sram.get(), m_memsize, 0); +} + +bool bq48x2_device::nvram_read(util::read_stream &file) +{ + auto const [err, actual] = util::read(file, m_sram.get(), m_memsize); + if (err || (actual != m_memsize)) + return false; + + transfer_to_access(); // Transfer the system time into the readable registers + + // Clear the saved flags + set_register(reg_flags, 0xf8, true); + + return true; +} + +bool bq48x2_device::nvram_write(util::write_stream &file) +{ + transfer_to_access(); + + auto const [err, actual] = util::write(file, m_sram.get(), m_memsize); + return !err; +} |