I think I've finally figured out why the Atari picture frame "app" wasn't quite working properly.

Here is all my theory and working out. I think it might be as PAL is slightly slower to scan relative to the RPi Pico compared to NTSC.

Once again, thanks to Nick Bild for putting it all together in the first place.

https://emalliab.wordpress.com/2025/12/18/atari-2600-cartridge-emulation-part-2/

#Atari #Atari2600 #RaspberryPiPico

Atari 2600 Cartridge Emulation – Part 2

Following on from my previous post: Atari 2600 Cartridge Emulation in this post I start to look at how to build a custom Atari 2600 ROM itself.

I’ve spent a bit of time pouring over the assembler for the Atari photo frame app and I can’t see any obvious places where the byte ordering might go awry, so I’ve decided to rebuild the ROM itself from scratch just to be sure that what is in the binary file is what I’m expecting.

It would be nice to solve the issue I was seeing, but this will also hopefully serve as a useful introduction to writing and building code for the Atari 2600.

Spoilers: I think it was the timing between the PAL Atari and the Pico’s ROM routines. Adjusting the Pico code seems to solve it.

Atari 2600 Development

There is a brilliant introductory tutorial for Atari 2600 development here: https://www.randomterrain.com/atari-2600-memories-tutorial-andrew-davie-01.html

The basic steps are:

  • Install the tool chain and programming environment. This is based on the DASM cross-platform assembler.
  • Install an emulator – Stella is the emulator of choice.
  • Grab any relevant documentation and references:
    • Stella Programmer’s Guide
    • AtariAge
  • Build and load the code.

Installing the Toolchain

I’m using my Ubuntu installation, so after a quick update of the core libraries, I installed DASM.

$ sudo apt-get update
$ sudo apt-get upgrade
$ sudo apt-get install dasm
$ wget https://raw.githubusercontent.com/johnidm/asm-atari-2600/master/vcs.h
$ wget https://raw.githubusercontent.com/johnidm/asm-atari-2600/master/macro.h

As well as installing DASM the Atari 2600 environment and macro files are required from here: https://github.com/johnidm/asm-atari-2600

At this point, to build an Atari 2600 ROM binary file requires the following:

$ dasm myfile.asm -f3 -omyfile.bin -lmyfile.lst

To get this into picoROM then requires some additional processing. There is a python script that will take a binary file and churn out a series of rom_contents[n] = val; statements which can then be pasted into the pico_rom.c file.

$ python3 translate_bin2rom.py myfile.bin > newrom.c

The contents of newrom.c will now have statements for rom_contents[0] through to rom_contents[4095] which need to replace those already in setup_rom_contents().

Return to the Photo Frame

I really wanted to figure out what was going on with the photo frame application from part 1. My initial suspicions are that it might be related to the PAL/NTSC difference, so that is the starting point.

What are the main differences? The refresh frame rate and number of lines scanned per frame:

PALNTSCFrame Rate50 Hz60 HzNumber of Scan Lines625525

I know from having read “Racing the Beam” how critical timing is to driving the display and interspersing logic and display code. The “Atari 2600 Programming for Newbies” Guide states it thus:

“But from the ‘2600 point of view, the difference in frequency (50Hz vs. 60Hz) and resolution (625 scanlines vs. 525 scanlines) is important—very important—because it is the PROGRAMMER who has to control the data going to the TV. It is not done by the ‘2600 (!!)—the ‘2600 only generates a signal for a single scanline.”

“This is completely at odds with how all other consoles work, and what makes programming the ‘2600 so much ‘fun’. Not only does the programmer have to worry about game mechanics—but he or she also has to worry about what the TV is doing (for example, what scanline it is drawing, and when it needs to start a new image, etc.).”

Another complication is that the displays are interlaced, that is half the scanlines are displayed first, then the rest. By displaying every other line, persistence of vision means that the fact that it takes time to go from top to bottom for each frame is largely hidden from view.

But the consequence of this is that the actual display frame rate, when interlacing is taken into account, is 25 Hz for PAL and 30 Hz for NTSC.

The TIA in the 2600 runs with a pixel clock of 3.58MHz and the 6502 runs at 1/3 that, so there is one CPU cycle per three pixels. I think this is universal across both PAL and NTSC. According to this guide there are 228 pixel clock counts for a horizontal line, which I believe means 1 line will take around 64uS.

A full NTSC frame would thus be around 262*64 = 16.8 mS which gives us the 60Hz refresh rate. A full PAL frame would be around 312*64 = 19.9mS which gives us the 50Hz refresh rate.

There are many tricks associated with squeezing the most out of the hardware and a key technique relates to changing the registers that support graphics processing “on the fly” – whilst the TV scan is actually happening – hence “Racing the Beam” in the title of the book.

The 48-Pixel Trick

One of those tricks used by the photo frame application is the “48 pixel trick” which is described in the following:

