Jump to content
Sign in to follow this  

SIMPL - An update on the Tiny Forth-like language

Recommended Posts

SIMPL  = Serial Interpreted Minimal Programming Language


It's been about a year since I talked about SIMPL - a tiny language that allows you basic control of a microcontroller using serial commands.  

In the 6 months I have coded it up in MSP430 assembly language to make it super compact - and fast - with high level commands taking about 1uS to execute on the virtual machine interpreter.

SIMPL is based on a jump table - so for any single, printable ascii character, the jump table will act on it and execute whatever function you choose to write. This technique gives amazing flexibility, and for under 1K of code it can offer a very powerful user interface to your latest MSP430 project.   One example is using the jump table to interpret commands from a text file to control a CNC mill or drill - or even a 3D printer.

The core routines can be applied to any MSP430 - you just have to change the initialisation routines to suit the DCO, GPIO and UART of the specific microcontroller

Just this week, I have got the looping to work, so you can now do things like send square waves to port pins for flashing LEDs and creating musical tones.

With the standard Launchpad (MSP430GR2553) clocked at 16MHz you can send a 1uS pulse to a port pin  - just by typing hl  (shorthand for high, low) at the serial terminal.

SIMPL is coded up in just 872 bytes of program memory - and can handle up to 96 separate commands.

Commands can be sent to the device from a text file - using teraterm or similar  - or just typed manually at the keyboard.

I have put the latest code (some recent changes) on this github gist https://gist.github.com/monsonite/6483a32404c1c53cd8027dd6f9dcea6e

This is a work in progress - and there are still a few bugs - but the basics seem to work OK.







Here's some more info about the various routines that make up the kernel

textRead       33 Instructions 90 bytes
Receive characters from the serial uart and place them into a buffer in RAM.  Two modes of operation are possible, immediate mode, where the characters are executed as instructions directly after a carriage return line feed is received, and compile mode, where the character sequences are preceded by a colon and stored in pre-calculated command buffers in RAM.
number      16 instructions 42 bytes
Number interprets sequences of consecutive digits as a 16-bit integer number and places the value in the register that is the top entry of the stack.
next           5 instructions 12 bytes
Next is the routine that all commands return the program flow back to once they have executed. It fetches the next character instruction from the RAM buffer and through a jump table technique passes program control to the code body that performs the task associated with that instruction.  The  jump table  is used to direct the character to the areas of code that will treat them correctly - for example on encountering a numerical digit, program control is passed to the number routine, whilst for upper case alphabetical characters, program control is passed to a routine that handles these separately.
jump_table       96 instructions   192 bytes
Primitives    72 instructions 166 bytes
SIMPL uses a collection of 32 instruction primitives from which other instructions can be synthesised.   The code body of these primitives is some 100 instructions or so.
Upper (called alpha in V1)    10 instructions   26 bytes
Upper  handles the capital letters - as these are user commands, and the user is able to write and store in RAM certain functionality based on these characters.  When a capital letter is encountered in the instruction buffer, Upper directs control to the correct command buffer. 
Lower   86 bytes   
Lower is an area of program that interprets the lower case characters and provides a higher level of program complexity than is achievable form the primitives alone.
printnum     30 instructions   106 bytes
This takes a 16 bit integer number from the stack and prints a string of ascii digit characters to the terminal.
Uart Routines   13 instructions 41 bytes
Low level communication with the uart is by way of the get_c and put_c routines
Initialisation         19 instructions    90 bytes
Here the hardware such as the oscillator, GPIO and uart are initialised for correct operation.  This is code specific to whichever microcontroller has been chosen
Interpreter           4 instructions       16 bytes
This is the main routine that runs the SIMPL interpreter combining textRead, next, number and Upper.
Here's the code - as it stands. Code window has mess up the formatting  - but it should still cut and paste
; SIMPL - a very small Forth Inspired Extensible Language
; Implementing the Initialisation, TextTead, TextEval and UART routines in MSP430 assembly language
; A Forth-Like Language in under 1024 bytes

