File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / mk48t02.cpp
Revision 1.1.1.17 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:06:01 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
//

//
// NVRAM/RTC (MK48T02)
//

// SRAM 部分はビッグエンディアンのままメモリに置かれるので、
// 必要ならアクセス時に都度変換すること。
//
// 仕様は 0x45000000 から 2KB だが、
// LUNA-I   では 0x44000000 から 0x47ffffff まで 2KB 単位でミラーが見える。
// LUNA-88K では 0x44000000 から 0x45ffffff までが
// (こちらはロングワード配置なので) 8KB 単位のミラーに見える。

#include "mk48t02.h"
#include "bitops.h"
#include "config.h"
#include "mainapp.h"
#include "mainbus.h"
#include "monitor.h"
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <array>

// コンストラクタ
MK48T02Device::MK48T02Device()
	: inherited()
{
	SetName("MK48T02");

	AddAlias("MK48T02");
	AddAlias("NVRAM");

	// 今の所 UTC として使うケースだけのようだ。
	use_localtime = false;

	monitor = gMonitorManager->Regist(ID_MONITOR_MK48T02, this);
	monitor->SetCallback(&MK48T02Device::MonitorScreen);
	monitor->SetSize(40, 13);
}

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

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

	struct stat st;

	// NetBSD/luna68k, OpenBSD/luna88k ともに epoch を 1970年としている
	// (2021/07 現在) 一方、4.4BSD (luna68k 版?) は 1900年としている。
	// MK48T02 本来定義は 0 というか 1900 というか。DecodeYear() も参照。
	const ConfigItem& item = gConfig->Find("rtc-epoch-year");
	int year_epoch_val;
	if (item.TryInt(&year_epoch_val) == false) {
		item.Err();
		return false;
	}
	// 雑にチェック
	if (year_epoch_val < 1900) {
		item.Err("Must be >= 1900");
		return false;
	}
	year_epoch = (uint)year_epoch_val;

	// MK48T02 のデータシートには年には西暦年の下位2桁を設定するように
	// 指示されており、4.4BSD (luna68k) はこの通りになっている。
	// しかし CMU Mach luna88k, NetBSD/luna68k, OpenBSD/luna88k はいずれも
	// 西暦年から 1970 引いた値を設定するようになっている (2021/07 現在)。
	// このため、うるう年が2年ずれた状態になる。
	//
	// NetBSD で MK48T02 を採用している luna68k 以外の port は
	// ソースコード上の変数 year0 もしくは YEAR0 に
	//  newsmips, news68k: 1900 (データシート規定通り)
	//  sun3, sparc, mvme68k: 1968 (1968 はうるう年なので問題ない)
	// を採用しており、バグっていない。
	// この year0 は UNIX EPOCH とは直接関係がない RTC ハードウェアに
	// 依存する値なのだが、LUNA では混同したのだろうか?
	//
	// たぶんこのバグはもう修正されることはないだろうから、こちらで補正する
	// スイッチを用意した。
	if (gMainApp.Has(VMCap::LUNA)) {
		if (gConfig->Find("luna-adjust-misused-epoch").AsBool()) {
			// エポック年のうるう年カウンタ相当を補正項として持つ
			adjust_leap = year_epoch % 4;
		} else {
			adjust_leap = 0;
		}
	}

	// MSX-DOS モードなら NVRAM 不要なのでここでダミーのメモリだけ用意。
	if (gMainApp.msxdos_mode) {
		// XXX アプリケーション寿命まで解放してないけど、まあいいか
		mem = (uint8 *)malloc(16 * 1024);
		return true;
	}

	// ファイルサイズを更新。
	// ver 0.2.3 までの NVRAM.DAT は 2040 バイトだったが、ver 0.2.4 以降は
	// 2048 バイトになるため、ここでファイルがあって 2040 バイトなら 2048
	// バイトに拡張する。失敗したらここでは何もしない。
	filename = gMainApp.GetVMDir() + "NVRAM.DAT";
	std::string dispname = string_format("NVRAM \"%s\"", filename.c_str());
	if (stat(filename.c_str(), &st) == 0 && st.st_size == 2040) {
		int fd;
		fd = open(filename.c_str(), O_WRONLY | O_APPEND);
		if (fd >= 0) {
			std::array<uint8, 8> zerobuf {};
			write(fd, &zerobuf[0], zerobuf.size());
			close(fd);
			stat(filename.c_str(), &st);
			warnx("%s extended to %jd bytes",
				dispname.c_str(), (intmax_t)st.st_size);
		}
	}

	// ファイルのロード
	file.SetFilename(filename);
	file.SetDispname(dispname);
	mem = file.OpenCreate(2048);
	if (mem == NULL) {
		// エラーメッセージは表示済み。
		return false;
	}

	return true;
}

