STM32F1 标准库笔记

CMSIS

为了解决不同的芯片厂商生产的 Cortex 微控制器软件的兼容性问题,ARM 与芯片厂商建立了 CMSIS 标准 (Cortex MicroController SoftwareInterface Standard)。

cmsis

CMSIS 标准中最主要的为 CMSIS 核心层,它包括了:

  • 内核函数层:其中包含用于访问内核寄存器的名称、地址定义,主要由 ARM 公司提供。
  • 设备外设访问层:提供了片上的核外外设的地址和中断定义,主要由芯片生产商提供。

CMSIS 层位于硬件层与操作系统或用户层之间,提供了与芯片生产商无关的硬件抽象层,可
以为接口外设、实时操作系统提供简单的处理器软件接口,屏蔽了硬件差异,这对软件的移植是
有极大的好处的。

cmsis

RCC

RCC :reset clock control 复位和时钟控制器。设 置系统时钟 SYSCLK、设置 AHB 分频因子(决定 HCLK 等于多少)、设置 APB2 分频因子(决定 PCLK2 等于多少)、设置 APB1 分频因子(决定 PCLK1 等于多少)、设置各个外设的分频因子;控制 AHB、APB2 和 APB1 这三条总线时钟的开启、控制每个外设的时钟的开启。对于 SYSCLK、HCLK、PCLK2、PCLK1 这四个时钟的配置一般是:PCLK2 = HCLK = SYSCLK = PLLCLK = 72M,PCLK1 = HCLK / 2 = 36M。

HSE 是高速的外部时钟信号,可以由有源晶振或者无源晶振提供,频率从 4-16MHZ 不等。当使用有源晶振时,时钟从 OSC_IN 引脚进入,OSC_OUT 引脚悬空,当选用无源晶振时,时钟从 OSC_IN 和 OSC_OUT 进入,并且要配谐振电容。

  • PLL 时钟源

    PLL 时钟来源可以有两个, 一个来自 HSE, 另外一个是 HSI / 2, 具体用哪个由时钟配置寄存器 CFGR 的位 16:PLLSRC 设置。HSI 是内部高速的时钟信号, 频率为 8M, 根据温度和环境的情况频率会有漂移, 一般不作为 PLL 的时钟来源。这里我们选 HSE 作为 PLL 的时钟来源。

  • PLL 时钟 PLLCLK

    通过设置 PLL 的倍频因子, 可以对 PLL 的时钟来源进行倍频, 倍频因子可以是:[2,3,4,5,6,7,8,9,10,11,12,13,14,15,16],具体设置成多少, 由时钟配置寄存器 CFGR 的位 21-18:PLLMUL [3:0] 设置。我们这里设置为 9 倍频, 因为上一步我们设置 PLL 的时钟来源为 HSE = 8M, 所以经过 PLL 倍频之后的 PLL 时钟: PLLCLK = 8M 9 = 72M。72M 是 ST 官方推荐的稳定运行时钟, 如果你想超频的话, 增大倍频因子即可, 最高为 128M。我们这里设置 PLL 时钟: PLLCLK = 8M9=72M。

  • 系统时钟 SYSCLK

    系统时钟来源可以是: HSI、PLLCLK、HSE, 具体的时钟配置寄存器 CFGR 的位 1-0:SW [1:0] 设置。我们这里设置系统时钟: SYSCLK = PLLCLK = 72M。

  • AHB 总线时钟 HCLK

    系统时钟 SYSCLK 经过 AHB 预分频器分频之后得到时钟叫 APB 总线时钟, 即 HCLK, 分频因子可以是:[1,2,4,8,16,64,128,256,512],具体的由时钟配置寄存器 CFGR 的位 7-4:HPRE [3:0] 设置。片上大部分外设的时钟都是经过 HCLK 分频得到, 至于 AHB 总线上的外设的时钟设置为多少, 得等到我们使用该外设的时候才设置, 我们这里只需粗线条的设置好 APB 的时钟即可。我们这里设置为 1 分频, 即 HCLK = SYSCLK = 72M。

  • APB2 总线时钟 PLCK2

    APB2 总线时钟 PCLK2 由 HCLK 经过高速 APB2 预分频器得到, 分频因子可以是:[1,2,4,8,16],具体由时钟配置寄存器 CFGR 的位 13-11:PPRE2 [2:0] 决定。PLCK2 属于高速的总线时钟, 片上高速的外设就挂载到这条总线上, 比如全部的 GPIO、USART1、SPI1 等。至于 APB2 总线上的外设的时钟设置为多少, 得等到我们使用该外设的时候才设置, 我们这里只需粗线条的设置好 APB2 的时钟即可。我们这里设置为 1 分频, 即 PCLK2 = HCLK = 72M。

  • APB1 总线时钟 PLCK

    APB1 总线时钟 PCLK1 由 HCLK 经过低速 APB 预分频器得到, 分频因子可以是:[1,2,4,8,16],具体的由时钟配置寄存器 CFGR 的位 10-8:PRRE1 [2:0] 决定。PLCK1 属于低速的总线时钟, 最高为 36M, 片上低速的外设就挂载到这条总线上, 比如 USART2/3/4/5、SPI2/3,I2C1/2 等。至于 APB1 总线上的外设的时钟设置为多少, 得等到我们使用该外设的时候才设置, 我们这里只需粗线条的设置好 APB1 的时钟即可。我们这里设置为 2 分频, 即 PCLK1 = HCLK/2 = 36M。

跑马灯实验

GPIO(通用输入输出)

寄存器 作用
GPIOx_CRL/CRH 引脚模式配置(输入/输出/复用)
GPIOx_IDR 输入数据寄存器(读引脚电平)
GPIOx_ODR 输出数据寄存器(写引脚电平)
GPIOx_BSRR/BRR 设置 / 复位某一位

LED 初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void LED_Init(void)
{

GPIO_InitTypeDef GPIO_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB|RCC_APB2Periph_GPIOE, ENABLE); //使能PB,PE端口时钟

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED0-->PB.5 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //IO口速度为50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure); //根据设定参数初始化GPIOB.5
GPIO_SetBits(GPIOB,GPIO_Pin_5); //PB.5 输出高

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; //LED1-->PE.5 端口配置, 推挽输出
GPIO_Init(GPIOE, &GPIO_InitStructure); //推挽输出 ,IO口速度为50MHz
GPIO_SetBits(GPIOE,GPIO_Pin_5); //PE.5 输出高
}

位带操作

1
2
#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 
#define PBin(n) BIT_ADDR(GPIOB_IDR_Addr,n) //输入

库函数操作

1
uint8_t GPIO_ReadInputDataBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)

串口实验

USART(串口通信)

