【普中STM32精灵开发攻略】--第 10 章 STM32位带操作

(1)实验平台:

普中STM32精灵开发板https://item.taobao.com/item.htm?id=739076227953(2)资料下载:普中科技-各型号产品资料下载链接


本章将向大家介绍 STM32F1 的位带操作,让 STM32 的位操作和51 单片机的位操作一样简单。本章最后通过一个简单的 LED 闪烁程序来讲述如何对STM32F1进行位操作。学习本章可以参考《STM32F10x 中文参考手册》存储器和总线构架章节、GPIO 章节,《Cortex M3 权威指南(中文)》chpt05 章的位带操作。本章分为如下几部分内容:

10.1 位带操作介绍

10.1.1 位带操作

在学习 51 单片机的时候就使用过位操作,通过关键字sbit 对单片机IO口进行位定义。但是 STM32 没有这样的关键字,而是通过访问位带别名区来实现。即将每个比特位膨胀成一个 32 位字,当访问这些字的时候就达到了访问比特的目的。比方说 BSRR 寄存器有 32 个位,那么可以映射到32 个地址上,当我们去访问这 32 个地址就达到访问 32 个比特的目的。

STM32F1 中有两个区域支持位带操作,一个是 SRAM 区的最低1MB 范围,一个是片内外设区的最低 1MB 范围(APB1、APB2、AHB 外设)。如下图所示:

从图中可知,SRAM 的最低 1MB 区域,地址范围是 0X2000 0000-0X200F FFFF。片内外设最低 1MB 区域,地址范围是 0X4000 0000-0X400F FFFF,在这个地址范围内包括了 APB1、APB2、AHB 总线上所有的外设寄存器。

在 SRAM 区中还有 32MB 空间,其地址范围是 0X2200 0000-0X23FF FFFF,它是 SRAM 的 1MB 位带区膨胀后的位带别名区,前面已经说过位带操作,要实现位操作即将每一位膨胀成一个 32 位的字,因此 SRAM 的1MB 位带区就膨胀为32MB的位带别名区,通过访问位带别名区就可以实现访问位带中每一位的目的。

片内外设区的 32MB 的空间也是一样的原理。片内外设区的32MB 地址范围是0X4200 0000-0X43FF FFFF。

通常我们使用位带操作都是在外设区,在外设区中应用比较多的也就是GPIO 外设,SRAM 区内很少使用位操作。

10.1.2 位带区与位带别名区地址转换

前面已经说过,位带操作就是将位带区中的每一位膨胀成位带别名区中的一个 32 位的字,通过访问位带别名区中的字就实现了访问位带区中位的目的。因此我们就可以使用指针来访问位带别名区的地址,从而实现访问位带区内位的目的。那么位带别名区与位带区地址是如何转换的,我们下面就来介绍下:

(1)外设位带别名区地址

对于片上外设位带区的某个比特,记它所在字节的地址为A,位序号为n,n值的范围是 0-7,则该比特在别名区的地址为:

AliasAddr=0x42000000+ (A-0x40000000)*8*4 +n*4

0x42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址,(A-0x40000000)表示该比特前面有多少个字节,一个字节有8 位,所以*8,一个位膨胀后是 4 个字节,所以*4,n 表示该比特在A 地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。

(2)SRAM 位带别名区地址

对于 SRAM 位带区的某个比特,记它所在字节的地址为A,位序号为n,n值的范围是 0-7,则该比特在别名区的地址为:

AliasAddr= =0x22000000+ (A-0x20000000)*8*4 +n*4

0x22000000 是 SRAM 位带别名区的起始地址,0x20000000 是SRAM 位带区的起始地址,(A-0x20000000)表示该比特前面有多少个字节,一个字节有8位,所以*8,一个位膨胀后是 4 个字节,所以*4,n 表示该比特在A 地址的序号,因为一个位经过膨胀后是四个字节,所以也*4。

上面我们已经把外设位带别名区地址和 SRAM 位带别名区地址使用公式表示出来,为了操作方便,我们将这两个公式进行合并,通过一个宏来定义,并把位带地址和位序号作为这个宏定义的参数。公式如下:

cpp 复制代码
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr&0xFFFFF)<<5)+(bitnum<<2))

