开发环境:
开发板:STM32PZ6806L
芯片:ARM_STM32F103_ZE
软件开发环境:KEIL5
开发所需资料:
STM32F1XX芯片电路原理图
STM32F1XX系列芯片手册
XX代表系列版本号,ARM公司开发的芯片大多数都是一样的,除非增加了新功能才会更正芯片手册,XX就代表该文档支持系列版本!
更新 2020.12.17
如没有这些文档则可以在这里下载:stm32开发板系列型号开发手册与教程视频下载地址_17岁boy想当攻城狮的博客-CSDN博客
在上面的链接里找到你开发板的型号,
本系列使用的是这个架构:
百度网盘链接:百度网盘 请输入提取码 提取码:2ykk
更新 2020.12.23
stm32f10x系列启动汇编文件:
第一步,分析电路原理图
首先第一步打开STM32F1XX芯片的电路原理图:
(图1.0)
找到LED模块电路的原理图,其次也是看一下这个板子上有没有设计LED电路模块:
(图1.1)
找到之后代表该板子已经设计了LED电路模块,那么放大来看一下:
(图1.2)
从LED模块电路原理图中可以看到,每个LED(LED1-LED8)都有一个二极管(D1-D8)连接着
二极管的两个引脚具有正负极区分,从原理图中可以得知该二极管的正极对应着LED,而负极对应着输入源
(图1.3)
所以如果我们想要让LED灯亮起,首先要经过连接在LED上的二极管,所以我们要让该二极管工作,从输入源里输入一个低电平,即可让二极管的负极一端引脚工作,相反输入高电平则二极管的负极一端不会产生任何作用!
(二极管极性连接识别方法)
(图1.4)
并且输入源中设有两个排阻,每个排阻中有四个电阻,用于简化PCB板设计
(图1.5)
但内部电阻作用原理图并没有明确给出,博主推断应该是上拉下拉电阻用于限流作用
(图1.6)
除此之外同时又得知该电路模块最大承受电平值为+3.3v(0-3.3v)
LED模块的电路原理图看懂了,那么就要知道LED模块连接在处理器的哪个总线上!
为什么需要知道在哪个总线上?
这里就拿找人打个比喻吧:
就像找人一样,你知道他是谁,是干什么的,但是你现在想要找到他,是不是要去他家里?
那么去他家里之前你要知道他家在哪儿,房子编号是多少,有个具体的路线和编号就可以轻而易举的找到他家,并找到他!
那么接下里我们看一下电路原理图中,CPU这一块的连接电路原理图:
(图1.7)
放大一点儿:
(图1.8)
(图1.9)
由此可以看到,每个LED模块(1-8)分别对应着PC特殊功能寄存器(0-7bit位)
知道LED对应的特殊功能寄存器,那么就要知道该特殊功能寄存器挂设在哪个总线上,映射地址是多少!
开始查找之前给大家补充一点儿知识:
什么是特殊功能寄存器?什么是映射地址?
答:特殊功能寄存器本质上也是一个存储单元和内存里的存储单元没有什么区别,只是内存里的存储单元用于存储单字节数据的,而微型处理器内部的存储单元往往都是4字节或8字节作为一个单元,用于做特殊计算/操作时用到的,比如DS段寄存器用于存储地址,它的大小决定了CPU的寻址能力!CPU会根据DS寄存器里的地址来寻址,再则地址总线位宽也要和DS寄存器一致,假如说地址总线位宽小于DS寄存器那么地址总线将没有能力表示出大于自身的地址!
什么叫映射地址?
答:这里做个比喻,假如说你家住某某小区xx栋楼xx号室,那么这个地址就是你房间的地址,每次点外卖或者快递我们一般都会填写这个地址,那么快递员有了这个地址就可以轻而易举的找到我们的房间并把货物递送给我们,这个地址就是你房间的映射,而对特殊功能寄存器的映射如上所说,就是把开发板上的一个物理地址分配给它,就叫做地址映射!
相关文章:深度理解“CPU内部寻址方式”, 详解:物理地址,虚拟地址,内存管理,逻辑地址之间的关系
言归正传,知道LED模块对应的特殊功能寄存器,我们就可以到STEM32的芯片手册里查看芯片各个寄存器以及地址映射和总线之间的介绍
打开STM32的芯片手册
(图2)
在2.3.30页找到待有对PC特殊功能寄存器总线挂接介绍的原理图:
(图2.1)
找到PC特殊功能寄存器:
(图2.2)
如上图可以看到PC特殊功能寄存器是由GPIO端口为C的GPIO管脚所连接的,而GPIO端口C挂设到APB2总线上,而APB2挂设在AHB2系统总线上!
注意这个挂设怎么区分的,首先GPIO端口C实则上是一组GPIO管脚组成的,只不过该管脚负责PC特殊功能寄存器的I/操作,其他GPIO管脚负责其它的特殊功能寄存器,列如PE,PB等,ARM为了加以区分,让开发人员更易读,所以为其进行了区分,也就是成了端口C,端口E,端口G等,分别对应不同的特殊功能寄存器,上面的总结和系统总线的区分打比喻就是一组里有小组的情况一样,每个小组对应不同的功能,但用管理一个组的方式管理所有的小组,而这个组的名字叫做系统总线。
地址总线,数据总线和控制总线均属于系统总线
就像人一样,你想要让某个人过来帮你忙,你是不是首先要叫他的名字,他才知道你是在叫他,如果在很多人的情况下,谁也不知道你在叫谁!
言归正传,那么有了名字要工作是不是要分组?让某一组去做或管理某个事情,形成一个项目体系,所以ARM为其进行了分组,方便于管理,GPIO端口C和其他端口被规划到了APB2总线上,也就是这一组的名字称为APB2,而APB2呢又被规划到了AHB2总线上,所以对应情况是:GPIO端口C和其他端口=APB2而APB2同时也等于AHB2,方便于区分和管理,且形成一个项目体系!
(图2.3)
这里从上图中可以知道,PC特殊功能寄存器所使用的总线连接引脚是GPIO,那么我们要查一下关于ARM是如何设计开发板的GPIO引脚的!
可以通过:STM32 Reference Manual这本开发文档里找到对GPIO引脚的设计,该开发文档对应所有芯片的GPIO引脚设计,除了一些特定的需要重新设计的开发板,因为ARM的系列STM开发板引脚设计所使用的方法都是基于此开发文档的,不会变更,由于文档较大,篇章较多建议下载中文版的:
可以在8.1里找到对GPIO引脚的设计图:
(图2.4)
从上图可以看到,STM32所使用的GPIO引脚内部带有保护二极管,用于防止过高过低的不正常电压进入芯片如果电压过高的话保护二极管会被烧掉,因为二极管会首先吸收电压判断电压值是否正确在让其进去芯片,倘若电压过高可能会直接烧掉二极管。
(图2.5)
接了两个是因为一个对应输入一个对应输出,GPIO是属于I/O引脚!
其次还有上拉下拉电阻,用于矫正电平!
(图2.6)
为什么有了保护二极管还需要上拉下拉电阻?
答:不同的模块所使用的电压不同,因为这些模块并非开发板自带的,而是后面焊接上去的,这些模块都是不同的硬件工程师开发的,所以接上上拉下拉电阻,可以把一个不确定的电压矫正成与模块所使用的正常电压!
其余的我们暂时不看,因为通过这些信息即可得知,我们这发送电压时无需考虑电脑所使用的电压是否与开发板子一直,当然要确定你的电脑电压不能太高!一般来说家用电脑都在3.xv左右,所以这种电压保护二极管是可以完全承受的!
其次STM32 Reference Manual开发文档中还有对存储器与总线之间的架构图:
(图2.7)
从上可以明了的看出总线与存储之间的分组架构:
(图2.8)
知道了挂设总线,就像上面打的比喻,知道了名字想要找到他让他帮忙是不是需要知道他家在哪?就算打电话是不是也要知道电话号码?
那么我们在芯片手册里找一下特殊功能寄存器的映射:
可以在芯片手册的第4章找到总线映射地址,注意特殊功能寄存器是连接在总线上的,所以也就是总线映射地址:
(图2.9)
下面来说一下上面的地址映射介绍
(图3)
从该图中可以得出,ARM将地址空间划分为了八块,每块大小为512-Mbyte(MB),名称叫做bloc x以及作用
从上可以的值,ARM将地址空间划分为了八块,每块大小是512MB,4*512=4G空间,也就是说ARM将4G空间划分为了4块,每块512MB,其作用我们看下图:
(图3.1)
从上图的内存映射来看,每块内存大致用途如下:
(图3.2)
讲解这些只是为了让大家对ARM的区域划分有个认识,最重要的地方还是地址映射:
(图3.3)
右侧ARM将划分四块地址,每块地址的总线名都给写出来了,那么就可以在地址映射表里找到与我们所需要操作的总线名和地址,便可以通过C语言指针方式来操控它,下面是四块内存的地址映射表:
(图3.4)
(图3.5)
根据对图1.9,2.2和2.3,3.1,3.2的分析得出,用于控制LED状态的特殊功能寄存器名字叫做:PC寄存器,而用于连接PC寄存器的GPIO端口为C,且该GPIO挂接在APB2片上外设总线上(外部总线),而APB2挂接在AHB2外设总线上,所以我们要操控PC寄存器就要找到GPIO端口为C的引脚:
(图3.6)
放大一点:
(图3.7)
从上图中可以得出该组引脚属于:bloc2内存块,且内存偏移地址为:0x40011000-0x400113FF(1024字节也就是1MB的大小)
(图3.8)
从电路图以及存储器映射表中已经的值我们需要的操控的LED模块属于哪个特殊功能寄存器控制以及偏移地址是多少,并且知道了连接该特殊功能寄存器挂设在哪个总线上:
LED模块对应的特殊功能寄存器:PC寄存器
PC寄存器对应的GPIO:GPIO_C
GPIO_C挂设在:APB2外设总线上,且偏移起始地址为:0x40011000-0x400113FF
APB2外设总线挂接在:AHB2系统总线上
AHB2地址空间划分在:bloc2空间中
注意从图2.8中可以看出,AHB总线是由RCC时钟电路控制的:
(图3.9)
从上图可以看到,凡是挂接在AHB系统总线上的任何总线都由RCC时钟电路控制其状态,下面来详细的解释一下ARM为什么这样做,以及时钟电路的工作原理:
ARM在总线上架设一个时钟电路的原因主要是为了降低开发板的功耗,起到节能省电的作用,但是同时也给开发人员带来了研究电路原理图和代码量的增长问题,其他的开发板一般都是找到物理地址,发送电平值使其芯片工作,也就写成了一个简单的硬件驱动!
但ARM对这些地址进行了分组,每组上都有一个时钟电路,包括GPIO每个端口都对应着一个时钟电路,这些时钟电路是根据对应的特殊功能寄存器状态来工作的!
就比如有一个总线,总线名叫:CBV1,那么这个总线是由一堆GPIO管脚组成的,且这些管脚被分组了,分成GPIO端口C,用于控制LED,GPIO端口为D的用于控制蜂鸣器等。
(图4.0)
那么在不工作的情况GPIO端口会不停的向这些芯片发送低电平(取决于二极管极性),就出现了即使这个模块不用但还是一直通电,浪费功耗的情况。
所以ARM就在每个端口前设置了一个时钟,用于限制电流经过,这些时钟的工作状态取决于对应的特殊功能寄存器,且这个特殊功能寄存器被架设在CBV1的总线上,这些时钟会根据CBV1总线上的特殊功能寄存器状态来工作,架设在CBV1总线上了所以偏移地址也从CBV1的基地址开始算,CBV1的基地址就是所有GPIO端口中地址最低的那个GPIO端口地址!
那么一个疑问来了,限流也就不让电平通过但是这些电平还是会被发送到时钟电路上啊!
答:PC寄存器有8个bit位,有GPIO端口C的I/P管脚来控制,那么GPIO端口C总共有8个管脚对应着,GPIO端口向PC寄存器写入某个电平值都会改变LED芯片工作。
LED芯片会根据特殊功能寄存器来工作,每写入一次寄存器,寄存器里的存储单元就要发生一次变化,那么这样的话所做的操作就需要更多的电流值来变换,相反这样的重复操作每次都无用的,所以ARM想直接给限制掉不写入任何值,这样的话存储单元就无需将新的电平值写入到存储单元当中去了!
所以我们要想要让LED灯亮起就必须将控制AHB系统总线的RCC时钟电路设置成推送状态,那么我们可以在STM32 Reference Manual开发文档中找到对RCC时钟电路的介绍:
(图4.1)
从上图文章栏中可以看到对外设时钟有很多方的介绍,这里我们只查找关于对APB2外设时钟使能寄存器的介绍,也就是6.3.7。
为什么不看AHB的?
答:上面说过APB2是挂设在AHB系统总线上的,但ARM在设计时并没有让其和AHB共用同一时钟电路,相反单独为其设置了时钟电路,所以APB2虽然说是挂设在AHB上的,但有自己的时钟电路,无论AHB系统总线的时钟电路开与关都与APB2无关,ARM这样画图只是为了表明APB2挂设在AHB系统总线上的用于分组,当然AHB的时钟电路也有自己的作用:
下面给大家看一下AHB的时钟电路介绍:
(图4.2)
可以看到大多数都是用于控制核心模块工作的,上面的时钟电路对应的特殊功能寄存器位介绍里没有针对GPI/O端口的控制位!
下面我们来看一下7.3.7对APB2的时钟电路介绍:
基本介绍:
(图4.3)
与电路时钟对应的状态寄存器每个bit位的读写权限介绍:
(图4.4)
与电路时钟对应的状态寄存器每个bit位状态介绍:
(图4.5)
从上图可以看到与APB2时钟电路对应的特殊功能寄存器的第4个bit位,就是对IO端口C(GPIO_C)时钟电路的控制:
(图4.6)
通过上面所知道APB2的地址总线上的时钟电路偏移地址为:0x18,那么就要知道位于APB2总线上的RCC时钟基址是多少!
通过对图2.2的分析可以得出,GPIO端口C位于APB2总线上的,又通过对图3.6的分析得出,GPIO端口C管脚挂接的地址总线空间被划分到block2上,所以我们可以直接在block2上找到时钟电路的基址:
(图4.7)
放大一点儿:
(图4.8)
通过基址可以得出APB2的时钟电路物理地址=0x4002100+0x18=0X4002118
到这步基本上所有的地址都已经分析完毕了,除此之外在实际开发之前还要知道关于对GPIO端口功能的介绍:
(图4.9)
并且通过对图2.4的分析:
(图5.0)
(图5.1)
从上图可以得出,该GPIO输入口处可以看到上拉和下拉电阻都接了VDD三极管和VSS三极管用于限制电流经过,所以我们如果要想让电流顺利通过上拉和下拉电阻写入到寄存器里,就必须让VDD开启推挽输出(推挽输出:推挽放大器电路中,一只三极管工作在导通、放大状态时,另一只三极管处于截止状态,当输入信号变化到另一个半周后,原先导通、放大的三极管进入截止,而原先截止的三极管进入导通、放大状态,两只三极管在不断地交替导通放大和截止变化,所以称为推挽放大器,一般用于低功耗输出大功率电路中。),否则就会变成如下情况:
(图5.2)
也就是变成了不导通状况,不让电流经过,如果想要让电流经过的话这时候就需要去查看关于对GPIO端口的特殊功能寄存器介绍了:
在8.2章节可以找到对GPIO端口寄存器的介绍:
,ARM为每个端口设置了7个寄存器用于控制端口状态!
可以在8.2文章看到:
对GPIOX端口寄存器的各个介绍(X为端口号)
下面我们来看一下:
(图5.3)
在8.2中可以找到对GPIO端口I/O介绍,我们找到对应的状态寄存器你并开启推挽输出,让其某一个三极管变成放大状态,这样的话我们就可以顺利让电流经过VDD了!
注意STM32中的推挽输出只需要设置一个bit位即可,上面介绍也说了。推挽输出时的三极管一个工作则另外一个就会不导通,另外一个不导通则另一个就会工作!
下面我们来看一下下GPIO的工作方法:
(图5.4)
GPIOX表示任意端口号,而后面的
X表示端口多少到端口多少,上面的为X=A..E说明GPIOA-GPIOE的端口均设有该寄存器!
CBFy其中的y代表相应的管脚号,也就是我们要操控的端口上的引脚号
(图5.5)
并且也详细介绍了寄存器上每个位的功能:
(图5.6)
大家通过上方的位介绍可以看出,每个位是以4bit做分割的:
(图5.7)
这里解释一下:
3:2这里想表达的是0,1,2,3这组bit位,而7:6实则上表达的是:4,5,6,7这组bit位,所以算出来每组相差为4个bit位,ARM编写开发文档时只是将高位标了出来,低位并没有写出来,这是一个容易令人迷惑的区域!
并且通过介绍可以看到:
(图5.8)
设置寄存器的第1:0个bit位来确定GPIO的输入输出状态!
并且下面也有不同的bit位设置不同的状态,一般来说虽然说有四个bit位做一组,但实则上我们一般只用到低位!
(图5.9)
继续分析下一个寄存器
(图6.0)
对BSRR寄存器分析一下
(图6.1)
其意思是低0-15位设置1则向对应的寄存器发送一个高电平(ODRy)(y为对应位,ODR为寄存器),16-31位设置1则向对应寄存器发送一个低电平(ODRy)(y为对应位,ODR为寄存器)!
在说明白点:假如:ODR是PC寄存器,而GPIOC对应着,我们向GPIOC端口的BSRR寄存器的高16位写入一个高电平(1)那么GPIOC就会向PC寄存器发送一个低电平,低位发送一个高电平(1),则向PC寄存器发送一个高电平,对应位分别是BSRR16+偏移量!
比如向PC0发送一个低电平(根据贴片二极管极性),让第一个LED灯点亮,那么就是向BSRR寄存器的高第16位+0写入一个高电平(1)即可
如果让其熄灭即向BSRR的低位0位写入一个高电平(1)即可
按上面所说的,如果想让第二个LED灯点亮,那么就是向BSRR寄存器的高16+1位写入一个高电平即可
如果想让其熄灭即向低位的0+1位写一个高电平即可
其他寄存器我们就暂时不分析,因为暂时用不到!
有了这些信息就可以开始实践动手开发了!
首先编写代码之前,我们打开kile5创建一个新的工程文件:
(图6.2)
(图6.3)
这里我保存到c盘test_led目录下,工程文件也叫led
(图6.4)
然后在弹出的CPU型号选择框里,选择与开发板对应的CPU型号
(图6.5)
(图6.6)
(图6.7)
选择完之后会弹出说明手册,直接跳过
(图6.8)
增加新的工程文件
(图6.9)
(图7.0)
添加新的工程文件完成之后我们配置一下kile5的魔术棒选项,让其生成二进制文件(hex),以便于烧录到开发板中,注意:我们使用的开发板是裸板,里面没有任何操作系统,所以必须生成纯二进制文件,才能让其CPU正确解析里面的二进制指令,像EXE这样的可执行文件格式中分为:头信息区和数据区,其中当我们运行EXE文件时操作系统会自动将头信息区的数据分离开,只留下数据区给CPU这样才避免了无法执行的指令问题!
(图7.1)
(图7.2)
除此之外也别忘记将stm官方提供的启动汇编文件添加进来一并编译,否则无法正常编译和运行:
(图7.3)
启动汇编文件中其他代码无需关系,这里我们看一下第149行开始中有非常关键的代码:
(图7.4)
学过汇编的应该很容易看出来,下面来解释一下:
第 149 行是定义了一个子程序: Reset_Handler。 PROC 是子程序定义伪指
令。这里就相当于 C 语言里定义了一个函数,函数名为 Reset_Handler。
第 150 行 EXPORT 表示 Reset_Handler 这个子程序可供其他模块调用。 相当于 C 语言的函数声明。关键字[WEAK] 表示弱定义,如果编译器发现在别处定
义了同名的函数,则在链接时用别处的地址进行链接,如果其它地方没有定义,编译器也不报错,以此处地址进行链接,如果不理解 WEAK,那就忽略它好了。第 151 行和第 152 行 IMPORT 说明 __main 和 SystemInit 这两个标号在其
他文件,在链接的时候需要到其他文件去寻找。相当于 C 语言中,从其它文件引入函数声明。以便下面对外部函数进行调用。
SystemInit 需要由我们自己实现,即我们要编写一个具有该名称的函数,用来初始化 STM32 芯片的时钟,一般包括初始化 AHB、 APB 等各总线的时钟,需要经过一系列的配置 STM32 才能达到稳定运行的状态。__main 其实不是我们定义的(不要与 C 语言中的 main 函数混淆),当编译器编译时,只要遇到这个标号就会定义这个函数,该函数的主要功能是:负责初始化栈、堆,配置系统环境,准备好 C 语言并在最后跳转到用户自定义的 main 函数,从此来到 C 的世界。
第 153 行把 SystemInit 的地址加载到寄存器 R0。
第 154 行程序跳转到 R0 中的地址执行程序,即执行 SystemInit 函数的内容。
第 155 行把__main 的地址加载到寄存器 R0。
第 156 行程序跳转到 R0 中的地址执行程序,即执行__main 函数,执行完毕之后即可进入 main 函数。
第 157 行表示子程序的结束。
总之,看完这段代码后,了解到如下内容即可:我们需要在外部定义一个SystemInit 函数设置 STM32 的时钟; STM32 上电后,会执行 SystemInit 函数,最后执行我们 C 语言中的 main 函数。
创建好工程文件之后就可以开始编写代码了:
首先第一步根据已经得知的地址信息来定义地址:
首先通过对图3.3的分析得知,GPIOC被划分到block_2的地址空间里去了,所以我们要先将基址定义出来:
(图7.5)
存储空间中地址为最小的那个就是存储空间的首地址/基址:
(图7.6)
即0x40000000,那么我们将其定义出来,后面定义地址只要在block2空间上的直接用基址加上偏移地址即可,方便于后期管理!
这里是以无符号整形的方式定义它,明确告诉编译器这个值不是负数,只能是整数!
//定义电路地址
#define BLOCK_2 (unsigned int)0x40000000 //BLOCK_2基址
那么在继续定义我们所需要的APB2的基址,通过对图1.8和图2.2的分析
(图7.7)
(图7.8)
可以得知LED模块对应在PC寄存器上的,而PC寄存器对应在GPIO端口C上的,GPIO_C这组引脚属于APB2总线,所以我们在这里定义一个GPIO_C的总线地址:
(图7.9)
所以定义如下地址:
#define BLOCK_2_APB2_GPIO_C (BLOCK_2+0x11000) //APB2总线上GPIO_C引脚基址
除此根据对图5.3的分析知道CRL寄存器是控制GPIO端口的推送状态
(图8.0)
偏移地址为0x00所以我们定义出来
//定义GPIOC寄存器的偏移地址
#define BLOCK_2_GPIOC_CRL *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x00)
注意这里一定要用*(usigned int*)指针的方式来定义,否则编译器会把这个宏看成常量,常量是不能作为左值运算的,所以我们以指针的方式修饰它,并在前面加上解引用,显示的告诉编译器这个常量为一个地址,如果不在前面加上*解引用的话编译器还是会把这个值看成一个常量地址,所以对其解引用就是访问这个地址空间!
这里我要讲一下啊,为什么第一行定义block2空间的基址不用以指针的方式定义它而是以整形的方式?
答:如果你把基址以指针的方式修饰它的话,那么当我们根据基址+偏移量时编译器就会这样做:
将基址空间里的值取出来然后加上偏移地址,而非让基址+偏移地址,所以这里不要搞混淆了!
上面也说了,如果想要控制GPIO端口发送高低电平就需要设置BSRR寄存器,我们根据偏移地址将其定义出来:
(图8.1)
#define BLOCK_2_GPIOC_BSRR *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x10)//GPIO_BSRR寄存器
除此之外,还要开启APB2时钟电路,通过对图4.3,和图4.7的分析得知时钟电路的基址是:0x40021000
(图8.2)
偏移地址是:
(图8.3)
所以我们定义一下基址:
#define BLOCK_2_RCC_BASE (BLOCK_2 + 0x21000) //bloc时钟电路基址
在定义一下APB2时钟基址:
#define BLOCK_2_RCC_APB2 *(unsigned int*)(BLOCK_2_RCC_BASE+0x18) //APB2时钟电路基址
完整代码:
//定义电路地址
#define BLOCK_2 (unsigned int)0x40000000 //BLOCK_2基址
#define BLOCK_2_APB2_GPIO_C (BLOCK_2+0x11000) //APB2总线上GPIOC_引脚端口
#define BLOCK_2_GPIOC_CRL *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x00) //GPIO_CRL寄存器
#define BLOCK_2_GPIOC_BSRR *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x10)//GPIO_BSRR寄存器
#define BLOCK_2_RCC_BASE (BLOCK_2 + 0x21000) //APB2时钟电路基址
#define BLOCK_2_RCC_APB2 *(unsigned int*)(BLOCK_2_RCC_BASE+0x18) //APB2时钟电路基址
地址定义好了,那么就可以开始编写实际代码了:
第一步先开启时钟:
通过对图4.6的分析
(图8.4)
可以得出对位4发送一个高电平(1)即可让时钟开启:
BLOCK_2_RCC_APB2 |= 1 << 4; //开启时钟电路
这里一定要用|运算,如果你不知道的话可以查看这篇文章:按位运算操作符底层实现原理
位移就是将1从低位开始移动四个位,并将第四位的值|运算上这个1!
然后在设置GPIO_C管脚为推送状态:
(图8.5)
BLOCK_2_GPIOC_CRL |= (2 << 4 * 0); //设置GPIO_C端口为推送状态
这里2转化为二进制就是10,左移4*0个上面也说了bit位的分组是按4个bit位为一组管理一个引脚,写入时都按低位开始写入,所以公式得出:4*0 = 0也就是从第一组的低0位开始写入,如果你要写入第二个的话4*1即可从第二组的低位开始写入!
这里要说一些为什么设置成复用功能推挽输出模式而不是通用推挽输出模式:
GPIO端口复用电路:用以提高主控芯片的GPIO端口的利用率,使得能够利用有限的GPIO端口实现更多的功能,节省GPIO端口资源。
GPIO端口通用电路:常规输入输出
那么如果想要让灯亮起来的话需要向LED模块发送一个低电平(根据极性),所以我们只需要向BSRR寄存器的高位+PC偏移量写入一个高电平,那么GPIO端口就会向LED模块的对应位发送一个低电平!
BLOCK_2_GPIOC_BSRR= (1 << (16 + 0)); //发送一个低电平
这里我们要第一个LED灯亮起来,所以就是16+0第0个,如果想要让第二个亮起那么就是16+1!
并且在main函数之前声明定义SystemInit函数,用于STM32启动文件的初始化:
void SystemInit()
{
}
完整代码:
//定义电路地址
#define BLOCK_2 (unsigned int)0x40000000 //BLOCK_2基址
#define BLOCK_2_APB2_GPIO_C (BLOCK_2+0x11000) //APB2总线上GPIOC_引脚端口
#define BLOCK_2_GPIOC_CRL *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x00) //GPIO_CRL寄存器
#define BLOCK_2_GPIOC_BSRR *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x10)//GPIO_BSRR寄存器
#define BLOCK_2_RCC_BASE (BLOCK_2 + 0x21000) //APB2时钟电路基址
#define BLOCK_2_RCC_APB2 *(unsigned int*)(BLOCK_2_RCC_BASE+0x18) //APB2时钟电路基址
//stm32初始化函数
void SystemInit()
{
}
int main(){
BLOCK_2_RCC_APB2 |= 1 << 4; //开启时钟电路
BLOCK_2_GPIOC_CRL |= (2 << 4 * 0); //设置GPIO_C端口为推送状态
BLOCK_2_GPIOC_BSRR = (1 << (16 + 0)); //向PC0发送一个低电平
}
我们点击编译生成来看一下是否有问题:
(图8.6)
编译结果:
(图8.7)
正常编译通过,到编译目录下看一下是否生成了hex文件:
最后在通过stm官方提供的烧录软件,将其hex程序烧录进去:
选择好我们开发板的通讯串口:
(图8.8)
在点程序下载:
(图8.9)
这里我们无需管程序会被下载到哪儿,因为在上面也说过:
0x0800 0000-0x0807 FFFF:片内 FLASH,这一块内存会存放我们烧录的程序,而CPU通电就会执行这块内存的指令,其烧录软件已经帮我们指定好了无需手动指定!
运行结果:
(图9.0)
成功让其亮起来了,那么在动动手让其闪烁
首先我们声明一个函数并实现它:
函数名为:
void sleep(int time);
无返回值,用于循环延时:
实现代码:
//延迟函数
void sleep(int time){
while (time--); //递减,while循环不为0循环继续,当time为0时循环结束
}
在修改main函数代码:
BLOCK_2_RCC_APB2 |= 1 << 4; //开启时钟电路
BLOCK_2_GPIOC_CRL |= (2 << 4 * 0); //设置GPIO_C端口为推送状态
BLOCK_2_GPIOC_BSRR = (1 << (16 + 0)); //设置高位16+0,即向pc0输出一个高电平
while (1){ //死循环闪烁
BLOCK_2_GPIOC_BSRR = (1 << (16 + 0)); //亮
sleep(0xFFFF);
BLOCK_2_GPIOC_BSRR = (1 << (0)); //灭
}
在运行时可以看下板子有没有在闪烁:
(图9.1)
完整代码:
//定义电路地址
#define BLOCK_2 (unsigned int)0x40000000 //BLOCK_2基址
#define BLOCK_2_APB2_GPIO_C (BLOCK_2+0x11000) //APB2总线上GPIOC_引脚端口
#define BLOCK_2_GPIOC_CRL *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x00) //GPIO_CRL寄存器
#define BLOCK_2_GPIOC_BSRR *(unsigned int*)(BLOCK_2_APB2_GPIO_C+0x10)//GPIO_BSRR寄存器
#define BLOCK_2_RCC_BASE (BLOCK_2 + 0x21000) //APB2时钟电路基址
#define BLOCK_2_RCC_APB2 *(unsigned int*)(BLOCK_2_RCC_BASE+0x18) //APB2时钟电路基址
//stm32初始化函数
void SystemInit()
{
}
//延迟函数
void sleep(int time){
while (time--); //递减,while循环不为0循环继续,当time为0时循环结束
}
int main(){
BLOCK_2_RCC_APB2 |= 1 << 4; //开启时钟电路
BLOCK_2_GPIOC_CRL |= (2 << 4 * 0); //设置GPIO_C端口为推送状态
BLOCK_2_GPIOC_BSRR = (1 << (16 + 0)); //设置高位16+0,即向pc0输出一个高电平
while (1){ //死循环闪烁
BLOCK_2_GPIOC_BSRR = (1 << (16 + 0)); //亮
sleep(0xFFFF);
BLOCK_2_GPIOC_BSRR = (1 << (0)); //灭
}
}
如果上面有没有说到位,或者电路图分析不到位或者对代码有不理解的地方可以在下面评论区提问出来。