寄存器 作用
USART_SR 状态寄存器(是否发送完成、是否收到)
USART_DR 数据寄存器(读出收到的数据 or 写入要发送的数据)
USART_BRR 波特率寄存器(设置通信速率)
USART_CR1 控制寄存器(使能串口/中断/接收/发送)

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
void uart_init(u32 bound)
{
//GPIO端口设置
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE); //使能USART1,GPIOA时钟

//USART1_TX GPIOA.9
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; //PA.9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.9

//USART1_RX GPIOA.10初始化
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;//PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;//浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);//初始化GPIOA.10

//Usart1 NVIC 配置
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3 ;//抢占优先级3
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道使能
NVIC_Init(&NVIC_InitStructure); //根据指定的参数初始化VIC寄存器

//USART 初始化设置

USART_InitStructure.USART_BaudRate = bound;//串口波特率
USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长为8位数据格式
USART_InitStructure.USART_StopBits = USART_StopBits_1;//一个停止位
USART_InitStructure.USART_Parity = USART_Parity_No;//无奇偶校验位
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;//无硬件数据流控制
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx; //收发模式

USART_Init(USART1, &USART_InitStructure); //初始化串口1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接受中断
USART_Cmd(USART1, ENABLE); //使能串口1

}

串口中断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
void USART1_IRQHandler(void)                	//串口1中断服务程序
{
u8 Res;
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntEnter();
#endif
if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) //接收中断(接收到的数据必须是0x0d 0x0a结尾)
{
Res =USART_ReceiveData(USART1); //读取接收到的数据

if((USART_RX_STA&0x8000)==0)//接收未完成
{
if(USART_RX_STA&0x4000)//接收到了0x0d
{
if(Res!=0x0a)USART_RX_STA=0;//接收错误,重新开始
else USART_RX_STA|=0x8000; //接收完成了
}
else //还没收到0X0D
{
if(Res==0x0d)USART_RX_STA|=0x4000;
else
{
USART_RX_BUF[USART_RX_STA&0X3FFF]=Res ;
USART_RX_STA++;
if(USART_RX_STA>(USART_REC_LEN-1))USART_RX_STA=0;//接收数据错误,重新开始接收
}
}
}
}
#if SYSTEM_SUPPORT_OS //如果SYSTEM_SUPPORT_OS为真,则需要支持OS.
OSIntExit();
#endif
}
#endif

🔧 串口配置流程说明(适用于 STM32 标准库)

✅ 1. 使能时钟

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
  • 使能 USART1GPIOA 的时钟;
  • 所有外设在使用前都需要先打开其时钟。

✅ 2. 配置串口 TX(发送)引脚

1
2
3
4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; // PA9
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 复用推挽输出
GPIO_Init(GPIOA, &GPIO_InitStructure);
  • PA9USART1_TX
  • 配置为复用功能 + 推挽输出,适合高速串口发送。

✅ 3. 配置串口 RX(接收)引脚

1
2
3
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; // PA10
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
  • PA10USART1_RX
  • 配置为浮空输入,可以接收外部电平变化。

✅ 4. 配置 NVIC(中断控制器)

1
2
3
4
5
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
  • 启用 USART1 的接收中断通道;
  • 设置抢占和子优先级,控制与其他中断的响应顺序;
  • 只有开启了中断,串口接收到数据时才能触发中断函数。

✅ 5. 配置串口参数

1
2
3
4
5
6
7
USART_InitStructure.USART_BaudRate = bound;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART1, &USART_InitStructure);
参数 说明
BaudRate 波特率(传输速率)
WordLength 每帧数据位数(8 位)
StopBits 停止位数(1 位)
Parity 是否使用奇偶校验
HardwareFlowControl 是否使用流控制
Mode 开启接收 + 发送模式

✅ 6. 启用接收中断

1
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);
  • RXNE:接收缓冲区非空;
  • 开启后当有数据到达串口,会自动进入中断函数。

✅ 7. 启用串口

1
USART_Cmd(USART1, ENABLE);

外部中断

初始化

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

EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;

KEY_Init(); // 按键端口初始化

RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE); //使能复用功能时钟

//GPIOE.2 中断线以及中断初始化配置 下降沿触发
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);

EXTI_InitStructure.EXTI_Line=EXTI_Line2; //KEY2
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling;
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器

//GPIOE.3 中断线以及中断初始化配置 下降沿触发 //KEY1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3);
EXTI_InitStructure.EXTI_Line=EXTI_Line3;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器

//GPIOE.4 中断线以及中断初始化配置 下降沿触发 //KEY0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4);
EXTI_InitStructure.EXTI_Line=EXTI_Line4;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器


//GPIOA.0 中断线以及中断初始化配置 上升沿触发 PA0 WK_UP
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);

EXTI_InitStructure.EXTI_Line=EXTI_Line0;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;
EXTI_Init(&EXTI_InitStructure); //根据EXTI_InitStruct中指定的参数初始化外设EXTI寄存器


NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; //使能按键WK_UP所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; //子优先级3
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);

NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn; //使能按键KEY2所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2,
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02; //子优先级2
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure);


NVIC_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; //使能按键KEY1所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; //子优先级1
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; //使能按键KEY0所在的外部中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; //抢占优先级2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; //子优先级0
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //使能外部中断通道
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

}

✅ 步骤 1:按键引脚初始化

1
KEY_Init(); // 按键 GPIO 初始化

此函数一般配置按键对应的 GPIO 端口为输入模式。


✅ 步骤 2:开启 AFIO 时钟

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

AFIO(Alternate Function I/O) 模块是 STM32 中管理外部中断与 GPIO 映射的模块。使用 EXTI 前必须开启该模块时钟。


✅ 步骤 3:将 GPIO 映射到 EXTI 中断线

1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE, GPIO_PinSource2);

该函数将 GPIOE.2 映射到 EXTI Line2,这样该引脚的电平变化就会触发 EXTI2 中断。

你会看到类似的配置:

  • GPIOE.2EXTI_Line2(KEY2)
  • GPIOE.3EXTI_Line3(KEY1)
  • GPIOE.4EXTI_Line4(KEY0)
  • GPIOA.0EXTI_Line0(WK_UP)

✅ 步骤 4:配置 EXTI 触发方式

1
2
3
4
5
EXTI_InitStructure.EXTI_Line = EXTI_Line2;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 或 Rising
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);

这里设置了以下关键参数:

  • 中断线路(EXTI_Line):哪个 EXTI 线(对应哪一个引脚)。
  • 中断模式(EXTI_Mode):中断触发(而不是事件触发)。
  • 触发条件(EXTI_Trigger)
    • EXTI_Trigger_Falling:下降沿触发。
    • EXTI_Trigger_Rising:上升沿触发。
  • 使能该中断线(EXTI_LineCmd)

✅ 步骤 5:配置 NVIC 中断优先级

1
2
3
4
5
NVIC_InitStructure.NVIC_IRQChannel = EXTI2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x02;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);

在 NVIC 中配置中断通道和优先级:

  • EXTI2_IRQn:中断通道号。
  • 抢占优先级(PreemptionPriority):可以打断优先级更低的中断。
  • 子优先级(SubPriority):在抢占优先级相同时,决定先响应谁。
  • ENABLE:启用该通道。

依次为不同按键配置了不同的子优先级,以保证响应顺序(KEY0 > KEY1 > KEY2)。


📌 总结:STM32 外部中断配置流程

步骤 操作说明
1️⃣ 初始化按键对应的 GPIO(KEY_Init
2️⃣ 使能 AFIO 时钟(RCC_APB2PeriphClockCmd
3️⃣ 将 GPIO 映射到 EXTI 线路(GPIO_EXTILineConfig
4️⃣ 配置 EXTI:中断线路、触发方式(EXTI_Init
5️⃣ 配置 NVIC:通道、优先级、使能(NVIC_Init

独立看门狗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//prer:分频数:0~7(只有低3位有效!)
//分频因子=4*2^prer.但最大值只能是256!
//rlr:重装载寄存器值:低11位有效.
//时间计算(大概):Tout=((4*2^prer)*rlr)/40 (ms).
void IWDG_Init(u8 prer,u16 rlr)
{
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); //使能对寄存器IWDG_PR和IWDG_RLR的写操作

IWDG_SetPrescaler(prer); //设置IWDG预分频值:设置IWDG预分频值为64

IWDG_SetReload(rlr); //设置IWDG重装载值

IWDG_ReloadCounter(); //按照IWDG重装载寄存器的值重装载IWDG计数器

IWDG_Enable(); //使能IWDG
}
//喂独立看门狗
void IWDG_Feed(void)
{
IWDG_ReloadCounter();//reload
}

独立看门狗(IWDG)是一种硬件看门狗定时器,通常用于确保系统在发生故障或软件崩溃时能够自动复位。它独立于系统主时钟运行,并且不受主系统的影响。因此,IWDG 是一种可靠的硬件机制,用来防止系统长时间卡死或死锁。

独立看门狗 (Independent Watchdog, IWDG) 配置流程


1. 使能 IWDG 寄存器写操作

1
IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable);
  • 说明:首先,必须使能对 IWDG写操作,否则你不能修改它的 预分频器 (IWDG_PR) 和 重装载寄存器 (IWDG_RLR)。
  • 步骤:调用 IWDG_WriteAccessCmd 函数,传入 IWDG_WriteAccess_Enable 参数,允许对相关寄存器进行写操作。

2. 设置 IWDG 预分频器

1
IWDG_SetPrescaler(prer);
  • 说明:IWDG 定时器有一个 预分频器,它通过对 IWDG 时钟源进行分频来控制计数器的计数速度。该预分频器的值由 3 位二进制数 prer 来决定,表示从 4 到 256 的分频因子。
    • IWDG 时钟:内部时钟频率通常是 40 kHz,因此预分频器控制的是将此时钟分频为多少。
    • 计算公式分频因子 = 4 * 2^prer,但最大分频因子为 256。
  • 步骤:调用 IWDG_SetPrescaler 函数,传入你想要的分频值 prer。例如,若 prer = 6,则分频因子为 4 * 2^6 = 256

3. 设置 IWDG 重装载值

1
IWDG_SetReload(rlr);
  • 说明重装载寄存器 (IWDG_RLR) 用来设定 IWDG 计数器的重载值,这个值决定了 IWDG 超时的触发时间。
    • 重装载寄存器的有效位为 低 11 位
    • 超时计算公式:
      Tout = ((4 * 2^prer) * rlr) / 40(单位:毫秒)。
  • 步骤:调用 IWDG_SetReload 函数,传入所需的重装载值 rlr,控制看门狗的超时触发时间。

4. 重装载 IWDG 计数器

1
IWDG_ReloadCounter();
  • 说明:每次触发 IWDG 计数器重装载 都会将计数器重新加载为 重装载寄存器的值,以避免计数器溢出。
  • 步骤:调用 IWDG_ReloadCounter 函数,重装载计数器,以防止触发系统复位。

5. 使能 IWDG

1
IWDG_Enable();
  • 说明:最后,启用 IWDG。一旦启用,IWDG 会开始计数,并在计数器超时时触发系统复位。
  • 步骤:调用 IWDG_Enable 函数,启动看门狗定时器。

6. 喂养 IWDG(防止复位)

1
IWDG_Feed();
  • 说明:喂养 IWDG 是通过调用 IWDG_ReloadCounter 来定期重装载计数器,防止看门狗复位系统。
    • 如果系统在 超时时间 内没有重新喂养看门狗,IWDG 将会复位系统。
  • 步骤:定期调用 IWDG_Feed 函数,以防止 IWDG 计数器溢出并触发复位。

独立看门狗的作用

  • 硬件独立性:独立看门狗定时器的最大优势是它不依赖于主系统时钟。因此,即使系统出现软件死锁或系统时钟失效,独立看门狗仍然能够正常运行并复位系统。
  • 复位机制:在 IWDG 计数器超时后,系统会被硬复位,恢复正常操作。这对嵌入式系统的稳定性和可靠性至关重要。

配置流程总结

步骤 操作 说明
1️⃣ 启用对寄存器写操作 允许修改 IWDG 配置寄存器
2️⃣ 设置预分频器 设置定时器的分频因子,控制看门狗计数器的速率
3️⃣ 设置重装载寄存器 设置计数器溢出的时间(超时触发)
4️⃣ 重装载 IWDG 计数器 重置计数器,防止计数器溢出触发复位
5️⃣ 启用 IWDG 启动看门狗定时器,开始计时
6️⃣ 定期喂养 IWDG 调用 IWDG_Feed 使计数器重装载,防止触发系统复位

窗口看门狗

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

//保存WWDG计数器的设置值,默认为最大.
u8 WWDG_CNT=0x7f;
//初始化窗口看门狗
//tr :T[6:0],计数器值
//wr :W[6:0],窗口值
//fprer:分频系数(WDGTB),仅最低2位有效
//Fwwdg=PCLK1/(4096*2^fprer).

void WWDG_Init(u8 tr,u8 wr,u32 fprer)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE); // WWDG时钟使能

WWDG_CNT=tr&WWDG_CNT; //初始化WWDG_CNT.
WWDG_SetPrescaler(fprer);////设置IWDG预分频值

WWDG_SetWindowValue(wr);//设置窗口值

WWDG_Enable(WWDG_CNT); //使能看门狗 , 设置 counter .

WWDG_ClearFlag();//清除提前唤醒中断标志位

WWDG_NVIC_Init();//初始化窗口看门狗 NVIC

WWDG_EnableIT(); //开启窗口看门狗中断
}
//重设置WWDG计数器的值
void WWDG_Set_Counter(u8 cnt)
{
WWDG_Enable(cnt);//使能看门狗 , 设置 counter .
}
//窗口看门狗中断服务程序
void WWDG_NVIC_Init()
{
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; //WWDG中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //抢占2,子优先级3,组2
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; //抢占2,子优先级3,组2
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_InitStructure);//NVIC初始化
}

void WWDG_IRQHandler(void)
{

WWDG_SetCounter(WWDG_CNT); //当禁掉此句后,窗口看门狗将产生复位

WWDG_ClearFlag(); //清除提前唤醒中断标志位

LED1=!LED1; //LED状态翻转
}

🆚 一、窗口看门狗(WWDG) vs 独立看门狗(IWDG)

特性对比 独立看门狗 IWDG 窗口看门狗 WWDG
时钟源 独立内部低速时钟(LSI,通常为 40 kHz) 主系统时钟 PCLK1 分频得到
复位触发方式 超时未喂狗就会复位系统 喂狗过早、过晚都会触发复位
响应机制 只要求在超时时间内任意时刻喂狗 只能在 “允许的窗口时间” 内喂狗,否则视为异常
窗口机制 有(例如只能在计数器低于某个值时喂狗)
中断功能 通常用于系统复位,不支持提前中断 支持提前唤醒中断,允许在接近超时时提前处理或记录
适用场景 稳定性要求高,不依赖系统时钟 时间控制更精确场合,适合更严格系统状态监控
优缺点 简单可靠,适合低功耗 灵活复杂,可中断处理但依赖主系统

🧭 二、窗口看门狗 WWDG 配置流程详解

📌 1. 使能看门狗时钟

1
RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
  • 打开 WWDG 外设的时钟(连接在 APB1 总线上)。

📌 2. 设置计数器初值(TR)

1
WWDG_CNT = tr & 0x7F;  // 只能是7位有效值(0x7F以内)
  • TR(T [6:0]):计数器初始值,最大为 0x7F(127),最小不能小于 0x40(否则立即复位)。

📌 3. 设置预分频器(WDGTB)

1
WWDG_SetPrescaler(fprer);
  • 用于控制 WWDG 的时钟频率:

    1
    Fwwdg = PCLK1 / (4096 * 2^WDGTB)

📌 4. 设置窗口值(WR)

1
WWDG_SetWindowValue(wr);
  • 窗口机制:只有当 WWDG 计数器值 小于 WR 且大于 0x40 时才能喂狗。
  • 如果太早喂狗(计数器 > WR)或太晚喂狗(计数器 < 0x40),都会触发复位。

📌 5. 启动看门狗(写入计数器值)

1
WWDG_Enable(WWDG_CNT);
  • 启动看门狗,注意只能通过写入一次计数器值来使能。

📌 6. 清除中断标志位(可选)

1
WWDG_ClearFlag();
  • 如果启用了中断,需要清除 “提前唤醒标志”。

📌 7. 配置中断控制器(NVIC)

1
WWDG_NVIC_Init();  // 设置优先级,开启中断通道
  • 可使用中断提前处理问题(在即将超时时触发)。

📌 8. 启用中断

1
WWDG_EnableIT();
  • 打开提前唤醒中断,使能中断响应函数 WWDG_IRQHandler()

📌 9. 喂狗操作(刷新计数器)

1
WWDG_SetCounter(cnt);  // 必须在窗口范围内
  • 若刷新时刻不满足 “窗口要求”,将立即复位系统。

🧩 三、WWDG 的中断服务函数解释

1
2
3
4
5
6
void WWDG_IRQHandler(void)
{
WWDG_SetCounter(WWDG_CNT); // 喂狗操作(在窗口内刷新)
WWDG_ClearFlag(); // 清除中断标志
LED1 = !LED1; // 可用于错误检测调试
}
  • 当计数器下降到某个值(接近 0x40)之前,WWDG 会发起提前中断,执行 WWDG_IRQHandler()
  • 你可以在此函数中喂狗或记录状态,以便采取补救措施。

PWM 定时器配置

PWM(Pulse Width Modulation,脉宽调制)是一种通过控制高电平占比(Duty Cycle)来模拟模拟信号或调节功率输出的方法。

TIM(定时器 / PWM)

寄存器 作用
PSC(预分频器) 控制定时器时钟分频(影响频率)
ARR(自动重载) 控制计数周期(越大周期越长)
CCR1~CCR4 比较 / 捕获寄存器(PWM 占空比、输入捕获等)
CNT 当前计数值
CR1 控制寄存器,启用 / 停止计数器、选择计数模式等
DIER 中断使能寄存器
SR 状态寄存器,是否溢出、中断等

🧭 二、TIM3 PWM 初始化配置流程

1
void TIM3_PWM_Init(u16 arr, u16 psc)
  • arr: 自动重装值(决定 PWM 周期)
  • psc: 时钟预分频值(降低计数频率)

🔄 PWM 输出流程图(TIM3 CH2 - PB5):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌────────────────────────────────────┐
│ 1. 打开时钟:TIM3 + GPIOB + AFIO │
└──────┬─────────────────────────────┘

┌───────────────────────────────┐
│ 2. 重映射 TIM3_CH2 到 PB5 │ ← GPIO_PinRemapConfig()
└──────┬────────────────────────┘

┌─────────────────────────────────────────────┐
│ 3. 初始化 GPIOB.5 为复用推挽输出 │ ← GPIO_Mode_AF_PP
└──────┬──────────────────────────────────────┘

┌────────────────────────────────────────────────────────┐
│ 4. 配置定时器 TIM3 时间基准 │
│ - 自动重装值 arr → 控制周期(Period) │
│ - 预分频值 psc → 控制计数速度(Prescaler) │
└──────┬─────────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│ 5. 配置通道2为 PWM 模式2(PWM2) │
│ - 高电平有效(极性高) │
│ - 输出使能 │
└──────┬─────────────────────────────────────────┘

┌───────────────────────────────┐
│ 6. 使能 CCR2 预装载寄存器 │ ← TIM_OC2PreloadConfig()
└──────┬────────────────────────┘

┌───────────────────────┐
│ 7. 启动定时器 TIM3 │ ← TIM_Cmd()
└───────────────────────┘

🧮 三、PWM 输出频率和占空比计算

设系统时钟 f_sys = 72MHz,则:

1
PWM_freq = f_sys / ((psc + 1) * (arr + 1))

例如:

1
2
TIM3_PWM_Init(7199, 0); // f_sys=72MHz
PWM_freq = 72MHz / ((0+1)*(7199+1)) = 10kHz

然后通过设置 TIM3->CCR2 来控制占空比:

1
2
// 占空比 = CCR2 / (ARR + 1)
TIM_SetCompare2(TIM3, 3600); // 占空比50%

🔄 PWM 模式说明(PWM1 vs PWM2)

模式 输出行为(在 CNT < CCR 时) 输出行为(在 CNT≥CCR 时)
PWM1 高电平 低电平
PWM2 低电平 高电平

你的代码中使用的是 PWM2 模式,所以:

  • CNT < CCR2 → 输出低
  • CNT ≥ CCR2 → 输出高

输入捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//定时器5通道1输入捕获配置

TIM_ICInitTypeDef TIM5_ICInitStructure;

void TIM5_Cap_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
NVIC_InitTypeDef NVIC_InitStructure;

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能TIM5时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能GPIOA时钟

GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; //PA0 清除之前设置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; //PA0 输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA,GPIO_Pin_0); //PA0 下拉

//初始化定时器5 TIM5
TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_Prescaler =psc; //预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位

//初始化TIM5输入捕获参数
TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; //CC1S=01 选择输入端 IC1映射到TI1上
TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获
TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; //映射到TI1上
TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM5_ICInitStructure.TIM_ICFilter = 0x00;//IC1F=0000 配置输入滤波器 不滤波
TIM_ICInit(TIM5, &TIM5_ICInitStructure);

//中断分组初始化
NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; //TIM3中断
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; //先占优先级2级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; //从优先级0级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; //IRQ通道被使能
NVIC_Init(&NVIC_InitStructure); //根据NVIC_InitStruct中指定的参数初始化外设NVIC寄存器

TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE);//允许更新中断 ,允许CC1IE捕获中断

TIM_Cmd(TIM5,ENABLE ); //使能定时器5



}

u8 TIM5CH1_CAPTURE_STA=0; //输入捕获状态
u16 TIM5CH1_CAPTURE_VAL; //输入捕获值

//定时器5中断服务程序
void TIM5_IRQHandler(void)
{

if((TIM5CH1_CAPTURE_STA&0X80)==0)//还未成功捕获
{
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)

{
if(TIM5CH1_CAPTURE_STA&0X40)//已经捕获到高电平了
{
if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)//高电平太长了
{
TIM5CH1_CAPTURE_STA|=0X80;//标记成功捕获了一次
TIM5CH1_CAPTURE_VAL=0XFFFF;
}else TIM5CH1_CAPTURE_STA++;
}
}
if (TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)//捕获1发生捕获事件
{
if(TIM5CH1_CAPTURE_STA&0X40) //捕获到一个下降沿
{
TIM5CH1_CAPTURE_STA|=0X80; //标记成功捕获到一次高电平脉宽
TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5);
TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
}else //还未开始,第一次捕获上升沿
{
TIM5CH1_CAPTURE_STA=0; //清空
TIM5CH1_CAPTURE_VAL=0;
TIM_SetCounter(TIM5,0);
TIM5CH1_CAPTURE_STA|=0X40; //标记捕获到了上升沿
TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); //CC1P=1 设置为下降沿捕获
}
}
}

TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); //清除中断标志位

}

当然可以。你提供的代码是基于 STM32 的 TIM5 定时器,通过 输入捕获模式 来检测外部信号的 高电平脉宽(比如测量 PWM 信号的占空比、红外脉冲、频率等)。我会分成几个部分来讲清楚你写的程序在做什么。


✅ 一、什么是外部输入捕获(Input Capture)?

输入捕获是定时器的一种功能,用来精确测量外部信号的时序特性,如:

  • 上升沿 / 下降沿发生的时间
  • 脉宽、周期
  • 信号频率

🧠 它的原理是:
当外部信号引脚上出现一个 ** 指定边沿(上升 / 下降)时,定时器的当前计数值被“锁存”** 进对应的捕获寄存器(如 CCR1)。

📌 1. 初始化函数 TIM5_Cap_Init

作用 内容
初始化 GPIO PA0 设置为输入、下拉模式(测量信号来自 PA0)
初始化 TIM5 基本参数 设置 ARRPSC 来定义计数频率和范围
设置输入捕获通道 捕获通道 1(CC1),第一次触发是上升沿捕获
开启中断 开启定时器更新中断(计数器溢出)和捕获中断(边沿)
启动定时器 启动 TIM5

📌 2. 中断服务函数 TIM5_IRQHandler

这个是程序的核心部分,执行流程大致如下:

➤ 【第一次捕获 - 上升沿】

1
if ((TIM5CH1_CAPTURE_STA & 0x40) == 0)
  • 第一次捕获,上升沿触发
  • 清零定时器,准备下一次捕获
  • 设置极性为 下降沿

➤ 【第二次捕获 - 下降沿】

1
if (TIM5CH1_CAPTURE_STA & 0x40)
  • 现在等待下降沿发生
  • 捕获下降沿时,CCR1 保存了从上升沿开始以来的时间(也就是高电平持续时间)
  • 标记完成 TIM5CH1_CAPTURE_STA |= 0x80

➤ 【计数器溢出处理】

1
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
  • 如果捕获过程中发生了溢出(比如高电平太长)
  • 累加溢出次数,如果次数太多就直接认为测量失败(0xFFFF

📌 3. 状态变量解析

1
2
u8  TIM5CH1_CAPTURE_STA=0;	// 捕获状态
u16 TIM5CH1_CAPTURE_VAL; // 捕获值(单位取决于定时器配置)
作用
bit7 (0x80) 表示捕获已完成
bit6 (0x40) 表示捕获到了第一次上升沿
bit5~0 表示计数器溢出次数

✅ 二、捕获的时长怎么换算成时间?

定时器的实际计数时间 = 定时器频率(单位时间) × CCR值

设定:

1
2
PSC = 71; // 72MHz / (71+1) = 1MHz(即1us一个计数)
ARR = 65535; // 最大值

那么:

1
2
捕获值 = 5000;
→ 表示脉宽 = 5000 * 1us = 5ms

✅ 三、应用举例

外设 / 信号 捕获方式
测量 PWM 占空比 连续测量高电平和低电平脉宽
红外解码 捕获每一段信号持续时间
超声测距 捕获反射回来的脉冲延迟
频率计 捕获信号周期 / 频率

✅ 四、总结

项目 说明
外部信号引脚 PA0 (TIM5_CH1)
捕获方式 上升沿→下降沿→计算高电平宽度
状态变量 用位标志记录捕获过程状态
中断处理 同时处理中断和溢出防护
应用场景 测量信号的高电平时间、频率等

触摸按键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#include "tpad.h"
#include "delay.h"
#include "usart.h"

#define TPAD_ARR_MAX_VAL 0XFFFF //最大的ARR值
vu16 tpad_default_val = 0; //空载的时候(没有手按下),计数器需要的时间


//初始化触摸按键
//获得空载的时候触摸按键的取值.
//返回值:0,初始化成功;1,初始化失败
u8 TPAD_Init(u8 psc)
{
u16 buf[10];
u16 temp;
u8 j, i;
TIM5_CH2_Cap_Init(TPAD_ARR_MAX_VAL, psc - 1); //以1Mhz的频率计数

for (i = 0; i < 10; i++) { //连续读取10次
buf[i] = TPAD_Get_Val();
delay_ms(10);
}

for (i = 0; i < 9; i++) { //排序
for (j = i + 1; j < 10; j++) {
if (buf[i] > buf[j]) { //升序排列
temp = buf[i];
buf[i] = buf[j];
buf[j] = temp;
}
}
}

temp = 0;

for (i = 2; i < 8; i++) {
temp += buf[i]; //取中间的6个数据进行平均
}

tpad_default_val = temp / 6;
printf("tpad_default_val:%d\r\n", tpad_default_val);

if (tpad_default_val > TPAD_ARR_MAX_VAL / 2) {
return 1; //初始化遇到超过TPAD_ARR_MAX_VAL/2的数值,不正常!
}

return 0;
}
//复位一次
void TPAD_Reset(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能PA端口时钟
//设置GPIOA.1为推挽使出
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //PA1 端口配置
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; //推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_ResetBits(GPIOA, GPIO_Pin_1); //PA.1输出0,放电
delay_ms(5);
TIM_SetCounter(TIM5, 0); //归0
TIM_ClearITPendingBit(TIM5, TIM_IT_CC2 | TIM_IT_Update); //清除中断标志
//设置GPIOA.1为浮空输入
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
//得到定时器捕获值
//如果超时,则直接返回定时器的计数值.
u16 TPAD_Get_Val(void)
{
TPAD_Reset();

while (TIM_GetFlagStatus(TIM5, TIM_IT_CC2) == RESET) { //等待捕获上升沿
if (TIM_GetCounter(TIM5) > TPAD_ARR_MAX_VAL - 500) {
return TIM_GetCounter(TIM5); //超时了,直接返回CNT的值
}
};

return TIM_GetCapture2(TIM5);
}
//读取n次,取最大值
//n:连续获取的次数
//返回值:n次读数里面读到的最大读数值
u16 TPAD_Get_MaxVal(u8 n)
{
u16 temp = 0;
u16 res = 0;

while (n--) {
temp = TPAD_Get_Val(); //得到一次值

if (temp > res) {
res = temp;
}
};

return res;
}
//扫描触摸按键
//mode:0,不支持连续触发(按下一次必须松开才能按下一次);1,支持连续触发(可以一直按下)
//返回值:0,没有按下;1,有按下;
#define TPAD_GATE_VAL 100 //触摸的门限值,也就是必须大于tpad_default_val+TPAD_GATE_VAL,才认为是有效触摸.
u8 TPAD_Scan(u8 mode)
{
static u8 keyen = 0; //0,可以开始检测;>0,还不能开始检测
u8 res = 0;
u8 sample = 3; //默认采样次数为3次
u16 rval;

if (mode) {
sample = 6; //支持连按的时候,设置采样次数为6次
keyen = 0; //支持连按
}

rval = TPAD_Get_MaxVal(sample);

if (rval > (tpad_default_val + TPAD_GATE_VAL)) { //大于tpad_default_val+TPAD_GATE_VAL,有效
if (keyen == 0) {
res = 1; //keyen==0,有效
}

//printf("r:%d\r\n",rval);
keyen = 3; //至少要再过3次之后才能按键有效
}

if (keyen) {
keyen--;
}

return res;
}
//定时器2通道2输入捕获配置
void TIM5_CH2_Cap_Init(u16 arr, u16 psc)
{
GPIO_InitTypeDef GPIO_InitStructure;
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_ICInitTypeDef TIM5_ICInitStructure;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE); //使能TIM5时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //使能PA端口时钟
//设置GPIOA.1为浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; //PA1 端口配置
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; //速度50MHz
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
GPIO_Init(GPIOA, &GPIO_InitStructure); //设置为浮空输入
//初始化TIM5
TIM_TimeBaseStructure.TIM_Period = arr; //设定计数器自动重装值
TIM_TimeBaseStructure.TIM_Prescaler = psc; //预分频器
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; //TIM向上计数模式
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); //根据TIM_TimeBaseInitStruct中指定的参数初始化TIMx的时间基数单位
//初始化通道2
TIM5_ICInitStructure.TIM_Channel = TIM_Channel_2; //CC1S=01 选择输入端 IC2映射到TI5上
TIM5_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; //上升沿捕获
TIM5_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM5_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; //配置输入分频,不分频
TIM5_ICInitStructure.TIM_ICFilter = 0x03;//IC2F=0011 配置输入滤波器 8个定时器时钟周期滤波
TIM_ICInit(TIM5, &TIM5_ICInitStructure);//初始化I5 IC2
TIM_Cmd(TIM5, ENABLE); //使能定时器5
}



🔧 原理概述:电容充电时间 + 输入捕获

  • 人体手指靠近触摸电极(如 PA1)时,由于人体的电容影响,会使得电极的充电速度变慢
  • 通过 STM32 的定时器输入捕获,可以测量 PA1 从低电平变为高电平的时间
  • 这个时间变长 → 表示可能有触摸发生。

🌀 工作流程分析

1. 初始化

函数:

1
TPAD_Init(u8 psc);
  • 设置定时器 TIM5 通道 2(CH2)为输入捕获模式(上升沿触发)。
  • 设置时基为 1 MHz(1 µs 分辨率)。
  • 连续采样 10 次,排序去除最大 / 最小值后求平均,得出一个空载基准值 tpad_default_val
  • 这个值表示 “没有手触摸时” 的正常充电时间。

2. 一次测量

函数:

1
TPAD_Get_Val();

测量流程:

  • 调用 TPAD_Reset()
    • 将 PA1 设置为推挽输出,输出低电平,给 “触摸电极” 放电。
    • 然后设置为浮空输入,让它自己 “慢慢充电”(通过体内上拉电阻、电容等)。
  • 此时定时器开始计时,等待 PA1 电平由 0 → 1。
  • 输入捕获通道(CH2)在检测到上升沿时会记录 CNT 值,表示电容充电到高电平所需时间。
  • 如果超过最大时间仍未触发上升沿,则视为超时。

3. 判断触摸

函数:

1
TPAD_Scan(mode);

流程如下:

  • 多次调用 TPAD_Get_Val() 获取最大值 rval
  • 如果 rval > tpad_default_val + TPAD_GATE_VAL(大于一定门限):
    • 认为有触摸事件。
    • 返回 1。
  • 还支持非连续 / 连续触发的模式控制。

RTC

二、RTC 配置流程总览

✅ 1. 启用时钟 & 访问后备寄存器

1
2
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
  • PWRBKP 是 RTC 模块相关的外设,必须开启。
  • PWR_BackupAccessCmd(ENABLE) 开启后备区域写访问权限(RTC 所在区域)。

