ラベル 電子工作 の投稿を表示しています。 すべての投稿を表示
ラベル 電子工作 の投稿を表示しています。 すべての投稿を表示

2022年6月19日日曜日

Seeed XIAO BLEの充電回路を使いこなす

最近、国内の各電子部品店でSeeed XIAO BLEという製品が取り扱われるようになりました。親指サイズのデバイスで、nRF52840というARM Cortex-M4 CPUを持ったマイコンモジュールで、Bluetooth 5.0に対応しています。もちろん技適も通っています。

末尾に"Sense"が付いたモデルと付いていないモデルの2種類があります。Sense付きは700円程度高くなりますが、6軸IMUとPDMマイクを搭載しているようです。それ以外は同じです。

さて、特記すべきことに、このデバイスにはBQ25101というTexas Instrument製のリチウムイオン電池充電コントローラーが付いています。そのため、適当なリチウムポリマー電池などを直結して、充電式のBluetoothデバイスなんかを簡単に開発することができます。なんというかすごい時代ですね。本当にIoTが個人レベルで作りやすくなったと感じます。

ハードウェア

回路全体

さて、一応Seeed wikiにこのマイコンボードの回路図は載っているのですが、非常に見にくくてわかりずらいです。そのうえ、現物との対応もあまりとれません。ということでいろいろひも解いてみました。

結論から言うと、回路は以下のようになっています(電源にかかわらない回路は省略しています)。

点線で書いた部分のみがこのボードの外の配線となります。ですので、バッテリーを基板の裏面にある「BAT+」「BAT-」端子に接続するだけです。裏面のSMD端子なのが少し不便ですが、まあそれはよいでしょう。

Seeed wikiより引用)

ただし、回路図から見てわかるように、Seeed XIAO BLEに搭載されているのは充電コントローラーだけで、過放電保護回路は付いていません。電子工作で手に入るリチウムポリマー電池は保護回路付のものも多いですが、18650サイズのリチウムイオン電池など、保護回路が付いていないものはDW01/FS8205などの過充放電保護ICなどを組み込むとよいでしょう。詳しくはこちらの記事を参照してください。

ちなみに、Q1はなぜダイオードではなくFETを付けているのかと思う人もいるかもしれませんが、これはダイオードでの約0.6Vの損失をなくすためです。FETだとダイオードに比べてほとんど電圧降下しませんので、3.7Vのリチウムイオン電池で駆動する際も、3.3V以上の電圧をレギュレーターに供給できるようになります。

充電コントロール回路

さて、メインディッシュは充電コントローラーのBQ25101となります。 とはいっても、BQ25101はよくあるタイプの充電コントローラーで、電圧がある一定(約4.2V)になるまでは定電流充電を行い、そこから先は定電圧充電を行うタイプのものです。電池の温度制御(温度が高くなりすぎないように電流を抑える機能)もあるようですが、今回は殺されている(TS端子に温度センサではなくただの抵抗が取り付けられている)ので機能しません。

充電プロファイル(データシートより引用)

ISET端子は充電電流を決める端子で、その抵抗値により決まります。範囲は13.5kΩ(10mA)から0.54kΩ(250mA)です。このボードではR18に2.7kΩが付いているので、デフォルトで50mA流れます。また、マイコンからP0.13をLOW出力してあげればさらに2.7kΩが並列につながるため、充電電流が50mA増えて合計100mAを流すことができます。

~CHG端子は充電状態か否かを示す端子です。オープンドレイン端子となっていて、LOW(=FET ON)で充電状態、Open(=FET OFF)で充電完了状態を意味します。充電LEDがこの回路に付いているのでマイコンの制御なしにそのままLEDが機能しますが、マイコン側からP0.17の値を読み込むことで充電中か否かを判断することもできます。

さらに、R16とR17で分圧されたバッテリー電圧をアナログピンで読み込む回路も一緒に入っています。これによってバッテリー電圧をマイコンで知ることができます。もちろんこの回路を有効にするにはP0.14をLOW出力する必要があります。普段はこのポートを入力(ハイインピーダンス)にしておくことで、R16-R17の計1.5MΩを流れる非常に微量の電流(約2uA)ではありますが節電することができます。

なお、このマイコンボードではバッテリー電圧を測ることしかできないため、例えば負荷が大きく上下するような環境でこの充電回路を使用した場合、負荷が上がれば(=電流が大きくなれば)バッテリーの内部抵抗での電圧降下が大きくなるためバッテリー電圧は下がり、その後負荷が下がったときにバッテリーの電圧は回復します。そのような場合は、バッテリー電圧しか見ていないと正しく電池の消耗状態を把握することができません。もっと厳密に電池残量を測るには、クーロンカウンタと呼ばれる電荷の移動量を監視するICが必要になってきますが、このボードにはそのようなものは無いため、外付けしない限りはあまり厳密な電池残量の把握はできません。電圧はあくまでも参考程度というものになります。

ソフトウェア

さて、充電回路がわかったところでソフトを作っていきましょう。今回はサンプルでBluetoothキーボードとして使えるデバイスを作っていきます。

Bluetooth 4.0から規格が制定され、もちろんこのSeeed XIAO BLEでも対応しているBLE(Bluetooth Low Energy)にはBattery Serviceというものがあり、ホスト側にバッテリー残量を知らせることができます。例えばWindows11だと、Battery Serviceに対応しているデバイスでは以下のように電池残量を示すアイコンが出てきて、極端にバッテリー残量が低下しているなどの場合は通知で警告してくるなどの機能があります。

このSeeed XIAO BLEはせっかくここまでの充電回路を持っているデバイスなので、このBattery Serviceを使いこなしていきましょう。

環境構築

さて、今回はArduinoで開発していきます。

Seeed Wikiにも書いてある通り、Arduinoの「追加のボードマネージャのURL」に "https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json" を追加して「Seeed nRF52 Boards」をインストールするのですが、ここで注意点があります。バージョン1.0.0をインストールしてください。デフォルトだと最新?の2.6.1?が出てきますが、ここで今回使うライブラリは1.0.0しか対応していないようなので、1.0.0を入れる必要があります。

あとは通常通りでOKです。ボード選択などを適宜して使える状態にすれば準備完了です。もしわからなくても、ググればそれなりに情報は出てくるはずです。

setupの実装

早速実装していきましょう。 Adafruitのライブラリを使用していきますが、特段ライブラリをインストールせずとも使えるはずです。

#include <bluefruit.h>

#define	PIN_HICHG	22
#define	PIN_INVCHG	23

BLEDis bledis;
BLEHidAdafruit blehid;
BLEBas blebas;

ヘッダーはこんな感じです。なぜかHICHG(100mA充電)端子のピンと~CHGのピンがヘッダファイルで定義されていないのでここで定義しておきます。

サービスは3つ使い、DIS (Device Information Service)、HID (Human Interface Service)、BAS (Battery Service)です。DISはデバイス情報を伝えるサービス、HIDはキーボード動作そのもののサービスです。

void setup() 
{
	setup_battery();

	Serial.begin(115200);

	setup_ble();
}

void setup_battery()
{
	// High speed charging (100mA)
	pinMode(PIN_HICHG, OUTPUT);
	digitalWrite(PIN_HICHG, LOW);

	pinMode(PIN_INVCHG, INPUT);
}

void setup_ble(void) 
{
	Bluefruit.autoConnLed(false);
	Bluefruit.begin();
	Bluefruit.setTxPower(4);  // Check bluefruit.h for supported values
	Bluefruit.setName("nRF52840 Keyboard");

	// Configure and Start Device Information Service
	bledis.setManufacturer("EH500_Kintarou");
	bledis.setModel("nRF52840");
	bledis.begin();

	blehid.begin();
	blebas.begin();

	// Advertising packet
	Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
	Bluefruit.Advertising.addTxPower();
	Bluefruit.Advertising.addAppearance(BLE_APPEARANCE_HID_KEYBOARD);

	// Include BLE HID service
	Bluefruit.Advertising.addService(blehid);

	// Include BLE battery service
	Bluefruit.Advertising.addService(blebas);

	// There is enough room for the dev name in the advertising packet
	Bluefruit.Advertising.addName();

	Bluefruit.Advertising.restartOnDisconnect(true);
	Bluefruit.Advertising.setInterval(32, 244);  // in unit of 0.625 ms
	Bluefruit.Advertising.setFastTimeout(30);    // number of seconds in fast mode
	Bluefruit.Advertising.start(0);  // 0 = Don't stop advertising after n seconds
}

Setupルーチンはこんな感じでよいでしょう。まずは100mA充電を有効化し、その後にBLEのセットアップをします。各Serviceを起動して、最後はアドバタイジングの設定をすればOKです。

バッテリー残量

さて、先ほど出てきた問題です。loopルーチンを実装するにあたって、バッテリー残量をバッテリー電圧から推定しなければなりません。ということで、マイコンからバッテリー電圧を見たらどのように見えるのかを確認してみました。使用したバッテリーはaitendoで売っている300mAhのリチウムポリマー電池です。なお購入してから数年間経っているため、性能が落ちているかもしれませんがそこはご容赦ください。

まずは充電です。電池を過放電防止保護がかかるまですっからかんにしてからSeeed XIAO BLEの100mA設定で充電を開始しました。

1時間45分くらいかけて4.3Vくらいまで電圧が上がって、その先は定電圧モードになっています。4時間くらいのところで充電が終了してその拍子に少し電圧が下がっているのが見て取れます。ちなみに、これはこのマイコンボードのA/D変換で測った電圧で、4.3Vくらいまで上がっていますが、手元のテスターで測るとピッタリ4.2Vでした。抵抗の誤差とかなのかなという感じです。

続いて放電です。充電が終わってから過放電保護がかかるまで電子負荷装置で100mAで放電をしました。

放電はこんな感じです。4.1Vくらいからほとんど一定に電圧が下がっていき、3.6Vくらいからガクンと落ちています。

loopの実装

さて、こんな感じでどうでしょう。

#define BAT_AVERAGE_COUNT	16
#define BAT_AVERAGE_MASK	0x0F

void loop()
{
	loop_led();
	loop_battery();
}

void loop_led()
{
	digitalWrite(LED_RED, HIGH);
	digitalWrite(LED_GREEN, HIGH);

	uint32_t ms = millis();

	if(Bluefruit.connected()) {
		uint32_t interval = ms % 3000;
		digitalWrite(LED_BLUE, (interval < 100) ? LOW : HIGH);
	} else {
		uint32_t interval = ms % 2000;
		digitalWrite(LED_BLUE, (interval < 100 || (interval >= 200 && interval < 300)) ? LOW : HIGH);
	}
}

