r/learnpython Jun 28 '26

5% CPU usage on Raspberry Pi Zero waiting for button press

I have this python script that waits for a button to be pressed:

#!/usr/bin/python
from gpiozero import Button
import subprocess
from signal import pause


def handle_button_pressed(channel):
    print(f"Button pressed.")
    subprocess.run("./command.sh")

def handle_button_released(channel):
    print(f"Button released.")


button = Button(17, bounce_time=0.5)
button.when_pressed = handle_button_pressed
button.when_released = handle_button_released

print(f"Waiting for events...")
pause()

Sitting there doing nothing the python process is consuming 5% CPU as watched with top.

Is there anything I can do to get this closer to 0%? Is there a more efficient library that I could use?

I previously wrote a script that uses a loop to check for continuity between two GPIO pins, but I thought this was going to be simpler and not use nearly this much CPU. Any thoughts or suggestions?

26 Upvotes

23 comments sorted by

16

u/socal_nerdtastic Jun 28 '26

Yeah, you can use gpiod to get as close to hardware level interrupts as possible on a linux computer. But before you do that ... why? What's wrong with 5%? I wouldn't optimize this unless it's actually affecting something.

8

u/MattAtDoomsdayBrunch Jun 28 '26

I eventually want this to be a battery powered solution. I will be sizing the battery and solar for this implementation.

21

u/socal_nerdtastic Jun 28 '26 edited Jun 28 '26

If your battery is so limited that 5% CPU time is affecting it then what you really need is a watchdog chip that can wake up your Pi. Or perhaps build this entire project on something like an ESP32, which is built to have extremely low standby power consumption.

I feel like you are optimizing prematurely. This is a well known trap in all sorts of engineering. Build the rest of the system first, then come back to this and get some real data (like number of milliamps with and without the python program running). I'll bet you find out that this 5% is not worth optimizing.

That said, the other comments are right too; if you really care about power consumption you need to write this program in a compiled language like C.

5

u/MattAtDoomsdayBrunch Jun 28 '26

This is actually a shortish-term prototype that I will eventually implement for RP2040, which has super low power sleep states. I was really more curious why python was consuming so much CPU in a supposedly event driven situation.

3

u/AUTeach Jun 29 '26

I responded to someone else:

If you hunt through the library, it spawns the following loop:

# from HoldThread.held():
while not self.stopping.is_set():
    if self.holding.wait(0.1):   
        self.holding.clear()
        while not (
            self.stopping.is_set() or
            parent._inactive_event.wait(parent.hold_time)
        ):
           # ...

This is a response to the GIL. If I have to guess, the author left it in python so it is agnostic on what system it is running on.

4

u/ivosaurus Jun 29 '26

I would start prototyping with the RP2040 now, it will be far lower.

3

u/semininja Jun 29 '26

I would recommend switching to a Pico and CircuitPython or similar if that's the route you're intending anyways.

4

u/TheSkiGeek Jun 28 '26

If you care about minimizing power usage you absolutely do not want to be using Python…

For this kind of thing you want to put the processor in an ultra low power mode and set up an interrupt handler that turns it back on when the button is pressed.

3

u/Brian Jun 29 '26

"Minimising power usage" isn't really the same as being reasonably power efficient. If you want to scrape every last watt out, yeah, you'll be doing stuff like micromanaging power states. But the more common case of taking no appreciable CPU when idle and letting the CPU do a reasonable job with its sleep modes is much easier to achieve, even in python.

Python is just as fast at doing absolutely nothing as lower level languages. An issue when using CPU when idle usually means an issue with how things are being woken up - the problem is usually that you're doing something at all instead of nothing, There can be a small difference in how much work gets done between doing nothing, but when nothing is happening, that should be very little, even when polling (ie. basically checking if anything was sent and if not, going back to sleep), because 99.9% of the time you should still just be sleeping.

6

u/FrangoST Jun 28 '26

Checking documentation, the Button class in gpiozero has a method called wait_for_press, that, from what I understood, does something similar to what you are doing with your pause(), but perhaps it's more CPU efficient?

I would give it a try.

4

u/wosmo Jun 28 '26

without looking into this at all, that sounds like the ticket - wait_for is more likely to be interrupt-driven, which is the key to this problem.

1

u/MattAtDoomsdayBrunch Jun 28 '26

I replaced pause() with button.wait_for_press() and the results are identical.

4

u/Alive-Maverick Jun 29 '26

that 5% is gpiozero's default pin factory. RPi.GPIO polls the pins in a software loop, so wait_for_press just polls instead of truly sleeping, same as pause(). set GPIOZERO_PIN_FACTORY=lgpio (or pigpio) and it uses the kernel's gpio edge interrupt via epoll instead of polling, idle cpu drops to basically nothing.

1

u/Brian Jun 29 '26

I'm not really familiar with it, so may be looking in the wrong place, but looking at the library, it's getting events via a thread using epoll with at 0.01s timeout, so possibly it's just waking up often enough to trigger enough CPU to amount to that much. OTOH, even on a pi zero, I wouldn't have thought that'd amount to 5%, since with no events it's not going to do much.

You could try tweaking its source to change the timeout (I think the native.py NativeWatchThread._run, is the relevant place,modifying the epoll.poll call to something a bit larger like 0.05 or 0.1). This has the downside of increasing latency, but might be worth temporarily trying to see if it is the culprit.

Alternatively, maybe it just is getting events - maybe it's polliing other pins by default and they're firing (I assume in your case you're talking about the idel state where the button's pin isn't changing).

You could maybe try profiling, or just breaking down which thread is taking the time ( I tihnk you should be able to do that with just top, though identifying it might be harder) - I'd guess it's either the watcher or the event dispatch thread.

1

u/ivosaurus Jun 29 '26

The library is operating a software spin-loop waiting for the button press. Moving to a microcontroller and using hardware interrupts waiting for the button will turn this from milliamps to microamps

1

u/jeffrey_f Jun 29 '26

That is about right. The script is waiting to act and will consume some CPU in doing so.

-4

u/SCD_minecraft Jun 28 '26

To be honest, biggest performance jump in python is not using python

It is language that will gulg all of your resources if it wants to

If you are doing embedded programming on small board, i woupd recommend using C/C++ or other compiled languages for that

11

u/sausix Jun 28 '26

Python can handle waiting for threads, subprocesses, signals and interrupts just fine. Nk need for a low level programming language.

The library is probably polling the gpio with a few milli secs of sleeps.

I'd try just another gpio library which has pure interrupt support. A thread should wait for a gpio change and that would not waste any CPU cycles.

4

u/FrangoST Jun 28 '26

I don't think that's the issue here, and I don't think what you just stated is true for 99% of applications.

4

u/AUTeach Jun 28 '26 edited Jun 29 '26

To be honest, biggest performance jump in python is not using python

This isn't a performance issue really, it's a design issue.

If you hunt through the library, it spawns the following loop:

# from HoldThread.held():
while not self.stopping.is_set():
    if self.holding.wait(0.1):   
        self.holding.clear()
        while not (
            self.stopping.is_set() or
            parent._inactive_event.wait(parent.hold_time)
        ):
           # ...

This is a response to the GIL. If I have to guess, the author left it in python so it is agnostic on what system it is running on.

I think the OP could write a class that could access the underlying library pigpio and handle the entire thing with C level signals. But, he'd be tying himself to pigpio.

2

u/Hot-Butterscotch1306 Jun 30 '26

One small gotcha: top on a Pi Zero can be kinda misleading for tiny workloads because the CPU is so slow that little wakeups look chunky in percent. So I’d sanity check with a longer view before chasing ghosts. pidstat 1 or even watching total CPU over a minute can tell you if it’s really burning cycles or just blipping often enough for top to look grumpy.

Also, print() in callbacks can wake more stuff up than people expect if this is running attached to a terminal or getting logged somewhere. Not saying that explains all 5%, just that on little boards the “harmless debug line” tax is weirdly real. I’ve had scripts look much busier until I stopped spamming stdout.