嘉立创筑基派FreeRTOS编码器+PWM+LED | Notes / STM32 | 氵工的博客

嘉立创筑基派FreeRTOS编码器+PWM+LED

发表于 2026-04-02 22:48 3021 字 16 min read

This post is not yet available in English. Showing the original.
嘉立创筑基派FreeRTOS,使用编码器以及LED,使用筑基板自带PB8 LED,EC11编码器

EC11 编码器控制 LED 亮度项目

1. 项目概述

本项目基于 STM32F407 微控制器和 FreeRTOS 操作系统,实现了使用 EC11 编码器控制 LED 亮度的功能。项目通过三个任务(线程)协同工作,实现了无阻塞的编码器读取、按键检测和 LED 亮度控制。

2. 技术框架

2.1 硬件平台

  • 微控制器:STM32F407
  • 编码器:EC11 旋转编码器(连接到 PD12 和 PD13)
  • 按键:PC13(编码器自带的按下功能)
  • LED:PB8(通过 PWM 控制亮度)
  • 定时器:TIM10(用于 PWM 输出)

2.2 软件架构

  • 开发环境:STM32CubeMX + VScode
  • 操作系统:FreeRTOS
  • 库函数:STM32 HAL 库
  • 代码结构:标准 STM32 HAL 项目结构 + FreeRTOS 任务管理

3. 代码结构分析

3.1 头文件和包含部分

#include "main.h"
#include "cmsis_os.h"

包含了主头文件和 CMSIS-OS 头文件,其中定义了 STM32 HAL 库的基本配置和 FreeRTOS 的相关函数。

3.2 全局变量定义

/* USER CODE BEGIN PV */
uint8_t led_enabled = 1;
uint16_t pwm_duty = 0;
const uint16_t pwm_arr = 65535;
const uint16_t pwm_step = 1000;
/* USER CODE END PV */
  • led_enabled:LED 使能状态(1=开启,0=关闭)
  • pwm_duty:PWM 占空比(0-65535)
  • pwm_arr:PWM 自动重载值(定时器周期)
  • pwm_step:每次旋转编码器的 PWM 变化步长

3.3 任务句柄定义

/* USER CODE BEGIN EV */
osThreadId_t EncoderTaskHandle;
osThreadId_t KEYTaskHandle;
osThreadId_t LEDTaskHandle;
/* USER CODE END EV */

定义了三个任务的句柄,用于任务管理。

3.4 任务函数原型

/* USER CODE BEGIN FunctionPrototypes */
void EncoderTask(void *argument);
void KEYTask(void *argument);
void LEDTask(void *argument);
/* USER CODE END FunctionPrototypes */

3.5 编码器状态表

const int8_t enc_table[16] = {
    0, -1,  1,  0,
    1,  0,  0, -1,
   -1,  0,  0,  1,
    0,  1, -1,  0
};

编码器旋转方向检测的状态表,用于查表法计算旋转方向。

4. 主要功能实现

4.1 主函数

