3.5.1.为什么会有标准外设库
3.5.2_3.外设库的结构介绍和之后的学习方法1_2
3.5.4.标准库对硬件信息的封装方式
3.5.5.使用结构体方式访问寄存器的原理
3.5.6.使用结构体方式访问寄存器的实践
3.5.7_8.使用标准库重写LED的程序
3.5.9_10.RCC模块的标准库全解析
3.5.11.RCC模块的标准库全解析3
3.5.12.RCC模块的标准库全解析4
3.5.13.RCC模块的标准库全解析5
3.5.14_15.使用库重写时钟设置函数1_2
3.5.16_17.GPIO模块的标准库全解析1_2
3.5.18.使用GPIO库函数来点亮LED
3.5.19.标准库中的面向对象思想
3.5.1.1、传统单片机软件开发方式
(1)芯片厂商提供数据手册、示例代码、开发环境
(2)单片机软件工程师面向产品功能,查阅数据手册,参考官方示例代码进行开发
(3)硬件操作的方式是用C语言对寄存器进行读写以操作硬件
(4)主要工作量:一是调通各种外设(可读写),二是实现产品功能
(5)在简单单片机(如51单片机)上这一套工作的很好,但是随着单片机变复杂就带来一些问题
3.5.1.2、外设库有什么价值
(1)外设库其实就是以前芯片公司提供的示例代码的标准化产物
(2)外设库简化了我们开发产品时的“调通各种外设”
(3)外设库以源码方式提供,这个源码本身写的很标准,可以用作学习素材
3.5.1.3、学习和使用外设库的难点
(1)要有规范化编程的意识和能力
(2)C语言功底要过关
(3)要有一定的框架和层次认识
(4)要会没有外设库时直接C语言操作寄存器的方式(看原理图、查数据手册、位操作等)
3.5.1.4、再次强调
(1)外设库只是帮助我们简化编程,简化的主要是劳动量
(2)外设库一定程度上降低了编程难度,但是只会库、离了库就不会编程、库函数调用出了问题就束手无策这种还是没戏。(难度降低是对所有人的,你并不能从中得到好处)
3.5.2.1、外设库的结构介绍
(1)最新版本库的下载和解压
官网下载链接
博客提供链接(百度网盘,解压密码:neu1)
(2)文件夹结构和主要文件的作用
_htmresc文件夹内是官方Logo图片,可以删掉。
Libraries 文件夹下面有 CMSIS 和 STM32F1xx_StdPeriph_Driver 两个目录,这两个目录包
含固件库核心的所有子文件夹和文件,是代码移植的重头戏。CMSIS 文件夹存放的是符合 CMSIS 规范的一些文件, Driver 文件夹下是STM32F1 标准外设固件库源码文件和对应的头文件,说白了就是将寄存器封装好的函数接口。
Project 文件夹下面有STM32F1xx_StdPeriph_Examples 和STM32F1xx_StdPeriph_Template 两个文件夹,Examples文件夹下是固件示例源码,Template文件夹下是工程模板。这些源码的学习对以后的开发学习非常重要。
Utilities 文件夹下就是官方评估板的一些对应源码,可以忽略不看。
根目录中还有一个固件库的帮助文档 stm32f1xx_dsp_stdperiph_lib_um.chm 文件。
(3)最重要的就是Libraries
CMSIS(STM32内部ARM核心相关内容)
CM3(Cortex-M3)
CoreSupport(内核相关的一些设置的寄存器集合及其封装)
DeviceSupport
ST
STM32F10x
startup(内核相关的一些设置的寄存器集合及其封装)
stm32f10x.h(很重要)
system_stm32f10x.c
system_stm32f10x.h
STM32F10x_StdPeriph_Driver(STM32F1外设驱动)
inc(include,头文件,.h)
src(source,源文件, .c)
目录树:
3.5.2.2、后续的学习方法
(1)先搞清楚库对STM32这个硬件的封装和表达方式
(2)再彻底理解库中使用的结构体式访问硬件寄存器的方式
(3)初步建立起面向对象式编程的概念并且去体会
(4)以模块为单位去研究这个模块的库函数,并且用库函数去编程,并且实验结果,并且分析代码,去体会去熟悉库函数使用的方法
(5)最终达到什么程度?眼里有库心中无库。用人话说就是:思维能够穿透库函数直达内部对寄存器的操作。
3.5.4.1、寄存器地址的封装
其实就是将寄存器在存储器中的地址,利用宏定义表示出来。(这里的地址是指在位带区内的地址,关于位带区和位带别名区请往后看)
3.5.4.2、寄存器位定义的封装
就是对于寄存器每一位的操作,有很多种情况,直接利用宏定义将不同的复制情况进行封装。
比如说,打开HSE时钟时,要操纵寄存器RCC_CR,给第16位赋值1,就可以打开HSE时钟。
RCC_TypeDef是个结构体变量,里面封装这RCC的各个寄存器,RCC_BASE是RCC寄存器组的基地址,于是RCC就是一个结构体指针,指向RCC_BASE下的RCC寄存器组。
道理一样!这样的好处就是更加直观,代码可读性更强!
3.5.4.3、外设操作的封装
其实就是很多的函数,比如说配置外部时钟72MHz,需要配置HSE,操纵不同的寄存器,在SPL中,这些工作都给做好了!
比如说,下面配置HSE的子函数,只要RCC_HSE准备就绪,就可以打开HSE!
(1)之前访问寄存器方式
C语言访问寄存器的本质是C语言访问内存
本质思路:
缺陷:
(2)解决思路:就是打包,批发式的定义,用结构体(想一下为什么不用数组?)的方式进行打包。
具体做法:
把整个一个模块的所有寄存器(地址是连接的)打包在一个结构体中,每个寄存器对应结构体中的一个元素,然后结构体基地址对应寄存器组的基地址,将来就可以通过结构体的各个元素来访问各个寄存器了。
(3)结构体方式来访问寄存器和指针式访问寄存器,本质上其实是一样的,区别是C语言的封装不同。
(4)volatile的作用
程序下载
3.5.7.1、分析标准库自带的工程模板
模板在"STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template\MDK-ARM"下,用MDK软件双击project即可打开。
模板目录:
3.5.7.2、建立自己的模板
1.创建My_Template文件夹
2.进入My_Template,复制\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\下的CMSIS、STM32F10x_StdPeriph_Driver到My_Template下
3.创建User、Project、Listings、Output文件夹
4.打开MDK,创建新Project(名字自定义),存放在My_Template/Project下
5.选择自己开发板的芯片,这里选择的是“STM32F103C8”,出来的Run-Time页面,直接关闭就行
6.添加各个文件夹到工程
7.添加启动文件组,启动文件在\My_Template\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm\下,STM32F103C8属于md产品,所以我选择xx_md.s
8.添加CMSIS组,添加\My_Template\CMSIS\CM3\CoreSupport\下的core_cm3.c和\My_Template\CMSIS\CM3\DeviceSupport\ST\STM32F10x\下的system_stm32f10x.c
9.添加StdPeriph_Driver组,把\My_Template\STM32F10x_StdPeriph_Driver\src\下的所有文件添加进来
10.添加User组,在User文件夹下新建一个main.c文件,复制\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template\下的stm32f10x_conf.h、stm32f10x_it.c、stm32f10x_it.h到User文件夹下,通过MDK把这两个.c文件添加进来。
11.点OK,完成Group的创建,然后点击Options for Target。
12.Target选项,勾选上User MicroLIB
13.OutPut选项
14.Listing选项(可选)
15.C/C++选项
添加宏定义:STM32F10X_MD,USE_STDPERIPH_DRIVER
其中STM32F10X_MD要根据自己的板子的属性,和startup下的xx_md.s对应
添加include path:如下图
点OK。
16.编辑User\main.c文件,添加以下内容。
#include "stm32f10x."
int main(void)
{
return 0;
}
17.点击Rebulid,如果不报错,说明模板创建完毕。
3.5.7.3、使用标准库方式操作点亮LED
1.复制My_Template文件夹,重命名为3.stdlib_led
2.打开Project/下的工程文件,修改main.c,粘贴下面的内容
#include "stm32f10x.h"
/*---------接线-------------
PB8--PB15接到LED1--LED8
---------------------------*/
void led_flash(void);
void led_init(void);
void delay(void);
int main(void)
{
led_init();
led_flash(); //内部时钟8MHz
while(1);
}
void led_init(void)
{
//GPIO时钟使能
RCC->APB2ENR = 0x00000008;
//GPIOB设置成推挽输出模式,速度是50MHz
GPIOB->CRH = 0x33333333;
}
void led_flash(void)
{
unsigned int i = 0;
for(i = 0;i<5;i++)
{
GPIOB->ODR = 0x00000000;
delay();
GPIOB->ODR = ~GPIOB->ODR;
delay();
}
}
void delay(void)
{
unsigned int i,j;
for(i = 0;i<1000;i++)
for(j = 0;j<1000;j++);
}
点击build,没有错误就可以烧录到板子上运行结果了。
1.一般模块都是成对存在(xxx.c+xxx.h)
一般在.h中是宏定义和函数的声明,.c中是函数的实现。
2.RCC.c和RCC.h里面都有宏定义,为什么不都放在RCC.h里面呢?
放在RCC.h里面的宏定义可以被RCC.c和其他文件使用,RCC.c里面的宏定义只用在RCC.c中,这是一个数据的封装,比较合理。
3.RCC.c中的位带别名区地址
(1)RCC的位带操作
不记得什么叫位带操作可以看一下这个链接:【ARM】---STM32位带操作总结---浅显易懂
我们只要记住下面这张图的公式即可:
外设位带别名区地址 = 0x42000000(PERIPH_BB_BASE) + 偏移量*32 + 位数*4
(2)RCC_OFFSET:RCC相对于外设位带区基地值的偏移量
(3)时钟控制寄存器(RCC_CR)的各个位在位带别名区的地址
(4)时钟配置寄存器(RCC_CFGR)的各个位在位带别名区的地址
后面一直到116行都是上面的分析方法。
4.RCC.c中的位掩码
位掩码作用:利用Reset进行|=~运算给某一位清零,然后再Set利用&运算就可以设置某位为1了。
默认:SET--写1,RESET--写0
所以:ReSet---第n位写0,其他位写1,Set---第n位写1,其他位写0.
以CR寄存器的HSEBYP、HSEON、HSITRIM为例。HSEBYP的Reset为0xFFFBFFFF
其他的类似分析方法。
在193、194行有两个预分频的全局变量,等用到了再看
5.RCC.c里面的函数
(1)void RCC_DeInit(void)
作用:将 RCC 时钟配置重置为默认重置状态。平时用的比较少!
(2)void RCC_HSEConfig(uint32_t RCC_HSE)
作用:配置外部高速振荡器(HSE)
Note:如果 HSE 直接使用或通过 PLL 作为系统时钟,则不能关闭HSE时钟
参数:RCC_HSE:指定HSE的新状态。此参数可以是以下值之一:
(3)assert_param解析
assert叫断言,assert机制是C语言里用来判断一个东西是对的还是错的,并且如果是对的那就直接忽略过去,如果是错的就以某种方式告诉我们(warrning error)让我们去改。
断言机制使用最多的就是:库函数中用断言来检查用户调用该库函数时传参到底对不对
判断断言表达式是否对,如果对就正常运行,如果不对,就调用assert_failed函数,这个函数可以自己编辑,比如可以报错:xx行有变量值错误
(4)ErrorStatus RCC_WaitForHSEStartUp(void)
作用:等待 HSE 启动稳定
返回值:
5.(5)FlagStatus RCC_GetFlagStatus(uint8_t RCC_FLAG)
作用:检查指定的 RCC 标志是否设置。
(6)void RCC_HSICmd(FunctionalState NewState)
作用:启用或禁用内部高速振荡器 (HSI)。
NewState: HSI的新状态。此参数可以是:启用或禁用。使用最原始的给寄存器某一位写值的方式
*(__IO uint32_t *) CR_HSION_BB = (uint32_t)NewState;
(7)void RCC_PLLConfig(uint32_t RCC_PLLSource, uint32_t RCC_PLLMul)
作用:配置 PLL 时钟来源和倍频因子。
Note:此功能必须仅在 PLL 被禁用时使用。
RCC_PLLSource有三个来源:RCC_PLLSource_HSI_Div2、RCC_PLLSource_HSE_Div1、RCC_PLLSource_HSE_Div2
RCC_PLLMul:[2,16]
(8)void RCC_SYSCLKConfig(uint32_t RCC_SYSCLKSource)
作用: 配置系统时钟 (SYSCLK)
参数RCC_SYSCLKSource有三个来源:
RCC_SYSCLKSource_HSE
RCC_SYSCLKSource_PLLCLK
5.(9)void RCC_HCLKConfig(uint32_t RCC_SYSCLK)
作用:配置AHB 时钟 (HCLK).
参数RCC_SYSCLK:/1,2,4,8,16,64,128,256,512
(10)void RCC_PCLK1Config(uint32_t RCC_HCLK)
作用:配置APB1 时钟 (PCLK1).
参数RCC_HCLK:/1,2,4,8,16
(11)void RCC_PCLK2Config(uint32_t RCC_HCLK)
作用:配置APB2 时钟 (PCLK2).
参数RCC_HCLK:/1,2,4,8,16
(12)void RCC_ITConfig(uint8_t RCC_IT, FunctionalState NewState)
作用:使能或者不使能 RCC中断,传入时钟源,当就绪之后,就使能触发中断。
参数RCC_IT:选择中断源:LSI、LSE、HSI、HSE、PLL
参数NewState:DISABLE / ENABLE
(13)后面依次是RCC_ADCCLKConfig、RCC_LSEConfig、RCC_LSICmd、RCC_RTCCLKConfig、RCC_RTCCLKCmd、RCC_GetClocksFreq......比较简单。
(14)1063行:void RCC_AHBPeriphClockCmd(uint32_t RCC_AHBPeriph, FunctionalState NewState)
作用:用于使能AHB外设时钟
可以使能的外设如下图
(15)void RCC_APB2PeriphClockCmd(uint32_t RCC_APB2Periph, FunctionalState NewState)
作用:用于使能APB2外设时钟
可以使能的外设如下图
(16)void RCC_APB1PeriphClockCmd(uint32_t RCC_APB1Periph, FunctionalState NewState)
作用:用于使能APB1外设时钟
可以使能的外设如下图
哪一行取消注释,上电后,默认以什么频率工作。
main函数
#include "stm32f10x.h"
/*---------接线-------------
PB8--PB15接到LED1--LED8
---------------------------*/
void led_flash(void);
void led_init(void);
void delay(void);
void Set_SysClockTo72M(void);
int main(void)
{
led_init();
led_flash(); //内部时钟8MHz
Set_SysClockTo72M();
led_flash(); 外部时钟72MHz
while(1);
}
void led_init(void)
{
//GPIO时钟使能
RCC->APB2ENR = 0x00000008;
//GPIOB设置成推挽输出模式,速度是50MHz
GPIOB->CRH = 0x33333333;
}
void led_flash(void)
{
unsigned int i = 0;
for(i = 0;i<5;i++)
{
GPIOB->ODR = 0x00000000;
delay();
GPIOB->ODR = ~GPIOB->ODR;
delay();
}
}
void delay(void)
{
unsigned int i,j;
for(i = 0;i<1000;i++)
for(j = 0;j<1000;j++);
}
新建clock.c,放在User下!
#include "stm32f10x.h"
#define PLL_STARTUP_TIMEOUT ((uint16_t)0x1000)
//等待PLL倍频后输入稳定
ErrorStatus RCC_WaitForPLLStartUp(void)
{
__IO uint32_t StartUpCounter = 0;
ErrorStatus status = ERROR;
FlagStatus PLLStatus = RESET; //没就位
/* Wait till PLL is ready and if Time out is reached exit */
do
{
PLLStatus = RCC_GetFlagStatus(RCC_FLAG_PLLRDY);
StartUpCounter++;
} while((StartUpCounter != PLL_STARTUP_TIMEOUT) && (PLLStatus == RESET));
if (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) != RESET)
{
status = SUCCESS;
}
else
{
status = ERROR;
}
return (status);
}
void Set_SysClockTo72M(void)
{
ErrorStatus sta = ERROR; //HSE就绪标志位
/*上电复位RCC_CR*/
RCC->CR = 0x00000083;
/*开启外部时钟HSE*/
RCC_HSEConfig(RCC_HSE_OFF);
RCC_HSEConfig(RCC_HSE_ON);
//等待HSE稳定
sta = RCC_WaitForHSEStartUp();
if(sta == SUCCESS) //HSE准备就绪
{
/*启用半周期访问、两个等待状态,当 48MHz < SYSCLK ≤ 72MHz*/
FLASH->ACR |= 0x10;
FLASH->ACR &= (~0x03);
FLASH->ACR |= (0x02);
/*选择HSE不分频作为PLL的输入时钟,HSE = 8M,设置PLL倍频系数为9倍*/
RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_9);
/*AHB、APB2不分频,APB1二分频*/
RCC_HCLKConfig(RCC_SYSCLK_Div1);
RCC_PCLK1Config(RCC_HCLK_Div2);
RCC_PCLK2Config(RCC_HCLK_Div1);
/*---开启PLL时钟---*/
RCC_PLLCmd(ENABLE);
sta = ERROR;
/*--等待PLL稳定--*/
sta = RCC_WaitForPLLStartUp();
if(sta == SUCCESS)
{
/*---设置PLL为系统时钟来源---*/
RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
}
else
{
while(1); //PLL配置超时
}
/***检查SWS位,等待PLL作为SYSCLK时钟启动成功***/
while(RCC_GetSYSCLKSource()!=0x08)
{
}
}
else
{
while(1); //HSE配置超时
}
}
和RCC分析一样,我们也是着重分析.c和.h文件
1.GPIO的位带操作
和RCC分析一样,就是套公式。
2.函数
(1)void GPIO_DeInit(GPIO_TypeDef* GPIOx)
作用:将 GPIOx(x=A:G) 外围寄存器降级到其默认重置值
GPIO_TypeDef是一个结构体类型:
(2)void GPIO_AFIODeInit(void)
作用:将 AFIO外围寄存器降级到其默认重置值
(3)void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct)
作用:根据参数GPIO_InitStruct来初始化GPIOx的外设
GPIO_InitTypeDef也是一个结构体类型:
(4)void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct)
作用:用默认值填充每个GPIO_InitStruct成员。
(5)uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:读取指定的输入端口引脚的值。
返回值:读取的值
(6)uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx)
作用:读取指定GPIO输入端口的值。
返回值:读取的值
(7)uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:读取指定的输出端口引脚的值。
返回值:读取的值
(8)uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx)
作用:读取指定GPIO输出端口的值。
返回值:读取的值
(9)void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:给指定端口的指定引脚置1
(10)void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:给指定端口的指定引脚置0
(11)void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal)
作用:设置或清除选定的数据端口位。和上面重复了!
(12)void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal)
作用:给端口赋值
(13)void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
作用:锁定GPIO引脚配置寄存器。
(14)void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
作用:AFIO选定指定引脚作为事件输出。
(15)void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState) ★
作用:更改指定管脚的映射。
(16)void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource)
作用:选择用作EXTI线的GPIO引脚。和中断相关。
代码下载:下载地址
3.5.19.1、面向对象介绍
(1)一种编程思想(面向过程、面向对象)
(2)什么是对象
(3)面向对象三大特征:封装、继承、多态
(4)面向对象编程思想和面向对象语言是两码事
3.5.19.2、标准库的面向对象特征
(1)各种数据类型结构体就是一种封装
(2)标准库是为了被复用
(3)GPIO的编程模式是典型的面向对象式编程
典型面向对象的编程模式:
第1步:先构建对象(可以理解为定义一个结构体类型)
第2步:用对象构造实例(可以理解为用结构体类型来定义结构体变量)malloc
第3步:填充实例(其实就是给结构体的各个元素赋值)
第4步:使用实例(其实就是把结构体变量作为参数传给某个函数使用)
第5步:销毁实例(其实就是把前面第2步定义的机构体变量给销毁掉)free
本节课结束!