├── ft232r.cfg ├── src ├── boards │ ├── longan-nano │ │ ├── board.s │ │ └── linker.ld │ └── longan-nano-lite │ │ ├── board.s │ │ └── linker.ld ├── 03-interrupts.s ├── 07-error-handling.s ├── 04-io-helpers.s ├── 01-variables-constants.s ├── 06-initialization.s ├── mcus │ └── gd32vf103 │ │ └── mcu.s ├── 02-macros.s ├── 05-internal-functions.s ├── 09-interpreter.s └── 08-forth-primitives.s ├── .gitignore ├── debug.gdb ├── docs ├── README.md ├── EXPLAIN.md ├── TUTORIALS.md ├── HOWTO.md └── REFERENCE.md ├── openocd.cfg ├── djb2.c ├── .github └── workflows │ └── main.yml ├── LICENSE ├── fiveforths.s ├── Makefile └── README.md /ft232r.cfg: -------------------------------------------------------------------------------- 1 | adapter driver ft232r 2 | adapter speed 1000 3 | -------------------------------------------------------------------------------- /src/boards/longan-nano/board.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Longan Nano 3 | ## 4 | 5 | .equ RAM_SIZE, 1024 * 32 # 32 KiB 6 | -------------------------------------------------------------------------------- /src/boards/longan-nano-lite/board.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Longan Nano Lite 3 | ## 4 | 5 | .equ RAM_SIZE, 1024 * 20 # 20 KiB 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.hex 2 | *.bin 3 | *.elf 4 | *.o 5 | *.dump 6 | _site 7 | vendor/ 8 | .sass-cache 9 | .jekyll-cache 10 | Gemfile.lock 11 | -------------------------------------------------------------------------------- /debug.gdb: -------------------------------------------------------------------------------- 1 | target extended-remote :3333 2 | 3 | # print demangled symbols 4 | set print asm-demangle on 5 | 6 | set confirm off 7 | 8 | # set backtrace limit to not have infinite backtrace loops 9 | set backtrace limit 32 10 | 11 | set pagination off 12 | monitor reset halt 13 | #load 14 | break interrupt_handler 15 | -------------------------------------------------------------------------------- /src/03-interrupts.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Interrupts 3 | ## 4 | 5 | .balign CELL 6 | .global interrupt_handler 7 | .type interrupt_handler, @function 8 | # unimplemented interrupt handler for now 9 | interrupt_handler: 10 | mret 11 | 12 | # Initialize the interrupt CSRs 13 | interrupt_init: 14 | # disable global interrupts 15 | csrc mstatus, 0x08 # mstatus = 0x300 16 | 17 | # clear machine interrupt enable bits 18 | csrs mie, zero # mie = 0x304 19 | 20 | # set interrupt handler jump address 21 | la t0, interrupt_handler 22 | csrw mtvec, t0 # mtvec = 0x305 23 | 24 | ret 25 | -------------------------------------------------------------------------------- /src/boards/longan-nano/linker.ld: -------------------------------------------------------------------------------- 1 | /* GD32VF103CB */ 2 | MEMORY 3 | { 4 | FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128k 5 | RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 32k 6 | } 7 | 8 | /* The entry point is the reset handler */ 9 | ENTRY(_start); 10 | 11 | SECTIONS { 12 | __stacktop = ORIGIN(RAM) + LENGTH(RAM); 13 | 14 | /* The program code and other data goes into FLASH */ 15 | .text : 16 | { 17 | . = ALIGN(4); 18 | *(.text) /* .text sections (code) */ 19 | *(.rodata) /* .rodata sections (constants, strings, etc.) */ 20 | } >FLASH 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/boards/longan-nano-lite/linker.ld: -------------------------------------------------------------------------------- 1 | /* GD32VF103C8 */ 2 | MEMORY 3 | { 4 | FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64k 5 | RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20k 6 | } 7 | 8 | /* The entry point is the reset handler */ 9 | ENTRY(_start); 10 | 11 | SECTIONS { 12 | __stacktop = ORIGIN(RAM) + LENGTH(RAM); 13 | 14 | /* The program code and other data goes into FLASH */ 15 | .text : 16 | { 17 | . = ALIGN(4); 18 | *(.text) /* .text sections (code) */ 19 | *(.rodata) /* .rodata sections (constants, strings, etc.) */ 20 | } >FLASH 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/07-error-handling.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Error handling 3 | ## 4 | 5 | print_error error, 4, reset 6 | print_error ok, 6, tib_init 7 | print_error reboot, 16, _start 8 | print_error tib, 14, reset 9 | print_error mem, 16, reset 10 | print_error token, 14, reset 11 | print_error underflow, 20, reset 12 | print_error overflow, 20, reset 13 | 14 | msg_error: .ascii " ?\n" 15 | msg_ok: .ascii " ok\n" 16 | msg_reboot: .ascii " ok rebooting\n" 17 | msg_tib: .ascii " ? tib full\n" 18 | msg_mem: .ascii " ? memory full\n" 19 | msg_token: .ascii " ? big token\n" 20 | msg_underflow: .ascii " ? stack underflow\n" 21 | msg_overflow: .ascii " ? stack overflow\n" 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # FiveForths: Documentation 2 | 3 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 4 | 5 | --- 6 | 7 | * [TUTORIALS](TUTORIALS.md): a quick guide to **get started** 8 | * [EXPLAIN](EXPLAIN.md): learn the story behind _FiveForths_ 9 | * [HOWTO](HOWTO.md): build, usage, and code examples in Forth and RISC-V Assembly 10 | * [REFERENCE](REFERENCE.md): learn the technical details, what's under the hood 11 | 12 | # License 13 | 14 | [MIT License](LICENSE) 15 | 16 | FiveForths documentation and source code copyright © 2021~ [Alexander Williams](https://a1w.ca) and licensed under the permissive open source [MIT](https://opensource.org/licenses/MIT) license. 17 | -------------------------------------------------------------------------------- /openocd.cfg: -------------------------------------------------------------------------------- 1 | # autoexit true 2 | 3 | set _CHIPNAME riscv 4 | jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x1000563d 5 | 6 | set _TARGETNAME $_CHIPNAME.cpu 7 | target create $_TARGETNAME riscv -chain-position $_TARGETNAME 8 | $_TARGETNAME configure -work-area-phys 0x20000000 -work-area-size 20480 -work-area-backup 0 9 | 10 | 11 | # Work-area is a space in RAM used for flash programming 12 | if { [info exists WORKAREASIZE] } { 13 | set _WORKAREASIZE $WORKAREASIZE 14 | } else { 15 | set _WORKAREASIZE 0x5000 16 | } 17 | 18 | # Allow overriding the Flash bank size 19 | if { [info exists FLASH_SIZE] } { 20 | set _FLASH_SIZE $FLASH_SIZE 21 | } else { 22 | # autodetect size 23 | set _FLASH_SIZE 0 24 | } 25 | 26 | # flash size will be probed 27 | set _FLASHNAME $_CHIPNAME.flash 28 | 29 | flash bank $_FLASHNAME gd32vf103 0x08000000 0 0 0 $_TARGETNAME 30 | riscv set_reset_timeout_sec 1 31 | init 32 | 33 | halt 34 | -------------------------------------------------------------------------------- /djb2.c: -------------------------------------------------------------------------------- 1 | /* 2 | FiveForths - https://github.com/aw/FiveForths 3 | RISC-V Forth implementation 4 | 5 | The MIT License (MIT) 6 | Copyright (c) 2021~ Alexander Williams, https://a1w.ca 7 | */ 8 | 9 | #include 10 | #include 11 | 12 | // source: http://www.cse.yorku.ca/~oz/hash.html 13 | unsigned long djb2(unsigned char *str) 14 | { 15 | unsigned long hash = 5381; 16 | int c; 17 | 18 | while (c = *str++) 19 | hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ 20 | 21 | return hash; 22 | } 23 | 24 | int main(int argc, char *argv[]) { 25 | unsigned char *str = argv[1]; 26 | unsigned long result = djb2(str); 27 | 28 | int length = strlen(str) << 24; // move the length to the last 8 bits (MSG) by shifting length by 24 29 | result = result & ~0xff000000; // zero out the 8-bit flags+length by inverting the mask 30 | result = result | length; // add the length of the string to the hash by bitwise OR'ing 31 | 32 | printf("djb2_hash: 0x%08x\n", result ); 33 | 34 | return 0; 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | timeout-minutes: 5 11 | 12 | strategy: 13 | matrix: 14 | device: ['longan-nano', 'longan-nano-lite'] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Update APT repos 20 | run: sudo apt-get update 21 | 22 | - name: Install RISC-V build dependencies 23 | run: sudo apt-get install build-essential binutils-riscv64-unknown-elf gcc-riscv64-unknown-elf 24 | 25 | - name: Build the FiveForths firmware binary for ${{matrix.device}} 26 | run: make ${{matrix.device}} 27 | 28 | - name: Rename firmware binary file 29 | run: mv fiveforths.bin fiveforths-${{matrix.device}}.bin 30 | 31 | - name: Obtain SHA256 hash of the firmware 32 | run: sha256sum fiveforths-${{matrix.device}}.bin > fiveforths-${{matrix.device}}.bin.sha256 33 | 34 | - uses: actions/upload-artifact@v3 35 | with: 36 | name: fiveforths-firmware-${{matrix.device}} 37 | path: fiveforths-${{matrix.device}}.bin* 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021~ Alexander Williams, https://a1w.ca 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /fiveforths.s: -------------------------------------------------------------------------------- 1 | /* 2 | FiveForths - https://github.com/aw/FiveForths 3 | RISC-V Forth implementation 4 | 5 | The MIT License (MIT) 6 | Copyright (c) 2021~ Alexander Williams, https://a1w.ca 7 | */ 8 | 9 | ## 10 | # Forth registers 11 | ## 12 | 13 | # sp = DSP = data stack pointer 14 | # a0 = W = working register 15 | # a1 = X = working register 16 | # a2 = Y = working register 17 | # a3 = Z = working register 18 | # s0 = FP = frame pointer (unused for now) 19 | # s1 = IP = instruction pointer 20 | # s2 = RSP = return stack pointer 21 | 22 | # Ensure the _start entry label is defined first 23 | .text 24 | .global _start 25 | _start: 26 | j boot 27 | 28 | # include board-specific functions and constants from src/boards// 29 | .include "board.s" 30 | 31 | # include MCU-specific functions and constants from src/mcus// 32 | .include "mcu.s" 33 | 34 | # include source files from src/ 35 | .include "01-variables-constants.s" 36 | .include "02-macros.s" 37 | .include "03-interrupts.s" 38 | .include "04-io-helpers.s" 39 | .include "05-internal-functions.s" 40 | .include "06-initialization.s" 41 | .include "07-error-handling.s" 42 | .include "08-forth-primitives.s" 43 | .include "09-interpreter.s" 44 | -------------------------------------------------------------------------------- /src/04-io-helpers.s: -------------------------------------------------------------------------------- 1 | ## 2 | # I/O Helpers 3 | ## 4 | 5 | uart_get: 6 | li t0, USART0_BASE_ADDRESS # load USART0 base address 7 | uart_get_loop: 8 | lw t1, UART_RX_STATUS(t0) # load value from status register 9 | andi t1, t1, UART_RX_BIT # load read data buffer not empty bit 10 | beqz t1, uart_get_loop # loop until ready to receive 11 | lbu a0, UART_RX_DATA(t0) # read character (zero-extended) from data register 12 | 13 | ret 14 | 15 | uart_put: 16 | li t0, USART0_BASE_ADDRESS # load USART0 base address 17 | uart_put_loop: 18 | lw t1, UART_TX_STATUS(t0) # load value from status register 19 | andi t1, t1, UART_TX_BIT # load transmit data buffer empty bit 20 | beqz t1, uart_put_loop # loop until ready to send 21 | sb a0, UART_TX_DATA(t0) # send character to data register 22 | 23 | ret 24 | 25 | # print a string to the uart 26 | # arguments: a1 = address of the message to be printed, a2 = address+length of the message 27 | uart_print: 28 | mv s3, ra # save the return address 29 | uart_print_loop: 30 | beq a1, a2, uart_print_done # done if we've printed all characters 31 | lbu a0, 0(a1) # load 1 character from the message string 32 | call uart_put 33 | addi a1, a1, 1 # increment the address by 1 34 | j uart_print_loop # loop to print the next message 35 | uart_print_done: 36 | mv ra, s3 # restore the return address 37 | ret 38 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # fiveforths - https://github.com/aw/fiveforths 2 | # 3 | # Makefile for building and testing 4 | 5 | PROGNAME = fiveforths 6 | FIRMWARE ?= $(PROGNAME).bin 7 | DEVICE ?= /dev/ttyUSB0 8 | CFLAGS := -g 9 | CROSS ?= /usr/bin/riscv64-unknown-elf- 10 | AS := $(CROSS)as 11 | LD := $(CROSS)ld 12 | OBJCOPY := $(CROSS)objcopy 13 | OBJDUMP := $(CROSS)objdump 14 | READELF := $(CROSS)readelf 15 | 16 | # MCU and board specific variables 17 | ARCH ?= rv32imac_zicsr 18 | EMU ?= elf32lriscv 19 | MCU ?= gd32vf103 20 | BOARD ?= longan-nano-lite 21 | 22 | .PHONY: clean 23 | 24 | build: $(PROGNAME).o $(PROGNAME).elf $(PROGNAME).bin $(PROGNAME).hex $(PROGNAME).dump 25 | 26 | %.o: %.s 27 | $(AS) $(CFLAGS) -march=$(ARCH) -I src/boards/$(BOARD) -I src/mcus/$(MCU) -I src -o $@ $< 28 | 29 | %.elf: %.o 30 | $(LD) -m $(EMU) -T src/boards/$(BOARD)/linker.ld -o $@ $< 31 | 32 | %.bin: %.elf 33 | $(OBJCOPY) -O binary $< $@ 34 | 35 | %.hex: %.elf 36 | $(OBJCOPY) -O ihex $< $@ 37 | 38 | %.dump: %.elf 39 | $(OBJDUMP) -D -S $< > $@ 40 | 41 | readelf: $(PROGNAME).elf 42 | $(READELF) -a $< 43 | 44 | serve: 45 | bundle exec jekyll serve 46 | 47 | openocd: 48 | /opt/riscv/bin/openocd -f ft232r.cfg -f openocd.cfg 49 | 50 | debug: 51 | /opt/riscv/bin/riscv64-unknown-elf-gdb -command=debug.gdb -q fiveforths.elf 52 | 53 | flash: 54 | stm32loader -p $(DEVICE) -ewv $(FIRMWARE) 55 | 56 | djb2: 57 | gcc -o djb2 djb2.c 58 | 59 | longan-nano: 60 | $(MAKE) build BOARD=longan-nano 61 | 62 | longan-nano-lite: 63 | $(MAKE) build BOARD=longan-nano-lite 64 | 65 | clean: 66 | rm -v *.bin *.elf *.o *.hex *.dump 67 | -------------------------------------------------------------------------------- /src/01-variables-constants.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Variables and constants 3 | ## 4 | 5 | .equ FORTH_VERSION, 3 6 | 7 | ## 8 | # Memory map 9 | ## 10 | 11 | # DSP, RSP, TIB stacks grow downward from the top of memory 12 | .equ DSP_TOP, RAM_BASE + RAM_SIZE # address of top of data stack 13 | .equ RSP_TOP, DSP_TOP - STACK_SIZE # address of top of return stack 14 | .equ TIB_TOP, RSP_TOP - STACK_SIZE # address of top of terminal buffer 15 | .equ TIB, TIB_TOP - STACK_SIZE # address of bottom of terminal buffer 16 | 17 | # variables 18 | .equ STATE, TIB - CELL # 1 CELL for STATE variable 19 | .equ TOIN, STATE - CELL # 1 CELL for TOIN variable (looks into TIB) 20 | .equ HERE, TOIN - CELL # 1 CELL for HERE variable 21 | .equ LATEST, HERE - CELL # 1 CELL for LATEST variable 22 | .equ NOOP, LATEST - CELL # 1 CELL for NOOP variable 23 | .equ PAD, NOOP - (CELL * 64) # 64 CELLS between NOOP and PAD 24 | 25 | # dictionary grows upward from the RAM base address 26 | .equ FORTH_SIZE, PAD - RAM_BASE # remaining memory for Forth 27 | 28 | ## 29 | # Interpreter constants 30 | ## 31 | 32 | .equ CHAR_NEWLINE, '\n' # newline character 0x0A 33 | .equ CHAR_CARRIAGE, '\r' # carriage return character 0x0D 34 | .equ CHAR_SPACE, ' ' # space character 0x20 35 | .equ CHAR_BACKSPACE, '\b' # backspace character 0x08 36 | .equ CHAR_COMMENT, '\\' # backslash character 0x5C 37 | .equ CHAR_COMMENT_OPARENS, '(' # open parenthesis character 0x28 38 | .equ CHAR_COMMENT_CPARENS, ')' # close parenthesis character 0x29 39 | .equ CHAR_MINUS, '-' # minus character 0x2D 40 | 41 | ## 42 | # Flags 43 | ### 44 | 45 | .equ F_IMMEDIATE, 0x80000000 # inverse = 0x7fffffff, immediate flag mask 46 | .equ F_HIDDEN, 0x40000000 # inverse = 0xbfffffff, hidden flag mask 47 | .equ F_USER, 0x20000000 # inverse = 0xdfffffff, user flag mask 48 | .equ FLAGS_MASK, 0xe0000000 # inverse = 0x1fffffff, 3-bit flags mask 49 | .equ FLAGS_LEN, 0xff000000 # inverse = 0x00ffffff, 8-bit flags+length mask 50 | -------------------------------------------------------------------------------- /src/06-initialization.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Initialization 3 | ## 4 | 5 | # board boot initializations 6 | boot: 7 | call interrupt_init # RISC-V interrupt CSR initialization 8 | call uart_init # board specific UART initialization 9 | call gpio_init # board specific GPIO initialization 10 | 11 | # initialize HERE variable 12 | li t0, RAM_BASE # load RAM_BASE memory address 13 | li t1, HERE # load HERE variable 14 | sw t0, 0(t1) # initialize HERE variable to contain RAM_BASE memory address 15 | 16 | # initialize LATEST variable 17 | la t0, word_SEMI # load address of the last word in Flash memory (;) for now 18 | li t1, LATEST # load LATEST variable 19 | sw t0, 0(t1) # initialize LATEST variable to contain word_SEMI memory address 20 | 21 | # display boot message 22 | la a1, msg_boot # load string message 23 | addi a2, a1, 74 # load string length 24 | call uart_print # call uart print function 25 | j reset 26 | 27 | # reset the Forth stack pointers, registers, variables, and state 28 | reset: 29 | # initialize stack pointers 30 | la sp, __stacktop # initialize DSP register 31 | la s1, interpreter_start # initialize IP register 32 | li s2, RSP_TOP # initialize RSP register 33 | 34 | # initialize function parameters 35 | mv a0, zero # initialize W register 36 | mv a1, zero # initialize X register 37 | mv a2, zero # initialize Y register 38 | mv a3, zero # initialize Z register 39 | 40 | # initialize STATE variable 41 | li t0, STATE # load STATE variable 42 | sw zero, 0(t0) # initialize STATE variable (0 = execute) 43 | 44 | .balign CELL 45 | # reset the RAM from the last defined word 46 | ram_init: 47 | li t0, HERE # load HERE memory address 48 | lw t0, 0(t0) # load HERE value 49 | li t1, PAD # load PAD variable 50 | ram_zerofill: 51 | # initialize the memory cells 52 | beq t0, t1,ram_done # loop until counter (HERE) == PAD 53 | sw zero, 0(t0) # zero-fill the memory address 54 | addi t0, t0, CELL # increment counter by 1 CELL 55 | j ram_zerofill # repeat 56 | ram_done: 57 | # continue to tib_init 58 | 59 | .balign CELL 60 | # reset the terminal input buffer 61 | tib_init: 62 | # initialize TOIN variable 63 | li t0, TIB # load TIB memory address 64 | li t1, TOIN # load TOIN variable 65 | li t2, TIB_TOP # load TIB_TOP variable 66 | sw zero, 0(t1) # initialize TOIN variable to contain zero 67 | tib_zerofill: 68 | # initialize the TIB 69 | beq t2, t0,tib_done # loop until TIB_TOP == TIB 70 | addi t2, t2, -CELL # decrement TIB_TOP by 1 CELL 71 | sw zero, 0(t2) # zero-fill the memory address 72 | j tib_zerofill # repeat 73 | tib_done: 74 | j interpreter_start # jump to the main interpreter REPL 75 | 76 | msg_boot: .ascii "FiveForths v0.5, Copyright (c) 2021~ Alexander Williams, https://a1w.ca \n\n" 77 | -------------------------------------------------------------------------------- /src/mcus/gd32vf103/mcu.s: -------------------------------------------------------------------------------- 1 | ## 2 | # GD32VF103 3 | ## 4 | 5 | .equ CELL, 4 # 32-bits cell size 6 | .equ RAM_BASE, 0x20000000 # base address of RAM 7 | .equ STACK_SIZE, 256 # 256 Bytes 8 | 9 | # UART 10 | .equ USART0_BASE_ADDRESS, 0x40013800 11 | .equ UART_RX_STATUS, 0x00 # USART status register offset (USART_STAT) 12 | .equ UART_RX_DATA, 0x04 # data register offset (USART_DATA) 13 | .equ UART_TX_STATUS, 0x00 # USART status register offset (USART_STAT) 14 | .equ UART_TX_DATA, 0x04 # data register offset (USART_DATA) 15 | .equ UART_RX_BIT, (1 << 5) # read data buffer not empty bit (RBNE) 16 | .equ UART_TX_BIT, (1 << 7) # transmit data buffer empty bit (TBE)d 17 | 18 | .balign CELL 19 | # Initialize the UART 20 | uart_init: 21 | # enable the RCU clocks 22 | li t0, 0x40021000 # load base address of the RCU 23 | lw t1, 0x18(t0) # load value from the APB2 enable register (RCU_APB2EN) 24 | li t2, (1 << 14) | (1 << 2) | (1 << 0) # set USART0EN (bit 14), PAEN (bit 2), AFEN (bit 0) 25 | or t1, t1, t2 # add the enabled bits to the existing RCU_APB2EN value 26 | sw t1, 0x18(t0) # store value in RCU_APB2EN register at offset 0x18 27 | 28 | # set the baud rate: USARTDIV = frequency (8 MHz) / baud rate (115200 bps) 29 | li t0, 0x40013800 # load USART0 base address 30 | li t1, ((8000000/115200) & 0x0000fff0) | ((8000000/115200) & 0x0000000f) # load baud rate divider 31 | sw t1, 0x08(t0) # store the value in the Baud rate register (USART_BAUD) 32 | 33 | # set the stop bits 34 | li t1, (0 << 12) | (0 << 13) # set STB (bits 12 and 13) to 00 (1 stop bit) 35 | sw t1, 0x10(t0) # store the value in the USART Control register 1 (USART_CTL1) 36 | 37 | # disable hardware flow control, half-duplex, etc 38 | sw zero, 0x14(t0) # store the value in the USART Control register 2 (USART_CTL2) 39 | 40 | # enable receiver, transmitter, uart, interrupts 41 | li t1, (1 << 2) | (1 << 3) | (1 << 5) | (1 << 6) | (1 << 7) | (1 << 13) # set REN, TEN, RBNEIE, TCIE, TBEIE, UEN bits to 1 42 | sw t1, 0x0C(t0) # store the value in the USART Control register 0 (USART_CTL0) 43 | 44 | uart_done: 45 | ret 46 | 47 | .balign CELL 48 | # Initialize the GPIO 49 | gpio_init: 50 | # configure TX on pin 9 of port A (0b1011) 51 | li t0, 0x40010800 # load base address of GPIOA 52 | lw t1, 0x04(t0) # load value from the Port control register 1 (GPIOA_CTL1) 53 | li t2, 0xfffff00f # load bitmask to clear 8 bits (MD9[1:0],CTL9[1:0],MD10[1:0],CTL10[1:0]) 54 | and t1, t1, t2 # clear the bits (TX 4,5,6,7) and (RX 8,9,10,11) 55 | or t1, t1, (1 << 4) | (1 << 5) | (0 << 6) | (1 << 7) # set the bits 4,5,6,7 (afio push-pull, max speed 50MHz) 56 | 57 | # configure RX on pin 10 of port A (0b0100) 58 | li t2, (0 << 8) | (0 << 9) | (1 << 10) | (0 << 11) # load the bits 8,9,10,11 59 | or t1, t1, t2 # set the bits 8,9,10,11 (input floating, mode) 60 | sw t1, 0x04(t0) # store the value in the Port control register 1 (GPIOA_CTL1) 61 | 62 | gpio_done: 63 | ret 64 | -------------------------------------------------------------------------------- /src/02-macros.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Macros 3 | ## 4 | 5 | # jump to the next subroutine (ITC), appended to each primitive 6 | .macro NEXT 7 | lw a0, 0(s1) # load memory address from IP into W 8 | addi s1, s1, CELL # increment IP by CELL size 9 | lw t0, 0(a0) # load address from W into temporary 10 | jr t0 # jump to the address in temporary 11 | .endm 12 | 13 | # pop top of data stack to register and move DSP 14 | .macro POP reg 15 | lw \reg, 0(sp) # load DSP value to register 16 | addi sp, sp, CELL # move the DSP up by 1 cell 17 | .endm 18 | 19 | # push register to top of stack and move DSP 20 | .macro PUSH reg 21 | li t0, RSP_TOP+CELL # load address of bottom of stack + 1 CELL 22 | blt sp, t0, err_overflow # jump to error handler if stack overflow 23 | 24 | sw \reg, -CELL(sp) # store the value in the register to the top of the DSP 25 | addi sp, sp, -CELL # move the DSP down by 1 cell 26 | .endm 27 | 28 | # push register to return stack 29 | .macro PUSHRSP reg 30 | li t0, TIB_TOP+CELL # load address of bottom of stack + 1 CELL 31 | blt s2, t0, err_overflow # jump to error handler if stack overflow 32 | 33 | sw \reg, -CELL(s2) # store value from register into RSP 34 | addi s2, s2, -CELL # decrement RSP by 1 cell 35 | .endm 36 | 37 | # pop top of return stack to register 38 | .macro POPRSP reg 39 | li t0, RSP_TOP # load address of top of RSP 40 | bge s2, t0, err_underflow # jump to error handler if stack underflow 41 | 42 | lw \reg, 0(s2) # load value from RSP into register 43 | addi s2, s2, CELL # increment RSP by 1 cell 44 | .endm 45 | 46 | # define a primitive dictionary word 47 | .macro defcode name, hash, label, link 48 | .section .rodata 49 | .balign CELL # align to CELL bytes boundary 50 | .globl word_\label 51 | word_\label : 52 | .4byte word_\link # 32-bit pointer to codeword of link 53 | .globl hash_\label 54 | hash_\label : 55 | .4byte \hash # 32-bit hash of this word 56 | .globl code_\label 57 | code_\label : 58 | .4byte body_\label # 32-bit pointer to codeword of label 59 | .globl body_\label 60 | body_\label : # assembly code below 61 | .endm 62 | 63 | # check a character 64 | .macro checkchar char, dest 65 | call uart_get # read a character from UART 66 | call uart_put # send the character to UART 67 | 68 | # validate the character which is located in the W (a0) register 69 | li t0, \char # load character into temporary 70 | beq a0, t0, \dest # jump to the destination if the char matches 71 | .endm 72 | 73 | # print a message 74 | .macro print_error name, size, jump 75 | .balign CELL 76 | err_\name : 77 | la a1, msg_\name # load string message 78 | addi a2, a1, \size # load string length 79 | call uart_print # call uart print function 80 | j \jump # jump when print returns 81 | .endm 82 | 83 | # restore HERE and LATEST variables 84 | .macro restorevars reg 85 | # update HERE 86 | li t0, HERE # load HERE variable into temporary 87 | sw \reg, 0(t0) # store the address of LATEST back into HERE 88 | 89 | # update LATEST 90 | li t0, LATEST # load LATEST variable into temporary 91 | lw t1, 0(\reg) # load LATEST variable value into temporary 92 | sw t1, 0(t0) # store LATEST word into LATEST variable 93 | .endm 94 | 95 | # check for stack underflow 96 | .macro checkunderflow stacktop 97 | li t0, DSP_TOP-\stacktop # load address of top of stack 98 | bge sp, t0, err_underflow # jump to error handler if stack underflow 99 | .endm 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FiveForths: 32-bit RISC-V Forth for microcontrollers 2 | 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/aw/fiveforths/main.yml) [![GitHub release](https://img.shields.io/github/release/aw/fiveforths.svg)](https://github.com/aw/fiveforths) [![justforfunnoreally.dev badge](https://img.shields.io/badge/justforfunnoreally-dev-9ff)](https://justforfunnoreally.dev) 4 | 5 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 6 | 7 | ![fiveforths-terminal](https://user-images.githubusercontent.com/153401/213904509-089dd490-62e2-4c9d-9f9c-a6e5fab34e17.png) 8 | 9 | _FiveForths_ currently uses the _indirect threading_ model and only has 19 built-in primitive words. It is 100% fully functional and can be extended by adding new primitives (in Assembly) or by defining new words (in Forth). This implementation is loosely inspired by [sectorforth](https://github.com/cesarblum/sectorforth), [jonesforth](https://github.com/nornagon/jonesforth), and [derzforth](https://github.com/theandrew168/derzforth). 10 | 11 | Development progress has been logged regularly in the [devlogs](https://fiveforths.a1w.ca/). 12 | 13 | --- 14 | 15 | 1. [Quick start](#quick-start) 16 | 2. [Documentation](#documentation) 17 | 3. [Todo](#todo) 18 | 4. [Contributing](#contributing) 19 | 5. [Changelog](#changelog) 20 | 6. [License](#license) 21 | 22 | # Quick start 23 | 24 | The quickest way to get started is to download and flash one of the firmware binaries listed below:. 25 | 26 | * [fiveforths-longan-nano-lite.bin](https://github.com/aw/fiveforths/releases/download/v0.5/fiveforths-longan-nano-lite.bin) (64K Flash, 20K RAM) 27 | * [fiveforths-longan-nano.bin](https://github.com/aw/fiveforths/releases/download/v0.5/fiveforths-longan-nano.bin) (128K Flash, 32K RAM) 28 | 29 | See the [TUTORIALS](docs/TUTORIALS.md) for detailed download and flashing information. 30 | 31 | # Documentation 32 | 33 | * [TUTORIALS](docs/TUTORIALS.md): a quick guide to **get started** 34 | * [EXPLAIN](docs/EXPLAIN.md): learn the story behind _FiveForths_ 35 | * [HOWTO](docs/HOWTO.md): build, usage, and code examples in Forth and RISC-V Assembly 36 | * [REFERENCE](docs/REFERENCE.md): learn the technical details, what's under the hood 37 | 38 | # TODO 39 | 40 | - [ ] Code cleanup and optimizations 41 | 42 | # Contributing 43 | 44 | Please create a pull-request or [open an issue](https://github.com/aw/picolisp-kv/issues/new) on GitHub. 45 | 46 | # Changelog 47 | 48 | ## 0.5 (2023-05-29) 49 | 50 | * Fix issue #19 - Error building without zicsr extension 51 | * Fix issue #20 - Specify `stm32loader` instead of `dfu-util` in docs 52 | 53 | ## 0.4 (2023-01-23) 54 | 55 | * Fix issue #16 - Add the ability to read hex numbers 56 | * Update documentation 57 | * Add example for toggling an LED 58 | 59 | ## 0.3 (2023-01-19) 60 | 61 | * Fix issue #7 - Implement bounds checks for stacks 62 | * Fix issue #8 - Implement bounds checks for user dictionary 63 | * Fix issue #13 - `TOIN` should not be an address 64 | * Fix issue #14 - `STORE` primitive is incorrect 65 | * Add better error messages 66 | * Add detailed documentation in [docs](docs/) 67 | * Add `djb2.c` to generate a word's hash locally 68 | * Add RAM zerofill of unused dictionary space on reset 69 | 70 | ## 0.2 (2023-01-10) 71 | 72 | * Fix issue #9 - Handling of carriage return 73 | * Fix issue #11 - Ignore non-printable characters 74 | * Re-organize code to support different boards and MCUs 75 | * Add boot message when the device is reset 76 | * Add GitHub action to automatically build and publish the firmware binaries 77 | 78 | ## 0.1 2023-01-09 - First release 79 | 80 | # License 81 | 82 | [MIT License](LICENSE) 83 | 84 | Copyright (c) 2021~ [Alexander Williams](https://a1w.ca) 85 | -------------------------------------------------------------------------------- /docs/EXPLAIN.md: -------------------------------------------------------------------------------- 1 | # FiveForths: Explain 2 | 3 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 4 | 5 | --- 6 | 7 | This document provides an explanation of _FiveForths_ and the story behind it. 8 | 9 | ## Menu 10 | 11 | 1. [Why another Forth?](#why-another-forth) 12 | 2. [Why not Lisp?](#why-not-lisp) 13 | 3. [Why indirect threading?](#why-indirect-threading) 14 | 4. [Why word hashing?](#why-word-hashing) 15 | 5. [Why so few primitives?](#why-so-few-primitives) 16 | 17 | ### Why another Forth 18 | 19 | Most likely, every **Forth** creator in this millenium has faced this question. I've already blogged about my [decision here](https://a1w.ca/p/2021-03-15-the-future-of-computing-with-riscv-fpgas-and-forth/) and [here](https://a1w.ca/p/2023-01-03-year-of-the-microcontroller/). To resume, I think _Forth_ is a nice way to start fresh in the world of microcontrollers and FPGAs. I think if we want to go back to owning our tools and creating things that run the way they should, then we need to start from a good minimal base. 20 | 21 | My initial goal was to start from [derzforth](https://github.com/theandrew168/derzforth/pulls?q=is%3Apr+is%3Aclosed) by contributing to the project and help bring it to a fully useable implementation, but I eventually decided to create my own which was more aligned with my personal goals. 22 | 23 | I want to use _FiveForths_ as an alternative to my current C++/Lua-based microcontroller projects. I also want to use it as a building block for deploying to larger RISC-V instruction sets (ex: 64-bit) on much more powerful devices. Eventually, like most dreamers, I'd like to create an operating system or something like that, so I guess I had to start somewhere. 24 | 25 | ### Why not Lisp 26 | 27 | Those who know me are aware that I've been programming in [PicoLisp](https://picolisp.com) for almost a decade. It's been my go-to language for almost everything at the high level, but I've been unable to get it to lower-level microcontroller projects. In the end, I'm not even sure that Lisp (of any kind) is suitable for a microcontroller. 28 | 29 | ### Why indirect threading 30 | 31 | At the start of 2023, I wrote about [what kind of threading](https://aw.github.io/fiveforths/devlog-29-what-kind-of-threads) I planned on using (direct-threading), but the implementation was ugly and buggy, so I switched to indirect-threading shortly after. The implementation is very similar to existing implementations such as [jonesforth](https://github.com/nornagon/jonesforth), so it was very easy to get it working perfectly and it's also easy to reason about. 32 | 33 | I'm aware that it may have some disadvantages on a modern RISC-V CPU architecture, but it's something that can eventually be changed if I start to notice some performance issues, and if I have time to work on it. 34 | 35 | ### Why word hashing 36 | 37 | A typical _Forth_ will have a variable word length for dictionary entries. The header that's built in memory would then store the word length and the characters of the word, and add padding to words for it to align on a boundary (ex: 4 byte boundary). A 32-character word header would end up requiring 11 CELLs of memory compared to a hash which would only require 3 CELLs for the header. 38 | 39 | The disadvantage of hashing is the number of cycles required to compute the hash. We're looking at an order of magnitude more time, but on a _Longan Nano_ running at 8 million cycles per second (8 MHz) it's still blazingly fast. 40 | 41 | Considering the relatively small memory size of the _Longan Nano Lite_ (20 KBytes), I feel like it would be a massive waste to use nearly 3x more memory by storing words the traditional way. Since it can be also be clocked up to 108 MHz, it seems much more sensible to focus on optimizing memory usage rather than optimizing the code execution path. 42 | 43 | 44 | ### Why so few primitives 45 | 46 | _FiveForths_ only has 19 built-in primitives, which is at least 150 less than a typical _Forth_. This means to get a "real" _Forth_ would require writing (or pasting) hundreds of lines of code into the terminal. Keeping with the idea of having an extremely minimal implementation, this approach seems fine for me. Not all primitives are needed for every use-case, and this allows the dictionary to be built specifically to suit ones needs. 47 | 48 | --- 49 | 50 | Now that you've read the answer to various questions, you're ready to read the other documents below: 51 | 52 | * [TUTORIALS](TUTORIALS.md): a quick guide to **get started** 53 | * [HOWTO](HOWTO.md): build, usage, and code examples in Forth and RISC-V Assembly 54 | * [REFERENCE](REFERENCE.md): learn the technical details, what's under the hood 55 | 56 | # License 57 | 58 | [MIT License](LICENSE) 59 | 60 | FiveForths documentation and source code copyright © 2021~ [Alexander Williams](https://a1w.ca) and licensed under the permissive open source [MIT](https://opensource.org/licenses/MIT) license. 61 | -------------------------------------------------------------------------------- /docs/TUTORIALS.md: -------------------------------------------------------------------------------- 1 | # FiveForths: Tutorials 2 | 3 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 4 | 5 | --- 6 | 7 | This document provides a quick guide to get started using _FiveForths_. 8 | 9 | ## Menu 10 | 11 | 1. [Requirements](#requirements) 12 | 2. [Wire the microcontroller](#wire-the-microcontroller) 13 | 3. [Get started](#get-started) 14 | 4. [Download it](#download-it) 15 | 5. [Build it](#build-it) 16 | 6. [Flash it](#flash-it) 17 | 7. [Use it](#use-it) 18 | 19 | ### Requirements 20 | 21 | * Linux (tested on Debian bullseye) with _RISC-V_ cross-compilation binaries installed 22 | * 32-bit GD32VF103 microcontroller 23 | * USB cable for flashing firmware (using `stm32loader`), or Serial/USB UART (`PA9`, `PA10`) pins 24 | * Serial/USB UART connected to JTAG (`PA13`, `PA14`, `PA15`, `PB3`) pins for debugging 25 | * Manually built `openocd` and `gdb` installed in `/opt/riscv/` for debugging 26 | 27 | ### Wire the microcontroller 28 | 29 | To wire the Serial/USB UART: 30 | 31 | | Serial/USB pins | Microcontroller pins | 32 | | :---- | :---- | 33 | | RX | PA9 (T0) | 34 | | TX | PA10 (R0) | 35 | | 3.3V | 3V3 | 36 | | GND | GND | 37 | 38 | To wire the JTAG: 39 | 40 | | Serial/USB JTAG pins | Microcontroller pins | 41 | | :---- | :---- | 42 | | RXD | JTDI | 43 | | TXD | JTCK | 44 | | RTS | JTDO | 45 | | CTS | JTMS | 46 | | 3.3V | 3V3 | 47 | | GND | GND | 48 | 49 | ### Get started 50 | 51 | It is possible to download a pre-built firmware binary, or build the firmware manually. 52 | 53 | ### Download it 54 | 55 | Download one of the firmware binaries from the [releases page](https://github.com/aw/fiveforths/releases). 56 | 57 | * [fiveforths-longan-nano-lite.bin](https://github.com/aw/fiveforths/releases/download/v0.5/fiveforths-longan-nano-lite.bin) (64K Flash, 20K RAM) 58 | * [fiveforths-longan-nano.bin](https://github.com/aw/fiveforths/releases/download/v0.5/fiveforths-longan-nano.bin) (128K Flash, 32K RAM) 59 | 60 | ### Build it 61 | 62 | The first step is to prepare the environment for building the firmware: 63 | 64 | ``` 65 | sudo apt-get install build-essential binutils-riscv64-unknown-elf gcc-riscv64-unknown-elf 66 | ``` 67 | 68 | This should install the _RISC-V_ binaries in `/usr/bin/` prefixed with: `riscv64-unknown-elf-` 69 | 70 | Next, clone this repository: 71 | 72 | ``` 73 | git clone https://github.com/aw/fiveforths.git 74 | cd fiveforths 75 | ``` 76 | 77 | Finally, build the firmware and debug files with `make`. The output should look like this: 78 | 79 | ``` 80 | $ make 81 | /usr/bin/riscv64-unknown-elf-as -g -march=rv32imac_zicsr -I src/boards/longan-nano-lite -I src/mcus/gd32vf103 -I src -o fiveforths.o fiveforths.s 82 | /usr/bin/riscv64-unknown-elf-ld -m elf32lriscv -T src/boards/longan-nano-lite/linker.ld -o fiveforths.elf fiveforths.o 83 | /usr/bin/riscv64-unknown-elf-objcopy -O binary fiveforths.elf fiveforths.bin 84 | /usr/bin/riscv64-unknown-elf-objcopy -O ihex fiveforths.elf fiveforths.hex 85 | /usr/bin/riscv64-unknown-elf-objdump -D -S fiveforths.elf > fiveforths.dump 86 | ``` 87 | 88 | Additional build options are explained in the [HOWTO](HOWTO.md) section. 89 | 90 | The firmware file is called `fiveforths.bin` and is **approximately 2.5 KBytes**. 91 | 92 | ### Flash it 93 | 94 | There are many ways to flash the firmware to the _Longan Nano_. There are a few good resources [here](https://github.com/riscv-rust/longan-nano/), [here](https://github.com/theandrew168/derzforth#program), [here](https://www.susa.net/wordpress/2019/10/longan-nano-gd32vf103/), [here](https://www.appelsiini.net/2020/programming-gd32v-longan-nano/), [here](https://sigmdel.ca/michel/ha/gd32v/longan_nano_01_en.html), and elsewhere. A reliable Serial/USB UART device is recommended, and flashing with the the python `stm32loader` tool is recommended, unless debugging with `GDB` then use `load` to flash the new firmware via the JTAG pins. 95 | 96 | #### stm32loader 97 | 98 | Install it with: 99 | 100 | ``` 101 | pip3 install stm32loader 102 | ``` 103 | 104 | Set the _Longan Nano_ into `boot mode`: 105 | 106 | ``` 107 | press BOOT, press RESET, release RESET, release BOOT 108 | ``` 109 | 110 | Flash the firmware with: 111 | 112 | ``` 113 | stm32loader -p /dev/ttyUSB0 -ewv fiveforths.bin 114 | ``` 115 | 116 | The output should look like this: 117 | 118 | ``` 119 | Activating bootloader (select UART) 120 | Bootloader version: 0x30 121 | Chip id: 0x410 (STM32F10x Medium-density) 122 | Supply -f [family] to see flash size and device UID, e.g: -f F1 123 | Extended erase (0x44), this can take ten seconds or more 124 | Write 8 chunks at address 0x8000000... 125 | Writing ████████████████████████████████ 8/8 126 | Read 8 chunks at address 0x8000000... 127 | Reading ████████████████████████████████ 8/8 128 | Verification OK 129 | ``` 130 | 131 | The device may be different from `/dev/ttyUSB0`. 132 | 133 | It may be necessary to reset the device after flashing: 134 | 135 | ``` 136 | press RESET, release RESET 137 | ``` 138 | 139 | ### Use it 140 | 141 | Connect to the microcontroller over UART using a Serial/USB device and a terminal program. 142 | 143 | #### pyserial 144 | 145 | Install it with: 146 | 147 | ``` 148 | pip3 install pyserial 149 | ``` 150 | 151 | Connect with: 152 | 153 | ``` 154 | pyserial-miniterm --eol LF /dev/ttyUSB0 115200 155 | ``` 156 | 157 | --- 158 | 159 | Now that you've completed the tutorials, you're ready to read the other documents below: 160 | 161 | * [EXPLAIN](EXPLAIN.md): learn the story behind _FiveForths_ 162 | * [HOWTO](HOWTO.md): build, usage, and code examples in Forth and RISC-V Assembly 163 | * [REFERENCE](REFERENCE.md): learn the technical details, what's under the hood 164 | 165 | # License 166 | 167 | [MIT License](LICENSE) 168 | 169 | FiveForths documentation and source code copyright © 2021~ [Alexander Williams](https://a1w.ca) and licensed under the permissive open source [MIT](https://opensource.org/licenses/MIT) license. 170 | -------------------------------------------------------------------------------- /src/05-internal-functions.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Internal functions 3 | ## 4 | 5 | # compute a hash of a word 6 | # arguments: a0 = buffer address, a1 = buffer size 7 | # returns: a0 = 32-bit hash value 8 | djb2_hash: 9 | li t0, 5381 # t0 = hash value 10 | li t1, 33 # t1 = multiplier 11 | mv t3, a1 # t3 = word length 12 | slli t3, t3, 24 # shift the length left by 24 bits 13 | djb2_hash_loop: 14 | beqz a1, djb2_hash_done 15 | lbu t2, 0(a0) # load 1 byte from a0 into t2 16 | mul t0, t0, t1 # multiply hash by 33 17 | add t0, t0, t2 # add character value to hash 18 | addi a0, a0, 1 # increase buffer address by 1 19 | addi a1, a1, -1 # decrease buffer size by 1 20 | j djb2_hash_loop # repeat 21 | djb2_hash_done: 22 | li t1, ~FLAGS_LEN # load the inverted 8-bit flags+length mask into temporary 23 | and a0, t0, t1 # clear the top eight bits (used for flags and length) 24 | or a0, a0, t3 # add the length to the final hash value 25 | ret # a0 = final hash value 26 | 27 | # obtain a word (token) from the terminal input buffer 28 | # arguments: a0 = buffer start address 29 | # returns: a0 = token start address, a1 = token size (length in bytes) 30 | token: 31 | li t1, CHAR_SPACE # initialize temporary to 'space' character 32 | mv t2, zero # initialize temporary counter to 0 33 | token_char: 34 | lbu t0, 0(a0) # read char from buffer 35 | addi a0, a0, 1 # move buffer pointer up 36 | beqz t0, token_zero # compare char with 0 37 | bgeu t1, t0, token_space # compare char with space 38 | addi t2, t2, 1 # increment the token size for each non-space byte read 39 | j token_char # loop to read the next character 40 | token_space: 41 | beqz t2, token_char # loop to read next character if token size is 0 42 | addi a0, a0, -1 # move buffer pointer down to ignore the space character 43 | sub a0, a0, t2 # store the start address in W 44 | j token_done 45 | token_zero: 46 | addi a0, a0, -1 # move buffer pointer down to ignore the 0 47 | token_done: 48 | mv a1, t2 # store the size in X 49 | ret 50 | 51 | # convert a string token to a 32-bit integer 52 | # arguments: a0 = token buffer start address, a1 = token size (length in bytes) 53 | # returns: a0 = integer value, a1 = 0 = OK, 1 or greater = ERROR 54 | number: 55 | li t1, 10 # initialize temporary to 10: number base (decimal) 56 | mv t0, zero # initialize temporary to 0: holds the final integer 57 | li t3, CHAR_MINUS # initialize temporary to minus character '-' 58 | mv t4, zero # initialize temporary to 0: sign flag of integer 59 | lbu t2, 0(a0) # load first character from W working register 60 | bne t2, t3, number_check # jump to number check if the first character is not a minus sign 61 | 62 | # first character is a minus sign, so the number will be negative 63 | li t4, 1 # number is negative, store a 1 flag in temporary 64 | addi a0, a0, 1 # increment buffer address by 1 character 65 | addi a1, a1, -1 # decrement the string length by 1 66 | beqz a1, number_error # jump to error if the number is only a minus '-' 67 | number_check: 68 | li t3, 0x00007830 # load the '0x' string into temporary 69 | lhu t2, 0(a0) # load the first 2 characters into temporary 70 | bne t2, t3, number_digit # jump to number digit loop if the first 2 characters are not '0x' 71 | 72 | # first 2 characters are '0x', so let's assume the rest are hex digits 73 | li t1, 16 # initialize temporary to 16: number base (hex) 74 | addi a0, a0, 2 # increment buffer address by 2 characters 75 | addi a1, a1, -2 # decrement the string length by 2 76 | beqz a1, number_error # jump to error if the number is only '0x' 77 | number_digit: 78 | beqz a1, number_done # if the size of the buffer is 0 then we're done 79 | mul t0, t0, t1 # multiply the number by the number base (10 or 16) 80 | lbu t2, 0(a0) # load next character into temporary 81 | addi a0, a0, 1 # increment buffer address by 1 character 82 | 83 | # convert the character to a number 84 | sltiu t3, t2, 0x30 # set the result in t3 if the character is lower than '0' 85 | bnez t3, number_done # we're done if it's not a digit! 86 | addi t2, t2, -0x30 # subtract '0' from the character 87 | sltiu t3, t2, 10 # set the result in t3 if the character is lower than 10 88 | bnez t3, number_number # the character is a number (0-9) 89 | sltiu t3, t2, 0x41-0x30 # set the result in t3 if the character is lower than 17 90 | bnez t3, number_done # we're done if it's not a letter! 91 | addi t2, t2, -7 # subtract 7 from the character to convert ascii to hex 92 | number_number: 93 | slt t3, t2, t1 # set the result in t3 if it's lower than the base (10 or 16) 94 | beqz t3, number_done # we're done if it's not a number (0-9) or (0-F) 95 | add t0, t0, t2 # add previous number to current digit 96 | addi a1, a1, -1 # decrement the string length by 1 97 | bnez a1, number_digit # loop to check the next character if the length is > 0 98 | number_done: 99 | beqz t4, number_store # don't negate the number if it's positive 100 | neg t0, t0 # negate the number using two's complement 101 | number_store: 102 | # the value in a1 will be greater than 0 if it wasn't a valid number 103 | mv a0, t0 # copy final number to W working register 104 | ret 105 | number_error: 106 | li a1, 1 # number is too large or not an integer, return 1 107 | ret 108 | 109 | # search for a hash in the dictionary 110 | # arguments: a0 = hash of the word, a1 = address of the LATEST word 111 | # returns: a0 = hash of the word, a1 = address of the word if found 112 | lookup: 113 | mv t2, a1 # copy the address of LATEST 114 | lookup_loop: 115 | beqz a1, lookup_error # error if the address is 0 (end of the dictionary) 116 | lw t0, 4(a1) # load the hash of the word from the X working register 117 | 118 | # check if the word is hidden 119 | li t1, F_HIDDEN # load the HIDDEN flag into temporary 120 | and t1, t0, t1 # read the hidden flag bit 121 | bnez t1, lookup_next # skip the word if it's hidden 122 | 123 | # remove the 3-bit flags using a mask 124 | li t1, ~FLAGS_MASK # load the inverted 3-bit flags mask into temporary 125 | and t0, t0, t1 # ignore flags when comparing the hashes 126 | beq t0, a0, lookup_done # done if the hashes match 127 | lookup_next: 128 | lw a1, 0(a1) # follow link to next word in dict 129 | j lookup_loop 130 | lookup_error: 131 | # check the STATE 132 | li t0, STATE # load the address of the STATE variable into temporary 133 | lw t0, 0(t0) # load the current state into a temporary 134 | beqz t0, err_error # if in execute mode (STATE = 0), jump to error handler to reset 135 | 136 | restorevars t2 # restore HERE and LATEST (t2) 137 | j err_error # jump to error handler 138 | lookup_done: 139 | ret 140 | -------------------------------------------------------------------------------- /docs/HOWTO.md: -------------------------------------------------------------------------------- 1 | # FiveForths: Howto 2 | 3 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 4 | 5 | --- 6 | 7 | This document provides more detailed information on build, use, and write code for this microcontroller. 8 | 9 | ## Menu 10 | 11 | 1. [Building for other boards](#building-for-other-boards) 12 | 2. [Rebuilding the firmware](#rebuilding-the-firmware) 13 | 3. [Debug with JTAG](#debug-with-jtag) 14 | 4. [Defining words (Forth)](#defining-words) 15 | 5. [Toggle an LED](#toggle-an-led) 16 | 6. [Adding primitives (Assembly)](#adding-primitives) 17 | 18 | ### Building for other boards 19 | 20 | There are currently 2 support boards: 21 | 22 | * [longan-nano-lite (default)](#build-longan-nano-lite) 23 | * [longan-nano](#build-longan-nano) 24 | 25 | #### Build longan nano lite 26 | 27 | To build the lite version of _Longan Nano_, which is limited to 64K Flash, 20K RAM, type the following: 28 | 29 | ``` 30 | make build BOARD=longan-nano-lite 31 | ``` 32 | 33 | #### Building longan nano 34 | 35 | To build the regular _Longan Nano_, which has 128K Flash, 32K RAM, type the following: 36 | 37 | ``` 38 | make build BOARD=longan-nano 39 | ``` 40 | 41 | ### Rebuilding the firmware 42 | 43 | To rebuild the firmware: 44 | 45 | ``` 46 | make build -B 47 | ``` 48 | 49 | Or first clean, then build: 50 | 51 | ``` 52 | make clean 53 | make build 54 | ``` 55 | 56 | ### Debug with JTAG 57 | 58 | JTAG debugging is necessary when modifying the firmware and to inspect memory and registers while the CPU is halted. 59 | 60 | #### Load openocd 61 | 62 | ``` 63 | make openocd & 64 | ``` 65 | 66 | If [openocd](https://openocd.org/pages/getting-openocd.html) doesn't work, it is necessary to install it to `/opt/riscv/` and it may be necessary to modify the adapter config in `ft232r.cfg` to match your adapter. 67 | 68 | #### Load GDB 69 | 70 | ``` 71 | make debug 72 | ``` 73 | 74 | Once `GDB` is loaded, the firmware can be uploaded quickly with: 75 | 76 | ``` 77 | load 78 | ``` 79 | 80 | Registers can be inspected with (for example): 81 | 82 | ``` 83 | info registers a0 a1 t0 t1 sp pc 84 | ``` 85 | 86 | or: 87 | 88 | ``` 89 | info all-registers 90 | ``` 91 | 92 | ### Defining words 93 | 94 | Accessing _FiveForths_ through the terminal should look similar to this: 95 | 96 | ``` 97 | $ pyserial-miniterm --eol LF /dev/ttyUSB0 115200 98 | --- Miniterm on /dev/ttyUSB0 115200,8,N,1 --- 99 | --- Quit: Ctrl+] | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H --- 100 | FiveForths v0.5, Copyright (c) 2021~ Alexander Williams, https://a1w.ca 101 | 102 | ``` 103 | 104 | Some basic words can then be defined (borrowed from [sectorforth hello-world](https://github.com/cesarblum/sectorforth/blob/master/examples/01-helloworld.f) and [planckforth bootstrap](https://github.com/nineties/planckforth/blob/main/bootstrap.fs)): 105 | 106 | ``` 107 | : dup sp@ @ ; 108 | : invert -1 nand ; 109 | : negate invert 1 + ; 110 | : - negate + ; 111 | : drop dup - + ; 112 | : over sp@ 4 + @ ; 113 | : swap over over sp@ 12 + ! sp@ 4 + ! ; 114 | : nip swap drop ; 115 | : 2dup over over ; 116 | : 2drop drop drop ; 117 | : and nand invert ; 118 | : or invert swap invert nand ; 119 | : = - 0= ; 120 | : <> = invert ; 121 | : , here @ ! here @ 4 + here ! ; 122 | : immediate latest @ 4 + dup @ 0x80000000 or swap ! ; 123 | : [ 0 state ! ; immediate 124 | : ] 1 state ! ; 125 | : branch rp@ @ dup @ + rp@ ! ; 126 | ``` 127 | 128 | Of course, it is possible to define many other words to suit your needs. 129 | 130 | ### Toggle an LED 131 | 132 | The following code can be used to turn on the green and blue LEDs on GPIOA pins 1 and 2: 133 | 134 | ``` 135 | : green_led_on 0x40010800 @ 0xFFFFFF0F and 0x00000030 or 0x40010800 ! ; 136 | : blue_led_on 0x40010800 @ 0xFFFFF0FF and 0x00000300 or 0x40010800 ! ; 137 | green_led_on 138 | blue_led_on 139 | ``` 140 | 141 | And to turn off the same LEDs: 142 | 143 | ``` 144 | : green_led_off 0x40010800 @ 0xFFFFFF0F and 0x00000040 or 0x40010800 ! ; 145 | : blue_led_off 0x40010800 @ 0xFFFFF0FF and 0x00000400 or 0x40010800 ! ; 146 | green_led_off 147 | blue_led_off 148 | ``` 149 | 150 | This requires the above defined words: `invert, over, swap, and, or`. 151 | 152 | To explain the values: 153 | 154 | * `0x40010800`: GPIOA base address with offset `0x00` for `CTL0` pins 0-7 (would be `CTL1` with offset `0x04` for pins 8-15). 155 | * `0xFFFFF0FF`: mask to clear GPIO pin 2 (would be the same for GPIO pin 10, while GPIO pin 1 would be `0xFFFFFF0F` and GPIO pin 8 would be `0xFFFFFFF0`). 156 | * `0x00000030`: GPIO pin 1 setting `0b0011` which is `push-pull output, max speed 50MHz`. 157 | * `0x00000040`: GPIO pin 1 setting `0b0100` which is `floating input`. 158 | * `0x00000300`: GPIO pin 2 setting `0b0011` which is `push-pull output, max speed 50MHz`. 159 | * `0x00000400`: GPIO pin 2 setting `0b0100` which is `floating input`. 160 | 161 | The code above uses those pre-calculated values to read the existing GPIOA config from a memory address (with `@`), apply a mask (with `and`), apply the new config (with `or`), then store it back to the memory address (with `!`), thus writing the new GPIOA which toggles the pins low/high (active-low, therefore low turns on the LED, high turns it off). 162 | 163 | ### Adding primitives 164 | 165 | New primitives can be written in RISC-V Assembly. It is recommended to add them to a **new file** and then include the file at _the end_ of `fiveforths.s`: 166 | 167 | ``` 168 | # fiveforths.s 169 | 170 | .include "my-primitives.s" 171 | ``` 172 | 173 | In `my-primitives.s`, each new primitive should start with a call to the `defcode` macro, and they should end with the `NEXT` macro. For example: 174 | 175 | ``` 176 | # exit ( r:addr -- ) Resume execution at address at the top of the return stack 177 | defcode "exit", 0x04967e3f, EXIT, LIT 178 | POPRSP s1 # pop RSP into IP 179 | NEXT 180 | ``` 181 | 182 | The macro arguments are: 183 | 184 | * primitive: used in the interactive **Forth** terminal. (ex: `exit`) 185 | * djb2_hash: the hash of the primitive with the first 3 bits (from the MSB) reserved for the flags (immediate, hidden, user), the next 5 bits reserved for the size (1-32). (ex: `0x04967e3f`) 186 | * name of the primitive: uppercase name used by the interpreter to find the label of the word. (ex: `EXIT`) 187 | * name of the previous primitive: uppercase name of the previous primitive this one _links_ to, used for dictionary lookups (linked list). (ex: `LIT`) 188 | 189 | Existing primitives can be used for inspiration, they are located in `src/08-forth-primitives.s`. 190 | 191 | #### Generating a djb2 hash 192 | 193 | First, compile the `djb2` hash program: 194 | 195 | ``` 196 | make djb2 197 | ``` 198 | 199 | Then run the binary with the name of the primitive, example: 200 | 201 | ``` 202 | ./djb2 exit 203 | djb2_hash: 0x04967e3f 204 | ``` 205 | 206 | The hash `0x04967e3f` will be printed, which can be used in the `defcode` declaration. 207 | 208 | --- 209 | 210 | Now that you've completed the howto, you're ready to read the other documents below: 211 | 212 | * [TUTORIALS](TUTORIALS.md): a quick guide to **get started** 213 | * [EXPLAIN](EXPLAIN.md): learn the story behind _FiveForths_ 214 | * [REFERENCE](REFERENCE.md): learn the technical details, what's under the hood 215 | 216 | # License 217 | 218 | [MIT License](LICENSE) 219 | 220 | FiveForths documentation and source code copyright © 2021~ [Alexander Williams](https://a1w.ca) and licensed under the permissive open source [MIT](https://opensource.org/licenses/MIT) license. 221 | -------------------------------------------------------------------------------- /src/09-interpreter.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Interpreter 3 | ## 4 | 5 | .balign CELL 6 | 7 | # here's where the program starts (the interpreter) 8 | interpreter_start: 9 | li t2, TIB # load TIB memory address 10 | li t3, TOIN # load the TOIN variable into unused temporary register 11 | lw a1, 0(t3) # load TOIN value into X working register 12 | add a1, a1, t2 # add TIB to TOIN to get the start address of TOIN 13 | 14 | interpreter: 15 | call uart_get # read a character from UART 16 | li t4, CHAR_NEWLINE # load newline into temporary 17 | beq a0, t4, skip_send # don't send the character if it's a newline 18 | 19 | # ignore specific characters 20 | beqz a0, interpreter # ignore the character if it matches 21 | li t4, CHAR_CARRIAGE # load 0x0D carriage return into temporary 22 | beq a0, t4, interpreter # ignore the character if it matches 23 | 24 | call uart_put # send the character to UART 25 | 26 | skip_send: 27 | # validate the character which is located in the W (a0) register 28 | li t0, CHAR_COMMENT # load comment character into temporary 29 | beq a0, t0, skip_comment # skip the comment if it matches 30 | 31 | li t0, CHAR_COMMENT_OPARENS # load opening parens into temporary 32 | beq a0, t0, skip_oparens # skip the opening parens if it matches 33 | 34 | li t0, CHAR_BACKSPACE # load backspace into temporary 35 | beq a0, t0, process_backspace # process the backspace if it matches 36 | 37 | interpreter_tib: 38 | # add the character to the TIB 39 | li t4, TIB_TOP # load TIB_TOP memory address 40 | bge a1, t4, err_tib # error if the terminal input buffer is full 41 | sb a0, 0(a1) # store the character from W register in the TIB 42 | addi a1, a1, 1 # increment TOIN value by 1 43 | li t0, CHAR_NEWLINE # load newline into temporary 44 | beq a0, t0, replace_newline # process the token if it matches 45 | 46 | j interpreter # return to the interpreter if it's not a newline 47 | 48 | skip_comment: 49 | checkchar CHAR_NEWLINE, interpreter # check if character is a newline 50 | j skip_comment # loop until it's a newline 51 | 52 | skip_oparens: 53 | checkchar CHAR_COMMENT_CPARENS, interpreter # check if character is a closing parens 54 | j skip_oparens # loop until it's a closing parens 55 | 56 | process_backspace: 57 | # erase the previous character on screen by sending a space then backspace character 58 | li a0, ' ' 59 | call uart_put 60 | li a0, '\b' 61 | call uart_put 62 | 63 | # erase a character from the terminal input buffer (TIB) if there is one 64 | beq a1, t2, interpreter # return to interpreter if TOIN == TIB 65 | addi a1, a1, -1 # decrement TOIN by 1 to erase a character 66 | 67 | j interpreter # return to the interpreter after erasing the character 68 | 69 | .balign CELL 70 | replace_newline: 71 | li a0, CHAR_SPACE # convert newline to a space 72 | sb a0, -1(a1) # replace previous newline character with space in W register 73 | 74 | process_token: 75 | # process the token 76 | li t2, TIB # load TIB memory address 77 | li t3, TOIN # load TOIN variable into unused temporary register 78 | lw a0, 0(t3) # load TOIN address value into temporary 79 | add a0, a0, t2 # add TIB to TOIN to get the start address of TOIN 80 | call token # read the token 81 | 82 | # move TOIN 83 | li t2, TIB # load TIB memory address 84 | add t0, a0, a1 # add the size of the token to TOIN 85 | sub t0, t0, t2 # subtract the address of TOIN from TIB to get the new size of TOIN 86 | sw t0, 0(t3) # move TOIN to process the next word in the TIB 87 | 88 | # bounds checks on token size 89 | beqz a1, err_ok # ok if token size is 0 90 | li t0, 32 # load max token size (2^5 = 32) in temporary 91 | bgtu a1, t0, err_token # error if token size is greater than 32 92 | 93 | # check if the token is a number 94 | mv t5, a0 # save a0 temporarily 95 | mv t6, a1 # save a1 temporarily 96 | call number # try to convert the token to an integer 97 | beqz a1, push_number # push the token to the stack or memory if it's a number 98 | mv a0, t5 # restore a0 99 | mv a1, t6 # restore a1 100 | 101 | call djb2_hash # hash the token 102 | 103 | li a1, LATEST # load LATEST variable into X working register 104 | lw a1, 0(a1) # load LATEST value into X working register 105 | call lookup # lookup the hash in the dictionary 106 | 107 | # check if the word is immediate 108 | lw t0, CELL(a1) # load the hash of the found word into temporary 109 | li t1, F_IMMEDIATE # load the IMMEDIATE flag into temporary 110 | and t0, t0, t1 # read the status of the immediate flag bit 111 | 112 | # load the STATE variable value 113 | li t1, STATE # load the address of the STATE variable into temporary 114 | lw t1, 0(t1) # load the current state into a temporary 115 | 116 | # decide if we want to execute or compile the word 117 | or t0, t0, t1 # logical OR the immediate flag and state 118 | addi t0, t0, -1 # decrement the result by 1 119 | beqz t0, compile # compile the word if the result is 0 120 | j execute # explicitly jump to the execute label 121 | 122 | .balign CELL 123 | execute: 124 | la s1, .loop # load the address of the interpreter into the IP register 125 | addi a0, a1, 2*CELL # increment the address of the found word by 8 to get the codeword address 126 | lw t0, 0(a0) # load memory address from W into temporary 127 | execute_done: 128 | jr t0 # jump to the address in temporary 129 | 130 | .loop: .word .dloop # double indirect jump to interpreter 131 | .dloop: .word process_token # indirect jump to interpreter after executing a word 132 | 133 | .balign CELL 134 | compile: 135 | addi t0, a1, 2*CELL # increment the address of the found word by 8 to get the codeword address 136 | li t1, HERE # load HERE variable into temporary 137 | lw t2, 0(t1) # load HERE value into temporary 138 | sw t0, 0(t2) # write the address of the codeword to the current definition 139 | 140 | addi t0, t2, CELL # increment HERE by 4 141 | sw t0, 0(t1) # store new HERE address 142 | compile_done: 143 | j process_token 144 | 145 | push_number: 146 | # push to stack if STATE = 0 (execute) 147 | li t1, STATE # load the address of the STATE variable into temporary 148 | lw t1, 0(t1) # load the current state into a temporary 149 | beqz t1, push_stack # if in execute mode, push the number to the stack 150 | # push to memory if STATE = 1 (compile) 151 | la t0, code_LIT # load the codeword address of LIT into temporary 152 | li t1, HERE # load HERE variable into temporary 153 | lw t2, 0(t1) # load HERE value into temporary 154 | sw t0, 0(t2) # write the codeword address of LIT to memory (HERE) 155 | sw a0, 4(t2) # write the number from W to the next memory cell (HERE+4) 156 | addi t0, t2, 2*CELL # increment HERE by 2 CELLs 157 | sw t0, 0(t1) # store new HERE address 158 | j process_token # jump back to process the next token 159 | push_stack: 160 | PUSH a0 # push the W working register to the top of the data stack 161 | j process_token # jump back to process the next token 162 | -------------------------------------------------------------------------------- /src/08-forth-primitives.s: -------------------------------------------------------------------------------- 1 | ## 2 | # Forth primitives 3 | ## 4 | 5 | .equ word_NULL, 0 6 | 7 | # reboot ( -- ) # Reboot the entire system and initialize memory 8 | defcode "reboot", 0x06266b70, REBOOT, NULL 9 | j err_reboot # jump to reboot 10 | 11 | # @ ( addr -- x ) Fetch memory at addr 12 | defcode "@", 0x0102b5e5, FETCH, REBOOT 13 | checkunderflow 0 # check for stack underflow of data stack (1 CELL) 14 | lw t0, 0(sp) # load the top of stack into temporary 15 | lw t0, 0(t0) # load the value from the temporary (addr) 16 | sw t0, 0(sp) # store the value back the top of stack (x) 17 | NEXT 18 | 19 | # ! ( x addr -- ) Store x at addr 20 | defcode "!", 0x0102b5c6, STORE, FETCH 21 | checkunderflow CELL # check for stack underflow of data stack (2 CELLs) 22 | lw t1, 0(sp) # load the DSP value (addr) into temporary 23 | lw t0, CELL(sp) # load the DSP value (x) into temporary 24 | sw t0, 0(t1) # store x into addr 25 | addi sp, sp, 2*CELL # move DSP up by 2 cells 26 | NEXT 27 | 28 | # sp@ ( -- addr ) Get current data stack pointer 29 | defcode "sp@", 0x0388aac8, DSPFETCH, STORE 30 | PUSH sp # store DSP in the top of the stack 31 | NEXT 32 | 33 | # rp@ ( -- addr ) Get current return stack pointer 34 | defcode "rp@", 0x0388a687, RSPFETCH, DSPFETCH 35 | PUSH s2 # store RSP in the top of the stack 36 | NEXT 37 | 38 | # 0= ( x -- f ) -1 if top of stack is 0, 0 otherwise 39 | defcode "0=", 0x025970b2, ZEQU, RSPFETCH 40 | checkunderflow 0 # check for stack underflow of data stack (1 CELL) 41 | lw t0, 0(sp) # load the DSP value (x) into temporary 42 | snez t0, t0 # store 0 in temporary if it's equal to 0, otherwise store 1 43 | addi t0, t0, -1 # store -1 in temporary if it's 0, otherwise store 0 44 | sw t0, 0(sp) # store value back into the top of the stack 45 | NEXT 46 | 47 | # + ( x1 x2 -- n ) Add the two values at the top of the stack 48 | defcode "+", 0x0102b5d0, ADD, ZEQU 49 | checkunderflow CELL # check for stack underflow of data stack (2 CELLs) 50 | POP t0 # pop DSP value (x1) into temporary 51 | lw t1, 0(sp) # load DSP value (x2) into temporary 52 | add t0, t0, t1 # add the two values 53 | sw t0, 0(sp) # store the value into the top of the stack 54 | NEXT 55 | 56 | # nand ( x1 x2 -- n ) Bitwise NAND the two values at the top of the stack 57 | defcode "nand", 0x049b0c66, NAND, ADD 58 | checkunderflow CELL # check for stack underflow of data stack (2 CELLs) 59 | POP t0 # pop DSP value (x1) into temporary 60 | lw t1, 0(sp) # load DSP value (x2) into temporary 61 | and t0, t0, t1 # perform bitwise AND of the two values 62 | not t0, t0 # perform bitwise NOT of the value 63 | sw t0, 0(sp) # store the value into the top of the stack 64 | NEXT 65 | 66 | # lit ( -- n ) Get the next word from IP and push it to the stack, increment IP 67 | defcode "lit", 0x03888c4e, LIT, NAND 68 | lw t1, 0(s1) # load the memory address from IP into temporary 69 | PUSH t1 # push the literal to the top of the stack 70 | addi s1, s1, CELL # increment IP by 1 CELL 71 | NEXT 72 | 73 | # exit ( r:addr -- ) Resume execution at address at the top of the return stack 74 | defcode "exit", 0x04967e3f, EXIT, LIT 75 | POPRSP s1 # pop RSP into IP 76 | NEXT 77 | 78 | ## 79 | # Forth I/O 80 | ## 81 | 82 | # key ( -- x ) Read 8-bit character from uart input 83 | defcode "key", 0x0388878e, KEY, EXIT 84 | call uart_get # read character from uart into W 85 | PUSH a0 # store character into top of data stack 86 | NEXT 87 | 88 | # emit ( x -- ) Write 8-bit character to uart output 89 | defcode "emit", 0x04964f74, EMIT, KEY 90 | checkunderflow 0 # check for stack underflow of data stack (1 CELL) 91 | li a0, CHAR_SPACE # load space character into W 92 | call uart_put # send character from W to uart 93 | POP a0 # copy top of data stack into W 94 | call uart_put # send character from W to uart 95 | NEXT 96 | 97 | ## 98 | # Forth variables 99 | ## 100 | 101 | # tib ( -- addr ) Store TIB variable address in top of data stack 102 | defcode "tib", 0x0388ae44, TIB, EMIT 103 | li t1, TIB # load variable into temporary 104 | PUSH t1 105 | NEXT 106 | 107 | # state ( -- addr ) Store STATE variable address in top of data stack 108 | defcode "state", 0x05614a06, STATE, TIB 109 | li t1, STATE # load variable into temporary 110 | PUSH t1 111 | NEXT 112 | 113 | # >in ( -- addr ) Store TOIN variable address in top of data stack 114 | defcode ">in", 0x0387c89a, TOIN, STATE 115 | li t1, TOIN # load variable into temporary 116 | PUSH t1 117 | NEXT 118 | 119 | # here ( -- addr ) Store HERE variable address in top of data stack 120 | defcode "here", 0x0497d3a9, HERE, TOIN 121 | li t1, HERE # load variable into temporary 122 | PUSH t1 123 | NEXT 124 | 125 | # latest ( -- addr ) Store LATEST variable address in top of data stack 126 | defcode "latest", 0x06e8ca72, LATEST, HERE 127 | li t1, LATEST # load variable into temporary 128 | PUSH t1 129 | NEXT 130 | 131 | ## 132 | # Forth words 133 | ## 134 | 135 | # : ( -- ) # Start the definition of a new word 136 | defcode ":", 0x0102b5df, COLON, LATEST 137 | li t2, TIB # load TIB memory address 138 | li t3, TOIN # load TOIN variable into unused temporary register 139 | lw a0, 0(t3) # load TOIN value into temporary 140 | add a0, a0, t2 # add TIB to TOIN to get the start address of TOIN 141 | call token # read the token 142 | 143 | # move TOIN 144 | li t2, TIB # load TIB memory address 145 | add t0, a0, a1 # add the size of the token to TOIN 146 | sub t0, t0, t2 # subtract the address of TOIN from TIB to get the new size of TOIN 147 | sw t0, 0(t3) # move TOIN to process the next word in the TIB 148 | 149 | # bounds checks on token size 150 | beqz a1, err_ok # ok if token size is 0 151 | li t0, 32 # load max token size (2^5 = 32) in temporary 152 | bgtu a1, t0, err_token # error if token size is greater than 32 153 | 154 | call djb2_hash # hash the token 155 | 156 | # set the HIDDEN flag in the 2nd bit from the MSB (bit 30) of the hash 157 | li t0, F_HIDDEN # load the HIDDEN flag into temporary 158 | or a0, a0, t0 # hide the word 159 | 160 | # copy the memory address of some variables to temporary registers 161 | li t0, HERE 162 | li t1, LATEST 163 | la a2, .addr # load the codeword address into Y working register 164 | 165 | # load and update memory addresses from variables 166 | lw t2, 0(t0) # load the new start address of the current word into temporary (HERE) 167 | lw t3, 0(t1) # load the address of the previous word into temporary (LATEST) 168 | 169 | # bounds check on new word memory location 170 | addi t4, t2, 3*CELL # prepare to move the HERE pointer to the end of the word 171 | li t5, PAD # load out of bounds memory address (PAD) 172 | bge t4, t5, err_mem # error if the memory address is out of bounds 173 | 174 | # update LATEST variable 175 | sw t2, 0(t1) # store the current value of HERE into the LATEST variable 176 | 177 | # build the header in memory 178 | sw t3, 0*CELL(t2) # store the address of the previous word 179 | sw a0, 1*CELL(t2) # store the hash 180 | sw a2, 2*CELL(t2) # store the codeword address 181 | 182 | # update HERE variable 183 | sw t4, 0(t0) # store the new address of HERE into the HERE variable 184 | 185 | # update STATE variable 186 | li t0, STATE # load the address of the STATE variable into temporary 187 | li t1, 1 # set the STATE variable to compile mode (1 = compile) 188 | sw t1, 0(t0) # store the current state back into the STATE variable 189 | NEXT 190 | 191 | docol: 192 | PUSHRSP s1 # push IP onto the return stack 193 | addi s1, a0, CELL # skip code field in W by adding 1 CELL, store it in IP 194 | NEXT 195 | 196 | .addr: 197 | j docol # indirect jump to interpreter after executing a word 198 | 199 | # ; ( -- ) # End the definition of a new word 200 | defcode ";", 0x8102b5e0, SEMI, COLON 201 | # unhide the word 202 | li t0, LATEST # copy the memory address of LATEST into temporary 203 | lw t0, 0(t0) # load the address value into temporary 204 | lw t1, CELL(t0) # load the hash into temporary 205 | li t2, ~F_HIDDEN # load the inverted HIDDEN flag into temporary 206 | and t1, t1, t2 # unhide the word 207 | sw t1, CELL(t0) # write the hash back to memory 208 | 209 | # store codeword into memory 210 | li t0, HERE # copy the memory address of HERE into temporary 211 | lw t2, 0(t0) # load the HERE value into temporary 212 | 213 | # bounds check on the exit memory location 214 | li t3, PAD # load out of bounds memory address (PAD) 215 | bge t2, t3, memory_error # error if the memory address is out of bounds 216 | 217 | la t1, code_EXIT # load the codeword address into temporary 218 | sw t1, 0(t2) # store the codeword address into HERE 219 | 220 | # move HERE pointer 221 | addi t2, t2, CELL # prepare to move the HERE pointer by 1 CELL 222 | sw t2, 0(t0) # store the new address of HERE into the HERE variable 223 | 224 | # update the STATE variable 225 | li t0, STATE # load the address of the STATE variable into temporary 226 | sw zero, 0(t0) # store the current state back into the STATE variable 227 | NEXT 228 | 229 | memory_error: 230 | li t2, LATEST # copy the memory address of LATEST into temporary 231 | lw t2, 0(t2) # load the address value into temporary 232 | restorevars t2 # restore HERE and LATEST (t2) 233 | j err_mem 234 | -------------------------------------------------------------------------------- /docs/REFERENCE.md: -------------------------------------------------------------------------------- 1 | # FiveForths: Reference 2 | 3 | [FiveForths](https://github.com/aw/fiveforths) is a tiny [Forth](https://www.forth.com/starting-forth/) written in hand-coded RISC-V assembly, initially designed to run on the 32-bit [Longan Nano](https://longan.sipeed.com/en/) (GD32VF103) microcontroller. 4 | 5 | --- 6 | 7 | This document provides technical details about what's under the hood of _FiveForths_. 8 | 9 | ## Menu 10 | 11 | 1. [FiveForths specification](#fiveforths-specification) 12 | 2. [Primitives list](#primitives-list) 13 | 3. [Registers list](#registers-list) 14 | 4. [Source files list](#source-files-list) 15 | 5. [Memory map](#memory-map) 16 | 6. [Word header](#word-header) 17 | 7. [Hash format](#hash-format) 18 | 8. [Other Forths](#other-forths) 19 | 20 | ### FiveForths specification 21 | 22 | Below is a list of specifications for _FiveForths_, most can be changed in the source files: 23 | 24 | * Support for 32-bit `GD32VF103` microcontrollers on the _Longan Nano_ board 25 | * CPU configured to run at `8 MHz` 26 | * UART configured for `115,200` baud rate, `8N1` 27 | * Data Stack (DSP) size: `256 Bytes` 28 | * Return Stack (RSP) size: `256 Bytes` 29 | * Terminal Input Buffer (TIB) size: `256 Bytes` 30 | * Pad Buffer (PAD) size: `256 Bytes` 31 | * Threading mode: `Indirect Threaded Code` (ITC) 32 | * Word header size: `12 Bytes` (3 CELLs) 33 | * Word name storage: `32-bit hash` (djb2) 34 | * Return character newline: `\n` 35 | * Maximum word length: `32 characters` 36 | * Stack effects comments support `( x -- x )`: **yes** 37 | * Stack and memory overflow/underflow protection: **yes** 38 | * Backslash comments support `\ comment`: **yes** 39 | * Decimal string number input support `12345` and `-202`: **yes** 40 | * Hex string number input support `0xCAFE4241` and `-0xCA`: **yes** 41 | * Multiline code definitions support: **no** 42 | * OK message: `" ok\n"` 43 | * ERROR message: `" ?\n"` 44 | 45 | ### Primitives list 46 | 47 | Below is the list of _Forth_ primitives, there are currently **19 primitives**: 48 | 49 | | Word | Stack Effects | Description | 50 | | :---- | :---- | :---- | 51 | | | 52 | | `reboot` | ( -- ) | Reboot the entire system and initialize memory | 53 | | `@` | ( addr -- x ) | Fetch memory at addr | 54 | | `!` | ( x addr -- ) | Store x at addr | 55 | | `sp@` | ( -- addr ) | Get current data stack pointer | 56 | | `rp@` | ( -- addr ) | Get current return stack pointer | 57 | | `0=` | ( x -- f ) | -1 if top of stack is 0, 0 otherwise | 58 | | `+` | ( x1 x2 -- n ) | Add the two values at the top of the stack | 59 | | `nand` | ( x1 x2 -- n ) | Bitwise NAND the two values at the top of the stack | 60 | | `lit` | ( -- n ) | Get the next word from IP and push it to the stack, increment IP | 61 | | `exit` | ( r:addr -- ) | Resume execution at address at the top of the return stack | 62 | | | 63 | | `key` | ( -- x ) | Read 8-bit character from uart input | 64 | | `emit` | ( x -- ) | Write 8-bit character to uart output | 65 | | | 66 | | `tib` | ( -- addr ) | Store `TIB` variable value in top of data stack 67 | | `state` | ( -- addr ) | Store `STATE` variable value in top of data stack 68 | | `>in` | ( -- addr ) | Store `TOIN` variable value in top of data stack 69 | | `here` | ( -- addr ) | Store `HERE` variable value in top of data stack 70 | | `latest` | ( -- addr ) | Store `LATEST` variable value in top of data stack 71 | | | 72 | | `:` | ( -- ) | Start the definition of a new word | 73 | | `;` | ( -- ) | End the definition of a new word | 74 | 75 | ### Registers list 76 | 77 | The following _Forth_ registers are assigned to _RISC-V_ registers below. The source files also use additional registers such as temporaries (`t0` to `t6`). 78 | 79 | | Forth name | RISC-V name | Description | 80 | | :----: | :----: | :---- | 81 | | DSP | sp | data stack pointer | 82 | | W | a0 | working register | 83 | | X | a1 | working register | 84 | | Y | a2 | working register | 85 | | Z | a3 | working register | 86 | | FP | s0 | frame pointer (unused for now) | 87 | | IP | s1 | instruction pointer | 88 | | RSP | s2 | return stack pointer | 89 | 90 | ### Source files list 91 | 92 | The firmware binary is built using `GNU as`, so all source files have the lowercase `.s` extension. 93 | 94 | | Filename | Description | 95 | | :---- | :---- | 96 | | [fiveforths.s](fiveforths.s) | Loads the actual source files from `src/` | 97 | | **`src/`** | 98 | | [01-variables-constants.s](src/01-variables-constants.s) | Some constants which are stored in Flash memory, but which may point to memory addresses to be used as variables | 99 | | [02-macros.s](src/02-macros.s) | Macros to avoid repeating code throughout the source files | 100 | | [03-interrupts.s](src/03-interrupts.s) | The interrupt initialization and handling routines | 101 | | [04-io-helpers.s](src/04-io-helpers.s) | Helpers to send and receive characters over the UART | 102 | | [05-internal-functions.s](src/05-internal-functions.s) | Functions called by the interpreter such as hashing and lookup functions | 103 | | [06-initialization.s](src/06-initialization.s) | Initialization routines when the board is booted or reset | 104 | | [07-error-handling.s](src/07-error-handling.s) | Error handling routines and messages to be printed | 105 | | [08-forth-primitives.s](src/08-forth-primitives.s) | The Forth primitive words | 106 | | [09-interpreter.s](src/09-interpreter.s) | The interpreter functions to process UART characters, execute and compile words | 107 | | **`src/boards//`** | 108 | | [boards.s](src/boards/longan-nano-lite/boards.s) | Variables and constant specific to the `` | 109 | | [linker.ld](src/boards/longan-nano-lite/linker.ld) | Linker script specific to the `` | 110 | | **`src/mcus//`** | 111 | | [mcu.s](src/mcus/gd32vf103/mcu.s) | Variables and constant specific to the `` | 112 | 113 | ### Memory map 114 | 115 | The stack size is defined in `mcu.s` and defaults to 256 bytes for the `Data, Return, Terminal` stacks. The `Data` and `Return` stacks grow _downward_ from the top of the memory. The `Terminal` buffer grows _upward_ from the start of the `Variables` area. The `User Dictionary` grows _upward_ from the bottom of the memory. Currently `5` Cells are used to store variables. There is also an additional `64` Cells reserved for the `Pad` area, which can grow _upward or downward_. The `Pad` area is not exposed in _Forth_ and should be used exclusively by internal code or new Assembly primitives - as an in-memory scratchpad without affecting the other stacks or user dictionary. 116 | 117 | ``` 118 | Top 119 | +-----------------+-------------------------+ 120 | | Memory Map | Size (1 Cell = 4 Bytes) | 121 | +-------------------------------------------+ 122 | | | | | 123 | | Data Stack | 64 Cells (256 Bytes) | | 124 | | | | v 125 | +-------------------------------------------+ 126 | | | | | 127 | | Return Stack | 64 Cells (256 Bytes) | | 128 | | | | v 129 | +-------------------------------------------+ 130 | | | | ^ 131 | | Terminal Buffer | 64 Cells (256 Bytes) | | 132 | | | | | 133 | +-------------------------------------------+ 134 | | | | 135 | | Variables | 5 Cells (20 Bytes) | 136 | | | | 137 | +-------------------------------------------+ 138 | | | | ^ 139 | | Pad Area | 64 Cells (256 Bytes) | | 140 | | | | v 141 | +-------------------------------------------+ 142 | | | | ^ 143 | | | | | 144 | | | | | 145 | | User Dictionary | Variable size | | 146 | | | | | 147 | | | | | 148 | | | | | 149 | +-----------------+-------------------------+ 150 | ``` 151 | 152 | ### Word header 153 | 154 | A dictionary word header contains 3 Cells (3 x 32 bits = 12 bytes). The `Link` is the value of the last defined word, which is stored in the variable `LATEST`. The `Hash` is generated by the `djb2_hash` function. And the `Codeword` is the address of the `.addr` label which jumps to the `docol` function. 155 | 156 | ``` 157 | +----------+----------+-------------+ 158 | | Link | Hash | Codeword | 159 | +----------+----------+-------------+ 160 | 32-bits 32-bits 32-bits 161 | ``` 162 | 163 | ### Hash format 164 | 165 | The hash is a 32-bit hash with the last 8 bits (from the LSB) used for the Flags (3 bits) and Length (5 bits) of the word. 166 | 167 | ``` 168 | 32-bit hash 169 | +-------+--------+------------------+ 170 | | FLAGS | LENGTH | HASH | 171 | +-------+--------+------------------+ 172 | 3-bits 5-bits 24-bits 173 | ``` 174 | 175 | ### Other Forths 176 | 177 | This document would be incomplete without listing other Forths which inspired me and are worth checking out: 178 | 179 | * [colorForth, by Chuck Moore (inventor)](https://colorforth.github.io/cf.htm) 180 | * [Mecrisp, batteries-included with FPGA support](https://mecrisp.sourceforge.net/) 181 | * [sectorforth, super tiny 16-bit implementation](https://github.com/cesarblum/sectorforth) 182 | * [jonesforth, 32-bit heavily documented](https://rwmj.wordpress.com/2010/08/07/jonesforth-git-repository/) 183 | * [derzforth, 32-bit risc-v inspiration](https://github.com/theandrew168/derzforth) 184 | * [nasmjf, the devlog idea and well documented](http://ratfactor.com/nasmjf/) 185 | * [CamelForth, by Brad Rodriguez (Moving Forth)](http://www.camelforth.com) 186 | * [muforth, the sum of all Forth knowledge](https://muforth.nimblemachines.com/) 187 | * [planckforth, how to bootstrap a Forth](https://github.com/nineties/planckforth/) 188 | 189 | Additional information can be found in the [devlogs](https://aw.github.io/fiveforths). 190 | 191 | --- 192 | 193 | Now that you've grokked the reference, you're ready to read the other documents below: 194 | 195 | * [TUTORIALS](TUTORIALS.md): a quick guide to **get started** 196 | * [EXPLAIN](EXPLAIN.md): learn the story behind _FiveForths_ 197 | * [HOWTO](HOWTO.md): build, usage, and code examples in Forth and RISC-V Assembly 198 | 199 | # License 200 | 201 | [MIT License](LICENSE) 202 | 203 | FiveForths documentation and source code copyright © 2021~ [Alexander Williams](https://a1w.ca) and licensed under the permissive open source [MIT](https://opensource.org/licenses/MIT) license. 204 | --------------------------------------------------------------------------------