SBC Cross-Development

Cross-developing on homebrew 6502s and other single board computer projects.

 

Over on Veronica, things have progressed to the point of needing some fairly substantial amounts of 6502 code to put in ROM. Writing this stuff by hand and manually assembling it is getting old very quick, and I need something more efficient. What I’d like is a system where I can just write assembly code on my laptop, push a button, and have that code execute on the homebrew computer. This is a pretty standard model of development; throughout history, development for a particular computer has been done on a bigger (better) computer connected to it. In modern times, an easy analogy is console game development (where I worked for many years). Consoles like the Xbox and PlayStation are very good at running games, but they’re lousy as general purpose computers. You wouldn’t want to do development for those machines on those machines directly.

Once I had this system running for Veronica, I realized there’s nothing especially Veronica-specific about it. Any homebrew single-board-computer-type project that needs ROM code could use it. Hence, this article!

First and foremost, this system depends on an AVR-based EEPROM programming setup. If, like me, you don’t want to buy a fancy expensive EEPROM programmer, head over to this project and build your own. An AVR microcontroller can be hooked up to an EEPROM as external RAM, and presto you have a programmer for a couple of bucks. I’m partial to the ATTiny series for this purpose. So, the trick here is that your 6502 (or other CPU) code needs to be included in the package that is downloaded to the AVR, so that the AVR can then write it into the EEPROM on your homebrew computer.

To start with, you need code to drive the AVR to program the EEPROM. Here’s my code for that:

/* Name: main.c
 * Author: Quinn Dunki
 * Copyright: ©2012 Quinn Dunki
 * License: All Rights Reserved
 *
 * ATtiny13A code to program an SRAM chip or EEPROM
 * For details, see http://www.quinndunki.com/blondihacks
 */


#include <avr/io.h>
#include <util/delay.h>
#include <avr/pgmspace.h>

#define QDDRB(n) (1<<(DDB##n))
#define QPINBIT(n) PB##n
#define SET_HI(pin) PORTB |= (1<<(QPINBIT(pin)))
#define SET_LO(pin) PORTB &= ~(1<<(QPINBIT(pin)))
#define PULSE(pin) SET_HI(pin); SET_LO(pin);
#define LOPULSE(pin) SET_LO(pin); SET_HI(pin);

#define SER_ADDR 0    	// Serial data to Address registers
#define SER_DATA 1		// Serial data to Data register
#define ROM_CLK 2		// Shift & Latch clocks for shift registers
#define BYTE_WRITE 3	// Chip Enable strobe on EEPROM
#define LEDS 4			// Status LEDs (HI=red, LO=green)

#define BASE_ADDRESS 0xf000		// Address at which to load the ROM image
#define ROM_SIZE 4096
#define EEPROM_PAGE_SIZE 64
#define EEPROM_PAGE_MASK 0xffc0

// Invoke our ROM image here, so the code can reference it
#include "rom.c"


// Main loop
int main(void)
{
	_delay_ms(15);	// Give the EEPROM time to wake up

	// Configure all pins as outputs
	DDRB = QDDRB(0) | QDDRB(1) | QDDRB(2) | QDDRB(3) | QDDRB(4);
	
	// Initialize some states
	SET_LO(ROM_CLK);
	SET_HI(BYTE_WRITE);
	SET_HI(LEDS);
	
	uint16_t byte = 0;
	uint16_t address = BASE_ADDRESS;
	uint16_t data = 0;
	uint16_t pageByte = 0;
	uint16_t pageNum = address & EEPROM_PAGE_MASK;
	int bit=0;
		
	// Iterate every byte in the ROM image
	for (; byte<ROM_SIZE; byte++)
	{
		// Calculate the effective address and desired byte from the image
		uint8_t db = pgm_read_byte(&romData[byte]);
		data = (db<<8) | db;	// Double-up the data byte, because it will be shifted out twice
		
		// Push the data and address out to the registers. Note one extra loop
		// to compensate for shift and latch clocks being tied together
		uint16_t mask = 1<<15;
		for (bit=0; bit<17; bit++)
		{
			if (address & mask)
			{
				SET_HI(SER_ADDR);
			}
			else
			{
				SET_LO(SER_ADDR);
			}
			
			if (data & mask)
			{
				SET_HI(SER_DATA);
			}
			else
			{
				SET_LO(SER_DATA);
			}
		
			mask >>= 1;
			PULSE(ROM_CLK);
		}

		LOPULSE(BYTE_WRITE);

		// After each EEPROM page, wait for the write to complete
		address++;
		uint16_t nextPageNum = address & EEPROM_PAGE_MASK;
		
		pageByte++;
		if (pageByte >= EEPROM_PAGE_SIZE || pageNum != nextPageNum)
		{
			pageByte = 0;
			pageNum = nextPageNum;
			_delay_ms(8);
		}		
	}
	
	// We're done, so give the green light
	SET_LO(LEDS);

    return 0;
}

