创建一个拍子控制的Arduino遥控器

我最近一直在与Arduino Uno玩耍,我想分享一些我学到的东西。


我对进入硬件感兴趣了一段时间,但我从未真正了解它。 几周前,我刚刚说了拧紧它,并购买了带有Uno的小型Arduino入门套件,小型面包板,一些跳线,LED,电阻和各种传感器:https://www.amazon.com/gp/product / B01DGD2GAO / ref = oh_aui_detailpage_o01_s00?ie = UTF8&psc = 1

有一段时间,我父亲一直在问我(有点开玩笑)让他成为电视的声控遥控器。 听起来很有趣,但我认为ASR略高于我的薪水。 也许将来我会尝试。

我们决定制作一个拍手控制的遥控器,这听起来比识别语音命令更可行。 在这篇文章中,我将描述编写软件和设置遥控器电路的过程。 希望您能像我所做的那样学到一些东西。


我怀疑大多数读者都知道电视遥控器是通过红外线向电视发送命令的。 电视制造商必须选择如何将每个数字命令(即功率,频道上/下)编码为红外二极管输出的模拟信号。

索尼SIRC协议

我有一台使用SIRC协议传输远程命令的索尼电视。 您拥有哪台电视会极大地影响您解码和读取遥控器信号的方式。

SIRC协议使用脉宽调制或PWM来编码0和1。 0由600微秒脉冲和600微秒间隔表示。 1是1.2毫秒脉冲(是0的两倍),后跟相同的等待时间。

每个命令都有一个附加的地址,该地址表示该命令将转到哪种设备(电视,VCR,CD播放器等)。

该协议有三种版本:12位,15位和2位版本。 在每个版本中,实际命令的长度为7位,不同版本之间唯一改变的是地址的长度。

我的电视使用12位版本,这就是我要谈论的版本。 在12位版本中,地址的长度为5位,对于电视,其地址为00001。每个命令采用以下形式:

如图所示,您以一个2.4毫秒的脉冲开始(随后是标准的600微秒等待时间)。 然后,您发送命令,然后发送地址。 命令和地址都是小端。

要对该协议进行更深入的说明,请参见以下文章:https://www.sbprojects.net/knowledge/ir/sirc.php

使用TSOP

要读取来自遥控器的红外信号,我们需要一个红外接收器。 您可能只使用常规的红外二极管,但是购买TSOP,IR接收器要比拥有一些额外的电路来处理增益和降噪要容易得多。 为此项目,我购买了TSOP4838。

如上所示,TSOP具有三个引脚:一个用于供电,一个用于接地,另一个用于读取输出。 正是我们所需要的。

让我们将TSOP连接到我们的Arduino。 您需要将+ 5V引脚连接到Vs,并将GND引脚接地(显然)。 然后,您需要将OUT连接到引脚以进行读取。 最初,我以为这必须是模拟引脚之一,但事实证明,只有在将模块连接到启用PWM的数字引脚之一时,您才能从该模块读取数据。 因此,我们将使用引脚11。

解码信号

现在我们可以从IR接收器读取数据,我们需要对其进行解码以查看命令是什么。 现在,我们可以使用Ken Schiriff的IRremote库,它非常方便。 但是,自己编写它会更有趣。

设置销钉:

  int irPin = 11;  void setup(){ 
Serial.begin(9600);
pinMode(irPin,INPUT);
}

我们稍后将需要访问串行监视器以读取命令,因此我们以9600 bps的速度开始传输。

现在,让我们编写代码以解码SIRC命令:

  int commandMask = 127; void loop(){ 
持续时间= pulseIn(irPin,LOW);
if(duration> = 2400){//开始爆发
int bitnum = 0;
int位串= 0;
int持续时间= pulseIn(irPin,LOW);

而(持续时间> = 600 &&持续时间<2000){
int currbit = 0;
如果(abs(持续时间1200)<abs(持续时间600))
currbit = 1;

位串=(currbit << bitnum)| 位串
++ bitnum;
持续时间= pulseIn(irPin,LOW);
}
int地址=位串>> 7;
int命令=位串&命令掩码;
Serial.print(“地址:”);
Serial.print(地址);
Serial.print(“,Command:”);
Serial.println(命令);
}
}

pulseIn函数返回第一个参数中的引脚转到第二个参数并再次返回所需的持续时间。 因此, pinMode(irPin, LOW)返回引脚11的值从HIGH变为LOW到HIGH所花费的时间(以微秒为单位)。

如果我们检测到至少2400微秒的脉冲,则说明我们已经收到SIRC协议的开始脉冲串。

接下来,我们初始化一些整数, bitnumbitstringbitnum是当前读取的位数, bitstring是当前读取的命令。 然后我们继续阅读,直到发出脉冲直到持续时间超出允许的值范围为止(我使用2000而不是2400作为上限,因为我必须受到一些干扰)。

对于每个脉冲,如果长度接近1200而不是600,则我们决定接收到逻辑1。否则,它为0。然后,将位串的第bitnum位设置为读取的逻辑位。 然后增加读取和重新读取脉冲的位数。

现在,位bitstring应该是12位,其中5个最高有效位是地址,而7个低位是命令。

注意:显然,每个命令被发送3次,相隔大约45毫秒。 因此,我们将打印三次命令。

将代码上传到Arduino,将遥控器指向TSOP,然后按下相应的按钮,我们可以记录所需的命令。 就我的目的而言,我所需要的只是电源(21),频道上/下(16/17)和音量上/下(18/19)。

现在,我们可以再次使用IRremote库,但是自己实现它更有趣。 令人惊讶的是,发送比接收要复杂得多。

传输很复杂,因为我们需要以比使用内置Arduino delayMicrosecondsdigitalWrite函数可以实现的频率更精细的频率(40kHz)和占空比(1/4)进行操作。

占空比

之前关于逻辑0的脉冲为600微秒长而1200为1的脉冲的讨论尚未完成。 这些脉冲在突发时间的100%内不是高电平。 它们通过占空比进行参数化:

PWM信号的占空比是“高”状态下的时间量与一个周期所花费的时间之比。 SIRC协议在40kHz时使用25%的占空比。 如果使用delayMicroseconds ,则需要每6.25微秒切换一次输出引脚上的值。 delayMicroseconds不提供亚毫秒级的分辨率。

Arduino计时器

值得庆幸的是,还有另一种方法。

ATmega328p带有三个用于PWM的定时器/计数器寄存器。 这些计时器的基本机制是它们增加一个计数器,并以规则的间隔递增该计数器,直到达到最大值为止。 一旦出现此值,计时器将重新启动。 该值确定PWM波的频率。

还有一个寄存器可以修改PWM波的占空比。 当该值被计数器计数时,将HIGH的值放在输出寄存器上,并在其后的某个时间返回LOW。

计时器有不同的操作模式。 这些模式修改属性,例如,达到限制后计时器如何重置或如何指定最高限制。

有关所有操作模式的详细说明,请访问:http://www.righto.com/2009/07/secrets-of-arduino-pwm.html。 我们将使用的工作模式或波形生成模式(WGM)称为相位校正PWM。

相位校正PWM

在相位校正PWM中,计时器连续递增计数,直到达到最大值,然后递减直到计数器为零。 通常,最大值是计数器寄存器的宽度(计时器0和2为255,计时器1为65535)。 但是,我们可以将自己的最大值放入寄存器OCRnA中。 对于定时器寄存器,小写的n是对应于定时器之一的数字0、1或2。

通过设置OCRnB ,我们可以修改占空比。 在相位校正PWM中,当计数器低于OCRnB中的值时,高电平电压将被施加到输出引脚OCnB上 ,当计数器高于OCRnB时它将返回低电平。 对于定时器2(我们将使用的定时器), OC2B是引脚3。从现在开始,所有寄存器名称中的n将替换为2。

我最初尝试使用计时器0,但是没有任何作用。 经过大量挖掘,我发现delaydelayMicroseconds实际上使用了计时器0! 将此定时器用于我们的自定义PWM将破坏这些功能。

虽然我们的PWM频率与OCR2A中的值成反比,但它并未完全指定频率。 该频率的实际公式为16MHz /(2 *预分频器* OCR2A)。 ATmega328p的CPU频率为16MHz,而预分频器为1、8、32、64、128、256或1028,并由寄存器TCCR2B的三个最低有效位指定

为了达到我们的目的,我们将使用1的预分频器,因此要实现40kHz的频率,必须将OCR2A设置为200。 16MHz /(2 * 200)= 40kHz。 然后,要使我们的期望占空比为25%,我们必须将其设置为50或200/4。

传输信号

现在是时候编写代码了。 首先定义一些常量:

  const int startBurstLength = 2400; 
const int oneBurstLength = 1200;
const int zeroBurstLength = 600;
const int waitLength = 600;
const字节topVal = 200;
const byte dutyCycle = topVal / 4;

