merge: task/X-20261029-ring-comment 合入 v2-on-ui
任务 X-20261029(5 个提交)经 task-worktree 回收: - docs(algo): 5.3 节补窗首下标的圆周图(三种写法对照) - docs(algo): 5.3 节补窗首下标的圆周图(三种写法对照) - docs(algo): sample_ring_window 注释收敛,长推导移到设计说明 - docs(algo): 采样环缓冲补「为什么需要/设计思想」两节与两张配图 - docs(algo): 补 sample_ring_window 注释,详解窗首下标推导 备注:sample_ring_window 详细注释(纯注释,不改逻辑)
This commit is contained in:
+18
-5
@@ -73,28 +73,41 @@ int sample_ring_push_frame(const float *values, uint8_t n) {
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 入口参数:ch - 语义通道(0 .. ADC_SEM_COUNT-1)
|
||||
* n - 取点数(1 .. SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(调用方保证长度 >= n,本层不校验容量)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。最老点下标 = head - n(模 DEPTH)。
|
||||
* 限定条件:仅 adcTask 上下文(写者与读者同任务串行,故不加锁、不用 volatile);
|
||||
* 须在同拍 sample_ring_push_frame() 之后调用。
|
||||
* 函数说明:以最新一点为终点、向前回溯 n 点;out[0] 最老、out[n-1] 最新,
|
||||
* 与 phasor_run() 的求和方向一致。
|
||||
* 为什么需要这一层、环内布局与窗首下标的推导 →
|
||||
* docs/算法/采集与算法层设计说明.md §5(本文件只留落地要点)。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
|
||||
uint16_t start;
|
||||
uint16_t i;
|
||||
|
||||
/* 通道号先转 unsigned 再比:枚举底层可为有符号,负值会骗过 ch >= COUNT 的判据。 */
|
||||
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
/* n = 0 是空窗;n > DEPTH 一个环装不下连续 n 点(回绕会把同一点重复读出)。 */
|
||||
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
|
||||
return -1;
|
||||
}
|
||||
/* -2 = 窗还没攒够,调用方跳过本拍即可(首周波启动期必然出现),不是故障。 */
|
||||
if (s_filled < (uint16_t)n) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
/* 窗首 = s_head - n(s_head 是"下一待写"位置,故窗尾 = s_head - 1)。
|
||||
* 必须先加 DEPTH 再减:直接减在 s_head < n 时下溢或得负数,而
|
||||
* (x mod 65536) mod 40 不等于 x mod 40、C99 的负数取模也不是同余解。
|
||||
* 加 DEPTH 不改同余类,并把中间值锁在 [0, 2·DEPTH - 2]。详见设计说明 §5.3。 */
|
||||
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
|
||||
|
||||
/* 逐点取模拷贝:窗可能跨环尾,环在内存里首尾不相连,不能用一次 memcpy。 */
|
||||
for (i = 0u; i < (uint16_t)n; i++) {
|
||||
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ int sample_ring_push_frame(const float *values, uint8_t n);
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。相量层以本函数取"以当前时刻为终止的一周波"。
|
||||
* ⚠ 仅 adcTask 上下文调用(写者与读者同任务串行,本层不加锁);
|
||||
* 为什么需要这一层、环内布局与窗首下标的推导 →
|
||||
* `docs/算法/采集与算法层设计说明.md` §5。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out);
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="600" viewBox="0 0 1060 600" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect x="0" y="0" width="1060" height="600" fill="#ffffff"/>
|
||||
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">上游只交最新一帧,相量却要一整周波 —— 采样环补的就是这 19 点</text>
|
||||
<text x="46.0" y="68.0" font-size="13" fill="#64748b">上游(adc_input)每 1 ms 交付一次,且只交本拍;旧值被覆盖、不保留历史。下游相量要的是「以当前时刻为终止的 20 点滑窗」。</text>
|
||||
<text x="46.0" y="88.0" font-size="13" fill="#64748b">本层不改采集、不改算法,只把「覆盖写的最新值」转成「可回溯的滑动窗」——三个消费者共用同一份。</text>
|
||||
<rect x="34.0" y="112.0" width="992.0" height="188.0" rx="12" fill="#fff1f2"/>
|
||||
<text x="46.0" y="136.0" font-size="14.5" font-weight="700" fill="#e11d48">① 上游交付物:1 kHz 覆盖写,每拍只交一帧</text>
|
||||
<text x="46.0" y="156.0" font-size="11" fill="#64748b">20 个刻度 = 一周波 20 点(1 kHz @ 50 Hz);上游只把最后一个点交出来</text>
|
||||
<line x1="62.0" y1="236.0" x2="806.0" y2="236.0" stroke="#334155" stroke-width="2"/>
|
||||
<line x1="74.0" y1="236.0" x2="74.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="112.0" y1="236.0" x2="112.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="150.0" y1="236.0" x2="150.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="188.0" y1="236.0" x2="188.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="226.0" y1="236.0" x2="226.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="264.0" y1="236.0" x2="264.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="302.0" y1="236.0" x2="302.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="340.0" y1="236.0" x2="340.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="378.0" y1="236.0" x2="378.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="416.0" y1="236.0" x2="416.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="454.0" y1="236.0" x2="454.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="492.0" y1="236.0" x2="492.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="530.0" y1="236.0" x2="530.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="568.0" y1="236.0" x2="568.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="606.0" y1="236.0" x2="606.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="644.0" y1="236.0" x2="644.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="682.0" y1="236.0" x2="682.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="720.0" y1="236.0" x2="720.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="758.0" y1="236.0" x2="758.0" y2="214.0" stroke="#94a3b8" stroke-width="1.4"/>
|
||||
<line x1="796.0" y1="236.0" x2="796.0" y2="214.0" stroke="#2563eb" stroke-width="2"/>
|
||||
<rect x="65.0" y="192.0" width="740.0" height="20.0" rx="6" fill="none" stroke="#e11d48" stroke-width="1.8" stroke-dasharray="7 5"/>
|
||||
<text x="435.0" y="207.0" font-size="11.5" text-anchor="middle" font-weight="700" fill="#e11d48">相量要的 20 点窗(一整周波)</text>
|
||||
<circle cx="796.0" cy="236.0" r="5.5" fill="#2563eb"/>
|
||||
<text x="808.0" y="241.0" font-size="11" font-weight="700" fill="#2563eb">最新一帧(本拍)</text>
|
||||
<rect x="65.0" y="244.0" width="702.0" height="5.0" rx="2.5" fill="#e11d48"/>
|
||||
<text x="74.0" y="266.0" font-size="11" text-anchor="middle" fill="#64748b">k−19</text>
|
||||
<text x="454.0" y="266.0" font-size="11" text-anchor="middle" fill="#64748b">k−9</text>
|
||||
<text x="796.0" y="266.0" font-size="11" text-anchor="middle" fill="#64748b">k</text>
|
||||
<text x="46.0" y="288.0" font-size="11" font-weight="700" fill="#e11d48">这 19 个点上游不保留 —— 不缓冲就永远取不到</text>
|
||||
<text x="46.0" y="334.0" font-size="14.5" font-weight="700" fill="#2563eb">② 下游需求:三个消费者,必须看同一份历史</text>
|
||||
<text x="46.0" y="354.0" font-size="11" fill="#64748b">共用同一份环 = 同一拍只有一个窗 —— 口径不会各自漂移</text>
|
||||
<rect x="46.0" y="370.0" width="300.0" height="112.0" rx="8" fill="#eff6ff" stroke="#2563eb" stroke-width="1.8"/>
|
||||
<text x="58.0" y="394.0" font-size="13.5" font-weight="700" fill="#0f172a">phasor_run()</text>
|
||||
<text x="58.0" y="416.0" font-size="11.5" fill="#64748b">每 1 ms 要一整周波 20 点,算全周傅氏相量(幅值</text>
|
||||
<text x="58.0" y="431.0" font-size="11.5" fill="#64748b"> / 相位)。窗短一点就不是「全周」,谐波抑制立刻</text>
|
||||
<text x="58.0" y="446.0" font-size="11.5" fill="#64748b">失效。</text>
|
||||
<rect x="365.0" y="370.0" width="300.0" height="112.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.8"/>
|
||||
<text x="377.0" y="394.0" font-size="13.5" font-weight="700" fill="#0f172a">freq_run()</text>
|
||||
<text x="377.0" y="416.0" font-size="11.5" fill="#64748b">要相邻两拍相量的相位差 → 反推频率。它与相量必须</text>
|
||||
<text x="377.0" y="431.0" font-size="11.5" fill="#64748b">落在同一个窗上,否则相位差里混进窗长误差。</text>
|
||||
<rect x="684.0" y="370.0" width="300.0" height="112.0" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.8"/>
|
||||
<text x="696.0" y="394.0" font-size="13.5" font-weight="700" fill="#0f172a">meas_tick()</text>
|
||||
<text x="696.0" y="416.0" font-size="11.5" fill="#64748b">每 200 ms 发布遥测快照。快照里的相量 / 频率 /</text>
|
||||
<text x="696.0" y="431.0" font-size="11.5" fill="#64748b"> 品质位都取自同一次窗,不允许各取各的。</text>
|
||||
<line x1="46.0" y1="516.0" x2="1014.0" y2="516.0" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="46.0" y="540.0" font-size="13" fill="#64748b">缓冲只解决「取得到历史」,不解决「这些点是否同刻」:同刻性归上游 adc_input 负责,本层只按时序存 / 取,不做任何跨通道运算。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.9 KiB |
@@ -0,0 +1,115 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="580" viewBox="0 0 1060 580" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect x="0" y="0" width="1060" height="580" fill="#ffffff"/>
|
||||
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">40 格环:写指针指「下一格」,窗永远紧贴它身后</text>
|
||||
<text x="46.0" y="68.0" font-size="13" fill="#64748b">DEPTH = 2 周波 × 20 点 = 40;窗长 n 取 1~40,取的是 [head−n, head−1] 这 n 格。</text>
|
||||
<text x="46.0" y="88.0" font-size="13" fill="#64748b">图例:蓝 = 本窗 20 点,灰 = 更旧的 10 点,琥珀 = 最旧的 10 点(下一批被覆盖)。</text>
|
||||
<text x="46.0" y="108.0" font-size="13" fill="#64748b">窗首 start = (head + 40 − n) % 40 —— 先加 40 再减,减法就不落到负数;每写一格整窗右移一格。</text>
|
||||
<rect x="46.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="57.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">0</text>
|
||||
<rect x="70.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="81.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">1</text>
|
||||
<rect x="94.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="105.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">2</text>
|
||||
<rect x="118.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="129.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">3</text>
|
||||
<rect x="142.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="153.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">4</text>
|
||||
<rect x="166.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="177.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">5</text>
|
||||
<rect x="190.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="201.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">6</text>
|
||||
<rect x="214.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="225.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">7</text>
|
||||
<rect x="238.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="249.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">8</text>
|
||||
<rect x="262.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#f1f5f9" stroke="#94a3b8" stroke-width="1.2"/>
|
||||
<text x="273.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">9</text>
|
||||
<rect x="286.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="297.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">10</text>
|
||||
<rect x="310.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="321.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">11</text>
|
||||
<rect x="334.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="345.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">12</text>
|
||||
<rect x="358.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="369.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">13</text>
|
||||
<rect x="382.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="393.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">14</text>
|
||||
<rect x="406.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="417.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">15</text>
|
||||
<rect x="430.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="441.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">16</text>
|
||||
<rect x="454.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="465.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">17</text>
|
||||
<rect x="478.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="489.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">18</text>
|
||||
<rect x="502.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="513.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">19</text>
|
||||
<rect x="526.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="537.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">20</text>
|
||||
<rect x="550.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="561.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">21</text>
|
||||
<rect x="574.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="585.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">22</text>
|
||||
<rect x="598.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="609.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">23</text>
|
||||
<rect x="622.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="633.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">24</text>
|
||||
<rect x="646.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="657.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">25</text>
|
||||
<rect x="670.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="681.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">26</text>
|
||||
<rect x="694.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="705.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">27</text>
|
||||
<rect x="718.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="729.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">28</text>
|
||||
<rect x="742.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#eff6ff" stroke="#2563eb" stroke-width="2"/>
|
||||
<text x="753.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">29</text>
|
||||
<rect x="766.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="777.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">30</text>
|
||||
<rect x="790.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="801.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">31</text>
|
||||
<rect x="814.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="825.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">32</text>
|
||||
<rect x="838.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="849.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">33</text>
|
||||
<rect x="862.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="873.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">34</text>
|
||||
<rect x="886.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="897.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">35</text>
|
||||
<rect x="910.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="921.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">36</text>
|
||||
<rect x="934.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="945.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">37</text>
|
||||
<rect x="958.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="969.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">38</text>
|
||||
<rect x="982.0" y="200.0" width="22.0" height="54.0" rx="3" fill="#fffbeb" stroke="#d97706" stroke-width="1.4"/>
|
||||
<text x="993.0" y="230.5" font-size="9" text-anchor="middle" fill="#64748b">39</text>
|
||||
<line x1="297.0" y1="184.0" x2="297.0" y2="198.0" stroke="#2563eb" stroke-width="1.4"/>
|
||||
<polygon points="297.0,200.0 292.5,192.0 301.5,192.0" fill="#2563eb"/>
|
||||
<text x="297.0" y="176.0" font-size="11" text-anchor="start" font-weight="700" fill="#2563eb">start(最老点)</text>
|
||||
<line x1="753.0" y1="184.0" x2="753.0" y2="198.0" stroke="#2563eb" stroke-width="1.4"/>
|
||||
<polygon points="753.0,200.0 748.5,192.0 757.5,192.0" fill="#2563eb"/>
|
||||
<text x="753.0" y="176.0" font-size="11" text-anchor="end" font-weight="700" fill="#2563eb">head−1(最新点)</text>
|
||||
<line x1="777.0" y1="184.0" x2="777.0" y2="198.0" stroke="#d97706" stroke-width="1.4"/>
|
||||
<polygon points="777.0,200.0 772.5,192.0 781.5,192.0" fill="#d97706"/>
|
||||
<text x="777.0" y="176.0" font-size="11" text-anchor="start" font-weight="700" fill="#d97706">head(下一拍写这里)</text>
|
||||
<line x1="993.0" y1="286.0" x2="65.0" y2="286.0" stroke="#e11d48" stroke-width="1.6" stroke-dasharray="7 5"/>
|
||||
<polygon points="57.0,286.0 66.0,281.5 66.0,290.5" fill="#e11d48"/>
|
||||
<text x="46.0" y="308.0" font-size="11" font-weight="700" fill="#e11d48">写满后回绕:39 的下一格是 0 —— 窗可能跨过环尾,所以取数必须逐点取模</text>
|
||||
<rect x="46.0" y="340.0" width="300.0" height="120.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.8"/>
|
||||
<text x="58.0" y="364.0" font-size="13.5" font-weight="700" fill="#0f172a">整帧写完才推进 head</text>
|
||||
<text x="58.0" y="386.0" font-size="11.5" fill="#64748b">9 路语义值一次写齐、再动指针。单帧内不存在「半</text>
|
||||
<text x="58.0" y="401.0" font-size="11.5" fill="#64748b">新半旧」,消费侧取到的窗永远落在整帧边界上。</text>
|
||||
<rect x="365.0" y="340.0" width="300.0" height="120.0" rx="8" fill="#eff6ff" stroke="#2563eb" stroke-width="1.8"/>
|
||||
<text x="377.0" y="364.0" font-size="13.5" font-weight="700" fill="#0f172a">只存不算是分层契约</text>
|
||||
<text x="377.0" y="386.0" font-size="11.5" fill="#64748b">同刻性归上游 adc_input 负责;本层不假设、不校</text>
|
||||
<text x="377.0" y="401.0" font-size="11.5" fill="#64748b">正,也不做任何跨通道运算——所以它能脱离硬件做主</text>
|
||||
<text x="377.0" y="416.0" font-size="11.5" fill="#64748b">机侧单测。</text>
|
||||
<rect x="684.0" y="340.0" width="300.0" height="120.0" rx="8" fill="#fffbeb" stroke="#d97706" stroke-width="1.8"/>
|
||||
<text x="696.0" y="364.0" font-size="13.5" font-weight="700" fill="#0f172a">容量取 2 周波</text>
|
||||
<text x="696.0" y="386.0" font-size="11.5" fill="#64748b">1 周波就够算全周傅氏;另 1 周波是给 1.5~2 周波</text>
|
||||
<text x="696.0" y="401.0" font-size="11.5" fill="#64748b">长窗(抗衰减直流)留的余量,改容量只动一个宏。</text>
|
||||
<line x1="46.0" y1="490.0" x2="1014.0" y2="490.0" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="46.0" y="514.0" font-size="13" fill="#64748b">n = 40 时 start == head:环满时「下一拍要覆盖的那一格」恰好就是最老点 —— 不是特例,是唯一自洽解。同理 n = 20 时窗正好是 head 身后的 20 格,不</text>
|
||||
<text x="46.0" y="533.0" font-size="13" fill="#64748b">多不少。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,74 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1060" height="600" viewBox="0 0 1060 600" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
|
||||
<rect x="0" y="0" width="1060" height="600" fill="#ffffff"/>
|
||||
<text x="46.0" y="42.0" font-size="21" font-weight="700" fill="#0f172a">窗首 = head − n:head 不够减会走到负数,所以先加一圈 DEPTH 再减</text>
|
||||
<text x="46.0" y="68.0" font-size="13" fill="#64748b">例:head = 5、n = 20 —— 最新点在 4,沿环往回退 19 步落在 25,途中跨过 0 与 39。DEPTH = 40 时 20 点窗正好占半圈,所以这条弧看起来是个半圆。</text>
|
||||
<text x="46.0" y="88.0" font-size="13" fill="#64748b">同一个例子三种写法,只有第三种同时做到「同余正确」与「不发生下溢」:</text>
|
||||
<circle cx="296.0" cy="352.0" r="148" fill="none" stroke="#cbd5e1" stroke-width="1.2"/>
|
||||
<circle cx="296.0" cy="204.0" r="6" fill="#2563eb"/>
|
||||
<circle cx="319.2" cy="205.8" r="6" fill="#2563eb"/>
|
||||
<circle cx="341.7" cy="211.2" r="6" fill="#2563eb"/>
|
||||
<circle cx="363.2" cy="220.1" r="6" fill="#2563eb"/>
|
||||
<circle cx="383.0" cy="232.3" r="6" fill="#2563eb"/>
|
||||
<circle cx="400.7" cy="247.3" r="6" fill="#d97706"/>
|
||||
<circle cx="415.7" cy="265.0" r="5" fill="#94a3b8"/>
|
||||
<circle cx="427.9" cy="284.8" r="5" fill="#94a3b8"/>
|
||||
<circle cx="436.8" cy="306.3" r="5" fill="#94a3b8"/>
|
||||
<circle cx="442.2" cy="328.8" r="5" fill="#94a3b8"/>
|
||||
<circle cx="444.0" cy="352.0" r="5" fill="#94a3b8"/>
|
||||
<circle cx="442.2" cy="375.2" r="5" fill="#94a3b8"/>
|
||||
<circle cx="436.8" cy="397.7" r="5" fill="#94a3b8"/>
|
||||
<circle cx="427.9" cy="419.2" r="5" fill="#94a3b8"/>
|
||||
<circle cx="415.7" cy="439.0" r="5" fill="#94a3b8"/>
|
||||
<circle cx="400.7" cy="456.7" r="5" fill="#94a3b8"/>
|
||||
<circle cx="383.0" cy="471.7" r="5" fill="#94a3b8"/>
|
||||
<circle cx="363.2" cy="483.9" r="5" fill="#94a3b8"/>
|
||||
<circle cx="341.7" cy="492.8" r="5" fill="#94a3b8"/>
|
||||
<circle cx="319.2" cy="498.2" r="5" fill="#94a3b8"/>
|
||||
<circle cx="296.0" cy="500.0" r="5" fill="#94a3b8"/>
|
||||
<circle cx="272.8" cy="498.2" r="5" fill="#94a3b8"/>
|
||||
<circle cx="250.3" cy="492.8" r="5" fill="#94a3b8"/>
|
||||
<circle cx="228.8" cy="483.9" r="5" fill="#94a3b8"/>
|
||||
<circle cx="209.0" cy="471.7" r="5" fill="#94a3b8"/>
|
||||
<circle cx="191.3" cy="456.7" r="6" fill="#2563eb"/>
|
||||
<circle cx="176.3" cy="439.0" r="6" fill="#2563eb"/>
|
||||
<circle cx="164.1" cy="419.2" r="6" fill="#2563eb"/>
|
||||
<circle cx="155.2" cy="397.7" r="6" fill="#2563eb"/>
|
||||
<circle cx="149.8" cy="375.2" r="6" fill="#2563eb"/>
|
||||
<circle cx="148.0" cy="352.0" r="6" fill="#2563eb"/>
|
||||
<circle cx="149.8" cy="328.8" r="6" fill="#2563eb"/>
|
||||
<circle cx="155.2" cy="306.3" r="6" fill="#2563eb"/>
|
||||
<circle cx="164.1" cy="284.8" r="6" fill="#2563eb"/>
|
||||
<circle cx="176.3" cy="265.0" r="6" fill="#2563eb"/>
|
||||
<circle cx="191.3" cy="247.3" r="6" fill="#2563eb"/>
|
||||
<circle cx="209.0" cy="232.3" r="6" fill="#2563eb"/>
|
||||
<circle cx="228.8" cy="220.1" r="6" fill="#2563eb"/>
|
||||
<circle cx="250.3" cy="211.2" r="6" fill="#2563eb"/>
|
||||
<circle cx="272.8" cy="205.8" r="6" fill="#2563eb"/>
|
||||
<path d="M 372.4 246.8 A 130.0 130.0 0 0 0 204.1 443.9" fill="none" stroke="#e11d48" stroke-width="1.8"/><polygon points="204.1,443.9 193.8,440.0 200.3,433.6" fill="#e11d48"/>
|
||||
<text x="226.5" y="316.6" font-size="11" text-anchor="middle" font-weight="700" fill="#e11d48">往回退 19 步</text>
|
||||
<text x="296.0" y="186.0" font-size="12" text-anchor="middle" fill="#64748b">0</text>
|
||||
<text x="466.0" y="356.0" font-size="12" text-anchor="start" fill="#64748b">10</text>
|
||||
<text x="296.0" y="526.0" font-size="12" text-anchor="middle" fill="#64748b">20</text>
|
||||
<text x="126.0" y="356.0" font-size="12" text-anchor="end" fill="#64748b">30</text>
|
||||
<text x="419.0" y="229.0" font-size="11" text-anchor="start" font-weight="700" fill="#d97706">head = 5</text>
|
||||
<text x="173.0" y="475.0" font-size="11" text-anchor="end" font-weight="700" fill="#2563eb">start = 25</text>
|
||||
<text x="296.0" y="346.0" font-size="11" text-anchor="middle" fill="#94a3b8">环 = 40 格</text>
|
||||
<text x="296.0" y="366.0" font-size="11" text-anchor="middle" fill="#94a3b8">窗 = 20 格(半圈)</text>
|
||||
<rect x="596.0" y="186.0" width="418.0" height="96.0" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-width="1.8"/>
|
||||
<text x="610.0" y="212.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">✗ 5 - 20</text>
|
||||
<text x="1000.0" y="212.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">= -15</text>
|
||||
<text x="610.0" y="234.0" font-size="11.5" fill="#64748b">负数。C99 的 % 向零截断,(-15) % 40 仍是 -15;</text>
|
||||
<text x="610.0" y="250.0" font-size="11.5" fill="#64748b">转成无符号就是 65521,拿去索引 s_ring 即越界读。</text>
|
||||
<rect x="596.0" y="296.0" width="418.0" height="96.0" rx="8" fill="#fff1f2" stroke="#e11d48" stroke-width="1.8"/>
|
||||
<text x="610.0" y="322.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">✗ (uint16_t)(5 - 20) % 40</text>
|
||||
<text x="1000.0" y="322.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#e11d48">= 65521 % 40 = 1</text>
|
||||
<text x="610.0" y="344.0" font-size="11.5" fill="#64748b">65536 不是 40 的倍数:(x mod 65536) mod 40 ≠ x mod 40。</text>
|
||||
<text x="610.0" y="360.0" font-size="11.5" fill="#64748b">回绕后再取模不等价,读到的是环尾附近而不是窗首。</text>
|
||||
<rect x="596.0" y="406.0" width="418.0" height="96.0" rx="8" fill="#f0fdf4" stroke="#16a34a" stroke-width="1.8"/>
|
||||
<text x="610.0" y="432.0" font-size="13" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#16a34a">✓ (5 + 40 - 20) % 40</text>
|
||||
<text x="1000.0" y="432.0" font-size="13" text-anchor="end" font-weight="700" font-family="Consolas, Cascadia Mono, DejaVu Sans Mono, monospace" fill="#16a34a">= 25 % 40 = 25</text>
|
||||
<text x="610.0" y="454.0" font-size="11.5" fill="#64748b">就是窗首。加 40 不改同余类(40 ≡ 0 mod 40),</text>
|
||||
<text x="610.0" y="470.0" font-size="11.5" fill="#64748b">中间值恒在 [0, 2·DEPTH - 2],两种提升规则都不下溢。</text>
|
||||
<line x1="46.0" y1="530.0" x2="1014.0" y2="530.0" stroke="#cbd5e1" stroke-width="1"/>
|
||||
<text x="46.0" y="556.0" font-size="13" fill="#64748b">加 DEPTH 不是「凑数」:它是环的周长,加一整圈等价于原地不动——但把减法从「可能为负」搬到了「必然非负」,取模才恢复成数学上的同余解。</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.4 KiB |
+77
-4
@@ -146,9 +146,79 @@ flowchart TD
|
||||
|
||||
## 5. 全周傅氏相量输入 `sample_ring.c`
|
||||
|
||||
**解决的问题**:相量计算要"以当前时刻为终止的一周波",而 语义取数与有效性每次只给最新一帧。本层按**语义通道**
|
||||
存最近 `SAMPLE_RING_CYCLES = 2` 个周波(`SAMPLE_RING_DEPTH = 40` 点),与参考系统
|
||||
`CN_BUFSAM_CYCLE = 2` 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。
|
||||
### 5.1 为什么需要这一层
|
||||
|
||||
**一句话**:上游每 1 ms 只交"本拍那一帧",下游相量要的是"以当前时刻为终止的一整周波",
|
||||
中间那 19 个历史点没有任何一层替它保留——本层就是补这个缺口的地方。
|
||||
|
||||
三个缺口,任一个单独存在都能让相量算不出来或算不准:
|
||||
|
||||
| 缺口 | 上游给什么 | 下游要什么 | 不缓冲的后果 |
|
||||
|---|---|---|---|
|
||||
| **点数** | 每拍 1 帧(9 路语义值) | 一周波 20 点(`PHASOR_N`) | 只有 1 点 → 全周傅氏退化成瞬时值,谐波抑制完全失效 |
|
||||
| **历史** | 覆盖写,新值顶掉旧值 | 以当前时刻为终点的**滑窗** | 每拍都要重算,但每拍只拿到当前点 → 窗永远凑不齐 |
|
||||
| **同源** | —— | `phasor` / `freq` / `meas` 必须看**同一个窗** | 各存一份历史 → 窗边界差一拍,相位差与频率跟着漂 |
|
||||
|
||||

|
||||
|
||||
**两种"更省事"的替代方案,都否决了**:
|
||||
|
||||
- **在 `phasor.c` 里自己存**:`freq` 要相邻两拍相量、`meas` 要同源快照,三个模块各存一份 →
|
||||
同一时刻存在三个"窗",一旦谁跳过一拍就没有单一真值可对,排查时分不清是采集问题还是缓存问题。
|
||||
- **在 `adc_task.c` 里存一个数组**:等于把环、窗、覆盖这些算法语义塞进任务层,既破坏
|
||||
"任务只消费 / 编排"的分层,又让这段逻辑失去主机侧单测能力(`adc_task.c` 依赖 FreeRTOS 与 HAL,
|
||||
不能原样 `#include`)。
|
||||
|
||||
结论:**"保存历史"必须是一个自己的、无依赖的、单一所有者的层**——这就是本模块存在的理由。
|
||||
|
||||
### 5.2 设计思想
|
||||
|
||||
五条,每条都对着一个具体的失败模式:
|
||||
|
||||
1. **按语义通道索引,不认物理通道**:第一维是 `adc_semantic_t`(`ADC_SEM_UA` 等),不是 `AINn`、
|
||||
不是 `ADCn`、也不是转换器通道号。V0.1 上 8 路电气量分属两片转换器、网表名与板级名有 13 路置换
|
||||
(见第 3 节),本层若认物理号,改版即全线错位。
|
||||
2. **整帧原子推进**:`push_frame()` 先把 9 路一次写齐,**最后**才动 `s_head`。消费侧看到的窗
|
||||
因此永远落在整帧边界上——单帧内不可能"半新半旧"。
|
||||
3. **只存不算**:本层不校正、不插值、不做任何跨通道运算。同刻性(8 路电气量是否同一时刻)
|
||||
归上游 `adc_input` 负责,本层既不保证也不假设。这条边界换来的是**可脱离硬件单测**:
|
||||
纯算法、不碰寄存器、不碰 RTOS,生产 `.c` 能原样 `#include` 进主机侧套件。
|
||||
4. **环形覆盖写,运行期不分配**:容量固定 40 格、静态数组,无 `malloc`、无 `memmove`。
|
||||
写满后新值直接覆盖最老值——相量只需要最近一周波,更远的历史没有保留价值。
|
||||
5. **容量取 2 周波而不是 1 周波**:1 周波够算全周傅氏,多留 1 周波是给 1.5~2 周波长窗
|
||||
(抗衰减直流分量)预留的余量,与参考系统 `CN_BUFSAM_CYCLE = 2` 同口径。改容量只动一个宏,
|
||||
但要同步 `docs/内存管理/RAM预算台账.md`(容量翻倍 = 1440 B 变 2880 B,见第 11 节)。
|
||||
|
||||

|
||||
|
||||
### 5.3 环内布局与取窗(`start` 为什么这么算)
|
||||
|
||||
`s_head` 是**下一个待写入**的位置,不是最后写入的位置——这是全部推导的起点。
|
||||
最后写入的一点是 `s_head - 1`,于是"以最新点为终点的 n 点窗"= 下标 `[s_head-n, s_head-1]`,
|
||||
窗首(最老点)就是 `s_head - n`,落到 `[0, DEPTH - 1]` 要取模。
|
||||
|
||||
取一个"head 不够减"的例子走一遍:`head = 5`、`n = 20`——最新点在 4,沿环往回退 19 步落在 25,
|
||||
途中跨过 0 与 39。直接写 `5 - 20` 会得到 −15,压根不在环上。
|
||||
|
||||

|
||||
|
||||
代码里写成 `(s_head + SAMPLE_RING_DEPTH - n) % SAMPLE_RING_DEPTH` 而不是 `(s_head - n) % DEPTH`,
|
||||
是因为后者有两个独立的坑:
|
||||
|
||||
- **16 位无符号下溢后再取模不等价**:`s_head < n` 时减法先回绕成 `65536-(n-s_head)` = 65521,
|
||||
而 65536 不是 40 的倍数,`(x mod 65536) mod 40 ≠ x mod 40`。同一个例子:正确窗首是 25,
|
||||
回绕后 `65521 % 40 = 1` → 整窗错位,读到的是环尾附近。
|
||||
- **有符号负取模**:两个 `uint16_t` 先提升成 `int`,`5 - 20 = -15`;C99 的 `%` 向零截断,
|
||||
`(-15) % 40 == -15`(不是 25),再转回无符号就是一次**越界读**——不报错、不 HardFault。
|
||||
|
||||
**先加一个 DEPTH** 同时解决两件事:`DEPTH ≡ 0 (mod DEPTH)`,加它不改变同余类、数学值不变;
|
||||
而 `s_head + DEPTH - n ∈ [0, 2·DEPTH - 2]`,恒非负、远小于 65535,两种整数提升规则下都不下溢。
|
||||
|
||||
边界自检:`n == DEPTH` 时 `start == s_head`——环满时「下一拍要覆盖的那格」就是最老点;
|
||||
未写满时 `s_head == s_filled`,故 `start >= 0`,窗一定不跨环尾。
|
||||
`App/algo/sample_ring.c` 里只留结论与指向本节的指针,推导以本节为准。
|
||||
|
||||
### 5.4 API 契约
|
||||
|
||||
| API | 契约 |
|
||||
|---|---|
|
||||
@@ -157,13 +227,16 @@ flowchart TD
|
||||
| `sample_ring_filled()` | 已累积点数(**饱和**于 40)——"够不够一周波"的判据 |
|
||||
| `sample_ring_frames()` | 累计入环帧数(**不饱和**,1 kHz 下约 49.7 天回绕)——与 `tpafe5160_frame_seq()` 对照即判丢帧 |
|
||||
|
||||
**要点**
|
||||
### 5.5 要点与禁令
|
||||
|
||||
- **整帧写入后才统一推进写指针** → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。
|
||||
- 本层**只做按时序存 / 取,不做任何跨通道运算**:同刻性由上游 语义取数与有效性负责,本层既不保证也不假设。
|
||||
判定也不在本层(本层只出计数)。
|
||||
- 某通道本拍取数失败 → **整拍跳过入环**(现 `adc_task.c` 的 `s_frames_skip` 即此),
|
||||
**切勿补 0 点**——单个 0 点会显著污染 20 点 DFT 窗。
|
||||
- **读者与写者必须同任务**:全仓唯一写者是 `adcTask` 的压环语句,读者(`phasor_run` / `meas_tick`)
|
||||
与之同拍串行,故本层不加锁、不用 `volatile`、不关中断。要把读者搬到别的任务,必须先在
|
||||
本层补双区 / 序号快照——`sample_ring_window()` 读 `s_head` 后逐点拷贝,跨任务读活缓冲会取到撕裂的窗。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@
|
||||
| `gen_data_trust_ladder_svg.py` | 数据可信台阶图 | 采集数据可信度的五级台阶:现在踩在哪一级、下一级挡什么 |
|
||||
| `gen_data_trust_dc_ac_svg.py` | ADC 直流标定与交流验证图 | 上排直流注入链、下排信号发生器正弦验证,中间列只有正弦能证的六件事 |
|
||||
| `gen_wave_record_flow_svg.py` | 故障录波数据流图 | 录波段数据的四个所有者与四条「不许」:谁独占写、谁只能读、标志怎么过界 |
|
||||
| `gen_sample_ring_why_svg.py` | 采样环缓冲_为什么需要 | 上游 1 kHz 覆盖写只交最新一帧 vs 下游要 20 点滑窗:缺口 19 点与三个消费者 |
|
||||
| `gen_sample_ring_layout_svg.py` | 采样环缓冲_环形布局 | 40 格环里哪 20 格是本窗、写指针指「下一格」、回绕与三条设计不变量 |
|
||||
| `gen_sample_ring_start_svg.py` | 采样环缓冲_窗首下标 | 窗首 = head − n 的圆周走法,与「直接减 / 16 位回绕 / 先加 DEPTH」三种写法对照 |
|
||||
|
||||
## 使用方式
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样环缓冲_环形布局.svg —— 采样环缓冲的设计思想落在图上的三件事:
|
||||
写指针指「下一格」而不是「最后一格」、窗永远紧贴指针身后、整窗随每次写入右移一格。
|
||||
|
||||
为什么不用 Mermaid:本图要读的是"40 个格子里哪 20 格是窗、指针停在第几格"——
|
||||
Mermaid 画不出等宽格子与指针位置,盒子宽度随文本变,"窗贴着指针"这个关系就没了。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_sample_ring_layout_svg.py
|
||||
输出:
|
||||
docs/images/采样环缓冲_环形布局.svg
|
||||
改内容:容量(周波数 / 每周波点数)或窗长变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样环缓冲_环形布局.svg")
|
||||
|
||||
# ----------------------------- 画布与尺寸 -----------------------------
|
||||
W = 1060
|
||||
H = 580
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
CARD_FONT = 13.5
|
||||
BODY_FONT = 11.5
|
||||
TINY_FONT = 11
|
||||
SLOT_FONT = 9
|
||||
|
||||
INK = "#0f172a"
|
||||
LINE = "#334155"
|
||||
MUTED = "#64748b"
|
||||
FAINT = "#94a3b8"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_AMBER = "#d97706"
|
||||
BG_AMBER = "#fffbeb"
|
||||
C_GREEN = "#16a34a"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_GREY = "#f1f5f9"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
DEPTH = 40 # SAMPLE_RING_DEPTH
|
||||
WIN_N = 20 # 示例窗长(一周波)
|
||||
HEAD = 30 # 示例写指针位置
|
||||
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 10
|
||||
|
||||
TITLE = "40 格环:写指针指「下一格」,窗永远紧贴它身后"
|
||||
SUB1 = "DEPTH = 2 周波 × 20 点 = 40;窗长 n 取 1~40,取的是 [head−n, head−1] 这 n 格。"
|
||||
SUB2 = "图例:蓝 = 本窗 20 点,灰 = 更旧的 10 点,琥珀 = 最旧的 10 点(下一批被覆盖)。"
|
||||
SUB3 = ("窗首 start = (head + 40 − n) % 40 —— 先加 40 再减,减法就不落到负数;"
|
||||
"每写一格整窗右移一格。")
|
||||
|
||||
MARK_START = "start(最老点)"
|
||||
MARK_NEW = "head−1(最新点)"
|
||||
MARK_HEAD = "head(下一拍写这里)"
|
||||
WRAP_NOTE = "写满后回绕:39 的下一格是 0 —— 窗可能跨过环尾,所以取数必须逐点取模"
|
||||
|
||||
CARDS = [
|
||||
("整帧写完才推进 head", C_GREEN, BG_GREEN,
|
||||
"9 路语义值一次写齐、再动指针。单帧内不存在「半新半旧」,"
|
||||
"消费侧取到的窗永远落在整帧边界上。"),
|
||||
("只存不算是分层契约", C_BLUE, BG_BLUE,
|
||||
"同刻性归上游 adc_input 负责;本层不假设、不校正,也不做任何跨通道运算——"
|
||||
"所以它能脱离硬件做主机侧单测。"),
|
||||
("容量取 2 周波", C_AMBER, BG_AMBER,
|
||||
"1 周波就够算全周傅氏;另 1 周波是给 1.5~2 周波长窗(抗衰减直流)留的余量,"
|
||||
"改容量只动一个宏。"),
|
||||
]
|
||||
FOOTNOTE = ("n = 40 时 start == head:环满时「下一拍要覆盖的那一格」恰好就是最老点 —— 不是特例,是唯一自洽解。"
|
||||
"同理 n = 20 时窗正好是 head 身后的 20 格,不多不少。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
SLOT_W = 22
|
||||
SLOT_GAP = 2
|
||||
SLOT_H = 54
|
||||
SLOT_X0 = PAD
|
||||
ROW_Y = 200
|
||||
|
||||
MARK_Y = 176 # 标记文字基线
|
||||
STEM_Y1 = 184 # 标记引线
|
||||
STEM_Y2 = ROW_Y
|
||||
|
||||
WRAP_Y = 286
|
||||
WRAP_TEXT_Y = 308
|
||||
|
||||
CARD_TOP = 340
|
||||
CARD_W = 300
|
||||
CARD_GAP = 19
|
||||
CARD_H = 120
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
OVERFLOW = []
|
||||
|
||||
|
||||
def check_fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||||
% (x, y, fs, a, w, fill, esc(s)))
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||||
% (x1, y1, x2, y2, color, width, d))
|
||||
|
||||
|
||||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s%s/>' % (
|
||||
x, y, w, h, rx, fill, s, d)
|
||||
|
||||
|
||||
def tri(points, fill):
|
||||
return '<polygon points="%s" fill="%s"/>' % (
|
||||
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
|
||||
|
||||
|
||||
def wrap(s, fs, avail, max_lines, where=""):
|
||||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||||
宁可提示文案太长,也不让字溢出盒子。"""
|
||||
if text_w(s, fs) <= avail:
|
||||
return [s]
|
||||
lines = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail:
|
||||
lines.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
if len(lines) > max_lines:
|
||||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||||
lines = lines[:max_lines]
|
||||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||||
return lines
|
||||
|
||||
|
||||
def slot_x(i):
|
||||
return SLOT_X0 + i * (SLOT_W + SLOT_GAP)
|
||||
|
||||
|
||||
def slot_c(i):
|
||||
"""第 i 格的横向中心。"""
|
||||
return slot_x(i) + SLOT_W / 2.0
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
avail = RIGHT - PAD
|
||||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||||
(SUB2, SUB_FONT, "sub2"), (SUB3, SUB_FONT, "sub3")):
|
||||
check_fit(s, fs, avail, where)
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||||
|
||||
# ---- 标题区 ----
|
||||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||||
add(txt(PAD, 108, SUB3, SUB_FONT, MUTED))
|
||||
|
||||
# ---- 40 个格子 ----
|
||||
win_lo = START
|
||||
win_hi = HEAD - 1
|
||||
for i in range(DEPTH):
|
||||
x = slot_x(i)
|
||||
if win_lo <= i <= win_hi:
|
||||
fill, stroke, sw = BG_BLUE, C_BLUE, 2.0
|
||||
elif i >= HEAD:
|
||||
fill, stroke, sw = BG_AMBER, C_AMBER, 1.4
|
||||
else:
|
||||
fill, stroke, sw = BG_GREY, FAINT, 1.2
|
||||
add(rect(x, ROW_Y, SLOT_W, SLOT_H, fill, stroke, rx=3, sw=sw))
|
||||
add(txt(slot_c(i), ROW_Y + SLOT_H / 2.0 + 3.5, str(i), SLOT_FONT, MUTED, anchor="middle"))
|
||||
|
||||
# ---- 三个关键位置标记 ----
|
||||
marks = [
|
||||
(slot_c(START), MARK_START, C_BLUE, "start"),
|
||||
(slot_c(HEAD - 1), MARK_NEW, C_BLUE, "end"),
|
||||
(slot_c(HEAD), MARK_HEAD, C_AMBER, "start"),
|
||||
]
|
||||
for cx, label, color, anchor in marks:
|
||||
add(line(cx, STEM_Y1, cx, STEM_Y2 - 2, color, 1.4))
|
||||
add(tri([(cx, STEM_Y2), (cx - 4.5, STEM_Y2 - 8), (cx + 4.5, STEM_Y2 - 8)], color))
|
||||
check_fit(label, TINY_FONT, 240, "mark %s" % label)
|
||||
add(txt(cx, MARK_Y, label, TINY_FONT, color, weight=700, anchor=anchor))
|
||||
|
||||
# ---- 回绕提示:39 的下一格是 0 ----
|
||||
x_from = slot_c(DEPTH - 1)
|
||||
x_to = slot_c(0)
|
||||
add(line(x_from, WRAP_Y, x_to + 8, WRAP_Y, C_ROSE, 1.6, dash="7 5"))
|
||||
add(tri([(x_to, WRAP_Y), (x_to + 9, WRAP_Y - 4.5), (x_to + 9, WRAP_Y + 4.5)], C_ROSE))
|
||||
for ln in wrap(WRAP_NOTE, TINY_FONT, avail, 1, "wrap note"):
|
||||
add(txt(PAD, WRAP_TEXT_Y, ln, TINY_FONT, C_ROSE, weight=700))
|
||||
|
||||
# ---- 三条设计不变量 ----
|
||||
inner = CARD_W - 24
|
||||
for i, (name, c, bg, body) in enumerate(CARDS):
|
||||
x = PAD + i * (CARD_W + CARD_GAP)
|
||||
add(rect(x, CARD_TOP, CARD_W, CARD_H, bg, c, rx=8, sw=1.8))
|
||||
check_fit(name, CARD_FONT, inner, "card %d name" % i)
|
||||
add(txt(x + 12, CARD_TOP + 24, name, CARD_FONT, INK, weight=700))
|
||||
y = CARD_TOP + 46
|
||||
for ln in wrap(body, BODY_FONT, inner, 5, "card %d body" % i):
|
||||
add(txt(x + 12, y, ln, BODY_FONT, MUTED))
|
||||
y += 15
|
||||
|
||||
# ---- 底部说明 ----
|
||||
fy = CARD_TOP + CARD_H + 30
|
||||
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||||
add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, H, W, H, FONT, "\n".join(out)))
|
||||
|
||||
d = os.path.dirname(OUT)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(svg)
|
||||
ET.fromstring(svg)
|
||||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||||
if OVERFLOW:
|
||||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,316 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样环缓冲_窗首下标.svg —— 窗首 start = head − n 为什么要先加一圈 DEPTH:
|
||||
head 不够减时直接减会走到负数,取模就再也回不到环上。
|
||||
|
||||
为什么不用 Mermaid:本图要读的是"沿圆环往回退 19 格,途中跨过 0"这条**弧**,
|
||||
以及三种写法算出来的三个**不同的数**。Mermaid 画不出圆周上的定位与角度,
|
||||
盒子宽度也随文本变,"退半圈落在哪一格"这个关系就没了。
|
||||
|
||||
与 5.2 的环形布局图分工:那张讲「窗在哪几格」(线性排开 40 格 + 指针),
|
||||
本图讲「这个下标怎么算出来」(圆周 + 三种写法对照),不重复。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_sample_ring_start_svg.py
|
||||
输出:
|
||||
docs/images/采样环缓冲_窗首下标.svg
|
||||
改内容:示例(head / n)或三种写法的结论变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||||
"""
|
||||
import html
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样环缓冲_窗首下标.svg")
|
||||
|
||||
# ----------------------------- 画布与尺寸 -----------------------------
|
||||
W = 1060
|
||||
H = 600
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
MONO = "Consolas, Cascadia Mono, DejaVu Sans Mono, monospace"
|
||||
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
BOX_FONT = 13
|
||||
BODY_FONT = 11.5
|
||||
TINY_FONT = 11
|
||||
DOT_LABEL_FONT = 12
|
||||
|
||||
INK = "#0f172a"
|
||||
LINE = "#334155"
|
||||
MUTED = "#64748b"
|
||||
FAINT = "#94a3b8"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
C_GREEN = "#16a34a"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
C_AMBER = "#d97706"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
DEPTH = 40
|
||||
WIN_N = 20
|
||||
HEAD = 5 # 示例写指针:故意取"不够减"的位置
|
||||
START = (HEAD + DEPTH - WIN_N) % DEPTH # = 25
|
||||
|
||||
TITLE = "窗首 = head − n:head 不够减会走到负数,所以先加一圈 DEPTH 再减"
|
||||
SUB1 = ("例:head = 5、n = 20 —— 最新点在 4,沿环往回退 19 步落在 25,途中跨过 0 与 39。"
|
||||
"DEPTH = 40 时 20 点窗正好占半圈,所以这条弧看起来是个半圆。")
|
||||
SUB2 = "同一个例子三种写法,只有第三种同时做到「同余正确」与「不发生下溢」:"
|
||||
|
||||
ARC_LABEL = "往回退 19 步"
|
||||
CENTER_L1 = "环 = 40 格"
|
||||
CENTER_L2 = "窗 = 20 格(半圈)"
|
||||
HEAD_LABEL = "head = 5"
|
||||
START_LABEL = "start = 25"
|
||||
|
||||
# (公式, 算出来是多少, 判定, 逐条理由, 主色, 底色)
|
||||
ROWS = [
|
||||
("5 - 20", "= -15", "✗",
|
||||
["负数。C99 的 % 向零截断,(-15) % 40 仍是 -15;",
|
||||
"转成无符号就是 65521,拿去索引 s_ring 即越界读。"],
|
||||
C_ROSE, BG_ROSE),
|
||||
("(uint16_t)(5 - 20) % 40", "= 65521 % 40 = 1", "✗",
|
||||
["65536 不是 40 的倍数:(x mod 65536) mod 40 ≠ x mod 40。",
|
||||
"回绕后再取模不等价,读到的是环尾附近而不是窗首。"],
|
||||
C_ROSE, BG_ROSE),
|
||||
("(5 + 40 - 20) % 40", "= 25 % 40 = 25", "✓",
|
||||
["就是窗首。加 40 不改同余类(40 ≡ 0 mod 40),",
|
||||
"中间值恒在 [0, 2·DEPTH - 2],两种提升规则都不下溢。"],
|
||||
C_GREEN, BG_GREEN),
|
||||
]
|
||||
FOOTNOTE = ("加 DEPTH 不是「凑数」:它是环的周长,加一整圈等价于原地不动——"
|
||||
"但把减法从「可能为负」搬到了「必然非负」,取模才恢复成数学上的同余解。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
CX = 296.0
|
||||
CY = 352.0
|
||||
R = 148.0
|
||||
DOT_R = 5.0
|
||||
DOT_R_HL = 6.0
|
||||
|
||||
BOX_X = 596
|
||||
BOX_W = RIGHT - BOX_X
|
||||
BOX_TOP = 186
|
||||
BOX_H = 96
|
||||
BOX_GAP = 14
|
||||
|
||||
NOTE_Y = 556
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
OVERFLOW = []
|
||||
|
||||
|
||||
def check_fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def txt(x, y, s, fs, fill, weight=400, anchor=None, mono=False):
|
||||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||||
f = "" if not mono else ' font-family="%s"' % MONO
|
||||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s%s fill="%s">%s</text>'
|
||||
% (x, y, fs, a, w, f, fill, esc(s)))
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||||
% (x1, y1, x2, y2, color, width, d))
|
||||
|
||||
|
||||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s%s/>' % (
|
||||
x, y, w, h, rx, fill, s, d)
|
||||
|
||||
|
||||
def circle(cx, cy, r, fill, stroke=None, sw=1.5):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
return '<circle cx="%.1f" cy="%.1f" r="%g" fill="%s"%s/>' % (cx, cy, r, fill, s)
|
||||
|
||||
|
||||
def tri(points, fill):
|
||||
return '<polygon points="%s" fill="%s"/>' % (
|
||||
" ".join("%.1f,%.1f" % (px, py) for px, py in points), fill)
|
||||
|
||||
|
||||
def pt(r, deg):
|
||||
"""圆上一点:deg 以 +x 轴为 0、屏幕坐标 y 向下(故 90° 在正下方)。"""
|
||||
a = math.radians(deg)
|
||||
return CX + r * math.cos(a), CY + r * math.sin(a)
|
||||
|
||||
|
||||
def angle_of(idx):
|
||||
"""第 idx 格的角度:0 格在正上方,顺时针每格 360/DEPTH 度。"""
|
||||
return -90.0 + idx * (360.0 / DEPTH)
|
||||
|
||||
|
||||
def arc_arrow(r, a_from, a_to, color, width):
|
||||
"""从 a_from 逆时针画到 a_to(a_to < a_from),末端带箭头。"""
|
||||
x0, y0 = pt(r, a_from)
|
||||
x1, y1 = pt(r, a_to)
|
||||
large = 1 if abs(a_to - a_from) > 180.0 else 0
|
||||
d = "M %.1f %.1f A %.1f %.1f 0 %d 0 %.1f %.1f" % (x0, y0, r, r, large, x1, y1)
|
||||
out = '<path d="%s" fill="none" stroke="%s" stroke-width="%g"/>' % (d, color, width)
|
||||
# 末端切向:用 a_to 与其后方 1° 两点之差,得到"行进方向"
|
||||
xb, yb = pt(r, a_to + 1.0)
|
||||
vx, vy = x1 - xb, y1 - yb
|
||||
n = math.hypot(vx, vy) or 1.0
|
||||
ux, uy = vx / n, vy / n
|
||||
hl = 10.0
|
||||
bx, by = x1 - ux * hl, y1 - uy * hl
|
||||
px, py = -uy, ux
|
||||
out += tri([(x1, y1), (bx + px * 4.6, by + py * 4.6), (bx - px * 4.6, by - py * 4.6)], color)
|
||||
return out
|
||||
|
||||
|
||||
def wrap(s, fs, avail, max_lines, where=""):
|
||||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||||
宁可提示文案太长,也不让字溢出盒子。"""
|
||||
if text_w(s, fs) <= avail:
|
||||
return [s]
|
||||
lines = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail:
|
||||
lines.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
if len(lines) > max_lines:
|
||||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||||
lines = lines[:max_lines]
|
||||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||||
return lines
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
avail = RIGHT - PAD
|
||||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||||
(SUB2, SUB_FONT, "sub2")):
|
||||
check_fit(s, fs, avail, where)
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||||
|
||||
# ---- 标题区 ----
|
||||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||||
|
||||
# ---- 左:40 格圆环 ----
|
||||
win_lo = START
|
||||
win_hi = HEAD - 1
|
||||
add(circle(CX, CY, R, "none", RULE, 1.2))
|
||||
for i in range(DEPTH):
|
||||
x, y = pt(R, angle_of(i))
|
||||
in_win = (i >= win_lo) or (i <= win_hi)
|
||||
if i == HEAD:
|
||||
add(circle(x, y, DOT_R_HL, C_AMBER))
|
||||
elif in_win:
|
||||
add(circle(x, y, DOT_R_HL, C_BLUE))
|
||||
else:
|
||||
add(circle(x, y, DOT_R, FAINT))
|
||||
|
||||
# 往回退的弧(贴着点圈内侧走,末端箭头落在 start 上)
|
||||
add(arc_arrow(R - 18.0, angle_of(win_hi), angle_of(win_lo) - 360.0, C_ROSE, 1.8))
|
||||
lx, ly = pt(R - 70.0, angle_of(33))
|
||||
add(txt(lx, ly, ARC_LABEL, TINY_FONT, C_ROSE, weight=700, anchor="middle"))
|
||||
|
||||
# 圆周上的刻度名
|
||||
for i in (0, 10, 20, 30):
|
||||
x, y = pt(R + 22.0, angle_of(i))
|
||||
anchor = "middle"
|
||||
if i == 10:
|
||||
anchor = "start"
|
||||
elif i == 30:
|
||||
anchor = "end"
|
||||
add(txt(x, y + 4, str(i), DOT_LABEL_FONT, MUTED, anchor=anchor))
|
||||
|
||||
# head / start 两个关键点
|
||||
hx, hy = pt(R + 26.0, angle_of(HEAD))
|
||||
check_fit(HEAD_LABEL, TINY_FONT, 200, "head label")
|
||||
add(txt(hx, hy, HEAD_LABEL, TINY_FONT, C_AMBER, weight=700, anchor="start"))
|
||||
sx, sy = pt(R + 26.0, angle_of(START))
|
||||
check_fit(START_LABEL, TINY_FONT, 200, "start label")
|
||||
add(txt(sx, sy, START_LABEL, TINY_FONT, C_BLUE, weight=700, anchor="end"))
|
||||
|
||||
# 圆心说明
|
||||
add(txt(CX, CY - 6, CENTER_L1, TINY_FONT, FAINT, anchor="middle"))
|
||||
add(txt(CX, CY + 14, CENTER_L2, TINY_FONT, FAINT, anchor="middle"))
|
||||
|
||||
# ---- 右:三种写法 ----
|
||||
inner = BOX_W - 32
|
||||
for i, (formula, value, mark, why, c, bg) in enumerate(ROWS):
|
||||
y = BOX_TOP + i * (BOX_H + BOX_GAP)
|
||||
add(rect(BOX_X, y, BOX_W, BOX_H, bg, c, rx=8, sw=1.8))
|
||||
add(txt(BOX_X + 14, y + 26, "%s %s" % (mark, formula), BOX_FONT, c, weight=700, mono=True))
|
||||
add(txt(BOX_X + BOX_W - 14, y + 26, value, BOX_FONT, c, weight=700, anchor="end", mono=True))
|
||||
yy = y + 48
|
||||
for ln in why:
|
||||
for sub in wrap(ln, BODY_FONT, inner, 1, "row %d why" % i):
|
||||
add(txt(BOX_X + 14, yy, sub, BODY_FONT, MUTED))
|
||||
yy += 16
|
||||
|
||||
# ---- 底部说明 ----
|
||||
add(line(PAD, NOTE_Y - 26, RIGHT, NOTE_Y - 26, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||||
add(txt(PAD, NOTE_Y + i * 19, ln, SUB_FONT, MUTED))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, H, W, H, FONT, "\n".join(out)))
|
||||
|
||||
d = os.path.dirname(OUT)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(svg)
|
||||
ET.fromstring(svg)
|
||||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""生成 docs/images/采样环缓冲_为什么需要.svg —— 采样环缓冲存在的理由:
|
||||
上游每拍只交"最新一帧"(覆盖写),下游相量却要"以当前时刻为终止的一整周波"(20 点滑窗),
|
||||
中间那 19 个历史点没有任何一层替它保留,本层就是补这个缺口的地方。
|
||||
|
||||
为什么不用 Mermaid:本图要读的是"同一条时间轴上,20 个刻度里只有 1 个点被交付"这个
|
||||
**数量差**——Mermaid 的节点按文本自适应宽、节点之间只有连线,表达不出"哪一段拿不到"。
|
||||
谁调用谁的拓扑另有 `docs/算法/采集与算法层设计说明.md` 第 2 节的 Mermaid 流程图,本图不重复。
|
||||
|
||||
用法(仓库根目录,任意 python3):
|
||||
python tools/arch_diagram/gen_sample_ring_why_svg.py
|
||||
输出:
|
||||
docs/images/采样环缓冲_为什么需要.svg
|
||||
改内容:口径(周波点数 / 消费者清单)变了才重画;调坐标 / 文案编辑下方常量后重跑。
|
||||
"""
|
||||
import html
|
||||
import os
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
def emit(s):
|
||||
"""控制台打印按目标编码替换,绝不因 GBK 终端遇上 ²/− 之类字符而崩
|
||||
(generated-fresh 钩子会把本脚本拷进临时目录重跑,崩了就是一片假红)。"""
|
||||
enc = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
sys.stdout.write(s.encode(enc, "replace").decode(enc, "replace") + "\n")
|
||||
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT = os.path.join(ROOT, "docs", "images", "采样环缓冲_为什么需要.svg")
|
||||
|
||||
# ----------------------------- 画布与尺寸 -----------------------------
|
||||
W = 1060
|
||||
H = 600
|
||||
PAD = 46
|
||||
RIGHT = W - PAD
|
||||
|
||||
FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif"
|
||||
|
||||
TITLE_FONT = 21
|
||||
SUB_FONT = 13
|
||||
PANEL_FONT = 14.5
|
||||
CARD_FONT = 13.5
|
||||
BODY_FONT = 11.5
|
||||
TINY_FONT = 11
|
||||
|
||||
INK = "#0f172a"
|
||||
LINE = "#334155"
|
||||
MUTED = "#64748b"
|
||||
FAINT = "#94a3b8"
|
||||
RULE = "#cbd5e1"
|
||||
WHITE = "#ffffff"
|
||||
|
||||
C_BLUE = "#2563eb"
|
||||
BG_BLUE = "#eff6ff"
|
||||
C_ROSE = "#e11d48"
|
||||
BG_ROSE = "#fff1f2"
|
||||
C_GREEN = "#16a34a"
|
||||
BG_GREEN = "#f0fdf4"
|
||||
C_AMBER = "#d97706"
|
||||
BG_AMBER = "#fffbeb"
|
||||
|
||||
# ----------------------------- 数据(改这里) -----------------------------
|
||||
TITLE = "上游只交最新一帧,相量却要一整周波 —— 采样环补的就是这 19 点"
|
||||
SUB1 = ("上游(adc_input)每 1 ms 交付一次,且只交本拍;旧值被覆盖、不保留历史。"
|
||||
"下游相量要的是「以当前时刻为终止的 20 点滑窗」。")
|
||||
SUB2 = "本层不改采集、不改算法,只把「覆盖写的最新值」转成「可回溯的滑动窗」——三个消费者共用同一份。"
|
||||
|
||||
PANEL_A = "① 上游交付物:1 kHz 覆盖写,每拍只交一帧"
|
||||
PANEL_A_NOTE = "20 个刻度 = 一周波 20 点(1 kHz @ 50 Hz);上游只把最后一个点交出来"
|
||||
BRACKET = "相量要的 20 点窗(一整周波)"
|
||||
GAP_NOTE = "这 19 个点上游不保留 —— 不缓冲就永远取不到"
|
||||
NOW_NOTE = "最新一帧(本拍)"
|
||||
|
||||
PANEL_B = "② 下游需求:三个消费者,必须看同一份历史"
|
||||
CARDS = [
|
||||
("phasor_run()", C_BLUE, BG_BLUE,
|
||||
"每 1 ms 要一整周波 20 点,算全周傅氏相量(幅值 / 相位)。"
|
||||
"窗短一点就不是「全周」,谐波抑制立刻失效。"),
|
||||
("freq_run()", C_GREEN, BG_GREEN,
|
||||
"要相邻两拍相量的相位差 → 反推频率。它与相量必须落在同一个窗上,"
|
||||
"否则相位差里混进窗长误差。"),
|
||||
("meas_tick()", C_AMBER, BG_AMBER,
|
||||
"每 200 ms 发布遥测快照。快照里的相量 / 频率 / 品质位都取自同一次窗,"
|
||||
"不允许各取各的。"),
|
||||
]
|
||||
BAND_NOTE = "共用同一份环 = 同一拍只有一个窗 —— 口径不会各自漂移"
|
||||
FOOTNOTE = ("缓冲只解决「取得到历史」,不解决「这些点是否同刻」:同刻性归上游 adc_input 负责,"
|
||||
"本层只按时序存 / 取,不做任何跨通道运算。")
|
||||
|
||||
# ----------------------------- 布局 -----------------------------
|
||||
PANEL_A_TOP = 112
|
||||
PANEL_A_BOT = 300
|
||||
AXIS_Y = 236
|
||||
TICK_X0 = 74
|
||||
TICK_STEP = 38
|
||||
N_TICKS = 20 # 一周波 20 点
|
||||
TICK_H = 22
|
||||
|
||||
BRACKET_TOP = 192
|
||||
BRACKET_H = 20
|
||||
|
||||
PANEL_B_TOP = 330
|
||||
CARD_W = 300
|
||||
CARD_GAP = 19
|
||||
CARD_H = 112
|
||||
CARD_X0 = PAD
|
||||
|
||||
|
||||
# ----------------------------- 工具函数 -----------------------------
|
||||
def esc(s):
|
||||
return html.escape(s, quote=True)
|
||||
|
||||
|
||||
def text_w(s, fs):
|
||||
"""粗略估算文本像素宽:CJK/全角 ≈ 1.0em,其余 ≈ 0.58em(宁可偏大)。"""
|
||||
return sum(fs * (1.0 if ord(ch) >= 0x2E80 else 0.58) for ch in s)
|
||||
|
||||
|
||||
OVERFLOW = []
|
||||
|
||||
|
||||
def check_fit(s, fs, avail, where):
|
||||
if text_w(s, fs) > avail:
|
||||
OVERFLOW.append("%s: %.0f > %.0f [%s]" % (where, text_w(s, fs), avail, s))
|
||||
|
||||
|
||||
def txt(x, y, s, fs, fill, weight=400, anchor=None):
|
||||
a = "" if anchor is None else ' text-anchor="%s"' % anchor
|
||||
w = "" if weight == 400 else ' font-weight="%d"' % weight
|
||||
return ('<text x="%.1f" y="%.1f" font-size="%g"%s%s fill="%s">%s</text>'
|
||||
% (x, y, fs, a, w, fill, esc(s)))
|
||||
|
||||
|
||||
def line(x1, y1, x2, y2, color=LINE, width=2.0, dash=None):
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return ('<line x1="%.1f" y1="%.1f" x2="%.1f" y2="%.1f" stroke="%s" stroke-width="%g"%s/>'
|
||||
% (x1, y1, x2, y2, color, width, d))
|
||||
|
||||
|
||||
def rect(x, y, w, h, fill, stroke=None, rx=8, sw=1.6, dash=None):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
d = "" if dash is None else ' stroke-dasharray="%s"' % dash
|
||||
return '<rect x="%.1f" y="%.1f" width="%.1f" height="%.1f" rx="%g" fill="%s"%s%s/>' % (
|
||||
x, y, w, h, rx, fill, s, d)
|
||||
|
||||
|
||||
def circle(cx, cy, r, fill, stroke=None, sw=1.5):
|
||||
s = "" if stroke is None else ' stroke="%s" stroke-width="%g"' % (stroke, sw)
|
||||
return '<circle cx="%.1f" cy="%.1f" r="%g" fill="%s"%s/>' % (cx, cy, r, fill, s)
|
||||
|
||||
|
||||
def wrap(s, fs, avail, max_lines, where=""):
|
||||
"""按估算宽度折行(CJK 逐字可断)。超出 max_lines 用省略号收尾——
|
||||
宁可提示文案太长,也不让字溢出盒子。"""
|
||||
if text_w(s, fs) <= avail:
|
||||
return [s]
|
||||
lines = []
|
||||
cur = ""
|
||||
for ch in s:
|
||||
if text_w(cur + ch, fs) > avail:
|
||||
lines.append(cur)
|
||||
cur = ch
|
||||
else:
|
||||
cur += ch
|
||||
if cur:
|
||||
lines.append(cur)
|
||||
if len(lines) > max_lines:
|
||||
OVERFLOW.append("%s: 折行 %d > %d" % (where, len(lines), max_lines))
|
||||
lines = lines[:max_lines]
|
||||
lines[max_lines - 1] = lines[max_lines - 1][:-1] + "…"
|
||||
return lines
|
||||
|
||||
|
||||
# ----------------------------- 生成 -----------------------------
|
||||
def build():
|
||||
out = []
|
||||
add = out.append
|
||||
|
||||
avail = RIGHT - PAD
|
||||
for s, fs, where in ((TITLE, TITLE_FONT, "title"), (SUB1, SUB_FONT, "sub1"),
|
||||
(SUB2, SUB_FONT, "sub2")):
|
||||
check_fit(s, fs, avail, where)
|
||||
if OVERFLOW:
|
||||
for o in OVERFLOW:
|
||||
emit("OVERFLOW WARN " + o)
|
||||
|
||||
add('<rect x="0" y="0" width="%d" height="%d" fill="%s"/>' % (W, H, WHITE))
|
||||
|
||||
# ---- 标题区 ----
|
||||
add(txt(PAD, 42, TITLE, TITLE_FONT, INK, weight=700))
|
||||
add(txt(PAD, 68, SUB1, SUB_FONT, MUTED))
|
||||
add(txt(PAD, 88, SUB2, SUB_FONT, MUTED))
|
||||
|
||||
# ---- 面板 A:上游覆盖写 vs 下游要 20 点 ----
|
||||
add(rect(PAD - 12, PANEL_A_TOP, avail + 24, PANEL_A_BOT - PANEL_A_TOP, BG_ROSE, None, rx=12))
|
||||
check_fit(PANEL_A, PANEL_FONT, avail - 24, "panel A")
|
||||
add(txt(PAD, PANEL_A_TOP + 24, PANEL_A, PANEL_FONT, C_ROSE, weight=700))
|
||||
add(txt(PAD, PANEL_A_TOP + 44, PANEL_A_NOTE, TINY_FONT, MUTED))
|
||||
|
||||
# 时间轴
|
||||
add(line(TICK_X0 - 12, AXIS_Y, TICK_X0 + (N_TICKS - 1) * TICK_STEP + 10, AXIS_Y, LINE, 2.0))
|
||||
xs = []
|
||||
for i in range(N_TICKS):
|
||||
x = TICK_X0 + i * TICK_STEP
|
||||
xs.append(x)
|
||||
# 前 19 点是"上游不保留"的,画成浅灰;最后一点是本拍交付的
|
||||
c = C_BLUE if i == N_TICKS - 1 else FAINT
|
||||
add(line(x, AXIS_Y, x, AXIS_Y - TICK_H, c, 2.0 if i == N_TICKS - 1 else 1.4))
|
||||
|
||||
# 20 点窗的括号
|
||||
bx1 = xs[0] - 9
|
||||
bx2 = xs[-1] + 9
|
||||
add(rect(bx1, BRACKET_TOP, bx2 - bx1, BRACKET_H, "none", C_ROSE, rx=6, sw=1.8, dash="7 5"))
|
||||
add(txt((bx1 + bx2) / 2.0, BRACKET_TOP + 15, BRACKET, BODY_FONT, C_ROSE, weight=700, anchor="middle"))
|
||||
|
||||
# 本拍交付点
|
||||
add(circle(xs[-1], AXIS_Y, 5.5, C_BLUE))
|
||||
add(txt(xs[-1] + 12, AXIS_Y + 5, NOW_NOTE, TINY_FONT, C_BLUE, weight=700))
|
||||
|
||||
# 轴下的缺口条:只覆盖前 19 点,直观标出"这 19 个点上游没留"
|
||||
add(rect(xs[0] - 9, AXIS_Y + 8, xs[-2] - xs[0] + 18, 5, C_ROSE, None, rx=2.5))
|
||||
|
||||
# 轴下刻度标注
|
||||
add(txt(xs[0], AXIS_Y + 30, "k−19", TINY_FONT, MUTED, anchor="middle"))
|
||||
add(txt(xs[0] + 10 * TICK_STEP, AXIS_Y + 30, "k−9", TINY_FONT, MUTED, anchor="middle"))
|
||||
add(txt(xs[-1], AXIS_Y + 30, "k", TINY_FONT, MUTED, anchor="middle"))
|
||||
add(txt(PAD, AXIS_Y + 52, GAP_NOTE, TINY_FONT, C_ROSE, weight=700))
|
||||
|
||||
# ---- 面板 B:三个消费者 ----
|
||||
check_fit(PANEL_B, PANEL_FONT, avail, "panel B")
|
||||
add(txt(PAD, PANEL_B_TOP + 4, PANEL_B, PANEL_FONT, C_BLUE, weight=700))
|
||||
add(txt(PAD, PANEL_B_TOP + 24, BAND_NOTE, TINY_FONT, MUTED))
|
||||
|
||||
card_y = PANEL_B_TOP + 40
|
||||
inner = CARD_W - 24
|
||||
for i, (name, c, bg, body) in enumerate(CARDS):
|
||||
x = CARD_X0 + i * (CARD_W + CARD_GAP)
|
||||
add(rect(x, card_y, CARD_W, CARD_H, bg, c, rx=8, sw=1.8))
|
||||
check_fit(name, CARD_FONT, inner, "card %d name" % i)
|
||||
add(txt(x + 12, card_y + 24, name, CARD_FONT, INK, weight=700))
|
||||
y = card_y + 46
|
||||
for ln in wrap(body, BODY_FONT, inner, 5, "card %d body" % i):
|
||||
add(txt(x + 12, y, ln, BODY_FONT, MUTED))
|
||||
y += 15
|
||||
|
||||
# ---- 底部说明 ----
|
||||
fy = card_y + CARD_H + 34
|
||||
add(line(PAD, fy, RIGHT, fy, RULE, 1.0))
|
||||
for i, ln in enumerate(wrap(FOOTNOTE, SUB_FONT, avail, 2, "foot wrap")):
|
||||
add(txt(PAD, fy + 24 + i * 19, ln, SUB_FONT, MUTED))
|
||||
|
||||
svg = ('<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" '
|
||||
'viewBox="0 0 %d %d" font-family="%s">\n%s\n</svg>\n'
|
||||
% (W, H, W, H, FONT, "\n".join(out)))
|
||||
|
||||
d = os.path.dirname(OUT)
|
||||
if not os.path.isdir(d):
|
||||
os.makedirs(d)
|
||||
with open(OUT, "w", encoding="utf-8", newline="\n") as fp:
|
||||
fp.write(svg)
|
||||
ET.fromstring(svg)
|
||||
emit("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, H, OUT))
|
||||
if OVERFLOW:
|
||||
emit("!! %d overflow warnings" % len(OVERFLOW))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
Reference in New Issue
Block a user