文章目录 展开 / 收起
这篇长文主要是给站点做压力测试用的:标题够多、有代码、有表格、有引用,用来检查目录折叠、目录跟随高亮、阅读时长统计和相关文章是否正常。内容本身也确实是这两个月在 hms_zep 上踩过的坑,顺手整理成清单。
为什么单独写一篇长文
短文章测不出目录的问题。目录组件通常只在标题数量超过一屏时才暴露毛病:折叠高度不对、点击跳转偏移、滚动高亮不跟手、PJAX 切换后监听器重复注册。这些都需要一篇足够长的文章才能复现。
所以这篇的目标很明确:
- 一级标题不少于 8 个,二级标题若干;
- 至少包含 3 段代码、1 个表格、1 张图片、1 段引用;
- 正文长度超过 2000 字,阅读时长落在 5 分钟以上。
如果你的站点打开这篇时目录能正常展开、滚动时高亮会跟着走、文末能推荐出其他文章,那这套文章模板基本就成型了。
线程、优先级与栈
Zephyr 里最先要建立的直觉是:优先级数字越小越优先,而且协作式线程(负数优先级)一旦不让出 CPU,同优先级以下的线程全部饿死。应用层一般只用 K_PRIO_PREEMPT(x),把 x 控制在 0–15 之间。
栈大小怎么估
栈不是越大越好,RAM 只有 128 KB 的时候,几个 4 KB 的栈就能把内存吃光。经验值:
| 线程类型 | 常见栈大小 | 备注 |
|---|---|---|
| 纯状态机/轮询 | 1 KB | 不调用 printf |
| 带日志的 worker | 2 KB | 日志走 deferred 模式 |
| 协议解析 | 2–3 KB | 局部数组要算进去 |
| shell | 3 KB | Zephyr 默认就要这么多 |
更靠谱的办法是打开 CONFIG_THREAD_ANALYZER 或 CONFIG_INIT_STACKS,跑一段时间后看栈水位,再留 20% 余量。
K_THREAD_STACK_DEFINE(proto_stack, 2048);
static struct k_thread proto_thread;
k_thread_create(&proto_thread, proto_stack,
K_THREAD_STACK_SIZEOF(proto_stack),
proto_entry, NULL, NULL, NULL,
K_PRIO_PREEMPT(8), 0, K_NO_WAIT);
k_thread_name_set(&proto_thread, "proto");别在中断里做重活
中断上下文不能睡眠,也不能拿互斥锁。只要看到 k_mutex_lock(..., K_FOREVER) 出现在 ISR 调用链里,就一定会出事。正确的做法是中断里只做三件事:清标志、把数据塞进队列、唤醒线程。
static void uart_isr(const struct device *dev, void *user_data)
{
uint8_t chunk[32];
int n = uart_fifo_read(dev, chunk, sizeof(chunk));
for (int i = 0; i < n; i++) {
k_msgq_put(&rx_q, &chunk[i], K_NO_WAIT);
}
}内存:静态分配优先
嵌入式项目里最怕的不是内存不够,而是内存碎片。k_malloc 用多了,跑几天之后就会出现「明明还有 20 KB,却申请不到 1 KB」的情况。所以除了启动阶段,运行期一律静态分配:
- 队列用
K_MSGQ_DEFINE; - 线程栈用
K_THREAD_STACK_DEFINE; - 缓冲区用
static uint8_t buf[N]; - 需要池化就用
k_mem_slab。
如果确实需要动态内存,至少把 CONFIG_HEAP_MEM_POOL_SIZE 显式写出来,别依赖默认值。
日志:级别与后端
日志是最容易被忽视的性能杀手。默认 CONFIG_LOG_MODE_DEFERRED=y 时,LOG_INF 只是把指针塞进队列,开销很小;但一旦改成 immediate 模式,每条日志都会在调用者上下文同步写串口,一条 100 字节的日志可能阻塞几毫秒。
判断日志是否拖慢系统,有个简单办法:把某条高频日志的级别从 INF 提到 DBG,看 CPU 占用有没有明显变化。如果没有变化,说明瓶颈不在这里;如果变化很大,就要考虑限流。
LOG_MODULE_REGISTER(hms_usb, CONFIG_LOG_DEFAULT_LEVEL);
/* 高频路径:只在状态变化时打日志 */
if (new_state != old_state) {
LOG_INF("usb state: %d -> %d", old_state, new_state);
}另外,第三方模块的日志级别要单独压。CONFIG_USBD_CDC_ACM_LOG_LEVEL_WRN=y 这一行就消灭了控制台每 20 ms 一次的 hexdump 刷屏。
zbus:轻量消息总线
zbus 解决的是「模块之间不想互相 include」的问题。它的模型很简单:通道持有最后一条消息,观察者订阅通道。发布者只管往通道里放,谁关心谁自己取。
四类观察者
- listener:在发布者上下文同步执行,必须极短,当 ISR 写;
- async listener:丢到 workqueue 执行,适合稍微重一点的回调;
- subscriber:自己的线程 + msgq,只收到「通道变了」的通知;
- message subscriber:线程 + FIFO,收到消息副本,需要
net_buf池。
几个容易踩的点
- 通道和观察者是链接段里的全局符号,
ZBUS_CHAN_DEFINE必须写在文件作用域,不能放类里; - subscriber 只通知、不排队,连续发布两次只会读到最新值,要保留中间值就用 message subscriber;
- 发布时通道是加锁的,listener 里再去读同一个通道并用
K_FOREVER,会直接死锁; - ISR 里发布只能用
K_NO_WAIT。
ZBUS_CHAN_DEFINE(hms_evt, hms::bus::Event, nullptr, nullptr,
ZBUS_OBSERVERS(hms_evt_mon), ZBUS_MSG_INIT(0, 0));
static void on_evt(const struct zbus_channel *chan)
{
const auto *ev = static_cast<const hms::bus::Event *>(zbus_chan_const_msg(chan));
/* listener 里只做拷贝和计数,绝不阻塞 */
record(ev->code, ev->arg);
}
ZBUS_LISTENER_DEFINE(hms_evt_mon, on_evt);USB CDC:字节流不是字符流
USB CDC ACM 在上面看是一个串口,但它本质是字节流 + 批量端点。几个实际遇到过的问题:
uart_tx()在没开CONFIG_UART_ASYNC_API时返回-ENOTSUP,因为在uart_internal.h里它只有在 async API 打开时才转发到驱动的tx;- CDC ACM 驱动没有实现
.tx,所以发送只能逐字节uart_poll_out(); - 主机没打开串口时,TX ring 满了会丢数据,而且只打一次
WRN_ONCE,很容易被忽略; - 没有 VBUS 检测时,
usbd_enable()在无主机的情况下会反复收到 suspend 事件。
自定义协议建议自己做分帧(比如 COBS + CRC16),不要指望 USB 帮你保留消息边界。
看门狗:喂狗的位置决定它能抓什么
最原始的喂狗方式是开个定时器,每 2 秒喂一次。这种做法只能抓「CPU 完全跑飞」,抓不到「线程卡死」——因为定时器在中断里照样跑。
更有效的做法是把喂狗放进一个监督线程:
- 每个关键模块定期调用
beat(module),只写一个原子时间戳; - 监督线程按固定周期检查时间戳,超过窗口就标记陈旧;
- 只有所有必需模块都不陈旧时才
wdt_feed(); - 必需模块由系统启动时按实际初始化结果设置,避免外设故障变成复位循环。
参数怎么定
- 检查周期:看门狗超时的 1/16 左右;
- 陈旧窗口:4 倍检查周期;
- 超时时间:要大于最坏情况下的合法阻塞时间。
以 4 秒超时为例,250 ms 检查、1000 ms 窗口,基本能覆盖一次完整的 USB 枚举 + 日志刷盘。
性能与体积
改完一轮功能,记得回头看两个数字:FLASH 和 RAM。一个朴素的办法是每次构建都记录 arm-zephyr-eabi-size 的输出,做一张趋势表。
| 阶段 | FLASH | RAM |
|---|---|---|
| 基础外设 | 131544 B | 29728 B |
| 加 zbus | 134348 B | 28352 B |
| 加看门狗监督 | 136484 B | 29632 B |
| 加 USB 协议 | 139552 B | 31723 B |
| 加事件推送 | 147272 B | 32000 B |
能看到两次明显跳变:协议层引入了 2 KB 线程栈和 512 B 队列;事件推送则主要是消息缓冲和观察者表。
检查清单
每次要发版之前,按这个顺序过一遍,能省掉大部分低级问题:
west build -p always干净构建通过,没有新增警告;- 分层检查脚本通过,没有跨层 include;
- 主机侧单元测试通过(分帧、CRC、TLV);
- 上板跑一遍冒烟:启动日志、shell 命令、USB 枚举、看门狗;
- 断电重启三次,确认没有偶发初始化失败;
- 记录体积变化,超过 2 KB 要说明原因。
常见错误速查
- HardFault 落在
k_sys_fatal_error_handler:先看栈溢出,再看空指针; - shell 命令参数读不到:Zephyr 会把 argv 移到最深一层命令,参数从
argv[1]开始; - USB 插上没反应:先确认
CONFIG_USB_DEVICE_STACK_NEXT,再看 VBUS 检测; - 看门狗一直复位:检查必需模块是不是包含了没起来的那个;
- 日志把串口堵死:确认日志级别,优先压第三方模块。
写到这里差不多 2500 字了。如果目录能正常展开、滚动高亮能跟上、文末相关文章有推荐,那这套模板就可以放心用来写正式文章了。
- 本文链接: https://graveblog.cn/p/long-test/
- 版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 许可协议。

