历史上的今天
返回首页

历史上的今天

今天是:2025年02月27日(星期四)

正在发生

2020年02月27日 | STM32直接操作寄存器点亮第一个LED灯

2020-02-27 来源:eefocus

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


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

推荐阅读

史海拾趣

CT [ Central Technologies ]公司的发展小趣事

经过数年的努力,CT公司终于研发出了一款具有颠覆性的新型芯片技术。这款芯片在性能上大幅超越了当时的同类产品,引起了市场的广泛关注。多家知名企业纷纷与CT公司展开合作,将这款芯片应用于他们的产品中。随着合作伙伴的增加和市场份额的扩大,CT公司的知名度逐渐提升,成为了电子行业的一匹黑马。

Grande Electronics Ltd公司的发展小趣事
定期清洁遥控器表面的灰尘和污垢,避免按键接触不良等问题。
Hittite Microwave(ADI)公司的发展小趣事
如果电源线路存在断路或短路问题,需要仔细检查并修复线路。
Comtronic Gmbh-Verbindungstechnik公司的发展小趣事
按照说明书正确使用遥控器,并存放在干燥、通风、无腐蚀性气体的环境中,以延长使用寿命。
Chip Supply Micro Devices公司的发展小趣事

Chip Supply Micro Devices非常重视产学研合作,积极与高校和研究机构建立合作关系。通过与高校和研究机构的合作,公司获得了前沿的技术支持和人才培养资源。同时,公司还设立了专门的研发团队,专注于微电子器件领域的创新研究。这些合作和研发活动为公司带来了源源不断的技术创新和产品升级,为公司的持续发展提供了强大动力。

Alpha Semiconductor公司的发展小趣事

Alpha Semiconductor位于美国加州硅谷的研发与生产基地是公司发展的重要支撑。在这里,公司拥有一流的研发团队和先进的生产设备,能够为客户提供高质量的产品和服务。硅谷作为全球科技创新的中心之一,为Alpha Semiconductor提供了丰富的技术资源和人才支持,使得公司能够不断推陈出新,保持在半导体行业的领先地位。

通过以上五个故事,我们可以看到Alpha Semiconductor公司在电子行业中的发展历程。从初创时期的代工服务起步,到后来的OEM定制产品、自主研发电源管理产品,再到高性能标准模拟和混合信号IC的制造实力,以及在美国硅谷的研发与生产基地的建立,每一个阶段都见证了公司的成长和进步。Alpha Semiconductor凭借着不断的技术创新和市场拓展,已经成为了半导体行业中的佼佼者。

问答坊 | AI 解惑

航海设备专用液晶显示器WEDC LCD

ENH038QD1-450/650 尺寸大小: 3.8" 分辨率: QVGA 320 x 240 接口: 6-bits TTL interface 亮度 : 450/650nit(cd/m2) 对比度:高对比度、大开口率 显示模式:Normally White 响应时间:Rise 30ms/ Fall 50ms 功耗:3.4W 工作温度: -30℃--- ...…

查看全部问答>

2009IEEE工程管理与服务科学国际会议征文klj

EMS 2009 Call for Papers: Sept. 20-22, 2009, Beijing, China ====================================================================== The 3rd Int’l Conference on Engineering Management and Service Sciences (EMS 2009) CALL FOR PAP ...…

查看全部问答>

lbing7来拿分

对于我这个刚入门的人,lbing7能耐心讲解基础,十分感谢。 由于到目前问的问题都是十分基础的,我就不写出来了。…

查看全部问答>

准备酝酿一个玩wince的人的开心网

我们刚创建的wince家园,想让所有玩wince的人变成朋友。 logo都没来得及做。想先看看坛子上面兄弟的看法。 目的是想找同道之人共同参与,共同出出好点子,让我们搞wince的变成一个强大的同盟。 试运行网站 http://www.armce.cn/ 嘿嘿~~~ 预祝xd ...…

查看全部问答>

wince 问题!!!!!!

有高人知道在wince工程下面 怎么给lable button等控件添加背景图片么?…

查看全部问答>

如何通过2440spi总线读写外设的寄存器?

平台:arm开发板,用spi总线外接1个fir红外芯片,s3c2440 + linux2.6.24.7 现在2440这边已经做好了,insmod bitbang.ko,s3c24xx.ko,insmod spidev.ko,用testspi,用示波器可以测到时钟和数据的波形都是对的,但不知道如何读写fir的寄存器?以前都 ...…

查看全部问答>

请教版主STM323.0的库函数有没有IIC的问题?

请教版主STM32 3.0的库函数有没有IIC的问题? 正准备做个项目希望 版主 给个答复!!…

查看全部问答>

收到348的套件了,说一下

收到套件 ,发现原装的也是配了很多made in china的东东? [ 本帖最后由 pig163xx 于 2011-10-29 22:10 编辑 ]…

查看全部问答>

请大家注意,用F28027的Lib可能存在Bug!

最近一直在学习F28027的库开发模式,用下来感觉其实一般,Ti官方的手册中也没有一个标准,什么函数什么时候调用,前后有什么顺序要求,就是给了一些例子和函数说明,看起来有些模糊。今天在调试SCI的程序,突然发现用库模式的SCI_LOOPBack_INITTrup ...…

查看全部问答>

launchpad c2000毕设,求高手!求意见!

以LaunchPadXL-C2000为核心板,设计一款可实现正弦波、方波和三角波信号输出的多函数信号发生器。信号频率0.1Hz-1MHz,输出幅度0.1V-5V可调,信号类型、频率及幅度均由按键选定。…

查看全部问答>