{ "cells": [ { "cell_type": "markdown", "id": "8ca08230-20c9-450e-9e15-7d959972e585", "metadata": {}, "source": [ "# STM32\n", "\n", "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. " ] }, { "cell_type": "markdown", "id": "6fcefb26-dde1-41f1-ba67-e4b52ee8e7f1", "metadata": {}, "source": [ "## dtoverlay\n", "\n", "The STM32 communicates with the Raspberry PI over UART. These need to be enabled. Login to [Balena](https://www.balena.io/) (this works only with custom installs!) and open the dashboard for your Raspberry PI. \n", "\n", "Choose `Device Conifguration` and change `Define DT overlays` to \n", "\n", "```\n", "\"vc4-fkms-v3d\", \"uart3\", \"uart4\", \"gpio-poweroff,gpiopin=16,active_low=1\", \"enable_uart=0\"\n", "``` \n", "\n", "similar to {numref}`Figure %s `. The Raspberry PI will reboot for the change to take effect.\n", "\n", "```{figure} figures/dtoverlay.png\n", ":width: 500px\n", ":name: dtoverlay\n", "\n", "dtoverlay: configure Raspberry PI IO\n", "```\n", "\n", "The Raspberry PI 4 pinout is as follows:\n", "\n", "```\n", " TXD RXD CTS RTS Board Pins\n", "uart0 14 15 8 10\n", "uart1 14 15 8 10\n", "uart2 0 1 2 3 27 28 (I2C)\n", "uart3 4 5 6 7 7 29\n", "uart4 8 9 10 11 24 21 (SPI0)\n", "uart5 12 13 14 15 32 33 (gpio-fan)\n", "```\n", "\n", "```{figure} figures/pi4-pinout.png\n", ":width: 800px\n", "\n", "Raspberry PI 4 pin assignment\n", "```" ] }, { "cell_type": "markdown", "id": "f766d2c7-5db9-466a-8648-8a8028ca6335", "metadata": {}, "source": [ "## Customize MicroPython\n", "\n", "We use a special MicroPython VM with the following customizations:\n", "\n", "* STM32 pin assignments\n", "* floats not stored on heap\n", "\n", "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. {numref}`Figure %s ` shows the pin assignment used for this project.\n", "\n", "```{figure} figures/stm32_pinout.png\n", ":name: stm32-pinout\n", "\n", "STM32 pin assignments (pdf)\n", "```\n", "\n", "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.\n", "\n", "### MicroPython Source" ] }, { "cell_type": "code", "execution_count": null, "id": "8d9833f3-9670-4308-89b7-53b71da18878", "metadata": { "tags": [ "hide-output" ] }, "outputs": [], "source": [ "%%bash\n", "\n", "# interpreter\n", "cd $IOT/mp\n", "if [ ! -d micropython ]\n", "then\n", " git clone git@github.com:micropython/micropython.git\n", "else\n", " cd micropython\n", " git checkout master\n", " git pull\n", " git merge master\n", "fi\n", "\n", "# library\n", "cd $IOT/mp\n", "if [ ! -d micropython-lib ]\n", "then\n", " git clone git@github.com:micropython/micropython-lib.git\n", "else\n", " cd micropython-lib\n", " git checkout master\n", " git pull\n", " git merge master\n", "fi" ] }, { "cell_type": "markdown", "id": "7ba22356-4b26-40f4-bb62-1a3cca0a92bd", "metadata": {}, "source": [ "### Compile" ] }, { "cell_type": "code", "execution_count": null, "id": "c22c440b-9a96-4f6b-806c-e4d5c6500e39", "metadata": { "tags": [ "hide-output" ] }, "outputs": [], "source": [ "%%service arm32\n", "\n", "cd $IOT/mp/micropython/ports/stm32\n", "cp -rf ../../../boards/MOTOR_HAT boards\n", "make submodules\n", "make BOARD=MOTOR_HAT clean\n", "make BOARD=MOTOR_HAT USER_C_MODULES=../../../modules" ] }, { "cell_type": "markdown", "id": "579a6b5b-aceb-4af6-ada0-5d2a5ada8628", "metadata": {}, "source": [ "### Flash\n", "\n", "Install stm32 flasher:" ] }, { "cell_type": "code", "execution_count": null, "id": "e71fca2f-2eb1-40e1-84e9-925459ce4ffb", "metadata": {}, "outputs": [], "source": [ "%%bash\n", "\n", "cd /tmp\n", "git clone https://git.code.sf.net/p/stm32flash/code stm32flash-code\n", "cd stm32flash-code\n", "sudo make install" ] }, { "cell_type": "code", "execution_count": null, "id": "dccc50c8-9e96-4530-a23f-88cec60668f1", "metadata": {}, "outputs": [], "source": [ "%%host\n", "\n", "import stm32\n", "stm32.flash(info_only=True)" ] }, { "cell_type": "markdown", "id": "f80168a4-9add-435d-9e5b-94e5e544e641", "metadata": {}, "source": [ "### REPL" ] }, { "cell_type": "code", "execution_count": 1, "id": "fc649927-34fd-43ea-9f54-a03289c54fcb", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[46m\u001b[30mConnected to 2c:00:29:00:09:50:52:42:4e:30:39:20 @ serial:///dev/ttyAMA1\u001b[0m\n", "pyboard\n" ] } ], "source": [ "%connect serial:///dev/ttyAMA1\n", "import sys\n", "print(sys.platform)" ] }, { "cell_type": "markdown", "id": "f68a7b7e-f40d-41cb-95c1-697c6f8b101d", "metadata": {}, "source": [ "## STM32 from Pi" ] }, { "cell_type": "markdown", "id": "55275b58-5669-47e2-9b85-82c27518bfff", "metadata": {}, "source": [ "`stm32.py` provides a number of convenience functions for accessing the STM32 from the Raspberry PI:" ] }, { "cell_type": "code", "execution_count": 1, "id": "6aed10ae-89fd-41ab-be18-21ecfa5ef58c", "metadata": { "collapsed": true, "jupyter": { "outputs_hidden": true }, "tags": [ "hide-output" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "from iot_device.pydevice import Pydevice\n", "from iot_device import DeviceRegistry, RemoteError\n", "from serial import Serial\n", "from gpiozero import LED as Pin\n", "import asyncio, subprocess, os, time\n", "\n", "\n", "def hard_reset(boot_mode=False):\n", " \"\"\"Hard reset STM32. Same as pressing reset button.\n", "\n", " @param boot_mode: bool Start in \"dfu\" boot-mode (default False).\n", " \"\"\"\n", " with Pin(21) as nrst, Pin(27) as boot0:\n", " if boot_mode:\n", " boot0.on()\n", " else:\n", " boot0.off()\n", " time.sleep(0.1)\n", " nrst.off()\n", " time.sleep(0.1)\n", " nrst.on()\n", " # let boot process finish\n", " time.sleep(1)\n", "\n", "def _flash_bin(address, firmware, dev, info_only):\n", " \"\"\"Flash helper. Used by flash method.\"\"\"\n", " if info_only:\n", " cmd = ['stm32flash', dev]\n", " else:\n", " cmd = ['stm32flash', '-v', '-S', address, '-w', firmware, dev]\n", " process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\n", " stdout, stderr = process.communicate()\n", " print(stdout.decode())\n", " if len(stderr) > 0:\n", " print(f\"***** {stderr.decode()}\")\n", "\n", "def flash(firmware_dir='$IOT/mp/micropython/ports/stm32/build-MOTOR_HAT/', dev='/dev/ttyAMA2', info_only=False):\n", " \"\"\"Flash MicroPython VM.\n", "\n", " @param firmware_dir Location of firmware.\n", " @param dev Device port.\n", " @param info_only Dry run if True (default False).\n", " \"\"\"\n", " dir = os.path.expandvars(firmware_dir)\n", " hard_reset(boot_mode=True)\n", " _flash_bin('0x08000000', os.path.join(dir, 'firmware0.bin'), dev, info_only)\n", " _flash_bin('0x08020000', os.path.join(dir, 'firmware1.bin'), dev, info_only)\n", " hard_reset(boot_mode=False)\n", "\n", "def exec(cmd, dev='serial:///dev/ttyAMA1'):\n", " \"\"\"Execute MicroPython code on STM32.\n", "\n", " @param cmd: string Code.\n", " \"\"\"\n", " registry = DeviceRegistry()\n", " registry.register(dev)\n", " with registry.get_device(dev) as repl:\n", " res = repl.exec(cmd)\n", " try:\n", " res = res.decode()\n", " except:\n", " pass\n", " return res\n", "\n", "def exec_no_follow(cmd, dev='/dev/ttyAMA1'):\n", " \"\"\"Execute MicroPython code on STM32 & do not wait for result.\"\"\"\n", " with Serial(dev, 115200, timeout=0.5, write_timeout=2, exclusive= True) as serial:\n", " pyd = Pydevice(serial)\n", " pyd.enter_raw_repl()\n", " pyd.exec_raw_no_follow(cmd)\n", " time.sleep(0.2)\n", " while serial.in_waiting:\n", " data = serial.read(serial.in_waiting)\n", " try:\n", " data = data.decode()\n", " except:\n", " pass\n", " print(f\"*** MCU: {data}\")\n", " time.sleep(0.1)\n", "\n", "async def async_exec(cmd, dev='/dev/ttyAMA1', pause=0.1):\n", " \"\"\"Asynchronous exec.\n", "\n", " @param cmd: string Code.\n", " @param pause: float Interval checking for output [seconds].\n", " \"\"\"\n", " with Serial(dev, 115200, timeout=0.5, write_timeout=2, exclusive=False) as serial:\n", " pyd = Pydevice(serial)\n", " pyd.enter_raw_repl()\n", " pyd.exec_raw_no_follow(cmd)\n", " while True:\n", " if serial.in_waiting:\n", " data = serial.read(serial.in_waiting)\n", " try:\n", " data = data.decode()\n", " except:\n", " pass\n", " print(f\"MCU: {data}\")\n", " await asyncio.sleep(0)\n", " else:\n", " await asyncio.sleep(pause)\n", "\n", "def rsync(dry_run=True, dev='serial:///dev/ttyAMA1'):\n", " registry = DeviceRegistry()\n", " registry.register(dev)\n", " with registry.get_device(dev) as repl:\n", " repl.rsync(data_consumer=lambda x: print(x, end=''), dry_run=dry_run)\n", "\n", "def rlist(dev='serial:///dev/ttyAMA1'):\n", " registry = DeviceRegistry()\n", " registry.register(dev)\n", " with registry.get_device(dev) as repl:\n", " repl.rlist(data_consumer=lambda x: print(x, end=''), show=True)\n", "\n", "def supply_voltage():\n", " \"\"\"Report unregulated supply voltage in [V] (nominally 12V).\"\"\"\n", " return float(exec(\n", "\"\"\"\n", "from pyb import ADC\n", "\n", "adc = pyb.ADC('V12_DIV')\n", "print(0.00655233*adc.read())\n", "\"\"\"))\n", "\n", "def power_off(delay=10):\n", " \"\"\"Turn off 5V power supply to Raspberry PI & STM32.\n", "\n", " @param delay: float Delay in seconds before turning power off.\n", "\n", " Warning: make sure Raspberry PI is shutdown before calling this!\n", " \"\"\"\n", " print(f\"shutting down ...\")\n", " exec_no_follow(\n", "f\"\"\"\n", "from pyb import Pin\n", "from time import sleep\n", "\n", "# declaring as input first sets the initial value after configuring as output\n", "shut_dn = Pin('PWR_EN', mode=Pin.IN, pull=Pin.PULL_UP)\n", "shut_dn.value(1)\n", "shut_dn = Pin('PWR_EN', mode=Pin.OUT_OD)\n", "sleep({delay})\n", "shut_dn.value(0)\n", "\"\"\")\n", " os.system(\"sudo halt\")\n" ] } ], "source": [ "!cat $IOT_PROJECTS/robot/code/rpi/stm32.py" ] }, { "cell_type": "code", "execution_count": 34, "id": "72f08ff5-ae55-4ddb-8a00-0c06c9341b44", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "exec(print(4+7, end=\"\")): 11\n", "supply voltage: 10.1V\n" ] } ], "source": [ "%%host\n", "\n", "# setup path\n", "import sys, os\n", "sys.path.append(os.path.join(os.getenv('IOT_PROJECTS'), 'robot/code/rpi'))\n", "\n", "import stm32\n", "\n", "cmd = 'print(4+7, end=\"\")'\n", "print(f\"exec({cmd}): {stm32.exec(cmd)}\")\n", "\n", "print(f\"supply voltage: {stm32.supply_voltage():.1f}V\")" ] }, { "cell_type": "markdown", "id": "929192cf-24f1-4514-80d2-5b34c614979a", "metadata": {}, "source": [ "## Device Configuration" ] }, { "cell_type": "code", "execution_count": 1, "id": "68d7be40-7f34-48cf-8825-8b044749c20c", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Writing /home/iot/iot49.org/docs/projects/devices/robot.yaml\n" ] } ], "source": [ "%%writefile $IOT_PROJECTS/devices/robot.yaml\n", "\n", "# motor controller etc.\n", "robot-stm32:\n", " # uid: 1c:00:26:00:09:50:52:42:4e:30:39:20\n", " uid: 2c:00:29:00:09:50:52:42:4e:30:39:20\n", " install-dir: /flash\n", " path: robot/code\n", " resources:\n", " - secrets.py:\n", " path: libs\n", " - state.py:\n", " path: robot/code/rpi/robot\n", " install-dir: /flash/lib/robot\n", " - pid.py:\n", " path: robot/code/rpi/robot\n", " install-dir: /flash/lib/robot\n", " - bno055:\n", " path: libs\n", " install-dir: /flash/lib\n", " - stm32\n", "\n", "# ble remote control\n", "robot-esp32:\n", " uid: 30:ae:a4:28:39:f0\n", " path: robot/code\n", " resources:\n", " - secrets.py:\n", " path: libs\n", " - esp32" ] }, { "cell_type": "code", "execution_count": 1, "id": "b3cca393-a9a0-4c69-9842-45bd667d9c11", "metadata": { "tags": [ "remove-output" ] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "\u001b[46m\u001b[30mConnected to robot-stm32 @ serial:///dev/ttyAMA1\u001b[0m\n", "\u001b[31mDELETE /flash/lib/state.py\n", "\u001b[0m\u001b[32mADD /flash/lib/duty_control.py\n", "\u001b[0m\u001b[34mUPDATE /flash/lib/comm.py\n", "\u001b[0m\u001b[34mUPDATE /flash/lib/controller.py\n", "\u001b[0m" ] } ], "source": [ "%connect serial:///dev/ttyAMA1\n", "%rsync" ] }, { "cell_type": "code", "execution_count": null, "id": "c388c085-2580-418d-a6eb-2b72ee77845f", "metadata": { "tags": [ "remove-cell" ] }, "outputs": [], "source": [ "% required for files to be copied to output directory\n", "\n", "![](figures/stm32_pinout.pdf)" ] } ], "metadata": { "kernelspec": { "display_name": "IoT", "language": "python", "name": "iot_kernel" }, "language_info": { "codemirror_mode": { "name": "python", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "pygments_lexer": "python3", "version": "3" } }, "nbformat": 4, "nbformat_minor": 5 }