Louve
Louve is a 16 bits processor architecture. She* has 16 operations and handles 16 bits numbers (which means the highest positive number is 65535).
Memory addresses are also 16 bits numbers, so the highest amount of memory a program can have is 64K words.
* Louve is a female wolf in the Ferale Computer project.
Operations
0 | MOV | Move a value from a source to a destination |
---|---|---|
1 | ADD | Add two values together |
2 | ADC | Add to values plus the carry of the previous operation |
3 | SUB | Subtract two values |
4 | SBC | Subtract two values and the carry of the previous operation |
5 | SHF | Shift bits left or right (multiply or divide by 2) (1) |
6 | ROT | Rotate bits left or right (1) |
7 | SWP | Swap two values |
8 | NOT | Inverse all bits of a value |
9 | AND | Logical AND between two values |
A | IOR | Inclusive OR between two values |
B | XOR | Exclusive OR between two values |
C | CMP | Compare two values and save the results in flags Z, N and V (2) |
D | DMC | Direct Memory Copy (3) |
E | Command (4) | |
F | Jump (5) |
Operation encoding
Each operation is expressed in binary as a 16 bits number. It's easy to express in hexadecimal because every nibble (pack of 4 bits) encodes a different part of the operation.
Operation | Source register | Destination register | Modes | |
---|---|---|---|---|
???? | ???? | ???? | ?? | ?? |
Example
1AB9 | ||||
---|---|---|---|---|
1 | A | B | 9 | |
Operation | Source register | Destination register | Modes | |
0001 | 1010 | 1011 | 10 | 01 |
ADD | Register A | Register B | Pull | Reference |
The operation 1AB9 reads memory at address given by register A, increment this address (pull mode), then reads memory at address given by register B, adds the values together (ADD) and puts the result in memory at address given by register B.
In assembly language, it would be written ADD (A++), (B)
.
In a real situation, it could be part of a "sum" function: Register A would point to the list of values to be summed, and B to the result. Calling this in a loop would sum up all values.
Modes
00 | Direct | R | Read or write the value in the register |
---|---|---|---|
01 | Reference | (R) | Read or write the value in memory at address in register |
10 | Pull | (R++) | Read or write in memory then increment address in register |
11 | Push | (--R) | Decrement address in register then read or write in memory |
Source mode | Destination mode | |
---|---|---|
00 | Source | |
01 | Memory[Source] | |
10 | Memory[Source] (then Source+1) | |
11 | Memory[Source-1] | |
00 | Destination | |
01 | Memory[Destination] | |
10 | Memory[Destination] (then Destination+1) | |
11 | Memory[Destination-1] |
(1) Shift and Rotate
Those two operations don't use the Source argument like others. Instead of using Source as a register, it tells how many bits to shift or rotate, and the Source mode gives the direction.
Shift | Bits to shift | Destination register | Direction | Destination mode | |
---|---|---|---|---|---|
0101 | ???? | ???? | ?? | ?? | |
00 | To the right | ||||
01 | To the left |
Rotate | Bits to rotate | Destination register | Direction | Destination mode | |
---|---|---|---|---|---|
0110 | ???? | ???? | ?? | ?? | |
00 | To the right | ||||
01 | To the left |
(2) Comparison
CMP compares two values and save the result in three flags. It's up to you to decide which result is significant depending on what the initial numbers were supposed to be.
Z | A=B |
---|---|
N | A>B (unsigned) |
V | A>B (signed) |
(3) Direct Memory Copy
DMC does an immediate copy of multiple consecutive values from one part of memory to another.
It's faster than implementing a loop yourself.
Copy | Source | Destination | Size |
---|---|---|---|
1101 | ???? | ???? | ???? |
Source and Destination registers must contain addresses. They will be read with mode 10 (pull).
Size register must contain an unsigned number which will act as a counter for the operation.
Example
MOV 1200, R8 ; source of the data
MOV F000, R9 ; destination address
MOV 80, RA ; size = 128 words
DMC R8,R9,RA
(4) Command
E0 | HALT | Stop the program |
---|---|---|
E1 | RTI | Return from interrupt |
E2 | SWI | Software interrupt to address in Destination |
E3 | INCB | Move to next register bank |
E4 | DECB | Move to previous register bank |
(5) Jump
Jump | Source register | Condition | Source Mode | Type | |
---|---|---|---|---|---|
1111 | ???? | ? | ??? | ?? | ?? |
Source | Condition | Mode & Type | |||
---|---|---|---|---|---|
???? | ?? | Source register and mode | |||
??? | Condition (which flag to check) | ||||
? | Negate condition? | ||||
00 | Jump absolute | ||||
01 | Call absolute (subroutine) | ||||
10 | Jump relative | ||||
11 | Call relative (subroutine) |
Jump example
FA95 | ||||||
---|---|---|---|---|---|---|
F | A | 9 | 5 | |||
Jump | Source | Condition | Mode & Type | |||
1111 | 1010 | 1 | 001 | 01 | 01 | |
1010 | 01 | Get address from Memory[Register A] | ||||
1 | 001 | If max is not set (flag 1) | ||||
01 | Call subroutine at absolute address |
Registers and flags
R0 | R1 | R2 | R3 | R4 | R5 | R6 | R7 |
---|---|---|---|---|---|---|---|
Banked registers (can be switched with INCB and DECB) |
R8 | R9 | RA | RB | RC | SP | SR | PC |
---|---|---|---|---|---|---|---|
Fixed registers | Stack pointer | Status register | Program counter |
SR | |||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
? | ? | ? | ? | ? | ? | ? | ? | I | V | N | Z | C | M | A | |
Bank pointer (changed by INCB and DECB) | (unused) | Mask interrupts | Overflow | Negative | Zero | Carry | Max | Always 1 | |||||||
111 | 110 | 101 | 100 | 011 | 010 | 001 | 000 |
Numbers
Numbers are 16 bits. It means there are encoded in binary with 16 digits.
Depending on your interpretation, numbers can be considerer unsigned (all the bits encode a positive value), or signed (the highest bit means "negative" and the remaining 15 bits encode the value).
Mixing them in operations is not recommended, and might trigger the "overflow" flag.
Min | Max | |
---|---|---|
Unsigned | 0000 | FFFF (65535) |
Signed | 8000 (-32768) | 7FFF (32767) |
Parts of a binary number have special names.
16 bits | ABCD | a word |
---|---|---|
8 bits | AB | a byte |
4 bits | A | a nibble |
Louve expects the memory to contain 64K words (65536 numbers of 16 bits).
It's usually implemented in hardware by putting two 64KB memory chips together.
Bourdon syntax (assembly language)
It can be tedious to write (and read!) a whole program in hexadecimal. So there's a program called an assembler, which reads source code and transforms it into the equivalent binary format.
The assembler for Louve is called Bourdon and has the following syntax.
Syntax
| A line can start with a label. |
| A label with spaces before is a sub-label. |
| Specific keywords are assembler's meta-operations. Args are separated by spaces. |
| Anything after a semicolon is a comment. |
Meta operations
| Move the assembler to a specific address. |
| Define a constant value. |
| Write raw binary data to the current position. |
| Repeat raw binary data to the current position NUM times. |
| Write raw unicode data (in UTF16) to the current position, until end of line. |
| Return from subroutine. Shortcut for MOV (SP++), PC . |
Meta examples
| Move to address F100 in the assembled binary. |
| Define the name "STDOUT" to be replaced everywhere in code by the value 7F80. |
| Write binary words 006C 006F 1234 at current position. |
| Fill the next words of the binary with A0A0 B0B0, 15 times (0F). |
| Write the unicode string at current position. An antislash is used to input any hexadecimal value directly. To write an antislash, use "\\". |
Hello world example
Address | Program | Code |
|
|
|
Lexer/Parser/Assembler algorithm
- Start with space?
- NO: Meta or Operation or Label
- YES: Meta or Operation or Sublabel
- Semicolon: Skip until end of line
Forbidden identifiers for labels
Operations: MOV ADD ADC SUB SBC SHF ROT SWP NOT AND IOR XOR CMP DMC JUMP CALL
Commands: HALT RTI SWI INCB DECB
Meta: AT DEF DATA FILL STR RET
1 letter words, including flags: I V N Z C M A
Register names: R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 RA RB RC RD RE RF SP SR PC
2 and 4 digits hexadecimal numbers: FEED, DEAD, BEAF, F00D, AD, FADE...
5 letters words and above will always pass as labels. The check is only required for the first 4 letters.
A finite state machine is used to quickly check if a token is a reserved keyword (and determine which one, to be used in the parser too).
A-D-D Operation 1
| \
| C Operation 2
|\
| N-D Operation 9
\
T Meta AT
S-U-B Operation 3
|\
| B-C Operation 4
|\
| H-F Operation 5
|\
| W-P Operation 7
| \
| I Command E2
|\
| P Register RD
|\
| R Register RE
\
T-R Meta STR
D-M-C Operation D
|\
| E-C-B Command E4
| \
| F Meta DEF
\
A-T-A Meta DATA
R-O-T Operation 6
|\
| T-I Command E1
|\
| E-T Meta RET
\
hex? Register R?
C-M-P Operation C
\
A-L-L Operation F (call)
I-O-R Operation A
\
N-C-B Command 3
P-C Register RF
M-O-V Operation 0
N-O-T Operation 8
X-O-R Operation B
J-U-M-P Operation F (jump)
H-A-L-T Command 0
F-I-L-L Meta FILL
This tree is used on every token to generate the Abstract Syntax Tree (AST) of the source code.
Then the parser checks the grammar.
Then the assembler can move through the AST and apply two passes: Generate as much binary as possible while leaving "holes" for the missing references, then passing again filling those holes with calculated values.
Credits
The initial Louve design was heavily inspired from the QNICE architecture by Bernd Ulmann and their team. Thanks to them!
The uxn architecture and the Varvara project were also a big influence on my work. The uxn community rocks!
Apart from those inspirations, the Ferale Computer project is made with love and dedication by Zoé since 2018.