stm32寄存器点灯写法详细教程
GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚,STM32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。所有的GPIO引脚都有基本的输入输出功能。最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED灯,那就可以控制LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电
目录
1.GPIO简介
GPIO是通用输入输出端口的简称,简单来说就是STM32可控制的引脚,STM32芯片的GPIO引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。所有的GPIO引脚都有基本的输入输出功能。
最基本的输出功能是由STM32控制引脚输出高、低电平,实现开关控制,如把GPIO引脚接入到LED灯,那就可以控制LED灯的亮灭,引脚接入到继电器或三极管,那就可以通过继电器或三极管控制外部大功率电路的通断。
最基本的输入功能是检测外部输入电平,如把GPIO引脚连接到按键,通过电平高低区分按键是否被按下。
编程如何控制单片机?
寄存器。
寄存器是什么?
存储电路开关状态/信息容量。
就比如家里的灯,是开还是关,我们需要拿一个容器去记录它的状态,当我修改完成这个状态之后,这个灯就变了,比如说我现在灯是关闭状态,我在容器里面写入一个0,如果我想要灯打开,我就在容器里面写入一个1,那么我是不是可以建立一个连接之后,那么我在寄存器里面写1就是开灯,写入0就是关灯。就相当于灯与这个容器绑定在一起的,绑定在一起后,我通过修改容器的值就可以改变灯的状态。
//定义一些指针类型的变量
int* led_config =(int*)0x100;//储存灯的模式等配置的容器名变量等价于 int* led_config; led_config =(int*)0x100
int* 1ed_enable=(int*)0x200;//生效配置的容器变量 不等价于int* led_enable; *led_enable =(int * )0x2
int* led_1 =(int*)0x300;
int* led_2 =(int*)0x400;
//现在指针变量存储了一个明确的地址
//对这个明确的地址进行读写,-》解引用语法
*led_config=6;//假设配置6号配置作为功能使用等价于 *(int *)0x100=6;
*led_enable=1;//生效配置
*led_1=ON; // * (int * )0x300=1;
*led_2=OFF;// * (int * )0x400 =0;
*led_1=OFF;// * (int * )0x300=1;
这部分代码用来举例,led_config指针和1ed_enable指针用来找到关联LED灯的配置的寄存器, led_1和 led_2指针是关联LED灯,led_config=6和led_enable=1用来对该寄存器里面的值进行配置设置,*led_1=ON;*led_2=OFF;*led_1=OFF用来修改关联LED灯的状态,开还是关闭。
2.IO控制实例


例如我们要控制GPIOA代码如下:
int *PA = 0x4001 0800 + 0X00 //其中0X4001 0800在上面表中查询到是GPIOA的寄存器起始地址
//+0X00是在起始地址偏移0X00个位置,定位到内部的某一个功能的寄存器
*PA = 1; //地址0X4001 0800 + 0X00 的寄存器存储的是0X0000 0001
//0x0000 0001这8位对应的是32位,每一位对应寄存器内部的四位
//每一个寄存器有32位,也就是说我通过PA找到地址是0X4001 0800 + 0X00对应的寄存器
//然后该寄存器有32位,此时我通过*PA = 1,将寄存器的最低位修改成1
//通过PA找到对应的GPIO寄存器地址,然后通过*PA = 1,就是修改该寄存器里面的值
如果我要是将这个地址的寄存器的倒数第二位修改成1那么 * PA = 2,修改倒数第三位是 * PA = 4,修改倒数第四位是 *PA = 8。
就是4位二进制数表示1个16进制数,由于寄存器有32位嘛,所以它可以用8位16进制表示32位寄存器
上面至于为什么修改倒数第一位值是1,*PA是1,修改倒数第二位,*PA是2,倒数第三位,*PA是4,倒数第四位,*PA是8,这就是可以用8421BCD码表示
例如有32位
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0001 倒数第一位是1,吧0001 头上标注8421,发现1的下面是1所以就是1那么这个寄存器表示的是0x0000 0001
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0010 倒数第二位是1,吧0010 头上标注8421,发现2的下面是1所以就是2那么这个寄存器表示的是0x0000 0002
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 0011 倒数第二位是1,倒数第一位是1,吧0011 头上标注8421,发现2的下面是1和1的下面是1,所以就是2+1 = 3
所以是3那么这个寄存器表示的是0x0000 0003
0000 0000 0000 0000 0000 0000 0000 0000
0000 0000 0000 0000 0000 0000 0000 1111 四位全部都是1,然后头上标注8421,就是8+2+4+1就是15对应的是F
所以是F那么这个寄存器表示的是0x0000 000F
总结:
0x4001 0800 + 0x00,第一个0x4001 0800是GPIOA这个大类的起始地址,GPIOA内部存在许多子寄存器;+0x00是GPIOA的一个子功能寄存器的地址。
每个子寄存器内部有32位,每一位都是0和1组成的二进制,用1位十六进制表示4位二进制(遵循8421权重规则),32位二进制对应8位十六进制(4×8=32)。如果32位全部为0,十六进制表示为0x00000000。
类似于数组,0X4001 0800相当于数组名,然后+0x00就是数组偏移地址,数组中某一个元素的地址
可以吧GPIOA想象成一个数组,GPIOA是一个数组名,数组呢有n个元素,GPIOA也有很多个子寄存器,通过数组偏移地址就可以找到对应的某一个元素的地址,同样的GPIOA + 0x00这个偏移量就相当于找到对应的子寄存器。
3.电路图

