前言
大家好,我是 wacky。
今天这篇文章,来源于一个真实的疑问,什么疑问呢?就是我一个BOOL类型的数据,它的值是TRUE或者FALSE;而一个Bit类型的数据,它的值,也是TRUE或者FALSE,那么它们二者是一样的吗?能否互相替换使用?
答案是不能的,今天我们这一篇就来聊一聊,BOOL和Bit究竟有什么区别,以及在.NET上位机中怎么去对接这两种类型的数据。
概念
我们做上位机开发,一般是要对接PLC数据,这里就需要引入IEC 61131‑3中的定义和规范了:
BOOL是布尔数据类型,逻辑上只有 TRUE/FALSE;但是存储占用1 个字节 (8bit),不是 1 个比特。虽然在PLC硬件的内部,可以用Bit位来存储(比如 I、Q、M 区的位变量),但BOOL 这个基础数据类型的原子大小定义是字节。而Bit属于内存寻址方式,并不是BOOL类型本身。
那么为什么规范会这样定义呢?主要有以下几个根本原因:
1、CPU 内存最小可寻址单元是字节而不是位
绝大多数 CPU(x86、ARM、各类 PLC 控制器 CPU)不能直接按 bit 独立读写内存。 CPU 访问内存的最小粒度是字节。
如果你想只修改某 1 个 bit:CPU 必须先读取整个字节 → bit 掩码运算 → 再写回整个字节。
如果把基础类型定义为 1bit,每一次读写 BOOL 都要做读‑改‑写,编译器逻辑会极度复杂。
2、跨硬件平台的可移植性
因为IEC61131-3是一个通用标准,所以它的目标是可移植,同样的PLC编程代码,如ST语言,它可以在西门子、汇川、倍福等各种PLC上运行。
所以如果把BOOL规定成1个Bit,那么指针、数组和结构体的对齐会出现巨大的混乱:比如我们定义了一个长度为10的BOOL类型的数组,如果BOOL=1Bit,这时数组的长度只占用了10个Bit,数组的下标、指针偏移和结构体OffSet全部都要按照比特偏移去计算。这对编译器和底层RunTime都是一场噩梦,很多CPU的硬件不支持Bit偏移寻址。
3、TRUE和FALSE的取值约定是非0为真
这个是什么意思呢?规定为一个字节,那么就可以使得任意非零字节都判断为TREU,常见的有16#01。
比如在Modbus或者其他通信协议上经常返回0x02,0xFF代表开启,可以直接识别为TRUE。这是一个兼容性的设计,如果只有一个Bit,那么就只能是0和1,不能是其他的值。
容易混淆的点
PLC中可以通过Bit来寻址,比如有一个%IX0.0的位地址,它是可以赋值给BOOL类型的变量的。但是这时候BOOL变量本身的类型大小,依然是1字节,编译器自己会做一个Bit到BOOL字节之间的转换。
而且在PLC内部的局部变量、全局变量区,编译器经常会把多个BOOL变量打包到同一个字节的不同Bit,用于节省内存。但这个是编译器内部的优化,不会改变语言标准中BOOL类型的逻辑大小。
如果这个BOOL变量映射到EtherCAT通信的PDO或者是结构体导出时,都会按照1字节来进行计算。
C#中要怎么玩
在C#语言中,BOOL类型本身也是占1字节,BitArray才是按照bit来进行打包。所以我们在用C#开发上位机和PLC结构体通信时,不要自作主张把PLC中的BOOL映射成C#的1bit,C#要使用BOOL(1字节),否则PDO解析就会错位。如果确实要解析EtherCAT PDO里面的bit位,就需要单独的做位掩码解析。
下面我们来看一下示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BoolAndBit
{
internal class PlcBitHelper
{
public static bool ReadPlcBoolByte(byte plcByte)
{
// IEC61131:0x00=false,任何非0=true
return plcByte != 0x00;
}
}
}
我们封装了一个方法用于判断PLC的字节数据是否返回TRUE或者FALSE,在IEC61131的规范中,只有0x00会返回FALSE,其他均为TRUE,如0x01~0xFF。
然后我们调用这个方法输出一些结果:
using BoolAndBit;
byte plcTrue1 = 0x01;
byte plcTrue2 = 0x05; // PLC BOOL为TRUE时可能返回非0x01
byte plcFalse = 0x00;
Console.WriteLine(PlcBitHelper.ReadPlcBoolByte(plcTrue1)); // True
Console.WriteLine(PlcBitHelper.ReadPlcBoolByte(plcTrue2)); // True
Console.WriteLine(PlcBitHelper.ReadPlcBoolByte(plcFalse)); // False
Console.Read();
最后输出结论:
很多人会在这里踩坑,只判断0x01为TRUE,而根据我们上述所说的规范,0x05也为TRUE。
那么如果是1个字节打包了8个Bit,我们要如何解析呢?这个场景常见于Modbus读取内存字节,1个字节中存储了8个Bit的开关量,此时我们就不能理解为IEC BOOL了,而是真正的Bit位。
所以我们的程序就需要从每个Bit位中提取指定的状态了,如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BoolAndBit
{
internal class PlcBitHelper
{
/// <summary>
/// 从字节中提取指定bit位状态
/// bitIndex:0~7
/// </summary>
public static bool GetBitFromByte(byte data, int bitIndex)
{
if (bitIndex < 0 || bitIndex > 7)
throw new ArgumentOutOfRangeException(nameof(bitIndex));
return (data & (1 << bitIndex)) != 0;
}
/// <summary>
/// 设置字节内某一位
/// </summary>
public static byte SetBitInByte(byte data, int bitIndex, bool value)
{
if (bitIndex < 0 || bitIndex > 7)
throw new ArgumentOutOfRangeException(nameof(bitIndex));
if (value)
{
data = (byte)(data | (1 << bitIndex));
}
else
{
data = (byte)(data & ~(1 << bitIndex));
}
return data;
}
}
}
然后我们再读取每一个Bit位的值,这里以字节0x05为例:
using BoolAndBit;
//字节0b00000101(0x05),bit0=1,bit2=1,plc中的排序是从bit7~bit0排列
byte input = 0x05;
Console.WriteLine(PlcBitHelper.GetBitFromByte(input, 0)); // True
Console.WriteLine(PlcBitHelper.GetBitFromByte(input, 1)); // False
Console.WriteLine(PlcBitHelper.GetBitFromByte(input, 2)); // True
PlcBitHelper.SetBitInByte(input, 1, true); // 设置bit1=1 → 0x07
Console.Read();
输出结论:
注意这里面也有一个坑点,那就是有些人会把Bit序号搞反,以为Bit7是第0位,这样所有的状态会全部颠倒。另一种情况就是直接用C#的BOOL数组去映射了PLC的Bit,C#的BOOL是1字节,不能直接用于内存映射。
后记
关于BOOL和Bit,我们今天就讲到这里,这个问题看似很小,但实际上会让不少人踩坑。我们今天也是从IEC标准讲起,再到真实的上位机对接示例结束,相信各位也从中学到了一些东西,也有了自己的感悟。那么下一次我们的踩坑话题要聊些什么呢?欢迎大家在评论区留言讨论!
本文首发于我的公众号【wacky的碎碎念】,欢迎关注追更!