void loop_battery()
{
	static uint32_t lastMeasure = 0;
	static bool lastIsCharging = false;
	uint32_t ms = millis();
	bool isCharging = battery_isCharging();
	static uint16_t rawvalues[BAT_AVERAGE_COUNT];
	static uint8_t index = 0;
	static uint8_t count = 0;
	
	if(ms - lastMeasure > 3000) {
		if(lastIsCharging != isCharging) {	// 充電状態が変わったらリセット
			index = 0;
			count = 0;
		}

		pinMode(VBAT_ENABLE, OUTPUT);
		digitalWrite(VBAT_ENABLE, LOW);
		rawvalues[index] = (uint16_t)analogRead(PIN_VBAT);
		pinMode(VBAT_ENABLE, INPUT);

		index = (index + 1) & BAT_AVERAGE_MASK;
		count = min(count + 1, BAT_AVERAGE_COUNT);
		uint16_t rawtotal = 0;
		for(int i = 0; i < count; i++)
			rawtotal += rawvalues[i];
		
		double volt = (double)rawtotal / count / 1024 * 3.6 / 510 * 1510;	// 10bit, Vref=3.6V, 分圧比1000:510

		if(isCharging) {
			if(volt <= 3.78)
				blebas.notify(1);
			else if(volt <= 4.02)
				blebas.notify(3);
			else if(volt <= 4.25)
				blebas.notify((uint8_t)((volt - 4) * 140 / 5 + 0.5) * 5);	// 4.25Vで35%になるよう5%単位
			else
				blebas.notify(70);	// ここからは定電圧領域になるので電圧じゃほとんどわからない
		} else {
			if(volt <= 3.5)
				blebas.notify(1);
			else if(volt <= 3.62)
				blebas.notify(3);
			else if(volt <= 3.8)
				blebas.notify((uint8_t)(((volt - 3.62) * 277.78 + 5) / 5 + 0.5) * 5);	// 3.8Vで55%、3.62Vで5%
			else if(volt <= 4.1)
				blebas.notify((uint8_t)(((volt - 3.8) * 150 + 55) / 5 + 0.5) * 5);	// 4.1Vで100%、3.8Vで55%
			else
				blebas.notify(100);
		}

		lastMeasure = ms;
		lastIsCharging = isCharging;
	}
}

bool battery_isCharging()
{
	return digitalRead(PIN_INVCHG) == LOW;
}

バッテリーは上の測定結果をもとにパーセンテージを表示するようにしています。しかし、先述の通り電圧によるバッテリー残量測定は目安程度にしかなりませんし、とくに充電なんかは定電圧領域に入ると電圧で判別することはほとんどできなくなるので70%固定とかいう雑な実装をしています。もう少し頑張りたかったら経過時間なども考慮に入れてみるのもいいかもしれません。

また、充放電以外にも接続前は青色LEDが2回点滅、接続後は1回点滅にしたかったのでそういう処理も入っています。

これでだいたいSeeed XIAO BLEをリチウムポリマー電池で使う準備が整いました。やりたいことの実装へ移っていきましょう。

2022年3月14日月曜日

e-Paper(電子ペーパー)を制御する

先日日本橋の電気街をぶらぶらしていたところ、組み込み用の電子ペーパーを見かけました。

電子ペーパーと言えばKindleなどに採用されているディスプレイで、電源を切っても表示内容を維持することができるタイプの画面です。そのため、原理的に画面更新時にしか電力を消費せず、電子書籍のように1分に1回とかの更新で良いような用途では有用なディスプレイですね。

しかし、そのお店で売っていた電子ペーパーは1万円台前半と非常に高く、「ちょっと買ってみるか」くらいでは手が出るものではありませんでした。ですが、ちょっと調べてみると、秋月電子で2,500円の電子ペーパーが売っているじゃありませんか。普通の液晶に比べればちょっと高めですが、まあ、買ってやれないものでもないでしょう。

ということで、実際に買って制御してみました。まずは完成形から見ていきましょう。

見ての通り、画面の更新に約30秒かかります。これは仕様のようです。

SPIでコントローラーのSRAMのバッファーを更新するのはほとんど一瞬で終わりますが、その後の画面リフレッシュに30秒くらいかかるようです。 時計などの用途を考えるとあんまりイケてなさそうですね。

回路設計

回路自体はそんなに難しくないのですが、ドキュメントがイケてないのでめちゃめちゃわかりにくいです。

File:2.13inch e-Paper HAT Schematic.pdf

こちらが回路図になります。ただとても読みにくい。何がどこにつながっているのかかなり難解ですし、挙句の果てには「P1」「J2」「U1」などの部品番号も基板上にプリントされていないという有様です。

解説すると、液晶コントローラー自体は3.3Vで動作するようで、 5V系のマイコンでも操作できるようにレギュレーターとレベルコンバーターが付いているようです。そのレベルコンバーターがU1、レギュレーターがU2です。P3(基板に横向きに出ている白色コネクタ)からコントロールする場合は5Vで制御できます。

一方で、P1はピンソケットで、Raspberry Pi Zeroに直接接続することを意図しているようです(というか液晶サイズもRaspberry Pi Zeroピッタリです)。ですので、ピン配列はRaspberry Pi Zeroのものを参照するとわかりやすいでしょう。ここのコネクタから3.3V系の回路に直結できますので、3.3Vで動作するマイコンに接続する場合はP3を使用する必要はなく、P1から直接接続することができます。

今回はPIC16F18326を使うことにしました。理由は、170円と安価な割にプログラムメモリが16kワードと大きいからです。今回の液晶は104×212ドットの2色表示のため、画像1枚で5.5kBほど使います。

回路図はこんな感じになります。一応e-paper側でDeep Sleepに入ると消費電力が数μAまで下がるようですが、完全にシャットアウトできるようにPICでe-paperの電源を掌握できるようにしています。

ソフト作成

ドライバソフトはメーカーのGithubに載っています。

e-Paper

ただし、PIC用は無いため、Arduino用のを移植する必要があります。今回使うのは3色タイプのLCDなので、e-Paper/Arduino/epd2in13bc/ 以下のファイルを移植していきます。

#include "epd2in13bc.h"


int EpdIf_Init(void) {
    /*
    SPI.begin();
    SPI.beginTransaction(SPISettings(2000000, MSBFIRST, SPI_MODE0));
    */
    
    return 0;
}

void EpdIf_SpiTransfer(unsigned char data) {
    DO_SCL = 0;
    DO_CS = 0;

    for(int i = 0; i < 8; i ++) {
        DO_SDA = (data & 0x80) ? 1 : 0;
        data <<= 1;
        DO_SCL = 1;
        NOP();
        NOP();
        DO_SCL = 0;
    }

    DO_CS = 1;
}



int Epd_Init(void) {
    /* this calls the peripheral hardware interface, see epdif */
    if(EpdIf_Init() != 0) {
        return -1;
    }
    /* EPD hardware init start */
    Epd_Reset();
    Epd_SendCommand(BOOSTER_SOFT_START);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendData(0x17);
    Epd_SendCommand(POWER_ON);
    Epd_WaitUntilIdle();
    Epd_SendCommand(PANEL_SETTING);
    Epd_SendData(0x8F);
    Epd_SendCommand(VCOM_AND_DATA_INTERVAL_SETTING);
    Epd_SendData(0x37);
    Epd_SendCommand(RESOLUTION_SETTING);
    Epd_SendData(EPD_WIDTH);     // width: 104
    Epd_SendData(0x00);
    Epd_SendData(EPD_HEIGHT);     // height: 212
    /* EPD hardware init end */
    return 0;

}

/**
 *  @brief: basic function for sending commands
 */
void Epd_SendCommand(unsigned char command) {
    DO_DC = 0;
    EpdIf_SpiTransfer(command);
}

/**
 *  @brief: basic function for sending data
 */
void Epd_SendData(unsigned char data) {
    DO_DC = 1;
    EpdIf_SpiTransfer(data);
}

/**
 *  @brief: Wait until the busy_pin goes HIGH
 */
void Epd_WaitUntilIdle(void) {
    while(DI_BUSY == 0) {      //0: busy, 1: idle
        __delay_ms(100);
    }      
}

/**
 *  @brief: module reset. 
 *          often used to awaken the module in deep sleep, 
 *          see Epd::Sleep();
 */
void Epd_Reset(void) {
    DO_RST = 0;
    __delay_ms(200);
    DO_RST = 1;
    __delay_ms(200);   
}

/**
 *  @brief: transmit partial data to the SRAM
 */
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the black part of SRAM
 */
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));        
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));        
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_1);
    if (buffer_black != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_black[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 *  @brief: transmit partial data to the red part of SRAM
 */
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l) {
    Epd_SendCommand(PARTIAL_IN);
    Epd_SendCommand(PARTIAL_WINDOW);
    Epd_SendData(x & 0xf8);     // x should be the multiple of 8, the last 3 bit will always be ignored
    Epd_SendData((unsigned char)(((x & 0xf8) + w  - 1) | 0x07));
    Epd_SendData((unsigned char)(y >> 8));
    Epd_SendData(y & 0xff);
    Epd_SendData((unsigned char)((y + l - 1) >> 8));
    Epd_SendData((y + l - 1) & 0xff);
    Epd_SendData(0x01);         // Gates scan both inside and outside of the partial window. (default) 
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);
    if (buffer_red != NULL) {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(buffer_red[i]);  
        }  
    } else {
        for(int i = 0; i < w  / 8 * l; i++) {
            Epd_SendData(0x00);  
        }  
    }
    __delay_ms(2);
    Epd_SendCommand(PARTIAL_OUT);  
}

/**
 * @brief: refresh and displays the frame
 */
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red) {
    if (frame_buffer_black != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_1);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_black[i]);
            //Epd_SendData(~(i & 0xFF));
        }
        __delay_ms(2);
    }
    if (frame_buffer_red != NULL) {
        Epd_SendCommand(DATA_START_TRANSMISSION_2);
        __delay_ms(2);
        for (int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
            Epd_SendData(frame_buffer_red[i]);
            //Epd_SendData(0xFF);
        }
        __delay_ms(2);
    }
    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: clear the frame data
 */
void Epd_ClearFrame(void) {
    Epd_SendCommand(DATA_START_TRANSMISSION_1);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);
    Epd_SendCommand(DATA_START_TRANSMISSION_2);           
    __delay_ms(2);
    for(int i = 0; i < EPD_WIDTH * EPD_HEIGHT / 8; i++) {
        Epd_SendData(0xFF);  
    }  
    __delay_ms(2);

    Epd_SendCommand(DISPLAY_REFRESH);
    Epd_WaitUntilIdle();
}

/**
 * @brief: This displays the frame data from SRAM
 */
void Epd_Refresh(void) {
    Epd_SendCommand(DISPLAY_REFRESH); 
    Epd_WaitUntilIdle();
}


/**
 * @brief: After this command is transmitted, the chip would enter the deep-sleep mode to save power. 
 *         The deep sleep mode would return to standby by hardware reset. The only one parameter is a 
 *         check code, the command would be executed if check code = 0xA5. 
 *         You can use Epd::Reset() to awaken and use Epd::Init() to initialize.
 */
void Epd_Sleep() {
    Epd_SendCommand(POWER_OFF);
    Epd_WaitUntilIdle();
    Epd_SendCommand(DEEP_SLEEP);
    Epd_SendData(0xA5);     // check code
}
#ifndef EPD2IN13B_H
#define EPD2IN13B_H

#include <xc.h>
#define _XTAL_FREQ 32000000    // Fosc = 8MHz


#define DO_SDA          LATCbits.LATC2
#define DO_SCL          LATCbits.LATC0
#define DO_CS           LATCbits.LATC4
#define DO_DC           LATCbits.LATC5
#define DO_RST          LATCbits.LATC3
#define DI_BUSY         PORTAbits.RA4


// Display resolution
#define EPD_WIDTH       104
#define EPD_HEIGHT      212

