Skip to content
Home » Embedded Systems » Basic Electronics » Signal Conditioning for Sensors: Amplification, Filtering, and Level Shifting

Signal Conditioning for Sensors: Amplification, Filtering, and Level Shifting

Sensors for Embedded Systems
Part 9 of 18 — View Full Path →

KEY TAKEAWAYS

  • Signal conditioning adapts raw sensor outputs (millivolts, resistance changes) to match the ADC input range of your microcontroller.
  • Instrumentation amplifiers (INA126, AD620) provide high-precision differential amplification for bridge sensors and thermocouples.
  • RC low-pass filters with cutoff frequency fc = 1/(2πRC) are the simplest and most effective noise removal technique for slow-changing sensor signals.
  • Lookup tables and polynomial approximation are the two main software linearization methods for nonlinear sensors like thermistors.
  • A moving average filter in software complements hardware filtering and is easy to implement with minimal RAM.

Part of the Complete Guide to Sensors for Embedded Systems series.

The raw output of many sensors — millivolts from a thermocouple, resistance changes from a strain gauge, or a 5V analog signal feeding a 3.3V MCU — cannot be directly connected to a microcontroller ADC. Signal conditioning bridges this gap, preparing sensor signals for accurate, safe digitization. This guide covers the key signal conditioning techniques with practical circuit designs and working C code you can use in your embedded projects.

Amplification Circuits

Many sensors produce signals in the millivolt range. A thermocouple generates about 41µV per °C, a strain gauge bridge changes by a few millivolts under load, and a photodiode produces microamps of current. These signals are far smaller than a typical ADC input range (0-3.3V or 0-5V).

An amplifier boosts the signal to fill the ADC range, maximizing the effective resolution of your measurement. For example, amplifying a 0-40mV thermocouple output by 100x produces 0-4V — a good fit for a 5V ADC. Without amplification, the 40mV signal would occupy only 0.8% of the ADC range, wasting 99% of the available resolution.

Non-inverting op-amp configuration: The simplest amplifier uses an op-amp with two resistors. The gain formula is: Gain = 1 + (R2 / R1). For a gain of 100, use R2 = 99kΩ and R1 = 1kΩ. The non-inverting configuration has very high input impedance, which avoids loading the sensor. Choose an op-amp with rail-to-rail output (MCP6001, OPA340) when the output needs to reach close to the supply voltage.

Instrumentation amplifiers for bridge sensors: For precision measurements with Wheatstone bridge sensors (strain gauges, load cells, pressure transducers), an instrumentation amplifier is essential. Devices like the INA126 and AD620 amplify the small differential voltage across the bridge while rejecting common-mode noise. A single external resistor sets the gain. For the INA126, the gain formula is: Gain = 5 + (80kΩ / Rg). To achieve a gain of 100, set Rg = 842Ω. The common-mode rejection ratio (CMRR) of 100dB or more means noise that appears on both inputs equally is attenuated by a factor of 100,000.

The Wheatstone Bridge

Many sensors — strain gauges, RTDs, and some pressure transducers — are resistive elements that change value by a small fraction under stimulus. The Wheatstone bridge converts this tiny resistance change into a measurable differential voltage. The bridge consists of four resistors arranged in a diamond pattern with an excitation voltage across one diagonal and the measurement output across the other.

When all four resistors are equal (balanced bridge), the output voltage is zero. When the sensor element changes resistance, the bridge becomes unbalanced and produces an output voltage proportional to the change. For a single active element (quarter-bridge), the output is approximately: Vout = Vexcitation × (ΔR / 4R), where ΔR is the resistance change. For a 350Ω strain gauge with a gauge factor of 2.0 under 1000 microstrain, ΔR = 0.7Ω, producing only 0.5mV per volt of excitation. This tiny signal requires an instrumentation amplifier with a gain of 500-1000 to reach usable ADC levels.

Filtering Techniques

Filters remove unwanted frequency components from the sensor signal while preserving the information you need. In most embedded sensor applications, the signal of interest changes slowly (temperature, pressure, humidity) while the noise is at higher frequencies (power line interference at 50/60Hz, switching noise, RF pickup).

Passive RC Low-Pass Filter: A resistor and capacitor in series create a first-order low-pass filter. The cutoff frequency is: fc = 1 / (2π × R × C). Signals below fc pass through with minimal attenuation; signals above fc are attenuated at 20dB per decade. For a temperature sensor that changes slowly (under 1Hz), an R=10kΩ, C=10µF filter (fc = 1.6Hz) removes all noise above 1.6Hz while passing the temperature signal. The filter also serves as an anti-aliasing filter, preventing high-frequency noise from being aliased into the measurement by the ADC sampling process.

