1 STM32简介

1.1 STM和ARM介绍

STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器(MCP)。ARM Cortex-M内核是STM32内部的核心部分,相当于整个芯片的CPU,这个内核是ARM公司设计的,其他外设部分是STM公司设计的。

STM32系列介绍

补充,ARM介绍

ARM既指ARM公司,也指ARM处理器内核

ARM公司是全球领先的半导体知识产权(IP)提供商,全球超95%的智能手机和平板电脑都采用ARM架构

ARM公司设计ARM内核,半导体厂商(ST公司)完善内核周边电路并生产芯片,结构如下图所示

1.2 本课程使用芯片介绍

1.2.1 芯片的命名规则

1.2.2 芯片基本信息

STM32F103C8T6芯片 实物图
• 系列:主流系列STM32F1
• 内核:ARM Cortex-M3
• 主频:72MHz
• RAM:20K(SRAM)
• ROM:64K(Flash)
• 供电:2.0~3.6V(标准3.3V)
• 封装:LQFP48(48个引脚)

1.2.3 芯片外设(Peripheral)

芯片外设就是STM公司在ARM内核上添加的外围电路,又称片上资源

外设英文缩写 外设中文名称 外设英文缩写 外设中文名称
NVIC 嵌套向量中断控制器 DMA 直接内存访问
SysTick 系统滴答定时器 ADC 模数转换器
RCC 复位和时钟控制 RTC 实时时钟
GPIO 通用IO口 CRC CRC校验
AFIO 复用IO口 PWR 电源控制
EXTI 外部中断 BKP 备份寄存器
TIM 定时器 IWDG 独立看门狗
USART 同步/异步串口通信 WWDG 窗口看门狗
I2C I2C通信 DAC 数模转换器
SPI SPI通信 SDIO SD卡接口
CAN CAN通信 FSMC 可变静态存储控制器
USB USB通信 USB OTG USB主机接口

其中,标红的NVIC和SysTick是位于Cortex-M3内核里面的外设,其他的都是内核外的外设。此外,标蓝的是我们学习的芯片所没有的外设。

SysTick是内核里面的一个定时器,主要用来给操作系统提供定时服务的,STM32是可以加入操作系统的,比如FreeRTOS、UCOS等,如果用了这些操作系统,就需要SysTick提供定时产生Tick中断来进行任务切换的功能。如果不使用操作系统,SysTick也可以当作一个简单的延时函数处理。

🔰

  • RCC
    • 可以对系统的时钟进行配置,还有就是使能各模块的时钟,在STM32中,其它的这些外设在上电的情况下默认是没有开启时钟的,不给时钟的情况下,操作外设是无效的,外设也不会工作,这样的目的是降低功耗。
  • AFIO
    • 它可以完成复用功能端口的重定义,还有中断端口的配置
  • EXTI
    • 配置好外部中断后,当GPIO引脚有电平变化时,就可以触发中断,让CPU来处理任务。
  • TIM
    • 是整个STMB2最常用、功能最多的外设
    • TIM分为高级定时器、通用定时器、基本定时器三种类型
    • 可以完成定时中断的任务,还可以完成测频率、生成PWM波形、配置成专用的编码器接口等功能
  • ADC
    • STM32内置了12位的AD转换器可以直接读取GPIO引脚的模拟电压值,无需外部连接AD芯片,使用非常方便。
  • DMA
    • 可以帮助CPU完成搬运大量数据这样的繁杂任务
  • I2C、SPI
    • 非常常用的两种通信协议,STMB2也内置了它们的控制器,可以用硬件来输出时波形。
    • 当然用通用GPIO口来模拟通信的时波形也是没有问题的,但还是硬件实现方式使用起来更高效。
  • CAN
    • CAN通信一般用于汽车领域
  • RTC
    • 在STM32内部完成年月日、时分秒的计时功能
    • 可以接外部备用电池,即使掉电也能正常运行
  • CRC
    • 专门设计的外部电路,用于进行数据校验,用于判断数据的正确性。
  • PWR
    • 电源控制,可以让芯片进入睡眠模式等状态,来达到省电的目的。
  • BKP
    • 这是一段存储器,当系统掉电时仍可由备用电池保持数据,这个根据需要,可以完成一些特殊功能。
  • IWDG、WWDG
    • 当单片机因为电磁干扰死机或者程序设计不合理出现死循环时,看门狗可以及时复位芯片,保证系统的稳定
  • DAC
    • 可以在I回直接输出模拟电压,是ADC模数转换的逆过程。
  • SDIO
    • 是SD卡接口,可以用来读取SD卡
  • FSMC
    • 可以用于扩展内存
    • 或者配置成其他总线协议,用于某些硬件的操作

1.2.4 芯片的系统结构

  • 三条总线:ICode指令总线(加载程序指令)、DCode数据总线(加载数据,比如常量和调试数据)、Systme系统总线。ICode与DCode总线主要用来连接Flash闪存(Flash存储的是编写的程序)。
    • ICode(Instruction,指令总线):程序编译后的指令存放在内部FLASH中,M3内核通过ICode总线取指,然后再执行指令。
    • DCode(Data,数据总线):程序有常量和变量。const修饰的变量为常量存储在内部FLASH中,变量不管是全局变量还是局部变量都存放在SRAM中。由于数据可以被DCode和DMA总线访问,所以就需要经过总线矩阵来仲裁。
    • Systme(系统总线):系统总线主要用来访问外设寄存器(即读写寄存器就是通过该总线完成),比如SRAM(用于存储程序运行时的变量数据)。
  • AHB(先进高性能总线)系统总线用于挂载主要的外设(挂载最基本或者性能比较高的外设,比如复位和时钟控制这些最基本的电路)SDIO也是挂载在AHB上的。
    • 两个桥接,接到了APB1和APB2两个外设总线上(APB代表先进外设总线,用来连接一般的外设)。因为AHB和APB的总线协议、总线速度还有数据传输格式的差异,所以中间需要加两个桥接来完成数据的转换和缓存
    • AHB的整体性能比APB高一些,APB2的性能比APB1高一些。APB2一般和AHB同频率都是72MHz,APB1一般是36MHz,所以APB2连接的一般是外设中稍微重要的部分(例如GPIO端口,还有一些外设的一号选手比如USART1、SPI1、TIM1、TIM8(高级定时器)、ADC、EXTI、AFIO),APB1连接次要一点的外设2、3、4号外设还有DAC/PWR/BKP等,在实际使用中我们感觉不到APB1和APB2的性能差异,只需要知道外设是挂载到哪个总线上的就可以了。
  • DMA是内核CPU的小秘书,比如一些大量的数据搬运这样简单且重复干的事情,让CPU来干会浪费时间。DMA通过DMA总线连接到总线矩阵上,可以拥有和CPU一样的总线控制权,用于访问外设小弟,当需要DMA搬运数据时,外设就会通过请求线发送DMA请求,然后DMA就会获得总线控制权,访问并转运数据,整个过程不需要CPU的参与,省下CPU的时间来干其他的事情。

1.2.5 芯片引脚定义

上表中,标橙红色的是电源相关的引脚,标蓝色的是最小系统相关的引脚,标绿色的是GPIO口、功能口这些引脚。如果我们想让STM32正常工作,首先就需要把电源部分和最小系统部分的电路连接好,也就是上表中标注红色和蓝色的部分。

表中第三列的含义:S代表电源、I代表输入、O代表输出、I/O代表输入输出。

表中第四列的含义:GPIO口所能容忍的电压,FT代表能容忍5V电压没有FT的只能容忍3.3V电压,如果没有FT的需要接5V的电平,就需要加装电平转换电路/芯片了。

表中第五列的含义:主功能就是上电后默认的功能,一般和引脚名称相同。如果不同的话引脚的实际功能是主功能而不是引脚名称的功能。

表中第六列的含义:默认复用功能,是IO口上同时连接的外设功能引脚,就是片上外设的端口和GPIO的连接关系,配置IO口时可以选择是通用IO口还是复用功能

表中第七列的含义:重定义功能,作用是如果有两个功能同时复用在了一个IO口上,而且确实需要用到这两个功能,可以将其中一个复用功能重映射到其他端口上(前提是,这个重定义功能的表里有对应的端口)。

推荐:优先使用加粗的IO口,没有加粗的IO口可能需要进行配置或者兼具其他功能。

详细引脚介绍如下表所示:

标号 引脚左右
1 VBAT是备用电池供电引脚,可接3v电池,当系统电源断电时,备用电池可给内部的RTC时钟和备份寄存器提供电源。
2 IO口或侵入检测或RTC,IO口可以根据程序输出或读取高低电平。侵入检测可以用来做安全保障的功能(比如你的产品安全性比较高,可以在外壳加一些防拆的触点,然后接上电路到这个引脚上,若有人强行拆开设备,则触点断开,这个引脚的电平变化就会触发STM32的侵入信号,然后就会清空数据来保证安全)。RTC的引脚可以用来输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲
3、4 IO口或者接32.768KHz的RTC晶振
5、6 接系统的主晶振,一般是8MHz,然后芯片内有锁相环电路,可以对这个8MHz的频率进行倍频,最终产生72Mhz的频率作为系统的主时钟
7 NRST是系统复位引脚,N代表是低电平复位
8、9 内部模拟部分的电源,比如ADC、RC振荡器等。VSS是负极,接GND,VDD是正级,接3.3V
10-19 都是IO口,其中PA0还兼具了WKUP功能(可以用于唤醒处于待机模式的STM32)
20 IO口或BOOT1引脚,BOOT引脚是用来配置启动模式的
21、22 IO口
23、24 VSS_1和VDD_1是系统的主电源口,同样的VSS是负极,VDD是正级
25-33 都是IO口
27、34-40 都是IO口或者调试端口,默认功能是调试端口(用来调试程序和下载数据),这个STM32支持SWD(需要两根线,分别是SWDIO和SWCLK)和JTAG(需要五根线,分别是JTMS、JTCK、JTDI、JTDO、NJTRST)两种调试方式。 如果作为调试端口,则需要编写程序配置为普通IO口。
41-46 除去44号引脚,都是IO口
44 是BOOT0引脚,和BOOT1一样用来做启动配置

1.2.6 启动配置

启动配置的作用是指定程序开始运行的位置,一般情况下,程序都是在Flash程序存储器开始执行(第一种启动模式)。但是在某些情况下,我们也可以让程序在别的地方开始执行。

第二种启动模式(串口下载用的,区别于使用JLink):系统存储器存的就是STM32中的一段BootLoader程序,BootLoader的作用就是接收串口的数据,然后刷新到主闪存中(最后再跳转到第一种位置处运行)。简单说这种模式就是使用串口下载程序。

第三种启动模式:主要用来程序调试的,用的比较少。

注意:BOOT引脚的值是在上电复位后的一瞬间有效的,之后就随便了。查看上面的引脚分布图,发现BOOT1和PB2是在同一个引脚上,也就是在上电瞬间是BOOT1功能,在第四个时钟过后,就是PB2的功能了

1.2.7 最小系统电路

如果想让STM32正常工作,首先就需要把电源和最小系统部分的电路连接好。也就是前面表格中标红色和蓝色的部分。

  • 供电部分电路

右边三个分区供电的主电源和模拟部分电源都连接到了供电引脚,VSS都连接了GND,VDD都连接了3,3V,在3.3V和GND之间,一般都会连接一个滤波电容,保证供电电压的稳定。

左上脚VBAT接的备用电池(纽扣电池),用来给RTC和备份寄存器服务的。如果不用备用电池,VBAT可以直接接3.3V或者悬空。

STM32的供电还是比较多的,而且芯片四周都有供电引脚,这个要是自己画板子的话,就会深有体会,走线比较头疼。

  • 晶振电路

接了一个8MHz的主时钟晶振,经过内部锁相环倍频,得到72MHz的主频。晶振连接到STM32的5、6号引脚。另外还需要接两个20pF的电容,作为起振电容,电容的另一端接地即可。

如果需要RTC功能,还需要再接一个32.768KHz的晶振,电路和这个一样接到3、4号引脚。OSC32就是32.768KHz晶振的意思。为什么要用32.768KHz?因为32768是2的15次方,内部RTC电路经过2的15次方分频,就可以生成1S的时间信号了。

  • 复位电路

复位电路是一个10k的电阻和0.1uF的电容组成的,用来给单片机提供复位信号。NRST接到STM32的7号引脚,NRST是低电平复位的,当这个复位电路在上电的瞬间,电容是没有电的,电源通过电阻开始向电容充电,并且此时电容呈现的是短路状态,NRST就会产生低电平,当电容逐渐充满电时,电容就相当于断路,此时、NRST就会被R1上拉为高电平。那上电瞬间的波形就是先低电平,然后逐渐高电平,这个低电平就可以提供STM32的上电复位信号。当然电容充电还是非常快的,所以在我们看来单片机在上电的一瞬间复位了,这就是复位电路的作用。

电容左边还并联了一个按键,提供手动复位的功能。按键按下时,电容被放电,并且NRST引脚也通过按键被直接接地了,相当于手动产生了低电平复位信号。按键松手后,NRST又回归高电平,此时单片机就从复位状态转为工作状态。一般复位按键都是在一个小孔里,拿针戳一下设备就复位了。

  • 启动配置

跳线帽的方式,接拨码开关也可以。

参考链接:江科大STM32最全笔记整理『上篇』 - CSDN

2 新建工程流程

2.1 库函数文件夹

使用库函数的方式,需要准备一个STM32库函数的压缩包,解压之后的文件内容如下:

库函数文件夹里的内容解释如下:

  1. _htmresc文件里面是两个公司图片,不用管
  2. Libraries里面就是库函数的文件,之后建工程会用到
  3. project里是官方提供的工程示例和模版,使用库函数可以参考一下
  4. Utilities是stm32官方评估板的相关例程,这个评估板是官方用STM32做的一个小电路板用来测评STM32的,这个文件夹存的就是这个小电路板的测评程序
  5. 最后面两个文件,一个是库函数的发布文档(有一些版本的说明),一个是使用手册(教如何使用库函数)

