STM32单片机外设初始化例程
一、起因
由于自己学习STM32单片机是零零散散的学习的,没有系统的进行学习,学习的东西非常的混乱,没有做过什么整体的框架整理,所以在此将STM32的外设的初始化进行系统的打包成一个文档,把程序的过程进行整理。
二、基本流程
1、基本STM32硬件知识点
STM32的单片机的开发程序发展的流程基本都是从寄存器阶段到标准库阶段到现在的HAL库、RTOS。所以第一步我们需要清楚单片机的整体工作流程。
通过图上可以看出,STM32单片机有一个Cortex-M3的内核CPU控制,分出三条数据总线控制各个外设:
- 指令存储区总线(两条):I-Code总线和D-Code总线
- 系统总线(System):用于访问内存和外设
- 私有外设总线:负责一部分私有外设的访问,主要就是访问调试组件
I-Code用于取指,D-Code用于查表等操作,它们按最佳执行速度进行优化。
系统总线(System)用于访问内存和外设,覆盖的区域包括SRAM,片上外设,片外RAM,片外扩展设备,以及系统级存储区的部分空间。
私有外设总线负责一部分私有外设的访问,主要就是访问调试组件。它们也在系统级存储区。
还有一个DMA总线,从字面上看,DMA是data memory access的意思,是一种连接内核和外设的桥梁,它可以访问外设、内存,传输不受CPU的控制,并且是双向通信。简而言之,这个家伙就是一个速度很快的且不受老大控制的数据搬运工。
从结构框图上看,STM32的外设有串口、定时器、IO口、FSMC、SDIO、SPI、I2C等,这些外设按照速度的不同,分别挂载到AHB、APB2、APB1这三条总线上。
其中寄存器其实可以理解为内存的地址,cpu通过地址访问对应的空间的内存数据,这个内存数据用来控制各个外设的开关。
stm32
的函数一切库的封装始于寄存器的映射操作。
如果进行寄存器开发,就需要怼地址以及对寄存器进行字节赋值,不仅效率低而且容易出错。
因此我们开始使用库函数进行编程。
内核库文件分析
cor_cm3.h
这个头文件实现了:
- 内核结构体寄存器定义
- 内核寄存器内存映射
- 内存寄存器位定义
跟处理器相关的头文件
stm32f10x.h
实现的功能一样,一个是针对内核的寄存器,一个是针对内核之外,即处理器的寄存器。
misc.h
内核应用函数库头文件,对应stm32f10x_xxx.h
。
misc.c
内核应用函数库文件,对应stm32f10x_xxx.c
。
在CM3这个内核里面还有一些功能组件,如:
NVIC
SCB
ITM
MPU
CoreDebug
CM3带有非常丰富的功能组件,但是芯片厂商在设计MCU的时候有一些并不是非要不可的,是可裁剪的,比如
MPU
、ITM
等在STM32里面就没有。
其中NVIC
在每一个CM3内核的单片机中都会有,但都会被裁剪,只能是CM3 NVIC的一个子集。在NVIC
里面还有一个SysTick
,是一个系统定时器,可以提供时基,一般为操作系统定时器所用。
misc.h
和mics.c
这两个文件提供了操作这些组件的函数,并可以在CM3内核单片机直接移植。
处理器外设库文件分析
startup_stm32f10x_hd.s
这个是由汇编编写的启动文件,是STM32上电启动的第一个程序,启动文件主要实现了:
- 初始化堆栈指针
SP
- 设置
PC
指针=Reset_Handler
- 设置向量表的地址,并初始化向量表(向量表里面放的是 STM32 所有中断函数的入口地址)
- 调用库函数
SystemInit
(把系统时钟配置成 72M,SystemInit
在库文件stytem_stm32f10x.c
中定义) - 跳转到标号
_main
,最终去到 C 的世界
system_stm32f10x.c
这个文件的作用是里面实现了各种常用的系统时钟设置函数,有:
- 72M
- 56M
- 48M
- 36M
- 24M
- 8M
我们使用的是把系统时钟设置成72M。
Stm32f10x.h
这个头文件非常重要,实现了:
- 处理器外设寄存器的结构体定义
- 处理器外设的内存映射
- 处理器外设寄存器的位定义
关于1和2我们在用寄存器点亮LED的时候有讲解。
关于第3点说明:
处理器外设寄存器的位定义就是把外设的每个寄存器的每一个位写1的16进制数定义成一个宏,宏名即用该位的名称表示。
示例:
1 | // 一般的操作方法 |
stm32f10x_xxx.h
外设xxx应用函数库头文件,这里面主要定义了实现外设某一功能的结构体。
比如通用定时器有很多功能:定时功能、输出比较功能、输入捕捉功能。这个头文件就为我们打包好了要实现某一个功能的寄存器(以结构体的形式定义)。
stm32f10x_xxx.c
外设xxx应用函数库,这里面写好了操作xxx外设的所有常用的函数。
我们使用库编程的时候,使用的最多的就是这里的函数。
SystemInit 相关问题
在工程中新建main.c
后直接编译会报错:
1 | Undefined symbol SystemInit (referred from startup_stm32f10x_hd.o). |
原因分析:
从启动文件startup_stm32f10x_hd.s
中我们知道:
1 | ;Reset handler |
汇编中
;
分号是注释的意思
Reset_Handler
调用了SystemInit
函数(用来初始化系统时钟),该函数是在库文件system_stm32f10x.c
中实现的。
解决方案:
在main文件里面定义一个SystemInit
空函数,为的是骗过编译器,把这个错误去掉。
关于配置系统时钟之后会出文章RCC时钟树详细介绍,主要配置:
- 时钟控制寄存器(RCC_CR)
- 时钟配置寄存器(RCC_CFGR)
但最好是直接使用CubeMX直接生成,因为它的配置过程有些冗长。
如果使用库,库函数SystemInit
会帮我们把系统时钟设置成72M。
2、基本stm32外设配置流程
程序模板
第一步:申明结构体;
1 | xxx_InitTypeDef xxx_InitStructure; |
第二步:开启时钟;
(第一步和第二步顺序不能调换:标准c要求所有变量/结构体,都必须在代码段之前声明)
1 | RCC_xPeriphClockCmd(RCC_AxBxPeriph_xxx, ENABLE) |
*2.5:引脚复用(如果有)并且开启复用的时钟
1 | GPIO_PinAFConfig(GPIOx,GPIO_PinSourcex,GPIO_AF_x) |
第三步:初始化结构体;
1 | xxx_Init(xxx,&xxx_InitStructure) |
*若有设置中断
中断名在startup_stm32f40_41xx.s中定义。
第一步:使能外设某特定中断(定时器,串口,ADC)
1 | xxx_ITConfig(xxx, xxx, ENABLE); |
第二步:初始化 NVIC
1 | NVIC_Init(&NVIC_InitStructure); |
第三步:设置系统中断优先级分组(通常在主函数中配置)
1 | NVIC_PriorityGroupConfig(NVIC_PriorityGroup_x); |
中断服务函数(典型)
1 | void xxx_IRQHandler(void) |
其它中断相关
1 | xxx_GetITStatus(xxx)//获取中断状态,查看中断是否发生 |
END.定时器/串口/ADC使能
1 | xxx_Cmd(xxx, ENABLE); |
3、结构体变量配置具体形式
初始化结构体初始化 GPIO 的常用格式
1 | GPIO_InitTypeDef GPIO_InitStructure; |
初始化结构体初始化 USART 的常用格式
1 | USART_InitTypeDef USART_InitStructure; |
初始化结构体初始化 NVIC 的常用格式
1 | NVIC_InitTypeDef NVIC_InitStructure; |
初始化结构体初始化外部中断的常用格式
1 | EXTI_InitTypeDef EXTI_InitStructure; |
初始化结构体初始化定时器中断的常用格式
1 | TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure; |
初始化结构体初始化输出比较的常用格式
1 | TIM_OCInitTypeDef TIM_OCInitStructure; |
设置 ADC 的通用控制寄存器 CCR( common control register)
1 | ADC_CommonInitTypeDef ADC_CommonInitStructure; |
初始化结构体初始化ADC的常用格式
1 | ADC_InitTypeDef ADC_InitStructure; |
三、GPIO配置
1、常用函数
GPIO_Init 初始化GPIO,设置GPIO的模式,速度,引脚数
GPIO_ReadInputDataBit读取一位GPIO的输入数据
GPIO_ReadInputData 读取GPIOx的输入数据
GPIO_ReadOutputDataBit 读取一位GPIO的输出数据
GPIO_ReadOutputData 读取GPIOx的输出数据
GPIO_SetBits 使GPIO设置为高电平,可一起设置多,也可以设置一个
GPIO_ResetBits 使GPIO设置为高电平,课一起设置多,也可以设置一个
GPIO_WriteBit 设置GPIO的一个管脚
GPIO_Write 设置GPIOx全部管脚
GPIO_ToggleBits 翻转指定的GPIO口
GPIO_PinAFConfig 改变指定管脚的映射关系,即配置指定管脚的复用功能
设计框图
例程代码
示例一:LED灯初始化GPIO口例程
1 | void LED_GPIO_Config(void) |
参数:GPIO_InitStruct,GPIO的初始化相关结构体。该结构体里的成员变量决定了我们具体的初始化参数。以下进行说明:
GPIO_Pin:指定具体的io脚,如GPIO_Pin_0,GPIO_Pin_1这样的宏定义。
GPIO_Mode:指定GPIO的模式,
输入模式
+ 输入浮空: GPIO_Mode_IN_FLOATING
+ 输入上拉: GPIO_Mode_IPU
+ 输入下拉 :GPIO_Mode_IPD
+ 模拟输入 :GPIO_Mode_AIN
输出模式
+ 开漏输出 GPIO_Mode_Out_OD
+ 推挽输出 GPIO_Mode_Out_PP
+ 复用功能推挽 GPIO_Mode_AF_PP
+ 复用功能开漏 GPIO_Mode_AF_OD
GPIO_Speed:指定IO最快翻转速度,也就是当使用IO产生频率(如PWM)的最大速度:
+ GPIO_Speed_10MHz,
+ GPIO_Speed_2MHz,
+ GPIO_Speed_50MHz等
示例二:把GPIO配置成输入
常规方式按键使用中断触发,本案例很少被使用在按键中。
1 | void KEY_Init(void) |
示例三:配置复用功能 PA9 PA10 配置成串口1的收发接口
1 | GPIO_InitTypeDef GPIO_InitStructure; |
四、外部中断
1、常用函数
void EXTI_DeInit(void); 重设为缺省值
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct); 根据EXTI_InitStruct结构体的配置进行初始化
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);把结构体变量的每一个变量按照缺省值填入。
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);产生一个中断
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);获取指定的EXTI线路挂起的标志位
void EXTI_ClearFlag(uint32_t EXTI_Line);清楚EXTI的挂起标志位
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);检查指定的EXTI线路触发请求发送与否
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);清楚EXTI线路挂起位
voidNVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)中断优先级分组
分组号 | 4 bit 分配情况 | 说明 |
---|---|---|
第0组 | 0 : 4 | 无抢占式优先级,16 个子优先级 |
第1组 | 1 : 3 | 2 个抢占式优先级,8 个子优先级 |
第2组 | 2 : 2 | 4 个抢占式优先级,4 个子优先级 |
第3组 | 3 : 1 | 8 个抢占式优先级,2 个子优先级 |
第4组 | 4 : 0 | 16 个抢占式优先级,无子优先级 |
如果用户没有设置优先级分组,即用户没有调用">NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup)则优先级分组默认设置为分组 0,即无抢占式优先级、16个子优先级。
NVIC_Init(&NVIC_InitStruct); 根据NVIC_InitStruct结构体的配置进行初始化
设计框图
例程代码
相关配置代码的介绍
1 | //0、初始化GPIO |
NVIC_InitTypeDef 结构体中间有四个成员变量,这四个成员变量的作用是:
- NVIC_IRQChannel:定义初始化的是哪个中断,这个我们可以在 stm32f10x.h 中找到每个中断对应的名字。
- NVIC_IRQChannelPreemptionPriority:定义这个中断的抢占优先级别。
- NVIC_IRQChannelSubPriority:定义这个中断的子优先级别。
- NVIC_IRQChannelCmd:使能or失能NVIC
EXTI的配置,EXTI_Trigger这里支持三种模式;
- EXTI_Trigger_Rising 上升沿触发;
- EXTI_Trigger_Falling 下降沿触发;
- EXTI_Trigger_Rising_Falling 上升沿和下降沿都可以触发;
中断服务函数在stm32f10x_it.c中编写,在汇编文件中查询
完整代码
1 | void CountSensor_Init(void) |
五、定时器
1、常用函数
void TIM_DeInit
void TIM_TimeBaseInit
void TIM_OC1Init
void TIM_OC2Init
void TIM_OC3Init
void TIM_OC4Init
void TIM_ICInit
void TIM_PWMIConfig
void TIM_BDTRConfig
void TIM_TimeBaseStructInit
void TIM_OCStructInit
void TIM_ICStructInit
void TIM_BDTRStructInit
void TIM_Cmd
void TIM_CtrlPWMOutputs
void TIM_ITConfig
void TIM_GenerateEvent
void TIM_DMAConfig
void TIM_DMACmd
void TIM_InternalClockConfig
void TIM_ITRxExternalClockConfig
void TIM_TIxExternalClockConfig
void TIM_ETRClockMode1Config
void TIM_ETRClockMode2Config
void TIM_ETRConfig
void TIM_PrescalerConfig
void TIM_CounterModeConfig
void TIM_SelectInputTrigger
void TIM_EncoderInterfaceConfig
void TIM_ForcedOC1Config
void TIM_ForcedOC2Config
void TIM_ForcedOC3Config
void TIM_ForcedOC4Config
void TIM_ARRPreloadConfig
void TIM_SelectCOM
void TIM_SelectCCDMA
void TIM_CCPreloadControl
void TIM_OC1PreloadConfig
void TIM_OC2PreloadConfig
void TIM_OC3PreloadConfig
void TIM_OC4PreloadConfig
void TIM_OC1FastConfig
void TIM_OC2FastConfig
void TIM_OC3FastConfig
void TIM_OC4FastConfig
void TIM_ClearOC1Ref
void TIM_ClearOC2Ref
void TIM_ClearOC3Ref
void TIM_ClearOC4Ref
void TIM_OC1PolarityConfig
void TIM_OC1NPolarityConfig
void TIM_OC2PolarityConfig
void TIM_OC2NPolarityConfig
void TIM_OC3PolarityConfig
void TIM_OC3NPolarityConfig
void TIM_OC4PolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
void TIM_CCxCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCx);
void TIM_CCxNCmd(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_CCxN);
void TIM_SelectOCxM(TIM_TypeDef* TIMx, uint16_t TIM_Channel, uint16_t TIM_OCMode);
void TIM_UpdateDisableConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_UpdateRequestConfig(TIM_TypeDef* TIMx, uint16_t TIM_UpdateSource);
void TIM_SelectHallSensor(TIM_TypeDef* TIMx, FunctionalState NewState);
void TIM_SelectOnePulseMode(TIM_TypeDef* TIMx, uint16_t TIM_OPMode);
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);
void TIM_SelectSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_SlaveMode);
void TIM_SelectMasterSlaveMode(TIM_TypeDef* TIMx, uint16_t TIM_MasterSlaveMode);
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);
void TIM_SetCompare2(TIM_TypeDef* TIMx, uint16_t Compare2);
void TIM_SetCompare3(TIM_TypeDef* TIMx, uint16_t Compare3);
void TIM_SetCompare4(TIM_TypeDef* TIMx, uint16_t Compare4);
void TIM_SetIC1Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC2Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC3Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetIC4Prescaler(TIM_TypeDef* TIMx, uint16_t TIM_ICPSC);
void TIM_SetClockDivision(TIM_TypeDef* TIMx, uint16_t TIM_CKD);
uint16_t TIM_GetCapture1(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture2(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture3(TIM_TypeDef* TIMx);
uint16_t TIM_GetCapture4(TIM_TypeDef* TIMx);
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
设计框图
类型 | 编号 | 总线 | 功能 |
---|---|---|---|
高级定时器 | TIM1、TIM8 | APB2 | 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能 |
通用定时器 | TIM2、TIM3、TIM4、TIM5 | APB1 | 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能 |
基本定时器 | TIM6、TIM7 | APB1 | 拥有定时中断、主模式触发DAC的功能 |
例程代码
定时器中断实现步骤
① 能定时器时钟。
RCC_APB1PeriphClockCmd();
② 初始化定时器,配置ARR,PSC。
TIM_TimeBaseInit();
③开启定时器中断,配置NVIC。
void TIM_ITConfig();
NVIC_Init();
④ 使能定时器。
TIM_Cmd();
⑥ 编写中断服务函数。
TIMx_IRQHandler();
1 | void TIM3_Int_Init(u16 arr,u16 psc) |
说些什么吧!