Back to Sensors & Actuators Series

Part 4: Signal Conditioning & Data Acquisition

July 14, 2025 Wasil Zafar 45 min read

From raw sensor output to clean digital data — op-amp circuits, filters, ADC architectures, digital protocols (I2C, SPI, UART, CAN), and calibration techniques.

Table of Contents

  1. Op-Amp Signal Conditioning
  2. Analog & Digital Filters
  3. ADC Architectures
  4. Digital Communication Protocols
  5. Sensor Calibration
  6. Conclusion & Next Steps

Op-Amp Signal Conditioning

Raw sensor outputs are often too small, noisy, or improperly referenced for direct ADC input. Operational amplifiers (op-amps) form the backbone of analog signal conditioning — amplifying, filtering, and buffering sensor signals.

Signal Conditioning Pipeline
flowchart LR
    A["🔌 Sensor
Raw Signal"] --> B["Amplification
Op-Amp Gain"] B --> C["Filtering
Low/High/Band Pass"] C --> D["Level Shifting
Offset + Scale"] D --> E["ADC
Analog → Digital"] E --> F["Digital Protocol
I2C / SPI / UART"] F --> G["🖥️ MCU
Processing"] style A fill:#3B9797,stroke:#3B9797,color:#fff style G fill:#132440,stroke:#132440,color:#fff style E fill:#fff5f5,stroke:#BF092F,color:#132440

Amplification Circuits

Common Op-Amp Configurations

Analog Signal Chain
ConfigurationGain FormulaUse Case
Non-Inverting$G = 1 + \frac{R_f}{R_1}$General amplification, high input impedance
Inverting$G = -\frac{R_f}{R_1}$Summing amplifier, precise gain
Differential$G = \frac{R_f}{R_1}(V_2 - V_1)$Wheatstone bridge output, noise rejection
Instrumentation$G = 1 + \frac{2R}{R_G}$High CMRR, strain gauges, biomedical
Instrumentation Amplifiers (INA): The INA128/INA333 are dedicated instrumentation amps with gains set by a single external resistor ($R_G$). They offer CMRR >100 dB, essential for rejecting common-mode noise on sensor lines. An INA is equivalent to three op-amps (two input buffers + one difference amp) in a single IC.

Wheatstone Bridge

The Wheatstone bridge converts small resistance changes (strain gauges, RTDs, piezoresistive sensors) into measurable voltage differences.

For a bridge with one active arm ($R_x$) and three fixed resistors ($R$), the output voltage is:

$$V_{out} = V_{ex} \cdot \frac{\Delta R}{2(2R + \Delta R)} \approx V_{ex} \cdot \frac{\Delta R}{4R} \quad \text{(for small }\Delta R\text{)}$$

Where $V_{ex}$ is the excitation voltage and $\Delta R$ is the resistance change. The bridge output (typically µV to mV) requires amplification via an instrumentation amplifier before ADC sampling.

Voltage Buffering

A voltage follower (unity-gain buffer) provides high input impedance and low output impedance, preventing the ADC from loading the sensor circuit:

// Using external op-amp buffer with STM32 ADC
// Circuit: Sensor → Op-Amp Buffer (LMV358) → PA0 (ADC1_CH0)
//
// The buffer prevents ADC input impedance (few kΩ during sampling)
// from loading high-impedance sensors (e.g., pH probe: >100 MΩ)

#include "stm32f4xx.h"

// Multi-channel ADC with DMA for continuous sensor reading
static volatile uint16_t adc_buffer[4];  // 4 channels

void adc_dma_init(void) {
    // Enable clocks
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN | RCC_AHB1ENR_DMA2EN;
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;

    // PA0-PA3 as analog inputs
    GPIOA->MODER |= (3U<<0) | (3U<<2) | (3U<<4) | (3U<<6);

    // DMA2 Stream0 Channel0 for ADC1
    DMA2_Stream0->CR = 0;
    DMA2_Stream0->PAR  = (uint32_t)&ADC1->DR;
    DMA2_Stream0->M0AR = (uint32_t)adc_buffer;
    DMA2_Stream0->NDTR = 4;
    DMA2_Stream0->CR  = DMA_SxCR_MSIZE_0 | DMA_SxCR_PSIZE_0
                       | DMA_SxCR_MINC | DMA_SxCR_CIRC;
    DMA2_Stream0->CR |= DMA_SxCR_EN;

    // ADC1: scan mode, 4 channels, continuous
    ADC1->CR1  = ADC_CR1_SCAN;
    ADC1->CR2  = ADC_CR2_DMA | ADC_CR2_DDS | ADC_CR2_CONT;
    ADC1->SQR1 = (3U << 20);  // 4 conversions
    ADC1->SQR3 = (0U) | (1U<<5) | (2U<<10) | (3U<<15);  // CH0-CH3
    ADC1->SMPR2 = 0x00000FFF;  // 480 cycles for all channels

    ADC1->CR2 |= ADC_CR2_ADON;
    ADC1->CR2 |= ADC_CR2_SWSTART;
}

Analog & Digital Filters

Passive RC Filters

A simple RC low-pass filter attenuates high-frequency noise above its cutoff frequency:

$$f_c = \frac{1}{2\pi R C}$$

For anti-aliasing before ADC sampling, set the cutoff frequency to less than half the sampling rate (Nyquist criterion): $f_c < \frac{f_s}{2}$.

Active Filters

Active Filter Topologies

TypeCharacteristicsUse Case
Sallen-Key2nd order, simple, good for low-QAnti-aliasing, general signal conditioning
Multiple Feedback2nd order, better high-frequency rejectionAudio, sensor bandpass filtering
ButterworthMaximally flat passbandWhen uniform gain in passband is critical
ChebyshevSteeper rolloff, passband rippleWhen sharp cutoff is more important than flatness
BesselLinear phase (no pulse distortion)Pulse/step response applications

Digital Filters (FIR/IIR)

After ADC conversion, digital filters offer flexibility that analog circuits cannot match. The two main categories are:

FIR vs IIR Filters:
  • FIR (Finite Impulse Response): Always stable, linear phase, higher order needed for sharp cutoff. $y[n] = \sum_{k=0}^{N} b_k \cdot x[n-k]$
  • IIR (Infinite Impulse Response): Lower order for same cutoff, but can be unstable, non-linear phase. $y[n] = \sum_{k=0}^{N} b_k \cdot x[n-k] - \sum_{k=1}^{M} a_k \cdot y[n-k]$
// Simple moving average filter (FIR) for sensor smoothing
#include <stdint.h>

#define FILTER_SIZE 16

typedef struct {
    uint16_t buffer[FILTER_SIZE];
    uint8_t  index;
    uint32_t sum;
    uint8_t  count;
} MovingAverage;

void ma_init(MovingAverage *ma) {
    for (int i = 0; i < FILTER_SIZE; i++) ma->buffer[i] = 0;
    ma->index = 0;
    ma->sum   = 0;
    ma->count = 0;
}

uint16_t ma_filter(MovingAverage *ma, uint16_t sample) {
    ma->sum -= ma->buffer[ma->index];
    ma->buffer[ma->index] = sample;
    ma->sum += sample;
    ma->index = (ma->index + 1) % FILTER_SIZE;
    if (ma->count < FILTER_SIZE) ma->count++;
    return (uint16_t)(ma->sum / ma->count);
}

// Exponential Moving Average (IIR, 1st order)
typedef struct {
    float alpha;
    float output;
    uint8_t initialized;
} EMA_Filter;

void ema_init(EMA_Filter *ema, float alpha) {
    ema->alpha = alpha;    // 0 < alpha < 1 (smaller = smoother)
    ema->output = 0;
    ema->initialized = 0;
}

float ema_filter(EMA_Filter *ema, float sample) {
    if (!ema->initialized) {
        ema->output = sample;
        ema->initialized = 1;
    } else {
        ema->output = ema->alpha * sample + (1.0f - ema->alpha) * ema->output;
    }
    return ema->output;
}

ADC Architectures

SAR ADC (Successive Approximation Register)

The most common ADC in microcontrollers. It uses a binary search algorithm to converge on the input voltage in $n$ clock cycles for $n$ bits of resolution.

SAR ADC Characteristics:
  • Resolution: 8–20 bits (typically 12-bit in MCUs)
  • Speed: 100 kSPS to 5 MSPS
  • Power: Low (ideal for battery-powered devices)
  • Latency: Low — result available after one conversion cycle
  • Examples: STM32 built-in ADC, ADS1115 (16-bit external)
SAR ADC Binary Search Algorithm
flowchart TD
    A["Start Conversion"] --> B["Set MSB = 1
bit n-1"] B --> C["DAC outputs
trial voltage"] C --> D{"Compare:
V_in ≥ V_DAC?"} D -->|Yes| E["Keep bit = 1"] D -->|No| F["Clear bit = 0"] E --> G{"More bits
to test?"} F --> G G -->|Yes| H["Move to next
lower bit"] H --> C G -->|No| I["Conversion Complete
n-bit result ready"] style A fill:#3B9797,stroke:#3B9797,color:#fff style I fill:#132440,stroke:#132440,color:#fff style D fill:#fff5f5,stroke:#BF092F,color:#132440

Sigma-Delta (ΣΔ) ADC

Sigma-Delta ADCs trade speed for resolution. They oversample the input at very high rates, then use digital decimation filtering to achieve 16–32 bits of effective resolution.

SAR vs Sigma-Delta Comparison

FeatureSAR ADCSigma-Delta ADC
Resolution8–20 bits16–32 bits
Speed100 KSPS–5 MSPS10–1000 SPS
LatencyLowHigh (filter settling time)
NoiseModerateVery low (oversampling + noise shaping)
Best ForMultiplexed, fast samplingPrecision: load cells, thermocouples, RTDs
Example ICsADS1115, STM32 ADCADS1256, HX711 (load cell)

ADC Selection Guide

Choosing the Right ADC:
  • Fast signals (vibration, audio): SAR ADC, 12+ bits, 100+ KSPS
  • Precision DC (temperature, weight): Sigma-Delta, 24-bit, 10–100 SPS
  • Multiple channels: SAR with multiplexer (most MCU ADCs)
  • Low power: SAR in single-shot mode with sleep between conversions
  • EMI-prone environments: Sigma-Delta (inherent noise filtering)

Digital Communication Protocols

I2C Protocol

I2C (Inter-Integrated Circuit) is a 2-wire synchronous protocol using SDA (data) and SCL (clock). It supports multiple masters and slaves on the same bus, with 7-bit addressing (up to 112 devices).

I2C Read Transaction Sequence
sequenceDiagram
    participant M as MCU (Master)
    participant S as Sensor (Slave)
    M->>S: START condition
    M->>S: Slave Address + Write bit
    S-->>M: ACK
    M->>S: Register Address
    S-->>M: ACK
    M->>S: REPEATED START
    M->>S: Slave Address + Read bit
    S-->>M: ACK
    S-->>M: Data Byte 1
    M-->>S: ACK
    S-->>M: Data Byte 2
    M-->>S: NACK (last byte)
    M->>S: STOP condition
                            
// I2C Master: Read sensor register (STM32 HAL-like bare metal)
#include "stm32f4xx.h"

void i2c_init(void) {
    RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOBEN;

    // PB6 = SCL, PB7 = SDA (AF4, open-drain)
    GPIOB->MODER  |= (2U << 12) | (2U << 14);
    GPIOB->OTYPER |= (1U << 6) | (1U << 7);   // Open-drain
    GPIOB->AFR[0] |= (4U << 24) | (4U << 28);

    // I2C1: 100 kHz standard mode (APB1 = 42 MHz)
    I2C1->CR2 = 42;           // APB clock in MHz
    I2C1->CCR = 210;          // 42 MHz / (2 * 100 kHz)
    I2C1->TRISE = 43;         // (42 MHz * 1000ns) / 1000 + 1
    I2C1->CR1 |= I2C_CR1_PE; // Enable I2C
}

uint8_t i2c_read_register(uint8_t dev_addr, uint8_t reg_addr) {
    // START + Write slave address + register address
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));
    I2C1->DR = (dev_addr << 1);           // Write mode
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    (void)I2C1->SR2;
    I2C1->DR = reg_addr;
    while (!(I2C1->SR1 & I2C_SR1_TXE));

    // Repeated START + Read
    I2C1->CR1 |= I2C_CR1_START;
    while (!(I2C1->SR1 & I2C_SR1_SB));
    I2C1->DR = (dev_addr << 1) | 1;       // Read mode
    while (!(I2C1->SR1 & I2C_SR1_ADDR));
    I2C1->CR1 &= ~I2C_CR1_ACK;            // NACK after 1 byte
    (void)I2C1->SR2;

    I2C1->CR1 |= I2C_CR1_STOP;
    while (!(I2C1->SR1 & I2C_SR1_RXNE));
    return (uint8_t)I2C1->DR;
}

