1. 将一个整型数据拆分成2字节无符号整型
uint8_t buffer[2]; // 创建一个长度为 2 的 uint8_t 数组,用于存储两个字节的数据
int num = 20; // 要拆分的整数
// 将num拆分为两个字节,并以大端字节序存储
buffer[0] = (num >> 8) & 0xFF; // High byte,将num右移8位得到高位字节,并通过掩码操作保留最低8位
buffer[1] = num & 0xFF; // Low byte,直接通过掩码操作获取低位字节
这段代码首先创建了一个长度为 2 的 uint8_t
数组 buffer
,用于存储两个字节的数据。然后,整数 num
被拆分为高位字节和低位字节,并以大端字节序存储在 buffer
数组中。高位字节存储在数组的第一个元素 (buffer[0]
),低位字节存储在数组的第二个元素 (buffer[1]
)。
这种拆分字节的方法通常用于处理网络通信或与外部设备进行数据交换时,确保数据在不同系统之间的一致性。在不同系统中,字节序可能是大端或小端,因此在进行数据传输时,需要考虑这一点。
这段代码是将变量 num
分割为两个字节并以大端(Big-Endian)字节序存储在一个字节数组 buffer
中。
让我们解释一下这段代码的每一步:
-
num >> 8
:这是右移操作符,将 num 右移8位。这个操作将 num 中的高8位移动到低8位的位置。这样做是为了获得整数的高字节(高8位)。 -
& 0xFF
:这是按位与操作符,用于掩码(Mask)操作。0xFF
是一个十六进制数,对应二进制为11111111
,用于提取一个字节中的低8位数据(即使高位被右移了)。这个操作保留了原始整数的低8位,以获取整数的低字节(低8位)。 -
buffer[0] = (num >> 8) & 0xFF;
:将提取的高字节(右移后的高8位)存储在 buffer
数组的索引为 0 的位置。这个操作将高字节放在数组的第一个元素,即大端字节序的高位部分。 -
buffer[1] = num & 0xFF;
:将提取的低字节存储在 buffer数组的索引为 1 的位置。这个操作将低字节放在数组的第二个元素,即大端字节序的低位部分。
综合来说,这段代码将一个16位整数 num
拆分成两个8位的字节,并以大端字节序存储在 buffer 数组中。这样做通常用于需要对整数进行字节级别操作或将整数转换为字节序列的情况。
解释一下为什么右移八位还要& 0xFF
这是因为右移运算 >>
可能会导致高位补充符号位,而这可能会导致一些问题,尤其是在处理无符号整数时。通过使用位掩码 & 0xFF
,可以确保只保留低8位,而高位全部清零,从而消除了符号位的影响。
具体来说,右移运算符 >>
在处理带符号整数时,会用符号位来填充高位。如果 num
是一个有符号整数,右移8位可能会导致符号位的扩展。通过使用 & 0xFF
这个位掩码,你可以将高位清零,只保留低8位,确保结果是一个无符号的字节值。
这是一种保险的做法,特别是在涉及到二进制位级别的操作时,以确保结果的正确性。如果你确定 num
是一个无符号整数,那么在右移的同时进行位掩码操作可能就不是必须的。
2. memcpy的使用
uint8_t destbuffer[6];
char asciiArray[5][22];
memcpy(destbuffer, &buffer[offset * 2], usNbr * 2);
memcpy(destbuffer, (char *)asciiArray + offset * 2, usNbr * 2);区别
这两行代码都使用了 memcpy
函数,但是它们的区别在于复制的源数据不同。
第一行代码:
memcpy(
destbuffer, &buffer[offset * 2], usNbr * 2);
这行代码将从 buffer
数组中以 offset * 2
为起始位置的内存,复制 usNbr
个连续的字节到 destbuffer
中。destbuffer
存储的是无符号整数的字节表示,每个整数占用两个字节。
第二行代码:
memcpy(
destbuffer, (char *)asciiArray + offset * 2, usNbr * 2);
这行代码将从 asciiArray
数组中以 offset * 2
为起始位置的内存,复制 usNbr
个连续的字节到 destbuffer中。通过强制类型转换 (char *)
,asciiArray
被视为字符数组,每个字符占用一个字节。
第一行使用了 &destbuffer[offset * 2]
,是一个指向 uint8_t
的指针。第二行使用了 (char *)asciiArray + offset * 2
,是一个指向 char
的指针。这可能会影响指针算术和复制的行为
总的来说,第一行代码复制的是以大端字节序存储的无符号整数的字节表示,而第二行代码复制的是字符数组的字节表示。因此,这两行代码适用于不同的数据类型和存储方式的数据。选择哪一种方式取决于你要处理的数据的类型和结构。
3. 结构体数组的使用
typedef struct {
int age;
int id;
const char *name;
const char *addr;
} ST_INFO;
static ST_INFO[] = {
{ .age = 10, .id = 0,.name = "萧炎", .addr = "山东"},
{ .age = 20, .id = 1,.name = "叶凡", .addr = "菏泽"},
{ .age = 30, .id = 2,.name = "秦尘", .addr = "曹县"},
{ .age = 40, .id = 3,.name = "罗峰", .addr = "世界"},
{ .age = 50, .id = 4,.name = "唐三", .addr = "宇宙"},
{ .age = 60, .id = 5,.name = "石昊", .addr = "中心"},
};
char asciiArray[4][6]; // 二维数组存储 ASCII 表示的 cDevType
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 6; ++j) {
asciiArray[i][j] = (char)ST_INFO[i].name[j];
}
}
4. 获取本地时间
// 获取当前时间戳
time_t currentTime;
time(¤tTime);
// 将时间戳转换为struct tm结构体,以获取各个时间部分
struct tm *localTime = localtime(¤tTime);
// 获取年月日时分秒
acpevt.year = localTime->tm_year - 100;
acpevt.month = localTime->tm_mon + 1;
acpevt.day =localTime->tm_mday;
acpevt.hour= localTime->tm_hour;
acpevt.minute= localTime->tm_min;
acpevt.second = localTime->tm_sec;
5. 程序中关于寄存器地址问题
在一些 Modbus 实现中,寄存器地址的计数是从 0 开始的,而不是从 1 开始。这意味着 Modbus 协议中的寄存器地址 1 实际上对应于实际设备内的寄存器地址 0,寄存器地址 2 对应于实际设备内的寄存器地址 1,依此类推。
当你在 Modbus 请求中指定寄存器的地址时,你需要将实际寄存器地址减 1,以在 Modbus 协议中正确表示。这是为了对齐 Modbus 协议和实际设备内的寄存器地址。
例如,如果你想要读取设备内的第一个寄存器,实际设备内的地址是 0,但在 Modbus 协议中你应该指定地址 1。同样,如果你想要读取实际设备内的第二个寄存器,地址是 1,但在 Modbus 协议中应该指定地址 2。
6. Uint8_t <-> uint 16_t (大小端)
小端模式(big-endian):高地址存高位数据,低地址存低位数据。
大端模式(little-endian):高地址存低位数据,低地址存高位数据。
大端16->8
static inline void u16_tobe(uint16_t v, uint8_t *buf) {
buf[0] = v >> 8;
buf[1] = v & 0xFF;
}
小端16->8
static inline void u16_tole(uint16_t v, uint8_t *buf) {
buf[0] = v & 0xFF;
buf[1] = v >> 8;
}
大端8->16
static inline uint16_t u16_frombe(const uint8_t *buf) {
return (buf[0] << 8) | buf[1];
}
补充另一种方式:
Uint8_t类型的数据转成uint16_t类型的数据
temp_buffer[i] = ((uint16_t)printtask.buffer[i*2] << 8) | printtask.buffer[i*2 + 1];: 这行代码执行以下操作:
printtask.buffer[i*2]: 从buffer数组中获取索引为i*2的元素。
(uint16_t)printtask.buffer[i*2] << 8: 将该元素强制转换为uint16_t类型,并将其左移8位(相当于乘以256)。例如,如果原始值是 0000 1111(二进制),左移8位后变为 1111 0000。
printtask.buffer[i*2 + 1]: 从buffer数组中获取索引为i*2 + 1的元素。
|: 使用位或运算符将上述两个结果合并。|: 这个位或运算符将上述两个结果合并。具体来说,它会将两个数字的每一位进行或运算。例如,如果两个数字的某一位都是1(例如 1111 0000 和 1100 1100),则结果的那一位就是1(1111 1100)。如果任一位是0(例如 1111 0000 和 0000 1000),则结果的那一位就是0(1111 1000)。
temp_buffer[i] = ...: 将合并后的结果存储在临时数组temp_buffer的相应位置。
总的来说,这段代码的目的是将printtask.buffer数组中的数据转换为一个更大的数据类型(这里是uint16_t),并将结果存储在temp_buffer数组中。
小端8->16
static inline uint16_t u16_fromle(const uint8_t *buf) {
return (buf[1] << 8) | buf[0];
}
这些函数用于处理16位无符号整数(uint16_t)与字节数组(uint8_t数组)之间的相互转换,以及大端(big-endian)和小端(little-endian)字节序之间的转换。
7. 枚举
枚举是C语言中的一种基本数据类型,它可以让数据更简洁,更易读。
枚举语法定义格式为:
enum 枚举名
{
枚举元素1,枚举元素2,…… //注意,各元素之间用逗号隔开
}; //注意,末尾有分号;
枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。
一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。
编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加。
C语言为何需要枚举?
C语言没有枚举是可以的。使用枚举其实就是对一些数字进行符号化编码,这样的好处就是编程时可以不用看数字而直接看符号。符号的意义是显然的,一眼可以看出。而数字所代表的含义除非看文档或者注释。
8. 宏定义和枚举的区别:
1、枚举是将多个有关联的符号封装在一个枚举中,而宏定义是完全散的。也就是说枚举其实是多选一,而且只能在这里面选,有时候有效防止其他不符数据的输入。
2、什么情况下用枚举?当我们要定义的常量是一个有限集合时(譬如一星期有7天,譬如一个月有31天,譬如一年有12个月····),最适合用枚举。(其实宏定义也行,但是枚举更好)
3、不适合用枚举的情况下(比如定义的常量符号之间无关联,或者无限的)用宏定义。