8000 Add accurate interrupt counter · Issue #3775 · micropython/micropython · GitHub
[go: up one dir, main page]

Skip to content

Add accurate interrupt counter #3775

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

Open
laurivosandi opened this issue May 7, 2018 · 11 comments
Open

Add accurate interrupt counter #3775

laurivosandi opened this issue May 7, 2018 · 11 comments
Labels
enhancement Feature requests, new feature implementations

Comments

@laurivosandi
Copy link
laurivosandi commented May 7, 2018

The microcontrollers support counting rising/falling edges at rather fast pace (1-10kHz), eg to measure RPM of a motor with hall sensor or disc encoder. It would be nice to have the interrupt handling implemented in C code and export something easier to use into Python:

from machine import Counter, Pin
from time import sleep
counter = Counter(Pin(34))
while True:
    count, duration = counter.poll()
    print("%d RPM" % (count * 60 / duration))
    sleep(1)

In this pseudo-code instantiating a counter object sets up interrupt handlers and an integer for storing the count of events and another integer for storing current timestamp. When poll() function is executed, count and timestamp delta are returned and variables are reset.

The main benefit of such approach is accuracy and this probably hogs the Python runtime less as well especially at higher frequecies (1kHz and above)

@adritium
Copy link
adritium commented May 8, 2018

This sounds like it's very specific to each port i.e. each microcontroller because, as you noticed, everything is done using C (and maybe assembly) + interrupts.

Almost all the work would be done setting up the counter peripheral and interrupts in C/assembly; the actual code to create the uPy module and update the counter for uPy to read is trivial.

NOTE that I said each MICROCONTROLLER not each core ISA i.e. ARM, ST, Freescale, NXP.
If you have 2 micros from any manufacturer (from two separate families), you'd have to write two different counter+interrupt set up C-functions; maybe they'd be a little similar and if you're lucky all that's different is the memory-mapped locations so that you can re-use your setup C-function.

A big complication is that for the more complicated micros, sometimes external pins (or even internal resources) are shared and you might get into a situation where you can have, for example, (edge counting enabled) or . . . (something else) but not both. Or even more complicated: (edge counting at max rate (like 10KHz)) or (edge counting at a slower rate AND some other peripheral enabled).

I haven't done low-level drivers in a long time but I did JUST talk to a guy who is doing it daily and he gave an example: certain micro has 2 CAN-FD bus channels and a SD-CARD channel. If you activate the second CAN-FD bus channel, the SD-CARD max data transfer rate is drastically reduced because the 2nd CAN-FD channel takes away pins from the SD-CARD and those are the pins that enable faster SD-CARD data transmission.

The same thing could happen with internal multiplexing: maybe peripheral A takes up timers/special RAM area/etc and activating peripheral B, which also needs some of those resources, causes peripheral A to run slower.

So, if you want to do this the user-friendly way, you'd have to implement all those dependencies - they're in the manual - and if someone tries to instantiate two peripherals with competing internal (or external) resources . . . I assume you'd want to warn the user.

Or take the easy way out: you expect the user to read the 2,000 page manual to figure out all the cross-dependencies and if instantiating the 2nd CAN-FD channel reduces their SD-CARD write speed, too bad for the user; they should've read the manual.

@laurivosandi
Copy link
Author

Hi, I am well aware of such caveats when it comes to the microcontrollers in general. I am saying the interrupt handling is already there in MicroPython, see https://micropython.org/resources/docs/en/latest/wipy/reference/isr_rules.html

What I really need is just have the the interrupt handler written in C and just have it counting the events (blah++) and timestamp of last poll - and when polled return the count of events and reset both variables.

My experience using MicroPython ESP32 shows that externally triggered interrupts and timers have pretty terrible jitter, but having a simple C probably yields near real-time response.

@adritium
Copy link
adritium commented May 8, 2018

I'm looking at this

import pyb, micropython
micropython.alloc_emergency_exception_buf(100)
class Foo(object):
    def __init__(self, timer, led):
        self.led = led
        timer.callback(self.cb)
    def cb(self, tim):
        self.led.toggle()

