File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / rp5c15.cpp
Revision 1.1.1.18 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:00 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2020 nono project
// Licensed under nono-license.txt
//

//
// RTC (RP5C15)
//

// アラームは未実装

#include "rp5c15.h"
#include "event.h"
#include "mfp.h"
#include "monitor.h"
#include "power.h"

// コンストラクタ
RP5C15Device::RP5C15Device()
	: inherited()
{
	// X68k の RTC epoch は 1980年固定で変更の必要はないので、設定も見ない。
	year_epoch = 1980;
	// X68k は通常ローカルタイム (というか JST) で使用する前提
	use_localtime = true;

	monitor = gMonitorManager->Regist(ID_MONITOR_RTC, this);
	monitor->SetCallback(&RP5C15Device::MonitorScreen);
	monitor->SetSize(38, 22);
}

// デストラクタ
RP5C15Device::~RP5C15Device()
{
}

// 初期化
bool
RP5C15Device::Init()
{
	if (inherited::Init() == false) {
		return false;
	}

	mfp = GetMFPDevice();
	power = dynamic_cast<X68030PowerDevice *>(GetPowerDevice());

	// デフォルトで 24 時間計にしておく
	reg.sel24 = 0x01;
	// デフォルトで 1HZ_ON, 16HZ_ON をネゲート(1)しておく
	reg.reset = RP5C15::RESET_1Hz | RP5C15::RESET_16Hz;

	return true;
}

busdata
RP5C15Device::ReadPort(uint32 offset)
{
	busdata data;

	// バンクを展開する
	uint bank = (reg.mode & RP5C15::MODE_BANK);
	uint n = bank * 0x10 + offset;

	// XXX 14, 15 は write only らしいが何が読めるか

	switch (n) {
	 case 0x1d:	// MODE  (Bank0 と同じ)
	 case 0x1e:	// TEST  (Bank0 と同じ)
	 case 0x1f:	// RESET (Bank0 と同じ)
		n -= 0x10;
		FALLTHROUGH;

	 case 0x00 ... 0x1c:
		data = reg.r[n];
		break;
	 default:
		VMPANIC("corrupted n=$%x", n);
	}

	// 下位4ビット(RP5C15) の中の未実装ビットは %0 だが
	// 上位4ビットは %1 になる。
	data |= 0xf0;
	putlog(2, "Bank%u $%x -> $%02x", bank, offset, data.Data());

	// InsideOut p.135
	const busdata read_wait = busdata::Wait(29 * 40_nsec);
	data |= read_wait;

	data |= BusData::Size1;
	return data;
}

