ARM架构与C语言(韦东山)学习笔记(6)-C语言的位操作、结构体与指针


一、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中的成员变量

这里便可以体现出两种运算符的区别了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值