Arduino模拟量的输入和PWM输出

模拟量的输入

在家里的开关,除了控制电灯亮灭的二值状态(0或1)的开关,我们往往还会引入一种可以连续的旋钮以便连续控制某一物理量,譬如睡灯的亮度、风扇的转速。无意中,我们就引入了一种称之为模拟量的东西。自然界中的物理量往往都是模拟量,譬如温度、湿度、气压等,我们用一类叫做传感器的电子装置把这些物理量转化为电学量(如电压、电流)。然后和Arduino的模拟量端口连接起来,让Arduino可以读取。

 

                                  
上图所示,就是电子市场上常见的一种元件,俗称电位器,你家里应该就有不少这个,譬如音响上调节声音大小的旋钮,用来调节吊扇无级变速的开关等等。其实它和你在初中物理中所学的电阻器没什么两样,只是实际应用中没什么可能弄个那么大的变阻器,它往往只是一种教学演示器材或者在电流很大等特殊场合才会使用。如果你拆开电位器,你就会发现,它里面其实就是一个金属接触片在270°的扇形碳膜上接触。如果你还稍加一点点想象,它是不是就是一个简单的角度传感器,将角度的大小通过阻值的大小转换,最终变成电压,或者说模拟信号以便用于输入到Arduino控制板中。只是由于碳膜和金属接触片接触过程中必然会因为接触面粗糙或存在杂质等磕磕碰碰,从而造成噪声并最终转化为干扰信号,显然,其动态性能并不太好。
我们从一个简单的实验开始,打开Arduino的开发环境,调出例子模拟输入(File->Example->Analog->AnalogInput)。程序一开始就有大段用/*标记开始和*/标记结束的灰色段落,是注释。它分别说明了这段程序功能,电路的接线方法以及作者和开源声明。这种注释一般称为块注释(block comments),它所注释的内容往往跨行,但是/* */注释对不能嵌套,否则会出错,你可以试试看。而//称为行注释(line comments),从它开始到本行结束,或者说遇到换行符为止。此外,我建议往程序的setup()和loop()过程中分别添加如下所示的波浪线语句,以方便我们从串口监控程序状况。

int sensorPin = A0; //选择电位器的引脚
int ledPin = 13; // 选择LED(发光二极管)的引脚
int sensorValue = 0; // 存储由传感器传来的值
 
void setup() {
  //定义LED(发光二极管)引脚为输出模式
  pinMode(ledPin, OUTPUT);
  //配置串口波特率,为9600
  Serial.begin(9600);
}
 
void loop() {
  //读取传感器的值,并把值赋给变量sensorValue
  sensorValue = analogRead(sensorPin);
  //打开LED(发光二极管)
  digitalWrite(ledPin, HIGH);
  //持续时间为sensorValue个毫秒
  delay(sensorValue);
  //关闭LED(发光二极管)
  digitalWrite(ledPin, LOW);
  //持续时间为sensorValue个毫秒
  delay(sensorValue);
  //向串口打印sensorValue的值
  Serial.print(sensorValue);
  //向串口打印一个制表符
  Serial.print('\t');
  //转换sensorValue为对应的电压值并打印到串口
  Serial.print(sensorValue*5.0/1023);
  //给电压值补上单位V(伏特)
  Serial.println("V");
}

按图示接好线后,打开串口监视程序(Tools->Serial Monitor),扭动电位器,我们就可以看到串口输出的值从0到1023,对应的电压从0V到5V。由此可知,Arduino对于模拟量的输入,其采样精度是10位的,因为210=1024,分辨率就是5V/1024=0.0049V,也就是说两个相差小于0.0049V的电压差Arduino无法分辨。此外,Arduino每隔100µs(微秒)=0.0001s(秒)对信号进行采样,将模拟量转换为0至1023中的一个值。这个采样的时间间隔,称之为采样周期,它的倒数(f=1/T),称为采样频率。


模拟量的输出
接下来我们要谈到的是模拟量的输出。Arduino并不能直接输出较为精确的模拟量,而是以一种称为PWM(脉宽调制)方波的形式来间接输出模拟量的。脉宽调制的原理请见下图,


