Close

RetroChallenge 2023/10 Log Entry #3

A project log for PicoRAM 2090

A Raspberry Pi Pico (RP2040)-based 2114 SRAM Emulator & Multi-Expansion for the Microtronic Computer System * RetroChallenge 2023/10 *

michael-wesselMichael Wessel 10/20/2023 at 15:370 Comments

The firmware is almost finished by now!
The co-processor idea has matured; by now, I am using the following "vacuous" op-codes to implement extra side-effects: 

   0xx ENTER LITERAL DATA x

   3Fx ENTER DATA FROM REG x 

   500 HEX DATA ENTRY MODE
   501 DEC DATA ENTRY MODE
   502 DISP CLEAR SCREEN 
   503 DISP TOGGLE UPDATE  
   504 DISP REFRESH
   505 DISP CLEAR LINE <X> 
   506 DISP SHOW CHAR <LOW><HIGH>
   507 DISP CURSOR SET CURSOR LINE <X>
   508 DISP SET CURSOR <X> <Y>
   509 DISP PLOT <X> <Y> 
   50A DISP LINE <X1> <Y1> <X2> <Y2>
   50B DISP LINE - <X> <Y> 
   50C SOUND PLAY NOTE <X><Y> (SOUND OFF FIRST) 
   50D SOUND PLAY FREQ <X><Y> 
   50E ENABLE DISP SPEAK ECHO  
   50F SPEAK BYTE <LOW><HIGH>

   70x SWITCH MEMORY BANK x

Note that all of these instructions / op-codes do not appear in ordinary existing Microtronic programs, 'cause they are basically vacuous. For example,  0xx copies register x to itself,  3Fx does an AND of register x with (immediate) value 0xF, 50x adds 0 to x, and 70x subtracts 0 from register x. It's great that the original designers of the Microtronic left us so many op-codes to play with! With these, we can now extend the existing programs with speech output, display, and sound, without interfering with the existing program at all.

All these op-codes have been implemented by now. I still need to do one more round of testing, and then write some demo programs.

Here is a simple example - to let the speech synthesizer speak 123, "one hundred and twenty three", the following program will do: 

00 50F # enable speech 
01 011 # send 1 (low nibble)
02 033 # send 3 (high nibble) -> 0x31 = ASCII for 1 
03 022 # send 2 
04 033 # send 3 -> 0x32 = ASCII for 2 
05 033 # send 3 
06 033 # send 3 -> 0x33 = ASCII for 3 
07 0AA # send LF, trigger text to speech 
08 000 # 0x0A -> Line feed 

Here, the operands / arguments for speech are supplied literally in the code. And the Microtronic doesn't have program-writable code memory. So if we wanted the speech synthesizer to utter something specified in some registers, then we can use the 3Fx instructions instead of the 0xx.

Here is a demo program that lets the user enter an ASCII code at runtime over the hex keypad, stores low and high nibble of the ASCII code in registers 0 and 1, and then sends the register contents of 0 and 1 to the OLED display for ASCII character output as well as to the speech synth:




This program looks as follows: 

00 F08 # clear registers
01 F20 # display registers 0 and 1 on Microtronic LED display
02 FF1 # enter high nibble into register 1
03 FF0 # enter low nibble into register 0
04 506 # show ASCII character extended op-code
05 3F0 # send value in register 0 as low nibble argument to 506
06 3F1 # send value in register 1 as high nibble argument to 506
07 50F # enable speech synth
08 3F0 # send value in reg 0 to speech synth
09 3F1 # send value in reg 1 to speech synth
0A 0AA # send linefeed to speech synth: 0x0A 
0B 000 # 
0C C00 # GOTO 00 

Note that the operands / arguments for ASCII output to the OLED (506) and speech (50F) are now coming from register memory. 

It was  actually quite challenging to implement this - in a Harvard architecture such as the Microtronic, there is no way to "materialize" the register values into the program memory! So how can we communicate the current register values to the Pico? Remember that the program memory, i.e., the current op-code and address being fetched / executed, is the only "peek" and interface that the Pico has into the Microtronic. The register memory is not directly accessible - it is stored in the TMS1600 microcontroller and hence in a "black box". So how can we open up the black box and turn it into a white box? 

Surely, we can write a Microtronic program - let's call it REGLOAD in the following - that compares a given register with a set of immediate (in-code, constant) values. The relevant op-code  is CMPI nx (9nx) - CoMPare Immediate register x with value n. Based on the outcome of the comparison, flags are set: the Zero flag is set if x equals n, and Carry if the value in x is greater than n. For these flags, conditional branch instructions exist: branch-if-zero (BRZ = Eyz) to an address, and branch-if-carry to an address (BRC = Dyz). By jumping conditionally to different memory addresses yz - and this is what the Pico can detect! - we can hence indirectly communicate the current value in register x to the Pico. 

Using binary search, determining the current register value only requires ~4 instructions. Again, the point here is - even though the Pico cannot directly access the value of register x, it can now execute this REGLOAD "sub program" and observe the memory addresses which are accessed. Different target addresses are reached for different register values, and the Pico can infer the register value from the reached target address. Simple, right? 

But how do we execute this REGLOAD  sub program when a 3Fx op-code is detected in the first place? This is a complex program, and it would be tedious to insert this sub program into the original program as this would require a runtime manipulation of the SRAM C array. The Pico is fast, but this operation will be time consuming. Instead, we use another idea: banked memory!  

