// // SAA5050.cpp // Clock Signal // // Created by Thomas Harte on 24/09/2025. // Copyright © 2025 Thomas Harte. All rights reserved. // #include "SAA5050.hpp" #include #include 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); } } } }