基于STM32F407的LED灯和蜂鸣器的实现(寄存器版本)

目录

一、问题的引入

二、STM32F4xxGPIO内部结构 

三、 STM32F4xxGPIO引脚简介 

四、STM32F4xxGPIO寄存器说明 

五、STM32F4xxGPIO时钟使能 

         1、练习: 点亮四个灯 

2、练习:实现流水灯 

3、练习:蜂鸣器的使用


一、问题的引入

学习STM32单片机前,要先了解单片机的最小系统组成

单片机的最小系统由以下四部分组成:
        芯片+晶振电路+复位电路+供电电路
    一个完整的系统则是:
        最小系统+其它的外设
    芯片: 
        整个系统的核心,相当于人类的大脑,会提供引脚与外部电路相连 
        芯片四周那些银白色的引脚是从芯片内部引申出来的。它负责芯片内部的控制单元与外部硬件的连接。
        那么一个引脚其实本质上就是一根"电线"。
    引脚拥有输入功能/输出功能,比如:
        1.引脚可以输出/输入一个电平信号(1/0)
            那么电平信号是针对于CPU来说的,因为CPU只能识别二进制0/1(数字信号)
        2.也可以称引脚可以输入/输出一个高低电压 
            因为对于外部电路来说,外部电路的工作是采用模拟电压信号(即多少V电压)
        为什么一个引脚对CPU和外部的电路的输入/输出是不一样的信号?它是怎么做到的?
            这是因为引脚在芯片内部还需要通过"控制单元"才能够进入CPU
        控制单元:
            不同硬件对应不同的控制单元:
                GPIO --->GPIO控制器 
                USART --->USART控制器 
                ......


二、STM32F4xxGPIO内部结构 


    参考<STM32F4xx中文参考手册.pdf>,英语基础比较好的同学也可以去看<STM32F4xx英文参考手册.pdf>。这两个手册可以到官网去下载,但是一般是全英文的。想要下载中文版的可以进入意法半导体STM32/STM8技术社区www.stmcu.org.cn ,里面有着STM32/STM8相关型号的芯片的各种资料。


    通过图得知,每个GPIO可以独立配置成不同的功能
    GPIO配置的功能如下:
    (1)输入功能 
        CPU可以通过该GPIO获取外部电路输入的一个电平状态 
        输入又分成以下几种模式:
        a.浮空输入:不接上拉电阻也不接下拉电阻 
            在这种情况下,IO引脚的电平状态完全由外部输入决定  3.3V-->1  0V-->0
        b.带上拉的输入:通过上拉电阻,接了VDD 
            此时,如果外部的引脚没有任何的输入的时候,CPU也能获取一个高电平(1)
            只有在外部输入一个低电平的时候,才能读取到一个低电平
        c.带下拉的输入:通过下拉电阻,接入了VSS 
            此时,如果外部的引脚没有任何的输入的时候,CPU也能获取一个低电平(0)
            只有在外部输入一个高电平的时候,才能读取到一个高电平 
        d.模拟输入 
            该引脚被配置为模拟输入,能够获取到的就是模拟信号
            通过ADC转换获取数字量 
    (2)输出功能 
        CPU可以通过该GPIO向外部电路输出一个电平 
        a.输出推挽(PP)
            CPU可以向外部电路输出高电平也可以输出低电平
        b.输出开漏(OD)
            CPU输出不了高电平,没有P-MOS管 
            CPU正常输出低电平 
    (3)复用功能(AF)
        复用功能是指GPIO用作其它的外设功能线 
        比如: 
            IIC、SPI、USART.....
        每个GPIO口理论上都可以配置成16种复用功能 
        具体哪个引脚可以复用成什么功能,由原理图以及手册决定

三、 STM32F4xxGPIO引脚简介 


    GPIO是什么 ?  通用功能输入输出
    GPIO就是从芯片内部引出的一根功能复用的线
    功能复用:GPIO引脚可以由CPU去配置成不同的功能
    比如: 
        输入功能、输出功能、复用功能、模拟功能等等
    STM32F407 共有144个引脚
    分成9组,记为GPIOA、GPIOB、.....GPIOI 
    简写模式:PA、PB.....
    每个组有16个引脚,编号从0~15 
    比如GPIOA组的引脚就有: 
        PA0~PA15 
    而这些GPIO引脚的功能,都有独立的寄存器组来配置,如果我们要使用这个GPIO,那么就必须先去配置它的寄存器组
    那么如果我们要去配置这些寄存器,就必须知道寄存器的地址。
    每组GPIO地址分布如下:
        可以通过<STM32F4xx中文参考手册>第2章可以查看
                    边界地址           外设        总线                   
            0x4002 2000 - 0x4002 23FF GPIOI                                                                
            0x4002 1C00 - 0x4002 1FFF GPIOH
            0x4002 1800 - 0x4002 1BFF GPIOG
            0x4002 1400 - 0x4002 17FF GPIOF
            0x4002 1000 - 0x4002 13FF GPIOE        AHB1
            0x4002 0C00 - 0x4002 0FFF GPIOD
            0x4002 0800 - 0x4002 0BFF GPIOC
            0x4002 0400 - 0x4002 07FF GPIOB
            0x4002 0000 - 0x4002 03FF GPIOA
    
    上述的表有几个名字需要解释: 
        边界地址:指寄存器组的起始地址(基址)和结束地址
        外设:该寄存器组对应的硬件控制器
        总线:该硬件控制器挂载的时钟线 
    那么对于GPIO来说,这些寄存器分别用来干嘛?


