-
RetroChallenge 2023/10 Log Entry #3
10/20/2023 at 15:37 • 0 commentsThe 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.
-
RetroChallenge 2023/10 Log Entry #2
10/14/2023 at 15:21 • 0 commentsVideo:
I've made some progress on the firmware - the SD card interface is now fully functional.
Also, the display font that came with the Pico C SDK SSD1306 OLED example was missing some characters, such as punctuation symbols. I substituted this font with another open-source font.
In the first line, the display is now showing the #bank number, followed by the address, then the 3 nibble op-code. The "-" indicates that op-code extensions are disabled; when enabled, it shows "*". Next, it either shows TTS or SND, depending on the position of the toggle switch. As mentioned earlier, I've exhausted the Pico's GPIOs and don't have enough IOs to support both sound and TTS simultaneously - at power-up, the toggle switch is used for selection.
The second display line shows the bank number, address, but mnemonics instead of the op-codes. The display mode can be toggled between 1) off, 2) only first line, and 3) first and second line (including mnemonics).
-
RetroChallenge 2023/10 Log Entry #1
10/11/2023 at 14:40 • 0 commentsI have signed up for the RetroChallenge 2023/10 - my goal is to turn this into the multi-functional Microtronic extension I've been dreaming of. As described on the RetroChallenge page:
My goal is to create a multi-purpose expansion for the Busch Microtronic Computer (1981, 4bit). I already have a Raspberry Pico set up to emulate the Microtronic's 2114 SRAM. The Pico enables me to store and restore Microtronic memory dumps as files to resp. from SDcard, in milliseconds (the cassette interface takes minutes for a decent program!) By "snooping" the address bus and SRAM (which the Pico emulates), the Pico also knows the instruction currently being executed by the Microtronic. I can hence turn the Pico into a co-processor, adding new op-codes to the original Microtronic. I plan to add op-codes for OLED display output, sound, speech synthesis, a Real Time Clock, as well as bank switching. The goal is to have a working prototype at the end of the Challenge month that can then be turned into a PCB (consisting of a Pico, OLED display, loudspeaker and op-amp, SDcard, speech chip, and a few buttons for the user interface).
As much as I liked the clean look of the Pico Freenove breakout board, the scope of the project now calls for a proper breadboard with some real estate for the different components. This is my current kitchen sink prototype:
You can see it's lots of voltage dividers. In addition to the SRAM cables, there is one new jumper cable that grabs R12 from the board, as described in my previous log entry: https://hackaday.io/project/192655-picoram-2090/log/223457-i-know-what-you-are-doing Unfortunately, it requires a hardware mod to the Microtronic, but it is relatively clean as the single pin header is mounted to an existing via on the PCB.
By now, I have the following components on board:
- Adafruit Micro SD SPI Breakout Board
- SSD1306 OLED Display (Blue and Yellow)
- PAM8403 Amplifier Board (instead of the Op-Amp)
- Epson S1V30120-based TextToSpeech speech synthesizer running DECtalk (used extensively in my previous projects)
- Loudspeaker, push buttons, resistors, and a double pole 6pin toggle switch
The OLED display is controlled over I2C, and SPI is used for the TTS and SDCard. I was nervous if the SDcard and TTS would co-exist on the same SPI0 port, but luckily that turned out to be the case. Not sure about the RTC yet.
The biggest challenge was porting the TTS firmware to the Pico VSCode C SDK - my previous projects with the MikroE TextToSpeech were written in WinAVR (e.g., https://hackaday.io/project/165677-lambdaspeak-3 and https://hackaday.io/project/171035-talker80-for-the-trs-80-model-1-3-and-4)
Thanks to the excellent Programming The Raspberry Pi Pico/W In C, Second Edition book I managed to port the firmware to the Pico. The Epson firmware image has to be loaded over SPI into the S1V30120 first, and that's not entirely trivial. So, I have a working DECtalk for the Pico now!
Next, for the SDcard, I used the excellent https://github.com/carlk3/no-OS-FatFS-SD-SPI-RPi-Pico library. Worked like a charm.
The OLED driver is supplied with the Pico SDK Examples.
Obviously, a number of user interface buttons are required - since I am running out of pins, I decided to hook up a 5-button resistor ladder to one of the Pico's analog inputs. So it's only 1 pin instead of 5. The circuit (and following schematics) is from the well-known Arduino LCD & Key Shield (3.3V is used for the Pico though):
For sound output, I first looked at https://github.com/martinkooij/pi-pico-tone but had problems getting this to work. So I am simply using hardware PWM to generate sound. This is not as fancy as using the PIO (in pi-pico-tone), but works without bugging down the 2nd core as well.
I am already short on GPIOs - I have exhausted all of the Pico's IOs. For that reason, I cannot use sound and TTS simultaneously. I had to find a compromise, and choosing between either sound or TTS during start-up seemed like a reasonable compromise. A physical double pole 6pin toggle switch is used to toggle between TTS and sound.
This is the allocation of the Pico pins:
// I2C OLED DISPLAY: #define PICO_DEFAULT_I2C_SDA_PIN 20 #define PICO_DEFAULT_I2C_SCL_PIN 21 // SRAM ADDRESS BUS: 2 - 11 #define ADR_INPUTS_START 2 #define DATA_GPIO_START (ADR_INPUTS_START + 10) // SRAM DATA BUS: 12 - 16 #define DATA_GPIO_END (DATA_GPIO_START + 4) // R12 DISPLAY MULTIPLEX LINE #define DISP_INPUT 0 // SRAM WRITE ENABLE INPUT #define WE_INPUT 1 // 5 PUSH BUTTONS VIA RESISTOR LADDER #define ADC_KEYS_INPUT 27 // PWM SOUND #define SOUND_OUTPUT 28 // EPSON TEXT2SPEECH #define EPS_RESET 28 #define EPS_READY 26 #define EPS_CS 22
As you can tell, I can either use GPIO 28 for resetting the TTS, or use it for sound output. I am using the double throw switch for routing; if the switch is in SOUND position, then the EPS_READY pin 26 is pulled high by the switch. The Pico firmware can determine the switch position during boot, and csn act accordingly (i.e., configure itself for TTS or sound). A few more GPIOs would have made this easier.
So, with the hardware and driver software fully operational, I am now in a position where I just need to grind away at the firmware!I am now using the 2nd core of the Pico to drive the display, user interface, and implement the op-code extensions (as explained previously, extra op-codes for sound, speech, display, and memory bank switching), while the 1st core is constantly emulating the SRAM.
I have 5 buttons for controlling the expansion; from left to right:
UP, DOWN, BACK, OK, CANCEL
This should be enough to, for example, select files from SDcard, create file names, and control some other functions.
There is a main menu in which the buttons have to following functions:
- UP: Load from SDCard
- DOWN: Save to SDCard
- BACK: Change current SRAM bank (16 banks)
- OK: Extended op-codes ON/OFF
- CANCEL: Change display mode (display off, address and op-code, address and op-code with mnemonics). In addition, the display also shows whether Sound or TTS is active, and if op-code extensions are enabled. Moreover, the current address is prefixed by the current bank number (0 to F).
By now, I have 8 Microtronic programs in the first 8 banks, and I can toggle through them with the "BACK" button. So banked memory works!Next, I am going to implement the SDcard interface (storing the current SRAM bank to SDcard, and loading a memory dump back into the current bank from SDCard).And then there is some work to be done for implementing the extended op-codes. Here is the table of the extended op-codes that I am planning to implement:0xx ENTER DATA x 500 HEX DATA ENTRY MODE 501 DEC DATA ENTRY MODE 502 DISP CLEAR SCREEN 503 DISP CLEAR LINE <X> 504 DISP SHOW CHAR <LOW><HIGH> 505 DISP CURSOR AT LINE <X> 506 DISP CURSOR <X> <Y> 507 DISP PLOT <X> <Y> 508 DISP LINE <X1> <Y1> <X2> <Y2> 509 SOUND OFF 50A SOUND PLAY NOTE <NOTE> 50B SOUND PLAY FREQ <LOW><HIGH> 50C SPEAK OFF 50D SPEAK BYTE <LOW><HIGH> 50E SPECIFY REG <REG> 50F LOAD VALUE FROM REG <REG> 70x SWITCH MEMORY BANK x
Video soon! -
I know what you are doing!
09/20/2023 at 15:44 • 0 commentsAlright Microtronic, I finally know what you are doing!
An additional motivation for this project is to make the Pico act as a co-processor - see, if the Pico knows the current op-code that is being executed (and the address / PC, obviously), I can make the Pico act on behalf of this op-code and implement "extra semantics" for some op-codes. What's "extra semantics" you might ask? Well, unfortunately, all 12bit op-codes are already occupied and already have a semantics in the Microtronic... but, some of these are "vacuous" and don't do anything meaningful - they are basically no-ops! For example, 0<x><x> copies register <x> onto itself. Same for "add 0 to register <x>", "subtract o from register <x>" - no Microtronic program is using these, and I can just grab them and give them extra-semantics to do something meaningful.
Looking at the schematics, we can see that the SRAM is addressed using the same GPIO lines that are also utilized for accessing the LED display and keyboard:
So how do we distinguish the SRAM accesses from all this additional activity on the GPIO "bus"? It took me many hours until I finally had a way of reliably detecting SRAM accesses.
A 12bit op-code starting at address <adr> is represented as follows in Microtronic SRAM:
the first nibble is at <adr>, the second nibble at <adr> | (1 << 9), and third nibble at <adr> | (1 << 8).My original idea was to simply implement a state machine that "tracks" the addresses as they show up on the bus: start in state 0, when <adr> arrives, go to state 1; when in state 1, and <adr> | (1 <<9) arrives, transition to state 2; when in state 2 and <adr> | (1 << 8) arrives, you have identified the SRAM access; reset to state 0. Right idea? Wrong!
The major problem with this approach is that the automata gets into wrong states - it is impossible to tell whether the transition from state 0 to state 1 is triggered by a real SRAM access, or by display multiplexing or keyboard scanning activities. Hence, the automata would sometimes get stuck, i.e., it transitioned into a wrong state and was waiting for a next adr pattern that didn't arrive as anticipated. I then thought, ok, let's reset the state when needed then (i.e., backtrack). But this strategy then had the problem that I would "miss" certain transitions, i.e., the automata wasn't getting stuck anymore, but when backtracking, I had already missed part of the address sequence. In addition to these "backtracking transitions" I then also started to incorporate timing-related info into the transition conditions, and things got more and more complicated, but I found no way of not "missing" certain addresses from wrong transitions and backtracking.
I had gotten myself into a dead end. Then I erased everything and started all over!
This time I had the right idea - I realized that I could only "retroactively" identify if an already presented sequence of addresses is part of the 12bit SRAM access pattern, but could not do so "proactively" (the automata approach had failed for this reason). I then simply kept a log ("sliding window") of the last four GPIO addresses: in addition to the current address in variable adr, I would also do a adr4 <- adr3, adr3 <- adr2, adr2 <- adr1, adr1 <- adr for each new recognized address change. By constantly scanning this window I then managed to identify the SRAM access with the following conditions:
if (adr4) { if (adr & (1 << 8)) { if (adr3 & (1 << 9)) { if ( (adr & adr3 ) == adr4) { // SRAM ACCESS FOUND! } } } }
However, I also needed to add one more hardware-mod - by routing the R12 signal from the TMS1600 which is used for multiplexing the display segments over a transistor (see schematics above), I could also filter out all address patterns that would "light up the segments". Luckily, the Microtronic has a via for that signal, and I could simply solder in a pin from one of these breakout pin-headers, so I didn't need to disassemble the whole thing and it's also looking neat. A DuPont wire is used to route it into the Pico, again over a voltage divider. It took a bit of time again to find the right resistors for the divider, but in the end, I could now at least also exclude the "active" display accesses. I still don't have a way of excluding the keyboard scanning accesses, but that's ok. It works.
The only problem with the above condition / approach is that I cannot detect adr 0. But that's OK; it's a little bit ugly for program debugging, but good enough for op-code extensions (we just can't have an extended op-code at adr 00 then). It works for all other addresses. Sometimes, a fresh approach and change of perspective is needed in order to make progress!
I also added an OLED display to visualize what the Microtronic is doing, and utilized the 2nd core of the Pico to constantly refresh / update the display. First I tried using the same core that does the SRAM emulation, but this is no longer fast enough then (timing is critical here!). This is great for debugging! I'll also add mnemonics to the display soon.
Well, great, I can now implement the "Pico co-processor" idea! And easy thing to do is to arrange for the vacuous op-codes to perform extra-side effects. For example, "0<x><x>" can be intercepted by the Pico and it could, for example, mean "play note <x> on the loudspeaker". We have just added a sound op-code to the Microtronic! Or it can mean "send character <x> to the OLED display" - the Microtronic can now display text messages! I think the original designers might be surprised by this :-)It's a little bit more complicated to implement op-codes that compute something that the Microtronic will then pick up. For example, imagine that the op-code "0<x><x>" means "calculate the square of <x> and write it to registers E,F". As the Microtronic is a Harvard architecture, I cannot write the 32 4bit registers ("Microtronic register file") with the Pico directly. SRAM is only for program memory, not for registers. But, I can inject a program segment with the Pico that, when executed by the Microtronic, will modify the register values to represent the computed value - "data as program"! Say the op-code "033" is detected by the Pico, now meaning "compute the square of 3 and write it to registers E,F!" If the current PC is adr, then the Pico will now first compute the square of 3, and subsequently, starting from adr+1, "inject" a sequence of MOVI (Move Immediate, i.e., move a constant to a register) instructions that write the Pico-computed values (0 and 9) to the appropriate registers (E and F). Hence, the Pico will present 10E at adr+1 to the Microtronic, and 19F at adr+2. The Microtronic will execute these, and the Pico-computed values will appear in the appropriate registers. At adr+3, the Pico will present a JMP / GOTO to adr+1. After the Microtronic has execute this last JMP / GOTO, the Pico continues presenting the original program from SRAM to the Microtronic again. The "injected" program sequence has been executed, and the Microtronic continues execution of the original program as usual. But has now the computed square 09 in its EF registers. Magic!
Next, I am going to work on the SDcard for SRAM images.
-
Thanks to Hackaday for covering the project!
09/11/2023 at 04:22 • 0 commentsGreat article by https://hackaday.com/author/robinkearey/ about the project - thanks much!
https://hackaday.com/2023/09/10/pi-pico-becomes-sram-for-1981-educational-computer/
-
Thanks to Hackster for the tweet!
09/09/2023 at 01:14 • 0 comments -
9-7-2023: Level Shifters added!
09/08/2023 at 15:19 • 0 comments