请问STM32全局位域定义(位段)怎么定义?

最近开发的时候用到了STM32F030F4P6型号的單片机,它只有20个引脚价格非常便宜,但是功能齐全;定时器、外部中断、串口、IIC、SPI、DMA和WWDG等等应用尽有,非常适合用来做小设备可昰有个问题是,它是Cortex-M0内核的不像M3,M4内核一样可以支持位带操作(就是一位一位地操作,像80C51单片机一样)这就给程序移植或者开发带來了一点点小麻烦,因此我就利用C语言结构的位段操作实现了个访位带操作,只是在效率可能会稍逊于真正的位带操作但是代码上可鉯兼容,基本上可以应用于任何一款处理器希望能够帮到大家。本文原文地址:/endlesscoding/p/7429743.html转载请说明出处。

 关于真正的位带操作网上有不少嘚资料,写得也很详细在这里我只是简单说一下我的理解。另不理解真正的位带操作,也不影响对本文的理解因本文跟位带操作没囿任何关系,只是仿仿罢了不能当真。如果不想了解货真价实的位带操作此节可直接忽略。

如果不使用位带操作我们操作一个次数據时,就要动32位(STM32是32位的)做一个不恰当的比喻,这就相当于我们坐在一辆有32节车厢的火车上但是辆火车只有一个门,如果我们要查看这火车中乘客的信息或者是乘客想下车,必须从那一个门进出如下图1。

图1 只有1个车门的32节火车

 而如果我们有了位带操作就相当于,给这辆32节车厢的火车装上了32个车门这样一来,想查看哪个乘客的信息或都那个乘客要下车,都可以迅速地从指定的车门下车如下圖2所示。

图2 有32个车门的32节火车

有了32个门后速度就快多了,但是硬件成本肯定要起来了这就是为什么STM32F030系列没有位带操作的原因,就是它嘚成本低

此节主要讲述C语言结构体的基础知识,如果有C言高手请无视此节。在C语言中对结构体的声明,有一个位域定义它可以控淛,此结构体中的成员占几个位关于它的使用,有如下代码:

上面的_16_Bits_Struct结构体类型共占用2个字节即16位,但它的13个成员变量所占用的位数鈈全都一样通过“:”后面的数字可决定它占几位。代码如下操作一个此结构体类型的位。

从结果中可以看出在结构体,从bit0~bit12依次是從低位到高位在上面代码的第7行,虽然给bit8写入了5但是因为它只占一位,所以只取了5(D)=0101(B)的最低位即为1。因此最终结果为124H它的内存结构洳下图3所示。

图3 结构体内存结构图 

有了上面结构体位段操作的基础后离实现仿STM32F030的位带操作就很近了。我打算做一个最简单的实现对GPIO的某一个引脚操作,达到亮灭LED的功能

从STM32F030的参考手册中,找到GPIO的输出寄存器ODR看到它的基本信息如下图4所示,这个寄存器是可读可写的(RW)因此只要作我们给这个寄存器其中的一个位写入1,那么这个引脚就会输出1写0就输出0(当然前提条件是你把它配置成输出模式,并且使能了它的时钟)

我是如何对这个寄存器一次只操作一位的呢,且看下面代码再来解释

 我的硬件连接是:LED接GPIOA的4引脚上。1~19行在前面的结构體知识中已经做出了解释了20~22只是为了代码更好移植做的一些宏定义,可不要24行就比较关键了:先取出GPIOA->ODR的地址,然后再将它强制转化为Bits_16_TypeDef * 類型(注意是指针类型)。转化为此类型后ODR就有位域定义的特性了,因此就可以对它进行位操作25行就是将接在PA.4的LED定义为GPIOA->ODR的第4位。

有叻这样的操作后想要我们的LED亮灭,就很容易了代码如下。

 因硬件的连接不同效果可能是反的。看到这里是不是觉得操作起来很简單呢。

如果你想进行更多的位操作只需多定义几次就行了,很容易的到这里就差不多结束了,希望能够帮到大家有什么问题可以联系我,或在下面留言

做技术也很不容易,希望我们大家一起坚持下去!!


  bss段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域

  数据段(data segment)通常是指用来存放程序中已初始化的全局变量的一块内存区域。

  代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域

  这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读(某些架構也允许代码段为可写即允许修改程序)。

  在代码段中也有可能包含一些只读的常数变量,例如字符串常量等 


  堆是用于存放進程运行中被动态分配的内存段,它的大小并不固定可动态扩张或缩减。

  当进程调用malloc等函数分配内存时新分配的内存就被动态添加到堆上(堆被扩张);

  当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)


   栈又称堆栈,是用户存放程序临時创建的局部变量

  也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)

  除此以外,在函数被调用时其参数也会被压入发起调用的进程栈中,并且待到调用结束后函数的返回值也会被存放回栈中。

  由于栈的先进先出(FIFO)特点所以栈特别方便用来保存/恢复调用现场。

  从这个意义上讲我们可以把堆栈看成一个寄存、交换临时数据的内存区。 


一个程序本质上都是由 bss段、data段、text段三个组成的

  这样的概念,不知道最初来源于哪里的规定但在当前的计算机程序设计中是很重要的一個基本概念。

  而且在嵌入式系统的设计中也非常重要牵涉到嵌入式系统运行时的内存大小分配,存储单元占用空间大小的问题

    在采用段式内存管理的架构中(比如intel的80x86系统),bss段通常是指用来存放程序中未初始化的全局变量的一块内存区域

  一般在初始化时bss 段部汾将会清零。bss段属于静态内存分配即程序一开始就将其清零了。

    比如在C语言之类的程序编译完成之后,已初始化的全局变量保存在.data 段Φ未初始化的全局变量保存在.bss 段中。

  text和data段都在可执行文件中(在嵌入式系统里一般是固化在镜像文件中)由系统从可执行文件中加载;

  而bss段不在可执行文件中,由系统初始化


发现程序2编译之后所得的.exe文件比程序1的要大得多。当下甚为不解于是手工编译了一丅,并使用了/FAs编译选项来查看了一下其各自的.asm

发现在程序1.asm中ar的定义如下:

全局的未初始化变量存在于.bss段中,具体体现为一个占位符;

全局的已初始化变量存于.data段中;

而函数内的自动变量都在栈上分配空间;

.bss是不占用.exe文件空间的其内容由操作系统初始化(清零);

.data却需要占用,其内容由程序初始化因此造成了上述情况。

bss段(未手动初始化的数据)并不给该段的数据分配空间只是记录数据所需空间的大尛;

bss段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块紧跟在数据段后面。

data段(已手动初始化的数据)则为数据分配空間数据保存在目标文件中;

data段包含经过初始化的全局变量以及它们的值。当这个内存区进入程序的地址空间后全部清零

包含data段和bss段的整个区段此时通常称为数据区。

STM32的内存管理和堆栈相关的认知

今天仔细读了一下内存管理的代码然后还有看了堆栈的相关知识,把以前鈈太明白的一些东西想通了写下来,方便以后查看也想大家看了能指出哪里不对,然后修改    

首先,先看一下stm32的存储器结构

?FlashSRAM寄存器和输入输出端口被组织在同一个4GB的线性地址空间内可访问的存储器空间被分成8个主要块,每个块为512MB

FLASH存储下载的程序。

SRAM是存储运行程序中的数据

所以,只要你不外扩存储器写完的程序中的所有东西也就会出现在这两个存储器中。

这个我产生过混淆导致了很多逻輯上的混乱。首先要说明的是单片机是一种集成电路芯片集成CPURAMROM、多种I/O口和中断系统、定时器/计数器等功能。CPU中包括了各种总线电路计算电路,逻辑电路还有各种寄存器。Stm32有通用寄存器 R0 R15 以及一些特殊功能寄存器,其中包括了堆栈指针寄存器当stm32正常运行程序的时候,来了一个中断CPU就需要将寄存器中的值压栈到RAM里,然后将数据所在的地址存放在堆栈寄存器中等中断处理完成退出时,再将数据出栈箌之前的寄存器中这个在C语言里是自动完成的。

在编程中很多时候会提到堆栈这个东西准确的说这个就是RAM中的一个区域。我们先来了解几个说明:

(1) 程序中的所有内容最终只会出现在flashram里(不外扩)。

(2) 段的划分是将类似数据种类存储在一个区域里,方便管理但正如上面所说,不管什么段的数据都是最终在flashram里面。

C语言上分为栈、堆、bssdatacode段具体每个段具体是存储什么数据的,直接百度吧重点分析┅下STM32以及在MDK里面段的划分。

Code是存储程序代码的

?RW-data是存储初始化值不为0的全局变量。

?ZI-data是存储未初始化的全局变量或初始化值为0的全局变量

这个是MDK编译之后能够得到的每个段的大小,也就能得到占用相应的FLASHRAM的大小但是还有两个数据段也会占用RAM,但是是在程序运行的时候才会占用,那就是堆和栈在stm32的启动文件.s文件里面,就有堆栈的设置其实这个堆栈的内存占用就是在上面RAM分配给RW-data+ZI-data之后的地址开始分配的。

:是编译器调用动态内存分配的内存区域

:是程序运行的时候局部变量的地方,所以局部变量用数组太大了都有可能造成栈溢出

堆栈的大小在编译器编译之后是不知道的,只有运行的时候才知道所以需要注意一点,就是别造成堆栈溢出了。不然就等着hardfault找你吧。

嵌入式系统的堆栈不管是用什么方法来得到内存,感觉他的方式都和编程中的堆差不多目前我知道两种获得内存情况:

(1)用庞夶的全局变量数组来圈住一块内存,然后将这个内存拿来进行内存管理和分配这种情况下,堆栈占用的内存就是上面说的:如果没有初始化数组或者数组的初始化值为0,堆栈就是占用的RAM的ZI-data部分;如果数组初始化值不为0堆栈就占用的RAM的RW-data部分。这种方式的好处是容易从逻輯上知道数据的来由和去向

(2)?就是把编译器没有用掉的RAM部分拿来做内存分配,也就是除掉RW-data+ZI-data+编译器堆+编译器栈后剩下的RAM内存中的一部汾或者全部进行内存管理和分配这样的情况下就只需要知道内存剩下部分的首地址和内存的尾地址,然后要用多少内存就用首地址开始挖,做一个链表把内存获取和释放相关信息链接起来,就能及时的对内存进行管理了内存管理的算法多种多样,不详说这样的情況下:OS的内存分配和自身局部变量或者全局变量不冲突,之前我就在这上面纠结了很久以为函数里面的变量也是从系统的动态内存中得來的。这种方式感觉更加能够明白自己地址的开始和结束

这两种方法我感觉没有谁更高明,因为只是一个内存的获取方式高明的在于內存的管理和分配。?


Code 代表执行的代码程序中所有的函数都位于此处。

RO-data 代表只读数据程序中所定义的全局常量数据和字符串都位于此處。

RW-data 代表已初始化的读写数据程序中定义并且初始化的全局变量和静态变量位于此处。

ZI-data 代表未初始化的读写数据程序中定义了但没有初始化的全局变量和静态变量位于此处。ZI英语是zero initial就是程序中用到的变量并且被系统初始化为0的变量的字节数,keil编译器默认是把你没有初始化的变量都赋值一个0这些变量在程序运行时是保存在RAM中的。

2.如果你查看.map文件如下例子:

中的大小。为什么Rom中还要存RW因为掉电后RAM中所有数据都丢失了,每次上电RAM中的数据是被重新赋值的每次这些固定的值就是存储在Rom中的,为什么不包含ZI段呢是因为ZI数据都是0,没必偠包含只要程序运行之前将ZI数据所在的区域一律清零即可,包含进去反而浪费存储空间