四、STM32F4xxGPIO寄存器说明 


    每个通用 I/O 端口包括
        4个32位配置寄存器(GPIOx_MODER、GPIOx_OTYPER、GPIOx_OSPEEDR 和 GPIOx_PUPDR)
        2个32位数据寄存器(GPIOx_IDR 和GPIOx_ODR)
        1个32位置位/复位寄存器 (GPIOx_BSRR)
        1个32位锁定寄存器(GPIOx_LCKR)
        2个32位复用功能选择寄存器(GPIOx_AFRH 和 GPIOx_AFRL)
    其中,上述寄存器名称中x表示GPIO分组,x=A.....I 
    下面学习寄存器
    (1)GPIO端口模式寄存器(GPIOx_MODER) (x = A..I)
        偏移地址:0x00  (基址+偏移地址 == 寄存器地址)
        比如:GPIOC_MODER地址就是0x40020800+0x00
        该寄存器用来控制GPIOx组的16个GPIO引脚的模式(4种:输入、输出、模拟、复用)
        每个引脚占用2bits
        编号为y(0~15)的GPIO引脚对应的位是该寄存器的[2y+1:2y]
        具体的配置如下: 
            GPIOx_MODER[2y+1:2y]      模式 
                00                 输入模式 
                01                 输出模式 
                10                 复用模式 
                11                 模拟模式 
        例子: 
            用C代码把PF9配置为输出模式 
            分析: 
                PF组的基址:0x40021400
                模式寄存器的偏移地址:0x00
                所以GPIOF_MODER地址:0x40021400+0x00 
            如果要把PF9配置为输出模式,就需要将GPIOF_MODER[19:18]-->01 
            把地址为0x40021400的寄存器中bit19清0,bit18置1
            怎么做?
            在STM32中表示地址用 unsigned long来表示地址 
                unsigned long *p = (unsigned long *)0x40021400;
            但是一般情况下我们会在地址前加一个volatile,如下
            volatile unsigned long *p = (volatile unsigned long *)0x40021400;
            volatile的作用是作为指令关键字,进制编译器优化,访问的就是实际地址,不需要优化。
                bit19清0,bit18置1:
                *p = *p & ~(1<<19);
                *p = *p | (1<<18);
            但是如上的操作实际上对寄存器进行了两次操作,效率比较低,能不能对寄存器一次性修改到位?可以
                比如: 
                    unsigned long r = *p;
                        r&=(~(1<<19));
                        r|=(1<<18);
                        *p = r;//通过中间变量,一步到位
    (2)GPIO 端口输出类型寄存器 (GPIOx_OTYPER) (x = A..I) 
            偏移地址:0x04
        输出类型有两种:推挽和开漏 
        该寄存器用来选择GPIOx组的16个引脚的输出类型
        每个引脚占1bit,编号为y的引脚对应比特位就是GPIOx_OTYPER[y] 
        具体配置如下: 
            GPIOx_OTYPER[y]    输出类型 
                1                开漏 
                0                推挽 
    (3)GPIO端口输出速度寄存器 (GPIOx_OSPEEDR) (x = A..I/)
        偏移地址:0x08
        用来控制GPIOx组的16个引脚的输出速率,每个引脚占2bits,编号为y的引脚对应比特位为[2y+1:2y] 
        具体配置速率如下:
            GPIOx_OSPEEDR[2y+1:2y]     速率 
                00                     低速 
                01                     中速
                10                     快速
                11                     高速 
    (4)GPIO 端口上拉/下拉寄存器 (GPIOx_PUPDR) (x = A..I/)
        偏移地址:0x0C
        用来控制GPIOx组的16个引脚的上下拉电阻的选择
        每个引脚占2bits,编号为y的引脚对应比特位为[2y+1:2y]
        具体配置如下:
            GPIOx_PUPDR[2y+1:2y]     上下拉选择 
                00                    无上下拉
                01                      上拉 
                10                      下拉 
                11                      保留,未使用
    (5)GPIO 端口输入数据寄存器 (GPIOx_IDR) (x = A..I)
        偏移地址:0x10 
        该寄存器可以用来表示GPIOx组的16个引脚的输入电平值
        每个引脚占1bits,该寄存器中只有[15:0]是可以使用的 
        GPIOx_IDR[y] --->GPIOxy的输入电平 
        因此 
            GPIOx_IDR[y]     y号引脚的电平 
                1               高电平
                0               低电平 
        比如: 
            CPU想要知道GPIOA7是高电平还是低电平:
            if(GPIOA_IDR & (1<<7))
            {
                PA7是高电平
            }
            else
            {
                PA7是低电平 
            }
    (6)GPIO 端口输出数据寄存器 (GPIOx_ODR) (x = A..I)    
        偏移地址:0x14 
        表示GPIOx组的16个引脚的输出值 
        具体配置 
            GPIOx_ODR[y]    y号引脚的电平   
                1               高电平
                0               低电平 
    (7)GPIO 端口置位/复位寄存器 (GPIOx_BSRR) (x = A..I)
        偏移地址:0x18 
        Bit Set  --->bit位置1 
        Bit Reset  --->bit位清0 
        该寄存器用来表示GPIOx组的16个引脚的输出状态
        其中 
            高16位用作端口复位寄存器
            低16位用作端口置位寄存器 
        这个寄存器有点特殊,写1有效,写0是无效 
        注意:如果同时对 BSx 和 BRx 置位,则 BSx 的优先级更高
    (8)GPIO 端口配置锁定寄存器 (GPIOx_LCKR) (x = A..I) 
    (9)GPIO 复用功能低位寄存器 (GPIOx_AFRL) (x = A..I)
    (10)GPIO 复用功能高位寄存器 (GPIOx_AFRH) (x = A..I)
        GPIOx_AFRL和GPIOx_AFRH两个寄存器放在一起使用的
        因为一个GPIO引脚最多有16种复用功能,那么1一个引脚需要占用4bits
        所以一个组的16个引脚必须需要2个寄存器才能够用
        GPIO编号为0~7由GPIOx_AFRL寄存器管理
        GPIO编号为8~15由GPIOx_AFRH寄存器管理 
    具体哪个值是何种复用,要结合电路图和功能手册才能看


五、STM32F4xxGPIO时钟使能 


    根据上述的寄存器,就可以去实现所有基于GPIO能够完成的功能
        比如: 
            LED点灯
            蜂鸣器
            按键
            .....
    之前有说过,任何一个硬件控制器想要工作,都必须使能时钟(GPIO所有的分组全部属于AHB1时钟线)
    那么时钟的相关配置,参考RCC
    RCC:Reset Clock Control 复位时钟控制 
        0x4002 3800 - 0x4002 3BFF RCC  基址:0x40023800
    那么我们现在的目的就是找到有关AHB1外设使能的寄存器
        RCC_AHB1ENR  偏移地址:0x30
    此寄存器[8:0]分别控制了GPIOx的时钟使能 
        1     使能时钟 
        0     禁止时钟 
    比如: 
        把RCC_AHB1ENR[5]--->1 
        使能了GPIOF组的时钟 
