您现在的位置是:首页 >其他 >5。STM32裸机开发(1)网站首页其他
5。STM32裸机开发(1)
嵌入式软件开发学习过程记录,本部分结合本人的学习经验撰写,系统描述各类基础例程的程序撰写逻辑。构建裸机开发的思维,为RTOS做铺垫(本部分基于库函数版实现),如有不足之处,敬请批评指正。
(1)中包括led点灯、stm32的各类时钟和简化程序的位带操作
一 点LED灯
实现点灯功能的程序逻辑为改变指定GPIO口的高/低电平(就要根据硬件电路具体判断),作为入门例程,要养成良好的代码习惯,即功能裸机实现代码在main.c中尽可能少的出现。因此此处新建APP文件夹,并在APP文件夹中新建LED文件夹,创建led.c和led.h文件
1)编写led.c文件 ,两步核心,而为了进一步解耦(解耦是贯穿你程序设计的始终思想),可以在.c文件中定义一些宏,这些宏在.h文件中赋值,比如LED_PIN。最后,别忘了将写好的ledinit()函数在.h文件中声明
1.初始化结构体变量并给内部各类变量赋值,
2.使能时钟
GPIO_InitTypeDef GPIO_InitStructure;//初始化结构体变量
RCC_APB2PeriphClockCmd(LED_PORT_RCC,ENABLE);//使能GPIOC外设时钟,此处因为GPIOC挂载在APB2总线上
GPIO_InitStructure.GPIO_Pin=LED_PIN; //设置 IO 口
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_Out_PP; //设置推挽输出模式(众多模式中的一种)
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz; //设置传输速率
/* 初始化 GPIO,这个很重要,第一个形参是 GPIO_TypeDef 类型的指针变
量,而 GPIO_TypeDef 又是一个结构体类型,封装了 GPIO 外设的所有寄存器,
所以给它传送 GPIO 外设基地址即可通过指针操作寄存器内容 */
GPIO_Init(LED_PORT,&GPIO_InitStructure);
GPIO_SetBits(LED_PORT,LED_PIN); //将 LED 端口拉高,熄灭所有 LED
//GPIO_ResetBits(GPIOC,GPIO_Pin_0); //输出低电平
// #define 可以定义各类宏,减少.c文件中的代码量
#define LED_PORT GPIOC
#define LED_PIN
(GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3|GPIO_Pin_4|GPIO_Pin_5|GPIO_Pin_6|GPIO_Pin_7)
#define LED_PORT_RCC RCC_APB2Periph_GPIOC
//写好的ledinit函数别忘了声明
void LED_Init(void);
此处,由GPIO_SetBits(LED_PORT,LED_PIN);GPIO_ResetBits(GPIOC,GPIO_Pin_0); ,可以拓展一些其他的函数
(1)读取输入引脚uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);功能:读取端口中的某个管脚输入电平。底层是通过读取 IDR 寄存器。uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);功能:读取某组端口的输入电平。底层是通过读取 IDR 寄存器。(2)读取输出引脚uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);功能:读取端口中的某个管脚输出电平。底层是通过读取 ODR 寄存器。uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);功能:读取某组端口的输出电平。底层是通过读取 ODR 寄存器。
二 STM32时钟系统(RCC配置)
由led点灯可以发现,使能时钟是GPIO初始化所必须的。因此必须对单片机的时钟树和系统时钟频率进行了解。
在stm32中,不同的外设需要不同的时钟频率,即高速时钟源与低速时钟源,以满足不同的传输速度需求
在STM32系列单片机中,时钟源是系统运行的基础,可以说是系统工作的“心脏”之一。STM32单片机提供了多个时钟源以适应不同应用场景的需求。下面介绍STM32时钟系统中的5个重要时钟源。
-
LSI(Low-Speed Internal):低速内部时钟源,RC振荡器,频率为40KHz左右。它比LSE和HSI更加节能,适合对功耗要求较高的场合,例如实时时钟(RTC)模块。LSI时钟可以通过使能RCC_APB1PeriphClockCmd函数中的RCC_APB1Periph_LSI来启用。
-
LSE(Low-Speed External):低速外部时钟源,晶体振荡器,频率为32.768KHz。由于LSE时钟稳定、精度高,主要用于STM32单片机的RTC模块和WWDG(独立看门狗)计时器。LSE时钟可以通过使能RCC_APB1PeriphClockCmd函数中的RCC_APB1Periph_PWR和RCC_BackupResetCmd函数来启用。
-
HSI(High-Speed Internal):高速内部时钟源,RC振荡器,频率为8MHz。它是STM32单片机出厂时默认的时钟源,并且使用最为广泛,可以提供高达72Mhz的系统时钟频率。HSI时钟可以通过使能RCC_HSICmd函数来启用。
-
HSE(High-Speed External):高速外部时钟源,晶体振荡器,频率可以选择4MHz、8MHz、12MHz、16MHz等不同频率,最大支持25MHz。通常在需要更高精度、更高稳定性的场合使用,例如USB模块、CAN模块等需要高速通信的场合。HSE时钟可以通过使能RCC_HSECmd函数来启用。
-
PLL(Phase-Locked Loop):锁相环时钟源,它是基于输入的参考时钟频率(通常是HSI或HSE),通过倍频、分频等数学运算得到更高频率的时钟输出。PLL的输出时钟频率可以高达72MHz,是STM32单片机中最快的时钟源。PLL时钟可以通过使能RCC_PLLCmd函数来启用。
需要注意的是,时钟源的选择要根据具体的应用场景和需求进行选择,不同的时钟源会对系统的性能、功耗等方面产生影响。因此,需要根据实际情况进行选择和配置。
注意:STM32的SystemInit函数执行完,默认的各时钟大小设置如下所示
SYSCLK(系统时钟) =72MHzAHB 总线时钟(HCLK=SYSCLK) =72MHzAPB1 总线时钟(PCLK1=SYSCLK/2) =36MHzAPB2 总线时钟(PCLK2=SYSCLK/1) =72MHzPLL 主时钟 =72MHz
void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState);void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);
其次介绍时钟源使能函数, STM32 有 5 大类时钟源,这里只挑几个重要的时钟源使能函数介绍,这些函数都是用来使能相应的时钟源,比如我们要使能 PLL 时钟,那么就调用 RCC_PLLCmd 函数,ENABLE 表示使能,DISABLE 表示失能。
void RCC_HSICmd(FunctionalState NewState);void RCC_LSICmd(FunctionalState NewState);void RCC_PLLCmd(FunctionalState NewState);void RCC_RTCCLKCmd(FunctionalState NewState);
时钟源和倍频因子配置函数
用于选择相应的时钟源和配置时钟倍频因子,比如系统时钟,它可以由 HSE、HSI 或者 PLLCLK 作为它的时钟源,具体选择哪个,就是通过时钟源配置函数实现。比如设置 HSE 作为系统时钟源,那么调用的函数就是:
RCC_SYSCLKConfig(RCC_SYSCLKSource_HSE);//配置时钟源为 HSE
时钟倍频因子配置函数主要用来修改系统的时钟频率,比如设置APB1 的时钟频率是 HCLK 的 2 分频。那么可以调用下面这个函数来实现:
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速 APB1 时钟(PCLK1)
void RCC_APB1PeriphResetCmd(uint32_t RCC_APB1Periph, FunctionalState NewState);void RCC_APB2PeriphResetCmd(uint32_t RCC_APB2Periph, FunctionalState NewState);
自定义系统时钟
一个系统时钟初始化函数
void RCC_HSE_Config(u32 div,u32 pllm) //自定义系统时间(可以修改时钟)
{
RCC_DeInit(); //将外设 RCC 寄存器重设为缺省值
RCC_HSEConfig(RCC_HSE_ON);//设置外部高速晶振(HSE)
if(RCC_WaitForHSEStartUp()==SUCCESS) //等待 HSE 起振
{
RCC_HCLKConfig(RCC_SYSCLK_Div1);//设置 AHB 时钟(HCLK)
RCC_PCLK1Config(RCC_HCLK_Div2);//设置低速 AHB 时钟(PCLK1)
RCC_PCLK2Config(RCC_HCLK_Div1);//设置高速 AHB 时钟(PCLK2)
RCC_PLLConfig(div,pllm);//设置 PLL 时钟源及倍频系数
RCC_PLLCmd(ENABLE); //使能或者失能 PLL
while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)==RESET);//检查指定的 RCC
标志位设置与否,PLL 就绪
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);// 设 置 系 统 时 钟
(SYSCLK)
while(RCC_GetSYSCLKSource()!=0x08);//返回用作系统时钟的时钟源,0x08:
PLL 作为系统时钟
}
}
系统初始化后的时钟是 72MRCC_HSE_Config(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);系统初始化后的时钟是 36MRCC_HSE_Config(RCC_PLLSource_HSE_Div2,RCC_PLLMul_9);
三 STM32位带操作(与库函数配合)
STM32中的位带操作是一种基于寄存器单个位的操作方式,可以直接对特定的位进行读写操作,而不需要像常规操作那样需要用掩码等方式来进行操作。它可以提高代码的执行效率,同时也方便了程序员对寄存器某个特定位的控制。
在STM32中,每个GPIO口都有一个32位的寄存器,称为数据寄存器(Data Register),同时每个GPIO口的每个IO口位都有一个独立的标志位Band,这些标志位被组织成一个独立的地址空间。通过利用这些标志位的地址,我们就可以使用位带操作方式对GPIO的某个具体的IO口进行读写控制。
例如,对于PA3这个IO口,我们可以使用如下位带操作方式:
#define PA3_IN *(volatile unsigned long *) 0x40020010
#define PA3_OUT *(volatile unsigned long *) 0x40020014
#define PA3_OFF PA3_OUT &= ~(1<<3)
#define PA3_ON PA3_OUT |= 1<<3
#define PA3_RD ((PA3_IN >> 3) & 0x01)
其中,PA3_IN和PA3_OUT是该IO口的数据输入输出寄存器的地址,PA3_OFF和PA3_ON分别是将该IO口输出设置为低电平和高电平的函数,PA3_RD是读取该IO口当前状态的函数。这样,我们就可以通过对PA3_OFF和PA3_ON函数的调用实现对该IO口输出状态的控制,同时也可以通过PA3_RD函数读取其当前状态。
以GPIO中的IDR和ODR两个寄存器的位操作为例,其中基地址的偏移量分别是8和12,(通过stm32参考手册可以查到),这种对位带操作的定义,根据程序解耦的思想,可以新建与APP同级的Public文件夹进行存放,并新建system.c和system.h文件
//IO 口地址映射
#define GPIOA_ODR_Addr (GPIOA_BASE+12) //0x4001080C
#define GPIOB_ODR_Addr (GPIOB_BASE+12) //0x40010C0C
#define GPIOC_ODR_Addr (GPIOC_BASE+12) //0x4001100C
#define GPIOD_ODR_Addr (GPIOD_BASE+12) //0x4001140C
#define GPIOE_ODR_Addr (GPIOE_BASE+12) //0x4001180C
#define GPIOF_ODR_Addr (GPIOF_BASE+12) //0x40011A0C
#define GPIOG_ODR_Addr (GPIOG_BASE+12) //0x40011E0C
#define GPIOA_IDR_Addr (GPIOA_BASE+8) //0x40010808
#define GPIOB_IDR_Addr (GPIOB_BASE+8) //0x40010C08
#define GPIOC_IDR_Addr (GPIOC_BASE+8) //0x40011008
#define GPIOD_IDR_Addr (GPIOD_BASE+8) //0x40011408
#define GPIOE_IDR_Addr (GPIOE_BASE+8) //0x40011808
#define GPIOF_IDR_Addr (GPIOF_BASE+8) //0x40011A08
#define GPIOG_IDR_Addr (GPIOG_BASE+8) //0x40011E08
其中,BIT_ADDR()函数是stm32中的一个宏定义,用于将位带区域的地址转换为内存中的标准地址。在stm32中,位带区域是指每个32位寄存器在内存中对应的一块区域,该区域中的每个比特位都可以被单独访问。
//IO 口操作,只对单一的 IO 口
//确保 n 的值小于 16
#define PAout(n) BIT_ADDR(GPIOA_ODR_Addr,n) //输出
#define PAin(n) BIT_ADDR(GPIOA_IDR_Addr,n) //输入
#define PBout(n) BIT_ADDR(GPIOB_ODR_Addr,n) //输出
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入
#define PCout(n) BIT_ADDR(GPIOC_ODR_Addr,n) //输出
#define PCin(n) BIT_ADDR(GPIOC_IDR_Addr,n) //输入
#define PDout(n) BIT_ADDR(GPIOD_ODR_Addr,n) //输出
#define PDin(n) BIT_ADDR(GPIOD_IDR_Addr,n) //输入
#define PEout(n) BIT_ADDR(GPIOE_ODR_Addr,n) //输出
#define PEin(n) BIT_ADDR(GPIOE_IDR_Addr,n) //输入
#define PFout(n) BIT_ADDR(GPIOF_ODR_Addr,n) //输出
#define PFin(n) BIT_ADDR(GPIOF_IDR_Addr,n) //输入
#define PGout(n) BIT_ADDR(GPIOG_ODR_Addr,n) //输出
#define PGin(n) BIT_ADDR(GPIOG_IDR_Addr,n) //输入
注意程序解耦设计的思路:在led.h中调用Public文件夹中的system.h文件,随后进行宏定义
#define led1 PCout(0) //D1 指示灯连接的是 PC0 管脚#define led2 PCout(1) //D2 指示灯连接的是 PC1 管脚#define led3 PCout(2) //D3 指示灯连接的是 PC2 管脚
此时在main.c文件中,我们只需~~~
LED_Init();led1=!led1; //D1 状态取反