MCU Boot ROM Emulation


Second Experiment - BootROM

Now we will start to use the microcontroller. This experiment focuses on the development of the basic interface between the MCU and the CPLD and the development of the base firmware of the MCU.

Redefinition of CONT

CONT can no longer be asserted immediately as it was before. Now we need to insert wait states, i.e. delay assertion of CONT, when the MCU is in charge of the IO as the MCU is performing verything in software and therefore we need to wait for the MCU to signal he has finished the IO processing.

CONT          = !LBS1  & !LBS0				/* Memory		*/
              # !LBS1  &  LBS0				/* Internal Register	*/
              #  LBS1  & !LBS0  & !MCUIO  &  SCTL	/* External IO Register	*/
              #  LBS1  &  LBS0				/* System Register 	*/
              #  LAIO:[AIOIACK] &  SCTL			/* Interrupt Acknolwedge*/
              #  LAIO:[AIOGPREAD]
              #  LAIO:[AIOGPWRITE];
              #  DONE

MCUIO is asserted whenever the MCU needs to take over. As we will continuously expand the IO the MCU handles CONT will change with each additional feature.

Data Interface

Next we create a data interface for the MCU, the MCU can then read the type of the cycle and the address accessed in the IO page. As the DAL of the DCJ11 is multiplexed the MCU cannot collect the AIO status and the address in the IO page in software, it is too slow and it has no programmable IOs as many modern 32-bit microcontrollers do have. Therefore the CPLD latches this information in hardware and makes it available to the MCU via a data interface.

LAIOB         =  LAIO3 &  LAIO0
              # !LAIO3 &  A0;

DATA.oe       =  RD;
DATA          = !RS1 & !RS0 & [A8..1]
              # !RS1 &  RS0 & [LAIO3, LAIO2, LAIO1, LAIOB, A12..9];

Note how we provide LAIO0 for read cycles and A0 for write cycles. For write cycles LAIO0 is undefined and hence has no meaning. On the other side A0 is only required for byte write cycles. This allows to present the cycle type and the full IO page address in just two bytes.

MCU Interface

For the MCU interface we use a state machine. In case the MCU needs to take over we assert RQST. RQST is supposed to trigger an interrupt. The MCU then reads the two bytes from the CPLD and then proceeds with the appropriate processing. When it is done it will assert ACKN.

In case of external IO register read, the microcontroller will put the data directly onto the data bus before it asserts ACKN. The state machine will then de-assert DV and RQST. The microcontroller then puts it’s data bus output into high-impedance state and only then de-asserts ACKN which is the signal to the CPLD to finish the cycle by asserting DONE. DONE is used for microcontroller IO to assert CONT.


MCU.ck        =  CLK;
RQST.ck       =  CLK;
DONE.ck       =  CLK;
DVIO.ck       =  CLK;
ACKI.ck       =  CLK;

ACKI.d        =  ACKN;

MCU.ar        =  STRB;
RQST.ar       =  STRB;
DONE.ar       =  STRB;
DVIO.ar       =  STRB;
ACKI.ar       =  STRB;

SEQUENCE MCU {
	PRESENT 'h'0   IF  !MCUIO  NEXT 'h'0;
	               IF   MCUIO  NEXT 'h'1  OUT RQST;

	PRESENT 'h'1   IF  !ACKI   NEXT 'h'1  OUT RQST;
	               IF   ACKI   NEXT 'h'2  OUT RQST;
	               
	PRESENT 'h'2   IF   ACKI   NEXT 'h'2  OUT DVIO;
	               IF  !ACKI   NEXT 'h'3  OUT DVIO;
	
	PRESENT 'h'3               NEXT 'h'3  OUT DVIO OUT DONE;

}


DV            = !DVIO;

MCU IO Interrupt Routine

