呼吸灯
PWM 驱动呼吸灯的基本原理
呼吸灯的效果是通过改变 LED 的亮度实现的。具体方法是:
- 使用 PWM 输出控制 LED:PWM 输出的占空比决定了 LED 的亮度。
- 占空比逐渐变化:通过软件控制,PWM 的占空比以一定的步长逐渐增加或减少,从而实现亮度的渐变。
PWM 信号由 STM32 的定时器生成,并通过 GPIO 引脚输出。
软件实现步骤
1. 配置定时器和 GPIO
- 选择定时器:STM32F103C8T6 的 TIM2~TIM4 支持 PWM 输出。
- GPIO 设置:将用于 PWM 输出的 GPIO 配置为复用推挽输出模式。
- 定时器设置:
- 设置定时器频率,确定 PWM 的周期。
- 配置 PWM 输出通道的比较值(占空比)。
2. 占空比调节实现呼吸效果
通过编写软件,改变占空比的值,使 LED 的亮度按一定时间间隔逐渐增加和减少,形成呼吸效果。
完整代码
以下是一个完整的呼吸灯程序,基于 STM32 HAL 库。
#include "stm32f1xx_hal.h"
TIM_HandleTypeDef htim2; // 定义定时器句柄
// 初始化 GPIO(PWM 输出引脚)
void GPIO_Init(void) {
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0; // PA0 输出 PWM
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出模式
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);//GPIO初始化
}
// 初始化定时器
void TIM2_Init(void) {
__HAL_RCC_TIM2_CLK_ENABLE(); // 使能 TIM2 时钟
// 配置 TIM2
htim2.Instance = TIM2; // TIM2
htim2.Init.Prescaler = 72 - 1; // 预分频器:72MHz / 72 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数模式
htim2.Init.Period = 1000 - 1; // 自动重装值:1000,PWM 频率 = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&htim2); // 初始化 TIM2 为 PWM 模式
// 配置 PWM 通道
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM 模式 1
sConfigOC.Pulse = 0; // 初始占空比为 0%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
// 启动 PWM 输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
// 主程序
int main(void) {
HAL_Init(); // 初始化 HAL 库
SystemClock_Config(); // 配置系统时钟为 72MHz
GPIO_Init(); // 初始化 GPIO
TIM2_Init(); // 初始化定时器
uint16_t duty = 0; // 占空比变量
int step = 10; // 每次增加或减少的步长
while (1) {
// 调整 PWM 占空比
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
// 增加或减少占空比
duty += step;
if (duty == 1000 || duty == 0) {
step = -step; // 反转步长方向
}
HAL_Delay(20); // 延时 20ms(呼吸效果的速度控制)
}
}
// 系统时钟配置函数
void SystemClock_Config(void) {
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
// 配置时钟源
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;
HAL_RCC_OscConfig(&RCC_OscInitStruct);
// 配置时钟分频
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK |
RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}
代码解析
-
GPIO 配置:
- 将 PA0 配置为 TIM2 的 PWM 输出引脚。
- 使用复用推挽模式保证 PWM 信号输出质量。
-
TIM2 配置:
- 使用定时器 TIM2,设置频率为 1kHz。
- 配置 PWM 模式 1:计数器值小于占空比时输出高电平。
-
PWM 占空比调节:
- 使用
__HAL_TIM_SET_COMPARE
修改占空比。 - 在主循环中通过增加和减少占空比实现呼吸效果。
- 使用
-
呼吸效果控制:
- 调整
step
和HAL_Delay
的值可以改变呼吸灯的速度。
- 调整
调试和优化
-
呼吸灯速度:
- 增大或减小
HAL_Delay
的值,控制呼吸的快慢。 - 调整
step
的大小,改变亮度变化的平滑程度。
- 增大或减小
-
PWM 分辨率:
- 增大
TIM_Period
的值可以提高 PWM 分辨率,使亮度变化更加细腻。
- 增大
-
低功耗优化:
- 使用 DMA 或定时器中断替代
while
循环可以降低功耗。
- 使用 DMA 或定时器中断替代
输入捕获
输入捕获是一种基于定时器的功能,用于测量输入信号的特性(如频率、周期、占空比等)。输入捕获的核心思想是利用定时器的寄存器记录外部信号发生事件的时间点,通过比较多个时间点,计算出输入信号的参数。
输入捕获工作流程
-
配置 GPIO 引脚
将定时器的捕获通道引脚(如 TIM2_CH1)配置为输入模式,以接收外部信号。 -
配置定时器为输入捕获模式
- 配置捕获通道为上升沿、下降沿或双边沿触发。
- 设置定时器时钟和分频系数,用于控制计数精度。
-
捕获输入信号的时间点
当输入信号发生触发事件时(如上升沿),定时器捕获寄存器(CCR)会记录当前计数值。 -
计算信号参数
根据连续两次捕获的时间点,计算输入信号的周期、频率或占空比。
输入捕获实现步骤
1. 硬件连接
将输入信号连接到 STM32 的定时器捕获通道引脚上(如 TIM2_CH1 使用 PA0)。
2. 配置 GPIO 引脚
使用标准库函数配置 GPIO 为输入浮空模式。
void GPIO_Config(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 选择 PA0(TIM2_CH1)
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; // 输入浮空模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始化 GPIO
}
3. 配置定时器
初始化定时器,设置输入捕获模式。
void TIM2_Capture_Config(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE); // 开启 TIM2 时钟
// 配置 TIM2 时间基准
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 定时器周期(16 位最大值)
TIM_TimeBaseStructure.TIM_Prescaler = 71; // 预分频器(72MHz / (71+1) = 1MHz,1µs 分辨率)
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up; // 向上计数模式
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
// 配置输入捕获通道
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1; // TIM2_CH1
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising; // 上升沿捕获
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI; // 直接映射 TI1
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1; // 无预分频
TIM_ICInitStructure.TIM_ICFilter = 0x0; // 不滤波
TIM_ICInit(TIM2, &TIM_ICInitStructure);
// 开启定时器和输入捕获
TIM_Cmd(TIM2, ENABLE); // 启动 TIM2
TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE); // 开启 CC1 捕获中断
}
4. 配置 NVIC(中断优先级)
在输入捕获模式中,通常需要通过中断处理捕获的时间值。
void NVIC_Config(void) {
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn; // TIM2 中断通道
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 抢占优先级
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; // 响应优先级
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; // 使能中断
NVIC_Init(&NVIC_InitStructure);
}
5. 中断服务函数(ISR)
在中断服务函数中读取捕获寄存器的值,计算信号周期或频率。
volatile uint16_t CaptureValue1 = 0; // 第一次捕获的计数值
volatile uint16_t CaptureValue2 = 0; // 第二次捕获的计数值
volatile uint8_t CaptureFlag = 0; // 捕获标志位
volatile uint32_t Frequency = 0; // 计算得到的频率
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) { // 检查 CC1 中断
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); // 清除中断标志位
if (CaptureFlag == 0) {
CaptureValue1 = TIM_GetCapture1(TIM2); // 读取第一次捕获值
CaptureFlag = 1; // 设置捕获标志
} else if (CaptureFlag == 1) {
CaptureValue2 = TIM_GetCapture1(TIM2); // 读取第二次捕获值
if (CaptureValue2 > CaptureValue1) {
Frequency = 1000000 / (CaptureValue2 - CaptureValue1); // 计算频率
} else {
Frequency = 1000000 / (0xFFFF - CaptureValue1 + CaptureValue2 + 1);
}
CaptureFlag = 0; // 清除捕获标志
}
}
}
详细解释
TIM_GetCapture1()
:读取输入捕获寄存器 CCR1 的值。- 时间差计算:根据两次捕获值计算信号周期,从而推导频率。
- 如果
CaptureValue2 > CaptureValue1
,说明计数器未溢出。 - 如果
CaptureValue2 < CaptureValue1
,说明计数器发生了溢出,需要修正。
- 如果
整体代码
#include "stm32f10x.h"
volatile uint16_t CaptureValue1 = 0;
volatile uint16_t CaptureValue2 = 0;
volatile uint8_t CaptureFlag = 0;
volatile uint32_t Frequency = 0;
void GPIO_Config(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
}
void TIM2_Capture_Config(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF;
TIM_TimeBaseStructure.TIM_Prescaler = 71;
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseStructure);
TIM_ICInitTypeDef TIM_ICInitStructure;
TIM_ICInitStructure.TIM_Channel = TIM_Channel_1;
TIM_ICInitStructure.TIM_ICPolarity = TIM_ICPolarity_Rising;
TIM_ICInitStructure.TIM_ICSelection = TIM_ICSelection_DirectTI;
TIM_ICInitStructure.TIM_ICPrescaler = TIM_ICPSC_DIV1;
TIM_ICInitStructure.TIM_ICFilter = 0x0;
TIM_ICInit(TIM2, &TIM_ICInitStructure);
TIM_Cmd(TIM2, ENABLE);
TIM_ITConfig(TIM2, TIM_IT_CC1, ENABLE);
}
void NVIC_Config(void) {
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
void TIM2_IRQHandler(void) {
if (TIM_GetITStatus(TIM2, TIM_IT_CC1) != RESET) {
TIM_ClearITPendingBit(TIM2, TIM_IT_CC1);
if (CaptureFlag == 0) {
CaptureValue1 = TIM_GetCapture1(TIM2);
CaptureFlag = 1;
} else if (CaptureFlag == 1) {
CaptureValue2 = TIM_GetCapture1(TIM2);
if (CaptureValue2 > CaptureValue1) {
Frequency = 1000000 / (CaptureValue2 - CaptureValue1);
} else {
Frequency = 1000000 / (0xFFFF - CaptureValue1 + CaptureValue2 + 1);
}
CaptureFlag = 0;
}
}
}
int main(void) {
GPIO_Config();
TIM2_Capture_Config();
NVIC_Config();
while (1) {
// 在调试工具中观察 Frequency 值
}
}
测试与结果
- 输入一个频率已知的方波信号(如 1kHz),通过串口或调试工具观察
Frequency
的值,验证捕获功能是否正确。 - 调整
TIM_Period
和TIM_Prescaler
可改变计数器的计时范围与精度。
编码器
STM32F103C8T6编码器概述
STM32F103C8T6 是一款基于 ARM Cortex-M3 内核的微控制器,广泛应用于嵌入式系统中,尤其适用于控制和信号处理任务。它具有丰富的外设,包括定时器、ADC、GPIO等,能够处理各种传感器和执行器的输入输出。对于编码器应用,STM32F103C8T6 具有多个硬件定时器,这些定时器可以配置为捕获模式,适应编码器的输入信号。
编码器的工作原理
编码器通常用于测量旋转或位移,常见的编码器有增量编码器和绝对编码器。增量编码器通过输出一对相位差90°的方波信号(一般为A相和B相)来测量旋转的方向和角度。
增量编码器的工作原理:
- A相和B相信号是正交的(90°相位差),通过比较两者的相位,可以确定旋转的方向。
- 每次编码器轴旋转一个固定角度时,A和B相会变化一小步(如1个脉冲)。
- 通过计算A相或B相的脉冲数,可以计算出旋转的角度和速度。
STM32F103C8T6与编码器接口
STM32F103C8T6的定时器具有专门的编码器接口功能,可以直接读取编码器信号,并计算旋转的角度、方向和速度。通常,使用的是定时器的编码器接口模式,这样可以减轻微控制器的负担。
STM32F103C8T6定时器编码器接口配置步骤
步骤 1: 使能时钟
首先,必须使能定时器和GPIO引脚的时钟,以确保硬件能够正确运行。以下代码将使能定时器1和GPIOA的时钟(PA8和PA9用于连接编码器A相和B相):
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2Periph_TIM1
:使能定时器1的时钟。RCC_APB2Periph_GPIOA
:使能GPIOA端口的时钟。
步骤 2: 配置GPIO引脚
然后,我们需要配置GPIO引脚,以便它们可以接收编码器的A相和B相信号。通常,A和B信号分别连接到定时器的输入引脚(例如,PA8和PA9)。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; // 配置PA8和PA9引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 设置为复用推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 设置引脚最大频率为50MHz
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_Pin_8 | GPIO_Pin_9
:选择PA8和PA9这两个引脚作为定时器输入。GPIO_Mode_AF_PP
:将这些引脚设置为复用推挽输出模式,这样可以通过定时器模块接收信号。GPIO_Speed_50MHz
:设置引脚的最大频率为50 MHz。
步骤 3: 配置定时器基本参数
接下来,我们需要配置定时器的一些基本参数,如预分频器、计数模式、周期等。定时器将根据这些参数进行计数。
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 无预分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_UP; // 向上计数模式
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 最大计数值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1; // 时钟分频
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0; // 重复计数器为0
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure); // 初始化定时器1
TIM_Prescaler = 0
:定时器不使用预分频,计数速度直接与系统时钟相同。TIM_CounterMode_UP
:定时器以向上计数的方式进行计数,即从0开始递增。TIM_Period = 0xFFFF
:设置定时器的计数周期为0xFFFF(即65535),表示定时器可以计数到65535。TIM_ClockDivision = TIM_CKD_DIV1
:设置时钟分频为1,确保时钟源传递给定时器的正确性。
步骤 4: 配置编码器接口
通过 TIM_EncoderInterfaceConfig()
函数将定时器配置为编码器模式。定时器将从A相和B相信号中读取数据,确定旋转方向,并进行计数。
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_EncoderMode_TI12
:设置定时器为编码器模式,表示定时器将接收A相(通道1)和B相(通道2)的信号。TIM_ICPolarity_Rising
:设置捕获信号的极性为上升沿,即每次A或B信号从低电平变为高电平时触发计数。
步骤 5: 启动定时器
完成上述配置后,我们通过 TIM_Cmd()
函数启动定时器,使其开始读取编码器信号并进行计数。
TIM_Cmd(TIM1, ENABLE); // 启动定时器1
TIM_Cmd(TIM1, ENABLE)
:启用定时器1,让定时器开始运行,接收编码器信号。
步骤 6: 初始化定时器计数器
为了确保定时器从零开始计数,可以在启动定时器之前将计数器重置。
TIM_SetCounter(TIM1, 0); // 重置定时器1的计数器
TIM_SetCounter(TIM1, 0)
:将定时器1的计数器清零,确保从零开始计数。
步骤 7: 读取计数器值
定时器开始计数后,我们可以通过 TIM_GetCounter()
函数读取定时器的计数值,表示编码器的当前位置。
uint32_t position = TIM_GetCounter(TIM1); // 获取定时器计数器值
TIM_GetCounter(TIM1)
:返回定时器1的计数器值,也即是编码器的当前位置。
步骤 8: 计算速度和方向
- 方向判断:可以通过A相和B相的相位差来确定旋转方向。通常情况下,若A相领先B相,则旋转方向为顺时针;反之则为逆时针。
- 速度计算:可以通过测量单位时间内的脉冲数来计算旋转速度。通过定时器的计数增量,以及时间差,可以计算出旋转的速度。
编码器的工作原理
编码器一般有两种类型:增量编码器和绝对编码器。这里重点讨论增量编码器的工作原理。
-
增量编码器:通过输出两条相位差90度的正交信号(A相和B相),每次编码器旋转一定角度后,A和B信号的状态发生变化。通过计数这些脉冲,可以得到旋转的角度和方向。
- A相和B相信号有90度的相位差。
- 通过检测A相和B相的相位差,可以确定旋转的方向(顺时针或逆时针)。
- 编码器输出的每个脉冲表示一个固定的旋转角度或步长。
STM32F103C8T6与编码器接口
STM32F103C8T6 配备了多种定时器(如 TIM1, TIM2, TIM3 等),它们可以配置为编码器接口模式,直接与编码器的A相和B相信号连接并进行计数。STM32F103C8T6的定时器能够通过硬件自动处理这些信号,无需额外的软件干预。
配置编码器模式的代码示例
以下是STM32F103C8T6配置定时器用于编码器读取的示例代码:
#include "stm32f10x.h"
void Encoder_Init(void)
{
// 使能定时器和GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1 | RCC_APB2Periph_GPIOA, ENABLE);
// 配置GPIO引脚PA8、PA9为定时器输入引脚(例如:TIM1_CH1和TIM1_CH2)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 配置为复用推挽输出模式
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置定时器为编码器模式
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_TimeBaseStructure.TIM_Prescaler = 0; // 不分频
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_UP;
TIM_TimeBaseStructure.TIM_Period = 0xFFFF; // 最大计数值
TIM_TimeBaseStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
// 设置编码器模式
TIM_EncoderInterfaceConfig(TIM1, TIM_EncoderMode_TI12, TIM_ICPolarity_Rising, TIM_ICPolarity_Rising);
TIM_Cmd(TIM1, ENABLE); // 启动定时器
// 初始化定时器计数器
TIM_SetCounter(TIM1, 0); // 重置计数器
}
int main(void)
{
Encoder_Init(); // 初始化编码器
while (1)
{
// 读取定时器计数器的值
uint32_t position = TIM_GetCounter(TIM1);
// 如果需要计算速度,可以根据时间差来计算
// 如果旋转方向需要判断,可以根据A和B信号的相位差判断
}
}
关键点解释
- GPIO配置:我们将PA8和PA9配置为定时器输入引脚,用于接收编码器的A相和B相信号。
- 定时器配置:定时器1(TIM1)被设置为编码器模式,计数器模式为向上计数,计数器周期设置为最大值。
- 编码器接口配置:通过
TIM_EncoderInterfaceConfig
函数将定时器配置为编码器接口模式,选择A相和B相信号的上升沿作为计数输入。 - 读取计数器值:定时器的计数器值存储了编码器的当前位置,可以直接读取这个值。
速度和方向计算
- 方向:如果A相相对于B相是领先的,说明编码器是顺时针旋转,反之则是逆时针旋转。
- 速度:速度的计算通常是基于单位时间内的脉冲数量,可以通过定时器的计数变化来计算速度。
ADC
STM32F103C8T6 的 ADC 模数转换器
STM32F103C8T6 微控制器的 ADC 模数转换器是 12 位分辨率的,支持单通道和多通道的采样,可以实现高精度的模拟信号数字化。该 ADC 提供多个功能,包括可调采样时间、转换精度、自动转换和扫描模式等。
单通道模式
在单通道模式下,ADC 只会对一个通道进行采样和转换。每次转换完成后,转换结果可以直接读取并用于后续处理。
工作流程:
- 配置 ADC 输入通道(例如,选择 PA0 引脚对应的通道)。
- 启动 ADC 转换(可以是软件启动或者外部触发)。
- 等待 ADC 完成转换。
- 读取 ADC 转换结果。
多通道模式
多通道模式下,ADC 可以依次采样多个模拟输入通道。转换完成后,可以选择读取单个通道的结果,也可以通过扫描模式读取所有通道的结果。
工作流程:
- 配置多个输入通道(如 PA0、PA1、PA2 等)。
- 配置 ADC 为扫描模式。
- 启动 ADC 转换。
- 等待 ADC 完成所有通道的转换。
- 读取多个通道的结果。
1. 启用 ADC 时钟
在 STM32 中,外设(如 ADC)需要时钟才能正常工作。因此,第一步是启用 ADC 所需的时钟。通过函数 RCC_APB2PeriphClockCmd
来使能 ADC 时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2Periph_ADC1
:表示启用 ADC1 时钟。ENABLE
:表示使能时钟。
2. 配置 GPIO 引脚为模拟模式
为了将模拟信号传输到 ADC,我们需要配置相应的 GPIO 引脚为模拟输入模式。在本例中,我们选择了 PA0
引脚来连接模拟信号。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 配置 PA0 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_Pin_0
:表示 PA0 引脚。GPIO_Mode_AIN
:将引脚设置为模拟输入模式。
3. 配置 ADC 参数
在这一步,我们需要配置 ADC 的工作模式、精度、采样时间等参数。这些配置决定了 ADC 如何执行转换。
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 设置分辨率为 12 位
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 禁用扫描模式(即单通道模式)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 禁用连续转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 触发方式(这里使用定时器)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_NbrOfConversion = 1; // 只进行一次转换
ADC_Init(ADC1, &ADC_InitStructure); // 应用设置到 ADC1
ADC_Resolution_12b
:设置 ADC 的分辨率为 12 位,表示返回的数字值范围是 0~4095。ADC_ScanConvMode
:禁用扫描模式,表示只有一个通道被采样。ADC_ContinuousConvMode
:禁用连续转换模式,转换完成后不会自动重新启动。ADC_ExternalTrigConv
:配置转换的触发源,可以选择外部事件触发。ADC_DataAlign_Right
:配置数据右对齐,确保数据对齐方式符合标准。ADC_NbrOfConversion
:配置进行的转换次数,这里设置为 1,表示只有一个通道的转换。
4. 启动 ADC 转换
完成配置后,下一步是启用 ADC 模块,并启动 ADC 转换。这里使用的是软件启动方式。
ADC_Cmd(ADC1, ENABLE); // 启用 ADC1 外设
ADC_SoftwareStartConv(ADC1); // 软件触发开始 ADC 转换
ADC_Cmd(ADC1, ENABLE)
:启用 ADC1 外设,使 ADC 能够开始工作。ADC_SoftwareStartConv(ADC1)
:通过软件触发启动 ADC 转换。
5. 等待转换完成
转换一旦开始,ADC 会进行采样和转换操作,直到转换完成。在这期间,我们可以通过轮询 ADC_FLAG_EOC
标志位来判断转换是否完成。
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
ADC_FLAG_EOC
:表示 ADC 转换结束标志(End Of Conversion)。ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)
:检查是否设置了转换结束标志。如果没有,继续等待。
6. 读取 ADC 转换结果
一旦 ADC 转换完成,我们可以读取转换结果。转换结果存储在 ADC 数据寄存器中。由于 STM32 使用右对齐数据,我们只需要读取 12 位数据。
uint16_t adcValue = ADC_GetConversionValue(ADC1); // 获取 ADC 转换结果
ADC_GetConversionValue(ADC1)
:获取 ADC1 转换结果,该值范围是 0~4095。
1. 启用 ADC 时钟
在 STM32 中,外设(如 ADC)需要时钟才能正常工作。因此,第一步是启用 ADC 所需的时钟。通过函数 RCC_APB2PeriphClockCmd
来使能 ADC 时钟。
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2Periph_ADC1
:表示启用 ADC1 时钟。ENABLE
:表示使能时钟。
2. 配置 GPIO 引脚为模拟模式
为了将模拟信号传输到 ADC,我们需要配置相应的 GPIO 引脚为模拟输入模式。在本例中,我们选择了 PA0
引脚来连接模拟信号。
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0; // 配置 PA0 引脚
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入模式
GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_Pin_0
:表示 PA0 引脚。GPIO_Mode_AIN
:将引脚设置为模拟输入模式。
3. 配置 ADC 参数
在这一步,我们需要配置 ADC 的工作模式、精度、采样时间等参数。这些配置决定了 ADC 如何执行转换。
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 设置分辨率为 12 位
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 禁用扫描模式(即单通道模式)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 禁用连续转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 触发方式(这里使用定时器)
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐
ADC_InitStructure.ADC_NbrOfConversion = 1; // 只进行一次转换
ADC_Init(ADC1, &ADC_InitStructure); // 应用设置到 ADC1
ADC_Resolution_12b
:设置 ADC 的分辨率为 12 位,表示返回的数字值范围是 0~4095。ADC_ScanConvMode
:禁用扫描模式,表示只有一个通道被采样。ADC_ContinuousConvMode
:禁用连续转换模式,转换完成后不会自动重新启动。ADC_ExternalTrigConv
:配置转换的触发源,可以选择外部事件触发。ADC_DataAlign_Right
:配置数据右对齐,确保数据对齐方式符合标准。ADC_NbrOfConversion
:配置进行的转换次数,这里设置为 1,表示只有一个通道的转换。
4. 启动 ADC 转换
完成配置后,下一步是启用 ADC 模块,并启动 ADC 转换。这里使用的是软件启动方式。
ADC_Cmd(ADC1, ENABLE); // 启用 ADC1 外设
ADC_SoftwareStartConv(ADC1); // 软件触发开始 ADC 转换
ADC_Cmd(ADC1, ENABLE)
:启用 ADC1 外设,使 ADC 能够开始工作。ADC_SoftwareStartConv(ADC1)
:通过软件触发启动 ADC 转换。
5. 等待转换完成
转换一旦开始,ADC 会进行采样和转换操作,直到转换完成。在这期间,我们可以通过轮询 ADC_FLAG_EOC
标志位来判断转换是否完成。
while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
ADC_FLAG_EOC
:表示 ADC 转换结束标志(End Of Conversion)。ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)
:检查是否设置了转换结束标志。如果没有,继续等待。
6. 读取 ADC 转换结果
一旦 ADC 转换完成,我们可以读取转换结果。转换结果存储在 ADC 数据寄存器中。由于 STM32 使用右对齐数据,我们只需要读取 12 位数据。
uint16_t adcValue = ADC_GetConversionValue(ADC1); // 获取 ADC 转换结果
ADC_GetConversionValue(ADC1)
:获取 ADC1 转换结果,该值范围是 0~4095。
7. 清除 ADC 标志
如果 ADC 转换完成,我们需要清除 ADC 的标志位,以便进行下一次转换。可以使用 ADC_ClearFlag
函数来清除 EOC
标志。
ADC_ClearFlag(ADC1, ADC_FLAG_EOC); // 清除 EOC 标志
```7. 清除 ADC 标志
如果 ADC 转换完成,我们需要清除 ADC 的标志位,以便进行下一次转换。可以使用 `ADC_ClearFlag` 函数来清除 `EOC` 标志。
```c
ADC_ClearFlag(ADC1, ADC_FLAG_EOC); // 清除 EOC 标志
模式区别
- 单通道模式:适用于只需要从一个输入通道读取数据的情况,简单且高效。
- 多通道模式:适用于同时从多个输入通道读取数据的情况,常用于需要对多个传感器进行采样的应用。
配置步骤
- 时钟配置:启用 ADC 外设的时钟。
- GPIO 配置:配置对应的模拟输入引脚(如 PA0、PA1 等)为模拟模式。
- ADC 配置:
- 配置 ADC 的分辨率、采样时间等参数。
- 在多通道模式下,配置通道顺序和扫描模式。
- 启动转换:通过软件或外部触发启动 ADC 转换。
- 读取结果:转换完成后读取结果。
示例:单通道采样代码(以 PA0 为例)
// 启用 ADC 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
// 配置 GPIO 为模拟输入模式(PA0)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置 ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b; // 12 位分辨率
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 非连续转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1; // 外部触发方式
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据对齐方式
ADC_InitStructure.ADC_NbrOfConversion = 1; // 单通道
ADC_Init(ADC1, &ADC_InitStructure);
// 启动 ADC
ADC_Cmd(ADC1, ENABLE);
// 等待 ADC 转换完成
ADC_SoftwareStartConv(ADC1);
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
uint16_t adcValue = ADC_GetConversionValue(ADC1); // 读取转换结果
示例:多通道采样代码(PA0 和 PA1)
// 启用 ADC 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
// 配置 GPIO 为模拟输入模式(PA0 和 PA1)
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 配置 ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Resolution = ADC_Resolution_12b;
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 启用扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T1_CC1;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_NbrOfConversion = 2; // 配置两个通道
ADC_Init(ADC1, &ADC_InitStructure);
// 配置 ADC 通道顺序
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_1Cycles5); // 配置 PA0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_1Cycles5); // 配置 PA1
// 启动 ADC
ADC_Cmd(ADC1, ENABLE);
// 启动转换
ADC_SoftwareStartConv(ADC1);
// 等待转换完成
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
uint16_t adcValue0 = ADC_GetConversionValue(ADC1); // 读取 PA0 的结果
ADC_ClearFlag(ADC1, ADC_FLAG_EOC); // 清除 EOC 标志
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待 PA1 转换完成
uint16_t adcValue1 = ADC_GetConversionValue(ADC1); // 读取 PA1 的结果
IIC
IIC(I²C)通信详解
IIC(Inter-Integrated Circuit,简称 I²C)是一种常用的多主从双向串行总线协议,适用于短距离设备间通信。STM32F103C8T6 提供硬件 I²C 接口,支持 I²C 协议,具有高效稳定的通信性能。
基本概念
-
主设备与从设备
- 主设备负责控制通信过程(如生成时钟信号、启动信号等)。
- 从设备根据主设备的请求响应数据传输。
-
I²C 总线协议
- 时钟线 (SCL):由主设备生成时钟信号。
- 数据线 (SDA):主从设备之间传输数据。
- 两根线均为开放集电极输出,需外接上拉电阻。
-
通信过程
- 起始信号:SDA 从高电平跳变到低电平时,SCL 线保持高电平。
- 停止信号:SDA 从低电平跳变到高电平时,SCL 线保持高电平。
- 数据传输:数据以 8 位为单位传输,从高位到低位,传输后从设备需发送一个 ACK(应答)信号。
- 应答信号 (ACK):表示从设备成功接收数据。
- 不应答信号 (NACK):表示从设备未成功接收数据。
STM32F103C8T6 I²C 使用步骤
1. 硬件连接
将 STM32F103C8T6 的 I²C 引脚(SCL 和 SDA)连接到从设备,同时通过上拉电阻(通常 4.7kΩ)接至电源。以 I²C1 为例:
- SCL:PB6
- SDA:PB7
2. 初始化 GPIO
将 SCL 和 SDA 配置为复用功能开漏输出模式。
void I2C_GPIO_Config(void) {
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // 开启 GPIOB 时钟
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6 | GPIO_Pin_7; // SCL 和 SDA
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_OD; // 复用开漏输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure); // 初始化 GPIO
}
3. 初始化 I²C
配置 STM32 的 I²C 外设参数,包括时钟频率、工作模式等。
void I2C_Config(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); // 开启 I²C1 时钟
I2C_InitTypeDef I2C_InitStructure;
I2C_InitStructure.I2C_ClockSpeed = 100000; // 设置 I²C 时钟频率为 100kHz
I2C_InitStructure.I2C_Mode = I2C_Mode_I2C; // I²C 模式
I2C_InitStructure.I2C_DutyCycle = I2C_DutyCycle_2; // 占空比 2:1
I2C_InitStructure.I2C_OwnAddress1 = 0x00; // 主设备无需设置地址
I2C_InitStructure.I2C_Ack = I2C_Ack_Enable; // 开启应答
I2C_InitStructure.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit; // 7 位地址模式
I2C_Init(I2C1, &I2C_InitStructure); // 初始化 I²C1
I2C_Cmd(I2C1, ENABLE); // 使能 I²C1
}
4. I²C 数据传输
- 起始信号
void I2C_Start(I2C_TypeDef *I2Cx) {
I2C_GenerateSTART(I2Cx, ENABLE); // 生成起始信号
while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_MODE_SELECT)); // 等待起始信号完成
}
- 发送从设备地址
void I2C_SendAddress(I2C_TypeDef *I2Cx, uint8_t Address, uint8_t Direction) {
I2C_Send7bitAddress(I2Cx, Address, Direction); // 发送从设备地址
if (Direction == I2C_Direction_Transmitter) { // 如果是写模式
while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));
} else { // 如果是读模式
while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));
}
}
- 发送数据
void I2C_WriteData(I2C_TypeDef *I2Cx, uint8_t Data) {
I2C_SendData(I2Cx, Data); // 发送数据
while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); // 等待发送完成
}
- 接收数据
uint8_t I2C_ReadData(I2C_TypeDef *I2Cx) {
while (!I2C_CheckEvent(I2Cx, I2C_EVENT_MASTER_BYTE_RECEIVED)); // 等待接收完成
return I2C_ReceiveData(I2Cx); // 返回接收到的数据
}
- 停止信号
void I2C_Stop(I2C_TypeDef *I2Cx) {
I2C_GenerateSTOP(I2Cx, ENABLE); // 生成停止信号
}
5. I²C 通信示例
以主设备向从设备写入数据为例。
void I2C_WriteExample(void) {
uint8_t slaveAddress = 0x50; // 从设备地址
uint8_t data = 0xA5; // 需要发送的数据
I2C_Start(I2C1); // 生成起始信号
I2C_SendAddress(I2C1, slaveAddress, I2C_Direction_Transmitter); // 发送从设备地址
I2C_WriteData(I2C1, data); // 发送数据
I2C_Stop(I2C1); // 生成停止信号
}
以读取从设备数据为例:
uint8_t I2C_ReadExample(void) {
uint8_t slaveAddress = 0x50; // 从设备地址
uint8_t receivedData = 0;
I2C_Start(I2C1); // 生成起始信号
I2C_SendAddress(I2C1, slaveAddress, I2C_Direction_Receiver); // 发送从设备地址
receivedData = I2C_ReadData(I2C1); // 读取数据
I2C_Stop(I2C1); // 生成停止信号
return receivedData;
}
6. 调试和验证
-
逻辑分析仪
使用逻辑分析仪监控 I²C 总线上的 SCL 和 SDA 信号,验证通信协议是否正确。 -
从设备响应
确保从设备地址正确,且从设备能够正常应答主设备。 -
错误处理
- 检查上拉电阻是否连接。
- 检查 GPIO 模式是否设置为开漏。
- 检查时钟频率是否匹配从设备支持的速率。
I²C 优缺点
-
优点
- 使用引脚少,仅需两根信号线(SCL 和 SDA)。
- 支持多主从设备连接。
-
缺点
- 通信速度较低(标准模式为 100kHz,快速模式为 400kHz)。
- 总线负载较高时,信号完整性可能受影响。
最近烦躁的异常,完全学不下去.心里讨厌的要死,感觉自己快25岁了,干什么都已经晚了,论文,STM32,项目,腰突,找工作,党务,全都来不及了.心里急得要死.不过就这样吧. 一点点努力,能咋样咋样吧. 功力必不唐捐…先把江科协的32教程学完…
感觉一个很重要的情况是,还是得慢慢来,先来个最简单的,先把视频看完再说,二倍速,三四天就看完了.干什么想那么多呢,把视频看完一遍就懂了一点呢,再学一遍又懂了一点,先学一点再说,不要完美注意.