ST MCU Finder
安装免费手机应用,
寻找理想的ST MCU

[应用相关] 我对STM32所用位带操作宏的超详细剖析、优势分析

[复制链接]
489|45
 楼主 | 2019-6-17 15:01 | 显示全部楼层 |阅读模式
在原子例程的sys.h中,使用宏定义建立了位带操作的基础,
使得操作IO端口可以像51一样实现位操作。
其实深入了解了位带操作的原理,几乎就可以实现对STM32所有外设寄存器的访问,
极端情况下,什么库函数版本,什么寄存器版本都可以不用,直接精准地操控所有寄存器的每一位的读写!!!

知道了STM32将所有外设寄存器的每一位都建立了位带别名区,
你只要再花一点点时间,彻底搞明白下面的三句宏定义,位带操作就都不在话下了:
#define BITBAND(addr, bitnum)          ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
#define MEM_ADDR(addr)                *((volatile unsigned long  *)(addr))
#define BIT_ADDR(addr, bitnum)       MEM_ADDR(BITBAND(addr, bitnum))

使用特权

评论回复
 楼主 | 2019-6-17 15:01 | 显示全部楼层
这三句是一环套一环的,
首先第一句:
#define    BITBAND(addr, bitnum)          ((addr & 0xF000 0000)+0x200 0000+((addr &0xF FFFF)<<5)+(bitnum<<2))
这一句定义了位带存储地址的计算方法,
知道了寄存器的地址,以及我们关心的寄存器的某一比特位,就可以根据此计算方法算出其对应的别名区地址
这个计算公式不仅对外设寄存器对应的别名区计算有用,对用户SRAM对应的别名区一样适用。
addr & 0xF000 0000 只取绝对地址的最高4位,实际上是用来区分段的,是寄存器段还是SRAM段。
+0x200 0000(值为32M)是别名区相对位段区的地址偏移量,别名区在相应位段上方的32M处;
(addr &0xF FFFF)<<5) 位段地址膨胀32倍,左移5位即可;
(bitnum<<2)由于每1比特膨胀为32位,32位占用4个字节的存储位置,所以计算地址时要乘以4,左移2位即是;

然后是第二句
#define   MEM_ADDR(addr)            *((volatile unsigned long  *)(addr))
上一句计算出来的地址只是一个数值,要将它强制转化成一个地址(并且声明这个地址存储的是一个32位的long型变量)
用(unsigned long  *)(addr) 即可,这样就成了一个真正的有血有肉的地址了。
前面再加一个*号,就可以访问这个地址得到其中的变量值了。
在C语言中,unsigned char *p; 定义p为一个指向unsigned char的地址指针;而 *p=1;就是向这个指针指向的地址所存储的变量赋值为1了。
至于中间加一个volatile关键字,则指示编译器不要自作主张对此进行优化,必须每次老老实实地去直接访问这个地址!!!

第三句呢?毫无难度,就是以前两句宏为基础的结合
#define BIT_ADDR(addr, bitnum)       MEM_ADDR(BITBAND(addr, bitnum))
给定寄存器的绝对地址addr,以及我们关心的比特位号bitnum,
先用BITBAND宏算出别名区对应的地址值
再用MEM_ADDR宏去访问这个地址

使用特权

评论回复
 楼主 | 2019-6-17 15:01 | 显示全部楼层
简单吧,这就是所有的位操作的奥秘了!!
有了这三句,你就可以完成所有的位操作,让我们举一个实例,比方说要置位GPIO A口的第9位,即让PA9输出高电平。
我们只须知道控制GPIO A的寄存器ODR的地址就行了,这个去查一下用户手册就行了,
一般手册上会给出两项,一是外设寄存器的基址,GPIOA的基址是0x4001 0800, 再找ODR,手册上一般给出其偏移量0C,
也就是说,GPIOA的ODR寄存器是0x4001 0800+0C=0x4001 080C
什么?你不知道寄存器的地址怎么查? 哈哈,早有人替你查好了,并且为你查好,定义了下列宏:
#define GPIOA_ODR_Addr    (0x4001 0800+0C) //0x4001080C
并且一切为你着想,好事做到底,还定义了宏:
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)

简单到你想置位GPIO A口的第9位,只须使用语句:PAout(9)=1;就行了。

使用特权

评论回复
 楼主 | 2019-6-17 15:02 | 显示全部楼层
怎么是这样的呢?因为有前面这些宏定义为基础,
反正闲着没事儿,我就当一回编译器,把这句PAout(9)=1一步步地编译出来,宏的展开就是一个替换的过程:
PAout(9)=1;因为定义了PAout(n) 要替换成 BIT_ADDR(GPIOA_ODR_Addr,n),所以展开成:
BIT_ADDR(GPIOA_ODR_Addr,9)=1;因为定义了BIT_ADDR(addr, bitnum) 要替换成 MEM_ADDR(BITBAND(addr, bitnum)),所以展开成:
MEM_ADDR(BITBAND(GPIOA_ODR_Addr, 9))=1;因为定义了BIT_ADDR(addr, bitnum) 要替换成 MEM_ADDR(BITBAND(addr, bitnum)),所以展开成:
MEM_ADDR((GPIOA_ODR_Addr & 0xF0000000)+0x2000000+((GPIOA_ODR_Addr &0xFFFFF)<<5)+(9<<2))=1;
最后一步,因为定义了MEM_ADDR(addr)要替换成 *((volatile unsigned long  *)(addr))
所以展开成为如下的语句,不要晕倒哦,*((volatile unsigned long  *)((GPIOA_ODR_Addr & 0xF0000000)+0x2000000+((GPIOA_ODR_Addr &0xFFFFF)<<5)+(9<<2)))=1;

神奇吧?
一句  PAout(9)=1;
与     *((volatile unsigned long  *)((GPIOA_ODR_Addr & 0xF0000000)+0x2000000+((GPIOA_ODR_Addr &0xFFFFF)<<5)+(9<<2)))=1;
是完全等效的。

而这,就是宏定义的效能和魅力!

使用特权

评论回复
 楼主 | 2019-6-17 15:02 | 显示全部楼层
还有:因为定义了GPIOA_ODR_Addr就是(0x4001 0800+0C),哦,等一下,我先算出数值来吧,GPIOA_ODR_Addr就是0x4001 080C,得到:   
*((volatile unsigned long  *)((0x4001 080C & 0xF0000000)+0x2000000+((0x4001 080C &0xFFFFF)<<5)+(9<<2)))=1;

