Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Last updated: Wednesday 29 April 2026 @ 11:05:00

PWM

Here we are going to continue working on the Rust crate bbb_io.

In the earlier C implementation, PWM was controlled through the Linux sysfs PWM interface:

/sys/class/pwm/pwmchip0/pwm0/period
/sys/class/pwm/pwmchip0/pwm0/duty_cycle
/sys/class/pwm/pwmchip0/pwm0/enable

The Rust implementation in this chapter follows the same design:

Physical pin name -> PWM chip + channel -> sysfs paths -> Linux kernel -> PWM output

For example:

P9_14 -> pwmchip0, pwm0
P9_16 -> pwmchip0, pwm1

This PWM chapter uses sysfs, not libgpiod.

The earlier sysfs.rs module is useful here because PWM control is still performed by writing values into files.

Warning

1. Creating the pwm.rs

The first step is to create the PWM module. This is the Rust equivalent of the bbb_pwm.c and bbb_pwm.h files from the C library.

  1. Open src/pwm.rs and add the following:

    Code: pwm.rs

    use std::path::PathBuf;
    use std::thread;
    use std::time::Duration;
    
    use crate::{sysfs, BbbError, BbbResult};
    
    const PWM_BASE_PATH: &str = "/sys/class/pwm";
    
    const PWM_PERIOD_DEFAULT: u32 = 10_000_000;
    const PWM_DUTY_CYCLE_DEFAULT: u32 = 0;
    const PWM_ENABLE_DEFAULT: u8 = 0;
    
    const MAX_ADC: u32 = 4095;
    
    #[derive(Debug, Clone, Copy)]
    pub struct PwmPin {
        pub physical_pin: &'static str,
        pub chip: u8,
        pub channel: u8,
    }
    
    const PWM_PIN_MAP: &[PwmPin] = &[
        PwmPin {
            physical_pin: "P9_14",
            chip: 0,
            channel: 0,
        },
        PwmPin {
            physical_pin: "P9_16",
            chip: 0,
            channel: 1,
        },
    ];
    
    #[derive(Debug)]
    pub struct Pwm {
        pub physical_pin: &'static str,
        pub chip: u8,
        pub channel: u8,
        period_path: PathBuf,
        duty_cycle_path: PathBuf,
        enable_path: PathBuf,
    }
    
    fn lookup_pwm_pin(pin_name: &str) -> BbbResult<PwmPin> {
        PWM_PIN_MAP
            .iter()
            .copied()
            .find(|entry| entry.physical_pin == pin_name)
            .ok_or_else(|| BbbError::InvalidPin(pin_name.to_string()))
    }
    
    fn pwmchip_path(chip: u8) -> PathBuf {
        PathBuf::from(format!("{PWM_BASE_PATH}/pwmchip{chip}"))
    }
    
    fn pwm_path(chip: u8, channel: u8) -> PathBuf {
        pwmchip_path(chip).join(format!("pwm{channel}"))
    }
    
    fn ensure_exported(chip: u8, channel: u8) -> BbbResult<()> {
        let path = pwm_path(chip, channel);
    
        if path.exists() {
            return Ok(());
        }
    
        let export_path = pwmchip_path(chip).join("export");
        sysfs::write_u32(export_path, channel as u32)?;
    
        thread::sleep(Duration::from_millis(100));
    
        Ok(())
    }
    
    impl Pwm {
        pub fn init(pin_name: &str) -> BbbResult<Self> {
            let pin = lookup_pwm_pin(pin_name)?;
    
            ensure_exported(pin.chip, pin.channel)?;
    
            let base = pwm_path(pin.chip, pin.channel);
    
            Ok(Self {
                physical_pin: pin.physical_pin,
                chip: pin.chip,
                channel: pin.channel,
                period_path: base.join("period"),
                duty_cycle_path: base.join("duty_cycle"),
                enable_path: base.join("enable"),
            })
        }
    
        pub fn cleanup(&self) -> BbbResult<()> {
            self.set_period(PWM_PERIOD_DEFAULT)?;
            self.set_duty_cycle(PWM_DUTY_CYCLE_DEFAULT)?;
    
            if PWM_ENABLE_DEFAULT == 1 {
                self.enable()
            } else {
                self.disable()
            }
        }
    
        pub fn set_period(&self, period_ns: u32) -> BbbResult<()> {
            sysfs::write_u32(&self.period_path, period_ns)
        }
    
        pub fn set_duty_cycle(&self, duty_cycle_ns: u32) -> BbbResult<()> {
            sysfs::write_u32(&self.duty_cycle_path, duty_cycle_ns)
        }
    
        pub fn enable(&self) -> BbbResult<()> {
            sysfs::write_string(&self.enable_path, "1")
        }
    
        pub fn disable(&self) -> BbbResult<()> {
            sysfs::write_string(&self.enable_path, "0")
        }
    }
    
    impl Drop for Pwm {
        fn drop(&mut self) {
            let _ = self.cleanup();
        }
    }
    
    pub fn map_adc_to_duty_cycle(adc_value: u16, period_ns: u32) -> BbbResult<u32> {
        let adc_value = adc_value as u32;
    
        if adc_value > MAX_ADC {
            return Err(BbbError::InvalidValue(format!(
                "ADC value out of range: {adc_value}"
            )));
        }
    
        Ok((adc_value * period_ns) / MAX_ADC)
    }
    

    This file gives the library a single PWM abstraction for the physical pins that support PWM.

    Instead of writing:

    /sys/class/pwm/pwmchip0/pwm0/period
    

    we can write:

    let pwm = Pwm::init("P9_14")?;
    

    Explanation

    Imports

    use std::path::PathBuf;
    use std::thread;
    use std::time::Duration;
    
    • PathBuf

      • used to build sysfs file paths safely
    • thread

      • used to briefly pause after exporting a PWM channel
    • Duration

      • used to express time clearly

    Read more:


    Reusing crate modules

    use crate::{sysfs, BbbError, BbbResult};
    
    • sysfs

      • contains helper functions for writing strings and integers to sysfs files
    • BbbError

      • our crate-specific error type
    • BbbResult

      • shorthand for:

        Result<T, BbbError>
        

    This keeps pwm.rs consistent with adc.rs.


    Constants

    const PWM_BASE_PATH: &str = "/sys/class/pwm";
    

    This is the base directory for Linux PWM sysfs.

    PWM channels are exposed under paths such as:

    /sys/class/pwm/pwmchip0/pwm0
    

    Default PWM values

    const PWM_PERIOD_DEFAULT: u32 = 10_000_000;
    const PWM_DUTY_CYCLE_DEFAULT: u32 = 0;
    const PWM_ENABLE_DEFAULT: u8 = 0;
    

    These mirror the C defaults:

    #define PWM_PERIOD_DEFAULT 10000000
    #define PWM_DUTY_CYCLE_DEFAULT 0
    #define PWM_ENABLE_DEFAULT 0
    

    They are used during cleanup so the PWM output is left in a predictable state.


    Why underscores in numbers?

    Rust allows numeric separators:

    10_000_000
    

    This is the same value as:

    10000000
    

    but easier to read.


    The PwmPin struct

    pub struct PwmPin {
        pub physical_pin: &'static str,
        pub chip: u8,
        pub channel: u8,
    }
    

    This is the Rust equivalent of the C mapping:

    typedef struct {
        char physical_pin[10];
        char pwm_chip_channel[10];
    } PinMap;
    

    The Rust version stores the chip and channel separately rather than parsing a string such as "0:0".

    • physical_pin

      • BeagleBone header pin, for example "P9_14"
    • chip

      • PWM chip number, for example 0 for pwmchip0
    • channel

      • PWM channel, for example 0 for pwm0

    So:

    PwmPin { physical_pin: "P9_14", chip: 0, channel: 0 }
    

    means:

    P9_14 -> /sys/class/pwm/pwmchip0/pwm0
    

    Why &'static str?

    pub physical_pin: &'static str
    

    The pin names are fixed strings compiled into the program.

    'static means:

    this string lives for the entire lifetime of the program

    Read more:


    Why #[derive(Debug, Clone, Copy)]?

    #[derive(Debug, Clone, Copy)]
    

    PwmPin contains only a string reference and small integers, so it is cheap to copy.

    • Debug

      • allows developer-style printing with {:?}
    • Clone

      • allows explicit copying
    • Copy

      • allows automatic copying

    Read more:


    The PWM pin table

    const PWM_PIN_MAP: &[PwmPin] = &[ ... ];
    

    This is equivalent in purpose to the C array:

    PinMap phy_pin_map[] = {
        {"P9_14", "0:0"},
        {"P9_16", "0:1"},
    };
    

    However, the Rust version avoids parsing "0:0" and stores the data directly as typed values.


    The Pwm struct

    pub struct Pwm {
        pub physical_pin: &'static str,
        pub chip: u8,
        pub channel: u8,
        period_path: PathBuf,
        duty_cycle_path: PathBuf,
        enable_path: PathBuf,
    }
    

    This is equivalent to the C PWM struct:

    typedef struct {
        char phy_pin[10];
        char chip[4];
        char channel[4];
        char period_path[128];
        char duty_cycle_path[128];
        char enable_path[128];
    } PWM;
    

    The important difference is that Rust stores file paths as PathBuf.

    This avoids fixed-size buffers such as:

    char period_path[128];
    

    and removes the risk of writing beyond the end of a buffer.


    Public and private fields

    pub physical_pin: &'static str,
    pub chip: u8,
    pub channel: u8,
    period_path: PathBuf,
    duty_cycle_path: PathBuf,
    enable_path: PathBuf,
    
    • physical_pin, chip, and channel are public

      • useful for printing and debugging
    • period_path, duty_cycle_path, and enable_path are private

      • user code should not write directly to these
      • user code should call set_period, set_duty_cycle, enable, and disable

    This gives us encapsulation.

    Read more:

