-
-
Notifications
You must be signed in to change notification settings - Fork 8.2k
ESP32 RMT: two enhancement suggestions #7015
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
Comments
This would be a great change to the RMT interface. For driving WS2811 LED's on RMT speed is everything. My current approach is to calculate the pulses and store them as lines of bytes in a file. For each animation tick I take that frames (line) bytes and transform them into a list/tuple. This transform step takes time (+ object memory) and impacts the achievable FPS of an animation. I think that C may be better and transforming bytes on the fly than having to create tuple/list objects just to suffice the interface. That said, for me this project has been on hold for some time and I need to give it a go with the updated firmware (project currently using 1.12) to see if there has been some incremental improvement. |
now, make it 3 enhancement suggestions :-) this is inspired by stepper motor use case from REQUEST No 3: please make USE CASE: to accelerate/decelerate a stepper motor, |
RE: #7015 GIST: https://gist.github.com/Andrei-Pozolotin/c4b3dd041efe53cfb92cfb4de9c67267 Here is a working experimental implementation. Basic approach is:
this is accomplished with addition of 2 functions to (a copy with changes:) ports/esp32/esp32_rmt.c// RMT.store_pulses(self, item_list:list[int]) -> None
STATIC mp_obj_t esp32_rmt_store_pulses(size_t n_args, const mp_obj_t *args) {
esp32_rmt_obj_t *self = MP_OBJ_TO_PTR(args[0]);
mp_obj_t item_list_obj = args[1];
size_t num_items = 0;
mp_obj_t *item_list_ptr = NULL;
mp_obj_get_array(item_list_obj, &num_items, &item_list_ptr);
if (num_items > self->num_items) {
self->items = (rmt_item32_t *)m_realloc(self->items, num_items * sizeof(rmt_item32_t *));
self->num_items = num_items;
}
for (mp_uint_t item_index = 0; item_index < num_items; item_index++) {
self->items[item_index].val = mp_obj_get_int(item_list_ptr[item_index]);
}
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_rmt_store_pulses_obj, 2, 2, esp32_rmt_store_pulses); // RMT.issue_pulses(self, tx_ready_func:callable, item_index:int, item_count:int, clock_div:int) -> None
STATIC mp_obj_t esp32_rmt_issue_pulses(size_t n_args, const mp_obj_t *args) {
esp32_rmt_obj_t *self = MP_OBJ_TO_PTR(args[0]);
mp_obj_t *tx_ready_fn = MP_OBJ_TO_PTR(args[1]);
mp_uint_t item_index = mp_obj_get_int(args[2]);
mp_uint_t item_count = mp_obj_get_int(args[3]);
self->clock_div = mp_obj_get_int(args[4]);
check_esp_err(rmt_set_clk_div(self->channel_id, self->clock_div));
rmt_register_tx_end_callback(esp32_rmt_private_tx_end_callback, tx_ready_fn);
check_esp_err(rmt_write_items(self->channel_id, self->items + item_index, item_count, false));
return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(esp32_rmt_issue_pulses_obj, 5, 5, esp32_rmt_issue_pulses); and the following
with core functionality expressed in: def make_rotation_one(self) -> None: def make_rotation_one(self) -> None:
"accelerate then decelerate during single drive turn"
plan_one = self.planner.make_plan_rotate(+1, 0.5)
plan_two = self.planner.make_plan_rotate(-1, 0.5)
plan_full = plan_one + plan_two + [PLAN_CMD_STOP]
# print(f"{plan_full=}")
self.persist_motion_plan(plan_full)
self.transmit_reactor() # start transmit with isr self call and def transmit_reactor(self): def transmit_reactor(self):
"""
function to execute stepper micro-program:
* invoked by self "make rotation" methods for motion start
* invoked by esp32.RMT driver "transmit ready" ISR to continue motion
note: use isr rules https://docs.micropython.org/en/latest/reference/isr_rules.html
"""
buffer = self.ringer
header = buffer.ring_get() # micro-command prefix
if header == PLAN_CMD_RAMP:
# micro-command parameters
item_index = buffer.ring_get()
item_count = buffer.ring_get()
clock_div = buffer.ring_get()
# start pulse sequence form rmt driver store
self.rmt_driver.issue_pulses(# non blocking invocation
self.transmit_function, # this is isr setup to self
item_index, # place in the store
item_count, # pulse ramp block size
clock_div, # frequency for this block
)
self.event_cmd_ramp.set()
return
if header == PLAN_CMD_STOP:
self.event_cmd_stop.set()
return
self.event_cmd_trap.set() # should not happen |
This module has not been built in years, since the (removed) esp8266 port. Delete the code, as it is not likely to be useful in its current form. Closes: micropython#7015
See micropython#7015. This adds 2 new functions to ports/esp32/esp32_rmt.c: // RMT.store_pulses(self, item_list:list[int]) -> None // RMT.issue_pulses(self, tx_ready_func:callable, item_index:int, item_count:int, clock_div:int) -> None
The reference to |
Thinking about this, I think the real problem is that the RMT hardware needs the pulse sequence formatted in a specific way in memory (32-bit structs containing two pulses of 15-bit output period and 1-bit output value). The current implementation converts the Perhaps the solution to the memory problems is to expose this safely as an object that supports the array/buffer protocols. For instance as a new from esp32 import RMT, RMTPulseArray
pulses1 = RMTPulseArray([(100, 1), (50, 0), (50, 1), (100, 0)])
pulses2 = RMTPulseArray()
pulses2.append((100, 1))
pulses2.append((50, 0))
pulses2.append((50, 1))
pulses2.append((100, 0)) Not necessarily wedded to the tuple argument to Then you dispatch these arrays with: rmt = RMT(0, pin=Pin(2), clk_div=80)
rmt.write(pulses1)
rmt.write(pulses2) This way you can create as many of these pulse arrays as you need and retain them efficiently. To retain API compatibility, Add an @mattytrentini is the original author of [Also worth remembering that esp32 |
@peterhinch : RE:
|
@jonathanhogg : RE:
|
Which is why I suggested that the API is extended to allow pre-generation of the buffers ( What I'm proposing is a more flexible version of your This could support your use case of only issuing a subset of the generated pulse array by implementing efficient (shared memory) slicing of rmt.write(pulses1[2:]) Alternatively…A simpler API would be to just have I'm not a giant fan of passing unknown lumps of memory to the RMT hardware, but it would allow direct creation and modification of the buffers via other low-level mechanisms (like a custom C module or ulab maybe). |
@jonathanhogg : RE:
great idea, thank you. my vote goes to this approach. |
@Andrei-Pozolotin I was referring to the Python code in |
@dpgeorge Damien: can you please help us get un-confused about specifically, expected code sequence around rmt isr event processing is a follows:
rmt_register_tx_end_callback()micropython/ports/esp32/esp32_rmt.c Line 401 in 207e296
esp32_rmt_private_tx_end_callback()micropython/ports/esp32/esp32_rmt.c Line 367 in 207e296
Stepper.transmit_reactor()https://gist.github.com/Andrei-Pozolotin/c4b3dd041efe53cfb92cfb4de9c67267#file-stepper-py-L106
The code shown in snippets above seems to work fine,
Thank you. |
On esp32, "hard ISR" Python callbacks are difficult. If you can get away with a soft callback then I suggest using that. The differences are:
The issue on esp32 with hard callbacks is that you must make sure the thread local state is correct. It may need an interlock to coordinate with other (Python) threads. See eg https://github.com/micropython/micropython/blob/master/extmod/modbluetooth.c#L1261-L1294 |
@dpgeorge : thank you @jonathanhogg @peterhinch : what do you think?
|
My goto on these things is always to look at existing implementations. For instance,
Supporting both would be consistent and allow the caller to make a decision about whether they need the timing guarantee of a hard interrupt or the flexibility of a soft one. Defaulting the argument to It's not a difficult implementation switch, you either dispatch the callback directly or pass it to the MPy scheduler. |
@jonathanhogg : RE:
excellent approach, my vote goes to this, thank you. |
@jonathanhogg : RE:
We really appreciate your willingness for "a broad refactor". Before that bright future arrives, do you have bandwidth now to complete the review of PR 9685 and merge it? The motivation is to have more people to play with ISR approach now and to share their feedback, i.e.: |
@Andrei-Pozolotin: I'm afraid that I'm not a core developer so I am unable to merge. I don't have time for a full review, but taking a quick look at the code, my biggest problem with it is the lack of any bounds/error checking. What happens if I do an issue without having done a store? What happens if I pass an index or length that extends beyond the buffer? Generally, it feels unready as an idea and an implementation to push to mainline. |
@jonathanhogg : RE:
got it , thank you |
@peterhinch I did want to support the buffer protocol but, as @jonathanhogg has pointed out, the memory had to be carefully laid out and available as a contiguous array when handing it over to the RMT methods. I did consider pre-allocating an internal array and unpacking the buffer but it was effectively doubling the memory. It's also possible to use two channels; send one while you continue to unpack the next, then toggle...but it all became difficult and error prone - seemed best to provide a simple solution to start with. However, we're only really scratching the surface of what we can do with RMT - it's a very flexible component! - and maybe it is time to re-think how we present the API. I'll try and find time to re-read the ESP-IDF docs again and see if I can help contribute with some improvements. |
These stem from my use of RMT for its original purpose. My "blaster" libraries for 433Mhz and IR remote controls support Pyboard, RP2 and ESP32. In each case ESP32 RAM use was larger than on Pyboard and RP2. This could be fixed with two non-breaking changes.
I can see two alternatives for doing N times looping.
My first thought was a callback=None arg to .write_pulses() but this poses a recursion issue. A new .done_callback(cb=None) method would be better, with the default removing any current callback.
The text was updated successfully, but these errors were encountered: