JavaScriptを有効にすると、ここにメニューを表示します。
Arduino UNOでFM77AV/AV20/AV40に赤外線キーボード信号を送る

ysflight.com

2019/07/23

Arduinoと赤外線LEDを使ってFM77AVに赤外線キーボード信号を送ることに成功した。

同じようなことは既にIRToy v2を使って成功していたが、IRToy v2にはひとつの大きな問題があった。いや、日本で売ってないという問題は最近だと割となんとかなりそうだから別として、もっと困るのが起動直後サンプラーモードに入る前に誤って'$' (0x49)を送信するとIRToy v2が自殺してしまう。いったん自殺したIRToy v2を蘇生させるにはWindows XPが必要。Windows 7だと50%程度の成功率らしく、Windows 10では確実に失敗するらしい。失敗しても死体が灰になったり消滅してしまったりはしないがわざわざアメリカから購入したIRToyが簡単に使用不能になってしまうのはちょっと困る。

自作FM77AVキーボードエミュレータではモードにかかわらず送信コードに'$'が含まれていたらブロックするように作ったから、FM77AVキーボードエミュレータが接続している間は大丈夫のはず。しかし、IRToyは他のプログラムからもアクセス可能のため、誤って'$'が送信されるリスクはある。

そうだったらArduinoを使ってFM77AV, AV40, AV20に赤外線信号を送ればいいのではないか。と、思った。


途中経過はいいから結果だけ欲しい人はこちら

[Arduino UNOに載せる回路の回路図とPCB]

[Arduinoソースコード]

最初の失敗

既にFM77AVキーボードエミュレータを作ったとき、ApolloさんクラシックPC研究会の小林さんの協力と、ですくまさんの情報により送信するコードはわかっていた。(ですくまさんにひとことお礼を言いたかったのだが連絡が取れない。)あとは送信するだけ。Arduinoで可視光LEDを点灯させるなんて簡単だから、同じ方法で点灯するだろう、と高をくくっていた。赤外線のLEDなんてどれも同じだろうと思ったので、Amazonから「岐阜県」なるブランドの赤外線LEDを買った。後に「岐阜県 (Gifken)」ではなく「ギクファン (Gikfun)」であることが判明したが、自分の脳内には「岐阜県」として記憶されてしまった。

IRToy v2で成功したときは、100us(一万分の1秒)のパルスを送れば良かったから、Arduinoのmicros()という関数で100us単位でオン・オフをコントロールしてみた。なお、usはマイクロセカンド。1usは100万分の1秒。ミリセカンドのひとつ下。この手の奴は、ミリ、マイクロ、ナノと1000分の1ずつ小さくなっていく。

しかし、我が実験機FM77AV40は無反応。あっけなく失敗に終わる。しかし、赤外線発生回路についてよくよく調べてみると、なんでも一般的な赤外線LEDは100mAの電流を消費するらしい。

普通の可視光LEDは大体2Vかければ20mA程度流れてよく光るようにできている。データピンから直接LED経由でGNDにつないでしまうと5Vかかる。と、LEDは電圧がかかりすぎて焼けてしまう。(ということになっているが、LEDのデータシート見ると結構最大電圧5Vって書いてあるんだけど大丈夫なのかな?) うまいこと2V, 20mAにするために、抵抗を挟む。直列につなぐわけだから、電流は一定。電流は割とホースの中を水が流れていくイメージで理解できる。ホースに入っていく水流量と出ていく水流量、これが電流量にあたる。入った分出ないとたまってしまうので、直列回路ではどこを切り出してきても流れている電流量は一定。

そして、LEDに2Vかかるのであれば抵抗が3V消費してくれると合計で5Vになる。3Vで20mAだったら、オームの法則から、3V/20mA=3/0.02=150。150Ωの抵抗を挟めばLEDに2Vがかかり、20mAが流れ、光る。このLEDの定格電圧Vfと定格電流IfはLEDによってお微妙に違うけど可視光LEDだったらとりあえず200~300Ωの抵抗を挟んでおけば大体大丈夫(少なくとも焼けない)らしい。

ということで余っていた220Ωの抵抗を使った。しかし、赤外線LEDの定格電圧は大体1.3~1.4V、定格電流が100mAらしい。100mA流そうと思ったら、抵抗が3.6V消費してくれる必要があり、100mAだから、3.6V/100mA=3.6V/0.1A=36Ω。220Ωも挟んでしまったらぜんぜん光らない。

だったら36Ωにしてみよう、と、思ったが、ここでApolloさんから、データピンだと20mAぐらいが限界とのこと。5Vかけて100mA流れるようにしてしまうと過電流。多分Arduinoには保護回路ぐらい入っていそうだから基板が焼けることは無いと思うが、少なくとも赤外線LEDに100mAは流れない。

ならば、と、思って220Ωと赤外線LEDのペアを4組作って4本のデータピンから電流を流したらなんとかならないか、と、思ったがやっぱりだめ。

なんでもLEDに流れる電流は電圧と線形の関係にないらしい。ということはかかる電圧が半分になったら光も半分になるということはなくて、電圧が下がると急激に電流量が低下して、ある電圧を下回るとほとんど電流が流れなくなるらしい。だから、多分220Ω挟んだ段階で赤外線LEDにはまったく電流が流れていない状態になり、まったく赤外線を発していなかった。多分。4本束ねたぐらいでまったく発光量は足りなかったらしい。

定電流回路

データピンから直列で赤外線LEDをつなげる作戦は失敗した。次の手としては、トランジスタをスイッチとして使う方法。それまで大雑把にしかわかってなかったんだけど、今回勉強したところによると(そして、Apolloさんからかなり教えていただいたところによると)、トランジスタには3本のピンがあって、コレクタ、ベース、エミッタと呼ばれる。このうちベース電圧をスイッチとして使うことができる。データピンからベースに直結、エミッタからGNDに直結、そして、5V電源ピンから赤外線LEDと36Ω抵抗を直列でコレクタにつなぐことで、発光させることができる、ということらしい。これは当然可視光LED+220Ω抵抗をつないで、1秒ごとにピンのオン・オフを切り替えることで目視で動作を確認できた。FM77AVにキーボード信号を送るには、可視光ではなく赤外線で10KHz (後に実は78KHz必要ということがわかるが、この時は10KHzでいいと思っていた) で明滅させなくてはならない。

ここで、既に成功実績のあるIRToy v2はどうなっているのか?ということで公開されている回路図を見てみた。IRTXから出る信号で、100mA IR LEDを点滅させている。ここの回路図は表面マウント型の部品でできてるから人間が手ではんだ付けするのはきついので、互換性のあるスルーホールマウント部品で回路を作り直すとこんな感じ。

理屈はわからないが(今はわかったんで後の方に書く)、これによって100mAの電流が流せるとのこと。そして、電流一定になるから電圧が許す限り赤外線LEDを直列につなげても、流れる電流は100mAとのこと。ためしに岐阜県の赤外線LEDでその回路を組んでみる。電流が一定なのであれば、赤外線LEDを1本にしようが、2本直列にしようが100mA流れるはず。100mAの電流が流れているということは赤外線LEDの両端点にかかっている電圧は1.3V程度になるはず。

そして、組んでみた。信号を送ってみた。だがFM77AV40は無反応。

