https://club.rt-thread.org/ask/question/e1d2904dcdff1591.html
[Discussion][CAN] RT-Thread CAN Framework 重构方案:统一 TX Engine、Mailbox Ownership 与 Lifecycle
摘要:统一 CAN TX/RX 核心、发送事务与邮箱所有权,解决乱序、超时、Flush、关闭重配置和中断并发问题,并保持旧 BSP ISR 接口兼容。
这是一份面向 RT-Thread CAN Framework 的重构讨论方案。目标不是针对某一个 BSP 修补问题,而是把近几年 blocking/non-blocking TX、发送顺序、timeout、full flush、bus-off/recovery、CAN FD 和多厂商 BSP 暴露出来的问题收敛到一套统一的 Framework contract 中。
本文先给出一套完整方案用于讨论。已经确认的总体方向直接作为 proposal,不再逐项讨论“要不要做”;希望社区重点评估的是兼容边界、接口细节、资源成本以及分阶段落地方式。
1. 当前实现中需要解决的结构性问题
当前 components/drivers/can/dev_can.c 中 TX 实际存在两套路径:
blocking TX:_can_int_tx() / _can_int_tx_priv() 使用 sendmsg()、TX mailbox freelist、semaphore、completion 和 status.sndchange;
non-blocking TX:_can_nonblocking_tx() 使用 sendmsg_nonblocking(),硬件忙时进入 nb_tx_rb;
RT_CAN_EVENT_TX_DONE / TX_FAIL 中断处理里还会直接从 nb_tx_rb 取下一帧并再次调用 sendmsg_nonblocking();
当 ISR refill 失败时,当前实现使用 rt_ringbuffer_put_force() 放回消息,这会改变 FIFO 顺序,并且在空间不足时具备覆盖旧数据的语义。
这导致 blocking/non-blocking 不只是 API 行为不同,而是 Framework 内部维护了不同的发送资源、排队方式和完成方式。随着 runtime bitrate reconfiguration、TX flush/abort、CAN FD 和 SMP 等需求增加,继续在现有结构上增加状态位和分支会越来越难维护。
相关讨论和问题包括:
当前主线代码:
2. 重构目标
目标 Framework 收敛为以下原则:
blocking / non-blocking 共用一个 TX Engine;
所有 TX 先成为 Framework 管理的 TX Request;
使用预分配 TX Request Descriptor Pool,不在 ISR 中动态分配;
software TX queue 保持严格 FIFO;
Framework 显式维护 mailbox -> request ownership;
BSP 统一通过 sendmsg(can, msg, mailbox) 提交硬件发送;
启用 non-blocking TX 的 driver,其 sendmsg() 必须 ISR-safe;
不引入 deferred scheduler、CAN worker thread 或 workqueue;
blocking wait timeout 不等价于硬件 transaction 已结束;
TX terminal event exactly-once;
Lifecycle State 与 CAN Bus State 分离;
TX/RX 使用独立 irq-safe spinlock;
RX 保留现有 node pool 思路,只重构 synchronization/ownership/lifecycle;
rt_hw_can_isr(can, event) 和现有 BSP ISR 调用方式第一阶段保持兼容;
第一阶段保持 rt_device_read/write/control 和 struct rt_can_msg 兼容;
struct rt_can_core 直接嵌入 struct rt_can_device,不增加二次动态分配;
不增加 RT_CAN_USING_RX / RT_CAN_USING_TX 这种基础裁剪项,CAN 默认同时具备 RX/TX;
Kconfig 只裁剪 blocking、non-blocking、callback、trace 和 request pool 等真正影响 RAM/Flash 的功能。
3. 目标 Framework 结构
flowchart TD
APP["Application / ISR"] --> API["rt_device_write / rt_can_send_async"]
API --> TXQ["TX Request Pool + FIFO Queue"]
TXQ --> SCH["TX Scheduler"]
SCH --> OWN["TX mailbox ownership"]
OWN --> SEND["ops->sendmsg(can, msg, mailbox)"]
SEND --> HW["CAN Controller"]
HW --> ISR["existing rt_hw_can_isr"]
ISR --> TERM["TX terminal handler"]
TERM --> SCH
TERM --> WAIT["blocking completion"]
TERM --> CB["async callback"]
HW --> RXISR["RX event"]
RXISR --> RXCORE["RX node pool + RX spinlock"]
RXCORE --> READ["rt_device_read"]
LC["Lifecycle Core"] --> TXQ
LC --> RXCORE
BUS["Bus State"] --> LC
Loading
Scheduler 不是独立线程,也不是后台任务。它只是一个“尝试把 pending TX Request 推进到硬件 mailbox”的短函数。
它主要由三个事件触发:
1. 新 TX Request 入队;
2. TX_DONE / TX_ERROR / TX_ABORTED 释放 mailbox;
3. controller recover / reconfigure 完成后重新允许 TX。
本方案不使用 deferred scheduling。
4. 统一 blocking / non-blocking TX Engine
blocking 与 non-blocking 的区别只保留在“调用者是否等待 terminal result”。
统一数据流:
CAN frame
↓
TX Request
↓
FIFO pending queue
↓
TX Scheduler
↓
TX mailbox
↓
ops->sendmsg()
↓
CAN hardware
↓
TX terminal event
blocking:
enqueue request
→ schedule
→ wait completion
→ terminal result
→ return
non-blocking:
enqueue request
→ schedule
→ accepted 后立即返回
→ terminal event 后 callback / auto retire
因此 BSP 不再区分“blocking send function”和“non-blocking send function”。blocking 是 Framework 的等待策略,不是 BSP 的发送方式。
5. sendmsg() 作为唯一 BSP TX submit 接口
现有很多 BSP 的 sendmsg(can, msg, box_num) 本质已经是“把一帧写入指定发送资源并立即返回”,它本身并不负责 blocking。
目标 contract 建议明确为:
/**
* Submit one CAN frame to the specified hardware TX mailbox.
*
* The function only performs hardware submission. It must not wait for
* transmission completion. Blocking/non-blocking semantics are owned by
* the CAN framework.
*
* When RT_CAN_USING_TX_NONBLOCKING is enabled for the driver, this callback
* must be safe to call from ISR context.
*
* @return
* - RT_EOK: frame accepted by the selected mailbox;
* - -RT_EBUSY: selected mailbox/resource is temporarily unavailable;
* - other error: controller/parameter failure.
*/
rt_ssize_t (* sendmsg )(struct rt_can_device * can ,
const void * buf ,
rt_uint32_t mailbox );
这里的 ISR-safe 至少意味着:
- 不 sleep;
- 不 take blocking mutex/semaphore;
- 不等待 TX complete;
- 不依赖只能在线程上下文运行的服务;
- 执行时间应保持为硬件 submit 所需的短路径。
为什么 non-blocking driver 必须满足 ISR-safe sendmsg()
当前 Framework 本来就在 TX_DONE ISR 中调用 sendmsg_nonblocking() refill software queue。
新 Framework 不再保留第二套 non-blocking hardware submit path,因此:
TX_DONE
↓
release mailbox
↓
TX Scheduler
↓
ops->sendmsg(next_request, mailbox)
会直接发生在 ISR context。
如果这里不允许调用 sendmsg(),又禁止 deferred/workqueue,那么 pending non-blocking queue 就可能在第一帧完成后停止推进。
因此本方案把能力关系定义得更直接:
一个 driver 如果支持新的 non-blocking TX,就必须保证统一的 sendmsg() 是 ISR-safe 的。
不再增加单独的 TX_SEND_ISR_SAFE flag,也不在 Framework 中维护两套 scheduler。
sendmsg_nonblocking() 如何处理
第一阶段建议仍保留 struct rt_can_ops 中现有 sendmsg_nonblocking 成员,使旧 BSP 的静态 initializer 和源码继续编译。
但是新的 TX Core 不再依赖该接口。
Target TX path:
sendmsg(can, msg, mailbox)
Legacy member:
sendmsg_nonblocking(...)
→ retained temporarily for source compatibility
→ not part of the new TX Engine contract
对于希望继续支持 non-blocking 的旧 BSP,需要把现有 sendmsg_nonblocking() 中的 ISR-safe 硬件 submit 能力合并到 sendmsg(can, msg, mailbox) 中。
这一迁移只涉及 BSP 的发送实现;现有 BSP ISR 调用 rt_hw_can_isr() 的代码不需要修改。
6. TX Request Descriptor Pool
软件队列不再只保存裸 rt_can_msg,而是保存完整发送事务。
enum rt_can_tx_req_state
{
RT_CAN_TX_REQ_FREE = 0 ,
RT_CAN_TX_REQ_QUEUED ,
RT_CAN_TX_REQ_SUBMITTING ,
RT_CAN_TX_REQ_HW_PENDING ,
RT_CAN_TX_REQ_ABORTING ,
RT_CAN_TX_REQ_DONE ,
RT_CAN_TX_REQ_ERROR ,
RT_CAN_TX_REQ_ABORTED ,
RT_CAN_TX_REQ_CANCELLED ,
};
建议 descriptor:
typedef void (* rt_can_tx_done_cb )(struct rt_can_device * can ,
const struct rt_can_msg * msg ,
rt_err_t result ,
void * arg );
struct rt_can_tx_request
{
rt_list_t node ;
struct rt_can_msg msg ;
enum rt_can_tx_req_state state ;
rt_err_t result ;
/* -1 means no hardware mailbox is currently owned. */
rt_int16_t mailbox ;
#ifdef RT_CAN_USING_TX_BLOCKING
rt_bool_t waiter_attached ;
struct rt_completion completion ;
#endif
#ifdef RT_CAN_USING_TX_CALLBACK
rt_can_tx_done_cb callback ;
void * callback_arg ;
#endif
#ifdef RT_CAN_USING_TX_TRACE
rt_uint32_t sequence ;
#endif
};
这里不再增加通用 flags 字段。发送生命周期由 state 表示,其它需要独立语义的内容使用明确字段,避免形成 state + flags 两套状态源。
request_count 与 sequence
request_count 是 TX Request Pool 的容量:
rt_uint16_t request_count ;
例如 request_count = 16 表示最多同时存在 16 个尚未 retire 的 TX Request。
sequence 只是可选 trace/debug 编号,不参与 ownership、FIFO 或 terminal correctness。
如果启用 trace,rt_uint32_t sequence 允许自然回绕:
0xFFFFFFFE
0xFFFFFFFF
0x00000000
0x00000001
不需要为溢出建立额外状态,因为真正的 request identity 和 mailbox ownership 由 descriptor 指针维护。
7. TX Core 与 Mailbox Ownership
不为只有一个成员的 mailbox 再增加独立结构体。
建议:
struct rt_can_tx_core
{
struct rt_spinlock lock ;
rt_list_t free_list ;
rt_list_t pending_list ;
struct rt_can_tx_request * requests ;
rt_uint16_t request_count ;
/*
* Array size uses existing can->config.sndboxnumber.
* mailbox_owner[n] == RT_NULL means mailbox n has no framework owner.
*/
struct rt_can_tx_request * * mailbox_owner ;
/* Protected by tx.lock; prevents scheduler re-entry. */
rt_bool_t scheduling ;
};
继续复用现有:
作为 mailbox 数量,不再增加重复的 mailbox_count。
Mailbox 是什么
这里的 mailbox 沿用 RT-Thread 当前概念。
对于 STM32 bxCAN:
mailbox 0 = sTxMailBox[0]
mailbox 1 = sTxMailBox[1]
mailbox 2 = sTxMailBox[2]
其它 controller 可能叫 TX Buffer / TX FIFO Element,但 Generic Framework 仍把“可以独立产生 TX terminal event 的发送资源编号”统一称为 mailbox。
关键 ownership 不变量
1. 一个 mailbox 同一时刻最多只有一个 owner;
2. HW_PENDING / ABORTING request 必须能反查到自己的 mailbox;
3. mailbox_owner[n] 未清空前,对应 request descriptor 不得复用;
4. stale / duplicate TX IRQ 不得修改新的 request;
5. status.sndchange 不再作为 ownership correctness 的依据。
8. TX Scheduler
Scheduler 是 TX Engine 的中心推进函数,但不是线程。
基本流程:
flowchart TD
KICK["TX schedule kick"] --> LOCK["tx.lock"]
LOCK --> CHECK["check lifecycle / scheduling"]
CHECK --> HEAD["take pending queue HEAD"]
HEAD --> BOX["select available mailbox"]
BOX --> RESERVE["mailbox_owner = request; state = SUBMITTING"]
RESERVE --> UNLOCK["tx.unlock"]
UNLOCK --> SEND["ops->sendmsg()"]
SEND --> RET{"return"}
RET -- "RT_EOK" --> PEND["HW_PENDING"]
RET -- "-RT_EBUSY" --> RESTORE["restore exact queue HEAD"]
RET -- "error" --> ERR["terminal ERROR"]
Loading
Driver/HAL 调用必须位于 tx.lock 外。
也就是说采用:
reserve
→ unlock
→ driver submit
→ lock
→ commit / rollback
而不是:
spinlock
→ HAL/driver
→ unlock
-RT_EBUSY 的处理
如果指定 mailbox 临时不可用:
Request = QUEUED
mailbox_owner = NULL
request 恢复到 pending queue HEAD
不再执行:
pop
→ send fail
→ put_force 到 tail
这样不会改变原始 FIFO 顺序,也不会覆盖旧 request。
9. Wire Transmission Order
本方案把目标定义为:
对同一个 rt_can_device,Framework 接受的 TX Request 应保持应用提交顺序,并在 Generic fallback 下保证本节点的 wire transmission order。
例如:
Application: A → B → C
Wire order: A → B → C
其它 CAN 节点仍然可以通过总线仲裁插入 A/B/C 之间,这不属于本节点 Framework 的顺序问题。
多 mailbox Controller
如果同时 preload:
A → mailbox 0
B → mailbox 1
C → mailbox 2
某些 controller 可能按照 CAN ID priority 或自己的 mailbox policy 决定发送顺序。
Generic Framework 因此默认采用严格模式:
如果不能证明多个 mailbox 可以保持 submit order,
同一时刻最多允许一个 TX Request 进入 hardware pending。
如果某个 controller 明确支持 ordered multi-mailbox,可以通过 capability 允许多 mailbox preload,例如:
#define RT_CAN_CAP_TX_ORDERED_MAILBOX (1UL << 0)
这样 strict order 是 Generic correctness baseline,多 mailbox 是 driver capability optimization。
10. Blocking timeout 不等于 TX 已结束
这是新 TX ownership 最重要的语义之一。
Request A
↓
HW_PENDING
↓
blocking thread waits
↓
wait timeout
timeout 只说明:
不能说明:
hardware 已经停止发送 Request A。
因此 timeout 后:
- mailbox ownership 不释放;
- descriptor 不复用;
- request 仍等待 TX_DONE / TX_ERROR / TX_ABORTED;
- waiter_attached = false;
- terminal event 到达后 Framework 自动 retire。
这可以避免旧 transaction 的 late IRQ 修改已经复用给新 transaction 的 slot/request。
11. 统一 TX terminal event
所有 TX transaction 最终只允许进入一次 terminal 状态:
enum rt_can_tx_result
{
RT_CAN_TX_RESULT_OK = 0 ,
RT_CAN_TX_RESULT_ERROR ,
RT_CAN_TX_RESULT_ABORTED ,
RT_CAN_TX_RESULT_CANCELLED ,
};
所有来源最终汇入同一处理函数:
TX_DONE
TX_FAIL
hardware abort complete
full flush cancel
close cancel
例如:
static void _can_tx_complete (struct rt_can_device * can ,
struct rt_can_tx_request * req ,
enum rt_can_tx_result result );
规则:
只能成功一次。
Abort 和 TX_DONE 发生竞争时,第一个 terminal event 完成 request;后续重复/过期 event 只能记录 diagnostics,不能再次 completion、callback 或 free。
12. Async completion callback 第一阶段直接开放
建议不再只做内部预留,第一阶段直接提供 async completion API:
#ifdef RT_CAN_USING_TX_CALLBACK
rt_err_t rt_can_send_async (struct rt_can_device * can ,
const struct rt_can_msg * msg ,
rt_can_tx_done_cb callback ,
void * arg );
#endif
默认 callback context 与 terminal event context 一致。
如果 TX_DONE 来自 ISR,则 callback 也在 ISR context 调用,但必须在 tx.lock 已释放以后。
因此文档 contract 必须明确 callback 不允许:
sleep
blocking mutex/semaphore
长时间运算
blocking I/O
需要线程上下文的应用可以在 callback 中自行投递 message/event;CAN Core 本身不引入 workqueue。
13. Lifecycle State
建议公共生命周期:
enum rt_can_lifecycle_state
{
RT_CAN_LC_STOPPED = 0 ,
RT_CAN_LC_RUNNING ,
RT_CAN_LC_QUIESCING ,
RT_CAN_LC_QUIESCED ,
RT_CAN_LC_RECONFIGURING ,
RT_CAN_LC_RECOVERING ,
RT_CAN_LC_CLOSING ,
};
语义:
STOPPED
controller/framework 未运行;
RUNNING
正常允许 TX/RX;
QUIESCING
已禁止新的 TX Request,正在处理旧 request;
QUIESCED
TX 已完全静默,可安全进行 reconfigure/stop;
RECONFIGURING
正在修改 baud/mode/CAN FD 等 controller 配置;
RECOVERING
正在执行 controller/bus recovery;
CLOSING
正在停止中断、终止 transaction 并释放 runtime resource。
不增加 admission_open / submit_refcnt
Lifecycle admission 与 TX enqueue 都在 tx.lock 下完成。
TX enqueue:
tx.lock
→ lifecycle == RUNNING ?
→ allocate request
→ enqueue
→ tx.unlock
进入 quiesce:
lifecycle_lock
→ tx.lock
→ RUNNING → QUIESCING
→ tx.unlock
因此一旦状态进入 QUIESCING,新的 TX enqueue 就无法越过状态切换。
不需要额外维护:
admission_open
submit_refcnt
QUIESCED 的严格条件
只有以下条件同时满足时进入:
pending_list empty
没有 SUBMITTING request
所有 mailbox_owner == NULL
没有 ABORTING request
scheduler 已退出
不增加 inflight_count,因为 inflight 可以直接由 mailbox ownership 推导,避免维护重复状态。
14. Quiesce / Drain / Full Flush / Abort
建议把这些语义分开。
enum rt_can_quiesce_policy
{
RT_CAN_QUIESCE_DRAIN = 0 ,
RT_CAN_QUIESCE_DROP_QUEUED ,
RT_CAN_QUIESCE_ABORT_ALL ,
};
DRAIN
禁止新 TX
+ queued request 全部正常发送
+ hardware pending 正常完成
→ QUIESCED
DROP_QUEUED
禁止新 TX
+ software queued request → CANCELLED
+ hardware pending 正常完成
→ QUIESCED
ABORT_ALL / Full TX Flush
禁止新 TX
+ software queued request → CANCELLED
+ HW_PENDING → ABORTING
+ driver hardware abort
+ 等所有 mailbox_owner 清空
→ QUIESCED
典型 runtime bitrate reconfiguration:
RUNNING
→ QUIESCING
→ ABORT_ALL
→ QUIESCED
→ RECONFIGURING
→ set bitrate
→ RUNNING
Hardware abort 仍然是可选 driver capability,例如:
#define RT_CAN_CAP_TX_ABORT (1UL << 1)
abort(mailbox) 调用成功只表示 abort request 已提交,不等价于 transaction 已 terminal;最终仍由 TX_ABORTED / TX_DONE / TX_ERROR 中的一个终态事件结束 request。
15. Bus State 与 Lifecycle 分离
Lifecycle 描述 Framework/controller 操作阶段;Bus State 描述 CAN 协议错误状态。
建议:
enum rt_can_bus_state
{
RT_CAN_BUS_UNKNOWN = 0 ,
RT_CAN_BUS_ERROR_ACTIVE ,
RT_CAN_BUS_ERROR_WARNING ,
RT_CAN_BUS_ERROR_PASSIVE ,
RT_CAN_BUS_OFF ,
};
合法组合例如:
Lifecycle = RUNNING
Bus State = ERROR_PASSIVE
或:
Lifecycle = RECOVERING
Bus State = BUS_OFF
BUS_OFF 不等于 STOPPED,否则 runtime recovery、queue policy 和 controller lifecycle 会继续混在一起。
第一阶段不新增复杂的 rt_can_bus_status_core 或 rt_can_statistics。现有 rt_can_status 继续承担 observability;新的 transaction correctness 不再依赖 sndchange/rcvchange 等状态字段。
16. RX 重构
RX 也需要重构,但不复制 TX Request state machine。
TX 的复杂性来自:
queue
mailbox ownership
waiter
timeout
abort
terminal
ordering
RX 是单向:
hardware frame
→ recvmsg
→ software RX node
→ RX queue
→ rt_device_read
因此 RX 保留现有 node pool / freelist / uselist 思路,主要收敛四件事。
16.1 RX 独立 spinlock
struct rt_can_rx_core
{
struct rt_spinlock lock ;
struct rt_can_rx_fifo * fifo ;
};
rx.lock 保护:
freelist
uselist / pending list
HDR list membership
node owner
hdr->msgs
RX queue counters
不再散落使用大范围 rt_hw_local_irq_disable() 保护复合状态。
16.2 Driver recvmsg() 仍在 lock 外
当前 recvmsg() 已经由 rt_hw_can_isr() 在 ISR context 调用,因此新 RX Core 保持这一调用上下文。
流程:
RX IRQ
→ existing rt_hw_can_isr()
→ lifecycle fast check
→ ops->recvmsg()
→ rx.lock
→ allocate/update RX node ownership
→ enqueue
→ rx.unlock
→ rx_indicate/filter callback
HAL/driver callback 和用户 callback 都不在 rx.lock 内执行。
16.3 RX overflow policy
建议 pool 满时:
drop current/new incoming frame
+ increment drop counter
不通过覆盖 software queue 中已经入队的旧 frame 来获取空间。
这样 RX software FIFO 的 ownership 和顺序更明确。
16.4 Lifecycle 与 RX close/reconfigure
close/reconfigure 需要保证:
controller RX IRQ/receive path 停止
→ rx.lock
→ detach runtime RX state
→ rx.unlock
→ free resource
不能先 free RX pool,再允许 ISR 继续访问旧指针。
17. TX/RX 公共 Core
TX/RX 不共用同一个 spinlock。
struct rt_can_core
{
struct rt_mutex lifecycle_lock ;
rt_atomic_t lifecycle_state ;
rt_atomic_t bus_state ;
rt_uint32_t capabilities ;
struct rt_can_tx_core tx ;
struct rt_can_rx_core rx ;
};
然后直接嵌入:
struct rt_can_device
{
struct rt_device parent ;
const struct rt_can_ops * ops ;
struct can_configure config ;
/* Existing compatibility fields as needed. */
struct rt_can_core core ;
};
不使用:
struct rt_can_core * core ;
理由是 core 与 CAN device 生命周期一致,直接嵌入更适合 MCU,也避免额外 malloc 和 ownership。
公共部分只包含:
Lifecycle
Bus State
Capabilities
configuration/open/close/recover transaction
TX/RX 各自维护自己的 queue、ownership 和 spinlock。
锁顺序固定为:
lifecycle_lock
↓
tx.lock / rx.lock
ISR 永远不会获取 lifecycle_lock。
18. 现有 BSP ISR 必须零修改兼容
第一阶段保留:
void rt_hw_can_isr (struct rt_can_device * can , int event );
保留现有 event 编码和 BSP 调用方式。
例如 BSP 现在:
rt_hw_can_isr (can ,
RT_CAN_EVENT_TX_DONE |
(mailbox << 8 ));
不需要修改。
新的 rt_hw_can_isr() 内部改成 compatibility dispatcher:
void rt_hw_can_isr (struct rt_can_device * can , int event )
{
rt_uint32_t type = event & 0xff ;
rt_uint32_t index = event >> 8 ;
switch (type )
{
case RT_CAN_EVENT_TX_DONE :
_can_tx_mailbox_event (can , index , RT_CAN_TX_RESULT_OK );
break ;
case RT_CAN_EVENT_TX_FAIL :
_can_tx_mailbox_event (can , index , RT_CAN_TX_RESULT_ERROR );
break ;
case RT_CAN_EVENT_RX_IND :
_can_rx_event (can , index );
break ;
default :
/* existing compatibility events */
break ;
}
}
这样新 Core 可以获得 typed internal event,但 BSP 不需要一次性迁移。
19. Kconfig 裁剪
不增加:
RT_CAN_USING_RX
RT_CAN_USING_TX
CAN Framework 默认同时具备 TX/RX。
只对真正影响代码和 RAM 的功能做裁剪,例如:
config RT_CAN_USING_TX_BLOCKING
bool "Enable CAN blocking TX"
config RT_CAN_USING_TX_NONBLOCKING
bool "Enable CAN non-blocking TX"
help
Drivers providing non-blocking TX must implement sendmsg()
as an ISR-safe hardware submission callback.
config RT_CAN_USING_TX_CALLBACK
bool "Enable CAN TX completion callback"
depends on RT_CAN_USING_TX_NONBLOCKING
config RT_CAN_USING_TX_TRACE
bool "Enable CAN TX request trace"
config RT_CAN_TX_REQUEST_COUNT
int "CAN TX request pool size"
range 1 256
条件编译目标:
blocking disabled
→ remove completion / waiter path
callback disabled
→ remove callback + callback_arg
trace disabled
→ remove sequence + trace code
Request Pool RAM 可以按:
sizeof(rt_can_tx_request) × RT_CAN_TX_REQUEST_COUNT
直接评估。
20. Public API 兼容
第一阶段继续保留:
rt_device_read ()
rt_device_write ()
rt_device_control ()
struct rt_can_msg
包括现有:
作为 compatibility policy 输入。
内部立即转换为统一 Request:
rt_device_write
→ compatibility adapter
→ allocate TX Request
→ unified TX Engine
同时新增 async completion API,但不要求旧应用迁移。
21. 现有字段到新 Core 的迁移关系
当前实现
新 Framework
_can_int_tx blocking 独立路径
Unified TX Engine
_can_nonblocking_tx 独立路径
Unified TX Engine
rt_can_tx_fifo mailbox freelist
TX Request Pool + mailbox ownership
tx_fifo->sem
Request/ownership 生命周期,不再作为 correctness 主体
mailbox slot completion
per-request completion
status.sndchange
不再用于 ownership
nb_tx_rb
删除,改为 TX Request FIFO
sendmsg_nonblocking()
第一阶段保留成员兼容,new core 不依赖
sendmsg(can,msg,box)
唯一 BSP TX submit primitive
TX ISR 中 put_force
删除,失败恢复 queue HEAD
raw local IRQ critical sections
TX/RX 独立 spin_lock_irqsave
RX freelist/uselist
保留,但统一由 rx.lock 保护
rt_hw_can_isr()
完整保留外部 BSP contract,内部适配新 Core
22. 新 Framework 解决的问题对应关系
痛点
方案
blocking/non-blocking 两套发送逻辑
Unified TX Engine
后来的 frame 绕过 software backlog
所有 TX 统一先进入 Request FIFO
put_force 导致 reorder/overwrite
reserve-submit-commit + restore queue HEAD
mailbox ownership 模糊
mailbox_owner[n] -> request
timeout 后 slot/request 过早复用
timeout != terminal
late IRQ 污染新 request
explicit mailbox ownership
duplicate/abort race
exactly-once terminal handler
full flush 无统一语义
QUIESCING + CANCEL + ABORT + QUIESCED
runtime bitrate change race
Lifecycle transaction
close 与 active TX/RX race
Lifecycle + TX/RX lock domain
non-blocking completion 无 identity
TX Request + callback
IRQ-off 范围过大
short TX/RX spinlock critical sections
SMP local IRQ disable 不足
irq-safe spinlock
bus-off 与 stop 混淆
Bus State 与 Lifecycle 分离
RX list/HDR compound state
RX ownership under one lock
大量 BSP 迁移成本
keep rt_hw_can_isr() / public device API
23. 分阶段落地建议
这份 Issue 讨论的是整体目标架构,不建议一个 PR 一次全部完成。
建议后续按以下层次拆分:
PR 1:Core synchronization foundation
- TX/RX spinlock domain
- lifecycle 基础状态
- 保持现有 public behavior
- 增加 core-level host/stub tests
PR 2:Unified TX Request + Scheduler
- Request Pool
- FIFO pending queue
- mailbox_owner
- reserve/submit/commit
- blocking/non-blocking unified engine
- remove nb_tx_rb correctness dependency
PR 3:Terminal / timeout correctness
- exactly-once terminal
- timeout waiter detach
- stale/duplicate IRQ handling
- callback API
PR 4:Strict TX order
- generic strict wire order
- ordered multi-mailbox capability
- resolve #11270 class ordering problems
PR 5:Quiesce / Full Flush / Abort
- QUIESCING / QUIESCED
- DRAIN / DROP_QUEUED / ABORT_ALL
- hardware abort capability
- support runtime bitrate reconfiguration
PR 6:Lifecycle / Bus-off / Recovery
- close safety
- reconfigure transaction
- Bus State
- controller recovery
PR 7:RX ownership cleanup and further capability normalization
- RX spinlock migration
- RX overflow policy
- filter/HDR ownership cleanup
- CAN FD / filter capability follow-up
实际拆分可根据 maintainer review 调整,但建议始终保持每个 PR 的 contract 清晰、可独立验证和可回滚。
24. 希望社区重点评估的细节
总体架构按上述 proposal 讨论,希望重点得到以下反馈:
sendmsg() 统一为 hardware submit primitive,并要求支持 non-blocking 的 driver 保证 ISR-safe,这个 BSP contract 是否合适;
sendmsg_nonblocking() 作为过渡成员保留多久比较合适;
Generic strict wire-order 默认串行 hardware pending,对不同 controller 的吞吐影响是否可接受;
ordered multi-mailbox capability 的抽象是否足够通用;
Request Pool 的默认容量和配置入口应该放在 global Kconfig 还是 can_configure;
async completion callback 默认 ISR context 是否符合 RT-Thread driver API 习惯;
privmode 在统一 scheduler 下继续使用指定 mailbox 时,是否需要保留现有并行语义;
RX pool 满时采用 drop-new 而不是覆盖已排队旧 frame,兼容性是否需要额外考虑;
Lifecycle 状态与现有 RT_CAN_CMD_START/STOP/CONFIG control command 的映射方式;
哪些现有 BSP 最适合作为第一批迁移和回归验证对象。
25. 当前建议冻结的架构基线
如果这套方向获得认可,后续实现建议以以下约束作为基线:
1. One Unified TX Engine.
2. Preallocated TX Request Descriptor Pool.
3. FIFO software pending queue.
4. Explicit mailbox -> request ownership.
5. One BSP TX submit API: sendmsg(can, msg, mailbox).
6. Non-blocking capable drivers require ISR-safe sendmsg().
7. No deferred scheduler / worker thread.
8. TX scheduler is kicked by enqueue and terminal events.
9. Generic strict wire-order guarantee.
10. Timeout is not a hardware terminal event.
11. Exactly-once terminal completion.
12. Lifecycle State and Bus State are separate.
13. Full flush is implemented through quiesce + cancel + hardware abort.
14. TX/RX have independent irq-safe spinlocks.
15. RX keeps the existing node-pool model but fixes ownership/synchronization.
16. Existing rt_hw_can_isr() call sites remain unchanged.
17. Existing rt_device API remains compatible in the first stage.
18. No RT_CAN_USING_RX / RT_CAN_USING_TX Kconfig split.
19. Optional blocking/nonblocking/callback/trace features can be compiled out.
20. status/statistics are observability only, not transaction ownership state.
这份 proposal 的核心目标是让 CAN Framework 从“多个发送路径 + 隐式 mailbox 状态 + IRQ critical sections”演进为:
explicit TX transaction ownership
+ ordered scheduler
+ explicit lifecycle
+ clear driver contract
+ independent TX/RX synchronization domains
如果整体 contract 可以达成共识,再进入具体结构体字段、error code、Kconfig 命名、BSP migration 和 PR implementation,会比继续围绕单个 bug 逐项打补丁更容易维护。
https://club.rt-thread.org/ask/question/e1d2904dcdff1591.html
[Discussion][CAN] RT-Thread CAN Framework 重构方案:统一 TX Engine、Mailbox Ownership 与 Lifecycle
这是一份面向 RT-Thread CAN Framework 的重构讨论方案。目标不是针对某一个 BSP 修补问题,而是把近几年 blocking/non-blocking TX、发送顺序、timeout、full flush、bus-off/recovery、CAN FD 和多厂商 BSP 暴露出来的问题收敛到一套统一的 Framework contract 中。
本文先给出一套完整方案用于讨论。已经确认的总体方向直接作为 proposal,不再逐项讨论“要不要做”;希望社区重点评估的是兼容边界、接口细节、资源成本以及分阶段落地方式。
1. 当前实现中需要解决的结构性问题
当前
components/drivers/can/dev_can.c中 TX 实际存在两套路径:_can_int_tx()/_can_int_tx_priv()使用sendmsg()、TX mailbox freelist、semaphore、completion 和status.sndchange;_can_nonblocking_tx()使用sendmsg_nonblocking(),硬件忙时进入nb_tx_rb;RT_CAN_EVENT_TX_DONE / TX_FAIL中断处理里还会直接从nb_tx_rb取下一帧并再次调用sendmsg_nonblocking();rt_ringbuffer_put_force()放回消息,这会改变 FIFO 顺序,并且在空间不足时具备覆盖旧数据的语义。这导致 blocking/non-blocking 不只是 API 行为不同,而是 Framework 内部维护了不同的发送资源、排队方式和完成方式。随着 runtime bitrate reconfiguration、TX flush/abort、CAN FD 和 SMP 等需求增加,继续在现有结构上增加状态位和分支会越来越难维护。
相关讨论和问题包括:
当前主线代码:
dev_can.c: https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/can/dev_can.cdev_can.h: https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/include/drivers/dev_can.h2. 重构目标
目标 Framework 收敛为以下原则:
mailbox -> requestownership;sendmsg(can, msg, mailbox)提交硬件发送;sendmsg()必须 ISR-safe;rt_hw_can_isr(can, event)和现有 BSP ISR 调用方式第一阶段保持兼容;rt_device_read/write/control和struct rt_can_msg兼容;struct rt_can_core直接嵌入struct rt_can_device,不增加二次动态分配;RT_CAN_USING_RX/RT_CAN_USING_TX这种基础裁剪项,CAN 默认同时具备 RX/TX;3. 目标 Framework 结构
flowchart TD APP["Application / ISR"] --> API["rt_device_write / rt_can_send_async"] API --> TXQ["TX Request Pool + FIFO Queue"] TXQ --> SCH["TX Scheduler"] SCH --> OWN["TX mailbox ownership"] OWN --> SEND["ops->sendmsg(can, msg, mailbox)"] SEND --> HW["CAN Controller"] HW --> ISR["existing rt_hw_can_isr"] ISR --> TERM["TX terminal handler"] TERM --> SCH TERM --> WAIT["blocking completion"] TERM --> CB["async callback"] HW --> RXISR["RX event"] RXISR --> RXCORE["RX node pool + RX spinlock"] RXCORE --> READ["rt_device_read"] LC["Lifecycle Core"] --> TXQ LC --> RXCORE BUS["Bus State"] --> LCScheduler不是独立线程,也不是后台任务。它只是一个“尝试把 pending TX Request 推进到硬件 mailbox”的短函数。它主要由三个事件触发:
本方案不使用 deferred scheduling。
4. 统一 blocking / non-blocking TX Engine
blocking 与 non-blocking 的区别只保留在“调用者是否等待 terminal result”。
统一数据流:
blocking:
non-blocking:
因此 BSP 不再区分“blocking send function”和“non-blocking send function”。blocking 是 Framework 的等待策略,不是 BSP 的发送方式。
5.
sendmsg()作为唯一 BSP TX submit 接口现有很多 BSP 的
sendmsg(can, msg, box_num)本质已经是“把一帧写入指定发送资源并立即返回”,它本身并不负责 blocking。目标 contract 建议明确为:
这里的 ISR-safe 至少意味着:
为什么 non-blocking driver 必须满足 ISR-safe
sendmsg()当前 Framework 本来就在 TX_DONE ISR 中调用
sendmsg_nonblocking()refill software queue。新 Framework 不再保留第二套 non-blocking hardware submit path,因此:
会直接发生在 ISR context。
如果这里不允许调用
sendmsg(),又禁止 deferred/workqueue,那么 pending non-blocking queue 就可能在第一帧完成后停止推进。因此本方案把能力关系定义得更直接:
不再增加单独的
TX_SEND_ISR_SAFEflag,也不在 Framework 中维护两套 scheduler。sendmsg_nonblocking()如何处理第一阶段建议仍保留
struct rt_can_ops中现有sendmsg_nonblocking成员,使旧 BSP 的静态 initializer 和源码继续编译。但是新的 TX Core 不再依赖该接口。
对于希望继续支持 non-blocking 的旧 BSP,需要把现有
sendmsg_nonblocking()中的 ISR-safe 硬件 submit 能力合并到sendmsg(can, msg, mailbox)中。这一迁移只涉及 BSP 的发送实现;现有 BSP ISR 调用
rt_hw_can_isr()的代码不需要修改。6. TX Request Descriptor Pool
软件队列不再只保存裸
rt_can_msg,而是保存完整发送事务。建议 descriptor:
这里不再增加通用
flags字段。发送生命周期由state表示,其它需要独立语义的内容使用明确字段,避免形成state + flags两套状态源。request_count与sequencerequest_count是 TX Request Pool 的容量:例如
request_count = 16表示最多同时存在 16 个尚未 retire 的 TX Request。sequence只是可选 trace/debug 编号,不参与 ownership、FIFO 或 terminal correctness。如果启用 trace,
rt_uint32_t sequence允许自然回绕:不需要为溢出建立额外状态,因为真正的 request identity 和 mailbox ownership 由 descriptor 指针维护。
7. TX Core 与 Mailbox Ownership
不为只有一个成员的 mailbox 再增加独立结构体。
建议:
继续复用现有:
作为 mailbox 数量,不再增加重复的
mailbox_count。Mailbox 是什么
这里的 mailbox 沿用 RT-Thread 当前概念。
对于 STM32 bxCAN:
其它 controller 可能叫 TX Buffer / TX FIFO Element,但 Generic Framework 仍把“可以独立产生 TX terminal event 的发送资源编号”统一称为 mailbox。
关键 ownership 不变量
8. TX Scheduler
Scheduler 是 TX Engine 的中心推进函数,但不是线程。
基本流程:
flowchart TD KICK["TX schedule kick"] --> LOCK["tx.lock"] LOCK --> CHECK["check lifecycle / scheduling"] CHECK --> HEAD["take pending queue HEAD"] HEAD --> BOX["select available mailbox"] BOX --> RESERVE["mailbox_owner = request; state = SUBMITTING"] RESERVE --> UNLOCK["tx.unlock"] UNLOCK --> SEND["ops->sendmsg()"] SEND --> RET{"return"} RET -- "RT_EOK" --> PEND["HW_PENDING"] RET -- "-RT_EBUSY" --> RESTORE["restore exact queue HEAD"] RET -- "error" --> ERR["terminal ERROR"]Driver/HAL 调用必须位于
tx.lock外。也就是说采用:
而不是:
-RT_EBUSY的处理如果指定 mailbox 临时不可用:
不再执行:
这样不会改变原始 FIFO 顺序,也不会覆盖旧 request。
9. Wire Transmission Order
本方案把目标定义为:
例如:
其它 CAN 节点仍然可以通过总线仲裁插入 A/B/C 之间,这不属于本节点 Framework 的顺序问题。
多 mailbox Controller
如果同时 preload:
某些 controller 可能按照 CAN ID priority 或自己的 mailbox policy 决定发送顺序。
Generic Framework 因此默认采用严格模式:
如果某个 controller 明确支持 ordered multi-mailbox,可以通过 capability 允许多 mailbox preload,例如:
这样 strict order 是 Generic correctness baseline,多 mailbox 是 driver capability optimization。
10. Blocking timeout 不等于 TX 已结束
这是新 TX ownership 最重要的语义之一。
timeout 只说明:
不能说明:
因此 timeout 后:
这可以避免旧 transaction 的 late IRQ 修改已经复用给新 transaction 的 slot/request。
11. 统一 TX terminal event
所有 TX transaction 最终只允许进入一次 terminal 状态:
所有来源最终汇入同一处理函数:
例如:
规则:
只能成功一次。
Abort 和 TX_DONE 发生竞争时,第一个 terminal event 完成 request;后续重复/过期 event 只能记录 diagnostics,不能再次 completion、callback 或 free。
12. Async completion callback 第一阶段直接开放
建议不再只做内部预留,第一阶段直接提供 async completion API:
默认 callback context 与 terminal event context 一致。
如果 TX_DONE 来自 ISR,则 callback 也在 ISR context 调用,但必须在
tx.lock已释放以后。因此文档 contract 必须明确 callback 不允许:
需要线程上下文的应用可以在 callback 中自行投递 message/event;CAN Core 本身不引入 workqueue。
13. Lifecycle State
建议公共生命周期:
语义:
不增加
admission_open/submit_refcntLifecycle admission 与 TX enqueue 都在
tx.lock下完成。TX enqueue:
进入 quiesce:
因此一旦状态进入
QUIESCING,新的 TX enqueue 就无法越过状态切换。不需要额外维护:
QUIESCED的严格条件只有以下条件同时满足时进入:
不增加
inflight_count,因为 inflight 可以直接由 mailbox ownership 推导,避免维护重复状态。14. Quiesce / Drain / Full Flush / Abort
建议把这些语义分开。
DRAIN
DROP_QUEUED
ABORT_ALL / Full TX Flush
典型 runtime bitrate reconfiguration:
Hardware abort 仍然是可选 driver capability,例如:
abort(mailbox)调用成功只表示 abort request 已提交,不等价于 transaction 已 terminal;最终仍由TX_ABORTED / TX_DONE / TX_ERROR中的一个终态事件结束 request。15. Bus State 与 Lifecycle 分离
Lifecycle 描述 Framework/controller 操作阶段;Bus State 描述 CAN 协议错误状态。
建议:
合法组合例如:
或:
BUS_OFF不等于STOPPED,否则 runtime recovery、queue policy 和 controller lifecycle 会继续混在一起。第一阶段不新增复杂的
rt_can_bus_status_core或rt_can_statistics。现有rt_can_status继续承担 observability;新的 transaction correctness 不再依赖sndchange/rcvchange等状态字段。16. RX 重构
RX 也需要重构,但不复制 TX Request state machine。
TX 的复杂性来自:
RX 是单向:
因此 RX 保留现有 node pool / freelist / uselist 思路,主要收敛四件事。
16.1 RX 独立 spinlock
rx.lock保护:不再散落使用大范围
rt_hw_local_irq_disable()保护复合状态。16.2 Driver
recvmsg()仍在 lock 外当前
recvmsg()已经由rt_hw_can_isr()在 ISR context 调用,因此新 RX Core 保持这一调用上下文。流程:
HAL/driver callback 和用户 callback 都不在
rx.lock内执行。16.3 RX overflow policy
建议 pool 满时:
不通过覆盖 software queue 中已经入队的旧 frame 来获取空间。
这样 RX software FIFO 的 ownership 和顺序更明确。
16.4 Lifecycle 与 RX close/reconfigure
close/reconfigure 需要保证:
不能先 free RX pool,再允许 ISR 继续访问旧指针。
17. TX/RX 公共 Core
TX/RX 不共用同一个 spinlock。
然后直接嵌入:
不使用:
理由是 core 与 CAN device 生命周期一致,直接嵌入更适合 MCU,也避免额外 malloc 和 ownership。
公共部分只包含:
TX/RX 各自维护自己的 queue、ownership 和 spinlock。
锁顺序固定为:
ISR 永远不会获取
lifecycle_lock。18. 现有 BSP ISR 必须零修改兼容
第一阶段保留:
保留现有 event 编码和 BSP 调用方式。
例如 BSP 现在:
不需要修改。
新的
rt_hw_can_isr()内部改成 compatibility dispatcher:这样新 Core 可以获得 typed internal event,但 BSP 不需要一次性迁移。
19. Kconfig 裁剪
不增加:
CAN Framework 默认同时具备 TX/RX。
只对真正影响代码和 RAM 的功能做裁剪,例如:
条件编译目标:
Request Pool RAM 可以按:
直接评估。
20. Public API 兼容
第一阶段继续保留:
包括现有:
作为 compatibility policy 输入。
内部立即转换为统一 Request:
同时新增 async completion API,但不要求旧应用迁移。
21. 现有字段到新 Core 的迁移关系
_can_int_txblocking 独立路径_can_nonblocking_tx独立路径rt_can_tx_fifomailbox freelisttx_fifo->semstatus.sndchangenb_tx_rbsendmsg_nonblocking()sendmsg(can,msg,box)put_forcespin_lock_irqsaverx.lock保护rt_hw_can_isr()22. 新 Framework 解决的问题对应关系
put_force导致 reorder/overwritemailbox_owner[n] -> requestrt_hw_can_isr()/ public device API23. 分阶段落地建议
这份 Issue 讨论的是整体目标架构,不建议一个 PR 一次全部完成。
建议后续按以下层次拆分:
PR 1:Core synchronization foundation
PR 2:Unified TX Request + Scheduler
PR 3:Terminal / timeout correctness
PR 4:Strict TX order
PR 5:Quiesce / Full Flush / Abort
PR 6:Lifecycle / Bus-off / Recovery
PR 7:RX ownership cleanup and further capability normalization
实际拆分可根据 maintainer review 调整,但建议始终保持每个 PR 的 contract 清晰、可独立验证和可回滚。
24. 希望社区重点评估的细节
总体架构按上述 proposal 讨论,希望重点得到以下反馈:
sendmsg()统一为 hardware submit primitive,并要求支持 non-blocking 的 driver 保证 ISR-safe,这个 BSP contract 是否合适;sendmsg_nonblocking()作为过渡成员保留多久比较合适;can_configure;privmode在统一 scheduler 下继续使用指定 mailbox 时,是否需要保留现有并行语义;RT_CAN_CMD_START/STOP/CONFIGcontrol command 的映射方式;25. 当前建议冻结的架构基线
如果这套方向获得认可,后续实现建议以以下约束作为基线:
这份 proposal 的核心目标是让 CAN Framework 从“多个发送路径 + 隐式 mailbox 状态 + IRQ critical sections”演进为:
如果整体 contract 可以达成共识,再进入具体结构体字段、error code、Kconfig 命名、BSP migration 和 PR implementation,会比继续围绕单个 bug 逐项打补丁更容易维护。