2. PWM pin lookup and path construction

The next part of pwm.rs performs the same role as the pin parsing code in the C implementation.

Code

fn lookup_pwm_pin(pin_name: &str) -> BbbResult<PwmPin> {
    PWM_PIN_MAP
        .iter()
        .copied()
        .find(|entry| entry.physical_pin == pin_name)
        .ok_or_else(|| BbbError::InvalidPin(pin_name.to_string()))
}

fn pwmchip_path(chip: u8) -> PathBuf {
    PathBuf::from(format!("{PWM_BASE_PATH}/pwmchip{chip}"))
}

fn pwm_path(chip: u8, channel: u8) -> PathBuf {
    pwmchip_path(chip).join(format!("pwm{channel}"))
}

Explanation

lookup_pwm_pin

fn lookup_pwm_pin(pin_name: &str) -> BbbResult<PwmPin>

This function searches the PWM pin table for a physical pin name.

Example:

let pin = lookup_pwm_pin("P9_14")?;

If the pin exists, it returns:

Ok(PwmPin { physical_pin: "P9_14", chip: 0, channel: 0 })

If the pin is unknown, it returns:

Err(BbbError::InvalidPin(...))

Iterator chain

PWM_PIN_MAP
    .iter()
    .copied()
    .find(|entry| entry.physical_pin == pin_name)

