🪄

SDメモリカードを抜き差しせずイメージを書き換えRaspberry Piをブートするツール(ソフトウェア作成編)

2023/07/22に公開

前回の記事

電源ON/OFFしているところ

前回、PCBをKiCad使って設計、部品の実装し、SDカードスロットに差して、Raspberry Piが起動するまでの試行錯誤を書きました。今回は、このツールのもう一つの機能である赤外線コントロールの部分のソフトウェアの実装について書きたいと思います。

どんなものかについては前回の記事を参照してみてください。

Raspberry Piの電源をON/OFFするには

Raspberry Piの電源はUSB Type-Cで供給していますが、このUSBケーブルを切ってリレー等でON/OFFする方法もありますが、Raspberry Pi以外の機器でもON/OFFしたく、100vの電源をON/OFFできるようにしたいと思いました。100vのON/OFFをリレーで行うのは、ちょっと電圧が高くハードルが高かったので、IoTでやるようなスイッチを探していると、赤外線でON/OFFができるのを見つけました。

大きさも小さく、価格もそんなに高くないので、これを使えると何かと便利です。これを、赤外線でコントロールして、100vの電源がON/OFFできれば、Raspberry PiへのUSBケーブルを切らなくても、ON/OFFできます。

赤外線コントロールのやり方

昔からある専用ICによる赤外線送信の方法では、この機器に合うかどうかわかりません。一般的な赤外線リモコンのフォーマットを調べてみましたが、これと決まった方法はなく、複数のフォーマットがあるようです。また、このリモコンがどのフォーマットなのかわからないので、実際を調べて見ました。

下記の赤外線リモコン受信モジュール(SPS-445-1:写真右)を用意し、下記の回路をブレッドボードで結線し、リモコンからのON/OFFを簡易ロジアナで信号を取ってみました。

赤外線受信回路

赤外線フォーマット確認

ONボタンを押した時の信号です。家電のフォーマットとまったく違うことがわかりました。これでは、専用ICでは送信できないのですが、この簡易ロジアナはUSBシリアルコンバータFTDIのFT232Hを使用したものです。これで波形が取れるなら、この波形のデータを赤外線LEDに送信すればできるのではないかと思いました。

ON信号

波形の立下りの部分は、実際は38KHzで変調されています。赤外線リモコン受信モジュールがこの変調された部分を1つのLow区間にまとめています。ですので、送信する際は、このLowの部分を38KHzの矩形波で埋めて送信すれば良いはずです。(実際のリモコンの矩形波は1/3dutyで、1周期の1/3がHighで2/3がLowですが、電池の電力消費を抑えるためそうしているとのことですので、今回はLEDは電池駆動ではないのでDutyは1/2にしました。)

受信モジュールの出力をFT232Hで受け、データを取り出します。ADBUS0のビットのみを順次取り出します。HIGHの部分が1となり、LOWの部分が0になります。送信時は、1の部分を0に反転し、0の部分を交互に1010となるようにエンコード(赤字部分)します。この送信データを赤外線LEDのADBUS1に38KHzのボーレートで順次送信することでコントロールします。

赤外線エンコードのイメージ

赤外線リモコンの信号を送受信するUSBシリアルコンバータ(FT232H)のコード

ソースコードの主要部分です。
すべてのコードは以下にあります。

FT232HのUSBポートをオープンします。

#include <ftdi.h>

#define USB_VENDER_ID		0x0403	// FTDI
#define USB_PRODUCT_ID		0x6014	// FT232H

#define CLOCK_RATE_FOR_IFR	3800 // 38kHz
#define CLOCK_RATE_FOR_IFR_TX	3960 // 38kHz * 1.04

static FT_STATUS ftdi_open(FT_HANDLE *ftdi){
	FT_STATUS fts;

	if((*ftdi = ftdi_new()) == 0){
		fprintf(stderr, "ftdi_new() failed.");
		exit(1);
	}

	fts = ftdi_usb_open(*ftdi, USB_VENDER_ID, USB_PRODUCT_ID);
	if (fts < 0 && fts != 5)
	{
		fprintf(stderr, "FT_Open failed (error %d).\n", (int)fts);
		exit(1);
	}

	return fts;
}

