PicoWeb Pcode Interpreter

A pcode instruction interpreter was developed for the PicoWeb Server. The use of a custom pcode provides:

Code simplification is achieved because the p-code makes no reference to native registers, and because the pcode "virtual machine" uses 16-bit wide data types for most operands. Pcode execution from EEPROM is possible because the pcode "instruction pointer" is 16 bits wide, more than is needed to address the program memory in the Atmel microcontroller. Therefore, the "extra" address bits can be used to indicate that the next pcode instruction should be fetched from EEPROM, and not from the microcontroller’s internal program memory.

Pcode Virtual Machine - The pcode virtual machine has no explicit registers, with the exception of a pcode flags register and a pcode program counter. Instead, most pcode instructions reference SRAM locations in the microcontroller. The same memory location labels used as part of normal AVR assembly language programming can be used as part of a pcode instruction instance. For example, the pcode instruction:

    pincw buf

increments the two-byte value stored at SRAM label buf. By convention, the PicoWeb server’s standard pcode uses an n-byte scratch buffer located starting at SRAM location buf for general purpose working storage, a kind of pcode "register set".

The pcode interpreter maintains a separate 5-deep pcode return address stack which is used store return address needed to implement the pcall and pret pcode instructions.

Refer to the section PicoWeb Pcode Instruction Definitions for a list of legal pcode instructions along with a description of their required operands. Pcode instructions can have a maximum of three operands. Most pcode built-in instructions work with 16-bit words. Like the AVR microcontroller, these 16-bit words are stored in memory in "little-endian" format (i.e., low byte is stored first in memory).

PicoWeb pcode can be freely intermixed with AVR assembly language providing that a pbegin pcode instruction us used to enter a pcode section and a pend pcode instruction is used to exit a pcode section. Failure to observe this rule will produce unpredictable results.

User-Supplied Pcode Opcodes - Users can add their own pcode opcodes, providing certain conventions are followed. The following example shows code for a user-supplied pcode instruction called pmycode that takes three operands:

  pmycode: pcode_routine 3
           .
           .  (user-supplied code goes here)
           .
           ret   ; return to pcode interpreter

The first line of this sample routine tells the PicoWeb development system that pmycode is a new, legal pcode instruction which requires exactly three operands.

Note that when invoked by the pcode interpreter, the pcode instruction’s three 16-bit operands are supplied to the user-supplied assembly language routine in registers r10-r11, r12-r13 and r14-r15 respectively. These operands will have been previously "de-referenced" by the pcode interpreter as indicated by the various operand word control bits (i.e., optional indirect addressing of words/bytes, byte swap, etc.). A user-supplied pcode opcode assembly routine is free to use the processor registers r0-r1, r10-r17, x, y, and z. Any other processor registers need to be saved and restored before exiting the user’s routine. A user-supplied pcode opcode routine cannot depend upon any of the processor registers being preserved from one pcode opcode routine execution to the next. If a user-supplied pcode opcode routine needs to preserve state between opcode invocations, then the microcontroller SRAM must be used to save this state.

Pcode Instruction Memory Format – Pcode source instructions are pre-processed by the PicoWeb development environment and converted into a series of AVR assembler .dw pseudo-ops. Each pcode instruction generates a single 16-bit pcode opcode word followed by zero or more 16-bit pcode operand words. The bottom 12 bits of the opcode word are the address of the native execution routine. The uppermost bit of the opcode word is an "extended addressing" flag. If this bit is set, extended addressing is enabled, otherwise it is disabled. Extended addressing controls how the operand words are interpreted. When extended addressing is disabled, all operand words are considered "immediate" operands. When extended addressing is enabled, the uppermost three bits in each operand word specify which extended addressing mode(s) are to be applied, with the lower 13 bits of the operand are treated as an address.

Pcode Opcode Word Format

Opcode Word Format

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

Extended addressing enabled

1

0

0

0

A

A

A

A

A

A

A

A

A

A

A

A

Extended addressing disabled

0

0

0

0

A

A

A

A

A

A

A

A

A

A

A

A

AAAAAAAAAAAA = address of pcode routine in on-chip or external EEPROM memory

Pcode Operand Word Format

Operand Word Format

15

14

13

12

11

10

9

8

7

6

5

4

3

2

1

0

Extended addressing enabled

0

I

I

I

I

I

I

I

I

I

I

I

I

I

I

I

X

W

S

A

A

A

A

A

A

A

A

A

A

A

A

A

Extended addressing disabled

I

I

I

I

I

I

I

I

I

I

I

I

I

I

I

I

