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_t、can_net_t 和 co_dev_t 的事件驱动有限状态机:
1 | 应用提交 LSS 请求 |
从站侧也不是“接收回调里直接改硬件”。lss.c 只负责协议状态、pending Node-ID、pending 位率和应答;真正切换 CAN 控制器位率、保存到非易失存储,必须由应用注册的回调完成。
源码基线与证据边界
本文围绕以下三个文件展开:
1 | include/lely/co/lss.h |
源码取自 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 | 应用代码 |
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 |
这两条固定通道不依赖普通 Node-ID,因此即使节点尚未配置、active Node-ID 为 0xFF,仍然可以参与 LSS 识别和配置。
LSS address 由对象字典 0x1018 Identity object 的四个 32 位数构成:
1 | 0x1018:01 Vendor-ID |
总长度为 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_t 或 co_dev_t,因为初始化阶段会从 NMT 对象中取得它们:
1 | co_lss_create(nmt) |
公开生命周期接口包括:
1 | co_lss_t *co_lss_create(co_nmt_t *nmt); |
创建成功后服务已经启动,不需要再额外调用一次 co_lss_start()。重复调用 start() 会直接返回成功,stop() 也具有幂等检查。
从站需要应用接管的两个动作
Lely 把两项与平台强相关的动作留给应用:
1 | typedef void co_lss_rate_ind_t( |
二者的职责不同:
| 回调 | 触发命令 | 应用职责 |
|---|---|---|
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 | int co_lss_is_master(const co_lss_t *lss); |
所有需要响应的 master 请求都先检查:
1 | 当前对象必须是 LSS master |
否则设置 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 | alloc() → __co_lss_alloc() |
COLSS 的成员函数基本都是一行转发:
1 | int start() noexcept { |
模板重载使用 c_obj_call 和 c_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 | nmt → co_nmt_t |
NMT 决定本节点角色和 pending Node-ID;device 提供 active Node-ID、0x1018 和位率能力;network 提供发送、接收分发和逻辑时间。
收发和定时资源
1 | recv → 一个 can_recv_t |
同一个 receiver 会根据角色和请求阶段切换监听方向:
1 | slave waiting/configuration:监听 0x7E5 |
Master 的 timer 同时承担两类时间:
- 多帧请求之间的 inhibit 间隔;
- 等待从站响应的 timeout。
默认值来自 lss.h:
1 |
运行时可以通过 co_lss_set_inhibit() 和 co_lss_set_timeout() 修改。timeout 设为 0 表示关闭响应超时。
扫描与响应上下文
Master build 还保存:
1 | next 多帧请求的下一帧序号 |
这些字段表明 Slowscan 和 Fastscan 不是外部循环反复调用单帧接口,而是 co_lss_t 内部持久化上下文的聚合异步操作。
完成回调
每一类响应都有独立回调和 void *data:
1 | cs_ind 命令响应 |
状态离开时先停止 receiver/timer,再调用相应回调。由于 co_lss_enter() 会先把当前状态切到目标状态,再调用旧状态的 on_leave(),回调执行时 master 通常已经回到 co_lss_wait_state,上层可以在回调内安全地提交下一项串行请求。
状态机为什么不用枚举加大 switch
Lely 为每个状态定义一个函数指针集合:
1 | struct __co_lss_state { |
事件只有当前状态关心的函数会被调用。co_lss_enter() 支持“瞬时状态”:某个 on_enter() 可以直接返回下一个状态,不需要等待新的 CAN 帧或 timer 事件。
1 | 外部事件 |
这种结构特别适合选择性切换和扫描:它们有大量“发送一帧 → 等间隔 → 再发一帧”“收到响应 → 先回 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 | co_lss_wait_state |
因此,co_lss_t 的角色来自它依附的 NMT 对象,而不是由 co_lss_create() 的单独参数指定。每次重新进入 wait 状态都会重新判断角色。
Slave:waiting 与 configuration 两个主状态
LSS slave 主要在两个协议状态之间切换:
1 | co_lss_start |
Waiting 状态只接受用于发现或进入 configuration 的命令;Node-ID、位率、保存和查询等配置命令只在 configuration 状态处理。这个状态隔离是网络安全性的关键:主站必须保证最终只有目标设备进入 configuration,否则广播式配置命令可能同时作用于多个节点。
全局切换
CS 0x04 携带 mode:
1 | mode = 0 → waiting |
全局切换会影响所有能接收该帧的 LSS slave。它适用于已确认网络中只有一个待配置节点,或者确实需要所有节点同步改变 LSS 状态的场景;它不能替代设备唯一选择。
选择性切换
选择性切换由四帧组成:
1 | 0x40 Vendor-ID |
从站用 lss->cs 记录下一步期望值。任一字段或顺序不匹配就清零匹配进度;只有四个字段全部匹配,才发送 CS 0x44 响应并进入 configuration。
下面的时序图同时展示了选择性切换与后续 Node-ID 配置。图中“请求完成”不等于新 Node-ID 已成为 active ID。
1 | 应用 LSS master CAN 总线 LSS slave / NMT |
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 | NMT 处于 BOOTUP / RESET_NODE / RESET_COMM |
Lely 官方命令行教程也展示了相同行为:执行 lss_set_node 后,新 ID 仍处于 pending;退出 LSS configuration 并执行 NMT reset communication 后,节点才以新 Node-ID 发送 boot-up。
这意味着完整链路至少有三步:
1 | LSS configure node-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 |
从站收到请求后先检查:
- selector byte 必须为
0; - 必须已经注册
rate_ind; 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 | store_ind(lss, |
响应语义由源码固定:
| 条件 | LSS error code |
|---|---|
| 回调成功 | 0 |
未注册 store_ind,不支持保存 |
1 |
回调返回 -1,存储失败 |
2 |
协议栈不会为应用选择 Flash 分区,也不会定义掉电原子性。实际产品应由持久化层决定:
1 | 参数格式和版本 |
这些属于设备实现,不是 CiA 305 帧级状态机的一部分。
Master 请求的统一执行模型
除全局状态切换和激活位率外,大多数 master 请求采用相同模式:
1 | 检查 is_master && is_idle |
co_lss_init_req() 总是把帧初始化为 8 字节,并把 byte0 写成 command specifier。未使用字节保持初始化值。
超时的表达方式不是单独的错误枚举,而是把回调中的 cs 置为 0:
1 | cs != 0 → 收到匹配响应 |
因此回调必须先检查 cs,再解释 err、id 或扫描结果指针。
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 | Vendor-ID 必须精确相等 |
Master 依次发送 CS 0x46..0x4B。从站逐段检查,全部匹配后发送 0x4F。这项服务回答的是“指定范围内是否存在匹配节点”,并不直接返回完整身份值。
Slowscan
co_lss_slowscan_req() 是建立在 identify remote slave 和 switch state selective 之上的聚合流程:
- 先确认给定范围内至少存在一个匹配节点;
- 在 revision 和 serial number 范围内执行二分搜索;
- 每轮根据“是否有响应”收缩上下界;
- 找到唯一地址后再执行选择性切换;
- 成功时把目标节点留在 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。
id 和 mask 的语义
co_lss_fastscan_req() 接收可选的 id 和 mask:
id保存已知位的值;mask中 bit 为1表示该位已经知道,扫描时跳过;- mask 为
0的 bit 需要逐位探测。
源码会先把未知位清零:
1 | id.field &= mask.field |
随后从 Vendor-ID 的 bit31 开始,依次处理 Product code、Revision 和 Serial number,直到四个字段全部确定。
响应与超时怎样表示一个 bit
Lely 的扫描状态机采用以下判断:
1 | 发送当前候选前缀 |
每次确定一个 bit 后,都把 mask 对应位置设为 1,再继续下一个未知 bit。流程如下:
1 | 发送 Fastscan reset(bitchk = 0x80) |
从站侧 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 | co_dev_get_id(dev) == 0xFF |
也就是说,源码同时检查 active Node-ID 与 pending Node-ID,还限制在 NMT 初始化阶段。仅把其中一个值设为 0xFF,或者节点已经进入正常运行状态,并不足以让它响应“identify non-configured slave”。
把完整运行链压缩成一张图
1 | NMT 决定本节点是 LSS master 还是 slave |
理解 Lely LSS 的关键不是记住所有 command specifier,而是抓住四个边界:
lss.h定义异步契约,返回值只表示请求是否提交;lss.hpp只包装 C API,不改变协议和调度模型;lss.c用 receiver、timer 和函数指针状态机实现主从流程;- 位率激活、持久化和最终 NMT 复位属于应用与系统集成责任。
参考资料
- Lely Core 官方 GitLab 仓库:https://gitlab.com/lely_industries/lely-core
- Lely Core 官方 GitHub 镜像:https://github.com/lely-industries/lely-core
include/lely/co/lss.h:https://github.com/lely-industries/lely-core/blob/master/include/lely/co/lss.hinclude/lely/co/lss.hpp:https://github.com/lely-industries/lely-core/blob/master/include/lely/co/lss.hppsrc/co/lss.c:https://github.com/lely-industries/lely-core/blob/master/src/co/lss.c- CiA 305 Layer Setting Services:https://www.can-cia.org/can-knowledge/cia-305-layer-setting-services-lss
- Lely CANopen standards support:https://opensource.lely.com/canopen/docs/standards/
- Lely CANopen build configuration:https://opensource.lely.com/canopen/docs/configuration/
- Lely CANopen finite-state machines:https://opensource.lely.com/canopen/docs/fsm/
- Lely CANopen command-line tutorial:https://opensource.lely.com/canopen/docs/cmd-tutorial/
- Lely CANopen control tool:https://opensource.lely.com/canopen/docs/coctl/









