Quantcast
Channel: Raspberry Pi Forums
Viewing all articles
Browse latest Browse all 7915

General • Macintosh Classic CRT

$
0
0
Hi ;) ,

For the past few days, I have been working on driving a Macintosh Classic display using an RP2040.
I am now able to display a checkerboard pattern, but it only works correctly if I start from address 1 of the framebuffer array, which likely indicates a memory alignment or synchronization issue. What do you think about my interpretation of the Macintosh CRT timing?
Also, do you have any insight into the framebuffer issue, particularly why the display only works correctly when starting from address 1?

The long-term goal is to integrate this code into a larger project: a Macintosh emulator called pico-mac (https://github.com/evansm7/pico-mac/tree/main)

Below is the code used, including:

Code:

#include <stdio.h>#include <string.h>#include <stdlib.h>#include "pico/stdlib.h"#include "hardware/pio.h"#include "hardware/dma.h"#include "hardware/clocks.h"// PIO#include "hsync.pio.h"#include "vsync.pio.h"#include "data.pio.h"// Mac Classic CRT specifications (base de la zone active)#define VIDEO_WIDTH     512#define VIDEO_HEIGHT    342// Macintosh Classic CRT timing constants (adjusted for corrected hsync timing)#define H_ACTIVE   575    // (active + frontporch - 1) = (512 + 12) - 1 = 523? but ignored#define V_ACTIVE   341    // (active - 1) = 342 - 1#define DATA_ACTIVE 15    // (horizontal active in words) - 1 = (512/32) - 1 = 15#define TXCOUNT (VIDEO_WIDTH * VIDEO_HEIGHT / 32) // Total number of 32-bit words// Frame buffer - 512x342 monochrome (1 bit per pixel, packed into 32-bit words)uint32_t framebuffer[(VIDEO_WIDTH * VIDEO_HEIGHT / 32)];char * address_pointer = (char *) &framebuffer[1];// Give the I/O pins that we're using some names that make sense#define HSYNC     21#define VSYNC     19#define DATA_PIN   18// Function prototypesvoid draw_pixel(uint16_t x, uint16_t y, bool color);void draw_line(int x0, int y0, int x1, int y1, bool color);void fill_framebuffer(bool white);void draw_calibration_pattern(void);void draw_checkerboard(uint8_t square_size);// A function for drawing a pixel with a specified color.// Note that because information is passed to the PIO state machines through// a DMA channel, we only need to modify the contents of the array and the// pixels will be automatically updated on the screen.// Simplified version using one-dimensional framebuffer.// Color semantics: 0 = white, 1 = black (as per task requirement).void draw_pixel(uint16_t x, uint16_t y, bool color) {    if (x >= VIDEO_WIDTH || y >= VIDEO_HEIGHT)        return;    uint32_t bit = y * VIDEO_WIDTH + x;    uint32_t index = bit >> 5;           // /32    uint32_t mask  = 1u << (31 - (bit & 31));    if (index >= (VIDEO_WIDTH * VIDEO_HEIGHT / 32)) {        printf("draw_pixel: index out of bounds!\n");        return;    }    if (color)        framebuffer[index] &= ~mask;    else        framebuffer[index] |= mask;}// Alias for backward compatibility (optional)#define drawPixel draw_pixel// Draw a line using Bresenham's algorithmvoid draw_line(int x0, int y0, int x1, int y1, bool color) {    int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;    int dy = -abs(y1 - y0), sy = y0 < y1 ? 1 : -1;    int err = dx + dy, e2;    while (1) {        drawPixel(x0, y0, color);        if (x0 == x1 && y0 == y1) break;        e2 = 2 * err;        if (e2 >= dy) { err += dy; x0 += sx; }        if (e2 <= dx) { err += dx; y0 += sy; }    }}// Generate a calibration pattern with a central cross and corner arrows// Includes a margin to ensure visibility on CRTs with overscanvoid draw_calibration_pattern(void) {    // Fill with black background    fill_framebuffer(false);        bool c_white = 0; // 0 = white in drawPixel logic        int w = VIDEO_WIDTH;    int h = VIDEO_HEIGHT;        // Draw Border at absolute edges (to check total overscan)    draw_line(0, 0, w-1, 0, c_white);       // Top    draw_line(0, h-1, w-1, h-1, c_white);   // Bottom    draw_line(0, 0, 0, h-1, c_white);       // Left    draw_line(w-1, 0, w-1, h-1, c_white);   // Right    // Margin for arrows (ensure they are visible inside overscan)    int margin = 20;         int cx = w / 2;    int cy = h / 2;        int min_x = margin;    int min_y = margin;    int max_x = w - 1 - margin;    int max_y = h - 1 - margin;        // Central Cross (spanning 100 pixels)    int cross_size = 50;    draw_line(cx - cross_size, cy, cx + cross_size, cy, c_white);    draw_line(cx, cy - cross_size, cx, cy + cross_size, c_white);        // Arrows in corners (inset by margin)    int arrow_len = 30;    int head_len = 10;        // Top-Left    draw_line(min_x + arrow_len, min_y + arrow_len, min_x, min_y, c_white);    draw_line(min_x, min_y, min_x + head_len, min_y, c_white);    draw_line(min_x, min_y, min_x, min_y + head_len, c_white);        // Top-Right    draw_line(max_x - arrow_len, min_y + arrow_len, max_x, min_y, c_white);    draw_line(max_x, min_y, max_x - head_len, min_y, c_white);    draw_line(max_x, min_y, max_x, min_y + head_len, c_white);        // Bottom-Left    draw_line(min_x + arrow_len, max_y - arrow_len, min_x, max_y, c_white);    draw_line(min_x, max_y, min_x + head_len, max_y, c_white);    draw_line(min_x, max_y, min_x, max_y - head_len, c_white);        // Bottom-Right    draw_line(max_x - arrow_len, max_y - arrow_len, max_x, max_y, c_white);    draw_line(max_x, max_y, max_x - head_len, max_y, c_white);    draw_line(max_x, max_y, max_x, max_y - head_len, c_white);}// Generate a checkerboard pattern using drawPixel.// square_size: size of each checkerboard square in pixels (default 8).void draw_checkerboard(uint8_t square_size) {    for (uint16_t y = 0; y < VIDEO_HEIGHT; y++) {        for (uint16_t x = 0; x < VIDEO_WIDTH; x++) {            // To match `draw_pixel` (0=white, 1=black), we pass `value` directly.            bool value = ((x / square_size) ^ (y / square_size)) & 1;            drawPixel(x, y, value);        }    }}// Fill entire framebuffer with solid color (0 = black, 1 = white)void fill_framebuffer(bool white) {    uint32_t word_value = white ? 0xFFFFFFFF : 0x00000000;    for (int i = 0; i < (VIDEO_WIDTH * VIDEO_HEIGHT / 32); i++) {        framebuffer[i] = word_value;    }}void mac_crt_generate_pattern(void) {    // Generate checkerboard test pattern using drawPixel (8x8 squares)    draw_checkerboard(8);}int main() {    stdio_init_all();    sleep_ms(2000);     printf("Manual GPIO test for video pin.\n");    set_sys_clock_khz(133*1000, true);    // Original VGA driver code    // Choose which PIO instance to use (there are two instances, each with 4 state machines)    PIO pio = pio0;    // Our assembled program needs to be loaded into this PIO's instruction    // memory. This SDK function will find a location (offset) in the    // instruction memory where there is enough space for our program. We need    // to remember these locations!    //    // We only have 32 instructions to spend! If the PIO programs contain more than    // 32 instructions, then an error message will get thrown at these lines of code.    //    // The program name comes from the .program part of the pio file    // and is of the form <program name_program>    uint hsync_offset = pio_add_program(pio, &hsync_program);    uint vsync_offset = pio_add_program(pio, &vsync_program);    uint data_offset = pio_add_program(pio, &data_program);    // Manually select a few state machines from pio instance pio0.    uint hsync_sm = 0;    uint vsync_sm = 1;    uint data_sm = 2;    // Call the initialization functions that are defined within each PIO file.    // Why not create these programs here? By putting the initialization function in    // the pio file, then all information about how to use/setup that state machine    // is consolidated in one place. Here in the C, we then just import and use it.    hsync_program_init(pio, hsync_sm, hsync_offset, HSYNC);    vsync_program_init(pio, vsync_sm, vsync_offset, VSYNC);    data_program_init(pio, data_sm, data_offset, DATA_PIN);    /////////////////////////////////////////////////////////////////////////////////////////////////////    // ===========================-== DMA Data Channels =================================================    /////////////////////////////////////////////////////////////////////////////////////////////////////    // DMA channels - 0 sends color data, 1 reconfigures and restarts 0    int data_chan_0 = 0;    int data_chan_1 = 1;    // Channel Zero (sends data to PIO CRT machine)    dma_channel_config c0 = dma_channel_get_default_config(data_chan_0);  // default configs    channel_config_set_transfer_data_size(&c0, DMA_SIZE_32);             // 32-bit txfers    channel_config_set_read_increment(&c0, true);                        // yes read incrementing    channel_config_set_write_increment(&c0, false);                      // no write incrementing    channel_config_set_dreq(&c0, DREQ_PIO0_TX2) ;                        // DREQ_PIO0_TX2 pacing (FIFO)    channel_config_set_chain_to(&c0, data_chan_1);                        // chain to other channel    dma_channel_configure(        data_chan_0,                 // Channel to be configured        &c0,                        // The configuration we just created        &pio->txf[data_sm],          // write address (DATA PIO TX FIFO)        &framebuffer[1],                // The initial read address (pixel monochrome framebuffer)        TXCOUNT,                    // Number of transfers; in this case each is 1 byte.        false                       // Don't start immediately.    );    // Channel One (reconfigures the first channel)    dma_channel_config c1 = dma_channel_get_default_config(data_chan_1);   // default configs    channel_config_set_transfer_data_size(&c1, DMA_SIZE_32);              // 32-bit txfers    channel_config_set_read_increment(&c1, false);                        // no read incrementing    channel_config_set_write_increment(&c1, false);                       // no write incrementing    channel_config_set_chain_to(&c1, data_chan_0);                         // chain to other channel    dma_channel_configure(        data_chan_1,                         // Channel to be configured        &c1,                                // The configuration we just created        &dma_hw->ch[data_chan_0].read_addr,  // Write address (channel 0 read address)        &address_pointer,                   // Read address (POINTER TO AN ADDRESS)        1,                                  // Number of transfers, in this case each is 4 byte        false                               // Don't start immediately.    );    /////////////////////////////////////////////////////////////////////////////////////////////////////    /////////////////////////////////////////////////////////////////////////////////////////////////////    // Initialize PIO state machine counters. This passes the information to the state machines    // that they retrieve in the first 'pull' instructions, before the .wrap_target directive    // in the assembly. Each uses these values to initialize some counting registers.    pio_sm_put_blocking(pio, hsync_sm, H_ACTIVE);    pio_sm_put_blocking(pio, vsync_sm, V_ACTIVE);    pio_sm_put_blocking(pio, data_sm, VIDEO_WIDTH - 1);    // Start the two pio machine IN SYNC    // Note that the video data state machine is running at full speed,    // so synchronization doesn't matter for that one. But, we'll    // start them all simultaneously anyway.    pio_enable_sm_mask_in_sync(pio, ((1u << hsync_sm) | (1u << vsync_sm) | (1u << data_sm)));    // Start DMA channel 0. Once started, the contents of the pixel color array    // will be continously DMA's to the PIO machines that are driving the screen.    // To change the contents of the screen, we need only change the contents    // of that array.    sleep_us(10);    dma_start_channel_mask((1u << data_chan_0)) ;    // Debug: print first few bytes of framebuffer after drawing pattern    printf("Starting VGA test patterns...\n");    // Test 4: Calibration Pattern    draw_calibration_pattern();    printf("Calibration Pattern - framebuffer sample (first 10 bytes):\n");    for (int i = 0; i < (VIDEO_WIDTH * VIDEO_HEIGHT / 32); i++) {        printf("%08x", framebuffer[i]);    }    printf("\n");    sleep_ms(5000);    /////////////////////////////////////////////////////////////////////////////////////////////////////    // ===================================== An Example =================================================    /////////////////////////////////////////////////////////////////////////////////////////////////////    //    // The remainder of this program is simply an example to show how to use the VGA system.    // This particular example just produces a diagonal array of colors on the VGA screen.    while (true) {        tight_loop_contents();        draw_checkerboard(16);        sleep_ms(16);    }}
PIO's:
HSYNC.PIO

Code:

;; HSync generation for VGA driver - Based on VHDL timing; Total line: 704 dots; Front porch: 14 dots high; Sync pulse: 288 dots low; Remaining (back porch + active): 402 dots high;.program hsync; Remove pull block.wrap_target; FRONT PORCH (14 dots high)set pins, 1 [13]        ; High for 14 cycles; SYNC PULSE (288 dots low); 288 = 32 * 9set x, 8                ; 9 iterations (0-8)sync_loop:    set pins, 0 [31]    ; Low for 32 cycles    jmp x-- sync_loop; REMAINING HIGH (402 dots high); 402 = 32*12 + 18set x, 11               ; 12 iterations (0-11)high_loop:    set pins, 1 [31]    ; High for 32 cycles    jmp x-- high_loopset pins, 1 [17]        ; High for final 18 cyclesirq 0       [0]         ; Set IRQ to signal end of line.wrap% c-sdk {static inline void hsync_program_init(PIO pio, uint sm, uint offset, uint pin) {    pio_sm_config c = hsync_program_get_default_config(offset);    // Map the state machine's SET pin group to one pin    sm_config_set_set_pins(&c, pin, 1);    // Set clock division (div by 8 for 15.625 MHz state machine)    //sm_config_set_clkdiv(&c, 7.979f);    sm_config_set_clkdiv(&c, 8.0f);        pio_gpio_init(pio, pin);    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);    pio_sm_init(pio, sm, offset, &c);}%}
VSYNC.PIO

Code:

;; VSync generation for VGA driver - Based on VHDL timing; Macintosh Classic CRT vertical timing; frontporch: 0   lines; sync pulse: 4   lines; back porch: 24  lines; active for: 342 lines;; Optimized instructions;.program vsync.side_set 1 optpull block                        ; Pull from FIFO to OSR (active line count).wrap_target                      ; Program wraps to here; SYNC PULSE (4 lines) - vsync low; Remove explicit set pins, use side-set in loopset x, 3                          ; 4 iterations (0-3)sync_loop:    wait 1 irq 0   side 0         ; Wait for hsync, drive VSYNC low    jmp x-- sync_loop; BACK PORCH (24 lines) - vsync high; Remove explicit set pins, use side-set in loopset y, 23                         ; 24 iterations (0-23)backporch:    wait 1 irq 0   side 1         ; Wait for hsync, drive VSYNC high    jmp y-- backporch; ACTIVE (342 lines) - vsync high, raise IRQ 1 each linemov x, osr                        ; Copy active line count from OSR to xactive:    wait 1 irq 0   side 1         ; Wait for hsync, maintain side-set high    irq 1                         ; Signal that we're in active mode    jmp x-- active                ; Remain in active, decrementing counter.wrap% c-sdk {static inline void vsync_program_init(PIO pio, uint sm, uint offset, uint pin) {    pio_sm_config c = vsync_program_get_default_config(offset);    // Map the state machine's SET pin group to one pin    sm_config_set_set_pins(&c, pin, 1);    sm_config_set_sideset_pins(&c, pin);    // Set clock division (div by 8 for 15.625 MHz state machine, close to dot clock 15.6672 MHz)    //sm_config_set_clkdiv(&c, 7.979f);    sm_config_set_clkdiv(&c, 8.0f);    // Set this pin's GPIO function (connect PIO to the pad)    pio_gpio_init(pio, pin);        // Set the pin direction to output at the PIO    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);    // Load our configuration, and jump to the start of the program    pio_sm_init(pio, sm, offset, &c);}%}
Data.pio

Code:

;; Video generation for Macintosh Classic CRT (monochrome); Dot clock ~15.6672 MHz. ; PIO runs at 125 MHz.;; Logic based on VHDL reference:; Total 704 dots. Blanking 192 dots. Active 512 dots.; HSYNC 14 to 302 (288 dots).; Video starts at 192 (Overlaps HSYNC!).;; 1. Wait for IRQ 1 (Line Start from VSYNC); 2. Delay for 192 dots (Blanking).;    192 dots * 8 cycles/dot = 1536 cycles.;    Loop 24 * 64 = 1536 cycles.; 3. Output 512 pixels.;    8 cycles/pixel (Standard 15.6 MHz).;.program data    ; Initialize: Get pixel count (511) from FIFO into ISR    pull block    mov isr, osr.wrap_target        wait 1 irq 1        ;         ; Delay 1536 cycles    ; Nested loop: 1 * 24 * 64 = 1536    set x, 0            ; 1 iteration (0-0)outer_delay:    set y, 23           ; 24 iterations (0-23)inner_delay:    nop [31]    jmp y-- inner_delay [31] ; 64 cycles    jmp x-- outer_delay      ; Jump back to reload Y        ; Reload pixel count (511)    mov x, isr    ; PIXEL OUTPUT: 512 pixels    ; 8 cycles per pixel (Square pixels)    ; out [6] = 7 cycles. jmp = 1 cycle. Total 8 cycles.pixel_loop:    out pins, 1 [6]    jmp x-- pixel_loop    ; Ensure output is black after active video    set pins, 0.wrap% c-sdk {static inline void data_program_init(PIO pio, uint sm, uint offset, uint pin) {    pio_sm_config c = data_program_get_default_config(offset);    // Map the state machine's SET and OUT pin group to one pin    sm_config_set_set_pins(&c, pin, 1);    sm_config_set_out_pins(&c, pin, 1);    // Configure out shift to shift LEFT (MSB first)    // "shift_right"=false (Shift Left)    sm_config_set_out_shift(&c, false, true, 32);    // Set clock division to 1.0 (125 MHz)    sm_config_set_clkdiv(&c, 1.0f);        pio_gpio_init(pio, pin);    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);    pio_sm_init(pio, sm, offset, &c);}%}
And the Macintosh CRT timing, which appears to be respected: Image

Statistics: Posted by TheKiwil — Sat Dec 27, 2025 2:48 pm



Viewing all articles
Browse latest Browse all 7915

Trending Articles