See, the emulated Microtronic is already organized into banks. Currently, its emulated SRAM consists of 16 banks: 

#define MAX_BANKS 0x10
uint8_t ram[MAX_BANKS][(uint32_t) 1 << 10] = {};

Switching the current bank is as easy as changing the value of the cur_bank variable - a very fast operation, no memory has to be copied or modified at all! The SRAM emulator - the Pico simply presents

val = ram[cur_bank][adr];

to the Microtronic. Hence, bank switching (and hence switching out the entire SRAM!) is a very cheap operation.

We can simply add another set of REGLOAD RAM banks that contain the REGLOAD programs - one for each register x (one for each 3Fx). Using another flag, the Pico and hence Microtronic can simply be switched into REGLOAD mode, with RAM being served from the REGLOAD banks as follows:

  if (reg_load_active) {
      val = reg_load_ram[cur_reg_load_bank][adr];
    } else {
      val = ram[cur_bank][adr];
    }

There are as many REGLOAD banks as there are registers: 

reg_load_ram[MAX_BANKS + 1][(uint32_t) 1 << 10] = {}; 

plus one more - the GOTO JUMP bank at 0x10 (see below).

Now, when the Microtronic encounters 3Fx it switches to REGLOAD for register x in reg_load_ram bank x and then executes the x-specific REGLOAD program. However, it needs a GOTO 01 jump to the start of the REGLOAD program at address 01 first! How do we execute this GOTO

In order to prevent having to modify the original program SRAM bank, i.e., programmatically inserting a C01 (GOTO 01) after the 3Fx instruction with the Pico which would have to be undone upon return from the REGLOAD "sub program", we can simply switch to another bank instead: one that only contains C01 (GOTO 01) at each address - the GOTO JUMP bank. 

The Microtronic hence executes 3Fx, continues to the next instruction. The Pico saves the address of 3Fx as a return address. Before the Microtronic is executing the next instruction, the Pico banks in the GOTO JUMP bank. The Microtronic now finds the C01 (GOTO 01) instruction at the next address and jumps to the start of the REGLOAD program at 01. At the same time, the Pico banks in the REGLOAD program bank x now. Upon arrival at address 01, the Microtronic hence finds the correct REGLOAD program for register x in memory, executes it, and the Pico observes the memory addresses that are encountered during its execution. Based on the target addresses encountered, the Pico now infers the value in the register x.

The following target addresses encode the given register values - the observed register value is then taken as an operand / argument for the currently active extended op-code, in the same way that an 0nn instruction would do (the same code is executed) . But now, the n value is coming from register x rather than being specified literally /immediately in the code: 

static byte reg_load_target_addresses[] = {255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0..9 
  1, 0, // A, B
  255, 255, // C, D
  3, 2, // E, F
  255, 255, 255, 255, // 10, 11, 12, 13
  5, 4, // 14, 15
  255, 255, // 16, 17
  7, 6, // 18, 19
  255, 255, 255, 255, 255, 255, // 1A, 1B, 1C, 1D, 1E, 1F 
  9, 8, // 20, 21 
  255, 255, // 22, 23
  0xB, 0xA, // 24, 25
  255, 255, 255, 255, // 26, 27, 28, 29 
  0xD, 0xC, // 2A, 2B
  255, 255, // 2C, 2D
  0xF, 0xE // 2E, 2F 
}; 

The actual REGLOAD program for register x = 0 looks as follows; the details are not super important, but it performs a binary search to execute as swiftly as possible (faster than linear search for the value). Note the 9nx (CPMI, Compare Immediate), Dyz (Branch if Carry to address yz), Eyz (Branch if Zeroto address yz), and Cyz (GOTO / JMP to address yz) instructions. The program starts at address 02; 01 is the jump target, because I cannot detect accesses to address 00, as explained in a previous log entry: 

#define REG_LOAD "F01 F01 970 D1A 930 D10 910 D0C 900 E0B C30 C30 920 E0F C30 C30 950 D16 940 E15 C30 C30 960 E19 C30 C30 9B0 D26 990 D22 980 E21 C30 C30 9A0 E25 C30 C30 9D0 D2C 9C0 E2B C30 C30 9E0 E2F C30 C30 " // from 30...40 there will be NOPs, and the Cxx return computed

The GOTO JUMP C01 bank is pre-computed programmtically with a loop and simply swapped in when need, as explained: 

  // C01 GOTO JMP BANK
  // cause 00 cannot be detected! 
  for (adr = 0; adr < 0x100; adr++) {
    reg_load_ram[0x10][adr] = 0xc; 
    reg_load_ram[0x10][adr | 1 << 9] = 0; 
    reg_load_ram[0x10][adr | 1 << 8] = 1; 
  }

We have determined the register value and supplied the operand. Now we need to resume execution of the original program. In order to jump back to the return address, the original instruction after the 3Fx (let's call it address yz) in the original program bank, we programmatically insert a Cyz jump at address 0x40 in the REGLOAD bank x. Between 0x30 and 0x40 the Microtronic executes some NOPs (FO1) in order to give the Pico some time to prepare the switch-back to the original memory bank, i.e., to compute the yz return address and insert the Cyz instruction. When that GOTO Cyz is then encountered and executed by the Microtronic, the Pico switches back to the original memory bank immediately. Upon arrival at the yz target address, the Microtronic will already find the original memory bank restored and simply continues to execute the next instruction there from the original program. 

All of this happened entirely transparent to the calling program.

Discussions