Arduino Uno Q is interesting (one would say weird) board with 2 chips: MCU and MPU. Disadvantage is that pins are always connected to only one of these, so you can’t control them from another chip. Most importantly, pins on headers are connected to STM32 MCU and thus can’t be controlled directly from Python scripts running on MPU. Official way is to setup communication between chips and drive pins from STM32. Driving pins directly from MPU is simply impossible. Or maybe it is not?
Without ballast around, here are bash command you can run on MPU to control pins directly:
# pinMode(LED3_R, OUTPUT)
/opt/openocd/bin/openocd \
-d0 -s /opt/openocd/ \
-f openocd_gpiod.cfg \
-c 'init; write_memory 0x42021C00 32 [ expr "([read_memory 0x42021C00 32 1] & ~(0x3 << (2 * 10))) | (0x1 << (2 * 10))" ]; exit'
# pinMode(LED3_R, INPUT)
/opt/openocd/bin/openocd \
-d0 -s /opt/openocd/ \
-f openocd_gpiod.cfg \
-c 'init; write_memory 0x42021C00 32 [ expr "[read_memory 0x42021C00 32 1] | (0x3 << (2 * 10))" ]; exit'
# digitalWrite(LED3_R, LOW)
/opt/openocd/bin/openocd \
-d0 -s /opt/openocd/ \
-f openocd_gpiod.cfg \
-c 'init; write_memory 0x42021C18 32 [ expr "1 << (10 + 16)" ]; exit'
# digitalWrite(LED3_R, HIGH)
/opt/openocd/bin/openocd \
-d0 -s /opt/openocd/ \
-f openocd_gpiod.cfg \
-c 'init; write_memory 0x42021C18 32 [ expr "1 << (10 + 0)" ]; exit'
# Almost digitalRead(LED3_R)
/opt/openocd/bin/openocd \
-d0 -s /opt/openocd/ \
-f openocd_gpiod.cfg \
-c 'init' -c 'read_memory 0x42021C10 32 1' -c 'exit'
I think experienced MCU devs know what I did there.
The same can be done for any other pin. Just numbers will change.
Python Library
I wrapped it in Python library. It can be run on MPU, but currently can’t be run in Arduino App Lab, because App Lab wraps Python programs in Docker and while it is possible to copy required things to container, it is not currently possible to expose /dev/gpiochip1 to container that openocd commands mentioned above rely on. I asked about that on Arduino Forum, so if they introduce some support for passing /dev/gpiochip1 to container, it becomes possible.
How it works?
All commands above connect to the STM32 as a debugger. They do not stop processor and just issue read/write request on internal bus to the GPIO peripheral. Every port has 13 registers which control mode and output value of GPIO port (and possibly few other properties). These commands simply writes / reads these registers on behalf of debugger.
First, I needed convert pin number to real STM32 pin. It is available in pinout doc available on Arduino Docs page. LED3_R is connected to PH10 on STM32.
First magic number in command is register address. Register bases are in STM32 Reference Manual (doc RM0456, rev 6). Peripheral base 0x42021C00 for GPIOH is described at page 147.
For setting pin direction, GPIOH_MODER register is used. It is described on page 633 in RM0456, rev 6 document. It is at offset 0 compared to base mentioned before. Bitmask in the command clears these bits for 10th GPIOH pin. Because configuration for each pin occupy 2 bits in this register, binary pattern shits are doubled twice.
For setting output value, GPIOH_BSSR register is used. It is documented at page 636. It is at offset 0x18 relative to GPIOH register base, so it is at 0x42021C00 + 0x18 which is 0x42021C18. This register can atomically set pin value to either LOW or HIGH without influencing other pins on GPIO H.
Reading input pin is only highlighted. It is done by reading GPIOH_IDR register. After reading, you get hexadecimal number which will represent input values of all pins on GPIO H. For getting single bit, you need to mask bit for specified bit manually. Python library shared above does it.