IIIIIIIIIIIIIIII = immediate data value

AAAAAAAAAAAAA = address of pcode routine in on-chip or external EEPROM memory

W = 0: fetch 16-bit data word at address AAAAAAAAAAAAA (indirect word addressing)

W = 1: fetch 8-bit data byte at address AAAAAAAAAAAAA (indirect byte addressing)

S = 0: do nothing after fetching data

S = 1: swap high/low bytes after fetching data

A pcode opcode word of all zeros (.dw 0) will cause the pcode interpreter to exit and jump to the first AVR assembly language instruction after the zero word. (The pcode opcode pend is defined to have all 16 opcode word bits set to zero.)

 

PicoWeb Pcode Example

The following example is a pcode routine called decconv which converts a passed 16-bit signed integer value into ASCII digits, then outputs that text (to the serial port or to the Ethernet) using the pcode I/O opcode pputch. This "pure pcode" routine uses 11 bytes of SRAM starting at the label buf to perform its work. It can be called from pcode as in the following pcode example, which will output the string "-12345":

   pmovwi  buf,-12345
   pcall   decconv

The routine decconv can be placed in the AVR microprocessor’s on-chip program memory, or it can be placed in the PicoWeb server’s external serial EEPROM memory. Execution speed is much slower from the external SEEPROM memory because in that case the opcode and operand words must be fetched serially as they are executed (i.e., one bit at a time at the rate of ~400 Kbits/sec over the I2C bus).

   ;
   ; decconv – integer to decimal converter
   ;
   ; inputs:
   ;   buf = 16-bit signed integer to convert
   ;
   ; outputs:
   ;   ASCII value, printed with pputc
   ;
   ; method:
   ;   repeatedly divide input by 10 until quotient is 0, using the remainder 
   ;   to make ASCII digits.  The ASCII digits are buffered, then output at 
   ;   end in reverse order.  Negative inputs get a leading '-' sign.
   ;   
   ; caveats:
   ;   MUST be called with a pcall!!
   ;   Destroys SRAM locations buf through buf+10
   ;
   #define DEC_VAL buf                   ; word to convert (destroyed!)
   #define DEC_REM buf+2                 ; remainder from divide op
   #define DIG_PTR buf+4                 ; text buffer pointer
   #define DIG_BUF buf+6                 ; text buffer (5 bytes max.)
   
   decconv:
       pmovwi DIG_PTR,DIG_BUF            ; initialize pointer to DIG_BUF
       pbitwi DEC_VAL,0x8000             ; what's the sign of input?
       pjumpeq decconv1                  ; positive...start conversion
       pnegw DEC_VAL                     ; negative...negate input word
       pputc '-'                         ; output leading minus sign
   decconv1:
       pdiv DEC_VAL,[DEC_VAL],10         ; do next digit (divide by 10)
       paddwi DEC_REM,'0'                ; convert digit for display
       pmovbi [DIG_PTR],[byte DEC_REM]   ; save ASCII digit
       pincw DIG_PTR                     ; bump past digit just stored
       psubwi DEC_VAL,0                  ; test remaining value
       pjumpne decconv1                  ; more to convert if non-zero
   decconv2:
       pdecw DIG_PTR                     ; point to next digit to output
       pputcb [byte DIG_PTR]             ; output ASCII digit
       pcmpwi DIG_PTR,DIG_BUF            ; are we back to the beginning?
       pjumpne decconv2                  ; nope...do another digit
       pret                              ; yes...return to caller

Preprocessor #define statements are used in the example to make the source code more readable. These #define statements call out SRAM locations in the PicoWeb server’s general purpose "scratch" buffer buf.

Note the use of square brackets ([ ]) on three of the sample pcode instruction lines indicating "indirect addressing". For example, the pmovbi pcode opcode (move byte) expects a destination memory address as its first operand and an immediate value as its second operand. In the example, the destination address is specified as [DIG_PTR], an indirect addressing form, which tells the pcode interpreter to instead use DIG_PTR as a pointer to the "real" target byte address. The routine increments DIG_PTR as it converts digits, allowing the ASCII digits to be stored sequentially starting at memory location DIG_BUF.

On the same sample pcode pmovbi instruction line, the second operand normally supplies an immediate source byte. However, in the example, this operand is specified as [byte DEC_REM], a more complex indirect addressing mode, which tells the pcode interpreter to fetch the byte stored at memory location DEC_REM, instead of simply taking the immediate value supplied as the second pcode operand. If the second operand was specified as simply "DEC_REM", then the pcode instruction would (incorrectly) repeatedly store the low byte of the literal address DEC_REM!