HLLPIC byte-code interpreter for PIC16-series microcontrollers ============================================================== This doc file applies to... (dates are mm/dd/yy) hllpic.sim v1.14 12/16/12 - firmware for PIC16F684 hllpic690.sim v1.23 12/28/12 - firmware for PIC16F690 hllcomp.bas 12/29/12 - HLL-to-bytecode compiler/decompiler hpicterm.bas 12/17/12 - serial terminal/uploader/extracter Homepage for this software is: http://newton.freehostia.com/embedded/ Application ----------- This software implements a fairly simple language for programming small PIC microcontrollers via a serial connection without the complexity of assembly language or "big" compilers, and once the firmware is programmed, without requiring PIC-programming hardware and software beyond a 9600-baud serial connection and serial terminal software that supports character delay. Real serial ports are rare on consumer PC's however USB-to-serial adapters are inexpensive and widely available. Successfully adapting this material to a particular application requires electronics knowledge, and the ability to use a PC to run command-line software. Unless binaries are provided, the SIMPLE2 compiler and the gputils package is needed to compile and assemble the firmware, and FreeBASIC is required to compile the PC apps. PIC programming software and hardware is required to load the firmware onto the target PIC chip. This material is compatible with Windows and Linux systems. 64-bit Linux systems require the installation of "ia32-libs" for running the apps. The intended applications for this software include... Low-speed logic replacement Analog data logging Controlling small hobby robots ...and other tasks that do not require fast processing or access to special PIC peripherals. For special applications, instructions can be added to the interpreter for performing high-speed or specialized functions, otherwise interpreted applications are limited to reading and setting pin states, reading analog pins, simple serial I/O, access to PIC registers and memory in the lower 256 bytes of ram, and byte-based math and logic operations. When running at 4mhz the interpreter fetches bytes from the external eeprom at a rate of about 3000 bytes per second, equivalent to roughly 1000 simple commands per second. The interpreted program can increase the clock rate to 8mhz to double the speed. Small programs can be executed from the PIC's internal eeprom about 3 times faster than from external eeprom, for an execution rate of up to about 6000 commands per second. As with any interpreter, the technique of interpreting bytes representing program code is much slower than running native machine code. The advantage of course is an interpreted language can be much easier to use than cryptic assembler instructions, so long as the app doesn't require fast processing. Apps that do require fast processing need to be written in assembly, or in a compiled language. Compilers can sometimes be just as or more complicated than assembly, so I use my own SIMPLE2 compiler, which as the name implies, is simple. However it does not hide the complexity of programming a PIC, and updating the code in the PIC requires programming hardware. The application I originally designed this interpreter for (in 1999) was adding programmability to a small solar-powered BEAM-style autonomous robot, the idea was to be able to connect a cable and reprogram the "brain" without distraction. For this kind of app the slow speed of the interpreter doesn't really matter as the device spends most of its time sleeping while the solar cell charges a super-capacitor, and processing delays while it thinks about what its next move will be just makes its behavior appear more life-like. Overview -------- The HLLPIC firmware consists of an interpreter that directly executes a higher-level language represented by bytes stored in an external 8KB 24FC64 eeprom chip or from the PIC's internal 256-byte eeprom, chip-specific drivers for initializing the PIC, reading analog pins and accessing the internal eeprom plus software drivers for serial I/O and accessing the external eeprom, and a menu-driven serial interface for loading and extracting bytes to and from external and internal eeprom. Presently two versions are available, a 2KW version for the 14-pin PIC16F684 chip (should work as-is for a PIC16F688), and a 4KW version for the PIC16F690. The two versions are mostly equivalent other than the '690 version puts the serial menu and associated code in the 2nd rom bank and has a menu option for listing external eeprom one 256-byte page at a time. The smaller '684 version can only extract all 8KB of the external eeprom. Either version should be adaptable to other PIC16-series PICs by modifying the initialization, A/D input code, and the internal eeprom code. The HLLPIC firmware is written using the SIMPLE2 compiler, which translates the source into Microchip-format instructions to be assembled using the gpasm assembler from the gputils package (or Microchip's MPASM[X] with minor mods). SIMPLE2 is available from my embedded page listed above. The HLLCOMP program compiles HLL source into hex bytes for loading into the HLLPIC firmware for execution. The program can also decompile extracted bytes, edit the extracted hex file to remove extra leftover code and data. HLLCOMP is a command-line console app written in a QBasic-style language, compile using FreeBASIC using the -e -lang qb options. Command line usage: hllcomp [-pause] -help hllcomp [-pause] [-debug] [-d] inputfile [outputfile] If run without parameters it prompts for the compile/decompile operation, the input file, and the output file. If outputfile is not specified then it defaults to basename.bytes when compiling and basename.decomp when decompiling, use -d to specify decompile. The -pause option causes "press a key" prompts when printing help text and before exiting the program. The -debug option causes additional information to be printed. Typical usages... hllcomp file.hll compile file.hll to file.bytes hllcomp -pause file.hll compile file.hll to file.bytes, pause at end hllcomp -d file.bytes decompile file.bytes to file.decomp The HPICTERM program is a simple console app that implements a basic serial terminal with HLLPIC-specific functions for uploading and extracting code. The program is written in the native FreeBASIC language. By default it connects to the first available serial port at 9600 baud, uses a character delay of 20mS, and assumes the compiler is named "hllcomp". Command line options can be used to specify: hpicterm [COMx [rate [delay [compiler]]]] The program functions as a simple terminal until Esc is pressed, then if it thinks it's at a HLLPIC prompt (last bytes received are "> ") then displays a menu with Load and Dump options. The Load option prompts for a filename, if not already in hex format calls the compiler to compile, then if the compile is successful, loads the code into the firmware. The Dump option prompts for a filename then extracts all 8KB from the external eeprom into a hex file. If Esc is pressed when not at a menu, the only option is Quit. When at a Esc menu, pressing any other key besides the listed options returns to terminal mode. Circuitry --------- The HLLPIC firmware requires 4 connections for serial input and output and for the external eeprom clock and data. The serial port is connected via current-limiting resistors, to avoid the possibility of excess voltage from 12V serial levels the schematics show a 5.1V zener diode across the 5 volt supply with a 10 ohm current-limiting resistor in series with the supply. If a 5V regulator is always connected then these components can be omitted, or for testing replaced with a 10K resistor across the supply lines to drain away the extra current. The zener diode method is better for low-power applications so when the processor is sleeping current drain is extremely low unless the supply approaches or exceeds 5V. An LED is recommended on the serial output pin, not only to serve as a serial activity and general indicator, but also to protect the PIC should the Rx and Tx lines be wired backwards. Here is a circuit for the stock PIC16F684 version... .----*------------*--*--*--*------------------*---10---o--< +5V _|_/ | 24FC64 | | | | | o--< ground //_\ | __ __ | | 10K *---0.1u----*--. 100K _|_ | 22K .-| U |-' 10K | | __ __ | _|_ | _|_ | *-| |-. | | `--| U |--' | DB9F connector 5.1V | *-| |-|--|--*-----| |--o------*---22K-----< pin 3 (Rx) zener | *-|_____|-|--*--------| |--o-------*--2.2K----> pin 2 (Tx) | _|_ _|_ .--o--| |------. | .--O pin 5 (gnd) | | --| |-- | 1K _|_ .--10--*-------------1K---' --| |-- 1K | LED1 _|O | --|_____|-- | `--|>|--. |O 0.1u | LED2 | _|_ _|_ PIC16F684 `------|>|--* Reset Sw. o=ISP connections _|_ Here is a circuit for the stock PIC16F690 version... .----*------*-------------------*--------*--*-*---10---o--< +5V _|_/ | | | | | | o--< ground //_\ | *---0.1u----*--. 100K | |10K _|_ | 22K | __ __ | _|_ | | 10K| _|_ | `--| U |--' | | | | DB9F connector 5.1V | --| |--o .-----*--------|--|-|-----22K---< pin 3 (Rx) zener | --| |--o-|--------------|--|-|--*--2.2K--> pin 2 (Tx) .--10--*--1K--o--| |-- | __ __ | | | | .--O pin 5 (gnd) _|O | --| |-- | .-| U |-' | | 1K _|_ |O 0.1u --| |-- | *-| |-. | | |LED1 _|_ _|_ --| |-- | *-| |-|--|-* `-|>|-. Reset Sw. --| |----|-. *-|_____|-|--* | _|_ --| |----' | _|_ _|_ | | --|_____|--. | 24FC64 | | | `---------------' | PIC16F690 `---------------------' The serial receive line was moved from RA0 to RB5 because the PICKIT 2 programmer puts too much load on the in-circuit programming lines, preventing serial reception. In an actual target app it can be moved back to RA0 by editing the HLLPIC source and recompiling/reassembling to permit using a single 5-pin header for both flash programming and connecting a serial cable using an adapter with the current-limiting resistors. The PICKIT 1 programmer does not interfere with RA0 so can be used to power the '684 version without interfering with the serial port. I've used this type of serial interface almost exclusively with PICs and have never had any issues, however if desired the HLLPIC firmware code can be modified to invert the serial polarity to use a serial interface or a USB TTL serial cable. PC software usage ----------------- Once the binaries have been compiled, the PIC flashed and the circuitry wired and connected via a serial cable, the HLLCOMP and HPICTERM programs can be used to load and run programs. For Windows... Copy the hllcomp.exe and hpicterm.exe programs to any directory, double-click hpicterm.exe to run. Press reset on the PIC circuit, the menu should appear. For Linux... Copy the hllcomp and hpicterm binaries to any directory. Create an empty text file named "hpicterm.sh" and carefully type in the following script... #!/bin/bash # path/location of hpicterm program... hpicterm="./hpicterm" # add . to path for running hllcomp.. PATH=".:$PATH" export PATH # uncomment one or edit to select terminal... xterm -e "$hpicterm" # konsole --workdir . -e "$hpicterm" # gnome-terminal -x "$hpicterm" Note.. If copy/pasting from this doc file you will probably need to convert the file to unix format using a command such as "flip -u hpicterm.sh" (the flip utility should be in the repository of most Linux distributions). Or copy each line without the line end and manually enter the lines. Note.. If using KDE, comment (#) the xterm line and uncomment the konsole line. If using Gnome, comment (#) the xterm line and uncomment the gnome-terminal line. Otherwise xterm needs to be installed. Note.. under Linux you need to be set up with the proper permissions to access the serial port. Enter the command: ls -la /dev/tty* and note the group name for ttyS0 or ttyUSB0 if using a USB adapter, typically "dialout". Add yourself to this group using your OS's facilities. Once saved, right-click/properties and set to allow execution. Double-click hpicterm.sh to run, if prompted select "Run". The app should run in a window, press reset on the PIC for the menu. If the HPICTERM app window does not appear, check the serial port, make sure Windows drivers are installed etc. When the HPICTERM program is run without parameters it assumes that the first serial port it detects is the one connected to the HLLPIC firmware. If this is not the case then it must be run from a command line to specify the comm port.. i.e: hpicterm COM2 Note... if using the PICKIT 2 programmer and powering the circuit from the programmer, then the programming software must be used to command the programmer to apply power to the circuit and release MCLR. To load software, create a text file in the same directory using a text editor. For example this simple LED-flashing demo... 'randomly flash LED do gosub scramble milliseconds = rtcc if milliseconds < 30 milliseconds = 30 flashled loop scramble: leftshift rtcc rtcc = rtcc xor n increment n return (saved as "test30.hll") The process looks like this... ***** HLLPIC TERMINAL 1.02 ***** Press reset switch for HLLPIC menu Press Esc to show options or quit [reset pressed on the PIC circuit] [after menu displayed, Esc pressed for load menu] HLLPIC 1.23 P) Prog int L) List int Z) List ram X) Prog ext Y) Dump ext S) Dump sel R) Run > [[[ Load Dump Quit ]]] L Hex or HLL file to load: test30.hll Running hllcomp to compile... HLLPIC Compiler Dec 29 2012 Compiling HLL file test30.hll to test30.bytes... Generated 29 bytes in 1 extent. X 0000: 30.13.1E.2B.4E.01.01.4E.2B.08.4F.1E.1E.2B.4F.1E. 0010: F6.20.00.AE.01.1E.01.4E.01.8D.02.3D.0D. END P) Prog int L) List int Z) List ram X) Prog ext Y) Dump ext S) Dump sel R) Run > R Using HLLCOMP and HLLPIC from a GUI ----------------------------------- I made the HPICTERM program as a way to "just make it work", but there are other more graphical ways to use HLLCOMP and the HLLPIC firmware. For Windows, source files can be associated with notepad and a batch file that runs HLLCOMP, then the resulting bytes files can be copy/pasted to a terminal emulator that supports character delay - the old TeraTerm program works for this. To set up a system like that, create a directory for hllcomp.exe and the batch file somewhere on your hard drive, "C:\hllcomp" is convenient. Open a command terminal in that directory and enter: notepad "run_hllcomp.bat" Confirm the file create and enter and save the following code... @echo off C:\hllcomp\hllcomp.exe -pause %1 Add this batch to the associations for whatever extension is being used for source files (I use .hll but any extension will work), along with a default text editor like notepad so that source files can be double-clicked to edit, and right-clicked selecting run_hllcomp.bat to compile to a name.bytes file. Associate .bytes files to notepad so they can be opened to select all and copy the code to the clipboard. Set up the serial terminal emulator for 9600 baud, 8 bits, no parity and 15ms to 20ms delay between each character. Caution... under Windows 7 it is fairly easy to make associations but removing mistaken associations requires Googling for instructions and fairly intense registry editing, so make sure the correct association is made. Modern operating systems really don't want users messing with associations. For GUI operation under Linux, the hllcomp binary has to be run from a script that runs it in xterm or another terminal shell so that output can be seen. To do this, copy the hllcomp binary to /usr/local/bin or another path dir, and create the following "run_hllcomp" script somewhere convenient... #!/bin/bash # uncomment one or edit to select terminal... xterm -e hllcomp -pause $1 # konsole --workdir . hllcomp -pause $1 # gnome-terminal -x hllcomp -pause $1 Also create the following "sendslow" script somewhere convenient... #!/bin/bash # send a file to the serial port with character and line delay serial=/dev/ttyS0 # set to serial device chardelay=0.015 # set to delay between characters linedelay=0.02 # set to delay after each line # serial=/dev/ttyUSB0 # use this instead for USB serial adapters # stty -F $serial 9600 cs8 # enable to set speed (or use with open terminal) # # Your user account needs to have permissions to access the serial port, # typically done by adding yourself to the dialout and/or uucp groups. # Otherwise have to run this as root using sudo (not recommended). # If it still doesn't work, do ls /dev/tty* to check the device names. # if [ -e "$1" ];then cat "$1" | tr -d '\r' | while read line;do len=${#line} if [ ! $len = 0 ];then len=$((len-1)) for ((pos=0;pos<=$len;pos++));do char="${line:pos:1}" echo -n "$char" > $serial sleep $chardelay done echo -en "\r" > $serial sleep $linedelay fi done fi Both "run_hllcomp" and "sendslow" need to be converted to use unix line-ends if copy/pasted from here, and the properties set to allow execution. How to make the associations depends on the distribution. Under Gnome, the simplest way to use is to copy the scripts to the ~/.gnome2/nautilus-scripts directory, then run by right-clicking files then selecting Scripts then the script to run. For other GUI's use the file manager to add the scripts to the associations for plain text files, keeping a text editor as the default. Any serial terminal can be used.. I like the super-simple dterm program run from a wrapper script to run it in Konsole, but GtkTerm works. You'll probably have to add yourself to the "dialout" group to get the terminal and/or sendslow script to work, enter the command: ls -la /dev/tty* and note the group name for the ttyS0 or ttyUSB0 device. To use, I edit my source, right-click and select run_hllcomp to compile, then with a serial terminal open and at the HLLPIC serial menu, right-click the compiled hex .bytes file and select sendslow to send it to the serial port, the feedback from the HLLPIC firmware appears on the serial terminal. The HLLPIC language ------------------- The HLLPIC language is free-form, (other than ending comments) newlines do not matter and extra space is ignored. It doesn't matter if code is written with structure like... do increment a if a = 0 break loop ...or all on one line: do increment a if a = 0 break loop The language provides 14 built-in variables named a to n, if that's not enough ram from location A0h and up can be used for additional variables. Ram accesses are written as a constant surrounded by parenthesis, define can be used to give ram locations and letter variables meaningful names. Constants are restricted to byte values 0-255 and assumed to be decimal unless h is appended for hex or b is appended for binary. Several commonly used PIC registers and program variables are pre-defined... trisa trisb trisc - tristate resisters for the PIC pins (1's=ins, 0's=outs) porta portb portc - registers for reading and writing pin states rtcc - "real time clock/counter", location 1 set to continuously increment eepage eeaddress eedata - for READMEMORY and WRITEMEMORY commands milliseconds - for MSDELAY and FLASHLED commands arrayindex arraydata - for READARRAY and WRITEARRAY commands xmtreg rcvreg hexbyte - for SENDSERIAL, GETSERIAL, SENDHEX commands adchannel adresult adresult_low adresult_high - for READANALOG command These can be set like variables, for example: trisc = 11110000b 'set port c 0-3 as outputs portc = 1010b 'set port c bits 1 and 3 Lesser-used registers have to be specified, for example: (8Fh) = 01110000b 'set OSCCON for 8mhz clock rate Assignments are written as var = expression where var is a variable name, a (constant) address, or a defined variable name or address. The expression is a sequence of vars and/or constants separated by operators: + - and or xor Expressions are evaluated left to right and can include self-references. For example if a = 2 and (A0h) = 5 the command: a = a + 2 + (A0h) and 7 will set a to a value of 1. Some logic and math operations are implemented as separate commands... invert var - change all the 1's in var to 0 and vice-versa (1's compliment) swapnibbles var - exchange the lower and upper 4-bit fields in var leftshift var - shift var left, replacing bit 0 with 0 (multiply by 2) rightshift var - shift var right, replacing bit 7 with 0 (divide by 2) increment var - add 1 to var decrement var - subtract 1 from var Conditional execution is implemented using various forms of IF... IF expression2 symbol expression2 command - run command if true (symbol must be one of = < > <= >= or <> to specify operation) IF BIT n OF var command - command is run if the specified bit is 1 IF NOT BIT n OF var command - command is run if the specified bit is 0 (n must be 0-7, can be defined but BIT and OF are required) IF ZERO command - command is run if last computation was 0 IF NOT ZERO command - command is run if last computation was not 0 IF MINUS command - command is run if last computation was >= 128 IF NOT MINUS command - command is run if last computation was < 128 IF CARRY command - command is run if the carry flag is set IF NOT CARRY command - command is run if the carry flag is clear The zero, minus and carry flags are set/cleared after every math operation including assignments and expressions in IF commands. After a shift, carry is set to the shifted out bit. After a increment or decrement, carry is set if the operation rolls over (FF to 00 or 00 to FF). After an addition, carry is set if the operation overflowed. After a subtract, carry is set if the operation did not overflow. Flow control... The GOTO label command branches to label: (words ending with : are labels), The GOSUB label command branches to label: then continues execution after the command when RETURN is run. The SYSTEM command exits the interpreter, which then reruns the program after performing system-related tasks. For example: (prints 0123456789... endlessly to serial output) a = 0 myloop: gosub mysub increment a if a < 10 goto myloop system mysub: xmtreg = 48 + a sendserial return For programming with more structure... THEN compiles to: goto _then_id goto _else_id _then_id: ELSE (optional) compiles to: goto _endif_id _else_id: ENDIF (required after then) compiles to the labels: _endif_id: or _else_id: DO compiles to the label: _do_id: BREAK compiles to: goto _loopend_id LOOP (required after do) compiles to: goto _do_id _loopend_id: In the generated labels, "id" is a unique identifier based on the nesting level and the usage number. These psuedo-commands permit writing code like: do if bit 0 of portc break a = 0 do if a < 5 then [do stuff] else [do other stuff] endif increment a if a < 10 loop loop system In this example, the inner loop is made conditional by the "if a < 10" command (loop is just a goto so that works), break is used to break out of the outer loop if pin RC0 is high. Setting and clearing bits... SET BIT n OF var - set a bit to 1 CLEAR BIT n OF var - clear a bit to 0 var can be a a-n variable, (ram) location or defined var/port/etc. For example: define outbit = 0 define outport = portc define outpins = trisc clear bit outbit of outpins 'make pin an output set bit outbit of outport 'set the pin high Bits are useful for flags to direct program flow... define flags = n define direction = 0 ... if bit direction of flags then [do stuff] clear bit direction of flags else [do stuff] set bit direction of flags endif Specialized commands... NOP - do nothing but waste time on a byte fetch The following commands use data in special variables to define their behaviors rather than parameters following the command word. Serial I/O... Only very simple serial commands are provided... SENDSERIAL - write the contents of the xmtreg variable to serial output GETSERIAL - get a byte from serial input and return in the rcvreg variable SENDHEX - write the number in hexbyte to serial output as 2 hex digits These are useful for debugging or data logging, but in general HLLPIC is not designed for extensive serial I/O due to the lack of string variables other than sending strings one byte at a time, or embedding the strings in the code and carefully calculating the addresses. Note... if OSCCON is used to change the clock frequency, it also changes the baud rate. The baud rate of 9600 is for the stock frequency of 4mhz after a reset, equivalent to "(8Fh) = 01100000b". Data storage... Long-term data can be stored in eeprom above the 4KB mark using the READMEMORY and WRITEMEMORY commands with the variables eepage, eeaddress, and eedata. Set the eepage variable to the 256 byte page to access, values from 0 to 15 correspond to eeprom addresses between 4KB and 8KB. The eepage variable defaults to 0 after a processor reset, so only needs to be used to access more than 256 bytes of data. The eeaddress variable specifies the data address in the page specified by eepage. The eedata variable is the data to write to eeprom or the data read from eeprom. For example: eepage = 0Fh eeaddress = FFh readmemory if eedata <> EEh then eedata = EEh writememory 'write EE to eeprom location 1FFF endif The actual eeprom address read or written = (eepage+16)*256+eeaddress. Set eepage to F0h to FFh access program memory. Warning! avoid frequent writes to eeprom or damage to the chip can result! each location is rated for "only" a million writes, a program stuck in a loop can exceed the rating in a short period of time. For rapidly-changing temporary indexed data, use the array commands instead. Another precaution is to use code similar to the above example to write the data only if it has actually changed. Small data arrays... The WRITEARRAY and READARRAY commands and the corresponding arrayindex and arraydata variables provide a way to temporarily store indexed data. The amount of data that can be stored and where in ram it is stored depends on the implementation. The '684 version (1.14) simply adds B0h to the arrayindex value to derive the ram location, care must be taken if indexing more than 16 bytes as it provides access to all of lower ram, improper indexes will crash the program. The '690 version (1.23) provides a (mostly) dedicated 128 bytes of storage by mapping indexes 0-79 to bank 2 locations 120h to 16Fh and indexes 80-127 to locations C0h to EFh. Bit 7 of arrayindex is ignored so indexes 128-255 wrap to 0-127 ("negative" indexes wrap to the top of the array). Ram locations A0h to BFh remain free for user variables. Example: arrayindex = 0 arraydata = 100 writearray 'store 100 in element 0 arrayindex = 5 arraydata = 50 writearray 'store 50 in element 5 arrayindex = 0 readarray 'arraydata now contains 100 Delays and flashing the status LED... MSDELAY - delay by an amount determined by the milliseconds variable FLASHLED - flash the status LED, milliseconds determines on and off period If milliseconds = 0 then the actual delay is 256 milliseconds. The stock HLLPIC firmware defines the LED as the same pin as serial output, generally the serial terminal will ignore the flashing as it's way outside of the normal terminal baud rate. The delay routine is programmed to take into account the OSCCON value and adjust itself for all but the slowest clock rate, however the timing is approximate, don't count on more than 5% or so accuracy. Also keep in mind that each program command requires about a millisecond or so to execute, give or take. Examples: milliseconds = 50 do blinkled loop milliseconds = 50 do set bit 1 of porta msdelay clear bit 1 of porta msdelay loop Analog input... The READANALOG command can be used to read voltages on the first 8 analog channels (AN0-AN7) relative to the PIC's supply voltage. The corresponding input pin must be set to an input. The adchannel variable controls which channel is read, the data is returned in the adresult variable, where 0-255 represents a voltage from 0 to Vcc. For more precision, the full 10 bit result is held in adresult_low (the lower 8 bits) and adresult_high (the top 2 bits). Example: 'flash led's connected to rc0-rc3 at a rate 'determined by the voltage on ra2 (an2) trisc = 11110000b 'set port c 0-3 to outputs a = 00000001b 'shifting pattern to send to the LED's clear bit 0 of c 'bit 0 of c used as a left/right flag do b = portc and 11110000b 'not needed but good practice portc = b or a 'set port c, preserve other bits if not bit 0 of c leftshift a if bit 0 of c rightshift a if bit 3 of a set bit 0 of c 'switch to rightshift if bit 0 of a clear bit 0 of c 'switch to leftshift adchannel = 2 readanalog milliseconds = 255 - adresult + 1 msdelay loop Sleeping and the watchdog... These commands are not appropriate unless the WDTCON register is set to enable the watchdog timer and set a period. The location of WDTCON varies depending on the PIC, on the PIC16F684 and PIC16F688 it is located at 18h, on the PIC16F690 it is located at 97h. Bit 0 is set to enable the watchdog, bits 1-4 determine the timeout period. A value of 1111b or 15 corresponds to a timeout of about 5 seconds. For example for the '690: (97h) = 1111b After enabling the watchdog, a CLRWDT or SYSTEM command must be given before the timeout expires or the program will reset. The SLEEP command will cause the processor to sleep until the remaining timeout expires. Note that if SLEEP is used without enabling the watchdog then the processor will sleep until it is reset. To sleep without having to worry about CLRWDT during program operation, do something like this: do milliseconds = 100 flashled 'or anything else regardless of how long it takes (97h) = 10001b 'set timeout to about 10 seconds sleep (97h) = 0 'disable wdt loop Other psuedo-commands... 'comments - anything after a ' to the end of the line is ignored. END_PROGRAM_CODE - compiles to 16 FFh bytes to mark the end of a program. When loading programs the code is overlaid over existing code, making it impossible to tell where the current program ends when examining eeprom data. Use END_PROGRAM_CODE after the end of normal program code (before any extra data >= 1000h) to make it obvious where the program code ends. This is required to use HLLCOMP's decompile feature on full eeprom dumps so it can know when to stop decompiling the code section. The CODE psuedo-command outputs raw bytes, typically to express data or strings but can also be used to implement new firmware instructions without having to modify the compiler. DEFINE can be used to label new instructions. The ADDRESS command sets the current compile address, typically to switch to the data section starting at 1000h for predefining data using CODE. Data can be labeled, DPAGE(label) is replaced by the equivalent eepage, DADDR(label) is replaced by the byte address. Example: eepage = dpage(text) eeaddress = daddr(text) do readmemory if eedata = 0 break xmtreg = eedata sendserial increment eeaddress loop hang: goto hang end_program_code address 1000h text: code "Hello World",13,10,0 The HLLPIC firmware requires that hex bytes files begin with "0000:" and address marks must be followed by bytes, so avoid starting a program with data or using ADDRESS without actual data. Doing so will encode a FFh byte to satisfy the firmware and issue a warning. The intended source code structure is to put program code first, followed by end_program_code, and if the program uses pre-initialized data or strings, an address marker pointing to >= 1000h, followed by labels and CODE constructions. Decompiler notes... HLLCOMP's decompile function turns a hex bytes file back into source code. Sort of, all labels, comments and custom defines in the original source are lost. The original purpose of the decompile function was so that I could tell which program was loaded into my hobby robot after being away from it for awhile. It is also useful for revealing the structure compiled by psuedo-commands like THEN and LOOP. The decompiler will convert bytes < 4KB to instructions, using CODE only if it does not recognize the byte. Bytes at addresses >= 4KB are output using the CODE word as a mix of hex bytes and quoted ascii. Usually the decompiled output can be recompiled to produce the same exact program, but this is not guaranteed, especially if the hex bytes file includes data in the code area, END_PROGRAM_CODE wasn't used after the program code, or uses any non-standard tricks. It is possible to encode text/data within the normal code area (< 4KB) either after normal code or by jumping around the data, but doing that will cause the decompiler to output garbage.. for example.. goto program_start text: code "Hello World",13,10,0,0,0 program_start: ...decompiles to... goto LOC18 + i and f and m and m and 32 - h and 114 and m and e return >= nop nop nop LOC18: When the data is above the 4KB mark the hello program decompiles to... eepage = 0 eeaddress = 0 LOC8: readmemory if eedata = 0 goto LOC26 xmtreg = eedata sendserial increment eeaddress goto LOC8 LOC26: goto LOC26 system [..15 more systems deleted..] address 1000h code "Hello World",0Dh,0Ah,00h ...much better. Sample program -------------- This is not exactly a typical embedded app, but without a specific app there's not much that can be demoed other than blinking LED's. The following program uses the serial and array features to implement a "line automata" generator - after prompting for a rule string which controls how the states change, it generates a random line then uses the rules to print new evolved lines. In other words it prints interesting patterns to the terminal but is otherwise useless other than showing how to use the language implemented by HLLCOMP and HLLPIC. If run on a "PicKit 2 Low Pin Count Demo" board modified to include the eeprom and serial port, it also blinks the 4 LED's. ---------- cut --------------------------------------- '4-state 1D cellular automata for HLLPIC '690 (1.23) 12/29/12 WTN 'at the rule prompt type 10 digits each 0-3 'or press enter to reuse the previous or default rule 'a few interesting rules include... [edited 12/22/12] ' 0020123010 0012301200 0012301100 0012130100 ' 0302120100 0003201300 0113123000 0003132000 ' 1002132000 0020331100 0003212000 0011232000 ' 0023310120 0103321100 0023321100 0023310200 ' 0023310021 0023230110 0023120120 0023012100 trisc = 11110000b 'blinkenlights on port c bits 0-3 eepage = dpage(prompt) 'point to sign-on page eeaddress = daddr(prompt) 'point to sign-on address gosub printcode eepage = dpage(rule) 'to simplify code rule must start at offset 0 a = 9 'start with most significant position do do getserial if rcvreg = 13 goto skiprule b = rcvreg - 48 if b >= 0 then if b <= 3 break endif loop xmtreg = rcvreg sendserial eeaddress = a eedata = b writememory decrement a if not carry 'keep looping until wraps to 255 loop skiprule: 'rule stored in eeprom 'pack 2 cells to a byte to store 80x2 cells in 80 bytes gosub crlf (8Fh) = 01110000b '8mhz 'set guard cells to 0 a = 0 b = 0 gosub setstate a = 79 b = 0 gosub setstate 'randomize line... a = 1 do b = rtcc and 7 gosub scramble rightshift b 'now 0-3 gosub setstate increment a if a < 79 loop mainloop: gosub copyline 'copy last new states to current states gosub printline 'output current states to serial i = 1 'start at cell 1 (cells 0 and 79 always 0) do e = 0 'sum of states a = i - 1 gosub getstate e = e + b 'add state of cell to left increment a gosub getstate e = e + b 'add state of current cell increment a gosub getstate eeaddress = e + b 'add state of cell to right to get offset readmemory 'read new state from the rule string b = eedata a = i gosub setstate increment i if i < 79 'keep looping until cell 79 reached loop goto mainloop scramble: 'uses incrementing variable N to scramble shifted RTCC 'not a great RNG but simple and random enough for this rightshift rtcc rtcc = rtcc xor n increment n return printcode: 'print string sub, eepage/eeaddress set to string code 'string must end with 0 to terminate do readmemory if eedata = 0 break xmtreg = eedata sendserial increment eeaddress loop return getstate: 'a specifies cell 0-79 'returns current state in b c = a rightshift c '/2 to get byte address arrayindex = c readarray b = arraydata if bit 0 of a swapnibbles b 'swap if odd address b = b and 3 'mask off unused data return setstate: 'a specifies cell 0-79 'b specifies state 0-3 (not preserved) c = a rightshift c arrayindex = c + 40 readarray 'get existing to merge d = arraydata if not bit 0 of a then 'merge even... d = d and 11110000b d = d or b else swapnibbles b d = d and 00001111b d = d or b endif arraydata = d writearray rightshift d 'output states to led's rightshift d portc = arraydata and 3 or d return copyline: a = 0 do arrayindex = a + 40 readarray arrayindex = a writearray increment a if a < 40 loop return crlf: xmtreg = 13 sendserial xmtreg = 10 sendserial return printline: a = 38 'display middle 4 cells on led's do leftshift g gosub getstate if b > 0 set bit 0 of g increment a if a < 42 loop portc = g (8Fh) = 01100000b '4mhz for 9600 baud xmtreg = 32 sendserial a = 1 'print middle 78 cells to serial out do gosub getstate xmtreg = 32 ' space if b = 1 xmtreg = 46 ' . if b = 2 xmtreg = 43 ' + if b = 3 xmtreg = 35 ' # sendserial increment a if a < 79 loop gosub crlf (8Fh) = 01110000b '8mhz return END_PROGRAM_CODE address 1000h 'switch output to data area rule: 'default rule (backwards), must be at offset 0 code 0,0,2,1,0,3,2,1,0,0 'rule 0012301200, usually doesn't stop prompt: 'displayed on startup to prompt for rule string code "4-state Line Automata Generator",13,10 code "Enter 10 digit rule (0-3, enter for last):",0 ---------- cut --------------------------------------- To make up for the lack of ram it packs 2 cells per array byte, providing 80 cells for the current states and 80 cells for the new states in a single 80 byte array. Cells 0 and 79 are kept zeroed for "guard" cells, only the middle 78 cells are printed for each line. Sample output... 4-state Line Automata Generator Enter 10 digit rule (0-3, enter for last):0023321100 .+.#++.#++.# ######.++##. +++..#+. .# +++....# .+.....#+ ++ ++ ..# . +#. .+##+###+##+## +##+ ++ .+##+###..++#+##+..#++.++...###..++..++. .#++ .##+ .# #. .# #+++..# +#+#++ #+##+####+.#+ +#+##++##. +#++.++ #. ++. .++ ++..#+##+#+. .#+ +++. .# ####+#+ ++ ++ .#++###+#++ +#. .#+..+#+## +#. +##++##..++. .# +#. .+++++.+#++ +++ ##+ +##++#+ #. .##+ .# ++ +#+##. ++. .##+..#######+++. .+#+ # #. .# +++#..++ ++ #. ++..+++#+ ++ .+#. ++ #+#+ +##. .#+# .#++ ++..+#+##+#+..++#++.+#++##++#..++. .##+ .++## +#. .# ++ +# # +#++. .+#++#+ +#++#+++###+++ ++##+##. ++ #..#+ ####+ ++#++..###. #++#. .#++++#. .#+++++#+ +#+..++ ++ .++#+###### #. .++++#+#+ ++ #++#+ +#+##+#+ +#+###++#. .#+#++#+. .++. .#++ #. .++ .###++ +#+++ #+++#. .#+ +#..#+ ++#+.+# ++++#. .##. +#++. .++..#+. ++ +++#++#+ #+#+#+ +##. .######. .+++#####+##+#+ ++++ .#++#. .##+###..++..+#+++++# # + +#. .# ++ ++ ++ .##+ +#..+##+.+#++#+ ++ +#+##++#++###+# .#.+##+.++#++..++. .++. ++ #. .###+# ###++++#. .++. .#+ +++++ # +### ###+++#++##. .##..++#++ ++ #..# +##+#+ .##. +##. .+###+. .. # #..# +#++++ ++ +++#+#++++. .++. .+##+..# +#. ++++ .# ++..# #. .. ...+##+..#++##++++..+#++ ++###. .##...# #+#+. .##+.+##+.++#+#+#+. .++ .. ..+# #+##++ +###++#++++++ ++ +++#.#+..# +#..++ ### ###++ + +#...#+. .. .+##..# ++..# ++++####+..++. .+#+#+##+#+####+#+## #..# +++.+###.###.... .# +##+. .+#+#+. .+##+ #++##. .#+ + + + ##+##+..+#### +++ +#... ++## #...#+ +#...# #. .#++ ++ +##+.. ... ....# #++# #..+#++###.. ++ #..+#.###+###.#+..++ +#+++++. .# #+. ... ...#+. .#++#. .+#+#+++ +#. ...etc. HLLPIC firmware features ------------------------ The present HLLPIC firmware is fairly generic, basically just initialization, the interpreter and the serial menu for loading and extracting eeprom data. The initialization code sets all pins to inputs except for the serial out pin, and for the '684 version, also RA2 for a demo LED or other output. Analog inputs and other PIC features are turned off (the READANALOG command enables AD inputs as needed). All PIC pins except for MCLR, Vcc and Vss are available for digital I/O. The PIC is configured to run at 4mhz from the internal oscillator, the watchdog timer is disabled by default. User software can enable the watchdog timer for timeout or sleeping, or change the effective clock rate by multiples of 2 (up to 8mhz), however using a crystal oscillator requires editing the code config and adapting the serial and millisecond timing code. The internal oscillator is generally accurate to within 1% which is accurate enough for serial I/O, robotics and other kinds of apps HLLPIC is designed to support (HLLPIC is not designed for timekeeping). The firmware checks internal eeprom locations FE/FF to determine whether to load and run code to/from the internal eeprom or an external eeprom. If FE/FF contains E4/EE then a flag is set to default to external eeprom. Otherwise code and data accesses use the internal eeprom. After initializing, the firmware checks to see if a serial terminal is attached, if not the interpreter runs the user program. If a program error occurs, if no serial terminal is attached the firmware blinks the "error" LED (assigned to the serial output pin), if a serial terminal is attached then it sets the processor speed to 4mhz (in case it was changed), dumps ram contents to serial output and displays the serial menu. In the present firmware, after running the program via the serial menu's R command, the only way to get back to the menu is by a processor reset, or if an error occurs. For debugging purposes, an error can be deliberately induced by using RETURN on the top level or running an invalid opcode (i.e. CODE D7h). For normal exit via the SYSTEM command, if eepage is set to E0h then eepage is set to EEh, the flag is set to run code from internal eeprom and the interpreter is restarted. Note that data access via READMEMORY and WRITEMEMORY remains set to external eeprom. On exit via SYSTEM, if eepage is set to EEh, the flag is set to run code from external eeprom and the interpreter is restarted. For any other value of eepage the interpreter simply restarts without changing the internal/external flag. This mechanism can be used to run small routines from internal eeprom for faster processing, the main app in external eeprom can check eepage for EEh on startup to simulate a return from subroutine. The compiler does not support this feature, the hex bytes file for the internal eeprom program has to be manually loaded or edited to replace the initial "0000:" with "P000:". Each pass through the interpreter executes a CLRWDT instruction before running the user code, if the watchdog is enabled then the CLRWDT command is only needed if the code doesn't do a SYSTEM or SLEEP before the timeout. The firmware features are dependent on specific adaptations, the descriptions only apply to the present generic versions of HLLPIC. For example, the firmware for a PIC with a MSSP module would likely operate the external eeprom fast enough to not bother with the eepage hack for jumping to internal eeprom. Applications like controlling a small robot can do other things with the SYSTEM exit, such as reading sensors, processing default actions, and sleeping if the power is low. HLLPIC interpreter internals ---------------------------- The following was taken from the HLLPIC interpreter comments and documents the byte-code language... (slightly edited) -------------------------------------------------------------------------- 00 NOP 01 IF (***) 02 YY INCREMENT adds 1 to ram location YY 03 YY DECREMENT subtracts 1 from ram location YY 04 reserved 05 CLRWDT have to run occasionally if watchdog enabled 06 SLEEP sleeps for a time configured by the WDTCON reg 07 > (**) 08 < (**) 09 = (**) 0A >= (**) 0B <= (**) 0C <> (**) 0D RETURN 0E XX [YY] SET bit and CLEAR bit XX=sbbbvvvv s=state b=bit 0F XX [YY] IF bit and IF NOT bit v=var A-N 14=ram YY 1v [YY] LET (implied by "v =") v=var A-N 14=ram YY 2h LL GOTO h=high addr nibble LL=low addr 3h LL GOSUB h=high addr nibble LL=low addr 4v [YY] + (*) v=var A-N 14=ram YY 15=const YY 5v [YY] - (*) v=var A-N 14=ram YY 15=const YY 6v [YY] AND (*) v=var A-N 14=ram YY 15=const YY 7v [YY] OR (*) v=var A-N 14=ram YY 15=const YY 8v [YY] XOR (*) v=var A-N 14=ram YY 15=const YY 9v [YY] NOT (aka INVERT) v=var A-N 14=ram YY (flips all bits) Av [YY] LEFTSHIFT v=var A-N 14=ram YY (bit 0 set to 0) Bv [YY] RIGHTSHIFT v=var A-N 14=ram YY (bit 7 set to 0) Cv [YY] SWAPNIBBLES v=var A-N 14=ram YY D0 IF CARRY as in: a = a + 1 if carry b = b + 1 D1 IF NOT CARRY D2 IF MINUS as in: a = a - 1 if minus a = 0 D3 IF NOT MINUS D4 IF ZERO D5 IF NOT ZERO ... (*) accumulates result into the last LET variable or condition temp (**) valid only after IF [expr] (***) follow by [expr1] [symbol] [expr2] - if the condition is not true, the following opcodes are turned into nops until another command opcode is encountered - anything besides (*) opcodes but IF should not be followed by another IF. abs means absolute ram address 0-255, constant is number 0-255 if v is 0-13 then user ram is addressed, if v = 14 (E) then the next byte is the memory address to access/modify, if v = 15 (F) the next byte is a constant. Weird but compact, variable ops in one byte, absolute address or constant ops in 2 bytes. LET and IF expressions can be any length but avoid long LET commands for IF targets as the interpreter still has to interpret them to get to the next command. Instructions with code outside of this block... F5 SENDHEX send hexbyte to serial out as hex digits F6 FLASHLED flash LED on/off each defined by milliseconds F7 GETSERIAL wait for and read serial in into rcvreg F8 SENDSERIAL send xmtreg to serial out F9 MSDELAY delay for period defined by milliseconds FA READARRAY read element arrayindex into arraydata FB WRITEARRAY write arraydata to element arrayindex FC READANALOG read analog(adchannel) into adresult FD READMEMORY read eeprom(eeadress) into eedata FE WRITEMEMORY write eedata to eeprom(eeaddress) FF SYSTEM exit program Fixed locations... 20h - eedata from READMEMORY or for WRITEMEMORY 21h - eeaddress for READMEMORY and WRITEMEMORY 22h - eepage for WRITEMEMORY - offset by 4Kbytes for 8Kbyte eeprom (if you really want to read/write ext ee program set to F0-FF) (when running from 256 byte internal ee then eepage is not implemented) 23h - adchannel for READANALOG 24h - adresult from READANALOG (10 bit result / 4) 25h - adresult_low from READANALOG (low 8 bits of result) 26h - adresult_high from READANALOG (top 2 bits of result) 27h - index for READARRAY and WRITEARRAY 28h - data for READARRAY and WRITEARRAY 29h - xmtreg for SENDSERIAL 2Ah - rcvreg for GETSERIAL 2Bh - milliseconds for MSDELAY and FLASHLED 2Ch - hexbyte for SENDHEX 30h to 3Dh - the 14 predefined variables A-N -------------------------------------------------------------------------- The byte-code language was designed to be fairly dense, branches are limited to 4KB to express GOTO and GOSUB in 2 bytes, ram access is limited to the bottom 256 bytes of ram, and 14 built-in variables can be used to encode LET and math/logic instructions as single bytes. A few encodings... A = 5 '10 4F 05 A = B - C '10 41 52 IF BIT 5 OF (A0h) GOTO label 'OF DE A0 2x xx (xxx = address of label) IF A < B INCREMENT A '01 40 08 41 02 30 IF A AND 7 <> B - C B = 0 '01 40 6F 07 0C 41 52 11 4F 00 CLEAR BIT 2 OF C '0E 22 The first operation in an expression for assignment (LET) or on either side of an IF comparison is always a + instruction, expressions are evaluated from left to right and can include built-in variables, absolute ram locations or constants separated by the instructions: + - and or xor INC/DEC only supports absolute ram so compiler needs to know A=(30h), locations for built-in variables and parameters are hard-coded into the compiler. Additional variables for specific apps need to be added after the interpreter byte assignments so the locations will remain fixed. Details of the syntax like "bit x of y" to specify bits and "(xxh)" to specify an absolute ram are determined by the compiler. In retrospect it probably have been better to write the compiler to encode bits like "var.bit" where var can be a built-in variable, symbol or (location), bit could be a constant or symbol, or the whole thing redefined as in "define outbit = porta.2" then just say "clear outbit", but that can be done later and doesn't affect the interpreter firmware. The interpreter code is written in a block to make it easier to adapt to more specific applications. Entry to the interpreter is by jumping to the inithll label, exit is by dropping out the end of the block. An error variable can be checked to determine if the exit is normal or because of an error. Code outside the block is jumped to or called to perform system-specific tasks... fetch subroutine - returns bytes to be interpreted s_readmemory jump - gets a byte from storage s_writememory jump - writes a byte to storage s_readanalog jump - gets byte and 10-bit values from an analog input s_readarray jump - gets a byte from a ram array s_writearray jump - writes a byte to a ram array s_sendserial jump - sends a byte to serial output s_sendhex jump - sends a byte to serial output as 2 hex digits s_getserial jump - waits for and gets a byte from serial input s_msdelay jump - delays a specified number of milliseconds s_flashled jump - flashes an LED Parameters and return values are transferred using byte variables. Only fetch is an actual subroutine, the other code is jumped to directly from the instruction decode table (on the base level so all subroutine call and return levels can be used). Each instruction implements its own conditional processing then jumps back to the interpreter. The general form for single-byte external instructions is... label: if execute then [do whatever the instruction does] endif goto endinstruction If an instruction is coded to require multiple bytes then it must call fetch however many times is needed before doing conditional execution, so if the condition is not true the entire instruction/command is skipped. Additional instructions are easily added to the F* opcode block by editing the source to replace "goto i_notimpl" with "goto instructionlabel". The E* block isn't decoded but can be added by moving the i_ecodes: label at the end to a jump table structured like the i_super: jump table. On startup at the inithll: label, the code issues CLRWDT, and zeros PClow, PChigh, sublevel, interror, hllflags and moreflags which contain state bits, sets the execute bit and drops through to the main loop at the hllrun: label. The main loop calls the fetch sub to get the next program byte in work, copies it to instruction for later reference, instlow is set to the lower nibble, sets/clears the constant and absolute bits based on instlow (Eh = abs ram, Fh = constant), sets the W register to the high nibble to select the instruction type, and jumps to maindispatch which is a jmp pc+w" jump table to each type of instruction. Similar jump tables do further decoding, for example F* instructions jump to i_super: which loads instlow into W then does jmp pc+w jumps to jump to each individual F* instruction. There are different classes of instructions... Normal instructions fetch any needed bytes, conditionally execute based on the execute bit, then jump to endinstruction which sets the execute bit, clears the condflag bit, and jumps to hllrun to fetch the next instruction. The LET instruction which sets letregister to the assign destination, copies the current destination content to selfref and clears the contents, and jumps to endinstruction. Math/logic instructions accumulate to wherever legregister points to. Note that simple assignments are encoded as a + instruction to just add the parameter to the destination. Conditional execution of LET is done by setting letregister to a dummy location if the condition is false. Math/logic instructions (+ - and or xor) are unconditional, they accumulate the result to the location pointed to by letregister, and set the internal zero, minus and carry bits to the current results then jump to hllrun. If the condflag bit is set, further operations are performed to set/clear the execute bit based on the comparison results. IF bit and IF NOT bit instructions set or clear the execute bit depending on if the specified bit is set or clear, then jump to hllrun to continue. IF comparisons work by clearing the condflag bit, setting letregister to the cond1 variable, clearing cond1 and cond2 and jumping to hllrun. The left side expression accumulates to cond1. When the comparison symbol is encountered (= <> < > <= or >=) it records the comparison type in condition to use with a jump table, sets the condflag bit, sets letregister to point to cond2 then jumps to hllrun to accumulate the right side expression. The condflag bit changes the behavior of the math/logic instructions so that after each operation, cond1 is compared to cond2 according to condition then the execute bit is set or cleared depending on if true or not. This scheme permits any number of operations on either side of the comparison symbol without having to specify when the condition ends, when a normal instruction is encountered it either runs or not then resets the flags to continue running the program. A disadvantage of this scheme is the IF command itself cannot be made conditional, so the target for an IF cannot be another IF. Compiler features compensate for this by compiling branches to support block IF/THEN/ELSE/ENDIF structures. Locations 4 (FSR) and 0 (indirect) are used throughout for pointing to destinations, so these registers cannot be used by the user app for indexing. ---------------------------------------------------------------------------- Last modified Dec 28, 2012 - Terry Newton (wtn90125@yahoo.com)