I don’t know enough about 2600 programming yet to describe it in detail, but I believe the key idea is something like the following:

  • It takes time to update the graphics registers to write to the display. There are registers for “player 0” and “player 1” sprites (not to be confused with “play field” registers).
  • But there are some tricks to “preload” the information and have it queued up ready to display quickly using either CPU registers or mechanisms built into the Atari’s TIA device.
  • There are some techniques for interleaving the P0/P1 data and repeating it across the scanline.
  • Key to this are the following:
    • VDEL – “vertical delay” which enables a kind of “shadow” graphics register as I understand things.
    • NUSIZ – “Number/Size Player/Missile” which can be used to indicate a number of copies of each sprite.

There is a great description from the Retrochallenge write-up:

“So, finally, we come to meet the famous 6-Digits Score Display, also known as Big Sprites, or 48-Pixel Display. It’s the best we can do in “high” resolution on the Atari 2600: 48 pixels in a row, composed of the two player sprites (8 pixels each) replicated 3 times at an offset of 16 pixels (“close”). The two sprites will be mended together, forming a continous strip of 48 pixels (8 × 6). Nothing out of the ordinary, since the VCS and its TIA chip provide for that. Our job is now to change the bit-patterns for the two sprites on the fly, 4 times, just-in-time, with perfect cycle count.”

It goes on to show how unfortunately updating the registers is just slightly too long compared to the “beam time”, but by clever use of the VDEL and the “shadow” registers, it can all be speeded up by preloading as much as possible.

There is one confusing property to note. Writing to GPR0 will trigger an update to the display from the shadow register for GPR1 and vice versa. this creates for a very confused sequence of updates, but does allow for time-critical updating of the display in sequence with “the beam”.

So looking at the photo frame display code, we can add the following annotations to the main display loop.

; VBLANK
WaitVBlank
lda INTIM
bne WaitVBlank
sta WSYNC ; Wait for the next horizontal sync
sta VBLANK ; Do vertical blanking period

ldy #HEIGHT
sty ImageHeightCnt ; Initialises and stores the image height counter
BigGraphicLoop ;Cycles(sum)[Pixels]
sta WSYNC ; 3 (0) [0] ; Hor Sync starts the process
lda sprite0 ; 4 (4) [12] ; We have 68 clock counts before things display
sta GRP0 ; 3 (7) [21] ; byte0 -> GRP0
lda sprite0 ; 4 (11) [33]
sta GRP1 ; 3 (14) [42] ; byte1 -> GRP1; byte0 -> GRP0A
lda sprite0 ; 4 (18) [54]
sta GRP0 ; 3 (21) [63] ; byte2 -> GRP0; byte1 -> GRP1A
lda sprite0 ; 4 (25*) [75]
tax ; 2 (27) [81] ; byte3 -> X
lda sprite0 ; 4 (31) [93]
sta Temp ; 3 (34) [102] ; byte4 -> Temp
lda sprite0 ; 4 (38) [114] ; byte5 -> A
ldy Temp ; 3 (41) [123] ; byte4 -> Y; at start of px 123 GRP0A (byte0) -> TV

stx GRP1 ; 3 (44) [132] ; byte3 -> GRP1; byte2 -> GRP0A; GRP1A (byte1) -> TV
sty GRP0 ; 3 (47) [141] ; byte4 -> GRP0; byte3 -> GRP1A; GRP0A (byte2) -> TV
sta GRP1 ; 3 (50) [150] ; byte5 -> GRP1; byte4 -> GRP0A; GRP1A (byte3) -> TV
sta GRP0 ; 3 (53) [159] ; dummy -> GRP0; byte5 -> GRP1A; GRP0A (byte4) -> TV
dec ImageHeightCnt ; 5 (58) [174] ; ; GRP1A (byte5) -> TV
ldy ImageHeightCnt ; 3 (61) [183]
bpl BigGraphicLoop ; 2/3 (64) [192]

lda #0 ; Clear registers
sta GRP1
sta GRP0
sta GRP1

ldx #(192-HEIGHT) ; Skip required number of lines for a full frame
VSLoop ; 192 for NTSC, 242 for PAL
sta WSYNC
dex
bne VSLoop

SetupOS ; Overscan (bottom of the display)
lda #36
sta TIM64T

; Overscan
WaitOverscan
lda INTIM
bne WaitOverscan

Why does the TV display only start at pixel position 123? That is determined by the call to SetHorizPos, as will be described next.

One other point of note. This code reads out 6 bytes, giving us the 48 pixels. But the image is a 64 pixel wide image. To solve this, a second graphics loop is performed on the interlaced scan for the final 16 pixels.

Setting the Horizontal Position

The SetHorizPos function needs a little explanation.

From the Stella Programmers Guide, section 7.0:

“The horizontal position of each object is set by writing to its associated reset register (RESP0, RESP1, RESM0, RESM1, RESBL) which are all “strobe” registers (they trigger their function as soon as they are addressed). That causes the object to be positioned wherever the electron bean was in its sweep across the screen when the register was reset. for example, if the electron beam was 60 color clocks into a scan line when RESP0 was written to, player 0 would be positioned 60 color clocks “in” on the next scan line. Whether or not P0 is actually drawn on the screen is a function of the data in the GP0 register, but if it were drawn, it would show up at 60. Resets to these registers anywhere during horizontal blanking will position objects at the left edge of the screen (color clock 0). Since there are 3 color clocks per machine cycle, and it can take up to 5 machine cycles to write the register, the programmer is confined to positioning the objects at 15 color clock intervals across the screen. This “course” positioning is “fine tuned” by the Horizontal Motion, explained in section 8.0.”

This is what is implemented in the SetHorizPos function. There is a great discussion of how it works here: https://forums.atariage.com/topic/308513-a-working-horizontal-positioning-routine/ and more detailed explanation here: https://bumbershootsoft.wordpress.com/2018/08/30/an-arbitrary-sprite-positioning-routine-for-the-atari-2600/

On entry, A = required x-coordinate and X is the reset register to work with where X=0 for RESP0, X=1 for RESP1.

SetHorizPos
sta WSYNC ; start a new line
bit 0 ; waste 3 cycles
sec ; set carry flag
DivideLoop
sbc #15 ; subtract 15
bcs DivideLoop ; branch until negative
eor #7 ; calculate fine offset
asl
asl
asl
asl
sta RESP0,x ; fix coarse position
sta HMP0,x ; set fine offset
rts ; return to caller

The basic idea is to wait until the scanning reaches the required point and then use the RESPx register to say “put sprite here”. The minimum loop for scanning will take up 15 pixels of time, which is also the time taken to subtract 15 from the required value and continually branch until negative, hence the use of the otherwise apparently magic number 15 above.

As the granularity is fixed at 15 pixels, the HMPx registers are used for further fine adjustment.

This is all spelled out in the Newbies tutorial here: https://www.randomterrain.com/atari-2600-memories-tutorial-andrew-davie-22.html

Vertical Blank Timing

One other trick for getting the vertical timing correct is to use the TIMxxx and INTIM registers. The TIMxxx registers are timers which can be checked using INTIM. TIM64T counts 64 cycle blocks and is used here as follows:

VERTICAL_SYNC
lda #44
sta TIM64T

... code ...

WaitVBlank
lda INTIM
bne WaitVBlank

... next block ...

This (and similar other sections) will ensure the next block of code is properly synchronised to the vertical scan requirements.

In this case, it is accounting for the 37 scanlines that form the top vertical blank:

  • 37 x 76 CPU instructions = 2812 CPU cycles
  • 2812 / 64 ~= 44

Similar code can work for the bottom overscan of 30 scanlines too:

  • 30 x 76 = 2280 CPU cycles
  • 2280 / 64 ~= 35.5

36 is used with TIM64T for the overscan.

Overall Structure

Putting everything together, the main code has the following structure:

; Constants and variables
HEIGHT = 84 + 1
Temp
ImageHeightCnt

; Initialise
CLEAN_START

; Start of each frame
VERTICAL_SYNC
Set horizontal positions for P0 to 55 and P1 to 63 (55+8)
Set VDEL, NUSIZ, COLUP for P0, P1

Vertical Blanking

Run main graphic loop for each line of the display
Read 6 values per line for display (pixels 0 to 47)

Overscan timing

VERTICAL_SYNC
Set horizontal positions for P0 to 103 and P1 to 111
Set VDEL, NUSIZ, COLUP for P0, P1

Vertical Blanking for interlaced frame
Interlaced frame has a second main graphics loop
Read 2 values per line for display (pixels 48 to 63)

Overscan timing

Repeat

Back to the Problem

So with this new understanding has the problem been solved? Nope. I’ve tried various things to adjust the timings, set the NTSC/PAL numbers of lines, and adjusting the sequencing of the registers as per the examples.

Nothing. Also running it in the Stella emulator seems to show that it ought to be working fine, but of course I can’t (easily) simulate the Pico changing a byte on every read of the sprite0 location.

So at this point I took a bit of a closer look at the Pico code which is relatively straight forward. It has the following basic structure:

main () {
// Initialises ROM contents
// Set up GPIO
// Overclock the Pico
while (true) {
put_data_on_bus(get_requested_address());
}
}

get_requested_address() {
return gpio_get_all() & 32767;
}

void put_data_on_bus(int address) {
IF address = special graphics byte, then return pixel data
ELSE return the value from the ROM contents
}

ROM Contents:
[0000-4093] = ROM Contents
[5000-5671] = Picture 1
[5672-6343] = Picture 2
etc

I decided to add some marker values at the start of the image:

rom_contents[5000] = 0x81;
rom_contents[5006] = 0xa1;
rom_contents[5012] = 0xc1;
rom_contents[5018] = 0xf1;

Then it was possible to attempt to see what was going on.

We can see that part way along the top line the 0xA1 (bin 1010 0001) marker can be seen, followed by the 0xC1 and 0xF1 markers, but the first 0x81 marker is missing. This implies to me that the code has somehow skipped the first byte of the image and then all subsequent bytes are 1 position out.

