Keywords
: PY32SLM6500SHT20 battery charging PW5100PW20 58.9-inch OLED driver circuit Type-C trick 5V
Reason for creation: I've used many DHT11 sensors, but their accuracy seems questionable. I happened to see this temperature and humidity sensor on Taobao, so I bought one to play around with, and it was also a good opportunity to try out battery power, something I hadn't done before. Long post warning!!!
All files (schematics, PCB, complete project code) are open source on the LCSC open source platform. For detailed images and text, please visit http://t.csdnimg.cn/kN38A
Objective/Requirements
: Powered by a single 18650 battery.
Continuously displays temperature, humidity, battery level, etc.
Type-C charging capability.
Uses PY32F002A as the main controller.
Minimizes power consumption to maintain long operating time. When searching
for
battery charging circuits
on Taobao, I found that most were from the TP series, with the TP4056 being a classic example. However, I found its charging speed too slow, so I chose the SLM6500 SOP-8E packaged charging chip. It offers a faster charging current of 2 amps, and its price of around 1 RMB is acceptable. The
official application circuit is shown below .
This chip is relatively easy to understand; for those who are unsure, please refer to the datasheet.
In the actual circuit
, the datasheet shows that the current-limiting resistor RS1 controls the charging current. Here, I chose a 0.06Ω resistor, which keeps the charging current above 1.5A.
NC and NS are charging status indicator lights. According to the manual, the NC and NS pins are normally floating. For example, when charging is complete, NS is pulled to ground; otherwise, they are floating. If I want to use a microcontroller to read this level, I need to find a way to avoid this floating state. Here, I wanted to use the on/off state of q1 and q2 to control the two LEDs to turn on and off, with the microcontroller detecting the pin voltage to read the battery charging status. However, this approach proved unsuitable in practice. When the pin is pulled to ground by the chip, the transistor can conduct, and the microcontroller can read the correct pin level. The problem is that the LEDs are too dim. Specifically, the forward voltage of the LEDs is too low. Replacing them with P-type MOSFETs (using CJ3401) solves this problem.
Another aspect is the design of the NTC thermistor. Consulting the datasheet reveals that when the voltage at the TS terminal is below 45% or above 80% of the VCC voltage, the chip will enter overheat or low-temperature protection and stop charging. Due to the non-linearity of the NTC resistor, a simple series voltage divider approach wouldn't be linear enough. Therefore, a parallel-series voltage divider approach was used. However, since the seller couldn't provide a resistance value table for the purchased NTC resistor, a pull-up resistor to VCC was used to disable the temperature protection. Regarding
the system power supply circuit,
we know that the standard voltage of a single 18650 battery is 2.75~4.2V. A simpler solution would be to use a linear regulator, such as the AMS117-3.3/HT7330. However, this has a drawback: when the power supply voltage is lower than the target voltage of the linear regulator plus the transistor's voltage drop, the output voltage will become unstable, affecting ADC sampling. Therefore, I need a more stable power supply voltage, regardless of whether the battery voltage is below 3.3V. Thus, a boost-then-buck approach was chosen. The circuit
for obtaining 5V from Type-C
is quite simple. It's worth noting the two 5.1K resistors; without them, a stable 5V voltage might not be obtained. Adding these resistors establishes a charging protocol between the device and the charger, requiring the charger to supply 5V to the device. (This seems to be the PD protocol.)
The PW5100 boost circuit
uses a fixed-output, low-power boost control chip. Initial verification showed the system's maximum operating current wouldn't exceed 15mA (at 3.3V), so we chose a 100uH inductor.
The PW5100's EN terminal is connected to VCC. The Q3 P-type MOSFET
in the diagram serves as a reverse connection protection, preventing damage to the circuit if the battery is installed incorrectly. After obtaining a stable 5V voltage from the PW5100, I used the PW2058 to step it down to 3.3V to power the microcontroller, OLED, and SHT20. Why not use 5V directly? Because OLEDs cannot be directly powered by 5V, the OLED modules we buy usually use an LDO chip to step down the 5V to supply power to the OLED. Since I'm using a bare screen, I need to add an extra step-down circuit, so I simply stepped down the 5V to 3.3V. The PY32F002A circuit itself has a simple peripheral circuit. I'm using the internal crystal oscillator instead of an external one, leaving a programming port, an ADC channel to read the battery voltage, two IICs, two external inputs, and a reset port. The 0.91-inch OLED driver circuit is straightforward; just pay attention to the pull-up resistor. The SHT20 circuit is also simple; make sure to include the power supply filter capacitor C27, and a larger capacitance is recommended. The connectors shown in some diagrams are actually my own designs, similar to switches and the SMD0805 package. Soldering them on makes them conductive, mainly for convenience when measuring power consumption. They can be removed and connected directly with wires. Our software design is also very simple, without using any advanced features. Initializing SHT20-IIC: For the SHT20, I used software IIC. The IIC initialization code is directly pasted below: /** * @brief Initialize I2C related MSPs */ void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c) { GPIO_InitTypeDef GPIO_InitStruct = {0}; __HAL_RCC_SYSCFG_CLK_ENABLE(); /*SYSCFG clock enable*/ __HAL_RCC_I2C_CLK_ENABLE(); /*I2C clock enable*/ __HAL_RCC_GPIOA_CLK_ENABLE(); /*GPIOA clock enable*/ /**I2C GPIO Configuration PA3 ------> I2C1_SCL PA2 ------> I2C1_SDA */ GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_OD; /* Open-drain */
GPIO_InitStruct.Pull = GPIO_PULLUP; /* Pull-up */
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_I2C; /* Multiplexed to I2C */
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* GPIO initialization */
/* Reset I2C */
__HAL_RCC_I2C_FORCE_RESET();
__HAL_RCC_I2C_RELEASE_RESET();
/* I2C1 interrupt Init */
HAL_NVIC_SetPriority(I2C1_IRQn, 0, 0); /* Interrupt priority setting */
HAL_NVIC_EnableIRQ(I2C1_IRQn); /* Enable I2C interrupt */
}
void APP_IICInit(void)
{
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK) {
Error_Handler();
}
}
![Click and drag to move.]()
This is only the initialization of the I2C driver; we also need to initialize SHT20.
According to the SHT20 official manual, it mainly has the following registers.
Its user configuration register is as follows.
Here I mainly edit bits 7 and 0 to configure its conversion precision. To configure its user configuration register, a specific timing is required. It is not simply a matter of notifying SHT20 that I want to write to your user register. How do I send the configuration content? The timing given in the official manual is as follows.
We need to first send the write instruction and the address to be read is the user configuration register. Then we start sending the read instruction and receive the fixed 00000010 and judge it. Then we send the write instruction, the write address and the content to be written. Here, the write address and the content to be written are sent together. The code implementation is as follows:
/**
* @brief Initialize SHT20, default maximum resolution
*
*/
void SHT20_Init(void)
{
uint8_t buff[1];
/* First REST */
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[0], 1, 100);
HAL_Delay(20);
/* Send user register */
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[1], 1, 100);
/*Read user register*/
HAL_I2C_Master_Receive(&hi2c1, SHT_R, buff, 1, 100);
/*Judgment and setting*/
if (buff[0] == 0x02) {
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, set, 2, 100);
}
}
![Click and drag to move.]()
When modifying the configuration, you only need to modify the first bit of the set array.
OLED-IIC
I used hardware IIC for OLED, mainly because the driver I had was hardware-driven, I was lazy!
So I configured two pins into output mode, and I won't post the code.
ADC
Here, one ADC is used to detect the battery voltage to display the battery level. The ADC is configured as a non-continuous scan, single conversion mode, which I manually triggered in the loop.
void APP_ADCConfig(void)
{
__HAL_RCC_ADC_FORCE_RESET();
__HAL_RCC_ADC_RELEASE_RESET();
__HAL_RCC_ADC_CLK_ENABLE(); /* Enable ADC clock */
hadc.Instance = ADC1;
if (HAL_ADCEx_Calibration_Start(&hadc) != HAL_OK) /* ADC calibration */
{
Error_Handler();
}
hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; /* Set ADC clock */
hadc.Init.Resolution = ADC_RESOLUTION_12B; /* Conversion resolution 12bit */
hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* Right alignment of data */
hadc.Init.ScanConvMode = DISABLE; /* Scan sequence direction: upward (from channel 0 to channel 11) */
hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* ADC_EOC_SINGLE_CONV: single sampling; ADC_EOC_SEQ_CONV: sequence sampling */
hadc.Init.LowPowerAutoWait = ENABLE; /* ENABLE = start the next conversion after reading the ADC value; DISABLE = direct conversion */
hadc.Init.ContinuousConvMode = DISABLE; /* Single conversion mode */
hadc.Init.DiscontinuousConvMode = DISABLE; /* /* Disable discontinuous mode */
hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* Software trigger */
hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; /* No trigger edge */ // hadc.Init.Overru = ADC_OVR_DATA_OVERWRITTEN; /* Overwrite the previous value when overload occurs */
hadc.Init.SamplingTimeCommon = ADC_SAMPLETIME_41CYCLES_5; /* Channel sampling time is 41.5 ADC clock cycles */
if (HAL_ADC_Init(&hadc) != HAL_OK) /* ADC initialization */
{
Error_Handler();
}
sConfig.Rank = 1; /* Set whether to rank. To set single-channel sampling, you need to configure ADC_RANK_NONE */
sConfig.Channel = ADC_CHANNEL_0; /* Set sampling channel */
if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) /* Configure ADC channel */
{
Error_Handler();
}
}
![Click and drag to move.]()
Other initializations Other initializations
mainly include the initialization of the OLED and the two battery charging status read pins, which are relatively simple and therefore not included here.
The main code
for reading the SHT20
refers to its manual. The SHT measurement read has master hold mode and non-hold mode; refer to the manual's explanation. Below,
I am using non-hold mode for convenience in handling other tasks. Hold mode will not be described. The timing of non-hold mode is as follows,
where the gray block is controlled by the SHT20. If the measurement is not completed during the "read" command, the sensor will not provide an ACK on bit 27. If bit 45 is changed to NACK, followed by a stop condition (P) checksum transmission, the checksum transmission is omitted.
The code implementation is as follows:
uint8_t cm[4] = {0xfe, 0xe7, 0xe3, 0xe5}; // SHT20 command
/**
* @brief Read SHT20 temperature data
*
* @return float
*/
void read_temp(void)
{
uint8_t buff[2];
uint16_t to = 0;
/* Send write command, then send the command to read temperature */
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[2], 1, 100);
if (sign_t == 0) {
tim_t = HAL_GetTick();
sign_t = 1;
}
las_t = HAL_GetTick();
if ((las_t > tim_t && (las_t - tim_t) >= 90) || las_t tim_t && (2 ^ 32 - tim_t + las_t) >= 90) {
/* Send a read command, passing the pointer to the data to be read */
HAL_I2C_Master_Receive(&hi2c1, SHT_R, buff, 2, 100);
to = (uint16_t)buff[0];
to = to 8;
to = (buff[1] & 0xFC) | to;
temp = -46.85 + 175.72 * ((float)to / 65536);
sign_t = 0;
las_t = 0;
}
}
/**
* @brief Read the humidity data of SHT20
*
* @return float Return the transformed temperature data 0.00 ~
*/
void read_humi(void)
{
uint8_t buff[2];
uint16_t to = 0;
/* Send the write command, and then send the command to read the humidity */
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[3], 1, 100);
if (sign_h == 0) {
tim_h = HAL_GetTick();
sign_h = 1;
}
las_h = HAL_GetTick();
if ((las_h > tim_h && (las_h - tim_h) >= 35) || las_h 2 ^ 32 - tim_h + las_h) >= 35) {
/* Send a read command and pass the pointer to read the data */
HAL_I2C_Master_Receive(&hi2c1, SHT_R, buff, 2, 100);
to = (uint16_t)buff[0];
to = to 8;
to = (buff[1] & 0xFC) | to;
humi = -6 + 125 * ((float)to / 65536);
sign_h = 0;
las_h = 0;
}
}
![Click and drag to move.]()
First, send the measurement conversion command, then delay and wait for the conversion. After the delay, convert the received data into the actual temperature and humidity according to the requirements of the manual.
Unlike the blocking delay of HAL_Delay, I used a tick timer for non-blocking delay here. The main idea is to record the current tick timer value as the initial value for comparison at the start of the delay and update the state machine sign_t to ensure that the initial value for comparison is not updated before the end of a delay. Then, las_t reads the current tick timer value and compares the difference between las_t and tim_t to determine whether the delay time has been reached. If the delay time has been reached, relevant operations and clearing of flags are performed. The advantage of this is that it improves the real-time performance of the loop and reduces the impact on other operations in the loop. The disadvantage is that this delay method cannot accurately delay the time and may have the risk of "timeout". For example, if the delay is 100ms, after the current tick timer value is recorded, the system executes other tasks, and one of the tasks needs to be executed for 500ms. When the delay judgment is reached, although the delay target has been reached, the delay time has been much longer than the set 100ms, which limits its use.
To determine the battery status
, refer to the SLM datasheet. NCHRG (pin 3) is the charging status indicator. When the charger is charging the battery, this pin is pulled low by an internal switch, indicating that charging is in progress; otherwise, the pin is in a high-impedance state. NSTDBY (pin 4) is the charging completion indicator. When the battery is fully charged, this pin is pulled low by an internal switch, indicating that charging is complete; otherwise, the pin is in a high-impedance state.
Therefore, we can determine whether the battery is charging or fully charged by reading whether these two pins are low. It's worth noting that when there is no battery, but the charger is connected, the NCHRG and NSTDBY pins will output a PWM-like signal level. In this case, the normal reading method cannot be used, and I haven't handled this state in the program.
The battery level is determined by stepping down the voltage to below 3.1V using a voltage divider resistor and then reading the value using an ADC. The ADC value is then converted to a battery level of 0-30% for use by the battery level display program.
/* Check if charging is in progress */
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET && HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) != GPIO_PIN_RESET) {
sign_d = 1; // Indicates charging
} else if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) == GPIO_PIN_RESET && HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) != GPIO_PIN_RESET) {
sign_d = 2; // Indicates fully charged
} else {
sign_d = 0;
}
/* While charging, cycle through the battery level from 0 to 100 */
if (sign_d == 1) {
if (sign_p == 0) {
tim_d = HAL_GetTick();
sign_d = 1;
}
las_d = HAL_GetTick();
if ((las_t > tim_t && (las_t - tim_t) >= 500) || las_t tim_t && (2 ^ 32 - tim_t + las_t) >= 500) {
/* Send a read command, passing the pointer to the data to be read */
showDL(sign_j++);
if (sign_j >= 30) {
sign_j = 0;
}
sign_t = 0;
las_t = 0;
}
}
/*If fully charged but the charger is not unplugged*/
if (sign_d == 2) {
showDL(30);
}
/*If not charging, read the ADC value and perform mean filtering*/
if (sign_d == 0) {
HAL_ADC_Start(&hadc);
HAL_ADC_PollForConversion(&hadc, 1000000);
adc_value[i_adc] = HAL_ADC_GetValue(&hadc);
i_adc++;
if (i_adc >= 9) {
for (uint8_t i = 0; i 10; i++) {
adcValue = adcValue + adc_value[i];
}
adcValue = adcValue / 10;
/* Calculate the measured voltage value. When using a voltage divider resistor, limit the voltage obtained to 3.1V. */
DC_value = ((float)adcValue / 4095.0) * 3.3;
i_adc = 0;
}
/* Divide the measured voltage into ranges and display it. Here, the battery voltage is limited to 2.9~4.2V. A voltage divider resistor of 2K / 5.6K is selected, so the corresponding measured voltage is 3.0947~2.1368. */
if (DC_value >= (3.0947 - ((3.0947 - 2.1368) / 30))) {
showDL(30);
} else if (DC_value 2.1368 + ((3.0947 - 2.1368) / 30))) {
showDL(0);
} else {
showDL((DC_value - 2.1368) / 0.03193); The
battery icon and power display
are due to the use of an OLED screen. A battery frame can be drawn using a model-taking software. Here, I've drawn a battery frame that is 32 pixels high and 16 pixels wide. After taking the model, the following array will be obtained and displayed as an image: uint8_t BP[64] = { 0xF8, 0x08, 0x08, 0x0F, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x08, 0x08, 0xF8, 0xFF, 0x0 ... 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0xFF, /*"Initial Battery Box",0*/ }; Now that we have the battery box, how do we display different battery levels inside it? Here, the pixels within the frame are edited one by one, with 14 pixels per layer, resulting in 30 layers. Different layers display different battery percentages. Observing the array obtained by modulo operation, we can see that its image storage method is binary, arranged vertically. Parsing the first row of the array, as follows (the cells without content are '0'): 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 The values ' 1' represent the pixels lit up on the OLED. If we want to change the display to the following table to show the battery percentage for each layer: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
![Click and drag to move.]()
Simply change the highest bit of the 16 elements in the first row of the array to ' 1 ' , and repeat
the process to display the battery percentage
. /** * @brief Function to update the battery display array * * @param nu Battery percentage 0~30 */ uint8_t cnt = 0; void showDL(uint8_t nu) { memcpy(DL, BP, sizeof(BP)); if (nu != 0) { for (uint8_t j = 0; j 14; j++) { if (nu 7) { DL[49 + j] = (BP[49 + j] >> nu) | (0xff 8 - nu)); } if (nu 15 && nu > 7) { DL[49 + j] = (BP[49 + j]) | 0xff; DL[33 + j] = (BP[33 + j]) | (0xff 8 - (nu % 8 + 1))); } if (nu > 15 && nu 23) { DL[49 + j] = (BP[49 + j] >> nu) | 0xff; DL[33 + j] = (BP[33 + j]) | (0xff 8 - (nu % 8 + 1))); } if (nu > 15 && nu 23) { DL[49 + j] = (BP[49 + j] >> nu) | 0xff; DL[33 + j] = (BP[33 + j]) | 0xff 8 - (nu % 8 + 1))); } j]) | 0xff; DL[33 + j] = (BP[33 + j]) | 0xff; DL[17 + j] = (BP[17 + j]) | (0xff 8 - (nu % 8 + 1))); } if (nu > 23 && nu 27) { DL[49 + j] = (BP[49 + j]) | 0xff; DL[33 + j] = (BP[33 + j]) | 0xff; DL[17 + j] = (BP[17 + j]) | 0xff; DL[1 + j] = (BP[1 + j]) | (0xff 8 - (nu % 8 + 1))); } if (nu > 27) { DL[49 + j] = (BP[49 + j]) | 0xff; DL[33 + j] = (BP[33 + j]) | 0xff; DL[17 + j] = (BP[17 + j]) | 0xff; if ((j + 1) 3 || (j + 1) >= 12) { DL[1 + j] = (BP[1 + j]) | 0xf8; } else { DL[1 + j] = (BP[1 + j]) | (0xff 8 - (nu % 8 + 1))); } } } } OLED_DrawBMP(112, 0, 128, 4, 1); } Power consumption test Test location Current mA Remarks Power bus 10.2±0.2 Battery power supply current to the whole system OLED power bus 4.5±0.2 Inaccurate PY32 (mcu) 0.6±0.1 Microcontroller power supply current SHT20 The 0.000050±0.000005 value is inaccurate. The 3.3V bus current is 10.2±0.2. Why is the current of the 3.3V power supply inaccurate? Because the IIC communication line also provides current. Observing the 3.3V bus current, it is much greater than the current of OLED+PY32+SHT20. During testing, I only disconnected its power supply line (3.3V). However, during testing, I disconnected the OLED's VCC, and PY32 and SHT20 worked normally. At this time, the OLED could still light up, although it was slightly flickering. The same was true for the tests on SHT20 and PY32! Power Consumption Analysis : Why are the input and output currents of the system power supply (battery input, boosted to 5V, then stepped down to 3.3V output) basically the same? Because PW5100 and PW2058 are both switching power devices with high conversion efficiency. Furthermore, the measurements were taken manually, which introduces a large error, so they are basically the same. (We only have one ammeter; otherwise, we could use two to observe and compare.) Let's calculate how long this battery will last.
![Click and drag to move.]()
The overall power consumption was remeasured using a 3.7V power supply. After the system stabilized, the following was recorded:

the battery was rated at 9800mWh. Based on this, we calculated that
the total power consumption per hour

could theoretically allow for

approximately 10.7 days of continuous use (the calculation method may not be entirely accurate). For the temperature/humidity
demonstration , since I didn't have calibrated instruments, I could only check if the display matched my empirical values. Please see the video; after several days of use, I felt it was quite accurate (judging by experience). The refresh rate and response speed were also good. The battery icon charging animation is also shown. In conclusion, the expected functionality was achieved. However, in terms of power consumption, using one battery for about 10 days is not ideal. Due to the use of an OLED screen, even with its brightness reduced to the minimum, the power consumption is still significant. Therefore, we could forgo the OLED screen and use a lower-power digital tube or LCD segment display, which could potentially control the operating current to around 5mA, greatly extending the usage time. This design also has many drawbacks, such as the lack of an automatic low-battery shutdown function and wasted MCU resources. PS: I'm a complete novice; please feel free to correct any mistakes and discuss !