总结: 
    利用寄存器实现GPIO功能的配置的步骤
    1)从原理图找出对应的引脚 比如:点灯 
            VCC---D1--PF9 
        经过分析:CPU对PF9输出一个低电平,D1就会亮 
    2)配置GPIO分组时钟
    3)配置GPIO模式寄存器
    4)配置GPIO输出类型
    5)配置GPIO输出速率寄存器
    6)配置GPIO上下拉寄存器
    7)输出模式,对输出数据寄存器进行操作,输入模式,读取输入数据寄存器


    
1、练习: 点亮四个灯 


        D1--PF9
        D2--PF10
        D3--PE13
        D4--PE14

leg_reg.c

#include "led_reg.h"

void led_reg_init(void)
{
	unsigned long r = 0;
	//1.使能GPIOE组和GPIOF组的时钟
	rRCC_AHB1ENR |= (1<<5);
	rRCC_AHB1ENR |= (1<<4);
	
	//2.配置功能模式 输出模式
	//PF9
	r = rGPIOF_MODER;
	r &= ~(1<<19);
	r |= (1<<18);
	//PF10
	r &= ~(1<<21);
	r |= (1<<20);
	rGPIOF_MODER = r;
	//PE13
	r = rGPIOE_MODER;
	r &= ~(1<<27);
	r |= (1<<26);
	//PE14
	r &= ~(1<<29);
	r |= (1<<28);
	rGPIOE_MODER = r;

	//3.配置输出类型 推挽输出
	r = rGPIOF_OTYPER;
	r &= (~(1<<9));
	r &= (~(1<<10));
	rGPIOF_OTYPER = r;

	r = rGPIOE_OTYPER;
	r &= (~(1<<13));
	r &= (~(1<<14));
	rGPIOE_OTYPER = r;

	//4.配置输出速率  
	r = rGPIOF_OSPEEDR;
	r &= ~(1<<19);
	r |= (1<<18);
	r &= ~(1<<21);
	r |= (1<<20);
	rGPIOF_OSPEEDR = r;

	r = rGPIOE_OSPEEDR;
	r &= ~(1<<27);
	r |= (1<<26);
	r &= ~(1<<29);
	r |= (1<<28);
	rGPIOE_OSPEEDR = r;

	//5.配置上下拉 输入悬空
	r = rGPIOF_PUPDR;
	r &= ~(1<<19);
	r &= ~(1<<18);
	r &= ~(1<<21);
	r &= ~(1<<20);
	rGPIOF_PUPDR = r;

	r = rGPIOE_PUPDR;
	r &= ~(1<<27);
	r &= ~(1<<26);
	r &= ~(1<<29);
	r &= ~(1<<28);
	rGPIOE_PUPDR = r;

	//6.默认状态(输出1)
	r = rGPIOF_ODR;
	r |= (1<<9);
	r |= (1<<10);
	rGPIOF_ODR = r;

	r = rGPIOE_ODR;
	r |= (1<<13);
	r |= (1<<14);
	rGPIOE_ODR = r;
}

void led_Ctrl(int led_num,int status)
{

	if(led_num == D1)
	{
		status ? D1_OFF : D1_ON;
	}
	if(led_num == D2)
	{
		status ? D2_OFF : D2_ON;
	}
	if(led_num == D3)
	{
		status ? D3_OFF : D3_ON;
	}
	if(led_num == D4)
	{
		status ? D4_OFF : D4_ON;
	}
}

void delay(void)     //延时函数、延时的时间可以自己改
{
	int i;
	for(i=0;i<100000;i++);
}


leg_reg.h

#ifndef __LED_REG_H__
#define __LED_REG_H__

