一,原理简述
什么是位带区?
位带区是指内存中的一块区域,用于存储位带操作的目标数据。在ARM3架构中,位带区的大小通常为32位,即可以存储32个位的数据。每个位在位带区中都有一个对应的位地址。
什么是位带别名区?
位带别名区是与位带区相对应的另一块区域,用于通过别名方式访问位带区的数据。位带别名区的大小与位带区相同,但是每个别名地址对应于位带区中的一个位。
它们如何配合实现位带操作?
位带操作的实现是通过位带别名区来完成的。当我们需要读取或写入位带区中的某个位时,可以通过位带别名区的别名地址来进行操作。具体过程如下:
- 首先,我们需要定义位带别名区的起始地址和结束地址。这些地址范围内的内存将被用作位带别名区。
- 根据位带区中的位地址,计算出对应的别名地址。ARM3架构使用了位带区的基地址和位地址的偏移来计算别名地址。
- 通过别名地址进行位带操作。读取操作时,可以直接读取别名地址对应的内存值;写入操作时,可以直接写入别名地址对应的内存值。
通过使用位带别名区,我们可以以位操作的方式读取或写入位带区中的单个位,而不需要进行位运算或者使用临时变量。这种方式可以提高代码的可读性和执行效率。需要注意的是,ARM3架构中的位带操作是通过特殊的位带指令来实现的,而不是直接访问内存地址。这些指令可以自动将位带别名区的别名地址转换为对应的位带区地址,从而完成位带操作。
最终实现的效果就是将按字节寻址变成按位寻址。
没看懂没关系其他的博主都有非常详细的原理讲解。但都没有实操的详细讲解,笔者将以此为重点展开讲解。
二,实操单独控制GPIOA8口
第一步
打开ARM3的内核指南,可以看到位带区到位带别名区的地址偏移的算法。
一个是片内的一个是片上的算法,很明显stm32的GPIO为片上外设,我们应该用下面的地址偏移算法。
第二步
寻找公式中的参数A和n,打开自己芯片的参考手册找到GPIOA的IDR和ODR寄存器地址即为参数A,为什么是这两个寄存器,大家应该都知道吧!因为它们控制输出与输入,这也侧面说明在进行位段操作前要记得初始管脚,n是8,因为我们要操作8号管脚。
我的芯片为STM32G431rbt,则我的GPIOA的IDR地址为:基地址:0x4800 0000+偏移地址:0x10同样的ODR的地址为:基地址:0x4800 0000+偏移地址:0x14
第三步:
代码编写
/* 位带宏定义 */
#define BITBAND(addr, bitnum) ((0x42000000+(addr-0x40000000)*32+bitnum*4))
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
/* 地址 */
#define GPIOA_ODR_Addr (GPIOA_BASE+0x14)
#define GPIOA_IDR_Addr (GPIOA_BASE+0x10)
/* 最终包装 */
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
代码解读:
1.addr即为A
2.bitnum即为n
3.GPIOA_BASE为hal库提供的基地址宏定义,它的值即为0x4800 0000
4.注意得到地址要把它转化为指针,这样才能访问指定地址的寄存器
5.#define BITBAND(addr, bitnum) ((0x42000000+(addr-0x40000000)*32+bitnum*4)) 其实可以改写为#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))这样会更快,实际上大家都是这么做的,因为单片机更擅长做位移操作。
细节注意:
位带操作的地址范围是0x4000 0000到0x400F FFFF,上面案例的GPIOA基地址:0x4800 0000不在该范围,经实际验证无法进行位带操作,以下的stm32f103c8的GPIOA为该范围内的正确演示,方法都是一样的只是要注意寄存器所在范围。在此感谢 superman0000007 读者的宝贵意见,笔者起初并未注意到该问题。
三,演示
stm32cubemx配置
main.c部分代码
#include "main.h"
#include "usart.h"
#include "gpio.h"
/* 位带宏定义 */
#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))
/* 地址 */
#define GPIOA_ODR_Addr (GPIOA_BASE+0x0C)
#define GPIOA_IDR_Addr (GPIOA_BASE+0x08)
/* 最终包装 */
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
void SystemClock_Config(void);
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
while (1)
{
PAout(8)=1;
HAL_Delay(500);
PAout(8)=0;
HAL_Delay(500);
}
}
实验结果
四,结束语
你可能疑惑为什么整这么麻烦,直接HAL_GPIO_WritePin()不就完了吗?单独点个灯或许没什么问题,但你在需要频繁改变状态且高速的LCD驱动中,使用HAL库提供的高度包装函数,会让你的LCD卡成PPT,除此之外它在许多的地方有广泛的应用。
有错欢迎纠正,期待你的关注!!!!
内核指南下载地址:链接:https://pan.baidu.com/s/1mKR8Kth0wCU2CJsTISUWhA 提取码:io74