Lely CANopen LSS:主从状态机、Fastscan 与配置流程

在这里插入图片描述

@[toc]

LSS(Layer Setting Services)用于解决一个基础但特殊的问题:当设备还没有有效 Node-ID,或者整个网络需要切换 CAN 位率时,主站不能依赖普通 SDO 通道完成配置,因为 SDO 本身通常已经依赖有效 Node-ID。LSS 因此使用固定的 CAN-ID,并用对象字典 0x1018 的四个 32 位身份字段识别设备。

在 Lely CANopen 中,co_lss_t 同时承载 LSS master 和 LSS slave 两种角色。它不是一组“发送命令后阻塞等待结果”的同步函数,而是一个依附于 co_nmt_tcan_net_tco_dev_t 的事件驱动有限状态机:

1
2
3
4
5
6
7
8
9
10
11
应用提交 LSS 请求

co_lss_*_req() 构造首帧或进入内部状态

can_net_send() 发送 0x7E5

can_recv_t 等待 0x7E4,can_timer_t 负责超时与帧间隔

当前状态的 on_recv / on_time 推进状态机

回到 idle 状态后调用完成回调

从站侧也不是“接收回调里直接改硬件”。lss.c 只负责协议状态、pending Node-ID、pending 位率和应答;真正切换 CAN 控制器位率、保存到非易失存储,必须由应用注册的回调完成。

源码基线与证据边界

本文围绕以下三个文件展开:

1
2
3
include/lely/co/lss.h
include/lely/co/lss.hpp
src/co/lss.c

源码取自 Lely Industries 官方 GitHub 镜像的 master 分支,查阅时间为 2026-07-31。该镜像由项目维护者提供,并与官方 GitLab 仓库同步。为避免把“当前网页版本”静默等同于某个发布版本,本文记录三个文件的 blob SHA:

文件 blob SHA
include/lely/co/lss.h 9ed835cae9f6eba48ea6a3d903dbb2a2306faaa8
include/lely/co/lss.hpp 8f634addb1d7aa04fe70fce5fc4b3f6f4e67ce91
src/co/lss.c 6fdc8c1de73b7d51f5277ec2e00ccf22b0e34b83

协议语义以 CiA 305 v3.0.0 的公开说明为边界;Lely 官方 standards support 页面确认其 LSS 实现覆盖全局/选择性状态切换、Node-ID 与位时序配置、身份与 Node-ID 查询、设备识别和 Fastscan,而“激活位时序”和“保存配置”明确属于 application-specific 部分。

本文没有声称完成目标板构建、CAN 抓包或真实位率切换验证。函数、字段、分支和调用链来自源码;对返回值一致性、接口使用风险和应用边界的说明属于基于源码的工程分析。

三个文件分别负责什么

先建立文件级边界,可以避免把 C++ 包装、C API 和协议状态机混在一起。

文件 层次 主要职责 不负责什么
lss.h 公共 C API 定义回调类型、生命周期接口、master 请求接口、默认超时和固定 CAN-ID 不保存运行状态,不执行状态迁移
lss.hpp C++ 包装层 incomplete_c_type 管理 C 对象,并把 C 回调适配为 callable 或成员函数 不重新实现 LSS,不创建线程,不增加 Future 状态机
lss.c C 协议实现 保存主从运行状态,注册 CAN receiver/timer,处理请求、响应、超时、Slowscan 和 Fastscan 不直接操作 SocketCAN,不直接切换硬件位率,不负责持久化介质

整体关系如下。图中需要观察的是:LSS 服务只消费 can_net_t 提供的 CAN 帧与逻辑时间,操作系统 I/O 仍位于更低层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
应用代码
├─ 直接调用 co_lss_* C API
└─ 通过 lely::COLSS C++ 包装

co_lss_t:事件驱动状态机
├─ co_nmt_t:角色、NMT 状态、pending Node-ID
├─ co_dev_t:0x1018、active Node-ID、位率能力
├─ can_net_t:CAN 分发与逻辑定时
├─ can_recv_t:监听 0x7E5 或 0x7E4
├─ can_timer_t:响应超时与 inhibit 间隔
├─ rate_ind:应用负责切换 CAN 位率
└─ store_ind:应用负责持久化参数

io_can_net / 平台桥接

SocketCAN 或目标平台 CAN 驱动

