ARM アーキテクチャと C 言語 (魏東山) 学習ノート (6) - C 言語のビット演算、構造、ポインタ


1. C言語のビット演算

ビットごとの
AND (&): 2 つの 2 進数の各ビットに対して AND 演算を実行します。2 つの数値の対応するビットが 1 の場合のみ、結果は 1 になり、それ以外の場合は 0 になります。
ビットごとの OR (|): 2 つの 2 進数の各ビットに対して OR 演算を実行します。2 つの数値の対応するビットのいずれかが 1 である限り、結果は 1 になり、それ以外の場合は 0 になります。
ビットごとの XOR (^): 2 つの 2 進数の各ビットを XOR します。2 つの数値の対応するビットが異なる場合、結果は 1 になり、そうでない場合は 0 になります。
ビットごとの反転 (~): 2 進数の各ビットを反転します。つまり、0 が 1 に変更され、1 が 0 に変更されます。
左シフト (<<): 2 進数の各ビットを指定されたビット数だけ左にシフトし、空いたビットを 0 で埋めます。
右シフト (>>): 2 進数の各ビットを指定されたビット数だけ右にシフトし、シフト前の最上位ビットの値に応じて、空いたビットを 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 デバイスのアドレスを示す定数 MPU_ADDR が定義されます。次に、1 バイトのデータを送信する関数 MPU_IIC_Send_Byte が定義されます。この関数は、送信されるバイトである 1 つのパラメータ txd を受け取ります。
次に、MPU_SDA_OUT()関数を使用して、SDA ピンを出力ピンとして設定します。次に、SCL ピンを Low にしてデータ転送を開始します。次に、for ループを使用して 8 ビット データを走査し、最上位ビット (ビット 7) から開始して、反復ごとにデータを 1 ビット左にシフトします。各ビットに対して、関数は SDA ピンを現在のビットの値 (0 または 1) に設定し、次に SCL ピンを High にプルして、短時間待機します (MPU_IIC_Delay() 関数を使用)。次に、SCL ピンを再び Low に引き下げ、さらに短時間待ちます。
この関数は、8 ビットすべてのデータが転送されると戻ります。最後に、MPU_IIC_Send_Byte 関数を呼び出して、MPU デバイスのアドレスを 1 ビット左にシフトし、最下位ビットを 0 (データの書き込みを意味する) に設定します。>

(1) (MPU_ADDR<<1)|0とは何ですか?
回答:マクロ定義では、0x68 は 01101000 として表されます。これは、左に 1 ビットシフトされて 11010000 になり、0 とビット単位の OR を実行して 11010000 を取得しますこのステップのポイントは何ですか? I2C プロトコルでは、デバイスのアドレスは 7 ビットの 2 進数であり、その最上位ビットは読み取りおよび書き込み操作を示す 0 または 1 でなければなりません。最下位ビットは、デバイスが読み取りまたは書き込み操作を実行することを示すために使用され、値 1 は読み取り操作を示し、値 0 は書き込み操作を示しますしたがって、デバイス アドレスを I2C バスに送信するときは、読み取り操作ではなく書き込み操作を保証するために、最下位ビットを 0 に設定する必要があります。
(2)C言語の観点から見ると、MPU_IIC_SDA=(txd&0x80)>>7;は何を意味しますか?
答え: 括弧内の txd&0x80 はビット単位の AND 演算で、16 進数の 0x80 は 10 進数の 128、つまり 10000000 です。この操作では、txd バイナリ表現の最上位ビット (つまり、7 番目のビット) を取得し、それを 7 ビット右にシフトすると、0 または 1 の値が取得されます。この値は、SDA ピンの出力値であるMPU_IIC_SDA 変数に割り当てられます。この動作は txd の最上位ビットを取り出して SDA 端子に書き込むのと同じです
(3) txd<<=1; とはどういう意味ですか?
答え: txd を 1 ビット左に移動します。(1) からわかるように、関数パラメーター txd=(MPU_ADDR<<1)|0 では、このコードでは、各サイクルが txd を 1 ビット左に移動するため、txd の各ビットをビットごとに処理できます。
初めて渡されるとき、txd==11010000、1 ビット左にシフトすると 10100000 になり、このとき 2 回目は MPU_IIC_SDA=(txd&0x80)>>7; でデータの 2 番目に上位のビットを送信し、txd を再び 1 ビット左にシフトする、というようになります。
txd の最上位ビットを処理した後、左シフト演算により 2 番目に上位のビットが最上位ビットとなり、次のサイクルで処理できるようになります。
(4) したがって。このコードは何をするのでしょうか?
回答: MPU_IIC_Send_Byte 関数では、最初に SDA ポートを出力モードに設定し、次にクロック ライン SCL を Low にしてデータ送信を開始します。次に、送信するデータ txd の各ビットを順番に SDA ラインに送信します。ビットを送信するたびに、クロック ライン SCL が一度 High に引き下げられ、次のビットの送信のために Low に引き下げられます。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 に格納し、読み取り操作の結果を返すことです。4 つのパラメータは次のとおりです。
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] の 2 バイト データを signed short 型に結合し、ポインタ変数 ax が指すアドレスに格納することです。具体的には、buf[0] の値を左に 8 ビットシフトし (つまり、256 を乗算)、buf[1] の値を加算して 16 ビットの符号なし整数を取得します。次に、この符号なし整数を short 型にキャストし、ポインター変数 *ax を使用して値を保存します。たとえば、0x1a と 0x2b を buf[0 と buf[1]] に格納する前に 16 ビット データにマージする必要があり、0x1a を強制的に 16 ビット データに変換して 8 ビット左にシフトして上位 8 ビットにすると、buf[1] とビット単位の OR 演算を行うと、データの下位 8 ビット == buf[1] となり、マージに成功します。

MPU6050センサーの加速度データは16ビットの符号付き整数であるため、buf[0]とbuf[1]の結合結果をsigned short型に変換する必要があります。ここでは、キャストを使用して、符号なし整数を符号付き整数に変換します。

組み込みにおけるビット演算の役割は何ですか?

回答: Embedded のビット演算は、バイナリ データの処理と操作に使用できます。次に例を示します。

圧縮データ: ディスプレイスメント、ビットごとの AND、ビットごとの OR などの演算によって複数のデータを 2 進数に圧縮でき、それによって記憶域スペースと送信帯域幅が削減されます。

効率の向上: 乗算、除算、剰余などの演算をビット演算に置き換えることができるため、コードがより簡潔かつ効率的になります。

制御ハードウェア:ビット操作によりレジスタのビットをセット、クリアすることができ、ハードウェアの制御、設定を実現します。

ビット フラグ: ビット操作を使用して、状態の管理と制御のためにフラグ ビットを設定およびクリアできます。

2. 構造は何ですか?

(1) 定義と例

C 言語の構造体 (struct) は、関連するデータ項目のグループを記述するために使用できるユーザー定義のデータ型です。構造体には、整数、浮動小数点数、文字、ポインタなどのさまざまなタイプのデータ項目を含めることができます。構造体の各データ項目は構造体メンバーと呼ばれ、メンバー アクセス演算子 (.) を介してアクセスできます。構造体の定義は通常、関数の外側に配置され、関数内でインスタンス化して使用できます。

単純な構造定義の例を次に示します。

struct student {
    
    
    char name[20];
    int age;
    float score;
};

上の例では、student という名前の構造体を定義しました。この構造体には、name、age、score の 3 つのメンバーが含まれています。このうち、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) この構造体を使用すると、struct xx を再初期化することなく、typedef の後に定義された名前を直接使用できるため、コードの単純さと読みやすさが向上します。

3. 構造の深い理解と応用

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 ライブラリ関数の開発において、構造体がペリフェラルのアドレスを表すことができるのはなぜですか?

組み込みシステムでは、通常、ペリフェラルのレジスタ マップ (レジスタ マップ) を表すために構造体が使用されますこれは、ペリフェラルは通常、メモリ マッピング (メモリ マップ 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中的成员变量

ここで 2 つの演算子の違いがわかります。

おすすめ

転載: blog.csdn.net/qq_53092944/article/details/131178100