看着很长,其实有了具体数值,算出结果就短了:
解&运算符:得到*((volatile unsigned long  *)(0x4000 0000 +0x200 0000+(0x0001 080C<<5)+(9<<2)))=1;
即:*((volatile unsigned long  *)(0x4200 0000 +(0x0001 080C<<5)+(9<<2)))=1;
解移位运算符:9=1001 经过<<2得到 100100 即0x24;
1 080C=0001 0000 1000 0000 1100经过<<5得到0010 0001 0000 0001 1000 0000 即0x21 0180
所以语句变成:*((volatile unsigned long  *)(0x4200 0000 + 0x21 0180 + 0x24)=1;
最后结果就是如下语句(以上这些过程都只是预编译器干的话,实际交付编译器的也就是下面这一句):
*((volatile unsigned long  *)(0x4221 01A4)=1;
说成大白话,就是给0x4221 01A4这个地址中所存储的变量赋值为1.  
   
(注意:这个变量是一个long型的,32位,占用从0x4221 01A4开始的连续4个存储单元)
但是ARM的设计师们并没有在物理上设计这些存储单元(也永远不允许这些存储单元实际存在!!!),取而代之的是设计了位映射机制:
凡是访问别名区域地址的操作,都被转换为访问其所映射对应的比特位,
*(0x4221 01A4)=1的执行结果就是:GPIOA的ODR寄存器第9位=1

使用特权

评论回复
 楼主 | 2019-6-17 15:02 | 显示全部楼层
除了可以操作STM32所有片上外设寄存器的每一比特位外,
我们还可以操作SRAM区的每一比特位。
因为,片上SRAM的全部,无一例外地都落在位带区。
我们正常程序中声明的所有变量,因此也都被分配在这一个位带区。
只要我们知道了变量的地址,也就可以通过其相应的别名区地址按比特访问。
比如:我们可以将程序中用到的标志位集中定义到一个变量(8-32位均可)
给这个变量分配一个固定地址的单元,
然后在程序中按位来访问这些标志,
这样可以提高软件的效率。

使用特权

评论回复
 楼主 | 2019-6-17 15:02 | 显示全部楼层
另外还要指出的是:
虽然位段区的每一位都被映射到别名区膨胀到了32位,
但这32位只是个名头而已,实际只有最低位有效。

对别名区的访问,是双向的:
对别名区的读:结果非0即1,反应的是对应位段的某一比特位的值。
对别名区的写:只有最低位有效,效果是将对应位段区的某一比特位置1或清0.    写入0和写入FE效果是完全一样的。

使用特权

评论回复
 楼主 | 2019-6-17 15:03 | 显示全部楼层
前面的分析我们是以寄存器区的位段操作为例来剖析的。
SRAM区的位段操作也是一样的机制,
我前面提到,
我们所定义的变量,都被分配在SRAM区,
只不过我们一般不关心这些变量的具体地址,
而现在我们必须知道地址,才能进行位段操作。
下面我和大家分享一下SRAM位段操作的具体方法:

使用特权

评论回复
 楼主 | 2019-6-17 15:03 | 显示全部楼层
首先是,如何才能事先得到变量的地址呢?
这就需要用到C语言的一项功能,声明变量时可以指定地址:
方法是使用attribute,格式是__attribute__((at(addr)))
注意要求是双下划线,再加双括号,
addr就是我们可以指定的地址。

使用特权

评论回复
 楼主 | 2019-6-17 15:04 | 显示全部楼层
网上有一种方法是用结构体来实现SRAM的位操作:
具体实现代码是:
#define __BITBAND__         __attribute__((bitband))
#define __BITBAND__ADDR(addr)         __attribute__((at(addr)))

//定义位带结构类型
typedef struct
{
        u32 a1:1;   //宽度为1位
        u32 a2:1;   //宽度为1位
}_STATE_FLAG __BITBAND__;

//声明一个结构类型变量,并指定存储地址
volatile _STATE_FLAG StateFlag __BITBAND__ADDR(0x2000A000);

//在程序中使用方法示例
//  StateFlag.a1=0;  
// if(!StateFlag.a1) StateFlag.a1=1;

具体原理我不详述了,反正我用不习惯。

使用特权

评论回复
 楼主 | 2019-6-17 15:04 | 显示全部楼层
这里我建议大家使用下面的方法:
这个方法沿袭了前面寄存器位带操作的成果
便于加深理解,并且感觉比上面的结构体少占内存。
代码是:
#define MyFlag_Addr    0x2000A100   //选择SRAM区的一个固定地址
//定义一个全局变量作为集中的标志位寄存器, 分配于上述固定地址
volatile u32 MyFlag __attribute__((at(MyFlag_Addr)));

#define My**(n) BIT_ADDR(MyFlag_Addr,n)  //可以通过改变n访问不同的标志位

//也可以通过下列宏定义访问单一的标志位, 每一个标志位可以自定义一个有明确意义的名称
#define My**0   BIT_ADDR(MyFlag_Addr,0)  //标志位0
#define My**1   BIT_ADDR(MyFlag_Addr,1)  //标志位1

//访问时用例:  My**(n)=1; 或者: My**0=1;

使用特权

评论回复
 楼主 | 2019-6-17 15:04 | 显示全部楼层
另外,还有一种方法也一样,那就是: 直接定义一个常数指针变量指向该标志位寄存器的别名区基址
volatile u32* const pMyFlag=(void *)(0x22000000+((MyFlag_Addr &0xFFFFF)<<5));
//用const通知编译器:该指针不能再被修改而指向其它地址

//访问时用例:  *(pMyFlag+n)=1; 或者数组形式: pMyFlag[n]=1;

以上两种方法和所贴出来的代码,我今天已经完全验证过了,可以放心使用。

使用特权

评论回复
 楼主 | 2019-6-17 15:05 | 显示全部楼层
本帖最后由 characteristic 于 2019-6-17 15:07 编辑

补充说明一下:
定义一个固定的地址的问题,如11楼的如下宏定义语句:
#define MyFlag_Addr    0x2000A100   //选择SRAM区的一个固定地址
这个地址如何确定呢?
我们在KEIL中编译完程序后,编译结果的后两项就是与SRAM相关的,即RW_data和ZI_data
系统对SRAM的需求就是这两项之和,从地址2000 0000开始。
我们定义的固定地址在这之后就可以了。即地址要大于2000 0000+RW_data+ZI_data
为了保险起见,再多加点余量,尽量往后一点定义就行了。

使用特权

评论回复
 楼主 | 2019-6-17 15:05 | 显示全部楼层
但注意不要超出你器件的SRAM地址范围,
如F103C8T6,SRAM的SRAM为20K,
即地址范围为:2000 0000至2000 5000
则定义地址时,不能超出2000 5000

使用特权

评论回复
 楼主 | 2019-6-17 15:05 | 显示全部楼层
STM32内部的位段操作机理的实质是:把对别名区的地址的读写--〉映射成对相应的位的读写
而不是反过来(注意理解二者的差别)。
这是STM32设计的一种内部机制,它无须CPU的寄存器(Rn)参与运算,直接在CACHE中完成原子操作,效率较高。
我们有些人经常反过来说:“比特位被映射到别名区”,其实是对这种内部机制规则的反向推算。
那个著名的别名地址计算公式实质是由比特位反推出对应的别名区地址,其目的就是为了
算得别名区地址后,运用STM32的内部机制,通过读写别名区实现对其相对应的比特位的读写。

使用特权

评论回复
 楼主 | 2019-6-17 15:06 | 显示全部楼层
具体效果是:
对2200 0000的读写,被映射成对2000 0000所存字节中BIT0的读写;
对2200 0004的读写,被映射成对2000 0000所存字节中BIT1的读写;
对2200 0008的读写,被映射成对2000 0000所存字节中BIT2的读写;
......
注意:别名区的地址是以字(32位,占用连续的4个字节)对齐的,
每个字只有LSB有意义。在访问别名区时,要把地址对齐到字的边界上,
否则会产生不可预料的结果。什么意思?就是说:只能对别名区中的LSB所在的地址进行读写。
如上述22000000、22000004、22000008、......等地址是有效的,这些地址都是4的整数倍。
必须避免对其它的地址如:22000001、20000002......20000007等地址的读写!!!

使用特权

评论回复
 楼主 | 2019-6-17 15:06 | 显示全部楼层
本帖最后由 characteristic 于 2019-6-17 15:07 编辑

回到在SRAM区实现位操作的话题。

个人认为,11楼所介绍的方法是最理想的方法,
手工分配一个固定地址存放标志位综合寄存器就好了。
有些人可能要问,如果不用手工分配固定地址的方法行不行?
我今天也进行了一些探索,是完全可行的。

那就是在程序中查询地址。
闲话少说,直接上代码:
u32 MyFlag;   //定义一个我们用于存放各类标志位的全局变量
volatile u32*  pMyFlag;   //再定义一个指向此变量对应的别名区地址的指针

//   在系统初始化代码中,查询并计算出这个别名指针的具体值
        u32 *pwMyFlag;  //临时定义一个指向MyFlag变量本身的指针
        pwMyFlag=&MyFlag;    //查询该变量的地址赋值给pwMyFlag
        pMyFlag=(void *)(0x22000000+(((u32)pwMyFlag & 0xFFFFF)<<5));  //计算出对应的别名区地址

//这样,所有的准备工作就完成了,以后可在程序的任何地方使用位操作
//访问时用例-置位某标志:  *(pMyFlag+n)=1; 或者数组形式: pMyFlag[n]=1;
//条件语句用例: if(pMyFlag[3])    pMyFlag[3]=0;

使用特权

评论回复
 楼主 | 2019-6-17 15:07 | 显示全部楼层
下面给出手工分配固定地址(即11楼本人推荐方法)的完整测试代码:
#define MyFlag_Addr    0x20001800   //选择SRAM区的一个固定地址
//定义一个全局变量作为集中的标志位寄存器, 分配于上述固定地址
volatile u32 MyFlag __attribute__((at(MyFlag_Addr)));

#define My**(n) BIT_ADDR(MyFlag_Addr,n)  //可以通过改变n访问不同的标志位
//也可以通过下列宏定义访问单一的标志位, 每一个标志位可以自定义一个有明确意义的名称
#define My**0   BIT_ADDR(MyFlag_Addr,0)  //标志位0
#define My**1   BIT_ADDR(MyFlag_Addr,1)  //标志位1
#define My**2   BIT_ADDR(MyFlag_Addr,2)  //标志位2
#define My**3   BIT_ADDR(MyFlag_Addr,3)  //标志位3

//测试例程:可以在main()中调用,也可直接作为main()的主体
void BITBAND_TEST(void)
{

        MyFlag=2;  //赋初值为2,即最低四位(下同,共32位这里只关注低四位的变化)为0010
        
        if(My**1) My**1=0;  //如果BIT1为1,则清为0,此时MyFlag变为0000
               
        My**3=1;   //置位BIT3  
        printf(temp, "Flag=%d ",MyFlag);        //显示MyFlag值,此时为1000值应为8;        
        
        My**0=1;                 //置位BIT0   
        printf("Flag=%d ",MyFlag);//显示MyFlag值,此时为1001值应为9;
        
        while(1);   //测试完毕,进入死循环。如要做其它测试,可对上述代码进行相应修改。
   
}

使用特权

评论回复
 楼主 | 2019-6-17 15:08 | 显示全部楼层
继续研究,对18楼的测试例程进行反汇编如下:
   143:         MyFlag=2;  //赋值为2,即最低四位(下同,共32位这里只关注低四位的变化)为0010
0x0800019A 2002      MOVS     r0,#0x02
0x0800019C 49FE      LDR      r1,[pc,#1016]  ; @0x08000598处所存的内容(MyFlag分配的地址值)赋值给R1
0x0800019E 6008      STR      r0,[r1,#0x00]        ;寻址这一MyFlag地址,将R0的值存入
   144:         if(My**1) My**1=0;  //如果BIT1为1,则清为0,此时MyFlag变为0000
   145:                  
0x080001A0 48FE      LDR      r0,[pc,#1016]  ; @0x0800059C,标志位My**0的地址(2203 0000),指向别名区
                                             
0x080001A2 6840      LDR      r0,[r0,#0x04]  ; 加#4后为标志位My**1的地址,指向别名区
0x080001A4 B110      CBZ      r0,0x080001AC
0x080001A6 2000      MOVS     r0,#0x00
0x080001A8 49FC      LDR      r1,[pc,#1008]  ; @0x0800059C,(此处存地址值2203 0000)
0x080001AA 6048      STR      r0,[r1,#0x04]
   146:         My**3=1;   //置位BIT3   
0x080001AC 2001      MOVS     r0,#0x01      
0x080001AE 49FB      LDR      r1,[pc,#1004]  ; @0x0800059C(此处存地址值2203 0000)
0x080001B0 60C8      STR      r0,[r1,#0x0C]  ; 加#12后为标志位My**3的地址,指向别名区
......
......

0x08000598 1800      DCW      0x1800       ; 这里存放了我们手工指定的地址值:2000 1800
0x0800059A 2000      DCW      0x2000
0x0800059C 0000      DCW      0x0000      ; 这里存放了计算出来的别名区基址:2203 0000
0x0800059E 2203      DCW      0x2203

使用特权

评论回复
 楼主 | 2019-6-17 15:08 | 显示全部楼层
由上楼可以清楚的看到:
编译出来的最终目标代码,为32位的变量MyFlag按我们手工指定,分配了固定地址:2000 1800
编译器根据我们的宏定义,算好了别名区基址:2203 0000
当我们访问My**0时,目标代码执行的是访问地址2203 0000
当我们访问My**1时,目标代码执行的是访问地址2203 0000+4
当我们访问My**2时,目标代码执行的是访问地址2203 0000+8
当我们访问My**3时,目标代码执行的是访问地址2203 0000+12
......
即访问My**[n]时,目标代码执行的是访问地址2203 0000+4*n;
KEILC编译器是把这些地址当成一般的正常地址来对待的,
而实际执行时,由STM32的内部机制,把对这些实际并不存在的地址的访问,
映射并执行成了对相应比特位的访问。

使用特权

评论回复
扫描二维码,随时随地手机跟帖
您需要登录后才可以回帖 登录 | 注册

本版积分规则

我要发帖 投诉建议 创建版块 申请版主

快速回复

您需要登录后才可以回帖
登录 | 注册
高级模式

论坛热帖

在线客服 快速回复 返回顶部 返回列表