Four hours today and four hours tomorrow, what is the plan?

1. Goals

  • Inventory what you’ve learned and target what you really want to see happen.

  • Whirlwind tour of the full stack[1] from semiconductors to (Micro)Python.

2. Python down to the machine

First thing today![2]

30 min of "lecture" time
Log on to your own PC and run Thonny.

2.1. Digital electronics

Semiconductors are not conductors and not insulators. Their electrical / mechanical (!) behavior can be changed by doping the material with impurities (n-type or p-type).

2.2. RP2040 chip

We start at the some of the lowest levels of what’s going on.

Raspberry Pi (the company) makes several products:

  • Full Linux-based single-board computers:

    • Raspberry Pi Zero 2 W

    • Raspberry Pi 3 Model B+

    • Raspberry Pi 4 Model B

    • Raspberry Pi 5

    • Raspberry Pi Compute Module 4

    • Raspberry Pi Compute Module 4S

    • Raspberry Pi Compute Module 3+

  • RP2040 microcontroller placed on boards called:

    • Raspberry Pi Pico

    • Raspberry Pi Pico W

pi pico family

rp2040 block
Figure 1. RP2040 internal block diagram

What is inside the RP2040?

  • CPU vs MCU vs "an Arduino" vs PC / Laptop / Phone

  • peripherals interact with the outside world

  • a CPU architecture: ARM Cortex-Mx, Intel x86, RISC-V, AMD, a hundred more for microcontrollers.

Going up the levels from digital electronics to Python:

  • Machine code

  • Assembly language

  • Higher-level languages

2.3. Python language

  • Compiled languages vs. interpreted

    • Compiled to machine code: C and C++. Arduino uses C++ with magic helpers.

    • Interpreted: Python, JavaScript, Ruby, Java

But the machine can only run machine code!

  • Python VM — virtual machine, runs bytecode. (this is how Python runs on your PC and on the Pi Pico without change)

  • Python → compiled to bytecode → bytecode runs on Virtual Machine

  • → which runs on the RP2040’s processor → which executes its ARM Cortex-M0+ machine code instructions.

3. Touchscreen demo

Pi Pico Touchscreen display with buttons.

Python code in main.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# built-in packages
from collections import namedtuple
from math import exp, pi


# MicroPython-specific packages
import utime
from machine import Pin
from machine import Timer
from machine import enable_irq, disable_irq
import micropython
micropython.alloc_emergency_exception_buf(100)


# Local packages
from LCD_3inch5 import LCD_3inch5

from loadcell import Loadcell

import gui
from gui import BarMeter
from gui import CenterBarMeter
from gui import Button
from gui import Point


# Load cell scale factor
LOADCELL_SCALE_N = 1

# bar meter
BAR_MIN = 0
BAR_MAX = 300

# deviation meter
DEVIATION_MIN = -20
DEVIATION_MAX = +20

# HX711 samples at 10 Hz
SAMPLE_RATE = 2

# FILTER_CUTOFF = 0.1
FILTER_CUTOFF = 0.1

TIME_CONSTANT = 1 - exp(-2 * pi * FILTER_CUTOFF / SAMPLE_RATE)
print(TIME_CONSTANT)


HX_OUT_PIN = Pin(27, Pin.IN, pull=Pin.PULL_DOWN)
HX_SCK_PIN = Pin(26, Pin.OUT)




_interrupt_state = None

def eint():
    enable_irq(_interrupt_state)

def dint():
    _interrupt_state = disable_irq()






scale = Loadcell(HX_SCK_PIN, HX_OUT_PIN, 128, TIME_CONSTANT)
# scale = LoadcellHX711(HX_SCK_PIN, HX_OUT_PIN, 128, TIME_CONSTANT)
scale.SCALE_N = LOADCELL_SCALE_N
scale.SCALE_N = float(2**16) / 300
scale.value = 150


#
# regularly sample the force sensor
#
sampler = Timer(mode=Timer.PERIODIC,
                freq=SAMPLE_RATE,
                callback=lambda t: scale.read())