突发长度是根据SIRC我们将用来传输0或1的topVal ,并且topValdutyCycle是前面讨论的用于生成正确的PWM脉冲dutyCycle的值。

设置与接收类似,但是现在将引脚3用于输出:

  void setup(){ 
Serial.begin(9600);
pinMode(3,输出);
}

现在,我们将编写或loop函数以通过串行监视器将我们发送的代码传输到Arduino:

 无效循环(){ 
如果(Serial.available()){
命令= Serial.readString()。toInt();
if(命令== 0)返回;

对于(int i = 0; i <3; i ++){
发送(命令);
延迟(40);
}
}
}

根据SIRC协议的规定,我们以40ms的延迟发送3次每个命令。

现在,我们必须编写函数来传输命令:

 无效标记(int delayUs){ 
TCCR2A | =(1 << COM2B1);
delayMicroseconds(delayUs);
空格(waitLength);
}
无效空间(int delayUs){
TCCR2A&=〜(1 << COM2B1); //禁用b的输出
delayMicroseconds(delayUs);
}
无效sendTVAddress(){
标记(oneBurstLength);
对于(char i = 0; i <4; i ++){
标记(zeroBurstLength);
}
}
无效sendCommand(字节命令){
TIMSK2&=〜(1 << TOIE2);

digitalWrite(3,LOW);

OCR2A = topVal;
OCR2B =占空比;
TCCR2A =(1 << WGM20);
TCCR2B =(1 << WGM22)| (1 << CS20);
标记(startBurstLength);

对于(char i = 0; i <7; i ++){
if(命令%2 == 0)mark(zeroBurstLength);
else mark(oneBurstLength);
命令>> = 1;
}
}
无效的传输(字节命令){
sendCommand(命令);
sendTVAddress();
}

我们使用两个过程来传输命令: sendCommandsendTVAddresssendCommand首先将TIMSK2的TOIE2位清零,这将禁用定时器2上的中断。如果不这样做,则PWM可能会被系统中断,从而使命令无效。 顺便说一句,我们使用1<<(OFFSET)语法设置寄存器的第OFFSET位。

然后,为了安全起见,我们将LOW值设为低。

如前所述,topVal(200)进入寄存器OCR2A ,占空比(50)进入OCR2B

我们在TCCR2A中设置了WGM20,在TTCR2B中设置了WGM22 ,以表示我们要使用相位校正PWM。 以下是为特定WGM设置WGM位的图表:

我们必须在两个寄存器之间拆分WGM位,因为这是TCCR2A / B的结构方式:

这些寄存器带有标志和参数,因此Atmel必须拆分三个WGM位。

关于sendCommand的寄存器初始化,最后要提到的是TCCR2BCS20的设置。 根据数据表,设置CS20对应于1的预分频器:

现在让我们看一下mark函数:

 无效标记(int delayUs){ 
TCCR2A | =(1 << COM2B1);
delayMicroseconds(delayUs);
空格(waitLength);
}

如果您在数据手册中查找,您会发现COM2B1位使能了定时器2的B输出(B输出是引脚3)。 我们将此位与TCCR2A进行“或” 运算 ,以使其等于1,然后根据要发送1还是0,让它运行600或1200微秒。最后,我们每个脉冲等待600微秒:

 无效空间(int delayUs){ 
TCCR2A&=〜(1 << COM2B1); //禁用b的输出
delayMicroseconds(delayUs);
}

这段代码与mark非常相似,但是我们将COM2B1取反,并将该位设置为0。

如果您想深入了解不同的WGM模式以及所有不同的寄存器如何影响时序,我强烈建议您查看ATmega328p数据手册。

放在一起

现在我们已经编写了代码,是时候连接IR二极管了。 设置完成后,应如下所示:

我在Fritzing应用程序中找不到红外组件,因此请确保该电路未使用红色LED。 我还建议将二极管朝面包板的背面放置,这样您就可以将其向上倾斜,因为这对于电视上的二极管至关重要。

如果您有一些杜邦公-母线,那就更好了,这样您就可以指向二极管而不必倾斜整个面包板。

现在一切都已设置好,您可以将代码上传到Arduino上。 然后打开串行监视器并输入一些命令,例如21通电或16信道接通。

如果电视正在响应,则可能是由于多种原因造成的:1)二极管接反了; 2)它没有对准电视的接收器; 3)发射代码不正确。 计时器代码对微小的变化非常敏感。 例如,如果仅更改TCCR2A / B中的一位 ,这可能会像更改WGM一样剧烈。

如果所有其他方法均失败,则可以从Ken Schiriff的IRremote库发送Sony。 稍后我将介绍如何使用该库。