Active Filters: Op-amp-based filters provide sharper cutoff slopes and can add gain simultaneously. A second-order Sallen-Key low-pass filter achieves 40dB/decade rolloff. Butterworth response provides a maximally flat passband with no ripple. Bessel response provides linear phase (no signal distortion in the time domain), which is important when the waveshape matters. For most embedded sensor applications, a Butterworth is the standard choice.

Notch Filters: These specifically remove a narrow frequency band. A twin-T notch filter tuned to 50Hz or 60Hz removes power line interference — common in strain gauge and biomedical applications. The notch depth depends on component matching; use 1% tolerance resistors and capacitors for best results.

For most embedded sensor applications, a simple passive RC filter at the ADC input combined with software averaging provides sufficient noise rejection. Reserve active filters for precision measurement systems where the noise frequency is close to the signal frequency.

Level Shifting and Voltage Translation

When a 5V sensor connects to a 3.3V microcontroller, the sensor output can exceed the MCU’s maximum input voltage, potentially damaging it. Level shifting safely converts between voltage domains.

Resistor Voltage Divider: Two resistors reduce a 5V signal to 3.3V. Using R1=1.8kΩ (top) and R2=3.3kΩ (bottom), the output voltage is: Vout = Vin × R2 / (R1 + R2) = 5V × 3.3 / 5.1 = 3.24V. This method is simple and cheap, but it consumes standing current (about 1mA in this example), reduces the signal amplitude (which reduces effective ADC resolution), and the output impedance of the divider can affect ADC accuracy if the ADC input impedance is not high enough. Add a buffer op-amp after the divider for best results.

Op-amp level shifter: An op-amp in a differential configuration can simultaneously scale and offset a signal. To convert a 0-5V sensor output to 0-3.3V for a 3.3V MCU: Vout = Vin × (3.3/5.0) = Vin × 0.66. Use a non-inverting amplifier with gain of 0.66 (actually an attenuator), or a unity-gain buffer powered from 3.3V with rail-to-rail output — the op-amp naturally clips the output at 3.3V, though this is not precise for signals near the rail.

Level Shifter ICs: Dedicated chips like TXB0108 or BSS138-based circuits provide bidirectional level translation for I2C and SPI buses. They maintain signal integrity and do not attenuate the signal. For digital communication buses, always use dedicated level shifters rather than resistor dividers.

Linearization: Lookup Table Method

Many sensors have nonlinear responses. Thermistors, for example, follow an exponential resistance-temperature curve. While the Steinhart-Hart equation can model this mathematically, a lookup table is often simpler and faster on resource-constrained microcontrollers. The table stores known resistance-to-temperature pairs, and the code interpolates between entries for intermediate values.

/* Thermistor linearization using lookup table with interpolation */
#include <stdint.h>

/* 10K NTC thermistor lookup table (resistance in ohms, temperature in 0.1°C) */
/* Entries at 5°C intervals from -20°C to 100°C */
typedef struct {
    uint32_t resistance;
    int16_t  temp_x10;   /* Temperature in 0.1°C units */
} ThermistorEntry;

static const ThermistorEntry therm_table[] = {
    {97080, -200},  /* -20.0°C */
    {67770,  -150}, /* -15.0°C */
    {47170,  -100}, /* -10.0°C */
    {33650,  -50},  /*  -5.0°C */
    {24270,   0},   /*   0.0°C */
    {17700,   50},  /*   5.0°C */
    {12960,  100},  /*  10.0°C */
    { 9588,  150},  /*  15.0°C */
    { 7150,  200},  /*  20.0°C */
    { 5372,  250},  /*  25.0°C (nominal 10K) */
    { 4064,  300},  /*  30.0°C */
    { 3101,  350},  /*  35.0°C */
    { 2383,  400},  /*  40.0°C */
    { 1843,  450},  /*  45.0°C */
    { 1433,  500},  /*  50.0°C */
    { 1122,  550},  /*  55.0°C */
    {  884,  600},  /*  60.0°C */
    {  700,  650},  /*  65.0°C */
    {  558,  700},  /*  70.0°C */
    {  447,  750},  /*  75.0°C */
    {  360,  800},  /*  80.0°C */
    {  291,  850},  /*  85.0°C */
    {  237,  900},  /*  90.0°C */
    {  194,  950},  /*  95.0°C */
    {  159, 1000},  /* 100.0°C */
};

#define TABLE_SIZE (sizeof(therm_table) / sizeof(therm_table[0]))

/**
 * Convert thermistor resistance to temperature using lookup + interpolation.
 * Returns temperature in 0.1°C units (e.g., 253 = 25.3°C).
 * Returns -9999 if resistance is out of table range.
 */
