Posts Tagged with "Design"

既に発行済みのブログであっても適宜修正・追加することがあります。
We may make changes and additions to blogs already published.

BSVによるUARTの再設計

posted by sakurai on August 18, 2026 #1106

Uart.bsvを若干修正しました。理由は、過去記事では呼び出しにdone=fsm.doneを返すようにしていたものを、ダミー関数drainの実行待ちでwaitさせるものです。

UartDrain.bsv

// UartDrain.bsv : drain版UART
// Uart.bsv (done版) との差分は done() → drain() のみ
package UartDrain;

import StmtFSM::*;

interface Uart_ifc;
      method Bit#(1) read();
      method Action load(Bit#(8) newdata);
      method Action drain();
endinterface

(* synthesize *)
module mkUartDrain(Uart_ifc);
      Reg#(Bit#(8)) data  <- mkReg(8'haa);
      Reg#(Bit#(1)) odata <- mkReg(1);

      Stmt s = seq
            odata <= 1'h0; // start bit
            repeat (8) action
                  odata <= data[0];
                  data <= (data >> 1);
            endaction
            odata <= 1'h1; // stop bit
      endseq;

      FSM fsm <- mkFSM(s);

      method Bit#(1) read();
            return odata;
      endmethod

      method Action load(Bit#(8) newdata) if (fsm.done);
            data <= newdata;
            fsm.start;
      endmethod

      // ダミー関数: 完了を暗黙条件とする空アクション
      // RDY_drain = fsm.done、EN_drain は内部で未使用
      method Action drain() if (fsm.done);
            noAction;
      endmethod
endmodule

endpackage

UARTモジュールの合成後の回路は同等であり、前者が

  // value method done
  assign done =
	     fsm_abort_whas_AND_fsm_abort_wget_OR_fsm_state_ETC___d71 &&
	     !fsm_start_reg ;

となり、呼び出し側に明示的にdoneを見せるのに対して、後者は、

  // action method drain
  assign RDY_drain =
	     fsm_abort_whas_AND_fsm_abort_wget_OR_fsm_state_ETC___d71 &&
	     !fsm_start_reg ;

となり、呼び出し側をBSVの自動バックプレッシャのRDYで待たせるため、全く同じ回路となります。


左矢前のブログ 次のブログ右矢

posted by sakurai on August 15, 2026 #1105

修理が完成した製品の動作の状況を示します。これはモード1の、青LEDがフェードアウトしていき、赤LEDがフェードインするモードです。白は元々、LED自体にチラツキ回路が入っているようです。

図1105.1
図1105.1 完成写真


左矢前のブログ 次のブログ右矢

posted by sakurai on August 11, 2026 #1104

Claudeに外部仕様を書いてもらったものを以下に示します。

NY_night LED コントローラ 外部仕様

PIC12F1501 による 2ch LED 調光基板(SMD / JLCPCB PCBA 版)


1. 概要

3Dペーパー製ライトボックスの LED モジュール 2 系統を、PWM で独立に調光する。 モードはタクトスイッチ 1 個で巡回切替。消灯時はマイコンが SLEEP に入る。

項目 仕様
制御 IC PIC12F1501(内蔵 4MHz)
出力 ch 数 2(L / R)
出力形式 N-ch MOSFET ローサイドスイッチ
PWM 周波数 15.6 kHz
調光分解能 8 bit(0〜255)
操作系 モーメンタリスイッチ ×1

2. 電源仕様

項目
動作電圧 3.0 〜 5.5 V
想定電源 単4 ×3本(4.5V 公称)/ USB・AC アダプタ 5V
動作電流(基板のみ) 約 1 mA(LED 電流を除く)
待機電流(SLEEP) µA オーダー
書込み時の最低電圧 2.7 V
  • 電源電圧が変わっても LED の明るさ以外の特性は変化しない。MOSFET のオン抵抗は Vgs = 2.5V でも 48 mΩ で、100 mA 通電時の電圧降下は 5 mV。
  • 電池と外部アダプタを同時に接続しないこと。 アダプタから電池へ逆流する。電源切替は基板外で行う(スイッチ付き DC ジャック等)。
  • 電圧低下時はブラウンアウトリセットで安全停止する(動作中のみ有効、SLEEP 中は無効)。

3. 外部接続

基板右辺に φ1.0 mm スルーホールのワイヤパッドが 6 個、3.81 mm 等ピッチで 1 列に並ぶ。

位置 名称 内容
上から 1 VDD 電源入力 +
上から 2 GND 電源入力 −
上から 3 L_N 左 ch LED カソード側(MOSFET ドレイン)
上から 4 L_P 左 ch LED アノード側(VDD に内部接続)
上から 5 R_N 右 ch LED カソード側(MOSFET ドレイン)
上から 6 R_P 右 ch LED アノード側(VDD に内部接続)

電源ペアが上、LED 2 系統が下という並び。各ペアが隣接しているので 2 本組で配線できる。

LED 出力の条件

  • LED モジュールは X_P と X_N の間に接続する。
  • 基板側に電流制限素子は無い。 LED モジュール内部に抵抗が含まれていること。含まれない裸の LED を直結すると破損する。
  • 1 ch あたりの最大電流は 1 A(配線幅 0.4 mm / 1oz 銅による制限)。MOSFET 自体の定格は 5.7 A。
  • ローサイドスイッチのため、LED のアノード側は常に VDD 電位。カソード側が接地・開放される。

4. ユーザ操作

基板裏面に実装されたロングステムのタクトスイッチ 1 個。PCB を台紙に取り付け、台紙を貫通させて裏側から押す。

モード遷移

ボタンを離した瞬間に次のモードへ進む。

電源投入 → [1] 交互ゆらぎ
   押す  → [2] 全点灯
   押す  → [3] 交互点滅
   押す  → [0] 消灯(SLEEP)
   押す  → [1] 交互ゆらぎ  …以下巡回
モード 動作
1 交互ゆらぎ L と R が逆位相の台形波でフェード。2 秒ランプ + 両端 600 ms ホールド、全周期 約 5.2 秒。デューティ範囲 5〜250 / 255
2 全点灯 両 ch フルオン
3 交互点滅 L と R が 300 ms ごとに交互に点灯(片方点灯中はもう片方消灯)
0 消灯 両 ch オフ、マイコン SLEEP
  • 電源投入時はモード 1 から始まる。
  • ボタン応答は全モードで 10 ms 以内。チャタリング除去は 30 ms。
  • SLEEP からの復帰は短いタップでも確実に反応する。復帰時は必ずモード 1 に入る。

5. 書込みインタフェース

基板上辺の 1×6、2.54 mm ピッチのスルーホール(JP1)に、ピンヘッダを直立で常設実装する。

ピン 信号
1 MCLR / VPP
2 VDD
3 VSS
4 ICSPDAT (RA0)
5 ICSPCLK (RA1)
6 N.C.
  • MCLRE = OFF かつ LVP = OFF のため、高電圧プログラミング(VPP 約 9V)に対応した書込み器が必要(PICkit 4 で実績あり)。
  • 書込みは基板側電源(電池等)を接続したまま、書込み器のターゲット給電を OFF にして行う。LED モジュール接続状態でツール給電すると過負荷になる。JP1 の 2 番ピンは電圧検出に使うため接続したままにする。
  • SLEEP 中でも書込み可能(VPP 印加でリセットがかかる)。

6. 機構仕様

項目
基板外形 23.7 × 24.7 mm
層構成 2 層(表: VDD ベタ + 信号 / 裏: GND ベタ)
板厚 1.6 mm
表面の最大部品高さ 2.5 mm(C2、1210 MLCC)。ほかに JP1 ピンヘッダ(約 9 mm)が立つ
裏面 タクトスイッチのみ(ロングステム)
取付穴 無し
  • 固定は両面テープ。PCB 裏面を台紙表面に接着する(取付穴は使わない設計)。
  • 台紙には SW1 本体が入る貫通穴(6×6 mm より大きめ)を開け、SW1 本体を穴に沈めて PCB 面で接着する。SW1 上面だけで接着すると不安定になるため不可。
  • SW1 の選定条件: 全高(本体 + ステム)が「両面テープ厚 + 台紙厚 + 押下ストローク 0.25 mm」を超えること。すなわち ステム長 ≥ 両面テープ厚 + 台紙厚 + 0.25 mm − 本体高さ。
  • 表面の周囲 1.0 mm 以上は銅箔なし。

7. 組立順序

  1. JLCPCB より基板到着(表面 SMD 10 点実装済み)
  2. JP1 にピンヘッダをはんだ付け(常設)
  3. ICSP でファーム書込み
  4. 最後に SW1 を裏面にはんだ付け

SW1 を先に付けるとステムが干渉して基板が平らに置けず、以降の裏面作業ができなくなる。


8. 制約事項

  • 基板・LED モジュールとも電流制限素子を持たない(実測: 5V・デューティ 255 で 240 mA、上昇傾向)。このためファームウェアが全モード共通のデューティ上限 DUTY_LIMIT(既定 150/255、平均約 140 mA)で制限している。上限値はソース冒頭の #define で変更可。
  • 逆接続保護が無い。VDD / GND を逆に繋ぐと破損する。
  • 電池とアダプタの同時接続は不可。
  • モード状態は電源断で失われる。再投入時は必ずモード 1。

左矢前のブログ 次のブログ右矢

posted by sakurai on August 10, 2026 #1103

JLCPCBのGerber Viewerによる図です。PCBを5枚だけでなく、PCBAも2枚依頼したので、以下は部品も実装した図となっています。

図1103.1
図1103.1 Gerber Viewer

製造はいつものようにJLCPCBに依頼し、待ちが0.8日間、PCBは0.8日間(5枚で2 USD)、PCBAは1.1日間(2枚で21.6 USD)、配送は2.9日(8.92 USD)でした。 最終的に8月4日夜に発注して、8月10日にPCBを香港からの航空便で受け取れたので、オーダーから5.7日(32.52 USD)という感じでした。23.7 × 24.7 mmのPCBです。

図1103.2
図1103.2 完成PCB

図1103.3に、2ch(L-ch/R-ch)がモード1で交互にPWM動作をしている図を示します。光の強さが交互に増減するモードです。

図1103.3
図1103.3 PWM動作


左矢前のブログ 次のブログ右矢

posted by sakurai on August 8, 2026 #1102

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 で見積もれる。

左矢前のブログ 次のブログ右矢

posted by sakurai on August 7, 2026 #1101

Studio 500 3D Pop Up Wall Artの修理の続きです。前回のボードは基本的に動作したので、SMD化の改版を行います。改版はAIに手伝ってもらいます。まず製品の外観です。

図1101.1
図1101.1 Studio 500

次にAIに改版してもらった論理図です。部品はSMDタイプを選択させました。電子スイッチは2SCを使用していましたが、CMOSに変更しています。

プルダウン抵抗を100kΩにしたのは、駆動時の無駄電流を抑えるためです。RA2/RA4がHighのとき、470Ωを通してゲートに5Vがかかり、100kΩ経由でGNDへ50µAが流れ続けます。2チャンネルで100µA。単4電池駆動なので、ここは小さいほうが望ましい。

一方でリーク電流に対する引き込み能力としては100kΩで十分です。AO3400Aのゲートリークは最大100nA程度なので、100kΩなら10µVしか持ち上がりません。

なお470Ω(R1/R2)ゲート容量への突入電流を制限してPICのポートを保護し、スイッチングのリンギングを抑えます。100kΩとの分圧比は99.5%なので、オン時のゲート電圧はほぼVDDそのままです。

なおAIの部品配置は配線が省略され適当なので、部品間の配線は人手で接続しています。

図1101.2
図1101.2 点滅回路図

次にレイアウト図を示します。人手で部品を配置し、基本的にはEAGLEで自動配線しました。ただし電流が大きいところは太さを変えています。

出来上がりをClaudeにみせたところ、背面GNDと前面GNDの間のVIAが少なく、前面GNDパッド位置でVIAを打ち、背面の大きなGNDプレーンにコンタクトすべきとのことで、VIAを追加しました。今回はSMDなので、前面にVDDとGNDが混在するように変更されたためです。

PCBサイズは23.7 × 24.7 mmとなりました。SMD化により旧ボードと比べて▲53%となっています。

図1101.3
図1101.3 点滅回路レイアウト図


左矢前のブログ 次のブログ右矢

GameFSMの改良 (25)

posted by sakurai on August 6, 2026 #1100

次に原作にもある「化石」という症状をインプリしてもらいました。

化石の動画

仕様: 化石の再現 隊列がシールド帯まで降下した状態で、左端での下降パス中に自弾が右端シールドに命中すると、これを個体命中と誤認する。シールド内の弾の位置に敵爆発音と敵爆発マークを発生させ、マーク消去時にシールドへ 16×8 の穴を開ける一方、実際に死ぬのは弾の真上とは別の列の最前列個体である。その個体の画像は消去されないまま画面に残り、以後は敵として撃てず、隊列だけが先へ進む。

発症条件の幾何定数 — 隊列の降下判定しきい値と、右端シールドの x 範囲。

`ifdef FOSSIL_BUG
`define FOSSIL_BUG_Y 176   // 原作バグ発症条件: 隊列がシールド上端(192)の1ピッチ以内                            
`define FOSSIL_BUG_X 182   // これ以右のシールド命中で発症(=右端シールド全体、x=182〜204)                       
`endif

爆発マークの描画・消去位置を保持する — 犠牲個体の座標から切り離すことで「マークと犠牲が別の場所」という原作バグの本質を表現可能にする。

`ifdef FOSSIL_BUG
   Reg#(UInt#(8)) expl_x <- mkRegU,   // インベーダ爆発マークの描画・消去位置                                   
                  expl_y <- mkRegU;   // (バグ再現時は個体位置とマーク位置が食い違う)                           
`endif

常キル時にマーク位置=個体位置を expl_x/expl_y へラッチする(挙動は従来と同一の中立改造)。

`ifdef FOSSIL_BUG
               eraseArea(
               expl_x,
               expl_y,
               16, 8);
`else
               eraseArea(
               inv_x[gx][gy],
               inv_y[gx][gy],
               16, 8);
`endif

マーク消去を個体座標ではなく expl_x/expl_y に対して行う(バグ時はシールド内を消して穴を開ける)。

`ifdef FOSSIL_BUG
      return (seq
         action
            alien_timer <= 1;
            expl_x <= inv_x[gx][gy];   // マーク位置=個体位置(通常キル)                                         
            expl_y <= inv_y[gx][gy];
         endaction
         copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
                  inv_x[gx][gy], inv_y[gx][gy],
                  16, 8);
      endseq);
`else
      return (seq
         alien_timer <= 1;
         copyArea(96, colorAtY(inv_y[gx][gy])*16+16,
                  inv_x[gx][gy], inv_y[gx][gy],
                  16, 8);
      endseq);
`endif

発症4条件成立時のシールド命中を誤認キル化する本体 — マークと音は弾の位置、死ぬのは左端側の生存列の最前列、絵は残って化石になる。

`ifdef FOSSIL_BUG
               endseq else if (fbase && ymax >= `FOSSIL_BUG_Y
                               && inv_move == MoveLeftDown
                               && bullet_x >= `FOSSIL_BUG_X) seq
                  // ---- 原作バグの再現(希少条件版) ----                                                       
                  // 右→左で来て左端で下降中(=MoveLeftDown、隊列スキュー中)、                                  
                  // かつ隊列がシールド帯まで降りた状態で右端シールドを撃つと、                                 
                  // 原作の座標→個体逆算の破綻(左下原点からの列計算オーバー                                    
                  // フローが隣行へ回り込む)を模して、左端の生存個体が誤って                                    
                  // 殺される。マークと消去は弾の位置、犠牲の絵は左端に無傷で                                   
                  // 残り、隊列に取り残される(化石)。                                                           
                  fhit_alien <= False;
                  for (gx <= 0; gx < `InvCols; gx <= gx + 1) seq
                     if (inv_s[gx][inv_lead_y[gx]]) seq   // 左端の生存列を探す                                 
                        fhit_alien <= True;
                        break;
                     endseq
                  endseq
                  if (fhit_alien) seq
                     gy <= inv_lead_y[gx];
                     erasePlayerBullet(bullet_x, bullet_y);
                     inv_s[gx][gy] <= False;
                     inv_no <= inv_no - 1;
                     updateColumnFrontY();
                     playSound(SND_ALIEN_EXPLODE);
                     action                        // マークは弾の位置に描く                                    
                        alien_timer <= 1;
                        expl_x <= bullet_x - 8;
                        expl_y <= bullet_y;
                     endaction
                     copyArea(96, colorAtY(bullet_y)*16+16,
                              bullet_x - 8, bullet_y, 16, 8);
                     action
                        score <= score + inv_score[gy];
                        bscore <= bscore + inv_score[gy];
                     endaction
                  endseq else seq
                     erasePlayerBullet(bullet_x, bullet_y);   // 生存列なし: 通常のシールド命中                 
                     explodePlayerBullet(35, bullet_x, bullet_y);
                     bullet_timer <= 1;
                  endseq
