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
$