// EPD2IN13B commands
#define PANEL_SETTING                               0x00
#define POWER_SETTING                               0x01
#define POWER_OFF                                   0x02
#define POWER_OFF_SEQUENCE_SETTING                  0x03
#define POWER_ON                                    0x04
#define POWER_ON_MEASURE                            0x05
#define BOOSTER_SOFT_START                          0x06
#define DEEP_SLEEP                                  0x07
#define DATA_START_TRANSMISSION_1                   0x10
#define DATA_STOP                                   0x11
#define DISPLAY_REFRESH                             0x12
#define DATA_START_TRANSMISSION_2                   0x13
#define VCOM_LUT                                    0x20
#define W2W_LUT                                     0x21
#define B2W_LUT                                     0x22
#define W2B_LUT                                     0x23
#define B2B_LUT                                     0x24
#define PLL_CONTROL                                 0x30
#define TEMPERATURE_SENSOR_CALIBRATION              0x40
#define TEMPERATURE_SENSOR_SELECTION                0x41
#define TEMPERATURE_SENSOR_WRITE                    0x42
#define TEMPERATURE_SENSOR_READ                     0x43
#define VCOM_AND_DATA_INTERVAL_SETTING              0x50
#define LOW_POWER_DETECTION                         0x51
#define TCON_SETTING                                0x60
#define RESOLUTION_SETTING                          0x61
#define GET_STATUS                                  0x71
#define AUTO_MEASURE_VCOM                           0x80
#define READ_VCOM_VALUE                             0x81
#define VCM_DC_SETTING                              0x82
#define PARTIAL_WINDOW                              0x90
#define PARTIAL_IN                                  0x91
#define PARTIAL_OUT                                 0x92
#define PROGRAM_MODE                                0xA0
#define ACTIVE_PROGRAM                              0xA1
#define READ_OTP_DATA                               0xA2
#define POWER_SAVING                                0xE3

int  Epd_Init(void);
void Epd_SendCommand(unsigned char command);
void Epd_SendData(unsigned char data);
void Epd_WaitUntilIdle(void);
void Epd_Reset(void);
void Epd_SetPartialWindow(const unsigned char* buffer_black, const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_SetPartialWindowBlack(const unsigned char* buffer_black, int x, int y, int w, int l);
void Epd_SetPartialWindowRed(const unsigned char* buffer_red, int x, int y, int w, int l);
void Epd_DisplayFrame(const unsigned char* frame_buffer_black, const unsigned char* frame_buffer_red);
void Epd_Refresh(void);
void Epd_ClearFrame(void);
void Epd_Sleep(void);

#endif

オリジナルのライブラリでは、ハードウェアのインターフェース部と論理部できれいに分けて実装されていたのですが、移植するにあたってごちゃ混ぜにしてしまいました。XC8がC++をサポートしていないうえ、PICのピン操作は直接レジスタ触ることになるというのが主な理由です。もう少しやりようはあったかもしれませんが、まあ処理速度優先ということで。

あと、一応回路上ではPICのMSSP(ハードウェアSPI)に対応したピンにアサインしているのですが、SPI送信中に何か別のことをしたいわけでもないので、ソフトウェアで簡単にSPIを実装しています。SPI程度ならMSSPの使い方をデータシート見ながら実装するよりGPIOで作っちゃったほうが簡単ですね。

void UpdateLcd()
{
    DO_LCD_PWR = 1;
    TRIS_LCD_PWR = 0;
    
    __delay_ms(100);
    
    Epd_Init();

    switch(content++) {
        case 0:
            Epd_DisplayFrame(img1_black, img1_yellow);
            break;
        case 1:
            Epd_DisplayFrame(img2_black, img2_yellow);
            break;
        default:
            Epd_ClearFrame();
            content = 0;
            break;
    }

    Epd_Sleep();    
    TRIS_LCD_PWR = 1;
    LATA = 0;
    LATC = 0;
}

void init()
{
    LATA = 0;
    WPUA = 0x20;
    ODCONA = 0;
    ANSELA = 0;
    TRISA = 0xFF;

    LATC = 0;
    WPUC = 0;
    ODCONC = 0;
    ANSELC = 0;
    TRISC = 0xC2;

    IOCAN = 0x20;
    PIE0 = 0x10;
    INTCON = 0x80;
}

void loop()
{
    SLEEP();
    NOP();
    
    if(DI_SW == 0)
        UpdateLcd();    
}

void __interrupt() ISR(void)
{
    if(PIR0bits.IOCIF) {
        if(IOCAFbits.IOCAF5) {
            IOCAFbits.IOCAF5 = 0;
        }
    }
}

void main(void) {
    init();
    
    while(1)
        loop();
}

main.cでは処理をしていない間はPICをスリープにするようにしています。スイッチを押されるとInterrupt On Change (IOC)にてスリープからウェイクアップすると続きの処理を実行するようになります。この際発生する割り込みの割り込みフラグはちゃんとリセットしないとスリープに入れない(常にスリープ復帰条件ができてしまう)ので、割り込みルーチンではフラグのリセットのみを行っています。

また、e-Paperをdeep sleepにした後はLCDの電源を切っていますが、その際に制御用ピンがHighになっていると、そちらから電源側へ電流が流れてしまう(おそらく液晶コントローラー内に保護用にダイオードが入っている)ので、同時にすべてのピンの出力をゼロにしています。

これで、待機中はナノアンペアオーダーの電流しか流れない、ボタン電池で駆動しても何の支障もないe-Paperができました。

 

それにしても、更新に30秒かかるのはちょっと残念ですね…。せめて数秒程度にしてほしかった…。

2020年9月14日月曜日

電子負荷装置を自作する

 さて、今回は(も?)ブログのタイトルに反してプログラミングは全く関係ない記事です。すみません。

1年ほど前、自作で電源装置を作りました。電源装置を作ったら当然負荷テストをしたくなりますよね。ということで、当時はセメント抵抗を何種類か買ってきてテストしていたのですが、抵抗値を変えようと思ったら繋ぎ変えが必要ですので、徐々に負荷を変化させていくということができません。思えば、この時から電子負荷装置が欲しかったのですが、ひとまずは電源装置も安定して動作しておりましたので特にその熱は一旦引いていました。

ですが、最近、日本橋の共立電子に行ったところ、 特設コーナーでスイッチング電源が安売りしていました。5V6Aが550円、12V9Aが1200円など、比較的高出力なものもかなり手ごろな値段で売っております(8月末~9月頭にかけての情報です。特売ですので、いつ行っても買えるとは限りません)。

普段は弱電ばっかり扱っている私ですので、たった数十Wでもパワみを感じて衝動買いしてしまいました。ですが、これだけ高出力な電源の性能を十分に発揮できるだけの負荷が手元にありません。

ここから、1年ほど前の「電子負荷装置」の熱が自分の中で再燃し、せっかくなので作ることを決意しました。

回路設計

電子負荷装置もいろいろなものがあります。商用レベルだと、数百Wとか数kWとかを消費でき、その上、その電力を電力系統に回生するようなものまで売っています。そういうものを自作するのは流石にレベルが高すぎますので、ここは簡単に数十W程度を消費できる単純な負荷を用意することとしました。

モードとしては、定電流モードと定抵抗モードを想定します。定電流モードは入力電圧にかかわらず電流を一定に保つモード、定抵抗モードは入力電圧に比例した電流を流すモードです。名前の通りですね。

この回路は、かなり簡単に作ることができます。オペアンプの基礎が分かっていれば、下記回路には何も説明はいらないでしょう。

たいてい回路図には現れませんが、オペアンプのLM358の電源は12V側から取っています。LM358は2回路入りなので、使わない側は周辺回路と容量結合とかして発振しないよう、適切に接地等してあげましょう。

SW1が定抵抗モード/定電流モードを切り替えるスイッチ、SW2がレンジのスイッチですね。

定電流モードで最大約10Aを流すことを想定しているので、オペアンプの+入力が最大約1Vになるよう抵抗値を調整しました。電源側ににさらに条件を付ければ外部電源無しでも作れますが、それは嫌だったのに加え、電源が必要な電圧/電流計を使用することにしたので外部電源を使うようにしました。

熱設計

電圧、電流、許容損失

さて、上記の通り回路は超シンプルで簡単です。電子負荷装置を作る上で最も難しく、コストもかさむのは熱処理です。

例えばサンケン電子製の2SK3711のデータシートを見ると、定格電圧が60V、定格電流が70Aです。

2SK3711のデータシートより引用)

これだけ見ると、12V10Aくらい余裕のよっちゃんに見えます。

ただ、電子負荷装置は、この12V10AすべてをこのFETで受け止めることになります。たいていのスイッチング用途は何か別の負荷がドレインにつながっていて、それをFETでON/OFFするだけなので、FET自体にかかる電圧は低く、FETそのもので消費する電力もそこまで大きくはなりません。しかし、今回の用途ではすべての電力をFETで受け止めることになります(大事なことなので2度言いました)。

ですが、許容損失は130Wと、12V10A=120Wの損失を発生させても大丈夫そうです。ただし、チャネル温度を150℃以下に抑えられるならね。

そう、一番厳しいのはここ、チャネル温度なのです。定格電流にも定格電圧にも達していなくても、チャネル温度が150℃を超えたらFETが焼け切れてしまうのです。

熱抵抗

熱抵抗というパラメーターがあります。単位はK/Wです。熱伝導率の逆数ですね。「何Wの熱を流すのに何K温度の差が付くか」という物理量です。電気抵抗も「何Aの電流を流すのに何Vの電位差が付くか」という物理量ですね。そのアナロジーで考えると簡単です。

例によって2SK3711のデータシートを見ると、過渡熱抵抗θj-cが定常状態で0.95℃/Wくらいです。過渡熱抵抗と言うとわかりにくいですが、下付き文字の「j-c」は「ジャンクション-ケース間」の意味と思われます。すなわち、チャネルで発生した熱をケース(=FETのパッケージ)に伝えるのにどれくらい温度差が付くか、ということですね。

0.95℃/Wということは、たとえば120Wの損失を作るとチャネルとケースに120×0.95=114℃の温度差が付きます。 超強い放熱器を付けて、ケースを30℃に抑えたとしても、チャネル温度は143℃となり絶対最大定格のぎりぎりとなります。

一方で、例えば東芝製TK70J20Dならば、チャネル-ケース間熱抵抗が最大0.305℃/Wです。先ほどと同じく120Wを消費したとしても、チャネルとケースは36.6℃しか差が付かないということになりますね。これならば少し弱い放熱器を付けて、ケースが110℃くらいまで上昇してしまっても絶対最大定格を超えることはありません。さすが、値段が高いだけありますね。放熱特性が高性能です。

このように、大きな電力を消費させるFETの場合、電流や電圧といった特性よりも何よりも、このようにチャネル温度が最も設計上のボトルネックとなります。

ケース選定

ヒートシンク付きケース

さて、放熱がどれほど重要か、という話をしましたが、そこで重要になってくるのが放熱器です。例えば共立電子ではジャンク扱いでクソデカ放熱器が売っていたりしますが、ちょっとこれはネタすぎてカッコよくないですよね。

今回はタカチ製のヒートシンクアルミケースのEXHシリーズを採用しました。パネルの大きさ、熱抵抗などからEXH14-5-19BBを選択しました。熱抵抗2.27℃/Wです。ちなみに黒アルマイト品のほうが値段が少々高くつきますが、放熱器は黒のほうが黒体放射で冷却性能がやや高いらしいです。ほんとかよ。

このケースだけで値段が5000円程度してしまいます。回路だけだと1000円も出せば十分に組み立てられてしまうのに、ほんと、「電子負荷装置の自作は放熱機構の自作である 」という感じですね。

なお、このケース、放熱器側にナット用の溝があり基板を取り付けることができます。表面にネジ穴を出さないスマート設計ですね。

カタログPDFより引用)

アルミ板を用意し、そこにFET、シャント抵抗、基板を固定しケースに取り付けました。良い感じです。

アルミ板と放熱器の間の空間

