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.

../../_images/dtoverlay.png

Fig. 21 dtoverlay: configure Raspberry PI IO#

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)
../../_images/pi4-pinout.png

Fig. 22 Raspberry PI 4 pin assignment#

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.

../../_images/stm32_pinout.png

Fig. 23 STM32 pin assignments (pdf)#

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
Hide 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