南向硬件协议库
更新时间:2025/10/17
在Gitcode上查看源码

在带外管理业务编写中,除了从CPLD寄存器中读取/写入某个值以外,还有与MCU、芯片等通过复杂协议进行通讯的场景,如SMBus、MCTP等。通过这些复杂协议,BMC可以传输更多的数据,和提供更复杂的带外管理能力。但复杂协议带来的协议管理成本也逐渐上升。openUBMC为了简化复杂协议的管理逻辑,让开发者可以更加聚焦在设备管理业务,开发了一套硬件协议管理库libmgmt_protocol。组件可以通过依赖这个库来简化协议的开发成本。

network_adapter组件中有大量的使用案例,可以参考组件的实现细节。

注意

仅适用于Lua开发框架。

协议支持

名称介绍传输通道传输组件
std_smbus通用smbus协议i2chwproxy
smbus私有smbus协议i2chwproxy
smbus_59025902专用smbus协议i2chwproxy
smbus_postboxNVIDIA SMBus SMBPBI协议i2chwproxy
ncsi_standardNCSI Over MCTP协议的标准协议部分MCTP over PCIE VDMmctpd
ncsi_huaweiNCSI Over MCTP协议的华为OEM协议MCTP over PCIE VDMmctpd
ncsi_mellanoxNCSI Over MCTP协议的Mellanox OEM协议MCTP over PCIE VDMmctpd
nvme_mi_standardnvme-mi Over MCTP协议的标准协议部分MCTP over PCIE VDMmctpd

目录结构

bash
├─ libmgmt_protocol/           -- libmgmt_protocol库
  ├─ init.lua                 -- libmgmt_protocol入口文件,提供对外函数解析协议配置文件
  ├─ protocol/                -- 协议文件夹
  ├─ protocol.lua          -- 协议interface文件,无实际作用
  ├─ ncsi_huawei.lua       -- NSCI Over MCTP 华为OEM协议
  ├─ ncsi_standard.lua     -- NSCI Over MCTP NSCI标准协议
  ├─ ncsi_mellanox.lua     -- NSCI Over MCTP Mellanox OEM协议
  ├─ nvme_mi_standard.lua  -- NCME-MI标准协议
  ├─ smbus_5902.lua        -- 5902专用smbus协议
  ├─ smbus_postbox.lua     -- NVIDIA SMBPBI协议
  ├─ smbus.lua             -- 私有smbus协议
  ├─ std_smbus.lua         -- 标准smbus协议
  ├─ transport/               -- 协议传输层,用于封装协议传输函数
  ├─ hw_communicator.lua   -- 用于创建单次访问/轮询
  ├─ scheduler.lua         -- 用于轮询访问硬件的插件
  ├─ common/
  ├─ init.lua              -- 用于共享常用协议解析工具
  ├─ bs_helper.lua         -- 通用bitstring格式
  ├─ lldp_tlv_parser.lua   -- lldp tlv报文解析函数
  ├─ vdpci_lldp_parser.lua -- vdpci lldp专属报文解析函数

使用方法

由于打包到lualib,所有组件可以直接require

lua
-- require libmgmt_protocol 库
local libmgmt_protocol = require 'libmgmt_protocol'

-- 各组件定义所属硬件的访问协议配置文件
local config_obj = require 'path.to.config.obj'

-- 根据文件创建可访问对象
-- 若配置有问题这里会抛异常
local obj = libmgmt_protocol.device_spec_parser(config_obj)

-- 根据配置文件的具体值获得对应硬件属性值
-- 对于单次访问返回值的属性,需要调用:value()获取具体的值
-- 若属性不存在或者无法获取属性值:value()会返回nil
-- 入参为配置请求数据的补充,若冲突则不会覆盖配置中的请求数据
local on_demand_property = obj:OnDemandProperty({data = 'some data'}):value()