co_lss_t 并不拥有独立线程。只要外部持续推进 can_net_t 的接收和时间,LSS 状态机就能工作;如果 can_net_t 没有收到帧或没有推进时间,master 的响应回调和超时也不会凭空发生。

LSS 的固定通信通道与 128 位地址

CiA 305 将设备分为 LSS manager 和 LSS server。Lely 的 API 仍使用 master/slave 命名,但协议方向一致:

方向 CAN-ID Lely 宏展开
LSS master → LSS slave 0x7E5 CO_LSS_CANID(1)
LSS slave → LSS master 0x7E4 CO_LSS_CANID(0)

lss.h 中的宏非常直接:

1
#define CO_LSS_CANID(master) (0x7e4 + !!(master))

这两条固定通道不依赖普通 Node-ID,因此即使节点尚未配置、active Node-ID 为 0xFF,仍然可以参与 LSS 识别和配置。

LSS address 由对象字典 0x1018 Identity object 的四个 32 位数构成:

1
2
3
4
0x1018:01  Vendor-ID
0x1018:02 Product code
0x1018:03 Revision number
0x1018:04 Serial number

总长度为 128 bit。选择性切换、设备识别、Slowscan 和 Fastscan 最终都围绕这四个值工作。lss.c 并没有另建一份独立身份数据库,而是通过 co_dev_find_obj(dev, 0x1018)co_obj_get_val_u32() 读取本地对象字典。

lss.h:公共接口按角色分为三组

生命周期和公共属性

co_lss_create() 接收一个 co_nmt_t *。LSS 不要求应用再次传入 can_net_tco_dev_t,因为初始化阶段会从 NMT 对象中取得它们:

1
2
3
4
5
6
7
8
co_lss_create(nmt)

__co_lss_init(lss, nmt)
├─ co_nmt_get_net(nmt)
├─ co_nmt_get_dev(nmt)
├─ can_recv_create()
├─ master build: can_timer_create()
└─ co_lss_start()

公开生命周期接口包括:

1
2
3
4
5
6
co_lss_t *co_lss_create(co_nmt_t *nmt);
void co_lss_destroy(co_lss_t *lss);

int co_lss_start(co_lss_t *lss);
void co_lss_stop(co_lss_t *lss);
int co_lss_is_stopped(const co_lss_t *lss);

创建成功后服务已经启动,不需要再额外调用一次 co_lss_start()。重复调用 start() 会直接返回成功,stop() 也具有幂等检查。

从站需要应用接管的两个动作

Lely 把两项与平台强相关的动作留给应用:

1
2
3
4
5
6
typedef void co_lss_rate_ind_t(
co_lss_t *lss, co_unsigned16_t rate, int delay, void *data);

typedef int co_lss_store_ind_t(
co_lss_t *lss, co_unsigned8_t id,
co_unsigned16_t rate, void *data);

二者的职责不同:

回调 触发命令 应用职责
rate_ind Activate bit timing parameters,CS 0x15 停止发送、等待切换延时、重配 CAN 控制器、再等待静默时间
store_ind Store configuration,CS 0x17 把 pending Node-ID 和位率写入 Flash、EEPROM 或其他持久介质

协议栈只把请求和参数交给应用,不知道 Linux ip link、MCU CAN 寄存器、Bootloader 参数区或 Flash 擦写策略。

Master 的异步请求和完成回调

Master 请求接口可以再分成五类:

类别 代表接口 完成方式
状态切换 co_lss_switch_req()co_lss_switch_sel_req() 全局切换无响应;选择性切换回调返回 CS 或超时
参数配置 co_lss_set_id_req()co_lss_set_rate_req()co_lss_switch_rate_req()co_lss_store_req() 配置/保存返回错误响应;激活位率无普通确认
参数查询 co_lss_get_vendor_id_req() 等、co_lss_get_id_req() 回调返回身份值或 Node-ID
设备识别 co_lss_id_slave_req()co_lss_id_non_cfg_slave_req() 回调返回匹配命令或超时
聚合扫描 co_lss_slowscan_req()co_lss_fastscan_req() 内部多轮状态机结束后回调返回完整 LSS address

这些函数的返回值只表示“请求是否成功提交”,不表示远端操作已经完成。远端成功、协议错误和超时由回调异步报告。

Master 还暴露两个运行状态:

1
2
int co_lss_is_master(const co_lss_t *lss);
int co_lss_is_idle(const co_lss_t *lss);

所有需要响应的 master 请求都先检查:

1
2
3
当前对象必须是 LSS master
并且
当前状态必须是 idle

否则设置 ERRNUM_PERM 并返回 -1。这意味着同一个 co_lss_t 同时只能执行一个 master 请求;如果上层需要连续执行“扫描 → 改 ID → 保存”,应在前一个完成回调中提交下一步,而不是并发调用。

lss.hpp:薄封装,而不是另一套 LSS 实现

lss.hpp 中的核心类型是:

1
class COLSS : public incomplete_c_type<__co_lss>;

它通过 c_type_traits<__co_lss> 把 C 生命周期函数接入通用不完整类型包装:

1
2
3
4
alloc() → __co_lss_alloc()
init() → __co_lss_init()
fini() → __co_lss_fini()
free() → __co_lss_free()

COLSS 的成员函数基本都是一行转发:

1
2
3
4
5
6
7
8
9
int start() noexcept {
return co_lss_start(this);
}

int setIdReq(co_unsigned8_t id,
co_lss_err_ind_t* ind,
void* data) noexcept {
return co_lss_set_id_req(this, id, ind, data);
}

模板重载使用 c_obj_callc_mem_call,允许把普通 callable 对象或成员函数适配为 C 回调。它解决的是调用形式与对象生命周期问题,不改变以下底层事实:

  • 仍然只有一个 co_lss_t 状态机;
  • 仍然通过 C 回调完成请求;
  • 仍然需要外部驱动 can_net_t
  • 没有额外的线程、Future 或队列;
  • C++ 方法返回 int 时,语义仍是 C API 的“是否成功提交”。

因此,读 lss.hpp 时不必重复追一遍协议。先理解 lss.c 的状态和回调时机,再回来看模板适配即可。

co_lss_t 保存了哪些运行状态

src/co/lss.c 中的 struct __co_lss 可以按四组理解。

依赖对象

1
2
3
4
nmt   → co_nmt_t
net → can_net_t
dev → co_dev_t
state → 当前 co_lss_state_t

NMT 决定本节点角色和 pending Node-ID;device 提供 active Node-ID、0x1018 和位率能力;network 提供发送、接收分发和逻辑时间。

收发和定时资源

1
2
recv  → 一个 can_recv_t
timer → master build 下的一个 can_timer_t

同一个 receiver 会根据角色和请求阶段切换监听方向:

1
2
slave waiting/configuration:监听 0x7E5
master waiting response:监听 0x7E4

Master 的 timer 同时承担两类时间:

  1. 多帧请求之间的 inhibit 间隔;
  2. 等待从站响应的 timeout。

默认值来自 lss.h

1
2
#define LELY_CO_LSS_INHIBIT 10   // 10 × 100 us = 1 ms
#define LELY_CO_LSS_TIMEOUT 100 // 100 ms

运行时可以通过 co_lss_set_inhibit()co_lss_set_timeout() 修改。timeout 设为 0 表示关闭响应超时。

扫描与响应上下文

Master build 还保存:

1
2
3
4
5
6
7
8
9
next       多帧请求的下一帧序号
lo / hi Slowscan 地址范围
id 当前识别到或正在构造的 LSS address
mask Fastscan 已知位掩码
bitchk 当前检查的 bit 位置
lsssub 当前检查第几个 32-bit 身份字段
cs 期望收到的 command specifier
err/spec 配置响应错误码
lssid/nid 查询结果

这些字段表明 Slowscan 和 Fastscan 不是外部循环反复调用单帧接口,而是 co_lss_t 内部持久化上下文的聚合异步操作。

完成回调

每一类响应都有独立回调和 void *data

1
2
3
4
5
cs_ind      命令响应
err_ind 配置错误响应
lssid_ind 身份字段查询
nid_ind Node-ID 查询
scan_ind Slowscan/Fastscan 结果

状态离开时先停止 receiver/timer,再调用相应回调。由于 co_lss_enter() 会先把当前状态切到目标状态,再调用旧状态的 on_leave(),回调执行时 master 通常已经回到 co_lss_wait_state,上层可以在回调内安全地提交下一项串行请求。

