重要的内容写在前面:
- 该系列是以up主太极创客的零基础入门学用Arduino教程为基础制作的学习笔记。
- 个人把这个教程学完之后,整体感觉是很好的,如果有条件的可以先学习一些相关课程,学起来会更加轻松,相关课程有数字电路(强烈推荐先学数电,不然可能会有一些地方理解起来很困难)、模拟电路等,然后就是C++(注意C++是必学的)。
- 文章中的代码都是跟着老师边学边敲的,不过比起老师的版本我还把注释写得详细了些,并且个人认为重要的地方都有详细的分析。
- 一些函数的介绍有参考太极创客官网给出的中文翻译,为了便于现查现用,把个人认为重要的部分粘贴了过来并做了一些修改。
- 如有错漏欢迎指正。
视频链接:3-0 本章介绍_哔哩哔哩_bilibili
太极创客官网:太极创客 – Arduino, ESP8266物联网的应用、开发和学习资料
二、红外遥控
1、IRremote库的安装
2、红外遥控原理
(1)红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专用的红外接收头进行解调输出,通常采用的通信协议标准是NEC标准。
(2)NEC标准中通信双方的基本发送与接收:
①空闲状态:红外LED不亮,接收头输出高电平。
②发送方发送低电平:红外LED以38KHz频率闪烁发光(规定特定频率的目的是抗干扰,否则容易被自然界的红外光影响,这个部分是在底层就封装好了的,下面提到的发送高电平其实落实到底层都是指不断闪烁的光,只是没在时序图上表现出来而已),接收头输出低电平。
③发送方发送高电平:红外LED不亮,接收头输出高电平。
(3)NEC编码:
①发送Data时低位在前,高位在后(Data共32位)。
②地址码是为了区分不同(品牌)的遥控器。
③设置反码是为了验证Data在传输过程中有没有出现错误。
④发送Data时,前560us低电平,后560us高电平,表示发送数据0;前560us低电平,后1690us高电平,表示发送数据1。
⑤如果按住遥控器的按键不放,会不断发送Repeat部分,直至松开按键。
⑥Start是起始标志。
(4)关于“协议”具体是怎么一回事,可以参考课程《计算机网络》,这里不再赘述。
3、Arduino做信号接收
(1)下面使用的红外接收器是1838红外接收器,它有3个引脚,分别为OUT(输出引脚)、VCC(连接5V电源)及GND(连接零电势),按照下图所示将电路连接好。
(2)将下面的程序下载到开发板中,然后进行人工调试。
#include <IRremote.h>
#define RECV_PIN 11
IRrecv irrecv(RECV_PIN); //创建一个红外接收器对象并与引脚11绑定
decode_results results; //存储接收到的红外遥控信息
void setup()
{
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, LOW); //开发板的LED灯初始状态为灭
Serial.begin(9600);
Serial.println("Enabling IRin");
irrecv.enableIRIn(); //启动irrecv对象的红外接收
Serial.println("Enabled IRin");
}
void loop()
{
if (irrecv.decode(&results)) //判断红外接收器所接收到的红外信号是否可以被解析
{
Serial.println(results.value, HEX); //results.value为红外遥控信号的具体数值
if(results.value == 0xFFA857) //如果控制信息数值为FFA857
{
Serial.println("Command Received: Turn On LED.");
digitalWrite(LED_BUILTIN, HIGH);
}
if(results.value == 0xFFE01F) //如果控制信息数值为FFE01F
{
Serial.println("Command Received: Turn Off LED.");
digitalWrite(LED_BUILTIN, LOW);
}
irrecv.resume(); // 恢复接收下一个红外遥控信号
}
delay(100);
}
①decode函数用于判断irrecv对象控制的红外接收器所接收到的红外信号是否可以被解析,如可以成功解析,则返回非零数值,并将解析结果存储于results中,如无法成功解析则返回零。(值得注意的是,本程序使用的IRremote库是2.6.1版本的,高版本库中编写的库函数可能不适用于本程序)
②每一次解析完成,都需要调用resume函数从而让irrecv对象控制的红外接收器开始准备接收下一个红外遥控指令。
③按下遥控器的按键,红外接收器可以接收到相应的可解析信号,Arduino通过串口将信号对应的的具体数值发送,由此调试者可测出遥控器每个按键所对应的解析数值(对于不同的遥控器,其按键所对应的解析数值可能是不同的,基本都需要在调试时自测一遍)。
④按下遥控器的按键,红外接收器可以接收到相应的可解析信号,Arduino根据解析出的数值判断用户按下了哪一个按键,进而做出相应的开灯或者熄灯操作。
4、Arduino做信号发射
(1)红外LED有两个引脚,短引脚为LED负极,长引脚为LED正极,同一般的发光二极管差不多,按照下图所示将电路连接好。
(2)将下面的程序下载到开发板中,然后进行人工调试(用上例的Arduino接收本例产生的信号,将可解析信号对应的数值通过串口发送到计算机上)。
#include <IRremote.h>
IRsend irsend; //创建一个红外发射器对象
void setup()
{
/* 无内容 */
}
void loop()
{
for (int i = 0; i < 3; i++) //连发三次,一次间隔40毫秒
{
irsend.sendNEC(0xF7C03F, 32); //发射NEC红外遥控协议F7C03F指令码
delay(40);
}
delay(5000); //延迟5秒
}
①IRremote库支持NEC、Sony、Philips RC5、Philips RC6等协议指令,本示例程序中Arduino将通过调用函数sendNEC(0xF7C03F, 32)来发射NEC协议指令,该函数的两个参数中,0xF7C03F为指令信息内容,32位指令信息位数。
②假如需要发射Sony协议指令则可以调用函数sendSony(0xa90, 12),其中0xa90为指令信息内容,12位指令信息位数。
③如果需要发射其它遥控协议指令请参考以下程序代码:
void sendRC5(unsigned long data, int nbits); //发射Philips RC5协议指令
void sendRC6(unsigned long data, int nbits); //发射Philips RC6协议指令
void sendSharp(unsigned long data, int nbits); //发射Sharp协议指令
void sendPanasonic(unsigned int address, unsigned long data); //发射Panasonic协议指令
void sendJVC(unsigned long data, int nbits, int repeat); //发射JVC协议指令
void sendRaw(unsigned int buf[], int len, int hz); //发射原始指令
④调用的发射指令函数默认以引脚3为输出口,所以红外LED的正极接在Arduino的引脚3上。
5、例——自适应万能遥控器
(1)按照下图所示将电路连接好。该电路可作为自适应万能遥控器的雏形,当用其它遥控器对1838接收器发送信号时,Arduino将信号解析后会将信号对应的数值存储在EEPROM中,按下电路的按键,Arduino将控制红外LED发射该数值对应的可解析信号。
(2)将下面的程序下载到开发板中,然后进行人工调试(可以用家用电器的遥控器进行实验,如果该自适应万能遥控器能够“学习”家用电器遥控器的功能,比如打开空调,说明调试通过)。
①全局变量、宏定义及包含头文件:
#include <IRremote.h>
#include <EEPROM.h>
#define codeTypeEAddr 0 //存放红外信号编码类型的EEPROM地址
#define codeLenEAddr 1 //存放红外信号编码长度的EEPROM地址
#define toggleEAddr 2 //存放红外信号RC5/RC6类型的EEPROM地址
#define codeValueEAddr 3 //存放红外信号数值的EEPROM地址
#define RECV_PIN 11 //红外接收器OUT引脚接Arduino的引脚11
#define BUTTON_PIN 12 //按键开关接Arduino的引脚12
#define STATUS_PIN LED_BUILTIN //状态显示LED使用开发板内置的LED
IRrecv irrecv(RECV_PIN); //创建红外遥控接收器对象,接引脚11
IRsend irsend; //创建红外遥控发射对象
decode_results results; //储存接收到的红外遥控信息
// 红外信号存储变量
int codeType; //记录红外信号编码类型
unsigned long codeValue; //记录红外信号数值(如果不是raw型)
unsigned int rawCodes[RAWBUF]; //记录raw型信号
int codeLen; //记录红外信号编码长度
int toggle; //记录红外信号RC5/RC6类型
int lastButtonState; //此变量用于判断发射红外信号的按键开关所处的状态
②初始化工作部分:除了一些基本的配置以外,需要将EEPROM中存储的红外信号信息读取到全局变量中。
void setup()
{
Serial.begin(9600);
irrecv.enableIRIn(); //启动红外接收
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(STATUS_PIN, OUTPUT);
delay(10);
loadEepromValues(); //从EEPROM中读取红外信号信息(具体信息见函数部分)
}
③记录收到的红外信号:对于接收到的信号,将其数值和信号类型解析出来,分别记录在程序的全局变量及EEPROM中。
void storeCode(decode_results *results)
{
codeType = results->decode_type; //记录接收信号所使用的协议
int count = results->rawlen; //记录raw型信号
if (codeType == UNKNOWN) //如果接收到的信号是无法识别的协议,则存储为raw型数据
{
Serial.println("Received unknown code, saving as raw");
codeLen = results->rawlen - 1; //将首个数值放弃(间隙)
//细微调整信号内容,将信息变短、空档间隙变长,从而取消红外接收信号的扰动
for (int i = 1; i <= codeLen; i++)
{
if (i % 2) //信号
{rawCodes[i - 1] = results->rawbuf[i]*USECPERTICK - MARK_EXCESS;Serial.print(" m");}
else //空档间隙
{rawCodes[i - 1] = results->rawbuf[i]*USECPERTICK + MARK_EXCESS;Serial.print(" s");}
Serial.print(rawCodes[i - 1], DEC);
}
Serial.println("");
}
else
{
if (codeType == NEC) //如果接收到的信号采用NEC协议
{
Serial.print("Received NEC: ");
if (results->value == REPEAT) //按键按住不松开,信号值为REPEAT
{Serial.println("repeat; ignoring.");return;}
}
else if (codeType == SONY) //如果接收到的信号采用SONY协议
Serial.print("Received SONY: ");
else if (codeType == PANASONIC) //如果接收到的信号采用PANASONIC协议
Serial.print("Received PANASONIC: ");
else if (codeType == JVC) //如果接收到的信号采用JVC协议
Serial.print("Received JVC: ");
else if (codeType == RC5) //如果接收到的信号采用RC5协议
Serial.print("Received RC5: ");
else if (codeType == RC6) //如果接收到的信号采用RC6协议
Serial.print("Received RC6: ");
else { //无法识别信号
Serial.print("Unexpected codeType ");
Serial.print(codeType, DEC);Serial.println("");
}
Serial.println(results->value, HEX); //输出信号数值
codeValue = results->value; codeLen = results->bits; //记录信号数值和位数
}
writeEepromVal(); //将收到的信号信息储存于EEPROM
}
④发射红外信号:根据记录的信号类型和信号对应数值,调用相应的库函数进行信号发射。
void sendCode(bool repeat)
{
if (codeType == NEC) { //如果是NEC协议信号
if (repeat) { //且如果是发射重复信号(也就是按键一直按下没有松开)
irsend.sendNEC(REPEAT, codeLen); //发射NEC协议的重复信号
Serial.println("Sent NEC repeat"); //串口监视器输出红外指令信号协议
}
else { //否则发射NEC协议红外指令信号
irsend.sendNEC(codeValue, codeLen); //发射NEC协议红外指令信号
Serial.print("Sent NEC "); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
}
else if (codeType == SONY) { //如果发射的信号是SONY协议信号
irsend.sendSony(codeValue, codeLen); //发射SONY协议红外指令信号
Serial.print("Sent Sony "); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
else if (codeType == PANASONIC) { //如果发射的信号是PANASONIC协议信号
irsend.sendPanasonic(codeValue, codeLen); //发射PANASONIC协议红外指令信号
Serial.print("Sent Panasonic"); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
else if (codeType == JVC) { //如果发射的信号是JVC协议信号
irsend.sendJVC(codeValue, codeLen, false); //发射JVC协议红外指令信号
Serial.print("Sent JVC"); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
else if (codeType == RC5 || codeType == RC6) { //如果发射的信号是RC5或RC6协议信号
if (!repeat) //新按键按下后反转toggle位
toggle = 1 - toggle;
//将toggle位放入信号代码中发送
codeValue = codeValue & ~(1 << (codeLen - 1)); //位清除
codeValue = codeValue | (toggle << (codeLen - 1)); //置位
if (codeType == RC5) { //发射的信号是RC5协议信号
irsend.sendRC5(codeValue, codeLen); //发射RC5协议红外指令信号
Serial.print("Sent RC5 "); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
else { //发射的信号是RC6协议信号
irsend.sendRC6(codeValue, codeLen); //发射RC6协议红外指令信号
Serial.print("Sent RC6 "); //串口监视器输出红外指令信号协议
Serial.println(codeValue, HEX); //串口监视器输出红外指令信号数值
}
}
else if (codeType == UNKNOWN) {
irsend.sendRaw(rawCodes, codeLen, 38); //假设信号为频率38KHz,发射raw型信号
Serial.println("Sent raw"); //串口监视器输出红外指令信号协议
}
}
⑤通过EEPROM读取红外信号信息:每次Arduino通电后,都会从EEPROM中读取存储的红外信号信息,从而确保自适应万能遥控器在断电后依然可以保持上一次运行时所存储的红外信号信息。
void loadEepromValues()
{
codeType = EEPROM.read(codeTypeEAddr);
delay(10);
codeLen = EEPROM.read(codeLenEAddr);
delay(10);
toggle = EEPROM.read(toggleEAddr);
delay(10);
toggle = EEPROM.read(toggleEAddr);
delay(10);
EEPROM.get(codeValueEAddr, codeValue);
}
⑥将红外信号信息储存于EEPROM:每一次自适应万能遥控器接收到新的红外遥控信号,都将最新接收的红外遥控信号存于EEPROM,从而确保信号信息不会因为Arduino断电而丢失。
void writeEepromVal()
{
EEPROM.write(codeTypeEAddr, codeType);
delay(10);
EEPROM.write(codeLenEAddr, codeLen);
delay(10);
EEPROM.write(toggleEAddr, toggleEAddr);
delay(10);
EEPROM.put(codeValueEAddr, codeValue);
delay(10);
}
⑦程序主循环部分:Arduino需不断读取当前开关状态,将其记录在变量buttonState中,并用一个全局变量lastButtonState记录上一次读取的开关状态,以此区分按键的四个状态——第一次按下、持续按住不放、松开及闲置,按键闲置时需要检测有没有外来的红外信号,有则将其接收并记录相关信息,按键第一次按下时则按照记录的信号信息进行信号发送,按键持续按住不放时,针对NEC协议应发送重复信号,按键松开时应重新打开红外接收。
void loop()
{
int buttonState = !digitalRead(BUTTON_PIN); //读取当前的按键开关状态(检查用户是否按下了按键开关)
if (lastButtonState == HIGH && buttonState == LOW) //如果按键开关是被按下后再抬起的
{
Serial.println("Released"); //通过串口监视器输出"按键抬起"
irrecv.enableIRIn(); //启动红外接收器信号接收
}
if (buttonState) //如果按键开关处于被按下的状态
{
Serial.println("Pressed, sending"); //通过串口监视器输出"按键按下"
digitalWrite(STATUS_PIN, HIGH); //闪烁状态显示红外LED告知用户当前"遥控器"正在发射红外信号(点亮LED)
sendCode(lastButtonState == buttonState); //信号发射(依据开关状态变量判断按键是否是被持续按下没有松开)
digitalWrite(STATUS_PIN, LOW); //闪烁状态显示红外LED告知用户当前"遥控器"正在发射红外信号(熄灭LED)
delay(50); //信号发射间歇
}
else if (irrecv.decode(&results)) //如果按键开关处于没有被按下的状态,则实时检查红外接收器并对接收到的信号进行解码
{ //如果接收到的红外信号可以通过decode函数成功解码
digitalWrite(STATUS_PIN, HIGH); //闪烁状态显示红外LED告知用户当前"遥控器"正在发射红外信号(点亮LED)
storeCode(&results); //将解码的红外信号信息进行储存
irrecv.resume(); //恢复红外接收器
digitalWrite(STATUS_PIN, LOW); //闪烁状态显示红外LED告知用户当前"遥控器"正在发射红外信号(熄灭LED)
}
lastButtonState = buttonState; //更新按键开关状态变量
}