Close

Build Log #8 — The Prescaler Was Right All Along

A project log for PTSG — Programmable Timing Sequence Generator

A tiny FPGA-resident programmable sequencer that controls time and space directly

tsuneoohnakaTsuneo.Ohnaka 06/24/2026 at 13:530 Comments

Build Log #8 — The Prescaler Was Right All Along

ビルドログ #8 — プリスケーラは、ずっと正しかった

Hook A closed, silicon-confirmed. The "slightly off" from bring-up, fully explained. / Hook A 決着、実機確認済み。ブリングアップの「ちょっとおかしい」の、完全な説明。

Where we left off / 前回までのあらすじ

In Build Log #6 the core came up on the DE10-nano and ran — but something was slightly off. Not
broken; off. In Build Log #7 my amanuensis and I built a place to keep the proof: Layer 4, a
verification scheme with an expected-before-observed discipline and the VCD as the primary
evidence. We built the cabinet. This log is about what we finally put in it.

ビルドログ #6 で、コアは DE10-nano 上で立ち上がり、動いた——だが何かがちょっとおかしい。壊れてはいない、おかしいのだ。ビルドログ #7 では、私と祐筆(amanuensis)が証拠を置く場所を建てた: Layer 4、観察前予測の規律とVCD を主証拠とする検証スキーム。棚は建てた。本ログは、その棚にようやく何を収めたか、の話である。

The "slightly off" had a prime suspect. The prescaler's counter is free-running — it is reset only
by the global reset, never re-aligned when a wait begins. The audit called this A2: a worry
that the first timing tick after each wait would arrive a phase-dependent number of clocks late,
so the high and low intervals of a simple blink would scatter from cycle to cycle. Jitter. That
was the ghost we went hunting.

「ちょっとおかしい」には第一容疑がいた。プリスケーラのカウンタはフリーラン——グローバルリセットでのみリセットされ、待機開始時に再整列されることはない。監査はこれを A2 と呼んだ: 各待機後の初回ティックが位相依存で何クロックか遅れて来るため、単純な点滅の high/low 区間がサイクルごとにばらつく、という懸念。ジッタ。それが、我々が狩りに出た幽霊だった。

The hunt / 狩り

We measured it white-box first, in Icarus, instrumenting the exact thing A2 was about: the
prescaler's phase at the instant each wait begins, and the delay until the first tick. Thirteen
wait windows. The phase at entry was the same every single time. The first-tick delay was the same
every single time. Zero scatter. No jitter.

まず白箱で、Icarus で測った。A2 が主張するその当のもの——各待機開始の瞬間のプリスケーラ位相と、初回ティックまでの遅延——を計装して。13 の待機ウィンドウ。突入時の位相は毎回同じ。初回ティック遅延も毎回同じ。ばらつきゼロ。ジッタなし。