; Ken Boak   May 22nd/23rd 2017
; Loops, I/O, Strings and Delays added
; This version 872 bytes
; Instructions take about 1uS cycle  time  - so about 1/16th of clockspeed
            .cdecls C,LIST,"msp430.h"     ; Include device header file

            .def    RESET                   ; Export program entry-point to
                                            ; make it known to linker.
; Variables
			.sect "vars"

			.bss parray, 256

 			.bss x, 2
			.bss name, 2

 ; Using the register model of CH Ting's Direct Thread Model of MSP430 eForth
 ; CPU registers

 ; Register Usage

; R0    MSP430 PC Program Counter
; R1    MSP430 SP Stack Pointer
; R2    MSP430 SR Status Register

tos 			.equ R4
stack 			.equ R5
ip 				.equ R6
temp0 			.equ R7			; loop start
temp1 			.equ R8			; loop counter k
temp2 			.equ R9			; millisecond delay
temp3 			.equ R10        ; microsecond delay
temp4 			.equ R11
instr 			.equ R12
temp5 			.equ R13
temp6 			.equ R14		;
temp7 			.equ R15	    ; Return from alpha  next IP

; Macros

pops 			.macro     	;DROP
				mov.w @stack +, tos

pushs 			.macro 		;DUP
				decd.w stack
				mov.w tos, 0(stack)

; Constants

$NEXT 			.macro
				jmp next
;				mov @ip+, pc ; fetch code address into PC

$NEST 			.macro
				.align 2
				call #DOLST ; fetch code address into PC, W = PFA

$CONST 			.macro
				.align 2
 				call #DOCON ; fetch code address into PC, W = PFA

;; Assembler constants

COMPO 		.equ 040H 		;lexicon compile only bit
IMEDD 		.equ 080H 		;lexicon immediate bit
MASKK 		.equ 07F1FH 	;lexicon bit mask
CELLL 		.equ 2 			;size of a cell
BASEE 		.equ 10 		;default radix
VOCSS 		.equ 8 			;depth of vocabulary stack
BKSPP 		.equ 8 			;backspace
LF 			.equ 10 		;line feed
CRR 		.equ 13 		;carriage return
ERR 		.equ 27 		;error escape
TIC 		.equ 39 		;tick
CALLL 		.equ 012B0H 	;NOP CALL opcodes
UPP 		.equ 200H
DPP 		.equ 220H
SPP 		.equ 378H 		;data stack
TIBB		.equ 380H 		;terminal input buffer
RPP 		.equ 3F8H 		;return stacl
CODEE  		.equ 0C000H 	;code dictionary
COLDD  		.equ 0FFFEH 	;cold start vector
EM  		.equ 0FFFFH 	;top of memory

            .text                           ; Assemble into program memory.
            .retain                         ; Override ELF conditional linking
                                            ; and retain current section.
            .retainrefs                     ; And retain any sections that have
                                            ; references to current section.
; This implements the SIMPL interpreter is MSP430 assembly Language

; textRead
; ------------------------------------------------------------------------------

; Get a character from the UART and store it in the input buffer starting at 0x0200

; Register Usage

; The input buffer - start is at 0x0200, which is pointed to by R14
; R11 is a counter to ensure that we don't exceed 64 characters in input buffer
; R12 receives the character from the uart_get_c routine and puts in the buffer, pointed to by R14
; R14 is the current character position in the input buffer

; 33 instructions

textRead:		MOV.W   #0x0200,R14		; R14 = start of input buffer in RAM
				CLR.B   R11				; i = 0

getChar:	   	CALL    #uart_getc    	; char ch = uart_getc()
				CMP.B   #0x000d,R12		; is it carriage return?  0d
                JEQ     textEnd
				CMP.B   #0x000a,R12     ; Is it  newline?  0a
                JEQ     textEnd
				CMP.B   #0x0020,R12     ; if (ch >= ' ' && ch <= '~')
                JLO     nonValid
				CMP.B   #0x007f,R12
				JHS     nonValid

				CMP.B   #0x003A,R12		; is it colon?  3A
				JNE     notColon