addr & 0xF0000000 是为了区分我们操作的是 SRAM 还是外设,实际上就是获取最高位的值是 4 还是 2。如果操作的是外设,那么addr & 0xF0000000 结果就是 0x40000000,后面+0x2000000 就等于 0X42000000,0X42000000 是外设别名区的起始地址。如果操作的是 SRAM,那么 addr & 0xF0000000 结果就是0x20000000,后面+0x2000000 就等于 0X22000000,0X22000000 是SRAM 别名区的起始地址。

addr & 0x000FFFFF 屏蔽了高三位,相当于减去 0X20000000 或者0X40000000,屏蔽高三位是因为 SRAM 和外设的位带区最高地址是0X200F FFFF和 0X400F FFFF,SRAM 或者外设位带区上任意地址减去其对应的起始地址,总是低 5 位有效,所以这里屏蔽高 3 位就相当于减去了 0X20000000 或者0X40000000。<<5 相当于*8*4, <<2 相当于*4,其作用在前面已经分析过。

最后就可以通过指针形式来操作这些位带别名区地址,实现位带区对应位的操作。代码如下:

cpp 复制代码
//把 addr 地址强制转换为 unsigned long 类型的指针
#define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
//把位带别名区内地址转换为指针 ,获取地址内的数据
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))

这里说明下 volatile 关键字,volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有 volatile 关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。更详细的内容大家可以百度查找。

10.1.3 位带操作的优点

在 STM32 应用程序开发中虽然可以使用库函数操作外设,但如果加上位操作就如虎添翼。想想 51 单片机内位操作的方便,就可以理解为什么要对STM32使用位操作。STM32 位操作优点非常多,我们这里就列举几个突出的:

(1)对于控制 GPIO 的输入和输出非常简单。

(2)操作串行接口芯片非常方便(DS1302、74HC595 等),如果采用库函数的话,那么这个时序编写就非常不方便。

(3)代码简洁,阅读方便

10.2 GPIO 位带操作

我们已经知道 STM32F1 支持的位带操作区有两个,其中应用最多的还是外设位带区,在外设位带区中包含了 APB1、APB2 还有 AHB 总线上的所有外设寄存器,使用位带操作应用最多的外设还属 GPIO,通过位带操作控制STM32 引脚输入与输出,因此我们就以 GPIO 中 IDR 和 ODR 这两个寄存器的位操作进行讲解。

根据《STM32F10x 中文参考手册》对应的 GPIO 寄存器章节中可以知道,IDR和 ODR 寄存器相对于 GPIO 基地址的偏移量是 8 和 12。所以可以通过宏定义实现这两个寄存器的地址映射,具体代码如下:

cpp 复制代码
//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
#define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
#define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
#define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
#define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
#define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
#define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    

#define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
#define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
#define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
#define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
#define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
#define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
#define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 

从上述代码中可以看到有 GPIOx_BASE,这个也是一个宏,里面封装的是相应 GPIO 端口的基地址,在库函数中有定义。

获取寄存器的地址以后,就可以采用位操作的方法来操作GPIO 的输入和输出,代码如下:

cpp 复制代码
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#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)  //输入

上述代码中我们已经将 STM32F1 芯片的所有端口都进行了位定义封装,假如要使用 PB5 管脚进行输出,那么就可以调用 PBout(n)宏,n 值即为5。假如使用的是 PB5 管脚作为输入,那么就可以调用 PBin(n)宏,n 值即为5。其他端口调用方法类似。

10.3 软件设计

前面我们已经将 GPIO 的 ODR 和 IDR 寄存器进行了位操作,下面我们把它应用到我们程序中去,通过位操作实现 LED 指示灯闪烁,LED 指示灯的硬件电路前面已经介绍过,这里就不重复。

我们复制上一章的工程文件夹,重新命名为" LED 闪烁(使用位带操作)",在其目录下新建一个 Public 文件夹,用于存放 STM32F1 的公共应用程序文件,比如本章所要实现的位操作功能,后面等到我们讲解 Systick 定时器和串口的时候也会把它们的驱动程序文件放入在此文件夹内,所以如果要对STM32F1 系列芯片进行程序开发,可以复制我们这个文件夹到你的工程中,实现位操作、Systick精确延时和串口功能。

打开工程程序,新建 system.c 和 system.h 文件,将其存放在Public 文件夹内,并在 KEIL5 内添加其头文件路径(具体操作可参考视频教程)。这里我们只介绍部分代码,其他的代码可以打开"\4--实验程序\1--基础实验\4-LED闪烁实验(使用位带操作)"工程程序查看。

system.c 内未写任何代码,只是将其头文件调用进来,方便工程中其他源文件调用,system.h 内把 GPIO 的 IDR 和 ODR 寄存器位操作进行了封装,具体代码如下:

cpp 复制代码
#ifndef _system_H
#define _system_H


#include "stm32f10x.h"


//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
//IO口操作宏定义
#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)) 
//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
#define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
#define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
#define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
#define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
#define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
#define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    

#define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
#define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
#define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
#define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
#define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
#define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
#define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
 
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#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)  //输入


#endif

从 system.h 内的代码可以看到,内部还调用了 stm32f10x.h 文件,因此在main.c 文件中可以直接调用 system.h,既可包含 stm32f10x.h 内容也能包含位操作功能。

要使用位操作来控制 LED,需要在 led.h 文件中调用system.h,由于我们核心板上有 1 个 LED,我们根据其 IO 管脚进行宏定义,代码如下:

cpp 复制代码
#define LED0 PCout(13) //LED 指示灯连接的是 PC13 管脚

其他的我们就不逐个定义,如果要控制 PC13 管脚输出一个高电平,直接就可以使用 LED0=1 来表示,这就和 51 单片机管脚操作方法一样。

最后看下主函数,主函数实现的功能比较简单,首先对LED 端口时钟及管脚模式配置进行初始化,然后通过位操作不断取反 PC13 管脚电平,实现LED 闪烁效果,里面还用到了一个 delay 延时,这个延时函数只是一个大约的延时,要实现精确延时,在后面章节中我们会讲解到。代码如下:

cpp 复制代码
#include "system.h"
#include "led.h"


/*******************************************************************************
* 函 数 名         : delay
* 函数功能		   : 延时函数,通过while循环占用CPU,达到延时功能
* 输    入         : i
* 输    出         : 无
*******************************************************************************/
void delay(u32 i)
{
	while(i--);
}

/*******************************************************************************
* 函 数 名         : main
* 函数功能		   : 主函数
* 输    入         : 无
* 输    出         : 无
*******************************************************************************/
int main()
{
	LED_Init();
	while(1)
	{
		LED0=!LED0;
		delay(6000000);
	}
}

10.3 实验现象

将"\4--实验程序\1--基础实验\4-LED 闪烁实验(使用位带操作)"中的程序下载到开发板内,可以看到核心板中的 D2 指示灯闪烁。

相关推荐
猫猫的小茶馆4 小时前
【STM32】HAL库中的实现(二):串口(USART)/看门狗(IWDG/WWDG)/定时器(TIM)
arm开发·驱动开发·stm32·单片机·嵌入式硬件·mcu·智能硬件
zhmc4 小时前
MCU程序的编译与链接及格式转换
arm开发·单片机·嵌入式硬件
HW-BASE5 小时前
C语言的结构体与联合体
c语言·单片机·嵌入式·编程·c
谢工碎碎念6 小时前
PCB工艺-四层板制作流程(简单了解下)
嵌入式硬件·物联网·硬件工程·iot·pcb工艺
FightingFreedom6 小时前
GPIO交换矩阵和IO_MUX
单片机·嵌入式硬件·esp32·arduino·esp32s3
TDengine (老段)6 小时前
TDengine IDMP 背后的技术三问:目录、标准与情景
大数据·数据库·物联网·算法·时序数据库·iot·tdengine
文火冰糖的硅基工坊7 小时前
[硬件电路-150]:数字电路 - 数字电路与模拟电路的异同
嵌入式硬件·fpga开发·电路·运放·跨学科融合
HW-BASE7 小时前
C语言控制语句练习题1
c语言·开发语言·单片机·算法·嵌入式·c
echo_pen8 小时前
蓝桥杯----锁存器、LED、蜂鸣器、继电器、Motor
单片机·蓝桥杯