C语言操作寄存器的方法总结

1、C语言位操作操作寄存器

操作位有两种方法,一种是位字段,另一种是使用按位运算符。下表为几种位操作符及其含义:

不改变其他位的值的状况下,对某几个位进行设值。

在嵌入式编程中,常常需要对一些寄存器进行配置,有的情况下需要改变一个字节中的某一位或者几位,但是又不想改变其它位原有的值,这时就可以使用按位运算符进行操作。下面进行举例说明,假如有一个8位的TEST寄存器:

当我们要设置第0位bit0的值为1时,可能会这样进行设置:

TEST = 0x01;

但是,这样设置是不够准确的,因为这时候已经同时操作到了高7位:bit1~bit7,如果这高7位没有用到的话,这么设置没有什么影响;但是,如果这7位正在被使用,结果就不是我们想要的了。

在这种情况下,我们就可以借用“&”和“|”进行配置。

对于二进制位操作来说,不管该位原来的值是0还是1,它跟0进行&运算,得到的结果都是0,而跟1进行&运算,将保持原来的值不变;不管该位原来的值是0还是1,它跟1进行|运算,得到的结果都是1,而跟0进行|运算,将保持原来的值不变。

所以,此时可以设置为:

TEST = TEST | 0x01;

其意义为:TEST寄存器的高7位均不变,最低位变成1了。在实际编程中,常改写为:

TEST |= 0x01;

这种写法可以一定程度上简化代码,是 C 语言常用的一种编程风格。

同样的,要给TEST的低4位清0,高4位保持不变,可以进行如下配置:

TEST &= 0xF0;

这个场景单片机开发中经常使用,方法就是先对需要设置的位用&操作符进行清零操作,然后用|操作符设值。比如我要改变GPIOA的状态,可以先对寄存器的值进行&清零操作:

GPIOA->CRL &= 0XFFFFFF0F; //将第4-7位清0

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

GPIOA->CRL |= 0X00000040; //设置相应位的值,不改变其他位的值

移位操作提高代码的可读性。

移位操作在单片机开发中也非常重要,下面让我们看看固件库的GPIO初始化的函数里面的一行代码:

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

这个操作就是将BSRR寄存器的第pinpos位设置为1,为什么要通过左移而不是直接设置一个固定的值呢?其实,这是为了提高代码的可读性以及可重用性。这行代码可以很直观明了的知道,是将第pinpos位设置为1。如果你写成:

GPIOx->BSRR = 0x0030;

这样的代码就不好看也不好重用了。
类似这样的代码很多:

GPIOA->ODR |= 1 << 5; //PA.5输出高,不改变其他位

这样我们一目了然,5告诉我们是第5位也就是第6个端口,1告诉我们是设置为1了。
~取反操作使用技巧

SR寄存器的每一位都代表一个状态,某个时刻我们希望去设置某一位的值为0,同时其他位都保留为1,简单的作法是直接给寄存器设置一个值:

TIMx->SR = 0xFFF7

这样的作法设置第3位为0,但是这样的作法同样不好看,并且可读性很差。看看库函数代码中怎样使用的:

TIMx->SR = (uint16_t)~TIM_FLAG;

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

#define TIM_FLAG_Update  ((uint16_t)0x0001)
#define TIM_FLAG_CC1     ((uint16_t)0x0002)
#define TIM_FLAG_CC2     ((uint16_t)0x0004)
#define TIM_FLAG_CC3     ((uint16_t)0x0008)
#define TIM_FLAG_CC4     ((int16_t)0x0010)
#define TIM_FLAG_COM     ((uint16_t)0x0020)
#define TIM_FLAG_Trigger ((uint16_t)0x0040)
#define TIM_FLAG_Break   ((uint16_t)0x0080)
#define TIM_FLAG_CC1OF   ((uint16_t)0x0200)
#define TIM_FLAG_CC2OF   ((uint16_t)0x0400)
#define TIM_FLAG_CC3OF   ((uint16_t)0x0800)
#define TIM_FLAG_CC4OF   ((uint16_t)0x1000)

即设置SR第3位为0时可设置为:
TIMx->SR = (uint16_t)~TIM_FLAG_CC3;

2、C语言位域解析及在操作寄存器方面的应用

位域的概念
位域(或者也能称之为位段,英文表达是 Bit field)是一种数据结构,可以把数据以位元的形式紧凑的存储,并允许程序员对此结构的位元进行操作。这种数据结构的好处是:
可以使数据单元节省存储空间,当程序需要成千上万个数据单元时,这种数据结构的优点也就很明显地突出出来了。
位段可以很方便地访问一个整数值的部分内容从而简化程序源代码。
位域的定义
总体来说位域的定义可以分为两大类,一个是结构体位域,一个是共用体体位域,由于共用体和结构体两者在定义上的形式都是相同的,因此对于位域的定义从形式上看,两者也都是相同的。
结构体位域
结构体位域定义的一般形式如下所示:

struct 位域结构体名
{
    类型说明符 位域名 : 长度;
}结构体变量名;
举个简单的例子进行说明:
struct example0{unsigned char x : 3;unsigned char y : 2;unsigned char z : 1;}ex0_t;

