1.sys.c(這個主要是定義位帶區地址的宏定義,因此主要的代碼都在sys.h中)
其實,本質上sys就是實現位帶區映射(每1Bit)到位帶別名區(每32位,即1字),從而我們可以操作位帶別名區里的“別名”,進而操控位帶區對應的位置。
支持了位帶操作后,可以使用普通的加載/存儲指令來對單一的比特進行讀寫。在CM3
中,有兩個區中實現了位帶。其中一個是SRAM 區的最低1MB 范圍,第二個則是片內外設
區的最低1MB 范圍。
支持位帶操作的兩個內存區的范圍是:
0x2000_0000‐0x200F_FFFF(SRAM 區中的最低1MB)
0x4000_0000‐0x400F_FFFF(片上外設區中的最低1MB)
原理:這兩個區中的地址除了可以像普通的RAM 一樣使用外,它們還都有自己的“位帶別名區”,
位帶別名區把每個比特膨脹成一個32 位的字。當你通過位帶別名區訪問這些字時,就可以達到
訪問原始比特的目的。
我們可以通過下圖來了解地址映射的過程:
接下來把SRAM位帶區和外設位帶區各自映射到的位帶別名區的計算公式列出來:
記位帶區所在字節地址為A,位序號
在別名區的地址為:
內存SRAM公式:SRAMAddr=0x22000000+((A‐0x20000000)*8+n)*4 =0x22000000+ (A‐0x20000000)*32 + n*4
片上外設公式:PERIPHAddr=0x42000000+((A‐0x40000000)*8+n)*4 =0x42000000+ (A‐0x40000000)*32 + n*4
這些可以通過宏定義來實現:
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
(addr & 0xF0000000)是起始地址,+0x2000000就成了位帶別名區的基地址,相當於取位帶區的地址(字節數)和位數,
然后映射到位帶別名區,其中+((addr &0xFFFFF)<<5)就是偏移地址(原始地址跟原始基地址的差距)*32,(bitnum<<2)就是位偏移*4;
//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)) //通過起始位帶地址的地址和位數取位帶別名區的地址的值
通過上邊,我們可以基本掌握位帶區到位帶別名區的轉換過程,可以通過下邊的兩個表練習一下。
通過下圖,我們可知GPIOA_BASE~GPIOG_BASE,即寄存器A~F的基地址
#define PERIPH_BASE ((uint32_t)0x40000000)
#define APB2PERIPH_BASE (PERIPH_BASE + 0x10000)
#define GPIOA_BASE (APB2PERIPH_BASE + 0x0800) //寄存器A的基地址是0x4001 0800
#define GPIOB_BASE (APB2PERIPH_BASE + 0x0C00) //寄存器B的基地址是0x4001 0C00
#define GPIOC_BASE (APB2PERIPH_BASE + 0x1000) //寄存器C的基地址是0x4001 1000
#define GPIOD_BASE (APB2PERIPH_BASE + 0x1400) //寄存器D的基地址是0x4001 1400
#define GPIOE_BASE (APB2PERIPH_BASE + 0x1800) //寄存器E的基地址是0x4001 1800
#define GPIOF_BASE (APB2PERIPH_BASE + 0x1C00) //寄存器F的基地址是0x4001 1C00
#define GPIOG_BASE (APB2PERIPH_BASE + 0x2000) //寄存器G的基地址是0x4001 2000
我們可以通過GPIO_IDR和GPIO_ODR寄存器控制GPIO的IO口輸入輸出
//IO口地址映射(GPIOA~G中寄存器的基地址加上偏移地址8就是對應寄存器的輸出控制地址)
#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
(GPIOA~G中寄存器的基地址加上偏移地址12就是對應寄存器的輸入控制地址)
#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口操作宏定義之后,我們可以直接宏定義某寄存器的具體 某一位,從而進行位帶操作。
//確保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) //輸入
位帶操作有什么優越性?
最容易想到的就是通過GPIO 的管腳來單獨控制每盞LED 的點亮與熄滅。
另一方面,也對操作串行接口器件提供了很大的方便(典型如74HC165,CD4094)。
總之位帶操作對於硬件I/O 密集型的底層程序最有用處了。
如何在其他C文件中使用?
直接在對應的C文件的.h文件中引用"#include "sys.h" ",
然后對相應的位重新宏定義一下,方便進一步對位操作。
舉例:#define LED0 PAout(5) //作用就是點亮PA5口的LED0燈
這時直接給LED0=1,就是BIT_ADDR(GPIOA_ODR_Addr,n) =1,就是BIT_ADDR(GPIOA_BASE+12,n)=1,
就是把寄存器A的輸出配置地址中的n位對應的值置1.(注意n位是0~F,要小於16)