2.2 建工程步骤

2.2.1 新建工程目录

给工程起一个通用的名字(注意,是工程名不是存放工程的文件夹名称),存放工程的文件夹的名称是方便改的,工程名称不太方便改。

2.2.2 选择型号

使用的是stm32f103c8t6

下面会弹出的是新建工程小助手,可以帮助快速新建工程。(本次实验不使用,可以先跳过)

到此,工程已经建立了,但工程文件是空的,现在这个工程还不能用,需要添加一点工程的必要文件。

2.2.3 添加工程必要文件

打开固件库的文件夹Libraries\CMSIS\CM3\DeviceSupport\ST\STM32F10x\startup\arm,如下图所示就是STM32的启动文件,STM32的程序就是从启动文件开始执行的。最简单的方式就是将全部启动文件全部复制下来,然后回到工程文件夹里,但是如果知道了芯片型号,只需要复制对应的启动文件即可,不用全部复制。

工程文件夹如下,里面是新建工程自动生成的文件:

在工程文件夹里,新建一个文件夹(可以起名为start)用来存放复制过来的启动文件

接着回到固件库的STM32F10x文件,可以看到stm32f10x.h和两个system开头的文件system_stm32f10x.csystem_stm32f10x.h文件,如下图所示,将这三个文件复制下来,也粘贴到Start文件夹下。

stm32f10x.h是STM32的外设寄存器描述文件,作用和51单片机的头文件REGX52.H一样,是用来描述stm32有哪些寄存器和它对应的地址的

两个system文件是用来配置时钟的,stm32主频72MHz,就是system文件里的函数配置的,可以手动修改这个文件,具体要掌握时钟树的配置流程。

接下来添加内核寄存器的描述文件

因为stm32是内核和内核外围的设备组成的,而且这个内核的寄存器描述文件和外围设备的描述文件不在一起,所以还需要添加一个内核寄存器的描述文件。

STM32由 ‌ARM Cortex-M3内核‌ 和 ‌ST设计的外设‌ 组成。内核寄存器由ARM定义,外设寄存器由ST定义,因此描述文件需分开存储。

内核寄存器描述文件

  • 文件名称:‌core_cm3.h‌(配套实现文件为 ‌core_cm3.c‌)
  • 作用:描述Cortex-M3内核的寄存器(如NVIC中断控制器、SysTick系统定时器等),并提供内核级配置函数。这些文件由ARM公司统一提供,与芯片厂商(ST公司)无关。

外设寄存器描述文件

  • 文件名称:‌stm32f10x.h
  • 作用:描述STM32芯片的外设寄存器(如GPIO、USART、TIM等)及其地址映射,相当于51单片机中的 REGX52.H 头文件。

打开固件库文件夹下的CM3\CoreSupport文件夹,有两个cm3(Cortex-M3)文件(core_cm3.hcore_cm3.c),.h文件就是内核的寄存器描述,当然还有一些内核的配置函数,所以多了个.c文件。将两个cm3文件复制粘贴到工程文件夹的Start文件夹下。

到此为止,工程的必要文件就复制完成了。

然后回到keil软件,将刚才复制的文件(start)添加到工程里。点击选中Source Group 1,然后再点击一下,把这个组改一下名字,也叫Start。

接着右键,选择添加已经存在的文件到组里。首先添加一下启动文件,启动文件有很多分类,我们只能添加其中一个,我们所用型号需要选择这个后缀为md.s的启动文件(为什么选择这个启动文件,在后面 “新建工程里的启动文件选择” 这个章节解释),选中它点击Add。

然后剩下的.c和.h文件都要添加进来,然后Close,这样我们的Start文件夹里面的文件就添加好了,如下图所示:

最后我们需要在工程选项里添加上这个文件夹的头文件路径,要不然软件是找不到.h文件的。

点击魔术棒按钮,打开工程选项,在c/c++里,找到这个Include Paths栏,然后点击右边的三个点的按钮,然后再点击新建路径,然后再点三个点的按钮,把start的路径添加进来,点击ok,就把这个文件夹的头文件路径添加进来了:

2.3 部分配置补充解释

2.3.1 新建工程里的启动文件选择

新建工程第一个加的就是启动文件,这个启动文件有很多类型,至于选择哪一个,要根据芯片型号来选择。在下面的表中,这是stm32f1系列中的型号分类,其中根据Flash的大小,分为了小容量产品LD,中容量产品MD、大容量产品H。stm32f100系列为超值系列,简写为VL,F105和F107为互联型产品CL,这个就没有根据Flash大小来分类。stm32f103c8t6的Flash为64K,所以选择MD的启动文件。

缩写 释义 Flash容量 型号
LD_VL 小容量产品超值系列 16~32K STM32F100
MD_VL 中容量产品超值系列 64~128K STM32F100
HD_VL 大容量产品超值系列 256~512K STM32F100
LD 小容量产品 16~32K STM32F101/102/103
MD 中容量产品 64~128K STM32F101/102/103
HD 大容量产品 256~512K STM32F101/102/103
XL 加大容量产品 大于512K STM32F101/102/103
CL 互联型产品 - STM32F105/107

2.3.2 新建文件步骤总结

  1. 建立工程文件夹,Keil中新建工程,选择型号
  2. 工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹
  3. 工程里对应建立Start、Library、User等同名称的分组,然后将文件夹内的文件添加到工程分组里
  4. 工程选项,C/C++,Include Paths内声明所有包含头文件的文件夹(因为像Start等文件夹是自己建的,keil并不知道,所以必须声明一下路径,最好就是自己新建文件就声明一下,这样就不会出现.h文件找不到的问题)
  5. 工程选项,C/C++,Define内定义USE_STDPERIPH_DRIVER(使用库函数就必须定义这个)
  6. 工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run

2.3.3 直接使用寄存器点亮LED

点亮一个LED小灯只需要配置3个寄存器即可。(LED的长脚是正极,短脚是负极)

  • 首先,找到打开STM32的参考手册,首先是RCC的一个寄存器,来使能GPIOC的时钟

GPIO都是APB2的外设,因此找到“APB2外设时钟使能寄存器(RCC_APB2ENR)”章节,寻找对应的寄存器。

如下图,可以看到这里有个IOPCEN,这位就是使能GPIOC的时钟的

数据手册的下面还有对应位的功能说明:

因此,要打开GPIOC的时钟,就需要将寄存器第4位设置为1,因为这是32位芯片,因此此寄存器的数值设置为0x0000 0010

  • 其次,要配置PC13口的工作模式

继续在数据手册中找到端口配置高寄存器GPOx_CRH,这个x可以是A到E的任意一个字母。如下图所示。

其中,CNFMODE两位配合使用,控制输入输出模式,以及输出电平的切换速度快慢。详细请参考手册,这里就不展开了,感觉图片占空间太多了。

  • 最后,设置PC13的输出电平的高低

如下图所示,ODR13位写1,端口输出高电平,写0,则输出低电平,该高低电平可控制LED灯的亮灭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "stm32f10x.h"

/**
*@brief 点亮PC13引脚的LED灯
*@note 直接使用寄存器方法,不使用库函数和HAL库
*/
int main(void)
{
// 根据数据手册,指定对应的寄存器,打开GPIOC的时钟
RCC->APB2ENR = 0x00000010;
// GPIOC13号端口设置为通用推挽输出模式,MODE配置为最大速度50MHz
GPIOC->CRH = 0x00300000;
// 配置GPIO PC13口输出高电平,但此时接在PC13口的LED不亮(此小灯是低电平点亮)
GPIOC->ODR = 0x00002000;
// 配置GPIO PC13口输出低电平,此时接在PC13口的LED亮起来
GPIOC->ODR = 0x00000000;
while (1)
{

}
return 0;
}

2.3.4 使用STM提供的库函数点亮LED

其实库函数本质就是把上面的这些寄存器操作封装成了库函数,这些库函数是由STM公司提供的。根据上面的工程目录,库函数文件存放在STM32F10x_StdPeriph_Lib_V3.5.0\STM32F10x_StdPeriph_Lib_V3.5.0\Libraries\STM32F10x_StdPeriph_Driver这个文件夹下面,该文件夹的结构如下图所示:

misc.c是与内核相关的库函数,其他的就是内核外的外设库函数了。

在工程中添加Library文件夹,并添加到Keil5的项目中,然后把头文件和源文件都复制到该目录下。但是此时库函数还是不能直接使用,我们进入官方给出的工程示例文件夹STM32F10x_StdPeriph_Lib_V3.5.0\STM32F10x_StdPeriph_Lib_V3.5.0\Project\STM32F10x_StdPeriph_Template该文件夹下面有3个文件需要用到:stm32f10x_conf.hstm32f10x_it.cstm32f10x_it.h,将这三个文件复制到User目录下面。

这个conf(configuration)文件是用来配置库函数头文件的包含关系的,另外这里面还有个用来这是所有库函数都需要的

两个it(interrupt)文件是用来存放中断函数的

此外,在stm32f10x.h头文件中有如下代码

1
2
3
4
// 这段代码是:是否使用ST官方提供的库函数的条件编译
#ifdef USE_STDPERIPHDRIVER
#include "stm32f10x_conf.h"
#endif

这也就意味着:USE_STDPERIPH_DRIVER(使用库函数就必须定义这个宏)

最后,可以开始编写基于库函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include "stm32f10x.h"
//PS,由于定义了USE_STDPERIPH_DRIVER宏,stm32f10x.h内部引用了stm32f10x_conf.h,就能使用库函数了

/**
*@brief 点亮PC13引脚的LED灯
*@note 使用库函数方法
*/
int main(void)
{
// 1. 使用库函数打开GPIOC的时钟
RCC_APB2PeriphclockCmd(RCC_APB2Periph_GPIOC, ENABLE);
// 2. 配置PC13为推挽输出
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStructure);

while(1)
{
// 3. 点亮LED(PC13低电平有效)
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
Delay(0xFFFFF);

// 4. 熄灭LED
GPIO_SetBits(GPIOC, GPIO_Pin_13);
Delay(0xFFFFF);
}
return 0;
}

2.3.5 简要介绍工程架构

  • 工程结构主动执行的部分(工程架构左边两个图)
    • 首先是startup启动文件,这个是程序执行最基本的文件,keil中启动文件是用汇编写的,启动文件内定义了中断向量表,中断服务函数等,这个中断函数中有个复位中断,这就是整个程序的入口,当stm32上电复位或者按下复位开关之后,程序就会进入复位中断函数执行,复位函数中断就主要做了两件事情,第一个是调用SystemInit函数,第二个是调用main函数,然后程序就结束了,当然,实际上单片机的程序永远都不会结束,所以在main函数的最后一定有一个死循环
    • SystemInit函数就是定义在System_xx开头的.c里的,在keil里也可以看到这个函数的定义(在main函数之前,单片机就已经执行了一堆东西了,帮我们把闪存接口,时钟等一系列杂碎的东西都配置好了)。
    • 另外在启动文件还定义了stm32所有的其他中断,这些中断达到触发条件后就会自执行,这个中断函数的定义就是在stm32fx_it里面的,自己的中断有建议位置,如下图有所示,当然我们还是习惯在哪里用中断就在哪里,写在别的地方也是可以的。以上就是中断部分的逻辑。

另外你也可以自己定义一些用户文件,来封装一些模块供主程序和中断调用,有利于程序结构的模块化,要不然所有的程序都在主函数里,那主函数就太长了,到此为止,这个工程结构主动执行的部分就介绍完了。

  • 被动执行部分(工程架构右1图)

被动执行部分,相当于stm32的资源了,我们在主函数或者中断函数里,就可以调用这些资源,右上角这两个stm32f10x.hcore_cm3这些文件就是外设和内核外设的寄存器描述,点开文件可以看到,都是寄存器和寄存器名字,还有地址信息等,如果直接调用这些寄存器来使用stm32,那就是寄存器的开发方式。

寄存器开发很麻烦,所以就提供了下面的两个文件,就是库函数文件,在keil中可以看到,这每个外设都提供了一大堆函数,这些函数封装了寄存器的操作,给我们提供更加人性化的函数调用方式,只要学会了操作套路,那配置一个外设就很简单,连手册都不需看。这个conf的文件就是用来配置头文件的包含关系的,在keil中可以看到conf文件include了所有的库函数头文件,同时我们在stm32f10x.h的最后又包含了conf,所以在使用这些库函数时,我们只需要包含stm32f10x.h这一个头文件,就相当于包含了所有的库函数头文件,这样我们就可以任意地调用库函数了,以上就是整个工程的结构和每个文件的使用。

3 外设一:GPIO

3.1 GPIO简介

GPIO(General Purpose Input Output)通用输入输出口。可配置为8种输入输出模式

引脚电平:0V~3.3V,部分引脚可容忍5V。(0V是低电平是数据0,3.3V是高电平是数据1。容忍5v意思是可以在这个端口输入5V的点电压,也认为是高电平,但对于输出而言,最大就只能输出3.3V,因为供电只有3.3V)

输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等。

(后面文章显示的LED和蜂鸣器的程序现象,就使用到了GPIO的输出模式。另外在其他的应用场景,只要是可以用高低电平来进行控制地方都可以用GPIO来完成;如果控制的是功率比较大的设备,只需要再加入驱动电路即可;此外,还可以用GPIO来模拟通信协议,比如I2C、SPI或某个芯片特定协议,我们都可以用GPIO的输出模式来模拟其中的输出时序部分)

输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等。(输入模式最常见的就是读取按键了,用来捕获我们的案件按下事件;另外,也可以读取带有数字输出的一些模块,比如,光敏电阻模块、热敏电阻模块等;如果这个模块输出的是模拟量,那GPIO还可以配置成模拟输入模式,再配合内部的ADC外设,就能读取端口的模拟电压了;除此之外,模拟通信协议时,接收通信线上的通信数据,也是靠GPIO的输入来完成的)。