状态机为什么不用枚举加大 switch

Lely 为每个状态定义一个函数指针集合:

1
2
3
4
5
6
7
8
struct __co_lss_state {
co_lss_state_t *(*on_enter)(co_lss_t *lss);
co_lss_state_t *(*on_recv)(co_lss_t *lss,
const struct can_msg *msg);
co_lss_state_t *(*on_time)(co_lss_t *lss,
const struct timespec *tp);
void (*on_leave)(co_lss_t *lss);
};

事件只有当前状态关心的函数会被调用。co_lss_enter() 支持“瞬时状态”:某个 on_enter() 可以直接返回下一个状态,不需要等待新的 CAN 帧或 timer 事件。

1
2
3
4
5
6
7
8
9
10
11
12
外部事件
├─ CAN frame → co_lss_emit_recv()
└─ timeout → co_lss_emit_time()

当前 state handler

返回 next state

co_lss_enter()
├─ previous.on_leave()
├─ next.on_enter()
└─ 若 on_enter 又返回 next,继续迁移

这种结构特别适合选择性切换和扫描:它们有大量“发送一帧 → 等间隔 → 再发一帧”“收到响应 → 先回 idle → 再执行用户回调”的过渡步骤。如果全部放进一个大 switch,状态与事件组合会迅速膨胀。

创建后如何确定 master 或 slave 角色

__co_lss_init() 并不把角色永久固定在构造参数中。初始化完成后调用 co_lss_start(),进入 co_lss_wait_state;该状态的 entry 函数执行:

1
lss->master = co_nmt_is_master(lss->nmt)

只有 NMT master 才能成为 LSS master:

1
2
3
4
co_lss_wait_state
├─ NMT 是 master → 留在 master idle 状态
└─ NMT 不是 master → 进入 co_lss_wait_slave_state
└─ receiver 监听 0x7E5

因此,co_lss_t 的角色来自它依附的 NMT 对象,而不是由 co_lss_create() 的单独参数指定。每次重新进入 wait 状态都会重新判断角色。

Slave:waiting 与 configuration 两个主状态

LSS slave 主要在两个协议状态之间切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
co_lss_start

进入 co_lss_wait_state,检查 NMT 角色

LSS waiting:监听 0x7E5
├─ Switch global mode=1 ───────────────┐
├─ Selective 四段身份完全匹配 ────────┤
└─ Fastscan 完成最后一段 ──────────────┤

LSS configuration
├─ 配置 pending Node-ID
├─ 配置 pending 位率
├─ 查询身份或 Node-ID
├─ 应用持久化配置
└─ 应用激活新位率

Switch global mode=0

LSS waiting

Waiting 状态只接受用于发现或进入 configuration 的命令;Node-ID、位率、保存和查询等配置命令只在 configuration 状态处理。这个状态隔离是网络安全性的关键:主站必须保证最终只有目标设备进入 configuration,否则广播式配置命令可能同时作用于多个节点。

全局切换

CS 0x04 携带 mode:

1
2
mode = 0 → waiting
mode = 1 → configuration

全局切换会影响所有能接收该帧的 LSS slave。它适用于已确认网络中只有一个待配置节点,或者确实需要所有节点同步改变 LSS 状态的场景;它不能替代设备唯一选择。

选择性切换

选择性切换由四帧组成:

1
2
3
4
0x40 Vendor-ID
0x41 Product code
0x42 Revision number
0x43 Serial number

从站用 lss->cs 记录下一步期望值。任一字段或顺序不匹配就清零匹配进度;只有四个字段全部匹配,才发送 CS 0x44 响应并进入 configuration。

下面的时序图同时展示了选择性切换与后续 Node-ID 配置。图中“请求完成”不等于新 Node-ID 已成为 active ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
应用                 LSS master              CAN 总线              LSS slave / NMT
│ │ │ │
│ switchSelReq(address) │ │ │
├──────────────────────>│ │ │
│ ├─ 0x7E5 0x40 Vendor ─>│────────────────────────>│
│ ├─ 0x7E5 0x41 Product ->│───────────────────────>│
│ ├─ 0x7E5 0x42 Revision >│────────────────────────>│
│ ├─ 0x7E5 0x43 Serial ─>│────────────────────────>│
│ │ │<─ 0x7E4 0x44 ───────────┤
│<─ cs_ind(0x44) ───────┤<──────────────────────│ │
│ │ │ │
│ setIdReq(new_id) │ │ │
├──────────────────────>│─ 0x7E5 0x11 new_id ─>│────────────────────────>│
│ │ │ ├─ co_nmt_set_id()
│ │ │<─ 0x7E4 0x11 err/spec ──┤
│<─ err_ind(...) ───────┤<──────────────────────│ │