void
MK48T02Device::StartTime()
{
	inherited::StartTime();

	// 内部カレンダをメモリに反映
	Store(reg.in);
}

// バイナリ表現を BCD 表現に変換する
/*static*/ inline uint8
MK48T02Device::num2BCD(uint8 num)
{
	return ((num / 10) << 4) + (num % 10);
}

// BCD 表現をバイナリ表現に変換する
/*static*/ inline uint8
MK48T02Device::BCD2num(uint8 bcd)
{
	return (bcd >> 4) * 10 + (bcd & 0x0f);
}

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

	data = mem[offset];
	putlog(3, "$%08x -> $%02x", GetMainbusDevice()->GetPaddr(), data.Data());

	// XXX wait?
	data |= BusData::Size1;
	return data;
}

busdata
MK48T02Device::WritePort(uint32 offset, uint32 data)
{
	putlog(3, "$%08x <- $%02x", GetMainbusDevice()->GetPaddr(), data);

	switch (offset - 2040) {
	 case 0:	// コントロール
		WriteCtrl(data);
		break;
	 case 1:	// 秒 (b7 は STOP BIT)
		reg.stop = data & MK48T02Clock::STOP;
		break;
	 case 2:	// 分
		break;
	 case 3:	// 時 (b7 は KICK START)
		reg.kickstart = data & MK48T02Clock::KICKSTART;
		break;
	 case 4:	// 曜日 (b6 は Frequency Test (未実装))
		// データシートでは b7, b5, b4 は '0' とあるが実際には '1' が
		// 書き込める。書き込んだ '1' が内部クロックの更新でどうなるか
		// 未確認のため、とりあえず放置。
		reg.freqtest = data & MK48T02Clock::FREQTEST;
		break;
	 case 5:	// 日
		// データシートでは b7, b6 は '0' とあるが実際には b7 には '1' が
		// 書き込める (b6 には書き込めない)。b7 に書き込んだ '1' が内部
		// クロックの更新でどうなるか未確認のため、とりあえずは放置。
		break;
	 case 6:	// 月
		break;
	 case 7:	// 年
		break;
	 default:
		break;
	}

	mem[offset] = data;

	// XXX wait?
	busdata r = BusData::Size1;
	return r;
}

busdata
MK48T02Device::PeekPort(uint32 offset)
{
	return mem[offset];
}

bool
MK48T02Device::PokePort(uint32 offset, uint32 data)
{
	// 時計部分にはコントロールレジスタもあるので書き込み不可。
	if (offset >= 2040) {
		return false;
	} else {
		if ((int32)data >= 0) {
			mem[offset] = data;
		}
		return true;
	}
}

