Files
microbit-micropython-cookbook/README.md
2020-06-14 02:33:56 +08:00

13 KiB

micro:bit MicroPython Cookbook (Updating)

1

BBC micro:bit MicroPython documentation

This is the collection of my notes, tricks and experiments about BBC micro:bit and MicroPython.

Easer Eggs

Enter the following codes in REPL:

import this
import love
import antigravity

The result from import this is a version of Zen of Python and import antigravity is from original Python easter egg.

Also you can try

this.authors()
love.badaboom()

Some Lesser Known Facts

Since both Python and MicroPython are interpreted languages, they eat a lot of memory. Also, the hex file generated by micro:bit Python editors are consisted of 2 parts: the MicroPython firmware (up to 248 KB) and user's script (up to only 8 KB). See Firmware Hex File. Which means it's less likely to build bigger projects with micro:bit's MicroPython.

One way to "minimize" your script size is to use 1-space indents instead of 4.

micro:bit's MicroPython is based on Python 3.4. Which means many built-in Python advanced feaetures, like string.format(), list comprehension, list slice, variable unpacking, lambda function, decorators, generators, @classmethod, @staticmethod, etc. can be used as well.

Also, about how micro:bit get its own version of MicroPython: The Story of MicroPython on the BBC micro:bit by Nicholas H. Tollervey, who also created the Mu editor.

Editor of Choice

The official Python online editor does not need installation and can be used anywhere with Internet and Chrome web browser. Support Web-USB. It's ok to use, really.

Personally, I would perfer Mu editor for any beginners. It has code check, (limited) auto-complete and can automatically detect/upload code to your micro:bit.

If you have experiences with MicroPython with ESP8266/ESP32 or CircuitPython, you can consider Thonny which allows you to access micro:bit's REPL directly without having to upload hex file.

Why You Shouldn't Use * For Import

The following import statement

from microbit import *

is a bad idea. This imports everything of the microbit module even you don't need many of the features and wastes extra memory.

Instead, you should only import sub-modules you are going to use:

from microbit import pin0, display, sleep

How Much Memory Left?

from micropython import mem_info

print(mem_info(1))

You can also try to turn on garbage collection if the memory is almost full:

import gc

gc.enable() # auto memory recycle
gc.collect() # force memory recycle

Classic Blinky

from microbit import display, sleep

while True:
    display.set_pixel(0, 0, 9)
    sleep(1000)
    display.set_pixel(0, 0, 0)
    sleep(1000)

Fill LED Display

Light up every LEDs. Use fillScreen() as default.

from microbit import display, Image, sleep

def fillScreen(b = 9):
    f = (str(b) * 5 + ":") * 5
    display.show(Image(f[:len(f)-1]))


while True:
    
    for _ in range(2):
        fillScreen()
        sleep(100)
        fillScreen(0)
        sleep(100)
    
    for i in range(9):
        fillScreen(i)
        sleep(50)
        
    for i in reversed(range(9)):
        fillScreen(i)
        sleep(50)

A More Convenient Pin Class

Make a Pin class to "rename" existing pin methods.

from microbit import pin0, pin2, sleep

class Pin:
    
    __slots__ = ["pin"]
    
    def __init__(self, pin):
        self.pin = pin
    
    def set(self, value):
        self.pin.write_digital(value)
    
    def setPWM(self, value):
        self.pin.write_analog(value)
    
    def get(self):
        self.pin.set_pull(self.pin.PULL_DOWN)
        return self.pin.read_digital()
    
    def pressed(self):
        self.pin.set_pull(self.pin.PULL_UP)
        return not self.pin.read_digital()
        
    def getADC(self):
        return self.pin.read_analog()


led = Pin(pin0)
button = Pin(pin2)

while True:
    led.set(button.pressed())
    sleep(50)

Another Version of Pin Class

Use namedtuple as a simple Pin class. Save more memory than regular class.

from microbit import pin0, pin2, sleep
from ucollections import namedtuple

Pin = namedtuple('Pin', ['set', 'get'])

def setPin(pin, pull_up=False):
    pin.set_pull(pin.PULL_UP if pull_up else pin.PULL_DOWN)
    return Pin(pin.write_digital, pin.read_digital)


led = setPin(pin0)
button = setPin(pin2, pull_up=True)

while True:
    led.set(not button.get())
    sleep(50)

LED Blinky Without Using Sleep

from microbit import display
import utime

delay = 1000
since = utime.ticks_ms()

while True:
    
    now = utime.ticks_ms()
    
    if utime.ticks_diff(now, since) >= delay:
        display.set_pixel(0, 0, 9 if display.get_pixel(0, 0) == 0 else 0)
        since = utime.ticks_ms()

This method would be useful if you want to do something at different intervals:

from microbit import display
import utime

delay1, delay2 = 1000, 300
since1, since2 = utime.ticks_ms(), utime.ticks_ms()

while True:
    
    now = utime.ticks_ms()
    
    if utime.ticks_diff(now, since1) >= delay1:
        display.set_pixel(0, 0, 9 if display.get_pixel(0, 0) == 0 else 0)
        since1 = utime.ticks_ms()
    
    if utime.ticks_diff(now, since2) >= delay2:
        display.set_pixel(4, 4, 9 if display.get_pixel(4, 4) == 0 else 0)
        since2 = utime.ticks_ms()

LED Bar Graph

A 25-level LED progress bar.

from microbit import display, sleep

def plotBarGraph(value, maxValue, brightness=9):
    bar = value / maxValue
    valueArray = ((0.96, 0.88, 0.84, 0.92, 1.00), 
                  (0.76, 0.68, 0.64, 0.72, 0.80),
                  (0.56, 0.48, 0.44, 0.52, 0.60), 
                  (0.36, 0.28, 0.24, 0.32, 0.40), 
                  (0.16, 0.08, 0.04, 0.12, 0.20))
    for y in range(5):
        for x in range(5):
            display.set_pixel(x, y, 
                brightness if bar >= valueArray[y][x] else 0)


while True:
    lightLevel = display.read_light_level()
    plotBarGraph(lightLevel, 255) # or plotBarGraph(lightLevel, 255, 9)
    sleep(50)

Since read_light_level() uses LEDs themselves as light sensors (see this video), in this example a short delay is added, but the LED screen would still flicker a bit.

Servo Control

from microbit import pin0, sleep

def servoWrite(pin, degree):
    pin.set_analog_period(20)
    pin.write_analog(round((degree * 92 / 180 + 30), 0))


servoPin = pin0

while True:
    servoWrite(servoPin, 0)
    sleep(1000)
    servoWrite(servoPin, 180)
    sleep(1000)

Do not use servos and buzzers at the same time. They require different PWM frequencies and most microcontrollers can only set one frequency accross all pins at a time.

Also: micro:bit's power output may just (barely) enough to power a single SG90 mini servo. External power supply recommended.

Value Mapping

Translate a value in a range to its corresponding value in anoher range. Borrowed from here.

def translate(value, leftMin, leftMax, rightMin, rightMax):
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin
    valueScaled = float(value - leftMin) / float(leftSpan)
    return rightMin + (valueScaled * rightSpan)

Get Pitch and Roll Degrees

These function cannot tell if the board is facing up or down. Probably need to use accelerometer.get_z() for that.

from microbit import accelerometer, sleep
import math

def rotationPitch():
    return math.atan2(
            accelerometer.get_y(), 
            math.sqrt(accelerometer.get_x() ** 2 + accelerometer.get_z() ** 2)
            ) * (180 / math.pi)

def rotationRoll():
    return math.atan2(
            accelerometer.get_x(), 
            math.sqrt(accelerometer.get_y() ** 2 + accelerometer.get_z() ** 2)
            ) * (180 / math.pi)


while True:
    print("Pitch:", rotationPitch(), " / roll:", rotationRoll())
    sleep(100)

NeoPixel Rainbow/Rotation

This code needs at least 3 LEDs in the NeoPixel chain. Of course, you can set a number (much) higher than actual LEDs to get smooth rainbow effects.

from microbit import pin0, sleep
from neopixel import NeoPixel
from micropython import const

led_num = const(12)
led_maxlevel = const(64) # max 255
led_pin = pin0

np = NeoPixel(led_pin, led_num)