上述定义是什么意思呢,用一张图就能很清楚地明白,下图是所定义的结构体位域在内存中的存储位置:

从图中我们可以看出,虽然 x 的类型是 unsigned char ,但是并没有占 8 个位,而是占了 3 个位,其取值范围也变成了 0 ~ 2^3-1。通过上述图片我们也可以猜到这个结构体位域的大小,笔者通过 printf 函数输出结构体位域的大小为:
1.The Value of sizeof(ex0_t) is : 1 byte
关于结构体位域的大小遵循这样一个原则:整个结构体位域的总大小为最宽基本类型成员大小的整数倍,这一原则与笔者在上一篇文章《结构体内存对齐解析》中写的结构体的总大小的原则是相同的。
共用体位域
共用体位域定义的一般形式跟结构体定义的一般形式是大致相同的,直接举一个简单的例子进行说明:

1.union example1
2.{
3.    unsigned char x : 3;
4.    unsigned char y : 2;
5.    unsigned char z : 1;
6.}ex1_u;

同样的,笔者在这里给出共用体位域在内存中的存储位置:

这里笔者也给出共用体位域的大小:

1.The Value of sizeof(ex1_u) is : 1 byte

由此也可以得出共用体位域大小遵循的原则是:共用体位域的总大小为最大基本类型成员的大小
结构体位域详解
位域的类型使用无符号型
正如标题所示,在位域的使用过程中使用无符号的数据类型,下面给出一个例子来说明这个例子:

1.    struct BitField_8
2.    {
3.        char a : 2;
4.        char b : 3;
5.    }BF8;
6.
7.    BF8.a = 0x3;/* 11 */
8.    BF8.b = 0x5;/* 101 */
9.    printf("%d,%d\n",BF8.a,BF8.b);

上述的输出结果为:
1.-1,-3
输出结果并不是我们想要的,究其原因,实际上是因为 BF.a ,BF.b 都是有符号的,那么自然也就有符号位的存在,而最高位为 1 代表负数,负数又是以补码的形式存储在计算机中的,所以也就有了上述的结果。因此为了避免上述这种问题的出现,应该将 BitField_8 中的 char 转换成 unsigned char ,那输出的结果就是 3,5
位域禁止的操作
由于位域的特殊,同时也有了一些跟普通变量不同的特性:
1.结构体位域成员不能够使用取址操作

1.    struct BitField_8
2.    {
3.        unsigned char a : 2;
4.    }BF8;
5.    printf("%p\n",&BF8.a); /*错误*/

1.结构体位域成员不能够用 static 修饰

1.    struct BitField_8
2.    {
3.        static unsigned char a : 2;/*错误*/
4.    }BF8;

结构体位域成员不能够使用数组

1.    struct BitField_8
2.    {
3.        unsigned char a[5] : 5;/*错误*/
4.    }BF8;

不同处理器,不同编译器对位域的影响
位域虽然能够以位的形式操作数据,但是也被人们告知要慎重使用,原因就在于不同的处理器结构,不同的编译器对于位域的一些特性会产生不同的结果,这也就是位域移植性差的原因
处理器影响
处理器对位域造成的影响也很容易理解,大端模式和小端模式的处理器会对下面的结构体位域产生不一样的存储方式,这里比较简单,如果对这个问题不清楚的朋友可以看笔者的这篇文章《union 的概念及在嵌入式编程中的应用》。
编译器影响
结构体位域成员不同类型
不同的编译器对于位域会有不同的结果,比如下面这段代码:

1.struct BitField_5
2.{
3.    unsigned int a : 4;
4.    unsigned char b : 4; 
5.}BF_8;
6.
7.int main(void)
8.{
9.    printf("The Value of sizeof(BF_8) is:%lu bytes\n",sizeof(BF_8));
10.}

上述所定义的结构体位域中,对于结构体位域内成员不同数据类型,不同的编译器有不同的处理,对于 Visual Studio 来说,面对不同的数据类型时,对于上述这个例子,存储完第一个成员 a 后,会重新另起 4 byte 的空间进行存储,因此对于上述代码在 Visual Studio 的运行结果是:
1.The Value of sizeof(BF_8) is 8 bytes
可见在 vs 环境下这样使用位域不但没有能够节省内存空间,反而相比于结构体还扩大了。上述是 VS 环境下的测试结果,下面是在 GCC 环境下的测试结果:
1.The Value of sizeof(BF_8) is 4 bytes
可见在 GCC 环境下,就算结构体位域成员的数据类型不一致,它其实按照“压缩”数据的方式进行存储的,也就是说结构体位域里的成员都是挨着存放的。
成员大小之和超过一个基本存储空间
除了上述成员不同类型对于不同编译器有不同的处理方式,当成员大小之和超过一个基本存储空间时,不同的编译器也有不同的处理方式,比如下面这段代码:

1.struct short_flag_t
2.{
3.    unsigned short a : 7;
4.    unsigned short b : 10;  
5.};

对于上面这段代码,同类型成员除了这样定义之外,也可以这样定义:

1.struct short_flag_t
2.{
3.    unsigned short a : 7,/*注意此处是逗号*/
4.                   b : 10;
5.};

上面的代码因为 unsigned short 的大小是 2 个字节,而成员 a,b加起来的大小已经超过了 2 个字节,所以这种情况下也就有了以下两种存储方式:
a , b 紧邻
b 在下一个可存储它的存储单元内分配内存
不同编译器可能面对这种情况会采用不同的存储方式,对于 GCC 来说,采用的是第二种,如果编译器采用的是第一种方式,而程序要求又需要按照第二种方式来进行存储,又该如何办呢?这时就要利用匿名 0 长度位域字段的语法强制位域在下一个存储单元存储,示例代码如下:

1.struct short_flag_t
2.{
3.    unsigned short a : 2;
4.    unsigned short   : 0;
5.    unsigned short b : 3;
6.}

上述代码对于 a , b 来讲,b 便不会紧挨着 a 进行存储,而是强制使 b 在下一个存储单元进行存储。
位域的应用
上述便是位域涉及的基本概念,那知道了基本概念之后,又能使用位域做些什么呢?最容易另人想到的就是使用结构体位域定义标志位,由于我们在裸机开发的过程中,没有信号量,事件等机制,通常会定义一些范围只存在于 0~1 的开关量,而在没有使用位域之前,最小的变量类型都是 1 个字节,使用结构体位域将能够根据取值范围定义该变量的位数,从而起到节省内存的作用。
用于访问微控制器的寄存器
位域受到处理器和编译器的影响,在使用前我们必须清楚当前处理器是大端对齐还是小端对齐,必须清楚当前编译器对所定义的位域有何影响
如果我们现在要使用位域访问一个 8 位的寄存器,这个寄存器大致长这个样子:
那么我们就可以使用结构体位域构造这样一个数据结构:

1.typedef union
2.{
3.    unsigned char Byte;
4.    struct
5.    {
6.        unsigned char bit012 : 3;
7.        unsigned char bit34  : 2;
8.        unsigned char bit5   : 1;
9.        unsigned char bit6   : 1;
10.        unsigned char bit7   : 1;
11.    }bits;
12.}registerType;

现在假设我们这个寄存器的地址是 0x0000 8000,那么我们就可以定义一个指针并使其指向这个地址,如下:

1.registerType *pReg = (registerType *)0x0000 8000;

在进行了上述定义之后,我们就可以对寄存器进行操作了,首先,我们可以使用位域的方式操作寄存器的位,比如这样:

1.pReg->bits.bit5 = 1;
2.pReg->bits.bit012 = 7;

当然也可以利用 union 的特性直接操作整个寄存器,如下:

1.pReg->Byte = 0x55;

使用位域完成对于寄存器的访问,对于上述例子来讲,我们必须要注意的一点是此例子是基于小端对齐模式的。
总结
位域的用法虽然看起来更加灵活了,但是在使用时也要对我们的处理器和编译器有所了解,如果为了写出移植性较高的程序,应该避免使用位域。

  • 24
    点赞
  • 143
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
C语言中,寄存器位移运算是通过位操作符来实现的。位操作符在操作寄存器时具有特殊作用。寄存器是按位进行规划和使用的,读写时是以32位整体进行操作的。在操作寄存器时,可以改变某一特定位,而不影响其他位。例如,可以通过位与操作符"&"来改变寄存器中的某些位。 举个例子,假设有两个无符号整数变量a和b,分别为0xa12aaaa7和0xFFFF00FF。要将a和b的对应位进行位与运算,可以使用位与操作符"&",并将结果赋值给另一个变量c。具体代码如下: unsigned int a = 0xa12aaaa7; unsigned int b = 0xFFFF00FF; unsigned int c; c = a & b; 上述代码将a和b进行位与运算,并将结果保存在变量c中。可以使用打印语句输出c的值。 printf("a & b = %#X.\n", c); printf("a & b = 0x%x.\n", c) 这样就可以打印出c的值。在这个例子中,位与运算会将a和b的对应位进行逐位比较,只有在对应位都为1时,结果位才为1,否则为0。 除了寄存器位移运算,还可以使用位移操作符来构建特殊的二进制数。通过位移操作符可以获取特定位为1或0的二进制数。例如,可以使用位移操作符来获取bit3-bit7为1,同时bit23-bit25为1,其余位都为0的数,代码如下: unsigned int num = (0x1F << 3) | (0x7 << 23); 这样就可以得到一个特殊的二进制数。通过位移操作符和按位或操作符,可以灵活地构建需要的二进制数。 总结起来,寄存器位移运算是通过位操作符来实现的。位操作符可以用来改变寄存器中的某一特定位,同时可以用来构建特殊的二进制数。在C语言中,位操作符有不同的作用,可以对位进行逐个取反,也可以进行逻辑运算。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C语言寄存器的位操作](https://blog.csdn.net/qixjocd12345/article/details/108304967)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [逻辑运算之寄存器移位运算](https://blog.csdn.net/weixin_38066782/article/details/129373220)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值