An ATTINY based TIS-100 Clone
To make the experience fit your profile, pick a username and tell us what interests you.
This is a simplified version of previous files.
ino - 4.14 kB - 06/08/2016 at 20:38
I have been working on other projects, but I do have the design and most of the code ready for a BEN in Python.
However, there are some questions left about how to connect the Pis - the simplest, but least authentic way being to use the MCN (Master Control Node) as the communication point between all nodes.
I've also been toying with Docker, but I think this would make serial comm. complicated.
Link to article.
From the very beginning I've wanted to implement this project with processors and parts that "felt authentic" - that's part of the challenge of a problem like the TIS-100.
I found a way of translating TIS-100 assembly programs into a byte-code that was efficient and took the fewest number of cycles to execute. I have experimented with single method C code so that nothing gets put on the stack. Through all of it, I thought that the ATTINY would be a sufficient chip.
The ATTINY has enough program space to hold the small program. The ATTINY has enough RAM to run our program. The ATTINY has enough IO (just enough) to support the communication protocol I came up with. However, it does not have enough raw speed.
The Raspberry Pi Zero is overkill. It's at least 10x faster than what we need, it runs a full OS, it has video out for every single node, it has more than enough IO, it has its own disk for each node. It's overkill, but it's overkill in a pleasant way. There are some freedoms to be gained from switching platforms to the Pi Zero.
My current intention is to still keep 'real' connections between each of the boards by building out a back-plane that MUX's the comms between the Pi Zeros. However, I will probably use the 12 nodes as Serial Gadgets hooked to a master node Pi Zero that will handle visualization, keyboard, and syncing.
However, there are some really dirt-cheap LCD monitors that are about 3 inches that I could hook to each one of the Zeros -- which would emulate the baffling visualization we see in the game.
I updated the emulator file for Arduino. I simplified some of the logic, which makes the program file slightly bigger, but makes the program itself drastically shorter.
However, there's a problem. The compiled Arduino code is really inefficient for our simple operations.
Even if we weren't using 16 bit Ints we'd still have some problems with speed.
If every single one of our emulated operations took 10 clock ticks we'd still have an operational speed of 1.6 Mhz. Currently every single operation is taking upwards of 100 clock ticks.
In AVR architecture some operations like moving a value from one register to another takes 2 clock ticks, but 100 seems excessive for the simple code we have.
The solution for a faster emulator will likely be AVR targeted C. Although I do write in assembly, I'd rather do this sort of work in C, so I think it will be a good compromise between speed and power.
I have shared working Arduino code for a single node - it just does an HCF and loops back.
However, you can now write and compile your own code.
It's manual right now because I haven't written a program that will allow you to transfer code to the emulator (I'm sorry, but I'll get to it soon).
1. Write code for a single TIS-100 node.
2. Compile the code using the compiler.
3. Copy from the 2nd position to the end for the code marked with % - this is your program code. Insert commas (,) between the arguments and put them in your program array in the Arduino code.
4. Copy from the 2nd position to the end for code marked with a $ - these are your constants - put them in the constants array.
5. Copy from the 2nd position to the end for code marked with a : - these are your labels - put them in the label array.
6. Compile the Arduino code.
I'm about to write a TIS-100 adding program, so I'll probably upload that as well.
I have added a multi-pass Assembler script, limited to assembling a single node at a time.
To use, you will need to open the script and edit the paths for the files to fit your own environment. The first file is the one you wish to compile, the second and third are output files for the build process. The third file outputs the program in Hex, then labels after the ':' and constants after the '$'.
This output can then be uploaded to the microcontroller running the Basic Execution Node emulator.
MOV LEFT RIGHT
MOV DOWN LEFT
You can see how these HEX values map to our opcodes.
If you don't read HEX you can either take my word, learn hex, or use a handy-dandy hex converter (which is what I do for bigger numbers): http://www.binaryhexconverter.com/hex-to-decimal-converter
I'll be uploading the Arduino code to run a single node in serial interactive mode in the next day or two.
Each node must be able to load code on the fly. I have created 3 execution modes in the core.
1. Run mode. In this mode the node will continually load the next instruction after receiving the 'begin cycle' signal.
2. Debug mode. In this mode the node will report its status to MCN after completing work.
3. Program mode. In this mode the node will wait for MCN to transmit a new program.
The program transmission is structured so the node knows exactly how much to load.
1. The number of program bytes is sent.
2. The number of constants is sent.
3. The number of labels is sent.
Then the data for each of these is transmitted and stored in the correct arrays.
This represents a complete OPCODE list with 14 Constants (int values) available and 10 Labels (byte values) available for creating subroutines. Constants were favored over Labels, but this is up for evaluation. NOP and HCF were both added to the official OPS-List. There is space for 2 more OPS...
Mixing data and op-codes seemed like an unpleasant consequence of the way I had earlier proposed writing op-codes. Besides that, parsing the op-code would take some bit-masking at a minimum, which although fast, still takes a cycle. We're trying to minimize cycles as much as possible and come as close to an effective operation speed of 5MHZ as we can.
When I woke up this morning a new plan was in my mind. Instead of having pointers to the constants held in another array as a second 8-bit instruction that we'd have to keep track of in the Program Counter, why not build a table of 256 commands that already included the combinations of <SRC> and <DST> built in? That way, we could evaluate where to send data extremely quickly. This blows up the number of directly addressed commands, but I think we can fit a few Constants and a few Labels in there and maintain this scheme.
The biggest bloat is going to be in the number of instructions needed for MOV <SRC> <DEST>. We have 5 available sources and each of those has 4 available destinations (we will ignore the fool who wants to move ACC to ACC). Then, we will need an additional code for every single Destination for every single CONST we allow to be assigned. If we allow 16 CONST definitions, our OP-CODE count for MOV jumps to 36. Honestly, 16 is probably more CONST than are needed per node, but I don't want to limit the user very much if we can avoid it.
This scheme allows a 1-to-1 parity between the number of OP-CODES we have in compiled code and the number our users type into the screen. This allows JMP commands to work on a 1-to-1 basis without inspecting the array. Binary table coming soon.
In the last project update, I introduced the concept of dealing with constants through the use of an array with a pointer.
So, in our program array, we might have something like this:
The 8 bits are split into the move command and a marker for VAL, then the second is marked with VAL and a position in the VAL array. Although all of this is clever, it ends up wasting bits.
It will be more efficient to have [(MOV|VAL) (11001101)(10101010) - This means that when we are moving a value, we will have to increment the Program Counter by 3 - which is a little bit of book-keeping, which is what we were trying to avoid, but it's more bit efficient.
However, it may prove problematic in the future when we're trying to calculate JMP commands. As we continue to evaluate how we're implementing the rest of the language, we will resolve this problem.
The assembly definition for the TIS-100 has several instructions that contain too much information for a single 8-bit instruction. There are two solutions that seem plausible to me: extend the instruction set so that combinations of Source/Destinations are encoded, or split the single problematic assembly instruction into 2 8-bit instructions. The second solution is more elegant, because the MOV command, which takes two arguments, also USUALLY takes two ticks (unless we're moving to ACC).
MOV <SRC>, <DEST> will be translated into byte-code as three discrete instructions for the Emu:
MOVTOACC <SRC> --------- OPCODE: 0000 | SRC
MOVTOTEMP <SRC> ------- OPCODE: 0001 | SRC
MOVFROMTEMP <DEST> - OPCODE: 0010 | DEST
The second instruction that is problematic for maintaining an 8 bit instruction width is any instruction that can use VAL or a LABEL instead of a register. Since both VAL and LABEL are <INT> types in the system, it would introduce a variable-width instruction set to have them embedded with the instruction. So, instead of embedding VAL or LABEL, we will instead replace the embedded val with a pointer to an array where they are stored. Since we have 4 bits -- we will be limited to 16 aliases for both LABEL and VAL per core, per program.
int Label - contains the JMP values.
int Val - contains the INT values defined in the program.
Using these two tricks, we can preserve an 8-bit instruction width which will make program execution smoother than a variable-width instruction set. It will also allow us to compute space better at compile time. (Apologies to Michael Covington, I still think this is a legitimate use of the word compile.)
Become a member to follow this project and never miss any updates