Улучшенный измеритель Емкости на Arduino Nano и OLED- Расширение диапазона

Как расширить диапазон измерения и повысить точность?

Использование внутренних таймеров микроконтроллера значительно повысит точность измерения малых ёмкостей, так как они позволяют измерять временные интервалы с гораздо более высоким разрешением, чем micros() (которая основана на прерываниях от таймеров, но имеет свой шаг в 4 мкс).

Мы будем использовать один из 16-битных таймеров Arduino Nano (например, Timer1). Он может измерять время с разрешением до 62.5 наносекунд (для Arduino на 16 МГц) или даже меньше при более высокой тактовой частоте.

Обновленная Схема Подключения:

Схема остается практически такой же, но мы будем использовать немного другие возможности пинов. Используем пин, который может быть связан с входом таймера (Input Capture Unit — ICU) для более точного измерения. Для Arduino Nano (ATMega328P), пин D8 связан с Timer1 Input Capture (ICP1).

  • OLED-дисплей:
    • VCC -> Arduino 5V
    • GND -> Arduino GND
    • SDA -> Arduino A4 (для I2C)
    • SCL -> Arduino A5 (для I2C)
  • Конденсатор и Резистор (RC-цепь):
    • Arduino Digital Pin 2 (для разрядки/управления) -> Один конец Резистора (R)
    • Другой конец Резистора (R) -> Один конец Конденсатора (C)
    • Другой конец Конденсатора (C) -> Arduino GND
    • Важно: Точка соединения Резистора (R) и Конденсатора (C) -> Arduino Digital Pin 8 (ICP1). Этот пин будет использоваться для измерения времени заряда.

Принцип измерения с Timer1 Input Capture (ICP1):

Мы не будем ждать порогового значения на аналоговом входе. Вместо этого, мы будем использовать пин D8 как цифровой вход, и как только напряжение на нём поднимется до логической единицы (примерно 2.5-3В для 5В логики), это вызовет прерывание Input Capture. Таймер запишет свое текущее значение (сколько тактов прошло с его сброса) в регистр. Это позволит нам измерить время заряда до этого логического порога с высокой точностью.

Необходимые библиотеки:

Те же самые:

  1. Adafruit GFX Library
  2. Adafruit SSD1306

Программа для Arduino Nano с использованием Timer1:

#include <Wire.h>           // Для связи по I2C (для OLED-дисплея)
#include <Adafruit_GFX.h>   // Базовая графическая библиотека
#include <Adafruit_SSD1306.h> // Драйвер для SSD1306 OLED-дисплея

// Определяем размеры OLED-дисплея
#define SCREEN_WIDTH 128    // Ширина в пикселях
#define SCREEN_HEIGHT 64    // Высота в пикселях

// Объект дисплея
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

// ---------- Пины для измерения конденсатора ----------
const int chargeDischargePin = 2; // Цифровой пин для зарядки/разрядки конденсатора
const int capturePin = 8;         // Пин Timer1 ICP1 (Input Capture Pin)

// ---------- Параметры измерения ----------
// Номинал резистора в Омах. ВАЖНО: измерьте точное значение мультиметром!
// Для нФ: 10 кОм - 100 кОм (10000 - 100000 Ом)
// Для пФ: 100 кОм - 1 МОм (100000 - 1000000 Ом)
const float R_VALUE_OHM = 100000.0; // Например, 100 кОм = 100000 Ом

// Пороговое напряжение для срабатывания логической единицы
// Обычно, около 0.6 * VCC. Для 5В логики, это 3В.
// Время заряда до 63.2% от VCC (1 RC)
// Время заряда до 0.6 * VCC (логической единицы) будет немного меньше 1 RC.
// Для простоты будем считать, что время до логической 1 соответствует 1 RC.
// Для более высокой точности, можно калибровать или использовать точное значение тау (t = -R*C*ln(1 - V_thresh/V_supply)).
// Пусть V_thresh = 3.0V, V_supply = 5.0V. t = -R*C*ln(1 - 3.0/5.0) = -R*C*ln(0.4) = -R*C*(-0.916) = 0.916*R*C
// То есть, измеренное время нужно будет делить на 0.916, чтобы получить 1 RC.
// Мы используем 16 МГц таймер. 1 такт = 1/16,000,000 сек = 0.0625 мкс = 62.5 нс.

// ---------- Переменные для измерения ----------
volatile unsigned long captureTime = 0; // Переменная для сохранения времени захвата (volatile для ISR)
volatile bool captureDone = false;      // Флаг завершения захвата (volatile для ISR)

// ---------- Обработчик прерывания Input Capture (ISR) ----------
// Срабатывает по изменению уровня на пине ICP1 (D8)
ISR(TIMER1_CAPT_vect) {
  if (bit_is_set(TCCR1B, ICES1)) { // Проверяем, если прерывание по восходящему фронту (зарядка)
    captureTime = ICR1; // Захватываем текущее значение таймера
    captureDone = true; // Устанавливаем флаг, что захват произошел
    TCCR1B &= ~(1 << ICES1); // Переключаем на захват по нисходящему фронту для следующего цикла (если нужен разряд)
  } else { // Если прерывание по нисходящему фронту (разрядка)
    // Здесь можно обрабатывать разрядку, если нужно.
    // Для нашего случая не используем, но оставим для понимания.
    TCCR1B |= (1 << ICES1); // Переключаем на захват по восходящему фронту
  }
}

void setup() {
  Serial.begin(115200); // Увеличим скорость для лучшей отладки
  Serial.println("Инициализация...");

  // Инициализация OLED-дисплея
  if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
    Serial.println(F("Ошибка инициализации SSD1306!"));
    for (;;) ;
  }
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println("Калибровка...");
  display.display();
  delay(1000);

  // Настройка пинов
  pinMode(chargeDischargePin, OUTPUT);
  digitalWrite(chargeDischargePin, LOW); // Разряжаем конденсатор перед стартом
  pinMode(capturePin, INPUT); // Пин D8 как вход

  // ---------- Настройка Timer1 для измерения времени ----------
  TCCR1A = 0; // Сброс регистров A
  TCCR1B = 0; // Сброс регистров B
  TCNT1 = 0;  // Сброс счетчика таймера

  // CTC Mode (Clear Timer on Compare Match) не нужен, используем Normal Mode
  // WGM13:0 = 0 (Normal mode)

  // Настройка делителя (Prescaler):
  // CS12 CS11 CS10
  // 0    0    1   -> no prescaling (clk_I/O / 1) -> 16 MHz / 1 = 16M тактов/сек -> 62.5 нс/такт
  // 0    1    0   -> clk_I/O / 8 -> 16 MHz / 8 = 2M тактов/сек -> 0.5 мкс/такт
  // 0    1    1   -> clk_I/O / 64 -> 16 MHz / 64 = 250K тактов/сек -> 4 мкс/такт
  // 1    0    0   -> clk_I/O / 256
  // 1    0    1   -> clk_I/O / 1024
  // Выберем делитель на 8 для начала (0.5 мкс/такт) - хороший компромисс между диапазоном и точностью
  TCCR1B |= (1 << CS11); // Делитель на 8

  // Настройка прерывания Input Capture:
  // ICES1 = 1: Захват по восходящему фронту (когда конденсатор заряжается и напряжение растет)
  TCCR1B |= (1 << ICES1); // Устанавливаем захват по восходящему фронту
  TIMSK1 |= (1 << ICIE1); // Разрешаем прерывание Input Capture

  Serial.println("Система готова. Начните измерение.");
  display.clearDisplay();
  display.setCursor(0, 0);
  display.println("Готово к изм.");
  display.display();
}

void loop() {
  // 1. Разрядка конденсатора:
  digitalWrite(chargeDischargePin, LOW); // Подключаем к земле
  pinMode(chargeDischargePin, OUTPUT); // Убеждаемся, что пин в режиме выхода
  delay(10); // Даем время на разрядку (достаточно для большинства конденсаторов)

  // 2. Сброс таймера и флага прерывания:
  TCNT1 = 0;           // Сбрасываем счетчик Timer1
  captureDone = false; // Сбрасываем флаг захвата

  // 3. Запуск зарядки и ожидание захвата:
  pinMode(chargeDischargePin, INPUT); // Отключаем пин от GND, позволяем конденсатору заряжаться через резистор.
                                       // Теперь конденсатор заряжается через резистор R_VALUE_OHM
                                       // от pull-up резистора аналогового пина A0, или внешнего источника.
                                       // Более правильно:
  // pinMode(chargeDischargePin, INPUT_PULLUP); // Если хотим, чтобы заряд шел от VCC через R и pull-up
  // Но в нашей схеме заряд идет от VCC через R к конденсатору,
  // а chargeDischargePin просто отключается от земли.
  // Заряд идет от пина A0, если он в режиме INPUT.
  // Самый надежный способ заряда:
  // pinMode(chargeDischargePin, OUTPUT);
  // digitalWrite(chargeDischargePin, HIGH); // Подаем HIGH для заряда
  // Но тогда резистор должен быть между chargeDischargePin и конденсатором, а capturePin на конденсаторе.

  // Учитывая схему: chargeDischargePin --- R --- C --- GND.
  // capturePin (D8) подключен между R и C.
  // Для зарядки: chargeDischargePin должен быть HIGH.
  // Для разрядки: chargeDischargePin должен быть LOW.

  // Переделываем логику зарядки-разрядки для ясности:
  // Разрядка (была в начале loop)
  digitalWrite(chargeDischargePin, LOW);
  pinMode(chargeDischargePin, OUTPUT);
  delay(10);

  // Сброс таймера перед началом заряда
  TCNT1 = 0;
  captureDone = false;
  // Убедимся, что прерывание по восходящему фронту активно
  TCCR1B |= (1 << ICES1); // Устанавливаем захват по восходящему фронту

  // Запуск заряда
  digitalWrite(chargeDischargePin, HIGH); // Начинаем заряд конденсатора
  
  // 4. Ожидание срабатывания прерывания Input Capture:
  // Максимальное время ожидания (timeout) для очень больших емкостей.
  // Например, 2 секунды (2,000,000 микросекунд).
  unsigned long timeout = 2000000;
  unsigned long startWaitTime = micros();

  while (!captureDone && (micros() - startWaitTime < timeout)) {
    // Ждем, пока сработает прерывание или наступит таймаут
  }

  // 5. Расчет ёмкости:
  float capacitance_pF = 0; // Емкость в пикофарадах
  if (captureDone) {
    // Измеренное время в тактах таймера
    unsigned long measuredTicks = captureTime;

    // Время в наносекундах (62.5 нс на такт при делителе 1, или 500 нс на такт при делителе 8)
    // Если делитель на 8 (TCCR1B |= (1 << CS11)), то 1 такт = 0.5 мкс = 500 нс
    const float usPerTick = 0.5; // (1000000 / (16000000 / 8)) = 0.5 мкс/такт
    float measuredTime_us = measuredTicks * usPerTick; // Время в микросекундах

    // Формула для RC: tau = R * C
    // Мы измерили время (t), когда напряжение достигло логической единицы.
    // Если это время соответствует 1 RC-постоянной (тау), то C = t / R.
    // Если t в мкс, R в Омах, то C будет в микрофарадах (мкФ).
    // Для более точного расчета, т.к. порог не 63.2%:
    // t = -R * C * ln(1 - V_threshold / V_supply)
    // C = -t / (R * ln(1 - V_threshold / V_supply))
    // C = t / (R * |ln(1 - V_threshold / V_supply)|)
    // Примем V_threshold ~ 3V, V_supply = 5V. ln(1 - 3/5) = ln(0.4) = -0.916.
    // Значит, C = measuredTime_us / (R_VALUE_OHM * 0.916) (для мкФ, если R в Омах, время в мкс)

    // Однако, для простоты и учитывая, что логический порог близок к 0.632 Vcc,
    // можно использовать C = время / R.
    // Если measuredTime_us в микросекундах (us) и R_VALUE_OHM в Омах (Ohm),
    // то ёмкость будет в микрофарадах (uF).
    // Для пикофарад (pF): C(pF) = measuredTime_us * 1,000,000 / R_VALUE_OHM
    // или более просто: C(pF) = measuredTime_us / (R_VALUE_OHM / 1000000.0)
    // C(pF) = measuredTime_us * (1000000.0 / R_VALUE_OHM)

    // Принимаем, что measuredTime_us соответствует RC-постоянной.
    // C (Фарад) = measuredTime_us (секунд) / R_VALUE_OHM (Ом)
    // Чтобы получить пикофарады (1 Ф = 10^12 пФ, 1 мкс = 10^-6 с):
    // C (пФ) = (measuredTime_us * 1e-6) / R_VALUE_OHM * 1e12
    // C (пФ) = measuredTime_us * 1e6 / R_VALUE_OHM
    
    // Давайте сделаем проще и точнее:
    // Время в секундах: measuredTime_us / 1000000.0
    // C (Фарад) = (measuredTime_us / 1000000.0) / R_VALUE_OHM
    // C (пФ) = C (Фарад) * 1000000000000.0
    // C (пФ) = (measuredTime_us / 1000000.0) / R_VALUE_OHM * 1000000000000.0
    // C (пФ) = measuredTime_us * 1000000.0 / R_VALUE_OHM  (ЭТА ФОРМУЛА НАИБОЛЕЕ ВЕРНА)
    
    capacitance_pF = measuredTime_us * 1000000.0 / R_VALUE_OHM;
    
    // Калибровочный множитель, если порог логической 1 не точно 63.2%
    // Для V_threshold = 3V, V_supply = 5V, множитель 1 / 0.916 = 1.0917
    // capacitance_pF *= 1.0917; // Раскомментировать для более точной калибровки

  } else {
    // Таймаут - конденсатор слишком большой или не подключен
    capacitance_pF = 0.0; // Или какой-то индикатор ошибки
    Serial.println("Таймаут измерения. Конденсатор слишком большой или не подключен.");
  }
  
  // 6. Вывод на OLED и Serial:
  display.clearDisplay();
  display.setCursor(0, 0);
  display.setTextSize(1);
  display.setTextColor(SSD1306_WHITE);

  if (capacitance_pF > 0.0 && captureDone) {
    if (capacitance_pF >= 1000000.0) { // Если более 1000000 пФ (1 мкФ)
        display.print("C: ");
        display.print(capacitance_pF / 1000000.0, 2);
        display.println(" uF");
        Serial.print("C: "); Serial.print(capacitance_pF / 1000000.0, 2); Serial.println(" uF");
    } else if (capacitance_pF >= 1000.0) { // Если более 1000 пФ (1 нФ)
        display.print("C: ");
        display.print(capacitance_pF / 1000.0, 2);
        display.println(" nF");
        Serial.print("C: "); Serial.print(capacitance_pF / 1000.0, 2); Serial.println(" nF");
    } else { // Менее 1 нФ (в пФ)
        display.print("C: ");
        display.print(capacitance_pF, 1);
        display.println(" pF");
        Serial.print("C: "); Serial.print(capacitance_pF, 1); Serial.println(" pF");
    }
  } else {
    display.println("Нет C / Ошибка");
    Serial.println("Нет C / Ошибка");
  }

  display.display();
  delay(1000); // Задержка 1 секунда между измерениями
}

Объяснение изменений и новых концепций:

  1. Пин capturePin (D8): Теперь мы используем именно цифровой пин 8, который подключен к функции Input Capture (ICP1) Timer1.
  2. Глобальные volatile переменные:
    • volatile unsigned long captureTime = 0;: Хранит количество тактов таймера, захваченное во время прерывания. volatile указывает компилятору не оптимизировать доступ к этой переменной, так как она может меняться неожиданно (из прерывания).
    • volatile bool captureDone = false;: Флаг, который устанавливается в true после успешного захвата. Также volatile.
  3. ISR(TIMER1_CAPT_vect): Это функция обработчика прерывания Input Capture.
    • Она срабатывает автоматически, когда на пине ICP1 происходит выбранное событие (например, восходящий фронт).
    • captureTime = ICR1;: ICR1 — это регистр Input Capture, который автоматически сохраняет текущее значение счетчика Timer1 в момент срабатывания прерывания. Это очень точный способ измерения времени.
    • captureDone = true;: Устанавливаем флаг.
    • TCCR1B &= ~(1 << ICES1); и TCCR1B |= (1 << ICES1);: Переключает тип фронта (восходящий/нисходящий), по которому происходит захват. Это может быть полезно для измерения длительности импульса, но для нас важно просто захватить момент зарядки. Мы всегда будем ждать восходящий фронт, поэтому после захвата можно сбросить его обратно на восходящий, если нужно. В данном коде мы его не меняем после первого захвата, так как всегда ждем восходящий.
  4. setup():
    • Serial.begin(115200);: Увеличиваем скорость последовательного порта.
    • Настройка Timer1:
      • TCCR1A = 0; TCCR1B = 0; TCNT1 = 0;: Обнуляем управляющие регистры и счетчик таймера.
      • TCCR1B |= (1 << CS11);: Устанавливаем делитель частоты таймера на 8. Это дает нам разрешение 0.5 микросекунд (мкс) на один такт таймера (16 МГц / 8 = 2 МГц, 1/2МГц = 0.5 мкс). Можно использовать делитель на 1 ((1 << CS10)) для разрешения 62.5 нс, но тогда максимальное время измерения уменьшится.
      • TCCR1B |= (1 << ICES1);: Устанавливаем бит ICES1, чтобы захват происходил по восходящему фронту (когда напряжение на конденсаторе поднимается и достигает логической единицы).
      • TIMSK1 |= (1 << ICIE1);: Разрешаем прерывание Input Capture.
  5. loop():
    • Разрядка: Сначала chargeDischargePin устанавливается в LOW (выход), чтобы гарантированно разрядить конденсатор.
    • Запуск заряда: chargeDischargePin переключается в HIGH (выход), начиная заряд конденсатора через резистор.
    • Ожидание захвата: Цикл while (!captureDone && ...) ждет, пока прерывание не сработает (captureDone станет true) или не наступит таймаут (для очень больших конденсаторов или отсутствия конденсатора).
    • Расчет ёмкости:
      • measuredTicks = captureTime;: Получаем количество тактов из прерывания.
      • measuredTime_us = measuredTicks * usPerTick;: Переводим такты в микросекунды.
      • capacitance_pF = measuredTime_us * 1000000.0 / R_VALUE_OHM;: Главная формула для расчета ёмкости. Если measuredTime_us в микросекундах, а R_VALUE_OHM в Омах, то результат будет в пикофарадах (пФ). Это потому что: C(F)=t(s)/R(Omega), а 1F=1012pF и 1s=106mus. Подставляя, получим C(pF)=t(mus)times106/R(Omega).
    • Отображение: Результаты выводятся в удобном формате (пФ, нФ, мкФ).

Преимущества этого подхода:

  • Значительно более высокая точность: Разрешение до 0.5 мкс (или 62.5 нс, если убрать делитель) позволяет измерять гораздо меньшие ёмкости.
  • Неблокирующий код: Основной цикл loop() не блокируется ожиданием, он просто проверяет флаг captureDone. Это позволяет Arduino выполнять другие задачи параллельно.
  • Автоматическое срабатывание: Прерывание захвата автоматически фиксирует точный момент изменения напряжения.

Калибровка и точность:

  • R_VALUE_OHM: Всегда используйте точное измеренное значение вашего резистора.
  • Пороговое напряжение: Помните, что Input Capture срабатывает, когда напряжение на пине достигает логической единицы (около 2.5-3В для 5В Arduino). Это не ровно 63.2% от VCC (3.16В). Для очень высокой точности нужно будет вычислить поправочный коэффициент, исходя из t=−RtimesCtimesln(1−V_textпорог/V_textпитания), как я упомянул в комментариях к коду.

Этот код позволит вам измерять ёмкости от нескольких десятков пикофарад (пФ) с достаточной точностью.

Author: admin

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *