参考:
https://blog.csdn.net/weixin_54742551/article/details/132409170?spm=1001.2014.3001.5502
https://blog.csdn.net/m0_61712829/article/details/132434192
https://blog.csdn.net/Johnor/article/details/128539267?spm=1001.2014.3001.5502
SPI:https://blog.csdn.net/weixin_62127790/article/details/132015224?spm=1001.2014.3001.5502
1、STM32简介
- STM32是ST公司基于ARM Cortex-M内核(程序指令的执行、加减乘除的运算都是在内核里完成的,相当于整个芯片的CPU。类似电脑一样,可以拿着intel或者AMD的CPU,然后自己完善外围电路,就可以推出自己品牌的电脑。ST公司拿着ARM公司设计的内核,再完善外围电路,整个封装起来,就做成了STM32)开发的32位微控制器
- STM32常应用在嵌入式领域,如智能车、无人机、机器人、无线通信、物联网、工业控制、娱乐电子产品等
- STM32功能强大、性能优异、片上资源丰富、功耗低,是一款经典的嵌入式微控制器
CoreMark是内核跑分,分数越高芯片性能越强。常用的F1系列主频是72MHz。
ARM
- ARM既指ARM公司,也指ARM处理器内核
- ARM公司是全球领先的半导体知识产权(IP)提供商,全世界超过95%的智能手机和平板电脑都采用ARM架构
- ARM公司只设计ARM内核而不生产实物,半导体厂商完善内核周边电路并生产芯片
Cortex这三个系列加起来正好构成A R M三个字母,可见这个命名方式还是很有讲究的。
我们主要学习的就是STM32的外设,通过程序配置外设,来完成我们想要的功能。
- NVIC:内核里面用于管理中断的设备,比如配置中断优先级这些东西
- SysTick:内核里面的定时器,主要用来给操作系统提供定时服务的,STM32是可以加入操作系统的,比如FreeRTOS、UCOS等。如果用了这些操作系统,就需要SysTick提供定时来进行任务切换的功能。也可以用这个定时器来完成Delay函数的功能
- RCC:可以对系统的时钟进行配置,还有就是使能各模块的时钟。在STM32中,其他(非内核)外设在上电的情况下默认是没有时钟的,不给时钟操作外设是无效的,目的是降低功耗。所以在操作外设前,必须要先使能时钟,这就需要用RCC来完成时钟的使能
- AFIO:可以完成复用功能端口的重定义,还有中断端口的配置
- EXTI:配置好外部中断后,当引脚有电平变化时,就可以触发中断,让CPU来处理任务
- TIM:整个STM32最常用、功能最多的外设。分为高级定时器、通用定时器、基本定时器
- ADC:STM32内置了12位的AD转换器,可以直接读取IO口的模拟电压值,无需外部连接AD芯片,使用非常方便
- DMA:帮助CPU完成搬运大量数据这样的繁杂工作
- RTC:在STM32内部完成年月日、时分秒的计时功能,而且可以接外部备用电池,即使掉电也能正常运行
- CRC:一种数据的校验方式,用于判断数据的准确性,有了这个外设的支持,进行CRC校验就会更加方便一些
- PWR:可以让芯片进入睡眠模式等状态,来达到省电的目的
- BKP:是一段存储器,当系统掉电时,仍可由备用电池保持数据,可以根据需要完成一些特殊功能
- WDG:当单片机因为电磁干扰死机或者程序设计不合理出现死循环时,看门狗可以及时复位芯片,保证系统的稳定
- DAC:它可以在IO口直接输出模拟电压,是ADC模数转换的逆过程
- FSMC:可以用于扩展内存,或者配置成其他总线协议,用于某些硬件的操作
- USB OTG:用OTG功能可以让STM32作为USB主机去读取其他USB设备
系统结构
-
三个总线icode指令总线(加载程序指令)、dcode数据总线(加载数据,比如常量何调试数据)、system系统总线。icode与dcode总线主要用来连接flash闪存(flasd存储的是编写的程序)。
-
ICode((Instruction)指令总线):
程序编译后的指令存放在内部FLASH中,M3内核通过ICode总线取指,然后再执行指令。 -
DCode((Data)数据总线):
程序有常量和变量。const修饰的变量为常量存储在内部FLASH中,变量不管是全局变量还是局部变量都存放在SRAM中。由于数据可以被DCode和DMA总线访问,所以就需要经过总线矩阵来仲裁。 -
Systme(系统总线):
系统总线主要用来访问外设寄存器(即读写寄存器就是通过该总线完成),比如SRAM(用于存储程序运行时的变量数据)。 -
存储器和寄存器映射:https://blog.csdn.net/weixin_58038211/article/details/128553364;https://blog.csdn.net/zywcxz/article/details/131035001
-
AHB(先进高性能总线)系统总线用于挂载主要的外设(挂载最基本或者性能比较高的外设,比如复位和时钟控制这些最基本的电路)SDIO也是挂载在AHB上的。
-
两个桥接,接到了APB1和APB2两个外设总线上(APB代表先进外设总线,用来连接一般的外设)。因为AHB和APB的总线协议、总线速度还有数据传输格式的差异,所以中间需要加两个桥接来完成数据的转换和缓存
-
AHB的整体性能比APB高一些,APB2的性能比APB1高一些。APB2一般和AHB同频率都是72MHz,APB1一般是36MHz,所以APB2连接的一般是外设中稍微重要的部分(例如GPIO端口,还有一些外设的一号选手比如USART1、SPI1、TIM1、TIM8(高级定时器)、ADC、EXTI、AFIO),APB1连接次要一点的外设2、3、4号外设还有DAC/PWR/BKP等,在实际使用中我们感觉不到APB1和APB2的性能差异,只需要知道外设是挂载到哪个总线上的就可以了。
-
DMA是内核CPU的小秘书,比如一些大量的数据搬运这样简单且重复干的事情,让cpu来干会浪费时间。DMA通过DMA总线连接到总线矩阵上,可以拥有和cpu一样的总线控制权,用于访问外设小弟,当需要DMA搬运数据时,外设就会通过请求线发送DMA请求,然后DMA就会获得总线控制权,访问并转运数据,整个过程不需要cpu的参与,省下CPU的时间来干其他的事情。
引脚定义
左上角有个小黑点,代表它左边的引脚是1号引脚,然后逆时针依次排列,直到48号引脚。
- 标红色的是电源相关的引脚
- 标蓝色的是最小系统相关的引脚
- 标绿色的是IO口、功能口这些引脚
第三列类型:S代表电源、I代表输入、O代表输出、I/O代表输入输出。
第四列IO口电平:表示IO口所能容忍的电压,FT代表能容忍5V电压,没有FT的只能容忍3.3V电压,如果没有FT的需要接5V的电平,就需要加装电平转换电路了。
如果我们想让STM32正常工作,首先就需要把电源部分和最小系统部分的电路连接好,也就是上表中标注红色和蓝色的部分。
主功能就是上电后默认的功能,一般和引脚名称相同。如果不同的话引脚的实际功能是主功能而不是引脚名称的功能。
默认复用功能,是IO口上同时连接的外设功能引脚,就是片上外设的端口和GPIO的连接关系,配置IO口时可以选择是通用IO口还是复用功能。
重定义功能,作用是如果有两个功能同时复用在了一个IO口上,而且确实需要用到这两个功能,可以将其中一个复用功能重映射到其他端口上(前提是,这个重定义功能的表里有对应的端口)
详细引脚介绍:
- 优先使用加粗的IO口,没有加粗的IO口可能需要进行配置或者兼具其他功能。
- 1引脚VBAT是备用电池供电引脚,可接3v电池,当系统电源断电时,备用电池可给内部的RTC时钟和备份寄存器提供电源。
- 2引脚是IO口或侵入检测或RTC,IO口可以根据程序输出或读取高低电平。侵入检测可以用来做安全保障的功能(比如你的产品安全性比较高,可以在外壳加一些防拆的触点,然后接上电路到这个引脚上,若有人强行拆开设备,则触点断开,这个引脚的电平变化就会触发STM32的侵入信号,然后就会清空数据来保证安全)。RTC的引脚可以用来输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
- 3、4引脚是IO口或者接32.768KHz的RTC晶振
- 5、6号引脚接系统的主晶振,一般是8MHz,然后芯片内有锁相环电路,可以对这个8MHz的频率进行倍频,最终产生72Mhz的频率作为系统的主时钟
- 7引脚NRST是系统复位引脚,N代表是低电平复位
- 8、9引脚是内部模拟部分的电源,比如ADC、RC振荡器等。VSS是负极,接GND,VDD是正级,接3.3V
- 10-19号引脚都是IO口,其中PA0还兼具了WKUP功能(可以用于唤醒处于待机模式的STM32)
- 20号引脚是IO口或BOOT1引脚,BOOT引脚是用来配置启动模式的
- 21、22引脚是IO口
- 23、24引脚是VSS_1和VDD_1是系统的主电源口,同样的VSS是负极,VDD是正级
- 下面的VSS_2和VDD_2以及VSS_3和VDD_3都是系统的主电源口,这里STM32内部采用分区供电的方式(分区供电的原因:https://archie.blog.csdn.net/article/details/135586216?spm=1001.2014.3001.5502),所以供电口比较多,在使用时,把VSS都接GND,VDD都接3.3V即可
- 25-33引脚都是IO口
- 34-40引脚再加27号引脚,都是IO口或者调试端口,默认功能是调试端口(用来调试程序和下载数据),这个STM32支持SWD(需要两根线,分别是SWDIO和SWCLK)和JTAG(需要五根线,分别是JTMS、JTCK、JTDI、JTDO、NJTRST)两种调试方式。
- 教程使用的是STLINK下载调试程序,属于SWD方式(只需占用PA13和PA14这两个IO口,剩下的PA15、PB3、PB4可以切换为普通的IO口使用(不过需在程序中配置,不配置的话默认是不会用作IO口的)
- 41、42、43、45、46引脚都是IO口
- 44引脚BOOT0和BOOT1一样用来做启动配置
启动配置
启动配置的作用是指定程序开始运行的位置,一般情况下,程序都是在Flash程序存储器开始执行(第一种启动模式)。但是在某些情况下,我们也可以让程序在别的地方开始执行。
第二种启动模式(串口下载用的,区别于使用Jlink):系统存储器存的就是STM32中的一段BootLoader程序,BootLoader的作用就是接收串口的数据,然后刷新到主闪存中(最后再跳转到第一种位置处运行)。
第三种启动模式:主要用来程序调试的,用的比较少。
BOOT引脚的值是在上电复位后的一瞬间有效的,之后就随便了。查看上面的引脚分布图,发现BOOT1和PB2是在同一个引脚上,也就是在上电瞬间是BOOT1功能,在第四个时钟过后,就是PB2的功能了。
最小系统
如果想让STM32正常工作,首先就需要把电源和最小系统部分的电路连接好。也就是前面表格中标红色和蓝色的部分。
供电部分电路:
右边三个分区供电的主电源和模拟部分电源都连接到了供电引脚,VSS都连接了GND,VDD都连接了3,3V,在3.3V和GND之间,一般都会连接一个滤波电容,保证供电电压的稳定。
左上脚VBAT接的备用电池(纽扣电池),用来给RTC和备份寄存器服务的。如果不用备用电池,VBAT可以直接接3.3V或者悬空。
STM32的供电还是比较多的,而且芯片四周都有供电引脚,这个要是自己画板子的话,就会深有体会,走线比较头疼。
晶振电路:
接了一个8MHz的主时钟晶振,经过内部锁相环倍频,得到72MHz的主频。晶振连接到STM32的5、6号引脚。另外还需要接两个20pF的电容,作为起振电容,电容的另一端接地即可。
如果需要RTC功能,还需要再接一个32.768KHz的晶振,电路和这个一样接到3、4号引脚。OSC32就是32.768KHz晶振的意思。为什么要用32.768KHz?因为32768是2的15次方,内部RTC电路经过2的15次方分频,就可以生成1S的时间信号了。
复位电路:
这个复位电路是一个10k的电阻和0.1uF的电容组成的,用来给单片机提供复位信号。NRST接到STM32的7号引脚,NRST是低电平复位的,当这个复位电路在上电的瞬间,电容是没有电的,电源通过电阻开始向电容充电,并且此时电容呈现的是短路状态,NRST就会产生低电平,当电容逐渐充满电时,电容就相当于断路,此时、NRST就会被R1上拉为高电平。那上电瞬间的波形就是先低电平,然后逐渐高电平,这个低电平就可以提供STM32的上电复位信号。当然电容充电还是非常快的,所以在我们看来单片机在上电的一瞬间复位了,这就是复位电路的作用。
电容左边还并联了一个按键,提供手动复位的功能。按键按下时,电容被放电,并且NRST引脚也通过按键被直接接地了,相当于手动产生了低电平复位信号。按键松手后,NRST又回归高电平,此时单片机就从复位状态转为工作状态。一般复位按键都是在一个小孔里,拿针戳一下设备就复位了。
启动配置:
跳线帽的方式,接拨码开关也可以。
2、软件安装、新建工程
安装芯片支持包:keil5之后需要安装支持包,因为芯片种类越来越多了,不可能兼容所有芯片,否则软件变得很臃肿,所以开发哪种芯片安装对应的支持包。
两种安装方式:离线安装(推荐,双击安装包即可安装)、在线安装(下载速度慢)。
库函数底层也是操作寄存器,只是封装了一下,方便我们使用。详细函数都在各个外设寄存器头文件里定义好了,可以去这些头文件里查看各接口的使用方法(入参、出参),用多了掌握套路就容易了,这样就不用查看寄存器和芯片手册了。
3、GPIO
片内外设、片上外设和片外外设的区别
GPIO简介
GPIO(General Purpose Input Output)通用输入输出口。
可配置为8种输入输出模式。
引脚电平:0V~3.3V,部分引脚可容忍5V。(0v就是低电平是数据0,3.3v是高电平是数据1。容忍5v意思是可以在这个端口输入5v的点电压,也认为是高电平,但是对于输出而言,最大就只能输出3.3v,因为供电就只有3.3v,具体哪些端口能容忍5v,可以参考一下stm32的引脚定义,带FT的就是可以容忍5v,不带FT的就只能接入3.3v电压)
输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。(后面文章显示的LED和蜂鸣器的程序现象,就使用到了GPIO的输出模式。另外在其他的应用场景,只要是可以用高低电平来进行控制地方都可以用GPIO来完成;如果控制的是功率比较大的设备,只需要再加入驱动电路即可;此外,还可以用GPIO来模拟通信协议,比如I2C、SPI或某个芯片特定协议,我们都可以用GPIO的输出模式来模拟其中的输出时序部分)
输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。(输入模式最常见的就是读取按键了,用来捕获我们的案件按下事件;另外,也可以读取带有数字输出的一些模块,比如,光敏电阻模块、热敏电阻模块等;如果这个模块输出的是模拟量,那GPIO还可以配置成模拟输入模式,再配合内部的ADC外设,就能读取端口的模拟电压了;除此之外,模拟通信协议时,接收通信线上的通信数据,也是靠GPIO的输入来完成的)
GPIO的基本结构
如下,为GPIO的整体构造,其中左边的是APB2外设总线;在stm32中所有的GPIO都是挂载在APB2外设总线上的,其中GPIO外设的名称都是按照GPIOA、GPIOB等等这样来命名的,每个GPIO外设,总共有16个引脚,编号是从0到15,GPIO的第0号引脚,我们一般把它称为PA0,接着第一号就是PA1…PA15以此来命名;
在每个GPIO模块内,主要包含了寄存器和驱动器,寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了,寄存器的每一位对应一个引脚,其中,输出寄存器写1,对应的引脚就会输出高电平,写0就会输出低电平,输入寄存器读取为1,就证明对应的端口目前是高电平,读取为0,就是低电平;
因为STM32是32位单片机,所以STM32内部的寄存器都是32位的,但这个端口只有16位,所以这个寄存器只有低16位对应的有端口,高16位是没有用到的;
驱动器是用来增加信号的驱动能力,寄存器只负责存储数据,如果要进行点灯这样的操作,还是需要驱动器来负责增大驱动能力。
如下,这些就是GPIO的整体基本结构了。
GPIO位结构(每一位的具体电路结构)
如下图为,stm32参考手册中的GPIO位结构的电路图。
左边三个就是寄存器,中间部分是驱动器,右边是某一个IO口的引脚。
整体结构可以分为两个部分,上面是输入部分,下面是输出部分。
一、输入部分
1.首先是这个IO引脚,这里接了两个保护二极管,这个是对输入电压进行限幅的,上面二极管接VDD,3.3V,下面二极管接VSS,0V;如果输入电压比3.3v还要高,那上方这个二极管就会导通,输入电压产生的电流就会直接流入VDD而不会流入内部电路,这样就可以避免过高的电压对内部电路产生伤害。
如果输入电压比0v还要低,这个电压是相对与VSS的电压,所以是可以有负电压的,那这时下方这个二极管就会导通,电流会从VSS直接流出来,电流会从VSS直接流出去,而不会从内部电路汲取电流,也是可以保护内部电路的。
如果输入电压在0-3.3v之间,那两个保护二极管均不会导通,这时二极管对电路没有影响,这就是保护二极管的用途。
2.上拉和下拉电阻
上拉和下拉的作用:是为了给输入提供一个默认的输入电平,因为对应一个数字的端口,输入不是高电平就是低电平;如果输入引脚哈都不接,这时输入就会处于一个浮空状态,引脚的输入电平极易受外界干扰而改变;为了避免引脚悬空导致的输入数据不稳定,我们就需要在这里加上上拉或下拉电阻。
上拉电阻至VDD,下拉电阻至VSS,这个开关是可以通过程序进行配置的。
上面导通、下面断开,就是上拉输入模式;上面断开、下面导通,就是下拉输入模式;上面断开、下面断开,就是浮空输入模式。
如果接入上拉电阻,当引脚悬空时,还有上拉电阻来保证引脚的高电平,所以上拉输入是默认为高电平的输入模式,下拉也是同理,默认为低电平的输入方式。
上拉电阻和下拉电阻的阻值都是比较大的,是一种弱上拉和弱下拉 ,目的是尽量不影响正常的输入操作。
3.TTL肖特基触发器
实际上这个应该是施密特触发器(应该是一个翻译错误)。如下:
施密特触发器的作用就是对输入电压进行整形的,它的执行逻辑是,如果输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平,这样可以有效的避免由于信号波动造成的输出抖动现象。
例子:因为这个引脚的波形是外界输入的(IO口输入),虽然是数字信号,实际情况可能会产生各种失真,比如,如下波形夹杂了波动的高低变化的电平信号(下图,红色线),如果没有施密特触发器,那很有可能因为干扰而导致误判,如果有了施密特触发器,那比如定一个阈值上限和下限(下图中绿色线),高于上限输出高,低于下限输出低,如下图蓝色为施密特信号,图中的第一个蓝色圈虽然由于波动再次低于上限了,但是对于施密特触发器来说,只有高于上限或者低于下限,输出才会变化,所以此时低于上限的情况,输出并不会变化,而是继续维持高电平,然后直到下次低于下限时,才会转为低电平,第二个蓝色圈信号即使在下限附近来回横跳,因为没有跳到上限上面去,所以输出仍然是稳定的,直到下一次高于上限,输出才会变成高电平,如下蓝色线就是施密特触发器的输出信号了,可以看到,相比较输入信号,经过整形的信号就很完美。在这里使用了两个比较阈值来进行判断,中间留有一定的变化范围(上下绿色阈值线),可以有效的避免因信号波动造成的输出抖动现象。
施密特前(右)是模拟量,后(左)是01组成的数字量
接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平了。
最后上面这还有两路线路,这些就是连接到片上外设的一些端口,其中有模拟输入,这个是连接到ADC上的,因为ADC需要接收模拟量,所以这根线是接到施密特触发器前面的;另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面。
二、输出部分
1、输出部分可以由输出数据寄存器或片上外设控制,两种控制方式通过这个数据选择器(输出控制左侧梯形)接到输出控制部分。
如果选择通过输出数据寄存器进行控制,就是普通的IO口输出,写这个输出数据寄存器的某一位就可以操作对应的某个端口了。
2、最左侧是位设置/清除寄存器:这个可以用来单独操作输出数据寄存器的某一位,而不影响其它位。因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的操作方式。
- 第一种方式是先读出这个寄存器,然后用 按位与 和 按位或 的方式更改某一位,最后再将更改后的数据写回去,在C语言中就是&=和 |=的操作,这种方法比较麻烦,效率不高,对于IO口的操作而言不太合适;
- 第二种方式是通过设置这个位设置和位清除寄存器,如果我们要对某一位进行置1的操作,在位设置寄存器的对应位写1便可,剩下不需要操作的位写0,这样它内部就会有电路,自动将输出数据寄存器中对应位置为1,而剩下写0的位则保持不变,这样就保证了只操作其中某一位而不影响其它位,并且这是一步到位的操作。如果想对某一位进行清0的操作,就在位清除寄存器的对应位写1即可,这样内部电路就会把这一位清0了,这就是第二种方式也就是这个位设置和位清除寄存器的作用。【作用:将设置/清除寄存器的某一位写1/0就能达到单独影响输出寄存器的某一位,从而单独影响某个端口】
- 第三种操作方式【了解即可】 ,就是读写STM32中的“位带”区域,这个位带的作用就跟51单片机的位寻址作用差不多,在STM32中,专门分配的有一段地址区域,这段地址映射了RAM和外设寄存器所有的位,读写这段地址中的数据,就相当于读写所映射位置的某一位,这就是位带的操作方式,这个方式我们本课程暂时不会用到。我们的教程主要使用的是库函数来操作的,库函数使用的是读写位设置和位清除寄存器的方法。
3.输出控制之后就接到了两个MOS管
上面是P-MOS,下面是N-MOS,这个MOS管就是一种电子开关,我们的信号来控制开关的导通和关闭,开关负责将IO口接到VDD或者VSS,这里可以选择推挽、开漏或关闭三种输出方式。
-
推挽输出模式
在推挽输出模式下,P-MOS和N-MOS均有效,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平,数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,就是输出低电平,这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推输出模式。在推挽输出模式下,STM32对IO口具有绝对的控制权,高低电平都由STM32说的算。 -
开漏输出模式
在开漏输出模式下,上面P-MOS是无效的,只有下面N-MOS在工作,数据寄存器为1时,下管断开,这时输出相当于断开,也就是高阻模式;数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平;这种模式下,只有低电平有驱动能力,高电平是没有驱动能力的。那这个模式有什么用呢,这个开漏模式可以作为通信协议的驱动方式,比如12C通信的引脚,就是使用的开漏模式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰,另外开漏模式还可以用于输出5V的电平信号。
比如在IO口外接一个上拉电阻到5V的电源,开漏模式下,输出1时,两个mos管都相当于关断,左侧相当于断路(高阻模式),外接5V的电能只能流向右侧,故输出5V(用于兼容一些5V电平的设备,这就是开漏输出的主要用途)。反之,输出0时,左下方mos管导通,外接5V的电能流到左下方Vss,且两者之间几乎没有电压降,可看做5V电压降在了上拉电阻上,故引脚输出0V。 -
关闭状态输出方式
剩下的一种状态就是关闭,这个是当引脚配置为输入模式的时候,这两个MOS管都无效,也就是输出关闭,端口的电平由外部信号来控制。
GPIO8种工作模式
通过配置GPIO的端口配置寄存器,上面的位结构的电路就会根据我们的配置进行改变(比如,开关的通断、N-MOS和P-MOS是否有效、数据选择器的选择等),端口可以配置成以下8种模式:
1.首先是前三个,浮空输入、上拉输入、下拉输入
这三个模式的电路结构基本是一样的,区别就是上拉电阻和下拉电阻的连接,它们都属于数字的输入口,那特征就是,都可以读取端口的高低电平,当引脚悬空时,上拉输入默认是高电平,下拉输入默认是低电平,而浮空输入的电平是不确定的,所以在使用浮空输入时,端口—定要接上一个连续的驱动源,不能出现悬空的状态。
那我们来看一下这三种模式的电路结构,这里可以看到,在输入模式下,输出驱动器是断开的,端口只能输入而不能输出,上面这两个电阻可以选择为上拉工作、下拉工作或者都不工作,对应的就是上拉输入、下拉输入和浮空输入,然后输入通过施密特触发器进行波形整形后,连接到输入数据寄存器。
另外右边这个输入保护这里,上面写的是VDD或者VDD_FT,这就是3.3V端口和容忍5V端口的区别。这个容忍5V的引脚,它的上边保护二极管要做一下处理,要不然这里直接接VDD 3.3V的话,外部再接入5V电压就会导致上边二极管开启,并且产生比较大的电流,这个是不太妥当的。
- 模拟输入
这个模拟输入可以说是ADC模数转换器的专属配置了,特征是GPIO无效,引脚直接接入内部ADC。
这里输出是断开的,输入的施密特触发器也是关闭的无效状态,所以整个GPIO的这些都是没用的,那么只剩下从引脚直接接入片上外设,也就是ADC,所以,当我们使用ADC的时候,将引脚配置为模拟输入就行了,其他时候,一般用不到模拟输入。
3.开漏输出和推挽输出
开漏输出和推挽输出,这两个电路结构也基本一样,都是数字输出端口,可以用于输出高低电平,区别就是开漏输出的高电平呈现的是高阻态,没有驱动能力,而推挽输出的高低电平都是具有驱动能力的。
输出是由输出数据寄存器控制的,如果P-MOS无效,就是开漏输出;如果P-MOS和N-MOS都有效,就是推挽输出。另外我们还可以看到,在输出模式下,输入模式也是有效的,但是在我们刚才的电路图,在所有输入模式下,输出都是无效的,这是因为,一个端口只能有一个输出,但可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入一下,这个也是没啥影响的。
4.复用开漏输出和复用推挽输出
最后我们再来看一下复用开漏输出和复用推挽输出,这俩模式跟普通的开漏输出和推挽输出也差不多。
可以看到通用的输出/数据寄存器没有连接的,引脚的控制权转移到了片上外设,由片上外设来控制,在输入部分,片上外设也可以读取引脚的电平,同时普通的输入也是有效的,顺便接收一下电平信号。
在GPIO的这8种模式中,除了模拟输入这个模式会关闭数字的输入功能,在其他的7个模式中,所有的输入都是有效的。
参考手册
当我们使用这些片上外设的引脚时,可以参考这个表里给的配置
相关寄存器,首先是GPIO配置寄存器,每一个端口的模式由4位进行配置,16个端口就需要64位,所以这里的配置寄存器有两个,一个是端口配置低寄存器,一个是端口配置高寄存器,具体怎么配置的可以看详细的位介绍。GPIO的输出速度可以限制输出引脚的最大翻转速度,这个设计出来,是为了低功耗和稳定性的,我们一般要求不高的时候直接配置成50MHz就可以了。
如下为,端口输入数据寄存器。
就是上面GPIO位结构的输入数据寄存器,里面的低16位对应16个引脚,高16位没有使用
如下为,端口输出数据寄存器,也就是上面GPIO位结构的输出数据寄存器,同样,低16位对应16个引脚,高16位没有使用
如下为,端口位设置/清除寄存器,也就是上面GPIO位结构的那部分寄存器,这个寄存器的低16位是进行位设置的,高16位是进行位清除的。写1就是设置或者清除,写0就是不产生影响。
如下为,端口位清除寄存器,这个寄存器的低16位和上面的寄存器功能是一样的,进行位清除的。
为啥还要有这个寄存器呢,这个是为方便操作设置的,如果只想单一的进行设置或者位清除,位设置用上面寄存器,位清除用下面这个寄存器,因为在设置和清除时,使用的都是低16位的数据,这样就方便一些;如果想对多个端口同时进行位设置和位清除,那就使用第一个寄存器就行了,这样可以保证位设置和位清除的同步性,当然你要对信号的同步性要求不高的话,先位设置再位清除也是没问题的
如下为,端口配置锁定寄存器。
这个可以对端口的配置进行锁定,防止意外更改,使用方法看介绍,这个我们暂时用的不多。
目前,有关stm32内部的GPIO外设,我们就讲完了
接下来,我们看一下stm32外部的设备和电路。
LED和蜂鸣器介绍
LED电路符号如下,左边是正级,右边是负极、
如下为LED实物图,如果引脚没有剪过,长脚为正极,短脚为负极 。通过LED内部也可以看正负极,较小的一半是正极,较大的一半是负极
无源蜂鸣器需要不断反转IO口。
有源蜂鸣器内部电路如下左图,这里用了一个三极管开关进行驱动,我们将VCC和GND分别接上正负极的供电,然后中间引脚2接低电平,蜂鸣器就会响,接高电平,蜂鸣器就关闭,
LED和蜂鸣器的硬件电路
1.如下两个图是使用stm32的GPIO口驱动LED的电路。
下图是低电平驱动的电路,LED正极接3.3v,负极通过一个限流电阻接到PA0上,当PA0输出低电平时,LED两端就会产生电压差,就会形成正向导通的电流,这样LED就会点亮了;当PA0输出高电平时,因为LED两端都是3.3v的电压,不会形成电流,所以高电平LED就会熄灭。
这里的限流电阻一般都是要接的,一方面它可以防止LED因为电流过大而烧毁,另一方面它可以调整LED的亮度,如果你觉得LED太亮可以适当的增大限流电阻的阻值。
下图是高电平驱动的电路。LED负极接到GND,正极通过一个限流电阻接到PA0上,这时就是高电平点亮,低电平熄灭。
针对选择电平驱动哪个方式:就得看IO口高低电平的驱动能力如何了,上面讲到,GPIO的推挽输出模式下,高低电平均有较强的驱动能力,所以两种方式都可以;在单片机的电路里,一般倾向使用第一种,低电平驱动的方式,因为很多单片机或者芯片,都使用了高电平弱驱动,低电平强驱动的规则,这样可以一定程度上避免高低电平打架,所以使用高电平驱动能力弱那就不能使用第二种连接方式了。
2.下面为蜂鸣器电路
这里使用了三极管开关的驱动方案,三极管开关是最简单的驱动电路了,对于功率稍微大一点的 ,直接用IO口驱动会导致STM32负担过重,这时可以用一个三极管驱动电路来完成驱动任务。
下图为PNP三极管的驱动电路,三极管的左边是基极,带箭头的是发射极,剩下的是集电极。左边的基极给低电平,三极管就会导通,再通过3.3V和GND就可以给蜂鸣器提供驱动电流了。基极给高电平,三极管截止,蜂鸣器没有电流。
下图为NPN三极管的驱动电路,同样,左边是基极,带箭头的是发射极,剩下的是集电极;它的驱动逻辑和上面的是相反的,基极给高电平导通,低电平断开。
需要注意,PNP的三极管最好接在上边,NPN的三极管最好接到下边,这是因为三极管的通断是需要在发射极和基极产生一定的开启电压的,如果将负载接在发射极这边,可能会导致三极管不能开启。
面包板的使用方法
当我们把原件的引脚插到面包板的孔里时,它内部的金属爪就会抓住引脚;
金属爪的排列规律是:中间的金属爪是竖着放的,上下四排是连在一个的四个整体的金属爪。那就对应这个面包板的孔的连接关系。竖着的五个孔内部是连接在一起的,如下,这样我们元件插在一纵排的不同孔位时,内部的金素爪就实现了线路的连接。
上下四排孔整体是连在一起的,这四排是用于供电的,标有正负极;如果我们需要供电,就从上下的孔位中,用跳线印出来即可。另外,再说明一下,这个供电的引脚,有的面包板并不是一整排都是连接的(如果中间是断开的,用跳线再连接起来)
演示:若用面包板实现电源点亮一个LED等的电路
首先,把上面两排的供电引脚接上电源的正负极,然后用跳线将正极引下来到一个孔(5孔其1)里,然后在纵向下面的孔,横着插一个限流电阻到右边的孔,横着插一个LED到右边的孔,然后再用跳线把右边引到负极。这样就可以了。
面包板正面如下:
面包板背面(金属爪)
金属爪示意图
GPIO输出实验:LED闪烁、LED流水灯、蜂鸣器编程实验
外设的GPIO配置查看
STM32F10xxx参考手册 P110有列出了各个外设的引脚配置,例如:
实战1: 如何进行基本的GPIO输入输出
操作STM32的GPIO总共需要3个步骤(涉及到RCC和GPIO两个外设):
第一步,使用RCC开启GPIO的时钟
涉及的函数如下:
void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
作用:使能(开启)或失能(关闭)APB2外设时钟
参数说明(右击跳转到.c文件的函数定义处,查看函数参数的注释说明):
其它两个外设时钟函数也是大差不差的,根据不同外设选择相应的函数开启就行。
第二步,使用GPIO_Init函数初始化GPIO
涉及的函数如下:
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
作用:根据GPIO_InitStruct结构体中的指定参数初始化GPIOx外设。
参数说明:
指定要配置的GPIO引脚。
其中 GPIO InitTypeDef结构体配置信息如下:
typedef struct
{
uint16_t GPIO_Pin;
GPIOSpeed_TypeDef GPIO_Speed;
GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;
参数说明:
引脚的工作模式如下:
举例:根据LED闪烁接线图设置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
第三步,使用输出或者输入的函数控制GPIO口
涉及的函数如下:
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:设置所选数据端口位。对某个端口写1,也就是将指定端口设置成高电平。
参数说明:
类似的还有:GPIO_ResetBits 函数,同样的用法,只不过这个函数是写0,将指定端口设置成低电平。
3-1.LED闪烁
接线图(LED的短脚负极接到PA0引脚):
低电平点亮的操作方式,为了方便就没有接限流电阻。
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main (void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStructure; //定义结构体
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure); //GPIO配置初始化
//GPIO_SetBits(GPIOA,GPIO_Pin_0);
while(1)
{
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);// 低电平
Delay_ms(500);// SysTick定时器实现的
GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);// 高电平
Delay_ms(500);
//下面这个带s说明可以同时设置多个引脚,通过按位或实现
GPIO_ResetBits(GPIOA,GPIO_Pin_0);// 低电平
Delay_ms(500);
GPIO_SetBits(GPIOA,GPIO_Pin_0); // 高电平
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);// 第三个参数强制类型转换成枚举型,否则编译有警告
Delay_ms(500);
GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);
Delay_ms(500);
}
}
视频还介绍了一个脚本工具keilkill.bat,双击运行用于删除编译过程产生的中间文件,这样20M减少到2M,不过这样操作就不能右键跳转到.c文件的函数定义了。
3-2.LED流水灯
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//开启时钟
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
// GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
// |或运算
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_All;//选中所有16个引脚
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStruct);
while(1)
{
// 查看函数实现,发现第二个参数值直接写到ODR寄存器里
GPIO_Write(GPIOA,~0x0001);// 0000 0000 0000 0001 取反前~
Delay_ms(500);
GPIO_Write(GPIOA,~0x0002);// 0000 0000 0000 0010
Delay_ms(500);
GPIO_Write(GPIOA,~0x0004);// 0000 0000 0000 0100
Delay_ms(500);
GPIO_Write(GPIOA,~0x0008);// 0000 0000 0000 1000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0010);// 0000 0000 0001 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0020);// 0000 0000 0010 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0040);// 0000 0000 0100 0000
Delay_ms(500);
GPIO_Write(GPIOA,~0x0080);// 0000 0000 1000 0000
Delay_ms(500);
}
}
3-3.蜂鸣器
PB12输出低电平,蜂鸣器就会响,输出高电平,蜂鸣器就不响。
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void)
{
//RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_Initstruct;
GPIO_Initstruct.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Initstruct.GPIO_Pin = GPIO_Pin_12;
GPIO_Initstruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_Initstruct);
while(1)
{
// GPIO_ResetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_SetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_ResetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(100);
// GPIO_SetBits(GPIOB,GPIO_Pin_12);
// Delay_ms(700);
}
}
前面是通过查看.h和.c文件了解库函数使用方法,还可以通过查看固件库函数用户手册的方式,以及百度方式。
GPIO输入
按键简介
按键:最常见的输入设备,按下导通,松手断开
按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。5-10ms的抖动对于人来说感觉不到,但对于高速运行的单片机来说就比较漫长了,单片机会感知到这段时间的电平抖动,消抖最常用的方法就是延时等待一段时间,耗过这段抖动时间。
传感器模块简介
传感器模块:传感器元件(传感器模块就是利用传感器元件,比如如下图的光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化(比如光线越强,光敏电阻的阻值就越小),通过与定值电阻进行串联分压即可得到模拟电压输出,再通过电压比较器进行二值化(二值化就是要么是高要么是低,就是滤波)即可得到数字电压输出。
如下为传感器模块的基本电路,详细介绍。
这个N1就是传感器元件所代表的可变电阻,它的阻值可以根据环境的光线、温度等模拟两进行变化。
对于光敏电阻传感器来说,这个N1就是光敏电阻;对于热敏电阻传感器来说,这个N1就是热敏电阻;对应这个红外传感器来说,这个N1就是一个红外接收管,对应还有点亮红外发射管的电路,发射管发射红外光,接收管接收红外光,模拟电压就表示的是接收光的强度。
N1上面的R1,是和N1进行分压的定值电阻,R1和N1串联,一端接VCC一端接VSS,这就构成了基本的分压电路,AO电压就由R1和N1两个电阻的分压得到。
N1左边的C2是一个滤波电容,它是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压波形的平滑。一般我们在电路里遇到这种一端接在电路中,另一端接地的电容都可以考虑一下这个是不是滤波电容的作用,如果是滤波电容的作用,那这个电容就是用来保证电路稳定的。并不是电路的主要框架,这时候我们在分析电路的时候,就可以先把这个电容给抹掉,这样就可以使我们的电路分析更加简单。
那我们把这个电容抹掉,整个电路的主要框架就是定值电阻和传感器电阻的分压电路了。在这里可以用分压定理来分析一下传感器电阻的阻值变化对输出电压的影响,当然我们还可以用上下拉电阻的思维来分析,当这个N1阻值变小时,下拉作用就会增强,中间的AO端的电压就会拉低,极端情况下,N1阻值为0,AO输出被完全下拉,输出0V;当N1阻值变大,下拉作用就会减弱,中间的引脚由于R1的上拉作用,电压就会升高极端情况下,N1阻值无穷大,相当于断路,输出电压被R1拉高至VCC。
AO这个输出端可以把它想象成一个水平杆子(下图红色直线),R1上拉电阻相当于拴在上方的弹簧,将杆子向上拉,N1下拉电阻相当于拴在地面的弹簧,将杆子向下拉;电阻的阻值越小,弹簧的拉力就越强,杆子的高度就相当于电路中的电压,杠子向拉力强的一端偏移(取决于两个弹簧的弹力之差);如果上下弹簧拉力一致,杆子处于居中位置也就是电路输出VCC/2的电压;如果上面的阻值小,拉力强,输出电压就会变高;反之下面的阻值小,输出电压就会变低 ;如果上下拉电阻的阻值都为0,就是两个无穷大的力在对抗,在电路中呈现的就是电源短路(应该避免)。单片机电路中会常出现这种上拉下拉电阻,比如弱上拉,强上拉等(强和弱就是指电阻阻值的大小,也就是这个弹簧拉力大小) ,最终输出电压就是在弹簧拉扯下最终杆子的高低。
在R1和N1电阻的分压下,AO就是我们想要的模拟电压输出了,所以这个AO电压就直接通过排针输出了。这就是AO电压的由来,仅需两个电阻分压即可得到。
那么接下来这个模块还支持数字输出,数字输出就是对AO进行二值化的输出。
二值化输出是通过这个LM393芯片来完成,这个LM393是一个电压比较器芯片,里面有两个独立的电压比较器电路,然后剩下的是VCC和GND供电,C1是电源供电的滤波电容。这个电压比较器其实就是一个运算放大器(作者在51单片机AD/DA章节讲过运算放大器)。
运算放大器当作比较器的情况:当这个同相输入端的电压大于反相输入端的电压时,输出就会瞬间升高为最大值也就是输出接VCC,反之当同相输入端的电压小于反相输入端的电压时,输出就会瞬间降低为最小值也就是输出接GND,这样就可以对一个模拟电压进行二值化了。
我们看一下实际的应用,这里同相输入端IN+接到了AO这里,就是模拟电压端,IN-呢,接了一个电位器,这个电位器的接法也是分压电阻的原理。
拧动电位器,IN-就会生成一个可调的阈值电压,两个电压(IN+IN-)进行比较,最终输出结果就是DO,数字电压输出,DO最终就接到了引脚的输出端,
这就是数字电压的由来,然后右边这里还有两个指示灯电路,左边的是电源指示灯,通电就亮;右边的是DO输出指示灯,它可以指示DO的输出电平,低电平点亮,高电平熄灭。那右边DO这里还多了个R5上拉电阻,这个是为了保证默认输出为高电平的。
按键和传感器硬件电路
一般来说我们用下接按键的方式,这个原因和LED的接法类似,是电路设计习惯和规范;下图是按键最常用的接法,按键按下时,PA0直接下拉到GND,此时读取PA0口的电压就是低电平,当按键松手时,PA0被悬空,这样PA0引脚电压不确定,所以在这种接法下,必须要求PA0是上拉输入模式,按键松下悬空,还是高电平。
下图,外部接了一个上拉电阻,当按键松手时,引脚由于上拉作用,保持为高电平,当按键按下时,引脚直接接到GND,也就是一股无穷大的力把这个引脚往下拉,那弹簧肯定对抗不了无穷大的力,所以引脚电平就为低电平(初中时候学的,电流走不带电阻的那条通路),这种状态下引脚不会出现悬空状态,所以此时PA0引脚就可以配置为浮空输入或者上拉输入。如果是上拉输入,那就是内外两个上拉电阻共同作用了,这是高电平就会更强一些,对应高电平就更加稳定。当然这样的话,当引脚被强行拉到低时,损耗也就会大一些。
上接按键的方式(仅了解)如下,要求将PA0必须配置成下拉输入模式,按键按下时引脚为高电平, 松手时引脚会回到默认值低电平。 这要求单片机的引脚可以配置为下拉输入的模式,一般单片机可能不一定有下拉输入的模式,所以最好还是用上面的接法。
下面这个外接一个下拉电阻,这个接法PA0需要配置为下拉输入模式或者浮空输入模式。
传感器模块电路
传感器模块电路如下,因为是使用模块的方案,所以电路还是非常简单的,DO是数字输出随便接一个端口,比如PA0,用于读取数字量。
AO模拟输出呢,我们之后学习ADC模数转换器的时候再用。
GPIO输入实验:按键控制LED&光敏传感器控制蜂鸣器
知识点:
上拉输入:若GPIO引脚配置为上拉输入模式,在默认情况下(GPIO引脚无输入),读取的GPIO引脚数据为1,即高电平。
下拉输入:若GPIO引脚配置为下拉输入模式,在默认情况下(GPIO引脚无输入),读取的GPIO引脚数据为0,即低电平。
按键控制LED
两个按键和两个LED,按键接到PB1和PB11两个口上,按键一端接GPIO口一端接地,LED接到PA1和PA2两个口上,LED一端接GPIO口一端接VCC,就是低电平点亮的接法。
LED驱动和按键驱动要单独放到各自.c和.h文件里,这就是模块化的编程思想。
驱动.c文件用来存放驱动程序的主体代码;驱动.h用来存放驱动程序可以对外提供的函数或变量声明。
.h文件要添加一个防止头文件重复包含的代码,格式固定,如下
#ifndef __LED_H //如果没有定义LED这个字符串
#define __LED_H //那么就定义这个字符串
//函数和变量声明放在这里
#endif //是和ifndef组成的括号
//空行结尾
//如下函数用于读取输入数据寄存器的某一位的值,返回值就是这个端口的高低电平
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//如下函数用于读取整个输入数据寄存器的值,返回值是一个16位的数据,每一位代表一个端口值
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
//如下函数用于输出模式下,读取输出数据寄存器的某一位,所以原则上来说并不是读取端口的输入数据的
//这个函数一般用于输出模式下,看一下自己输出的是什么
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
//如下函数,少了个bit,用来读取整个输出寄存器
uint8_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
程序代码如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h" //包含led的头文件
#include "key.h"
uint8_t keynum; //全局变量,用来存键码的返回值,与局部变量作用域不同
//局部变量只能在本函数使用,全局变量每个函数都可使用
//在函数里优先使用自己的局部变量,如果没有才会使用外部的全局变量
int main(void)
{
led_init(); //完成led的初始化,默认低电平
key_init(); //初始化按键
while(1)
{
keynum = key_getnum(); //不断读取键码值,放在keynum变量里
if(keynum == 1) //按键1按下
{
led1_turn(); //电平翻转,led状态取反,需用到GPIO_readoutput函数
}
if(keynum == 2)
{
led2_turn();
}
// led1_on();
// led2_off();
// Delay_ms(500);
// led1_off();
// led2_on();
// Delay_ms(500);
}
}
led.c
#include "stm32f10x.h" // Device header
void led_init(void)//初始化led
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE); //打开时钟
//赋值结构体
GPIO_InitTypeDef GPIO_InitStructA; //结构体变量名GPIO_InitStructA
GPIO_InitStructA.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructA.GPIO_Pin = GPIO_Pin_1 |GPIO_Pin_2; //按位或来选择多个引脚
GPIO_InitStructA.GPIO_Speed = GPIO_Speed_50MHz; //默认50mhz输出
GPIO_Init(GPIOA,&GPIO_InitStructA); //使用的是地址传递,将指定的GPIO外设初始化好
GPIO_SetBits(GPIOA,GPIO_Pin_1 | GPIO_Pin_2); //这样后,初始化led是高电平是熄灭的 带s的函数可同时操作多个IO
}
void led1_on(void) //点亮led1,就是pa1口
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1); //低电平点亮
}
void led1_off(void) //熄灭led1,就是pa1口
{
GPIO_SetBits(GPIOA,GPIO_Pin_1); //高电平熄灭
}
void led1_turn(void) //led1状态取反,电平翻转
//GPIO_ReadOutputDataBit这个函数,来读取端口输出的是什么
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_1); //状态取反,0变1
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_1);//状态取反,1变0
}
}
//下方雷同
void led2_on(void)
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
void led2_off(void)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
void led2_turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2) == 0)
{
GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
else
{
GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}
}
led.h
#ifndef __LED_H
#define __LED_H
void led_init(void); //对模块外部声明,这个函数是可以被外部调用的函数
void led1_on(void);
void led1_off(void);
void led2_on(void);
void led2_off(void);
void led1_turn(void);
void led2_turn(void);
#endif
key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
void key_init(void) //按键初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟
//配置端口模式
GPIO_InitTypeDef GPIO_InitStructB; //结构体变量名GPIO_InitStructB
GPIO_InitStructB.GPIO_Mode = GPIO_Mode_IPU; //需要上拉输入,按键未按时默认高电平
GPIO_InitStructB.GPIO_Pin = GPIO_Pin_1 |GPIO_Pin_11;
GPIO_InitStructB.GPIO_Speed = GPIO_Speed_50MHz;//在输入模式下,这个参数其实无用,无影响
GPIO_Init(GPIOB,&GPIO_InitStructB);
}
uint8_t key_getnum(void) //读取按键值的函数
{
uint8_t keynum = 0; //没有按下就返回0
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) //读取GPIO端口,返回值就是输入数据寄存器的某一位值,等于0代表低电平按键按下
{
Delay_ms(20); //按键按下消抖20ms
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0); // 死循环等待 直到松手
Delay_ms(20); //按键松手消抖
keynum = 1; //键码为1.传递出去
}
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)
{
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0);
Delay_ms(20);
keynum = 2;
}
return keynum;
}
key.h
#ifndef __KEY_H
#define __KEY_H
void key_init(void);
uint8_t key_getnum(void);
#endif
光敏传感器控制蜂鸣器
左边蜂鸣器模块(接PB12),右边光敏传感器模块(DO数字输出端,接PB13引脚)。
对于光敏传感器,遮住光线时输出指示灯灭(传感器自带的灯),代表输出高电平,反之输出指示灯亮,代表输出低电平,光敏传感器自带一个电位器,可以调节高低电平的判断阈值,前面传感器模块硬件电路有讲到。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "buzzer.h"
#include "lightsenoer.h"
int main(void)
{
buzzer_init(); //初始化蜂鸣器
lightsenoer_init(); //初始化光敏传感器
while(1)
{
if(lightsenoer_get() == 1) //光线暗,模块本身不亮指示灯
{
buzzer_off(); //关闭蜂鸣器
}
else
{
buzzer_on(); //否者,打开蜂鸣器
}
}
}
buzzer.c
#include "stm32f10x.h" // Device header
void buzzer_init(void) //蜂鸣器初始化
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启时钟
GPIO_InitTypeDef GPIO_InitStruct; //定义结构体变量
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12; //pa12端口
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);//传递地址
GPIO_SetBits(GPIOB,GPIO_Pin_12);//初始化为高电平,蜂鸣器不响
}
void buzzer_on(void)
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
void buzzer_off(void)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
void buzzer_turn(void)
{
if(GPIO_ReadOutputDataBit(GPIOB,GPIO_Pin_12) == 0)
{
GPIO_SetBits(GPIOB,GPIO_Pin_12);
}
else
{
GPIO_ResetBits(GPIOB,GPIO_Pin_12);
}
}
buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
void buzzer_init(void);
void buzzer_on(void);
void buzzer_off(void);
void buzzer_turn(void);
#endif
lightsenoer.c
#include "stm32f10x.h" // Device header
void lightsenoer_init(void) //光敏传感器初始化函数
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; //上拉输入,默认高电平状态; 若始终接在端口上,也可以选择浮空输入,只要保证引脚不会悬空即可
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13; //pb13端口
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB,&GPIO_InitStruct);
}
uint8_t lightsenoer_get(void) //返回端口值函数
{
return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}
lightsenoer.h
#ifndef __LIGHTSENOER_H
#define __LIGHTSENOER_H
void lightsenoer_init(void);
uint8_t lightsenoer_get(void);
#endif
4、OLED显示屏调试工具
试想一下,如果学习C语言不允许使用print这个打印函数,那就根本没法学,同样单片机如果没有任何可以显示的东西,那也没法学习单片机。所以程序调试很重要。
调试方式:
- 串口调试:通过串口通信,将调试信息发送到电脑端,电脑使用串口助手显示调试信息
- 显示屏调试:直接将显示屏连接到单片机,将调试信息打印在显示屏上(视频提供显示屏驱动函数模块,移植会调用即可,原理后续会介绍)
- Keil调试模式:借助Keil软件的调试模式,可使用单步运行、设置断点、查看寄存器及变量等功能
- 点灯调试法:在指定位置放一个点灯代码,运行到了,灯就亮
- 注释调试法:将新加入的程序全部注释,然后依次一行一行解除注释,直到错误出现
- 对照法:找到一个没有问题的程序,对照程序逻辑
总之,测试程序的基本思想就是:缩小范围、控制变量、对比测试等
OLED简介
OLED(Organic Light Emitting Diode):有机发光二极管(每一个像素都是一个单独的发光二极管,每一个像素都可以自发光,不像LCD需要有背光)
OLED显示屏:性能优异的新型显示屏,具有功耗低、响应速度快(刷新率高)、总线时序快避免阻塞我们的程序、宽视角(自发光,在任何角度看显示内容都是清晰的,手机都是OLED屏幕)、轻薄柔韧等特点
0.96寸OLED模块:小巧玲珑、占用接口少、简单易用,是电子设计中非常常见的显示屏模块
供电:3~5.5V,通信协议:I2C/SPI,分辨率:128*64
规格:4针脚,像素为白色,一般I2C通信
规格:7针脚,占用IO口多一些,一般SPI通信
蓝色像素版本
黄蓝双色版本,上面1/4像素固定为黄色,下面3/4固定为蓝色,适和做需要显示标题行和内容的界面。无论哪个规格版本,驱动方式都是一样的。
OLED硬件电路
4针脚版本:SCL和SDA是I2C的通信引脚,需要接在I2C通信的引脚上(教程给的驱动函数模块是使用GPIO口模拟的I2C通信,所以这两个端口可以接在任意的GPIO口上)
7针脚版本:除GND和VCC外的引脚是SPI通信协议的引脚,(如果是GPIO口模拟的通信协议,也是可以任意接GPIO口)
OLED驱动函数模块
OLED实物图及对应的屏幕坐标图如下:将OLED分割成了4行16列的小区块,除了显示以下内容外,还可以显示图片。
知识点get:
1.STM32的引脚上电后,如果不初始化,默认是浮空输入模式,在这个模式下,引脚不会输出电平,所以不会有什么影响;做实践项目时,最好还是给OLED用电源供电,不用GPIO口供电
2.字符需要单引号括起来。字符串用双引号括起来
3.c语言不能直接写二进制的数,只能用十六进制来代替。
示例程序(OLED驱动函数)
改引脚配置和端口初始化,就可以直接使用OLED驱动函数了。
比如我这里SCL接在了PB8,那这个地方就是GPIOB,GPIO_Pin_8,如果你换个端口,比如接在PA6上,那这个地方就要改成GPIOA,GPIO_Pin_6;下面这个SDA的引脚配置也是一样,SDA接在了哪个位置,就改成GPIO啥,GPIO_Pin_啥。
初始化具体更改就是,使用到的GPIO外设都先用RCC开启一下时钟,然后下面初始化GPIOB的Pin8,再初始化GPIOB的Pin9。
程序如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
int main(void)
{
OLED_Init(); //初始化OLED
OLED_ShowChar(1,1,'A'); //第一行第一列显示单字符A,字符使用单引号括起来
OLED_ShowString(1,3,"hellow word!");//第一行第三列开始显示字符串hello word!字符串使用双引号括起来
OLED_ShowNum(2,1,12345,5); //显示无符号十进制数字,第二行第一列开始,长度为5
OLED_ShowSignedNum(2,7,-66,2); //显示有符号(带正负号)十进制数,长度为2
OLED_ShowHexNum(3,1,0xAA55,4); //显示十六进制数,长度为4
OLED_ShowBinNum(4,1,0xAA55,16); // 显示二进制数,c语音不能直接写二进制数,只能用16进制数代替,C语言这么底层的语言竟然不支持二进制数
//OLED_Clear(); //清屏;若只想清除部分字符,可以用OLED_ShowString在想清除的地方显示空格即可
while(1)
{
}
}
OLED.c
#include "stm32f10x.h"
#include "OLED_Font.h"
/*需要更改的只有'引脚配置'和'引脚初始化'*/
/*引脚配置, 选择的是你硬件电路把SCL和SDA这两个引脚接在了哪两个端口*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
//下面为 I2C通信的基本时序 和后面OLED用户调用的代码
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的一个字节
* @retval 无
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
OLED_W_SDA(Byte & (0x80 >> i));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
/**
* @brief OLED写命令
* @param Command 要写入的命令
* @retval 无
*/
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
/**
* @brief OLED写数据
* @param Data 要写入的数据
* @retval 无
*/
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
/**
* @brief OLED清屏
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for(i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/**
* @brief OLED显示一个字符
* @param Line 行位置,范围:1~4
* @param Column 列位置,范围:1~16
* @param Char 要显示的一个字符,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/**
* @brief OLED显示字符串
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/**
* @brief OLED次方函数
* @retval 返回值等于X的Y次方
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十六进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFFFFFF
* @param Length 要显示数字的长度,范围:1~8
* @retval 无
*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/**
* @brief OLED显示数字(二进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
OLED_I2C_Init(); //端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); //OLED清屏
}
OLED.h
#include "stm32f10x.h"
#include "OLED_Font.h"
/*需要更改的只有'引脚配置'和'引脚初始化'*/
/*引脚配置, 选择的是你硬件电路把SCL和SDA这两个引脚接在了哪两个端口*/
#define OLED_W_SCL(x) GPIO_WriteBit(GPIOB, GPIO_Pin_8, (BitAction)(x))
#define OLED_W_SDA(x) GPIO_WriteBit(GPIOB, GPIO_Pin_9, (BitAction)(x))
/*引脚初始化*/
void OLED_I2C_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_OD; //开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8;
GPIO_Init(GPIOB, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_Init(GPIOB, &GPIO_InitStructure);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
//下面为 I2C通信的基本时序 和后面OLED用户调用的代码
/**
* @brief I2C开始
* @param 无
* @retval 无
*/
void OLED_I2C_Start(void)
{
OLED_W_SDA(1);
OLED_W_SCL(1);
OLED_W_SDA(0);
OLED_W_SCL(0);
}
/**
* @brief I2C停止
* @param 无
* @retval 无
*/
void OLED_I2C_Stop(void)
{
OLED_W_SDA(0);
OLED_W_SCL(1);
OLED_W_SDA(1);
}
/**
* @brief I2C发送一个字节
* @param Byte 要发送的一个字节
* @retval 无
*/
void OLED_I2C_SendByte(uint8_t Byte)
{
uint8_t i;
for (i = 0; i < 8; i++)
{
OLED_W_SDA(Byte & (0x80 >> i));
OLED_W_SCL(1);
OLED_W_SCL(0);
}
OLED_W_SCL(1); //额外的一个时钟,不处理应答信号
OLED_W_SCL(0);
}
/**
* @brief OLED写命令
* @param Command 要写入的命令
* @retval 无
*/
void OLED_WriteCommand(uint8_t Command)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x00); //写命令
OLED_I2C_SendByte(Command);
OLED_I2C_Stop();
}
/**
* @brief OLED写数据
* @param Data 要写入的数据
* @retval 无
*/
void OLED_WriteData(uint8_t Data)
{
OLED_I2C_Start();
OLED_I2C_SendByte(0x78); //从机地址
OLED_I2C_SendByte(0x40); //写数据
OLED_I2C_SendByte(Data);
OLED_I2C_Stop();
}
/**
* @brief OLED设置光标位置
* @param Y 以左上角为原点,向下方向的坐标,范围:0~7
* @param X 以左上角为原点,向右方向的坐标,范围:0~127
* @retval 无
*/
void OLED_SetCursor(uint8_t Y, uint8_t X)
{
OLED_WriteCommand(0xB0 | Y); //设置Y位置
OLED_WriteCommand(0x10 | ((X & 0xF0) >> 4)); //设置X位置高4位
OLED_WriteCommand(0x00 | (X & 0x0F)); //设置X位置低4位
}
/**
* @brief OLED清屏
* @param 无
* @retval 无
*/
void OLED_Clear(void)
{
uint8_t i, j;
for (j = 0; j < 8; j++)
{
OLED_SetCursor(j, 0);
for(i = 0; i < 128; i++)
{
OLED_WriteData(0x00);
}
}
}
/**
* @brief OLED显示一个字符
* @param Line 行位置,范围:1~4
* @param Column 列位置,范围:1~16
* @param Char 要显示的一个字符,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowChar(uint8_t Line, uint8_t Column, char Char)
{
uint8_t i;
OLED_SetCursor((Line - 1) * 2, (Column - 1) * 8); //设置光标位置在上半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i]); //显示上半部分内容
}
OLED_SetCursor((Line - 1) * 2 + 1, (Column - 1) * 8); //设置光标位置在下半部分
for (i = 0; i < 8; i++)
{
OLED_WriteData(OLED_F8x16[Char - ' '][i + 8]); //显示下半部分内容
}
}
/**
* @brief OLED显示字符串
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param String 要显示的字符串,范围:ASCII可见字符
* @retval 无
*/
void OLED_ShowString(uint8_t Line, uint8_t Column, char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i++)
{
OLED_ShowChar(Line, Column + i, String[i]);
}
}
/**
* @brief OLED次方函数
* @retval 返回值等于X的Y次方
*/
uint32_t OLED_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y--)
{
Result *= X;
}
return Result;
}
/**
* @brief OLED显示数字(十进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~4294967295
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十进制,带符号数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:-2147483648~2147483647
* @param Length 要显示数字的长度,范围:1~10
* @retval 无
*/
void OLED_ShowSignedNum(uint8_t Line, uint8_t Column, int32_t Number, uint8_t Length)
{
uint8_t i;
uint32_t Number1;
if (Number >= 0)
{
OLED_ShowChar(Line, Column, '+');
Number1 = Number;
}
else
{
OLED_ShowChar(Line, Column, '-');
Number1 = -Number;
}
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i + 1, Number1 / OLED_Pow(10, Length - i - 1) % 10 + '0');
}
}
/**
* @brief OLED显示数字(十六进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~0xFFFFFFFF
* @param Length 要显示数字的长度,范围:1~8
* @retval 无
*/
void OLED_ShowHexNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i, SingleNumber;
for (i = 0; i < Length; i++)
{
SingleNumber = Number / OLED_Pow(16, Length - i - 1) % 16;
if (SingleNumber < 10)
{
OLED_ShowChar(Line, Column + i, SingleNumber + '0');
}
else
{
OLED_ShowChar(Line, Column + i, SingleNumber - 10 + 'A');
}
}
}
/**
* @brief OLED显示数字(二进制,正数)
* @param Line 起始行位置,范围:1~4
* @param Column 起始列位置,范围:1~16
* @param Number 要显示的数字,范围:0~1111 1111 1111 1111
* @param Length 要显示数字的长度,范围:1~16
* @retval 无
*/
void OLED_ShowBinNum(uint8_t Line, uint8_t Column, uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i++)
{
OLED_ShowChar(Line, Column + i, Number / OLED_Pow(2, Length - i - 1) % 2 + '0');
}
}
/**
* @brief OLED初始化
* @param 无
* @retval 无
*/
void OLED_Init(void)
{
uint32_t i, j;
for (i = 0; i < 1000; i++) //上电延时
{
for (j = 0; j < 1000; j++);
}
OLED_I2C_Init(); //端口初始化
OLED_WriteCommand(0xAE); //关闭显示
OLED_WriteCommand(0xD5); //设置显示时钟分频比/振荡器频率
OLED_WriteCommand(0x80);
OLED_WriteCommand(0xA8); //设置多路复用率
OLED_WriteCommand(0x3F);
OLED_WriteCommand(0xD3); //设置显示偏移
OLED_WriteCommand(0x00);
OLED_WriteCommand(0x40); //设置显示开始行
OLED_WriteCommand(0xA1); //设置左右方向,0xA1正常 0xA0左右反置
OLED_WriteCommand(0xC8); //设置上下方向,0xC8正常 0xC0上下反置
OLED_WriteCommand(0xDA); //设置COM引脚硬件配置
OLED_WriteCommand(0x12);
OLED_WriteCommand(0x81); //设置对比度控制
OLED_WriteCommand(0xCF);
OLED_WriteCommand(0xD9); //设置预充电周期
OLED_WriteCommand(0xF1);
OLED_WriteCommand(0xDB); //设置VCOMH取消选择级别
OLED_WriteCommand(0x30);
OLED_WriteCommand(0xA4); //设置整个显示打开/关闭
OLED_WriteCommand(0xA6); //设置正常/倒转显示
OLED_WriteCommand(0x8D); //设置充电泵
OLED_WriteCommand(0x14);
OLED_WriteCommand(0xAF); //开启显示
OLED_Clear(); //OLED清屏
}
OLED.Font.h
#ifndef __OLED_FONT_H
#define __OLED_FONT_H
/*OLED字模库,宽8像素,高16像素*/
/*OLED的字库数据,因为这个OLED显示屏是不带字库的
想要显示字符图形,还得先定义字符的点阵数据,如下就为点阵数据,也就是字库
OLED.c文件的显示函数会用到这些数据,字库不需修改*/
const uint8_t OLED_F8x16[][16]=
{
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,// 0
0x00,0x00,0x00,0xF8,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x33,0x30,0x00,0x00,0x00,//! 1
0x00,0x10,0x0C,0x06,0x10,0x0C,0x06,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//" 2
0x40,0xC0,0x78,0x40,0xC0,0x78,0x40,0x00,
0x04,0x3F,0x04,0x04,0x3F,0x04,0x04,0x00,//# 3
0x00,0x70,0x88,0xFC,0x08,0x30,0x00,0x00,
0x00,0x18,0x20,0xFF,0x21,0x1E,0x00,0x00,//$ 4
0xF0,0x08,0xF0,0x00,0xE0,0x18,0x00,0x00,
0x00,0x21,0x1C,0x03,0x1E,0x21,0x1E,0x00,//% 5
0x00,0xF0,0x08,0x88,0x70,0x00,0x00,0x00,
0x1E,0x21,0x23,0x24,0x19,0x27,0x21,0x10,//& 6
0x10,0x16,0x0E,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//' 7
0x00,0x00,0x00,0xE0,0x18,0x04,0x02,0x00,
0x00,0x00,0x00,0x07,0x18,0x20,0x40,0x00,//( 8
0x00,0x02,0x04,0x18,0xE0,0x00,0x00,0x00,
0x00,0x40,0x20,0x18,0x07,0x00,0x00,0x00,//) 9
0x40,0x40,0x80,0xF0,0x80,0x40,0x40,0x00,
0x02,0x02,0x01,0x0F,0x01,0x02,0x02,0x00,//* 10
0x00,0x00,0x00,0xF0,0x00,0x00,0x00,0x00,
0x01,0x01,0x01,0x1F,0x01,0x01,0x01,0x00,//+ 11
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x80,0xB0,0x70,0x00,0x00,0x00,0x00,0x00,//, 12
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x01,0x01,0x01,0x01,0x01,0x01,0x01,//- 13
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x30,0x30,0x00,0x00,0x00,0x00,0x00,//. 14
0x00,0x00,0x00,0x00,0x80,0x60,0x18,0x04,
0x00,0x60,0x18,0x06,0x01,0x00,0x00,0x00,/// 15
0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
0x00,0x0F,0x10,0x20,0x20,0x10,0x0F,0x00,//0 16
0x00,0x10,0x10,0xF8,0x00,0x00,0x00,0x00,
0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//1 17
0x00,0x70,0x08,0x08,0x08,0x88,0x70,0x00,
0x00,0x30,0x28,0x24,0x22,0x21,0x30,0x00,//2 18
0x00,0x30,0x08,0x88,0x88,0x48,0x30,0x00,
0x00,0x18,0x20,0x20,0x20,0x11,0x0E,0x00,//3 19
0x00,0x00,0xC0,0x20,0x10,0xF8,0x00,0x00,
0x00,0x07,0x04,0x24,0x24,0x3F,0x24,0x00,//4 20
0x00,0xF8,0x08,0x88,0x88,0x08,0x08,0x00,
0x00,0x19,0x21,0x20,0x20,0x11,0x0E,0x00,//5 21
0x00,0xE0,0x10,0x88,0x88,0x18,0x00,0x00,
0x00,0x0F,0x11,0x20,0x20,0x11,0x0E,0x00,//6 22
0x00,0x38,0x08,0x08,0xC8,0x38,0x08,0x00,
0x00,0x00,0x00,0x3F,0x00,0x00,0x00,0x00,//7 23
0x00,0x70,0x88,0x08,0x08,0x88,0x70,0x00,
0x00,0x1C,0x22,0x21,0x21,0x22,0x1C,0x00,//8 24
0x00,0xE0,0x10,0x08,0x08,0x10,0xE0,0x00,
0x00,0x00,0x31,0x22,0x22,0x11,0x0F,0x00,//9 25
0x00,0x00,0x00,0xC0,0xC0,0x00,0x00,0x00,
0x00,0x00,0x00,0x30,0x30,0x00,0x00,0x00,//: 26
0x00,0x00,0x00,0x80,0x00,0x00,0x00,0x00,
0x00,0x00,0x80,0x60,0x00,0x00,0x00,0x00,//; 27
0x00,0x00,0x80,0x40,0x20,0x10,0x08,0x00,
0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x00,//< 28
0x40,0x40,0x40,0x40,0x40,0x40,0x40,0x00,
0x04,0x04,0x04,0x04,0x04,0x04,0x04,0x00,//= 29
0x00,0x08,0x10,0x20,0x40,0x80,0x00,0x00,
0x00,0x20,0x10,0x08,0x04,0x02,0x01,0x00,//> 30
0x00,0x70,0x48,0x08,0x08,0x08,0xF0,0x00,
0x00,0x00,0x00,0x30,0x36,0x01,0x00,0x00,//? 31
0xC0,0x30,0xC8,0x28,0xE8,0x10,0xE0,0x00,
0x07,0x18,0x27,0x24,0x23,0x14,0x0B,0x00,//@ 32
0x00,0x00,0xC0,0x38,0xE0,0x00,0x00,0x00,
0x20,0x3C,0x23,0x02,0x02,0x27,0x38,0x20,//A 33
0x08,0xF8,0x88,0x88,0x88,0x70,0x00,0x00,
0x20,0x3F,0x20,0x20,0x20,0x11,0x0E,0x00,//B 34
0xC0,0x30,0x08,0x08,0x08,0x08,0x38,0x00,
0x07,0x18,0x20,0x20,0x20,0x10,0x08,0x00,//C 35
0x08,0xF8,0x08,0x08,0x08,0x10,0xE0,0x00,
0x20,0x3F,0x20,0x20,0x20,0x10,0x0F,0x00,//D 36
0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,
0x20,0x3F,0x20,0x20,0x23,0x20,0x18,0x00,//E 37
0x08,0xF8,0x88,0x88,0xE8,0x08,0x10,0x00,
0x20,0x3F,0x20,0x00,0x03,0x00,0x00,0x00,//F 38
0xC0,0x30,0x08,0x08,0x08,0x38,0x00,0x00,
0x07,0x18,0x20,0x20,0x22,0x1E,0x02,0x00,//G 39
0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,
0x20,0x3F,0x21,0x01,0x01,0x21,0x3F,0x20,//H 40
0x00,0x08,0x08,0xF8,0x08,0x08,0x00,0x00,
0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//I 41
0x00,0x00,0x08,0x08,0xF8,0x08,0x08,0x00,
0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,0x00,//J 42
0x08,0xF8,0x88,0xC0,0x28,0x18,0x08,0x00,
0x20,0x3F,0x20,0x01,0x26,0x38,0x20,0x00,//K 43
0x08,0xF8,0x08,0x00,0x00,0x00,0x00,0x00,
0x20,0x3F,0x20,0x20,0x20,0x20,0x30,0x00,//L 44
0x08,0xF8,0xF8,0x00,0xF8,0xF8,0x08,0x00,
0x20,0x3F,0x00,0x3F,0x00,0x3F,0x20,0x00,//M 45
0x08,0xF8,0x30,0xC0,0x00,0x08,0xF8,0x08,
0x20,0x3F,0x20,0x00,0x07,0x18,0x3F,0x00,//N 46
0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,
0x0F,0x10,0x20,0x20,0x20,0x10,0x0F,0x00,//O 47
0x08,0xF8,0x08,0x08,0x08,0x08,0xF0,0x00,
0x20,0x3F,0x21,0x01,0x01,0x01,0x00,0x00,//P 48
0xE0,0x10,0x08,0x08,0x08,0x10,0xE0,0x00,
0x0F,0x18,0x24,0x24,0x38,0x50,0x4F,0x00,//Q 49
0x08,0xF8,0x88,0x88,0x88,0x88,0x70,0x00,
0x20,0x3F,0x20,0x00,0x03,0x0C,0x30,0x20,//R 50
0x00,0x70,0x88,0x08,0x08,0x08,0x38,0x00,
0x00,0x38,0x20,0x21,0x21,0x22,0x1C,0x00,//S 51
0x18,0x08,0x08,0xF8,0x08,0x08,0x18,0x00,
0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,//T 52
0x08,0xF8,0x08,0x00,0x00,0x08,0xF8,0x08,
0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,//U 53
0x08,0x78,0x88,0x00,0x00,0xC8,0x38,0x08,
0x00,0x00,0x07,0x38,0x0E,0x01,0x00,0x00,//V 54
0xF8,0x08,0x00,0xF8,0x00,0x08,0xF8,0x00,
0x03,0x3C,0x07,0x00,0x07,0x3C,0x03,0x00,//W 55
0x08,0x18,0x68,0x80,0x80,0x68,0x18,0x08,
0x20,0x30,0x2C,0x03,0x03,0x2C,0x30,0x20,//X 56
0x08,0x38,0xC8,0x00,0xC8,0x38,0x08,0x00,
0x00,0x00,0x20,0x3F,0x20,0x00,0x00,0x00,//Y 57
0x10,0x08,0x08,0x08,0xC8,0x38,0x08,0x00,
0x20,0x38,0x26,0x21,0x20,0x20,0x18,0x00,//Z 58
0x00,0x00,0x00,0xFE,0x02,0x02,0x02,0x00,
0x00,0x00,0x00,0x7F,0x40,0x40,0x40,0x00,//[ 59
0x00,0x0C,0x30,0xC0,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x01,0x06,0x38,0xC0,0x00,//\ 60
0x00,0x02,0x02,0x02,0xFE,0x00,0x00,0x00,
0x00,0x40,0x40,0x40,0x7F,0x00,0x00,0x00,//] 61
0x00,0x00,0x04,0x02,0x02,0x02,0x04,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//^ 62
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x80,//_ 63
0x00,0x02,0x02,0x04,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//` 64
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
0x00,0x19,0x24,0x22,0x22,0x22,0x3F,0x20,//a 65
0x08,0xF8,0x00,0x80,0x80,0x00,0x00,0x00,
0x00,0x3F,0x11,0x20,0x20,0x11,0x0E,0x00,//b 66
0x00,0x00,0x00,0x80,0x80,0x80,0x00,0x00,
0x00,0x0E,0x11,0x20,0x20,0x20,0x11,0x00,//c 67
0x00,0x00,0x00,0x80,0x80,0x88,0xF8,0x00,
0x00,0x0E,0x11,0x20,0x20,0x10,0x3F,0x20,//d 68
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
0x00,0x1F,0x22,0x22,0x22,0x22,0x13,0x00,//e 69
0x00,0x80,0x80,0xF0,0x88,0x88,0x88,0x18,
0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//f 70
0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,
0x00,0x6B,0x94,0x94,0x94,0x93,0x60,0x00,//g 71
0x08,0xF8,0x00,0x80,0x80,0x80,0x00,0x00,
0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,//h 72
0x00,0x80,0x98,0x98,0x00,0x00,0x00,0x00,
0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//i 73
0x00,0x00,0x00,0x80,0x98,0x98,0x00,0x00,
0x00,0xC0,0x80,0x80,0x80,0x7F,0x00,0x00,//j 74
0x08,0xF8,0x00,0x00,0x80,0x80,0x80,0x00,
0x20,0x3F,0x24,0x02,0x2D,0x30,0x20,0x00,//k 75
0x00,0x08,0x08,0xF8,0x00,0x00,0x00,0x00,
0x00,0x20,0x20,0x3F,0x20,0x20,0x00,0x00,//l 76
0x80,0x80,0x80,0x80,0x80,0x80,0x80,0x00,
0x20,0x3F,0x20,0x00,0x3F,0x20,0x00,0x3F,//m 77
0x80,0x80,0x00,0x80,0x80,0x80,0x00,0x00,
0x20,0x3F,0x21,0x00,0x00,0x20,0x3F,0x20,//n 78
0x00,0x00,0x80,0x80,0x80,0x80,0x00,0x00,
0x00,0x1F,0x20,0x20,0x20,0x20,0x1F,0x00,//o 79
0x80,0x80,0x00,0x80,0x80,0x00,0x00,0x00,
0x80,0xFF,0xA1,0x20,0x20,0x11,0x0E,0x00,//p 80
0x00,0x00,0x00,0x80,0x80,0x80,0x80,0x00,
0x00,0x0E,0x11,0x20,0x20,0xA0,0xFF,0x80,//q 81
0x80,0x80,0x80,0x00,0x80,0x80,0x80,0x00,
0x20,0x20,0x3F,0x21,0x20,0x00,0x01,0x00,//r 82
0x00,0x00,0x80,0x80,0x80,0x80,0x80,0x00,
0x00,0x33,0x24,0x24,0x24,0x24,0x19,0x00,//s 83
0x00,0x80,0x80,0xE0,0x80,0x80,0x00,0x00,
0x00,0x00,0x00,0x1F,0x20,0x20,0x00,0x00,//t 84
0x80,0x80,0x00,0x00,0x00,0x80,0x80,0x00,
0x00,0x1F,0x20,0x20,0x20,0x10,0x3F,0x20,//u 85
0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,
0x00,0x01,0x0E,0x30,0x08,0x06,0x01,0x00,//v 86
0x80,0x80,0x00,0x80,0x00,0x80,0x80,0x80,
0x0F,0x30,0x0C,0x03,0x0C,0x30,0x0F,0x00,//w 87
0x00,0x80,0x80,0x00,0x80,0x80,0x80,0x00,
0x00,0x20,0x31,0x2E,0x0E,0x31,0x20,0x00,//x 88
0x80,0x80,0x80,0x00,0x00,0x80,0x80,0x80,
0x80,0x81,0x8E,0x70,0x18,0x06,0x01,0x00,//y 89
0x00,0x80,0x80,0x80,0x80,0x80,0x80,0x00,
0x00,0x21,0x30,0x2C,0x22,0x21,0x30,0x00,//z 90
0x00,0x00,0x00,0x00,0x80,0x7C,0x02,0x02,
0x00,0x00,0x00,0x00,0x00,0x3F,0x40,0x40,//{ 91
0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0xFF,0x00,0x00,0x00,//| 92
0x00,0x02,0x02,0x7C,0x80,0x00,0x00,0x00,
0x00,0x40,0x40,0x3F,0x00,0x00,0x00,0x00,//} 93
0x00,0x06,0x01,0x01,0x02,0x02,0x04,0x04,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,//~ 94
};
#endif
keil的调试模式
这个方法可以精确追踪我们的程序是如何运行的,如果你不清楚程序是如何一步一步运行的,那在这个调试模式里单步运行探索一下,相信你对程序的运行逻辑会有更深的理解。
工程选型,Debug里可以选择是哪个方式进行仿真,右边为在硬件上进行仿真(需要把STLINK和stm32都连接好),左边为仿真器仿真,这样就是电脑模拟stm32的运行了。
基于硬件仿真,需要提前编译一下,确保无误。
点击此处进入调试模式
主窗口就是我们的C语言程序,上面窗口为c语言翻译成的汇编程序,左边窗口是寄存器组和状态标志位等信息(单片机硬件底层很重要的东西,如果你使用汇编编程的话,这些东西都必须非常清楚的,如果使用的C语言,这些就不用管了)。
复位
全速运行
停止全速运行
单步运行
跳过当前行单步运行
跳出当前函数单步运行
跳到光标指定行单步运行
黄色箭头指示的就是下一句将要执行的代码,我们点一下单步运行,那它就执行到了下一行,如图就是第六行。
点击左侧行数,出现红色点为断点,点击全速运行,程序就会一直运行,直到断点停下。如果没有断点点击全速运行,程序就不会自动停下来,点击停止按钮程序才会停下来。
命令窗口,点击可打开或关闭命令窗口
反汇编窗口,点击可打开或关闭
符号窗口,在这里可实时查看程序中所有的变量值
比如在符号窗口中,右键可查看具体某个值的变化
单步运行,就能看到值的变化了。
串口显示
逻辑分析仪
外设菜单栏,系统资源查看,这里可看到所有的外设寄存器
比如选择GPIOA,右边显示GPIOA外设的所有寄存器
这个ODR0就是PA0的输出数据寄存器,会实时显示输出寄存器的变化。
所以当遇到一个比较难得程序,比如不知道程序是如何执行的、想要看一大堆变量却不方便显示、想看一下寄存器是不是配置正确等都可以考虑使用一下这个keil自带的调试模式。
不能在调试模式下修改程序的,修改程序,需要退出调试模式,再编译,再进入调试模式。
调试模式下,还有很多的工具都是非常强大的,大家可以自己去了解一下。
5、EXTI外部中断
对射式红外传感器计次程序现象:对射式红外传感器挡光后触发下降沿,这个下降沿触发单片机引脚的外部中断,然后执行数字加1的中断程序。
旋转编码器计次程序现象:
中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一,所以本节我们就借助外部中断来学习一下中断系统。在以后学习其它外设的时候,也是会经常和中断打交道的。
中断系统
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源,比如对于外部中断来说,可以是引脚发生了电平跳变;对于定时器来说,可以是定时的时间到了;对于串口通信来说,可以是接收到了数据),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。(就好比晚上睡觉前定了个闹钟,时间到了提醒你,不管时间到不到你可以安心睡觉,如果你没有闹钟,那你就得不断地看时间,生怕错过了起床点)。如果没有中断系统,为了防止外部中断被忽略或者串口数据被覆盖,那主程序就只能不断地查询是否有这些事件发生,不能再干其他事情了。
中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。(这个中断优先级是我们根据程序设计的需求,自己设置的)。
中断嵌套:(中断程序再次中断,二次中断现象)当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。(也是为了照顾非常紧急的中断)。
中断执行流程
中断程序的执行流程如下,当它执行到某个地方时,外设的中断条件满足了,那这时,无论主程序是在干什么事情(比如OLED显示程序才执行一半,Delay函数还在等待等)中断来了,主程序都得立即暂停,程序由硬件电路自动跳转到中断程序中,当中断程序执行完之后,程序再返回被暂停的地方继续运行(这个暂停的地方,叫做断点)。为了程序能在中断返回后继续原来的工作,在中断执行前,会对程序的现场进行保护,中断执行后,会再还原现场(这个还原不需要我们来做),这样保证主程序被中断了,回来之后也能继续执行。
中断嵌套的执行流程如下。当一个中断正在执行时,又有新的优先级更高的中断来,那个旧中断会被打断,执行新的中断,新的中断结束,再继续执行原来的中断,原来的中断结束,再继续主程序,这就是中断嵌套的执行流程。
c语言中,中断的执行流程如下。上面是主函数,while(1)死循环里就是主程序,正常情况下,程序就是在主程序中不断循环执行,当中断条件满足时,主程序就会暂停,然后自动跳转到中断程序里运行,中断程序执行完之后,再返回主程序执行。一般中断程序都是在一个子函数里,这个函数不需要我们调用,当中断来临时,由硬件自动调用这个函数,这就是在c语言中,中断的执行流程。
STM32中断
68个可屏蔽中断通道(中断源),包含EXTI(外部中断)、TIM、ADC(模数转换器)、USART(串口)、SPI、I2C、RTC(实时时钟)等多个外设。(几乎所有模块都能申请中断)
使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级。
NVIC就是STM32中用来管理中断、分配优先级的,NVIC的中断优先级共有16个等级。
EXTIx是外部中断对应的中断资源。
下图为stm32的中断资源,上面灰色的是内核中断(我们一般不用,了解即可),比如第一个复位中断,当产生复位事件时,程序就会自动执行复位中断函数,也就是复位后程序开始执行的位置。下面不是灰色的部分就是stm32外设的中断了,比如第一个窗口看门狗,这个是用来监测程序运行状态的中断,比如你程序卡死了,没有及时喂狗,窗口看门狗就会申请中断,让你的程序跳到窗口看门狗的中断程序里,那你在中断程序里就可以进行一些错误检查,看看是什么出问题了。然后PVD电源电压监测,如果你的供电电压不足,PVD电路就会申请中断,你在中断里赶紧保存一下重要数据。外设电路检测到有什么异常或事件,需要提示一下CPU的时候,它就可以申请中断,让程序调到对应的中断函数里运行一次,用来处理这个异常或事件。
图中最右边是中断的地址,因为程序中的中断函数,它的地址是由编译器来分配的,是不固定的,但是我们的中断跳转,由于硬件的限制,只能跳到固定的地址执行程序,所以为了硬件能够跳转到一个不固定的中断函数里,这里就需要在内存中定义一个地址的列表,这个列表的地址是固定的,中断发生后,就跳到这个固定位置,然后在这个固定位置,由编译器,再加上一个跳转到中断函数的代码,这样中断跳转就可以跳转到任意位置了,这个中断地址的列表,就叫中断向量表,相当于中断跳转的一个跳板,不过我们用c编程,是不需要管这个中断向量表的,因为编译器都帮我们做好了。
NVIC基本结构
NVIC(嵌套中断向量控制器),在stm32中,它是用来统一分配中断优先级和管理中断的,NVIC是一个内核外设,是CPU的小助手(如果把中断全接到CPU上,会很麻烦,毕竟CPU主要是用来运算的,所以中断分配的任务就让NVIC来负责。),NVIC有很多输入口,你有多少个中断线路都可以接过来。图中线上划了个斜杠上面写了n(这个意思是:一个外设可能会同时占用多个中断通道,所以这里有n条线),然后NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,之后通过右边这一输出口就告诉CPU该处理哪个中断,对于中断先后顺序分配的任务,CPU不需要知道。
举个例子:比如CPU是医生,如果医院只有一个医生时,当看病人很多时,医生就得先安排一下先看谁后看谁,如果有紧急的病人,那还得让紧急的病人最先来,这个安排先后顺序的任务很繁琐会影响医生看病的效率,所以医院就安排了一个叫号系统(NVIC),来病人了统一取号并且根据病人的等级,分配一个优先级,然后叫号系统看一下现在在排队的病人,优先叫号紧急的病人,最后叫号系统给医生输出的就是一个一个排好队的病人,医生就可以专心看病了。(EXTI、TIM、ADC等就是病人)
NVIC优先级分组
为了处理不同形式的优先级,STM32的NVIC可以对优先级进行分组,分为抢占优先级和响应优先级。
抢占优先级和响应优先级的区别,例子理解:还是病人叫号的例子,对于紧急的病人,其实有两种形式的优先。一种是,上一个病人1在看病,外面排队了很多病人,当病人1看完后,外面排队中的紧急病人最先进去看病即使这个紧急病人是最后来的,这种在排队中的插队的就叫响应优先级,响应优先级高的可以插队提前看病。另一种是,上一个病人1在看病,外面排队中的病人2比病人1更加紧急,病人2可以不等病人1看完直接冲到医生的屋里,让病人1先靠边站,先给病人2看病,病人2看完病接着病人1看病,然后外面排队的病人再进来,这种形式的优先级就是中断嵌套,这种决定是不是可以中断嵌套的优先级,就叫抢占优先级,抢占优先级高的,可以进行中断嵌套。
为了将优先级区分为抢占优先级和响应优先级,就需要对这16个优先级优先级进行分组,NVIC的中断优先级由优先级寄存器的4位(0~15,4位二进制,对应16个优先级)决定,优先级的数值越小,优先级越高,0就是最高优先级。这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级
抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队(中断号是中断表的左边数字,数值小的优先响应),所以stm32的中断不存在先来后到的排队方式,在任何时候都是优先级高的先响应。
下表,因为优先级总共是4位,所以就有(0,4)、(1.3)、(2,2)、(3,1)、(4、0)这五种分组方式,分组0,就是0位的抢占等级,取值为0,4位的响应等级,取值为0~15,分组1234雷同。这个分组方式是我们在程序中自己进行选择的,选好分组方式后,就要注意抢占优先级和响应优先级的取值范围了,不要超出这个表里规定的取值范围。
分组方式 | 抢占优先级 | 响应优先级 |
---|---|---|
分组0 | 0位,取值为0 | 4位,取值为0~15 |
分组1 | 1位,取值为0~1 | 3位,取值为0~7 |
分组2 | 2位,取值为0~3 | 2位,取值为0~3 |
分组3 | 3位,取值为0~7 | 1位,取值为0~1 |
分组4 | 4位,取值为0~15 | 0位,取值为0 |
EXTI简介
EXTI(Extern Interrupt)外部中断
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。(简单说:引脚电平变化,申请中断)。
支持的触发方式(引脚电平的变化类型):上升沿(电平从低电平变到高电平的瞬间触发中断)/下降沿(电平从高电平变到低电平的瞬间触发中断)/双边沿(上升沿和下降沿都可以触发中断)/软件触发(程序执行代码就能触发中断)
支持的GPIO口(外部中断引脚):所有GPIO口都能触发中断,但相同的Pin不能同时触发中断(比如PA0和PB0不能同时使用,只能选一个作为中断引脚;所以如果有多个中断引脚要选择不同的pin引脚,比如PA0和PA1、PA9和PB15、PB6和PB7就可以)
通道数:总共有20个中断线路。16个GPIO_Pin(对应引脚GPIO_pin0到15,是外部中断的主要功能),外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒(这4个中断线路,是因为外部中断有个功能是从低功耗模式的停止模式下唤醒STM32,那对于PVD电源电压检测,当电源从电压过低恢复时就需要PVD借助一下外部中断,退出停止模式;对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒,这也需要借助外部中断,剩余USB唤醒、以太网唤醒也是类似的作用)
触发响应方式:中断响应(引脚电平触发中断,申请中断,让CPU执行中断函数)/事件响应(不会触发中断,而是触发别的外设操作,属于外设之间的联合工作。外部中断的信号不会通向CPU而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等)
EXTI基本结构
外部中断的整体结构图如下:
首先,最左边是GPIO口的外设,每个GPIO外设有16个引脚,所以进来16根线;如果每个引脚占用一个通道,那EXTI的16个通道是不够用的,所以在这里会有一个AFIO中断引脚选择的电路模块,这个AFIO就是一个数据选择器(可以将图中前面的3个GPIO外设的16个引脚中的其中一个连接到后面的EXTI通道(16个GPIO通道),所以对于PA0\PB0\PC0这些,通过AFIO选择之后只有其中一个能接到EXTI的通道0上,同理PA1\PB1\PC1这些,也只能有一个连接到通道1上,这就是所有GPIO口都能触发中断,但相同的Pin不能同时触发中断的原因),然后通过AFIO选择后的16个通道,就接到了EXTI边沿检测及控制电路上,同时下面这4个蹭网的外设(PVD\PTC\USB\ETH)也是并列接进来的,这些加起来就组成了EXTI的20个输入信号,然后经过EXTI电路之后,分为了两种输出,也就是中断响应和事件响应(上面接到了NVIC用来触发中断,下面有20条输出线路输出线路到了其它外设,这就是用来触发其他外设操作的,也就是事件响应)
注意点:本来20路输入,应该有20路中断的输出,可能ST公司觉得20个输出太多了比较占用NVIC的通道资源,所以就把其中的外部中断9~ 5,15~10,给分到了一个通道,EXTI9_5是外部中断的5,6,7,8,9分到了一个通道里,EXTI15_10也是一样;也就是说外部中断的9到5会触发同一个中断函数,15到10也会触发同一个中断函数;在编程的时候,我们在这两个中断函数里,需要再根据标志位区分到底是哪个中断进来的。
AFIO复用IO内部电路
内部电路就是一系列的数据选择器,如图的最上面输入是PA0\PB0\PC0等尾号都是0,然后通过数据选择器最终选择一个,连接到EXTI0上,上面写的文字是说配置这个寄存器的哪一个位就可以决定选择哪一个输入,图中后面部分内容都雷同。
AFIO主要用于引脚复用功能的选择和重定义(也就是数据选择器的作用)。
在STM32中,AFIO主要完成两个任务:复用功能引脚重映射(就是最开始提到的引脚定义表,当想把这些默认复用功能的引脚换到重定义功能时,就是用AFIO来完成的,这也是AFIO的一大主要功能)、中断引脚选择。
EXTI内部电路框图
EXTI的右边就是20根输入线,然后输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发还是下降沿触发或者两个都触发,接着硬件触发信号和软件中断寄存器的值就进入到这个或门的输入端(也就是任意一个为1,或门就可以输出1),然后触发信号通过这个或门后就兵分两路,上一路是触发中断的(至NVIC中断控制器),下一路是触发事件的(脉冲发生器):触发中断首先会置一个挂起寄存器(挂起寄存器相当于一个中断标志位,可以读取这个寄存器判断是哪个通道触发的中断,如果挂起寄存器置1,它就会继续向左走和中断屏蔽寄存器共同进入一个与门(与门实际上就是开关控制作用,中断屏蔽寄存器给1那,那另一个输入就是直接输出,也就是允许中断;中断屏蔽寄存器给0,那另一个输入无论是什么,输出都是0,相当于屏蔽了这个中断),然后是NVIC中断控制器)。接着就是下一路的选择是触发事件,首先也是一个事件屏蔽寄存器进行开关控制,最后通过一个脉冲发生器到其它外设(脉冲发生器就是给一个电平脉冲,用来触发其它外设的动作)
补充:这些画一个斜线写着20表示这是20根线,代表20个通道,框图最上面两个就是外设接口和APB总线,我们可以通过总线访问这些寄存器。
或门(无直边)。它可以有多个输入,但只能有一个输出。执行的是或的逻辑,在输入端(曲边),只要有一个高电平1,输出就是高电平1;只有全部输入低电平0,输出才为0。(尖头为输出)。(或1为1,全0则0)
与门(直边)。它可以有多个输入,但只能有一个输出。执行的是与的逻辑,在输入端(直边),只要有一个是低电平0,输出就是0;只有全部输入1,输出才为1。(与0为0,全1则1)
非门(三角号加个圈)。它只有一个输入,一个输出;输入1就输出0,输入0就输出1,执行的是非的逻辑(圈为输出,取反)数据选择器(一个梯形)。有多个输入,一个输出,在侧面有选择控制端,根据控制端的数据,从输入选择一个接到输出。
表示20根线,代表20个通道
EXTI外部中断的特性和使用场景
外部中断的使用场景:
什么样的设备需要用到外部中断,使用外部中断有什么好处呢?大概总结了使用外部中断模块的特性:就是对于stm32来说,想要获取的信号是外部驱动的很快的突发信号。
比如旋转编码器的输出信号,你可能很久都不会拧它,这时不需要STM32做任何事,但是我一拧它,就会有很多脉冲波形需要STM32接收。这个信号是突发的,STM32不知道什么时候会来,同时它是外部驱动的,STM32只能被动读取,最后这个信号非常快,STM32稍微晚一点来读取,就会错过很多波形。那对于这种情况来说,就可以考虑使用STM32的外部中断了。有脉冲过来,STM32立即进入中断函数处理,没有脉冲的时候,STM32就专心做其它事情。
另外还有,比如红外遥控接收头的输出,接收到遥控数据之后,它会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要我们用外部中断来读取。
最后还有按键,虽然它的动作也是外部驱动的突发事件,但我并不推荐用外部中断来读取按键。因为用外部中断不好处理按键抖动和松手检测的问题,对于按键来说,它的输出波形也不是转瞬即逝的。所以要求不高的话可以在主程序中循环读取,如果不想用主循环读取的话,可以考虑一下定时器中断读取的方式。这样既可以做到后台读取按键值、不阻塞主程序,也可以很好地处理按键抖动和松手检测的问题。
手册
大概看一下每个外设在手册的介绍。
NVIC是内核外设,所以要在这个内核cortex-m3编程手册中查看,这个cortex-m3编程手册就是内核和内核外设的详细介绍,想研究一下内核的运转细节,可以看一下这个手册,不过这个手册都是英文的。
NVIC的一些寄存器,包括中断使能寄存器
这个中断优先级寄存器就是用来设置每个中断的优先级的,如果直接配置寄存器来设置优先级的话,那还是比较复杂的,用库函数就简单了,直接给结构体赋值就行了,大概看一下就行,要知道库函数要最终落实到寄存器上来的。
中断分组配置寄存器被分配到了这个SCB里面
这三位就是用来配置中断分组的
中断和外部中断的介绍在参考手册中
AFIO介绍
我们使用库函数更加方便,调用一个函数,填两个参数就行了。
EXTI中断示例程序(对射式红外传感器计次&旋转编码器计次)
本节先主要学习外部中断读取编码器计次数据的用法,后面学了定时器,还会再来看一下编码器测速的用途。
旋转编码器简介
旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。
类型:机械触点式/霍尔传感器式/光栅式
1.下面的是一种最简单的编码器样式,这里使用的也是对射式红外传感器来测速的,为了测速还需配合一个光栅编码盘(银色圆圈),当这个编码盘转动时,红外传感器的红外光就会出现遮挡、透过、遮挡、透过这样的现象,对应模块输出的电平就是高低电平交替的方波,方波的个数代表了转过的角度,方波的频率表示转速,我们就可以用外部中断来捕获这个方波的边沿,以此来判断位置和速度,不过这个模块只有一路输出,正转反转输出波形没法区分,所以这种测试方法只能测位置和速度,不能测量旋转方向,为了进一步测量方向,我们就可以用后面的几种编码器。
2.如下是我们接下来将要使用的旋转编码器,左边是外观,右边是内部拆解的结构;可以看到内部是用金属触点进行通断的,所以它是一种机械触点式编码器,左右是两部分开关触点;中间银色圆形金素片为一个按键,这个旋转编码器的轴是可以按下去的,这种编码器一般是用来进行调节的,比如音响调节音量,因为它是触点接触的形式,所以不适合电机这种高速旋转的地方,另外三种都是非接触的形式,可以用于电机测速(电机测速在电机驱动的应用中还是很常见的)
下面为详细讲解旋转编码器的硬件部分:
金属触点
内侧的两根细的触点都是和中间的引脚c连接的,外侧触点一个连接A,一个连接B。
中间圆形金属片(按键)的两根线,就在上面引出来了;这个旋转编码器的轴是可以按下去的,按键的轴按下,上面两根线短路,松手,上面两根线断开,就是个普通的按键
轴的外侧是白色的编码盘,它也是一系列光栅一样的东西,只不过这是金属触点,在旋转时,依次接通和断开两边的触点;这个金属盘的位置是经过设计的,它能让两侧触点的通断产生一个90度的相位差,最终配合一下外部电路,这个编码器的两个输出就会输出如下这样的正交波形,带正交波形输出的编码器是可以用来测方向的(这就是单相输出和两相正交输出的区别),当然还有的编码器不是输出正交波形,而是一个引脚输出方波信号代表转速,另一个输出高低电平代表旋转方向,这种不是正交输出的编码器也是可以测方向的。
当正转时,A相引脚输出一个方波波形,B相引脚输出一个和它相位相差90的波形(正交波形),如下。
当反向旋转时,A相引脚还是方波信号,B相引脚会提前90度,如下。
3.霍尔传感器形式编码器,这种是直接附在电机后面的编码器,中间是一个圆形磁铁,边上有两个位置错开的霍尔传感器,当磁铁旋转时,通过霍尔传感器就可以输出正交的方波信号,如下。
4.这是独立的编码器元件,它的输入轴转动时,输出就会有波形,这个也是可以测速和测方向的,具体用法再看相应的手册。如下。
旋转编码器的硬件电路
模块的电路图如下,图中正方形区域就是旋转编码器,上面按键的两根线这个模块没有使用,是悬空的
下面为模块电路细节介绍:
这里是编码器内部的两个触点,旋转轴旋转时,这两个触点以相位相差90度的方式交替导通,因为这只是个开关信号,所以要配合外围电路才能输出高低电平
左边接了一个10k的上拉电阻,默认没旋转的情况下,这个点被上拉为高电平,再通过R3这个电阻输出到A端口的就也是高电平,当旋转时,内部触点导通,那C端口处就直接被拉低到GND,再通过R3输出,A端口就是低电平了,之后这个R3是一个输出限流电阻(是为了防止模块引脚电流过大的);C1是输出滤波电容,可以防止一些输出信号抖动。剩下的右边电路和左边是一样的。
使用这个模块时的接线如下,下面的A相输出和B相输出接到STM32的两个引脚上(注意GPIO_Pin引脚的编号不能一样),中间的C引脚就是GND,我们暂时不用。
接线图
对射式红外传感器,DO数字输出端随意选择一个GPIO口接上就行,这里接到了PB14引脚。当我们的挡光片或者编码盘在这个对射式红外传感器中间经过时,这个DO就会输出电平变化的信号,触发STM32 PB14号口的中断,我们在中断函数里执行变量++的程序,主循环里调用OLED显示这个变量。
旋转编码器
把这条信号电路给打通就行了。
第一步,配置RCC,将程序涉及的外设的时钟都打开
第二步,配置GPIO,选择端口为输入模式,手册上介绍3种都可以
第三步,配置AFIO,选择硬件所用用的哪一路GPIO,连接到后面的EXTI
涉及函数如下:
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
作用:配置AFIO的数据选择器,来选择我们想要的中断引脚。
参数说明:
第一个参数:选择某个GPIO外设作为外部中断源 第二个参数:指定要配置的外设中断线
当执行完这个函数,AFIO的第14个数据选择器就拨好了,其中输入端被拨到了GPIOB外设上,对应的就是PB14号引脚,输出端固定连接的是EXTI的第14个中断线路,这样PB14号引脚的电平信号就可以顺利通过AFIO,进入到后级EXTI电路了。
第四步,配置EXTI,选择边沿触发方式,比如上升沿、下降沿或者双边沿,还有选择触发响应方式,可以选择中断响应和事件响应。
涉及函数如下:
161行函数:用来软件触发外部中断,参数给一个指定的中断线,就能软件触发一次这个外部中断线,
如果只需要外部引脚触发中断,那就不需要这个函数了
外部中断来了,挂起寄存器会置了一个标志位,对于其他外设,比如串口收到数据会置标志位,定时器时间到也会置标志位,这些标志位都是放在状态寄存器里
162行函数,可以获取指定的标志位是否被置1了,
163行函数,对置1的标志位进行清除
对于这些标志位,有的比较紧急,在置标志位后会触发中断,在中断函数里,如果你想查看标志位和清除标志位,就用下面两个函数
164行函数,获取中断标志位是否被置1了
165行函数,清除中断挂起标志位
总结一下:
如果你想在主程序里查看和清除标志位,就用162和163行函数
如果想在中断函数里查看和清除标志位,就用164和165行函数
本质上,这四个函数都是对状态寄存器的读写,上面两个和下面两个都是类似的功能,都是读写状态寄存器的值
只不过下面两个函数只能读写与中断有关的标志位,并且对中断是否允许做出了判断
而上面两个函数只是一般的读写标志位,没有额外的处理,能不能触发中断的标志位都能读取
所以建议在主程序里用上面两个,中断程序里用下面两个,非要倒过来使用也没问题
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct)
作用:根据EXTI InitStruct中的指定参数初始化EXTI外设。
参数说明:
EXTI InitTypeDef结构体说明:
typedef struct
{
uint32_t EXTI_Line;
EXTIMode_TypeDef EXTI_Mode;
EXTITrigger_TypeDef EXTI_Trigger;
FunctionalState EXTI_LineCmd;
}EXTI_InitTypeDef;
参数说明以及举例
举例:
/* Enables external lines 12 and 14 interrupt generation on falling
edge */
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line12 | EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
第五步,配置NVIC,给我们这个中断选择一个合适的优先级,NVIC是内核外设,所以它的库函数在misc.h
196:用来中断分组的,参数是中断分组的方式
197:根据NVIC InitStruct中指定的参数初始化NVIC外设
198:设置中断向量表(用的不多)
199:系统低功耗配置(用的不多)
涉及函数如下:
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)
作用:配置优先级分组:抢占优先级和子优先级。
参数说明:
取值范围:
例如:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);// 选择第二个分组,2位抢占2位相应,比较平均一些
最后,通过NVIC,外部中断信号就能进入CPU了,这样CPU才能收到中断信号,才能跳转到中断函数里执行中断程序。
涉及函数如下:
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct)
作用:根据NVIC InitStruct中指定的参数初始化NVIC外设。
参数说明:
举例:
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
中断程序应该放在哪呢?这就需要我们写一个中断函数了,在STM32中,中断函数的名字都是固定的,每个中断通道都对应一个中断函数,中断函数的名字我们可以参考一下启动文件,中断函数的格式:
根据中断向量表,找到所需中断函数,这里面以IRQHandler结尾的字符串就是中断函数的名字,再根据名字写中断函数。
例如:void EXTI15_10_IRQHandler(void){ }
这就是中断函数的格式,中断函数都是无参无返回值的,中断函数的名字不要写错了,写错了就进不了中断了,最好是直接从启动文件复制过来,这样就不会有问题了。
注:启动文件为
然后在中断函数里,一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的这个函数,因为这个函数EXTI10到EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的。所用函数:EXTI_GetITStatus(uint32_t EXTI_Line)
最后,中断程序结束后,一定要再调用一下清除中断标志位的函数,因为只要中断标志位置1了,程序就会跳转到中断函数。如果你不清除中断标志位,那它就会一直申请中断,这样程序就会不断响应中断,执行中断函数,那程序就卡死在中断函数里了。所用函数:EXTI_ClearITPendingBit(uint32_t EXTI_Line)
中断函数就不用声明了,因为中断函数不需要调用,它是自动执行的。
其它涉及函数:
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line)
作用:检查指定的 EXTI 线路触发请求发生与否(是不是我们想要的中断触发源)
返回值:(SET或RESET)
参数说明:
void GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:读取指定端口管脚的输入
参数说明:
void EXTI_ClearITPendingBit(uint32_t EXTI_Line)
作用:清除EXTI线路挂起位
参数说明:
EXTI和NVIC两个外设,这两个外设的时钟是一直都打开着的,不需要我们再开启时钟了。EXIT模块是由NVIC模块直接控制的,并不需要单独的外设时钟。NVIC也不需要开启时钟,是因为NVIC是内核的外设,内核的外设都是不需要开启时钟的。
代码如下:
蓝线部分是我自己需要注意的地方
当我们的挡光片或者编码盘在这个对射式红外传感器中间经过时,这个DO就会输出电平跳变的信号,然后这个电平跳变的信号触发STM32 PB14号口的中断,我们在中断函数里,执行变量++的程序,然后主循环里用OLED显示这个变量,这样第一个程序就完成了。
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "countsensor.h"
int main(void)
{
countsensor_init();//初始化红外对射式模块计次
OLED_Init(); //初始化OLED
OLED_ShowString(1,1,"Count:");//第一行第三列开始显示字符串
while(1)
{
OLED_ShowNum(1,7,countsersor_get(),5);//显示countsersor_get的返回值,长度为5
}
}
countsensor.c
#include "stm32f10x.h" // Device header
uint16_t countsensor_count; //这个数字来统计中断触发的次数
//初始化函数,将模块要用的资源配置好
void countsensor_init(void)
{
//第一步,时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//EXTI和NVIC两个外设的时钟是一直开的 ,NVIC是内核外设都是不需要开启时钟,RCC管的都是内核外的外设
//EXTI作为一个独立外设,按理说是需要开启时钟的,但是寄存器里面却没有EXTI时钟的控制位,这个原因手册里没找到,网上也没有确切的答案
// 猜测是和EXTI唤醒有关,或者是其他的一些电路设计上的考虑
//第二步,配置GPIO
//首先定义结构体
GPIO_InitTypeDef GPIO_initstruct; //结构体名字GPIO_initstruct
//将结构体成员引出来
//对于EXTI来说,模式为浮空输入|上拉输入|下拉输入都可以;不知该写什么模式,可以看参考手册中的外设GPIO配置
GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU;// 上拉输入,默认高电平的输入方式
GPIO_initstruct.GPIO_Pin = GPIO_Pin_14;
GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
//最后初始化GPIO
GPIO_Init(GPIOB,&GPIO_initstruct); //传地址
//第三步,配置AFIO外设中断引脚选择
//ST公司给AFIO的库函数和GPIO在一个文件里,可以查看Library文件夹中的gpio.h最下面查看函数
//第一个参数:选择某个GPIO外设作为外部中断源 第二个参数:指定要配置的外设中断线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource14);//代表连接PB14号口的第14个中断线路
//第四步,配置EXTI,这样PB14的电平信号就能够通过EXTI通向下一级的NVIC了
EXTI_InitTypeDef EXTI_InitStructure;//结构体类型名EXTI_InitTypeDef,变量名EXTI_InitStructure
EXTI_InitStructure.EXTI_Line = EXTI_Line14;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//因为上面是GPIO_Mode_IPU设置为高电平,所以触发中断是下降沿
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//第五步,配置NVIC,NVIC是内核外设,所以它的库函数在misc.h
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组方式,整个芯片只能用一种。如放在模块中进行分组,要确保每个模块分组都选的是同一个;或者将这个代码放在主函数的最开始
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI15_10_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//抢占优先级 因为我们这个程序只有一个,所以中断优先级的配置也是非常随意的
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;//响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
uint16_t countsersor_get(void)
{
return countsensor_count;
}
//中断函数,都是无参无返回值的,名字固定在startup文件向量表中
//中断函数不需声明,它是自动执行的
void EXTI15_10_IRQHandler(void)
{
/*一般都是先进行一个中断标志位的判断,确保是我们想要的中断源触发的函数,因为这个函数EXTI10到
EXTI15都能进来,所以要先判断一下是不是我们想要的EXTI14进来的*/
if(EXTI_GetFlagStatus(EXTI_Line14) == SET)
{
countsensor_count++;
//每次中断函数结束后,都应该清除一下中断标志位
EXTI_ClearITPendingBit(EXTI_Line14);
}
}
countsensor.h
#ifndef __COUNTSENSOR_H
#define __COUNTSENSOR_H
void countsensor_init(void);
uint16_t countsersor_get(void);
#endif
5-2 旋转编码器计次
A,B相的输出引脚,分别接到STM32的PB0和PB1连个引脚,A、B相都触发中断
在写中断函数的核心思想:
只有在B相下降沿和A相低电平时,才判断为正转
在A相下降沿和B相低电平时,才判断为反转
在A相下降沿和B相低电平时,判断为反转。
代码如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "encoder.h"
int16_t num;
int main(void)
{
OLED_Init(); //初始化OLED
encoder_init();
OLED_ShowString(1,1,"num:");//第一行第三列开始显示字符串hello word!
while(1)
{
num += encoder_get();
OLED_ShowSignedNum(1,5,num,5);
}
}
encoder.c
#include "stm32f10x.h" // Device header
int16_t encoder_count;
void encoder_init(void)
{
//第一步,时钟配置
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE); //开启RCC时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //开启AFIO时钟
//EXTI和NVIC两个外设的时钟是一直开的 ,NVIC内核外设都是不需要开启时钟
//第二步,配置GPIO
//首先定义结构体
GPIO_InitTypeDef GPIO_initstruct; //结构体名字GPIO_initstruct
//将结构体成员引出来
//对于EXTI来说,模式为浮空输入|上拉输入|下拉输入;不知该写什么模式,可以看参考手册中的外设GPIO配置
GPIO_initstruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_initstruct.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_initstruct.GPIO_Speed = GPIO_Speed_50MHz;
//最后初始化GPIO
GPIO_Init(GPIOB,&GPIO_initstruct); //传地址
//第三步,配置AFIO外设中断引脚选择
//AFIO的库函数是和GPIO在一个文件里,可以查看Library文件中的gpio.h查看函数
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource0);//将第0个线路拨到GPIOB上
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB,GPIO_PinSource1);//将第1个线路拨到GPIOB上
//第四步,配置EXTI,这样PB14的电平信号就能够通过EXTI通向下一级的NVIC了
EXTI_InitTypeDef EXTI_InitStructure;
EXTI_InitStructure.EXTI_Line = EXTI_Line0 | EXTI_Line1;//第0条线路和第1条线路都初始化为中断模式、下降沿触发
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;//因为上面是GPIO_Mode_IPU设置为高电平,所以触发中断是下降
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
//第五步,配置NVIC,NVIC是内核外设,所以它的库函数在misc.h
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //分组方式,整个芯片只能用一种。如放在模块中进行分组,要确保每个模块分组都选的是同一个;或者将这个代码放在主函数的最开始
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
int16_t encoder_get(void)
{
int16_t temp;
temp = encoder_count;
encoder_count = 0;
return temp;
}
void EXTI0_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line0) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)
{
encoder_count--;
}
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
void EXTI1_IRQHandler(void)
{
if(EXTI_GetITStatus(EXTI_Line1) == SET)
{
if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_0) == 0)
{
encoder_count++;
}
EXTI_ClearITPendingBit(EXTI_Line1);
}
}
//如果你使用的是9_5和15_10这些中断,那只能写一个中断函数,将两个中断服务函数if并列放到一个函数里就行
encoder.h文章来源:https://www.toymoban.com/news/detail-781307.html
#ifndef __ENCODER_H
#define __ENCODER_H
void encoder_init(void);
int16_t encoder_get(void);
#endif
中断程序注意事项:
第一:在中断函数里,最好不要执行耗时过长的代码,中断函数要简短快速,别刚进中断就执行一个Delay多少毫秒这样的代码,因为中断是处理突发的事情,如果你为了一个突发的事情待着中断里不出来了,那主程序就会受到严重的阻塞。
第二:最好不要在中断函数和主函数调用相同的函数或者操作同一个硬件,尤其是硬件相关的函数,比如OLED显示函数,硬件会显示错误。比如,在主程序里OLED刚显示一半,突然进中断了,结果中断里还是OLED显示函数,呢OLED就挪到其他位置显示了,这时还没有问题,但中断结束后,需要继续原来的显示,这就出问题了,因为硬件的显示位置被挪到其他地方了,所以再回来的时候,继续显示的内容就会跟着跑到其他地方去。虽然在中断进入和退出的时候,会有保护现场和恢复现场,但这只能保证CPU程序能正常返回不出问题,对于外部硬件的话,并没有在进入中断时进行现场保护,所以中断返回后就出问题了。因此最好不要在主程序和中断程序里,操作可能产生冲突的硬件。在实现功能的时候,可以相本节代码这样,在中断里操作变量或者标志位,当中断返回时,再对这个变量进行显示和操作,这样既能保证中断函数的简短快速,又能保证不产生冲突的硬件操作。文章来源地址https://www.toymoban.com/news/detail-781307.html
到了这里,关于江科大STM32的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!