
* RMT: Move some methods from (Tx|Rx)ChannelInternal to ChannelInternal Adds a new `ChannelInternal` trait implemented on `Channel`s, which bundles some methods that conceptually make sense for both rx and tx channels (whether the implementation is exactly the same is chip-specific). This avoids a small amount of code duplication. * RMT: Define input/output signals via a const array Allowing a default implementation of the getter functions in the ChannelInternal trait, and more importantly, paving the way for type-erased channels (where it will become necessary to map Channel number to signal at runtime). The array is indexed by ch_index, i.e. the index of the channel among channels of the same type (for devices with separate rx/tx channels). * RMT: move some `Sized` bounds from methods to trait To avoid repetition. There's no downside, since the trait is only implemented for a single type anyway, which is Sized. These traits are user-visible, but since they can't be implemented by users, and this only makes the bounds stricter, it should require not changes to user code. * RMT: Use composition with ConstChannelAccess, reduces macro usage instead of an extension trait implemened via the impl_*_channel macros. This reduces macro usage, making the code easier to reason about, and it also paves the way for type-erased channels by adding a second implementation of RawChannelAccess. This touches many lines, but is a fairly mechanical change that should be easier to review by ignoring whitespace changes. Previously, channel architecture was as follows: - `Channel` is parameterized by a const generic `CHANNEL: u8` number - low-level hardware operations are implemented via the *ChannelInternal traits directly on `Channel`. This is done via the `impl_*x_channel` macros to account for the different channel capabilities (rx/tx only or rx+tx) This PR changes this to: - `Channel` contains an `Raw: RawChannelAccess<Dir=Rx|Tx>` where `Rx` and `Tx` are ZSTs used as markers for a channel configured for a given direction. - low-level operations are implemented on the `Raw` type, depending on a bound on RawChannelAccess::Dir - the `Raw` types can only be constructed safely from the `ChannelCreator`, which ensures that only valid combinations of channel number and `Dir=Rx|Tx` can exist. - currently, the only implementation of `RawChannelAccess` is `ConstChannelAccess`, which has a `CHANNEL: u8` const generic parameter, just as `Channel` did before. Thus, the compiler should be able to inline and const-propagate code just as before. These new types are user-visible. Thus, if code directly names `Channel` types, it needs to be adapted. If it just uses a method chain such as `rmt.channelX.configure(...).transmit(...)`, no changes should be required. * RMT: rm (Rx|Tx)ChannelCreatorAsync, use mode generic on (Rx|Tx)ChannelCreator this de-duplicates some code, and may be useful to implement user code (e.g. setup functions) that is independent of DriverMode * RMT: Rewrite pending_interrupt_for_channel using indexed PAC accessors This deduplicates some code. I've also changed the return type (usize -> u8) for better consistency, since channel indices are generally typed as u8. * RMT: add DynChannelAccess as basis for type-erased channels Channels can now be `degrade`d to their type-erased variants. * RMT: Move around some code The channel definition used to be somewhere in the middle of channel implementation. There's no change to the code other than its location. * RMT: slightly more readable subsclicing * RMT: Move some chip-specific code to a cfg_if! switch There's no reason for these to reside in separate modules, and this restructuring meshes well with moving the Rmt definition to a macro as well, which will be done next. * RMT: Declare Rmt struct via macro to avoid repetition Reduces boilerplate at the cost of a somewhat complex macro. * RMT: Move RmtState Which was previously in the middle of channel implementation, but conceptually is more global to the module, thus more natural define earlier. * RMT: explicity mark a few private functions in submodule as pub(super) * RMT: use DynChannelAccess::conjure to simplify async_interrupt_handler If the compiler decides to unroll the loops, the resulting code should be essentially the same. Otherwise, it should be more compact. In any case, from a developer point of view, this is much more concise and removes one chip-specific case. * RMT: Use type-erased channels for some HIL tests * RMT: don't reset clock divider in start_tx This seems to fix flaky loopback tests where tx/rx pulse code length differs by 1. This matches IDF, which also doesn't reset channels on each transmit operation, but only once on channel creation, and when a sync_manager is used (which the Rust driver doesn't support, and which would also need to be handled differently anyway). * RMT: implement degrade() even if the channel is already type-erased This makes wrapping channels in custom structs slightly more convenient since it allows taking any channel and type-erasing it in that structs constructor. * RMT: rename (Rx|Tx)ChannelCreator methods to avoid trait disambiguation problems For devices with channels that support both Rx and Tx, Rust cannot disambiguate the trait at the call site (because it doesn't look at the argument types to do so). Renaming the methods avoids that. The alternative is to use fully-qualified names to call the trait methods (i.e. left-side turbofish), or to import the traits only in a limited scope. Both are much more verbose than the _rx/_tx suffixes to method names. * RMT: add basic async HIL test * RMT: remove overcomplicated WithMode trait from tests according to a suggestion by @bugadani --------- Co-authored-by: Scott Mabin <scott@mabez.dev>
Examples
This package contains a number of binary applications demonstrating the use of various hardware peripherals found within ESP32 the family of devices.
Each device has its own unique set of peripherals, and as such not every example will run on every device. We recommend building and flashing the examples using the xtask
method documented below, which will greatly simplify the process.
To check if a device is compatible with a given example, check the metadata comments above the imports, which will list all supported devices following the //% CHIPS:
designator. If this metadata is not present, then the example will work on any device supported by esp-hal
.
As previously stated, we use the cargo-xtask pattern for automation. Commands invoking this tool must be run from the root of the repository.
Building Examples
You can build all examples for a given device using the build examples
subcommand:
cargo xtask build examples esp-hal esp32
Note that we must specify which package to build the examples for, since this repository contains multiple packages (specifying esp-hal will build the examples in the examples package instead).
Running Examples
You can also build and then subsequently flash and run an example using the run example
subcommand. With a target device connected to your host system, run:
cargo xtask run example esp-hal esp32c6 --example embassy_hello_world
Again, note that we must specify which package to build the example from, plus which example to build and flash to the target device.
Adding Examples
If you are contributing to esp-hal
and would like to add an example, the process is generally the same as any other project.
One major difference in our case is the metadata comments which state the compatible devices and required features for an example. Both of these designators are optional; if //% CHIPS:
is omitted then all devices considered to be supported, and if //% FEATURES:
is omitted then no features are enabled at build time.
To demonstrated, in src/bin/embassy_hello_world.rs
you will see the following:
//% CHIPS: esp32 esp32c2 esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: embassy esp-hal-embassy/integrated-timers
Another thing to be aware of is the GPIO pins being used. We have tried to use pins available the DevKit-C boards from Espressif, however this is being done on a best-effort basis.
In general, the following GPIO are recommended for use, though be conscious of whether certain pins are used for UART, strapping pins, etc. on some devices:
- GPIO0
- GPIO1
- GPIO2
- GPIO3
- GPIO4
- GPIO5
- GPIO8
- GPIO9
- GPIO10