找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1385

积分

0

好友

177

主题
发表于 2026-2-15 06:23:55 | 查看: 33| 回复: 0

1 位操作

位操作与位带操作并不相同。位操作针对的是变量的每一位进行运算,而逻辑位操作则是对整个变量进行运算。在嵌入式开发,特别是像 STM32 这样的单片机编程中,位操作是直接操作硬件寄存器的必备技能。

以下是六种常用的位操作运算符:

C语言位运算符含义速查表

按位取反

void test01()
{
    int num = 7;
    printf("~num = %d\n", ~num); //-8

    // 0111  按位取反   1000   机器中存放的都是补码   
    //补码转换原码需要分有符号数和无符号数两种
}

按位与

void test02()
{
    int num = 128;
    //换算为八位,1换算就是00000001, 这样只要所给数字的二进制最后一位是1.那么就是奇数,否则就是偶数
    if ( (num & 1) == 0)    
    {
        printf("num为偶数\n");
    }
    else
    {
        printf("num为奇数\n");
    }
}

按位异或

void test03()
{
    //按位异或的意思是,两个数字相同为0,不同为1。我们可以利用按位异或实现两个数的交换
    num01 = 1; // 0001
    num02 = 4; // 0100
    printf("num01 ^ num02 = %d", num01 ^ num02); // 5  两个二进制按位异或之后是: 0101

    printf("交换前\n");
    printf("num01 = %d\n", num1);
    printf("num02 = %d\n", num2);

    num01 = num01 ^  num02;
    num02 = num01 ^  num02;
    num01 = num01 ^  num02;
    //不用临时数字实现两个变量交换
    printf("交换后\n");
    printf("num01 = %d\n", num1);
    printf("num02 = %d\n", num2);
}

按位或

计算方法:
参加运算的两个数,换算为二进制(0、1)后,进行或运算。只要相应位上有一个为1时就取1,全为0时才取0。

printf 是格式化输出函数,可以直接打印十进制、八进制、十六进制,对应的输出控制符分别为 %d%o%x。但标准库没有提供二进制的直接输出方式。如果需要输出二进制,可以手动转换,也可以调用 stdlib.h 里的 itoa 函数。虽然 itoa 不是标准库函数,但大多数编译器都支持它。

#include <stdio.h>
#include <stdlib.h>

int main()
{
    test04();    
}

int test04()
{
    int a = 6;                  //二进制0110
    int b = 3;                  //二进制0011
    int c = a | b;              //a、b按位或,结果7,二进制111,赋值给c
    char s[10];
    itoa(c, s, 2);
    printf("二进制 --> %s\n", s); //输出:二进制 -->111
}

左移运算符

void test05()
{
    int num = 6;
    printf("%d\n", num << 3); //左移三位,就是48 (0011 0000)
}

右移运算符

void test06()
{
    int num = 6; //0110
    printf("%d\n", num >> 1); //右移一位,就是0011,输出3
}

以上是用普通C代码举的例子。下面我们来看一下在STM32实际开发中,位操作通常如何使用:

(1)改变寄存器特定位的状态。例如要操作 GPIOA->BSRRL 寄存器,通常先对目标位进行 & 清零操作。

GPIOA->BSRRL &= 0xFF0F; //将第4位到第7位清零(注意编号是从0开始的)

然后再与需要设置的值进行 | 或运算:

GPIOA->BSRRL |= 0x0040; //将第4位到第7位设置为我们需要的数字

(2)通过位移操作提高代码的可读性:

GPIOx->ODR = (((uint32_t)0x01) << pinpos);

这行代码的意思是,先将 0x01 转换为三十二位二进制数,然后左移 pinpos 位。pinpos 是一个变量,其值就是需要设置的引脚位。最终效果是将 ODR 寄存器的第 pinpos 位设置为1。

(3)使用取反操作设置寄存器:

状态寄存器(SR)的每一位代表一个状态。如果某个时刻我们想将某一位设为0,同时保证其他位都为1,最简单的做法是直接给寄存器赋一个值:

TIMx->SR=0xFFF7;

这种写法虽然将第3位设为了0,但可读性很差。我们看看ST官方库函数里是怎么做的:

TIMx->SR = (uint16_t)~TIM_FLAG;

TIM_FLAG 是通过宏定义定义的值:

#define TIM_FLAG_Update            ((uint16_t)0x0001) 
#define TIM_FLAG_CC1               ((uint16_t)0x0002)

2 define宏定义

define 是C语言中的预处理命令,用于宏定义。它可以提高源代码的可读性,并为编程提供便利。在STM32的库文件和工程配置中,宏定义被大量使用。

常见的格式:

#define 标识符 字符串

“标识符”即所定义的宏名,“字符串”可以是常数、表达式或格式串等。例如,在STM32的时钟配置中常见:

#define PLL_Q 7  //注意,这个定义语句的最后不需要加分号

3 ifdef条件编译

在嵌入式程序开发,尤其是跨平台或多配置项目中,条件编译 #ifdef 非常常用。

