Lely CANopen `spscring` 单生产者单消费者环形队列完整机制学习笔记
Lely CANopen spscring 单生产者单消费者环形队列完整机制学习笔记 @[toc] 1. 先明确边界:spscring 不是数据缓冲区SPSC 是 Single-Producer Single-Consumer,即单生产者、单消费者。 Lely 的 spscring 与常见“结构体内部直接包含数组”的环形队列不同。它只管理: 哪些索引可以由生产者写入; 哪些索引可以由消费者读取; 生产者何时把写入结果发布给消费者; 消费者何时把已读槽位归还给生产者; 队列由空变为可读或由满变为可写时,是否需要触发回调。 实际数据存储由调用者单独提供: 12struct spscring ring;struct io_can_frame *rxbuf; 两者的关系是: 12345spscring= 索引所有权与可见性控制器rxbuf= 真正保存 CAN 帧的内存数组 因此,调用 spscring_p_alloc() 只会得到一个可写索引,不会返回数据指针,也不会复制任何数据。 1234567flowchart LR Prod[Producer] -->|p...
Lely CANopen CAN 完整机制学习笔记
Lely CANopen IO2 CAN 与 SocketCAN 完整机制学习笔记 @[toc] 1. CANopen、liblely-co、io2 与 SocketCAN 的边界Lely CANopen 协议栈本身是被动状态机。它不直接拥有 Linux socket,也不自行创建线程。Linux 上的实际 I/O 由 liblely-io2 提供,任务调度由 liblely-ev 提供。 12345678910flowchart TB APP[业务代码 / CANopen Master] --> CO[liblely-co / coapp] CO --> NET[CAN network / protocol state machine] NET --> IO2[liblely-io2 abstract CAN API] IO2 --> CHAN[Linux CanChannel] CHAN --> SOCK[SocketCAN raw socket] SOCK --> KERNEL[Lin...
Lely CANopen 事件调度机制详解:从 `ev_task`、Executor、`std_exec` 到 `ev_loop`、Future 与 Poll
Lely CANopen 事件调度机制详解:从 ev_task、Executor、std_exec 到 ev_loop、Future 与 Poll @[toc] 0. 阅读前先记住六个结论 ev_task 只是“可执行工作”的载体,不负责排队、线程切换或等待。 ev_exec_t 是抽象 Executor 接口,本质是 C 语言虚函数表。 ev_std_exec 不是另一个事件循环,而是一个适配层:只要求后端实现 post/abort/on_task_init/on_task_fini,它补齐 dispatch/defer/run。 ev_loop 才拥有真正的全局任务队列、等待线程、Poll 线程和 outstanding work 状态。 ev_exec_run() 的关键价值不是简单调用回调,而是建立“当前线程正在这个 Executor 内执行任务”的 TLS 上下文。 Future、I/O、定时器最终都不会直接“执行用户逻辑”;它们把 ev_task 投递给 Executor,由 ev_loop 取出并运行。 可以先把整个系统压缩成下面这条链: 1234...
Lely CANopen Future Promise 机制原理与源码详解
Lely CANopen Future/Promise 机制原理与源码详解 @[toc] 1. 先给结论:Lely Future 到底是什么Lely 的 Future/Promise 不是一个“阻塞等待结果”的线程同步工具,而是一个和 executor、task、event loop 深度结合的一次性异步完成通知与结果共享机制。 可以先记住以下五句话: ev_promise_t 是异步结果的生产端,负责把共享状态从未完成推进到完成; ev_future_t 是观察端,负责检查结果是否完成、读取结果、登记完成后的任务; Future 本身不会执行 I/O,也不会主动运行回调;真正执行回调的是 task 所属的 executor; ev_future_submit() 不阻塞,而是把 task 暂存在 Future 内部队列,Future ready 后再提交给 executor; Promise 和 Future 不是两个独立对象,它们共享同一块内存、同一个引用计数和同一个生命周期。 官方 C 接口头文件直接强调:与 C++11 的 Futur...
Lely CANopen `ev_loop` 详解
Lely CANopen ev_loop 详解 @[toc] 1. 先区分三个容易混淆的“停止”状态loop.c 中至少存在三类停止或完成状态: 状态 所属范围 作用 loop->stopped 每个 ev_loop_t 整个事件循环是否处于 stopped 状态;ev_loop_stop() 会影响所有正在运行该 loop 的线程。 ev_loop_thrd.stopped 每个线程 中断该线程当前最内层的一次 run/wait 调用;返回后通常会清零,以便下次重新运行。 ctx->ready 每个等待 context 表示正在等待的 future 已经 ready,应结束本次等待。 不能把它们当作同一个状态: 12345678loop->stopped= loop 级停止thread-local stopped= 线程级、单次运行中断ctx->ready= 本次等待目标已经完成 ev_loop_ctx_wait_one() 的主循环实际同时检查这些条件: 12345loop 没有全局停止并且当前线程没有被 kill并且fu...
Lely canopen`io_poll` 机制原理与完整运行流程详解
Lely canopenio_poll 机制原理与完整运行流程详解 @[toc] 1. 先给结论Lely 的 io_poll 是一个基于 Linux epoll 的 I/O 事件分发器。它不解析 CANopen,也不主动读取 SocketCAN;它只负责: 把需要关注的文件描述符 fd 登记到 Linux epoll; 阻塞等待这些 fd 出现可读、可写、异常或断开事件; 根据内核返回的 fd,在红黑树中找到对应的 io_poll_watch; 调用 watch->func(watch, events); 因为所有登记都附加了 EPOLLONESHOT,每次通知后当前一次监听失效; 调用方若要继续接收后续事件,必须再次调用 io_poll_watch() 恢复下一次监听。 最重要的完整周期是: 1234567891011121314151617181920212223创建 Poll ↓初始化 io_poll_watch ↓io_poll_watch(events != 0) ↓首次登记:EPOLL_CTL_ADD ↓ev_poll_wait() ...
Lely CANopen :IO 服务 io_ctx
Lely CANopen :I/O 服务 io_ctx @[toc] 摘要在 Lely CANopen 应用中,lely::io::Context 往往是最先创建、最后销毁的对象之一。它看起来只是一个很薄的 C++ 包装类,但其底层 io_ctx_t 承担了一个关键职责: 维护一组已注册的 Lely I/O 服务,并在进程 fork() 或系统退出时,按确定顺序向这些服务分发维护事件。 ctx.c 本身并不执行 CANopen 协议,也不负责运行事件循环。它更像一个轻量级的“服务生命周期协调器”: 通过双向链表登记 Timer、CAN 网络接口及其他底层 I/O 服务; 通过服务虚表调用各组件自己的 notify_fork 和 shutdown 回调; 在 fork() 前后按不同方向遍历服务; 在 shutdown 时按注册顺序的逆序关闭服务; 通过服务内部的 _shutdown 标志保证每个服务最多关闭一次; 通过 C++ 的 move-only 包装实现单个底层 Context 的唯一所有权; 允许同一进程创建多个彼此独立的 Cont...
Lely CANopen Timer 的实现原理与运行流程
Lely CANopen: Timer 的实现原理与运行流程 @[toc] 一句话结论Lely 的 lely::io::Timer 不是一个独立线程,也不是一个直接执行回调的传统软件定时器。它本质上是一个异步事件适配器: 123456789Linux timerfd 负责计时 ↓io_poll 负责监听 timerfd 的可读事件 ↓内部 wait_task 负责读取到期计数 ↓ev_exec 负责调度完成任务 ↓io_timer_wait 对应的回调最终被执行 创建 Timer、设置到期时间和提交等待操作是三个不同动作: 123创建 Timer:建立 timerfd 并注册到 io_poll设置时间:调用 timerfd_settime(),决定何时到期提交等待:把 io_timer_wait 放入 wait_queue,决定到期后通知谁 从 CANopen 主站视角看,还要再增加两层调度: 123456789BasicMaster / Node ↓ 传入专用 io::TimerBase&io...
VS Code + GDB 远程调试中的系统库边界、跳过策略与反汇编排查
VS Code + GDB 远程调试中的系统库边界、跳过策略与反汇编排查@[toc] 在嵌入式 Linux 远程调试中,经常会出现一种容易误判的情况: 业务程序已经使用 Debug 参数构建; 依赖的第三方动态库已经能够加载符号; info sharedlibrary 中目标库已经显示 Syms Read = Yes; 断点可以命中 main() 或业务入口; 但继续调试时仍然可能遇到: 在某一行按 F11 后,VS Code 直接变成“运行中”; std::string、容器、内存分配等语句无法稳定单步; 点击暂停后只看到一个地址和 ??; step、next、finish 报 Cannot find bounds of current function; 明明已经配置了 skip,仍然会停进系统库; 无法同步目标板系统库到宿主机,不知道该继续调试还是直接绕过。 这类问题说明:动态库符号加载成功,只解决了“某个库能否源码级调试”的问题,并不等于整个进程中的所有执行路径都具备源码、函数边界和行号信息。 本文重点讨论以下场景: 已确认业务程序和目标动态库可调试; 仍...
VS Code Remote GDB 调试动态库源码断点灰色:问题分析与解决方案
VS Code Remote GDB 调试动态库源码断点灰色:问题分析与解决方案 @[toc] 1. 问题概述在嵌入式 Linux 交叉调试中,主程序源码断点能够正常命中,但第三方动态库源码中的断点一直显示为灰色空心状态。GDB 中对应断点显示为: 1<PENDING> 这表示 GDB 已接受断点请求,但尚未将源码行解析为实际运行地址。 动态库源码断点同时依赖以下条件: 动态库包含 DWARF 调试信息; 动态库未被剥离; 构建输出、GDB sysroot 和目标设备中的动态库完全一致; GDB 能找到正确的本地动态库副本; GDB 已读取该动态库的完整调试符号; DWARF 中记录的源码路径能够映射到当前工作区; 调试会话建立时机与共享库加载时机匹配。 2. 调试链路12345678flowchart LR A[源码目录] --> B[构建输出动态库] B --> C[GDB sysroot 动态库] B --> D[目标设备动态库] C --> E[交叉 GDB 读取 DWARF] D -->...






