ABC: Minimal Controller Idea

Small device with a pre-programmed microcontroller like a z8 or PIC (e.g. 18F8760) or PSoC programmed with code to drive a small LCD panel, scan some key switches, communicate on a serial port and read or write to an FLASH / EEPROM. Any left over pins are available for general purpose IO.

On power up, it can read and execute a byte code language called ABC , like the CUMP Byte Code or BitScope Command Set from the serial port, if a device is connected or from the EEPROM if not. Unlike Forth or Mouse, ABC appears to be a forward notation language. E.g. instead of B 1 + A ! to assign the value of B+1 to the variable A, we can say A:B+1. (: is the assignment operator and it's actually optional). Like mouse, or Logo, ABC is very lightweight and can be implemented in a few hundred lines of C code. Like an assembly language, ABC is based entirely on moving things from source to destination with operations along the way.

Registers / Memory

Internal RAM / fast memory is addressed by the lower case letters 'a' to 'z' as a register array: 8 bit (unsigned char). Each letter points the the first of 4, 8 bit values or 32 bits of memory. e.g. "a" is register 0, "b" is register 4. 26 letters times 4 bytes is 104 ,4 byte values or 108 bytes of RAM. If available, additional memory is added to the end, as 'z' + an offset, for use as a stack up to 128 bytes. In addition, there are the following special case registers:

NUM stores numbers interpreted from the command text . The commands come from any of several input sources: The default setting is EEPROM, but if no "autostart" program is loaded, or after it has run, then commands from serial or if that isn't connected, from the local LCD / Keys are processed. NUM is used extensivly. Numbers offset register addresses, hold values from SRC to be acted upon by OP and placed in DST, act as literals, etc... NUM is an int, e.g. 32 bits signed.

SRC and DST reference one of the 108 8 bit memory location registers or (high bit set) one of the available devices (LCD/Keys, Serial, EEPROM, IO). The DST is set first, and the SRC/DST flag toggled so that SRC is loaded next at which point the current operation and comparison are performed. If SRC is not loaded (zero), NUM is the SRC. After each operation SRC is cleared. At the end of each line, the SRC/DST flag, OP and DST are also cleared. Each character location in the LCD is mapped to an address in DST. When writing to the LCD, DST is incremented after each write.

OP stores the operation to perform once we have a SRC (we should already have a DST) or when the line ends. Since there is only one SRC at a time, binary operations like addition always accumulate into DST. i.e. DST is the implied second source in binary operations. The default operation, when no other is specified, is copy. i.e. if only a DST, then a SRC are specified, the value of the SRC is simply written to the DST. OP is not cleared between SRC's so that " (the quote mark) can escape " (the quote mark).

FLAGS

Bytecode:

The goal here is to get as close to a high level language, or at least a very understandable syntax, without including a compiler, and using the minimum resources possible to interpret the bytecodes. How little code can interpret something that at least looks like a HLL?

0-9 A-F	nibble swap NUM, load hex digit into low nibble of NUM. 
	Conversion from/to decimal is too much? Maybe not?
:	Copy operation, effectivly a noop. Not really needed, included only for readability.
a-z	set SRC or DST to register. a is register 0. b is register 4. z is 100.
	a:5 sets register 0 to 5. a:b copies register 4 to register 0.
	NUM is added to the register address first. 3a is the third byte after the start of a. 
	Register 125 is 7Da or 19z (19h=25d, z=100, +25=125) SRC, DST, etc start at 5z.
	After DST is loaded, SRC/DST is set and the next address is loaded to SRC.
@	index. Replace SRC or DST with the value at that address
	 and clear op. This sets the stage for another op and SRC.
	e.g. b@a sets the DST to the address of b plus the value of a.
	If the SRC is a port or port pin, read that value in. 
"	(Quote) Text. Each following char is copied to the DST until the ending quote.
	If the DST is a variable, the chars are actually copied into FLASH and the var is
	set to the starting address of the string in FLASH.
	If the operation was already " when a new starting " is seen, 
	put a " to the dest then enter text mode. "Push ""START""" prints Push "START"
#	Converts the value of source to decimal digits and copies it to DST.
	incrementing DST after each digit. 
+	set operation to add. a+b adds b to a. a:b+5 sets a to b then adds 5. 
	if there is no SRC, the NUM is used as the SRC. a+1 increments a.
	maybe if last op was +, load 1 into NUM. a++ increments a.
-	set operation to add, pre operation to negate or not, and set carry
&	set operation to bitwise AND. a-&b ANDs a with NOT b. a&-b subtracts b from a (& is ignored)
|	set operation to bitwise OR
=	set compare type to equal
<	set compare type to less than
>	set compare type to greater than
~	Not. Toggle true/false flag. Use with greater less and equal. 
		; e.g. a<b~ will set the true flag if a is greater than or equal to b.
		; >~ is less than or equal too. <~ is greater than or equal too. =~ is not equal
?	if. Skip to the next line if the comparison fails (not TRUE)& keep skipping indented lines.
!	else. Skip to the next line if the comparison succeeded & keep skipping indented lines. 
(	parms. Prep for a function call by pushing parameters. 
)	call. Call the function pointed to by DST by incrementing PCP and loading DST to PC.
.	return. Process OP/SRC, decrement PCP.
A	(Analog) set Port pin in DST to output PWM in SRC. e.g. P2A100
	set Port pin in SRC to read analog values in e.g. i:2P1A
D	(Delay) DST microseconds between IO commands. Clears DST. e.g. 100DP0HLHL
K	(Local) set SRC or DST to the LCD/Keys. 
	The actual value stored is 0x88
	NUM is used to select the position?
S	(Servo) set Port pin in DST to drive RC servo to postion in SRC. e.g. P1S90
T	(Terminal) set SRC or DST to the Serial port. 0x89
P	(Port) set SRC or DST to IO pins. The value stored will be 0x80-0x87. e.g. 2P1 is port 2 pin 1
	NUM before P selects the port if more than 1 available. stored in the lower 3 bits of the value.
	NUM after P selects the pin. These are 1 to 8, not 0 to 7 so that 0 can indicate the entire port.
I	(In) set the Port or Port pin in SRC to an Input. E.g. a:2P7I@ reads port 2 pin 7 into a
O	(Out) set the Port or Port pin in DST to an Output (Can't H or L just do this?)
H	(High) set the Port pin(s) in DST to high. e.g. P1H sets port 0, pin 1 (the second pin) high.
L	(Low) set the Port pin(s) in DST to low. e.g. 2PL sets all pins on port 2 low.
	When the pin is an input, H and L set or clear TRUE based on the pins value.
U	(Up) set Port pin(s) in DST to inputs will internal pull-up
W	wait. Delay for DST u seconds. Not implemented.
J	(Jump) move NUM lines ?

Quote Escaping

Case '"' //Start putting out text
 while
  temp = SerialGet // after the '"'
  temp=='"'? // but two quotes
   LCDPutChr temp // puts out a quote chr
   temp = SerialGet // after the '"'
- - - - Until temp '"' //Until the next quote

"Push ""START"""

  1. While true
    
  2. - CMD = GetCMD
    
  3. - Select CMD
    
  4. 
      
  5. - - Case '"' 			//A After a '"'
    
  6. - - - temp = GetCMD 		//B  start reading text
    
  7. - - - temp=='"'? 		//C  but two quotes
    
  8. - - - - PutDST temp 		//D  puts out a quote chr
    
  9. - - - While temp NOT '"' 	//E Until the next quote
    
  10. - - - - PutDST temp 		//F  put out chrs
    
  11. - - - - temp = GetCMD 		//G  and read more text
    

Notice how something like "Push ""START""" gets executed: The first quote gets us to line //B above where temp gets loaded with "P". //C fails and //D is skipped. Since temp is no longer a quote, we are now inside the While that started at //E and we put the character out at //F and get another at //G repeatedly until we reach the second quote. At that point, we fall out of the loop, all the way back to the outer loop where we get another command

Comments:

Indexing

Let's think about the indexing of one array by another and how that can be supported with minimal effort. Lower case letters cause their address to be loaded to DST or SRC, not their value. If you want to addr an element of 'a' by an offset in 'i' then the value in 'i' must be added to the addr of 'a' . There are two ways to do this: Make the var code translate any existing value in SRC or DST from address to value before adding in the next var addr. Or: xlate the new var adr to a value before adding it to DST or SRC only when they are not empty.

Make lower case letters add their base address to what ever is in NUM before loading SRC or DST. This moves the offset before the variable so 4a is the same as b.

Copy operation?

One of the goals of this design is to have a syntax that is as close to a high level language as possible without a compiler. To that end, should we add a character for the default copy operation? e.g. a=b+1 is totally clear, but = is also needed for comparisons and really, no operation needs to be specified there at all: ab+1 does the same thing. a:b+1 is used for 'syntactic sugar'.

Double Operations?

Is it useful to detect and use the case of more than one operation being specified without a SRC between them? E.g. ++ for increment. &- for AND NOT (rather than -&). In some cases, the change requires a lot of work: a<=b is not as easy as it looks since a>b~ does the same job without requiring a new, combined operator. We can "fake" ++ by loading NUM with 1 when we see + and the last op was + as well. Is it worth it? Not implemented.

@@	?
""	put a " to the dest stay in text mode. "Push ""START""" prints Push "START"
++	load 1 into NUM. a++ increments a.
--	load 1 into NUM, so a-- decrements a.
&&	set operation to logical AND?
||	set operation to logical OR
==	?
<<	set operation to shift left?
>>	set operation to shift right?
~~	?
??	?

Multiple Character Operations?

<=	make less than into less that or equal. represented internally by {
>=	greater than or equal. OP is }.  

We might be able to cheat and just add multiple opcodes together. e.g. '<'(60)+'='(61) = 'y'(121) so if we treat an op of 'y' as less than or equal two, we don't have to take any care when we find ops other than just total up their values.

Sadly '>'(62)+'>'(62) = '|'(124) which means that shift right crashes into the logical or operation. As a result, we probably need active detection of multichar opcodes.

Repeated Operations?

If NUM is not zero when parsing an operation, set a count. Then when performing the operation, repeat it, after incrementing SRC and DST, then decrement count and loop until count is zero. So a3:b copies register 4,5,6, and 7 to registers 0,1,2, and 3. If multibyte values are taken in LSB first (little endian) order, then a3:b3+c actually moves a LONG at b to a then adds c as a LONG.

Writing to perminant memory

We might use :@ as the command to write to free flash memory and store the starting address in the destination. e.g. a:@'P1H W10 L' Update: Just note that the destination is a variable so the @ isn't needed. e.g. a:"P1H" records P1H to FLASH and sets a to the starting address of that string.

Program Counter

Something needs to address the EEPROM or FLASH when instructions are being pulled from there. That is our program counter (PC). If we used one of the registers, we could affect the flow of the program with more than the skips. Use "p" or "z"? Or keep the PC internal?

It would be nice to come up with a clever way to save the current PC when jumping to a new location in the program. This would allow more complex flow control like call, parameter passing, and return to be written in the language itself. One possibility is to follow the 1802 model.

Program Counter Pointer: In an 1802, there is no specific program counter register. Instead, there are a set of general purpose registers R(x), any one of which can be used as the program counter called R(P). Another register (P) pointed to the register that would be used as the PC. There are also no call instruction. To call, you load another general purpose register wth the address of the subroutine, then set P (the PC pointer) to that register. The subroutine then executes and returns by setting P back to the original R. Let's call the 1802 P register the Program Counter Pointer or PCP

For commonly used routines, you dedicate a register to the subroutine, and initiallize it to start, not at the beginning, but a few instructions into the sub. The sub, when it is ready to return, jumps back to it's very start, where PCP (the PC Pointer) is set back to the main PC, leaving the PC of the subroutine back at the entry point, ready for the next call.

Although the 1802 had a dedicated stack and jump instruction, I see no reason why these are really necessary: To jump, you could just load a new register to be used as the PC with a new address rather than attempting to preserve the address all the time.  To form a call/return stack, you can use the next  register to point to the subroutine and the subroutine returns by decrementing the PCP. (the real 1802 didn't have an inc or dec P instruction!).

If we initialize the program counter to z and the program counter pointer to y on startup, then that means there is no stack and returns will would fail because the system could prevent the PC from Decrementing past the PCP. If the platform has more memory the starting program can set y to z+some number effectively allocating a stack of that size. Maybe z can be the PCP and the initial PC at the same time? A 1 byte PCP followed by a 3 byte PC?

Or have the PC be 'z' or some other register, and have the PCP internal.

Parameters

The real trick is making that work in a way that looks more like standard subroutine and function calls in a higher level language.

If the "a" register has been initialized to point to the beginning of a sub thread of bytecode in the EEPROM, and we write "a(b)" the "(" could set a flag, indicating that a subroutine call was being parameterized, then the following byte codes could be loaded into a special parameter call stack as references to the actual memory addresses. The ")" would then push the return address (the current PC), a count of parameters, and load the value of the a register into PC.

In the sub thread, references to "a", "b", "c", etc... would point to the values of the parameters on the stack, instead of to the regular memory location for those registers. E.g. in the "a" routine, a reference to "a" would actually end up affecting the value of "b" since the call was started with "a(b)". If the call to "a" had been made with "a(c)" then a reference to "a" would affect "c".

At the end of the sub thread, the PC would be popped from the stack and the parameter pointers cleared.

This mixing of letters as registers and as pointers to subroutines is less than ideal, but perhaps better than limiting the number of subroutines that are possible.

Devices

LCD: Try to include space for a 15 pin header with an extra IO line on pin 15 to support e.g. 4x16 displays with a second Enable line.

Ports

P0.0
P0.1
P0.2

P2.0
P2.1
P2.2
P2.3
P2.4 LCD.D4
P2.5 LCD.D5
P2.6 LCD.D6
P2.7 LCD.D7

P3.0 AN1
P3.1 AN2
P3.2 AO1

Notes

There is a case that is currently unused: When DST has not be set (zero) and an operation or number is found in the command text. Can we think of a use for that? 9=a? is better programming practice than a=9? so maybe NUM should be the dest when DST is not loaded?

It would be nice to bit twiddle pins without specifying port. e.g. 1H10W1L to put a 10us pulse on pin 1 of the default port.

See also:

Comments:

Questions: