说起进制,大家都能想到二进制、10进制、16进制、8进制等等,但是在互联网应用开发中,却很少用到这些换算。在物联网短指令应用中,却十分常见。
理解字节本质和二进制
无论是互联网应用还是物联网应用,在网络传输层传送的其实都是二进制数据。因为现代通信设备对信号处理都是用的数字电路,数字电路的输入输出只有两种状态,那就是高电平或低电平,也就是对应二进制数据的1和0。按照一定的时序和时钟基准,就可以代表了不同的信息。理解二进制数据的形成对物联网开发的深入是很有必要的。
上图中绿色线代表数据电平,红色线代表用于鉴别二进制每一个数据位的时间基准。
在前面我们已经说到网络传输中是按照字节传输的,而每个字节通常是按照8位二进制组成的,那么上图说明了一个字节的数据在数字电路中电平变化情况,也是在网络传输中各个通信设备间传输的电平变化情况。绿色线是数据线上的电平,可以看到有两个凸起的高电平,高电平代表1,低电平代表0,那么用二进制表示,是不是就是“01010”呢,事实上是不对的。上图的一个字节的二进制值应该是“10011000”,为什么会识别多识别出来几个位呢?原因是还有一个时间间隔的基准参照,就是时钟线上的电平变化。
每次从低电平变化为高电平,再从高电平变化为低电平,这个过程产生的电平波形是个凸起的方波,我们称之为高脉冲(因像脉搏跳动一样而得名,也有用低脉冲的,原理相同)。时钟线上连续不断产生着固定时间间隔和脉冲宽度的高脉冲,而数字电路在每个时钟高脉冲到来的时候,去识别一下数据线上的电平高低,如果是高就代表当前数据为是1,否则为0。就这样每一个时钟高脉冲都去识别一次,将结果依次记录下来,就组成了二进制数据。上图中数据线上的第二个高电平被识别成二进制“11”,就是因为在数据线高电平期间经历了两个时钟高脉冲,所以是两个1,而不是一个1,数据线上的低电平被识别成多个0的原理也是一样的。
一个字节由8个二进制位组成,通常标准是高位在前,最低位序号是0,最高位序号是7,所以上图中时钟高脉冲上面的数字是描述一个字节中的8个位的序号,也就是顺序。传完一个字节紧接着传下一个字节,原理相同。
为什么叫二进制?我们知道通常生活中使用的数字计数是10进制的,因逢10进1而得名。那二进制也是因为逢2进1而得名的。一个字节的8位二进制实际就是一个计数标记,由8个位组合在一起表示。因为每个位只能有0和1两种变化,所以要计数到2的时候就得进位了。看下面一组数据:
0000 0000 //这个字节的值是0,也就是10进制的0,中间空格为书写描述方便,实际不存在
0000 0001 //这个字节的值是1,也就是10进制的1
0000 0010 //这个字节的值是2,也就是10进制的2,需进位
0000 0011 //这个字节的值是3,也就是10进制的3
0000 0100 //这个字节的值是4,也就是10进制的4,需进位
0000 0101 //这个字节的值是5,也就是10进制的5
......
1111 1111 //这个字节的值是255,也就是10进制的255
通过上面一组数据,就能看出逢2进1的关系了,也知道为什么一个字节的值是在0-255之间了。那么8个位中,只有一个位为1,其他位为0的时候,分别在不同位置时,他们的值是怎样的关系呢,看下面一组数据:
0000 0001 //这个字节的值是1
0000 0010 //这个字节的值是2
0000 0100 //这个字节的值是4
0000 1000 //这个字节的值是8
0001 0000 //这个字节的值是16
0010 0000 //这个字节的值是32
0100 0000 //这个字节的值是64
1000 0000 //这个字节的值是128
从上面这组数据可以看出,值为1的位,每前进1位,其值就会是之前的2倍。这与10进制的1、10、100等每前进1位值就是之前的10倍是一样的规律,这样更好理解二进制位变化的关系了。那么我们在代码中,定义一个整数变量,赋值之后,它具体的二进制是什么样的呢,再看下面例子:
// C语言代码
unsigned char tmp = 65;
定义了一个无符号8位整数(单字节)变量 tmp,并同时给赋了初值为10进制整数65,运行时这个变量的初值的二进制(也是内存中实际保存的形式)是“0100 0001”,因为这个二进制对应的10进制值就是65。而不是保存6和5这两个数字的ASCii值(54,53)到内存中的。
那么单字节最大值是255,大于255的值是怎么表示的呢,各个语言都有不同的整数数据类型,他们对应的字节数量是不一样的,看下面的例子:
1111 1111 //255,无符号单字节整数最大值
1111 1111 1111 1111 //65535,无符号双字节整数最大值
1111 1111 1111 1111 1111 1111 1111 1111 //4294967295,无符号四字节整数最大值
根据上面这组数据,在你使用的开发语言的数据类型中找到该数据类型是多少个字节的,就知道他可以最大记录的数值是多少了。
对于整数变量是这样记录的,但是字符这类的是如何记录的呢,这就不得不提ASCii码(美国信息交换标准代码)。ASCii码将常见的英文字母、阿拉伯数字、英文标点符号等可见字符和常用的非可见控制符号都定义了对应的二进制值,每个符号占用一个字节,具体可网络搜索“ASCii码”有完整介绍,这里就不用详细介绍了。定义字符变量后,实际的变量二进制值是什么,看下面的例子:
// C语言代码
unsigned char strA = 'A'; //变量二进制值:0100 0001;10进制值:65
unsigned char str1 = '1'; //变量二进制值:0011 0001;10进制值:49
上例中变量str1的值为什么是49而不是1,是因为这个‘1’是字符1,也就相当于是我们写字描述数量用的标识符号1,而不是计数时候的整数值本身,所以要使用字符‘1’的ASCii码值49来表示出来,对应的二进制为“0011 0001”。还有个差别,这个变量的值实际是整数49,但是在系统调试输出的时候,会显示出字符‘1’的,也就是可见的。而真正的那个整数1是需要转换后才可以看到他的值的,否则不可见或看到的是错误的或乱码。大写字母A的ASCii码值是65,所以变量strA实际记录的也是整数65的二进制值。
理解十六机制
理解了字节的本质是二进制组成的,也知道了与十进制的对应关系,已经可以与日常计数换算了,那么十六进制又是什么鬼,为什么还要弄出来一个十六进制呢?
首先我们看二进制的写法,一个字节要写8次,显然很不方便,也不好读,口算成十进制数有很大难度。那么十六进制标准书写是两个字符表示一个字节,等宽制的,便于编辑浏览。更重要的是与二进制的换算刚好将8位分成两部分,每4位对应一个字符,两个字符拼在一起代表了完整的8位。下面我们通过数据看一下对应关系:
0000 1010 //16进制为0A,10进制为10
0000 1111 //16进制为0F,10进制为15
0001 0000 //16进制为10,10进制为16
1000 0010 //16进制为82,10进制为130
1111 1111 //16进制为FF,10进制为255
十六进制,顾名思义应是逢16进1才对,可是阿拉伯数字只有0-9,那么逢16进1至少要能表示到15才可以。因此十六进制在0-9的基础上又增加了A-F这6个字母,分别代表10-15。对应10进制值,00表示0,09表示9,0A表示10,0F表示15,10表示16(因为满16就进位了)。上图中把一个字节的8个位四四分开,分别对应前后两个16进制字符,就很快可以换算出来。
第一组前四位都是0,所以十六进制第一个字符写0;后四位是1010,是十进制的10,用十六进制应该是A,所以第二个字符应该是A。这样合起来的十六进制值就是0A。
第二组前四位都是0,所以十六进制第一个字符写0;后四位是1111,是十进制的15,用十六进制应该是F,所以第二个字符应该是F。这样合起来的十六进制值就是0F。
第三组前四位是0001,十六进制值应该是1,所以十六进制第一个字符写1;后四位都是0,用十六进制应该是0,所以第二个字符应该是0。这样合起来的十六进制值就是10。
第四组前四位是1000,十六进制值应该是8,所以十六进制第一个字符写8;后四位是0010,用十六进制应该是2,所以第二个字符应该是2。这样合起来的十六进制值就是82。
第四组前四位是1111,十六进制值应该是F,所以十六进制第一个字符写F;后四位是1111,用十六进制应该是F,所以第二个字符应该是F。这样合起来的十六进制值就是FF,也就是十进制的255,单字节的最大值。
十六进制在不同的编程语言里面,标识符(用于标注声明这是16进制的数字)有所不同,在C语言里用“0x”标识,如下面代码:
// C语言代码
unsigned char a = 0xB3; //变量a等于十进制的179
unsigned char b = 0x0C; //变量b等于十进制的12
关于十六进制字面量的表示方法请查阅各语言的官方手册,这里不再 一一列举。
关于数据值的沟通
在我们最初的物联网开发团队中,嵌入式团队与服务端团队沟通时,经常出现这样的发问“你那边是发送的16进制的数据还是10进制的数据啊”,或类似“你是以16进制形式发送的还是以10进制形式发送的啊”、“你用16进制发的,我也得转成16进制啊,能不能你改成10进制发送啊”等等。
为什么会有上面的问题,上面的问题应该这样问吗?后来我发现从互联网开发转过来的程序员多数会有这个问题,这个问题是不应该这样问的,究其原因主要有一下几种:
1、嵌入式开发人员涉及到数据的底层操作和编辑,多数都是习惯用16进制描述数据,数据编辑器也多数都是16进制的,很少有10进制的。所以沟通的时候,16进制的数值描述首先从嵌入式人员说出来了。
2、由于上面原因,嵌入式的文档中描述协议标志的时候,也是用16进制进行定义的,这方便数据编辑(后面会讨论到),所以在讨论文档的时候也会出现16进制的描述。
3、互联网开发人员多数习惯了10进制的数值描述,对进制转换和字节的本质因长期不涉及相对底层的开发,就搞不清之间的关系了。
4、最重要的是问的人对数据传输的字节底层含义已经淡忘了,所以一时不能适应了。
其实在沟通中,发送方只要告诉接收方发送的是哪个进制的多少值就可以了。接收方用16进制读取还是10进制读取决定与自己,因为数据本身就是那个二进制没变。你读的进制不同,值的面量就不同了,根据需要自己选择换算就可以了。例如下面的代码:
// C语言代码
unsigned char a = 0x0B; //16进制赋值
unsigned char b = 11; //10进制赋值
if(a == b){
printf('OK');
}else{
printf('NO');
}
上面代码执行后,控制台输出的是“OK”,因为十六进制的 0x0B 和十进制的 11 的二进制是一样的。
本节完,待续......