環境光センサーのMAX44009を使ったLCD輝度制御の単純な実装

要約

このアプリケーションノートでは、スマートフォンやタブレットコンピュータなどのポータブルアプリケーションにおけるバックライト制御用環境光センサーのMAX44009を使った実装について説明します。バックライト輝度の調整に2つの異なる制御方式を紹介します。このアプリケーションノートでは、性能向上の追加のヒント、およびこの記事で考察したアルゴリズムを実装するサンプルコードを提供します。

概要

環境光センサー(ALS) ICは、節電やユーザー体験の向上のために、幅広いディスプレイと照明アプリケーションでますます使用されています。ALSソリューションの場合、システム設計者は環境光の量に基づいてディスプレイの明るさを自動的に調整することができます。バックライトはシステムの電力バジェット面の大きな部分を占めるため、ダイナミックな輝度制御は大きな節電が可能になります。また、これは、ユーザー体験も向上することができるため、環境光の状態に基づくスクリーン輝度(明るさ)の最適化が可能です。

そのようなシステムの実装には、環境光量監視用の光センサー、データ処理用のデバイス(通常はマイクロコントローラ)、およびバックライトからの電流制御用のアクチュエータの3つのセクションが必要です。

バックライト制御:環境光センサー

図1は、バックライト制御を実装したシステムのブロック図例を提供します。光センサーは、環境の光レベルに関する情報をシステムの他の部分に提供するため、このセットアップの主要部分です。光センサーは、光を電気信号に変換するトランスデューサ(フォトダイオードまたはCdSフォトレジスタ)、何らかの増幅および/または信号調整、およびアナログ-デジタルコンバータ(ADC)を備えている必要があります。

図1. バックライト制御を実装したシステムのブロック図

図1. バックライト制御を実装したシステムのブロック図

図2は、フォトダイオード回路のディスクリート実装を示しています。見てわかるように、この回路は、1つ以上のオペアンプが必要で、1つはI-V変換用、おそらくもう1つは追加の利得用です。また、この回路は、これらの全部品に給電し堅牢な信号チェーンを確保するための特別なルーティングも備えています。スペースが貴重なアプリケーションでは、必要な部品数が多いと問題となる可能性があります。

図2. フォトダイオード回路のディスクリート実装

図2. フォトダイオード回路のディスクリート実装

次に、2番目のより微妙な問題があります。特に、望ましいのは環境光が人間の目の光応答を再現する方法で測定されるようにすることです。これは多くの場合、CIE明順応曲線(図3)で記述されます。しかし、フォトダイオードは、赤外線(IR)感度が大きい場合が多いため、この応答をほとんど再現することはありません。この感度によって、白熱電球や太陽からのようなIRが多い光の下では、読取りの誤りが発生します。

これに対する1つの方法は、2つのフォトダイオードを使用することで、1つは可視光線 + 赤外線成分、もう1つは赤外線成分のみを包含させます。こうすると、2つの応答を互いに減算して、可視光線成分のみを取得することができ、赤外線部分が最小限に抑えられます。

このソリューションは、効果的ですが、上述したディスクリート回路で追加スペースが必要です。また、赤外線干渉の除去に十分なほど緊密にディスクリートフォトダイオードをマッチングさせることは、不可能でないにしても、非常に困難です。ダイナミックレンジは、ログアンプなどのアンプの非常に高度な実装がないと制限されると予想されます。そのようなセットアップでは再現可能な結果を得ることは困難です。

図3. CIE曲線 対 標準フォトダイオード

図3. CIE曲線 対 標準フォトダイオード

集積ソリューションは、人間の目の光応答にはるかに忠実な光読取りをもたらすだけでなく、多くのスペースも節約します。環境光センサーのMAX44009 などのデバイスは、すべての信号調整とA/D変換回路をスモールファクタフォーム(2mm x 2mm UTDFN)に集積して、スペースに制限のあるアプリケーションでかなりのボード面積を節約します。

