STM32 笔记 _《GPIO配置从低层走向高层》
目录
一.寄存器直接地址写入法
二.寄存器地址命名写入法
三.其它命名、及使用结构体 /枚举来归类 (逐步走向库函数)
三.GPIO一些参考图
一.寄存器直接地址写入法
操作IO口分三步: 1.打开相应的时钟;
2. 配置相应的I/O模式和频率(内部会以对应频率的驱动电路来匹配输出,输入时频率无效);
3. 输出或输入数据(给相应的IO寄存器写或读数据)
1. 配置时钟寄存器RCC地址 :示例 打开PC/PB口时钟
偏移地址:0x18,
*(unsigned int*) 0x4002 1018 |=0x00000018 ; //0x4002 1018地址的寄存器IOPC/IOPB设置为1,其余不变。
(时钟打开的是A-E中其中一组或多组所有16个Pin脚)。
2. 配置PC13、PB0口IO模式寄存器:
A--E各自的基址如上上图。
A--E各相同Pin脚的配置寄存器、包括下面的数据寄存器的 偏移地址都是一样的。
*(unsigned int*) 0x40011004 &=~(0x0F<<20); //先将23-20位置0,其余不变。*(unsigned int*) 0x40011004 |= 0x00100000; // 再将MODE13置01,PC13为10M、推挽输出模式*(unsigned int*)0x40010C00 &=~0x0F; //先将PB:3-0位置0,其余不变。*(unsigned int*)0x40010C00 |= 0x00000004; // 再将MODE0置01,PB0为浮空输入模式
注1:复位后各IO口为 0100 即浮空输入模式。
注2:RCC时钟配置是整个PC口的时钟;IO模式配置只是某个Pin脚 的输入/出模式。
注3:这些与或操作都是为了保持其余IO口配置不被改变,&1、|0 都是不改变原位状态。&0是置0、| 1是置1。
3. 输出数据寄存器:
*(unsigned int*) 0x4001100C &=0xFFFFDFFF; //将13位置0,其余位不变。D:1101*(unsigned int*) 0x4001100C |= 0x00002000; //将13位置1,其余位不变。2: 0010while(1){if ((*(unsigned int*)0x40010C08&0x00000001)>0) //取PB0*(unsigned int*)0x4001100C |=0x00002000; //输出高电平;else *(unsigned int*)0x4001100C &=0xFFFFDFFF; //输出低电平}
二.寄存器地址命名写入法
直接地址法就是对照手册配置对应地址的寄存器写入或读出。地址不好记,程序不直观。
下面所有看似高大上的所有工作都是为了让程序的可读性变强。
先从地址命名法开始,就是预告把这些难记的地址用 #define重新命名:#define 木易电子 xxxxxxxxx
首先,我们把总总线和三大分总线基地址各个起名:
PERIPH_BASE :翻译为:外设 基地
/*把下面的命名保存一个头文件里"stm32f10x.h" ,方便统一管理*/#define PERIPH_BASE 0x40000000 //总总线基址 (中国)
#define APB1PERIPH_BASE PERIPH_BASE //APB1总线基址(北京)
#define APB2PERIPH_BASE (PERIPH_BASE+0x00010000) //APB2总线基址(上海)
#define AHBPERIPH_BASE (PERIPH_BASE+0x00020000) //AHB总线基址 (广东)#define RCC_BASE (AHBPERIPH_BASE+0x1000) //RCC分支基址 (广东东莞)
#define RCC_APB2ENR *(unsigned int*)(RCC_BASE+0x18) //RCC的APB2ENR (东莞石龙镇)#define GPIOC_BASE (APB2PERIPH_BASE+0x1000) //GPIOC分支基址 (上海某区)
#define GPIOC_CRL *(unsigned int*)(GPIOC_BASE+0x00) //PC配置与输入输出寄存器(上海某区某街)
#define GPIOC_CRH *(unsigned int*)(GPIOC_BASE+0x04)
#define GPIOC_IDR *(unsigned int*)(GPIOC_BASE+0x08)
#define GPIOC_ODR *(unsigned int*)(GPIOC_BASE+0x0C)#define GPIOB_BASE (APB2PERIPH_BASE+0x0C00) //GPIOB分支基址 (上海某区)
#define GPIOB_CRL *(unsigned int*)(GPIOB_BASE+0x00) //PB配置与输入输出寄存器(上海某区某街)
#define GPIOB_CRH *(unsigned int*)(GPIOB_BASE+0x04)
#define GPIOB_IDR *(unsigned int*)(GPIOB_BASE+0x08)
#define GPIOB_ODR *(unsigned int*)(GPIOB_BASE+0x0C)
这样以后新建工程时将这个头文件#include "xxx.h " 就行了,程序里直接用地址名称来赋值即可。
如上一章节的例子:没有区别,只是将地址更换成名称
int main()
{RCC_APB2ENR |=0x00000018; //打开GPIOC和GPIOB时钟GPIOC_CRH &=~(0x00F00000); //先将PC:23-20位置0,其余不变。GPIOC_CRH |= 0x00100000; // 再将MODE13置01,为10M、推挽输出模式GPIOB_CRL &=~0x0000000F; //先将PB:3-0位置0,其余不变。GPIOB_CRL |= 0x00000004; // 再将MODE0置01,为浮空输入模式GPIOC_ODR &=0xFFFFDFFF; //输出低电平delay_ms(100);GPIOC_ODR |=0x00002000; //输出高电平;delay_ms(100);while(1){if ((GPIOB_IDR & 0x00000001)>0) //仅取PB0一位GPIOC_ODR |= 0x00002000; //输出高电平;else GPIOC_ODR &= 0xFFFFDFFF; //输出低电平}
}
三.其它命名、及使用结构体 /枚举来归类 (逐步走向库函数)
从上一章看出,寄存器的名字好记了,但其中哪一位赋什么值还是要去查资料,可以继续深入命名 位名称和值名称 并加以同属性归类;
可以看出各组的很多寄存器是相同属性的,那么就可以用归类一统一管理(结构体或枚举)。
1. 定义2个结构体类型,并用一命名(结构体变量名)指向上一章所述的对应基址。
/*上一章节的命名这里省略了。。。。*/typedef unsigned int uint32_t;typedef struct
{uint32_t CRL; //第一个成员首地址就是些结构体变量的首地址uint32_t CRH; //以下成员地址依次增加,因为是32位,故+0x4uint32_t IDR;uint32_t ODR;uint32_t BSRR;uint32_t BSR;uint32_t LCKR;}GPIO_TypeDef; //GPIO结构体变量:给GPIO寄存器同属性归类typedef struct
{uint32_t CR;uint32_t CFGR;uint32_t CIR;uint32_t APB2RSTR;uint32_t APB1RSTR;uint32_t AHBENR;uint32_t APB2ENR;uint32_t APB1ENR;uint32_t BDCR;uint32_t CSR;
}RCC_TypeDef; //RCC结构体变量:给RCC寄存器同属性归类#define GPIOC ((GPIO_TypeDef*)GPIOC_BASE) //命名:将0x40021000 指定为一结构体类型的地址
#define GPIOB ((GPIO_TypeDef*)GPIOB_BASE) //结构体类型地址:说明此地址是成员0的首址,共有N个成员的地址
#define RCC ((RCC_TypeDef*)RCC_BASE)
然后,主程序里就可以这样了:
RCC->APB2ENR |=0x00000018; //打开GPIOC和GPIOB时钟
GPIOC->CRH &=~(0x00F00000); //先将PC:23-20位置0,其余不变。
GPIOC->CRH |= 0x00100000; // 再将MODE13置01,为10M、推挽输出模式
GPIOC->CRL &=~(0x0FF); //先将PC:8-0位置0,其余不变。
GPIOC->CRL |= 0x11; // 再将MODE1/0置01,为10M、推挽输出模式
看出来了,只是变了下 ->, 感觉更高大上而已,没啥意思吧,哈哈,接着来...
如果这样:配置时:我告诉一个函数:PC组的13脚,推挽输出,10MHZ。然后此函数自动去配置,我们不需要查哪个寄存器,也不需要查哪个位?置1还是置0。好,下面的工作都是为了达到这个目的而繁琐。开始苦点,未来可幸福了,#include即可。
/*上部分就是上程序图,这里省略了。。。*/
#define GPIO_Pin_0 ((uint16_t)0x0001) //16个Pin脚在对应的16位寄存器值
#define GPIO_Pin_1 ((uint16_t)0x0002)
//Pin2.3.4........14略
#define GPIO_Pin_15 ((uint16_t)0x8000)typedef enum //GPIO输入输出模式对应值用命名后用枚举归类
{GPIO_Mode_AIN = 0x00, //GPIOMode_TypeDef就是一变量类型名,GPIO_Mode_IN_FLOATING = 0x04, //此类型变量只能是此枚举中所列的成员GPIO_Mode_IPD = 0x28,GPIO_Mode_IPU = 0x48,GPIO_Mode_Out_OD = 0x14,GPIO_Mode_Out_PP = 0x10,GPIO_Mode_AF_OD = 0x1C,GPIO_Mode_AF_PP = 0x18,
}GPIOMode_TypeDef;
typedef enum //GPIO输出频率对应值用命名后用枚举归类
{GPIO_Speed_10MHZ=1,GPIO_Speed_2MHZ ,GPIO_Speed_50MHZ ,
}GPIOSpeed_TypeDef;typedef struct //GPIO配置结构体:把配置所需的共性部分集合在一起
{uint16_t GPIO_Pin; //GPIOSpeed_TypeDef GPIO_Speed;GPIOMode_TypeDef GPIO_Mode;
}GPIO_InitTypeDef;/*以上命名、枚举、结构体都只是给寄存器、管脚、设置值 归类并起个能读懂的名字:给烹饪材料和菜肉起名分门别类
以下函数就是:将上面的素材进行加工,直接做成菜单上的菜,外人不需要知道怎么做的,更不需要了解那些讨厌的素材*/
void GPIO_SetBits(GPIO_TypeDef*GPIOx,uint16_t GPIO_Pin)
{GPIOx->BSRR |=GPIO_Pin; //GPIOx组的GPIO_Pin_y脚=1
}void GPIO_ResetBits(GPIO_TypeDef*GPIOx,uint16_t GPIO_Pin)
{GPIOx->BRR |=GPIO_Pin; //GPIOx组的GPIO_Pin_y脚=0
}//配置输入输出模式数
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
{uint32_t currentmode = 0x00, currentpin = 0x00, pinpos = 0x00, pos = 0x00;uint32_t tmpreg = 0x00, pinmask = 0x00;/* Check the parameters */assert_param(IS_GPIO_ALL_PERIPH(GPIOx));
还有配置函数GPIO_Init().配置的主要思路可以这样:根据Mode取得3、2二位的配置,根据Speed取得1、0位;
根据Pin的值可取得是CRH还是CRL; 再根据Pin的1在哪个位上,对应的CRH/L的对应的4位写入上面的Mode | Speed即可。
具体略了。还有RCC配置函数等,可以直接调用库,你需不需要知道内部原理,想了解更好。不知道也能用,比如不懂汽车原理也能当个老司机一样。
另外,上面写的命名、结构体、函数等都在一个文件里是不合C语言约定的,命名申明等有.h;
函数功能用.c。主程序只需要#incelud ''xxxxx.h"即可,C语言约定会自动寻找同名的.c文件。