`endif

左矢前のブログ 次のブログ右矢

GameFSMの改良 (24)

posted by sakurai on August 3, 2026 #1099

最近は推論能力が高いため、もっぱらClaude Fable 5を使用しています。さてCaludeに以下の仕様を実装してもらいました。

「自弾がインベーダに命中すると、爆発マークの表示期間(ALIEN_EXPL_TMAX tick)のあいだ隊列全体の移動を停止する。停止中も自機・自弾・敵弾・UFO・サウンドの処理は継続し、爆発マークの消去とともに、停止した個体から隊列の移動を再開する。」

これはオリジナル動作を観察して発見した仕様です。理由は不明ながら、爆発マーク表示中も隊列を動かすと、インベーダの動きにより爆発マークが欠ける場合があり、その干渉を防止するためと考えられます。

            for (noy <= 0; noy < `InvRows; noy <= noy + 1) seq
               // 爆発凍結中はカーソルを進めない(増分が no-op になる)
               for (nox <= 0; nox < `InvCols;
                    nox <= (alien_timer == 0) ? nox + 1 : nox) seq
                  if (inv_s[nox][noy] || alien_timer != 0) seq
                     if (alien_timer == 0)
                        updateAliens();
                     else
                        eraseAlienExplosion();
                     updatePlayer();
                     ...(以下、元のまま)...
                  endseq  // if inv
               endseq // for nox
            endseq // for noy

左矢前のブログ 次のブログ右矢

bitmap editorの開発

posted by sakurai on July 28, 2026 #1098

Claude Fable 5に仕様を渡して表記エディタを開発して貰いました。AI以前はこのような小さなヘルパーツールでも、一週間程度はかけて作成及びデバッグしたものでしたが、改良も含めて10分程でできてしまうことに感激です。もちろんコードは一行も書いてません。

ここで仕様の一部を示すと、ベースのcsvを読ませ、エディタによりaパターンとbパターンの作成を行うと、自動的にプレビューでパターン間の動作を確認できます。OKなら最下段のOKボタンを押すと、ベースのデータとスプライトのデータが合成され、ROMデータのcsvとなります。スプライトcsvは別にロードセーブ可能です。

図1098.1
図1098.1 スプライトエディタ


左矢前のブログ 次のブログ右矢

posted by sakurai on March 29, 2026 #1070

Geminiに設計してもらったプログラムを以下に示します。

// --- コンフィギュレーション設定(マイコンの基本動作設定) ---
#pragma config FOSC = INTOSC    // 内蔵クロックを使用
#pragma config WDTE = OFF       // ウォッチドッグタイマーOFF
#pragma config PWRTE = ON       // パワーアップタイマーON
#pragma config MCLRE = OFF      // 4番ピンをリセットに使わずGPIO(RA3)として使う
#pragma config CP = OFF         // コードプロテクトOFF
#pragma config BOREN = ON       // 電圧低下で安全停止
#pragma config CLKOUTEN = OFF   // クロック出力OFF
#pragma config LVP = OFF        // 低電圧プログラミングOFF(ICSP動作を安定させる)

#include <xc.h>

// 動作クロックの定義(__delay_msの計算に必要)
#define _XTAL_FREQ 4000000 

void main(void) {
    // --- マイコンの初期化 ---
    OSCCON = 0x6A;          // 内部クロック4MHzに設定
    ANSELA = 0x00;          // 全ピンをデジタルモードに設定(スイッチ入力に必須)
    TRISA = 0b00100000;     // RA5(2番ピン)を入力、他を出力に設定
    WPUA = 0b00100000;      // RA5を内部プルアップ設定
    OPTION_REGbits.nWPUEN = 0; // 内部プルアップ機能を有効化

    // PWM(調光)の初期設定
    PWM1CON = 0xC0;         // PWM1(5番ピン/RA2)を有効化
    PWM3CON = 0xC0;         // PWM3(3番ピン/RA4)を有効化
    T2CON = 0x04;           // Timer2 ON, プリスケーラ1:1
    PR2 = 255;              // PWMの周期設定

    // 制御用変数の宣言
    unsigned char mode = 1;      // 0:消灯, 1:交互ゆらぎ, 2:全点灯, 3:点滅
    unsigned char duty_L = 0;    // 左側の明るさ用変数
    signed char direction = 5;   // 明るさの変化量(プラスで明るく、マイナスで暗く)

    while(1) {
        // --- モード切り替えスイッチ判定(2番ピンをGNDに落とすと反応) ---
        if (RA5 == 0) {
            __delay_ms(20);      // チャタリング防止
            if (RA5 == 0) {      // 押し続けられているか再確認
                while(RA5 == 0); // 指を離すまで待機
                __delay_ms(20);  // 離した後の安定待ち
                
                mode++;          // モードを1つ進める
                if (mode > 3) mode = 0; // モード0〜3を循環
                
                // モード切替時に明るさ変数をリセット
                duty_L = 0;
                direction = 5;
            }
        }

        // --- 各モードの動作実行 ---
        switch(mode) {
            case 0: // 【全消灯】
                PWM1DCH = 0;
                PWM3DCH = 0;
                break;

            case 1: // 【アナログ交互ゆらぎ】★初期起動モード
                PWM1DCH = duty_L;           // 5番ピン(L)を出力
                PWM3DCH = (unsigned char)(255 - duty_L); // 3番ピン(R)は逆位相で出力
                
                // 明るさを変化させる
                if (direction > 0) {
                    if (duty_L < 250) {
                        duty_L += 5;
                    } else {
                        direction = -5; // 上限で反転
                    }
                } else {
                    if (duty_L > 5) {
                        duty_L -= 5;
                    } else {
                        direction = 5;  // 下限で反転
                    }
                }
                __delay_ms(40); // 変化のスピード調整(ニューヨークの夜景風)
                break;

            case 2: // 【全点灯】
                PWM1DCH = 255;
                PWM3DCH = 255;
                break;

            case 3: // 【同時点滅】
                PWM1DCH = 255;
                PWM3DCH = 255;
                __delay_ms(300);
                PWM1DCH = 0;
                PWM3DCH = 0;
                __delay_ms(300);
                break;
        }
        __delay_ms(10); // ループの安定用ウェイト
    }
}

コンパイル(build)は金槌のアイコンをクリックします。PICへの書き込みはダウンロードのような下向き矢印アイコンをクリックします。読み出しはアップロードのような上向き矢印アイコンをクリックします。書き込んだPICかみ書き込みかは読みだすとわかります。以下はプログラムしたPICをhexで読みだしたファイルの中身です。

:020000040000FA
:100000000128FE2BFF3FFF3FFF3FFF3FFF3FFF3F2A
:10001000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FF0
:10002000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FE0
:10003000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FD0
:10004000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FC0
:
:10061000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FEA
:10062000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FDA
:10063000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FCA
:10064000FF3FFF3FFF3FFF3FFF3FFF3FFF3FFF3FBA
:10065000FF3FFF3FF501F31F2E2B2F2B362BF20907
:10066000F309F20A0319F30A0130F500F11F392BDF
:100670003A2B3F2BF009F109F00A0319F10A70082F
:1006800071040319442B452B6C2B0130F4004F2BC4
:100690000130F035F10D890B492B0130F407F11FC2
:1006A000522B532B482B71087302031D592B7008D2
:1006B0007202031C5C2B5D2B612B7008F202710827
:1006C000F33B0130F136F00C890B622B0130F40260
:1006D000031D6B2B6C2B532B75080319702B712B7F
:1006E000762BF209F309F20A0319F30A7308F100F1
:1006F0007208F0000800203021008C0023008C01DB
:10070000203024008C0021009513043020009C0030
:10071000FF309B00C0302C009300C0309900F801DE
:10072000F90120008C1A952B962BED2B0330F0004D
:100730000030F1007808013EF2000030793DF3000E
:1007400083312A2383317108F9007008F8000230E0
:10075000F7008630F6009930890BAC2BF60BAC2BEA
:10076000F70BAC2B000020008C1EB72BB82BB32B43
:10077000ED2BFF302C009200FF309800912BFA01F6
:100780007A082C0092007A0998000D30F600FC30AF
:10079000890BC82BF60BC82B0130FA077A0A031D08
:1007A000D22BD32BC02BFF30FA007A082C009200FA
:1007B0007A0998000D30F600FC30890BDD2BF60B22
:1007C000DD2B0130FA027A08031DE72BE82BD52B2D
:1007D000912B2C0092019801912B7908003A031972
:1007E000F22B912B7808003A0319B92B013A03191F
:1007F000BF2B033A0319E92B912B012820007B2BF7
:020000040001F9
:04000E00843FFF3FED
:08000000FF3FFF3FFF3FFF3F00
:00000001FF

左矢前のブログ 次のブログ右矢


ページ: