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

2023-11-02

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;

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言操作寄存器的方法总结 的相关文章

  • 最大类间方差(大津法)

    1 概述 最大类间方差法是由日本学者大津 Nobuyuki Otsu 于1979年提出的 是一种自适应的阈值确定的方法 又叫大津法 简称OTSU 它是按图像的灰度特性 将图像分成背景和目标2部分 背景和目标之间的类间方差越大 说明构成图像的

随机推荐

  • 好玩的脚本代码大全_Scriptable脚本——网易云热评

    Scriptable脚本 网易云热评 今天我为大家带来新的作品 iOS14桌面组件神器 Scriptable 原创脚本 精美作品分享 喜欢的话就点关注吧 更多脚本正在路上 效果图 如何使用 iPhone 上下载 Scriptable App
  • Tree with Maximum Cost---CF1092F 树上DP

    F Tree with Maximum Cost time limit per test2 seconds memory limit per test256 megabytes inputstandard input outputstand
  • LoRa学习总结(三)

    之前总结是LoRa的基本知识 接下来是都是代码的结构或者细节方面的总结 1 协议介绍 协议将分成三层 射频层 MRFI 网络层 NWK 应用层 APP 2 射频层 这里没有通常所说的物理层和数据链路层 因为这是有Radio芯片将传输过来的数
  • 第3期大模型前沿讲习班报名中,顶尖专家面授,多角度系统培训

    人工智能研究与应用范式正经历一场剧变 越来越多的顶级团队和杰出人才纷纷加入这一变革浪潮 作为AI大模型科研先锋 智源研究院携手一批卓越的学者与工程师 致力于将尖端技术与经验传授给有潜力的学习者 通过高效的学习方式 让更多人能迅速融入这一重要
  • 基于JAVA新型农村消费贷电商平台计算机毕业设计源码+系统+数据库+lw文档+部署

    基于JAVA新型农村消费贷电商平台计算机毕业设计源码 系统 数据库 lw文档 部署 基于JAVA新型农村消费贷电商平台计算机毕业设计源码 系统 数据库 lw文档 部署 本源码技术栈 项目架构 B S架构 开发语言 Java语言 开发软件 i
  • C++ 小记 :使用 string 定义变量要先初始化

    使用 string 时先给其初始化 否则可能出现未知错误 初始化时 如下是错误示例 std string str NULL 如下是正确示例 std string str
  • doxygen简明使用教程

    1 工作环境 ubuntu18 04 2 安装二进制 sudo apt get install doxygen doxygen doc doxygen gui graphviz 一次性二进制全安装上 发现竟然已经安装过了 3 调用该命令雕出
  • AIGC图像生成的原理综述与落地畅想

    AIGC 这个当前的现象级词语 本文尝试从文生图的发展 对其当前主流的 Stable Diffusion 做一个综述 以下为实验按要求生成的不同场景 风格控制下的生成作品 概述 技术演进一 昙花初现 GAN 家族 GAN 系列算法开启了图片
  • Android中Application类用法

    Application类 Application和Activity Service一样是Android框架的一个系统组件 当Android程序启动时系统会创建一个 Application对象 用来存储系统的一些信息 Android系统自动会
  • Unity查找物体方法的详细对比

    Unity查找物体方法的详细对比 我们开发中常用的查找物体的方法有 GameObject Find transform Find FindGameObjectWithTag FindGameObjectsWithTag FindObject
  • Ubuntu系统里使用gcc和Makefile编译c程序

    1 在windows环境下通过虚拟机软件 比如Vmware VirtualBox 安装Ubuntu 16 04 18 04 Desktop 并设置网络参数保证系统能上网 熟练掌握Ubuntu常用命令 并学习使用vi vim 或nano 或g
  • Visual Studio 2017 集成Crystal Report为ASP.NET MVC呈现报表

    最近项目需要实现报表功能 平衡各方面的因素 还是使用Crystal Report 水晶报表 下载较新版本 http downloads businessobjects com akdlm cr4vs2010 CRforVS 13 0 21
  • GetBytes的长度区别

    C 的GetBytes的长度区别 string s 欧 耶 string ss abc defg Encoding Default 是指当前系统设置的 默认字符集编码方式 Encoding ASCII 将 Unicode 字符编码为单个 7
  • Android动态化UI框架一、Virtualview-Android

    Android动态化UI框架一 Virtualview Android GitHub alibaba Virtualview Android 简介 A light way to build UI in custom XML Virtualv
  • 华为OD机试真题-机房布局/栈解法【2023.Q1】

    小明正在规划一个大型数据中心机房 需要满足的条件是 确保在每个机柜边上至少要有一个电箱 已知 机房排成1排 我们用M表示机柜 I表示间隔 请你返回这整排机房 至少需要多少个电箱 如果无解请返回 1 输入描述 第一行输入一个字符串 由 M 和
  • 人与计算机通信,需要达到哪些要求?

    目前人们使用计算机时 大多是用计算机的高级语言 如C Java等语言 编制程序来告诉计算 做什么 以及 怎么做 的 这对计算机的利用带来了诸多不便 严重阻碍了计算机应用的进一步推广 如果能让计算机 听懂 看懂 人类语言 如汉语 英语等 那将
  • 深度对抗学习在图像分割和超分辨率中的应用

    原文 http blog csdn net shenziheng1 article details 72821001 深度对抗学习在图像分割和超分辨率中的应用 原创 2017年05月31日 16 43 15 1982 1 前言 深度学习已经
  • Flutter 屏幕适配

    志当存高远 诸葛亮 屏幕尺寸大全 菜单栏共有5个选项 包括手机 平板 手表 电脑 显示器 分别显示屏幕尺寸 PPI 纵横比 dp和px 单位下的宽 高 以及DPI 官方设计规范 适配原理 屏幕尺寸 严格来说 屏幕尺寸实际被物理尺寸和显示分辨
  • sqlserver 如何备份或恢复一个表_MS SQL Server

    如果只想备份或恢复单个表而不想备份或恢复整个数据库的话 往往有以下方法 1 在Sql server2000 中可以使用DTS来将该表的数据导出成另外的文件格式 当需要恢复时 可以将该文件中数据再通过DTS导入 或者建立DTS脚本来完成 2
  • C语言操作寄存器的方法总结

    1 C语言位操作操作寄存器 操作位有两种方法 一种是位字段 另一种是使用按位运算符 下表为几种位操作符及其含义 不改变其他位的值的状况下 对某几个位进行设值 在嵌入式编程中 常常需要对一些寄存器进行配置 有的情况下需要改变一个字节中的某一位