I think the issue could be related to the timing of the updating code which looks for the requested address changing from general ROM access to the special address 0xF00 (which actually comes out as 0xFF00 in the assembler, but is 0xF00 in the C code. The cartridge only has 12 bits as significant for the 2600 and they start at 0xF000). When the change is detected, i.e. the first write is being performed, the data value is sent out and then the index into the picture changes.

if (address == 3840) {
gpio_put_masked(8355840, rom_contents[img_pos] << 15);
if (last_address != 3840) {
img_pos++;
}

I think this means that there is only one read that results in the image data being written before it changes, so what I think might be happening is something like the following:

Atari Address Pico Scanning
ROM address Returns ROM code
ROM address Returns ROM code
FF00 Returns byte N from picture and updates picture index
FF00 Returns byte N+1
FF00 Returns byte N+1
ROM address Returns ROM code again

I don’t know how much of a problem this is, but I can see how the timing might be quite brittle if it does work.

I’ve changed the logic of the code to the following:

Setup:
img_pos = 5000
img_rom = rom_contents[img_pos]

Scanning Loop:
IF (address == 0xF00) THEN
Return img_rom value as the image data
ELSE IF (last address == 0xF00 && address != 0xF00) THEN
After last picture read, update index and store new img_rom value for next time
ELSE
Return ROM value

It is not perfect but when it all cycles round everything eventually seems fine. There does often seem to be one spurious read on power up which can put the whole first sequence out by a byte. In the end, I initialise the first img_pos pointer to 4999 rather than 5000. Once everything gets going it seems to work ok.

It is interesting that the interlacing is so visible on this modern TV. I can see why people seek out CRTs for their retro gear! Anyway, now the full first byte can be seen to be displayed correctly and then everything else follows.

I still don’t know if the issue is related to the PAL vs NTSC thing. I initially wondered that if the speed of the 2600 relative to the Pico is different, which I thought it would be when comparing 50Hz scanning to 60Hz, then maybe that means the original code isn’t so robust. Maybe at 60Hz the single address read is fast enough to get the right data byte, but at 50Hz it is slightly slower, meaning it is the changed byte that gets read instead.

But then I realised that the horizontal timing is the same for each, it is only the time it takes for the number of vertical lines that is different, so actually I don’t know what is going on. Maybe the clock in my old 2600 is slightly off. Or maybe the Pico isn’t overclocking reliably.

Either way, it seems a lot more robust for me with the update.

I am now wondering if I could add another special address location that could act as a sync between the Pico and 6502 which could be used to correctly signal the start of the frame.

Below are some of the various interim screens I ended up with whilst adjusting the assembler and Pico sequencing.

But I finally have a working picture frame app and have learned a lot about the Atari 2600 in the process.

There is a branch of the original project that contains all my messing around here: https://github.com/diyelectromusic/atari_2600_digital_frame/tree/kevins_learning

Update to the Build Process

One final additional update, I’ve now changed pico_rom.c to take the ROM and image data from two header files that are generated by the two provided python scripts.

The basic build process is now as follows:

  • Use DASM to assemble the code for the Atari ROM.
  • Use translate_bin2rom.py to create pico_rom_contents.h
  • Use read_img.py to create up to four images in pico_rom_images.h
  • Use cmake to create the build environment.
  • Use make to build the final pico_rom.uf2 file for installing on the Pico.

This is all captured in a new build.sh file which builds four sample images from the img/for_display area and all of the above is now in my learning branch in GtHub.

There is one final build step I’ve not looked at – the magic file ‘slower_boot2_padded_checksummed.S” has some hex data in it that is build as part of the original picorom project. I might try to get that over at some point too, so the whole thing will build from source.

I’d also like to find out how to include the above python steps as part of the cmake/make process, but I don’t get on very well with cmake…

At some point I’d like to create an empty “how to build a Pico Atari ROM” project from all the above making it fairly easy to load and run homemade ROMs. There might even be an option for a future PicoW version that would support dynamic loading of a ROM binary file…

Kevin

#0 #15 #36 #44 #7 #atari #atari2600 #HEIGHT #picotari #raspberryPiPico

It's projects like this that give me hope for a new #Psion SSD based around the RP2350.

https://hackaday.com/2025/12/06/emulate-roms-at-12mhz-with-pico2-pio/

#RaspberryPiPico #RetroComputing

Emulate ROMs At 12MHz With Pico2 PIO

Nothing lasts forever, and that includes the ROMs required to make a retrocomputer run. Even worse, what if you’re rolling your own firmware? Period-appropriate EPROMs and their programmers a…

Hackaday

How I Built a Pico W Remote to Trigger My Alexa Routines

https://lemmy.world/post/39821670

How I Built a Pico W Remote to Trigger My Alexa Routines - Lemmy.World

Lemmy

Have the #RaspberryPiPico SDK PDFs vanished from the Raspberry Pi website? I’m getting 404.

Atari 2600 Cartridge Emulation

I’ve been messing around with the Atari 2600 and at some point stumbled across the “Picotari” 2600 cartridge by Nick Bild, which emulates a ROM cartridge for the Atari 2600 using a Raspberry Pi Pico.

You might not recognise the name Nick Bild, but you will be very familiar with some of their projects which seem to blend quirky concepts with some neat engineering solutions. Most of their interesting projects will make it to Hackaday and similar places.

The Picotari was developed to be able to create a pixelated “photo” slideshow on an Atari 2600 turning it into a low-res digital photo frame. One of their recent projects links an LLM into a vintage speech chip to recreate the WOPR talking in War Games. More of their projects here: https://www.youtube.com/channel/UCcUMG56v69cuzsbM0gaSsOQ

There are some details of the Picotari build as part of the project notes on hackaday.io: https://hackaday.io/project/202729-atari-2600-digital-photo-frame/details and it also references another one of their projects: https://github.com/nickbild/picoROM.

Although there are Gerber files provided for the PCB used, and a high level description of the photo frame project, I don’t really have the expertise to follow along fully. I was finding it quite difficult to pin down the actual details of how the project was working, so this is me using that as a basis and guide as I dig into the innards of the Atari 2600 cartridges and documenting what I found out on the way.

The Picotari PCB

The Picotari PCB looks perfect for what I’d like to do, so I decided to see if I could work out what is going on. I started by examining the Gerber files in KiCad’s Gerber viewer. Some points to note about the PCB:

  • A 74LVC245A is used as a level shifter between the Atari’s 5V and the Pico’s 3V3. There are only 8 lines though, so I’ll have to work out which signals they are used for.
  • A 74AC04 hex inverter is used.
  • There are four layers of copper. One of the layers is a ground plane.

KiCad has an option to pull the Gerber layers back into the PCB editor tool, so I did that to make things a little easier to work with. Note that I had to make sure it included all four layers of copper and that they mapped through into sensible layers in the PCB editor too (I ended up with F, In1, In2, B copper layers with In1 as the ground plane).

From this, I can note the following:

  • It looks like only one of the inverters is actually used. All other inputs are connected to the Pico’s VSYS line.
  • The inverter that is in use is connected to the /OE pin of the 74LVC245, so presumably is used to enable the whole ROM somehow.
  • All eight buffers for the level shifter appear to be connected, so presumably these support the eight data lines.
  • GPIO 12, 13 and 14 on the Pico appear to be tied to GND.

From earlier browsing around I noticed that there was a pin definitions file in the PicoROM GitHub repository that maps GPIO to data and address lines as follows:

  • A0-A14 -> GPIO 0 to GPIO 14
  • D0-D7 -> GPIO15 to GPIO 22

And that seems to mirror what is going on here. PicoROM is designed to emulate 27C256 devices, hence requiring 15 address lines: 32K devices require an address space $0000-$7FFF, but with GPIO 12, 13 and 14 fixed to GND that reduces down to 4K.

Mapping all these back onto some existing KiCad footprints and adding some labels gives me the following.

So I can expand on my notes, now noting:

  • As already mentioned GPIO 13, 14, 15 are tied to GND dropping the addressable space down from 32K to 4K.
  • The Atari edge connector is usually labelled D1-D8, so these map to GPIO15-22.
  • A0-A11 do seem to map onto GPIO 0 to GPIO 11.
  • 12 address lines will support addressing 4K devices, with an address space $0000-$0FFF.
  • A12 goes to the inverter, so A12 going HIGH will enable the active LOW signal of the 74LVC245. This puts it address at $1000-$1FFF. If A13-A15 are used then it will also repeat at all higher addresses $3000, $5000, etc.
  • The 8 data lines go through the LVC245 level shifter with D1-D8 mapped to inputs A1-A8; and outputs B1 to B8 mapped onto GPIO 15 to GPIO 22.
  • The LVC245 is powered from the edge connector 5V and DIR is also at 5V.
  • The AC04 inverter is also powered from the edge connector 5V and that also connects to the Pico’s VSYS.

Looking in a bit more detail at the level shifting, I can see from the 74LVC245 datasheet:

With DIR tied HIGH, it would appear that whatever is presented on the A lines should appear on the B lines (at the voltage level of VCC). In fact the Adafruit product page describes it thus:

“We suggest checking out the 74LVC245 datasheet for details but essentially: connect VCC to your logic level you want to convert to (say 3.3V), Ground connects to Ground. Wire OE (output enable) to ground to enable the device and DIR (direction) to VCC. Then digital logic on the A pins up to 5V will appear on the B pins shifted down to the VCC logic.”

This seems the wrong way round for me. I’d have thought that as these are the (to be read by the Atari) data lines, these should be going from lower voltage Pico logic (on the B bus) to higher voltage Atari logic (on the A bus).

What is also curious is that there is no logic shifting on any of the address lines. I would have thought these would be the inputs (from the Pico point of view), but they seem to be running natively from 5V logic levels and are being fed into the Pico.

I do note that the original PicoROM breadboard used three 74LVC245 chips, but couldn’t find a schematic for either build: https://github.com/nickbild/picoROM

On going back to the original project pages, I noticed this:

I’ve not found any details of what the bodge actually was, but I think there is a good chance that the DIR pin is now connected to GND via that bodge wire to reverse the direction of the device. DIR = LOW means go from B (3V3) to A (VCC or 5V). That is quite a tricky bodge though as the pad for that pin of the chip has 4 tracks connected to it linked to VCC…

As I understand things, the LVC245 should run at the “output” logic level, which in this direction is 5V. But the data suggests that ideally VCC would be 1.8V to 3.6V, but it does say it will accept anything up to 5V as an input. The absolute maximum ratings for VCC (for an SN74LVC245 at least) are -0.5V to +6.5V, but the recommended values are as follows:

The point to note is the Input and Output voltages: the input is always 0 to 5V, but the output will be 0 to VCC.

So at this stage in my understanding, it seems like the 74LVC245 would be great for 5V logic in, 3.3V logic out (when powered by VCC=3.3V) but there are probably alternatives for going the other way. But as the absolute max for VCC is still more than 5V, using it in this direction is probably ok, but there aren’t figures in the datasheet for what high-level and low-level look like for VCC=5V. I guess using a 3.3V high for a VCC=5V must work ok though and the output will be at 5V.

That still leaves the puzzle about the levels of the 12 address lines used. It looks like the original PicoROM had level shifting on them all, but for this board the 5V from the atari goes directly into the Pico GPIO pins.

There is some interesting discussion about the 5V tolerance of the RP2040 here: https://www.derekfountain.org/zx_pico_5v.php

The summary is that the Pico can accept 5V as long as it is powered. But until its 3V3 power is active, it should not see anything more than 3.6V. So this is effectively a power-on race condition – will the Pico see 3V3 power before the Atari sends it some data?

At the time of writing, it has just been announced that there is a new version of the RP2530 which, as well as fixing the now well-known bug with the internal pull-down resistors, is also apparently now essentially 5V tolerant. That might be one option if I can get a Pico with the new version of the chip installed.

Or maybe I should be adding my own level shifting on the address lines.

To be honest, at this point I am wondering if it is worth designing my own PCB building on the above knowledge now gained. I’m just not sure I’m up to trying to design a four-layer board yet. It would be a lot to ask to try to route between the Atari connector and a Pico with just two layers I think.

Maybe I’ll just accept that it seemed to work, so just pick out a sacrificial Pico and leave it to take its chance with the potential 5V inputs…

This is my finished build. Note – I added some insulating tape over the DIR pad of the LVC245 and bent the pin from the socket out to allow me to solder a jumper wire to the GND link of the nearby capacitor.

Atari 2600 Cart Details

There is a lot of detail online about the design and implementation of cartridges for the 2600 system. There were developments of the basic system for the 400/800 and later systems, but they all remained essentially backwards compatible with what went before them for the 2600.

References:

Key details:

  • The 6507 CPU has an 8-bit data bus and 13-bit address bus, so can access up to 8K of memory.
  • All data and address pins are present on the cartridge connector.
  • A12 is used to select the cartridge, which occupies the top 4K of the address space.
  • Weirdly, all the documents list the cart address as $F000-$FFFF when really it is (surely) $1000-$1FFF with the entire address space being $0000-$1FFF – hence 8K, using A0 to A12.
    • Update: yes, according to “Making Games for the Atari 2600” by Stevem Hugg, there are 8 equivalent repeats of the memory map: $0000-$1FFF, $2000-$3FFF, through to $E000-$FFFF. By convention a non-bank-switched cart is treated as address $F000. Bank switching can use some of the additional ranges.
  • There are some 2K cartridges. These end up appearing mirrored in the address space at two locations: $F000 and $F800.
  • Larger cartridges use bank switching, but I’m not worrying about that at present.
  • A12 is usually used as the chip select/enable for any cart ROM.

There are some ROM switcher options out there that allow different 2K/4K ROM images to be stored in a larger EEPROM and presented to the Atari depending on the settings of certain jumpers.

Here are a couple of options:

And of course there were a number of “ROM scanners” and switchers back in the day that allowed choosing between different cartridges. There is a great write-up of one of them here: https://www.the-liberator.net/site-files/retro-games/hardware/Atari-2600/atari-2600-marjac-rom-scanner-1983.htm

PicoROM Code

All the code to allow the Pico to respond as a ROM device for the Atari can be found here: https://github.com/nickbild/picoROM. There are a few things to note:

  • The Pico is overclocked to 400MHz, but not on power up which has to be slower to allow access to the flash.
  • The whole of the 32K ROM is read into the Pico’s RAM prior to overclocking.
  • Data is read from the address lines, and written to the data bus in one go using gpio_get_all() and gpio_put_masked().
  • The ROM contents is a set of 32768 rom_contents[idx] = value statements. Well, 4904 statements as it happens for this specific application. These are created by a python script.
  • The sample application is not for an Atari 2600, but for his Vectron 65 6502 based computer.

The best reference for building code for an Atari 2600 is “Dr Boo’s Woodgrain Wizardry” here: https://www.taswegian.com/WoodgrainWizard/tiki-index.php?page=Start-Here

It recommends using the Stella emulator for ease of development. This would get to the point of having a binary ROM image which can then be built into the Pico using the script mentioned above, building using the Pico SDK.

The actual running code on the Pico is pretty straightforward:

void main () {
while (true) {
put_data_on_bus(get_requested_address());
}
}

int get_requested_address() {
// Return only first 15 bits.
return gpio_get_all() & 32767;
}

void put_data_on_bus(int address) {
// gpio mask = 8355840; // i.e.: 11111111000000000000000
// Shift data 15 bits to put it in correct position to match data pin defintion.
gpio_put_masked(8355840, rom_contents[address] << 15);
}

That is basically it. As fast as possible.

Recall that GPIO0 to GPIO14 are the address lines, hence only grabbing the first 15 bits from the gpio_get_all() call. And GPIO15 to GPIO22 are the data lines, hence masking off the correct 8 bits for gpio_put_masked().

Building PicoROM

First I updated my Pico SDK environment by doing a ‘git pull’ in the sdk, extras, examples, and picotool areas. All my pico stuff is in a src/pico area:

~/src/pico$ cd pico-sdk
~/src/pico/pico-sdk$ git pull
~/src/pico/pico-sdk$ cd ../pico-extras
~/src/pico/pico-extras$ git pull
~/src/pico/pico-extras$ cd ../pico-examples
~/src/pico/pico-examples$ git pull
~/src/pico/pico-examples$ cd ../picotool
~/src/pico/picotool$ git pull

Then I cloned the original PicoROM build and the Picotari project, so I had something to work with.

~/src/pico$ mkdir picorom
~/src/pico/picorom$ cd picorom
~/src/pico/picorom$ git clone git@github.com:nickbild/atari_2600_digital_frame.git
~/src/pico/picorom$ git clone git@github.com:nickbild/picoROM.git

Then I tried to build picoROM as a starting point. And failed.

I don’t know if things changed with a more recent SDK or I just don’t know what I’m doing (which is almost a certainty with the Pico SDK – I find it quite impenetrable and don’t use it enough to get past that right now), but in the end this is what I had to do to build picoROM:

  • Remove the artifacts from the original build:
~/src/picorom$ cd picoROM
~/src/picorom/picoROM/picoROM$ rm CMakeCache.txt
~/src/picorom/picoROM/picoROM$ rm Makefile
~/src/picorom/picoROM/picoROM$ rm -rf CMakeFiles

Then I had to recreate some of the basic boilerplate files and entries, which I cribbed in my ignorance, from a previous project.

  • Copy in the main Pico SDK “user project” include stuff:
~/src/picorom/picoROM$ cp ../../pico-sdk/external/pico_sdk_import.cmake .
  • Add some common initialising instructions to CMakeLists.txt
cmake_minimum_required(VERSION 3.13)
include(pico_sdk_import.cmake)
project(picorom LANGUAGE C CXX ASM)
pico_sdk_init()

Note that without detailing that assembly language is required in the project() directive, you’ll get an error something like “CMake Error: Error required internal CMake variable not set, cmake may not be built correctly. Missing variable is: CMAKE_ASM_LINK_EXECUTABLE”.

Create a build area.

~/src/pico/picorom/picoROM$ mkdir build

Then I can finally run cmake and then attempt a build…

~/src/pico/picorom/picoROM$ cd build
~/src/picp/picorom/picoROM$ cmake ..
~/src/pico/picorom/picoROM$ make

This indeed does appear to give me a pico_rom.uf2 file. I can’t test this one as it is an application for his bespoke DIY computer, but now I know I can build the original project, I repeat the above for the Atari picture frame app…

To start with, there is no associated build infrastructure, so I just copy the missing files from the picoROM area and rebuild.

~src/picorom$ cd atari_2600_digital_frame
~src/picorom/atari_2600_digital_frame$ cp ../picoROM/pin_definitions.h .
~src/picorom/atari_2600_digital_frame$ cp ../picoROM/CMakeFiles.txt .
~src/picorom/atari_2600_digital_frame$ cp ../picpROM/slower_boot2_padded_checksummed.S .
~src/picorom/atari_2600_digital_frame$ cp ../../pico-sdk/external/pico_sdk_import.cmake .

The CMakeFiles.txt file needs a minor update to rename rom.c to pico_rom.c. Then I tried a build

~src/picorom/atari_2600_digital_frame$ mkdir build
~src/picorom/atari_2600_digital_frame$ cd build
~src/picorom/atari_2600_digital_frame/build$ cmake ..
~src/picorom/atari_2600_digital_frame/build$ make

And copied the resulting pico_rom.uf2 to the Pico on the Picotari PCB.

Amazingly I got a display! But it was off – there was obviously something odd going on somewhere…

Then unfortunately it stopped working. I thought I’d killed my 2600 junior as it wasn’t working even with a normal cart, so I dug out my original woody 2600 with an original cart but that wasn’t working either, so concluded it was the TV or at least the TV’s tuning. It turns out that the TV struggles to spot a signal from time to time on the analogue channels, but switching preset channels out and back seems to wake it up again.

The photo frame code is meant to return different bytes from the embedded graphic when a specific address is read. The cart itself “believes” it is just continually reading the same byte. At least that is my understanding of how it is meant to work.

So to me, this looks like a data ordering issue in the code somewhere. As I’m not particularly interested in the picture frame application, this was just a test with a supposedly known-good application, I’m happy that the hardware appears to be essentially working fine.

But just thinking about this for a moment. It could be an issue with scan line timings. It isn’t clear to me if the original project is meant for NTSC or PAL, but given the author is in the US (I believe), I suspect NTSC. It may well be that the Atari ROM code assumes the 30 frame/s update rate of NTSC rather than 25 frames/s for PAL and that messes with the timing of the data. The refresh rates are actually 60Hz and 50Hz but it is an interlaced display so two scans are required per frame of image. PAL images are also 625 lines compared to NTSC’s 525.

With the Atari, all this is pretty important as the CPU has to manage everything to do with the display, timing and all, alongside anything else it is trying to do (see the excellent book “Racing the Beam” for some of the amazing tricks people came up with for getting the most out of this system!).

Of course, it could also be nothing to do with this. At this point I just don’t know enough personally about Atari code to delve into it to see if I could correct it.

I’d like to know if I could get this working, but I suspect that may be a project for another time.

Game ROM

One final test – could I build in an existing game ROM and have that boot and run?

I grabbed a ROM collection off the Internet and found the ROM for space invaders, then built that into the original PicoROM code using a translate python script from the Atari repository.

~/src/pico/picorom/picoROM$ python ../atari_2600_digital_frame/translate_bin2rom.py spaceinv.bin > romdata.c

Then just had to replace the original rom_contents[] = XX lines from rom.c with the new lines in romdata.c and build. There are less lines in the space invaders ROM, but once built and plugged into the Atari, sure enough, I get to see Space Invaders up and running on the display.

I’m not getting sound at the moment, but that could be any combination of an odd ROM or slightly incompatible version (there were about 5 to choose from!) or the TV tuning, which I still have little confidence over.

But as the game does seem to boot and run and is playable, so I’m happy that the basic system seems to be working.

Update: It was TV tuning. Fiddling around and I finally got some sound! 🙂

Conclusion

This is a very clever project and I’m really grateful for Nick Bild for releasing the details of their build.

I feel like I’ve got a bit more of a handle on what is going on now and have ended up with a really useful starting point for a few of my own project ideas.

My next step is to work out how to build a custom Atari ROM executable of my own.

Kevin

#2600 #atari #picotari #raspberryPiPico #vcs

CJMCU AS3935 - Simular rayos con pulsos PWM

En este vídeo explico cómo utilizo un microcontrolador secundario para testear el sensor de rayos CJMCU-AS3935 para simular con pulsos PWM los pulsos electromagnéticos de un relámpago o rayo y de esta forma asegurarnos que nuestro dispositivo funciona correctamente o testear el código que desarrollemos para utilizar el sensor.

https://youtu.be/lgofPB-SshQ

#iot #raspberry #raspberrypipico #maker #microcontroller

CJMCU AS3935 - Simular rayos con pulsos PWM

YouTube

I am slowly getting better at soldering, 4 out of 7 successful #MechanicalKeyboard PCBs now - all three of my diode-free #GraphTheory wired keyboard PCB designs have worked (eventually) ⌨️🎊

1️⃣ #RaspberryPiPico #RP2040 ‘Gamma Omega TC36K’ (USB only) http://astrobeano.blogspot.com/2025/08/my-first-self-built-computer-keyboard.html

2️⃣ #ProMicro #nRF52840 BLE ‘Gamma Omega Hesse’ https://astrobeano.blogspot.com/2025/10/hesse-diode-free-bluetooth-keyboard.html

3️⃣ #SeeedStudio #Xiao #SplitKeyboard BLE split Forager ‘Acid’ http://astrobeano.blogspot.com/2025/10/partial-heawood-forager-keyboard.html

#ErgoMechKeyboard #ErgonomicKeyboard

USBSID-Pico SID Player Demo Highlights - The Oasis BBS

See USBSID-Pico SID player in action with real SID sound, Deepsid streaming, and stereo playback. Watch the video for a full demo.

The Oasis BBS

I've been intrigued by the idea of #RISCV recently but I've never really dabbled in low-level electronics. Any recommendations for a small #SBC I can try it out with? Something similar to an #Arduino or #RaspberryPiPico would be preferred.

#AskFedi