Master 在发送四段选择性请求时,会用 inhibit 控制相邻帧间隔。每发完一段非末帧,源码停止接收器,并把 timer 设置为:

1
当前 can_net 时间 + 100 us × inhibit

默认 inhibit=10,即约 1 ms。发送最后一帧后,才切换 receiver 到 0x7E4 并启动响应 timeout。

Slave 如何配置 pending Node-ID

Configuration 状态收到 CS 0x11 后调用:

1
co_nmt_set_id(lss->nmt, msg->data[1]);

若 Node-ID 非法,源码保留原错误上下文,并在响应 byte1 返回错误码 1。成功时响应错误码为 0

这里修改的是 NMT 管理的 pending Node-ID,不是立即重写 co_dev_t 的 active Node-ID。源码在处理“查询 Node-ID”时明确区分二者:

1
2
3
4
5
NMT 处于 BOOTUP / RESET_NODE / RESET_COMM
→ 返回 co_nmt_get_id(),即 pending Node-ID

其他 NMT 状态
→ 返回 co_dev_get_id(),即 active Node-ID

Lely 官方命令行教程也展示了相同行为:执行 lss_set_node 后,新 ID 仍处于 pending;退出 LSS configuration 并执行 NMT reset communication 后,节点才以新 Node-ID 发送 boot-up。

这意味着完整链路至少有三步:

1
2
3
4
5
6
7
LSS configure node-ID
↓ pending
可选:LSS store configuration
↓ 持久化
NMT reset communication / reset node

新 Node-ID 成为运行期 active ID

Slave 如何配置和激活位率

CS 0x13 只配置 pending 位率。Master API 接受 kbit/s,并映射为 CiA 305 的 table index:

rate 参数 table index
1000 0
800 1
500 2
250 3
125 4
50 6
20 7
10 8
0,自动检测 9

从站收到请求后先检查:

  1. selector byte 必须为 0
  2. 必须已经注册 rate_ind
  3. co_dev_get_baud() 对应能力位必须支持目标速率。

通过后只调用:

1
co_dev_set_rate(dev, rate)

这仍然没有操作 CAN 控制器。

CS 0x15 才是 Activate bit timing parameters。它携带一个 16 位 delay,源码直接调用:

1
rate_ind(lss, co_dev_get_rate(dev), delay, rate_data);

应用必须自行实现两个静默区间和硬件重配置。协议栈无法知道:

  • 当前平台怎样停止 CAN TX;
  • 是否需要先等待发送邮箱清空;
  • SocketCAN 使用 ip link、netlink 还是外部管理进程;
  • MCU 是否需要进入 init mode;
  • 时钟树、采样点、SJW 和预分频器怎样配置;
  • 自动位率检测由哪一层执行。

此外,激活位率请求没有普通的 LSS 确认响应。主站和所有目标节点必须按同一延时计划切换,否则主站可能在新旧位率边界失去通信。

保存配置为什么必须由应用实现

CS 0x17 触发 store_ind。Lely 把当前 pending Node-ID 和 pending rate 作为参数传给应用:

1
2
3
4
store_ind(lss,
co_nmt_get_id(nmt),
co_dev_get_rate(dev),
store_data);

响应语义由源码固定:

条件 LSS error code
回调成功 0
未注册 store_ind,不支持保存 1
回调返回 -1,存储失败 2

协议栈不会为应用选择 Flash 分区,也不会定义掉电原子性。实际产品应由持久化层决定:

1
2
3
4
5
6
参数格式和版本
CRC 或校验策略
双备份或 journal
擦写寿命
掉电恢复
何时把 pending 参数提交为 active 配置

这些属于设备实现,不是 CiA 305 帧级状态机的一部分。

Master 请求的统一执行模型

除全局状态切换和激活位率外,大多数 master 请求采用相同模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
检查 is_master && is_idle

构造 8-byte LSS 请求帧,CAN-ID=0x7E5

