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

CMSIS 标准中最主要的为 CMSIS 核心层,它包括了:
- 内核函数层:其中包含用于访问内核寄存器的名称、地址定义,主要由 ARM 公司提供。
- 设备外设访问层:提供了片上的核外外设的地址和中断定义,主要由芯片生产商提供。
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); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure); GPIO_SetBits(GPIOB,GPIO_Pin_5);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5; GPIO_Init(GPIOE, &GPIO_InitStructure); GPIO_SetBits(GPIOE,GPIO_Pin_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_InitTypeDef GPIO_InitStructure; USART_InitTypeDef USART_InitStructure; NVIC_InitTypeDef NVIC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1|RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure);
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);
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); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); 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
| void USART1_IRQHandler(void) { u8 Res; #if SYSTEM_SUPPORT_OS OSIntEnter(); #endif if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET) { Res =USART_ReceiveData(USART1); if((USART_RX_STA&0x8000)==0) { if(USART_RX_STA&0x4000) { if(Res!=0x0a)USART_RX_STA=0; else USART_RX_STA|=0x8000; } else { 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 OSIntExit(); #endif } #endif
|
🔧 串口配置流程说明(适用于 STM32 标准库)
✅ 1. 使能时钟
1
| RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);
|
- 使能 USART1 和 GPIOA 的时钟;
- 所有外设在使用前都需要先打开其时钟。
✅ 2. 配置串口 TX(发送)引脚
1 2 3 4
| GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(GPIOA, &GPIO_InitStructure);
|
PA9 为 USART1_TX;
- 配置为复用功能 + 推挽输出,适合高速串口发送。
✅ 3. 配置串口 RX(接收)引脚
1 2 3
| GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure);
|
PA10 为 USART1_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);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource2);
EXTI_InitStructure.EXTI_Line=EXTI_Line2; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource3); EXTI_InitStructure.EXTI_Line=EXTI_Line3; EXTI_Init(&EXTI_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOE,GPIO_PinSource4); EXTI_InitStructure.EXTI_Line=EXTI_Line4; EXTI_Init(&EXTI_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA,GPIO_PinSource0);
EXTI_InitStructure.EXTI_Line=EXTI_Line0; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; EXTI_Init(&EXTI_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x03; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
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_InitStructure.NVIC_IRQChannel = EXTI3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x01; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
NVIC_InitStructure.NVIC_IRQChannel = EXTI4_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0x02; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0x00; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); }
|
✅ 步骤 1:按键引脚初始化
此函数一般配置按键对应的 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.2 → EXTI_Line2(KEY2)
GPIOE.3 → EXTI_Line3(KEY1)
GPIOE.4 → EXTI_Line4(KEY0)
GPIOA.0 → EXTI_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; 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
|
void IWDG_Init(u8 prer,u16 rlr) { IWDG_WriteAccessCmd(IWDG_WriteAccess_Enable); IWDG_SetPrescaler(prer); IWDG_SetReload(rlr); IWDG_ReloadCounter(); IWDG_Enable(); }
void IWDG_Feed(void) { IWDG_ReloadCounter(); }
|
独立看门狗(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 重装载值
- 说明:重装载寄存器 (
IWDG_RLR) 用来设定 IWDG 计数器的重载值,这个值决定了 IWDG 超时的触发时间。
- 重装载寄存器的有效位为 低 11 位。
- 超时计算公式:
Tout = ((4 * 2^prer) * rlr) / 40(单位:毫秒)。
- 步骤:调用
IWDG_SetReload 函数,传入所需的重装载值 rlr,控制看门狗的超时触发时间。
4. 重装载 IWDG 计数器
- 说明:每次触发 IWDG 计数器重装载 都会将计数器重新加载为 重装载寄存器的值,以避免计数器溢出。
- 步骤:调用
IWDG_ReloadCounter 函数,重装载计数器,以防止触发系统复位。
5. 使能 IWDG
- 说明:最后,启用 IWDG。一旦启用,IWDG 会开始计数,并在计数器超时时触发系统复位。
- 步骤:调用
IWDG_Enable 函数,启动看门狗定时器。
6. 喂养 IWDG(防止复位)
- 说明:喂养 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
|
u8 WWDG_CNT=0x7f;
void WWDG_Init(u8 tr,u8 wr,u32 fprer) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
WWDG_CNT=tr&WWDG_CNT; WWDG_SetPrescaler(fprer);
WWDG_SetWindowValue(wr);
WWDG_Enable(WWDG_CNT);
WWDG_ClearFlag();
WWDG_NVIC_Init();
WWDG_EnableIT(); }
void WWDG_Set_Counter(u8 cnt) { WWDG_Enable(cnt); }
void WWDG_NVIC_Init() { NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = WWDG_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE; NVIC_Init(&NVIC_InitStructure); }
void WWDG_IRQHandler(void) {
WWDG_SetCounter(WWDG_CNT);
WWDG_ClearFlag();
LED1=!LED1; }
|
🆚 一、窗口看门狗(WWDG) vs 独立看门狗(IWDG)
| 特性对比 |
独立看门狗 IWDG |
窗口看门狗 WWDG |
| 时钟源 |
独立内部低速时钟(LSI,通常为 40 kHz) |
主系统时钟 PCLK1 分频得到 |
| 复位触发方式 |
超时未喂狗就会复位系统 |
喂狗过早、过晚都会触发复位 |
| 响应机制 |
只要求在超时时间内任意时刻喂狗 |
只能在 “允许的窗口时间” 内喂狗,否则视为异常 |
| 窗口机制 |
无 |
有(例如只能在计数器低于某个值时喂狗) |
| 中断功能 |
通常用于系统复位,不支持提前中断 |
支持提前唤醒中断,允许在接近超时时提前处理或记录 |
| 适用场景 |
稳定性要求高,不依赖系统时钟 |
时间控制更精确场合,适合更严格系统状态监控 |
| 优缺点 |
简单可靠,适合低功耗 |
灵活复杂,可中断处理但依赖主系统 |
🧭 二、窗口看门狗 WWDG 配置流程详解
📌 1. 使能看门狗时钟
1
| RCC_APB1PeriphClockCmd(RCC_APB1Periph_WWDG, ENABLE);
|
- 打开
WWDG 外设的时钟(连接在 APB1 总线上)。
📌 2. 设置计数器初值(TR)
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. 启动看门狗(写入计数器值)
📌 6. 清除中断标志位(可选)
📌 7. 配置中断控制器(NVIC)
📌 8. 启用中断
- 打开提前唤醒中断,使能中断响应函数
WWDG_IRQHandler()。
📌 9. 喂狗操作(刷新计数器)
🧩 三、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); PWM_freq = 72MHz / ((0+1)*(7199+1)) = 10kHz
|
然后通过设置 TIM3->CCR2 来控制占空比:
1 2
| TIM_SetCompare2(TIM3, 3600);
|
🔄 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
|
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); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_ResetBits(GPIOA,GPIO_Pin_0); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler =psc; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); TIM5_ICInitStructure.TIM_Channel = TIM_Channel_1; 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 = 0x00; TIM_ICInit(TIM5, &TIM5_ICInitStructure); NVIC_InitStructure.NVIC_IRQChannel = TIM5_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); TIM_ITConfig(TIM5,TIM_IT_Update|TIM_IT_CC1,ENABLE); TIM_Cmd(TIM5,ENABLE );
}
u8 TIM5CH1_CAPTURE_STA=0; u16 TIM5CH1_CAPTURE_VAL;
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) { if(TIM5CH1_CAPTURE_STA&0X40) { TIM5CH1_CAPTURE_STA|=0X80; TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5); TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); }else { TIM5CH1_CAPTURE_STA=0; TIM5CH1_CAPTURE_VAL=0; TIM_SetCounter(TIM5,0); TIM5CH1_CAPTURE_STA|=0X40; TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); } } } TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update); }
|
当然可以。你提供的代码是基于 STM32 的 TIM5 定时器,通过 输入捕获模式 来检测外部信号的 高电平脉宽(比如测量 PWM 信号的占空比、红外脉冲、频率等)。我会分成几个部分来讲清楚你写的程序在做什么。
输入捕获是定时器的一种功能,用来精确测量外部信号的时序特性,如:
- 上升沿 / 下降沿发生的时间
- 脉宽、周期
- 信号频率
🧠 它的原理是:
当外部信号引脚上出现一个 ** 指定边沿(上升 / 下降)时,定时器的当前计数值被“锁存”** 进对应的捕获寄存器(如 CCR1)。
📌 1. 初始化函数 TIM5_Cap_Init
| 作用 |
内容 |
| 初始化 GPIO |
将 PA0 设置为输入、下拉模式(测量信号来自 PA0) |
| 初始化 TIM5 基本参数 |
设置 ARR 和 PSC 来定义计数频率和范围 |
| 设置输入捕获通道 |
捕获通道 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
| 捕获值 = 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 vu16 tpad_default_val = 0;
u8 TPAD_Init(u8 psc) { u16 buf[10]; u16 temp; u8 j, i; TIM5_CH2_Cap_Init(TPAD_ARR_MAX_VAL, psc - 1);
for (i = 0; i < 10; i++) { 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]; }
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; }
return 0; }
void TPAD_Reset(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; 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); delay_ms(5); TIM_SetCounter(TIM5, 0); TIM_ClearITPendingBit(TIM5, TIM_IT_CC2 | TIM_IT_Update); 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); } };
return TIM_GetCapture2(TIM5); }
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; }
#define TPAD_GATE_VAL 100 u8 TPAD_Scan(u8 mode) { static u8 keyen = 0; u8 res = 0; u8 sample = 3; u16 rval;
if (mode) { sample = 6; keyen = 0; }
rval = TPAD_Get_MaxVal(sample);
if (rval > (tpad_default_val + TPAD_GATE_VAL)) { if (keyen == 0) { res = 1; }
keyen = 3; }
if (keyen) { keyen--; }
return res; }
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); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure); TIM_TimeBaseStructure.TIM_Period = arr; TIM_TimeBaseStructure.TIM_Prescaler = psc; TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure); TIM5_ICInitStructure.TIM_Channel = TIM_Channel_2; 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; TIM_ICInit(TIM5, &TIM5_ICInitStructure); TIM_Cmd(TIM5, ENABLE); }
|
🔧 原理概述:电容充电时间 + 输入捕获
- 当人体手指靠近触摸电极(如 PA1)时,由于人体的电容影响,会使得电极的充电速度变慢。
- 通过 STM32 的定时器输入捕获,可以测量 PA1 从低电平变为高电平的时间。
- 这个时间变长 → 表示可能有触摸发生。
🌀 工作流程分析
1. 初始化
函数:
- 设置定时器 TIM5 通道 2(CH2)为输入捕获模式(上升沿触发)。
- 设置时基为 1 MHz(1 µs 分辨率)。
- 连续采样 10 次,排序去除最大 / 最小值后求平均,得出一个空载基准值
tpad_default_val。
- 这个值表示 “没有手触摸时” 的正常充电时间。
2. 一次测量
函数:
测量流程:
- 调用
TPAD_Reset():
- 将 PA1 设置为推挽输出,输出低电平,给 “触摸电极” 放电。
- 然后设置为浮空输入,让它自己 “慢慢充电”(通过体内上拉电阻、电容等)。
- 此时定时器开始计时,等待 PA1 电平由 0 → 1。
- 输入捕获通道(CH2)在检测到上升沿时会记录 CNT 值,表示电容充电到高电平所需时间。
- 如果超过最大时间仍未触发上升沿,则视为超时。
3. 判断触摸
函数:
流程如下:
- 多次调用
TPAD_Get_Val() 获取最大值 rval。
- 如果
rval > tpad_default_val + TPAD_GATE_VAL(大于一定门限):
- 还支持非连续 / 连续触发的模式控制。
RTC
二、RTC 配置流程总览
✅ 1. 启用时钟 & 访问后备寄存器
1 2
| RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE);
|
- PWR 和 BKP 是 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_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 类似,也是转成秒数:
- 之后时间到了就会触发
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)
这个函数通过延时轮询方式,判断 WKUP 是否被长按超过 3 秒(用于防止误触)。
③ 待机模式函数(Sys_Standby 和 Sys_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);
|
- 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;
|
1
| ADC_InitStructure.ADC_NbrOfChannel = 1;
|
1 2
| ADC_Init(ADC1, &ADC_InitStructure); ADC_Cmd(ADC1, ENABLE);
|
④ 启动 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); RCC_APB1PeriphClockCmd(RCC_APB1Periph_DAC, ENABLE);
|
- 必须先开启 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 2 3
| DAC_Cmd(DAC_Channel_1, ENABLE);
DAC_SetChannel1Data(DAC_Align_12b_R, 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; temp = temp * 4096 / 3.3; DAC_SetChannel1Data(DAC_Align_12b_R, temp); }
|
- 参数
vol 为电压值,单位为 毫伏(mV)
- STM32 DAC 最大输出电压为 Vref+(通常等于 3.3V)
- 通过比例换算,将 0
3300mV 转换为 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 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);
|
🔁 三、启动 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 通道在传输完成后自动关闭(普通模式)