int main(void)
{
  /* 系统初始化 */
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_TIM10_Init();
  MX_FREERTOS_Init();

  /* 启动PWM输出 */
  if (HAL_TIM_PWM_Start(&htim10, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }

  /* 启动任务调度器 */
  osKernelStart();

  /* 永远不会执行到这里 */
  while (1)
  {
  }
}

主函数首先进行系统初始化,然后配置 GPIO、TIM10 定时器和 FreeRTOS,启动 PWM 输出和任务调度器。

4.2 任务初始化

void MX_FREERTOS_Init(void)
{
  /* 创建任务 */
  EncoderTaskHandle = osThreadNew(EncoderTask, NULL, &EncoderTask_attributes);
  KEYTaskHandle = osThreadNew(KEYTask, NULL, &KEYTask_attributes);
  LEDTaskHandle = osThreadNew(LEDTask, NULL, &LEDTask_attributes);
}

使用osThreadNew函数创建三个任务,并设置任务属性。

4.3 Encoder 任务

void EncoderTask(void *argument)
{
  static uint8_t enc_last = 0;
  uint8_t enc_current;
  int8_t enc_dir;

  for (;;)
  {
    /* 读取编码器状态 */
    enc_current = ((GPIOE->IDR & GPIO_PIN_12) >> 12) | ((GPIOE->IDR & GPIO_PIN_13) >> 12);
    
    /* 计算旋转方向 */
    enc_dir = enc_table[(enc_last << 2) | enc_current];
    enc_last = enc_current;
    
    /* 根据方向调整PWM占空比 */
    if (enc_dir > 0 && pwm_duty < pwm_arr)
    {
      pwm_duty += pwm_step;
      if (pwm_duty > pwm_arr) pwm_duty = pwm_arr;
    }
    else if (enc_dir < 0 && pwm_duty > 0)
    {
      pwm_duty -= pwm_step;
      if (pwm_duty > pwm_arr) pwm_duty = 0; // 防止溢出
    }
    
    osDelay(10);
  }
}

Encoder 任务使用查表法检测编码器旋转方向,并根据方向调整 PWM 占空比。

4.4 KEY 任务

void KEYTask(void *argument)
{
  static uint8_t key_state = 0;
  static uint32_t key_time = 0;

  for (;;)
  {
    /* 按键防抖处理 */
    if (HAL_GPIO_ReadPin(GPIOC, GPIO_PIN_13) == GPIO_PIN_RESET)
    {
      if (key_state == 0)
      {
        key_state = 1;
        key_time = osKernelGetTickCount();
      }
      else if (key_state == 1)
      {
        if (osKernelGetTickCount() - key_time > 50)
        {
          key_state = 2;
          /* 切换LED使能状态 */
          led_enabled = !led_enabled;
        }
      }
    }
    else
    {
      key_state = 0;
    }
    
    osDelay(10);
  }
}

KEY 任务实现了按键的防抖处理,当检测到按键按下时,切换 LED 的使能状态。

4.5 LED 任务

void LEDTask(void *argument)
{
  for (;;)
  {
    /* 根据PWM占空比和LED使能状态控制LED亮度 */
    uint16_t out = led_enabled ? (uint16_t)(pwm_arr - pwm_duty) : pwm_arr;
    __HAL_TIM_SET_COMPARE(&htim10, TIM_CHANNEL_1, out);
    
    osDelay(50);
  }
}

LED 任务通过 TIM10 的 PWM 输出控制 LED 亮度。由于 PB8 是低电平有效,所以需要将 PWM 占空比取反。

4.6 TIM10 定时器初始化

static void MX_TIM10_Init(void)
{
  TIM_OC_InitTypeDef sConfigOC = {0};

  htim10.Instance = TIM10;
  htim10.Init.Prescaler = 0;
  htim10.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim10.Init.Period = 65535;
  htim10.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim10.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim10) != HAL_OK)
  {
    Error_Handler();
  }
  if (HAL_TIM_PWM_Init(&htim10) != HAL_OK)
  {
    Error_Handler();
  }
  sConfigOC.OCMode = TIM_OCMODE_PWM1;
  sConfigOC.Pulse = 0;
  sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
  sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
  if (HAL_TIM_PWM_ConfigChannel(&htim10, &sConfigOC, TIM_CHANNEL_1) != HAL_OK)
  {
    Error_Handler();
  }
  HAL_TIM_MspPostInit(&htim10);
}

TIM10 定时器初始化函数配置了定时器的基本参数和 PWM 输出通道。

4.7 GPIO 初始化

static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  /* GPIO Ports Clock Enable */
  __HAL_RCC_GPIOC_CLK_ENABLE();
  __HAL_RCC_GPIOD_CLK_ENABLE();
  __HAL_RCC_GPIOB_CLK_ENABLE();

  /* 配置PC13为输入 */
  GPIO_InitStruct.Pin = GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);

  /* 配置PD12和PD13为输入 */
  GPIO_InitStruct.Pin = GPIO_PIN_12|GPIO_PIN_13;
  GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
  GPIO_InitStruct.Pull = GPIO_PULLUP;
  HAL_GPIO_Init(GPIOD, &GPIO_InitStruct);
}

GPIO 初始化函数配置了按键和编码器的输入引脚。

5. 技术细节

5.1 编码器工作原理

EC11 编码器是一种旋转式位置传感器,通过两个输出信号(A 相和 B 相)的相位差来检测旋转方向。当编码器顺时针旋转时,A 相领先 B 相;当编码器逆时针旋转时,B 相领先 A 相。

本项目使用查表法检测旋转方向,通过读取 A 相和 B 相的状态,计算出当前状态码,然后根据上一次的状态码和当前状态码查表得到旋转方向。

5.2 PWM 控制原理

PWM(脉冲宽度调制)是一种通过改变脉冲宽度来控制信号的技术。在 LED 控制中,PWM 的占空比(高电平时间与整个周期的比值)决定了 LED 的平均亮度。占空比越高,LED 越亮;占空比越低,LED 越暗。

本项目使用 TIM10 定时器生成 PWM 信号,定时器时钟为 84MHz(APB2 总线时钟),预分频器为 0,自动重装载值为 65535,因此 PWM 频率为:

PWM频率 = 定时器时钟 / (预分频器+1) / (自动重装载值+1) = 84MHz / 1 / 65536 ≈ 1281Hz

5.3 FreeRTOS 任务管理

FreeRTOS 是一个开源的实时操作系统,提供了任务管理、队列、信号量等功能。本项目使用 FreeRTOS 的任务管理功能,创建了三个任务:

  • Encoder 任务:优先级正常,负责读取编码器状态并计算旋转方向
  • KEY 任务:优先级低,负责检测按键状态
  • LED 任务:优先级低,负责控制 LED 亮度

任务调度器根据任务优先级和状态,决定哪个任务获得 CPU 控制权。

5.4 按键防抖处理

按键在按下和释放时会产生机械抖动,导致多次触发。本项目实现了软件防抖,通过检测按键状态的持续时间来判断按键是否真正被按下。当按键状态持续 50ms 以上时,才认为按键被按下。

5.5 任务间通信

当前项目使用全局变量进行任务间通信,Encoder 任务更新pwm_duty变量,KEY 任务更新led_enabled变量,LED 任务读取这些变量并控制 LED 亮度。

6. 工作流程

  1. 系统初始化

    • 配置系统时钟
    • 初始化 GPIO
    • 初始化 TIM10 定时器
    • 初始化 FreeRTOS
  2. 任务创建

    • 创建 Encoder 任务
    • 创建 KEY 任务
    • 创建 LED 任务
  3. 任务执行

    • Encoder 任务
      • 读取编码器状态
      • 计算旋转方向
      • 根据方向调整 PWM 占空比
    • KEY 任务
      • 检测按键状态(带防抖)
      • 当按键按下时,切换 LED 使能状态
    • LED 任务
      • 根据 PWM 占空比和 LED 使能状态控制 LED 亮度

7. 代码优化建议

7.1 使用队列替代全局变量

当前代码使用全局变量传递数据,建议使用 FreeRTOS 的队列进行任务间通信,提高代码的可靠性和可维护性。例如:

// 创建队列
osMessageQueueId_t pwmQueue = osMessageQueueNew(1, sizeof(uint16_t), NULL);

// 发送消息
osMessageQueuePut(pwmQueue, &pwm_duty, 0, 0);

// 接收消息
osMessageQueueGet(pwmQueue, &pwm_duty, NULL, 0);

7.2 增加参数配置

可以将 PWM 的步长、按键防抖时间等参数定义为可配置的宏,方便调整:

#define PWM_STEP        1000    // PWM变化步长
#define KEY_DEBOUNCE_MS 50      // 按键防抖时间
#define ENCODER_DELAY   10      // 编码器检测延迟
#define LED_UPDATE_DELAY 50     // LED更新延迟

7.3 增加错误处理

在关键操作处增加错误处理,提高系统的稳定性:

if (HAL_TIM_PWM_Start(&htim10, TIM_CHANNEL_1) != HAL_OK)
{
  Error_Handler();
}

7.4 优化任务优先级

