嘉立创产业服务站群
发作品签到
专业版

CW32电压电流表

工程标签

171
0
0
1

简介

这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表

简介:这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表
电压电流表训练营【立创开发板&CW32】

开源协议

Public Domain

创建时间:2024-07-18 15:38:30更新时间:2024-08-30 02:12:02

描述

简介

这是基于CW32F030C8T6单片机,即立创地文星开发板的简易电流电压表,可以测量电压,电流,功率等参数。

本文将分别对硬件和软件两部分的设计思路进行简单的介绍。
软件代码开源链接

硬件

1.电压采样电路

既然是简易的电压电流表,那必须有电压与电流的采样电路,如图所示:

vol-test1.png
可以看到有两个挡位的电压量程。这里以0~30V挡位为例。
其中V+为电压输入端,经过电阻R9R10分压后,通过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等,导致采集的电压数据不一定是精确的,所以这里需要对电压测量进行标定。所以设计有模拟电压测量的电路:

vol-test2.png
通过将输入的电压使用可调电阻进行分压,可以调节出我们需要的电压值,用于后续在软件上进行标定。

2.电流采样电路

cur-test.png
如图所示,根据初中所学的物理知识,U=IR,已知UR,可以简单的得出I。电流采样的原理就是通过测量采样电阻两端的电压值,从而得出流经采样电阻的电流值。
在图中,电流由I+流经采样电阻R17,然后流到I-,而ADC的地与I-连接,所以只需要采集I+的电压即可采集到电流数据。
当然,在调试和标定过程中,很难得到想要的电流流过R17,但是得到需要的电压却比较简单。所以,也设计了模拟电流采集的电路:

curr-test2.png
我们先不焊接R17,通过调节R15的值来得到相应的电压值,从而模拟出相应的电流。

3.电源电路

power1.png

由于开发板上的LDO耐压不算很高,为了能够正常工作,我们需要一款耐压较高的LDO输出一个5V电压提供给开发板和一些外围电路。
在LDO前端,有D1组成的防反接电路,有C18 C17 C20 C21 U3组成的低通滤波电路,该电路可以滤除一些高频的干扰,增加电路的稳定性。U3实际上是电阻。

4.控制电路

control.png
控制电路主要有3个按键以及一块0.96寸的OLED显示屏组成的人机交互电路。

5.其他

其余的一些电路有:

vol-base.png
电压基准电路,可以提供额外的电压基准输入到ADC上。

软件

软件上主要展示比较重要的功能代码,具体代码将会在giteegithub上开源.
目录结构:

├─.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引脚:

oled_init.png
由于使用到了u8g2图形库,所以还需要移植图形库的代码:

u8g2.png
移植完以后,需要编写显示电压电流数据,电压标定,电流标定界面的代码:

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));
}

在主函数中,根据界面变量显示不同的界面:

ui-mainc.png

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_Sqr0ChmuxADC_Sqr1Chmux分别配置了Sqr0Sqr1。其中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值对应的物理量(电压值):

voltammeter_20240806_164521.png
上图中的斜率k:
k =(Ymax-Ymin)/(Xmax-Xmin)
所以,上图中任一点的AD值对应的物理量:
y = k×(Xad- Xmin)+0

  • 上面的算法只是在“零点”和“最大点”之间做了标定,如果使用中间的AD采样值会带来很大的对应物理量的误差,解决的办法是多插入一些标定点。
    如下图,分别插入了标定点(x1,y1)、(x2,y2)、(x3,y3)、(x4,y4) 四个点:

voltammeter_20240806_164708.png

这样将获得不再是一条直线,而是一条“折现”(相当于分段处理),若欲求解落在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内。电流标定同理。

MVIMG_20240817_152756_compress69.jpg

IMG_20240817_152818_compress96.jpg

MVIMG_20240817_152801_compress99.jpg

设计图

未生成预览图,请在编辑器重新保存一次

BOM

暂无BOM

附件

序号文件名称下载次数
1
1.voltage-ammeter.zip
21
克隆工程
添加到专辑
0
0
分享
侵权投诉

工程成员

评论

全部评论(1)
按时间排序|按热度排序
粉丝0|获赞0
相关工程
暂无相关工程

底部导航