feat(屏板): 帧 B 组帧 + 中断起发 TX DMA + 按键队列 + 离线判定(批 2)
批 2 五项全部落地(docs/任务规划/屏板链路.md §3): - 2-1 sb_link_frame_update():uiTask 渲染后按需组帧,不留 3200 B 影子 副本,直接拿活显存与帧内点阵段 memcmp;组完自校验,过了才置 ready。 静态 4208 B 帧缓冲实测落 RW_IRAM1(0x20006d98),不在 CCM。 - 2-2 空闲中断校完 6 B 召唤帧直接点 TX DMA(sb_tx_kick),不排队不攒批; busy 期间再来的召唤丢帧并计数,绝不叠发(屏板收帧一次 IDLE 一包)。 - 2-3 按键走环形队列(中断入、任务出),uiTask 按 §4.3 表映射 UI_KEY_* 后调 ui_port_queue_key;9/10(RESET/FACTORY)按决策四先丢弃并留痕。 - 2-4 HAL_UART_TxCpltCallback 在 rs485.c 加第三行转发(三个全局回调 落点收一处,防日后重复定义);busy 冻结帧缓冲,错误中断按 gState 收拾被打断的 TX,防残留 busy 把链路永久锁死。 - 2-5 连续 >=10 s 无有效召唤帧才判离线(不按 500 ms——屏板约 5000 s 自复位有 1 s 空窗),恢复即清标志;CRC 错帧静默丢弃并计数。 实测:台架 40 s(PC 顶替屏板,--key-fixed 2 持续改画面)rx=143 ok=143 keys=143 crc=0 / tx=143 done=143 drop=0 abor=0 fb=144 bad=0;脚本一停 10 s 准点出 OFFLINE。组帧耗时 509 µs 典型 / 709 µs 峰值——原 0.3 ms 判据系估算,已按实测在规划文档改口为 1 ms 并给出压降路径(未做)。 附带:rs485_on_rx 补 CODE-RULE-09 文档块(本轮改 rs485.c 触发整文件 重扫被拦);CRC 循环计数改 32 位(消掉 ARMCC 每轮 UXTH);bench 脚本 加 --reply/--key-fixed/--dump,并写明 CH340 收不了 700 kbaud 连续流、 --reply 收不全 4208 B 不是固件问题。 主机单测 146/146 PASSED(+65);Keil 0 错误 0 警告;ci_local 全绿 (SKIP=nested-comment,worktree 限制,主树脚本另查 0 新发)。 Code=238,936 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %。
This commit is contained in:
@@ -34,6 +34,20 @@
|
||||
* TC-SBLINK-013 统计上报的时基回绕:now_ms 跨 0xFFFFFFFF 仍按差值法工作
|
||||
* TC-SBLINK-014 统计快照:空指针守卫、字段逐项对得上
|
||||
*
|
||||
* 覆盖点(对应 docs/任务规划/屏板链路.md §3 批 2 的五项):
|
||||
* TC-SBLINK-016 组帧入口守卫:未初始化 / 空显存指针
|
||||
* TC-SBLINK-017 帧 B 内容:点阵逐字节、info[0..3] 小端掩码、info[4..7] 非 LOGO、
|
||||
* 998 B 填充全 0、CRC 覆盖 [0, 4205](2-1 判据)
|
||||
* TC-SBLINK-018 变化检测:内容不变不重组;点阵改一字节要重组;只改掩码也要重组
|
||||
* TC-SBLINK-019 中断起发:未组帧不发;一召唤一帧 4208 B;TX 期间不叠发(2-2 判据)
|
||||
* TC-SBLINK-020 TX 占用期间帧缓冲逐字节冻结,TxCplt 后补组(2-4 的"无脏帧")
|
||||
* TC-SBLINK-021 起发失败要清 busy,否则链路此后永不发送
|
||||
* TC-SBLINK-022 错误中断里清残留 busy;但 TX 真的还在发时不得判死(2-4 的"无残留 busy")
|
||||
* TC-SBLINK-023 按键队列:单次交付、周期帧不带键、溢出丢新并计数、FIFO 顺序(2-3)
|
||||
* TC-SBLINK-024 离线判定:9.999 s 不判、10 s 判、不重复刷屏、一帧即恢复(2-5 判据)
|
||||
* TC-SBLINK-025 组帧耗时换算:周期数 → µs 的除数(SystemCoreClock/1e6)
|
||||
* TC-SBLINK-026 上报行:fb/tx 列可见;离线行排在周期行之前且不重复
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
*
|
||||
@@ -83,6 +97,7 @@ static bsp_err_t setup_link(void)
|
||||
s_uart.Init.Mode = 3U;
|
||||
s_uart.Init.OverSampling = 16U;
|
||||
s_uart.RxState = HAL_UART_STATE_READY;
|
||||
s_uart.gState = HAL_UART_STATE_READY;
|
||||
s_uart.ErrorCode = 0U;
|
||||
|
||||
s_other.Instance = &g_sb_uart;
|
||||
@@ -114,6 +129,30 @@ static void make_frame(uint8_t *p_frame, uint8_t addr, uint8_t func, uint8_t cmd
|
||||
p_frame[5] = (uint8_t)((crc >> 8) & 0xFFU);
|
||||
}
|
||||
|
||||
/* 假显存:组帧的输入(内容按 i 变化,方便"改一个字节"的用例) */
|
||||
static uint8_t s_bmp[SB_FRAME_B_BMP_LEN];
|
||||
|
||||
static void fill_bmp(uint8_t seed)
|
||||
{
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0U; i < SB_FRAME_B_BMP_LEN; i++) {
|
||||
s_bmp[i] = (uint8_t)(seed + (uint8_t)(i & 0xFFU));
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 模拟"HAL 的 DMA 传完 → gState 打回 READY → 调 TxCplt 回调"。
|
||||
* 真机这两步在 HAL_DMA_IRQHandler 里连着做(UART_DMATransmitCplt 里先改 gState
|
||||
* 再回调),所以测试也必须按这个顺序——否则"错误中断里靠 gState 判残留 busy"
|
||||
* 那条用例的前提就不成立了。
|
||||
*/
|
||||
static void tx_complete(void)
|
||||
{
|
||||
s_uart.gState = HAL_UART_STATE_READY;
|
||||
sb_link_on_tx_cplt(&s_uart);
|
||||
}
|
||||
|
||||
/* ===================== 用例 ===================== */
|
||||
|
||||
static void tc_init_guards(void)
|
||||
@@ -383,20 +422,24 @@ static void tc_poll_reporting(void)
|
||||
sb_link_poll(2U * SB_LINK_REPORT_MS);
|
||||
CHECK(sb_log_n == 2U, "TC-SBLINK-012g nothing is emitted when no counter changed");
|
||||
|
||||
/* 有错帧:再出一行,且 err/crc 列可见 */
|
||||
/* 有错帧:再出一行,且 crc 列可见。
|
||||
* 同时喂一帧正常召唤:本用例的时基是每步 +5 s,单靠错帧会让"连续 10 s 无**有效**
|
||||
* 召唤帧"在第三步就成立(那是 2-5 的离线判定,由 TC-SBLINK-027 专测),
|
||||
* 那行 OFFLINE 会插进来把行号错开,让本用例悄悄变成在测别的东西。 */
|
||||
{
|
||||
uint8_t bad[SB_FRAME_A_LEN];
|
||||
|
||||
(void)memcpy(bad, k_rt, SB_FRAME_A_LEN);
|
||||
bad[5] ^= 0xFFU;
|
||||
feed(bad, SB_FRAME_A_LEN);
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
}
|
||||
sb_link_poll(3U * SB_LINK_REPORT_MS);
|
||||
CHECK(sb_log_n == 3U, "TC-SBLINK-012h a CRC error triggers a new report");
|
||||
CHECK(sb_log_has(2U, "crc=1") == 1, "TC-SBLINK-012i the report carries the CRC error count");
|
||||
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.rx_events == 3U, "TC-SBLINK-012j (sanity) three events were fed in total");
|
||||
CHECK(st.rx_events == 4U, "TC-SBLINK-012j (sanity) four events were fed in total");
|
||||
}
|
||||
|
||||
static void tc_poll_wraparound(void)
|
||||
@@ -438,6 +481,328 @@ static void tc_stats_snapshot(void)
|
||||
CHECK(st.rearm_fail == 0U, "TC-SBLINK-014e ... rearm_fail");
|
||||
}
|
||||
|
||||
/* ===================== 批 2 用例(帧 B 组帧 / 起发 / 收尾 / 按键 / 离线) ===================== */
|
||||
|
||||
static void tc_frame_update_guards(void)
|
||||
{
|
||||
fill_bmp(0x11U);
|
||||
|
||||
/* 未初始化:STATE。直接清 s_huart(本用例白盒:测试 TU 里 include 了 sb_link.c,
|
||||
* 静态量可见)——否则本用例能否通过就取决于"有没有人先跑过 setup_link",
|
||||
* 那是用例顺序依赖,加一条用例就可能把它弄红。 */
|
||||
s_huart = NULL;
|
||||
sb_fake_reset();
|
||||
sb_log_reset();
|
||||
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_STATE,
|
||||
"TC-SBLINK-016 a frame update before init is refused");
|
||||
|
||||
(void)setup_link();
|
||||
CHECK(sb_link_frame_update(NULL, 0U) == BSP_ERR_PARAM,
|
||||
"TC-SBLINK-016b a NULL framebuffer is refused");
|
||||
}
|
||||
|
||||
static void tc_frame_build_content(void)
|
||||
{
|
||||
uint32_t i;
|
||||
uint32_t pad_bad = 0U;
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x11U);
|
||||
|
||||
CHECK(sb_link_frame_update(s_bmp, 0xA5A55A5AU) == BSP_OK,
|
||||
"TC-SBLINK-017 the first frame is built");
|
||||
CHECK(memcmp(s_frame, s_bmp, SB_FRAME_B_BMP_LEN) == 0,
|
||||
"TC-SBLINK-017b ... the bitmap region is a verbatim copy of the live framebuffer");
|
||||
/* 0xA5A55A5A 小端 = 5A 5A A5 A5 */
|
||||
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 0U] == 0x5AU) &&
|
||||
(s_frame[SB_FRAME_B_INFO_OFF + 1U] == 0x5AU) &&
|
||||
(s_frame[SB_FRAME_B_INFO_OFF + 2U] == 0xA5U) &&
|
||||
(s_frame[SB_FRAME_B_INFO_OFF + 3U] == 0xA5U),
|
||||
"TC-SBLINK-017c ... the LED mask lands in info[0..3] little-endian");
|
||||
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 4U] == 0U) &&
|
||||
(s_frame[SB_FRAME_B_INFO_OFF + 5U] == 0U),
|
||||
"TC-SBLINK-017d ... info[4..5] is not the LOGO magic");
|
||||
CHECK((s_frame[SB_FRAME_B_INFO_OFF + 6U] == 0U) &&
|
||||
(s_frame[SB_FRAME_B_INFO_OFF + 7U] == 0U),
|
||||
"TC-SBLINK-017e ... info[6..7] reserved is zero");
|
||||
for (i = SB_FRAME_B_PAD_OFF; i < SB_FRAME_B_CRC_OFF; i++) {
|
||||
if (s_frame[i] != 0U) {
|
||||
pad_bad++;
|
||||
}
|
||||
}
|
||||
CHECK(pad_bad == 0U, "TC-SBLINK-017f ... all 998 padding bytes are zero");
|
||||
CHECK(sb_frame_b_verify(s_frame, (uint16_t)SB_LINK_FRAME_LEN) == BSP_OK,
|
||||
"TC-SBLINK-017g ... and the CRC covers [0, 4205]");
|
||||
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_build == 1U, "TC-SBLINK-017h ... counted exactly once");
|
||||
CHECK(st.frame_bad == 0U, "TC-SBLINK-017i ... self-check clean");
|
||||
}
|
||||
|
||||
static void tc_frame_unchanged(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x11U);
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
|
||||
/* 同样的内容再来一次:不重组。uiTask 每 10 ms 调一次,这条决定了常态成本只有 memcmp */
|
||||
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
|
||||
"TC-SBLINK-018 an unchanged framebuffer returns OK");
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_build == 1U, "TC-SBLINK-018b ... without rebuilding the frame");
|
||||
|
||||
/* 点阵改一个字节:必须重组 */
|
||||
s_bmp[1234] ^= 0xFFU;
|
||||
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
|
||||
"TC-SBLINK-018c a changed byte is picked up");
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_build == 2U, "TC-SBLINK-018d ... and triggers a rebuild");
|
||||
|
||||
/* 点阵没变、只有掩码变:也要重组(info[0..3] 在 CRC 覆盖区内) */
|
||||
CHECK(sb_link_frame_update(s_bmp, 0x00000001U) == BSP_OK,
|
||||
"TC-SBLINK-018e a changed LED mask is picked up");
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_build == 3U, "TC-SBLINK-018f ... and triggers a rebuild too");
|
||||
CHECK(s_frame[SB_FRAME_B_INFO_OFF] == 0x01U,
|
||||
"TC-SBLINK-018g ... with the new mask in info[0]");
|
||||
}
|
||||
|
||||
static void tc_tx_kick(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x22U);
|
||||
|
||||
/* 还没组帧:召唤来了也不发(发一帧全 0 的图等于让屏板白屏,而且它不报错) */
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_kick == 0U, "TC-SBLINK-019 no frame is sent before the first build");
|
||||
CHECK(st.tx_busy_drop == 1U, "TC-SBLINK-019b ... the summon is counted as dropped instead");
|
||||
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_kick == 1U, "TC-SBLINK-019c a valid summon kicks the transfer");
|
||||
CHECK(sb_tx_calls == 1U, "TC-SBLINK-019d ... through exactly one HAL call");
|
||||
CHECK(sb_tx_len == SB_LINK_FRAME_LEN, "TC-SBLINK-019e ... of 4208 bytes");
|
||||
CHECK(sb_tx_buf == s_frame,
|
||||
"TC-SBLINK-019f ... from the static frame buffer (the DMA-reachable one)");
|
||||
|
||||
/* 屏板会一直召唤:上一帧没发完就再来一次 → 绝不叠发 */
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(sb_tx_calls == 1U,
|
||||
"TC-SBLINK-019g a second summon during TX does not stack another transfer");
|
||||
CHECK(st.tx_busy_drop == 2U, "TC-SBLINK-019h ... it is counted as dropped");
|
||||
|
||||
/* TxCplt 之后又能发了 */
|
||||
tx_complete();
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_done == 1U, "TC-SBLINK-019i TxCplt is counted");
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
CHECK(sb_tx_calls == 2U, "TC-SBLINK-019j ... and the next summon goes out again");
|
||||
}
|
||||
|
||||
static void tc_tx_busy_freezes_frame(void)
|
||||
{
|
||||
uint8_t before[SB_FRAME_B_BMP_LEN];
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x33U);
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
feed(k_rt, SB_FRAME_A_LEN); /* 起发 → busy */
|
||||
|
||||
(void)memcpy(before, s_frame, sizeof(before));
|
||||
|
||||
fill_bmp(0x77U); /* 画面变了,但正在发 */
|
||||
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_ERR_BUSY,
|
||||
"TC-SBLINK-020 a frame update during TX is refused");
|
||||
CHECK(memcmp(s_frame, before, sizeof(before)) == 0,
|
||||
"TC-SBLINK-020b ... the buffer DMA is reading is left byte-for-byte untouched");
|
||||
|
||||
tx_complete();
|
||||
CHECK(sb_link_frame_update(s_bmp, 0U) == BSP_OK,
|
||||
"TC-SBLINK-020c ... and the pending change is picked up right after TxCplt");
|
||||
CHECK(memcmp(s_frame, s_bmp, sizeof(before)) == 0,
|
||||
"TC-SBLINK-020d ... so no stale frame survives the busy window");
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_build == 2U, "TC-SBLINK-020e ... exactly one rebuild, no double build");
|
||||
}
|
||||
|
||||
static void tc_tx_kick_failure(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x44U);
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
|
||||
sb_tx_force = SB_FAKE_TX_BUSY;
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_kick_fail == 1U, "TC-SBLINK-021 a failed HAL kick is counted");
|
||||
CHECK(st.tx_kick == 0U, "TC-SBLINK-021b ... and is not counted as a transfer");
|
||||
|
||||
/* 失败必须把 busy 清掉,否则此后每次召唤都被判"上一帧还在发",链路再也不发 */
|
||||
sb_tx_force = SB_FAKE_TX_OK;
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_kick == 1U, "TC-SBLINK-021c ... and the next summon retries successfully");
|
||||
}
|
||||
|
||||
static void tc_tx_abort_on_error(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x55U);
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
feed(k_rt, SB_FRAME_A_LEN); /* 起发,gState = BUSY_TX */
|
||||
|
||||
/* 真机:错误中断打断 TX 时 HAL 把 gState 打回 READY,但**不会**再调 TxCplt →
|
||||
* busy 会永久挂着。这正是"拔掉屏板后无残留 busy"要防的那条。 */
|
||||
sb_ore_latched = 1U;
|
||||
s_uart.RxState = HAL_UART_STATE_READY;
|
||||
s_uart.gState = HAL_UART_STATE_READY;
|
||||
s_uart.ErrorCode = HAL_UART_ERROR_ORE;
|
||||
sb_link_on_error(&s_uart);
|
||||
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_abort == 1U, "TC-SBLINK-022 a stuck TX busy is cleared by the error callback");
|
||||
|
||||
s_uart.gState = HAL_UART_STATE_READY;
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
CHECK(sb_tx_calls == 2U, "TC-SBLINK-022b ... so the link keeps transmitting afterwards");
|
||||
|
||||
/* 反例:TX 真的还在发(gState 仍是 BUSY_TX)时不得判死 */
|
||||
sb_ore_latched = 0U;
|
||||
s_uart.RxState = HAL_UART_STATE_BUSY_RX; /* 非阻塞型错误:接收照跑 */
|
||||
s_uart.ErrorCode = HAL_UART_ERROR_NE;
|
||||
sb_link_on_error(&s_uart); /* gState 仍 BUSY_TX */
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.tx_abort == 1U, "TC-SBLINK-022c ... but a still-running TX is not declared dead");
|
||||
CHECK(sb_tx_calls == 2U, "TC-SBLINK-022d ... and no extra transfer was started");
|
||||
}
|
||||
|
||||
static void tc_key_queue(void)
|
||||
{
|
||||
uint8_t i;
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
|
||||
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023 an empty queue yields 0");
|
||||
|
||||
feed(k_key, SB_FRAME_A_LEN); /* 屏板实测向量:key = 9 */
|
||||
CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023b the key byte reaches the consumer");
|
||||
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023c ... exactly once");
|
||||
|
||||
/* 周期召唤的 key 恒 0:不占队列(屏板自己的语义就是"没有按键事件") */
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023d a periodic summon never queues a key");
|
||||
|
||||
/* 队列满:丢**新**的并计数(旧键已经在路上,新键还会有下一次召唤)。
|
||||
* 环形队列留一格判满 → 容量 = SB_LINK_KEY_QUEUE_LEN - 1 = 3,推 6 次丢 3 次。 */
|
||||
for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN + 2U); i++) {
|
||||
feed(k_key, SB_FRAME_A_LEN);
|
||||
}
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.key_drop == 3U, "TC-SBLINK-023e overflow drops the newest and counts it");
|
||||
|
||||
for (i = 0U; i < (uint8_t)(SB_LINK_KEY_QUEUE_LEN - 1U); i++) {
|
||||
CHECK(sb_link_key_take() == 9U, "TC-SBLINK-023f ... FIFO order is preserved");
|
||||
}
|
||||
CHECK(sb_link_key_take() == 0U, "TC-SBLINK-023g ... and the queue is empty again");
|
||||
}
|
||||
|
||||
static void tc_offline(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
|
||||
sb_link_poll(0U);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.offline == 0U, "TC-SBLINK-024 a fresh link is online");
|
||||
|
||||
/* 9.999 s 无召唤:还不算离线(屏板自复位约 1 s 空窗,按 500 ms 判会反复误报) */
|
||||
sb_link_poll(SB_LINK_OFFLINE_MS - 1U);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.offline == 0U, "TC-SBLINK-024b just under 10 s of silence is still online");
|
||||
|
||||
/* 满 10 s:置离线,并且只打一行 */
|
||||
sb_log_reset();
|
||||
sb_link_poll(SB_LINK_OFFLINE_MS);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.offline == 1U, "TC-SBLINK-024c 10 s of silence marks the board offline");
|
||||
CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-024d ... with one dedicated line");
|
||||
|
||||
/* 再久也不重复刷屏 */
|
||||
sb_log_reset();
|
||||
sb_link_poll(SB_LINK_OFFLINE_MS + 1000U);
|
||||
CHECK(sb_log_n == 0U, "TC-SBLINK-024e the offline line is not repeated every poll");
|
||||
|
||||
/* 屏板回来:一帧就够,立刻清标志并报恢复 */
|
||||
sb_log_reset();
|
||||
feed(k_rt, SB_FRAME_A_LEN);
|
||||
sb_link_poll(SB_LINK_OFFLINE_MS + 1010U);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.offline == 0U, "TC-SBLINK-024f a single valid summon brings it back online");
|
||||
CHECK(sb_log_has(0U, "back online") == 1, "TC-SBLINK-024g ... and says so");
|
||||
}
|
||||
|
||||
static void tc_frame_timing(void)
|
||||
{
|
||||
sb_link_stats_t st;
|
||||
|
||||
(void)setup_link();
|
||||
fill_bmp(0x66U);
|
||||
|
||||
/* 每次读 CYCCNT 前进 16800 周期 = 100 µs @168 MHz;组帧读两次 → 差值正好 16800。
|
||||
* 这条钉的是"周期 → µs"的除数(SystemCoreClock/1e6),换算错了会在台架上
|
||||
* 把 0.17 ms 报成 17 ms 或 1.7 µs,两种都会被当成"组帧太慢/没测到"。 */
|
||||
sb_cyccnt_step = 16800U;
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_us == 100U, "TC-SBLINK-025 the cycle count is converted with SystemCoreClock");
|
||||
CHECK(st.frame_us_max == 100U, "TC-SBLINK-025b ... and the peak is tracked");
|
||||
|
||||
s_bmp[0] ^= 0xFFU;
|
||||
sb_cyccnt_step = 50400U; /* 300 µs */
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
sb_link_get_stats(&st);
|
||||
CHECK(st.frame_us == 300U, "TC-SBLINK-025c ... the last build duration");
|
||||
CHECK(st.frame_us_max == 300U, "TC-SBLINK-025d ... and the peak follows the slower build");
|
||||
}
|
||||
|
||||
static void tc_tx_report(void)
|
||||
{
|
||||
(void)setup_link();
|
||||
fill_bmp(0x88U);
|
||||
(void)sb_link_frame_update(s_bmp, 0U);
|
||||
|
||||
sb_log_reset();
|
||||
sb_link_poll(SB_LINK_REPORT_MS);
|
||||
CHECK(sb_log_n == 1U, "TC-SBLINK-026 a changed frame counter produces a report");
|
||||
CHECK(sb_log_has(0U, "fb=1") == 1, "TC-SBLINK-026b ... carrying the build count");
|
||||
CHECK(sb_log_has(0U, "tx=0") == 1, "TC-SBLINK-026c ... and the transfer counters");
|
||||
|
||||
/* 这一拍会先出一行 OFFLINE(10 s 无有效召唤,2-5),但不该再出 TX 行。
|
||||
* 断言行数而不是"没看见 tx=",是为了把"离线判定排在周期上报之前"这个顺序也钉住。 */
|
||||
sb_log_reset();
|
||||
sb_link_poll(2U * SB_LINK_REPORT_MS);
|
||||
CHECK(sb_log_n == 1U, "TC-SBLINK-026d only the offline line is emitted, no TX report");
|
||||
CHECK(sb_log_has(0U, "OFFLINE") == 1, "TC-SBLINK-026e ... and it is the offline one");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== ScreenLink Link Tests ===\n");
|
||||
@@ -476,6 +841,29 @@ int main(void)
|
||||
printf("-- stats snapshot --\n");
|
||||
tc_stats_snapshot();
|
||||
|
||||
printf("-- batch 2: frame update guards --\n");
|
||||
tc_frame_update_guards();
|
||||
printf("-- batch 2: frame B content --\n");
|
||||
tc_frame_build_content();
|
||||
printf("-- batch 2: change detection --\n");
|
||||
tc_frame_unchanged();
|
||||
printf("-- batch 2: TX kick from the RX callback --\n");
|
||||
tc_tx_kick();
|
||||
printf("-- batch 2: frame frozen while TX runs --\n");
|
||||
tc_tx_busy_freezes_frame();
|
||||
printf("-- batch 2: TX kick failure --\n");
|
||||
tc_tx_kick_failure();
|
||||
printf("-- batch 2: TX aborted by an error interrupt --\n");
|
||||
tc_tx_abort_on_error();
|
||||
printf("-- batch 2: key queue --\n");
|
||||
tc_key_queue();
|
||||
printf("-- batch 2: offline detection --\n");
|
||||
tc_offline();
|
||||
printf("-- batch 2: frame build timing --\n");
|
||||
tc_frame_timing();
|
||||
printf("-- batch 2: TX report line --\n");
|
||||
tc_tx_report();
|
||||
|
||||
printf("\n=== ScreenLink Link Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
|
||||
@@ -35,6 +35,7 @@ typedef enum {
|
||||
/* RxState 的两个取值:错误回调里"要不要重挂"就靠它判(见 sb_link.c 的 on_error 说明) */
|
||||
#define HAL_UART_STATE_READY (0x20U)
|
||||
#define HAL_UART_STATE_BUSY_RX (0x22U)
|
||||
#define HAL_UART_STATE_BUSY_TX (0x24U)
|
||||
|
||||
/* HAL 的错误位(ErrorCode):ORE 是阻塞型,NE/FE/PE 是非阻塞型 */
|
||||
#define HAL_UART_ERROR_PE (0x01U)
|
||||
@@ -146,7 +147,70 @@ static HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *p_huart
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ===== ④ 测试用的复位/查询把手 ===== */
|
||||
/* ===== ④ 假 Transmit_DMA(批 2:起发在中断里,收尾在 TxCplt) ===== */
|
||||
#define SB_FAKE_TX_OK (0U)
|
||||
#define SB_FAKE_TX_BUSY (1U)
|
||||
#define SB_FAKE_TX_ERROR (2U)
|
||||
|
||||
static uint32_t sb_tx_calls;
|
||||
static uint32_t sb_tx_force; /* SB_FAKE_TX_* */
|
||||
static const uint8_t *sb_tx_buf;
|
||||
static uint16_t sb_tx_len;
|
||||
|
||||
static HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *p_huart,
|
||||
const uint8_t *p_buf,
|
||||
uint16_t size)
|
||||
{
|
||||
sb_tx_calls++;
|
||||
sb_tx_buf = p_buf;
|
||||
sb_tx_len = size;
|
||||
|
||||
if (sb_tx_force == SB_FAKE_TX_BUSY) {
|
||||
return HAL_BUSY;
|
||||
}
|
||||
if (sb_tx_force == SB_FAKE_TX_ERROR) {
|
||||
return HAL_ERROR;
|
||||
}
|
||||
/* 真机成功起发后 gState = BUSY_TX,TxCplt 才打回 READY。
|
||||
* 错误回调里"TX 是不是还在发"就靠这一位判(防残留 busy)。 */
|
||||
p_huart->gState = HAL_UART_STATE_BUSY_TX;
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/* ===== ⑤ 假 DWT / CoreDebug / SystemCoreClock(组帧耗时统计用) ===== */
|
||||
/* 真机由 core_cm4.h 提供。宿主侧要能"推进"计数器,所以 CYCCNT 也做成函数指针
|
||||
* (与 SR / DR 同一手法):每读一次前进 sb_cyccnt_step,于是 sb_link 里那两次
|
||||
* 读之间的差值 = 一个 step,测试就能钉住 "周期 → µs" 的除数。 */
|
||||
typedef struct {
|
||||
volatile uint32_t CTRL;
|
||||
uint32_t (*cyccnt_cell)(void);
|
||||
} DWT_TypeDef;
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t DEMCR;
|
||||
} CoreDebug_TypeDef;
|
||||
|
||||
#define CYCCNT cyccnt_cell()
|
||||
|
||||
static uint32_t sb_cyccnt_val;
|
||||
static uint32_t sb_cyccnt_step; /* 每次读前进多少周期(0 = 恒 0,默认) */
|
||||
|
||||
static uint32_t sb_cyccnt_cell(void)
|
||||
{
|
||||
sb_cyccnt_val += sb_cyccnt_step;
|
||||
return sb_cyccnt_val;
|
||||
}
|
||||
|
||||
static DWT_TypeDef g_sb_dwt = { 0U, sb_cyccnt_cell };
|
||||
static CoreDebug_TypeDef g_sb_coredebug;
|
||||
static uint32_t SystemCoreClock = 168000000U;
|
||||
|
||||
#define DWT (&g_sb_dwt)
|
||||
#define CoreDebug (&g_sb_coredebug)
|
||||
#define CoreDebug_DEMCR_TRCENA_Msk (0x01000000U)
|
||||
#define DWT_CTRL_CYCCNTENA_Msk (0x00000001U)
|
||||
|
||||
/* ===== ⑥ 测试用的复位/查询把手 ===== */
|
||||
static void sb_fake_reset(void)
|
||||
{
|
||||
uint32_t i;
|
||||
@@ -161,6 +225,15 @@ static void sb_fake_reset(void)
|
||||
sb_arm_force = SB_FAKE_ARM_OK;
|
||||
sb_arm_buf = NULL;
|
||||
sb_arm_size = 0U;
|
||||
|
||||
sb_tx_calls = 0U;
|
||||
sb_tx_force = SB_FAKE_TX_OK;
|
||||
sb_tx_buf = NULL;
|
||||
sb_tx_len = 0U;
|
||||
|
||||
g_sb_dwt.CTRL = 0U;
|
||||
sb_cyccnt_val = 0U;
|
||||
sb_cyccnt_step = 0U;
|
||||
}
|
||||
|
||||
/* 寄存器读序列是否为期望的字符串(如 "SD"):长度不等或内容不符都返回 0 */
|
||||
|
||||
+170
-16
@@ -3,15 +3,22 @@
|
||||
"""屏板链路台架激励:用 PC + USB 转 TTL **顶替屏板**,按屏板的召唤节拍发 6 B 帧 A。
|
||||
|
||||
做什么:
|
||||
在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤;
|
||||
可穿插按键帧(屏板有键时最早 205 ms 就会再来一帧,本脚本只在周期点替换内容,
|
||||
不额外插帧——批 1 只验"收得到、长度对、CRC 过",不验应答时序)。
|
||||
主机侧的统计由固件自己打(`[SBLINK] rx=... ok=... crc=... len=...`),
|
||||
本脚本只负责"有东西可收",不读被测板——**收侧观测面是串口日志,不是本脚本**。
|
||||
1. **发**:在一个 TTL 串口上按屏板的节拍(默认 280 ms)发帧 A,模拟屏板的周期召唤;
|
||||
可穿插按键帧(`--key N`)。
|
||||
2. **收**(`--reply`,批 2 起):每发一帧就收主机回的 4208 B 帧 B,校验长度 / CRC /
|
||||
998 B 填充,量"召唤末位 → 应答首位"的间隔与整帧耗时,可选把点阵 dump 成 PBM。
|
||||
主机侧的统计由固件自己打(`[SBLINK] rx=... tx=...`),**收侧观测面是串口日志**;
|
||||
本脚本的 `--reply` 是另一路观测(应答本身对不对),两者互补,不能互相替代。
|
||||
|
||||
为什么不用串口助手 GUI:
|
||||
1-3 的判据含"连续 10 min 无丢帧",手点是做不到的;而且帧内容必须逐字节对
|
||||
(CRC 低字节在前,写错一位就是 100 % CRC 错帧,与"波特率错"同形)。
|
||||
2-2 的判据含"整帧 59.6 ms ±0.5 ms、≤200 ms 内结束",更是只能靠脚本量。
|
||||
|
||||
⚠ **USB 转 TTL 适配器量不到"帧内无间隙"**:USB 帧是 1 ms 量级,适配器把字节成批交上来,
|
||||
所以 `--reply` 报的 `span` 只能证明"整帧在 60 ms 量级内发完",不能证明"没有 ≥28 µs
|
||||
的间隙"(那是屏板断帧的硬约束,只能上逻辑分析仪或接真屏板时看)。别把 span 通过
|
||||
当成那条判据通过。
|
||||
|
||||
接法(docs/任务规划/屏板链路.md §2 决策一):
|
||||
适配器 TXD -> U5.B10(网络 ST_RX0,PA10) 适配器 RXD -> U5.A10(网络 ST_TX0,PA9)
|
||||
@@ -26,11 +33,15 @@
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 30
|
||||
& '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --seconds 600 # 10 min 判据
|
||||
& '.venv\\Scripts\\python.exe' test/host/screenlink_bench.py --port COM6 --reply --dump lcd.pbm
|
||||
--key N 每 N 个周期把召唤帧换成按键帧(键值在 1/2/3/4/7/8/9 里轮换;0 = 不发按键帧)
|
||||
--reply 每帧收应答并校验(批 2 的 2-2 / 2-4)
|
||||
--dump P 把最后一帧应答的点阵存成 PBM(P4,160x160,可直接看)
|
||||
|
||||
退出码:0 = 按时发完;1 = 串口打不开 / 发送失败;2 = 用法错误。
|
||||
退出码:0 = 按时发完且(`--reply` 时)所有应答都对;1 = 串口打不开 / 发送失败;
|
||||
2 = 用法错误;3 = 有应答不合格。
|
||||
|
||||
设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、§5 台架联调步骤第 2 步。
|
||||
设计依据:docs/任务规划/屏板链路.md §3 批 1 的 1-3 判据、批 2 的 2-2/2-4、§5 台架联调步骤第 2、3 步。
|
||||
创建日期:2026-10-29
|
||||
"""
|
||||
import argparse
|
||||
@@ -72,6 +83,76 @@ def build_frame(cmd, key=0):
|
||||
return body + bytes((crc & 0xFF, (crc >> 8) & 0xFF))
|
||||
|
||||
|
||||
# ---- 帧 B(主机 -> 屏板,4208 B):批 2 的应答校验 ----
|
||||
FRAME_B_LEN = 4208
|
||||
FRAME_B_BMP = 3200
|
||||
FRAME_B_INFO = 3200
|
||||
FRAME_B_PAD_OFF = 3208
|
||||
FRAME_B_CRC_OFF = 4206
|
||||
SCREEN_W = 160
|
||||
SCREEN_H = 160
|
||||
|
||||
|
||||
def verify_frame_b(data):
|
||||
"""校验一帧帧 B;返回 (是否合格, 说明)。
|
||||
|
||||
只验"结构对不对":长度、CRC(覆盖 [0, 4205],低字节在前)、998 B 填充全 0、
|
||||
info[4..5] 不是 LOGO 魔数。**不验点阵内容**——那要么 dump 出来看(--dump),
|
||||
要么拿主机侧显存逐字节比(需要主机侧配合,台架这一级做不到)。
|
||||
"""
|
||||
if len(data) != FRAME_B_LEN:
|
||||
return False, "len=%d(应 %d)" % (len(data), FRAME_B_LEN)
|
||||
|
||||
crc = crc16_modbus(data[:FRAME_B_CRC_OFF])
|
||||
if (data[FRAME_B_CRC_OFF] != (crc & 0xFF)) or (data[FRAME_B_CRC_OFF + 1] != ((crc >> 8) & 0xFF)):
|
||||
return False, "CRC 不符(帧尾 %02X %02X,算得 %04X 低字节在前)" % (
|
||||
data[FRAME_B_CRC_OFF], data[FRAME_B_CRC_OFF + 1], crc)
|
||||
|
||||
pad = data[FRAME_B_PAD_OFF:FRAME_B_CRC_OFF]
|
||||
if any(pad):
|
||||
bad = next(i for i, b in enumerate(pad) if b)
|
||||
return False, "填充段非 0(偏移 %d)" % (FRAME_B_PAD_OFF + bad)
|
||||
|
||||
if (data[FRAME_B_INFO + 4] == 0x89) and (data[FRAME_B_INFO + 5] == 0x45):
|
||||
return False, "info[4..5] 是 LOGO 魔数(本应是普通图帧)"
|
||||
|
||||
return True, "OK"
|
||||
|
||||
|
||||
def dump_pbm(path, data):
|
||||
"""把点阵段存成 PBM(P4,160x160)。显存本身就是 1 bpp / 20 B 一行 / MSB 在前,
|
||||
与 PBM 的二进制布局逐字节同构,所以直接拼头 + 原字节即可。"""
|
||||
with open(path, "wb") as fp:
|
||||
fp.write(b"P4\n%d %d\n" % (SCREEN_W, SCREEN_H))
|
||||
fp.write(data[:FRAME_B_BMP])
|
||||
|
||||
|
||||
def capture_reply(ser, frame, budget_s=0.6):
|
||||
"""发一帧召唤,收主机回的 4208 B;返回 (数据, 写时刻, 首字节时刻, 末字节时刻)。
|
||||
|
||||
⚠ **CH340 收不了 700 kbaud 的连续流**(实测 2026-10-29:4208 B 只收到 ~2373 B,
|
||||
等效约 9.5 kB/s)。这不是固件问题——同一次运行里固件侧 `tx=94 done=93` 说明 4208 B
|
||||
每次都发完了;CH340 的 RX FIFO 只有几十字节、USB 每 1 ms 才搬一次,70 kB/s 必然溢出。
|
||||
要真正核对帧 B 的内容,换 FTDI / CP210x 之类的适配器,或等批 4 接真屏板。
|
||||
"""
|
||||
ser.reset_input_buffer()
|
||||
t_write = time.monotonic()
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
|
||||
buf = bytearray()
|
||||
t_first = None
|
||||
deadline = t_write + budget_s
|
||||
while len(buf) < FRAME_B_LEN and time.monotonic() < deadline:
|
||||
# 大块读:read(n) 会等到收满 n 或超时,比"按 in_waiting 一个字节一个字节读"快得多
|
||||
chunk = ser.read(min(1024, FRAME_B_LEN - len(buf)))
|
||||
if chunk:
|
||||
if t_first is None:
|
||||
t_first = time.monotonic()
|
||||
buf += chunk
|
||||
return bytes(buf), t_write, t_first, time.monotonic()
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="屏板链路台架激励(PC 顶替屏板)")
|
||||
ap.add_argument("--port", required=True, help="USB 转 TTL 适配器的串口,如 COM6")
|
||||
@@ -81,16 +162,27 @@ def main():
|
||||
help="召唤周期(ms),默认 280(屏板是 RT_count>55 -> 第 56 拍 x 5 ms)")
|
||||
ap.add_argument("--key", type=int, default=0,
|
||||
help="每 N 个周期把帧换成按键帧(0 = 只发周期召唤)")
|
||||
ap.add_argument("--key-fixed", type=int, default=0,
|
||||
help="固定发这个键值(1/2/3/4/7/8/9),与 --key 二选一。"
|
||||
"用来「持续改画面」以便拿到多次组帧样本;2=UP / 3=DOWN 只移动高亮,"
|
||||
"不会确认任何设置(1=ENTER 可能改配置,别用来做长跑)")
|
||||
ap.add_argument("--glitch-baud", type=int, default=0,
|
||||
help="先按这个(错的)波特率灌一串垃圾再正常发,用来验 ORE/NE/FE 之后能否自愈"
|
||||
"(0 = 不灌;建议 115200)")
|
||||
ap.add_argument("--glitch-bytes", type=int, default=32,
|
||||
help="灌多少字节垃圾(默认 32)")
|
||||
ap.add_argument("--reply", action="store_true",
|
||||
help="每帧收主机回的 4208 B 应答并校验(批 2 的 2-2 / 2-4)")
|
||||
ap.add_argument("--dump", default="",
|
||||
help="把最后一帧应答的点阵存成 PBM(P4 160x160,可直接看)")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.period_ms <= 0 or args.seconds <= 0:
|
||||
print("[ERR] --period-ms / --seconds 必须为正")
|
||||
return 2
|
||||
if args.dump and not args.reply:
|
||||
print("[ERR] --dump 需要同时给 --reply(没有应答就没有点阵可存)")
|
||||
return 2
|
||||
|
||||
rt_frame = build_frame(CMD_RT)
|
||||
print("RT frame : %s" % rt_frame.hex(" ").upper())
|
||||
@@ -112,13 +204,24 @@ def main():
|
||||
print("[WARN] 噪声注入失败(继续正常发):%s" % exc)
|
||||
|
||||
try:
|
||||
ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1, timeout=0)
|
||||
# 收应答时要给 read() 一个超时,否则 timeout=0 会让 read(n) 立刻返回空、空转
|
||||
ser = serial.Serial(args.port, args.baud, bytesize=8, parity="N", stopbits=1,
|
||||
timeout=(0.1 if args.reply else 0))
|
||||
except Exception as exc: # noqa: BLE001 - 打开失败原因要原样给人看
|
||||
print("[ERR] 打开 %s @%d 失败:%s" % (args.port, args.baud, exc))
|
||||
return 1
|
||||
|
||||
sent = 0
|
||||
keys = 0
|
||||
reply_ok = 0
|
||||
reply_bad = 0
|
||||
lat_min = None
|
||||
lat_max = 0.0
|
||||
lat_sum = 0.0
|
||||
span_min = None
|
||||
span_max = 0.0
|
||||
span_sum = 0.0
|
||||
last_bmp = None
|
||||
t0 = time.monotonic()
|
||||
next_at = t0
|
||||
try:
|
||||
@@ -130,7 +233,11 @@ def main():
|
||||
time.sleep(min(next_at - now, 0.02))
|
||||
continue
|
||||
|
||||
if args.key > 0 and (sent % args.key) == (args.key - 1):
|
||||
if args.key_fixed > 0:
|
||||
frame = build_frame(CMD_KEY, args.key_fixed)
|
||||
keys += 1
|
||||
tag = "KEY %d" % args.key_fixed
|
||||
elif args.key > 0 and (sent % args.key) == (args.key - 1):
|
||||
key = KEY_VALUES[keys % len(KEY_VALUES)]
|
||||
frame = build_frame(CMD_KEY, key)
|
||||
keys += 1
|
||||
@@ -139,12 +246,40 @@ def main():
|
||||
frame = rt_frame
|
||||
tag = "RT"
|
||||
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
sent += 1
|
||||
if sent <= 3 or tag != "RT":
|
||||
print(" #%d %s -> %s" % (sent, tag, frame.hex(" ").upper()))
|
||||
if args.reply:
|
||||
data, t_write, t_first, t_last = capture_reply(ser, frame)
|
||||
good, why = verify_frame_b(data)
|
||||
if t_first is None:
|
||||
lat = span = None
|
||||
else:
|
||||
lat = (t_first - t_write) * 1000.0
|
||||
span = (t_last - t_first) * 1000.0
|
||||
lat_sum += lat
|
||||
lat_min = lat if lat_min is None else min(lat_min, lat)
|
||||
lat_max = max(lat_max, lat)
|
||||
span_sum += span
|
||||
span_min = span if span_min is None else min(span_min, span)
|
||||
span_max = max(span_max, span)
|
||||
if good:
|
||||
reply_ok += 1
|
||||
last_bmp = data
|
||||
else:
|
||||
reply_bad += 1
|
||||
|
||||
if sent < 3 or tag != "RT" or not good:
|
||||
if t_first is None:
|
||||
print(" #%d %s -> 无应答(%.0f ms 内没收到首字节)" % (sent + 1, tag, 600.0))
|
||||
else:
|
||||
print(" #%d %s -> %d B %s lat=%.2f ms span=%.2f ms"
|
||||
% (sent + 1, tag, len(data), ("OK" if good else "BAD: " + why),
|
||||
lat, span))
|
||||
else:
|
||||
ser.write(frame)
|
||||
ser.flush()
|
||||
if sent <= 3 or tag != "RT":
|
||||
print(" #%d %s -> %s" % (sent + 1, tag, frame.hex(" ").upper()))
|
||||
|
||||
sent += 1
|
||||
next_at += args.period_ms / 1000.0
|
||||
# 落后太多就别追了(PC 侧卡顿不该变成突发流量)
|
||||
if next_at < time.monotonic() - 1.0:
|
||||
@@ -157,8 +292,27 @@ def main():
|
||||
elapsed = time.monotonic() - t0
|
||||
print("[DONE] 发送 %d 帧(其中按键帧 %d)用时 %.1f s,平均周期 %.1f ms"
|
||||
% (sent, keys, elapsed, (elapsed * 1000.0 / sent) if sent else 0.0))
|
||||
print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0" % sent)
|
||||
return 0
|
||||
if args.reply:
|
||||
print("[REPLY] 合格 %d / 不合格 %d" % (reply_ok, reply_bad))
|
||||
if reply_bad > 0 and reply_ok == 0:
|
||||
print("[HINT] 一个都没收全:先怀疑适配器。CH340 的 RX 顶不住 700 kbaud 连续流"
|
||||
"(4208 B 只到 ~2400 B 就是它的典型表现),换 FTDI / CP210x 再试。")
|
||||
print("[HINT] 固件侧是否真发了:看日志口 grep '[SBLINK]' 的 tx / done / fb 三列。")
|
||||
if reply_ok > 0:
|
||||
print("[REPLY] 召唤末位→应答首位:min %.2f / avg %.2f / max %.2f ms"
|
||||
"(判据 30 µs ~ 5 ms;USB 适配器自身有 1~2 ms 量级延迟,只能证上界)"
|
||||
% (lat_min, lat_sum / reply_ok, lat_max))
|
||||
print("[REPLY] 整帧耗时(首字节→末字节):min %.2f / avg %.2f / max %.2f ms"
|
||||
"(线上 59.6 ms;**不能**据此判「帧内无间隙」,见文件头)"
|
||||
% (span_min, span_sum / reply_ok, span_max))
|
||||
print("[REPLY] 整帧 ≤200 ms 判据:%s" % ("PASS" if lat_max + span_max < 200.0 else "FAIL"))
|
||||
if args.dump and last_bmp is not None:
|
||||
dump_pbm(args.dump, last_bmp)
|
||||
print("[DUMP] 点阵已存 %s(P4 160x160,可直接打开看)" % args.dump)
|
||||
|
||||
print("[NEXT] 回看日志口:grep '[SBLINK]' —— 期望 rx == %d、ok == rx、crc=0、len=0%s"
|
||||
% (sent, "、tx == ok、done == tx、drop=0、fail=0、abort=0" if args.reply else ""))
|
||||
return 3 if reply_bad > 0 else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user