根据实际需求调整任务优先级,确保关键任务能够及时响应:

  • Encoder 任务:优先级较高,确保及时响应编码器输入
  • KEY 任务:优先级中等
  • LED 任务:优先级较低,因为亮度更新不需要实时响应

7.5 增加状态指示

可以增加 LED 状态指示,例如当 LED 开启时,使用另一个 LED 指示状态:

if (led_enabled)
{
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_SET);
}
else
{
  HAL_GPIO_WritePin(GPIOB, GPIO_PIN_9, GPIO_PIN_RESET);
}

8. 测试方法

  1. 基本功能测试

    • 旋转编码器,观察 LED 亮度变化
    • 按下编码器的按键,观察 LED 的开关状态
  2. 性能测试

    • 快速旋转编码器,测试系统的响应速度
    • 多次按下按键,测试防抖功能
  3. 边界测试

    • 旋转编码器到最小亮度,测试是否能正确停止
    • 旋转编码器到最大亮度,测试是否能正确停止
  4. 稳定性测试

    • 长时间运行,测试系统的稳定性
    • 重复操作,测试系统的可靠性

9. 项目结构

FreeRTOS\F02_Encoder_PWM_LED_1\
├── Core\
│   ├── Inc\
│   │   ├── main.h
│   │   └── stm32f4xx_hal_conf.h
│   └── Src\
│       ├── main.c
│       ├── stm32f4xx_hal_msp.c
│       ├── stm32f4xx_it.c
│       └── sysmem.c
├── Drivers\
│   ├── CMSIS\
│   │   ├── Device\
│   │   └── Include\
│   └── STM32F4xx_HAL_Driver\
│       ├── Inc\
│       └── Src\
├── FreeRTOS\
│   ├── CMSIS_RTOS_V2\
│   └── Source\
└── README.md

10. 技术要点

  1. FreeRTOS 任务创建与管理:使用osThreadNew创建任务,设置任务优先级和栈大小。

  2. 编码器读取算法:使用查表法实现编码器旋转方向的检测,提高检测速度和准确性。

  3. PWM 控制:使用 TIM10 的 PWM 模式控制 LED 亮度,实现平滑的亮度调节。

  4. 按键防抖:实现软件防抖,提高按键检测的可靠性。

  5. 任务间通信:当前使用全局变量,建议使用队列进行任务间通信。

  6. STM32 HAL 库使用:使用 HAL 库函数配置 GPIO、定时器等外设。

  7. 代码规范:遵循 STM32 HAL 库的代码规范和注释风格。

11. 项目特点

  1. 模块化设计:将功能分为三个独立的任务,便于维护和扩展。

  2. 实时响应:使用 FreeRTOS 实现实时任务调度,确保编码器输入和按键操作能够及时响应。

  3. 平滑控制:通过 PWM 技术实现 LED 亮度的平滑调节,避免亮度突变。

  4. 可靠性:实现了按键防抖,提高了系统的可靠性。

  5. 可扩展性:代码结构清晰,便于添加新的功能或修改现有功能。

12. 应用场景

本项目实现的 EC11 编码器控制 LED 亮度技术可以应用于多种场景:

  1. 照明控制:调节室内灯光亮度
  2. 设备参数调节:调节设备的各种参数,如音量、速度等
  3. 仪表盘控制:控制仪表盘的显示亮度
  4. 智能家居:调节智能灯具的亮度
  5. 工业控制:调节设备的输出功率

13. 总结

本项目成功实现了使用 EC11 编码器控制 LED 亮度的功能,通过 FreeRTOS 的任务管理实现了无阻塞的操作。项目结构清晰,代码简洁,功能完整,可以作为学习 FreeRTOS 和 STM32 PWM 控制的参考示例。

通过本项目的学习,可以掌握以下技术:

  1. STM32 微控制器的 GPIO 和定时器配置
  2. FreeRTOS 任务创建和管理
  3. 编码器读取和旋转方向检测
  4. PWM 控制技术
  5. 按键防抖处理
  6. 任务间通信方法

这些技术在嵌入式系统开发中非常实用,可以应用于各种需要用户输入和输出控制的场景。