STM32#
The control of the robot - motors and balancing - is delegated to a dedicated STM32. Although the Raspberry PI has certainly sufficient compute power for this function, its Linux operating system is not well suited for real time control. Depending on activity, a process may be suspended for a potentially sufficiently long time for the robot to crash. While with light CPU loads this may never happen, there is no guarantee and consequently such two (or multiple) CPU arrangements are typical in control applications where response time is critical.
dtoverlay#
The STM32 communicates with the Raspberry PI over UART. These need to be enabled. Login to Balena (this works only with custom installs!) and open the dashboard for your Raspberry PI.
Choose Device Conifguration
and change Define DT overlays
to
"vc4-fkms-v3d", "uart3", "uart4", "gpio-poweroff,gpiopin=16,active_low=1", "enable_uart=0"
similar to Figure 21. The Raspberry PI will reboot for the change to take effect.
The Raspberry PI 4 pinout is as follows:
TXD RXD CTS RTS Board Pins
uart0 14 15 8 10
uart1 14 15 8 10
uart2 0 1 2 3 27 28 (I2C)
uart3 4 5 6 7 7 29
uart4 8 9 10 11 24 21 (SPI0)
uart5 12 13 14 15 32 33 (gpio-fan)
Customize MicroPython#
We use a special MicroPython VM with the following customizations:
STM32 pin assignments
floats not stored on heap
The STM32 has many powerful peripherals such as quadrature decoders that in this project will be used for measuring the true wheel RPM. Many of these functions are available only on specific pins. Figure 23 shows the pin assignment used for this project.
The second customization configures the MicroPython VM to store floats inline, rather than on the heap so that they may be safely used in interrupt handlers.
MicroPython Source#
%%bash
# interpreter
cd $IOT/mp
if [ ! -d micropython ]
then
git clone git@github.com:micropython/micropython.git
else
cd micropython
git checkout master
git pull
git merge master
fi
# library
cd $IOT/mp
if [ ! -d micropython-lib ]
then
git clone git@github.com:micropython/micropython-lib.git
else
cd micropython-lib
git checkout master
git pull
git merge master
fi
Compile#
%%service arm32
cd $IOT/mp/micropython/ports/stm32
cp -rf ../../../boards/MOTOR_HAT boards
make submodules
make BOARD=MOTOR_HAT clean
make BOARD=MOTOR_HAT USER_C_MODULES=../../../modules
Flash#
Install stm32 flasher:
%%bash
cd /tmp
git clone https://git.code.sf.net/p/stm32flash/code stm32flash-code
cd stm32flash-code
sudo make install
%%host
import stm32
stm32.flash(info_only=True)
REPL#
%connect serial:///dev/ttyAMA1
import sys
print(sys.platform)
Connected to 2c:00:29:00:09:50:52:42:4e:30:39:20 @ serial:///dev/ttyAMA1
pyboard
STM32 from Pi#
stm32.py
provides a number of convenience functions for accessing the STM32 from the Raspberry PI:
!cat $IOT_PROJECTS/robot/code/rpi/stm32.py
Show code cell output
from iot_device.pydevice import Pydevice
from iot_device import DeviceRegistry, RemoteError
from serial import Serial
from gpiozero import LED as Pin
import asyncio, subprocess, os, time
def hard_reset(boot_mode=False):
"""Hard reset STM32. Same as pressing reset button.
@param boot_mode: bool Start in "dfu" boot-mode (default False).
"""
with Pin(21) as nrst, Pin(27) as boot0:
if boot_mode:
boot0.on()
else:
boot0.off()
time.sleep(0.1)
nrst.off()
time.sleep(0.1)
nrst.on()
# let boot process finish
time.sleep(1)
def _flash_bin(address, firmware, dev, info_only):
"""Flash helper. Used by flash method."""
if info_only:
cmd = ['stm32flash', dev]
else:
cmd = ['stm32flash', '-v', '-S', address, '-w', firmware, dev]
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
print(stdout.decode())
if len(stderr) > 0:
print(f"***** {stderr.decode()}")
def flash(firmware_dir='$IOT/mp/micropython/ports/stm32/build-MOTOR_HAT/', dev='/dev/ttyAMA2', info_only=False):
"""Flash MicroPython VM.
@param firmware_dir Location of firmware.
@param dev Device port.
@param info_only Dry run if True (default False).
"""
dir = os.path.expandvars(firmware_dir)
hard_reset(boot_mode=True)
_flash_bin('0x08000000', os.path.join(dir, 'firmware0.bin'), dev, info_only)
_flash_bin('0x08020000', os.path.join(dir, 'firmware1.bin'), dev, info_only)
hard_reset(boot_mode=False)
def exec(cmd, dev='serial:///dev/ttyAMA1'):
"""Execute MicroPython code on STM32.
@param cmd: string Code.
"""
registry = DeviceRegistry()
registry.register(dev)
with registry.get_device(dev) as repl:
res = repl.exec(cmd)
try:
res = res.decode()
except:
pass
return res
def exec_no_follow(cmd, dev='/dev/ttyAMA1'):
"""Execute MicroPython code on STM32 & do not wait for result."""
with Serial(dev, 115200, timeout=0.5, write_timeout=2, exclusive= True) as serial:
pyd = Pydevice(serial)
pyd.enter_raw_repl()
pyd.exec_raw_no_follow(cmd)
time.sleep(0.2)
while serial.in_waiting:
data = serial.read(serial.in_waiting)
try:
data = data.decode()
except:
pass
print(f"*** MCU: {data}")
time.sleep(0.1)
async def async_exec(cmd, dev='/dev/ttyAMA1', pause=0.1):
"""Asynchronous exec.
@param cmd: string Code.
@param pause: float Interval checking for output [seconds].
"""
with Serial(dev, 115200, timeout=0.5, write_timeout=2, exclusive=False) as serial:
pyd = Pydevice(serial)
pyd.enter_raw_repl()
pyd.exec_raw_no_follow(cmd)
while True:
if serial.in_waiting:
data = serial.read(serial.in_waiting)
try:
data = data.decode()
except:
pass
print(f"MCU: {data}")
await asyncio.sleep(0)
else:
await asyncio.sleep(pause)
def rsync(dry_run=True, dev='serial:///dev/ttyAMA1'):
registry = DeviceRegistry()
registry.register(dev)
with registry.get_device(dev) as repl:
repl.rsync(data_consumer=lambda x: print(x, end=''), dry_run=dry_run)
def rlist(dev='serial:///dev/ttyAMA1'):
registry = DeviceRegistry()
registry.register(dev)
with registry.get_device(dev) as repl:
repl.rlist(data_consumer=lambda x: print(x, end=''), show=True)
def supply_voltage():
"""Report unregulated supply voltage in [V] (nominally 12V)."""
return float(exec(
"""
from pyb import ADC
adc = pyb.ADC('V12_DIV')
print(0.00655233*adc.read())
"""))
def power_off(delay=10):
"""Turn off 5V power supply to Raspberry PI & STM32.
@param delay: float Delay in seconds before turning power off.
Warning: make sure Raspberry PI is shutdown before calling this!
"""
print(f"shutting down ...")
exec_no_follow(
f"""
from pyb import Pin
from time import sleep
# declaring as input first sets the initial value after configuring as output
shut_dn = Pin('PWR_EN', mode=Pin.IN, pull=Pin.PULL_UP)
shut_dn.value(1)
shut_dn = Pin('PWR_EN', mode=Pin.OUT_OD)
sleep({delay})
shut_dn.value(0)
""")
os.system("sudo halt")
%%host
# setup path
import sys, os
sys.path.append(os.path.join(os.getenv('IOT_PROJECTS'), 'robot/code/rpi'))
import stm32
cmd = 'print(4+7, end="")'
print(f"exec({cmd}): {stm32.exec(cmd)}")
print(f"supply voltage: {stm32.supply_voltage():.1f}V")
exec(print(4+7, end="")): 11
supply voltage: 10.1V
Device Configuration#
%%writefile $IOT_PROJECTS/devices/robot.yaml
# motor controller etc.
robot-stm32:
# uid: 1c:00:26:00:09:50:52:42:4e:30:39:20
uid: 2c:00:29:00:09:50:52:42:4e:30:39:20
install-dir: /flash
path: robot/code
resources:
- secrets.py:
path: libs
- state.py:
path: robot/code/rpi/robot
install-dir: /flash/lib/robot
- pid.py:
path: robot/code/rpi/robot
install-dir: /flash/lib/robot
- bno055:
path: libs
install-dir: /flash/lib
- stm32
# ble remote control
robot-esp32:
uid: 30:ae:a4:28:39:f0
path: robot/code
resources:
- secrets.py:
path: libs
- esp32
Writing /home/iot/iot49.org/docs/projects/devices/robot.yaml
%connect serial:///dev/ttyAMA1
%rsync