On the AVR microcontroller RQST is connected to PF5. PF5 will be configured as a raising edge port change interrupt. Whenever the RQST is asserted the microcontroller will execute the port change interrupt routine for port F. As only one pin of port F is configured as interrupt source we need no code to identify which pin case the port F port change interrupt.

I have decided that from the beginning I will use the content of CPLD register 1 as the entry into a jump table.

io_isr:
	push	r8			; Save Status Register
	in	r8, cpu_sreg
	push	yl			; Save work registers
	push	yh
	push	zl
	push	zh
	cbi	b_RS0			; Select Register 0
	cbi	b_RS1
	ldi	yl, 0x00
	out	datadir, yl
	sbi	b_RD			; Read Register
	nop				; Wait for synchronizer logic
	nop
	nop
	in	yl, VPORTG_IN		; Fetch Register data
	sbi	b_RS0			; Select Register 1
	nop				; Wait for synchronizer logic
	nop
	nop
	in	zl, VPORTG_IN		; Fetch Register data
	sts	extregaddr+0, yl
	sts	extregaddr+1, zl
	ldi	zh, high(io_isr_jmptbl)
	ijmp

The jump table will be aligned to a 256 byte boundary so we can use zl als the lower byte to the ijmp instruction. This also makes the content of register 1 available to all routines without having to access the CPLD again. Also the address bits A12..9 form a unique index for all planned IO

;	17752x	x'xx1'111'xxx'xxx'xxx		Boot and Diagnostic Register Set
;	176000	x'xx1'110'xxx'xxx'xxx		SLUA and SLUB
;	175000	x'xx1'101'xxx'xxx'xxx		n.a.
;	174000	x'xx1'100'xxx'xxx'xxx		n.a.
;	173000	x'xx1'011'xxx'xxx'xxx		Bootrom 1
;	172000	x'xx1'010'xxx'xxx'xxx		n.a.
;	171000	x'xx1'001'xxx'xxx'xxx		n.a.
;	170000	x'xx1'000'xxx'xxx'xxx		n.a.
;
;	167000	x'xx0'111'xxx'xxx'xxx		GPIO
;	166000	x'xx0'110'xxx'xxx'xxx		SPI
;	165000	x'xx0'101'xxx'xxx'xxx		Bootrom 2
;	164000	x'xx0'100'xxx'xxx'xxx		n.a.
;	163000	x'xx0'011'xxx'xxx'xxx		n.a.
;	162000	x'xx0'010'xxx'xxx'xxx		n.a.
;	161000	x'xx0'001'xxx'xxx'xxx		n.a.
;	160000	x'xx0'000'xxx'xxx'xxx		n.a.

The jump table of course will have a lot of repeated entries and is much larger than required. But the AVR128DB64 has plenty of flash to allow for a 256 instruction long jump table.

You could also use a TABLE in the CPLD to reduce the full index to the required bits, however this requires a lot of CPLD resources and the format is not as clear as a jump table in AVR assembler.

The following shows just the first 32 entries which cover word writes to the IO page. Note that the entries start with the AIO code 0b0000 and address 0b0000. During tests I decided to make the ROM regions writable.

io_isr_jmptbl:
;
;	'b'0000	Write Word
;
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_bootrom2w
	rjmp	io_isr_spiw
	rjmp	io_isr_gpiow
	
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_bootrom1w
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_sluw
	rjmp	io_isr_registersw
;
;	'b'0001	Write Word
;
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_bootrom2w
	rjmp	io_isr_spiw
	rjmp	io_isr_gpiow
	
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_bootrom1w
	rjmp	io_isr_noio
	rjmp	io_isr_noio
	rjmp	io_isr_sluw
	rjmp	io_isr_registersw
	...
	...

The jump table already includes all planned entries but the entries point to the same dummy routine, which just acknowledges the request. Note that the CPLD decides in fact whether the MCU is triggered, hence they are most likely not even used.

