文章目录
一、STM32内部地址
这篇博客所用的单片机为STM32F103C8T6,属于Cortex-M3。Cortex-M3有32根地址线,所以它的寻址空间大小为232 bit=4 GB。ARM公司设计时,预先把这4 GB的寻址空间分配好了。它把从0x40000000至0x5FFFFFFFF(512 MB)的地址分配给片上外设。通过把片上外设的寄存器映射到这个地址区,就可以简单地以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。这样,片上外设可以使用C语言来操作。CM3存储器映射见图(1)。
![](https://img-blog.csdnimg.cn/20211009101033643.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_17,color_FFFFFF,t_70,g_se,x_16)
现对这些地址系统地做一下说明:
一、Code区,地址在0x0000 0000到0x1FFF FFFF之间。
用途说明 | 地址范围 |
预留 | 0x1FFEC008 ~ 0x1FFFFFFF |
选项字节:用于配置读写保护、BOR级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不小心被锁住之后,可以从RAM里面启动来修改这部分相应的寄存器位 | 0x1FFFF800 ~ 0x1FFFF80F |
系统存储器:里面存的是ST出厂时烧写好的ISP自举程序(即Bootloader),用户无法改动。串口下载的时候需要用到这部分的程序。 | 0x1FFFF000 ~ 0x1FFFF7FF |
预留 | 0x08080000 ~ 0x1FFFF7FF |
FLASH:我们写的程序放在这里 | 0x08000000 ~ 0x0807FFFF(512KB) |
预留 | 0x00080000 ~ 0x07FFFFFF |
取决于BOOT引脚,为FLASH、系统存储器、SRAM的别名 | 0x00000000 ~ 0x0007FFFF |
二、SRAM区,地址范围在0x20000000到0x3FFFFFFF之间。
用途说明 | 地址范围 |
预留 | 0x20010000 ~ 0x3FFFFFFF |
SRAM(64KB) | 0x20000000 ~ 0x2000FFFF |
三、片上外设,这里主要介绍APB1、APB2和AHB总线,地址范围在0x40000000到0x5003FFFF之间。
用途说明 | 地址范围 |
APB1总线外设 | 0x40000000 ~ 0x400077FF |
APB2总线外设 | 0x40010000 ~ 0x40013FFF |
AHB总线外设 | 0x40018000 ~ 0x5003FFFF |
二、注意事项
2.1、启动文件
![](https://img-blog.csdnimg.cn/20211008204253165.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
注意启动文件里,那三行关于系统时钟配置的汇编代码要注释掉,因为在主函数中并没有配置系统时钟。如果没有注释掉,就会报图(3)中的错误。注释掉后,STM32会把HSI当做系统时钟,HSI = 8 MHz,由芯片内部的振荡器提供。
![](https://img-blog.csdnimg.cn/20211008205132984.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
关于上面提到的启动文件那几行汇编代码的含义如下:
Reset_Handler PROC ;Reset_Handler子程序开始 EXPORT Reset_Handler[WEAK] ;输出子程序Reset_Handler到外部文件 IMPORT__main ;从外部文件引入__main函数 IMPORT SystemInit ;从外部文件引入SystemInit函数 LDR R0,=SystemInit ;把SystemInit函数调用地址加载到通用寄存器R0 BLX R0 ;跳转到R0中保存的地址执行程序(调用SystemInit函数) LDR R0,=__main ;把main函数调用地址加载到通用寄存器R0 BX R0 ;跳转到R0中保存的地址执行程序(调用main函数) ENDP ;Reset_Handler子程序结束
注:汇编语言中用 ;(分号 ) 注释
2.2、debug配置
在烟花棒->Target里面,有两个地方值得注意,一个地方就是System Viewer File中的路径,要选择STM32F103xx.SFR所在的路径,具体看个人安装的地方,我的是E:\software\Keil5\MDK\ARM\PACK\Keil\STM32F1xx_DFP\2.3.0\SVD\STM32F103xx.SFR;另一个地方就是Use Custom前面的√要打上。如果那两个地方没有配置好,图(6)Debug过程中Peripherals下面的菜单就不会出现,也就不能通过分步调试观察寄存器的变化。
![](https://img-blog.csdnimg.cn/20211008205824995.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211008205955481.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211008210952106.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
三、LED点亮的原理
我所用的开发板中小灯连接的GPIO为GPIOC的第13引脚,如图(7)所示,所以给这个引脚低电平,LED就能点亮。
![](https://img-blog.csdnimg.cn/20211010110657537.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_13,color_FFFFFF,t_70,g_se,x_16)
四、相关寄存器以及地址介绍
4.1、STM32系统架构
STM32的系统架构如图(8)所示,可以看到,GPIOC挂载在APB2总线下,所以要想GPIOC工作,首先要做的就是使能APB2。
![](https://img-blog.csdnimg.cn/20211010111219343.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
4.2、RCC_APB2ENR寄存器
在STM32参考手册中可以看到APB2 外设时钟使能寄存器为RCC_APB2ENR,其偏移地址为0x18,这个偏移是相对于RCC寄存器的偏移,RCC寄存器的基址如图(9)所示,为0x4002 1000 ,所以 RCC_APB2ENR的地址就是0x4002 1018。
![](https://img-blog.csdnimg.cn/2021101011251967.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
RCC_APB2ENR的每个位如图(10)所示,由于我们需要点亮的LED所使用的GPIO口为GPIOC,所以我们重点关注GPIOC,可以看到在第4位。由图(11)可以看到,将该位置1,就可以开启GPIOC的时钟。
![](https://img-blog.csdnimg.cn/2021101011324854.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010113723124.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
4.3、GPIO寄存器
与点亮LED相关的GPIO寄存器有两位,端口配置高寄存器(GPIOx_CRH)和端口输出数据寄存器(GPIOx_ODR)。
因为STM32中一个寄存器只有32位,一个输出引脚占4位,所以一个寄存器中只能放8个引脚的数据。而一个GPIO下有16个引脚,所以就有端口配置低寄存器(GPIOx_CRL)和端口配置高寄存器(GPIOx_CRH)之分。而我所用的LED连接的为PC13,所以需要用端口配置高寄存器(GPIOx_CRH)。
端口配置高寄存器(GPIOx_CRH)的偏移地址为0x04,而GPIOC的基地址为0x4001 1000,如图(12)所示,所以GPIOC_CRH的地址为0x4001 1004.。
![](https://img-blog.csdnimg.cn/20211010121442149.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010121749527.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010121903361.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
我们需要将PC13配置成通用推挽输出模式,结合图(13)和图(14)可知,CNF13配置成00,MODE13配置成11即可。
端口输出数据寄存器(GPIOx_ODR)的偏移地址为0x0C,而GPIOC的基地址为0x4001 1000,所以GPIOC_ODR的地址为0x4001 100C。
![](https://img-blog.csdnimg.cn/20211010143918748.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/2021101014400753.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
由图(15)和图(16)可知,当我们把ODR13置0后,PC13就会输出低电平,LED也就会被点亮。
五、详细代码
typedef unsigned int u32;
#define PERIPH_BASE 0x40000000 //外设基址
#define GPIOC_BASE 0x40011000 //GPIOC的基址
#define APB2_BASE (PERIPH_BASE + 0x10000) //APB2的基址
#define AHB_BASE (PERIPH_BASE + 0x20000) //AHB总线外设基地址,不同芯片AHB总线外设基地址可能不同
#define RCC_BASE (AHB_BASE + 0x1000) //复位和时钟控制(RCC)的基址
#define RCC_APB2ENR (RCC_BASE + 0x18) //RCC_APB2ENR寄存器的地址,作用是打开APB2外设时钟,0x18是其偏移RCC寄存器的地址
#define GPIOX_CRH_OFFSET 0x0004 //GPIOX_CRH寄存器的偏移地址
#define GPIOX_ORH_OFFSET 0x000C //GPIOX_ORH寄存器的偏移地址
#define GPIOC_CRH (GPIOC_BASE + GPIOX_CRH_OFFSET) //GPIOX_CRH寄存器的地址
#define GPIOC_ORH (GPIOC_BASE + GPIOX_ORH_OFFSET) //GPIOX_CRH寄存器的地址
void delay(u32 x) //差不多延时5秒
{
u32 i = 0;
while(x--)
{
i = 10000000;
while(i--);
}
}
int main(void)
{
*((unsigned int *)RCC_APB2ENR) |= 0x00000010; //打开APB2时钟
//LED为PC13
*((unsigned int *)GPIOC_CRH) &= 0xFF0FFFFF;
*((unsigned int *)GPIOC_CRH) |= 0x00300000; //000000000 00110000 00000000 00000000,配置PC13为推挽输出,速度为50MHz
*((unsigned int *)GPIOC_ORH) &= ~(1 << 13); //只需要清一位,不能直接像上面一样直接与,因为十六进制中一下改变4位
*((unsigned int *)GPIOC_ORH) |= 0x00002000; //默认关LED
while(1)
{
//LED亮
*((unsigned int *)GPIOC_ORH) &= ~(1 << 13);
*((unsigned int *)GPIOC_ORH) |= 0x00000000;
delay(1);
//LED灭
*((unsigned int *)GPIOC_ORH) &= ~(1 << 13);
*((unsigned int *)GPIOC_ORH) |= 0x00002000;
delay(1);
}
// return 0;
}
详细代码如上面所示,这里将几点注意问题:
第一,就是AHB的基地址问题,很多地方都写了AHB的基址为0x40018000,其实这个要根据所使用的芯片而定。我所用的是STM32F103C8T6,AHB的基址则为0x40020000。具体的各个芯片AHB基址的对应关系,我也没能找到好的参考资料。其实在上面的程序写的有点复杂了。像APB2的基址为0x4001 0000,RCC的基址为0x4002 1000,这些在STM32参考手册里都能找到,不用自己根据偏移地址来算。
关于AHB的基址,我也这么尝试过,就当偏移地址为0x18000。虽然定义AHB的基址是为了确定RCC的基址,为了使得RCC的基址和STM32参考手册中的地址一样,我想当然地把偏移地址设置成0x3000,最终RCC的基址也为0x4002 1000。我以为既然地址是对的,LED应该会被点亮,但事实上LED无论如何都没亮。于是我开始Debug,观察寄存器的变化,结果分步调试时跳到一段汇编代码后就卡死在那里,如图(17)所示。
#define AHB_BASE (PERIPH_BASE + 0x18000) //AHB总线外设基地址,不同芯片AHB总线外设基地址可能不同
#define RCC_BASE (AHB_BASE + 0x3000) //复位和时钟控制(RCC)的基址![]()
图(17)
第二,关于*((unsigned int *)RCC_APB2ENR),这里做下解释。虽然 RCC_APB2ENR本身是一个地址,但是我们的编译器却不认识,所以需要(unsigned int *)把RCC_APB2ENR强制转换成 unsigned int * 类型的指针。学过C语言的都知道,在指针变量外面加一个 *,就可以访问里面所存的内容。这样,通过这条语句,我们就可以操作RCC_APB2ENR寄存器,改变里面的内容。
第三,当我们操作寄存器里面相关的某几位或者某一位时,要善于使用&、|、<< 和 >>操作。例如在上面的程序中,我们操作GPIOC_CRH寄存器时,由于一下改变4位,先&上 0xFF0FFFFF,将里面的4位清除,同时不改变其他位,再|上0x00300000,这样就把值写进去。对于GPIOC_ORH,由于只改变1位,所以不能像前面那样操作,可以&上 ~(1 << 13),这样就清除了第13位里面的内容,同时也不影响其他位,然后|上0x00000000,就把LED点亮了。
六、Debug分步调试
Debug中的分步调试也属于STM32中比较重要的一个部分,通过观察寄存器的变化,可以帮助我们快速定位问题,这里通过点亮LED操作,简单说一下Dubug的步骤。
首先我们打两个断点,如图(18)所示。然后在Peripherals->System Viewer里面选择GPIOC和RCC寄存器,如图(19)所示。
![](https://img-blog.csdnimg.cn/2021101015350544.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010153918528.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
然后我们点击RST、Run 、step over,开始运行程序。首先打开RCC寄存器,在APB2ENR那里可以看到IOPCEN打了一个√,这表明IOPCEN已经置1,GPIOC的时钟开启,如图(21)所示。继续运行,发现CRH寄存器中的Mode13变成0x03,说明PC13已经被设置成通用推挽输出模式,速率为50MHz,如图(22)所示。继续运行,发现ODR寄存器中的ODR13前面打了一个√,说明ODR13输出高电平,如图(23)所示。由于我所用LED为低电平点亮,所以此时LED不会亮,我们把√去掉后发现LED就亮了!
![](https://img-blog.csdnimg.cn/20211010154335277.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_20,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010154555130.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_7,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010155200751.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_6,color_FFFFFF,t_70,g_se,x_16)
![](https://img-blog.csdnimg.cn/20211010155033210.png?x-oss-process=image/watermark,type_ZHJvaWRzYW5zZmFsbGJhY2s,shadow_50,text_Q1NETiBA5LiN6Imv5bCR5bm05ZOm,size_6,color_FFFFFF,t_70,g_se,x_16)