You're saying that instead of the ISR calling self.cb, you want it to call only a C-function wherein you update blah and update some timeStamp member of Foo which is accessed via uPy Poll()?

@laurivosandi
Copy link
Author

Hi,

yes something like that. I am saying the overhead of the Python interpreter in the callback machinery is probably the source of jitter/lag, having it do the counting in C and occasionally report it to the Python code probably gets the job done way better

@adritium
Copy link
adritium commented May 8, 2018

In order to completely avoid

the overhead of the Python interpreter in the callback machinery

you would have to create your own custom uPy firmware and flash it.
However, there are some optimizations that may give you the speedup you require without necessitating building your own firmware. FYI: once the firmware is built, it's not possible to add C-code, yet: #3311 , #583

Besides compiling to bytecode - which results in Python VM interpreter getting involved - you can choose to compile, at the granualarity of a uPy function, to 3 other binary formats: Native, Viper, assembly. NOTE: frozen .mpy with these formats may not be implemented.

Native or Viper code emitter. The explanation is here. Of course it's entirely possible that even the Native/Viper formats will be too slow; I have no experience with it. If those two formats are not fast enough, you can actually write native assembly.

I haven't personally used any of these but based on this I'd guess it does avoid the virtual machine (after maybe a header is processed) because there are no bytecodes; it's all assembly instructions.

But if writing assembly is too hard or Native/Viper is too slow, your only other option is creating custom firmware.

ISR overhead
I manually traced through the path for a timer interrupt and it doesn't seem like there's much C-code up to the point of uPy VM.

ftm0_isr() -> ftm_irq_handler() -> ftm_handle_irq_callback() ->mp_call_function_1()

So, the majority of overhead is definitely, as you stated, after you call mp_call_function_1(). If you can get the Native/Viper/assembly working you should see a huge speedup.

@dhylands
Copy link
Contributor
dhylands commented May 9, 2018

The real issue on the ESP32 and ESP8266 is that there is essentially an underlying OS running under micropython and micropython isn't really running completely bare metal. The underlying OS will wind up disabling interrupts and/or perform other activities which you have no control over. These activities are what cause the jitter. This really has nothing to do with micropython itself. You'd notice the same behavior if you were coding in C.

If you're running on a pyboard, there is no other OS. Interrupt latency is much more deterministic, and consequently you get much less jitter.

There are techniques that can be used to reduce the jitter, but these typically require intimate knowledge of the microcontroller and OS that you're running on.

@adritium
Copy link
adritium commented May 9, 2018

The underlying OS will wind up disabling interrupts and/or perform other activities which you have no control over.

Can't you configure the underlying OS?

@WayneKeenan
Copy link

@adritium
Copy link
adritium commented May 9, 2018

@WayneKeenan that should be for @laurivosandi; my question was rhetorical i.e. "you can configure the OS so the OS shouldn't be an issue"

@dpgeorge
Copy link
Member

@laurivosandi thanks for the input on the addition of such a counter. There is an existing function machine.time_pulse_us() which is related and times a single pulse width. What you suggest is a way to time multiple pulses in an asynchronous way (ie in the background).

IMO this is a good thing to try and add, precisely because it is 1) timing sensitive so best written in C; 2) different to implement on each MCU so helps the user if they don't need to remember/know details of all controllers.

The hard part is to define a good, general API for such a functionality, that can be implemented on a broad range of devices. As I see it, the main concepts/building-blocks here is a counter object which can be incremented by an external event, in this case a pin change event. Note that such a thing already exists on pyboard/stm32.

@IhorNehrutsa
Copy link
Contributor

docs\machine: Add Counter and Encoder classes. #8072
ESP32: New hardware PCNT Counter() and Encoder(). #6639

@jonnor jonnor added the enhancement Feature requests, new feature implementations label Sep 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Feature requests, new feature implementations
Projects
None yet
Development

No branches or pull requests

7 participants
0