can_net_send()

co_lss_init_ind(expected_cs)
├─ receiver 监听 0x7E4
└─ timer 启动 timeout

收到 expected_cs 或超时

停止 receiver/timer

回到 wait/idle

调用用户回调

co_lss_init_req() 总是把帧初始化为 8 字节,并把 byte0 写成 command specifier。未使用字节保持初始化值。

超时的表达方式不是单独的错误枚举,而是把回调中的 cs 置为 0

1
2
cs != 0 → 收到匹配响应
cs == 0 → timeout 或未找到节点

因此回调必须先检查 cs,再解释 errid 或扫描结果指针。

co_lss_abort_req() 的函数体只执行一次状态迁移:co_lss_enter(lss, co_lss_wait_state)。它没有在函数内统一调用 can_timer_stop()can_recv_stop();资源清理和回调是否发生,取决于被中止状态是否定义了 on_leave()。例如错误响应、身份查询和 Node-ID 查询状态的 leave handler 会停止 receiver/timer 并调用回调,而部分多帧发送状态没有统一的 leave handler。调用方不能把 abort 理解成“所有请求都以同一种方式静默取消”,应按目标源码版本逐项确认正在使用的请求状态。若需要彻底停用服务,co_lss_stop() 才会显式停止 timer 和 receiver。

设备识别与 Slowscan

Identify remote slave

co_lss_id_slave_req() 接受上下界:

1
2
3
4
Vendor-ID      必须精确相等
Product code 必须精确相等
Revision 位于 [lo, hi]
Serial number 位于 [lo, hi]

Master 依次发送 CS 0x46..0x4B。从站逐段检查,全部匹配后发送 0x4F。这项服务回答的是“指定范围内是否存在匹配节点”,并不直接返回完整身份值。

Slowscan

co_lss_slowscan_req() 是建立在 identify remote slave 和 switch state selective 之上的聚合流程:

  1. 先确认给定范围内至少存在一个匹配节点;
  2. 在 revision 和 serial number 范围内执行二分搜索;
  3. 每轮根据“是否有响应”收缩上下界;
  4. 找到唯一地址后再执行选择性切换;
  5. 成功时把目标节点留在 LSS configuration 状态。

源码计算中点时显式避免整数溢出:

1
mid = low + (high - low) / 2

收到第一条响应后并不立刻结束本轮,而是进入 wait 状态,忽略后续响应直到 timeout。这样可以给所有匹配节点完整响应窗口,再统一更新搜索边界。

Lely 官方控制工具把 _lss_slowscan 标为 Lely-specific 聚合命令。它不是新的 CAN 帧协议,而是对标准 identify 和 selective switch 的本地编排。

Fastscan 如何逐位恢复 128 位身份

Fastscan 的核心不是“读取 0x1018”,而是反复询问:

1
网络中是否有设备的当前身份前缀与这个候选值匹配?

Master 不需要预先知道 Node-ID。它维护四个 32 位字段、一个 mask、当前字段序号 lsssub 和当前 bit bitchk

idmask 的语义

co_lss_fastscan_req() 接收可选的 idmask

  • id 保存已知位的值;
  • mask 中 bit 为 1 表示该位已经知道,扫描时跳过;
  • mask 为 0 的 bit 需要逐位探测。

源码会先把未知位清零:

1
id.field &= mask.field

随后从 Vendor-ID 的 bit31 开始,依次处理 Product code、Revision 和 Serial number,直到四个字段全部确定。

响应与超时怎样表示一个 bit

Lely 的扫描状态机采用以下判断:

1
2
3
发送当前候选前缀
├─ 收到 0x4F 响应 → 当前候选前缀存在,当前 bit 保持 0
└─ timeout → 当前 bit 设为 1

每次确定一个 bit 后,都把 mask 对应位置设为 1,再继续下一个未知 bit。流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
发送 Fastscan reset(bitchk = 0x80)

是否收到 0x4F?
├─ 否:未找到可扫描节点,scan_ind(cs=0)
└─ 是:从 Vendor-ID bit31 开始选择最高未知 bit

发送候选前缀(0x7E5,CS 0x51)

timeout 前是否收到 0x4F?
├─ 是:当前 bit 保持 0
└─ 否:当前 bit 设置为 1

mask 对应 bit 置 1