#ifdef PLL_Q
   程序段1
#else
  程序段2
#endif

这段代码的作用是:如果标识符 PLL_Q 已经被定义过,则编译“程序段1”;否则,编译“程序段2”。当然,和普通的 if-else 语句一样,#else 分支也可以省略。

#ifndef 的含义则正好相反:

#ifndef PLL_Q    //意思就是如果没有定义这个标识符

4 extern变量声明

C语言中,extern 可以放在变量或函数前,表示该变量或函数的定义在别的文件中,提示编译器遇到此变量或函数时到其他模块中寻找其定义。注意,一个变量只能定义一次,但可以被 extern 声明多次。

使用例子如下:

extern u16 USART_RX_STA;

这行代码声明了变量 USART_RX_STA 在其他文件中已经定义。这里的 u16uint16_t 的类型别名,表示16位的无符号整数。

5 结构体

结构体是C语言中组织相关变量的强大工具,在STM32的HAL库和寄存器映射中广泛应用。定义一个 结构体 的一般形式为:

struct 结构名
{
  成员列表
};

成员列表由若干个成员组成,每个成员都是该结构的一个组成部分。对每个成员也必须作类型说明,形式如下:

类型说明符  成员名; //比如:int num;

结合上面的说明,我们可以构建一个简单的结构体例子:

struct sutdent
{
  int num;
  char name[20];  //20个字节长的字符
  char sex;
  int age;
  float score;
  char addr[30]; //30个字节长的字符
};

定义结构体变量有两种方式:在定义结构体的同时定义,或者先定义结构体类型再另外定义变量。

struct sutdent
{
  int num;
  char name[20];  //20个字节长的字符
  char sex;
  int age;
  float score;
  char addr[30]; //30个字节长的字符
}student01,student02; //变量名表列(如果已有结构体变量名,那么我们可以不写结构体名称)

有时我们可能需要用到结构体的嵌套:

struct date
{
  int year, month,day;
};
struct sutdent
{
  int num;
  char name[20];  //20个字节长的字符
  char sex;
  struct date birthday; //这里就用到了结构体的嵌套
  int age;
  float score;
  char addr[30]; //30个字节长的字符
}student01,student02; //变量名表列(如果已有结构体变量名,那么我们可以不写结构体名称)

如果需要引用结构体里面的成员,使用点操作符 .

student01.name = 小李; 
// 结构体变量名.成员名(注意这里用的是点),这里是对这个成员的赋值

结构体指针

结构体指针变量说明的一般形式为:

struct 结构名 *结构指针变量名

假如我们想定义一个指向结构体 student 的指针变量 pstu

struct student *pstu;

如果要给一个结构体指针变量赋初值,应使用结构体变量的地址:

struct student
{
  char name[66];
  int num;
  char sex;
}stu;

pstu = &stu;

重要提示:赋值时必须使用结构体变量(如 &stu),而不能使用结构体名(如 &student)。因为结构名只是一种类型形式,编译器不会为其分配内存空间(即没有地址);而结构体变量是实体,编译器会为其分配内存。

访问结构体成员有两种等价形式:

(*pstu).name;   //(1)(*结构指针变量).成员名;
 pstu->name;    //(2)结构指针变量->成员名

6 typedef类型别名

typedef 用来为现有类型创建一个新的名字(类型别名),以简化变量的定义。例如前面 extern 声明中的 u16,就是对 uint16_t 的简化。在STM32开发中,typedef 最常用于定义结构体和枚举的类型别名。

假设我们定义了一个结构体 _GPIO

struct _GPIO
{
  __IO uint32_t MODER;
  __IO uint32_t OTYPER;
  ...
};

如果要用它来定义一个结构体变量 GPIOA,代码是这样的:

struct _GPIO GPIOA;

虽然可行,但每次都要写 struct _GPIO 比较繁琐。使用 typedef 可以大大简化:

typedef struct
{
  __IO uint32_t MODER;
  __IO uint32_t OTYPER;
  ...
} GPIO_TypeDef;

定义完成后,我们可以像使用基本类型一样使用这个别名来定义变量:

GPIO_TypeDef _GPIOA, _GPIOB;

这正是ST标准库中采用的方式。通过 typedef,代码变得更加简洁清晰,这也是阅读和理解STM32 HAL库或标准外设库代码的基础。

掌握这些C语言核心知识,尤其是位操作、指针 与结构体的运用,是进行高效、可靠STM32嵌入式开发的基石。在实践中多思考、多练习,你就能更得心应手地操控硬件寄存器,写出更优雅的嵌入式代码。如果你对这些底层知识有更多的兴趣或疑问,欢迎来 云栈社区 与更多开发者一起交流探讨。




上一篇:Claude Code Agent Teams 配置与实战:解决单智能体上下文瓶颈,实现高效多角色协作
下一篇:掌握H桥电路:实现直流电机的正反转与PWM调速
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 12:59 , Processed in 0.960118 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表