后端开发第一次碰到位掩码
0 1 2 3 4 和 0 1 2 4 8 16 32
后端开发定义枚举,习惯性地从 0 开始往上加:
public enum DeviceType {
TEMP(0),
HUMIDITY(1),
LIGHT(2),
PRESSURE(3),
MOTOR(4);
}
对接固件协议的时候,看到的值却是这样的:
{
"hwInstalled": 19,
"hwEnabled": 17,
"hwHealth": 1
}
19 是什么意思?怎么不是个数组?
因为固件那边不是在编号,而是在占位。每个设备占一个二进制位:
| 设备 | 位 | 十进制值 |
|---|---|---|
| 温度传感器 | bit 0 | 1 |
| 湿度传感器 | bit 1 | 2 |
| 光照传感器 | bit 2 | 4 |
| 气压传感器 | bit 3 | 8 |
| 电机控制器 | bit 4 | 16 |
19 的二进制是 10011,第 0 位、第 1 位、第 4 位是 1,代表”装了温度传感器、湿度传感器和电机控制器”。
为什么不老老实实用数组
后端要表示”设备上装了温度、湿度、电机三个模块”,大概会这样写:
{
"hwInstalled": ["TEMP", "HUMIDITY", "MOTOR"]
}
固件用一个整数就搞定了:1 + 2 + 16 = 19。
一个 32 位整数能塞 32 个开关,64 位能塞 64 个。不需要数组,不需要序列化,一次内存读取就拿到所有状态。在嵌入式场景里,内存和带宽都很紧张,这种表示方式非常自然。
怎么读:按位与
拿到一个 hwInstalled = 19,想知道电机控制器在不在里面:
long installed = 19;
long MOTOR = 16; // 即 1 << 4
if ((installed & MOTOR) != 0) {
// 电机控制器存在
}
& 是按位与,两个数的同一位都是 1 结果才是 1。19 & 16:
10011 (19)
10000 (16)
-----
10000 (16,非零,说明电机控制器是在的)
换一个,查光照传感器(bit 2 = 4):
long LIGHT = 4;
if ((installed & LIGHT) != 0) {
// 光照传感器存在
}
19 & 4:
10011 (19)
00100 (4)
-----
00000 (0,光照传感器不在)
怎么写:按位或、异或、取反
设置一个位用 |(按位或):
long flags = 0;
flags |= (1L << 0); // 开温度传感器,flags = 1
flags |= (1L << 4); // 开电机控制器,flags = 17
清除一个位用 & ~(按位与 + 取反):
flags &= ~(1L << 4); // 关电机控制器,flags 回到 1
翻转一个位用 ^(异或):
flags ^= (1L << 4); // 电机开着就关,关着就开
一次解出多个设备
实际对接时,拿到一个整数要把所有置位的设备都解出来。遍历每一位就行:
String[] names = {"TEMP", "HUMIDITY", "LIGHT", "PRESSURE", "MOTOR"};
long installed = 19;
for (int i = 0; i < names.length; i++) {
if ((installed & (1L << i)) != 0) {
System.out.println(names[i] + " 已安装");
}
}
输出:
TEMP 已安装
HUMIDITY 已安装
MOTOR 已安装
三个字段,同一套位定义
固件协议里经常会用同一套位定义、配上不同语义的字段。比如硬件模块可能有三个状态字段:
| 字段 | 含义 |
|---|---|
hwInstalled | 物理上装了哪些模块 |
hwEnabled | 当前启用了哪些 |
hwHealth | 哪些工作正常 |
三个值做一次按位与,就能找出”装了、启用了、而且工作正常”的模块:
long working = installed & enabled & health;
如果某个模块 installed 里有但 health 里没有,说明硬件在但出了故障。这种判断一行位运算就够了,不需要三个数组做交集。
后端对接时的注意点
用 Java / Kotlin 对接这类协议,有几个容易踩的地方:
- 用
long不用int。固件协议里的位掩码经常超过 32 位,int只有 32 位会溢出。字段声明和字面量都要用long/1L - 位号从 0 开始。
1 << 0是第一个设备,不是1 << 1 <<优先级低于比较运算符。flags & 1 << 5实际上是flags & (1 << 5)——这个恰好没问题,但flags & 1 << 5 != 0会被解析成flags & (1 << (5 != 0)),所以位运算一定要加括号:(flags & (1L << 5)) != 0