.NET上位机踩坑:BOOL和BIT

前言

大家好,我是 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的碎碎念】,欢迎关注追更!