江协科技STM32学习笔记(第02章 GPIO通用输入输出)

第02章 GPIO通用输入输出

01 GPIO输出

1.1 GPIO简介

(1)GPIO(General Purpose Input Output)通用输入输出口

(2)可配置为8种输入输出模式

(3)引脚电平:0V~3.3V,部分引脚可容忍5V

数据0就是低电平,也就是0V,数据1就是高电平,也就是3.3V。容忍5V就是指可以在这个端口输入5V的电压,也认为是高电平,但是对于输出而言,最大就只能输出3.3V,因为供电就只有3.3V。具体哪些端口可以容忍5V,可以参考引脚定义。

(4)输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。

在其它的应用场景,只要是可以用高低电平来控制的,都可以用GPIO来完成,如果控制的是功率比较大的设备,只需要加个驱动电路即可。用GPIO可模拟通信协议(比如I2C,SPI或者某个芯片特定的协议),都可以用GPIO的输出模式来模拟其中的输出时序部分。

(5)输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。

输入模式最常见的就是读取按键,用来捕获按键按下事件。另外也可以读取带有数字输出的一些模块,比如光敏电阻模块、热敏电阻模块等。如果这个模块输出的是模拟量,那GPIO还可以配置成模拟输入的模式,再配合内部的ADC外设,就能直接读取端口的模拟电压了。除此之外,模拟通信协议时,接收通信线上的数据,也是靠GPIO的输入来完成的。

1.2 GPIO基本结构

在STM32中,所有SPIO都是挂载在APB2外设总线上的,其中GPIO外设的名称是按照GPIOA、GPIOB、GPIOC等等这样来命名的,每个GPIO外设,总共有16个引脚,编号是从0到15。那GPIOA的第0号引脚,我们一般把它称作PA0,接着第1号就是PA1,然后PA2,依次类推,直到PA15。

在每个GPIO模块内,主要包含了寄存器和驱动器这些东西,寄存器就是一个特殊的存储器,内核通过APB2总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了。这个寄存器的每一位对应一个引脚,其中输出寄存器写1,对应的引脚就会输出高电平,写0输出低电平;输入寄存器读取为1,就证明对应的端口目前是高电平,读取为0就是低电平。

因为STM32是32位的单片机,所以STM32内部的寄存器都是32位的。但这个端口只有16位,所以只有低16位对应的有端口,高16位是没有用到的。

驱动器是用来增加信号的驱动能力的,寄存器只负责存储数据。如果要进行点灯这样的操作的话,还是需要驱动器来负责增大驱动能力。

1.3 GPIO位结构

整体结构分为两个部分,上面输入部分,下面输出部分。

IO引脚,接了两个保护二极管,这个是对输入电压进行限幅的。上面二极管接VDD、3.3V,下面接VSS、0V。

如果输入电压比3.3V还要高,那上方的二极管就会导通,输入电压产生的电流就会直接流入VDD而不会流入内部电路,就可以避免过高的电压对内部电路产生伤害。

如果输入电压比0V还要低,这个电压是相对于VSS的电压,所以是可以有负电压的,那么下方这个二极管导通,电流会直接从VSS流出去,而不会从内部电路汲取电流,也是可以保护内部电路的。

如果输入电压在0~3.3V之间,那两个二极管均不会导通,这时二极管对电路没有影响,这就是保护二极管的用途。

1.3.1 输入部分

①上拉和下拉电阻

输入部分接了一个上拉电阻和一个下拉电阻,上拉电阻至VDD、下拉电阻至VSS,开关是可以通过程序进行配置的,如果上面导通、下面断开,就是上拉输入模式;如果下面导通、上面断开,就是下拉输入模式;如果两个都断开,就是浮空输入模式。

上拉和下拉的作用其实是为了给输入提供一个默认的输入电平的, 因为对于一个数字端口,输入不是高电平就是低电平,那如果输入引脚啥都不接,那到底算高电平还是低电平。这就不好说了,实际情况是,如果输入啥都不接,这时候输入就会处于一种悬空的状态,引脚的输入电平极易受外界干扰而改变,为了避免引脚悬空导致的输入数据不确定,我们就需要在这里加上上拉或者下拉电阻了。如果接入上拉电阻,当引脚悬空时,还有上拉电阻来保证引脚的高电平,所以上拉电阻又可以称作是默认位高电平的输入模式;下拉也是同理,就是默认位低电平的输入方式。上拉电阻和下拉电阻的阻值都是比较大的,是一种弱上拉和弱下拉,目的是尽量不影响正常的输入操作。 

②肖特基触发器

实际上应该是施密特触发器,这里写肖特基触发器应该是翻译错误。这个施密特触发器的作用就是对输入电压进行整型的。它的执行逻辑是,如果输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平。

举例:IO引脚的波形是外界输入的,虽然是数字信号,实际情况下可能会产生各种失真。下图红色为输入信号,蓝色为整形后的信号。

一个夹杂了波动的高低变化电平信号,如果没有施密特触发器,那很有可能因为干扰而导致误判。如果有了施密特触发器,那比如定一个这样的阈值上限和下限,高于上限输出高,低于下限输出低。假设由于信号波动导致某一时刻低于上限单未低于下限,对于施密特触发器来说,只有高于上限或者低于下限,输出才会变化,所以此时输出并不会变化。可以看出,相比较输入信号,经过整形以后的信号就很完美了。在这里使用两个比较阈值来进行判断,中间留有一定的变化范围,可以有效地避免因信号波动造成地输出抖动现象。

经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,再用程序读取输入寄存器的输入寄存器对应的某一位数据,就可以知道端口的输入电平了。

③片上外设

上面还有两路线路,这些就是连接到片上外设的一些端口。其中有模拟输入,这个是连接到ADC上的,因为ADC需要接收模拟量,所以这根线是接到施密特触发器前面的。另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的数字量,所以在施密特触发器后面。

1.3.2 输出部分

数字部分可以由输出寄存器或片上外设控制,两种控制方式通过数据选择器接到了输出控制部分,如果选择通过输出寄存器进行控制,就是普通的IO口输出,写这个寄存器的某一位就可以操作对应的某个端口了。左边还有一个叫做位设置/清除寄存器,可以用来单独操作输出数据寄存器的某一位,而不影响其它位。因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其它端口的话,就需要一些特殊的操作方式。第一种方式是先读出这个寄存器,然后用按位与和按位或的方式更爱某一位,最后再将更改后的数据写回去,在C语言中就是&=|=的操作,这种方法比较麻烦,效率不高,对于IO口的操作而言不太合适。第二种方式就是通过设置位设置/清除寄存器,如果我们要对某一位进行置1的操作,在位设置寄存器的对应位写1即可,剩下不需要操作的写0,这样它内部就会有电路,自动将输出数据寄存器对应位置置为1,而剩下写0的位则保持不变,这样就保证了只操作其中某一位而不影响其它位,并且这是一步到位的操作,如果想对某一位进行清0的操作,就在位清除寄存器的对应位写1即可,这样内部就会把这一位清0了,这就是第二种方式也就是位设置/清除寄存器的作用。另外还有第三种操作,就是读写STM32中的“位带”区域,这个位带的作用跟51单片机的位寻址作用差不多,在STM32中,专门分配的有一段地址区域,这段地址映射了RAM和外设寄存器的所有位,读写这段地址中的数据,就相当于读写所映射位置的某一位,这就是位带的操作方式。库函数使用就是读写位设置/清除寄存器的方法。

输出控制之后接到了两个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,也就是输出低电平;这种模式下只有低电平有驱动能力,高电平没有是没有驱动能力的;这个开漏模式可以作为通信协议的驱动方式,比如I2C通信的引脚,就是使用的开漏模式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰;另外开漏模式还可以用于输出5V的电平信号,比如在IO口外接一个上拉电阻到5V的电源,当输出低电平时,由内部的N-MOS直接接VSS,当输出高电平时,由外部的上拉电阻拉高至5V,这样就可以输出5V的电平信号,用于兼容一些5V电平的设备,这就是开漏输出的主要用途。剩下的一种状态就是关闭,这个是当引脚配置为输入模式的时候,这两个MOS管都无效,也就是输出关闭,端口的电平由外部信号来控制。

1.4 GPIO工作模式

通过配置GPIO的端口配置寄存器,端口可以配置成以下8种模式:

模式名称

性质

特征

浮空输入

数字输入

可读取引脚电平,若引脚悬空,则电平不确定

上拉输入

数字输入

可读取引脚电平,内部连接上拉电阻,悬空时默认高电平

下拉输入

数字输入

可读取引脚电平,内部连接下拉电阻,悬空时默认低电平

模拟输入

模拟输入

GPIO无效,引脚直接接入内部ADC

开漏输出

数字输出

可输出引脚电平,高电平为高阻态,低电平接VSS

推挽输出

数字输出

可输出引脚电平,高电平接VDD,低电平接VSS

复用开漏输出

数字输出

由片上外设控制,高电平为高阻态,低电平接VSS

复用推挽输出

数字输出

由片上外设控制,高电平接VDD,低电平接VSS

浮空输入、上拉输入、下拉输入这三个模式的电路结构基本是一样的,区别就是上拉电阻和下拉电阻的连接,都可以读取端口的高电平。浮空输入的电平是不确定的,所以在使用浮空输入时,端口一定要接上一个连续的驱动源,不能出现悬空的状态。

1.4.1 浮空/上拉/下拉输入

在输入模式下,输出驱动器是断开的,端口只能输入而不能输出,上面两个电阻可以选择为上拉工作、下拉工作或者都不工作。对应的就是上拉输入、下拉输入或浮空输入,然后通过施密特触发器进行波形整形后,连接到输入数据寄存器。另外输入保护那里写着VDD或VDD_FT,这就是3.3V端口和容忍5V端口的区别。这个容忍5V的引脚,它的保护二极管要做一下处理,要不然直接接VDD3.3V的话,外部再接入5V电压就会导致上边二极管开启,并且产生比较大的电流,这个是不太妥当的。

1.4.2 模拟输入

ADC模数转换器的专属配置。输出是断开的,输入的施密特触发器也是关闭的无效状态,只剩下从引脚直接接入片上外设,也就是ADC,所以当我们使用ADC的时候,将引脚配置为模拟输入就行了,其他时候一般用不到模拟输入。

1.4.3 开漏/推挽输出 

输出是由输出数据寄存器控制的,P-MOS无效,就是开漏输出,如果P-MOS和N-MOS都有效,就是推挽输出。另外在输出模式下,输入模式也是有效的 ,在输入模式下,输出是无效的,这是因为一个端口只能有一个输出,但可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入以下,这个也是没啥影响的。

1.4.4 复用开漏/推挽输出

这两模式跟普通的开漏输出和推挽输出也差不多, 只不过是复用的输出,引脚电平是由片上外设控制的。

引脚的控制权转移到了片上外设,由片上外设来控制,在输入部分,片上外设也可以读取引脚的电平,同时普通的输入也是有效的,顺便接收一下电平信号。

在GPIO的8中模式中,处理模拟输这个模式会关闭数字的输入功能,在其它的7个模式中,素有的输入都是有效的。

1.5 STM32数据手册-GPIO

GPIO配置寄存器,每一个端口的模式由4位进行配置, 16个端口就需要64位,所以这里的配置寄存器有2个,一个是端口配置低寄存器,另一个是端口配置高寄存器。

这个 GPIO的输出速度可以限制输出引脚的最大翻转速度,这个设计出来是为了低功耗和稳定性的,一般要求不高的时候直接配置成MHz就可以了。

端口输入数据寄存器,里面的低16位对应16个引脚,高16位没有使用。

端口输出数据寄存器,里面的低16位对应16个引脚,高16位没有使用。 

端口位设置/清除寄存器:这个寄存器的高16位是进行位清除的,低16位是进行位设置的。 写1就是设置或者清除,写0就是不产生影响。

端口位清除寄存器:这个寄存器的低16位和端口位设置/清除寄存器高16位功能是一样的 ,是为了方便操作设置的。如果只想单一的进行位设置或者位清除,那位设置时,用端口位设置/清除寄存器,位清除时,用端口位清除寄存器。因为在设置和清除时,使用的都是低16位的数据,就会方便一些。如果想对多个端口同时进行位设置和位清除,那就使用端口位设置/清除寄存器,这样就可以保证设置和位清除的同步性。当然如果对信号的同步性要求不高的话,先位设置再位清除也是没有问题的。

可以对端口的配置进行锁定,防止意外更改。 

02 STM32外部设备和电路

2.1 LED和蜂鸣器

(1)LED:发光二极管,正向通电点亮,反向通电不亮。

如果是引脚没有剪过的LED,那其中长脚是正极、短脚是负极,通过LED内部也可以判断,较小的一半是正极,较大的一半是负极。

(2)有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定。

(3)无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。

2.2 LED的硬件电路

使用STM32的GPIO口驱动LED的电路:

上面是低电平驱动的电路,LED正极接3.3V,负极通过一个限流电阻接到PA0上;

限流电阻一般都是要接的,一方面防止LED因为电流过大而烧毁,另一方面它可以调整LED的亮度。

下面是高电平驱动的电路。

两种方式如何选择就得看IO口高低电平的驱动能力如何了,这个IO口载推挽输出模式下,高低电平均有比较强的驱动能力,所以在这里,两种方法均可。但是在单片机电路里,一般倾向于第一种接法,因为很多单片机或者芯片,都使用了高电平弱驱动,低电平强驱动的规则,这样可以一定程度上避免高低电平打架,所以如果高电平驱动能力弱,那就不能用用第二种连接方法了。

2.3 蜂鸣器电路 

 使用了三极管开关的驱动方案,三极管开关是最简单的驱动电路了,对于功率稍微大一点的,直接用IO口驱动就会导致STM32负担过重,这是就可以用一个三极管驱动电路来完成驱动的任务。

上面是PNP三极管的驱动电路,三极管的左边是基极,带箭头的是发射极,剩下的是集电极,它左边的基极给低电平,三极管就会导通,通过3.3V和GND,就可以给蜂鸣器提供驱动电流了;基极给高电平,三极管截至,蜂鸣器就没有电流i。