図4は、MAX44009のファンクションブロックダイアグラムを示します。このデバイスは、マイクロコントローラとの高速で単純なインタフェース方法を可能にするI²C通信プロトコルを使用しています。これに加え、このソリューションの集積特性によって、このデバイスをフレックスケーブル上に配置して、メイン回路基板から離れた希望の位置に設定することができます。

図4. MAX44009のファンクションブロックダイアグラム

図4. MAX44009のファンクションブロックダイアグラム

バックライト制御:スクリーン輝度の変調

この制御方式の2番目の部分には、スクリーン上のバックライトの変化に作動することが含まれます。これは、アプリケーションで使用されるスクリーンモジュールに応じて、多くの方法で行うことができます。2つの最も簡単な方法は、パルス幅変調(PWM)を介した直接的な方法、またはスクリーンコントローラチップを使った間接的な方法です。

多くのディスプレイモジュールは現在、コントローラを内蔵しており、ユーザーはシリアルコマンドをデバイスに送出することによって輝度を直接設定することができます。しかし、これが利用できない場合は、画面の背後でバックライトを提供する一連の白色LEDに供給される電力を制御することによって、簡単なバックライト制御アクチュエータを実装することができます。これを実装する1つの粗雑な方法は、FETをLEDと直列に配置して、PWM信号を使って瞬時にスイッチオン/オフする方法です(図5)。しかし、これは、1つのチップ、すなわちLED用のステップアップ電流レギュレータのMAX1698を使えば、より簡潔で堅牢な方法で行うことができます(図6)。この実装に関する詳細については、アプリケーションノート3866 「Low-power PWM output controls LED brightness」を参照してください。

図5. 単純なPWM制御回路

図5. 単純なPWM制御回路

図6. MAX1698ベースのLEDレギュレータ

図6. MAX1698ベースのLEDレギュレータ

バックライト制御:ギャップのブリッジ

T最後のステップは、センサーとアクチュエータの間のギャップをブリッジすることで、これはマイクロコントローラ内で行われます。最初の質問として、「どんな方法で環境光をバックライト輝度にマッピングするか?」と聞かれるでしょう。実際、これを行う方法について説明したさまざまな仕様が存在しています。マッピングの一例は、Microsoft®がWindows® 7を実行するコンピュータ用に推奨しているものです¹。図7の曲線は、環境光レベルを完全輝度の比率(%)としてスクリーン輝度にマッピングするために、Microsoftによって提供されたものです。

図7. 環境光レベルを最適なスクリーン輝度にマッピングする輝度曲線の例

図7. 環境光レベルを最適なスクリーン輝度にマッピングする輝度曲線の例

この特定の曲線は、次のような関数によって記述することができます。

Eq01.

アプリケーションが輝度制御内蔵のLCDコントローラチップを利用している場合は、輝度は、希望する値でコマンドをチップに送信することによって容易に設定することができます。アプリケーションがPWMを使って輝度を直接制御している場合は、比率信号を輝度にマッピングする方法について考慮する必要があります。

MAX1698の例では、データシートに説明されているように、駆動電流を電圧にマッピングすることができます。その場合、多くの場合、LEDの電流がほぼリニアに強度と相関していると仮定することができます。したがって、PWMの実効電圧へのマッピングを要因とする上記の式に定数を乗算することができ、その後、LED電流にマッピングされて、スクリーン輝度に変換されます。

実装に関する注記

1つの設定値から別の設定値に直接ジャンプしないのがベストです。むしろ、バックライト輝度は滑らかにランプアップ/ダウンさせて、レベル間のシームレスな遷移を確保する必要があります。これをベストな方法で行うには、LEDからの電流の制御に使用されるPWM値またはディスプレイコントローラチップに送出されるシリアルコマンドのいずれかを徐々にシフトする固定または可変の輝度ステップサイズでタイミングされた割込みを使用します。図8は、そのようなアルゴリズムの例を提供します。

図8. 輝度をステップするアルゴリズムの例

図8. 輝度をステップするアルゴリズムの例

