Skip to content
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

Add coproc module #6902

Merged
merged 1 commit into from
Oct 20, 2022
Merged

Add coproc module #6902

merged 1 commit into from
Oct 20, 2022

Conversation

microdev1
Copy link
Collaborator

@microdev1 microdev1 commented Sep 14, 2022

The coproc module adds ability to load, run and halt pre-compiled programs on a co-processor or another cpu core.
Currently, this is implemented on esp32s2 and esp32s3 utilizing the built-in risc-v co-processor.

@jepler
Copy link
Member

jepler commented Sep 14, 2022

Thanks, this looks neat! I didn't do any detailed review, but I do have some questions after a quick glance.

How, if at all, does this control what pin(s) the coprocessor can access (read/write)? With rp2pio, which is conceptually in a similar vein, scott took care to make sure it could not interact badly with other pin uses like pwm or digitalio. However, it's also pretty easy to tell via introspection what it is possible for the loaded code to do. Saying that this is harder with risc-v code is an understatement to be sure.

Is there example code (to load on the coprocessor) that could be used for testing? Is there an explanation of how to produce new code (assemble it)?

Is there any sign this way of thinking would be useful on other microcontroller families? For instance, is there any way it could be used to access the 2nd CPU core on an rp2040?

Is there any facility for the coprocessor and python to communicate after the coprocessor task has been launched, in either direction?

Is there a reason there isn't anything along the lines of .stop(), .deinit(), or the functions to support usage in a with statement?

@microdev1
Copy link
Collaborator Author

microdev1 commented Sep 15, 2022

How, if at all, does this control what pin(s) the coprocessor can access (read/write)?

The idea with this module is to provide pre-compiled examples on top of which a user can build there code.
For instance, a pin-control example code running on the co-processor can have the ability to keep track of the available/busy status of pin(s) with CircuitPython. On the S2 this is achieved by defining global variables to which both main cpu and co-processor have read/write access.

Is there an explanation of how to produce new code (assemble it)?

The esp-idf provides toolchain for compiling risc-v co-processor code which is written in C or assembly.
Here is the esp-idf documentation for ulp co-processor programming.

Is there example code (to load on the coprocessor) that could be used for testing?

I tested this with a simple blink example which is attached below. More examples are available in the esp-idf documentation.

Is there any sign this way of thinking would be useful on other microcontroller families? For instance, is there any way it could be used to access the 2nd CPU core on an rp2040?

I do expect to have this ability expand to other microcontroller families. I haven't looked into how this could be done on the rp2040 but have had discussions with discord community members in the past who requested this and say that this is possible.

Is there any facility for the coprocessor and python to communicate after the coprocessor task has been launched, in either direction?

For this, we can have api to access a shared global variable in case of the S2 for communication. I plan to add this in the future.
Another thing that I plan to add is CoprocAlarm, this will allow CircuitPython to go into sleep on the main cpu while the low-power co-processor is alive and also has the ability to wake-up the main cpu.

Is there a reason there isn't anything along the lines of .stop(), .deinit(), or the functions to support usage in a with statement?

I just haven't looked down this path for the api. Any suggestions are welcome.

🚨 Simple Blink Example:

# CircuitPython
import coproc
with open("ulp_blink.bin", "rb") as f:
    coproc.load(f.read())
coproc.run()
while True:
    pass
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "ulp_riscv/ulp_riscv.h"
#include "ulp_riscv/ulp_riscv_utils.h"
#include "ulp_riscv/ulp_riscv_gpio.h"

int main (void) {
    bool gpio_level = true;

    ulp_riscv_gpio_init(GPIO_NUM_21);
    ulp_riscv_gpio_output_enable(GPIO_NUM_21);

    while(1) {
        ulp_riscv_gpio_output_level(GPIO_NUM_21, gpio_level);
        ulp_riscv_delay_cycles(1000*1000 * ULP_RISCV_CYCLES_PER_US);
        gpio_level = !gpio_level;
    }

    return 0;
}

Here is the compiled version of the above ulp code. The zip contains ulp_blink.bin.

Node: The python side has an infinite loop because when the execution ends all pins are reset due to which the blink program is no longer able to control the pin unless it initializes it again. I believe this is similar to your concern regarding pin access.

@microdev1 microdev1 requested review from dhalbert and jepler October 19, 2022 18:14
Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just one thing about the args.

I am fine with including this and describing it as "experimental" and subject to change. @jepler do you have a comment?

shared-bindings/coproc/Coproc.c Outdated Show resolved Hide resolved
shared-bindings/coproc/CoprocMemory.c Show resolved Hide resolved
@microdev1
Copy link
Collaborator Author

I have made changes to the API and have added CoprocMemory which provides access to the shared memory region.

