STM32直接操作寄存器点亮第一个LED灯

来做个实验:不依赖任何库,直接操作寄存器点亮第一个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);

经过上面的这么一些操作,我们又可以简化一些繁杂的操作了……可能到这里有些学过固件库的同学已经看得很熟悉了,,没错,这就是固件库的由来了(可以说是指针和结构体的完美结合),固件库其实就是将各种寄存器进行封装得到的固件库源码包(里面封装了各种外设接口);我们平时开发主要也是用固件库进行开发,因为开发时间快,不用老是去翻看手册查各种寄存器,但是呢其实直接寄存器操作是效率更高的,因为固件库封装了一堆函数虽然让我们比较容易看懂了,但是函数的压栈出栈是要占用时间的;因此有时候根据需要我们也经常两者结合使用。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值