Geminiに設計してもらったプログラムをClaudeに修正してもらったものを以下に示します。
/* ============================================================
* NY_night -- PIC12F1501 LED driver
* rev.13: mode 3 changed from simultaneous to L/R alternating blink
*
* RA2 = PWM1 -> R2 -> T2 gate -> L_N (Left channel)
* RA4 = PWM3 -> R1 -> T1 gate -> R_N (Right channel)
* RA5 = mode button (SW1, to GND, internal pull-up)
* RA3 = unused input, brought out to JP1 pin 1 (MCLR/VPP)
*
* Modes: 1 alternating fade -> 2 steady -> 3 alt-blink -> SLEEP -> 1 ...
* In sleep the core is stopped; a button press wakes it via IOC.
* ============================================================ */
#pragma config FOSC = INTOSC /* internal oscillator */
#pragma config WDTE = OFF /* watchdog off */
#pragma config PWRTE = ON /* power-up timer on */
#pragma config MCLRE = OFF /* pin 4 is RA3, not MCLR */
#pragma config CP = OFF /* code protect off */
#pragma config BOREN = NSLEEP /* BOR active when running, off asleep */
#pragma config CLKOUTEN = OFF /* no clock output */
#pragma config BORV = LO /* low BOR threshold (battery) */
#pragma config LPBOR = OFF /* low-power BOR off */
#pragma config LVP = OFF /* HV programming via JP1 */
#include
#include
#define _XTAL_FREQ 16000000
/* ---- timing, all expressed in 10 ms ticks ---- */
#define TICK_MS 10
#define FADE_TICKS 7 /* 70 ms per fade step (30 steps = 2.1 s) */
#define BLINK_TICKS 30 /* 300 ms per blink phase */
#define DEBOUNCE_TICKS 3 /* 30 ms settle time */
#define DUTY_LIMIT 150 /* global duty cap, all modes.
* LED module has no current limiting of its
* own (measured 240 mA and rising at 5 V,
* duty 255). Average LED current scales
* linearly: I = 240 mA x DUTY_LIMIT / 255. */
#define HOLD_TICKS 60 /* 600 ms dwell at each end (trapezoid) */
/* Perceptual (gamma 2.0) ramp: duty = DUTY_LIMIT * (i / FADE_STEPS)^2.
* Human brightness perception is roughly logarithmic, so a linear duty
* ramp looks flat at the top and collapses abruptly near zero. The
* squared table spends most of its steps in the low-duty region where
* the eye actually sees the change. Regenerate if DUTY_LIMIT changes. */
#define FADE_STEPS 30
static const uint8_t fade_tab[FADE_STEPS + 1] = {
0, 0, 1, 2, 3, 4, 6, 8,
11, 14, 17, 20, 24, 28, 33, 38,
43, 48, 54, 60, 67, 73, 81, 88,
96, 104, 113, 122, 131, 140, 150
};
/* ---- PWM frequency exploration ----------------------------------
* The module's flicker-IC white LEDs are progressively suppressed as
* the PWM frequency rises (3.9 kHz: strong ~10 Hz flicker,
* 15.6 kHz: mostly suppressed). To let the flicker live, choose the
* frequency with the Timer2 prescaler alone; clock and all tick
* timing stay untouched. PR2 = 255 in every case.
*
* T2CON_RUN prescale PWM freq
* 0x04 1:1 15.625 kHz (= rev.9)
* 0x05 1:4 3.906 kHz (= original board feel)
* 0x06 1:16 977 Hz
* 0x07 1:64 244 Hz (floor; below this the PWM
* itself becomes visible)
* ---------------------------------------------------------------- */
#define T2CON_RUN 0x04
#define PR2_VAL 255
#define SET_L(v) (PWM1DCH = (uint8_t)(v)) /* duty ratio = v/256 */
#define SET_R(v) (PWM3DCH = (uint8_t)(v))
#define SW_PRESSED (RA5 == 0)
/* ------------------------------------------------------------
* Output stage on / off
* With the PWM modules disabled the pins revert to port
* control; LATA = 0 holds both gates low, and R3/R4 back
* that up so the MOSFETs cannot float on.
* ---------------------------------------------------------- */
static void outputs_off(void)
{
SET_L(0);
SET_R(0);
PWM1DCL = 0;
PWM3DCL = 0;
PWM1CON = 0x00;
PWM3CON = 0x00;
T2CONbits.TMR2ON = 0;
LATA = 0x00;
}
static void outputs_on(void)
{
LATA = 0x00;
SET_L(0);
SET_R(0);
PWM1DCL = 0; /* POR value undefined -> would leave
* ~0.3% duty even at DCH = 0 */
PWM3DCL = 0;
PR2 = PR2_VAL;
T2CON = T2CON_RUN; /* prescale select + TMR2ON */
PWM1CON = 0xC0;
PWM3CON = 0xC0;
}
/* ------------------------------------------------------------
* Sleep until the button is pressed.
* GIE stays 0, so the core resumes at the instruction after
* SLEEP() instead of vectoring to an ISR -- no ISR needed.
* ---------------------------------------------------------- */
static void sleep_until_button(void)
{
outputs_off();
IOCAF = 0x00; /* clear any stale edge flag */
IOCANbits.IOCAN5 = 1; /* wake on RA5 falling edge */
INTCONbits.IOCIE = 1;
INTCONbits.GIE = 0;
SLEEP();
NOP();
IOCANbits.IOCAN5 = 0;
INTCONbits.IOCIE = 0;
IOCAF = 0x00;
outputs_on();
}
int main(void)
{
/* ---------------- initialisation ---------------- */
OSCCON = 0x7A; /* 16 MHz internal -> PWM 15.6 kHz.
* Raised from 4 MHz / 3.9 kHz: a white
* LED in the module flickers ~10 Hz
* irregularly, suspected self-flasher IC
* resetting on the 3.9 kHz chopped supply */
ANSELA = 0x00; /* all pins digital */
LATA = 0x00; /* outputs low before they are enabled */
TRISA = 0b00101000; /* RA5 = button in, RA3 = input only */
WPUA = 0b00101000; /* pull up RA5 AND RA3 */
OPTION_REGbits.nWPUEN = 0; /* enable weak pull-ups globally */
outputs_on(); /* PWM up, both channels at duty 0 */
/* ---------------- state ---------------- */
uint8_t mode = 1; /* 0:sleep 1:fade 2:steady 3:blink */
uint8_t fi = 0; /* index into fade_tab, 0..FADE_STEPS */
int8_t step = 1; /* ramp direction, +1 / -1 */
uint8_t fade_t = 0;
uint8_t hold_t = 0; /* >0: dwelling at an end of the ramp */
uint8_t blink_t = 0;
uint8_t blink_on = 1;
uint8_t sw_raw = 0; /* 1 = pressed */
uint8_t sw_prev = 0;
uint8_t sw_state = 0; /* debounced */
uint8_t dbnc = DEBOUNCE_TICKS;
while (1) {
/* -------- button: non-blocking, acts on release -------- */
sw_raw = SW_PRESSED ? 1 : 0;
if (sw_raw != sw_prev) {
dbnc = 0; /* level changed, restart timer */
} else if (dbnc < DEBOUNCE_TICKS) {
if (++dbnc == DEBOUNCE_TICKS) { /* level has settled */
if (sw_state == 1 && sw_raw == 0) {
mode = (uint8_t)((mode + 1) & 0x03);
fi = 0;
step = 1;
fade_t = 0;
hold_t = 0;
blink_t = 0;
blink_on = 1;
}
sw_state = sw_raw;
}
}
sw_prev = sw_raw;
/* -------- mode output -------- */
switch (mode) {
case 0: /* off -> sleep */
sleep_until_button();
/* Woken by a press. Go straight to mode 1 so that even a
* very short tap responds, then swallow this press so the
* release does not advance the mode a second time. */
mode = 1;
while (SW_PRESSED) {
__delay_ms(TICK_MS);
}
__delay_ms(30); /* release bounce */
sw_raw = sw_prev = sw_state = 0;
dbnc = DEBOUNCE_TICKS;
fi = 0;
step = 1;
fade_t = 0;
hold_t = 0;
blink_t = 0;
blink_on = 1;
break;
case 1: /* alternating fade, trapezoid,
* perceptual (gamma) ramp */
if (hold_t > 0) {
hold_t--; /* dwelling at an end: L off with
* R full on, or the reverse */
} else if (++fade_t >= FADE_TICKS) {
fade_t = 0;
if (step > 0) {
if (fi < FADE_STEPS) {
fi++;
if (fi >= FADE_STEPS) hold_t = HOLD_TICKS;
} else {
step = -1;
}
} else {
if (fi > 0) {
fi--;
if (fi == 0) hold_t = HOLD_TICKS;
} else {
step = 1;
}
}
}
/* both channels follow the same perceptual curve, mirrored */
SET_L(fade_tab[fi]);
SET_R(fade_tab[FADE_STEPS - fi]);
break;
case 2: /* steady on */
SET_L(DUTY_LIMIT);
SET_R(DUTY_LIMIT);
break;
case 3: /* alternating blink: L and R
* swap every BLINK_TICKS */
if (++blink_t >= BLINK_TICKS) {
blink_t = 0;
blink_on ^= 1;
}
if (blink_on) { SET_L(DUTY_LIMIT); SET_R(0); }
else { SET_L(0); SET_R(DUTY_LIMIT); }
break;
}
__delay_ms(TICK_MS);
}
}
NY_night ファームウェア設計概要(内部設計)
PIC12F1501 / XC8、rev.13(最終版)
モード遷移・操作方法・書込みインタフェースなどの外部仕様は
spec.md(NY_night LED コントローラ 外部仕様)を参照。
本書はファームウェアの内部構造と実装判断のみを記す。
ステートマシン
10 ms ティックの単一ループ。ブロッキング待ちを排し、フェード・点滅・ボタン判定を
すべてティックカウンタで進める。旧版(Gemini 版)にあった
__delay_ms(300) や while(RA5 == 0); による CPU 占有を廃した。
これによりボタン応答は全モードで 10 ms 以内。
消灯 = SLEEP(消費電流削減)
消灯中は CPU を停止し、待機電流を µA オーダーに落とす。
RA5 の状態変化割り込み(IOC)で復帰するが、GIE = 0 のままにして ISR を書かない
(割り込み許可だけ立てておけば SLEEP から復帰でき、割り込みベクタへ飛ばず
SLEEP 命令の次から実行が続く仕様を利用)。
SLEEP 前に PWM モジュールを止め、LATA = 0 でゲートを直接 Low に落とす。
基板側の R3 / R4(100 kΩ プルダウン)が二重の保険となり、
ハイインピーダンス期間中も MOSFET が誤オンしない。
BOREN = NSLEEP により、ブラウンアウト検出は動作中のみ有効・SLEEP 中は無効。
BOR 回路自身の消費(数 µA)を待機時から除く。
ボタン処理
- 毎ティック非同期サンプリング + 30 ms デバウンス(3 ティック連続一致で確定)。
- モード遷移は離しエッジで確定し、押しっぱなしでの連続遷移を防ぐ。
- SLEEP 復帰時は「復帰したこと」自体をモード 1 入りのトリガとし、
その押下は飲み込む。短いタップでも確実に反応し、離しエッジで
二重にモードが進むことも防ぐ。
台形フェードの実装
ランプが上限・下限に到達した瞬間にホールドカウンタ hold_t を仕込み、
それが尽きるまでフェード処理をスキップする。ホールド中もボタン判定は
毎ティック回っており、応答性は変わらない。
L と R は PWM_R = 255 - duty の逆位相なので、L が下限ホールドの間
R は上限ホールドになる。
ピン設定の根拠
| ピン |
役割 |
設定 |
| RA2 |
PWM1 → T2 ゲート → L_N |
出力 |
| RA4 |
PWM3 → T1 ゲート → R_N |
出力 |
| RA5 |
モードボタン |
入力、内蔵プルアップ |
| RA3 |
未使用(JP1 の MCLR/VPP に接続) |
入力、内蔵プルアップで固定 |
RA3 のプルアップは新基板で必須。旧基板と異なり RA3 は未実装スルーホールに
しか繋がらず浮くため、固定しないと CMOS 入力の貫通電流が生じる
(WPUA = 0b00101000)。
PWM は Timer2 / PR2 = 255、分解能 8 bit(デューティ比 = v/256)。
周波数は Timer2 プリスケーラ 1 箇所(#define T2CON_RUN)で選択でき、
0x04=15.6 kHz / 0x05=3.9 kHz / 0x06=977 Hz / 0x07=244 Hz。
最終値は 0x04(15.6 kHz)。
経緯: モジュール内の白 LED 数個が PWM 駆動下で約 10 Hz の不規則な
チラつきを示した。DC 直結テスト(dctest rev.2)で「非常にちらつく」
ことが確認され、フリッカー IC 内蔵 LED の本来動作と確定。PWM 駆動
(平均 59%・断続給電)では IC が電力不足で本来のフリッカーを出せず、
周波数によって誤動作の出方が変わっていただけだった。全開のフリッカー
は不要との判断で、最も抑制される 15.6 kHz を最終仕様とした。
調整パラメータ
ソース冒頭の #define で変更する。時間系はすべて 10 ms 単位。
| 名称 |
既定値 |
意味 |
FADE_TICKS |
4 |
フェード 1 段の間隔(40 ms) |
HOLD_TICKS |
60 |
台形の両端ホールド(600 ms、最大 255) |
BLINK_TICKS |
30 |
交互点滅の切替間隔(300 ms) |
DEBOUNCE_TICKS |
3 |
デバウンス確定時間(30 ms) |
FADE_STEP |
5 |
フェード 1 段のデューティ増分 |
DUTY_MAX / DUTY_MIN |
250 / 5 |
ランプの上下限 |
モード 3(交互点滅)の実装
rev.13 で同時点滅から L/R 交互に変更。blink_on のトグルで
L=DUTY_LIMIT/R=0 と L=0/R=DUTY_LIMIT を入れ替えるだけで、
常にどちらかが点灯しているため暗転しない。
ビルド
- XC8 Free モードで Flash 約 30% / RAM 約 27%。PRO ライセンス不要。
- コンフィグは
BOREN = NSLEEP / BORV = LO / LPBOR = OFF を含む
(XC8 v2.x で確認済み)。
既知の留意点
- LED モジュールに電流制限が無いことが実測で確定した
(5 V・デューティ 255 で 240 mA、温度上昇に伴う増加傾向)。
rev.5 で全モード共通のデューティ上限 DUTY_LIMIT(既定 150)を導入し、
平均電流を約 140 mA に制限。フェードの上限 DUTY_MAX は
DUTY_LIMIT - FADE_STEP として連動し、逆位相側も DUTY_LIMIT - duty で生成。
平均電流は 240 mA x DUTY_LIMIT / 255 で見積もれる。
前のブログ
次のブログ