STM32
STM32简介
STM32是意法半导体(STMicroelectronics)推出的一系列基于ARM Cortex-M内核的微控制器。该系列微控制器以其高性能、低功耗和丰富的外设选项而广泛应用于各种嵌入式系统。
主要特点
-
核心架构:
- Cortex-M0:低功耗,适合简单应用。
- Cortex-M3/M4:平衡性能与功耗,适合中等复杂度的应用,M4支持数字信号处理(DSP)。
- Cortex-M7:高性能,适合需要计算密集型处理的应用。
-
多样化型号:
- STM32系列包含多个家族,如STM32F(通用)、STM32L(低功耗)、STM32H(高性能)等,满足不同需求。
-
丰富的外设:
- 丰富的外设接口,包括GPIO、定时器、ADC、DAC、UART、SPI、I2C等。
- 许多型号还包括USB、CAN和以太网等高级特性。
-
开发工具:
- STM32支持多种开发环境(如STM32CubeIDE、Keil、IAR等),并提供丰富的软件库和硬件抽象层(HAL),简化开发流程。
STM32总体架构
启动配置
STM32F10xxx 复位
STM32F10xxx微控制器支持三种复位形式:系统复位、电源复位和备份区域复位。
1. 系统复位
系统复位会将所有寄存器复位为初始状态,除了时钟控制器的RCC_CSR寄存器中的复位标志位和备份区域中的寄存器。
系统复位的触发事件包括:
1
来实现。识别复位事件
可以通过查看RCC_CSR控制状态寄存器中的复位状态标志位来识别复位事件的来源。
软件复位
通过设置Cortex-M3的复位控制寄存器中的SYSRESETREQ位为1
,可以触发软件复位。有关详细信息,请参考Cortex-M3技术参考手册。
低功耗管理复位
低功耗管理复位可以在以下情况下产生:
-
进入待机模式时:
- 通过将用户选择字节中的nRST_STDBY位置为
1
,使能该复位。 - 在此情况下,即使执行了进入待机模式的过程,系统会被复位,而不是进入待机模式。
-
进入停止模式时:
- 通过将用户选择字节中的nRST_STOP位置为
1
,使能该复位。 - 同样地,即使执行了进入停机模式的过程,系统会被复位,而不是进入停机模式。
2. 电源复位
电源复位发生在以下事件之一发生时:
电源复位将复位除了备份区域外的所有寄存器。复位信号将在NRST引脚上输出,脉冲发生器确保每个复位源(外部或内部)都有至少20μs的脉冲延时。复位入口矢量被固定在地址0x0000 0004
。
3. 备份区域复位
备份区域有两个专门的复位,它们只影响备份区域。备份区域复位的触发事件包括:
- 软件复位:通过设置备份域控制寄存器(RCC_BDCR)中的BDRST位来产生。
- 在VDD和VBAT两者掉电的情况下,VDD或VBAT上电将引发备份区域复位。
时钟系统
STM32微控制器的时钟系统是其核心组成部分之一,负责提供和管理各种外设和核心的时钟信号。
1. 时钟源
STM32微控制器支持多种时钟源,以满足不同的应用需求:
在STM32中有3种不同的时钟源用来驱动系统时钟(SYSCLK):
- HSI振荡器时钟(High Speed Internal oscillator,高速内部时钟)
- HSE振荡器时钟(High Speed External(Oscillator / Clock),高速外部时钟)
- PLL时钟(Phase Locked Loop 锁相环/倍频器)
还有2种2级时钟:
- LSI时钟(Low Speed Internal,低速内部时钟)
- LSE时钟(Low Speed External oscillator,低速外部时钟)。
提供多种时钟源的主要目标是节能。具体原因包括:
根据需求选择时钟:
动态关闭不必要的时钟:
适应不同工作模式:
2.时钟树
高速外部时钟是由外部时钟源提供,目前几乎所有的STM32单片机的设计都是在外部接一个8MHz的晶振,经过PLL倍频(9倍频)后得到一个72MHz的系统时钟。
如果HSE晶体振荡器失效,HSI时钟会被作为备用时钟源。
GPIO外设
GPIO(General-purpose Input/Output,通用型输入输出)是微控制器中最基本的外设之一。在STM32微控制器中,GPIO引脚允许用户通过程序控制或读取其状态。
GPIO的主要功能
-
输入功能:
- GPIO引脚可以配置为输入模式,读取外部信号(如开关、传感器输出等)。
- 支持上拉、下拉和浮空输入配置,以适应不同的应用需求。
-
输出功能:
- 可以配置为输出模式,通过引脚输出高电平或低电平,驱动LED、继电器等负载。
- 支持推挽输出和开漏输出两种类型。
-
中断功能:
- GPIO引脚还可以配置为中断源,响应输入信号的变化(例如,按钮按下或释放),使得应用程序能够以事件驱动的方式运行。
-
复用功能:
- 每个GPIO引脚可以被配置为支持的多种功能,支持复用,允许同一引脚用于不同的外设(如UART、SPI、I2C等)。
GPIO的应用场景
GPIO的主要特点
-
不同型号的IO口数量:
不同型号的STM32微控制器,其GPIO引脚的数量可能会有所不同,用户可以根据具体需求选择合适的型号。
-
快速翻转:
GPIO引脚能够实现快速翻转,STM32F1系列最快可以达到50MHz的翻转速度。
-
外部中断支持:
- 每个GPIO引脚都可以配置为外部中断源,能够快速响应外部信号的变化。
-
支持8种工作模式:
- GPIO引脚可以配置为多种工作模式,灵活适应不同应用场景。
GPIO的8种工作模式
GPIO端口的每个位(引脚)可以通过寄存器配置为以下8种模式,但同一时间只能处于一种模式:
模式编号 | 模式名称 | 描述 | 用途 | 典型应用场景 |
---|---|---|---|---|
1 | 输入浮空(Input Floating) | 引脚不连接任何电源,处于高阻抗状态。 | 读取不确定状态的信号,适合临时连接。 | 开关检测、传感器连接 |
2 | 输入上拉(Input Pull-Up) | 引脚通过内部上拉电阻连接到高电平。 | 防止引脚浮空,确保输入为高状态。 | 按钮输入、开关状态检测 |
3 | 输入下拉(Input Pull-Down) | 引脚通过内部下拉电阻连接到低电平。 | 防止引脚浮空,确保输入为低状态。 | 按钮输入、传感器信号读取 |
4 | 模拟输入(Analog) | 引脚配置为模拟输入,通常用于ADC。 | 读取模拟信号,进行数据采集。 | 温度传感器、光传感器、模拟信号处理 |
5 | 通用开漏输出(Output Open-Drain) | 引脚配置为开漏输出,驱动能力取决于外部电源。 | 适用于多主机环境,允许多个设备共享同一线路。 | I2C总线、多个LED灯控制 |
6 | 通用推挽式输出(Output Push-Pull) | 引脚配置为推挽输出,能够提供较强的驱动能力。 | 驱动负载,提供高电平和低电平输出。 | LED控制、蜂鸣器、继电器 |
7 | 推挽式复用功能(Alternate Function Push-Pull) | 引脚配置为特定的复用功能,支持推挽输出。 | 实现特定外设功能,如UART、SPI等。 | 串口通信、SPI通信、PWM输出 |
8 | 开漏复用功能(Alternate Function Open-Drain) | 引脚配置为特定的复用功能,支持开漏输出。 | 实现特定外设功能,适用于多主机环境。 | I2C通信、信号共享 |
详细说明
-
输入浮空(Input Floating):
- 描述:引脚处于高阻抗状态,既不连接高电平也不连接低电平。
- 用途:适合临时连接外部信号,避免引脚受到干扰。
- 典型应用:用于开关状态检测,连接到传感器的信号引脚。
-
输入上拉(Input Pull-Up):
- 描述:引脚通过内部上拉电阻连接到Vcc(高电平)。
- 用途:确保引脚在未连接时保持高电平,避免浮空状态。
- 典型应用:用于读取按钮状态,确保按钮未按下时引脚为高状态。
-
输入下拉(Input Pull-Down):
- 描述:引脚通过内部下拉电阻连接到地(低电平)。
- 用途:确保引脚在未连接时保持低电平,避免浮空状态。
- 典型应用:用于读取按钮状态,确保按钮未按下时引脚为低状态。
-
模拟输入(Analog):
- 描述:引脚配置为模拟输入,通常与ADC配合使用。
- 用途:用于读取模拟信号并转换为数字信号。
- 典型应用:温度传感器、光传感器等模拟信号的读取。
-
通用开漏输出(Output Open-Drain):
- 描述:引脚可以拉低电平,但不能拉高。需要外部上拉电阻连接到高电平。
- 用途:适用于多个设备共享同一线路,允许多个设备同时驱动。
- 典型应用:I2C总线、LED灯控制。
-
通用推挽式输出(Output Push-Pull):
- 描述:引脚能够提供高电平和低电平输出,驱动能力较强。
- 用途:适合直接驱动负载。
- 典型应用:LED、蜂鸣器、继电器等。
-
推挽式复用功能(Alternate Function Push-Pull):
- 描述:引脚配置为特定的复用功能,支持推挽输出。
- 用途:实现特定外设功能,如UART、SPI等。
- 典型应用:串口通信、SPI通信、PWM输出。
-
开漏复用功能(Alternate Function Open-Drain):
- 描述:引脚配置为特定的复用功能,支持开漏输出,适用于多主机环境。
- 用途:实现特定外设功能,允许多个设备共享信号。
- 典型应用:I2C通信、信号共享。
1. 复位期间的GPIO配置
2. JTAG引脚的状态
复位后,JTAG引脚被配置为特定的输入模式:
这一配置确保在复位后,JTAG接口的引脚能够正常工作,不会因为浮空状态而引起信号干扰。
输出配置
当GPIO引脚被配置为输出时:
输入配置
当I/O端口被配置为输入时,STM32微控制器的GPIO引脚具有以下特点:
输出缓冲器被禁止:在输入模式下,输出缓冲器不工作,以确保引脚只作为输入。
施密特触发输入激活:施密特触发器能够有效消除输入信号的噪声,提高输入信号的稳定性和可靠性。
弱上拉和下拉电阻:根据配置(上拉、下拉或浮空),弱上拉和下拉电阻会被连接到引脚,确保输入信号的稳定性。
数据采样:I/O脚上出现的数据在每个APB2时钟周期被采样到输入数据寄存器(GPIOx_IDR)。
读取状态:通过对输入数据寄存器的读访问,可以获得I/O引脚的当前状态。
当I/O端口被配置为输出时,相关特性如下:
输出缓冲器激活:输出缓冲器被启用,以支持输出信号。
开漏模式:
0
激活N-MOS,将引脚拉低。1
将引脚置于高阻状态要接上拉电阻才能置1,P-MOS不会被激活。推挽模式:
0
激活N-MOS,将引脚拉低。1
激活P-MOS,将引脚拉高。施密特触发输入激活:在输出模式下也激活施密特触发输入,以提高信号的稳定性。
禁止弱上拉和下拉电阻:在输出模式下,弱上拉和下拉电阻不会连接,以避免影响输出状态。
数据采样:在输出模式下,数据在每个APB2时钟周期被采样到输入数据寄存器(GPIOx_IDR)。
读取状态:
复用功能配置
当I/O端口被配置为复用功能时,STM32微控制器的GPIO引脚具有以下特点:
输出缓冲器打开:在开漏或推挽配置中,输出缓冲器被激活,允许信号从内置外设驱动到引脚。
内置外设信号驱动:复用功能输出信号可以通过GPIO引脚传递,这使得某些外设(如UART、SPI等)能够在特定引脚上工作。
施密特触发输入激活:施密特触发器在复用模式下仍然活跃,确保输入信号的稳定性。
禁止弱上拉和下拉电阻:在复用模式下,弱上拉和下拉电阻被断开,避免影响外设的输出信号。
数据采样:在每个APB2时钟周期,出现在I/O脚上的数据会被采样到输入数据寄存器(GPIOx_IDR)。
开漏模式读取:在开漏模式下,读取输入数据寄存器可以获取I/O口的状态。
推挽模式读取:在推挽模式下,读取输出数据寄存器将返回最后一次写入的值。
复用功能寄存器
STM32提供了一组复用功能I/O寄存器,允许用户将某些复用功能重新映射到不同的引脚。这种灵活性使得用户能够根据实际需要配置引脚,以实现不同的功能。
模拟输入配置
当I/O端口被配置为模拟输入时,具有以下特点:
输出缓冲器被禁止:在模拟输入模式下,输出缓冲器不工作,以确保引脚仅作为输入。
施密特触发输入禁用:施密特触发器在此模式下被禁用,确保引脚上的零消耗。
禁止弱上拉和下拉电阻:在模拟输入模式下,弱上拉和下拉电阻也被断开,以避免对模拟信号产生干扰。
读取输入数据寄存器时值为0
:在此模式下,读取输入数据寄存器时会返回0
,因为引脚用于模拟输入而不是数字输入。
GPIO寄存器描述
必须以字(32位)的方式操作这些外设寄存器。
GPIO操作例程
点亮PA0和PC13引脚的LED灯
//#include <stdint.h> // 包含标准整数类型定义
#include "stm32f10x.h"//包含标准整数类型定义以及各个寄存器的宏定义
int main(void)
{
// 1. 配置时钟
// 这里设置 RCC (时钟控制) 的 APB2 使能寄存器。
// 0x40021000 是 RCC 基地址,0x18 是 APB2ENR 的偏移地址。
// 将值 0x14 写入 APB2 外设时钟使能寄存器,表示同时使能 GPIOA 和 GPIOC 时钟。
//*(uint32_t*)(0x40021000 + 0x18) = 0x14;//0001 0100 0x4是代表使能GPIOA时钟 0x10代表使能GPIOC时钟 按位与在一起就是0x14同时使能 GPIOA 和 GPIOC 时钟。
//RCC->APB2ENR = 0X14;//使用寄存器的宏定义实现更方便
//RCC->APB2ENR |= (5<<2);//等价 RCC->APB2ENR |= (1<<4); RCC->APB2ENR |= (1<<2);
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;//#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000004)
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN;//#define RCC_APB2ENR_IOPCEN ((uint32_t)0x00000010)
// 2. GPIO 工作模式配置
// 配置 GPIOA 的模式低寄存器(CRL),设置为通用推挽输出模式最大速度50MHz 。
// 0x40010800 是 GPIOA 基地址,0x00 是 CRL 的偏移。
// 将值 03 写入 CRL,表示将 PA0 配置为通用推挽输出模式最大速度50MHz。
//*(uint32_t*)(0x40010800 + 0x00) = 0x03; // PA0: 输出模式
//GPIOA->CRL = 0x03;
//GPIOA->CRL &= ~(3<<2);//等价 GPIOA->CRL &= ~(1<<2); GPIOA->CRL &= ~(1<<3);
GPIOA->CRL &= ~GPIO_CRL_CNF0;//#define GPIO_CRL_CNF0 ((uint32_t)0x0000000C)
//GPIOA->CRL |= (3<<0);//等价 GPIOA->CRL |= (1<<0); GPIOA->CRL |= (1<<1);
GPIOA->CRL |= GPIO_CRL_MODE0;//#define GPIO_CRL_MODE0 ((uint32_t)0x00000003)
// 配置 GPIOC 的模式寄存器(CRH),设置为通用推挽输出模式最大速度50MHz 。
// 0x40011000 是 GPIOC 基地址,0x04 是 CRH 的偏移。
// 将值 0x300000 写入 CRH,表示将 PC13 配置为通用推挽输出模式最大速度50MHz。
//*(uint32_t*)(0x40011000 + 0x04) = 0x0300000; // PC13: 输出模式
//GPIOC->CRH = 0x0300000;
//GPIOC->CRH &= ~(3<<22);//等价 GPIOC->CRH &= ~(1<<22); GPIOC->CRH &= ~(1<<23);
GPIOC->CRH &= ~GPIO_CRH_CNF13;//#define GPIO_CRH_CNF13 ((uint32_t)0x00C00000)
//GPIOC->CRH |= (3<<20);//等价 GPIOC->CRH |= (1<<20); GPIOC->CRH |= (1<<21);
GPIOC->CRH |= GPIO_CRH_MODE13;//#define GPIO_CRH_MODE13 ((uint32_t)0x00300000)
// 3. PA0 输出低电平
// 这里设置 GPIOA GPIOC 的输出数据寄存器(ODR低16位有效),将 PA0 PC13 输出高电平。
// 0x40010800 + 0x0C 是 GPIOA ODR 的偏移。
// 将值 0x01 写入 ODR,表示将 PA0 设为高电平。
//*(uint32_t*)(0x40010800 + 0x0C) = 0x01; // PA0 输出高电平
//GPIOA->ODR = 0x01;
//GPIOA->ODR &= ~(1<<0);// PA0 输出低电平
GPIOA->ODR &= ~GPIO_ODR_ODR0;//#define GPIO_ODR_ODR0 ((uint16_t)0x0001)
// 0x40011000 + 0x0C 是 GPIOC ODR 的偏移。
// 将值 0x2000 写入 ODR,表示将 PC13 设为高电平。
//*(uint32_t*)(0x40011000 + 0x0C) = 0x2000; // PC13 输出高电平
//GPIOC->ODR = 0x2000;
//GPIOC->ODR &= ~(1<<13); // PC13 输出低电平
//GPIOC->ODR &= ~GPIO_ODR_ODR13;//#define GPIO_ODR_ODR13 ((uint16_t)0x2000)
//GPIOC->BSRR |= GPIO_BRR_BR13;
//GPIOC->BSRR |= GPIO_BSRR_BR13;
//GPIOC->BRR |= GPIO_BRR_BR13;
GPIOC->ODR &= ~GPIO_ODR_ODR13;
// 进入无限循环,保持程序运行
while(1)
{
//nothing to do now
}
}
#include <stdint.h> // 包含标准整数类型定义
#include "stm32f10x.h" // 包含 STM32F10x 系列的寄存器宏定义
int main(void)
{
// 1. 配置时钟
// 使能 GPIOA 和 GPIOC 的时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能 GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPCEN; // 使能 GPIOC 时钟
// 2. GPIO 工作模式配置
// 配置 GPIOA 的模式寄存器(CRL),设置 PA0 为通用推挽输出模式,最大速度 50MHz
GPIOA->CRL &= ~GPIO_CRL_CNF0; // 清除 PA0 的配置位,准备设置为推挽输出
GPIOA->CRL |= GPIO_CRL_MODE0; // 设置 PA0 为输出模式,最大速度 50MHz
// 配置 GPIOC 的模式寄存器(CRH),设置 PC13 为通用推挽输出模式,最大速度 50MHz
GPIOC->CRH &= ~GPIO_CRH_CNF13; // 清除 PC13 的配置位,准备设置为推挽输出
GPIOC->CRH |= GPIO_CRH_MODE13; // 设置 PC13 为输出模式,最大速度 50MHz
// 3. PA0 和 PC13 输出低电平
// 设置 GPIOA 的输出数据寄存器(ODR),将 PA0 输出低电平
GPIOA->ODR &= ~GPIO_ODR_ODR0; // 将 PA0 输出低电平
// 设置 GPIOC 的输出数据寄存器(ODR),将 PC13 输出低电平
GPIOC->ODR &= ~GPIO_ODR_ODR13; // 将 PC13 输出低电平
// 进入无限循环,保持程序运行
while(1)
{
// 主循环中可以添加其他逻辑
// 此处暂时无操作
}
}
STM32中断
1. 中断的基本概念
2. 中断嵌套
3. 中断源分类
NVIC嵌套向量中断控制器
中断优先级
NVIC 提供了中断优先级管理功能,以方便软件管理中断。每个中断可以设置优先级,优先级是通过 4个位进行控制的,值越小优先级越高。中断优先级分为两种:抢占优先级和响应优先级。
优先级规则:
优先级分组:
NVIC 将优先级分为 5 组,在程序中先对中断进行分组。分组只能设置一次,后续的设置将被忽略。
分组 | 抢占优先级 | 响应优先级 |
---|---|---|
0 | 0 位(取值范围:0) | 4 位(取值范围:0-15) |
1 | 1 位(取值范围:0-1) | 3 位(取值范围:0-7) |
2 | 2 位(取值范围:0-3) | 2 位(取值范围:0-3) |
3 | 3 位(取值范围:0-7) | 1 位(取值范围:0-1) |
4 | 4 位(取值范围:0-15) | 0 位(取值范围:0) |
NVIC_SetPriorityGrouping(3);//抢占优先级
NVIC_SetPriority(USART1_IRQn,3);//设置串口优先级
NVIC_EnableIRQ(USART1_IRQn);//使能串口中断
外部中断控制器
外部中断控制器(EXTI)负责管理外部中断的输入信号,并将其转发给 NVIC。
AFIO (Alternate Function I/O)
AFIO(Alternate Function I/O)是STM32微控制器中的一个重要模块,负责管理引脚的复用功能。AFIO允许用户将GPIO引脚配置为不同的功能,以支持多种外设接口和功能。
1. AFIO的主要功能
2. AFIO寄存器
AFIO模块包含多个寄存器,用于配置引脚的功能。以下是一些关键寄存器:
外部中断例程
通过PA11引脚控制LED的亮灭
#include "key.h"
void Key_Init(void)
{
// 1.配置时钟 (EXTI和NVIC时钟始终开启,无需手动开启)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能GPIOA
RCC->APB2ENR |= RCC_APB2ENR_AFIOEN; // 使能AFIO时钟,使得用户可以配置引脚的复用功能和外部中断源。
// 2.GPIO工作模式配置(设置PA11为下拉输入模式)
GPIOA->CRH &= ~GPIO_CRH_MODE11; // 设置PA11为输入模式
GPIOA->CRH &= ~GPIO_CRH_CNF11_0;
GPIOA->CRH |= GPIO_CRH_CNF11_1; // 设置PA11为上下拉输入
GPIOA->ODR &= ~GPIO_ODR_ODR11; // 设置输出寄存器低电平,在输入模式下相当于设置下拉模式
// 3.AFIO配置引脚复用
AFIO->EXTICR[2] |= AFIO_EXTICR3_EXTI11_PA; // 设置外部中断配置寄存器3,将PA11配置为EXTI11的中断源
// 4.EXTI中断线和触发模式配置
EXTI->RTSR |= EXTI_RTSR_TR11; // 配置EXTI11为上升沿触发
EXTI->IMR |= EXTI_IMR_MR11; // 使能EXTI11中断线
// 5.NVIC配置
NVIC_SetPriorityGrouping(3); // 全部设为抢占优先级
NVIC_SetPriority(EXTI15_10_IRQn, 2); // 设置EXTI11的优先级为2
NVIC_EnableIRQ(EXTI15_10_IRQn); // 使能EXTI11中断
}
// 中断处理程序(名字是固定的不能随便取名)
void EXTI15_10_IRQHandler(void)
{
// 先清除中断挂起标志位
EXTI->PR |= EXTI_PR_PR11; // 清除中断标志. 写1清除中断
// 按键防抖延时
Delay_ms(10);
//判断输入寄存器的值为高电平就翻转LED电平
if(GPIOA->IDR & GPIO_IDR_IDR11)
{
LED_toggle(LED1);
}
}
USART串口通讯
串口通讯(Serial Communication)是一种广泛使用的串行通讯方式。由于其简单和便捷,几乎所有电子设备都支持这种通讯方式。
通讯基础知识
并行通讯和串行通讯
单工、半双工、全双工通讯
同步和异步通讯
串口通讯协议
-
波特率
- 波特率(Baudrate)表示每秒传输的码元数量。在二进制中,码元与位是等价的,通常用每秒传输的比特数来表示。
- STM32 提供异步串口通讯,异步通讯中由于没有时钟信号,通讯设备之间必须约定波特率,以便正确解码信号。常见波特率包括 4800、9600、115200 等。
-
空闲位
- 串口协议规定,当总线处于空闲状态时,信号线的状态为‘1’(高电平),表示当前线路上没有数据传输。
-
通讯的起始位
- 每次通讯开始时,发送方会发送一个逻辑“0”的信号(低电平),表示数据传输的开始。因为总线空闲时为高电平,所以低电平信号明显区分于空闲状态。
-
通讯的停止位
- 停止位可由 0.5、1、1.5 或 2 个逻辑“1”数据位表示,双方需一致约定。
-
有效数据位
- 在起始位之后,紧接着是要传输的有效数据,长度通常为 5、6、7 或 8 位。数据传输时,先发送最低位,最后发送最高位,低电平表示‘0’,高电平表示‘1’。
-
校验位
- 校验位用于数据传输的正确性验证,使得“1”的位数为偶数(偶校验)或奇数(奇校验)。串口校验的几种方式包括:
- 无校验(No Parity):不使用校验位。
- 奇校验(Odd Parity):如果数据位中“1”的数量为偶数,则校验位为“1”;如果为奇数,则校验位为“0”。
- 偶校验(Even Parity):如果数据位中“1”的数量为偶数,则校验位为“0”;如果为奇数,则校验位为“1”。
USART外设
STM32提供了USART(Universal Synchronous Asynchronous Receiver and Transmitter)通用同步异步收发器。是一个串行通信设备,可以灵活地与外部设备进行全双工数据交换。
USART 介绍
通用同步异步收发器(USART)提供了一种灵活的方式,用于与使用工业标准 NRZ(非归零)异步串行数据格式的外部设备进行全双工数据交换。USART 利用分数波特率发生器,支持宽范围的波特率选择,适应不同的通讯需求。
主要特性
1. 通讯方式
2. 波特率和数据格式
3. 其他功能
4. 状态和标志
5. 中断和多处理器通信
1. 引脚配置
RX(接收数据输入):用于接收串行数据。采用过采样技术区分数据和噪声,从而恢复有效数据。
TX(发送数据输出):用于发送串行数据。当发送器被禁用时,TX 引脚恢复为其 I/O 端口配置。在不发送数据时,TX 引脚处于高电平。在单线和智能卡模式下,此引脚同时用于数据的发送和接收。
2. 数据传输格式
在进行数据传输时,总线在发送或接收前应处于空闲状态,数据帧的结构如下:
起始位:每次数据传输开始时发送一个起始位(低电平)。
数据字:包含 8 或 9 位有效数据,其中最低有效位(LSB)优先发送。
停止位:可以是 0.5、1.5 或 2 个停止位,用于标识数据帧的结束。
3. 主要寄存器
USART 主要使用以下寄存器:
USART_SR(状态寄存器):用于监测 USART 的状态。
USART_DR(数据寄存器):用于存放发送和接收的数据。
USART_BRR(波特率寄存器):用于设置波特率,采用 12 位整数和 4 位小数的形式表示。
USART_GTPR(保护时间寄存器):在智能卡模式下使用,用于设置保护时间。
4. 同步模式
在同步模式下,除了 RX 和 TX 引脚外,还需要以下引脚:
5. IrDA 模式
在 IrDA 模式下,需要以下引脚:
IrDA_RDI:IrDA 模式下的数据输入引脚。
IrDA_TDO:IrDA 模式下的数据输出引脚。
6. 硬件流控模式
在硬件流控模式下,需要以下引脚:
nCTS(清除发送):当 nCTS 为高电平时,阻断下一次数据发送,直到当前数据传输结束。
nRTS(发送请求):当 nRTS 为低电平时,表示 USART 准备好接收数据。
要计算 USART 的波特率寄存器(BRR)的值,我们可以按照以下步骤进行:
-
确定波特率:如目标波特率为 115200。
-
计算分频值:
- 假设系统时钟频率为 72 MHz(STM32F103 系列常用时钟频率)。
- 波特率计算公式为:
- 代入值:
-
分解 BRR 值:
- 整数部分:39
- 小数部分:0.0625
-
转换小数部分:
0.0625 = 1/16 = 1/2^4刚好是BRR小数表示的最高精度
-
构建 BRR 值:
- 整数部分的十六进制表示:39 = 0x27
- 小数部分的十六进制表示:1 = 0x01
- 因此,BRR 寄存器的值为:
BRR=0x0271所以,写入到 BRR 寄存器的值为 0x0271。
串口UART通信例程
#include "usart.h"
#include <stdint.h> // 确保 uint8_t 的定义
void USART_Init(void) // 串口初始化
{
// 1. 配置时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // 使能 GPIOA 的时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN; // 使能 USART1 的时钟
// 2. GPIO 工作模式
// PA9 USART TX 复用推挽输出 CNF:10 MODE:11
// PA10 USART RX 复用浮空输入 CNF:01 MODE:00
GPIOA->CRH |= GPIO_CRH_MODE9; // 设置 PA9 为输出模式
GPIOA->CRH |= GPIO_CRH_CNF9_1; // 设置 PA9 为复用功能
GPIOA->CRH &= ~GPIO_CRH_CNF9_0; // 清除 CNF9_0,确保 PA9 为推挽输出
GPIOA->CRH &= ~GPIO_CRH_MODE10; // 清除 MODE10,设置为输入模式
GPIOA->CRH &= ~GPIO_CRH_CNF10_1; // 清除 CNF10_1,确保 PA10 为浮空输入
GPIOA->CRH |= GPIO_CRH_CNF10_0; // 设置 CNF10_0,使 PA10 为复用输入
// 3. 串口配置
USART1->BRR = 0x0271; // 设置波特率为 115200
USART1->CR1 = (USART_CR1_UE | USART_CR1_TE | USART_CR1_RE); // 使能 USART
// USART_CR1_UE: 使能 USART
// USART_CR1_TE: 使能发送功能
// USART_CR1_RE: 使能接收功能
USART1->CR1 &= ~USART_CR1_M; // 清除 M 位,设置为 8 位数据长度
USART1->CR1 &= ~USART_CR1_PCE; // 清除 PCE 位,禁用奇偶校验
USART1->CR2 &= ~USART_CR2_STOP; // 清除 STOP 位,设置为 1 个停止位
// 开启中断使能
USART1->CR1 |= (USART_CR1_IDLEIE | USART_CR1_RXNEIE); // 开启空闲中断和接收中断
// NIVC配置
NVIC_SetPriorityGrouping(3); // 抢占优先级
NVIC_SetPriority(USART1_IRQn, 3); // 设置串口优先级
NVIC_EnableIRQ(USART1_IRQn); // 使能串口中断
}
void USART_SendChar(uint8_t ch) // 发送一个字符
{
// 等待发送缓冲区空
while ((USART1->SR & USART_SR_TXE) == 0)
;
// 向 DR 写入要发送的数据
USART1->DR = ch;
}
void USART_SendString(uint8_t *str, uint8_t len) // 发送指定长度的字符串
{
for (uint8_t i = 0; i < len; i++)
{
USART_SendChar(str[i]); // 发送当前字符
}
}
// 接收中断处理程序
void USART1_IRQHandler(void)
{
// 首先判断中断类型
if (USART1->SR & USART_SR_RXNE)
{
// 完成一个字符的接收
buf[len++] = USART1->DR; // 保存字符到buf
}
else if (USART1->SR & USART_SR_IDLE)
{
// 完成字符串的接收
// 清除IDLE标志位
USART1->DR; // 已经读取了USART1->SR只需读取DR即可清除标志位
// 发送字符串到上位机
// USART_SendString(buf, len);//这样加重了中断的处理时间
// 清空len
//len = 0;
send = 1;//添加标志位
}
}
重定向printf
重定向printf,把数据打印到串口,从而在电脑端接收调试信息。
当调用printf的时候,底层会自动调用fputc方法,要实现重定向到串口只需要在这调用一个通过串口发送字符的函数就可以了
int fputc(int c, FILE *file)
{
GPIOA_USART_SendChar(c);// 重定向 printf 的输出到串口
return c;
}
使用重定向printf必须使用MicroLIB或者关闭半主机模式否则程序卡死
半主机模式
半主机模式是ARM开发中的一种常见机制,它允许ARM目标设备与运行仿真器的PC主机之间进行输入输出交互。主要作用包括:
-
输入输出重定向:
在开发阶段,可以将ARM目标程序的printf、scanf等标准C库函数的输入输出重定向到PC主机的屏幕和键盘上,方便调试。
-
资源共享:
通过半主机模式,ARM目标程序可以使用PC主机上的更丰富的资源,如文件系统、网络通信等。
-
降低开发成本:
使用半主机模式可以减少ARM目标上的外围设备,如显示屏、键盘等,降低开发成本。
使用C标准库(stdio.h)中的函数,例如printf()之类的函数,在半主机模式,会发生软件异常,导致程序无法运行。
所以要使用目标 ARM器件的输入输出设备,在不使用Microlib时,首先要关掉半主机机制。然后再将输入输出重定向到 ARM 器件上。
卡死解决办法:
解决办法有两种,要么使用Microlib,要么关闭标准库下的半主机模式。
来源:STM32中MicroLIB的关闭为什么会导致卡死—-解析-CSDN博客
Semihosting 半启动
半启动的功能是为了方便设备进行调试的时候使用的。甚至,在 ARM 编译器中半启动就是 C 标准库默认的实现形式。
因为在单片机上跑的很多都是裸机程序,这个裸机程序根本就不能预判你希望将你的输出重定向到哪里?到底是 UART?I2C?还是调试器等位置。
这个半启动会对单片机的运行打上一个断点。当你连接调试器的时候,调试器可以捕获半启动的发生,并在对单片机的内存空间进行读取,并完成解析。然后调试器再让单片机从中断返回以继续运行程序。
为什么会卡住
如果不使用 MicroLIB 的话,默认情况下使用 printf 就会使得单片机卡住。 这是因为 printf 是的参数是基于 FILE ,也就是文件流的。
里面一定会有 sys_open, sys_exit 等函数。 下面的图里就会看到,_sys_open 的在汇编码中就有 BKPT ,就会使得单片机停止运行。
关闭半主机模式
在任何c文件中加入以下代码就可以退出半主机模式(STM32默认是开启半主机模式)
/* 告知连接器不从C库链接使用半主机的函数 */
// 标准库需要的支持函数
struct __FILE
{
int handle;
};
// 定义_sys_exit()以避免使用半主机模式
void _sys_exit(int x)
{
x = x;
}
FILE __stdout;
MicroLIB(微库)
-
什么是MicroLIB
ARM-MDK官方的解释:MicroLib 是一个高度优化的库,适用于用 C 编写的基于 ARM 的嵌入式应用程序。与 ARM 编译器工具链中包含的标准 C 库相比,MicroLib 提供了许多嵌入式系统所需的显着代码大小优势。 -
MicroLib 和标准 C 库之间的主要区别是:
- 使用MicroLib的优缺点
-
MDK中使用MicroLib模式
在点开MDK软件的魔术棒,勾选Target选卡中的"Use MicroLIB"。这样就可以使用printf()函数通过USART输出数据到电脑串口助手。 -
Microlib模式下使用printf()函数
在Microlib模式下使用printf()函数,重定义fputc函数的代码如下/*使用microLib的方法*/ int fputc(int c, FILE *file) { GPIOA_USART_SendChar(c);// 重定向 printf 的输出到串口 return c; }
总结
半主机模式依赖于仿真器:
半主机模式通过仿真器与PC主机进行通信,允许嵌入式程序使用主机的输入输出功能(如屏幕和键盘)。在没有仿真器的情况下,这种通信无法建立。
BAEB 中断问题:
如果在没有仿真器的情况下仍然试图使用半主机模式,程序可能会进入软件中断地址 BAEB,导致系统无法继续执行。这是一种异常状态,通常会使程序卡死。为了防止这种情况发生,必须在代码中禁用半主机模式。
I2C 通讯
I2C(Inter-Integrated Circuit)是由飞利浦公司发明的一种串行通信协议,广泛用于微控制器与各种外设之间的通信,如传感器、存储器和LCD显示器等。I2C具有简单、低成本和灵活的特点,适合于短距离的通信。
I2C 的基本概念
总线结构:I2C使用两条线进行通信:
主从设备:
地址:每个从设备在总线上都有一个唯一的地址,主设备通过地址选择要与哪个从设备进行通信。
I2C 通信流程
-
开始条件:主设备拉低SDA线,同时保持SCL线为高电平,表示通信开始。
-
发送地址:主设备发送从设备的地址,并指明是读操作还是写操作。地址通常是7位或10位。
-
应答信号:
- 从设备在接收到地址后,如果自身地址匹配,则拉低SDA线以应答。
- 主设备在发送完地址后,检查应答信号。
-
数据传输:
- 主设备或从设备根据操作方向(读或写)发送数据字节。
- 每发送一个字节后,接收方必须发送应答信号。
-
停止条件:通信结束时,主设备将SDA线拉高,同时保持SCL线为高电平,表示通信结束。
I2C通信例程
#include "IIC.h"
// 初始化
void I2C_Init(void)
{
// 1. 配置时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB1ENR |= RCC_APB1ENR_I2C2EN;
// 2. GPIO工作模式配置:复用开漏输出 CNF-11,MODE-11
GPIOB->CRH |= (GPIO_CRH_MODE10 | GPIO_CRH_MODE11 | GPIO_CRH_CNF10 | GPIO_CRH_CNF11);
// 3.IIC2配置
I2C2->CR1 &= ~I2C_CR1_SMBUS; // 关闭SMBUS
I2C2->CCR &= ~I2C_CCR_FS; // 标准模式
I2C2->CR2 |= 36; // 配置时钟频率36MHz
I2C2->CCR |= 180; // 配置CCR对应传输速率100Kb/s,SCL高电平时间为5us。1个时钟周期1/36MHz, 5*36 = 180
I2C2->TRISE |= 37; // SCL上升沿最大时钟周期数+1
I2C2->CR1 |= I2C_CR1_PE; // 使能I2C2模块
}
// 发出起始信号
uint8_t I2C_Start(void)
{
// 产生一个起始信号
I2C2->CR1 |= I2C_CR1_START;
// 引入一个超时时间
uint16_t timeout = 0xffff;
// 等待起始信号发出
while ((I2C2->SR1 & I2C_SR1_SB) == 0 && timeout--)
;
return timeout ? OK : FAIL;
}
// 设置接收完成后发出停止信号
void I2C_Stop(void)
{
I2C2->CR1 |= I2C_CR1_STOP;
}
// 主机设置使能应答信号
void I2C_Ack(void)
{
I2C2->CR1 |= I2C_CR1_ACK;
}
// 主机设置使能非应答信号
void I2C_Nack(void)
{
I2C2->CR1 &= ~I2C_CR1_ACK;
}
// 主机发送设备地址(写入),并等待应答
uint8_t I2C_SendAddr(uint8_t addr)
{
// 直接将要发送的地址给到DR
I2C2->DR = addr;
// 引入一个超时时间
uint16_t timeout = 0xffff;
// 等待应答
while ((I2C2->SR1 & I2C_SR1_ADDR) == 0 && timeout--)
;
// 访问SR2,清除ADDR标志位
I2C2->SR2;
return timeout ? OK : FAIL;
}
// 主机发送一个字节的数据(写入),并等待应答
uint8_t I2C_SendByte(uint8_t byte)
{
// 1.先等DR为空,上一个字节数据已经发送完毕
uint16_t timeout = 0xffff;
// 等待应答
while ((I2C2->SR1 & I2C_SR1_TXE) == 0 && timeout--)
;
// 2.将要发实施的字节放入DR中
I2C2->DR = byte;
timeout = 0xffff;
// 等待应答
while ((I2C2->SR1 & I2C_SR1_BTF) == 0 && timeout--)
;
return timeout ? OK : FAIL;
}
// 主机从EEPROM接收一个字节的数据(读取)
uint8_t I2C_ReadByte(void)
{
// 1.先等DR为满
uint16_t timeout = 0xffff;
// 等待应答
while ((I2C2->SR1 & I2C_SR1_RXNE) == 0 && timeout--)
;
// 2.将收到的字节数据返回
return timeout ? I2C2->DR : FAIL;
}
定时器
系统定时器
STM32的系统定时器(SysTick)是一个内置的定时器,常用于操作系统的时间管理、延时功能和任务调度。
SysTick 定时器概述
1 / SYSCLK
,使得它能够以微秒级的精度工作。SysTick 的功能与用途
-
操作系统支持:
SysTick常用于实时操作系统(RTOS)中,作为系统时基,提供心跳信号,确保系统按预定时间调度任务。
-
时间测量:
可以用于测量事件的持续时间,如延时、计时器等。
-
闹铃功能:
SysTick可以配置为在特定时间后触发中断,类似于闹铃功能。
注意事项
- 系统时钟:确保在初始化 SysTick 之前,系统时钟已经正确配置。
- 中断优先级:根据需要设置中断优先级,确保 SysTick 中断不会影响其他重要中断。
- 计数范围:SysTick 的计数范围是24位,因此在长时间延时的情况下,可能需要额外的处理来避免溢出。
SysTick 控制和状态寄存器 (STK_CTRL)
地址偏移: 0x00
重置值: 0x0000 0000
所需权限: 特权
SysTick CTRL 寄存器启用 SysTick 功能。
位 31:17 保留,必须保持清除状态。
Bit 16 COUNTFLAG:如果自上次读取此消息以来 timer 计数为 0,则返回 1。保留位 15:3,必须保持清除状态。
Bit 2 CLKSOURCE: Clock source selection 选择时钟源。
0:AHB/8 1:处理器时钟 (AHB)
位 1 TICKINT:启用 SysTick 异常请求
0:倒计时到零不会断言 SysTick 异常请求
1:倒计时到零以断言 SysTick 异常请求。注意:软件可以使用 COUNTFLAG 来确定 SysTick 是否曾经计数为零。
Bit 0 ENABLE:计数器启用计数器。当 ENABLE 设置为 1 时,计数器从 LOAD 寄存器加载 RELOAD 值,然后进行倒计时。达到 0 时,它将 COUNTFLAG 设置为 1,并可选择根据 TICKINT 的值断言 SysTick。然后,它再次加载 RELOAD 值,并开始计数。
0:计数器已禁用
1:计数器已启用
SysTick 重新加载值寄存器 (STK_LOAD)
地址偏移量:0x04
重置值:0x0000 0000
所需权限:特权
保留位 31:24,必须保持清除状态。
Bits 23:0 RELOAD[23:0]: RELOAD 值 LOAD 寄存器指定当计数器启用且达到 0 时要加载到 VAL 寄存器的起始值。
计算 RELOAD 值 RELOAD 值可以是 0x00000001-0x00FFFFFF 范围内的任何值。起始值为 0 是可能的,但不起作用,因为在从 1 计数到 0 时,会激活 SysTick 异常请求和 COUNTFLAG。RELOAD 值根据其用途计算: l 要生成周期为 N 个处理器时钟周期的 multi-shot 计时器,请使用 RELOAD 值 N-1。例如,如果每 100 个时钟脉冲需要 SysTick 中断,则将 RELOAD 设置为 99。l 要在 N 个处理器时钟周期的延迟后提供单个 SysTick 中断,请使用值 N 的 RELOAD。例如,如果在 400 个时钟脉冲后需要 SysTick 中断,则将 RELOAD 设置为 400。
SysTick 当前值寄存器 (STK_VAL)
地址偏移量:0x08
重置值:0x0000 0000
所需权限:特权
保留位 31:24,必须保持清除状态。
Bits 23:0 CURRENT[23:0]: 当前计数器值VAL 寄存器包含 SysTick 计数器的当前值。读取返回 SysTick 计数器的当前值。写入任何值都会将字段清零为 0,并将 STK_CTRL 寄存器中的 COUNTFLAG 位清零为 0。
SysTick相关的寄存器
SysTick控制和状态寄存器CTRL
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
16 | COUNTFLAG | R | 0 | 如果在上次读取本寄存器后,SysTick 已经计数到0,则该位为1。如果读取该位,该为自动清零。 |
2 | CLKSOURCE | R/W | 0 | 0:外部时钟源(STCLK),1:内核时钟(FCLK) |
1 | TICKINT | R/W | 0 | 1:SysTick 倒数计数到 0 时产生 SysTick 异常请求 ;0:无动作 |
0 | ENABLE | R/W | 0 | SysTick 定时器的使能位。 |
说明: CLKSOURCE位,为0时,时钟频率是AHB/8, 为1时,时钟频率是AHB
SysTick重装载寄存器LOAD和SysTick当前数值寄存器VAL
位段 | 名称 | 类型 | 复位值 | 描述 |
---|---|---|---|---|
23:0 | RELOAD | R/W | 0 | 当倒数计数到 0 时要加载的值。将决定 SysTick 定时器的周期。 |
23:0 | CURRENT | R/Wc | 0 | 读取当前 SysTick 计数器的值。读取该寄存器会清零计数器的值。 |
基本定时器
STM32F103系列提供了8个定时器:2个基本定时器(TIM6和TIM7),4个通用定时器(TIM2-5),2个高级定时器(TIM1和TIM8)。
基本定时器TIM6和TIM7各包含一个16位自动装载计数器,由各自的可编程预分频器驱动。 它们可以作为通用定时器提供时间基准,特别地可以为数模转换器(DAC)提供时钟。实际上,它 们在芯片内部直接连接到DAC并通过触发输出直接驱动DAC。 这2个定时器是互相独立的,不共享任何资源。
#include "TIM3.h"
#include "led.h"
#include "usart.h"
void TIM3_Init(void)
{
//1.开启时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN;
//2.设置预分频值7200-1,做7200分频,得到10000Hz
TIM3->PSC = 7199;
//3.设置自动重装载值9999,表示计数10000次产生一个更新事件UEV
TIM3->ARR = 9999;
//4.更新中断使能
TIM3->DIER |= TIM_DIER_UIE;
//5.NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(TIM3_IRQn,2);
NVIC_EnableIRQ(TIM3_IRQn);
//6.开启定时器
TIM3->CR1 |= TIM_CR1_CEN;
}
//中断服务程序
void TIM3_IRQHandler(void)
{
//清除中断标志位
TIM3->SR &= ~TIM_SR_UIF;
//翻转led
LED_Toggle(LED1);
printf("%d",GPIOA->ODR & GPIO_ODR_ODR0);
}
通用定时器
在 STM32 微控制器中,通用定时器(如 TIM2、TIM3、TIM4 等)可以用于多种功能,包括 PWM 生产、定时器中断、输入捕获、输出比较等。
通用定时器是一个通过可编程预分频器驱动的16位自动装载计数器构成。 它适用于多种场合,包括测量输入信号的脉冲长度(输入捕获)或者产生输出波形(输出比较和 PWM)。 使用定时器预分频器和RCC时钟控制器预分频器,脉冲长度和波形周期可以在几个微秒到几个毫秒间调整。 每个定时器都是完全独立的,没有互相共享任何资源。
捕获/比较模块由一个预装载寄存器和一个影子寄存器组成。读写过程仅操作预装载寄存器。
#include "TIM3.h"
void TIM3_Init(void)
{
// 1.开启时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM3EN; // TIM3定时器使能
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; // GPIOA口使能
// 2.GPIO工作模式:PA6(TIM3-CH1) 浮空输入 CNF-01,MODE-00
GPIOA->CRL &= ~GPIO_CRL_MODE6;
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
// 3.定时器配置
// 时基部分
TIM3->PSC = 71; // 预分频值71,得到72MHz/(71+1)=1000000Hz=1MHz 1/1000000Hz = 0.000001s = 0.001ms = 1us
TIM3->ARR = 65535; // 重装载值65535,每隔1us*(65535+1)=65.536ms溢出一次 1/65.536ms = 1/0.065536s = 15.2587890625Hz
TIM3->CR1 &= ~TIM_CR1_DIR; // 计数方向0向上计数,1向下计数 默认0 可以不设置
// 4.输入通道部分
// TI1的输入选择
TIM3->CR2 &= ~TIM_CR2_TI1S; // 默认路径
// 输入滤波器
TIM3->CCMR1 &= ~TIM_CCMR1_IC1F; // 不配置滤波
// 配置极性
TIM3->CCER &= ~TIM_CCER_CC1P; // 上升沿触发
// 通道2下降沿触发
TIM3->CCER |= TIM_CCER_CC2P;
// 选择通道1的输入映射为TI1:CC1S - 01
TIM3->CCMR1 &= ~TIM_CCMR1_CC1S_1;
TIM3->CCMR1 |= TIM_CCMR1_CC1S_0;
// 选择通道2的输入映射为TI1:CC2S - 10
TIM3->CCMR1 |= TIM_CCMR1_CC2S_1;
TIM3->CCMR1 &= ~TIM_CCMR1_CC2S_0;
// 预分频器
TIM3->CCMR1 &= ~TIM_CCMR1_IC1PSC; // 不分频
TIM3->CCMR1 &= ~TIM_CCMR1_IC2PSC; // 不分频
// 通道1输入捕获使能
TIM3->CCER |= TIM_CCER_CC1E;
// 配置触发输入信号TRGI TS - 101
TIM3->SMCR |= TIM_SMCR_TS_2;
TIM3->SMCR &= ~TIM_SMCR_TS_1;
TIM3->SMCR |= TIM_SMCR_TS_0;
// 配置从模式为复位模式SMS - 100
TIM3->SMCR |= TIM_SMCR_SMS_2;
TIM3->SMCR &= ~TIM_SMCR_SMS_1;
TIM3->SMCR &= ~TIM_SMCR_SMS_0;
// 通道2输入捕获使能
TIM3->CCER |= TIM_CCER_CC2E;
}
void TIM3_Start(void)
{
TIM3->CR1 |= TIM_CR1_CEN;
}
void TIM3_Stop(void)
{
TIM3->CR1 &= ~TIM_CR1_CEN;
}
//周期单位为us,返回ms
double TIM3_GetPWMCycle(void)
{
return TIM3->CCR1/1000.0;
}
//周期单位为us,返回s也就是Hz
double TIM3_GetPWMFreq(void)
{
return 1000000.0/TIM3->CCR1;
}
double TIM3_GetPWMDutyCycle(void)
{
return (TIM3->CCR2)*100.0/TIM3->CCR1;
}
#include "TIM2.h"
void TIM2_Init(void)
{
//1.开启时钟
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
//2.GPIO工作模式:PA3(TIM2-CH4) 复用推挽输出CNF-10,MODE-11
GPIOA->CRL |= GPIO_CRL_MODE3;
GPIOA->CRL |= GPIO_CRL_CNF3_1;
GPIOA->CRL &= ~GPIO_CRL_CNF3_0;
//3.定时器配置
TIM2->PSC = 7199;//预分频值7199,得到72MHz/(7199+1)=10000Hz 1/10000Hz = 0.0001s = 0.1ms
TIM2->ARR = 99;//重装载值99,每隔0.1ms*(99+1)=10ms溢出一次 1/10ms = 1/0.01s = 100Hz
TIM2->CR1 &= ~TIM_CR1_DIR;//计数方向0向上计数,1向下计数 默认0 可以不设置
TIM2->CCR4 = 50;//设置通道4的初始CCR(占空比)值
TIM2->CCMR2 &= ~TIM_CCMR2_CC4S;//配置通道4为输出模式(一个CCMRx配置2个通道)
//配置通道4为PWM1模式,OC4M-110
TIM2->CCMR2 |= TIM_CCMR2_OC4M;//111
TIM2->CCMR2 &= ~TIM_CCMR2_OC4M_0;//111 & ~001 = 111 & 110 = 110
//使能输出通道
TIM2->CCER |= TIM_CCER_CC4E;
}
void TIM2_Start(void)
{
TIM2->CR1 |= TIM_CR1_CEN;
}
void TIM2_Stop(void)
{
TIM2->CR1 &= ~TIM_CR1_CEN;
}
void TIM2_SetDutyCycle(uint8_t dutycycle)//设置占空比
{
TIM2->CCR4 = dutycycle;
}
高级定时器
在 STM32 微控制器中,高级定时器(如 TIM1 和 TIM8)提供了一些额外的功能,适用于复杂的 PWM 生成、输入捕获、输出比较以及其他时间相关的任务。
高级定时器基本功能
- PWM 生成: 支持多路输出,可以生成高精度的 PWM 信号。
- 输入捕获: 可以捕获外部信号的频率和周期。
- 输出比较: 可以在特定时间触发事件。
- 死区时间生成: 适用于半桥或全桥驱动电路,确保安全切换。
高级控制定时器(TIM1和TIM8)由一个16位的自动装载计数器组成,它由一个可编程的预分频器 驱动。 它适合多种用途,包含测量输入信号的脉冲宽度(输入捕获),或者产生输出波形(输出比较、 PWM、嵌入死区时间的互补PWM等)。 使用定时器预分频器和RCC时钟控制预分频器,可以实现脉冲宽度和波形周期从几个微秒到几 个毫秒的调节。 高级控制定时器(TIM1和TIM8)和通用定时器(TIMx)是完全独立的,它们不共享任何资源。
时基单元
可编程高级控制定时器的主要部分是一个16位计数器和与其相关的自动装载寄存器。这个计数 器可以向上计数、向下计数或者向上向下双向计数。此计数器时钟由预分频器分频得到。 计数器、自动装载寄存器和预分频器寄存器可以由软件读写,即使计数器还在运行读写仍然有效。
时基单元包含:
● 计数器寄存器(TIMx_CNT)
● 预分频器寄存器 (TIMx_PSC)
● 自动装载寄存器 (TIMx_ARR)
● 重复次数寄存器 (TIMx_RCR)
自动装载寄存器是预先装载的,写或读自动重装载寄存器将访问预装载寄存器。根据在 TIMx_CR1寄存器中的自动装载预装载使能位(ARPE)的设置,预装载寄存器的内容被立即或在 每次的更新事件UEV时传送到影子寄存器。当计数器达到溢出条件(向下计数时的下溢条件)并当 TIMx_CR1寄存器中的UDIS位等于0时,产生更新事件。更新事件也可以由软件产生。随后会详 细描述每一种配置下更新事件的产生。
计数器由预分频器的时钟输出CK_CNT驱动,仅当设置了计数器TIMx_CR1寄存器中的计数器使 能位(CEN)时,CK_CNT才有效。(更多有关使能计数器的细节,请参见控制器的从模式描述)。 注意,在设置了TIMx_CR寄存器的CEN位的一个时钟周期后,计数器开始计数。
预分频器可以将计数器的时钟频率按1到65536之间的任意值分频。它是基于一个(在TIMx_PSC 寄存器中的)16位寄存器控制的16位计数器。因为这个控制寄存器带有缓冲器,它能够在运行时被改变。新的预分频器的参数在下一次更新事件到来时被采用。
#include "TIM1.h"
void TIM1_Init(void)
{
//1.开启时钟
RCC->APB2ENR |= RCC_APB2ENR_TIM1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
//2.GPIO工作模式:PA8(TIM1-CH1) 复用推挽输出CNF-10,MODE-11
GPIOA->CRH |= GPIO_CRH_MODE8;
GPIOA->CRH |= GPIO_CRH_CNF8_1;
GPIOA->CRH &= ~GPIO_CRH_CNF8_0;
//3.定时器配置
TIM1->PSC = 7199;//预分频值7199,得到72MHz/(7199+1)=10000Hz 1/10000Hz = 0.0001s = 0.1ms
TIM1->ARR = 4999;//重装载值4999,每隔0.1ms*(4999+1)=500ms溢出一次 1/500ms = 1/0.5s = 2Hz
TIM1->CR1 &= ~TIM_CR1_DIR;//计数方向0向上计数,1向下计数 默认0 可以不设置
//重复计数次数
TIM1->RCR = 4;//计数5次
//4.输出通道部分
TIM1->CCMR1 &= ~TIM_CCMR1_CC1S;//配置通道1为输出模式
//配置通道1为PWM1模式,OC1M - 110
TIM1->CCMR1 |= TIM_CCMR1_OC1M_2;
TIM1->CCMR1 |= TIM_CCMR1_OC1M_1;
TIM1->CCMR1 &= ~TIM_CCMR1_OC1M_0;
//配置CCR,占空比50%
TIM1->CCR1 = 2500;
//配置极性
TIM1->CCER &= ~TIM_CCER_CC1P;
TIM1->CR1 |= TIM_CR1_URS;
//产生一个更新事件,把配置数据写入相应影子寄存器
TIM1->EGR |= TIM_EGR_UG;
//TIM1->SR &= ~TIM_SR_UIF;//清除中断标志位
//通道1使能
TIM1->CCER |= TIM_CCER_CC1E;
//主输出使能
TIM1->BDTR |= TIM_BDTR_MOE;
//5.中断功能
TIM1->DIER |= TIM_DIER_UIE;
//NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(TIM1_UP_IRQn,3);
NVIC_EnableIRQ(TIM1_UP_IRQn);
}
void TIM1_Start(void)
{
TIM1->CR1 |= TIM_CR1_CEN;
}
void TIM1_Stop(void)
{
TIM1->CR1 &= ~TIM_CR1_CEN;
}
//中断复位程序
void TIM1_UP_IRQHandler(void)
{
printf("into interrupt...\n");
//清除中断标志位
TIM1->SR &= ~TIM_SR_UIF;
//关闭定时器
TIM1_Stop();
}
DMA
DMA(直接存储器访问)是一种在不通过 CPU 的情况下,直接在外设和存储器之间传输数据的技术。它可以显著提高数据传输的效率,降低 CPU 的负担。
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传 输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。 两个DMA控制器有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自 于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
#include "dma.h"
//初始化DMA
void DMA1_Init(void)
{
//1.开启时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
//2.DMA相关配置
//数据传输方向:从存储器读,发往串口外设
DMA1_Channel4->CCR |= DMA_CCR4_DIR;
//数据宽度:8位 - 00
DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
//地址自增:开启自增,串口地址不能自增
DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;
DMA1_Channel4->CCR |= DMA_CCR4_MINC;
//开启数据传输完成中断
DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
//NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel4_IRQn,2);
NVIC_EnableIRQ(DMA1_Channel4_IRQn);
//3.使能串口DMA传输功能
USART1->CR3 |= USART_CR3_DMAT;
}
//数据传输
void DMA1_Transmit(uint32_t srcAddr,uint32_t destAddr,uint16_t dataLen)
{
//设置外设地址
DMA1_Channel4->CPAR = destAddr;
//设置存储器地址
DMA1_Channel4->CMAR = srcAddr;
//设置传输的数据量
DMA1_Channel4->CNDTR = dataLen;
//开启通道,开始传输数据
DMA1_Channel4->CCR |= DMA_CCR4_EN;
}
//中断服务程序
void DMA1_Channel1_IRQHandler(void)
{
//判断中断标志位
if(DMA1->ISR & DMA_ISR_TCIF4)
{
//清除中断标志
DMA1->IFCR |= DMA_IFCR_CTCIF4;
//关闭DMA通道
DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
}
}
#include "usart.h"
#include "delay.h"
#include <string.h>
#include "dma.h"
//定义变量,放在RAM中,接收数据
uint8_t src[] = "hello DMA\n";
int main(void)
{
// 初始化
USART_Init();
DMA1_Init();
// 发送字符串
printf("DMA测试\n\n");
Delay_us(83);//延时防止DMA通道覆盖还没发完的数据
//开启DMA通道进行传输
DMA1_Transmit((uint32_t)src,(uint32_t)&(USART1->DR),strlen((char*)src));
while(1)
{
}
}
ADC(模数转换)
ADC(Analog-to-Digital Converter,模数转换器)是一种将连续的模拟信号转换为离散的数字信号的设备。模数转换器在许多应用中非常重要,如传感器数据采集、音频处理和通信系统等。
ADC 的工作原理
ADC 的基本工作流程包括以下几个步骤:
- 采样: 捕获输入的模拟信号。
- 保持: 在转换期间保持模拟信号的值。
- 量化: 将模拟信号分成多个离散值。
- 编码: 将量化后的值转换为数字格式。
ADC 的关键参数
STM32F103 系列微控制器提供了三个 ADC,每个 ADC 具备 12 位的分辨率,最多支持 16 个通道和 2 个内部信号源。这些 ADC 使用逐次逼近型架构,适用于各种应用场景,如传感器读取和信号处理。
12位ADC是一种逐次逼近型模拟数字转换器。它有多达18个通道,可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右 对齐方式存储在16位数据寄存器中。 模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。 ADC的输入时钟不得超过14MHz,它是由PCLK2经分频产生。
ADC 特性
1.1 精度与分辨率
1.2 工作模式
1.3 模拟看门狗
ADC 具备模拟看门狗功能,允许应用程序监测输入电压是否超出预定义的高/低阈值。这对保护电路和避免系统故障非常有用。
1.4 输入时钟
3 必须避免同时为规则和注入组设置间断模式。间断模式只能作用于一组转换。
例如: 当ADCCLK=14MHz,采样时间为1.5周期 TCONV = 1.5 + 12.5 = 14周期 = 1μs
#include "adc.h"
// 初始化ADC1
void ADC1_Init(void)
{
// 1. 使能ADC1时钟
RCC->APB2ENR |= RCC_APB2ENR_ADC1EN; // 使能ADC1时钟
// 2. 配置ADC时钟分频
RCC->CFGR |= RCC_CFGR_ADCPRE_1; // 设置ADC时钟为6分频,得到12MHz
RCC->CFGR &= ~RCC_CFGR_ADCPRE_0; // 清除其他位以确保正确配置
// 3. 配置GPIO(PA5)为模拟输入
GPIOA->CRL &= ~(GPIO_CRL_MODE5 | GPIO_CRL_CNF5); // 将PA5配置为模拟输入模式
// 4. 开启温度传感器和内部参考电压
ADC1->CR2 |= ADC_CR2_TSVREFE; // 激活温度传感器和VREFINTs
// 5. 配置ADC工作模式
ADC1->CR1 |= ADC_CR1_SCAN; // 设置为多通道扫描模式
ADC1->CR2 |= ADC_CR2_CONT; // 设置为连续转换模式
ADC1->CR2 &= ~ADC_CR2_ALIGN; // 数据右对齐
// 6. 设置通道5的采样时间
ADC1->SMPR2 &= ~ADC_SMPR2_SMP5; // 清除通道5的采样时间设置
ADC1->SMPR2 |= ADC_SMPR2_SMP5_0; // 设置通道5的采样时间为7.5个时钟周期
// 7. 设置通道16(内部温度传感器)的采样时间
ADC1->SMPR1 &= ~ADC_SMPR1_SMP16; // 清除通道16的采样时间设置
ADC1->SMPR1 |= ADC_SMPR1_SMP16_0; // 设置通道16的采样时间为7.5个时钟周期
// 8. 配置规则组通道序列
ADC1->SQR1 &= ~ADC_SQR1_L; // 清除通道数量设置
ADC1->SQR1 |= ADC_SQR1_L_0; // 设置通道个数L = 2
// 设置通道顺序
ADC1->SQR3 &= ~ADC_SQR3_SQ1; // 清零SQ1
ADC1->SQR3 |= 5 << 0; // 设置SQ1为通道5
ADC1->SQR3 &= ~ADC_SQR3_SQ2; // 清零SQ2
ADC1->SQR3 |= 16 << 5; // 设置SQ2为通道16(温度传感器)
// 9. 选择软件触发ADC(可选)
// ADC1->CR2 &= ~ADC_CR2_EXTTRIG; // 禁用外部触发
// ADC1->CR2 |= ADC_CR2_EXTSEL; // 允许软件触发
}
// 初始化DMA
void ADC1_DMA_Init(void)
{
// 1. 使能DMA时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN; // 使能DMA1时钟
// 2. 配置DMA通道1
DMA1_Channel1->CCR &= ~DMA_CCR1_DIR; // 设置为外设到内存的传输方向
DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE_1; // 数据宽度16位
DMA1_Channel1->CCR |= DMA_CCR1_PSIZE_0; // 设置为16位
DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE_1; // 内存数据宽度16位
DMA1_Channel1->CCR |= DMA_CCR1_MSIZE_0; // 设置为16位
// 3. 设置地址自增模式
DMA1_Channel1->CCR &= ~DMA_CCR1_PINC; // 外设地址不自增
DMA1_Channel1->CCR |= DMA_CCR1_MINC; // 内存地址自增
// 4. 开启循环模式(可选)
DMA1_Channel1->CCR |= DMA_CCR1_CIRC; // 启用循环模式,如果需要一次性读取可以禁用
// 5. 开启DMA模式
ADC1->CR2 |= ADC_CR2_DMA; // 使能DMA传输
}
// 开启转换(带DMA)
void ADC1_DMA_StartConvert(uint32_t destAddr, uint8_t len)
{
// 1. 配置DMA传输参数
DMA1_Channel1->CPAR = (uint32_t)&(ADC1->DR); // 设置外设地址为ADC数据寄存器
DMA1_Channel1->CMAR = destAddr; // 设置内存地址为目标地址
DMA1_Channel1->CNDTR = len; // 设置要传输的数据数量
// 2. 启用DMA通道
DMA1_Channel1->CCR |= DMA_CCR1_EN; // 使能DMA通道
// 3. 上电ADC
ADC1->CR2 |= ADC_CR2_ADON; // 开启ADC
// 4. 执行校准
ADC1->CR2 |= ADC_CR2_CAL; // 启动ADC校准
while (ADC1->CR2 & ADC_CR2_CAL); // 等待校准完成
ADC1->CR2 |= ADC_CR2_ADON; // 再次开启ADC
// 6. 等待首次转换完成
while ((ADC1->SR & ADC_SR_EOC) == 0); // 等待转换完成标志
}
#include "usart.h"
#include "delay.h"
#include <string.h>
#include "adc.h"
int main(void)
{
// 初始化
USART_Init();
printf("%s","hell ptm\n");
ADC1_Init();
ADC1_DMA_Init();
uint16_t arr[2]={0};
ADC1_DMA_StartConvert((uint32_t)arr, 2);
while(1)
{
// 添加调试信息,查看arr[1]的原始值
printf("Raw ADC Values: arr[0] = %d, arr[1] = %d\n", arr[0], arr[1]);
printf("ADC1-5:%.3lf V,内部芯片温度 :%.3lf ℃\n",arr[0]*3.3/4095,(1.43-(arr[1]*3.3/4095))/0.0043 + 25);
Delay_ms(1000);
double a = 0;
for (int i = 0; i < 10000; i++) {
a += 0.00001; // 每次循环对 a 进行小幅度增加
}
printf("Final value of a: %f\n", a);
}
}
SPI通信
作者:胖提莫