设计思路
之前从公众号看到一篇关于1.5V碱性电池充电的文章,再加上家里小孩玩玩具超级费电,这次刚好收到GD32F427开发板,加上之前买的INA226单路电流电压检测模块,就想着做一个数控电池充电工装,当然也可很简单的改为电源数控表头。
图1 1.5v电池充电电路及分析
这是用LM358制作的一个双路1.5V电池充电器,使用TL431生成2.5V基准,50Ω和100Ω构成分压电路,使LM358的同向端电压为 2.5/150*100=1.666V,当反向端检测的电压达到1.666V时,关断三极管,停止充电。R3100Ω作为限流电阻,保证最大电流不超过60mA。
其实这里还有电池内阻,特别是使用越久电压越低的电池,内阻越大,所以电流不会超过50mA。
硬件设计
1、电路仿真
参考这个电路,我在multisim上也仿真了一个电路,不过我的调流使用的电位器+MOS管实现。
图2 仿真充电电路可行性
2、MOS管实测
实际MOS管选用的FSP740,实测GS电压3.21v开始导通,4.14v可通过500mA电流,MOS管只是轻微发热,如图
图3 MOS管Vgs电压实测: ch1为gs电压电流,ch2是ds电压电流
实际上我们充电时,通过INA226(单路)来采集充电电压电流信息,反馈给gd32,再控制数字电位器调节MOS管Vgs,进而调节电流。够成一个pid反馈环。
3、电路参数分析
设想模块最终可对1.2v镍氢电池,1.5v碱性电池,保证控制mos管的控制电压始终处于大部分都可覆盖到,且能保证控制电压可以关断mos管实现停止充电。实际电路中,图2的R3,R4都使用的微型电位器503,即50k。
先假设充电电池电压1.2v(实际电压约为0.9~1.4v)和1.5v(电池电压约为1.3~1.7v)档,即需要在0.9~1.7v范围内实现控制mos管,电位器(4路10k并联成2.5k)上分压范围为0.9+4.14=5.04v到1.7+3.21=4.91v,实际1.2和1.5v电池充电电流都<100mA,电压取4.2v到5v即可,此时R3电阻分压为4.2v,通过电流为(0.8/2500=0.00032A),R3=4/0.00032=12500欧,R4电阻为12-5=7/0.00032=21875欧,
即: R3: 12.5K, R4: 21.875K
软件设计
好了,硬件分析好了,开始写代码,主要四个部分:
1,INA226驱动,I2C接口;
2,AD8403驱动,spi接口;
3,反馈PID控制,调节充电电流;
4,输出系统。目前是通过usb虚拟串口上报的。
软件基于例程中 cdc_acm (路径为:GD32F4xx_Firmware_Library\Examples\USB\USB_Device\cdc_acm )编写,可以通过USB接口虚拟串口上报数据,便于在上位机上把数据绘制成曲线。
1、INA226驱动代码
在这里向大家推荐一个app,『半导小芯』,可以很方便的查看芯片资料,同类替换,国产化型号等。INA226是具有警报功能的 36V、16 位、超精密 12C 输出电流/电压/功率监控器,IN+/IN-可以通过接不同阻值的采样电阻来采样不同档位的电流,VBUS直接接IN-测量电压为36v,如果加上分压电路可扩展采样电压范围,所以是一个非常适合数控表头的芯片。
图4 INA226模块
我买的模块,将A0/A1都焊接到VCC,此时模块地址为:
地址为 100 0101,即0x45,代码中按这个地址访问I2C即可。
图5 INA226典型电路
和GD32的连接引脚为PB7/PB8,PB7为I2C0_SDA,PB8为I2C0_SCL,并通过2K电阻上拉到3.3V。
.h文件
#define I2C0_IN226_ADDRESS7 0x45
#define CFG_REG 0x00
#define SV_REG 0x01
#define BV_REG 0x02
#define PWR_REG 0x03
#define CUR_REG 0x04
#define CAL_REG 0x05
#define INA226_GET_ADDR 0XFF
#define CFG_RESET (3<<14)
#define SHUNT_OVER_VOLTAGE_ENABLE (1<<15)
#define SHUNT_OVER_VOLTAGE_DISABLE ~(1<<15)
#define SHUNT_UNDER_VOLTAGE_ENABLE (1<<14)
#define SHUNT_UNDER_VOLTAGE_DISABLE ~(1<<14)
#define BUS_OVER_VOLTAGE_ENABLE (1<<13)
#define BUS_OVER_VOLTAGE_DISABLE ~(1<<13)
#define BUS_UNDER_VOLTAGE_ENABLE (1<<12)
#define BUS_UNDER_VOLTAGE_DISABLE ~(1<<12)
#define POWER_OVER_LIMIT_ENABLE (1<<11)
#define POWER_OVER_LIMIT_DISABLE (1<<11)
#define CONVERSION_READY_ENABLE (1<<10)
#define CONVERSION_READY_DISABLE (1<<10)
#define ALERT_POLARITY_ACTIVE_LOW ~(1<<1)
#define ALERT_POLARITY_ACTIVE_HIGH (1<<1)
#define ALERT_LATCH_ENABLE (1)
#define ALERT_LATCH_DISABLE 0xFFFE
#define AVG_MODE_MASK ~(7<<9)
#define AVG_MODE_1 (0<<9)
#define AVG_MODE_4 (1<<9)
#define AVG_MODE_16 (2<<9)
#define AVG_MODE_64 (3<<9)
#define AVG_MODE_128 (4<<9)
#define AVG_MODE_256 (5<<9)
#define AVG_MODE_512 (6<<9)
#define AVG_MODE_1024 (7<<9)
#define BUS_VOLTAGE_CONVERSIOM_TIME_MASK ~(7<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_140_US (0<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_204_US (1<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_332_US (2<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_588_US (3<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_1100_US (4<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_2116_US (5<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_4156_US (6<<6)
#define BUS_VOLTAGE_CONVERSIOM_TIME_8244_US (7<<6)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_MASK ~(7<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_140_US (0<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_204_US (1<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_332_US (2<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_588_US (3<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_1100_US (4<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_2116_US (5<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_4156_US (6<<3)
#define SHUNT_VOLTAGE_CONVERSIOM_TIME_8244_US (7<<3)
#define OPERATING_MODE_MASK ~(7<<0)
#define OPERATING_MODE_POWER_DOWN_1 (0<<0)
#define OPERATING_MODE_SHUNT_VOLTAGE_TRIG (1<<0)
#define OPERATING_MODE_BUS_VOLTAGE_TRIG (2<<0)
#define OPERATING_MODE_SHUNT_BUS_VOLTAGE_TRIG (3<<0)
#define OPERATING_MODE_POWER_DOWN_2 (4<<0)
#define OPERATING_MODE_SHUNT_VOLTAGE_CONT (5<<0)
#define OPERATING_MODE_BUS_VOLTAGE_CONT (6<<0)
#define OPERATING_MODE_SHUNT_BUS_VOLTAGE_CONT (7<<0)
#define INA226_RANG_BUS_VOLTAGE_MV_BIT 1.25f
#define INA226_RANG_SHUNT_VOLTAGE_UV_BIT 2.5f
#define INA226_SAMPLE_RES_MR 1
#define INA226_RANG_CURRENT_MA_MAX 15000
#define INA226_RANG_CURRENT_UA_BIT_X1 (UINT16_T)( INA226_RANG_CURRENT_MA_MAX*1000/(1<<15))
#define INA226_CALIB_REG_DEFAULT_X1 (UINT16_T)( 5120*1000/(INA226_RANG_CURRENT_UA_BIT_X1*INA226_SAMPLE_RES_MR) )
#define INA226_RANG_CURRENT_UA_BIT_X2 (UINT16_T)( INA226_RANG_CURRENT_UA_BIT_X1*2 )
#define INA226_CALIB_REG_DEFAULT_X2 (UINT16_T)( INA226_CALIB_REG_DEFAULT_X1*2 )
typedef struct _INA226_HandlerType INA226_HandlerType;
typedef struct _INA226_HandlerType * pINA226_HandlerType;
#define I2C_SCK GPIO_PIN_8
#define I2C_SDA GPIO_PIN_7
#define IIC_SCL_H gpio_bit_set( GPIOB, I2C_SCK)
#define IIC_SCL_L gpio_bit_reset(GPIOB, I2C_SCK)
#define IIC_SDA_H gpio_bit_set( GPIOB, I2C_SDA)
#define IIC_SDA_L gpio_bit_reset(GPIOB, I2C_SDA)
#define READ_SDA gpio_input_bit_get(GPIOB, I2C_SDA)
#define SDA_READ gpio_mode_set( GPIOB, GPIO_MODE_INPUT, GPIO_PUPD_NONE, I2C_SDA );
#define SDA_WRITE gpio_mode_set( GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, I2C_SDA );
void INA226_Init(void);
void INA226_SendData(uint8_t addr,uint8_t reg,uint16_t data);
uint16_t INA226_ReadData(uint8_t addr);
void INA226_SetRegPointer(uint8_t addr,uint8_t reg);
long INA226_GetShunt_Current(uint8_t addr);
uint16_t INA226_Get_ID(uint8_t addr);
uint16_t INA226_GET_CAL_REG(uint8_t addr);
uint32_t INA226_GetBusVoltage(uint8_t addr);
uint32_t INA226_GetShuntVoltage(uint8_t addr);
uint16_t INA226_Get_Power(uint8_t addr);
void Get_Shunt_voltage(float *Voltage);
void Get_Shunt_Current(float *Current);
.c文件
void INA226_IIC_Init(void)
{
rcu_periph_clock_enable(RCU_GPIOB);
gpio_mode_set( GPIOB, GPIO_MODE_OUTPUT, GPIO_PUPD_PULLUP, I2C_SCK | I2C_SDA );
gpio_output_options_set(GPIOB, GPIO_OTYPE_PP, GPIO_OSPEED_50MHZ, I2C_SCK | I2C_SDA );
IIC_SDA_H;
IIC_SCL_H;
delay_nms(5);
}
void INA226_Init(void)
{
INA226_IIC_Init();
delay_nms(10);
INA226_SendData(INA226_ADDR1, CFG_REG,
AVG_MODE_64 | BUS_VOLTAGE_CONVERSIOM_TIME_2116_US | OPERATING_MODE_SHUNT_BUS_VOLTAGE_CONT );
INA226_SendData(INA226_ADDR1, CAL_REG, 0x800);
}
void INA226_SendData(uint8_t addr, uint8_t reg, uint16_t data)
{
uint8_t temp = 0;
INA226_IIC_Start();
INA226_IIC_Send_Byte(addr);
INA226_IIC_Wait_Ack();
INA226_IIC_Send_Byte(reg);
INA226_IIC_Wait_Ack();
temp = (uint8_t)(data>>8);
INA226_IIC_Send_Byte(temp);
INA226_IIC_Wait_Ack();
temp = (uint8_t)(data&0x00FF);
INA226_IIC_Send_Byte(temp);
INA226_IIC_Wait_Ack();
INA226_IIC_Stop();
}
void INA226_SetRegPointer(uint8_t addr,uint8_t reg)
{
INA226_IIC_Start();
INA226_IIC_Send_Byte(addr);
INA226_IIC_Wait_Ack();
INA226_IIC_Send_Byte(reg);
INA226_IIC_Wait_Ack();
INA226_IIC_Stop();
}
uint16_t INA226_ReadData(uint8_t addr)
{
uint16_t temp=0;
INA226_IIC_Start();
INA226_IIC_Send_Byte(addr+1);
INA226_IIC_Wait_Ack();
temp = INA226_IIC_Read_Byte(1);
temp<<=8;
temp |= INA226_IIC_Read_Byte(0);
INA226_IIC_Stop();
return temp;
}
long INA226_GetShunt_Current(uint8_t addr)
{
long temp=0;
INA226_SetRegPointer(addr, CUR_REG);
temp = INA226_ReadData(addr);
if(temp&0x8000)
{
temp = ~(temp - 1);
temp = (uint16_t)temp - 2 * (uint16_t)temp;
}
return temp;
}
uint16_t INA226_Get_ID(uint8_t addr)
{
uint32_t temp=0;
INA226_SetRegPointer(addr, INA226_GET_ADDR);
temp = INA226_ReadData(addr);
return (uint16_t)temp;
}
uint16_t INA226_GET_CAL_REG(uint8_t addr)
{
uint32_t temp=0;
INA226_SetRegPointer(addr, CAL_REG);
temp = INA226_ReadData(addr);
return (uint16_t)temp;
}
uint32_t INA226_GetBusVoltage(uint8_t addr)
{
uint32_t temp=0;
INA226_SetRegPointer(addr, BV_REG);
temp = INA226_ReadData(addr);
return (uint32_t)temp;
}
uint32_t INA226_GetShuntVoltage(uint8_t addr)
{
uint32_t temp=0;
INA226_SetRegPointer(addr, SV_REG);
temp = INA226_ReadData(addr);
if(temp&0x8000)
temp = ~(temp - 1);
return (uint32_t)temp;
}
uint16_t INA226_Get_Power(uint8_t addr)
{
int16_t temp=0;
INA226_SetRegPointer(addr, PWR_REG);
temp = INA226_ReadData(addr);
return (uint16_t)temp;
}
void GetVoltage(float *Voltage)
{
Voltage[0] = INA226_GetBusVoltage(INA226_ADDR1)*INA226_RANG_BUS_VOLTAGE_MV_BIT;
}
void Get_Shunt_voltage(float *Voltage)
{
Voltage[0] = (INA226_GetShuntVoltage(INA226_ADDR1)*INA226_RANG_SHUNT_VOLTAGE_UV_BIT);
}
void Get_Shunt_Current(float *Current)
{
Current[0] = (INA226_GetShunt_Current(INA226_ADDR1)* 2.5f);
}
void GetPower()
{
GetVoltage(&INA226_data.voltageVal);
Get_Shunt_voltage(&INA226_data.Shunt_voltage);
Get_Shunt_Current(&INA226_data.Shunt_Current);
INA226_data.powerVal=INA226_data.voltageVal*0.001f * INA226_data.Shunt_Current*0.001f;
}
2、AD8403 4路10K,256级数字电位器
图6 AD8403ARU引脚图
和GD32相连的引脚为:CS:PB12;CLK:PB13;SDI:PB15;SDO:PB14;SHDN上拉;RS上拉或接GD32的RESET引脚。
/***********************************************************************************
函 数 名: Write_AD8403
功 能:
参 数:channal - AD8403通道
send_data - 写入值
返 回: 无
**********************************************************************************/
void Write_AD8403( uint8 channal, uint8 send_data)
{
SPI_cs_low();
BSP_SPI_Write(channal);
BSP_SPI_Write(send_data);
SPI_cs_high();
}
3、反馈调节
采集端:INA226,构成电流、电压采样输入;
控制端:4路数字电位器驱动MOS管;
以其中一路来说,控制环路如下:
图7 反馈环路示意图
这里电压电流不会跳变,所以采样频率不用太快,保证电流电压不会过调即可。我是用的一个100ms的定时器处理,数据上报也是在此处理的。
PID三个系数,一般简单系统使用PI即可,P即比例系数,控制每次变化量的大小,I是积分系数,修正稳态误差。此处P不能太大,太大容易引起振荡,在此就是电流忽大忽小,我们能容忍电流缓升,但是不敢过流(过流会引起电池过热,甚至爆炸),基于这些分析,暂时设置参数为: P= 1;I=0.1; (此处后期还要慢慢优化,找最合适的系数)
代码为:
.h文件
enum PID_MODE
{
PID_POSITION = 0,
PID_DELTA
};
typedef struct
{
uint8_t mode;
float Kp;
float Ki;
float Kd;
float max_out;
float max_iout;
float set;
float fdb;
float out;
float Pout;
float Iout;
float Dout;
float Dbuf[3];
float error[3];
} pid_type_def;
extern float g_setTemp;
extern pid_type_def stPID;
extern float kPID[3] ;
extern int g_pwm_val;
void PID_init(pid_type_def *pid, uint8_t mode, const float PID[3], float max_out, float max_iout);
float PID_calc(pid_type_def *pid, float ref, float set);
void PID_clear(pid_type_def *pid);
float LimitMax(float input, float max);
.c文件
void Timer_config(void)
{
timer_parameter_struct timer_initpara;
rcu_periph_clock_enable(RCU_TIMER1);
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4);
timer_deinit(TIMER1);
timer_initpara.prescaler = 999;
timer_initpara.alignedmode = TIMER_COUNTER_EDGE;
timer_initpara.counterdirection = TIMER_COUNTER_UP;
timer_initpara.period = 167999;
timer_initpara.clockdivision = TIMER_CKDIV_DIV1;
timer_initpara.repetitioncounter = 0;
timer_init(TIMER1, &timer_initpara);
timer_auto_reload_shadow_enable(TIMER1);
timer_interrupt_flag_clear(TIMER1, TIMER_INT_CH3);
timer_interrupt_enable(TIMER1,TIMER_INT_CH3);
timer_enable(TIMER1);
nvic_irq_enable(TIMER1_IRQn, 1, 1);
}
void TIMER1_IRQHandler(void)
{
float current1= 0;
float pidRes1 = 0;
static int Res_val=0;
if(SET == timer_interrupt_flag_get( TIMER1,TIMER_INT_CH3)){
timer_interrupt_flag_clear( TIMER1,TIMER_INT_CH3);
Get_Shunt_Current( ¤t1 );
pidRes1 = PID_calc( &stPID, temperature, g_setTemp );
Res_val = pidRes1 ;
if( Res_val> MAX_AD8403) Res_val= MAX_AD8403;
if( Res_val< 0 ) Res_val= 0;
Write_AD8403( Ch1, Res_val );
Usb_printf("curr:%0.1f,Res:%d\n", temperature, Res_val );
}
}
void PID_init(pid_type_def *pid, uint8_t mode, const float PID[3], float max_out, float max_iout)
{
if (pid == NULL || PID == NULL){
return;
}
pid->mode = mode;
pid->Kp = PID[0];
pid->Ki = PID[1];
pid->Kd = PID[2];
pid->max_out = max_out;
pid->max_iout = max_iout;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->error[0] = pid->error[1] = pid->error[2] = pid->Pout = pid->Iout = pid->Dout = pid->out = 0.0f;
}
float LimitMax( float input, float max)
{
if (input > max) {
return max;
} else if (input < -max){
return -max;
}else{
return input;
}
}
float PID_calc(pid_type_def *pid, float ref, float set)
{
if (pid == NULL) {
return 0.0f;
}
pid->error[2] = pid->error[1];
pid->error[1] = pid->error[0];
pid->set = set;
pid->fdb = ref;
pid->error[0] = set - ref;
if (pid->mode == PID_POSITION)
{
pid->Pout = pid->Kp * pid->error[0];
pid->Iout += pid->Ki * pid->error[0];
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
pid->Dbuf[0] = (pid->error[0] - pid->error[1]);
pid->Dout = pid->Kd * pid->Dbuf[0];
if(pid->Iout > 0)
pid->Iout = LimitMax(pid->Iout, pid->max_iout);
if(pid->Iout > 0)
pid->Iout = LimitMax(pid->Iout, pid->max_iout*-1);
pid->out = pid->Pout + pid->Iout + pid->Dout;
if(pid->out > 0)
pid->out = LimitMax(pid->out, pid->max_out);
if(pid->out < 0)
pid->out = LimitMax(pid->out, pid->max_out*-1);
}
else if (pid->mode == PID_DELTA) {
pid->Pout = pid->Kp * (pid->error[0] - pid->error[1]);
pid->Iout = pid->Ki * pid->error[0];
pid->Dbuf[2] = pid->Dbuf[1];
pid->Dbuf[1] = pid->Dbuf[0];
pid->Dbuf[0] = (pid->error[0] - 2.0f * pid->error[1] + pid->error[2]);
pid->Dout = pid->Kd * pid->Dbuf[0];
pid->out += pid->Pout + pid->Iout + pid->Dout;
if(pid->out > 0)
pid->out = LimitMax(pid->out, pid->max_out);
if(pid->out < 0)
pid->out = LimitMax(pid->out, pid->max_out*-1);
}
return pid->out;
}
void PID_clear(pid_type_def *pid)
{
if (pid == NULL){
return;
}
pid->error[0] = pid->error[1] = pid->error[2] = 0.0f;
pid->Dbuf[0] = pid->Dbuf[1] = pid->Dbuf[2] = 0.0f;
pid->out = pid->Pout = pid->Iout = pid->Dout = 0.0f;
pid->fdb = pid->set = 0.0f;
}
main函数中完成PID初始化
pid_type_def stPID;
float kPID[3] = {
1,
0.1,
0.0
};
PID_init( &stPID, PID_DELTA, kPID, MAX_AD8403, MAX_AD8403);
4、数据上报
在定时器中上报电压、电流,1S上报一次。 放上 usb_printf 函数的代码。
void usb_printf(char* fmt,...)
{
uint16_t i,j;
va_list ap;
va_start(ap,fmt);
vsprintf((char*)USART_PRINTF_Buffer, fmt, ap);
va_end(ap);
i=strlen((const char*)USART_PRINTF_Buffer);
usbd_ep_send( &cdc_acm, CDC_DATA_IN_EP, USART_PRINTF_Buffer, i );
}
实物展示
最终实物如下,请不要嫌弃拙劣的焊接技巧,毕竟咱就是个程序猿^_^
原作者:兆易创新GD32 MCU จุ๊บ冰语