This code will program EEPROMs of this type, using this circuit. Basically all it’s doing is using shift registers to push bytes into the EEPROM from the ATTiny. EEPROMs of this type are written to just like SRAMs, but the timing needs to be done just right in order to trigger “page write” mode. Otherwise writing is painfully slow. This article has full details on the page write mode, if you’d like to know more.

If you look at the top of the source code above, you’ll see that it #includes “rom.c”. That’s where the data is that will end up in the SBC’s ROM. By including the 6502 code directly in the AVR’s source code, it ends up in Program Memory on the AVR. That’s necessary because AVRs have very little RAM, but quite a bit of program memory. Your AVR needs to have enough program memory to hold the entire contents of the SBC’s ROM.

Here’s what that “rom.c” file looks like:

/* Name: rom.c
 * Author: Quinn Dunki
 * Copyright: ©2012 Quinn Dunki
 * License: All Rights Reserved
 *
 * ROM image for Veronica
 * For details, see http://www.quinndunki.com/blondihacks
 */


uint8_t romData[ROM_SIZE] PROGMEM = {

    #include "romImage.inc"		// This file is generated from romImage.S using ca65 and hexdump

	
// $FFFA		Non-maskable interrupt vector
0x00,0x00,
	
// $FFFC		Restart vector
0x00,0xF0,
	
// $FFFE		Interrupt vector
0x00,0x00	

};

As you can see, it’s just a big C array with some hex bytes in it. Those hex bytes land at the very end of ROM, which is where the 6502 looks for the special vectors used to start up. But what’s this? Yet another file is being included- romImage.inc. Where does THAT file come from?  Well, this extra layer of indirection allows the ROM source code to change, but keeps the special reset vectors and such in the correct place at the end of ROM.

Here’s a sample romImage.inc file:

0xa9,0x01,0x8d,0xff,0xef,0xa9,0x00,0x8d,0xff,0xef,
0xa9,0x00,0x85,0x00,0xa9,0x02,0x85,0x01,0xa0,0x00,
0xa9,0x42,0x91,0x00,0xa9,0x00,0xb1,0x00,0xc9,0x42,
0xf0,0x0d,0xa9,0x03,0x8d,0xff,0xef,0xa9,0x58,0x8d,
0xff,0xef,0x4c,0x37,0xf0,0xa9,0x03,0x8d,0xff,0xef,
0xa9,0x4f,0x8d,0xff,0xef,0xe6,0x00,0xd0,0x02,0xe6,
0x01,0xa5,0x01,0xc9,0xdf,0xd0,0xcf,0xa5,0x00,0xc9,
0xff,0xd0,0xc9,0x4c,0x66,0xf0,0xa9,0x01,0x8d,0xff,
0xef,0xa9,0x24,0x8d,0xff,0xef,0x4c,0x66,0xf0,0xa9,
0x01,0x8d,0xff,0xef,0xa9,0x30,0x8d,0xff,0xef,0x4c,
0x66,0xf0,0x4c,0x66,0xf0,0xa0,0x00,0xb1,0x00,0xf0,
0x13,0xaa,0xa9,0x03,0x8d,0xff,0xef,0x8a,0x8d,0xff,
0xef,0xe6,0x00,0xd0,0xee,0xe6,0x01,0x4c,0x6b,0xf0,
0x60,0x56,0x45,0x52,0x4f,0x4e,0x49,0x43,0x41,0x20,
0x52,0x41,0x4d,0x20,0x43,0x48,0x45,0x43,0x4b,0x20,
0x20,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
...

That’s just a piece of it. The whole thing is 4090 hex bytes separated by commas. My ROM is exactly 4k in size. So, you can see how splicing that hex data into the array in rom.c would create a 4k ROM image as a global C array, with the final 6 bytes being the special 6502 vectors. Neat!

So where does romImage.inc come from? Now we get to the meat of things. This is all done automagically by our old friend ‘make’. Here’s the makefile for this project:

# Makefile
#
#

