MicroPython Workshop ==================== Getting started --------------- ### Operating systems, drivers... You might need drivers for the CP210x USB-to-serial converter, which you can get from [the Silicon Labs site][silabs]. On Windows, the device should show up as some COM port (e.g. COM3), on Linux as e.g. /dev/ttyUSB0, and on OSX as /dev/tty.SLAB_USBtoUART (not as /dev/tty.usbserial!) [silabs]: https://www.silabs.com/products/development-tools/software/usb-to-uart-bridge-vcp-drivers ### Installing MicroPython Get a recent build of Pycopy, a fork of MicroPython that we will be using for this workshop. A build of the current state as of 2019-11-24 is [here](firmware.bin). Afterwards, get `esptool` and use it to flash your board, using the correct port: ``` sh pip install esptool esptool.py --port /dev/ttyUSB0 erase_flash esptool.py --port /dev/ttyUSB0 --chip esp32 \ write_flash -z 0x1000 firmware.bin ``` You can now connect to your board's REPL with e.g. `screen` or `putty`: ``` sh screen /dev/ttyUSB0 115200 ``` In Putty, select "Serial" as connection type and use your COM port as port and 115200 as speed. You'll get a command prompt and be able to execute the first commands ``` python help() print("Hello, world") ``` A reference of all the default libraries can be found on [readthedocs.io](https://pycopy.readthedocs.io/en/latest/) ### File upload using `ampy` You can use `ampy` to manage files from the CLI: ``` sh pip install adafruit-ampy export AMPY_PORT=/dev/ttyUSB0 # List files ampy ls # Print file contents ampy get boot.py # Save a file ampy get boot.py boot.py # Create some example file echo "print('hello, world')" > hello.py # Run a local file, without uploading or saving it ampy run hello.py # Save it to the board ampy put hello.py # Delete it again ampy rm hello.py ``` The contents of `boot.py` are run each time the controller resets. It may contain instructions for setting up your network connection, local time, peripherals, etc. Make sure not to put an infinite loop in your `boot.py`. Hello, LED ---------- You can control the builtin LED, which is on pin 5: ``` python from machine import Pin led = Pin(5, Pin.OUT) # The LED is connected between GPIO 5 and 3.3 V, so # to allow current to flow and turn the LED on, you # need a lower voltage on your pin: led.value(0) # This is (in this case somewhat confusingly) equivalent to led.off() # or led(0) # These will all turn it off: led.value(1) led(1) led.on() # You can also use booleans: led.value(False) led.value(True) ``` ### Brightness control with PWM To control the brightness, we will be using [Pulse-Width Modulation][pwm], which quickly toggles the LED between on and off at some frequency, while varying the time it is on: ``` python from machine import PWM pwm = PWM(led) # Read the frequency pwm.freq() # Set it pwm.freq(1000) # Read and set the duty cycle pwm.duty() pwm.duty(1023) pwm.duty(512) pwm.duty(0) ``` Contrary to what some people with previous embedded programming experience might expect, the frequency here is not the frequency of the counter, but how often the LED toggles per second. The maximum duty cycle is normally 1023 (which applies 3.3 volts for 1023 out of 1024 cycles). For high frequencies this might change. Frequencies as low as 1 Hz are possible - you can not use floating point numbers as your PWM frequency. [pwm]: https://en.wikipedia.org/wiki/Pulse-width_modulation ### Reserved pins Some of the GPIOs are reserved for certain functions. Most of their numbers are not visible on the TTGO-T8 board: - UART: Pins 1 and 3 are TX/RX - visible as TXD and RXD - Flash: 6, 7, 8, 11, 16, 17. Messing with these will probably crash your program. - 34-39 are input only and do not have pullups. Only 34 and 35 are available on the board at all. All other pins can be freely used for any kind of digital input or output. Pins 2, 4, 12, 13, 14, 15 are connected the the micro-SD slot and can not be used while the SD card is being used. Digital and analog input ------------------------ Use `machine.Pin` for digital input ``` python from machine import Pin pin = Pin(4, Pin.IN) pin.value() # You can use pullups: pin = Pin(4, Pin.IN, Pin.PULL_UP) pin = Pin(4, Pin.IN, Pin.PULL_DOWN) ``` When connecting a button between a pin and the supply voltage (3V3 pins), use a pull-down - then the normal state will read as 0, and switch to 1 when the button is pressed. You can also connect the button between ground (GND) and a pin and use a pull-up, now the pin will normally read as 1 and switch to 0 when pressed. For analog input, use `machine.ADC`. By default, the ADC has an input range of 1.0 V, but this can be changed to up to 3.6 V using `ADC.atten`. Available pins are 32 through 35. ``` python from machine import Pin, ADC pin = ADC(Pin(32, Pin.IN, None)) pin.read() # You can set the resolution, it defaults to 12 bits pin.width(ADC.WIDTH_10BIT) pwm = machine.PWM(Pin(5, Pin.OUT)) while True: pwm.duty(1023 - pin.read()) ``` Timers ------ MicroPython on the ESP32 has virtual timers and 4 hardware timers. You can use a lot more virtual timers, but they are more prone to jitter. In either case, you can not allocate memory in a timer, as they are executed from an interrupt context. Timers execute a callback, which takes the timer as an argument ``` python from machine import Timer, Pin import utime # Create a virtual timer with ID -1. # IDs >= 0 are hardware timers. timer = Timer(-1) # Create variables used by the interrupt data = [0] * 32 index = 0 # Use the normal LED as output led = Pin(5, Pin.OUT) def callback(timer): global data, index # Store the time since startup (in ms) data[index] = utime.ticks_ms() # Next write will be at the next index # Restarts at index 0 when the array is full! index = (index + 1) % 32 # Toggle the LED led(not led()) # Run the callback once, after 100 ms timer.init(mode = Timer.ONE_SHOT, period = 100, callback = callback) # Run the callback every second timer.init(period = 1000, callback = callback) # After a few seconds # Most of the values will have the same last three digits # You might see something like # [ 456, 5662, 6662, 7662, 8662, ... ] print(data) # Stop the timer timer.deinit() ``` Files ----- MicroPython has an internal filesystem that is stored after the firmware. You can access it in the usual way: ``` python import os os.listdir("/") print( open("/boot.py").read() ) # Write to a file. Deletes existing content. f = open("data.txt", "w") f.write("Hello ") f.close() # Append f = open("data.txt", "a") f.write("world.\n") f.close() print(open("data.txt").read()) ``` Sleep modes and power consumption --------------------------------- When transmitting data to a WLAN, power consumption will be 160 - 260 mA (so use `WLAN.active(false)` when you don't need it). Even with a deactivated modem, the power consumption can still be up to 20 mA when the CPU is running. ### Idle In idle mode, the CPU is paused but peripherals keep working. ``` python import machine # Enter idle mode indefinitely machine.idle() ``` Execution continues when an interrupt happens, usually after a few milliseconds ### Light sleep In light sleep, most of the microprocessor will be inactive. Power consumption will be at about 0.8 mA. ``` python import machine # Enter light sleep until the next interrupt machine.lightsleep() # Enter lightsleep until the next interrupt, but # at most for one second machine.lightsleep(1000) ``` ### Deep sleep In deep sleep, most of the RAM and all digital peripherals are turned off. Only the RTC and ULP co-processor stay active. Power consumption is about 10 uA. After deep sleep is exited, the ESP32 will reset. ``` python import machine # Same behaviour as with lightsleep machine.deepsleep() machine.deepsleep(10000) # To further reduce power consumption, disable pullups: p1 = machine.Pin(4, machine.Pin.IN, machine.Pin.PULL_HOLD) ``` After the reset, use `machine.reset_cause()` to check if we were in a deep sleep: ``` python import machine if machine.reset_cause() == machine.DEEPSLEEP_RESET: print("Woke up from deep sleep") ``` ### RTC The RTC of the ESP32 is not only used to keep track of time, but can also save up to 2 kiB of data during deep sleep. This memory is still volatile, however, and will be deleted if you use the reset button or remove power ``` python import machine rtc = machine.RTC() rtc.memory('some data') rtc.memory() ``` Network ------- To create or connect to a WLAN, we will use the `network` module. ### Creating a network You can easily create a network with a DHCP server for any clients: ``` python import network wlan = network.WLAN(network.AP_IF) wlan.config(essid = "micropython", authmode = network.AUTH_WPA2_PSK, password = "YELLOWSUBMARINE") # You can look at some interface information: # Address, subnet mask, gateway, DNS server. # Pass in a tuple containing these values to # set the information instead. wlan.ifconfig() ``` ### Connecting to an existing network ``` python import network wlan = network.WLAN(network.STA_IF) # Activate the interface wlan.active(True) # Scan for visible networks wlan.scan() # Check connection status wlan.isconnected() # We are not connected. Change that wlan.connect("micropython", "YELLOWSUBMARINE") ``` ### TCP/UDP sockets ``` python import usocket # Create a TCP socket tcp = usocket.socket(usocket.AF_INET, usocket.SOCK_STREAM) # or UDP udp = usocket.socket(usocket.AF_INET, usocket.SOCK_DGRAM) # Listen on a port and accept a connection from a client address = usocket.getaddrinfo('0.0.0.0', 42)[0][-1] tcp.bind(address) tcp.listen() conn, remote_address = tcp.accept() # Connect to a server address = usocket.getaddrinfo('192.168.4.1', 42) tcp.connect(address) # Write data tcp.write(b"Hello\n") # Receive data conn.read(16) conn.readline() ``` HTTP ---- ### Requests ``` python import upip upip.install('urequests') import urequests urequests.get('https://imaginaerraum.de') ``` ### Server First, we need to install `picoweb` ``` python import upip upip.install("picoweb") upip.install("pycopy-ulogging") ``` This will automatically install `picoweb` and its dependencies into `/lib/` if you have an internet connection. Now you can create simple websites. ``` python import picoweb app = picoweb.WebApp(__name__) @app.route("/") def index(request, response): yield from picoweb.start_response(response) yield from response.awrite("Hello, world") app.run("0.0.0.0", 80, debug=True) ``` Next steps ---------- In no particular order: - WebREPL - Talking to peripherals with SPI, I2C, ... - LED libraries for WS2812, APA102, ... - Interrupt handlers - Using the SD card - Using `uselect` to handle multiple sockets - Handling sensor data with `umqtt`