Ichigo Exception <3

日々のつらい気持ち

PIC18で遊んだ

以前作成した PIC18F26K20 を使った GC 入力信号表示装置の改良版として、信号を GC に入力できるような装置を作ろうと思っています(思っているだけ)。

それで久々に PIC18 を触りましたが、やはり C で普通に記述できる魅力はすごいです(アーキテクチャの特性上自由にポインタ操作はできませんが)。フリーで使用できる開発環境 MPLAB とコンパイラ xc8/16/32 がチップベンダーの Microchip 社から提供されています。
GC 信号を入出力するには、1us 周期で HIGH/LOW を制御できる必要がありますが、この調整に苦労していました。
PIC18F26K20 ではチップ内蔵オシレータを最大の 16 MHz で動作させ、PLL で 4 倍の周波数を得ると、IPC が 4 なので秒間 16 サイクルを得られます。随分余裕があるように思いましたが、配列操作にやたら時間がかかり、以下のような普通の for ループでは間に合わず、アセンブリで書いても微妙に 1us に収まりませんでした。

for (i = 0; i < ARRAY_SIZE(a); i++) {
  arr[i] = PORTCbits.RC0;
}

結局全部マクロ展開に任せることにしました。xc8 の制約として、配列インデックスは 128 が最大なので、要素 2n 個で使える方法でぴったりです。

#define EVAL_128(exp, arr, port, n) EVAL_64(exp, arr, port, n - 64) EVAL_64(exp, arr, port, n)
#define EVAL_64(exp, arr, port, n)  EVAL_32(exp, arr, port, n - 32) EVAL_32(exp, arr, port, n)
#define EVAL_32(exp, arr, port, n)  EVAL_16(exp, arr, port, n - 16) EVAL_16(exp, arr, port, n)
#define EVAL_16(exp, arr, port, n)  EVAL_8(exp, arr, port, n - 8)   EVAL_8(exp, arr, port, n)
#define EVAL_8(exp, arr, port, n)   EVAL_4(exp, arr, port, n - 4)   EVAL_4(exp, arr, port, n)
#define EVAL_4(exp, arr, port, n)   EVAL_2(exp, arr, port, n - 2)   EVAL_2(exp, arr, port, n)
#define EVAL_2(exp, arr, port, n)   EVAL_1(exp, arr, port, n - 1)   EVAL_1(exp, arr, port, n)
#define EVAL_1(exp, arr, port, n) exp(arr, port, n)
#define RD(arr, port, n) arr[n] = port;
#define WB(arr, port, n) port = arr[n];

EVAL_128(RD, a, reg, ARRAY_SIZE(a) - 1)  // ポート入力
EVAL_128(WB, a, out, ARRAY_SIZE(a) - 1)  // ポート出力

EVO2017 のホテル・航空券予約が終わり、もう引き返せないところまできました。POINT OF NO RETURN です。

先週、プラスティック・メモリーズを見ました。老体になる前に寿命が来るとは、残酷な世界観です。まほろまてぃっくを感じました。
とはいえ、キャラクター達は悪意のない性格で、少女に戻ったような素直な気持ちで全話観終わることができました(少女ではなかった)。 アイラ可愛いです。

GC コントローラの配線

コントローラの信号を取り出す上で必要な、ケーブルの配線の説明です。ケーブルを切ると、以下の画像のように6本のワイヤが引き出せます。画像ではすでに撚ってしまっていますが、被覆無しのワイヤは元々他5本のシールド線になっています。

f:id:ichigo-exception:20170211231039j:plain

尚、GC 本体側のピンは7本あり、うち1本は浮いているので、残った6本のみが使用されています。それぞれの色は、以下の信号に対応しています。

ケーブル色 機能
振動用電源 +5V
データ用電源 +3.43V
データ用信号 +3.43V Active Low
GND
GND
シールド GND

GND がいっぱいありますが、GC 本体側、コントローラ側両方共に、白はシールド線に繋がっています。緑に関しては、GC 側は白とシールドと全部一緒に繋がっていますが、コントローラ側はシールドにだけ繋がっていて、白には繋がっていません。ノイズ対策で、コントローラ側の回路を本体側で一点アースしているということでしょうか(電気属性の人間じゃないので、良くわかりませんが)。

割り込みアフィニティ

割り込みは物理的には、割り込みコントローラ(PIC: Programmable Interrupt Controller)によって CPU の割り込みピンがアサートされることで発生します(内部割り込みは違います)。SMP(マルチプロセッサ)環境では、割り込みのアフィニティ(どの CPU に割り込みを上げるのか)を決定するため、PIC の Destination Register をセットする必要があります。現代の普通の CPU では、コア数に対応した PIC がチップ内に収められているので、基板を見ても存在しません。古いドキュメントでは、単独チップの Intel 8259 が割り込みの説明に使われているようです。

