您现在的位置是:首页 >其他 >5。STM32裸机开发(1)网站首页其他

5。STM32裸机开发(1)

西伯利亚大草原的狼 2024-06-17 10:14:25
简介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个重要时钟源。

  1. LSI(Low-Speed Internal):低速内部时钟源,RC振荡器,频率为40KHz左右。它比LSE和HSI更加节能,适合对功耗要求较高的场合,例如实时时钟(RTC)模块。LSI时钟可以通过使能RCC_APB1PeriphClockCmd函数中的RCC_APB1Periph_LSI来启用。

  2. LSE(Low-Speed External):低速外部时钟源,晶体振荡器,频率为32.768KHz。由于LSE时钟稳定、精度高,主要用于STM32单片机的RTC模块和WWDG(独立看门狗)计时器。LSE时钟可以通过使能RCC_APB1PeriphClockCmd函数中的RCC_APB1Periph_PWR和RCC_BackupResetCmd函数来启用。

  3. HSI(High-Speed Internal):高速内部时钟源,RC振荡器,频率为8MHz。它是STM32单片机出厂时默认的时钟源,并且使用最为广泛,可以提供高达72Mhz的系统时钟频率。HSI时钟可以通过使能RCC_HSICmd函数来启用。

  4. HSE(High-Speed External):高速外部时钟源,晶体振荡器,频率可以选择4MHz、8MHz、12MHz、16MHz等不同频率,最大支持25MHz。通常在需要更高精度、更高稳定性的场合使用,例如USB模块、CAN模块等需要高速通信的场合。HSE时钟可以通过使能RCC_HSECmd函数来启用。

  5. PLL(Phase-Locked Loop):锁相环时钟源,它是基于输入的参考时钟频率(通常是HSI或HSE),通过倍频、分频等数学运算得到更高频率的时钟输出。PLL的输出时钟频率可以高达72MHz,是STM32单片机中最快的时钟源。PLL时钟可以通过使能RCC_PLLCmd函数来启用。

需要注意的是,时钟源的选择要根据具体的应用场景和需求进行选择,不同的时钟源会对系统的性能、功耗等方面产生影响。因此,需要根据实际情况进行选择和配置。

注意:STM32的SystemInit函数执行完,默认的各时钟大小设置如下所示

SYSCLK(系统时钟) =72MHz
AHB 总线时钟(HCLK=SYSCLK) =72MHz
APB1 总线时钟(PCLK1=SYSCLK/2) =36MHz
APB2 总线时钟(PCLK2=SYSCLK/1) =72MHz
PLL 主时钟 =72MHz
stm32中的时钟使能函数包括外设时钟使能和时钟源使能。首先介绍外设时钟使能相关函数,对应了STM32的3条总线,若想使能各类外设,必须找到他是挂载在哪个总线上的
这点我们可以通过STM32参考手册查找,或通过固件库stm32f10x_rcc.h 文件中查找
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 作为系统时钟
}
}
系统初始化后的时钟是 72M
RCC_HSE_Config(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
系统初始化后的时钟是 36M
RCC_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 状态取反

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。