# 電池管理系統(Power Memage Unit) ***說明:用MCU做鋰電池狀態監控,同時控制其他DC電源系統的開關*** ###### tags: `省電` `Arduino' 'Arduino Pro Mini` `I2C` ## 材料 1. MCU:[Arduino Mini Pro(8Mhz 3.3V)](https://www.sparkfun.com/products/11114) 2. Module: * 電源監控:INA219 * DC迴路開關控制:n-MOSFet ## 架構 ### Arduino Pro Mini 的功能 1. 控制某個mcu的電源迴路:使用MOSFet(p-chennel of n-chennel) 2. 提供I2C介面讓MCU來查詢/設定下列功能(以library形式打包): * 設定I2C Address * 設定下次喚醒間隔??秒 * 是否進入睡眠 * 是否監控電池電壓:電池電壓太低時延長喚醒週期 * 設定啟動腳位 * 設定INA219的Address 3. 控制流程(Ver 1.0) 1. 不同階段閃爍不同的LED燈 (P13,6.5mA) * I2C Master 階段 (2短(200ms)/loop, 300mS間隔) * I2C Slaver 階段 (1短(200ms)/loop) * 讀取config 階段 (1長(1000ms)) * 睡眠 階段 (關閉) 2. 開機後先進入Master階段讀取config及INA219 3. 開啟MCU電源後,立刻進入slaver階段等待MCU下指令 4. MCU開機進行Setup 5. MCU再Loop結束前須決定是否要轉移給PMU控制電源/PMU控制+休眠/MCU持續運作 6. PMU收到關閉指令後,立即把I2C切換成Master 7. PMU睡醒後須先讀取INA219,按照設定決定要不要啟動MCU ![目前想法](https://i.imgur.com/nybAIxm.png) ### 控制迴路 1.持續供電: Arduino Pro Mini 2.控制迴路A:INA219 3.控制迴路B:LinkIt7697 4.控制迴路C:SIMCOM700E / --- ## Arduino Mini Pro 省電模式測試 * [參考來源](http://www.home-automation-community.com/arduino-low-power-how-to-run-atmega328p-for-a-year-on-coin-cell-battery/) * [使用library:RoketScream](https://github.com/rocketscream/Low-Power) * BOD的說明(TODO:待測試) * [參考來源:BOD](http://ww1.microchip.com/downloads/en/devicedoc/doc7903.pdf) * 目前測試下免兩個差異不大 > *(Timer 2 還不知道怎麼用 orz)* > * powerDown(period_t period, adc_t adc, bod_t bod) > * powerSave(period_t period, adc_t adc, bod_t bod, timer2_t timer2) ### 使用Raw腳供電(4-12V輸入) * [Sparkfun 教學](https://learn.sparkfun.com/tutorials/using-the-arduino-pro-mini-33v) ![Pinout](https://i.imgur.com/R8vaMXY.png) ### 睡眠測試:1.359mA, 未拆LED及線性穩壓器(LDO) ```cpp=1 // **** INCLUDES ***** #include "LowPower.h" #define LED 13 uint16_t c = 0; // 控制LED閃爍的funciton void LEDFlash(uint8_t _ts) { for (uint8_t _i = 0; _i < _ts; _i++) { digitalWrite(LED, HIGH); delay(250); digitalWrite(LED, LOW); delay(250); } } void setup() { Serial.begin(115200); pinMode(LED, OUTPUT); LEDFlash(3); } void loop() { Serial.print(c); Serial.print(F("-開始睡眠: ")); switch (c % 3) { case 0: Serial.print(F("Power Down Mode")); delay(500); // 預留一些時間讓uart 能把資料吐完 LowPower.powerDown(SLEEP_4S, ADC_OFF, BOD_OFF); // 進入睡眠模式 break; case 1: Serial.print(F("Power Save Mode")); delay(500); LowPower.powerSave(SLEEP_4S, ADC_OFF, BOD_OFF, TIMER2_OFF); break; default: Serial.print(F("active Mode")); delay(500); delay(4000); } Serial.println(F("\t-起床")); LEDFlash(3); c++; } ``` ## 測試:Arduino Mini Pro ### Upload: * 編譯的時候先按下RESET鈕,看到Uploading時再放開就可以上傳 ### SoftReset: ```cpp=1 // 重新啟動 // Restarts program from beginning but does not reset the peripherals and registers void vSoftwareReset(void) { asm __volatile__(" jmp 0"); } ``` ### SRAM size * [參考來源](https://playground.arduino.cc/Code/AvailableMemory/?fbclid=IwAR0qzHze5c3ZWY6_otxIAVZlpPmcJ-2MNviBiAMuyr91_M39ZtlslZf-_LI) ```cpp=1 // 剩餘的SRAM // Arduino Mini Pro 共有2048bytes空間(2K) int iGetFreeRam(void) { extern int _heap_start, * __brkval; int v; return (int)&v - (__brkval == 0 ? (int)&__malloc_heap_start : (int)__brkval); } ``` ### I2C communication * [參考來源](https://www.arduino.cc/en/reference/wire) * ## 測試:PMU端 ### PMU端 WaterBox_PMU.h 檔 ```cpp=1 #include <Arduino.h> #ifndef _WATERBOX_PMU_ #define _WATERBOX_PMU_ #define _I2C_BUFFER_SIZE 30 #define COMMUNICATIONMODULE enum POWER { OFF, ON }; enum STATE { MASTER, SLEEP, SLAVER, }; enum UART { NONE, EOL, H_PMU, H_I2C, H_CMD }; enum IC2Request { Req_NONE, // 無Request Req_ERROR, // 錯誤指令 Req_TIME, // 設定鬧鐘(秒), 回傳OK -> timer ? sec Req_SLEEP, // 進入睡眠, 不回傳 Req_LOWPOWER, // 省電模式, 回傳OK->LowPower ON/OFF Req_BAT_VOLATE, // 電池電壓, 回傳OK->volate Req_BAT_CURRENT, // 電池電壓, 回傳OK->current Req_AT // AT Cmd( 給外面的NBIOT用) }; class WaterBox_PMU { public: WaterBox_PMU(); // 建構函式 WaterBox_PMU(uint8_t _addr); // 建構函式(指定 Slaver時的I2C Address) ~WaterBox_PMU(); // 解構函式 void init(uint16_t _pin); void setINA219(uint8_t _addr); static void setDebuger(Stream& refSer); // 設定debug用的輸出 static void setWakeUpVolate(float _v); // 設定喚醒電壓 static void setSleepSec(uint32_t _sec); static uint8_t Sleep(); // 開始睡眠:切換到Master,關閉供電後開始進入睡眠循環,循環結束時更新電池狀態,低於喚醒電源後再開始供電返回1 (可以用while(Sleep())一值睡 static void PowerSaveMode(POWER _state); static void ControlPower(POWER _state); static void getBetteryState(); // 取得電池狀態:切換到Master,呼叫INA219拿資料後更新變數 static void LED(uint16_t _times, uint16_t _interval); static float Volate; // 電池上次的電壓 static float Current; // 電池上次的電流量 static enum STATE stete; #ifdef COMMUNICATIONMODULE static String ATCMD; // 給NBIOT的Command static void ATClear(); #endif // COMMUNICATIONMODULE private: static uint8_t _Debug; static uint16_t _ControlPin; static Stream& refSerial; static uint8_t _Addr; uint8_t _INA219Addr = 0x40; //uint8_t _SleepMode; // 未使用 static uint8_t _PowerSave; // 定義是不是要進入省電模式 static uint16_t _SleepSec; // 睡眠秒數 static uint16_t _i_for; static float _WakeUpVoltage; static void _SwitchToSlaver(uint8_t _addr); static void _receiveEvent(int howMany); static void _requestEvent(); static void _cmd(String _str); static String _ComStr; static String _recBuffer; static IC2Request _REQUEST; static void _Deguber(String _msg, UART _header, UART _uart = NONE); }; #endif ``` ### PMU端 WaterBox_PMU.cpp 檔 ```cpp=1 #include "WaterBox_PMU.h" #include <Wire.h> #include <LowPower.h> #include <Adafruit_INA219.h> #define SLEEPTIME SLEEP_1S uint8_t WaterBox_PMU::_Debug; Stream& WaterBox_PMU::refSerial = Serial; uint16_t WaterBox_PMU::_i_for; uint8_t WaterBox_PMU::_PowerSave = false; uint16_t WaterBox_PMU::_SleepSec; IC2Request WaterBox_PMU::_REQUEST; String WaterBox_PMU::_ComStr; String WaterBox_PMU::_recBuffer; float WaterBox_PMU::_WakeUpVoltage = 3.6; float WaterBox_PMU::Volate; float WaterBox_PMU::Current; enum STATE WaterBox_PMU::stete = MASTER; uint16_t WaterBox_PMU::_ControlPin; uint8_t WaterBox_PMU::_Addr = 0x70; #ifdef COMMUNICATIONMODULE String WaterBox_PMU::ATCMD; #endif // COMMUNICATIONMODULE WaterBox_PMU::WaterBox_PMU() { } WaterBox_PMU::WaterBox_PMU(uint8_t _addr) { _Addr = _addr; } WaterBox_PMU::~WaterBox_PMU() { } void WaterBox_PMU::init(uint16_t _pin) { _ControlPin = _pin; pinMode(_ControlPin, OUTPUT); } void WaterBox_PMU::setINA219(uint8_t _addr) { _INA219Addr = _addr; _Deguber(F("Setting INA219 I2C address: 0x"), H_PMU); _Deguber(String(_INA219Addr, HEX), EOL); } void WaterBox_PMU::setDebuger(Stream& refSer) { _Debug = true; refSerial = refSer; } void WaterBox_PMU::setWakeUpVolate(float _v) { _WakeUpVoltage = _v; _Deguber(F("Setting WAKE UP Volate: "), H_PMU); _Deguber(String(_WakeUpVoltage), NONE); _Deguber(F(" V."), NONE, EOL); } void WaterBox_PMU::setSleepSec(uint32_t _sec) { _SleepSec = _sec; _Deguber(F("Setting Sleep Time: "), H_PMU); _Deguber(String(_SleepSec), NONE); _Deguber(F(" sec."), NONE, EOL); } // 開始睡眠:切換到Master,關閉供電後開始進入睡眠循環,循環結束時更新電池狀態,低於喚醒電源後再開始供電返回1 (可以用while(Sleep())一值睡 uint8_t WaterBox_PMU::Sleep() { LED(1,700); _Deguber(F("Prepare SLEEP Mode"), H_PMU, EOL); _Deguber(F("Off the LED & Control POWER"), H_PMU, EOL); digitalWrite(LED_BUILTIN, LOW); ControlPower(OFF); _Deguber(F("Enter SLEEP Mode"), H_PMU, EOL); delay(100); for (_i_for = 0; _i_for < _SleepSec; _i_for++) { if (_PowerSave) LowPower.powerDown(SLEEPTIME, ADC_OFF, BOD_OFF); else delay(1000); } stete = MASTER; _Deguber(F("Waker up, init I2C with Master and get bettery state..."), H_PMU, EOL); ControlPower(ON); Wire.begin(); getBetteryState(); ControlPower(OFF); if (Volate < _WakeUpVoltage) { _Deguber(F("ERROR: Volate < Wake up voltage"), H_PMU); _Deguber(String(_WakeUpVoltage), NONE, EOL); return 1; } else { ControlPower(ON); _Deguber(F(" I2C --> Slaver"), H_PMU, EOL); _SwitchToSlaver(_Addr); stete = SLAVER; return 0; } } void WaterBox_PMU::PowerSaveMode(POWER _state) { if (_state == ON) _Deguber(F("Power Save Mode: ON"), H_PMU, EOL); else if (_state == OFF) _Deguber(F("Power Save Mode: OFF"), H_PMU, EOL); _PowerSave = _state; } void WaterBox_PMU::ControlPower(POWER _state) { if (_state == ON) { digitalWrite(_ControlPin, HIGH); _Deguber(F("Control: ON"), H_PMU, EOL); } else if (_state == OFF) { digitalWrite(_ControlPin, LOW); _Deguber(F("Control: OFF"), H_PMU, EOL); } } void WaterBox_PMU::getBetteryState() { Adafruit_INA219 bettery(INA219_ADDRESS); bettery.begin(); // To use a slightly lower 32V, 1A range (higher precision on amps): //ina219.setCalibration_32V_1A(); // Or to use a lower 16V, 400mA range (higher precision on volts and amps): bettery.setCalibration_16V_400mA(); Volate = bettery.getBusVoltage_V(); Current = bettery.getCurrent_mA(); _Deguber(F("Bettery Volate: "), H_PMU); _Deguber(String(Volate), NONE); _Deguber(F(", Current: "), NONE); _Deguber(String(Current), NONE, EOL); } void WaterBox_PMU::LED(uint16_t _times, uint16_t _ratio) { pinMode(LED_BUILTIN, OUTPUT); for (_i_for = 0; _i_for < _times; _i_for++) { digitalWrite(LED_BUILTIN, HIGH); delay(_ratio); digitalWrite(LED_BUILTIN, LOW); delay(300); } } void WaterBox_PMU::ATClear() { ATCMD = ""; } void WaterBox_PMU::_Deguber(String _msg, UART _header, UART _uart = NONE) { if (_Debug) { switch (_header) { case H_PMU: refSerial.print(F("[PMU]\t")); break; case H_I2C: refSerial.print(F("[I2C]\t")); break; case H_CMD: refSerial.print(F("[CMD]\t")); break; default: break; } switch (_uart) { case NONE: refSerial.print(_msg); break; case EOL: refSerial.println(_msg); break; default: break; } } } void WaterBox_PMU::_requestEvent() { switch (_REQUEST) { case Req_NONE: _recBuffer = F("No String"); break; case Req_TIME: _recBuffer = F("OK,timer -> "); _recBuffer += String(_SleepSec); _recBuffer += F(" sec"); break; case Req_SLEEP: _recBuffer = F("Sleep"); break; case Req_LOWPOWER: _recBuffer = F("OK, LowPower -> "); _recBuffer += String(_PowerSave); break; case Req_BAT_VOLATE: _recBuffer = F("OK,"); _recBuffer += String(Volate); break; case Req_BAT_CURRENT: _recBuffer = F("OK,"); _recBuffer += String(Current); break; case Req_AT: _recBuffer = F("OK"); break; default: _recBuffer = F("Command ERROR"); break; } uint16_t _BufferSize = _recBuffer.length(); for (_i_for = 0; _i_for + _BufferSize < _I2C_BUFFER_SIZE; _i_for++) { _recBuffer += F(" "); } _Deguber(_recBuffer, H_I2C, EOL); _Deguber("", NONE, EOL); Wire.write(_recBuffer.c_str()); } void WaterBox_PMU::_receiveEvent(int howMany) { _ComStr = ""; while (Wire.available()) { char _c_ = Wire.read(); _ComStr += String(_c_); } LED(1,300); _cmd(_ComStr); } void WaterBox_PMU::_cmd(String _str) { /* TIME,sec // 設定鬧鐘(秒) SLEEP // 進入睡眠 LOWPOWER,1 // 省電模式 VOLATE // 回傳 volate CURRENT // 回傳 current ATCMD,***** // 輸入AT Commend 資料 */ _str.toUpperCase(); _ComStr = _str.substring(_str.indexOf(F(",")) + 1); // 重新利用_ComStr if (_str.length() > 0) _REQUEST = Req_ERROR; else _REQUEST = Req_NONE; if (_str.indexOf(F("TIME")) > -1) { _Deguber(F("TIME -> "), H_CMD); _Deguber(_ComStr, NONE, EOL); _SleepSec= _ComStr.toInt(); setSleepSec(_SleepSec); _REQUEST = Req_TIME; } if (_str.indexOf(F("SLEEP")) > -1) { _Deguber(F("Enter SLEEP"), H_CMD,EOL); Sleep(); _REQUEST = Req_SLEEP; } if (_str.indexOf(F("LOWPOWER")) > -1) { _Deguber(F("LOWPOWER -> "), H_CMD); _Deguber(_ComStr, NONE, EOL); _PowerSave = _ComStr.toInt(); PowerSaveMode(POWER(_PowerSave)); _REQUEST = Req_LOWPOWER; } if (_str.indexOf(F("VOLATE")) > -1) { _Deguber(F("GET BATTERY VOLATE"), H_CMD, EOL); _REQUEST = Req_BAT_VOLATE; } if (_str.indexOf(F("CURRENT")) > -1) { _Deguber(F("GET BATTERY CURRENT"), H_CMD, EOL); _REQUEST = Req_BAT_CURRENT; } if (_str.indexOf(F("ATCMD")) > -1) { ATClear(); _Deguber(F("ATCMD >> "), H_CMD); _Deguber(_ComStr, NONE, EOL); ATCMD = _ComStr; _REQUEST = Req_AT; } } void WaterBox_PMU::_SwitchToSlaver(uint8_t _addr) { Wire.begin(_addr); Wire.onRequest(&WaterBox_PMU::_requestEvent); // I2C Master的指令:回傳 Wire.onReceive(&WaterBox_PMU::_receiveEvent); // I2C Master的指令:不回傳 } ``` ### PMU端 *.ino 檔 ```cpp=1 /* Name: PowerManagUnit.ino Created: 2020/2/18 下午 07:37:23 Author: Liu */ #include "WaterBox_PMU.h" #define CONTROL_PIN 10 #include <SoftwareSerial.h> #define UART_TX 8 #define UART_RX 9 WaterBox_PMU PMU; static int _c = 0; void showState() { Serial.print(F("PMU STATE >> ")); switch (PMU.stete) { case MASTER: Serial.println(F("MASTER")); break; case SLEEP: Serial.println(F("SLEEP")); break; case SLAVER: Serial.println(F("SLAVER")); break; default: break; } } void setup() { Serial.begin(115200); PMU.init(CONTROL_PIN); PMU.setDebuger(Serial); PMU.setSleepSec(5); PMU.setWakeUpVolate(3.5); PMU.PowerSaveMode(ON); Serial.println(F("Setup Done")); showState(); PMU.Sleep(); showState(); } void loop() { //Serial.print(F("Loop ")); //Serial.println(_c); if (PMU.ATCMD.length() > 0) { Serial.println(F("[LOOP]\t")); Serial.print(F("[AT] \t")); Serial.println(PMU.ATCMD); PMU.ATClear(); PMU.LED(3,200); } _c++; //if(_c%50==0) PMU.Sleep(); //else delay(1000); } ``` ## 測試:7697端(受控制端) ### 受控制端 WaterBox_PowerSaving.h ```cpp=1 #include <Arduino.h> #ifndef _WATERBOX_POWERSAVING_ #define _WATERBOX_POWERSAVING_ #define RequestBufferSize (uint8_t)30 class WaterBox_PowerSaving { public: WaterBox_PowerSaving(uint8_t _addr); ~WaterBox_PowerSaving(); enum POWER { NORMAL, // 對照OFF LOWPOWER // 對照ON }; void init(); void setDebuger(Stream& refSer); // 設定Deguber void setSleepSec(uint32_t _sec); // 設定睡眠時間(秒) void setSleepMode(POWER _state); // 設定睡眠模式 void sendData(String _data); void getState(); void Sleep(); // 進入睡眠狀態 static void _Receive(uint8_t _addr, String _cmd); // I2C Master的指令:回傳 static void _Reques(uint8_t _addr); // I2C Master的指令:不回傳 static String ReqString; // I2C 回傳字串 static float Volate; // 系統電壓 static float Current; // 系統電池 private: enum UART { NONE, EOL, H_PMU, H_I2C, H_CMD }; struct COMMAND { String TIME = F("TIME,"); String AT = F("ATCMD,"); String SLEEP = F("SLEEP"); String VOLATE = F("VOLATE"); String CURRENT = F("CURRENT"); String LOWPOWER = F("LOWPOWER,"); }; static uint8_t _Debug; static Stream& refSerial; static uint8_t _Addr; static String _CMDBuffer; static void _sendCMD(String _cmd, uint8_t _req = false); static void _Deguber(String _msg, UART _header, UART _uart = NONE); COMMAND _cmd; }; #endif ``` ### 受控制端 WaterBox_PowerSaving.cpp ```cpp=1 #include "WaterBox_PowerSaving.h" #include <Wire.h> #include <Adafruit_INA219.h> uint8_t WaterBox_PowerSaving::_Debug = false; Stream& WaterBox_PowerSaving::refSerial = Serial; uint8_t WaterBox_PowerSaving::_Addr = 0x70; String WaterBox_PowerSaving::_CMDBuffer = ""; String WaterBox_PowerSaving::ReqString = ""; WaterBox_PowerSaving::WaterBox_PowerSaving(uint8_t _addr) { _Addr = _addr; } WaterBox_PowerSaving::~WaterBox_PowerSaving() { } void WaterBox_PowerSaving::init() { Wire.begin(); } void WaterBox_PowerSaving::setDebuger(Stream& refSer) { _Debug = true; refSerial = refSer; } void WaterBox_PowerSaving::setSleepSec(uint32_t _sec) { _CMDBuffer = _cmd.TIME + String(_sec); _sendCMD(_CMDBuffer, true); } void WaterBox_PowerSaving::setSleepMode(POWER _state) { _CMDBuffer = _cmd.LOWPOWER + String(_state); _sendCMD(_CMDBuffer, true); } void WaterBox_PowerSaving::sendData(String _data) { _CMDBuffer = _cmd.AT + _data; _sendCMD(_CMDBuffer, true); } void WaterBox_PowerSaving::getState() { _sendCMD(_cmd.VOLATE, true); _sendCMD(_cmd.CURRENT, true); } void WaterBox_PowerSaving::Sleep() { _sendCMD(_cmd.SLEEP, false); } void WaterBox_PowerSaving::_Receive(uint8_t _addr, String _cmd) { Wire.beginTransmission(_addr); Wire.write(_cmd.c_str()); Wire.endTransmission(); } void WaterBox_PowerSaving::_Reques(uint8_t _addr) { Wire.requestFrom(_addr, RequestBufferSize); ReqString = ""; while (Wire.available()) { char c = Wire.read(); ReqString += String(c); } _Deguber(ReqString, H_I2C, EOL); } void WaterBox_PowerSaving::_sendCMD(String _cmd, uint8_t _req) { _Deguber(_cmd, H_CMD, EOL); _Receive(_Addr, _cmd); delay(10); // 必須留時間做為緩衝 if (_req) _Reques(_Addr); } void WaterBox_PowerSaving::_Deguber(String _msg, UART _header, UART _uart) { if (_Debug) { switch (_header) { case H_PMU: refSerial.print(F("[PMU]\t")); break; case H_I2C: refSerial.print(F("[I2C]\t")); break; case H_CMD: refSerial.print(F("[CMD]\t")); break; default: break; } switch (_uart) { case NONE: refSerial.print(_msg); break; case EOL: refSerial.println(_msg); break; default: break; } } } ``` ### 受控制端 *.ino ```cpp=1 /* Name: PowerControled.ino Created: 2020/3/2 上午 11:49:52 Author: Liu */ #include "WaterBox_PowerSaving.h" WaterBox_PowerSaving PSM(0x70); void setup() { Serial.begin(9600); PSM.init(); PSM.setDebuger(Serial); } uint8_t _c = 0; void loop() { switch (_c % 6) { case 0: Serial.println("設定睡眠模式"); PSM.setSleepMode(PSM.NORMAL); break; case 1: Serial.println("設定睡眠秒數:增加"); PSM.setSleepSec(30 + _c); break; case 2: Serial.println("讀取狀態"); PSM.getState(); break; case 3: Serial.println("設定睡眠模式:低功耗"); PSM.setSleepMode(PSM.LOWPOWER); break; case 4: Serial.println("AT 指令"); PSM.sendData("AT=123456\n\r"); break; case 5: Serial.println("開始睡眠"); PSM.Sleep(); delay(50000); break; } _c++; if (_c > 20) _c = 0; delay(5000); } ```
{"metaMigratedAt":"2023-06-15T03:34:40.421Z","metaMigratedFrom":"Content","title":"電池管理系統(Power Memage Unit)","breaks":true,"contributors":"[{\"id\":\"3bb96581-f11e-4e8b-8446-0937f0304d6f\",\"add\":37633,\"del\":18756}]"}
Expand menu