打印
[应用相关]

STM32的位带操作

[复制链接]
554|12
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
为什么需要位带操作?
因为编程需要操作某个bit位来达到我们想要的功能,比如点灯需要操作GPIOA->ODR

的某个bit假设是第2bit,写1就可以让GPIO输出一个高电平。

GPIOA->ODR |= 1<<2;


这样写其实有三个隐含的操作:

//1.读取ODR寄存器的值到内存
//2.改写第2bit的值
//3.再把改写后的值写进ODR寄存器

这样的缺点:效率低

位带操作就是为了解决这个问题,前提是硬件支持这么做。

位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32没有这样的关键字,而是通过访问位带别名区来实现,例如

sbit LED P1^2
LED = 1;//输出高电平
LED = 0;//输出低电平


这样的优点:效率高

什么是位带别名区?
STM32本身不支持位操作,它发明了一种位带操作来让32的某些资源支持位操作。

这两个区域一个是 SRAM 区的最低 1MB 空间,令一个是外设区最低 1MB 空间。

这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。

位带别名区就是就是就是本来位的区域,变成了字的区域。

这里有个形象的解释:

打个形象的比方,以某个村,就张村把,该村有3户人家分别为A,B,C,我想给张村的A送礼,但是明文规定,不能给具体的个人送礼,但是可以给村委会送礼,那我该怎么办呢,OK,即日起,A不叫A了,改名叫做村委会1,B和C分别改叫做村委会2和村委会3,哦了,可以给A送礼了,虽然我送礼的对象是村委会1,听起来好像比个人级别高一点,但是最终收到礼物的还是个人A。同理,STM32不允许对某个端的某一个IO口进行操作,也就是PA.1 = 0或者PA.1 = 1这样的操作是非法的,好了,那我就给PA.1起个别名,将原来PA.1的位地址扩展成一个32位的字地址,对32位的地址进行操作,这个是STM32允许的,肯定是可以的,STM32对所有的寄存器配置,都是对某个32位地址的操作,因此说白了,操作一个32位寄存器来影响某个位的操作叫做位带操作。

例子来源:

https://www.cnblogs.com/szhb-5251/p/6662417.html

什么是位带区?
我们可以看到下面图中有两个位带区,分别是SRAM区里的0x20000000-0x200FFFFF地址段和片内外设区里的0x40000000-0x400FFFFF地址段(图中标号①处),它们的地址空间大小都是1M字节,在SRAM段内和外设地址段内的这1M大小的空间就是位带区,说白了就是支持位带操作的区域就是位带区。




位带区跟位带别名区有怎样的关系?
从上面映射图上可以看到,SRAM区里的0x22000000-0x23FFFFFF地址段和外设区里0x42000000-0x43FFFFFF地址段都是位带别名区,两个别名区空间大小都是32MB。那么,这32MB的位带别名区地址空间是怎么与1MB的位带区地址空间对应起来的呢?

答案:地址映射

那么问题来了?将1M字节里面的每一个bit映射到32M字节里面去,那么怎么映射呢?

首先明确一些概念:

1字节= 8bit
1字  = 4字节 = 32bit


看图




将1bit映射到1个字空间(扩大了32倍)

映射前的1个字节 = 映射后的8个字(扩大了32倍 8 * 4 = 32字节)

那么就得出以下结论:

映射前的1个字节 = 映射后的32个字节

映射前的1M字节 = 映射后的32M字节




0x40000000地址处的1个bit变成了0x42000010地址处的32个bit

为什么要将1bit空间要映射到一个字空间里去呢?我映射到1字节或者2字节的地址空间不行吗?我只能说,STM32是一个32位的机器,内核按字寻址的话寻址速度是最快的,所以别问这么多为什么,如果问了,答案就是为了速度。就好比你买个电脑用一个小箱子装着但是顺丰快递发货走的是集装箱,理论上来说装到集装箱里空运是最快的,要不然没办法上飞机啊…各位想想好像是这么个道理哈

位带操作该怎么用?
我们已经知道了位带区就是支持位操作的地址段,位带别名区就是位带区的地址映射,操作位带别名区就等价于操作位带区,并且我们知道了大致的映射过程,那么在STM32实际使用中又是怎么应用的呢?

在《Cortex M3权威指南》中,前人已经整理出了位带别名区与位带区地址对应关系的表达式,使用的时候只要套用公式就可以,如下图




将两个公式合并一下就得到:

AliasAddr = ((A & 0xF0000000)+0x02000000+((A &0x00FFFFFF)<<5)+(n<<2))

式中A为位带区地址,n为位序号

<<5 <<2又是什么鬼

2进制左移5位就相当于乘以2^5次方 就是扩大32倍的意思 为什么不写成*32 问就是效率 <<2同理扩大4倍

使用以下开源代码即可完成映射
// 把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x000FFFFF)<<5)+(bitnum<<2))

// 把一个地址转换成一个指针
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr))

// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum))


