Sunday, December 11, 2022

Tali Forth 2 on Linux

When people on IRC have questions about Forth or I can't remember how a particular word works, it's nice to have a Forth handy to try things out on. For this, I usually open Tali Forth 2 in a browser window on my website. Even though it's a 6502 Forth running in a JavaScript 6502 emulator rather than a native x86 Forth like Gforth, it works just fine for experimenting. Since most of my programming is on Linux now, it seemed like a good idea to have a way to do my tests in a terminal.

There were several choices for getting Tali Forth 2 running on Linux. One of the easiest ways is to use an existing emulator like Py65 since Tali Forth 2 comes with a pre-compiled binary ready to be emulated. However, this was less interesting than using one of my own emulators. Another option was the JavaScript 6502 emulator running on my website. As mentioned in the post about my 6507 calculator, this also runs in a terminal thanks to node.js. Another option was to extract the emulation engine from my 6502 Interactive Assembler, although it seemed this would be time consuming to untangle since the emulator was not designed to work without the assembler. In the end, it was the most convenient to use the 6502 emulator written in MIPS assembly I've been working on for another project using the tools mentioned in my post about MIPS development in Linux. As mentioned in that post, a binary compiled for MIPS will run right from the Ubuntu command line as if it were native thanks to QEMU and also have access to the terminal through Linux syscalls. Going with this emulator was also an opportunity to double check my MIPS code emulates the 6502 correctly.

The first step was assembling a custom version of Tali Forth 2 using the Ophis assembler. This is really easy since all the information specific to a particular platform is abstracted into a single file where the routines for input and output are defined. Many emulators map I/O to memory addresses since that matches how a real 6502 works. The disadvantage of this setup is that checking all memory access for reads and writes to those addresses could really degrade emulator performance. The alternative I went with is using the BRK instruction as a type of software interrupt with the byte following the BRK specifying a write or read to the terminal or to end the program. Other codes after BRK helped with debugging and getting information out of the emulator when necessary. Assembling Tali Forth 2 with the Ophis assembler on Windows then transferring the result to Linux each time was kind of a pain, so I installed the assembler for Linux and moved Tali Forth 2 over to that. Like most of my projects, it tends to be more convenient to move as much as possible to Linux and avoid sending files back and forth between Windows.

After the modified Tali Forth 2 was assembled, it failed to boot as expected in the emulator. The startup message would print and the program would accept keyboard input, but nothing happened after pressing enter. Even though it didn't seem like a good idea initially, I did extract the emulator code from my 6502 Interactive Assembler to use as a comparison. Unfortunately, this did not boot Tali Forth 2 correctly either which might be because some part of the emulator was not decoupled from the assembler code successfully. In any case, having two different versions of the emulator let me log the state of the processor cycle by cycle and compare them side by side with WinMerge on Windows. After a while, transferring the cycle logs from Linux to Windows got to be annoying, so I made a Python script to do the comparison and found that the MIPS code was not setting the 6502's V flag properly in the SBC instruction. Tali Forth 2 booted and functioned as expected after fixing this.

There are three different sets of syscall numbers for MIPS Linux, and all three sets are different than the syscall numbers for x86 Linux. Compiling a short C program for little-endian 32-bit MIPS on Compiler Explorer that references syscall constants like SYS_read and SYS_write showed in the assembly listing which set of numbers to use. By default, SYS_read accepts a whole line of input until the user presses enter which is not ideal in Tali Forth 2 since it expects input one byte at a time. One way around this would be to accept a whole line of input then feed the bytes into Tali Forth 2 one by one, but because input is echoed, everything typed in would appear twice. Digging into how Linux handles the terminal showed that the termio functionality accessible through the SYS_ioctl syscall can adjust all kinds of things about terminal input including whether it echoes and whether it returns after a single byte of input. Testing out some of this functionality showed that if the program ends without restoring the termio settings, the changes stick around making the terminal impossible to use since it will then only accept a single byte of input and that input is not visible. I tried installing a handler for SIGINT to catch Ctrl+C, which ends the program, and restore the termio settings, but QEMU intercepted the Ctrl+C and exited before the emulated program could restore anything. Next, I made a C program to save the termio settings before running QEMU then restore them after, but testing the program showed that QEMU only leaves the terminal misconfigured if it exits before Ctrl+C is pressed. Adding MIPS code to restore the termio settings when Tali Forth 2 exits normally was enough to solve the problem and eliminate the need for the wrapper C program.

After Tali Forth 2 was working as expected, I turned to mecrisp-across which runs in a Linux terminal on QEMU for ARM. Like my first try with the MIPS version above, mecrisp-across accepts a whole line of input before doing anything then echoes the input as output, so I suspected the same kind of strategy with termio would fix the problem. Poking around the source for mecrisp-across didn't turn up the offending call to SYS_read. The author, Matthias Koch, was very helpful explaining how everything is arranged and pointed me toward the source for mecrisp-stellaris which mecrisp-across links in. He also explained that terminal behavior can be modified by wrapping the program in a short shell script:

    #!/bin/bash

    stty -icanon -echo
    qemu-arm-static ./mecrisp-stellaris-linux-with-mecrisp-across
    stty sane

This eliminates the need to modify mecrisp in any way and produces the same result where the program accepts input one byte at a time. Now I have both Tali Forth 2 and mecrisp-across to experiment with in a terminal. 

No comments:

Post a Comment