赞
踩
目录
4. 将Keil4生成的hex文件导入Proteus,进行仿真
三、STM32F103系列芯片的地址映射和寄存器映射原理,GPIO初始化设置的一般步骤及流水灯程序解释
1. 嵌入式C程序代码对内存(RAM)中的各变量的修改操作,与对外部设备(寄存器--->对应相关管脚)的操作有哪些相同与差别?
2. 为什么51单片机的LED点灯编程要比STM32的简单?
proteus安装可参考 Protues 8.9 仿真(安装包和安装步骤) - 知乎 (zhihu.com)
keil4安装可参考 Keil uVision4保姆级安装教程 - 知乎 (zhihu.com)
1. 打开Proteus,点击新建工程
2. 名称处输入工程文件名称,路径选择工程保存的路径,
点击下一步。
3. 选择从选中的模板中创建原理图,选择默认:DEFAULT
点击下一步。
4. 由于此仿真实验不需要绘制PCB,所以不需要创建PCB布板设计,
点击下一步。
5. 选择创建固件项目,在第二个选项栏的下拉选项中选择AT89C51,其他的默认都是51,
点击下一步。
点击完成。
6. 进入原理图绘制界面:
7. 在界面中左上角有标识符P,P表示放置元器件,里面有很多仿真电子器件,点击P
8. 在关键词处搜索“LED-”,找到发光二极管,可以看到有很多型号,选择绿色发光二极管
9. 开始放置LED灯,旋转角度
10. 一端连接到P2-0接口,另一端准备接地,接地在左边选择栏中的终端处,选择选择ground
11 .放置8个led灯,分别连接到P2-0~7端口采用共阴极接法
至此,原理图创建完成。
1. 打开keil4,点击工程project,选择创建一个新的工程,New uVision Project
2. 在新弹出的窗口中输入工程文件名称和选择工程的保存路径
3. 在弹出的窗口中选择数据库,选择Atmel,在下拉选项中选择AT89C51
4. 在弹出的窗口中选择 是
5. 进入主页面,创建一个新文件,点击保存,命名为led.c
6. 在里面编写程序
7. 在文件夹下添加led.c文件
右键点击文件夹,点击Add Files to Group ……
8. 点击魔术棒,在output中勾选输出hex文件
9. 编译文件,生成hex文件
依次点击 translate build rebuild
查看编译输出文件,build output:
无警告和错误,且生成了hex文件,编译通过。
1. 双击51单片机
2. 在Programe File 中选择刚才Keil4生成的hex文件
3. 点击右下角仿真按钮,进行仿真
Keil5安装参考:keil5安装教程_Matcha_ice_cream的博客-CSDN博客
1. 创建新的工程,点击project ,new uvision project
2. 输入工程文件名,选择工程路径
3. 选择stm32f103zet6对应的库文件,搜索stm32f103ze,点击ok
4. 选择ok
5. 创建led.c文件,并且进行编译,和keil4操作基本一样,不再赘述
在led.c中输入如下代码:
- #define PERIPH_BASE ((unsigned int)0x40000000)//AHB的地址
- #define APB2PERIPH_BASE (PERIPH_BASE + 0x10000) //APB2地址
- #define GPIOA_BASE (APB2PERIPH_BASE + 0x0800) //GPIOA地址
- #define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C //GPIOA_ODR地址
- #define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2))
- #define MEM_ADDR(addr) *((volatile unsigned long *)(addr))
- typedef struct{
-
- volatile unsigned int CR;
- volatile unsigned int CFGR;
- volatile unsigned int CIR;
- volatile unsigned int APB2RSTR;
- volatile unsigned int APB1RSTR;
- volatile unsigned int AHBENR;
- volatile unsigned int APB2ENR;
- volatile unsigned int APB1ENR;
- volatile unsigned int BDCR;
- volatile unsigned int CSR;
- } RCC_TypeDef;
-
- #define RCC ((RCC_TypeDef *)0x40021000)
- typedef struct
- {
- volatile unsigned int CRL;
- volatile unsigned int CRH;
- volatile unsigned int IDR;
- volatile unsigned int ODR;
- volatile unsigned int BSRR;
- volatile unsigned int BRR;
- volatile unsigned int LCKR;
- } GPIO_TypeDef;
- #define GPIOA ((GPIO_TypeDef *)GPIOA_BASE)
- void delay(unsigned int time)
- {
- unsigned int i=0;
- while(time--)
- {
- i=12000;
- while(i--);
- }
- }
-
- int main(void)
- {
- RCC->APB2ENR|=1<<2;
- GPIOA->CRL&=0x00000000;
- GPIOA->CRL|=0x33333333;
- GPIOA->ODR=0x01;
- while(1)
- {
- delay(1000);
- GPIOA->ODR=GPIOA->ODR<<1;
- if(GPIOA->ODR==0x0100)
- {
- GPIOA->ODR=0x01;
- }
- }
- }
6. 进行编译
1. 安装Jlink驱动
2. 配置Jlink烧录环境
点击魔法棒
点击debug
选择ST-LIN/v2 端口选择sw
3. 烧录程序
点击下载程序
:连接图
实验效果:
STM32流水灯实验效果
stm32采用的是Cortex-M3处理器,32位操作系统,具有:
32位寄存器、32位内部数据通路以及32位总线接口。那么其寻址能力就是2^32bit=4G
Cortex-M3采用的是哈佛计算机结构,即数据、指令分开存储,具有数据存储器和指令存储器
其最大4G的地址分配如下:
将4GB的大小分为代码区0.5GB、SRAM0.5GB、外设0.5GB、外部存储器RAM1G、外部设备 1G和私有外设0.5GB
一个典型的CPU是由运算器、控制器、寄存器等器件构成,这些器件都通过内部总线相连,CPU中,运算器进行信息处理、寄存器进行信息存储、控制器控制各种器件进行工作、内部总线连接各种器件,在它们之间进行数据的传输。
寄存器的作用是信息存储,通常存储的类型有数据、指令和地址。从本质上来讲,寄存器都是存放的01二进制编码,我们可以把其当作数据、指令和地址进行处理,根据不同的情况和需求,做对应的变换。
简单来说:
寄存器映射就是通过给绝对地址取一个别名,然后通过寄存器,修改该绝对地址的状态。
我们知道,一个单片机是通过CPU、存储器和许多外设组成
如下是stm32的系统架构
从图中可以看出,CPU连接有ICode总线、Dcode总线、系统总线和DMA总线
ICode总线连接着Flash接口,通过FLASH可以烧录程序
DCode,System和DMA通过总线矩阵连接着各种外设,如SRAM、FSMC
通过总线矩阵,又引出了AHB总线,AHB总线通过桥接的方式分为APB1和APB2总线
APB1和APB2连接着不同的外设,我们需要操作对应的外设,就要通过总线连接,找到对应地址
stm32参考手册中给出了寄存器映像,即外设的地址分布:
这里没有给完整,详情可以在参考手册中查看。
GPIO也是外设,其地址分布在0x40000000~0x5fffffff之内,和上面从Cortex的存储映像符合
那么如果我们要操作GPIOA的某一个端口,就需要修改对应寄存器的值,从图中可以找到寄存器 的范围在0x40010800-0x400100BFF
1. 时钟使能
为什么要时钟使能?因为外设的状态都是保存在寄存器中的,如果我们要修改外设的状态,那么就要修改对应寄存器的值,stm32内部寄存器都是采用的D触发寄存器,因此需要时钟使能才能使能D触发,才能修改寄存器中的值,从而改变外设的状态
2. IO口初始化(端口配置)
stm32的输出口有很多种模式,IO口初始化是用配置系统最开始的模式
3. 操作IO口
操作IO口一般是在循环中要完成的操作
根据上面的操作步骤首先是时钟使能
通过查看stm系统架构,知道了GPIOA是连接在APB2这条总线上,因此我们使能GPIOA口需要 操作APB2的外设时钟使能寄存器,查看APB2的外设时钟使能寄存器:
因此我们修改APB2时钟使能寄存器的第二位为1,就成功使能GPIOA端口了。
那么APB2的使能寄存器在哪里呢?
通过查表发现在AHB总线 复位和时钟控制 RCC的起始地址为0x4002100,
为什么APB2的时钟使能控制寄存器在AHB1上呢?
因为系统架构是通过AHB桥接成APB1和APB2的,换句话说就是通过AHB上面的时钟频率分频给 APB1和APB2
RCC总共有8个寄存器:CR 、CF、GR、CIR、APB2RSTR、APB1RSTR、AHBENR、 APB2ENR、APB1ENR、BDCR、CSR,我们需要使能GPIOA时钟,用到的是APB2ENR寄存器
其相对于RCC的偏移地址为0x18,那么使能寄存器映射地址就为:
0x40021000+0x18=0x40021018
在上面的程序中是通过volatile关键字声明了8个寄存器的RCC结构体完成的,其作用和直接声明寄存器映射地址效果是一样的:
可以简化为:#define APB2ENR ((unsigned int)0x40021018)
下一步就是使能时钟了,就是将该寄存器第二位设置为1:
现在就已经完成了GPIOA端口时钟使能了。
stm32的GPIO口总共分为8种模式
输入:上拉输入、下拉输入、浮空输入、模拟输入
输出: 复用推挽输出、普通推挽输出、复用开漏输出、普通开漏输出
在此次实验中所需要配置的类型为普通推挽输出,那么如何配置呢?还是通过操作寄存器配置
需要用到的两个寄存器分别为端口配置低寄存器(端口配置高寄存器),端口输出数据寄存器
那么首先应该找到这两个寄存器的映射地址:
这两个寄存器都在GPIOA寄存器组里面,因此首先要找到GPIOA寄存器的映射地址:
在表中APB2的起始地址为0x4001 0000,GPIOA寄存器组的起始地址为0x40010800
其次找到对应的两个寄存器偏移地址:
所以端口配置低寄存器映射地址为0x40010800+0x00=0x40010800
端口输出数据寄存器映射地址为:0x40010800+0x0c=0x4001080c
同样也可以通过直接声明寄存器的映射地址来完成
在代码中也是通过创建volatile关键词的GPIO结构体完成的:
前面说过,stm32是32位操作系统,端口配置低寄存器控制32个bit,每4位为一个端口,从小到大分别为PA0~PA8
每个4位的高两位CNF选择配置上面说的GPIO的8种的一种模式
第两位选择输入模式或者输出模式的输出频率
我们需要配置的是通用推挽输出模式,速度选择50Mhz,因此为00 11 在16进制里面为3
因为需要用到8个端口,所以可以将这8个端口全部设置为推挽输出,速度为50MHz:
这时端口输出模式选择完成了,但是不知道输出是0,还是1,因此需要端口输出数据寄存器
一个bit对应一个端口,最小系统板只有8个端口,则其有效位位0-8,当配置为0的时候,PA0则输出低电平,配置为1的时候,则输出高电平
初始化的时候,将PA0设置为高电平,其他为低电平,即该寄存器的值可以设置为:0x0001
到这里,初始化就完成了
完成上述程序配置后,PA的8个端口全部为推挽输出模式,并且输出速度为50Mhz
流水灯程序还需要一个延时程序
在程序中采用的是软件延时的方法:
在此次实验中,采用的是共阴极接法,8个led的阴极全部接地,阳极接PA的8个端口,当PA端 口输出高电平的时候灯亮,输出高电平的时候灯灭。
程序思路:初始化PA0为高电平,在循环中,先延时,然后左移,PA1为高电平,再判断是否0 ~8端口是否全为0,如果是的话,则又赋值为0x0001
程序流程图:
对外部的设备操作是通过找到寄存器地址,然后修改寄存器的值,从而修改外部设备的状态,一般可以通过宏定义一个指针,指向寄存器的地址:
对内存的修改操作也可以通过指针变量的方式进行修改。
寄存器是CPU内部的组成部分,通过内部总线与CPU直接相连,内存是通过外部地址总线与C PU相连,通过外部总线寻址,通过寄存器修改变量的值比内存更快。
因此在GPIO端口配置的时候,需要时钟信号来使能D触发器(寄存器),从而修改端口状态
51单片机的时钟只有一个,且没有进行分频等操作
将STM32虽然也只有一个系统时钟,但是可以通过分频,将APB1或者APB2的时钟时钟频率 更改,比51单片机多一个分频的操作,所以在开始修改寄存器时,51单片机直接能对寄存器 进行操作,而stm32则需要进行时钟配置使能。
此外,stm32是32位单片机,而51是位8单片机,stm32能够实现的功能能多,也就意味着其更 复杂。
register声明的变量直接存在在CPU的寄存器中,程序可以直接对该变量进行读写。访问内存的变量比访问寄存器的变量更慢,因此将需要快速访问的变量放入寄存器能提高程序的效率。
比较如下了两个c语言程序:
- for(int i=0;i<=100000000;i++)
- {
-
-
-
-
- }
-
- for(register int i=0;i<100000000;i++)
- {
-
-
-
- }
这里访问了变量i很多次,如果把i定义在内存中,那么CPU每次都会从内存中访问i,这样相比在寄存器中就会很慢;如果把i用 register关键字修饰,那么访问CPU就会通过寄存器的方式访问它,从而能提高程序运行的效率。
volatile声明的变量表示可以被某些编译器未知的因素更改,比如操作系统、硬件或者其他线程, 遇到这个关键字声明的变量,编译器对访问改变量的代码不在进行优化,从而可以提供对特殊地 址的访问。每次使用它的时候必须从内存中取出它的值,不会从寄存器中使用,从而保证了对某 特殊地址的稳定访问。
在嵌入式开发中,往往在宏定义的时候会看到这样的代码:
#define GPIOB_BASE ((unsigned int)0x40010800)
这样的好处便是在嵌入式编程中,硬件寄存器和内存映射设备常常需要使用volatile关键字,以确保编译器不会对其进行优化,从而保证与硬件的交互是准确的。
1. C51不需要分频,且为8位单片机,STM32需要时钟分频且为32位单片机,功能更复杂,C51 编写流水灯程序时比STM32更简单。
2. stm32存储器映射是给绝对地址命名个别名,通过寄存器更改绝对地址里面的值。
3. 寄存器是CPU内部的组成部分,通过内部总线与CPU直接相连,内存是通过外部地址总线与C PU相连,通过外部总线寻址,通过寄存器修改变量的值比内存更快。
4. register声明的变量尽可能直接放在寄存器中存储,volatile声明的变量只能从绝对地址中访问。
PS:参考网站:
proteus安装可参考 Protues 8.9 仿真(安装包和安装步骤) - 知乎 (zhihu.com)
Keil5安装参考:keil5安装教程_Matcha_ice_cream的博客-CSDN博客
keil4安装可参考 Keil uVision4保姆级安装教程 - 知乎 (zhihu.com)
4个案例代码告诉你,C语言中volatile关键字的高级玩法 (baidu.com)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。