MCU & FPGA DSP Mediana em medições via ADC e o algoritmo de seleção por partição (Quickselect)

Mediana em medições via ADC e o algoritmo de seleção por partição (Quickselect)

Em medições reais com ADC (Conversor Analógico–Digital), ruído impulsivo e leituras “espúrias” aparecem com frequência: comutação de fonte, EMI (Interferência Eletromagnética), contato ruim, saturação momentânea do front-end, etc. Se você faz apenas média aritmética, um único pico pode arrastar o resultado. A mediana, por outro lado, é um estimador robusto: ela “ignora” a magnitude dos extremos e depende basicamente da posição (ordem) dos valores. Isso a torna excelente para filtrar leituras em janelas deslizantes (por exemplo, 9, 15 ou 31 amostras), entregando um valor estável sem “amassar” tanto degraus quanto um passa-baixas forte faria.

A ideia prática em firmware é simples: você acumula N amostras do ADC em um buffer, calcula a mediana do buffer e usa esse valor como leitura filtrada. O detalhe é que ordenar tudo (por exemplo, com quicksort) custa mais do que precisamos. Para achar a mediana, basta encontrar o elemento “do meio” na ordem — e isso pode ser feito com seleção por partição, também conhecida como Quickselect.


Fórmulas: o que é a mediana (caso ímpar e par)

Considere uma janela com N amostras \(x_0, x_1, \dots, x_{N-1}\). Imagine a versão ordenada dessas amostras \(x_{(0)} \le x_{(1)} \le \dots \le x_{(N-1)}\), onde \(x_{(k)}\) é o k-ésimo menor valor.

Quando N é ímpar, a mediana é o elemento central:

\[
\mathrm{med}(x) = x_{\left(\frac{N-1}{2}\right)}
\]

Quando N é par, há duas convenções comuns. A mais usada em processamento numérico é a média dos dois centrais:

\[
\mathrm{med}(x) = \frac{ x_{\left(\frac{N}{2}-1\right)} + x_{\left(\frac{N}{2}\right)} }{2}
\]

Em ADC (inteiro), essa divisão por 2 costuma ser feita com cuidado para não estourar o tipo (por isso somamos em 32 bits e só depois dividimos).


Por que “seleção por partição” resolve melhor que ordenar tudo

Ordenar N valores tipicamente custa \(O(N \log N)\). Já o Quickselect encontra o k-ésimo menor em tempo médio (O(N)), porque ele faz o mesmo “corte” do quicksort (partição em torno de um pivô), mas só continua recursivamente/iterativamente no lado que contém o índice k. Para a mediana, você quer \(k = (N-1)/2\) (ou os dois centrais se N for par).

Para firmware, isso é valioso: menos ciclos, menos consumo, e você pode rodar a cada janela sem pesar no tempo real. Em geral, o algoritmo trabalha in-place (ele rearranja o buffer). Se você precisa preservar as amostras originais, basta copiar a janela para um buffer temporário e aplicar o Quickselect nesse temporário.


Código em C: Quickselect iterativo + mediana para ADC (uint16_t)

Abaixo vai uma implementação iterativa (evita recursão, o que é mais amigável a microcontroladores), com partição estilo Lomuto. Ela funciona bem para medianas em janelas pequenas/médias (bem típico em ADC). Está tudo comentado e pronto para colar em um projeto bare-metal/RTOS.

#include <stdint.h>
#include <stddef.h>

/**
 * @brief Troca dois elementos uint16_t.
 */
static inline void swap_u16(uint16_t *a, uint16_t *b)
{
    uint16_t t = *a;
    *a = *b;
    *b = t;
}

/**
 * @brief Particiona o array no intervalo [left, right] usando o pivô em pivot_index.
 *
 * Após a partição:
 * - todos os elementos < pivot ficam à esquerda,
 * - todos os elementos >= pivot ficam à direita,
 * - a função retorna a posição final do pivô.
 *
 * @param a Array de amostras (será rearranjado in-place).
 * @param left Índice inicial do intervalo.
 * @param right Índice final do intervalo.
 * @param pivot_index Índice do pivô escolhido.
 * @return size_t Posição final do pivô após a partição.
 */
static size_t partition_lomuto_u16(uint16_t *a, size_t left, size_t right, size_t pivot_index)
{
    const uint16_t pivot = a[pivot_index];

    // Move pivô para o final para particionar mais facilmente
    swap_u16(&a[pivot_index], &a[right]);

    size_t store = left;

    for (size_t i = left; i < right; i++)
    {
        if (a[i] < pivot)
        {
            swap_u16(&a[i], &a[store]);
            store++;
        }
    }

    // Coloca o pivô na posição final
    swap_u16(&a[store], &a[right]);
    return store;
}

/**
 * @brief Escolha simples de pivô: "mediana de três" (left, mid, right).
 *
 * Isso reduz a chance de pior caso em dados parcialmente ordenados,
 * algo comum em ADC com rampa lenta.
 */
