こんにちは、ENGかぴです。
Seeeduino XIAOと赤外線受信機モジュールのGrove-Infrared Receiverを接続するとリモコンと通信ができます。通信フォーマットを解析してデータを取得する方法とIRremoteライブラリを使用する方法をまとめました。
リモコンの操作でRGB LEDの点灯パターンを切り替えて動作確認を行います。RGB LEDはマイコン内蔵RGB LED 5mm PL9823-F5(秋月電子で購入)を使用しています。
リモコンはArduinoスターターキットに付属していたCar mp3リモコン(メーカ不明)を使用しています。Seeeduino XIAO用拡張ボード(Seeed Studio)及びGrove-Infrared Receiver(Seeed Studio)を使用しています。
Seeeduino XIAOを使って動作確認を行ったことを下記リンクにまとめています。
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
Grove-Infrared Receiverからデータを取得する
Grove-Infrared ReceiverはリモコンからのIR信号を検出します。IR検出器は38KHz変調されたIR信号を復調器が内蔵されており、10メートル以内の信号が受信できます。詳細は下記リンクで確認できます。
Grove – Infrared Receiver | Seeed Studio Wiki
受信したIR信号を復調したデジタル信号をDIに取り込むことでデータを取得することができます。赤外線リモコンの通信フォーマットの確認方法やArduinoのDIでリモコンのデータを受信する方法について下記記事にまとめています。
Seeeduino XIAOでも同様の方法でリモコンのデータを取得することができます。NECフォーマットのリモコンのデータ取得について説明します。

NECフォーマットの構成はリーダーコード、カスタムコード、データコード、ストップビットの順にLSBファーストで送信されます。通信フォーマットは最短でも108msの間隔で送信されます。108ms以上リモコンのボタンを押し続けているとリピートコードが送信されます。リーダーコードを解析することで通信フォーマットを区別することができます。
リモコンの出力が赤外線受信機モジュールの内部でデジタル信号に変換されますが、受信機モジュールの出力はプルアップされているためリモコンの通信フォーマットに対してLOW(0)とHIGH(1)が反転するので注意が必要です。通信フォーマットからデータを取得する手順を説明します。以下ではLOWを0、HIGHを1とします。
- 手順1通信フォーマットの確認
リーダーコードの長さから通信フォーマットを判断する。
- 手順2データコードの取得
リーダーコード以降はデータコードとなるので通信フォーマットに応じたビット分のデータビットを1バイトデータで取得する。データビットの0/1は1ビットあたりの1の長さで判断する。
- 手順3データの確認
手順2で取得したデータコードから1バイトのデータと反転データを生成し、2つのデータの論理和(0xFF)または論理積(0x00)でデータの健全性を確認する。
下記の説明で使用しているT(変調単位)はNECフォーマットでは562usになります。図では()内にms換算して表記しています。
手順1では通信フォーマットの確認を行います。リーダーコードの長さから通信フォーマットを判定します。

NECフォーマットを受信機から見た場合、0の区間が16T(9ms)間続き、1の区間が8T(4.5ms)間続きます。リーダーコードで通信フォーマットを解析することができます。
手順2ではデータコードのDIを確認して0の区間と1の区間の長さから各データビットの判定を行います。データビットの判断はデータビットの1と0を判定するで説明しています。

データコードはカスタム番号、データ値、データ反転値で構成されています。カスタム番号はメーカ識別番号で16bit構成されており、データは8ビット構成、チェック用の反転データが8ビット構成になっています。
受信機から見た場合、1の区間が1T(0.5ms)の場合はデータビットが0と判断できます。1の区間が3T(1.69ms)の場合はデータビットが1と判断できます。この判定を繰り返し行いデータコードとストップビットを取得します。
手順3では手順2で取得したデータの配列から1バイトのデータ値と反転データを生成し、論理和または論理積を計算してデータの健全性を確認します。データが正常であればリモコンのデータとして処理します。
PR:わからないを放置せず、あなたにあった最低限のスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
データビットの1と0を判定する

受信機から見た場合データビットは1の区間の長さで決まります。100us毎にDIを確認してカウント値を更新しながら0の区間と1の区間の長さを確認します。0の区間はデータビットの判定に使用しませんが、カウント値は5以下(3~5)の値になります。
データビットが0の場合は1の区間が1Tになるので0の区間と同様の値となります。一方データビットが1の場合は1の区間が3Tになるのでカウント値が16以下(14~16)になります。
以下にデータビットの判定の例を示します。判定の結果を1バイトデータの配列に格納します。
uint8_t *dat;
dat = &remote.cus[0]; //1バイトの配列にデータビットを格納
for(uint8_t i=0; i < 33; i++){
lcnt = 0; //LOWの部分のカウントをクリア
while( digitalRead(REMOTE_DI ) == LOW && errflg == false){
++lcnt;
delayMicroseconds(100); //100usウェイト
}
hcnt = 0; //HIGHの部分のカウントをクリア
while( digitalRead(REMOTE_DI ) == HIGH && errflg == false){
++hcnt;
if(hcnt >= 14 && i == 32){
break; //ストップビットは1なので14以上であれば処理を停止
}
delayMicroseconds(100); //100usウェイト
}
if(hcnt >= 10){ //10(2T)以上であれば1と判断する
*dat = 1;
}
else{
*dat = 0;
}
++dat;
}
digitalRead()関数でDIを確認します。最初にwhile()で0の区間のカウント値(lcnt)を確認します。delayMicroseconds()関数で100usのウェイトを置くことで更新頻度の調整を行っています。DIが1の区間に切り替わるとループを抜けます。
次にwhile()で1の区間のカウント値(hcnt)を確認します。delayMicroseconds()関数で100usのウェイトを置くことで更新頻度の調整を行っています。DIが0の区間に切り替わるとループを抜けますが、ストップビットの受信後DIが1に固定されるためストップビットの場合のみカウント値が14以上でループを抜けるようにしています。
データビットの0か1かは1の区間のカウント値で判定します。データビットが0となる場合のカウント値の目安は6以下であり、データビットが1となるカウント値の目安は16以下であることから6よりも大きければデータビットが1となると判断できます。
判定の基準に余裕をみて2T分の経過カウント数の目安である10以上であれば該当のビットが1であると判断して1を格納します。10未満であればビットが0であると判断して0を格納します。
データビットの判定までの1セットとしてカスタムからストップビットまでの33ビット分を繰り返します。
PR:スキマ時間で自己啓発!スマホで学べる人気のオンライン資格講座【スタディング】まずは気になる講座を無料で体験しよう!
リモコンデータの生成とチェック
uint8_t i;
uint16_t cus_l=0;
uint16_t cus_h=0;
uint16_t cus =0;
uint8_t data=0;
uint8_t idata=0;
uint8_t chk;
for(i=0; i < 8; i++){ //16ビットのうち下位8ビット
cus_l |= ( remote.cus[i] << i); //カスタムコード下位
}
for(i=0; i < 8; i++){ //16ビットのうち上位8ビット
cus_h |= ( remote.cus[i + 8] << i); //カスタムコード上位
}
cus = (cus_h << 8) | cus_l; //16ビットデータを生成
for(i=0; i < 8; i++){
data |= ( remote.data[i] << i); //データ
idata |= ( remote.Idata[i] << i); //反転データ
}
if( (data | idata) == 0xFF){
btnval = data; //データを格納
}
1バイトデータの配列にデータビットを順番に格納しているためデータの配列からシフト演算を使ってデータビットを1バイトのリモコンのデータを生成します。データコードはLSBファーストで送信されているため配列の1番目から順に左にシフトしながら論理和を計算することで1バイトデータを生成することができます。
データと反転データの生成を例にすると、LSBファースト(最下位ビットから送信)するので配列のデータを左に0回シフト(×1と同じ)して論理和を計算します。続けて2ビット目は更新した配列のデータを左に1回シフト(×2と同じ)して論理和を計算します。同様にして3ビット目~MSBまで左にシフトしながら論理和を計算すると1バイトデータ(8ビットのデータ)が生成できます。
カスタムコードについても同様の方法で生成できますが、16ビットの構成なので下位の8ビットと上位の8ビットが入れ替わっているため注意が必要です。例えばカスタムコードが0xFF00の場合は、LSBファーストで送っているので2バイトデータの場合は上位ビットが0x00で下位ビットが0xFFになります。
データと反転データでデータの健全性を確認します。反転データを反転(データを反転)して2つのデータを比較する方法、2つのデータの論理和を計算して0xFFを確認する方法、2つのデータの論理積を計算して0x00を確認する方法があります。これらの方法でデータが健全であると判断できればリモコンのデータとして採用します。
OLEDにRGB LEDの状態を表示する
Seeeduino XIAO用拡張ボードのOLEDにRGB LEDの値を表示します。OLEDの表示はU8g2ライブラリを使用しています。U8g2ライブラリの追加方法については下記記事を参考にしてください。
Seeeduino XIAOの拡張ボードのOLEDを使用する
本記事ではOLED表示に使用した関数について説明します。
char chr1[17];
sprintf(&chr1[0]," R:%3d",red);//文字列を生成
u8x8.drawString(0,2, &chr1[0]); //文字列の書き込み
sprintf()関数は文字列を生成する標準関数です。第1引数に生成後の文字列を格納するアドレスを指定します。第2引数に生成する文字列のフォーマットを指定します。%dなどの変換指定子を含めることでカウント値など任意の数値を文字列に変換することができます。第3引数以降は第2引数の変換指定子に対応する変数を指定します。例では変数redを指定しています。
drawString()関数は行と列および文字列を指定して書き込みを行う関数です。第1引数に行番号を指定します。行はセル群(5×8等のフォント)において左端の位置から右に向かって0~昇順になります。第2引数に列番号を指定します。列はセル群(5×8等のフォント)において上端から下に向かって0~昇順になります。
例では16文字の生成に対してNULL文字を考慮して17バイトの領域を確保しています。生成した文字列が確保した領域を超えてしまうと領域オーバーした部分の値が書き換わってしまうため予期しない動作不良の原因になる場合があります。
IRremoteライブラリを追加して使用する(参考)