def showRainbow():
    change_amount = int(led_maxlevel / (led_num / 3))
    index = (0, int(led_num / 3), int(led_num / 3 * 2))
    for i in range(led_num):
        color = [0, 0, 0]
        for j in range(3):
            if abs(i - index[j]) <= index[1]:
                color[j] = led_maxlevel - abs(i - index[j]) * change_amount
                if color[j] < 0:
                    color[j] = 0
        if i >= index[2]:
            color[0] = led_maxlevel - (led_num - i) * change_amount
            if color[0] < 0:
                color[0] = 0
        np[i] = tuple(color)
    np.show()

def ledRotate():
    tmp = np[led_num - 1]
    for i in reversed(range(1, led_num)): # clockwise
        np[i] = np[i - 1]
    np[0] = tmp
    np.show()


showRainbow()

while True:
    ledRotate()
    sleep(50)

HC-SR04 Ultrasonic Sensor

Get detected distance from HC-SR04/HC-SR04P sonar sensors. Set the parameter unit to 'cm' or 'inch'. External power supply recommended.

from machine import time_pulse_us
from utime import sleep_us
from microbit import pin1, pin2, display, Image, sleep

def sonar(trig_pin, echo_pin, unit='cm'):
    
    trig_pin.write_digital(0)
    sleep_us(2)
    trig_pin.write_digital(1)
    sleep_us(10)
    trig_pin.write_digital(0)
    
    while echo_pin.read_digital() == 0:
        pass
    
    duration = time_pulse_us(echo_pin, 1, 30000)
    
    if unit == 'cm':
        return duration / 2.0 * 0.03313
    elif unit == 'inch':
        return duration / 2.0 * 0.01304
    else:
        return duration


while True:
    
    distance = sonar(trig_pin=pin1, echo_pin=pin2, unit='cm')
    print(distance)
    
    if 2 <= distance <= 20:
        display.show(Image.YES)
    else:
        display.clear()
    
    sleep(100)

Calcualte Fibonacci Sequence

from microbit import display

Fibonacci_num = 42
a = 0
b = 1

for i in range(Fibonacci_num - 2):
    a, b = b, a + b

print(b)
display.scroll(b)

Calcuate a List of Prime Numbers

Prime numbers (except 2, 3) are either 6n - 1 or 6n + 1.

from microbit import display

limit = 50
primes = [2, 3]

for p in range(6, limit + 1, 6):
    for p_test in range(p - 1, p + 2, 2):
        p_test_is_prime = True
        for prime in primes:
            if p_test % prime == 0:
                p_test_is_prime = False
                break
        if p_test_is_prime:
            primes.append(p_test)

print(primes)
for prime in primes:
    display.scroll(prime)

Conway's Game of Life on 5x5 LED Display

The code would reset the micro:bit if there's no cell left or the cells are stable.

from microbit import display
from machine import reset
from random import randint

Born = '3'
Sustain = '23'

matrix = [bytearray((1 if randint(0, 2) == 0 else 0) 
            for _ in range(5)) for _ in range(5)]

def display_matrix():
    for i in range(5):
        for j in range(5):
            display.set_pixel(i, j, 9 if matrix[i][j] else 0)

def calculate_next_gen():
    global matrix
    matrix_buf = [bytearray(0 for _ in range(5)) for _ in range(5)]
    for i in range(5):
        for j in range(5):
            cell_num = 0
            for k in range(3):
                for l in range(3):
                    x = i + k - 1
                    y = j + l - 1
                    if x < 0:
                        x = 5 - 1
                    elif x >= 5:
                        x = 0 
                    if y < 0:
                        y = 5 - 1
                    elif y >= 5:
                        y = 0   
                    if matrix[x][y]:
                        cell_num += 1
            if not matrix[i][j]:
                matrix_buf[i][j] = 1 if str(cell_num) in Born else 0
            else:
                cell_num -= 1
                matrix_buf[i][j] = 1 if str(cell_num) in Sustain else 0
    matrix = matrix_buf

generation = 0
cell_count = 0
prev_cell_count = 0
cell_repeat = 0

while True:
    
    calculate_next_gen()
    cell_count = sum(map(sum, matrix))
    print(cell_count, 'cell(s)')
    display_matrix()
    
    if prev_cell_count == cell_count:
        cell_repeat += 1
    else:
        cell_repeat = 0
    prev_cell_count = cell_count
    
    if cell_count == 0 or cell_repeat >= 7:
        print('Resetting...')
        print('')
        reset()