STM32开发笔记 - 经验分享
时间:2023-10-11 23:37:00
前言
在进行底层开发时,尤其是C语言,我们时常与暂存器打交道,不过到底暂存器的确切定义是甚麽?有时很难确切定义
有些书把临时存储器想象成一排书柜里的一个特定的网格。对于这些特殊的抽屉,你可以打开抽屉拿里面的纸条,或者把新的纸条放进去。就我个人而言,我喜欢这个隐喻,但它也让我我是否可以以更准确的方式定义临时存储器
思考重点
- 暂存器与记忆体映射的关系
- 存在暂存器的意义
- 如何查找数据手册?
- 编写点灯案例
暂存器概念
为了澄清暂存器的概念,我特意找到了32块bits的STM32F4型开发版,核心使用STM429IGT6.事实上,我们的编控制这个程序CPU许多引脚可以满足特定的需求,例如,传感器的输入是在运行后通过的GPIO引脚输出控制
我们可以通过控制引脚的输出和输入来实现特定的目标,。开发版的引脚分配了一组独特的地址位置,通过更改
这些地址存储的值可以有效地控制如何输出和如何输出引脚。因此,我们可以将引脚作为控制的最基本单位,而临时存储器是引脚背后的控制原理
记忆体映射
其实记忆体本身是不具有地址概念的,所谓的地址是由芯片厂商或用户自行规划出来的,也就是说地址的概念其实是我们抽象出来的
关键是如何知道虚拟地址的范围?
为了便于理解,我们从STM相应的官方网站下载data sheet(使用我的开发版STMF从下图可以看出,记忆体的映射范围为0x0000 0000~0xFFFF FFFF,共有4294967296,即4296G大小空间。请注意4G大小并不代表核心版的真实储存大小,而是核心版有能力表示这麽大的空间,这两者是有差别的
4G = 4294967296 = 2^简单来说就是处理器位元数的次方数。STM32F429开发版的核心处理器是32位元,有许多临时存储器负责存储数据,而这些临时存储器的长度范围只有32位bits
假设一个长度是32bits临时存储器存储一个数值为4的整数型态数据,将4转换为二进位系统等于0万 0000 0000 0000 0000 0000 0000 0100,我们将这一系列二进位数存储到0x0000 0000~0x0000 001F在地址空间中,这一块32bits长度的连续空间称为暂存器
可以看出,每个临时存储器的起始地址之间有32个bits(4bytes)差距,起始位置为0x0000 0000,最大值是0xFFFF FFFF,这个范围构建了4G寻址空间大小,官网Memory Mapping就是这样计算出来的。因此,我们称记忆体分配空间的行为为为记忆体映射
暂存器映射
命名记忆体地址的过程称为暂存器映射
临时存储映射的目的是在编写程式时使用定义的临时存储器名,而不是每次调用难以理解的16进制,例如以下程序
/* GPIOA 16个引脚输出高电位 */ *(unsigned int*)(0x40020014) = 0xffff; // 单存操作暂存器地址 #define GPIOA_ODR *(unsigned*)0x40020014 GPIOA_ODR = 0xffff; // 使用临存器映射
其中使用(unsigned int*)强制转换的功能是让编译器知道它是地址类型的常数。通常,我们会考虑它的具体意义来命名暂存器,例如GPIOA_ODR代表该GPIO A引脚的通用输出暂存器(Output Data Register),命名尽可能容易理解
C语言结构包装
假设我们想实现GPIOA首先要知道暂存器的控制GPIOA所以参考起始地址STM官方网站的reference manual可以在手册中找到记忆体映射表GPIOA蓝色方框显示下图的地址范围GPIOA起始地址为0X4002 0000
右侧显示GPIOA位于AHB1高速总线块(系统)GPIO引脚位于这里),通过data sheet的查找发现AHB被分配到名字上Block2分区内(很明显,地片上的外设位于Block2),所以我们可以很容易地做到GPIOA标记地址的目的是在编写程式时偏移三个层次的地址
这三种偏移分别是:
- 外设基地址偏移
- 总线基地址偏移
- GPIO基地址偏移
透过查找Memory Table外设的起始地址可以在表中找到,GPIO起始地址和GPIOA我们使用嵌套来映射这些临存器地址
/*外设基地址*/ #define PERIPH_BASE ((unsigned int)0x40000000) // 外设起始地址 /*总线基地址*/ #define AHB1_PERIPH_BASE (PERIPH_BASE 0x00020000) // GPIO的起始地址 /*GPIO基地址*/ #define GPIO_A_BASE (AHB1_PERIPH_BASE + 0x0000) // GPIOA起始地址
定义偏移地址的好处就是更好的扩充性,比如我今天想要define一个GPIOI地址,只需要将AHB1_PERIPH_BASE+0X2000就好了,不需要从基地址的暂存器地址开始计算偏移
这三种层次由大到小,使用者只须要依照想要使用的引脚范围进行定义,一旦基地址定义完成,开发者只需要选择距离目标引脚地址最小的偏移量基地址开始定义即可,另一方面这种方式也利于开发者阅读
GPIO端口设有10个暂存器,而且连续储存于GPIOA的连续记忆体空间中。因此我们可以透过自定义一个结构体数据类型来模拟内存空间中的暂存器
typedef unsigned int uint32_t;
typedef struct{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDER;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint32_t BSRR;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF
typedef GPIO_TYPEDEF* GPIO_Typedef; // 指向GPIO结构体的指标
我们透过将指标指向GPIO_X的基地址,使结构体内的成员的地址刚好与各个暂存器对应上,所以当我们对结构体成员操作时,事实上是在操作GPIO对应的暂存器:
GPIO_Typedef GPIO_X;
GPIO_X = GPIOX_BASE;
GPIO_X->MODER = 0X0003;
GPIO_X->OTYPER = 0X0001;
GPIO_X->OSPEEDER = 0X0003;
uint32_t tmp;
tmp = GPIO_X->IDR; // 读取暂存器
查找手册
查找完手册上对应的外设地址,然后利用程式编写暂存器映射的阵列指标后,我们必须再次查看手册,釐清引脚背后每个暂存器的控制意义,GPIO暂存器的介绍在data sheet的General-purpose I/Os(GPIO)/GPIO registers下可以找到:
STM32F429的每一个GPIO端口均配置10个长度为32bits的暂存器,依种类不同大致可以分成以下5大类:
模式配置类型
模式配置类型是GPIO引脚重要的功能部分之一,它决定引脚后续的工作性质、输出速度以及工作状态
进入模式配置类型暂存器介绍之前,我们先用一个盖览图来抓住模式配置类型暂存器的大框架: 依照不同模式去配置不同的引脚特性
MODER
配置GPIO引脚的工作模式,包含输入、输出、複用功能开启以及类比功能
GPIO端口首地址偏移量: 0x00
每个引脚由两个位元进行控制,分别有4种不同的模式:
- 00: GPIO输入模式
- 01: GPIO输出模式
- 10: 开启複用功能(UART、SPI、I2C…)
- 11: 类比输入输出(输入输出会由ADC/DAC进行转换)
OTYPER
当GPIO选择为输出模式,就需要选择输出模式,主要有推挽模式与开漏模式两种
0: 推挽模式
- 可以输出高低两电位,适合连接数位器件,为一般引脚最常见的配置
- 输出高电位为强高电位,驱动能力较强
- 输出反映快速
1: 开漏模式
- 通常运用在I2C、SMBUS等需要wired AND(线与)功能的汇流排(总线)电路上
- 输出低电位以及强低电位,也就是说开漏输出无法真正达到输出高电位
- 控制输出为1时,为高阻态,即强低电位(不高也不低),需要靠外部电路上拉才能实现真正高电位
OSPEEDER
设置GPIO引脚的输出速度
- 00: 低速模式(2MHz)
- 01: 中速模式(25MHz)
- 10: 快速模式(50MHz)
- 11: 高速模式(100MHz)
PUPDR
为了避免引脚在没有任何输入或输出(看引脚是配置成甚麽模式)下产生浮动,也就是说引脚的值是不确定的,需要依照MCU的特性去配置预设状态的电位,PUPDR就是在处理这个问题。例如将输入模式切换成输出模式之间的空档有可能会出现浮动,这时就需要配置一个确定的值
- 00: 没有设定上下拉
- 01: 上拉,预设为高电位
- 10: 下拉,预设为低电位
- 11: 保留,目前尚未开发的功能(尽量避免使用)
点灯
最后我们试着使用一个点灯程式来整合学到的暂存器观念。引脚输出方面,我使用GPIO A的引脚4、5、6作为红、绿、蓝LED输出脚位
首先若要启用GPIO,一定要先对其外设时钟控制暂存器RCC进行置位,我们根据data sheet对Memory Map的描述找到GPIO对应的RCC地址
紧接着同样在header文件中建立暂存器基地址映射、RCC地址映射以及GPIO暂存器结构体
#ifndef __STM32F4XX_H
#define __STM32F4XX_H
#include
#include
//#define GPIO_register 1
/*Memory mapping*/
#define PERIPH_BASE ((unsigned int)0x40000000)
#define AHB1_PERIPH_BASE (PERIPH_BASE + 0x00020000)
#define GPIO_H_BASE (AHB1_PERIPH_BASE + 0x1C00)
/*RCC*/
#define RCC_BASE (AHB1_PERIPH_BASE + 0x3800)
#define RCC_AHB1_ENR *(unsigned int*)(RCC_BASE+0x30)
/*GPIO*/
typedef struct{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDER;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint16_t BSRRL;
uint16_t BSRRH;
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFGH;
}GPIO_TYPEDEF;
typedef GPIO_TYPEDEF* GPIO_Typedef; //GPIO pointer
#endif
我使用两种方式点灯,第一种是纯粹的暂存器控制。第二种是封装成类库函式形式。主要使用GPIO_register来切换(看mian的条件编译),所以也附上封装的header与source files
#ifndef __STM_GPIO__H
#define __STM_GPIO__H
#include "stm32f4xx.h"
#include
typedef enum{
port_A=0,
port_B,
port_C,
port_D,
port_E,
port_F,
port_G,
port_H
}port;
extern void GPIO_Init(GPIO_Typedef, uint8_t);
extern void GPIO_LED_Control(GPIO_Typedef, uint8_t, bool);
#endif
#include "stm_gpio.h"
void GPIO_Init(GPIO_Typedef gpio, uint8_t port){
RCC_AHB1_ENR |= (1<<port);
gpio->MODER = 0x00;
gpio->OTYPER = 0x00;
gpio->OSPEEDER = 0x00;
gpio->PUPDR = 0x00;
}
void GPIO_Config(GPIO_Typedef gpio, uint8_t pin){
gpio->MODER |= (1<<2*pin);
gpio->OTYPER |= (0<<pin);
gpio->OSPEEDER |= (2<<2*pin);
gpio->PUPDR |= (1<<2*pin);
}
void GPIO_SET(GPIO_Typedef gpio, uint8_t pin){
gpio->BSRRL &= ~(1<<pin);
gpio->BSRRL |= (1<<pin);
}
void GPIO_RESET(GPIO_Typedef gpio, uint8_t pin){
gpio->BSRRH &= ~(1<<pin);
gpio->BSRRH |= (1<<pin);
}
void GPIO_LED_Control(GPIO_Typedef gpio, uint8_t pin, bool output){
GPIO_Config(gpio, pin);
if(output)
GPIO_SET(gpio, pin);
else
GPIO_RESET(gpio, pin);
}
最后学长编写主函式main,LED依照需求亮灭。编译成功可以将code烧进板子检查看看是否点灯成功
#include "stm32f4xx.h"
#include "stm_gpio.h"
/** * main */
int main(void)
{
GPIO_Typedef GPIO = (GPIO_Typedef)GPIO_A_BASE;
#ifdef GPIO_register
RCC_AHB1_ENR |= (1<<0);
/*MODER*/
GPIO->MODER &= ~(3<<2*4);
GPIO->MODER &= ~(3<<2*5);
GPIO->MODER &= ~(3<<2*6);
GPIO->MODER |= (1<<2*4);
GPIO->MODER |= (1<<2*5);
GPIO->MODER |= (1<<2*6);
/*OTYPER*/
GPIO->OTYPER &= ~(1<<4);
GPIO->OTYPER &= ~(1<<5);
GPIO->OTYPER &= ~(1<<6);
GPIO->OTYPER |= (0<<4);
GPIO->OTYPER |= (0<<5);
GPIO->OTYPER |= (0<<6);
/*OSPEEDER*/
GPIO->OSPEEDER &= ~(3<<2*4);
GPIO->OSPEEDER &= ~(3<<2*5);
GPIO->OSPEEDER &= ~(3<<2*6);
GPIO->OSPEEDER |= (2<<2*4);
GPIO->OSPEEDER |= (2<<2*5);
GPIO->OSPEEDER |= (2<<2*6);
/*PUPDR*/
GPIO->PUPDR &= ~(3<<2*4);
GPIO->PUPDR &= ~(3<<2*5);
GPIO->PUPDR &= ~(3<<2*6);
GPIO->PUPDR |= (1<<2*4);
GPIO->PUPDR |= (1<<2*5);
GPIO->PUPDR |= (1<<2*6);
/*BSRRL*/
GPIO->BSRRL &= ~(1<<4);
GPIO->BSRRL &= ~(1<<5);
GPIO->BSRRL &= ~(1<<6);
// GPIO_H->BSRRL |= (1<<4);
GPIO->BSRRL |= (1<<5);
GPIO->BSRRL |= (1<<6);
/*BSRRH*/
GPIO->BSRRH &= ~(1<<4);
GPIO->BSRRH &= ~(1<<5);
GPIO->BSRRH &= ~(1<<6);
GPIO->BSRRH |= (1<<4);
// GPIO->BSRRH |= (1<<5);
// GPIO->BSRRH |= (1<<6);
#else
/* GPIO A Initial */
GPIO_Init(GPIO, port_A);
/* pin 4 config*/
GPIO_LED_Control(GPIO, 4, 0);
/* pin 5 config*/
GPIO_LED_Control(GPIO, 5, 1);
/* pin 6 config*/
GPIO_LED_Control(GPIO, 6, 0);
#endif
while(1);
}
// void SystemInit(void)
// {
// }