-
Notifications
You must be signed in to change notification settings - Fork 511
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Badger2040 Micropython battery improvements #313
Conversation
…resses. Change badge app to halt when image displayed
This is really awesome stuff, thank you! I have a vague plan to add configurable default clock config upstream to MicroPython, so that Badger2040 can reduce clock by default. See: micropython/micropython#8208 When running from USB and working on code using - presumably - Thonny, do you notice any issues with using halt? The 3v3 enable should have no effect in this case, and the code will drop through to State should definitely be an on-change only thing. Though I think using the regular filesystem affords us some protection against flash wear, since we can't predict where exactly a state file will be saved. Anything but reset as a method of exiting an app makes a lot of sense, at the moment it's a crude mechanism to make sure MicroPython starts with a fresh state and avoid myriad issues with memory, resources, etc. I think apps should simply return, and the launcher can handle resetting to a clean slate as an exiting app will drop through to the launcher context and end up here:
I need time to test this and see how it feels, but it's looking good and potentially a boon for MicroPython users. Would be happy to merge. |
Configurable default clock in Micropython would be great. Until then I think just using Using Thonny on Windows I've had no issues with the repeated reboots other than Windows making the device removed/device inserted noise every time I press a button! I have had the USB on my RPi 4 stop working a couple of times though, which it has done before when talking to Picos, but this repeated rebooting may well be exacerbating it. Anyway, it would ideally be nice if we could do this without restarting the USB - maybe instead of resetting we could throw an exception that was handled in a wrapper around launcher.py that deleted absolutely everything and reimported launcher? Note that I'll change the apps I've modified so far so they only rewrite the file when necessary. I'll also look at making launcher itself work like this, as that's another key screen that people will leave their Badgers on, and take a look at how we could handle exit more cleanly. |
Just gave it a try and it's working phenomenally. Nice job! Of course I'm always developing tethered to USB and never on battery so I'm a little detached from the full impact 😬 I need to get out more. I didn't expect this to work quite as well as it does in MicroPython. The startup time is fast enough to catch a "normal" button press, but does miss if you hit them really quickly. I wonder if there's anything I can do to latch the button input state earlier without unleashing even more hacks on the MicroPython codebase 😬 The USB connect/disconnect bounce is pretty gnarly and it's easy to get into a state where a manual "reset" is required to get Thonny working again, and very difficult to debug code that halts before it gets a chance to output any error state. I wonder if it's possible to handle both cases so that on external USB power I also managed to get stuck in the "To add images" screen of the Image demo which is how I came about the above debugging issue. Both cases could be handled with a pattern like this: while True:
if display.pressed(button): # Internally returns pressed_to_wake OR pressed
# Do something
display.halt() # Doesn't halt if connected via USB to Thonny The But via Thonny it would just continue to loop and run continuously for debugging. Edit: your button wake state stuff is far more clever than I had anticipated. But I don't think it precludes the above pattern. AFAICT the pimoroni-pico/micropython/modules/badger2040/badger2040.cpp Lines 240 to 244 in c41714c
Could be something like this: mp_obj_t Badger2040_pressed(mp_obj_t self_in, mp_obj_t button) {
_Badger2040_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Badger2040_obj_t);
self->badger2040->update_button_states();
bool wake_state = (button_wake_state.get() >> mp_obj_get_int(button)) & 1;
bool state = self->badger2040->pressed(mp_obj_get_int(button));
return state || wake_state ? mp_const_true : mp_const_false;
} Which folds Bit sketchy around clearing the state after this, but maybe we could drop the clear function and just clear them on read by making it: bool wake_state = button_wake_state.get(mp_obj_get_int(button); // Function reads, stores, clears and returns the chosen pin |
The I have also seen it get stuck with the text up after pressing button C on the image app. In this state it isn't halted, it's somehow stuck in the I like your idea of making a |
Agreed. There might be a sleep bug, since that does ring a bell, though it works in a while loop for some reason. Very odd! I think having "pressed()" return the wake state versus regular button state could be possibly confusing but I can't really elucidate on why that might be since it doesn't differ all that much from what I had in mind. One argument against it is that it's pesky keeping track of "pressed" having been called on a per-button basis. My current untested changed: diff --git a/micropython/modules/badger2040/badger2040.cpp b/micropython/modules/badger2040/badger2040.cpp
index f15468d..9cda9ff 100644
--- a/micropython/modules/badger2040/badger2040.cpp
+++ b/micropython/modules/badger2040/badger2040.cpp
@@ -14,7 +14,16 @@ namespace {
gpio_put(pimoroni::Badger2040::ENABLE_3V3, 1);
}
- uint32_t get() const { return state; }
+ uint32_t get(uint32_t pin) const {
+ return state & (0b1 << pin);
+ }
+
+ uint32_t get_once(uint32_t pin) {
+ uint32_t mask = 0b1 << pin;
+ bool value = state & mask;
+ state &= ~mask;
+ return value;
+ }
void clear() { state = 0; }
private:
@@ -201,7 +210,7 @@ MICROPY_EVENT_POLL_HOOK
#endif
self->badger2040->update_button_states();
}
- watchdog_reboot(0, SRAM_END, 0);
+ //watchdog_reboot(0, SRAM_END, 0);
return mp_const_none;
}
@@ -240,12 +249,13 @@ mp_obj_t Badger2040_thickness(mp_obj_t self_in, mp_obj_t thickness) {
mp_obj_t Badger2040_pressed(mp_obj_t self_in, mp_obj_t button) {
_Badger2040_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Badger2040_obj_t);
self->badger2040->update_button_states();
+ bool wake_state = button_wake_state.get_once(mp_obj_get_int(button));
bool state = self->badger2040->pressed(mp_obj_get_int(button));
- return state ? mp_const_true : mp_const_false;
+ return (state || wake_state) ? mp_const_true : mp_const_false;
}
mp_obj_t Badger2040_pressed_to_wake(mp_obj_t button) {
- bool state = (button_wake_state.get() >> mp_obj_get_int(button)) & 1;
+ bool state = button_wake_state.get(mp_obj_get_int(button));
return state ? mp_const_true : mp_const_false;
} I'll be honest, I don't fully understand the absolute magic you're working with the wake states, and in particular: |
Yes that's pretty much what I had in mind. I'll apply that to my branch and give it a test this evening. That Edit to add: Ah, I probably confused you by declaring the struct and instantiating it in a single statement, which is a rather C thing to do, not very C++. |
I'm having trouble detecting if an app has been woken up versus first run. On first run - for example - I guess the litmus test for "have I been woken up" is any button being set in the wake state. Hypothesizing something like: mp_obj_t Badger2040_woken(mp_obj_t self_in) {
(void)self_in;
return button_wake_state.any() ? mp_const_true : mp_const_false;
} Coupled with: import os
import sys
import time
import badger2040
from badger2040 import WIDTH, HEIGHT
REAMDE = """
Images must be 296x128 pixel with 1bit colour depth.
You can use examples/badger2040/image_converter/convert.py to convert them:
python3 convert.py --binary --resize image_file_1.png image_file_2.png image_file_3.png
Create a new "images" directory via Thonny, and upload the .bin files there.
"""
OVERLAY_BORDER = 40
OVERLAY_SPACING = 20
OVERLAY_TEXT_SIZE = 0.5
TOTAL_IMAGES = 0
# Try to preload BadgerPunk image
try:
os.mkdir("images")
except OSError:
pass
try:
import badgerpunk
with open("images/badgerpunk.bin", "wb") as f:
f.write(badgerpunk.data())
f.flush()
with open("images/readme.txt", "w") as f:
f.write(REAMDE)
f.flush()
del badgerpunk
except (OSError, ImportError):
pass
try:
IMAGES = [f for f in os.listdir("/images") if f.endswith(".bin")]
TOTAL_IMAGES = len(IMAGES)
except OSError:
pass
display = badger2040.Badger2040()
image = bytearray(int(296 * 128 / 8))
current_image = 0
show_info = True
# Draw an overlay box with a given message within it
def draw_overlay(message, width, height, line_spacing, text_size):
# Draw a light grey background
display.pen(12)
display.rectangle((WIDTH - width) // 2, (HEIGHT - height) // 2, width, height)
# Take the provided message and split it up into
# lines that fit within the specified width
words = message.split(" ")
lines = []
current_line = ""
for word in words:
if display.measure_text(current_line + word + " ", text_size) < width:
current_line += word + " "
else:
lines.append(current_line.strip())
current_line = word + " "
lines.append(current_line.strip())
display.pen(0)
display.thickness(2)
# Display each line of text from the message, centre-aligned
num_lines = len(lines)
for i in range(num_lines):
length = display.measure_text(lines[i], text_size)
current_line = (i * line_spacing) - ((num_lines - 1) * line_spacing) // 2
display.text(lines[i], (WIDTH - length) // 2, (HEIGHT // 2) + current_line, text_size)
def show_image(n):
file = IMAGES[n]
name = file.split(".")[0]
open("images/{}".format(file), "r").readinto(image)
display.image(image)
if show_info:
name_length = display.measure_text(name, 0.5)
display.pen(0)
display.rectangle(0, HEIGHT - 21, name_length + 11, 21)
display.pen(15)
display.rectangle(0, HEIGHT - 20, name_length + 10, 20)
display.pen(0)
display.text(name, 5, HEIGHT - 10, 0.5)
for i in range(TOTAL_IMAGES):
x = 286
y = int((128 / 2) - (TOTAL_IMAGES * 10 / 2) + (i * 10))
display.pen(0)
display.rectangle(x, y, 8, 8)
if current_image != i:
display.pen(15)
display.rectangle(x + 1, y + 1, 6, 6)
display.update()
def save_appstate():
# Tell launcher to relaunch this app on wake and record state
with open("appstate.txt", "w") as f:
f.write("image\n")
f.write("{}\n{}\n".format(current_image, show_info))
if TOTAL_IMAGES == 0:
display.pen(15)
display.clear()
draw_overlay("To run this demo, create an /images directory on your device and upload some 1bit 296x128 pixel images.", WIDTH - OVERLAY_BORDER, HEIGHT - OVERLAY_BORDER, OVERLAY_SPACING, OVERLAY_TEXT_SIZE)
display.update()
sys.exit()
try:
with open("appstate.txt", "r") as f:
if f.readline() == "image":
current_image = int(f.readline().strip('\n'))
show_info = f.readline().strip('\n') == "True"
except OSError:
pass
changed = not display.woken()
while True:
if display.pressed(badger2040.BUTTON_UP):
if current_image > 0:
current_image -= 1
changed = True
if display.pressed(badger2040.BUTTON_DOWN):
if current_image < TOTAL_IMAGES - 1:
current_image += 1
changed = True
if display.pressed(badger2040.BUTTON_A):
show_info = not show_info
changed = True
if display.pressed(badger2040.BUTTON_B) or display.pressed(badger2040.BUTTON_C):
display.pen(15)
display.clear()
draw_overlay("To add images connect Badger2040 to a PC, load up Thonny, and see readme.txt in images/", WIDTH - OVERLAY_BORDER, HEIGHT - OVERLAY_BORDER, OVERLAY_SPACING, 0.5)
display.update()
print(current_image)
time.sleep(4)
changed = True
if changed:
save_appstate()
show_image(current_image)
changed = False
# Halt the Badger to save power, it will wake up if any of the front buttons are pressed
display.halt() |
Can you use whether the appstate.txt was preset to tell whether the app has just been started? That said, I imagine having a woken method could potentially be useful. |
Patch for diff --git a/micropython/modules/badger2040/badger2040.c b/micropython/modules/badger2040/badger2040.c
index 18d63b4..536811d 100644
--- a/micropython/modules/badger2040/badger2040.c
+++ b/micropython/modules/badger2040/badger2040.c
@@ -10,6 +10,7 @@ MP_DEFINE_CONST_FUN_OBJ_1(Badger2040_update_obj, Badger2040_update);
MP_DEFINE_CONST_FUN_OBJ_KW(Badger2040_partial_update_obj, 4, Badger2040_partial_update);
MP_DEFINE_CONST_FUN_OBJ_1(Badger2040_halt_obj, Badger2040_halt);
+MP_DEFINE_CONST_FUN_OBJ_1(Badger2040_woken_obj, Badger2040_woken);
MP_DEFINE_CONST_FUN_OBJ_2(Badger2040_invert_obj, Badger2040_invert);
MP_DEFINE_CONST_FUN_OBJ_2(Badger2040_led_obj, Badger2040_led);
@@ -47,6 +48,7 @@ STATIC const mp_rom_map_elem_t Badger2040_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_partial_update), MP_ROM_PTR(&Badger2040_partial_update_obj) },
{ MP_ROM_QSTR(MP_QSTR_halt), MP_ROM_PTR(&Badger2040_halt_obj) },
+ { MP_ROM_QSTR(MP_QSTR_woken), MP_ROM_PTR(&Badger2040_woken_obj) },
{ MP_ROM_QSTR(MP_QSTR_invert), MP_ROM_PTR(&Badger2040_invert_obj) },
{ MP_ROM_QSTR(MP_QSTR_led), MP_ROM_PTR(&Badger2040_led_obj) },
diff --git a/micropython/modules/badger2040/badger2040.cpp b/micropython/modules/badger2040/badger2040.cpp
index 1736daa..c3c0c6c 100644
--- a/micropython/modules/badger2040/badger2040.cpp
+++ b/micropython/modules/badger2040/badger2040.cpp
@@ -14,6 +14,10 @@ namespace {
gpio_put(pimoroni::Badger2040::ENABLE_3V3, 1);
}
+ bool any() const {
+ return state > 0;
+ }
+
bool get(uint32_t pin) const {
return state & (0b1 << pin);
}
@@ -199,6 +203,11 @@ MICROPY_EVENT_POLL_HOOK
return mp_const_none;
}
+mp_obj_t Badger2040_woken(mp_obj_t self_in) {
+ (void)self_in;
+ return button_wake_state.any() ? mp_const_true : mp_const_false;
+}
+
mp_obj_t Badger2040_halt(mp_obj_t self_in) {
_Badger2040_obj_t *self = MP_OBJ_TO_PTR2(self_in, _Badger2040_obj_t);
diff --git a/micropython/modules/badger2040/badger2040.h b/micropython/modules/badger2040/badger2040.h
index ea48655..0586b3e 100644
--- a/micropython/modules/badger2040/badger2040.h
+++ b/micropython/modules/badger2040/badger2040.h
@@ -16,6 +16,7 @@ extern mp_obj_t Badger2040_update(mp_obj_t self_in);
extern mp_obj_t Badger2040_partial_update(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args);
extern mp_obj_t Badger2040_halt(mp_obj_t self_in);
+extern mp_obj_t Badger2040_woken(mp_obj_t self_in);
extern mp_obj_t Badger2040_invert(mp_obj_t self_in, mp_obj_t invert);
extern mp_obj_t Badger2040_led(mp_obj_t self_in, mp_obj_t brightness); It's possible to reset via Thonny or otherwise in ways that don't affect the appstate.txt but might want to do an initial refresh. Might need to have a think! |
6c29483
to
fe943a0
Compare
b52f537
to
39a9f05
Compare
Note if apps set up their own interrupt handlers then they take precedence, but in that case the app should be responsible for quitting.
39a9f05
to
d313a9d
Compare
Status is that image, badge and launcher itself all use the new system. They don't reset the Badger except when transitioning from an app back to launcher (which is how it's always been done). I made the change to handle quitting an app using an interrupt handler (though it doesn't require you to hold the buttons down for any period). This is done in the Micropython layer so if an app registers its own handler then it'll override the badger_os one. That's probably OK as apps should be using the new system - but if we want to allow this we could probably set a shared IRQ handler at the C++ layer instead, though we'd have to be careful not to tread on Micropython's toes. |
This. 😬 Welp I'm up earlier than anticipated so I guess it's time to give this a whirl! Ha - 1 in a million fluke: I plugged my badger2040 into USB and it flipped the invert bit on the e-ink display. |
I'm still seeing that weird bug in image.py where changing the image doesn't latch the power on. I think it's somehow doing a non-blocking image update and dropping straight through to |
This is on battery? I've just had a decent play around and can't reproduce it. The thing I was seeing before was the python sleep() never exiting when the host USB had got into a bad state, but with no USB connected that obviously can't be the cause. Only thing I can think of is if the display busy pin is slow to activate - maybe try a few microseconds of sleep in |
This is on LiPo. I just wrote a bunch of code to ensure the display update function waits for the display update to finish and it made no difference, I was barking up the wrong tree it seems. Experimenting with using the "act" LED to actually indicate when Badger2040 is awake, too. |
I cannot reproduce it again now, it's very odd! Act LED is great, though. Should absolutely turn it on first thing in the launcher. |
This may be a fix or a placebo: b1fd893 |
…' into battery-improvements
There's been some chat in the Discord about how the Badger2040 eats through the battery when it's not doing anything. I had a look into the Micropython side of things and saw that Badger doesn't halt when running these apps (and it's running at 125MHz), which means it's using 20mA or so instead of almost nothing.
This is my attempt to start resolving this problem. In summary I have:
Exposing halt and pressed to wake
I've not directly exposed the Badger2040 methods for these, instead implementing them independently in the Micropython module. For halt, this allows Thonny to interrupt even when the Badger is "halted" but connected to USB so the power doesn't go off.
For pressed to wake, the motivation is to capture the button press as soon as possible. Waiting for python to come up and the Badger2040 instance to be created is way too slow, you have to hold down the button for ages for it to register. Instead, I have a static singleton class and grab the button state in its constructor. This does catch most reasonable length button presses, but it still sometimes misses short ones. This could probably be improved by moving this function into preinit, so that the state was captured even earlier during boot - I might look into that later.
For pressed to wake, I've exposed the button state as a global function on the badger2040 module, as well as on the Badger2040 object. This allows launcher to quickly check the state without having to instantiate a Badger object it would then immediately destroy.
Enable 3v3 earlier
Like capturing the button presses, waiting until the Badger object is instantiated before setting the 3v3 enable line makes it hard to wake the Badger up when it's on battery. Instead, I now set the 3v3 enable pin high in the singleton constructor discussed above.
Launcher changes - appstate.txt
In order to give the experience of seamlessly using an app while the badger is actually rebooting between each button press, launcher needs to immediately drop into the app again instead of displaying the menu. I've acheived this by writing a short text file appstate.txt, which contains the name of the app on the first line, and any app specific state can be stored on subsequent lines.
If this file is present and the first line is a valid app name, then launcher loads that app straight away, unless buttons A and C are pressed. This is my suggestion of a new convention whereby pressing buttons A and C together cause the app to exit, instead of having to use the reset button. Using the reset button doesn't work because on battery pressing only the reset button doesn't wake the Badger.
There is maybe a question of whether we could wear out the flash with frequent writes - possibly it would be best to at least not to rewrite the file if state hadn't changed. Another possibility might be to reserve some flash in the image and provide API methods to read/write this data instead of using the file system, then the location of that block could be changed on each firmware update at least.
Note that not all apps have to use this mechanism - more interactive apps or apps that you aren't likely to leave running probably shouldn't use it, though potentially they should time out and halt after a few minutes.
On my way past I've also added a
machine.freq(48000000)
to the top of launcher, which ~halves power usage and I haven't seen a noticeable difference in performance. I considered putting a call to reduce the clock into the singleton constructor, but I thought it was useful to users to make clear how the clock was being configured, and running the boot at 125MHz isn't going to do any singificant harm.Badge and image examples
I've changed these in the obvious way - the image example shows how additional state can be stored in the appstate file.
I haven't changed any more examples yet, but if you'd like to accept these changes then I'm happy to do so.
With these changes, I can just about run these apps off a single new coin cell, providing I give it a bit of time to rest rather than mashing buttons continuously. One coin cell is unlikely to be able to provide sufficient current past the very early stages of its life though.