char: add pwm subsystem, add sifive pwm driver
This commit is contained in:
@@ -214,6 +214,27 @@
|
||||
interrupts = <0x13>, <0x15>, <0x16>, <0x14>;
|
||||
reg = <0x00 0x2010000 0x00 0x1000>;
|
||||
};
|
||||
|
||||
pwm0: pwm@10020000 {
|
||||
compatible = "sifive,fu740-c000-pwm", "sifive,pwm0";
|
||||
reg = <0x00 0x10020000 0x00 0x1000>;
|
||||
interrupt-parent = <&plic>;
|
||||
interrupts = <44>, <45>, <46>, <47>;
|
||||
clocks = <&prci CLK_PCLK>;
|
||||
#pwm-cells = <3>;
|
||||
status = "okay";
|
||||
};
|
||||
|
||||
pwm1: pwm@10021000 {
|
||||
compatible = "sifive,fu740-c000-pwm", "sifive,pwm0";
|
||||
reg = <0x00 0x10021000 0x00 0x1000>;
|
||||
interrupt-parent = <&plic>;
|
||||
interrupts = <48>, <49>, <50>, <51>;
|
||||
clocks = <&prci CLK_PCLK>;
|
||||
#pwm-cells = <3>;
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
memory@80000000 {
|
||||
@@ -226,23 +247,23 @@
|
||||
|
||||
green-d12 {
|
||||
label = "green:d12";
|
||||
pwms = <0x0d 0x00 0x773594 0x01>;
|
||||
active-low = <0x01>;
|
||||
max-brightness = <0xff>;
|
||||
pwms = <&pwm0 0 7812500 0x01>;
|
||||
active-low = <1>;
|
||||
max-brightness = <255>;
|
||||
linux,default-trigger = "none";
|
||||
};
|
||||
|
||||
green-d2 {
|
||||
label = "green:d2";
|
||||
pwms = <0x0d 0x01 0x773594 0x01>;
|
||||
active-low = <0x01>;
|
||||
max-brightness = <0xff>;
|
||||
pwms = <&pwm0 1 7812500 0x01>;
|
||||
active-low = <1>;
|
||||
max-brightness = <255>;
|
||||
linux,default-trigger = "none";
|
||||
};
|
||||
|
||||
red-d2 {
|
||||
label = "red:d2";
|
||||
pwms = <0x0d 0x02 0x773594 0x01>;
|
||||
pwms = <&pwm0 2 7812500 0x01>;
|
||||
active-low = <0x01>;
|
||||
max-brightness = <0xff>;
|
||||
linux,default-trigger = "none";
|
||||
@@ -250,9 +271,9 @@
|
||||
|
||||
blue-d2 {
|
||||
label = "blue:d2";
|
||||
pwms = <0x0d 0x03 0x773594 0x01>;
|
||||
active-low = <0x01>;
|
||||
max-brightness = <0xff>;
|
||||
pwms = <&pwm0 3 7812500 0x01>;
|
||||
active-low = <1>;
|
||||
max-brightness = <255>;
|
||||
linux,default-trigger = "none";
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
ranges;
|
||||
|
||||
uart0: serial@10010000 {
|
||||
interrupts = <0x04>;
|
||||
interrupts = <4>;
|
||||
interrupt-parent = <&plic>;
|
||||
clocks = <&prci 0x03>;
|
||||
reg = <0x00 0x10010000 0x00 0x1000>;
|
||||
@@ -100,7 +100,7 @@
|
||||
};
|
||||
|
||||
uart1: serial@10011000 {
|
||||
interrupts = <0x05>;
|
||||
interrupts = <5>;
|
||||
interrupt-parent = <&plic>;
|
||||
clocks = <&prci 0x03>;
|
||||
reg = <0x00 0x10011000 0x00 0x1000>;
|
||||
@@ -110,7 +110,7 @@
|
||||
pwm0: pwm@10020000 {
|
||||
#pwm-cells = <0>;
|
||||
clocks = <&prci 0x03>;
|
||||
interrupts = <0x2a>, <0x2b>, <0x2c>, <0x2d>;
|
||||
interrupts = <42>, <43>, <44>, <45>;
|
||||
interrupt-parent = <&plic>;
|
||||
reg = <0x00 0x10020000 0x00 0x1000>;
|
||||
compatible = "sifive,pwm0";
|
||||
@@ -119,10 +119,11 @@
|
||||
pwm1: pwm@10021000 {
|
||||
#pwm-cells = <0>;
|
||||
clocks = <&prci 0x03>;
|
||||
interrupts = <0x2e>, <0x2f>, <0x30>, <0x31>;
|
||||
interrupts = <46>, <47>, <48>, <49>;
|
||||
interrupt-parent = <&plic>;
|
||||
reg = <0x00 0x10021000 0x00 0x1000>;
|
||||
compatible = "sifive,pwm0";
|
||||
status = "disabled";
|
||||
};
|
||||
|
||||
gmac0: ethernet@10090000 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use alloc::sync::Arc;
|
||||
use device_api::{
|
||||
clock::{ClockController, ClockHandle, Hertz},
|
||||
device::Device,
|
||||
device::{Device, DeviceInitContext},
|
||||
};
|
||||
use device_tree::{
|
||||
DeviceTreePropertyRead, TProp,
|
||||
@@ -9,8 +9,14 @@ use device_tree::{
|
||||
};
|
||||
use libk::error::Error;
|
||||
use libk_mm::device::DeviceMemoryIo;
|
||||
use libk_util::sync::IrqSafeSpinlock;
|
||||
use tock_registers::{interfaces::Readable, register_structs, registers::ReadWrite};
|
||||
use libk_util::{OneTimeInit, sync::IrqSafeSpinlock};
|
||||
use tock_registers::{
|
||||
interfaces::{Readable, Writeable},
|
||||
register_structs,
|
||||
registers::ReadWrite,
|
||||
};
|
||||
|
||||
use crate::pll::{PllCfg, WrpllData};
|
||||
|
||||
const CLK_COREPLL: u32 = 0;
|
||||
const CLK_DDRPLL: u32 = 1;
|
||||
@@ -20,7 +26,7 @@ const CLK_TLCLK: u32 = 3;
|
||||
register_structs! {
|
||||
Regs {
|
||||
(0x000 => hfxosccfg: ReadWrite<u32>),
|
||||
(0x004 => corepllcfg0: ReadWrite<u32>),
|
||||
(0x004 => corepllcfg0: ReadWrite<u32, PllCfg::Register>),
|
||||
(0x008 => _0),
|
||||
(0x00C => ddrpllcfg0: ReadWrite<u32>),
|
||||
(0x010 => ddrpllcfg1: ReadWrite<u32>),
|
||||
@@ -38,9 +44,62 @@ struct Prci {
|
||||
clk_osc: ClockHandle,
|
||||
clk_rtc: ClockHandle,
|
||||
regs: IrqSafeSpinlock<DeviceMemoryIo<'static, Regs>>,
|
||||
init: OneTimeInit<()>,
|
||||
}
|
||||
|
||||
impl Regs {
|
||||
fn read_core_pll(&self) -> WrpllData {
|
||||
let pll = self.corepllcfg0.extract();
|
||||
let divr = pll.read(PllCfg::PLLR);
|
||||
let divf = pll.read(PllCfg::PLLF);
|
||||
let divq = pll.read(PllCfg::PLLQ);
|
||||
WrpllData {
|
||||
divq,
|
||||
divr,
|
||||
divf,
|
||||
int_feedback: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Prci {
|
||||
fn ensure_init(&self) {
|
||||
// U-Boot should configure coreclk to 1GHz, but when booting
|
||||
// in QEMU, there is not U-Boot stage, and some peripherals
|
||||
// like PWM expect the clock to be configured this way. So
|
||||
// this needs to be done manually here
|
||||
self.init.or_init_with(|| {
|
||||
let regs = self.regs.lock();
|
||||
if regs.coreclksel.get() != 0 {
|
||||
// coreclk configured to hfclk
|
||||
// configure corepll to output 1GHz and switch to it
|
||||
// divf = 150
|
||||
// divr = 4
|
||||
// divq = 1
|
||||
regs.corepllcfg0.write(
|
||||
PllCfg::PLLF.val(150)
|
||||
+ PllCfg::PLLR.val(4)
|
||||
+ PllCfg::PLLQ.val(1)
|
||||
+ PllCfg::PLLFSEBYPASS::SET,
|
||||
);
|
||||
while !regs.corepllcfg0.matches_all(PllCfg::PLLLOCK::SET) {
|
||||
core::hint::spin_loop();
|
||||
}
|
||||
regs.coreclksel.set(0);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Prci {
|
||||
unsafe fn init(self: Arc<Self>, _cx: DeviceInitContext) -> Result<(), Error> {
|
||||
self.ensure_init();
|
||||
if let Ok(coreclk) = self.clock_rate(Some(CLK_COREPLL)) {
|
||||
log::info!("coreclk = {coreclk}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"sifive,fu540-c000-prci"
|
||||
}
|
||||
@@ -48,19 +107,27 @@ impl Device for Prci {
|
||||
|
||||
impl ClockController for Prci {
|
||||
fn clock_rate(&self, clock: Option<u32>) -> Result<Hertz, Error> {
|
||||
self.ensure_init();
|
||||
let _ = &self.clk_rtc;
|
||||
let regs = self.regs.lock();
|
||||
match clock {
|
||||
Some(CLK_TLCLK) => {
|
||||
// 0 = CORE_PLL
|
||||
// 1 = HFCLK
|
||||
let coreclksel = regs.coreclksel.get() & 1 != 0;
|
||||
let glcm = if coreclksel {
|
||||
self.clk_osc.rate()?
|
||||
} else {
|
||||
todo!()
|
||||
Some(CLK_COREPLL) => {
|
||||
let regs = self.regs.lock();
|
||||
let hfclk = self.clk_osc.rate()?;
|
||||
// == 0 -> use corepll
|
||||
// == 1 -> use hfclk
|
||||
let coreclksel = regs.coreclksel.get() != 0;
|
||||
let coreclk = match coreclksel {
|
||||
true => hfclk,
|
||||
false => {
|
||||
let pll = regs.read_core_pll();
|
||||
pll.output_rate(hfclk)
|
||||
}
|
||||
};
|
||||
Ok(glcm / 2)
|
||||
Ok(coreclk)
|
||||
}
|
||||
Some(CLK_TLCLK) => {
|
||||
let coreclk = self.clock_rate(Some(CLK_COREPLL))?;
|
||||
Ok(coreclk / 2)
|
||||
}
|
||||
Some(_) => todo!(),
|
||||
None => Err(Error::InvalidArgument),
|
||||
@@ -72,6 +139,7 @@ impl ClockController for Prci {
|
||||
}
|
||||
|
||||
fn enable_clock(&self, clock: Option<u32>) -> Result<(), Error> {
|
||||
self.ensure_init();
|
||||
match clock {
|
||||
Some(CLK_GEMGXLPLL) | Some(CLK_DDRPLL) | Some(CLK_COREPLL) => todo!(),
|
||||
Some(CLK_TLCLK) => Ok(()),
|
||||
@@ -110,7 +178,8 @@ device_tree_driver! {
|
||||
let prci = Arc::new(Prci {
|
||||
regs: IrqSafeSpinlock::new(regs),
|
||||
clk_osc,
|
||||
clk_rtc
|
||||
clk_rtc,
|
||||
init: OneTimeInit::new()
|
||||
});
|
||||
|
||||
node.make_clock_controller(prci.clone());
|
||||
|
||||
@@ -12,10 +12,12 @@ use libk_mm::device::DeviceMemoryIo;
|
||||
use libk_util::sync::IrqSafeSpinlock;
|
||||
use tock_registers::{
|
||||
interfaces::Readable,
|
||||
register_bitfields, register_structs,
|
||||
register_structs,
|
||||
registers::{ReadOnly, ReadWrite},
|
||||
};
|
||||
|
||||
use crate::pll::{PllCfg, WrpllData};
|
||||
|
||||
// const CLK_COREPLL: u32 = 0;
|
||||
// const CLK_DDRPLL: u32 = 1;
|
||||
// const CLK_GEMGXLPLL: u32 = 2;
|
||||
@@ -26,39 +28,6 @@ use tock_registers::{
|
||||
const CLK_PCLK: u32 = 7;
|
||||
// const CLK_PCIE_AUX: u32 = 8;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WrpllData {
|
||||
divr: u32,
|
||||
divf: u32,
|
||||
divq: u32,
|
||||
int_feedback: bool,
|
||||
}
|
||||
|
||||
register_bitfields! {
|
||||
u32,
|
||||
PllCfg [
|
||||
PLLR OFFSET(0) NUMBITS(6) [],
|
||||
PLLF OFFSET(6) NUMBITS(9) [],
|
||||
PLLQ OFFSET(15) NUMBITS(3) [],
|
||||
PLLBYPASS OFFSET(24) NUMBITS(1) [],
|
||||
PLLFSEBYPASS OFFSET(25) NUMBITS(1) [],
|
||||
PLLLOCK OFFSET(31) NUMBITS(1) [],
|
||||
]
|
||||
}
|
||||
|
||||
impl WrpllData {
|
||||
fn fbdiv(&self) -> u32 {
|
||||
if self.int_feedback { 2 } else { 1 }
|
||||
}
|
||||
|
||||
fn output_rate(&self, input_rate: Hertz) -> Hertz {
|
||||
let fbdiv = self.fbdiv();
|
||||
let n = input_rate * fbdiv * (self.divf + 1);
|
||||
let n = n / (self.divr + 1);
|
||||
n >> self.divq
|
||||
}
|
||||
}
|
||||
|
||||
register_structs! {
|
||||
Regs {
|
||||
(0x00 => hfxosccfg: ReadWrite<u32>),
|
||||
|
||||
@@ -5,4 +5,6 @@ extern crate alloc;
|
||||
mod clock_fu540;
|
||||
mod clock_fu740;
|
||||
mod ethernet;
|
||||
mod pll;
|
||||
mod pwm;
|
||||
mod uart;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use device_api::clock::Hertz;
|
||||
use tock_registers::register_bitfields;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WrpllData {
|
||||
pub(crate) divr: u32,
|
||||
pub(crate) divf: u32,
|
||||
pub(crate) divq: u32,
|
||||
pub(crate) int_feedback: bool,
|
||||
}
|
||||
|
||||
register_bitfields! {
|
||||
u32,
|
||||
pub PllCfg [
|
||||
PLLR OFFSET(0) NUMBITS(6) [],
|
||||
PLLF OFFSET(6) NUMBITS(9) [],
|
||||
PLLQ OFFSET(15) NUMBITS(3) [],
|
||||
PLLBYPASS OFFSET(24) NUMBITS(1) [],
|
||||
PLLFSEBYPASS OFFSET(25) NUMBITS(1) [],
|
||||
PLLLOCK OFFSET(31) NUMBITS(1) [],
|
||||
]
|
||||
}
|
||||
|
||||
impl WrpllData {
|
||||
fn fbdiv(&self) -> u32 {
|
||||
if self.int_feedback { 2 } else { 1 }
|
||||
}
|
||||
|
||||
pub(crate) fn output_rate(&self, input_rate: Hertz) -> Hertz {
|
||||
let fbdiv = self.fbdiv();
|
||||
let n = input_rate * fbdiv * (self.divf + 1);
|
||||
let n = n / (self.divr + 1);
|
||||
n >> self.divq
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
use alloc::sync::Arc;
|
||||
use device_api::{
|
||||
clock::{ClockHandle, Hertz},
|
||||
device::{Device, DeviceInitContext},
|
||||
pwm::{PwmController, PwmControllerInfo},
|
||||
};
|
||||
use device_tree::driver::{Node, ProbeContext, device_tree_driver};
|
||||
use libk::{device::manager::DEVICE_REGISTRY, error::Error};
|
||||
use libk_mm::device::DeviceMemoryIo;
|
||||
use libk_util::sync::IrqSafeSpinlock;
|
||||
use tock_registers::{
|
||||
interfaces::{ReadWriteable, Readable, Writeable},
|
||||
register_bitfields, register_structs,
|
||||
registers::ReadWrite,
|
||||
};
|
||||
use yggdrasil_abi::time::NANOSECONDS_IN_SECOND;
|
||||
|
||||
const CHANNEL_COUNT: usize = 4;
|
||||
const CMPWIDTH: usize = 16;
|
||||
const SCALE_MAX: u32 = (1 << 4) - 1;
|
||||
const MAX_PERIOD_CYCLES: u64 = (1u64 << (CMPWIDTH + 16)) - 1;
|
||||
const MAX_COMPARE_VALUE: u32 = (1u32 << 16) - 1;
|
||||
|
||||
register_bitfields! {
|
||||
u32,
|
||||
PwmCfg [
|
||||
SCALE OFFSET(0) NUMBITS(4) [],
|
||||
STICKY OFFSET(8) NUMBITS(1) [],
|
||||
ZEROCMP OFFSET(9) NUMBITS(1) [],
|
||||
DEGLITCH OFFSET(10) NUMBITS(1) [],
|
||||
ENALWAYS OFFSET(12) NUMBITS(1) [],
|
||||
ENONESHOT OFFSET(13) NUMBITS(1) [],
|
||||
CMP_CENTER OFFSET(16) NUMBITS(4) [],
|
||||
CMP_GANG OFFSET(24) NUMBITS(4) [],
|
||||
CMP_IP OFFSET(28) NUMBITS(4) [],
|
||||
]
|
||||
}
|
||||
|
||||
register_structs! {
|
||||
Regs {
|
||||
(0x00 => pwmcfg: ReadWrite<u32, PwmCfg::Register>),
|
||||
(0x04 => _0),
|
||||
(0x08 => pwmcount: ReadWrite<u32>),
|
||||
(0x0C => _1),
|
||||
(0x10 => pwms: ReadWrite<u32>),
|
||||
(0x14 => _2),
|
||||
(0x20 => pwmcmp: [ReadWrite<u32>; CHANNEL_COUNT]),
|
||||
(0x30 => @END),
|
||||
}
|
||||
}
|
||||
|
||||
struct Pwm {
|
||||
name: &'static str,
|
||||
clock: ClockHandle,
|
||||
regs: IrqSafeSpinlock<DeviceMemoryIo<'static, Regs>>,
|
||||
}
|
||||
|
||||
impl Regs {
|
||||
fn ns_to_compare_value(&self, input_clk: Hertz, ns: u64) -> Option<u32> {
|
||||
let scale = self.pwmcfg.read(PwmCfg::SCALE);
|
||||
let tick_duration_ns = NANOSECONDS_IN_SECOND / (input_clk.0 >> scale);
|
||||
let ticks = ns / tick_duration_ns;
|
||||
if ticks < MAX_COMPARE_VALUE as u64 {
|
||||
Some(ticks as u32)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn compare_value_to_ns(&self, input_clk: Hertz, cval: u32) -> u64 {
|
||||
let scale = self.pwmcfg.read(PwmCfg::SCALE);
|
||||
let tick_duration_ns = NANOSECONDS_IN_SECOND / (input_clk.0 >> scale);
|
||||
tick_duration_ns * cval as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Pwm {
|
||||
unsafe fn init(self: Arc<Self>, _cx: DeviceInitContext) -> Result<(), Error> {
|
||||
// pwm counter registers are (cmpwidth + 15) bits wide
|
||||
self.clock.enable()?;
|
||||
// Set all channels to max value and channel 0 sets the period
|
||||
{
|
||||
let regs = self.regs.lock();
|
||||
|
||||
regs.pwmcfg
|
||||
.write(PwmCfg::ZEROCMP::SET + PwmCfg::ENALWAYS::SET);
|
||||
for channel in 0..CHANNEL_COUNT {
|
||||
regs.pwmcmp[channel].set(MAX_COMPARE_VALUE);
|
||||
}
|
||||
}
|
||||
|
||||
DEVICE_REGISTRY.pwm.register_controller(self)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
self.name
|
||||
}
|
||||
}
|
||||
|
||||
impl PwmController for Pwm {
|
||||
fn controller_info(&self) -> Result<PwmControllerInfo, Error> {
|
||||
Ok(PwmControllerInfo {
|
||||
channel_count: CHANNEL_COUNT,
|
||||
per_channel_period: false,
|
||||
})
|
||||
}
|
||||
|
||||
fn channel_period(&self, _channel: u32) -> Result<u64, Error> {
|
||||
let input_clk = self.clock.rate()?;
|
||||
let regs = self.regs.lock();
|
||||
let compare_value = regs.pwmcmp[0].get();
|
||||
if compare_value == MAX_COMPARE_VALUE {
|
||||
return Ok(0);
|
||||
}
|
||||
let period_ns = regs.compare_value_to_ns(input_clk, compare_value);
|
||||
Ok(period_ns)
|
||||
}
|
||||
|
||||
fn set_channel_period(&self, _channel: u32, period: u64) -> Result<u64, Error> {
|
||||
// Channel 0 sets the period
|
||||
let input_clk = self.clock.rate()?;
|
||||
let period_cycles = period * u64::from(input_clk) / NANOSECONDS_IN_SECOND;
|
||||
let mut prescaler = 0;
|
||||
if period_cycles > MAX_PERIOD_CYCLES {
|
||||
return Err(Error::InvalidArgument);
|
||||
}
|
||||
|
||||
while (period_cycles >> prescaler) >= MAX_COMPARE_VALUE as u64 {
|
||||
prescaler += 1;
|
||||
}
|
||||
|
||||
if prescaler > SCALE_MAX {
|
||||
log::warn!("Cannot fit {prescaler} into 4-bit prescaler");
|
||||
return Err(Error::InvalidArgument);
|
||||
}
|
||||
|
||||
let period_ticks = period_cycles >> prescaler;
|
||||
|
||||
let regs = self.regs.lock();
|
||||
|
||||
regs.pwmcfg
|
||||
.modify(PwmCfg::SCALE.val(prescaler) + PwmCfg::ZEROCMP::SET);
|
||||
regs.pwmcmp[0].set(period_ticks as u32);
|
||||
|
||||
let tick_duration_ns = NANOSECONDS_IN_SECOND / (input_clk.0 >> prescaler);
|
||||
let real_period_ns = tick_duration_ns * period_ticks;
|
||||
|
||||
Ok(real_period_ns)
|
||||
}
|
||||
|
||||
fn set_duty_cycle(&self, channel: u32, t: u64) -> Result<(), Error> {
|
||||
// Channel 0 sets the period
|
||||
if channel == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
let input_clk = self.clock.rate()?;
|
||||
let regs = self.regs.lock();
|
||||
let cval = regs
|
||||
.ns_to_compare_value(input_clk, t)
|
||||
.ok_or(Error::InvalidArgument)?;
|
||||
// If the cval exceeds the "period" comparator, the PWM will never fire
|
||||
let cval = cval.min(regs.pwmcmp[0].get().saturating_sub(1));
|
||||
regs.pwmcmp[channel as usize].set(cval);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn duty_cycle(&self, channel: u32) -> Result<u64, Error> {
|
||||
if channel == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
let input_clk = self.clock.rate()?;
|
||||
let regs = self.regs.lock();
|
||||
Ok(regs.compare_value_to_ns(input_clk, regs.pwmcmp[channel as usize].get()))
|
||||
}
|
||||
|
||||
fn stop_channel(&self, channel: u32) -> Result<(), Error> {
|
||||
if channel == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
self.regs.lock().pwmcmp[channel as usize].set(MAX_COMPARE_VALUE);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
device_tree_driver! {
|
||||
compatible: ["sifive,pwm0"],
|
||||
driver: {
|
||||
fn probe(&self, node: &Arc<Node>, context: &mut ProbeContext) -> Option<Arc<dyn Device>> {
|
||||
let name = node.name().unwrap_or("pwm");
|
||||
let base = node.map_base(context, 0)?;
|
||||
let clock = node.clock(0)?;
|
||||
|
||||
let regs = unsafe { DeviceMemoryIo::map(base, Default::default()) }.ok()?;
|
||||
|
||||
let pwm = Arc::new(Pwm {
|
||||
name,
|
||||
regs: IrqSafeSpinlock::new(regs),
|
||||
clock,
|
||||
});
|
||||
|
||||
Some(pwm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,7 +39,7 @@ impl PciInterruptMap {
|
||||
pub fn map_interrupt(
|
||||
&self,
|
||||
interrupt: &PciInterrupt,
|
||||
priority: IrqPriority,
|
||||
#[allow(unused)] priority: IrqPriority,
|
||||
) -> Result<PciInterruptRoute, Error> {
|
||||
match self {
|
||||
Self::Fixed(map) => map.get(interrupt).cloned().ok_or(Error::DoesNotExist),
|
||||
|
||||
@@ -48,6 +48,12 @@ impl From<u64> for Hertz {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Hertz> for u64 {
|
||||
fn from(value: Hertz) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Shr<u32> for Hertz {
|
||||
type Output = Hertz;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ pub mod device;
|
||||
pub mod gpio;
|
||||
pub mod i2c;
|
||||
pub mod interrupt;
|
||||
pub mod pwm;
|
||||
pub mod serial;
|
||||
pub mod spi;
|
||||
pub mod timer;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use alloc::sync::Arc;
|
||||
use yggdrasil_abi::error::Error;
|
||||
|
||||
use crate::device::Device;
|
||||
|
||||
pub enum PwmChannelMode {
|
||||
OneShot,
|
||||
Repeat,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct PwmControllerInfo {
|
||||
// If true, channel period can be configured individually,
|
||||
// if false, channel 0 is used to set the period, and cannot be used to output PWM
|
||||
pub per_channel_period: bool,
|
||||
pub channel_count: usize,
|
||||
}
|
||||
|
||||
pub struct PwmControllerDevice {
|
||||
pub device: Arc<dyn PwmController>,
|
||||
}
|
||||
|
||||
pub struct PwmChannelDevice {
|
||||
pub device: Arc<dyn PwmController>,
|
||||
pub channel: u32,
|
||||
}
|
||||
|
||||
pub trait PwmController: Device {
|
||||
fn controller_info(&self) -> Result<PwmControllerInfo, Error>;
|
||||
|
||||
fn channel_period(&self, channel: u32) -> Result<u64, Error>;
|
||||
fn set_channel_period(&self, channel: u32, period: u64) -> Result<u64, Error>;
|
||||
fn duty_cycle(&self, channel: u32) -> Result<u64, Error>;
|
||||
fn set_duty_cycle(&self, channel: u32, t: u64) -> Result<(), Error>;
|
||||
|
||||
fn stop_channel(&self, channel: u32) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
impl From<Arc<dyn PwmController>> for PwmControllerDevice {
|
||||
fn from(value: Arc<dyn PwmController>) -> Self {
|
||||
Self { device: value }
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for PwmControllerDevice {
|
||||
fn display_name(&self) -> &str {
|
||||
self.device.display_name()
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for PwmChannelDevice {
|
||||
fn display_name(&self) -> &str {
|
||||
self.device.display_name()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,13 @@ use crate::{DeviceTreePropertyRead, TProp, driver::DeviceTreeGpioPins};
|
||||
|
||||
use super::{Node, lookup_phandle};
|
||||
|
||||
pub(crate) struct InterruptIter<'dt> {
|
||||
pub(crate) interrupts: TProp<'dt>,
|
||||
pub(crate) interrupt_parent: Arc<Node>,
|
||||
pub(crate) interrupt_cells: usize,
|
||||
pub(crate) offset: usize,
|
||||
}
|
||||
|
||||
pub(crate) struct ClockIter<'dt> {
|
||||
pub(crate) clocks: TProp<'dt>,
|
||||
pub(crate) offset: usize,
|
||||
@@ -27,6 +34,19 @@ pub(crate) struct GpioIter<'o, 'dt> {
|
||||
pub(crate) offset: usize,
|
||||
}
|
||||
|
||||
impl Iterator for InterruptIter<'_> {
|
||||
type Item = IrqHandle;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.offset >= self.interrupts.len() {
|
||||
return None;
|
||||
}
|
||||
let irq = map_interrupt_at(&self.interrupt_parent, &self.interrupts, self.offset)?;
|
||||
self.offset += self.interrupt_cells;
|
||||
Some(irq)
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for ClockIter<'_> {
|
||||
type Item = ClockHandle;
|
||||
|
||||
|
||||
@@ -22,7 +22,9 @@ use yggdrasil_abi::error::Error;
|
||||
use crate::{
|
||||
DeviceTree, DeviceTreeNodeExt, DeviceTreePropertyRead, TNode, TProp,
|
||||
driver::{
|
||||
DeviceTreeGpioPins, DeviceTreeSyscon, controller::GpioIter, traits::DeviceTreePinController,
|
||||
DeviceTreeGpioPins, DeviceTreeSyscon,
|
||||
controller::{GpioIter, InterruptIter},
|
||||
traits::DeviceTreePinController,
|
||||
},
|
||||
tree,
|
||||
};
|
||||
@@ -402,6 +404,19 @@ impl Node {
|
||||
.and_then(|i| self.reset(i))
|
||||
}
|
||||
|
||||
/// Returns an iterator over the node's interrupts
|
||||
pub fn interrupts(&self) -> Option<impl Iterator<Item = IrqHandle>> {
|
||||
let interrupts = self.property("interrupts")?;
|
||||
let interrupt_parent = self.interrupt_controller()?;
|
||||
let interrupt_cells = interrupt_parent.self_interrupt_cells()?;
|
||||
Some(InterruptIter {
|
||||
interrupts,
|
||||
interrupt_parent,
|
||||
interrupt_cells,
|
||||
offset: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns an iterator over the node's input clocks
|
||||
pub fn clocks(&self) -> Option<impl Iterator<Item = ClockHandle>> {
|
||||
let clocks = self.property("clocks")?;
|
||||
|
||||
@@ -8,6 +8,7 @@ use yggdrasil_abi::{error::Error, process::ProcessId};
|
||||
use crate::vfs::{CommonImpl, FileReadiness, NodeRef};
|
||||
|
||||
pub mod i2c;
|
||||
pub mod pwm;
|
||||
pub mod spi;
|
||||
|
||||
#[async_trait]
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
use core::task::{Context, Poll};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use async_trait::async_trait;
|
||||
use device_api::pwm::{PwmChannelDevice, PwmControllerDevice};
|
||||
use yggdrasil_abi::error::Error;
|
||||
|
||||
use crate::{device::char::CharDevice, vfs::FileReadiness};
|
||||
|
||||
// TODO PWM controller operations
|
||||
#[async_trait]
|
||||
impl CharDevice for PwmControllerDevice {
|
||||
async fn read(&self, buffer: &mut [u8]) -> Result<usize, Error> {
|
||||
let _ = buffer;
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
async fn write(&self, buffer: &[u8]) -> Result<usize, Error> {
|
||||
let _ = buffer;
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
fn read_nonblocking(&self, buffer: &mut [u8]) -> Result<usize, Error> {
|
||||
let _ = buffer;
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
fn write_nonblocking(&self, buffer: &[u8]) -> Result<usize, Error> {
|
||||
let _ = buffer;
|
||||
Err(Error::InvalidOperation)
|
||||
}
|
||||
fn device_request(&self, option: u32, buffer: &mut [u8], len: usize) -> Result<usize, Error> {
|
||||
let _ = (option, buffer, len);
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileReadiness for PwmControllerDevice {
|
||||
fn poll_read(&self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
let _ = cx;
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl CharDevice for PwmChannelDevice {
|
||||
async fn read(&self, buffer: &mut [u8]) -> Result<usize, Error> {
|
||||
self.read_nonblocking(buffer)
|
||||
}
|
||||
async fn write(&self, buffer: &[u8]) -> Result<usize, Error> {
|
||||
self.write_nonblocking(buffer)
|
||||
}
|
||||
fn read_nonblocking(&self, buffer: &mut [u8]) -> Result<usize, Error> {
|
||||
if buffer.len() < 8 {
|
||||
return Err(Error::BufferTooSmall);
|
||||
}
|
||||
let mut len = 8;
|
||||
let duty_cycle = self.device.duty_cycle(self.channel)?;
|
||||
buffer[0..8].copy_from_slice(&u64::to_ne_bytes(duty_cycle));
|
||||
if buffer.len() >= 16 {
|
||||
let period = self.device.channel_period(self.channel)?;
|
||||
buffer[8..16].copy_from_slice(&u64::to_ne_bytes(period));
|
||||
len += 8;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
fn write_nonblocking(&self, buffer: &[u8]) -> Result<usize, Error> {
|
||||
if buffer.len() < 8 {
|
||||
return Err(Error::InvalidArgument);
|
||||
}
|
||||
let mut len = 8;
|
||||
let duty_cycle = u64::from_ne_bytes(buffer[0..8].try_into().unwrap());
|
||||
let period = if buffer.len() >= 16 {
|
||||
len += 8;
|
||||
Some(u64::from_ne_bytes(buffer[8..16].try_into().unwrap()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(period) = period {
|
||||
self.device.set_channel_period(self.channel, period)?;
|
||||
}
|
||||
if duty_cycle != u64::MAX {
|
||||
self.device.set_duty_cycle(self.channel, duty_cycle)?;
|
||||
}
|
||||
Ok(len)
|
||||
}
|
||||
fn device_request(&self, option: u32, buffer: &mut [u8], len: usize) -> Result<usize, Error> {
|
||||
let _ = (option, buffer, len);
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl FileReadiness for PwmChannelDevice {
|
||||
fn poll_read(&self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
|
||||
let _ = cx;
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use core::{
|
||||
use alloc::{collections::BTreeMap, format, sync::Arc, vec::Vec};
|
||||
use device_api::{
|
||||
i2c::{I2CController, I2CDevice},
|
||||
pwm::{PwmChannelDevice, PwmController, PwmControllerDevice},
|
||||
spi::SpiController,
|
||||
};
|
||||
use libk_util::{OneTimeInit, sync::spin_rwlock::IrqSafeRwLock};
|
||||
@@ -61,12 +62,25 @@ pub struct SpiDeviceRegistry {
|
||||
registry: GenericRegistry<Arc<SpiControllerDevice>>,
|
||||
}
|
||||
|
||||
// Manages devices: pwm<N>, pwm<N>-<M>
|
||||
#[allow(unused)]
|
||||
struct PwmDeviceRegistryEntry {
|
||||
controller: Arc<PwmControllerDevice>,
|
||||
controller_node: Option<NodeRef>,
|
||||
channels: BTreeMap<u32, (Arc<PwmChannelDevice>, Option<NodeRef>)>,
|
||||
}
|
||||
|
||||
pub struct PwmDeviceRegistry {
|
||||
registry: GenericRegistry<PwmDeviceRegistryEntry>,
|
||||
}
|
||||
|
||||
pub struct DeviceRegistry {
|
||||
pub display: DisplayDeviceRegistry,
|
||||
pub terminal: TerminalRegistry,
|
||||
pub serial_terminal: SerialTerminalRegistry,
|
||||
pub i2c: I2CDeviceRegistry,
|
||||
pub spi: SpiDeviceRegistry,
|
||||
pub pwm: PwmDeviceRegistry,
|
||||
}
|
||||
|
||||
pub static DEVICE_REGISTRY: DeviceRegistry = DeviceRegistry {
|
||||
@@ -75,6 +89,7 @@ pub static DEVICE_REGISTRY: DeviceRegistry = DeviceRegistry {
|
||||
serial_terminal: SerialTerminalRegistry::new(),
|
||||
i2c: I2CDeviceRegistry::new(),
|
||||
spi: SpiDeviceRegistry::new(),
|
||||
pwm: PwmDeviceRegistry::new(),
|
||||
};
|
||||
|
||||
impl TerminalRegistry {
|
||||
@@ -168,6 +183,62 @@ impl SpiDeviceRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
impl PwmDeviceRegistry {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
registry: GenericRegistry::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_controller(&self, controller: Arc<dyn PwmController>) -> Result<u32, Error> {
|
||||
let controller_info = controller.controller_info()?;
|
||||
|
||||
let id = self.registry.register_with(|id| {
|
||||
let controller_device = Arc::new(PwmControllerDevice::from(controller.clone()));
|
||||
let controller_name = format!("pwm{id}");
|
||||
log::info!("Register PWM controller {controller_name:?}");
|
||||
let controller_node = devfs::add_named_char_device(
|
||||
controller_device.clone(),
|
||||
controller_name,
|
||||
FileMode::new(0o600),
|
||||
)
|
||||
.ok();
|
||||
|
||||
let start_channel = if controller_info.per_channel_period {
|
||||
0
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let mut channels = BTreeMap::new();
|
||||
for channel in start_channel..controller_info.channel_count {
|
||||
let channel_name = format!("pwm{id}-{channel}");
|
||||
log::info!("Register PWM channel {channel_name:?}");
|
||||
let channel_device = Arc::new(PwmChannelDevice {
|
||||
device: controller.clone(),
|
||||
channel: channel as u32,
|
||||
});
|
||||
let channel_node = devfs::add_named_char_device(
|
||||
channel_device.clone(),
|
||||
channel_name,
|
||||
FileMode::new(0o600),
|
||||
)
|
||||
.ok();
|
||||
channels.insert(channel as u32, (channel_device, channel_node));
|
||||
}
|
||||
|
||||
let entry = PwmDeviceRegistryEntry {
|
||||
controller: controller_device,
|
||||
controller_node,
|
||||
channels,
|
||||
};
|
||||
|
||||
entry
|
||||
})?;
|
||||
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> GenericRegistry<T> {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
@@ -180,7 +251,13 @@ impl<T> GenericRegistry<T> {
|
||||
let mut members = self.members.write();
|
||||
let id = self.last_index.fetch_add(1, Ordering::Release);
|
||||
members.insert(id, member);
|
||||
Ok(id)
|
||||
}
|
||||
|
||||
pub fn register_with<F: FnOnce(u32) -> T>(&self, mapper: F) -> Result<u32, Error> {
|
||||
let mut members = self.members.write();
|
||||
let id = self.last_index.fetch_add(1, Ordering::Release);
|
||||
members.insert(id, (mapper)(id));
|
||||
Ok(id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn add_named_char_device<S: AsRef<str>>(
|
||||
dev: Arc<dyn CharDevice>,
|
||||
name: S,
|
||||
mode: FileMode,
|
||||
) -> Result<(), Error> {
|
||||
) -> Result<NodeRef, Error> {
|
||||
let name = name.as_ref();
|
||||
log::info!("Add char device: {}", name);
|
||||
|
||||
@@ -65,7 +65,8 @@ pub fn add_named_char_device<S: AsRef<str>>(
|
||||
);
|
||||
|
||||
let filename = OwnedFilename::new(name)?;
|
||||
DEVFS_ROOT.get().add_child(filename, node)
|
||||
DEVFS_ROOT.get().add_child(filename, node.clone())?;
|
||||
Ok(node)
|
||||
}
|
||||
|
||||
/// Adds a block device with a custom name
|
||||
|
||||
@@ -182,6 +182,10 @@ path = "src/top.rs"
|
||||
name = "serial"
|
||||
path = "src/serial.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pwm"
|
||||
path = "src/pwm.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "tst"
|
||||
path = "src/tst.rs"
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{self, Read},
|
||||
path::PathBuf,
|
||||
process::ExitCode,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
#[clap(subcommand)]
|
||||
action: Action,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Action {
|
||||
List,
|
||||
Get {
|
||||
chip: u32,
|
||||
channel: u32,
|
||||
},
|
||||
Set {
|
||||
chip: u32,
|
||||
channel: u32,
|
||||
duty: Nanoseconds,
|
||||
period: Option<Nanoseconds>,
|
||||
},
|
||||
}
|
||||
|
||||
struct Chip {
|
||||
id: u32,
|
||||
channels: Vec<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Nanoseconds(u64);
|
||||
|
||||
impl From<u64> for Nanoseconds {
|
||||
fn from(value: u64) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Nanoseconds> for u64 {
|
||||
fn from(value: Nanoseconds) -> Self {
|
||||
value.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Nanoseconds {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let (scale, value) = if let Some(value) = s.strip_suffix("ns") {
|
||||
(1, value)
|
||||
} else if let Some(value) = s.strip_suffix("us") {
|
||||
(1000, value)
|
||||
} else if let Some(value) = s.strip_suffix("ms") {
|
||||
(1000000, value)
|
||||
} else if let Some(value) = s.strip_suffix("s") {
|
||||
(1000000000, value)
|
||||
} else {
|
||||
(1, s)
|
||||
};
|
||||
let value = value
|
||||
.parse::<u64>()
|
||||
.map_err(|_| format!("Invalid value: {value}"))?;
|
||||
let value = value * scale;
|
||||
Ok(Self(value))
|
||||
}
|
||||
}
|
||||
|
||||
fn channel_path(chip: u32, channel: u32) -> PathBuf {
|
||||
PathBuf::from(format!("/dev/pwm{chip}-{channel}"))
|
||||
}
|
||||
|
||||
fn add_channel(list: &mut Vec<Chip>, chip: u32, channel: Option<u32>) {
|
||||
let chip_index = list.iter().position(|p| p.id == chip).unwrap_or_else(|| {
|
||||
list.push(Chip {
|
||||
id: chip,
|
||||
channels: vec![],
|
||||
});
|
||||
list.len() - 1
|
||||
});
|
||||
|
||||
if let Some(channel) = channel {
|
||||
let chip = &mut list[chip_index];
|
||||
if !chip.channels.contains(&channel) {
|
||||
chip.channels.push(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_chips() -> io::Result<()> {
|
||||
let mut entries = vec![];
|
||||
for entry in fs::read_dir("/dev")? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let name = name.to_str().unwrap();
|
||||
let Some(name) = name.strip_prefix("pwm") else {
|
||||
continue;
|
||||
};
|
||||
if let Some((chip_id, channel_id)) = name.split_once('-') {
|
||||
let Ok(chip) = chip_id.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
let Ok(channel) = channel_id.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
add_channel(&mut entries, chip, Some(channel));
|
||||
} else {
|
||||
let Ok(chip) = name.parse::<u32>() else {
|
||||
continue;
|
||||
};
|
||||
add_channel(&mut entries, chip, None);
|
||||
}
|
||||
}
|
||||
for entry in entries.iter_mut() {
|
||||
entry.channels.sort();
|
||||
}
|
||||
entries.sort_by_key(|p| p.id);
|
||||
|
||||
for entry in entries {
|
||||
print!("pwm{}: ", entry.id);
|
||||
for (i, channel) in entry.channels.iter().enumerate() {
|
||||
if i != 0 {
|
||||
print!(",");
|
||||
}
|
||||
print!("{channel}");
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get(chip: u32, channel: u32) -> io::Result<()> {
|
||||
let path = channel_path(chip, channel);
|
||||
let mut buffer = [0; 16];
|
||||
let len = File::open(path)?.read(&mut buffer)?;
|
||||
let data = &buffer[..len];
|
||||
if data.len() >= 8 {
|
||||
let duty = u64::from_ne_bytes(data[0..8].try_into().unwrap());
|
||||
println!("duty={duty}");
|
||||
}
|
||||
if data.len() >= 16 {
|
||||
let period = u64::from_ne_bytes(data[8..16].try_into().unwrap());
|
||||
println!("period={period}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set(chip: u32, channel: u32, duty: Nanoseconds, period: Option<Nanoseconds>) -> io::Result<()> {
|
||||
let path = channel_path(chip, channel);
|
||||
let mut buffer = [0; 16];
|
||||
buffer[0..8].copy_from_slice(&duty.0.to_ne_bytes());
|
||||
let mut len = 8;
|
||||
if let Some(period) = period {
|
||||
buffer[8..16].copy_from_slice(&period.0.to_ne_bytes());
|
||||
len += 8;
|
||||
}
|
||||
fs::write(path, &buffer[..len])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run(args: Args) -> io::Result<()> {
|
||||
match args.action {
|
||||
Action::List => list_chips(),
|
||||
Action::Set {
|
||||
chip,
|
||||
channel,
|
||||
duty,
|
||||
period,
|
||||
} => set(chip, channel, duty, period),
|
||||
Action::Get { chip, channel } => get(chip, channel),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
match run(args) {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ const PROGRAMS: &[(&str, &str)] = &[
|
||||
("mv", "bin/mv"),
|
||||
("poweroff", "sbin/poweroff"),
|
||||
("ps", "bin/ps"),
|
||||
("pwm", "bin/pwm"),
|
||||
("random", "bin/random"),
|
||||
("reboot", "sbin/reboot"),
|
||||
("rm", "bin/rm"),
|
||||
|
||||
Reference in New Issue
Block a user