SPI Protocol

SPI (Serial Peripheral Interface) uses 4 wires: MOSI, MISO, SCK, and CS (chip select). It’s faster than I2C (up to 100 MHz) and operates in full-duplex mode.

I2C vs SPI Comparison

FeatureI2CSPI
Wires2 (SDA, SCL)4 (MOSI, MISO, SCK, CS)
Speed100k–3.4 MHz1–100+ MHz
DuplexHalf-duplexFull-duplex
Addressing7-bit address in protocolDedicated CS line per device
Multi-deviceEasy (shared bus)One CS pin per device
Best ForMany slow sensors (BME280, OLED)Fast data: ADC, flash, display

UART Protocol

UART (Universal Asynchronous Receiver/Transmitter) is an asynchronous point-to-point protocol. Both sides must agree on baud rate (9600, 115200, etc.), data bits, parity, and stop bits. Common for GPS modules, Bluetooth (HC-05), and debug output.

CAN Bus

CAN (Controller Area Network) is a robust, multi-master protocol designed for automotive and industrial environments. It uses differential signaling (CAN_H, CAN_L) for noise immunity and supports priority-based arbitration.

CAN Bus Features:
  • Speed: Up to 1 Mbps (CAN 2.0), 5 Mbps (CAN FD)
  • Distance: Up to 1000m at 50 kbps
  • Error detection: CRC, bit stuffing, acknowledgment, error frames
  • Message-based: No addresses — messages have IDs, all nodes receive all messages
  • Priority: Lower ID = higher priority (bitwise arbitration)

