一、C语言中的位操作
以下是一些常见的位操作:
按位与(&):将两个二进制数的每一位进行与操作,只有两个数的相应位都为1时,结果才为1,否则为0。
按位或(|):将两个二进制数的每一位进行或操作,只要两个数的相应位中有一个为1,结果就为1,否则为0。
按位异或(^):将两个二进制数的每一位进行异或操作,如果两个数的相应位不同,则结果为1,否则为0。
按位取反(~):将二进制数的每一位进行取反操作,即将0变为1,将1变为0。
左移(<<):将二进制数的每一位向左移动指定的位数,空出的位用0填充。
右移(>>):将二进制数的每一位向右移动指定的位数,空出的位用0或1填充,取决于移动前最高位的值。
例一:
#define MPU_ADDR 0X68
void MPU_IIC_Send_Byte(u8 txd)
{
u8 t;
MPU_SDA_OUT();
MPU_IIC_SCL=0;//拉低时钟开始数据传输
for(t=0;t<8;t++)
{
MPU_IIC_SDA=(txd&0x80)>>7;
txd<<=1;
MPU_IIC_SCL=1;
MPU_IIC_Delay();
MPU_IIC_SCL=0;
MPU_IIC_Delay();
}
}
MPU_IIC_Send_Byte((MPU_ADDR<<1)|0);//发送器件地址+写命令
从嵌入式的角度来说,这是一个通过IIC操作MPU6050的操作
首先,定义了一个常量MPU_ADDR,表示MPU设备的地址。然后定义了一个函数MPU_IIC_Send_Byte,用于发送单个字节的数据。该函数接收一个参数txd,它是要传输的字节。
接下来,使用MPU_SDA_OUT()函数将SDA引脚设置为输出引脚。然后将SCL引脚拉低以开始数据传输。接着,使用一个for循环遍历8位数据,从最高位(位7)开始,每次迭代将数据向左移动一位。对于每个位,函数将SDA引脚设置为当前位的值(0或1),然后将SCL引脚拉高并等待一段短时间(使用MPU_IIC_Delay()函数)。然后再次将SCL引脚拉低并等待另一段短时间。
当所有8位数据都传输完毕后,该函数返回。最后,调用MPU_IIC_Send_Byte函数,将MPU设备的地址左移一位并将最低位设置为0,表示写入数据。>
(1)(MPU_ADDR<<1)|0是多少?
答:宏定义中0x68表示为01101000,左移一位后变为11010000,再与0按位或得:11010000。那这一步的意义何在?在 I2C 协议中,设备的地址是一个 7 位的二进制数,其中最高位是必须为 0 或者 1 的,表示读写操作。最低位是用来表示设备要进行读或写操作的,值为 1 表示读操作,值为 0 表示写操作。因此,在将设备地址发送到 I2C 总线上时,需要将最低位设置为 0,以确保进行的是写操作,而不是读操作。
(2)从C语言的角度来看,MPU_IIC_SDA=(txd&0x80)>>7;是什么意思呢?
答:括号里面的txd&0x80是按位与操作,十六进制的0x80是十进制的128,也即10000000。这个操作可以得到txd二进制表示中的最高位(即第7位),然后将其右移7位,将得到一个0或1的值。这个值将被赋给MPU_IIC_SDA变量,即SDA引脚的输出值。这个操作相当于提取txd的最高位并将其写入SDA引脚.
(3)txd<<=1;是什么意思呢?
答:将txd向左移动一位。由(1)可知,函数的传参txd=(MPU_ADDR<<1)|0,在该代码中,每次循环都将txd向左移动一位,这样可以逐位地处理txd的每一位。
第一次传入时,txd==11010000,左移一位变为10100000,这时第二次MPU_IIC_SDA=(txd&0x80)>>7;把数据的次高位传输,让txd再次左移一位,如此循环。
在处理完txd的最高位后,通过左移操作将次高位变成了最高位,从而可以在下一次循环中处理它。
(4)因此。这段代码的作用是什么?
答:在 MPU_IIC_Send_Byte 函数中,首先将 SDA 端口设置为输出模式,然后将时钟线 SCL 拉低,开始数据传输。接下来,将要传输的数据 txd 的每一位依次通过 SDA 线发送出去。在每次传输完一位后,时钟线 SCL 被拉高一次,然后再拉低,用于传输下一位。在传输完 8 位后,数据传输结束。最低位设置为 0 相当于将其转换为写命令。
例二:
//得到加速度值(原始值)
//gx,gy,gz:陀螺仪x,y,z轴的原始读数(带符号)
//返回值:0,成功
// 其他,错误代码
u8 MPU_Get_Accelerometer(short *ax,short *ay,short *az)
{
u8 buf[6],res;
res=MPU_Read_Len(MPU_ADDR,MPU_ACCEL_XOUTH_REG,6,buf);
if(res==0)
{
*ax=((u16)buf[0]<<8)|buf[1];
*ay=((u16)buf[2]<<8)|buf[3];
*az=((u16)buf[4]<<8)|buf[5];
}
return res;;
}
(1)MPU_Read_Len(MPU_ADDR,MPU_ACCEL_XOUTH_REG,6,buf)是什么意思?
答:函数的作用是从 MPU6050 传感器中读取指定地址开始的多个字节数据,并将其存储到指定的数组 buf 中,返回读取操作的结果。四个传参分别是:
addr:表示要读取的 I2C 设备的地址。在 MPU6050 传感器中,加速度计和陀螺仪的地址都是 0x68(二进制为 01101000)。
reg:表示要读取的寄存器地址。在 MPU6050 传感器中,加速度计和陀螺仪的寄存器地址是不同的,加速度计的寄存器地址从 0x3B 开始,陀螺仪的寄存器地址从 0x43 开始。
len:表示要读取的字节数。在 MPU6050 传感器中,加速度计和陀螺仪的数据都是 16 位的,因此每个轴向的数据需要读取 2 个字节。
buf:表示用于存储读取到的数据的数组。在 MPU_Get_Accelerometer 函数中,buf 的长度为 6,用于存储 3 个轴向的加速度数据,每个轴向占用 2 个字节。
(2)*ax=((u16)buf[0]<<8)|buf[1];
*ay=((u16)buf[2]<<8)|buf[3];
*az=((u16)buf[4]<<8)|buf[5];
这是什么操作?
答:这行代码的作用是将 buf[0] 和 buf[1] 两个字节的数据合并成一个有符号的 short 类型,并将其存储到指针变量 ax 所指向的地址中。具体来说,将 buf[0] 的值左移 8 位(即乘以 256),然后加上 buf[1] 的值,得到一个 16 位的无符号整数。然后,将这个无符号整数强制转换为 short 类型,并使用指针变量 *ax 存储该值。比如前面将0x1a,0x2b存储在buf[0和buf[1]中,需要将其合并为一个16位数据,将0x1a强制转换成16位数据并左移8位,使其变成高8位,与buf[1]按位或后,数据的低八位==buf[1],这样就成功合并了。
由于 MPU6050 传感器的加速度计数据是 16 位的有符号整数,因此需要将 buf[0] 和 buf[1] 的合并结果转换成有符号的 short 类型。这里使用了强制类型转换,将无符号整数转换为有符号整数。
嵌入式里的位操作有什么作用?
答:嵌入式中的位操作可以用来处理和操作二进制数据,例如:
压缩数据:通过位移、按位与、按位或等操作,可以将多个数据压缩成一个二进制数,从而减少存储空间和传输带宽。
提高效率:位操作可以替代乘除、取模等运算,使代码更加简洁和高效。
控制硬件:通过位操作可以对寄存器中的位进行设置和清除,从而实现对硬件的控制和配置。
位标志:位操作可以用来设置和清除标志位,从而实现状态的管理和控制
二、结构体是什么?
(1)定义及示例
C语言的结构体(struct)是一种用户自定义的数据类型,可以用来描述一组相关的数据项。结构体可以包含不同类型的数据项,如整型、浮点型、字符型、指针等。结构体中的每个数据项称为结构体成员,可以通过成员访问运算符(.)来访问结构体成员。结构体的定义通常放在函数外部,可以在函数内部进行实例化和使用。
下面是一个简单的结构体定义的示例:
struct student {
char name[20];
int age;
float score;
};
在上面的示例中,我们定义了一个名为student的结构体,它包含了3个成员:name、age和score。其中,name是一个字符数组,age是一个整数,score是一个浮点数。我们可以在程序中创建一个该结构体类型的变量来存储学生的信息:
struct student stu1;
接着,我们可以使用成员访问运算符(.)来访问结构体成员:
strcpy(stu1.name, "Tom");
stu1.age = 18;
stu1.score = 90.5;
上面的代码将学生的姓名、年龄和分数分别赋值为Tom、18和90.5。我们可以使用成员访问运算符来访问结构体成员,如下所示:
#include <stdio.h>
struct student {
char name[20];
int age;
float score;
};
int main() {
struct student stu1;
strcpy(stu1.name,"TOM");
stu1.age = 18;
stu1.score = 90.5;
printf("Name: %s\n", stu1.name);
printf("Age: %d\n", stu1.age);
printf("Score: %.1f\n", stu1.score);
return 0;
}
上述代码将输出学生的姓名、年龄和分数。
(2)在STM32操作外设时以一个示例作为介绍
1.STM32的GPIO初始化函数
代码如下:
void Beep_Init(void)
{
GPIO_InitTypeDef gpio_initstruct;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);//打开GPIOB的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENABLE);//禁止JTAG功能
gpio_initstruct.GPIO_Mode = GPIO_Mode_Out_PP; //设置为输出
gpio_initstruct.GPIO_Pin = GPIO_Pin_3; //将初始化的Pin脚
gpio_initstruct.GPIO_Speed = GPIO_Speed_50MHz; //可承载的最大频率
GPIO_Init(GPIOB, &gpio_initstruct); //初始化GPIO
Beep_Set(BEEP_OFF); //初始化完成后,关闭蜂鸣器
}
这里的GPIO_InitTypeDef就是一个结构体类型,用于初始化和配置STM32芯片的GPIO引脚。
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
这里注意了,为什么这里的结构体的写法和上面C语言举例里的写法不一样呢?
struct student {
char name[20];
int age;
float score;
};
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
这里就涉及到一个关键字:typedef。
2.struct和typedef构造结构体的区别
(1)struct和typedef都可以用来构造结构体。
(2)struct和typedef构造结构体的区别在于:
struct是用来定义结构体的名称和成员变量;
typedef是用来定义类型别名的关键字,它可以在定义结构体的同时给结构体定义一个别名。
(3)在使用该结构体时,可以直接使用typedef后面定义的名称而不需要再初始化一个struct xx,提高了代码的简洁性和可读性。
三、结构体的深入理解与应用
1.
#include <stdio.h>
struct student {
char name[10];
int age;
float score;
};
int main() {
struct student stu1[10];//构建一个10个成员的结构体变量
strcpy(stu1[0].name,"c");
stu1[0].age = 18;
stu1[0].score = 90.5;
stu1[1].age=19;
printf("Name: %s\n", stu1[0].name);
printf("Age: %d\n", stu1[0].age);
printf("Score: %.1f\n", stu1[0].score);
printf("Name addr: %p\n", &stu1[0].name);
printf("Age addr: %p\n", &stu1[0].age);
printf("Score addr: %p\n", &stu1[0].score);
return 0;
}
运行结果为:
结构体在内存中是一段连续的内存空间,用来存储结构体中的各个成员变量的值。具体来说,结构体的内存布局如下:
①结构体中的每个成员变量按照其定义的顺序依次排列,各个成员变量之间没有任何间隔。
②如果结构体中的成员变量是基本数据类型,它们的大小和对齐方式与普通变量一样。例如,一个int类型的成员变量通常需要占用4字节的内存空间,它的地址通常是4的倍数。
③如果结构体中的成员变量是数组类型,它们的内存布局也与普通数组一样,即数组中的各个元素依次排列,相邻元素之间没有任何间隔。
④如果结构体中的成员变量是指针类型,它们的大小通常是4或8字节,具体取决于操作系统和编译器的位数。指针变量本身的值存储的是一个内存地址,指向实际的数据存储区域。
⑤如果结构体中的成员变量是结构体类型,它们的内存布局也与普通变量一样,即按照结构体定义的顺序依次排列,各个成员变量之间没有任何间隔。
2.stm32库函数开发里面,结构体为什么能代表外设的地址?
在嵌入式系统中,通常会使用结构体来表示外设的寄存器映射表(Register Map)。这是因为,外设通常都是通过内存映射(Memory-Mapped I/O)的方式来与CPU交互,即将外设的寄存器映射到一段特定的内存地址空间中。通过访问这些内存地址,CPU就可以读写外设的寄存器,从而控制外设的行为。
为了方便访问这些寄存器,通常会将寄存器映射表定义为一个结构体,结构体中的每个成员变量对应一个外设的寄存器。这样,在程序中就可以通过结构体变量来访问外设的寄存器,而不需要手动计算寄存器的地址。
3.访问结构体成员的运算符->和.
(1).运算符用于访问结构体类型变量的成员变量,例如struct_name.member_name,其中struct_name是结构体类型变量的名称,member_name是结构体类型中的成员变量名称。
(2)->运算符则用于访问结构体类型指针变量的成员变量,例如struct_pointer->member_name,其中struct_pointer是结构体类型指针变量的名称,member_name是结构体类型中的成员变量名称。
struct person {
char name[20];
int age;
};
struct person p1 = {"Alice", 20};//初始化成员变量name=alice和age=20
struct person *p2 = &p1;//定义了一个名为p2的指向person类型结构体的指针变量,并将其初始化为指向p1结构体变量的地址
printf("%s %d\n", p1.name, p1.age); // 使用.运算符访问结构体变量p1中的成员变量
printf("%s %d\n", p2->name, p2->age); // 使用->运算符访问结构体指针变量p2中的成员变量
这里便可以体现出两种运算符的区别了。