I've made a technical decision to move away from using Adafruit's CircuitPython to the upstream MicroPython. The reason, in a word, is stability of the implementation. I find, for my uses, that MicroPython is more stable for embedded unattended execution than CircuitPython is. The developer ecosystem seems to be richer for CircuitPython, but for long term development I can write or modify what's out there for my needs if the underlying embedded Python implementation is stable enough.
Another feature that MicroPython has that CircuitPython doesn't is the ability to get a microcontroller's unique ID. I use that characteristic extensively because I need it for embedded devices that are part of a larger wireless network to manage them all. For just one example I use the last four characters of the unique ID to synthesis an SSID for devices that will be part of my local home WiFi network. You can see an example in the photo above, in the third line of text (RP2-9633).
While a lot has been made of flashing a Raspberry Pi Pico/W with CircuitPython, flashing with MicroPython is very easy, as it's the same with either. Make sure you have the UF2 file for MicroPython, then with the device unplugged, hold down the BOOTSEL button, plug in the USB cable, then release the BOOTSEL button. The Raspberry Pi Pico will come up in bootloader mode looking like a flash drive, allowing you to drag and drop the MicroPython UF2 file onto the device. Once flashed, the Pico will disappear from your file system, and you'll need a tool such as Thonny to communicate with, and program the device.
MPY: soft reboot MEM FREE: 183,584 BYTES FS TOTAL: 868,352 BYTES FS FREE: 847,872 BYTES PLATFORM: MicroPython-1.23.0-arm--with-newlib4.3.0 UID: E6614C775B969633 SSID: RP2-9633 CPU FREQ: 125,000,000 Hz I2C: SoftI2C(scl=27, sda=26, freq=500000) I2C: DEVICES FOUND: ['0x3d'] ssid: Dashmeister rssi: -82 ssid: ESP32S3-5F50 rssi: -21 ssid: GuestNetwork rssi: -61 ssid: NETGEAR04 rssi: -90 ssid: NotTheWiFiYoureLookingFor rssi: -91 ssid: SmartLife-EEFB rssi: -68 MicroPython v1.23.0 on 2024-06-02; Raspberry Pi Pico W with RP2040 Type "help()" for more information.
What follows is the code running on this particular Raspberry Pi Pico W. It is in two parts, the main followed by the OLED display class.
print() import gc print(f" MEM FREE: {gc.mem_free():,} BYTES") import os UNAME = os.uname().sysname.upper() stat_vfs = os.statvfs('/') print(f" FS TOTAL: {stat_vfs[0] * stat_vfs[2]:,} BYTES") print(f" FS FREE: {stat_vfs[0] * stat_vfs[3]:,} BYTES") import platform print(f" PLATFORM: {platform.platform()}") import binascii import machine as ma UNIQUE_ID = binascii.hexlify(ma.unique_id()).decode('ascii').upper() print(f" UID: {UNIQUE_ID}") SSID = UNAME + '-' + UNIQUE_ID[-4:] print(f" SSID: {SSID}") print(f" CPU FREQ: {ma.freq():,} Hz") # Scan I2C bus for devices # # I2C pins for Raspberry Pi Pico W, device I2C1 import SSD1306 SDA_PIN = 26 SCL_PIN = 27 SOFT_I2C = ma.SoftI2C(scl=ma.Pin(SCL_PIN), sda=ma.Pin(SDA_PIN)) print(f" I2C: {SOFT_I2C}") print(" I2C: DEVICES FOUND:", [hex(device_address) for device_address in SOFT_I2C.scan()]) # Display the Micropython logo on the SSD1306 OLED display. # display = SSD1306.SSD1306_I2C(SOFT_I2C) display.fill(0) display.framebuf.fill_rect(0, 0, 32, 32, 1) display.framebuf.fill_rect(2, 2, 28, 28, 0) display.framebuf.vline(9, 8, 22, 1) display.framebuf.vline(16, 2, 22, 1) display.framebuf.vline(23, 8, 22, 1) display.framebuf.fill_rect(26, 24, 2, 4, 1) display.text('MicroPython', 40, 0, 1) display.text('-'.join(platform.platform().split('-')[1:3]), 40, 12, 1) display.text(SSID, 40, 24, 1) display.show() print() import network wifi = network.WLAN(network.STA_IF) wifi.active(True) access_points = wifi.scan() networks = {} for network in access_points: if len(network[0]) > 0 and bytearray(network[0])[0] != 0: ssid = network[0].decode('utf-8') networks[ssid] = network[3] for ssid in sorted(networks.keys()): print(f"ssid: {ssid:24} rssi: {networks[ssid]}") print()
Inside of main, lines 19 and 19 find the board's unique ID and then use the last four characters of the unique ID to synthesize the board's SSID. The block of code from line 23 to line 47 enable the OLED display and display the MicroPython logo as well as identifying information about that specific Raspberry Pi Pico W. You can see what it produces in the photo at the top of the post.
# MicroPython SSD1306 OLED driver, I2C interface # # Originally written by Adafruit # # Adafruit has deprecated this code, and is now devoting # development time and resources for the version that # works with Circuit Python. import time import framebuf from micropython import const # register definitions SET_CONTRAST = const(0x81) SET_ENTIRE_ON = const(0xa4) SET_NORM_INV = const(0xa6) SET_DISP = const(0xae) SET_MEM_ADDR = const(0x20) SET_COL_ADDR = const(0x21) SET_PAGE_ADDR = const(0x22) SET_DISP_START_LINE = const(0x40) SET_SEG_REMAP = const(0xa0) SET_MUX_RATIO = const(0xa8) SET_COM_OUT_DIR = const(0xc0) SET_DISP_OFFSET = const(0xd3) SET_COM_PIN_CFG = const(0xda) SET_DISP_CLK_DIV = const(0xd5) SET_PRECHARGE = const(0xd9) SET_VCOM_DESEL = const(0xdb) SET_CHARGE_PUMP = const(0x8d) # display definitions OLED_WIDTH = const(128) OLED_HEIGHT = const(64) OLED_LINE_MAX = const(6) OLED_ADDR = const(0x3D) class SSD1306(): def __init__(self, width, height, external_vcc): self.width = width self.height = height self.external_vcc = external_vcc self.pages = self.height // 8 # Note the subclass must initialize self.framebuf to a framebuffer. # This is necessary because the underlying data buffer is different # between I2C and SPI implementations (I2C needs an extra byte). self.poweron() self.init_display() def init_display(self): for cmd in ( SET_DISP | 0x00, # off # address setting SET_MEM_ADDR, 0x00, # horizontal # resolution and layout SET_DISP_START_LINE | 0x00, SET_SEG_REMAP | 0x01, # column addr 127 mapped to SEG0 SET_MUX_RATIO, self.height - 1, SET_COM_OUT_DIR | 0x08, # scan from COM[N] to COM0 SET_DISP_OFFSET, 0x00, SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12, # timing and driving scheme SET_DISP_CLK_DIV, 0x80, SET_PRECHARGE, 0x22 if self.external_vcc else 0xf1, SET_VCOM_DESEL, 0x30, # 0.83*Vcc # display SET_CONTRAST, 0xff, # maximum SET_ENTIRE_ON, # output follows RAM contents SET_NORM_INV, # not inverted # charge pump SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14, SET_DISP | 0x01): # on self.write_cmd(cmd) self.fill(0) self.show() def poweroff(self): self.write_cmd(SET_DISP | 0x00) def contrast(self, contrast): self.write_cmd(SET_CONTRAST) self.write_cmd(contrast) def invert(self, invert): self.write_cmd(SET_NORM_INV | (invert & 1)) def show(self): x0 = 0 x1 = self.width - 1 if self.width == 64: # displays with width of 64 pixels are shifted by 32 x0 += 32 x1 += 32 self.write_cmd(SET_COL_ADDR) self.write_cmd(x0) self.write_cmd(x1) self.write_cmd(SET_PAGE_ADDR) self.write_cmd(0) self.write_cmd(self.pages - 1) self.write_framebuf() def fill(self, col): self.framebuf.fill(col) def pixel(self, x, y, col): self.framebuf.pixel(x, y, col) def scroll(self, dx, dy): self.framebuf.scroll(dx, dy) def text(self, string, x, y, col=1): self.framebuf.text(string, x, y, col) class SSD1306_I2C(SSD1306): def __init__(self, i2c, width=OLED_WIDTH, height=OLED_HEIGHT, addr=0x3D, external_vcc=False): self.i2c = i2c self.addr = addr self.temp = bytearray(2) # Add an extra byte to the data buffer to hold an I2C data/command byte # to use hardware-compatible I2C transactions. A memoryview of the # buffer is used to mask this byte from the framebuffer operations # (without a major memory hit as memoryview doesn't copy to a separate # buffer). self.buffer = bytearray(((height // 8) * width) + 1) self.buffer[0] = 0x40 # Set first byte of data buffer to Co=0, D/C=1 self.framebuf = framebuf.FrameBuffer1(memoryview(self.buffer)[1:], width, height) super().__init__(width, height, external_vcc) def write_cmd(self, cmd): self.temp[0] = 0x80 # Co=1, D/C#=0 self.temp[1] = cmd self.i2c.writeto(self.addr, self.temp) def write_framebuf(self): # Blast out the frame buffer using a single I2C transaction to support # hardware I2C interfaces. self.i2c.writeto(self.addr, self.buffer) def poweron(self): pass # A convenience method to print by line number unlike the text() method. # This assumes that you are using a 128 x 64 pixel OLED display. # Line numbers are 1-6 inclusive. There is no line 0. # def line(self, string, line_number): if line_number > 0 and line_number <= OLED_LINE_MAX: self.text(string,0,(line_number - 1)*10) # A way to test a 128 x 64 pixel OLED display. # def test_oled(self): for i in range(1, OLED_LINE_MAX + 1): self.fill(0) self.line('LINE {} ----+----'.format(i), i) self.show() time.sleep_ms(500) self.fill(0) self.show()
The code in the SSD1306 class was originally created by Adafruit, but they abandoned it when they decided to only support their CircuitPython fork of MicroPython. That's fine, and you an find the original source on GitHub. I cleaned up some things, removed bits I didn't need, and added some bits I found handy.
I'm quite comfortable moving towards MicroPython. The solid reliability of MicroPython trumps all other concerns.
No comments:
Post a Comment