shino's bar

お家で使うLinux tux

PICマイコンを使ったファンの速度制御

7 Sep 2008 SDCCのバグについて補筆

(初出:12 May 2007)

PICマイコンを使ったファンの速度制御を紹介します。 PIC12F683は8ピンながら盛りだくさんな機能を備えています。そのうちA/D変換とPWMモードの使い方が主なポイントです。 フリーのCコンパイラSDCCを使ったサンプルプログラムで解説します。

--> SDCCのインストール
PIC16F84AによるLEDの点滅
PIC16F84Aによるリモコン電子ボリューム

装置の概要

動作概要

制御対象は、パソコンのケースファンなどに使われるDCファンです。 ファンを速度制御する目的は、ケース内温度が低いときはファンの回転数を落とすことで、 静音を目指すということが多いでしょう。 温度の測定にはサーミスタを使ったり、温度センサICなどを使うところですが、 今回はその予備実験として、ボリュームで設定値を与えています。

制御モードは2つあり、スイッチで切替えます。 ひとつはオープン・ループ制御で、ボリュームで与えたデューティ比でファンに電圧を供給します。 回転センサを持たないファンの場合はこのモードで使います。 2つ目はフィードバック制御で、ファンの回転センサより回転数を読み取り、 目標の速度となるよう出力のデューティ比を加減します。

オープン・ループ制御モードでは出力デューティ比を40%-100%まで分解能0.5%で調整できますが、 50%以下ではファンが停止してしまうことがあります。

フィードバック制御モードでは回転数をフルスピードの5%-100%の範囲で分解能1%で調整できます。

Circuit

回路

回路の概略は図のようなものです。 使用するファンの定格は12Vです。 ファン用の電源から3端子レギュレータを使い、マイコン用の電源を取っています。 電源のマイナス側(黒線)を共通にしたいために、出力部分が少し複雑になっています (NPNトランジスタとPチャンネルMOSFETの組合せ)。 これは必ずしも必要ではなく、 マイナス側に主スイッチ(NチャンネルMOSFET)を置けば、 そのゲートをマイコンの出力ピンで直接ドライブできるので、もっとシンプルになります。 ファンの両端に接続されるダイオードと抵抗は、ファンからのキックバックの吸収用です。

クロックはマイコン内蔵のRC発振を使っているので、そのための外付け部品はありません。 おかげでピンが2つ余りました。 ここにデバッグ用の信号を出力させ、オシロスコープなどで動作確認することもできます。

この回路図はgschemで描いています。

制御の概要

PWM制御について

「PWM制御」とは、出力をON/OFFスイッチさせ、 ON時間とOFF時間の比率(デューティ)を加減することで平均的な出力を制御するものです。 ON時間とOFF時間の和(周期)を一定にしておき、 そのうちのON時間を変化させるのが一般的です。 周期あるいは周波数は負荷の特性により選定されます。 ファンのように電磁機器が負荷である場合は可聴音を避けるために 20kHz以上とするのが好ましいのですが、今回は10kHzとしています。

マイコンによってPWM出力する方法はいくつかありますが、 PIC12F683には内蔵のタイマー2を使ってPWMを作成する機能があるので、今回はこれを使っています。 PIC12F683のPWM機能は最大分解能を10ビットすなわち0.1%程度にすることができますが、 今回は8ビット、約0.5%の分解能で使っています。

オープンループ制御

オープンループ制御モードではボリュームの値によって決まったデューティ比で出力しています。 ただし、ファンの起動には定常時に比べ大きな力が必要なので、電源投入時に少しの間(約0.2秒)フル出力としています。

与えた出力(デューティ比)でファンの回転数がどうなるかは、ファンの能力とともに摩擦や空気抵抗に左右されるので定かではありません。 出力結果(回転数)を見ておらず、フィードバック・ループが完成していないので、このような制御法を 「オープンループ」と呼びます。

出力を絞る(デューティ比を小さくする)と回転数は小さくなりますが、 テストに使ったファンではデューティ比50%以下のあたりで止まってしまいます。 この制御モードではファンが停止してもそれを検知できず、無駄に負荷電流が流れることとなります。 ファンが停止すると負荷電流が増加するので、これを検知して電圧の供給を停止することもできますが、 今回はそれを行っていません。

フィードバック制御

フィードバック制御では、目標値(ボリュームの値)に対しての測定値(回転数)の偏差から、 制御量(デューティ比)を加減することで目標値に近付けます。 代表的な手法では次のように制御量を決定します(比例制御)。

Xn = Xn-1 + G・E

    Xn:  新しい制御量
    Xn-1:以前の制御量
    G:   比例定数(フィードバック・ゲイン)
    E:   偏差 = 目標値 - 測定値

フィードバック・ゲインが大きいと目標値に早く近付くことができる(追従速度が早く残差も小さい)のですが、 行き越して振動することがあります。 ファンには慣性があるので、行き越し(振動)しやすい制御対象です。 回転数が早まったり遅くなったりと振動があると、音が変わるので、かえって耳につきます。 多少ゆっくりでも良いから、行き越しが少ないように制御することがポイントのひとつです。

後に詳細説明するように今回はこれを少しアレンジし、追従速度が比較的早くて、かつ振動の少ない方法を採っています。 残差はほとんどありませんが、制御量が離散値(デジタル)であることに起因する小さな振動は残ります。 制御パラメータの調整はプログラム内の定数を書換えることで行なうので、回路に調整機構は必要ありません。 マイコンを使用する利点のひとつです。

回転数の取り込み

ファンに付いている回転センサはロータリーエンコーダに相当します。 1回転あたり2パルス出力されます。 オープンコレクタ出力となっています。接続するGP3は内蔵プルアップ抵抗が使えないので、 プルアップ抵抗を外付けしています。

ところが、ファンの電源が切れると回転センサ出力はいつもオープンになってしまいます。 ファンの電源はPWMでON/OFFしていますから、このままではまともに回転の信号を得ることができません。 そこでPWMのON/OFF信号に同期して回転センサ出力をサンプルすることとしました。

使用したファンの定格回転数(フルスピード)は3400rpm、毎秒に直すと58回転。 回転センサ出力は116Hzということになります。

制御の詳細

マイコンが内蔵するタイマー1を使って、一定周期、約0.22秒を作っています。 1制御周期の間の回転センサ(エンコーダ)のパルス数をカウントします。 フルスピードで約25パルスとなります。 これを4倍すれば100となり、目標の回転数を%で表した数字になります。

1制御周期のパルス数を4倍したものと目標の%とを比較し、これを偏差Eとします。 偏差に対し次のグラフのような関係でデューティ比Xを増減します(図は模式で、正確なものではありません)。

	                 制御量Xの増減値
	                     ↑
	                     |
	                     |+20/200           -
	                     |                -
	                     |              -
	                     |            -
	                     |          -
	                     |        -
	-100/100             |0     -           100/100
	  +--------+---------+---------+---------+->偏差E
	              	-    |
	             -       |
	           -         |
	         -           |
	       -             |
	     -               |
	   -                 |-20/200

一見さきの比例制御に似ていますが、偏差が±20程度の範囲では出力デューティ比を触らない、 不感帯を設けています。 このままでは、 偏差がこの不感帯に入るとそれ以上追従しないことになりますが、少しトリックがあります。 このときの残差はここでは修正されませんが、 次の制御周期までそれは持ち越されます。 残差が集積され、不感帯を抜け出たところで制御量を増減するという具合です。

これにより、不感帯と見えたところでも実質的にはフィードバック・ゲインは0ではなく、 小数点以下のゲインが存在することになります。 1制御周期ではフルスピードでも約25パルスしかありませんが、数サイクルの制御周期を通して見ると、 分解能も上がるという寸法です。

ロック保護

フィードバック制御では回転数を検知しているので、これを用いてファンのロック保護ができます。 約1秒間エンコーダ出力が得られないときはファンが停止していると見なし、 出力を停止します。 その後約10秒後にフルパワーを与えて再起動を試みます。

プログラムの解説

Cのソースプログラムはこちらです。 A/D変換とPWMなどPIC12F683の今回使った機能を中心に順不同でかいつまみ説明します。

ヘッダ

#include <sdcc-lib.h>
#include <pic12f683.h>
#include "asm.h"

asm.hは よく使われるアセンブラ・コードを定義した自作のものです。 今回使っているのはウォッチドッグタイマをクリアする命令だけなので、
#include "asm.h" の行は 次のような#define文を置くことでも代用できます。

#define ClrWdt()        do { _asm clrwdt _endasm; } while(0)

CPUクロック