This means:

  1. .iter()

    • visit each PWM pin entry
  2. .copied()

    • copy the PwmPin value from the table
  3. .find(...)

    • return the first matching entry

Read more:


ok_or_else

.ok_or_else(|| BbbError::InvalidPin(pin_name.to_string()))

find(...) returns an Option:

Some(pin)
None

But our function returns a Result:

Ok(pin)
Err(error)

ok_or_else(...) converts:

Some(value) -> Ok(value)
None        -> Err(...)

Read more:


Building paths

fn pwmchip_path(chip: u8) -> PathBuf {
    PathBuf::from(format!("{PWM_BASE_PATH}/pwmchip{chip}"))
}

For chip 0, this creates:

/sys/class/pwm/pwmchip0

Then:

fn pwm_path(chip: u8, channel: u8) -> PathBuf {
    pwmchip_path(chip).join(format!("pwm{channel}"))
}

For chip 0 and channel 0, this creates:

/sys/class/pwm/pwmchip0/pwm0

This replaces the C snprintf approach with safer path construction.

3. Exporting the PWM channel

Before we can write to:

/sys/class/pwm/pwmchip0/pwm0/period

the channel must exist. If it does not exist, it must be exported.

Code

fn ensure_exported(chip: u8, channel: u8) -> BbbResult<()> {
    let path = pwm_path(chip, channel);

    if path.exists() {
        return Ok(());
    }

    let export_path = pwmchip_path(chip).join("export");
    sysfs::write_u32(export_path, channel as u32)?;

    thread::sleep(Duration::from_millis(100));

    Ok(())
}

Explanation

What does export mean?

In sysfs PWM, a PWM channel may not appear until it is exported.

For example, if this path does not exist:

/sys/class/pwm/pwmchip0/pwm0

you can create it by writing the channel number to:

/sys/class/pwm/pwmchip0/export

Equivalent terminal command:

echo 0 > /sys/class/pwm/pwmchip0/export

Checking whether the channel exists

if path.exists() {
    return Ok(());
}

If the channel is already exported, we do nothing.


Exporting the channel

let export_path = pwmchip_path(chip).join("export");
sysfs::write_u32(export_path, channel as u32)?;

This writes the channel number to the export file.

The ? operator means:

if writing fails, return the error immediately


Why sleep?

thread::sleep(Duration::from_millis(100));

After export, the kernel may take a short time to create:

/sys/class/pwm/pwmchip0/pwm0

The short delay gives the sysfs tree time to appear.

4. Implementing the PWM methods

The impl Pwm block contains the methods that correspond to the C library functions.

Code

impl Pwm {
    pub fn init(pin_name: &str) -> BbbResult<Self> {
        let pin = lookup_pwm_pin(pin_name)?;

        ensure_exported(pin.chip, pin.channel)?;

        let base = pwm_path(pin.chip, pin.channel);

        Ok(Self {
            physical_pin: pin.physical_pin,
            chip: pin.chip,
            channel: pin.channel,
            period_path: base.join("period"),
            duty_cycle_path: base.join("duty_cycle"),
            enable_path: base.join("enable"),
        })
    }

    pub fn cleanup(&self) -> BbbResult<()> {
        self.set_period(PWM_PERIOD_DEFAULT)?;
        self.set_duty_cycle(PWM_DUTY_CYCLE_DEFAULT)?;

        if PWM_ENABLE_DEFAULT == 1 {
            self.enable()
        } else {
            self.disable()
        }
    }

    pub fn set_period(&self, period_ns: u32) -> BbbResult<()> {
        sysfs::write_u32(&self.period_path, period_ns)
    }

    pub fn set_duty_cycle(&self, duty_cycle_ns: u32) -> BbbResult<()> {
        sysfs::write_u32(&self.duty_cycle_path, duty_cycle_ns)
    }

    pub fn enable(&self) -> BbbResult<()> {
        sysfs::write_string(&self.enable_path, "1")
    }

    pub fn disable(&self) -> BbbResult<()> {
        sysfs::write_string(&self.enable_path, "0")
    }
}

Explanation

Pwm::init

pub fn init(pin_name: &str) -> BbbResult<Self>

This is the Rust equivalent of:

int pwm_init(PWM *pwm, const char *pin_name)

In C, the caller creates a PWM struct and passes a pointer into pwm_init.

In Rust, Pwm::init creates and returns the Pwm value:

let pwm = Pwm::init("P9_14")?;

This is more natural in Rust because ownership of the PWM handle is returned to the caller.


Setup sequence

let pin = lookup_pwm_pin(pin_name)?;
ensure_exported(pin.chip, pin.channel)?;
let base = pwm_path(pin.chip, pin.channel);

This performs:

pin name -> lookup chip/channel -> export if needed -> build paths

Returning Self

Ok(Self {
    physical_pin: pin.physical_pin,
    chip: pin.chip,
    channel: pin.channel,
    period_path: base.join("period"),
    duty_cycle_path: base.join("duty_cycle"),
    enable_path: base.join("enable"),
})

Self means:

the type currently being implemented

Here, Self means Pwm.

Read more:


cleanup

pub fn cleanup(&self) -> BbbResult<()>

This mirrors the C function:

int pwm_cleanup(PWM *pwm)

It restores default values:

period     -> 10,000,000 ns
duty cycle -> 0
enable     -> 0

This leaves the PWM output in a safe state.


Setting the period

pub fn set_period(&self, period_ns: u32) -> BbbResult<()> {
    sysfs::write_u32(&self.period_path, period_ns)
}

This writes a number to:

/sys/class/pwm/pwmchip0/pwm0/period

For example:

pwm.set_period(1_000_000)?;

means:

set period to 1,000,000 ns

which is 1 ms, or 1 kHz.


Setting the duty cycle

pub fn set_duty_cycle(&self, duty_cycle_ns: u32) -> BbbResult<()> {
    sysfs::write_u32(&self.duty_cycle_path, duty_cycle_ns)
}

This writes to:

/sys/class/pwm/pwmchip0/pwm0/duty_cycle

The duty cycle must be less than or equal to the period.

For example:

pwm.set_period(1_000_000)?;
pwm.set_duty_cycle(500_000)?;

This gives a 50% duty cycle.


Enabling and disabling