割り込みを上げる CPU を決定するのは、Linux では request_irqIRQ を登録するタイミングです。ここでは IRQ は、CPU の物理的なピンを指しています。Linux の /proc/interrupts で見える番号(virq)は、IRQ に対応した仮想的な値です。 以下、カーネルバージョン 2.6.39 、CPU は e500mc コア、PIC は MPIC の例です。

/kernel/irq/manage.c
extern int __must_check
request_threaded_irq(unsigned int irq, irq_handler_t handler,
                     irq_handler_t thread_fn,
                     unsigned long flags, const char *name, void *dev);

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
            const char *name, void *dev)
{
        return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

request_irq は request_threaded_irq のラッパーです。

/kernel/irq/manage.c
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
                         irq_handler_t thread_fn, unsigned long irqflags,
                         const char *devname, void *dev_id)
{
        struct irqaction *action;
        struct irq_desc *desc;
        int retval;
        /*
         * Sanity-check: shared interrupts must pass in a real dev-ID,
         * otherwise we'll have trouble later trying to figure out
         * which interrupt is which (messes up the interrupt freeing
         * logic etc).
         */
        if ((irqflags & IRQF_SHARED) && !dev_id)
                return -EINVAL;
        /* desc 番号を取っておく */
        desc = irq_to_desc(irq);
        if (!desc)
                return -EINVAL;
        if (!irq_settings_can_request(desc))
                return -EINVAL;
        if (!handler) {
                if (!thread_fn)
                        return -EINVAL;
                handler = irq_default_primary_handler;
        }
        action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
        if (!action)
                return -ENOMEM;
        action->handler = handler;
        /*
         * request_irq だと thread_fn は NULL
         * thread_fn はカーネルスレッドで実行される
         */
        action->thread_fn = thread_fn;
        action->flags = irqflags;
        action->name = devname;
        action->dev_id = dev_id;
        chip_bus_lock(desc);
        /* action を desc テーブルに登録する */
        retval = __setup_irq(irq, desc, action);
        chip_bus_sync_unlock(desc);
        if (retval)
                kfree(action);

        /* 略 */
        return retval;
}

割り込みハンドラ、フラグ、デバイス名等は、desc テーブル(割り込みディスクリプタ)に登録されます。