下面这个图是NPN三极管驱动电路,同样左边是基极,带箭头的是发射极,剩下的是集电极,驱动逻辑跟上面是相反的。基极给高电平导通,低电平断开。

PNP的三极管最好接在上边,NPN的三极管最好接在下边,这是因为三极管的通断,是需要在发射极和基极直接产生一定的开启电压的,如果把负载接在发射极那边,可能会导致三极管不能开启。

03 LED闪烁

3.1 硬件电路

3.2 新建工程

略,参考第一章 。注意:通过以下方式可快速创建文件分组。

 3.3 实用工具

该文件可以把工程编译产生的中间文件都删掉,把它复制到工程文件夹里, 工程产生的中间文件主要在“Listings”和“Objects”文件夹下,而且都比较大,使用这个批处理文件可快速把中间文件都删掉。

3.4 软件实现 

3.4.1 RCC外设

打开Library文件夹下“_rcc.h”文件,在.h文件的最下面,一般都是库函数所有函数的声明。RCC提供了很多库函数,最常用的是以下三个库函数(RCC AHB外设时钟控制、RCC APB2外设时钟控制、RCC APB1外设时钟控制):

3.4.2 GPIO外设 

下图是GPIO全部的库函数,目前需要了解的是粉红色线框内的函数。

GPIO8种模式:

typedef enum
{ GPIO_Mode_AIN = 0x0,            //模拟输入
  GPIO_Mode_IN_FLOATING = 0x04,   //浮空输入
  GPIO_Mode_IPD = 0x28,           //下拉输入
  GPIO_Mode_IPU = 0x48,           //上拉输入
  GPIO_Mode_Out_OD = 0x14,        //开漏输出
  GPIO_Mode_Out_PP = 0x10,        //推挽输出      
  GPIO_Mode_AF_OD = 0x1C,         //复用开漏
  GPIO_Mode_AF_PP = 0x18          //复用推挽
}GPIOMode_TypeDef;

GPIO写入函数:

void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);     //把指定端口设置为高电平
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);   //把指定端口设置为低电平
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal); //前两个参数指定端口、第三个是根据参数的值来设置指定的端口
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal); //第一个参数选择外设,第二个参数是PortValue,这个函数可以同时对16个端口进行写入操作

 要实现LED闪烁,需要延时功能。在工程种新建一个“System”文件夹,将下图文件复制进去。

接PA0的LED闪烁程序如下: 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);  //配置RCC_APB2时钟,因为GPIO外设都是挂载在APB2上的,引脚PA0,使能后开启时钟
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出工作模式
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;                //引脚0
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;        //输入输出速度50MHz
	GPIO_Init(GPIOA,&GPIO_InitStruct);           //GPIOA外设的初始化,使用一个结构体定义了GPIO的工作模式、引脚、输入输出速度
//	GPIO_SetBits(GPIOA,GPIO_Pin_0);              //将GPIO外设引脚0设置为高电平
//	GPIO_ResetBits(GPIOA,GPIO_Pin_0);            //将GPIO外设引脚0设置为低电平
	
	while(1)
	{
//		GPIO_ResetBits(GPIOA,GPIO_Pin_0);           //将GPIO外设引脚0设置为低电平,点亮LED
//		Delay_ms(500);                              //延时500ms
//		GPIO_SetBits(GPIOA,GPIO_Pin_0);             //将GPIO外设引脚0设置为高电平,熄灭LED
//		Delay_ms(500);                              //延时500ms
//		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_RESET);  //将PA0设置为低电平,点亮LED
//		Delay_ms(500);                              //延时500ms
//		GPIO_WriteBit(GPIOA,GPIO_Pin_0,Bit_SET);    //将PA0设置为高电平,熄灭LED
//		Delay_ms(500);                              //延时500ms
		/*如果想用0和1表示高低电平,需要用BitAction将0和1强制转换为枚举类型*/
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)0);     //将PA0设置为低电平,点亮LED
		Delay_ms(500);                                    //延时500ms
		GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)1);     //将PA0设置为高电平,熄灭LED
		Delay_ms(500);  
	}
}

将LED正负极调转一下可以测试推挽输出模式下高低电平都有较强的驱动能力,将GPIO工作模式设置为开漏输出模式,可验证开漏输出模式下高电平不具备驱动能力、低电平具备驱动能力。 

04 LED流水灯

4.1 硬件电路

4.2 软件部分

这里我们需要配置PA0-PA7八个引脚,可按如下操作:

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;      //按位或

原因如下:

#define GPIO_Pin_0                 ((uint16_t)0x0001)  /*!< Pin 0 selected */
#define GPIO_Pin_1                 ((uint16_t)0x0002)  /*!< Pin 1 selected */
#define GPIO_Pin_2                 ((uint16_t)0x0004)  /*!< Pin 2 selected */
GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2相当于0000 0000 0000 0001 | 0000 0000 0000 0010 | 0000 0000 0000 0100   -->0000 0000 0000 0111

处了GPIO引脚可以用这种方式外,时钟控制也可以用这种方式选择多个外设。

接PA0-PA7的LED流水灯程序如下: 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);  //配置RCC_APB2时钟,因为GPIO外设都是挂载在APB2上的,引脚PA0,使能后开启时钟
	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;              //所有位都位1,相当于选择中了所有的引脚	
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;        //输入输出速度50MHz
	GPIO_Init(GPIOA,&GPIO_InitStruct);           //GPIOA外设的初始化,使用一个结构体定义了GPIO的工作模式、引脚、输入输出速度

	
	while(1)
	{
		GPIO_Write(GPIOA,~0x0001);//第二个参数需要直接写到GPIO的ODR寄存器里的,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);
	}
}

05 蜂鸣器 

5.1 硬件电路

注意:这款芯片A15、B3、B4这几个端口先别选 ,从引脚定义可知,这三个是JTAG的调试端口,如果要用做普通IO口,还需要再进行一些配置。

5.2 软件部分 

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件

int main(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);  //配置RCC_APB2时钟,因为GPIO外设都是挂载在APB2上的,引脚PB12,使能后开启时钟
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;         //推挽输出工作模式
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_12;                //引脚0
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;        //输入输出速度50MHz
	GPIO_Init(GPIOB,&GPIO_InitStruct);           //GPIOB外设的初始化,使用一个结构体定义了GPIO的工作模式、引脚、输入输出速度
	
	while(1)
	{
		GPIO_ResetBits(GPIOB,GPIO_Pin_12);           //将GPIO外设引脚12设置为低电平,蜂鸣器发出声音
		Delay_ms(100);                              
		GPIO_SetBits(GPIOB,GPIO_Pin_12);             //将GPIO外设引脚0设置为高电平,蜂鸣器停止发出声音
		Delay_ms(100);                              
		GPIO_SetBits(GPIOB,GPIO_Pin_12);             //将GPIO外设引脚0设置为高电平,蜂鸣器停止发出声音
		Delay_ms(100);                              
		GPIO_SetBits(GPIOB,GPIO_Pin_12);             //将GPIO外设引脚0设置为高电平,蜂鸣器停止发出声音
		Delay_ms(700);                              
	}
}

06 GPIO输入 

6.1 按键简介

按键:常见的输入设备,按下导通,松手断开。

按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。

假设按键没按下是高电平,按下为低电平,在按下的瞬间, 信号由高电平变为低电平时,就会来回的抖动几下,这个抖动比较快,通常在5~10ms之间,人眼是分辨不出来的,但是对于告诉运行的单片机而言,5~10ms的时间还是很漫长的,所以要对这个抖动进行过滤,否则就会出现按键按一下,单片机却反应了多次的现象。另外在按键松手的时候,也会有一段时间的抖动,这个我们在程序中也要注意过滤一下。最简单的过滤办法就是加一段延时,把这个抖动的时间耗过去,这样就没问题了。

6.2 传感器模块介绍

传感器模块:传感器元件(光敏电阻/热敏电阻/红外接收管等)的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。

下图四个传感器分别是:光敏电阻传感器、热敏电阻传感器、对射式红外传感器、反射式红外传感器。

第三部分电路:

 N1就是传感器元件所代表的可变电阻,它的阻值可以根据环境的光线、温度等模拟量进行变化。上面的R1是和N1进行分压的定值电阻,R1和N1串联,一端接在VCC正极、一端接在GND负极。这就构成了基本的分压电路,左边的C2是一个滤波电容,它是为了给中间的电压输出进行滤波的,用来滤除一些干扰,保证输出电压的平滑。一般我们在电路里遇到这种一端接在电路中,另一端接地的电容,都可以考虑一下这个是不是滤波电容的作用,如果是滤波电容的作用,那这个电容就是用来保证电路稳定的,并不是电路的主要框架,这时候我们在分析电路的时候,就可以把这个电路先抹掉,这样就可以使我们的电路分析更加简单。我们把电容给抹掉以后,整个电路的主要框架就是定值电阻和传感器电阻的分压电路了,在这里可以用分压定理来分析一下传感器电阻的阻值变化对输出电压的影响,当然还可以用上下拉电阻的思维来分析。当这个N1阻值变小时,下拉作用就会增强,中间的AO端的电压就会拉低;极端情况下,N1阻值为0,AO输出被完全下拉,输出0V;当N1阻值变大,下拉作用减弱,中间的引脚由于R1的上拉作用,电压就会升高;极端情况下,N1阻值无穷大,相当于断路,输出电压被R1拉高至VCC。

AO就是我们想要的模拟电压输出了。

第一部分电路:

模块还支持有数字输出,这个数字输出就是对AO进行二值化的输出。这个二值化是通过LM393来完成的,这个LM393是一个电压比较器芯片,里面有两个独立的电压比较器电路,然后剩下的是VCC和GND供电,VCC接到了电路的VCC,GND也接到了电路的GND。

左边还有一个电容,是一个电源供电的滤波电容。这个电压比较器其实就是一个运算放大器。当同相输入端的电压大于反相输入端的电压时,输出就会瞬间升为最大值也就是输出接VCC,反之当同相输入端的电压小于反相输入端的电压时,输出就会瞬间降为最小值,也就是输出接GND,这样就可以对一个模拟电压进行二值化了。

同相输入端IN+接到AO,就是模拟电压端,IN-接了一个电位器,这个电位器的接法也是分压电阻的原理,拧动电位器,IN-就会生成一个可调的阈值电压,两个电压进行比较,最终输出结果就是DO,数字电压输出,DO最终接到了引脚的输出端,这就是数字电压的由来。

第四部分电路:

还有两个指示灯电路,左边LED1的是电源指示灯,通电就亮;右边的LED2是DO输出指示灯,它可以指示DO的输出电平,低电平点亮、高电平熄灭,右边DO这里还多了个R5上拉电阻,这是为了保证默认输出为高电平的。P1的排针分别是VCC、GND、DO和AO。

对于光敏电阻传感器来说,N1就是光敏电阻;对于热敏电阻传感器来说,N1就是热敏电阻;对于红外传感器来说,N1就是红外接收管,当然还会多一个点亮红外发射管的电路,发射管发射红外光,接收管接收红外光。模拟电压表示的是接收光的强度。

对射式红外传感器通常用来检测通断,所以阈值也不需要过多的调整。、

反射式红外传感器也是一个红外发射管和一个红外接收管,只不过它是向下发射红外光,然后检测反射光的。这个可以用来做循迹小车。

6.3 按键和传感器模块硬件电路

(1)按键硬件电路

上面两种是下接按键的方式,下面两个是上接按键的方式,一般来说我们的按键都是用上两种方式,也就是下接的方式 。原因跟LED的接法类似,是电路设计的习惯和规范。

第一个图是电源按键的最常用的接法了,在这里随便选取一个GPIO口,比如PA0,然后通过K1接到地,当按键按下时,PA0直接下拉到GND,此时读取PA0的电压就是低电平。当按键松手时,PA0被悬空,悬空后会导致引脚的电压不确定,因此这时候必须要求PA0是上拉输入的模式(在内部接一个上拉电阻),否则就会出现引脚电压不确定的错误现象。如果PA0是上拉输入模式,引脚再悬空,PA0就是高电平。所以这种方式下,按下按键,引脚为低电平,松手,引脚为高电平。

第二个图相比于第一个图在外部接了一个上拉电阻,当按键松手时,引脚由于上拉作用,自然保持为高电平,当按键按下时,引脚直接接到GND,也就是一股无穷大的力把这个引脚往下拉,引脚就为低电平。这种状态下,引脚不会出现悬空状态,所以此时PA0引脚可以配置为浮空输入或者上拉输入。如果是上拉输入,那就是内外两个上拉电阻共同作用了,这是高电平就会更强一些,对应高电平更加稳定,但是这样的话,当引脚被强行拉到低时,损耗也会大一些。

第三个图,PA0通过按键接到3.3V,这样也是可以的,不过要求PA0必须要配置成下拉输入的模式,当按键按下时候,引脚为高电平,松手时,引脚回到默认值低电平,这要求单片机的引脚可以配置为下拉输入模式,一般单片机可能不一定有下拉输入的模式,所以最好还是用上面的接法。

最后一种方法需要PA0需要配置为下拉输入模式或者浮空输入模式。

总结:上面两种接法按键按下是低电平,松手是高电平;下面两种接法按键按下时是高电平,松手是低电平。左边两种接法要求引脚必须是上拉或者下拉输入的模式,右边两种接法可以允许引脚是浮空输入的模式,因为已经外置了上拉电阻和下拉电阻。一般都用上面两种接法,下面两种接法用的比较少。

(2)传感器硬件电路

因为是使用模块的方案,所以电路还是非常简单的。 DO数字输出随便接一个端口,比如PA0,用于读取数字量,AO模拟输出呢,之后学习ADC模数转换的时候使用,暂时不用接。

07 C语言学习补充

7.1 C语言数据类型

关键字

位数

表示范围

stdint关键字

ST关键字

char

8

-128 ~ 127

int8_t

s8

unsigned char

8

0 ~ 255

uint8_t

u8

short

16

-32768 ~ 32767

int16_t

s16

unsigned short

16

0 ~ 65535

uint16_t

u16

int

32

-2147483648 ~ 2147483647

int32_t

s32

unsigned int

32

0 ~ 4294967295

uint32_t

u32

long

32

-2147483648 ~ 2147483647

unsigned long

32