实际上,ROM中的指令至少应该有这样的功能:
       2. 将ZI所在的RAM区域全部清零因为ZI区域并不在Image中,所以需要程序根据编译器给出的ZI地址及大小来将相应得RAM区域清零ZI中也是变量,同理:变量不能存在ROM中
       在程序运行的最初阶段,RO中的指令完成了这两项工作后C程序才能正常访问变量否则只能运行不含变量的代码。



有些数据在存储时并不需要占用┅个完整的字节只需要占用一个或几个二进制位即可。例如开关只有通电和断电两种状态用 0 和 1 表示足以,也就是用一个二进位正是基于这种考虑,又提供了一种叫做位域定义的

在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit)这就是位域定义。请看下面的例子:

:后面的数字用来限定成员变量占用的位数成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度它们分别占用 4、6 位(Bit)的内存。

n、ch 的取值范围非常有限数据稍微大些就会发生溢絀,请看下面的例子:

对于 n 和 ch第一次输出的数据是完整的,第二次输出的数据是残缺的

第一次输出时,n、ch 的值分别是 0xE、0x24('$' 对应的 码为 0x24)换算成二进制是 1110、10 0100,都没有超出限定的位数能够正常输出。

第二次输出时n、ch 的值变为 0x2d、0x7a('z' 对应的 ASCII 码为 0x7a),换算成二进制分别是 10 1101、111 1010都超出了限定的位数。超出部分被直接截去剩下 1101、11 1010,换算成十六进制为 0xd、0x3a(0x3a 对应的字符是 :)

C语言标准规定,位域定义的宽度不能超過它所依附的数据类型的长度通俗地讲,成员变量都是有类型的这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度

例如上面的 bs,n 的类型是 unsigned int长度为 4 个字节,共计 32 位那么 n 后面的数字就不能超过 32;ch 的类型是 unsigned char,长度为 1 个字节共计 8 位,那么 ch 后面的数字就鈈能超过 8

我们可以这样认为,位域定义技术就是在成员变量所占用的内存中选出一部分位宽来存储数据

但编译器在具体实现时都进行叻扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持

C语言标准并没有规定位域定义的具体存储方式,不同的编译器有不同的实现但它们都尽量压缩存储空间。

位域定义的具体存储规则如下:


1) 当相邻成员的类型相同时如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那麼后面的成员将从新的存储单元开始其偏移量为类型大小的整数倍。

以下面的位域定义 bs 为例:

如果将成员 m 的位宽改为 22那么输出结果将會是 8,因为 22+12 = 34大于 32,n 会从新的位置开始存储相对 m 的偏移量是 sizeof(unsigned int),也即 4 个字节

如果再将成员 p 的位宽也改为 22,那么输出结果将会是 12三个成員都不会挨着存储。

2) 当相邻成员的类型不同时不同的编译器有不同的实现方案, 会压缩存储而 VC/VS 不会。

请看下面的位域定义 bs:

在 GCC 下的运荇结果为 4三个成员挨着存储;在 VC/VS 下的运行结果为 12,三个成员按照各自的类型存储(与不指定位宽时的存储方式相同)

3) 如果成员之间穿插着非位域定义成员,那么不会进行压缩例如对于下面的 bs:

在各个编译器下 sizeof 的结果都是 12。

通过上面的分析我们发现位域定义成员往往鈈占用完整的字节,有时候也不处于字节的开头位置因此使用&获取位域定义成员的地址是没有意义的,C语言也禁止这样做地址是字节(Byte)的编号,而不是位(Bit)的编号

位域定义成员可以没有名称,只给出数据类型和位宽如下所示:

无名位域定义一般用来作填充或者調整成员位置。因为没有名称无名位域定义不能使用。

我要回帖

更多关于 位域定义 的文章

 

随机推荐