当前 32-bit 字段是否完成?
├─ 否:继续下一个未知 bit
└─ 是:切换到下一个身份字段

四个字段是否全部完成?
├─ 否:bitchk 重新从 31 开始
└─ 是:目标从站进入 configuration,返回完整 LSS address

从站侧 co_lss_fastscan() 会用对象 0x1018 的真实值检查候选前缀。完成最后一个字段后,lssnext 回绕,从站进入 configuration 状态,并仍发送 0x4F 表示匹配。

当所有 128 bit 都未知且 timeout 使用默认 100 ms 时,扫描的主要时延由逐位响应窗口决定。Lely 官方教程的全零 mask 示例会检查 128 bit,典型用时略多于 13 秒;已知位越多,mask 跳过的 bit 越多,扫描越快。

多个节点响应时的处理

Fastscan 和 Slowscan 都可能在一轮中收到多个从站响应。源码在收到第一条匹配响应后进入 wait 状态,并忽略后续响应,直到本轮 timeout 到期再更新结果。这不是为了丢弃错误,而是把“至少有一个匹配节点”压缩成布尔结果。

最终能否唯一定位设备,取决于网络中 0x1018:01..04 是否唯一。若多个节点拥有完全相同的 128 位 LSS address,Fastscan 无法凭相同身份把它们区分为两个设备;它们可能同时进入 configuration。协议栈不能替代制造阶段的序列号唯一性管理。

非配置节点识别的条件比 active ID 无效更严格

收到 CS 0x4C 时,从站只有同时满足以下条件才返回 0x50

1
2
3
4
5
co_dev_get_id(dev) == 0xFF
并且
co_nmt_get_id(nmt) == 0xFF
并且
NMT 位于 BOOTUP / RESET_NODE / RESET_COMM

也就是说,源码同时检查 active Node-ID 与 pending Node-ID,还限制在 NMT 初始化阶段。仅把其中一个值设为 0xFF,或者节点已经进入正常运行状态,并不足以让它响应“identify non-configured slave”。

把完整运行链压缩成一张图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
NMT 决定本节点是 LSS master 还是 slave

co_lss_start() 进入 wait
├─ master:等待应用提交一个异步请求
│ ↓
│ 发送 0x7E5
│ ↓
│ receiver 监听 0x7E4 + timer 计时
│ ↓
│ 收到响应或 timeout
│ ↓
│ 回到 idle,再调用用户回调

└─ slave:receiver 长期监听 0x7E5

waiting 状态只处理发现和状态选择

唯一目标进入 configuration

修改 pending Node-ID / pending rate

rate_ind 负责真实位率切换
store_ind 负责真实持久化

NMT reset communication 使 pending Node-ID 生效

理解 Lely LSS 的关键不是记住所有 command specifier,而是抓住四个边界:

  • lss.h 定义异步契约,返回值只表示请求是否提交;
  • lss.hpp 只包装 C API,不改变协议和调度模型;
  • lss.c 用 receiver、timer 和函数指针状态机实现主从流程;
  • 位率激活、持久化和最终 NMT 复位属于应用与系统集成责任。

参考资料

  1. Lely Core 官方 GitLab 仓库:https://gitlab.com/lely_industries/lely-core
  2. Lely Core 官方 GitHub 镜像:https://github.com/lely-industries/lely-core
  3. include/lely/co/lss.hhttps://github.com/lely-industries/lely-core/blob/master/include/lely/co/lss.h
  4. include/lely/co/lss.hpphttps://github.com/lely-industries/lely-core/blob/master/include/lely/co/lss.hpp
  5. src/co/lss.chttps://github.com/lely-industries/lely-core/blob/master/src/co/lss.c
  6. CiA 305 Layer Setting Services:https://www.can-cia.org/can-knowledge/cia-305-layer-setting-services-lss
  7. Lely CANopen standards support:https://opensource.lely.com/canopen/docs/standards/
  8. Lely CANopen build configuration:https://opensource.lely.com/canopen/docs/configuration/
  9. Lely CANopen finite-state machines:https://opensource.lely.com/canopen/docs/fsm/
  10. Lely CANopen command-line tutorial:https://opensource.lely.com/canopen/docs/cmd-tutorial/
  11. Lely CANopen control tool:https://opensource.lely.com/canopen/docs/coctl/