Mecrisp-Stellaris Forth IDE for the Feather RP2040
Where I describe how I develop Forth on my Mac using Sublime Text 3 and Serial, as my Forth IDE.
Summary
- Edit Forth code in Sublime Text 3 using Textmate Forth bundle for code highlighting.
- Execute ST3’s Build command which transfers the code to the board for on-board compiling for Forth, typically a 2-3 second transfer (1500 bytes/sec).
- Use the macOS app, Serial for communicating with Forth on the board.
Sources
- FlashForth Shell
- Piotr’s xfr for Mecrisp-Stellaris Forth
- Jan’s upload.c for Mecrisp-Stellaris Forth
- Terry’s IDE Notes
On the Shoulders of Giants
In Terry’s comments as to a Forth IDE, he goes into great detail as to the speed of transmission, the debugging capabilities and register documentation of his prescribed IDE. I honor what he describes and it certainly is a powerful and capable IDE. That said, I’ve been searching for a way to do development using my system (Mac vs. Linux), my tools (Sublime Text vs. Vim) and my board (RP2040-based vs. STM).
The sources above were all great inspirations to my end product. I recommend you take a look at them to gain a better understanding of what is capable. In the meantime, I give my version of my Forth IDE (for now):
Editing: Sublime Text 3 (ST3)
I edit my file in ST3, using an old TextMate bundle for Forth. It does a pretty good job of highlighting Forth syntax properly. Once I’ve finished editing, I need to send the text file to the Feather RP2040.
ST3 has the ability to automate a build, whether its for a C++ program or Python. Or in my case, “build” is simply transferring the file to the board and having Forth compile it. I wrote a absurdly simple build program and saved it per ST3’s requirements, I saved it in
"~/Library/Application Support/Sublime Text 3/Packages/User/forth.sublime-build":
{
"cmd": ["/Users/lkoepsel/Documents/python/forth/upload.py", "$file"],
"selector": "source.forth",
}
Forth Transfer Examples
I reviewed upload.c, xfr.py and ff_shell.py, then created my own (albeit Frankenstein version) of an upload program. Some notes as to what I liked about each:
upload.c
Jan does a nice job of documenting the patches required to the Mecrisp-Stellaris code. He makes specific changes to the interpreter and datastackandmacro code so that it can be more efficient with the uploader. I haven’t incorporated all of his changes, I use a different acknowledgement scheme for now. He uses ACK/NAK which is a great way to do it and I’m going to look at for my code in the future.
xfr.py
While attempting to fix some things in upload.c, I ran across this note by Piotr regarding an absurdly simple program xfr.py:
#!/usr/bin/env python3
import sys
for l in open(sys.argv[1], "rt"):
sys.stdout.write(l)
r = sys.stdin.readline()
sys.stderr.write(r)
if not r.endswith("ok.\n"):
sys.exit(1)
You can invoke this using picocom:
picocom -b 115200 /dev/ttyACM0 --imap lfcrlf,crcrlf --omap delbs,crlf --send-cmd "./xfr.py"
And it works, quite well! This source on Github is his more fleshed out version. It works extremely well and was my solution for a period of time.
However, once I finally understood how to increase the RP2040 baud rate, I wasn’t able to run picocom at 921600 on my Mac. Which meant, I needed a more full-feature version, one that didn’t require a terminal program.
ff_shell.py
This version works well with a previous version of Forth, called FlashForth. I really like FF, however, Mecrisp-Stellaris Forth is significantly more advanced and supported so I’ve made the move.
This program uses PySerial to communicate directly with the board and PySerial was the missing link. Once I began to use PySerial as the basis for communication, it just seemed like the way to go.
upload.py
All of this resulted in upload.py, which is a stand-alone app which is the basis of the build function in ST3.
#!/usr/bin/env python3
import argparse
import serial
import sys
import os
import re
import datetime
import traceback
class Config(object):
def __init__(self):
self.serial_port = '/dev/cu.usbserial-A10K59P8'
self.rate = '921600'
self.hw = False
self.sw = False
self.chardelay = '0'
self.newlinedelay = '0'
self.charflowcontrol = False
def serial_open(config):
if config.cfg:
print("Port:"
+ str(config.port)
+ " Speed:"
+ str(config.rate)
+ " hw:"
+ str(config.hw)
+ " sw:"
+ str(config.sw)
+ " newlinedelay:"
+ str(config.chardelay)
+ " chardelay:"
+ str(config.chardelay)
+ " charfc:"
+ str(config.charflowcontrol))
try:
config.ser = serial.Serial(config.port, config.rate, timeout=1,
rtscts=config.hw,
xonxoff=config.sw)
except serial.SerialException as e:
print('Could not open serial port', config.serial_port, 'due to:',
os.strerror(e.errno))
# raise e
def parse_arg(config):
parser = argparse.ArgumentParser(
description="Upload app for Mecrisp-Stellaris Forth",
epilog="""Upload to board at higher speeds.""")
parser.add_argument("file", metavar="FILE", help="file to send")
parser.add_argument("--config", "-f", action="store_true",
default=False,
help="Print port configuration")
parser.add_argument("--port", "-p", action="store",
type=str, default='/dev/cu.usbserial-A10K59P8',
help="Serial port name")
parser.add_argument("--hw", action="store_true",
default=False, help="Serial port RTS/CTS enable")
parser.add_argument("--sw", action="store_true",
default=False, help="Serial port XON/XOFF enable")
parser.add_argument("--speed", "-s", action="store",
type=str, default=921600, help="Serial port speed")
parser.add_argument("--chardelay", "-d", action="store",
type=str, default=0,
help="Character delay(milliseconds)")
parser.add_argument("--newlinedelay", "-n", action="store",
type=str, default=0,
help="Newline delay(milliseconds)")
parser.add_argument("--cc", "-c", action="store_true",
default=False,
help="Character by character flow control")
parser.add_argument("-t", "--print-statistics", action="store_true",
default=True, help="print transfer statistics")
parser.add_argument("-I", "--include-path", metavar="DIR", action="append",
default=["."],
help="append directory to include paths")
arg = parser.parse_args()
config.cfg = arg.config
config.file = arg.file
config.port = arg.port
config.hw = arg.hw
config.sw = arg.sw
config.rate = arg.speed
config.chardelay = arg.chardelay
config.newlinedelay = arg.newlinedelay
config.charflowcontrol = arg.cc
config.stats = arg.print_statistics
config.incl = arg.include_path
# specific function to show hex ASCII values for hidden characters on error
def rtoASCII(r):
print(str(bytearray(r)))
return " ".join(str(hex(char)) for char in r)
def main():
config = Config()
parse_arg(config)
serial_open(config)
t0 = datetime.datetime.now()
n = xfr(None, 0, config)
elapsed_time = datetime.datetime.now() - t0
speed = int(n[0] / elapsed_time.total_seconds())
if config.stats:
print(f'*** lines read: {n[1]}')
print(f"*** elapsed time: {elapsed_time} ({speed} bytes/s) ***")
def xfr(parent_fname, parent_lineno, config):
comments = re.compile(r'^\s*\\ .*')
empty = re.compile(r'^\s*$')
lineno = 1
prevlines = 1
prev = 8
n_bytes_sent = 0
response = []
orig_file = ['Start']
try:
for line in open(config.file, "rt"):
if lineno > prev:
prevlines = lineno - prev
else:
prevlines = lineno - lineno
orig_file.append(line)
if (not comments.match(line)) and (not empty.match(line)):
config.ser.write(str.encode(line))
n_bytes_sent += len(line)
response_line = config.ser.readline()
response.append(response_line)
if not response_line.endswith(b'ok.\r\n'):
print(f'*** Compilation error, line: {lineno}')
sys.stderr.write(rtoASCII(response_line))
for line in range(prevlines, lineno + 1):
print(line, orig_file[line], end='')
sys.exit()
lineno += 1
return [n_bytes_sent, lineno]
except (OSError) as e:
if parent_fname is not None:
sys.stderr.write(f"*** {parent_fname}({parent_lineno}): {e} ***")
else:
sys.stderr.write(f"*** : {e} ***")
sys.exit(1)
try:
sys.exit(main())
except Exception as e:
traceback.print_exc()
print("sys.exit {0}".format(e))
Comments powered by Talkyard.