Files
gps/soc/software/pps_python/generate_report.py
T

1461 lines
61 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate the Turkish GPS PPS theory and project-mapping report as PDF.
The report is intentionally generated with matplotlib/PdfPages so it remains
self-contained in the existing Python environment and all schematics stay
vector-based in the PDF.
"""
from __future__ import annotations
import argparse
import os
import textwrap
from pathlib import Path
os.environ.setdefault("MPLCONFIGDIR", "/tmp/pps_report_matplotlib")
import matplotlib
matplotlib.use("Agg")
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_pdf import PdfPages
from matplotlib.patches import Circle, FancyArrowPatch, FancyBboxPatch, Rectangle
ROOT = Path(__file__).resolve().parent
OUTPUT = ROOT / "output"
DEFAULT_PDF = ROOT / "GPS_PPS_Teori_Proje_Mapping.pdf"
NAVY = "#12304A"
BLUE = "#176B9B"
CYAN = "#3BA7C4"
GREEN = "#2D8A61"
ORANGE = "#E8893A"
RED = "#C84C4C"
INK = "#17212B"
MUTED = "#596773"
LIGHT = "#EEF3F6"
PALE_BLUE = "#E7F2F8"
PALE_GREEN = "#E9F5EF"
PALE_ORANGE = "#FCF0E5"
A4 = (8.27, 11.69)
class Report:
def __init__(self, output_pdf: Path) -> None:
self.output_pdf = output_pdf
self.pdf = PdfPages(
output_pdf,
metadata={
"Title": "GPS PPS Teorisi ve Proje Mapping'i",
"Author": "Rocket GPS receiver design study",
"Subject": "LNAV, WLS clock bias, cross-timestamp and PI disciplined PPS",
"Keywords": "GPS PPS WLS TOW HOW ABS_SAMPLE PI APB",
},
)
self.page_number = 0
def close(self) -> None:
self.pdf.close()
def new_page(self, title: str, section: str = "GPS PPS TASARIM RAPORU"):
self.page_number += 1
fig = plt.figure(figsize=A4, facecolor="white")
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
ax.add_patch(Rectangle((0, 0.945), 1, 0.055, color=NAVY))
ax.text(0.055, 0.973, section, color="white", fontsize=8.5, va="center")
ax.text(0.055, 0.915, title, color=NAVY, fontsize=19, weight="bold", va="top")
ax.plot([0.055, 0.945], [0.885, 0.885], color=CYAN, linewidth=1.5)
self._footer(ax)
return fig, ax
def _footer(self, ax) -> None:
ax.plot([0.055, 0.945], [0.045, 0.045], color="#C6D0D7", linewidth=0.7)
ax.text(
0.055,
0.022,
"Mevcut yazılım/RTL analizi ve Python zaman modeli",
fontsize=7.2,
color=MUTED,
va="center",
)
ax.text(
0.945,
0.022,
str(self.page_number),
fontsize=8,
color=MUTED,
ha="right",
va="center",
)
def save(self, fig) -> None:
self.pdf.savefig(fig, bbox_inches="tight", pad_inches=0)
plt.close(fig)
def cover(self) -> None:
self.page_number += 1
fig = plt.figure(figsize=A4, facecolor="white")
ax = fig.add_axes([0, 0, 1, 1])
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis("off")
ax.add_patch(Rectangle((0, 0), 1, 1, color=NAVY))
ax.add_patch(Rectangle((0.06, 0.08), 0.012, 0.84, color=CYAN))
ax.text(0.11, 0.83, "GPS PPS", fontsize=42, color="white", weight="bold")
ax.text(
0.11,
0.755,
"Teoriden mevcut projeye zaman mapping'i",
fontsize=20,
color="#D9EBF3",
)
ax.text(
0.11,
0.68,
"LNAV / HOW / TOW • ABS_SAMPLE • WLS clock bias\n"
"125 MHz cross-timestamp • APB atomic commit • PI loop",
fontsize=12.5,
color="white",
linespacing=1.55,
)
self.box(
ax,
0.11,
0.39,
0.77,
0.18,
"Tasarım hedefi",
"Navigation çözümünün ürettiği GPS zaman bilgisini, RF ölçüm epoch'unu "
"kaybetmeden 125 MHz free-running zaman tabanına bağlamak; yazılım ve APB "
"gecikmesini PPS fazına taşımadan düşük bant genişlikli bir PI ile kararlı "
"1 PPS üretmek.",
face="#173B58",
edge="#4AA8C3",
title_color="#8ED6E8",
text_color="white",
)
ax.text(
0.11,
0.19,
"Kapsam: mevcut C ve RTL davranışı + sentetik event-driven doğrulama modeli",
fontsize=10,
color="#BFD5E1",
)
ax.text(
0.11,
0.14,
"Kaynak: software/pps_python",
fontsize=9,
color="#8FB3C6",
)
ax.text(0.89, 0.06, str(self.page_number), fontsize=8, color="#8FB3C6", ha="right")
self.save(fig)
@staticmethod
def box(
ax,
x: float,
y: float,
width: float,
height: float,
title: str,
text: str,
*,
face: str = LIGHT,
edge: str = "#9FB3BF",
title_color: str = NAVY,
text_color: str = INK,
fontsize: float = 9.2,
) -> None:
patch = FancyBboxPatch(
(x, y),
width,
height,
boxstyle="round,pad=0.012,rounding_size=0.012",
facecolor=face,
edgecolor=edge,
linewidth=1.0,
)
ax.add_patch(patch)
ax.text(
x + 0.018,
y + height - 0.025,
title,
color=title_color,
fontsize=10.2,
weight="bold",
va="top",
)
chars = max(24, int(width * 108))
wrapped = "\n".join(textwrap.wrap(text, width=chars))
ax.text(
x + 0.018,
y + height - 0.063,
wrapped,
color=text_color,
fontsize=fontsize,
va="top",
linespacing=1.35,
)
@staticmethod
def arrow(ax, start, end, color=BLUE, text: str | None = None, rad=0.0) -> None:
arrow = FancyArrowPatch(
start,
end,
arrowstyle="-|>",
mutation_scale=12,
linewidth=1.4,
color=color,
connectionstyle=f"arc3,rad={rad}",
)
ax.add_patch(arrow)
if text:
x = (start[0] + end[0]) / 2
y = (start[1] + end[1]) / 2 + 0.018
ax.text(x, y, text, fontsize=8, color=color, ha="center")
@staticmethod
def paragraph(ax, x, y, text, width=92, fontsize=9.5, color=INK, spacing=1.38):
paragraphs = text.split("\n")
lines: list[str] = []
for paragraph in paragraphs:
if paragraph.strip():
lines.extend(textwrap.wrap(paragraph, width=width))
else:
lines.append("")
ax.text(
x,
y,
"\n".join(lines),
fontsize=fontsize,
color=color,
va="top",
linespacing=spacing,
)
line_height = 0.0180 * (fontsize / 9.5) * spacing / 1.38
return y - len(lines) * line_height
@staticmethod
def bullets(ax, x, y, items, width=82, fontsize=9.2, color=INK, gap=0.014):
for item in items:
wrapped = textwrap.wrap(item, width=width)
ax.text(x, y, "•", fontsize=fontsize + 2, color=BLUE, va="top")
ax.text(
x + 0.022,
y,
"\n".join(wrapped),
fontsize=fontsize,
color=color,
va="top",
linespacing=1.32,
)
y -= len(wrapped) * 0.021 * (fontsize / 9.2) + gap
return y
@staticmethod
def equation(ax, y, equation: str, caption: str | None = None, face=PALE_BLUE):
height = 0.083 if caption else 0.06
ax.add_patch(
FancyBboxPatch(
(0.095, y - height),
0.81,
height,
boxstyle="round,pad=0.008,rounding_size=0.008",
facecolor=face,
edgecolor="#A9C7D7",
)
)
equation_fontsize = 14.0
if len(equation) > 70:
equation_fontsize = 11.5
if len(equation) > 105:
equation_fontsize = 9.6
ax.text(
0.5,
y - 0.027,
f"${equation}$",
fontsize=equation_fontsize,
color=NAVY,
ha="center",
va="center",
)
if caption:
ax.text(0.5, y - 0.060, caption, fontsize=8.2, color=MUTED, ha="center")
return y - height - 0.018
def page_executive(report: Report) -> None:
fig, ax = report.new_page("Yönetici özeti")
y = 0.85
y = report.paragraph(
ax,
0.07,
y,
"Bu projede konum WLS'i doğru çalışırken elde edilen dördüncü state, doğrudan "
"125 MHz receiver clock fazı değildir. Pseudorange'lar her epoch'ta minimum "
"ABS_SAMPLE ile normalize edildiği için RF sayacının mutlak fazı observation'dan "
"çıkar. WLS bias; 68.802 ms sentetik datum ile minimum kanalın gözlenen uçuş "
"süresi arasındaki ortak farkı taşır.",
width=100,
fontsize=10.2,
)
report.box(
ax,
0.07,
0.61,
0.4,
0.16,
"Yanlış başlangıç",
"İlk HOW/TOW interrupt'ı geldiğinde PPS zamanını 'şimdi' kabul etmek. HOW transmit "
"subframe sınırını etiketler; receive event bundan signal flight time kadar sonradır.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.box(
ax,
0.53,
0.61,
0.4,
0.16,
"Doğru başlangıç",
"125 MHz raw counter sürekli çalışır. İlk HOW mutlak zaman etiketini sağlar. İlk "
"coherent WLS, uçuş süresini çıkarır ve GPS receive time'ı aynı epoch'un 125 MHz "
"timestamp'ına bağlar.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.equation(
ax,
0.55,
r"T_{GPS,k}=T_{TX,k}+S-\frac{b_k}{c}",
"Minimum corrected-ABS kanalının receive GPS zamanı; ilk delivery için TTX,k = HOW + 0.180 s",
)
report.equation(
ax,
0.43,
r"e_k=T_{GPS,k}-T_{LOCAL}(C_{125,k})",
"PI'ye girecek gerçek phase error; GPS reference − local disciplined time",
face=PALE_GREEN,
)
report.bullets(
ax,
0.08,
0.32,
[
"Minimum kanal yalnız datum'u seçer; clock bias bütün uydularla birlikte çözülür.",
"WLS compute ve APB gecikmesi epoch timestamp ile taşınır; PI bu gecikmeyi oscillator hatası sanmaz.",
"Raw 125 MHz counter resetlenmez. PPS, ayrı bir disciplined accumulator/NCO üzerinden üretilir.",
"İlk dört uydulu çözüm zaman açısından daha gürültülüdür; coarse alignment yapılabilir fakat loop düşük bant genişlikli tutulmalıdır.",
],
width=94,
)
report.save(fig)
def page_time_vocabulary(report: Report) -> None:
fig, ax = report.new_page("Önce zamanları ayıralım")
report.box(
ax,
0.06,
0.70,
0.27,
0.15,
"GPS system time",
"Uyduların navigation mesajında etiketlenen ortak zaman. HOW Z-count bunun 6 saniyelik kaba sınırını verir.",
face=PALE_BLUE,
)
report.box(
ax,
0.365,
0.70,
0.27,
0.15,
"RF sample time",
"16.368 MHz AFE kenarlarının sayımı. ABS_SAMPLE sinyalin receiver'a hangi RF sample civarında ulaştığını ölçer.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
)
report.box(
ax,
0.67,
0.70,
0.27,
0.15,
"Tracking/PPS time",
"125 MHz free-running counter ve ondan türetilen disciplined GPS accumulator. Fiziksel PPS kenarı 8 ns grid üzerindedir.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.arrow(ax, (0.33, 0.775), (0.365, 0.775), text="ölçüm")
report.arrow(ax, (0.635, 0.775), (0.67, 0.775), text="mapping")
y = 0.64
y = report.paragraph(
ax,
0.075,
y,
"Sorunun özü üç zamanın aynı şey sanılmasıdır. GPS TOW transmit zamanını, "
"ABS_SAMPLE receive olayını, 125 MHz counter ise local oscillator fazını temsil eder. "
"PPS tasarımı bu üçü arasında timestamp'lı bir dönüşüm kurar.",
width=99,
fontsize=10,
)
report.equation(ax, 0.52, r"t_{RX,i}=t_{TX,i}+\tau_i", "Signal time-of-flight ile transmit → receive dönüşümü")
report.equation(ax, 0.41, r"C_{125}(t)\simeq C_0+125\,\mathrm{MHz}\,(1+\epsilon_f)t", "Free-running local oscillator; εf drift/frequency error")
report.equation(ax, 0.30, r"T_{DISC}(C)=T_0+\alpha(C-C_0)", "PPS modülünün kuracağı GPS-time mapping; α PI ile yavaşça disipline edilir", face=PALE_GREEN)
report.box(
ax,
0.12,
0.09,
0.76,
0.10,
"Temel kural",
"Bir zaman değeri tek başına yeterli değildir. Her GPS zaman referansı, geçerli olduğu hardware counter değeri ve epoch sequence ile birlikte taşınmalıdır.",
face="#F6F8F9",
)
report.save(fig)
def page_lnav_timeline(report: Report) -> None:
fig, ax = report.new_page("GPS L1 C/A LNAV zaman hiyerarşisi")
ax.text(0.07, 0.84, "Bir navigation bit", fontsize=9, color=MUTED)
ax.add_patch(Rectangle((0.20, 0.815), 0.13, 0.045, facecolor=ORANGE, edgecolor="white"))
ax.text(0.265, 0.837, "20 ms", color="white", weight="bold", ha="center", va="center", fontsize=9)
report.arrow(ax, (0.34, 0.838), (0.43, 0.838), text="30 bit")
ax.add_patch(Rectangle((0.45, 0.815), 0.18, 0.045, facecolor=BLUE, edgecolor="white"))
ax.text(0.54, 0.837, "1 word = 600 ms", color="white", weight="bold", ha="center", va="center", fontsize=9)
report.arrow(ax, (0.65, 0.838), (0.72, 0.838), text="10 word")
ax.add_patch(Rectangle((0.74, 0.815), 0.18, 0.045, facecolor=GREEN, edgecolor="white"))
ax.text(0.83, 0.837, "1 subframe = 6 s", color="white", weight="bold", ha="center", va="center", fontsize=9)
ax.text(0.07, 0.75, "Bir GPS frame = 5 subframe = 30 s", fontsize=10, color=NAVY, weight="bold")
colors = ["#367FA7", "#438EAE", "#519DB4", "#63ACB9", "#78BBC0"]
for index in range(5):
x = 0.08 + index * 0.17
ax.add_patch(Rectangle((x, 0.685), 0.16, 0.05, facecolor=colors[index], edgecolor="white"))
ax.text(x + 0.08, 0.71, f"SF {index+1}\n6 s", color="white", ha="center", va="center", fontsize=8)
ax.text(0.07, 0.62, "Bir subframe'in 10 word'ü", fontsize=10, color=NAVY, weight="bold")
for index in range(10):
x = 0.08 + index * 0.085
color = ORANGE if index < 2 else "#C9D7DE"
ax.add_patch(Rectangle((x, 0.555), 0.078, 0.05, facecolor=color, edgecolor="white"))
label = "TLM" if index == 0 else "HOW" if index == 1 else f"W{index+1}"
ax.text(x + 0.039, 0.58, label, color="white" if index < 2 else INK, ha="center", va="center", fontsize=7.5, weight="bold" if index < 2 else "normal")
report.arrow(ax, (0.205, 0.545), (0.86, 0.49), color=ORANGE, text="HOW Z-count: sonraki 6 s subframe sınırının transmit-time etiketi", rad=-0.12)
ax.add_patch(FancyBboxPatch((0.18, 0.445), 0.64, 0.045, boxstyle="round,pad=0.006", facecolor=PALE_ORANGE, edgecolor=ORANGE))
ax.text(0.50, 0.467, "Mevcut hardware 9. takip eden bitte teslim eder: 9 × 20 ms = POSTPONE_TOW = 180 ms", fontsize=7.6, color=ORANGE, ha="center", va="center", weight="bold")
report.box(
ax,
0.07,
0.31,
0.41,
0.13,
"Projede hardware ne yapıyor?",
"TLM preamble detection, word polarity/tümleyen ve parity akışı tracking wrapper'da yapılır. Software'e 10 adet navigation word ve subframe-ready interrupt gelir.",
face=PALE_BLUE,
)
report.box(
ax,
0.52,
0.31,
0.41,
0.13,
"Software ne yapıyor?",
"parse_subframe(), HOW alanını çıkarıp Z-count × 6 ile tow.sec yapar; subframe 1–3 ephemeris, SF4 page 18 iono/UTC, SF5 almanac referansını günceller.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.box(
ax,
0.12,
0.12,
0.76,
0.10,
"PPS açısından kritik anlam",
"HOW zaman etiketi receiver interrupt anı değildir. Uydunun saat düzeltmesi, signal flight time ve tracking/pipeline gecikmesi eklenmeden 125 MHz local counter'a bağlanamaz.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.save(fig)
def page_tx_rx(report: Report) -> None:
fig, ax = report.new_page("Transmit subframe sınırı receiver'a ne zaman gelir?")
ax.text(0.08, 0.82, "GPS transmit time", fontsize=9, color=NAVY, weight="bold")
ax.plot([0.12, 0.88], [0.76, 0.76], color=NAVY, linewidth=2)
for x, label in [(0.22, "SF n başı"), (0.62, "SF n+1 başı\nHOW etiketi")]:
ax.plot([x, x], [0.735, 0.785], color=NAVY, linewidth=2)
ax.text(x, 0.70, label, fontsize=8, ha="center", color=NAVY)
satellites = [("SV-A", 0.62, 0.67, "yakın"), ("SV-B", 0.62, 0.655, "orta"), ("SV-C", 0.62, 0.635, "uzak")]
ax.text(0.08, 0.60, "Receiver events", fontsize=9, color=GREEN, weight="bold")
ax.plot([0.12, 0.88], [0.54, 0.54], color=GREEN, linewidth=2)
for name, tx_x, rx_x, distance in satellites:
report.arrow(ax, (tx_x, 0.75), (rx_x, 0.55), color=ORANGE)
ax.plot([rx_x, rx_x], [0.515, 0.565], color=GREEN, linewidth=1.6)
ax.text(rx_x, 0.48, f"{name}\n{distance}", ha="center", fontsize=7.5)
ax.annotate(
"τi = path / c − satellite clock",
xy=(0.69, 0.64),
xytext=(0.73, 0.69),
arrowprops=dict(arrowstyle="->", color=ORANGE),
fontsize=8,
color=ORANGE,
)
report.equation(ax, 0.42, r"T_{RX,i}=T_{HOW}+\frac{\rho_i+I_i+T_i}{c}-\delta t_{SV,i}", "Aynı transmit sınırı her kanalda farklı receive timestamp üretir")
ax.text(0.50, 0.325, r"Hardware delivery epoch: $T_{event,i}=T_{HOW}+0.180\,s+\tau_i$", fontsize=9.2, color=ORANGE, ha="center", weight="bold")
report.box(
ax,
0.07,
0.19,
0.41,
0.12,
"Bütün kanallar zaten locked ise",
"Aynı subframe sınırında en kısa gözlenen uçuş süresine sahip kanal önce event üretir. Fark birkaç ms mertebesinde olabilir.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.box(
ax,
0.52,
0.19,
0.41,
0.12,
"Cold-start sırasında",
"Acquisition tamamlanması, bit lock ve hangi tam subframe'in yakalandığı 6 saniyelik adımlarla değişebilir; ilk HOW yalnız yakınlıkla seçilmez.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
)
report.save(fig)
def page_project_map(report: Report) -> None:
fig, ax = report.new_page("Mevcut projede ölçümün uçtan uca akışı")
boxes = [
(0.06, 0.73, 0.17, 0.10, "RF / AFE", "16.368 MHz\n4-bit RF data", PALE_ORANGE, ORANGE),
(0.28, 0.73, 0.20, 0.10, "sample_counter.sv", "trk_clk alanında\nAFE edge sayımı", PALE_ORANGE, ORANGE),
(0.54, 0.73, 0.18, 0.10, "trck_top / bit_gen", "ABS_SAMPLE\nREM_CODE_PHASE", PALE_BLUE, BLUE),
(0.77, 0.73, 0.17, 0.10, "tracking_wrapper", "PCLK register\ninterrupt flags", PALE_BLUE, BLUE),
(0.77, 0.54, 0.17, 0.10, "Tracking ISR", "event queue +\nshadow counter", LIGHT, NAVY),
(0.54, 0.54, 0.18, 0.10, "Linked-list state", "TOW owner\nsequence + sample", LIGHT, NAVY),
(0.28, 0.54, 0.20, 0.10, "Coherent snapshot", "25 ms window\nmodulo unwrap", PALE_GREEN, GREEN),
(0.06, 0.54, 0.17, 0.10, "Pseudorange", "minimum +\n68.802 ms", PALE_GREEN, GREEN),
(0.06, 0.34, 0.17, 0.10, "Satellite model", "TOW + 180 ms\nephemeris/clock", PALE_BLUE, BLUE),
(0.32, 0.34, 0.23, 0.10, "Weighted LS", "[X Y Z b]\niono + tropo + URA/el", PALE_GREEN, GREEN),
(0.64, 0.34, 0.30, 0.10, "Bugünkü çıkış", "position log + static last_solution[4]\nclock bias dışarı taşınmıyor", "#FBEAEA", RED),
]
for x, y, w, h, title, text, face, color in boxes:
report.box(ax, x, y, w, h, title, text, face=face, edge=color, title_color=color, fontsize=7.5)
for start, end in [
((0.23, 0.78), (0.28, 0.78)),
((0.48, 0.78), (0.54, 0.78)),
((0.72, 0.78), (0.77, 0.78)),
((0.855, 0.73), (0.855, 0.64)),
((0.77, 0.59), (0.72, 0.59)),
((0.54, 0.59), (0.48, 0.59)),
((0.28, 0.59), (0.23, 0.59)),
((0.145, 0.54), (0.145, 0.44)),
((0.23, 0.39), (0.32, 0.39)),
((0.55, 0.39), (0.64, 0.39)),
]:
report.arrow(ax, start, end)
report.box(
ax,
0.08,
0.14,
0.84,
0.11,
"PPS için eklenecek veri bağı",
"ABS_SAMPLE ve REM_CODE_PHASE ile aynı logical measurement eventinde 64-bit tracking_counter_125 latch edilecek. WLS sonucu [b, GPS transmit label, reference SVID, counter timestamp, epoch sequence] olarak atomik taşınacak.",
face="#E8F4F8",
edge=CYAN,
title_color=BLUE,
)
report.save(fig)
def page_nav_epoch(report: Report) -> None:
fig, ax = report.new_page("Nav-solve epoch ve coherency davranışı")
ax.text(0.07, 0.84, "Transmit nav bit sınırları (20 ms)", fontsize=9, color=NAVY, weight="bold")
for index in range(11):
x = 0.1 + index * 0.075
ax.plot([x, x], [0.785, 0.82], color=NAVY, linewidth=1)
if index in (0, 10):
ax.text(x, 0.76, f"{index*20} ms", fontsize=7, ha="center", color=MUTED)
ax.plot([0.1, 0.85], [0.80, 0.80], color=NAVY, linewidth=1.4)
ax.annotate("NAV_SOLVE_TIMER=10", xy=(0.475, 0.825), ha="center", fontsize=8.5, color=BLUE)
rows = [("SV 2", 0.12, BLUE), ("SV 5", 0.145, GREEN), ("SV 7", 0.17, ORANGE), ("SV 9", 0.19, RED)]
base_y = 0.66
for row, (name, delay, color) in enumerate(rows):
y = base_y - row * 0.08
ax.text(0.07, y, name, fontsize=8.5, color=color, va="center")
ax.plot([0.14, 0.90], [y, y], color="#C8D2D8", linewidth=1)
x = 0.62 + delay
ax.plot([x, x], [y - 0.022, y + 0.022], color=color, linewidth=3)
ax.text(x, y + 0.03, "ABS/REM", fontsize=6.8, ha="center", color=color)
ax.add_patch(Rectangle((0.72, 0.385), 0.18, 0.32, fill=False, edgecolor=CYAN, linewidth=1.5, linestyle="--"))
ax.text(0.81, 0.37, "25 ms coherence window", fontsize=8, color=CYAN, ha="center")
report.box(
ax,
0.07,
0.20,
0.40,
0.11,
"Software snapshot",
"En yeni raw ABS_SAMPLE referans alınır. Diğer kanallar signed modulo-32 delta ile unwrap edilir; corrected double yalnız pseudorange'a gider.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.box(
ax,
0.53,
0.20,
0.40,
0.11,
"TOW ownership",
"İlk gerçek nav-solve eventine kadar HOW günceller. Sonra her event TOW'u sabit N×20 ms artırır; arkadaki nav bits ephemeris'i günceller fakat live TOW'u yazmaz.",
face=PALE_BLUE,
)
ax.text(
0.08,
0.13,
"PPS timestamp da aynı snapshot kuralını izlemeli; ISR'ın okuma anı ölçüm epoch'u değildir.",
fontsize=9.5,
color=RED,
weight="bold",
)
report.save(fig)
def page_pseudorange(report: Report) -> None:
fig, ax = report.new_page("Mevcut pseudorange datum'u nasıl kuruluyor?")
y = 0.84
ax.text(0.08, y, "1 Fractional RF sample düzeltmesi", fontsize=11, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"A_i^{corr}=A_i^{raw}-\frac{REM_i}{1.023\times10^6}")
ax.text(0.08, y, "2 Wrap-safe coherent unwrap ve minimum seçimi", fontsize=11, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"A_{min}=\min_i\left(A_i^{corr}\right)")
ax.text(0.08, y, "3 C kodundaki sentetik propagation datum'u", fontsize=11, color=NAVY, weight="bold")
y = report.equation(
ax,
y - 0.025,
r"P'_i=c\left[\frac{A_i^{corr}-A_{min}}{16.368\,MHz}+S\right]+c\,\delta t_{SV,i}",
"S = 68.802 ms; gps_ip.c içinde ms formunda hesaplanır",
face=PALE_ORANGE,
)
report.box(
ax,
0.07,
0.17,
0.40,
0.14,
"Minimum neden var?",
"Raw ABS_SAMPLE'ın receiver açılışına bağlı büyük ortak sayısını çıkarır. Uydular arası arrival-time farkları korunur; konum geometrisi bu farklarda bulunur.",
face=PALE_BLUE,
)
report.box(
ax,
0.53,
0.17,
0.40,
0.14,
"68.802 ms neden var?",
"Göreli range'leri fiziksel 20–26 bin km büyüklüğüne taşır ve WLS common bias'ı sıfıra yakın tutar. Yeni bilgi eklemez; datum seçer.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
ax.text(
0.10,
0.10,
"Sonuç: common RF counter fazı (A0) minimum çıkarımında iptal olur. Bu nedenle WLS b doğrudan physical 125 MHz clock phase değildir.",
fontsize=9.2,
color=RED,
weight="bold",
)
report.save(fig)
def page_wls_theory(report: Report) -> None:
fig, ax = report.new_page("Weighted least squares ve dördüncü state")
y = 0.84
y = report.paragraph(
ax,
0.075,
y,
"Standart GNSS observation modeli receiver position ve receiver clock bias'ı aynı anda çözer. "
"Bu projede uydu saat düzeltmesi pseudorange'a eklenir; ionosphere ve troposphere WLS residual modelinde çıkarılır.",
width=99,
fontsize=10,
)
y = report.equation(ax, y - 0.01, r"P_i=\rho_i(X,Y,Z)+b+I_i+T_i+\epsilon_i")
y = report.equation(ax, y, r"\mathbf{x}=[X\;Y\;Z\;b]^T,\qquad A_i=[-\hat{u}_{ix}\;-\hat{u}_{iy}\;-\hat{u}_{iz}\;1]")
y = report.equation(ax, y, r"\Delta\mathbf{x}=(A^TWA)^{-1}A^TW\,\mathbf{r}")
report.box(
ax,
0.07,
0.30,
0.40,
0.16,
"Ağırlık modeli",
"URA varyansı ile elevation-dependent 3 m/sin(el) varyansı toplanır; ağırlık 1/variance olur ve maksimum ağırlığa normalize edilir. Normalize etmek çözümü değiştirmez.",
face=PALE_BLUE,
)
report.box(
ax,
0.53,
0.30,
0.40,
0.16,
"Projeye özgü b",
"Matematiksel olarak common range bias state'idir. Fakat observation minimum ABS_SAMPLE ve S ile tanımlandığı için fiziksel local timer − GPS time anlamı ancak TOW ve timestamp ile handover yapıldığında oluşur.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
)
report.box(
ax,
0.10,
0.12,
0.80,
0.10,
"C implementasyon notu",
"last_solution[4] bütün state'leri bir sonraki solve'a seed eder. Convergence normVect(xVec) ile yalnız XYZ update'e bakar; final clock update Python CSV'sinde ayrıca izlenir.",
face="#F6F8F9",
)
report.save(fig)
def page_bias_derivation(report: Report) -> None:
fig, ax = report.new_page("WLS bias'tan signal flight time nasıl çıkar?")
y = 0.84
ax.text(0.08, y, "Aynı transmit nav-bit sınırı için receiver arrival zamanı:", fontsize=10, color=NAVY, weight="bold")
y = report.equation(ax, y - 0.025, r"t_{RX,i}=t_{TX}+\frac{\rho_i+I_i+T_i}{c}-\delta t_{SV,i}")
ax.text(0.08, y, "Bu ifade minimum-normalized observation'a yerleştirilirse:", fontsize=10, color=NAVY, weight="bold")
y = report.equation(
ax,
y - 0.025,
r"b=cS-(\rho_{min}+I_{min}+T_{min})+c\,\delta t_{SV,min}",
"b, bütün uydularla çözülür fakat observation origin'i minimum kanaldır",
face=PALE_ORANGE,
)
ax.text(0.08, y, "Dolayısıyla minimum kanalın gözlenen uçuş süresi:", fontsize=10, color=NAVY, weight="bold")
y = report.equation(
ax,
y - 0.025,
r"\tau_{min}^{obs}=S-\frac{b}{c}=\frac{\rho_{min}+I_{min}+T_{min}}{c}-\delta t_{SV,min}",
"Bu değer transmit GPS time'ı receive GPS time'a taşır",
face=PALE_GREEN,
)
report.box(
ax,
0.07,
0.16,
0.40,
0.13,
"Neden raw b/c PI'ye verilmez?",
"Referans uydu değişince path_min değişir ve b/c milisaniyelik basamak yapar. Bu oscillator phase step değildir; datum değişimidir.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.box(
ax,
0.53,
0.16,
0.40,
0.13,
"Neden TOW + S − b/c süreklidir?",
"Her epoch'un transmit etiketi ve seçilen kanalın timestamp'ı birlikte değişir. Doğru çift kurulduğunda receiver GPS time sürekli kalır.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.save(fig)
def page_counter_mapping(report: Report) -> None:
fig, ax = report.new_page("GPS time ile 125 MHz counter mapping'i")
ax.text(0.08, 0.84, "İlk coherent WLS epoch'u k", fontsize=11, color=NAVY, weight="bold")
boxes = [
(0.07, 0.68, 0.25, 0.11, "Transmit etiketi", "İlk delivery: HOW + 0.180 s\nsonra nav-solve epoch", PALE_BLUE, BLUE),
(0.375, 0.68, 0.25, 0.11, "WLS flight time", "τmin = S − b/c\nall-satellite solution", PALE_GREEN, GREEN),
(0.68, 0.68, 0.25, 0.11, "Receive reference", "T_GPS,k = T_TX,k + τmin", PALE_ORANGE, ORANGE),
(0.375, 0.47, 0.25, 0.11, "Hardware timestamp", "C125,k\naynı ABS/REM event", PALE_BLUE, BLUE),
(0.68, 0.47, 0.25, 0.11, "Mapping anchor", "(C125,k, T_GPS,k, seq)", PALE_GREEN, GREEN),
]
for x, y, w, h, title, text, face, color in boxes:
report.box(ax, x, y, w, h, title, text, face=face, edge=color, title_color=color, fontsize=8)
report.arrow(ax, (0.32, 0.735), (0.375, 0.735))
report.arrow(ax, (0.625, 0.735), (0.68, 0.735))
report.arrow(ax, (0.805, 0.68), (0.805, 0.58))
report.arrow(ax, (0.625, 0.525), (0.68, 0.525))
ax.text(
0.50,
0.632,
"Ownership: baremetal T_TX,k = eph.tow + POSTPONE_TOW'u bir kez kurar;\n"
"PPS peripheral tekrar eklemez.",
fontsize=7.2,
color=RED,
ha="center",
va="top",
weight="bold",
linespacing=1.2,
)
report.equation(
ax,
0.39,
r"T_{GPS}(C)=T_{GPS,k}+\alpha_k(C-C_{125,k})",
"Başlangıçta α ≈ 1/125 MHz; TRACK state'inde PI ile küçük frequency düzeltmesi eklenir",
face=PALE_GREEN,
)
report.box(
ax,
0.07,
0.14,
0.40,
0.12,
"Bir başka uydu kullanılırsa",
"Kendi ρ+I+T ve satellite-clock düzeltmesiyle kendi receive time'ı bulunur; kendi C125 timestamp'ına bağlandığında aynı affine mapping çıkmalıdır.",
face=PALE_BLUE,
)
report.box(
ax,
0.53,
0.14,
0.40,
0.12,
"Coherency residual",
"Per-SV mapping farkları PPS'i ayrı ayrı başlatmak için değil, stale sample, yanlış TOW veya channel timing hatasını yakalamak için kullanılabilir.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
)
report.save(fig)
def page_state_machine(report: Report) -> None:
fig, ax = report.new_page("PPS zaman ownership state machine")
states = {
"FREE_RUNNING": (0.08, 0.68, PALE_BLUE, BLUE),
"TOW_KNOWN": (0.39, 0.68, PALE_ORANGE, ORANGE),
"COARSE_ALIGNED": (0.69, 0.68, PALE_GREEN, GREEN),
"TRACK": (0.69, 0.42, PALE_GREEN, GREEN),
"HOLDOVER": (0.39, 0.42, LIGHT, NAVY),
}
descriptions = {
"FREE_RUNNING": "Raw 125 MHz counter çalışır\nGPS time valid değil",
"TOW_KNOWN": "İlk valid HOW/week\n6 s absolute label",
"COARSE_ALIGNED": "İlk coherent WLS\nanchor atomik commit",
"TRACK": "Her epoch phase error\nPI rate discipline",
"HOLDOVER": "Son frequency tutulur\nPPS quality downgraded",
}
for name, (x, y, face, color) in states.items():
report.box(ax, x, y, 0.23, 0.12, name, descriptions[name], face=face, edge=color, title_color=color, fontsize=8)
report.arrow(ax, (0.31, 0.74), (0.39, 0.74), text="ilk HOW")
report.arrow(ax, (0.62, 0.74), (0.69, 0.74), text="WLS + timestamp")
report.arrow(ax, (0.805, 0.68), (0.805, 0.54), text="coarse commit")
report.arrow(ax, (0.69, 0.48), (0.62, 0.48), text="solution loss")
report.arrow(ax, (0.51, 0.54), (0.70, 0.64), text="valid WLS returns", rad=-0.22)
report.box(
ax,
0.08,
0.20,
0.40,
0.12,
"PPS valid politikası",
"TOW_KNOWN yalnız kaba time-of-week sağlar. Accurate PPS valid bayrağı COARSE_ALIGNED sonrası açılır. Dört uydu çözümü valid olabilir fakat quality düşük raporlanır.",
face=PALE_BLUE,
)
report.box(
ax,
0.53,
0.20,
0.40,
0.12,
"Sayaç reset politikası",
"Raw counter hiçbir state geçişinde resetlenmez. Coarse alignment yalnız disciplined accumulator'ın anchor/offset'ini değiştirir; TRACK sırasında phase jump yapılmaz.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.save(fig)
def page_cross_timestamp(report: Report) -> None:
fig, ax = report.new_page("Cross-timestamp ve clock-domain sınırı")
ax.add_patch(Rectangle((0.06, 0.40), 0.40, 0.40, facecolor="#FFF6EC", edgecolor=ORANGE, linewidth=1.2))
ax.add_patch(Rectangle((0.54, 0.40), 0.40, 0.40, facecolor="#EAF5F8", edgecolor=BLUE, linewidth=1.2))
ax.text(0.08, 0.77, "RF / measurement tarafı", color=ORANGE, weight="bold", fontsize=11)
ax.text(0.56, 0.77, "125 MHz / PPS tarafı", color=BLUE, weight="bold", fontsize=11)
report.box(ax, 0.10, 0.61, 0.31, 0.10, "sample_counter", "AFE edge count\nABS_SAMPLE raw", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8)
report.box(ax, 0.10, 0.46, 0.31, 0.10, "code boundary", "REM_CODE_PHASE\ncorrected ABS", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8)
report.box(ax, 0.59, 0.61, 0.30, 0.10, "raw_counter_125", "64-bit, always running\n8 ns integer tick", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=8)
report.box(ax, 0.59, 0.46, 0.30, 0.10, "source snapshot", "counter latch + event toggle\nstable bundled data", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8)
report.arrow(ax, (0.41, 0.515), (0.59, 0.515), color=CYAN, text="aynı logical event")
report.arrow(ax, (0.74, 0.61), (0.74, 0.56), color=BLUE)
report.box(
ax,
0.07,
0.23,
0.40,
0.10,
"CDC kuralı",
"Increment eden 64-bit counter bus'ı doğrudan PCLK'de okunmaz. Source domain latch + toggle/ack veya yeterince uzun stable bundled-data protokolü kullanılır.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.box(
ax,
0.53,
0.23,
0.40,
0.10,
"Fractional düzeltme",
"REM correction RF raw edge'den code boundary'ye geri taşır. Aynı süre 125 MHz timestamp'a da Q-format fractional tick olarak uygulanmalıdır.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
ax.text(
0.09,
0.14,
"Önerilen measurement tuple: {epoch_seq, raw_abs, rem_code_phase, tracking_counter_125, SVID}",
fontsize=9.3,
color=NAVY,
weight="bold",
)
report.save(fig)
def page_apb_latency(report: Report) -> None:
fig, ax = report.new_page("WLS compute ve APB gecikmesi faza nasıl karışmaz?")
timeline_y = 0.74
ax.plot([0.08, 0.92], [timeline_y, timeline_y], color=NAVY, linewidth=2)
events = [
(0.14, "capture\nCk, ABSk", BLUE),
(0.30, "son uydu\nsnapshot hazır", GREEN),
(0.59, "WLS bitti", ORANGE),
(0.78, "APB COMMIT", RED),
(0.89, "current Cnow", NAVY),
]
for x, label, color in events:
ax.plot([x, x], [timeline_y - 0.03, timeline_y + 0.03], color=color, linewidth=2)
ax.text(x, timeline_y - 0.065, label, fontsize=7.5, ha="center", color=color)
ax.annotate("satellite arrival span", xy=(0.22, 0.79), ha="center", fontsize=7.5, color=GREEN)
ax.annotate("compute + scheduler", xy=(0.445, 0.79), ha="center", fontsize=7.5, color=ORANGE)
ax.annotate("APB", xy=(0.685, 0.79), ha="center", fontsize=7.5, color=RED)
report.equation(
ax,
0.59,
r"T_{GPS,now}=T_{GPS,k}+\alpha(C_{now}-C_k)",
"Reference eski epoch'a aittir; peripheral counter farkıyla commit anına taşır",
face=PALE_GREEN,
)
report.box(
ax,
0.07,
0.34,
0.40,
0.13,
"Timestamp yoksa",
"T_GPS,k değeri APB geldiği anda geçerli sanılır. Capture→commit süresi doğrudan phase offset gibi görünür ve PI oscillator'ı yanlış yönde sürer.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.box(
ax,
0.53,
0.34,
0.40,
0.13,
"Timestamp varsa",
"Software latency ölçümden çıkar. Geriye feedback komutunun gecikmesi kalır; düşük bant genişlikli loop içinde yalnız phase margin'i etkiler.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
)
report.equation(ax, 0.26, r"E_{used}(s)=E(s)e^{-sD}", "D sabit feedback delay; bandwidth yükseldikçe phase margin azalır")
ax.text(0.10, 0.12, "APB register'ları shadow olmalı; bütün 64-bit/fractional alanlar yazıldıktan sonra tek sequence-tagged COMMIT yapılmalı.", fontsize=9.3, color=NAVY, weight="bold")
report.save(fig)
def page_pi(report: Report) -> None:
fig, ax = report.new_page("PI kontrollü disciplined PPS mimarisi")
report.box(ax, 0.06, 0.69, 0.17, 0.11, "WLS reference", "e = GPS − local\n5 Hz update", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=8)
report.box(ax, 0.29, 0.69, 0.17, 0.11, "PI", "Kp e + ∫Ki e dt\nanti-windup", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8)
report.box(ax, 0.52, 0.69, 0.18, 0.11, "Rate word", "nominal increment\n+ signed correction", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=8)
report.box(ax, 0.76, 0.69, 0.18, 0.11, "GPS accumulator", "fractional phase\ninteger-second wrap", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=8)
report.arrow(ax, (0.23, 0.745), (0.29, 0.745))
report.arrow(ax, (0.46, 0.745), (0.52, 0.745))
report.arrow(ax, (0.70, 0.745), (0.76, 0.745))
report.arrow(ax, (0.85, 0.69), (0.85, 0.61), text="PPS")
report.arrow(ax, (0.76, 0.66), (0.15, 0.66), color=CYAN, text="local time at captured counter", rad=-0.08)
report.equation(ax, 0.55, r"I_k=clip(I_{k-1}+K_iT_s e_k)")
report.equation(ax, 0.46, r"u_k=clip(I_k+K_p e_k),\qquad f_{disc}=f_{raw}(1+u_k)")
report.box(
ax,
0.07,
0.22,
0.40,
0.14,
"Neden raw counter değişmez?",
"Debug ve timestamp monotonic kalır; phase jump oluşmaz. Coarse alignment ve PI yalnız GPS accumulator mapping'ini değiştirir.",
face=PALE_BLUE,
)
report.box(
ax,
0.53,
0.22,
0.40,
0.14,
"125 MHz ne sağlar?",
"Fiziksel PPS edge 8 ns grid'dedir. Fractional accumulator uzun dönem frequency çözünürlüğünü iyileştirir fakat tek edge'i clock kenarları arasına koyamaz.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
)
ax.text(0.09, 0.13, "Python başlangıç parametreleri: natural frequency 0.035 Hz, damping 0.9, max slew 20 ppm. Final değerler kart oscillator logundan seçilmelidir.", fontsize=8.8, color=RED, weight="bold")
report.save(fig)
def page_control_loop_model(report: Report) -> None:
fig, ax = report.new_page("Kontrol loop'unun kapalı çevrim modeli")
# Main forward path: phase detector -> PI -> delayed rate actuation ->
# oscillator/NCO frequency sum -> phase integrator.
sum_xy = (0.16, 0.72)
ax.add_patch(Circle(sum_xy, 0.027, facecolor="white", edgecolor=NAVY, linewidth=1.4))
ax.text(sum_xy[0], sum_xy[1], "Σ", fontsize=12, color=NAVY, ha="center", va="center", weight="bold")
ax.text(0.105, 0.75, "+", fontsize=10, color=GREEN, weight="bold")
ax.text(0.145, 0.675, "−", fontsize=11, color=RED, weight="bold")
ax.text(0.055, 0.72, r"$T_{GPS,k}$", fontsize=10, color=GREEN, va="center")
report.arrow(ax, (0.105, 0.72), (0.133, 0.72), color=GREEN)
report.box(ax, 0.22, 0.665, 0.15, 0.11, "PI C(z)", "Kp·e + integral\nanti-windup", face=PALE_GREEN, edge=GREEN, title_color=GREEN, fontsize=7.6)
report.box(ax, 0.42, 0.665, 0.16, 0.11, "Actuation delay", "z⁻ᵈ / variable D\nrate clamp", face=PALE_ORANGE, edge=ORANGE, title_color=ORANGE, fontsize=7.6)
freq_sum = (0.64, 0.72)
ax.add_patch(Circle(freq_sum, 0.027, facecolor="white", edgecolor=NAVY, linewidth=1.4))
ax.text(freq_sum[0], freq_sum[1], "Σ", fontsize=12, color=NAVY, ha="center", va="center", weight="bold")
report.box(ax, 0.70, 0.665, 0.13, 0.11, "Plant G(z)", "frequency → phase\naccumulator", face=PALE_BLUE, edge=BLUE, title_color=BLUE, fontsize=7.4)
ax.text(0.905, 0.72, r"$\theta=T_{LOCAL}-T_{GPS}$", fontsize=8.2, color=NAVY, ha="center", va="center")
report.arrow(ax, (0.187, 0.72), (0.22, 0.72), text="e")
report.arrow(ax, (0.37, 0.72), (0.42, 0.72), text="u")
report.arrow(ax, (0.58, 0.72), (0.613, 0.72))
ax.text(0.596, 0.685, "u[k−d]", fontsize=7.0, color=BLUE, ha="center")
report.arrow(ax, (0.667, 0.72), (0.70, 0.72))
report.arrow(ax, (0.83, 0.72), (0.86, 0.72))
ax.text(0.64, 0.835, r"$y_{osc}$", fontsize=9.5, color=RED, ha="center")
ax.text(0.64, 0.805, "raw 125 MHz\nfractional frequency error", fontsize=7.0, color=MUTED, ha="center", va="top")
report.arrow(ax, (0.64, 0.79), (0.64, 0.747), color=RED)
# Timestamped measurement path. The delay remains in control application,
# not in the association between reference and local time.
report.box(
ax,
0.40,
0.49,
0.27,
0.10,
"Timestamp'lı phase detector",
"TGPS,k ve TLOCAL(C125,k)\naynı measurement epoch",
face="#E8F4F8",
edge=CYAN,
title_color=BLUE,
fontsize=7.6,
)
report.arrow(ax, (0.88, 0.695), (0.67, 0.545), color=CYAN, rad=-0.12)
report.arrow(ax, (0.40, 0.54), (0.16, 0.693), color=CYAN, text="feedback at epoch k", rad=-0.10)
ax.text(0.84, 0.62, "PPS edge\nquantizer: 8 ns", fontsize=7.2, color=BLUE, ha="center")
report.arrow(ax, (0.84, 0.70), (0.84, 0.64), color=BLUE)
report.equation(
ax,
0.43,
r"C(s)=K_p+\frac{K_i}{s},\qquad G(s)=\frac{1}{s},\qquad L(s)=\frac{C(s)e^{-sD}}{s}",
"PI rate düzeltmesi üretir; accumulator frequency error'ı phase'e integre eder",
face=PALE_BLUE,
)
report.equation(
ax,
0.31,
r"I_{k+1}=clip(I_k+K_iT_s e_k),\quad u_k=clip(I_k+K_pe_k)",
"Ts = NAV_SOLVE_TIMER × 20 ms = 200 ms; update rate 5 Hz",
face=PALE_GREEN,
)
report.equation(
ax,
0.19,
r"\theta_{k+1}=\theta_k+T_s\left(y_{osc,k}+u_{k-d}\right)+w_k,\qquad e_k=-\theta_k+v_k",
"θ local−GPS phase; v WLS/timestamp noise, w unmodelled phase disturbance",
face=PALE_ORANGE,
)
ax.text(
0.075,
0.085,
"\n".join(textwrap.wrap(
"Coarse alignment bu lineer TRACK loop'un dışında, bir defalık anchor yüklemesidir. "
"HOLDOVER'da e_k güncellenmez; integrator/rate word dondurulur.",
width=118,
)),
fontsize=7.6,
color=RED,
weight="bold",
va="top",
linespacing=1.25,
)
report.save(fig)
def page_sim_method(report: Report) -> None:
fig, ax = report.new_page("Python event-driven modelinde ne taklit edildi?")
items = [
("RF clock", "16.368 MHz, ortak/bağımsız ppm drift, 32-bit wrap, fractional REM correction"),
("Navigation", "Ortak 20 ms transmit-bit epoch; NAV_SOLVE_TIMER=10 → 200 ms"),
("Satellite", "Sentetik gerçekçi ECEF/az-el geometrisi, satellite clock, path delay"),
("Corrections", "Earth rotation, Klobuchar, aktif C troposfer modeli, URA/elevation noise"),
("Firmware", "Modulo unwrap, minimum + 68.802 ms, clock correction, cached four-state WLS"),
("Latency", "Satellite arrival span, WLS compute, scheduler stall, APB jitter"),
("PPS", "125 MHz raw oscillator, coarse mapping, timestamped/untagged PI, 8 ns edge grid"),
]
y = 0.82
for index, (name, detail) in enumerate(items):
color = [BLUE, GREEN, ORANGE][index % 3]
ax.add_patch(FancyBboxPatch((0.08, y - 0.055), 0.18, 0.055, boxstyle="round,pad=0.006", facecolor=color, edgecolor=color))
ax.text(0.17, y - 0.028, name, color="white", fontsize=8.7, weight="bold", ha="center", va="center")
ax.add_patch(FancyBboxPatch((0.28, y - 0.055), 0.64, 0.055, boxstyle="round,pad=0.006", facecolor="#F5F7F8", edgecolor="#C4D0D7"))
ax.text(0.30, y - 0.028, detail, color=INK, fontsize=8.2, va="center")
y -= 0.082
report.box(
ax,
0.08,
0.12,
0.84,
0.10,
"Modelin sınırı",
"Gerçek broadcast ephemeris ve kart oscillator phase-noise kaydı yerine sentetik geometri/drift kullanılır. Bu rapordaki sayısal PI sonucu mimariyi doğrular; final performans garantisi değildir.",
face="#FBEAEA",
edge="#E1A2A2",
title_color=RED,
)
report.save(fig)
def plot_page(report: Report, title: str, image_path: Path, caption: str) -> None:
fig, ax = report.new_page(title, section="PYTHON MODEL SONUÇLARI")
image = mpimg.imread(image_path)
image_ax = fig.add_axes([0.07, 0.13, 0.86, 0.72])
image_ax.imshow(image)
image_ax.axis("off")
wrapped = "\n".join(textwrap.wrap(caption, width=120))
ax.text(0.07, 0.095, wrapped, fontsize=8.5, color=MUTED, va="top", linespacing=1.35)
report.save(fig)
def page_registers(report: Report) -> None:
fig, ax = report.new_page("Önerilen PPS APB sözleşmesi")
columns = [0.07, 0.25, 0.47, 0.91]
headers = ["Register", "Yön/format", "Anlam"]
for index, header in enumerate(headers):
x0 = columns[index]
x1 = columns[index + 1]
ax.add_patch(Rectangle((x0, 0.80), x1 - x0, 0.045, facecolor=NAVY, edgecolor="white"))
ax.text(x0 + 0.008, 0.822, header, color="white", fontsize=8.5, weight="bold", va="center")
rows = [
("CONTROL", "RW / bitfield", "enable, coarse_commit, track_enable, holdover, clear_status"),
("STATUS", "RO / bitfield", "tow_known, aligned, tracking, pps_valid, stale_seq, saturation"),
("RAW_COUNTER", "RO / uint64", "Değiştirilmeyen 125 MHz free-running counter"),
("EPOCH_COUNTER", "WO shadow / uint64", "WLS referansının geçerli olduğu captured counter"),
("GPS_SECONDS", "WO shadow / uint64", "Baremetal tarafından epoch-aligned edilmiş GPS reference second"),
("GPS_FRACTION", "WO shadow / Q0.32", "Receive GPS time fractional second"),
("EPOCH_SEQUENCE", "WO shadow / uint32", "Out-of-order/stale APB commit koruması"),
("KP / KI", "RW / fixed point", "PI coefficients; units register spec'te açık tanımlanmalı"),
("MAX_SLEW", "RW / signed ppm/Q", "Rate correction clamp ve anti-windup sınırı"),
("COMMIT", "WO / pulse", "Bütün shadow alanları tek tracking-clock eventinde aktive eder"),
("PPS_DIAG", "RO / signed", "Son phase error, integrator, rate word, age/quality"),
]
y = 0.755
row_h = 0.057
for row_index, row in enumerate(rows):
face = "#F6F8F9" if row_index % 2 == 0 else "white"
for index, value in enumerate(row):
x0 = columns[index]
x1 = columns[index + 1]
ax.add_patch(Rectangle((x0, y), x1 - x0, row_h, facecolor=face, edgecolor="#D2DADF", linewidth=0.6))
wrapped = "\n".join(textwrap.wrap(value, width=[18, 21, 53][index]))
ax.text(x0 + 0.008, y + row_h - 0.012, wrapped, fontsize=7.1, color=INK, va="top", linespacing=1.18)
y -= row_h
report.box(
ax,
0.08,
0.075,
0.84,
0.075,
"Format ilkesi",
"Hardware'a double taşınmaz. Counter integer tick, GPS fraction ve rate correction için açık signed/unsigned Q formatları; software conversion ve saturation kuralları birlikte tanımlanır.",
face=PALE_ORANGE,
edge="#D9B58D",
title_color=ORANGE,
fontsize=7.8,
)
report.save(fig)
def page_implementation(report: Report) -> None:
fig, ax = report.new_page("Uygulama sırası: ölçülebilir küçük adımlar")
steps = [
("1", "Measurement contract", "WLS result struct: XYZ, b, reference SVID, corrected ABS, C125, TOW, sequence."),
("2", "Timestamp-only RTL", "64-bit raw counter ve ABS/REM ile paired per-channel capture; PPS üretme."),
("3", "Cross-check", "ΔC125 ≈ ΔABS × 125/16.368 ilişkisi, wrap ve fractional REM testi."),
("4", "Open-loop coarse", "İlk HOW + coherent WLS ile affine anchor; PI=0, yalnız PPS scheduling."),
("5", "Latency injection", "WLS/APB'ye yapay delay ekle; timestamp'lı PPS fazı değişmemeli."),
("6", "P-only", "İşaret, Q format, max slew ve phase response doğrulaması."),
("7", "PI track", "Kart drift/noise logundan Kp/Ki seç; saturation ve holdover testleri."),
("8", "System tests", "4/5/7 SV, drop/re-acq, reference change, week rollover, stale commit."),
]
y = 0.82
for number, title, detail in steps:
ax.add_patch(FancyBboxPatch((0.08, y - 0.055), 0.06, 0.055, boxstyle="circle,pad=0.008", facecolor=BLUE, edgecolor=BLUE))
ax.text(0.11, y - 0.028, number, color="white", fontsize=9, weight="bold", ha="center", va="center")
ax.text(0.17, y - 0.006, title, fontsize=9.3, color=NAVY, weight="bold", va="top")
ax.text(0.17, y - 0.030, "\n".join(textwrap.wrap(detail, 82)), fontsize=8.2, color=INK, va="top", linespacing=1.2)
if number != "8":
ax.plot([0.11, 0.11], [y - 0.063, y - 0.082], color=CYAN, linewidth=1.4)
y -= 0.086
report.box(
ax,
0.10,
0.075,
0.80,
0.075,
"Go/no-go kuralı",
"Bir aşamanın timestamp ve sign testi geçmeden sonraki kontrol katmanı açılmaz. İlk hardware hedefi iyi PPS değil, doğru ölçüm epoch'udur.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
fontsize=8,
)
report.save(fig)
def page_verification(report: Report) -> None:
fig, ax = report.new_page("Doğrulama matrisi ve beklenen sonuç")
tests = [
("ABS common +5 ms", "XYZ aynı; normalized b aynı", "Observation common phase iptalini kanıtlar"),
("STARTOFFSET +1 ms", "XYZ aynı; b +c×1 ms", "Datum dönüşüm işaretini kanıtlar"),
("32-bit ABS wrap", "Pseudorange süreklidir", "Signed modulo unwrap"),
("Reference SVID değişimi", "Raw b step; mapped GPS time continuous", "Min datum ownership"),
("APB +50 ms delay", "Timestamped PPS fazı değişmez", "Epoch association"),
("Out-of-order commit", "Eski sequence reject", "Stale result koruması"),
("4 → 7 satellite", "Timing noise azalır; loop stabil kalır", "Cold-start geometri"),
("Tracking drop/re-acq", "No phase jump; quality/holdover doğru", "State transition"),
("GPS week rollover", "Continuous seconds ve PPS düzgün", "TOW normalization"),
("HOW pipeline sweep", "Sabit calibrated offset", "POSTPONE/pipeline semantiği"),
]
columns = [0.06, 0.29, 0.61, 0.94]
for index, header in enumerate(["Test", "Beklenen", "Kanıtlanan"]):
ax.add_patch(Rectangle((columns[index], 0.81), columns[index + 1] - columns[index], 0.045, facecolor=NAVY, edgecolor="white"))
ax.text(columns[index] + 0.008, 0.832, header, color="white", fontsize=8.5, weight="bold", va="center")
y = 0.755
h = 0.063
for row_index, row in enumerate(tests):
face = "#F6F8F9" if row_index % 2 == 0 else "white"
for index, value in enumerate(row):
ax.add_patch(Rectangle((columns[index], y), columns[index + 1] - columns[index], h, facecolor=face, edgecolor="#D2DADF", linewidth=0.6))
ax.text(columns[index] + 0.007, y + h - 0.012, "\n".join(textwrap.wrap(value, [22, 31, 31][index])), fontsize=7.0, color=INK, va="top", linespacing=1.16)
y -= h
report.box(
ax,
0.08,
0.075,
0.84,
0.075,
"Kart ölçümünde gerekli log",
"epoch sequence, SVID set, raw/corrected ABS, C125 capture, TOW source, WLS b, mapped phase error, PI integrator/rate, compute/APB age ve PPS edge timestamp.",
face=PALE_BLUE,
fontsize=7.9,
)
report.save(fig)
def page_open_points(report: Report) -> None:
fig, ax = report.new_page("Projede ölçülerek kapatılacak açık noktalar")
points = [
("HOW boundary semantiği", "parse_subframe Z-count × 6 yapıyor. Full-subframe interrupt, bit_gen ABS_SAMPLE ve gerçek transmit next-subframe sınırı arasındaki sabit pipeline cycle sayısı scope/ILA ile ölçülmeli."),
("POSTPONE_TOW = 0.180", "Mevcut RTL readout gecikmesidir: doğrulama dokuzuncu takip eden bitte yapılır; 50 bit/s hızda 180 ms. Mevcut satpos() kullanım anında bir kez ekler; PPS peripheral tekrar eklemez."),
("RF ↔ 125 MHz bağı", "Aynı PLL/reference mı, bağımsız mı? Ortak drift ve independent jitter ölçülmeli; cross-timestamp uncertainty buna göre bütçelenmeli."),
("Cold-start WLS timing", "Dört uyduda vertical/clock korelasyonu yüksek. Coarse alignment yapılabilir; PPS quality düşük işaretlenmeli ve PI bandwidth agresif seçilmemeli."),
("Atmosfer/multipath", "Iono/tropo modele girse de residual b ve position'a sızar. Timing-only mode veya sabit/smoothed position ileride clock çözümünü iyileştirebilir."),
("Hardware fixed point", "Phase, frequency word, Kp/Ki ve fractional timestamp Q formatlarının overflow, saturation ve rounding davranışı bit-accurate Python modeline eklenmeli."),
]
y = 0.82
for index, (title, detail) in enumerate(points):
color = [ORANGE, RED, BLUE, GREEN, ORANGE, BLUE][index]
ax.add_patch(Rectangle((0.07, y - 0.085), 0.012, 0.085, color=color))
ax.text(0.10, y, title, fontsize=10, color=color, weight="bold", va="top")
ax.text(0.10, y - 0.031, "\n".join(textwrap.wrap(detail, 98)), fontsize=8.6, color=INK, va="top", linespacing=1.28)
y -= 0.118
report.box(
ax,
0.10,
0.075,
0.80,
0.075,
"Tasarım sonucu",
"Bu açık noktalar PPS mimarisini değiştirmez; yalnız sabit offset, uncertainty bütçesi ve final PI katsayılarını belirler.",
face=PALE_GREEN,
edge="#9BC9AF",
title_color=GREEN,
fontsize=8.1,
)
report.save(fig)
def page_conclusion(report: Report) -> None:
fig, ax = report.new_page("Sonuç: projeye uygun en yalın PPS zinciri")
y = 0.80
chain = [
("1", "Raw clocks", "16.368 MHz measurement + 125 MHz monotonic counter"),
("2", "Paired capture", "ABS / REM / C125 / sequence aynı event"),
("3", "Coherent WLS", "minimum datum + all-satellite [XYZ b]"),
("4", "GPS receive time", "T_TX(epoch-aligned) + 68.802 ms − b/c"),
("5", "Atomic mapping", "(C125,k, TGPS,k, seq) shadow + commit"),
("6", "Disciplined PPS", "immutable raw counter + low-bandwidth PI accumulator"),
]
for index, (number, title, detail) in enumerate(chain):
x = 0.09 if index % 2 == 0 else 0.53
yy = y - (index // 2) * 0.19
color = [BLUE, ORANGE, GREEN][index // 2]
ax.add_patch(FancyBboxPatch((x, yy - 0.11), 0.38, 0.11, boxstyle="round,pad=0.010", facecolor="#F5F8FA", edgecolor=color, linewidth=1.2))
ax.add_patch(FancyBboxPatch((x + 0.012, yy - 0.052), 0.045, 0.045, boxstyle="circle,pad=0.002", facecolor=color, edgecolor=color))
ax.text(x + 0.0345, yy - 0.030, number, color="white", fontsize=8, weight="bold", ha="center", va="center")
ax.text(x + 0.072, yy - 0.015, title, fontsize=9.5, color=NAVY, weight="bold", va="top")
ax.text(x + 0.072, yy - 0.050, "\n".join(textwrap.wrap(detail, 40)), fontsize=8.1, color=INK, va="top")
report.equation(ax, 0.30, r"e_k=\left(T_{TX,k}+S-b_k/c\right)-T_{LOCAL}(C_{125,k})", "Bütün raporun tek satırlık özeti", face=PALE_GREEN)
report.box(
ax,
0.10,
0.10,
0.80,
0.09,
"En önemli mimari karar",
"İlk uydu PPS'i başlatmaz. Baremetal HOW'a 180 ms readout düzeltmesini bir kez uygular; ilk coherent WLS bu etiketi free-running 125 MHz counter'a bağlar. PPS peripheral 180 ms'yi tekrar eklemez.",
face=NAVY,
edge=NAVY,
title_color="#8ED6E8",
text_color="white",
fontsize=8.4,
)
report.save(fig)
def generate(output_pdf: Path) -> None:
required_plots = [
OUTPUT / "01_wls_time_model.png",
OUTPUT / "02_latency_timeline.png",
OUTPUT / "03_pi_loop_response.png",
OUTPUT / "04_pps_edge_error.png",
OUTPUT / "05_startup_tow_alignment.png",
]
missing = [str(path) for path in required_plots if not path.exists()]
if missing:
raise SystemExit(
"Missing simulation plots. Run simulate_pps.py first:\n" + "\n".join(missing)
)
output_pdf.parent.mkdir(parents=True, exist_ok=True)
report = Report(output_pdf)
try:
report.cover()
page_executive(report)
page_time_vocabulary(report)
page_lnav_timeline(report)
page_tx_rx(report)
page_project_map(report)
page_nav_epoch(report)
page_pseudorange(report)
page_wls_theory(report)
page_bias_derivation(report)
page_counter_mapping(report)
page_state_machine(report)
page_cross_timestamp(report)
page_apb_latency(report)
page_pi(report)
page_control_loop_model(report)
page_sim_method(report)
plot_page(
report,
"WLS datum, reference değişimi ve cold-start geometri",
required_plots[0],
"Raw b/c referans SVID değişince milisaniyelik basamak yaparken TOW+bias ile rekonstrükte edilen zaman hatası ns seviyesinde kalır. Dört uydulu ilk bölüm position/clock açısından belirgin şekilde daha gürültülüdür.",
)
plot_page(
report,
"İlk HOW neden PPS receive-now değildir?",
required_plots[4],
"Sentetik acquisition/subframe Monte Carlo: mevcut hardware delivery, dokuzuncu takip eden bit nedeniyle HOW sınırından 180 ms ve signal flight time kadar sonradır. HOW +180 ms +68.802 ms kaba yaklaşım bırakır; WLS flight-time + paired timestamp doğru handover'dır.",
)
plot_page(
report,
"Capture-to-commit gecikme bileşenleri",
required_plots[1],
"Satellite arrival span, WLS compute ve APB delay aynı epoch'un age bilgisidir. Timestamp varsa phase offset değildir; timestamp yoksa doğrudan PPS hatasına dönüşür.",
)
plot_page(
report,
"Timestamp'lı PI loop cevabı",
required_plots[2],
"Timestamp'lı PI raw oscillator driftini rate command ile karşılar. PI kapalı clock yüzlerce µs sürüklenir; eski GPS referansını APB-now sanan kontrol ise ms seviyesinde yanlış faza oturur.",
)
plot_page(
report,
"PPS edge sonucu ve 8 ns output grid",
required_plots[3],
"Timestamp'lı loop ilk transient sonrasında sub-µs bölgeye iner. Bu sentetik katsayı sonucu mimari karşılaştırmadır; gerçek kart noise/drift verisi olmadan final doğruluk iddiası değildir.",
)
page_registers(report)
page_implementation(report)
page_verification(report)
page_open_points(report)
page_conclusion(report)
finally:
report.close()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--output", type=Path, default=DEFAULT_PDF)
return parser.parse_args()
def main() -> int:
args = parse_args()
generate(args.output.resolve())
print(args.output.resolve())
return 0
if __name__ == "__main__":
raise SystemExit(main())