DEVICE     = attiny85
CLOCK      = 8000000UL
PROGRAMMER = -c usbtiny
OBJECTS    = main.o
ROMBASE    = 0xf000
FUSES      = -U lfuse:w:0xe2:m -U hfuse:w:0xdf:m -U efuse:w:0xff:m

AVRDUDE = avrdude $(PROGRAMMER) -p $(DEVICE)
COMPILE = avr-gcc -Wall -O3 -DF_CPU=$(CLOCK) -mmcu=$(DEVICE)
ASSEMBLE = ca65 -D BASE=$(ROMBASE) -l

# Symbolic targets:
all:    romImage.inc main.hex

.c.o:
	$(COMPILE) -c $< -o $@

.S.o:
	$(ASSEMBLE) $< -o $@

.c.s:
	$(COMPILE) -S $< -o $@

flash:	all
	$(AVRDUDE) -U flash:w:main.hex:i

fuse:
	$(AVRDUDE) $(FUSES)

image:	romImage.bin

# Xcode uses the Makefile targets "", "clean" and "install"
install: flash fuse

# If you use a bootloader, change the command below appropriately:
load: all
	bootloadHID main.hex

clean:
	rm -f main.hex main.elf romImage.o romImage.bin romImage.inc romImagePad.bin $(OBJECTS)

# file targets:
main.elf: $(OBJECTS)
	$(COMPILE) -o main.elf $(OBJECTS)

main.hex: main.elf
	rm -f main.hex
	avr-objcopy -j .text -j .data -O ihex main.elf main.hex

romImage.bin: romImage.o
	cl65 romImage.o --target none --start-addr $(ROMBASE) -o romImage.bin

romImage.inc: romImage.bin
	dd if=romImage.bin ibs=4090 count=1 of=romImagePad.bin conv=sync
	hexdump -ve '10/1 "0x%.2x," 1/0 "\n"' romImagePad.bin > romImage.inc
	
# If you have an EEPROM section, you must also create a hex file for the
# EEPROM and add it to the "flash" target.

# Targets for code debugging and analysis:
disasm:	main.elf
	avr-objdump -d main.elf

cpp:
	$(COMPILE) -E main.c

This makefile relies on some external tools, but any standard un*x will have them. It also relies on the awesome 6502 toolset called CC65. The assembler in that package is called CA65, and that’s only element being used here. If you’re on OSX, there’s no compiled binary of CC65 available. However, you can build it from source very easily with these instructions.

So here’s what ‘make’ is doing:

  1. Finds the 6502 assembly source file (called romImage.S)
  2. Assembles that file using CA65. The flags are set such that CA65 generates raw machine code- no relocation headers or ELF format or anything fancy.
  3. The ‘dd‘ tool is used to pad that machine code file to exactly 4090 bytes (for a 4k ROM). This is critical, because otherwise the special vectors won’t land in the right place at the top of the ROM image. Our source code can be any size (less than 4090 bytes). This step will simply pad the final ROM image with zeroes to make everything work out.
  4. The padded binary image is run through the ‘hexdump‘ tool to make a human-readable C-source version
  5. The C-source is written out to romImage.inc, which is already included by the AVR code
  6. The AVR code is compiled, installed, and run on the AVR using avrdude in the usual way. The rom image has been magically swept up in this process, hiding in the AVR’s program memory
  7. When the AVR automatically reboots after the code download, it copies the code into the EEPROM and then just halts, with the satisfaction of a job well done.

 

It sounds like a lot of fiddly steps, and it is, but once it’s set up, you can write familiar 6502 source code like this:

; Name: VeronicaROM.s
; Author: Quinn Dunki
; Copyright: ©2012 Quinn Dunki
; License: All Rights Reserved
;
; 6052 code for Veronica's main system ROM
; For details, see http://www.quinndunki.com/blondihacks
;
; Zero Page:
;    $00..$01:	Subroutine parameter 1
;	$10..$11:	Scratch


.org $f000

ramCheck:
	lda		#$01		; Clear screen
	sta		$efff
	lda		#$00
	sta		$efff

	lda		#<prompt	; Show prompt
	sta		$00
	lda		#>prompt
	sta		$01
	jsr		printStr

	lda		#$00		; Initialize test address
	sta		$00
	lda		#$02
	sta		$01