さて、この取り付け方、カタログPDFに書いてある通りなのですが、いかんせん、放熱器への熱が流れるルートが少なすぎます。アルミ板とケースが接しているのはナットのレール部分のみ、放熱器本体とアルミ板の間は空気です。空気はかなり熱抵抗が大きな素材です。これでは、効率よく熱を逃がせません。

そこで、間を熱伝導率の高いもので埋めることを考えました。調べていたところ、ワイドワーク製の放熱ゴムというものを見つけました。図面上は隙間が6mm程度ですので、12mm厚のものを購入しました。潰して半分くらいの厚さにできるかなと思いましたが、硬く無理だったので、ハサミでカットして使いました。

熱伝導率2.4W/m・Kなので、面積100mm×100mm(=0.01㎡)、厚さ6mm(=0.006m)とすると2.4×0.01/0.006=4W/Kになります。逆数を取れば熱抵抗で、0.25K/Wとなります。ケースの熱抵抗は2.27K/Wなので、足して2.5K/W程度となります。これで、そこそこの放熱環境を整えられたと言えるでしょう。

耐久テスト

自然冷却

さて、調子に乗って電流を流すと次々とFETが焼けていくんですねえ。さらに、チャネル温度が限界突破して焼けた場合、ドレイン―ソース間がショートモードで故障するらしく、あまり良くない状況になります。

最終的に、TK70J20Dを使って連続30W運転程度が落としどころとなりました。

チャネル―外気間の熱抵抗を3K/Wとすると、30Wで約90℃の上昇となります。気温25℃なら115℃ですね。理論値はこうですが、いろいろと挟んでいるものもあるはずですので、まあこの辺が現実的な解かと思います。

強制空冷

もっと冷やしたくなったら強制空冷ですね。ケースの上にサーキュレーターを置いて、ガンガンヒートシンクを冷やしてみました。

80Wまでなんとかもちました。チャネル温度と気温の温度差を125℃とすると熱抵抗1.56K/Wです。もうすでにこの時点でケースの自然冷却時の熱抵抗を下回っています。さすが空冷ですね。一説によると、熱抵抗が1桁以上変わるんだとか。

ちなみに、90Wにしたら焼けました(◞‸◟)

こうやってみると、TDPが100Wを超えるようなCPUも普通にありますが、その性能をフルに発揮するヒートシンクって実はなかなかに大変なものなんでしょうね。

まとめ

というわけで、電子負荷装置を自作しましたが、話の中心は電気的なところではなく熱的なところとなりました。まあそれも予定調和ですね。

前半のほうに少し触れた電力系統への回生機能を持った電子負荷装置は、単にエコだけでなく、このような強烈な放熱機構の必要性という面からも非常に重要なもの、ということですね。数百Wも放熱するような電子負荷装置を作るとしたら、一体どれだけ大きな放熱機構が必要になることやら。

 

おまけ

デカール貼り

今回、レンジとモードのスイッチを持っていて表記を作らないとわかりにくいことから、デカールを用意して貼り付けました。

小さいころから鉄道模型はやっていたのでインスタントレタリングこと「インレタ」は何度かやったことがあったのですが、デカールはたぶん初めてでした。

難しいですね。小さい文字なんか、器用に貼ろうとしてもどうしても傾いてブサイクになってしまいます。

殉職したFETの方々

今回、実際どの程度FETが持つのかということを知りたくて、FET焼く覚悟で高負荷をかけて実験しました。これらのFETの死は無駄ではなかった…と信じたいです。どうか、来世ではスイッチング電源にでもなって活躍されますように。

FET交換の工夫

最初はFETをはんだ付けしていたのですが、だんだんとそれ自体が面倒になってきます。だってすぐに焼けるんだもん。

ということで、端子台を用意し、はんだ付け不要でFETを交換できるようにしました。

2019年7月30日火曜日

フルアナログの自励式フライバック・コンバータを自作する

前回の記事から遅くなってしまいましたが、今度は自励式フライバック・コンバータを自作してみました。そりゃあね、原理がわかったら自分で試したくなるのがサガってもんでしょう。

注意:スイッチング電源はそのサージにより数百Vの電圧が発生する部位がありますので感電したら重大な事故になることがあります。また、状況により上手く動作せず素子が焼損したり、最悪爆発して重大な事故になる可能性もあります。危険性を認識し安全対策を行ったうえ、各自の責任で製作するようお願いします。万が一の事態が発生しても当方では一切責任を負いかねます

基本設計

まずは基本設計をします。どれくらいのスペックの電源が欲しいかを設定します。

入力電圧 AC100V
出力電圧 2~15V
出力電流 最大3A
スイッチング周波数 75kHz

こんな感じでしょうか。日本でコンセントに接続して使う感じです。出力は可変とし、3Aくらい流せるように作ります。フライバックコンバーターだと数十W程度の出力が限度と言われておりますので、まあこんなもんでしょう。
スイッチング周波数は少なくとも可聴域(~20kHz)は外すようにします。そうしないとコイル鳴きが聞こえて不快です。たいていフライバックコンバータは70~80kHz程度を狙うそうです。

トランス製作

さて、まずはトランスを自作することとします。というよりか、スイッチング電源はトランスが最も鍵となるパーツの上、使用環境によってパラメーターがごっそり変わってきます。市販品をそのまま使うことはまずできません。
となると、トランスのボビンとコアを買ってきて自分で巻かなければなりません。ですが、割とニッチな趣味なのかあまり小売りしている電子部品店は無さそうでした。比較的入手性の良さそうなのはaitendoかと思います。aitendoでは2種類のサイズを売っておりますので、大きいほうのEE25コアを採用すること前提で設計をしていきます(なんか販売終了予定になっているようですので必要な方はお急ぎを)

手順としては、基本設計パラメーターをもとにトランスを設計していきます。それをもとに実際にトランスを巻き、出来上がったトランスのパラメーターを測定します。どれくらい狙ったところにパラメーターを合わせ込めるかはわかりませんが、出来上がったトランスのパラメーターを使って回路全体を設計していくこととします。

トランス設計

aitendoにはトランスコアのデータシートはありませんが、一般的なEE25のパラメーターを使用します。

Le 48.7mm
Ae 40mm2
Bs 420mT@100℃

$L_e$は実効磁路長、$A_e$は実効断面積です。$B_s$は飽和磁束密度ですので、常にこの磁束密度は超えないように注意しなければなりません。

トランスはフライバック型ですので1次側をONしてエネルギーを溜め、その後2次側から放出するという流れになります。そのほか、スイッチング制御用に補助巻線を用意します。
電流はこの図のように流します。$D_{on}$はトランジスタをONしているデューティー比、$D_{off}$はトランジスタをOFFしているタイミング、すなわち2次側にエネルギーを放出しているときのデューティー比です。$D_{delay}$はスナバコンデンサとトランスが共振してスナバコンデンサの電圧が最小になるまで待つ時間です。これを挿入することによりスナバコンデンサの電荷を回生できるので効率が上がります。

1次側

1次電圧は最小で102Vとしました。深い意味はありません。理想は$100{\rm V}×\sqrt 2 =141{\rm V}$となりますが、実際はダイオードブリッジのドロップだったり、平滑リプルによる実効値の低下だったりいろいろあります。ですので、$85{\rm V}\times0.85\times \sqrt 2$にしてみました。

すると、出力は最大で15V3Aに設定しているので出力は45W、効率を仮に82%と置いてみると入力側の電力は55W必要となります。ですので、入力の電流は0.54Aとなります。
その入力電流は平均値です。フライバックコンバータは、実際はトランジスタがONしている間に電流が時間に比例して増えていき、OFFしている間はゼロとなります。ですので、電流の最大値$I_{peak}$は以下の式で表せます。\[I_{peak}=I_{ave} \times \frac{1}{2D_{on}}\]分母に2が付いているのはノコギリ波だからですね。
$D_{on}=0.5$としていますので、$I_{peak}=2.15{\rm A}$となります。

さて、電圧と電流が定まると1次側の自己インダクタンスを求めることができます。
スイッチング周波数を75kHz、デューティー比を0.5からON時間は6.67usとなります。この時間で電流が0から2.15Aまで増えるときにトランス両端に102Vが生まれるわけですから、インダクタの基本式\[V=L \frac{{\rm d}I}{\rm dt}\]を用いてLは317.1uHとなります。

一方で、巻き線のターン数はファラデーの法則スタートで計算できます。\[V=-N\frac{{\rm d}\Phi}{\rm dt}\]コアの最大磁束密度420mTより、安全率0.72を取って最大の磁束密度を302mTとします。そうすると、ON時間(=6.67us)間にこれだけの磁束の変化があればいいわけですから、代入して1次側巻き数を$N_p=56$と求めることができます。

最後にギャップ長を求めておきます。トランスにギャップを付けるとギャップが磁気抵抗のほぼすべてと近似できるため、以下の式が成り立ちます。\[\mathcal R_m=\frac{l_g}{\mu_0 A_e}\]これと磁気抵抗の公式$\mathcal R_m=N^2/L$からギャップを求めると0.497mmとなります。これは実効磁路長におけるトータルのギャップ長ということに注意してください。EE25コアを浮かせると3か所にギャップができますので、1つ当たり1/3でだいた0.165mmとなります。ギャップ長はだいたい0.5mm以下程度を目安とするといいでしょう。あまり大きくなりすぎると漏れ磁束が大きくなり、結合度が下がるだけでなく、サージも大きくなってしまいます。

2次側

トランス2次側は平均値として3Aを出力しなければなりません。トランスから2次側にエネルギーを放出している時間のデューティー比を0.4とし、トランスの出力波形がノコギリ波であることを踏まえると$I_{peak}=3\times2/0.4=15{\rm A}$となります。
2次側自己インダクタンス$L_s$は1次側と同様に求め、5.55uHとなります。ただし、電源装置として出力15Vを確保するために、トランスの出力としては整流ダイオードのドロップ分を考慮して15.6Vとして計算しています。

トランスは1次側と2次側の巻線比の2乗がインダクタンス比ですから、それをもとに2次側の巻き数を求めると7ターンとなります。

補助巻線

補助巻線は1次巻線と巻線の向きが同じですから、1次側との電圧比が巻線比となります。10V出力とすると6ターンとなります。自己インダクタンスは4.08uHとなります。

文章と数式だけで長々と書いてしまいましたが、まとめると以下のようなパラメーターとなります。

1次 自己インダクタンス 317.1uH
巻数 56
2次 自己インダクタンス 5.547uH
巻数 7
補助 自己インダクタンス 4.075uH
巻数 6

トランス巻き

