Prism

2019年7月15日 星期一

冷氣機紅外線遙控訊號連動 - 04 紅外線發射程式測試

< 本文請勿轉載,謝謝 ! >

※ 2019.8 後補「積木式免焊接 記憶型空調紅外線轉譯器」一文,可供快速製作成品

紅外線發射程式測試


使用 Arduino IDE 內含之 IRremote程式庫 進行測試

PS : IRsendRawDemo 方式不適用冷氣機,本範例僅供參考不需實作,是因這個過程才發現解決的方式。
開啟讀取程式庫中的範例檔 IRsendRawDemo.ino ( 範例檔案位置在「文件」下的Arduino\libraries\IRremote\examples\IRsendRawDemo\IRsendRawDemo.ino ),將前篇 03 中的陣列參數 ( unsigned int rawData[ ... ) 代入貼上

測試結果:
1. 冷氣機接收狀況不穩定,有時有作用,有時沒作用 --> 可能信號多,解錯的機率也變多,這樣實務上不行啊...
2. 前篇 03 中 IRrecvDumpV2 每次取樣到的的資料都不盡相同 --> 取樣會有誤差範圍
3. 另測試DVD遙控器訊號,發現沒什麼問題,但僅在偶數次時才會有作用 --> 一般遙控器信號較少,相對發生解碼錯誤的機率也較少(也有可能容錯率較大);偶數次時才會有作用的問題,應是相同信號要接收二次比對確認,關於這點我查到有的冷氣機也是要求發送二次相同的信號才能有作用。但是從 IRrecvDumpV2 得出的陣列中沒看到有發送二次相同訊號?


紅外線接收程式修改

經過好幾天的失敗、嘗試與查證後,還好找到了解決方法:
1. 關於因取樣數量較多產生誤差機率變多,而使接收變得不穩定的問題,從冷氣機紅外線編碼的時序圖來看

需要找出下列參數:
1. HDR_MARK          38kHz 載波的持續時間 (PS : 上圖黃色部份皆為載波的持續時間
2. HDR_SPACE        無訊號時間,1+2 構成了起始訊號,通知冷氣機有訊號要開始傳送
3. BIT_MARK            通知有資料訊號要傳送
4. ZERO_SPACE      資料位元 0 的無訊號時間, 3+4 構成了位元 0 的資料訊號
5. ONE_SPACE        資料位元 1 的無訊號時間, 3+5 構成了位元 1 的資料訊號
6. RPT_MARK          通知有資料訊號要傳送
7. RPT_SPACE        長間隔無訊號時間,6+7 後準備發送第二次相同資料

上述 7個參數的時間長度是固定的,但Arduino取樣時因有誤差而不相同,分析前篇 03 中 IRrecvDumpV2 取得的紅外線陣列參數,可判讀出上列參數:
1. HDR_MARK          陣列的第 1 個數值
2. HDR_SPACE        陣列的第 2 個數值
3. BIT_MARK            統計從第 3 個開始的奇數位置,找出出現最多次的數值
4. ZERO_SPACE      統計從第 4 個開始的偶數位置,找出出現最多次的短間隔
5. ONE_SPACE        統計從第 4 個開始的偶數位置,找出出現最多次的長間隔
6. RPT_MARK          找尋 1+2 是否有重覆出現,有的話其出現位置的 -2位置為 RPT_MARK
7. RPT_SPACE        找尋 1+2 是否有重覆出現,有的話其出現位置的 -1位置為 RPT_SPACE

然後將 3+4 , 3+5 的 0、1 信號編碼為 Byte (8位元),陣列的長度約減少16倍,且資料型態由 unsigned int (16位元) 變為 Byte (8位元),整個資料空間約共減少了 32 倍,日後若有需要放多組訊號抽樣資料的話也不用擔心記憶體不夠用的問題。

而之前提到沒看到DVD遙控器訊號送出二次相同信號,猜想可能因 IRremote 程式庫的預設取樣停止判斷時間太短,而未去收第二組訊號,找到該參數為 _GAP ,位於 IRremoteInt.h (同修改 RAWBUF 參數的檔案),修改如下:
// Minimum gap between IR transmissions
#define _GAP            30000 //預設 5000
#define GAP_TICKS       (_GAP/USECPERTICK)

根據以上方法,修改前篇 03中 IRrecvDumpV2 檔案(同樣要修改第9行 int recvPin = 2; ),將 163 行以後取代如下:
//+=============================================================================
// 取得原生訊號參數及重新編碼 (By MGP. 2019.7.13  https://mygraphpaper.blogspot.com/)
// 需修改 IRremoteInt.h
// Line 43 #define RAWBUF  400    // 預設儲存信號長度為 101 個 , 但冷氣機編碼超過, 需加大供測試
// Line 52 int rawlen; //上的大小超過 Byte 255計數, 改為 int 
// Line 98 #define _GAP    30000  // 預設信號結束的空白時間 5000, 但太小導致取樣不到 RAW_RPT_SPACE 的間隔參數, 需加大供測試
// 以上參數, 僅本程式測試時修改用, 不執行本取樣程式請改回預設值
//
unsigned int Probability[3],Temporary[3],CheckCount[3];   //取樣陣列

//FindMaxCount(原始編碼訊號, 處理編碼訊號陣列起始位置, 是否需比對 MARK 值, 判斷 Space 是短的判斷值)
unsigned int FindMaxCount(decode_results *results, int StartPos, unsigned int Check_RAW_BIT_MARK, unsigned int MAX_RAW_ZERO_SPACE) {
  unsigned int Return_MAX_Value = 0;  //回傳值
  
  for (unsigned int i=0 ; i < (sizeof(Probability) / sizeof(Probability[0])) ; i++) {
    Probability[i]=0; //初始陣列
    Temporary[i]=0;
  }
  
  for (int i = StartPos; i < results->rawlen; i+=2) {
    if ((Check_RAW_BIT_MARK == 0) || (results->rawbuf[i-1] == Check_RAW_BIT_MARK)){ //是否需比對 MARK 值
      if ((MAX_RAW_ZERO_SPACE == 0) || (results->rawbuf[i] <= MAX_RAW_ZERO_SPACE)){ //判斷 Space 短的判斷值     
        if (results->rawbuf[i] > Probability[0]){
          Probability[2] = Probability[1];      //暫存 RAW 值, 並排列大小
          Probability[1] = Probability[0];      
          Probability[0] = results->rawbuf[i];  //該 RAW 值出現次數
          Temporary[2] = Temporary[1];
          Temporary[1] = Temporary[0];
          Temporary[0] = 0;        
        } else {
          if (results->rawbuf[i] != Probability[0]) {
            if (results->rawbuf[i] > Probability[1]) {
              Probability[2] = Probability[1];
              Probability[1] = results->rawbuf[i];
              Temporary[2] = Temporary[1];
              Temporary[1] = 0;    
            } else {
              if (results->rawbuf[i] != Probability[1]) {
                if (results->rawbuf[i] > Probability[2]) {
                  Probability[2] = results->rawbuf[i];
                  Temporary[2] = 0;    
                }
              }
            }
          }
        }
      }
    }
    if (results->rawbuf[i] == Probability[0]) Temporary[0]++;         //RAW 值出現次數
    else if (results->rawbuf[i] == Probability[1]) Temporary[1]++;
         else if(results->rawbuf[i] == Probability[2]) Temporary[2]++;  
  }

  for (unsigned int i=0 ; i < (sizeof(Probability) / sizeof(Probability[0])) ; i++) {
    CheckCount[i] = Temporary[i];
    if (CheckCount[i] < 3) CheckCount[i]=0;     //RAW 值出現次數太小,忽略取樣
  }
  
  if ((Probability[1] - Probability[2]) != 1) CheckCount[2] = 0;    //取樣值相差太多,忽略取樣
  if ((Probability[0] - Probability[1]) != 1) {
    CheckCount[2] = 0;
    CheckCount[1] = 0;
  }

  if ( (CheckCount[0]+CheckCount[1]+CheckCount[2]) >= 3 ) {     //取得相近取樣值中, 出現最多次的回傳
    if((CheckCount[2] > CheckCount[1]) && (CheckCount[2] > CheckCount[0]) ) Return_MAX_Value = Probability[2];
    else if ((CheckCount[1] > CheckCount[0])) Return_MAX_Value = Probability[1];
         else Return_MAX_Value = Probability[0];  
  }
  
  return Return_MAX_Value;
}


void EncodeRAWCode (decode_results *results)
{
  unsigned int RAW_HDR_MARK    = 0;   //初始信號 RAW_HDR_MARK + RAW_HDR_SPACE
  unsigned int RAW_HDR_SPACE   = 0;
  unsigned int RAW_BIT_MARK    = 0;   //0 與 1 的信號組合,
  unsigned int RAW_ONE_SPACE   = 0;   //1 = RAW_BIT_MARK + RAW_ONE_SPACE
  unsigned int RAW_ZERO_SPACE  = 0;   //0 = RAW_BIT_MARK + RAW_ZERO_SPACE
  unsigned int RAW_RPT_MARK    = 0;   //冷氣機訊號通常會發射2次相同編碼以做比對, RAW_RPT_MARK + RAW_RPT_SPACE 為間隔訊號
  unsigned int RAW_RPT_SPACE   = 0;


  RAW_HDR_MARK = results->rawbuf[1];  //第一個訊號[0]無效,[1]為RAW_HDR_MARK
  RAW_HDR_SPACE = results->rawbuf[2]; //[2]RAW_HDR_SPACE
  Serial.println("");
  Serial.println(F("// My Data Start , Mark & Ctrl-C Copy ---------------")); 
  Serial.println(""); 
  Serial.print(F("#define RAW_HDR_MARK    "));
  Serial.print(RAW_HDR_MARK * USECPERTICK, DEC);  //次數 X 間隔(50us)
  Serial.println("");  
  Serial.print(F("#define RAW_HDR_SPACE   "));
  Serial.print(RAW_HDR_SPACE * USECPERTICK, DEC);  


  //取得重覆訊號的位置
  int Tolerance_scope = 4;    //取樣會有誤差, 若有需要可嘗試修改
  for (int i = 3; i < results->rawlen; i+=2) {    //找出 RAW_HDR_MARK+RAW_HDR_SPACE 重覆出現的位置
    if ((results->rawbuf[i]>=RAW_HDR_MARK-Tolerance_scope) && (results->rawbuf[i]<=RAW_HDR_MARK+Tolerance_scope)) {
      if ((results->rawbuf[i+1]>=RAW_HDR_SPACE-Tolerance_scope) && (results->rawbuf[i+1]<=RAW_HDR_SPACE+Tolerance_scope)){
        //檢查 重覆訊號的位置 是否正確
        if(i==(((results->rawlen-4)/2)+3)) {  //總長度-1頭-1尾-2RAW_RPT, 取半為一次傳送長度, 再+2RAW_RPT之後的位置
          RAW_RPT_MARK = results->rawbuf[i-2];      //該位置的前2個為 RAW_RPT_MARK
          RAW_RPT_SPACE = results->rawbuf[i-1];     //該位置的前1個為 RAW_RPT_SPACE
          results->rawlen = i-2;    //只保留第一次完整編碼的長度, 第二次重覆部份不使用
        } 
      }
    }
  }


  //取得找 BIT_MARK , 最大值範圍中最多出現次數 , 3開始的奇數位置為 RAW_BIT_MARK 值
  RAW_BIT_MARK = FindMaxCount(results , 3, 0, 0);
  Serial.println("");   
  Serial.print(F("#define RAW_BIT_MARK    "));
  Serial.print(RAW_BIT_MARK * USECPERTICK, DEC);
  Serial.print("     // ");
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[0] , DEC);
  Serial.print(" times.  "); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[1] , DEC);
  Serial.print(" times.  ");
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[2] , DEC);
  Serial.print(" times.");    

 
  //取得找 ONE_SPACE , 最大值範圍中最多出現次數, 4開始的偶數位置為 RAW_ONE_SPACE 值
  RAW_ONE_SPACE = FindMaxCount(results , 4, RAW_BIT_MARK, 0); 
  Serial.println(""); 
  Serial.print(F("#define RAW_ONE_SPACE   "));
  Serial.print(RAW_ONE_SPACE * USECPERTICK, DEC);
  Serial.print("     // ");
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[0] , DEC);
  Serial.print(" times. "); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[1] , DEC);
  Serial.print(" times. ");
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[2] , DEC);
  Serial.print(" times.");  


  //取得找 ZERO_SPACE , 最大值範圍中最多出現次數, 4開始的偶數位置為 RAW_ONE_SPACE 值
  unsigned int MAX_RAW_ZERO_SPACE = RAW_ONE_SPACE/2 + 2;    //自訂ZERO_SPACE最大值的上限, 若有需要可嘗試修改
  RAW_ZERO_SPACE = FindMaxCount(results , 4, RAW_BIT_MARK, MAX_RAW_ZERO_SPACE);  
  Serial.println(""); 
  Serial.print(F("#define RAW_ZERO_SPACE  "));
  Serial.print(RAW_ZERO_SPACE * USECPERTICK, DEC);
  Serial.print("     // ");
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[0] , DEC);
  Serial.print(" times. "); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[1] , DEC);
  Serial.print(" times. ");
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(" -> ");
  Serial.print(Temporary[2] , DEC);
  Serial.print(" times.");  
  Serial.println("");

  Serial.print(F("#define RAW_RPT_MARK    "));
  Serial.print(RAW_RPT_MARK * USECPERTICK, DEC);
  Serial.println(F("      //Repeat Gap"));

  Serial.print(F("#define RAW_RPT_SPACE   "));
  Serial.print(RAW_RPT_SPACE * USECPERTICK, DEC);


  //將 Mark+Space 的 0 1 編碼為 Byte 型態存放
  byte rawData[(results->rawlen - 2)/8+1] ; //計算陣列長度, 前三個非資料扣掉
  byte rawTemp = 0; //暫存值
  unsigned int Bits_Count = 0;  //目前 Bit 計數
  unsigned int rawData_Count = 0;   //目前陣列位置計數
  for (int i = 4; i < results->rawlen; i+=2) {
    Bits_Count++;
    rawTemp = rawTemp << 1;
    if (results->rawbuf[i] > MAX_RAW_ZERO_SPACE) rawTemp = rawTemp | 1;
    if (Bits_Count==8) {    //完成 1 Byte 存放入rawData
      rawData[rawData_Count] = rawTemp;
      Bits_Count = 0;
      rawData_Count ++;
      rawTemp = 0;
    }
  }

  if (Bits_Count != 0) {    //不足 1Byte 的補完
      for (unsigned int i = (Bits_Count+1); i <= 8; i++) {
        rawTemp = rawTemp << 1;
      }
      rawData[rawData_Count] = rawTemp;
      rawData_Count ++;
  }
  
  Serial.println("");
  Serial.println("");  
  Serial.print(F("int rawData_Bits = ")); 
  Serial.print((results->rawlen-3)/2, DEC); 
  Serial.print(" ;");   
  Serial.println("");   
  Serial.print("byte rawData[");
  Serial.print(rawData_Count, DEC);
  Serial.print("] = { ");

  for (unsigned int i = 0;  i < rawData_Count;  i++) {
    Serial.print("0x");
    Serial.print(rawData[i], HEX);
    if ( i < rawData_Count-1 ) Serial.print(", ");
  }
  Serial.print(" };");
  Serial.println("");
  Serial.println("");
  Serial.println(F("// My Data End --------------- By MGP."));  
  Serial.println("");
  Serial.println("");  
}
//=============================================================================


