3 GPIO通用输入输出口
文章目录
注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的Github仓库。
3.1 GPIO输入输出原理
GPIO(General Purpose Input Output)通用输入输出口 可配置为8种输入输出模式。引脚电平范围为0V~3.3V,部分引脚可容忍5V(图1-6中IO口电平为FT标识的)。输出模式 下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等,当然若驱动大功率设备还需要添加驱动电路。输入模式 下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。
![](https://img-blog.csdnimg.cn/d6b0b844378f4ef7b4ea572fbdcfaa16.png)
上图给出了GPIO的基本结构图。在STM32中,所有的GPIO都挂载在APB2外设总线上。命名方式采用GPIOA、GPIOB、GPIOC…的方式来命名。每个GPIO模块内,主要包括寄存器、驱动器等。
- 寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,从而完成输出电平和读取电平的功能。该寄存器的每一位都对应一个引脚,由于stm32是32位的单片机,所以所有的寄存器都是32位的,也就是说只有寄存器的低16位对应上了相应的GPIO口。
- 驱动器就是增加信号的驱动能力的。
注:stm32f103c8t6芯片上48个引脚,除了基本的电源和晶振等维持系统正常运行的引脚外,分别包括PA0~PA15、PB0~PB15、PC13~PC15。
![](https://img-blog.csdnimg.cn/3a421be31c934e99a601d848e49d1a6c.png)
上图就是将“GPIO的基本结构”进行放大,得到的实际的位结构。
输入部分:
- 整体框架从左到右依次是寄存器、驱动器、IO引脚,从上到下分为“输入”、“输出”。
- 最右侧的IO引脚上两个保护二极管,其作用是对IO引脚的输出电压进行限幅在0~3.3V之间,进而可以避免过高的IO引脚输入电压对电路内部造成伤害。VDD=3.3V,VSS=0V。
- 输入驱动器的上、下拉电阻:相应的两个开关可以通过程序进行配置,分别有上拉输入模式(上开关导通&下开关断开)、下拉输入模式(下开关导通&上开关断开)、浮空输入模式(两个开关都断开)。上下拉电阻的作用就是给引脚输入提供一个默认的输入电平,进而避免引脚悬空导致的不确定。都属于弱上拉、弱下拉。
- 输入驱动器的触发器:这里是用肖特基管构成的施密特触发器。只有高于上限、低于下限电压才进行变化,作用是对输入电压进行整形,可以消除电压波纹、使电压的上升沿/下降沿更加陡峭。也就是说,stm32的GPIO端口会自动对输入的数字电压进行整形。
- “模拟输入”、“复用功能输入”:都是连接到片上外设的一些端口,前者用于ADC等需要模拟输入的外设,后者用于串口输入引脚等需要数字量的外设。
输出部分:
- 输出数据:可以由输出数据寄存器(普通的IO口输出)、片上外设来指定,数据选择器控制数据来源。
- 位设置/清除寄存器:单独操作输出数据的某一位,而不影响其他位。
- 驱动器中的MOS管:MOS管相当于一种开关,输出信号来控制这两个MOS管的开启状态,进而输出信号。可以选择推挽、开漏、关闭三种输出方式。
- 推挽输出模式:两个MOS管均有效,stm32对IO口有绝对的控制权,也称为强推输出模式。
- 开漏输出模式:P-MOS无效。只有低电平有驱动能力,高电平输出高阻。
- 关闭模式:两个MOS管均无效,端口电平由外部信号控制。
额外补充:stm32如何将数据写入寄存器?
- 通过软件的方式。由于stm32的寄存器只能进行整体读写,所以可以先将数据全部读出,然后代码中用
&=
清零、|=
置位的方式改变单独某一位的数据,再将改写后的数据写回寄存器。此方法比较麻烦、效率不高,对于IO口进行操作不合适。- 通过位设置/清除寄存器。若对某一位 置1,只需对位设置寄存器的相应位 置1;若对某一位 清零,则对清除寄存器相应位 清零。这种方式通过内置电路完成操作,一步到位。
- 通过读写STM32中的“位带”区域。在STM32中,专门分配有一段地址区域,该区域映射了RAM和外设寄存器所有的位。读写这段地址中的数据,就相当于读写所映射位置的某一位。整体流程与51单片机中的位寻址作用差不多。本教程不涉及。
模式名称 | 性质 | 特征 |
---|---|---|
浮空输入 | 数字输入 | 可读取引脚电平,若引脚悬空则电平不确定,需要连续驱动源 |
上拉输入 | 数字输入 | 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平 |
下拉输入 | 数字输入 | 可读取引脚电平,内部连接下拉电阻,悬空时默认低电平 |
模拟输入 | 模拟输入 | GPIO无效,引脚直接接入内部ADC(ADC专属配置) |
开漏输出 | 数字输出 | 可输出引脚电平,高电平为高阻态,低电平接VSS |
推挽输出 | 数字输出 | 可输出引脚电平,高电平接VDD,低电平接VSS |
复用开漏输出 | 数字输出 | 由片上外设控制,高电平为高阻态,低电平接VSS |
复用推挽输出 | 数字输出 | 由片上外设控制,高电平接VDD,低电平接VSS |
上表给出了GPIO的8种模式,通过配置GPIO的端口配置寄存器即可选择相应的模式。
- 每一个端口的模式由4位进行控制,16个端口就需要64位,也就是两个32位寄存器,即端口配置低寄存器、端口配置高寄存器。
- 输入模式下,输出无效;而输出模式下,输入有效。这是因为一个IO口只能有一个输出,但可以有多个输入,所以直接将输出信号输入回去也没问题。
3.2 硬件介绍-LED、蜂鸣器、面包板
首先,简单介绍一下stm32芯片外围的电路。
- LED:发光二极管,正向通电点亮,反向通电不亮。
- 有源蜂鸣器(本实验):内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定。上图所示的蜂鸣器模块使用三极管作为开关。
- 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。
- 下面是其实物图:
注:LED长脚为正极、灯内部小头为正极。本实验的蜂鸣器低电平驱动。
![](https://img-blog.csdnimg.cn/d0228d8a6e3e4e02b4e70b141ad25a25.png)
上图则是给出了LED和蜂鸣器的驱动电路图。注意,三极管的发射极一定要直接接正电源/地,这是因为三极管的开启需要发射极和基极之间有一定的电压,如果接在负载侧有可能会导致三极管无法正常开启。
![](https://img-blog.csdnimg.cn/ae15f60d85ba4aa481c8d044daccce11.png)
上图给出了面包板的示意图。可以看出,面包板中间的金属爪是竖着排列的,用于插各种元器件;上下四排金属爪是横着排列的,一般用于供电。注意,在使用面包板之前,一定要观察孔位的连接情况。
3.3 实验:LED闪烁、LED流水灯、蜂鸣器提示
需求1: 面包板上的LED以1s为周期进行闪烁。亮0.5s、灭0.5s……
- LED低电平驱动。
- 需要用到延时函数
Delay.h
、Delay.c
,在UP注提供的“程序源码”中,为了方便管理,应在工程内创建System文件夹,专门存放这些可以复用的代码。
![](https://img-blog.csdnimg.cn/ab06c44a3f714538bee7931a0b4ecf14.png)
注:实际上,应该在LED和驱动电源之间接上保护电阻,但是由于本电路过于简单,于是直接省略保护电阻。后面“LED流水灯”、“蜂鸣器提示”实验同样省略保护电阻。
![](https://img-blog.csdnimg.cn/222524d7a4844bed90e5aca1d563205e.png)
代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void){
// 开启APB2-GPIOA的外设时钟RCC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 初始化PA0端口:定义结构体及参数
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
<span class="token comment">//下面是对GPIO端口赋值的常用的四种方式</span>
// GPIO_ResetBits(GPIOA, GPIO_Pin_0);//复位PA0
// GPIO_SetBits(GPIOA, GPIO_Pin_0);//将PA0置1
// GPIO_WriteBit(GPIOA, GPIO_Pin_0, Bit_RESET);//将PA0清零
// GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);//此函数可以对16位端口同时操作
while(1){
//正常思路
GPIO_ResetBits(GPIOA, GPIO_Pin_0);//复位PA0
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0);//将PA0置1
Delay_ms(500);
<span class="token comment">//使用GPIO_WriteBit函数,且强制类型转换</span>
<span class="token function">GPIO_WriteBit</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> GPIO_Pin_0<span class="token punctuation">,</span> <span class="token punctuation">(</span>BitAction<span class="token punctuation">)</span><span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment">//把0类型转换成BitAction枚举类型</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_WriteBit</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> GPIO_Pin_0<span class="token punctuation">,</span> <span class="token punctuation">(</span>BitAction<span class="token punctuation">)</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- Delay.h
#ifndef __DELAY_H
#define __DELAY_H
void Delay_us(uint32_t us);
void Delay_ms(uint32_t ms);
void Delay_s(uint32_t s);
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- Delay.c
#include "stm32f10x.h"
/**
- @brief 微秒级延时
- @param xus 延时时长,范围:0~233015
- @retval 无
/
void Delay_us(uint32_t xus)
{
SysTick->LOAD = 72 xus; //设置定时器重装值
SysTick->VAL = 0x00; //清空当前计数值
SysTick->CTRL = 0x00000005; //设置时钟源为HCLK,启动定时器
while(!(SysTick->CTRL & 0x00010000));//等待计数到0
SysTick->CTRL = 0x00000004; //关闭定时器
}
/**
- @brief 毫秒级延时
- @param xms 延时时长,范围:0~4294967295
- @retval 无
*/
void Delay_ms(uint32_t xms)
{
while(xms–)
{
Delay_us(1000);
}
}
/**
- @brief 秒级延时
- @param xs 延时时长,范围:0~4294967295
- @retval 无
*/
void Delay_s(uint32_t xs)
{
while(xs–)
{
Delay_ms(1000);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
注:此后Delay.h
、Delay.c
将作为常用函数长期存放于System文件夹
中,后续如果使用到将直接调用不会再在笔记中展示源代码。
编程感想:
- Keil编译过后,整个工程会比较大,不利于分享给别人。可以使用UP主提供的批处理程序,删掉工程中的中间文件后再分享给别人,其他人使用的时候只需要重新编译一下就行。
- 本教程用到了RCC和GPIO两个外设,这些外设的库函数在Library中,一般存放在相应的 .h 文件的最后。
- 将LED的短脚接负极,长脚接PA0口,就是高电平驱动方式,但是现象和低电平相同。
- 将GPIO设置成开漏输出模式,可以发现高电平(高阻态)无驱动能力,低电平有驱动能力。
需求2: 面包板上的8个LED以0.5s切换一个的速度,实现流水灯。低电平驱动。
![](https://img-blog.csdnimg.cn/24a90f75e95d481dbc03cacf5d8d0f51.png)
代码调用关系与“LED闪烁”实验相同,下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void){
// 开启APB2-GPIOA的外设时钟RCC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 初始化PA的8个端口:定义结构体及参数
GPIO_InitTypeDef GPIO_InitStructure;
//同时定义某几个端口
GPIO_InitStructure.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_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOA, &GPIO_InitStructure);
<span class="token keyword">while</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
<span class="token comment">//使用GPIO_SetBits、GPIO_ResetBits进行赋值,这里仅用于演示“或操作”同时赋值</span>
<span class="token function">GPIO_SetBits</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> GPIO_Pin_0 <span class="token operator">|</span> GPIO_Pin_1 <span class="token operator">|</span> GPIO_Pin_2 <span class="token operator">|</span> GPIO_Pin_3 <span class="token operator">|</span>
GPIO_Pin_4 <span class="token operator">|</span> GPIO_Pin_5 <span class="token operator">|</span> GPIO_Pin_6 <span class="token operator">|</span> GPIO_Pin_7<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_ResetBits</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> GPIO_Pin_0<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token comment">// //对指定的端口同时赋值</span>
<span class="token comment">// GPIO_Write(GPIOA, ~0x01);</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x02</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x04</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x08</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x10</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x20</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x40</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_Write</span><span class="token punctuation">(</span>GPIOA<span class="token punctuation">,</span> <span class="token operator">~</span><span class="token number">0x80</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">500</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
编程感想:
- 使用或操作
|
就可以实现只初始化定义某几个GPIO,或者某几个外设的时钟。
需求3: 蜂鸣器不断地发出滴滴、滴滴……的提示音。蜂鸣器低电平触发。
注:蜂鸣器执行四个动作为1个周期,分别是响0.1s、静0.1s、响0.1s、静0.7s。
![](https://img-blog.csdnimg.cn/79bc78df376f4cecbb6404faca54a88c.png)
代码调用关系与“LED闪烁”实验相同,下面是代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
int main(void){
// 开启APB2-GPIOB的外设时钟RCC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
// 初始化PB12端口:定义结构体及参数
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_Init(GPIOB, &GPIO_InitStructure);
<span class="token keyword">while</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
<span class="token function">GPIO_ResetBits</span><span class="token punctuation">(</span>GPIOB<span class="token punctuation">,</span> GPIO_Pin_12<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_SetBits</span><span class="token punctuation">(</span>GPIOB<span class="token punctuation">,</span> GPIO_Pin_12<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_ResetBits</span><span class="token punctuation">(</span>GPIOB<span class="token punctuation">,</span> GPIO_Pin_12<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">GPIO_SetBits</span><span class="token punctuation">(</span>GPIOB<span class="token punctuation">,</span> GPIO_Pin_12<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">100</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">Delay_ms</span><span class="token punctuation">(</span><span class="token number">600</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
编程感想:
- 控制蜂鸣器的IO端口可以随便选,但是不要选择三个JTAG调试端口:PA15、PB3、PB4。本实验选择PB12端口进行输出。
- 关于调用库函数,有以下几种方法:
- 直接查看每一个外设的
.h
函数,拖到最后就可以看到本外设的所有库函数,然后在对应的.c文件中查看函数定义和调用方式即可。- 查看库函数的用户手册——“STM32F103xx固件函数库用户手册.pdf”,这个中文版比较老;新版本的用户手册可以在ST公司的帮助文档中查看,但只有英文版。
- 百度一下别人的代码。
3.4 硬件介绍-按键开关、光敏电阻
![](https://img-blog.csdnimg.cn/eea6658101ca410da77abfed1bd15c23.png)
按键是最常见的输入设备,按下导通,松手断开。由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。
虽然前面已经说过,GPIO端口有专门的肖特基触发器对输入信号进行整形,但按键开关的抖动幅度大、时间长,所以还是 需要“软件消抖”。基本思路就是延迟5~10ms,跳过抖动时间范围即可。
![](https://img-blog.csdnimg.cn/563e982716b64a6cbfeacc7866451afc.png)
上图给出了按键开关的硬件电路设计图。对于按键开关来说,常见以上四种设计方法,而行业规范中,单片机端口一般都有 上拉输入模式(弱上拉),所有基本上就选择 内部上拉/外部上拉 的设计电路。
- 如果电路同时存在内部上拉和外部上拉,那么其高电平的驱动能力更强,但是低电平会更加耗电。两个下拉电路则可以使低电平驱动能力更强,而不会明显增加损耗。
- 浮空输入模式下,每部没有上下拉,此时必须在外部有上下拉电路。
- 注意 内部和外部的上下拉模式必须一致! 内部有上下拉时,就可以不用配置外部上下拉。
![](https://img-blog.csdnimg.cn/c86cb2c8158448439d9ffca8940db59f.png)
上面给出了传感器模块的实物图,从左到右依次是光敏电阻传感器、热敏电阻传感器、对射式红外传感器、反射式红外传感器。传感器元件的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。
![](https://img-blog.csdnimg.cn/dc76f3fe01d549ea9935e077d17f390f.png)
上面所给出的原理图则是一个比较通用的传感器模块格式:
- 左起第三个模块:下面的可变电阻就是各种传感器模块所对应的阻值,与上面的分压电阻R1进行分压,进而输出模拟电压值AO。电容C2是滤波电容。
- 左起第一个模块:使用LM393模块,通过运算放大器实现“电压比较器”的功能。IN- 是一个可以调节的阈值,IN+ 则直接连接传感器的模拟分压AO,当 AO > IN- 时,数字输出DO拉高;当 AO < IN- 时,数字输出DO拉低。
- 左起第二个模块:通过一个滑动变阻器实现比较电压 IN- 的调整。
- 左起第四个模块:电源指示灯。
- 左起第五个模块:传感器模块的端口。LED2用于指示数字输出DO的值。注意R5上拉电阻保证DO的默认值为高。
补充情况:
- 对于对射式红外传感器来说,N1就是红外接收管,并且额外还有一个点亮红外发射管的电路,模拟电压表示接收红外信号的强度。并且该模块常用于检测通断,所以用两个电阻将阈值固定为1/2的参考电压,而不是采用滑动变阻器。
- 对于反射式红外传感器,向下发射和接收红外光,可以做寻迹小车。
而对于传感器模块的电路设计来说,由于采用模块的方案,所以直接给传感器接上VCC和GND,然后将模拟信号AO和数字信号DO接在stm32的对应端口上即可。
本次实验采用数字信号DO接入,关于模拟信号接入的使用方法在后面AD/DA的实验中继续讲解。
3.5 实验:按键控制LED、光敏传感器控制蜂鸣器
需求1: 一个按键开关控制一个LED,每次按下按键,LED就改变自己的亮灭状态;两套系统互不影响。
- LED低电平驱动。
- 按键B11控制LEDA2,按键B1控制LEDA1。
- LED的状态改变是“松开触发”。
![](https://img-blog.csdnimg.cn/ba96ab6ed58b4d5f999913ca837de4b7.png)
![](https://img-blog.csdnimg.cn/b63bbb5258d340fe91c693fc0d42e67d.png)
代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
int main(void){
LED_Init();
Key_Init();
<span class="token keyword">while</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span>
<span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">Key_GetNum</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">==</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span><span class="token function">LED1_TURN</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>
<span class="token keyword">else</span> <span class="token keyword">if</span><span class="token punctuation">(</span><span class="token function">Key_GetNum</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">==</span><span class="token number">2</span><span class="token punctuation">)</span><span class="token punctuation">{<!-- --></span><span class="token function">LED2_TURN</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">;</span>
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- LED.h
#ifndef __LED_H
#define __LED_H
void LED_Init(void);
void LED1_TURN(void);
void LED2_TURN(void);
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- LED.c
#include "stm32f10x.h" // Device header
/**
- @brief 初始化PA2、PA1作为两个LED的输出端口
- @param 无
- @retvl 无
*/
void LED_Init(void){
// 开启APB2-GPIOA的外设时钟RCC
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
// 初始化PA的输出端口:定义结构体及参数
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
//默认输出为低电平-LED初始不亮
GPIO_SetBits(GPIOA, GPIO_Pin_2 | GPIO_Pin_1);
};
/**
- @brief LED1状态翻转
- @param 无
- @retvl 无
*/
void LED1_TURN(void){
if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_1)==1){
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
}else{
GPIO_SetBits(GPIOA, GPIO_Pin_1);
}
}
/**
- @brief LED2状态翻转
- @param 无
- @retvl 无
*/
void LED2_TURN(void){
if(GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_2)==1){
GPIO_ResetBits(GPIOA, GPIO_Pin_2);
}else{
GPIO_SetBits(GPIOA, GPIO_Pin_2);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- Key.h
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- Key.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
/**
- @brief 初始化B11、B1作为按键2、按键1
- @param 无
- @retvl 无
*/
void Key_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;//只有输出才有速度等级,但是这里也可以定义
GPIO_Init(GPIOB, &GPIO_InitStructure);
};
/**
- @brief 检测哪个按键已经按下-松开触发
- @param 无
- @retvl 返回按下的按键编号
-
@arg 0,1,2
*/
uint8_t Key_GetNum(void){
uint8_t keynum = 0;
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)0){
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11)0);
Delay_ms(20);
keynum = 2;
}
if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)0){
Delay_ms(20);
while(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)0);
Delay_ms(20);
keynum = 1;
}
return keynum;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
编程感想:
- 代码提示框如果不弹出,可以使用
ctrl + space
弹出代码提示框。如果没用的话,大概率是和输入法中/英切换快捷键冲突,输入法右键设置取消即可。- GPIO配置好之后默认就是低电平,所以配置好后LED会默认是亮的状态。
- 本工程创建了全新的驱动函数文件夹Hardware,专门用于存放程序中使用到的外设(如LED、按键、光敏传感器等)的驱动函数。做好驱动代码的提取是非常重要的,可以极大地方便程序梳理。
- 其实写完之后发现,这个按键开关非常不灵敏,经常出现按键松手后LED没有反应的情况。大概这就是设置“光敏传感器控制蜂鸣器”实验的原因吧。
需求2: 光敏电阻被遮挡,蜂鸣器长鸣,光敏电阻不被遮挡,蜂鸣器不响。
- 蜂鸣器低电平驱动。
- 光敏传感器,光强越强阻值越小,分压越小;DO的LED指示灯低电平驱动。
![](https://img-blog.csdnimg.cn/dbb9642a58b34ed291da42e8a7711341.png)
![](https://img-blog.csdnimg.cn/d887c39fee104b40928f11a593eef065.png)
代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Buzzer.h"
#include "LightSensor.h"
int main(void){
Buzzer_Init();
LightSensor_Init();
while(1){
if(LightSensor_Get()==1){
Buzzer_ON();}
else {
Buzzer_OFF();}
};
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- Buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
void Buzzer_Init(void);
void Buzzer_ON(void);
void Buzzer_OFF(void);
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- Buzzer.c
#include "stm32f10x.h" // Device header
/**
-
@brief 蜂鸣器初始化-PB12推挽输出
*/
void Buzzer_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;//推挽输出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_12;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
/**
- @brief 蜂鸣器开启-低电平驱动
*/
void Buzzer_ON(void){
GPIO_ResetBits(GPIOB, GPIO_Pin_12);
}
/**
- @brief 蜂鸣器关闭-输入高电平
*/
void Buzzer_OFF(void){
GPIO_SetBits(GPIOB, GPIO_Pin_12);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- LightSensor.h
#ifndef __LIGHTSENSOR_H
#define __LIGHTSENSOR_H
void LightSensor_Init(void);
uint8_t LightSensor_Get(void);
#endif
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- LightSensor.c
#include "stm32f10x.h" // Device header
/**
- @brief 光敏传感器初始化-PB13上拉输入
*/
void LightSensor_Init(void){
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/**
- @brief 读取光敏传感器的数字输入信号-光强越强分压越低
- @retvl 读取到的数字输入信号0/1
*/
uint8_t LightSensor_Get(void){
return GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_13);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
编程感想:
- 模块化编程真香!!每个模块都写一个专门的驱动函数存放在Hardware文件夹中,看似麻烦,但是会使得main函数极其简单,几乎就可以直接按照正常的功能逻辑书写代码。
3.6 C语言的语法
1. 数据类型
关键字 | 位数 | 表示范围 | 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 |
- 51单片机中
int
型 为16位;stm32中int
型 为32位。- 倒数第二列是C语言给这些类型提供的别名;最后一列是老版本ST公司库函数给这些类型提供的别名,新版的库函数已经全部替换成倒数第二列。以后写程序时尽量使用上表中加粗的关键字。
2. 宏定义#define
关键字 #define
,主要用于:用一个字符串代替一个数字,便于理解,防止出错;或者提取程序中经常出现的参数,便于快速修改。
//定义宏定义:
#define ABC 12345
//引用宏定义:
int a = ABC; //等效于int a = 12345;
- 1
- 2
- 3
- 4
3. 关键字typedef
关键字 typedef
,常用于将一个比较长的变量类型名换个名字,便于使用。
//定义typedef:
typedef unsigned char uint8_t;
//引用typedef:
uint8_t num1; //等效于unsigned char num1;
- 1
- 2
- 3
- 4
相比于#define
来说,typedef
在进行改名时会进行变量类型检查,所以更加安全。
4. 结构体
关键字 struct
,用途:数据打包,将不同类型变量组成一个集合。
//在main函数中定义结构体变量:
struct{
char x;
int y;
float z;
} StructName;
//因为结构体变量类型较长,所以通常在main函数外用typedef更改变量类型名
typedef struct{
char x;
int y;
float z;
} StructName;
//引用结构体成员:方法一
StructName struct1;
struct1.x = ‘A’;
struct1.y = 66;
struct1.z = 1.23;
//引用结构体成员:方法二
pStructName->x = ‘A’; //pStructName为结构体的地址
pStructName->y = 66;
pStructName->z = 1.23;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
5. 枚举类型
关键字 enum
,用途:定义一个取值受限制的整型变量,用于限制变量取值范围;枚举也相当于一个宏定义的集合,可以直接把里面的枚举变量拿出来用。注意枚举变量用逗号隔开,且最后一个枚举变量不加逗号。
//函数内定义枚举变量:
enum{
FALSE = 0,
TRUE = 1
} EnumName;
//因为枚举变量类型较长,所以通常在函数外用typedef更改变量类型名
typedef enum{
FALSE = 0,
TRUE = 1
} EnumName;
//引用枚举成员:
EnumName emu1;
emu1 = FALSE;
emu1 = TRUE;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15