江科大51单片机入门学习笔记合集
Posted 灰海宽松
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了江科大51单片机入门学习笔记合集相关的知识,希望对你有一定的参考价值。
文章目录
- 软件下载
- 介绍
- C语言语法
- 程序编写前言
- HELLO WORLD——LED部分
- 蜂鸣器实验
- 数码管
- 按键
- 矩阵按键
- IO 扩展(串转并)-74HC595
- 步进电机
- 中断
- 通信
- 封装头文件;绘制LED动画
- IIC, AT24C02
- 温度传感器 DS18B20
- 时钟
- 红外传感器
- LCD1602
- 直流电机
- ADC
- 完结:库函数封装说明
软件下载
开发:Keil。如果想要没乱码的中文注释,那么设置编码方式为 UTF8 或 GB2312。
程序文件下载到单片机:STC/普中(STC需要冷启动,先点击下载再开启单片机电源)
介绍
Micro Controller Unit, MCU 单片机,其中包含了CPU RAM ROM 输入输出设备 等一系列电脑硬件常用功能。
功能:通过传感器采集数据,通过CPU处理数据,控制硬件。
可以说是一个性能低的小电脑,是了解计算机原理的很好的学习方法。
右上角的跳线帽使用数码管时跳到VCC,使用点阵时跳到GND。
STC89C52RC 命名规则
STC:芯片为 STC 公司生产的产品。
8:该芯片为 8051 内核芯片。
9:表示内部含有 Flash EEPROM 存储器,还有如 80C51 中 0 表内部含有 MaskROM(掩模 ROM)存储器;如 87C51 中 7 表示内部含有 EPROM(紫外线可擦除 ROM)存储器。
C–表示该器件为 CMOS 产品。还有如 89LV52 和 89LE58 中的 LV 和 LE 都表示 该芯片为低电压产品(通常为 3.3V 电压供电);而 89S52 中 S 表示该芯片含有 可串行下载功能的 Flash 存储器,即具有 ISP 可在线编程功能。
5–固定不变。
2:表示该芯片内部程序存储(FLASH)空间大小,1 为 4KB,2 为 8KB,3 为 12KB,即该数乘以 4KB 就是芯片内部的程序存储空间大小。程序空间大小决定了 一个芯片所能装入执行代码的多少。一般来说,程序存储空间越大,芯片价格也 越高,所以我们再选择芯片的时候要根据自己需求选择合适芯片。 RC–STC 单片机内部 RAM(随机读写存储器)为 512B。还有如 RD+表示内部 RAM 为 1280B。还有芯片会省略此部分
芯片介绍
芯片在 PDIP 里。黑色的部分 PDIP 是一种封装方式,可能还有 LQFP 等(一个正方形的形状)封装方式。
8051 内核基本上都是中间绿色块的样子,只是外设、封装等方式不同。
管脚图:
Vcc 是电源,XTAL 管时钟,RST 是复位,等等。
整个是一个总线结构,所有外设都挂在上面。如最下面一行左边是晶振,右边是外部引脚。
只有这一个单片机是不能运行的,看我们的开发板上面还外接了好多好多外设呢。能让单片机运行的最小应用系统如下:
三角是正极,三线符号是负极。
首先需要 Vcc 接正,GND 接负。
然后需要接晶振。没有晶振单片机程序无法一条条往下执行,有了晶振按照固定的周期才能一条条往下执行。晶振就是板子上银色的椭圆形的一个东西,频率写在上面,一般是有12MHz和11.多MHz的两种(有的芯片自带晶振。不过很明显我们的芯片并不自带)。
然后还有复位电路,让程序回到第一条的位置。
开发板介绍
中间黑色的是刚刚介绍的单片机。拉起拉杆,可以取下单片机,但放回时一定不能放反。单片机有缺口的一端左侧从01开始,逆时针逐渐增大到40。
右侧中间有8个 LED 灯,我是点灯大师!
下面是一个矩阵按键,用户可以通过按按键输入。
最下面一行右侧有个红外接收传感器,接收红外线的。
左边无线模块,8个插孔的,做无线模块(如2.4G)用的。
再左边四个独立按键。
最左下是 USB 自动下载模块,插上 USB 线后按开关就会自动下载程序,不用了解。
DS1302 时钟芯片,可以做一个小时钟,读取时间。
红色按钮是 RST 按钮。
AD/DA 模数转换器,使单片机在数字与模拟领域之间转化。
74H595 可以扩展出更多的 IO 口。
步进电机可以精确控制脚步(转一圈、转半圈)。比如空调会用。
超声波模块可以测距。
蜂鸣器模块可以放歌。但我()()()()。
138译码器控制数码管,也可以扩展 IO 口。
24c02 也是一种 ROM,还是 EEP ROM(掉电不丢失)。其实单片机自带的 Flash ROM 更先进,但是只能用来存储程序。
温度传感器可以用来检测温度。
74HC245 可以驱动数码管(我的单片机是 HC138)。
左上角的电位器和排座用于接显示屏。电位器可以调整显示屏的亮度。
最大的黑色方阵是一个 LED 点阵。可以点亮8*8的方阵,甚至用来做动画。
之后的课程中还会详细介绍每一个模块,以及对应的电路图。
逻辑运算
&与,|或,!非,⊙同或(相同结果才=1),异或⊕(不同结果才为1)
C语言语法
int 16位,char 8位。
基本语法其他的都好说,再复习一下位运算。
左右移补0.
位运算符也可以参与成为复合赋值运算符,如^=, <<=
逗号运算符=最后一个表达式的值
函数在C语言基础上做的拓展
重入函数
在函数形参括号后加修饰符 reentrant,代表这个函数是重入函数,可以被递归调用,但这样就不能有bit变量,也不能进行位运算。
中断函数
在函数形参括号后加修饰符 interrupt m,系统编译时把对应函数转化为中断函数,自动加上程序头段和尾段,并按 51 系 统中断的处理方式自动把它安排在程序存储器中的相应位置。
在该修饰符中,m 的取值为 0~31,对应的中断情况如下:
0——外部中断 0
1——定时/计数器 T0
2——外部中断 1
3——定时/计数器 T1
4——串行口中断
5——定时/计数器 T2
其它值预留。
外部函数
如果要调用的函数不在本文件内,在其他文件内,定义函数时函数开头要加 extern 修饰符。
sfr sbit
用于定义特殊功能寄存器或特殊位。
sfr P0=0x80;//把地址 0x80 处的寄存器定义为 P0
sbit P0_1=P0^1;//取第一位定义为 P0_1
其实头文件 regx52.h 中都有。
能不能给位单独赋值要看是不是可位寻址。因为物理地址有限,每8个寄存器只能有一个可位寻址。
51单片机最小系统组成
- 晶振电路,提供时钟,相当于心脏
- 复位电路,系统运行不正常时可以重启
- 电源电路,注意单片机的供电电压要求
- 下载电路,烧入程序
另外注意,单片机的P0口是漏级开路,输出高电平会导致高阻态,因此输出高电平时要接上拉电阻,通常选择 4.7K~10K 阻值。
程序编写前言
新建项目 new μversion project
选择 CPU 型号:Keil 中没有完全对应的 STC89C52 版本,用Atmel 中的 AT89C52 即可,不用把8051启动文件添加到工程中。
AT 和 STC 是两种型号的单片机。有的 STC 单片机上面还有 AT 接口,AT 使用那个接口烧录程序。STC 就用 USB 下载。
新建好后有一个文件夹:source group,代码文件都在其中。
选中该文件夹,右键新建new item,新建c语言文件。可以选c/cpp/asm
在魔术棒 Output 选项中添加 “ create HEX file".
程序框架
#include "reg52.h"
void main()
while(1)
编译:translate按钮
建立:build按钮,也有编译的作用,只编译发生变动的文件。
重新建立:rebuild,编译所有文件(速度慢不建议)。
报错如果显示:缺少root segment根段,即没有找到主函数。
头文件作用
#include<reg52.h>
和`#include “reg52.h"都可以。区别在于<>直接去软件安装处搜索头文件,而”"先在该项目下查找头文件,找不到再去软件安装处,再找不到就报错。
查看头文件可以在左侧的结构树对应的c文件目录下打开,或者右键“reg52.h" open 打开。
该头文件中定义了52单片机内部所有功能寄存器,把地址值如0x80赋值给P0等端口。
程序烧录
程序编译建立没有错误,也开启了魔术棒创建 HEX 文件选项,那么 build 后就会在对应路径中找到生成的 HEX 文件。
在 STC-ISP 中选定单片机型号、串口、晶振频率(可以直接看开发板上的晶振上面有写),选择对应的 HEX 文件,先断电开发板,再点击下载,再开机,就可以查看呈现在开发板上的效果。
HELLO WORLD——LED部分
LED 发光二极管。
下面两个黑色的方块就是8个电阻。电阻是限流作用,防止电流过大烧毁 LED。
电阻上面写着小小的“102”,代表10*10^2,即1kΩ。
每个 LED 正极是一定通电流的,如果负极接地,那么这个 LED 被点亮。否则两头都是高电平点不亮(这里的电平是 TTL 电平,高5低0)。
单片机如何驱动高低电平?在 MCU 内,CPU 接到指令(如P2^0口赋1,即高电平)CPU 把数据写入寄存器,寄存器数据通过驱动器放大后变为5V/0V 电平输出。
点亮 LED
GPIO(general purpose input output) 即通用输入输出端口,可以通过软件控制其输入和输出.
- 电源引脚: Vcc, GND
- 晶振引脚:XTAL1 2
- 复位引脚:RST VPD,不做其他功能。
- 下载引脚:TXD RXD
- GPIO引脚:Px.x的都是 GPIO 引脚,大致分为P0 P1 P2 P3,每组8个IO,P3还有附加功能,比如串口、外部中 断、计数器等。每个引脚每次只能使用一个功能。
#include "reg52.h"
sbit LED1=P2^0; //将 P2.0 管脚定义为 LED1
//我们也可以直接给P2整个赋值。比如P2=0xFE,即1111 1110,就只会点亮最后一个 LED 灯,和 P2^0=0 效果是一样的。
//另,我们的这种做法只是寻找特殊寄存器P2的第几位。而头文件 REGX52.H 中是真正包含所有引脚信息的,如P2_0 就是2.0引脚,也能起到一样的效果。
void main()
LED1=0; //LED1 端口设置为低电平,就会被点亮
while(1)//单片机默认不断执行主程序。如果没有这个死循环,单片机就会不断点亮点亮点亮点亮……不如点亮一次之后无限延时。
编译结果里面的几个数据的意义:
code:表示程序所占用 FLASH 的大小。
data:数据储存器内部 RAM 占用大小。
xdata:数据储存器外部 RAM 占用大小。
LED 闪烁
只需要点亮——延时——熄灭——延时循环即可。
单片机频率单位是 MHz 兆赫兹,所以只是单纯的亮灭亮灭肉眼看不出亮灭的效果。所以需要延时。
延时可以写一个这样的函数:
typedef unsigned int u16
void delay(u16 ten_us)
while(ten_us--);
u16 代表16位的无符号整型数据。这是一个比较常用的定义,unsigned char 定义为 u8, unsigned int 定义为 u16。当 ten_us 超出 u16 的范围后,跳出 while 循环。
然后就LED1=0;delay(50000);LED1=1;delay(50000);
循环即可.
但是,STC-ISP 可以根据晶振频率和要延时的时间生成延时函数,真的牛!不过注意软件上标明的适用系列版本。
其中 _nop_() 函数包括在 INTRINS.H 头文件中,是一个空语句,就只会产生延时的效果。
不过 STC-ISP 只能生成固定时长的延时函数。如果想要像自己写的那个 delay() 函数一样传入参数,延时对应长度的毫秒/微秒呢?
很简单,我们先生成延时1毫秒/微秒的函数,然后把函数中的内容重复执行传入参数遍。
void Delay1ms(unsigned int xms) //@11.0592MHz
unsigned char i, j;
while(xms--)//这里是修改过的
_nop_();
_nop_();
_nop_();
i = 11;
j = 190;
do
while (--j);
while (--i);
Keil 软件仿真
使用仿真功能查看 LED 闪烁案例中的实际延时时间。
- 点击魔术棒,选择 Target 选项卡,设置 Xtal 为12M或11.0592M,根据开发板晶振修改对应值。
- 点击黑色放大镜中有红色d的仿真按钮,进入仿真页面
我们要关注的参数是sec。
- 点击RST按钮重新复位系统参数,sec 变为0。然后在要调试的行前双击,就会出现红色块的断点,点击8运行时就会直接运行到断点处。再次点击就会运行到下一处断点处。
- 点击红色标记8运行,运行到36行时显示用时:0.00039s,再次点击运行到37行,用时:0.45s
- 可见delay花费时间约为0.45s
LED 流水灯
学会了点亮和延时,流水灯的原理就很好懂了。就是给P2的所有端口赋值为:1111 1110,每次只有一个为0即点亮,这个点亮的0从最高位逐渐降到最低位。
取反后即为:
1000 0000
0100 0000
0010 0000
0001 0000
0000 1000
0000 0100
0000 0010
0000 0001
也就是一个移位运算,0x01<<i的循环。
#include "reg52.h"
# define LED P2
void delay(unsigned int i)
while(i--)
void main()
while(1)
int i=0;
for(i;i<8;i++)
LED=~(0x01<<i);
delay(50000);
移位函数
位运算的移位操作只能补0,但是 Keil C51 软件内有对应的移位库函数,左移_crol_(),右移_cror_(),包含在 intrins.h 库中。
移位函数会把移出去的位补到空位,一个循环。
#include "reg52.h"
#include "intrins.h"
# define LED P2
void delay(unsigned int i)
while(i--)
void main()
LED=~(0x01);
delay(50000);
while(1)
LED=_crol_(LED,1);
delay(50000);
蜂鸣器实验
蜂鸣器简单地说,就是电磁线圈和磁铁对振动膜的作用。
单片机的是无源蜂鸣器,不能一直充电,需要外部控制器发送震荡信号,可以改变频率产生不同的音色、音调。
大多数有源蜂鸣器则没有这个效果,有源蜂鸣器外观与之相同,内部自带震荡源,接上电就能响,但不能改变频率。
我们知道三极管的作用是不用单片机自己直接驱动单片机。
另一种方法是步进电机。
ULN 2003,高电压 高电流驱动器,给信号就被驱动。IN 取反输出 OUT。
简谱
首先整个谱大概分为几个区。大字组、小字组、小字1组、小字2组。每个组之间差8度,每相邻的两个键(如黑白)差半音,相邻的两个同色键差一个全音。
几个白键的表示方法就是下面的简谱,差半音的黑键用左上角的#表示升半音,b表示降半音。
演奏两大要素:音高和时值。
谱上一个数字是1/4 音符,二分是其两倍,数字加个横线 - 。全音符就是(2 - - -)。这个线叫增时线。
八分是其1/2,数字下加一条线(2).再/2就再加一条,叫减时线。
试着识一个完整的谱:
4/4:以四分音符为一拍,每小节有四拍。
第二节 1 ˙ \\dot1 1˙ · 上面的点我们知道代表高音,后面的点代表:前一位音符延长1/2长度,即四分音符+1/2的四分音符。也就是3/8哈哈哈。
看第一节,一般连着两个八分音符就把 underline 连起来。但是这种哪怕是一个音,中间也要先断开再重响。比如右上角的3 3。
升音和降音在本小节中有效。如第三行的 7 #4 4 7 ,两个4都是升音。
不过如果顶端画了延音线,就是连起来的不用断开。如中间的 77 ^ \\widehat7 7 77 ,拆开写是为了好读谱。
接下来就是如何把谱转化为单片机代码。左上角 1=c 说明是c调的。d大调会出现黑键,c调只有白键。
音具体是怎么定义的?首先以中音a为基准,高音a是其2倍,低音a是其1/2。
中间每次升音都是等比数列递增的,即*2的1/12次方
使用蜂鸣器
响起来很简单:不断反转 P1^5 口(是不是这个口得看自己的板子型号)。
void main()
u16 i=2000;//决定时值
while(1)
while(i--)
BEEP=!BEEP;
delay10Us(100);//决定音高
i=2000;
BEEP=0;
时值还好确认,音高怎么说?
首先我们有上图的音符与频率对照表。我们把频率转化为周期,即1/频率。这里周期单位是us。
然后周期时长转化为机器周期,即记一个数需要的时间。我们看看需要多少机器周期。
1机器周期=12时钟周期,时钟周期=1/单片机晶振。比如对于我的11.0592MHZ 晶振,机器周期=12/11.0592MHZ (单位:us)。
据此把“需要切换的周期时长”转化为“需要切换的周期需要执行几次指令”。即周期/机器周期。如果是12MHZ 晶振这一步相当于没有。
然后电平从低到高,从高到低才是一个周期。所以实际电平反转一次的周期是周期的一半。
我们知道定时器原理是 TH TL 加至65536触发中断。因此重装载值(定时器初值)=65536-取整值。
音符 | 频率 | 周期 | 需要的机器周期数 | 需要的机器周期数/2 | 取整 | 重装载值 |
---|---|---|---|---|---|---|
1 | 262 | 3816.794 | 3517.557252 | 1758.778626 | 1759 | 63777 |
1# | 277 | 3610.108 | 3327.075812 | 1663.537906 | 1664 | 63872 |
2 | 294 | 3401.361 | 3134.693878 | 1567.346939 | 1567 | 63969 |
2# | 311 | 3215.434 | 2963.344051 | 1481.672026 | 1482 | 64054 |
3 | 330 | 3030.303 | 2792.727273 | 1396.363636 | 1396 | 64140 |
4 | 349 | 2865.33 | 2640.687679 | 1320.34384 | 1320 | 64216 |
4# | 370 | 2702.703 | 2490.810811 | 1245.405405 | 1245 | 64291 |
5 | 392 | 2551.02 | 2351.020408 | 1175.510204 | 1176 | 64360 |
5# | 415 | 2409.639 | 2220.722892 | 1110.361446 | 1110 | 64426 |
6 | 440 | 2272.727 | 2094.545455 | 1047.272727 | 1047 | 64489 |
6# | 466 | 2145.923 | 1977.682403 | 988.8412017 | 989 | 64547 |
7 | 494 | 2024.291 | 1865.587045 | 932.7935223 | 933 | 64603 |
1 | 523 | 1912.046 | 1762.141491 | 881.0707457 | 881 | 64655 |
1# | 554 | 1805.054 | 1663.537906 | 831.7689531 | 832 | 64704 |
2 | 587 | 1703.578 | 1570.017036 | 785.0085179 | 785 | 64751 |
2# | 622 | 1607.717 | 1481.672026 | 740.8360129 | 741 | 64795 |
3 | 659 | 1517.451 | 1398.482549 | 699.2412747 | 699 | 64837 |
4 | 698 | 1432.665 | 1320.34384 | 660.1719198 | 660 | 64876 |
4# | 740 | 1351.351 | 1245.405405 | 622.7027027 | 623 | 64913 |
5 | 784 | 1275.51 | 1175.510204 | 587.755102 | 588 | 64948 |
5# | 831 | 1203.369 | 1109.025271 | 554.5126354 | 555 | 64981 |
6 | 880 | 1136.364 | 1047.272727 | 523.6363636 | 524 | 65012 |
6# | 932 | 1072.961 | 988.8412017 | 494.4206009 | 494 | 65042 |
7 | 988 | 1012.146 | 932.7935223 | 466.3967611 | 466 | 65070 |
1 | 1046 | 956.0229 | 881.0707457 | 440.5353728 | 441 | 65095 |
1# | 1109 | 901.7133 | 831.018936 | 415.509468 | 416 | 65120 |
2 | 1175 | 851.0638 | 784.3404255 | 392.1702128 | 392 | 65144 |
2# | 1245 | 803.2129 | 740.2409639 | 370.1204819 | 370 | 65166 |
3 | 1318 | 758.7253 | 699.2412747 | 349.6206373 | 350 | 65186 |
4 | 1397 | 715.8196 | 659.6993558 | 329.8496779 | 330 | 65206 |
4# | 1480 | 675.6757 | 622.7027027 | 311.3513514 | 311 | 65225 |
5 | 1568 | 637.7551 | 587.755102 | 293.877551 | 294 | 65242 |
5# | 1661 | 602.047 | 554.846478 | 277.423239 | 277 | 65259 |
6 | 1760 | 568.1818 | 523.6363636 | 261.8181818 | 262 | 65274 |
6# | 1865 | 536.193 | 494.155496 | 247.077748 | 247 | 65289 |
7 | 1976 | 506.0729 | 466.3967611 | 233.1983806 | 233 | 65303 |
使用方法:TH=重装载值/256,TL=重装载值%256.
音高从低到高逐位响起代码:
#include "reg52.h"
#include "Delay.h"
#include "Timer0.h"
sbit beep=P1^5;
unsigned int beep_table[]=//可以加个0代表不响的0
63777,63872,63969,64054,64140,64216,64291,64360,64426,64489,64547,64603,
64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070,
65095,65120,65144,65166,65186,65206,65225,65242,65259,65274,65289,65303
;
unsigned char beep_select=0;
void main()
unsigned char i;
timer0Init();
while(1)
beep_select++;
delayMs(50);//时值
void timer0Interrupt() interrupt 1
TH0 = beep_table[beep_select]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[beep_select]%256;
beep=!beep;
编曲:
根据乐谱写一个数组。
unsigned int little_star[]=12, 12, 19, 19,
21, 21, 19, //增时线
17, 17, 16, 16,
14, 14, 12,
19, 19, 17, 17,
16, 16, 14,
19, 19, 17, 17,
16, 16, 14,
12, 12, 19, 19,
21, 21, 19,
17, 17, 16, 16,
14, 14, 12
;
遍历数组,得到的音高再去 beep_table 中获取重装载值。
TH0 = beep_table[little_star[beep_select]]/256; // 因为触发中断时,TH TL 归零,所以记得赋初值!
TL0 = beep_table[little_star[beep_select]]%256;
但是播放起来都是连着的,听起来效果并不好。可以每次播完一个音先关闭中断并延时一段时间,再继续播放。
while(1)
beep_select++;
delayMs(50);
TR0=0;
delayUs(1);
TR0=1;
増时线如何处理?中间是不断开一直想的,因此需要几个特定的音符delay时间更长一些。怎么区分哪些音符加长哪些不加呢?
最好还是存储乐谱时搞一个二维数组(逻辑上物理上都可以),既能存储音高,也能存储时值。
unsigned int little_star[]=12, 4,
12, 4,
19, 4,
19, 4,
21, 4,
21, 4,
19, 8, //增时线
17, 4,
17, 4,
16, 4,
16, 4,
14, 4,
14, 4,
12, 8,
19, 4,
19, 4,
17, 4,
17, 4,
16, 4,
16, 4,
14, 8,
19, 4,
19, 4,
17, 4,
17, 4,
16, 4,
16, 4,
14, 8,
12, 4,
12, 4,
19, 4,
19, 4,
21, 4,
21, 4,
19, 8,
17, 4,
17, 4,
16, 4,
16, 4,
14, 4,
14, 4,
12, 8,
0xFF,4//终止标志防越界
;
如果数组大小超限,在魔术棒-Target-Memory Model 中选择第三个。不过这只是治标不治本,因为 RAM 只有512字节所以存不下太长。可以在定义数组时加上关键词 code 来存在 ROM 8K 的闪存中。
stm32学习笔记-3GPIO通用输入输出口
3 GPIO通用输入输出口
[toc]
注:笔记主要参考B站 江科大自化协 教学视频“STM32入门教程-2023持续更新中”。
注:工程及代码文件放在了本人的Github仓库。
3.1 GPIO输入输出原理
GPIO(General Purpose Input Output)通用输入输出口 可配置为8种输入输出模式。引脚电平范围为0V~3.3V,部分引脚可容忍5V(图1-6中IO口电平为FT标识的)。输出模式 下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等,当然若驱动大功率设备还需要添加驱动电路。输入模式 下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。
图3-1 GPIO基本结构上图给出了GPIO的基本结构图。在STM32中,所有的GPIO都挂载在APB2外设总线上。命名方式采用GPIOA、GPIOB、GPIOC…的方式来命名。每个GPIO模块内,主要包括寄存器、驱动器等。
图3-2 GPIO位结构
- 寄存器就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,从而完成输出电平和读取电平的功能。该寄存器的每一位都对应一个引脚,由于stm32是32位的单片机,所以所有的寄存器都是32位的,也就是说只有寄存器的低16位对应上了相应的GPIO口。
- 驱动器就是增加信号的驱动能力的。
注:stm32f103c8t6芯片上48个引脚,除了基本的电源和晶振等维持系统正常运行的引脚外,分别包括PA0~PA15、PB0~PB15、PC13~PC15。
上图就是将“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如何将数据写入寄存器?
表3-1 GPIO的8种模式
- 通过软件的方式。由于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芯片外围的电路。
图3-3 LED和蜂鸣器驱动电路设计
- LED:发光二极管,正向通电点亮,反向通电不亮。
- 有源蜂鸣器(本实验):内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定。上图所示的蜂鸣器模块使用三极管作为开关。
- 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同频率的声音。
- 下面是其实物图:
注:LED长脚为正极、灯内部小头为正极。本实验的蜂鸣器低电平驱动。
上图则是给出了LED和蜂鸣器的驱动电路图。注意,三极管的发射极一定要直接接正电源/地,这是因为三极管的开启需要发射极和基极之间有一定的电压,如果接在负载侧有可能会导致三极管无法正常开启。
图3-4 面包板实物图上图给出了面包板的示意图。可以看出,面包板中间的金属爪是竖着排列的,用于插各种元器件;上下四排金属爪是横着排列的,一般用于供电。注意,在使用面包板之前,一定要观察孔位的连接情况。
3.3 实验:LED闪烁、LED流水灯、蜂鸣器提示
需求1: 面包板上的LED以1s为周期进行闪烁。亮0.5s、灭0.5s……
图3-5 LED闪烁-接线图
- LED低电平驱动。
- 需要用到延时函数
Delay.h
、Delay.c
,在UP注提供的“程序源码”中,为了方便管理,应在工程内创建System文件夹,专门存放这些可以复用的代码。
注:实际上,应该在LED和驱动电源之间接上保护电阻,但是由于本电路过于简单,于是直接省略保护电阻。后面“LED流水灯”、“蜂鸣器提示”实验同样省略保护电阻。
图3-6 LED闪烁-代码调用(除库函数之外)代码展示:
- 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);
//下面是对GPIO端口赋值的常用的四种方式
// 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);
//使用GPIO_WriteBit函数,且强制类型转换
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)0);//把0类型转换成BitAction枚举类型
Delay_ms(500);
GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)1);
Delay_ms(500);
;
- 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
- 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);
注:此后Delay.h
、Delay.c
将作为常用函数长期存放于System文件夹
中,后续如果使用到将直接调用不会再在笔记中展示源代码。
编程感想:
- Keil编译过后,整个工程会比较大,不利于分享给别人。可以使用UP主提供的批处理程序,删掉工程中的中间文件后再分享给别人,其他人使用的时候只需要重新编译一下就行。
- 本教程用到了RCC和GPIO两个外设,这些外设的库函数在Library中,一般存放在相应的 .h 文件的最后。
- 将LED的短脚接负极,长脚接PA0口,就是高电平驱动方式,但是现象和低电平相同。
- 将GPIO设置成开漏输出模式,可以发现高电平(高阻态)无驱动能力,低电平有驱动能力。
需求2: 面包板上的8个LED以0.5s切换一个的速度,实现流水灯。低电平驱动。
图3-7 LED流水灯-接线图代码调用关系与“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);
while(1)
//使用GPIO_SetBits、GPIO_ResetBits进行赋值,这里仅用于演示“或操作”同时赋值
GPIO_SetBits(GPIOA, 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_ResetBits(GPIOA, GPIO_Pin_0);
// //对指定的端口同时赋值
// GPIO_Write(GPIOA, ~0x01);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x02);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x04);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x08);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x10);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x20);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x40);
Delay_ms(500);
GPIO_Write(GPIOA, ~0x80);
Delay_ms(500);
;
编程感想:
- 使用或操作
|
就可以实现只初始化定义某几个GPIO,或者某几个外设的时钟。
需求3: 蜂鸣器不断地发出滴滴、滴滴……的提示音。蜂鸣器低电平触发。
注:蜂鸣器执行四个动作为1个周期,分别是响0.1s、静0.1s、响0.1s、静0.7s。
代码调用关系与“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);
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(100);
Delay_ms(600);
;
编程感想:
- 控制蜂鸣器的IO端口可以随便选,但是不要选择三个JTAG调试端口:PA15、PB3、PB4。本实验选择PB12端口进行输出。
- 关于调用库函数,有以下几种方法:
- 直接查看每一个外设的
.h
函数,拖到最后就可以看到本外设的所有库函数,然后在对应的.c文件中查看函数定义和调用方式即可。- 查看库函数的用户手册——“STM32F103xx固件函数库用户手册.pdf”,这个中文版比较老;新版本的用户手册可以在ST公司的帮助文档中查看,但只有英文版。
- 百度一下别人的代码。
3.4 硬件介绍-按键开关、光敏电阻
图3-9 按键开关实物图按键是最常见的输入设备,按下导通,松手断开。由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。
虽然前面已经说过,GPIO端口有专门的肖特基触发器对输入信号进行整形,但按键开关的抖动幅度大、时间长,所以还是 需要“软件消抖”。基本思路就是延迟5~10ms,跳过抖动时间范围即可。
上图给出了按键开关的硬件电路设计图。对于按键开关来说,常见以上四种设计方法,而行业规范中,单片机端口一般都有 上拉输入模式(弱上拉),所有基本上就选择 内部上拉/外部上拉 的设计电路。
图3-11 传感器模块实物图
- 如果电路同时存在内部上拉和外部上拉,那么其高电平的驱动能力更强,但是低电平会更加耗电。两个下拉电路则可以使低电平驱动能力更强,而不会明显增加损耗。
- 浮空输入模式下,每部没有上下拉,此时必须在外部有上下拉电路。
- 注意 内部和外部的上下拉模式必须一致! 内部有上下拉时,就可以不用配置外部上下拉。
上面给出了传感器模块的实物图,从左到右依次是光敏电阻传感器、热敏电阻传感器、对射式红外传感器、反射式红外传感器。传感器元件的电阻会随外界模拟量的变化而变化,通过与定值电阻分压即可得到模拟电压输出,再通过电压比较器进行二值化即可得到数字电压输出。
图3-12 传感器模块原理图上面所给出的原理图则是一个比较通用的传感器模块格式:
- 左起第三个模块:下面的可变电阻就是各种传感器模块所对应的阻值,与上面的分压电阻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就改变自己的亮灭状态;两套系统互不影响。
图3-13 按键控制LED-接线图 图3-14 按键控制LED-代码调用(非库函数)
- LED低电平驱动。
- 按键B11控制LEDA2,按键B1控制LEDA1。
- LED的状态改变是“松开触发”。
代码展示:
- main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "LED.h"
#include "Key.h"
int main(void)
LED_Init();
Key_Init();
while(1)
if(Key_GetNum()==1)LED1_TURN();
else if(Key_GetNum()==2)LED2_TURN();
;
- LED.h
#ifndef __LED_H
#define __LED_H
void LED_Init(void);
void LED1_TURN(void);
void LED2_TURN(void);
#endif
- 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);
- Key.h
#ifndef __KEY_H
#define __KEY_H
void Key_Init(void);
uint8_t Key_GetNum(void);
#endif
- 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;
;
编程感想:
- 代码提示框如果不弹出,可以使用
ctrl + space
弹出代码提示框。如果没用的话,大概率是和输入法中/英切换快捷键冲突,输入法右键设置取消即可。- GPIO配置好之后默认就是低电平,所以配置好后LED会默认是亮的状态。
- 本工程创建了全新的驱动函数文件夹Hardware,专门用于存放程序中使用到的外设(如LED、按键、光敏传感器等)的驱动函数。做好驱动代码的提取是非常重要的,可以极大地方便程序梳理。
- 其实写完之后发现,这个按键开关非常不灵敏,经常出现按键松手后LED没有反应的情况。大概这就是设置“光敏传感器控制蜂鸣器”实验的原因吧。
需求2: 光敏电阻被遮挡,蜂鸣器长鸣,光敏电阻不被遮挡,蜂鸣器不响。
图3-15 光敏传感器控制蜂鸣器-接线图 ![在这里插入图片描述]() 图3-16 光敏传感器控制蜂鸣器-代码调用(非库函数)
- 蜂鸣器低电平驱动。
- 光敏传感器,光强越强阻值越小,分压越小;DO的LED指示灯低电平驱动。
代码展示:
- 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();
;
- Buzzer.h
#ifndef __BUZZER_H
#define __BUZZER_H
void Buzzer_Init(void);
void Buzzer_ON(void);
void Buzzer_OFF(void);
#endif
- Buzzer.c
#include "stm32f10x.h" // Device header
/**
* @brief 蜂鸣器初始化-PB12推挽输出
*/
void Buzzer_Init(void)
RCC_APB2PeriphClockCmd[学习笔记]15个QA让你快速入门51单片机开发