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

ADC

Warning

If you have already completed the C_Library/ADC Library, the introductory material is omitted here and the focus will specifically be on the bbb_io crate implementation for ADC.

If not, it is strongly recommended that you work through that chapter first.


1. The ADC module

  1. Create or update ~/bbb_io/src/adc.rs.

    Code: adc.rs [190 lines]

    use std::fs::File;
    use std::io::Read;
    use std::path::PathBuf;
    
    use crate::{sysfs, BbbError, BbbResult};
    
    const SYSFS_ADC_DIR: &str = "/sys/bus/iio/devices/iio:device0";
    const SYSFS_ADC_ENABLE_PATH: &str = "/sys/bus/iio/devices/iio:device0/buffer/enable";
    const SYSFS_ADC_LENGTH_PATH: &str = "/sys/bus/iio/devices/iio:device0/buffer/length";
    const SYSFS_ADC_SCAN_DIR: &str = "/sys/bus/iio/devices/iio:device0/scan_elements";
    
    const IIO_DEVICE_PATH: &str = "/dev/iio:device0";
    
    const REF_VOLTAGE: f32 = 1.8;
    const MAX_ADC: f32 = 4096.0;
    const MAX_ADC_PIN: u8 = 6;
    
    #[derive(Debug, Clone, Copy)]
    pub enum AdcPin {
        A0,
        A1,
        A2,
        A3,
        A4,
        A5,
        A6,
    }
    
    impl AdcPin {
        pub fn index(self) -> u8 {
            match self {
                AdcPin::A0 => 0,
                AdcPin::A1 => 1,
                AdcPin::A2 => 2,
                AdcPin::A3 => 3,
                AdcPin::A4 => 4,
                AdcPin::A5 => 5,
                AdcPin::A6 => 6,
            }
        }
    }
    
    fn validate_adc_pin(adc_pin: u8) -> BbbResult<()> {
        if adc_pin > MAX_ADC_PIN {
            return Err(BbbError::InvalidValue(format!(
                "invalid ADC pin {adc_pin}; expected 0..=6"
            )));
        }
    
        Ok(())
    }
    
    fn adc_raw_path(adc_pin: u8) -> BbbResult<PathBuf> {
        validate_adc_pin(adc_pin)?;
    
        let mut path = PathBuf::from(SYSFS_ADC_DIR);
        path.push(format!("in_voltage{adc_pin}_raw"));
        Ok(path)
    }
    
    fn adc_scan_enable_path(adc_pin: u8) -> BbbResult<PathBuf> {
        validate_adc_pin(adc_pin)?;
    
        let mut path = PathBuf::from(SYSFS_ADC_SCAN_DIR);
        path.push(format!("in_voltage{adc_pin}_en"));
        Ok(path)
    }
    
    pub fn get_value(adc_pin: u8) -> BbbResult<u32> {
        sysfs::read_u32(adc_raw_path(adc_pin)?)
    }
    
    pub fn read_raw(pin: AdcPin) -> BbbResult<u32> {
        get_value(pin.index())
    }
    
    pub fn fd_open(adc_pin: u8) -> BbbResult<File> {
        Ok(File::open(adc_raw_path(adc_pin)?)?)
    }
    
    pub fn fd_close(file: File) -> BbbResult<()> {
        drop(file);
        Ok(())
    }
    
    pub fn enable_channel(adc_pin: u8) -> BbbResult<()> {
        sysfs::write_string(adc_scan_enable_path(adc_pin)?, "1")
    }
    
    pub fn disable_channel(adc_pin: u8) -> BbbResult<()> {
        sysfs::write_string(adc_scan_enable_path(adc_pin)?, "0")
    }
    
    pub fn enable_buffer() -> BbbResult<()> {
        sysfs::write_string(SYSFS_ADC_ENABLE_PATH, "1")
    }
    
    pub fn disable_buffer() -> BbbResult<()> {
        sysfs::write_string(SYSFS_ADC_ENABLE_PATH, "0")
    }
    
    pub fn set_buffer_length(len: u32) -> BbbResult<()> {
        sysfs::write_u32(SYSFS_ADC_LENGTH_PATH, len)
    }
    
    pub fn to_voltage(adc_value: u32, voltage: &mut f32) -> BbbResult<()> {
        if adc_value as f32 > MAX_ADC {
            return Err(BbbError::InvalidValue(format!(
                "ADC value {adc_value} exceeds expected maximum {MAX_ADC}"
            )));
        }
    
        *voltage = (adc_value as f32 * REF_VOLTAGE) / MAX_ADC;
    
        Ok(())
    }
    
    fn read_buffer_sample(file: &mut File) -> BbbResult<u32> {
        let mut buf = [0u8; 2];
    
        file.read_exact(&mut buf)?;
    
        Ok(u16::from_le_bytes(buf) as u32)
    }
    
    pub struct BufferedAdc {
        file: File,
        pin: AdcPin,
    }
    
    impl BufferedAdc {
        pub fn open(pin: AdcPin, buffer_len: u32) -> BbbResult<Self> {
            let adc_pin = pin.index();
    
            let _ = disable_buffer();
    
            for ch in 0..=MAX_ADC_PIN {
                let _ = disable_channel(ch);
            }
    
            enable_channel(adc_pin)?;
            set_buffer_length(buffer_len)?;
            enable_buffer()?;
    
            let file = File::open(IIO_DEVICE_PATH)?;
    
            Ok(Self { file, pin })
        }
    
        pub fn read_raw(&mut self) -> BbbResult<u32> {
            read_buffer_sample(&mut self.file)
        }
    
        pub fn read_voltage(&mut self, voltage: &mut f32) -> BbbResult<()> {
            let raw = self.read_raw()?;
            to_voltage(raw, voltage)
        }
    
        pub fn pin(&self) -> AdcPin {
            self.pin
        }
    }
    
    impl Drop for BufferedAdc {
        fn drop(&mut self) {
            let _ = disable_buffer();
    
            for ch in 0..=MAX_ADC_PIN {
                let _ = disable_channel(ch);
            }
        }
    }
    
    pub fn get_buffer(adc_pin: u8) -> BbbResult<u32> {
        validate_adc_pin(adc_pin)?;
    
        let pin = match adc_pin {
            0 => AdcPin::A0,
            1 => AdcPin::A1,
            2 => AdcPin::A2,
            3 => AdcPin::A3,
            4 => AdcPin::A4,
            5 => AdcPin::A5,
            6 => AdcPin::A6,
            _ => unreachable!(),
        };
    
        let mut adc = BufferedAdc::open(pin, 16)?;
        adc.read_raw()
    }
    

    Explanation

    2. Constants and paths

    The first part of the module defines the paths and constants used by the ADC code:

    const SYSFS_ADC_DIR: &str = "/sys/bus/iio/devices/iio:device0";
    const SYSFS_ADC_ENABLE_PATH: &str = "/sys/bus/iio/devices/iio:device0/buffer/enable";
    const SYSFS_ADC_LENGTH_PATH: &str = "/sys/bus/iio/devices/iio:device0/buffer/length";
    const SYSFS_ADC_SCAN_DIR: &str = "/sys/bus/iio/devices/iio:device0/scan_elements";
    const IIO_DEVICE_PATH: &str = "/dev/iio:device0";
    

    These correspond directly to the paths used in the C implementation.

    The distinction is important:

    • in_voltageX_raw files are used for direct one-shot reads
    • scan_elements selects which channels are included in buffered sampling
    • buffer/length controls the number of samples stored in the kernel buffer
    • buffer/enable starts or stops the kernel buffer
    • /dev/iio:device0 is the character device used to read buffered sample data

    So the Rust code is not inventing a new mechanism. It is wrapping the same Linux IIO interface in safer Rust functions.

    The voltage constants are:

    const REF_VOLTAGE: f32 = 1.8;
    const MAX_ADC: f32 = 4096.0;
    

    Recall that the BeagleBone Black ADC is a 12-bit ADC, so the raw values represent the range from 0 to 4095. The conversion used later is:

    voltage = adc_value × 1.8 / 4096
    

    3. Representing ADC channels

    In the C version, ADC channels are passed as integers:

    adc_get_value(0, &adc_reading);
    

    In Rust, we introduce an enum:

    #[derive(Debug, Clone, Copy)]
    pub enum AdcPin {
        A0,
        A1,
        A2,
        A3,
        A4,
        A5,
        A6,
    }
    

    This makes the code more descriptive:

    adc::read_raw(AdcPin::A0)
    

    rather than:

    adc::get_value(0)
    

    An enum restricts the allowed values. With raw integers, a caller could accidentally write:

    adc::get_value(99)
    

    With AdcPin, only valid channels can be named.

    Read more:


    4. Validating ADC channels

    The function:

    fn validate_adc_pin(adc_pin: u8) -> BbbResult<()>
    

    checks whether the channel number is valid.

    if adc_pin > MAX_ADC_PIN {
        return Err(BbbError::InvalidValue(format!(
            "invalid ADC pin {adc_pin}; expected 0..=6"
        )));
    }
    

    This is the Rust equivalent of defensive checks that are often missing in C code.

    BbbResult<()> means the function does not return a useful value when successful. It only tells us whether validation passed or failed:

    Ok(())  → valid
    Err(..) → invalid
    

    Read more:


    5. Building ADC file paths

    The C code builds paths using snprintf:

    snprintf(buf, sizeof(buf), SYSFS_ADC_DIR "/in_voltage%d_raw", adcPin);
    

    The Rust code builds paths using PathBuf:

    fn adc_raw_path(adc_pin: u8) -> BbbResult<PathBuf> {
        validate_adc_pin(adc_pin)?;
    
        let mut path = PathBuf::from(SYSFS_ADC_DIR);
        path.push(format!("in_voltage{adc_pin}_raw"));
        Ok(path)
    }
    

    This produces paths such as:

    /sys/bus/iio/devices/iio:device0/in_voltage0_raw
    /sys/bus/iio/devices/iio:device0/in_voltage1_raw
    

    PathBuf is Rust’s owned, growable path type. It is preferable to manually concatenating strings because it represents filesystem paths explicitly.

    Read more:


    6. One-shot ADC reads

    The C function:

    int adc_get_value(unsigned int adcPin, unsigned int *value)
    

    opens:

    /sys/bus/iio/devices/iio:device0/in_voltageX_raw
    

    reads the text value, converts it to an integer, then returns it through a pointer.

    The Rust equivalent is:

    pub fn get_value(adc_pin: u8) -> BbbResult<u32> {
        sysfs::read_u32(adc_raw_path(adc_pin)?)
    }
    

    This reuses the sysfs.rs helper we created earlier.

    The Rust-friendly wrapper is:

    pub fn read_raw(pin: AdcPin) -> BbbResult<u32> {
        get_value(pin.index())
    }
    

    Both are useful:

    • get_value(u8) mirrors the C library
    • read_raw(AdcPin) is the more Rust-like interface

    7. File descriptors and Rust ownership

    The C implementation includes:

    int adc_fd_open(unsigned int adcPin);
    int adc_fd_close(unsigned int fd);
    

    In Rust this becomes:

    pub fn fd_open(adc_pin: u8) -> BbbResult<File> {
        Ok(File::open(adc_raw_path(adc_pin)?)?)
    }
    
    pub fn fd_close(file: File) -> BbbResult<()> {
        drop(file);
        Ok(())
    }
    

    In C, file descriptors must be closed manually with close(fd). In Rust, File closes automatically when it goes out of scope.

    This is RAII:

    Resource Acquisition Is Initialisation
    

    A resource is acquired when the object is created, and released when the object is dropped.

    Read more:


    8. Enabling channels and buffers

    For continuous sampling, the kernel needs to know:

    1. which ADC channel to scan
    2. how large the buffer should be
    3. whether the buffer is enabled

    The Rust functions mirror the C functions:

    pub fn enable_channel(adc_pin: u8) -> BbbResult<()> {
        sysfs::write_string(adc_scan_enable_path(adc_pin)?, "1")
    }
    
    pub fn disable_channel(adc_pin: u8) -> BbbResult<()> {
        sysfs::write_string(adc_scan_enable_path(adc_pin)?, "0")
    }
    
    pub fn enable_buffer() -> BbbResult<()> {
        sysfs::write_string(SYSFS_ADC_ENABLE_PATH, "1")
    }
    
    pub fn disable_buffer() -> BbbResult<()> {
        sysfs::write_string(SYSFS_ADC_ENABLE_PATH, "0")
    }
    
    pub fn set_buffer_length(len: u32) -> BbbResult<()> {
        sysfs::write_u32(SYSFS_ADC_LENGTH_PATH, len)
    }
    

    These correspond to terminal commands such as:

    echo 1 > /sys/bus/iio/devices/iio:device0/scan_elements/in_voltage0_en
    echo 100 > /sys/bus/iio/devices/iio:device0/buffer/length
    echo 1 > /sys/bus/iio/devices/iio:device0/buffer/enable
    

    All of these operations are file writes, so the ADC module reuses sysfs::write_string(...) and sysfs::write_u32(...).


    9. Converting raw ADC values to voltage

    The C function was:

    int adc_to_voltage(unsigned adcValue, float *voltage) {
        *voltage = (((float)adcValue * REFVOLTAGE) / (float)MAXADC);
        return 0;
    }
    

    The Rust version intentionally follows the same style:

    pub fn to_voltage(adc_value: u32, voltage: &mut f32) -> BbbResult<()> {
        if adc_value as f32 > MAX_ADC {
            return Err(BbbError::InvalidValue(format!(
                "ADC value {adc_value} exceeds expected maximum {MAX_ADC}"
            )));
        }
    
        *voltage = (adc_value as f32 * REF_VOLTAGE) / MAX_ADC;
    
        Ok(())
    }
    

    C uses:

    float *voltage
    

    Rust uses:

    &mut f32
    

    They serve a similar role: the function writes the result into a variable owned by the caller. However, Rust’s mutable reference is safer because it cannot be null and is checked by the borrow checker.

    Read more:


    10. Reading buffered samples

    The one-shot raw files return text:

    in_voltage0_raw → "1234\n"
    

    The buffered IIO character device returns binary sample data through:

    /dev/iio:device0
    

    That is why the Rust code has a separate function:

    fn read_buffer_sample(file: &mut File) -> BbbResult<u32> {
        let mut buf = [0u8; 2];
    
        file.read_exact(&mut buf)?;
    
        Ok(u16::from_le_bytes(buf) as u32)
    }
    

    The ADC scan type is typically:

    le:u12/16>>0
    

    This means:

    • le → little endian
    • u → unsigned
    • 12 → 12 useful bits
    • 16 → stored in 16 bits
    • >>0 → no shift required

    So each buffered sample is read as two bytes and converted from little-endian bytes.

    Read more:


    11. The BufferedAdc struct

    The struct:

    pub struct BufferedAdc {
        file: File,
        pin: AdcPin,
    }
    

    represents an active buffered ADC stream.

    It owns:

    • the open /dev/iio:device0 file
    • the selected ADC channel

    Opening a buffered ADC performs the setup sequence:

    disable buffer
    disable all channels
    enable selected channel
    set buffer length
    enable buffer
    open /dev/iio:device0
    return BufferedAdc
    

    Most IIO buffer settings can only be changed while the buffer is disabled. That is why the code begins with:

    let _ = disable_buffer();
    

    Only one ADC channel is enabled at a time in this first implementation, so each sample read from /dev/iio:device0 corresponds to the selected channel.

    The method:

    pub fn read_raw(&mut self) -> BbbResult<u32>
    

    needs &mut self because reading changes the file’s internal state.


    12. Cleanup with Drop

    The Rust code includes:

    impl Drop for BufferedAdc {
        fn drop(&mut self) {
            let _ = disable_buffer();
    
            for ch in 0..=MAX_ADC_PIN {
                let _ = disable_channel(ch);
            }
        }
    }
    

    This is a major Rust improvement over the C version.

    In C, the programmer must remember to call cleanup functions:

    disable_channel(adcPin);
    disable_buffer();
    

    In Rust, cleanup happens automatically when BufferedAdc goes out of scope.

    Drop::drop cannot return a Result, so cleanup is best-effort. That is why the code uses:

    let _ = disable_buffer();
    

    Read more:


    13. get_buffer(...)

    The function:

    pub fn get_buffer(adc_pin: u8) -> BbbResult<u32>
    

    is kept as a C-style convenience wrapper.

    It:

    1. validates the channel
    2. maps the channel number to AdcPin
    3. opens a temporary buffered ADC stream
    4. reads one sample
    5. drops the stream

    For continuous reading, this is not the preferred interface.

    The better approach is:

    let mut adc = BufferedAdc::open(AdcPin::A0, 16)?;
    
    loop {
        let raw = adc.read_raw()?;
    }
    

    This avoids repeatedly enabling/disabling the buffer.

  2. Now we can build:

    Terminal

    cargo build
    