def main():
    LCD = LCD_3inch5()
    LCD.bl_ctrl(100)
    LCD.fill(gui.BLACK)
    LCD.show_up()

    METER_MAX = 300

    reference = 150

    bar = BarMeter(
        LCD,
        Point(0, 0),
        width=320,
        height=150,
        label="Force (N)",
        label_align='left',
        draw_limits=True)
    bar.value_min = BAR_MIN
    bar.value_max = BAR_MAX

    buttonSub = Button(
        LCD,
        Point(0, 150),
        width=60,
        height=240 - 150,
        label="-5",
        label_align='center')

    deviation = CenterBarMeter(
        LCD,
        Point(60, 150),
        width=320 - 60 - 60,
        height=240 - 150,
        label="",
        label_align="left",
        text_color=gui.RED,
        draw_limits=True)
    deviation.value_min = DEVIATION_MIN
    deviation.value_max = DEVIATION_MAX

    buttonAdd = Button(
        LCD,
        Point(320-60, 150),
        width=60,
        height=240 - 150,
        label="+5",
        label_align='center')

    buttonTare = Button(
        LCD,
        Point(0, 0),
        width=50,
        height=40,
        label="Tare",
        label_align='center')

    while True:
        if val := scale.newtons:
            bar.label = f"Force: {val:5.1f} N"
            bar.draw(val, reference)
            deviation.draw(val - reference)

        tp = LCD.touch_get()
        if tp is not None:
            # swap
            tp  = Point(tp.y, tp.x)
            x = 320 - int((tp.x - 430) * 320 / 3270)
            y = int((tp.y - 430) * 240 / 3270)
            tp = Point(x, y)

        btn, edge = buttonSub.handle(tp)
        if edge > 0:
            reference -= 5

        btn, edge = buttonAdd.handle(tp)
        if edge > 0:
            reference += 5

        btn, edge = buttonTare.handle(tp)
        if edge > 0:
            scale.tare()
            # print(f"hx_offset: {scale.OFFSET}")

        deviation.label = f"{reference:5.1f}"

        LCD.show_up()
        utime.sleep(0.1)


if __name__ == "__main__":
    main()

4. Multiple tasks at the “same time”

First example async_blink.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# Built-in modules
import asyncio


# MicroPython-specific modules
from machine import Pin


# Local custom modules


# Setup hardware configuration
onboard_led = Pin(25, Pin.OUT)



##############################################
#
# TASK
#
# Blink an LED with a certain period
# NOTE:
#   - async in front of the function definition
#   - sleeping is "await asyncio.sleep_ms(...)
#
async def blink(led, period_ms):
    print("blink() started.")
    # do this forever
    while True:
        # same as led.value(1), but nicer to read
        led.on()
        
        # A normal time.sleep(0.005) or time.sleep_ms(5) will also wait,
        # but those hog the processor and don't allow it to do anything
        # else besides do nothing very quickly.
        #
        # This version says "come back to me in 5ms, AND releases the CPU
        # to do other things in the meantime.
        await asyncio.sleep_ms(5)
        
        led.off()
        
        # Same idea: stop *this* function, but let other things take over
        # until the time is up.
        await asyncio.sleep_ms(period_ms)
        
    print("blink() finished.")


# This async task *starts* two other tasks, for a total of 3
# "things" going at once.  The "main()" task's role is simply to start
# the "real" work, after which its work is done.
async def main(led1, led2):
    print("Starting tasks.")
    task1 = asyncio.create_task(blink(led1, 700))
    task2 = asyncio.create_task(blink(led2, 400))
    
    # hold here for a while (how long?)
    await asyncio.sleep_ms(10_000)
    print("Stopping tasks.")
    # then exits


# Above is all setup of variables and functions.
# Code that makes things happen begins now.

# Line below never finishes because blink() has a "while True:"
# (Control-C to stop, or click Stop)
asyncio.run(blink(onboard_led, 500))
print("After run(blink()).")


# Comment out the above lines so that the following line will run
# What does it do?
asyncio.run(main(onboard_led, onboard_led))

print("main() has finished.")