もう1つの懸念は、どの程度の速さでシステムが環境光レベルの変化に応答すべきか、の問題です。輝度レベルをあまり高速に変化させないようにする必要があります。この懸念は、光(窓やランプのそばを通るなど)の過渡変化によって、バックライト輝度に不要な変化が発生する可能性があり、イライラさせられるユーザーもいることです。また、より遅い応答時間を使用すれば、光センサーを繰り返しポーリングする必要が減り、一部のマイクロコントローラのリソースが解放されます。

基本的方法は、1、2秒ごとに光センサーにポーリングした後、輝度を変化させることです。より良い方法は、特定の時間の間、光レベルが一定範囲から離れたときにのみ輝度を変化させることです。たとえば、現在の光レベルが200luxの場合、数秒より長く、光レベルが180lux以下に低下したとき、または220lux以上に上昇したときにのみ、輝度を変化させることができます。幸いなことに、MAX44009は、割込みピンとスレッショルドレジスタを備えており、これを非常に容易に行うことができます。

付録:サンプルコード

#define MAX44009_ADDR	0x96
// begin definition of slave addresses for MAX44009
#define INT_STATUS	0x00
#define INT_ENABLE	0x01
#define CONFIG_REG	0x02
#define HIGH_BYTE	0x03
#define LOW_BYTE	0x04
#define THRESH_HIGH	0x05
#define THRESH_LOW	0x06
#define THRESH_TIMER	0x07
// end definition of slave addresses for MAX44009

extern float SCALE_FACTOR;	// captures scaling factors to map from % brightness to PWM
float currentBright_pct;	// the current screen brightness, in % of maximum
float desiredBright_pct;	// the desired screen brightness, in % of maximum
float stepSize;			// the step size to use to go from the current 
				// brightness to the desired brightness
uint8 lightReadingCounter;	

/**
 *	Function:	SetPWMDutyCycle
 *
 *	Arguments:	uint16 dc - desired duty cycle
 *
 *	Returns:	none
 *
 *	Description:	Sets the duty cycle of a 16-bit PWM, assuming that in this 
 *			architecture, 0x0000 = 0% duty cycle
 * 			0x7FFF = 50% and 0xFFFF = 100%
**/
extern void SetPWMDutyCycle(uint16 dc);

/**
 *	Function:	I2C_WriteByte
 *
 *	Arguments:	uint8 slaveAddr - address of the slave device
 *			uint8 command - destination register in slave device
 *			uint8 data - data to write to the register
 *
 *	Returns:	ACK bit
 *
 *	Description:	Performs necessary functions to send one byte of data to a 
 *			specified register in a specific device on the I2C bus
**/
uint8 2C_WriteByte(uint8 slaveAddr, uint8 command, uint8 data);

/**
 *	Function:	I2C_ReadByte
 *
 * 	Arguments:	uint8 slaveAddr - address of the slave device
 *			uint8 command - destination register in slave device
 *			uint8 *data - pointer data to read from the register
 * 
 *	Returns:	ACK bit
 *
 *	Description:	Performs necessary functions to get one byte of data from a 
 *			specified register in a specific device	on the I2C bus
**/
uint8 I2C_ReadByte(uint8 slaveAddr, uint8 command, uint8* data);

/**
 *	Function:	getPctBrightFromLuxReading
 *	
 *	Arguments:	float lux - the pre-computed ambient light level
 *
 *	Returns:	The % of maximum brightness to which the backlight should be set 
 *			given the ambient light (0 to 1.0)
 *
 *	Description:	Uses a function to map the ambient light level to a backlight 
 *			brightness by using a predetermined function
**/
float getPctBrightFromLuxReading(float lux);

/**
 *	Function:	mapPctBrighttoPWM
 *
 *	Arguments:	float pct
 *
 *	Returns:	PWM counts needed to achieve the specified % brightness (as 
 *			determined by some scaling factors)
**/	
uint16 mapPctBrighttoPWM(float pct);

/**
 *	Function:	getLightLevel
 *
 *	Arguments:	n/a
 *
 *	Returns:	the ambient light level, in lux
 *
 *	Description:	Reads both the light registers on the device and returns the 
 *			computed light level
**/
float getLightLevel(void);

