Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/Cargo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ jobs:
strategy:
matrix:
FEATURES: [d13x]
EXAMPLES: [pbp-blinky, pbp-hello-world, pbp-i2c-master, pbp-boot-info, pbp-flash, pbp-dma]
EXAMPLES: [pbp-blinky, pbp-hello-world, pbp-i2c-master, pbp-boot-info, pbp-flash, pbp-dma, pbp-pwm]
steps:
- uses: actions/checkout@v4
- uses: actions-rust-lang/setup-rust-toolchain@v1
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ members = [
"examples/pbp/pbp-boot-info",
"examples/pbp/pbp-flash",
"examples/pbp/pbp-dma",
"examples/pbp/pbp-pwm",
]
resolver = "3"
3 changes: 3 additions & 0 deletions artinchip-hal/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod gpio;
pub mod gtc;
pub mod i2c;
pub mod pad;
pub mod pwm;
pub mod qspi;
pub mod rtc;
pub mod sdmc;
Expand All @@ -34,6 +35,7 @@ pub mod prelude {
pub mod traits {
pub use crate::dma::DmaExt as _;
pub use crate::gtc::GtcExt as _;
pub use crate::pwm::PwmExt as _;
pub use crate::rtc::RtcExt as _;
pub use crate::uart::UartExt as _;
pub use crate::wdog::WdogExt as _;
Expand All @@ -53,6 +55,7 @@ pub mod instances {
pub use crate::dma::Dma;
pub use crate::gtc::Gtc;
pub use crate::i2c::I2c;
pub use crate::pwm::{PwmChannel, PwmChannels};
pub use crate::qspi::Qspi;
pub use crate::rtc::Rtc;
pub use crate::sdmc::Sdmc;
Expand Down
3 changes: 3 additions & 0 deletions artinchip-hal/src/pad.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ impl<const I: u8> super::qspi::MasterOutSlaveIn<I> for NoPad {}
impl<const I: u8> super::qspi::ChipSelect<I> for NoPad {}
impl<const I: u8> super::qspi::Hold<I> for NoPad {}
impl<const I: u8> super::qspi::WriteProtect<I> for NoPad {}

impl<const I: u8> super::pwm::PortA<I> for NoPad {}
impl<const I: u8> super::pwm::PortB<I> for NoPad {}
15 changes: 15 additions & 0 deletions artinchip-hal/src/pwm.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
//! Pulse Width Modulation (PWM).

mod config;
mod driver;
mod instance;
mod pad;
mod pwm_ext;
mod register;

pub use config::*;
pub use driver::*;
pub use instance::*;
pub use pad::*;
pub use pwm_ext::PwmExt;
pub use register::*;
67 changes: 67 additions & 0 deletions artinchip-hal/src/pwm/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use super::register::*;
use embedded_time::duration::Nanoseconds;
use embedded_time::rate::Hertz;

/// PWM action.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Action {
/// Counts down and `TBCTR` = `CMPB` action.
pub cbd: ActionMode,
/// Counts up and `TBCTR` = `CMPA` action.
pub cbu: ActionMode,
/// Counts down and `TBCTR` = `CMPA` action.
Comment on lines +8 to +12

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

Field doc for cbu is incorrect: it says "Counts up and TBCTR = CMPA" but CBU corresponds to the CMPB-up event (see AqControl::set_cbu_mode docs). Please fix the comment to avoid confusing users configuring actions.

Copilot uses AI. Check for mistakes.
pub cad: ActionMode,
/// Counts up and `TBCTR` = `CMPA` action.
pub cau: ActionMode,
/// `TBCTR` = `PRD` action.
pub prd: ActionMode,
/// `TBCTR` = 0 action.
pub zro: ActionMode,
}

/// PWM configuration.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PwmConfig {
pub freq: Hertz,
/// Unit: nanoseconds.
pub duty_a: Nanoseconds,
/// Unit: nanoseconds.
pub duty_b: Nanoseconds,
/// Unit: nanoseconds.
pub period: Nanoseconds,
pub cnt_mode: CntMode,
pub tb_clk_rate: Hertz,
pub init_level: InitLevel,
pub action_0: Option<Action>,
pub action_1: Option<Action>,
}

impl Default for PwmConfig {
fn default() -> Self {
Self {
freq: Hertz::new(1_000),
duty_a: Nanoseconds(500_000),
duty_b: Nanoseconds(500_000),
period: Nanoseconds(1_000_000),
cnt_mode: CntMode::CountUp,
tb_clk_rate: Hertz(24_000_000),
init_level: InitLevel::High,
action_0: Some(Action {
cbd: ActionMode::NoOp,
cbu: ActionMode::NoOp,
cad: ActionMode::NoOp,
cau: ActionMode::SetLow,
prd: ActionMode::SetHigh,
zro: ActionMode::NoOp,
}),
action_1: Some(Action {
cbd: ActionMode::NoOp,
cbu: ActionMode::SetLow,
cad: ActionMode::NoOp,
cau: ActionMode::NoOp,
prd: ActionMode::SetHigh,
zro: ActionMode::NoOp,
}),
}
}
}
224 changes: 224 additions & 0 deletions artinchip-hal/src/pwm/driver.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
//! Pwm channel implementation.

use embedded_time::duration::Nanoseconds;
use embedded_time::rate::Hertz;

use super::config::PwmConfig;
use super::instance::PwmChannel;
use super::pad::*;
use super::register::*;
use crate::cmu::Cmu;

/// Pwm channel driver with statically known channel number and pad.
pub struct PwmChDriver<'a, const I: u8, PAD>
where
PAD: PwmPads<I>,
{
reg: &'a RegisterBlock,
pad: PAD,
config: PwmConfig,
}

impl<'a, const I: u8, PAD> PwmChDriver<'a, I, PAD>
where
PAD: PwmPads<I>,
{
const DEFAULT_PWM_CLK: u32 = 48_000_000;

/// Create a new PWM channel.
pub fn __new(reg: &'a RegisterBlock, pad: PAD, config: PwmConfig, cmu: &mut Cmu) -> Self {
let clk = &cmu.register_block().clock_pwm;
let fix_mod_div = 24;
if !clk.read().is_bus_clk_enabled() {
unsafe {
// Initialize module clock.
// Reference: https://aicdoc.artinchip.com/topics/ic/cmu/cmu-function2-d13x.html#topic_yvp_f24_4bc__table_qb3_bn5_ydc
clk.modify(|v| v.set_module_clk_div(fix_mod_div).enable_module_clk());
clk.modify(|v| v.enable_bus_clk());
clk.modify(|v| v.enable_module_reset());
riscv::asm::delay(500);
clk.modify(|v| v.disable_module_reset());

// Enable PWM clk.
reg.ctrl.modify(|v| v.enable());
}
}

unsafe {
let channel = &reg.channels[I as usize];
// Enable channel clock.
reg.ck_ctrl.modify(|v| v.enable_ch_clk(I));
// Set shadow register load mode.
channel.cmp_ctrl.modify(|v| {
v.set_cmp_a_shdw_ld_mode(ShdwLdMode::Mode2)
.set_cmp_b_shdw_ld_mode(ShdwLdMode::Mode2)
});

// Set clock div.
let ch_tb_div = (Self::DEFAULT_PWM_CLK.saturating_div(config.tb_clk_rate.0) - 1) as u16;

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

set_clk_div calculation can underflow or produce an invalid divider. If config.tb_clk_rate.0 is 0, saturating_div returns u32::MAX; if it’s > DEFAULT_PWM_CLK, the division yields 0 and the subsequent - 1 wraps. Consider validating tb_clk_rate (non-zero, <= source clock) and using checked_div/saturating_sub(1) with a sensible clamp (e.g., 0..=0x0FFF).

Suggested change
let ch_tb_div = (Self::DEFAULT_PWM_CLK.saturating_div(config.tb_clk_rate.0) - 1) as u16;
let src_clk_hz = Self::DEFAULT_PWM_CLK;
// Clamp requested tb_clk_rate to a safe, non-zero range within the source clock.
let requested_tb_clk_hz = config.tb_clk_rate.0;
let clamped_tb_clk_hz =
core::cmp::max(1, core::cmp::min(requested_tb_clk_hz, src_clk_hz));
// Compute divider: src_clk / tb_clk - 1, with saturation and hardware-range clamp.
let mut div = src_clk_hz
.checked_div(clamped_tb_clk_hz)
.unwrap_or(1)
.saturating_sub(1);
// Hardware divider is typically limited; clamp to 0x0FFF as a safe upper bound.
div = core::cmp::min(div, 0x0FFF);
let ch_tb_div = div as u16;

Copilot uses AI. Check for mistakes.
channel
.tb_ctrl
.modify(|v| v.set_clk_div(ch_tb_div).set_cnt_mode(config.cnt_mode));

// Set action qualifier control registers.
if let Some(action) = config.action_0 {
channel.aq_ctrl_0.modify(|v| {
v.set_init_level(config.init_level)
.set_cbd_mode(action.cbd)
.set_cbu_mode(action.cbu)
.set_cad_mode(action.cad)
.set_cau_mode(action.cau)
.set_prd_mode(action.prd)
.set_zro_mode(action.zro)
});
}

if let Some(action) = config.action_1 {
channel.aq_ctrl_1.modify(|v| {
v.set_init_level(config.init_level)
.set_cbd_mode(action.cbd)
.set_cbu_mode(action.cbu)
.set_cad_mode(action.cad)
.set_cau_mode(action.cau)
.set_prd_mode(action.prd)
.set_zro_mode(action.zro)
});
}

reg.int_ctrl.modify(|v| v.disable_ch_int(I));
reg.int_stat.modify(|v| v.clear_ch_int_pending(I));
}
Self { reg, config, pad }
}

/// Enable channel.
pub fn enable(&mut self) {
unsafe {
self.reg.m_ctrl.modify(|v| v.enable_ch(I));
}
}

/// Disable channel.
pub fn disable(&mut self) {
unsafe {
self.reg.m_ctrl.modify(|v| v.disable_ch(I));
}
}

/// Convert period in nanoseconds to frequency in Hertz.
fn period2hertz(&self, period: Nanoseconds) -> Hertz {
Hertz::new(1_000_000_000 / period.0)
}

/// Set period and duty time.
///
/// - `period`: desired period in nanoseconds.
/// - `duty_a`: desired duty cycle for channel A in nanoseconds.
/// - `duty_b`: desired duty cycle for channel B in nanoseconds.
pub fn set_period_and_duty(
&mut self,
period: Nanoseconds,
duty_a: Nanoseconds,
duty_b: Nanoseconds,
) {
let channel = &self.reg.channels[I as usize];
self.config.freq = self.period2hertz(period);
self.config.period = period;
self.config.duty_a = duty_a;
self.config.duty_b = duty_b;

let tb_clk = self.config.tb_clk_rate.0;
let freq = self.config.freq.0;
let prd_reg_val = if self.config.cnt_mode == CntMode::CountUpAndDown {
((tb_clk / freq) / 2).min(u16::MAX as u32) as u16
} else {
((tb_clk / freq) - 1).min(u16::MAX as u32) as u16
Comment on lines +130 to +135

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

prd_reg_val computation can divide by zero / underflow: freq can become 0 (e.g., large period causing 1_000_000_000 / period == 0), and ((tb_clk / freq) - 1) will also underflow when tb_clk < freq. Consider computing period ticks directly from period and tb_clk_rate, or at least clamp freq to >= 1 and use saturating_sub(1)/checked_div to avoid wraparound.

Suggested change
let tb_clk = self.config.tb_clk_rate.0;
let freq = self.config.freq.0;
let prd_reg_val = if self.config.cnt_mode == CntMode::CountUpAndDown {
((tb_clk / freq) / 2).min(u16::MAX as u32) as u16
} else {
((tb_clk / freq) - 1).min(u16::MAX as u32) as u16
let tb_clk = self.config.tb_clk_rate.0 as u64;
let period_ns = period as u64;
let ticks = tb_clk
.saturating_mul(period_ns)
/ 1_000_000_000u64;
let prd_reg_val = if self.config.cnt_mode == CntMode::CountUpAndDown {
(ticks / 2).min(u16::MAX as u64) as u16
} else {
ticks
.saturating_sub(1)
.min(u16::MAX as u64) as u16

Copilot uses AI. Check for mistakes.
};

let cmp_a_val = if prd_reg_val > 0 {
let cmp = (duty_a.0 as u64 * prd_reg_val as u64) / period.0 as u64;
if cmp == prd_reg_val as u64 {
(prd_reg_val as u64 + 1).min(u16::MAX as u64) as u16
} else {
cmp as u16
}
} else {
0
};

let cmp_b_val = if prd_reg_val > 0 {
let cmp = (duty_b.0 as u64 * prd_reg_val as u64) / period.0 as u64;
if cmp == prd_reg_val as u64 {
(prd_reg_val as u64 + 1).min(u16::MAX as u64) as u16
} else {
cmp as u16
}
} else {
0
};

unsafe {
channel.tb_prd.modify(|v| v.set_tb_prd(prd_reg_val));
channel.cmp_a.modify(|v| v.set_cmp(cmp_a_val));
channel.cmp_b.modify(|v| v.set_cmp(cmp_b_val));
}
}

/// Set PWM frequency and duty cycle ratio.
///
/// - `freq`: desired frequency in Hertz (must be > 0).
/// - `ratio_a`: PWM port A duty cycle percentage, clamped to 0.0..100.0.
/// - `ratio_b`: PWM port B duty cycle percentage, clamped to 0.0..100.0.
pub fn set_freq_and_ratio(&mut self, freq: Hertz, ratio_a: f32, ratio_b: f32) {
let ratio_a = ratio_a.clamp(0.0, 100.0);
let ratio_b = ratio_b.clamp(0.0, 100.0);
let period_ns = (1_000_000_000 / freq.0).max(1);
let duty_ns_a = ((period_ns as f32 * ratio_a / 100.0) + 0.5) as u32;
let duty_ns_b = ((period_ns as f32 * ratio_b / 100.0) + 0.5) as u32;
let duty_ns_a = duty_ns_a.min(period_ns);
let duty_ns_b = duty_ns_b.min(period_ns);
self.config.freq = freq;
Comment on lines +175 to +180

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

set_freq_and_ratio documents freq must be > 0, but period_ns is computed with 1_000_000_000 / freq.0 before any validation. If freq.0 == 0, this will panic (division by zero). Add an early check (e.g., return Result or clamp to 1 Hz) before performing the division.

Suggested change
let period_ns = (1_000_000_000 / freq.0).max(1);
let duty_ns_a = ((period_ns as f32 * ratio_a / 100.0) + 0.5) as u32;
let duty_ns_b = ((period_ns as f32 * ratio_b / 100.0) + 0.5) as u32;
let duty_ns_a = duty_ns_a.min(period_ns);
let duty_ns_b = duty_ns_b.min(period_ns);
self.config.freq = freq;
let freq_hz = freq.0.max(1);
let period_ns = (1_000_000_000 / freq_hz).max(1);
let duty_ns_a = ((period_ns as f32 * ratio_a / 100.0) + 0.5) as u32;
let duty_ns_b = ((period_ns as f32 * ratio_b / 100.0) + 0.5) as u32;
let duty_ns_a = duty_ns_a.min(period_ns);
let duty_ns_b = duty_ns_b.min(period_ns);
self.config.freq = Hertz(freq_hz);

Copilot uses AI. Check for mistakes.
self.config.period = Nanoseconds(period_ns);
self.config.duty_a = Nanoseconds(duty_ns_a);
self.config.duty_b = Nanoseconds(duty_ns_b);
self.set_period_and_duty(self.config.period, self.config.duty_a, self.config.duty_b);
}

/// Get current frequency and duty cycle ratio.
pub fn freq_and_ratio(&self) -> (Hertz, f32, f32) {
(
self.config.freq,
(self.config.duty_a.0 as f32 / self.config.period.0 as f32) * 100.0,
(self.config.duty_b.0 as f32 / self.config.period.0 as f32) * 100.0,
)
}

/// Get current period and duty cycle.
pub fn period_and_duty(&self) -> (Nanoseconds, Nanoseconds, Nanoseconds) {
(self.config.period, self.config.duty_a, self.config.duty_b)
}

/// Free the PWM channel and return PwmChannel instance, port A and B pads.
pub fn free(self, cmu: &Cmu) -> (PwmChannel<I>, PAD) {
let mut reset_is_needed = true;
// Check if any channel is still working.
for ch in 0..4 {
if self.reg.ck_ctrl.read().is_ch_clk_enabled(ch) {
reset_is_needed = false;
}
}

if reset_is_needed {
Comment on lines +201 to +211

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

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

free() never disables this channel’s clock (ck_ctrl) or module enable bit (m_ctrl) before checking whether any channels are still active. As written, reset_is_needed will almost always become false because the current channel clock remains enabled, so the PWM module may never be reset/clock-gated. Consider disabling channel I (module + clock) first, then checking remaining channels, and optionally disabling reg.ctrl when the last channel is released.

Copilot uses AI. Check for mistakes.
unsafe {
let clk = &cmu.register_block().clock_pwm;
clk.modify(|v| {
v.disable_bus_clk()
.disable_module_clk()
.enable_module_reset()
});
}
}

(PwmChannel::__new(self.reg), self.pad)
}
}
Loading
Loading