pub fn enable(&self) -> BbbResult<()> {
    sysfs::write_string(&self.enable_path, "1")
}

and:

pub fn disable(&self) -> BbbResult<()> {
    sysfs::write_string(&self.enable_path, "0")
}

These are equivalent to:

echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable
echo 0 > /sys/class/pwm/pwmchip0/pwm0/enable

5. Drop and automatic cleanup

Rust allows us to automatically clean up the PWM output when the Pwm value goes out of scope.

Code

impl Drop for Pwm {
    fn drop(&mut self) {
        let _ = self.cleanup();
    }
}

Explanation

What is Drop?

Drop is Rust’s cleanup mechanism.

When a value goes out of scope, Rust automatically calls its drop method.

Read more:


C vs Rust cleanup

In C, cleanup must be called manually:

pwm_disable(&pwm);
pwm_cleanup(&pwm);

In Rust, cleanup can be attached to the value itself:

impl Drop for Pwm {
    fn drop(&mut self) {
        let _ = self.cleanup();
    }
}

This means:

when pwm goes out of scope -> cleanup runs automatically

Why let _ = ...?

Drop::drop cannot return a Result.

So this line:

let _ = self.cleanup();

means:

try to clean up, but ignore any cleanup error

This is common in destructors and cleanup code.


RAII

This is the same pattern used in the GPIO chapter.

Resource Acquisition Is Initialisation

Meaning:

create object -> acquire/configure resource
object ends   -> release/reset resource

6. Mapping ADC values to PWM duty cycle

The C library included:

unsigned int map(u_int16_t adc_value, unsigned int period_ns);

The Rust equivalent is:

Code

pub fn map_adc_to_duty_cycle(adc_value: u16, period_ns: u32) -> BbbResult<u32> {
    let adc_value = adc_value as u32;

    if adc_value > MAX_ADC {
        return Err(BbbError::InvalidValue(format!(
            "ADC value out of range: {adc_value}"
        )));
    }

    Ok((adc_value * period_ns) / MAX_ADC)
}

Explanation

Purpose

This function maps an ADC reading into a PWM duty cycle.

ADC range: 0..4095
PWM range: 0..period_ns

So if:

adc_value = 2048
period_ns = 1,000,000

then the duty cycle will be approximately:

500,000 ns

or 50%.


Why return BbbResult<u32>?

The C version returns 1 if the ADC value is out of range.

The Rust version returns a proper error:

Err(BbbError::InvalidValue(...))

This makes failure explicit and consistent with the rest of the crate.


Why convert u16 to u32?

let adc_value = adc_value as u32;

The multiplication is done using u32 so that:

adc_value * period_ns

has enough range for typical period values.

Read more:

7. Build

Run:

Terminal

cargo clean && cargo build

If successful, you should see a compiled library under:

target/debug/libbbb_io.rlib

The libbbb_io.rlib is the Rust library that our standalone scripts can link against.

This is similar in purpose to a C static library such as:

libioctrl.a

8. Example: pwm_test.rs