同样周期的脉冲方波,其高电平所占据整个周期的比例将会影响到诸如发光二极管、电机等装置的效果。还记得上一节的程序吗?扭动电位器,当闪烁的时间间隔很小,如10ms以下时,你是否发现肉眼已经无法分辨,而觉得发光二极管L是持续点亮的。下面,我们对程序做一些小小的改动,来验证我们的想法。
map()函数是一种映射,它将从一个区间的相对值映射成另一个区间的相对值并返回。譬如我们规定冰点时温度为0摄氏度,沸点为100摄氏度,而华氏温度把冰点温度定为32华氏度,沸点为212华氏度。根据这一关系,我们就可以直接写出从华氏温度到摄氏温度转换的语句,Celsius = map(Fahrenheit, 32, 212, 0, 100),不过仍需注意的是map函数只能适用于不带小数的整数。

int sensorPin = A0; //选择电位器的引脚
int ledPin = 13; // 选择LED(发光二极管)的引脚
int sensorValue = 0; // 存储由传感器传来的值
int pulseWidth;
 
void setup() {
  //定义LED(发光二极管)引脚为输出模式
  pinMode(ledPin, OUTPUT);
  pinMode(6, INPUT);
  //digitalWrite(6, HIGH);
  //配置串口波特率,为9600
  Serial.begin(9600);
}
 
void loop() {
  //读取传感器的值,并把值赋给变量sensorValue
  sensorValue = analogRead(sensorPin);
  pulseWidth = map(sensorValue, 0, 1023, 0, 10000);
  //打开LED(发光二极管)
  digitalWrite(ledPin, HIGH);
  //根据输入模拟量的大小,控制延迟时间以达到调节占空比的目的
  delay(pulseWidth/1000);
  //关闭LED(发光二极管)
  digitalWrite(ledPin, LOW);
  //根据输入模拟量的大小,控制延迟时间以达到调节占空比的目的
  delay((10000-pulseWidth)/1000);
  //向串口打印sensorValue的值
  Serial.print(map(sensorValue, 0, 1023, 0, 100));
  //向串口打印一个制表符
  Serial.print("%");
  Serial.print('\t');
  //转换sensorValue为对应的电压值并打印到串口
  Serial.print(sensorValue*5.0/1023);
  //给电压值补上单位V(伏特)
  Serial.println("V");
}

打开串口监控窗口,缓慢的条件电位器,你将会发现,发光二极管L逐渐的由亮变暗。不过还有一个奇特的现象,当电压模拟量由低到高的变化过程中,约在0.5V左右的,发光二极管L因为导通而陡然变亮,似乎亮度不是连续变化的。如果你学过电子技术的课程,或者见过二极管的伏安特性曲线,你就会明白这是导通前的死区照成的,你也可以凭此现象大概的估算开启电压的大小。
上面所述的确实是PWM工作的真正原理,不过这种调用delay()延时函数的方法并没有真正利用Arduino的潜能,如果我们以此方法来产生模拟量输出,不仅占用控制器的工作时间,而且难以同时输出多个模拟量。真正使用Arduino内置定时器以驱动模拟输出口输出PWM的方法如下例所示。需要注意的是,并不是所有的数字端口都能输出PWM方波的模拟量,拿起你的Arduino,仔细观察就会发现,上面标有~的6个端口才能输出模拟量,它们分别是3、5、6、9、10和11。我们选择9号端口,这次就不能利用13号端口自带的发光二极管L了,我们需要借助面包板连接电路,电路和程序如下。

int sensorPin = A0; //选择电位器的引脚
int ledPin = 9; // 选择LED(发光二极管)的引脚
int sensorValue = 0; // 存储由传感器传来的值
int pulseWidth;
 
void setup() {
  //配置串口波特率,为9600
  Serial.begin(9600);
}
 
void loop() {
  //读取传感器的值,并把值赋给变量sensorValue
  sensorValue = analogRead(sensorPin);
  pulseWidth = map(sensorValue, 0, 1023, 0, 255);
  analogWrite(ledPin, pulseWidth);
  Serial.print(map(sensorValue, 0, 1023, 0, 100));
  //向串口打印一个制表符
  Serial.print("%");
  Serial.print('\t');
  //转换sensorValue为对应的电压值并打印到串口
  Serial.print(sensorValue*5.0/1023);
  //给电压值补上单位V(伏特)
  Serial.println("V");
}

上面的示例其实就是将上例带波浪线的4行语句等价替换成简洁的analogWrite()函数来输出结果。原理其实完全相同,只是这次无须再占用控制器资源,此外,对于模拟量的PWM方式输出无须在setup()过程中调用pinMode()函数。

配套实验:AnalongInOutSerial

打赏

发表评论