八、位带操作
1. 位带操作介绍
为了减少“读-改-写”操作的次数,Cortex-M4处理器提供了一个可以执行单原子比特操作的位带功能。存储器映射包含了两个支持位带操作的区域。其中一个是SRAM区的最低1MB范围,第二个是片内外设区的最低1MB范围。这两个区域中的地址除了普通应用外,还有自己的“位带别名区”。位带别名区把每个比特扩展成一个32位的字。当用户访问位带别名区时,就可以达到访问原始比特的目的。总结就是CPU不能直接对位带区中的单个数据位位寻址,只能通过对位带别名区的访问(或读/写)实现对位带区单个数据位的访问(或读/写),这种操作被称为位带操作。使用位带操作的目的是能够像51单片机那样直接给IO口拉高拉低,例如PBOUT(2) = 1这种操作。
2. 位带操作内存地址
前面我们说了存储器映射支持两个位带操作的区域。关于支持位带操作的两个内存区的范围是: 0x2000_0000‐0x200F_FFFF(SRAM 区的最低1MB) 0x4000_0000‐0x400F_FFFF(片上外设区的最低 1MB)
对应的位段别名区的起始地址分别为0x2200 0000和0x4200 0000,这个在编程的时候需要使用。
3. 位带操作优势
- 更高效
- 读取更简单
- 访问速度快
- 相对安全。在带有操作系统的开发中,多任务并发运行的时候就有可能在任务切换的过程中发生不可预料的问题,而位带操作由于是属于硬件完成的不可被异常打断的操作(原子操作),相对于读-写-改的操作模式会更安全。
- 提高运行效率和节省代码空间。简单的程序直接使用库函数或者寄存器操作,对于比较复杂的程序建议尽量使用位带操作来实现。
4. 配置位带操作
通过 bit_word_addr = bit_band_base +(byte_offset×32)+(bit_number×4) 这个公式可以计算出对应的位带别名区的地址。
bit_band_base
是位带区域的基地址。对于外设位带操作,这个地址是 0x42000000。byte_offset
是目标寄存器的地址减去外设基地址(0x40000000)的结果。bit_number
是在寄存器中要操作的位的位号(从0开始计数)。
这里给大家介绍一下这个公式如何去使用,以PB2为例。
先分析一下这个公式,要计算位带操作别名区地址,首先要知道位带别名区的起始地址、位带区目标比特所在的字节的字节地址偏移量和目标比特在对应字节中的位置。位带别名区的地址在1.2节可以了解到是0x42000000。我们需要配置PB2引脚的输出功能,那么对应的寄存器为端口输出控制寄存器(GPIOx_OCTL),我们要操作的是这一个字节地址的第2位,那对应的字节地址偏移量就是GPIOx_OCTL的偏移量0x14 + GPIOB 的地址然后还要减去地址0x40000000(片上外设的起始地址为0x4000_0000),对应目标比特在对应字节中的位置就是7。如果是配置为输入功能的话,要操作的寄存器为端口输入状态寄存器(GPIOx_ISTAT),对应的偏移量为0x10 + GPIOB 的地址然后还要起始地址0x40000000,对应目标比特在对应字节中的位置也是2。
bit_word_addr =bit_band_base +(byte_offset×32)+(bit_number×4)转化为代码就是:
// 位带操作的宏定义,直接使用您提供的BIT_ADDR宏
#define BIT_ADDR(byte_offset,bitnum) (volatile unsigned long*)(0x42000000 + (byte_offset * 32) + (bitnum * 4))
// 计算GPIOB寄存器的位带别名区地址
#define GPIOB_OCTL_OFFSET ((CM_GPIO_BASE + 0x14) - 0x40000000)
#define GPIOB_ISTAT_OFFSET ((CM_GPIO_BASE + 0x10) - 0x40000000)
// 定义输出和输入宏
#define PBout(n) *(BIT_ADDR(GPIOB_OCTL_OFFSET,n)) // PB输出
#define PBin(n) *(BIT_ADDR(GPIOB_ISTAT_OFFSET,n)) // PB输入
2
3
4
5
6
7
8
9
10
11
宏定义解释
BIT_ADDR(byte_offset, bitnum)
宏计算特定位的位带别名地址。这里,0x42000000
是位带区域的基地址,byte_offset
是目标寄存器相对于外设基地址0x40000000
的偏移量,bitnum
是寄存器中的位号(0-31)。这个宏返回一个指向该位带别名地址的指针。GPIOB_OCTL_OFFSET
定义了GPIOB输出数据寄存器(ODR,偏移量为0x14)相对于外设基地址的字节偏移量。GPIOB_ISTAT_OFFSET
定义了GPIOB输入数据状态寄存器(IDR,偏移量为0x10)相对于外设基地址的字节偏移量。PBout(n)
和PBin(n)
宏分别用于访问GPIOB的第n位的输出和输入状态。这些宏通过BIT_ADDR
宏计算出的地址来操作特定的位,n是位号(0-15)。
需要注意的是当你使用位带功能时,要访问的变量必须用volatile来定义。因为c编译器并不知道同一个比特可以有两个地址。通过volatile使得编译器每次都如实地把新数值写入存储器,避免语句被优化。
5. 位带操作文件
完整的GPIO位带操作文件代码:
/*
* 立创开发板软硬件资料与相关扩展板软硬件资料官网全部开源
* 开发板官网:www.lckfb.com
* 技术支持常驻论坛,任何技术问题欢迎随时交流学习
* 立创论坛:club.szlcsc.com
* 关注bilibili账号:【立创开发板】,掌握我们的最新动态!
* 不靠卖板赚钱,以培养中国工程师为己任
* Change Logs:
* Date Author Notes
* 2024-04-09 LCKFB-LP first version
*/
#ifndef __SYS_H__
#define __SYS_H__
#include "hc32_ll.h"
// 位带操作的宏定义,直接使用您提供的BIT_ADDR宏
#define BIT_ADDR(byte_offset,bitnum) (volatile unsigned long*)(0x42000000 + (byte_offset * 32) + (bitnum * 4))
// 计算GPIOB寄存器的位带别名区地址
#define GPIOA_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 0)) - 0x40000000)
#define GPIOB_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 1)) - 0x40000000)
#define GPIOC_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 2)) - 0x40000000)
#define GPIOD_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 3)) - 0x40000000)
#define GPIOE_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 4)) - 0x40000000)
#define GPIOF_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 5)) - 0x40000000)
#define GPIOG_OCTL_OFFSET ((CM_GPIO_BASE + 0x4 + (0x10 * 6)) - 0x40000000)
#define GPIOA_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 0)) - 0x40000000)
#define GPIOB_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 1)) - 0x40000000)
#define GPIOC_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 2)) - 0x40000000)
#define GPIOD_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 3)) - 0x40000000)
#define GPIOE_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 4)) - 0x40000000)
#define GPIOF_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 5)) - 0x40000000)
#define GPIOG_ISTAT_OFFSET ((CM_GPIO_BASE + (0x10 * 6)) - 0x40000000)
// 定义输出和输入宏
#define PAin(n) *(BIT_ADDR(GPIOA_ISTAT_OFFSET,n)) // PA输入
#define PBin(n) *(BIT_ADDR(GPIOB_ISTAT_OFFSET,n)) // PB输入
#define PCin(n) *(BIT_ADDR(GPIOC_ISTAT_OFFSET,n)) // PC输入
#define PDin(n) *(BIT_ADDR(GPIOD_ISTAT_OFFSET,n)) // PD输入
#define PEin(n) *(BIT_ADDR(GPIOE_ISTAT_OFFSET,n)) // PE输入
#define PFin(n) *(BIT_ADDR(GPIOF_ISTAT_OFFSET,n)) // PF输入
#define PGin(n) *(BIT_ADDR(GPIOG_ISTAT_OFFSET,n)) // PG输入
#define PAout(n) *(BIT_ADDR(GPIOA_OCTL_OFFSET,n)) // PA输出
#define PBout(n) *(BIT_ADDR(GPIOB_OCTL_OFFSET,n)) // PB输出
#define PCout(n) *(BIT_ADDR(GPIOC_OCTL_OFFSET,n)) // PC输出
#define PDout(n) *(BIT_ADDR(GPIOD_OCTL_OFFSET,n)) // PD输出
#define PEout(n) *(BIT_ADDR(GPIOE_OCTL_OFFSET,n)) // PE输出
#define PFout(n) *(BIT_ADDR(GPIOF_OCTL_OFFSET,n)) // PF输出
#define PGout(n) *(BIT_ADDR(GPIOG_OCTL_OFFSET,n)) // PG输出
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
6. LED灯闪烁实验
前面章节我们学习了如何去使用库函数和滴答定时器实现LED灯闪烁的效果。这一章就在原来的基础上把用库函数输出高低电平的函数替换为我们的位带操作输出高低电平。只需要
PBout(2) = 1; // 输出高电平
PBout(2) = 0; // 输出低电平
2
然后编译运行即可。
烧写我们的代码之后,可以看到开发板的LED2将会0.5s亮0.5s灭。