野火进阶+STM32江科大基础篇GPIO,AFIO,NVIC,EXIT,TIMER

部分转载,原文链接:https://blog.csdn.net/weixin_54742551/article/details/132409170

目录

一、STM32介绍

1.片上资源/外设

2.系统结构

存储器映射

3.引脚定义

二、STM32新建工程

1.工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹:

1.1Start

1.2.Library

1.3User

2.包含头文件的文件夹

3.Define内定义USE_STDPERIPH_DRIVER

4.调试器

5.工程架构

三、GPIO

GPIO的简介

1.GPIO的结构

2.GPIO的输入

2.GPIO的输出

复用开漏/推挽输出

3.GPIO常用库函数

GPIO寄存器操作

寄存器点灯(PA0)

GPIO初始化函数

数据类型常用定义(stdint.h文件中)

示例 1: 初始化GPIO引脚作为输出

示例 2: 设置和复位GPIO引脚

示例 3: 读取GPIO引脚的输入和输出状态

外设GPIO的配置表

四.AFIO

五.EXIT外部中断

逻辑门

M法测速

外部中断配置(***)

EXIT常用库函数

EXIT案例

六.NVIC

NVIC常见库函数(misc.h中)

SysTick—系统定时器

七TIMER定时器

基本定时器

TIM6和TIM7简介

TIM6和TIM7的主要特性

通用定时器

TIMx简介

TIMx主要功能

定时器级联

​​外部时钟模式1的输入

PWM

​编辑

​编辑

360度舵机

外部时钟模式1:外部输入脚(TIx)

外部时钟模式2:外部触发输入(ETR)

时序图

高级定时器

​编辑

定时器常用函数

TIMER案例

定时器内部时钟

定时器外部时钟

编码器接口

​编辑

​编辑

RCC时钟树

ADC(模拟数字转换器)

ADC的简介

逐次逼近型ADC

74HC138译码器

​编辑核心结构

STM32的ADC

ADC框图

注入规则组和规则通道组:

外部触发转换

ADC的基本结构

规则组的4种转换模式

1.单次转换,非扫描模式

2.单次转换,扫描模式

3.连续转换,非扫描模式

4.连续转换,扫描模式

5.间断模式

数据对齐

转换时间

校准

ADC外围电路设计

ADC常用库函数

函数说明

函数举例

规则组AD单通道

规则组AD多通道

DMA

存储器映像

DMA框图讲解

DMA基本结构

DMA转运部分:

基于DMA基本结构的一些问题

 数据宽度与对齐

ADC扫描模式+DMA

DM常用函数

DMA转运案例

ADC+DMA转运

存储器


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。

一、STM32介绍

1.片上资源/外设

2.系统结构

ICode总线:(Instruction bus)用于访问存储空间里指令的总线;

DCode总线:(Data bus):用于访问存储空间里数据的总线;

System总线:用于访问指令、数据以及调试模块接口;

DMA总线:用于内存与外设之间的数据传输;

Bus matrix(总线矩阵):用于总线之间的访问优先级管理控制;

APB总线:用于外设接口的数据传输;ARM公司推出AMBA片上总线结构,该总线主要包含先进高速总线(Advanced High-speed Bus,AHB)和先进外设总线(Advanced Peripheral Bus,APB),分别连接高速设备和低速设备。基于这个总线结构,ICode、Dcode、System Bus都是AHB总线。这里AHB系统总线经过两个AHB-APB桥转换成了两个APB总线。APB1上挂接有DAC、UART等外设,其最高频率可达36MHz;APB2上挂接有ADC、GPIO等外设,其最高频率可达72MHz。

RAM:20K(SRAM)       ROM:64K(Flash)

存储器映射

BLOCK2重点(外设)

3.引脚定义

二、STM32新建工程

1.工程文件夹里建立Start、Library、User等文件夹,复制固件库里面的文件到工程文件夹:

1.1Start

尖括号表示文件不再工程目录下,在编译器文件下

双引号表示在工程目录下面找,找不到去安装目录下面找

stm32f10x.h 有哪些寄存器和他对应的地址

system_stm32f10x.c 和system_stm32f10x.h 配置时钟

core_cm3.c和core_cm3.h 内核的寄存器和配置函数

图中缩写和启动文件后缀名相同

寄存器开发文件:基于寄存器开发的文件所需完成所有文件,然后添加Start的头文件路径就可以; 

1.2.Library

   

1.3User

但是还不能直接使用还需要三个文件,和main函数存放在一起

stm32f10x_conf.h配置库函数头文件的包含关系,参数检查的函数定义

stm32f10x_it.c和stm32f10x_it.h存放中断函数

2.包含头文件的文件夹

3.Define内定义USE_STDPERIPH_DRIVER

4.调试器

工程选项,Debug,下拉列表选择对应调试器,Settings,Flash Download里勾选Reset and Run

5.工程架构

startup_stm32f01x_md.s启动文件

system_stm32f10x.c 和system_stm32f10x.h 配置时钟

stm32f10x_it.c和stm32f10x_it.h存放中断函数

stm32f10x.h 有哪些寄存器和他对应的地址

core_cm3.c和core_cm3.h 内核的寄存器和配置函数

stm32f10x_conf.h配置库函数头文件的包含关系,参数检查的函数定义

三、GPIO

GPIO的简介

  • 每个GPI/O端口有两个32位配置寄存器(GPIOx_CRL,GPIOx_CRH),两个32位数据寄存器 (GPIOx_IDR和GPIOx_ODR),一个32位置位/复位寄存器(GPIOx_BSRR),一个16位复位寄存 器(GPIOx_BRR)和一个32位锁定寄存器(GPIOx_LCKR)。 根据数据手册中列出的每个I/O端口的特定硬件特征,GPIO端口的每个位可以由软件分别配置成 多种模式
  • 可配置为8种输入输出模式 引脚电平:0V~3.3V,部分引脚可容忍5V
  • 输出模式下可控制端口输出高低电平,用以驱动LED、控制蜂鸣器、模拟通信协议输出时序等
  • 输入模式下可读取端口的高低电平或电压,用于读取按键输入、外接模块电平信号输入、ADC电压采集、模拟通信协议接收数据等

1.GPIO的结构

2.GPIO的输入

I/O引脚的保护二极管是对输入电压进行限幅的上面的二极管接VDD, 3.3V,下面接VSS, 0V,当输入电压

  • >3.3V

那上方这个二极管就会导通,输入电压产生的电流就会直接充入VDD而不会流入内部电路;

  • <0V

(这个电压是相对于VSS的电压,所以是可以有负电压的)那这时下方这个二极管就会导通,电流会从VSS直接流出去,而不会从内部电路汲取电流,也是可以保护内部电路的;

  • 在0~3.3V之间

那两个二极管均不会导通,这时二极管对电路没有影响,这就是保护二极管的用途。

模式:

typedef enum
{
  GPIO_Mode_AIN = 0x0,             // 模拟输入模式
  GPIO_Mode_IN_FLOATING = 0x04,    // 浮空输入模式
  GPIO_Mode_IPD = 0x28,            // 下拉输入模式
  GPIO_Mode_IPU = 0x48,            // 上拉输入模式
  GPIO_Mode_Out_OD = 0x14,         // 开漏输出模式
  GPIO_Mode_Out_PP = 0x10,         // 推挽输出模式
  GPIO_Mode_AF_OD = 0x1C,          // 复用开漏输出模式  
  GPIO_Mode_AF_PP = 0x18           // 复用推挽输出模式
}GPIOMode_TypeDef; // GPIO模式类型定义

如果上面导通、下面断开,就是上拉输入模式

如果下面导通、上面断开,就是下拉输入模

如果两个都断开,就是浮空输入模式

上拉和下拉的作用——>为了给输入提供一个默认的输入电平
因为对应一个数字的端口,输入不是高电平就是低电平,那如果输入引脚什么都不接,那就不确定算高电平还是低电平。而实际情况是,如果啥也不接,这时输入就会处于一种浮空的状态,引脚的输入电平极易受外界干扰而改变。为了避免引脚悬空导致的输入数据不确定,我们就需要在这里加上拉或者下拉电阻了,如果接入上拉电阻,当引脚悬空时,还有上拉电阻来保证引脚的高电平,所以上拉输入又可以称作是默认为高电平的输入模式。下拉也是同理,就是默认为低电平的输入方式。

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

施密特触发器的作用就是对输入电压进行整形的,它的执行逻辑是,如果输入电压大于某一阈值,输出就会瞬间升为高电平,如果输入电压小于某一阈值,输出就会瞬间降为低电平。

接下来经过施密特触发器整形的波形就可以直接写入输入数据寄存器了,我们再用程序读取数据输存器对应某一位的数据,就可以知道端口的输入电平了。最后上面这还有两路线路,这些就是连接到片上外设的一些端口,

其中有模拟输入,这个是连接到ADC上的,因为ADC需要接收模拟量,所以这根线是接到施密特触发器前面的

另一个是复用功能输入,这个是连接到其他需要读取端口的外设上的,比如串口的输入引脚等,这根线接收的是数字量,所以在施密特触发器后面。

2.GPIO的输出

输出部分可以由 输出数据寄存器或片上外设 控制,两种控制方式通过这个数据选择器接到了输出控制部分。
如果选择通过输出数据寄存器进行控制,就是普通的IO口输出,写这个数据寄存器的某一位就可以操作对应的某个端口了。
位设置/清除寄存器:这个可以用来单独操作输出数据寄存器的某一位,而不影响其它位。因为这个输出数据寄存器同时控制16个端口,并且这个寄存器只能整体读写,所以如果想单独控制其中某一个端口而不影响其他端口的话,就需要一些特殊的操作方式。

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

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

  • 推挽输出模式

在推挽输出模式下,P-MOS和N-MOS均有效,数据寄存器为1时,上管导通,下管断开,输出直接接到VDD,就是输出高电平,数据寄存器为0时,上管断开,下管导通,输出直接接到VSS,就是输出低电平,这种模式下,高低电平均有较强的驱动能力,所以推挽输出模式也可以叫强推输出模式。在推挽输出模式下,STM32对IO口具有绝对的控制权,高低电平都由STM32说的算。

  • 开漏输出模式

在开漏输出模式下,这个P-MOS是无效的,只有N-MOS在工作,数据寄存器为1时,下管断开,这时输出相当于断开,也就是高阻模式;数据寄存器为0时,下管导通,输出直接接到VSS,也就是输出低电平;这种模式下,只有低电平有驱动能力,高电平是没有驱动能力的。那这个模式有什么用呢,这个开漏模式可以作为通信协议的驱动方式,比如I2C通信的引脚,就是使用的开漏模式,在多机通信的情况下,这个模式可以避免各个设备的相互干扰,另外开漏模式还可以用于输出5V的电平信号。

在输出模式下,输入模式也是有效的,但是在我们刚才的电路图,在所有输入模式下,输出都是无效的,这是因为,一个端口只能有一个输出,但可以有多个输入,所以当配置成输出模式的时候,内部也可以顺便输入一下,这个也是没啥影响的。

复用开漏/推挽输出

可以看到通用的输出/数据寄存器没有连接的,引脚的控制权转移到了片上外设,由片上外设来控制,在输入部分,片上外设也可以读取引脚的电平,同时普通的输入也是有效的,顺便接收一下电平信号其实在GPIO的这8种模式中,除了模拟输入这个模式会关闭数字的输入功能,在其他的7个模式中,所有的输入都是有效的

那这高电平驱动和低电平驱动两种驱动方式应该如何选择呢?
这就得看这个I0口高低电平的驱动能力如何了,我们刚才介绍,这个GPIO在推挽输出模式下,高低电平均有比较强的驱动能力,所以在这里,这两种接法均可。但是在单片机的电路里,一般倾向使用第一种接法(低电平驱动),因为很多单片机或者芯片,都使用了高电平弱驱动,低电平的强驱动的规则,这样可以一定程度上避免高低电平打架。所以如果高电平驱动能力弱,那就不能使用第一种连接方法了
 

3.GPIO常用库函数

GPIO寄存器操作

与清零 (有0则0)或相加(有1则1)
//P0口输出低电平
1   -----0000 0000 0000 0001
1取反----1111 1111 1111 1110
&一下----有1的不变 0位变0
GPIO_ODR &=~(1<<0);
//P0口输出高电平
GPIO_ODR |=(1<<0);

寄存器点灯(PA0)
#include "stm32f10x.h"
//置位|=          清0 &=~ 
int main(void)
{
	//APB2 外设时钟使能寄存器(RCC_APB2ENR)
	*(unsigned int *)0x40021018 |=(1<<2); 
	//设置推挽输出
	*(unsigned int *)0x40010800 &=~(12<<0);
	*(unsigned int *)0x40010800 |=~(2<<0);	
	//配置端口输入数据寄存器(GPIOx_IDR) (x=A..E)
	*(unsigned int *)0x4001080C &=~(1<<0);
	
}


void SystemInit()
{
}

#include "stm32f10x.h"

// 外设基地址
#define PERIPH_BASE          ((unsigned int)0x40000000)

// APB1 基地址
#define APB1_PERIPH_BASE     PERIPH_BASE

// APB2 基地址
#define APB2_PERIPH_BASE     (PERIPH_BASE + 0x10000)

// AHB 基地址
#define AHB_PERIPH_BASE      (PERIPH_BASE + 0x20000)

// RCC 基地址
#define AHB_RCC_PERIPH_BASE  (AHB_PERIPH_BASE + 0x1000)

// GPIOA 基地址
#define GPIOA_BASE           (APB2_PERIPH_BASE + 0x0800)

// RCC_APB2ENR 寄存器
#define APB2RCC_APB2ENR      *(unsigned int*)(AHB_RCC_PERIPH_BASE  + 0x18)

// GPIOA_CRL 寄存器
#define GPIOA_CRL            *(unsigned int*)(GPIOA_BASE)

// GPIOA_ODR 寄存器
#define GPIOA_ODR            *(unsigned int*)(GPIOA_BASE + 0x0C)

int main(void)
{
    // 使能 GPIOA 时钟 (RCC_APB2ENR 寄存器地址: 0x40021018)
    APB2RCC_APB2ENR |= (1 << 2);  // IOPAEN = 1
    
    // 配置 PA0 为推挽输出模式,最大速度 2MHz (GPIOA_CRL 寄存器地址: 0x40010800)
    GPIOA_CRL &= ~(0XC << 0);     // 清除 CRL 寄存器第 0-3 位
    GPIOA_CRL |= (0x2 << 0);      // 设置 CRL 寄存器第 0-3 位为 0010 (2MHz, 推挽输出)

    // 点亮 LED (假设 LED 连接在 PA0)
    GPIOA_ODR &= ~(1 << 0);        // 将 PA0 设置为高电平,点亮 LED
     GPIOA_ODR |= (1 << 0);        // 将 PA0 设置为高电平,点亮 LED
    while (1)
    {
        // 保持 LED 点亮,可以在这里添加闪烁代码
    }
}

void SystemInit(void)
{
    // 系统初始化函数,可以留空
}

结构体定义GPIOA的寄存器

#include "stm32f10x.h"

// 外设基地址
#define PERIPH_BASE          ((unsigned int)0x40000000)

// APB1 基地址
#define APB1_PERIPH_BASE     PERIPH_BASE

// APB2 基地址
#define APB2_PERIPH_BASE     (PERIPH_BASE + 0x10000)

// AHB 基地址
#define AHB_PERIPH_BASE      (PERIPH_BASE + 0x20000)

// RCC 基地址
#define AHB_RCC_PERIPH_BASE  (AHB_PERIPH_BASE + 0x1000)

// GPIOA 基地址
#define GPIOA_BASE           (APB2_PERIPH_BASE + 0x0800)

// RCC_APB2ENR 寄存器
#define APB2RCC_APB2ENR      *(unsigned int*)(AHB_RCC_PERIPH_BASE  + 0x18)

// GPIOA_CRL 寄存器
#define GPIOA_CRL            *(unsigned int*)(GPIOA_BASE)

// GPIOA_ODR 寄存器
#define GPIOA_ODR            *(unsigned int*)(GPIOA_BASE + 0x0C)


typedef unsigned int    uint32_t; 
typedef unsigned short  uint16_t; 
typedef struct
 {
	 uint32_t CRL; 
	 uint32_t CRH;
	 uint32_t IDR;
	 uint32_t ODR;
	 uint32_t BSRR;
	 uint32_t BRR;
	 uint32_t LCKR;
 }GPIOA_TypeDef;
#define  GPIOA               ((GPIOA_TypeDef*)GPIOA_BASE)
int main(void)
{
	
   
    // 使能 GPIOA 时钟 (RCC_APB2ENR 寄存器地址: 0x40021018)
    APB2RCC_APB2ENR |= (1 << 2);  // IOPAEN = 1
    
    // 配置 PA0 为推挽输出模式,最大速度 2MHz (GPIOA_CRL 寄存器地址: 0x40010800)
    GPIOA->CRL &= ~(0XC << 0);     // 清除 CRL 寄存器第 0-3 位
    GPIOA->CRL |= (0x2 << 0);      // 设置 CRL 寄存器第 0-3 位为 0010 (2MHz, 推挽输出)

    // 点亮 LED (假设 LED 连接在 PA0)
    GPIOA->ODR &= ~(1 << 0);        // 将 PA0 设置为高电平,点亮 LED
     GPIOA->ODR |= (1 << 0);        // 将 PA0 设置为高电平,点亮 LED
    while (1)
    {
        // 保持 LED 点亮,可以在这里添加闪烁代码
    }
}

void SystemInit(void)
{
    // 系统初始化函数,可以留空
}

// 外设基地址
#define PERIPH_BASE          ((unsigned int)0x40000000)

// APB1 基地址
#define APB1_PERIPH_BASE     PERIPH_BASE

 

// AHB 基地址
#define AHB_PERIPH_BASE      (PERIPH_BASE + 0x20000)

// RCC 基地址
#define AHB_RCC_PERIPH_BASE  (AHB_PERIPH_BASE + 0x1000)

外设基地址(APB1)+0X10000=APB2+0X800=GPIOA+寄存器偏移0X18 

// APB2 基地址
#define APB2_PERIPH_BASE     (PERIPH_BASE + 0x10000)

// GPIOA 基地址
#define GPIOA_BASE           (APB2_PERIPH_BASE + 0x0800)

// RCC_APB2ENR 寄存器
#define APB2RCC_APB2ENR      *(unsigned int*)(APB2_PERIPH_BASE + 0x18)

开启  APB2 外设时钟使能寄存器(RCC_APB2ENR)
 *(unsigned int *)0x40021018 |=(1<<2); 

设置推挽输出

 *(unsigned int *)0x40010804 |=(3<<0);

配置低位而不是配置高位寄存器(P0-P7)

    //配置端口输入数据寄存器(GPIOx_IDR) (x=A..E)
    *(unsigned int *)0x4001080C &=~(1<<0);

GPIO初始化函数

数据类型常用定义(stdint.h文件中)

// 复位指定的GPIO端口到其初始状态
void GPIO_DeInit(GPIO_TypeDef* GPIOx);

// 复位AFIO寄存器到其初始状态
void GPIO_AFIODeInit(void);

// 初始化指定的GPIO端口
void GPIO_Init(GPIO_TypeDef* GPIOx, GPIO_InitTypeDef* GPIO_InitStruct);

// 使用默认参数初始化GPIO_InitTypeDef结构体
void GPIO_StructInit(GPIO_InitTypeDef* GPIO_InitStruct);

// 读取指定GPIO端口的指定引脚的输入数据位
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 读取指定GPIO端口的输入数据
uint16_t GPIO_ReadInputData(GPIO_TypeDef* GPIOx);

// 读取指定GPIO端口的指定引脚的输出数据位
uint8_t GPIO_ReadOutputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 读取指定GPIO端口的输出数据
uint16_t GPIO_ReadOutputData(GPIO_TypeDef* GPIOx);

// 设置指定GPIO端口的指定引脚
void GPIO_SetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 复位指定GPIO端口的指定引脚
void GPIO_ResetBits(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 写入指定GPIO端口的指定引脚的状态
void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal);

// 写入指定GPIO端口的输出数据
void GPIO_Write(GPIO_TypeDef* GPIOx, uint16_t PortVal);

// 配置指定GPIO端口的指定引脚的锁定功能
void GPIO_PinLockConfig(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin);

// 配置GPIO事件输出
void GPIO_EventOutputConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

// 使能或禁用GPIO事件输出
void GPIO_EventOutputCmd(FunctionalState NewState);

// 配置GPIO引脚重映射
void GPIO_PinRemapConfig(uint32_t GPIO_Remap, FunctionalState NewState);

// 配置GPIO外部中断线路
void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

// 配置以太网媒体接口
void GPIO_ETH_MediaInterfaceConfig(uint32_t GPIO_ETH_MediaInterface);

示例 1: 初始化GPIO引脚作为输出

// 包含所需的头文件
#include "stm32f10x.h"

void Example_GPIO_Init_Output(void) {
    GPIO_InitTypeDef GPIO_InitStructure;

    // 启用GPIO时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

    // 使用默认参数初始化GPIO结构体
    GPIO_StructInit(&GPIO_InitStructure);

    // 配置PC13为推挽输出模式
    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);
}

示例 2: 设置和复位GPIO引脚

void Example_GPIO_Set_Reset(void) {
    // 设置PC13引脚
    GPIO_SetBits(GPIOC, GPIO_Pin_13);

    // 复位PC13引脚
    GPIO_ResetBits(GPIOC, GPIO_Pin_13);

    // 或者使用GPIO_WriteBit函数
    GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET);
    GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_RESET);
}

示例 3: 读取GPIO引脚的输入和输出状态

void Example_GPIO_Read(void) {
    uint8_t inputState, outputState;

    // 读取PC13引脚的输入数据位
    inputState = GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_13);

    // 读取PC13引脚的输出数据位
    outputState = GPIO_ReadOutputDataBit(GPIOC, GPIO_Pin_13);
}

外设GPIO的配置表

四.AFIO

五.EXIT外部中断

68个可屏蔽中断通道,包含EXTI、TIM、ADC、USART、SPI、I2C、RTC等多个外设

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

NVIC通过每个中断的优先级输出一个通道(病人叫号)

中断号就是上面那个表55

但相同的Pin不能同时触发中断:这个意思就是,比如PA0和PB0不能同时用,或者,PA1、PB1、PC1这样的,端口GPIO_Pin一样的。
然后再看一下外部中断占用的通道,其中有16个GPIO_Pin,这就对应GPIO_Pin_0到GPIO_Pin_15,外加PVD输出、RTC闹钟、USB唤醒、以太网唤醒,这些加起来总共有20个中断线路。这里的16个GPIO_Pin是外部中断的主要功能,后面跟着的这四个东西其实是来“蹭网”的。因为这个外部中断有个功能,就是从低功耗模式的停止模式下唤醒STM32,那对于PVD电源电压监测,当从电源从电压过低恢复时,就需要PVD借助一下外部中断退出停止模式;对于RTC闹钟来说,有时候为了省电,RTC定一个闹钟之后,STM32会进入停止模式,等到闹钟响的时候再唤醒,这也需要借助外部中断;还有USB唤醒、以太网唤醒,也都是类似的作用。
中断响应,就是申请中断,让CPU执行中断函数;事件响应是STM32对外部中断增加的一种额外的功能。当外部中断检测到引脚电平变化时,正常的流程是选择触发中断,但是在STM32中,也可以选择触发一个事件,如果选择触发事件,那外部中断的信号就不会通向CPU了,而是通向其它外设,用来触发其它外设的操作,比如触发ADC转换、触发DMA等。所以总结一下:中断响应是正常的流程,引脚电平变化触发中断;事件响应不会触发中断,而是触发别的外设操作,属于外设之间的联合工作。

AFIO主要用于引脚复用功能的选择和重定义 在STM32中,AFIO主要完成两个任务:复用功能引脚重映射、中断引脚选择

梯形图是数据选择器(多个输入,一个输出)

逻辑门

外部中断的使用场景:
就是对于STM32来说,想要获取的信号是外部驱动的很快的突发信号。比如旋转编码器的输出信号,你可能很久都不会拧它,这时不需要STM32做任何事,但是我一拧它,就会有很多脉冲波形需要STM32接收。这个信号是突发的,STM32不知道什么时候会来,同时它是外部驱动的,STM32只能被动读取,最后这个信号非常快,STM32稍微晚一点来读取,就会错过很多波形。那对于这种情况来说,就可以考虑使用STM32的外部中断了。有脉冲过来,STM32立即进入中断函数处理,没有脉冲的时候,STM32就专心做其它事情。
另外还有,比如红外遥控接收头的输出,接收到遥控数据之后,它会输出一段波形,这个波形转瞬即逝,并且不会等你,所以就需要我们用外部中断来读取。
最后还有按键,虽然它的动作也是外部驱动的突发事件,但我并不推荐用外部中断来读取按键。因为用外部中断不好处理按键抖动和松手检测的问题,对于按键来说,它的输出波形也不是转瞬即逝的。所以要求不高的话可以在主程序中循环读取,如果不想用主循环读取的话,可以考虑一下定时器中断读取的方式。这样既可以做到后台读取按键值、不阻塞主程序,也可以很好地处理按键抖动和松手检测的问题。

编码器是将信号(如比特流)或数据进行编制、转换为可用以通讯、传输和存储的信号形式的设备。编码器把角位移或直线位移转换成电信号,前者称为码盘,后者称为码尺。按照读出方式编码器可以分为接触式和非接触式两种;按照工作原理编码器可分为增量式和绝对式两类。

增量式编码器是将位移转换成周期性的电信号,再把这个电信号转变成计数脉冲,用脉冲的个数表示位移的大小。绝对式编码器的每一个位置对应一个确定的数字码,因此它的示值只与测量的起始和终止位置有关,而与测量的中间过程无关。

M法测速

又叫做频率测量法。该方法是在一个固定的时间内(以秒为单位),统计这段时间的编码器脉冲数,计算速度值。M法适合测量高速

假设:

编码器单圈总脉冲数为C(常数)

统计时间为 T0 (固定值,单位秒)

该时间内统计到的编码器脉冲数为 M0(测量值)

则:转速n(圈/秒)的计算公式为:

外部中断配置(***)

RCC-GPIO -AFIO-EXIT-NVIC

1.打开RCC时钟:EXTI 和NVIC两个外设一直都是打开的,只需要打开GPIO和AFIO    EXTI外部中断线配置上拉下拉或浮空

2.配置GPIO 

3.配置GPIO外部中断线路void GPIO_EXTILineConfig(uint8_t GPIO_PortSource, uint8_t GPIO_PinSource);

4.配置EXIT Exit_Init();

5.配置NVIC Nvic_Init();

void Example_GPIO_EXTI_Config(void) {
    // 定义GPIO和EXTI配置结构体
    EXTI_InitTypeDef EXTI_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    // 启用GPIOA和AFIO时钟   EXTI 和NVIC两个外设一直都是打开的
    // GPIOA用于输入引脚,AFIO用于配置引脚重映射等功能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

    // 配置PA0为输入浮空模式
    // 输入浮空模式意味着引脚没有内部上拉或下拉电阻
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    // 将PA0引脚连接到EXTI线路0
    // GPIO_PortSourceGPIOA表示选择GPIOA端口,GPIO_PinSource0表示选择引脚0
    GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);

    // 配置EXTI线路0的设置
    // EXTI_Line0表示线路0,对应于PA0引脚
    // EXTI_Mode_Interrupt表示中断模式
    // EXTI_Trigger_Rising表示触发类型为上升沿触发
    // EXTI_LineCmd = ENABLE表示使能该线路
    EXTI_InitStructure.EXTI_Line = EXTI_Line0;
    EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
    EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
    EXTI_InitStructure.EXTI_LineCmd = ENABLE;
    EXTI_Init(&EXTI_InitStructure);

    // 配置和使能NVIC中的EXTI0中断
    // NVIC_IRQChannel = EXTI0_IRQn表示中断源为EXTI0
    // NVIC_IRQChannelPreemptionPriority = 0x0F表示抢占优先级最低
    // NVIC_IRQChannelSubPriority = 0x0F表示子优先级最低
    // NVIC_IRQChannelCmd = ENABLE表示使能该中断
    NVIC_InitTypeDef NVIC_InitStructure;
    NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
    NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x0F;
    NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x0F;
    NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
    NVIC_Init(&NVIC_InitStructure);
}

// EXTI0中断处理函数
void EXTI0_IRQHandler(void) {
    // 检查EXTI线路0的中断标志是否被设置
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        // 在此处处理中断事件,例如读取数据或更改输出状态

        // 清除EXTI线路0的中断挂起标志
        // 这一步骤是必要的,否则中断将不会被清除
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

EXIT常用库函数

/**
 * @brief  复位EXTI外部中断寄存器到其默认状态。
 * @note   该函数会禁用所有EXTI线路的中断请求,并清除所有挂起的中断标志。
 */
void EXTI_DeInit(void);

/**
 * @brief  初始化指定的EXTI外部中断线路。
 * @param  EXTI_InitStruct: 指向EXTI_InitTypeDef结构体的指针,
 *         该结构体包含EXTI线路的配置信息。
 * @retval 无
 * @note   根据EXTI_InitStruct中的设置,配置线路的模式(中断或事件)、触发条件
 *         (上升沿、下降沿或双边沿)以及使能状态。
 */
void EXTI_Init(EXTI_InitTypeDef* EXTI_InitStruct);

/**
 * @brief  用默认值初始化EXTI_InitTypeDef结构体。
 * @param  EXTI_InitStruct: 指向EXTI_InitTypeDef结构体的指针,
 *         该结构体将被初始化为默认值。
 * @retval 无
 * @note   默认设置为所有EXTI线路禁用中断,触发模式为上升沿。
 */
void EXTI_StructInit(EXTI_InitTypeDef* EXTI_InitStruct);

/**
 * @brief  生成指定EXTI线路的软件中断。
 * @param  EXTI_Line: 指定要生成软件中断的EXTI线路,可以是EXTI_Line0到EXTI_Line19。
 * @retval 无
 * @note   该功能主要用于测试或控制逻辑中手动触发中断。
 */
void EXTI_GenerateSWInterrupt(uint32_t EXTI_Line);

/**
 * @brief  获取指定EXTI线路的标志状态。
 * @param  EXTI_Line: 指定EXTI线路,可以是EXTI_Line0到EXTI_Line19。
 * @retval FlagStatus: 标志状态,可以是SET或RESET。
 * @note   检查指定的EXTI线路是否有挂起的中断标志。
 */
FlagStatus EXTI_GetFlagStatus(uint32_t EXTI_Line);

/**
 * @brief  清除指定EXTI线路的标志。
 * @param  EXTI_Line: 指定要清除标志的EXTI线路,可以是EXTI_Line0到EXTI_Line19。
 * @retval 无
 * @note   该函数用于清除指定EXTI线路的挂起标志。
 */
void EXTI_ClearFlag(uint32_t EXTI_Line);

/**
 * @brief  获取指定EXTI线路的中断状态。
 * @param  EXTI_Line: 指定EXTI线路,可以是EXTI_Line0到EXTI_Line19。
 * @retval ITStatus: 中断状态,可以是SET或RESET。
 * @note   检查指定的EXTI线路是否有挂起的中断。
 */
ITStatus EXTI_GetITStatus(uint32_t EXTI_Line);

/**
 * @brief  清除指定EXTI线路的中断挂起位。
 * @param  EXTI_Line: 指定要清除中断挂起位的EXTI线路,可以是EXTI_Line0到EXTI_Line19。
 * @retval 无
 * @note   该函数用于清除指定EXTI线路的中断挂起状态,
 *         确保中断处理程序已完成处理。
 */
void EXTI_ClearITPendingBit(uint32_t EXTI_Line);

EXIT案例

#include "stm32f4xx.h" // 假设使用STM32系列微控制器

// 中断初始化函数
void EXTI_Config(void) {
    // 创建并初始化一个中断配置结构体
    EXTI_InitTypeDef EXTI_InitStruct;

    // 重置外部中断配置到默认状态
    EXTI_DeInit();

    // 初始化结构体为默认值
    EXTI_StructInit(&EXTI_InitStruct);

    // 设置中断线0
    EXTI_InitStruct.EXTI_Line = EXTI_Line0;

    // 设置为中断模式
    EXTI_InitStruct.EXTI_Mode = EXTI_Mode_Interrupt;

    // 设置上升沿触发
    EXTI_InitStruct.EXTI_Trigger = EXTI_Trigger_Rising;

    // 使能中断线
    EXTI_InitStruct.EXTI_LineCmd = ENABLE;

    // 根据配置初始化外部中断
    EXTI_Init(&EXTI_InitStruct);
}

// 主函数
int main(void) {
    // 配置外部中断
    EXTI_Config();

    while (1) {
        // 检查中断线0的标志状态
        if (EXTI_GetFlagStatus(EXTI_Line0) == SET) {
            // 如果标志被设置,生成中断线0的软中断
            EXTI_GenerateSWInterrupt(EXTI_Line0);

            // 清除中断线0的中断标志
            EXTI_ClearFlag(EXTI_Line0);
        }

        // 检查中断线0的中断状态
        if (EXTI_GetITStatus(EXTI_Line0) == SET) {
            // 处理中断

            // 清除中断线0的中断挂起位
            EXTI_ClearITPendingBit(EXTI_Line0);
        }
    }
}

六.NVIC

NVIC常见库函数(misc.h中)

/**
 * @brief  配置NVIC中断优先级分组。
 * @param  NVIC_PriorityGroup: 指定优先级分组,可以是以下值之一:
 *         - NVIC_PriorityGroup_0: 0 位抢占优先级, 4 位子优先级
 *         - NVIC_PriorityGroup_1: 1 位抢占优先级, 3 位子优先级
 *         - NVIC_PriorityGroup_2: 2 位抢占优先级, 2 位子优先级
 *         - NVIC_PriorityGroup_3: 3 位抢占优先级, 1 位子优先级
 *         - NVIC_PriorityGroup_4: 4 位抢占优先级, 0 位子优先级
 * @retval 无
 * @note   该函数用于配置中断的抢占优先级和子优先级分组。
 */
void NVIC_PriorityGroupConfig(uint32_t NVIC_PriorityGroup);

/**
 * @brief  初始化NVIC中断通道。
 * @param  NVIC_InitStruct: 指向NVIC_InitTypeDef结构体的指针,
 *         该结构体包含指定中断的配置信息。
 * @retval 无
 * @note   该函数用于配置特定中断的抢占优先级、子优先级和使能状态。
 */
void NVIC_Init(NVIC_InitTypeDef* NVIC_InitStruct);

/**
 * @brief  设置向量表位置和偏移。
 * @param  NVIC_VectTab: 指定向量表的基址,可以是以下值之一:
 *         - NVIC_VectTab_RAM: 使用RAM作为向量表
 *         - NVIC_VectTab_FLASH: 使用FLASH作为向量表
 * @param  Offset: 向量表的偏移地址。
 * @retval 无
 * @note   该函数用于配置中断向量表的位置和偏移,一般在系统初始化时使用。
 */
void NVIC_SetVectorTable(uint32_t NVIC_VectTab, uint32_t Offset);

/**
 * @brief  配置系统低功耗模式。
 * @param  LowPowerMode: 指定要配置的低功耗模式,可以是以下值之一:
 *         - NVIC_LP_SEVONPEND: 设置SEVONPEND位
 *         - NVIC_LP_SLEEPDEEP: 设置SLEEPDEEP位
 *         - NVIC_LP_SLEEPONEXIT: 设置SLEEPONEXIT位
 * @param  NewState: 启用或禁用低功耗模式,该参数可以是: ENABLE 或 DISABLE。
 * @retval 无
 * @note   该函数用于配置系统进入低功耗模式时的行为。
 */
void NVIC_SystemLPConfig(uint8_t LowPowerMode, FunctionalState NewState);

/**
 * @brief  配置SysTick定时器的时钟源。
 * @param  SysTick_CLKSource: 指定SysTick时钟源,可以是以下值之一:
 *         - SysTick_CLKSource_HCLK_Div8: HCLK/8 作为SysTick时钟源
 *         - SysTick_CLKSource_HCLK: HCLK 作为SysTick时钟源
 * @retval 无
 * @note   该函数用于配置SysTick定时器的时钟源,可以影响定时器的计数频率。
 */
void SysTick_CLKSourceConfig(uint32_t SysTick_CLKSource);

SysTick—系统定时器

STM32入门:Systick(嘀嗒定时器)学习-CSDN博客

/**
 * @brief  初始化并启动SysTick计数器及其中断。
 *
 * @param   ticks   两次中断之间的滴答计数值
 * @return  1 = 失败, 0 = 成功
 *
 * 初始化系统滴答定时器及其中断,并以自由运行模式启动系统滴答定时器/计数器,以生成周期性中断。
 */
static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{ 
  // 检查传入的滴答计数值是否超过了SysTick的最大值
  if (ticks > SysTick_LOAD_RELOAD_Msk)  
    return (1);  // 如果超过最大值,则返回1表示失败
  
  // 设置重装载寄存器,即每次计数到这个值后重置计数器
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      
  
  // 设置SysTick中断的优先级,设置为最低优先级
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  
  
  // 清空当前的计数值,使计数器从0开始
  SysTick->VAL   = 0;                                          
  
  // 启用SysTick计数器、启用中断,并选择时钟源
  SysTick->CTRL  = SysTick_CTRL_CLKSOURCE_Msk | 
                   SysTick_CTRL_TICKINT_Msk   | 
                   SysTick_CTRL_ENABLE_Msk;                    
  
  // 如果一切设置成功,返回0
  return (0);                                                  
}

七TIMER定时器

为什么在72MHz计数时钟下可以实现最大59.65s的定时?
72M/65536/65536,得到的是中断频率,然后取倒数,就是59.65秒多,大家可以自己算一下。
详细解释:在定时器中,预分频器和计数器都是16位的,所以它们的最大值是65535,而不是65536。预分频器的最大值决定了计数时钟的频率,而计数器的最大值决定了定时器的最大计数周期。因此,如果预分频器和计数器的最大值都设置为65535,那么定时器的最大时间就是72MHz/65536/65536,得到的是中断频率,倒数就是中断时间。【最大值是65536,但计数是从0~65535】

基本定时器

TIM6和TIM7简介

 基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。 它们可以作为通用定时器提供时间基准,特别地可以为数模转换器(DAC)提供时钟。实际上,它 们在芯片内部直接连接到DAC并通过触发输出直接驱动DAC。 这2个定时器是互相独立的,不共享任何资源

TIM6和TIM7的主要特性

TIM6和TIM7定时器的主要功能包括:

● 16位自动重装载累加计数器

● 16位可编程(可实时修改)预分频器,用于对输入的时钟按系数为1~65536之间的任意数值 分频 ● 触发DAC的同步电路

● 在更新事件(计数器溢出)时产生中断/DMA请求

来自RCC的TIMXCLK内部时钟 72hz

实际分频系数=预分频器的值+1

CNT计数器:就是有一个上升沿就加1

自动重装载寄存器: 是自己设置的,CNT计了多少个数就触发中断

通用定时器

TIMx简介

通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成。 它适用于多种场合,包括测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)。 使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个 毫秒间调整。 每个定时器都是完全独立的,没有互相共享任何资源。

TIMx主要功能

通用TIMx (TIM2、TIM3、TIM4和TIM5)定时器功能包括:

● 16位向上、向下、向上/向下(中央对齐)自动装载计数器

● 16位可编程(可以实时修改)预分频器,计数器时钟频率的分频系数为1~65536之间的任意 数值 ● 4个独立通道:

─ 输入捕获

─ 输出比较

─ PWM生成(边缘或中间对齐模式)

─ 单脉冲模式输出

● 使用外部信号控制定时器和定时器互连的同步电路

● 如下事件发生时产生中断/DMA:

─ 更新:计数器向上溢出/向下溢出,计数器初始化(通过软件或者内部/外部触发)

─ 触发事件(计数器启动、停止、初始化或者由内部/外部触发计数)

─ 输入捕获

─ 输出比较

● 支持针对定位的增量(正交)编码器和霍尔传感器电路

● 触发输入作为外部时钟或者按周期的电流管理

定时器级联

这个TIM2的CH1和ETR脚都复用在PA0引脚,下面还有CH2、CH3、CH4(CH是通道)和其他定时器的一些引脚,也都可以在这里找到。

ITRO接在其他定时器的TRGO 的后面  见表78 (通过更新事件映射到TRGO上可以实现定时器的级联)

eg:比如初始化TIM3将更新事件映射到TRGO,在初始化TIM2,查表TIM-ITR2-TIM3

​​外部时钟模式1的输入

1.ETR引脚,

2.其他定时器(ITR0,ITR1,ITR2,ITR3)

3.CH1引脚的边沿(TIIF_ED)

4.CH1和CH2引脚(TI1FP1,T21FP2)【注:编码器接口可以读取编码器的输出波形】

PWM

TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;				//输出比较模式,选择PWM模式1
	TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;		//输出极性,选择为高,若选择极性为低,则输出高低电平取反
	TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;	//输出使能
	TIM_OCInitStructure.TIM_Pulse = 0;								//初始的CCR值
	TIM_OC1Init(TIM2, &TIM_OCInitStructure);						//将
TIM_SetCompare1(TIM2, Compare);		//设置CCR1的值

TIM_TimeBaseInitStructure.TIM_Period = 20000 - 1;	//计数周期,即ARR的值TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;//预分频器,即PSC的值100/2 =50HZ =0.02s=20ms
/**
  * 函    数:PWM设置CCR
  * 参    数:Compare 要写入的CCR的值,范围:0~100
  * 返 回 值:无
  * 注意事项:CCR和ARR共同决定占空比,此函数仅设置CCR的值,并不直接是占空比
  *           占空比Duty = CCR / (ARR + 1)  500/20000*20MS=0.5ms
 *                                         2500/20000*20ms =2.5ms
  */
TIM_SetCompare2(TIM2, Compare);		//设置CCR2的值   

/**
  * 函    数:舵机设置角度
  * 参    数:Angle 要设置的舵机角度,范围:0~180
  * 返 回 值:无
  */
void Servo_SetAngle(float Angle)
{
	PWM_SetCompare2(Angle / 180 * 2000 + 500);	//设置占空比
												//将角度线性变换,对应到舵机要求的占空比范围上
}

360度舵机

360度
360度舵机可以360度旋转,因此与180度舵机有相当大的区别,首先360度舵机不能够控制旋转角度,一般的舵机是给一个特定的PWM,舵机会转到相应的角度,而360度舵机是只能够控制方向和旋转转速,所以360度舵机给定一个PWM,会以特定的速度和方向转动。
所以有:
0.5ms----------------正向最大转速;
1.5ms----------------速度为0;
2.5ms----------------反向最大转速;

外部时钟模式1:外部输入脚(TIx)


当TIMx_SMCR寄存器的SMS=111时,此模式被选中。计数器可以在选定输入端的每个上升沿或下降沿计数。


当这个TRGI当做外部时钟来使用的时候,这一路就叫做“外部时钟模式1”,那通过这一路的外部时钟都有哪些呢?

第一个,就是ETR引脚的信号

然后第二个,就是ITR信号,这一部分的时钟信号是来自其他定时器,从右边可以看出,这个主模式的输出TRGO可以通向其他定时器,那通向其他定时器的时候,就接到了其他定时器的ITR引脚上来了。

这个ITRO到ITR3分别来自其他4个定时器的TRGO输出,至于具体的连接方式是怎么的,手册的这个位置有一张表。这里可以看到,TIM2的ITRO是接在了TIM1的TRGO上,ITR1接在了TIM8,ITR2接在了TIM3,ITR3接在了TIM4,其他定时器也都可以参照一下这个表,这就是TR和定时器的连接关系。通过这一路我们就可以实现定时器级联的功能.比如我可以先初始化TIM3,然后使用主模式把它的更新事件映射到TRGO上,接着再初始化TIM2,这里选择ITR2,对应的就是TIM3的TRGO,然后后面再选择时钟为外部时钟模式1,这样TIM3的更新事件就可以驱动TIM2的时基单元,也就实现了定时器的级联.

这里还可以选择TI1F_ED,这里连接的是这里输入捕获单元的CH1引脚,也就是从CH1引脚获得时钟,这里后缀加一个ED(Edge)就是边沿的意思,也就是通过这一路输入的时钟,上升沿和下降沿均有效

最后,这个时钟还能通过TI1FP1和TI2FP2获得

总结一下就是,外部时钟模式1的输入可以是ETR引脚、其他定时器,CH1引脚的边沿、CH1引脚和CH2引脚,这还是比较复杂的,一般情况下外部时钟通过ETR引脚就可以了。上面设置这么复杂的输入,不仅仅是为了扩大时钟输入的范围,更多的还是为了某些特殊应用场景而设计的,比如为了定时器的级联而设计的ITRx引脚,最后的一部分,我们之后讲输入捕获和测频率时,还会继续讲到。
注:对于时钟输入而言,最常用的还是内部的72MHz的时钟,如果要使用外部时钟,如果要使用外部时钟,首选ETR引脚外部时钟模式2的输入,这一路最简单、最直接。

外部时钟模式2:外部触发输入(ETR)


计数器能够在外部触发ETR的每一个上升沿或下降沿计数。

这个ETR(External)引脚的位置,可以参考一下引脚定义表。

可以看到这里有TIM2_CH1_ETR,意思就是这个TIM2的CH1和ETR都是复用在了这个位置,也就是PA0引脚,下面还有CH2,CH3,CH4和其他定时器的一些引脚,也都可以在这里找到。
那这里我们可以在这个TIM2的ETR引脚,也就是PA0上接一个外部方波时钟,然后配置一下内部的极性选择、边沿检测和预分频器电路,再配置一下输入滤波电路,这两块电路可以对外部时钟进行一定的整形。因为是外部引脚的时钟,所以难免会有的毛刺,那这些电路就可以对输入的波形进行滤波,同时也可以选择一下极性和预分频器。最后,滤波后的信号,兵分两路,上面一路ETRF进入触发控制器,紧跟着就可以选择作为时基单元的时钟了。
如果你想在ETR外部引脚提供时钟或者想对ETR时钟进行计数,把这个定时器当做计数器来用的话,那就可以配置这一路的电路,在STM32中,这一路也叫做“外部时钟模式2“。


例如,要配置在ETR下每2个上升沿计数一次的向上计数器,使用下列步骤:

1,本例中不需要滤波器,置TIMx_SMCR寄存器中的ETF(握)= 0000
2,设置预分频器,置TIMx_SMCR寄存器中的早期胸腺祖细胞(1:0)= 1
3.设置在ETR的上升沿检测,置TIMx_SMCR寄存器中的ETP=0
4,开启外部时钟模式2,置TIMx_SMCR寄存器中的ECE=1
5.,启动计数器,置TIMx_CR1寄存器中的CEN=1
计数器在每2个ETR上升沿计数一次。
在ETR的上升沿和计数器实际时钟之间的延时取决于在ETRP信号端的重新同步电路。


内部触发输入(ITRx)(定时器同步)
所有TIMx定时器在内部相连,用于定时器同步或链接。当一个定时器处于主模式时,它可以对另一个处于从模式的定时器的计数器进行复位、启动、停止或提供时钟等操作。


配置定时器1为主模式,它可以在每一个更新事件UEV时输出一个周期性的触发信号。在TIM1_CR2寄存器的MMS='010’时,每当产生一个更新事件时在TRGO1上输出一个上升沿信号。

连接定时器1的TRGO1输出至定时器2,设置TIM2_SMCR寄存器的TS =‘000’,配置定时器2为使用ITR1作为内部触发的从模式。(为什么是‘000’,硬件底层已经根据不同选择定义好了)


然后把从模式控制器置于外部时钟模式1(TIM2 SMCR寄存器的SMS-111):这样定时器2即可由定时器1周期性的上升沿(即定时器1的计数器溢出)信号驱动。

最后,必须设置相应(TIMx_CR1寄存器)的CEN位分别启动两个定时器。如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。
注:如果OCx已被选中为定时器1的触发输出(MMS=1xx),它的上升沿用于驱动定时器2的计数器。
这一段内容是涉及参考手册14.3.15的内容,关于这个模式还有更多功能,比如:使用一个定时器使能另一个定时器;使用一个定时器去启动另一个定时器;使用一个定时器作为另一个的预分频器;使用一个外部触发同步地启动2个定时器,感兴趣的可以自己去了解

时序图

高级定时器

 

 

定时器常用函数

/**
 * @brief  复位指定的TIM外设寄存器到其默认状态。
 * @param  TIMx: 指定要复位的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @retval 无
 * @note   该函数将指定TIM的寄存器恢复到复位后的默认值。
 */
void TIM_DeInit(TIM_TypeDef* TIMx);
/**
 * @brief  配置TIM外设的通道1作为输出比较。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_OCInitStruct: 指向TIM_OCInitTypeDef结构的指针,
 *         该结构包含了指定TIM外设的通道1的配置信息。
 * @retval 无
 * @note   该函数用于初始化TIM外设的通道1为输出比较模式。
 */
void TIM_OC1Init(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct) {
    /* 检查参数 */
    assert_param(IS_TIM_ALL_PERIPH(TIMx));
    assert_param(IS_TIM_OC_MODE(TIM_OCInitStruct->TIM_OCMode));
    assert_param(IS_TIM_OUTPUT_STATE(TIM_OCInitStruct->TIM_OutputState));
    assert_param(IS_TIM_OUTPUTN_STATE(TIM_OCInitStruct->TIM_OutputNState));
    assert_param(IS_TIM_OC_POLARITY(TIM_OCInitStruct->TIM_OCPolarity));
    assert_param(IS_TIM_OCN_POLARITY(TIM_OCInitStruct->TIM_OCNPolarity));
    assert_param(IS_TIM_OCIDLE_STATE(TIM_OCInitStruct->TIM_OCIdleState));
    assert_param(IS_TIM_OCNIDLE_STATE(TIM_OCInitStruct->TIM_OCNIdleState));

    /* 禁用通道1: 重置CC1E Bit */
    TIMx->CCER &= (uint16_t)(~TIM_CCER_CC1E);
    
    /* 设置输出比较模式 */
    TIMx->CCMR1 &= (uint16_t)(~TIM_CCMR1_OC1M);
    TIMx->CCMR1 |= TIM_OCInitStruct->TIM_OCMode;
    
    /* 设置输出状态 */
    TIMx->CCER &= (uint16_t)(~TIM_CCER_CC1P);
    TIMx->CCER |= TIM_OCInitStruct->TIM_OCPolarity;
    TIMx->CCER |= TIM_OCInitStruct->TIM_OutputState;
    
    /* 设置捕获/比较1寄存器值 */
    TIMx->CCR1 = TIM_OCInitStruct->TIM_Pulse;
    
    /* 设置输出比较1空闲状态 */
    TIMx->CR2 &= (uint16_t)(~TIM_CR2_OIS1);
    TIMx->CR2 |= TIM_OCInitStruct->TIM_OCIdleState;
}


/**
 * @brief  初始化TIM外设的输入捕获通道。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_ICInitStruct: 指向TIM_ICInitTypeDef结构体的指针,
 *         该结构体包含输入捕获通道的配置信息,如通道、捕获极性、捕获预分频器、滤波器等。
 * @retval 无
 * @note   该函数用于配置输入捕获模式、极性、滤波器等参数。
 */
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);

/**
 * @brief  初始化TIM_TimeBaseInitTypeDef结构体的每一个成员为默认值。
 * @param  TIM_TimeBaseInitStruct: 指向TIM_TimeBaseInitTypeDef结构体的指针,
 *         该结构体将被初始化为默认值。
 * @retval 无
 * @note   该函数用于快速设置结构体的默认值,方便用户进行后续的修改配置。
 */
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);

/**
 * @brief  启用或禁用指定的TIM中断。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_IT: 指定要启用或禁用的TIM中断源。
 *         这个参数可以是一个或多个以下值的组合:
 *           @arg TIM_IT_Update: TIM更新中断
 *           @arg TIM_IT_CC1: 捕获比较1中断
 *           @arg TIM_IT_CC2: 捕获比较2中断
 *           @arg TIM_IT_CC3: 捕获比较3中断
 *           @arg TIM_IT_CC4: 捕获比较4中断
 *           @arg TIM_IT_COM: COM中断
 *           @arg TIM_IT_Trigger: 触发中断
 *           @arg TIM_IT_Break: 断裂中断
 * @param  NewState: TIM中断的新状态。
 *         这个参数可以是: ENABLE 或 DISABLE。
 * @retval 无
 * @note   该函数用于控制TIM中断的启用和禁用。
 */
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

/**
 * @brief  配置TIM的时钟源为内部时钟。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @retval 无
 * @note   该函数用于选择内部时钟作为TIM的时钟源。
 */
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);

/**
 * @brief  配置TIM的时钟源为ITRx外部时钟。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_InputTriggerSource: 指定ITRx外部时钟的输入触发源。
 *         这个参数可以是以下值之一:
 *           @arg TIM_TS_ITR0: 内部触发器0
 *           @arg TIM_TS_ITR1: 内部触发器1
 *           @arg TIM_TS_ITR2: 内部触发器2
 *           @arg TIM_TS_ITR3: 内部触发器3
 * @retval 无
 * @note   该函数用于选择ITRx作为TIM的外部时钟源。
 */
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);

/**
 * @brief  配置TIM的时钟源为TIx外部时钟。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_TIxExternalCLKSource: 指定TIx外部时钟的输入源。
 *         这个参数可以是以下值之一:
 *           @arg TIM_TIxExternalCLK1Source_TI1: TI1触发输入源
 *           @arg TIM_TIxExternalCLK1Source_TI2: TI2触发输入源
 *           @arg TIM_TIxExternalCLK1Source_TI1ED: TI1边沿检测输入源
 * @param  TIM_ICPolarity: 指定输入捕获的极性。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ICPolarity_Rising: 上升沿捕获
 *           @arg TIM_ICPolarity_Falling: 下降沿捕获
 * @param  ICFilter: 输入捕获的滤波器值。
 *         这个参数必须是0x0到0xF之间的值。
 * @retval 无
 * @note   该函数用于选择TIx作为TIM的外部时钟源。
 */
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);

/**
 * @brief  配置TIM的外部触发模式1。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_ExtTRGPrescaler: 指定外部触发输入的预分频器。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPSC_OFF: 外部触发输入无预分频
 *           @arg TIM_ExtTRGPSC_DIV2: 外部触发输入预分频2
 *           @arg TIM_ExtTRGPSC_DIV4: 外部触发输入预分频4
 *           @arg TIM_ExtTRGPSC_DIV8: 外部触发输入预分频8
 * @param  TIM_ExtTRGPolarity: 指定外部触发输入的极性。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPolarity_Inverted: 反向极性
 *           @arg TIM_ExtTRGPolarity_NonInverted: 非反向极性
 * @param  ExtTRGFilter: 指定外部触发输入的滤波器值。
 *         这个参数必须是0x0到0xF之间的值。
 * @retval 无
 * @note   该函数用于配置外部触发模式1。
 */
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);

/**
 * @brief  配置TIM的外部触发模式2。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_ExtTRGPrescaler: 指定外部触发输入的预分频器。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPSC_OFF: 外部触发输入无预分频
 *           @arg TIM_ExtTRGPSC_DIV2: 外部触发输入预分频2
 *           @arg TIM_ExtTRGPSC_DIV4: 外部触发输入预分频4
 *           @arg TIM_ExtTRGPSC_DIV8: 外部触发输入预分频8
 * @param  TIM_ExtTRGPolarity: 指定外部触发输入的极性。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPolarity_Inverted: 反向极性
 *           @arg TIM_ExtTRGPolarity_NonInverted: 非反向极性
 * @param  ExtTRGFilter: 指定外部触发输入的滤波器值。
 *         这个参数必须是0x0到0xF之间的值。
 * @retval 无
 * @note   该函数用于配置外部触发模式2。
 */
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);

/**
 * @brief  配置TIM的外部触发输入。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_ExtTRGPrescaler: 指定外部触发输入的预分频器。
 *         这个参数可以是以下值之一:
 *
/**
 * @brief  配置TIM的外部触发输入。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_ExtTRGPrescaler: 指定外部触发输入的预分频器。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPSC_OFF: 外部触发输入无预分频
 *           @arg TIM_ExtTRGPSC_DIV2: 外部触发输入预分频2
 *           @arg TIM_ExtTRGPSC_DIV4: 外部触发输入预分频4
 *           @arg TIM_ExtTRGPSC_DIV8: 外部触发输入预分频8
 * @param  TIM_ExtTRGPolarity: 指定外部触发输入的极性。
 *         这个参数可以是以下值之一:
 *           @arg TIM_ExtTRGPolarity_Inverted: 反向极性
 *           @arg TIM_ExtTRGPolarity_NonInverted: 非反向极性
 * @param  ExtTRGFilter: 指定外部触发输入的滤波器值。
 *         这个参数必须是0x0到0xF之间的值。
 * @retval 无
 * @note   该函数用于配置外部触发输入的相关参数,包括预分频器、极性和滤波器。
 */
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,
                   uint16_t ExtTRGFilter);

/**
 * @brief  配置TIM的预分频器。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  Prescaler: 指定TIM的预分频器值。
 *         这个参数可以是0x0000到0xFFFF之间的任何值。
 * @param  TIM_PSCReloadMode: 指定预分频器的重载模式。
 *         这个参数可以是以下值之一:
 *           @arg TIM_PSCReloadMode_Update: 在下一个更新事件时重载
 *           @arg TIM_PSCReloadMode_Immediate: 立即重载
 * @retval 无
 * @note   该函数用于设置TIM的预分频器值和重载模式。
 */
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);

/**
 * @brief  配置TIM的计数模式。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_CounterMode: 指定TIM的计数模式。
 *         这个参数可以是以下值之一:
 *           @arg TIM_CounterMode_Up: 向上计数模式
 *           @arg TIM_CounterMode_Down: 向下计数模式
 *           @arg TIM_CounterMode_CenterAligned1: 中心对齐模式1
 *           @arg TIM_CounterMode_CenterAligned2: 中心对齐模式2
 *           @arg TIM_CounterMode_CenterAligned3: 中心对齐模式3
 * @retval 无
 * @note   该函数用于设置TIM的计数模式,如向上计数、向下计数或中心对齐模式。
 */
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);



/**
 * @brief  设置TIM的计数器值。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  Counter: 指定TIM的计数器值。
 *         这个参数必须是0x0000到0xFFFF之间的值。
 * @retval 无
 * @note   该函数用于直接设置TIM的计数器当前值。
 */
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);

/**
 * @brief  设置TIM的自动重装载值。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  Autoreload: 指定TIM的自动重装载值。
 *         这个参数必须是0x0000到0xFFFF之间的值。
 * @retval 无
 * @note   该函数用于设置TIM的自动重装载值,计数器计数到该值后会重新计数。
 */
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);

/**
 * @brief  设置TIM的比较值1。
 * @param  TIMx: 指定要配置的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  Compare1: 指定TIM的比较值1。
 *         这个参数必须是0x0000到0xFFFF之间的值。
 * @retval 无
 * @note   该函数用于设置TIM的比较值1,用于输出比较功能。
 */
void TIM_SetCompare1(TIM_TypeDef* TIMx, uint16_t Compare1);

/**
 * @brief  获取TIM的计数器值。
 * @param  TIMx: 指定要读取的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @retval TIM的当前计数器值。
 * @note   该函数用于获取TIM的当前计数器值。
 */
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);

/**
 * @brief  获取TIM的预分频器值。
 * @param  TIMx: 指定要读取的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @retval TIM的当前预分频器值。
 * @note   该函数用于获取TIM的当前预分频器值。
 */
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);

/**
 * @brief  检查指定的TIM标志是否已设置。
 * @param  TIMx: 指定要检查的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_FLAG: 指定要检查的标志。
 *         这个参数可以是以下值之一:
 *           @arg TIM_FLAG_Update: 更新中断标志
 *           @arg TIM_FLAG_CC1: 捕获比较1中断标志
 *           @arg TIM_FLAG_CC2: 捕获比较2中断标志
 *           @arg TIM_FLAG_CC3: 捕获比较3中断标志
 *           @arg TIM_FLAG_CC4: 捕获比较4中断标志
 *           @arg TIM_FLAG_COM: COM中断标志
 *           @arg TIM_FLAG_Trigger: 触发中断标志
 *           @arg TIM_FLAG_Break: 断裂中断标志
 *           @arg TIM_FLAG_CC1OF: 捕获比较1过捕获标志
 *           @arg TIM_FLAG_CC2OF: 捕获比较2过捕获标志
 *           @arg TIM_FLAG_CC3OF: 捕获比较3过捕获标志
 *           @arg TIM_FLAG_CC4OF: 捕获比较4过捕获标志
 * @retval FlagStatus: TIM_FLAG的状态,可以是SET或RESET。
 * @note   该函数用于检查指定的TIM标志是否已设置。
 */
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
 * 断中断标志
 *           @arg TIM_FLAG_CC1OF: 捕获比较1过捕获标志
 *           @arg TIM_FLAG_CC2OF: 捕获比较2过捕获标志
 *           @arg TIM_FLAG_CC3OF: 捕获比较3过捕获标志
 *           @arg TIM_FLAG_CC4OF: 捕获比较4过捕获标志
 * @retval 无
 * @note   该函数用于清除指定的TIM标志。
 */
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

/**
 * @brief  检查指定的TIM中断状态。
 * @param  TIMx: 指定要检查的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_IT: 指定要检查的中断源。
 *         这个参数可以是以下值之一:
 *           @arg TIM_IT_Update: 更新中断
 *           @arg TIM_IT_CC1: 捕获比较1中断
 *           @arg TIM_IT_CC2: 捕获比较2中断
 *           @arg TIM_IT_CC3: 捕获比较3中断
 *           @arg TIM_IT_CC4: 捕获比较4中断
 *           @arg TIM_IT_COM: COM中断
 *           @arg TIM_IT_Trigger: 触发中断
 *           @arg TIM_IT_Break: 断裂中断
 * @retval ITStatus: TIM_IT的状态,可以是SET或RESET。
 * @note   该函数用于检查指定的TIM中断是否发生。
 */
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);

/**
 * @brief  清除TIM的中断挂起位。
 * @param  TIMx: 指定要操作的TIM外设。
 *         这个参数可以是TIM1到TIM17的任意一个。具体TIMx取决于具体的微控制器型号。
 * @param  TIM_IT: 指定要清除的中断源。
 *         这个参数可以是以下值的组合:
 *           @arg TIM_IT_Update: 更新中断
 *           @arg TIM_IT_CC1: 捕获比较1中断
 *           @arg TIM_IT_CC2: 捕获比较2中断
 *           @arg TIM_IT_CC3: 捕获比较3中断
 *           @arg TIM_IT_CC4: 捕获比较4中断
 *           @arg TIM_IT_COM: COM中断
 *           @arg TIM_IT_Trigger: 触发中断
 *           @arg TIM_IT_Break: 断裂中断
 * @retval 无
 * @note   该函数用于清除指定的TIM中断挂起位。
 */
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);

TIMER案例

1.启用TIM2内部时钟 RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

2.配置TIM2的基础时间参数TIM_TimeBaseStructInit();

计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1)   = CK_PSC / (PSC + 1) / (ARR + 1)

时间=频率的倒数

3. 启用TIM2的中断输出控制    TIM_ITConfig();

4中断分组 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  

5.配置NVIC Nvic_Init();

6.使能TIM外设TIM_Cmd();

如果要设置PWM则需要配置输出比较通道

TIM_OC1Init(TIM2, &TIM_OCInitStructure);     //将结构体变量交给TIM_OC1Init,配置TIM2的输出比较通道1

TIM_SetCompare1(TIM2, Compare);        //设置CCR1的值

定时器内部时钟

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	
	/*配置时钟源*/
	TIM_InternalClockConfig(TIM2);		//选择TIM2为内部时钟,若不调用此函数,TIM默认也为内部时钟
	
	/*时基单元初始化*/
	//定义结构体变量
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;	
//时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能	
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
	
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	
	TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;				//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;			//重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);				//将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
	
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

 //定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

定时器外部时钟

#include "stm32f10x.h"                  // Device header

/**
  * 函    数:定时中断初始化
  * 参    数:无
  * 返 回 值:无
  * 注意事项:此函数配置为外部时钟,定时器相当于计数器
  */
void Timer_Init(void)
{
	/*开启时钟*/
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);			//开启TIM2的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);			//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);						//将PA0引脚初始化为上拉输入
	
	/*外部时钟配置*/
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
																//选择外部时钟模式2,时钟从TIM_ETR引脚输入
																//注意TIM2的ETR引脚固定为PA0,无法随意更改
																//最后一个滤波器参数加到最大0x0F,可滤除时钟信号抖动
	
	/*时基单元初始化*/
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;				//定义结构体变量
	TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;		//时钟分频,选择不分频,此参数用于配置滤波器时钟,不影响时基单元功能
	TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;	//计数器模式,选择向上计数
	TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;					//计数周期,即ARR的值
	TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;				//预分频器,即PSC的值
	TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;			//重复计数器,高级定时器才会用到
	TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);				//将结构体变量交给TIM_TimeBaseInit,配置TIM2的时基单元	
	
	/*中断输出配置*/
	TIM_ClearFlag(TIM2, TIM_FLAG_Update);						//清除定时器更新标志位
																//TIM_TimeBaseInit函数末尾,手动产生了更新事件
																//若不清除此标志位,则开启中断后,会立刻进入一次中断
																//如果不介意此问题,则不清除此标志位也可
																
	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);					//开启TIM2的更新中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);				//配置NVIC为分组2
																//即抢占优先级范围:0~3,响应优先级范围:0~3
																//此分组配置在整个工程中仅需调用一次
																//若有多个中断,可以把此代码放在main函数内,while循环之前
																//若调用多次配置分组的代码,则后执行的配置会覆盖先执行的配置
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;						//定义结构体变量
	NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;				//选择配置NVIC的TIM2线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;				//指定NVIC线路使能
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;	//指定NVIC线路的抢占优先级为2
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;			//指定NVIC线路的响应优先级为1
	NVIC_Init(&NVIC_InitStructure);								//将结构体变量交给NVIC_Init,配置NVIC外设
	
	/*TIM使能*/
	TIM_Cmd(TIM2, ENABLE);			//使能TIM2,定时器开始运行
}

/**
  * 函    数:返回定时器CNT的值
  * 参    数:无
  * 返 回 值:定时器CNT的值,范围:0~65535
  */