0 ~ 4294967295

long long

64

-(2^64)/2 ~ (2^64)/2-1

int64_t

unsigned long long

64

0 ~ (2^64)-1

uint64_t

float

32

-3.4e38 ~ 3.4e38

double

64

-1.7e308 ~ 1.7e308

(1)需要注意的是,在51单片机中,int是占16位的,在STM32中int是占32位的,如果要用16位的数据,要用short来表示。 

(2)stdint关键字和ST关键字是对这些变量的重命名,因为左边的名字比较长,而且这个int的位数根据系统的不同还有可能不一样,还有的时候这个名字会有名不对题(比如char本意是字符型数据的意思,按名字来说它应该是存放字符的,但单片机中通常用它来存放整数而不是字符),所以stdint关键字和ST关键字给这些变量换了个名字。C语言提供的有stdint这个头文件,使用了新的名字,比如int8_t就是char的新名字,表示的意思就是8位整型数据,右边加个_t表示这是用typeddef重新明名的变量类型。

ST关键字是老版本库函数的命名方式,不过目前新版本库函数仍可以使用,建议使用stdint关键字,这是C语言stdint.h头文件里提供的官方定义。

7.2 C语言宏定义

关键字:#define

用途:用一个字符串代替一个数字,便于理解,防止出错;提取程序中经常出现的参数,便于快速修改

定义宏定义:

#define ABC 12345

 引用宏定义:

  int a = ABC;  //等效于int a = 12345;

7.3 C语言typedef

关键字:typedef

用途:将一个比较长的变量类型名换个名字,便于使用

定义typedef

typedef unsigned char uint8_t;

引用typedef:

uint8_t a;	//等效于unsigned char a;

7.4 宏定义和typedef的区别 

宏定义的新名字在左边,typedef的新名字在右边;宏定义不需要分号,typedef后面必须加分号;宏定义任何名字都可以换,而typedef只能专门给变量类型换名字。对于变量类型重命名而言,使用typedef更加安全。

7.5 C语言结构体

数组只能组合相同类型的数据,于是结构体就出现了,可组合不同类型的数据。

关键字:struct

用途:数据打包,不同类型变量的集合

定义结构体变量:

struct{char x; int y; float z;} StructName;

因为结构体变量类型较长,所以通常用typedef更改变量类型名

引用结构体成员:

StructName.x = 'A';

StructName.y = 66;

StructName.z = 1.23;

pStructName->x = 'A';  //pStructName为结构体的地址  
pStructName->y = 66;
pStructName->z = 1.23;

7.6 C语言枚举 

关键字:enum

用途:定义一个取值受限制的整型变量,用于限制变量取值范围;宏定义的集合

定义枚举变量:

enum{FALSE = 0, TRUE = 1} EnumName;

因为枚举变量类型较长,所以通常用typedef更改变量类型名

引用枚举成员:

EnumName = FALSE;
EnumName = TRUE;

08 按键控制LED 

8.1 硬件电路

8.2 软件部分 

在这个工程下完成按键和LED的驱动代码,但是如果把这两部分代码都混在主函数里面,代码会比较混乱,不容易管理,也不容易移植,所以对于这种驱动代码而言,我们一般会把它封装起来。单独放在另外的.c和.h文件里,这就是模块化编程的方式。

(1)在工程目录下新建“Hardware”文件夹,用来存放硬件驱动程序。

(2)编写硬件驱动程序

①LED驱动程序

LED.c文件用来存放LED驱动程序的主体代码,LED.h用来存放这个驱动程序可以对外提供的函数或者变量的声明。

 keil如果不显示代码提示,可以按下“Ctrl+Alt+空格”,这样就可以显示代码提示框了。

 ②按键驱动程序

(3) LED.c

#include "stm32f10x.h"                  // Device header

/*LED初始化函数*/
void LED_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);        //开启时钟,LED接在PA1和PA2引脚,GPIOA端口,GPIO外设挂载在APB2上
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_SetBits(GPIOA,GPIO_Pin_1|GPIO_Pin_2);                   //将PA1和PA2引脚置位高电平,不操作的时候LED熄灭     
}

/*点亮LED1(PA1)的函数*/
void LED1_ON(void)
{ 
	GPIO_ResetBits(GPIOA,GPIO_Pin_1);
}

/*熄灭LED1(PA1)的函数*/
void LED1_OFF(void)
{ 
	GPIO_SetBits(GPIOA,GPIO_Pin_1);
}

/*LED1(PA1)端口电平翻转函数*/
void LED1_Turn(void)
{
	if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_1)==0)         //如果GPIO_Pin_1的输出寄存器为0,此时LED1点亮,
	{
		GPIO_SetBits(GPIOA,GPIO_Pin_1);
	}
	else
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_1);
	}
}