busdata
RP5C15Device::WritePort(uint32 offset, uint32 data)
{
	// バンクを展開する
	uint bank = (reg.mode & RP5C15::MODE_BANK);
	uint n = bank * 0x10 + offset;

	putlog(2, "Bank%u $%x <- $%x", bank, offset, (data & 0x0f));

	switch (n) {
	 case RP5C15::SEC1:
	 case RP5C15::SEC10:
	 case RP5C15::MIN1:
	 case RP5C15::MIN10:
	 case RP5C15::HOUR1:
	 case RP5C15::HOUR10:
	 case RP5C15::WDAY:		// 曜日カウンタは日付などとは一切連動していない
	 case RP5C15::DAY1:
	 case RP5C15::DAY10:
	 case RP5C15::MON1:
	 case RP5C15::MON10:
	 case RP5C15::YEAR1:
	 case RP5C15::YEAR10:
	 case RP5C15::SEL24:
	 case RP5C15::LEAP:
		reg.r[n] = data & reg.mask[n];
		break;

	 case RP5C15::CLKSEL:
		reg.clksel = data & reg.mask[n];
		switch (reg.clksel) {
		 case 0:	// ハイインピーダンス
		 case 1:	// 16384Hz
		 case 2:	// 1024Hz
		 case 3:	// 128Hz
			// 128Hz 点滅とか点灯みたいなもんだろ(適当)
			clkout = true;
			break;

			// (?) たぶん内部ではずっとカウンタ部分は動いていて
			// 出力位置を切り替えているだけのはずなので、
			// CLKSEL を変えた時の初期状態は内部カウンタの状態に依存するはず。
		 case 4:	// 16Hz
			clkout = ((cnt & 1) == 0);
			break;
		 case 5:	// 1Hz
			clkout = ((cnt & 16) == 0);
			break;
		 case 6:	// 1/60Hz
			clkout = (cnt_60sec < 30);
			break;
		 case 7:
			clkout = false;
			break;
		}
		break;

		// アラーム関係は未実装
	 case RP5C15::ALARM_MIN1 ... RP5C15::ALARM_DAY10:
	 case RP5C15::notused1:
	 case RP5C15::notused2:
		// マスクして書き込む
		reg.r[n] = data & reg.mask[n];
		break;

	 case RP5C15::ADJUST:
	 {
		// %0 なら何もしない?
		if ((data & 1) == 0) {
			break;
		}
		// %1 ならアジャスト。

		// 現在の秒が 0..29秒なら 0秒に切り下げ。
		// 30..59秒なら +1分0秒に繰り上げ。
		// いずれにしても 1秒と10秒の位は 0 にする。
		// XXX 秒以下のカウンタもクリアするのでは?
		if (GetSec() >= 30) {
			// 1分繰り上げ
			CountUpMin();
		}
		// マニュアルを読む限り秒以下をクリア(?)
		reg.sec1 = 0;
		reg.sec10 = 0;
		cnt = 0;
		clock_hold = false;
		break;
	 }

	 case RP5C15::MODE:
	 case RP5C15::MODE_BANK1:
		reg.mode = data & reg.mask[RP5C15::MODE];

		if ((reg.mode & RP5C15::MODE_TIMER_EN)) {
			if (clock_hold) {
				// ホールドされていた秒カウントアップを実行
				CountUpSec();
				clock_hold = false;
			}
		}
		break;

	 case RP5C15::TEST:
	 case RP5C15::TEST_BANK1:
		data &= reg.mask[RP5C15::TEST];
		if (data == 0) {
			// 通常 0 を書き込むこと、となっているので 0 は許可
		} else {
			putlog(0, "TEST <- $%1x (NOT IMPLEMENTED)", data);
		}
		reg.test = data;
		break;

	 case RP5C15::RESET:
	 case RP5C15::RESET_BANK1:
		reg.reset = data & reg.mask[RP5C15::RESET];
		if ((reg.reset & RP5C15::RESET_TIMER)) {
			// %1 を書き込むと、秒未満の内部カウンタをリセット
			// 繰り上がり分の保持も消す。
			cnt = 0;
			clock_hold = false;
		}
		if ((reg.reset & RP5C15::RESET_16Hz)) {
			alarm_16Hz = false;
		}
		if ((reg.reset & RP5C15::RESET_1Hz)) {
			alarm_1Hz = false;
		}
		ChangeAlarmOut();
		break;

	 default:
		break;
	}

	// InsideOut p.135
	const busdata write_wait = busdata::Wait(38 * 40_nsec);

	busdata r = write_wait;
	r |= BusData::Size1;
	return r;
}

busdata
RP5C15Device::PeekPort(uint32 offset)
{
	int n;

	// バンクを展開する
	int bank = (reg.mode & RP5C15::MODE_BANK);
	n = bank * 0x10 + offset;

	switch (n) {
	 case RP5C15::MODE_BANK1:
	 case RP5C15::TEST_BANK1:
	 case RP5C15::RESET_BANK1:
		// Bank0 と同じものが見える
		n -= 0x10;
		FALLTHROUGH;

	 case 0x00 ... 0x1c:
		return reg.r[n] | 0xf0;

	 default:
		return 0xff;
	}
}

bool
RP5C15Device::PokePort(uint32 offset, uint32 data)
{
	return false;
}

void
RP5C15Device::MonitorScreen(Monitor *, TextScreen& screen)
{
	int x = 18;
	int y = 0;

	screen.Clear();

	uint year = reg.year10 * 10 + reg.year1 + year_epoch;
	uint hour10;
	const char *ampm;
	if (reg.sel24) {
		ampm = "";
		hour10 = reg.hour10;
	} else {
		ampm = (reg.hour10 & 2) == 0 ? " AM": " PM";
		hour10 = reg.hour10 & 1;
	}
	screen.Print(0, y++, "%04u[%x%x]/%x%x/%x%x(%s) %x%x:%x%x:%x%x.%02x%s",
		year, reg.year10, reg.year1,
		reg.mon10, reg.mon1, reg.day10, reg.day1, wdays[reg.wday],
		hour10, reg.hour1, reg.min10, reg.min1, reg.sec10, reg.sec1,
		((uint32)cnt & 0x1f), ampm);

	screen.Print(0, y, "TimeZone: %s", (use_localtime) ? "Local" : "UTC");
	screen.Print(x, y, "Epoch: %4u",  year_epoch);

	// バンク0
	y = 3;
	screen.Print(0, y++, "Bank0");
	static const char * const b0names[] = {
		" 1sec",
		"10sec",
		" 1min",
		"10min",
		" 1hour",
		"10hour",
		"wday",
		" 1day",
		"10day",
		" 1mon",
		"10mon",
		" 1year",
		"10year",
	};
	for (int i = 0; i < countof(b0names); i++) {
		screen.Print(0, y, "$%x(%s)", i, b0names[i]);
		screen.Print(10, y, ": %x", reg.r[i]);
		y++;
	}

	// バンク1
	y = 3;
	screen.Print(x, y++, "Bank1");
	static const char * const b1names[] = {
		"CLKSEL",
		"ADJ",
		"AL 1m",
		"AL10m",
		"AL 1h",
		"AL10h",
		"ALwday",
		"AL 1d",
		"AL10d",
		"",
		"12/24H",
		"LEAP",
		"",
	};
	for (int i = 0; i < countof(b1names); i++) {
		if (b1names[i][0] != '\0') {
			screen.Print(x, y, "$%x(%s)", i, b1names[i]);
			screen.Print(x + 10, y, ": %x", reg.r[16 + i]);
		} else {
			// $9 と $c は欠番
			screen.Print(x, y, "$%x - ", i);
		}
		y++;
	}

	// CLKSEL
	static const char * const clksel_s[] = {
		"HiZ",
		"16384Hz",
		"1024Hz",
		"128Hz",
		"16Hz",
		"1Hz",
		"1/60Hz",
		"L",
	};
	screen.Print(x + 14, 4, "(%s)", clksel_s[reg.clksel]);

	// 12/24H
	screen.Print(x + 14, 14, "(%uH)", reg.sel24 ? 24 : 12);

	// MODE
	y = 17;
	screen.Print(0, y, "$d(MODE):   %x", reg.mode);
	screen.Puts(14, y, TA::OnOff(reg.mode & RP5C15::MODE_TIMER_EN), "TIMER_EN");
	screen.Puts(23, y, TA::OnOff(reg.mode & RP5C15::MODE_ALARM_EN), "ALARM_EN");
	screen.Print(32, y, "BANK=%u", reg.mode & RP5C15::MODE_BANK);

	// TEST
	y++;
	screen.Print(0, y, "$e(TEST):   %x", reg.test);

	// RESET
	y++;
	screen.Print(0, y, "$f(RESET):  %x", reg.reset);
	screen.Print(14, y, TA::OnOff(reg.reset & RP5C15::RESET_1Hz),  "!1Hz");
	screen.Print(19, y, TA::OnOff(reg.reset & RP5C15::RESET_16Hz), "!16Hz");
	screen.Puts(25, y, TA::OnOff(reg.reset & RP5C15::RESET_TIMER), "TIMER");
	screen.Puts(31, y, TA::OnOff(reg.reset & RP5C15::RESET_ALARM), "ALARM");

	// PIN
	y++;
	y++;
	// ハードウェアの ~ALARM ピンは負論理なのでアクティブ L だが、
	// ここでは分かりやすさ優先で正論理としている。
	screen.Print(0, y, "Pin: CLKOUT=%c  ALARM=%c",
		clkout ? 'H' : 'L',
		alarm_out ? 'H' : 'L');
}

// 秒を取得
uint
RP5C15Device::GetSec() const
{
	return reg.sec10 * 10 + reg.sec1;
}

// 秒を設定
void
RP5C15Device::SetSec(uint v)
{
	reg.sec1  = v % 10;
	reg.sec10 = v / 10;
}

// 分を取得
uint
RP5C15Device::GetMin() const
{
	return reg.min10 * 10 + reg.min1;
}

// 分を設定
void
RP5C15Device::SetMin(uint v)
{
	reg.min1  = v % 10;
	reg.min10 = v / 10;
}

// 時を取得
uint
RP5C15Device::GetHour() const
{
	if (reg.sel24) {
		return reg.hour10 * 10 + reg.hour1;
	} else {
		uint h10;
		h10 = (reg.hour10 & 0x1) * 10 + ((reg.hour10 & 0x2) >> 1) * 12;
		return h10 + reg.hour1;
	}
}

// 時を設定
void
RP5C15Device::SetHour(uint v)
{
	if (reg.sel24) {
		reg.hour1  = v % 10;
		reg.hour10 = v / 10;
	} else {
		uint pm = (v < 12) ? 0x0 : 0x2;
		v %= 12;
		reg.hour1  = v % 10;
		reg.hour10 = v / 10 + pm;
	}
}

// 曜日を取得
uint
RP5C15Device::GetWday() const
{
	return reg.wday;
}

// 曜日を設定
void
RP5C15Device::SetWday(uint v)
{
	reg.wday = v;
}

// 日を取得
uint
RP5C15Device::GetMday() const
{
	return reg.day10 * 10 + reg.day1;
}

// 日を設定
void
RP5C15Device::SetMday(uint v)
{
	reg.day1  = v % 10;
	reg.day10 = v / 10;
}

// 月を取得
uint
RP5C15Device::GetMon() const
{
	return reg.mon10 * 10 + reg.mon1;
}

// 月を設定
void
RP5C15Device::SetMon(uint v)
{
	reg.mon1  = v % 10;
	reg.mon10 = v / 10;
}