int16_t thermistor_resistance_to_temp(uint32_t resistance) {
    /* Check bounds */
    if (resistance >= therm_table[0].resistance) return therm_table[0].temp_x10;
    if (resistance = therm_table[i+1].resistance) {
            /* Linear interpolation */
            int32_t r_range = therm_table[i].resistance - therm_table[i+1].resistance;
            int32_t t_range = therm_table[i+1].temp_x10 - therm_table[i].temp_x10;
            int32_t r_offset = therm_table[i].resistance - resistance;
            return therm_table[i].temp_x10 +
                   (int16_t)((r_offset * t_range) / r_range);
        }
    }
    return -9999;  /* Should not reach here */
}

/**
 * Read thermistor via ADC with voltage divider.
 * Circuit: VCC -- [R_fixed] -- ADC_pin -- [Thermistor] -- GND
 */
int16_t read_thermistor_temp(uint16_t adc_value, uint16_t adc_max,
                              uint32_t r_fixed) {
    if (adc_value == 0) return -9999;
    uint32_t r_therm = (uint32_t)r_fixed * adc_value / (adc_max - adc_value);
    return thermistor_resistance_to_temp(r_therm);
}

Software Filtering: Moving Average

A moving average filter in software complements hardware filtering. It smooths out random noise by averaging the last N samples. The implementation uses a circular buffer for efficiency — each new sample replaces the oldest, and the running sum is updated incrementally without re-adding all samples.

/* Moving average filter - circular buffer implementation */
#include <stdint.h>

#define FILTER_SIZE  16  /* Must be power of 2 for fast modulo */

typedef struct {
    int32_t  buffer[FILTER_SIZE];
    int32_t  sum;
    uint8_t  index;
    uint8_t  count;  /* Tracks fill level for startup */
} MovingAvgFilter;

void filter_init(MovingAvgFilter *f) {
    f->sum = 0;
    f->index = 0;
    f->count = 0;
    for (int i = 0; i < FILTER_SIZE; i++) f->buffer[i] = 0;
}

int32_t filter_update(MovingAvgFilter *f, int32_t new_sample) {
    /* Subtract oldest sample, add new one */
    f->sum -= f->buffer[f->index];
    f->buffer[f->index] = new_sample;
    f->sum += new_sample;

    f->index = (f->index + 1) & (FILTER_SIZE - 1);  /* Fast modulo */
    if (f->count < FILTER_SIZE) f->count++;

    return f->sum / f->count;  /* Correct average during fill-up */
}

/* Usage example: smooth ADC readings */
MovingAvgFilter adc_filter;

void sensor_task(void) {
    filter_init(&adc_filter);
    while (1) {
        int32_t raw_adc = read_adc_channel(0);
        int32_t filtered = filter_update(&adc_filter, raw_adc);
        /* Use 'filtered' value for your application */
    }
}

ADC Interface Considerations

The ADC is where the conditioned analog signal becomes a digital value. Several factors affect measurement quality at this stage:

Sampling rate: The Nyquist theorem requires sampling at least 2x the highest signal frequency. For a temperature sensor changing at 0.1Hz, a 1Hz sample rate is sufficient. For vibration monitoring at 1kHz, you need at least 2kHz sampling. In practice, sample at 5-10x the signal frequency for clean reconstruction.

Resolution: A 12-bit ADC divides the input range into 4096 steps. With a 3.3V reference, each step is 0.8mV. If your conditioned sensor signal spans only 0-500mV, you are using only 625 of the 4096 steps — effectively reducing your resolution to about 9.3 bits. This is why amplification to fill the ADC range is important.

Anti-aliasing filter: Any noise above half the sampling frequency will be folded back (aliased) into the measurement band, appearing as false signal content that cannot be removed by software filtering. An analog low-pass filter (the RC filter discussed earlier) placed before the ADC input with a cutoff below half the sampling rate prevents this. This anti-aliasing filter is the one filter that absolutely cannot be replaced by software.

Reference voltage: ADC accuracy depends on the stability of the reference voltage. The MCU’s VCC is often noisy from digital switching. For precision measurements, use an external voltage reference (e.g., REF3033 for 3.3V) or the MCU’s internal bandgap reference if available. A 1% error in the reference voltage causes a 1% error in every measurement.

📖 Related: LM35 Temperature Sensor: Working, Circuit, and Arduino Code

Going further: Once your hardware-side filtering is in place, residual noise needs software cleanup — see Filtering Noisy ADC Readings for the practical embedded techniques.

Going further: A real-world example: the FlexiForce conditioning circuit uses this non-inverting op-amp topology with a 2.2 kΩ feedback resistor.

Leave a Reply

Your email address will not be published. Required fields are marked *