/*AHB1寄存器*/
#define rRCC_AHB1ENR *((volatile unsigned long *)(0x40023800+0x30))
/*GPIO每组的基址*/
#define GPIOA_BASE 0x40020000
#define GPIOB_BASE 0x40020400
#define GPIOC_BASE 0x40020800
#define GPIOD_BASE 0x40020C00
#define GPIOE_BASE 0x40021000
#define GPIOF_BASE 0x40021400
#define GPIOG_BASE 0x40021800
#define GPIOH_BASE 0x40021C00
#define GPIOI_BASE 0x40022000
/*GPIOF相关的寄存器*/
#define rGPIOF_MODER *((volatile unsigned long *)(GPIOF_BASE+0x00))
#define rGPIOF_OTYPER *((volatile unsigned long *)(GPIOF_BASE+0x04))
#define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE+0x08))
#define rGPIOF_PUPDR *((volatile unsigned long *)(GPIOF_BASE+0x0C))
#define rGPIOF_IDR *((volatile unsigned long *)(GPIOF_BASE+0x10))
#define rGPIOF_ODR *((volatile unsigned long *)(GPIOF_BASE+0x14))
/*GPIOE相关的寄存器*/
#define rGPIOE_MODER *((volatile unsigned long *)(GPIOE_BASE+0x00))
#define rGPIOE_OTYPER *((volatile unsigned long *)(GPIOE_BASE+0x04))
#define rGPIOE_OSPEEDR *((volatile unsigned long *)(GPIOE_BASE+0x08))
#define rGPIOE_PUPDR *((volatile unsigned long *)(GPIOE_BASE+0x0C))
#define rGPIOE_IDR *((volatile unsigned long *)(GPIOE_BASE+0x10))
#define rGPIOE_ODR *((volatile unsigned long *)(GPIOE_BASE+0x14))

/*灯的编号*/
enum LED_NUM
{
	D1,
	D2,
	D3,
	D4
};
/*灯的状态*/
enum LED_STATUS
{
	ON,
	OFF
};
//灯的控制
#define D1_ON (rGPIOF_ODR &= ~(1<<9))  
#define D1_OFF (rGPIOF_ODR |= (1<<9)) 
#define D2_ON (rGPIOF_ODR &= ~(1<<10))  
#define D2_OFF (rGPIOF_ODR |= (1<<10)) 
#define D3_ON (rGPIOE_ODR &= ~(1<<13))  
#define D3_OFF (rGPIOE_ODR |= (1<<13)) 
#define D4_ON (rGPIOE_ODR &= ~(1<<14))  
#define D4_OFF (rGPIOE_ODR |= (1<<14))



void led_reg_init(void);
void led_Ctrl(int led_num,int status);
void delay(void);
#endif

main.c

#include "led_reg.h"

int main()
{
	led_reg_init();
	led_Ctrl(D1, ON);
	led_Ctrl(D2, ON);
	led_Ctrl(D3, ON);
	led_Ctrl(D4, ON);
	while(1);
//	while(1)
//	{
//		led_Ctrl(D1, ON);
//		delay();
//		led_Ctrl(D1, OFF);
//		
//		led_Ctrl(D2, ON);
//		delay();
//		led_Ctrl(D2, OFF);

//		led_Ctrl(D3, ON);
//		delay();
//		led_Ctrl(D3, OFF);

//		led_Ctrl(D4, ON);
//		delay();
//		led_Ctrl(D4, OFF);
//	}
//	beep_Ctrl(beep_ON);
}


        
手动编写一个假延时函数: 
    void mydelay(int ms)
    {
        int i;
        while(ms--)
        {
            for(i=0;i<0x6000;i++);
        }
    }
        


2、练习:实现流水灯 


        现象:D1亮-->延时-->D1灭-->D2亮-->D2灭->......>D4灭 

调用上面的leg_reg.c和leg_reg.h,将main()修改一下即可

main.c

#include "led_reg.h"
#include "beep_reg.h"
#include "key_reg.h"

int main()
{
	led_reg_init();
//	led_Ctrl(D1, ON);
//	led_Ctrl(D2, ON);
//	led_Ctrl(D3, ON);
//	led_Ctrl(D4, ON);
//	while(1);
	while(1)
	{
		led_Ctrl(D1, ON);
		delay();
		led_Ctrl(D1, OFF);
		
		led_Ctrl(D2, ON);
		delay();
		led_Ctrl(D2, OFF);

		led_Ctrl(D3, ON);
		delay();
		led_Ctrl(D3, OFF);

		led_Ctrl(D4, ON);
		delay();
		led_Ctrl(D4, OFF);
	}
//	beep_Ctrl(beep_ON);
}