14. Example: simple ADC read

  1. Once you have rebuilt bbi_crate in the previous section, you can now create examples/adc_read.rs to put it in.

    Code: examples/adc_read.rs [17 lines]

    use std::thread;
    use std::time::Duration;
    
    use bbb_io::adc::{self, AdcPin};
    
    fn main() -> bbb_io::BbbResult<()> {
        loop {
            let raw = adc::read_raw(AdcPin::A0)?;
    
            let mut volts = 0.0;
            adc::to_voltage(raw, &mut volts)?;
    
            println!("AIN0: raw={raw}, voltage={volts:.3} V");
    
            thread::sleep(Duration::from_millis(500));
        }
    }
    

    Explanation

    This example reads from AIN0 in one-shot mode.

    let raw = adc::read_raw(AdcPin::A0)?;
    

    This reads:

    /sys/bus/iio/devices/iio:device0/in_voltage0_raw
    

    Then:

    let mut volts = 0.0;
    adc::to_voltage(raw, &mut volts)?;
    

    converts the raw ADC value into a voltage.

    The delay slows the loop so the terminal output remains readable.

  2. Compile it:

    Terminal

    rustc examples/adc_read.rs \
    --edition=2021 \
    -L target/debug \
    --extern bbb_io=target/debug/libbbb_io.rlib \
    -o adc_read
    
  3. Build the circuit like this:

    Figure: Active High LED cirucit

    Danger

    The BeagleBone Black ADC input range is 0 V to 1.8 V.

    Do not connect 3.3 V directly to an ADC input pin.

    If using a 3.3 V source, use a voltage divider or a safe external 1.8 V reference.

    As a reminder here is the bbb headers map

    BBB Headers

  4. Run it:

    Terminal

    ./adc_read