![]() |
The board support package for the STM32WB Nucleo Board is restricted to the Arduino UNO R3 pin header and the onboard LEDs and switches (buttons). The STM32 has much more capabilities then 16 digital I/O pins, 6 analog input pins, UART, SPI, and I2C interfaces. But if you want to use the more advanced features you can use the CubeMX to create source code for the internal peripherals. This project wants to show how to use the Cube Ecosystem for a Forth system (or vice versa) and can't implement all features and possibilities the STM32WB has. It is a good starting point for your project.
For GPIO pin defaults see table.
led1! ( f -- ) set LED1 (blue)
led2! ( f -- ) set LED2 (green)
led3! ( f -- ) set LED3 (red)
led1@ ( -- f ) get LED1 (blue)
led2@ ( -- f ) get LED2 (green)
led3@ ( -- f ) get LED3 (red)
switch1? ( -- f ) get switch1, closed=TRUE
switch2? ( -- f ) get switch2, closed=TRUE
switch3? ( -- f ) get switch3, closed=TRUE
dport! ( n -- ) set the digital output port (D0=bit0 .. D15=bit15).
dport@ ( -- n ) get the digital input/output port (D0=bit0 .. D15=bit15).
dpin! ( f u -- ) set the digital output port pin u (D0=0 .. D15=15, A0=16 .. A5=21) to f (TRUE 1, FALSE 0)
dpin@ ( u -- f ) get the digital input/output port pin u
dmod ( u1 u2 -- ) set the pin u2 to mode u1: 0 in, 1 in pull-up, 2 in pull-down, 3 out push pull, 4 out open drain, 5 out push pull PWM
6 input capture, 7 output compare, 8 I2C, 9 UART, 10 SPI, 11 analog
pwmpin! ( u1 u2 -- ) set the digital output port pin u2 (D3=3, D6=6, D9=9) to a PWM value u1 (0..1000). Default frequency is 1 kHz, TIMER1
pwmprescale ( u -- ) set the PWM prescale for TIMER1. 32 kHz / prescale, default 32 -> PWM frequency 1 kHz
EXTImod ( u1 u2 -- ) set for pin u2 (D2, D4, D7, D10) the EXTI mode u1: 0 rising, 1 falling, 2 both edges, 3 none
EXTIwait ( u1 u2 -- ) wait for EXTI interrupt on pin u2 (D2, D4, D7, D10), timeout u2 in [ms]
ICOCprescale ( u -- ) set the input capture / output compare prescale for TIMER2. default 32 -> 32 MHz / 32 = 1 MHz, timer resolution 1 us
ICOCperiod! ( u -- ) set the input capture / output compare (TIMER2) period. default $FFFFFFFF (4'294'967'295).
When the up counter reaches the period, the counter is set to 0.
For prescale 32 the maximum time is about 1 h 11 m
ICOCcount! ( u -- ) set the input capture / output compare counter for TIMER2
ICOCcount@ ( -- u ) get the input capture / output compare counter for TIMER2
ICOCstart ( -- ) start the ICOC period
ICOCstop ( -- ) stop the ICOC period
OCmod ( u1 u2 -- ) set for pin u2 (D0, D1, D5) the Output Compare mode u1: 0 frozen, 1 active level on match, 2 inactive level on match,
3 toggle on match, 4 forced active, 5 forced inactive
OCstart ( u1 u2 -- ) start the output compare mode for pin u2 with pulse u1
OCstop ( u -- ) stop output compare for pin u
ICstart ( u -- ) start input capture u: 0 rising edge, 1 falling edge, 2 both edges
ICstop ( -- ) stop input capture
waitperiod ( -- ) wait for the end of the TIMER2 period
OCwait ( u -- ) wait for the end of output capture on pin u
ICwait ( u1 -- u2 ) wait for the end of input capture with timeout u1, returns counter u2
apin@ ( u1 -- u2 ) get the analog input port pin u1 (A0 .. A5). Returns a 12 bit value u2 (0..4095)
vref@ ( -- u ) get the Vref voltage in mV (rather the VDDA, about 3300 mV)
vbat@ ( -- u ) get the Vbat voltage in mV (about 3300 mV)
CPUtemp@ ( -- u ) get CPU temperature in degree Celsius
I2Cput ( c- u1 u2-- ) put a message with length u (count in bytes) from buffer at a to the I2C slave device u
I2Cget ( c- u1 u2 -- ) get a message c- with length u1 from I2C slave u2 device to buffer at c-
I2Cputget ( c- u1 u2 u3 -- ) put a message with length u1 from buffer at c- to the I2C slave device u3
and get a message with length u2 from device to buffer at c-
SPIget ( c- u -- ) get a message with length u from SPI slave device to buffer at c-
SPIput ( c- u -- ) put a message with length u from buffer at c- to the SPI slave device
SPIputget ( c- u1 u2 -- ) put a message with length u1 from buffer at c- to the SPI slave device
and get a message with length u2 from device to buffer at c-
SPImutex ( -- a- ) get the SPI mutex address
This example is a very simple chase lighting program inspired by Knight Rider. You need 8 LEDs and 8 resistors.
: init-port ( -- )
11 16 dmod \ set A0/D16 to analog
8 0 do
0 i dpin!
3 i dmod \ port is output
loop
;
: delay ( -- )
50 osDelay drop \ wait 50 ms
;
: left ( -- )
7 0 do
1 i dpin!
delay
0 i dpin!
loop
;
: right ( -- )
8 1 do
1 8 i - dpin!
delay
0 8 i - dpin!
loop
;
: knigthrider ( -- )
init-port
begin
left right
switch1? until
0 0 pin dpin!
;
apin@ ( a -- u )
returns the ADC value (12 bit, 0 .. 4095) from one of
the analog pins A0 to A2 (0 .. 2). Here I use the A0 to control the
delay.
: delay ( -- )
0 apin@ 10 / osDelay drop \ delay depends on A0
;
To get an idea how fast the ADC, RTOS, and the Forth program are. The
left
or right
word takes about 125 us, the knightrider
loop about
50 us (no osDelay). Pretty fast for my opinion (STM32WB55 @ 32 MHz sysclock).
Create a task for the knigthrider (details see How to Use Tasks) to run it in the background:
task knigthrider&
knigthrider& construct
' knigthrider knigthrider& start-task
Only two port pins are supported so far. The TIMER1 is used for the timebase, time resolution is 1 us (32 MHz SysClk divided by 32). The PWM scale is from 0 (0 % duty cycle) to 1000 (100 % duty cycle), this results in a PWM frequency of 1 kHz. If you need higher PWM frequencies, decrease the divider and/or the scale.
PWM port pins: D3 (TIM1CH3), D6 (TIM1CH1), D9 (TIM1CH2)
Simple test program to set brightness of a LED on pin D3 with a
potentiometer on A0. Default PWM frequency is 1 kHz (prescaler set to
32). You can set the prescale with the word pwmprescale
from 32 kHz
(value 1) down to 0.5 Hz (64000).
5 6 dmod \ set D6 to PWM
11 16 dmod \ set A0/D16 to analog
: pwm ( -- )
begin
0 apin@ 4 / 6 pwmpin!
10 osDelay drop
switch1?
until
;
https://en.wikipedia.org/wiki/Servo_(radio_control): The control signal is a digital PWM signal with a 50 Hz frame rate. Within each 20 ms timeframe, an active-high digital pulse controls the position. The pulse nominally ranges from 1.0 ms to 2.0 ms with 1.5 ms always being center of range. Pulse widths outside this range can be used for "overtravel" - moving the servo beyond its normal range.
A servo pulse of 1.5 ms width will typically set the servo to its "neutral" position (typically half of the specified full range), a pulse of 1.0 ms will set it to 0°, and a pulse of 2.0 ms to 90° (for a 90° servo). The physical limits and timings of the servo hardware varies between brands and models, but a general servo's full angular motion will travel somewhere in the range of 90° – 180° and the neutral position (45° or 90°) is almost always at 1.5 ms. This is the "standard pulse servo mode" used by all hobby analog servos.
The BSPs default PWM frequency is 1 kHz, 50 Hz is 20 times slower. The divider is therefore 32 * 20 = 640.
angle | time | n |
---|---|---|
0° | 1 ms | 50 |
45° | 1.5 ms | 75 |
90° | 2 ms | 100 |
135° | 2.5 ms | 125 |
180° | 3 ms | 150 |
225° | 3.5 ms | 175 |
270° | 4 ms | 200 |
640 pwmprescale
5 6 dmod \ set D6 to PWM
11 16 dmod \ set A0/D16 to analog
: servo ( -- )
begin
100 50 do
i 6 pwmpin!
i neopixel!
i 50 = if
1000 \ give some more time to get back
else
200
then
osDelay drop
10 +loop
key? until
key drop
;
640 pwmprescale
5 6 dmod \ set D6 to PWM
11 16 dmod \ set A0/D16 to analog
: slowservo ( -- )
begin
100 50 do
i 6 pwmpin!
50 osDelay drop
1 +loop
50 100 do
i 6 pwmpin!
50 osDelay drop
-1 +loop
key? until
key drop
;
Default timer resolution is 1 us. The 32 bit TIMER2 is used as time base for Input Capture / Output Compare. For a 5 s period 5'000'000 cycles are needed. All channels (input capture / output compare) use the same time base.
: period ( -- )
5000000 ICOCperiod! \ 5 s period
ICOCstart
begin
waitperiod
cr .time
key? until
key drop
;
Output compare TIM2: D0 (TIM2CH4), D1 (TIM2CH3), D5 (TIM2CH1)
7 0 dmod \ output compare for D0
7 1 dmod \ output compare for D1
7 5 dmod \ output compare for D5
: oc-toggle ( -- )
5000000 ICOCperiod! \ 5 s period
ICOCstart
3 0 OCmod 1000000 0 OCstart \ toggle D0 after 1 s
3 1 OCmod 2000000 1 OCstart \ toggle D1 after 2 s
3 5 OCmod 3000000 5 OCstart \ toggle D5 after 3 s
begin
waitperiod
cr .time
key? until
key drop
;
When you abort (hit any key) the program, the timer still runs and controls the port pins. To stop the port pins:
0 OCstop 1 OCstop 5 OCstop
Or change the prescale to make it faster or slower:
1 ICOCprescale
This sample program measures the time between the edges on port A2. If no event occurs within 2 seconds, "timeout" is issued. Hit any key to abort program.
: ic-test ( -- )
6 18 dmod \ input capture on A2
ICOCstart
2 ICstart \ both edges
ICOCcount@ ( -- count )
begin
2000 \ 2 s timeout
ICwait ( -- old-capture capture )
cr
dup 0= if
." timeout" drop
else
dup rot ( -- capture capture old-capture )
- . ." us"
then
key? until
key drop
drop
ICstop
;
If you use a push button for D13, there could be several events on pressing the push button once. This is called bouncing. Bouncing time is about 250 us for my push button.
GPIO pins D2, D4, D7 and D10 can be used as an EXTI line, D4 and D7 share the same EXTI line. EXTIs are external interrupt lines, D2 uses EXTI_9_5 (EXTI Line 9..5 interrupt), D2 EXTI_9_5, D4 EXTI_15_10, D7 EXTI_15_10, and D10 EXTI_4.
If you use a push button for D2, there could be several events on pressing the push button once. This is called bouncing. For details see A Guide to Debouncing.
: exti-test ( -- )
2 2 EXTImod \ both edges on D2
begin
2000 2 EXTIwait \ wait for edge on D2 with 2 s timeout
cr
0= if
2 dpin@ if
." rising edge"
else
." falling edge"
then
else
." timeout"
then
key? until
key drop
;
Most development boards have at least a switch or a push button, the Nucleo has 3 switches and Dongle has 1 switche.
switch1? .
The result is 0. But if you press and hold the OK Button, the result will be -1.
There is no debouncing for the switchx?
words.
Feather adaptor built with a Arduino Proto Shield Uno rev3. There is also a Grove I2C interface for an Octopus 128x64 OLED-Display, a microSD adaptor (SPI), and a Neopixel.
Nucleo right | Function | Arduino | Feather | Micro-SD | Grove |
---|---|---|---|---|---|
PB8 | I2C1 | D15 SCL | JP3.11 SCL | SCL Pin1 | |
PB9 | I2C1 | D14 SDA | JP3.12 SDA | SDA Pin2 | |
AVDD | AREF | ||||
GND | GND | JP1.13 GND | Pin3, Pin6 | GND Pin4 | |
PA5 | SPI1 | D13 | JP1.6 SCK | Pin5 | |
PA6 | SPI1 | D12 | JP1.4 MISO | Pin7 | |
PA7 | SPI1 | D11 | JP1.5 MOSI | Pin2 | |
PA4 | (SPI) | D10 | JP3.7 D10 | Pin1 | |
PA9 | D9 | JP3.8 D9 | |||
PC12 | Neopixel | D8 | |||
PC13 | D7 | ||||
PA8 | D6 | JP3.9 D6 | |||
PA15 | D5 | JP3.10 D5 | |||
PC10 | D4 | ||||
PA10 | D3 | ||||
PC6 | D2 | ||||
PA2 | UART | D1 Tx | JP1.2 D1 | ||
PA3 | UART | D0 rx | JP1.3 D0 |
Nucleo left | Function | Arduino | Feather | Micro-SD | Grove |
---|---|---|---|---|---|
NC | IOREF | ||||
NRST | RESET | JP1.16 RST | |||
3V3 | 3.3V | JP1.14/15 3V3 | Pin4 | VCC Pin3 | |
5V | 5V | JP3.3 USB | |||
GND | GND | ||||
GND | GND | ||||
VIN | Vin | ||||
PC0 | A0 | JP1.12 A0 | |||
PC1 | A1 | JP1.11 A1 | |||
PA1 | A2 | JP1.10 A2 | |||
PA0 | A3 | JP1.9 A3 | |||
PC3 | A4 | JP1.8 A4 | |||
PC2 | A5 | JP1.7 A5 | |||
JP3.1 VBAT | |||||
JP3.2 EN |
Remove the USB Type A plug from the dongle and add a Adafruit Micro B breakout board. It is convenient to have a Micro-SD breakout board (level shifter is not needed) and JTAG connector (I prefer the 14 pin STM variant to have a serial interface by the ST-LINK). Everything mounted on headers on the backside of FeatherWing Tripler Mini Kit.
Description | Dongle | Function | Feather | Micro-SD | JTAG 14pin |
---|---|---|---|---|---|
GND | CN1.1 | GND | JP1.13 GND | GND | 5, 7, 11 |
NRST | CN1.2 | RES | JP1.16 RST | 12 | |
PA13 | CN1.3 | SWDIO | - | 4 | |
PA14 | CN1.4 | SWDCLK | - | 6 | |
PB3 | CN1.5 | SWO | A4? | 8 | |
3V3 | CN1.6 | 3V3 | JP1.14/15 3V3 | 3V 5V | 3 |
PB2 | CN1.7 | SPI_CS | - | CS | |
PA5 | CN1.8 | D13 SCK | JP1.6 SCK | CLK | |
PA6 | CN1.9 | D12 MISO | JP1.4 MISO | DO | |
PA7 | CN1.10 | D11 MOSI | JP1.5 MOSI | DI | |
PB8 | CN2.1 | D15 SCL | JP3.11 SCL | ||
PB9 | CN2.2 | D14 SDA | JP3.12 SDA | ||
PA0 | CN2.3 | A3 | JP1.9 A3 | ||
PA2 | CN2.4 | D1 | JP1.2 D1 | ||
PA3 | CN2.5 | D0 | JP1.3 D0 | ||
PB6 | CN2.6 | UARTRX | 13 | ||
PA9 | CN2.7 | D9 | JP3.8 D9 | ||
PB7 | CN2.7 | UARTTX | 14 | ||
PA8 | CN2.8 | D6 Neopixel | JP3.9 D6 | ||
GND | CN2.9 | GND | GND | ||
PA1 | CN2.10 | A2 | JP1.10 A2 | ||
!USB5V | 5V | JP3.3 USB | |||
BOOT0 | BOOT0 | JP1.1 B0 | |||
PB0 AT2 ? | JP1.12 A0 | ||||
PB1 AT2 ? | JP1.11 A1 | ||||
PB3 ? | CN1.5 | SWO | JP1.8 A4? | 8 | |
JP1.7 A5 | |||||
JP3.1 VBAT | |||||
JP3.2 EN |
NeoPixel is Adafruit's brand of individually addressable red-green-blue (RGB) LED. They are based on the WS2812 LED and WS2811 driver, where the WS2811 is integrated into the LED, for reduced footprint. Adafruit manufactures several products with NeoPixels with form factors such as strips, rings, matrices, Arduino shields, traditional five-millimeter cylinder LED and individual NeoPixel with or without a PCB. The control protocol for NeoPixels is based on only one communication wire.
For the Nucleo I use D8 for the Neopixel. It takes about 30 us to set one Neopixel, during this time the interrupts are disabled.
3 8 dmod \ D8 output $ff0000 neopixel! \ red LED 100 % brightness
NeoPixelWing uses the D6 as datapin for the Neopixels.
Switch on the first 4 NeoPixels
3 6 dmod \ D6 output
32 cells buffer: pixelbuffer \ create buffer for the neopixels
$ff0000 pixelbuffer ! \ 1st Neopixel red
$00ff00 pixelbuffer 1 cells + ! \ 2nd Neopixel green
$0000ff pixelbuffer 2 cells + ! \ 3th Neopixel blue
$7f7f7f pixelbuffer 3 cells + ! \ 4th Neopixel white 50 %
pixelbuffer 4 neopixels
Switch on all 32 NeoPixels
create pixels
$010000 , $020000 , $040000 , $080000 , $100000 , $200000 , $400000 , $800000 , \ 1st row red
$008000 , $004000 , $002000 , $001000 , $000800 , $000400 , $000200 , $000100 , \ 2nd row green
$000001 , $000002 , $000004 , $000008 , $000010 , $000020 , $000040 , $000080 , \ 3th row blue
$808080 , $404040 , $202020 , $101010 , $080808 , $040404 , $020202 , $010101 , \ 4th row white
pixels 32 neopixels
It takes about 30 us to set one Neopixel, for 32 Pixels it takes nearly 1 ms, during this time the interrupts are disabled. Consider this for RT programs and interrupt latency.
plex-emit
works like the standard word emit
. It blocks the calling thread,
as long as the character is not written to the Plex display (less than 300 us
for a 6x8 character and 400 kHz I2C).
Horizontal (x) position is in pixel (0 to 15). The plex display is default shutdown,
to switch on 1 plexshutdown
.
Implentation plex.c.
plex-emit ( c -- ) Emit a character (writes a character to the Plex display)
plex-emit? ( -- f ) Plex ready to get a character (I2C not busy)
hook-emit ( -- a- ) Hooks for redirecting terminal IO on the fly
hook-emit? ( -- a- )
plexpos! ( u -- ) Set Plex cursor position/column u
plexpos@ ( -- u ) Get the current Plex cursor position
plexclr ( -- ) clear the Plex display, set the cursor to 0
plexfont ( u -- ) Select the font, u: 0 6x8, 1 8x8
plexpwm ( u -- ) default PWM 1 .. 255 (brightness)
plexshutdown ( f -- ) 1 activate Plex dispaly, 0 shutdown display
plexcolumn! ( u1 u2 n -- ) write LEDs (7 pixels) u2 at the position/column u1 (0 to 15) with the brightness n
plexcolumn@ ( u1 -- u2 ) read LEDs at position/column u1
plexpixel! ( u1 u2 n -- ) write one pixel at column u1 and row u2 with brightness n
plexpixel@ ( u1 u2 -- f ) read one pixel at column u1 and row u2
plexframe! ( u -- ) Set the active frame u (0 .. 7) for write and read
plexframe@ ( -- u ) Get the active frame u
plexdisplay! ( u -- ) Show the display frame u
plexdisplay@ ( -- u ) Which frame is showed
Adafruit 15x7 CharliePlex LED Matrix Display. Driver is the IS31FL3731 datasheet.
1 plexshutdown
0 0 100 plexpixel!
1 1 200 plexpixel!
: count-down ( -- )
plexclr
-1 -1 -1 alarm! \ an alarm every second
wait-alarm
10 0 do
1 plexpos!
i 1 + 25 * plexpwm \ set brightness
i 0 = if
[char] 1 plex-emit
[char] 0 plex-emit
else
[char] 0 plex-emit
10 i - [char] 0 + plex-emit
then
wait-alarm
loop
0 $ff -1 plexcolumn!
14 $ff -1 plexcolumn!
1 plexpos!
[char] 0 dup plex-emit plex-emit
cr ." Launch!" cr
;
: LCD>plex ( u -- ) \ copy LCD from column u to plex
15 0 do \ write 15 charlie columns
dup i + dup 126 mod swap 126 / ( u -- u x y)
lcdpos! lcdcolumn@ \ read LCD column
i swap 50 plexcolumn! \ write PLEX column
loop
drop
;
: Marquee ( c- u -- ) \ marquee a string on charlie plex
lcdclr 0 lcdfont
2dup >lcd 2swap type >term \ write string to LCD
nip ( c- u -- u )
3 - \ remove trailing spaces
begin
dup 6 * 0 do \ all string columns, a char is 6 pixels wide
i LCD>plex
40 osDelay drop
switch1? if leave then
loop
switch1? until
drop ( u -- )
;
1 plexshutdown
200 buffer: message
message .str" MECRISP-CUBE REAL-TIME FORTH ON THE GO! "
message strlen Marquee
- https://www.st.com/en/evaluation-tools/p-nucleo-wb55.html
- Schematic Nucleo Board MB1355
- Schematic Nucleo Dongle MB1293 dongle
Signal name | STM32WB55 pin |
---|---|
SWITCH1 | PC4 (PC13) |
SWITCH2 | PD0 |
SWITCH3 | PD1 |
Signal name | STM32WB55 pin | Comment |
---|---|---|
LD1 | PB5 | |
LD2 | PB0 | |
LD3 | PB1 | |
Neopixel | PC12 | D8 |
Signal name | STM32WB55 pin | Comment |
---|---|---|
IC2_SCL | PA9 | I2C1_SCL |
IC2_SDA | PA10 | I2C1_SDA |
Signal name | STM32WB55 pin | Comment |
---|---|---|
UART_TX | PB6 | USART1_TX |
UART_RX | PB7 | USART1_RX |
Signal name | STM32WB55 pin | Comment |
---|---|---|
FLASH_NCS | PD3 | QUADSPI_BK1_NCS |
FLASH_IO0 | PB9 | QUADSPI_BK1_IO0 |
FLASH_IO1 | PD5 | QUADSPI_BK1_IO1 |
FLASH_IO2 | PD6 | QUADSPI_BK1_IO2 |
FLASH_IO3 | PD7 | QUADSPI_BK1_IO3 |
FLASH_SCLK | PA3 | QUADSPI_BK1_SCLK |
Signal name | STM32WB55 pin | Comment |
---|---|---|
SWITCH1 | PC12 | WKUP3 |
Signal name | STM32WB55 pin | Comment |
---|---|---|
LD1 | PB5 | |
LD2 | PB0 | |
LD3 | PA4 | |
Neopixel | PC12 | D6 |