现在我们已经具备了发送红外命令的机制,我们需要一种监听环境并检测/计数发生拍手的方法。 在我们开始考虑之前,我们需要将麦克风连接到Arduino。

使用麦克风

对于麦克风,我使用的是从旧座机上撕下来的两针驻极体。 (我有几个MAX4466三引脚驻极体,但尚未将其焊接)。

现在,我不是电气工程师(我是计算机科学专业的学生),所以我很难弄清楚如何正确连接麦克风。 我在网上搜寻,但大多数电路都包含某种运算放大器或晶体管。 我没有像这样的东西可支配,因此我认为我只会使用经过尝试和真正的反复试验的方法。

经过大量的反复试验,我决定采用以下设置:

我有点想模仿一个RC高通滤波器,我阅读了拍板的用法,但是我很确定以上内容并非完全如此(同样,没有电气工程教育)。

电阻器在上面的电路中至关重要,而电容器似乎仅提供了较小的改进。 因此,如果没有可支配的东西,请不要担心。

如果有电气工程经验的人想教我电路的外观,那将是非常受欢迎的。

这是上述电路的电路板草图:

从麦克风读取

从麦克风读取就像在引脚A0(或您选择的模拟引脚)上执行analogRead一样简单。 如果您在本教程的前面各节中都遵循了以下说明,则下面的代码应该很容易解释:

  int micPin = A0; int micVal; void setup(){ 
Serial.begin(9600);

pinMode(micPin,INPUT);
}
无效循环(){
micVal = AnalogRead(micPin);
Serial.println(micVal);
}

检测拍手

现在我们可以相当可靠地检测声压,我们需要一种检测拍手的方法。 可以预期,拍手在时域中的范围很小。 因此,它将包含许多频率,尤其是那些比环境中自然存在的频率更高的频率。 这只是傅里叶变换的本质。

如果您不明白我在说什么,请不要担心,因为这不是最终使用的方法。

我试图缓冲传入的音频并对其执行FFT以进行频率分析,但结果完全不可靠。 这可能是由于我正在使用的媒体库,麦克风设置不正确或两者都有的结果。

因此,我们将需要在时域中检测拍手的方法。

基于攻击的方法

让我们看看拍手声和快速喊叫声的波形:

如您所见,两个记录达到的最大音量大致相同。 主要的区别是拍手在天文数字上比喊叫声快得多。 这次录音达到最高点的时间称为Attack

让我们再放大一点,只是强调拍手的攻击速度有多快:

Arduino上基于攻击的检测

现在,我们已经有了一个非常可靠的方法来根据拍手的声音识别拍手声和其他大声噪音,让我们在代码中实现它:

  int micPin = A0; 
int micVal;
整数阈值= 620;
unsigned int nClaps = 0;
unsigned long lastClapEnd;
int clapTimeout = 500;
int clapMax;
无符号长currTime;
无符号的长期开始;
int timeToClapMax;
int clapMaxThreshold = 630;
int maxAttackTime = 10;
void setup(){
Serial.begin(9600);
pinMode(micPin,INPUT);
}
void getClapMaxInfo(){
开始= millis();
clapMax = micVal;
timeToClapMax = 0;
currTime = millis();
while(currTime-start <100){
micVal = AnalogRead(micPin);
如果(micVal> clapMax){
clapMax = micVal;
timeToClapMax = currTime-start;
}
currTime = millis();
}
}
void waitUntilBelowThreshold(){
micVal = AnalogRead(micPin);
while(micVal>阈值){
延迟(10);
micVal = AnalogRead(micPin);
}
}
boolean isClap(){
返回clapMax> clapMaxThreshold && timeToClapMax <= maxAttackTime;
}
无效循环(){
micVal = AnalogRead(micPin);

如果(micVal>阈值){
getClapMaxInfo();

如果(isClap())
++ nClaps;
waitUntilBelowThreshold();
lastClapEnd = millis();
}其他{
如果(nClaps> 0 && millis()-lastClapEnd> = clapTimeout){
Serial.println(nClaps);
nClaps = 0;
}
}
}

让我们分解一下。

loop ,就像上一节中一样,我们从引脚A0读取。 然后我们说,如果此值大于threshold值,则会发出很大的声音。 经过一些实验后,此阈值选择为620(麦克风通常输出1023/2〜512)。

确定响亮的噪声已开始之后,我们既获得了由该噪声产生的最大幅度的值,又获得了达到该值所花费的时间。 这是在getClapMaxInfo完成的。

getClapMaxInfo从麦克风读取100毫秒(任意选择),并在每次读取的值大于当前clapMax时更新clapMaxclapMax

在获得有关攻击的信息之后,我们使用isClap来确定我们刚刚听到的100ms声音是否确实在鼓掌。 为此,我们使用两个阈值:一个用于clapMax ,一个用于timeToClapMax 。 (尽管拍手最大值的值不如达到最大值的时间可靠,但拍手通常会比其他声音高,因此我们对其进行检查)。

如果clapMax大于630并且timeToClapMax小于或等于10ms,那么我们说我们刚刚听到了一个拍子!

然后,我们使用waitUntilBelowThreshold继续读取麦克风,直到该值低于我们的初始阈值620。

我们在此之后将时间戳记记录在变量lastClapEnd因为我们需要某种方法来确定何时发生一个多拍击命令与多个单拍击命令。 我们将超时。

读取拍手并返回到阈值以下后, loop将重新执行。 如果我们继续低于threshold ,我们将输入else子句。

在这里,如果我们已经读取拍手并且超时(500毫秒)已经过去,那么我们将打印计数的拍手数量并重置拍手计数器。

将此代码上传到Arduino,您将看到它运行良好。 您需要快速拍手(彼此之间相隔不到半秒),而且声音很大,但是代码在区分拍手和其他声音方面做得很好。

如果您的设置不起作用,请尝试使用阈值,直到达到极限为止。 您的麦克风可能具有不同的质量,导致您需要较低/较高的幅度阈值或较短/较长的起音阈值。


我们终于可以创建完整的遥控器。 您可以通过将红外传输和麦克风读取部分的电路基本结合起来来实现:

对不起难看的图,但是我对Fritzing应用程序不太满意。

现在,该代码基本上是上一节中描述的拍手检测,但是还有一些额外的行用于命令传输。

为了进行传输,我们实际上将使用IRremote库,以便使代码更易于理解和排序:

  #include  const int numCommands = 5; 
int nClaps2Command [numCommands] = {0x90,0x890,0xa90,0x490,0xc90};
int times2Repeat [numCommands] = {1,1,1,5,5};
IRsend irsend; ...其他全局变量

我们包括IRremote库并初始化一个irsend对象,因此我们将能够执行以前手动进行的传输。

我们还创建了两个数组。 nClaps2Command是从检测到的拍手数量到要发送的命令的映射。 1->调高频道,2->调低频道,3->电源,4->调高音量,5->调低音量。 选择此映射时要牢记最常用的命令。

我花了比我更愿意承认的时间来解密IRremote接受的命令的形式。 但是,一旦将十六进制值放入十六进制至二进制转换器中,它立即变得清晰起来。 0x90 = 0000 10010000。这是16位小端格式(用于频道递增的十进制命令),后跟1位小端格式(电视的五位地址)。

我们还具有times2Repeat来将拍手数映射到执行每个命令的次数。 这样做的唯一目的是,当检测到音量增大或减小时,我们执行五次,因为仅更改一个音量就不得不不断拍四到五次会很烦人。

现在,我们的简单传输函数如下所示:

 无效的传输(诠释命令,诠释重复){ 
对于(char n = 0; n <repeat; n ++){
对于(char i = 0; i <3; i ++){
irsend.sendSony(command,12);
延迟(40);
}
}
}

sendSony的第二个参数在sendSony基本上是SIRC协议的哪个版本(即12位版本)。

现在,我们只需要对loop进行一些更改即可执行检测到的命令:

 无效循环(){ 
...
如果(isClap()){
++ nClaps;
如果(nClaps> numCommands)
nClaps = 0;
}
...
}其他{
如果(nClaps> 0 && millis()-lastClapEnd> = clapTimeout){
...
发送(nClaps2Command [nClaps-1],times2Repeat [nClaps-1]);
}
}
}

当我们听到拍手声时,我们将nClaps复位, nClaps我们已读取了不止numCommands因此我们不会访问数组外部的内存。

当拍手序列超时时,我们调用transmit并简单地通过索引两个数组来传递命令和时间以重复。

上载此代码,将IR二极管对准您的电视,并在麦克风附近拍手,您应该可以通过拍手控制电视!

这是我最终设置的照片:


我希望您了解了有关Arduino的知识,电视遥控器的工作方式以及如何处理音频数据。 我知道我确实做到了。

您还可以通过修改nClaps2Commandtimes2Repeat轻松地从拍手数更改为命令的映射,或添加更多命令。

感谢您的阅读,希望您喜欢它!