サイクルコントロールユニットの自作
試料を加熱するために電気炉を使っているが、そのヒーターの制御には、ソリッドステートリレー(SSR)またはサイリスタを使っている。 より精密な制御にはサイリスタの方が望ましいが、当然価格は高くなり、部品も大きくなってしまう。 電流がそれほど大きく無い場合には、コンパクトな理化工業のSSNP-15Fをよく使っていたのだが、今は廃盤になってしまっている。
代わりに使えるようなコンパクトなサイリスタが無いかを探していたら、OMRONのサイクルコントロールユニットG32A-Eというものを見付けた。 これは、ゼロクロス方式のSSRと組み合せることによって、サイクル制御方式のサイリスタとしての役割をもたせるためのものである。 小型のSSRと組合せれば、コンパクトに収められるのではと考えて、仕様の詳細を調べたら、分解能が低いことが分かった。 このコントローラーでは、0.2sの間で交流の何サイクルかをONにするという制御を行っているために、その間の波の数で分解能が決ってしまうのである。 これでは、ちょっと使えない。
15年ぐらい前に私が設計したサイリスタコントローラーでは、1サイクル毎に出力の割合を積算して、それが1を超えたら1サイクル出力して、積算値から1を引くというやり方をしたが、そうすれば単純な計算で十分な分解能にできる。 売って無いなら作れば良いだろうということで、温度調節機からの4-20mAの信号を読み取ってSSRを制御する機器を作ってみることにした。 後で知ったのだが、OMRONの多点パワーコントローラG3ZAというものがあり、そこでは高い分解能と同時に8個までのSSRを制御でき、今回作成したものと同じような機能を持つことが分かったが、入力が特別な信号になるようだ。
このような機器を実現するにあたって、交流のサイクルをどのように検出するかが課題である。 15年前には、トランスで電圧を落として、その電圧をマイコンで読み取ることで判定していた。 それと同時に降圧した電圧からマイコン用の5Vも作るようにいていた。 しかし、トランスで大きくなるし、以前に使っていた基板用のトランスも廃盤になってしまっているので、色々と考えて今回は、フォトカプラを用いて検出することにした。 具体的には、フォトカプラの入力とダイオードを極性を逆にして並列にしたものに、100kohm程度の抵抗を直列に繋いで、それをAC100Vに接続して、フォトカプラの出力をプルアップしたマイコンのピンで読み取る。 すると、ダイオードによって交流は半波になり、その電圧が高くなったときに、マイコンのピンに電流が流れてlowになるという仕組みで、交流のサイクルを検出できるのである。 ただ、フォトカプラには寿命があるので、できるだけ電流は小さく抑えて寿命を長くして、さらに素子を交換可能にした。 一方、マイコン用の電源は、安価で小型の電源ユニットを用いることにした。 そして、温度調節機の出力を220ohmに流したときの電圧を、ADCで読み取って、それを元にサイクル毎のon/offを判定して、マイコンから出力すれば良いのである。 マイコンボードにはUIAPduinoを使うと、それを合わせても、千円以下しかかからない。 ちなみに、ADCが6個あったので、6つまで制御できるようにしてみた。 作ったプログラムは以下の通りである。
#include "ch32fun.h"
#include <stdio.h>
#define NUM_HEATERS 6 // the number of heaters
#define ADC_4MA_MIN 180 // 220 ohm
#define ADC_20MA_MAX 900 //
#define RESOLUTION_MAX (ADC_20MA_MAX - ADC_4MA_MIN)
#define RESOLUTION_MAX_SCALED (RESOLUTION_MAX << 8)
// OUT pins { 3, 4, 10, 8, 9, 7 }
static volatile uint8_t heater_pins[NUM_HEATERS] = { PC1, PC2, PD0, PC6, PC7, PC5 };
// ADC pins {TX, RX, A0, A1, A2, A3 }
static volatile uint8_t analog_pins[NUM_HEATERS] = { PD5, PD6, PA2, PA1, PC4, PD2 };
static volatile uint8_t adc_channels[NUM_HEATERS] = { 5, 6, 0, 1, 2, 3 };
// AC 100V - 33kohm - (photo-coupler + diode) - 33kohm - AC 100V
// to disable reset for PD7, please run ./minichlink -D
// PC3 : No.5 pin
#define ZERO_CROSS_PIN PC3
#define ZERO_PORT (ZERO_CROSS_PIN >> 4)
#define ZERO_LINE (ZERO_CROSS_PIN & 0x0F)
#define LED_PIN PD4
#define ERR_PIN PC0
// skip 12ms
#define DEBOUNCE_TICKS (48000000 / 1000 * 12)
// timeout 50ms
#define SAFETY_TIMEOUT_TICKS (48000000 / 1000 * 50)
// 1ms * 200
#define WATCH_DOG_TIMEOUT 200
volatile uint32_t duties[NUM_HEATERS] = {0};
volatile uint32_t err_accumulators[NUM_HEATERS] = {0};
volatile uint32_t led_counter = 0;
volatile uint32_t last_interrupt_time = 0;
void EXTI7_0_IRQHandler(void) __attribute__((interrupt));
void EXTI7_0_IRQHandler(void) {
if (EXTI->INTFR & (1 << ZERO_LINE)) {
EXTI->INTFR = (1 << ZERO_LINE);
uint32_t current_time = funSysTick32();
if ((current_time - last_interrupt_time) < DEBOUNCE_TICKS) {
return;
}
//funDigitalWrite(ERR_PIN, FUN_LOW);
last_interrupt_time = current_time;
led_counter++;
funDigitalWrite( LED_PIN , (led_counter & 4)>>2 );
for (int i = 0; i < NUM_HEATERS; i++) {
err_accumulators[i] += duties[i];
if (err_accumulators[i] >= RESOLUTION_MAX_SCALED) {
funDigitalWrite(heater_pins[i], FUN_HIGH);
err_accumulators[i] -= RESOLUTION_MAX_SCALED;
} else {
funDigitalWrite(heater_pins[i], FUN_LOW);
}
}
}
}
void Init_Hardware(void) {
SystemInit();
RCC->APB2PCENR |= RCC_APB2Periph_AFIO;
funGpioInitAll();
funAnalogInit();
funPinMode(LED_PIN, GPIO_CFGLR_OUT_10Mhz_PP);
funPinMode(ERR_PIN, GPIO_CFGLR_OUT_10Mhz_PP);
for (int i = 0; i < NUM_HEATERS; i++) {
funPinMode(heater_pins[i], GPIO_CFGLR_OUT_10Mhz_PP);
funDigitalWrite(heater_pins[i], FUN_LOW);
funPinMode(analog_pins[i], GPIO_CFGLR_IN_ANALOG);
}
funPinMode(ZERO_CROSS_PIN, GPIO_CFGLR_IN_PUPD);
funDigitalWrite(ZERO_CROSS_PIN, FUN_HIGH);
AFIO->EXTICR &= ~(0x03 << (ZERO_LINE * 2));
AFIO->EXTICR |= (ZERO_PORT << (ZERO_LINE * 2));
EXTI->RTENR &= ~(1 << ZERO_LINE); // disable rising edge
EXTI->FTENR |= (1 << ZERO_LINE); // enable falling edge
EXTI->INTENR |= (1 << ZERO_LINE); //
NVIC_EnableIRQ(EXTI7_0_IRQn);
SysTick->CTLR = SysTick_CLKSource_HCLK | SYSTICK_CTLR_STE;
last_interrupt_time = funSysTick32();
funAnalogInit();
for (int i = 0; i < NUM_HEATERS; i++) {
uint32_t val = funAnalogRead(adc_channels[i]);
if (val < ADC_4MA_MIN) val = ADC_4MA_MIN;
else if (val > ADC_20MA_MAX) val = ADC_20MA_MAX;
duties[i] = (val - ADC_4MA_MIN) << 8;
}
}
void Init_IWDG(void) {
IWDG->CTLR = 0x5555;
IWDG->PSCR = IWDG_Prescaler_128; // 128kHz/128 -> 1kHz 1ms
IWDG->CTLR = 0x5555;
IWDG->RLDR = WATCH_DOG_TIMEOUT;
IWDG->CTLR = 0xCCCC; // start
IWDG->CTLR = 0xAAAA; // feed
}
int main() {
Init_Hardware();
Init_IWDG();
while(1) {
IWDG->CTLR = 0xAAAA;
for (int i = 0; i < NUM_HEATERS; i++) {
uint32_t adc_raw = funAnalogRead(adc_channels[i]);
if (adc_raw < ADC_4MA_MIN) {
adc_raw = ADC_4MA_MIN;
} else if (adc_raw > ADC_20MA_MAX) {
adc_raw = ADC_20MA_MAX;
}
uint32_t duty_raw = adc_raw - ADC_4MA_MIN;
__disable_irq();
duties[i] = ((duties[i] * 230) >>8) + (duty_raw * 26);
__enable_irq();
Delay_Ms(1);
}
if ((funSysTick32() - last_interrupt_time) > SAFETY_TIMEOUT_TICKS) {
__disable_irq();
funDigitalWrite(ERR_PIN, FUN_HIGH);
for (int i = 0; i < NUM_HEATERS; i++) {
funDigitalWrite(heater_pins[i], FUN_LOW);
}
__enable_irq();
}
}
}
基板のピンの順番の関係で、通常はresetに使われているピンをサイクルの検出に使った。 そのため、いくつかの手順が必要だった。 まず、WCH-LinkEに繋いで、./minichlink -DとしてこのピンをGPIOとして使えるようにした。 すると、UIAPduinoのbootloaderが立上がらなくなるので、bootloaderをrv003usbのものに入れ替えた。 その際、DPUを4にして、USBと接続している間はずっとbootloaderが有効になるようにして、それ以外は1秒ぐらいでユーザープログラムに移行するように設定を変更してから、コンパイルした。 再びWCH-LinkEに繋いで./minichlink -w bootloader.bin bootloaderとして、書き込んだ。 これで、USBと接続すれば、プログラムの書き込みができるようになる。
実際に駆動させる場合には、断線やノイズなどのトラブルが起る可能性があるので、それらに対して、安全になるように配慮してプログラムを行った。 このとき、UIAPduinoにはpower LED以外にPC0とPD4にLEDが接続されているが、通常はUSBに使われているPD4も今回は使えるので、二つのLEDを以下のように稼動状態を表すために使用した。 まず、サイクルが正常に検出できているかを表すために、4サイクル毎にLEDのon/offを切り換えて、肉眼で点滅を確認できるようにした。 そして、数サイクルの時間がたっても次のサイクルを検出出来無いときには、ヒーターをオフにして、もう一つのLEDを点灯して、エラーを表示することにした。 さらに、マイコンが暴走して0.2秒ぐらい応答がなければ、ウォッチドッグタイマでリセットがかかるようにした。 ADCの値はフィルターを通すことによって、多少は応答速度が遅くなるが、ノイズに強くした。 この計算式が誤っていて、最初はなかなか動かなくて苦労したが。 以上の工夫により、実用に耐えるものになったのでは無いかと考えている。 今後、実際に動かしてみて、動作を確認してみる予定である。