colon:			; If the input character is a colon

				CALL    #uart_getc    	    ; get the next character - which is the NAME
				MOV.B   R12,R13			    ; move the 1st character after the colon to "name" variable in R13

times_32:		SUB.B   #0x0041,R13			; Calculate the destination address - subtract 65 to remove offset of letter A
				ADD.W   R13,R13				; Double R13	; multiply by 2
				ADD.W   R13,R13				; Double R13    ; multiply by 4
				ADD.W   R13,R13				; Double R13	; multiply by 8
				ADD.W   R13,R13				; Double R13	; multiply by 16
				ADD.W   R13,R13				; Double R13	; multiply by 32
				ADD.W   R13,R14				; Add (32*R13) to the index pointer R14
				ADD.W   #0x020,R14			; Add to array pointer  0x0220
				MOV.B   R12,0x0000(R14)     ; Store character at RAM buffer indexed by R14
											; R14 now contains the destination address
				JMP     incPointer

notColon:		INC.W   R14			    	; Increment buffer pointer
				MOV.B   R12,0xffff(R14) 	; Store character at RAM buffer indexed by R14

incPointer:		INC.B   R11			    	; Increment the input buffer pointer i++;

nonValid:		CMP.B   #0x003f,R11			; If input pointer <64 loop back to start
                JLO     getChar		    	; loop back and get next character

textEnd:		mov.b	#0x00,0x0000(R14)	; Put a null terminating  (0x80) zero on the end of the buffer
		;		MOV.B   R11,0x0000(R14)		; Put a null terminating  (0x80) zero on the end of the buffer


; We now come onto the textEval - where based on the value of the character we perform some action routine
; But first we need to determine whether the characers form part of a number - and these must be decoded separately
; and put on the stack

; Register Usage

; ip  - instruction pointer to the current character in the input buffer
; R12   is the accumulator for the number - then stored in location #0x380
; R13   Temporary - use in x10 multipication
; R14

; 16 Instructions

number:			SUB.W   #0x0030,R12				; subtract 0x30 to get a decimal number

number1: 		CMP.B   #0x0030,0x0000(ip)		; >= '0'   Is the next digit a number
				JLO     endNumber				; break
				CMP.B   #0x003a,0x0000(ip)		; <= '9'
				JHS     endNumber				; break

times_10:										; This multipies R12 by 10
				ADDC.W	R12,R12                 ; R12 = 2 * R12
				MOV.W	R12,R13				    ; R13 = 2 * R12
				ADDC.W	R12,R12				    ; R12 = 4 * R12
				ADDC.W	R12,R12				    ; R12 = 8 x R12
				ADDC.W  R13,R12                 ; R12 = 10 x R12

				MOV.B   @ip+,R14			    ; Increment the instruction pointer fetching the next digit
				SUB.W   #0x0030,R14
				ADD.W   R14, R12			    ; Add in the next digit
				JMP     number1					; process the next digit

endNumber:		MOV.W   R12, tos				; Put in tos - the top of stack
				JMP     next				    ; process the next character

												; Character is either a primitive or an alpha - so form CALL address
												; Restore R14 to start of RAM buffer
												; Get the current character location
												; If it's a primitive between 0x20 and 0x3F - point to a look-up table and fetch it's code segment address
												; If its an Alpha, or character >0x40 calculate it's code address from (char - 65)x32
												; Character is in R13 so  calculate the destination address

; -------------------------------------------------------------------------------------------------------------------
; next fetches the next ascii character instruction from memory, decodes it into a jump address and executes the code
; found at that code address
; Each executed word jumps back to next
; Numbers are treated differenty - they are enummerated and put onto the stack by the number routine

; Now we need to decode the instructions using a jump table
; Jump table uses 2 bytes per instruction - so  2 x 96  = 192 bytes