/* configuration bits */
int at 0x2007 __config = _INTOSC & _WDT_ON & _PWRTE_ON & _MCLRE_OFF;

	/* CPUクロック */
	OSCCON = 0x71;	// 8MHz
	OSCTUNE = 0x00;

コンフィギュレーションビット中の _INTOSC で内部RC発振を使うことを指定しています。 12F683の内部RC発振はプログラム中でいくつか周波数を選べるので、 OSCCONレジスタでそれを指定しています。OSCTUNEはその周波数の微調です。

入出力の定義

#ifdef GPIO1	/* GPIO bits が使えるとき */
#define CMODE	GPIO1
#define GATE	GPIO2
#define INPULSE	GPIO3
#define MONITOR	GPIO4
#define OUT_MONITOR_0 MONITOR = 0
#define OUT_MONITOR_1 MONITOR = 1
#define MONITOR2 GPIO5
#define OUT_MONITOR2_0 MONITOR2 = 0
#define OUT_MONITOR2_1 MONITOR2 = 1
#endif

GPIO0がここには現れませんが、次でアナログ入力として設定しています。

入出力ポートの初期設定

	/* GPIO */
	WPU = 0x37;	//  GP3を除きDIはすべてプルアップ
	CMCON0 = 0x07;	// コンパレータ使わない
	CMCON1 = 0x00;
	VRCON = 0x00;
	TRISIO = 0x0B;	// GP2,GP4,GP5は出力

PIC12F683にはアナログ・コンパレータ機能もありますが、 今回はこれを使っていません。
CMCON0 = 0x07; としてこれを明示的に設定しておかないと、 GPIOの一部ビットをデジタル入出力として使えませんので注意が必要です。 CMCON1, VRCON もアナログ・コンパレータ機能に関係する特殊レジスタです。

A/D変換

	ANSEL = 0x51;	// Fosc/16, ANS0
	ADCON0 = 0x01;	// 変換結果上位8ビットを使う, AN0, ADON

A/D変換機能に関係する特殊レジスタです。 ANSELではA/D変換のための1.6us-2us程度のクロックを作るための指定と、 アナログ入力に使うピンなどを指定をします。 ADCON0でも入力ピン指定がありますが、 アナログ入力に使うピンが複数ある場合はこのレジスタで都度A/D変換器への入力ピンを切替えます。 今回はアナログ入力ピンはひとつなので、これは固定です。

/*** ボリューム値の読み込み ***/
unsigned char volume() {
//    unsigned char datum;
    GO_DONE = 1;		// A/D変換開始
    while ( GO_DONE ){;}	// 変換終了を待つ
    datum = ADRESH;
    return datum;
}

この前にA/D変換器への入力ピン切替えがあるところですが、今回は固定です。 また、その後20usほどのウェイトが必要ですが、他のルーチンでその時間が掛かっているので、 その部分も省略しています。 GO_DONEビットを1にセットするとA/D変換が始まり、終了すると0にリセットされるので、それをルーチン内で待っています。 PWMはマイコン内蔵のハードが勝手にやってくれ、その制御も比較的ゆっくりであるため時間的余裕があるので、 このように単純な手順で済ましています。 このルーチンを呼ぶごとにA/D変換に掛かる時間、約50us経過します。

PIC12F683に内蔵されるA/D変換器は10ビットですが、ADCON0のさきの設定ではそのうち上位8ビットが ADRESHレジスタにあり、これだけを使っています。 ノイズなどの対処は行っていません。ボリューム値が多少ふらついたとしても、 制御がゆっくりなのでその影響が少ないからです。

PWM

	/* Timer2 PWMモード */
	T2CON = 0x00;	// TMR2プリスケーラなし
	CCP1CON = 0x0C;	//PWM mode active-high
	CCPR1L = 0;
	CCPR1H = 0;
	TMR2 = 0;
	PR2 = POWFULL;
	TMR2ON = 1;	// TMR2スタート
	・・・
	CCPR1L = POWFULL;	// 最大値からスタート

内蔵のPWM機能は主にタイマー2によります。 T2CONCCP1CONでクロックやプリスケーラを設定したうえで PR2レジスタがPWMの基本周波数と分解能を決め、 CCPR1LレジスタにそのうちのON時間を与えます。 分解能は最大10ビット取ることができますが、今回は8ビットを使っています。

回転センサの取り込み