// GPIO ODR 和 IDR 寄存器地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+20)
#define GPIOB_ODR_Addr    (GPIOB_BASE+20)   
#define GPIOC_ODR_Addr    (GPIOC_BASE+20)  
#define GPIOD_ODR_Addr    (GPIOD_BASE+20)
#define GPIOE_ODR_Addr    (GPIOE_BASE+20)
#define GPIOF_ODR_Addr    (GPIOF_BASE+20)      
#define GPIOG_ODR_Addr    (GPIOG_BASE+20)
#define GPIOH_ODR_Addr    (GPIOH_BASE+20)      
#define GPIOI_ODR_Addr    (GPIOI_BASE+20)
#define GPIOJ_ODR_Addr    (GPIOJ_BASE+20)      
#define GPIOK_ODR_Addr    (GPIOK_BASE+20)

#define GPIOA_IDR_Addr    (GPIOA_BASE+16)  
#define GPIOB_IDR_Addr    (GPIOB_BASE+16)  
#define GPIOC_IDR_Addr    (GPIOC_BASE+16)   
#define GPIOD_IDR_Addr    (GPIOD_BASE+16)  
#define GPIOE_IDR_Addr    (GPIOE_BASE+16)   
#define GPIOF_IDR_Addr    (GPIOF_BASE+16)   
#define GPIOG_IDR_Addr    (GPIOG_BASE+16)  
#define GPIOH_IDR_Addr    (GPIOH_BASE+16)
#define GPIOI_IDR_Addr    (GPIOI_BASE+16)
#define GPIOJ_IDR_Addr    (GPIOJ_BASE+16)
#define GPIOK_IDR_Addr    (GPIOK_BASE+16)


// 单独操作 GPIO的某一个IO口,n(0,1,2...16),n表示具体是哪一个IO口
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出   
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入   

#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出   
#define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入   

#define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出   
#define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入   

#define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出   
#define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入   

#define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出   
#define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入  

#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出   
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入  

#define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出   
#define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入  

#define PHout(n)   BIT_ADDR(GPIOH_ODR_Addr,n)  //输出   
#define PHin(n)    BIT_ADDR(GPIOH_IDR_Addr,n)  //输入  

#define PIout(n)   BIT_ADDR(GPIOI_ODR_Addr,n)  //输出   
#define PIin(n)    BIT_ADDR(GPIOI_IDR_Addr,n)  //输入

#define PJout(n)   BIT_ADDR(GPIOJ_ODR_Addr,n)  //输出   
#define PJin(n)    BIT_ADDR(GPIOJ_IDR_Addr,n)  //输入  

#define PKout(n)   BIT_ADDR(GPIOK_ODR_Addr,n)  //输出   
#define PKin(n)    BIT_ADDR(GPIOK_IDR_Addr,n)  //输入  



理论上我们不仅可以使用公式对所有GPIO端口进行封装,我们也可以对STM32所有片内外设的寄存器进行封装(FSMC除外)

如图




使用注意事项
使用上面封装好的位带操作之前,要先对IO端口进行配置,否则操作结果不可预期。
PAout(n)作为左值使用,PAin(n)作为右值使用。(跟51单片机一样,我想你是懂51的)
最后,使用的过程中要注意一点,强制地址转换的时候一定要使用volatile关键字进行修饰,否则这个操作可能会被编译器优化掉
使用例子
Led.h 增加位带操作代码
#define LED0 PFout(9)
#define LED1 PFout(10)
#define BEEP PFout(8)


Key.h增加位带操作代码
#define KEY0  PEin(4)
#define KEY1  PEin(3)
#define KEY2  PEin(2)
#define KEY_UP PAin(0)


main.c示例代码
#include "stm32f4xx.h"
#include "led.h"
#include "delay.h"
#include "key.h"
#include "usart.h"
#include "bit_band.h"
int main(void)
{
        uint8_t i,key;
        LED_Init();
        KEY_Init();
        USART1_Init(115200);
        while(1)
        {
                key=ScanKeyVal(0);
                if(key)
                {
                        i=!i;
                        LED0=!LED0;
                        LED1=!LED1;
                }
        }
}


三、DS18B20温度传感器示例-位带控制实现时序
#include "ds18b20.h"
/*
函数功能: 硬件初始化--IO配置
硬件连接: PB15
*/
void DS18B20_Init(void)
{
    /*1. 开时钟*/
    RCC->APB2ENR|=1<<3; //PB
    /*2. 配置GPIO口模式*/
    GPIOB->CRH&=0x0FFFFFFF;
    GPIOB->CRH|=0x30000000;
    /*3. 上拉*/
    GPIOB->ODR|=1<<15;
}

/*
函数功能: 发送复位脉冲检测DS18B20硬件--建立通信过程
返 回 值: 0表示成功  1表示失败  
*/
u8 DS18B20_Check(void)
{
    u8 i;
    DS18B20_OUT_MODE(); //配置IO口为输出模式
    DS18B20_OUT=0;      //拉低
    delay_us(580);      
    DS18B20_OUT=1;      //拉高

    DS18B20_IN_MODE();  //配置IO口为输入模式
    for(i=0;i<100;i++)
    {
        if(DS18B20_IN==0)break;
        delay_us(1);
    }
    if(i==100)return 1;

    for(i=0;i<250;i++)
    {
       if(DS18B20_IN)break;
       delay_us(1);
    }
    if(i==250)return 1;
    return 0;
}

/*
函数功能: DS18B20写一个字节数据
*/
void DS18B20_WriteOnebyte(u8 cmd)
{
    u8 i;
    DS18B20_OUT_MODE(); //输出模式
    for(i=0;i<8;i++)
    {
        if(cmd&0x01) //发送1
        {
            DS18B20_OUT=0;
            delay_us(15);
            DS18B20_OUT=1;
            delay_us(45);
            DS18B20_OUT=1;
            delay_us(2);
        }
        else //发送0
        {
            DS18B20_OUT=0;
            delay_us(15);
            DS18B20_OUT=0;
            delay_us(45);
            DS18B20_OUT=1;
            delay_us(2);
        }
        cmd>>=1;
    }
}

/*
函数功能: DS18B20读一个字节数据
*/
u8 DS18B20_ReadOnebyte(void)
{
    u8 i;
    u8 data=0;
    for(i=0;i<8;i++)
    {
        DS18B20_OUT_MODE(); //输出模式
        DS18B20_OUT=0;
        delay_us(2);
        DS18B20_IN_MODE();
        delay_us(8);
        data>>=1; //右移1位
        if(DS18B20_IN)data|=0x80;
        delay_us(50);
        DS18B20_OUT=1;
        delay_us(2);
    }
    return data;
}

/*
函数功能: 读取一次DS18B20的温度数据
返回值: 读取的温度数据高低位
*/
u16 DS18B20_ReadTemp(void)
{
   u16 temp;
   u8 t_L,t_H;
   if(DS18B20_Check())return 1;
   DS18B20_WriteOnebyte(0xCC); //跳跃 ROM 指令 --不验证身份
   DS18B20_WriteOnebyte(0x44); //发送温度转换指令

   if(DS18B20_Check())return 2;
   DS18B20_WriteOnebyte(0xCC); //跳跃 ROM 指令 --不验证身份
   DS18B20_WriteOnebyte(0xBE); //读取RAM里的数据

   //读取温度
   t_L=DS18B20_ReadOnebyte(); //低字节
   t_H=DS18B20_ReadOnebyte(); //高字节
   temp=t_H<<8|t_L;
   return temp;
}
eturn 1;
   DS18B20_WriteOnebyte(0xCC); //跳跃 ROM 指令 --不验证身份
   DS18B20_WriteOnebyte(0x44); //发送温度转换指令

   if(DS18B20_Check())return 2;
   DS18B20_WriteOnebyte(0xCC); //跳跃 ROM 指令 --不验证身份
   DS18B20_WriteOnebyte(0xBE); //读取RAM里的数据

   //读取温度
   t_L=DS18B20_ReadOnebyte(); //低字节
   t_H=DS18B20_ReadOnebyte(); //高字节
   temp=t_H<<8|t_L;
   return temp;
}
————————————————
版权声明:本文为CSDN博主「初出茅庐的小李」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43176183/article/details/130447504

使用特权

评论回复
沙发
海滨消消| | 2023-5-30 11:01 | 只看该作者
这个解释确实生动又形象

使用特权

评论回复
板凳
kkzz| | 2023-6-7 21:25 | 只看该作者
在STM32的开发中,位操作是非常常用的操作之一。

使用特权

评论回复
地板
MessageRing| | 2023-6-7 22:26 | 只看该作者
实现位待操作的方法很妙啊

使用特权

评论回复
5
robincotton| | 2023-6-8 14:26 | 只看该作者
怎么直接操作寄存器               

使用特权

评论回复
6
modesty3jonah| | 2023-6-8 16:14 | 只看该作者
在进行位操作时要确保对应的寄存器已经被使能,否则操作可能会失败。

使用特权

评论回复
7
louliana| | 2023-6-8 16:32 | 只看该作者
位设置(Set Bit):将指定位置为1,可以使用“或”操作符(|) 实现,例如:GPIOA->ODR |= GPIO_ODR_OD0

位清零(Clear Bit):将指定位置为0,可以使用“与非”操作符(~),例如:GPIOA->ODR &= ~GPIO_ODR_OD0

使用特权

评论回复
8
louliana| | 2023-6-13 22:12 | 只看该作者
STM32怎么才能并行操作低八位的IO口或高八位的IO口?

使用特权

评论回复
9
Stahan| | 2023-6-13 22:53 | 只看该作者
robincotton 发表于 2023-6-8 14:26
怎么直接操作寄存器

直接指针操作地址写入数据

使用特权

评论回复
10
xiaoyaodz| | 2023-6-14 19:31 | 只看该作者
stm32中如何进行位定义               

使用特权

评论回复
11
MessageRing| | 2023-6-14 22:31 | 只看该作者
没看懂怎么实现位操作的

使用特权

评论回复
12
abotomson| | 2023-6-19 08:42 | 只看该作者
stm32g0系列支持位带操作吗

使用特权

评论回复
13
iyoum| | 2023-6-19 12:06 | 只看该作者
stm32 如何进行位运算?              

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

85

主题

4142

帖子

4

粉丝