重要的内容写在前面:
- 该系列是以up主太极创客的零基础入门学用Arduino教程为基础制作的学习笔记。
- 个人把这个教程学完之后,整体感觉是很好的,如果有条件的可以先学习一些相关课程,学起来会更加轻松,相关课程有数字电路(强烈推荐先学数电,不然可能会有一些地方理解起来很困难)、模拟电路等,然后就是C++(注意C++是必学的)。
- 文章中的代码都是跟着老师边学边敲的,不过比起老师的版本我还把注释写得详细了些,并且个人认为重要的地方都有详细的分析。
- 一些函数的介绍有参考太极创客官网给出的中文翻译,为了便于现查现用,把个人认为重要的部分粘贴了过来并做了一些修改。
- 如有错漏欢迎指正。
太极创客官网:太极创客 – Arduino, ESP8266物联网的应用、开发和学习资料
七、模拟输出
1、模拟输出函数analogWrite
(1)analogWrite函数借助PWM调制,可以用于输出模拟信号,它有两个参数,第一个参数是模拟引脚号,第二个参数是0到255之间的PWM频率值,0对应“off”,255对应“on”。
(2)在Arduino UNO控制器中,5号引脚和6号引脚的PWM频率为980Hz。在一些基于ATmega168和ATmega328的Arduino控制器中,analogWrite函数支持引脚 3、5、6、9、10、11(在开发板上的引脚处有“~”标记)。
(3)PWM(Pulse Width Modulation)即脉冲宽度调制,在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速、开关电源等领域。
(4)PWM频率值0-255对应占空比0%-100%,呈正比关系,如下图所示。如果有规律地改变PWM频率值,就能产生等效的模拟波形(如上图紫色虚线描绘的波形),当然,PWM频率值固定,也能产生一个等效的模拟电压,其值不局限于高电平或低电平标定的电压,电压值取决于PWM频率值。
2、例1——按键控制LED亮度
(1)根据下图所示将电路连接好,其中电阻可选220Ω(总之在确保不宜过小的前提下不要太大即可)。
(2)将下面的程序下载到开发板中,首先LED会获得一个适中的亮度,持续按下按键1,LED的亮度会持续下降直至熄灭,持续按下按键2,LED的亮度会持续上升直至程序设定的最大值。
bool pushButton1; // 创建布尔型变量用来存储按键开关1的电平状态
bool pushButton2; // 创建布尔型变量用来存储按键开关2的电平状态
int ledPin = 9; //LED引脚号
int brightness = 128; //LED亮度参数(255/2=127.5,一个适中的亮度参数)
void setup()
{
pinMode(2, INPUT_PULLUP); //将引脚2设置为输入上拉模式
pinMode(8, INPUT_PULLUP); //将引脚8设置为输入上拉模式
pinMode(ledPin, OUTPUT); //将LED引脚设置为输出模式
Serial.begin(9600); //启动串口通讯,波特率为9600
}
void loop()
{
pushButton1 = digitalRead(2); //读取引脚2电平状态并将其赋值给布尔变量pushButton1
pushButton2 = digitalRead(8); //读取引脚8电平状态并将其赋值给布尔变量pushButton2
if (!pushButton1 && brightness > 0) //当(持续)按下按键开关1并且LED亮度参数大于0
{
brightness--; //减低LED亮度参数
}
else if (!pushButton2 && brightness < 255) //当(持续)按下按键开关2并且LED亮度参数小于255
{
brightness++; //增加LED亮度参数
}
analogWrite(ledPin, brightness); //模拟输出控制LED亮度
Serial.println(brightness); //将LED亮度参数显示在串口监视器上
delay(10);
}
①持续按下按键1,引脚2将持续处于低电平,每执行一次loop函数,brightness都进行一次自减,直至按键1被松开或者亮度参数减小至0,同时loop函数会将当前的亮度参数作为模拟量输出到引脚9上,并通过串口将当前的亮度参数输出到监视器上。
②持续按下按键2,引脚8将持续处于低电平,每执行一次loop函数,brightness都进行一次自减,直至按键2被松开或者亮度参数增加至255,同时loop函数会将当前的亮度参数作为模拟量输出到引脚9上,并通过串口将当前的亮度参数输出到监视器上。
③程序没有做异常情况的处理,比如两个按键同时按下,这种情况理论上是不允许的。
④亮度参数的取值范围为0-255,若不将其限定在此范围,那么它作为analogWrite函数的参数,会发生强制类型转换,最终的结果也会在0-255之间,至于强制类型转换的具体过程,这里不再赘述。
3、例2——LED呼吸灯
(1)根据下图所示将电路连接好,其中电阻可选220Ω(总之在确保不宜过小的前提下不要太大即可)。
(2)将下面的程序下载到开发板中,可以发现LED灯的亮度从暗变亮,再从亮变暗,以此往复,做一个周期性的“呼吸”。
void setup()
{
pinMode(9, OUTPUT); //设置9号引脚为输出模式
Serial.begin(9600); //启动串口通讯
}
void loop()
{
// LED由暗到明
for (int brightness = 0; brightness <= 255; brightness+=5)
{
analogWrite(9, brightness);
Serial.println(brightness);
delay(10);
}
// LED由明到暗
for (int brightness = 255; brightness >=0 ; brightness-=5)
{
analogWrite(9, brightness);
Serial.println(brightness);
delay(10);
}
}
①LED由暗到明的过程由一个for循环控制,首先亮度参数的初始值为0,它会随着时间的流逝而逐渐增加,直至到达最大值255,for循环结束。
②LED由明到暗的过程也由一个for循环控制,首先亮度参数的初始值为255,它会随着时间的流逝而逐渐减小,直至到达最小值0,for循环结束。
③loop函数重复执行上述两个for循环,以此达到呼吸灯的效果。
八、模拟输入
1、模拟输入函数analogRead
(1)analogRead函数仅有一个参数用于指示模拟引脚,该函数用于从Arduino的模拟输入引脚读取模拟电压的数值。
(2)Arduino控制器有多个10位数模转换通道,这意味着Arduino可以将0-5V的电压输入信号映射到数值0-1023()。关于数模转换这里不进行详细介绍,总之就是将输入的模拟量转换成单片机能“读懂”的数字信号,或者说将模拟量转换为二进制的形式供计算机接收。
(3)引脚的输入范围以及解析度可以使用analogReference指令进行调整。
(4)Arduino控制器读取一次模拟输入需要消耗100微秒的时间(0.0001秒),控制器读取模拟输入的最大频率是每秒10000次。
(5)在模拟输入引脚没有任何连接的情况下,用analogRead指令读取该引脚,这时获得的返回值为不固定的数值,这个数值可能受到多种因素影响,比如将手靠近引脚也可能使得该返回值产生变化。
2、例——电位器控制LED灯亮度
(1)根据下图所示将电路连接好,其中电位器的总电阻为10kΩ,连接LED的电阻可选220Ω(总之在确保不宜过小的前提下不要太大即可)。
(2)将下面的程序下载到开发板中,转动电位器,可以发现LED灯的亮度随之发生变化。
void setup()
{
Serial.begin(9600); //串口通讯初始化(9600 bps)
pinMode(9, OUTPUT); //设置9号引脚为输出模式
}
void loop()
{
int analogInputVal = analogRead(A0); //读取模拟输入值
int brightness = map(analogInputVal, 0, 1023, 0, 255); //将模拟输入数值(0 - 1023)等比映射到模拟输出数值区间(0-255)内,根据该映射关系由analogInputVal得出brightness
analogWrite(9, brightness); //根据模拟输入值调节LED亮度
//将结果通过串口监视器显示
Serial.print("analogInputVal = ");
Serial.println(analogInputVal);
Serial.print("brightness = ");
Serial.println(brightness);
Serial.println("");
}
①电位器的两端引脚分别接5V和GND,中间引脚连接Arduino的引脚A0,转动电位器,引脚A0的电压随之发生改变,程序需要不断地将当前电压记录在变量analogInputVal中,当然,记录的值并不完全等于电压值,只是它与实际电压值有一个映射关系(或者说线性关系)。
②analogRead函数的返回值范围为0-1023(针对本项目而言),而LED灯的亮度参数取值范围为0-255,虽然二者取值范围不同,但是可以为它们构造一个等比映射的关系,如下图所示,这样,引脚A0的电压值就能与LED灯的亮度参数存在一个映射关系(或者说线性关系)。
③工作原理示意图:
九、Arduino内存
1、Arduino内存的存储介质
(1)FLASH(闪存):可用于存储数量较大的静态信息,如Arduino程序,其特点是烧录进开发板后无需做其它修改(而且也不能修改,Arduino程序只能对FLASH进行读操作),另外当Arduino断电后,FALSH中存储的内容不会丢失。
(2)SRAM(静态随机存储器):可用于存储数量较小的动态信息,如程序的变量,Arduino程序可对SARM中存储的内容进行修改,另外当Arduino断电后,SARM中存储的内容会随之丢失。
(3)EEPROM(电可擦除可编程只读存储器):可用于存储断电后需要保持的程序变量,其存储的信息可通过电信号擦除,而且还可通过电信号将信息写入其中,但是程序只能读取其中的信息,而不能写入或修改其中的信息,当Arduino断电后,EEPROM中存储的内容不会丢失。
2、EEPROM的读写
(1)EEPROM的主要操作为读取和写入。对于最基本的读写操作,可以通过EEPROM.read()以及EEPROM.write()来完成,但是这两个函数具有局限性,EEPROM的每一个地址可以存储的信息为1字节,这就限制了EEPROM的每一个地址内所能单独存储的整数数值为0~255区间。由于EEPROM.read()以及EEPROM.write()每一次只能读或写一个字节的数据,假如需要存储超出0~255范围的整数数值或者带有小数点的浮点数,就需要用多个EEPROM协作存储来完成,好在Arduino库还配有EEPROM.put()和EEPROM.get()这两个函数。
(2)例——向EEPROM存储数据:
#include <EEPROM.h> //使用EEPROM库需包含其头文件
int addr = 0; //被写入数据的EEPROM地址编号(即哪一个存储地址将要被写入数据)
void setup()
{
/** setup内无内容 **/
}
void loop()
{
int val = 123; //将要存储于EEPROM的整数数值
EEPROM.write(addr, val); //将数值val写入EEPROM,地址为addr
addr = addr + 1; //转入下一存储地址
if (addr == EEPROM.length())
{
addr = 0; //当存储地址序列号达到EEPROM的存储空间结尾,重新返回到EEPROM的开始地址
}
delay(100);
}
①EEPROM.write(addr, val):将1字节大小的数值val(取值范围0-255)写入EEPROM,地址为addr。
②EEPROM.length():返回EEPROM的总存储空间大小。(不同型号Arduino开发板具有不同大小的EEPROM存储空间,对于Arduino Uno,其EEPROM共有1kb存储空间,即允许使用的EEPROM地址序列号为0-1023)
(3)例——基于上例,从EEPROM读取数据:
#include <EEPROM.h> //使用EEPROM库需包含其头文件
int address = 0; //从EEPROM的第一个字节(地址序号0)开始读取
byte value;
void setup()
{
//初始化串口通讯并等待初始化完成
Serial.begin(9600);
while (!Serial); //等待初始化串口通讯初始化完成
}
void loop()
{
value = EEPROM.read(address); //从EEPROM存储地址为address的存储单元中读取数据
Serial.print(address);
Serial.print("\t");
Serial.print(value, DEC);
Serial.println();
address = address + 1;
if (address == EEPROM.length())
{
address = 0;//当存储地址序列号达到EEPROM的存储空间结尾,重新返回到EEPROM的开始地址
}
delay(500);
}
①EEPROM.read(address):从EEPROM存储地址为address的存储单元中读取1字节的数据。
②EEPROM.length():返回EEPROM的总存储空间大小。(不同型号Arduino开发板具有不同大小的EEPROM存储空间,对于Arduino Uno,其EEPROM共有1kb存储空间,即允许使用的EEPROM地址序列号为0-1023)
(4)EEPROM.put(Addr, Var):将Var写入EEPROM,Var的大小可以不止1字节,那么它在EEPROM中可以占用不止1字节的空间,其起始地址为Addr。
①例——向EEPROM存储浮点型数据:每一个浮点型变量所占的内存大小是4个字节,假如选择从EEPROM地址序号0的存储单元开始来存储变量floatVar1,那么Arduino将会把序号0-3的存储单元都分配给变量floatVar1,所以变量floatVar2的存储起始地址序号为4。
#include <EEPROM.h>
void setup()
{
Serial.begin(9600);
float floatVar1 = 123.456; //将要存储入EEPROM的浮点型数据1
float floatVar2 = 234.567; //将要存储入EEPROM的浮点型数据2
int fVar1Addr = 0; //存储floatVar1的EEPROM地址
int fVar2Addr = 4; //存储floatVar2的EEPROM地址
EEPROM.put(fVar1Addr, floatVar1); //将floatVar1存入EEPROM地址1
delay(10);
EEPROM.put(fVar2Addr, floatVar2); //将floatVar2存入EEPROM地址2
delay(10);
Serial.println("Finished writing float data!");
}
void loop()
{
/* 无内容 */
}
②例——向EEPROM存储整型数据:每一个整型变量所占的内存大小是2个字节(对于Arduino Uno来说是这样),假如选择从EEPROM地址序号1的存储单元开始来存储变量i,那么Arduino将会把序号1-2的存储单元都分配给变量i。
#include <EEPROM.h>
void setup()
{
Serial.begin(9600);
int i = 9999; //将要存储入EEPROM的整型数据
int address = 1; //EEPROM存储地址
EEPROM.put(address, i); //将整型变量i存入EEPROM
Serial.println("Finished writing int data type!");
}
void loop()
{
/* 无内容 */
}
(5)EEPROM.get(Addr, Var):以Addr为起始地址,从EEPROM的相应存储单元开始读取数据,具体读取多少字节由Var的变量类型决定。
①例——从EEPROM读取浮点型数据:在例“向EEPROM存储浮点型数据”的基础上,确保EEPROM中已经写入可以获取的浮点型数据了,由于floatVar1的变量类型为浮点型,占用4个字节,所以Arduino会从EEPROM的0号存储单元开始读取4字节数据返回到floatVar1,于是下一个浮点数据需从4号存储单元开始读取。
#include <EEPROM.h>
void setup()
{
float floatVar1; //此变量用于存储获取到的EEPROM浮点型数据1
float floatVar2; //此变量用于存储获取到的EEPROM浮点型数据2
int fVar1Addr = 0; //存储floatVar1的EEPROM地址
int fVar2Addr = 4; //存储floatVar2的EEPROM地址
Serial.begin(9600);
Serial.println("Get float from EEPROM: ");
//从EEPROM中获取浮点型数据
EEPROM.get(fVar1Addr, floatVar1);
delay(10);
EEPROM.get(fVar2Addr, floatVar2);
delay(10);
Serial.print("floatVar1 = ");
Serial.println(floatVar1, 3);
Serial.print("floatVar2 = ");
Serial.println(floatVar2, 3);
}
②例——从EEPROM读取整型数据:在例“向EEPROM存储整型数据”的基础上,确保EEPROM中已经写入可以获取的整型数据了,由于i的变量类型为整型,占用2个字节,所以Arduino会从EEPROM的1号存储单元开始读取2字节数据返回到i。
#include <EEPROM.h>
void setup()
{
int i; //此变量用于存储获取到的EEPROM整数型数据
int eeAddress = 1; //起始获取信息的EEPROM地址
Serial.begin(9600);
Serial.print("Get int from EEPROM: ");
EEPROM.get(eeAddress, i); //从eeAddress地址获取整数型数据
Serial.println(i);
}
void loop()
{
/* Empty loop */
}
(6)EEPROM.update()与EEPROM.write()类似,同样可以用来向EEPROM写入数据,但是与EEPROM.write()不同的是,EEPROM.update()只会更新EEPROM中的数据,也就是说,只有在将要写入EEPROM的数据与EEPROM内现存的数据不同时,EEPROM.update()才会将这一数据写入EEPROM。
3、内存优化
(1)使用串口监视器输出一个字符串常量时,这个字符串常量存储在SARM中,但实际上这个字符串常量在程序编译后并不需要改变,它完全可以存储在FALSH中,于是可以将这个字符串常量用“F()”包含,这样,这个字符串常量就会存储在FALSH中,从而节省SARM的空间,更合理地利用内存。
(2)在全局区建立的常变量原本也默认存放在SARM中,但全局常变量本身就不允许被改变,而且它也不像局部变量那样生命周期有限,程序只要运行,全局(常)变量就一直存在,所以它也可存放在FALSH中,具体声明方式为在类型声明后添加“PROGMEM”。
(3)在能实现需求的前提下,应尽可能地使用占用内存少的数据类型,从而减少内存的浪费。
(4)在能实现需求的前提下,必须删除无用的代码,另外仅用于调试的代码,在产品上市前也应将其删除(或将其注释,或用条件编译区分也可)。