1 项目介绍

1.1 背景

高中物理课本有这样一个实验 测绘小灯泡的伏安特性曲线

书上的方法是:利用电学实验器材中的 电压表电流表 测量若干组数据,将其标记在坐标图中,并用平滑的曲线连接,从而得到 $I-U$ 图像。

这种方法有以下几个弊端:

  • 实验所用到的电压、电流表并非理想测量器件,内阻对测量造成的影响较大
  • 由于 测量的数据较少,无法准确描地绘伏安特性曲线。
  • 实验中的读数和计算均为手工操作,容易造成 人为误差

物理张老师告诉我们,有一款 电子式的电压、电流传感器,可以实现较为精确的测量。
随后我便在网上查询,发现这种传感器的售价大约在 $¥500$ 左右,性价比较低。
介于曾经自学过 单片机 的相关知识,在 编程 方面也有一定的基础,笔者打算自己设计一款 电子式电压、电流传感器

1.2 所用器材

1.2.1 硬件
  • Arduino 单片机
  • ACS712 电流传感器
  • 物理电学实验器材
1.2.2 软件
  • Arduino IDE
  • 串口调试助手
  • Dev C++
  • Excel、WPS

1.3 成本统计

1.3.1 财务成本
  • 单片机 约¥25
  • 电流传感器 约¥5
  • 其他 约¥5

共计约 $¥35$

网上的传感器成品售价约 $¥500$。
1.3.2 时间成本

笔者是一位高中生,热爱编程和单片机设计,实验阶段花费 约8小时


2 成果

2.1 数据部分

2.1.1 小灯泡(2.5V 0.3A)

light_graph.png

2.1.2 定值电阻(5Ω)

r_5_graph.png

2.2 电路部分

circuit.jpg


3 研究过程

3.1 数据采集

3.1.1 传感器设计

Arduino 单片机有模拟输入针脚,可读取输入电压值。
电流测量需要用到电流模块,如下图:
ACS712.jpg
左侧两根线为 被测电流输入和输出
右侧三根线(从上往下)分别为 GND(参考地线)OUT(信号输出)VCC(模块供电)

3.1.2 程序设计

Arduino 编程采用 Arduino IDE

电压测量实现如下:

void readU()
{
    for (int i = 0; i < SZ; i++)
        val_u[i] = analogRead(PINU);
}

// 调用入口(返回值:mV)
double getU()
{
    readU();
    long long sum = 0;
    double avg = 0, rst = 0;
    for(int i = 0; i < SZ; i++)
        sum += val_u[i];
    avg = (double)sum / SZ;
    rst = avg / 1024.0 * vref / 1000;
    return rst;
}

电流测量:

void readI()
{
    for (int i = 0; i < SZ; i++)
        val_i[i] = analogRead(PINI);
}

// 调用入口(返回值:mA)
double getI()
{
    readI();
    long long sum = 0;
    double avg = 0, rst = 0;
    for(int i = 0; i < SZ; i++)
        sum += val_i[i];
    avg = (double)sum / SZ;
    rst = (avg / 1024.0 * vref - vref / 2.0) / mVperAmp;
    rst = rst < 0 ? 0 : rst;
    return rst;
}

以上程序中的常量(变量)定义:

const int PINU = A1; // 电压测量针脚
const int PINI = A0; // 电流测量针脚
const int SZ = 100; // 每次测量采样个数
const int mVperAmp = 185; // 电流模块转换系数

int vref = readVref(); // 最大电压
int val_i[SZ], val_u[SZ]; // 保存采样点

/*read reference voltage*/
long readVref()
{
    long result;
#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328__) || defined (__AVR_ATmega328P__)
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB1286__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    ADCSRB &= ~_BV(MUX5);   // Without this the function always returns -1 on the ATmega2560 http://openenergymonitor.org/emon/node/2253#comment-11432
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#endif
#if defined(__AVR__)
    delay(2);                                        // Wait for Vref to settle
    ADCSRA |= _BV(ADSC);                             // Convert
    while (bit_is_set(ADCSRA, ADSC));
    result = ADCL;
    result |= ADCH << 8;
    result = 1126400L / result;  //1100mV*1024 ADC steps http://openenergymonitor.org/emon/node/1186
    return result;
#elif defined(__arm__)
    return (3300);                                  //Arduino Due
#else
    return (3300);                                  //Guess that other un-supported architectures will be running a 3.3V!
#endif
}

