30179df8e6
Remove the fixed pseudorange start offset, preserve per-PRN navigation assistance across RF loss and power cycles, and seed coherent navigation measurement phase on reacquisition. Add full LNAV almanac visibility scheduling, power-safe QSPI journaling, RF-domain reset isolation, and PPS Gray CDC hardening with Linux, sanitizer, RTL, and end-to-end coverage.
1461 lines
61 KiB
Python
1461 lines
61 KiB
Python
#!/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 corrected RF "
|
||
"sample ve satellite transmit-time farklarıyla kurulduğu için sayacın mutlak fazı "
|
||
"observation'dan çıkar. WLS bias, referans kanalın negatif uçuş süresini ve "
|
||
"satellite clock correction'ini taşıyan göreli datumdur.",
|
||
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", "ΔS_RF −\nΔT transmit", 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 Corrected RF sample ve transmit-time farkı", fontsize=11, color=NAVY, weight="bold")
|
||
y = report.equation(ax, y - 0.025, r"\Delta t_i=\frac{S_i-S_{ref}}{16.367981\,MHz}-(T_{tx,i}-T_{tx,ref})")
|
||
ax.text(0.08, y, "3 Offset-free göreli pseudorange datum'u", fontsize=11, color=NAVY, weight="bold")
|
||
y = report.equation(
|
||
ax,
|
||
y - 0.025,
|
||
r"P'_i=c\left[\Delta t_i+\delta t_{SV,i}\right]",
|
||
"Free-running counter başlangıcı ve ortak uzun-dönem drift farkta yok olur",
|
||
face=PALE_ORANGE,
|
||
)
|
||
report.box(
|
||
ax,
|
||
0.07,
|
||
0.17,
|
||
0.40,
|
||
0.14,
|
||
"Referans neden var?",
|
||
"RF counter'ın receiver açılışına bağlı ortak sayısını wrap-safe çıkarır. Uydular arası arrival/transmit-time farkları korunur; konum geometrisi bu farklarda bulunur.",
|
||
face=PALE_BLUE,
|
||
)
|
||
report.box(
|
||
ax,
|
||
0.53,
|
||
0.17,
|
||
0.40,
|
||
0.14,
|
||
"Sabit offset neden yok?",
|
||
"WLS dördüncü state göreli datumun büyük ortak bias'ını zaten çözer. Fiziksel konum için kalibre edilmiş nominal uçuş süresi gerekmez.",
|
||
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", "Raw-counter/transmit-time difference, 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"),
|
||
("RF counter common shift", "XYZ ve göreli b aynı", "RF counter origin iptalini 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) − 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 tek başına uçuş süresi kadar hata bırakır; offset-free 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())
|