位带简介
位操作就是可以单独的对一个比特位读和写,这个在 51 单片机中非常常见。51 单片机中通过关键字 sbit 来实现位定义,STM32 没有这样的关键字,而是通过访问位带别名区来实现。
在 STM32 中,有两个地方实现了位带,一个是 SRAM 区的最低 1MB 空间,另一个是外设区最低 1MB 空间。这两个 1MB 的空间除了可以像正常的 RAM 一样操作外,他们还有自己的位带别名区,位带别名区把这 1MB 的空间的每一个位膨胀成一个 32 位的字,当访问位带别名区的这些字时,就可以达到访问位带区某个比特位的目的。
外设位带区
外 设 外 带 区 的 地 址 为:0X40000000~0X40100000, 大 小 为1MB, 这1MB的 大 小 在103系 列 大/中/小 容 量 型 号 的 单 片 机 中 包 含 了 片 上 外 设 的 全 部 寄 存 器, 这 些 寄 存 器的 地 址 为:0X40000000~0X40029FFF。 外 设 位 带 区 经 过 膨 胀 后 的 位 带 别 名 区 地 址 为:0X42000000~0X43FFFFFF,这个地址仍然在 CM3 片上外设的地址空间中。在 103 系列大/中小容量型号的单片机里面,0X40030000~0X4FFFFFFF 属于保留地址,膨胀后的 32MB 位带别名区刚好就落到这个地址范围内,不会跟片上外设的其他寄存器地址重合。
STM32 的全部寄存器都可以通过访问位带别名区的方式来达到访问原始寄存器比特位的效果,这比 51 单片机强大很多。因为 51 单片机里面并不是所有的寄存器都是可以比特位操作,有些寄存器还是得字节操作,比如 SBUF。
虽然说全部寄存器都可以实现比特操作,但我们在实际项目中并不会这么做,甚至不会这么做。有时候为了特定的项目需要,比如需要频繁的操作很多 IO 口,这个时候我们可以考虑把 IO 相关的寄存器实现比特操作。
SRAM 位带区
SRAM 的位带区的地址为:0X2000 0000~X2010 0000,大小为 1MB,经过膨胀后的位带别名区地址为:0X2200 0000~0X23FF FFFF,大小为 32MB。操作 SRAM 的比特位这个用得很少。
位带区和位带别名区地址转换
位带区的一个比特位经过膨胀之后,虽然变大到 4 个字节,但是还是 LSB 才有效。有人会问这不是浪费空间吗,要知道 STM32 的系统总线是 32 位的,按照 4 个字节访问的时候是最快的,所以膨胀成 4 个字节来访问是最高效的。
我们可以通过指针的形式访问位带别名区地址从而达到操作位带区比特位的效果。那这两个地址直接如何转换,我们简单介绍一下。
外设位带别名区地址
对于片上外设位带区的某个比特,记它所在字节的地址为 A, 位序号为 n(0<=n<=31)(n 的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr = 0x42000000+ (A-0x40000000)*8*4 +n*4
0X42000000 是外设位带别名区的起始地址,0x40000000 是外设位带区的起始地址, (A-0x40000000)表示该比特前面有多少个字节,一个字节有 8 位,所以 *8,一个位膨胀后是 4 个字节,所以 *4,n 表示该比特在 A 地址的序号,因为一个位经过膨胀后是四个字节,所以也 *4。
SRAM 位带别名区地址
对于 SRAM 位带区的某个比特,记它所在字节的地址为 A, 位序号为 n(0<=n<=31)(n 的范围根据具体寄存器能控制的位决定),则该比特在别名区的地址为:
AliasAddr = 0x22000000+ (A-0x20000000)*8*4 +n*4
公式原理则与上面一样。
统一公式
为了方便操作,我们可以把这两个公式合并成一个公式,把“位带地址 + 位序号”转换成别名区地址统一成一个宏。
// 把“位带地址 + 位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
addr & 0xF0000000 是为了区别 SRAM 还是外设,实际效果就是取出 4 或者 2,如果是外设,则取出的是 4,+0X02000000 之后就等于 0X42000000,0X42000000 是外设别名区的起始地址。如果是 SRAM,则取出的是 2,+0X02000000 之后就等于 0X22000000,0X22000000 是 SRAM 别名区的起始地址。
addr & 0x00FFFFFF 屏蔽了高三位,相当于减去 0X20000000 或者 0X40000000,但是为什么是屏蔽高三位?因为外设的最高地址是:0X20100000,跟起始地址 0X20000000 相减的时候,总是低5 位才有效,所以干脆就把高三位屏蔽掉来达到减去起始地址的效果,具体屏蔽掉多少位跟最高地址有关。SRAM 同理分析即可。«5 相当于 84,«2 相当于 *4,这两个我们在上面分析过。最后我们就可以通过指针的形式操作这些位带别名区地址,最终实现位带区的比特位操作。
// 把一个地址转换成一个指针
#define MEM_ADDR(addr) *((volatile unsigned long*)(addr))
// 把位带别名区地址转换成指针
#define BIT_ADDR(addr, bitnum) MEM_ADDR(BITBAND(addr, bitnum))
GPIO 位带操作
外设的位带区,覆盖了全部的片上外设的寄存器,我们可以通过宏为每个寄存器的位都定义一个位带别名地址,从而实现位操作。但这个在实际项目中不是很现实,也很少人会这么做,我们在这里仅仅演示下 GPIO 中 ODR 和 IDR 这两个寄存器的位操作。
从手册中我们可以知道 ODR 和 IDR 这两个寄存器对应 GPIO 基址的偏移是 12 和 8,我们先实现这两个寄存器的地址映射,其中 GPIOx_BASE 在库函数里面有定义。
别忘了的事
添加bsp_led_bitbang.c文件和bsp_led_bitbang.h文件
别忘了添加文件的时候,在魔术棒的地方加路径。(加上bsp_led_bitbang.h存放的路径)
GPIO 寄存器映射(bsp_led_bitbang.h中)
// 这里只定义了 GPIO ODR和IDR这两个寄存器的位带别名区地址,其他寄存器的没有定义
//SRAM 位带区: 0X2000 0000~0X2010 0000
//SRAM 位带别名区:0X2200 0000~0X23FF FFFF
//外设 位带区: 0X4000 0000~0X4010 0000
//外设 位带别名区:0X4200 0000~0X43FF FFFF
// 把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
/*
*addr & 0xF0000000,取地址的高4位,看看是2还是4,用于区分SRAM和外设地址,
*如果是2,+0x02000000则=0X2200 0000,即是SRAM,如果是4,+0x02000000则=0X4200 0000,即是外设
*
*addr & 0x000FFFFFF,屏蔽掉高两位,相当于-0X2000 0000或者-0X4000 0000,结果表示偏移位带区多少个字节
*<<5 等于*8*4,因为位带区一个地址表示一个字节,一个字节有8个bit,一个bit可以膨胀成一个字,即4个字节
*<<2 等于*4,因为一个位可以膨胀成一个字,即4个字节
*
*分解成两条公式应该就是这样:
*SRAM位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*外设位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*/
// 把一个地址转换成一个指针
#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+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
现在我们就可以用位操作的方法来控制 GPIO 的输入和输出了,其中宏参数 n 表示具体是哪一个IO 口,n(0,1,2⋯15)。这里面包含了端口 A~G ,并不是每个单片机型号都有这么多端口,使用这部分代码时,要查看你的单片机型号,如果是 64pin 的则最多只能使用到 C 端口。
GPIO 位操作(bsp_led_bitbang.h中)
// 单独操作 GPIO的某一个IO口,n(0,1,2...15),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) //输入
就是分别使用别名操作每个I/O口的ODR寄存器和IDR寄存器实现输入和输出。
GPIO测试(bsp_led_bitbang.c中)
有关 LED GPIO 初始化和软件延时等函数我们直接用(以前写过的bsp_led.c和bsp_key.c文件里面的函数),修改的是控制 GPIO 输出的部分改成了位操作。该实验我们让 按键控制IO 口输出高低电平来控制LED 的亮灭,负逻辑点亮。具体使用哪一个 IO 和点亮方式由硬件平台决定。
void Led_Bitbang_GPIO_Test(void)
{
//LED灯初始化
LED_GPIO_Config();
//按键初始化
Key_GPIO_Config();
//红灯亮
PBout(5) = 0;
/* 轮询按键状态,若按键按下则反转 LED(还没学到DMA或者中断呢) */
while(1){
if(Key_Scan_Bitbang('A',0) == KEY_ON){
/*LED1 开启 */
PBout(5) = 0;
}
if(Key_Scan_Bitbang('C',13) == KEY_ON){
/*LED1 关闭 */
PBout(5) = 1;
}
}
}
代码
bsp_led_bitbang.c
/**
* *****************************************************************************
* @file bsp_led_bitbang.c
* @brief 使用位带操作点亮或者熄灭LED灯
* @author (六千里)
* @date 2024-03-09
* @copyright 无
* *****************************************************************************
*/
#include "bsp_led_bitbang.h"
/**
* @brief检测是否有按键按下
* @paramGPIOX: 具体的端口, GPIOX 可以是(A...G)
* @paramGPIO_PIN: 具体的端口位, 可以是 x(x 可以是 0...15)
* @retval 按键的状态
* @arg KEY_ON: 按键按下
* @arg KEY_OFF: 按键没按下
**/
static uint8_t Key_Scan_Bitbang(uint8_t GPIOX,uint8_t GPIO_Pin)
{
switch (GPIOX) {
case 'A': {
/* 检测是否有按键按下 */
if((PAin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PAin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'B':{
/* 检测是否有按键按下 */
if((PBin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PBin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'C':{
/* 检测是否有按键按下 */
if((PCin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PCin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'D':{
/* 检测是否有按键按下 */
if((PDin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PDin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'E':{
/* 检测是否有按键按下 */
if((PEin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PEin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'F':{
/* 检测是否有按键按下 */
if((PFin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PFin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
case 'G':{
/* 检测是否有按键按下 */
if((PGin(GPIO_Pin)) == KEY_ON){
//因为有硬件消抖,故不写软件消抖
/* 等待按键释放 */
while(PGin(GPIO_Pin) == KEY_ON);
return KEY_ON;
}else{
return KEY_OFF;
}
}
default: return KEY_OFF;
}
}
void Led_Bitbang_GPIO_Test(void)
{
//LED灯初始化
LED_GPIO_Config();
//按键初始化
Key_GPIO_Config();
//红灯亮
PBout(5) = 0;
/* 轮询按键状态,若按键按下则反转 LED(还没学到DMA或者中断呢) */
while(1){
if(Key_Scan_Bitbang('A',0) == KEY_ON){
/*LED1 开启 */
PBout(5) = 0;
}
if(Key_Scan_Bitbang('C',13) == KEY_ON){
/*LED1 关闭 */
PBout(5) = 1;
}
}
}
bsp_led_bitbang.h
#ifndef __BSP_LED_BITBANG_H
#define __BSP_LED_BITBANG_H
/**
* *****************************************************************************
* 包含的头文件
* *****************************************************************************
*/
#include "bsp_led.h"
#include "bsp_key.h"
/**
* *****************************************************************************
* 宏定义
* *****************************************************************************
*/
// 这里只定义了 GPIO ODR和IDR这两个寄存器的位带别名区地址,其他寄存器的没有定义
//SRAM 位带区: 0X2000 0000~0X2010 0000
//SRAM 位带别名区:0X2200 0000~0X23FF FFFF
//外设 位带区: 0X4000 0000~0X4010 0000
//外设 位带别名区:0X4200 0000~0X43FF FFFF
// 把“位带地址+位序号”转换成别名地址的宏
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x02000000+((addr & 0x00FFFFFF)<<5)+(bitnum<<2))
/*
*addr & 0xF0000000,取地址的高4位,看看是2还是4,用于区分SRAM和外设地址,
*如果是2,+0x02000000则=0X2200 0000,即是SRAM,如果是4,+0x02000000则=0X4200 0000,即是外设
*
*addr & 0x000FFFFFF,屏蔽掉高两位,相当于-0X2000 0000或者-0X4000 0000,结果表示偏移位带区多少个字节
*<<5 等于*8*4,因为位带区一个地址表示一个字节,一个字节有8个bit,一个bit可以膨胀成一个字,即4个字节
*<<2 等于*4,因为一个位可以膨胀成一个字,即4个字节
*
*分解成两条公式应该就是这样:
*SRAM位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*外设位带别名地址
*AliasAddr= 0x22000000+((A-0x20000000)*8+n)*4 =0x22000000+ (A-0x20000000)*8*4 +n*4
*/
// 把一个地址转换成一个指针
#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+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
// 单独操作 GPIO的某一个IO口,n(0,1,2...15),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) //输入
/**
* *****************************************************************************
* .c文件中包含的函数
* *****************************************************************************
*/
static uint8_t Key_Scan_Bitbang(uint8_t GPIOX,uint8_t GPIO_Pin);
void Led_Bitbang_GPIO_Test(void);
#endif /*__BSP_LED_BITBANG_H*/
stm32f10x_conf.h
/**
******************************************************************************
* @file Project/STM32F10x_StdPeriph_Template/stm32f10x_conf.h
* @author MCD Application Team
* @version V3.5.0
* @date 08-April-2011
* @brief Library configuration file.
******************************************************************************
* @attention
*
* THE PRESENT FIRMWARE WHICH IS FOR GUIDANCE ONLY AIMS AT PROVIDING CUSTOMERS
* WITH CODING INFORMATION REGARDING THEIR PRODUCTS IN ORDER FOR THEM TO SAVE
* TIME. AS A RESULT, STMICROELECTRONICS SHALL NOT BE HELD LIABLE FOR ANY
* DIRECT, INDIRECT OR CONSEQUENTIAL DAMAGES WITH RESPECT TO ANY CLAIMS ARISING
* FROM THE CONTENT OF SUCH FIRMWARE AND/OR THE USE MADE BY CUSTOMERS OF THE
* CODING INFORMATION CONTAINED HEREIN IN CONNECTION WITH THEIR PRODUCTS.
*
* <h2><center>© COPYRIGHT 2011 STMicroelectronics</center></h2>
******************************************************************************
*/
/* Define to prevent recursive inclusion -------------------------------------*/
#ifndef __STM32F10x_CONF_H
#define __STM32F10x_CONF_H
/* Includes ------------------------------------------------------------------*/
/* Uncomment/Comment the line below to enable/disable peripheral header file inclusion */
#include "stm32f10x_adc.h"
#include "stm32f10x_bkp.h"
#include "stm32f10x_can.h"
#include "stm32f10x_cec.h"
#include "stm32f10x_crc.h"
#include "stm32f10x_dac.h"
#include "stm32f10x_dbgmcu.h"
#include "stm32f10x_dma.h"
#include "stm32f10x_exti.h"
#include "stm32f10x_flash.h"
#include "stm32f10x_fsmc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_i2c.h"
#include "stm32f10x_iwdg.h"
#include "stm32f10x_pwr.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_rtc.h"
#include "stm32f10x_sdio.h"
#include "stm32f10x_spi.h"
#include "stm32f10x_tim.h"
#include "stm32f10x_usart.h"
#include "stm32f10x_wwdg.h"
#include "misc.h" /* High level functions for NVIC and SysTick (add-on to CMSIS functions) */
/**
*自己书写文件的头文件
**/
#include "bsp_led.h"
#include "bsp_key.h"
#include "bsp_led_bitbang.h"
/* Exported types ------------------------------------------------------------*/
/* Exported constants --------------------------------------------------------*/
/* Uncomment the line below to expanse the "assert_param" macro in the
Standard Peripheral Library drivers code */
/* #define USE_FULL_ASSERT 1 */
/* Exported macro ------------------------------------------------------------*/
#ifdef USE_FULL_ASSERT
/**
* @brief The assert_param macro is used for function's parameters check.
* @param expr: If expr is false, it calls assert_failed function which reports
* the name of the source file and the source line number of the call
* that failed. If expr is true, it returns no value.
* @retval None
*/
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))
/* Exported functions ------------------------------------------------------- */
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert_param(expr) ((void)0)
#endif /* USE_FULL_ASSERT */
#endif /* __STM32F10x_CONF_H */
/******************* (C) COPYRIGHT 2011 STMicroelectronics *****END OF FILE****/
main.c
/**
* *****************************************************************************
* @file main.c
* @brief 主函数
* @author (六千里)
* @date 2024-03-09
* @copyright 无
* *****************************************************************************
*/
#include "stm32f10x.h"
int main(void)
{
// 来到这里的时候,系统的时钟已经被配置成72M。
Led_Bitbang_GPIO_Test();
}
参考https://doc.embedfire.com/products/link/zh/latest/index.html