-- 对于轮询访问的属性,返回体为scheduler对象
-- 若请求数据不需要变化,则不用传入任何数据
local on_schedule_property_scheduler = obj:OnScheduleProperty()
-- scheduler不会自动开启,需要手动调用:start()开启,同时会返回第一次获取的数据
local on_schedule_property = on_schedule_property_scheduler:start()
-- scheduler支持订阅数据变更信号,当获取数据不同于scheduler缓存数据时,会发送
-- on_data_change信号和新的数据
on_schedule_property_scheduler.on_data_change:on(function(data)
  print('new data received!')
end)
-- scheduler支持订阅错误信号,当每次轮询获取数据失败(nil)时,会发送on_error信号
on_schedule_property_scheduler.on_error:on(function()
  error('unable to read data from hardware!')
end)
-- scheduler支持更换入参,调用后下一次轮询时便会使用新的参数
on_schedule_property_scheduler:update_params({
  name = 'another_property_name', -- 新属性名
  protocol = 'new protocol', -- 新协议名称(暂不支持加载未使用过的协议)
  request = {...}, -- 协议所需的入参
})
-- scheduler支持更换轮询周期,调用后下一次轮询时便会使用新的参数
on_schedule_property_scheduler:set_period(10) -- 10s轮询一次

-- scheduler会根据配置的轮询周期一直访问数据,在不使用时需要调用:deconstruct()停止轮询访问
on_schedule_property_scheduler:deconstruct()

--对于不存在的属性,调用scheduler的任意函数都不会起效,用于在不确定属性是否存在时
local not_exist_property = obj:NotExistProperty()
not_exist_property:start()                    -- no op
not_exist_property.on_data_change:on(function()
  assert('never reach here')                  -- 此行永远无法到达
end)
not_exist_property.on_error:on(function()
  assert('never reach here')                  -- 此行永远无法到达
end)
not_exist_property:deconstruct()              -- no op

属性访问文件配置

属性访问文件用于配置硬件属性访问所需的固定参数,由协议依赖结构体和多个属性结构体组成

目前仅支持使用Lua进行配置。

lua
local a_network_adapter = {
  protocol_dependencies = {
    smbus = {           -- 所用通过I2C的协议都需要传入ref_chip对象,
      ref_chip = nil,   -- 通过hwproxy对硬件进行访问
      buffer_len = 64,  -- 协议支持的最大字节数
    },
    ncsi_huawei = {     -- 所有通过mctp的协议都需要传入endpoint对象,
      endpoint = nil    -- 通过mctpd对硬件进行访问
    }
  },
  properties = {
    ...
  }
}

return a_network_adapter

配置可以放在组件的任意地方,推荐单独存放,以便于后续迭代。

属性配置

每个属性配置由属性名为键,值由协议名、访问方法、请求体和响应函数构成

lua
properties = {
  ChipTemp = {        -- 属性名,也是属性访问接口, obj:ChipTemp()
    protocol = 'smbus',   -- 协议名,必须是已支持的协议
    action = 'on_schedule',   -- 单次访问还是轮询访问
    period_in_sec = 2,      -- 轮询访问的周期,单位为秒
    request = {             -- 协议所需的请求数据,具体格式由各协议检查
      opcode = 0x3,         -- smbus协议需要的opcode
      expect_data_len = 2   -- smbus需要的返回数据长度
    },
    response = function(data) -- 返回数据解析函数,会去掉协议中协议的部分,data仅包括具体数据
      -- 仅在硬件正常返回响应时才会调用此函数,有错误时不会调用此函数
      local r = bs.new([[<<temp:16>>]]):unpack(data, true) -- 支持使用bitstring进行二进制解包
      return r.temp -- 返回值为obj:ChipTemp()的返回值
    end
  },
  MacAddressNCSI = {
    protocol = 'ncsi_huawei',
    action = 'on_demand',       -- 单次访问,单次访问不需要配置period_in_sec
    request = {
      huawei_cmd_id = 0x1,
      sub_cmd_id = 0x0
    },
    response = function(data)
      local r = bs.new([[<<
        _:8,
        mac_address_count:16/big,
        _:16,                     # libmgmt_protocol提供一些通用的bitstring格式,例如MAC_Address
        mac_addrs:8/MAC_ADDRESS    
      >>]], libmgmt_protocol.common_bs_helper):unpack(data, true) -- 在创建bs的时候直接传入即可
      return r.mac_addrs[1].mac_address
    end
  }
}

NOTE

更复杂配置可参考network_adapter组件仓src/lualib/hardware_config下的配置