3.2 GPIO的基本结构

如下图所示,为GPIO的整体构造,其中左边的是APB2外设总线;在stm32中所有的GPIO都是挂载在APB2外设总线上的,其中GPIO外设的名称都是按照GPIOA、GPIOB等等这样来命名的,每个GPIO外设,总共有16个引脚,编号是从0到15,GPIOA的第0号引脚,我们一般把它称为PA0,接着第一号就是PA1…PA15以此来命名。

在每个GPIO模块内,主要包含了寄存器驱动器

  • 寄存器
    • 就是一段特殊的存储器,内核可以通过APB2总线对寄存器进行读写,这样就可以完成输出电平和读取电平的功能了,寄存器的每一位对应一个引脚,其中,输出寄存器写1,对应的引脚就会输出高电平,写0就会输出低电平,输入寄存器读取为1,就证明对应的端口目前是高电平,读取为0,就是低电平;
    • 因为STM32是32位单片机,所以STM32内部的寄存器都是32位的,但这个端口只有16位,所以这个寄存器只有低16位对应的有端口,高16位是没有用到的;
  • 驱动器
    • 是用来增加信号的驱动能力,寄存器只负责存储数据,如果要进行点灯这样的操作,还是需要驱动器来负责增大驱动能力。

3.3 GPIO位结构(每一位的具体电路结构)

如下图为,stm32参考手册中的GPIO位结构的电路图。

图中左边三个就是寄存器,中间部分是驱动器,右边是某一个IO口的引脚。整体结构可以分为两个部分,上面是输入部分,下面是输出部分。

3.3.1 输入部分

(一)IO引脚

首先是这个IO引脚,这里接了两个保护二极管,这个是对输入电压进行限幅的,上面二极管接VDD(3.3V),下面二极管接VSS((0V);如果输入电压比3.3V还要高,那上方这个二极管就会导通,输入电压产生的电流就会直接流入VDD而不会流入内部电路,这样就可以避免过高的电压对内部电路产生伤害。同理,如果输入电压比0V还要低,这个电压是相对与VSS的电压,所以是可以有负电压的,那这时下方这个二极管就会导通,电流会从VSS直接流出来,电流会从VSS直接流出去,而不会从内部电路汲取电流,也是可以保护内部电路的。 如果输入电压在0-3.3V之间,那两个保护二极管均不会导通,这时二极管对电路没有影响,这就是保护二极管的用途。

(二)上拉和下拉电阻

上拉和下拉的作用:是为了给输入提供一个默认的输入电平,因为对应一个数字的端口,输入不是高电平就是低电平;如果输入引脚啥都不接,这时输入就会处于一个浮空状态,引脚的输入电平极易受外界干扰而改变;为了避免引脚悬空导致的输入数据不稳定,我们就需要在这里加上上拉或下拉电阻。

上拉电阻至VDD,下拉电阻至VSS,这个开关是可以通过程序进行配置的。上面导通、下面断开,就是上拉输入模式;上面断开、下面导通,就是下拉输入模式;上面断开、下面断开,就是浮空输入模式。

如果接入上拉电阻,当引脚悬空时,还有上拉电阻来保证引脚的高电平,所以上拉输入是默认为高电平的输入模式,下拉也是同理,默认为低电平的输入方式。

上拉电阻和下拉电阻的阻值都是比较大的,是一种弱上拉和弱下拉 ,目的是尽量不影响正常的输入操作。

(三)施密特触发器

施密特触发器的作用就是对输入电压进行整形的,它的执行逻辑是,如果输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平,这样可以有效的避免由于信号波动造成的输出抖动现象。

图中显示的是肖特基触发器,应该是翻译错误,实际就是施密特触发器。

接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取输入数据寄存器对应某一位的数据,就可以知道端口的输入电平了。

最后上面这还有两路线路,这些就是连接到片上外设的一些端口,其中有模拟输入,这个是连接到ADC外设上的,因为ADC需要接收模拟量,所以这根线是接到施密特触发器前面的;另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面。

3.3.2 输出部分

输出部分可以由输出数据寄存器片上外设控制,两种控制方式通过这个数据选择器(输出控制左侧梯形)接到输出控制部分。 如果选择通过输出数据寄存器进行控制,就是普通的IO口输出,写这个输出数据寄存器的某一位就可以操作对应的某个端口了。

最左侧是位设置/清除寄存器:这个可以用来单独操作输出数据寄存器的某一位,而不影响其它位。因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的操作方式。

  • 第一种方式是先读出这个寄存器,然后用 按位与 和 按位或 的方式更改某一位,最后再将更改后的数据写回去,在C语言中就是&=|=的操作,这种方法比较麻烦,效率不高,对于IO口的操作而言不太合适;
  • 第二种方式是通过设置这个位设置和位清除寄存器,如果我们要对某一位进行置1的操作,在位设置寄存器的对应位写1便可,剩下不需要操作的位写0,这样它内部就会有电路,自动将输出数据寄存器中对应位置为1,而剩下写0的位则保持不变,这样就保证了只操作其中某一位而不影响其它位,并且这是一步到位的操作。如果想对某一位进行清0的操作,就在位清除寄存器的对应位写1即可,这样内部电路就会把这一位清0了,这就是第二种方式也就是这个位设置和位清除寄存器的作用。【作用:将设置/清除寄存器的某一位写1/0就能达到单独影响输出寄存器的某一位,从而单独影响某个端口】。
  • 第三种操作方式【了解即可】 ,就是读写STM32中的“位带”区域,这个位带的作用就跟51单片机的位寻址作用差不多,在STM32中,专门分配的有一段地址区域,这段地址映射了RAM和外设寄存器所有的位,读写这段地址中的数据,就相当于读写所映射位置的某一位,这就是位带的操作方式,这个方式我们本课程暂时不会用到。我们的教程主要使用的是库函数来操作的,库函数使用的是读写位设置和位清除寄存器的方法。

输出控制之后就接到了两个MOS管

上面是P-MOS,下面是N-MOS,这个MOS管就是一种电子开关,我们的信号来控制开关的导通和关闭,开关负责将IO口接到VDD或者VSS,这里可以选择推挽、开漏或关闭三种输出方式。

  • 推挽输出模式
    • 在推挽输出模式下,P-MOS和N-MOS均有效,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平,数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,就是输出低电平,这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推输出模式。在推挽输出模式下,STM32对IO口具有绝对的控制权,高低电平都由STM32说的算。
  • 开漏输出模式
    • 在开漏输出模式下,上面P-MOS是无效的(没接P-MOS,就是开放的,所以叫开漏,open drain,韦东山老师讲过一个很有意思的GPIO子系统BUG,就是和开漏有关,观看地址),只有下面N-MOS在工作,数据寄存器为1时,下管断开,这时输出相当于断开,也就是高阻模式;数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平;这种模式下,只有低电平有驱动能力,高电平是没有驱动能力的。那这个模式有什么用呢,这个开漏模式可以作为通信协议的驱动方式,比如I2C通信的引脚,就是使用的开漏模式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰,另外开漏模式还可以用于输出5V的电平信号。
    • 比如在IO口外接一个上拉电阻到5V的电源,开漏模式下,输出1时,两个mos管都相当于关断,左侧相当于断路(高阻模式),外接5V的电能只能流向右侧,故输出5V(用于兼容一些5V电平的设备,这就是开漏输出的主要用途)。反之,输出0时,左下方mos管导通,外接5V的电能流到左下方Vss,且两者之间几乎没有电压降,可看做5V电压降在了上拉电阻上,故引脚输出0V。
  • 关闭状态输出方式
    • 这个是当引脚配置为输入模式的时候,这两个MOS管都无效,也就是输出关闭,端口的电平由外部信号来控制。

3.3.3 GPIO8种工作模式

通过配置GPIO的端口配置寄存器,上面的位结构的电路就会根据我们的配置进行改变(比如,开关的通断、N-MOS和P-MOS是否有效、数据选择器的选择等),端口可以配置成以下8种模式:

模式名称 性质 特征 对应代码
浮空输入 数字输入 可读取引脚电平,若引脚悬空,则电平不确定 GPIO_Mode_IN_FLOATING
上拉输入 数字输入 可读取引脚电平,内部连接上拉电阻,悬空时默认高电平 GPIO_Mode_IPU
下拉输入 数字输入 读取引脚电平,内部连接下拉电阻,悬空时默认低电平 GPIO_Mode_IPD
模拟输入 模拟输入 GPIO无效,引脚直接接入内部ADC GPIO_Mode_AIN
开漏输出 数字输出 可输出引脚电平,高电平为高阻态,低电平接VSS GPIO_Mode_Out_OD
推挽输出 数字输出 可输出引脚电平,高电平接VDD,低电平接VSS GPIO_Mode_Out_PP
复用开漏输出 数字输出 由片上外设控制,高电平为高阻态,低电平接VSS GPIO_Mode_AF_OD
复用推挽输出 数字输出 由片上外设控制,高电平接VDD,低电平接VSS GPIO_Mode_AF_PP

注意:

在输出模式下,输入模式也是有效的,但是在输入模式下,输出都是无效的,这是因为一个端口只能有一个输出,但可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入一下

总之:

其实在GPIO的这8种模式中,除了模拟输入这个模式会关闭数字的输入功能,在其他的7个模式中,所有的输入都是有效的。(PS:这句话还是要好好掂量掂量,参考一下这个链接——STM32的GPIO为输出模式时获取其输出状态_单片机推挽输出可以读取引脚的电平吗-CSDN博客 )。

但是,至少开漏输出的时候是可以读取引脚上的输入电平的

3.4 GPIO实验

3.4.1 GPIO输出实验-LED灯闪烁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "stm32f10x.h"
#include "Delay.h"

int main(void)
{
// 开启GPIOC的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

// 初始化GPIOC的PC13端口
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);

while(1)
{
// 设置PC13引脚输出低电平,LED亮
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
// 延时1s
Delay_ms(1000);
// 设置PC13引脚输出高电平,LED灭
GPIO_SetBits(GPIOC, GPIO_Pin_13);
// 延时1s
Delay_ms(1000);
}

}

3.4.2 GPIO输入-按键控制LED

按键:最常见的输入设备,按下导通,松手断开。

按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。5-10ms的抖动对于人来说感觉不到,但对于高速运行的单片机来说就比较漫长了,单片机会感知到这段时间的电平抖动,消抖最常用的方法就是延时等待一段时间,耗过这段抖动时间。

按键按照下接的方式接入电路,功能是:按键按下,灯亮,再按下,灯灭,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include "stm32f10x.h"
#include "Delay.h"

// 检测按键是否按下
uint8_t key_press(void)
{
uint8_t key_num = 0;
// 获取PA0端口的输入,若为低电平,则说明按键按下
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
// 软件消抖,上面的低电平可能是信号毛刺,因此这里延时一段时间在判断
Delay_ms(20);
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
// 处理按键按着一直不松手的情况
while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0);
key_num = 1;
}
}
return key_num;
}


int main(void)
{
// 开启GPIOA、GPIOC的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

// 初始化GPIOA的PA0端口
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);
// 初始化GPIOC的PC13端口
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_13;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOC, &GPIO_InitStruct);

int num = 0;

while(1)
{
if(key_press() == 1)
{
if(num == 0)
{
// 设置PC13引脚输出低电平,LED亮
GPIO_ResetBits(GPIOC, GPIO_Pin_13);
num = 1;
}
else
{
// 设置PC13引脚输出高电平,LED灭
GPIO_SetBits(GPIOC, GPIO_Pin_13);
num = 0;
}
}

}

}

PS1:上面的代码还没经过测试,不知道是否正确;

PS2:UP的编写方法和我不同,UP使用了GPIO_ReadoutputDataBit(GPIOA, GPIo_Pin_0),该函数是读取GPIO输出寄存器的值,如果读取到当前输出为0(LED亮),则把PA0设置为1,反之亦然。

4 OLED显示屏调试工具

5 EXTI外部中断

中断系统是管理和执行中断的逻辑结构,外部中断是众多能产生中断的外设之一,所以本节我们就借助外部中断来学习一下中断系统。在以后学习其它外设的时候,也是会经常和中断打交道的。

5.1 中断系统介绍

5.1.1 中断

在主程序运行过程中,出现了特定的中断触发条件(中断源,比如对于外部中断来说,可以是引脚发生了电平跳变;对于定时器来说,可以是定时的时间到了;对于串口通信来说,可以是接收到了数据),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行。(就好比晚上睡觉前定了个闹钟,时间到了提醒你,不管时间到不到你可以安心睡觉,如果你没有闹钟,那你就得不断地看时间,生怕错过了起床点)。如果没有中断系统,为了防止外部中断被忽略或者串口数据被覆盖,那主程序就只能不断地查询是否有这些事件发生,不能再干其他事情了。

5.1.2 中断优先级

当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源。(这个中断优先级是我们根据程序设计的需求,自己设置的)。

5.1.3 中断嵌套

中断程序再次中断,二次中断现象,当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。(也是为了照顾非常紧急的中断)。

5.1.4 中断执行流程

中断程序的执行流程如下,当它执行到某个地方时,外设的中断条件满足了,那这时,无论主程序是在干什么事情(比如OLED显示程序才执行一半,Delay函数还在等待等)中断来了,主程序都得立即暂停,程序由硬件电路自动跳转到中断程序中,当中断程序执行完之后,程序再返回被暂停的地方继续运行(这个暂停的地方,叫做断点)。

为了程序能在中断返回后继续原来的工作,在中断执行前,会对程序的现场进行保护,中断执行后,会再还原现场(这个还原不需要我们来做),这样保证主程序被中断了,回来之后也能继续执行。

中断嵌套的执行流程如下。当一个中断正在执行时,又有新的优先级更高的中断来,那个旧中断会被打断,执行新的中断,新的中断结束,再继续执行原来的中断,原来的中断结束,再继续主程序,这就是中断嵌套的执行流程。

c语言中,中断的执行流程如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(void)
{
while (1)
{
// 主程序
// ...
// 主程序
}
}