5. motor module

First, open up the motors demo code and read how it works:

  • S: / COE / COEguest Shared / Robotics / beetlebot / 2. Python Tutorials / 2.Python_Codes / Project_06_Motor_Drive_And_Speed_Regulation

(Take a minute to actually read that code.)


Module motors.py code
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# MicroPython-specific modules
from machine import Pin
from machine import PWM

# Right wheel
pin1 = Pin(14, Pin.OUT)

pin2 = PWM(Pin(16))
pin2.freq(50)


# Left wheel
pin3 = Pin(15, Pin.OUT)

pin4 = PWM(Pin(17))
pin4.freq(50)



def forward():
    pin1.value(0)
    pin2.duty_u16(50_000)
    pin3.value(0)
    pin4.duty_u16(50_000)


def back():
    pin1.value(1)
    pin2.duty_u16(10_000)
    pin3.value(1)
    pin4.duty_u16(10_000)


def left():
    pin1.value(0)
    pin2.duty_u16(50_000)
    pin3.value(1)
    pin4.duty_u16(32_768)


def right():
    pin1.value(1)
    pin2.duty_u16(32_768)
    pin3.value(0)
    pin4.duty_u16(50_000)


def stop():
    pin1.value(0)
    pin2.duty_u16(0)
    pin3.value(0)
    pin4.duty_u16(0)

Main code demonstrating motors module
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# Built-in modules
import time

# MicroPython-specific modules
from machine import Pin, PWM

# Custom modules
import motors



while True:
    motors.forward()
    time.sleep(2)

    motors.back()
    time.sleep(2)

    motors.left()
    time.sleep(2)

    motors.right()
    time.sleep(2)

    motors.stop()
    time.sleep(2)
Blink and Motor driving at the same time!
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# Built-in modules
import asyncio


# MicroPython-specific modules
from machine import Pin


# Local custom modules
import motors


# Setup hardware configuration
onboard_led = Pin(25, Pin.OUT)



##############################################
#
# TASK
#
# Blink an LED with a certain period
# NOTE:
#   - async in front of the function definition
#   - sleeping is "await asyncio.sleep_ms(...)
#
async def blink(led, period_ms):
    print("blink() started.")
    # do this forever
    while True:
        # same as led.value(1), but nicer to read
        led.on()

        # A normal time.sleep(0.005) or time.sleep_ms(5) will also wait,
        # but those hog the processor and don't allow it to do anything
        # else besides do nothing very quickly.
        #
        # This version says "come back to me in 5ms, AND releases the CPU
        # to do other things in the meantime.
        await asyncio.sleep_ms(5)

        led.off()

        # Same idea: stop *this* function, but let other things take over
        # until the time is up.
        await asyncio.sleep_ms(period_ms)

    print("blink() finished.")



##############################################
#
# TASK
#
# Drive a specifc path, repeatedly.
#
async def drive_path():
    while True:
        motors.forward()
        await asyncio.sleep(2)

        motors.back()
        await asyncio.sleep(2)

        motors.left()
        await asyncio.sleep(2)

        motors.right()
        await asyncio.sleep(2)

        motors.stop()
        await asyncio.sleep(2)


# This async task *starts* two other tasks, for a total of 3
# "things" going at once.  The "main()" task's role is simply to start
# the "real" work, after which its work is done.
async def main(led):
    print("Starting tasks.")
    task1 = asyncio.create_task(blink(led, 700))
    task2 = asyncio.create_task(drive_path())

    # hold here for a while (how long?)
    await asyncio.sleep_ms(30_000)
    print("Stopping tasks.")
    # then exits
    #
    # Can you make this go for *forever* ???


# Above is all setup of variables and functions.
# Code that makes things happen begins now.


# What does the following do?
asyncio.run(main(onboard_led))

print("main() has finished.")

1. There is a term called full stack developer that typically means the person or role deals with servers, code running on servers, and code running on the user’s web browser. We are going all the way down the stack to the silicon_. Remember that, if you have a sinking feeling, the "house" really is build on sand ;)
2. or it won’t happen…​