/kernel/irq/manage.c
static int
__setup_irq(unsigned int irq, struct irq_desc *desc, struct irqaction *new)
{
        struct irqaction *old, **old_ptr;
        const char *old_name = NULL;
        unsigned long flags, thread_mask = 0;
        int ret, nested, shared = 0;
        cpumask_var_t mask;

        if (!desc)
                return -EINVAL;

        if (desc->irq_data.chip == &no_irq_chip)
                return -ENOSYS;
        /*
         * Some drivers like serial.c use request_irq() heavily,
         * so we have to be careful not to interfere with a
         * running system.
         */
        if (new->flags & IRQF_SAMPLE_RANDOM) {
                /*
                 * This function might sleep, we want to call it first,
                 * outside of the atomic block.
                 * Yes, this might clear the entropy pool if the wrong
                 * driver is attempted to be loaded, without actually
                 * installing a new handler, but is this really a problem,
                 * only the sysadmin is able to do this.
                 */
                rand_initialize_irq(irq);
        }

        /* 略 thread_fn 関連 */

        if (!alloc_cpumask_var(&mask, GFP_KERNEL)) {
                ret = -ENOMEM;
                goto out_thread;
        }

        /*
         * The following block of code has to be executed atomically
         */
        /* IRQ 共有時のチェック */
        raw_spin_lock_irqsave(&desc->lock, flags);
        old_ptr = &desc->action;
        old = *old_ptr;
        if (old) {
                /*
                 * Can't share interrupts unless both agree to and are
                 * the same type (level, edge, polarity). So both flag
                 * fields must have IRQF_SHARED set and the bits which
                 * set the trigger type must match. Also all must
                 * agree on ONESHOT.
                 */
                /*
                 * 異なるタイプの割り込みを SHARED で登録することはできない
                 * 例えば、エッジトリガとレベルトリガのデバイスが同じIRQ番号を共有することはできない
                 */
                if (!((old->flags & new->flags) & IRQF_SHARED) ||
                    ((old->flags ^ new->flags) & IRQF_TRIGGER_MASK) ||
                    ((old->flags ^ new->flags) & IRQF_ONESHOT)) {
                        old_name = old->name;
                        goto mismatch;
                }

                /* All handlers must agree on per-cpuness */
                if ((old->flags & IRQF_PERCPU) !=
                    (new->flags & IRQF_PERCPU))
                        goto mismatch;

                /* add new interrupt at end of irq queue */
                do {
                        thread_mask |= old->thread_mask;
                        old_ptr = &old->next;
                        old = *old_ptr;
                } while (old);
                shared = 1;
        }

        /*
         * Setup the thread mask for this irqaction. Unlikely to have
         * 32 resp 64 irqs sharing one line, but who knows.
         */
        if (new->flags & IRQF_ONESHOT && thread_mask == ~0UL) {
                ret = -EBUSY;
                goto out_mask;
        }
        new->thread_mask = 1 << ffz(thread_mask);

        if (!shared) {
                init_waitqueue_head(&desc->wait_for_threads);

                /* Setup the type (level, edge polarity) if configured: */
                if (new->flags & IRQF_TRIGGER_MASK) {
                        ret = __irq_set_trigger(desc, irq,
                                        new->flags & IRQF_TRIGGER_MASK);

                        if (ret)
                                goto out_mask;
                }

                desc->istate &= ~(IRQS_AUTODETECT | IRQS_SPURIOUS_DISABLED | \
                                  IRQS_ONESHOT | IRQS_WAITING);
                irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);

                if (new->flags & IRQF_PERCPU) {
                        irqd_set(&desc->irq_data, IRQD_PER_CPU);
                        irq_settings_set_per_cpu(desc);
                }

                if (new->flags & IRQF_ONESHOT)
                        desc->istate |= IRQS_ONESHOT;

                if (irq_settings_can_autoenable(desc))
                        irq_startup(desc);
                else
                        /* Undo nested disables: */
                        desc->depth = 1;

                /* Exclude IRQ from balancing if requested */
                if (new->flags & IRQF_NOBALANCING) {
                        irq_settings_set_no_balancing(desc);
                        irqd_set(&desc->irq_data, IRQD_NO_BALANCING);
                }

                /* Set default affinity mask once everything is setup */
                /* ここで割り込みを上げる CPU の初期値を決定する */
                setup_affinity(irq, desc, mask);

        } else if (new->flags & IRQF_TRIGGER_MASK) {
                unsigned int nmsk = new->flags & IRQF_TRIGGER_MASK;
                unsigned int omsk = irq_settings_get_trigger_mask(desc);

                if (nmsk != omsk)
                        /* hope the handler works with current  trigger mode */
                        pr_warning("IRQ %d uses trigger mode %u; requested %u\n",
                                   irq, nmsk, omsk);
        }

        new->irq = irq;
        *old_ptr = new;

        /*
         * 略 後処理色々
         * スピンロックを解除する
         */

        return 0;

mismatch:
        /* 略 */
out_mask:
        /* 略 */
out_thread:
        /* 略 */
}

setup_affinity が、割り込みの上がる CPU を決定する本体のコードです。

/kernel/irq/manage.c
#ifndef CONFIG_AUTO_IRQ_AFFINITY
/*
 * Generic version of the affinity autoselector.
 */
static int
setup_affinity(unsigned int irq, struct irq_desc *desc, struct cpumask *mask)
{
        struct irq_chip *chip = irq_desc_get_chip(desc);
        struct cpumask *set = irq_default_affinity;
        int ret;

        /* Excludes PER_CPU and NO_BALANCE interrupts */
        if (!irq_can_set_affinity(irq))
                return 0;

        /*
         * Preserve an userspace affinity setup, but make sure that
         * one of the targets is online.
         */
        /* /proc/irq/xxx/smp_affinity での設定を準備すると思われる */
        if (irqd_has_set(&desc->irq_data, IRQD_AFFINITY_SET)) {
                if (cpumask_intersects(desc->irq_data.affinity,
                                       cpu_online_mask))
                        set = desc->irq_data.affinity;
                else
                        irqd_clear(&desc->irq_data, IRQD_AFFINITY_SET);
        }

        cpumask_and(mask, cpu_online_mask, set);
        /*
         * アフィニティを設定する
         * chip->irq_set_affinity はハードウェア(PIC)に依存するコード
         */
        ret = chip->irq_set_affinity(&desc->irq_data, mask, false);
        switch (ret) {
        case IRQ_SET_MASK_OK:
                cpumask_copy(desc->irq_data.affinity, mask);
        case IRQ_SET_MASK_OK_NOCOPY:
                irq_set_thread_affinity(desc);
        }
        return 0;
}
#else
static inline int
setup_affinity(unsigned int irq, struct irq_desc *d, struct cpumask *mask)
{
        return irq_select_affinity(irq);
}
#endif