// 中断函数
void EXTI0_IRQHandler(void)
{
// 中断程序
// ...
// 中断程序
}

上面是主函数,while(1)死循环里就是主程序,正常情况下,程序就是在主程序中不断循环执行,当中断条件满足时,主程序就会暂停,然后自动跳转到中断程序里运行,中断程序执行完之后,再返回主程序执行。一般中断程序都是在一个子函数里,这个中断函数不需要我们调用,当中断来临时,由硬件自动调用这个函数,这就是在c语言中,中断的执行流程。

5.2 STM32中断介绍

5.2.1 STM32中断概述

STM32有68个可屏蔽中断通道(中断源),包含EXTI(外部中断)、TIM、ADC(模数转换器)、USART(串口)、SPI、I2C、RTC(实时时钟)等多个外设。(几乎所有模块都能申请中断)

使用NVIC统一管理中断,每个中断通道都拥有16个可编程的优先等级,可对优先级进行分组,进一步设置抢占优先级和响应优先级

NVIC就是STM32中用来管理中断、分配优先级的,NVIC的中断优先级共有16个等级。EXTIx是外部中断对应的中断资源。

5.2.2 STM32中断资源

下图为stm32的中断资源。

img

  • 内核中断

上面灰色的是内核中断(我们一般不用,了解即可),比如第一个复位中断,当产生复位事件时,程序就会自动执行复位中断函数,也就是复位后程序开始执行的位置。

  • 外设中断

下面不是灰色的部分就是stm32外设的中断了,比如第一个窗口看门狗,这个是用来监测程序运行状态的中断,比如你程序卡死了,没有及时喂狗,窗口看门狗就会申请中断,让你的程序跳到窗口看门狗的中断程序里,那你在中断程序里就可以进行一些错误检查,看看是什么出问题了。然后PVD电源电压监测,如果你的供电电压不足,PVD电路就会申请中断,你在中断里赶紧保存一下重要数据。外设电路检测到有什么异常或事件,需要提示一下CPU的时候,它就可以申请中断,让程序调到对应的中断函数里运行一次,用来处理这个异常或事件。

图中最右边是中断的地址因为程序中的中断函数,它的地址是由编译器来分配的,是不固定的,但是我们的中断跳转,由于硬件的限制,只能跳到固定的地址执行程序,所以为了硬件能够跳转到一个不固定的中断函数里,这里就需要在内存中定义一个地址的列表,这个列表的地址是固定的,中断发生后,就跳到这个固定位置,然后在这个固定位置,由编译器,再加上一个跳转到中断函数的代码,这样中断跳转就可以跳转到任意位置了,这个中断地址的列表,就叫中断向量表,相当于中断跳转的一个跳板,不过我们用c编程,是不需要管这个中断向量表的,因为编译器都帮我们做好了。

5.3 NVIC嵌套中断向量控制器

5.3.1 NVIC基本结构

NVIC(嵌套中断向量控制器),在STM32中,它是用来统一分配中断优先级和管理中断的,NVIC是一个内核外设,是CPU的小助手(如果把中断全接到CPU上,会很麻烦,毕竟CPU主要是用来运算的,所以中断分配的任务就让NVIC来负责),NVIC有很多输入口,你有多少个中断线路都可以接过来。图中线上划了个斜杠上面写了n(这个意思是:一个外设可能会同时占用多个中断通道,所以这里有n条线),然后NVIC只有一个输出口,NVIC根据每个中断的优先级分配中断的先后顺序,之后通过右边这一输出口就告诉CPU该处理哪个中断,对于中断先后顺序分配的任务,CPU不需要知道。

举个想象化的比喻例子:

比如CPU是医生,如果医院只有一个医生时,当看病人很多时,医生就得先安排一下先看谁后看谁,如果有紧急的病人,那还得让紧急的病人最先来,这个安排先后顺序的任务很繁琐会影响医生看病的效率,所以医院就安排了一个叫号系统(NVIC),来病人了统一取号并且根据病人的等级,分配一个优先级,然后叫号系统看一下现在在排队的病人,优先叫号紧急的病人,最后叫号系统给医生输出的就是一个一个排好队的病人,医生就可以专心看病了。(EXTI、TIM、ADC等就是病人)

5.3.2 NVIC优先级分组

为了处理不同形式的优先级,STM32的NVIC可以对优先级进行分组,分为抢占优先级和响应优先级

抢占优先级和响应优先级的区别,例子理解:还是病人叫号的例子,对紧急的病人,有两种形式的优先:

  1. 一种是,上一个病人1在看病,外面排队了很多病人,当病人1看完后,外面排队中的紧急病人最先进去看病即使这个紧急病人是最后来的,这种在排队中的插队的就叫响应优先级,响应优先级高的可以插队提前看病。
  2. 另一种是,上一个病人1在看病,外面排队中的病人2比病人1更加紧急,病人2可以不等病人1看完直接冲到医生的屋里,让病人1先靠边站,先给病人2看病,病人2看完病接着病人1看病,然后外面排队的病人再进来,这种形式的优先级就是中断嵌套,这种决定是不是可以中断嵌套的优先级,就叫抢占优先级,抢占优先级高的,可以进行中断嵌套。

小结:

中断A抢占优先级比B高,那么A的中断能够在B的中断里面触发,忽略响应优先级;(即中断嵌套)
中断A和中断B抢占优先级同样,则A、B的响应优先级决定谁先响应。

为了将优先级区分为抢占优先级和响应优先级,就需要对这16个优先级优先级进行分组,NVIC的中断优先级由优先级寄存器的4位(0~15,4位二进制,对应16个优先级)决定,优先级的数值越小,优先级越高,0就是最高优先级。这4位可以进行切分,分为高n位的抢占优先级和低4-n位的响应优先级

抢占优先级高的可以中断嵌套,响应优先级高的可以优先排队,抢占优先级和响应优先级均相同的按中断号排队(中断号是中断表的左边数字,数值小的优先响应),所以STM32的中断不存在先来后到的排队方式,在任何时候都是优先级高的先响应。

5.4 EXTI外部中断

5.4.1 EXTI的概念

EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI这个外设硬件电路将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序。(简单说:引脚电平变化,EXTI向NVIC申请中断)。

  • 支持的触发方式(引脚电平的变化类型):
    • 上升沿,即电平从低电平变到高电平的瞬间触发中断
    • 下降沿,即电平从高电平变到低电平的瞬间触发中断
    • 双边沿,即上升沿和下降沿都可以触发中断
    • 软件触发,即程序执行代码就能触发中断
  • 支持的GPIO口(外部中断引脚):
    • 所有GPIO口都能触发中断,但相同的Pin不能同时触发中断(比如PA0和PB0不能同时使用,只能选一个作为中断引脚;所以如果有多个中断引脚要选择不同的pin引脚,比如PA0和PA1、PA9和PB15、PB6和PB7就可以,简单说:就是对于端口PXn,字母X可以相同,但是数字n不能相同)
  • 通道数(总共有20个中断线路):
    • 16个GPIO_Pin(对应引脚GPIO_pin0到15,是外部中断的主要功能),外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒
    • 注意,后面这4个中断线路,是因为外部中断有个功能是从低功耗模式的停止模式下唤醒STM32,那对于PVD电源电压检测,当电源从电压过低恢复时就需要PVD借助一下外部中断,退出停止模式;对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒,这也需要借助外部中断,剩余USB唤醒、以太网唤醒也是类似的作用
  • 触发响应方式
    • 中断响应,即引脚电平触发中断,申请中断,让CPU执行中断函数
    • 事件响应,即不会触发中断,而是触发别的外设(非CPU)操作,属于外设之间的联合工作。外部中断的信号不会通向CPU而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等

5.4.2 EXTI基本结构

外部中断的整体结构图如下:

首先,最左边是GPIO口的外设,每个GPIO外设有16个引脚,所以进来16根线;若每个引脚占用一个通道,那EXTI的16个通道是不够用的,所以在这里会有一个AFIO中断引脚选择的硬件电路模块,这个AFIO就是一个数据选择器

可以将图中前面的3个GPIO外设的16个引脚中的其中一个连接到后面的EXTI通道(16个GPIO通道),所以对于PA0\PB0\PC0这些,通过AFIO选择之后只有其中一个能接到EXTI的通道0上,同理PA1\PB1\PC1这些,也只能有一个连接到通道1上,这就是所有GPIO口都能触发中断,但相同的Pin不能同时触发中断的原因。

然后,通过AFIO选择后的16个通道,就接到了EXTI边沿检测及控制电路上,同时下面这4个“蹭网”的外设(PVD\PTC\USB\ETH)也是并列接进来的,这些加起来就组成了EXTI的20个输入信号,然后经过EXTI电路之后,分为了两种输出,也就是中断响应和事件响应(上面接到了NVIC用来触发中断,下面有20条输出线路输出线路到了其它外设,这就是用来触发其他外设操作的,也就是事件响应)

注意点:本来20路输入,应该有20路中断的输出,可能ST公司觉得20个输出太多了比较占用NVIC的通道资源,所以就把其中的外部中断9~ 5,15~10,给分到了一个通道,EXTI9_5是外部中断的5,6,7,8,9分到了一个通道里,EXTI15_10也是一样;也就是说外部中断的9到5会触发同一个中断函数,15到10也会触发同一个中断函数;在编程的时候,我们在这两个中断函数里,需要再根据标志位区分到底是哪个中断进来的

5.5 AFIO硬件电路结构

5.5.1 AFIO复用IO内部电路

内部电路就是一系列的数据选择器,如图的最上面输入是PA0\PB0\PC0等尾号都是0,然后通过数据选择器最终选择一个,连接到EXTI0上,上面写的文字是说配置这个寄存器的哪一个位就可以决定选择哪一个输入,图中后面部分内容都雷同。

AFIO主要用于引脚复用功能的选择和重定义(也就是数据选择器的作用)。

在STM32中,AFIO主要完成两个任务:复用功能引脚重映射(就是最开始提到的引脚定义表,当想把这些默认复用功能的引脚换到重定义功能时,就是用AFIO来完成的,这也是AFIO的一大主要功能)、中断引脚选择。

5.5.2 EXTI内部电路框图

EXTI的右边就是20根输入线,然后输入线首先进入边沿检测电路,在上面的上升沿寄存器和下降沿寄存器可以选择是上升沿触发还是下降沿触发或者两个都触发。接着硬件触发信号和软件中断寄存器的值就进入到这个或门的输入端(也就是任意一个为1,或门就可以输出1)。

然后触发信号通过这个或门后就兵分两路,上一路是触发中断的(至NVIC中断控制器)下一路是触发事件的(脉冲发生器)

  • 触发中断,首先会置一个挂起寄存器(挂起寄存器相当于一个中断标志位,可以读取这个寄存器判断是哪个通道触发的中断,如果挂起寄存器置1,它就会继续向左走和中断屏蔽寄存器共同进入一个与门(与门实际上就是开关控制作用,中断屏蔽寄存器给1那,那另一个输入就是直接输出,也就是允许中断;中断屏蔽寄存器给0,那另一个输入无论是什么,输出都是0,相当于屏蔽了这个中断),然后是NVIC中断控制器)。
  • 触发事件,首先也是一个事件屏蔽寄存器进行开关控制,最后通过一个脉冲发生器到其它外设(脉冲发生器就是给一个电平脉冲,用来触发其它外设的动作)

5.6 中断和外部中断的参考手册

首先是NVIC,因为NVIC是内核外设,所以要在这个Cortex-M3编程手册里找:

其次是中断相关的介绍(包括AFIO硬件电路)请参考STN32手册的第9章中断和事件。

5.7 EXTI中断示例程序

① 对射式红外传感器计次实验;② 旋转编码器计次实验;

本节先主要学习外部中断读取编码器计次数据的用法,后面学了定时器,还会再来看一下编码器测速的用途。

5.7.1 旋转编码器简介

旋转编码器:用来测量位置、速度或旋转方向的装置,当其旋转轴旋转时,其输出端可以输出与旋转速度和方向对应的方波信号,读取方波信号的频率和相位信息即可得知旋转轴的速度和方向。

如下是我们接下来将要使用的旋转编码器,左边是外观,右边是内部拆解的结构;可以看到内部是用金属触点进行通断的,所以它是一种机械触点式编码器,左右是两部分开关触点;中间银色圆形金素片为一个按键,这个旋转编码器的轴是可以按下去的,这种编码器一般是用来进行调节的,比如音响调节音量,因为它是触点接触的形式,所以不适合电机这种高速旋转的地方,另外三种都是非接触的形式,可以用于电机测速(电机测速在电机驱动的应用中还是很常见的)

在旋转时,依次接通和断开两边的触点;这个金属盘的位置是经过设计的,它能让两侧触点的通断产生一个90度的相位差,最终配合一下外部电路,这个编码器的两个输出就会输出如下这样的正交波形,带正交波形输出的编码器是可以用来测方向的(这就是单相输出和两相正交输出的区别),当然还有的编码器不是输出正交波形,而是一个引脚输出方波信号代表转速,另一个输出高低电平代表旋转方向,这种不是正交输出的编码器也是可以测方向的。

当正转时,A相引脚输出一个方波波形,B相引脚输出一个和它相位相差滞后90的波形(正交波形),如下。

当反向旋转时,A相引脚还是方波信号,B相引脚会提前90度,如下 。

5.7.2 对射式红外传感器计次

编程前的几点说明:

  1. 根据5.4.2 EXTI基本结构小节的第一个图,可知需要配置GPIO外设电路、AFIO外设电路、EXTI外设电路、NVIC外设电路。
  2. NVIC属于内核外设,内核是上电就接通时钟的,因此不需要开启时钟,另外比较特别的是EXTI也不需要开启时钟,但EXTI是片上外设。
  3. ST公司并没有给AFIO单独编写外设驱动文件,二是和GPIO外设写在同一个文件中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int counter = 0;

/**
*@brief 初始化GPIOB的14号引脚,并进行中断设置
*@note 模式为
*/
void PB14_Init(void)
{
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

// 初始化GPIOB的14号引脚
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStruct);

// 配置AFIO的数据选择器,选择某个GPIO口作为外部中断源,也即将PB14引脚连接到外设EXTI线
GPIO_EXTILineConfig(GPIO_PortSourceGPIOB, GPIO_PinSource14);

// 配置外设EXTI
EXTI_InitTypeDef EXTI_InitStruct;
EXTI_InitStruct.EXTI_Line = EXTI_Line14; // 指定配置EXTI线14
EXTI_InitStruct.EXTI_LineCmd = ENABLE; // 使能该EXTI线
EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt; // 设置为中断模式(非事件模式)
EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Falling; // 设置为下降沿触发
EXTI_Init(&EXTI_InitStruct);

// 配置NVIC
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 设置优先级分组为2(2位抢占优先级+2位响应优先级)
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = EXTI15_10_IRQn; // 指定EXTI15_10中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; // 使能该中断通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1; // 设置抢占优先级为1
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 1; // 设置响应优先级为1
NVIC_Init(&NVIC_InitStruct);
}

int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
// 初始化配置
PB14_Init();

while (1)
{
OLED_ShowNum(1, 1, counter, 5);
}
}


// 中断函数
void EXTI15_10_IRQHandler(void)
{
// 这个函数EXTI10~EXTI15都能进来,所以需要判断具体是哪个
if(EXTI_GetITStatus(EXTI_Line14) == SET)
{
counter++;
}

// 中断函数一定要记得清除中断标志位,否则会一直进入中断
EXTI_ClearITPendingBit(EXTI_Line14);
}

程序现象:对射式红外传感器挡光后触发下降沿,这个下降沿触发单片机引脚的外部中断,然后执行数字加1的中断程序。

5.7.3 旋转编码器计次

上面这些内容的主要参考链接:

江科大STM32最全笔记整理『上篇』 - CSDN

6 TIM定时器

6.1 TIM简介

6.1.1 定时器基本概念

TIM(Timer)定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断

16位计数器、预分频器、自动重装寄存器的时基单元,在72MHz计数时钟下可以实现最大59.65s的定时,其中预分频器是指可以对计数器的时钟进行分频,让计数更加灵活。

不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。

根据复杂度和应用场景分为了高级定时器通用定时器基本定时器三种类型 ,如下表所示:

类型 编号 总线 功能
高级定时器 TIM1、TIM8 APB2 拥有通用定时器全部功能,并额外具有重复计数器、死区生成、互补输出、刹车输入等功能
通用定时器 TIM2、TIM3、TIM4、TIM5 APB1 拥有基本定时器全部功能,并额外具有内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等功能。
基本定时器 TIM6、TIM7 APB1 拥有定时中断、主模式触发DAC的功能

本教程使用的芯片STM32F103C8T6定时器资源:TIM1、TIM2、TIM3、TIM4。

不同型号,定时器的数量不一样,操作外设之前要查清楚是否有这个外设。

6.1.2 定时器工作原理

TIM(Timer)定时器是一种计时器,在微控制器中常用于产生精确的定时或延时操作。它的工作原理可以简单分为以下几个步骤:

  1. 时钟源选择:定时器通常由微控制器的时钟源提供时钟信号。时钟源可以是内部的时钟信号或外部的时钟信号,根据应用需求选择不同的时钟源。

  2. 计数器:TIM定时器内部有一个计数器,用来记录经过的时钟周期数。计数器会根据时钟信号的频率不断增加。

  3. 预设值设置:在初始化时,需要设定一个预设值来决定定时器的计数周期,也就是定时器溢出前的计数次数。一旦计数器累计到预设值,定时器会产生一个定时中断。

  4. 中断处理:当定时器溢出时,会触发中断请求,通知微控制器进行相应的处理。在中断服务程序中,可以执行特定的操作,比如更新某些变量、切换任务、生成PWM波形等。

  5. 重载:在中断服务程序中可以重新加载预设值,使定时器不断工作,实现循环定时功能。

总的来说,TIM定时器通过不断累计时钟周期来实现计时功能,并在计数达到预设值时产生中断,从而实现精确的定时功能。

6.1.3 主从工作模式说明

STM32 通用定时器(TIMx)中的主模式(Master Mode)和从模式(Slave Mode)这两个概念是理解定时器之间、定时器与其他外设(如 DMA、ADC、DAC)协同工作的关键。

核心思想:主从模式解决的定时器内部或之间的同步、触发问题。

  • 主模式(Master Mode): 当一个定时器配置为主模式时,它有能力生成一个触发输出信号(TRGO)。这个信号可以来源于定时器内部发生的特定事件(如更新事件、捕获/比较事件等)。这个 TRGO 信号可以被用来触发其他外设(如另一个定时器、DMA、ADC、DAC) 执行某个动作。
  • 从模式(Slave Mode): 当一个定时器配置为从模式时,它能够接收一个触发输入信号(TIx, ETR, ITRx)。这个信号通常是来自另一个定时器主模式的 TRGO 信号,或者是来自外部引脚(如 TI1, TI2, ETR)的信号。从模式定时器会根据配置,将这个输入信号解释为特定的命令(如启动、停止、复位、计数时钟等),从而控制自身的计数器行为。

一个经典应用实例:主从定时器同步生成 PWM (相位对齐)

  1. 主定时器 (TIM1 - 主模式)

    配置为 PWM 模式。
    配置主模式:TIM1_CR2.MMS = 010 (选择 Update 作为 TRGO 源)。这样,每当 TIM1 计数器溢出/更新时(即一个 PWM 周期结束),它就会在 TRGO 上输出一个脉冲。

  2. 从定时器 (TIM2 - 从模式)

    配置为 PWM 模式(频率和占空比参数独立于 TIM1 设置)。
    配置从模式:

    • TIM2_SMCR.TS = 000 (选择ITR0,假设 TIM2 的 ITR0 连接 TIM1 的 TRGO)。
    • TIM2_SMCR.SMS = 100 (选择 Reset Mode)。
  3. 工作过程:

    • 启动 TIM1 和 TIM2。
    • TIM1 开始计数并输出 PWM。
    • 当 TIM1 计数到 ARR 产生更新事件时,它的 TRGO 输出有效(一个脉冲)。
    • TIM2 通过 ITR0 检测到这个 TRGI 脉冲。
    • 由于 TIM2 工作在复位模式,这个 TRGI 脉冲会立即复位 TIM2 的计数器(CNT=0)。
    • 复位后,TIM2 在其下一个有效时钟边沿开始从 0 重新计数并输出 PWM。
  4. 效果:

    • TIM2 的 PWM 输出周期起始边界(0 点)被 TIM1 的更新事件(即 TIM1 PWM 周期的结束/也是下一个周期的开始)严格对齐。
    • 无论 TIM2 的 ARR 值是多少,它的 PWM 波形总是与 TIM1 的 PWM 波形同步开始,实现了精确的相位对齐。这对于多相电机控制(如三相逆变器中需要 120 度相位差的 PWM)或需要多个严格同步信号的应用至关重要。

6.2 定时器结构

6.2.1 基本定时器结构

  • 时基单元

    可编程通用定时器的主要部分是一个16位计数器和与其相关的自动装载寄存器。这个计数器可以向上计数、向下计数或者向上向下双向计数。此计数器时钟由预分频器分频得到。计数器、自动装载寄存器和预分频器寄存器可以由软件读写,在计数器运行时仍可以读写。
    时基单元包含:

    • 预分频器寄存器 (TIMx_PSC)
      • 预分频器描述:预分频器可以将计数器的时钟频率按1到65536之间的任意值分频。它是基于一个(在TIMx_PSC寄存器中的)16位寄存器控制的16位计数器。这个控制寄存器带有缓冲器,它能够在工作时被改变。新的预分频器参数在下一次更新事件到来时被采用。
    • 计数器寄存器(TIMx_CNT)
      • 计数器由预分频器的时钟输出CK_CNT驱动,仅当设置了计数器TIMx_CR1寄存器中的计数器使能位(CEN)时, CK_CNT才有效。 (有关计数器使能的细节,请参见控制器的从模式描述)。
      • 对预分频后的计数时钟进行计数,计数时钟每来一个上升沿,计数器的值就加1,这个计数器也是16位的,所以里面的值可以从0一直加到65535。如果再加,计数器就会回到0重新开始,所以计数器的值会不停的自增运行,当自增到目标值时,产生中断,就完成了定时任务。现在还需要一个存储目标值的寄存器,那就是自动重装载器。
      • 注:真正的计数器使能信号CNT_EN是在CEN的一个时钟周期后被设置。
    • 自动装载寄存器 (TIMx_ARR)
      • 自动重装载寄存器也是十六位的,它存的就是我们写入的计数目标,在运行的过程中,计数器不断的自增,自动重装值是固定的目标,当计数值等于自动重装载值时,计时时间就开始。那它就会产生一个中断信号,并且清零计数器,计数器自动开始下一次的计数计时。
      • 自动装载寄存器是预先装载的,写或读自动重装载寄存器将访问预装载寄存器。根据在TIMx_CR1寄存器中的自动装载预装载使能位(ARPE)的设置,预装载寄存器的内容被立即或在每次的更新事件UEV时传送到影子寄存器。当计数器达到溢出条件(向下计数时的下溢条件)并当TIMx_CR1寄存器中的UDIS位等于’0’时,产生更新事件。更新事件也可以由软件产生。
  • 计数模式

    • 向上计数模式(掌握)
      • 在向上计数模式中,计数器从0计数到自动加载值(TIMx_ARR计数器的内容),然后重新从0开始计数并且产生一个计数器溢出事件。(基本定时器仅支持这一种定时器模式,通用定时器和高级定时器支持则三种模式)
    • 向下计数模式 (了解)
      • 在向下计数模式中,计数器自动装入的值(TIMx_ARR计数器的值)开始向下计数到0,然后从自动装入的值重新开始并且产生一个计数器向下溢出事件。
    • 中央对齐模式(向上/向下计数)(了解)
      • 在中央对齐模式,计数器从0开始计数到自动加载的值(TIMx_ARR寄存器),产生一个计数器溢出事件,然后向下计数到1并且产生一个计数器下溢事件;然后再从0开始重新计数。
  • 主模式触发DAC功能

    • 用途是在我们使用DAC的时候,可能会用DAC输出一段波形,那就需要每隔一段时间来触发一次DAC,让它输出下一个电压点。如果用正常的思路来实现的话,就是先设置一个定时器产生中断,每隔一段时间在中断程序中调用代码手动触发一次DAC转换,然后DAC输出。这样也是没问题的,但是这样会使主程序处于频繁被中断的状态
    • 所以定时器就设计了一个主模式,使用这个主模式可以把这个定时器的更新事件映射到这个触发输出TRGO(Trigger Out)的位置,然后TRGO直接接到DAC的触发转换引脚上。这样,定时器的更新就不需要再通过中断来触发DAC转换了,仅需要把更新事件通过主模式映射到TRGO,然后TRGO就会直接去触发DAC了。整过程不需要软件的参与,实现了硬件的自动化,这就是主模武的作用。

6.2.2 通用定时器结构

时钟输入部分

时钟输入部分详解:

  • 内部时钟:来自RCC的TIMxCLK
  • 外部时钟
    • 外部时钟模式1
    • 外部时钟模式2

ETR引脚位置可参考引脚定义表:

  • 输出比较电路:位于右下角部分
    • 输出比较电路,总共有四个通道,分别对应CH1到CH4的引脚,可以用于输出PWM波形,驱动电机。
  • 输入捕获电路:位于左下角部分
    • 输入捕获电路,也是有四个通道,分别对应CH1到CH4的引脚,可以用于测输入方波的频率等。

中间这个寄存器捕获/比较寄存器,是输入捕获和输出比较电路共用的。因为输入捕获和输出比较不能同时使用,所以该寄存器是共用的,引脚也是共用的

6.2.3 高级定时器结构

与通用定时器的区别:

  1. 在计数时间到,申请中断的地方,增加了一个重复次数计数器(REP寄存器)。有了这个计数器之后,就可以实现每隔几个计数周期,才发生一次更新事件和更新中断。
  2. DTG(Dead Time Generate)是死区生成电路,右边这里的输出引脚由原来的一个,变为了两个互补的输出,可以输出一对互补的PWM波。这些电路是为了驱动三相无刷电机的,三相无刷电机还是比较常用的。比如四轴飞行器、电动车的后轮、电钻等,里面都可能是这个三相无刷电机。因为三相无刷电机的驱动电路一般需要3个桥臂,每个桥臂2个大功率开关管来控制,总共需要6个大功率开关管来控制,所以这里的输出PWM引脚的前三路就变为了互补的输出,而第四路却没什么变化,因为三相电机只需要三路就行了。

为了防止互补输出的PWM驱动杯臂时,在开关切换的瞬间,由于器件的不理想,造成短暂的直通现象,所以前面这里就加上了死区生成电路,在开关切换的瞬间,产生一定时长的死区,让桥臂的上下管全都关断,防止直通现象。

  1. 刹车输入功能,这个是为了给电机驱动提供安全保障的,如果外部引脚TIMx_BKIN产生了刹车信号,或者内部时钟失效,产生了故障,那么控制电路就会自动切断电机的输出,防止意外的发生。

6.2.4 定时中断基本结构

定时器这一部分,建议结合铁头山羊的定时器教学视频,我感觉他讲的挺好的,电路的结构框图也绘制的很清晰明了。

6.3 定时器时序分析

6.3.1 预分频器时序

预分频缓存器也是靠计数器来分频的,当预分频值为0时,预分频计数器也一直为0,直接输出原频率,当预分频值为1时,计数器才01010101这样计数,在回到0的时候输出一个脉冲,这样输出频率就是输入频率的二分频。预分频器的值与实际分频系数之间就会有一个值的偏移。

6.3.2 计数器时序

注意,自动重装寄存器ARR是有影子寄存器的,可以设置是否使用这个影子寄存器,如果设置ARPE=1,则使用影子寄存器,若设置ARPE=0,则不使用影子寄存器。

6.4 RCC时钟树

关于时钟树,江科大只是在[6-1 TIM定时中断]结尾8分钟提了一下,这里建议观看铁头山羊的时钟树教程,讲解了两个小节。

RCC是Reset and Clock Control (复位和时钟控制)的缩写,它是STM32内部的一个重要外设,负责管理各种时钟源和时钟分频,以及为各个外设提供时钟使能。RCC模块可以通过寄存器操作或者库函数来配置。

参考资料1:【STM32】外设-RCC_stm32 rcc-CSDN博客
参考资料2:【STM32】系统时钟RCC详解(超详细,超全面)_rcc时钟-CSDN博客

RCC(Reset and Clock Control)模块是嵌入式系统中常见的一个模块,用于控制系统的复位和时钟。时钟树则是指系统中所有时钟信号的传输路径组成的结构。在嵌入式系统设计中,对于时钟树的设计和优化是非常重要的,可以影响系统的性能和功耗。

这个时钟树,就是STM32中用来产生和配置时钟,并且把配置好的时钟发送到各个外设的系统,时钟是所有外设运行的基础,所以时钟也是最先需要配置的东西,我们之前说过,程序中主函数之前还会执行一个SystemInit函数,这个函数就是用来配置这个时钟树的,ST公司已经帮我们编写好了配置这个时钟树的SvstemInit函数,当然我们也可以进去修改,修改时钟信号。

RCC模块通常会控制系统中各个模块的时钟信号,包括CPU、外设和其他核心组件。它可以配置时钟源、时钟分频和时钟输出等参数,确保系统各个模块之间的时钟信号能够同步和协调工作。

在设计时钟树时,需要考虑以下几个方面:

选择合适的时钟源:时钟源的质量和稳定性会直接影响系统的性能和稳定性。常见的时钟源包括晶体振荡器、PLL(Phase-Locked Loop)等。

时钟分频和时钟树结构:通过合理的时钟分频和时钟树结构设计,可以减少时钟信号的传输延迟和功耗,提高系统的性能和效率。

时钟信号的布线和阻抗匹配:布线过程中需要考虑时钟信号的传输路径长度、走线方式和阻抗匹配,确保时钟信号的传输质量和稳定性。

6.5 定时器实验1

使用定时器,最核心的流程就是根据“6.2.4 定时中断基本结构”这一小节的两个定时器结构框图,把其中的每一个硬件电路外设都配置好就能使用了。

  1. RCC开启时钟,这个基本上每个代码都是第一步。
  2. 选择时基单元的时钟源,比如,对于一般定时中断,我们就选择内部时钟源。
  3. 配置时基单元、设置运行控制:包括这里的预分频器、自动重装器、计数模式,控制向上/下计数等等,程序中用一个结构体就可以配置。
  4. 配置输出中断控制,允许更新中断输出到NVIC。
  5. 配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级。
  6. 使能定时器,要不然定时器是不会运行的。

stm32f10x_tim.h中是STM公司提供的“海量”定时器函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 定时器复位函数
void TIM_DeInit(TIM_TypeDef* TIMx);

// 时基单元初始化函数
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

// 将结构体赋默认值
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

// 使能计数器
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);

// 使能中断输出信号;
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

// 用来单独写预分频值
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);

// 改变计数器的计数模式
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);

// 自动重装器预装功能配置,寄存器有无与装置就用这个函数
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);

// 给计数器写入一个值
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);

// 给自动重装器写入一个值
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

// 获取当前计数器的值
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);

// 获取当前预分频器的值
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);

// ================================================================
// 六个时钟源函数
// ================================================================

// 选择内部时钟
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);

// 选择ITRx其他定时器的时钟
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);

// 选择TIx捕获通道的时钟
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx,
uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity,
uint16_t ICFilter);

// 选择ETR通过外部时钟模式1输入的时钟
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx,
uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);

// 选择ETR通过外部时钟模式2输入的时钟
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx,
uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);

// 单独配置ETR引脚预分频器,极性,滤波器等参数
void TIM_ETRConfig(TIM_TypeDef* TIMx,
uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity,
uint16_t ExtTRGFilter);

6.5.1 定时器定时中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include "Timer.h"

void Timer_Init(void)
{
// 第一步:开启定时器这个外设的时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

// 第二步:选择时基单元的时钟
// 不过这个选择时钟的函数,很多人的代码都没有写,因为定时器上电后默认就是使用内部时钟
TIM_InternalClockConfig(TIM2); // 选择内部时钟RCC

// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 10000-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 7200-1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

TIM_ClearFlag(TIM2, TIM_FLAG_Update);

// 使能TIM中断
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);

// 配置NVIV
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0;
NVIC_Init(&NVIC_InitStruct);

// 使能定时器
TIM_Cmd(TIM2, ENABLE);
}

// TIM2的中断函数
void TIM2_IRQHandler(void)
{
// 检测是否是更新中断产生的中断
if(TIM_GetITStatus(TIM2, TIM_IT_Update) == 0)
{
NUM++;
// 清除标志位
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Timer.h"

int NUM = 0;

int main()
{
OLED_Init();
while(1)
{
OLED_ShowNum(1, 1, NUM, 5);

OLED_ShowNum(1, 1, TIM_GetCounter(TIM2), 5);
}
}

6.5.2 定时器外部时钟

6.6 定时器输出比较

输出比较可以通过比较CNT与CCR(捕获/比较寄存器)值的关系,来对输出电平进行置1、置0或翻转的操作,用于输出一定频率和占空比的PWM波形。

• 每个高级定时器和通用定时器都拥有4个输出比较通道
• 高级定时器的前3个通道额外拥有死区生成和互补输出的功能

每个高级定时器和通用定时器都拥有4个输出比较通道,可以同时输出四路PWM波形,这四路PWM波形的频率相同,但是占空比可以不同

CCR寄存器有预装功能,即存在影子寄存器。

6.6.1 PWM

(一) PWM简介

  • PWM(Pulse Width Modulation)脉冲宽度调制
  • 在具有惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要的模拟参量,常应用于电机控速等领域
  • PWM参数:频率 = 1 / T_s,占空比 = T_{on} / T_s,分辨率 = 占空比变化步距

(二) PWM基本结构

参数计算公式:

  • PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1)
    • 从上图中的波形可以看出,PWM的频率就等于计数器的更新频率,与AAR寄存器相关
  • PWM占空比: Duty = CCR / (ARR + 1)
  • PWM分辨率: Reso = 1 / (ARR + 1)

举例:产生频率为1KHz,占空比为50%,分辨率为1% 的PWM波形

PWM频率: Freq = CK_PSC / (PSC + 1) / (ARR + 1) =72000000 / 720 / 100 = 1000
PWM占空比: Duty = CCR / (ARR + 1) 50 / 100 = 50%
PWM分辨率: Reso = 1 / (ARR + 1) 1 / 100 = 1%

6.6.2 定时器如何输出PWM

下面简单说一下定时器的输出比较模块是怎么来输出PWM波形的。

(一) 输出比较通道(通用)

CNT计数器和CCR1(第一路的捕获比较寄存器)进行比较,当CNT>=CCR1时,就会给输出模式控制器传一个信号,然后输出模式控制器就会改变它输出OC1REF的高低电平(REF信号实际上就是指这里信号的高低电平)。

上面的ERTF输入,是定时器的一个小功能,暂时不用了解。

然后,接着这个REF信号既可以前往主模式控制器(把这个REF映射到主模式的TRGO输出上去),又可以通过下面一路到达极性选择,给这个寄存器写0,信号就会往上走,就是信号电平不翻转,写1就会往下走,信号通过一个非门取反,那输出的信号就是输入信号高低电平反转的信号,就是选择是不是要把高低电平反转一下,OC1引脚,就是CH1通道的引脚。

最后是输出使能电路,选择是否要把PWM通过OC1引脚输出给外界使用,在引脚定义表里就可以知道具体是哪个GPIO口了。

下面为输出模式控制器可配置的模式,输出模式控制器控制什么时候给OC1REF高电平,什么时候给OC1REF低电平。

模式 描述
冻结 CNT=CCR时,REF保持为原状态
匹配时置有效电平 CNT=CCR时,REF置有效(高)电平
匹配时置无效电平 CNT=CCR时,REF置无效(低)电平
匹配时电平翻转 CNT=CCR时,REF电平翻转
强制为无效电平 CNT与CCR无效,REF强制为无效电平
强制为有效电平 CNT与CCR无效,REF强制为有效电平
PWM模式1 向上计数:CNT向下计数:CNT>CCR时,REF置无效电平,CNT≤CCR时,REF置有效电平
PWM模式2 向上计数:CNT向下计数:CNT>CCR时,REF置有效电平,CNT≤CCR时,REF置无效电平

6.7 定时器实验2

根据6.6.1节的(二) PWM基本结构的框图,可总结要定时器工作在输出比较(PWM)模式下的步骤:

  • 第一步:RCC开启时钟,把我们要用的TIM外设和GPIO外设的时钟打开。
  • 第二步:配置时基单元,包括这前面的时钟源选择和时基单元,都配置好。
  • 第三步,配置输出比较单元,包括CCR的值、输出模式控制器、极性选择、输出使能这些参数。
  • 第四步:初始化GPIO,定时器通过GPIO输出PWM波,推荐把GPIO初始化为复用推挽输出模式。
  • 第五步:使能定时器,要不然定时器是不会运行的。

6.7.1 相关的硬件简介

(一) 舵机

舵机是一种根据输入PWM信号占空比来控制输出角度的装置,

PS:其实,这里PWM很像一个通信协议,发送给舵机一个PWM波,舵机就能根据PWM波形的占空比调整旋转角度,类似于一个通信信息。

(二) 直流电机和驱动

直流电机是一种将电能转换为机械能的装置,有两个电极,当电极正接时,电机正转,当电极反接时,电机反转

  • 直流电机属于大功率器件,GPIO口无法直接驱动,需要配合电机驱动电路来操作
    • TB6612是一款双路H桥型的直流电机驱动芯片,可以驱动两个直流电机并且控制其转速和方向。

  • VM引脚:用于驱动电机,其电压约为4.5V-10V,建议与电机额定电压一致。
  • VCC引脚:是逻辑电平输入端,范围是2.7V-5.5V,这个要和我们控制器的电源保持一致,比如你使用STM32,是3.3V的器件,那就接3.3V。
  • AO1、AO2、BO1、B2:是两路电机的输出,可以像图中那样分别接两个电机
    • AO1和AO2就是A路的两个输出,它的控制端就是上面的这三个PWMA、AIN2和AIN1,这三个引脚控制下面A路的一个电机,PWMA、AIN2和AIN1这三个端口直接连接STM32的GPIO端口,用于输出相应的控制信号。
    • BO1和BO2就是B路的两个输出,它的控制端就是上面的这三个PWMB、BIN2和BIN1,这三个引脚控制下面A路的一个电机,PWMB、BIN2和BIN1这三个端口直接连接STM32的GPIO端口,用于输出相应的控制信号。
  • PWMA、PWMB引脚要接定时器的PWM信号输出端,
  • 其它两个引脚可以任意接两个普通的GPIO口,控制电机的转动方向

PWM、IN1、IN2这三个引脚给一个低功率的控制信号,驱动电路就会从VM汲取电流,来输出到电机,这样就能完成低功率的控制信号控制大功率设备的目的了。

6.7.2 PWM驱动呼吸灯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Delay.h"


/**
*@brief 定时器初始化
*@note 定时器工作在输出比较模式
*/
void PWM_Init(void)
{
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIO端口

// 选择TIM的时钟源(使用内部时钟的时候可不写)
TIM_InternalClockConfig(TIM2); // 选择内部时钟RCC

// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 100-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

// 配置输出比较模块
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 50;
TIM_OC1Init(TIM2, &TIM_OCInitStruct);

// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽,因为这时候不是CPU控制而是定时器控制,因此用复用
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能TIM
TIM_Cmd(TIM2, ENABLE);
}


int main()
{
OLED_Init();
PWM_Init();

while(1)
{
for(int i = 0; i <= 100; i++)
{
// 修改CCR的数值
TIM_SetCompare1(TIM2, i);
Delay_ms(10);
}
for(int i = 100; i >= 0; i--)
{
// 修改CCR的数值
TIM_SetCompare1(TIM2, i);
Delay_ms(10);
}
}
}

6.7.3 插入—AFIO引脚重映射

从引脚定义表里看到:TIM2的CH1可以从PA0挪到PA15号脚,所以可以使用AFIO做引脚重映射。进行重映射的关键函数是void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState),即引脚重映射配置函数。

1
2
3
4
5
6
7
8
9
10
// 第一步:开启AFIO的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

// 第二步:进入 stm32f10x_gpio.h 文件,找到GPIO_PinRemapConfig()函数
// 根据手册提醒,选择重映射方式作为函数的参数:部分重映射、完全重映射
GPIO_PinRemapConfig(GPIO_PartialRemap1_TIM2, ENALE); // PA0 --> PA15

// 但是PA15号引脚上电后默认复用为了调试端口JTDI,所以如果想让他作为普通的GPIO或者复用定时器的通道,
// 那还需要先关闭调试端口的复用,通用使用GPIO_PinRemapConfig()函数来关闭
GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable, ENALE); // 关闭PA15的JTAG调试端口

6.7.4 PWM驱动舵机

6.7.5 PWM驱动直流电机

6.8 定时器的输入捕获

6.8.1 基本概念

IC(Input Capture)输入捕获

输入捕获模式下,当通道输入引脚出现指定电平跳变时,当前CNT的值将被锁存到CCR中,可用于测量PWM波形的频率、占空比、脉冲间隔、电平持续时间等参数。

