Промышленное производство
Промышленный Интернет вещей | Промышленные материалы | Техническое обслуживание и ремонт оборудования | Промышленное программирование |
home  MfgRobots >> Промышленное производство >  >> Manufacturing Technology >> Производственный процесс

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

Компоненты и материалы

Arduino UNO×1
MCP4725 I2C ЦАП (SparkFun Breakout)×1
Линейный регулятор с регулируемым выходом (LM317)×1
Два операционных усилителя (LM358, TI)×1
Резистор 6,8 кОм×2
Резистор 3,3 кОм×1
Резистор 5,1 кОм×1
Резистор 1 кОм×1
Резистор 10 Ом (шунт тока)×1
Реле (управление 5 В)×1
Резистор 20 кОм×1
Блок питания макетной платы 5В/3.3В (SparkFun)×1
Блок питания 17 В (для цепи заряда)×1
I2C LCD 1602 модуль (YwRobot)×1

О проекте

Искал умное зарядное для 9В NiMH (заряжающее за пару часов) — не нашел. Все существующие — «глупые»: неизвестный ток, нет завершения заряда при полном заряде. Такие зарядки перезаряжают батарею и убивают её ресурс. Решил сделать «умное» зарядное.

Первая версия — минимальная: заряд постоянным током, автоматическое завершение по -dV/dt, догонный (trickle) заряд, измерение переданного заряда.

В следующей версии планирую: разряд, измерение емкости, циклы заряд-разряд.

Внимание: заряд большими токами может вызвать взрыв или пожар. Не оставляйте зарядку без присмотра. Не заряжайте щелочные батареи. Тестировалось только на NiMH (используете на свой риск, ответственности не несу). Для других типов батарей нужен другой код.

Теория

Полезные факты для понимания параметров зарядки.

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

Базовые параметры зарядной схемы

9В батарея обычно содержит 7 последовательных элементов (иногда 6 или 8). Регулятор должен выдавать до 8×1,6=12,8 В. Падение на LM317 до 2 В → нужно питание ~15 В (без учета шунта). При макс. токе 200 мА и шунте 10 Ом падение на шунте 2 В → нужно 17 В питания. Глубоко разряженный элемент может иметь очень низкое, даже отрицательное напряжение. Минимум LM317 ≈ 1,2 В.

Схема

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1
Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

Пояснения к схеме

Суть: измеряем ток заряда (падение напряжения на шунте R5 = 10,2 Ом) и регулируем выходное напряжение LM317 через ЦАП MCP4725 до достижения заданного тока. I = V/R.

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1
Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1
Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

Функционал

Кнопок настройки нет — ток заряда задается в коде (charger.ino):

//*************************** Параметры заряда *****************************************
float target_current_mA = 30;              // Ток заряда, мА
float battery_nominal_capacity_mA = 170;   // Номинальная емкость, мАч
float max_time_for_trickle_charge = 6;     // Макс. время догонного заряда, мин (до 600)
//**************************************************************************************

После включения зарядка проверяет наличие батареи. Если есть — зарядает постоянным током до полного заряда. Завершение по отрицательному dV/dt в течение 5 минут. Затем переходит в догонный заряд (C/40). Отключается от батареи по истечении макс. времени догонного заряда.

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1
Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1
Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

На дисплее: 1 — dV/dt, 2 — время заряда (мин) / 1 — время заряда, 2 — переданный заряд (мАч).

Доп. инфо по NiMH:

  1. Energizer Application Manual
  2. Battery University

Код

Проект состоит из нескольких файлов: charger.ino (главный), main.ino (автомат состояний), hw.ino (железо), lcd.ino (дисплей), calculations.ino (математика). Используется библиотека New LiquidCrystal.

charger.ino (основной файл)

// Зарядка 9В NiMH (6, 7 или 8 элементов по 1,25В → номинал 7,5–10В)
#include 
#include 
#include 
#include 
#include 

LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);

#define MCP4725_ADDR 0x60
#define DAC_REF_VOLTAGE 5.0
#define CHARGE_RELAY_PIN 2
#define DISCONNECT_CHARGE_RELAY HIGH
#define CONNECT_CHARGE_RELAY LOW
#define CURRENT_SENSING_SESISTOR_PIN A2
#define CURRENT_SENSING_SESISTOR 10.2
#define BATTERY_VOLTAGE_PIN A0
#define R1 5090
#define R2 19910
#define ADC_REF_VOLTAGE 4.89
#define R3 3300.0
#define R4 6800.0
#define AMP_GAIN (1+R4/R3)

#define BATTERY_GOOD_VOLTAGE_THRESHOLD 7.0
#define MAXIMIM_ALLOWED_CURRENT 200
#define MAXIMUM_BATTERY_VOLTAGE 15.0
#define VOLTAGE_STEP 0.001
#define POINTS_FOR_AVERAGING 100
#define MEASURE2MEASURE_US 5000
#define VOLTAGE_LOG_TIME_MIN 30.0
#define MINUTES_TO_LOG 5
#define TRICKLE_CURRENT_RATIO 40

#define INITIALIZATION 0
#define NO_BATTERY 5
#define BATTERY_CONNECTED 10
#define BATTERY_VOLTAGE_LOW 15
#define CURRENT_RAMP_UP 20
#define CHARGING 30
#define CURRENT_RAMP_DOWN 32
#define TRICKLE 35
#define END_OF_TRICKLE 37
#define UNEXPECTED_CURRENT_FALL 40
#define UNABLE_TO_REACH_CURRENT 60
#define FULLY_CHARGED 70
#define CURRENT_TOO_HIGH 80
#define REGULATION_FAILED 90
#define OVERCURRENT 100
#define OVERVOLTAGE 101
#define FINAL_STATE 200

// ... (переменные состояния, буферы усреднения, таймеры)

void setup() {
  pinMode(CHARGE_RELAY_PIN, OUTPUT);
  disconnect_charging_circuit();
  Wire.begin();
  dac_write_voltage(0);
  Serial.begin(115200);
  last_second = second();
  lcd_last_second = second();
  log_last_second = second();
  Timer1.initialize(MEASURE2MEASURE_US);
  Timer1.attachInterrupt(read_hw);
  averaging_index = 0;
  sec_index = 0; min_index = 0;
  charger_state = 0;
  wanted_dac_voltage = 0;
  last_blf = 1.0;
  trickle_current_mA = battery_nominal_capacity_mA / TRICKLE_CURRENT_RATIO;
  lcd.begin(16,2);
  lcd.backlight();
  lcd.clear();
  update_lcd(F("Power up..."), "");
  delay(1000);
}

float log_battery_voltage() {
  if (log_last_second == second()) return last_blf;
  log_last_second = second();
  sec_log[sec_index] = battery_voltage;
  if (sec_index < 59) sec_index++;
  else {
    if (min_index >= MINUTES_TO_LOG) min_index = 0;
    sec_index = 0;
    float sum_v = 0;
    for (int i=0; i<60; i++) sum_v += sec_log[i];
    float min_average = sum_v / 60.0;
    for (int i=1; i

main.ino (автомат состояний)

void loop() {
  String msg1;
  switch(charger_state) {
    case INITIALIZATION:
      disconnect_charging_circuit(); dac_write_voltage(0); wanted_dac_voltage = 0; delay(100);
      read_status();
      if (battery_voltage > 0.1) {
        charger_state = BATTERY_CONNECTED;
        update_lcd(F("Battery detected"), ""); Serial.println(F("Battery detected")); delay(2000);
      } else {
        Serial.println(F("No battery"));
        update_lcd(F("No battery"), construct_status_string());
        charger_state = NO_BATTERY; delay(1000);
      }
      break;
    case NO_BATTERY:
      read_status();
      if (battery_voltage > 0.1) {
        charger_state = BATTERY_CONNECTED;
        Serial.println(F("Battery detected"));
        update_lcd(F("Battery detected"), construct_status_string()); delay(1500);
      } else {
        update_lcd(F("No battery"), construct_status_string()); delay(1100);
      }
      break;
    case BATTERY_CONNECTED:
      dac_write_voltage(0); wanted_dac_voltage = 0; delay(100);
      read_status();
      if (battery_voltage > BATTERY_GOOD_VOLTAGE_THRESHOLD) {
        charger_state = CURRENT_RAMP_UP;
        update_lcd(F("Battery ok"), construct_status_string()); Serial.println(F("Battery ok")); delay(2000);
        wanted_dac_voltage = get_approximated_dac_voltage(battery_voltage);
        connect_charging_circuit(); delay(200);
      } else {
        charger_state = BATTERY_VOLTAGE_LOW;
        Serial.println(F("Battery voltage too low"));
        update_lcd(F("V Battery low"), construct_status_string()); delay(1000);
      }
      break;
    case BATTERY_VOLTAGE_LOW:
      update_lcd(F("V Battery low"), construct_status_string());
      Serial.println(F("Battery voltage too low"));
      charger_state = FINAL_STATE; break;
    case CURRENT_RAMP_UP:
      read_status(); update_lcd(F("Ramp up"), construct_status_string()); delay(50);
      if (current_mA < target_current_mA) {
        if (wanted_dac_voltage < 4.99) { wanted_dac_voltage += VOLTAGE_STEP; dac_write_voltage(wanted_dac_voltage); }
        else charger_state = UNABLE_TO_REACH_CURRENT;
      } else {
        charger_state = CHARGING; ChargingTimeMillis = 0;
      }
      break;
    case CHARGING:
      delay(200); read_status();
      if (current_mA < 1.0) charger_state = UNEXPECTED_CURRENT_FALL;
      if (battery_voltage > MAXIMUM_BATTERY_VOLTAGE) charger_state = OVERVOLTAGE;
      if (abs(current_mA - target_current_mA) > 0.2) {
        if (current_mA < target_current_mA) {
          if (wanted_dac_voltage < 4.99) wanted_dac_voltage += VOLTAGE_STEP;
          else charger_state = UNABLE_TO_REACH_CURRENT;
        } else {
          if (wanted_dac_voltage > 0.01) wanted_dac_voltage -= VOLTAGE_STEP;
          else charger_state = CURRENT_TOO_HIGH;
        }
      }
      if (abs(current_mA - target_current_mA) > 5) charger_state = REGULATION_FAILED;
      dac_write_voltage(wanted_dac_voltage);
      if (total_minutes_average < MINUTES_TO_LOG) msg = String(F("Charging ")) + "---";
      else msg = String(F("Charging ")) + String(last_blf, 3);
      update_lcd(msg, construct_status_string());
      log_battery_voltage();
      if (last_blf < 0) charger_state = FULLY_CHARGED;
      break;
    case CURRENT_RAMP_DOWN:
      read_status(); update_lcd(F("Ramp down"), construct_status_string()); Serial.println(F("Ramp down")); delay(50);
      if (current_mA > trickle_current_mA) {
        if (wanted_dac_voltage > VOLTAGE_STEP) { wanted_dac_voltage -= VOLTAGE_STEP; dac_write_voltage(wanted_dac_voltage); }
        else charger_state = CURRENT_TOO_HIGH;
      } else {
        charger_state = TRICKLE; TrickleChargingTimeMillis = 0; Serial.println(F("Trickle charge"));
      }
      break;
    case TRICKLE:
      delay(200); read_status();
      if (current_mA < 0.2) charger_state = UNEXPECTED_CURRENT_FALL;
      if (battery_voltage > MAXIMUM_BATTERY_VOLTAGE) charger_state = OVERVOLTAGE;
      if (abs(current_mA - trickle_current_mA) > 0.2) {
        if (current_mA < trickle_current_mA) {
          if (wanted_dac_voltage < 4.99) wanted_dac_voltage += VOLTAGE_STEP;
          else charger_state = UNABLE_TO_REACH_CURRENT;
        } else {
          if (wanted_dac_voltage > 0.01) wanted_dac_voltage -= VOLTAGE_STEP;
          else charger_state = CURRENT_TOO_HIGH;
        }
      }
      if (abs(current_mA - trickle_current_mA) > 5) charger_state = REGULATION_FAILED;
      dac_write_voltage(wanted_dac_voltage);
      if ((second() % 8) < 4) update_lcd(F("Trickle charge"), construct_status_string());
      else update_lcd(eoc_line1, eoc_line2);
      elapsed_time = mills2time(TrickleChargingTimeMillis);
      if (elapsed_time.total_minutes > max_time_for_trickle_charge) {
        update_lcd(eoc_line1, eoc_line2);
        charger_state = END_OF_TRICKLE;
        disconnect_charging_circuit();
      }
      break;
    case END_OF_TRICKLE:
      if ((second() % 8) < 4) update_lcd(F("Trickle finished"), construct_status_string());
      else update_lcd(eoc_line1, eoc_line2);
      break;
    case UNEXPECTED_CURRENT_FALL:
      Serial.println(F("Current unexpectedly fell"));
      disconnect_charging_circuit(); wanted_dac_voltage = 0;
      update_lcd(F("No current"), construct_status_string());
      charger_state = FINAL_STATE; delay(1000); break;
    case UNABLE_TO_REACH_CURRENT:
      Serial.println(F("Unable to reach desired current"));
      disconnect_charging_circuit(); wanted_dac_voltage = 0; dac_write_voltage(0); delay(1000);
      update_lcd(F("I unreachable"), construct_status_string());
      charger_state = FINAL_STATE; break;
    case FULLY_CHARGED:
      elapsed_time = mills2time(ChargingTimeMillis);
      int charge_mAh = calculate_charge(ChargingTimeMillis);
      msg = String(F("Completed ")) + construct_time_string(elapsed_time);
      msg1 = String(F("Charge ")) + String(charge_mAh) + "mAh";
      eoc_line1 = msg; eoc_line2 = msg1;
      update_lcd(msg, msg1); Serial.println(msg); delay(3000);
      charger_state = CURRENT_RAMP_DOWN; break;
    case CURRENT_TOO_HIGH:
      Serial.println(F("Unable to lower current to target"));
      disconnect_charging_circuit(); wanted_dac_voltage = 0; dac_write_voltage(0);
      update_lcd(F("High current"), construct_status_string()); delay(1000);
      charger_state = FINAL_STATE; break;
    case REGULATION_FAILED:
      Serial.println(F("Regulation fault"));
      disconnect_charging_circuit(); wanted_dac_voltage = 0; dac_write_voltage(0);
      update_lcd(F("Regulation fault"), construct_status_string()); delay(1000);
      charger_state = FINAL_STATE; break;
    case OVERCURRENT:
      disconnect_charging_circuit(); Serial.println(F("Current too high"));
      wanted_dac_voltage = 0; dac_write_voltage(wanted_dac_voltage);
      update_lcd(F("Current too high"), construct_status_string()); delay(1000);
      charger_state = FINAL_STATE; break;
    case OVERVOLTAGE:
      disconnect_charging_circuit(); Serial.println(F("Voltage too high"));
      wanted_dac_voltage = 0; dac_write_voltage(wanted_dac_voltage);
      update_lcd(F("Voltage too high"), construct_status_string()); delay(1000);
      charger_state = FINAL_STATE; break;
    case FINAL_STATE: delay(10000); break;
    default: wanted_dac_voltage = 0; charger_state = 0;
  }
  read_status();
  if (last_second != second()) {
    Serial.print(current_mA); Serial.print(","); Serial.println(battery_voltage);
    last_second = second();
  }
}

hw.ino (работа с железом)

float get_approximated_dac_voltage(float vbat) {
  float offset_voltage = 1.2;
  float adc_voltage = (vbat - offset_voltage) / AMP_GAIN - 0.5;
  if (adc_voltage < 0) adc_voltage = 0;
  return adc_voltage;
}

int voltage_to_code(float voltage) { return 4095.0 / DAC_REF_VOLTAGE * voltage; }

void dac_write(int code) {
  Wire.beginTransmission(MCP4725_ADDR);
  Wire.write(64); Wire.write(code >> 4); Wire.write((code & 15) << 4);
  Wire.endTransmission();
}

void read_status() {
  voltage_sum = 0; current_sum = 0;
  for (int i=0; i= POINTS_FOR_AVERAGING) averaging_index = 0;
  if (tmp_battery_voltage > MAXIMUM_BATTERY_VOLTAGE) { disconnect_charging_circuit(); charger_state = OVERVOLTAGE; }
  if (tmp_current_mA > MAXIMIM_ALLOWED_CURRENT) { disconnect_charging_circuit(); charger_state = OVERCURRENT; }
}

void disconnect_charging_circuit() { digitalWrite(CHARGE_RELAY_PIN, DISCONNECT_CHARGE_RELAY); }
void connect_charging_circuit() { digitalWrite(CHARGE_RELAY_PIN, CONNECT_CHARGE_RELAY); }

lcd.ino (дисплей)

void update_lcd(String first_line, String second_line) {
  if (lcd_last_string1 != first_line) {
    lcd.clear(); lcd.setCursor(0,0); lcd.print(first_line); lcd_last_string1 = first_line;
    lcd.setCursor(0,1); lcd.print(second_line); lcd_last_string2 = second_line;
  }
  if (lcd_last_second != second()) {
    if (lcd_last_string2 != second_line) {
      lcd.setCursor(0,1); lcd.print(second_line); lcd_last_string2 = second_line;
    }
  }
  lcd_last_second = second();
}

String construct_status_string() {
  String v = (battery_voltage < 10) ? String(battery_voltage, 2) : String(battery_voltage, 1);
  String i = (current_mA < 10) ? String(current_mA, 2) : String(current_mA, 1);
  mytime elapsed; String msg, msg_time;
  switch(charger_state) {
    case CHARGING: elapsed = mills2time(ChargingTimeMillis); break;
    case TRICKLE: elapsed = mills2time(TrickleChargingTimeMillis); break;
  }
  if (charger_state == CHARGING || charger_state == TRICKLE) {
    if (elapsed.total_minutes < 10) msg_time = String(elapsed.total_minutes) + " ";
    else if (elapsed.total_minutes < 100) msg_time = String(elapsed.total_minutes) + " ";
    else msg_time = String(elapsed.total_minutes);
  }
  switch(charger_state) {
    case CHARGING: msg = v + "V " + i + "mA " + msg_time; break;
    case TRICKLE: msg = v + "V " + i + "mA " + msg_time; break;
    default: msg = v + "V " + i + "mA";
  }
  msg.replace("-", "");
  return msg;
}

String construct_time_string(mytime t) {
  return String(t.hours) + ":" + (t.minutes < 10 ? "0" : "") + String(t.minutes);
}

calculations.ino (математика)

float best_linear_fit(float y[MINUTES_TO_LOG]) {
  float sx=0, sy=0, sxx=0, sxy=0;
  for (int i=0; i

Библиотека New LiquidCrystal

Требуется для YwRobot I2C LCD. Скачать и установить в Arduino IDE.

Схемы

Умное зарядное устройство для 9В NiMH аккумуляторов — точное, надежное, версия 1

charger_AgK96zxw2T.zip (файл схемы)

Производственный процесс

  1. Умный датчик полива «Thirst Alert» на Arduino — DIY сигнализация засухи для растений
  2. Итальянские словесные часы на Arduino Nano, NeoPixel, LCD и RTC
  3. Экономия пинов Arduino: подключение 4×3 клавиатуры по трём аналоговым входам
  4. Надежный счетчик кВт·ч на Sigfox с Arduino MKR Fox 1200
  5. Bluetooth-монитор температуры на Arduino Nano с NTC-термистором
  6. Бесконтактный жестовый замок: APDS-9960 + Arduino для безопасного доступа
  7. Набор-компаньон для Arduino Nano — необходимые компоненты и инструменты для DIY-электроники
  8. Набор USB-MIDI адаптера для Arduino Micro / Leonardo — полный список деталей
  9. Гальванически разделённый аналоговый вход для Arduino на LM331 и оптопаре 4N25
  10. Точный тест времени реакции на Arduino UNO