来做个实验:不依赖任何库,直接操作寄存器点亮第一个LED灯
分析上图可知,四个led灯等分别连接着STM32F407的四个引脚,引脚输出低电平,led亮,输出高电平,led灭
因为STM32与51单片机不同,它多一个时钟系统,旨在产生不同频率供不同设备使用,使用之前,必须先开启对应的时钟,所以在控制GPIO寄存器之前,我们要先打开GPIOF组的时钟
通过查找《STM32F4xx中文参考手册》第53页得知,RCC的基地址(也就是起始地址)为0x40023800,在135页查得其外设时钟使能寄存器的偏移地址为0x30,该偏移是相对于RCC基地址的偏移,因此计算外设时钟使能寄存器的地址为:
RCC_AHB1ENR = RCCADDR+0x30
=0x40023800+0x30
RCC_AHB1ENR是一个32位的寄存器,其中第5位控制这GPIOF的时钟,从下图中可看出,要想使能GPIOF时钟,需使相应位置1
(0x40023800+0x30)代表着寄存器RCC_AHB1ENR的地址,我们操作寄存器一般都是通过C语言中的指针去操作,因此需将这个地址先转型为指针
(volatile unsigned int *)(0x40023800+0x30) //强转为unsigned int型的指针
接下来便是对寄存器中的内容进行操作
*((volatile unsigned int *)(0x40023800+0x30)) |= (0x01<<5);
//最左边 * 的作用,解引用;此时左边代表着寄存器中的内容
//右边,位带操作,只改变第五位的值,不影响其他位的值
//假设原来寄存器中的值未知(但每一位的值无非也就是0或1),将运算展开如下,
// xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx
// (0x01<<5) 等 0000 0000 0000 0000 0000 0000 0001 0000
// 第五为,不管什么值,或上1后肯定为1 ;其他位,或上0不变,为原来的值
打开GPIO对应的时钟后,接下来要配置GPIO寄存器
以配置端口模式寄存器GPIOx_MODER为例,只要能看懂一个,其他寄存器的配置都是大同小异;
通过查看手册53页,可以看到GPIOF的基地址为0x40021400,在这里我们要使用的是PF9引脚,因此配置GPIOx_MODER时,它的偏移地址就相对于GPIOF的基地址而言,
同样的,在这里我们要对模式寄存器GPIOF_MODER进行赋值,先将其地址强转为指针,
(volatile unsigned int *)(0x40021400+0x00)
通过指针解引用对寄存器中内容进行赋值操作
*((volatile unsigned int *)(0x40021400+0x00)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x00)) |= (0x01<<2*9);
//在这里我们是需要将19和18位中的数据赋值为0和1,代表着将PF9选择为输出模式
//同时为了不影响其他位的数值,先将19和18位这两位清零,然后或上01,
接着配置端口输出类型寄存器(GPIOF_OTYPER)
将其地址强转为指针:
(volatile unsigned int *)(0x40021400+0x04)
通过解引用指针对寄存器中内容操作:
*((volatile unsigned int *)(0x40021400+0x04)) &= ~(0x01<<9);
//将第九位置零,为引脚PF9选择推挽输出
接下来是GPIO端口输出速度寄存器
其实这里端口输出速度对于我们点亮led灯没有什么实际用处,陪不配置都行,不过在这里我们还是给它配置一个50Hz的输出速度:
*((volatile unsigned int *)(0x40021400+0x08)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x08)) |= (0x02<<2*9);
配置上拉或下拉
在这里我们选择配置为带上拉输出:
*((volatile unsigned int *)(0x40021400+0x0C)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x0C)) |= (0x01<<2*9);
最后终于到我们的数据输出寄存器了
将该寄存器中的第九位数值赋0,即可让LED0亮
*((volatile unsigned int *)(0x40021400+0x014)) &= ~(0x01<<9);
上面我们配置了那么多个寄存器,现在来汇总一下点亮第一个LED灯的最终版代码:
/*
*点亮第一个LED led.c
*引脚PF9对应着LED0,当PF9输出低电平时,LED0亮
*/
int main(void)
{
// 开启时钟 GPIOF
*((volatile unsigned int *)(0x40023800+0x30)) |= (0x01<<5);
// 选择PF9的模式为输出模式
*((volatile unsigned int *)(0x40021400+0x00)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x00)) |= (0x01<<2*9);
//配置PF9的输出类型为推挽输出
*((volatile unsigned int *)(0x40021400+0x04)) &= ~(0x01<<9);
//配置PF9的输出速度为50Hz
*((volatile unsigned int *)(0x40021400+0x08)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x08)) |= (0x02<<2*9);
//配置PF9 为带上拉输出
*((volatile unsigned int *)(0x40021400+0x0C)) &= ~(0x03<<2*9);
*((volatile unsigned int *)(0x40021400+0x0C)) |= (0x01<<2*9);
//PF9输出低电平,LED0亮
*((volatile unsigned int *)(0x40021400+0x014)) &= ~(0x01<<9);
}
没错,上面就是不依赖任何库,通过直接操作寄存器来实现的点亮第一个LED
(话说翻看手册看得有点眼花,这是正常的,因为寄存器实在是太多了……)
而且有没有发现,上面的代码基本上是对一堆数字(地址)进行操作,一旦离开手册,你又如何记得那些数字代表什么呢?纵使记忆力惊人,这全部记下来也是不现实的,那么接下来我们将它包装一下,让它看起来人性化一点:
/*
*新版本的led.c
*/
#define RCC_BASEADDR 0x40023800
#define RCC_AHB1ENR *((volatile unsigned int *)(0x40023800+0x30))
#define GPIOF_BASEADDR 0x40021400
#define GPIOF_MODER *((volatile unsigned int *)(GPIOF_BASEADDR+0x00))
#define GPIOF_OTYPER *((volatile unsigned int *)(GPIOF_BASEADDR+0x04))
#define GPIOF_OSPEEDR *((volatile unsigned int *)(GPIOF_BASEADDR+0x08))
#define GPIOF_PUPDR *((volatile unsigned int *)(GPIOF_BASEADDR+0x0C))
#define GPIOF_ODR *((volatile unsigned int *)(GPIOF_BASEADDR+0x014))
//通过上面的一些宏定义,我们用一些通俗易看的单词简写组合去代表寄存器,
//下面,则可以通过这些替代的符号对寄存器中的内存进行操作
// 开启时钟 GPIOF
RCC_AHB1ENR |= (0x01<<5);
// 选择PF9的模式为输出模式
GPIOF_MODER &= ~(0x03<<2*9);
GPIOF_MODER |= (0x01<<2*9);
//配置PF9的输出类型为推挽输出
GPIOF_OTYPER &= ~(0x01<<9);
//配置PF9的输出速度为50Hz
GPIOF_OSPEEDR &= ~(0x03<<2*9);
GPIOF_OSPEEDR |= (0x02<<2*9);
//配置PF9 为带上拉输出
GPIOF_PUPDR &= ~(0x03<<2*9);
GPIOF_PUPDR |= (0x01<<2*9);
//PF9输出低电平,LED0亮
GPIOF_ODR &= ~(0x01<<9);
上面的代码看起来,是不是相对舒服了点,起码左边的单词我大概能看懂是什么意思了,至于右边,是位带运算的赋值操作,这个看不懂的话就要复习C语言了
关于STM32中的地址映射
之所以说STM32是32位单片机,是因为它由32根地址线,可产生2的32次方=4G的寻址空间,不过这4G 的地址空间ARM公司在设计内核的时候已经已经大致分配好了。它把从0x40000000至0x5FFFFFFF(512MB)的地址分配给片上外设。通过把片上外设的寄存器映射到地址区,就可以简单的以访问内存的方式,访问这些外设的寄存器,从而控制外设的工作。
下面来粗略描绘一下如何查找一个寄存器的地址:
总结一下,我们在上面要操作的寄存器,无非都是要先找到该寄存器在内存空间中的地址,然后将地址强转为指针,通过指针去操作寄存器中的值,这也告诉我们,C语言指针在实际应用中是非常重要的!!!务必要熟练掌握指针的运算,这样面对这些代码才不会晕头转向的。
那么针对于上面的新版led.c ,我们还可以再进行改版,,因为我们发现,上面仅仅是配置一个引脚PF9,就用了很多个宏定义;那么我要使用其他组别的GPIO口,也要给出类似那么多个宏定义,如下:
#define GPIOA_BASEADDR 0x40020000
#define GPIOE_BASEADDR 0x40021000
#define GPIOF_BASEADDR 0x40021400
#define GPIOA_MODER *((volatile unsigned int *)(GPIOA_BASEADDR+0x00)) //GPIOA组
#define GPIOA_OTYPER *((volatile unsigned int *)(GPIOA_BASEADDR+0x04))
#define GPIOA_OSPEEDR *((volatile unsigned int *)(GPIOA_BASEADDR+0x08))
#define GPIOA_PUPDR *((volatile unsigned int *)(GPIOA_BASEADDR+0x0C))
#define GPIOA_IDR *((volatile unsigned int *)(GPIOA_BASEADDR+0x010))
#define GPIOE_MODER *((volatile unsigned int *)(GPIOE_BASEADDR+0x00)) //GPIOE组
#define GPIOE_OTYPER *((volatile unsigned int *)(GPIOE_BASEADDR+0x04))
#define GPIOE_OSPEEDR *((volatile unsigned int *)(GPIOE_BASEADDR+0x08))
#define GPIOE_PUPDR *((volatile unsigned int *)(GPIOE_BASEADDR+0x0C))
#define GPIOE_IDR *((volatile unsigned int *)(GPIOE_BASEADDR+0x010))
#define GPIOE_ODR *((volatile unsigned int *)(GPIOE_BASEADDR+0x014))
#define GPIOF_MODER *((volatile unsigned int *)(GPIOF_BASEADDR+0x00)) //GPIOF组
#define GPIOF_OTYPER *((volatile unsigned int *)(GPIOF_BASEADDR+0x04))
#define GPIOF_OSPEEDR *((volatile unsigned int *)(GPIOF_BASEADDR+0x08))
#define GPIOF_PUPDR *((volatile unsigned int *)(GPIOF_BASEADDR+0x0C))
#define GPIOF_ODR *((volatile unsigned int *)(GPIOF_BASEADDR+0x014))
但是查看上面的代码,又发现他们其实有相似之处,此处有没有很想艾特一下结构体呢?
没错,如果把每一组的GPIO当成一个结构体,那么他们的成员属性是相同的,假设我们这样定义一个结构体:
typedef struct{
//根据结构体字节对齐原则,可以得到每个成员的地址偏移如下
volatile unsigned int MODER; //0x00
volatile unsigned int OTYPER; //0x04
volatile unsigned int OSPEEDR; //0x08
volatile unsigned int PUPDR; //0x0c
volatile unsigned int IDR; //0x10
volatile unsigned int ODR; //0x14
}GPIO_Typedef;
//此时GPIO_Typedef是结构体类型,类似于int类型,要使用该类型的结构体,就定义相应的变量
此时这个结构体就可以大家共用了,它的地址偏移完全对得上号
//要使用哪一组的GPIO,则进行相应的宏定义
#define GPIOA_BASEADDR 0x40020000
#define GPIOE_BASEADDR 0x40021000
#define GPIOF_BASEADDR 0x40021400
#define GPIOA (GPIO_Typedef *)(GPIOA_BASEADDR+0x00)
#define GPIOE (GPIO_Typedef *)(GPIOE_BASEADDR+0x00)
#define GPIOF (GPIO_Typedef *)(GPIOF_BASEADDR+0x00)
//此时GPIOF是一个GPIO_Typedef类型的指针,通过该指针可以通过->访问结构体成员
//例如要将PF9引脚设置成输出模式,就可以写成如下:
GPIOF->MODER &= ~(0x03<<2*9);
GPIOF->MODER |= (0x03<<2*9);
经过上面的这么一些操作,我们又可以简化一些繁杂的操作了……可能到这里有些学过固件库的同学已经看得很熟悉了,,没错,这就是固件库的由来了(可以说是指针和结构体的完美结合),固件库其实就是将各种寄存器进行封装得到的固件库源码包(里面封装了各种外设接口);我们平时开发主要也是用固件库进行开发,因为开发时间快,不用老是去翻看手册查各种寄存器,但是呢其实直接寄存器操作是效率更高的,因为固件库封装了一堆函数虽然让我们比较容易看懂了,但是函数的压栈出栈是要占用时间的;因此有时候根据需要我们也经常两者结合使用。
上一篇:stm32新手入门遇到的问题
下一篇:通用同步异步收发器 (USART)的使用
推荐阅读最新更新时间:2024-11-13 12:30
设计资源 培训 开发板 精华推荐
- A6261 受保护 LED 阵列驱动器的典型应用
- L78L18AC 正压稳压器的典型应用,用于高输出电流短路保护
- OP484FSZ输出过载恢复运放测试电路典型应用
- TWR-56F8400,用于 DSC 的开发塔式系统模块 MC56F84789 MCU 用于电机和功率控制塔式系统模块,用于节能创新
- SPIRIT1-低数据速率收发器-915 MHz-USB dongle
- AD9446、16 位、80-MSPS ADC 用作测试平台,仅更改了背对背二极管的源
- KIT50XS4200EKEVB: 评估套件 - MC50XS4200,双高边开关
- LTC5542 1.6GHz 至 2.7GHz 高动态范围下变频混频器的典型应用
- 制作USB-C电源(深入了解USB-PD原理,含原理图和英文设计说明等)
- 使用 Analog Devices 的 LT1816 的参考设计