从电源3.3V出来经过LED灯,经过电阻,最后到达IO口。
如果IO口是写0,左边3.3V是高电平,IO口是低电平,发光二极管点亮
如果IO口是写1,左边3.3V是高电平,IO口也是高电平,发光二极管不会点亮。
4. STM32创建项目
使用的单片机是STM32F103C8T6
1.打开KEIL5后创建项目
2.选择存放项目文件的路径,以及输出项目文件的名字
3.选择芯片型号STM32F103C8,点击OK
4.在项目文件里面创建一个文件夹名user,用来保存main.c
5.点击创建文本文件,点击保存
6.输入main.c,保存路径找到创建的文件夹user中
7.点击品图标,双击中间那个地方
8.修改文件名是user,点击Add Files,选择user文件里面的main.c,点击Add导入到项目来
9.最终项目文件创建好了
10.关闭软件,然后找到这个项目文件后缀是.uvprojx双击打开
11.又会进入该文件
5.字符集乱码问题
就是当从别的文本文件里面吧代码复制到keil5里面去,会发现别的文本文件注释是中文,但是复制到keil5来的时候,变成了???,这种许多问号,这是因为字符集问题
例如我的文本文件中文是中国,但是我吧中国中文复制到keil5里面去变成了两个问号,这就是字符集问题。


这是因为我的记事本右下角显示的字符集是UTF-8编码,而在keil5小扳手点开,页面显示的字符集是ANSI编码,所以两个文本文件的字符集不同导致复制进来变成乱码或者问号形式。

当我吧keil5字符集修改成UTF-8编码后,此时字符集编码跟记事本编码一样,我现在从记事本复制进来中文就不是乱码了。
与此同时还需要设置缩进长度
这个地方需要勾选,设置4,按Tab键盘,缩进是4,原本不修改默认是2,这里需要修改成4。
6.DAP配置以及keil5一些配置
1.点击魔术棒,在Target选项中,下拉菜单中选择5,以及勾选

2.在Debug页面中,找到Settings左边有个下拉菜单,找到DAP选项
3.DAP下载器连接32单片机以及电脑接口,点击Settings

如果这两个蓝色的框框有东西就成功了
下载器与STM32核心板都有5根排针
下载器5根排针标注的是:RST CLK GND DIO 3V3
STM32核心板5根排针标注的是:RST TCK GND TWS 3V3
RST - RST
CLK - TCK
GND - GND
DIO - TWS
3V3 - 3V3
这样一 一相连
3.选择Flash Download 勾选Reset and Run
勾选这个选项,下载后,程序自动运行的。
7.代码编译一些报错解决

这里有一个警告提示
main.c文件最后一行没有换行符结束
此时我在第24行按下一个回车,出现25行,这个警告就解决了。
这里还有一个报错,报错的内容是:
编译器已经吧C代码翻译成了机器码,但链接器不知道吧这些代码"扔"到芯片内存的哪一个位置
就是怎么与芯片进行关联起来呢?怎么跑到main函数里面去呢?在我们之前学C语言的思维是我们的程序是从main函数进入的,这就相当于我们电脑启动为什么启动是从C盘启动不从D盘启动的,就有一个启动文件的。
启动文件:
startup_stm32f10x_md.s
1.在创建项目目录下创建一个文件夹startup
2.将启动文件放入到该文件夹里面
3.品符号,创建文件,命名startup,点击Add Files,在startup里面找到启动文件,点击Add,
4.最终左边出现文件夹startup下有一个添加进来的启动文件。
5.当我吧main函数注释掉提示报错
提示没有main和SystemInit错误
6.双击打开启动文件
程序运行从这个启动文件的__main进入,然后在我们的main.c文件去调用这个main函数,发现没有找到就报错了(我们吧main注释掉了),然后main调用完成后,然后还要调用SystemInit函数,既然也要调用SystemInit函数,那么我们就加上SystemInit函数。
此时我们加上SystemInit函数,在编译,就0警告,0错误。
8.启动文件注意事项


在keil5里面的Flash查看到大小是128k属于中等容量大小,选择的启动文件应该以md.s的文件
启动文件有3个,分别是以hd.s,ld.s,md.s结尾的文件
hd:大容量
md:中等容量
ld:小容量
而我们通过keil5查看到flash大小是128k,属于是中等容量,所以应该选择md.s结尾的启动文件。
9.通过DAP下载器下载程序到stm32核心板上
DAP选项在keil5设置
点击魔术棒选择Debug,选择DAP,点击Settings出现下面界面
在Flsh页面下勾选 Rest and Run

DAP连接到STM32核心板以及电脑USB后,在keil5点击下载按键,提示Verify OK,下载完成

最终吧板子上三个LED灯点亮。
10 代码的理解
为什么代码可以将板子上三个LED点亮
板子上的三个LED接入到PA1,PA2,PA3三个IO口上,通过同时给三个IO口给低电平就可以点亮了。
/*
位运算常用技巧
举例两个常用:
|= 通常用来置1
&=~ 通常用来复0
*/
void SystemInit(void)
{
}
int main (void)
{
unsigned int* led_rcc = (unsigned int*)(0x40021000+0x18);//灯具时钟的开关地址
unsigned int*led_config=(unsigned int*)(0x40010800+0x00);//灯具配置端口的开关地址
unsigned int*led_output=(unsigned int*)(0x40010800+0x0c);//灯具输出状态的开关地址
*led_rcc |= 0x4; //开启端口时钟
*led_config |= 0x3330; //配置端口引脚的模式
*led_output |= 0xE; //让端口引脚输出1,使得灯灭
*led_output &= ~0xE; //让端口引脚输出1,使得灯灭
}





因为我的3个LED在GPIOA上,所以我需要开启GPIOA端口,只有我开启了GPIOA端口设置,我才能使用这个端口,所以我需要找到开启GPIOA端口的寄存器,然后在该寄存器进行设置。
unsigned int* led_rcc = (unsigned int*)(0x40021000+0x18) //灯具时钟的开关地址
*led_rcc |= 0x4; //开启端口时钟
其中0x40021000是复位时钟RCC寄存器的起始地址,然后0x40021000+0x18找到对应的子寄存器
找到了该寄存器后,该寄存器有32位,其中位2是用来设置GPIOA时钟开启和关闭,这一位设置1,表示GPIOA开启,如果设置0表示GPIOA关闭,对应的32位二进制是:
0000 0000 0000 0000
0000 0000 0000 0100
对应的8位二进制是0x0000 0004所以代码
*led_rcc |= 0x4; //开启端口时钟
这里写0x4
这里的或运算是,当我其他位置是设置1的时候,为了不影响其他位置的原本设置的值,然后改变我想要改变的位,比如:
1000 0000 0000 0000
0000 0000 0000 0000
我最高位是1,但是我想要在位2设置1但是又不影响最高位的1设置,所以就需要用到或运算进行。
还有一个需要注意的点是
复位值是0,表示这个寄存器内部32位,默认情况下都是0的。
现在我已经设置了开启GPIOA端口,然后我还需要对GPIOA的配置
unsigned int*led_config=(unsigned int*)(0x40010800+0x00);//灯具配置端口的开关地址
*led_config |= 0x3330; //配置端口引脚的模式

GPIOA的起始地址是0x40010800
然后偏移地址是0X00
这个寄存器GPIOX_CRL(x = A…E)
- 当 x = A → 基地址 = 0x40010800
- 当 x = B → 基地址 = 0x40010C00
- 当 x = C → 基地址 = 0x40011000
y表示0-7
如果X = A就是GPIOA端口组别
那么表示PA0 - PA7
如果X = B就是GPIOB端口组别
那么表示PB0 - PB7
这里的偏移地址就是对某一个IO口组别的功能模式设置
位0 - 位3设置PA0
位4 - 位7设置PA1
位8 - 位11设置PA2
位12 - 位15设置PA3
位0 1表示MODE,2 3表示CNF
位4 5表示MODE,6 7表示CNF
位8 9表示MODE,10 11表示CNF
位12 13表示MODE,14 15表示CNF…
但是我电路中三个LED灯接在PA1 PA2 PA3所以我需用用到从位4到位15的设置
*led_config |= 0x3330; //配置端口引脚的模式
这里将寄存器里面设置的值是
该寄存器默认的值是0X4444 4444对应的32位是:
0100 0100 0100 0100
0100 0100 0100 0100
这里我将位4到位15加粗
此时我或运算设置0X0000 3330
0100 0100 0100 0100
0111 0111 0111 0100
位4 - 位7,PA1
位8 - 位11,PA2
位12 - 位15,PA3
位4 5表示MODE,6 7表示CNF
位8 9表示MODE,10 11表示CNF
位12 13表示MODE,14 15表示CNF

此时位4,位5 都是1,MODE对应的是输出模式,最大速度50MHZ
位7 位6 位5 位4 对应的是 0111 对应的位5 和位4都是1,根据8421BCD码得到3
当MODE[4,5]计算的权值结果是3 > 0,对应的CNF[6,7]是0 1得到的功能的是开漏输出模式
所以PA1设置的模式是开漏输出模式
由于
位4 - 位7,PA1
位8 - 位11,PA2
位12 - 位15,PA3
位7到位4是0111
位11到位8是0111
位15到位12是0111都相同,所以PA1和PA2和PA3都是开漏输出模式
开漏输出模式:可以给IO口输入低电平,给IO口输入高电平处于高阻态开路状态。
推挽输出模式:可以给IO口输入高电平和低电平
unsigned int*led_output=(unsigned int*)(0x40010800+0x0c);//灯具输出状态的开关地址
*led_output |= 0xE;

GPIO的理解,输出控制是由位设置/清除寄存器和输出数据寄存器控制,当输出控制这里得到一个1,然后经过两个MOS管,上边MOS管中间位置通过取反得到0,电压从VDD流向I/O引脚流出去了。IO口是高电平
如果输出控制这个地方是0的话,上面MOS管中间引脚通过取反得到1,此时上面MOS管就截止,下面MOS管从VSS流向I/O引脚,I/O是低电平。
所以控制GPIO端口可以通过两个寄存器控制,位设置/清除寄存器和输出数据寄存器控制
分别来看两个寄存器:位设置/清除寄存器
这里的ODRy表示对应的IO口,比如x是A,y是0,GPIOA组别里面的PA0,如果y是1就是PA1,如果y是15就是PA15
如果X是A的话这个寄存器
0-15位写1对应的PA0 - PA15的IO口是1
16-31位写1对应的PA0-PA15的IO口是0
如果IO口低电平亮
比如
让PA0,亮,就让位16给高电平,灭,就让位0给高电平
让PA1,亮,就让位17给高电平,灭,就让位1给高电平
让PA2,亮,就让位18给高电平,灭,就让位2给高电平
让PA3,亮,就让位19给高电平,灭,就让位3给高电平
让PA4,亮,就让位20给高电平,灭,就让位4给高电平
让PA5,亮,就让位21给高电平,灭,就让位5给高电平
让PA6,亮,就让位22给高电平,灭,就让位6给高电平
让PA7,亮,就让位23给高电平,灭,就让位7给高电平
让PA8,亮,就让位24给高电平,灭,就让位8给高电平
让PA9,亮,就让位25给高电平,灭,就让位9给高电平
让PA10,亮,就让位26给高电平,灭,就让位10给高电平
让PA11,亮,就让位27给高电平,灭,就让位11给高电平
让PA12,亮,就让位28给高电平,灭,就让位12给高电平
让PA13,亮,就让位29给高电平,灭,就让位13给高电平
让PA14,亮,就让位30给高电平,灭,就让位14给高电平
让PA15,亮,就让位31给高电平,灭,就让位15给高电平
也就是说16-31位控制PA0-PA15的亮,0-15位控制PA0-PA15的灭
unsigned int*led_output=(unsigned int*)(0x40010800+0x0c);//灯具输出状态的开关地址
但是我代码没用偏移+0x10这个寄存器,而是用偏移0X0C寄存器,下面看另外一个寄存器,也就是我现在代码所用到的寄存器控制IO端口。

这个寄存器默认值是0X0000 0000也就是32位都是0
如果X是A,对应的GPIOA组别
位0-位15分别对应PA0 - PA15的IO端口控制
比如位0写入0,那么PA0就是0,位0写入1,那么PA1就是1
而我的三个LED灯分别接入的是PA1 PA2 PA3端口,并且输出低电平的时候LED才亮
那么就让位1 位2 位3输出低电平
*led_output |= 0xE; //让端口引脚输出1,使得灯灭
这个代码让最低位变成1110
初始化的32位是
0000 0000 0000 0000
0000 0000 0000 0000
此时因为上面那个代码32位的数据是
0000 0000 0000 0000
0000 0000 0000 1110
让位1,位2,位3输出高电平,也就是PA1 PA1 PA3输出的是高电平,灯就是灭
*led_output &= ~0xE; //让端口引脚输出1,使得灯灭
这个代码0X0000 000E取反是
1111 1111 1111 1111
1111 1111 1111 0001
寄存器的32位数据:
0000 0000 0000 0000
0000 0000 0000 1110
与运算:
1111 1111 1111 1111
1111 1111 1111 0001
变成了
0000 0000 0000 0000
0000 0000 0000 0000
最终位1,位2,位3对应的PA1,PA2,PA3都是0,所以LED点亮。
假设代码改成
unsigned int*led_output=(unsigned int*)(0x40010800+0x10);
偏移是0x10,选择的是GPIOA
如果让PA1 PA2 PA3亮那么该寄存器的32位表示为:
0000 0000 0000 1110 0000 0000 0000 0000
让位17 18 19设置高电平,则PA1 PA PA3输出的是0 亮
如果让PA1 PA2 PA3灭那么该寄存器的32位表示为:
0000 0000 0000 0000 0000 0000 0000 1110
让位1 2 3设置高电平,则PA1 PA2 PA3输出的是1 灭
上面已经解释过这个寄存器了
位0和位16控制PA0的亮灭
位1和位17控制PA1的亮灭
位2和位18控制PA2的亮灭
位3和位19控制PA3的亮灭…
所以代码应该是:
unsigned int*led_output=(unsigned int*)(0x40010800+0x10); //位设置/清除寄存器
*led_output |= 0XE; //让 PA1 PA2 PA3灭
*led_output = 0X000E0000; //让PA1 PA2 PA3亮
最终的实验现象
让三个灯全亮,分别用两个寄存器实现LED亮的功能
分别是寄存器端口输出寄存器和位设置/清除寄存器。
更多推荐



所有评论(0)