The following shows the code for the boot ROMs. During initialisation the MCU initialises the RAM regions bootrom1 and bootrom2 with a copy of the boot ROMs from my Plessey Multifunction Card. There is nothing special about the boot ROM of this card and I took it as an example. You can always place your own code there.

There are individual entries for boot ROM 1 and boot ROM 2. They set up zh:zl with the start address of the RAM region. Then twice the word offset which is in yl and is just the content of register 0 of the CPLD to make the correct offset to the word in the RAM region. In case of byte writes one is added to the address to point to the correct byte. Read routines place the values int yh:yl and write routines read DAL0..15 which are connected to two 8-bit ports named dallowin and dalhighin.

;
;	The default Boot ROM is copied to RAM
;
io_isr_bootrom1r:
	ldi	zl, low(bootrom1)
	ldi	zh, high(bootrom1)
	rjmp	io_isr_bootromr
io_isr_bootrom2r:
	ldi	zl, low(bootrom2)
	ldi	zh, high(bootrom2)
io_isr_bootromr:
	clr	yh
	add	zl, yl
	adc	zh, yh
	add	zl, yl
	adc	zh, yh
	ld	yl, Z+
	ld	yh, Z+
	sts	extreg+0, yl
	sts	extreg+1, yh
	rjmp	io_isr_exit_read

;
;	During Tests the Boot ROM copy can be written by the PDP-11
;
io_isr_bootrom1w:
	ldi	zl, low(bootrom1)
	ldi	zh, high(bootrom1)
	rjmp	io_isr_bootromw
io_isr_bootrom2w:
	ldi	zl, low(bootrom2)
	ldi	zh, high(bootrom2)
io_isr_bootromw:
	clr	yh
	add	zl, yl
	adc	zh, yh
	add	zl, yl
	adc	zh, yh
	in	yl, dallowin
	in	yh, dalhighin
	st	Z+, yl
	st	Z+, yh
	sts	extreg+0, yl
	sts	extreg+1, yh
	rjmp	io_isr_exit_write
;
;	Write Lower Byte only
;
io_isr_bootrom1l:
	ldi	zl, low(bootrom1)
	ldi	zh, high(bootrom1)
	rjmp	io_isr_bootroml
io_isr_bootrom2l:
	ldi	zl, low(bootrom2)
	ldi	zh, high(bootrom2)
io_isr_bootroml:
	clr	yh
	add	zl, yl
	adc	zh, yh
	add	zl, yl
	adc	zh, yh
	in	yl, dallowin
	in	yh, dalhighin		; For debugging purposes we read the word
	st	Z+, yl
	sts	extreg+0, yl
	sts	extreg+1, yh
	rjmp	io_isr_exit_write
;
;	Write Upper Byte only
;
io_isr_bootrom1u:
	ldi	zl, low(bootrom1)
	ldi	zh, high(bootrom1)
	rjmp	io_isr_bootromu
io_isr_bootrom2u:
	ldi	zl, low(bootrom2)
	ldi	zh, high(bootrom2)
io_isr_bootromu:
	clr	yh
	add	zl, yl
	adc	zh, yh
	add	zl, yl
	adc	zh, yh
	adiw	zh:zl, 1
	in	yl, dallowin
	in	yh, dalhighin		; For debugging purposes we read the word
	st	Z+, yh
	sts	extreg+0, yl
	sts	extreg+1, yh
	rjmp	io_isr_exit_write

Test of Boot ROM

The ROM content is taken from the Plessey Multifunction card. It provides a simple command line interface to boot from various devices. It even allows to use non standard base CSR addresses and arbitrary unit numbers. This is just used as an exmaple. Later you will see how you can boot from a TU-58 by entering DD and RETURN.


@17773000/000005 
17773002/010700 
17773004/062700 
17773006/000660 
17773010/105737 
17773012/177564 
@17765000/052115 
17765002/172522 
17765004/000010 
17765006/172060 
@173000g
$XX
$