
CW32电压电流表
简介
这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表
简介:这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表开源协议
:Public Domain
描述
简介
这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表,可以测量电压,电流,功率等参数。
本文将分别对硬件和软件两部分的设计思路进行简单的介绍。
软件代码开源链接
硬件
1.电压采样电路
既然是简易的电压电流表,那必须有电压与电流的采样电路,如图所示:
可以看到有两个挡位的电压量程。这里以0~30V挡位为例。
其中V+
为电压输入端,经过电阻R9
和R10
分压后,通过C23
滤波和D2
钳位,输出到Cw32的ADC通道11。设CW32的ADC参考电压为1.5V,那么我们可以计算出,
1.5/10K = Vmax / (10K + 220K)
,即Vmax= 1.5 * 23 = 34.5V
的极限电压。
由于电阻是有精度范围的,220K的电阻也不一定是220K,有可能是219K,也有可能是221K等,导致采集的电压数据不一定是精确的,所以这里需要对电压测量进行标定。所以设计有模拟电压测量的电路:
通过将输入的电压使用可调电阻进行分压,可以调节出我们需要的电压值,用于后续在软件上进行标定。
2.电流采样电路
如图所示,根据初中所学的物理知识,U=IR
,已知U
和R
,可以简单的得出I
。电流采样的原理就是通过测量采样电阻两端的电压值,从而得出流经采样电阻的电流值。
在图中,电流由I+
流经采样电阻R17
,然后流到I-
,而ADC的地与I-
连接,所以只需要采集I+
的电压即可采集到电流数据。
当然,在调试和标定过程中,很难得到想要的电流流过R17,但是得到需要的电压却比较简单。所以,也设计了模拟电流采集的电路:
我们先不焊接R17,通过调节R15的值来得到相应的电压值,从而模拟出相应的电流。
3.电源电路
由于开发板上的LDO耐压不算很高,为了能够正常工作,我们需要一款耐压较高的LDO输出一个5V电压提供给开发板和一些外围电路。
在LDO前端,有D1
组成的防反接电路,有C18 C17 C20 C21 U3
组成的低通滤波电路,该电路可以滤除一些高频的干扰,增加电路的稳定性。U3实际上是电阻。
4.控制电路
控制电路主要有3个按键以及一块0.96寸的OLED显示屏组成的人机交互电路。
5.其他
其余的一些电路有:
电压基准电路,可以提供额外的电压基准输入到ADC上。
软件
软件上主要展示比较重要的功能代码,具体代码将会在gitee和github上开源.
目录结构:
├─.cmsis
│ └─include
├─.eide
├─.pack
│ └─WHXY
│ └─CW32F030_DFP.1.0.5
│ ├─Device
│ │ ├─Include
│ │ └─Source
│ │ └─ARM
│ ├─Flash
│ └─SVD
├─.vscode
├─APP
│ └─ui
├─Board
├─BSP
│ ├─key
│ ├─log
│ ├─oled
│ ├─u8g2
│ └─uart
├─build
│ └─Project
│ └─.obj
│ ├─APP
│ │ └─ui
│ ├─Board
│ ├─BSP
│ │ ├─key
│ │ ├─log
│ │ ├─oled
│ │ ├─u8g2
│ │ └─uart
│ ├─Libraries
│ │ └─src
│ └─Module
│ ├─adc
│ ├─flash
│ ├─oled
│ ├─timer
│ └─u8g2
├─Libraries
│ ├─inc
│ └─src
├─Module
│ ├─adc
│ ├─flash
│ └─timer
└─Project
└─MDK
└─RTE
├─Device
│ └─CW32F030C8
├─_Project
└─_Target_1
1.显示部分
在上面可以,显示电压电流数据是由一块0.96寸的OLED屏完成的,所以需要编写OLED屏幕的驱动,这里参考中景园的代码,需要初始化连接OLED的GPIO引脚:
由于使用到了u8g2图形库,所以还需要移植图形库的代码:
移植完以后,需要编写显示电压电流数据,电压标定,电流标定界面的代码:
void UI_main(u8g2_t *u8g2) // 主界面,显示电压电流信息
{
u8g2_SetFont(u8g2, u8g2_font_fub30_tf);
Vol_ADC = mean_value_filter(VLotage_buff, ADC_SAMPLE_SIZE);
Cur_ADC = mean_value_filter(Current_buff, ADC_SAMPLE_SIZE);
if (Vol_ADC > X05)
{
Vol_Real = (float32_t)((Vol_ADC - X05) * K + Y05);
}
else
{
Vol_Real = (float32_t)(Vol_ADC * K);
}
if (Cur_ADC > IX05)
{
Cur_Real = (float32_t)(((Cur_ADC - IX05) * KI + IY05) / 100);
}
else
{
Cur_Real = (float32_t)(Cur_ADC * KI / 100);
}
u8g2_FirstPage(u8g2);
do
{
sprintf(buff, "%.2fv", Vol_Real);
u8g2_DrawStr(u8g2, 0, 31, buff);
sprintf(buff, "%.3fa", Cur_Real);
u8g2_DrawStr(u8g2, 0, 63, buff);
} while (u8g2_NextPage(u8g2));
}
void UI_SetVOl(u8g2_t *u8g2) //电压标定界面
{
u8g2_SetFont(u8g2, u8g2_font_t0_16_tf);
Vol_ADC = mean_value_filter(VLotage_buff, ADC_SAMPLE_SIZE);
if (Vol_ADC > X05)
{
Vol_Real = (float32_t)((Vol_ADC - X05) * K + Y05);
}
else
{
Vol_Real = (float32_t)(Vol_ADC * K);
}
u8g2_FirstPage(u8g2);
do
{
sprintf(buff, "Vol Setup");
u8g2_DrawStr(u8g2, 0, 15, buff);
sprintf(buff, "Vol->ADC:%d", Vol_ADC);
u8g2_DrawStr(u8g2, 0, 31, buff);
switch (Vol_List)
{
case CA_Y05:
sprintf(buff, "Vol->Set:%.2fv", (float32_t)Y05);
break;
case CA_Y15:
sprintf(buff, "Vol->Set:%.2fv", (float32_t)Y15);
break;
default:
break;
}
u8g2_DrawStr(u8g2, 0, 47, buff);
sprintf(buff, "Vol->Now:%.2fv", Vol_Real);
u8g2_DrawStr(u8g2, 0, 63, buff);
} while (u8g2_NextPage(u8g2));
}
void UI_SetCurr(u8g2_t *u8g2) //电流标定界面
{
u8g2_SetFont(u8g2, u8g2_font_t0_16_tf);
Cur_ADC = mean_value_filter(Current_buff, ADC_SAMPLE_SIZE);
if (Cur_ADC > IX05)
{
Cur_Real = (float32_t)(((Cur_ADC - IX05) * KI + IY05) / 100);
}
else
{
Cur_Real = (float32_t)(Cur_ADC * KI / 100);
}
u8g2_FirstPage(u8g2);
do
{
sprintf(buff, "Cur Setup");
u8g2_DrawStr(u8g2, 0, 15, buff);
sprintf(buff, "Cur->ADC:%d", Cur_ADC);
u8g2_DrawStr(u8g2, 0, 31, buff);
switch (Cur_List)
{
case CA_Y05:
sprintf(buff, "Cur->Set:%.3fa", (float32_t)IY05 / 100);
break;
case CA_Y15:
sprintf(buff, "Cur->Set:%.3fa", (float32_t)IY15 / 100);
break;
default:
break;
}
u8g2_DrawStr(u8g2, 0, 47, buff);
sprintf(buff, "Cur->Now:%.3fa", Cur_Real);
u8g2_DrawStr(u8g2, 0, 63, buff);
} while (u8g2_NextPage(u8g2));
}
在主函数中,根据界面变量显示不同的界面:
2.按键控制部分
由于按键在按下的过程中,是会有抖动的存在,这样会导致mcu读取引脚的电平是出现错误,所以需要滤波。在硬件设计时,为每个按键均添加了电容器作为硬件滤波器。在软件上,这里采用定时器和有限状态机算法,实现对按键的滤波。
首先是配置定时器,这里将基本定时器1配置为1ms中断:
void BTIME1_InitFor1ms()
{
__RCC_BTIM_CLK_ENABLE();
__disable_irq();
NVIC_EnableIRQ(BTIM1_IRQn);
__enable_irq();
BTIM_TimeBaseInitTypeDef BTIM_TimeBaseInitStruct = {
.BTIM_Mode = BTIM_Mode_TIMER,
.BTIM_OPMode = BTIM_OPMode_Repetitive,
.BTIM_Period = 8192,
.BTIM_Prescaler = BTIM_PRS_DIV8};
BTIM_TimeBaseInit(CW_BTIM1, &BTIM_TimeBaseInitStruct);
// 使能BTIM1的溢出中断
BTIM_ITConfig(CW_BTIM1, BTIM_IT_OV, ENABLE);
// 启动定时器BTIM1
BTIM_Cmd(CW_BTIM1, ENABLE);
}
然后让按键扫描的标志每10ms置1 一次。
uint16_t t;
extern uint8_t adc_en;
void BTIM1_IRQHandler(void)
{
// 判断是否为溢出中断
if (BTIM_GetITStatus(CW_BTIM1, BTIM_IT_OV))
{
// 清除溢出中断标志
BTIM_ClearITPendingBit(CW_BTIM1, BTIM_IT_OV);
FSM_KeyScanHeadler(1);//状态机的心跳
if(++t == 500)
{
t = 0;
adc_en = 1;
}
}
}
然后初始化按键的GPIO
void KEY_Init()
{
RCC_AHBPeriphClk_Enable(RCC_AHB_PERIPH_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure = {
.Pins = KEY1_PIN | KEY2_PIN | KEY3_PIN,
.Mode = GPIO_MODE_INPUT,
.Speed = GPIO_SPEED_HIGH};
GPIO_Init(KEY_PORT, &GPIO_InitStructure);
KEY_defconfig(KEY);
KEY[KEY_LEFT].ReadKeyValue = KEY_LeftRead;
KEY[KEY_RIGHT].ReadKeyValue = KEY_RightRead;
KEY[KEY_OK].ReadKeyValue = KEY_OkRead;
}
首先,我们定义一些按键相关的结构体等:
/// @brief 按键相关
typedef enum
{
KEY_STATE_RELEASE = 0,
KEY_STATE_PRESS,
} KEY_STATE;
typedef enum
{
KEY_LEVEL_LOW = 0,
KEY_LEVEL_HIGH
} KEY_LEVEL;
typedef enum
{
KEY_EVEN_NULL = 0,
KEY_EVEN_PRESS,
KEY_EVEN_RELEASE,
KEY_EVEN_CLICK,
KEY_EVEN_REPEAT,
KEY_EVEN_LONGPRESS_STAR,
KEY_EVEN_LONGPRESS_HOLD,
} KEY_EVEN;
typedef enum KEY_LIST_t
{
KEY_LEFT = 0,
KEY_OK,
KEY_RIGHT,
KEY_COUNT // 按键数量,需固定保留,第一个按键枚举需定为0
} KEY_LIST;
/// @brief FSM相关
typedef enum
{
FSM_KEY_Up = 0, // 按键释放
FSM_KEY_DownShake, // 按键按压抖动
FSM_KEY_Down, // 按键按压状态
FSM_KEY_UpShake, // 按键释放抖动
} FSM_State_t;
typedef enum
{
FSM_EVEN_INIT = 0,
FSM_EVEN_PRESS,
FSM_EVEN_CLICK,
FSM_EVEN_REPEATDOWN,
FSM_EVEN_LONGPRESS
} FSM_EVEN_State_t;
typedef struct
{
FSM_State_t state; // 按键状态
FSM_EVEN_State_t even_state;
uint8_t volatile cnt; // 定时器
uint8_t volatile click_times; // 连按次数
} FSM_value_t;
typedef struct
{
KEY_LEVEL key_pressLevel;
FSM_value_t FSM_value;
KEY_STATE key_state;
void (*EvenCallBack)(KEY_EVEN, void *);
uint32_t (*ReadKeyValue)(void);
} KEY_t;
按键状态机
由于按键在按下和抬起的状态切换过程中,会出现抖动,所以我们将按键的状态分为4个:按键释放状态 按键按下抖动状态 按键按下状态 按键释放状态
1、默认情况下,按键处于释放状态。我们每10ms对按键进行一次检测,当读取到的电平为按键按下的电平,则进入到按键按下抖动状态,否则保持按键释放的状态。
2、在按下抖动状态时,我们再次检测按键的电平(此时已经又过去了10ms),如果按键的电平为按下的电平,那么就可以确认按键确实是按下了,进入到按键按下状态。
3、按键按下状态。(此时又经过了10ms)再次判断按键电平,如果保持为按下的电平,则保持为按键按下状态,否则,进入到按键释放抖动状态
4、按键释放抖动状态。同理,当按键电平为释放电平时,进入按键释放状态,否则回退到按键按下状态。
至此,按键状态扫描的有限状态机完成
代码如下:
if (FSM_Scan_Count >= TICKS_INTERVAL) // 默认每次进入为10ms
{
FSM_Scan_Count = 0;
for (uint8_t i = 0; i < KEY_COUNT; i++)
{
/**
* @brief 此部分为按键状态的扫描
*
*/
if (KEY[i].ReadKeyValue == NULL)
continue;
switch (KEY[i].FSM_value.state)
{
case FSM_KEY_Up: // 如果按键按下,则进入按键按下抖动状态,否则为按键释放状态
if (KEY[i].ReadKeyValue() == KEY[i].key_pressLevel)
KEY[i].FSM_value.state = FSM_KEY_DownShake;
else
{
KEY[i].key_state = KEY_STATE_RELEASE;
}
break;
case FSM_KEY_DownShake: // 按键按下抖动状态,此时为经过一次时间延迟,如果按键为
if (KEY[i].ReadKeyValue() == KEY[i].key_pressLevel)
KEY[i].FSM_value.state = FSM_KEY_Down;
else
KEY[i].FSM_value.state = FSM_KEY_Up;
break;
case FSM_KEY_Down:
if (KEY[i].ReadKeyValue() == KEY[i].key_pressLevel)
{
KEY[i].key_state = KEY_STATE_PRESS;
}
else
KEY[i].FSM_value.state = FSM_KEY_UpShake;
break;
case FSM_KEY_UpShake:
if (KEY[i].ReadKeyValue() == KEY[i].key_pressLevel)
KEY[i].FSM_value.state = FSM_KEY_Down;
else
KEY[i].FSM_value.state = FSM_KEY_Up;
break;
default:
break;
}
按键的事件处理部分参考了github上的multibotton
/**
* @brief 此部分为按键事件的检测,如单击,双击等,并调用相应的回调函数
*
*/
switch (KEY[i].FSM_value.even_state)
{
case FSM_EVEN_INIT: // 初始状态
if (KEY[i].key_state == KEY_STATE_PRESS)
{
KEY[i].FSM_value.even_state = FSM_EVEN_PRESS;
KEY[i].EvenCallBack(KEY_EVEN_PRESS, NULL); // 出发按键按下事件
}
else
KEY[i].FSM_value.even_state = FSM_EVEN_INIT;
break;
case FSM_EVEN_PRESS:
if (KEY[i].key_state == KEY_STATE_PRESS)
{
if (KEY[i].FSM_value.cnt++ > (200 / TICKS_INTERVAL)) // 如果按下,进入长按状态
{
KEY[i].FSM_value.cnt = 0;
KEY[i].FSM_value.even_state = FSM_EVEN_LONGPRESS;
KEY[i].EvenCallBack(KEY_EVEN_LONGPRESS_STAR, NULL); // 出发长按开始事件
}
}
else
{
KEY[i].FSM_value.click_times++;
KEY[i].FSM_value.even_state = FSM_EVEN_CLICK; // 否则进入点击状态
KEY[i].EvenCallBack(KEY_EVEN_RELEASE, NULL);
}
break;
case FSM_EVEN_CLICK:
if (KEY[i].key_state == KEY_STATE_PRESS)
{
KEY[i].FSM_value.cnt = 0;
KEY[i].FSM_value.even_state = FSM_EVEN_REPEATDOWN; // 如果按下,进入连击状态
}
else // 否则根据条件,触发连击事件或者点击事件
{
if (KEY[i].FSM_value.cnt++ >= SHORT_TICKS)
{
KEY[i].FSM_value.cnt = 0;
KEY[i].FSM_value.even_state = FSM_EVEN_INIT;
if (KEY[i].FSM_value.click_times > 1)
KEY[i].EvenCallBack(KEY_EVEN_REPEAT, (void *)&KEY[i].FSM_value.click_times);
else
KEY[i].EvenCallBack(KEY_EVEN_CLICK, NULL);
KEY[i].FSM_value.click_times = 0;
}
}
break;
case FSM_EVEN_REPEATDOWN:
if (KEY[i].key_state == KEY_STATE_PRESS)
{
if (KEY[i].FSM_value.cnt++ >= LONG_TICKS / 2)
{
KEY[i].FSM_value.click_times++;
KEY[i].EvenCallBack(KEY_EVEN_REPEAT, (void *)&KEY[i].FSM_value.click_times);
KEY[i].FSM_value.click_times = 0;
KEY[i].FSM_value.even_state = FSM_EVEN_LONGPRESS;
KEY[i].FSM_value.cnt = 0;
}
}
else
{
KEY[i].FSM_value.even_state = FSM_EVEN_CLICK;
KEY[i].FSM_value.click_times++;
}
break;
case FSM_EVEN_LONGPRESS:
if (KEY[i].key_state == KEY_STATE_PRESS)
{
if (KEY[i].FSM_value.cnt++ >= LONG_TICKS)
{
KEY[i].FSM_value.cnt = 0;
KEY[i].EvenCallBack(KEY_EVEN_LONGPRESS_HOLD, NULL);
}
}
else
{
KEY[i].FSM_value.even_state = FSM_EVEN_INIT;
KEY[i].FSM_value.cnt = 0;
KEY[i].EvenCallBack(KEY_EVEN_RELEASE, NULL);
}
break;
default:
KEY[i].FSM_value.even_state = FSM_EVEN_INIT;
break;
}
这部分采用了事件与回调的方式进行按键事件的处理:
比如,按键在一定时间内,按下后松开,可以触发按键点击事件,并调用按键回调函数,处理点击事件。
如电压电流表中的一个按键的回调函数:其中的args为按键回调时的参数,这里时多次点击的次数。
void KeyLeft_CB(KEY_EVEN EVEN, void *args)
{
switch (EVEN)
{
case KEY_EVEN_PRESS:
break;
case KEY_EVEN_LONGPRESS_STAR:
break;
case KEY_EVEN_LONGPRESS_HOLD:
break;
case KEY_EVEN_RELEASE:
break;
case KEY_EVEN_CLICK:
switch (uip->page_now)
{
case UI_MAIN:
break;
case UI_SETVOL:
uip->page_now = UI_MAIN; //切换界面回到主界面
break;
case UI_SETCUR:
uip->page_now = UI_SETVOL;//切换界面到电压设置界面
break;
default:
break;
}
break;
case KEY_EVEN_REPEAT:
break;
default:
break;
}
}
3.电压电流采集
这部分参考了训练营的一些教程:cw32数字电压电流表训练营
ADC配置
由于电压电流的采集用到了ADC,所以需要将ADC进行配置:
首先是ADC的初始化:
void Module_ADC_init()
{
__RCC_GPIOA_CLK_ENABLE();
__RCC_ADC_CLK_ENABLE();
PB10_ANALOG_ENABLE();
PB01_ANALOG_ENABLE();
PB11_ANALOG_ENABLE();
PB00_ANALOG_ENABLE();
ADC_InitTypeDef ADC_InitStructure;
ADC_StructInit(&ADC_InitStructure);
ADC_InitStructure.ADC_ClkDiv = ADC_Clk_Div4;
ADC_InitStructure.ADC_VrefSel = ADC_Vref_BGR1p5;
ADC_InitStructure.ADC_SampleTime = ADC_SampTime10Clk;
ADC_SerialChTypeDef ADC_SerialChStructure;
// ADC_SerialChStructure.ADC_SqrEns = ADC_SqrEns0;
ADC_SerialChStructure.ADC_Sqr0Chmux = ADC_SqrCh11;
ADC_SerialChStructure.ADC_Sqr1Chmux = ADC_SqrCh12;
ADC_SerialChStructure.ADC_SqrEns = ADC_SqrEns01;
ADC_SerialChStructure.ADC_InitStruct = ADC_InitStructure;
ADC_SerialChContinuousModeCfg(&ADC_SerialChStructure);
ADC_ClearITPendingAll();
ADC_Enable();
ADC_SoftwareStartConvCmd(ENABLE);
}
这里配置了两个ADC采样序列:ADC_Sqr0Chmux
和ADC_Sqr1Chmux
分别配置了Sqr0
和Sqr1
。其中Sqr0
为电压采集序列,Sqr1
为电流采集序列。
电压电流转换算法
在硬件设计中我们知道,电压的采样是经过分压电阻的,电流的采样是经过采样电阻的。所以在显示电压与电流数据时,需要对采集到的ADC数据进行一定的计算。
由于噪声的存在,直接使用ADC采集的数据进行计算会导致显示波动较大,所以先使用滤波算法,将ADC数据进行平滑处理,这里使用均值滤波,每次取10次ADC数据,去掉最大最小值后进行算数平均:
// 连续获取电压值
void GetVoltagContinue(uint16_t *buff)
{
for (uint8_t i = 0; i < ADC_SAMPLE_SIZE; i++)
{
ADC_GetSqr0Result(buff + i);
}
}
// 连续获取电流
void GetCurrentContinue(uint16_t *buff)
{
for (uint8_t i = 0; i < ADC_SAMPLE_SIZE; i++)
{
ADC_GetSqr1Result(buff + i);
}
}
// 均值滤波
uint32_t mean_value_filter(uint16_t *value, uint16_t size)
{
uint32_t sum;
uint16_t max;
uint16_t min = 0xffff;
for (int i = 0; i < size; i++)
{
sum += value[i];
if (value[i] > max)
max = value[i];
else if (value[i] < min)
min = value[i];
else
;
}
sum -= (max + min);
sum = sum / (size - 2);
return sum;
}
得到经过平滑处理的ADC数据后,需要进行一些计算才能得到电压或者电流数据:
因为CW32F030的ADC是12bit的,所以ADC的数据范围是:0 ~ 4095
,那么,在1.5V参考电压下,ADC的数据对应的电压值为 (ADC / 4096) * 1.5
,此时是ADC引脚上的电压。我们知道,
U * (R1 / R1 + R2) = UR1
所以在已知UR1的情况下U = UR1 * (R1 + R2 / R1)
由此,在理想情况下,ADC采集到数据表示为电压数据的话(以30V挡位为例):
U = 1.5 * (ADC / 4096) * (220K + 10K) / 10K
;同理可知电流数据的表示方式。
但是!由于电阻存在误差,直接使用这种方式表示电压电流数据并不准确,所以这里采用另一种方式,同样参考了训练营的教程。
假设一个采样系统,AD部分可以得到数字量,对应的物理量为电压(或电流);
- 1 若在“零点”标定一个AD值点Xmin,在“最大处”标定一个AD值点Xmax,根据“两点成一条直线”的原理,可以得到一条由零点和最大点连起来的一条直线,这条直线的斜率k很容易求得,然后套如直线方程求解每一个点X(AD采样值),可以得到该AD值对应的物理量(电压值):
上图中的斜率k:
k =(Ymax-Ymin)/(Xmax-Xmin)
所以,上图中任一点的AD值对应的物理量:
y = k×(Xad- Xmin)+0
- 上面的算法只是在“零点”和“最大点”之间做了标定,如果使用中间的AD采样值会带来很大的对应物理量的误差,解决的办法是多插入一些标定点。
如下图,分别插入了标定点(x1,y1)、(x2,y2)、(x3,y3)、(x4,y4)
四个点:
这样将获得不再是一条直线,而是一条“折现”(相当于分段处理),若欲求解落在x1和x2之间一点Xad值对应的电压值:
y = k×(Xad– X1)+ y1
由上看出,中间插入的“标定点”越多,得到物理值“精度”越高。
在电压电流表测量可以使用“电压电流标定板”“万用表”等配合适合,对采集的电压电流进行标定处理。标定点越多,测量越精确。
这里取电压 5V 15V
电流 0.5A 1.5A
进行标定
// 5V与15V 校准
unsigned int X05 = 0;
unsigned int X15 = 0;
unsigned int Y15 = 12; // 由于作者没有15V电源,故以12V代替
unsigned int Y05 = 5;
float32_t K; // 斜率
// 0.5A与1.5A 校准
unsigned int IX05 = 0;
unsigned int IX15 = 0;
unsigned int IY15 = 150; // 1.5A
unsigned int IY05 = 50; // 0.5A
float32_t KI; // 斜率
void ComputeK(void)
{
K = (Y15 - Y05);
K = K / (X15 - X05); // 电压斜率
KI = (IY15 - IY05);
KI = KI / (IX15 - IX05); // 电流斜率
}
void save_calibration(void)
{
uint16_t da[5];
da[0] = 0xaa;
da[1] = X05;
da[2] = X15;
da[3] = IX05;
da[4] = IX15;
flash_erase();
flash_write(0, da, 5);
}
/**
* @brief
*
*/
void read_vol_cur_calibration(void)
{
uint16_t da[5];
flash_read(0, da, 5);
if (da[0] != 0xaa) // 还没校准过时,计算理论值,并存储
{
X15 = 15.0 / 23 / 1.5 * 4096;
X05 = 5.0 / 23 / 1.5 * 4096;
IX05 = 0.5 * 0.1 / 1.5 * 4096;
IX15 = 1.5 * 0.1 / 1.5 * 4096;
save_calibration();
}
else
{
X05 = da[1];
X15 = da[2];
IX05 = da[3];
IX15 = da[4];
}
}
所以,最终显示电压电流的转换公式为:
if (Vol_ADC > X05)
{
Vol_Real = (float32_t)((Vol_ADC - X05) * K + Y05);
}
else
{
Vol_Real = (float32_t)(Vol_ADC * K);
}
if (Cur_ADC > IX05)
{
Cur_Real = (float32_t)(((Cur_ADC - IX05) * KI + IY05) / 100);
}
else
{
Cur_Real = (float32_t)(Cur_ADC * KI / 100);
}
这里电流需要除以100,是因为在计算K时,电流的Y坐标是放大了100倍进行计算并存储的。
演示
板子上有3个按键,分别以<- + ->
表示,其中<-
键为LEFT,+
键为OK ->
键为RIGHT。
在开机时,可以用->
和<-
键进行页面切换,在电压和电流界面中,按下+
键可以设置电压电流的标定。
在电压标定界面,默认为电压5V标定,此时将模拟电压调节到5V,按下+
键,保存并切换到电压15V标定。设置好后,按下+
键保存并切换会5V标定,此时电压的标定数据已经保存在flash内。电流标定同理。
设计图

BOM


评论