Small device with a pre-programmed microcontroller like a z8 or PIC (e.g. 18F8760) programmed with code to drive a small LCD panel, scan some key switches, communicate on a serial port and read or write to an 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). 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.
a-z register array: 8 bit (unsigned char) unless indicated. Each letter points the the first of 4 8 bit values. e.g. "a" is register 0, "b" is register 4.
NUM stores numbers from the command text
SRC and DST reference one of the 125 8 bit registers or (high bit set) one of the available devices (LCD/Keys, Serial, EEPROM, IO). The default setting is LCD/Keys. 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, SRC/DST and DST is cleared.
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 so that " can escape ".
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. : 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. @ Set operation to index. When SRC is loaded, replace 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. " (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 ? skip to the next line if the comparison fails (not TRUE) K (Local) set SRC or DST to the LCD/Keys. The actual value stored is 0x88 NUM is used to select the position? 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 DST to an Input O (Out) set the Port or Port pin in DST to an Output 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. J (Jump) move NUM lines
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
- CMD = GetCMD
- Select CMD
- - Case '"' //A After a '"'
- - - temp = GetCMD //B start reading text
- - - temp=='"'? //C but two quotes
- - - - PutDST temp //D puts out a quote chr
- - - While temp NOT '"' //E Until the next quote
- - - - PutDST temp //F put out chrs
- - - - 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
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 5a is the same as b.
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 a possibility.
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?
@@ ? "" 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? ~~ ? ?? ?
:@ record the following line to EEPROM or FLASH and assign the starting address to the DST.
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.
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.
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"?
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?
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.
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.
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
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?
|file: /Techref/idea/minimalcontroller.htm, 16KB, , updated: 2017/6/21 14:48, local time: 2017/12/14 01:46,
|©2017 These pages are served without commercial sponsorship. (No popup ads, etc...).Bandwidth abuse increases hosting cost forcing sponsorship or shutdown. This server aggressively defends against automated copying for any reason including offline viewing, duplication, etc... Please respect this requirement and DO NOT RIP THIS SITE. Questions?|
<A HREF="http://www.piclist.com/techref/idea/minimalcontroller.htm"> Minimal Controller Idea</A>
|Did you find what you needed?|
PICList 2017 contributors:
o List host: MIT, Site host massmind.org, Top posters @20171214 RussellMc, Van Horn, David, Sean Breheny, James Cameron, alan.b.pearce, IVP, Neil, Bob Blick, David C Brown, John Gardner,
* Page Editors: James Newton, David Cary, and YOU!
* Roman Black of Black Robotics donates from sales of Linistep stepper controller kits.
* Ashley Roll of Digital Nemesis donates from sales of RCL-1 RS232 to TTL converters.
* Monthly Subscribers: Gregg Rew. on-going support is MOST appreciated!
* Contributors: Richard Seriani, Sr.
Welcome to www.piclist.com!