/*** 回転数検知 ***/
unsigned char rotate() {
//    unsigned char rotold;
//    static signed char rotsum;
    rotold = rot;
    /* TMR2 == POWMIN に同期 */
    while ( TMR2 > POWMIN ) {;}
    while ( TMR2 < POWMIN ) {;}
    /* 回転検知パルスを積分 */
    if ( INPULSE ) {
	if ( ++rotsum >= SMOOTH ) {
		rotsum = SMOOTH;
		rot = 1;
	}
    } else {
	if ( --rotsum <= 0 ) {
		rotsum = 0;
		rot =0;
	}
    }
    return ( rotold && (rot == 0) ); // 立ち下がりエッジ
}

TMR2 == POWMIN となるタイミングというのは、 たいていはPWMの出力がONであり、あるいはONからOFFに変わった直後です。 このとき回転センサの出力は安定しているので(OFF→ONのあと数usは不安定。ON→OFF直後の数usはまだ安定。)、そのときの値をサンプリングしています。 これをさらに積分してノイズを除去したあと、 立ち下がりエッジを検出すると1パルスをカウントします。

PWMの周期に同期させているので、 このルーチンを呼ぶごとにPWMの1周期、約0.1ms経過します。

割り込み

PIC12F683はPIC16F84Aに比べ割り込み要因がとても多くなっています。 どの要因による割り込みを許可するかは INTCONレジスタとPIE1レジスタとを使います。 今回は割り込みをいっさい使っていないので、 INTCON = 0x00 としていますが、 PIE1レジスタにある割り込み要因を使う場合は、 INTCONレジスタのビット6も1にしておかねばならないので注意が必要です。

その他

PIC16F84AやPIC12F683に乗除算命令は基本的にはありません。 コンパイラが処置してくれる場合もありますが、乗除算を避けるようコーディングしましょう。 たとえば乗算は加算のくり返し、除算は減算のくり返しで実現できます。 あるいはシフトを使って、例えば 3*a の代用は a<<1 + a でできます。。

できるだけ8bit幅を使いますが、油断するとすぐにオーバーフロー、アンダーフローします。 signed char は -128〜127, unsigned char は 0〜255 の範囲であることを常に注意しましょう。 例えば次のようなコーディングは間違いです。

unsigned char i;
if ( --i < 0 ) { i = 0; }	// unsigned char に負数はないので間違い

この例ではコンパイラが注意を出してくれますが、 コンパイラでは検出出来ないミスもあるので気を付けましょう。 上の例は次のように書けば正しいものとなります。

unsigned char i;
if ( i > 0 ) { --i; }	// アンダーフローを事前にチェックしているのでOK

デバイスへの書き込み

PIC12F683への書き込みはAKI-PICプログラマーversion3では対応しません。 ADWINのPIC PROGRAMMER を使っていますが、書き込みソフトはWindows上でしか動きません。 LinuxでコンパイルしたものをWindowsに運んで…ということもできますが、 けっきょくは SDCCGPUTILS もWindows上にインストールして使っています。

SDCCのバグ

バグその1 (最近のバージョン sdcc-2.8.0では解決されています)

SDCCは特殊レジスタをビット毎に操作できるのですが、 初版執筆当時のスナップショット版では PIC12F683のGPIOポートに限って 前述のようなビット毎の操作ができなかったので、 次のような定義でこれを代用しました。

#ifndef GPIO1	/* GPIO bits が使えないとき */
#define CMODE	GPIO & 0x02
#define GATE	GPIO & 0x04
#define INPULSE	GPIO & 0x08
#define MONITOR GPIO & 0x10
#define OUT_MONITOR_0 GPIO &= ~0x10
#define OUT_MONITOR_1 GPIO |= 0x10
#define OUT_MONITOR2_0 GPIO &= ~0x20
#define OUT_MONITOR2_1 GPIO |= 0x20
#endif

バグその2 (sdcc-2.7.0-rc2 以降では解決されています)

機械語レベルで直接16ビットを操作する命令は無いので、 8ビットを操作する命令の組合せにコンパイラが自動的に変換してくれます。 しかし、16ビットと8ビットの混合演算で期待どおりにならないことがありました。

unsigned char vol;
signed int speedsum;
speedsum +=  vol;	// うまくいかない
unsigned char vol;
signed int speedsum, value;
/* こうするとうまくいった */
value = vol;
speedsum +=  value;

関連ページ

ネット上の参考情報

ご意見、お問い合わせは Linux掲示板


shino's bar goto [シノバー店内案内] [お家で使うLinux]