1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-04-19 20:37:34 +00:00

Merge pull request #1488 from TomHarte/ATKeyboard

Implement some proportion of the AT keyboard controller.
This commit is contained in:
Thomas Harte 2025-03-12 13:19:15 -04:00 committed by GitHub
commit 143d1d5e35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 311 additions and 63 deletions

View File

@ -14,7 +14,7 @@ using namespace Zilog::SCC;
namespace {
Log::Logger<Log::Source::SCC> log;
Log::Logger<Log::Source::SCC> logger;
}
@ -54,7 +54,7 @@ std::uint8_t z8530::read(const int address) {
case 2: // Handled non-symmetrically between channels.
if(address & 1) {
log.error().append("Unimplemented: register 2 status bits");
logger.error().append("Unimplemented: register 2 status bits");
} else {
result = interrupt_vector_;
@ -111,11 +111,11 @@ void z8530::write(const int address, const std::uint8_t value) {
case 2: // Interrupt vector register; used only by Channel B.
// So there's only one of these.
interrupt_vector_ = value;
log.info().append("Interrupt vector set to %d", value);
logger.info().append("Interrupt vector set to %d", value);
break;
case 9: // Master interrupt and reset register; there is also only one of these.
log.info().append("Master interrupt and reset register: %02x", value);
logger.info().append("Master interrupt and reset register: %02x", value);
master_interrupt_control_ = value;
break;
}
@ -152,7 +152,7 @@ uint8_t z8530::Channel::read(const bool data, const uint8_t pointer) {
if(data) {
return data_;
} else {
log.info().append("Control read from register %d", pointer);
logger.info().append("Control read from register %d", pointer);
// Otherwise, this is a control read...
switch(pointer) {
default:
@ -237,10 +237,10 @@ void z8530::Channel::write(const bool data, const uint8_t pointer, const uint8_t
data_ = value;
return;
} else {
log.info().append("Control write: %02x to register %d", value, pointer);
logger.info().append("Control write: %02x to register %d", value, pointer);
switch(pointer) {
default:
log.info().append("Unrecognised control write: %02x to register %d", value, pointer);
logger.info().append("Unrecognised control write: %02x to register %d", value, pointer);
break;
case 0x0: // Write register 0 — CRC reset and other functions.
@ -248,13 +248,13 @@ void z8530::Channel::write(const bool data, const uint8_t pointer, const uint8_t
switch(value >> 6) {
default: /* Do nothing. */ break;
case 1:
log.error().append("TODO: reset Rx CRC checker.");
logger.error().append("TODO: reset Rx CRC checker.");
break;
case 2:
log.error().append("TODO: reset Tx CRC checker.");
logger.error().append("TODO: reset Tx CRC checker.");
break;
case 3:
log.error().append("TODO: reset Tx underrun/EOM latch.");
logger.error().append("TODO: reset Tx underrun/EOM latch.");
break;
}
@ -262,24 +262,24 @@ void z8530::Channel::write(const bool data, const uint8_t pointer, const uint8_t
switch((value >> 3)&7) {
default: /* Do nothing. */ break;
case 2:
// log.info().append("reset ext/status interrupts.");
// logger.info().append("reset ext/status interrupts.");
external_status_interrupt_ = false;
external_interrupt_status_ = 0;
break;
case 3:
log.error().append("TODO: send abort (SDLC).");
logger.error().append("TODO: send abort (SDLC).");
break;
case 4:
log.error().append("TODO: enable interrupt on next Rx character.");
logger.error().append("TODO: enable interrupt on next Rx character.");
break;
case 5:
log.error().append("TODO: reset Tx interrupt pending.");
logger.error().append("TODO: reset Tx interrupt pending.");
break;
case 6:
log.error().append("TODO: reset error.");
logger.error().append("TODO: reset error.");
break;
case 7:
log.error().append("TODO: reset highest IUS.");
logger.error().append("TODO: reset highest IUS.");
break;
}
break;
@ -304,7 +304,7 @@ void z8530::Channel::write(const bool data, const uint8_t pointer, const uint8_t
b1 = 1 => transmit buffer empty interrupt is enabled; 0 => it isn't.
b0 = 1 => external interrupt is enabled; 0 => it isn't.
*/
log.info().append("Interrupt mask: %02x", value);
logger.info().append("Interrupt mask: %02x", value);
break;
case 0x2: // Write register 2 - interrupt vector.
@ -319,7 +319,7 @@ void z8530::Channel::write(const bool data, const uint8_t pointer, const uint8_t
case 2: receive_bit_count = 6; break;
case 3: receive_bit_count = 8; break;
}
log.info().append("Receive bit count: %d", receive_bit_count);
logger.info().append("Receive bit count: %d", receive_bit_count);
/*
b7,b6:

View File

@ -0,0 +1,49 @@
//
// CPUControl.hpp
// Clock Signal
//
// Created by Thomas Harte on 08/03/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#pragma once
#include "Memory.hpp"
#include "ProcessorByModel.hpp"
#include "Registers.hpp"
#include "Segments.hpp"
#include "Analyser/Static/PCCompatible/Target.hpp"
#include "Outputs/Log.hpp"
namespace PCCompatible {
template <Analyser::Static::PCCompatible::Model model>
class CPUControl {
public:
CPUControl(
Registers<processor_model(model)> &registers,
Segments<processor_model(model)> &segments,
Memory<model> &memory
) : registers_(registers), segments_(segments), memory_(memory) {}
void reset() {
registers_.reset();
segments_.reset();
}
void set_a20_enabled(const bool enabled) {
// Assumed: this'll be something to set on Memory.
log_.info().append("A20 line is now: ", enabled);
}
private:
Registers<processor_model(model)> &registers_;
Segments<processor_model(model)> &segments_;
Memory<model> &memory_;
Log::Logger<Log::Source::PCCompatible> log_;
};
}

View File

@ -8,14 +8,22 @@
#pragma once
#include "CPUControl.hpp"
#include "PIC.hpp"
#include "Speaker.hpp"
namespace PCCompatible {
/*!
Provides an implementation of either an XT- or AT-style keyboard controller,
as determined by the model template parameter.
*/
template <Analyser::Static::PCCompatible::Model, typename Enable = void>
class KeyboardController;
/*!
Models the XT keyboard controller.
*/
template <Analyser::Static::PCCompatible::Model model>
class KeyboardController<model, typename std::enable_if_t<is_xt(model)>> {
public:
@ -85,6 +93,8 @@ public:
pics_.pic[0].template apply_edge<1>(true);
}
void set_cpu_control(CPUControl<model> *) {}
private:
enum class Mode {
NormalOperation = 0b01,
@ -99,24 +109,43 @@ private:
int reset_delay_ = 0;
};
/*!
Models the AT keyboard controller.
*/
template <Analyser::Static::PCCompatible::Model model>
class KeyboardController<model, typename std::enable_if_t<is_at(model)>> {
public:
KeyboardController(PICs<model> &pics, Speaker &speaker) : pics_(pics), speaker_(speaker) {}
void run_for([[maybe_unused]] const Cycles cycles) {
void run_for(const Cycles cycles) {
if(!write_delay_) return;
write_delay_ -= cycles.as<int>();
if(write_delay_ <= 0) {
write_delay_ = 0;
perform_command();
}
}
void post([[maybe_unused]] const uint8_t value) {
void post(const uint8_t value) {
input_ = value;
has_input_ = true;
command_phase_ = CommandPhase::WaitingForCommand;
pics_.pic[0].template apply_edge<1>(true);
}
template <typename IntT>
void write([[maybe_unused]] const uint16_t port, [[maybe_unused]] const IntT value) {
void write(const uint16_t port, const uint8_t value) {
switch(port) {
default:
log_.error().append("Unimplemented AT keyboard write: %04x to %04x", value, port);
break;
case 0x0060:
// log_.error().append("Keyboard parameter set to %02x", value);
output_ = value;
has_output_ = true;
write_delay_ = 10;
break;
case 0x0061:
// TODO:
// b7: 1 = reset IRQ 0
@ -124,30 +153,170 @@ public:
// b2: enable parity check
speaker_.set_control(value & 0x01, value & 0x02);
break;
case 0x0064:
// log_.info().append("AT keyboard command %04x", value);
command_ = value;
has_command_ = true;
command_phase_ = CommandPhase::WaitingForData;
write_delay_ = performance_delay(command_);
if(!write_delay_) {
perform_command();
}
break;
}
}
template <typename IntT>
IntT read([[maybe_unused]] const uint16_t port) {
IntT read(const uint16_t port) {
switch(port) {
default:
log_.error().append("Unimplemented AT keyboard read from %04x", port);
// log_.error().append("Unimplemented AT keyboard read from %04x", port);
break;
case 0x0060:
log_.error().append("Read from keyboard controller of %02x", input_);
has_input_ = false;
return input_;
case 0x0061:
// In a real machine bit 4 toggles as a function of memory refresh; it is often
// used by BIOSes to check that refresh is happening, with no greater inspection
// than that it is toggling. So toggle on read.
//
// TODO: is this really from the keyboard controller?
refresh_toggle_ ^= 0x10;
log_.info().append("AT keyboard: %02x from %04x", refresh_toggle_, port);
// log_.info().append("AT keyboard: %02x from %04x", refresh_toggle_, port);
return refresh_toggle_;
case 0x0064: {
// Status:
// b7 = 1 => parity error on transmission;
// b6 = 1 => receive timeout;
// b5 = 1 => transmit timeout;
// b4 = 1 => keyboard active;
// b3 = 1 = data at 0060 is command, 0 = data;
// b2 = 1 = selftest OK; 0 = just powered up or reset;
// b1 = 1 => 'input' buffer full (i.e. don't write 0x60 or 0x64 now — this is input to the controller);
// b0 = 1 => 'output' data is full (i.e. reading from 0x60 now makes sense — output is to PC).
const uint8_t status =
0x10 |
(command_phase_ == CommandPhase::WaitingForData ? 0x08 : 0x00) |
(is_tested_ ? 0x04 : 0x00) |
(has_output_ ? 0x02 : 0x00) |
(has_input_ ? 0x01 : 0x00);
log_.error().append("Reading status: %02x", status);
return status;
}
}
return IntT(~0);
}
void set_cpu_control(CPUControl<model> *const control) {
cpu_control_ = control;
}
private:
static constexpr bool requires_parameter(const uint8_t command) {
return
(command >= 0x60 && command < 0x80) ||
(command == 0xc1) || (command == 0xc2) ||
(command >= 0xd1 && command < 0xd5);
}
static constexpr int performance_delay(const uint8_t command) {
if(requires_parameter(command)) {
return 3;
}
switch(command) {
case 0xaa: return 5;
default: return 0;
}
}
void perform_command() {
// Wait for a command.
if(!has_command_) return;
// Wait for a parameter if one is needed.
if(requires_parameter(command_) && !has_output_) {
return;
}
{
auto info = log_.info();
info.append("Keyboard command: %02x", command_);
if(has_output_) {
info.append(" / %02x", output_);
}
}
// Consume command and parameter, and execute.
has_command_ = has_output_ = false;
switch(command_) {
default:
log_.info().append("Keyboard command unimplemented", command_);
break;
case 0xaa: // Self-test; 0x55 => no issues found.
log_.error().append("Keyboard self-test");
post(0x55);
break;
case 0xd1: // Set output byte. b1 = the A20 gate.
log_.error().append("Should set A20 gate: %d", output_ & 0x02);
cpu_control_->set_a20_enabled(output_ & 0x02);
break;
case 0x60:
is_tested_ = output_ & 0x4;
break;
case 0xc0: // Read input port.
post(0xd0);
break;
case 0xf0: case 0xf1: case 0xf2: case 0xf3:
case 0xf4: case 0xf5: case 0xf6: case 0xf7:
case 0xf8: case 0xf9: case 0xfa: case 0xfb:
case 0xfc: case 0xfd: case 0xfe: case 0xff:
log_.error().append("Should reset: %x", command_ & 0x0f);
if(!(command_ & 1)) {
cpu_control_->reset();
}
break;
}
command_phase_ = CommandPhase::WaitingForCommand;
}
Log::Logger<Log::Source::PCCompatible> log_;
PICs<model> &pics_;
Speaker &speaker_;
CPUControl<model> *cpu_control_ = nullptr;
uint8_t refresh_toggle_ = 0;
bool has_input_ = false;
uint8_t input_;
bool has_output_ = false;
uint8_t output_;
bool has_command_ = false;
uint8_t command_;
int write_delay_ = 0;
bool is_tested_ = false;
enum class CommandPhase {
WaitingForCommand,
WaitingForData,
} command_phase_ = CommandPhase::WaitingForCommand;
};
}

View File

@ -9,6 +9,7 @@
#include "PCCompatible.hpp"
#include "CGA.hpp"
#include "CPUControl.hpp"
#include "DMA.hpp"
#include "FloppyController.hpp"
#include "KeyboardController.hpp"
@ -292,7 +293,7 @@ public:
if constexpr (is_xt(model)) {
ppi_.write(port, uint8_t(value));
} else {
keyboard_.write(port, value);
keyboard_.write(port, uint8_t(value));
}
break;
@ -884,14 +885,15 @@ private:
segments(registers),
memory(registers, segments),
flow_controller(registers, segments),
cpu_control(registers, segments, memory),
io(pit, dma, ppi, pics, card, fdc, keyboard, rtc)
{
keyboard.set_cpu_control(&cpu_control);
reset();
}
void reset() {
registers.reset();
segments.reset();
cpu_control.reset();
}
InstructionSet::x86::Flags flags;
@ -899,6 +901,7 @@ private:
Segments<x86_model> segments;
Memory<pc_model> memory;
FlowController<pc_model> flow_controller;
CPUControl<pc_model> cpu_control;
IO<pc_model, video> io;
static constexpr auto model = processor_model(pc_model);
} context_;
@ -917,7 +920,11 @@ private:
using namespace PCCompatible;
namespace {
#ifndef NDEBUG
static constexpr bool ForceAT = true;
#else
static constexpr bool ForceAT = false;
#endif
template <Target::VideoAdaptor video>
std::unique_ptr<Machine> machine(const Target &target, const ROMMachine::ROMFetcher &rom_fetcher) {
@ -942,10 +949,10 @@ std::unique_ptr<Machine> Machine::PCCompatible(
const ROMMachine::ROMFetcher &rom_fetcher
) {
const Target *const pc_target = dynamic_cast<const Target *>(target);
using VideoAdaptor = Target::VideoAdaptor;
switch(pc_target->adaptor) {
case Target::VideoAdaptor::MDA: return machine<Target::VideoAdaptor::MDA>(*pc_target, rom_fetcher);
case Target::VideoAdaptor::CGA: return machine<Target::VideoAdaptor::CGA>(*pc_target, rom_fetcher);
case VideoAdaptor::MDA: return machine<VideoAdaptor::MDA>(*pc_target, rom_fetcher);
case VideoAdaptor::CGA: return machine<VideoAdaptor::CGA>(*pc_target, rom_fetcher);
default: return nullptr;
}
}

View File

@ -12,6 +12,9 @@
namespace PCCompatible {
/*!
Implements enough of the MC146818 to satisfy those BIOSes I've tested.
*/
class RTC {
public:
template <int address>

View File

@ -9,6 +9,7 @@
#pragma once
#include "InstructionSets/x86/Model.hpp"
#include "Numeric/RegisterSizes.hpp"
namespace PCCompatible {

View File

@ -1457,6 +1457,7 @@
4B1FBE222D7B739700BAC888 /* FloppyController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = FloppyController.hpp; sourceTree = "<group>"; };
4B1FBE232D7B73C800BAC888 /* Speaker.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Speaker.hpp; sourceTree = "<group>"; };
4B1FBE242D7B753000BAC888 /* KeyboardController.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = KeyboardController.hpp; sourceTree = "<group>"; };
4B1FBE252D7C0B2200BAC888 /* CPUControl.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = CPUControl.hpp; sourceTree = "<group>"; };
4B2005402B804AA300420C5C /* OperationMapper.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = OperationMapper.hpp; sourceTree = "<group>"; };
4B2005422B804D6400420C5C /* ARMDecoderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ARMDecoderTests.mm; sourceTree = "<group>"; };
4B2005462B8BD7A500420C5C /* Registers.hpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.h; path = Registers.hpp; sourceTree = "<group>"; };
@ -2560,6 +2561,7 @@
children = (
425739372B051EA800B7D1E4 /* PCCompatible.cpp */,
429B13632B20234B006BB4CB /* CGA.hpp */,
4B1FBE252D7C0B2200BAC888 /* CPUControl.hpp */,
4267A9C92B0D4F17008A59BB /* DMA.hpp */,
4B1FBE222D7B739700BAC888 /* FloppyController.hpp */,
4B1FBE242D7B753000BAC888 /* KeyboardController.hpp */,

View File

@ -10,6 +10,8 @@
#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <string>
namespace Log {
// TODO: if adopting C++20, std::format would be a better model to apply below.
@ -88,7 +90,7 @@ constexpr bool is_enabled(const Source source) {
}
}
constexpr const char *prefix(Source source) {
constexpr const char *prefix(const Source source) {
switch(source) {
default: return nullptr;
@ -137,41 +139,56 @@ constexpr const char *prefix(Source source) {
}
}
template <Source source, bool enabled>
struct LogLine;
template <Source source>
struct LogLine<source, true> {
public:
explicit LogLine(FILE *const stream) noexcept : stream_(stream) {
const auto source_prefix = prefix(source);
if(!source_prefix) return;
output_.resize(strlen(source_prefix) + 4);
std::snprintf(output_.data(), output_.size(), "[%s] ", source_prefix);
output_.pop_back();
}
~LogLine() {
fprintf(stream_, "%s\n", output_.c_str());
}
template <size_t size, typename... Args>
void append(const char (&format)[size], Args... args) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat"
const auto append_size = std::snprintf(nullptr, 0, format, args...);
const auto end = output_.size();
output_.resize(output_.size() + size_t(append_size) + 1);
std::snprintf(output_.data() + end, size_t(append_size) + 1, format, args...);
output_.pop_back();
#pragma GCC diagnostic pop
}
private:
std::string output_;
FILE *stream_;
};
template <Source source>
struct LogLine<source, false> {
explicit LogLine(FILE *) noexcept {}
template <size_t size, typename... Args>
void append(const char (&)[size], Args...) {}
};
template <Source source>
class Logger {
public:
static constexpr bool enabled = is_enabled(source);
struct LogLine {
public:
LogLine(FILE *const stream) : stream_(stream) {
if constexpr (!enabled) return;
const auto source_prefix = prefix(source);
if(source_prefix) {
fprintf(stream_, "[%s] ", source_prefix);
}
}
~LogLine() {
if constexpr (!enabled) return;
fprintf(stream_, "\n");
}
void append(const char *const format, ...) {
if constexpr (!enabled) return;
va_list args;
va_start(args, format);
vfprintf(stream_, format, args);
va_end(args);
}
private:
FILE *stream_;
};
LogLine info() { return LogLine(stdout); }
LogLine error() { return LogLine(stderr); }
LogLine<source, enabled> info() { return LogLine<source, enabled>(stdout); }
LogLine<source, enabled> error() { return LogLine<source, enabled>(stderr); }
};
}