つづいてトランスを巻いていきます。
巻き終わりました(((
結合度を上げるために、1次巻線を半分巻いたら2次巻線を巻き、その上に1次巻線を巻くといった巻き方をしています。
最後は自己インダクタンスを測定しながら上記の値になるようにギャップを調整して終了です。

パラメーター測定

最後に、後ほどシミュレーションできるようにしっかりパラメーターを確認しておきます。各端子の自己インダクタンスのほか、他の端子を短絡させたうえでのインダクタンスも測定しておきます。それにより、結合部分の自己インダクタンスがゼロとなるので、漏れ磁束による自己インダクタンスを求めることができます。そこから巻線間の結合度も導くことができますので、のちのシミュレーションに非常に役に立ちます。
今回私が巻いたものの測定結果は以下の通りになりました。

自己インダクタンス 1次 329.9uH
2次 5.231uH
補助 4.022uH
結合度 1次—2次 0.9865
2次—補助 0.9208
補助—1次 0.9383

ちゃんとした商品の電源トランスは結合度0.9999とか平気で超えるそうですね。ただ、今回はギャップがあったり自分の工作精度だったりでここまで値が下がっています。1次—2次は交互に巻いたので結合度が高いですが、その上に補助巻線を巻いたので、補助巻線と他の巻線の結合度は1次―2次間に比べて悪くなってしまっております。

絶縁抵抗計などを持っていれば1次側と2次側の絶縁性を確認しておくのも良いでしょう。私はあの2万円くらいする計測器は買えませんでした…。

回路設計(シミュレーション)

回路は上記のトランスをもとに作っていきます。

上の画像はシミュレーション用に作ったLTSpiceの回路です。良い部品がなかった場合は類似品としているので、完璧にシミュレーションできているわけではない点に注意してください。
メインのスイッチング用FETは東芝製のTK20A60Uとしました。耐圧600Vと比較的高く、ON抵抗も0.165Ωとかなり抑えられています。

C6とC3は部品選定がなかなか重要です。整流平滑用コンデンサはスイッチング周波数で充放電が繰り返されますので、かなり激しく電流が行き来し、コンデンサの内部抵抗で発熱をします。そのため、それに耐えられるかどうかの指標として「許容リプル電流」というものが定められており、これが使用する回路に適合するように選んでいく必要があります。
とはいえ、リプル電流の実効値を計算していくのはなかなか難しいです。それだけで記事が一つできてしまいそうですので、皆さん各自勉強してください(私ももうちょっとちゃんと勉強したいです)。
今回はC6はニチコンLSシリーズの250V220uF(リプル電流1260mA)、C3はニチコンVRシリーズの25V2200uF(リプル電流1550mA)の2並列としました。回路の中でも大型部品となりますので、箱にちゃんと納められるのかという意味での制約も出てきて選定になかなか苦労します。

他は敢えて説明するほどのものでもないかもしれませんが、少しだけ説明しておきます。C1はM1のゲートを駆動するためのコンデンサです。前回の記事ではバイポーラでしたが、今回はMOSFETですので電荷の供給元が必要です。Q2はフォトカプラだけだとゲインが足りなかったので付けました。なかなかこの辺の回路構成は試行錯誤でした。

あとは、自分の求めたい出力電圧や出力電流を設定しながら、ひたすらトライアンドエラーで回路パラメーターを調整していきます。上手くしないと変に共振してトランス1次側のサージ電圧が高くなってしまったり、2次側の出力電流が大きくなりすぎてトランスが磁気飽和するレベルになってしまったりしますので、実際に作ったときに事故を起こさないようにあらかじめシミュレーションを追い込んでおきましょう。


シミュレーション結果はこんな感じになりました。15V3A出力で安定して出力できています。トランジスタのドレイン電圧も定格はオーバーせず、電流も飽和しないレベルです。

ある程度回路シミュレーションが追い込めたら、次は試作を行いましょう。今回の記事では省略しますが、実際シミュレーション通りに動くとは限りませんからね。
私はそこでトランジスタをいくつか焼いています。

基板設計

さて、試作も終わりある程度作れる目星がついてきたら次は基板を起こします。
回路は基本的にシミュレーションと同じですが、部品単位で起こしたので少し見た目が変わっているのと、ソフトが違うので部品番号が変わってしまっています。また、シャントレギュレーターはシミュレーションではTL431を使いましたが、試作したところおそらく位相余裕が無く負荷条件によっては発散してしまうようで、ここでは製品として高周波ゲインが抑えられた新日本無線のNJM2825を使いました。これだと最小1.2Vまで下げられますし、今回の可変用途にもうってつけです。

さて、今回はKiCadで基板を起こしてみました。


J2はパイロットランプ用の出力、J4は電圧調整用ボリュームのコネクタです。
余談ですが、NJM2825はカソード側が基準となっているオペアンプのため、Bカーブのボリュームの角度に比例した電圧を出力させようとするとこのような位置にボリュームを設置せざるをえなくなってしまいました。この場合、J4を差し忘れたり接触不良が起こった場合、出力電圧が最大値まで跳ね上がってしまうフェイルセーフではない設計となってしまっています。あまりそういうのは良くないでしょうが、かと言ってR15の位置にボリュームを付けると電源装置として使いにくくなってしまいますので、悩んだ末に安全を捨てました。安全を捨てたといっても、コネクタを差しさえすれば問題は無いですので、ある日突然…ということはまあまず無いでしょう。

また、コンセントからアース端子を基板に取り込めるようにしており、出力側はC9を介してアースに接続しております。この回路図には出てきていませんが、筐体に取り付けたACインレットはノイズフィルタ付きの製品を使っておりますので、厳密には1次側と2次側が絶縁された回路と言うわけではなくなってしまいます。
ただ、2次側の出力が完全に浮いているような設計にしてしまっても、回路である以上何かしらの容量で結合しています。そのようなよくわからない電位を設定するくらいなら、ちゃんとマイナス側をアースにコンデンサ結合させておいたほうが安心ですし安定もするはずです。



基板はこのような感じになりました。
基本的には1次側と2次側は分離しております。中央少し左のトランスとフォトカプラを境に1次側と2次側が分かれております。唯一茶色のレイヤーでそこをまたいでいるベタパターンがアースです。
スイッチング回路はノイズを出しますし、他から受けても困りますので、できるだけ機能ごとにまとめます。スイッチング制御回路は中央下側、フィードバック回路は左下側にまとめています。
スイッチング用トランジスタと出力の整流ダイオードは熱を発しますので、放熱板を設置できるような位置に配置します。また、電解コンデンサは熱でどんどん寿命が縮んでいくため発熱部品からは離して配置し…たいところなのですが、そうはいきませんでした…。

それにしても、KiCadの3D表示機能は圧巻ですね。発注した基板が到着するのがいつもより待ち遠しくなってきます。

製作

基板が届いたら基板への部品の実装と箱の加工を進めていきます。
今回、箱はタカチのTS-1というアルミケースを使ってみました。結構ギリギリでしたが、まあ何とか納めることができました。
それにしても、なかなか金属加工というものは骨が折れるものです。できれば工作機械でパパッと穴を開けたいところでしたね。そんないいもの私は持っていないので、ドリルと金ノコ、テーパーリーマとやすりでひたすら頑張りました。

先ほど少し言いましたが、ACの入力側にはノイズフィルタ付きのインレットを使っております。スイッチング電源は高周波で動作しますので、家庭のコンセント側に高周波ノイズをばらまいてしまわないようにとの配慮です。
基板からインレットまでの配線もできるだけツイストし、その配線から出る輻射ノイズもできるだけ抑えるようにしてあげましょう。
動作確認というほどたいそうなことでもありませんが、つまみを動かしたり適当な負荷をぶら下げてみて動作確認をします。
シミュレーションでは15V3A流れましたが、5Ω負荷をぶら下げると5V程度で出力がサチってしまいました。なかなか計算通りにはいかないものです。こういう時に電子負荷装置でも持っていると、負荷を変えながら特性を詳細に探ることができるのでしょうが。

調整

さて、電流が思ったほど出なかったので現物合わせでパラメーター調整をしていきます。
R5でC3を充電し、Q1のベース電圧が上がるとMOSFETをOFFする、という流れになりますので、充電時間を長くすればQ1がなかなかMOSFETをOFFしに行ってくれないのでたくさんの電力を取り出せるようになるはずです。
というわけで、R5を増やしていきました。増やしていったところ、7.5kΩにて短絡電流が3A程度になったので、ここで調整終了としました。 このように複雑なアナログ回路は計算通りに動いてくれないので、最後は現物合わせという形を取る必要が出てきます。

5Ω負荷で1.7A出力ですので、当初予定ほどの性能は出ませんでした、でも、短絡時にメーターが振り切れても困るのでまあこんなところで良いでしょう。これくらいの出力が取れれば、まあ趣味の電子工作程度ではそうそう困ることは無いかと思いますし。

おまけ


消費電力計を買ってきて作ったスイッチング電源の効率を測ってみました。

もちろん負荷によって変わりますが、高出力状態の時でおおよそ75%程度です。もうちょい、実用的には8割以上は行きたいところですが、まあ適当設計なのでこんなもんでしょう。
力率は0.57程度と低めです。というのも、スイッチング電源は入力段にダイオードブリッジとコンデンサでの整流回路を備えていますが、そのような整流回路は電圧がピークになった周辺でしか電流が流れないため電流波形が大きく崩れます。そのため力率は低く、一般的にこれくらいになるそうです。
フライバックコンバータ程度だと問題ありませんが、比較的大きな負荷だと電力系統にダメージを与えてしまいますので、整流回路を制御して力率を改善する回路を入れたりするそうです。


また、出力が2V前後では結構出力リプルが大きくなってしまいました。これはまあフィードバック回路の性能ですので、シャントレギュレーター周りのフィルタを調整することで改善できるのかもしれませんが、基板パターンを改造しなければならないので今回は現状とします。なかなか広い範囲で出力電圧を安定させるというのは難しいのですね。

まとめ

さて、フルアナログのフライバックコンバータ型スイッチング電源を自作してみました。トランスの製作、回路の設計から実際の電源装置の製作までやり、計画していた性能には届きませんでしたが、なんとか実用レベルのものを作りあげることができました。
1個数百円するトランジスタを何個も焼きながら試行錯誤して、結構いろいろな勉強になりました。普段パソコンとかマイコンとかでプログラミングばっかりやっているときとは違う頭の使い方をしますし、アナログ回路って本当に生身で物理の世界に挑んでいる感じがありますのでまた別の面白さがあると思います。

さて、次は何作ろうかな。

2019年5月18日土曜日

フルディスクリートのフライバック・コンバータを解剖する

最近プログラミングらしいプログラミングのネタが無いので、もう割り切って一切プログラミング関係ないことを記事にしようと思います。

この前とある部品屋に行ったところ、900円の特価でスイッチング電源が売っていました。しかも基板を見るとICらしいICが付いていません。これは買っていろいろ調べるしかない…!
※買った数週間後、別のパーツ屋に行ったら500円で売ってました。ショック…。


というわけで買ってきました。
先に結論を書いてしまいますが、回路図はこのようになっておりました。


順に解剖していきます。

トランスの基本

さて、どこまで基本的なことを説明するか、という話になってしまいますが、トランスについて「1次側と2次側の巻数比で電圧が変換され、1次側から2次側へ電力が伝わる」程度の理解しかしていない人のために少しだけ原理的な話をしておきます。

トランスは同じコアの上に複数の巻線を巻いたものです。そのため、コア内の磁束を$\Phi$とすると巻線の両端の電圧は次式で表せます。\[V=-N\frac{d\Phi}{dt}\]何を迷うこともない、ただのファラデーの法則です。
ここで重要なのは、コアを共用しているため、トランスの全ての巻線はいつどの瞬間でも巻線比に応じた電圧が発生するということです。交流/直流とか正弦波/矩形波とか巻線に流れている電流とか何も関係ありません。コアの磁束の変化はどの巻線にとっても同じなので、理論上は巻線比$N$のみで電圧が決まります
※磁束の漏れとか巻線抵抗とかそういう話をしだすとその理論上の話からはずれていきますが、それは基本をマスターした人が考えてください。

さて、ではその磁束はどうやって生まれるのという話ですが、こちらはアンペールの法則になります。\[\oint_C \mathbf{H} \cdot d\mathbf{l}=I\]電流の周りには磁界が生まれるという法則です。
トランスのコア内の磁束を具体的に計算するのならばアンペールの法則と等価なビオ・サバールの法則を使うと良いでしょう。ただ、ここではあまり具体的な数字には興味が無いので計算は省略します。大事なのは「コイルに電流が流れると、それに比例した大きさの磁束がコア内に生まれる」ということです。言い方を変えれば「コア内に磁束があるということは巻線に電流が流れている」と言えます。
※アンペールの法則では「電流に比例した磁界が生まれる」とまでしか言っていません。磁界は通る材料の透磁率に比例した磁束密度を生み出し、磁束密度に断面積をかけると磁束となります。これらが合わさって初めて「トランスのコア内には電流に比例した磁束が生まれる」という説明になります。

ですので、トランスの1次側に電圧を印加した際の物理としては、
  1. 1次巻線に電圧が加わる 
  2. トランスのコア内に1次電圧に対応した磁束の変化が必要となる
  3. 1次巻線の電流が変化することで磁束の変化が発生する
  4. 2次巻線にはコアの磁束の変化に応じた電圧が発生する
  5. 2次側の負荷によって電流が流れた場合、その電流に応じた磁束がコア内に発生する
  6. 2次電流による磁束の変化によって1次電圧が変化するのは不都合(電圧源につながっている)なので、その磁束変化を打ち消す分の電流が1次側に流れる
という流れとなります(言葉での説明ですので受け取り方によっては多少因果関係がおかしく感じるかもしれません。気になった方は計算して納得してください)。

なお、この説明まででは直流でもトランスで電圧が変換できそうに見えてしまうと思います。実際ここまででの説明ではできてしまいます(電流はべらぼうに流れますが)。しかし実際はできません。
足りていないお話は「磁気飽和」です。電流が磁界を生み、磁界が磁性体の中を通ると磁束密度が生まれます。しかし、磁束密度の上限は材料によって決まっているのです。それより先はいくら磁界が大きくなっても磁束密度は増えません。すなわち、電流がある一定に達すると、それ以上電流が増えてもコイル両端の電圧が上がらなくなるどころかゼロになってしまうのです。ですので、トランスは磁気飽和が起こらない範囲で電流を調整しながら使わなければなりません。

フライバック・コンバータ

ところで、スイッチング電源と一口に言ってもいろいろな方式があります。今回買ってきたスイッチング電源はいわゆる「フライバック・コンバータ」と呼ばれる形式のスイッチング電源になります。比較的シンプルな回路で部品点数も少なくなり、数十Wクラスまでならこの方法で充分に対応できるため、電子工作レベルのスイッチング電源には多用されています。

主回路は次のような構成になっています。



トランスの●は極性を示すものですね。フライバックコンバータでは1次側と2次側で必ず逆極性となります。そのため、1次側に電流が流れているときに2次側に電流が流れるわけではないということに注意してください。


まずはトランジスタをONしたときです。トランス1次側には電源電圧と同じ電圧vがかかります。と同時に2次側にも巻線比nを掛けた電圧nvが出力されますが、ダイオードが付いているため電流が一切流れません。
となると、トランスはただのインダクタとして機能し、1次側の自己インダクタンスに従って線形的に電流が増えていきます。この時流れた電流はトランスのコアに磁気エネルギーとして蓄えられます。


続いてトランジスタがOFFすると1次側では電流が一切流れなくなります。しかし、磁束は連続的にしか変化できないので、2次側に電流を流すことでコアの磁束の変化を連続的にしようとします。すなわち、コアに蓄えられた磁気エネルギーが2次側に電流として放出されるのです。放出された電流はダイオードを通ってコンデンサに充電されます。


タイムチャートにするとこんな感じです。青が1次側、橙が2次側です。シンプルでわかりやすいですね。

発振回路

さて、フライバック・コンバータを実現するには、トランジスタを周期的にON/OFFしなければなりません。そのためには発振回路が必要となります。
複雑な回路やプログラムを持ったICやマイコンを使っても良いのですが(モダンな電源で高効率化を目指すのならばそういうのが必要なのでしょうが)、ここは基本的なアナログ回路での発振を実現しています。発振回路の基本は正帰還となります。


トランスが3巻線になりました。1次・2次でないものを「補助巻線」と定義します。
トランジスタがONしているとき、トランスの1次巻線には電源電圧が印加されます。そうすると補助巻線にも電圧が発生します。電圧が発生するとトランジスタのベースに電流が流れるためさらに1次巻線に電流を流す方向へ進みます。まさに正帰還ですね。
しかし、1次巻線に電圧がかかると電流は線形的に伸びていきます。一方で1次巻線の電圧は一定なので補助巻線に生まれる電圧も一定になります。そのためベース電流も一定です。バイポーラトランジスタはベース電流に対してコレクタ電流の流れる電流が決まりますから、いつか1次巻線の電流がコレクタ電流の上限に到達し、電流が制限されてしまいます。電流が上限に達し一定になると、コイルに流れる電流が変化しなくなりますから1次電圧が急激に下がります。そうすると補助巻線の電圧も下がり、トランジスタも急激にOFFします。まさに正帰還です。

ただ、これだけでは最初にトランジスタをONすることができません。
ですので、ONしてくれる程度の大きな抵抗を電源側に付けてあげます。


これで起動もバッチリ、後は上記の正帰還によって発振を続けていきます。

フィードバック回路

さて、これで発振しても出力電圧を安定化できません。毎周期ごとにトランスに蓄えたエネルギーを2次側のコンデンサに移すだけですので、負荷が無ければどんどんコンデンサの電圧は上がっていきますし、負荷があれば下がります。
すなわち、出力電圧を監視してスイッチングを止めたり動かしたりする必要が出てきます。


その回路を追加したのがこれになります。
トランスを使った電源回路の1つの大きなメリットとして、入力と出力が絶縁されるということが挙げられますが、そのメリットを生かしつつ出力を制御にフィードバックするにはフォトカプラを使います。
そのフォトカプラの手前にはシャントレギュレーターを設置しており、ここで出力が基準電圧より高いか低いかを判断します。基準電圧より高くなるとカソードーアノード間が通になり、フォトカプラが光ります。
フォトカプラが光ると今度は補助巻線に電圧が発生しているときにフォトカプラを経由して補助トランジスタのベースに電流が流れ、主トランジスタのベース電流を補助トランジスタで吸収してしまいます。そうすると主トランジスタがすぐさまOFFし、2次側へエネルギーを供給する量が減ります。これによって出力電圧が下がるのです。
出力電圧が下がり、フォトカプラがOFFするとまた通常のスイッチング動作が始まり、2次側へエネルギーを供給する状態になります。

細かい回路の工夫

ここまでが回路の基本的な動作の仕組みとなります。
ただ、これだけでは充分ではなかったり、逆に効率を上げるために他の工夫をしなければならなかったりして、さらに一ひねり二ひねりある回路を付け加えています。

共振によるサージ吸収

フライバック・コンバータの動作説明で「1次側の電流を瞬間的に切ることによって、磁束の連続性を維持するために2次側に電流が生まれる」と説明をしました。
理想的なトランスならばそれで問題ないのですが、現実はそう甘くありません。

トランスには「漏れインダクタンス」というものがあります。巻線が作る磁界がすべてコアの中を通ってくれれば良いのですが、どうしても一部漏れてしまいます。その漏れが他の巻線に入らなければ、ただの自己インダクタンスとなってしまいます。
自己インダクタンスということは、何が何でもその分の磁束は電流遮断時に自分で賄う必要があります。となると、それによって何とか電流を流そうとトランス1次側に非常に高い電圧が発生してしまいます。サージです。

スイッチング電源のサージ吸収はスナバ回路と呼ばれる回路で吸収することが多いのですが、そうするとそれがそのまま熱として捨てることになってしまいます。ちょっともったいないです。そこで、トランジスタに並列にコンデンサを入れることで、ここで自己インダクタンスに溜まっていたエネルギーを吸収することができ、さらにはトランスの起電力が無くなった後(=2次側からエネルギーを抜き取った後)に整流コンデンサにエネルギーを回生できます。



スイッチングのデューティー比調整

先の「発振回路」のところでフライバック・コンバータは補助巻線がそもそも正帰還なのでそれだけで発振できると書きました。しかし、それだけでは発振のパラメーター調整が難しいです。
そもそもフライバック・コンバータの発振におけるデューティー比は生命線です。ON時間が長すぎたりOFF時間が短すぎるとトランスが磁気飽和を起こして事故が起こりますし、ON時間が短かったりOFF時間が長いとエネルギーを効率的に2次側へ移せません。それを主トランジスタの飽和電流だけで制御するのは流石に無理があります。
また、上記の共振回路によるエネルギーの回生では、共振コンデンサのエネルギーが抜けきったタイミングを見てトランジスタをONしなければなりません。かなり高度なタイミング制御が必要なのです。

そこで、主トランジスタの飽和電流だけでスイッチング周期を決めるようなことは通常しません。万が一電流が流れ過ぎたとき(=磁気飽和する一歩手前の状態になったとき)に止めるための安全装置として機能します。
普段は副トランジスタがスイッチングのタイミングを決めることになります。


まず主トランジスタがONしているときですが、補助巻線は●側が+になりますので副トランジスタのベース手前のコンデンサがどんどん充電されていきます。そして、副トランジスタのしきい値電圧に達したときに主トランジスタはOFFします。


主トランジスタがOFFすると全ての巻線には逆起電力が発生しますので、今度は補助巻き線の●側が-になり、副巻線のコンデンサを放電していきます。コンデンサが放電されるとまた主トランジスタはONし、この動作を繰り返していきます。

ON時間とOFF時間を変えるために、間にはツェナーダイオードが入っています。ツェナーダイオードは順方向ではただのダイオード、逆方向ではツェナー電圧まで電流を流しませんので、ざっくり電流の向きによって抵抗が異なる素子として振る舞います。そのため、充電と放電の時間を変えることができるのです。

フィードバックの位相補償

出力電圧のフィードバック回路ですが、こちらもフィードバック回路ですので発振には注意しなければなりません。
位相補償はC61が担当しています。この容量を小さくすると負荷の変動に対する応答は早くなりますが、開ループ利得があるうちに位相が180°を超えてしまい発振します。大きくし過ぎると負荷変動に対する応答が遅くなります。

負荷調整

フィードバック回路が用意されているとはいえ、補助巻線に起電力が生じなくなった時点で起動抵抗を介して主トランジスタがONしてしまいます。主トランジスタがONすると補助巻き線に起電力が生じ、出力電圧が過剰ならばスイッチングを止めてくれます。
しかし、このサイクルの間にやはり少しのエネルギー移動が発生します。
この際、負荷がほとんど無いと全然そのエネルギーを消化できずにどんどんコンデンサに溜まっていきます。最もスイッチングがされていない条件でもエネルギー供給量が多すぎるのです。これは出力過電圧を起こしますので、割と忌々しき問題です。

上のスイッチング電源では、そのため68Ωという比較的小さな抵抗を出力段に入れています。ここに常時100mA弱の電流を流し、アイドル状態で伝わるエネルギーを消費し過電圧にならないように調整しているのです。
負荷が予めはっきりしていればこの抵抗は不要という判断もできますが、無負荷で使えない電源というのも分が悪いですね…。

その他

出力段のコイルはリプル調整用とか、入力段のコイルはコモンモードノイズフィルタとか、その辺の話は良いですよね。気になる人は自分で調べてください。

まとめ

フルディスクリートのスイッチング電源の回路を調べ、紐解いていきました。
特別なICを積まなくてもLCRといくつかの能動素子のみでスイッチング電源が作れてしまうことがわかりました。面白いですね。と同時に、そのようなシンプルな回路でも安全かつ効率よく動くために様々な工夫が凝らされていることもわかりました。

ここまで回路を見ると自分でも作ってみたくなっちゃいますよね!
スイッチング電源用のトランスは市販されていない(回路によって必要なパラメーターが大きく変わるため汎用品として売れない)ため自分で設計して、それに応じて回路のパラメーターを調整していく必要があります。もちろん部品の入手性の問題も絡んできますし、定性的に原理を理解しても、実際に回路パラメーターをどうするのかというのは一筋縄では決められません。
ある程度は計算で方針を決められても、その次にはコンピューターで回路シミュレーションをしなければなりませんし、それができれば次は試作をし、パラメーターの最終調整をしていかなければなりません。

その辺の話はまた別の機会に。

2017年3月6日月曜日

aitendoのリチウムポリマー電池を使ってみる

お久しぶりです。

最近、ふと思い立ってリチウムポリマー電池を買ってみることにしました。
リチウムポリマー電池とは、 リチウムイオン電池の一種です。電池の電解質にポリマー高分子を使っているため、リチウムイオン電池に比べて比較的安全なようです。

リチウムイオン電池の最大の魅力と言えば、そのエネルギー密度です。ニッケル水素電池などと比べると、非常に小型軽量で高容量の電池が作れるため、最近のパソコン、スマートフォン等の持ち運び性が重要な電子機器の電池はたいていリチウムイオン電池ですよね。

しかし、リチウムイオン電池はそのエネルギー密度の大きさや電解液が可燃性などの理由から、ニッケル水素電池などに比べてかなり危険です。よく燃えます。最近ではSamsung Galaxy Note 7のバッテリー発火事故は記憶に新しいですし、そのちょっと前にはボーイング787のバッテリー発火事故なんていうのもありました。



特に過充電は非常に危険で、上記の動画のように勢いよく火を噴きます。

過充電による発火メカニズムなどの詳しいことは省きますが、このような事故を起こさないためには充放電をしっかり管理してあげる必要があります。

というわけで、世の中にはリチウムイオン電池の充電を管理するICというのも出回っています。それを使えば、下手に自分で回路を作ったりプログラムで制御するより安全にリチウムイオン電池が扱えるでしょう。

比較的入手性の良さそうなリチウムイオン電池(リチウムポリマー電池)と充電制御モジュールは、aitendoで売っています。
上に挙げたのは300mAhの電池ですが、aitendoでは3種類(110/300/850mAh)のリチウムポリマー電池を売っているようです。

ちなみに、実際に買ってみて驚いたのですが、この電池思っていた以上にコンパクトでした。すげえ。


さて、肝心の充電制御モジュール「CHR4056-MCU1A」ですが、aitendoのサイトには具体的なことが「TP4056使用」としか書いてなくて、「過電流出力保護チップ搭載」と言うもののその具体的な型番も書いていなければ、モジュールの回路図も使い方も書いていません。さすがaitendoクオリティとでも言うところでしょうか…。せっかくですので、買ってみて回路図を起こしてみました。



たぶんこんな感じです。コンデンサの容量はわかりませんが、容量が重要なコンデンサはあまりなさそうです。

TP4056の周辺はTP4056でググるとよく出てくる感じの回路です。電源5Vとバッテリー陽極の間に入って充電電流をコントロールしてくれます。データシートを見ると詳しく書いてありますが、電圧が低いうちは定電流制御をし、電池の電圧が4.2Vに達すると定電圧制御をしてくれるようです。
定電流制御をする電流はR3で決まります。最大1Aで、その時の抵抗値は1.2kΩになります。私はUSBを想定して400mAの3kΩに取り換えました。

さて、問題の過電流出力保護についての回路です。
これには2つのチップが使われていますが、DW01というICのほうがその頭脳になります。FS8205AはただのFETです。
ただのFETではあるのですが、2つFETが、しかもドレイン同士がつながっているへんてこな回路になっています。ここでポイントになってくるのがこのFETに存在する寄生ダイオードです。DW01がOC端子をHigh(>Vth)、OD端子をLow(<Vth)出力すると、上記回路図の左側のFETがONになり、右側のFETはOFFとなります。しかし、寄生ダイオードを経由して2,3番ピンから6,7番ピンへの電流ならばは流すことができます(逆方向は無理)。すなわち、充電する方向のみへの電流を流すことができる状態になっていると言えます。
逆にOCをLow、ODをHighとした場合は放電する方向のみに電流を流せまし、両方Highにすれば両方向へ電流が流せます。もちろん両方Lowにすれば両方向電流を止められますよね。なかなか面白い回路です。

OCのみHighになった場合の電流方向

充電や放電を止めたり許可したりする方法はわかりました。では、どうやって電流を検出しているのでしょうか。
ヒントはCS端子にありました。
FETにはON抵抗がありますから、両方のFETがONになっていると仮定すると、CS端子は放電時にはGND+2×R_on×Iの電圧がかかります。この電圧を測ることで過電流を検出しているようで、GNDとの電位差が150mV以上を10ms、または1.35mVを300usで電流を遮断するように作ってあるそうです。
データシートを見るとFS8205AのR_onは20mΩ程度なので、およそ3.75Aの電流で過電流検知をしてくれる、といったところでしょうか。

ついでに、このICはVCCを測ることで過充電と過放電も監視してくれているようです。4.25Vで過充電検知、2.40Vで過放電検知をするようです。
なかなかに賢いICですね。

ちなみにですが、R5とC2の回路はリプル除去(ローパスフィルター)が目的のようです。負荷変動などで電池の出力電圧が変わっても、DW01に与える電源電圧の変化をできるだけ抑えるようにするために付けていると解釈できます。


なかなか素晴らしいモジュールだということがわかりました。
リチウムイオン電池をいじってみたければ、とりあえず電池とセットでこのモジュールを買うとよいでしょう。

しかし、自分の回路に組み込もうとなると、モジュールをそのまま載せたのでは見栄えが悪くなりますよね。あと重要なことなのですが、このモジュールのOUT+、B+などの端子は2.54mmピッチではありません。ピンヘッダなどを付けてユニバーサル基板やブレッドボードに取り付けようと思ってた皆さん、残念でした。

そうすると、皆さんは自前でカッチョイイ充電モジュールを作ろうと考えますよね。大丈夫です、私もそう考えます。ですが、TP4056DW01はaitendoで見つかりますが、FS8205Aはなかなか扱っている場所は無さそうです。
そんなあなたに、代替としてBR8205というFETモジュールがaitendoで売っているのでオススメしておきます。FS8205Aと同じようにFETが縦に2つつながったもので、使用目的も同じく充電制御に使うためのもののようです。R_onはFS8205に比べたらほんの少しだけ高そうですが、まあ問題になるほどではないでしょう。ピン配置やパッケージが違うことにさえ注意すれば、十分に使えそうです。


最初説明した通り、リチウムイオン電池はその容量密度は非常に魅力的であるものの、扱いを誤ると発火などの危険があり、どうしても敷居が高い電池です。

今回は、aitendoの充電制御モジュールを解剖することによって、リチウムイオン電池の充放電管理の回路やICを学びました。これをうまく使いこなせば、より電子工作ライフが充実してきそうです。

2015年1月8日木曜日

NTP時計のうるう秒対応

2015年7月1日(JST)にうるう秒が挿入されることが決まりました。

うるう秒実施日一覧

すなわち、JSTで1秒ごとに8:59:59→8:59:60→9:00:00と時間が進んでいくことになります。
これは、現在、国際的な標準時を決めるための原子時計と地球の自転速度のズレ(自転速度がそもそも一定ではないため)によって発生する時間差を調整するためのものです。これを導入しないと、だんだん朝、昼、夜と言った太陽の昇り沈みに関する感覚と時刻が対応しなくなってきてしまいます。
最近に導入されたのは2012年7月1日(JST) で、慣例で12/31と6/30の24時直前(UTC)すなわち、1/1と7/1の9時直前(JST)に挿入されることが多いようです。また、いまのところ1分が61秒になる追加パターンしか実施されたことがないないようです。

さて、このうるう秒ですが、対応している時計は8:59:60の表示がされるそうです。
しかし、例えばWindowsをはじめとして多くの時計は対応しておらず、うるう秒が過ぎた後の時刻合わせで正確な時刻に合わせられるというのが実情のようです。そもそもうるう年と違ってうるう秒は不定期に挿入されるものなので電波時計やNTP等の外部と通信しているものでなければ原理上うるう秒に対応させられませんし、ずれは1秒しかなく、日常においては些細な差でしかありません。そのため実装が進まないんでしょうね。
もちろん、今回のNTP時計もうるう秒に対応しておりません。うるう秒の後の同期で正常な時刻になるだけです(自動内蔵オシレーター調整機能があるのでそれが多少ずれると考えられますが、後に収束すると考えられます)。

ですが、せっかくのうるう秒なので、これを機に実装してみることにしました。

まず、うるう秒をどうやって知るかです。
NTP時計では、SNTPプロトコルでインターネットを介して時刻情報を仕入れているわけですが、実はこのSNTPプロトコルにはうるう秒を通知する仕組みがあります。

http://tools.ietf.org/html/rfc5905#page-20

Leap Indicator (LI)と呼ばれる2bitのフィールドがSNTPのパケットにあり、これを見ることでその日にうるう秒が挿入されているかどうかがわかります。00(2進数)のときにうるう秒無し、01(2進数)のときに1日の最後の1分が61秒になる、10(2進数)のときに1日の最後の1分が59秒になることを示しています。もちろんこれはUTCなので、JSTでは午前9時を境にこのフィールドが変わります。

ネットワ-クによる時刻情報提供サービス(NTPサ-ビス)のうるう秒対応

上記のリンクには、2005年末に挿入されたうるう秒に関するNTPのLIの動きとNTP時刻の動きが示されています。
2005/12/31 0:00:00(UTC)になると同時にLIが01になり、2006/1/1 0:00:00(UTC)にLIが00に戻っています。そして、2005/12/31 23:59:60と2006/1/1 0:00:00はUTCタイムが同じ値になっています。
ここには60秒から00秒への遷移の間の細かいことは書いていませんね。サーバーが使用しているプログラムによっては、NTP時刻が3345062400.99から3345062400.00に戻る『時間逆行』仕様になっているものもあるらしいですし、時間が逆行しないように時間の進みを遅めている仕様になっているものもあるらしいので、このあたりはNTPサーバーに問い合わせをしないほうが無難かと思います。もしも時間逆行仕様だったら、単にLIを23:59:60か0:00:00かの判定に使って秒未満の桁をそのまま採用することができますが、本来のNTPの仕様では逆行しないようにするように言われている(まあ物理に沿っていると言えばこっちのほうが正しいですよね)ようなので、このあたりは本当に微妙なところかと思います。


さて、それでは実装の話に移ってみましょう。

typedef enum _tagLeapIndicator {
    LI_NoWarning = 0,
    LI_Increase = 1,
    LI_Decrease = 2,
} LeapIndicator;

まずはなんといってもLeapIndicatorの定義です。NTPパケットのビットフィールドにそのまま連動して、このような列挙型を作ってあげました。
次に、受信パケットについてLIの処理を追加します。(今まではこのようになっていました)

case SM_UDP_RECV:
// Look for a response time packet
if(!UDPIsGetReady(MySocket)) 
{
    if((TickGet()) - dwTimer > NTP_REPLY_TIMEOUT)
    {
        // Abort the request and wait until the next timeout period
        UDPClose(MySocket);
        //dwTimer = TickGetDiv64K();
        //SNTPState = SM_SHORT_WAIT;
        SNTPState = SM_HOME;
        MySocket = INVALID_UDP_SOCKET;
        break;
    }
    break;
}

// Get the response time packet
w = UDPGetArray((BYTE*) &pkt, sizeof(pkt));
UDPClose(MySocket);
dwTimer = TickGetDiv64K();
SNTPState = SM_WAIT;
MySocket = INVALID_UDP_SOCKET;
bForceSync = FALSE;

// Validate packet size
if(w != sizeof(pkt)) 
{
    break;    
}

// Set out local time to match the returned time
dwLastUpdateTick = TickGet();
dwSNTPSeconds = swapl(pkt.tx_ts_secs) - NTP_EPOCH;
// Do rounding.  If the partial seconds is > 0.5 then add 1 to the seconds count.
if(((BYTE*)&pkt.tx_ts_fraq)[0] & 0x80)
    dwSNTPSeconds++;

{
    QWORD now,  qwReceive, qwTx, qwDelay, qwPeriod;
    
    now = MillisecondToNTPTimestamp(SNTPGetUTCMilliseconds());
    qwPeriod = now - qwLastUpdateNTPTimestamp;
    if((liLastUpdate != LI_Increase) && (pkt.flags.leapIndicator != LI_NoWarning))
        qwPeriod += (QWORD)1 << 32;
    else if((liLastUpdate != LI_Decrease) && (pkt.flags.leapIndicator != LI_NoWarning))
        qwPeriod -= (QWORD)1 << 32;

    qwLastUpdateTick = TickGetQWord();

    qwReceive = PacketToNTPTimestamp(pkt.recv_ts_secs, pkt.recv_ts_fraq);
    qwTx = PacketToNTPTimestamp(pkt.tx_ts_secs, pkt.tx_ts_fraq);
    qwDelay = (now - qwLastSendNTPTimestamp - (qwTx - qwReceive)) / 2;

    qwLastUpdateNTPTimestamp = qwTx + qwDelay;

    if(qwPeriod > 0) {
        dOscillatorError = (double)((LONGLONG)(now - qwLastUpdateNTPTimestamp)) / qwPeriod;
        dwSyncCount++;
    }
}

Stratum = pkt.stratum;
liLastUpdate = pkt.flags.leapIndicator;

break;

主に改造したのは最後のブロック部分です。
まず、同期周期のqwPeriodですが、今まではうるう秒を何も考慮しておりませんでした。今回の修正では、以前の同期でLIが1秒増しで今はLI無しだった場合、以前の同期以降にうるう秒が挿入されたと言えるのでqwPeriodに1秒を足しています。同様に、LIが1秒減を示していたら1秒減らす処理をしています。
あとは、最後にliLastUpdateとして、pkt.flags.leapIndicatorを保存してあげています。

次は、LIを外から取得できるように、そういったメソッドを作ってあげます。

LeapIndicator GetLeapIndicator()
{
    QWORD lastupdate = SNTPGetLastUpdateUTCMilliseconds();
    QWORD delta = GetDeltaMs();

    switch(liLastUpdate) {
     case LI_NoWarning:
         return LI_NoWarning;
     case LI_Increase:
        if((lastupdate / 86400000) != ((lastupdate + delta - 1000) / 86400000))    //Now leap second has already passed.
            return LI_NoWarning;
        else
            return liLastUpdate;
     case LI_Decrease:
        if((lastupdate / 86400000) != ((lastupdate + delta + 1000) / 86400000))    //Now leap second has already passed.
            return LI_NoWarning;
        else
            return liLastUpdate;
    }
    return liLastUpdate;
}

BOOL IsNowLeapSecond()    //Now Leaping 23:59:60
{
    if(liLastUpdate == LI_Increase) {
        QWORD now = SNTPGetLastUpdateUTCMilliseconds() +  GetDeltaMs();

        if((now / 86400000) != ((now - 1000) / 86400000))    //Now leap second
            return TRUE;
    }
    return FALSE;
}

GetLeapIndicator()のほうですが、これは単にそのまま保存したLIの値を返してしまうと、直前のNTPサーバーとの同期は0:00:00(UTC)以前なのに今は0:00:00(UTC)過ぎという時刻のときに、その日は別にうるう秒があるわけではないのにLIが値を持ってしまうことがあります。なので、0:00:00(UTC)を過ぎたかどうかを判定して、そうでない時のみLIを返すようにしています。
じゃあ逆にうるう秒の日になったとき、うるう秒の日の前日の情報が残っててLIがNoWarningのままないんじゃね?という発想も当然出てくるかと思います。ですが、そういった場合はNTPサーバーに問い合わせない限りわかりませんし、別にそのタイミングはうるう秒が挿入されるわけでもなんでもないのでそんなに大した問題じゃないですね。

IsNowLeapSecond()は、現在うるう秒かどうかを調べるメソッドです。すなわち、23:59:60(UTC)の1秒間のみTRUEを返すメソッドです。

QWORD SNTPGetUTCMilliseconds(void)
{
    QWORD now = SNTPGetLastUpdateUTCMilliseconds() + GetDeltaMs();

    if(GetLeapIndicator() != liLastUpdate) {
        switch(liLastUpdate) {
         case LI_Increase:
            return now - 1000;
         case LI_Decrease:
            return now + 1000;
         default:
            break;
        }
    }
    return now;
}

つづいて、UTCの積算ミリ秒を取得するメソッドです。
GetLeapIndicator()は先ほど述べた通りすでにうるう秒を過ぎていたらLI_NoWarningを返しますが、liLastUpdateはNTPサーバーと同期しない限り更新されません。なので、この2者が異なる=うるう秒が過ぎてからNTPサーバーと同期されるまでの間ということになります。その場合はうるう秒に応じて1秒足したり引いたりした値を現在時刻として返しています。

さて、UTCミリ秒を表示するとき、うるう秒で1秒減のときは単に23:59:59(UTC)がスキップされるだけですので全然問題がありません。しかし、1秒増のときは23:59:60(UTC)が挿入されるので通常の手法では表示できませんよね。というわけで、そのあたりの実装をしました。

void UTCMillisecondToLocaltime(QWORD utcms, int diffmin, struct tm *pTime)
{
    if(pTime != NULL) {
        BOOL IsLeap = IsNowLeapSecond();

        if(IsLeap)
            utcms -= 1000;

        time_t tick = ((DWORD)(utcms / 1000) + diffmin * 60) & 0x7FFFFFFF;
        struct tm *ptm = localtime(&tick);
        memcpy(pTime, ptm, sizeof(struct tm));

        if(IsLeap)
            pTime->tm_sec = 60;
    }
}

diffminはUTCとのずれの分です。JSTはUTC+9なので、9 * 60を渡してもらうということになります。
1秒増のうるう秒のとき、IsLeapがTRUEになります。そのときにはNTP的には翌日0:00:00扱いになるので、とりあえず1秒引いて23:59:59にした上でlocaltime()を呼んでいます。そして、その後にstruct tmのtm_secメンバーを60にして23:59:60を示すようにしました。


これでうるう秒周りの実装が終わりました。





このように、見事にうるう秒で増える場合、増えない場合の動作ができました。

ちなみに、おまけとしてLIの値を表示する機能も追加しておきました。



さて、これで今年の7月1日が楽しみですね。

2014年7月15日火曜日

PIC24FJ64GB002でUSBメモリーにアクセスする

最近、秋月電子でPIC24FJ64GB002の取り扱いが始まりました。
USB-OTGに対応したPICとしてはかなり有名どころで、多くの書籍やウェブページでも取り上げられており、資料は豊富にあります。にもかかわらず入手性は最悪で、国内の通販では共立電子くらいでしか取り扱っていなくて、さもなくばDigiKeyとかで海外から取り寄せるくらいしか方法はありませんでした。
しかし、ついに秋月電子で取り扱いが始まりました。秋葉原に行くだけで買える!しかも、共立電子の半額以下という破格の値段です。

というわけで、早速買ってきました。

 
手前がPIC24FJ64GB002で、奥がPIC32MX250F128Bです。
この2つ、前者は16bitマイコン、 後者は32bitマイコンでアーキテクチャも何もかも全く違うのかなと思ったら、なんとまあピンコンパチ(多分)でした。少なくとも電源やUSB周りは同じで、PIC32MXのほうをいじってみたときに使った回路をそのまま転用することができました。
というわけで、そのPIC32MXをいじったときに最終的にいきついたFatFsを動かすお話に用意したプログラムをPIC24FJ64GB002に移植してみました。


と言ってもほとんど苦労はしません。

そもそもこのデモプログラムはPIC24FJ64GB004でも動くように作られてて、多分これと002の違いはピン数とかパッケージとかその程度です。また、FatFsは完全にデバイス非依存で、型の大きさとかに気をつけておく程度でちゃんと動作はしてくれるはずです。というわけで、プログラムの修正はほとんど必要ありません。


まずは前回のプロジェクトのプロパティからDeviceをPIC24FJ64GB002に変更します。
…と言いたいところですが、PIC32MXのほうの設定を消すのはもったいないです。かと言ってバックアップとってゴニョゴニョやるのもめんどくさい。というわけで、プロジェクトのコンフィグレーションを追加してしまいましょう。MPLAB Xのプロジェクトはコンフィグレーションと呼ばれるものを複数作っておくことで、同じ構成のプログラムを別のデバイスに簡単に移植できるようになっているようです。もちろん、コードがそれに対応していたらの話ですが。

プロパティのカテゴリーの欄の下にある「Manage Configurations...」というボタンを押すとこんな画面が出てきます。


Duplicateで既存のコンフィグレーションをコピーできますが、まあコピーしてもコンパイラが違うとコンパイラの設定が全部吹っ飛んじゃうんであまり意味無いでしょう。ということで、Newを押して、適当な名前のコンフィグレーションを作ります。そして、Set Activeボタンを押せば、そのコンフィグレーションがアクティブになります。


そして、作ったPIC24FJ64GB002のほうのコンフィグレーションに対して、設定をしていきます。デバイスが未設定なので、右上のデバイスの欄からPIC24FJ64GB002を選択します。そして、コンパイラをXC16に設定します。そして前回同様、インクルードファイルのパスとヒープ領域の容量を設定してあげます。


次に、ソースコードの修正を若干します。修正箇所は、コンフィグレーションビットの設定とマイコンの初期化のプログラムのみです。

とは言っても、デモプログラムのほうにPIC24FJ64GB004のコードが入っているので、プリプロセッサの__PIC24FJ64GB004__を__PIC24FJ64GB002__に変更してやるだけです。というより、#if definedで追加してやる形にしました(具体的なコードはMicrochipの著作物なので掲載は遠慮しておきます。各自MLAのサンプルコードからコピーしてください)。

#if defined(__PIC24FJ64GB002__) || defined(__PIC24FJ64GB004__)
    //中略
#elif defined( __PIC32MX__ )
    //中略
#else
    #error Unsupposed Processor
#endif

コンパイルは、クリーンビルドをしてください。PIC32MXのほうの何かが残っているとリンクで謎のエラーが起きて焦ります。

そして、書き込みが終わればUSBメモリーを挿して、めでたくサンプルファイルの書き込みがされて完成です。

めでたしめでたし。



ここまで書いてなんですが、値段で見ると、PIC24FJ64GB002が340円、 PIC32MX250F128Bが360円です。20円差でより高性能なPICが買えるなら、別にPIC32MXのほうでいいんじゃないかなーって気はします。
まあでも最初に言った通り、PIC24のほうは資料の豊富さは格別です。気が向いたらBluetooth Stackとかに手を出してみようかなー。