Add HIL testing (#1297)

* Create the `hil-test` package

* Add a simple integration test to verify basic GPIO functionality

* WIP

* feat: Update with esp-hal unification

* build: Update dependencies

* feat: Add a simple CI workflow test

* ci: Avoid using a gh-hosted-runner to build

* ci: Remove building bins in gh-hosted-runner

* ci: Remove HIL Gpio CI test

* ci: Test all the available tests

* test: Add spi_full_duplex test

* docs: Add documentation

* test: Add uart test

* style: Remove unused imports

* docs: Update wiring, document H2 VM

* ci: Enable H2 tests

* ci: Add rust-cache action

* docs: Document H2 vm

* test: Add timeout

* ci: Enable ESP32-C3 tests

* feat: Add timeouts

* feat: Add aes test

* ci: Avoid running CI workflow when we change hil-test stuff

* test: Remove warnings

* feat: Address feedback

* feat: Update features names and spi methods

* ci: Remove rust-cache action

* Update HIL to probe-rs#2292 (#1307)

* feat: Update probe-rs/embedded-test to probe-rs#2292

* feat: Remove lib

* ci: Use a matrix

* ci: Enable ESP32C3

* feat: Add a way to cfg away test for unsuported peripherals

* ci: Update trigger conditions

* feat: Update pins to make it work on s3

* feat: Changes enabling S3

* feat: Remove log feature

* feat: Adapt for rebase

* feat: Remove env

* feat: enable S3

* chore: Remove todo

* build: Pin dependencies

* feat: Add target alias

* docs: Update readme

* fix: Fix traits imports after rebase. Use debug

* build: Remove lto

* feat: Build tests on release mode

---------

Co-authored-by: Jesse Braham <jesse@beta7.io>
This commit is contained in:
Sergio Gasquez Arcos 2024-03-21 15:28:27 +01:00 committed by GitHub
parent 1444b62777
commit baea915935
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 638 additions and 0 deletions

View File

@ -18,12 +18,14 @@ on:
paths-ignore: paths-ignore:
- "**/CHANGELOG.md" - "**/CHANGELOG.md"
- "**/README.md" - "**/README.md"
- "**/hil-test/**"
push: push:
branches-ignore: branches-ignore:
- "gh-readonly-queue/**" - "gh-readonly-queue/**"
paths-ignore: paths-ignore:
- "**/CHANGELOG.md" - "**/CHANGELOG.md"
- "**/README.md" - "**/README.md"
- "**/hil-test/**"
merge_group: merge_group:
workflow_dispatch: workflow_dispatch:

59
.github/workflows/hil.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: HIL
on:
merge_group:
workflow_dispatch:
inputs:
repository:
description: "Owner and repository to test"
required: true
default: 'esp-rs/esp-hal'
branch:
description: "Branch, tag or SHA to checkout."
required: true
default: "main"
env:
CARGO_TERM_COLOR: always
jobs:
# Test RISC-V targets:
riscv-hil:
name: HIL Test | ${{ matrix.target.soc }}
runs-on:
labels: [self-hosted, "${{ matrix.target.runner }}"]
strategy:
fail-fast: false
matrix:
target:
- soc: esp32c3
runner: rustboard
rust-target: riscv32imc-unknown-none-elf
- soc: esp32c6
runner: esp32c6-usb
rust-target: riscv32imac-unknown-none-elf
- soc: esp32h2
runner: esp32h2-usb
rust-target: riscv32imac-unknown-none-elf
steps:
- uses: actions/checkout@v4
if: github.event_name != 'workflow_dispatch'
- uses: actions/checkout@v4
if: github.event_name == 'workflow_dispatch'
with:
repository: ${{ github.event.inputs.repository }}
ref: ${{ github.event.inputs.branch }}
- uses: dtolnay/rust-toolchain@v1
with:
target: ${{ matrix.target.rust-target }}
toolchain: nightly
components: rust-src
- name: Run tests
working-directory: hil-test
run: cargo ${{ matrix.target.soc }}
# Test Xtensa targets:
# TODO: Add jobs for Xtensa once supported by `probe-rs`

View File

@ -9,4 +9,5 @@ exclude = [
"esp-metadata", "esp-metadata",
"esp-riscv-rt", "esp-riscv-rt",
"examples", "examples",
"hil-test",
] ]

View File

