Let's make a Pi Pico 2 powered video card.
-
@gloriouscow With your graphics card, all that's needed is an Pico 8086 emulator and you've got an "Oops, all Pico!" IBM PC

@Kroc Don't give me more ideas
-
@Kroc Don't give me more ideas
@Kroc There's already a Teensy 8088 emulator so i'm sure its possible
-
This gives you an rp-hal project, and GlyphBlaster is currently written against Embassy, and I don't really feel like rewriting all the overclocking code, so I'm just going to switch this to Embassy too and copy-paste that stuff.
I would like to thank FreddyV of PicoMEM for giving me the tips on how to stably overclock a Pico. It requires tweaking the on-board flash timings in a way I never would have figured out for myself.
The key do doing this in Embassy is this bit:
const PICO_SYS_CLOCK_HZ: u32 = 300_000_000;
const FLASH_QMI_TIMING_UPDATE_THRESHOLD_HZ: u32 = 280_000_000;
const FLASH_QMI_TIMING_HIGH_SPEED_HZ: u32 = 380_000_000;
const OVERCLOCK_FLASH_QMI_CLKDIV: u8 = if PICO_SYS_CLOCK_HZ > FLASH_QMI_TIMING_HIGH_SPEED_HZ {
4
} else {
3
};
const OVERCLOCK_FLASH_QMI_RXDELAY: u8 = if PICO_SYS_CLOCK_HZ > FLASH_QMI_TIMING_HIGH_SPEED_HZ {
4
} else {
3
};
//...
let timing = embassy_rp::pac::QMI.mem(0).timing();
timing.modify(|w| {
w.set_clkdiv(OVERCLOCK_FLASH_QMI_CLKDIV);
w.set_rxdelay(OVERCLOCK_FLASH_QMI_RXDELAY);
});I'll use GPIO16 for this which conveniently puts it on the top right corner of the Pico 2 board.
Our PIO program is stupid simple:
let clock_program = pio_asm!(
".wrap_target",
"set pins, 1 [10]",
"set pins, 0 [10]",
".wrap"
);.wrap_targetis just a standard label for the PIO loop, which will be restarted at the end with.wrap. The value in brackets is how many cycles to spin - the set itself takes one cycle, then we spin for 10 after. This should give us the 11 cycles on, 11 cycles off behavior we want.'pins' here just targets GPIO16, via this:
let clock_pin = pio1.common.make_pio_pin(p.PIN_16);
clock_sm_config.set_set_pins(&[&clock_pin]); -
I'll use GPIO16 for this which conveniently puts it on the top right corner of the Pico 2 board.
Our PIO program is stupid simple:
let clock_program = pio_asm!(
".wrap_target",
"set pins, 1 [10]",
"set pins, 0 [10]",
".wrap"
);.wrap_targetis just a standard label for the PIO loop, which will be restarted at the end with.wrap. The value in brackets is how many cycles to spin - the set itself takes one cycle, then we spin for 10 after. This should give us the 11 cycles on, 11 cycles off behavior we want.'pins' here just targets GPIO16, via this:
let clock_pin = pio1.common.make_pio_pin(p.PIN_16);
clock_sm_config.set_set_pins(&[&clock_pin]);PIO is the secret sauce that makes Picos so good at interfacing with retro hardware. They allow you to react to pin changes instantly, and read and write busses, doing the sort of high speed bus interactions that were normally the exclusive purview of FPGAs.
-
PIO is the secret sauce that makes Picos so good at interfacing with retro hardware. They allow you to react to pin changes instantly, and read and write busses, doing the sort of high speed bus interactions that were normally the exclusive purview of FPGAs.
All in all, not too shabby for a $5 board. Let's see how our clock looks, right after I unbag this and hook it up.

-
All in all, not too shabby for a $5 board. Let's see how our clock looks, right after I unbag this and hook it up.

14.318MHz, baby.
Believe it or not, a sawtooth clock isn't that hideous. This is pretty much what the OSC pin looks like for realsies.

-
14.318MHz, baby.
Believe it or not, a sawtooth clock isn't that hideous. This is pretty much what the OSC pin looks like for realsies.

@gloriouscow Could that be a probe issue? Not all probes go up that high in frequency.
-
@gloriouscow Could that be a probe issue? Not all probes go up that high in frequency.
@casandro Nah, I've gotten a clean read from a 66MHz crystal on this probe from a 386 motherboard. This is probably a limitation of the Pico's GPIO drive strength. I'm not sure if that's something you can configure on a Pico, you can on an STM32, although I usually don't as it can produce overshoots which ends up being worse to deal with.
-
@casandro Nah, I've gotten a clean read from a 66MHz crystal on this probe from a 386 motherboard. This is probably a limitation of the Pico's GPIO drive strength. I'm not sure if that's something you can configure on a Pico, you can on an STM32, although I usually don't as it can produce overshoots which ends up being worse to deal with.
@casandro The real CGA also cleans up the OSC pin by immediately passing it through an 74LS04 which we will probably also do.
-
I'll use GPIO16 for this which conveniently puts it on the top right corner of the Pico 2 board.
Our PIO program is stupid simple:
let clock_program = pio_asm!(
".wrap_target",
"set pins, 1 [10]",
"set pins, 0 [10]",
".wrap"
);.wrap_targetis just a standard label for the PIO loop, which will be restarted at the end with.wrap. The value in brackets is how many cycles to spin - the set itself takes one cycle, then we spin for 10 after. This should give us the 11 cycles on, 11 cycles off behavior we want.'pins' here just targets GPIO16, via this:
let clock_pin = pio1.common.make_pio_pin(p.PIN_16);
clock_sm_config.set_set_pins(&[&clock_pin]);@gloriouscow the 2350 also has an extremely flexible clock tree for micro; each off the gpouts has an individual int/frac divider with duty cycle correction and coarse phase adjustment
The PLL can also run up to 1600MHz (don't think any of the IO buffers can manage that though) and you can input an external clock to it (I think this is what you're doing)
-
14.318MHz, baby.
Believe it or not, a sawtooth clock isn't that hideous. This is pretty much what the OSC pin looks like for realsies.

It's actually kind of silly how close we already are to outputting something on a real CGA monitor.
We could drive two additional PIO state machines with divisors to control HSYNC and VSYNC.
Now, if you just output color all the time, you have no idea whether your picture is actually synchronized - the monitor will just keep the beam on all the time, so if you just say, emit magenta forever, you'll just see a solid magenta screen even if the monitor has no vertical hold.
-
It's actually kind of silly how close we already are to outputting something on a real CGA monitor.
We could drive two additional PIO state machines with divisors to control HSYNC and VSYNC.
Now, if you just output color all the time, you have no idea whether your picture is actually synchronized - the monitor will just keep the beam on all the time, so if you just say, emit magenta forever, you'll just see a solid magenta screen even if the monitor has no vertical hold.
So we're going to have a color latch like the CGA does.
This is a 74LS174 flip-flop, that is fed our generated colors and is clocked by /OSC.
The CGA doesn't use the 174's clear input, but we can - another GPIO output of the Pico should be able to pull that low to blank the screen, I think. Having a vertical blanking area will let us tell if we have vertical hold.
-
So we're going to have a color latch like the CGA does.
This is a 74LS174 flip-flop, that is fed our generated colors and is clocked by /OSC.
The CGA doesn't use the 174's clear input, but we can - another GPIO output of the Pico should be able to pull that low to blank the screen, I think. Having a vertical blanking area will let us tell if we have vertical hold.
I can't overstate how useful it is to have a working digital simulation of the thing you are intending to make.
-
@gloriouscow the 2350 also has an extremely flexible clock tree for micro; each off the gpouts has an individual int/frac divider with duty cycle correction and coarse phase adjustment
The PLL can also run up to 1600MHz (don't think any of the IO buffers can manage that though) and you can input an external clock to it (I think this is what you're doing)
@ldcd The plan is to drive the ISA board version off OSC, yeah. Can you give a Pico 2 board an external oscillator?
-
@ldcd The plan is to drive the ISA board version off OSC, yeah. Can you give a Pico 2 board an external oscillator?
@gloriouscow you can clock the system off of GPIN yes, although I don't think you can clock the PLLs off of it.
(So like if you had an NTSC freq input you could clock a PIO SM off of it or the CPU off of it but you couldn't generate a multiple of it with the PLL unless you remove the crystal and solder a lead to one of the pads)
-
I can't overstate how useful it is to have a working digital simulation of the thing you are intending to make.
Time to start breadboarding!

-
@gloriouscow you can clock the system off of GPIN yes, although I don't think you can clock the PLLs off of it.
(So like if you had an NTSC freq input you could clock a PIO SM off of it or the CPU off of it but you couldn't generate a multiple of it with the PLL unless you remove the crystal and solder a lead to one of the pads)
@gloriouscow I wish you could clock them off of gpclk0/1 but that would have probably been a pain to route
-
Time to start breadboarding!

You know, I just thought of something - the same technique I use in GlyphBlaster of formatting video frames as 912x262 would work for a static test. Except we expand it to 8 bits. That's 238KB which will still fit in the Pico's RAM.
The lower nibble will drive the RGBI outputs, while two bits in the upper nibble can directly drive HSYNC and VSYNC. We just need to center a 640x200 image in a 912x262 black bitmap, then just paint the sync periods in the overscan.
Screw boring test patterns, lets go directly for graphics!
-
You know, I just thought of something - the same technique I use in GlyphBlaster of formatting video frames as 912x262 would work for a static test. Except we expand it to 8 bits. That's 238KB which will still fit in the Pico's RAM.
The lower nibble will drive the RGBI outputs, while two bits in the upper nibble can directly drive HSYNC and VSYNC. We just need to center a 640x200 image in a 912x262 black bitmap, then just paint the sync periods in the overscan.
Screw boring test patterns, lets go directly for graphics!
we can represent this with a 8-bit palette in Aesprite. We have our normal RGBI palette, then we have hsync (green), vsync (blue) and hsync-in-vsync (cyan).
Now I just need a good picture to use. The easiest thing is to take a 320x200 4bpp image from Tandy or EGA graphics, and horizontally stretch it 2x.

-
we can represent this with a 8-bit palette in Aesprite. We have our normal RGBI palette, then we have hsync (green), vsync (blue) and hsync-in-vsync (cyan).
Now I just need a good picture to use. The easiest thing is to take a 320x200 4bpp image from Tandy or EGA graphics, and horizontally stretch it 2x.

There sure is a lot of 320x200 pornography.