From 1ce930f7a83aa27ccbc121f5eb40725b3ba0c289 Mon Sep 17 00:00:00 2001 From: Robin Mueller Date: Wed, 2 Apr 2025 16:26:39 +0200 Subject: [PATCH] PWM works --- examples/embassy/src/bin/pwm.rs | 151 ++++++++++++++++++++++++++ zedboard-fpga-design/src/zedboard.xdc | 6 +- zynq7000-hal/src/ttc.rs | 82 ++++++++++---- zynq7000/src/ttc.rs | 12 +- 4 files changed, 226 insertions(+), 25 deletions(-) create mode 100644 examples/embassy/src/bin/pwm.rs diff --git a/examples/embassy/src/bin/pwm.rs b/examples/embassy/src/bin/pwm.rs new file mode 100644 index 0000000..fc5ee75 --- /dev/null +++ b/examples/embassy/src/bin/pwm.rs @@ -0,0 +1,151 @@ +//! PWM example which uses a PWM pin routed through EMIO. +//! +//! On the Zedboard, the PWM waveform output will be on the W12 pin of PMOD JB1. The Zedboard +//! reference FPGA design must be flashed onto the Zedboard for this to work. +#![no_std] +#![no_main] + +use core::panic::PanicInfo; +use cortex_ar::asm::nop; +use embassy_executor::Spawner; +use embassy_time::{Duration, Ticker}; +use embedded_hal::{digital::StatefulOutputPin, pwm::SetDutyCycle}; +use embedded_io::Write; +use fugit::RateExtU32; +use log::{error, info}; +use zynq7000_hal::{ + BootMode, + clocks::Clocks, + gic::{GicConfigurator, GicInterruptHelper, Interrupt}, + gpio::{MioPins, PinState}, + gtc::Gtc, + time::Hertz, + uart::{ClkConfigRaw, Uart, UartConfig}, +}; + +use zynq7000::PsPeripherals; +use zynq7000_rt as _; + +// Define the clock frequency as a constant +const PS_CLOCK_FREQUENCY: Hertz = Hertz::from_raw(33_333_300); + +/// Entry point (not called like a normal main function) +#[unsafe(no_mangle)] +pub extern "C" fn boot_core(cpu_id: u32) -> ! { + if cpu_id != 0 { + panic!("unexpected CPU ID {}", cpu_id); + } + main(); +} + +#[embassy_executor::main] +#[unsafe(export_name = "main")] +async fn main(_spawner: Spawner) -> ! { + let dp = PsPeripherals::take().unwrap(); + // Clock was already initialized by PS7 Init TCL script or FSBL, we just read it. + let clocks = Clocks::new_from_regs(PS_CLOCK_FREQUENCY).unwrap(); + // Set up the global interrupt controller. + let mut gic = GicConfigurator::new_with_init(dp.gicc, dp.gicd); + gic.enable_all_interrupts(); + gic.set_all_spi_interrupt_targets_cpu0(); + gic.enable(); + unsafe { + gic.enable_interrupts(); + } + let mio_pins = MioPins::new(dp.gpio); + + // Set up global timer counter and embassy time driver. + let gtc = Gtc::new(dp.gtc, clocks.arm_clocks()); + zynq7000_embassy::init(clocks.arm_clocks(), gtc); + + // Unwrap is okay, the address is definitely valid. + let ttc_0 = zynq7000_hal::ttc::Ttc::new(dp.ttc_0).unwrap(); + let mut pwm = + zynq7000_hal::ttc::Pwm::new_with_cpu_clk(ttc_0.ch0, clocks.arm_clocks(), 1000.Hz()) + .unwrap(); + pwm.set_duty_cycle_percent(50).unwrap(); + + // Set up the UART, we are logging with it. + let uart_clk_config = ClkConfigRaw::new_autocalc_with_error(clocks.io_clocks(), 115200) + .unwrap() + .0; + let uart_tx = mio_pins.mio48.into_uart(); + let uart_rx = mio_pins.mio49.into_uart(); + let mut uart = Uart::new_with_mio( + dp.uart_1, + UartConfig::new_with_clk_config(uart_clk_config), + (uart_tx, uart_rx), + ) + .unwrap(); + uart.write_all(b"-- Zynq 7000 Embassy Hello World --\n\r") + .unwrap(); + // Safety: We are not multi-threaded yet. + unsafe { zynq7000_hal::log::init_unsafe_single_core(uart, log::LevelFilter::Trace, false) }; + + let boot_mode = BootMode::new(); + info!("Boot mode: {:?}", boot_mode); + + let mut ticker = Ticker::every(Duration::from_millis(1000)); + let mut led = mio_pins.mio7.into_output(PinState::Low); + let mut current_duty = 0; + loop { + led.toggle().unwrap(); + + pwm.set_duty_cycle_percent(current_duty).unwrap(); + info!("Setting duty cycle to {}%", current_duty); + current_duty += 5; + if current_duty > 100 { + current_duty = 0; + } + + ticker.next().await; + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn _irq_handler() { + let mut gic_helper = GicInterruptHelper::new(); + let irq_info = gic_helper.acknowledge_interrupt(); + match irq_info.interrupt() { + Interrupt::Sgi(_) => (), + Interrupt::Ppi(ppi_interrupt) => { + if ppi_interrupt == zynq7000_hal::gic::PpiInterrupt::GlobalTimer { + unsafe { + zynq7000_embassy::on_interrupt(); + } + } + } + Interrupt::Spi(_spi_interrupt) => (), + Interrupt::Invalid(_) => (), + Interrupt::Spurious => (), + } + gic_helper.end_of_interrupt(irq_info); +} + +#[unsafe(no_mangle)] +pub extern "C" fn _abort_handler() { + loop { + nop(); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn _undefined_handler() { + loop { + nop(); + } +} + +#[unsafe(no_mangle)] +pub extern "C" fn _prefetch_handler() { + loop { + nop(); + } +} + +/// Panic handler +#[panic_handler] +fn panic(info: &PanicInfo) -> ! { + error!("Panic: {:?}", info); + loop {} +} diff --git a/zedboard-fpga-design/src/zedboard.xdc b/zedboard-fpga-design/src/zedboard.xdc index 57a66d3..001e72e 100644 --- a/zedboard-fpga-design/src/zedboard.xdc +++ b/zedboard-fpga-design/src/zedboard.xdc @@ -70,6 +70,6 @@ set_property IOSTANDARD LVCMOS33 [get_ports UART_rxd] set_property PACKAGE_PIN AA11 [get_ports UART_txd] set_property IOSTANDARD LVCMOS33 [get_ports UART_txd] -# TTC1 Wave Out -set_property PACKAGE_PIN W12 [get_ports {TTC1_WAVEOUT}] -set_property IOSTANDARD LVCMOS33 [get_ports {TTC1_WAVEOUT}] \ No newline at end of file +# TTC0 Wave Out +set_property PACKAGE_PIN W12 [get_ports {TTC0_WAVEOUT}] +set_property IOSTANDARD LVCMOS33 [get_ports {TTC0_WAVEOUT}] \ No newline at end of file diff --git a/zynq7000-hal/src/ttc.rs b/zynq7000-hal/src/ttc.rs index b40b1d4..4037c7d 100644 --- a/zynq7000-hal/src/ttc.rs +++ b/zynq7000-hal/src/ttc.rs @@ -1,8 +1,10 @@ //! Triple-timer counter (TTC) high-level driver. +//! +//! This module also contains support for PWM and output waveform generation. use core::convert::Infallible; -use arbitrary_int::{u3, u4}; +use arbitrary_int::{Number, u3, u4}; use zynq7000::ttc::{MmioTtc, TTC_0_BASE_ADDR, TTC_1_BASE_ADDR}; #[cfg(not(feature = "7z010-7z007s-clg225"))] @@ -87,6 +89,8 @@ pub struct Ttc { impl Ttc { /// Create a new TTC instance. The passed TTC peripheral instance MUST point to a valid /// processing system TTC peripheral. + /// + /// Returns [None] if the passed peripheral block does not have a valid PS TTC address. pub fn new(ps_ttc: impl PsTtc) -> Option { ps_ttc.id()?; let regs = ps_ttc.reg_block(); @@ -116,6 +120,14 @@ impl TtcChannel { &mut self.regs } + #[inline] + pub fn read_counter(&self) -> u16 { + self.regs + .read_current_counter(self.id as usize) + .unwrap() + .count() + } + pub fn id(&self) -> ChannelId { self.id } @@ -137,16 +149,28 @@ pub enum TtcConstructionError { FrequencyIsZero(#[from] FrequencyIsZeroError), } -pub fn calculate_prescaler_reg_and_interval_ticks(mut ref_clk: Hertz, freq: Hertz) -> (u4, u16) { +pub fn calculate_prescaler_reg_and_interval_ticks( + mut ref_clk: Hertz, + freq: Hertz, +) -> (Option, u16) { // TODO: Can this be optimized? - let mut prescaler_reg = 0; + let mut prescaler_reg: Option = None; let mut tick_val = ref_clk / freq; while tick_val > u16::MAX as u32 { ref_clk /= 2; - prescaler_reg += 1; + if let Some(prescaler_reg) = prescaler_reg { + // TODO: Better error handling for this case? Can this even happen? + if prescaler_reg.value() == u4::MAX.value() { + break; + } else { + prescaler_reg.checked_add(u4::new(1)); + } + } else { + prescaler_reg = Some(u4::new(0)); + } tick_val = ref_clk / freq; } - (u4::new(prescaler_reg), tick_val as u16) + (prescaler_reg, tick_val as u16) } pub struct Pwm { @@ -155,8 +179,9 @@ pub struct Pwm { } impl Pwm { - /// Create a new PWM instance which uses the CPU 1x clock as the clock source. - pub fn new_with_mio_wave_out( + /// Create a new PWM instance which uses the CPU 1x clock as the clock source and also uses + /// a MIO output pin for the waveform output. + pub fn new_with_cpu_clk_and_mio_waveout( channel: TtcChannel, arm_clocks: &ArmClocks, freq: Hertz, @@ -165,16 +190,30 @@ impl Pwm { if wave_out.mux_conf() != TTC_MUX_CONF { return Err(InvalidTtcPinConfigError(wave_out.mux_conf()).into()); } + Ok(Self::new_with_cpu_clk(channel, arm_clocks, freq)?) + } + + /// Create a new PWM instance which uses the CPU 1x clock as the clock source. + pub fn new_with_cpu_clk( + channel: TtcChannel, + arm_clocks: &ArmClocks, + freq: Hertz, + ) -> Result { + Self::new_generic(channel, arm_clocks.cpu_1x_clk(), freq) + } + + /// Create a new PWM instance based on a reference clock source. + pub fn new_generic( + channel: TtcChannel, + ref_clk: Hertz, + freq: Hertz, + ) -> Result { if freq.raw() == 0 { - return Err(FrequencyIsZeroError.into()); + return Err(FrequencyIsZeroError); } - let (prescaler_reg, tick_val) = - calculate_prescaler_reg_and_interval_ticks(arm_clocks.cpu_1x_clk(), freq); + let (prescaler_reg, tick_val) = calculate_prescaler_reg_and_interval_ticks(ref_clk, freq); let id = channel.id() as usize; - let mut pwm = Self { - channel, - ref_clk: arm_clocks.cpu_1x_clk(), - }; + let mut pwm = Self { channel, ref_clk }; pwm.set_up_and_configure_pwm(id, prescaler_reg, tick_val); Ok(pwm) } @@ -193,6 +232,11 @@ impl Pwm { Ok(()) } + #[inline] + pub fn ttc_channel_mut(&mut self) -> &mut TtcChannel { + &mut self.channel + } + #[inline] pub fn max_duty_cycle(&self) -> u16 { self.channel @@ -229,7 +273,7 @@ impl Pwm { .unwrap(); } - fn set_up_and_configure_pwm(&mut self, id: usize, prescaler_reg: u4, tick_val: u16) { + fn set_up_and_configure_pwm(&mut self, id: usize, prescaler_reg: Option, tick_val: u16) { // Disable the counter first. self.channel .regs @@ -244,8 +288,8 @@ impl Pwm { zynq7000::ttc::ClockControl::builder() .with_ext_clk_edge(false) .with_clk_src(zynq7000::ttc::ClockSource::Pclk) - .with_prescaler(prescaler_reg) - .with_prescale_enable(prescaler_reg.value() > 0) + .with_prescaler(prescaler_reg.unwrap_or(u4::new(0))) + .with_prescale_enable(prescaler_reg.is_some()) .build(), ) .unwrap(); @@ -266,10 +310,10 @@ impl Pwm { .write_cnt_ctrl( id, zynq7000::ttc::CounterControl::builder() - .with_wave_polarity(zynq7000::ttc::WavePolarity::HighToLowOnMatch1) + .with_wave_polarity(zynq7000::ttc::WavePolarity::LowToHighOnMatch1) .with_wave_enable_n(zynq7000::ttc::WaveEnable::Enable) .with_reset(true) - .with_match_enable(false) + .with_match_enable(true) .with_decrementing(false) .with_mode(zynq7000::ttc::Mode::Interval) .with_disable(false) diff --git a/zynq7000/src/ttc.rs b/zynq7000/src/ttc.rs index 8f00a38..76fe4d8 100644 --- a/zynq7000/src/ttc.rs +++ b/zynq7000/src/ttc.rs @@ -1,12 +1,14 @@ //! Triple-timer counter (TTC) register module. use arbitrary_int::u4; -pub const TTC_0_BASE_ADDR: usize = 0xF800_1004; -pub const TTC_1_BASE_ADDR: usize = 0xF800_2004; +pub const TTC_0_BASE_ADDR: usize = 0xF800_1000; +pub const TTC_1_BASE_ADDR: usize = 0xF800_2000; +#[derive(Debug, Default)] #[bitbybit::bitenum(u1, exhaustive = true)] pub enum ClockSource { /// PS internal bus clock. + #[default] Pclk = 0b0, External = 0b1, } @@ -25,13 +27,14 @@ pub struct ClockControl { prescale_enable: bool, } +#[derive(Debug)] #[bitbybit::bitenum(u1, exhaustive = true)] pub enum Mode { Overflow = 0b0, Interval = 0b1, } -#[derive(Default)] +#[derive(Debug, Default)] #[bitbybit::bitenum(u1, exhaustive = true)] pub enum WavePolarity { /// The waveform output goes from high to low on a match 0 interrupt and returns high on @@ -43,6 +46,7 @@ pub enum WavePolarity { LowToHighOnMatch1 = 0b1, } +#[derive(Debug)] #[bitbybit::bitenum(u1, exhaustive = true)] pub enum WaveEnable { Enable = 0b0, @@ -158,6 +162,8 @@ pub struct Ttc { event_reg: [EventCount; 3], } +static_assertions::const_assert_eq!(core::mem::size_of::(), 0x84); + impl Ttc { /// Create a new TTC MMIO instance for TTC0 at address [TTC_0_BASE_ADDR]. ///