ここで、もうひとつ実験を試みる。もしも、仮に赤外線LEDが赤外線を発しているのであれば、FM77AV40にしてみれば信号がHIGHになったまま固定に見えるはず。その状態でIRToy v2から信号を送ったとしても我が赤外線LEDの発光にかき消されて信号は認識できないはず。

というわけでIRToy v2を使って赤外線キーボード信号を送信。

....

普通にタイプできました。

どうやらこの岐阜県の赤外線LEDは暗いらしい。赤外線が出ていることはIRToy v2のセンサーを使って確認済み。パルスを送ると受信LEDが点滅するから。例えば1秒間隔で100usのオン・オフパルスを発したら1秒ごとにLEDがチカっと点滅する。しかし、FM77AV40が認識するには暗すぎるようだ。

一体何ボルトかかっているのか?赤外線LEDが点灯しっぱなしの状態 (データピンをHIGHに固定した状態)で、赤外線LED両端の電圧を測ったところ、1V。ただ、定電流になっているようで、赤外線LEDを2本直列にしてもそれぞれに1Vかかっていた。

しかし、1V?1.3Vじゃないの?ついでに、念のためデータピンとGND間の電圧を測ると、2V。2V!!!???5Vのはずだろう?何度測っても2V。Arduinoのデータピンって実は2Vだったのか?3.3Vの可能性はあると思っていたけど2V?

とにかく、いろんなことがおかしい。なぜこんなことになっているかというと、僕が適当に素性のわからない赤外線LEDを使っているからだ。不幸だったのは、この岐阜県の赤外線LED、赤外線LEDと赤外線センサーのセットだった。試しに自力で赤外線スキャナを作れないかそのうちやってみようという色気を出したのが失敗だった。Amazonのページに出ている仕様1.2-1.3Vで1.5Wというのが定格なのか?それとは別にVf=1.1-1.4Vとあるのは何なのか?Maximum Power 70mW、Maximum Forward Current 30mA、これはLEDの仕様かセンサーの仕様なのか?さっぱりわからない。赤外線LEDで最大電流が30mAってことは無いと思うんだよね。

(その後、何もつなげないでHIGH状態のデータピンとGND間の電圧を測りなおしたら5Vだった。)

定電流回路(2)

一点反省した。訳の分からない電子部品を使ったら訳の分からないことが起こる。DigikeyからデータシートのはっきりわかるLEDを買うことにする。IRToy v2と同じLEDを調達できればいいのだが、IRToy v2はTSAL7400という赤外線LEDを使っているらしいが既に新品は売ってない。性能が近いと思われるVSLB3940という赤外線LEDを選んだ。

赤外線LEDを点灯固定状態にして両端の電圧を測ったところLEDひとつあたり1.32V。このLEDは1.32Vかかっていれば100mA程度流れているはずだから想定通りの赤外線を発しているはずだ。

ついでに、一念発起して定電流回路の理屈を調べてみた。それによって新たに理解したのは、トランジスタのベース・エミッタ間で消費される電圧(電圧降下)、ダイオードが消費する電圧は大体0.7V程度である、という点だった。単純な乾電池で豆電球を点灯するような回路だと、電圧は電池が決める。豆電球がどれほど電圧を消費するか決める権限が無い。しかし、トランジスタやダイオードの場合、ある程度以内の電流が流れている限り、消費電圧は0.7V程度になるらしい。

厳密に何ボルト消費するのかを知るにはダイオードのデータシートを見なくてはならない。ダイオードのデータシートをいくつか見てみたところによると、流れる電流と電圧の関係が載っていて、電流が少しでも流れると大体0.6V電圧を消費して、電流が増えて行っても消費電圧は最初急激に0.8Vぐらいまで伸びて伸びは緩やかになる。というか、0.6Vか0.7Vかけると1mAとかの電流が流れ、0.8Vぐらいかけると10mAを超えて、以後電圧を増やすと急激に電流が増える。電流が先か、電圧が先か、ではなくて、ちょうどつりあいの状態に落ち着くと考えると良くて、1mAの電流が流れるはずなら0.6~0.7Vの電圧がかかっているはずと考えてもいいし、0.6~0.7Vの電圧をかけると1mA~10mA以内の電流が流れているはず、と考えてもいい。

大体この手の物理現象にはつり合いの状態というのが存在する。多分、この定電圧回路も電圧が最初にかかってから極めて短い時間で流れる電流がちょっとずつ増えて行って、ある電流量になったところでつり合いになって安定する。その安定状態で、トランジスタやダイオードが消費する電圧を測ってみたら0.7Vになるということだと思う。

すると、何が言えるかと言うと、GNDを0Vとすると、2本のダイオード分だけ上流にさかのぼった位置の電圧は1.4Vのはずだ。ダイオード2本分降下すると0Vになるから。そうであれば、ベース・エミッタ間で電圧は0.7V降下するというんだから、6.8Ω抵抗にかかる電圧は1.4-0.7=0.7Vのはず。6.8Ω抵抗は何ボルトかかろうが6.8Ωのままだから、電流量は0.7/6.8で大体0.1A。ミリアンペアに直すと100mA。

ベース・エミッタ間にも少しの電流は流れるけれど、ベースの上流には3.6KΩの抵抗がある。ダイオード2本分で1.4V使ってしまっているので、3.6KΩ抵抗にかかる電圧は、電源電圧5Vから1.4Vを引いた値で3.6V。流れる電流は、3.6V/3.6K=1/1K=0.001A=1mA。そのうちどのぐらいがダイオードに流れてどのぐらいがエミッタに流れるのかは知らないけれど、100mAに対して1%以内の誤差になる。

6.8Ω抵抗に100mAが流れることはわかったけれど、どこか別のところから電流を持ってこないとつじつまが合わない。どこから来るかというとデータピンから赤外線LEDを経由して流れてくるしかない。上にも書いたけど水の流れるホースをイメージすると良くて、流れ込む水の量と流れ出す水の量が一致していなければ、ホースはつぶれて真空パックになってしまうか、あるいは膨らんでそのうち破裂してしまう。実は、ホースが流しきれないほどのものすごい水圧をかけたら膨らんでパンクするんだけど、その点も電圧をかけすぎると電子部品が焼けるのと似ている。定電流回路の場合、6.8Ω抵抗に100mAが流れるためにはその上流の赤外線LEDにも100mAが流れなくてはならないことになる。

という理屈がわかったので、赤外線LEDつけっぱなしの状態で、本当にダイオードで0.7Vの電圧降下が起きているのか、赤外線LED、3.6KΩ抵抗、6.8Ω抵抗にそれぞれ理論通りの電圧がかかっているのかテスターで調べてみた。すると赤外線LEDには1.32V (上に書いたけど)、3.6KΩ抵抗には3.6V、6.8Ω抵抗には0.69~0.7V。まったく理論値通りの電圧がかかっていた。しかし、ダイオードの両端電圧を測ってみたら、ベース直結の方が2.5V、GND直結の方が-0.3V。ベース・エミッタ間の電圧を測ったら、2.5Vとかいう変な値になった。半導体部品だとテスターの中を流れる微弱な電流でも影響されてしまうらしい。

もうひとつ実験で、100usパルスを連続で送信状態にしてIRToy v2が受信するかを調べてみたところ、今度はUSBケーブルを引っ張れる限界の距離の1m程度離しても余裕で反応があった。強い赤外線が出ている。