//+=============================================================================
// The repeating section of the code
//
void  loop ( )
{
  decode_results  results;        // Somewhere to store the results

  if (irrecv.decode(&results)) {  // Grab an IR code
    dumpInfo(&results);           // Output the results
    dumpRaw(&results);            // Output the results in RAW format
    dumpCode(&results);           // Output the results as source code
    EncodeRAWCode(&results);      // 取得冷氣編碼 (By MGP.)
    Serial.println("");           // Blank line between entries
    irrecv.resume();              // Prepare for the next value
  }
}

編譯上傳後,取樣冷氣機紅外線遙控器信號如下:(同一按鍵信號可多按幾次,找多次出現的參數增加準確率)

因收到紅外線編碼資料二次,RAWBUF 400 個也不夠用,只取第一次資料做分析判斷。
紅外線發射所需資料為虛線之間的程式碼
------------------------------------------------
#define RAW_HDR_MARK    3400
#define RAW_HDR_SPACE   1700
#define RAW_BIT_MARK    400     // 450 -> 29 times.  400 -> 93 times.  350 -> 22 times.
#define RAW_ONE_SPACE   1300     // 1350 -> 7 times. 1300 -> 19 times. 500 -> 30 times.
#define RAW_ZERO_SPACE  450     // 500 -> 30 times. 450 -> 74 times. 400 -> 6 times.
#define RAW_RPT_MARK    400      //Repeat Gap
#define RAW_RPT_SPACE   17000