static size_t pivot_median_of_three_u16(uint16_t *a, size_t left, size_t right)
{
    size_t mid = left + (right - left) / 2;

    uint16_t x = a[left];
    uint16_t y = a[mid];
    uint16_t z = a[right];

    // Retorna o índice da mediana entre x, y, z
    if ((x <= y && y <= z) || (z <= y && y <= x)) return mid;
    if ((y <= x && x <= z) || (z <= x && x <= y)) return left;
    return right;
}

/**
 * @brief Quickselect iterativo: encontra o k-ésimo menor elemento (0-based).
 *
 * @param a Array (será rearranjado in-place).
 * @param n Tamanho do array.
 * @param k Índice (0..n-1) do elemento desejado na ordem crescente.
 * @return uint16_t Valor do k-ésimo menor elemento.
 */
uint16_t quickselect_k_u16(uint16_t *a, size_t n, size_t k)
{
    size_t left = 0;
    size_t right = n - 1;

    while (left <= right)
    {
        size_t pivot_index = pivot_median_of_three_u16(a, left, right);
        size_t pivot_final = partition_lomuto_u16(a, left, right, pivot_index);

        if (pivot_final == k)
        {
            return a[pivot_final];
        }
        else if (k < pivot_final)
        {
            // Continua só no lado esquerdo
            if (pivot_final == 0) break; // proteção contra underflow
            right = pivot_final - 1;
        }
        else
        {
            // Continua só no lado direito
            left = pivot_final + 1;
        }
    }

    // Em condições normais não chega aqui; retorno defensivo
    return a[k];
}

/**
 * @brief Calcula a mediana de uma janela ADC (uint16_t).
 *
 * Observação importante: o Quickselect altera o array.
 * Se você precisa preservar o buffer original, passe uma cópia.
 *
 * @param window Buffer de N amostras.
 * @param n Número de amostras (N).
 * @return uint16_t Mediana (para N par, retorna a média dos dois centrais).
 */
uint16_t median_adc_u16_inplace(uint16_t *window, size_t n)
{
    if (n == 0) return 0;

    if (n & 1u)
    {
        // N ímpar: elemento central
        size_t k = (n - 1u) / 2u;
        return quickselect_k_u16(window, n, k);
    }
    else
    {
        // N par: média dos dois centrais
        size_t k1 = (n / 2u) - 1u;
        size_t k2 = (n / 2u);

        uint16_t m1 = quickselect_k_u16(window, n, k1);
        uint16_t m2 = quickselect_k_u16(window, n, k2);

        // Soma em 32 bits para evitar overflow e divide por 2
        uint32_t sum = (uint32_t)m1 + (uint32_t)m2;
        return (uint16_t)(sum / 2u);
    }
}

Como usar em um fluxo típico de ADC (janela deslizante)

Em prática embarcada, você costuma ter um buffer circular com N amostras mais recentes. Quando a janela “fecha” (por exemplo, a cada amostra ou a cada bloco DMA), você copia para um buffer temporário e calcula a mediana nele, preservando o circular.

Uma versão de uso (exemplo) ficaria assim:

#include <string.h> // memcpy

#define N  15

static uint16_t adc_ring[N];
static uint16_t tmp[N];
static size_t wr = 0;

/**
 * @brief Atualiza a janela com uma nova amostra e devolve a mediana.
 *
 * @param sample Nova leitura ADC.
 * @return uint16_t Mediana das últimas N amostras.
 */
uint16_t adc_update_and_median(uint16_t sample)
{
    adc_ring[wr] = sample;
    wr = (wr + 1u) % N;

    // Copia a janela para processar sem bagunçar o ring
    memcpy(tmp, adc_ring, sizeof(tmp));

    return median_adc_u16_inplace(tmp, N);
}

Esse padrão é especialmente bom quando você tem ruídos impulsivos: a mediana remove “picos” isolados com eficiência sem a latência de um filtro passa-baixas forte.


Observações de engenharia para ficar “bom de firmware”

Como o Quickselect rearranja dados, o custo de memória vira uma decisão de projeto: se você pode destruir a janela, roda direto; se não pode, copie. Em microcontroladores pequenos, dá para manter um tmp[] estático do tamanho da janela. Para tempo real, prefira N pequeno (9, 15, 31) e pivô “mediana de três” como no código para reduzir casos ruins quando o sinal muda lentamente. Se você estiver usando FreeRTOS, essa função pode rodar numa tarefa de processamento após um evento do DMA, mantendo o ISR enxuto.

Se você quiser ir além, dá para combinar mediana com média: primeiro aplica mediana para “limpar” impulsos e depois uma média móvel curta para reduzir ruído branco, o que costuma ficar excelente para sensores resistivos, NTC e shunt (onde spikes de comutação aparecem).


0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x