Добавить в избранное

АЦП, EEPROM и спящие режимы AVR: примеры на чистом C

Чтение ≈ 5 мин

Три важнейших подсистемы AVR в одном материале с кодом на чистом C: 10-битный АЦП ATmega328P (опорное напряжение, усреднение, измерение батареи через внутреннее 1.1V), работа с EEPROM без износа и спящие режимы с пробуждением по WDT и прерываниям для устройств на батарейках.

АЦП в ATmega328P: что под капотом

10-битный последовательно-приближающийся АЦП с 8-канальным мультиплексором (ADC0..ADC7 — ножки PC0..PC5 на DIP-корпусе). Опорное напряжение выбирается: AREF (внешнее), AVCC (по питанию) или внутреннее прецизионное 1,1 В.

Ключевые регистры:

  • ADMUX — выбор опорного (REFS1:0) и канала (MUX3:0), бит ADLAR для выравнивания результата влево.
  • ADCSRA — включение (ADEN), старт преобразования (ADSC), автотриггер (ADATE), предделитель тактирования (ADPS2:0).
  • ADC — 16-битный регистр результата (ADCL + ADCH).

Тактирование АЦП должно быть 50–200 кГц: при F_CPU 16 МГц предделитель 128 даёт 125 кГц — в самый раз.

Однократное преобразование (polling)

#define F_CPU 16000000UL
#include <avr/io.h>

static void adc_init(void)
{
    // опорное = AVCC (REFS0=1), канал ADC0
    ADMUX  = (1 << REFS0);
    // ADEN=1, предделитель 128
    ADCSRA = (1 << ADEN) | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
}

static uint16_t adc_read(uint8_t channel)
{
    ADMUX = (ADMUX & 0xF0) | (channel & 0x0F); // выбрать канал
    ADCSRA |= (1 << ADSC);                     // старт
    while (ADCSRA & (1 << ADSC))
        ;                                      // ждём ~104 мкс
    return ADC;                                // 0..1023
}

Первое преобразование после включения длится 25 тактов АЦП (инициализация), последующие — 13. Перевод в милливольты:

uint16_t mv = (uint32_t)adc_read(0) * 5000 / 1023;   // при Vref=5В

Умножение uint32_t обязательно до деления — 723 * 5000 переполняет uint16.

Усреднение против шума

Одиночное чтение шумит на ±2–3 LSB. Классика — 16 выборок и сдвиг на 4:

static uint16_t adc_read_avg(uint8_t ch, uint8_t samples)
{
    uint32_t acc = 0;
    for (uint8_t i = 0; i < samples; i++)
        acc += adc_read(ch);
    return (uint16_t)(acc / samples);
}

Ещё лучше — экспоненциальный фильтр: y = y + (x - y) / K. Дёшево, не хранит массив:

static uint16_t filtered = 0;
filtered += ((int16_t)adc_read(0) - (int16_t)filtered) / 8;

АЦП по прерыванию и в фоне

Автозапуск (free-running): ADATE=1, источник — continuous. Преобразования идут сами, в ISR забираем результат:

volatile uint16_t g_adc_result = 0;

ISR(ADC_vect)
{
    g_adc_result = ADC;
}

static void adc_freerun_init(void)
{
    ADMUX  = (1 << REFS0);                       // AVCC, канал 0
    ADCSRA = (1 << ADEN) | (1 << ADATE) | (1 << ADIE)
           | (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0);
    ADCSRB = 0;                                  // free running
    ADCSRA |= (1 << ADSC);                       // погнали
}

Main просто читает g_adc_result — всегда свежие данные без ожидания.

Измерение напряжения батареи без делителя

Фишка AVR: можно переключить вход АЦП на внутренний bandgap 1,1 В, а опорным сделать VCC — тогда по показанию вычисляется само напряжение питания:

static uint16_t read_vcc_mv(void)
{
    ADMUX = (1 << REFS0)              // опорное = AVCC
          | (1 << MUX3) | (1 << MUX2) | (1 << MUX1); // канал = 1.1V bandgap
    _delay_ms(2);                     // bandgap стабилизируется
    ADCSRA |= (1 << ADSC);
    while (ADCSRA & (1 << ADSC));
    return (uint16_t)(1100UL * 1023 / ADC);   // реальное Vcc в мВ
}

Идеально для устройств с питанием напрямую от Li-Ion или 3×AA без стабилизатора — следите за разрядом и засыпайте при критическом пороге.

EEPROM: данные после выключения

В ATmega328P — 1024 байта EEPROM, ~100 000 циклов записи на ячейку. avr-libc даёт готовые функции <avr/eeprom.h>:

#include <avr/eeprom.h>

// переменная, привязанная к EEPROM при прошивке
uint16_t EEMEM ee_counter;

int main(void)
{
    uint16_t cnt = eeprom_read_word(&ee_counter);
    cnt++;
    eeprom_update_word(&ee_counter, cnt);   // пишет только при изменении!
}

Важные функции:

  • eeprom_read_byte/word/dword/block — чтение;
  • eeprom_write_* — запись всегда;
  • eeprom_update_* — запись только если байт изменился — главный инструмент против износа, используйте всегда вместо write.

Сохранение структуры настроек:

typedef struct {
    uint8_t  brightness;
    uint8_t  mode;
    uint16_t threshold;
    uint8_t  crc;
} settings_t;

settings_t EEMEM ee_settings;

static void settings_save(const settings_t *s)
{
    eeprom_update_block(s, &ee_settings, sizeof(settings_t));
}

static uint8_t settings_load(settings_t *s)
{
    eeprom_read_block(s, &ee_settings, sizeof(settings_t));
    uint8_t crc = crc8((uint8_t *)s, sizeof(settings_t) - 1);
    return crc == s->crc;                    // 0 = данные битые, взять дефолт
}

CRC (или хотя бы магическое байт-слово вроде 0xA5) обязательна: первый запуск на чистом чипе вернёт 0xFF во всех байтах.

Спящие режимы: жизнь от батарейки

ATmega328P в активе кушает ~12 мА на 16 МГц, в power-down — 0,1 мкА. Разница в 100 000 раз — именно она определяет, проработает датчик от CR2032 неделю или 5 лет.

Режимы по убыванию потребления:

  • idle — спит CPU, вся периферия работает (экономия ~40%);
  • adc-noise-reduction — для точных измерений;
  • power-save — как power-down, но Timer2 (асинхронный) тикает — часы реального времени;
  • standby — power-down + запущенный кварц (быстрый старт);
  • power-down — всё выключено; будят только внешние прерывания по уровню, PCINT, TWI-адрес и WDT.

Минимальный сон с пробуждением по WDT:

#include <avr/sleep.h>
#include <avr/wdt.h>
#include <avr/interrupt.h>

ISR(WDT_vect) { /* просто пробуждение */ }

static void wdt_init_8s(void)
{
    cli();
    wdt_reset();
    WDTCSR |= (1 << WDCE) | (1 << WDE);       // вход в конфиг-режим
    WDTCSR  = (1 << WDIE)                     // прерывание (не сброс!)
            | (1 << WDP3) | (1 << WDP0);      // 8 секунд
    sei();
}

int main(void)
{
    wdt_init_8s();
    ADCSRA = 0;                               // АЦП выкл — экономия
    // выключить неиспользуемую периферию:
    PRR = (1 << PRTWI) | (1 << PRUSART0) | (1 << PRSPI) | (1 << PRTIM1);

    while (1) {
        set_sleep_mode(SLEEP_MODE_PWR_DOWN);
        sleep_enable();
        sleep_bod_disable();                  // отключить BOD на время сна
        sei();
        sleep_cpu();                          // ← сон до WDT-прерывания
        sleep_disable();

        // проснулись: измерить, передать, снова спать
        do_measurement();
    }
}

Будильник из 75 циклов по 8 с = 10 минут сна между замерами. Счётчик в volatile инкрементирует ISR(WDT_vect).

Периферия жрёт даже в активе

Регистр PRR выключает тактирование неиспользуемых блоков — до 30% экономии в активном режиме. Также не забывайте:

  • ненужные входы — в pull-up или выходы (плавающий вход потребляет);
  • светодиоды питания на платах Nano/Pro Mini съедают ~2 мА — отпаять;
  • LDO-стабилизатор на плате — тоже потребляет; для батарейки питайте напрямую.

Типовые ошибки

  • AREF замкнут на AVCC внутренним выбором — при REFS=01 не подавайте внешнее напряжение на AREF.
  • Запись EEPROM в цикле — 100 000 циклов изнашиваются за минуты: пишите по событию и через update.
  • WDT как сброс вместо будильника — забыт WDE при настройке даст перезагрузку через 8 с.
  • Забыли sei() перед sleep_cpu() — уснёт навсегда (прерывание не разрешено).
  • Чтение ADC сразу после смены канала — дайте мультиплексору settle-время или сделайте холостое преобразование.

Итог

АЦП + EEPROM + сон = классическая связка автономного датчика: проснулся по WDT → измерил → сравнил с порогом → при изменении сохранил в EEPROM или отправил → снова спать. Такой цикл от CR2032 живёт годами.

Поделиться
Понравился материал?

Комментарии

Свернуть форму

Необходимо Войти или Зарегистрироваться, чтобы оставлять комментарии