// 年を取得
uint
RP5C15Device::GetYear() const
{
	return reg.year10 * 10 + reg.year1 + year_epoch;
}

// 年を設定
void
RP5C15Device::SetYear(uint v)
{
	v -= year_epoch;
	reg.year1  = v % 10;
	reg.year10 = v / 10;
}

uint
RP5C15Device::GetLeap() const
{
	return reg.leap;
}

void
RP5C15Device::SetLeap(uint v)
{
	reg.leap = v & 3;
}

// ALARM OUT 信号
void
RP5C15Device::ChangeAlarmOut()
{
	bool old = alarm_out;

	// アラーム出力は内部のアラーム用 16Hz, 1Hz と
	// アラーム時刻一致の alarm_on との OR 論理となっている。
	alarm_out = alarm_16Hz | alarm_1Hz | alarm_on;

	if (old != alarm_out) {
		power->SetAlarmOut(alarm_out);
		mfp->SetAlarmOut(alarm_out);
	}
}

// 32Hz 仮想 RTC からのコール
void
RP5C15Device::Tick32Hz()
{
	if (reg.clksel == 4) {
		// CLKOUT 16Hz は duty50% なのでオン/オフ駆動は 32Hz になる
		clkout = ((cnt & 1) == 0);
	}

	// ALARM 端子の 16Hz パルスも CLKOUT に同期するとは書いてないけど、
	// 面倒なのでとりあえず同じところで処理してしまう。
	// データシートの書き方的には同じ波形になると思われる。

	// アラーム端子 16Hz パルス出力は %0 ならオン
	if ((reg.reset & RP5C15::RESET_16Hz) == 0) {
		alarm_16Hz = !alarm_16Hz;
		ChangeAlarmOut();
	}
}

// 2Hz 仮想 RTC からのコール
// 分周段 0/32, 16/32 秒でコールされる。
void
RP5C15Device::Tick2Hz()
{
	if (reg.clksel == 5) {
		// CLKOUT 1Hz は duty50% なのでオン/オフ駆動は 2Hz になる。
		// CLKOUT 端子は秒の繰り上がりに同期している。
		// TIMER_EN (秒への繰り上がり) が無効でも反転自体は起きる気が
		// するので、ここでは UPDATE_SEC ではなく cnt で判定する。
		// CLKOUT は 1秒の繰り上がりが起きる時に 'H' になる。

		clkout = ((cnt & 15) == 0);
	}

	// ALARM 端子の 1Hz パルスも CLKOUT に同期するとは書いてないけど、
	// 面倒なのでとりあえず同じところで処理してしまう。
	// データシートの書き方的には同じ波形になると思われる。

	// アラーム端子 16Hz パルス出力は %0 ならオン
	if ((reg.reset & RP5C15::RESET_1Hz) == 0) {
		alarm_1Hz = !alarm_1Hz;
		ChangeAlarmOut();
	}
}

// 1Hz 仮想 RTC からのコール
// 分周段 0/32 秒でコールされる。
void
RP5C15Device::Tick1Hz()
{
	if ((reg.mode & RP5C15::MODE_TIMER_EN)) {
		// 秒カウンタ更新
		CountUpSec();
	} else {
		// 秒カウンタは更新させない。内部のクロックホールドをアサート。
		clock_hold = true;
	}

	// CLKOUT 1/60 Hz については
	// 「秒のカウントアップのタイミングでアップエッジ」
	// としか書かれておらず、00 or 30 秒で動作するとは書かれていない。
	// データシートの図からはカレンダとは独立しているように読める。

	// 1/60Hz CLKOUT のための 60sec カウンタ。
	// このカウンタは clksel によらず常にカウントする。
	cnt_60sec++;
	if (cnt_60sec == 60) {
		cnt_60sec = 0;
	}

	if (reg.clksel == 6) {
		// CLKOUT 1/60Hz は duty50% なのでオン/オフ駆動は 30sec になる
		clkout = (cnt_60sec < 30);
	}
}

/*static*/ const uint8
RP5C15::mask[] = {
	0x0f,	0x07,	0x0f,	0x07,	// 1秒	10秒	1分		10分
	0x0f,	0x03,	0x07,	0x0f,	// 1時	10時	曜日	1日
	0x01,	0x0f,	0x01,	0x0f,	// 10日	1月		10月	1年
	0x0f,	0x0d,	0x0f,	0x0f,	// 10年	MODE	TEST	RESET

	0x07,	0x01,	0x0f,	0x07,	// CLKO	Adjust	AL1分	AL10分
	0x0f,	0x03,	0x07,	0x0f,	// AL1時 AL10時	AL曜日	AL1日
	0x03,	0x00,	0x01,	0x03,	// AL10日 ---	12/24	閏年
	0x00							// ---
};

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.