loop:
	ldy		#$00		; Store and retrieve the test value
	lda		#$42
	sta		($00),y

	lda		#$00
	lda		($00),y
	cmp		#$42
	beq		goodChar
	lda		#$03
	sta		$efff
	lda		#$58
	sta		$efff
	jmp		next

goodChar:
	lda		#$03
	sta		$efff
	lda		#$4F
	sta		$efff

next:
	inc		$00			; Increment the 16-bit test address
	bne		checkForDone
	inc		$01

checkForDone:
	lda		$01
	cmp		#$df		; Have we reached the last address?
	bne		loop
	lda		$00
	cmp		#$ff
	bne		loop
	jmp		spinlock

success:
	lda		#$01
	sta		$efff
	lda		#$24
	sta		$efff
	jmp		spinlock

fail:
	lda		#$01
	sta		$efff
	lda		#$30
	sta		$efff
	jmp		spinlock

spinlock:
	jmp		spinlock

printStr:				; Args: Address of string to print
	ldy		#$00

printStrLoop:
	lda		($00),y		; Render the next character
	beq		printStrNull
	tax
	lda		#$03
	sta		$efff
	txa
	sta		$efff

	inc		$00			; Increment pointer
	bne		printStrLoop
	inc		$01
	jmp		printStrLoop

printStrNull:
	rts

prompt:
.byte		"VERONICA RAM CHECK  ",$00

… push one button, and in a couple of seconds you have new ROM running on your homebrew computer. You don’t necessarily need to understand that whole ‘make’ process to use this technique. Just change the numbers for your ROM size and starting address, and it should just work in any un*x environment.

One final note- once you start using a system like this to write large amounts of code, you may quickly find that debugging by binary reduction is no longer sufficient. For most low-level SBC code, you can build things up very slowly and get away with no debugging tools beyond careful examination of your code. When things get a little tougher, a simulator is a big help. The cross-development system I describe here dovetails nicely with this generic 6502 simulator. The hexdump stage of this build process can be tweaked to output code that can be pasted directly into memory on that simulator. From there, you can step through your code and debug it quite effectively. Here’s the hexdump command I use for this:

hexdump -ve ’10/1 “%.2x ” 1/0 “\n”‘ romImage.bin

Rapid iteration is key to efficient software development, so I hope some of you find this technique useful when you come to write the ROM for whatever crazy retro computer project you’re working on!

 

 

3 thoughts on “SBC Cross-Development

  1. What would be nice is to take this, and allow you to send pages at a time to be written over the uart on the AVR. There should be enough ram on most of them to do that, and then you wouldn’t have to wear out the flash on the AVR too. Though I suspect at some point the EEPROM will likely end up just loading from some other fancier place to store programs so maybe it’s a moot point.

    1. Yah, I considered that, but there were a couple of reasons I didnt go that route. The ATTinys I have on hand don’t have UARTs (only certain models do), and there wouldn’t be any speed advantage. In fact, there could be a speed loss, because the timing for writing to the EEPROM In page-write mode (64 byte bursts) is very tight. Without page-write mode, writing is around 5ms per byte. Glacial! Wearing out the EEPROM on the ATTiny isn’t too much of a concern, since it’s only getting worked out during the ROM development stage. If it wears out, the chip is socketed and they’re cheap anyway. 🙂

  2. Road map? Crystal ball? At some point you need a bare bones monitor in rom and lots of ram so you can download cross compiled code directly to ram on Veronica. Then it’s time for some mass storage on Veronica. Floppies seem like a nice retro thing to do and the diskettes and drives are still out there but the controller hw might be hard to find. Discrete floppy controler chips long ago became assimilated onto BGA ASICS on PC motherboards. You might get lucky and find some old XT floppy interface cards or find the required chips somewhere (BGmicro). A better idea would be to dig up some old lower capacity IDE disk drives and build an adapter for Veronica. Two 8 bit latches to convert the girl’s 8 bit data bus to the 16 bit IDE interface and a few control lines. No DMA or high speed routines are required as all the dirty work is done by the IDE interface itself inside the drive. Just issue commands and pump the data in/out at your own speed. Even an old 1GB drive will look like an infinite amount of space for a computer with a 64K address limit!

    I recently found the documentation I had buried for the DEC T11 chip I have in my junkbox. I’m giving serious thought to building a PDP-11/20 clone and would use an AVR to create the front panel of the machine. I’ll still need to write some PDP-11 code to put in rom to drive the micro-ODT routines on the processor. Hopefully I can activate the PDP-11 cross support in GNU-GCC on Linux.

Comments are closed.