next:			MOV.B   @ip+,R12				; Get the next character from the instruction memory
				MOV.W   R12,R13				    ; Copy into R13 - as needed to decode Jump Address
				SUB.w   #0x0020,R13				; subtract 32 to remove offset of space
				ADD.w	R13,R13					; double it for word address
				add.w	R13,pc					; jump to table entry

tabstart:		jmp space			; SP
				jmp store			; !
				jmp dup				; "
				jmp lit				; #
				jmp swap			; $
				jmp over			; %
				jmp and				; &
				jmp drop			; '
				jmp left_par		; (
				jmp right_par		; )
				jmp mult			; *
				jmp add				; +
				jmp push			; ,
				jmp sub				; -
				jmp pop				; .
				jmp div				; /
				jmp number			; 0
				jmp number			; 1
				jmp number			; 2
				jmp number			; 3
				jmp number			; 4
				jmp number			; 5
				jmp number			; 6
				jmp number			; 7
				jmp number			; 8
				jmp number			; 9
				jmp colon			; :
				jmp semi			; ;
				jmp less			; <
				jmp equal			; =
				jmp greater			; >
				jmp	query			; ?
				jmp fetch			; @
				jmp alpha			; A
				jmp alpha			; B
				jmp alpha			; C
				jmp alpha			; D
				jmp alpha			; E
				jmp alpha			; F
				jmp alpha			; G
				jmp alpha			; H
				jmp alpha			; I
				jmp alpha			; J
				jmp alpha			; K
				jmp alpha			; L
				jmp alpha			; M
				jmp alpha			; N
				jmp alpha			; O
				jmp alpha			; P
				jmp alpha			; Q
				jmp alpha			; R
				jmp alpha			; S
				jmp alpha			; T
				jmp alpha			; U
				jmp alpha			; V
				jmp alpha			; W
				jmp alpha			; X
				jmp alpha			; Y
				jmp alpha			; Z
				jmp	square_left		; [
				jmp f_slash			; \ ;
				jmp square_right	; ]
				jmp xor				; ^
				jmp underscore		; _
				jmp tick			; `
				jmp lower_a			; a
				jmp lower_b			; b
				jmp lower_c			; c
				jmp lower_d			; d
				jmp lower_e			; e
				jmp lower_f			; f
				jmp lower_g			; g
				jmp lower_h			; h
				jmp lower_i			; i
				jmp lower_j			; j
				jmp lower_k			; k
				jmp lower_l			; l
				jmp lower_m			; m
				jmp lower_n			; n
				jmp lower_o			; o
				jmp lower_p			; p
				jmp lower_q			; q
				jmp lower_r			; r
				jmp lower_s			; s
				jmp lower_t			; t
				jmp lower_u			; u
				jmp lower_v			; v
				jmp lower_w			; w
				jmp lower_x			; x
				jmp lower_y			; y
				jmp lower_z			; z
				jmp	curly_left		; {
				jmp or				; |
				jmp curly_right		; }
				jmp inv				; ~
				jmp delete			; del
				jmp textEval_end	; 0x80 is used as null terminator


; Handle the alpha and lower case chars

alpha:			SUB.B   #0x0041,R12				; subtract 65 to remove offset of letter A from original character
				MOV.W	R12,R13					; get it into R13 for multiplying
				ADD.W   R13,R13					; Double R13	; multiply by 2
				ADD.W   R13,R13					; Double R13    ; multiply by 4
				ADD.W   R13,R13					; Double R13	; multiply by 8
				ADD.W   R13,R13					; Double R13	; multiply by 16
				ADD.W   R13,R13					; Double R13	; multiply by 32
				ADD.W   #0x220,R13				; Add (32*R13) to the index pointer R14
				MOV.W   ip,R15				    ; Save the current ip on the return stack R15
												; R13 now contains the jump address for the alpha code
				MOV.W	R13,ip					; instruction pointer

				JMP     next				    ; process the next character

; Handle the primitive instructions

space:			pushs			; Move a 2nd number onto the stack

store:			mov.w @stack +, 0(tos)

dup:            pushs



swap:           mov.w tos, temp0
                mov.w @stack, tos
                mov.w temp0,0( stack)

over:			mov.w tos, temp0
				mov.w @stack, tos
				mov.w temp0,0( stack)

and:			and @stack +, tos

drop:			pops

left_par:									; code enters here on getting a left parenthesis

				MOV.W   tos,R8				; save tos to R8  (R8 is the loop counter k)
				MOV.W   ip,R7				; loop-start = ip the current instruction pointer at start of loop
				JMP     next				; get the next character and execute it

right_par:									; code enters here if instruction it's a right parenthesis

				; TST.W   R8				; is loop counter zero
				; JEQ     next			    ; terminate loop

				DEC.W   R8					; decrement loop counter R8
				JEQ     next			    ; terminate loop
				MOV.W   R7,ip				; set instruction pointer to the start of the loop
				JMP     next				; go around loop again until loop counter = 0



add:			add @stack +, tos



sub:			sub @stack +, tos
;				jmp NEGAT

NEGAT:			inv tos
				inc tos

				jmp     printNum		; go to decimal number print



semi:			; On encountering a semicolon return program control to the next character in the input buffer

				MOV.W	R15,ip			; restore the ip

query:			$NEXT

fetch:			mov.w @tos, tos






underscore:								; Print the enclosed text

				MOV.B   @ip+,R12	    ; Get the next character from the instruction memory
				CMP.B   #0x005f,R12     ; is it an underscore
				jeq     print_end
				CALL 	#uart_putc	    ; send it to uart
				jmp  	print_start
print_end		call    #crlf			; line feed at end of text string

tick:			; tick allows access to the loop counter

				MOV.W   R8,tos

delete:			$NEXT

or:				bis @stack +, tos

xor:			xor 	@stack +, tos

inv:			inv tos

less:			cmp @stack +, tos
				jz FALSE
				jge TRUE
				jmp FALSE

equal:			xor @stack +, tos
				jnz FALSE
				jmp TRUE

greater:		cmp @stack +, tos
				jge FALSE
				jmp TRUE

FALSE: 			clr tos

TRUE: 			mov #0x01, tos

;lower case routines















				MOV.B   #0x0001,&P1OUT							; P1OUT  = BIT0   LED1 on





				; k allows access to the loop counter variable stored in R8

				MOV.W   R8,tos


				MOV.B   #0x0000,&P1OUT					; P1OUT  = BIT0   LED1 off


lower_m:												; millisecond delay
				MOV.W   tos,R10
				mov.w   #5232,R9						; 5232 gives 1mS at 16MHz
uS3_loop:		DEC.W	R9
				JNE		uS3_loop

				DEC.W	R10
				JNE		mS_loop





				JMP printNum










lower_u:									; 3 microsecond deelay

				MOV.W   tos,R10
				DEC.W	R10
				JNE		uS_loop










; User Routines

printNum:		; Take the 16 bit value in R4 stack register and print to terminal as an integer
				; do by repeated subtraction of powers of 10
				; Uses R10,11,12,13
					MOV.W	#10000,R10		; R10 used as the decimation register
	;				CLR.W   R12				; use R12 as a counter
					CLR.W   R11				; Use R11 as scratch
					CLR.W	R13
					MOV.W   tos,R12			; copy the top of stack into R12
					CLRC					; clear the carry

sub10K:				SUB.W   R10,R12
				    JLO      end10K

add10K:				ADD.B #1,R11			; increments the digit count
add_zero:			ADD.W R10,R13			; R13 increases by the decimal value each time
					JMP   sub10K

end10K:				ADD.B  #0x30,R11		; make it a number
					MOV.W  R11,R12
					CALL 	#uart_putc		; output character
					SUB.W   R13,tos			; Decrement the stack count by n x 10
					CLR.W   R11				; Use R11 as scratch
					CLR.W	R13
					MOV.W   tos,R12

decimate:			CMP.W   #10000,R10
					JEQ     use1K
					CMP.W   #1000,R10
					JEQ     use100
					CMP.W   #100,R10
					JEQ     use10
					CMP.W   #10,R10
					JEQ     use1

newline:     		MOV.W #0x0A, R12
					CALL 	#uart_putc			; output CR
					MOV.W #0x0D, R12
					CALL 	#uart_putc			; output LF
					JMP     next

use1K:				MOV.W	#1000,R10
					JMP		sub10K
use100:				MOV.W	#100,R10
					JMP		sub10K
use10:				MOV.W	#10,R10
					JMP		sub10K
use1:				MOV.W	#1,R10
					JMP		sub10K


; Uses R12 to send receive chars via the UART at 115200 baud.

uart_getc:		BIT.B   #1,&IFG2			; while (!(IFG2&UCA0RXIFG)) // USCI_A0 RX buffer ready?
				JEQ     (uart_getc)
				MOV.B   &UCA0RXBUF,R12		; return UCA0RXBUF;

uart_putc:		BIT.B   #2,&IFG2			; while (!(IFG2&UCA0TXIFG))  // USCI_A0 TX buffer ready?
				JEQ     (uart_putc)
                MOV.B   R12,&UCA0TXBUF		; UCA0TXBUF = c;    // TX

crlf:			MOV.W #0x0A, R12
				CALL 	#uart_putc			; output CR
				MOV.W #0x0D, R12
				CALL 	#uart_putc			; output LF

; Main loop here

RESET:       ;	mov.w   #03E0h,SP               ; Initialize stackpointer

				mov #RPP, SP 					; set up stack
				mov #SPP, stack
				clr tos

StopWDT:     	mov.w   #WDTPW|WDTHOLD,&WDTCTL  ; Stop watchdog timer WDTCTL  = WDTPW + WDTHOLD; Stop WDT

OSC_GPIO_init:	; Run the CPU at full 16MHz with 11500baud UART

				MOV.B   &CALBC1_16MHZ,&BCSCTL1					;BCSCTL1 = CALBC1_16MHZ;  Set DCO

SetupP1:     	bis.b   #041h,&P1DIR            				;P1.0 P1.6  output  as P1.0 and P1.6 are the red+green LEDs
				MOV.B   #0x0000,&P1OUT							;P1OUT  = BIT0 + BIT6; 				// All LEDs off

uart_init:      MOV.B   #0x0006,&P1SEL							;Initialise the UART for 115200 baud
				MOV.B   #0x0006,&P1SEL2							;P1SEL2 = RXD + TXD;
				BIS.B   #0x0080,&UCA0CTL1						;UCA0CTL1 |= UCSSEL_2// SMCLK
				MOV.B   #0x008A,&UCA0BR0						;UCA0BR0 = 138 // 16MHz 115200
				CLR.B   &UCA0BR1								;UCA0BR1 = 0 // 16MHz 115200
				MOV.B   #2,&UCA0MCTL							;UCA0MCTL = UCBRS0  // Modulation UCBRSx = 1
				BIC.B   #1,&UCA0CTL1							;UCA0CTL1 &= ~UCSWRST Initialize USCI state machine

				MOV.W #0x4F, R12
				CALL 	#uart_putc			; output "O"
				MOV.W #0x4B, R12
				CALL 	#uart_putc			; output "K"						; Print OK


				call	#textRead
  				MOV.W   #0x0200,ip				; set ip (instruction pointer) - to start of input buffer in RAM at address 0x0200
				jmp    next						; get the next instruction

textEval_end:  	jmp     interpreter				; loop around

; Stack Pointer definition
            .global __STACK_END
            .sect   .stack

; Interrupt Vectors
            .sect   ".reset"                ; MSP430 RESET Vector
            .short  RESET







Share this post

Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Create New...