• 每个高级定时器和通用定时器都拥有4个输入捕获通道
• 可配置为PWMI模式(PWM的输入模式),同时测量频率和占空比
• 可配合主从触发模式,实现硬件全自动测量

  • Tips:
    • 计数器(CNT)和重装载器(ARR)为16位,数值max = 65535
    • 同一个定时器内输入捕获和输出比较功能不能同时使用,且一个定时器只能使用一次输入捕获/输出比较功能(多少个输入捕获/输出比较功能就用多少个定时器)。输入捕获和输出比较共用一个CCR(捕获/比较寄存器),同一个通道中不能同时使用输入捕获和输出比较;且同一个定时器中TIMx_CH1~TIMx_CH4也是共用一个引脚;
    • 滤波器属于硬件电路,用于稳定电路,去除信号毛刺滤波,不影响待测信号频率,N越大,滤波效果越好,但太大了也有可能在测量时会漏掉一小部分信号;
    • 每次捕获完毕后,都需要清0计数器,以便在下一周期的捕获中重新计数(避免数据不准确);
    • 输入的PWM信号只有输入通道1和通道2才能实现硬件全自动运行(硬件自动清0计数器)。TIxFP信号中,只有TI1FP1和TI1FP2被作为触发输入信号,才能将从模式控制器被配置成复位模式(硬件自动清0计数器)。
    • 交叉连接:灵活切换后续捕获电路的输入和同一个引脚通道的输入信号同时映射到两个捕获单元(可用于PWMI模式)。
    • 若使用串口(USART/UART)发送时,由于串口发送需要一定时间,建议频率不要设置太高,避免两次捕获间隔时间太短导致串口不能及时发送数据(有的数据可能会漏发)

6.8.2 频率测量方法

6.8.3 输入捕获过程

(一) 过程分析

  1. 输入信号从其中一个通道输入,旁边的异或门主要是为三相无刷电机准备的;

  2. 输入滤波器:用于过滤不稳定信号,去除信号中的毛刺信号,这是硬件电路上的操作,只是用软件设置滤波大小,并不会影响信号频率;

  3. 边沿检测器:检测指定信号电平跳变(软件设置上升沿/下降沿/双边沿)即可触发捕获并进入后续电路(和中断相似);

  4. TI1FP1~TI4FP4:代表时钟信号分别流入IC1~IC4;其中通道1和2、通道3和4是一对可相互交叉输入时钟信号的通道(软件设置直接连接/交叉连接);以TI1为例:(一个通道输入的时钟信号可以进入另一个通道的后续电路/一个通道输入的时钟信号可以同时进入2个对应通道的后续电路,实现时钟信号多通道输入);通道3和4同理;选择TI1FP1的有效极性(软件设置上升沿/下降沿/双边沿),用于边沿检测;

    只有TI1FP1和TI2FP2连到了从模式控制器,(因此,在PWM输入模式中只有TI1FP1和TI2FP2可作为触发输入信号,而从模式控制器被配置成复位模式,实现在输入捕获后硬件自动清0计数器CNT,进行下一个捕获计数周期);通道3和通道4要在每次捕获后清0计数器,则需要在中断程序中软件清0;

  5. 经过预分频器后可自动触发捕获中断(如果设置了)和事件;

  6. 经过前面的电路,此时可将CNT计数器的值锁存在捕获/比较寄存器(CCR)中。

注:在边沿检测未被触发时计数器一直以一个恒定频率计数(以此测试一个信号周期的时长 [f = 1/T]),直到发生指定跳变引起边沿检测的触发,CNT值被锁存到CCR,并清0计数器CNT以进入下一个信号周期的检测,在信号输入期间一直循环这个过程(连续检测信号周期,CCR为最近一次捕获完成后的CNT的值)。

(二) 输入捕获通道结构框图

(三) 主从触发模式

  • 主模式:可以将定时器内部的信号映射到TRGO引脚,触发其他外设。
  • 从模式:接收其他外设或自身外设的信号,执行自身定时器的运行,被别的信号所控制。、
  • 触发源选择:选择从模式信号触发源的,选择指定的信号,得到TRGI,触发从模式,选择指定操作。
    • PS:可以看到,触发源只有只有TI1FP1TI2FP2,没有T13和T14的信号,所以如果想使用从模式自动清零CNT,就只能用通道1和通道2,对于通道3和通道4,就只能开启捕获中断,在中断里手动清零了。

在库函数里也非常简单,这三块东西就对应三个函数,调用函数,给个参数,就行了。详细介绍查看手册。

6.8.4 输入捕获基本结构

上面这个结构框图只使用了一个通道,因此只能用于测量频率。

  • 时基单元配置好,启动定时器,CNT就会在预分频之后的这个时钟驱动下,不断自增,(CNT:测周法用来计数的东西)。经过预分频之后的时钟频率,就是驱动CNT的标准频率fc(标准频率 = 72M / 预分频系数)。
  • 之后,下面输入捕获通道1 的GPIO后,输入一个如图的方波信号,经过滤波器和边沿检测,设置TI1FP1为上升沿触发,之后选择直连的通道,分频器为不分频,当TI1FP1出现上升沿之后,CNT的当前计数值转运到CCR1里。
  • 同时触发源选择,选中TI1FP1为触发信号,从模式被设置为了复位操作这样TI1FP1的上升沿就会沿着上面的这一路去触发CNT清零(当然是先转运CNT的值到CCR中,在触发从模式给CNT清零,或者是非阻塞的同时转移,CNT的值转移到CCR,同时0转移到CNT里面去,总之,不会是先清零,再捕获,要不捕获的肯定是0)
  • 电路工作时,CCR1的值始终保持为最新一个周期的计数值(N),计算频率只需要fc / N
  • ARR最大为65535,CNT最大65535

6.8.5 PWMI基本结构

如下图所示,这个PWMI模式,使用了两个通道同时捕获一个引脚,能够分别测量周期和占空比:

  • 首先,TI1FP1,配置上升沿触发,用于触发捕获和清零CNT;
  • 其次,TI1FP2,配置为下降沿触发,通过交叉通道去触发通道2的捕获单元;

  • 使用两个通道来捕获频率和占空比

    • CCR1:一整个周期的计数值
    • CCR2:高电平期间的计数值
    • 占空比:CCR2 / CCR1

根据左上角的信号图,简单描述一下流程:

最开始产生上升沿,TI1FP1所在的通道CCR1捕获,同时清零CNT,之后CNT一直++,然后,在下降沿时刻,TI1FP2所在的通道触发CCR2捕获,所以这时CCR2的值,就是高电平期间的计数值,CCR2捕获,并不触发CNT清零,所以CNT继续++,直到下一次上升沿,CCR1捕获周期,CNT清零。

这样执行后,CCR1是一整个周期的计数值,CCR2是高电平期间的计数值,用CCR2/CCR1就是占空比了。

6.9 定时器实验3

根据6.8.4 输入捕获基本结构和 6.8.5 PWMI基本结构的框图,可总结要定时器工作在输出比较(PWM)模式下的步骤:

  • 第一步,RCC开启时钟,把GPIO和TIM的时钟打开
  • 第二步,GPIO初始化,把GPIO配置成输入模式,一般选择上拉输入或者浮空输入模式
  • 第三步,配置时基单元,让CNT计数器在内部时钟的驱动下自增运行
  • 第四步,配置输入捕获单元,包括滤波器、极性、直连通道还是交又通道、分频器这些参数
  • 第五步,选择从模式的触发源,触发源选择为TI1FP1,这里调用一个库函数,给一个参数就行了
  • 第六步,选择触发之后执行的操作,执行Reset操作,这里也是调用一个库函数

涉及的库函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);

void TIM_PWMIConfig(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);

void TIM_ICStructInit(TIM_ICInitTypeDef* TIM_ICInitStruct);

/**
*@brief 选择输入触发源TRGI
*@note 从模式的触发源选择
*/
void TIM_SelectInputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);

/**
*@brief 选择输出触发源TRGO
*@note 主模式的触发源选择
*/
void TIM_SelectOutputTrigger(TIM_TypeDef* TIMx, uint16_t TIM_TRGOSource);

/**
*@brief 下面四个函数分别单独配置通道1、2、3、4的分频器
*/
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);

/**
*@brief 下面四个函数分别读取4个通道的CCR寄存器的值
*/
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);

6.9.1 输入捕获模式测频率(测周法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
#include "stm32f10x.h"                  // Device header
#include "OLED.h"
#include "Delay.h"


/**
*@brief 定时器2初始化
*@note 定时器2工作在输出比较模式,在此主要是模拟一个PWM波供输入捕获测量
*/
void PWM_Init(void)
{
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIO端口

// 选择TIM的时钟源(使用内部时钟的时候可不写)
TIM_InternalClockConfig(TIM2); // 选择内部时钟RCC

// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 100-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 720-1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

// 配置输出比较模块
TIM_OCInitTypeDef TIM_OCInitStruct;
TIM_OCStructInit(&TIM_OCInitStruct);
TIM_OCInitStruct.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStruct.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OCInitStruct.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStruct.TIM_Pulse = 50;
TIM_OC1Init(TIM2, &TIM_OCInitStruct);

// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽,因为这时候不是CPU控制而是定时器控制,因此用复用
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能TIM
TIM_Cmd(TIM2, ENABLE);
}


/**
*@brief 定时器3初始化
*@note 定时器3工作在输入捕获模式,使用TIM3的CH1,也即对应PA6
*/
void IC_Init(void)
{
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIO端口

// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU; // 配置为输入上拉模式
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 选择TIM的时钟源(使用内部时钟的时候可不写)
TIM_InternalClockConfig(TIM3); // 选择内部时钟RCC

// 初始化定时器:配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 72-1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStruct);

// 配置定时器的输入捕获模块
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStruct.TIM_ICPrescaler = TIM_ICPSC_DIV1;
// 选择触发信号从哪个引脚输入, 也就是配置CC1S寄存器的[0,1]两位
TIM_ICInitStruct.TIM_ICSelection = TIM_ICSelection_DirectTI; // 这里选择直连通道
TIM_ICInit(TIM3, &TIM_ICInitStruct);

// 配置从模式的触发源
TIM_SelectInputTrigger(TIM3, TIM_TS_TI1FP1);

// 配置从模式为Reset,清除CNT计数
TIM_SelectSlaveMode(TIM3, TIM_SlaveMode_Reset);

// 使能定时器
TIM_Cmd(TIM3, ENABLE);
}


uint32_t IC_GetFreq(void)
{
// 读取定时器3的通道1的CCR寄存器的数值
uint16_t ccr_num = TIM_GetCapture1(TIM3);
return 1000000 / (ccr_num + 1);
}


int main()
{
OLED_Init();
PWM_Init();
IC_Init();

OLED_ShowString(1, 1, "Freq = ");
OLED_ShowString(1, 13, "Hz");

while(1)
{
OLED_ShowNum(1, 8, IC_GetFreq(), 5);
}
}

6.9.2 PWMI模式测频率占空比

6.10 TIM编码器接口

与前面讲到的旋转编码器外部中断计次不同,这个主要用到的是TIM的编码器接口来自动计次(因为编码器接口是ST公司专门在定时器内集成的一个硬件电路,所以使用编码器接口的好处就是节约软件资源,如果使用外部中断来计次,例如计次电机旋转时,电机高速旋转时,每秒钟产生成千上万个脉冲,程序就得频繁进中断,导致软件资源容易被占用),所以用到编码器接口计次。

编码器测速的运用场景一般用在电机控制的项目上,使用PWM驱动电机,再使用编码器测量电机的速度,然后再用PID算法进行闭环控制。

6.10.1 编码器接口(Encoder Interface)介绍

  1. 编码器接口可接收增量(正交)编码器的信号,根据编码器旋转产生的正交信号脉冲,自动控制CNT自增或自减,从而指示编码器的位置、旋转方向和旋转速度。
  2. 每个高级定时器和通用定时器都拥有1个编码器接口,整个F1只有四个编码器接口资源,且定时器用于做编码器接口后就做不了其他的事情了
  3. 两个输入引脚只能借用输入捕获的通道1和通道2(CH1和CH2引脚) ,CH3和CH4不具有编码器功能。

正交编码器一般可以测量位置,或者带有方向的速度值,它一般有两个信号输出引脚,一个是A相,一个是B相

当编码器的旋转轴转起来时,A相和B相会输出上图所示的方波信号,转的越快,这个方波的频率就越高,所以方波的频率就代表了速度,取出任意一相的信号来测频率就能知道旋转速度了。但是只有一相的信号,无法测量旋转方向,这就是为什么使用A、B两相方波信号了。当正转时,A相提前B相90度,反转时,A相滞后B相90度。

编码器接口的设计逻辑就是:首先把A相和B相的所有边沿作为计数器的计数时钟,出现边沿信号时,就计数自增或自减。那增还是减呢?这个计数的方向由另一相的状态来确定。当出现某个边沿时,我们判断另一相的高低电平,如果对应另一相的状态出现在上面这个表里,那就是正转,计数自增;反之出现在下面表里就是反转,计数自减。这样就能实现编码器接口功能,这也是STM32定时器编码器接口的执行逻辑。

编码器接口在定时器当中的位置:

从上图可以看出,编码器接口的两个引脚TI1FP1TI2FP2借用了输入捕获单元的前两个通道(CH1和CH2),所以最终编码器的输入引脚就是定时器的CH1和CH2这两个引脚,CH3和CH4与编码器接口无关。

编码器接口的输出部分,本质上就相当示从模式控制器了,去控制CNT的计数时钟和计数方向。此时计数时钟和计数方问都处手编码器接口托管的状态,我们之前配置使用的72MHZ内部时钟和我们在时基单元初始化时设置的计数方向,并不会使用。计数器的自增和自减,受编码器控制。

6.10.2 编码器接口基本结构

从该图可以看出:输入捕获的前两个通道,通过GPIO口接入编码器的A、B相,然后通过滤波器和边沿检测极性选择产生T11FP1和T12FP2,通向编码器接口,编码器接口通过预分频器控制CNT计数器的时钟,同时,编码器接口还根据编码器的旋转方向,控制CNT的计数方向(编码器正转时,CNT自增,编码器反转时,CNT自减)。另外这里ARR也是有效的,一般我们会设置ARR为最大量程65535。

十分注意:这里反转的时候应该要输出负数,但是直接输出数值却不是负数,-1对应65535、-2对应65534等,需要进行特殊处理——补码,简单说就是强制类型转换

6.10.3 工作模式

这个表,描述的就是我们刚才说的,编码器接口的工作逻辑,上边TI1FP1和TI2FP2接的就是编码器的A、B相,在A相和B相的上升沿或者下降沿触发计数,到底是向上计数还是向下计数,取决于边沿信号发生的这个时刻另一相的电平状态(表中第2列)。

表的左侧一列还显示了这个编码器还分了3种工作模式,分别是仅在TI1计数、仅在TI2计数和TI1、TI2都计数,这3个模式是啥意思呢?

一般用第三种,在TI1和TI2上计数,这个模式精度最高,而且该模式下,正转的状态都向上计数,反转的状态都向下计数

(TI1反相可简单理解为加了一个非门,使得输入的TI1信号翻转了一下)

6.11 定时器实验4

根据6.10.2 编码器接口基本结构的结构图进行编码。

  1. RCC开启时钟,开启GPIO和定时器的时钟。
  2. 配置GPIO,6-8中把PA6和PA7配置为输入模式。
  3. 配置时基单元,预分频器一般选择不分配,自动重装一般给最大65535,只需要个CNT执行计数就行了。
  4. 配置输入捕获单元,这里输入捕获单元只有滤波器和极性两个参数有用。
  5. 配置编码器接口模式,调用库函数。
  6. 最后,调用TIM_Cmd,启动定时器

相关的库函数:

1
2
3
4
5
6
7
8
9
10
11
/**
*@brief 定时器编码器接口配置
*@param TIMx 选择的定时器
*@param TIM_EncoderMode 编码器模式
*@param TIM_IC1Polarity 选择通道1的电平极性
*@param TIM_IC2Polarity 选择通道2的电平极性
*/
void TIM_EncoderInterfaceConfig(TIM_TypeDef* TIMx,
uint16_t TIM_EncoderMode,
uint16_t TIM_IC1Polarity,
uint16_t TIM_IC2Polarity);

编码器接口测速实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/**
* 注意,这里暂时并没有实现测速的功能,只是简单读取计数值
*/
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "Timer.h"

int NUM = 0;

void Encoder_Init(void)
{
// 开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); // 定时器时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // GPIO端口

// 不选择TIM的时钟源(因为编码器接口相当于一个带方向的外部时钟,输入到定时器)

// 配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_TimeBaseInitStruct.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStruct.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStruct.TIM_Period = 65536-1;
TIM_TimeBaseInitStruct.TIM_Prescaler = 1-1;
TIM_TimeBaseInitStruct.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStruct);

// 配置【部分】定时器的输入捕获模块的参数
TIM_ICInitTypeDef TIM_ICInitStruct;
TIM_ICStructInit(&TIM_ICInitStruct); // 初始化
TIM_ICInitStruct.TIM_Channel = TIM_Channel_1;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
// 这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效的
// 这里的上升沿参数代表的是高低电平极性不反转
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3, &TIM_ICInitStruct);
TIM_ICInitStruct.TIM_Channel = TIM_Channel_2;
TIM_ICInitStruct.TIM_ICFilter = 0xF;
// 这里的上升沿并不代表上升沿有效,因为编码器接口始终都是上升沿、下降沿都有效的
// 这里的上升沿参数代表的是高低电平极性不反转
TIM_ICInitStruct.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInit(TIM3, &TIM_ICInitStruct);

// 配置编码器接口
TIM_EncoderInterfaceConfig(TIM3,
TIM_EncoderMode_TI12, // TI1和TI2都计数的模式
TIM_ICPolarity_Rising, // 通道不反相
TIM_ICPolarity_Rising); // 通道不反相

// 初始化GPIO
GPIO_InitTypeDef GPIO_InitStruct;
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7;
GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStruct);

// 使能TIM
TIM_Cmd(TIM2, ENABLE);
}

int main()
{
OLED_Init();
Encoder_Init();
OLED_ShowString(1, 1, "CNT = ");
while(1)
{
/* 这里涉及反转负数问题,必须另作处理——一种简单的方法是强制类型转换
uint16_t CNT = TIM_GetCounter(TIM3);
OLED_ShowSignedNum(1, 5, CNT, 5);
*/
int16_t CNT = TIM_GetCounter(TIM3);
OLED_ShowSignedNum(1, 5, CNT, 5);
}
}

参考链接:江科大STM32(1-4):定时器

N 概念解析

N.1 片内外设、片上外设和片外外设的区别

1. 为什么NVIC、SysTick、RCC这三个也是外设?

1.1 “片上外设” 的定义

在嵌入式系统(如 STM32 单片机)中,“片上外设” 指集成在芯片内部、独立于处理器内核的功能模块。这些模块由内核通过总线访问和控制,用于扩展芯片的功能(如通信、计时、电源管理等)。

简单说:“片上” 表示 “在芯片内部”;“外设” 表示 “相对于内核独立的功能模块”—— 即使某些模块(如 NVIC、SysTick)位于内核内部,但因它们是 “独立于 CPU 核心的功能单元”,仍被视为广义的 “片上外设”。

1.2 为什么 NVIC、SysTick、RCC 属于外设?

结合芯片架构(以 STM32+ARM Cortex-M 内核为例),逐一分析:

  • NVIC(嵌套向量中断控制器)
    • 功能:管理芯片所有中断的优先级、使能 / 禁用中断、处理中断嵌套等。
    • 归属:属于 Cortex-M 内核内部的模块(是 ARM 内核的一部分)。
    • 为何算 “外设”
      虽然 NVIC 位于内核内部,但它是 “独立于 CPU 核心(如运算单元)的功能单元”,负责专门的 “中断管理” 工作。从功能模块化的角度,它被视为内核的 “外设”(即 “内核级外设”)。
  • SysTick(系统滴答定时器)
    • 功能:提供精确的时基(如延时函数、RTOS 任务调度的时间基准),是一个 24 位递减计数器。
    • 归属:同样属于 Cortex-M 内核内部的模块(ARM 内核自带)。
    • 为何算 “外设”
      SysTick 是独立于 CPU 核心的 “定时器功能单元”,负责 “计时 / 定时”—— 这类 “专门功能模块” 在架构上被归类为内核的 “外设”(类似 NVIC,属于内核内的外设)。
  • RCC(复位和时钟控制器)
    • 功能:管理整个芯片的时钟源(如外部晶振 HSE、内部高速时钟 HSI、锁相环 PLL 等),并控制所有外设的时钟使能 / 禁用(比如要使用 GPIO,必须先通过 RCC 打开 GPIO 的时钟)。
    • 归属:位于 Cortex-M 内核外部(是芯片厂商(如 ST)在 ARM 内核基础上添加的模块)。
    • 为何算 “外设”
      RCC 是独立于内核的 “时钟 / 复位管理单元”,属于芯片内部的 “片上资源”,因此被归类为 “内核外外设”。

2. 能否详细说明SysTick(系统滴答定时器)和RCC(复位和时钟控制器)这两个外设,这两个外设都与系统的时间有关系,它们各自负责的主要功能是什么?两者有什么关联和区别?

  • RCC(复位和时钟控制器):系统时钟的 “总开关” 与 “调度中心”
    RCC 是 STM32 芯片内全局时钟与复位的控制中心,负责管理整个系统的 “时钟源”“时钟分频 / 倍频”“外设时钟使能”,以及 “系统 / 外设复位”。核心功能包括:

    • 时钟源管理:选择并配置芯片的主时钟源(如内部高速时钟 HSI、外部高速时钟 HSE、内部低速时钟 LSI、外部低速时钟 LSE 等)。例如,HSI 是默认的 8 MHz 内部时钟,HSE 是外部晶振(如 8 MHz/12 MHz),LSI/LSE 则用于低功耗场景(如 RTC、看门狗)。
    • 时钟分频与倍频:通过 PLL(锁相环)、预分频器等调整系统时钟(SYSCLK)、总线时钟(AHB、APB1、APB2)的频率。例如,将 HSE 经 PLL 倍频到 72 MHz 作为 SYSCLK,再分频给 GPIO、USART 等外设。
    • 外设时钟控制:为所有外设(如 GPIO、USART、SPI、I2C 等)单独提供时钟 “开关”—— 只有通过 RCC 使能某外设的时钟,该外设才能工作(否则处于断电状态,节省功耗)。
    • 复位与安全监控:支持系统复位(重启整个芯片)或外设复位(仅重启某模块);还能通过 “时钟安全系统(CSS)” 监控外部时钟(如 HSE)是否失效,若失效则自动切换到内部时钟(如 HSI),防止系统崩溃。
  • SysTick(系统滴答定时器):内核级的 “精准时间工具”

    SysTick 是 ARM Cortex-M 内核内置的 24 位递减定时器,专注于为系统提供精准时基(如延时函数、RTOS 任务调度的 “心跳”)。核心功能包括:

    • 定时 / 延时生成:通过 “重装载值(Reload Value)” 设置定时周期,计数器从 “重装载值” 开始递减计数,当计数到 0 时触发中断(或仅置位标志位),并自动重新加载初始值,循环往复。
    • 时钟源灵活选择:SysTick 的时钟可来自两种源(通过寄存器 CLKSOURCE 配置):
      • CLKSOURCE=1:使用系统时钟(SYSCLK)(即 RCC 最终配置的主时钟,如 72 MHz);
      • CLKSOURCE=0:使用系统时钟的 1/8(即 SYSCLK/8,如 72 MHz/8 = 9 MHz)。
    • 极简但高效:仅需4个寄存器即可控制(配置时钟源、使能 / 禁用、设置重装载值、清除中断标志),常被用于实现 HAL_Delay()(毫秒级延时)、RTOS 的 “时间片调度”(如每 1 ms 触发一次任务切换)。
  • 两者的关联:RCC 为 SysTick 提供 “时间基准”

    • SysTick 的定时功能依赖 RCC 配置的系统时钟——RCC 决定了 SysTick 时钟源的 “频率”,而 SysTick 基于该频率实现 “精准定时”。
    • 举个例子: 若 RCC 将 SYSCLK 配置为 72 MHz(即系统时钟频率为 72 MHz),且 SysTick 选择 CLKSOURCE=1(直接用 SYSCLK),那么 SysTick 每 “计数一次” 的时间是1/72MHz = 13.89us。若要实现 1 ms 定时,需设置重装载值为71999(因为计数器从 “重装载值” 开始递减,到 0 时触发中断,实际计数次数是 “重装载值 + 1” 次)。
  • SysTick 的常用场景

    SysTick 在裸机开发中核心作用是 “提供精准时基”,典型场景包括:

    1. 实现毫秒 / 微秒级延时:替代低效的 “软件空循环”,尤其在需要精准延时的场景(如通信时序、传感器采样)。
    2. 作为 RTOS 时基:为 FreeRTOS、uCOS 等实时操作系统提供 “心跳中断”(通常 1 ms 一次),驱动任务调度。
    3. 性能统计:通过统计 SysTick 中断次数,计算代码执行时间或系统负载。

参考资料:片内外设、片上外设和片外外设的区别 - CSDN
参考资料:嵌入式知识复习(一)——片上外设篇

N.2 STM32单片机中AHB、APB1和APB2的区分

在 STM32 中,AHB、APB1、APB2 总线本身并非直接提供时钟,而是外设与内核/内存进行数据传输的物理通道。时钟信号通过独立的时钟树网络分配到各总线及外设,具体机制如下:

  1. 总线核心作用:数据传输通道
    AHB总线:连接 CPU、DMA、内存(Flash/SRAM)等高速模块,传输地址、数据及控制信号。
    APB1/APB2总线:连接低速外设(如 UART、SPI、定时器),传输外设寄存器的读写信号。

⚠️ 总线是通信路径,而非时钟源。例如:

配置 GPIO 时,通过 APB 总线写入 GPIOx->ODR 寄存器;
DMA 搬运数据时,通过 AHB 总线访问内存。

  1. 外设时钟如何提供?
    外设时钟由时钟树(Clock Tree) 分配,通过以下流程:
    • 时钟源:
      • 外部晶振(HSE)、内部 RC 振荡器(HSI)等提供基础时钟。
    • 分频与倍频:
      • 系统时钟 SYSCLK 经分频器生成 HCLK(AHB 总线时钟);
      • HCLK 再分频生成 PCLK1(APB1 总线时钟)和 PCLK2(APB2 总线时钟)。
    • 外设时钟分配:
      • 挂载在APB1上的外设(如 TIM2、I2C1)使用 PCLK1;
      • 挂载在APB2上的外设(如 GPIOA、ADC1)使用 PCLK2;
      • 部分外设(如 USB、SDIO)直接使用 AHB 时钟 HCLK 或经 PLL 倍频的专用时钟⁵⁷。

💡 关键区别:

总线:决定外设的寄存器访问路径(APB1/APB2);
时钟树:决定外设的工作频率(PCLK1/PCLK2)。

  1. 时钟使能:外设工作的前提
    即使时钟信号已分配到总线,外设仍需单独开启时钟使能位:
    • 操作寄存器:
      • RCC->AHBENR:使能 AHB 外设(如 DMA);
      • RCC->APB1ENR:使能 APB1 外设(如 USART2);
      • RCC->APB2ENR:使能 APB2 外设(如 SPI1)。
    • 未开启的后果:
      • 访问外设寄存器会触发硬件错误或读取无效数据²⁶⁸。

参考资料1:STM32单片机中AHB、APB1和APB2的区分-CSDN博客
参考资料2:AHB与APB总线你需要知道的事儿 - 知乎
参考资料3:AHB、APB1、APB2在STM32开发中的角色_单片机中apb1-CSDN博客