void
MK48T02Device::MonitorScreen(Monitor *, TextScreen& screen)
{
	uint32 addr;
	uint32 v;
	int y = 0;

	screen.Clear();

	// BCD なので %x で表示する
	screen.Print(0, y++,
		"Internal: %04u[%02x]/%02x/%02x(%s) %02x:%02x:%02x.%1x",
		DecodeYear(reg.in.year), reg.in.year,
		reg.in.mon, reg.in.mday, wdays[reg.in.wday],
		reg.in.hour, reg.in.min, reg.in.sec, (uint32)((cnt >> 1) & 0xf));

	auto ex = Load();
	screen.Print(0, y++,
		"Register: %04u[%02x]/%02x/%02x(%s) %02x:%02x:%02x",
		DecodeYear(ex.year), ex.year,
		ex.mon, ex.mday, wdays[ex.wday],
		ex.hour, ex.min, ex.sec);

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

	addr = 0x800;
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (year=%02x)", addr, v, v);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (mon =%02x)", addr, v, v);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (date=%02x)", addr, v, v);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (wday=%02x  FT=%u)",
		addr, v, (v & 0x07), (v & MK48T02Clock::FREQTEST) ? 1 : 0);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (hour=%02x  KS=%u)",
		addr, v, (v & 0x3f), (v & MK48T02Clock::KICKSTART) ? 1 : 0);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (min =%02x)", addr, v, (v & 0x7f));
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (sec =%02x  ST=%u)",
		addr, v, (v & 0x7f), (v & MK48T02Clock::STOP) ? 1 : 0);
	v = PeekPort(--addr);
	screen.Print(0, y++, "$%3x: $%02x (W=%u R=%u)",
		addr, v,
		(v & MK48T02Clock::WRITE) ? 1 : 0,
		(v & MK48T02Clock::READ)  ? 1 : 0);
}

// コントロールレジスタへの書き込み
void
MK48T02Device::WriteCtrl(uint32 data)
{
	uint8 old = mem[0x7f8];

	bool write_fall = bit_falling(old, data, MK48T02Clock::WRITE);

	// ホールドしているかどうかは状態なのでエッジではない
	hold = (data & (MK48T02Clock::READ | MK48T02Clock::WRITE));

	if (write_fall) {
		// WRITE ビットの立ち下がりエッジでメモリから内部カウンタへロード
		reg.in = Load();

		// 分周段は規定されていないが意味的にクリアするほうが良いだろう
		cnt = 0;
	}
}

// NVRAM からカレンダをロード
MK48T02Clock::calendar
MK48T02Device::Load() const
{
	MK48T02Clock::calendar cal;

	cal.sec = mem[0x7f9] & 0x7f;
	cal.min = mem[0x7fa] & 0x7f;
	cal.hour = mem[0x7fb] & 0x3f;
	cal.wday = mem[0x7fc] & 0x07;
	cal.mday = mem[0x7fd] & 0x3f;
	cal.mon = mem[0x7fe] & 0x1f;
	cal.year = mem[0x7ff];

	return cal;
}

// カレンダを NVRAM にストア
void
MK48T02Device::Store(const MK48T02Clock::calendar& cal)
{
	mem[0x7f9] = (mem[0x7f9] & ~0x7f) | cal.sec;
	mem[0x7fa] = (mem[0x7fa] & ~0x7f) | cal.min;
	mem[0x7fb] = (mem[0x7fb] & ~0x3f) | cal.hour;
	mem[0x7fc] = (mem[0x7fc] & ~0x07) | cal.wday;
	mem[0x7fd] = (mem[0x7fd] & ~0x3f) | cal.mday;
	mem[0x7fe] = (mem[0x7fe] & ~0x1f) | cal.mon;
	mem[0x7ff] = cal.year;
}

uint
MK48T02Device::GetSec() const
{
	return BCD2num(reg.in.sec);
}

void
MK48T02Device::SetSec(uint v)
{
	reg.in.sec = num2BCD(v);
}

uint
MK48T02Device::GetMin() const
{
	return BCD2num(reg.in.min);
}

void
MK48T02Device::SetMin(uint v)
{
	reg.in.min = num2BCD(v);
}

uint
MK48T02Device::GetHour() const
{
	return BCD2num(reg.in.hour);
}

void
MK48T02Device::SetHour(uint v)
{
	reg.in.hour = num2BCD(v);
}

uint
MK48T02Device::GetWday() const
{
	// データシートの正規の値範囲は 1..7 だが 0 も問題なく保持できて、
	// NetBSD/luna68k, OpenBSD/luna88k は日付を日曜に変更する際には 0 を
	// レジスタに書き込んでいる。そのためレジスタ値は 0 と 7 を日曜、
	// 1を月曜…という運用ということにする。
	// 戻り値は 0..6 (0が日曜)。

	uint v = reg.in.wday;
	if (v == 7) {
		v = 0;
	}
	return v;
}