@ -0,0 +1,29 @@
[alias]
# esp32 = "test --release --features=esp32 --target=xtensa-esp32-none-elf -- --chip esp32-3.3v"
# esp32c2 = "test --release --features=esp32c2 --target=riscv32imc-unknown-none-elf -- --chip esp32c2"
esp32c3 = "test --release --features=esp32c3 --target=riscv32imc-unknown-none-elf -- --chip esp32c3"
esp32c6 = "test --release --features=esp32c6 --target=riscv32imac-unknown-none-elf -- --chip esp32c6"
esp32h2 = "test --release --features=esp32h2 --target=riscv32imac-unknown-none-elf -- --chip esp32h2"
# esp32p4 = "test --release --features=esp32p4 --target=riscv32imafc-unknown-none-elf -- --chip esp32p4"
# esp32s2 = "test --release --features=esp32s2 --target=xtensa-esp32s2-none-elf -- --chip esp32s2"
esp32s3 = "test --release --features=esp32s3 --target=xtensa-esp32s3-none-elf -- --chip esp32s3"
[target.'cfg(target_arch = "riscv32")']
runner = "probe-rs run"
rustflags = [
"-C", "link-arg=-Tlinkall.x",
"-C", "link-arg=-Tembedded-test.x",
"-C", "link-arg=-Tdefmt.x",
]
[target.'cfg(target_arch = "xtensa")']
runner = "probe-rs run"
rustflags = [
"-C", "link-arg=-nostartfiles",
"-C", "link-arg=-Wl,-Tlinkall.x",
"-C", "link-arg=-Tdefmt.x",
"-C", "link-arg=-Tembedded-test.x",
]
[unstable]
build-std = ["core"]

93
hil-test/Cargo.toml Normal file
View File

@ -0,0 +1,93 @@
[package]
name = "hil-test"
version = "0.0.0"
edition = "2021"
publish = false
[[test]]
name = "aes"
harness = false
[[test]]
name = "gpio"
harness = false
[[test]]
name = "spi_full_duplex"
harness = false
[[test]]
name = "uart"
harness = false
[dependencies]
defmt = { version = "0.3.5" }
defmt-rtt = { version = "0.4.0" }
esp-hal = { path = "../esp-hal", features = ["embedded-hal", "embedded-hal-02", "defmt"], optional = true }
embedded-hal-02 = { version = "0.2.7", package = "embedded-hal", features = ["unproven"] }
embedded-hal-async = { version = "1.0.0", optional = true }
embedded-hal = { version = "1.0.0" }
embedded-hal-nb = { version = "1.0.0", optional = true }
embassy-executor = { default-features = false, version = "0.5.0", features = ["executor-thread", "arch-riscv32"], optional = true }
semihosting = "0.1.6"
[dev-dependencies]
# Add the `embedded-test/defmt` feature for more verbose testing
embedded-test = {git = "https://github.com/probe-rs/embedded-test", rev = "8e3f925"}
[features]
# Device support (required!):
esp32 = ["esp-hal/esp32"]
esp32c2 = ["esp-hal/esp32c2"]
esp32c3 = ["esp-hal/esp32c3"]
esp32c6 = ["esp-hal/esp32c6"]
esp32h2 = ["esp-hal/esp32h2"]
esp32s2 = ["esp-hal/esp32s2"]
esp32s3 = ["esp-hal/esp32s3"]
# Async & Embassy:
async = ["dep:embedded-hal-async", "esp-hal?/async"]
embassy = ["esp-hal?/embassy", "embedded-test/embassy", "dep:embassy-executor"]
embassy-executor-interrupt = ["esp-hal?/embassy-executor-interrupt"]
embassy-executor-thread = ["esp-hal?/embassy-executor-thread"]
embassy-time-systick-16mhz = ["esp-hal?/embassy-time-systick-16mhz"]
embassy-time-systick-80mhz = ["esp-hal?/embassy-time-systick-80mhz"]
embassy-time-timg0 = ["esp-hal?/embassy-time-timg0"]
# cargo build/run
[profile.dev]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 'z' # <-
overflow-checks = true # <-
# cargo test
[profile.test]
codegen-units = 1
debug = 2
debug-assertions = true # <-
incremental = false
opt-level = 3 # <-
overflow-checks = true # <-
# cargo build/run --release
[profile.release]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
opt-level = 3 # <-
overflow-checks = false # <-
# cargo test --release
[profile.bench]
codegen-units = 1
debug = 2
debug-assertions = false # <-
incremental = false
opt-level = 3 # <-
overflow-checks = false # <-
[patch.crates-io]
semihosting = { git = "https://github.com/taiki-e/semihosting", rev = "c829c19" }

95
hil-test/README.md Normal file
View File

@ -0,0 +1,95 @@
# hil-test
Hardware-in-loop testing for `esp-hal`.
For assistance with this package please [open an issue] or [start a discussion].
[open an issue]: https://github.com/esp-rs/esp-hal/issues/new
[start a discussion]: https://github.com/esp-rs/esp-hal/discussions/new/choose
## Quickstart
We use [embedded-test] as our testing framework, which relies on [defmt] internally. This allows us to write unit and integration tests much in the same way you would for a normal Rust project, when the standard library is available, and to execute them using Cargo's built-in test runner.
[embedded-test]: https://github.com/probe-rs/embedded-test
[defmt]: https://github.com/knurling-rs/defmt
### Running Tests Locally
We use [probe-rs] for flashing and running the tests on a target device, however, this **MUST** be installed from the correct revision, and with the correct features enabled:
```text
cargo install probe-rs \
--git=https://github.com/probe-rs/probe-rs \
--rev=b431b24 \
--features=cli \
--bin=probe-rs
```
Target device **MUST** connected via its USB-Serial-JTAG port, or if unavailable (eg. ESP32, ESP32-C2, ESP32-S2) then you must connect a compatible debug probe such as an [ESP-Prog].
You can run all test for a given device using:
```shell
cargo +nightly esp32c6
# or
cargo +esp esp32s3
```
For running a single test on a target:
```shell
# Run GPIO tests for ESP32-C6
CARGO_BUILD_TARGET=riscv32imac-unknown-none-elf \
PROBE_RS_CHIP=esp32c6 \
cargo +nightly test --features=esp32c6 --test=gpio
```
- If the `--test` argument is omitted, then all tests will be run.
- The build target **MUST** be specified via the `CARGO_BUILD_TARGET` environment variable or as an argument (`--target`).
- The chip **MUST** be specified via the `PROBE_RS_CHIP` environment variable or as an argument of `probe-rs` (`--chip`).
Some tests will require physical connections, please see the current [configuration in our runners](#running-tests-remotes-ie-on-self-hosted-runners).
### Running Tests Remotes (ie. On Self-Hosted Runners)
The [`hil.yml`] workflow builds the test suite for all our available targets and executes them.
Our Virtual Machines have the following setup:
- ESP32-C3 (`rustboard`):
- Devkit: `ESP32-C3-DevKit-RUST-1` connected via USB-Serial-JTAG.
- `GPIO2` and `GPIO4` are connected.
- VM: Configured with the following [setup](#vm-setup)
- ESP32-C6 (`esp32c6-usb`):
- Devkit: `ESP32-C6-DevKitC-1 V1.2` connected via USB-Serial-JTAG (`USB` port).
- `GPIO2` and `GPIO4` are connected.
- VM: Configured with the following [setup](#vm-setup)
- ESP32-H2 (`esp32h2-usb`):
- Devkit: `ESP32-H2-DevKitM-1` connected via USB-Serial-JTAG (`USB` port).
- `GPIO2` and `GPIO4` are connected.
- VM: Configured with the following [setup](#vm-setup)
[`hil.yml`]: https://github.com/esp-rs/esp-hal/blob/main/.github/workflows/hil.yml
#### VM Setup
```bash
# Install Rust:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- --default-toolchain stable -y --profile minimal
# Source the current shell:
source "$HOME/.cargo/env"
# Install dependencies
sudo apt install -y pkg-config libudev-dev
# Install probe-rs
cargo install probe-rs --git=https://github.com/probe-rs/probe-rs --rev=b431b24 --features=cli --bin=probe-rs --locked --force
# Add the udev rules
wget -O - https://probe.rs/files/69-probe-rs.rules | sudo tee /etc/udev/rules.d/69-probe-rs.rules > /dev/null
# Add the user to plugdev group
sudo usermod -a -G plugdev $USER
# Reboot the VM
```
## Adding New Tests
1. Create a new integration test file (`tests/$PERIPHERAL.rs`)
2. Add a corresponding `[[test]]` entry to `Cargol.toml` (**MUST** set `harness = false`)
3. Write the tests
4. Document any necessary physical connections on boards connected to self-hosted runners
5. Write some documentation at the top of the `tests/$PERIPHERAL.rs` file with the pins being used and the required connections, if applicable.

99
hil-test/tests/aes.rs Normal file
View File

@ -0,0 +1,99 @@
//! AES Test
#![no_std]
#![no_main]
use defmt_rtt as _;
use esp_hal::{
aes::{Aes, Mode},
peripherals::Peripherals,
};
struct Context<'a> {
aes: Aes<'a>,
}
impl Context<'_> {
pub fn init() -> Self {
let peripherals = Peripherals::take();
let aes = Aes::new(peripherals.AES);
Context { aes }
}
}
#[cfg(not(any(
feature = "esp32c3",
feature = "esp32c6",
feature = "esp32h2",
feature = "esp32s3"
)))]
mod not_test {
#[esp_hal::entry]
fn main() -> ! {
semihosting::process::exit(0)
}
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
}
#[cfg(test)]
#[cfg(any(
feature = "esp32c3",
feature = "esp32c6",
feature = "esp32h2",
feature = "esp32s3"
))]
#[embedded_test::tests]
mod tests {
use defmt::assert_eq;
use super::*;
#[init]
fn init() -> Context<'static> {
Context::init()
}
#[test]
fn test_aes_encryption(mut ctx: Context<'static>) {
let keytext = "SUp4SeCp@sSw0rd".as_bytes();
let plaintext = "message".as_bytes();
let encrypted_message = [
0xb3, 0xc8, 0xd2, 0x3b, 0xa7, 0x36, 0x5f, 0x18, 0x61, 0x70, 0x0, 0x3e, 0xd9, 0x3a,
0x31, 0x96,
];
// create an array with aes128 key size
let mut keybuf = [0_u8; 16];
keybuf[..keytext.len()].copy_from_slice(keytext);
// create an array with aes block size
let mut block_buf = [0_u8; 16];
block_buf[..plaintext.len()].copy_from_slice(plaintext);
let mut block = block_buf.clone();
ctx.aes.process(&mut block, Mode::Encryption128, &keybuf);
assert_eq!(block, encrypted_message);
}
#[test]
fn test_aes_decryption(mut ctx: Context<'static>) {
let keytext = "SUp4SeCp@sSw0rd".as_bytes();
let plaintext = "message".as_bytes();
let mut encrypted_message = [
0xb3, 0xc8, 0xd2, 0x3b, 0xa7, 0x36, 0x5f, 0x18, 0x61, 0x70, 0x0, 0x3e, 0xd9, 0x3a,
0x31, 0x96,
];
// create an array with aes128 key size
let mut keybuf = [0_u8; 16];
keybuf[..keytext.len()].copy_from_slice(keytext);
ctx.aes
.process(&mut encrypted_message, Mode::Decryption128, &keybuf);
assert_eq!(&encrypted_message[..plaintext.len()], plaintext);
}
}

