diff --git a/.github/workflows/micropython.yml b/.github/workflows/micropython.yml index 8696b66..9780570 100644 --- a/.github/workflows/micropython.yml +++ b/.github/workflows/micropython.yml @@ -7,7 +7,8 @@ on: types: [created] env: - MICROPYTHON_VERSION: f80d040c038c343b0709eba537014fb52bc8115e + MICROPYTHON_VERSION: 38e7b842c6bc8122753cbf0845eb141f28fbcb72 + PIMORONI_PICO_VERSION: v1.19.18 jobs: deps: @@ -74,6 +75,7 @@ jobs: env: RELEASE_FILE: pimoroni-${{matrix.shortname}}-${{github.event.release.tag_name || github.sha}}-micropython.uf2 RELEASE_FILE_WITH_OS: pimoroni-${{matrix.shortname}}-${{github.event.release.tag_name || github.sha}}-micropython-with-badger-os.uf2 + FIRMWARE_DIR: "$GITHUB_WORKSPACE/badger2040/firmware" BOARD_DIR: "$GITHUB_WORKSPACE/badger2040/firmware/${{matrix.board}}" BADGER_OS_DIR: "$GITHUB_WORKSPACE/badger2040/badger_os" @@ -104,6 +106,7 @@ jobs: - uses: actions/checkout@v3 with: repository: pimoroni/pimoroni-pico + ref: ${{env.PIMORONI_PICO_VERSION}} submodules: true path: pimoroni-pico @@ -114,12 +117,12 @@ jobs: ref: v0.0.1 path: dir2uf2 - # HACK: Patch Wakeup GPIO features into Pico SDK - - name: "HACK: Wakeup GPIO Patch" + # HACK: Patch startup overclock into Pico SDK + - name: "HACK: Startup Overclock Patch" shell: bash working-directory: micropython/lib/pico-sdk run: | - git apply "${{env.BOARD_DIR}}/wakeup_gpio.patch" + git apply "${{env.FIRMWARE_DIR}}/startup_overclock.patch" # Install apt packages - name: Install CCache & Compiler diff --git a/README.md b/README.md index b8aca81..faab322 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# Badger 2040 & Badger 2040 W -## Firmware, Examples & Documentation +# Badger 2040 & Badger 2040 W +## Firmware, Examples & Documentation + +Badger 2040 and Badger 2040 W are maker-friendly all-in-one badge wearables, featuring a 2.9", 296x128 pixel, monochrome e-paper display. + +- [Install](#install) + - [Badger 2040](#badger-2040) + - [Badger 2040 W](#badger-2040-w) + +## Install + +Grab the latest release from [https://github.com/pimoroni/badger2040/releases/latest](https://github.com/pimoroni/badger2040/releases/latest) + +There are four .uf2 files to pick from. + +:warning: Those marked `with-badger-os` contain a full filesystem image that will overwrite both the firmware *and* filesystem of your Badger: + +* pimoroni-badger2040-vX.X.X-micropython-with-badger-os.uf2 +* pimoroni-badger2040w-vX.X.X-micropython-with-badger-os.uf2 + +The regular builds just include the firmware, and leave your files alone: + +* pimoroni-badger2040-vX.X.X-micropython.uf2 +* pimoroni-badger2040w-vX.X.X-micropython.uf2 + +### Badger 2040 + +1. Connect your Badger 2040 W to your computer using a USB A to C cable. + +2. Reset your device into bootloader mode by holding BOOT/USR and pressing the RST button next to it. + +3. Drag and drop one of the `badger2040` .uf2 files to the "RPI-RP2" drive that appears. + +4. Your device should reset and, if you used a `with-badger-os` variant, show the Badger OS Launcher. + +### Badger 2040 W + +1. Connect your Badger 2040 to your computer using a USB A to microB cable. + +2. Reset your device into bootloader mode by holding BOOTSEL (onboard the Pico W) and pressing RESET (next to the qw/st connector). + +3. Drag and drop one of the `badger2040w` .uf2 files to the "RPI-RP2" drive that appears. + +4. Your device should reset and, if you used a `with-badger-os` variant, show the Badger OS Launcher. diff --git a/badger_os/readme.md b/badger_os/README.md similarity index 56% rename from badger_os/readme.md rename to badger_os/README.md index ab8139b..e1707b6 100644 --- a/badger_os/readme.md +++ b/badger_os/README.md @@ -1,7 +1,9 @@ -# Badger 2040 W MicroPython Examples - -- [About Badger 2040 W](#about-badger-2040-w) -- [Badger 2040 W and PicoGraphics](#badger-2040-w-and-picographics) +# Badger 2040 MicroPython Examples + +These MicroPython examples demonstrate a variety of applications and are distributed as a sort of "OS" for Badger 2040. + +They should help you get started quickly and give you something to modify for your own requirements. + - [Examples](#examples) - [Badge](#badge) - [Clock](#clock) @@ -17,25 +19,6 @@ - [Weather](#weather) - [Other Resources](#other-resources) - -## About Badger 2040 W - -Badger 2040 W is a programmable E Paper/eInk/EPD badge with 2.4GHz wireless connectivity, powered by Raspberry Pi Pico W. It can go into a deep sleep mode between updates to preserve battery. - -- :link: [Badger 2040 W store page](https://shop.pimoroni.com/products/badger-2040-w) - -Badger 2040 W ships with MicroPython firmware pre-loaded, but you can download the most recent version at the link below (you'll want the `pimoroni-badger2040w` .uf2). If you download the `-with-examples` file, it will come with examples built in. - -- [MicroPython releases](https://github.com/pimoroni/pimoroni-pico/releases) -- [Installing MicroPython](../../../setting-up-micropython.md) - -## Badger 2040 W and PicoGraphics - -The easiest way to start displaying cool stuff on Badger is by using our `badger2040w` module (which contains helpful functions for interacting with the board hardware) and our PicoGraphics library (which contains a bunch of functions for drawing on the E Ink display). - -- [Badger 2040 W function reference](../../modules/badger2040w/README.md) -- [PicoGraphics function reference](../../modules/picographics/README.md) - ## Examples Find out more about how to use these examples in our Learn guide: @@ -47,40 +30,74 @@ Find out more about how to use these examples in our Learn guide: Customisable name badge example. +Loads badge details from the `/badges` directory on the device, using a file called `badge.txt` which should contain: + +* Company +* Name +* Detail 1 title +* Detail 1 text +* Detail 2 title +* Detail 2 text +* Badge image path + +For example: + +```txt +mustelid inc +H. Badger +RP2040 +2MB Flash +E ink +296x128px +/badges/badge.jpg +``` + +The image should be a 104x128 pixel JPEG. Any colours other than black/white will be dithered. + ### Clock [clock.py](examples/clock.py) Clock example with (optional) NTP synchronization and partial screen updates. +Press button B to switch the clock into time set mode. + +On Badger 2040 this allows you to set the internal RTC, but it will not survive a sleep/wake cycle even with a connected battery. + +On Badger 2040 W it will set the external RTC and the time will persist even after sleep/wake. + ### Ebook [ebook.py](examples/ebook.py) View text files on Badger. +Currently reads an abridged copy of "The Wind in the Willows" out of the `/books` directory. + ### Fonts [fonts.py](examples/fonts.py) -View all the built in fonts. +A basic example that lets you preview how all of the built-in fonts will appear on the display. ### Help [help.py](examples/help.py) -How to navigate the launcher. +Gives instructions on to navigate the launcher. ### Image [image.py](examples/image.py) -Display .jpegs on Badger. +Display JPEG images. Images are read out of the `/images` directory on device. + +Press button B to show/hide the image filename. ### Info [info.py](examples/info.py) -Info about Badger 2040 W. +Info about Badger 2040. ### List [list.py](examples/list.py) -A checklist to keep track of to-dos or shopping. +A checklist to keep track of to-dos or shopping. Use A/C and Up/Down to navigate and B to check/uncheck items. ### Net Info [net_info.py](examples/net_info.py) diff --git a/badger_os/examples/badge.py b/badger_os/examples/badge.py index aedf171..8b8ec77 100644 --- a/badger_os/examples/badge.py +++ b/badger_os/examples/badge.py @@ -1,8 +1,7 @@ -import time import badger2040 -import badger_os import jpegdec + # Global Constants WIDTH = badger2040.WIDTH HEIGHT = badger2040.HEIGHT @@ -110,6 +109,8 @@ def draw_badge(): display.text(detail2_title, LEFT_PADDING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE) display.text(detail2_text, LEFT_PADDING + name_length + DETAIL_SPACING, HEIGHT - (DETAILS_HEIGHT // 2), WIDTH, DETAILS_TEXT_SIZE) + display.update() + # ------------------------------ # Program setup @@ -160,13 +161,9 @@ def draw_badge(): draw_badge() while True: - if display.pressed(badger2040.BUTTON_A) or display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C) or display.pressed(badger2040.BUTTON_UP) or display.pressed(badger2040.BUTTON_DOWN): - badger_os.warning(display, "To change the text, connect Badger2040 to a PC, load up Thonny, and modify badge.txt") - time.sleep(4) - - draw_badge() - - display.update() + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() # If on battery, halt the Badger to save power, it will wake up if any of the front buttons are pressed display.halt() diff --git a/badger_os/examples/clock.py b/badger_os/examples/clock.py index 536c6ca..560ac32 100644 --- a/badger_os/examples/clock.py +++ b/badger_os/examples/clock.py @@ -1,6 +1,5 @@ import time import machine -import ntptime import badger2040 @@ -10,17 +9,105 @@ WIDTH, HEIGHT = display.get_bounds() +if badger2040.is_wireless(): + import ntptime + try: + display.connect() + if display.isconnected(): + ntptime.settime() + except (RuntimeError, OSError) as e: + print(f"Wireless Error: {e.value}") + +# Thonny overwrites the Pico RTC so re-sync from the physical RTC if we can try: - display.connect() - if display.isconnected(): - ntptime.settime() -except (RuntimeError, OSError): - pass # no WiFI + badger2040.pcf_to_pico_rtc() +except RuntimeError: + pass rtc = machine.RTC() display.set_font("gothic") +cursors = ["year", "month", "day", "hour", "minute"] +set_clock = False +toggle_set_clock = False +cursor = 0 +last = 0 + +button_a = badger2040.BUTTONS[badger2040.BUTTON_A] +button_b = badger2040.BUTTONS[badger2040.BUTTON_B] +button_c = badger2040.BUTTONS[badger2040.BUTTON_C] + +button_up = badger2040.BUTTONS[badger2040.BUTTON_UP] +button_down = badger2040.BUTTONS[badger2040.BUTTON_DOWN] + + +# Button handling function +def button(pin): + global last, set_clock, toggle_set_clock, cursor, year, month, day, hour, minute + + time.sleep(0.01) + if not pin.value(): + return + + if button_a.value() and button_c.value(): + machine.reset() + + adjust = 0 + + if pin == button_b: + toggle_set_clock = True + if set_clock: + rtc.datetime((year, month, day, 0, hour, minute, second, 0)) + if badger2040.is_wireless(): + badger2040.pico_rtc_to_pcf() + return + + if set_clock: + if pin == button_c: + cursor += 1 + cursor %= len(cursors) + + if pin == button_a: + cursor -= 1 + cursor %= len(cursors) + + if pin == button_up: + adjust = 1 + + if pin == button_down: + adjust = -1 + + if cursors[cursor] == "year": + year += adjust + year = max(year, 2022) + day = min(day, days_in_month(month, year)) + + if cursors[cursor] == "month": + month += adjust + month = min(max(month, 1), 12) + day = min(day, days_in_month(month, year)) + + if cursors[cursor] == "day": + day += adjust + day = min(max(day, 1), days_in_month(month, year)) + + if cursors[cursor] == "hour": + hour += adjust + hour %= 24 + + if cursors[cursor] == "minute": + minute += adjust + minute %= 60 + + draw_clock() + + +def days_in_month(month, year): + if month == 2 and ((year % 4 == 0 and year % 100 != 0) or year % 400 == 0): + return 29 + return (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[month - 1] + def draw_clock(): global second_offset, second_unit_offset @@ -29,10 +116,18 @@ def draw_clock(): ymd = "{:04}/{:02}/{:02}".format(year, month, day) hms_width = display.measure_text(hms, 1.8) - hms_offset = int((WIDTH / 2) - (hms_width / 2)) + hms_offset = int((badger2040.WIDTH / 2) - (hms_width / 2)) + h_width = display.measure_text(hms[0:2], 1.8) + mi_width = display.measure_text(hms[3:5], 1.8) + mi_offset = display.measure_text(hms[0:3], 1.8) ymd_width = display.measure_text(ymd, 1.0) - ymd_offset = int((WIDTH / 2) - (ymd_width / 2)) + ymd_offset = int((badger2040.WIDTH / 2) - (ymd_width / 2)) + y_width = display.measure_text(ymd[0:4], 1.0) + m_width = display.measure_text(ymd[5:7], 1.0) + m_offset = display.measure_text(ymd[0:5], 1.0) + d_width = display.measure_text(ymd[8:10], 1.0) + d_offset = display.measure_text(ymd[0:8], 1.0) display.set_pen(15) display.clear() @@ -41,15 +136,29 @@ def draw_clock(): display.text(hms, hms_offset, 40, 0, 1.8) display.text(ymd, ymd_offset, 100, 0, 1.0) - display.set_update_speed(2) - display.update() - display.set_update_speed(3) - hms = "{:02}:{:02}:".format(hour, minute) second_offset = hms_offset + display.measure_text(hms, 1.8) hms = "{:02}:{:02}:{}".format(hour, minute, second // 10) second_unit_offset = hms_offset + display.measure_text(hms, 1.8) + if set_clock: + display.set_pen(0) + if cursors[cursor] == "year": + display.line(ymd_offset, 120, ymd_offset + y_width, 120, 4) + if cursors[cursor] == "month": + display.line(ymd_offset + m_offset, 120, ymd_offset + m_offset + m_width, 120, 4) + if cursors[cursor] == "day": + display.line(ymd_offset + d_offset, 120, ymd_offset + d_offset + d_width, 120, 4) + + if cursors[cursor] == "hour": + display.line(hms_offset, 70, hms_offset + h_width, 70, 4) + if cursors[cursor] == "minute": + display.line(hms_offset + mi_offset, 70, hms_offset + mi_offset + mi_width, 70, 4) + + display.set_update_speed(2) + display.update() + display.set_update_speed(3) + def draw_second(): global second_offset, second_unit_offset @@ -70,8 +179,12 @@ def draw_second(): s = "{}".format(second % 10) display.text(s, second_unit_offset, 40, 0, 1.8) display.partial_update(second_unit_offset, 8, 75 - (second_unit_offset - second_offset), 56) + time.sleep(0.9) +for b in badger2040.BUTTONS.values(): + b.irq(trigger=machine.Pin.IRQ_RISING, handler=button) + year, month, day, wd, hour, minute, second, _ = rtc.datetime() if (year, month, day) == (2021, 1, 1): @@ -83,12 +196,20 @@ def draw_second(): while True: - year, month, day, wd, hour, minute, second, _ = rtc.datetime() - if second != last_second: - if minute != last_minute: - draw_clock() - last_minute = minute - else: - draw_second() - last_second = second + if not set_clock: + year, month, day, wd, hour, minute, second, _ = rtc.datetime() + if second != last_second: + if minute != last_minute: + draw_clock() + last_minute = minute + else: + draw_second() + last_second = second + + if toggle_set_clock: + set_clock = not set_clock + print(f"Set clock changed to: {set_clock}") + toggle_set_clock = False + draw_clock() + time.sleep(0.01) diff --git a/badger_os/examples/ebook.py b/badger_os/examples/ebook.py index 0ffb75d..cf4394a 100644 --- a/badger_os/examples/ebook.py +++ b/badger_os/examples/ebook.py @@ -189,6 +189,10 @@ def render_page(): state["offsets"] = [] while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + # Was the next page button pressed? if display.pressed(badger2040.BUTTON_DOWN): state["current_page"] += 1 diff --git a/badger_os/examples/fonts.py b/badger_os/examples/fonts.py index d4329fd..b66dea4 100644 --- a/badger_os/examples/fonts.py +++ b/badger_os/examples/fonts.py @@ -102,18 +102,23 @@ def draw_fonts(): display.led(128) display.set_update_speed(badger2040.UPDATE_FAST) -changed = not badger2040.woken_by_button() +changed = True # ------------------------------ # Main program loop # ------------------------------ while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + if display.pressed(badger2040.BUTTON_UP): state["selected_font"] -= 1 if state["selected_font"] < 0: state["selected_font"] = len(FONT_NAMES) - 1 changed = True + if display.pressed(badger2040.BUTTON_DOWN): state["selected_font"] += 1 if state["selected_font"] >= len(FONT_NAMES): diff --git a/badger_os/examples/help.py b/badger_os/examples/help.py index df33f48..890926b 100644 --- a/badger_os/examples/help.py +++ b/badger_os/examples/help.py @@ -38,4 +38,5 @@ # Call halt in a loop, on battery this switches off power. # On USB, the app will exit when A+C is pressed because the launcher picks that up. while True: + display.keepalive() display.halt() diff --git a/badger_os/examples/image.py b/badger_os/examples/image.py index 85fda9d..ab72e91 100644 --- a/badger_os/examples/image.py +++ b/badger_os/examples/image.py @@ -1,39 +1,20 @@ import os -import sys -import time import badger2040 from badger2040 import HEIGHT, WIDTH import badger_os import jpegdec -REAMDE = """ -Images must be 296x128 pixel JPEGs - -Create a new "images" directory via Thonny, and upload your .jpg files there. -""" - -OVERLAY_BORDER = 40 -OVERLAY_SPACING = 20 -OVERLAY_TEXT_SIZE = 0.5 - TOTAL_IMAGES = 0 # Turn the act LED on as soon as possible display = badger2040.Badger2040() display.led(128) +display.set_update_speed(badger2040.UPDATE_NORMAL) jpeg = jpegdec.JPEG(display.display) -# Try to preload BadgerPunk image -try: - os.mkdir("/images") - with open("/images/readme.txt", "w") as f: - f.write(REAMDE) - f.flush() -except (OSError, ImportError): - pass # Load images try: @@ -77,42 +58,36 @@ def show_image(n): if TOTAL_IMAGES == 0: - display.set_pen(15) - display.clear() - badger_os.warning(display, "To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.") - time.sleep(4.0) - sys.exit() + raise RuntimeError("To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.") badger_os.state_load("image", state) -changed = not badger2040.woken_by_button() +changed = True while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + if display.pressed(badger2040.BUTTON_UP): if state["current_image"] > 0: state["current_image"] -= 1 changed = True + if display.pressed(badger2040.BUTTON_DOWN): if state["current_image"] < TOTAL_IMAGES - 1: state["current_image"] += 1 changed = True + if display.pressed(badger2040.BUTTON_A): state["show_info"] = not state["show_info"] changed = True - if display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C): - display.set_pen(15) - display.clear() - badger_os.warning(display, "To add images connect Badger2040 to a PC, load up Thonny, and see readme.txt in images/") - display.update() - print(state["current_image"]) - time.sleep(4) - changed = True if changed: - badger_os.state_save("image", state) show_image(state["current_image"]) + badger_os.state_save("image", state) changed = False # Halt the Badger to save power, it will wake up if any of the front buttons are pressed diff --git a/badger_os/examples/info.py b/badger_os/examples/info.py index aa6f430..e592bfd 100644 --- a/badger_os/examples/info.py +++ b/badger_os/examples/info.py @@ -41,4 +41,5 @@ # Call halt in a loop, on battery this switches off power. # On USB, the app will exit when A+C is pressed because the launcher picks that up. while True: + display.keepalive() display.halt() diff --git a/badger_os/examples/list.py b/badger_os/examples/list.py index 5b74125..3232124 100644 --- a/badger_os/examples/list.py +++ b/badger_os/examples/list.py @@ -161,7 +161,7 @@ def draw_checkbox(x, y, size, background, foreground, thickness, tick, padding): # Program setup # ------------------------------ -changed = not badger2040.woken_by_button() +changed = True state = { "current_item": 0, } @@ -213,6 +213,10 @@ def draw_checkbox(x, y, size, background, foreground, thickness, tick, padding): # ------------------------------ while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + if len(list_items) > 0: if display.pressed(badger2040.BUTTON_A): if state["current_item"] > 0: diff --git a/badger_os/examples/net_info.py b/badger_os/examples/net_info.py index 0e5ff01..96db200 100644 --- a/badger_os/examples/net_info.py +++ b/badger_os/examples/net_info.py @@ -45,4 +45,5 @@ # Call halt in a loop, on battery this switches off power. # On USB, the app will exit when A+C is pressed because the launcher picks that up. while True: + display.keepalive() display.halt() diff --git a/badger_os/examples/news.py b/badger_os/examples/news.py index 87aa553..db08275 100644 --- a/badger_os/examples/news.py +++ b/badger_os/examples/news.py @@ -176,8 +176,7 @@ def draw_page(): draw_page() -while 1: - +while True: changed = False if button_down.value(): diff --git a/badger_os/examples/qrgen.py b/badger_os/examples/qrgen.py index f1d7ff8..0914819 100644 --- a/badger_os/examples/qrgen.py +++ b/badger_os/examples/qrgen.py @@ -15,16 +15,28 @@ text = open("/qrcodes/qrcode.txt", "r") except OSError: text = open("/qrcodes/qrcode.txt", "w") - text.write("""https://pimoroni.com/badger2040 + if badger2040.is_wireless(): + text.write("""https://pimoroni.com/badger2040w Badger 2040 W * 296x128 1-bit e-ink -* 2.4GHz wireless +* 2.4GHz wireless & RTC * five user buttons * user LED * 2MB QSPI flash Scan this code to learn more about Badger 2040 W. +""") + else: + text.write("""https://pimoroni.com/badger2040 +Badger 2040 +* 296x128 1-bit e-ink +* five user buttons +* user LED +* 2MB QSPI flash + +Scan this code to learn +more about Badger 2040. """) text.flush() text.seek(0) @@ -109,9 +121,13 @@ def draw_qr_file(n): badger_os.state_load("qrcodes", state) -changed = not badger2040.woken_by_button() +changed = True while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + if TOTAL_CODES > 1: if display.pressed(badger2040.BUTTON_UP): if state["current_qr"] > 0: diff --git a/badger_os/examples/weather.py b/badger_os/examples/weather.py index 1e4e97a..c5692fa 100644 --- a/badger_os/examples/weather.py +++ b/badger_os/examples/weather.py @@ -103,4 +103,5 @@ def draw_page(): # Call halt in a loop, on battery this switches off power. # On USB, the app will exit when A+C is pressed because the launcher picks that up. while True: + display.keepalive() display.halt() diff --git a/badger_os/launcher.py b/badger_os/launcher.py index 645cfe6..b54f102 100644 --- a/badger_os/launcher.py +++ b/badger_os/launcher.py @@ -164,6 +164,10 @@ def button(pin): display.set_update_speed(badger2040.UPDATE_FAST) while True: + # Sometimes a button press or hold will keep the system + # powered *through* HALT, so latch the power back on. + display.keepalive() + if display.pressed(badger2040.BUTTON_A): button(badger2040.BUTTON_A) if display.pressed(badger2040.BUTTON_B): diff --git a/docs/overview.md b/docs/overview.md new file mode 100644 index 0000000..08a3f5f --- /dev/null +++ b/docs/overview.md @@ -0,0 +1,15 @@ +# Badger 2040 & Badger 2040 W: Overview + +Badger 2040 and Badger 2040 W are small, low-power, badge-style boards with a 2.9", 296x128 pixel, monochrome e-paper display. + +They are best used with MicroPython, and we've created a custom firmware with built-in drivers so you can get the most out of it. + +## Badger 2040 + +Badger 2040 uses an onboard RP2040 chip paired with 2MB flash. + +## Badger 2040 W + +Badger 2040 W mounts a Pico W in lieu of an RP2040. This includes wireless functionality onboard. + +Badger 2040 W also includes a PCF85063A real-time clock, which can wake Badger up from its power-off state. \ No newline at end of file diff --git a/docs/porting-guide.md b/docs/porting-guide.md new file mode 100644 index 0000000..f836751 --- /dev/null +++ b/docs/porting-guide.md @@ -0,0 +1,68 @@ +# Badger 2040: Porting Guide + +The original Badger 2040 release predated the all-encompassing PicoGraphics and used its own custom drawing library. + +Badger has since been updated to use PicoGraphics, and some parts of your code will need to change to be compatible. + +- [Badger OS Changes](#badger-os-changes) + - [Structure \& Filesystem](#structure--filesystem) + - [Add Your Own](#add-your-own) +- [Function Changes](#function-changes) + - [Thick Lines](#thick-lines) + - [Images](#images) + + +## Badger OS Changes + +### Structure & Filesystem + +Apps have been moved to `examples/` to keep things tidy. + +Many apps have top level directories for their associated files, these include: + +* `badges/` for badge .txt files and images +* `images/` for image viewer .jpg files +* `books/` for text books +* `icons/` for weather icons + +### Add Your Own + +You no longer need to edit `launcher.py` to include your own custom apps, just place them in `examples/app_name.py` and include a corresponding `examples/icon-app_name.py`. + +## Function Changes + +The switch from Badger's own library to PicoGraphics changed a few minor things: + +* `pen()` is now `set_pen()` +* `update_speed()` is now `set_update_speed()` +* `thickness()` is now `set_thickness()` and *only* applies to Hershey fonts + +Additionally some features have been outright dropped: + +* `image()` and `icon()` are deprecated, use JPEGs instead. +* `invert()` is no longer supported. + +If you're porting from Badger 2040 to Badger 2040 W, note that it does not have a `USER` button. You'll have to adjust your control scheme accordingly. + +### Thick Lines + +While `set_thickness()` no longer applies to drawing operations, you can draw thick lines using `line(x1, y1, x2, y2, thickness)`. + +### Images + +Using `.bin` files for images is discouraged, albeit still possible. They were always tricky to convert and not cross-compatible with other displays. + +Now you can use `jpegdec` to load and display a JPEG image: + +```python +import badger2040 +import jpegdec + +badger = badger2040.Badger2040() +jpeg = jpegdec.JPEG(badger.display) + +jpeg.open_file("image_file.jpg") +jpeg.decode(x, y) +``` + +JPEG files will be dithered to 1-bit and you might find that low-quality JPEGs have the odd random black pixel caused by compression artifacts. Bump up the quality until you're happy with the result. \ No newline at end of file diff --git a/docs/reference.md b/docs/reference.md new file mode 100644 index 0000000..8afdd0c --- /dev/null +++ b/docs/reference.md @@ -0,0 +1,282 @@ +# Badger 2040 & Badger 2040 W: Reference + +Badger 2040 W and Badger 2040 are Raspberry Pi Pico W powered E Ink badges. + +This function reference should give you a basic understanding of how to programming for them in MicroPython. + +- [Summary](#summary) + - [Differences between Badger 2040 W and Badger 2040](#differences-between-badger-2040-w-and-badger-2040) + - [Getting Started](#getting-started) + - [Constants](#constants) + - [Screen Size](#screen-size) + - [E Ink Pins](#e-ink-pins) + - [Power Pins](#power-pins) + - [Activity LED Pin](#activity-led-pin) +- [Function Reference](#function-reference) + - [Basic Drawing Settings](#basic-drawing-settings) + - [Pen Colour](#pen-colour) + - [Pen Thickness](#pen-thickness) + - [Displaying Images](#displaying-images) + - [Updating The Display](#updating-the-display) + - [Update](#update) + - [Clear](#clear) + - [Partial Update](#partial-update) + - [Update Speed](#update-speed) + - [LED](#led) + - [Buttons](#buttons) + - [Waking From Sleep](#waking-from-sleep) + - [Button Presses](#button-presses) + - [Real-time Clock](#real-time-clock) + - [Update Speed](#update-speed-1) + - [System speed](#system-speed) + +# Summary + +## Differences between Badger 2040 W and Badger 2040 + +Badger 2040 W includes networking support, which eats a chunk of flash and RAM for associated libraries and drivers. It includes the following additional modules: + +* `network` - for establishing and managing a WiFi connection +* `mip` - for installing MicroPython packages +* `ntptime` - for setting the RTC time +* `urequests` - for making web requests +* `urllib.urequest` - a slightly different library for the above +* `umqtt.simple` - a simple mqtt client + +These, plus the baked-in WiFi drivers, reduce the available filesystem size from 1,408K (on Badger 2040) to 848K (on Badger 2040 W). + +WiFi also eats some system RAM, reducing MicroPython's available RAM from 192K (Badger 2040) down to 166K (Badger 2040 W). + +Badger 2040 W does not have a "user" button since the BOOTSEL button (which originally doubled as "user") is now aboard the attached Pico W. + +Badger 2040 W includes a PCF85063A real-time clock, which can wake Badger up from its power-off state. + +## Getting Started + +:info: If you're using a Badger 2040 W you should first populate `WIFI_CONFIG.py` with your WiFi details. + +To start coding your Badger 2040, you will need to add the following lines of code to the start of your code file. + +```python +import badger2040 +badger = badger2040.Badger2040() +``` + +This will create a `Badger2040` class instance called `badger` that will be used in the rest of the examples going forward. + +## Constants + +Below is a list of other constants that have been made available, to help with the creation of more advanced programs. + +### Screen Size + +* `WIDTH` = `296` +* `HEIGHT` = `128` + +### E Ink Pins + +* `BUSY` = `26` + +### Power Pins + +* `ENABLE_3V3` = `10` + +### Activity LED Pin + +* `LED` = `22` + +# Function Reference + +## Basic Drawing Settings + +Since Badger 2040 is based upon PicoGraphics you should read the [PicoGraphics function reference]([../picographics/README.md](https://github.com/pimoroni/pimoroni-pico/blob/main/micropython/modules/picographics/README.md)) for more information about how to draw to the display. + +### Pen Colour + +There are 16 pen colours - or "shades of grey" - to choose, from 0 (black) to 15 (white). + +Since Badger 2040 cannot display colours other than black and white, any value from 1 to 14 will apply dithering when drawn, to simulate a shade of grey. + +```python +set_pen( + colour # int: colour from 0 to 15 +) +``` + +### Pen Thickness + +:warning: Applies to Hershey fonts only. + +Thickness affects Hershey text and governs how thick the component lines should be, making it appear bolder: + +```python +set_thickness( + value # int: thickness in pixels +) +``` + +## Displaying Images + +Badger 2040 can display basic JPEG images. They must not be progressive. It will attempt to dither them to the black/white display. + +To display a JPEG, import and set up the `jpegdec` module like so: + +```python +import badger2040 +import jpegdec + +badger = badger2040.Badger2040() +jpeg = jpegdec.JPEG(badger.display) +``` + +`badger.display` points to the PicoGraphics instance that the Badger2040 class manages for you. + +You can open and display a JPEG file like so: + +```python +jpeg.open_file("/image.jpg") +jpeg.decode(x, y) +``` + +Where `x, y` is the position at which you want to display the JPEG. + +## Updating The Display + +### Update + +Starts a full update of the screen. Will block until the update has finished. + +Update takes no parameters, but the update time will vary depending on which update speed you've selected. + +```python +badger.update() +``` + +### Clear + +Before drawing again it can be useful to `clear` your display. + +`clear` fills the drawing buffer with the pen colour, giving you a clean slate: + +```python +badger.clear() +``` + +### Partial Update + +Starts a partial update of the screen. Will block until the update has finished. + +A partial update allows you to update a portion of the screen rather than the whole thing. + +That portion *must* be a multiple of 8 pixels tall, but can be any number of pixels wide. + +```python +partial_update( + x, # int: x coordinate of the update region + y, # int: y coordinate of the update region (must be a multiple of 8) + w, # int: width of the update region + h # int: height of the update region (must be a multiple of 8) +) +``` + +### Update Speed + +Badger 2040 is capable of updating the display at multiple different speeds. + +These offer a tradeoff between the quality of the final image and the speed of the update. + +There are currently four constants naming the different update speeds from 0 to 3: + +* `UPDATE_NORMAL` - a normal update, great for display the first screen of your application and ensuring good contrast and no ghosting +* `UPDATE_MEDIUM` - a good balance of speed and clarity, you probably want this most of the time +* `UPDATE_FAST` - a fast update, good for stepping through screens such as the pages of a book or the launcher +* `UPDATE_TURBO` - a super fast update, prone to ghosting, great for making minor changes such as moving a cursor through a menu + +```python +set_update_speed( + speed # int: one of the update constants +) +``` + +## LED + +The white indicator LED can be controlled, with brightness ranging from 0 (off) to 255: + +```python +led( + brightness # int: 0 (off) to 255 (full) +) +``` + +## Buttons + +Badger 2040 and Badger 2040 W feature five buttons on its front, labelled A, B, C, ↑ (up) and ↓ (down). These can be read using the `pressed(button)` method, which accepts the button's pin number. For convenience, each button can be referred to using these constants: + +* `BUTTON_A` = `12` +* `BUTTON_B` = `13` +* `BUTTON_C` = `14` +* `BUTTON_UP` = `15` +* `BUTTON_DOWN` = `11` + +Additionally you can use `pressed_any()` to see if _any_ button has been pressed. + +Badger 2040 has an additional "user" button (which doubles as boot select), availble as: + +* `BUTTON_USER` = `23` + +On Badger 2040 W the `BUTTON_USER` constant is set to `None`. + +## Waking From Sleep + +Turning off Badger 2040 and Badger 2040 W will put them into a low-power mode with - in the case of Badger 2040 W - only the RTC running. + +* `turn_off()` - cut system power, on USB this will block until a button or alarm state is raised + +There are several ways to wake your Badger back up: + +### Button Presses + +When running on battery, pressing a button on Badger 2040 will power the unit on. It will automatically be latched on and `main.py` will be executed. + +There are some useful functions to determine if Badger 2040 has been woken by a button, and figure out which one: + +* `badger2040.woken_by_button()` - determine if any button was pressed during power-on. +* `badger2040.pressed_to_wake(button)` - determine if the given button was pressed during power-on. +* `badger2040.reset_pressed_to_wake()` - clear the wakeup GPIO state. +* `badger2040.pressed_to_wake_get_once(button)` - returns `True` if the given button was pressed to wake Badger, and then clears the state of that pin. + +### Real-time Clock + +Badger 2040 W includes a PCF85063a RTC which continues to run from battery when the Badger is off. It can be used to wake the Badger on a schedule. + +The following functions provide a simple API to the RTC features: + +* `badger2040.sleep_for(minutes)` - set the RTC alarm for the desired number of minutes and turn off Badger 2040 W. +* `badger2040.pico_rtc_to_pcf()` - copy the time from the Pico W's onboard RTC to the PCF85063a (useful since Pico W's own RTC is set automatically by Thonny.) +* `badger2040.pcf_to_pico_rtc()` - copy the PCF85063a time to the Pico W's onboard RTC. +* `badger2040.woken_by_rtc()` - returns `True` if the RTC alarm was set when the Badger 2040 W powered on. + +## Update Speed + +The E Ink display on Badger 2040 supports several update speeds. These can be set using `set_update_speed(speed)` where `speed` is a value from `0` to `3`. For convenience these speeds have been given the following constants: + +* `UPDATE_NORMAL` = `0` +* `UPDATE_MEDIUM` = `1` +* `UPDATE_FAST` = `2` +* `UPDATE_TURBO` = `3` + +## System speed + +The system clock speed of the RP2040 can be controlled, allowing power to be saved if on battery, or faster computations to be performed. Use `badger2040.system_speed(speed)` where `speed` is one of the following constants: + +* `SYSTEM_VERY_SLOW` = `0` _4 MHz if on battery, 48 MHz if connected to USB_ +* `SYSTEM_SLOW` = `1` _12 MHz if on battery, 48 MHz if connected to USB_ +* `SYSTEM_NORMAL` = `2` _48 MHz_ +* `SYSTEM_FAST` = `3` _133 MHz_ +* `SYSTEM_TURBO` = `4` _250 MHz_ + +On USB, the system will not run slower than 48MHz, as that is the minimum clock speed required to keep the USB connection stable. + +It is best to set the clock speed as the first thing in your program, and you must not change it after initializing any drivers for any I2C hardware connected to the Qwiic port. To allow you to set the speed at the top of your program, this method is on the `badger2040` module, rather than the `badger` instance, although we have made sure that it is safe to call it after creating a `badger` instance. + +:info: Note that `SYSTEM_TURBO` overclocks the RP2040 to 250MHz, and applies a small over voltage to ensure this is stable. We've found that every RP2040 we've tested is happy to run at this speed without any issues. diff --git a/firmware/PIMORONI_BADGER2040/lib/badger2040.py b/firmware/PIMORONI_BADGER2040/lib/badger2040.py index 16fa622..b01c971 100644 --- a/firmware/PIMORONI_BADGER2040/lib/badger2040.py +++ b/firmware/PIMORONI_BADGER2040/lib/badger2040.py @@ -51,11 +51,18 @@ WAKEUP_MASK = 0 +enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT) +enable.on() + def is_wireless(): return False +def woken_by_rtc(): + return False # Badger 2040 does not include an RTC + + def woken_by_button(): return wakeup.get_gpio_state() & BUTTON_MASK > 0 @@ -84,6 +91,32 @@ def system_speed(speed): pass +def turn_on(): + enable.on() + + +def turn_off(): + time.sleep(0.05) + enable.off() + # Simulate an idle state on USB power by blocking + # until a button event + while True: + for pin, button in BUTTONS.items(): + if pin == BUTTON_USER: + if not button.value(): + return + continue + if button.value(): + return + + +def sleep_for(minutes=None): + raise RuntimeError("Badger 2040 does not include an RTC.") + + +pico_rtc_to_pcf = pcf_to_pico_rtc = sleep_for + + class Badger2040(): def __init__(self): self.display = PicoGraphics(DISPLAY_INKY_PACK) @@ -121,11 +154,10 @@ def thickness(self, thickness): raise RuntimeError("Thickness not supported in PicoGraphics.") def halt(self): - time.sleep(0.05) - enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT) - enable.off() - while not self.pressed_any(): - pass + turn_off() + + def keepalive(self): + turn_on() def pressed(self, button): return BUTTONS[button].value() == (0 if button == BUTTON_USER else 1) or pressed_to_wake_get_once(button) diff --git a/firmware/PIMORONI_BADGER2040/micropython.cmake b/firmware/PIMORONI_BADGER2040/micropython.cmake index 796b4a6..b1207d4 100644 --- a/firmware/PIMORONI_BADGER2040/micropython.cmake +++ b/firmware/PIMORONI_BADGER2040/micropython.cmake @@ -3,9 +3,14 @@ include(${CMAKE_CURRENT_LIST_DIR}/../pimoroni_pico_import.cmake) include_directories(${PIMORONI_PICO_PATH}/micropython) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../") list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython") list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython/modules") +# Enable support for string_view (for PicoGraphics) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + # Essential include(pimoroni_i2c/micropython) include(pimoroni_bus/micropython) @@ -25,7 +30,18 @@ include(pcf85063a/micropython) # Utility include(adcfft/micropython) -include(wakeup/micropython) + +# Use our LOCAL wakeup module from firmware/modules/wakeup +include(firmware/modules/wakeup/micropython) +target_compile_definitions(usermod_wakeup INTERFACE + -DWAKEUP_PIN_MASK=0b10000000000000010000000000 + -DWAKEUP_PIN_DIR=0b10000000000000010000000000 + -DWAKEUP_PIN_VALUE=0b10000000000000010000000000 +) + +# Note: cppmem is *required* for C++ code to function on MicroPython +# it redirects `malloc` and `free` calls to MicroPython's heap +include(cppmem/micropython) # LEDs & Matrices include(plasma/micropython) diff --git a/firmware/PIMORONI_BADGER2040/wakeup_gpio.patch b/firmware/PIMORONI_BADGER2040/wakeup_gpio.patch deleted file mode 100644 index 3cad91b..0000000 --- a/firmware/PIMORONI_BADGER2040/wakeup_gpio.patch +++ /dev/null @@ -1,138 +0,0 @@ -diff --git a/src/rp2_common/pico_runtime/runtime.c b/src/rp2_common/pico_runtime/runtime.c -index f9018d0..ae8c479 100644 ---- a/src/rp2_common/pico_runtime/runtime.c -+++ b/src/rp2_common/pico_runtime/runtime.c -@@ -20,6 +20,7 @@ - #include "hardware/clocks.h" - #include "hardware/irq.h" - #include "hardware/resets.h" -+#include "hardware/gpio.h" - - #include "pico/mutex.h" - #include "pico/time.h" -@@ -35,6 +36,21 @@ - #include "pico/bootrom.h" - #endif - -+// Pins to toggle on wakeup -+#ifndef PICO_WAKEUP_PIN_MASK -+#define PICO_WAKEUP_PIN_MASK ((0b1 << 10) | (0b1 << 25)) -+#endif -+ -+// Direction -+#ifndef PICO_WAKEUP_PIN_DIR -+#define PICO_WAKEUP_PIN_DIR ((0b1 << 10) | (0b1 << 25)) -+#endif -+ -+// Value -+#ifndef PICO_WAKEUP_PIN_VALUE -+#define PICO_WAKEUP_PIN_VALUE ((0b1 << 10) | (0b1 << 25)) -+#endif -+ - extern char __StackLimit; /* Set by linker. */ - - uint32_t __attribute__((section(".ram_vector_table"))) ram_vector_table[48]; -@@ -64,7 +80,13 @@ void runtime_install_stack_guard(void *stack_bottom) { - | 0x10000000; // XN = disable instruction fetch; no other bits means no permissions - } - --void runtime_init(void) { -+void runtime_user_init(void) { -+ gpio_init_mask(PICO_WAKEUP_PIN_MASK); -+ gpio_set_dir_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_DIR); -+ gpio_put_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_VALUE); -+} -+ -+void runtime_reset_peripherals(void) { - // Reset all peripherals to put system into a known state, - // - except for QSPI pads and the XIP IO bank, as this is fatal if running from flash - // - and the PLLs, as this is fatal if clock muxing has not been reset on this boot -@@ -89,7 +111,9 @@ void runtime_init(void) { - RESETS_RESET_UART1_BITS | - RESETS_RESET_USBCTRL_BITS - )); -+} - -+void runtime_init(void) { - // pre-init runs really early since we need it even for memcpy and divide! - // (basically anything in aeabi that uses bootrom) - -diff --git a/src/rp2_common/pico_standard_link/crt0.S b/src/rp2_common/pico_standard_link/crt0.S -index d061108..e48d870 100644 ---- a/src/rp2_common/pico_standard_link/crt0.S -+++ b/src/rp2_common/pico_standard_link/crt0.S -@@ -10,6 +10,8 @@ - #include "hardware/regs/sio.h" - #include "pico/asm_helper.S" - #include "pico/binary_info/defs.h" -+#include "hardware/regs/resets.h" -+#include "hardware/regs/rosc.h" - - #ifdef NDEBUG - #ifndef COLLAPSE_IRQS -@@ -226,6 +228,23 @@ _reset_handler: - cmp r0, #0 - bne hold_non_core0_in_bootrom - -+ // Increase ROSC frequency to ~48MHz (range 14.4 - 96) -+ // Startup drops from ~160ms to ~32ms on Pico W MicroPython -+ ldr r0, =(ROSC_BASE + ROSC_DIV_OFFSET) -+ ldr r1, =0xaa2 -+ str r1, [r0] -+ -+ ldr r1, =runtime_reset_peripherals -+ blx r1 -+ -+ ldr r1, =runtime_user_init -+ blx r1 -+ -+ // Read GPIO state for front buttons and store -+ movs r3, 0xd0 // Load 0xd0 into r3 -+ lsls r3, r3, 24 // Shift left 24 to get 0xd0000000 -+ ldr r6, [r3, 4] // Load GPIO state (0xd0000004) into r6 -+ - // In a NO_FLASH binary, don't perform .data copy, since it's loaded - // in-place by the SRAM load. Still need to clear .bss - #if !PICO_NO_FLASH -@@ -252,6 +271,10 @@ bss_fill_test: - cmp r1, r2 - bne bss_fill_loop - -+ // runtime_wakeup_gpio_state gets zero init above -+ ldr r2, =runtime_wakeup_gpio_state // Load output var addr into r2 -+ str r6, [r2] // Store r6 to r2 -+ - platform_entry: // symbol for stack traces - // Use 32-bit jumps, in case these symbols are moved out of branch range - // (e.g. if main is in SRAM and crt0 in flash) -@@ -311,6 +334,19 @@ data_cpy_table: - runtime_init: - bx lr - -+.weak runtime_user_init -+.type runtime_user_init,%function -+.thumb_func -+runtime_user_init: -+ bx lr -+ -+.weak runtime_reset_peripherals -+.type runtime_reset_peripherals,%function -+.thumb_func -+runtime_reset_peripherals: -+ bx lr -+ -+ - // ---------------------------------------------------------------------------- - // If core 1 somehow gets into crt0 due to a spectacular VTOR mishap, we need to - // catch it and send back to the sleep-and-launch code in the bootrom. Shouldn't -@@ -335,3 +371,9 @@ hold_non_core0_in_bootrom: - .align 2 - .equ HeapSize, PICO_HEAP_SIZE - .space HeapSize -+ -+.section .data._reset_handler -+.global runtime_wakeup_gpio_state -+.align 4 -+runtime_wakeup_gpio_state: -+.word 0x00000000 -\ No newline at end of file diff --git a/firmware/PIMORONI_BADGER2040W/lib/badger2040.py b/firmware/PIMORONI_BADGER2040W/lib/badger2040.py index 8912112..6cc1e40 100644 --- a/firmware/PIMORONI_BADGER2040W/lib/badger2040.py +++ b/firmware/PIMORONI_BADGER2040W/lib/badger2040.py @@ -1,13 +1,9 @@ import machine import micropython from picographics import PicoGraphics, DISPLAY_INKY_PACK -import network -from network_manager import NetworkManager -import WIFI_CONFIG -import uasyncio import time -import gc import wakeup +import pcf85063a BUTTON_DOWN = 11 @@ -30,6 +26,7 @@ UPDATE_FAST = 2 UPDATE_TURBO = 3 +RTC_ALARM = 8 LED = 22 ENABLE_3V3 = 10 BUSY = 26 @@ -55,11 +52,23 @@ WAKEUP_MASK = 0 +i2c = machine.I2C(0) +rtc = pcf85063a.PCF85063A(i2c) +i2c.writeto_mem(0x51, 0x00, b'\x00') # ensure rtc is running (this should be default?) +rtc.enable_timer_interrupt(False) + +enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT) +enable.on() + def is_wireless(): return True +def woken_by_rtc(): + return bool(wakeup.get_gpio_state() & (1 << RTC_ALARM)) + + def woken_by_button(): return wakeup.get_gpio_state() & BUTTON_MASK > 0 @@ -86,6 +95,65 @@ def system_speed(speed): pass +def turn_on(): + enable.on() + + +def turn_off(): + time.sleep(0.05) + enable.off() + # Simulate an idle state on USB power by blocking + # until an RTC alarm or button event + rtc_alarm = machine.Pin(RTC_ALARM) + while True: + if rtc_alarm.value(): + return + for button in BUTTONS.values(): + if button.value(): + return + + +def pico_rtc_to_pcf(): + # Set the PCF85063A to the time stored by Pico W's RTC + year, month, day, dow, hour, minute, second, _ = machine.RTC().datetime() + rtc.datetime((year, month, day, hour, minute, second, dow)) + + +def pcf_to_pico_rtc(): + # Set Pico W's RTC to the time stored by the PCF85063A + t = rtc.datetime() + # BUG ERRNO 22, EINVAL, when date read from RTC is invalid for the Pico's RTC. + try: + machine.RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0)) + return True + except OSError: + return False + + +def sleep_for(minutes): + year, month, day, hour, minute, second, dow = rtc.datetime() + + # if the time is very close to the end of the minute, advance to the next minute + # this aims to fix the edge case where the board goes to sleep right as the RTC triggers, thus never waking up + if second >= 55: + minute += 1 + + minute += minutes + + while minute >= 60: + minute -= 60 + hour += 1 + + if hour >= 24: + hour -= 24 + + rtc.clear_alarm_flag() + rtc.set_alarm(0, minute, hour) + rtc.enable_alarm_interrupt(True) + + turn_off() + + class Badger2040(): def __init__(self): self.display = PicoGraphics(DISPLAY_INKY_PACK) @@ -123,11 +191,10 @@ def thickness(self, thickness): raise RuntimeError("Thickness not supported in PicoGraphics.") def halt(self): - time.sleep(0.05) - enable = machine.Pin(ENABLE_3V3, machine.Pin.OUT) - enable.off() - while not self.pressed_any(): - pass + turn_off() + + def keepalive(self): + turn_on() def pressed(self, button): return BUTTONS[button].value() == 1 or pressed_to_wake_get_once(button) @@ -171,12 +238,19 @@ def status_handler(self, mode, status, ip): self.update() def isconnected(self): + import network return network.WLAN(network.STA_IF).isconnected() def ip_address(self): + import network return network.WLAN(network.STA_IF).ifconfig()[0] def connect(self): + from network_manager import NetworkManager + import WIFI_CONFIG + import uasyncio + import gc + if WIFI_CONFIG.COUNTRY == "": raise RuntimeError("You must populate WIFI_CONFIG.py for networking.") self.display.set_update_speed(2) diff --git a/firmware/PIMORONI_BADGER2040W/lib/network_manager.py b/firmware/PIMORONI_BADGER2040W/lib/network_manager.py index 78b53f5..fb21c2c 100644 --- a/firmware/PIMORONI_BADGER2040W/lib/network_manager.py +++ b/firmware/PIMORONI_BADGER2040W/lib/network_manager.py @@ -7,7 +7,7 @@ class NetworkManager: _ifname = ("Client", "Access Point") - def __init__(self, country="GB", client_timeout=30, access_point_timeout=5, status_handler=None, error_handler=None): + def __init__(self, country="GB", client_timeout=60, access_point_timeout=5, status_handler=None, error_handler=None): rp2.country(country) self._ap_if = network.WLAN(network.AP_IF) self._sta_if = network.WLAN(network.STA_IF) @@ -74,8 +74,8 @@ async def client(self, ssid, psk): self._ap_if.active(False) self._sta_if.active(True) - self._sta_if.connect(ssid, psk) self._sta_if.config(pm=0xa11140) + self._sta_if.connect(ssid, psk) try: await uasyncio.wait_for(self.wait(network.STA_IF), self._client_timeout) diff --git a/firmware/PIMORONI_BADGER2040W/micropython.cmake b/firmware/PIMORONI_BADGER2040W/micropython.cmake index 796b4a6..2901f6c 100644 --- a/firmware/PIMORONI_BADGER2040W/micropython.cmake +++ b/firmware/PIMORONI_BADGER2040W/micropython.cmake @@ -3,9 +3,14 @@ include(${CMAKE_CURRENT_LIST_DIR}/../pimoroni_pico_import.cmake) include_directories(${PIMORONI_PICO_PATH}/micropython) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../") list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython") list(APPEND CMAKE_MODULE_PATH "${PIMORONI_PICO_PATH}/micropython/modules") +# Enable support for string_view (for PicoGraphics) +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + # Essential include(pimoroni_i2c/micropython) include(pimoroni_bus/micropython) @@ -25,7 +30,19 @@ include(pcf85063a/micropython) # Utility include(adcfft/micropython) -include(wakeup/micropython) + +# Use our LOCAL wakeup module from firmware/modules/wakeup +include(firmware/modules/wakeup/micropython) +target_compile_definitions(usermod_wakeup INTERFACE + -DWAKEUP_HAS_RTC=1 + -DWAKEUP_PIN_MASK=0b10000000000010000000000 + -DWAKEUP_PIN_DIR=0b10000000000010000000000 + -DWAKEUP_PIN_VALUE=0b10000000000010000000000 +) + +# Note: cppmem is *required* for C++ code to function on MicroPython +# it redirects `malloc` and `free` calls to MicroPython's heap +include(cppmem/micropython) # LEDs & Matrices include(plasma/micropython) @@ -36,5 +53,5 @@ include(servo/micropython) include(encoder/micropython) include(motor/micropython) -# version.py and pimoroni.py +# version.py, pimoroni.py and boot.py include(modules_py/modules_py) diff --git a/firmware/PIMORONI_BADGER2040W/wakeup_gpio.patch b/firmware/PIMORONI_BADGER2040W/wakeup_gpio.patch deleted file mode 100644 index 4eee288..0000000 --- a/firmware/PIMORONI_BADGER2040W/wakeup_gpio.patch +++ /dev/null @@ -1,138 +0,0 @@ -diff --git a/src/rp2_common/pico_runtime/runtime.c b/src/rp2_common/pico_runtime/runtime.c -index f9018d0..ae8c479 100644 ---- a/src/rp2_common/pico_runtime/runtime.c -+++ b/src/rp2_common/pico_runtime/runtime.c -@@ -20,6 +20,7 @@ - #include "hardware/clocks.h" - #include "hardware/irq.h" - #include "hardware/resets.h" -+#include "hardware/gpio.h" - - #include "pico/mutex.h" - #include "pico/time.h" -@@ -35,6 +36,21 @@ - #include "pico/bootrom.h" - #endif - -+// Pins to toggle on wakeup -+#ifndef PICO_WAKEUP_PIN_MASK -+#define PICO_WAKEUP_PIN_MASK ((0b1 << 10) | (0b1 << 22)) -+#endif -+ -+// Direction -+#ifndef PICO_WAKEUP_PIN_DIR -+#define PICO_WAKEUP_PIN_DIR ((0b1 << 10) | (0b1 << 22)) -+#endif -+ -+// Value -+#ifndef PICO_WAKEUP_PIN_VALUE -+#define PICO_WAKEUP_PIN_VALUE ((0b1 << 10) | (0b1 << 22)) -+#endif -+ - extern char __StackLimit; /* Set by linker. */ - - uint32_t __attribute__((section(".ram_vector_table"))) ram_vector_table[48]; -@@ -64,7 +80,13 @@ void runtime_install_stack_guard(void *stack_bottom) { - | 0x10000000; // XN = disable instruction fetch; no other bits means no permissions - } - --void runtime_init(void) { -+void runtime_user_init(void) { -+ gpio_init_mask(PICO_WAKEUP_PIN_MASK); -+ gpio_set_dir_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_DIR); -+ gpio_put_masked(PICO_WAKEUP_PIN_MASK, PICO_WAKEUP_PIN_VALUE); -+} -+ -+void runtime_reset_peripherals(void) { - // Reset all peripherals to put system into a known state, - // - except for QSPI pads and the XIP IO bank, as this is fatal if running from flash - // - and the PLLs, as this is fatal if clock muxing has not been reset on this boot -@@ -89,7 +111,9 @@ void runtime_init(void) { - RESETS_RESET_UART1_BITS | - RESETS_RESET_USBCTRL_BITS - )); -+} - -+void runtime_init(void) { - // pre-init runs really early since we need it even for memcpy and divide! - // (basically anything in aeabi that uses bootrom) - -diff --git a/src/rp2_common/pico_standard_link/crt0.S b/src/rp2_common/pico_standard_link/crt0.S -index d061108..e48d870 100644 ---- a/src/rp2_common/pico_standard_link/crt0.S -+++ b/src/rp2_common/pico_standard_link/crt0.S -@@ -10,6 +10,8 @@ - #include "hardware/regs/sio.h" - #include "pico/asm_helper.S" - #include "pico/binary_info/defs.h" -+#include "hardware/regs/resets.h" -+#include "hardware/regs/rosc.h" - - #ifdef NDEBUG - #ifndef COLLAPSE_IRQS -@@ -226,6 +228,23 @@ _reset_handler: - cmp r0, #0 - bne hold_non_core0_in_bootrom - -+ // Increase ROSC frequency to ~48MHz (range 14.4 - 96) -+ // Startup drops from ~160ms to ~32ms on Pico W MicroPython -+ ldr r0, =(ROSC_BASE + ROSC_DIV_OFFSET) -+ ldr r1, =0xaa2 -+ str r1, [r0] -+ -+ ldr r1, =runtime_reset_peripherals -+ blx r1 -+ -+ ldr r1, =runtime_user_init -+ blx r1 -+ -+ // Read GPIO state for front buttons and store -+ movs r3, 0xd0 // Load 0xd0 into r3 -+ lsls r3, r3, 24 // Shift left 24 to get 0xd0000000 -+ ldr r6, [r3, 4] // Load GPIO state (0xd0000004) into r6 -+ - // In a NO_FLASH binary, don't perform .data copy, since it's loaded - // in-place by the SRAM load. Still need to clear .bss - #if !PICO_NO_FLASH -@@ -252,6 +271,10 @@ bss_fill_test: - cmp r1, r2 - bne bss_fill_loop - -+ // runtime_wakeup_gpio_state gets zero init above -+ ldr r2, =runtime_wakeup_gpio_state // Load output var addr into r2 -+ str r6, [r2] // Store r6 to r2 -+ - platform_entry: // symbol for stack traces - // Use 32-bit jumps, in case these symbols are moved out of branch range - // (e.g. if main is in SRAM and crt0 in flash) -@@ -311,6 +334,19 @@ data_cpy_table: - runtime_init: - bx lr - -+.weak runtime_user_init -+.type runtime_user_init,%function -+.thumb_func -+runtime_user_init: -+ bx lr -+ -+.weak runtime_reset_peripherals -+.type runtime_reset_peripherals,%function -+.thumb_func -+runtime_reset_peripherals: -+ bx lr -+ -+ - // ---------------------------------------------------------------------------- - // If core 1 somehow gets into crt0 due to a spectacular VTOR mishap, we need to - // catch it and send back to the sleep-and-launch code in the bootrom. Shouldn't -@@ -335,3 +371,9 @@ hold_non_core0_in_bootrom: - .align 2 - .equ HeapSize, PICO_HEAP_SIZE - .space HeapSize -+ -+.section .data._reset_handler -+.global runtime_wakeup_gpio_state -+.align 4 -+runtime_wakeup_gpio_state: -+.word 0x00000000 -\ No newline at end of file diff --git a/firmware/modules/wakeup/micropython.cmake b/firmware/modules/wakeup/micropython.cmake new file mode 100644 index 0000000..c067074 --- /dev/null +++ b/firmware/modules/wakeup/micropython.cmake @@ -0,0 +1,22 @@ +add_library(usermod_wakeup INTERFACE) + +target_sources(usermod_wakeup INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/wakeup.c + ${CMAKE_CURRENT_LIST_DIR}/wakeup.cpp +) + +target_include_directories(usermod_wakeup INTERFACE + ${CMAKE_CURRENT_LIST_DIR} +) + +target_compile_definitions(usermod_wakeup INTERFACE + -DMODULE_WAKEUP_ENABLED=1 +) + +target_link_libraries(usermod INTERFACE usermod_wakeup) + +set_source_files_properties( + ${CMAKE_CURRENT_LIST_DIR}/wakeup.c + PROPERTIES COMPILE_FLAGS + "-Wno-discarded-qualifiers" +) diff --git a/firmware/modules/wakeup/wakeup.c b/firmware/modules/wakeup/wakeup.c new file mode 100644 index 0000000..f570f64 --- /dev/null +++ b/firmware/modules/wakeup/wakeup.c @@ -0,0 +1,22 @@ +#include "wakeup.h" + +STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_get_gpio_state_obj, Wakeup_get_gpio_state); +STATIC MP_DEFINE_CONST_FUN_OBJ_0(Wakeup_reset_gpio_state_obj, Wakeup_reset_gpio_state); + +STATIC const mp_map_elem_t wakeup_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_wakeup) }, + { MP_ROM_QSTR(MP_QSTR_get_gpio_state), MP_ROM_PTR(&Wakeup_get_gpio_state_obj) }, + { MP_ROM_QSTR(MP_QSTR_reset_gpio_state), MP_ROM_PTR(&Wakeup_reset_gpio_state_obj) } +}; +STATIC MP_DEFINE_CONST_DICT(mp_module_wakeup_globals, wakeup_globals_table); + +const mp_obj_module_t wakeup_user_cmodule = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t*)&mp_module_wakeup_globals, +}; + +#if MICROPY_VERSION <= 70144 +MP_REGISTER_MODULE(MP_QSTR_wakeup, wakeup_user_cmodule, MODULE_WAKEUP_ENABLED); +#else +MP_REGISTER_MODULE(MP_QSTR_wakeup, wakeup_user_cmodule); +#endif \ No newline at end of file diff --git a/firmware/modules/wakeup/wakeup.config.hpp b/firmware/modules/wakeup/wakeup.config.hpp new file mode 100644 index 0000000..539f5e0 --- /dev/null +++ b/firmware/modules/wakeup/wakeup.config.hpp @@ -0,0 +1,36 @@ +#include "hardware/i2c.h" + +// Pins to toggle on wakeup +#ifndef WAKEUP_PIN_MASK +#define WAKEUP_PIN_MASK ((0b1 << 10) | (0b1 << 25)) +#endif + +// Direction +#ifndef WAKEUP_PIN_DIR +#define WAKEUP_PIN_DIR ((0b1 << 10) | (0b1 << 25)) +#endif + +// Value +#ifndef WAKEUP_PIN_VALUE +#define WAKEUP_PIN_VALUE ((0b1 << 10) | (0b1 << 25)) +#endif + +#ifndef WAKEUP_HAS_RTC +#define WAKEUP_HAS_RTC (0) +#endif + +#ifndef WAKEUP_RTC_SDA +#define WAKEUP_RTC_SDA (4) +#endif + +#ifndef WAKEUP_RTC_SCL +#define WAKEUP_RTC_SCL (5) +#endif + +#ifndef WAKEUP_RTC_I2C_ADDR +#define WAKEUP_RTC_I2C_ADDR 0x51 +#endif + +#ifndef WAKEUP_RTC_I2C_INST +#define WAKEUP_RTC_I2C_INST i2c0 +#endif \ No newline at end of file diff --git a/firmware/modules/wakeup/wakeup.cpp b/firmware/modules/wakeup/wakeup.cpp new file mode 100644 index 0000000..d0fe635 --- /dev/null +++ b/firmware/modules/wakeup/wakeup.cpp @@ -0,0 +1,54 @@ +#include "hardware/gpio.h" +#include "wakeup.config.hpp" + + +struct Wakeup { + public: + uint32_t wakeup_gpio_state = 0; + + Wakeup() { + // Assert wakeup pins (indicator LEDs, VSYS hold etc) + gpio_init_mask(WAKEUP_PIN_MASK); + gpio_set_dir_masked(WAKEUP_PIN_MASK, WAKEUP_PIN_DIR); + gpio_put_masked(WAKEUP_PIN_MASK, WAKEUP_PIN_VALUE); + + wakeup_gpio_state = gpio_get_all(); + sleep_ms(5); + wakeup_gpio_state |= gpio_get_all(); + +#if WAKEUP_HAS_RTC==1 + // Set up RTC I2C pins and send reset command + i2c_init(WAKEUP_RTC_I2C_INST, 100000); + gpio_init(WAKEUP_RTC_SDA); + gpio_init(WAKEUP_RTC_SCL); + gpio_set_function(WAKEUP_RTC_SDA, GPIO_FUNC_I2C); gpio_pull_up(WAKEUP_RTC_SDA); + gpio_set_function(WAKEUP_RTC_SCL, GPIO_FUNC_I2C); gpio_pull_up(WAKEUP_RTC_SCL); + + // Turn off CLOCK_OUT by writing 0b111 to CONTROL_2 (0x01) register + uint8_t data[] = {0x01, 0b111}; + i2c_write_blocking(WAKEUP_RTC_I2C_INST, WAKEUP_RTC_I2C_ADDR, data, 2, false); + + i2c_deinit(WAKEUP_RTC_I2C_INST); + + // Cleanup + gpio_init(WAKEUP_RTC_SDA); + gpio_init(WAKEUP_RTC_SCL); +#endif + } +}; + +Wakeup wakeup __attribute__ ((init_priority (101))); + +extern "C" { +#include "wakeup.h" + +mp_obj_t Wakeup_get_gpio_state() { + return mp_obj_new_int(wakeup.wakeup_gpio_state); +} + +mp_obj_t Wakeup_reset_gpio_state() { + wakeup.wakeup_gpio_state = 0; + return mp_const_none; +} + +} \ No newline at end of file diff --git a/firmware/modules/wakeup/wakeup.h b/firmware/modules/wakeup/wakeup.h new file mode 100644 index 0000000..9a4aa67 --- /dev/null +++ b/firmware/modules/wakeup/wakeup.h @@ -0,0 +1,5 @@ +#include "py/runtime.h" +#include "py/objstr.h" + +extern mp_obj_t Wakeup_get_gpio_state(); +extern mp_obj_t Wakeup_reset_gpio_state(); \ No newline at end of file diff --git a/firmware/startup_overclock.patch b/firmware/startup_overclock.patch new file mode 100644 index 0000000..0d88477 --- /dev/null +++ b/firmware/startup_overclock.patch @@ -0,0 +1,26 @@ +diff --git a/src/rp2_common/pico_standard_link/crt0.S b/src/rp2_common/pico_standard_link/crt0.S +index d061108..864d31f 100644 +--- a/src/rp2_common/pico_standard_link/crt0.S ++++ b/src/rp2_common/pico_standard_link/crt0.S +@@ -10,6 +10,8 @@ + #include "hardware/regs/sio.h" + #include "pico/asm_helper.S" + #include "pico/binary_info/defs.h" ++#include "hardware/regs/resets.h" ++#include "hardware/regs/rosc.h" + + #ifdef NDEBUG + #ifndef COLLAPSE_IRQS +@@ -226,6 +228,12 @@ _reset_handler: + cmp r0, #0 + bne hold_non_core0_in_bootrom + ++ // Increase ROSC frequency to ~48MHz (range 14.4 - 96) ++ // Speeds up memory zero init and preinit phases. ++ ldr r0, =(ROSC_BASE + ROSC_DIV_OFFSET) ++ ldr r1, =0xaa2 ++ str r1, [r0] ++ + // In a NO_FLASH binary, don't perform .data copy, since it's loaded + // in-place by the SRAM load. Still need to clear .bss + #if !PICO_NO_FLASH