上周实验课照例很水,首先是准备工作没做好,J-Link的驱动没装好,而且由于机房电脑本身的问题好多机子无法正确装驱动,或者在进入keil后会弹出莫名错误、闪退等情况,方老师说得好,当我们浪费时间再做这些事情的时候(浪费时间很大程度上是因为机房电脑造成的),好一点的学校早就在写程序了。这么多时间已经浪费了,还有多少能剩下来看代码进而理解它呢?
从新建一个工程开始学习,再加上上周实验课的位带操作相关内容,有需要的同学可以看看,也希望指正相关错误:)
1.新建工程
在keil中新建一个基于51的工程挺简单,不过新建一个STM32工程要复杂一些,多了一些步骤,需要建立更详细的工程目录,导入一些CMSIS(Cortex Microcontroller Software Interface Standard)文件、标准外设驱动文件、启动文件等等,并要进行一些参数设置。下面这篇博客已经说明得挺好了(在SITP中我也是参考的这篇博客),因此不再赘述。
http://www.cnblogs.com/emouse/archive/2012/03/01/2375146.html
至于相关的文件,可以在网上找到,我也在百度网盘里面传了一个上学期总线课用到的无线模块通信的工程,可以在里面找到所需的文件。
2.位带操作
先摘抄一些书上的内容,再结合代码分析
注:根据我的理解,右边那一大块,对应的地址 从0x0000 0000 到0xFFFF FFFF 是存储器映射地址,通俗一点说就是序号,每一个地址(序号)对应内存中的一个字节的区域
2.1
在Cortex-M3中,有两个区中实现了位带(Bit Band)操作,其一是内部SRAM区最低的1MB范围,其二是片内外设去的最低1MB范围,这两个区中的地址还有自己的位带别名区(Bit Band Alias Region)。位带别名区把每个比特膨胀成一个32位的字,当通过位带别名区访问这些字时,就可以达到访问原始比特的目的。
图2.2 位带区与位带别名区的膨胀映射关系
注1:和图2.1一样,图上显示的是地址,也就是序号,就像是第几号房间,而每个房间里面有8张床 (8个格子) (8bit 可以存东西的空间) 可以放0/1
注2:上半部分位带别名区,从起始地址开始,每4个序号(如0x2200 0000~ 0x2200 0003)对应下半部分位带区的一个序号中的一个位(如0x2000 0000. 0),这样就把位带区的1位扩展成了32位(4个序号 ,每个序号对应内存中的1字节=8位,4×8=32,更具体一点,就是0x2200 0000.0 ~ 0x2200 0003.7的空间),即1字。
/*
在位带区,每个比特都映射到别名地址区的一个字,该字只有最低位有效。当一个别名地址被访问时,会先把该地址变换成位带地址。对于读操作,读取位带地址中的一个字,在把需要的位右移到需要的最低位并把最低位返回。对于写操作,把需要写的位左移至对应的位序号出,然后执行一个原子的“读——改——写”过程。
*/
对于内部SRAM位带区的某个比特,记它所在的字节的地址为Addr,字节中位序号为n(0≤n≤7),则该比特在别名区的地址为:
AliasAddr = 0x2200 0000 + ( ( Addr – 0x2000 0000 ) × 8 + n ) × 4
= 0x2200 0000 + ( Addr - 0x2000 0000 ) × 32 + n × 4 (转换公式)
上式中 × 4 是因为 1字 = 4字节, × 8 表示 1字节 = 8比特。
2.2
2.2.1
下面放上代码再具体说明
1 /* Private define ------------------------------------------------------------*/ 2 #define RAM_BASE 0x20000000 3 #define RAM_BB_BASE 0x22000000 4 5 /* Private macro -------------------------------------------------------------*/ 6 #define Var_ResetBit_BB(VarAddr, BitNumber) \ 7 (*(__IO uint32_t *) (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2)) = 0) 8 9 #define Var_SetBit_BB(VarAddr, BitNumber) \ 10 (*(__IO uint32_t *) (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2)) = 1) 11 12 #define Var_GetBit_BB(VarAddr, BitNumber) \ 13 (*(__IO uint32_t *) (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2))) 14 15 /* Private variables ---------------------------------------------------------*/ 16 17 __IO uint32_t Var, VarAddr = 0, VarBitValue = 0; 18 19 Var = 0x00005AA5; 20 21 VarAddr = (uint32_t)&Var;
首先是定义基址,RAM_BASE是位带区的起始地址,RAM_BB_BASE是位带别名区的起始地址,这也可以从图2.1 看出。
然后是三个宏定义#define,可以看成是三个函数,其中 \ 是续行符,表示下面一行是紧接着当前行的,一般用于将很长的代码语句分几段写,但要注意 \ 后面除了换行回车不能有任何字符,空格也不行。
接下来以Var_ResetBit_BB为例:
#define Var_ResetBit_BB(VarAddr, BitNumber) (*(__IO uint32_t *) (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2)) = 0)
这一行第一眼看去不明觉厉。。需要仔细想想。把 Var_ResetBit_BB 定义成这样一个函数,它的两个参数是(VarAddr, BitNumber)
//用这种形式表示
void Reset(VarAddr, BitNumber){
首先是 (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2)) 假设得到的结果是A
然后是 (__IO uint32_t *) A 这是强制类型转换,即把A 的类型转换成 __IO uint32_t 的指针,假设结果是B,即B = (__IO uint32_t *) A
接下来 * B 即取 B的内容,假设 C = *B
最后是 C = 0
}
接着再来具体分析一下函数里面的过程,也就是转换公式的实现部分 ( RAM_BB_BASE | ( (VarAddr - RAM_BASE) << 5) | ( (BitNumber) << 2) )
VarAddr对应于公式中的 Addr ,BitNumber 对应于 n
(1) 首先,二进制数左移n位,就相当于乘以2的n次方(D),因此
(VarAddr - RAM_BASE) << 5 就相当于 (VarAddr - RAM_BASE) × 32 ,(BitNumber) << 2 相当于 (BitNumber) × 4
对照转换公式可以发现两者很像了,如果 按位或| 和 公式中的 + 在这里能得到相同的结果,那么这个函数就可以用了。下面来看看是不是这样。
(2)
RAM_BB_BASE=0x2200 0000,转换成二进制就是
0010 0010 0000 0000 0000 0000 0000 0000
由于低25位全都是0(加上一个数肯定不会产生进位),因此 与一个数相加 和 与一个数按位或 的结果是一样的(但是 或| 更快),当然这个数不能超过25位。
而位带区地址最多是0x2000 0000开始的1MB=2^20范围,也就是VarAddr-RAM_BASE涉及到的范围是在0x00000 ~ 0xFFFFF,也即
0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 (20位)
左移5位后也就是最多25位,因此符合上面的条件,按位或和加法等效,不存在进位问题,精妙的设计!
每一个序号(地址)对应的内存中有8位,也就是说BitNumber范围是0~7 (0000 ~ 0111),左移2位后是 00000 ~ 11100,不超过VarAddr-RAM_BASE左移5位 后多出来的0,因此也符合条件,按位或和加法等效。
因此( RAM_BB_BASE | ( (VarAddr - RAM_BASE) << 5) | ( (BitNumber) << 2) ) = 转换公式的右边
再回到 (*(__IO uint32_t *) (RAM_BB_BASE | ((VarAddr - RAM_BASE) << 5) | ((BitNumber) << 2)) = 0) ,在刚才假设的那个void Reset(VarAddr, BitNumber) 函数中,A(或者说是B)代表变量在位带别名区的对应地址(序号),C=*B,也就是取内容,即取这个序号的内容(其实是由此开始的4个序号,即 32位 )。
若要Reset,则C = 0,若要Set 则 C = 1(不用 =31,因为该字只有最低位有效)。由此实现了:若要对位带别名区的地址(序号)中的内容进行复位/置位(其实是写一个字进去),就可以改变位带区中对应的地址中的某一位的值(比如),而这个位带别名区的地址只需通过VarAddr = (uint32_t)&Var 得到在位带区中的地址,并把要改动该地址中的哪一位(BitNumber)一起作为参数给 Reset /Set 函数即可 的功能。而GetBit则是到 *B 为止,即return位带别名区地址的内容(32位)。
但是现在有个问题: VarAddr = (uint32_t)&Var 只是从Var的开始地址算起,Offset = VarAddr - BB_BASE已经固定了,无法转到下一个字(n=0~7),这要是Var这个变量过大,那不就够不着对应的位带别名区了。。?
2.2.2
结合代码中调用这几个“函数”的部分来看,会有进一步发现。
//Var = 0x00005AA5;
//VarAddr = (uint32_t)&Var;
(1)
1 /* Modify Var variable bit 0 -----------------------------------------------*/ 2 Var_ResetBit_BB(VarAddr, 0); /* Var = 0x00005AA4 = 0000 0000 0000 0000 0101 1010 1010 0100 */ 3 printf("VAR=0x%x\n",Var); 4 5 Var_SetBit_BB(VarAddr, 0); /* Var = 0x00005AA5 = 0000 0000 0000 0000 0101 1010 1010 0101 */ 6 printf("VAR=0x%x\n",Var);
位带区地址(序号) | …… | 0x2000 0003.(7 ~ 0) | 0x2000 0002.(7 ~ 0) | 0x2000 0001.(7 ~ 0) | 0x2000 0000.(7 ~ 0) |
位带区地址对应的内容 .(7 ~ 0) | …… | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
Reset(VarAddr, 0)之后 | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0100 | |
再Set(VarAddr, 0)之后 | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
BitNumber=0,Reset得到的结果是把第一个地址的.0位(第1位)变成了0,Set后又回到了1。
(2)
1 /* Modify Var variable bit 11 -----------------------------------------------*/ 2 Var_ResetBit_BB(VarAddr, 11); /* Var = 0x000052A5 = 0000 0000 0000 0000 0101 0010 1010 0101*/ 3 printf("VAR=0x%x",Var); 4 5 /* Get Var variable bit 11 value */ 6 VarBitValue = Var_GetBit_BB(VarAddr, 11); /* VarBitValue = 0x00000000 */ 7 printf(" Bit11=%x\n",VarBitValue); 8 9 //****************************************** 10 Var_SetBit_BB(VarAddr, 11); /* Var = 0x00005AA5 = 0000 0000 0000 0000 0101 1010 1010 0101*/ 11 printf("VAR=0x%x",Var); 12 13 /* Get Var variable bit 11 value */ 14 VarBitValue = Var_GetBit_BB(VarAddr, 11); /* VarBitValue = 0x00000001 */ 15 printf(" Bit11=%x\n",VarBitValue);
位带区地址(序号) | …… | 0x2000 0003.(7 ~ 0) | 0x2000 0002.(7 ~ 0) | 0x2000 0001.(7 ~ 0) | 0x2000 0000.(7 ~ 0) |
位带区地址对应的内容 .(7 ~ 0) | …… | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
Reset(VarAddr, 11)之后 | 0000 0000 | 0000 0000 | 0101 0010 | 1010 0101 | |
再Set(VarAddr, 11)之后 | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
BitNumber=0,Reset得到的结果是把第一个地址的.11位(第12位)变成了0,Set后又回到了1。从这里这里看到,当要改变下一个地址中的内容时,不是用VarAddr+1,而是用更大的BitNumber,即n不限制在0~7,而是0~31,或者更大。这样又出现了一个问题:之前说到的
( RAM_BB_BASE | ( (VarAddr - RAM_BASE) << 5) | ( (BitNumber) << 2) ) = 转换公式的右边
前提之一是 n=0~7,即只与低位的0按位或,这样才和加法是等效的。
结合2.2.1最后提到的问题,其实这也是一种等效。当BitNumber > 7 时,可以看成是一次进位。
假设VarAddr = 0x2000 0000 ,当BitNumber = 7, 则指的是 0x2000 0000.7 即第1个地址的第8位
当BitNumber = 11 = 7 + 4 ,相当于VarAddr+1,指的是0x2000 0001.3 即第2个地址的第四位
因此BitNumber 和 VarAddr就像个位和十位的关系一样,不过是逢八进一。而且这样做有个好处,只需要改BitNumber就行,而不需要同时改BitNumber和VarAddr,比如在 Var_ResetBit_BB(VarAddr, BitNumber)函数中不用 Var_ResetBit_BB(VarAddr+1, BitNumber)了,而是直接根据需要,修改BitNumber就行。
另外,在Set以后也可以看到,VarBitValue变成了1(即只有一个字中的最低位变成了1)。
(3)
1 /* Modify Var variable bit 31 -----------------------------------------------*/ 2 Var_SetBit_BB(VarAddr, 31); /* Var = 0x80005AA5 = 0x1000 0000 0000 0000 0101 1010 1010 0101 */ 3 printf("VAR=0x%x",Var); 4 5 /* Get Var variable bit 31 value */ 6 VarBitValue = Var_GetBit_BB(VarAddr, 31); /* VarBitValue = 0x00000001 */ 7 printf(" Bit31=%x\n",VarBitValue); 8 9 //****************************************** 10 Var_ResetBit_BB(VarAddr, 31); /* Var = 0x00005AA5 = 0x0000 0000 0000 0000 0101 1010 1010 0101 */ 11 printf("VAR=0x%x",Var); 12 13 /* Get Var variable bit 31 value */ 14 VarBitValue = Var_GetBit_BB(VarAddr, 31); /* VarBitValue = 0x00000000 */ 15 printf(" Bit31=%x",VarBitValue);
位带区地址(序号) | …… | 0x2000 0003.(7 ~ 0) | 0x2000 0002.(7 ~ 0) | 0x2000 0001.(7 ~ 0) | 0x2000 0000.(7 ~ 0) |
位带区地址对应的内容 .(7 ~ 0) | …… | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
Set(VarAddr, 31)之后 | 1000 0000 | 0000 0000 | 0101 1010 | 1010 0101 | |
再Reset(VarAddr, 31)之后 | 0000 0000 | 0000 0000 | 0101 1010 | 1010 0101 |
结果和(2)中得到的结论相符。
2.2.3
到这儿基本上把位带操作及其实现的基础部分写完了,最后再来一发从位带别名区地址反推回位带区地址的过程,备忘。
假设 AliasAddr = 0x2200 002C = 0010 0010 0000 0101 0000 0000 0010 1100
可以把AliasAddr分成3段,1[ 0010 001 ] 2[ 0 0000 0101 0000 0000 001 ] 3[ 0 1100 ]
第1段,在后面补上25个0,可以看成是位带别名区的起始0x2200 0000
第2段,就是offset=VarAddr - BB_Base ,也即相对位带区的偏移 0 0000 0101 0000 0000 001 = 0000 0010 1000 0000 0001 = 0x02801
第3段,右移两位后就是BitNumber啦 右移后得到011 = 3
这样可以得到相应的位带区 地址.位 是 (0x2000 0000 + 0x0000 2801) . 3= (0x2000 2801) . 3
再加一点内容吧,关于内存对齐,来自C语言吧
【
如果你了解体系结构,就会知道,计算机内存寻址并不是一个字节一个字节读取的,而是一次读取多个,比如32bit数据线的计算机就可一次读取4字节,既一个int值.这时就出现问题了,比如你在结构体中定义如下:
struct a{
char c;
int i;
}
那么计算机在内存中该如何存放呢?
比较笨的办法是c占第一个字节,i占用2-5字节.那么假设你的程序正好处于寻址边界,比如0x0000这样的位置,那么计算机为了获取i,就必须先获取1-4字节,然后左移8位,再获取5-8字节,右移24位,然后再相加,才能得到i,无意这种办法是比较傻的.所以计算机在处理这种问题的时候,往往会将内存按4字节对齐,比如c占用第一字节,i占用5-8字节.这样i就和c在4位上对齐了.相当于我们写字一行不够了,干脆就不写在一行,直接重起一行.主要是方便寻址,提高性能.
】
没想到写这篇博客花了这么长时间,对于位带操作及其实现的认识也是反反复复,写的时候再一思考发现部分原来的理解是错误的。
如果还有其他错误,还希望读到这一篇文章的你能够帮忙指正,也帮助我学习 :)
参考资料:1 http://www.cnblogs.com/emouse/archive/2012/03/01/2375146.html
2 http://blog.chinaunix.net/uid-26285146-id-3071387.html
3 http://www.amobbs.com/thread-5464765-1-1.html
4 http://tieba.baidu.com/p/2138899913
5 《嵌入式系统及其应用》_同济大学出版社 P37~P42
补充一句:
其实一般来说,初学不需要掌握函数内部的知识、过程,只需要知道怎么用就好了,《码农的原罪》里面有一句“没必要就不用学,有必要的时候你自然就会了。”
刚入门时学会新建一个工程、导入文件、相关设置才是更重要的。
2014.5.20补充
由同学指出,新发现一点疑问,就是这段话
在位带区,每个比特都映射到别名地址区的一个字,该字只有最低位有效。当一个别名地址被访问时,会先把该地址变换成位带地址。对于读操作,读取位带地址中的一个字,在把需要的位右移到需要的最低位并把最低位返回。对于写操作,把需要写的位左移至对应的位序号出,然后执行一个原子的“读——改——写”过程。
参考下面两篇
还没重看程序,我觉着我们只需要改相应地址的位带别名区的内容(最低位)就好,而改完之后,就由ARM内核自动完成了“位带”功能,即在发现位带别名区改变之后,自动改变相应的位带区内容。
以后看有时间能不能再仔细研究一下
今天第一次面试,比较累。
在5.20,同济,生日快乐~