In Circuit EPROM emulator and debugger
Posted: 17 Apr 2019, 13:25
I'm planing to use the Gigatron as a teaching tool for computer science because I think it fills very well the link between simple digital circuits, like decoders or counters, and microprocessors. The main problem for this is the EPROM. Its erasing time is too long and you have to remove it every time you want to test an student code. So, I think the first thing I'm going to do after ordering a gigatron is to design a ROM emulator to overcome the burden of EPROM erasing and programming.
The block diagram of such a circuit is that of the figure. Here the EPROM role is played by an equivalent SRAM, whose contents have been previously loaded by a microcontroller.
Notice that the address lines of the RAM can't be generated by the microcontroller because that would create an electrical contention with the Gigatron's PC. Address lines are always inputs to the microcontroller. The main idea here is to use the PC as an address counter and, in order to do this, the clock signal of the Gigatron is also generated by the microcontroller. It is not necessary to remove the crystal, the low impedance output of the microcontroller is going to override the clock signal.
Program ROM emulation
The PC reset is not connected here, therefore the first thing we have to do to load a ROM image into the RAM is to reset the PC by means of these operations:
1 - Put a 'LD $00,Y' opcode in the data bus.
2 - Pulse CLK.
3 - Put a 'JMP Y,$00' opcode in the data bus.
4 - Pulse CLK.
5 - put a 'NOP' opcode in the data bus.
6 - Pulse CLK.
Using a similar trick we can load any address into the PC. After reset the RAM loading is going to be done this way:
1 - Put memory content in the data bus.
2 - Pulse /WE. This stores the data in the RAM.
3 - Put a 'NOP' opcode in the data bus.
4 - Pulse CLK. This increments the PC.
After loading another reset sequence will put the PC pointing at zero. Then we can change the data lines as inputs to the microcontroller, lower the /OE signal, and start to pulse the CLK signal at maximum speed to execute the newly loaded code.
Debugger
This circuit can also do a lot of interesting things. In fact, it can debug programs step by step. This means the microcontroller has to:
1 - Stop execution. No problem because we are controlling the clock.
2 - Load and save the Gigatron state. This means reading the values of the Gigatron's registers and also possibly some of the data RAM.
3 - Show the state to the user and wait for it's command.
A resume execution command will need to:
4 - Restore the Gigatron state by writing the modified registers with their original values.
5 - Continuing applying clock pulses.
Reading state
Lets see how to read the Gigatron state:
1 - The PC value can be read directly from the EPROM address bus.
2 - Executing a 'JMP Y,xx, NOP' we can get the value of the Y register in the high address lines.
3 - Executing a 'LD AC,Y, JMP Y,xx, NOP' sequence we can read the accumulator value.
4 - Executing a 'LD IN,Y, JMP Y,xx, NOP' sequence we can read the input register.
5 - The X register is more difficult to read, but not impossible. One possible way would be:
5.1 - Read the page zero data. This data is also of interest, so this step was going to be taken anyway. This is achieved by executing 256 'LD [nn],Y, JMP Y,xx, NOP' sequences.
5.2 - Write all the zero page bytes with zeros by executing a 'LD $0,AC' followed by 256 'ST AC,[nn]' instructions.
5.3 - Execute a 'LD $1,AC, ST AC,[X]' sequence.
5.4 - Read the zero page data and stop when the value read is one. The count gives us the value of the X register.
The OUT and XOUT registers can't be read, but as they are used for video/audio generation their particular value is of little interest. And the LED value can be read by just looking at the Gigatron.
Restoring state
This is going to be easier. Let see:
1- X, OUT, and XOUT registers were unchanged.
2- The page zero data has to be written with its original values. This can be done with 256 sequences of 'LD dd,AC, ST AC,[nn]'
3- The AC value is restored with a single 'LD dd,AC'
4- Then follows the PC. First compute PC-1, and then execute 'LD hh,Y, JMP Y,ll',where hh and ll are the high and low bytes of PC-1.
5- Finally we restore the Y register by executing 'LD nn,Y'. This also increments the PC, which now points to the same address it has when the execution was stopped.
More features
The contents of the program memory and data memory can be read, allowing for disassembling and memory dumps.
The first one requires to change the data pins to input in the microcontroller and to enable the /OE line of the program RAM for read. Then disable /OE, put the data pins as outputs, place a NOP in the data pins, and pulse clock for the next address.
For the data RAM any position can be read with the instruction sequence 'LD hh,Y, LD [Y,ll],AC, LD AC,Y, JMP Y,xx, NOP', where hh and ll are the high and low bytes of the address.
Breakpoints can also be implemented in two different ways:
- Address breakpoint: The PC value is read in each clock cycle, compared to the breakpoint, and if a matching is found the clock is stopped. This will slow down the clock generation, and should be restricted to a single breakpoint.
- Instruction breakpoint: First we need an unused op-code, like 'LD IN,[Y,X++],OUT' to be used as a breakpoint. Then we replace with this op-code the places of the program memory where the execution is going to stop. For each clock cycle we read the op-code at the output of the program memory and if it is a BREAK the clock is stopped. Then the original content of the program memory can be restored before resuming execution.
Clock generation
In order to run code real time a fast microcontroller will be needed. I'm thinking about using an LPC1114 (ARM cortex-M0) running at 48MHz for this project. Also, the clock signal can be mapped to a PWM output, obtaining up to 24MHz. Unfortunately, the 6.25MHz is not obtainable, but 6MHz is and I hope a -4% error in the frequency is going to be tolerated by most VGA monitors.
If the clock is generated by program its frequency is going to be lower, but still the MHz range can be achieved. We'll probably have to generate the clock while checking the address or data for breakpoints. This will require about 14 cycles of the ARM CPU per one Gigatron cycle, lowering the frequency to around 3MHz.
The block diagram of such a circuit is that of the figure. Here the EPROM role is played by an equivalent SRAM, whose contents have been previously loaded by a microcontroller.
Notice that the address lines of the RAM can't be generated by the microcontroller because that would create an electrical contention with the Gigatron's PC. Address lines are always inputs to the microcontroller. The main idea here is to use the PC as an address counter and, in order to do this, the clock signal of the Gigatron is also generated by the microcontroller. It is not necessary to remove the crystal, the low impedance output of the microcontroller is going to override the clock signal.
Program ROM emulation
The PC reset is not connected here, therefore the first thing we have to do to load a ROM image into the RAM is to reset the PC by means of these operations:
1 - Put a 'LD $00,Y' opcode in the data bus.
2 - Pulse CLK.
3 - Put a 'JMP Y,$00' opcode in the data bus.
4 - Pulse CLK.
5 - put a 'NOP' opcode in the data bus.
6 - Pulse CLK.
Using a similar trick we can load any address into the PC. After reset the RAM loading is going to be done this way:
1 - Put memory content in the data bus.
2 - Pulse /WE. This stores the data in the RAM.
3 - Put a 'NOP' opcode in the data bus.
4 - Pulse CLK. This increments the PC.
After loading another reset sequence will put the PC pointing at zero. Then we can change the data lines as inputs to the microcontroller, lower the /OE signal, and start to pulse the CLK signal at maximum speed to execute the newly loaded code.
Debugger
This circuit can also do a lot of interesting things. In fact, it can debug programs step by step. This means the microcontroller has to:
1 - Stop execution. No problem because we are controlling the clock.
2 - Load and save the Gigatron state. This means reading the values of the Gigatron's registers and also possibly some of the data RAM.
3 - Show the state to the user and wait for it's command.
A resume execution command will need to:
4 - Restore the Gigatron state by writing the modified registers with their original values.
5 - Continuing applying clock pulses.
Reading state
Lets see how to read the Gigatron state:
1 - The PC value can be read directly from the EPROM address bus.
2 - Executing a 'JMP Y,xx, NOP' we can get the value of the Y register in the high address lines.
3 - Executing a 'LD AC,Y, JMP Y,xx, NOP' sequence we can read the accumulator value.
4 - Executing a 'LD IN,Y, JMP Y,xx, NOP' sequence we can read the input register.
5 - The X register is more difficult to read, but not impossible. One possible way would be:
5.1 - Read the page zero data. This data is also of interest, so this step was going to be taken anyway. This is achieved by executing 256 'LD [nn],Y, JMP Y,xx, NOP' sequences.
5.2 - Write all the zero page bytes with zeros by executing a 'LD $0,AC' followed by 256 'ST AC,[nn]' instructions.
5.3 - Execute a 'LD $1,AC, ST AC,[X]' sequence.
5.4 - Read the zero page data and stop when the value read is one. The count gives us the value of the X register.
The OUT and XOUT registers can't be read, but as they are used for video/audio generation their particular value is of little interest. And the LED value can be read by just looking at the Gigatron.
Restoring state
This is going to be easier. Let see:
1- X, OUT, and XOUT registers were unchanged.
2- The page zero data has to be written with its original values. This can be done with 256 sequences of 'LD dd,AC, ST AC,[nn]'
3- The AC value is restored with a single 'LD dd,AC'
4- Then follows the PC. First compute PC-1, and then execute 'LD hh,Y, JMP Y,ll',where hh and ll are the high and low bytes of PC-1.
5- Finally we restore the Y register by executing 'LD nn,Y'. This also increments the PC, which now points to the same address it has when the execution was stopped.
More features
The contents of the program memory and data memory can be read, allowing for disassembling and memory dumps.
The first one requires to change the data pins to input in the microcontroller and to enable the /OE line of the program RAM for read. Then disable /OE, put the data pins as outputs, place a NOP in the data pins, and pulse clock for the next address.
For the data RAM any position can be read with the instruction sequence 'LD hh,Y, LD [Y,ll],AC, LD AC,Y, JMP Y,xx, NOP', where hh and ll are the high and low bytes of the address.
Breakpoints can also be implemented in two different ways:
- Address breakpoint: The PC value is read in each clock cycle, compared to the breakpoint, and if a matching is found the clock is stopped. This will slow down the clock generation, and should be restricted to a single breakpoint.
- Instruction breakpoint: First we need an unused op-code, like 'LD IN,[Y,X++],OUT' to be used as a breakpoint. Then we replace with this op-code the places of the program memory where the execution is going to stop. For each clock cycle we read the op-code at the output of the program memory and if it is a BREAK the clock is stopped. Then the original content of the program memory can be restored before resuming execution.
Clock generation
In order to run code real time a fast microcontroller will be needed. I'm thinking about using an LPC1114 (ARM cortex-M0) running at 48MHz for this project. Also, the clock signal can be mapped to a PWM output, obtaining up to 24MHz. Unfortunately, the 6.25MHz is not obtainable, but 6MHz is and I hope a -4% error in the frequency is going to be tolerated by most VGA monitors.
If the clock is generated by program its frequency is going to be lower, but still the MHz range can be achieved. We'll probably have to generate the clock while checking the address or data for breakpoints. This will require about 14 cycles of the ARM CPU per one Gigatron cycle, lowering the frequency to around 3MHz.