ここでは PIC は MPIC を使用するので、chip->irq_set_affinity は mpic_set_affinity になります。 MPIC は AMD の OpenPIC 規格がベースになっているマルチプロセッサ向け割り込みコントローラです。

/arch/powerpc/sysdev/mpic.c
int mpic_set_affinity(struct irq_data *d, const struct cpumask *cpumask,
                      bool force)
{
        struct mpic *mpic = mpic_from_irq_data(d);
        unsigned int src = mpic_irq_to_hw(d->irq);

        /* 通常 MPIC_SINGLE_DEST_CPU をコンフィグするはず */
        if (mpic->flags & MPIC_SINGLE_DEST_CPU) {
                int cpuid = irq_choose_cpu(cpumask);

                mpic_irq_write(src, MPIC_INFO(IRQ_DESTINATION), 1 << cpuid);
        } else {
                cpumask_var_t tmp;

                alloc_cpumask_var(&tmp, GFP_KERNEL);

                cpumask_and(tmp, cpumask, cpu_online_mask);

                mpic_irq_write(src, MPIC_INFO(IRQ_DESTINATION),
                               mpic_physmask(cpumask_bits(tmp)[0]));

                free_cpumask_var(tmp);
        }

        return 0;
}
/arch/powerpc/sysdev/mpic.c
static int irq_choose_cpu(const struct cpumask *mask)
{
        int cpuid;

        if (cpumask_equal(mask, cpu_all_mask)) {
                static int irq_rover = 0;
                static DEFINE_RAW_SPINLOCK(irq_rover_lock);
                unsigned long flags;

                /* Round-robin distribution... */
                /* CPU の数に応じて、ラウンドロビン形式で割り込み先 CPU を指定している */
        do_round_robin:
                raw_spin_lock_irqsave(&irq_rover_lock, flags);

                irq_rover = cpumask_next(irq_rover, cpu_online_mask);
                if (irq_rover >= nr_cpu_ids)
                        irq_rover = cpumask_first(cpu_online_mask);

                cpuid = irq_rover;

                raw_spin_unlock_irqrestore(&irq_rover_lock, flags);
        } else {
                cpuid = cpumask_first_and(mask, cpu_online_mask);
                if (cpuid >= nr_cpu_ids)
                        goto do_round_robin;
        }

        return get_hard_smp_processor_id(cpuid);
}

IRQ_DESTINATION は、MPIC の割り込み Destination Register に対応しています。 CPU にもよりますが、グローバルタイマ割り込み・IPI(プロセッサ間割り込み)のように複数 CPU を宛先とする割り込みではない外部割り込みは、このレジスタに複数 CPU に相当するビットを立てると動作が未定義になります。このあたりは CPU のリファレンスを読むと詳しく理解することができるでしょう。

また、CONFIG_AUTO_IRQ_AFFINITY がコンフィグされていたら、以下のオートセレクタが動作します。こちらはセレクト方法が PIC に依存しません。

/linux/arch/alpha/kernel/irq.c
#ifdef CONFIG_SMP
static char irq_user_affinity[NR_IRQS];

int irq_select_affinity(unsigned int irq)
{
        struct irq_data *data = irq_get_irq_data(irq);
        struct irq_chip *chip;
        static int last_cpu;
        int cpu = last_cpu + 1;

        if (!data)
                return 1;
        chip = irq_data_get_irq_chip(data);

        if (!chip->irq_set_affinity || irq_user_affinity[irq])
                return 1;

        /*
         * cpu_possible マクロで搭載された CPU かどうか、
         * この CPU にアフィニティ設定ができるかを検査し、CPU をラウンドロビンで選択する
         *
         */
        while (!cpu_possible(cpu) ||
               !cpumask_test_cpu(cpu, irq_default_affinity))
                cpu = (cpu < (NR_CPUS-1) ? cpu + 1 : 0);
        last_cpu = cpu;

        /* アフィニティを設定する */
        cpumask_copy(data->affinity, cpumask_of(cpu));
        chip->irq_set_affinity(data, cpumask_of(cpu), false);
        return 0;
}
#endif /* CONFIG_SMP */

以上で、request_irq から割り込みのアフィニティが決定するまでの過程は終了です。基本的にはカーネル起動時に登録される順序、モジュールがinsmodされる順序は変わらないので、手動で設定しない限り、再起動しても割り込みが上がるCPUは固定になるはずです。