Add GPIOI loopback interrupt test for AST1060
diff --git a/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/BUILD.bazel b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/BUILD.bazel new file mode 100644 index 0000000..23a6f6b --- /dev/null +++ b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/BUILD.bazel
@@ -0,0 +1,71 @@ +# Licensed under the Apache-2.0 license +# SPDX-License-Identifier: Apache-2.0 + +load("@pigweed//pw_kernel/tooling:system_image.bzl", "system_image") +load("@pigweed//pw_kernel/tooling:target_codegen.bzl", "target_codegen") +load("@pigweed//pw_kernel/tooling:target_linker_script.bzl", "target_linker_script") +load("@rules_rust//rust:defs.bzl", "rust_binary") +load("//target/ast10x0:defs.bzl", "TARGET_COMPATIBLE_WITH", "system_image_test") + +filegroup( + name = "system_config", + srcs = ["system.json5"], +) + +target_codegen( + name = "codegen", + arch = "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + system_config = ":system_config", + target_compatible_with = TARGET_COMPATIBLE_WITH, +) + +target_linker_script( + name = "linker_script", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + template = "//target/ast10x0:linker_script_template", +) + +rust_binary( + name = "target", + srcs = ["target.rs"], + edition = "2024", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + deps = [ + ":codegen", + ":linker_script", + "//target/ast10x0:entry", + "//target/ast10x0/peripherals", + "@pigweed//pw_kernel/arch/arm_cortex_m:arch_arm_cortex_m", + "@pigweed//pw_kernel/kernel", + "@pigweed//pw_kernel/subsys/console:console_backend", + "@pigweed//pw_kernel/target:target_common", + "@pigweed//pw_log/rust:pw_log", + "@rust_crates//:cortex-m", + "@rust_crates//:embedded-hal", + ], +) + +system_image( + name = "gpioi_loopback", + kernel = ":target", + platform = "//target/ast10x0", + system_config = ":system_config", + tags = ["kernel"], + target_compatible_with = TARGET_COMPATIBLE_WITH, + userspace = False, + visibility = ["//visibility:public"], +) + +system_image_test( + name = "gpioi_loopback_test", + image = ":gpioi_loopback", + tags = ["hardware"], + target_compatible_with = select({ + "//target/ast10x0:qemu_enabled": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], +)
diff --git a/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/system.json5 b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/system.json5 new file mode 100644 index 0000000..8048499 --- /dev/null +++ b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/system.json5
@@ -0,0 +1,17 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +// AST10x0 kernel-only GPIOI output and loopback test configuration. +{ + arch: { + type: "armv7m", + vector_table_start_address: 0x00000000, + vector_table_size_bytes: 1280, + }, + kernel: { + flash_start_address: 0x00000500, + flash_size_bytes: 262144, + ram_start_address: 0x00040500, + ram_size_bytes: 391936, // ends at RAM_NC boundary (0x000A0000) + }, +}
diff --git a/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/target.rs b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/target.rs new file mode 100644 index 0000000..2cb5b76 --- /dev/null +++ b/target/ast10x0/tests/peripherals/gpio/gpioi_loopback/target.rs
@@ -0,0 +1,196 @@ +// Licensed under the Apache-2.0 license +// SPDX-License-Identifier: Apache-2.0 + +//! AST10x0 GPIOI loopback test target. + +#![no_std] +#![no_main] + +use ast10x0_peripherals::gpio::{gpioi, ActiveLow, GpioExt, InterruptMode}; +use ast10x0_peripherals::scu::{pinctrl, ScuRegisters}; +use console_backend::console_backend_write_all; +use embedded_hal::digital::{InputPin, OutputPin, StatefulOutputPin}; +use target_common::{declare_target, TargetInterface}; +use {console_backend as _, entry as _}; + +pub struct Target {} + +const GPIO_LEVEL_DELAY_CYCLES: u32 = 200_000_000; + +#[allow(dead_code)] +fn test_gpio_output() -> bool { + // On the AST1060 fixture board, GPIOI0 and I2C2 SCL share the same exposed + // pin, while GPIOI1 and I2C2 SDA share the same exposed pin. Connect GPIOI0 + // and GPIOI1 to external test equipment to verify the output levels. + let gpioi = unsafe { + let scu = ScuRegisters::new_global_unlocked(); + scu.apply_pinctrl_group(pinctrl::PINCTRL_GPIOI0); + scu.apply_pinctrl_group(pinctrl::PINCTRL_GPIOI1); + gpioi::GPIOI::new_global().split() + }; + pw_log::info!("--- GPIOI output test ---"); + + // set GPIOI0 to push-pull output and drive it low, then high. + let mut pi0 = gpioi.pi0.into_push_pull_output(); + if pi0.set_low().is_err() || !pi0.is_set_low().unwrap_or(false) { + pw_log::error!("GPIOI0 push-pull output did not latch low"); + return false; + } + pw_log::info!("GPIOI0 push-pull output latched low"); + + cortex_m::asm::delay(2 * GPIO_LEVEL_DELAY_CYCLES); + + if pi0.set_high().is_err() || !pi0.is_set_high().unwrap_or(false) { + pw_log::error!("GPIOI0 push-pull output did not latch high"); + //return false; + } + pw_log::info!("GPIOI0 push-pull output latched high"); + + let mut pi1 = gpioi.pi1.into_open_drain_output::<ActiveLow>(); + if pi1.set_low().is_err() || !pi1.is_set_low().unwrap_or(false) { + pw_log::error!("GPIOI1 open-drain output did not latch low"); + return false; + } + pw_log::info!("GPIOI1 open-drain output latched low"); + + cortex_m::asm::delay(2 * GPIO_LEVEL_DELAY_CYCLES); + + if pi1.set_high().is_err() || !pi1.is_set_high().unwrap_or(false) { + pw_log::error!("GPIOI1 open-drain output did not latch high"); + return false; + } + pw_log::info!("GPIOI1 open-drain output latched high"); + + true +} + +fn test_gpio_loopback() -> bool { + // The loopback requires a jumper between the fixture board's exposed + // GPIOI0/I2C2 SCL pin and GPIOI1/I2C2 SDA pin. + let gpioi = unsafe { + let scu = ScuRegisters::new_global_unlocked(); + scu.apply_pinctrl_group(pinctrl::PINCTRL_GPIOI0); + scu.apply_pinctrl_group(pinctrl::PINCTRL_GPIOI1); + gpioi::GPIOI::new_global().split() + }; + pw_log::info!("--- GPIOI loopback test ---"); + + let mut output = gpioi.pi0.into_push_pull_output(); + let mut input = gpioi.pi1.into_pull_down_input(); + + let interrupt_cases = [ + ("level-high", InterruptMode::LevelHigh, false, true), + ("level-low", InterruptMode::LevelLow, true, false), + ("rising-edge", InterruptMode::EdgeRising, false, true), + ("falling-edge", InterruptMode::EdgeFalling, true, false), + ("both-edge rising", InterruptMode::EdgeBoth, false, true), + ("both-edge falling", InterruptMode::EdgeBoth, true, false), + ]; + + for (name, mode, initial_high, trigger_high) in interrupt_cases { + pw_log::info!("=== Testing GPIOI1 {} interrupt ===", name as &str); + + input.set_interrupt_mode(InterruptMode::Disabled); + input.clear_interrupt(); + + let initial_result = if initial_high { + output.set_high() + } else { + output.set_low() + }; + if initial_result.is_err() { + pw_log::error!("{}: GPIOI0 could not drive initial level", name as &str); + return false; + } + let initial_level = if initial_high { "high" } else { "low" }; + pw_log::info!("{}: GPIOI0 as output drive initial level {}", name as &str, initial_level as &str); + cortex_m::asm::delay(GPIO_LEVEL_DELAY_CYCLES); + + let input_matches = if initial_high { + input.is_high().unwrap_or(false) + } else { + input.is_low().unwrap_or(false) + }; + if !input_matches { + pw_log::error!("{}: input and output GPIO level mismatch", name as &str); + return false; + } + pw_log::info!("{}: GPIOI1 as input read initial level {}", name as &str, initial_level as &str); + + input.clear_interrupt(); + input.set_interrupt_mode(mode); + if input.get_interrupt_status() { + pw_log::error!("{}: interrupt pending before trigger", name as &str); + input.set_interrupt_mode(InterruptMode::Disabled); + return false; + } + + let trigger_result = if trigger_high { + output.set_high() + } else { + output.set_low() + }; + if trigger_result.is_err() { + pw_log::error!("{}: GPIOI0 could not drive trigger level", name as &str); + input.set_interrupt_mode(InterruptMode::Disabled); + return false; + } + let trigger_level = if trigger_high { "high" } else { "low" }; + pw_log::info!("{}: GPIOI0 as output drive trigger level {}", name as &str, trigger_level as &str); + cortex_m::asm::delay(GPIO_LEVEL_DELAY_CYCLES); + + let input_matches = if trigger_high { + input.is_high().unwrap_or(false) + } else { + input.is_low().unwrap_or(false) + }; + if !input_matches { + pw_log::error!("{}: input and output GPIO level mismatch", name as &str); + input.set_interrupt_mode(InterruptMode::Disabled); + return false; + } + pw_log::info!("{}: GPIOI1 as input read trigger level {}", name as &str, trigger_level as &str); + + if !input.get_interrupt_status() { + pw_log::error!("{}: interrupt status was not set", name as &str); + input.set_interrupt_mode(InterruptMode::Disabled); + return false; + } + pw_log::info!("GPIOI1 {} interrupt status set", name as &str); + + // Disable level-sensitive modes before clearing so an active level + // cannot immediately reassert the status bit. + input.set_interrupt_mode(InterruptMode::Disabled); + input.clear_interrupt(); + if input.get_interrupt_status() { + pw_log::error!("{}: interrupt status did not clear", name as &str); + return false; + } + } + + true +} + +fn run_gpioi_loopback_test() -> bool { + pw_log::info!("=== AST10x0 GPIOI loopback test ==="); + // connect a jumper between the exposed GPIOI0/I2C2 SCL and GPIOI1/I2C2 SDA pins to verify the loopback. + test_gpio_loopback() +} + +impl TargetInterface for Target { + const NAME: &'static str = "AST10x0 GPIOI loopback test"; + + fn main() -> ! { + let sentinel = if run_gpioi_loopback_test() { + b"TEST_RESULT:PASS\n" + } else { + b"TEST_RESULT:FAIL\n" + }; + let _ = console_backend_write_all(sentinel); + + #[expect(clippy::empty_loop)] + loop {} + } +} + +declare_target!(Target);