本文基于STM32F407ZGT6,适用于绝大部分M3/M4内核的STM32芯片。
——————————————————————————————
STM32的IO口初始化完毕了,然后我们想进行一个点亮流水灯\打开蜂鸣器这样的操作。
很简单,只要把IO口置高电平或低电平即可,我们可以调用STM32的库函数去实现这样的操作。
可以实现该功能的库函数有五个:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//设置某一个GPIO口为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//设置某一个GPIO口为低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal); //用于配置BSRRH/BSRRL寄存器,设置一个IO口电平
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//用于配置ODR,设置一组IO口电平
void GPIO_ToggleBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);//用于GPIO口输出电平的反转
一般我们使用的是前两个函数GPIO_SetBits和GPIO_ResetBits。
其实使用库函数去设置电平已经很方便了,但是我们想要更方便更直观一些,就像操作51单片机一样直接操作IO口。
那么这就涉及了STM32的位带(原子)操作了。
以下内容摘自《Cortex M3权威指南》
其实M3的内核与M4内核的区别不大,总线结构和储存结构存在差别还有M4引入了浮点运算。
在CM3中,有两个区中实现了位带。其中一个是 SRAM 区的最低 1MB 范围,第二个则是片内外设区的最低 1MB 范围。这两个区中的地址除了可以像普通的 RAM 一样使用外,它们还都有自己的“位带别名区”,位带别名区把每个比特膨胀成一个 32 位的字。当你通过位带别名区访问这些字时,就可以达到访问原始比特的目的。
位带区:
位带别名区:
位带区的每个bite对应位带别名区地址(32bite)
即位带区0X2000 0000地址的第0位(1个bite)对应了位带别名区的地址0X2200 0000—0X2200 0004,一共4个字节(32个bite)。
具体的例子:
-1. 在地址 0x20000000 处写入 0x3355AACC
- 2.读取位带别名区地址 0x22000008。本次读访问将读取到0x20000000,并提取了比特 2,值为 1。
- 3.往位带别名区地址 0x22000008 处写 0。本次操作将被映射成对地址 0x20000000 的“读-改-写”操作(原子的),把比特 2 清 0。
- 4.现在再从位带区地址读取 0x20000000,将返回值 0x3355AAC8(bit[2]已清零)。
位带操作C语言实现方法:
在 C 编译器中并没有直接支持位带操作。比如, C 编译器并不知道同一块内
存能够使用不同的地址来访问,也不知道对位带别名区的访问只对 LSB 有效。欲在 C 中使用
位带操作,最简单的做法就是#define 一个位带别名区的地址。
例如:
下面引用以下原子哥写好的例程:
//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).M4同M3类似,只是寄存器地址变了.
//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+20) //0x40020014
#define GPIOB_ODR_Addr (GPIOB_BASE+20) //0x40020414
#define GPIOC_ODR_Addr (GPIOC_BASE+20) //0x40020814
#define GPIOD_ODR_Addr (GPIOD_BASE+20) //0x40020C14
#define GPIOE_ODR_Addr (GPIOE_BASE+20) //0x40021014
#define GPIOF_ODR_Addr (GPIOF_BASE+20) //0x40021414
#define GPIOG_ODR_Addr (GPIOG_BASE+20) //0x40021814
#define GPIOH_ODR_Addr (GPIOH_BASE+20) //0x40021C14
#define GPIOI_ODR_Addr (GPIOI_BASE+20) //0x40022014
#define GPIOA_IDR_Addr (GPIOA_BASE+16) //0x40020010
#define GPIOB_IDR_Addr (GPIOB_BASE+16) //0x40020410
#define GPIOC_IDR_Addr (GPIOC_BASE+16) //0x40020810
#define GPIOD_IDR_Addr (GPIOD_BASE+16) //0x40020C10
#define GPIOE_IDR_Addr (GPIOE_BASE+16) //0x40021010
#define GPIOF_IDR_Addr (GPIOF_BASE+16) //0x40021410
#define GPIOG_IDR_Addr (GPIOG_BASE+16) //0x40021810
#define GPIOH_IDR_Addr (GPIOH_BASE+16) //0x40021C10
#define GPIOI_IDR_Addr (GPIOI_BASE+16) //0x40022010
//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) //输入
#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) //输入
这样子STM32的GPIO口原子操作就完成了。想要输出高低电平将由
库函数模式:
GPIO_ResetBits(GPIO_Pin_9);//GPIO9设置低
GPIO_SetBits(GPIO_Pin_10);//GPIO10设置高
变成了位带操作:
//LED端口定义
#define LED0 PFout(9) // 宏定义
#define LED1 PFout(10) // 宏定义
LED0=0; //GPIOF9置低,LED0亮
LED1=1; //GPIOF9置高,LED1灭
可以看出用位带操作程序可读性提高了,而且位带操作使程序写起来更加简便。免除了库函数的跳转执行,程序的指令减少了,效率也提高了。