赞
踩
打算往单片机方向发展,故编写此系列的博文作为笔记使用。
使用开发板:STM32F407ZGT6开发板
MDK版本信息如下图所示:

固件包版本信息:Keil.STM32F4xx_DFP.2.15.0
本文所涉及到的开发板原理图:
MCU上相关引脚:

LED灯引脚:



#include "stm32f4xx.h"
int main(void)
{
while(1);
}
void SystemInit(void)
{
}
// 注意,这里有一行空行,否则编译会警告。事实上不是强迫症可以不管。


在上一个项目工程的基础上,添加一些新玩意儿,使用操控寄存器的方法点亮一个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环境,其中的代码也会无法执行,造成点灯失败(经验之谈):

编译结果和烧写结果:

开发板现象:

直接使用寄存器编写代码的缺点是可读性不高,如果将其寄存器相关操作做成宏定义将会提高可读性,所以将上述代码进行重新编写。将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) { }
进行编译和烧写:

开发板中的现象:

完成上述实验后,可以试着让两个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):

在上方的所有操作中,对寄存器的操控都是使用绝对地址去操控,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) { }
编译及烧写情况:

在开发板中的运行现象:

虽然封装了相关寄存器成为结构体,但是对其进行操作时还是不够直观,比如上述程序代码中有一行是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) { }
编译及烧写结果:

在开发板中的运行现象:

至此,入门结束。如果想要进一步提高,可以试着仿照标准库写一个库,但这个太麻烦了,而且此博文太长了,后边就不写了。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。