├── hardware ├── TinyCharger_BOM.tsv ├── TinyCharger_gerber.zip └── TinyCharger_schematic.pdf ├── documentation ├── TinyCharger_pic1.jpg ├── TinyCharger_pic2.jpg ├── TinyCharger_pic3.jpg ├── TinyCharger_pic4.jpg └── TinyCharger_pic5.jpg ├── LICENSE ├── software ├── makefile ├── tinycharger.hex └── TinyCharger.ino └── README.md /hardware/TinyCharger_BOM.tsv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/hardware/TinyCharger_BOM.tsv -------------------------------------------------------------------------------- /hardware/TinyCharger_gerber.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/hardware/TinyCharger_gerber.zip -------------------------------------------------------------------------------- /documentation/TinyCharger_pic1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/documentation/TinyCharger_pic1.jpg -------------------------------------------------------------------------------- /documentation/TinyCharger_pic2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/documentation/TinyCharger_pic2.jpg -------------------------------------------------------------------------------- /documentation/TinyCharger_pic3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/documentation/TinyCharger_pic3.jpg -------------------------------------------------------------------------------- /documentation/TinyCharger_pic4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/documentation/TinyCharger_pic4.jpg -------------------------------------------------------------------------------- /documentation/TinyCharger_pic5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/documentation/TinyCharger_pic5.jpg -------------------------------------------------------------------------------- /hardware/TinyCharger_schematic.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/HEAD/hardware/TinyCharger_schematic.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. 2 | To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or send 3 | a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 4 | -------------------------------------------------------------------------------- /software/makefile: -------------------------------------------------------------------------------- 1 | # =================================================================================== 2 | # Project: tinyCharger 3 | # Author: Stefan Wagner 4 | # Year: 2021 5 | # URL: https://github.com/wagiminator 6 | # =================================================================================== 7 | # Type "make help" in the command line. 8 | # =================================================================================== 9 | 10 | # Input and Output File Names 11 | SKETCH = TinyCharger.ino 12 | TARGET = tinycharger 13 | 14 | # Microcontroller Settings 15 | DEVICE ?= attiny85 16 | CLOCK = 1000000 17 | LFUSE = 0x62 18 | HFUSE = 0xD5 19 | EFUSE = 0xFF 20 | 21 | # Programmer Settings 22 | PROGRMR ?= usbasp 23 | 24 | # Toolchain 25 | CC = avr-gcc 26 | OBJCOPY = avr-objcopy 27 | OBJDUMP = avr-objdump 28 | AVRSIZE = avr-size 29 | AVRDUDE = avrdude -c $(PROGRMR) -p $(DEVICE) 30 | CLEAN = rm -f *.lst *.obj *.cof *.list *.map *.eep.hex *.o *.s *.d 31 | 32 | # Compiler Flags 33 | CFLAGS = -Wall -Os -flto -mmcu=$(DEVICE) -DF_CPU=$(CLOCK) -x c++ 34 | 35 | # Symbolic Targets 36 | help: 37 | @echo "Use the following commands:" 38 | @echo "make all compile and build $(TARGET).elf/.bin/.hex/.asm for $(DEVICE)" 39 | @echo "make hex compile and build $(TARGET).hex for $(DEVICE)" 40 | @echo "make asm compile and disassemble to $(TARGET).asm for $(DEVICE)" 41 | @echo "make bin compile and build $(TARGET).bin for $(DEVICE)" 42 | @echo "make upload compile and upload to $(DEVICE) using $(PROGRMR)" 43 | @echo "make fuses burn fuses of $(DEVICE) using $(PROGRMR) programmer" 44 | @echo "make install compile, upload and burn fuses for $(DEVICE)" 45 | @echo "make clean remove all build files" 46 | 47 | all: buildelf buildbin buildhex buildasm removetemp size 48 | 49 | elf: buildelf removetemp size 50 | 51 | bin: buildelf buildbin removetemp size removeelf 52 | 53 | hex: buildelf buildhex removetemp size removeelf 54 | 55 | asm: buildelf buildasm removetemp size removeelf 56 | 57 | flash: upload fuses 58 | 59 | install: upload fuses 60 | 61 | upload: hex 62 | @echo "Uploading $(TARGET).hex to $(DEVICE) using $(PROGRMR) ..." 63 | @$(AVRDUDE) -U flash:w:$(TARGET).hex:i 64 | 65 | fuses: 66 | @echo "Burning fuses of $(DEVICE) ..." 67 | @$(AVRDUDE) -U lfuse:w:$(LFUSE):m -U hfuse:w:$(HFUSE):m -U efuse:w:$(EFUSE):m 68 | 69 | clean: 70 | @echo "Cleaning all up ..." 71 | @$(CLEAN) 72 | @rm -f $(TARGET).elf $(TARGET).bin $(TARGET).hex $(TARGET).asm 73 | 74 | buildelf: 75 | @echo "Compiling $(SKETCH) for $(DEVICE) @ $(CLOCK)Hz ..." 76 | @$(CC) $(CFLAGS) $(SKETCH) -o $(TARGET).elf 77 | 78 | buildbin: 79 | @echo "Building $(TARGET).bin ..." 80 | @$(OBJCOPY) -O binary -R .eeprom $(TARGET).elf $(TARGET).bin 81 | 82 | buildhex: 83 | @echo "Building $(TARGET).hex ..." 84 | @$(OBJCOPY) -j .text -j .data -O ihex $(TARGET).elf $(TARGET).hex 85 | 86 | buildasm: 87 | @echo "Disassembling to $(TARGET).asm ..." 88 | @$(OBJDUMP) -d $(TARGET).elf > $(TARGET).asm 89 | 90 | size: 91 | @echo "------------------" 92 | @echo "FLASH: $(shell $(AVRSIZE) -d $(TARGET).elf | awk '/[0-9]/ {print $$1 + $$2}') bytes" 93 | @echo "SRAM: $(shell $(AVRSIZE) -d $(TARGET).elf | awk '/[0-9]/ {print $$2 + $$3}') bytes" 94 | @echo "------------------" 95 | 96 | removetemp: 97 | @echo "Removing temporary files ..." 98 | @$(CLEAN) 99 | 100 | removeelf: 101 | @echo "Removing $(TARGET).elf ..." 102 | @rm -f $(TARGET).elf 103 | -------------------------------------------------------------------------------- /software/tinycharger.hex: -------------------------------------------------------------------------------- 1 | :10000000A7C0B6C0B5C0B4C0B3C0B2C0B1C0B0C064 2 | :10001000AFC0AEC079C1ACC0ABC0AAC0A9C07C1F84 3 | :100020000220022002207C1F0000000000000000CF 4 | :100030007C1F001F8220822082207C000000822002 5 | :10004000822082207C1F7C008000800080007C1F3A 6 | :100050007C00822082208220001F7C1F8220822040 7 | :100060008220001F0C000200020002007C1F7C1F87 8 | :100070008220822082207C1F7C008220822082209D 9 | :100080007C1FF03F8C0082008C00F03FFE3F8220FE 10 | :100090008220822002207C10822082208220041F65 11 | :1000A00002000200FE3F02000200FE0700180020CE 12 | :1000B0000018FE07FE3F800080008000003F803F68 13 | :1000C0008000803F8000003F0000300630060000C6 14 | :1000D000000000000000000000000000C01CA0188C 15 | :1000E000901488028401FC3F042006200420FC2F89 16 | :1000F000FC3F043806380438FC3FFC3F043F063F11 17 | :10010000043FFC3FFC3FE43FE63FE43FFC3FFC3F55 18 | :10011000FC3FFE3FFC3FFC3FE80364000A00010097 19 | :10012000A81F2001DA028D14AFA1C8100A0FFF101A 20 | :100130000E12FF100A12FF64005E01EE02E80312C5 21 | :100140000C0B0D11FFB80B100EA00F0410FFFF00D9 22 | :1001500011241FBECFE5D2E0DEBFCDBF20E0A0E678 23 | :10016000B0E001C01D92A436B207E1F7F2D090C210 24 | :1001700047CF282FB89898E080E0880FBA98B099B8 25 | :100180008160BA9A9150C9F72111B89ABA98BA9A6F 26 | :100190000895B89ABA98B898089598E087FF02C071 27 | :1001A000B89801C0B89ABA98BA9A880F9150B1F726 28 | :1001B000B898BA98BA9A08951F93CF93DF93E82F0F 29 | :1001C000EE0F880F880F880FCE2FC80F80E0E5DF75 30 | :1001D00080E0E3DFDAE0DC0F11E01C0FEC2FF0E051 31 | :1001E000E25EFF4F8491D9DFC12FD113F5CFDF91AC 32 | :1001F000CF911F910895B89ABA9ACFCFCF93C82FB5 33 | :1002000088E7F9DF80E0C9DF82E2C7DF8C2FC5DF36 34 | :1002100081E08C0FC2DF80E0C0DF80E1BEDF8C2F89 35 | :10022000806BBBDFCF91B5CFCF93DF93C82F80E832 36 | :10023000E2DF8C2FB2DFB898BA9881E8DCDF81E08A 37 | :1002400098DFC82F80E095DFD0E0DC2FCC27C82BCB 38 | :10025000A0DFCE01DF91CF910895CF93C82F8436D0 39 | :1002600008F0C3E688E7C7DF80E497DF80E0CA30A4 40 | :1002700018F08F5FCA50FBCF9FDF8C2F9DDFCF918F 41 | :1002800088CF0F931F93CF93DF93EC01C03187E2A8 42 | :10029000D80710F0CFE0D7E288E7ADDF80E47DDF5C 43 | :1002A00008E111E0F8012591349180E0C217D307ED 44 | :1002B00020F08F5FC21BD30BF9CF7EDF0E5F1F4F85 45 | :1002C00081E00032180771F7DF91CF911F910F91F4 46 | :1002D00060CFCF93DF93EC0188E78DDF80E45DDFB3 47 | :1002E000FE0184918F3F19F067DF2196F9CFDF91EE 48 | :1002F000CF914FCFCF93C82F88E77DDF80E44DDFCC 49 | :100300008C2F5ADFCF9145CF1F920F920FB60F92CD 50 | :1003100011248F939F93AF93BF938091600090912E 51 | :100320006100A0916200B09163000196A11DB11D12 52 | :100330008093600090936100A0936200B09363008B 53 | :10034000BF91AF919F918F910F900FBE0F901F9013 54 | :100350001895CF93DF93CDB7DEB72A970FB6F894F1 55 | :10036000DEBF0FBECDBFC19ABB9ABC9A80E843DF07 56 | :1003700080E013DF86E011DF87E60FDF0ADF80E829 57 | :100380003ADF85E00ADF84E108DF80E006DF01DF95 58 | :100390008CE789BD82E08ABD83BF80E189BF88E7A1 59 | :1003A0002ADF80E0FADE60E271E07A8369830BE2A3 60 | :1003B00011E0E981FA818491F0DE69817A816F5FD1 61 | :1003C0007F4F7A8369830617170799F7E2DE80E08B 62 | :1003D00015DF88E710DF80E4E0DE198280E0DDDEF3 63 | :1003E0007981715079837111F9CFD3DE82E006DF14 64 | :1003F00088E701DF80E4D1DE80E0CFDE89818150B3 65 | :1004000089838111F9CFC5DEF89460916000709105 66 | :100410006100809162009091630069837A838B838D 67 | :100420009C83789401E010E0812C912C5401412CA4 68 | :10043000512C320182E0F8DE969587951C017CEF05 69 | :10044000272284E0F1DE9A87898705978B3F9F47B3 70 | :1004500010F01A861986D12CED2DF0E0EE0FFF1F5B 71 | :10046000EB5BFE4F859194918215930510F4D39424 72 | :10047000F3CFE985FA85369728F0F4E0DF1202C061 73 | :1004800023E0D22EF8946091600070916100809119 74 | :100490006200909163006D837E838F839887789448 75 | :1004A00089859A85069708F12D813E814F8158856F 76 | :1004B00069817A818B819C81261B370B480B590BF4 77 | :1004C000420E531E641E751EE985FA85CF01A0E019 78 | :1004D000B0E0BC01CD018DD020E13EE040E050E035 79 | :1004E000B5D0820E931EA41EB51EC301B20128EE24 80 | :1004F00033E040E050E0AAD07901203A3C4820F0B7 81 | :100500009FE9E92E9CE8F92EB19916C0011116C099 82 | :1005100069857A856630710588F41F5F143008F0AC 83 | :1005200010E010FF02C0C39A01C0C39811FF02C0BF 84 | :10053000C49A04C0C49802C000E001C001E080E099 85 | :100540005DDE84E18D0DD6DE89859A85069710F0F3 86 | :1005500083E101C082E1CEDE8FE391E0BADEE12FDC 87 | :10056000F0E0EE0FFF1FE95CFE4F8591949189DE6C 88 | :1005700083E391E0AEDEC70160E17EE053D06C0121 89 | :10058000862FB8DE81E1B6DEC6016CE370E04AD0AA 90 | :10059000862F63DE81E1AEDEC7016CE370E042D0FE 91 | :1005A0005CDE82E02BDEC1016CDE8FE291E091DE49 92 | :1005B00089859A8566DE83E391E08BDEC501B4010F 93 | :1005C00028EE33E040E050E041D0C9015ADE8BE232 94 | :1005D00091E07FDEA3EDB0E31197F1F700C00000DA 95 | :1005E0006D817E818F81988569837A838B839C83DB 96 | :1005F00021CFEE27FF27AA27BB2708C0A20FB31FD2 97 | :10060000E41FF51F220F331F441F551F9695879532 98 | :100610007795679598F37040A9F7009799F7BD0112 99 | :10062000CF010895AA1BBB1B51E107C0AA1FBB1F26 100 | :10063000A617B70710F0A61BB70B881F991F5A956E 101 | :10064000A9F780959095BC01CD010895A1E21A2EDD 102 | :10065000AA1BBB1BFD010DC0AA1FBB1FEE1FFF1F66 103 | :10066000A217B307E407F50720F0A21BB30BE40BB6 104 | :10067000F50B661F771F881F991F1A9469F76095FD 105 | :100680007095809590959B01AC01BD01CF010895B7 106 | :04069000F894FFCF0C 107 | :00000001FF 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TinyCharger - Single Cell Li-Ion Battery Charger with Monitoring 2 | TinyCharger is an ATtiny25/45/85-based, single-cell Li-Ion battery charger with selectable charging current limit (100mA - 1000mA) and an OLED display for monitoring. 3 | 4 | - Project Video (Youtube): https://youtu.be/JtS_8n5oOiI 5 | - Design Files (EasyEDA): https://easyeda.com/wagiminator/attiny85-lipo-charger 6 | 7 | ![pic1.jpg](https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/main/documentation/TinyCharger_pic1.jpg) 8 | 9 | # Hardware 10 | ## Power Connectors 11 | The device is equipped with a Micro-USB and a USB Type-C connector for power supply. Only one connector can be used at a time. The supply voltage must be 5V. 12 | 13 | ## Battery Connectors 14 | The device is equipped with a JST-PH 2.0mm socket and a 2.54mm pin header for connection to the battery. Only one battery can be charged at a time. 15 | 16 | ## Battery Charger 17 | For battery charging the [TP4056](https://datasheet.lcsc.com/szlcsc/1904031009_TPOWER-TP4056_C382139.pdf) is used. The TP4056 is a complete constant-current/constant-voltage linear charger for single-cell lithium-ion batteries. The maximum charge voltage is fixed at 4.2V and the charge current can be programmed externally with a resistor. The total resistance is determined by three resistors connected in parallel, two of which can be switched on and off by the ATtiny via MOSFETs. The TP4056 automatically terminates the charge cycle when the charge current drops to 1/10th the programmed value after the final float voltage is reached. Other features include current monitor, under voltage lockout and automatic recharge. 18 | 19 | ## Voltage and Current Measurement 20 | An [INA219](https://www.ti.com/lit/ds/symlink/ina219.pdf) is used to measure voltage and current. The INA219 is a current shunt and power monitor with an I²C-compatible interface. The device monitors both shunt voltage drop and bus supply voltage, with programmable conversion times and filtering. A programmable calibration value, combined with an internal multiplier, enables direct readouts of current in amperes. The selected shunt resistance of 8mΩ enables both a very small influence on the circuit and a measurement with a resolution of 1mA. For an accurate measurement, a shunt resistor with a low tolerance (1% or better) should be selected. 21 | 22 | ## User Interface 23 | The user interface utilizes two buttons and an [SSD1306 128x32 pixels OLED display](http://aliexpress.com/wholesale?SearchText=128+32+0.91+oled). An [ATtiny25/45/85](https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf) microcontroller handles the user interface as well as the control and monitoring of the charging functions. 24 | 25 | ![pic4.jpg](https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/main/documentation/TinyCharger_pic4.jpg) 26 | 27 | # Software 28 | ## Basic Principle 29 | The charging current is programmed via a resistor, the value of which can be set by the ATtiny via two MOSFETs and changed by pressing the SET button. An INA219 continuously measures the charging current and voltage and transmits the values to the ATtiny via I2C. From this, the ATtiny calculates the state of charge and capacity and displays the values on the OLED screen. 30 | 31 | ## I²C OLED Implementation 32 | The I²C protocol implementation is based on a crude bitbanging method. It was specifically designed for the limited resources of ATtiny10 and ATtiny13, but it works with some other AVRs (including the ATtiny25/45/85) as well. The functions for the OLED are adapted to the SSD1306 OLED module, but they can easily be modified to be used for other modules. In order to save resources, only the basic functionalities which are needed for this application are implemented. For a detailed information on the working principle of the I²C OLED implementation visit [TinyOLEDdemo](https://github.com/wagiminator/attiny13-tinyoleddemo). 33 | 34 | ## Accuracy of Time and Capacity Determination 35 | The internal oscillator of the ATtiny is used to determine the charging time and the electric charge. The accuracy of the internal oscillator is +/-10% with the factory calibration. This can be improved to +/-2% or better by [manual calibration](https://github.com/wagiminator/ATtiny84-TinyCalibrator). The calibration value determined in this way can be set in the source code. In addition to the time measurement, the internal resistance of the battery being charged also plays a role for the accuracy of the calculated capacity. Due to these factors, the displayed capacity is only a rough estimate. 36 | 37 | ## Compiling and Uploading 38 | Since there is no ICSP header on the board, you have to program the ATtiny either before soldering using an [SOP adapter](https://aliexpress.com/wholesale?SearchText=sop-8+150mil+adapter), or after soldering using an [EEPROM clip](https://aliexpress.com/wholesale?SearchText=sop8+eeprom+programming+clip). The [AVR Programmer Adapter](https://github.com/wagiminator/AVR-Programmer/tree/master/AVR_Programmer_Adapter) can help with this. 39 | 40 | ### If using the Arduino IDE 41 | - Make sure you have installed [ATtinyCore](https://github.com/SpenceKonde/ATTinyCore). 42 | - Go to **Tools -> Board -> ATtinyCore** and select **ATtiny25/45/85 (No bootloader)**. 43 | - Go to **Tools** and choose the following board options: 44 | - **Chip:** ATtiny25 or 45 or 85 (depending on your chip) 45 | - **Clock:** 1 MHz (internal) 46 | - **B.O.D.Level:** B.O.D. enabled (2.7V) 47 | - Leave the rest at the default settings 48 | - Connect your programmer to your PC and to the ATtiny. 49 | - Go to **Tools -> Programmer** and select your ISP programmer (e.g. [USBasp](https://aliexpress.com/wholesale?SearchText=usbasp)). 50 | - Go to **Tools -> Burn Bootloader** to burn the fuses. 51 | - Open TinyCharger sketch and click **Upload**. 52 | 53 | ### If using the precompiled hex-file 54 | - Make sure you have installed [avrdude](https://learn.adafruit.com/usbtinyisp/avrdude). 55 | - Connect your programmer to your PC and to the ATtiny. 56 | - Open a terminal. 57 | - Navigate to the folder with the hex-file. 58 | - Execute the following command (if necessary replace "t85" with your chip and "usbasp" with the programmer you use): 59 | ``` 60 | avrdude -c usbasp -p t85 -U lfuse:w:0x62:m -U hfuse:w:0xd5:m -U efuse:w:0xff:m -U flash:w:tinycharger.hex 61 | ``` 62 | 63 | ### If using the makefile (Linux/Mac) 64 | - Make sure you have installed [avr-gcc toolchain and avrdude](http://maxembedded.com/2015/06/setting-up-avr-gcc-toolchain-on-linux-and-mac-os-x/). 65 | - Connect your programmer to your PC and to the ICSP header of the device. 66 | - Open a terminal. 67 | - Navigate to the folder with the makefile and the Arduino sketch. 68 | - Run `DEVICE=attiny85 PROGRMR=usbasp make install` to compile, burn the fuses and upload the firmware (change DEVICE and PROGRMR accordingly). 69 | 70 | # Operating Instructions 71 | 1. Connect the device via the Micro-USB or USB Type-C port to a 5V power supply that can provide sufficient current. 72 | 2. Use the SET button to select the maximum charging current. Note that it can overshoot by a few milliamperes! 73 | 3. Connect the Li-Ion battery to one of the battery connectors. Pay attention to the correct polarity! 74 | 4. The battery is charged immediately. The SET button is locked during the charging process to prevent the charging current from being changed accidentally. The charging process stops automatically when the battery is fully charged. The total charging time and the charged capacity remain displayed as long as the device is supplied with power. These values can be reset using the RESET button. 75 | 76 | ![pic5.jpg](https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/main/documentation/TinyCharger_pic5.jpg) 77 | 78 | # Characteristics 79 | |Parameter|Value| 80 | |:-|:-| 81 | |Supply Voltage|4.3 - 5.5V| 82 | |Charge Voltage Limit|4.2V| 83 | |Charge Current Limits|100, 350, 750, 1000mA (selectable)| 84 | |Voltage Measurement Resolution|4mV| 85 | |Current Measurement Resolution|1mA| 86 | 87 | # References, Links and Notes 88 | 1. [ATtiny25/45/85 Datasheet](https://ww1.microchip.com/downloads/en/DeviceDoc/Atmel-2586-AVR-8-bit-Microcontroller-ATtiny25-ATtiny45-ATtiny85_Datasheet.pdf) 89 | 2. [TP4056 Datasheet](https://datasheet.lcsc.com/szlcsc/1904031009_TPOWER-TP4056_C382139.pdf) 90 | 3. [INA219 Datasheet](https://www.ti.com/lit/ds/symlink/ina219.pdf) 91 | 4. [SSD1306 Datasheet](https://cdn-shop.adafruit.com/datasheets/SSD1306.pdf) 92 | 5. [128x32 OLED on Aliexpress](http://aliexpress.com/wholesale?SearchText=128+32+0.91+oled) 93 | 94 | ![pic2.jpg](https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/main/documentation/TinyCharger_pic2.jpg) 95 | ![pic3.jpg](https://raw.githubusercontent.com/wagiminator/ATtiny85-TinyCharger/main/documentation/TinyCharger_pic3.jpg) 96 | 97 | # License 98 | ![license.png](https://i.creativecommons.org/l/by-sa/3.0/88x31.png) 99 | 100 | This work is licensed under Creative Commons Attribution-ShareAlike 3.0 Unported License. 101 | (http://creativecommons.org/licenses/by-sa/3.0/) 102 | -------------------------------------------------------------------------------- /software/TinyCharger.ino: -------------------------------------------------------------------------------- 1 | // =================================================================================== 2 | // Project: TinyCharger - Li-Ion Battery Charger based on ATtiny25/45/85 3 | // Version: v1.1 4 | // Year: 2021 5 | // Author: Stefan Wagner 6 | // Github: https://github.com/wagiminator 7 | // EasyEDA: https://easyeda.com/wagiminator 8 | // License: http://creativecommons.org/licenses/by-sa/3.0/ 9 | // =================================================================================== 10 | // 11 | // Description: 12 | // ------------ 13 | // TinyCharger is a simple single cell Li-Ion battery charger with selectable 14 | // charging current and OLED display for charging monitoring. The TP4056 is used 15 | // for charging. The charging current is programmed via a resistor, the value of 16 | // which can be set by the ATtiny via two MOSFETs and changed by pressing the 17 | // SET button. An INA219 continuously measures the charging current and voltage 18 | // and transmits the values to the ATtiny via I2C. From this, the ATtiny calculates 19 | // the state of charge and capacity and displays the values on the OLED screen. 20 | // 21 | // References: 22 | // ----------- 23 | // The I²C OLED implementation is based on TinyOLEDdemo 24 | // https://github.com/wagiminator/ATtiny13-TinyOLEDdemo 25 | // 26 | // Wiring: 27 | // ------- 28 | // +-\/-+ 29 | // RESET --- RST ADC0 PB5 1|° |8 Vcc 30 | // PROG1 ------- ADC3 PB3 2| |7 PB2 ADC1 -------- OLED/INA SCK 31 | // PROG2 ------- ADC2 PB4 3| |6 PB1 AIN1 OC0B --- SET BUTTON 32 | // GND 4| |5 PB0 AIN0 OC0A --- OLED/INA SDA 33 | // +----+ 34 | // 35 | // Compilation Settings: 36 | // --------------------- 37 | // Core: ATtinyCore (https://github.com/SpenceKonde/ATTinyCore) 38 | // Board: ATtiny25/45/85 (No bootloader) 39 | // Chip: ATtiny25 or 45 or 85 (depending on your chip) 40 | // Clock: 1 MHz (internal) 41 | // B.O.D.: 2.7V 42 | // 43 | // Leave the rest on default settings. Don't forget to "Burn bootloader"! 44 | // No Arduino core functions or libraries are used. Use the makefile if 45 | // you want to compile without Arduino IDE. 46 | // 47 | // Note: The internal oscillator may need to be calibrated for precise 48 | // charging capacity calculation. 49 | // 50 | // Fuse settings: -U lfuse:w:0x62:m -U hfuse:w:0xd5:m -U efuse:w:0xff:m 51 | // 52 | // Operating Instructions: 53 | // ----------------------- 54 | // Connect the device via the Micro-USB or USB-C port to a 5V power supply that 55 | // can provide sufficient current. Use the SET button to select the maximum 56 | // charging current. Note that it can overshoot by a few milliamperes! Connect 57 | // the Li-Ion battery to one of the battery connectors. Pay attention to the 58 | // correct polarity! The battery is charged immediately. The SET button is locked 59 | // during the charging process to prevent the charging current from being changed 60 | // accidentally. The charging process stops automatically when the battery is 61 | // fully charged. The total charging time and the charged capacity remain 62 | // displayed as long as the device is supplied with power. These values can be 63 | // reset using the RESET button. 64 | 65 | 66 | // =================================================================================== 67 | // Libraries, Definitions and Macros 68 | // =================================================================================== 69 | 70 | // Oscillator calibration value (uncomment and set if necessary) 71 | // #define OSCCAL_VAL 0x48 72 | 73 | // Libraries 74 | #include // for GPIO 75 | #include // for interrupts 76 | #include // for data stored in program memory 77 | #include // for delays 78 | 79 | // Pin definitions 80 | #define PIN_SDA PB0 // I2C serial data pin (OLED/INA219) 81 | #define PIN_SCL PB2 // I2C serial clock pin (OLED/INA219) 82 | #define PIN_SET PB1 // pin connected to SET button 83 | #define PIN_PROG1 PB3 // charging current prog pin 1 84 | #define PIN_PROG2 PB4 // charging current prog pin 2 85 | 86 | // Pin manipulation macros 87 | #define pinInput(x) DDRB &= ~(1<<(x)) // set pin to INPUT 88 | #define pinOutput(x) DDRB |= (1<<(x)) // set pin to OUTPUT 89 | #define pinLow(x) PORTB &= ~(1<<(x)) // set pin to LOW 90 | #define pinHigh(x) PORTB |= (1<<(x)) // set pin to HIGH 91 | #define pinPullup(x) PORTB |= (1<<(x)) // enable PULLUP resistor 92 | #define pinRead(x) (PINB & (1<<(x))) // READ pin 93 | 94 | // Current limit values and battery level voltage thresholds 95 | const uint16_t PROG_CURRENT[] PROGMEM = {100, 350, 750, 1000}; 96 | const uint16_t BAT_LEVEL[] PROGMEM = {3000, 3600, 4000, 4100, 65535}; 97 | 98 | // =================================================================================== 99 | // I2C Master Implementation 100 | // =================================================================================== 101 | 102 | // I2C macros 103 | #define I2C_SDA_HIGH() pinInput(PIN_SDA) // release SDA -> pulled HIGH by resistor 104 | #define I2C_SDA_LOW() pinOutput(PIN_SDA) // SDA as output -> pulled LOW by MCU 105 | #define I2C_SCL_HIGH() pinInput(PIN_SCL) // release SCL -> pulled HIGH by resistor 106 | #define I2C_SCL_LOW() pinOutput(PIN_SCL) // SCL as output -> pulled LOW by MCU 107 | #define I2C_SDA_READ() pinRead(PIN_SDA) // read SDA line 108 | #define I2C_CLOCKOUT() I2C_SCL_HIGH();I2C_SCL_LOW() // clock out 109 | 110 | // I2C transmit one data byte to the slave, ignore ACK bit, no clock stretching allowed 111 | void I2C_write(uint8_t data) { 112 | for(uint8_t i=8; i; i--, data<<=1) { // transmit 8 bits, MSB first 113 | (data&0x80)?I2C_SDA_HIGH():I2C_SDA_LOW(); // SDA depending on bit 114 | I2C_CLOCKOUT(); // clock out -> slave reads the bit 115 | } 116 | I2C_SDA_HIGH(); // release SDA for ACK bit of slave 117 | I2C_CLOCKOUT(); // 9th clock pulse is for the ignored ACK bit 118 | } 119 | 120 | // I2C start transmission 121 | void I2C_start(uint8_t addr) { 122 | I2C_SDA_LOW(); // start condition: SDA goes LOW first 123 | I2C_SCL_LOW(); // start condition: SCL goes LOW second 124 | I2C_write(addr); // send slave address 125 | } 126 | 127 | // I2C restart transmission 128 | void I2C_restart(uint8_t addr) { 129 | I2C_SDA_HIGH(); // prepare SDA for HIGH to LOW transition 130 | I2C_SCL_HIGH(); // restart condition: clock HIGH 131 | I2C_start(addr); // start again 132 | } 133 | 134 | // I2C stop transmission 135 | void I2C_stop(void) { 136 | I2C_SDA_LOW(); // prepare SDA for LOW to HIGH transition 137 | I2C_SCL_HIGH(); // stop condition: SCL goes HIGH first 138 | I2C_SDA_HIGH(); // stop condition: SDA goes HIGH second 139 | } 140 | 141 | // I2C receive one data byte from the slave (ack=0 for last byte, ack>0 if more bytes to follow) 142 | uint8_t I2C_read(uint8_t ack) { 143 | uint8_t data = 0; // variable for the received byte 144 | I2C_SDA_HIGH(); // release SDA -> will be toggled by slave 145 | for(uint8_t i=8; i; i--) { // receive 8 bits 146 | data <<= 1; // bits shifted in right (MSB first) 147 | I2C_SCL_HIGH(); // clock HIGH 148 | if(I2C_SDA_READ()) data |= 1; // read bit 149 | I2C_SCL_LOW(); // clock LOW -> slave prepares next bit 150 | } 151 | if(ack) I2C_SDA_LOW(); // pull SDA LOW to acknowledge (ACK) 152 | I2C_CLOCKOUT(); // clock out -> slave reads ACK bit 153 | return data; // return the received byte 154 | } 155 | 156 | // =================================================================================== 157 | // OLED Implementation 158 | // =================================================================================== 159 | 160 | // OLED definitions 161 | #define OLED_ADDR 0x78 // OLED write address 162 | #define OLED_CMD_MODE 0x00 // set command mode 163 | #define OLED_DAT_MODE 0x40 // set data mode 164 | #define OLED_INIT_LEN 11 // 9: no screen flip, 11: screen flip 165 | 166 | // OLED init settings 167 | const uint8_t OLED_INIT_CMD[] PROGMEM = { 168 | 0xA8, 0x1F, // set multiplex for 128x32 169 | 0x20, 0x01, // set vertical memory addressing mode 170 | 0xDA, 0x02, // set COM pins hardware configuration to sequential 171 | 0x8D, 0x14, // enable charge pump 172 | 0xAF, // switch on OLED 173 | 0xA1, 0xC8 // flip the screen 174 | }; 175 | 176 | // OLED 5x16 font 177 | const uint8_t OLED_FONT[] PROGMEM = { 178 | 0x7C, 0x1F, 0x02, 0x20, 0x02, 0x20, 0x02, 0x20, 0x7C, 0x1F, // 0 0 179 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x1F, // 1 1 180 | 0x00, 0x1F, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x7C, 0x00, // 2 2 181 | 0x00, 0x00, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x7C, 0x1F, // 3 3 182 | 0x7C, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x7C, 0x1F, // 4 4 183 | 0x7C, 0x00, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x00, 0x1F, // 5 5 184 | 0x7C, 0x1F, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x00, 0x1F, // 6 6 185 | 0x0C, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x7C, 0x1F, // 7 7 186 | 0x7C, 0x1F, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x7C, 0x1F, // 8 8 187 | 0x7C, 0x00, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x7C, 0x1F, // 9 9 188 | 0xF0, 0x3F, 0x8C, 0x00, 0x82, 0x00, 0x8C, 0x00, 0xF0, 0x3F, // A 10 189 | 0xFE, 0x3F, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x02, 0x20, // E 11 190 | 0x7C, 0x10, 0x82, 0x20, 0x82, 0x20, 0x82, 0x20, 0x04, 0x1F, // S 12 191 | 0x02, 0x00, 0x02, 0x00, 0xFE, 0x3F, 0x02, 0x00, 0x02, 0x00, // T 13 192 | 0xFE, 0x07, 0x00, 0x18, 0x00, 0x20, 0x00, 0x18, 0xFE, 0x07, // V 14 193 | 0xFE, 0x3F, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x00, 0x3F, // h 15 194 | 0x80, 0x3F, 0x80, 0x00, 0x80, 0x3F, 0x80, 0x00, 0x00, 0x3F, // m 16 195 | 0x00, 0x00, 0x30, 0x06, 0x30, 0x06, 0x00, 0x00, 0x00, 0x00, // : 17 196 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 18 197 | 0xC0, 0x1C, 0xA0, 0x18, 0x90, 0x14, 0x88, 0x02, 0x84, 0x01, // CHRG 19 198 | 0xFC, 0x3F, 0x04, 0x20, 0x06, 0x20, 0x04, 0x20, 0xFC, 0x2F, // BAT0 20 199 | 0xFC, 0x3F, 0x04, 0x38, 0x06, 0x38, 0x04, 0x38, 0xFC, 0x3F, // BAT1 21 200 | 0xFC, 0x3F, 0x04, 0x3F, 0x06, 0x3F, 0x04, 0x3F, 0xFC, 0x3F, // BAT2 22 201 | 0xFC, 0x3F, 0xE4, 0x3F, 0xE6, 0x3F, 0xE4, 0x3F, 0xFC, 0x3F, // BAT3 23 202 | 0xFC, 0x3F, 0xFC, 0x3F, 0xFE, 0x3F, 0xFC, 0x3F, 0xFC, 0x3F // BAT4 24 203 | }; 204 | 205 | // OLED BCD conversion array 206 | const uint16_t DIVIDER[] PROGMEM = {1000, 100, 10, 1}; 207 | 208 | // OLED init function 209 | void OLED_init(void) { 210 | I2C_start(OLED_ADDR); // start transmission to OLED 211 | I2C_write(OLED_CMD_MODE); // set command mode 212 | for(uint8_t i=0; i> 4)); // set high nibble of start column 225 | I2C_write(0xB0 | (ypos)); // set start page 226 | I2C_stop(); // stop transmission 227 | } 228 | 229 | // OLED clear screen 230 | void OLED_clearScreen(void) { 231 | OLED_setCursor(0, 0); // set cursor at upper half 232 | I2C_start(OLED_ADDR); // start transmission to OLED 233 | I2C_write(OLED_DAT_MODE); // set data mode 234 | uint8_t i = 0; // count variable 235 | do {I2C_write(0x00);} while(--i); // clear upper half 236 | I2C_stop(); // stop transmission 237 | OLED_setCursor(0, 2); // set cursor at lower half 238 | I2C_start(OLED_ADDR); // start transmission to OLED 239 | I2C_write(OLED_DAT_MODE); // set data mode 240 | do {I2C_write(0x00);} while(--i); // clear upper half 241 | I2C_stop(); // stop transmission 242 | } 243 | 244 | // OLED plot a character 245 | void OLED_plotChar(uint8_t ch) { 246 | ch = (ch << 1) + (ch << 3); // calculate position of character in font array 247 | I2C_write(0x00); I2C_write(0x00); // print spacing between characters 248 | for(uint8_t i=10; i; i--) I2C_write(pgm_read_byte(&OLED_FONT[ch++])); // print character 249 | } 250 | 251 | // OLED print a character 252 | void OLED_printChar(uint8_t ch) { 253 | I2C_start(OLED_ADDR); // start transmission to OLED 254 | I2C_write(OLED_DAT_MODE); // set data mode 255 | OLED_plotChar(ch); // plot the character 256 | I2C_stop(); // stop transmission 257 | } 258 | 259 | // OLED print a string from program memory; terminator: 255 260 | void OLED_printString(const uint8_t* p) { 261 | I2C_start(OLED_ADDR); // start transmission to OLED 262 | I2C_write(OLED_DAT_MODE); // set data mode 263 | uint8_t ch = pgm_read_byte(p); // read first character from program memory 264 | while(ch < 255) { // repeat until string terminator 265 | OLED_plotChar(ch); // plot character on OLED 266 | ch = pgm_read_byte(++p); // read next character 267 | } 268 | I2C_stop(); // stop transmission 269 | } 270 | 271 | // OLED print 16-bit value as 4-digit decimal (BCD conversion by substraction method) 272 | void OLED_printDec4(uint16_t value) { 273 | if(value > 9999) value = 9999; // limit 4-digit value 274 | I2C_start(OLED_ADDR); // start transmission to OLED 275 | I2C_write(OLED_DAT_MODE); // set data mode 276 | for(uint8_t digit = 0; digit < 4; digit++) { // 4 digits 277 | uint8_t digitval = 0; // start with digit value 0 278 | uint16_t divider = pgm_read_word(&DIVIDER[digit]); // current divider 279 | while(value >= divider) { // if current divider fits into the value 280 | digitval++; // increase digit value 281 | value -= divider; // decrease value by divider 282 | } 283 | OLED_plotChar(digitval); // print the digit 284 | } 285 | I2C_stop(); // stop transmission 286 | } 287 | 288 | // OLED print 8-bit value as 2-digit decimal (BCD conversion by substraction method) 289 | void OLED_printDec2(uint8_t value) { 290 | if(value > 99) value = 99; // limit 2-digit value 291 | I2C_start(OLED_ADDR); // start transmission to OLED 292 | I2C_write(OLED_DAT_MODE); // set data mode 293 | uint8_t digitval = 0; // start with digit value 0 294 | while(value >= 10) { // if current divider fits into the value 295 | digitval++; // increase digit value 296 | value -= 10; // decrease value by divider 297 | } 298 | OLED_plotChar(digitval); // print first digit 299 | OLED_plotChar(value); // print second digit 300 | I2C_stop(); // stop transmission 301 | } 302 | 303 | // =================================================================================== 304 | // INA219 Implementation 305 | // =================================================================================== 306 | 307 | // INA219 register values 308 | #define INA_ADDR 0x80 // I2C write address of INA219 309 | #define INA_CONFIG 0b0000011001100111 // INA config register according to datasheet 310 | #define INA_CALIB 5120 // INA calibration register according to R_SHUNT 311 | #define INA_REG_CONFIG 0x00 // INA configuration register address 312 | #define INA_REG_CALIB 0x05 // INA calibration register address 313 | #define INA_REG_SHUNT 0x01 // INA shunt voltage register address 314 | #define INA_REG_VOLTAGE 0x02 // INA bus voltage register address 315 | #define INA_REG_POWER 0x03 // INA power register address 316 | #define INA_REG_CURRENT 0x04 // INA current register address 317 | 318 | // INA219 write a register value 319 | void INA_write(uint8_t reg, uint16_t value) { 320 | I2C_start(INA_ADDR); // start transmission to INA219 321 | I2C_write(reg); // write register address 322 | I2C_write(value >> 8); // write register content high byte 323 | I2C_write(value); // write register content low byte 324 | I2C_stop(); // stop transmission 325 | } 326 | 327 | // INA219 read a register 328 | uint16_t INA_read(uint8_t reg) { 329 | uint16_t result; // result variable 330 | I2C_start(INA_ADDR); // start transmission to INA219 331 | I2C_write(reg); // write register address 332 | I2C_restart(INA_ADDR | 0x01); // restart for reading 333 | result = (uint16_t)(I2C_read(1) << 8) | I2C_read(0); // read register content 334 | I2C_stop(); // stop transmission 335 | return result; // return result 336 | } 337 | 338 | // INA219 write inital configuration and calibration values 339 | void INA_init(void) { 340 | INA_write(INA_REG_CONFIG, INA_CONFIG); // write INA219 configuration 341 | INA_write(INA_REG_CALIB, INA_CALIB); // write INA219 calibration 342 | } 343 | 344 | // INA219 read voltage 345 | uint16_t INA_readVoltage(void) { 346 | return((INA_read(INA_REG_VOLTAGE) >> 1) & 0xFFFC); 347 | } 348 | 349 | // INA219 read sensor values 350 | uint16_t INA_readCurrent(void) { 351 | uint16_t result = INA_read(INA_REG_CURRENT); // read current from INA 352 | if(result > 32767) result = 0; // ignore negative currents 353 | if(result < 5) result = 0; // ignore tiny currents 354 | return result; // return result 355 | } 356 | 357 | // =================================================================================== 358 | // Millis Counter Implementation for Timer0 359 | // =================================================================================== 360 | 361 | volatile uint32_t MIL_counter = 0; // millis counter variable 362 | 363 | // Init millis counter 364 | void MIL_init(void) { 365 | OCR0A = 124; // TOP: 124 = 1000kHz / (8 * 1kHz) - 1 366 | TCCR0A = (1< 5); // set charging state variable 430 | batState = 0; // set state of charge ... 431 | while(voltage > pgm_read_word(&BAT_LEVEL[batState])) batState++; 432 | if(isCharging && (batState == 4)) batState = 3; 433 | 434 | // Timing stuff 435 | nowmillis = MIL_read(); // read millis 436 | interval = nowmillis - lastmillis; // calculate time interval 437 | lastmillis = nowmillis; // update lastmillis 438 | if(isCharging) { // if in charging mode: 439 | duration += interval; // calculate total charging duration 440 | capacity += interval * current / 3600; // calculate uAh 441 | } 442 | seconds = (uint32_t)duration / 1000; // calculate total charging time in seconds 443 | if(seconds > 35999) seconds = 35999; // limit seconds timer 444 | 445 | // Check button and set charging current limit accordingly (blocked in charging mode) 446 | if(pinRead(PIN_SET)) lastbutton = 0; 447 | else if(!lastbutton) { 448 | if(!isCharging) { 449 | if(++progState > 3) progState = 0; 450 | (progState & 0x01) ? pinHigh(PIN_PROG1) : pinLow(PIN_PROG1); 451 | (progState & 0x02) ? pinHigh(PIN_PROG2) : pinLow(PIN_PROG2); 452 | } 453 | lastbutton = 1; 454 | } 455 | 456 | // Update OLED 457 | OLED_setCursor(0,0); 458 | OLED_printChar(batState + 20); 459 | isCharging ? OLED_printChar(19) : OLED_printChar(18); 460 | OLED_printString(SET); 461 | OLED_printDec4(pgm_read_word(&PROG_CURRENT[progState])); 462 | OLED_printString(mA); 463 | OLED_printChar(seconds / 3600); 464 | OLED_printChar(17); 465 | OLED_printDec2((seconds % 3600) / 60); 466 | OLED_printChar(17); 467 | OLED_printDec2(seconds % 60); 468 | 469 | OLED_setCursor(0,2); 470 | OLED_printDec4(voltage); 471 | OLED_printString(mV); 472 | OLED_printDec4(current); 473 | OLED_printString(mA); 474 | OLED_printDec4(capacity / 1000); 475 | OLED_printString(mAh); 476 | 477 | _delay_ms(50); 478 | } 479 | } 480 | --------------------------------------------------------------------------------