✅ 2. 判断是否第一次配置 RTC

1
if (BKP_ReadBackupRegister(BKP_DR1) != 0x5050)
  • 使用备份寄存器 BKP_DR1 存储 “配置标志”,避免重复初始化。
  • 如果读取的值不等于 0x5050,表示 “第一次配置 RTC”。

✅ 3. 启动 LSE 外部低速晶振(32.768 kHz)

1
2
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET && temp<250)
  • 配置 LSE(Low Speed External)为 RTC 时钟源。
  • 等待晶振稳定(最多等 2.5 秒)。

✅ 4. 配置 RTC 时钟源并启动 RTC

1
2
3
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
  • 选择 LSE 作为 RTC 时钟源。
  • 开启 RTC。
  • 等待寄存器同步完成。

✅ 5. 设置中断 & 初始化时间

1
2
3
RTC_ITConfig(RTC_IT_SEC, ENABLE);	// 开启秒中断
RTC_SetPrescaler(32767); // 设置预分频值,使 RTC 秒中断每秒触发一次
RTC_Set(2015,1,14,17,42,55); // 设置初始时间
  • 32767 是因为:LSE = 32.768kHz,32768 分频后为 1Hz。
  • 将人类可读时间转换为自 1970 年起的 “总秒数”。

✅ 6. 写入标志 & 配置 NVIC 中断

1
2
BKP_WriteBackupRegister(BKP_DR1, 0x5050);
RTC_NVIC_Config();
  • 写入初始化标志。
  • 配置 NVIC(嵌套中断控制器)允许 RTC 中断触发。

⏰ 三、RTC 中断服务函数 RTC_IRQHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void RTC_IRQHandler(void)
{
if (RTC_GetITStatus(RTC_IT_SEC) != RESET)
{
RTC_Get(); // 每秒更新时间
}
if (RTC_GetITStatus(RTC_IT_ALR) != RESET)
{
RTC_ClearITPendingBit(RTC_IT_ALR);
RTC_Get(); // 闹钟触发
printf("Alarm Time: ...");
}
RTC_ClearITPendingBit(RTC_IT_SEC | RTC_IT_OW);
}
  • 秒中断 RTC_IT_SEC:每秒调用一次,用于更新时间。
  • 闹钟中断 RTC_IT_ALR:闹钟时间到了触发,可用于事件提醒等。

⌚ 四、时间设置函数 RTC_Set

1
RTC_Set(年, 月, 日, 时, 分, 秒)
  • 将年月日时分秒转换成 自 1970-01-01 起的秒数
  • 调用 RTC_SetCounter(seccount); 设置 RTC 当前计数值。

⏰ 五、设置闹钟 RTC_Alarm_Set

RTC_Set 类似,也是转成秒数:

1
RTC_SetAlarm(seccount);
  • 之后时间到了就会触发 RTC_IT_ALR 中断。

🕒 六、读取当前时间 RTC_Get

1
RTC_GetCounter() -> 当前秒数
  • 根据累计的秒数,倒推出当前的年月日、时分秒、星期。
  • 注意闰年判断、月份天数处理都在这个函数里。

📅 七、获取星期 RTC_Get_Week

使用公式推算出当前日期对应的星期几

待机唤醒

💡 一、待机唤醒核心原理

  • 待机模式(Standby) 是 STM32 功耗最低的模式(比休眠还低)。
  • 所有外设都关闭,仅保持少量 RTC 和后备寄存器。
  • 唤醒方式主要通过:
    • WKUP 引脚(PA0,低电平到高电平的跳变)
    • RTC 闹钟唤醒
    • 独立看门狗(IWDG)

🧭 二、配置步骤解析(基于 WKUP 引脚唤醒)


① 配置 IO 和中断(WKUP_Init

1
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);

启用 GPIOA 和 AFIO 时钟,为中断配置和输入功能做准备。

1
2
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; // 下拉输入

PA0 配置为下拉输入(防止浮空误触发)。

1
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); 

连接 PA0 到 EXTI0 外部中断线路。

1
2
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising;

设置为中断模式,触发方式是上升沿(即从低到高的电平跳变)。

1
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;

配置中断优先级,并启用中断。


