实验一:以 STM32最小系统核心板(STM32F103C8T6)+3只LED 搭建电路,使用GPIOA/GPIOB/GPIOC端口控制LED灯,轮流闪烁,间隔时长1秒(标准外设库方式)。并使用Keil的软件仿真逻辑分析仪功能观察管脚的时序波形。
常用寄存器
1. RCC寄存器
2. GPIO寄存器
(1) 端口配置低寄存器GPIOX_CRL
每个GPIOX有16个输入、输出端口,即PX0-PX15。GPIO_CRL控制PX0-PX7,GPIO_CRH控制PX8-PX15。
每四位控制一个端口,因此,用十六进制数刚好能够描述每一位的状况。这里所说的状况就是输入输出模式。每四位中的高两位控制输出模式,低两位控制输出速度,因此由四位控制整个端口输出。
(2) 端口配置高寄存器GPIOX_CRH
(3) 端口输入数据寄存器GPIOX_IDR
IDR和ODR均能对数据进行存储。但是IDR是对输入数据进行存储,ODR是对输出数据进行存储。
高十六位始终为零,只用低十六位存储十六个输入输出引脚的数据。
(4) 端口输出数据寄存器GPIOX_ODR
3. 输出模式
输出模式由红色方框中的器件进行控制。
(1) 推挽输出
推挽输出时,P-MOS有效、N-MOS无效。高低电平均能输出。
(2) 开漏输出
开楼输出时,P-MOS无效、N-MOS有效。只能输出低电平,不能输出高电平。
点亮LED
设计三个led,接入引脚分别为PA0、PB0、PC15,控制led灯的亮灭,其实就是控制对应I/O口的高低电平。
1. 配置时钟寄存器RCC
根据该系统结构图,如果我们要对GPIOX进行控制输出,我们就需要让APB2工作,因此,在配置时钟时,我们选用的时APB2外设时钟使能寄存器
由此,计算出该寄存器的绝对地址=0x40021000+0x18=0x4002018
根据上图,我们需要将选用的GPIOA、GPIOB、GPIOC 的时钟均开启,开启之后才能进行工作,因此就需要将位2、位3、位4置1,代码如下:
*(unsigned int*)0x40021018 |= (0X7 << 2); // 7的二进制位111
2. 配置输出模式-推挽输出
点亮led灯只需要输出高低电平就能实现led的亮与灭,需要对输出数据进行存储,因此应该将CRL或者CRH设置为推挽模式
采用的是输出最大速度为50MHz的推挽输出模式,因此二进制表示为0011,转化为十六进制为3。
又因为接入引脚分别为PA0、PB0、PC15,所以分别将对应的GPIOX的位0、位0、位15设置为3即可。
代码为:
// PA0
*(unsigned int*)0x40010800 |= 0x00000003;
// PB0
*(unsigned int*)0x40010C00 |= 0x00000003;
// PC15
*(unsigned int*)0x40011004 |= 0x30000000;
3. 配置端口输出数据寄存器
低电平点亮、高电平熄灭。
代码为:
// 全亮
*(unsigned int*)0x4001080C &= ~(0x0);
*(unsigned int*)0x40010C0C &= ~(0x0);
*(unsigned int*)0x4001100C &= ~(0x0 << 15);
delay();
// AB亮,C灭
*(unsigned int*)0x4001080C &= ~(0x0);
*(unsigned int*)0x40010C0C &= ~(0x0);
*(unsigned int*)0x4001100C = 0x1 << 15;
delay();
// A亮,BC灭
*(unsigned int*)0x4001080C &= ~(0x0);
*(unsigned int*)0x40010C0C = 0x1 << 0;
*(unsigned int*)0x4001100C = 0x1 << 15;
delay();
// ABC全灭
*(unsigned int*)0x4001080C = 0x1;
*(unsigned int*)0x40010C0C = 0x1;
*(unsigned int*)0x4001100C = 0x1 << 15;
delay();
最终代码
#include "stm32f10x.h" // Device header
// 延时
void delay()
{
int i, j;
for (i = 0; i < 100000; i++)
{
for (j = 0; j < 100; j++)
{
}
}
}
int main(void)
{
// 初始地址GPIOA:0x40010800、GPIOB:0x40010C00、GPIOC:0x40011000
// 配置复位和时钟控制寄存器(RCC)-使能寄存器APB2
*(volatile unsigned long *)0x40021018 |= 0x7 << 2;
// 配置为推挽输出
*(volatile unsigned long *)0x40010800 |= 0x3; // PA0
*(volatile unsigned long *)0x40010C00 |= 0x3; // PB0
*(volatile unsigned long *)0x40011008 |= 0x30 << 16; // PC13
// 循环点亮
while (1)
{
// 全亮
*(volatile unsigned long *)0x4001080C &= ~(0x1 << 0); // PA0
*(volatile unsigned long *)0x40010C0C &= ~(0x1 << 0); // PB0
*(volatile unsigned long *)0x4001100C &= ~(0x1 << 13); // PC13
delay();
// AB亮,C灭
*(volatile unsigned long *)0x4001080C &= ~(0x1 << 0); // PA0
*(volatile unsigned long *)0x40010C0C &= ~(0x1 << 0); // PB0
*(volatile unsigned long *)0x4001100C |= 0x1 << 13; // PC13
delay();
// A亮,BC灭
*(volatile unsigned long *)0x4001080C &= ~(0x1 << 0); // PA0
*(volatile unsigned long *)0x40010C0C |= 0x1 << 0; // PB0
*(volatile unsigned long *)0x4001100C |= 0x1 << 13; // PC13
delay();
// ABC全灭
*(volatile unsigned long *)0x4001080C |= 0x1 << 0; // PA0
*(volatile unsigned long *)0x40010C0C |= 0x1 << 0; // PB0
*(volatile unsigned long *)0x4001100C |= 0x1 << 13; // PC13
delay();
}
}
6、烧录结果
点灯
补充
由于STM32最小系统核心板子出厂时已经焊接好了1个led灯(标注了PC13处),一般可通过此灯的点亮让编程者验证自己烧录的代码是否正常运行了。下面是将这个灯也用在流水灯中,进行点灯。在代码中只需要将位移15的地方改为13、以及推挽输出的位置。
最终代码如下:
#include "stm32f10x.h" // Device header
// 延时
void delay()
{
int i, j;
for (i = 0; i < 100000; i++)
for (j = 0; j < 100; j++)
;
}
int main(void)
{
// 初始地址GPIOA:0x40010800、GPIOB:0x40010C00、GPIOC:0x40011000
// &= ~用来清0(给某一位单独置0)
// |= 用来给某一位单独置1
// 配置复位和时钟控制寄存器(RCC)-使能寄存器APB2
*(unsigned int *)0x40021018 |= (0X7 << 2);
// 配置为推挽输出
*(unsigned int *)0x40010800 |= 0x00000003; // PA0,低位,偏移地址0x00
*(unsigned int *)0x40010C00 |= 0x00000003; // PB0,低位,偏移地址0x00
*(unsigned int *)0x40011004 |= 0x00300000; // PC13,高位,偏移0x04
// 配置端口输出数据寄存器ODR,偏移地址0x0Ch
while (1)
{
// 全亮
*(unsigned int *)0x4001080C &= ~(0x0);
*(unsigned int *)0x40010C0C &= ~(0x0);
*(unsigned int *)0x4001100C &= ~(0x0 << 13);
delay();
// AB亮,C灭
*(unsigned int *)0x4001080C &= ~(0x0);
*(unsigned int *)0x40010C0C &= ~(0x0);
*(unsigned int *)0x4001100C = 0x1 << 13;
delay();
// A亮,BC灭
*(unsigned int *)0x4001080C &= ~(0x0);
*(unsigned int *)0x40010C0C = 0x1 << 0;
*(unsigned int *)0x4001100C = 0x1 << 13;
delay();
// ABC全灭
*(unsigned int *)0x4001080C = 0x1;
*(unsigned int *)0x40010C0C = 0x1;
*(unsigned int *)0x4001100C = 0x1 << 13;
delay();
}
}
实验二:用C语言寄存器方式编程实现,写出程序设计思路,包括GPIOx端口的各寄存器地址和详细参数
一、创建项目
新建项目的过程如上了,这里我直接创建了,然后新建一个main.c文件并保存(配置自己按需求调试),并添加启动文件
二、过程分析
1.思路
- 打开对应的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
这个函数用于使能GPIOA和GPIOC的时钟,以便后续对这两个GPIO端口进行配置和控制。
(2)初始化端口
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitTypeDef GPIO_InitStructure
:一个结构体变量,用于存储GPIO端口的初始化配置。GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP
:将GPIO端口配置为推挽输出模式,即可以输出高电平或低电平。GPIO_InitStructure.GPIO_Pin = GPIO_Pin_All
:将GPIO端口配置为初始化所有的引脚,即初始化16个引脚。GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz
:设置GPIO的输出速度为50MHz。GPIO_Init(GPIOA, &GPIO_InitStructure)
和GPIO_Init(GPIOC, &GPIO_InitStructure)
:分别对GPIOA和GPIOC端口进行初始化配置。
(3) 使用无限循环实现流水灯的效果
GPIO_SetBits(GPIOA, GPIO_Pin_All); // 熄灭所有小灯
while (1)
{
GPIO_ResetBits(GPIOA, GPIO_Pin_0); // 点亮第一个小灯
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_0); // 熄灭第一个小灯
GPIO_ResetBits(GPIOA, GPIO_Pin_1);
Delay_ms(500);
GPIO_SetBits(GPIOA, GPIO_Pin_1);
//...
}
三、波形分析
利用KEIL的软件仿真的逻辑分析仪功能观察GPIO的波形
由于设置GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP;为推挽输出,高低电平都有驱动能力以直观地计算波形的宽度:移动光标的位置到某一个点,单击鼠标左键就会把这个点设置为起始 点,然后移动光标到另一个位置就可以看到很多的信息,包括时间还有两点之间的时间差,也就是宽度,如图所示:
观察GPIO端口的输出波形,并分析时序状态正确与否、高低电平转换周期(LED闪烁周期)实际为多少。
发现时延为:14.50079s-14.00666s=0.49413s,与设置的时延500ms误差较小。