ADC
- Make sure you have completed the following chapters:
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
-
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() }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_rawfiles are used for direct one-shot readsscan_elementsselects which channels are included in buffered samplingbuffer/lengthcontrols the number of samples stored in the kernel bufferbuffer/enablestarts or stops the kernel buffer/dev/iio:device0is 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:
- Rust enums: https://doc.rust-lang.org/book/ch06-00-enums.html
match: https://doc.rust-lang.org/book/ch06-02-match.html
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(..) → invalidRead more:
Result: https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html- Unit type
(): https://doc.rust-lang.org/std/primitive.unit.html
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_rawPathBufis 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_rawreads 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.rshelper 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 libraryread_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,Filecloses automatically when it goes out of scope.This is RAII:
Resource Acquisition Is InitialisationA resource is acquired when the object is created, and released when the object is dropped.
Read more:
File: https://doc.rust-lang.org/std/fs/struct.File.htmlDrop: https://doc.rust-lang.org/std/ops/trait.Drop.html
8. Enabling channels and buffers
For continuous sampling, the kernel needs to know:
- which ADC channel to scan
- how large the buffer should be
- 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/enableAll of these operations are file writes, so the ADC module reuses
sysfs::write_string(...)andsysfs::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 *voltageRust uses:
&mut f32They 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:
- References and borrowing: https://doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
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:device0That 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>>0This means:
le→ little endianu→ unsigned12→ 12 useful bits16→ 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:
Read::read_exact: https://doc.rust-lang.org/std/io/trait.Read.html#method.read_exactu16::from_le_bytes: https://doc.rust-lang.org/std/primitive.u16.html#method.from_le_bytes
11. The
BufferedAdcstructThe struct:
pub struct BufferedAdc { file: File, pin: AdcPin, }represents an active buffered ADC stream.
It owns:
- the open
/dev/iio:device0file - 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 BufferedAdcMost 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:device0corresponds to the selected channel.The method:
pub fn read_raw(&mut self) -> BbbResult<u32>needs
&mut selfbecause reading changes the file’s internal state.
12. Cleanup with
DropThe 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
BufferedAdcgoes out of scope.Drop::dropcannot return aResult, 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:
- validates the channel
- maps the channel number to
AdcPin - opens a temporary buffered ADC stream
- reads one sample
- 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.
-
Now we can build:
14. Example: simple ADC read
-
Once you have rebuilt bbi_crate in the previous section, you can now create
examples/adc_read.rsto 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)); } }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_rawThen:
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.
-
Compile it:
-
Build the circuit like this:

Figure: Active High LED cirucit
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

-
Run it: