摘要:你知道内存是怎么读取数据的吗?知道数据是怎么一个一个字节发送的吗?是低字节先发还是高字节先发?是bit0先发还是bit7先发?是从低地址开始读还是从高地址开始读?看完本篇比应该就明白了~
内存的读写永远从低地址开始读/写,从低到高!从低到高!从低到高!重要的话说三遍
大端模式和小端模式
大端模式和小端是实际的字节顺序和存储的地址顺序对应关系的两种模式。
大端模式:高位字节存放在低地址中,低位字节存放在高地址中。最直观的字节序。
小端模式:高位字节存放在高地址中,低位字节存放在低地址中。最符合人的思维的字节序,x86、ARM都这么搞(KEIL C51中,变量都是大端模式的;KEIL MDK中,变量是小端模式的。)。
用图表示更加容易理解。以unsigned int value = 0x12345678
为例,分别按照大端模式和小端模式存放在芯片中。
内存地址 | 0x00000001 | 0x00000002 | 0x00000003 | 0x00000004 |
---|---|---|---|---|
大端模式 | 0x12 | 0x34 | 0x56 | 0x78 |
小端模式 | 0x78 | 0x56 | 0x34 | 0x12 |
再换一种图示:同样以unsigned int value = 0x12345678
为例,分别看看在两种字节序下其存储情况,我们可以用unsigned char buf[4]
来表示value
。
不管是大端还是小端模式,我们在读取和存储数据的时候一定都是从内存的低地址依次向高地址读取或写入。另外注意,x86平台是小端的,ARM平台是小端的,而PowerPC平台是大端的。
字节高低位
一般左边为高位,右边为低位(这个高低来自于人类的阅读习惯,数字从左向右,表示由大到小)
一个16位(双字节)的数据,比如0xFF1A
,那么高位字节就是0xFF
,低位是0x1A
。
如果是32位的数据,比如0x3F68415B
。高位字(不是字节)是0x3F68
,低位字是0x415B
。
右边是低位位,左边是高位(人的阅读习惯)
LSB和MSB
最高有效位(most mignificant bit,msb)指的是一个n位二进制数字中的n-1位,具有最高的权值2^(n-1)。 有时也指Most Significant Byte(MSB),指多字节序列中具有最大权重的字节。
同理,最低有效位(least significant bit,lsb)和的是一个n位二进制数字中的0位,具有最低的权值2^0。有时也指Least Significant Byte(LSB),指多字节序列中具有最小权重的字节。
所以0x12345678的最高有效字节就是0x12,最低有效字节就是0x78,这样明白了吧!
举个栗子
当选择模数转换器(ADC)时,最低有效位(LSB)这一参数的含义是什么?
对于一个12位串行转换器,它会输出由1或0组成的12位数串。通常,转换器首先送出的是最高有效位(MSB)(即LSB + 11)。有些转换器也会先送出LSB。我们假设先送出的是MSB,然后依次送出MSB-1 (即 LSB + 10)和MSB -2(即LSB + 9)并依次类推。转换器最终送出MSB -11(即LSB)作为位串的末位。
LSB这一术语有着特定的含义,它表示的是数字流中的最后一位,也表示组成满量程输入范围的最小单位。对于12位转换器来说,LSB的值相当于模拟信号满量程输入范围除以2^12 或 4096的商。如果用真实的数字来表示的话,对于满量程输入范围为4.096V的情况,一个12位转换器对应的LSB大小为1mV。但是,将LSB定义为4096个可能编码中的一个编码对于我们的理解是有好处的。
高位先行msb 、低位先行lsb
高位先行即在传输一个字节的时候先传输高位msb;低位先行即在传输一个字节的时候先传输低位lsb。高位先行和低位先行是针对串行数据传输方式来说的。常见的串行传输方式有串口(UAR)、I2C、SPI等。以串口传输方式为例,标准的串口传输方式是低位先行,芯片在通过TX引脚发送数据时,依次发送位0、位1……位7。
串口传输是低位先行
UART在数据传输时,协议规定了数据传输必须是低位先行,看下面的时序图你就知道了~
IIC传输是高位先行
IIC的数据和地址均以8位字节传输,MSB 在前。从图中可以清楚地看到:
这一点也反映在代码中,我们随便找一个IIC的读字节和写字节的函数看看:
void i2c_SendByte(uint8_t _ucByte)
{
uint8_t i;
/* 先发送字节的高位bit7 */
for (i = 0; i < 8; i++)
{
if (_ucByte & 0x80)
{
I2C_SDA_1();
}
else
{
I2C_SDA_0();
}
i2c_Delay();
I2C_SCL_1();
i2c_Delay();
I2C_SCL_0();
if (i == 7)
{
I2C_SDA_1(); // 释放总线
}
_ucByte <<= 1; /* 左移一个bit */
i2c_Delay();
}
}
从第7行代码中可以看到,在发送一个字节时,首先将要发送的字节与0x80进行与运算,取出最高位,然后循环左移8次就可以将一个字节数据发送出去了。你有没有想过为什么这里我们不把要发送的字节与0x01进行与运算,取出最低位,然后循环右移8次也可以将一个字节数据发送出去呢?
答:因为我们说了I2C在数据传输时,协议规定了数据传输必须是高位先行,所以你要发送一个字节的数据肯定必须先取出最高位,然后循环左移将数据发出,如果你与上0x01,就是低位先行,虽然你也将一个字节发出去了,但是你发的是歪门邪道的数据,人家单片机也不认识,对吧?你品,你细品~
同样在接收一个字节时,接收到的第1位认为是最高位,接收一个字节代码如下:
uint8_t i2c_ReadByte(void)
{
uint8_t i;
uint8_t value;
/* 读到第1个bit为数据的bit7 */
value = 0;
for (i = 0; i < 8; i++)
{
value <<= 1;
I2C_SCL_1();
i2c_Delay();
if (I2C_SDA_READ())
{
value++;
}
I2C_SCL_0();
i2c_Delay();
}
return value;
}
所有使用I2C的设备必须遵循I2C协议,必须都是高位先行的,这样才能实现通用性。怎么样?是不是又get到了一个小技巧~
字节序、比特序
字节序就是串行发送多字节时发送的顺序,比如value=0x12345678,按字节发送是0x12、0x34、0x56、0x78顺序还是0x78、0x56、0x34、0x12顺序。
同理,比特序在bit层面进行排序,如果一个字节,指先发bit0还是bit7, 如果是一个Word型,先发bit31还是先发bit0
串口是lsb优先,I2C是msb优先,这里的msb、lsb指的是比特序,二进制位的位置。区别于【字节序】通信中,先发送低字节,还是高字节的问题,那是字节序的MSB还是LSB,当然也有人混称上面所说的为大端发送big-endian、小端发送little-endian
验证MCU平台存储方式?
这里以STM32开发单片机的keil平台为例,以下代码如果打印0x04就是小端存储,如果0x01则是大端存储。
因为0x04是低字节,读取数据是从低地址开始读,打印的是data的低地址,所以如果打印出的是0x04就表明低地址存储低字节,就为小端存储。明白了吗?
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "key.h"
#include "lcd.h"
#include "SEGGER_RTT.h"
#include "math.h"
int main(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(8,336,2,7);//设置时钟,168Mhz
delay_init(168); //初始化延时函数
while(1)
{
uint32_t data =0x01020304;
char *p = (char*)&data;
printf("0x0%x\n",*p);//看输出的是0x01还是0x04
delay_ms(1000);
}
}
编译、链接、下载,通过RTT查看试验结果:
可以看出STM32是小端存储。
总结:内存的读写永远从低地址开始读/写。大小端存储指字节在内存存储方式,X86、ARM平台都是小端存储(低-低),MSB/LSB只发送字节序或者比特序,串口是比特序LSB,IIC是比特序MSB。也有人将MSB、big-endian、大端发送都混为一谈,这时候一般指字节序上MSB。