Arduino IDEでIRremoteライブラリを追加する方法でリモコンのデータを取得する方法を説明します。Arduino IDEのライブラリマネージャの検索欄にirremoteを入力するとライブラリの候補が表示されます。候補の中からIRremote by shirriff,z3t0,ArminJoをインストールします。使用例は以下の通りです。
#include <Adafruit_NeoPixel.h>
#include <U8x8lib.h>
#include <IRremote.hpp>
#define REMOTE_DI 0
#define TIME_UP 0
#define TIME_OFF -1
#define BASE_CNT 10 //10msがベースタイマとなる
#define TIME_WAIT 9
#define TIME_OLED 30
#define PIXEL_PIN 10
#define NUMPIXELS 1 // Popular NeoPixel ring size
typedef enum{
LED_R = 0,
LED_G,
LED_B,
LED_MAX
}LED_MODE_NO;
uint8_t re_md;
int16_t timoled;
uint8_t btnval;
uint8_t red;
uint8_t green;
uint8_t blue;
uint8_t colormd;
uint32_t beforetimCnt = millis();
bool oledflg;
Adafruit_NeoPixel pixels(NUMPIXELS, PIXEL_PIN, NEO_RGB + NEO_KHZ400);
U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8( SCL, SDA, U8X8_PIN_NONE);
/* プロトタイプ宣言 */
void mainTimer(void);
void mainApp(void);
void BtnCnt(uint8_t md, bool plus);
void OledSet(void);
void setup() {
Serial.begin(115200);
red = 0;
green = 0;
blue = 0;
pixels.begin();
pixels.clear();
delay(100);
pixels.show();
u8x8.begin();
u8x8.setFlipMode(1); // set number from 1 to 3
u8x8.setFont(u8x8_font_5x8_r);
u8x8.setCursor(0, 0);
u8x8.print("Remote Control");
u8x8.setCursor(0, 2);
u8x8.print(" ->R: 0 ");
u8x8.setCursor(0, 4);
u8x8.print(" G: 0 ");
u8x8.setCursor(0, 6);
u8x8.print(" B: 0 ");
IrReceiver.begin(REMOTE_DI);
}
void loop() {
mainTimer();
mainApp();
OledSet();
}
/* タイマ管理 */
void mainTimer(void){
if ( millis() - beforetimCnt >= BASE_CNT ){
beforetimCnt = millis();
if( timoled > TIME_UP ){
--timoled;
}
}
}
/* リモコン受信処理 */
void mainApp(void){
if( IrReceiver.decode()){ //受信したか
IrReceiver.printIRResultShort(&Serial); //通信フォーマットなどデータを表示
IrReceiver.printIRSendUsage(&Serial); //リモコンのアドレスやボタンによる指令値の表示
btnval = IrReceiver.decodedIRData.command;
oledflg = true;
switch(btnval){
case 7: //-
BtnCnt(colormd,false);
break;
case 21: //+
BtnCnt(colormd,true);
break;
case 22: //0
red = 0;
green = 0;
blue = 0;
break;
case 12: //1
colormd = LED_R;
break;
case 24: //2
colormd = LED_G;
break;
case 94: //3
colormd = LED_B;
break;
case 8: //4
red = 255;
green = 255;
blue = 0;
break;
case 28: //5
red = 255;
green = 0;
blue = 255;
break;
case 90: //6
red = 0;
green = 255;
blue = 255;
break;
case 66: //7
red = 255;
green = 255;
blue = 255;
break;
case 82: //8
red /= 2;
green /= 2;
blue /= 2;
break;
case 74: //9
red *= 2;
green *= 2;
blue *= 2;
break;
}
pixels.setPixelColor(0,red,green,blue);
pixels.show();
IrReceiver.resume(); //次の受信を許可
}
}
/* ボタンによる値の更新 */
void BtnCnt(uint8_t md, bool plus){
switch(md){
case LED_R:
if(plus){
++red;
}
else{
--red;
}
break;
case LED_G:
if(plus){
++green;
}
else{
--green;
}
break;
case LED_B:
if(plus){
++blue;
}
else{
--blue;
}
break;
}
}
/* OLED表示 */
void OledSet(void){
char chr1[17];
if( timoled == TIME_UP ){
timoled = TIME_OLED;
if( oledflg ){
oledflg = false;
if( colormd == LED_R ){
sprintf(&chr1[0]," ->R:%3d",red);
}
else{
sprintf(&chr1[0]," R:%3d",red);
}
u8x8.drawString(0,2, &chr1[0]);
if( colormd == LED_G ){
sprintf(&chr1[0]," ->G:%3d",green);
}
else{
sprintf(&chr1[0]," G:%3d",green);
}
u8x8.drawString(0,4, &chr1[0]);
if( colormd == LED_B ){
sprintf(&chr1[0]," ->B:%3d",blue);
}
else{
sprintf(&chr1[0]," B:%3d",blue);
}
u8x8.drawString(0,6, &chr1[0]);
}
}
}
IRremote.hppをインクルードします。IrRecvクラスでインスタンス化されているIrReciver変数を使って処理を行います。setup()関数内でメンバー関数のbegin()関数で受信機からのデータを受け取るDIを指定します。
Loop()関数内で受信したときの処理を追加します。decode()関数はデータの受信を確認します。受信があれば戻り値にtrueを返します。
printIRResultShort()関数は通信フォーマットデータの情報やデータの表示をまとめた文字列を生成します。引数にシリアル通信のハードウェアのアドレスを指定します。Serialのアドレスを指定することで文字列がシリアルモニターで表示されます。
printIRsendUsage()関数はリモコンのアドレスやデータの表示をまとめた文字列を生成します。引数にシリアル通信のハードウェアのアドレスを指定します。Serialのアドレスを指定すると文字列がシリアルモニターに表示されます。
decodedIRData.commandはリモコンのデータが格納されています。リモコンのボタン毎にデータ値が異なるためシリアルモニターで確認しながら処理のパターンを追加します。
resume()関数はリモコンからの次の受信を許可します。
PR:わからないを放置せず、あなたにあったスキルを身に着けるコツを教える テックジム 「書けるが先で、理解が後」を体験しよう!
動作確認

