1
0
mirror of https://github.com/TomHarte/CLK.git synced 2025-11-03 09:16:11 +00:00
Files
CLK/Components/SAA5050/SAA5050.cpp
2025-10-02 20:35:53 -04:00

352 lines
12 KiB
C++

//
// SAA5050.cpp
// Clock Signal
//
// Created by Thomas Harte on 24/09/2025.
// Copyright © 2025 Thomas Harte. All rights reserved.
//
#include "SAA5050.hpp"
#include <algorithm>
#include <cstdint>
namespace {
// SAA5050 font, padded out to one byte per row. The least-significant five bits of each byte
// are the meaningful pixels for that row, with the LSB being on the right.
constexpr uint8_t font[][10] = {
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, // Character 32.
{0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x0a, 0x0a, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x06, 0x09, 0x08, 0x1c, 0x08, 0x08, 0x1f, 0x00, 0x00, },
{0x00, 0x0e, 0x15, 0x14, 0x0e, 0x05, 0x15, 0x0e, 0x00, 0x00, },
{0x00, 0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03, 0x00, 0x00, },
{0x00, 0x08, 0x14, 0x14, 0x08, 0x15, 0x12, 0x0d, 0x00, 0x00, },
{0x00, 0x04, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x02, 0x04, 0x08, 0x08, 0x08, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x08, 0x04, 0x02, 0x02, 0x02, 0x04, 0x08, 0x00, 0x00, },
{0x00, 0x04, 0x15, 0x0e, 0x04, 0x0e, 0x15, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x04, 0x1f, 0x04, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x00, 0x00, 0x00, },
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x11, 0x0a, 0x04, 0x00, 0x00, },
{0x00, 0x04, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x01, 0x06, 0x08, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x06, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x02, 0x06, 0x0a, 0x12, 0x1f, 0x02, 0x02, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x1e, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x06, 0x08, 0x10, 0x1e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x0e, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x0f, 0x01, 0x02, 0x0c, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x04, 0x04, 0x08, 0x00, },
{0x00, 0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1f, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x02, 0x04, 0x04, 0x00, 0x04, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0e, 0x00, 0x00, },
{0x00, 0x04, 0x0a, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x1f, 0x10, 0x10, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x10, 0x13, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x1f, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x11, 0x1b, 0x15, 0x15, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x10, 0x10, 0x10, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0d, 0x00, 0x00, },
{0x00, 0x1e, 0x11, 0x11, 0x1e, 0x14, 0x12, 0x11, 0x00, 0x00, },
{0x00, 0x0e, 0x11, 0x10, 0x0e, 0x01, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x1f, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x11, 0x15, 0x15, 0x15, 0x0a, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x11, 0x11, 0x0a, 0x04, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x1f, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1f, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x08, 0x1f, 0x08, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x16, 0x01, 0x02, 0x04, 0x07, },
{0x00, 0x00, 0x04, 0x02, 0x1f, 0x02, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x04, 0x0e, 0x15, 0x04, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x0a, 0x0a, 0x1f, 0x0a, 0x1f, 0x0a, 0x0a, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x00, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x01, 0x0f, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x10, 0x10, 0x0f, 0x00, 0x00, },
{0x00, 0x01, 0x01, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x1f, 0x10, 0x0e, 0x00, 0x00, },
{0x00, 0x02, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
{0x00, 0x10, 0x10, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x04, 0x00, 0x0c, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x04, 0x00, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04, 0x08, },
{0x00, 0x08, 0x08, 0x09, 0x0a, 0x0c, 0x0a, 0x09, 0x00, 0x00, },
{0x00, 0x0c, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1a, 0x15, 0x15, 0x15, 0x15, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x11, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0e, 0x11, 0x11, 0x11, 0x0e, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x1e, 0x11, 0x11, 0x11, 0x1e, 0x10, 0x10, },
{0x00, 0x00, 0x00, 0x0f, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x01, },
{0x00, 0x00, 0x00, 0x0b, 0x0c, 0x08, 0x08, 0x08, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x0f, 0x10, 0x0e, 0x01, 0x1e, 0x00, 0x00, },
{0x00, 0x04, 0x04, 0x0e, 0x04, 0x04, 0x04, 0x02, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x0a, 0x0a, 0x04, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0a, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x0a, 0x04, 0x0a, 0x11, 0x00, 0x00, },
{0x00, 0x00, 0x00, 0x11, 0x11, 0x11, 0x11, 0x0f, 0x01, 0x0e, },
{0x00, 0x00, 0x00, 0x1f, 0x02, 0x04, 0x08, 0x1f, 0x00, 0x00, },
{0x00, 0x10, 0x10, 0x10, 0x10, 0x11, 0x03, 0x05, 0x07, 0x01, },
{0x00, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x0a, 0x00, 0x00, },
{0x00, 0x18, 0x04, 0x18, 0x04, 0x19, 0x03, 0x05, 0x07, 0x01, },
{0x00, 0x00, 0x04, 0x00, 0x1f, 0x00, 0x04, 0x00, 0x00, 0x00, },
{0x00, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x1f, 0x00, 0x00, },
};
enum ControlCode: uint8_t {
RedAlpha = 0x01,
GreenAlpha = 0x02,
YellowAlpha = 0x03,
BlueAlpha = 0x04,
MagentaAlpha = 0x05,
CyanAlpha = 0x06,
WhiteAlpha = 0x07,
Flash = 0x08,
Steady = 0x09,
RedGraphics = 0x11,
GreenGraphics = 0x12,
YellowGraphics = 0x13,
BlueGraphics = 0x14,
MagentaGraphics = 0x15,
CyanGraphics = 0x16,
WhiteGraphics = 0x17,
Conceal = 0x18,
ContinuousGraphics = 0x19,
SeparatedGraphics = 0x1a,
NormalHeight = 0xc,
DoubleHeight = 0xd,
BlackBackground = 0x1c,
NewBackground = 0x1d,
HoldGraphics = 0x1e,
ReleaseGraphics = 0x1f,
};}
using namespace Mullard;
void SAA5050Serialiser::begin_frame(const bool is_odd) {
line_ = -2;
row_ = 0;
odd_frame_ = is_odd;
row_has_double_height_ = false;
double_height_offset_ = 0;
++frame_counter_;
}
void SAA5050Serialiser::begin_line() {
line_ += 2;
if(line_ == 20) {
line_ = 0;
++row_;
if(row_has_double_height_) {
double_height_offset_ = (double_height_offset_ + 5) % 10;
}
row_has_double_height_ = false;
}
output_.reset();
has_output_ = false;
apply_control(ControlCode::WhiteAlpha);
apply_control(ControlCode::Steady);
apply_control(ControlCode::NormalHeight);
apply_control(ControlCode::ContinuousGraphics);
apply_control(ControlCode::BlackBackground);
apply_control(ControlCode::ReleaseGraphics);
}
bool SAA5050Serialiser::has_output() const {
return has_output_;
}
SAA5050Serialiser::Output SAA5050Serialiser::output() {
has_output_ = false;
return output_;
}
void SAA5050Serialiser::apply_control(const uint8_t value) {
const auto set_alpha = [&](const uint8_t colour) {
alpha_mode_ = true;
conceal_ = false;
output_.alpha = colour;
hold_graphics_ = false;
};
const auto set_graphics = [&](const uint8_t colour) {
alpha_mode_ = false;
conceal_ = false;
output_.alpha = colour;
hold_graphics_ = false;
};
switch(value) {
default: break;
case RedAlpha: set_alpha(0b100); break;
case GreenAlpha: set_alpha(0b010); break;
case YellowAlpha: set_alpha(0b110); break;
case BlueAlpha: set_alpha(0b001); break;
case MagentaAlpha: set_alpha(0b101); break;
case CyanAlpha: set_alpha(0b011); break;
case WhiteAlpha: set_alpha(0b111); break;
case Flash: flash_ = true; break;
case Steady: flash_ = false; break;
case RedGraphics: set_graphics(0b100); break;
case GreenGraphics: set_graphics(0b010); break;
case YellowGraphics: set_graphics(0b110); break;
case BlueGraphics: set_graphics(0b001); break;
case MagentaGraphics: set_graphics(0b101); break;
case CyanGraphics: set_graphics(0b011); break;
case WhiteGraphics: set_graphics(0b111); break;
case Conceal: conceal_ = true; break;
case ContinuousGraphics: separated_graphics_ = false; break;
case SeparatedGraphics: separated_graphics_ = true; break;
case NormalHeight: double_height_ = false; break;
case DoubleHeight: double_height_ = row_has_double_height_ = true; break;
case BlackBackground: output_.background = 0; break;
case NewBackground: output_.background = output_.alpha; break;
case HoldGraphics: hold_graphics_ = true; break;
case ReleaseGraphics: hold_graphics_ = false; last_graphic_ = 32; break;
}
}
void SAA5050Serialiser::set_reveal(const bool reveal) {
reveal_ = reveal;
}
void SAA5050Serialiser::add(const Numeric::SizedInt<7> c) {
has_output_ = true;
if(c.get() < 32) {
if(hold_graphics_) {
load_pixels(last_graphic_);
} else {
output_.reset();
}
apply_control(c.get());
return;
}
load_pixels(c.get());
}
void SAA5050Serialiser::load_pixels(const uint8_t c) {
if(flash_ && ((frame_counter_&31) > 23)) { // Complete guess on the blink period here.
output_.reset();
return;
}
if(conceal_ && !reveal_) {
output_.reset();
return;
}
// Divert into graphics only if both the mode and the character code allows it.
if(!alpha_mode_ && (c & (1 << 5))) {
last_graphic_ = c;
// Graphics layout:
//
// |----|----|
// | | |
// | b0 | b1 |
// | | |
// |----|----|
// | | |
// | b2 | b3 |
// | | |
// |----|----|
// | | |
// | b4 | b6 |
// | | |
// |----|----|
if(separated_graphics_ && (line_ == 6 || line_ == 12 || line_ == 18)) {
output_.reset();
return;
}
uint8_t pixels;
if(line_ < 6) {
pixels =
((c & 1) ? 0b111'000 : 0) |
((c & 2) ? 0b000'111 : 0);
} else if(line_ < 14) {
pixels =
((c & 4) ? 0b111'000 : 0) |
((c & 8) ? 0b000'111 : 0);
} else {
pixels =
((c & 16) ? 0b111'000 : 0) |
((c & 64) ? 0b000'111 : 0);
}
if(separated_graphics_) {
pixels &= 0b011'011;
}
output_.load(pixels);
return;
}
if(double_height_) {
const auto top_address = (line_ >> 2) + double_height_offset_;
const uint8_t top = font[c - 32][top_address];
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
if(line_ & 2) {
output_.load(bottom, top);
} else {
output_.load(top, bottom);
}
} else {
if(double_height_offset_) {
output_.reset();
} else {
const auto top_address = line_ >> 1;
const uint8_t top = font[c - 32][top_address];
const uint8_t bottom = font[c - 32][std::min(9, top_address + 1)];
if(odd_frame_) {
output_.load(bottom, top);
} else {
output_.load(top, bottom);
}
}
}
}