One True Sprite Proposal

Here is my second proposal for the authors of GIME FPGA/CPLD devices: [https://tlindner.macmess.org/?p=1103]

As a reminder the first one was: [https://tlindner.macmess.org/?p=977]

This proposes adding a single hardware graphic cursor avaiable in both text and graphics modes. An emulated example is avaiable in this Mame feature branch in my fork: https://github.com/tlindner/mame/tree/ots

Here is an example program in BASIC to demo the feature:

10 WIDTH 40
15 ATTR 4,3
17 CLS
20 POKE &HFF97,2                 ' SYNC=1: begin shape upload
30 FOR X=0 TO 15
40 READ A
50 POKE &HFF96,A
60 NEXT X                        ' after 16 bytes, $FF96 auto-reverts to position mode
70 CTRL = &HB1                   ' EN=1, FG=palette1, BG=palette2, SIZE=1, (bit 2 now reserved)
80 POKE &HFF97,CTRL              ' one-time setup - no need to repeat per move
81 ? "Hello World!"
86 PALETTE 1,RND(63)
87 PALETTE 2,RND(63)
88 X = JOYSTK(0)*9:LOCATE0,0
90 POKE &HFF96,X/255             ' X high bits (0-3)
100 POKE &HFF96,X AND 255        ' X low byte - smooth left/right
110 POKE &HFF96,JOYSTK(1)*3      ' Y position
120 GOTO 85
1000 DATA &H80,&H80,&H40,&H80,&H60,&H80,&H70,&H80,&H78
1010 DATA &H80,&H7C,&H80,&H7E,&H80,&H00,&HFF

And here is a minty fresh quicktime showing it off:

One True Sprite

GIME Hardware Cursor Extension Proposal

Overview

The GIME video system can provide a hardware mouse cursor by adding a small independent cursor overlay. The cursor is not stored in normal CoCo memory and does not require software to modify the display memory.

The cursor is drawn after the normal video image is generated. Software only needs to provide:

  • A cursor shape.
  • A cursor position.
  • Two palette selections.
  • Enable/disable control.

This provides a fast, flicker-free cursor suitable for graphical user interfaces while requiring very little programming overhead.

The hardware cursor is especially useful for GUI applications because moving the mouse pointer no longer requires saving the background, drawing the cursor, restoring the background, or modifying screen memory.


Registers

The cursor uses two memory-mapped registers.

Address Name Description
$FF96 Cursor Data Cursor shape upload or cursor position data
$FF97 Cursor Control Cursor enable, colors, and data mode control

Cursor Control Register ($FF97)

Bit  7   6   5   4   3   2   1   0
    EN  W1  W0  B1  B0  --  SYNC SIZE

Bit 7: EN – Cursor Enable

0 = Cursor disabled
1 = Cursor displayed

Bits 6-5: W1:W0 – Foreground Palette Slot

Selects the GIME palette entry used for foreground cursor pixels.

Valid values:

00 = palette entry 0
01 = palette entry 1
10 = palette entry 2
11 = palette entry 3

Bits 4-3: B1:B0 – Background Palette Slot

Selects the GIME palette entry used for background cursor pixels.

Valid values:

00 = palette entry 0
01 = palette entry 1
10 = palette entry 2
11 = palette entry 3

Bit 2: Reserved

Unused. Reads back as 0. Reserved for future expansion.


Bit 1: SYNC – Enter Shape Upload Mode / Upload Status

SYNC is both readable and writable, and its meaning differs by direction.

Writing SYNC

Writing a one switches the Cursor Data register ($FF96) into shape-upload mode for the next sixteen writes, and resets the upload pointer to zero.

Write 0 = no action
Write 1 = begin (or restart) a 16-byte cursor shape upload

This is the only way $FF96 enters shape-upload mode. At all other times – including immediately after power-on, and immediately after the 16th shape byte is written – $FF96 is in position mode (see below). Software never needs to write SYNC to move the cursor; it is only needed when replacing the cursor’s image.

Reading SYNC

0 = A shape upload is in progress and not yet at byte zero
1 = $FF96 is ready for a fresh shape upload

In practice this reads as 1 essentially all the time, since $FF96 sits in position mode – upload pointer parked at zero – except for the brief window during an actual upload.


Position Mode (default state of $FF96)

Unlike shape upload, position mode is not something software enters – it is where $FF96 always is unless a shape upload is in progress. Every write to $FF96 while in position mode is consumed by a free-running, self-aligning 3-byte cycle:

Byte 0: Cursor X high bits
Byte 1: Cursor X low byte
Byte 2: Cursor Y position

…then it repeats: byte 3 is X high again, byte 4 is X low, and so on. No control-register write is needed to start, continue, or reset this cycle – software can simply POKE three bytes per cursor move, forever, with zero per-move overhead beyond the position data itself. This is the expected hot path for mouse polling.

The cursor position range is:

X: 0-639
Y: 0-199

The X high byte only uses the lower two bits:

Bit 1-0 = X bits 9-8

If a shape upload (triggered by SYNC) interrupts a position write partway through its 3-byte cycle, the position pointer is reset to zero as part of entering upload mode, and reset to zero again as part of leaving it. This guarantees the next 3 writes to $FF96 after an upload finishes are always a clean, aligned position triple – a position write can never be left half-consumed by an upload landing in the middle of it.


Bit 0: SIZE – Cursor Size

Selects cursor rendering size.

0 = 8x8 cursor
1 = 16x16 cursor

When 16×16 mode is selected, each cursor pixel is doubled horizontally and vertically.

The cursor image memory remains the same 8×8 image in both modes.


Cursor Shape Data Register ($FF96)

The cursor image is an 8×8 two-bit-per-pixel image.

The image requires sixteen bytes.

Each row uses two bytes:

Byte 0: Plane 0, Row 0
Byte 1: Plane 1, Row 0

Byte 2: Plane 0, Row 1
Byte 3: Plane 1, Row 1

...

Byte 14: Plane 0, Row 7
Byte 15: Plane 1, Row 7

Bits are arranged:

Bit 7 = leftmost pixel
Bit 0 = rightmost pixel

Each pixel is decoded as:

Plane 1 Plane 0 Result
0 0 Transparent — underlying pixel unchanged
0 1 Background palette color
1 0 Foreground palette color
1 1 XOR — underlying pixel value inverted

Clarification: XOR Mode Semantics

The original pixel decode table left the “Foreground XOR Background” result ambiguous — it could be read as either a fixed third color (foreground palette color combined with background palette color), or as a true XOR against whatever is already on screen. Only the latter reading satisfies the stated goal that “the cursor color remains visible over changing backgrounds,” so the specification is clarified as follows:

XOR pixel behavior

For pixels where both bitplanes are set, the cursor does not draw a color at all. Instead, it inverts whatever pixel is already present at that screen location: output_pixel = existing_pixel XOR $FFFFFF This is the same trick used by hardware cursors on other classic platforms (and by early VGA/Mac text cursors): because the operation is applied to the live framebuffer contents rather than to a fixed cursor color, the cursor pixel is guaranteed to differ from its background regardless of what that background is — solid color, photo, or another rapidly animating sprite. Inverting twice restores the original pixel, so no background save/restore buffer is required even in XOR regions.

Foreground and background palette slots (W1:W0 and B1:B0) have no effect on XOR pixels — those bits only apply to the solid-color (10/01) cases. XOR pixels are purely a function of the two bitplane bits and the framebuffer they land on.

Practical implication for cursor art

Because XOR pixels invert rather than paint, they read differently depending on the underlying pixel’s luminance and hue — a common tradeoff for XOR cursors. Designers using XOR pixels (e.g., a thin outline meant to “always show”) should expect variable, not fixed, apparent color, and treat it as a contrast trick rather than a color choice.


Uploading a Cursor Shape

A cursor shape is uploaded by:

  1. Setting SYNC.
  2. Writing sixteen bytes to Cursor Data.

After the sixteenth byte, $FF96 automatically reverts to position mode – no further register write is needed before moving the cursor again.

Example 6809 code:

CURSOR_CTRL  EQU $FF97
CURSOR_DATA  EQU $FF96

load_cursor:
        lda   CURSOR_CTRL
        ora   #%00000010      ; SYNC - begin shape upload
        sta   CURSOR_CTRL

        ldx   #cursor_shape
        ldb   #16

upload_loop:
        lda   ,x+
        sta   CURSOR_DATA
        decb
        bne   upload_loop
                               ; $FF96 is back in position mode here
        rts

Moving the Cursor

Cursor position is the default behavior of Cursor Data – no control register write is needed at all. Software simply writes three bytes, high-byte-first, matching the 6809’s natural big-endian storage order:

CURSOR_DATA  EQU $FF96

set_cursor_position:
        lda   cursor_x         ; X high bits (bits 9-8)
        anda  #%00000011
        sta   CURSOR_DATA

        lda   cursor_x+1       ; X low byte
        sta   CURSOR_DATA

        lda   cursor_y
        sta   CURSOR_DATA

        rts

Because this is the default mode of the register, the same three writes can be issued every frame from a mouse-polling loop with no per-move setup cost. A shape upload landing between two calls to this routine cannot desynchronize it – see SYNC above.


Example Cursor Shape: Black Arrow with White Outline

The cursor palette selections can be used to create a traditional high-contrast pointer.

For this example:

  • Background palette slot = black
  • Foreground palette slot = white
  • Transparent pixels leave the underlying display unchanged.

The cursor image:

W.......
WB......
WBB.....
WBBB....
WBBBB...
WBBBBB..
WBBBBBB.
WWWWWWWW

Where:

  • . = transparent
  • B = solid black pixel (background palette color)
  • W = white outline pixel (foreground palette color)

The cursor hotspot is always the top-left corner of the 8×8 cursor image. In this example, the active pointing location is the upper-left W pixel. Software positions the cursor by writing the desired screen coordinate of this corner.

The two bitplanes are:

Plane 0 (background/black pixels)

Row 0: 10000000  $80
Row 1: 01000000  $40
Row 2: 01100000  $60
Row 3: 01110000  $70
Row 4: 01111000  $78
Row 5: 01111100  $7C
Row 6: 01111110  $7E
Row 7: 00000000  $00

Plane 1 (foreground/white outline pixels)

Row 0: 10000000  $80
Row 1: 10000000  $80
Row 2: 10000000  $80
Row 3: 10000000  $80
Row 4: 10000000  $80
Row 5: 10000000  $80
Row 6: 10000000  $80
Row 7: 11111111  $FF

The cursor upload data is interleaved by row:

cursor_arrow:
        ; Plane 0, Plane 1

        fcb $80,$80
        fcb $40,$80
        fcb $60,$80
        fcb $70,$80
        fcb $78,$80
        fcb $7C,$80
        fcb $7E,$80
        fcb $00,$FF

When the SIZE bit is enabled, each cursor pixel is doubled horizontally and vertically, producing a 16×16 cursor while preserving the same top-left hotspot location.


Crosshair Cursor

Useful for drawing programs:

...X....
...X....
...X....
XXXXXXXX
...X....
...X....
...X....
...X....

Box Cursor

Useful for text selection:

XXXXXXXX
X......X
X......X
X......X
X......X
X......X
X......X
XXXXXXXX

Benefits

A hardware cursor provides several advantages:

  • GUI applications no longer need to redraw the mouse pointer.
  • Screen memory is never modified by cursor movement.
  • Cursor movement requires only three writes, with no control-register overhead – position mode is the default state of Cursor Data.
  • Cursor shape changes require only sixteen writes, plus one SYNC write to begin.
  • The cursor works in all GIME display modes.
  • No software background save/restore routines are needed.
  • Cursor updates are independent of application graphics.

The interface requires only two registers while providing a complete programmable cursor system compatible with the GIME design philosophy.

CoCo Buttons

Reading CoCo Joystick Buttons in Color BASIC

The joystick buttons on the TRS-80 Color Computer 1 (CoCo 1) are connected to the PIA (Peripheral Interface Adapter). They can be read from memory location $FF00, which is decimal 65280.

In Color BASIC, the value can be read using PEEK:

B=PEEK(65280)

The joystick buttons are active low. This means that a button that is not pressed reads as a 1, and a pressed button pulls the bit to .

The button bits are:

Bit Mask Button
0 1 Right joystick button
1 2 Left joystick button

Color BASIC supports bitwise AND, which makes checking the buttons straightforward:

10 B=PEEK(65280)
20 IF (B AND 1)=0 THEN PRINT "RIGHT BUTTON"
30 IF (B AND 2)=0 THEN PRINT "LEFT BUTTON"
40 GOTO 10

The parentheses are important. Color BASIC’s operator precedence can produce surprising results if you write:

PEEK(65280) AND 128 = 0

This is not interpreted as:

(PEEK(65280) AND 128) = 0

Instead, the comparison happens first, effectively making it:

PEEK(65280) AND (128 = 0)

Since 128 = 0 is false, the expression becomes an AND with zero, which will always produce zero.

The correct way to test a bit is to put the bit operation inside parentheses:

(PEEK(65280) AND 128) = 0

The evaluation order is:

  1. PEEK(65280) reads the hardware register.
  2. AND 128 masks the desired bit.
  3. = 0 tests whether that bit is clear.

This is a good habit for Color BASIC programming: whenever using bit masks in a comparison, put the masked expression in parentheses.

The CoCo’s joystick interface is simple, but remembering that the buttons are active low and that Color BASIC’s operator precedence is different from many modern languages will save a lot of debugging time.

Teaching BBEdit to Know Soft Wrap setting for OCR Text Files

I recently ran into a problem while processing a large batch of OCR text files.

The workflow was simple: scanned PDFs → OCR → text files → BBEdit search. The OCR output was fine structurally, with one line per paragraph, but reading it in BBEdit was unpleasant. Long lines ran off the right side of the window. Search still worked, but visual scanning was harder than it should be.

What I wanted was simple. Every OCR text file should open in BBEdit with Soft Wrap enabled.

BBEdit does not provide a setting for this at the file level. There is no xattr or modeline for it.

Where BBEdit stores document state

BBEdit stores per-file state in a property list here:

~/Library/Containers/com.barebones.bbedit/Data/Library/BBEdit/Document State.plist

This file acts like a database. Each entry is keyed by file path, and contains a dictionary of state data for that file.

The setting I cared about is:

<key>SoftWrapText</key>
<true/>

It lives inside:

StateEntries_v1
    └── file path
        └── BBEditStateData

BBEdit uses this structure to store editor state per file.

Path matching matters

My first attempt did not work correctly.

I had entries like:

1981/08_rainbow.txt

But BBEdit was using full paths like:

/Volumes/ARCHIVE/Rainbows/1981/08_rainbow.txt

BBEdit matches state strictly by path. If the key does not match exactly, it is ignored.

The fix was to always normalize paths before writing them.

Files on external volumes stay absolute. Files inside the home directory can use ~/.

In Python, this is handled with:

path = path.resolve()

Automating Soft Wrap for many files

Once the structure was correct, I wrote a script that:

  • finds all .txt files recursively
  • computes the correct BBEdit key for each file
  • sets SoftWrapText = true
  • preserves existing BBEdit metadata

The key function is:

def bbedit_key_for(path: Path) -> str:
    path = path.resolve()

    try:
        rel = path.relative_to(Path.home())
        return f"~/{rel}"
    except ValueError:
        return str(path)

Everything else is plist editing.

Result

After running this across the OCR archive:

  • all text files open with wrapping enabled
  • BBEdit search still works normally
  • no changes to the original files
  • no need to insert line breaks into OCR output

The workflow is now easier to read without affecting how search behaves.

What this is doing

BBEdit stores per-file editor state in a plist that behaves like a small database keyed by file path.

Once that is visible, it becomes possible to automate editor behavior for large sets of files instead of adjusting settings manually.

This can be extended beyond Soft Wrap to other per-file state BBEdit tracks.

Refine the SAC

Reverse Engineering the CoCo Speech/Sound Cartridge Sound Activity Circuit

The Color Computer Speech/Sound Cartridge (S/SC) was one of the more ambitious peripherals Tandy produced for the CoCo line. It packed a TMS-7040 microcontroller, an SPO256-AL2 speech synthesizer, an AY-3-8913 programmable sound generator, and 2K of RAM into a single cartridge. It was complex enough that most CoCo owners never fully understood what it was doing.

I am a contributor to the Mame emulator project, and I maintain the CoCo SSC slot device. It emulates the TMS-7040, the SPO256, and the AY-3-8913 reasonably well. But there is one small piece of the hardware I had never properly emulated: an analog circuit that generates a single status bit indicating whether the PSG is currently producing sound. The bit is active low, lives at address $FF7E bit 5, and I called it the Sound Activity Circuit.

This is the story of how I figured out what it does, measured it on real hardware, and built a proper emulation of it.

The Schematic

The Sound Activity Circuit is a small analog section hanging off the AY-3-8913’s audio output. Looking at the schematic, the signal chain goes like this:

The PSG’s analog output feeds into an op-amp stage (one section of a quad NJM2058D) through a high-pass filter formed by C9 and R6. This strips the DC component and boosts the AC audio signal. The amplified signal then goes into a precision half-wave rectifier built from D2, D1, R1, and R2. This only passes positive half-cycles, and charges a 15µF tantalum capacitor C25 through the rectifier. C25 charges quickly when audio is present, and drains slowly through R9 (56K) when audio stops. Finally, a comparator stage with hysteresis (formed by R3 and R8) watches the voltage on C25 and snaps a clean digital output when C25 crosses the threshold.

In plain terms: the circuit answers the question “is the PSG making sound right now?” with a deliberate time delay on the answer going from yes to no. C25 stays charged for a while after the PSG goes quiet, so brief silences between sounds don’t glitch the status bit.

From the RC values in the schematic, I calculated a charge time constant of about 150ms and a decay time constant of about 840ms. Those were my starting estimates.

The Initial Emulation

In Mame, I implemented this as a stacked audio filter device, cocossc_sac_device, inserted between the AY-3-8913’s output and the speaker. Every time Mame calls sound_stream_update(), the filter sees the PSG’s sample buffer, computes the RMS energy, and stores it in a small circular buffer. The sound_activity_circuit_output() function averages the circular buffer and applies a threshold to decide whether to assert the status bit.

bool cocossc_sac_device::sound_activity_circuit_output()
{
    float sum = std::accumulate(std::begin(m_rms), std::end(m_rms), 0.0f);
    float average = (sum / BUFFER_SIZE);
    return average < 0.317f;
}

This worked, in the sense that it compiled and ran. But the threshold of 0.317 was a magic number with no physical basis, and I had no idea how the decay timing related to real hardware. I was guessing.

I decided to measure the real hardware.

The Plan

The Sound Activity Circuit only responds to PSG output. The SP0256 speech chip bypasses it entirely. So the measurement is straightforward: start the PSG playing a tone, measure how long before the status bit asserts, let C25 fully charge, silence the PSG, then measure how long before the status bit deasserts.

Since I don’t own an oscilloscope (I know, I know), I decided to use the CoCo itself as the measuring instrument. At 0.89MHz, a tight polling loop runs at roughly one iteration per 11 microseconds. More than enough resolution to measure a decay time in the hundreds of milliseconds.

I wrote a small test program in 6809 assembly, wrapped in a C program using CMOC, that would:

  1. Reset the SSC to a known state
  2. Put the SSC into PSG register mode
  3. Program the AY-3-8913 for a 1KHz tone on channel A at maximum volume
  4. Count polling loop iterations until the sound activity bit asserts (charge time)
  5. Wait for C25 to fully charge
  6. Silence the PSG
  7. Count polling loop iterations until the sound activity bit deasserts (decay time)
  8. Print the results

Simple enough in theory. In practice, it took a while to get right.

Pitfall One: The Busy Bit Is Active Low. So Is Everything Else.

The status byte at $FF7E has three active-low bits:

  • Bit 7 ($80) – busy (low = busy)
  • Bit 6 ($40) – speech active (low = speaking)
  • Bit 5 ($20) – sound active (low = PSG making sound)

I initially wrote the busy-wait loop as:

bw1     lda     $ff7e
        bmi     bw1       ; wait while bit 7 set

But since busy is active low, bit 7 being set means not busy. The loop was waiting for the wrong condition. It would spin forever waiting for not-busy while the SSC was idle. The correct test is bpl (branch while plus), meaning while bit 7 is clear (busy):

bw1     lda     $ff7e
        bpl     bw1       ; wait while bit 7 clear = busy

Pitfall Two: The TMS-7040 Needs Time

The SSC uses a TMS-7040 microcontroller as an intermediary between the CoCo and all the other devices in the cartridge. When the CoCo writes a byte to $FF7E, it triggers an interrupt on the TMS-7040, which queues the byte and asserts the busy bit. When in the correct mode, the TMS-7040’s main loop then dequeues the byte and writes it to the PSG, then clears busy.

I assumed that the busy bit clearing meant the TMS-7040 was truly ready for the next byte. It is not. The busy bit clears before the microcontroller is fully ready to accept another write. If you send the next byte too quickly, it gets dropped silently.

The BASIC manual’s example program inadvertently works around this because the BASIC interpreter is slow. The overhead between a POKE and the next GOSUB is enough dead time that the TMS-7040 catches up. In tight assembly code, you have to add explicit delays.

Thru bisection I found that a delay loop of 49 iterations (at 8 cycles each, about 440 microseconds) was the minimum reliable inter-byte delay. I rounded up to 64 for safety. The WRPSG subroutine ended up looking like this:

WR2PSG
        ldx     #64
wp1lp   leax    -1,x
        bne     wp1lp
        pshs    a
bw1     lda     SSCDAT
        bpl     bw1
        puls    a
        sta     SSCDAT
WR1PSG
        ldx     #64
wp2lp   leax    -1,x
        bne     wp2lp
bw2     lda     SSCDAT
        bpl     bw2
        stb     SSCDAT
        rts

Pitfall Three: The Audio Is Not Routed Automatically

The CoCo has a sound multiplexer that selects the audio source for the internal speaker. By default it does not route cartridge audio. Before you can hear anything from the SSC, you need to configure the multiplexer:

*((unsigned char *)0xff01) = 0x34; // PIA 0 CA2 = 0 \_ Set mixer to CART
*((unsigned char *)0xff03) = 0x3f; // PIA 0 CB2 = 1 /
*((unsigned char *)0xff23) = 0x3c; // PIA 1 CB2 = 1 - Turn on sound

The BASIC example programs in the SSC manual does this as a matter of course, but it is easy to miss when writing a standalone C program. Without these writes, the PSG produces output that goes nowhere, and you hear nothing.

The Results

After working through all of those pitfalls, the test program finally ran correctly. The results from multiple runs on real hardware over a ten-minute thermal soak:

  • Charge time: 740 iterations (approximately 8ms). Rock solid across all runs and the entire thermal soak
  • Decay time: 12079-12163 iterations (approximately 133-134ms). Slowly increasing as the board warmed up, with ambient temperature affecting the value measurably. Running a fan near the CoCo during testing dropped the value by roughly 55 counts

The ratio of decay to charge is about 16:1, which is in the right ballpark compared to the schematic’s predicted 6:1. The difference is likely due to component variation in the actual capacitor and resistors on my particular board.

The most interesting finding is that the decay time is 134ms, not the 840ms predicted by the schematic. Either C25 has drifted significantly from its marked value over 40 years, or the board revision I own uses different component values than the schematic I was working from. Either way, real hardware measurements beat schematic theory every time.

The Revised Emulation

Armed with real numbers, I rewrote the Mame emulation. The original RMS circular buffer approach had several problems:

  • The window size had no relationship to the circuit’s actual time constants
  • It was sensitive to Mame’s buffer update rate
  • The AY-3-8913 emulation produces a DC offset at idle which the RMS accumulated, making the circuit appear permanently active
  • The threshold of 0.317 had no physical basis

The analog circuit is fundamentally asymmetric. C25 charges quickly through the rectifier diode but drains slowly through the resistor network. The correct digital model is an envelope follower with asymmetric attack and decay coefficients, preceded by a high-pass filter to remove the DC offset, matching the real circuit’s C9/R6 input filter.

void cocossc_sac_device::sound_stream_update(sound_stream &stream)
{
    int count = stream.samples();

    for (int sampindex = 0; sampindex < count; sampindex++)
    {
        float x = stream.get(0, sampindex);

        // High pass filter to remove DC offset (models C9/R6)
        float y = HPF_ALPHA * (m_hpf_prev_out + x - m_hpf_prev_in);
        m_hpf_prev_in = x;
        m_hpf_prev_out = y;

        // Envelope follower, asymmetric attack/decay (models C25)
        float rect = std::abs(y);
        if (rect > m_envelope)
            m_envelope += (rect - m_envelope) * ATTACK_COEFF;
        else
            m_envelope += (rect - m_envelope) * DECAY_COEFF;

        stream.put(0, sampindex, x);
    }
}

bool cocossc_sac_device::sound_activity_circuit_output()
{
    m_stream->update();

    if (m_sound_active && m_envelope < THRESH_OFF)
        m_sound_active = false;
    else if (m_envelope > THRESH_ON)
        m_sound_active = true;

    return !m_sound_active;  // active low
}

The attack and decay coefficients were derived from the hardware measurements and tuned against the emulated system:

static constexpr float HPF_ALPHA    = 0.99f;
static constexpr float ATTACK_COEFF = 0.0026f;  // models ~8ms charge time
static constexpr float DECAY_COEFF  = 0.0003f;  // models ~134ms decay time
static constexpr float THRESH_ON    = 0.05f;
static constexpr float THRESH_OFF   = 0.01f;

The envelope follower operates sample by sample, so it produces consistent results regardless of Mame’s buffer update rate or the user’s configured sample rate.

Conclusion

I have never encountered any CoCo software that actually polls the sound activity bit during normal operation. This entire exercise was in the service of correctness. If the hardware has the bit, the emulator should get it right.

The process turned out to be more interesting than I expected. What looked like a simple threshold comparison in the schematic revealed a carefully designed asymmetric envelope detector. And the busy-wait protocol had a hidden timing requirement. One that is documented in the manual, but easy to miss when writing tight assembly code.

If I had an oscilloscope, the waveforms on C25 charging and draining would have made a much better illustration for this post. Maybe next time.

The source code for the test program can be found here. The pull request for Mame is here.

ToolShed Tap for Homebrew

I’ve been a Homebrew users for a long time. Very nice experience. My only complaint is using it with old operating systems is not a thing they optimize for. But with regard to ToolShed, the disk image tool I contribute to, no one has every made a recipe for it. So I put together a 3rd party tap. Follow instructions there to learn how easy it is to use. I am choosing not to try to get it mainlined because of how the project includes copyrighted code from others.

SAM/VDG Sizzler

I recently thought it would be nice to have a picture of every video mode a Color Computer could produce. So I wrote a program to make it easy to do that. Sizzle Screen Shot You can find this program over on github. I have also taken the time to make videos of the four major implementations of the CoCo SAM/VDG: <a href="https://archive.org/details/siz-cc-1"><a href="https://archive.org/details/siz-cc-1">https://archive.org/details/siz-cc-1</a></a> <a href="https://archive.org/details/siz-cc-2b_202408"><a href="https://archive.org/details/siz-cc-2b_202408">https://archive.org/details/siz-cc-2b_202408</a></a> <a href="https://archive.org/details/siz-cc-3-86"><a href="https://archive.org/details/siz-cc-3-86">https://archive.org/details/siz-cc-3-86</a></a> <a href="https://archive.org/details/siz-cc-3-87"><a href="https://archive.org/details/siz-cc-3-87">https://archive.org/details/siz-cc-3-87</a></a>

Demon Attack on a CoCo 3

Demon Attack cartridge (26-3099) has a small bug when running on a CoCo 3. The Programmer’s credit and version information is drawn using garbage rather than the correct data from the ROM.

capture1

This has to do with setting bit 3 of $FF90 causing RAM to appear at $FExx. This does not happen on a CoCo 1 or 2.

Capture

CoCo 3 PIA 2 Mirror

The second PIA has a smaller mirror in the CoCo 3 than the CoCo 1/2. It goes from $FF20 to $FF2F. The CoCo 1/2 mirror is from $FF20 to $FF3F.

Recently I was challenged to find a citation for this information. But I could not. Tepolt has this wrong. So I decided to create a test program and accompanying video to document this first hand.

Here is the test program. PIA Test Program It is a menu driven program that allows you to choose a specific mirror address for PIA 2, then allows you to choose one of three tests.

Test ‘S’ produces a square wave using the CoCo’s DAC. This DAC is connect to port A of the PIA. Test ‘B’ produces a square wave using the CoCo 1 bit sound source. This is connected to bit 1 of port B of the PIA. Test ‘V’ toggles the A/G line of the VDG. This is connected to bit 7 of port B of the PIA.

When using this program on a CoCo 3 you will find the the second PIA only responds from $FF20 to $FF2F. And the VDG test only responds to $FF20 to $FF23, because the GIME only see writes to these specific addresses.