電源を入れるとリモコンからの受信を待ちます。リモコンからデータを受信するとデータを生成し、ボタンの「+」「-」及び「0」~「9」で処理を分岐します。
「0」を押すとLED1を消灯します。「1」~「3」はRGB LEDの変更する色の選択です。「1」は赤色、「2」は緑色、「3」は青色の選択です。「1」~「3」で色を選択すると選択している色の前に->を表示します。各色の値の範囲は0~255段階で「+」を押すと値を+1します。「-」を押すと値を-1します。
「4」を押すと黄色点灯、「5」を押すと紫色点灯、「6」を押すと水色点灯、「7」を押すと白色点灯します。「8」は現在の値を÷2し、「9」は現在の値を×2倍します。
「+」、「-」を長押しするとリピートコードによって値が更新されます。OLEDの更新頻度はリモコンの受信処理に影響を与えないようにタイミングを調整しているため少し遅れて表示します。

リモコンの「4」を押すと黄色点灯、「6」を押すと水色点灯することが確認できました。他のボタンについても点灯することが確認できています。「+」を押すと選択している色の値が増加し、「-」を押すと値が減少することも確認できています。

シリアルモニターには各ボタンを押した時のカスタムコード(cus:)、データ(data:)、反転データ(Idata:)を表示しています。シリアルモニターでリモコンのボタンを押した時のデータを確認することができました。
ソースコード全体
ソースコードは記事作成時点において動作確認できていますが、使用しているライブラリの更新により動作が保証できなくなる可能性があります。また、ソースコードを使用したことによって生じた不利益などの一切の責任を負いかねます。参考資料としてお使いください。
リンクからZIPファイル形式のファイルをダウンロードし、任意の場所に展開していただくとテキストファイルが生成されます。
使用するリモコンによって通信フォーマットや指令値が変わるためシリアルモニターであらかじめ指令値を確認して処理を分岐するように調整する必要があります。
関連リンク
Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。
Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方
Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方
ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方
PR:(即戦力のスキルを身に着ける:DMM WEBCAMP 学習コース(はじめてのプログラミングコース))
最後まで、読んでいただきありがとうございました。
リモコンのボタンの数だけDIが拡張でき応用範囲が広がりますが、OLEDの表示を頻繁に行ったりするとリモコンの受信処理が遅延し受信できなくなることがあるので注意が必要です。