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寄存器中的复位标志位和备份区域中的寄存器。
系统复位的触发事件包括:
- NRST引脚上的低电平:外部复位信号。
- 窗口看门狗计数终止(WWDG复位)。
- 独立看门狗计数终止(IWDG复位)。
- 软件复位(SW复位):通过将Cortex-M3中的SYSRESETREQ位置为
1
来实现。 - 低功耗管理复位。
识别复位事件
可以通过查看RCC_CSR控制状态寄存器中的复位状态标志位来识别复位事件的来源。
软件复位
通过设置Cortex-M3的复位控制寄存器中的SYSRESETREQ位为1
,可以触发软件复位。有关详细信息,请参考Cortex-M3技术参考手册。
低功耗管理复位
低功耗管理复位可以在以下情况下产生:
-
进入待机模式时:
- 通过将用户选择字节中的nRST_STDBY位置为
1
,使能该复位。 - 在此情况下,即使执行了进入待机模式的过程,系统会被复位,而不是进入待机模式。
- 通过将用户选择字节中的nRST_STDBY位置为
-
进入停止模式时:
- 通过将用户选择字节中的nRST_STOP位置为
1
,使能该复位。 - 同样地,即使执行了进入停机模式的过程,系统会被复位,而不是进入停机模式。
- 通过将用户选择字节中的nRST_STOP位置为
2. 电源复位
电源复位发生在以下事件之一发生时:
- 上电/掉电复位(POR/PDR复位)。
- 从待机模式返回。
电源复位将复位除了备份区域外的所有寄存器。复位信号将在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,低速外部时钟)。
提供多种时钟源的主要目标是节能。具体原因包括:
-
根据需求选择时钟:
- 高速设备(如CPU和高速外设)使用高速时钟,确保性能。
- 低速设备(如RTC和看门狗)使用低速时钟,减少功耗。
-
动态关闭不必要的时钟:
- 在不需要时,用户可以独立启动或关闭任意时钟源,从而优化功耗。
-
适应不同工作模式:
- 在待机或低功耗模式下,系统可以切换到低速时钟,确保最低能耗,同时保持必要的功能。
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的应用场景
- LED控制:通过输出信号控制LED的状态(开/关)。
- 按钮输入:读取按钮的状态,进行相应的处理(如开始或停止某个功能)。
- 传感器接口:连接传感器,读取其输出数据。
- 事件响应:使用中断功能,对外部事件(如传感器触发)做出快速响应。
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配置
- 在复位期间和刚复位后,所有GPIO端口默认配置为浮空输入模式,具体配置为:
- CNFx[1:0] = 01b(配置为浮空输入)
- MODEx[1:0] = 00b(选择输入模式)
2. JTAG引脚的状态
复位后,JTAG引脚被配置为特定的输入模式:
- PA15(JTDI):置于上拉模式
- PA14(JTCK):置于下拉模式
- PA13(JTMS):置于上拉模式
- PB4(JNTRST):置于上拉模式
这一配置确保在复位后,JTAG接口的引脚能够正常工作,不会因为浮空状态而引起信号干扰。
输出配置
当GPIO引脚被配置为输出时:
- 输出数据寄存器(GPIOx_ODR):写入该寄存器的值将被输出到相应的I/O引脚。
- 输出模式:
- 推挽模式:可以输出高电平和低电平,能够驱动较大的负载。
- 开漏模式:仅在输出0时,N-MOS被打开,适合与多个设备共享同一信号线。
输入配置
- 输入数据寄存器(GPIOx_IDR):该寄存器在每个APB2时钟周期捕捉I/O引脚上的数据,允许用户读取引脚的当前状态。
- 内部弱上下拉:所有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状态。
- 在推挽模式下,读取输出数据寄存器将返回最后一次写入的值。
复用功能配置
当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. 中断的基本概念
- 中断:在主程序运行过程中,特定事件会使CPU暂停当前程序,转而处理这个事件。处理完成后,CPU会返回到被打断的位置继续执行。
- 中断源:触发中断的特定事件,可能是外部设备的信号(如GPIO)或内部事件(如定时器溢出)。
- 断点:被中断源打断的程序执行位置。
- 中断处理程序(ISR):处理特定事件的函数,称为中断服务例程。
2. 中断嵌套
- 中断嵌套:在执行一个中断处理程序时,如果有更高优先级的中断源触发,则CPU可以暂停当前的中断处理程序,转而执行新的中断处理程序。
- 优先级管理:中断的优先级决定了一个中断是否可以打断另一个中断。优先级高的中断可以打断优先级低的中断,而优先级低的中断无法打断优先级高的中断。
3. 中断源分类
- 外部中断源:来自外部设备的中断信号,如按键、传感器等,通常通过GPIO引脚触发。
- 内部中断源:来自微控制器内部的事件,如定时器溢出、ADC转换完成等。这些内部中断有时被称为异常。
NVIC嵌套向量中断控制器
- NVIC(Nested Vectored Interrupt Controller) 是与处理器核心紧密集成的中断控制器。它能够实现低延迟的中断处理,并高效管理所有中断,包括内核异常和外部中断。
- NVIC 负责决定哪个中断的处理程序将由 CPU 执行。每个外部中断都可以被使能或禁止,并可以处于挂起状态或清除状态。
- 中断可以是电平触发或脉冲触发,这样 NVIC 能够处理多种中断源。
- STM32 系列中,16 个 I/O 中断与 PVD(电源电压检测)、RTC(实时时钟)、USB、以太网检测等 20 个外部中断通过 EXTI 控制,最终交给 NVIC 处理。其他中断则直接交给 NVIC。
中断优先级
NVIC 提供了中断优先级管理功能,以方便软件管理中断。每个中断可以设置优先级,优先级是通过 4个位进行控制的,值越小优先级越高。中断优先级分为两种:抢占优先级和响应优先级。
优先级规则:
- 优先级值越小,优先级越高。
- 如果不设置优先级,默认优先级为 0。
- 先比较抢占优先级:抢占优先级高的可以打断抢占优先级低的。
- 若抢占优先级相同,再比较响应优先级;但响应优先级不会导致中断嵌套。
- 若挂起的优先级(抢占和响应)都相同,则查找中断向量表,值小的先响应。
优先级分组:
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。
- 中断源:EXTI 可以从多个引脚接收中断信号,支持多种外部事件的触发。
- 触发方式:可以配置为上升沿、下降沿或双边沿触发。
- 中断使能:每个中断源可以独立使能或禁用,以控制系统对外部事件的响应。
- 中断挂起:当外部事件发生时,EXTI 可以将中断请求挂起,直到被处理。
AFIO (Alternate Function I/O)
AFIO(Alternate Function I/O)是STM32微控制器中的一个重要模块,负责管理引脚的复用功能。AFIO允许用户将GPIO引脚配置为不同的功能,以支持多种外设接口和功能。
1. AFIO的主要功能
- 引脚复用:通过AFIO模块,用户可以将GPIO引脚配置为不同的功能,如定时器输出、通信接口(如USART、SPI、I2C)等。
- 外部中断配置:AFIO支持将GPIO引脚配置为外部中断源,允许用户通过配置特定引脚来响应外部事件。
- 功能切换:用户可以在不同的功能之间切换,比如在某些应用中将引脚用作普通I/O,而在其他情况下将其配置为特定的外设功能。
2. AFIO寄存器
AFIO模块包含多个寄存器,用于配置引脚的功能。以下是一些关键寄存器:
- AFIO_MAPR (AFIO模式选择寄存器):
- 控制某些外设的引脚映射,如控制USART、I2C和SPI等的引脚配置。
- AFIO_EXTICR (外部中断配置寄存器):
- 允许用户选择哪个GPIO引脚作为外部中断源。每个EXTI线可以配置为连接到不同的GPIO引脚。
外部中断例程
通过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. 通讯方式
- 全双工异步通信:允许同时进行数据发送和接收。
- NRZ 标准格式:使用非归零编码格式。
- 支持多种通讯模式:
- 同步单向通信:提供时钟信号以同步数据传输。
- 半双工单线通信:在一条线上实现双向通讯。
- LIN(局部互连网):专用于低速通讯的协议。
- 智能卡协议:符合 ISO7816-3 标准。
- IrDA(红外数据组织):支持红外通讯。
2. 波特率和数据格式
- 分数波特率发生器:支持最高达 4.5 Mbits/s 的可编程波特率。
- 可编程数据字长度:支持 8 位或 9 位数据长度。
- 可配置的停止位:支持 1 或 2 个停止位。
3. 其他功能
- LIN 主发送同步断开符能力:生成 13 位断开符,检测 10/11 位断开符。
- IRDA SIR 编码器解码器:正常模式下支持 3/16 位的持续时间。
- 智能卡模拟功能:支持 ISO7816-3 标准定义的异步智能卡协议,支持 0.5 和 1.5 个停止位。
- DMA 支持:可配置的多缓冲器通信,利用 DMA 实现高效的数据传输。
4. 状态和标志
- 发送和接收独立使能位:可单独控制发送和接收功能。
- 检测标志:
- 接收缓冲器满
- 发送缓冲器空
- 传输结束标志
- 错误检测:包括溢出错误、噪声错误、帧错误和校验错误。
5. 中断和多处理器通信
- 中断源:支持多达 10 种中断源,包括 CTS 改变、LIN 断开符检测、发送和接收缓冲器状态等。
- 多处理器通信:通过地址匹配实现静默模式,如果地址不匹配,则进入静默状态。
- 唤醒功能:支持通过空闲总线检测或地址标志检测从静默模式唤醒。
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 引脚外,还需要以下引脚:
- CK(时钟输出引脚):用于发送器输出时钟信号,以同步数据传输。数据可以在 RX 上同步接收。该引脚的时钟相位和极性可通过软件配置。在智能卡模式下,CK 可为智能卡提供时钟信号。
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 专为深度嵌入式应用而设计。
- MicroLib 经过优化,可以使用比使用 ARM 标准库更少的代码和数据存储器。
- MicroLib 设计为无需操作系统即可工作,但这并不妨碍它与任何操作系统或 RTOS(例如 Keil RTX)一起使用。
- MicroLib 不包含文件 I/O 或宽字符支持。
- 由于 MicroLib 已经过优化以最小化代码大小,因此某些函数的执行速度将比 ARM 编译工具中可用的标准 C 库例程更慢。
- MicroLib 和 ARM 标准库都包含在 Keil MDK-ARM 中。
- 使用MicroLib的优缺点
- 使用MicroLIB,简化嵌入式开发操作,例如你用printf()函数的时候,就会从串口1输出字符串,当然也可以重定义到其他串口;
- 使用MicroLIB会优化代码空间,但会降低某些程序的执行效率(比如: memcpy()),效率换空间;
- 由于MicroLIB不支持浮点运算,所以在有FPU单元的MCU上,使用MicroLIB并开启FPU会让程序死机或跑飞。
- Microlib不支持C++,在使用C++开发MCU时,首要条件是不能使用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使用两条线进行通信:
- SDA(Serial Data Line):数据线,用于传输数据。
- SCL(Serial Clock Line):时钟线,由主设备生成,用于同步数据传输。
-
主从设备:
- 主设备:发起通信的设备,控制总线的时钟信号。
- 从设备:响应主设备的请求,执行数据的读写操作。
-
地址:每个从设备在总线上都有一个唯一的地址,主设备通过地址选择要与哪个从设备进行通信。
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 定时器概述
- 功能:SysTick 定时器可以生成定时中断,以实现定时任务的调度和延时功能。
- 时钟源:SysTick 可以使用系统时钟(HCLK)作为时钟源。
- 配置:可以设置定时器的重载值,以定义中断发生的频率。
- 内嵌于CM3内核:SysTick定时器是ARM Cortex-M3 (CM3)内核的一部分,集成在嵌套向量中断控制器(NVIC)中。
- 24位向下计数器:它是一个24位的向下递减计数器,每次计数的时间为
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 的关键参数
- 分辨率: ADC 的分辨率决定了它可以产生多少个不同的数字值。例如,12 位的 ADC 可以输出 4096 个不同的值(0-4095)。
- 采样速率: ADC 的采样速率是指每秒钟能够采样的次数。较高的采样速率可以捕捉更快变化的信号。
- 参考电压: ADC 的参考电压决定了转换范围,通常是 ADC 输入的最大值。例如,若参考电压为 3.3V,则输出的数字值范围为 0-4095。
STM32F103 系列微控制器提供了三个 ADC,每个 ADC 具备 12 位的分辨率,最多支持 16 个通道和 2 个内部信号源。这些 ADC 使用逐次逼近型架构,适用于各种应用场景,如传感器读取和信号处理。
12位ADC是一种逐次逼近型模拟数字转换器。它有多达18个通道,可测量16个外部和2个内部信号源。各通道的A/D转换可以单次、连续、扫描或间断模式执行。ADC的结果可以左对齐或右 对齐方式存储在16位数据寄存器中。 模拟看门狗特性允许应用程序检测输入电压是否超出用户定义的高/低阀值。 ADC的输入时钟不得超过14MHz,它是由PCLK2经分频产生。
ADC 特性
1.1 精度与分辨率
- 分辨率: 12 位,能够提供 4096 种不同的数字输出(0-4095)。
- 数据存储: ADC 的转换结果可以选择左对齐或右对齐,以适应不同的数据处理需求。
1.2 工作模式
- 单次模式: 仅执行一次转换,适用于偶尔读取的场景。
- 连续模式: 持续不断地执行转换,适合实时监测。
- 扫描模式: 自动依次读取多个通道,适合需要监测多个信号源的应用。
- 间断模式: 在特定条件下进行转换,可以通过外部触发来控制。
1.3 模拟看门狗
ADC 具备模拟看门狗功能,允许应用程序监测输入电压是否超出预定义的高/低阈值。这对保护电路和避免系统故障非常有用。
1.4 输入时钟
- 最大频率: ADC 输入时钟不得超过 14 MHz。
- 时钟源: ADC 的时钟由 PCLK2 经过分频产生。确保根据 CPU 的运行频率合理配置 ADC 时钟,以满足最大频率的要求。
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通信
SPI(串行外设接口)是一种用于微控制器和外部设备之间通信的同步串行通信协议。它常用于短距离通信,特别是在嵌入式系统中,因其简单且高效而被广泛使用。
SPI 的基本概念
-
主设备和从设备:SPI 通信总是由一个主设备和一个或多个从设备组成。主设备控制通信的时序,而从设备响应主设备的请求。
-
信号线:
- MOSI(Master Out Slave In):主设备发送数据到从设备的线。
- MISO(Master In Slave Out):从设备发送数据到主设备的线。
- SCLK(Serial Clock):由主设备生成的时钟信号,控制数据传输的时序。
- SS(Slave Select):选择特定从设备的信号线,低电平有效。
SPI 工作原理
-
初始化:主设备初始化 SPI 接口,包括配置时钟频率、数据格式(如数据位数、时钟极性和相位等)及选择从设备。
-
选择从设备:通过拉低 SS 线,主设备选择想要通信的从设备。
-
数据传输:
- 主设备通过 MOSI 线发送数据,同时在 SCLK 线上提供时钟信号。
- 从设备在 MISO 线上返回数据,通常是在主设备发送数据的同时。
- 数据传输是双向的,主设备和从设备可以同时发送和接收数据。
-
结束通信:主设备完成数据传输后,拉高 SS 线,结束与从设备的通信。
SPI 的优缺点
优点
- 速度快:SPI 可以支持较高的时钟频率,适合高数据速率传输。
- 全双工:同时发送和接收数据,提高了效率。
- 简单的硬件:只需四条信号线,易于实现。
缺点
- 占用引脚多:每个从设备需要独立的 SS 线,随着从设备数量增加,所需引脚也会增加。
#include "spi.h"
//初始化
void SPI_Init(void)
{
//1.开启时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
RCC->APB2ENR |= RCC_APB2ENR_IOPBEN;
RCC->APB2ENR |= RCC_APB2ENR_SPI1EN;
//2.GPIO工作模式
//PB12:通用推挽输出,CNF - 00,MODE - 11
GPIOB->CRH &= ~GPIO_CRH_CNF12;
GPIOB->CRH |= GPIO_CRH_MODE12;
//PA5,PA7,复用推挽输出,CNF - 10,MODE - 11
GPIOA->CRL &= ~GPIO_CRL_CNF5_0;
GPIOA->CRL |= GPIO_CRL_CNF5_1;
GPIOA->CRL |= GPIO_CRL_MODE5;
GPIOA->CRL &= ~GPIO_CRL_CNF7_0;
GPIOA->CRL |= GPIO_CRL_CNF7_1;
GPIOA->CRL |= GPIO_CRL_MODE7;
//PA6: MISO 浮空输入,CNF - 01,MODE - 00
GPIOA->CRL &= ~GPIO_CRL_CNF6_1;
GPIOA->CRL |= GPIO_CRL_CNF6_0;
GPIOA->CRL &= ~GPIO_CRL_MODE6;
//SPI相关配置
SPI1->CR1 |= SPI_CR1_MSTR;//配置SPI为主模式
//使用软件控制片选信号为高,不使用从模式
SPI1->CR1 |= SPI_CR1_SSM;
SPI1->CR1 |= SPI_CR1_SSI;
//配置工作模式0,时钟极性和相位
SPI1->CR1 &= ~SPI_CR1_CPOL;
SPI1->CR1 &= ~SPI_CR1_CPHA;
//配置时钟分频系数,波特率选择:BR - 001 四分频
SPI1->CR1 &= ~SPI_CR1_BR;
SPI1->CR1 |= SPI_CR1_BR_0;
//设置数据帧格式
SPI1->CR1 &= ~SPI_CR1_DFF;
//配置高位先行MSB
SPI1->CR1 &= ~SPI_CR1_LSBFIRST;
//SPI模块使能
SPI1->CR1 |= SPI_CR1_SPE;
}
//数据传输的开始和结束
void SPI_Start(void)
{
CS_LOW;
}
void SPI_Stop(void)
{
CS_HIGH;
}
//主从设备交换一个字节的数据
uint8_t SPI_SwapByte(uint8_t Byte)
{
//写入数据到缓冲区
while((SPI1->SR & SPI_SR_TXE)==0);//等待发送缓冲区为空
SPI1->DR = Byte;//写入数据到DR寄存器
//读取MISO发来的数据
while((SPI1->SR & SPI_SR_RXNE)==0);//等待接收缓冲区满
return (uint8_t)(SPI1->DR & 0xff);//返回接收到的数据
}
STM32的存储器
STM32包含片内SRAM(64K):它可以以字节、半字(16位)或全字(32位)访问。SRAM的起始地址是0x2000 0000。片内Flash(最大可达2M)。
存储器映射
什么叫存储器映射呢?存储器本身并不具备地址信息,那么CPU要准确找到存储某个信息的存储单元,就必须为这些单元分配一个相互可区分的标识,这个标识就是常说的地址编码。
STM32中集成多种存储器(各种外设也需要分配地址),同一类型的存储器当作一组block,为每一个block分配一个数值连续,存储单元数相等,以16进制表示的自然数集合作为存储器Block的地址编码。这种自然数集合与存储器Block的对应关系就是存储器映射。
存储器映射其实就是将芯片理论上的地址分配给各个存储器。
需要注意的是:存储器映射并不是只针对SRAM和片内Flash做地址映射,其实所有的片内外设(比如IO口)都需要地址,也都需要做映射。
STM32具体存储器映射图
芯片能访问的存储空间有多大,是由谁定的?这个是由芯片的地址总线的数量决来定的,STM32芯片内部的地址总线为32根。所以STM32有4G的地址空间。(这个4GB的是STM32理论分配的地址空间。也就是说实际上并不是有这么大的存储单元,很多地址都是预留地址,空着还没用呢)。
程序存储器、数据存储器、寄存器和输入输出端口被组织在这个4GB的线性地址空间内。数据字节以小端格式(先存低位再存高位)存放在存储器中。
ARM把可访问的存储器空间分成8个主要块,每个块为512MB。这个容量是非常大的,因此芯片厂商就在每块容量范围内设计各自特色的外设。但是每块区域容量占用越大,芯片成本就越高,所以说我们使用的 STM32 芯片都是只用了其中一部分。ARM 在对这 4GB 容量分块的时候是按照其功能划分,每块都有它特殊的用途。
在这8个Block里面,要特别注意Block0、Block1和Block2这3个块。因为其中包含了STM32芯片的内部 Flash、RAM和片上外设。
0x0000 0000-0x0007 FFFF:取决于BOOT引脚,可以是 Flash 的别名,也可以是系统存储器的别名。(512K)
0x0008 0000-0x07FF FFFF:预留。(1M)
0x0800 0000-0x0807 FFFF:片内 FLASH,我们编写的程序就放在这一区域(512K)
0x0808 0000-0x1FFF EFFF:预留。(383M)
0x1FFF F000-0x1FFF F7FF:系统存储器,里面存放的是 ST 出厂时烧写好的ISP自举程序,用户无法改动。使用串口下载的时候需要用到这部分程序。(2K)
0x1FFF F800-0x1FFF F80F:可选字节,用于配置读写保护、BOR级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。当芯片不小心被锁住之后,我们可以从RAM里面启动来修改这部分相应的寄存器位。
0x1FFF F810-0x1FFF FFFF:预留。
寄存器
寄存器是单片机(如STM32)内部的一种特殊存储器,用于控制和管理各种功能和外设。
寄存器的定义
寄存器是存储在CPU内部或片上外设中的小型存储单元,通常用于存储控制信息和状态数据。它们以特定的地址映射到内存空间,使得程序可以通过这些地址直接访问和控制硬件功能。
寄存器的特点
-
高速性:寄存器的访问速度比RAM快得多,因为它们直接与CPU相连,用于执行指令时的快速数据存取。
-
功能性:每个寄存器通常对应特定的功能或外设,例如控制GPIO、定时器、ADC等。通过写入或读取寄存器,可以直接控制硬件的行为。
-
地址映射:寄存器在存储器中有固定的地址,通过存储器映射,开发者可以使用指针或直接的地址访问这些寄存器。
-
别名:为了方便编程,寄存器通常会被赋予易于记忆的别名,开发者可以通过这些别名来访问寄存器,而不是使用难以记忆的地址。
工作原理
-
存储器映射:在STM32等单片机中,所有寄存器都被映射到特定的存储器区域。每个寄存器的起始地址对应于特定功能的控制单元。
-
C语言访问:在C语言中,可以使用指针操作来访问寄存器。例如,通过定义一个指向特定内存地址的指针,可以方便地读取或写入寄存器的值。
例子
例如,在STM32中,如果要控制一个GPIO端口,可以通过设置对应的寄存器来配置引脚为输入或输出状态。假设某个GPIO的控制寄存器的地址为0x48000000
,可以在C语言中这样定义:
#define GPIOA_MODER (*(volatile unsigned int*)0x48000000)
通过这种方式,开发者可以使用GPIOA_MODER
来直接访问和修改GPIOA的模式寄存器,而不需要记住具体的地址。
总结
寄存器是单片机内部重要的存储单元,通过它们可以实现对硬件的精确控制。在编程时,通过存储器映射和别名访问,可以简化对寄存器的操作,提高开发效率。
寄存器映射
寄存器映射是将已经分配好地址的特定功能存储器单元赋予易于理解的别名的过程。这一过程使得开发者在编程时能够更方便地访问和控制硬件外设,而不必直接使用地址。
在STM32中的应用
在STM32微控制器中,ST公司通过提供的头文件 stm32f10x.h
完成了寄存器的映射。这些头文件中定义了所有外设的寄存器名称和对应的地址,使得开发者可以直接使用寄存器名进行操作。