smbus协议请求体参数说明

通用smbus、私有smbus、5902专用smbus等i2c相关协议,在request参数实现上较为灵活。详细说明如下:

expect_data_len

  • -1:读请求,响应为不定长度。按最大单帧长度读
  • > 0 :可能是读请求;也可能是写请求,回读写响应。按expect_data_len和最大单帧长度的较小值读
  • = 0或nil:如果有data字段,则为写请求,不回读写响应。按data长度和最大单帧长度的较小值写;如果无data字段,理论上无此场景,按最大单帧长度读或写

batch_write

指定为true时可调用框架的数据批量写入函数,常用于固件升级场景。正常情况下必须指定data


write_without_read

指定为true时表示写请求不回读写响应。正常情况下必须指定data且不指定expect_data_len


offset

指定为-1时表示在多帧写请求的场景下,请求报文的offset字段固定填充0


align_len

若指定该参数,在多帧写请求的场景下,如果最后一帧不足align_len将按align_len补足'\x00'


max_frame_len

可指定某条命令的最大单帧长度


data_object_index

指定被管理芯片的索引,用于单个MCU管理多个其他芯片的场景

参数支持情况详见各协议的request_params_template字段


通用协议解析库

框架提供了通用的bitstring格式和通用的协议响应数据解析函数,用于解析协议返回的响应数据。

通用bitstring格式

通用bitstring格式存于include/libmgmt_protocol/common/init.lua, 使用时调用libmgmt_protocol.common_bs_helper

目前支持的格式:

名称bitstring名称返回格式
Mac地址MAC_ADDRESS{mac_address = '00:11:22:33:44:55'}
Ipv4地址IPV4{ipv4 = '192.168.1.255'}

通用响应数据解析函数

目前支持的函数:

名称介绍入参返回格式
create_array_parser用于解析bitstring数组(单个bitstring定义字符串, 单个数据长度)表数组

LLDP TLV报文解析函数

基于IEEE 802.1ab标准协议解析LLDP TLV报文

无法解析的报文会抛异常,请使用pcall

目前支持的LLDP TLV报文:

TLV ID名称介绍返回格式
1ChassisID解析ChassisID TLV报文{chassis_id:string, chassis_id_subtype:string}
2PortID解析PortID TLV报文{port_id:string, port_id_subtype:string}
3TTL解析TTL TLV报文{ttl:U16}
5SystemName解析SystemName TLV报文{system_name:string}
127OrgSpecific解析OrgSpecific TLV报文,目前用于获取vlanid{vlan_id:U16}

vdpci LLDP报文解析函数

基于华为网卡,通过MCTP的自定义通道从网卡获取的lldp报文专属解析函数

无法解析的报文会抛异常,请使用pcall


新增协议

协议采用继承对象方法,相似协议中相同步骤的部分和差异的部分已继承的方法实现。例如:

  • smbusstd_smbus的传输方法相同,仅发送协议不同,因此smbus继承std_smbus的大部分函数,除了协议拼装函数
  • ncsi_huaweincsi_mellanox都是ncsi_standard的oem命令部分,因此他们继承ncsi_standard的所有函数,仅添加额外拼装和解包步骤

协议必要的函数:

lua
-- @function 构建函数
-- @param    request:table
function protocol:ctor(params)
  -- params为协议配置文件中的protocol_dependencies,由使用组件定义
  -- 例如mctp中需要传入endpoint,smbus需要传入ref_chip和buffer_len
  -- 需要与组件配置文件协同
end

-- @function 传输请求函数
-- @param    request:table
-- @return   binary|boolean|nil
function protocol:send_request(request)
  -- request为协议配置文件中各属性的request请求结构体,包括运行态和配置静态的结合体
  -- 返回响应体的具体数据,若配置文件中配置了响应解析函数,这里的返回数据为响应解析函数的入参
  -- 若请求失败,返回nil
  -- 若无需调用响应解析函数,则返回true代表发送成功
end

-- @function 传输请求函数
-- @param    request:table
-- @return   binary
function protocol:validate_request_params(request)
  -- request为协议配置文件中个属性的request请求结构体
  -- 在解析配置文件时会调用一次,入参为配置文件中的静态数据
  -- 在运行时会再调用一次,入参为运行时传入的动态数据,若为nil则不会调用
  -- 返回true才可继续传输,返回false则不会进行传输