② 按键检测 + 唤醒判断(Check_WKUP

1
u8 Check_WKUP(void)

这个函数通过延时轮询方式,判断 WKUP 是否被长按超过 3 秒(用于防止误触)。


③ 待机模式函数(Sys_StandbySys_Enter_Standby

1
2
PWR_WakeUpPinCmd(ENABLE); 
PWR_EnterSTANDBYMode();
  • 开启 WKUP 引脚唤醒功能
  • 执行 PWR_EnterSTANDBYMode() 进入待机,执行后 MCU 会停止运行(最低功耗)

④ EXTI0 中断处理函数(EXTI0_IRQHandler

1
2
EXTI_ClearITPendingBit(EXTI_Line0);
if(Check_WKUP()) Sys_Enter_Standby();

触发中断后:

  • 清除中断标志位
  • 判断是否按下超 3 秒
  • 是则进入 Sys_Enter_Standby(),让系统转入待机

✅ 总结:待机唤醒配置重点

步骤 关键点说明
GPIO 配置 PA0 输入模式 + 下拉
EXTI 配置 上升沿中断触发
PWR 配置 开启 PWR / WKUP
进入待机 PWR_EnterSTANDBYMode()
唤醒触发源 PA0 的上升沿(高电平)
唤醒后状态 系统复位重启,从 main() 开始

ADC

一、ADC 初始化流程(Adc_Init()

① 启用 ADC 和 GPIO 时钟

1
2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
RCC_ADCCLKConfig(RCC_PCLK2_Div6); // 设置 ADC 时钟为 PCLK2 的 1/6,12MHz
  • STM32F1 的 ADC 最大时钟为 14MHz
  • 如果主频是 72MHz,需要除以 6 得到 12MHz,确保 ADC 工作正常

② 配置 GPIO 为模拟输入(AIN)

1
2
3
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
  • ADC1 通道 1(PA1)需要设置为 模拟输入(AIN) 模式
  • GPIO 在模拟模式下不会开启输入 / 输出缓冲,适合高阻抗电压输入

③ 配置 ADC 基本参数

1
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
  • 设置为独立模式(即只用 ADC1,不与 ADC2 协同工作)
1
ADC_InitStructure.ADC_ScanConvMode = DISABLE;
  • 扫描模式关闭,仅转换一个通道
1
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
  • 非连续模式,即每次转换需要手动触发
1
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
  • 不使用外部触发,采用软件触发
1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
  • 采样结果右对齐(12 位有效数据)
1
ADC_InitStructure.ADC_NbrOfChannel = 1;
  • 设置转换的通道数为 1
1
2
ADC_Init(ADC1, &ADC_InitStructure);
ADC_Cmd(ADC1, ENABLE); // 使能 ADC

④ 启动 ADC 校准(必须的)

1
2
3
4
ADC_ResetCalibration(ADC1);
while(ADC_GetResetCalibrationStatus(ADC1));
ADC_StartCalibration(ADC1);
while(ADC_GetCalibrationStatus(ADC1));
  • 重置并启动校准,是提高 ADC 精度的关键步骤
  • 校准完成后才能获得准确值

🔍 二、ADC 获取数值(Get_Adc()

1
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5);
  • 设置要转换的 ADC 通道,比如 ch = 1 对应 PA1(ADC1_IN1)
  • 设置采样时间为 239.5 个周期,采样时间越长精度越高但速度越慢
1
2
3
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 软件启动转换
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); // 等待转换完成
return ADC_GetConversionValue(ADC1); // 返回转换结果
  • 启动 ADC
  • 等待转换完成(EOC 标志置位)
  • 读取转换结果(12 位无符号整数,范围 0~4095)

📊 三、ADC 多次采样平均(Get_Adc_Average()

1
2
3
4
5
6
for(t = 0; t < times; t++)
{
temp_val += Get_Adc(ch);
delay_ms(5);
}
return temp_val / times;
  • 重复采样 times 次,取平均值,降低噪声
  • 通常用在电压读取、温度检测等需要稳定数值的场合

DAC

🧭 一、DAC 的基本概念

DAC(Digital-to-Analog Converter,数模转换器)可将 MCU 内部的数字值转换为电压信号输出。


⚙️ 二、DAC 初始化配置流程(Dac1_Init()

① 使能时钟

1
2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);  // GPIOA 时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE); // DAC 时钟
  • 必须先开启 GPIOA 和 DAC 外设的时钟才能对其进行配置

② 配置 DAC 对应的 GPIO 为模拟输入

1
2
3
4
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
  • DAC 通道 1 对应 PA4
  • 这里设置为 GPIO_Mode_AIN 是为了关闭数字输入 / 输出缓冲,防止干扰,实际上 DAC 输出时此引脚为 模拟电压输出

③ 配置 DAC 控制参数

1
DAC_InitType.DAC_Trigger = DAC_Trigger_None;
  • 不使用触发模式,数据一写入立即更新输出
1
DAC_InitType.DAC_WaveGeneration = DAC_WaveGeneration_None;
  • 不使用内置波形生成器(如噪声或三角波)
1
DAC_InitType.DAC_LFSRUnmask_TriangleAmplitude = DAC_LFSRUnmask_Bit0;
  • 此设置用于波形生成器,这里没有用到,可以保留默认值
1
DAC_InitType.DAC_OutputBuffer = DAC_OutputBuffer_Disable;
  • 关闭输出缓存(BOFF = 1),电压跟随能力较弱,但功耗较低
  • 若希望输出驱动能力增强,可改为 DAC_OutputBuffer_Enable
1
DAC_Init(DAC_Channel_1, &DAC_InitType);
  • 初始化 DAC 通道 1 的参数

④ 启用 DAC 并设置初始值

1
2
3
DAC_Cmd(DAC_Channel_1, ENABLE);  // 启用 DAC 通道 1

DAC_SetChannel1Data(DAC_Align_12b_R, 0); // 设置输出值为 0(最低电压)
  • DAC 采用 12 位右对齐格式,有效数据范围为 0 ~ 4095

🔋 三、设置电压值函数(Dac1_Set_Vol()

1
2
3
4
5
6
7
void Dac1_Set_Vol(u16 vol)
{
float temp = vol;
temp /= 1000; // mV 转换为 V
temp = temp * 4096 / 3.3; // 转换为 DAC 数值范围
DAC_SetChannel1Data(DAC_Align_12b_R, temp);
}
  • 参数 vol 为电压值,单位为 毫伏(mV)
  • STM32 DAC 最大输出电压为 Vref+(通常等于 3.3V)
  • 通过比例换算,将 03300mV 转换为 04095 的 DAC 数值

例:
若想输出 1.65V:
Dac1_Set_Vol(1650); → 实际输出电压 ≈ 1.65V

DMA

🧠 一、什么是 DMA?

DMA 是一个硬件控制器,可以在内存和外设之间自动搬运数据,无需 CPU 介入,大幅度提高数据传输效率,减少 CPU 负载。

适用于:

  • ADC 采样数据自动搬运
  • 串口/定时器/DAC 等周期性数据传输
  • 批量数据发送 / 接收(缓冲区传输)

⚙️ 二、DMA 初始化配置流程(MYDMA_Config

函数原型:

1
void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx, u32 cpar, u32 cmar, u16 cndtr);
参数 说明
DMA_CHx 指定 DMA 通道
cpar 外设寄存器地址
cmar 内存缓冲区地址
cndtr 数据传输长度(单位:字节)

① 使能 DMA 时钟

1
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
  • STM32F1 的 DMA 控制器挂在 AHB 总线上
  • 必须先开启对应 DMA 控制器(如 DMA1)

② 清除通道状态并设置结构体参数

1
DMA_DeInit(DMA_CHx); // 重置通道

防止旧配置干扰新配置。

1
2
3
4
DMA1_MEM_LEN = cndtr;
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar;
DMA_InitStructure.DMA_MemoryBaseAddr = cmar;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
  • 设置 DMA 传输方向:从内存 → 外设(如 USART 发数据、DAC 输出)
  • 可根据需要改为 DMA_DIR_PeripheralSRC(外设 → 内存,如 ADC)
1
2
3
DMA_InitStructure.DMA_BufferSize = cndtr;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
  • 外设寄存器地址固定(通常为 USARTx_DR 或 DAC_DHRx)
  • 内存地址递增(从数组或缓冲区连续读取)
1
2
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
  • 设置传输单位:这里使用 8 位传输(常用于字符、波形)
1
2
3
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
  • DMA_Mode_Normal:传输一次就停止
  • 可改为 DMA_Mode_Circular 实现循环传输(如 ADC 连续采样)
  • DMA_M2M 禁用表示 外设与内存之间传输,否则就是内存对内存拷贝

③ 初始化 DMA 通道

1
DMA_Init(DMA_CHx, &DMA_InitStructure);
  • 最终调用 HAL 底层库函数设置寄存器

🔁 三、启动 DMA 传输(MYDMA_Enable()

1
2
3
4
5
6
void MYDMA_Enable(DMA_Channel_TypeDef* DMA_CHx)
{
DMA_Cmd(DMA_CHx, DISABLE); // 先关闭
DMA_SetCurrDataCounter(DMA_CHx, DMA1_MEM_LEN); // 设置长度
DMA_Cmd(DMA_CHx, ENABLE); // 启用
}

⚠️ 注意:

  • 每次传输前 必须重新设置长度和打开通道
  • 某些 DMA 通道在传输完成后自动关闭(普通模式)