/**
 *	Function:	stepBrightness
 *
 *	Arguments:	n/a
 *
 *	Returns:	n/a
 *
 *	Description:	This function would be called by an interrupt. It looks at the 
 *			current brightness setting, then the desired brightness setting. 
 *			If there is a difference between the two, the current brightness 
 *			setting is stepped closer to its goal.
**/
void stepBrightness(void);

/**
 *	Function:	timerISR
 *
 *	Arguments:	n/a
 *
 *	Returns:	n/a
 *
 *	Description:	An interrupt service routine which fires every 100ms or so. This 
 *			handles all the ambient light sensor and backlight
 *			control code.
**/
void timerISR(void);

void main() {
	
	SetupMicro();				// some subroutine which initializes this CPU
	
	I2C_WriteByte(MAX44009_ADDR, CONFIG_REG, 0x80);	// set to run continuously
	
	lightReadingCounter = 0;
	stepSize = .01;
	currentBright_pct = 0.5;
	desiredBright_pct = 0.5;
	SetPWMDutyCycle(mapPctBrighttoPWM(currentBright_pct));
	InitializeTimerInterrupt();		// set this to fire every 100ms
	
	while(1) {
		// do whatever else you need here, the LCD control is done in interrupts
		Idle();
	}
} // main routine

// the point at which the function clips to 100%
#define MAXIMUM_LUX_BREAKPOINT	1254.0
float getPctBrightFromLuxReading(float lux) {
	if (lux > MAXIMUM_LUX_BREAKPOINT)
		return 1.0;
	else
		return (9.9323*log(x) + 27.059)/100.0;
} // getPctBrightFromLuxReading

uint16 mapPctBrighttoPWM(float pct) {
	return (uint16)(0xFFFF * pct * SCALE_FACTOR);
} // mapPctBrighttoPWM

float getLightLevel(void) {
	uint8* lowByte;
	uint8* highByte;
	uint8 exponent;
	uint8 mantissa;
	float result;
	
	I2C_ReadByte(MAX44009_ADDR, HIGH_BYTE, highByte);
	I2C_ReadByte(MAX44009_ADDR, LOW_BYTE, lowByte);
	
	exponent = (highByte & 0xF0) >> 4;// upper four bits of high byte register
	mantissa = (highByte & 0x0F) << 4;// lower four bits of high byte register = 
 					  // upper four bits of mantissa
	mantissa += lowByte & 0x0F; 	  // lower four bits of low byte register = 
					  // lower four bits of mantissa
	
	result = mantissa * (1 << exponent) * 0.045;
	
	return result;
} //getLightLevel

void stepBrightness(void) { 
	// if current is at desired, don't do anything
	if (currentBright_pct == desiredBright_pct)
		return;
	// is the current brightness above the desired brightness?
	else if (currentBright_pct > desiredBright_pct) {
		// is the difference between the two less than one step?
		if ( (currentBright_pct-stepSize) < desiredBright_pct)
			currentBright_pct = desiredBright_pct;
		else	
			currentBright_pct -= stepSize;
	} // else if
	else if (currentBright_pct < desiredBright_pct) {
		// is the difference between the two less than one step?
		if ( (currentBright_pct+stepSize) > desiredBright_pct)
			currentBright_pct = desiredBright_pct;
		else	
			currentBright_pct += stepSize;
	} // else if
	
	SetPWMDutyCycle(mapPctBrighttoPWM(currentBright_pct));
	return;
} // stepBrightness

void timerISR(void) {
	float lux;
	float pctDiff;
	
	stepBrightness();
	if (lightReadingCounter)
		lightReadingCounter--;
	else {
		lightReadingCounter = 20;	// 2 second delay
		lux = getLightLevel();
		desiredBright_pct = getPctBrightFromLuxReading(lux);
		pctDiff = abs(desiredBright_pct - currentBright_pct);
		stepSize = (pctDiff <= 0.01) ? 0.01:pctDiff/10;
	} // else
	
	ClearInterruptFlag();
} // timerISR