uint16_t Timer_GetCounter(void)
{
	return TIM_GetCounter(TIM2);	//返回定时器TIM2的CNT
}

// 定时器中断函数,可以复制到使用它的地方
void TIM2_IRQHandler(void)
{
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
	{
		
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

编码器接口

那使用正交信号相比较单独定义一个方向引脚,有什么好处呢?
首先就是正交信号精度更高,因为A、B相都可以计次,相当于计次频率提高了一倍;其次就是正交信号可以抗噪声,因为正交信号,两个信号必须是交替跳变的,所以可以设计一个抗噪声电路。如果一个信号不变,另一个信号连续跳变,也就是产生了噪声,那这时计次值是不会变化的。

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

注意使用编码器模式的时候,我们之前一直在使用的72MHz内部时钟,和我们在时基单元初始化时设置的计数方向,并不会使用。因为此时计数时钟和计数方向都处于编码器接口托管的状态,计数器的自增和自减,受编码器控制.

然后我们看一下这里,我给出的一个编码器接口基本结构。

输入捕获的前两个通道,通过GPIO口接入编码器的A、B相,然后通过滤波器和边沿检测极性选择 ,产生TI1FP1和TI2FP2,通向编码器接口。编码器接口通过预分频器控制CNT计数器的时钟,同时,编码器接口还根据编码器的旋转方向,控制CNT的计数方向,编码器正转时,CNT自增,编码器反转时,CNT自减。
另外这里ARR也是有效的,一般我们会设置ARR为65535,最大量程,这样的话,利用补码的特性,很容易得到负数。比如CNT初始为0,我正转,CNT自增,0、 1、2、3、4、5、6、7等等,显示都没问题,但是我反转呢,CNT自减,0下一个数就是65535,接着是65534、65533等等这里负数不应该是-1、-2吗,65535是不是就出问题了。但是没关系,直接把这个16位的无符号数转换为16位的有符号数。根据补码的定义,这个65535就对应-1,65534就对应-2(有符号编码时负数按补码计算,2^16 的补码= -1)等等,这样就可以直接得到负数,非常方便,这就是我们读出数据得到负数的一个小技巧。

最后我们来看一些工作细节,和两个小例子。
这个工作描述的表,描述的就是我们刚才说什么时候正转、反转的,编码器接口的工作逻辑

这个实例展示的是极性的变化对计数的影响。
TI1反相是什么意思呢?

此时看下这个图,这里TI1和TI2进来,都会经过这个极性选择的部分。

在输入捕获模式下,这个极性选择是选择上升没有效还是下降沿有效的。但是根据我们刚才的分析,编码器接口,显然始终都是上升沿和下降沿都有效的,上升沿和下降沿都需要计次,所以在编码器接口模式下,这里就不再是边沿的极性选择了而是高低电平的极性选择。如果我们选择上升沿的参数,就是信号直通过来,高低电平极性不反转;如果选择下降沿的参数,就是信号通过一个非门过来,高低电平极性反转,所以这里就会有两个控制极性的参数,选择要不要在这里加一个非门,反转一下极性。

显然,这两个实例图的计数方向是相反的,这有什么作用呢?
比如你接一个编码器,发现它数据的加减方向反了,你想要正转的方向,结果它自减了,你想要反转的方向,结果它自增了,这时,就可以调整一下极性,把任意一个引脚反相,就能反转计数方向了。当然如果想改变计数方向的话,我们还可以直接把A、B相两个引脚换一下。

RCC时钟树

三种不同的时钟源可被用来驱动系统时钟(SYSCLK):

● HSI振荡器时钟

● HSE振荡器时钟

#elif defined SYSCLK_FREQ_72MHz
/**
  * @brief  将系统时钟频率设置为72MHz,并配置HCLK、PCLK2和PCLK1的分频器。
  * @note   此函数应在复位后使用。
  * @param  无
  * @retval 无
  */
static void SetSysClockTo72(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;
  
  /* SYSCLK, HCLK, PCLK2 和 PCLK1 配置 ---------------------------*/    
  /* 启用HSE(高速外部时钟) */    
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);
 
  /* 等待HSE准备就绪,如果超时则退出 */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;  // 检查HSE是否准备就绪
    StartUpCounter++;  // 启动计数器自增
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));  // 如果HSE未就绪且未超时,则继续等待

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;  // HSE已准备就绪
  }
  else
  {
    HSEStatus = (uint32_t)0x00;  // HSE未能启动
  }  

  if (HSEStatus == (uint32_t)0x01)
  {
    /* 启用预取缓冲区 */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* 设置Flash等待状态为2个等待周期 */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;    

    /* 配置AHB时钟(HCLK)为系统时钟 */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
      
    /* 配置APB2时钟(PCLK2)为HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
    
    /* 配置APB1时钟(PCLK1)为HCLK的一半 */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

#ifdef STM32F10X_CL
    /* 配置PLL(相位锁定环)----------------------------------------------*/
    /* PLL2配置:PLL2CLK = (HSE / 5) * 8 = 40 MHz */
    /* PREDIV1配置:PREDIV1CLK = PLL2 / 5 = 8 MHz */
        
    RCC->CFGR2 &= (uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
                              RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
    RCC->CFGR2 |= (uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
                             RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);
  
    /* 启用PLL2 */
    RCC->CR |= RCC_CR_PLL2ON;
    /* 等待PLL2准备就绪 */
    while((RCC->CR & RCC_CR_PLL2RDY) == 0)
    {
    }
    
    /* 配置主PLL:PLLCLK = PREDIV1 * 9 = 72 MHz */ 
    RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 | 
                            RCC_CFGR_PLLMULL9); 
#else    
    /* 配置PLL:PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

    /* 启用PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* 等待PLL准备就绪 */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }
    
    /* 选择PLL作为系统时钟源 */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;    

    /* 等待PLL被用作系统时钟源 */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }
  }
  else
  { 
    /* 如果HSE启动失败,应用程序将有错误的时钟配置。
       用户可以在此处添加一些代码来处理此错误 */
  }
}







#include "rccclkconfig.h"
#include "stm32f10x.h"
void HSE_SetSysClk(uint32_t RCC_PLLMul_x)
{
	ErrorStatus  HSEStatus;
	RCC_DeInit();
	RCC_HSEConfig(RCC_HSE_ON);
	HSEStatus = RCC_WaitForHSEStartUp();
	if(HSEStatus == SUCCESS)
	{
		 /* 启用预取缓冲区 */
		FLASH_PrefetchBufferCmd(FLASH_PrefetchBuffer_Enable);
		/* 设置Flash等待状态为2个等待周期 */
		FLASH_SetLatency(FLASH_Latency_2);
		 /* 配置AHB时钟(HCLK)为系统时钟 */
		RCC_HCLKConfig(RCC_SYSCLK_Div1);
    RCC_PCLK1Config(RCC_SYSCLK_Div2);
		RCC_PCLK2Config(RCC_SYSCLK_Div1);
	 /* 配置PLL:PLLCLK = HSE * 9 = 72 MHz */
	 RCC_PLLConfig(RCC_PLLSource_HSE_Div1,RCC_PLLMul_x);
    /* 启用PLL */
	RCC_PLLCmd(ENABLE);
	  /* 等待PLL准备就绪 */
    while(RCC_GetFlagStatus(RCC_FLAG_PLLRDY)== SET)
    {
			//选择系统时钟
		RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK);
     while(RCC_GetSYSCLKSource()!=0x08)
			{
				
			}
    }
	}
		 else
  { 
    /* 如果HSE启动失败,应用程序将有错误的时钟配置。
       用户可以在此处添加一些代码来处理此错误 */
  }
}

● PLL时钟

这些设备有以下2种二级时钟源:

● 40kHz低速内部RC,可以用于驱动独立看门狗和通过程序选择驱动RTC。RTC用于从停机/ 待机模式下自动唤醒系统。

● 32.768kHz低速外部晶体也可用来通过程序选择驱动RTC(RTCCLK)。 当不被使用时,任一个时钟源都可被独立地启动或关闭,由此优化系统功耗。

HSI RC 高速8HZ RC振荡器

HSI OSC 高速4-16HZ 石英振荡器,也就是晶振 经过PPLMUL倍频9=72HZ

LSE OSE 外部32.768HZ低速晶振,一般给RTC提供时钟

LSI RC 40HZ低速RC振荡器 给看门狗提供时钟

外设时钟使能   就是RCC_APBXPeriphClockCmd()

6-1 TIM定时中断 47:00 rcc时钟树

#elif defined SYSCLK_FREQ_72MHz
/**
  * @brief  将系统时钟频率设置为72MHz,并配置HCLK、PCLK2和PCLK1的分频器。
  * @note   此函数应在复位后使用。
  * @param  无
  * @retval 无
  */
static void SetSysClockTo72(void)
{
  __IO uint32_t StartUpCounter = 0, HSEStatus = 0;
  
  /* SYSCLK, HCLK, PCLK2 和 PCLK1 配置 ---------------------------*/    
  /* 启用HSE(高速外部时钟) */    
  RCC->CR |= ((uint32_t)RCC_CR_HSEON);
 
  /* 等待HSE准备就绪,如果超时则退出 */
  do
  {
    HSEStatus = RCC->CR & RCC_CR_HSERDY;  // 检查HSE是否准备就绪
    StartUpCounter++;  // 启动计数器自增
  } while((HSEStatus == 0) && (StartUpCounter != HSE_STARTUP_TIMEOUT));  // 如果HSE未就绪且未超时,则继续等待

  if ((RCC->CR & RCC_CR_HSERDY) != RESET)
  {
    HSEStatus = (uint32_t)0x01;  // HSE已准备就绪
  }
  else
  {
    HSEStatus = (uint32_t)0x00;  // HSE未能启动
  }  

  if (HSEStatus == (uint32_t)0x01)
  {
    /* 启用预取缓冲区 */
    FLASH->ACR |= FLASH_ACR_PRFTBE;

    /* 设置Flash等待状态为2个等待周期 */
    FLASH->ACR &= (uint32_t)((uint32_t)~FLASH_ACR_LATENCY);
    FLASH->ACR |= (uint32_t)FLASH_ACR_LATENCY_2;    

    /* 配置AHB时钟(HCLK)为系统时钟 */
    RCC->CFGR |= (uint32_t)RCC_CFGR_HPRE_DIV1;
      
    /* 配置APB2时钟(PCLK2)为HCLK */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE2_DIV1;
    
    /* 配置APB1时钟(PCLK1)为HCLK的一半 */
    RCC->CFGR |= (uint32_t)RCC_CFGR_PPRE1_DIV2;

#ifdef STM32F10X_CL
    /* 配置PLL(相位锁定环)----------------------------------------------*/
    /* PLL2配置:PLL2CLK = (HSE / 5) * 8 = 40 MHz */
    /* PREDIV1配置:PREDIV1CLK = PLL2 / 5 = 8 MHz */
        
    RCC->CFGR2 &= (uint32_t)~(RCC_CFGR2_PREDIV2 | RCC_CFGR2_PLL2MUL |
                              RCC_CFGR2_PREDIV1 | RCC_CFGR2_PREDIV1SRC);
    RCC->CFGR2 |= (uint32_t)(RCC_CFGR2_PREDIV2_DIV5 | RCC_CFGR2_PLL2MUL8 |
                             RCC_CFGR2_PREDIV1SRC_PLL2 | RCC_CFGR2_PREDIV1_DIV5);
  
    /* 启用PLL2 */
    RCC->CR |= RCC_CR_PLL2ON;
    /* 等待PLL2准备就绪 */
    while((RCC->CR & RCC_CR_PLL2RDY) == 0)
    {
    }
    
    /* 配置主PLL:PLLCLK = PREDIV1 * 9 = 72 MHz */ 
    RCC->CFGR &= (uint32_t)~(RCC_CFGR_PLLXTPRE | RCC_CFGR_PLLSRC | RCC_CFGR_PLLMULL);
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLXTPRE_PREDIV1 | RCC_CFGR_PLLSRC_PREDIV1 | 
                            RCC_CFGR_PLLMULL9); 
#else    
    /* 配置PLL:PLLCLK = HSE * 9 = 72 MHz */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_PLLSRC | RCC_CFGR_PLLXTPRE |
                                        RCC_CFGR_PLLMULL));
    RCC->CFGR |= (uint32_t)(RCC_CFGR_PLLSRC_HSE | RCC_CFGR_PLLMULL9);
#endif /* STM32F10X_CL */

    /* 启用PLL */
    RCC->CR |= RCC_CR_PLLON;

    /* 等待PLL准备就绪 */
    while((RCC->CR & RCC_CR_PLLRDY) == 0)
    {
    }
    
    /* 选择PLL作为系统时钟源 */
    RCC->CFGR &= (uint32_t)((uint32_t)~(RCC_CFGR_SW));
    RCC->CFGR |= (uint32_t)RCC_CFGR_SW_PLL;    

    /* 等待PLL被用作系统时钟源 */
    while ((RCC->CFGR & (uint32_t)RCC_CFGR_SWS) != (uint32_t)0x08)
    {
    }
  }
  else
  { 
    /* 如果HSE启动失败,应用程序将有错误的时钟配置。
       用户可以在此处添加一些代码来处理此错误 */
  }
}

ADC(模拟数字转换器)

ADC的简介

逐次逼近型这是这个ADC的工作模式。然后12位和1us的转换时间,这里就涉及到ADC的两个关键参数了,第一个是分辨率一般用多少位来表示,12位AD值,它的表示范围就是0-2^12-1,就是量化结果的范围是0~4095。位数越高,量化结果就越精细,对应分辨率就越高;第二个是转换时间,就是转换频率,AD转换是需要花一小段时间的,这里1us就表示从AD转换开始到产生结果,需要花1us的时间,对应AD转换的频率就是1MHz,这个就是STM32 ADC的最快转换频率。如果你需要转换一个频率非常高的信号,那就要考虑一下这个转换频率是不是够用,如果你的信号频率比较低,那这个最大1MHz的转换频率也完全够用了。

 

外部信号源就是16个GPIO口,在引脚上直接接模拟信号就行了,不需要任何额外的电路,引脚就直接能测电压。2个内部信号源是内部温度传感器和内部参考电压。温度传感器可以测量CPU的温度,比如你电脑可以显示一个CPU温度,就可以用ADC读取这个温度传感器来测量;内部参考电压是一个1.2V左右的基淮电压,这个基准电压是不随外部供电电压变化而变化的,所以如果你芯片的供电不是标准的3.3V,那测量外部引脚的电压可能就不对,这时就可以读取这个基准电压进行校准,这样就能得到正确的电压值了。

规则组和注入组两个转换单元,这个就是STM32 ADC的增强功能了。普通的AD转换流程是,启动一次转换、读一次值,然后再启动、再读值,这样的流程。但是STM32的ADC就比较高级,可以列一个组,一次性启动一个组,连续转换多个值。并且有两个组,一个是用于常规使用的规则组,一个是用于突发事件的注入组。

模拟看门狗自动监测输入电压范围,这个ADC,一般可以用于测量光线强度、温度这些值,并且经常会有个需求,就是如果光线高于某个阈值、低于某个阈值或者温度高于某个阈值、低于某个阈值时,执行一些操作。这个高于某个阈值、低于某个阈值的判断,就可以用模拟看门狗来自动执行。模拟看门狗可以监测指定的某些通道,当AD值高于它设定的上阈值或者低于下阈值时,它就会申请中断,你就可以在中断函数里执行相应的操作,这样你就不用不断地手动读值,再用if进行判断了。

ADC可以将模拟信号转换为数字信号,是模拟电路到数字电路的桥梁。那反过来,有模拟到数字的桥梁,那肯定就有数字到模拟的桥梁。这就是DAC,数字模拟转换器,使用DAC就可以将数字变量转化为模拟电压。
不过在上一节,我们还学到了一个数字到模拟的桥梁,PWM。上一节我们使用PWM来控制LED的亮度、电机的速度,这就是DAC的功能,同时PWM只有完全导通和完全断开两种状态,在这两种状态上都没有功率损耗。所以在直流电机调速这种大功率的应用场景,使用PWM来等效模拟量,是比DAC更好的选择,并且PWM电路更加简单,更加常用。所以可以看出PWM还是挤占了DAC的很多应用空间。
目前DAC的应用主要是在波形生成这些领域,比如信号发生器、音频解码芯片等,这些领域PWM还是不好替代的。

接下来我们来了解一下这个逐次逼近型ADC到底是怎么测电压的,我们看一下这个图,这就是逐次逼近型ADC的内部结构。了解这个结构对你学习STM32的ADC有很大帮助,因为STM32的ADC原理和这个是一样的,但是STM32只画了一个框表示ADC,并没有描述内部结构,所以我们先介绍一下这个结构,这样再理解STM32的ADC就会简单一些了。
我们来看一下,这个图是ADCO809的内部结构图,它是一个独立的8位逐次逼近型ADC芯片。在以前单片性能不太好的时候,是通过外挂一个ADC芯片才能进行AD转换,这个ADCO809就是一款比较经典的ADC芯片。随着单片机的性能和集成度都有很大的提升,很多单片机内部就已经集成了ADC外设。

逐次逼近型ADC

首先左边这里INO~IN7,是8路输入通道,通过通道选择开关,选中一路,输入到所标点进行转换。
下面这里是地址锁存和译码,就是你想选中哪个通道,就把通道号放在这三个脚(ADD…)上,然后给一个锁存信号(ALU),上面这里对应的通路开关就可以自动拨好了。这部分就相当于一个可以通过模拟信号的数据选择器。
因为ADC转换是一个很快的过程,你给个开始信号,过几个us就转换完成了。所以说如果你想转换多路信号,那不必设计多个AD转换器,只需要一个AD转换器,然后加一个多路选择开关,想转换哪一路,就先拨一下开关,选中对应通道,然后再开始转换就行了。这就是这个输入通道选择的部分,这个ADC0809只有8个输入通道,我们STM32内部的ADC是有18个输入通道的,所以对应输入电路,就是一个18路输入的多路开关

74HC138译码器

核心结构


那然后输入信号选好了,到这里(所标红点)来,怎么才能知道这个电压对应的编码数据是多少呢?这就需要我们用逐次逼近的方法来——比较了

首先这是一个电压比较器,它可以判断两个输入信号电压的大小关系,输出一个高低电平指示谁大谁小。它的两个输入端,一个是待测的电压,另一个是这里DAC的电压输出端,DAC是数模转换器。我们之前说过了,给它一个数据,它就可以输出数据对应的电压,DAC内部是使用加权电阻网络来实现的转换,具体可以江科大51单片机教程里的AD/DA那一节。
那现在,我们有了一个外部通道输入的未知编码的电压,和一个DAC输出的已知编码的电压。它俩同时输入到电压比较器,进行大小判断,如果DAC输出的电压比较大,我就调小DAC数据;如果DAC输出的电压比较小,我就增大DAC数据,直到DAC输出的电压和外部通道输入的电压近似相等 ,这样DAC输入的数据就是外部电压的编码数据了,这就是DAC的实现原理。这个电压调节的过程就是这个逐次逼近SAR来完成的。

为了最快找到未知电压的编码,通常我们会使用二分法进行寻找。比如这里是8位的ADC,那编码就是从0~255。第一次比较的时候,我们就给DAC输入255的一半,进行比较,那就是128,然后看看谁大谁小,如果DAC电压大了;第二次比较的时候,再就给128的一半,64,如果还大,第三次比较的时候就给32,如果这次DAC电压小了,那第四次就给32到64中间的值,然后继续,这样依次进行下去,就能最快地找到未知电压的编码。并且这个过程,如果你用二进制来表示的话,你会发现,128、64、32这些数据,正好是二进制每一位的位权,这个判断过程就相当于是,对二进制从高位到低位依次判断是1还是0的过程,这就是逐次逼近型名字的来源。**那对于8位的ADC,从高位到低位依次判断8次就能找到未知电压的编码了,对于12位的ADC,就需要依次判断12次,**这就是逐次逼近的过程。

STM32的ADC

ADC框图

注入规则组和规则通道组:

比喻解释注入组和规则组:
这有什么作用呢?举个例子,这就像是你去餐厅点菜,普通的ADC是,你指定一个菜,老板给你做,然后做好了送给你;这里就是,你指定一个菜单,这个菜单最多可以填16个菜,然后你直接递个菜单给老板,老板就按照菜单的顺序依次做好,一次性给你端上菜,这样的话就可以大大提高效率。当然,你的菜单也可以只写一个菜,这样这个菜单就简化成了普通的模式了。
那对于这个菜单呢,也有两种,一种是规则组菜单,可以同时上16个菜,但是它有个尴尬的地方。就是这个规则组只有一个数据寄存器,就是这个桌子比较小,最多只能放一个菜,你如果上16个菜,那不好意思,前15个菜都会被挤掉些,你只能得到第16个菜。所以对于规则组转换来说,如果使用这个菜单的话,最好配合DMA来实现。DMA是一个数据转运小帮手,它可以在每上一个菜之后,把这个菜挪到其他地方去,防止被覆盖。这个DMA我们下一节就会讲,现在先大概了解一下,那现在我们就知道了,这个规则组虽然可以同时转换16个通道,但是数据寄存器只能存一个结果,如果不想之前的结果被覆盖,那在转换完成之后,就要尽快把结果拿走。
接着我们看一下注入组,这个组就比较高级了,它相当于是餐厅的VIP座位,在这个座位上,一次性最多可以点4个菜,并且这里数据寄存器有4个,是可以同时上4个菜的。对于注入组而言,就不用担心数据覆盖的问题了,这就是规则组和注入组的介绍。
一般情况下,我们使用规则组就完全足够了,如果要使用规则组的菜单,那就再配合DMA转运数据,这样就不用担心数据覆盖的问题了。所以接下来就只讲规则组的操作,注入组涉及的不多,大家可以看手册自行了解。

那我们接着继续看这个模数转换器外围的一些线路

外部触发转换

转换可以由外部事件触发(例如定时器捕获,EXTI线)。如果设置了EXTTRIG控制位,则外部事 件就能够触发转换。EXTSEL[2:0]和JEXTSEL2:0]控制位允许应用程序选择8个可能的事件中的 某一个,可以触发规则和注入组的采样。

注意: 当外部触发信号被选为ADC规则或注入转换时,只有它的上升沿可以启动转换

首先,左下角这里是触发转换的部分,也就是这里的START信号,开始转换。那对于STM32的ADC,触发ADC开始转换的信号有两种,一种是软件触发,就是你在程序中手动调用一条代码,就可以启动转换了另一种是硬件触发,就是这里的这些触发源。上面这些是注入组的触发源,下面这些是规则组的触发源,这些触发源主要是来自于定时器,有定时器的各个通道,还有TRGO定时器主模式的输出,这个之前讲定时器的时候也介绍过。定时器可以通向ADC、 DAC这些外设,用于触发转换。那因为ADC经常需要过一个固定时间段转换一次。比如每隔1ms转换一次,正常的思路就是,用定时器,每隔1ms申请一次中断,在中断里手动开始一次转换,这样也是可以的。但是频繁进中断对我们的程序是有一定影响的,比如你有很多中断都需要频繁进入,那肯定会影响主程序的执行,并且不同中断之间,由于优先级的不同,也会导致某些中断不能及时得到响应。如果触发ADC的中断不能及时响应,那我们ADC的转换频率就肯定会产生影响了。所以对于这种需要频繁进中断,并且在中断里只完成了简单工作的情况,一般都会有硬件的支持。

比如这里,就可以给TIM3定个1ms的时间,并且把TIM3的更新事件选择为TRGO输出,然后在ADC这里,选择开始触发信号为TIM3的TRGO,这样TIM3的更新事件就能通过硬件自动触发ADC转换了。整个过程不需要进中断,节省了中断资源,这就是这里定时器触发的作用。当然这里还可以选择外部中断引脚来触发转换,都可以在程序中配置。这就是触发转化的部分。

然后接着看,左上角这里是VREF+、VREF-、VDDA和VSSA。上面两个是ADC的参考电压,决定了ADC输入电压的范围;下面两个是ADC的供电引脚。一般情况下,VREF+要接VDDA,VREF-要接VSSA,在我们这个芯片上,没有VREF+和VREF-的引脚,它在内部就已经和VDDA和VSSA接在一起了。VDDA和VSSA是内部模拟部分的电源,比如ADC、RC振荡器、锁相环等。在这里VDDA接3.3V, VSSA接GND,所以ADC的输入电压范围就是0~3.3V。

然后继续看 右边这里是ADCCLK是ADC的时钟,也就是这里的CLOCK,是用于驱动内部逐次比较的时钟。这个ADCCLK是来自ADC预分频器,而ADC预分频器是来源于RCC的。

APB2时钟72MHZ,然后通过ADC预分频器进行分频,得到ADCCLK,ADCCLK最大是14MHZ,所以这个预分频器就有点尴尬。它可以选择2、4、6、8分频,如果选择2分频,72M/2=36M,超出允许范围了;4分频之后是18M,也超了,所以对于ADC预分频器只能选择6分频,结果是12M和8分频,结果是9M,这两个值。这个在程序里要注意一下

继续看上面这里是DMA请求,这个就是用于触发DMA进行数据转运的

ADC的基本结构

左边是输入通道,16个GPIO口,外加两个内部的通道,然后进入AD转换器。AD转换器里有两个组,一个是规则组,一个是注入组,规则组最多可以选中16个通道,注入组最多可以选择4个通道。然后转换的结果可以存放在AD数据寄存器里,其中规则组只有1个数据寄存器,注入组有4个。
然后下面这里有触发控制,提供了开始转换这个START信号,触发控制可以选择软件触发和硬件触发。硬件触发主要是来自于定时器,当然也可以选择外部中断的引脚,右边这里是来自于RCC的ADC时钟CLOCK,ADC逐次比较的过程就是由这个时钟推动的。
然后上面,可以布置一个模拟看门狗用于监测转换结果的范围,如果超出设定的阈值,就通过中断输出控制,向NVIC申请中断,另外,规则组和注入组转换完成后会有个EOC信号,它会置一个标志位,当然也可以通向NVIC。最后右下角这里还有个开关控制,在库函数中,就是ADC_Cmd函数,用于给ADC上电的,那这些,就是STM32 ADC的内部结构了。

ADC1和ADC2的引脚全都是相同的,既然都相同,那要ADC2还有啥用呢。这个就要再说一个ADC的高级功能了,就是双ADC模式,,这个模式比较复杂。这里只简单介绍一下,不需要掌握。双ADC模式就是ADC1和ADC2一起工作,它俩可以配合组成同步模式、交叉模式等等模式。比如交叉模式,ADC1和ADC2交叉地对一个通道进行采样,这样就可以进一步提高采样率。

规则组的4种转换模式

1.单次转换,非扫描模式

这里我画了一个列表,这个表就是规则组里的菜单,有16个空位,分别是序列1到序列16,你可以在这里“点菜”,就是写入你要转换的通道,在非扫描的模式下,这个菜单就只有第一个序列1的位置有效,这时,菜单同时选中一组的方式就退化为简单地选中一个的方式了。在这里我们可以在序列1的位置指定我们想转换的通道,比如通道2,写到这个位置。然后,我们就可以触发转换,ADC就会对这个通道2进行模数转换,过一小段时间后,转换完成,转换结果放在数据寄存器里,同时给EOC标志位置1,整个转换过程就结束了。我们判断这个EOC标志位,如果转换完了, 那我们就可以在数据寄存器里读取结果了。如果我们想再启动一次转换,那就需要再触发一次,转换结束,置EOC标志位,读结果。如果想换一个通道转换,那在转换之前,把第一个位置的通道2改成其他通道,然后再启动转换,这样就行了。这就是单次转换,非扫描的转换模式。没有用到这个菜单列表,也是比较简单的一种模式

2.单次转换,扫描模式

这个模式也是单次转换,所以每触发一次,转换结束后,就会停下来,下次转换就得再触发才能开始。然后它是扫描模式,这就会用到这个菜单列表了,你可以在这个菜单里点菜,比如第一个菜是通道2,第二个菜是通道5,等等等等,这里每个位置是通道几可以任意指定,并且也是可以重复的,然后初始化结构体里还会有个参数,就是通道数目。因为这16个位置你可以不用完,只用前几个,那你就需要再给一个通道数目的参数,告诉它,我有几个通道。比如这里指定通道数目为7,那它就只看前7个位置,然后每次触发之后,它就依次对这前7个位置进行AD转换,转换结果都放在数据寄存器里,这里为了防止数据被覆盖,就需要用DMA及时将数据挪走。那7个通道转换完成之后,产生EOC信号,转换结束,然后再触发下一次,就又开始新一轮的转换,这就是单次转换,扫描模式的工作流程。

3.连续转换,非扫描模式

首先,它还是非扫描模式,所以菜单列表就只用第一个,然后它与上一种单次转换不同的是,它在一次转换结束后不会停止,而是立刻开始下一轮的转换,然后一直持续下去。这样就只需要最开始触发一次,之后就可以一直转换了。这个模式的好处就是,开始转换之后不需要等待一段时间的,因为它直都在转换,所以你就不需要手动开始转换了,也不用判断是否结束的,想要读AD值的时候,直接从数据寄存器取就是了。这就是连续转换,非扫描的模式

4.连续转换,扫描模式

它就是在上一个模式的基础上,变了一点,就是一次转换完成后,立刻开始下一次的转换。和上面这里非扫描模式的单次和连续是一个套路,这就是连续转换,扫描模式。

当然在扫描模式的情况下,还可以有一种模式,叫间断模式。它的作用是,在扫描的过程中,每隔几个转换,就暂停一次,需要再次触发,才能继续。这个模式没有列出来,要不然模式太多了。大家了解一下就可以了,暂时不需要掌握,好,这些就是STM32 ADC的4种转换模式

5.间断模式

数据对齐

转换时间

这个大概讲一下,不过转换时间这个参数,我们一般不太敏感,因为一般AD转换都很快,如果不需要非常高速的转换频率,那转换时间就可以忽略了。
我们来看一下,之前我们说了,AD转换是需要一小段时间的,就像厨子做菜一样,也是需要等一会儿才能上菜的,那AD转换的时候都有哪些步骤需要花时间呢?AD转换的步骤,有4步,分别是采样,保持,量化,编码,其中采样保持可以放在一起,量化编码可以放在一起,总共是这两大步。量化编码好理解,就是我们之前讲过的,ADC逐次比较的过程,这个是要花一段时间的,一般位数越多,花的时间就越长。
那采样保持是干啥的呢?这个我们前面这里并没有涉及,为什么需要采样保持呢?这是因为,我们的AD转换,就是后面的量化编码,是需要一小段时间的,如果在这一小段时间里,输入的电压还在不断变化,那就没法定位输入电压到底在哪了,所以在量化编码之前,我们需要设置一个采样开关。先打开采样开关,收集一下外部的电压,比如可以用一个小容量的电容存储一下这个电压,存储好了之后,断开采样开关,再进行后面的AD转换。这样在量化编码的期间,电压始终保持不变,这样才能精确地定位未知电压的位置,这就是采样保持电路。
那采样保持的过程,需要闭合采样开关,过一段时间再断开,这里就会产生一个采样时间。那回到这里,我们就得到了第二条,STM32 ADC的总转换时间为TCONV=采样时间+12.5个ADC周期,采样时间是采样保持花费的时间,这个可以在程序中进行配置,采样时间越大,越能避兔一些毛刺信号的干扰,不过转换时间也会相应延长。12.5个ADC周期是量化编码花费的时间,因为是12位的ADC,所以需要花费12个周期,这里多了半个周期,可能是做其他一些东西花的时间。ADC周期就是从RCC分频过来的ADCCLK,这个ADCCLK最大是14MHz。
所以下面有个例子,这里就是最快的转换时间,当ADCCLK=14MHz,采样时间为1.5个ADC周期,TCONV = 1.5 +12.5 = 14个ADC周期,在14MHz ADCCLK的情况下就 = 1us,这就是转化时间最快1us时间的来源。如果你采样周期再长些,它就达不到1us了;另外你也可以把ADCCLK的时钟设置超过14MHz,这样的话ADC就是在超频了,那转换时间可以比1us还短,不过这样稳定性就没法保证了。

校准

ADC外围电路设计

ADC常用库函数

函数说明

/**
 * @brief  复位ADC外设寄存器为默认值。
 * @param  ADCx: 指定要复位的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 无
 */
void ADC_DeInit(ADC_TypeDef* ADCx);

/**
 * @brief  初始化ADC外设。
 * @param  ADCx: 指定要初始化的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_InitStruct: 指向ADC初始化结构体的指针。
 *         该结构体包含ADC的各种初始化参数,如分辨率、数据对齐模式等。
 * @retval 无
 */
void ADC_Init(ADC_TypeDef* ADCx, ADC_InitTypeDef* ADC_InitStruct);

/**
 * @brief  填充ADC_InitStruct结构体默认值。
 * @param  ADC_InitStruct: 指向将被初始化的ADC_InitTypeDef结构体的指针。
 *         该结构体会被设置为默认配置。
 * @retval 无
 */
void ADC_StructInit(ADC_InitTypeDef* ADC_InitStruct);

/**
 * @brief  启用或禁用指定的ADC外设。
 * @param  ADCx: 指定要启用或禁用的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_Cmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  启用或禁用指定ADC外设的DMA请求。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  启用或禁用指定的ADC中断。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_IT: 指定要配置的ADC中断源。
 *         这个参数可以是ADC_IT_EOC(转换完成中断)、ADC_IT_AWD(模拟看门狗中断)等。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_ITConfig(ADC_TypeDef* ADCx, uint16_t ADC_IT, FunctionalState NewState);

/**
 * @brief  重置指定ADC外设的校准寄存器。
 * @param  ADCx: 指定要复位的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 无
 */
void ADC_ResetCalibration(ADC_TypeDef* ADCx);

/**
 * @brief  获取指定ADC外设的校准复位状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus ADC_GetResetCalibrationStatus(ADC_TypeDef* ADCx);

/**
 * @brief  开始指定ADC外设的校准过程。
 * @param  ADCx: 指定要校准的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 无
 */
void ADC_StartCalibration(ADC_TypeDef* ADCx);

/**
 * @brief  获取指定ADC外设的校准状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus ADC_GetCalibrationStatus(ADC_TypeDef* ADCx);

/**
 * @brief  启用或禁用指定ADC的常规转换的软件启动。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_SoftwareStartConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  获取指定ADC的常规转换的软件启动状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus ADC_GetSoftwareStartConvStatus(ADC_TypeDef* ADCx);

/**
 * @brief  配置指定ADC的非连续模式通道计数。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  Number: 非连续模式下的转换通道数。
 *         这个参数必须是1到8之间的值。
 * @retval 无
 */
void ADC_DiscModeChannelCountConfig(ADC_TypeDef* ADCx, uint8_t Number);

/**
 * @brief  启用或禁用指定ADC的非连续模式。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_DiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  配置指定ADC的常规转换通道。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_Channel: 指定要配置的ADC通道。
 *         这个参数可以是ADC_Channel_0到ADC_Channel_17的任意一个。
 * @param  Rank: 指定该通道在常规转换序列中的顺序。
 *         这个参数必须是1到16之间的值。
 * @param  ADC_SampleTime: 指定ADC的采样时间。
 *         这个参数可以是ADC_SampleTime_1Cycles5到ADC_SampleTime_239Cycles5的任意一个。
 * @retval 无
 */
void ADC_RegularChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);

/**
 * @brief  启用或禁用指定ADC的外部触发启动常规转换。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_ExternalTrigConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  返回指定ADC常规转换的结果数据。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 返回16位的转换结果。
 */
uint16_t ADC_GetConversionValue(ADC_TypeDef* ADCx);

/**
 * @brief  返回双模式下的转换结果数据。
 * @retval 返回32位的转换结果数据。
 */
uint32_t ADC_GetDualModeConversionValue(void);

/**
 * @brief  启用或禁用自动注入模式。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_AutoInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);
/**
 * @brief  启用或禁用指定ADC的注入非连续模式。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_InjectedDiscModeCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  配置指定ADC的注入组转换的外部触发源。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_ExternalTrigInjecConv: 指定外部触发源。
 *         这个参数可以是ADC_ExternalTrigInjecConv_T1_TRGO等。
 * @retval 无
 */
void ADC_ExternalTrigInjectedConvConfig(ADC_TypeDef* ADCx, uint32_t ADC_ExternalTrigInjecConv);

/**
 * @brief  启用或禁用指定ADC的注入组转换的外部触发启动。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_ExternalTrigInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  启用或禁用指定ADC的注入组转换的软件启动。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_SoftwareStartInjectedConvCmd(ADC_TypeDef* ADCx, FunctionalState NewState);

/**
 * @brief  获取指定ADC的注入组转换的软件启动状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus ADC_GetSoftwareStartInjectedConvCmdStatus(ADC_TypeDef* ADCx);

/**
 * @brief  配置指定ADC的注入通道。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_Channel: 指定要配置的ADC通道。
 *         这个参数可以是ADC_Channel_0到ADC_Channel_17的任意一个。
 * @param  Rank: 指定该通道在注入转换序列中的顺序。
 *         这个参数必须是1到4之间的值。
 * @param  ADC_SampleTime: 指定ADC的采样时间。
 *         这个参数可以是ADC_SampleTime_1Cycles5到ADC_SampleTime_239Cycles5的任意一个。
 * @retval 无
 */
void ADC_InjectedChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel, uint8_t Rank, uint8_t ADC_SampleTime);

/**
 * @brief  配置指定ADC的注入序列长度。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  Length: 指定注入序列的长度。
 *         这个参数必须是1到4之间的值。
 * @retval 无
 */
void ADC_InjectedSequencerLengthConfig(ADC_TypeDef* ADCx, uint8_t Length);

/**
 * @brief  设置指定ADC的注入通道偏移。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_InjectedChannel: 指定的注入通道。
 *         这个参数可以是ADC_InjectedChannel_1到ADC_InjectedChannel_4的任意一个。
 * @param  Offset: 指定的通道偏移值。
 *         这个参数必须是12位以内的值。
 * @retval 无
 */
void ADC_SetInjectedOffset(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel, uint16_t Offset);

/**
 * @brief  返回指定ADC的注入组转换结果数据。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_InjectedChannel: 指定的注入通道。
 *         这个参数可以是ADC_InjectedChannel_1到ADC_InjectedChannel_4的任意一个。
 * @retval 返回16位的转换结果。
 */
uint16_t ADC_GetInjectedConversionValue(ADC_TypeDef* ADCx, uint8_t ADC_InjectedChannel);

/**
 * @brief  启用或禁用指定ADC的模拟看门狗。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_AnalogWatchdog: 指定模拟看门狗的配置。
 *         这个参数可以是ADC_AnalogWatchdog_SingleRegEnable等。
 * @retval 无
 */
void ADC_AnalogWatchdogCmd(ADC_TypeDef* ADCx, uint32_t ADC_AnalogWatchdog);

/**
 * @brief  配置指定ADC的模拟看门狗高低阈值。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  HighThreshold: 模拟看门狗的高阈值。
 *         这个参数必须是12位以内的值。
 * @param  LowThreshold: 模拟看门狗的低阈值。
 *         这个参数必须是12位以内的值。
 * @retval 无
 */
void ADC_AnalogWatchdogThresholdsConfig(ADC_TypeDef* ADCx, uint16_t HighThreshold, uint16_t LowThreshold);

/**
 * @brief  配置指定ADC的模拟看门狗单通道。
 * @param  ADCx: 指定要配置的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_Channel: 指定的ADC通道。
 *         这个参数可以是ADC_Channel_0到ADC_Channel_17的任意一个。
 * @retval 无
 */
void ADC_AnalogWatchdogSingleChannelConfig(ADC_TypeDef* ADCx, uint8_t ADC_Channel);

/**
 * @brief  启用或禁用温度传感器和Vref内部参考电压通道。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void ADC_TempSensorVrefintCmd(FunctionalState NewState);

/**
 * @brief  获取指定ADC的标志状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_FLAG: 指定的标志。
 *         这个参数可以是ADC_FLAG_EOC(转换完成标志)等。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus ADC_GetFlagStatus(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);

/**
 * @brief  清除指定ADC的标志。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_FLAG: 指定要清除的标志。
 *         这个参数可以是ADC_FLAG_EOC等。
 * @retval 无
 */
void ADC_ClearFlag(ADC_TypeDef* ADCx, uint8_t ADC_FLAG);

/**
 * @brief  获取指定ADC的中断状态。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_IT: 指定的中断源。
 *         这个参数可以是ADC_IT_EOC(转换完成中断)等。
 * @retval 返回ITStatus状态。
 *         这个值可以是SET或RESET。
 */
ITStatus ADC_GetITStatus(ADC_TypeDef*
/**
 * @brief  清除指定ADC的中断挂起位。
 * @param  ADCx: 指定的ADC外设。
 *         这个参数可以是ADC1到ADC3的任意一个,具体取决于微控制器型号。
 * @param  ADC_IT: 指定要清除的中断源。
 *         这个参数可以是以下值之一:
 *           - ADC_IT_EOC: 转换完成中断
 *           - ADC_IT_AWD: 模拟看门狗中断
 *           - ADC_IT_JEOC: 注入组转换完成中断
 * @retval 无
 */
void ADC_ClearITPendingBit(ADC_TypeDef* ADCx, uint16_t ADC_IT);

函数举例

1.开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  

2.设置时钟预分频RCC_ADCCLKConfig()

3. 规则组通道配置ADC_RegularChannelConfig()

4.初始化ADC ADC_Init()

5.ADC使能 ADC_Cmd()

6.ADC校准  

    ADC_ResetCalibration(ADC1);                                //固定流程,内部有电路会自动执行校准
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);

规则组AD单通道
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*///最高14MHZ
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA0引脚初始化为模拟输入
	
	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);		//规则组序列1的位置,配置为通道0
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:无
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(void)
{
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}
规则组AD多通道
#include "stm32f10x.h"                  // Device header

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA0、PA1、PA2和PA3引脚初始化为模拟输入
	
	/*不在此处配置规则组序列,而是在每次AD转换前配置,这样可以灵活更改AD转换的通道*/
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;						//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;		//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;	//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;		//连续转换,失能,每转换一次规则组序列后停止
	ADC_InitStructure.ADC_ScanConvMode = DISABLE;			//扫描模式,失能,只转换规则组的序列1这一个位置
	ADC_InitStructure.ADC_NbrOfChannel = 1;					//通道数,为1,仅在扫描模式下,才需要指定大于1的数,在非扫描模式下,只能是1
	ADC_Init(ADC1, &ADC_InitStructure);						//将结构体变量交给ADC_Init,配置ADC1
	
	/*ADC使能*/
	ADC_Cmd(ADC1, ENABLE);									//使能ADC1,ADC开始运行
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
}

/**
  * 函    数:获取AD转换的值
  * 参    数:ADC_Channel 指定AD转换的通道,范围:ADC_Channel_x,其中x可以是0/1/2/3
  * 返 回 值:AD转换的值,范围:0~4095
  */
uint16_t AD_GetValue(uint8_t ADC_Channel)
{
	ADC_RegularChannelConfig(ADC1, ADC_Channel, 1, ADC_SampleTime_55Cycles5);	//在每次转换前,根据函数形参灵活更改规则组的通道1
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);					//软件触发AD转换一次
	while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);	//等待EOC标志位,即等待AD转换结束
	return ADC_GetConversionValue(ADC1);					//读数据寄存器,得到AD转换的结果
}
AD0 = AD_GetValue(ADC_Channel_0);		//单次启动ADC,转换通道0
		AD1 = AD_GetValue(ADC_Channel_1);		//单次启动ADC,转换通道1
		AD2 = AD_GetValue(ADC_Channel_2);		//单次启动ADC,转换通道2
		AD3 = AD_GetValue(ADC_Channel_3);		//单次启动ADC,转换通道3

注入组AD转换

#include "stm32f10x.h"

// 初始化ADC配置
void ADC_Configuration(void)
{
    // 使能ADC1和GPIOC的时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1 | RCC_APB2Periph_GPIOC, ENABLE);

    // 配置PC.03(ADC通道13)为模拟输入
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOC, &GPIO_InitStructure);

    // ADC1配置
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
    ADC_InitStructure.ADC_ScanConvMode = DISABLE;      // 扫描模式关闭
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;// 关闭连续转换模式
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 不使用外部触发
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
    ADC_InitStructure.ADC_NbrOfChannel = 1;             // 转换通道数设置为1
    ADC_Init(ADC1, &ADC_InitStructure);

    // 配置注入通道
    ADC_InjectedChannelConfig(ADC1, ADC_Channel_13, 1, ADC_SampleTime_239Cycles5);

    // 设置注入组转换的触发方式(例如软件触发)
    ADC_ExternalTrigInjectedConvConfig(ADC1, ADC_ExternalTrigInjecConv_T1_TRGO);

    // 使能ADC1
    ADC_Cmd(ADC1, ENABLE);

    // 重置校准寄存器
    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));

    // 校准ADC
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    // 启动注入组转换
    ADC_SoftwareStartInjectedConvCmd(ADC1, ENABLE);
}

int main(void)
{
    // 配置ADC
    ADC_Configuration();

    while (1)
    {
        // 等待注入组转换完成
        while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_JEOC));

        // 读取注入通道的转换结果
        uint16_t ADC_InjectedValue = ADC_GetInjectedConversionValue(ADC1, ADC_InjectedChannel_1);
        
        // 可以在此处使用转换后的值
    }
}

DMA

所以存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发

我们来看一下STM32的存储器映像,既然DMA是在存储器之间进行数据转运的,那我们就应该要了解一下,STM32中都有哪些存储器,这些存储器又是被安排到了哪些地址上,这就是存储器映像的内容。

在这个表里,无论是Flash,还是SRAM,还是外设寄存器,它们都是存储器的一种,包括外设寄存器,实际上也是存储器。在DMA简介中,我们说的是外设到存储器,存储器到存储器,本质上其实都是存储器之间的数据转运,说成外设到存储器,只不过是STM32他特别指定了可以转运外设的存储器而已。

存储器映像

变量存储在SRAM中地址 0X2000  0000开头

使用const变量修饰的常量存储在Flash中,只能读不能改写地址0X800 0000开头

外设寄存器地址0X4000 000        寄存器地址=寄存器所在外设的起始地址+偏移

ADC1的地址0X4001 2400开头

ADC1->DR的寄存器地址映像是4C  +0X4001 2400 =0X4001 244C


/*ADC_TypeDef是一个结构体,用于表示ADC(模数转换器)外设的寄存器布局。每个成员都代表一个特定的ADC寄存器,通常用来配置和控制ADC的操作。*/
typedef struct
{
  __IO uint32_t SR;      // 状态寄存器 (Status Register)
  __IO uint32_t CR1;     // 控制寄存器1 (Control Register 1)
  __IO uint32_t CR2;     // 控制寄存器2 (Control Register 2)
  __IO uint32_t SMPR1;   // 采样时间寄存器1 (Sample Time Register 1)
  __IO uint32_t SMPR2;   // 采样时间寄存器2 (Sample Time Register 2)
  __IO uint32_t JOFR1;   // 注入通道数据偏移寄存器1 (Injected Channel Data Offset Register 1)
  __IO uint32_t JOFR2;   // 注入通道数据偏移寄存器2 (Injected Channel Data Offset Register 2)
  __IO uint32_t JOFR3;   // 注入通道数据偏移寄存器3 (Injected Channel Data Offset Register 3)
  __IO uint32_t JOFR4;   // 注入通道数据偏移寄存器4 (Injected Channel Data Offset Register 4)
  __IO uint32_t HTR;     // 高阈值寄存器 (Higher Threshold Register)
  __IO uint32_t LTR;     // 低阈值寄存器 (Lower Threshold Register)
  __IO uint32_t SQR1;    // 常规序列寄存器1 (Regular Sequence Register 1)
  __IO uint32_t SQR2;    // 常规序列寄存器2 (Regular Sequence Register 2)
  __IO uint32_t SQR3;    // 常规序列寄存器3 (Regular Sequence Register 3)
  __IO uint32_t JSQR;    // 注入序列寄存器 (Injected Sequence Register)
  __IO uint32_t JDR1;    // 注入数据寄存器1 (Injected Data Register 1)
  __IO uint32_t JDR2;    // 注入数据寄存器2 (Injected Data Register 2)
  __IO uint32_t JDR3;    // 注入数据寄存器3 (Injected Data Register 3)
  __IO uint32_t JDR4;    // 注入数据寄存器4 (Injected Data Register 4)
  __IO uint32_t DR;      // 数据寄存器 (Data Register)
} ADC_TypeDef;

__IO 是一个宏定义,用于指示变量的读写属性。它通常在嵌入式系统的寄存器定义中使用,以便代码的可读性和移植性。具体来说,__IO 表示 "可读可写"(Input/Output),意味着该变量可以被读取和写入。它通常用于表示那些可以通过软件进行读写操作的硬件寄存器。

在不同的微控制器或编译器中,这类宏可能有不同的定义,但常见的含义如下:

  • __IO: 可读可写(读写访问)
  • __I: 只读(Read-Only)
  • __O: 只写(Write-Only)

这些宏有助于提高代码的可读性,并使代码更具可移植性,因为它们抽象了底层硬件寄存器的实际访问方式。此外,这些宏还可以帮助编译器优化代码。例如,编译器可能会对 __IO 标记的变量进行优化,因为它知道这些变量在执行过程中可能会被修改或读取。

在 ARM Cortex 微控制器的 CMSIS(Cortex Microcontroller Software Interface Standard)标准库中,这些宏的定义通常如下:

#define __I  volatile  /*!< 定义为只读权限 */
#define __O  volatile  /*!< 定义为只写权限 */
#define __IO volatile  /*!< 定义为读写权限 */

一一对应,把ADC1的基地址赋  赋值给 这个 结构体的地址,通过这个结构体就可以访问ADC1寄存器的地址

