3. BACnet协议入门:协议栈架构、对象模型与服务初探

好,咱们今天聊聊BACnet。说实话,在暖通空调通信领域混了这么多年,BACnet是我见过最「懂行」的协议之一。它不像Modbus那样简单粗暴,也不像KNX那样封闭。BACnet的设计思路,说白了就是——让不同厂家的设备能坐在一起开会。

我记得刚入行那会儿,接手一个项目,现场有霍尼韦尔的DDC、西门子的控制器、还有国产的冷机群控系统。甲方要求统一监控。我当时头都大了。后来发现,只要大家都支持BACnet,这事儿就简单多了。嗯,这就是BACnet的价值所在。

3.1 BACnet协议栈架构

BACnet的协议栈,你可以把它想象成一栋楼。每一层各司其职,上层不用管下层怎么干活。

它参考了OSI模型,但做了精简。BACnet只用了五层:

  • 物理层:支持多种介质。RS-485、以太网、BACnet/IP、甚至ARCnet。我个人最常用的是BACnet/IP,走以太网,调试方便。
  • 数据链路层:负责组帧、寻址、差错检测。MS/TP(主从/令牌传递)是RS-485上的主流,你想想看,现场总线大多用这个。
  • 网络层:负责路由。不同BACnet网段之间怎么通信?靠它。我遇到过一个问题,两个楼宇的BACnet设备跨路由器通信不上,查了半天,发现是网络层报文中的DNET(目标网络号)没配对。
  • 应用层:这才是核心。对象、服务、属性,全在这一层定义。我们写代码打交道最多的就是它。

重点:BACnet的应用层直接封装了所有业务逻辑。你不需要关心底层是RS-485还是以太网,上层代码几乎不用改。这就是分层设计的好处。

3.2 BACnet对象模型

BACnet最巧妙的设计,我觉得就是「对象模型」。它把物理世界里的传感器、执行器、控制器,全部抽象成了标准对象。

举个例子。一个温度传感器,在BACnet里就是一个Analog Input(模拟输入)对象。一个风机启停开关,就是一个Binary Output(二进制输出)对象。每个对象都有固定的属性,比如当前值、单位、状态等。

这样做的好处是什么?你想想看,不同厂家的温度传感器,硬件可能千差万别,但只要它对外暴露的是标准的Analog Input对象,你的上位机软件就能统一读取。不用管它用的是NTC热敏电阻还是PT100铂电阻。

3.2.1 常用对象类型

对象类型 用途 典型属性
Analog Input (AI) 模拟量输入(温度、压力、湿度) Present_Value, Units, Resolution
Analog Output (AO) 模拟量输出(阀门开度、变频器频率) Present_Value, Priority_Array, Relinquish_Default
Binary Input (BI) 数字量输入(风机运行状态、门禁状态) Present_Value, Polarity, Change_Of_State_Time
Binary Output (BO) 数字量输出(启停控制、开关控制) Present_Value, Priority_Array, Inactive_Text
Device (设备) 描述BACnet设备本身 Object_Name, Vendor_Name, Firmware_Revision

我在项目中遇到过最坑的事,就是有人把AI对象当AO用。他想通过写AI的Present_Value来控制阀门。结果呢?写是写进去了,但阀门纹丝不动。因为AI对象本身就不支持写入控制。这就是没搞懂对象模型。

小技巧:每个BACnet设备必须有一个Device对象,且Object_Identifier在整个网络中唯一。这是BACnet设备身份的唯一标识。调试时,我习惯先用BACnet扫描工具看看Device对象能不能读到,能读到,说明网络层没问题。

3.3 BACnet服务

对象是「名词」,服务就是「动词」。你想对对象做什么?读它、写它、还是让它执行某个动作?这些操作,都由服务来完成。

BACnet定义了35种服务,但说实话,日常开发中90%的场景只用到了两三个。咱们重点讲最核心的两个:ReadPropertyWriteProperty

3.3.1 ReadProperty 服务

这个服务用来读取对象的某个属性。比如,你想知道1号空调机组的送风温度,就发一个ReadProperty请求,目标对象是Analog Input,Instance为1,属性是Present_Value。

报文结构大致是这样的:

// 伪代码示意
ReadProperty-Request:
  Object_Identifier: (Analog_Input, 1)
  Property_Identifier: Present_Value

// 响应
ReadProperty-Ack:
  Object_Identifier: (Analog_Input, 1)
  Property_Identifier: Present_Value
  Property_Value: 22.5  // 单位:摄氏度

我曾经调试一个冷站群控系统,发现读取的温度值总是跳变。从22度突然跳到55度,然后又回来。查了半天,发现是现场总线上的终端电阻没加,信号反射导致数据错位。嗯,物理层的问题,应用层再怎么写也救不了。

3.3.2 WriteProperty 服务

写属性服务。比如你要远程启动一台风机,就写Binary Output对象的Present_Value为ACTIVE。

这里有个坑,我必须提醒你:

避坑指南:我曾经在调试一个项目时,用WriteProperty直接写Binary Output的Present_Value,结果发现风机启动后,过一会儿自己又停了。后来查资料才发现,BACnet有优先级数组(Priority_Array)机制。如果你写的优先级不够高,其他更高优先级的命令(比如本地手动按钮)会覆盖你的控制。

所以,写属性时,建议明确指定优先级。一般上位机软件用优先级8(最低),本地手动面板用优先级1(最高)。这样设计,才能保证现场安全。

WriteProperty的报文示例:

// 伪代码示意
WriteProperty-Request:
  Object_Identifier: (Binary_Output, 5)  // 5号风机
  Property_Identifier: Present_Value
  Property_Value: ACTIVE
  Priority: 8  // 指定优先级

// 响应
WriteProperty-Ack:
  // 成功则返回空确认

3.4 小结与个人体会

BACnet入门,说白了就三件事:

  1. 搞懂分层架构——知道问题出在哪一层。
  2. 认准对象模型——每个物理设备对应什么对象,心里要有数。
  3. 用好读写服务——ReadProperty和WriteProperty能解决80%的需求。

我刚开始学BACnet时,也觉得很复杂。那么多对象类型,那么多服务。后来发现,别贪多,先把AI、AO、BI、BO这四个玩明白,Device对象搞清楚,ReadProperty和WriteProperty用熟练。剩下的,遇到再查,完全来得及。

下一章,咱们会深入BACnet的报文编码——ASN.1和BACnet的二进制编码规则。嗯,那才是真正考验协议栈开发功底的地方。做好准备。