home.social

#zxspectrum — Public Fediverse posts

Live and recent posts from across the Fediverse tagged #zxspectrum, aggregated by home.social.

  1. This week on the blog: Some stuff I didn't get to in the ZX Spectrum tour. I've been trying to get better sound out of its 1-bit speaker and the results have been... mixed. I've still got some homework left to do but I got some *very* nice chord progressions out at least.

    bumbershootsoft.wordpress.com/

    #retrocomputing #zxspectrum

  2. I'm thinking of using this little song I made a few years ago, repurposing it for the little Oakhollow game with the little knighty, if I can figure out how to play it in-game for the Spectrum 128k models with the AY sound chip.

    #music #Spectrum #zxspectrum #speccy

  3. I'm thinking of using this little song I made a few years ago, repurposing it for the little Oakhollow game with the little knighty, if I can figure out how to play it in-game for the Spectrum 128k models with the AY sound chip.

    #music #Spectrum #zxspectrum #speccy

  4. I'm thinking of using this little song I made a few years ago, repurposing it for the little Oakhollow game with the little knighty, if I can figure out how to play it in-game for the Spectrum 128k models with the AY sound chip.

    #music #Spectrum #zxspectrum #speccy

  5. I'm thinking of using this little song I made a few years ago, repurposing it for the little Oakhollow game with the little knighty, if I can figure out how to play it in-game for the Spectrum 128k models with the AY sound chip.

    #music #Spectrum #zxspectrum #speccy

  6. I'm thinking of using this little song I made a few years ago, repurposing it for the little Oakhollow game with the little knighty, if I can figure out how to play it in-game for the Spectrum 128k models with the AY sound chip.

    #music #Spectrum #zxspectrum #speccy

  7. Footballer of the Year 2 by Ben Daglish

    ./f/FootballerOfTheYear2.ay?#0
    Title: Footballer of the Year 2
    Author: Ben Daglish
    #ZXSpectrum
    #chiptune

  8. 128K Title by Keith Tinman

    ./m/MidnightResistance.ay?#0
    Title: 128K Title
    Author: Keith Tinman
    #ZXSpectrum
    #chiptune

  9. Gary Lineker's Super Skills by Ben Daglish

    ./g/GaryLinekersSuperSkills.ay?#0
    Title: Gary Lineker's Super Skills
    Author: Ben Daglish
    #ZXSpectrum
    #chiptune

  10. Shatranj 1.2 — Connected Rooks, by Ignacio Monge.

    Shatranj now brings the same network chess game to ZX Spectrum Classic,
    Spectrum Next, Spectranext, Windows, macOS, and Linux. Any supported client can play any other through Direct TCP or MQTT.

    Download:
    github.com/IgnacioMonge/Shatra

    #zxspectrum #chess #retrogaming

  11. Lazerwheel (title) by unknown

    ./l/Lazerwheel.ay
    Title: Lazerwheel (title)
    Author: unknown
    #ZXSpectrum
    #chiptune

  12. The main difference of this CIRCLE implementation is not only that it's faster (which it is), but that it also clips to viewport without triggering an "Integer out of range" error.

    #zenpoint #zenzx #zxspectrum #speccy #emulator #golang #foss #spectrum

  13. Rasputin (128k speech) by David Lowe

    ./r/Rasputin.ay?#0
    Title: Rasputin (128k speech)
    Author: David Lowe
    #ZXSpectrum
    #chiptune

  14. ZX Spectrum Compatible Video for RC2014 – Part 3

    The ZX Spectrum video is now working satisfactorily as described in Part 2 but when I came to use it I found applications often expected to have the 50Hz video scan interrupt which was originally generated from the ULA based on the video synchronisation signals. I looked at some ways to generate this here: RC2014 50 Hz Interrupt Source but with my video card, I have a microcontroller and a couple of spare GPIO pins (just) so now I’m wondering if I can get the video card to also provide the 50 Hz sync interrupt. That is what this post is looking at.

    • Part 1 – Background information, design considerations, prior art.
    • Part 2 – PCB design for a RC2014 Spectrum Compatible video card.
    • Part 3 – Adding a 50Hz interrupt to the design and details of PCB V2.

    Back to the Scanvideo Library

    My starting point is to go back to the PIO use in the scanvideo library as some kind of autonomous timing pulse is the kind of thing PIO makes pretty easy.

    Whilst scanning through I noticed two definitions that look really interesting:

    PICO_SCANVIDEO_ENABLE_DEN_PIN
    PICO_SCANVIDEO_ENABLE_CLOCK_PIN

    These are undefined by default, but if they are set to 1 in the Makefile configuration it has the result of adding two additional GPIO to the timing PIO code in: src/rp2_common/pico_scanvideo_dpi/timing.pio which uses the PIO side step to output a CLOCK and enable pin for use.

    I believe (from setup_sm() in scanvideo.c) that the order of GPIO for timing signals is:

    GPIO base + 0H SyncGPIO base + 1V SyncGPIO base + 2DEN – “Enable”?GPIO base + 2 or +3CLOCK (follows DEN if configured)

    But there is also the following comment elsewhere in scanvideo.c, which presumably is referring to the case where CLOCK is enabled by DEN isn’t:

    const uint BASE = PICO_SCANVIDEO_SYNC_PIN_BASE; // hsync and vsync are +0 and +1, clock is +2

    I’ve not found a proper description of these latter two GPIO pins so far, but I believe the CLOCK in this case is the pixel clock. In VGA terms I think that is the clock that delineates, within the analog signal, when a new pixel is being represented.

    Apparently the original VGA standard specified a pixel clock of 25.175 MHz or 28.322 MHz. I’m not going to get into how the timings work – to be honest, I’m not totally clear myself, but from these main clocks, we can get the line and frame timings for various resolutions.

    In terms of software, the VGA mode that is used by default is defined in vga_modes.c as:

    const scanvideo_timing_t vga_timing_640x480_60_default =
    {
    .clock_freq = 24000000,

    .h_active = 640,
    .v_active = 480,

    .h_front_porch = 16,
    .h_pulse = 64,
    .h_total = 800,
    .h_sync_polarity = 1,

    .v_front_porch = 1,
    .v_pulse = 2,
    .v_total = 500,
    .v_sync_polarity = 1,

    .enable_clock = 0,
    .clock_polarity = 0,

    .enable_den = 0
    };

    There is only one VGA mode turns on the clock, so I’m not quite sure how to get all this working, but in principle I could probably use logic to divide the 24MHz clock (in the above example) down to something useful.

    But if I’m going to be adding logic, I may as well go back to doing my own thing as described in RC2014 50 Hz Interrupt Source.

    PIO and Video Timings

    At some point scanvideo will be outputting a video frame at a useful frame rate, so what is controlling the timing? Timing.pio has some clues, as you might expect:

    ; these are the values used in the out
    ; exec in video_htiming
    .program video_htiming_states
    .side_set 1
    ; state 0 = set irq 0
    irq 0 side 0
    ; state 1 = set irq 1
    irq 1 side 0
    ; state 2 = set irq 4
    irq 4 side 0
    ; state 3 = clear irq 4
    irq clear 4 side 0

    These seem to be called directly from the C code at various points using pio_encode_… direct API calls within pio_sm_exec() calls. I think most of the magic is going on in the prepare_for_active_scanline_irqs_enabled function.

    How PIO IRQs map onto actual RP2350 interrupts is a little confusing. There are 8 PIO IRQ flags which can be seen by all state machines. But there are just two actual interrupt request lines (as the CPU would recognise them) per PIO. These can be mapped onto interrupt service routines with special definitions as follows (from SDK:src/rp2350/hardware_regs/include/hardware/regs/intctrl.h):

    #define isr_dma_0 isr_irq10
    #define isr_dma_1 isr_irq11
    #define isr_dma_2 isr_irq12
    #define isr_dma_3 isr_irq13

    #define isr_pio0_0 isr_irq15
    #define isr_pio0_1 isr_irq16
    #define isr_pio1_0 isr_irq17
    #define isr_pio1_1 isr_irq18
    #define isr_pio2_0 isr_irq19
    #define isr_pio2_1 isr_irq20

    The two isr functions for each of these have the following details:

    isr_pio0_0:
    // handler for explicit PIO_IRQ0
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to start a DMA for a scanline
    // this called once per scanline during non vblank
    if (video_pio->irq & 1u) {
    ...
    }

    // handler for explicit PIO_IRQ1
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to prepare for a scanline
    // this is only called once per scanline during vblank
    if (video_pio->irq & 2u) {
    ...
    }

    isr_pio0_1:
    top_up_timing_pio_fifo();

    isr_dma_0:
    // DMA complete
    scanline_dma_complete_irqs_enabled();

    So both of these appear to be something to do with the timing state machine, which is SM3. These are enabled from the function scanvideo_timing_enable():

    pio_set_irq0_source_mask_enabled(video_pio,
    (1u << pis_interrupt0) // SM IRQ flag 0 int
    | (1u << pis_interrupt1), // SM IRQ flag 1 int
    true);
    pio_set_irq1_source_enabled(video_pio,
    pis_sm0_tx_fifo_not_full
    + PICO_SCANVIDEO_TIMING_SM,
    true);
    irq_set_mask_enabled((1u << PIO0_IRQ_0)
    | (1u << PIO0_IRQ_1)
    #if !PICO_SCANVIDEO_NO_DMA_TRACKING
    | (1u << DMA_IRQ_0)
    #endif
    , enable);

    Confusingly, when the definition of pis_inerrupt0 is looked up, we find it defined as follows:

    pis_interrupt0 = PIO_INTR_SM0_LSB,
    ///< PIO interrupt 0 is raised

    But then we can see that SM0 is not state machine 0, but refers to fields in the IRQ0_INTE and IRQ1_INTE registers, which include SM0 through to SM7. These are the 8 IRQ flags available to all state machines (although actually only SM0-SM3 can be mapped on an RP2040 – all 8 are available on RP2350).

    IRQ 1 is mapped as follows:

    pis_sm0_tx_fifo_not_full = PIO_INTR_SM0_TXNFULL_LSB,
    ///< State machine 0 TX FIFO is not full

    Which is another definition for the PIO interrupt registers. But this time I think it does map onto a state machine itself, hence when initialised it is offset by the TIMING_SM value (which is 3).

    I find this kind of thing a lot when working through the RP SDKs and such confusing inconsistencies are really draining when trying to figure out what is going on in largely undocumented code…

    Anyway, back to those four PIO states for IRQ handling. They map over into the C world using the follownig.

    // 4 possible instructions; index into program below
    enum {
    SET_IRQ_0 = 0u,
    SET_IRQ_1 = 1u,
    SET_IRQ_SCANLINE = 2u,
    CLEAR_IRQ_SCANLINE = 3u,
    };

    And here is an example of it being used:

    // clear scanline irq
    pio_sm_exec(
    video_pio, PICO_SCANVIDEO_TIMING_SM,
    video_htiming_states_program.instructions[CLEAR_IRQ_SCANLINE]
    );

    But that is the only one that seems to be used directly. Most instances of their use seem to be via the timing_encode() macro and a series of redefinitions for _CMD sequences as follows:

    #define TIMING_CYCLE 3u
    #define timing_encode(state, length, pins) \
    ((video_htiming_states_program.instructions[state] & side_set_mask) \
    | (((uint32_t)(length) - TIMING_CYCLE) << 16u) \
    | ((uint32_t)(pins) << 29u))

    #define A_CMD SET_IRQ_0
    #define A_CMD_VBLANK SET_IRQ_1
    #define B1_CMD CLEAR_IRQ_SCANLINE
    #define B2_CMD CLEAR_IRQ_SCANLINE
    #define C_CMD SET_IRQ_SCANLINE
    #define C_CMD_VBLANK CLEAR_IRQ_SCANLINE

    These are then written into the timing programme based on the video timings provided in the various configuration structures.

    Why do I care about all this? In fact why this rather significant diversion in the first place? Well, I’m hoping I can see where the core video frame timing happens to see if I can piggy back on that for my 50Hz timing signal.

    But, at the present time, I’m failing to see it. There appear to be several asynchronous cycles going on passing instructions across to the PIO via the FIFOs or DMA. I think all the intelligence I want to hook into is not happening in real time, but by queuing up the instructions for PIO to run in a time sensitive manner.

    So it may be possible to hook into this somehow for a timing signal (I was hoping I might be able to wait on the IRQs flying around), but at present, I don’t see how to do that.

    Added to the fact I’m not totally sure it is 50Hz anyway (aren’t VGA timings configurable?) then this is probably not going to get me any further.

    Independent PIO 50Hz Generator

    What I’d really like to do is have a PIO state machine configured with the same 5uS pulse every 20mS that I managed to implement in logic. It would be nice to synchronise this to the video scan, but not essential.

    If I make this a pulse HIGH then it can go through a 74HCT14 inverter to invert the signal, buffer the signal, and also switch from the RP2350’s 3V3 IO to a 5V signal. This requires powering the 74HCT14 from the RC bus 5V and not the RP2350 PGA signals.

    From Simplified Pico VGA I noted that the scanvideo library is using PIO 0 with the following state machines:

    • 0 – scanline state machine.
    • 1, 2 – optional additional video planes (if configured).
    • 3 – timing state machine.

    As each PIO only has four state machines, these would appear to all be in use. Except I’m only using a single “video plane” as far as I know, so I might try and hijack SM2 and see how it goes.

    As each state machine has its own clock divisor, I should be able to configure it to run at something useful. If I want a pulse, the simplest way to implement it would be as follows (see “Hello PIO” from PIO on the Raspberry Pi Pico – Part 2):

    .wrap_target
    set pins, 1 // 1 cycle
    set pins, 0 [30] // 31 cycles
    .wrap

    This has the GPIO HIGH for 1 cycle and LOW for 31 so the pulse width is dependent on the number of waits I can configure. The maximum delay per instruction is [31]. There is a balance between the number of “units” to divide the 20mS period into and the pulse width to use. My original RC2014 50 Hz Interrupt Source used a ~5uS pulse, which meant dividing into 4096 units.

    I would ideally be able to reproduce this with something like the following:

    .wrap_target
    set pins, 1 // 1st cycle on
    set pins, 0 // 2nd cycle off
    set x, 4093 // 4096-3 cycles left to loop
    loop:
    jmp x-- loop // 1 cycle each test
    .wrap

    But I can only set a value up to 31, so for anything larger it has to be pulled in from outside. This is the PIO code I’ve ended up with:

    .program int50hz
    pull block ; Grab counter value for delay
    .wrap_target
    set pins, 1 ; [1] One cycle on
    set pins, 0 ; [1] One cycle off
    mov x,osr ; [1] reset counter
    loop:
    jmp x--, loop ; [1] one cycle per loop
    .wrap

    % c-sdk {
    // Loop value less non-looping cycles to total 4096
    #define INT50HZ_LOOP (4095-3)

    static inline void int50hz_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = int50hz_program_get_default_config(offset);

    // 50 Hz clock over 4096 cycles
    float clk50hz = (float)clock_get_hz(clk_sys) / (50.0*4096.0);
    sm_config_set_clkdiv(&c, clk50hz);

    // Configure 1 output pin for use with "set"
    sm_config_set_set_pins(&c, pin, 1);
    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
    }
    %}

    The state machine will then have to run at a frequency that gives me the 50Hz cycle time, as given in the clk50hz part of the initialisation code.

    The following code can be used to initialise and run the 50 Hz PIO:

    #define Z80_INT_GPIO 37
    #define Z80_INT_PIO pio0
    #define Z80_INT_SM 2

    void int50hz_setup() {
    uint offset = pio_add_program(Z80_INT_PIO, &int50hz_program);
    int50hz_program_init(Z80_INT_PIO, Z80_INT_SM,
    offset, Z80_INT_GPIO);
    pio_sm_put_blocking(Z80_INT_PIO, Z80_INT_SM, INT50HZ_LOOP);
    pio_sm_set_enabled(Z80_INT_PIO, Z80_INT_SM, true);
    }

    The latest version of the code can be found on GitHub here: https://github.com/diyelectromusic/sdemp/tree/main/src/Misc/PicoRC2014Video

    Hardware

    The output is currently a HIGH 3V3 pulse so needs to go through a buffer. If it goes through a 74HCT14 (HCT not HC) powered from the RC2014 VCC bus signal then it acts as a buffer, an inverter, and voltage converter from 3V3 to 5V.

    In the final circuit I’ll have the /INT connection via a jumper to allow this to be optional. No other circuitry is required.

    Adding this to my RC2014 Spectrum Video card is a relatively simple thing to do.

    Updated RC2014 Spectrum Video PCB

    In V1 of the PCB /INT is directly connected to GPIO 33. In V2 of the PCB /INT is now connected via the buffer and a jumper.

    Here is an updated schematic and boad diagram.

    Updates:

    • Addition of the 74HCT14 between GPIO 33 and /INT.
    • Addition of two optional LEDs and resistors on GPIO 37 and 39.

    The updated PCB files can be found on GitHub here: https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RC2014/RC2014-PGA2350%20-%20VGA

    Conclusion

    This has worked out really well. I have some new PCB designs now in the post, which I’ll confirm work ok and then update here, but I’ve tried everything with a slightly modified V1 PCB and it all seems to work well.

    It is a great simplification to have the video card doing the 50Hz interrupt too.

    Kevin

    #pcb #pga2350 #rc2014 #vga #zxSpectrum
  15. ZX Spectrum Compatible Video for RC2014 – Part 3

    The ZX Spectrum video is now working satisfactorily as described in Part 2 but when I came to use it I found applications often expected to have the 50Hz video scan interrupt which was originally generated from the ULA based on the video synchronisation signals. I looked at some ways to generate this here: RC2014 50 Hz Interrupt Source but with my video card, I have a microcontroller and a couple of spare GPIO pins (just) so now I’m wondering if I can get the video card to also provide the 50 Hz sync interrupt. That is what this post is looking at.

    • Part 1 – Background information, design considerations, prior art.
    • Part 2 – PCB design for a RC2014 Spectrum Compatible video card.
    • Part 3 – Adding a 50Hz interrupt to the design and details of PCB V2.

    Back to the Scanvideo Library

    My starting point is to go back to the PIO use in the scanvideo library as some kind of autonomous timing pulse is the kind of thing PIO makes pretty easy.

    Whilst scanning through I noticed two definitions that look really interesting:

    PICO_SCANVIDEO_ENABLE_DEN_PIN
    PICO_SCANVIDEO_ENABLE_CLOCK_PIN

    These are undefined by default, but if they are set to 1 in the Makefile configuration it has the result of adding two additional GPIO to the timing PIO code in: src/rp2_common/pico_scanvideo_dpi/timing.pio which uses the PIO side step to output a CLOCK and enable pin for use.

    I believe (from setup_sm() in scanvideo.c) that the order of GPIO for timing signals is:

    GPIO base + 0H SyncGPIO base + 1V SyncGPIO base + 2DEN – “Enable”?GPIO base + 2 or +3CLOCK (follows DEN if configured)

    But there is also the following comment elsewhere in scanvideo.c, which presumably is referring to the case where CLOCK is enabled by DEN isn’t:

    const uint BASE = PICO_SCANVIDEO_SYNC_PIN_BASE; // hsync and vsync are +0 and +1, clock is +2

    I’ve not found a proper description of these latter two GPIO pins so far, but I believe the CLOCK in this case is the pixel clock. In VGA terms I think that is the clock that delineates, within the analog signal, when a new pixel is being represented.

    Apparently the original VGA standard specified a pixel clock of 25.175 MHz or 28.322 MHz. I’m not going to get into how the timings work – to be honest, I’m not totally clear myself, but from these main clocks, we can get the line and frame timings for various resolutions.

    In terms of software, the VGA mode that is used by default is defined in vga_modes.c as:

    const scanvideo_timing_t vga_timing_640x480_60_default =
    {
    .clock_freq = 24000000,

    .h_active = 640,
    .v_active = 480,

    .h_front_porch = 16,
    .h_pulse = 64,
    .h_total = 800,
    .h_sync_polarity = 1,

    .v_front_porch = 1,
    .v_pulse = 2,
    .v_total = 500,
    .v_sync_polarity = 1,

    .enable_clock = 0,
    .clock_polarity = 0,

    .enable_den = 0
    };

    There is only one VGA mode turns on the clock, so I’m not quite sure how to get all this working, but in principle I could probably use logic to divide the 24MHz clock (in the above example) down to something useful.

    But if I’m going to be adding logic, I may as well go back to doing my own thing as described in RC2014 50 Hz Interrupt Source.

    PIO and Video Timings

    At some point scanvideo will be outputting a video frame at a useful frame rate, so what is controlling the timing? Timing.pio has some clues, as you might expect:

    ; these are the values used in the out
    ; exec in video_htiming
    .program video_htiming_states
    .side_set 1
    ; state 0 = set irq 0
    irq 0 side 0
    ; state 1 = set irq 1
    irq 1 side 0
    ; state 2 = set irq 4
    irq 4 side 0
    ; state 3 = clear irq 4
    irq clear 4 side 0

    These seem to be called directly from the C code at various points using pio_encode_… direct API calls within pio_sm_exec() calls. I think most of the magic is going on in the prepare_for_active_scanline_irqs_enabled function.

    How PIO IRQs map onto actual RP2350 interrupts is a little confusing. There are 8 PIO IRQ flags which can be seen by all state machines. But there are just two actual interrupt request lines (as the CPU would recognise them) per PIO. These can be mapped onto interrupt service routines with special definitions as follows (from SDK:src/rp2350/hardware_regs/include/hardware/regs/intctrl.h):

    #define isr_dma_0 isr_irq10
    #define isr_dma_1 isr_irq11
    #define isr_dma_2 isr_irq12
    #define isr_dma_3 isr_irq13

    #define isr_pio0_0 isr_irq15
    #define isr_pio0_1 isr_irq16
    #define isr_pio1_0 isr_irq17
    #define isr_pio1_1 isr_irq18
    #define isr_pio2_0 isr_irq19
    #define isr_pio2_1 isr_irq20

    The two isr functions for each of these have the following details:

    isr_pio0_0:
    // handler for explicit PIO_IRQ0
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to start a DMA for a scanline
    // this called once per scanline during non vblank
    if (video_pio->irq & 1u) {
    ...
    }

    // handler for explicit PIO_IRQ1
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to prepare for a scanline
    // this is only called once per scanline during vblank
    if (video_pio->irq & 2u) {
    ...
    }

    isr_pio0_1:
    top_up_timing_pio_fifo();

    isr_dma_0:
    // DMA complete
    scanline_dma_complete_irqs_enabled();

    So both of these appear to be something to do with the timing state machine, which is SM3. These are enabled from the function scanvideo_timing_enable():

    pio_set_irq0_source_mask_enabled(video_pio,
    (1u << pis_interrupt0) // SM IRQ flag 0 int
    | (1u << pis_interrupt1), // SM IRQ flag 1 int
    true);
    pio_set_irq1_source_enabled(video_pio,
    pis_sm0_tx_fifo_not_full
    + PICO_SCANVIDEO_TIMING_SM,
    true);
    irq_set_mask_enabled((1u << PIO0_IRQ_0)
    | (1u << PIO0_IRQ_1)
    #if !PICO_SCANVIDEO_NO_DMA_TRACKING
    | (1u << DMA_IRQ_0)
    #endif
    , enable);

    Confusingly, when the definition of pis_inerrupt0 is looked up, we find it defined as follows:

    pis_interrupt0 = PIO_INTR_SM0_LSB,
    ///< PIO interrupt 0 is raised

    But then we can see that SM0 is not state machine 0, but refers to fields in the IRQ0_INTE and IRQ1_INTE registers, which include SM0 through to SM7. These are the 8 IRQ flags available to all state machines (although actually only SM0-SM3 can be mapped on an RP2040 – all 8 are available on RP2350).

    IRQ 1 is mapped as follows:

    pis_sm0_tx_fifo_not_full = PIO_INTR_SM0_TXNFULL_LSB,
    ///< State machine 0 TX FIFO is not full

    Which is another definition for the PIO interrupt registers. But this time I think it does map onto a state machine itself, hence when initialised it is offset by the TIMING_SM value (which is 3).

    I find this kind of thing a lot when working through the RP SDKs and such confusing inconsistencies are really draining when trying to figure out what is going on in largely undocumented code…

    Anyway, back to those four PIO states for IRQ handling. They map over into the C world using the follownig.

    // 4 possible instructions; index into program below
    enum {
    SET_IRQ_0 = 0u,
    SET_IRQ_1 = 1u,
    SET_IRQ_SCANLINE = 2u,
    CLEAR_IRQ_SCANLINE = 3u,
    };

    And here is an example of it being used:

    // clear scanline irq
    pio_sm_exec(
    video_pio, PICO_SCANVIDEO_TIMING_SM,
    video_htiming_states_program.instructions[CLEAR_IRQ_SCANLINE]
    );

    But that is the only one that seems to be used directly. Most instances of their use seem to be via the timing_encode() macro and a series of redefinitions for _CMD sequences as follows:

    #define TIMING_CYCLE 3u
    #define timing_encode(state, length, pins) \
    ((video_htiming_states_program.instructions[state] & side_set_mask) \
    | (((uint32_t)(length) - TIMING_CYCLE) << 16u) \
    | ((uint32_t)(pins) << 29u))

    #define A_CMD SET_IRQ_0
    #define A_CMD_VBLANK SET_IRQ_1
    #define B1_CMD CLEAR_IRQ_SCANLINE
    #define B2_CMD CLEAR_IRQ_SCANLINE
    #define C_CMD SET_IRQ_SCANLINE
    #define C_CMD_VBLANK CLEAR_IRQ_SCANLINE

    These are then written into the timing programme based on the video timings provided in the various configuration structures.

    Why do I care about all this? In fact why this rather significant diversion in the first place? Well, I’m hoping I can see where the core video frame timing happens to see if I can piggy back on that for my 50Hz timing signal.

    But, at the present time, I’m failing to see it. There appear to be several asynchronous cycles going on passing instructions across to the PIO via the FIFOs or DMA. I think all the intelligence I want to hook into is not happening in real time, but by queuing up the instructions for PIO to run in a time sensitive manner.

    So it may be possible to hook into this somehow for a timing signal (I was hoping I might be able to wait on the IRQs flying around), but at present, I don’t see how to do that.

    Added to the fact I’m not totally sure it is 50Hz anyway (aren’t VGA timings configurable?) then this is probably not going to get me any further.

    Independent PIO 50Hz Generator

    What I’d really like to do is have a PIO state machine configured with the same 5uS pulse every 20mS that I managed to implement in logic. It would be nice to synchronise this to the video scan, but not essential.

    If I make this a pulse HIGH then it can go through a 74HCT14 inverter to invert the signal, buffer the signal, and also switch from the RP2350’s 3V3 IO to a 5V signal. This requires powering the 74HCT14 from the RC bus 5V and not the RP2350 PGA signals.

    From Simplified Pico VGA I noted that the scanvideo library is using PIO 0 with the following state machines:

    • 0 – scanline state machine.
    • 1, 2 – optional additional video planes (if configured).
    • 3 – timing state machine.

    As each PIO only has four state machines, these would appear to all be in use. Except I’m only using a single “video plane” as far as I know, so I might try and hijack SM2 and see how it goes.

    As each state machine has its own clock divisor, I should be able to configure it to run at something useful. If I want a pulse, the simplest way to implement it would be as follows (see “Hello PIO” from PIO on the Raspberry Pi Pico – Part 2):

    .wrap_target
    set pins, 1 // 1 cycle
    set pins, 0 [30] // 31 cycles
    .wrap

    This has the GPIO HIGH for 1 cycle and LOW for 31 so the pulse width is dependent on the number of waits I can configure. The maximum delay per instruction is [31]. There is a balance between the number of “units” to divide the 20mS period into and the pulse width to use. My original RC2014 50 Hz Interrupt Source used a ~5uS pulse, which meant dividing into 4096 units.

    I would ideally be able to reproduce this with something like the following:

    .wrap_target
    set pins, 1 // 1st cycle on
    set pins, 0 // 2nd cycle off
    set x, 4093 // 4096-3 cycles left to loop
    loop:
    jmp x-- loop // 1 cycle each test
    .wrap

    But I can only set a value up to 31, so for anything larger it has to be pulled in from outside. This is the PIO code I’ve ended up with:

    .program int50hz
    pull block ; Grab counter value for delay
    .wrap_target
    set pins, 1 ; [1] One cycle on
    set pins, 0 ; [1] One cycle off
    mov x,osr ; [1] reset counter
    loop:
    jmp x--, loop ; [1] one cycle per loop
    .wrap

    % c-sdk {
    // Loop value less non-looping cycles to total 4096
    #define INT50HZ_LOOP (4095-3)

    static inline void int50hz_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = int50hz_program_get_default_config(offset);

    // 50 Hz clock over 4096 cycles
    float clk50hz = (float)clock_get_hz(clk_sys) / (50.0*4096.0);
    sm_config_set_clkdiv(&c, clk50hz);

    // Configure 1 output pin for use with "set"
    sm_config_set_set_pins(&c, pin, 1);
    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
    }
    %}

    The state machine will then have to run at a frequency that gives me the 50Hz cycle time, as given in the clk50hz part of the initialisation code.

    The following code can be used to initialise and run the 50 Hz PIO:

    #define Z80_INT_GPIO 37
    #define Z80_INT_PIO pio0
    #define Z80_INT_SM 2

    void int50hz_setup() {
    uint offset = pio_add_program(Z80_INT_PIO, &int50hz_program);
    int50hz_program_init(Z80_INT_PIO, Z80_INT_SM,
    offset, Z80_INT_GPIO);
    pio_sm_put_blocking(Z80_INT_PIO, Z80_INT_SM, INT50HZ_LOOP);
    pio_sm_set_enabled(Z80_INT_PIO, Z80_INT_SM, true);
    }

    The latest version of the code can be found on GitHub here: https://github.com/diyelectromusic/sdemp/tree/main/src/Misc/PicoRC2014Video

    Hardware

    The output is currently a HIGH 3V3 pulse so needs to go through a buffer. If it goes through a 74HCT14 (HCT not HC) powered from the RC2014 VCC bus signal then it acts as a buffer, an inverter, and voltage converter from 3V3 to 5V.

    In the final circuit I’ll have the /INT connection via a jumper to allow this to be optional. No other circuitry is required.

    Adding this to my RC2014 Spectrum Video card is a relatively simple thing to do.

    Updated RC2014 Spectrum Video PCB

    In V1 of the PCB /INT is directly connected to GPIO 33. In V2 of the PCB /INT is now connected via the buffer and a jumper.

    Here is an updated schematic and boad diagram.

    Updates:

    • Addition of the 74HCT14 between GPIO 33 and /INT.
    • Addition of two optional LEDs and resistors on GPIO 37 and 39.

    The updated PCB files can be found on GitHub here: https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RC2014/RC2014-PGA2350%20-%20VGA

    Conclusion

    This has worked out really well. I have some new PCB designs now in the post, which I’ll confirm work ok and then update here, but I’ve tried everything with a slightly modified V1 PCB and it all seems to work well.

    It is a great simplification to have the video card doing the 50Hz interrupt too.

    Kevin

    #pcb #pga2350 #rc2014 #vga #zxSpectrum
  16. ZX Spectrum Compatible Video for RC2014 – Part 3

    The ZX Spectrum video is now working satisfactorily as described in Part 2 but when I came to use it I found applications often expected to have the 50Hz video scan interrupt which was originally generated from the ULA based on the video synchronisation signals. I looked at some ways to generate this here: RC2014 50 Hz Interrupt Source but with my video card, I have a microcontroller and a couple of spare GPIO pins (just) so now I’m wondering if I can get the video card to also provide the 50 Hz sync interrupt. That is what this post is looking at.

    • Part 1 – Background information, design considerations, prior art.
    • Part 2 – PCB design for a RC2014 Spectrum Compatible video card.
    • Part 3 – Adding a 50Hz interrupt to the design and details of PCB V2.

    Back to the Scanvideo Library

    My starting point is to go back to the PIO use in the scanvideo library as some kind of autonomous timing pulse is the kind of thing PIO makes pretty easy.

    Whilst scanning through I noticed two definitions that look really interesting:

    PICO_SCANVIDEO_ENABLE_DEN_PIN
    PICO_SCANVIDEO_ENABLE_CLOCK_PIN

    These are undefined by default, but if they are set to 1 in the Makefile configuration it has the result of adding two additional GPIO to the timing PIO code in: src/rp2_common/pico_scanvideo_dpi/timing.pio which uses the PIO side step to output a CLOCK and enable pin for use.

    I believe (from setup_sm() in scanvideo.c) that the order of GPIO for timing signals is:

    GPIO base + 0H SyncGPIO base + 1V SyncGPIO base + 2DEN – “Enable”?GPIO base + 2 or +3CLOCK (follows DEN if configured)

    But there is also the following comment elsewhere in scanvideo.c, which presumably is referring to the case where CLOCK is enabled by DEN isn’t:

    const uint BASE = PICO_SCANVIDEO_SYNC_PIN_BASE; // hsync and vsync are +0 and +1, clock is +2

    I’ve not found a proper description of these latter two GPIO pins so far, but I believe the CLOCK in this case is the pixel clock. In VGA terms I think that is the clock that delineates, within the analog signal, when a new pixel is being represented.

    Apparently the original VGA standard specified a pixel clock of 25.175 MHz or 28.322 MHz. I’m not going to get into how the timings work – to be honest, I’m not totally clear myself, but from these main clocks, we can get the line and frame timings for various resolutions.

    In terms of software, the VGA mode that is used by default is defined in vga_modes.c as:

    const scanvideo_timing_t vga_timing_640x480_60_default =
    {
    .clock_freq = 24000000,

    .h_active = 640,
    .v_active = 480,

    .h_front_porch = 16,
    .h_pulse = 64,
    .h_total = 800,
    .h_sync_polarity = 1,

    .v_front_porch = 1,
    .v_pulse = 2,
    .v_total = 500,
    .v_sync_polarity = 1,

    .enable_clock = 0,
    .clock_polarity = 0,

    .enable_den = 0
    };

    There is only one VGA mode turns on the clock, so I’m not quite sure how to get all this working, but in principle I could probably use logic to divide the 24MHz clock (in the above example) down to something useful.

    But if I’m going to be adding logic, I may as well go back to doing my own thing as described in RC2014 50 Hz Interrupt Source.

    PIO and Video Timings

    At some point scanvideo will be outputting a video frame at a useful frame rate, so what is controlling the timing? Timing.pio has some clues, as you might expect:

    ; these are the values used in the out
    ; exec in video_htiming
    .program video_htiming_states
    .side_set 1
    ; state 0 = set irq 0
    irq 0 side 0
    ; state 1 = set irq 1
    irq 1 side 0
    ; state 2 = set irq 4
    irq 4 side 0
    ; state 3 = clear irq 4
    irq clear 4 side 0

    These seem to be called directly from the C code at various points using pio_encode_… direct API calls within pio_sm_exec() calls. I think most of the magic is going on in the prepare_for_active_scanline_irqs_enabled function.

    How PIO IRQs map onto actual RP2350 interrupts is a little confusing. There are 8 PIO IRQ flags which can be seen by all state machines. But there are just two actual interrupt request lines (as the CPU would recognise them) per PIO. These can be mapped onto interrupt service routines with special definitions as follows (from SDK:src/rp2350/hardware_regs/include/hardware/regs/intctrl.h):

    #define isr_dma_0 isr_irq10
    #define isr_dma_1 isr_irq11
    #define isr_dma_2 isr_irq12
    #define isr_dma_3 isr_irq13

    #define isr_pio0_0 isr_irq15
    #define isr_pio0_1 isr_irq16
    #define isr_pio1_0 isr_irq17
    #define isr_pio1_1 isr_irq18
    #define isr_pio2_0 isr_irq19
    #define isr_pio2_1 isr_irq20

    The two isr functions for each of these have the following details:

    isr_pio0_0:
    // handler for explicit PIO_IRQ0
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to start a DMA for a scanline
    // this called once per scanline during non vblank
    if (video_pio->irq & 1u) {
    ...
    }

    // handler for explicit PIO_IRQ1
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to prepare for a scanline
    // this is only called once per scanline during vblank
    if (video_pio->irq & 2u) {
    ...
    }

    isr_pio0_1:
    top_up_timing_pio_fifo();

    isr_dma_0:
    // DMA complete
    scanline_dma_complete_irqs_enabled();

    So both of these appear to be something to do with the timing state machine, which is SM3. These are enabled from the function scanvideo_timing_enable():

    pio_set_irq0_source_mask_enabled(video_pio,
    (1u << pis_interrupt0) // SM IRQ flag 0 int
    | (1u << pis_interrupt1), // SM IRQ flag 1 int
    true);
    pio_set_irq1_source_enabled(video_pio,
    pis_sm0_tx_fifo_not_full
    + PICO_SCANVIDEO_TIMING_SM,
    true);
    irq_set_mask_enabled((1u << PIO0_IRQ_0)
    | (1u << PIO0_IRQ_1)
    #if !PICO_SCANVIDEO_NO_DMA_TRACKING
    | (1u << DMA_IRQ_0)
    #endif
    , enable);

    Confusingly, when the definition of pis_inerrupt0 is looked up, we find it defined as follows:

    pis_interrupt0 = PIO_INTR_SM0_LSB,
    ///< PIO interrupt 0 is raised

    But then we can see that SM0 is not state machine 0, but refers to fields in the IRQ0_INTE and IRQ1_INTE registers, which include SM0 through to SM7. These are the 8 IRQ flags available to all state machines (although actually only SM0-SM3 can be mapped on an RP2040 – all 8 are available on RP2350).

    IRQ 1 is mapped as follows:

    pis_sm0_tx_fifo_not_full = PIO_INTR_SM0_TXNFULL_LSB,
    ///< State machine 0 TX FIFO is not full

    Which is another definition for the PIO interrupt registers. But this time I think it does map onto a state machine itself, hence when initialised it is offset by the TIMING_SM value (which is 3).

    I find this kind of thing a lot when working through the RP SDKs and such confusing inconsistencies are really draining when trying to figure out what is going on in largely undocumented code…

    Anyway, back to those four PIO states for IRQ handling. They map over into the C world using the follownig.

    // 4 possible instructions; index into program below
    enum {
    SET_IRQ_0 = 0u,
    SET_IRQ_1 = 1u,
    SET_IRQ_SCANLINE = 2u,
    CLEAR_IRQ_SCANLINE = 3u,
    };

    And here is an example of it being used:

    // clear scanline irq
    pio_sm_exec(
    video_pio, PICO_SCANVIDEO_TIMING_SM,
    video_htiming_states_program.instructions[CLEAR_IRQ_SCANLINE]
    );

    But that is the only one that seems to be used directly. Most instances of their use seem to be via the timing_encode() macro and a series of redefinitions for _CMD sequences as follows:

    #define TIMING_CYCLE 3u
    #define timing_encode(state, length, pins) \
    ((video_htiming_states_program.instructions[state] & side_set_mask) \
    | (((uint32_t)(length) - TIMING_CYCLE) << 16u) \
    | ((uint32_t)(pins) << 29u))

    #define A_CMD SET_IRQ_0
    #define A_CMD_VBLANK SET_IRQ_1
    #define B1_CMD CLEAR_IRQ_SCANLINE
    #define B2_CMD CLEAR_IRQ_SCANLINE
    #define C_CMD SET_IRQ_SCANLINE
    #define C_CMD_VBLANK CLEAR_IRQ_SCANLINE

    These are then written into the timing programme based on the video timings provided in the various configuration structures.

    Why do I care about all this? In fact why this rather significant diversion in the first place? Well, I’m hoping I can see where the core video frame timing happens to see if I can piggy back on that for my 50Hz timing signal.

    But, at the present time, I’m failing to see it. There appear to be several asynchronous cycles going on passing instructions across to the PIO via the FIFOs or DMA. I think all the intelligence I want to hook into is not happening in real time, but by queuing up the instructions for PIO to run in a time sensitive manner.

    So it may be possible to hook into this somehow for a timing signal (I was hoping I might be able to wait on the IRQs flying around), but at present, I don’t see how to do that.

    Added to the fact I’m not totally sure it is 50Hz anyway (aren’t VGA timings configurable?) then this is probably not going to get me any further.

    Independent PIO 50Hz Generator

    What I’d really like to do is have a PIO state machine configured with the same 5uS pulse every 20mS that I managed to implement in logic. It would be nice to synchronise this to the video scan, but not essential.

    If I make this a pulse HIGH then it can go through a 74HCT14 inverter to invert the signal, buffer the signal, and also switch from the RP2350’s 3V3 IO to a 5V signal. This requires powering the 74HCT14 from the RC bus 5V and not the RP2350 PGA signals.

    From Simplified Pico VGA I noted that the scanvideo library is using PIO 0 with the following state machines:

    • 0 – scanline state machine.
    • 1, 2 – optional additional video planes (if configured).
    • 3 – timing state machine.

    As each PIO only has four state machines, these would appear to all be in use. Except I’m only using a single “video plane” as far as I know, so I might try and hijack SM2 and see how it goes.

    As each state machine has its own clock divisor, I should be able to configure it to run at something useful. If I want a pulse, the simplest way to implement it would be as follows (see “Hello PIO” from PIO on the Raspberry Pi Pico – Part 2):

    .wrap_target
    set pins, 1 // 1 cycle
    set pins, 0 [30] // 31 cycles
    .wrap

    This has the GPIO HIGH for 1 cycle and LOW for 31 so the pulse width is dependent on the number of waits I can configure. The maximum delay per instruction is [31]. There is a balance between the number of “units” to divide the 20mS period into and the pulse width to use. My original RC2014 50 Hz Interrupt Source used a ~5uS pulse, which meant dividing into 4096 units.

    I would ideally be able to reproduce this with something like the following:

    .wrap_target
    set pins, 1 // 1st cycle on
    set pins, 0 // 2nd cycle off
    set x, 4093 // 4096-3 cycles left to loop
    loop:
    jmp x-- loop // 1 cycle each test
    .wrap

    But I can only set a value up to 31, so for anything larger it has to be pulled in from outside. This is the PIO code I’ve ended up with:

    .program int50hz
    pull block ; Grab counter value for delay
    .wrap_target
    set pins, 1 ; [1] One cycle on
    set pins, 0 ; [1] One cycle off
    mov x,osr ; [1] reset counter
    loop:
    jmp x--, loop ; [1] one cycle per loop
    .wrap

    % c-sdk {
    // Loop value less non-looping cycles to total 4096
    #define INT50HZ_LOOP (4095-3)

    static inline void int50hz_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = int50hz_program_get_default_config(offset);

    // 50 Hz clock over 4096 cycles
    float clk50hz = (float)clock_get_hz(clk_sys) / (50.0*4096.0);
    sm_config_set_clkdiv(&c, clk50hz);

    // Configure 1 output pin for use with "set"
    sm_config_set_set_pins(&c, pin, 1);
    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
    }
    %}

    The state machine will then have to run at a frequency that gives me the 50Hz cycle time, as given in the clk50hz part of the initialisation code.

    The following code can be used to initialise and run the 50 Hz PIO:

    #define Z80_INT_GPIO 37
    #define Z80_INT_PIO pio0
    #define Z80_INT_SM 2

    void int50hz_setup() {
    uint offset = pio_add_program(Z80_INT_PIO, &int50hz_program);
    int50hz_program_init(Z80_INT_PIO, Z80_INT_SM,
    offset, Z80_INT_GPIO);
    pio_sm_put_blocking(Z80_INT_PIO, Z80_INT_SM, INT50HZ_LOOP);
    pio_sm_set_enabled(Z80_INT_PIO, Z80_INT_SM, true);
    }

    The latest version of the code can be found on GitHub here: https://github.com/diyelectromusic/sdemp/tree/main/src/Misc/PicoRC2014Video

    Hardware

    The output is currently a HIGH 3V3 pulse so needs to go through a buffer. If it goes through a 74HCT14 (HCT not HC) powered from the RC2014 VCC bus signal then it acts as a buffer, an inverter, and voltage converter from 3V3 to 5V.

    In the final circuit I’ll have the /INT connection via a jumper to allow this to be optional. No other circuitry is required.

    Adding this to my RC2014 Spectrum Video card is a relatively simple thing to do.

    Updated RC2014 Spectrum Video PCB

    In V1 of the PCB /INT is directly connected to GPIO 33. In V2 of the PCB /INT is now connected via the buffer and a jumper.

    Here is an updated schematic and boad diagram.

    Updates:

    • Addition of the 74HCT14 between GPIO 33 and /INT.
    • Addition of two optional LEDs and resistors on GPIO 37 and 39.

    The updated PCB files can be found on GitHub here: https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RC2014/RC2014-PGA2350%20-%20VGA

    Conclusion

    This has worked out really well. I have some new PCB designs now in the post, which I’ll confirm work ok and then update here, but I’ve tried everything with a slightly modified V1 PCB and it all seems to work well.

    It is a great simplification to have the video card doing the 50Hz interrupt too.

    Kevin

    #pcb #pga2350 #rc2014 #vga #zxSpectrum
  17. ZX Spectrum Compatible Video for RC2014 – Part 3

    The ZX Spectrum video is now working satisfactorily as described in Part 2 but when I came to use it I found applications often expected to have the 50Hz video scan interrupt which was originally generated from the ULA based on the video synchronisation signals. I looked at some ways to generate this here: RC2014 50 Hz Interrupt Source but with my video card, I have a microcontroller and a couple of spare GPIO pins (just) so now I’m wondering if I can get the video card to also provide the 50 Hz sync interrupt. That is what this post is looking at.

    • Part 1 – Background information, design considerations, prior art.
    • Part 2 – PCB design for a RC2014 Spectrum Compatible video card.
    • Part 3 – Adding a 50Hz interrupt to the design and details of PCB V2.

    Back to the Scanvideo Library

    My starting point is to go back to the PIO use in the scanvideo library as some kind of autonomous timing pulse is the kind of thing PIO makes pretty easy.

    Whilst scanning through I noticed two definitions that look really interesting:

    PICO_SCANVIDEO_ENABLE_DEN_PIN
    PICO_SCANVIDEO_ENABLE_CLOCK_PIN

    These are undefined by default, but if they are set to 1 in the Makefile configuration it has the result of adding two additional GPIO to the timing PIO code in: src/rp2_common/pico_scanvideo_dpi/timing.pio which uses the PIO side step to output a CLOCK and enable pin for use.

    I believe (from setup_sm() in scanvideo.c) that the order of GPIO for timing signals is:

    GPIO base + 0H SyncGPIO base + 1V SyncGPIO base + 2DEN – “Enable”?GPIO base + 2 or +3CLOCK (follows DEN if configured)

    But there is also the following comment elsewhere in scanvideo.c, which presumably is referring to the case where CLOCK is enabled by DEN isn’t:

    const uint BASE = PICO_SCANVIDEO_SYNC_PIN_BASE; // hsync and vsync are +0 and +1, clock is +2

    I’ve not found a proper description of these latter two GPIO pins so far, but I believe the CLOCK in this case is the pixel clock. In VGA terms I think that is the clock that delineates, within the analog signal, when a new pixel is being represented.

    Apparently the original VGA standard specified a pixel clock of 25.175 MHz or 28.322 MHz. I’m not going to get into how the timings work – to be honest, I’m not totally clear myself, but from these main clocks, we can get the line and frame timings for various resolutions.

    In terms of software, the VGA mode that is used by default is defined in vga_modes.c as:

    const scanvideo_timing_t vga_timing_640x480_60_default =
    {
    .clock_freq = 24000000,

    .h_active = 640,
    .v_active = 480,

    .h_front_porch = 16,
    .h_pulse = 64,
    .h_total = 800,
    .h_sync_polarity = 1,

    .v_front_porch = 1,
    .v_pulse = 2,
    .v_total = 500,
    .v_sync_polarity = 1,

    .enable_clock = 0,
    .clock_polarity = 0,

    .enable_den = 0
    };

    There is only one VGA mode turns on the clock, so I’m not quite sure how to get all this working, but in principle I could probably use logic to divide the 24MHz clock (in the above example) down to something useful.

    But if I’m going to be adding logic, I may as well go back to doing my own thing as described in RC2014 50 Hz Interrupt Source.

    PIO and Video Timings

    At some point scanvideo will be outputting a video frame at a useful frame rate, so what is controlling the timing? Timing.pio has some clues, as you might expect:

    ; these are the values used in the out
    ; exec in video_htiming
    .program video_htiming_states
    .side_set 1
    ; state 0 = set irq 0
    irq 0 side 0
    ; state 1 = set irq 1
    irq 1 side 0
    ; state 2 = set irq 4
    irq 4 side 0
    ; state 3 = clear irq 4
    irq clear 4 side 0

    These seem to be called directly from the C code at various points using pio_encode_… direct API calls within pio_sm_exec() calls. I think most of the magic is going on in the prepare_for_active_scanline_irqs_enabled function.

    How PIO IRQs map onto actual RP2350 interrupts is a little confusing. There are 8 PIO IRQ flags which can be seen by all state machines. But there are just two actual interrupt request lines (as the CPU would recognise them) per PIO. These can be mapped onto interrupt service routines with special definitions as follows (from SDK:src/rp2350/hardware_regs/include/hardware/regs/intctrl.h):

    #define isr_dma_0 isr_irq10
    #define isr_dma_1 isr_irq11
    #define isr_dma_2 isr_irq12
    #define isr_dma_3 isr_irq13

    #define isr_pio0_0 isr_irq15
    #define isr_pio0_1 isr_irq16
    #define isr_pio1_0 isr_irq17
    #define isr_pio1_1 isr_irq18
    #define isr_pio2_0 isr_irq19
    #define isr_pio2_1 isr_irq20

    The two isr functions for each of these have the following details:

    isr_pio0_0:
    // handler for explicit PIO_IRQ0
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to start a DMA for a scanline
    // this called once per scanline during non vblank
    if (video_pio->irq & 1u) {
    ...
    }

    // handler for explicit PIO_IRQ1
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to prepare for a scanline
    // this is only called once per scanline during vblank
    if (video_pio->irq & 2u) {
    ...
    }

    isr_pio0_1:
    top_up_timing_pio_fifo();

    isr_dma_0:
    // DMA complete
    scanline_dma_complete_irqs_enabled();

    So both of these appear to be something to do with the timing state machine, which is SM3. These are enabled from the function scanvideo_timing_enable():

    pio_set_irq0_source_mask_enabled(video_pio,
    (1u << pis_interrupt0) // SM IRQ flag 0 int
    | (1u << pis_interrupt1), // SM IRQ flag 1 int
    true);
    pio_set_irq1_source_enabled(video_pio,
    pis_sm0_tx_fifo_not_full
    + PICO_SCANVIDEO_TIMING_SM,
    true);
    irq_set_mask_enabled((1u << PIO0_IRQ_0)
    | (1u << PIO0_IRQ_1)
    #if !PICO_SCANVIDEO_NO_DMA_TRACKING
    | (1u << DMA_IRQ_0)
    #endif
    , enable);

    Confusingly, when the definition of pis_inerrupt0 is looked up, we find it defined as follows:

    pis_interrupt0 = PIO_INTR_SM0_LSB,
    ///< PIO interrupt 0 is raised

    But then we can see that SM0 is not state machine 0, but refers to fields in the IRQ0_INTE and IRQ1_INTE registers, which include SM0 through to SM7. These are the 8 IRQ flags available to all state machines (although actually only SM0-SM3 can be mapped on an RP2040 – all 8 are available on RP2350).

    IRQ 1 is mapped as follows:

    pis_sm0_tx_fifo_not_full = PIO_INTR_SM0_TXNFULL_LSB,
    ///< State machine 0 TX FIFO is not full

    Which is another definition for the PIO interrupt registers. But this time I think it does map onto a state machine itself, hence when initialised it is offset by the TIMING_SM value (which is 3).

    I find this kind of thing a lot when working through the RP SDKs and such confusing inconsistencies are really draining when trying to figure out what is going on in largely undocumented code…

    Anyway, back to those four PIO states for IRQ handling. They map over into the C world using the follownig.

    // 4 possible instructions; index into program below
    enum {
    SET_IRQ_0 = 0u,
    SET_IRQ_1 = 1u,
    SET_IRQ_SCANLINE = 2u,
    CLEAR_IRQ_SCANLINE = 3u,
    };

    And here is an example of it being used:

    // clear scanline irq
    pio_sm_exec(
    video_pio, PICO_SCANVIDEO_TIMING_SM,
    video_htiming_states_program.instructions[CLEAR_IRQ_SCANLINE]
    );

    But that is the only one that seems to be used directly. Most instances of their use seem to be via the timing_encode() macro and a series of redefinitions for _CMD sequences as follows:

    #define TIMING_CYCLE 3u
    #define timing_encode(state, length, pins) \
    ((video_htiming_states_program.instructions[state] & side_set_mask) \
    | (((uint32_t)(length) - TIMING_CYCLE) << 16u) \
    | ((uint32_t)(pins) << 29u))

    #define A_CMD SET_IRQ_0
    #define A_CMD_VBLANK SET_IRQ_1
    #define B1_CMD CLEAR_IRQ_SCANLINE
    #define B2_CMD CLEAR_IRQ_SCANLINE
    #define C_CMD SET_IRQ_SCANLINE
    #define C_CMD_VBLANK CLEAR_IRQ_SCANLINE

    These are then written into the timing programme based on the video timings provided in the various configuration structures.

    Why do I care about all this? In fact why this rather significant diversion in the first place? Well, I’m hoping I can see where the core video frame timing happens to see if I can piggy back on that for my 50Hz timing signal.

    But, at the present time, I’m failing to see it. There appear to be several asynchronous cycles going on passing instructions across to the PIO via the FIFOs or DMA. I think all the intelligence I want to hook into is not happening in real time, but by queuing up the instructions for PIO to run in a time sensitive manner.

    So it may be possible to hook into this somehow for a timing signal (I was hoping I might be able to wait on the IRQs flying around), but at present, I don’t see how to do that.

    Added to the fact I’m not totally sure it is 50Hz anyway (aren’t VGA timings configurable?) then this is probably not going to get me any further.

    Independent PIO 50Hz Generator

    What I’d really like to do is have a PIO state machine configured with the same 5uS pulse every 20mS that I managed to implement in logic. It would be nice to synchronise this to the video scan, but not essential.

    If I make this a pulse HIGH then it can go through a 74HCT14 inverter to invert the signal, buffer the signal, and also switch from the RP2350’s 3V3 IO to a 5V signal. This requires powering the 74HCT14 from the RC bus 5V and not the RP2350 PGA signals.

    From Simplified Pico VGA I noted that the scanvideo library is using PIO 0 with the following state machines:

    • 0 – scanline state machine.
    • 1, 2 – optional additional video planes (if configured).
    • 3 – timing state machine.

    As each PIO only has four state machines, these would appear to all be in use. Except I’m only using a single “video plane” as far as I know, so I might try and hijack SM2 and see how it goes.

    As each state machine has its own clock divisor, I should be able to configure it to run at something useful. If I want a pulse, the simplest way to implement it would be as follows (see “Hello PIO” from PIO on the Raspberry Pi Pico – Part 2):

    .wrap_target
    set pins, 1 // 1 cycle
    set pins, 0 [30] // 31 cycles
    .wrap

    This has the GPIO HIGH for 1 cycle and LOW for 31 so the pulse width is dependent on the number of waits I can configure. The maximum delay per instruction is [31]. There is a balance between the number of “units” to divide the 20mS period into and the pulse width to use. My original RC2014 50 Hz Interrupt Source used a ~5uS pulse, which meant dividing into 4096 units.

    I would ideally be able to reproduce this with something like the following:

    .wrap_target
    set pins, 1 // 1st cycle on
    set pins, 0 // 2nd cycle off
    set x, 4093 // 4096-3 cycles left to loop
    loop:
    jmp x-- loop // 1 cycle each test
    .wrap

    But I can only set a value up to 31, so for anything larger it has to be pulled in from outside. This is the PIO code I’ve ended up with:

    .program int50hz
    pull block ; Grab counter value for delay
    .wrap_target
    set pins, 1 ; [1] One cycle on
    set pins, 0 ; [1] One cycle off
    mov x,osr ; [1] reset counter
    loop:
    jmp x--, loop ; [1] one cycle per loop
    .wrap

    % c-sdk {
    // Loop value less non-looping cycles to total 4096
    #define INT50HZ_LOOP (4095-3)

    static inline void int50hz_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = int50hz_program_get_default_config(offset);

    // 50 Hz clock over 4096 cycles
    float clk50hz = (float)clock_get_hz(clk_sys) / (50.0*4096.0);
    sm_config_set_clkdiv(&c, clk50hz);

    // Configure 1 output pin for use with "set"
    sm_config_set_set_pins(&c, pin, 1);
    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
    }
    %}

    The state machine will then have to run at a frequency that gives me the 50Hz cycle time, as given in the clk50hz part of the initialisation code.

    The following code can be used to initialise and run the 50 Hz PIO:

    #define Z80_INT_GPIO 37
    #define Z80_INT_PIO pio0
    #define Z80_INT_SM 2

    void int50hz_setup() {
    uint offset = pio_add_program(Z80_INT_PIO, &int50hz_program);
    int50hz_program_init(Z80_INT_PIO, Z80_INT_SM,
    offset, Z80_INT_GPIO);
    pio_sm_put_blocking(Z80_INT_PIO, Z80_INT_SM, INT50HZ_LOOP);
    pio_sm_set_enabled(Z80_INT_PIO, Z80_INT_SM, true);
    }

    The latest version of the code can be found on GitHub here: https://github.com/diyelectromusic/sdemp/tree/main/src/Misc/PicoRC2014Video

    Hardware

    The output is currently a HIGH 3V3 pulse so needs to go through a buffer. If it goes through a 74HCT14 (HCT not HC) powered from the RC2014 VCC bus signal then it acts as a buffer, an inverter, and voltage converter from 3V3 to 5V.

    In the final circuit I’ll have the /INT connection via a jumper to allow this to be optional. No other circuitry is required.

    Adding this to my RC2014 Spectrum Video card is a relatively simple thing to do.

    Updated RC2014 Spectrum Video PCB

    In V1 of the PCB /INT is directly connected to GPIO 33. In V2 of the PCB /INT is now connected via the buffer and a jumper.

    Here is an updated schematic and boad diagram.

    Updates:

    • Addition of the 74HCT14 between GPIO 33 and /INT.
    • Addition of two optional LEDs and resistors on GPIO 37 and 39.

    The updated PCB files can be found on GitHub here: https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RC2014/RC2014-PGA2350%20-%20VGA

    Conclusion

    This has worked out really well. I have some new PCB designs now in the post, which I’ll confirm work ok and then update here, but I’ve tried everything with a slightly modified V1 PCB and it all seems to work well.

    It is a great simplification to have the video card doing the 50Hz interrupt too.

    Kevin

    #pcb #pga2350 #rc2014 #vga #zxSpectrum
  18. ZX Spectrum Compatible Video for RC2014 – Part 3

    The ZX Spectrum video is now working satisfactorily as described in Part 2 but when I came to use it I found applications often expected to have the 50Hz video scan interrupt which was originally generated from the ULA based on the video synchronisation signals. I looked at some ways to generate this here: RC2014 50 Hz Interrupt Source but with my video card, I have a microcontroller and a couple of spare GPIO pins (just) so now I’m wondering if I can get the video card to also provide the 50 Hz sync interrupt. That is what this post is looking at.

    • Part 1 – Background information, design considerations, prior art.
    • Part 2 – PCB design for a RC2014 Spectrum Compatible video card.
    • Part 3 – Adding a 50Hz interrupt to the design and details of PCB V2.

    Back to the Scanvideo Library

    My starting point is to go back to the PIO use in the scanvideo library as some kind of autonomous timing pulse is the kind of thing PIO makes pretty easy.

    Whilst scanning through I noticed two definitions that look really interesting:

    PICO_SCANVIDEO_ENABLE_DEN_PIN
    PICO_SCANVIDEO_ENABLE_CLOCK_PIN

    These are undefined by default, but if they are set to 1 in the Makefile configuration it has the result of adding two additional GPIO to the timing PIO code in: src/rp2_common/pico_scanvideo_dpi/timing.pio which uses the PIO side step to output a CLOCK and enable pin for use.

    I believe (from setup_sm() in scanvideo.c) that the order of GPIO for timing signals is:

    GPIO base + 0H SyncGPIO base + 1V SyncGPIO base + 2DEN – “Enable”?GPIO base + 2 or +3CLOCK (follows DEN if configured)

    But there is also the following comment elsewhere in scanvideo.c, which presumably is referring to the case where CLOCK is enabled by DEN isn’t:

    const uint BASE = PICO_SCANVIDEO_SYNC_PIN_BASE; // hsync and vsync are +0 and +1, clock is +2

    I’ve not found a proper description of these latter two GPIO pins so far, but I believe the CLOCK in this case is the pixel clock. In VGA terms I think that is the clock that delineates, within the analog signal, when a new pixel is being represented.

    Apparently the original VGA standard specified a pixel clock of 25.175 MHz or 28.322 MHz. I’m not going to get into how the timings work – to be honest, I’m not totally clear myself, but from these main clocks, we can get the line and frame timings for various resolutions.

    In terms of software, the VGA mode that is used by default is defined in vga_modes.c as:

    const scanvideo_timing_t vga_timing_640x480_60_default =
    {
    .clock_freq = 24000000,

    .h_active = 640,
    .v_active = 480,

    .h_front_porch = 16,
    .h_pulse = 64,
    .h_total = 800,
    .h_sync_polarity = 1,

    .v_front_porch = 1,
    .v_pulse = 2,
    .v_total = 500,
    .v_sync_polarity = 1,

    .enable_clock = 0,
    .clock_polarity = 0,

    .enable_den = 0
    };

    There is only one VGA mode turns on the clock, so I’m not quite sure how to get all this working, but in principle I could probably use logic to divide the 24MHz clock (in the above example) down to something useful.

    But if I’m going to be adding logic, I may as well go back to doing my own thing as described in RC2014 50 Hz Interrupt Source.

    PIO and Video Timings

    At some point scanvideo will be outputting a video frame at a useful frame rate, so what is controlling the timing? Timing.pio has some clues, as you might expect:

    ; these are the values used in the out
    ; exec in video_htiming
    .program video_htiming_states
    .side_set 1
    ; state 0 = set irq 0
    irq 0 side 0
    ; state 1 = set irq 1
    irq 1 side 0
    ; state 2 = set irq 4
    irq 4 side 0
    ; state 3 = clear irq 4
    irq clear 4 side 0

    These seem to be called directly from the C code at various points using pio_encode_… direct API calls within pio_sm_exec() calls. I think most of the magic is going on in the prepare_for_active_scanline_irqs_enabled function.

    How PIO IRQs map onto actual RP2350 interrupts is a little confusing. There are 8 PIO IRQ flags which can be seen by all state machines. But there are just two actual interrupt request lines (as the CPU would recognise them) per PIO. These can be mapped onto interrupt service routines with special definitions as follows (from SDK:src/rp2350/hardware_regs/include/hardware/regs/intctrl.h):

    #define isr_dma_0 isr_irq10
    #define isr_dma_1 isr_irq11
    #define isr_dma_2 isr_irq12
    #define isr_dma_3 isr_irq13

    #define isr_pio0_0 isr_irq15
    #define isr_pio0_1 isr_irq16
    #define isr_pio1_0 isr_irq17
    #define isr_pio1_1 isr_irq18
    #define isr_pio2_0 isr_irq19
    #define isr_pio2_1 isr_irq20

    The two isr functions for each of these have the following details:

    isr_pio0_0:
    // handler for explicit PIO_IRQ0
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to start a DMA for a scanline
    // this called once per scanline during non vblank
    if (video_pio->irq & 1u) {
    ...
    }

    // handler for explicit PIO_IRQ1
    // from PICO_SCANVIDEO_TIMING_SM
    // at a good time to prepare for a scanline
    // this is only called once per scanline during vblank
    if (video_pio->irq & 2u) {
    ...
    }

    isr_pio0_1:
    top_up_timing_pio_fifo();

    isr_dma_0:
    // DMA complete
    scanline_dma_complete_irqs_enabled();

    So both of these appear to be something to do with the timing state machine, which is SM3. These are enabled from the function scanvideo_timing_enable():

    pio_set_irq0_source_mask_enabled(video_pio,
    (1u << pis_interrupt0) // SM IRQ flag 0 int
    | (1u << pis_interrupt1), // SM IRQ flag 1 int
    true);
    pio_set_irq1_source_enabled(video_pio,
    pis_sm0_tx_fifo_not_full
    + PICO_SCANVIDEO_TIMING_SM,
    true);
    irq_set_mask_enabled((1u << PIO0_IRQ_0)
    | (1u << PIO0_IRQ_1)
    #if !PICO_SCANVIDEO_NO_DMA_TRACKING
    | (1u << DMA_IRQ_0)
    #endif
    , enable);

    Confusingly, when the definition of pis_inerrupt0 is looked up, we find it defined as follows:

    pis_interrupt0 = PIO_INTR_SM0_LSB,
    ///< PIO interrupt 0 is raised

    But then we can see that SM0 is not state machine 0, but refers to fields in the IRQ0_INTE and IRQ1_INTE registers, which include SM0 through to SM7. These are the 8 IRQ flags available to all state machines (although actually only SM0-SM3 can be mapped on an RP2040 – all 8 are available on RP2350).

    IRQ 1 is mapped as follows:

    pis_sm0_tx_fifo_not_full = PIO_INTR_SM0_TXNFULL_LSB,
    ///< State machine 0 TX FIFO is not full

    Which is another definition for the PIO interrupt registers. But this time I think it does map onto a state machine itself, hence when initialised it is offset by the TIMING_SM value (which is 3).

    I find this kind of thing a lot when working through the RP SDKs and such confusing inconsistencies are really draining when trying to figure out what is going on in largely undocumented code…

    Anyway, back to those four PIO states for IRQ handling. They map over into the C world using the follownig.

    // 4 possible instructions; index into program below
    enum {
    SET_IRQ_0 = 0u,
    SET_IRQ_1 = 1u,
    SET_IRQ_SCANLINE = 2u,
    CLEAR_IRQ_SCANLINE = 3u,
    };

    And here is an example of it being used:

    // clear scanline irq
    pio_sm_exec(
    video_pio, PICO_SCANVIDEO_TIMING_SM,
    video_htiming_states_program.instructions[CLEAR_IRQ_SCANLINE]
    );

    But that is the only one that seems to be used directly. Most instances of their use seem to be via the timing_encode() macro and a series of redefinitions for _CMD sequences as follows:

    #define TIMING_CYCLE 3u
    #define timing_encode(state, length, pins) \
    ((video_htiming_states_program.instructions[state] & side_set_mask) \
    | (((uint32_t)(length) - TIMING_CYCLE) << 16u) \
    | ((uint32_t)(pins) << 29u))

    #define A_CMD SET_IRQ_0
    #define A_CMD_VBLANK SET_IRQ_1
    #define B1_CMD CLEAR_IRQ_SCANLINE
    #define B2_CMD CLEAR_IRQ_SCANLINE
    #define C_CMD SET_IRQ_SCANLINE
    #define C_CMD_VBLANK CLEAR_IRQ_SCANLINE

    These are then written into the timing programme based on the video timings provided in the various configuration structures.

    Why do I care about all this? In fact why this rather significant diversion in the first place? Well, I’m hoping I can see where the core video frame timing happens to see if I can piggy back on that for my 50Hz timing signal.

    But, at the present time, I’m failing to see it. There appear to be several asynchronous cycles going on passing instructions across to the PIO via the FIFOs or DMA. I think all the intelligence I want to hook into is not happening in real time, but by queuing up the instructions for PIO to run in a time sensitive manner.

    So it may be possible to hook into this somehow for a timing signal (I was hoping I might be able to wait on the IRQs flying around), but at present, I don’t see how to do that.

    Added to the fact I’m not totally sure it is 50Hz anyway (aren’t VGA timings configurable?) then this is probably not going to get me any further.

    Independent PIO 50Hz Generator

    What I’d really like to do is have a PIO state machine configured with the same 5uS pulse every 20mS that I managed to implement in logic. It would be nice to synchronise this to the video scan, but not essential.

    If I make this a pulse HIGH then it can go through a 74HCT14 inverter to invert the signal, buffer the signal, and also switch from the RP2350’s 3V3 IO to a 5V signal. This requires powering the 74HCT14 from the RC bus 5V and not the RP2350 PGA signals.

    From Simplified Pico VGA I noted that the scanvideo library is using PIO 0 with the following state machines:

    • 0 – scanline state machine.
    • 1, 2 – optional additional video planes (if configured).
    • 3 – timing state machine.

    As each PIO only has four state machines, these would appear to all be in use. Except I’m only using a single “video plane” as far as I know, so I might try and hijack SM2 and see how it goes.

    As each state machine has its own clock divisor, I should be able to configure it to run at something useful. If I want a pulse, the simplest way to implement it would be as follows (see “Hello PIO” from PIO on the Raspberry Pi Pico – Part 2):

    .wrap_target
    set pins, 1 // 1 cycle
    set pins, 0 [30] // 31 cycles
    .wrap

    This has the GPIO HIGH for 1 cycle and LOW for 31 so the pulse width is dependent on the number of waits I can configure. The maximum delay per instruction is [31]. There is a balance between the number of “units” to divide the 20mS period into and the pulse width to use. My original RC2014 50 Hz Interrupt Source used a ~5uS pulse, which meant dividing into 4096 units.

    I would ideally be able to reproduce this with something like the following:

    .wrap_target
    set pins, 1 // 1st cycle on
    set pins, 0 // 2nd cycle off
    set x, 4093 // 4096-3 cycles left to loop
    loop:
    jmp x-- loop // 1 cycle each test
    .wrap

    But I can only set a value up to 31, so for anything larger it has to be pulled in from outside. This is the PIO code I’ve ended up with:

    .program int50hz
    pull block ; Grab counter value for delay
    .wrap_target
    set pins, 1 ; [1] One cycle on
    set pins, 0 ; [1] One cycle off
    mov x,osr ; [1] reset counter
    loop:
    jmp x--, loop ; [1] one cycle per loop
    .wrap

    % c-sdk {
    // Loop value less non-looping cycles to total 4096
    #define INT50HZ_LOOP (4095-3)

    static inline void int50hz_program_init(PIO pio, uint sm, uint offset, uint pin) {
    pio_sm_config c = int50hz_program_get_default_config(offset);

    // 50 Hz clock over 4096 cycles
    float clk50hz = (float)clock_get_hz(clk_sys) / (50.0*4096.0);
    sm_config_set_clkdiv(&c, clk50hz);

    // Configure 1 output pin for use with "set"
    sm_config_set_set_pins(&c, pin, 1);
    pio_gpio_init(pio, pin);
    pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
    pio_sm_init(pio, sm, offset, &c);
    pio_sm_set_enabled(pio, sm, true);
    }
    %}

    The state machine will then have to run at a frequency that gives me the 50Hz cycle time, as given in the clk50hz part of the initialisation code.

    The following code can be used to initialise and run the 50 Hz PIO:

    #define Z80_INT_GPIO 37
    #define Z80_INT_PIO pio0
    #define Z80_INT_SM 2

    void int50hz_setup() {
    uint offset = pio_add_program(Z80_INT_PIO, &int50hz_program);
    int50hz_program_init(Z80_INT_PIO, Z80_INT_SM,
    offset, Z80_INT_GPIO);
    pio_sm_put_blocking(Z80_INT_PIO, Z80_INT_SM, INT50HZ_LOOP);
    pio_sm_set_enabled(Z80_INT_PIO, Z80_INT_SM, true);
    }

    The latest version of the code can be found on GitHub here: https://github.com/diyelectromusic/sdemp/tree/main/src/Misc/PicoRC2014Video

    Hardware

    The output is currently a HIGH 3V3 pulse so needs to go through a buffer. If it goes through a 74HCT14 (HCT not HC) powered from the RC2014 VCC bus signal then it acts as a buffer, an inverter, and voltage converter from 3V3 to 5V.

    In the final circuit I’ll have the /INT connection via a jumper to allow this to be optional. No other circuitry is required.

    Adding this to my RC2014 Spectrum Video card is a relatively simple thing to do.

    Updated RC2014 Spectrum Video PCB

    In V1 of the PCB /INT is directly connected to GPIO 33. In V2 of the PCB /INT is now connected via the buffer and a jumper.

    Here is an updated schematic and boad diagram.

    Updates:

    • Addition of the 74HCT14 between GPIO 33 and /INT.
    • Addition of two optional LEDs and resistors on GPIO 37 and 39.

    The updated PCB files can be found on GitHub here: https://github.com/diyelectromusic/sdemp_pcbs/tree/main/RC2014/RC2014-PGA2350%20-%20VGA

    Conclusion

    This has worked out really well. I have some new PCB designs now in the post, which I’ll confirm work ok and then update here, but I’ve tried everything with a slightly modified V1 PCB and it all seems to work well.

    It is a great simplification to have the video card doing the 50Hz interrupt too.

    Kevin

    #pcb #pga2350 #rc2014 #vga #zxSpectrum
  19. Words of Power: Logos (2026) by 7TowersoftGames for ZX Spectrum. A verbal duel where you type a better phrase than your opponent before the clock runs out. Rare letters and linked chains score big, but replaying your best words costs you.

    🎁 7towersoftgames.itch.io/words-

    #homebrew #zxspectrum #retro #game #wordsofpowerlogos

  20. Single Dragon (2025) by JuanGM for ZX Spectrum. The last dragon of Beardopolis faces magical springs that snuff out his flame. Dodge traps, outwit foes, and quench every spring before they quench you. 🔥💧

    🎁 jgajete.itch.io/single-dragon

    #homebrew #zxspectrum #retro #game #singledragon

  21. Gauntlet Online:

    Online multiplayer fork, JavaScript port. A single-player browser client, a standalone C++ Windows server, and online play for up to 4 players:

    github.com/cookertron/Gauntlet

    #zxspectrum #retrogaming

  22. Nodes of Yesod (title speech) by Fred Gray

    ./n/NodesOfYesod.ay?#0
    Title: Nodes of Yesod (title speech)
    Author: Fred Gray
    #ZXSpectrum
    #chiptune

  23. Ready64 nos trae Proton Keep, un juego de plataformas para #ZXSpectrum y acción que nos mete en los pasillos de un castillo lleno de bichos que hay que ir despachando
    👉 pulsayjuega.es/2026/09/03/un-c 🕹️ #retrogaming

  24. Ready64 nos trae Proton Keep, un juego de plataformas y acción para #ZXSpectrum que nos mete en los pasillos de un castillo lleno de bichos que hay que ir despachando
    👉 pulsayjuega.es/2026/09/03/un-c 🕹️ #retrogaming

  25. Ready64 nos trae Proton Keep, un juego de plataformas para #ZXSpectrum y acción que nos mete en los pasillos de un castillo lleno de bichos que hay que ir despachando
    👉 pulsayjuega.es/2026/09/03/un-c 🕹️ #retrogaming

  26. Ready64 nos trae Proton Keep, un juego de plataformas para #ZXSpectrum y acción que nos mete en los pasillos de un castillo lleno de bichos que hay que ir despachando
    👉 pulsayjuega.es/2026/09/03/un-c 🕹️ #retrogaming

  27. Ready64 nos trae Proton Keep, un juego de plataformas para #ZXSpectrum y acción que nos mete en los pasillos de un castillo lleno de bichos que hay que ir despachando
    👉 pulsayjuega.es/2026/09/03/un-c 🕹️ #retrogaming

  28. The ZX Spectrum Introduction Manual, cover art by John Harris, 1982

    This was the same year Tron was released, some ideas were probably already floating in the air. Neuromancer would follow in 1984, and Max Headroom in 1985.

    #cyberpunk #ZXSpectrum #Spectrum #speccy #retrocomputing

  29. The Stone (2026) by Animated ALex for ZX Spectrum. Falling stones speed up as you shoot them. Can you crack the 42 point high score? Simple, tense, pure arcade pressure.

    🎁 animatedal.itch.io/the-stone

    #homebrew #zxspectrum #retro #game #thestone

  30. The Stone (2026) by Animated ALex for ZX Spectrum. Falling stones speed up as you shoot them. Can you crack the 42 point high score? Simple, tense, pure arcade pressure.

    🎁 animatedal.itch.io/the-stone

    #homebrew #zxspectrum #retro #game #thestone

  31. The Stone (2026) by Animated ALex for ZX Spectrum. Falling stones speed up as you shoot them. Can you crack the 42 point high score? Simple, tense, pure arcade pressure.

    🎁 animatedal.itch.io/the-stone

    #homebrew #zxspectrum #retro #game #thestone

  32. The Stone (2026) by Animated ALex for ZX Spectrum. Falling stones speed up as you shoot them. Can you crack the 42 point high score? Simple, tense, pure arcade pressure.

    🎁 animatedal.itch.io/the-stone

    #homebrew #zxspectrum #retro #game #thestone

  33. The Stone (2026) by Animated ALex for ZX Spectrum. Falling stones speed up as you shoot them. Can you crack the 42 point high score? Simple, tense, pure arcade pressure.

    🎁 animatedal.itch.io/the-stone

    #homebrew #zxspectrum #retro #game #thestone

  34. Sword Slayer (128k title) by Mike Brown & Andy Severn

    ./s/SwordSlayer.ay
    Title: Sword Slayer (128k title)
    Author: Mike Brown & Andy Severn
    #ZXSpectrum
    #chiptune

  35. Sword Slayer (128k title) by Mike Brown & Andy Severn

    ./s/SwordSlayer.ay
    Title: Sword Slayer (128k title)
    Author: Mike Brown & Andy Severn
    #ZXSpectrum
    #chiptune

  36. Sword Slayer (128k title) by Mike Brown & Andy Severn

    ./s/SwordSlayer.ay
    Title: Sword Slayer (128k title)
    Author: Mike Brown & Andy Severn
    #ZXSpectrum
    #chiptune

  37. Sword Slayer (128k title) by Mike Brown & Andy Severn

    ./s/SwordSlayer.ay
    Title: Sword Slayer (128k title)
    Author: Mike Brown & Andy Severn
    #ZXSpectrum
    #chiptune

  38. Sword Slayer (128k title) by Mike Brown & Andy Severn

    ./s/SwordSlayer.ay
    Title: Sword Slayer (128k title)
    Author: Mike Brown & Andy Severn
    #ZXSpectrum
    #chiptune

  39. CaliRun (2026) by Adam Bient for ZX Spectrum. An OutRun-inspired racer built in Sinclair BASIC, with a Z80 machine code scroll routine packed into DATA statements for that LCD-style colour-only look. 🏎️🌴

    🎁 adambient.itch.io/calirun

    #homebrew #zxspectrum #retro #game #calirun

  40. CaliRun (2026) by Adam Bient for ZX Spectrum. An OutRun-inspired racer built in Sinclair BASIC, with a Z80 machine code scroll routine packed into DATA statements for that LCD-style colour-only look. 🏎️🌴

    🎁 adambient.itch.io/calirun

    #homebrew #zxspectrum #retro #game #calirun