ADC1基地址=APB2外设基地址+0X2400

APB2外设基地址= 外设基地址+0x10000

外设基地址

DMA框图讲解

左上角这里是Cortex-M3内核,里面包含了CPU和内核外设等等,剩下的这所有东西,你都可以把它看成是存储器,所以总共就是CPU和存储器两个东西。Flash是主闪存,SRAM是运行内存,各个外设,都可以看成是寄存器,也是一种SRAM存储器。
奇存器是一种特殊的存储器,一方面,CPU可以对奇存器进行读写,就像读写运行内存一样,另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当做计数器、数据寄存器等等。所以,寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。
回到这里,既然外设就是寄存器,寄存器就是存储器,那使用DMA进行数据转运,就都可以归为一类问题了。就是从某个地址取内容,再放到另一个地址去.

我们看图,为了高效有条理地访问存储器,这里设计了一个总线矩阵,总线矩阵的左端,是主动单元,也就是拥有存储器的访问杈,右边这些,是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有DCode和系统总线,可以访问右边的存储器,其中DCode总线是专门访问Flash的,系统总线是访问其他东西的,另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。那主动单元,除了内核CPU,剩下的就是DMA总线了。这里DMA1有一条DMA总线,DMA2也有一条DMA总线,下面这还有一条DMA总线,这是以太网外设自己私有的DMA,这个可以不用管的。
在DMA1和DMA2里面,可以看到,DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立地工作了。

接着下面这里有个仲裁器,这个是因为,虽然多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,那就会由仲裁器,根据通道的优先级来决定谁来使用。另外在总线矩阵这里,也会有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU也能正常的工作。

接着继续看这里,是DMA请求,请求就是触发的意思,这条线路右边的触发源,是各个外设,所以这个DMA请求就是DMA的硬件触发源。比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了。这就是DMA请求的作用

到这里,有关DMA的结构就讲的差不多了,其中包括:用于访问各个存储器的DMA总线;内部的多个通道,可以进行独立的数据转运;仲裁器,用于调度各个通道,防止产生冲突;AHB从设备,用于配置DMA参数;DMA请求,用于硬件触发DMA的数据转运,这就是这个DMA的各个部分和作用。

注意一下:就是这里的Flash,就是这它是ROM只读存储器的一种,如果通过总线直接访问的话,无论是CPU,还是DMA,都是只读的,只能读取数据,而不能写入,如果你DMA的目的地址,填了Flash的区域,那转运时,就会出错。当然Flash也不是绝对的不可写入,我们可以配置这个Flash接口控制器,对Flash进行写入,这个流程就比较麻烦了,要先对Flash按页进行擦除,再写入数据。总之就是CPU或者DMA直接访问Flash的话,是只可以读而不可以写的,然后SRAM是运行内存,可以任意读写,没有问题,外设寄存器的话,得看参考手册里面的描述。

DMA基本结构

这就是外设站点和存储器站点各自的3个参数了。
在STM32手册里,所说的存储器,一般是特指Flash和SRAM,不包含外设寄存器。外设寄存器,他一般直接称作外设,所以就是外设到存储器,存储器到存储器,这样来描述。虽然我们刚才说了,寄存器也是存储器的一种,但是STM32还是使用了外设和存储器来作为区分,这个注意一下描述方法的不同。那在这里可以看到,
这就是外设站点和存储器站点各自的3个参数了。

DMA转运部分:

然后最后,就是开关控制了,也就是DMA_Cmd函数.当给DMA使能后,DMA就准备就绪,可以进行转运了。

基于DMA基本结构的一些问题


问题1:那如何进行存储器到存储器的数据转运,方向反过来可以吗?
如果要进行存储器到存储器的数据转运。那我们就需要把其中一个存储器的地址,放在外设的这个站点,这样就能进行存储器到存储器的转运了。只要你在外设起始地址里写Flash或者SRAM的地址,那它就会去Flash或SRAM找数据。这个站点虽然叫外设寄存器,但是它就只是个名字而已。甚至你可以在外设站点写存储器的地址,存储器站点写外设的地址,然后方向参数给反过来,这样也是可以的,只是ST公司给它起了这样的名字而已。你也可以把它叫做站点A、站点B,从A到B或者从B到A转运数据。

问题2:在DMA中软件触发的执行逻辑?和外部中断、ADC的软件触发有什么区别?
这个软件触发并不是调用某个函数一次,触发一次,它这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。所以这里的软件触发,和我们之前外部中断和ADC的软件触发可能不太一样,你可以把它理解成连续触发,那这个软件触发和(自动重装器)循环模式,不能同时用。因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,那DMA就停不下来了,这就是软件触发。

问题3:DMA的转运条件?
DMA进行转运,有几个条件,第一,就是开关控制,DMA_Cmd必须使能;第二,就是传输计数器必须大于0;第三,就是触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时。这时无论是否触发,DMA都不会再进行转运了,此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。
注意一下,写传输计数器时,必须要先关闭DMA,再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。

几个小知识点|细节:

 数据宽度与对齐

DMA数据转运的两个站点,都有一个数据宽度的参数,如果数据宽度都一样,那就是正常的一个个转运,如果数据宽度不一样,那会怎么处理呢?
这个表就是来说明问题的,

总之一这个表的意思就是如果你把小的数据转到大的里面去,高位就会补0;如果把大的数据转到小的里面去高位,就会舍弃掉;如果数据宽度一样,那就没事。

那最后,我们再来看两个例子,看看在这些实际的任务下,DMA是如何工作的。这两个例子和程序例子对应的。

这个例子的任务是将SRAM里的数组DataA,转运到另一个数组DataB中,我们看一下这种情况下,这个基本结构里的各个参数该如何配置。
首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。那在这个任务里,外设地址显然应该填DataA数组的首地址,存储器地址,给DataB数组的首地址,然后数据宽度,两个数组的类型都是uint8_t,所以数据宽度都是按8位的字节传输。之后地址是否自增,在中间可以看到,我们想要的效果是DataA[0]转到DataB[0],DataA[1]转到DataB[1],等等。所以转运完DataA[0]和DataB[0]之后,两个站点的地址都应该自增,都移动到下一个数据的位置,继续转运DataA[1]和DataB[1],这样来进行。
之后,这里的方向参数,那显然就是外设站点转运到存储器站点了,当然如果你想把DataB的数据转运到DataA,那可以把方向参数换过来,这样就是方向转运了。
然后是传输计数器和是否要自动重装,在这里,显然要转运7次,所以传输计数器给7,自动重装暂时不需要,之后触发选择部分,这里,我们要使用软件触发。因为这是存储器到存储器的数据转运,是不需要等待硬件时机的,尽快转运完成就行了。
那最后,调用DMA_Cmd,给DMA使能,这样数据就会从DataA转运到DataB了。转运7次之后,传输计数器自减到0,DMA停止,转运完成。这里的数据转运是一种复制转运,转运完成后DataA的数据并不会消失,这个过程相当于是把DataA的数据复制到了DataB的位置。

ADC扫描模式+DMA

左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面。那我们要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。所以在这里DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址。
之后数据宽度,因为ADC_DR和SRAM数组,我们要的都是uint16_t的数据,所以数据宽度都是16位的半字传输。
接着判断地址是否自增,那从这个图里,显然是外设地址不自增,存储器地址自增;传输方向,是外设站点到存储器站点;传输计数器,这里通道有7个,所以计数7次;计数器是否自动重装,这里可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止,如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运,ADC和DMA同步工作。
最后是触发选择,这里ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发。
最后硬件触发这里要说明一下,我们上一节说了,ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断。所以我们程序不太好判断,某一个通道转换完成的时机是什么时候。但是根据UP主的研究,虽然单个通道转换完成后,不产生任何标志位和中断,但是它应该会产生DMA请求,去触发DMA转运,这部分内容,手册里并没有详细描述,根据我实际实验,单个通道的DMA请求肯定是有的。
这些就是ADC扫描模式和DMA配合使用的流程。一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,这个缺陷使ADC和DMA成为了最常见的伙伴。

DM常用函数

/**
 * @brief  复位指定DMA通道的寄存器为默认值。
 * @param  DMAy_Channelx: 指定要复位的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @retval 无
 */
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);

/**
 * @brief  根据指定参数初始化DMA通道。
 * @param  DMAy_Channelx: 指定要初始化的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @param  DMA_InitStruct: 指向DMA_InitTypeDef结构体的指针,该结构体包含DMA的配置信息。
 * @retval 无
 */
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);

/**
 * @brief  用默认值初始化DMA_InitStruct成员。
 * @param  DMA_InitStruct: 指向DMA_InitTypeDef结构体的指针,该结构体将被初始化。
 * @retval 无
 */
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);

/**
 * @brief  启用或禁用指定的DMA通道。
 * @param  DMAy_Channelx: 指定的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);

/**
 * @brief  启用或禁用指定DMA通道的中断。
 * @param  DMAy_Channelx: 指定的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @param  DMA_IT: 指定的DMA中断源。
 *         这个参数可以是以下值的组合:
 *           - DMA_IT_TC: 传输完成中断
 *           - DMA_IT_HT: 半传输完成中断
 *           - DMA_IT_TE: 传输错误中断
 * @param  NewState: 启用或禁用的状态。
 *         这个参数可以是ENABLE或DISABLE。
 * @retval 无
 */
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);

/**
 * @brief  设置DMA通道的当前数据传输量。
 * @param  DMAy_Channelx: 指定的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @param  DataNumber: 指定要传输的数据量。
 *         这个参数必须是16位的整数值。
 * @retval 无
 */
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); 

/**
 * @brief  返回指定DMA通道的当前剩余数据传输量。
 * @param  DMAy_Channelx: 指定的DMA通道。
 *         这个参数可以是DMA1_Channel1到DMA2_Channel7的任意一个,具体取决于微控制器型号。
 * @retval 返回剩余的数据量。
 */
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);

/**
 * @brief  获取指定DMA标志状态。
 * @param  DMAy_FLAG: 指定的DMA标志。
 *         这个参数可以是以下值之一:
 *           - DMA1_FLAG_GL1: 全局中断标志
 *           - DMA1_FLAG_TC1: 传输完成标志
 *           - DMA1_FLAG_HT1: 半传输完成标志
 *           - DMA1_FLAG_TE1: 传输错误标志
 *         等等,取决于DMA通道和微控制器型号。
 * @retval 返回FlagStatus状态。
 *         这个值可以是SET或RESET。
 */
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);

/**
 * @brief  清除指定DMA标志。
 * @param  DMAy_FLAG: 指定要清除的DMA标志。
 *         这个参数可以是以下值之一:
 *           - DMA1_FLAG_GL1: 全局中断标志
 *           - DMA1_FLAG_TC1: 传输完成标志
 *           - DMA1_FLAG_HT1: 半传输完成标志
 *           - DMA1_FLAG_TE1: 传输错误标志
 *         等等,取决于DMA通道和微控制器型号。
 * @retval 无
 */
void DMA_ClearFlag(uint32_t DMAy_FLAG);

/**
 * @brief  获取指定DMA中断状态。
 * @param  DMAy_IT: 指定的DMA中断源。
 *         这个参数可以是以下值之一:
 *           - DMA1_IT_GL1: 全局中断
 *           - DMA1_IT_TC1: 传输完成中断
 *           - DMA1_IT_HT1: 半传输完成中断
 *           - DMA1_IT_TE1: 传输错误中断
 *         等等,取决于DMA通道和微控制器型号。
 * @retval 返回ITStatus状态。
 *         这个值可以是SET或RESET。
 */
ITStatus DMA_GetITStatus(uint32_t DMAy_IT);

/**
 * @brief  清除指定DMA中断挂起位。
 * @param  DMAy_IT: 指定要清除的DMA中断源。
 *         这个参数可以是以下值之一:
 *           - DMA1_IT_GL1: 全局中断
 *           - DMA1_IT_TC1: 传输完成中断
 *           - DMA1_IT_HT1: 半传输完成中断
 *           - DMA1_IT_TE1: 传输错误中断
 *         等等,取决于DMA通道和微控制器型号。
 * @retval 无
 */
void DMA_ClearITPendingBit(uint32_t DMAy_IT);

DMA转运案例

#include "stm32f10x.h"  // 包含STM32F10x系列微控制器的设备头文件

uint16_t MyDMA_Size;    // 定义全局变量,用于保存传输的数据大小,以便在传输函数中使用

/**
  * @brief  DMA初始化
  * @param  AddrA 外设基地址
  * @param  AddrB 存储器基地址
  * @param  Size  传输的数据大小(次数)
  * @retval 无
  */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
    MyDMA_Size = Size;  // 保存传输的数据大小到全局变量

    /* 开启DMA时钟 */
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);  // 启用DMA1的时钟

    /* DMA初始化结构体配置 */
    DMA_InitTypeDef DMA_InitStructure;  // 定义DMA初始化结构体
    DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;  // 设置外设基地址
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;  // 外设数据宽度为字节
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;  // 使能外设地址自增
    DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;  // 设置存储器基地址
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;  // 存储器数据宽度为字节
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  // 使能存储器地址自增
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 数据传输方向,从外设到存储器
    DMA_InitStructure.DMA_BufferSize = Size;  // 设置传输的数据大小
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;  // 设置DMA模式为正常模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;  // 使能存储器到存储器传输
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  // 设置DMA优先级为中等
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);  // 初始化DMA1的通道1 软件触发通道无所谓

    /* 使能DMA通道 */
    DMA_Cmd(DMA1_Channel1, DISABLE);  // 初始化后不立即使能DMA,等待传输启动时使能
}

/**
  * @brief  启动DMA数据传输
  * @param  无
  * @retval 无
  */
void MyDMA_Transfer(void)
{
    DMA_Cmd(DMA1_Channel1, DISABLE);  // 禁用DMA通道,准备传输
    DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);  // 设置传输计数器,指定传输次数
    DMA_Cmd(DMA1_Channel1, ENABLE);  // 使能DMA通道,开始传输

    while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);  // 等待传输完成
    DMA_ClearFlag(DMA1_FLAG_TC1);  // 清除传输完成标志
}

ADC+DMA转运

#include "stm32f10x.h"  // 包含STM32F10x系列微控制器的设备头文件

uint16_t AD_Value[4];  // 定义全局数组,用于存放ADC转换结果

/**
  * @brief  ADC初始化函数
  * @param  无
  * @retval 无
  */
void AD_Init(void)
{
    /* 开启时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);  // 开启ADC1时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启GPIOA时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);    // 开启DMA1时钟

    /* 设置ADC时钟 */
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);  // 配置ADC时钟为PCLK2的1/6,即12MHz

    /* GPIO初始化 */
    GPIO_InitTypeDef GPIO_InitStructure;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;  // 设置GPIO模式为模拟输入
    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;  // 设置引脚
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;  // 设置GPIO速度(非模拟引脚忽略)
    GPIO_Init(GPIOA, &GPIO_InitStructure);  // 初始化GPIOA的PA0, PA1, PA2, PA3

    /* 配置ADC规则组通道 */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);  // 通道0,序列1
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);  // 通道1,序列2
    ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);  // 通道2,序列3
    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);  // 通道3,序列4

    /* ADC初始化 */
    ADC_InitTypeDef ADC_InitStructure;
    ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  // 独立ADC模式
    ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;  // 数据右对齐
    ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;  // 软件触发
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换模式
    ADC_InitStructure.ADC_ScanConvMode = ENABLE;  // 扫描模式
    ADC_InitStructure.ADC_NbrOfChannel = 4;  // 通道数为4
    ADC_Init(ADC1, &ADC_InitStructure);  // 初始化ADC1

    /* DMA初始化 */
    DMA_InitTypeDef DMA_InitStructure;
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;  // 外设基地址,ADC数据寄存器
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  // 外设数据宽度,半字(16位)
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;  // 外设地址自增失能
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;  // 存储器基地址
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;  // 存储器数据宽度,半字
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;  // 存储器地址自增
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  // 数据传输方向,从外设到存储器
    DMA_InitStructure.DMA_BufferSize = 4;  // 数据传输大小,与ADC通道数一致
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  // 存储器到存储器传输失能
    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  // DMA优先级,中等
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);  // 初始化DMA1通道1

    /* 启用DMA和ADC */
    DMA_Cmd(DMA1_Channel1, ENABLE);  // 启用DMA1通道1
    ADC_DMACmd(ADC1, ENABLE);  // 启用ADC1的DMA请求
    ADC_Cmd(ADC1, ENABLE);  // 启用ADC1

    /* ADC校准 */
    ADC_ResetCalibration(ADC1);  // 复位校准
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);  // 等待复位校准完成
    ADC_StartCalibration(ADC1);  // 开始校准
    while (ADC_GetCalibrationStatus(ADC1) == SET);  // 等待校准完成

    /* 启动ADC软件转换 */
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);  // 启动ADC转换
}

存储器

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值