最终程序:

const int PINU = A1;
const int PINI = A0;
const int SZ = 100;
const int REPT = 5; // 每次采集重复次数
const int mVperAmp = 185;

int vref = 0;
int val_i[SZ], val_u[SZ];

void setup() {
    Serial.begin(115200);
    vref = readVref(); //read the reference votage(default:VCC)
}

void loop() {
    char c = Serial.read();
    if (c != 0x00 && c != 0x01)
        return;

    double sum = 0, data_i = 0, data_u = 0;
    for (int i = 0; i < REPT; i++)
        sum += getU();
    data_u = sum / REPT;
    output((int)(data_u * 1000));

    sum = 0;
    for (int i = 0; i < REPT; i++)
        sum += getI();
    data_i = sum / REPT;
    Serial.print(" "), output((int)(data_i * 1000));

    if (c == 0x01)
        Serial.print(" "), Serial.print(data_u / data_i);

    Serial.println();
    while (Serial.available())
        Serial.read();
    delay(10);
}

void output(int val)
{
    if (val < 1000)
        Serial.print(0);
        if (val < 100)
            Serial.print(0);
            if (val < 10)
                Serial.print(0);
    Serial.print(val);
}

void readI()
{
    for (int i = 0; i < SZ; i++)
        val_i[i] = analogRead(PINI);
}

double getI()
{
    readI();
    long long sum = 0;
    double avg = 0, rst = 0;
    for(int i = 0; i < SZ; i++)
        sum += val_i[i];
    avg = (double)sum / SZ;
    rst = (avg / 1024.0 * vref - vref / 2.0) / mVperAmp;
    rst = rst < 0 ? 0 : rst;
    return rst;
}

void readU()
{
    for (int i = 0; i < SZ; i++)
        val_u[i] = analogRead(PINU);
}

double getU()
{
    readU();
    long long sum = 0;
    double avg = 0, rst = 0;
    for(int i = 0; i < SZ; i++)
        sum += val_u[i];
    avg = (double)sum / SZ;
    rst = avg / 1024.0 * vref / 1000;
    return rst;
}

/*read reference voltage*/
long readVref()
{
    long result;
#if defined(__AVR_ATmega168__) || defined(__AVR_ATmega328__) || defined (__AVR_ATmega328P__)
    ADMUX = _BV(REFS0) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
#elif defined(__AVR_ATmega32U4__) || defined(__AVR_ATmega1280__) || defined(__AVR_ATmega2560__) || defined(__AVR_AT90USB1286__)
    ADMUX = _BV(REFS0) | _BV(MUX4) | _BV(MUX3) | _BV(MUX2) | _BV(MUX1);
    ADCSRB &= ~_BV(MUX5);   // Without this the function always returns -1 on the ATmega2560 http://openenergymonitor.org/emon/node/2253#comment-11432
#elif defined (__AVR_ATtiny24__) || defined(__AVR_ATtiny44__) || defined(__AVR_ATtiny84__)
    ADMUX = _BV(MUX5) | _BV(MUX0);
#elif defined (__AVR_ATtiny25__) || defined(__AVR_ATtiny45__) || defined(__AVR_ATtiny85__)
    ADMUX = _BV(MUX3) | _BV(MUX2);
#endif
#if defined(__AVR__)
    delay(2);                                        // Wait for Vref to settle
    ADCSRA |= _BV(ADSC);                             // Convert
    while (bit_is_set(ADCSRA, ADSC));
    result = ADCL;
    result |= ADCH << 8;
    result = 1126400L / result;  //1100mV*1024 ADC steps http://openenergymonitor.org/emon/node/1186
    return result;
#elif defined(__arm__)
    return (3300);                                  //Arduino Due
#else
    return (3300);                                  //Guess that other un-supported architectures will be running a 3.3V!
#endif
}

串口通讯说明:

  • 当接收到 $0x00$ 时,返回格式如下:
    电压($mV$) 电流($mA$)
  • 当接收到 $0x01$ 时,返回格式如下:
    电压($mV$) 电流($mA$) 电阻($Ω$)

若不修改程序参数,一组数据的采集时间约为 $60ms$

3.1.3 电路设计