/*点亮LED2(PA2)的函数*/
void LED2_ON(void)
{ 
	GPIO_ResetBits(GPIOA,GPIO_Pin_2);
}

/*熄灭LED2(PA2)的函数*/
void LED2_OFF(void)
{ 
	GPIO_SetBits(GPIOA,GPIO_Pin_2);
}
	
/*LED2(PA11)端口电平翻转函数*/
void LED2_Turn(void)
{
	if(GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_2)==0)         //如果GPIO_Pin_11的输出寄存器为0,此时LED2点亮
	{
		GPIO_SetBits(GPIOA,GPIO_Pin_2);
	}
	else
	{
		GPIO_ResetBits(GPIOA,GPIO_Pin_2);
	}
}

 (2)LED.h

#ifndef __LED_H         
#define __LED_H        /*如果没有定义__LED_H这个字符串,那么就定义__LED_H这个字符串*/
void LED_Init(void);
void LED1_ON(void);
void LED1_OFF(void);
void LED1_Turn(void);
void LED2_ON(void);
void LED2_OFF(void);
void LED2_Turn(void);
#endif                 //下方有一条空行,文件最后一行一定要以空行结尾,不然会警告

 (3)Key.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"

/*按键初始化函数*/
void Key_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_1|GPIO_Pin_11;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    //GPIO输出速度,在输入模式下,实际上没用的
	GPIO_Init(GPIOB,&GPIO_InitStruct);
}

/*获取按下按键的键码*/
uint8_t Key_GetNum(void)
{
	uint8_t KeyNum = 0;
	if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0)        //读取GPIO_Pin_1端口值如果等于0,代表按键按下   
	{
		Delay_ms(20);                                       //延时20ms,消抖  
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_1) == 0) //20ms后GPIO_Pin_1端口值如果等于0,那么按键确实按下
		Delay_ms(20);
		KeyNum = 1;		
	}
	else if(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)        //读取GPIO_Pin_11端口值如果等于0,代表按键按下   
	{
		Delay_ms(20);                                             //延时20ms,消抖  
		while(GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_11) == 0)      //20ms后GPIO_Pin_11端口值如果等于0,那么按键确实按下
		Delay_ms(20);
		KeyNum = 2;		
	}
	return KeyNum;
}	

(4)Key.h

#ifndef __KEY_H         
#define __KEY_H   
void Key_Init(void);
uint8_t Key_GetNum(void);

#endif        

 (5)main.c

①实现按键1按下,LED1点亮,按键2按下,LED1熄灭

#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();          // 读取键码值
		if (KeyNum == 1)                // 如果按键1被按下,点亮LED1
		LED1_ON();
		if(KeyNum == 2)            // 如果按键2被按下,熄灭LED1
		LED1_OFF();                       
	}
}

②实现按键1按下,LED1点亮,按键1再次按下,LED1熄灭;按键2按下,LED2点亮,按键2再次按下,LED2熄灭。

#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();          // 读取键码值
		if (KeyNum == 1)                // 如果按键1被按下,点亮LED1
		LED1_Turn();
		if(KeyNum == 2)            // 如果按键2被按下,熄灭LED1
		LED2_Turn();                       
	}
}

09 光敏传感器控制蜂鸣器

9.1 硬件电路

9.2 软件部分 

(1)新建驱动文件

(2)Buzzer.c

#include "stm32f10x.h"                  // Device header

/*蜂鸣器初始化函数*/
void Buzzer_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);        //开启时钟,蜂鸣器接在PB12,GPIO外设挂载在APB2上
	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);
	GPIO_SetBits(GPIOB,GPIO_Pin_12);                   //将PA12引脚置位高电平,不操作的时候蜂鸣器不响
}

/*开启蜂鸣器的函数*/
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);
	}
}

(3)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

(4)LightSensor.c

#include "stm32f10x.h"                  // Device header

/*初始化光敏传感器引脚的函数*/
void LightSensor_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;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;    //GPIO输出速度,在输入模式下,实际上没用的
	GPIO_Init(GPIOB,&GPIO_InitStruct);
}

/*读取光敏传感器引脚信号的函数*/
uint8_t LightSensor_Get(void)
{
	return GPIO_ReadInputDataBit(GPIOB,GPIO_Pin_13);
}

(5)LightSensor.h

#ifndef __LIGHT_SENSOR_H
#define __LIGHT_SENSOR_H
void LightSensor_Init(void);
uint8_t LightSensor_Get(void);
#endif

(6)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "Buzzer.h"                     // 调用蜂鸣器驱动文件


int main(void)
{
	Buzzer_Init();                      // 初始化蜂鸣器
	LightSensor_Init();                 // 初始化光敏传感器
	while(1)
	{	
		if(LightSensor_Get() == 1)		// 光线比较暗的情况,蜂鸣器鸣叫
		Buzzer_ON();
		else
		Buzzer_OFF();
	}
}

  • 28
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值