Sensor Calibration

Single-Point Calibration

Applies a constant offset correction. Measure a known reference point and adjust all readings by the error:

$$\text{corrected} = \text{raw} + (\text{reference} - \text{raw\_at\_reference})$$

Multi-Point & Curve Fitting

#!/usr/bin/env python3
"""Multi-point sensor calibration with linear regression"""

import numpy as np

# Known reference values (from calibration standards)
reference_values = np.array([0.0, 25.0, 50.0, 75.0, 100.0])

# Corresponding raw sensor readings
raw_readings = np.array([12.3, 36.8, 62.1, 87.5, 112.4])

# Linear regression: y = mx + b
coefficients = np.polyfit(raw_readings, reference_values, 1)
slope, intercept = coefficients
print(f"Calibration: corrected = {slope:.4f} * raw + {intercept:.4f}")

# Create calibration function
calibrate = np.poly1d(coefficients)

# Test calibration
test_readings = [20.0, 45.0, 70.0, 95.0]
for raw in test_readings:
    corrected = calibrate(raw)
    print(f"  Raw: {raw:.1f} → Corrected: {corrected:.2f}")

# R-squared (goodness of fit)
predicted = calibrate(raw_readings)
ss_res = np.sum((reference_values - predicted) ** 2)
ss_tot = np.sum((reference_values - np.mean(reference_values)) ** 2)
r_squared = 1 - (ss_res / ss_tot)
print(f"\nR² = {r_squared:.6f}")

Conclusion & Next Steps

Signal conditioning is the bridge between the physical world and digital computation. From op-amp amplification to digital filtering and calibration, each stage matters for achieving accurate, reliable sensor data.

Key Takeaways:
  • Op-amps amplify, buffer, and filter sensor signals before ADC conversion
  • SAR ADCs are fast and versatile; Sigma-Delta ADCs offer extreme precision
  • I2C is simple for multi-sensor buses; SPI is faster for high-bandwidth data
  • CAN bus provides industrial-grade reliability for automotive and automation
  • Multi-point calibration with curve fitting corrects for non-linearity and offset

In Part 5, we turn to the output side: Actuators Deep Dive — DC motors, stepper motors, servos, solenoids, and pneumatic systems.