In the following example:

  • 0x500007fc is the address of shared_mem variable.
  • The value of coproc.memory(program)[0] decides the LED blink interval.
  • Here is the pre-compiled binary of the ulp program.

🚨 Blink Example:

# CircuitPython

import coproc

shared_mem = coproc.CoprocMemory(address=0x500007fc, length=1024)

with open("program.bin", "rb") as f:
    program = coproc.Coproc(buffer=f.read(), memory=shared_mem)

coproc.run(program)
print(coproc.memory(program)[0])
# coproc.halt(program)

while True:
    pass
// ULP-RISCV

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "ulp_riscv/ulp_riscv.h"
#include "ulp_riscv/ulp_riscv_utils.h"
#include "ulp_riscv/ulp_riscv_gpio.h"

// global variables will be exported as public symbols, visible from main CPU
uint8_t shared_mem[1024];
uint16_t shared_mem_len = 1024;

int main (void) {
    shared_mem[0] = 10;
    shared_mem_len = 1024;

    bool gpio_level = true;

    ulp_riscv_gpio_init(GPIO_NUM_21);
    ulp_riscv_gpio_output_enable(GPIO_NUM_21);

    while(1) {
        ulp_riscv_gpio_output_level(GPIO_NUM_21, gpio_level);
        ulp_riscv_delay_cycles(shared_mem[0] * 10 * ULP_RISCV_CYCLES_PER_MS);
        gpio_level = !gpio_level;
    }

    // ulp_riscv_shutdown() is called automatically when main exits
    return 0;
}

shared-bindings/coproc/Coproc.c Outdated Show resolved Hide resolved
@microdev1 microdev1 requested a review from dhalbert October 20, 2022 03:40
Copy link
Collaborator

@dhalbert dhalbert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 can merge after builds

@microdev1 microdev1 merged commit b5b6498 into adafruit:main Oct 20, 2022
@microdev1 microdev1 deleted the coproc branch October 20, 2022 07:22
@s-ol
Copy link

s-ol commented Oct 21, 2022

Very interesting addition. I'm trying to decide whether I'd find this useful for RP2040 or not:
In the ESP32 case it's very neat because there is no other way to bring in native code with a different architecture for the coprocessor, at least without a custom CircuitPython build.

For the RP2040 though, it should already be possible to have native code run on the second core using native modules (.mpy); these are not explicitly documented in CircuitPython but the upstream docs contain examples.

The native module approach has the added benefit that the module can expose a full python API for a very comfortable integration experience on the Python side, as well as being fully in control about starting, stopping, and communication with the other core (using the platform APIs, e.g. pico_multicore_fifo, without being limited by some architecture-independent abstractions.

OTOH a benefit of this approach could be that it's not necessary to set up a full CircuitPython build environment, it's enough to have a third-party toolchain for the target architecture that can dump the binary machine code.

EDIT: another benefit of this approach is that it can explicitly track the usage of the external core(s) and can e.g. raise an exception if the core is already running when a Coproc instance is created, whereas multiple mpy modules have no way to know if someone else is trying to access the hardware.

@jepler
Copy link
Member

jepler commented Oct 22, 2022

In the following example:

0x500007fc is the address of shared_mem variable.

In a CircuitPython program, how do I determine a permissible address for a shared memory variable? What happens if I specify an address that is in use for some other purpose, such as one used by the CircuitPython or ESP-IDF heaps? Is this function safe to call with 'bad' inputs -- do they lead to exceptions, or do they continue and corrupt memory in use for another purpose?

@microdev1
Copy link
Collaborator Author

microdev1 commented Oct 22, 2022

how do I determine a permissible address for a shared memory variable?

That can be grabbed from the build files ld, map or sym.

What happens if I specify an address that is in use for some other...and corrupt memory in use for another purpose?

⚠️ This is direct access! We can do some sanitization like in the case ULP:

RTC_SLOW_MEM_START = 0x50000000
RTC_SLOW_MEM_END = RTC_SLOW_MEM_START + 8176
lower_limit = RTC_SLOW_MEM_START + loaded_program_len
upper_limit = RTC_SLOW_MEM_END - COPROC_MEMORY_LEN

@tannewt
Copy link
Member

tannewt commented Nov 16, 2022

Hi! I think this is really neat but needs a bit more polish. Where should I put my feedback? Here or a new issue?

@microdev1
Copy link
Collaborator Author

microdev1 commented Nov 16, 2022

Where should I put my feedback? Here or a new issue?

An issue would be fine.

@tannewt tannewt mentioned this pull request Nov 16, 2022
4 tasks
@tannewt
Copy link
Member

tannewt commented Nov 16, 2022

Where should I put my feedback? Here or a new issue?

An issue would be fine.

Ok, I've opened #7218

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants