고급 9V NiMH 배터리 스마트 충전기: 정전류, -dV/dt 종료, 트리클 충전
부품 및 소모품
부품
수량
비고
아두이노 우노 (Arduino UNO)
1
제어용
스파크펀 I2C DAC 브레이크아웃 (MCP4725)
1
12비트 DAC, 전압 제어용
가변 출력 리니어 레귤레이터 (LM317 등)
1
충전 전압/전류 제어
TI 범용 듀얼 오피앰프 (LM358)
1
DAC 출력 증폭용
저항 6.8kΩ
2
오피앰프 이득 설정
저항 3.3kΩ
1
오피앰프 이득 설정
저항 5.1kΩ
1
배터리 전압 분배
저항 1kΩ
1
LM317 최소 부하용
저항 10Ω (전류 감지용)
1
정밀 저항 권장
릴레이 (5V 제어, 상시 개방 접점)
1
충전 회로 차단용
저항 20kΩ
1
배터리 전압 분배
브레드보드 파워 서플라이 (5V/3.3V)
1
DAC/LCD 전원용
17V DC 파워 서플라이
1
충전 회로 입력 전원
I2C 시리얼 LCD 1602 모듈 (YwRobot 등)
1
상태 표시용
프로젝트 개요
9V NiMH 배터리를 2~3시간 내 완충하면서 과충전 방지하는 스마트 충전기를 찾다 없어서 직접 만들었습니다. 시중 충전기는 전류 불명, 충전 종료 기능 없어 수명 단축 우려가 큽니다. 1차 버전은 정전류 충전(CC), -dV/dt 자동 종료, 트리클 충전, 충전량 측정 핵심 기능만 구현. 향후 버전에 방전, 용량 측정, 사이클링 추가 예정.
⚠ 안전 경고
고전류 충전 시 배터리 폭발/화재 위험. 충전 중 방치 금지. 알카라인 등 비충전지 충전 금지. 본 충전기는 NiMH만 테스트됨(버그로 인한 손해 책임 없음). 다른 배터리 종류는 코드 수정 필요.
이론: NiMH 충전 핵심 포인트
C = 배터리 공칭 용량과 같은 수치의 전류 (예: 170mAh 배터리면 C = 170mA)
1C 충전 시 단일 셀 전압 최대 1.6V 도달 가능 (노후 배터리는 더 높음)
공칭 전압 1.2V, 완충 시 개방 전압 ~1.5V
완충 후 트리클 충전: 0.025C (C/40) 이하 권장
충전 방식 두 가지:
급속 충전 : 0.5C~1C. -dV/dt(전압 변화율) 또는 dT/dt(온도 변화율)로 종료 검출
완만 충전 : 0.1C, 14~16시간. 타이머로 종료. 저전류에서 -dV/dt 검출 어려움, 0.5C 미만에선 -dV/dt 신뢰성 낮음
충전 회로 기본 파라미터
9V 배터리 = 보통 7직렬 셀 (6~8셀 가능). 충전 전압 최대 8×1.6V = 12.8V 필요
LM317 드롭아웃 ~2V, 전류 감지 저항 10Ω에서 200mA 시 2V 강하 → 입력 전압 최소 17V 필요
완전 방전 셀 전압 매우 낮음(음수 가능). LM317 최소 출력 ~1.2V
회로도
회로 설명
전류 제어 루프 : 전류 감지 저항 R5(10Ω) 양단 전압 측정 → I = V/R. 아두이노 A2 핀으로 읽음.
DAC 제어 : MCP4725 (I2C, 12비트, 0~5V 출력) → 오피앰프 LM358로 증폭 (이득 = 1+R4/R3 = 1+6.8k/3.3k ≈ 3.06) → 0~15V 범위 확보.
전압 레귤레이터 : 오피앰프 출력을 LM317 ADJ 단자에 입력. LM317은 ADJ-OUT 간 1.2V 유지 → 배터리 전압 1.2V~16.2V 가변 제어. R6(1kΩ)로 최소 부하(3.5mA) 확보, C1로 출력 안정화.
전압 측정 : 두 지점 분압 측정
A2: R5 전압 → 충전 전류 계산
A0: 배터리 전압 분압 (R1=5.1kΩ, R2=20kΩ → 1/5 비율) → 5V 이하로 아두이노 ADC 입력
릴레이 : 충전 회로 물리적 차단. 상시 개방(NO) 접점에 배터리 연결이 안전.
LCD : YwRobot I2C 1602 모듈 사용. 표준 LiquidCrystal_I2C 라이브러리 미지원 시 New LiquidCrystal 라이브러리 사용. 주소 0x27, 핀 매핑 코드 참고.
전원 : DAC/LCD는 브레드보드 파워 서플라이(5V/3.3V) 또는 아두이노 5V 핀에서 공급. 충전 회로는 별도 17V 입력 필요 (없으면 XL6009 부스트 컨버터 모듈 활용).
기능 동작 흐름
버튼 없이 코드 내 상수로 충전 전류 설정 (charger.ino 상단):
float target_current_mA = 30; // 충전 전류 (mA)
float battery_nominal_capacity_mA = 170; // 배터리 공칭 용량 (mAh)
float max_time_for_trickle_charge = 6; // 최대 트리클 시간 (분, 최대 600분=10시간)
충전 전류: 공칭 용량 이하로 설정 (예: 170mAh 배터리 → 최대 170mA). 최소 보통 C/10 (17mA).
전원 투입 → 배터리 감지 → 정전류 충전 시작 → 5분간 -dV/dt(음의 전압 기울기) 감지 시 완충 판정 → 트리클 충전(C/40) 전환 → 설정된 최대 트리클 시간 경과 후 릴레이로 완전 차단.
참고 자료
Energizer NiMH 응용 매뉴얼
Battery University: NiMH 충전
코드 (모듈화된 5개 파일 + 라이브러리)
charger.ino (메인 상태 머신)
// 9V NiMH 배터리 충전용 (6,7,8 셀 가능 → 공칭 7.5~10V)
#include
#include
#include
#include
#include
// YwRobot I2C LCD 핀 매핑 (New LiquidCrystal 라이브러리 필요)
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) // ≈3.06
// 안전 한계
#define BATTERY_GOOD_VOLTAGE_THRESHOLD 7.0
#define MAXIMIM_ALLOWED_CURRENT 200 // mA 하드 컷오프
#define MAXIMUM_BATTERY_VOLTAGE 15.0 // V 하드 컷오프
#define VOLTAGE_STEP 0.001 // DAC 전압 조정 스텝
#define POINTS_FOR_AVERAGING 100 // 평균화 샘플 수
#define MEASURE2MEASURE_US 5000 // 측정 간격 µs
#define VOLTAGE_LOG_TIME_MIN 30.0 // dV/dt 판정용 로그 간격(분)
#define MINUTES_TO_LOG 5 // 기울기 계산용 분 단위 샘플 수
#define TRICKLE_CURRENT_RATIO 40 // 트리클 전류 = C/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
// 전역 변수 (생략: 배열, 합계, 상태, 타이머, LCD 캐시 등)
struct mytime { unsigned char hours, minutes; unsigned int total_minutes; } elapsed_time;
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);
}
// loop() 내 거대 switch-case로 상태 머신 구현 (생략: 전체 코드는 GitHub 참조)
// 주요 상태: INITIALIZATION → NO_BATTERY/BATTERY_CONNECTED → CURRENT_RAMP_UP → CHARGING
// → (dV/dt<0 시) FULLY_CHARGED → CURRENT_RAMP_DOWN → TRICKLE → END_OF_TRICKLE → FINAL_STATE
// 각 상태에서 전압/전류 모니터링, DAC 피드백 제어, LCD 업데이트, 안전 컷오프 수행.
// 주요 헬퍼 함수들 (hw.ino, lcd.ino, calculations.ino에 분산)
hw.ino (하드웨어 인터페이스)
float get_approximated_dac_voltage(float vbat) {
float offset_voltage = 1.2; // LM317 기준 전압
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 (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 (LCD 표시)
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;
} else if (lcd_last_second != second() && 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)+":"+String(t.minutes); }
calculations.ino (수학 연산)
float best_linear_fit(float y[MINUTES_TO_LOG]) { // 최소제곱법 기울기
float sx=0, sy=0, sxx=0, sxy=0;
for (i=0;i
New LiquidCrystal 라이브러리
YwRobot I2C LCD 호환을 위해 New LiquidCrystal 라이브러리 사용 (다운로드만 가능, 프리뷰 없음).
회로도 및 PCB
charger_AgK96zxw2T.zip (EAGLE/KiCad 등 설계 파일)