赤外線リモコン受信モジュールの信号を受信し、ファイルにデータを保存部分です。

// リモコンコマンド受信
int command_capture(char *cmdname, int dumpbit){
	FT_STATUS	fts = FT_OK;
	FT_HANDLE	fth;

    // FT232Hポートをオープン
	fts = ftdi_open(&fth);
	if (fts != FT_OK){
		return fts;
	}

    // 受信ボーレートを設定 38KHz
	fts = ftdi_set_baudrate(fth, CLOCK_RATE_FOR_IFR);
	if (fts != FT_OK) 
	{
		printf("FT_SetBaudRate failed (error %d).\n", (int)fts);
		goto exit;
	}

    // 受信開始
	DWORD cmdlen;
	UCHAR buf[CAPTURE_BUFFER_SIZE];
	fts = ifr_cmd_capture(fth, buf, CAPTURE_BUFFER_SIZE, &cmdlen);
	if(fts != FT_OK){
		goto exit;
	}
	if(dumpbit){
		bit_show(buf, cmdlen, 0);
	}
    // 受信完了後、コマンドを送信できるようにエンコード
	encode_ifr(buf, cmdlen);
    // コマンドを保存
	save_ifr_cmd(buf, cmdlen, cmdname);
	if(cmdlen > 0)
		fts = FT_OK;
	printf("Receive IFR data (%dB)\n", cmdlen);

exit:
    // ポートをクローズ
	ftdi_usb_close(fth);
	ftdi_free(fth);	
	return fts;
}

// 受信処理
int ifr_cmd_capture(FT_HANDLE fth, UCHAR *buf, int maxbuf, DWORD *prcvbuf)
{
	UCHAR tmpbuf[TMP_BUF_SIZE];
	FT_STATUS 	fts;
	DWORD bytesRead = 0, outlen = 0, outofs = 0, room;
	int wait;

	if (maxbuf < MIN_COMMAND_BUF_LEN){
		printf("Invalid buffer length %d\n", maxbuf);
		return -1;
	}

	// FT232HのADBUSポートをBITBANGモードにセットする。
	fts = ftdi_set_bitmode(fth, 0x00, BITMODE_BITBANG);
	if (fts != FT_OK) 
	{
		printf("FT_SetBitMode failed (error %d).\n", (int)fts);
		return fts;
	}

	printf("Capture Start.. \n");
	//フィルタ初期値セット
	ifr_filter(NULL, 0, 1);
	ifr_lowpass_filter(NULL, 0, 1);
	outofs = PREAMBLE_BIT_SIZE;
	room = maxbuf - PREAMBLE_BIT_SIZE;
	memset(buf, 0x01, outofs); 
	for(wait=0;wait < MAX_CAPTURE_WAIT;wait++){

		// データを受信 ADBUS0 01:HIGH,  00:LOW
		fts = ftdi_read_data(fth, tmpbuf, TMP_BUF_SIZE);
		if(fts < 0){
			return fts;
		}
		bytesRead = fts;
		// ノイズを除去
		ifr_lowpass_filter(tmpbuf, bytesRead, 0);
		// コマンド列を抽出
		outlen = ifr_filter(tmpbuf, bytesRead, 0);
		if(outlen == 0){
			if(outofs > PREAMBLE_BIT_SIZE){
				*prcvbuf = outofs;
				return FT_OK;
			}
			continue;			
		}
		if(room < outlen){
			printf("overflow buffer room=%d outlen=%d\n", room, outlen);
			outlen = room;
		}
		memcpy(buf+outofs, tmpbuf, outlen);
		outofs += outlen;
		room -= outlen;
	}
	printf("expired wait time.\n");
	*prcvbuf = outofs;
	return FT_OK;
}


赤外線LEDから送信する部分です。