end

添加全新协议

只要满足上述协议必要的函数即可

添加相似协议

通过利用重写基类函数,可以实现相似协议代码共享。


重写拼装和解包函数

NCSI标准协议和NCSI华为协议差异仅在拼装和解包上,NCSI华为协议需要额外的拼装和解包。

此处参考:protocol/ncsi_standard.lua

lua
-- ncsi_standard拼装函数只需要拼装ncsi标准协议的部分
function ncsi_standard:construct_request_data(ctx, request)
  return req_ctx, req_bin
end

-- ncsi_standard解析函数只需要解析ncsi标准协议的部分
function ncsi_standard:unpack_response_data(ctx, rsp_data_bin)
  return ...  
end

-- 将协议拼装和解包拆成独立的函数
function ncsi_standard:send_request(request)
  local req_ctx, req_bin = self:construct_request_data(ctx, request)
  local ok, rsp_data_bin = pcall(self.endpoint.Request, self.endpoint, req_ctx, req_bin, 0)
  return self:unpack_response_data(ctx, rsp_data_bin)
end

需要新增华为oem的协议封装:protocol/ncsi_huawei.lua

lua
local ncsi_huawei = class(ncsi_standard)

-- ncsi_huawei重写基类封装函数,封装自己的部分,再调用基类封装函数
function ncsi_huawei::construct_request_data(ctx, request)
  local request_with_ncsi_huawei_info = ...
  return ncsi_huawei.super.construct_request_data(ctx, request_with_ncsi_huawei_info)
end

-- ncsi_huawei重写基类解析函数,代替基类函数直接返回自己的解析数据
function ncsi_standard:unpack_response_data(ctx, rsp_data_bin)
  return data
end

重写硬件访问函数

smbus_5902跟std_smbus封装方法一致,但是在硬件访问时需要多一些判断

protocol/std_smbus.lua

lua
-- std_smbus硬件访问使用hwproxy的WriteRead函数
function std_smbus:send_and_receive(data, len)
  return pcall(function()
    return self.ref_chip:WriteRead(ctx:new(), data, len)
  end)
end

-- 将硬件访问拆成独立的函数
function std_smbus:send_request(request)
  ...
  local ok, rsp_bin = self:send_and_receive(data, len)
  ...
end

protocol/smbus_5902.lua

lua
local smbus_5902 = class(std_smbus)

-- smbus_5902要求在发送之前检查硬件是否可以访问,所以必须将Write和Read单独发送
-- 可以通过重写基类硬件访问函数,加入自己所需的步骤
function smbus_5902:send_and_receive(data, len)
  if self:check_idle() then
    self:send(data)
    if self:check_idle() then
      return self:receive(len)
    end
  end
end

重写参数检查

smbus私有协议和std_smbus的参数检查逻辑一致,但是可接受的参数不一致

参考:protocol/std_smbus.lua

lua
-- std_smbus允许arg和data参数
local request_params_template<const> = {
  opcode = true,
  expect_data_len = true,
  arg = true,
  data = true
}

-- 在构建函数时存储request_params_template
function std_smbus:ctor()
  self.request_params_template = request_params_template
end

-- 将request_params_template独立出来
function std_smbus:validate_request_params(request)
  for key in pairs(req) do
    if not self.request_params_template[key] then
      return false
    end
  end
  return true
end

参考:protocol/smbus.lua

lua
local smbus = class(std_smbus)

local request_params_template<const> = {
  opcode = true,
  expect_data_len = true
}

-- 在构建时会先调用基类构建函数,再调用子类构建函数
-- 在这里存储request_params_template即可覆盖,在调用validate_request_params时
-- 会使用本地的request_params_template
function smbus:ctor()
  self.request_params_template = request_params_template
end

测试

单元测试以测试独立协议为主,每个协议单元测试必须覆盖协议最基本的函数,包括:

  • protocol:ctor()
  • protocol:validate_request_params()
  • protocol:send_request()

由于传输层是传入的,所以打桩传输对象即可模拟硬件访问