72
hil-test/tests/gpio.rs Normal file
View File

@ -0,0 +1,72 @@
//! GPIO Test
//!
//! Folowing pins are used:
//! GPIO2
//! GPIO4
#![no_std]
#![no_main]
use defmt_rtt as _;
use embedded_hal::digital::{InputPin as _, OutputPin as _, StatefulOutputPin as _};
use esp_hal::{
gpio::{GpioPin, Input, Output, PullDown, PushPull, IO},
peripherals::Peripherals,
};
struct Context {
io2: GpioPin<Input<PullDown>, 2>,
io4: GpioPin<Output<PushPull>, 4>,
}
impl Context {
pub fn init() -> Self {
let peripherals = Peripherals::take();
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
Context {
io2: io.pins.gpio2.into_pull_down_input(),
io4: io.pins.gpio4.into_push_pull_output(),
}
}
}
#[cfg(test)]
#[embedded_test::tests]
mod tests {
use defmt::assert_eq;
use super::*;
#[init]
fn init() -> Context {
Context::init()
}
#[test]
fn test_gpio_input(mut ctx: Context) {
// `InputPin`:
assert_eq!(ctx.io2.is_low(), Ok(true));
assert_eq!(ctx.io2.is_high(), Ok(false));
}
#[test]
fn test_gpio_output(mut ctx: Context) {
// `StatefulOutputPin`:
assert_eq!(ctx.io4.is_set_low(), Ok(true));
assert_eq!(ctx.io4.is_set_high(), Ok(false));
assert!(ctx.io4.set_high().is_ok());
assert_eq!(ctx.io4.is_set_low(), Ok(false));
assert_eq!(ctx.io4.is_set_high(), Ok(true));
// `ToggleableOutputPin`:
assert!(ctx.io4.toggle().is_ok());
assert_eq!(ctx.io4.is_set_low(), Ok(true));
assert_eq!(ctx.io4.is_set_high(), Ok(false));
assert!(ctx.io4.toggle().is_ok());
assert_eq!(ctx.io4.is_set_low(), Ok(false));
assert_eq!(ctx.io4.is_set_high(), Ok(true));
// Leave in initial state for next test
assert!(ctx.io4.toggle().is_ok());
}
}

View File

