adafruit / circuitpython

CircuitPython - a Python implementation for teaching coding with microcontrollers
https://circuitpython.org
Other
4.08k stars 1.2k forks source link

Any reason why `time.sleep()` does not support microseconds #8415

Open ZodiusInfuser opened 1 year ago

ZodiusInfuser commented 1 year ago

Hi,

I am attempting to implement half duplex uart for a feature on the upcoming Pimoroni Yukon board, and am in need of doing sub millisecond time delays. Micropython has sleep_us on their RP2040 build but CircuitPython does not, and although the regular sleep function accepts any floating point value, it is quantised to milliseconds.

Here is my analyser output from running time.sleep() with any microsecond value: image In it the uart TX is being routed to the half duplex line based on the direction pin. As you can see, the direction pin changes before all the data is sent, meaning it gets cut off.

Doing a 1 millisecond delay has the direction change take too long: image

Yet, if I add in my own sleep_us function to the CircuitPython build, I can get the direction change happening exactly where it's needed with a time.sleep_us(350) delay.

STATIC mp_obj_t time_sleep_us(mp_obj_t microseconds_o) {
    mp_int_t usecs = mp_obj_get_int(microseconds_o);
    if (usecs < 0) {
        mp_raise_ValueError(translate("sleep length must be non-negative"));
    }
    common_hal_time_delay_us(usecs);
    return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(time_sleep_us_obj, time_sleep_us);

image

This was very easy for me to add, which makes me think there is a good reason why CircuitPython does not have this ability? My immediate thought is boards that do not support us delays, which my code above does not check for.

Perhaps someone could point me to how I could make this change just for RP2040, or maybe just for the Pimoroni Yukon, which I already have an in-progress PR for (https://github.com/adafruit/circuitpython/pull/7440)

Thanks

deshipu commented 1 year ago

https://docs.circuitpython.org/en/latest/shared-bindings/microcontroller/index.html#microcontroller.delay_us

deshipu commented 1 year ago

The reason why this is not in the time module is compatibility with cpython.

jepler commented 1 year ago

I'm not sure why time.sleep can't call the microsecond sleep function, though, similar to (untested)

STATIC mp_obj_t time_sleep(mp_obj_t seconds_o) {
    mp_float_t seconds = mp_obj_get_float(seconds_o);
    if (seconds < 0) {
        mp_raise_ValueError(translate("sleep length must be non-negative"));
    }
    if (seconds < MICROPY_FLOAT_CONST(.001)) {
        mp_float_t usecs = MICROPY_FLOAT_CONST(1e-6) * seconds + MICROPY_FLOAT_CONST(0.5);
        common_hal_time_delay_us(usecs);
    } else {
        mp_float_t msecs = MICROPY_FLOAT_CONST(1e-3) * seconds + MICROPY_FLOAT_CONST(0.5);
        common_hal_time_delay_ms(msecs);    
    }   
    return mp_const_none;
}
MP_DEFINE_CONST_FUN_OBJ_1(time_sleep_obj, time_sleep);
dhalbert commented 1 year ago

It could also use the microsecond sleep for that part of the requested sleep that was less than 1 msec, and use the millisecond sleep for the other part. The microsecond sleep is a busy-wait loop. It doesn't turn off interrupts, so any delay might be longer than requested due to intervening interrupt handling. There are routines to turn interrupts off and on.

jepler commented 1 year ago

I don't think this bug is specific to rp2, the implementation of time_sleep that always calls into delay_ms is in shared-bindings.

ZodiusInfuser commented 1 year ago

Thanks @deshipu! I was completely unaware of that function existing, so that explains my confusion about it being seemingly absent.

@jepler That would be nice, though for my needs I wouldn't want to push for a potentially breaking change like that.

tannewt commented 1 year ago

I'm ok with time.sleep() not being super precise because python code execution isn't really either. A GC or background task can take non-trivial time at nearly any point. Python code really shouldn't assume precise timing.