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

发布者:游走人间最新更新时间:2020-02-27 来源: eefocus关键字: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);


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

关键字:STM32  直接操作寄存器  LED灯 引用地址:STM32直接操作寄存器点亮第一个LED灯

上一篇:stm32新手入门遇到的问题
下一篇:通用同步异步收发器 (USART)的使用

推荐阅读最新更新时间:2024-11-13 12:30

汽车照明串联LED灯珠的开路保护方案
LED 汽车 照明 灯种类繁多,主要分为内部照明和外部照明两大类。在介绍方案之前,我们得清楚的知道汽车照明到底有哪些?细分如下:   1. 内部照明: 背光,调光,集成HMI   2. 外部照明:前照灯,日间行驶灯(DRL),雾灯(FOG),转向灯(TI),装饰灯,位置灯(POS),远近光灯(HLB),角灯(CRN)   3. 通信支持:SPI/I?C, LIN ,CANBUS   内部照明控制台设计参考:   1. 电容传感按钮用于功能选择   2. 每个按钮均具有RGB背光和LED指示灯   3. 顶灯采用高亮度 白光LED   4. 整个装置采用12V电源输入和CAN/LIN总行I/O   外部照明设计难点:   1
[电源管理]
汽车照明串联<font color='red'>LED灯</font>珠的开路保护方案
STM32内部flash分配
在Keil中编译工程成功后,在下面的Bulid Ouput窗口中会输出下面这样一段信息: Program Size: Code=6320 RO-data=4864 RW-data=44 ZI-data=1636 代表的意思: Code :是程序中代码所占字节大小 RO-data :程序中所定义的指令和常量大小 (个人理解 :Read Only) RW-data :程序中已初始化的变量大小 (个人理解”:Read/Write) ZI-Data :程序中未初始化的变量大小 (个人理解 :Zero Initialize) ROM(Flash) size = Code+RO-data+RW-data; RAM size = RW-data
[单片机]
基于STM32介绍DMA的双缓冲模式
目前STM32家族中有些系列支持DMA的双缓冲模式,比如STM32F2/STM32F4/STM32F7等系列。尤其随着人们对STM32F4/F7系列应用不断拓宽和加深,在设计中运用到DMA双缓冲的场合也越来越多。STM32芯片中的DMA又可分为两大类,一类是通用DMA,一类是专用DMA,比如用于USB,TFT LCD,ETHERNET等外设应用上的DMA。这里要谈的是基于通用DMA的话题,不妨以STM32F4系列芯片为例。 关于STM32F4的DMA双缓冲传输在STM32F4系列的参考手册里做了简单描述。因为它是基于介绍了单缓冲模式的DMA介绍之后接着介绍的,稍显言简意赅。 相比单缓冲的数据流,双缓冲多了一个DMA存储区和
[单片机]
基于<font color='red'>STM32</font>介绍DMA的双缓冲模式
STM32的定时器有两种捕获模式——PWM输入模式和普通输入模式
一个定时器最多能同时捕获几路PWM波? 我只需要得到PWM的高电平宽度,PWM的频率是50HZ STM32的定时器有两种捕获模式 PWM输入模式和普通输入模式 在PWM输入模式下,一个定时器只能同时捕获一路PWM波 在普通输入模式下,理论上是可以同时捕获4路PWM波 即,在定时器中断中改变触发模式(上升沿、下降沿) 然后寄存器两次的差值即近似为高电平长度
[单片机]
STM32之SPI的思考
选择了与硬件打交道,就得戒骄戒躁,踏踏实实,一步一步的走下去。可能因为一个非常小的问题,就导致你失败。失败不可怕,可怕的是不能静下心来去思考。我在公司第一次调试硬件,spi的通信,是stm32的硬件既有的通信接口。之前用51的io口模拟过i2c的,感觉spi相对来说更简单些,结果调试spi的读写花费了我3天的时间。stm32f0与网上关于stm32f1大量的例程还不一样,刚刚上市半年多,应该是,并且有几项设置是stm32f1没有的,这也正是关键的地方。你直接把他们的程序拿过来用,可能就卡死了,为什么?因为f0多了个fifo设置,fifo不设置,默认应该是half of 32 bits,当你只接收到8个bit时,rxne不会置位,程
[单片机]
STM32唯一ID读取方法
STM32唯一ID STM32微控制器有一个96位的产品唯一身份标识,在任何情况下都是唯一且不允许修改 这个96位的产品唯一身份标识,可以以字节(8位)为单位读取,也可以以半字(16位)或者全字(32位)读取。 基地址:0x1FFF F7E8 以全字(32bit)读取 u32 GetLockCode(void) { u32 stm32Id ={0}; stm32Id =*(vu32*)(0x1ffff7e8); stm32Id =*(vu32*)(0x1ffff7ec); stm32Id =*(vu32*)(0x1ffff7f0); return (stm32Id 1)+(stm32Id 2
[单片机]
stm32使用HX711读电子秤的值
使用HX711变送器模块+5kg的传感器。 #define HX711_DATA PEin(0) #define HX711_SCK PEout(1) void HX711_init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOE, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.G
[单片机]
<font color='red'>stm32</font>使用HX711读电子秤的值
STM32模拟I2C时序读写EEPROM精简版
平台:STM32ZET6(核心板)+ST-LINK/V2+SD卡+USB串口线+外部EEPROM(不需要上拉电阻) 工程介绍:主要文件在USER组中,bsp_i2c_ee.c,bsp_i2c_ee.h,bsp_eeprom.c,bsp_eeprom.h和main.c,其中bsp_i2c_ee.c中主要时基本的模拟I2C时序,而bsp_eeprom.c中主要利用前一个文件中定义的基本操作,进行EEPROM的读写操作。其他类似I2C时序的协议,均可以保留bsp_i2c_ee.c的基础上添加新的内容。本文有些内容借鉴了其他网友的总结,在此表示感谢。 1.硬件部分:电路连接较为简单,笔者在淘宝上买的24C02N主要有四根线,两根电
[单片机]
<font color='red'>STM32</font>模拟I2C时序读写EEPROM精简版
小广播
设计资源 培训 开发板 精华推荐

最新单片机文章
何立民专栏 单片机及嵌入式宝典

北京航空航天大学教授,20余年来致力于单片机与嵌入式系统推广工作。

换一换 更多 相关热搜器件
更多往期活动

 
EEWorld订阅号

 
EEWorld服务号

 
汽车开发圈

电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 电信业务审批[2006]字第258号函 京公网安备 11010802033920号 Copyright © 2005-2024 EEWORLD.com.cn, Inc. All rights reserved