void
MK48T02Device::SetWday(uint v)
{
	// 引数は 0..6 (0が日曜)、MK48T02 の正規の範囲は 1..7 で曜日との対応は
	// 運用依存のようだ。NetBSD/luna68k, OpenBSD/luna88k は 0 を日曜、1 を
	// 月曜…と扱っているが、MK48T02 の曜日レジスタは 7 の翌日 1 に繰り上がる
	// 動作なので、ここでもレジスタには 1..7 (1が月曜、7が日曜) をセットする。
	if (v == 0)
		v = 7;
	reg.in.wday = v;
}

uint
MK48T02Device::GetMday() const
{
	return BCD2num(reg.in.mday);
}

void
MK48T02Device::SetMday(uint v)
{
	reg.in.mday = num2BCD(v);
}

uint
MK48T02Device::GetMon() const
{
	return BCD2num(reg.in.mon);
}

void
MK48T02Device::SetMon(uint v)
{
	reg.in.mon = num2BCD(v);
}

uint
MK48T02Device::DecodeYear(uint bcd_year) const
{
	uint y = BCD2num(bcd_year);
	y += year_epoch;

	// 1970年より前を 2000 年代とみなすのはシステム依存だが
	// UNIX 系はみんなこう(?)
	// XXX ここの 1970 と RTC エポックを一致させれば端折れるじゃん!と
	//     思いついたのかしら?
	if (y < 1970) {
		y += 100;
	}
	return y;
}

uint
MK48T02Device::GetYear() const
{
	// see DecodeYear()
	return DecodeYear(reg.in.year);
}

void
MK48T02Device::SetYear(uint v)
{
	// see DecodeYear()
	v -= year_epoch;
	reg.in.year = num2BCD(v % 100);
}

uint
MK48T02Device::GetLeap() const
{
	// 補正項込みで計算
	return (BCD2num(reg.in.year) + adjust_leap) & 3;
}

void
MK48T02Device::SetLeap(uint v)
{
	// nop
	// MK48T02 ではうるう年カウンタはなく、西暦年でうるう年を判断している。
}

// 1Hz 仮想 RTC からのコール
// (分周段 0/32 秒でコールされる)
void
MK48T02Device::Tick1Hz()
{
	if (reg.stop) {
		// 実機は電力消費を抑えるため発振器ごと止めるが、
		// 分周段が規定されていないので秒カウントを止めれば十分なはず
		//nop
	} else {
		CountUpSec();
	}

	if (hold == false) {
		// 秒の更新でメモリにコピーする (Datasheet p2)
		Store(reg.in);
	}
}

// 指定したアドレスからのゼロ終端文字列を返す
// (内蔵 ROM からのアクセス用)
std::string
MK48T02Device::PeekString(uint32 addr) const
{
	std::string rv = "";

	addr &= 0x7ff;
	for (; mem[addr]; addr++) {
		rv += mem[addr];
	}
	return rv;
}

// 指定したアドレスに文字列を書き込み、
// 16 バイトに満たないところはゼロでパディングする。
// 書き込めれば true を、書き込めなければ false を返す。
// (内蔵 ROM からの書き込み用)
bool
MK48T02Device::WriteString(uint32 addr, const std::string& data)
{
	int i = 0;

	// ここの文字列は16バイト未満なら一見ゼロ終端のようだが、
	// 16バイトならゼロ終端せず16文字とするフォーマット。

	if (data.length() > 16) {
		return false;
	}

	addr &= 0x7ff;
	for (; i < data.length(); i++) {
		mem[addr + i] = data[i];
	}
	for (; i < 16; i++) {
		mem[addr + i] = 0;
	}
	return true;
}

// 全域をゼロクリアする。
// クリアするだけなので、その後何か書いたりチェックサムを計算したりする
// 必要はある。
// (内蔵 ROM からの書き込み用)
void
MK48T02Device::ClearAll()
{
	for (int i = 0, sz = file.GetMemSize(); i < sz; i++) {
		mem[i] = 0;
	}
}

unix.superglobalmegacorp.com

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