// 赤外線コマンドの送信
int command_tx(char *cmdname, int n_tx, int dumpbit){
	FT_STATUS	fts = FT_OK;
	FT_HANDLE	fth;

	// FT232Hポートをオープン
	fts = ftdi_open(&fth);
	if (fts != FT_OK){
		return fts;
	}

	// 送信ボーレートを設定 38KHz * 1.02倍
	fts = ftdi_set_baudrate(fth, CLOCK_RATE_FOR_IFR_TX);
	if (fts != FT_OK) 
	{
		printf("FT_SetBaudRate failed (error %d).\n", (int)fts);
		goto exit;
	}
	DWORD cmdlen;
	UCHAR buf[CAPTURE_BUFFER_SIZE];

	// 送信するコマンドのデータをファイルからリード
	cmdlen = load_ifr_cmd(buf, CAPTURE_BUFFER_SIZE, cmdname);
	if(dumpbit){
		bit_show(buf, cmdlen, 1);
	}
	for(int j=0;j<n_tx;j++){
		// コマンドビット列を送信
		tx_ifr_cmd(fth, buf, cmdlen);
	}
	printf("Tx Command %s (%d)\n", cmdname, cmdlen);

exit:
	// ポートのクローズ
	ftdi_usb_close(fth);
	ftdi_free(fth);	
	return fts;
}

// 送信処理
DWORD tx_ifr_cmd(FT_HANDLE fth, UCHAR *buf, DWORD cmdlen){
	DWORD bytesWritten;
	FT_STATUS fts;

    // ADBUSポートのモードをBITBANGモードにセット
	fts = ftdi_set_bitmode(fth, 0xFF, BITMODE_BITBANG);
	if (fts != FT_OK) 
	{
		printf("FT_SetBitMode failed (error %d).\n", fts);
		return fts;
	}
    // コマンドデータを送信 ASBUS1に送信 02: HIGH,  00: LOW
	fts = ftdi_write_data(fth, buf, cmdlen);
	if (fts < 0)
	{
		printf("FT_Write failed (error %d).\n", fts);
	}
	bytesWritten = (DWORD)fts;
	return bytesWritten;
}

メモリカードMUXの切替信号を出す部分です。

// val: 1:ホスト側, 0:ターゲット側
int write_to_tbctl(UCHAR val){
	FT_STATUS	fts = FT_OK;
	FT_HANDLE	fth;

	fts = ftdi_open(&fth);
	if (fts != FT_OK){
		return fts;
	}
	// ACBUS5(C5)を1または0にコントロールすることでメモリカードの方向を切替
	// 1: Host side, 0: Target side
	fts = ftdi_set_bitmode(fth,
						0xF0 | (val & 0x0F), // C9,C8,C6,C5
						BITMODE_CBUS);
	if (fts != FT_OK)
	{
		printf("FT_SetBitMode failed (error %d).\n", (int)fts);
	}
	ftdi_usb_close(fth);
	ftdi_free(fth);	
	return 0;
}

使い方(2023.7.23追記)

Githubからソースコードを取り出し、ビルドします。Ubuntu 22.04 LTSを使用しています。

$ sudo apt -y install build-essential libftdi-dev
$ git clone https://github.com/hnz1102/sdcdmux-emmc.git
$ cd sdcdmux-emmc/src
$ make

接続確認します。

$ ./sdcdmux
Memory Device is connected to : HOST.

リモコンのONコマンドをツールに覚えさせます。

$ ./sdcdmux -c on
Capture Start.. 

リモコンのONを押します。
リモコンコマンドの学習

以下のように受信しました。

Receive IFR data (10776B)
Captured command for on

同様にリモコンのOFFコマンドをツールに覚えさせます。

$ ./sdcdmux -c off
Capture Start..

リモコンのOFFを押します。以下のように受信しました。

Receive IFR data (10135B)
Captured command for off

今度は、覚えたコマンドで、電源ON/OFF操作できるか確認します。

$ ./sdcdmux -x on
Tx Command on (10776)

カチッといってONできました。

次はOFFにしてみます。

$ ./sdcdmux -x off
Tx Command off (10135)

カチッといってOFFできました。

SDカードをターゲットに切り替えて、Raspberry Piを起動してみます。

$ ./sdcdmux -s target
Memory Device is connected to : TARGET.
$ ./sdcdmux -x on
Tx Command on (10776)

カチッといってRaspberry Piの電源が入り、OSが起動しました。

覚えたコマンドでON/OFFを繰り返したい時、以下の-lコマンドで自動でON/OFFを繰り返しします。繰り返しブート試験等に使えます。
コマンドオプションの意味は-o <秒数>でONの時間を-f <秒数>でOFFの時間を指定します。ONした時間とOFFした時間が出力されます。

$ ./sdcdmux -l -o 30 -f 10
POWER ON : No.1  2023/07/23 09:59:08 Sun
Tx Command on (10776)
POWER OFF: No.1  2023/07/23 09:59:38 Sun
Tx Command off (10135)
POWER ON : No.2  2023/07/23 09:59:48 Sun
Tx Command on (10776)
POWER OFF: No.2  2023/07/23 10:00:19 Sun
Tx Command off (10135)
POWER ON : No.3  2023/07/23 10:00:29 Sun
Tx Command on (10776)

ON/OFFできな場合があったが、送信ボーレートを調整して解決

最初は、1回の送信で電源ONしないまたはOFFしないことがありました。出力が弱いのかと思ったのですが、近づけても変わりません。赤外線LEDの送信の波形をとっても、38KHzで変調されています。パルスの長さも、受信時の通りに出力していました。全く動かないわけでもないので、コードはあっているように思えます。

赤外線LED出力波形

行き詰っていましたが、この赤外線LEDが送信したものを、赤外線受信モジュールはどう出力しているのか見てみることにしました。つまり、自身が出した赤外線LEDの出力を、この受信モジュールはどう認識しているかを見ました。

送信と受信

するとこの赤外線受信モジュールは、実際の変調された部分より出力が遅れ、かつ長くなることがわかりました。つまり、実際のリモコンから受信した波形は、リモコンが送信した長さのパルスに比べて少し長く受信モジュールのLOW部分が長くなっていました。このままの長さで送信しても、受信長がさらに長くなり合わなくなってしまいます。

個々のパルスがどれくらい長くなるか計ってみましたが、バラバラで一律、何クロック削減すればよいか定まりませんが、大体でパルス長を合わせればよいと考え、送信ボーレートを約1.04倍になるよう39.6KHzで送信したところ、安定してON/OFFできるようになりました。

赤外線モジュールのデータシートはそういう記述がないので、なぜこうなるのか原因はわかっていません。

また、FT232Hのボーレートの設定は、ftdi_set_baudrate(fth, CLOCK_RATE_FOR_IFR)で設定するのですが、第2引数のCLOCK_RATE_FOR_IFRは3800です。38KHzなら38000なのですが、1/10にしないと38KHzになりません。送信は39.6KHzにしましたので、3960です。他を検索しても、BITBANGモードでは1/16だろうとありましたが、1/16では小さすぎました。オシロで確認してみると1/10のようです。非同期シリアル通信の10bit(8bitデータ+Start/Stop-bit)から来ているのかもしれませんが、BITBANGモードとset_baudrateとの関係はよくわかりません。

おまけ:赤外線コマンドがキャプチャできることで、いろんな機器が操作できた

ともあれ、安定して動作したので、いろいろな身近にある赤外線リモコンを学習させてみました。シーリングライト(天井のライト)、ディスプレイのリモコンは、ON/OFFできました。エアコンについては、LEDの出力が弱いようで認識しませんでした。近づければ動くのかもしれませんが、我が家のエアコンは古いので、そのせいかもしれません。TVは近くにないので、ためしてませんが、たぶん動くだろうと思います。

秋月電子に以下のボードが売っていますので、リモコン部分だけであれば試すことができます。赤外線LEDを複数並べて送信出力を上げて、PC操作の自作IoT家電制御リモコンにしてもよいかもしれません。(2023.07.23追記)

次回

このツールのSDカードへの書き込みに4GBで約8分ほどかかってました。これをなんとか時短にするために、フラッシュメモリ(eMMC)にしたところ、2分ちょっとで書込みができるようになりました。次回、そのeMMC版の作成の経緯を記したいと思います。ここまで読んでくださりありがとうございました。

Discussion