@ -0,0 +1,115 @@
//! SPI Full Duplex Test
//!
//! Folowing pins are used:
//! SCLK GPIO0
//! MISO GPIO2
//! MOSI GPIO4
//! CS GPIO5
//!
//! Connect MISO (GPIO2) and MOSI (GPIO4) pins.
#![no_std]
#![no_main]
use defmt_rtt as _;
use embedded_hal::spi::SpiBus;
use esp_hal::{
clock::ClockControl,
gpio::IO,
peripherals::Peripherals,
prelude::*,
spi::{master::Spi, FullDuplexMode, SpiMode},
};
struct Context {
spi: Spi<'static, esp_hal::peripherals::SPI2, FullDuplexMode>,
}
impl Context {
pub fn init() -> Self {
let peripherals = Peripherals::take();
let system = peripherals.SYSTEM.split();
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let sclk = io.pins.gpio0;
let miso = io.pins.gpio2;
let mosi = io.pins.gpio4;
let cs = io.pins.gpio5;
let spi = Spi::new(peripherals.SPI2, 1000u32.kHz(), SpiMode::Mode0, &clocks).with_pins(
Some(sclk),
Some(mosi),
Some(miso),
Some(cs),
);
Context { spi }
}
}
#[cfg(test)]
#[embedded_test::tests]
mod tests {
use defmt::assert_eq;
use super::*;
#[init]
fn init() -> Context {
Context::init()
}
#[test]
fn test_symestric_transfer(mut ctx: Context) {
let write = [0xde, 0xad, 0xbe, 0xef];
let mut read: [u8; 4] = [0x00u8; 4];
ctx.spi
.transfer(&mut read[..], &write[..])
.expect("Symmetric transfer failed");
assert_eq!(write, read);
}
#[test]
fn test_asymestric_transfer(mut ctx: Context) {
let write = [0xde, 0xad, 0xbe, 0xef];
let mut read: [u8; 4] = [0x00; 4];
ctx.spi
.transfer(&mut read[0..2], &write[..])
.expect("Asymmetric transfer failed");
assert_eq!(write[0], read[0]);
assert_eq!(read[2], 0x00u8);
}
#[test]
fn test_symestric_transfer_huge_buffer(mut ctx: Context) {
let mut write = [0x55u8; 4096];
for byte in 0..write.len() {
write[byte] = byte as u8;
}
let mut read = [0x00u8; 4096];
ctx.spi
.transfer(&mut read[..], &write[..])
.expect("Huge transfer failed");
assert_eq!(write, read);
}
#[test]
#[timeout(3)]
fn test_symestric_transfer_huge_buffer_no_alloc(mut ctx: Context) {
let mut write = [0x55u8; 4096];
for byte in 0..write.len() {
write[byte] = byte as u8;
}
ctx.spi
.transfer_in_place(&mut write[..])
.expect("Huge transfer failed");
for byte in 0..write.len() {
assert_eq!(write[byte], byte as u8);
}
}
}

73
hil-test/tests/uart.rs Normal file
View File

@ -0,0 +1,73 @@
//! UART Test
//!
//! Folowing pins are used:
//! TX GPIP2
//! RX GPIO4
//!
//! Connect TX (GPIO2) and RX (GPIO4) pins.
#![no_std]
#![no_main]
use defmt_rtt as _;
use embedded_hal_02::serial::{Read, Write};
use esp_hal::{
clock::ClockControl,
gpio::IO,
peripherals::{Peripherals, UART0},
prelude::*,
uart::{
config::{Config, DataBits, Parity, StopBits},
TxRxPins,
Uart,
},
};
use nb::block;
struct Context {
uart: Uart<'static, UART0>,
}
impl Context {
pub fn init() -> Self {
let peripherals = Peripherals::take();
let system = peripherals.SYSTEM.split();
let clocks = ClockControl::boot_defaults(system.clock_control).freeze();
let io = IO::new(peripherals.GPIO, peripherals.IO_MUX);
let pins = TxRxPins::new_tx_rx(
io.pins.gpio2.into_push_pull_output(),
io.pins.gpio4.into_floating_input(),
);
let config = Config {
baudrate: 115200,
data_bits: DataBits::DataBits8,
parity: Parity::ParityNone,
stop_bits: StopBits::STOP1,
};
let uart = Uart::new_with_config(peripherals.UART0, config, Some(pins), &clocks);
Context { uart }
}
}
#[cfg(test)]
#[embedded_test::tests]
mod tests {
use defmt::assert_eq;
use super::*;
#[init]
fn init() -> Context {
Context::init()
}
#[test]
#[timeout(3)]
fn test_send_receive(mut ctx: Context) {
ctx.uart.write(0x42).ok();
let read = block!(ctx.uart.read());
assert_eq!(read, Ok(0x42));
}
}