文章目录
前言
打算往单片机方向发展,故编写此系列的博文作为笔记使用。
开发环境
使用开发板:STM32F407ZGT6开发板
MDK版本信息如下图所示:
固件包版本信息:Keil.STM32F4xx_DFP.2.15.0
开发板原理图
本文所涉及到的开发板原理图:
MCU上相关引脚:
LED灯引脚:
0x010 新建工程
- 创建一个项目工程文件,并新建两个文件,一个为main.c,另一个为stm32f4xx.h,别忘记放一个启动文件在项目工程的文件夹中:
- main.c中编写下列代码:
#include "stm32f4xx.h"
int main(void)
{
while(1);
}
void SystemInit(void)
{
}
// 注意,这里有一行空行,否则编译会警告。事实上不是强迫症可以不管。
- 更改编译器版本
- 编译
0x020 使用寄存器简单点亮LED灯
在上一个项目工程的基础上,添加一些新玩意儿,使用操控寄存器的方法点亮一个LED灯。可以按照0x010 新建工程中再建一个工程,但如果对这方面完全熟悉的话,建议直接copy一份刚刚的工程进行下面的操作比较快捷。
我们需要点亮LED 0这个LED灯,根据文章头部的介绍的开发板原理图,可以看出LED 0接到了MCU的PF9引脚,那么根据《STM32F4xx中文参考手册》:
查看到第53页,里面可以看到GPIOF和所挂载到的总线:
可以看出出GPIOF挂载在AHB1总线上,起始地址为0x40021400。下一步需要找出相关寄存器所在的位置及对应的状态位。
由于个引脚的时钟都为关闭状态,所以需要找出PF9对应的的时钟寄存器,并进行使能(ENABLE,启用、打开的意思),从上面可知GPIOF是挂载到AHB1总线上的,所以我们要配置RCC AHB1 外设时钟使能寄存器,该寄存器各位如下图所示:
虽然说现在已经知道了AHB1的外设时钟使能寄存器的各位信息及偏移位置,那么RCC复位和时钟的基地址呢?让我们回到手册的第53页,可以看到RCC的基地址为0x40023800,也就意味着,RCC AHB1 外设时钟使能寄存器的绝对地址为基地址+偏移地址,则:0x40023800 + 0x30 = 0x40023830。这个地址需要稍微注意一下,后面编写代码的操控寄存器编程的时候需要使用到。
在本章节中,仅需要使用最简单的方式去点亮一颗LED灯,现在还需要知道三个寄存器的信息,它们分别是GPIO端口模式寄存器 (GPIOx_MODER)和GPIO端口输出数据寄存器 (GPIOx_ODR),这两个寄存器的信息如下图所示:
结合上述所知,GPIOF的基地址为0x40021400,那么对应的GPIO端口模式寄存器的地址为0x40021400+0x00 = 0x40021400,输出数据寄存器的地址为0x40021400 + 0x14 = 0x40021414。
整理一下上述信息:
1. RCC AHB1 外设时钟使能寄存器 (RCC_AHB1ENR)的地址为:0x40023830
2. GPIO 端口模式寄存器 (GPIOx_MODER)的地址为:0x40021400
3. GPIO 端口输出数据寄存器 (GPIOx_ODR)的地址为:0x40021414
有了上述信息,可以进行编程了。此时main.c文件的代码为:
#include "stm32f4xx.h"
int main(void)
{
/**
* 第一步:开时钟
*/
*(unsigned int *)(0x40023800 + 0x30) |= (0x01 << 5);
/**
* 第二步:清除GPIOF_MODER上的第18-19位数据,再置为0x01
*/
*(unsigned int *)(0x40021400 + 0x00) &= ~(0x03 << (2 * 9));
*(unsigned int *)(0x40021400 + 0x00) |= (0x01 << (2 * 9));
/**
* 第三步:将GPIOF_ODR上的第9位置0
*/
*(unsigned int *)(0x40021400 + 0x14) &= ~(0x01 << 9);
while(1);
}
void SystemInit(void)
{
}
进行编译前要记得将微库勾选上,否则将会无法创建出main环境,其中的代码也会无法执行,造成点灯失败(经验之谈):
编译结果和烧写结果:
开发板现象:
0x030 使用寄存器简单点亮LED灯(代码优化)
直接使用寄存器编写代码的缺点是可读性不高,如果将其寄存器相关操作做成宏定义将会提高可读性,所以将上述代码进行重新编写。将0x020的代码重新copy一份,然后进行下列操作:
首先,先在stm32f4xx.h头文件中使用宏定义封装寄存器地址:
stm32f4xx.h
#ifndef __STM32F407xx_H__
#define __STM32F407xx_H__
#define RCC_AHB1ENR *(unsigned int *)(0x40023800 + 0x30)
#define GPIOF_MODER *(unsigned int *)(0x40021400 + 0x00)
#define GPIOF_ODR *(unsigned int *)(0x40021400 + 0x14)
#endif /* __STM32F407xx_H__ */
其次,改写main.c文件:
main.c
#include "stm32f4xx.h"
int main(void)
{
/**
* 第一步:开时钟
*/
RCC_AHB1ENR|= (0x01 << 5);
/**
* 第二步:将输出模式设置为推挽输出模式
*/
GPIOF_MODER &= ~(0x03 << (2 * 9));
GPIOF_MODER |= (0x01 << (2 * 9));
/**
* 第三步:输出低电平
*/
GPIOF_ODR &= ~(0x01 << 9);
while(1);
}
void SystemInit(void)
{
}
进行编译和烧写:
开发板中的现象:
0x040 两LED灯间隔亮起
完成上述实验后,可以试着让两个LED灯间隔亮起,结合“开发板原理图”中的信息可知,另一颗LED灯接在了PF10上,所以在0x030的代码基础上进行一些更改。下列是main.c文件的详细内容,而原本的stm32f4xx.h保持不变即可。
main.c
#include "stm32f4xx.h"
// 十分简单的延时函数
void Delay(unsigned int number)
{
while(--number);
}
int main(void)
{
/**
* 第一步:开时钟
*/
RCC_AHB1ENR|= (0x01 << 5);
/**
* 第二步:将PF9和PF10输出模式设置为推挽输出模式
*/
GPIOF_MODER &= ~(0x03 << (2 * 9));
GPIOF_MODER |= (0x01 << (2 * 9));
GPIOF_MODER &= ~(0x03 << (2 * 10));
GPIOF_MODER |= (0x01 << (2 * 10));
/**
* 第三步:先让两颗LED灯熄灭,简而言之就是PF9和PF10都输出高电平
*/
GPIOF_ODR |= (0x1 << 9);
GPIOF_ODR |= (0x1 << 10);
while(1)
{
// 点亮LED0,延时一段时间后熄灭
GPIOF_ODR &= ~(0x1 << 9);
Delay(0x0FFFFF);
GPIOF_ODR |= (0x1 << 9);
// 点亮LED1,延时一段时间后熄灭
GPIOF_ODR &= ~(0x1 << 10);
Delay(0x0FFFFF);
GPIOF_ODR |= (0x1 << 10);
}
}
void SystemInit(void)
{
}
上述代码进行编译和烧写:
开发板中的现象(此图为GIF动图,大小为1.75MB):
0x050 使用结构体的方式封装寄存器地址
在上方的所有操作中,对寄存器的操控都是使用绝对地址去操控,STM32F407具有上千个地址,如果每个地址都计算和定义出来,那是一个十分庞大的工程,。因此,在知道基地址和GPIOF的偏移地址之后,可以使用结构体的方式来操控寄存器,由于寄存器占用空间都是连续的,这跟结构体相似,使用这种方法就很好的解决需要重复定义寄存器地址的问题。
那么如何编写代码呢?
第一步:先给一些数据类型重起一个别名,以后使用这个别名就能看出相关变量占多少位。
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;
第二步:根据数据手册上的GPIO寄存器排列顺序,定义一个结构体。
typedef struct
{
uint32_t MODER; // GPIO 端口模式寄存器
uint32_t OTYPER; // GPIO 端口输出类型寄存器
uint32_t OSPEEDR; // GPIO 端口输出速度寄存器
uint32_t PUPDR; // GPIO 端口上拉/下拉寄存器
uint32_t IDR; // GPIO 端口输入数据寄存器
uint32_t ODR; // GPIO 端口输出数据寄存器
uint16_t BSRRL; // GPIO 端口置位寄存器
uint16_t BSRRH; // GPIO 端口复位寄存器
uint32_t LCKR; // GPIO 端口配置锁定寄存器
uint32_t AFRL; // GPIO 复用功能低位寄存器
uint32_t AFRH; // GPIO 复用功能高位寄存器
}GPIO_TypeDef;
第三步:定义相关基地址,及结合上方所定义的结构体,对完善对应的寄存器的定义
// GPIOF的基地址
#define GPIOF_BASE ((unsigned int)0x40021400)
// RCC时钟的基地址
#define RCC_BASE ((unsigned int)0x40023800)
// 以指针形式进行定义的GPIOF基地址
#define GPIOF ((GPIO_TypeDef *)GPIOF_BASE)
// AHB1寄存器的地址,写成解引用方式方便后续操作
#define RCC_AHB1ENR *(unsigned int *)(RCC_BASE+0X30)
结合上述内容,整个stm32f4xx.h文件就变成了:
stm32f4xx.h
#ifndef __STM32F407xx_H__
#define __STM32F407xx_H__
typedef unsigned int uint32_t;
typedef unsigned short uint16_t;
typedef struct
{
uint32_t MODER; // GPIO 端口模式寄存器
uint32_t OTYPER; // GPIO 端口输出类型寄存器
uint32_t OSPEEDR; // GPIO 端口输出速度寄存器
uint32_t PUPDR; // GPIO 端口上拉/下拉寄存器
uint32_t IDR; // GPIO 端口输入数据寄存器
uint32_t ODR; // GPIO 端口输出数据寄存器
uint16_t BSRRL; // GPIO 端口置位寄存器
uint16_t BSRRH; // GPIO 端口复位寄存器
uint32_t LCKR; // GPIO 端口配置锁定寄存器
uint32_t AFRL; // GPIO 复用功能低位寄存器
uint32_t AFRH; // GPIO 复用功能高位寄存器
}GPIO_TypeDef;
// GPIOF的基地址
#define GPIOF_BASE ((unsigned int)0x40021400)
// RCC时钟的基地址
#define RCC_BASE ((unsigned int)0x40023800)
// 以指针形式进行定义的GPIOF基地址
#define GPIOF ((GPIO_TypeDef *)GPIOF_BASE)
// AHB1寄存器的地址,写成解引用方式方便后续操作
#define RCC_AHB1ENR *(unsigned int *)(RCC_BASE+0X30)
#endif /* __STM32F407xx_H__ */
main.c文件的代码为:
main.c
#include "stm32f4xx.h"
// 简单的延时函数
void delay(uint32_t number)
{
while(--number);
}
int main(void)
{
// 第一步:开时钟
RCC_AHB1ENR |= (1 << 5);
// 第二步:配置GPIO输出模式
GPIOF->MODER &= ~(0x03 << (2 * 9));
GPIOF->MODER |= (0x01 << (2 * 9));
// 第三步:先关闭LED0灯
GPIOF->ODR |= (1 << 9);
while(1)
{
// 第四步:开启LED0灯
GPIOF->ODR &= ~(1 << 9);
// 延时
delay(0x0FFFFF);
// 第五步:关闭LED0灯
GPIOF->ODR |= (1 << 9);
delay(0x0FFFFF);
}
}
void SystemInit(void)
{
}
编译及烧写情况:
在开发板中的运行现象:
0x060 封装复位/置位寄存器操作成为函数
虽然封装了相关寄存器成为结构体,但是对其进行操作时还是不够直观,比如上述程序代码中有一行是GPIOF->ODR &= ~(1 << 9);
,虽然开发的时候知道的是将输出数据寄存器的第9位置为0,输出低电平点亮LED灯,但是时间久了,不一定能够想起来这是什么操作,为什么这个结构体里面的ODR成员要进行一个位操作,这是什么意思?有什么用?所以为了让程序具有更高的可读性,代码可以再进一步封装成函数。
那么应该如何去封装呢?此例以点亮绿色的LED1灯为主。
第一步:创建一个stm32f4xx_gpio.h文件,将复位/置位操作所需要到的引脚宏定义都编写在这个文件中。
// 各引脚定义
#define GPIO_Pin_0 ((uint16_t)(1<<0))
#define GPIO_Pin_1 ((uint16_t)(1<<1))
#define GPIO_Pin_2 ((uint16_t)(1<<2))
#define GPIO_Pin_3 ((uint16_t)(1<<3))
#define GPIO_Pin_4 ((uint16_t)(1<<4))
#define GPIO_Pin_5 ((uint16_t)(1<<5))
#define GPIO_Pin_6 ((uint16_t)(1<<6))
#define GPIO_Pin_7 ((uint16_t)(1<<7))
#define GPIO_Pin_8 ((uint16_t)(1<<8))
#define GPIO_Pin_9 ((uint16_t)(1<<9))
#define GPIO_Pin_10 ((uint16_t)(1<<10))
#define GPIO_Pin_11 ((uint16_t)(1<<11))
#define GPIO_Pin_12 ((uint16_t)(1<<12))
#define GPIO_Pin_13 ((uint16_t)(1<<13))
#define GPIO_Pin_14 ((uint16_t)(1<<14))
#define GPIO_Pin_15 ((uint16_t)(1<<15))
#define GPIO_Pin_All ((uint16_t)(0xFFFF))
第二步:新创建一个stm32f4xx_gpio .c文件,编写置位/复位函数
// 置位函数
void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
GPIOx->BSRRL = GPIO_Pin;
}
// 复位函数
void GPIO_ResetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
GPIOx->BSRRH = GPIO_Pin;
}
第三步:改写main.c文件
综合上述,相关文件的代码为:
stm32f4xx_gpio.h
#ifndef __STM32F4XX_GPIO_H__
#define __STM32F4XX_GPIO_H__
#include "stm32f4xx.h"
// 各引脚定义
#define GPIO_Pin_0 ((uint16_t)(1 << 0))
#define GPIO_Pin_1 ((uint16_t)(1 << 1))
#define GPIO_Pin_2 ((uint16_t)(1 << 2))
#define GPIO_Pin_3 ((uint16_t)(1 << 3))
#define GPIO_Pin_4 ((uint16_t)(1 << 4))
#define GPIO_Pin_5 ((uint16_t)(1 << 5))
#define GPIO_Pin_6 ((uint16_t)(1 << 6))
#define GPIO_Pin_7 ((uint16_t)(1 << 7))
#define GPIO_Pin_8 ((uint16_t)(1 << 8))
#define GPIO_Pin_9 ((uint16_t)(1 << 9))
#define GPIO_Pin_10 ((uint16_t)(1 << 10))
#define GPIO_Pin_11 ((uint16_t)(1 << 11))
#define GPIO_Pin_12 ((uint16_t)(1 << 12))
#define GPIO_Pin_13 ((uint16_t)(1 << 13))
#define GPIO_Pin_14 ((uint16_t)(1 << 14))
#define GPIO_Pin_15 ((uint16_t)(1 << 15))
#define GPIO_Pin_All ((uint16_t)(0xFFFF))
// 声明复位/置位函数
void GPIO_SetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
void GPIO_ResetBits(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);
#endif /* __STM32F4XX_GPIO_H__ */
stm32f4xx_gpio.c
#include "stm32f4xx_gpio.h"
// 置位函数
void GPIO_SetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
GPIOx->BSRRL = GPIO_Pin;
}
// 复位函数
void GPIO_ResetBits(GPIO_TypeDef *GPIOx,uint16_t GPIO_Pin)
{
GPIOx->BSRRH = GPIO_Pin;
}
main.c
#include "stm32f4xx.h"
#include "stm32f4xx_gpio.h"
// 简单的延时函数
void delay(uint32_t number)
{
while(--number);
}
int main(void)
{
// 第一步:开时钟
RCC_AHB1ENR |= (1 << 5);
// 第二步:配置GPIO输出模式
GPIOF->MODER &= ~(0x03 << (2 * 10));
GPIOF->MODER |= (0x01 << (2 * 10));
// 第三步:先关闭LED0灯
GPIOF->ODR |= (1 << 10);
while(1)
{
// 第四步:开启LED0灯
GPIO_ResetBits(GPIOF, GPIO_Pin_10);
// 延时
delay(0x0FFFFF);
// 第五步:关闭LED0灯
GPIO_SetBits(GPIOF, GPIO_Pin_10);
delay(0x0FFFFF);
}
}
void SystemInit(void)
{
}
编译及烧写结果:
在开发板中的运行现象:
至此,入门结束。如果想要进一步提高,可以试着仿照标准库写一个库,但这个太麻烦了,而且此博文太长了,后边就不写了。