そして、再度FM77AV40に向けて赤外線を送信してみる!

しかし無反応。

無反応だったが、ここで一歩前進。試しに赤外線LEDを点灯状態にしてIRToy v2からキーボード信号を送ったところ、FM77AV40は無反応。IRToyの信号を妨害できるレベルの強さの赤外線が出ている。妨害されるということはFM77AV40の赤外線センサーはこの定電流回路から発せられる赤外線を認識しているということだ。

PWM? (ケーブルテレビの制御に成功)

定電流回路はIRToy v2と同じ回路を模した。そして赤外線も出ている。ひっかかっていたのは、去年の1月、Apolloさんが計測して送ってくれたFM77AVのキーボードが発する赤外線パルスだった。それによると、100usのパルスは100usの間赤外線が出っぱなしではなく、25usの間隔で点灯・消灯を繰り返しているということだった。それも、試しにやってみたのだが、やはり無反応。

ただし、IRToy v2にも気になる記述があって、IRToy v2の定電流回路はハードウェアPWM付きのデータピンから出ているということだった。

PWM (Pulse Width Modulation)って何?

検索したところ、PWMには何種類かあるらしい。ひとつはオン・オフパルスを使って出力信号が綺麗なサイン波になるようにする方法、とのことで、関係無さそうだった。

だが、さらに読み進めると赤外線リモートコントロールは一般的に38KHz, 50% duty cycleのPWMを使うらしいということがわかった。どういうことかというと、100usの間赤外線信号をHIGHにするというのは100usの間赤外線を出しっぱなしにするのではなく、赤外線を連打するらしい。連打というのは1秒間16連射とかいうレベルではなく、1秒間38K回 (38000回) の連射。しかも、1回撃つときは 赤外線を1/76000秒の間だけオン、1/76000秒の間オフにするものらしい。

なんでこれがPWMと呼ばれているのかさっぱりわからない。ただの連射であって、何もモジュレートしてないと思うのだが。だけど、赤外線界ではこれをPWMと呼んでいるらしい。

100usのパルスだと、4連射になる。これはApolloさんが送ってくれた計測結果の倍に相当する。しかし、IRToy v2のマニュアルを詳しく読んだところ、デフォルトでは38KHz、50% duty cycleのPWMで信号を送信という記述を発見!

今度こそ、成功しそうな雰囲気になってきた。

それと、もう一点反省して、いきなり100usパルスが必要なFM77AVのキーボードではなくもっとゆるいテレビのリモコンからスタートすることにした。IRToy v2はFM77AVのキーボードの赤外線信号のキャプチャに失敗した実績があるものの、テレビのリモートはキャプチャして、それを送信することでケーブルテレビの制御に成功した実績もあるので、ケーブルテレビのリモコン程度なら正しく計測できるらしい。

そして、ケーブルテレビの制御に成功!以下はそのときのArduinoソースコード。

#define PIN_IR_LED        11
#define PIN_PILOT_LED     13

unsigned char xfinity_guide_button[]=
{
209,161,105,93,11,89,52,152,11,90,52,154,11,86,52,150,11,93,52,145,11,97,104,252,11,93,104,254,11,92,52,151,11,90,52,151,11,92,52,151,11,91,52,147,11,96,52,155,11,86,52,153,11,89,104,255,11,91,52,152,11,91,104,255,11,90,105,2,11,89,255,255,209,167,52,246,11,86,255,255,209,170,52,244,11,89,255,255
};
unsigned char xfinity_exit_button[]=
{
209,141,105,103,11,82,53,57,10,183,105,8,11,84,52,159,11,83,52,170,11,72,105,4,11,85,52,157,11,86,52,161,11,81,52,170,11,70,52,158,11,84,52,159,11,83,52,155,11,87,52,167,11,75,105,6,11,85,52,164,11,221,104,127,11,77,105,26,11,64,255,255,209,161,53,3,11,226,255,255,209,159,52,255,11,79,255,255,209,155,52,254,11,229,255,255,209,160,52,255,11,78,255,255
};
unsigned char xfinity_ok_button[]=
{
209,164,105,96,11,85,105,0,11,91,52,155,11,87,52,151,11,90,52,148,11,94,105,4,11,86,52,145,11,97,52,149,11,93,52,145,11,98,52,155,11,87,52,156,11,85,52,151,11,91,52,145,11,98,52,145,11,97,104,250,11,95,104,254,11,94,104,251,11,94,255,255,209,162,52,240,11,94,255,255,209,166,52,238,11,93,255,255
};

static unsigned long testCycle[64];

void MakeCycleFromRawSample(unsigned char sample[])
{
  int k=0;
  for(int i=0; sample[i]!=0xff || sample[i+1]!=0xff; i+=2)
  {
    testCycle[k]=sample[i];
    testCycle[k]<<=8;
    testCycle[k]+=sample[i+1];

    testCycle[k]*=17;   // 1 tick is supposed to be 0.17us
    testCycle[k]/=100;

    ++k;
  }
  if(0!=(k&1))
  {
    testCycle[k]=0;
    ++k;
  }
  testCycle[k  ]=0xffff;
  testCycle[k+1]=0xffff;
}

void setup() {
  Serial.begin(115200);
  
  // put your setup code here, to run once:
  pinMode(PIN_PILOT_LED,OUTPUT);
  pinMode(PIN_IR_LED,OUTPUT);
}

// Arudino UNO Digital Pin 
//    9=PB1
//   11=PB3

// #define SetIRHigh digitalWrite(PIN_IR_LED,HIGH);
#define SetIRHigh PORTB|=_BV(3);

// #define SetIRLow digitalWrite(PIN_IR_LED,LOW);
#define SetIRLow PORTB&=~_BV(3);

void SendCyclePWM(unsigned long cycle[])
{
  // noInterrupts();
  unsigned long tm=micros();
  for(int i=0; cycle[i]!=0xffff; i+=2)
  {
    auto tm0=tm;
    tm+=cycle[i];

    for(;;)
    {
      auto t=micros();
      if(tm<=t)
      {
        break;
      }
      // auto d=(((t-tm0)<<1)/25)&1; // 40KHz 50% PWM
      // auto d=(((t-tm0)<<1)/26)&1; // 38KHz 50% PWM
      auto d=(((t-tm0)<<1)/28)&1; // 36KHz 50% PWM
      // auto d=((t-tm0)/25)&1; // 20KHz 50% PWM
      if(0==d)
      {
        SetIRHigh;
      }
      else
      {
        SetIRLow;
      }
    }

    SetIRLow;
    tm+=cycle[i+1];
    while(micros()<tm);
  }
  SetIRLow;
}

void loop() {
  digitalWrite(PIN_PILOT_LED,HIGH);
  MakeCycleFromRawSample(xfinity_guide_button);
  SendCyclePWM(testCycle);
  delay(2000);

  digitalWrite(PIN_PILOT_LED,LOW);
  MakeCycleFromRawSample(xfinity_exit_button);
  SendCyclePWM(testCycle);
  delay(2000);

  digitalWrite(PIN_PILOT_LED,HIGH);
  MakeCycleFromRawSample(xfinity_ok_button);
  SendCyclePWM(testCycle);
  delay(2000);

  digitalWrite(PIN_PILOT_LED,LOW);
  MakeCycleFromRawSample(xfinity_exit_button);
  SendCyclePWM(testCycle);
  delay(2000);
}

