关键词:PY32\SLM6500\SHT20\电池充电\PW5100\PW2058\0.9寸OLED驱动电路\Type-c诱骗5V
制作原因:用了很多DHT11,感觉精度很玄乎,偶然在TB看见了这款温湿度传感器,买一个来玩,正好也可以玩一下没玩过的电池供电。长文预警!!!
所有文件(原理图、PCB、完整工程代码)均在立创开源平台开源,需要的请自行前往下载:工程 - 立创开源硬件平台
目标/要求
- 使用单节18650电池供电。
- 连续显示温度、湿度、电池电量等信息。
- 可以Type-c充电。
- 使用PY32F002A作为主控。
- 尽量低功耗以保持长久的使用时间。
方案选型/电路设计
电池充电电路
在淘宝搜索发现大部分的都是TP系列,比较经典的就是TP4056,但是我嫌弃它的充电速度太慢,所以找到一个型号为SLM6500的SOP-8E封装的充电芯片,充电电流两安要快一些,而且1块钱左右的价格也可以接受。
官方应用电路如下
该芯片比较好理解,不清楚的可以看手册。
实际电路
查询手册可以发现,限流电阻RS1控制充电电流,这里我选用0.06Ω的电阻,可以把充电电流控制在大于1.5A。
NC和NC是充电状态指示灯,查询手册可以知道NC和NS引脚常态悬空,比如当充电完成的时候,NS拉到地,其他的时候是悬空的,如果我想用单片机来读取这个电平,就要想办法避免他的悬空状态,这里我想利用q1和q2的开/断去控制两颗LED的亮和灭,单片机检测引脚电压,实现电池充电状态的读取,在实际使用中发现不可取,当引脚被芯片拉到地时,三极管可以导通,单片机也可以读到正确引脚电平,而不可取的地方在于LED太暗了,具体的原因是LED的正向电压太小了,换成P型MOS管可以解决(换了CJ3401)。
另外就是NTC热敏电阻的设计,查询手册可以知道芯片在TS端的电压低于45%或者高于80%的VCC电压时,芯片会进入过热或者低温保护停止充电,由于NTC电阻的非线性,我们如果想用单纯的串联分压的方式不太线性,所以这里用了并联加串联分压的方式,但是最终由于买回来的ntc电阻阻值表商家无法提供,所以直接用电阻上拉到VCC取消温度保护。
系统供电电路
我们知道18650电池单节标准电压为2.75~4.2V,简单一点的方案可以直接使用线性稳压器,比如AMS117-3.3/HT7330等,这样有一个弊端,当我们的电源电压低于线性稳压器的目标电压加上管压降时,输出电压将不再稳定,这会影响到adc采样,所以我想我需要一个更加稳定的电源电压,不管他电池电压是否低于3.3V,所以选择先升压后降压的方案!
Type-c获取5V
这电路很简单,值得注意的是两个5.1K电阻,如果没有这两个电阻,可能获取不到稳点的5V电压。而加入这两个电阻后相当于设备于充电器之间建立了充电协议,协议要求充电器给设备供电5V。(好像是PD协议来着)
PW5100升压电路
PW5100为固定输出的低功耗升压控制芯片,在前期的验证可以得出系统的工作电流最大不超过15mA(3.3V时),则经过计算我们将电感值选择为100uH。
PW5100的EN端连接到VCC。
而图中的Q3 P型MOS管在电路中其到防反接的作用,以防止电池在安装时出现装反的现象损坏电路。
PW2058降压电路
在通过PW5100获得稳定的5V电压后,我通过PW2058将其降压为3.3V给单片机、OLED、SHT20等供电。
为什么不直接使用5V呢?因为OLED不能5V直接供电,我们买到的OLED模块一般会使用一颗LDO芯片将5V降压才能供给OLED,我这里使用的是裸屏,还要额外加一个降压电路,所以我就直接5V降3.3V。
PY32F002A电路
PY32本身的外围电路就简单,我这里不用外部晶振,使用内部晶振,留出了烧写口、一个ADC通道读取电池电压、两组IIC、两个外部输入、复位。
0.91寸OLED驱动电路
没啥好说的,注意上拉电阻。
SHT20电路
也很简单,注意电源滤波电容C27一定要放,并且建议容值大一点。
几张图中出现的其实是我自己画的连接器,类似开关,和SMD0805封装差不多,将其焊上为导通,主要测功耗时方便一点,可以把它去掉直接导线连接。
软件设计
我们的软件其实也非常简单,没用到什么高级功能。
初始化
SHT20-IIC
对于SHT20我使用了软件的IIC,IIC初始化代码直接贴
/**
* @brief 初始化I2C相关MSP
*/
void HAL_I2C_MspInit(I2C_HandleTypeDef *hi2c)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_SYSCFG_CLK_ENABLE(); /*SYSCFG时钟使能*/
__HAL_RCC_I2C_CLK_ENABLE(); /*I2C时钟使能*/
__HAL_RCC_GPIOA_CLK_ENABLE(); /*GPIOA时钟使能*/
/**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; /*开漏*/
GPIO_InitStruct.Pull = GPIO_PULLUP; /*上拉*/
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF12_I2C; /*复用为I2C*/
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /*GPIO初始化*/
/*复位I2C*/
__HAL_RCC_I2C_FORCE_RESET();
__HAL_RCC_I2C_RELEASE_RESET();
/* I2C1 interrupt Init */
HAL_NVIC_SetPriority(I2C1_IRQn, 0, 0); /*中断优先级设置*/
HAL_NVIC_EnableIRQ(I2C1_IRQn); /*使能I2C中断*/
}
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();
}
}
这只是对IIC驱动的初始化,我们还需要对SHT20进行初始化。
查询SHT20官方手册,可知其主要有以下寄存器
其用户配置寄存器如下
我这里主要编辑第7、0位,配置其转换的精度,而要配置其用户配置寄存器需要特定的时序,并非单纯的通知SHT20我要写你的用户寄存器了,如何就把配置内容发过去,官方手册给出的时序如下
需要我们先发送写指令和读的地址是用户配置寄存器,然后开始发送读指令和接收到固定的00000010并判断,然后再发送写指令和写地址和写的内容,这里的写地址和写的内容是一起发送的,代码实现如下
/**
* @brief 初始化SHT20,默认最高分辨率
*
*/
void SHT20_Init(void)
{
uint8_t buff[1];
/*先REST*/
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[0], 1, 100);
HAL_Delay(20);
/*发用户寄存器*/
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, &cm[1], 1, 100);
/*读用户寄存器*/
HAL_I2C_Master_Receive(&hi2c1, SHT_R, buff, 1, 100);
/*判断并设置*/
if (buff[0] == 0x02) {
HAL_I2C_Master_Transmit(&hi2c1, SHT_W, set, 2, 100);
}
}
修改配置时只要修该set数组的第1位数据即可。
OLED-IIC
OLED我用的硬件IIC,主要时手上的驱动就是硬件驱动的,懒!
所以就两个引脚配成输出模式,代码就不贴了。
ADC
这里使用了一路ADC对电池电压进行检测,以显示电池电量,ADC配置为非连续扫描、单次转换模式,我在循环里自己手动触发。
void APP_ADCConfig(void)
{
__HAL_RCC_ADC_FORCE_RESET();
__HAL_RCC_ADC_RELEASE_RESET();
__HAL_RCC_ADC_CLK_ENABLE(); /* 使能ADC时钟 */
hadc.Instance = ADC1;
if (HAL_ADCEx_Calibration_Start(&hadc) != HAL_OK) /* ADC校准 */
{
Error_Handler();
}
hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV2; /* 设置ADC时钟*/
hadc.Init.Resolution = ADC_RESOLUTION_12B; /* 转换分辨率12bit*/
hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT; /* 数据右对齐 */
hadc.Init.ScanConvMode = DISABLE; /* 扫描序列方向:向上(从通道0到通道11)*/
hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV; /* ADC_EOC_SINGLE_CONV:单次采样 ; ADC_EOC_SEQ_CONV:序列采样*/
hadc.Init.LowPowerAutoWait = ENABLE; /* ENABLE=读取ADC值后,开始下一次转换 ; DISABLE=直接转换 */
hadc.Init.ContinuousConvMode = DISABLE; /* 单次转换模式 */
hadc.Init.DiscontinuousConvMode = DISABLE; /* 不使能非连续模式 */
hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START; /* 软件触发 */
hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; /* 触发边沿无 */ // hadc.Init.Overru= ADC_OVR_DATA_OVERWRITTEN;/* 当过载发生时,覆盖上一个值 */
hadc.Init.SamplingTimeCommon = ADC_SAMPLETIME_41CYCLES_5; /* 通道采样时间为41.5ADC时钟周期 */
if (HAL_ADC_Init(&hadc) != HAL_OK) /* ADC初始化*/
{
Error_Handler();
}
sConfig.Rank = 1; /*设置是否排行, 想设置单通道采样,需配置ADC_RANK_NONE */
sConfig.Channel = ADC_CHANNEL_0; /* 设置采样通道 */
if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK) /* 配置ADC通道 */
{
Error_Handler();
}
}
其他初始化
其他主要还有我们对OLED的初始化和两个电池充电状态读取引脚的初始化,比较简单所以不贴了。
主要代码
读取SHT20
参考其手册,SHT测量读取时有主机保持模式和非保持模式,可以看手册的释疑,如下
我使用的是非保持模式,以方便处理其他事务,对于保持模式不做介绍,非保持模式的时序如下
其中灰色块由 SHT20控制。如果在 “read” 命令时未完成测量,则传感器不会在位 27 上提供 ACK。如果将第 45 位更改为 NACK,后跟停止条件 (P) 校验和传输,则省略校验和传输。
代码实现如下
uint8_t cm[4] = {0xfe, 0xe7, 0xe3, 0xe5}; // SHT20命令
/**
* @brief 读取SHT20的温度数据
*
* @return float
*/
void read_temp(void)
{
uint8_t buff[2];
uint16_t to = 0;
/*发送写指令,在将读取温度的指令发送*/
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) {
/*发送读指令,将读取数据的指针传过去*/
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 读取SHT20的湿度数据
*
* @return float 返回经过变换的温度数据 0.00 ~
*/
void read_humi(void)
{
uint8_t buff[2];
uint16_t to = 0;
/*发送写指令,在将读取湿度的指令发送*/
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 < tim_h && (2 ^ 32 - tim_h + las_h) >= 35) {
/*发送读指令,将读取数据的指针传过去*/
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;
}
}
先发送测量转换指令,然后延时等待转换,延时结束后根据手册的要求将接收到的数据换算成实际的温度与湿度。
区别与HAL_Delay的阻塞式延时,我在这里使用滴答定时器做的非阻塞式延时,主要思路为在开始延时时tim_t记录下当前的滴答定时器值作为比较的初始值,并更新状态机sign_t,以保证在一次延时没结束时不会更新比较的初始值,然后las_t读取当前的滴答定时器值,通过对比las_t和tim_t的差值来知道是否达到延时的时间,如果达到了延时的时间,则进行相关操作和对标志的清零等操作。这样的好处是提高循环的实时性,减少对循环中其它操作的影响,缺点是这种延时方式不能准确延时,可能存在“超时”风险,例如延时100ms,开始记录当前的滴答定时器值后,系统去执行其它的任务了,而有某个任务需要执行500ms,则当执行到延时的判断时,虽然延时目标达到了,但是延时时间已经远远大于设定的100ms,这就有了使用的局限性。
电池状态判断
首先参考SLM手册可知,NCHRG(引脚3)为充电状态指示端。当充电器向电池充电时,该管脚被内部开关拉至低电平,表示充电 正在进行;否则该管脚处于高阻态。 NSTDBY(引脚4)为充电完成指示端。当电池充电完成时,该管脚被内部开关拉至低电平,表示充电完成。 否则该管脚处于高阻态。
所以我们只要读取这两个引脚是否是低电平就可以得知其是否处于充电、充满的状态。值得注意的是在没有电池的时候,接入充电器,NCHRG和NSTDBY引脚会输出类似PWM信号的电平状态,这里就无法使用普通的读取方式判断,我在程序中也没有处理这种状态。
判断电池电量使用的是通过分压电阻降压到3.1V以下再用ADC读取的方式,ADC读取的值再经过换算成电池电量0~30,以供显示电量程序使用。
/*判断是否在充电状态*/
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0) == GPIO_PIN_RESET && HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_3) != GPIO_PIN_RESET) {
sign_d = 1; // 代表在充电
} 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; // 代表充满电了
} else {
sign_d = 0;
}
/*在充电时,则循环显示电池从0到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) {
/*发送读指令,将读取数据的指针传过去*/
showDL(sign_j++);
if (sign_j >= 30) {
sign_j = 0;
}
sign_t = 0;
las_t = 0;
}
}
/*如果充满电但是没拔充电器*/
if (sign_d == 2) {
showDL(30);
}
/*如果没有在充电,则读取ADC的值并进行均值滤波*/
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;
/*换算出测量电压值,在使用分压电阻时,限制分得的电压为3.1V*/
DC_value = ((float)adcValue / 4095.0) * 3.3;
i_adc = 0;
}
/*对测量电压进行分档并显示,这里限制电池电压为2.9~4.2V,选择分压电阻为2K / 5.6K,则对应测量电压为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);
}
}
电池图标及电量显示
由于我们使用的是OLED屏幕。可以用取模软件画一个电池框,我这里画的就是电池框高32个像素点、宽16个像素点,取模后会得到如下一个数组、以图片的形式显示
uint8_t BP[64] = {
0xF8, 0x08, 0x08, 0x0F, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0F, 0x08, 0x08, 0xF8,
0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF,
0xFF, 0x00, 0x00, 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, 0x80, 0x80, 0x80, 0xFF, /*"电池初始框",0*/
};
那么电池的框有了,怎么在里面显示不同的电量呢?
这里通过对框内的像素的进行逐个编辑,以一层像素点为14个,共可以有30层,不同层数的电量显示不同的电量百分比。观察取模得到的数组可以发现,其图像存储方式为二进制,纵向排列,将数组的第一行解析开来如下(为填写内容的格子是‘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’的就是在OLED上被点亮的像素点,如果我们要将显示改为如下表显示一层表示电量
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 |
只需要将数组的第一行的16个元素的最高位改为‘1’即可,如法炮制,便可以显示电量百分比。
/**
* @brief 电量显示数组更新函数
*
* @param nu 电量百分比 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]) | 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);
}
功耗测试
测试位置 | 电流mA | 备注 |
电源总线 | 10.2±0.2 | 电池对整个系统的供电电流 |
OLED电源母线 | 4.5±0.2 | 不准确 |
PY32(mcu) | 0.6±0.1 | 单片机的供电电流 |
SHT20 | 0.000050±0.000005 | 不准确 |
3.3V母线 | 10.2±0.2 | 3.3V电源的电流 |
为什么备注不准确呢?因为IIC通信线也会提供电流,观察3.3V母线母线电流,其远大于OLED+PY32+SHT20的电流,因为在测试时,我只会断开其电源线(3.3V),而在测试中,我断开OLED的VCC,PY32与SHT20正常工作,此时OLED居然还能亮,只是有点忽暗忽灭,对SHT20和PY32的测试亦是如此!!!!!
功耗分析
那为什么系统电源的输入输出电流(电池进线输入,经过5V升压,再降压到3.3V输出)电流基本一致呢?因为PW5100和PW2058都是开关型电源器件,转换效率高,再加上测量时为人工读数有较大误差,所以基本一致了。(只有一台电流表,不然可以两台一起上观察比较)
那么我们来计算一下这颗电池可以使用多久
重新测量整体功耗,使用电源箱3.7V供电,系统稳定后记录:
使用的电池标的9800mWh,则我们就以此计算
一小时整体功耗为
理论可以连续使用
约10.7天(计算方法不一定正确)
效果演示
温度/湿度
由于没有校准的仪器,所以只能看显示是否复合经验值了,请看视频
sht20效果
我几天的使用,感觉还是挺准(凭经验判断),刷新率、响应速度也不错。
电池图标\充电动画
SHT20温湿度计充电效果
结论
预期的功能是实现了。
但就功耗来说,一节电池使用10天左右,并不是很理想,由于使用了OLED屏幕,即便我将他的亮度降到最低,其功耗依然巨大,所以我们也可以不用OLED屏幕,使用功耗更低的数码管或者说LCD段显屏,则有望可以把工作电流控制在5mA左右,大大延长使用时间。
该设计也有很多缺点,比如说没有低电量自动关机功能、mcu有浪费等!!
PS:小小白一个,如果有说错的地方欢迎指正,讨论!