用ARM汇编指令写了一个STM32的项目启动文件

start.s

stack_size EQU 0x200	;定义堆栈的大小
	IMPORT main
;在keil5中定义堆栈段
	AREA my_stack,NOINIT,READWRITE,ALIGN=3
stack_mem	;标记堆栈的开始
	SPACE stack_size	;开辟堆栈空间
stack_end	;标记堆栈从此处开始结束  栈顶指针
	PRESERVE8	;8字节对齐
	THUMB	;处理器均为THUMB状态
;在keil5中定义RESET段
	AREA RESET,DATA,READONLY
__Vectors
	DCD stack_end	;中断向量表的第0个必须是栈顶指针
	DCD __start		;第1个必须是用户代码
	SPACE 0x400		;预留空间
;定义代码段
	AREA |.text|,CODE,READONLY
__start PROC
	BL main
	B .
	ENDP
	END

3、练习:蜂鸣器的使用

beep_reg.c

#include "beep_reg.h"

void beep_reg_init(void)
{
	unsigned long r = 0;
	//1.使能GPIOF组的时钟
	rRCC_AHB1ENR |= (1<<5);
	
	//2.配置功能模式
	//PF8
	r = rGPIOF_MODER;
	r &= ~(1<<17);
	r |= (1<<16);
	rGPIOF_MODER = r;

	//3.配置输出类型
	r = rGPIOF_OTYPER;
	r &= (~(1<<8));
	rGPIOF_OTYPER = r;

	//4.配置输出速率
	r = rGPIOF_OSPEEDR;
	r |= (1<<17);
	r |= (1<<16);
	rGPIOF_OSPEEDR = r;

	//5.配置上下拉
	r = rGPIOF_PUPDR;
	r |= (1<<17);
	r &= ~(1<<16);
	rGPIOF_PUPDR = r;

	//6.默认状态(输出0)
	r = rGPIOF_ODR;
	r &= ~(1<<8);
	rGPIOF_ODR = r;
}

void beep_Ctrl(int status)
{
	status ? BEEP_OFF : BEEP_ON;
}

beep_reg.h

#ifndef __BEEP_REG_H__
#define __BEEP_REG_H__

/*AHB1寄存器*/
#define rRCC_AHB1ENR *((volatile unsigned long *)(0x40023800+0x30))
/*GPIO每组的基址*/
#define GPIOA_BASE 0x40020000
#define GPIOB_BASE 0x40020400
#define GPIOC_BASE 0x40020800
#define GPIOD_BASE 0x40020C00
#define GPIOE_BASE 0x40021000
#define GPIOF_BASE 0x40021400
#define GPIOG_BASE 0x40021800
#define GPIOH_BASE 0x40021C00
#define GPIOI_BASE 0x40022000
/*GPIOF相关的寄存器*/
#define rGPIOF_MODER *((volatile unsigned long *)(GPIOF_BASE+0x00))
#define rGPIOF_OTYPER *((volatile unsigned long *)(GPIOF_BASE+0x04))
#define rGPIOF_OSPEEDR *((volatile unsigned long *)(GPIOF_BASE+0x08))
#define rGPIOF_PUPDR *((volatile unsigned long *)(GPIOF_BASE+0x0C))
#define rGPIOF_IDR *((volatile unsigned long *)(GPIOF_BASE+0x10))
#define rGPIOF_ODR *((volatile unsigned long *)(GPIOF_BASE+0x14))
/*蜂鸣器的状态*/
enum BEEP_STATUS
{
	beep_ON,
	beep_OFF
};
//蜂鸣器的控制
#define BEEP_OFF (rGPIOF_ODR &= ~(1<<8))  
#define BEEP_ON (rGPIOF_ODR |= (1<<8)) 

void beep_reg_init(void);
void beep_Ctrl(int status);
#endif

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值