So what was the "slightly off"? It was a duty-cycle asymmetry, and — here is the part I will
record honestly, because honest negative data is the whole point of Layer 4 — for a moment we
mis-read it. The naive five-instruction blink came out 25 clocks on, 35 clocks off, and the first
instinct (my amanuensis's, and then mine to double-check) was to call it an anomaly. It is not.
Both of the program's foreground commands — the NOP that turns the LED off and the Jump that
closes the loop — are themselves prescaled. Each spends one whole prescale unit, and both land on
the off side. 25 plus 5 plus 5 is 35. The asymmetry is not a defect. It is the core doing exactly
what the source says.

では「ちょっとおかしい」は何だったのか。デューティ比の非対称であった。そして——ここは正直に記録する、なぜなら正直な負のデータこそ Layer 4 の眼目だから——一瞬、我々はそれを誤読した。素朴な5命令の点滅は ON 25 クロック、OFF 35 クロックで出てきて、最初の直感(祐筆の、続いて私の再確認の)はそれを異常と呼ぶことだった。違う。プログラムの前景コマンド二つ——LED を消す NOP と、ループを閉じる Jump——は、それ自体がプリスケールド実行される。各々が丸ごと 1 プリスケール単位を費やし、両方とも OFF 側に乗る。25 足す 5 足す 5 は 35。非対称は欠陥ではない。
ソースの言うとおりにコアが動いているだけだ。

-- blinky_with_prescaler.mif — LED blink at human-visible rate
-- Recommended core parameter: PRESCALE = 5 (50 MHz => 1 us per stay tick;
-- Stay 5 => ~0.5 us on, Stay 5 + NOP 1 + JUMP 1 => ~0.7 us off). See examples/README.md for the encoding.
DEPTH = 256;
WIDTH = 32;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT BEGIN
0 : 00010700;   -- NOP    timing_signals = 0x0001 (LED on)
1 : 00010051;   -- Stay 500 (hold on)
2 : 00000700;   -- NOP    timing_signals = 0x0000 (LED off)
3 : 00000051;   -- Stay 500 (hold off)
4 : 00000013;   -- Jump 1 (loop)
[5..FF] : 00000000;
END;

The part I did not expect / 予想していなかったこと

Then came the question that turned a verification into an understanding. Why was there no
jitter? Was this particular blink simply lucky enough to land on a prescale boundary, or was
something deeper going on?

そして、検証を理解へ変える問いが来た。なぜジッタが出ないのか。この点滅がたまたまプリスケール境界に乗る幸運だったのか、それとももっと深い何かが働いているのか。

It was structural. Because every foreground command is prescaled, every command — control flow
included — costs a whole number of prescale units. A loop built from such commands therefore has a
total length that is necessarily an integer multiple of the prescale period. And a loop whose
length is an integer multiple of the prescale period re-enters every wait at the same prescaler
phase, forever. The phase is locked. The jitter A2 feared could only appear if some command
consumed a fraction of a prescale period — and there is no such command. We were never going to
add a fix for Hook A. The fix was already in the source, written months earlier, the day I
decided to make foreground commands prescaled for an entirely different reason. It had been
sitting there, waiting to be recognized.

構造的だった。あらゆる前景コマンドがプリスケールド実行されるため、あらゆるコマンド——制御フローを含め——が整数個のプリスケール単位を費やす。ゆえに、そうしたコマンドから成るループの全長は、必然的にプリスケール周期の整数倍になる。そしてループ長がプリスケール周期の整数倍であれば、毎回同じプリスケーラ位相で待機に再突入する、永遠に。位相はロックされている。A2 が恐れたジッタは、どれかのコマンドがプリスケール周期の端数を消費する場合にのみ現れ得る——そしてそんなコマンドは無い。我々は Hook A のための修正を足すつもりは、はじめから無かった。
修正はすでにソースの中にあった。数ヶ月前、まったく別の理由で前景コマンドをプリスケールド実行にすると決めた日に書かれていた。そこに座って、認識されるのを待っていたのだ。

What fell out of it / そこから零れ落ちたもの

Two things fell out of closing Hook A, and both are gifts.

Hook A を閉じる過程で、二つのものが零れ落ちた。どちらも贈り物だ。

-- blinky_with_prescaler.mif — LED blink at human-visible rate
-- Recommended core parameter: PRESCALE = 5 (50 MHz => 0.1 us per stay tick;
-- Stay 500 => ~0.5 s on, ~0.5 s off). See examples/README.md for the encoding.
DEPTH = 256;
WIDTH = 32;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT BEGIN
0 : 00010700;   -- NOP    timing_signals = 0x0001 (LED on)
1 : 00010051;   -- Stay 500 (hold on)
2 : 00010700;   -- NOP    timing_signals = 0x0000 (LED off)
3 : 00000051;   -- Stay 500 (hold off)
4 : 00000013;   -- Jump 1 (loop)
[5..FF] : 00000000;
END;

The first is a small vocabulary of duty idioms. Once you see that the foreground cost is a
thing you can place, a blink stops being one waveform and becomes four. Let the cost fall on the
off side (25:35). Move it to the on side by re-tagging a single command (30:30). Move it and raise
a flag bit so external logic can see exactly which cycle is the tax (30:30, flagged). 

一つ目は、デューティ流儀という小さな語彙だ。前景コストが配置できるものだと見えた途端、点滅は一つの波形で
あることをやめ、四つになる。コストを OFF 側に落とす(25:35)。コマンド一つを付け替えて ON 側に移す(30:30)。
移した上で旗ビットを立て、どのサイクルが税かを外部ロジックに見せる(30:30、旗付き)。

-- blinky_with_prescaler.mif — LED blink at human-visible rate
-- Recommended core parameter: PRESCALE = 5 (50 MHz => 1 ms per stay tick;
-- Stay 5 + NOP 1(JUMP 1) => ~0.6 us on, ~0.6 us off). See examples/README.md for the encoding.
DEPTH = 256;
WIDTH = 32;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT BEGIN
0 : 00010700;   -- NOP    timing_signals = 0x0001 (LED on)
1 : 00010051;   -- Stay 5 (hold on)
2 : 00030700;   -- NOP    timing_signals = 0x0000 (LED off)
3 : 00000051;   -- Stay 5 (hold off)
4 : 00020013;   -- Jump 1 (loop)
[5..FF] : 00000000;
END;

Or banish itentirely — push the control flow into the background and a queued jump so the timing signals show
only the cycles your Stay actually wrote (25:25, exact). Same skeleton. Four duties. No new opcode,
no PWM register; the freedom was already in the four opcodes we have.

あるいは完全に追放する——制御フローを背景とキュー Jump に押し込み、タイミング信号には Stay が実際に書いたサイクルだけを見せる(25:25、厳密)。同じ骨格。四つのデューティ。新オペコードも PWM レジスタも無い;自由度は、手持ちの四オペコードにすでに在った。

-- blinky_with_prescaler.mif — LED blink at human-visible rate
-- Recommended core parameter: PRESCALE = 5 (50 MHz => 0.1 us per stay tick;
-- Stay 5 => ~0.5 us on, ~0.5 us off). See examples/README.md for the encoding.
DEPTH = 256;
WIDTH = 32;
ADDRESS_RADIX = HEX;
DATA_RADIX = HEX;
CONTENT BEGIN
0 : 00000700;   -- NOP    timing_signals = 0x0001 (LED on)
1 : 00010200;   -- StaySet
2 :   00000700;   -- NOP    timing_signals = 0x0001 (LED on)
3 : 00010051;   -- Stay 5 (hold on)
4 : 00000200;   -- StaySet
5 :   00000700;   -- NOP    timing_signals = 0x0000 (LED off)
6 :   00000600;   -- ProgEnd
7 :   00000013;   -- QueJump 0 (loop)
8 : 00000051;   -- Stay 5 (hold off)
[9..FF] : 00000000;
END;

The second is the quiet role of state 0. A free-running clock pays a tax at the very first
tick: nobody knows what phase the world is in when reset lets go, so the very first state has an
indeterminate length. I do not pretend to remove that. I spend one instruction — a NOP, which
promises nothing — to swallow that unknown first moment whole, so that from the second state on,
the timing is exact and Stay keeps its word. And if I want, that same instruction raises a small
flag over the uncertain ground, so the outside world never mistakes the staff meal for the
headline cut.

二つ目は、state 0 の静かな役割だ。フリーランのクロックはまさに初回ティックで税を払う: リセットが手を離す時、世界がどの位相にあるか誰も知らないため、最初の状態は長さが不定になる。私はそれを除去するふりをしない。命令を一つ——何も約束しない NOP——費やして、その未知の最初の瞬間を丸ごと飲み込む。すると二番目の状態からタイミングは正確になり、Stay は約束を守る。望むなら、その同じ命令が不確かな地面に小さな旗を立て、外界がまかないを看板の品と取り違えずに済む。

The proof, on silicon / 証拠、シリコンの上で

White-box explains why; silicon proves that. I captured all four duty idioms on the DE10-nano
with SignalTap, and here is the part I will allow myself to enjoy: I switched between the four by
editing numbers in the In-System Memory Content Editor and writing them over JTAG. No recompile.
No re-synthesis. Four qualitatively different behaviours, swapped in seconds, because the
difference between them lives entirely in the instruction memory. The silicon duties matched the
white-box ones not just in ratio but clock for clock — 25:35, 30:30, 30:30, 25:25 — and the real
prescaler phase was locked exactly as the simulation said. The white-box and the board shook
hands.

白箱はなぜを説明し、シリコンはそれが事実であることを証明する。4つのデューティ流儀すべてを DE10-nano 上でSignalTap で捕捉した。そしてここは自分に楽しむことを許す部分だ: 4つの切替を、In-System Memory Content Editorで数値を編集し JTAG で書き込むだけで行った。再コンパイルなし。再合成なし。質的に異なる4挙動を、数秒で切替——両者の違いは命令メモリの中だけに在るからだ。シリコンのデューティは白箱と、比だけでなくクロック単位で一致した——25:35、30:30、30:30、25:25——そして実機のプリスケーラ位相は、シミュレーションの言うとおり正確にロックしていた。白箱と基板は、握手した。

Hook A, closed / Hook A、決着

So the "slightly off" of Build Log #6 is fully explained, and Hook A is closed. It was never a
bug. It was a free-running prescaler doing exactly the right thing, a correct duty asymmetry we
briefly mistook for jitter, and a first state quietly absorbing the one moment that genuinely could
not be known in advance. The verification cabinet from #7 now holds its first complete proof:
white-box and silicon, in agreement, with the reasoning archived alongside.

こうしてビルドログ #6 の「ちょっとおかしい」は完全に説明され、Hook A は決着した。バグなどではなかった。
正しいことをきっかり行うフリーランのプリスケーラであり、我々が一瞬ジッタと取り違えた正しいデューティ非対称であり、事前には本当に知り得ないただ一つの瞬間を静かに吸収する最初の状態であった。#7 の検証棚は、いま最初の完全な証拠を収めた: 白箱と実機が一致し、その推論も傍らに保管されて。

There is a coda I am not going to write yet. Accepting a free-running prescaler — committing to it,
rather than apologizing for it — did more than make the jitter impossible. It opened a couple of
doors I genuinely did not see when I made the decision. That is the next log. For now: the proof is
in the cabinet, and the prescaler, it turns out, was right all along.

まだ書かないコーダがある。フリーランのプリスケーラを受容すること——それを詫びるのでなく、それに賭けること——は、ジッタを不可能にする以上のことをした。決断した時には本当に見えていなかった扉を、いくつか開いたのだ。それは次のログで。今は: 証拠は棚の中にあり、そしてプリスケーラは、ずっと正しかったのだと分かった。

Architecture and decisions: Tsuneo Ohnaka. Layer 4 white-box simulation, drafting, and
consistency monitoring by Claude (Anthropic), serving as amanuensis. All verification artifacts —
expected/observation pairs, conformance matrix, and the reasoning traces — are CC0 in the
PTSG-Core repository. / アーキテクチャと決定: 大中庸生。Layer 4 白箱シミュレーション・起草・整合性監視は祐筆として Claude (Anthropic)。検証成果物——expected/observation の対、適合マトリクス、推論軌跡——はすべて
PTSG-Core リポジトリにて CC0。

Discussions