ESP32Forth Programming Guide
ESP32Forth Programming Guide
Installation ........................................................................................................................4
GPIO PINS......................................................................................................................15
GPIO INPUT/OUTPUT....................................................................................16
1
AUDIO ............................................................................................................................32
LEDC VOCABULARY .....................................................................................33
DAC WAVEFORMS ........................................................................................35
TIMERS ..........................................................................................................39
FINAL DAC/TIMER CODE WORDS................................................................41
2
3
Installation
The Forth install file, [Link], loads from the Arduino IDE just like any other
Program Sketch, which is amazing considering that it results in a complete Forth
programming development environment for your ESP32. The suggested ESP32
versions to use are the WROOM or the DO-IT-KIT. The installation steps are simple if
you are familiar with the Arduino IDE application.
2. There is a necessary add-on Library for the Arduino IDE that allows you to create
and run programs on the ESP32. Thanks to Random Nerd Tutorials for providing a
tutorial on how to install and use this Library. [Link]
installing-the-esp32-board-in-arduino-ide-windows-instructions/
3. Once your ESP32 is connected to your computer through a USB cable, it should
show up under the Arduino TOOLS Menu where you can select exactly what kind of
ESP32 you have. If it does not show up, check your USB cable. There are many USB
cables out there that are charge-only and are not well labeled as such.
4. Under the Tools Menu are some settings for your ESP32. Make sure they are set as
shown here:
4
6. Under the Arduino FILE Menu choose OPEN, and then find your unzipped
[Link] installation file. Finally, select the UPLOAD button for it to compile and
load. It is a large file and will take a full minute to finish.
7. If all goes well you should be ready to program your ESP32 in Forth Language from
a Serial Terminal Application or the Monitor available as an Arduino IDE Tool. On the
Mac I use "Serial" by [Link].
8. The ultimate go-to guide is "The Great Book for ESP32forth" by Marc Petremann.
It covers everything from installation to hardware control, making it a perfect aid to
driving your ESP32 board using Forth programming language. Download it from Marc's
Github page at [Link] If you are looking
for more foundational reading on the environment by the creators Dr. Ting and Brad
Nelson, you can also check out the ESP32forth Documents Page.
5
Program Development
Extending the Forth Dictionary with your own word commands is most commonly
accomplished with the following program construct:
The parenthesized item is a non-executing comment noting what values are expected
on the stack before your word command is executed and what is left on the stack after
finished executing. Stack operations are probably the most important thing to figure
out to get comfortable with the language. Note that your created words will extend the
Forth Dictionary into RAM memory space which will disappear after any reset or power
cycling. Read on to find out how to get them into the non-volatile flash memory space.
Forth Development is usually done from your main computer using a simple text editor
like TextEdit on the Apple Mac or an IDE programming environment like Visual Studio
Code or BBedit, both of which have GIT capabilities. Then use a simple Copy and
Paste to get your work into the ESP32Forth -- copy the code from any text editor on
your main computer and then paste it onto the Serial Application's Forth Terminal
Screen at the cursor.
For storing larger amounts of development code, the ESP32 has 4MB, 8MB or 16MB
of flash memory, usually on a separate chip. This flash memory is non-volatile,
meaning that the data stored there will remain even after powering down or resetting
6
the ESP32. It has limited write cycles, as low as 10,000, so treat it as semi permanent
memory space, not as a working memory space such as RAM. Since it exists on a
separate chip, the main ESP32 processor communicates with it over a serial line,
specifically SPI, making access to it much slower than the processor's on board RAM
memory. Because of this the Forth flash access words will create a buffer in RAM
memory from which to edit and load your code. When done, the buffers are then
transferred to Flash memory.
There are several methods for loading and editing Forth Programs, both new words
and immediate executable code lines, into the Flash Memory Space. All the methods
involve copying the text code from some program development app on your main
computer, and then pasting it into the Terminal Application that is talking with the
ESP32Forth micro over USB serial lines. ESP32Forth cannot directly read a program
file from your main computer, which is why you must manually transfer the program
data to the ESP32's own Flash Memory using copy/paste from your main working
computer.
Once loaded onto the Flash Memory space, the code can easily be accessed by the
ESP32forth system. Flash Memory files can be read, listed, copied, removed, edited,
and most important - compiled into the Forth dictionary and/or executed. What follows
are four methods for loading and editing Forth Programs.
The [Link] system was actually loaded and stored directly onto the ESP32's
flash memory by the Arduino IDE when you pressed UPLOAD, as it would with any
program sketch. It is programmed for two-way serial communication with a host
computer using any Terminal Application. You can create new words or execute
existing words by simply typing the code onto a line on the Terminal App. The line can
be edited by backspace erasing and will not be compiled or executed until the
"Return" key is pressed to complete the code entry.
"Serial", a Terminal program for the Mac by [Link] has a space at the
bottom of the terminal screen where you can paste or type in one line of code and edit
it. It will load the line when you hit Return. Pasting more than one line of code there
doesn't quite seem to work. Under its File Menu you can also "Send File". The
Terminal will process the file one line at a time showing you its progress in the Terminal
Window. I also found that you can simply paste any number of lines of code at the
Terminal cursor and it will process them one line at a time. Other Terminal Applications
7
may have different code entry features.
In this way you can directly and immediately create new words for the Forth Dictionary
or execute existing words; however, with this method the forth dictionary will be
extended with your new words into RAM memory space, not the Flash Memory space
which holds all the core ESP32forth system words. Those new words will disappear
when the ESP32 is rebooted or powered off. This is fine for short, quick, and simple
code explorations, but may prove cumbersome for larger projects. You may also find
that the rebooting is useful in the beginning stages of your work for erasing it all and
starting over. There is a tiny pushbutton on the ESP32 board used for rebooting the
ESP32forth system back to its starting vocabulary. Note also that a word can be
redefined in the same session and any future use of it will use the latest defined
version.
Block-based Edit and Storage has been a feature of Forth Programming Systems since
its beginning. It was originally used to access storage space on hard and floppy disk
drives. Our "floppy drive" here is the Flash Memory Chip incorporated on the ESP32
microprocessor board.
A Block to be edited is first moved from Flash Memory to a RAM Buffer. Editing a block
is accomplished within the RAM buffers, not the Flash Memory space, one 64
character line at a time. Here are the word commands available with the Block Editor:
editor opens the vocabulary set of words for the block editor
n list lists the contents of block n, and makes n the current block
l lists the contents of the current block, along with the Block number
n selects the next block
p selects the previous block
8
na inserts the text that follows at line n, moves down all lines below n
nr replaces contents of line n with text that follows, terminated with <cr>
flush Save all buffer blocks back to Flash Memory and then empty the buffers
save-buffers Save all buffer blocks back to Flash Memory
As you can see here, the block contents are filled one line at a time. Your work might
entail creating a forth code work file on a text editor from your main computer and then
copying the code three lines at a time from your text editor work file. Here is an
example:
Copy these three lines from your work file, and then paste them to the block editor
running from the Terminal Application. Note that the "n r" editor replace command is
included at the start of each line. The block "editor" will re-list the block contents after
each line entry. Continue this for all 16 lines in the block. When finished editing a
block be sure to use the "flush" command to save your work, moving it from RAM
Buffers back to Flash Storage.
The "n load" command will compile and/or execute all the code from Block number
"n". One special Block could be filled with "n m thru" and "n load" commands along
with progress report print commands to serve as the main source code loading block.
You will find that the speed of execution is almost instantaneous.
The source code stored in these Blocks will be available for all future ESP32Forth
sessions even after power down and reboot cycles.
It was inevitable that the ancient Block Disk Access, described above, would be
replaced by something less constricting. When modern Forth implementations
showed up running under host systems such as Windows, Linux, or MacOS, a Forth
Vocabulary was developed to enable access to files living on those host systems.
9
Forth Data files are created and managed on the host computer. They are simple text
files of ASCII characters and, unlike the Block Disks, can be of any size. The filenames
use character strings and can include system path information using the forward slash
character ( / ) to designate a folder structure.
However, there is a problem. The ESP32Forth system lives on and operates from the
ESP32 micro board, not a host computer system. It can only communicate with a host
computer through a Serial USB cable and a Terminal Application program on the host
computer. This means that the ESP32Forth system does not have direct access to
files on a host operating system using the File Based Disk Access word vocabulary.
There is a way around this problem. Check out this short word definition developed by
Bob Edwards that draws from the File Access word vocabulary:
CREATE-FILE Creates a file in ESP32Forth with the pathname designated after the
RECORDFILE word command and gives it a file ID.
10
WRITE-FILE Will write a designated number of characters into the file referenced by
file ID. It will update FILE-POSITION and FILE-SIZE, two variables associated with the
file ID. The characters are picked up from a given address in ESP32 RAM memory
space.
These three word definitions were created to handle Forth's File based disk access.
One more word definition will help explain what is happening here in RECORDFILE.
tib Will return the address of the terminal input buffer where the serial input text
stream is being held. This sets up the location where WRITE-FILE will pick up its
ASCII characters.
RECORDFILE /spiffs/[Link]
/ my silly code test
: print44 44 . ;
print44
<EOF>
When this code is entered onto the Terminal Application window a new file called
[Link] (fs stands for forth source) is created on the ESP32Forth system with the
content shown. The three lines of code here can be pasted from a text work file on the
host computer. The pasted code can be of any length. Note the <EOF> end of file
marker at the end of the code. This must be included and must be capitalized.
We could also create a master loading file that compiles and/or executes the code from
several forth source files stored in flash memory as shown here.
RECORDFILE /spiffs/[Link]
11
<EOF>
include /spiffs/[Link]
As you can see, with this file access method we are still stuck with the same copy/
paste to the Terminal App window, but we are left with a much more convenient file
based storage. RECORDFILE is a good method for creating and loading Forth data
files onto the ESP32Forth Flash Memory. What follows are words defined to manage
our newly created files in the ESP32 Flash memory.
Flash memory is on a separate chip on the ESP32 board. The ESP32Forth system
uses a special file management system to read, write, and delete files on the flash
memory chip over the serial protocol SPI. It is called SPIFFS (Serial Peripheral
Interface Flash File System).
There are several ESP32forth words designed to handle Flash memory files over
SPIFFS:
To see a list of files, including block files, stored in SPIFFS use the word ls :
ls /spiffs/ \ to list all the files
ls /spiffs/dir1 \ to list only those files in the subdirectory dir1
12
To insert string contents into a file, use the word dump-file :
r| : mysillydef 2 dup dup ; |
s" /spiffs/[Link]" dump-file
~~~~~~~~~~~~~~~~~~~~~~~~~~~
r| ***** | ( string| -- a n )
Creates a temporary counted string ending with |
s" **** " ( -- addr cnt )
creates a string until a terminating ". Leaves the
string address addr and the character count cnt on the stack
dump-file ( a n a n -- )
Use visual edit to create and edit source files in the SPIFFS Flash file system.
If [Link] exists it will come up in the editor. If it doesn't exist, it is created. The entire
text file will be listed in the Terminal screen. A blinking "|" text insertion cursor will be
available within the body of the text. This text cursor can be moved up, down, left, and
right using the arrow keys on your keyboard. Delete/Backspace can be used to erase
characters. Code is entered and edited by typing it in. Use Return at the end of lines.
Text Copy/Paste is also supported.
A block cursor will also appear below the text file. Make sure that it is not sitting on
top of any of the text. If it is, type a Return at the end of the last text line. This block
cursor is where the "control" exit commands are displayed. To save the file being
edited type CTRL-S. To exit the editor type CTRL-X followed by a Y to save all changes
or an N to exit without file changes.
[Link] is a special system forth source file whose contents are automatically
loaded at startup. After the visual edit line shown above, type in or paste the code for
the RECORDFILE definition displayed previously, including the crlf word. Then press
CTRL-S to save it and CTRL-X and Y to exit visual. Use ls, cat and include to test that
13
[Link] is correct and working.
Relaunch ESP32Forth. If all went well, RECORDFILE is now available at the top of the
WORD stack whenever ESP32Forth restarts. You are welcome to add other word
commands into the [Link] file.
14
GPIO PINS
Access to the ESP32 GPIO Pins using ESP32Forth programming should look familiar
to all who have experience programming the ESP32 from the Arduino IDE. The
commands are very similar. Check the website [Link]
pinout-diagram for your ESP32 version and a pinout showing the functions each pin is
capable of serving, and the few pins that should be left alone.
With Forth there is also the possibility of directly accessing the ESP32 registers that
control the GPIO pins by using bit masks and binary bit manipulations with the words
m@ and m!. This is useful when the programming calls for fast and complex Input/
Output manipulations. See Marc PETREMANN's (petremann@[Link]) pdf
book for a good description and examples of this.
Here, though, I would like to make GPIO access as simple and clear as possible. What
follows is a simple LED blink program example in Forth Programming Language. Four
word definitions are created and the forth one, the "blink" definition, which
incorporates the previous three, is executed.
How it Works:
If there is an LED onboard the ESP32 it is usually connected to pin 2. If you connect
your own LED, the anode side ( the longer wire lead ) is connected to a GPIO pin and
the cathode side ( the lead nearest a straight edge on the casing ) is connected to a
resistor of about 200 ohms to Ground, which determines how bright it lights up. No
harm if you connect it backwards, it just won't light. Turn it around.
HIGH 2 pin: Sets pin 2 to HIGH (3.3V) to turn the LED on.
LOW 2 pin: Sets pin 2 to LOW (0V, Ground) to turn the LED off.
15
500 ms: Pauses execution for 500 milliseconds (half a second).
begin ... key? until: Creates an infinite loop that blinks the LED repeatedly until you
press any key on your keyboard to jump out of the loop, which stops it.
Various triggers:
#GPIO_INTR_DISABLE
#GPIO_INTR_POSEDGE
#GPIO_INTR_NEGEDGE
#GPIO_INTR_ANYEDGE
#GPIO_INTR_LOW_LEVEL
#GPIO_INTR_HIGH_LEVEL
16
gpio_wakeup_disable ( pin -- 0/err )
gpio_pullup_en ( pin -- 0/err )
gpio_pullup_dis ( pin -- 0/err )
gpio_pulldown_en ( pin -- 0/err )
gpio_pulldown_dis ( pin -- 0/err )
gpio_hold_en ( pin -- 0/err )
gpio_hold_dis ( pin -- 0/err )
gpio_deep_sleep_hold_en ( -- )
gpio_deep_sleep_hold_dis ( -- )
gpio_install_isr_service ( a -- ) Typically ESP_INTR_FLAG_DEFAULT
gpio_uninstall_isr_service
gpio_isr_handler_add ( pin xt arg -- 0/err )
gpio_isr_handler_remove ( pin -- 0/err )
gpio_set_drive_capability ( pin cap -- 0/err )
gpio_get_drive_capability ( pin cap* -- 0/err )
esp_intr_alloc ( source flags xt args handle* -- 0/err )
esp_intr_free ( handle -- 0/err )
17
Sample GPIO Programs
What follows are heavily commented short program examples for simple GPIO sensor
circuits. (The pin connections are based on my ESP32 4-voice sound synthesizer
project described at [Link] )
The figure below illustrates four sensor circuits. The small circle in each circuit is the
connection point to an ESP32 GPIO pin. Most GPIO pins can be connected to any of
these circuits, especially those pins labelled as ADC capable. Note the 470 ohm
resistor in the potentiometer and switch circuits. This small resistance acts as short
circuit (to ground or 3.3v) protection if the pin is mistakenly setup as an output instead
of input.
18
LED LIGHTS
The 470 ohm resistor in series with the LED controls how much current goes through
the LED and therefore affects how bright the LED shines. It can be set as low as about
100 ohms for a very bright light. The color of the LED will also affect the brightness. A
blue LED, for example, will require a lower resistor. Try out different resistances to get
the result you want. Be careful not to connect a live GPIO pin directly to a voltage or
Ground which can blow the ESP32 chip!
When a GPIO pin, set up as a digital output, is high at 3.3 volts, the LED will light.
When it is a low of ground (0 volts) there is no voltage difference, therefore no current
and the LED will be dark.
If the GPIO pin is programmed to oscillate high and low at a fast frequency the LED will
seem dimmer because it is only "on" half of the time. With PWM, pulse width
modulation, you can control the LED brightness with the oscillation's duty cycle, the
percentage of each cycle that is high versus low. The ledc vocabulary in ESP32Forth
has words designed to work with PWM waveforms controlling LEDs.
The LED circuit can be connected in several ways. The resistor could be inserted into
the bottom ground connection instead of the top pin connection as shown. The circuit
ground connection could instead be a 3.3v connection, in which case a Low voltage on
the GPIO pin will turn on the LED. In order to be lit the LED circuit arrow must point in
the direction of high to low voltage. If it is connected backwards, no harm is done, it
just won't light up.
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ AY Synth Board, routines for using the LED pins
\ John Talbert July 2026
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
19
\ pins used for LEDs
5 CONSTANT LED1
15 CONSTANT LED2
2 CONSTANT LED3 \ the Blue LED on ESP32S boards
: led-blink BEGIN
LED1 led-on LED2 led-off LED3 led-on timeOn @ MS
LED1 led-off LED2 led-on LED3 led-off timeOff @ MS
KEY? UNTIL
;
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ LED fade using Pulse Width Modulation provided in the LEDC vocabulary.
ledc \ open ledc vocabulary. Using version 3 -- Pin based instead of channels.
The above code sets LED2 to oscillate at 100Hz. The PWM duty cycle has an 8-bit
resolution meaning that it can be set with values between 0 and 255. The duty cycle
is then looped between 0 and 255, fading the LED in slowly over and over.
20
POTENTIOMETER CONTROLS
The top left circuit connection diagram in the circuit figure is for a Rotary or Slide Pot.
The moving wiper of the pot is designated by an arrow and it can move between 0
volts of Ground and 3.3 volts at the other end. The ESP32 ADC (Analog to Digital
Converter) converters have a 12-bit range of 0 to 4095 corresponding with voltages
from 0 volts (Ground) to 3.3 volts (the ESP32 power supply). The pot wiper is
connected to an ADC capable GPIO pin after a small protection resistor. The word
analogRead ( pin# -- n result) is used to read the pot's wiper voltage.
The pot resistance (shown as 10k) can be anything above around 5k, but it must have a
Linear Taper, not Log, for a better response.
The lower right circuit connection diagram in the circuit figure is used for any variable
resistance sensor. The arrow over the resistor indicates a resistance that varies
between some low value and high value. This fits the description of a number of
sensors - flex sensor, pressure sensor, light sensor, moisture sensor. It can even be as
simple as two #2 pencil smudges on paper pressed together.
The 100k resistor in series with the variable resistor can be tweaked in value to obtain
the greatest range of voltage at the center connection point. An analogRead will result
in some minimum value above 0 and some maximum value below 4095, all depending
on the minimum and maximum resistance values of the sensor and that fixed resistor.
You can mathematically find the optimum value for the fixed resistor. Measure the
sensor's MAX and MIN resistance values. The highest voltage you can get from the
circuit is
Subtrack the above highest equation from the lowest, graph that equation on a
scientific calculator and read the value of R at its peak. Most likely it will be near 100k.
The following Forth code can be used for any variable resistance sensor.
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ AY Synth Board, routines for using Pot Controllers
\ or any analog sensor such as light sensors, flex sensors, pressure sensors
21
\ John Talbert July 2026
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
36 CONSTANT POT1
39 CONSTANT POT2
34 CONSTANT POT3
35 CONSTANT POT4
POT1 INPUT pinMode POT2 INPUT pinMode POT3 INPUT pinMode POT4 INPUT pinMode
VARIABLE pot_value1
VARIABLE pot_value2
VARIABLE pot_value3
VARIABLE pot_value4
: potPrint ( -- ) \ print out all 4 pot values until any key is pressed
BEGIN
allPotRead!
CR pot_value1 @ . 3 SPACES
pot_value2 @ . 3 SPACES
pot_value3 @ . 3 SPACES
pot_value4 @ . 3 SPACES
KEY? UNTIL
;
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
22
SWITCH CONTROLS
The top right circuit in the circuit figure shows the circuit hookup for a simple SPST
switch, either toggle or pushbutton. It shows the bottom of the switch connected to
Ground and the top pin connection with a "pullup" resistor to 3.3 volts. There is also a
small 470 ohm resistor that protects the pin from being directly shorted to Ground in
case the pin is set up as an output by mistake.
There are two possibilities for the pullup resistor. It can be an actual physical resistor
of around 100k connected between the pin and 3.3 volt power, or it can be provided by
the ESP32 as an "internal pullup". Enabling or disabling an internal pullup for a GPIO
pin involves a couple words found in the "interrupts" vocabulary. See the forth code
below for how to implement an internal pullup in code.
Other types of digital switches can use the same digitalRead word, sensors like the PIR
Infrared Motion detector, a magnetic contact Reed Switch, photoelectric light beam
sensor, proximity switch, Tilt switch, Foot Pedals. Some of these require you to hook
up a circuit as shown in the figure with a pullup resistor. Others will include the circuitry
for a digital output and may just need to be powered with Ground and 3.3v
connections.
As an interesting side note, the ESP32/Arduino platform has the capability of setting up
some ESP32 pins as Capacitive touch switches using the C function touchRead( ).
However, at the present time, the touchRead command is not available on the
ESP32forth platform. A way around this is to use the TTP223 sensor, an inexpensive
sensor device that converts capacitive touch to a simple digital output, requiring only
power connections.
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ AY Synth Board, routines for using Switches
\ John Talbert July 2026
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
33 CONSTANT SWITCH1
32 CONSTANT SWITCH2
VARIABLE switch_value1
VARIABLE switch_value2
23
\ gpio words in the interrupts vocabulary can enable or disable pin pullups
\ gpio_pullup_en ( pin -- 0/err ), gpio_pullup_dis ( pin -- 0/err )
: switchPrint ( == ) \ print the two switch values until any key is pressed
BEGIN
SWITCH1 digitalRead switch_value1 !
SWITCH2 digitalRead switch_value2 !
CR
switch_value1 @ . 3 SPACES switch_value2 @ .
KEY? UNTIL
;\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
24
MIDI Input/Output
The ESP32 WROOM has three Hardware UARTs for serial communications. One of
them is used for the ESP32's USB connection ( simply called UART with word
commands labeled as [Link]). That leaves UART1 and UART2. MIDI I/O on the
ESP32 normally uses UART2 with Forth word commands labeled as [Link] in the
"serial" vocabulary. GPIO pin 17 is usually the MIDI TX or transmit output and pin 16 is
the MIDI RX or Receive Input. (Note that this won't work on the ESP32 WROVER.)
A small amount of circuitry is required to connect the TX and RX pins to two 5-pin DIN
sockets, one for MIDI IN and one for MIDI OUT. As you can see, TX is very easy,
needing just one 220 ohm resistor from the 3.3 volt board power and a direct TX pin
connection to the DIN MIDI OUT plug (you may want to include a small 47 ohm
protection resistor). The RX MIDI INPUT side is a bit more involved required an opto-
isolator chip.
Modern MIDI I/O has moved on to a USB type interface. The old 5-pin DIN plugs are
rarely used now but are useful in cases like this where the ESP32 board has only one
USB connection available. Inexpensive cable converters are readily available from
Amazon for $20 that convert the two MIDI DIN I/O to USB.
25
ESP32forth includes the following set of word commands in its "serial" vocabulary for
the Serial UART interface.
These words are for the USB connection on the ESP32. The same set is available for
the Serial UART2, labeled as Serial2. Basically, the serial interface is initialized with
[Link], MIDI bytes are transmitted using [Link], and MIDI bytes are
received using [Link] and [Link].
Thank you Claude AI from Anthropic for suggesting a solution to this problem. It
consists of changing one line in the [Link] install file. This can be done from
any text editor or even from the Arduino IDE before you reload it.
26
XV(serial, "[Link]", SERIAL2_BEGIN, [Link](tos); DROP) \
replace it with:
XV(serial, "[Link]", SERIAL2_BEGIN, [Link](n3, n2, n1, n0); DROPn(4)) \
Reload the edited installation file from Arduino IDE. Your code can then use the full
[Link] command to initialize all the serial parameters:
MIDI OUTPUT
MIDI code is transmitted one byte (8-bits) at a time along with one start bit, one stop
bit, and no parity (Serial_8N1). There are two main types of MIDI byte codes, Status
and Data bytes. The STATUS byte has a 1 in the most significant bit and designates the
type of command such as NoteOn, NoteOff, ControlChange, ProgramChange in the
higher 4 bits of the byte. The Status byte also has the MIDI Channel number (0 to 15)
in the lower 4 bits. The DATA bytes all have a 0 in the most significant bit, constricting
the data to 7 bits, with a range of 0 to 127. Each specific type of Status byte is
followed by one or two data bytes. For example, a NoteOn Status byte is always
followed by a velocity (loudness) data byte and a Note Number data byte.
With this information you can more easily follow along with the code below designed
for MIDI Output:
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ MIDI INPUT/OUTPUT Interface for ESP32Forth
\ by John Talbert July 2026
\
\ On an ESP32 WROOM MIDI can be set up on Serial2
\ RX2 and TX2 (pins 16 and 17). Does not work on ESP32 WROVER
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27
$C00001C constant SERIAL_8N1 \ 8 bits, no parity, 1 stop bit
midiSetup
\ All MIDI values except status operation codes, must be in the range of 0 to 127
\ If needed to insure this do a "$7F AND" on MIDI values before [Link]
: sendNoteOff ( note -- )
$90 chan @ OR [Link] \ NoteOn + channel
[Link] \ a saved note that is on
28
0 [Link] \ velocity of zero to turn off note
;
: sendProgramChange ( Program -- )
$C0 chan @ OR [Link] \ Program Change + channel
[Link] \ Program change number
;
MIDI INPUT
Serial2 has an input buffer area of 64 bytes to receive incoming MIDI data bytes.
Generally, the first step in your MIDI Input code is to check this buffer to see if any data
has been received using the command [Link] ( -- f ). The resulting "f" value
from this Serial2 command reveals how many bytes have been received in the serial
input buffer. In our code we are only interested in whether or not it is non zero since
we will be processing only one byte at a time.
The next step is to parse the byte just read and stored by [Link].
Command words can be built that are triggered by a specific MIDI status byte and then
processed using its MIDI data bytes. In the example below we look only for MIDI
NoteOn status bytes in order to print out the note number and velocity whenever one is
received - a sort of MIDI Note Monitor. Anything not associated with MIDI NoteOn is
ignored. Another example might be to build a chord out of the incoming NoteOn's by
transmitting two more NoteOn's for each incoming NoteOn. Parsing for any number of
the other incoming Status bytes could easily be done using a forth CASE structure,
29
each specific status byte having its own special triggered word command.
\ Be sure to first run [Link] (midisetup) and open the serial vocabulary
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
VARIABLE byteIN
VARIABLE note#
VARIABLE velocity
: [Link] ( -- byteIN@)
\ check for one byte of input and send it to [Link]
[Link]
IF byteIN 1 [Link] DROP
byteIN C@ [Link]
THEN
;
\ If MIDI NoteOn is received then get note# and velocity to run HandleNoteOn
\ [Link] will wait and time out at 1 sec if no data comes in.
: [Link] ( byteIN@ -- )
$F0 AND $90 =
IF \ Is it a NoteOn MIDI Command?
30
note# 1 [Link] DROP \ load note#
velocity 1 [Link] DROP \ load velocity
\ Continuously watch for MIDI Input. Run [Link] when something shows up
: [Link] ( -- )
." Begin MIDI Input Watch"
BEGIN
[Link]
KEY? \ stop on key press, not MIDI In activity
UNTIL
CR ." End MIDI Input Watch"
;
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are wanting to run other code, besides just this MIDI Input polling, Forth does
have multitasking capabilities in which case you may want a YIELD inside the
[Link] command to allow other tasks a chance to run.
To emphasize the wide possibilities of this MIDI Input code, any number of parsing
triggers can be set up in a CASE structure. Consider coding triggers on different MIDI
Notes, setting up your MIDI keyboard as a kind of command center. The resulting
"Handle" word commands can perform any tasks you can imagine, though you will
want to make them fast and efficient so as not to slow down the polling.
31
AUDIO
Audio signals, between about 30Hz and 10KHz, can easily be produced on most any
GPIO pin. Set the pin as an output with pinMode and then oscillate it between high
and low digital output values using time delays to set the frequency.
This GPIO pin signal cannot directly drive a speaker cone. That would require an audio
amplifier, which can be as simple as a transistor circuit. However, it can easily be wired
to an audio connector along with Ground, and then connected to an audio amplifier
input or any audio device with a "line" level input connection and at least a
headphones output. It is good practice to add a 1k Ohm resistor in series with the pin
output for ESP32 protection.
32
LEDC VOCABULARY
In spite of its name association with LEDs, LEDC is the ideal vocabulary for producing
audio squarewave and pulsewaves on a GPIO pin. Best of all, it has no CPU cost
since it uses ESP32 internal hardware timers to create the signal.
This command is used to initialize a pin for use with the ledc vocabulary. It associates
the pin with a separate channel and a hardware timer. It must be followed by
ledcWrite to set a dutycycle before it will sound.
This is the main command for changing the duty cycle of the waveform. It will not
affect the frequency, but will add more harmonics to the audio signal as the pulse width
is narrowed. With a bit resolution of 8, set in ledcAttach, the pulse width can be varied
with duty cycle values between 0 and 255. A value of 127 will produce a squarewave
with 50% pulse width.
ledcWriteTone ( pin# freq -- freq) \ Will set dutycycle to 50% --> squarewave
ledcWriteTone is the main command for changing the frequency of pin's audio signal.
However, it will automatically set the duty cycle to 50% creating a squarewave. Use
ledcChangeFrequency if you want to change the frequency without affecting the duty
cycle. The frequency value is an integer given in Hertz, or cycles per second. Audio
signals range from about 30Hz to 10,000Hz. To silence a signal use a frequency of
zero.
33
ledcChangeFrequency ( pin# freq res -- flag)
For musical note applications this command allows you specify frequency as the notes
on an "equal-tempered" musical keyboard, like a piano. "Note" specifies one of twelve
notes in an octave -- C, C#, D, D#, E, F, F#, G, G#, A, A#, B -- with numbers 0 to 11.
"Octave" specifies the keyboard octave with 0 being the lowest and 10 the highest.
To give you a better idea of the note frequencies on a musical keyboard here is a Forth
byte array of MIDI NoteON frequency values from 0 to 127 that you can use instead
with ledcWriteTone.
ROM-TABLE MIDI_Freq \ create the Table MIDI_Freq, 128 NoteOn key frequencies
\ C C# D D# E F F# G G# A A# B
8 , 9 , 9 , 10 , 10 , 11 , 12 , 12 , 13 , 14 , 15 , 15 ,
16 , 17 , 18 , 19 , 21 , 22 , 23 , 24 , 26 , 28 , 29 , 31 , \ C0
33 , 35 , 37 , 39 , 41 , 44 , 46 , 49 , 52 , 55 , 58 , 62 , \ C1
65 , 69 , 73 , 78 , 82 , 87 , 92 , 98 , 104 , 110 , 117 , 123 , \ C2
131 , 139 , 147 , 156 , 165 , 175 , 185 , 196 , 208 , 220 , 233 , 247 , \ C3
262 , 277 , 294 , 311 , 330 , 349 , 370 , 392 , 415 , 440 , 466 , 494 , \ C4
523 , 554 , 587 , 622 , 659 , 698 , 740 , 784 , 831 , 880 , 932 , 988 , \ C5
1047 , 1109 , 1175 , 1245 , 1319 , 1397 , 1480 , 1568 , 1661 , 1760 , 1865 , 1976 , \ C6
2093 , 2217 , 2349 , 2489 , 2637 , 2794 , 2960 , 3136 , 3322 , 3520 , 3729 , 3951 , \ C7
4186 , 4435 , 4699 , 4978 , 5274 , 5588 , 5920 , 6272 , 6645 , 7040 , 7459 , 7902 , \ C8
8372 , 8870 , 9397 , 9956 , 10548 , 11175 , 11840 , 12544 , \ C9
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
34
These are the words found inside the version 3 ledc vocabulary. Note that this
updated ledc vocabulary is more pin oriented, compared with version 2 which talks
more of "channels". Channels are a way of grouping pins together so that a change to
one will affect all in the group.
Producing Audio signals from the ESP32 is easy if you stick to digital signals which are
either high or low. ESP32forth makes it super convenient with the ledc vocabulary.
With these commands a squarewave or pulse wave of any pitch can be produced on
most of the ESP32 pins. You can even get a pitched noise waveform by sending
random duty cycle values to the waveform. The main limitation here is that ledc only
produces these digital, low or high value waveforms.
DAC WAVEFORMS
To get beyond the rather harsh sounding squarewaves you can employ DACs, digital to
analog converters. The ESP32 provides two pins, 25 and 26, that can act as 8-bit
DACs using the command dacWrite ( pin, value --- ). Here, "value" is loaded into one
of the two available DAC pins and has a range of 0 to 255 (8-bits).
A zero written to the DAC pin 25 with "25 0 dacWrite" will produce 0 volts on pin 25.
255 written to DAC pin 25 with "25 255 dacWrite" will produce 3.3v on pin 25. So far,
this does nothing more interesting than digitalWrite ( pin, value -- ) with the value set as
HIGH or LOW, which can also be used on the DAC pins 25 and 26. However, the
command 26 127 dacWrite will produce a voltage of about 1.7 volts on pin 26, which is
neither high or low, but right in the middle. Using dacWrite results in 256 possible
voltages ranging from zero to 3.3 volts as the input value ranges from 0 to 255.
By loading dacWrite input values over time at a fast audio sample rate you can
produce any sound from the DAC output pins, which is explored in the following forth
code:
35
\ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
\ ESP32Forth Code to demonstrate dacWrite and Timers
\ John Talbert July 2026
CREATE Sinewave
127 C, 130 C, 133 C, 136 C, 139 C, 143 C, 146 C, 149 C, 152 C, 155 C,
158 C, 161 C, 164 C, 167 C, 170 C, 173 C, 176 C, 179 C, 182 C, 184 C,
187 C, 190 C, 193 C, 195 C, 198 C, 200 C, 203 C, 205 C, 208 C, 210 C,
213 C, 215 C, 217 C, 219 C, 221 C, 224 C, 226 C, 228 C, 229 C, 231 C,
233 C, 235 C, 236 C, 238 C, 239 C, 241 C, 242 C, 244 C, 245 C, 246 C,
247 C, 248 C, 249 C, 250 C, 251 C, 251 C, 252 C, 253 C, 253 C, 254 C,
254 C, 254 C, 254 C, 254 C, 255 C, 254 C, 254 C, 254 C, 254 C, 254 C,
253 C, 253 C, 252 C, 251 C, 251 C, 250 C, 249 C, 248 C, 247 C, 246 C,
245 C, 244 C, 242 C, 241 C, 239 C, 238 C, 236 C, 235 C, 233 C, 231 C,
229 C, 228 C, 226 C, 224 C, 221 C, 219 C, 217 C, 215 C, 213 C, 210 C,
208 C, 205 C, 203 C, 200 C, 198 C, 195 C, 193 C, 190 C, 187 C, 184 C,
182 C, 179 C, 176 C, 173 C, 170 C, 167 C, 164 C, 161 C, 158 C, 155 C,
152 C, 149 C, 146 C, 143 C, 139 C, 136 C, 133 C, 130 C, 127 C, 124 C,
121 C, 118 C, 115 C, 111 C, 108 C, 105 C, 102 C, 99 C, 96 C, 93 C,
90 C, 87 C, 84 C, 81 C, 78 C, 75 C, 72 C, 70 C, 67 C, 64 C, 61 C,
59 C, 56 C, 54 C, 51 C, 49 C, 46 C, 44 C, 41 C, 39 C, 37 C, 35 C,
33 C, 30 C, 28 C, 26 C, 25 C, 23 C, 21 C, 19 C, 18 C, 16 C, 15 C,
13 C, 12 C, 10 C, 9 C, 8 C, 7 C, 6 C, 5 C, 4 C, 3 C, 3 C, 2 C, 1 C,
1 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 0 C, 1 C,
1 C, 2 C, 3 C, 3 C, 4 C, 5 C, 6 C, 7 C, 8 C, 9 C, 10 C, 12 C, 13 C,
15 C, 16 C, 18 C, 19 C, 21 C, 23 C, 25 C, 26 C, 28 C, 30 C, 33 C,
35 C, 37 C, 39 C, 41 C, 44 C, 46 C, 49 C, 51 C, 54 C, 56 C, 59 C,
61 C, 64 C, 67 C, 70 C, 72 C, 75 C, 78 C, 81 C, 84 C, 87 C, 90 C,
93 C, 96 C, 99 C, 102 C, 105 C, 108 C, 111 C, 115 C, 118 C, 121 C,
124 C,
36
\ Three Waveform Generators, sends one indexed wave sample to DAC
: sine ( -- )
index_increment
DAC0 Sinewave INDEX @ + C@ dacWrite
;
: square ( -- )
index_increment
DAC0 INDEX @ 127 < IF 0 ELSE 255 THEN dacWrite
;
: triangle ( -- )
index_increment
DAC0 INDEX @ DUP 128 < IF 1 LSHIFT ELSE 255 swap - 1 LSHIFT THEN dacWrite
;
\ Cheap software delay using do loops, smaller time intervals than using MS
\ Run sine, square or triangle with cheap delay to control the frequency
\ Operation commands from the keyboard to select waveform, change frequency, and quit
: keycommand
KEY? IF KEY \ true if key in input buffer, ready to read with KEY
CASE
[CHAR] 1 OF ['] sine WAVEFORM ! ENDOF \ 1 sinewave
[CHAR] 2 OF ['] square WAVEFORM ! ENDOF \ 2 squarewave
[CHAR] 3 OF ['] triangle WAVEFORM ! ENDOF \ 3 triangle
[CHAR] > OF FREQ @ 1 - 2 MAX FREQ ! ENDOF \ > frequency up
[CHAR] < OF FREQ @ 1 + 2 MAX FREQ ! ENDOF \ < frequency down
[CHAR] h OF 1 INDEX_INC ! ENDOF \ h Index increment 1
[CHAR] j OF 2 INDEX_INC ! ENDOF \ j Index increment 2
[CHAR] k OF 4 INDEX_INC ! ENDOF \ k Index increment 4
[CHAR] l OF 8 INDEX_INC ! ENDOF \ l Index increment 8
[CHAR] q OF 1 QUIT ! ENDOF \ q quit
\ ignore unused keys
ENDCASE
THEN
;
: mainwave
0 QUIT ! ['] sine WAVEFORM ! 10 FREQ ! 1 INDEX_INC !
BEGIN
37
keycommand
wave
QUIT @
UNTIL
The code above starts off with three waveform generators. The sinewave generator
simply steps through a 256 sample sinewave table of values between 0 to 255 that
build one cycle of a sinewave. The squarewave generator outputs zero for the first 127
samples and 255 for the second half of the 255 samples. The triangle wave generator
produces an up ramp for the first 127 samples and then calculates a down ramp for the
second half of the samples by subtracting the up ramp values from 255.
The execution of these three waveform words uses an interesting Forth Language
construct call the execution token, or xt. The code for each word in Forth is stored in
memory. The memory address that holds the starting code for executing any particular
Forth word is called its xt, or execution token.
The xt for any word is easily extracted by using the "tick" word.
Note that, when used inside a colon word definition, the bracketed tick must be used
instead [']. To start execution of a word from its xt, use the word EXECUTE ( xt -- ). In
the code above, the xt of the current waveform is stored at the address of the
VARIABLE "WAVEFORM". To execute the xt of the current word stored in the
WAVEFORM address, use this code found in the "wave" word:
WAVEFORM @ EXECUTE
To change the current waveform just load some other xt into the WAVEFORM address,
as seen in the "keycommand" word.
38
Using this special pointer-type Forth construct, the waveforms produced can easily be
changed at will as happens in the "keycommand" word.
Now, to actually produce an audio waveform signal, the words sine, square, or triangle
must be repeated at a high speed which brings us to the topic of Timers.
TIMERS
As it turns out, however, the code for "sine" itself takes around 13 microseconds to
complete making it impossible to produce a 1kHz tone with its paltry 3.9 microsecond
delay between samples. If the word "sine" is simply repeated as fast as possible, what
is produced at the DAC pin out is a very nice sinewave tone a bit above Middle C on
the piano (256Hz).
So high frequencies present a problem given our forth code and the speed of the
ESP32, but frequencies below Middle C can readily be produced by adding a delay
between the single sample loads performed by sine, square, or triangle.
ESP32forth only has the MS time delay word. It sets up a time delay in millisecond
increments, or 1000 microsecond increments, which is way too large for our purposes.
Arduino ESP32 code does have a Microsecond( ) command which suggests that it may
be possible to port it over in the Forth install file. A simple, quick and dirty, solution is
to use a forth word in the form of an empty do-loop. In the above code this is built into
the "wave" word and fed by the contents of a VARIABLE called FREQ. The time delay
produced is simply set by how long our processor takes to count to FREQ from 1.
: delay 1 DO LOOP ; ( n -- )
Another possibility that, at first, seemed to hold a lot of promise were the commands
found in ESP32forth's "timers" vocabulary. These timer words deal directly with the
ESP32's internal timer registers to set up an interrupt driven time interval. An interrupt
system allows other code to run while the time interval is counted down. Since there
are four available timers, it would also allow coding for more than one waveform to
sound at the same time.
39
The timers code centers around the word "interval", easily set up with a single line:
"interval" requires three stack values -- the timer number (0 to 3), the time interval in
microseconds ( FREQ @ ), and the xt address of the word to be executed at the end of
the time interval. Note that older descriptions talk about a "rerun" word that will
retrigger the timer, usually placed at the end of the word triggered. The "rerun" word
was demoted after retriggering was included inside the "interval" word and is no longer
included in the vocabulary.
A problem with the "interval" setup immediately became apparent. Small microsecond
time interval values would crash the system. Perhaps the "interval" timer was
triggering "wave" a second time before the previous one was even finished. A problem
that might have been prevented by the "rerun" construct. Larger time interval values
worked fine but the timers method was abandoned when it was determined that it
would not work with the smaller values needed for audio waveform sample spacings.
Here are some other useful words constructed with the timers vocabulary.
: [Link] ( -- ) 0 0 enable! ;
: [Link] ( -- ) 1 0 enable! ;
Another useful word missing from the timers vocabulary is an isr-removal command to
deconstruct the interrupt set up by "interval", which would have allowed us to re-run
the "mainwave" command in our code after a "quit".
The problem still remains that we are only able to produce waveform frequencies at
Middle C on the piano keyboard and lower. Is there any way to get all those other
higher key frequencies? Turns out there is a way. By loading every other sample
instead of all 256 in our waveform table we can get the C an octave above middle C.
Similarly, by loading every forth sample in the table we can get two octaves above
middle C, every eighth sample to get three octaves above middle C, and so on. What
is lost in this process is the waveform's resolution. By sampling every other table value
we are effectively only using a 128 sample table instead of the original 256 sample
table. Continuing this process you will start seeing steps in the resulting wave which
introduces a high frequency "edge" to the sound.
40
The code above provides a way to explore this method of skipping samples in the
wave table. Checkout the VARIABLE called INDEX_INT (index interval). You can see it
used in the "index-increment" word and set up in "keycommand".
The "keycommand" word in the code provides a way for users to control the audio
waveform coming out the the DAC pin using the computer keyboard. If no key has
been pressed recently this command is exited almost immediately when it sees that
KEY? results in a zero value. Conversely, if a key press has been detected, KEY will
read its value and present it to a CASE statement which decides what to do with it.
Ten special keys are set up in the CASE statement to perform short modifications on
the audio waveform such as changing the waveform or affecting its frequency in
various ways.
Finally, the "mainwave" word pulls together all the previously defined words. It first sets
up some initial conditions, prints out some user instructions, and then runs an infinite
loop constantly running the "wave" command loading one waveform sample at a time
into the DAC at the loop speed, along with checking the "keycommand" for any user
input. The "q" key will quit out of the loop and stop the wave, but "mainwave" can be
entered from the keyboard to restart it at any time.
41
Install File Revisions
The [Link] install file is built using the C/C++ preprocessor X-Macros. It can
list and build all the words found in the Forth Vocabulary. It can even port over Arduino
and ESP32 library functions and build Forth words from them. In the process,
parameters passed to and from C functions are turned into Forth stack operations.
A short description for adding words to the [Link] install file can be found at
the end of this Wordpress website [Link]
v-7-5-list-of-words/. Here is a description of the process.
Locate the macro called PLATFORM_OPCODE_LIST in [Link] and add your words
there, using the macros Y, X, YV, or XV.
2. The shortcut: Y
To add a word named with only letters, numbers, or underscore use the shortened macro
version - Y. Y(MY_WORD123, c_function_to_call()) \ No internal C identifier
needed.
Here, several REQUIRED or OPTIONAL xxx_SUPPORT Word Groups are listed. Each Group
then has its own list of words defined with X, Y, XV, and XY under the "# define"
42
macro.
You can place your own defined words in any of these word groups, even the ones
within a specified Vocabulary using XV(...) and YV(...). However, note the Group
"USER_WORDS \" at the top of the PLATFORM_OPCODE_LIST. This group was created for
you, the USER. User words can go into its own group initiated with the line "#define
USER_WORDS \" or they can go into an external file named userwords.h as specified
earlier in [Link] (// Hook to pull in words from optional userwords.h).
Don't leave out the backslashes. Each backslash glues the next line onto the same
macro definition. The last entry in a group list does not need the backslash.
4. Stack Operations
Inside your code, you can read/write the Forth stack via tos (top of stack) and sp
(rest of stack), both of type cell_t. You can also refer to elements on the stack
with the following types of stack positional names:
Compare that to plain X, which had no vocabulary argument — those words land in the
default vocabulary (FORTH), visible everywhere. XV lets a word be filed under a
specific vocabulary instead, so it's only reachable when that vocabulary is active on
the search stack.
43
Notice word names YIELD and EVALUATE1 have only letters and numbers.
Not needed -> an internal C identifier seen in XV before the word code.
2 Add a line before the closing of some word group list, ending in \
X("myword!", MY_WORD_BANG, c_function_to_call()) \
3 Inside your code, you can read/write the Forth stack via tos (top of
stack) and sp (rest of stack), both of type cell_t.
8. Examples
To add custom words to trigger an LED, you simply locate PLATFORM_OPCODE_LIST within
the install file [Link], create the USER_WORDS group with "define USER_WORDS
\" and place your words in that word group. Or you can place the word definitions
within a file named userwords.h within the same folder as [Link].
#define USER_WORDS \
Y(LED_ON, digitalWrite(2, HIGH)) \
Y(LED_OFF, digitalWrite(2, LOW)) \
When you hit compile in the Arduino IDE, the preprocessor duplicates those lines into
the dictionary name arrays, the ID enums, and the inner interpreter engine
44
automatically.
Here is another example word that reads an analog sensor, maps its value, and leaves
the result on the Forth data stack.
How it works:
Example Explanation
analogRead(sp[0])
The code peeks at the next item down on the stack (sp[0]), which contains the GPIO
pin number.
tos = ...
The newly calculated value overwrites the old max_val register, putting the final
answer at the very top of the stack.
sp++
Because two inputs (pin and max_val) were consumed but only one output was returned,
the stack pointer increments by one to cleanly drop the pin variable.
45
Some Install File Additions
Here are some revisions to the [Link] install file that I found necessary for my
own applications. These are good examples of how this is accomplished, done with
the help of Anthopic's AI Claude.
[Link]
The Forth code can then use the full [Link] command to initialize all the serial
parameters needed for MIDI I/O.
YV(timers, timer_isr_callback_remove, \
n0 = timer_isr_callback_remove((timer_group_t) n1, (timer_idx_t) n0); NIP) \
46
SPI Support
Exposes just enough of the Arduino SPI.h API to drive simple write-only SPI
peripherals (like the MCP4921 DAC) from Forth. Chip-select is intentionally NOT
handled here. It's just a GPIO, and can easily be handled using pinMode/digitalWrite.
Creates the mcp-dac-spi.h header file to include in the folder with [Link],
plus some changes to [Link] to create the SPI vocabulary. A more complete
version can be found in "The Great Book for ESP32forth" by Marc Petremann.
mcp-dac-spi.h
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// [Link]
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
* ESP32forth generic SPI bus support (add-on module)
*
* Exposes just enough of the Arduino SPI.h API to drive simple
* write-only SPI peripherals (e.g. the MCP4921 DAC) from Forth.
* Chip-select is intentionally NOT handled here -- it's just a GPIO,
* and this build already exposes pinMode/digitalWrite for that.
*/
#include <SPI.h>
static void SpiBeginPins(cell_t sck, cell_t miso, cell_t mosi, cell_t ss) {
[Link]((int8_t) sck, (int8_t) miso, (int8_t) mosi, (int8_t) ss);
}
47
[Link]();
return (cell_t) result;
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
changes to [Link]
~~~~~~~~~
near line 151, before http_client_vocabulary
OPTIONAL_SPI_VOCABULARY \
~~~~~~~~~
near lines 595 to 601, before http-client.h
~~~~~~~~~
near line 656, before http_client_support
OPTIONAL_SPI_SUPPORT \
~~~~~~~~
near lines 3176 to 3181, after DEFINED? rmt-builtins
48
DEFINED? SPI-builtins [IF]
vocabulary SPI SPI definitions
transfer SPI-builtins
forth definitions
[THEN]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Using it with the MCP4921
Wiring: SCK→DAC's SCK, MOSI→DAC's SDI, any free GPIO→DAC's CS (pulled high
at idle). MISO is unused (the 4921 has no data output), so it's fine to leave
unconnected or pass -1 if you use SpiBeginPins.
The MCP4921 wants one continuous 16-bit frame while CS is low: a 4-bit config nibble
followed by your 12-bit value. 0x3000 | value gives you unbuffered input, 1× gain,
active mode — the common default (use 0x7000 | value instead if you want buffered
input).
forth
SPI definitions
SpiBegin
forth definitions
Then 2048 mcp4921! sets mid-scale output. I started the clock at 1000000 (1 MHz) — the
4921 is rated up to 20 MHz, but I'd get it working reliably at 1 MHz first, then push the
speed up once you've confirmed the wiring, especially if you're on a breadboard.
Microsecond Delay
49
The Arduino has a millisecond delay, delay( ), which Forth retains. However it left out
Arduino's microsecond delay which is easily added. Note that this may conflict with
some timer interrupts. For a dirty and simpler microsecond delay just use an empty
DO LOOP.
~~~~~~~~~~~
near line 699, after MS_TICKS
~~~~~~~~~~~
near lines 2300 to 2302, after ms defined
The ESP32 has two core processors that can run separate programs simultaneously.
ESP32forth does have the multitasking words task, start-task, pause, but these just
share the processor in a round-robin manner all in Core 1. WIFI and BlueTooth do use
Core 0 for stack and system housekeeping. Other than this, Core 0 mostly sits idle
with no way to pin a word task to it.
However, ESP32forth does have true FreeRTOS dual-core tasks — this is a separate
rtos vocabulary, and it's real, direct ESP-IDF/FreeRTOS bindings, same style as the
timers module:
xPortGetCoreID is trivial and useful right now — run it anywhere and you'll see 1. The
problem here is that vTaskCreatPinnedToCore does not use a Forth xt execution token
which would allow pinning an actual Forth word to Core 0, exactly how timer interrupts
are set up with the word "interval". Instead, it uses a C function pointer, only allowing
pinning to an already compiled C function.
To fix this, the word on-core is added to the rtos vocabulary inside [Link] to
be used like so:
50
' some-word 0 on-core
Launches "some-word" in Core 0 and returns the task Handle. Note that if this is
contained in a word definition, the tick [ ' ] must be used instead.
~~~~~~~~~~~~
near line 823, after #ifndef ENABLE_FREERTOS_SUPPORT block ... #endif
struct core_task_args {
cell_t xt;
};
51
return 0;
}
return (cell_t) handle;
}
~~~~~~~~~~~~~~
near line 827, after YV(rtos, xPortGetCoreID, PUSH xPortGetCoreID()) \
~~~~~~~~~~~~~~
near line 1917, after transfer rtos-builtins
Random
52
esp_random ( -- r ) esp_random() — raw 32-bit
value, unbounded
near line 632 among the "REQUIRED" lines, inside #define PLATFORM_OPCODE_LIST \
REQUIRED_ARDUINO_RANDOM_SUPPORT \
53