int rawData_Bits = 144 ;
byte rawData[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x0, 0x18, 0xB0, 0x6C, 0x2, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7C };
------------------------------------------------


紅外線發射程式測試

讀取本頁開頭的 IRsendRawDemo.ino 範例,要增加解碼發射程式,整個檔案修改如下,另虛線之間的程式碼請依自家測得的冷氣機參數代入

#include <IRremote.h>

IRsend irsend;

// My Data Start , Mark & Ctrl-C Copy ---------------

#define RAW_HDR_MARK    3400
#define RAW_HDR_SPACE   1700
#define RAW_BIT_MARK    400     // 450 -> 29 times.  400 -> 93 times.  350 -> 22 times.
#define RAW_ONE_SPACE   1300     // 1350 -> 7 times. 1300 -> 19 times. 500 -> 30 times.
#define RAW_ZERO_SPACE  450     // 500 -> 30 times. 450 -> 74 times. 400 -> 6 times.
#define RAW_RPT_MARK    400      //Repeat Gap
#define RAW_RPT_SPACE   17000

int rawData_Bits = 144 ;
byte rawData[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x0, 0x18, 0xB0, 0x6C, 0x2, 0x71, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x7C };

// My Data End --------------- By MGP.


//發送上方編碼陣列,由左至右, 資料共計 rawData_Bits = 144 Bits
//void RAWIRSend(發送陣列資料, 陣列大小, 發射位元數, 發射頻率)
void RAWIRSend(byte SendRAWData[], unsigned int SendRAWData_Len, unsigned int SendRAWData_Bits, unsigned int hz)
{
  int Rep_Temp = 0; 
  unsigned int rawData_Bits_Count =0;
  byte Mask = 1; 

  if (RAW_RPT_SPACE > 0) Rep_Temp = 2;
  else Rep_Temp = 1;
    
  irsend.enableIROut(hz);  // 38khz
  irsend.space(max(RAW_HDR_MARK,RAW_HDR_SPACE));  //Zero Single
  
  for (int j = 0; j < Rep_Temp; j++) {  // Repeat
    //Start Single
    irsend.mark(RAW_HDR_MARK);
    irsend.space(RAW_HDR_SPACE);
    for (unsigned int i = 0; i < SendRAWData_Len; i++) {
      for (Mask = B10000000; Mask > 0; Mask >>= 1) {
        if (rawData_Bits_Count < SendRAWData_Bits) {
          if (SendRAWData[i] & Mask) {
            //Bit 1
            irsend.mark(RAW_BIT_MARK);
            irsend.space(RAW_ONE_SPACE);
          }
          else {
            //Bit 0
            irsend.mark(RAW_BIT_MARK);
            irsend.space(RAW_ZERO_SPACE);
          }
          rawData_Bits_Count++;
        }
      }
    }
    if (Rep_Temp > 1) {
      //Send Repeat Single
      irsend.mark(RAW_RPT_MARK);
      irsend.space(RAW_RPT_SPACE);      
    }
    irsend.space(max(RAW_HDR_MARK,RAW_HDR_SPACE));  //Stop Single
  }  
}


void setup()
{

}

void loop() {
  int khz = 38; // 38kHz carrier frequency
  
  //冷氣機 打開
  RAWIRSend(rawData, sizeof(rawData) / sizeof(rawData[0]), rawData_Bits, khz);

  //延遲 10 分鐘
  delay(600000); 
}


測試結果成功且穩定!!

測試時請注意,紅外線發射LED的正上方有聚光的作用,請一定要將上方圓頂對準冷氣機紅外線接收器的位置,另按下 Arduino 開發板上的 Reset 鍵,可重覆發射紅外線測試訊號。

接下來將麵包板測試電路做到電路板上...


冷氣機紅外線遙控訊號連動 - 05 紅外線接收、發射電路板製作

Xiaomi Mijia universal infrared ray remote control for Unsupported air conditioner.