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.
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
| Configuration | Gain Formula | Use 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 |
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
| Type | Characteristics | Use Case |
|---|---|---|
| Sallen-Key | 2nd order, simple, good for low-Q | Anti-aliasing, general signal conditioning |
| Multiple Feedback | 2nd order, better high-frequency rejection | Audio, sensor bandpass filtering |
| Butterworth | Maximally flat passband | When uniform gain in passband is critical |
| Chebyshev | Steeper rolloff, passband ripple | When sharp cutoff is more important than flatness |
| Bessel | Linear 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 (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.
- 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)
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
| Feature | SAR ADC | Sigma-Delta ADC |
|---|---|---|
| Resolution | 8–20 bits | 16–32 bits |
| Speed | 100 KSPS–5 MSPS | 10–1000 SPS |
| Latency | Low | High (filter settling time) |
| Noise | Moderate | Very low (oversampling + noise shaping) |
| Best For | Multiplexed, fast sampling | Precision: load cells, thermocouples, RTDs |
| Example ICs | ADS1115, STM32 ADC | ADS1256, HX711 (load cell) |
ADC Selection Guide
- 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).
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
| Feature | I2C | SPI |
|---|---|---|
| Wires | 2 (SDA, SCL) | 4 (MOSI, MISO, SCK, CS) |
| Speed | 100k–3.4 MHz | 1–100+ MHz |
| Duplex | Half-duplex | Full-duplex |
| Addressing | 7-bit address in protocol | Dedicated CS line per device |
| Multi-device | Easy (shared bus) | One CS pin per device |
| Best For | Many 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.
- 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.
- 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.