ガイドボタン、終了ボタン、OKボタン、終了ボタンの順にひたすら繰り返す。テレビを見てれば反応があるかが確認できる。

なお、このときBreadboardで作った回路はこれ。

PIN8のD3は単にいろいろテスト用につなげておいただけ。

ちなみに、delayMicrosecondsでタイミングを取ってる例題は多分動かない。なぜかというとArduinoのdelayMicroseconds()は4usの誤差がある。そして、delayMicrosecondsとdelayMicrosecondsの間にどの程度の処理が入るかによって誤差がどんどん溜まっていってしまう。それよりは、ひとつのパルス送信がやや長くなったら次のパルスをそれに応じて短くして全体として見ると大体正しい長さの信号になるように調整した方がいい。

どうやればいいかというと、時間ではなく時刻で赤外線のオン・オフを制御する。信号送信開始時のmicros()の値を変数t0に記憶しておいて、100us単位のパルスだったらmicros()の値がt0+100まで、次のパルスはt0+200まで、というように時間を測る。ついでに、PWMの周囲もいくつか変えてみた。が、ケーブルテレビのリモコンは許容範囲が広いらしく、20KHz~40KHzまでどれでも反応してしまった。

ATmega328, CPUタイマー

だとしても、ケーブルテレビのリモコンで反応したんだから、FM77AV40でも反応するのではないか?

という期待はあっさり崩れ去って、やっぱりFM77AV40は無反応。

しかし、ケーブルテレビが反応するんだから赤外線は出ていてPWMも動いている。となると、もう残るのは時間の精度が足りないとしか思えない。

38KHz 50% duty cycleのPWMとすると、ひとつのサイクルで赤外線が13usオン、13usオフ。micors()を使う方法では4usの精度だから、これが実際は最短12usになってみたり、最長16usになってみたりする。さらに処理中に割り込みがかかると誤差はもっと大きくなる。

ついでに、Arduino UNOは16MHzのCPUだから、マイクロセカンド単位の処理なんて余裕、かと思たらそうでもなかった。考えてみたら1usは100万分の1秒。16MHzを100万で割ると(メガで割るから) 16。16CPUクロック分の処理しかできない。アセンブラで書いてるならまだしも、コンパイラが出すコードで16CPUクロックと言ったら大したことができない。Arduino UNOのCPU、ATmega 368のデータシートによるとほとんどの命令は1~2クロックで実行できるらしい。だが、micros()はおそらく割り込み停止、使うレジスタを退避、タイマー値読み込み、戻り値のメモリアドレスに書き込み、使うレジスタを復帰、割り込み再開、リターン、とこのぐらいやっているはず。ちなみに8ビットCPUだから32ビットの値を読み書きするには4度の読み書きが必要。余裕で16クロックぐらい使いそうだ。micros()呼び出し自体で1usぐらいの誤差は出ると見た方がいい。

けど、もう手詰まりか、と、思ったそのとき、Arduinoのピンを見たら「~」マークが。しかも、その下に「DIGITAL (PWM~)」。ArduinoにPWM機能あったのか!

ArduinoのハードウェアPWMは、CPUの内部タイマーによって実現できるらしい。CPUのインストラクションを経由せずにタイマーのカウントを元にデータピンの出力をトグルできる。

なお、Arduinoのタイマーによるピン出力制御には、CTCモードとPWMモードがあり、PWMモードにはのこぎり波を生成するモードと三角波を生成するモードがあるらしい。今求めているのは38KHzのPWMだから、PWMモードを使えばいいのかと思ったら大きな間違いで、矩形波(オン・オフ)を作ろうと思ったらCTCモードを使う。ちなみにPWMモードは周期を自由に変更できない。周期はCPUの1クロック×整数倍×256しか選ぶことができなくて、しかも整数倍というのも任意の整数ではなく、プリスケーラに設定可能な 1, 8, 32, 64, 128, 256, 1024のうちの一つしか選ぶことができない。38KHz 50% duty cycleを作りたいという点でもArduinoが言うPWMモードは用が足りない。

任意の周期の矩形波PWMが欲しい時使うのはCTCモード。CTCはClear on Time Compareの略で、タイマーのカウンタがあらかじめ設定した値に達したとき、カウンタがゼロに戻る。カウンタがゼロに戻るとき、特定の出力ピンの値をトグルすることができる。これによってCPUがタイマーを見ながらピンをオン・オフするよりも圧倒的に高い精度で一定周期の矩形波を出すことができる。

では、38KHz 50% duty cycleのPWMを出すためのカウンタの値は何か?このタイマーはCPUクロックによってカウントアップする。何クロックで1回カウントアップするか、プリスケーラというのを使ってコントロールできるが、この場合1倍を使う。高い精度を出すためにはプリスケール値は小さい方がいい。ただし、8ビットカウンタなので(タイマ1だけ16ビットカウンタだけど、アトミックに取ってこれるのは8ビットまで)欲しい周期に対応するカウンタ値が255(タイマ1だと65535)を超えてしまう場合はプリスケーラを使わなくてはならない。

この場合、16MHzのCPUで38KHzの倍で76KHzがほしいので、16M/76K=210.53。1倍のプリスケーラを使うと211回に一度ピン出力をトグルすればいい。8ビットの範囲に収まっている。誤差は0.53/16M秒。マイクロセカンドで考えると0.53/16us=0.03us。十分小さい値と言える。

38KHz 50% duty cycleのPWMはCPUタイマー1で生成するとして、100usのパルス幅はどうやって測るか? それはタイマー2を使うことにした。送信を開始するとき割り込みをマスクして、邪魔が入らないようにする。そして、busy waitでマイクロセカンドを計測する。プリスケーラを1倍に設定すると、カウント自体は1/16usの精度で上がっていくはずだが、問題はそれをチェックするプログラムがタイマーの値を取得して次に何かするときにはタイマーの値が必ずカウントアップしてしまう。8倍にすると精度は0.5us。200回のカウントで100usに対応する。256回カウントアップしてオーバーフローする前にカウンタをゼロにしてその分待ち時間から引き算する処理が必要になるけれど、8クロック以内にタイマーのカウンタを0に戻しさえすればロスは無い。8倍の次だと32倍になってしまうが、すると精度は2us。もう少し高い精度が欲しい。

ということでプリスケーラは8倍にする。そして作った送信部分がこれ。

#define SetPin9and10Low PORTB&=~(_BV(1)|_BV(2));

#define START_TIMER1 {TCNT1=PWM_TIMER_THRESHOLD-1;TCCR1A=bit(COM1A0)|bit(COM1B0);TCCR1B=bit(CS10)|bit(WGM12);}
#define STOP_TIMER1 {TCCR1A=0;TCCR1B=0;TCNT1=0;}

void SendCycleHWPWM(unsigned long cycle[])
{
  noInterrupts();

  for(int i=0; cycle[i]!=0xffff; i+=2)
  {
    TCNT2=0;
    START_TIMER1;
    auto w=cycle[i];
    while(TCNT2<(w<<1))
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=120;
      }
    }

    STOP_TIMER1;
    SetPin9and10Low;

    TCNT2=0;
    w=cycle[i+1];
    while(TCNT2<(w<<1))
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=120;
      }
    }
  }
  SetPin9and10Low;

  interrupts();
}

cycle[2*i], cycle[2*i+1]でHIGH, LOWの時間。単位はマイクロセカンド。関数呼び出しのオーバーヘッドを避けるためにピンをオフにするのはCPUのピンに直接書き込むようにしてdigitalWriteを使わない。タイマーの開始もマクロ。タイマー1を開始すると止めるまでの間38KHz 50% duty cycleの矩形波信号がピン9から出る。なお、タイマー1はピン9とピン10をコントロールすることができる。一応ピン10にも出してるけど使わなくてもいいけど。

タイマー2の値はBusy Waitでチェックしていて余裕を見るために240になるたびに0にクリアして、待ち時間から120を引く。なお240に達した瞬間を確認したら引き算よりも前にTCNT2=0;とすることによってカウンタをまずリセットすることで、w-=120;の間にTCNT2が上がってしまう確率を下げる。多分8クロックかからないと思うからまず安全。

考えたのはHIGHからLOWに転換するところでまずTCNT2=0;としてからタイマー1を止めて消灯するか、先にタイマー1を止めて消灯してからTCNT2=0;とするか。TCNT2=0; はおそらく、CPU的にはOUT命令一発だから2クロックだと思う。が、消灯までのこの2クロックの遅れが悪影響を及ぼすか?

多分、どっちが先でもあまり影響ないと思ったので、とりあえず先にタイマー停止と消灯させることにした。

このコードも、いきなりFM77AV40で試す前にうちのケーブルテレビで実験。動作を確認。

そして、今度こそと思ってFM77AV40にキーコード"fm77av"を送信したところ、ついにキーコード送信に成功!


赤外線送信装置プロトタイプ

しかし、数回に一度キーを取りこぼす。"fm77av"が"f77av"になってみたり"fm7av"になってみたりするのでまだ実用化できなかった。

最後の決め手は100us, 125us, 175usだった

この時点で考えたのは2点。キーリリースコードを送っていないからではないか?もうひとつは、クラシックPC研究会の小林さんからいただいた情報、100us, 125us, 175usパルスなのではないか?

ちなみにFM77AVのキーコードはひとつにつき30ビット。100us, 125us, 175usパルスの場合は3ビットが合計400usなので、それを10回繰り返して4000usとなる。100usパルスで送る場合は3ビット目ずつを2倍の長さで送信するので合計4000us。別の見方では100us, 100us, 200usの繰り返しで送信すると見ても一緒。

IRToy v2では100usパルスで成功していたので、まずはキーリリースコードを疑ってみた。キーストロークコードの後必ずキーリリースコードも送るようにしてみた。が、状況は変わらず。

次に100us, 125us, 175usパルスを試してみると、ほとんど取りこぼしが発生しなくなった。我がFM77AV40はArduinoからの赤外線信号を受け取り、ひたすら"fm77av"の文字列をタイプしてゆく。以下はそのときのコード。

// 50% duty cycle: On and off within 1/38000 sec.  Must togle every 1/76000 sec.
#define TIMER_THRESHOLD_38K_WITH_1xPRESCALE ((F_CPU/38000)/2)
// Experiment with FM77AV40 IR LED receiver:  19000Hz No response 34000-36000 drops key.  38000 perfect.  40000 drops keys.

#define PWM_TIMER_THRESHOLD TIMER_THRESHOLD_38K_WITH_1xPRESCALE

// Succeeded Xfinity remote

#define PIN_OC1A 9
#define PIN_OC1B 10
#define PIN_OC2A 11
#define PIN_OC2B 3

// PIN9=PB1 (PortB bit 1)
#define SetPin9High  PORTB|=_BV(1);
#define SetPin9Low   PORTB&=~_BV(1);
// PIN10=PB2 (PortB bit 2)
#define SetPin10High PORTB|=_BV(2);
#define SetPin10Low  PORTB&=~_BV(2);
// PIN11=PB3 (PortB bit 3)
#define SetPin11High PORTB|=_BV(3);
#define SetPin11Low  PORTB&=~_BV(3);
// PIN3=PD3 (PortD bit 3)
#define SetPin3High  PORTD|=_BV(3);
#define SetPin3Low   PORTD&=~_BV(3);


#define SetPin9and10Low PORTB&=~(_BV(1)|_BV(2));



void setup() {
  Serial.begin(1600000); // 115200);
  
  pinMode(13,OUTPUT);
  pinMode(PIN_OC1A,OUTPUT);
  pinMode(PIN_OC1B,OUTPUT);
  pinMode(PIN_OC2A,OUTPUT);
  pinMode(PIN_OC2B,OUTPUT);

  // Reset timers 1 and 2
  TCCR1A=0;
  TCCR1B=0;
  TCCR2A=0;
  TCCR2B=0;
  TCNT1=0;
  TCNT2=0;

  // Timer 1 (8-bit) for 38K PWM IR LED output
  // ATmega 328 datasheet pp.134
  OCR1A=PWM_TIMER_THRESHOLD;
  OCR1B=0;

  // Timer 2 for measuring 1us tick.
  TCCR2A=0;
  TCCR2B=bit(CS21);
  // CS20=0,CS21=1,CS22=0 means 8x pre-scaling.  0.5us per tick.  ATmega328 datasheet pp.162
  OCR2A=0;
  OCR2B=0;
}

#define START_TIMER1 {TCNT1=PWM_TIMER_THRESHOLD-2;TCCR1A=bit(COM1A0)|bit(COM1B0);TCCR1B=bit(CS10)|bit(WGM12);}
#define STOP_TIMER1 {TCCR1A=0;TCCR1B=0;TCNT1=0;}
// ATmega 328 datasheet pp.134
// TCCR1A  COM1A0=1, COM1B0=1 means toggle OC1A and OC1B on compare match
// TCCR1B  WGM10=0,WGM11=0,WGM12=1 means CTC mode
//         CS10=1,CS11=0,CS12=0 means 1x pre-scale (no scaling).
//         CS10=0,CS11=0,CS12=0 means timer stop.



// cycle[2*i] HIGH duration in microseconds  cycle[2*i+1] LOW duration in microseconds
// cycle[2*k]==0xffff means end of the transmission.
// Last off duration is unnecessary.  Therefore set cycle[2*k-1]=0.
void SendCycleHWPWM(unsigned long cycle[])
{
  noInterrupts();

  for(int i=0; cycle[i]!=0xffff; i+=2)
  {
    TCNT2=0;
    START_TIMER1;
    register auto w=(cycle[i]<<1);
    while(TCNT2<w)
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=240;
      }
    }

    TCNT2=0;
    STOP_TIMER1;
    SetPin9and10Low;

    w=(cycle[i+1]<<1);
    while(TCNT2<w)
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=240;
      }
    }
  }
  SetPin9and10Low;

  interrupts();
}




/* Repeat every 3rd of 4-byte groups.
   The raw pulse looks to be going 100us,125us,175us.  Thanks Mr. Kobayashi of Classic PC and Retro Gaming JAPAN!
   But, approximating it with 100us 100us 200us looks to work just fine.
*/
void FM77AV30bitTo40bit(char code40[41],const char code30[])
{
  int k=0;
  for(int i=0; i<30; i+=3)
  {
    code40[k++]=code30[i];
    code40[k++]=code30[i+1];
    code40[k++]=code30[i+2];
    code40[k++]=code30[i+2];
  }
  code40[40]=0;
  if(40!=k)
  {
    Serial.println("Internal error!\n");
    Serial.println("Did I count correctly?\n");
  }
}

unsigned int FM77AVGet1stByteIn30bitEncoding(const char code30[])
{
  unsigned int sum=0;
  unsigned int bitShift=0x80;
  for(int i=4; i<12; ++i)
  {
    if('1'==code30[i])
    {
      sum+=bitShift;
    }
    bitShift>>=1;
  }
  return sum;
}

void FM77AVPut1stByteIn30bitEncoding(char code30[],unsigned int byteData)
{
  unsigned int bitShift=0x80;
  for(int i=4; i<12; ++i)
  {
    if(0!=(byteData&bitShift))
    {
      code30[i]='1';
    }
    else
    {
      code30[i]='0';
    }
    bitShift>>=1;
  }
}

void FM77AVMakeKeyReleasePattern(char code30[],const char pressPtn[])
{
  /* Thanks to Mr. Kobayashi from Classic PC & Retro Game Club JAPAN
     for calculation of the key-release code.
  */

  auto firstByte=FM77AVGet1stByteIn30bitEncoding(pressPtn);
  firstByte-=0x60;
  firstByte&=255;

  for(int i=0; i<31; ++i)
  {
    code30[i]=pressPtn[i];
  }
  FM77AVPut1stByteIn30bitEncoding(code30,firstByte);
}

void MakeOnOffCycle40(unsigned long cycle[],const char code30[])
{
  char code40[41];
  FM77AV30bitTo40bit(code40,code30);

  int k=0;
  bool irLedState=true;
  unsigned long accum=0;
  for(int i=0; i<31; ++i)
  {
    cycle[i]=0;
  }
  for(int i=0; i<40; ++i)
  {
    auto nextIrLedState=('1'==code40[i]);
    if(nextIrLedState!=irLedState)
    {
      ++k;
    }
    cycle[k]+=100;
    irLedState=nextIrLedState;
  }
  if(0==k%2)
  {
    ++k;
    cycle[k]=0;
  }
  cycle[k+1]=0xffff;
  cycle[k+2]=0xffff;
}

void MakeOnOffCycle30(unsigned long cycle[],const char code30[])
{
  // int us[3]={80,145,175};  // Drops keys.
  // int us[3]={90,135,175};  // Works!
  int us[3]={100,125,175}; // Works!  Based on Kobayashi-san.
  // int us[3]={110,115,175}; // Works!
  // int us[3]={120,105,175}; // No response
  // int us[3]={130,95,175};  // No response

  // int us[3]={150,50,200};   // No response.
  // int us[3]={125,75,200};   // No response.
  // int us[3]={110,90,200};   // Works!
  // int us[3]={108,92,200};   // Drops keys.
  // int us[3]={105,95,200};   // Drops keys.
  // int us[3]={100,100,200};  // Drops keys.
  // int us[3]={90,110,200};   // Works!
  // int us[3]={80,120,200};   // Drops keys.
  // int us[3]={70,130,200}; // No response.

  // int us[3]={100,110,190};  // Works!
  // int us[3]={100,120,180};  // Drops keys.


  int k=0;
  bool irLedState=true;
  unsigned long accum=0;
  for(int i=0; i<31; ++i)
  {
    cycle[i]=0;
  }
  for(int i=0; i<30; ++i)
  {
    auto nextIrLedState=('1'==code30[i]);
    if(nextIrLedState!=irLedState)
    {
      ++k;
    }
    cycle[k]+=us[i%3];
    irLedState=nextIrLedState;
  }
  if(0==k%2)
  {
    ++k;
    cycle[k]=0;
  }
  cycle[k+1]=0xffff;
  cycle[k+2]=0xffff;
}

void MakeOnOffCycle(unsigned long cycle[],const char code30[])
{
  MakeOnOffCycle30(cycle,code30);
}


void loop() {
  const char *code30f=    "101010110101010110110110101001";  // F
  const char *code30m=    "101010110101001010110110110101";  // M
  const char *code30full7="101010110110110101010110110101";  // Full-key 7
  const char *code30num7= "101010110101001001010101010101";  // Num-key 7
  const char *code30a=    "101010110110101001001001010101";  // A
  const char *code30v=    "101010110101010101001010101001";  // V

  unsigned long cycle40[40];
  char releaseCode30[31];

  unsigned long keyInterval=10000;

  digitalWrite(13,HIGH);
  MakeOnOffCycle(cycle40,code30f);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30f);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );


  digitalWrite(13,LOW);
  MakeOnOffCycle(cycle40,code30m);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30m);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );


  digitalWrite(13,HIGH);
  MakeOnOffCycle(cycle40,code30full7);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30full7);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );


  digitalWrite(13,LOW);
  MakeOnOffCycle(cycle40,code30num7);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30num7);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );


  digitalWrite(13,HIGH);
  MakeOnOffCycle(cycle40,code30a);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30a);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );


  digitalWrite(13,LOW);
  MakeOnOffCycle(cycle40,code30v);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+40; );

  FM77AVMakeKeyReleasePattern(releaseCode30,code30v);
  MakeOnOffCycle(cycle40,releaseCode30);
  SendCycleHWPWM(cycle40);
  for(auto t0=micros(); micros()<t0+keyInterval; );
}

IRToy v2だったら100usだけでも成功したのに、なぜArduinoでは100us, 125us, 175us....の3通りのパルスの繰り返しでのみ成功するのか。ここから先はオシロスコープを使ってIRToy v2とArduinoで赤外線LEDにかかる電圧を計測して比較するしかないが、とりあえず安定してキーコードを送信できるようになったので、ひとまず成功と言っていいレベルに達した。


100us, 125us, 175usにしたところ取りこぼさなくなった。

なお、簡単にPWM周波数、パルス長を変更して実験できるようになったので、いろいろ実験した結果、PWM周波数は2KHz間隔で反応を試したところ、36K, 40Kではキーコードを送信できるものの取りこぼしが多数発生。その外側ではまったくキーコードを認識しなかった。38Kが最適のようだ。

また、パルス長は、いろいろ試したところ、

 80,145,175 キーを取りこぼす
 90,135,175 完全に動作
100,125,175 完全に動作
110,115,175 完全に動作
120,105,175 全く反応なし
130, 95,175 全く反応なし

150, 50,200 全く反応なし
125, 75,200 全く反応なし
110, 90,200 完全に動作
108, 92,200 キーを取りこぼす
105, 95,200 キーを取りこぼす
100,100,200 キーを取りこぼす
 90,110,200 完全に動作
 80,120,200 キーを取りこぼす
 70,130,200 全く反応なし

100,110,190 完全に動作
100,120,180 キーを取りこぼす

ということで、3ビット目を175usに固定した場合、最初の2ビットを100us, 125usにするのがちょうど安定する範囲の中間のようなのだが、3ビット目を200usにすると、最初の2ビット100us, 100usの前後は動作するのに、なぜか両方100usでキーの取りこぼし発生。

いまいち法則は謎のままになっている。 

シリアル接続赤外線送信デバイス化

とにかく、安定して動作するようになったので既に書いてあるFM77AVキーボードエミュレータからArduino経由でキーコードを送信できるようにしてみた。

Arduino UNOに載せて動作する赤外線発光部分の回路図とPCBデザインはこれ

最初、IRToyと同じコマンドを実装すればWindowsのホストプログラムの方は変更しなくても済む、などと思っていたのは大きな間違いで、シリアル通信と赤外線発光が同時に発生すると失敗するという問題が出た。シリアル接続処理用のチップを持たないArduinoは全部CPUがシリアル送受信を制御している。IRQをマスクしてしまうとその間にホストPCからArduinoに送信された信号は無視されてしまうことがわかった。IRQをマスクしないと正確なタイミングで赤外線は送信できない。赤外線送信中はホストプログラムには送信を待ってもらわなくてはならないが、ArduinoはCTS/RTSによるフロー制御ができないので、送信が終わったということを明示的にホストPCに送信しない限りホストPCはArduinoが赤外線信号を出している途中なのか、終わったのかわからない。

しかし、そんな信号はIRToyは送ってくれない。送信終了のアクノリッジを待つようにホストプログラムを書き換えると今度はIRToy非対応になってしまう。IRToyのマニュアルには送信完了のアクノリッジを送らせることも可能とあるのだが、本物のIRToyはそのように設定してもアクノリッジを返さない。結局、ホストプログラムはつながってるのがArduinoなのかIRToyなのか識別して送信部分は別処理をしなくてはならなくなった。識別は、初期化中にバージョンナンバーを受け取るとき"Vxxx"ではなく"Axxx"が返ってきたらArduinoと判定することにした。

だったら、Arduinoと認識したら30ビット信号を4バイトに詰めて送信すればコマンドと合わせて5バイトでキーコードをひとつ送信できるようになってしまうので、50バイト程度必要なIRToyより効率化できる。そういうわけで、ざっくり書き換えることにした。


PCBにしてみた。背景はホスト機に使ってるThinkPad。

ただ、一応、一般的な赤外線送信装置としても使えるように、0x03に続けて、2バイトずつ (High-byte, Low-byteの順。Big-endian)マイクロセカンドでオン・オフの長さ、最後に0xff,0xff で任意のパルスも送信できるようにした。

ホスト側では、パルス情報を送信後、Arduinoから'C'が返ってきたら送信終了と判断して次のパルスを送信することができる。なお、ボーレートは380000bps固定。ソースの先頭の方に書いてあるから変更が必要であれば変更できるけど。

なお、下がArduinoのソースコード。Arduino UNOで確認。その他のArduinoはCPUが違ったりピン割り当てが違ったりして使えない鴨しれない。Nanoでも動作すると思う。LED発光回路は上の回路図のままで大丈夫。可視光LEDは電源が入ると発光状態になってキーを押すたびに一瞬消灯する。

