测绘伏安特性曲线(I-U图像)
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)
2.1.2 定值电阻(5Ω)
2.2 电路部分
3 研究过程
3.1 数据采集
3.1.1 传感器设计
Arduino 单片机有模拟输入针脚,可读取输入电压值。
电流测量需要用到电流模块,如下图:
左侧两根线为 被测电流输入和输出
;
右侧三根线(从上往下)分别为 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$,电压传感器内阻非常大,所以传感器内阻对实验造成的误差基本上可以忽略。在这里,为保证实验的严谨性,笔者仍然选择了 电流表外接法
。
实物图如下:
3.1.4 串口通讯 & 数据测量
在 $3.1.2$ 的最后,笔者提到了串口通讯的格式。
接下来,我们开始测量数据。
上图为串口调试助手。
我们将收到的数据保存到文件,以便于后续处理。
3.2 数据处理
3.2.1 初步尝试
拿到实验数据后,笔者的第一个想法是用Excel绘图。
图像出现了环状结构。
经过分析,笔者发现:由于测量误差存在,电压和电流并不是 一一对映
的关系。
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)
定值电阻(5Ω)
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 测量过程集成化、自动化
在这个项目中,笔者需要手动完成数据从一个软件到另一个软件的拷贝,稍有些繁琐。笔者打算编写一个软件,集成数据采集和分析工作,从而方便大家的使用。
本文系作者 @WonderBoy 原创发布在翘楚小站站点。未经许可,禁止转载。
评论