和高中物理书中的实验一样,滑动变阻器采用 分压式接法,可为被测元件提供从 $0$ 开始的连续电压。

由于电流传感器内阻基本为 $0$,电压传感器内阻非常大,所以传感器内阻对实验造成的误差基本上可以忽略。在这里,为保证实验的严谨性,笔者仍然选择了 电流表外接法

实物图如下:

circuit.jpg

3.1.4 串口通讯 & 数据测量

在 $3.1.2$ 的最后,笔者提到了串口通讯的格式。

接下来,我们开始测量数据。

serial.jpg

上图为串口调试助手。

我们将收到的数据保存到文件,以便于后续处理。

3.2 数据处理

3.2.1 初步尝试

拿到实验数据后,笔者的第一个想法是用Excel绘图。

first_graph.png

图像出现了环状结构。

经过分析,笔者发现:由于测量误差存在,电压和电流并不是 一一对映 的关系。

3.2.2 数据优化处理

我们所希望得到的是 I-U图像,即每个电压数据,只能对映一个电流数据。

所以笔者写了一个 c++ 小程序,如果一个电压对映多个电流,则取电流的平均值(这里是算术平均数,关于平均数的选择,我们会在 $4.1.1$ 中讨论)。

程序如下:

#include <cstdio>
#include <map>

using std::map;

map<int, int> data;
map<int, int> num;
map<int, int>::iterator iter;

int main()
{
    freopen("data.txt", "r", stdin);
    freopen("deal.txt", "w", stdout);
    int data_u, data_i;
    while (~scanf("%d%d", &data_u, &data_i))
        data[data_u] += data_i, num[data_u]++;
    for (iter = data.begin(); iter != data.end(); iter++)
    {
        data[iter->first] = (double)data[iter->first] / num[iter->first] + 0.5;
        printf("%d %d\n", iter->first, iter->second);
    }
    return 0;
}

原始数据文件:data.txt
处理后文件:deal.txt

3.2.3 最终成果

我们将处理后的数据导入Excel,进行绘图,并为图像生成一条 趋势线

小灯泡(2.5V 0.3A)

light_graph.png

定值电阻(5Ω)

r_5_graph.png


4 评价与反思

4.1 几点说明

4.1.1 平均数的选取

3.1.2 程序设计3.2.2 数据优化处理 中,都出现了求平均的步骤。
根据公式 $R = U / I$, 为了使电阻等效,我们应该计算电流的 调和平均数,但笔者最终还是选择了 算术平均数,主要有以下几点原因:

  • 误差来自 传感器的精度问题,以及 电磁干扰静电噪音,调和平均数并不能真实地反应瞬时电流值,反而会使得测量结果偏小。
  • 单片机的 计算能力有限,调和平均数的计算时间较长,不利于数据的测量。
4.1.2 数据的回归分析

3.2.3 最终成果 中,笔者使用Excel完成对数据的 回归分析。其中 小灯泡 的趋势线是一个多项式回归方程(最高项次数为 $6$),而 定值电阻 的趋势线是一个线性回归方程。

4.2 不足之处 & 改进方案

4.2.1 测得的图像不过原点

相比较电压传感器,电流传感器的精度较低,检测不到很小的电流

这主要是电流传感器的原理所致,笔者所用到的这款传感器是一个 霍尔元件,价格便宜,但精度不高(精度大约 $10mA$),而且容易受到电磁干扰。

笔者留意到网上有一款精度较高的电流传感器(精度大约 $1.25mA$),这款传感器采用 GMR巨磁阻 技术,价格稍高(约 $¥60$)。这是一个不错的改进方案。

4.2.2 数据处理算法的优化

3.2.3 最终成果 中,笔者使用Excel完成对数据的 回归分析。随着数据规模的增大,这样回归算法的效率并不是很高。而且Excel只能完成 $6$ 次多项式回归方程的计算。

笔者曾经接触过 神经元网络 算法,这种算法可以在较短的时间内完成 数据点->曲线 的拟合操作。笔者计划建立一个数学模型,实现对数据更加精确的分析。

4.2.3 测量过程集成化、自动化

在这个项目中,笔者需要手动完成数据从一个软件到另一个软件的拷贝,稍有些繁琐。笔者打算编写一个软件,集成数据采集和分析工作,从而方便大家的使用。

分类: 默认分类 标签: 暂无标签

评论