/*
38KHz 50% duty cycle Infra-Red Signal Emitter
Copyright 2019 CaptainYS (http://www.ysflight.com)  All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, 
   this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice, 
   this list of conditions and the following disclaimer in the documentation 
   and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS 
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE 
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


#define SERIAL_BPS 380000
// Confirmed to work up to 440000bps.
// Stopped working at 460000bps.
// Not tested between 440000 and 460000bps.


#define RECVBUF_SIZE 384
int nRecvBuf=0;
unsigned char recvBuf[RECVBUF_SIZE];
unsigned int cycle[RECVBUF_SIZE/2];
bool transmitMode=false;
unsigned long lastDataReceivedTime=0;
unsigned char processingCmd=0;


unsigned char pulseWidthTable[30];
const unsigned char pulseWidthSource[3]={100,125,175};


#define CMD_RESET 0
#define CMD_IRMAN_COMPATIBLE_MODE 'R'
#define CMD_VERSION 'V'
#define CMD_SELFTEST 't'
#define CMD_SAMPLERMODE 'S'
#define CMD_ENABLE_BYTECOUNT 0x24
#define CMD_ENABLE_TRANSMISSION_NOTIFY 0x25
#define CMD_ENABLE_HANDSHAKE 0x26
#define CMD_TRANSMIT_MICROSEC 0x80
#define CMD_TRANSMIT_30BIT_100US 0x81

#define NOTIFY_READY 'C'
#define NOTIFY_FAIL 'F'

// 50% duty cycle: On and off within 1/38000 sec.  Must togle every 1/76000 sec.
#define TIMER_THRESHOLD_38K_WITH_1xPRESCALE ((F_CPU/38000)/2)
// Experiment with FM77AV40 IR LED receiver:  19000Hz No response 34000-36000 drops key.  38000 perfect.  40000 drops keys.

#define PWM_TIMER_THRESHOLD TIMER_THRESHOLD_38K_WITH_1xPRESCALE

// Succeeded Xfinity remote

#define PIN_OC1A 9
#define PIN_OC1B 10
#define PIN_OC2A 11
#define PIN_OC2B 3

#define PIN_POWER 13
#define PIN_STATUS 8

// PIN9=PB1 (PortB bit 1)
#define SetPin9High  PORTB|=_BV(1);
#define SetPin9Low   PORTB&=~_BV(1);
// PIN10=PB2 (PortB bit 2)
#define SetPin10High PORTB|=_BV(2);
#define SetPin10Low  PORTB&=~_BV(2);
// PIN11=PB3 (PortB bit 3)
#define SetPin11High PORTB|=_BV(3);
#define SetPin11Low  PORTB&=~_BV(3);
// PIN3=PD3 (PortD bit 3)
#define SetPin3High  PORTD|=_BV(3);
#define SetPin3Low   PORTD&=~_BV(3);

// PIN13=PB5 PortB bit 5
#define SetPin13High PORTB|=_BV(5);
#define SetPin13Low PORTB&=~_BV(5);
// PIN8=PB0 Port B bit 0
#define SetPin8High PORTB|=_BV(0);
#define SetPin8Low PORTB&=~_BV(0);

#define SetPin9and10Low PORTB&=~(_BV(1)|_BV(2));


#define SET_CTC_MODE {TCCR1B=bit(CS10)|bit(WGM12);}
// TCCR1B  WGM10=0,WGM11=0,WGM12=1 means CTC mode
//         CS10=1,CS11=0,CS12=0 means 1x pre-scale (no scaling).
//         CS10=0,CS11=0,CS12=0 means timer stop.

#define SET_OC1A_OC1B_TOGGLE {TCCR1A=bit(COM1A0)|bit(COM1B0);}
// ATmega 328 datasheet pp.134
// TCCR1A  COM1A0=1, COM1B0=1 means toggle OC1A and OC1B on compare match

#define SET_OC1A_OC1B_LOW {TCCR1A=(bit(COM1A1)|bit(COM1B1));TCCR1C=(bit(FOC1A)|bit(FOC1B));TCCR1C=0;}
// TCCR1A=(bit(COM1A1)|bit(COM1B1));  // Clear OC1A low on compare match
// TCCR1C=(bit(FOC1A)|bit(FOC1B));    // Force match
// TCCR1C=0;                          // Do I need to clear?


// Failed Attmept:
// For each HIGH pulse, force OC1A to be HIGH, and then start timer.  This approach didn't work.
// Probable reason is that it makes the first duty cycle slightly shorter.
// This method FM77AV40 misses one in every 80 to 100 key strokes.



void setup() {
  for(int i=0; i<30; ++i)
  {
    pulseWidthTable[i]=pulseWidthSource[i%3];
  }
  
  Serial.begin(SERIAL_BPS);

  pinMode(PIN_POWER,OUTPUT);
  pinMode(PIN_STATUS,OUTPUT);

  pinMode(PIN_OC1A,OUTPUT);
  pinMode(PIN_OC1B,OUTPUT);
  pinMode(PIN_OC2A,OUTPUT);
  pinMode(PIN_OC2B,OUTPUT);

  // Reset timers 1 and 2
  TCCR1A=0;
  TCCR1B=0;
  TCCR2A=0;
  TCCR2B=0;
  TCNT1=0;
  TCNT2=0;

  // Timer 1 (8-bit) for 38K PWM IR LED output
  // ATmega 328 datasheet pp.134
  OCR1A=PWM_TIMER_THRESHOLD;
  OCR1B=0;

  SET_OC1A_OC1B_LOW;

  // Timer 2 for measuring 1us tick.
  TCCR2A=0;
  TCCR2B=bit(CS21);
  // CS20=0,CS21=1,CS22=0 means 8x pre-scaling.  0.5us per tick.  ATmega328 datasheet pp.162
  OCR2A=0;
  OCR2B=0;

  SetPin13Low;
  SetPin8Low;
  SetPin9and10Low;
}

void SendCycleHWPWM(unsigned int cycle[])
{
  noInterrupts();

  TCNT1=0;
  SET_CTC_MODE;
  SET_OC1A_OC1B_LOW;
  // Now pins 9 and 10 are under control of Timer 1, OC1A, OC1B both low.
  // Pin9=OC1A
  // Pin10=OC1B

  for(int i=0; cycle[i]!=0xffff; i+=2)
  {
    TCNT2=0;

    TCNT1=PWM_TIMER_THRESHOLD-8;
    // Timer 1 pre-scalar is 1x.
    // Need to start toggling within 8 cycles.  0.5us error.
    SET_OC1A_OC1B_TOGGLE;

    auto w=cycle[i]<<1;
    while(TCNT2<w)
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=240;
      }
    }

    TCNT2=0;
    SET_OC1A_OC1B_LOW;
    w=cycle[i+1]<<1;
    while(TCNT2<w)
    {
      if(240<=TCNT2)
      {
        TCNT2=0;
        w-=240;
      }
    }
  }

  SET_OC1A_OC1B_LOW;
  SetPin9and10Low;

  interrupts();
}

void MakeCycle(unsigned int cycle[],int nSample,unsigned char sample[])
{
  unsigned char k=0;
  switch(processingCmd)
  {
  case CMD_TRANSMIT_MICROSEC:
    {
      for(int i=0; i+1<nSample && (sample[i]!=0xff || sample[i+1]!=0xff); i+=2)
      {
        cycle[k]=(sample[i]<<8);
        cycle[k]+=sample[i+1];
        ++k;
      }
    }
    break;
  case CMD_TRANSMIT_30BIT_100US:
    {
      unsigned char samplePtr=0,sampleBit=1;
      cycle[k]=0;
      for(unsigned char i=0; i<30; ++i)
      {
        bool currentBit=((~k)&1);
        bool nextBit=(sample[samplePtr]&sampleBit);
        if(currentBit!=nextBit)
        {
          ++k;
          cycle[k]=0;
        }
        cycle[k]+=pulseWidthTable[i];

        if(128==sampleBit)
        {
          ++samplePtr;
          sampleBit=1;
        }
        else
        {
          sampleBit<<=1;
        }
      }
      ++k;
    }
    break;
  }
  cycle[k]=0;
  k+=(k&1); // Force it to be even.
  cycle[k  ]=0xffff;
  cycle[k+1]=0xffff;
}

void Transmit()
{
  SetPin8High;
  MakeCycle(cycle,nRecvBuf,recvBuf);
  SendCycleHWPWM(cycle);

  while(0==Serial.availableForWrite());
  Serial.write(NOTIFY_READY);

  transmitMode=false;

  SetPin8Low;
}

void loop() {
  bool received=false;
  while(0<Serial.available())
  {
    auto recvByte=Serial.read();
    if(true!=transmitMode)
    {
      processingCmd=recvByte;
      if(CMD_TRANSMIT_MICROSEC==recvByte ||
         CMD_TRANSMIT_30BIT_100US==recvByte)
      {
        transmitMode=true;
        nRecvBuf=0;
      }
      else if(CMD_IRMAN_COMPATIBLE_MODE==recvByte)
      {
        Serial.println("OK");
      }
      else if(CMD_VERSION==recvByte)
      {
        Serial.println("A277");
      }
      else if(CMD_SAMPLERMODE==recvByte)
      {
        Serial.println("S77");
      }
    }
    else
    {
      recvBuf[nRecvBuf++]=recvByte;
      if(CMD_TRANSMIT_30BIT_100US==processingCmd && 4<=nRecvBuf)
      {
        Transmit();
      }
      else if(RECVBUF_SIZE<=nRecvBuf)
      {
        Transmit();
      }
      else if(2<=nRecvBuf && 0xff==recvBuf[nRecvBuf-1] && 0xff==recvBuf[nRecvBuf-2])
      {
        Transmit();
      }
    }
    received=true;
  }

  unsigned long t=millis();
  if(true==received)
  {
    lastDataReceivedTime=t;
  }
  else
  {
    // Second mode of failure.
    // In transmit mode, the FIFO buffer is overwhelmed and starts losing bytes.
    // The terminator 0xffff won't be caught, however, 0x03 included in the
    // pulse-width data put it into the transmitMode again, and never recover.
    // To get out of this mode, the user needs to release the key for 100ms.
    // If no byte is received for 100ms, the program goes back to command mode.
    if(true==transmitMode)
    {
      if(t<lastDataReceivedTime ||   // Timer overflow
         100<t-lastDataReceivedTime) // 100ms no transmittion from host
      {
        transmitMode=false;
        Serial.write(NOTIFY_FAIL);
        Serial.write(NOTIFY_READY);
      }
    }
  }
}

最後に残った謎

既にこのエミュレータは実用化(?)して、今は我がFM77AV40を使うときはIRToyではなくArduinoでタイプしている。

ただ、最後に残った謎は、なぜ100us, 125us, 175usが動作して、100us, 100us, 200usが動作しないのか、ということがまだわからない。IRToyが100,125,175しか受け付けないのであればそういうものだろうと思って納得もできるが、IRToyで出したらどっちも大丈夫で、Arduinoだと片方しか受け付けないというのはどうにも理解できない。

CMUのコンピュータクラブのオシロスコープを使って波形を調べさせてもらった。下はその波形。


キーコード'f'の出力

拡大したところ、ひとつの山が13.2usで、綺麗な波形が出ている。

が、問題はこれがどのように赤外線LEDに伝わっているかということなのだが、なぜか赤外線LEDのピンにかかる電圧を測定しようとするとオシロスコープがキャプチャできなかった。多分定電流回路であることと何かの関係があるのではないかという話になったのだが、結論は不明。

ピンが正しい波形を送っているようであるということはわかったものの、なぜIRToyで成功する 100us, 100us, 200us の組み合わせだと取りこぼしが多くて、100us, 125us, 175us の組み合わせだとまったく取りこぼしが発生しないかの解明にはいたらなかった。

Comments are welcome.  Send E-Mail to: 

Back to http://www.ysflight.com