Now we can make the Rust equivalent of the earlier pwm_test.c.

  1. Create an examples directory if it does not already exist:

    Terminal

    mkdir -p examples
    
  2. Create examples/pwm_test.rs:

    Code: examples/pwm_test.rs

    use std::thread;
    use std::time::Duration;
    
    use bbb_io::pwm::Pwm;
    
    fn main() -> bbb_io::BbbResult<()> {
        let pwm = Pwm::init("P9_14")?;
    
        let period = 1_000_000; // 1 ms = 1 kHz
    
        println!("Phy: {}", pwm.physical_pin);
        println!("Channel: {}", pwm.channel);
        println!("Chip: {}", pwm.chip);
    
        pwm.set_period(period)?;
        pwm.set_duty_cycle(1_000_000)?;
        pwm.enable()?;
    
        thread::sleep(Duration::from_secs(2));
        pwm.set_duty_cycle(500_000)?;
    
        thread::sleep(Duration::from_secs(2));
        pwm.set_duty_cycle(200_000)?;
    
        thread::sleep(Duration::from_secs(2));
        pwm.set_duty_cycle(100_000)?;
    
        pwm.disable()?;
        pwm.cleanup()?;
    
        Ok(())
    }
    

    Explanation

    Imports

    use std::thread;
    use std::time::Duration;
    

    These are used to pause the program between duty-cycle changes.

    use bbb_io::pwm::Pwm;
    

    This imports the Pwm type from our crate.


    Creating the PWM handle

    let pwm = Pwm::init("P9_14")?;
    

    This is the Rust equivalent of:

    PWM pwm;
    pwm_init(&pwm, "P9_14");
    

    It performs:

    lookup P9_14 -> export pwm0 if needed -> build sysfs paths -> return Pwm handle
    

    Period

    let period = 1_000_000;
    

    This sets the PWM period to 1,000,000 ns.

    1,000,000 ns = 1 ms = 1 kHz
    

    Printing debug information

    println!("Phy: {}", pwm.physical_pin);
    println!("Channel: {}", pwm.channel);
    println!("Chip: {}", pwm.chip);
    

    This is equivalent to printing the C struct fields.

    Unlike the C version, the file paths are private fields, so they are not printed directly here.


    Setting PWM parameters

    pwm.set_period(period)?;
    pwm.set_duty_cycle(1_000_000)?;
    pwm.enable()?;
    

    This configures and enables the PWM output.

    Note:

    pwm.set_duty_cycle(1_000_000)?;
    

    with:

    period = 1_000_000
    

    gives 100% duty cycle.

    In the original C example, the comment said this was 50%, but that comment was incorrect.

    For 50% duty cycle, use:

    pwm.set_duty_cycle(500_000)?;
    

    Changing duty cycle over time

    thread::sleep(Duration::from_secs(2));
    pwm.set_duty_cycle(500_000)?;
    

    This waits for two seconds, then changes the duty cycle.

    The example steps through:

    100% -> 50% -> 20% -> 10%
    

    Cleanup

    pwm.disable()?;
    pwm.cleanup()?;
    

    This manually disables and resets the PWM output.

    Even if this were omitted, Drop would attempt cleanup automatically when pwm goes out of scope.

    Explicit cleanup is included here so students can clearly see the lifecycle.

  3. Compile the example:

    Terminal

    rustc examples/pwm_test.rs \
    --edition=2021 \
    -L target/debug \
    --extern bbb_io=target/debug/libbbb_io.rlib \
    -o pwm_test
    

    Explanation

    • rustc examples/pwm_test.rs

      • Compile this standalone Rust program.
    • --edition=2021

      • Selects the Rust language edition.

      Read more:

    • -L target/debug

      • Tells rustc where to look for compiled libraries.
    • --extern bbb_io=target/debug/libbbb_io.rlib

      • Links the program against the compiled bbb_io crate.

      • This is why the program can write:

        use bbb_io::pwm::Pwm;
        
    • -o pwm_test

      • Names the output binary:

        ./pwm_test
        
    • We do not use -l gpiod here.

      GPIO needed -l gpiod because it used a C library through FFI.

      PWM uses sysfs file writes, so it only needs the Rust crate.

  4. Run the example:

    Terminal

    sudo ./pwm_test
    

    You may need sudo because writing to /sys/class/pwm/... often requires elevated permissions.

9. Circuit

Wire the PWM output to an LED or appropriate load.

For a basic LED test:

P9_14  -> resistor -> LED anode
LED cathode -> GND

Use a suitable resistor, for example:

220Ω to 1kΩ

Warning

Do not drive high-current loads directly from a BeagleBone pin.

For motors, relays, or LED strips, use a transistor, MOSFET, motor driver, or appropriate interface circuit.

10. Expected behaviour

If everything is working, the output should step through different duty cycles:

100% -> 50% -> 20% -> 10% -> off

For an LED, this should appear as different brightness levels.

For a scope or logic analyser, you should see the high-time within each PWM period change.

Success

If you got here, then your Rust crate can now control PWM through sysfs.


12. Summary

This chapter translated the C PWM library into Rust.

The mapping is:

C function              Rust method/function
------------------------------------------------
pwm_init                Pwm::init
pwm_cleanup             Pwm::cleanup / Drop
pwm_set_period          Pwm::set_period
pwm_set_duty_cycle      Pwm::set_duty_cycle
pwm_enable              Pwm::enable
pwm_disable             Pwm::disable
map                     map_adc_to_duty_cycle

The larger pattern is:

Physical pin name
    v
PWM pin map
    v
sysfs paths
    v
Linux kernel PWM subsystem
    v
PWM output

PWM is therefore a good contrast with GPIO:

GPIO -> libgpiod / /dev/gpiochipN
PWM  -> sysfs / /sys/class/pwm