Funpack5-2 - 基于EV41C56A实现触摸输入与PWM控制LED
该项目使用了Microchip的EV41C56A开发板,实现了触摸输入与PWM控制LED平滑调光的设计,它的主要功能为:触摸短按控制LED开/关;触摸长按,连续调节亮度,并按照【亮度从“灭 → 最亮→ 灭”线性变化】的逻辑循环。
标签
Funpack活动
PWM
Microchip
触摸
MPLAB
EV41C56A
子午鼠
更新2026-06-17
7

一、项目介绍

1.1 项目背景

本项目是参加硬禾科技联合DigiKey发起的Funpack活动第五季第2期Funpack S5 #2。本期板卡是来自Microchip的EV41C56A(PIC32CM LS00 Curiosity Nano+ Touch Evaluation Kit),这是一款专为触摸应用设计的安全低功耗评估套件。

1.2 项目概述

本项目基于Microchip EV41C56A开发板,实现了触摸控制LED亮灭及调光功能。EV41C56A开发板,即PIC32CM LS00 Curiosity Nano+ Touch Evaluation Kit 是一款专为评估 PIC32CM5164LS00048 微控制器而设计的硬件平台。该套件基于安全且超低功耗的 ARM® Cortex®-M23 内核,集成了丰富的安全功能和触摸控制能力。这款评估套件将安全启动、TrustZone® 技术、加密加速器与增强型触摸控制完美融合,是开发安全触摸应用的理想选择。

_DSC5743.png

图:EV41C56A开发板特性

板卡核心特性

  • PIC32CM5164LS00048 ARM® Cortex®-M23 内核微控制器
  • 512KB Flash 存储器,超低功耗设计
  • 安全启动(Secure Boot)+ ARM TrustZone® 技术
  • 集成加密加速器,硬件安全保障
  • 增强型外设触摸控制器(PTC),支持电容式触摸
  • 智能模拟功能:运算放大器、ADC、DAC、模拟比较器
  • 板载调试器,支持 MPLAB X IDE 开发环境
  • 电压范围 1.7V-3.6V,MIC5353 稳压器支持最大 500mA
  • 兼容 Curiosity Nano Base for Click boards™ 扩展
  • 支持 mikroBUS™、Xplained Pro 接口扩展

本项目旨在实现触摸控制LED亮灭及调光功能,具体功能如下:

  • 通过PWM控制LED亮度
  • 触摸短按:LED开/关
  • 触摸长按:连续调节亮度,并按照【亮度从“灭 → 最亮”线性变化;当到达最亮后,保持长按可实现“最亮 → 灭”反向变化】的逻辑循环
  • 亮度变化过程应平滑(无明显闪烁或跳变)

1.4 项目成果

通过本项目的开发,成功实现了:

  • 了解EV41C56A开发板的硬件结构和特性
  • ✅ 基本掌握PIC32 MPLAB Harmony的系统开发
  • ✅ 实现基础任务:使用EV41C56A开发板板载触摸按键功能
  • ✅ 实现进阶任务:使用EV41C56A开发板板载触摸控制LED亮灭及调光功能

二、硬件介绍

2.1 微控制器:PIC32CM5164LS00048

基于 Arm® Cortex-M23® 处理器 PIC32CM LS00 系列设备将超低功耗、触控、安全性和智能模拟集成集成于一体。它们具备创新的低功耗技术,包括 SleepWalking Periphereals、Arm TrustZone、不可变安全启动、行业领先的耐水触控、运算放大器、模拟转数字转换器(ADC)和数字转模拟转换器(DAC)。

image.png

图:微控制器PIC32CM5164LS00048

安全特性:安全启动、ARM TrustZone®、加密加速器,为您的应用提供全面安全保障

触摸控制:增强型外设触摸控制器(PTC),支持按键、滑条、滚轮等多种触摸界面

超低功耗:SleepWalking 外设技术,实现极致低功耗运行,适合电池供电设备

2.2 Microchip EV41C56A开发板概述

PIC32CM LS00 Curiosity Nano+ Touch(EV41C56A)评估工具包是用于评估PIC32CM5164LS00048 MCU 的硬件平台。

image.png

图:PIC32CM LS00 Curiosity Nano+ Touch 评估工具包

名称

特性

PIC32CM5164LS00048 MCU

PIC32CM5164LS00048单片机

User LED

用户应用黄色LED

User Switch

用户应用机械按钮

Touch Button

用户应用触摸按钮

Device USB

用于器件控制的USB,可用于为板供电

Debug USB

用于调试器的USB,可用于为板供电,用于对板进行编程或调试

nEDBG Debugger

目标器件通过板上Nano 调试器进行编程和调试,无需外部编程器或调试工具

Power Status LED

绿色电源/状态LED

2.3 引脚排列

除USB 外设控制引脚PA24 和PA25 之外,大多数PIC32CM5164LS00048 I/O 引脚都可通过PIC32CM LS00 Curiosity Nano+ Touch 评估工具包上的边缘引脚访问。

image.png

图:PIC32CM LS00 Curiosity Nano+ Touch 引脚排列

本项目主要用到用户黄色LED和用户触摸按钮。PIC32CM LS00 Curiosity Nano+ Touch 评估工具包带有一个黄色LED和一个带驱动屏蔽层的QTouch®按钮。黄色LED对应PIC32CM5164LS00048的PA15引脚,触摸按钮对应PIC32CM5164LS00048的PA22引脚,后续需要在MPLAB中对引脚进行配置。

image.png

image.png

2.4 原理图

PIC32CM LS00 Curiosity Nano+ Touch 评估工具包原理图主要包括电源原理图、MCU原理图和调试器原理图。其中最应关注的是MCU原理图中的两个引脚PA15和PA22,分别对应黄色LED和触摸按钮。

image.png

图:电源原理图

image.png

图:MCU原理图

image.png

图:调试器原理图


三、方案框图和项目设计思路

3.1 基础任务方案框图

基础题目:

使用 EV41C56A 开发板板载触摸按键功能,实现以下逻辑:

  1. 单次触摸 → 点亮板载LED
  2. 再次触摸 → 关闭LED

功能要求:

  1. 正确初始化触摸通道
  2. LED 控制逻辑清晰,不出现误触发

根据以上基础任务要求,确定方案框图。

image.png

图:基础任务方案框图

3.2 基础任务项目设计思路

  • 硬件:使用板载触摸按钮(连接至 PA22)和黄色 LED(连接至 PA15)。PA15 配置为 GPIO 输出,低电平点亮 LED。
  • 软件核心
    • 初始化 GPIO(PA15 设为输出,初始高电平使 LED 灭)和触摸传感器(touch_init())。
    • 主循环周期性调用 touch_process() 获取触摸状态。
    • 引入 边沿检测 + 软件去抖 机制:
      • 记录上一次稳定触摸状态 last_touch
      • 读取当前原始触摸值 raw,与 last_touch 比较,若发生变化则启动去抖计数器 debounce_cnt(例如设为 5,对应约 5ms 去抖时间)。
      • 去抖计数器递减,当计数器归零且当前稳定状态为触摸按下(touched == 1)时,执行有效触摸动作:翻转 LED 状态(GPIO_PA15_Toggle())。
      • 无论是否产生动作,最终更新 last_touch 为当前稳定状态。
    • 去抖期间不会重复触发,有效滤除触摸信号的机械抖动和噪声。

3.3 进阶任务方案框图

在基础题之上,实现触摸长按调节LED 亮度功能

功能要求:

  1. 通过 PWM 控制 LED 亮度
  2. 短按:LED 开 / 关
  3. 长按:连续调节亮度,并按照【亮度从“灭 → 最亮”线性变化;当到达最亮后,保持长按可实现“最亮 → 灭”反向变化】的逻辑循环
  4. 亮度变化过程应平滑(无明显闪烁或跳变)

根据以上进阶任务要求,确定方案框图。

image.png

image.png

图:进阶任务方案框图

3.4 进阶任务项目设计思路

  • 硬件:PA15 复用为 TCC0 通道 1 的 PWM 输出,LED 仍为低电平点亮,软件输出取反。触摸按钮同基础任务。
  • 软件功能
    • 短按:带亮度记忆的开关(关灯记忆亮度,开灯恢复亮度)。
    • 长按:超过阈值(500ms)后进入调光模式,亮度以固定步进(10ms/步,步长 10 级)线性变化,到达边界(0 或周期值)自动反向,实现来回扫描。松手后退出调光模式,亮度保持。
    • 时间基准:利用主循环计数器 loop_cnt 模拟相对时间,不依赖硬件中断。
    • 状态机:区分普通模式与长按调光模式,在释放事件中判断短按或结束长按。
    • 极性处理:内部亮度值 brightness (0=灭,period=最亮),实际写入占空比 = period - brightness

四、调试软件介绍、软件流程图及关键代码

4.1 调试软件介绍

4.1.1 开发环境

由于之前已经安装过MPLAB X IDE v6.20,因此未下载最新版本。建议使用MPLAB X IDE最新版本,或者v6.25以上版本。

软件

版本

用途

MPLAB X IDE

v6.20

集成开发环境

MPLAB Code Configurator

v5.7.1

图形化代码配置插件

Harmony

v3.0

PIC32和SAM系列模块化软件框架

XC32

v5.10

Microchip 32位单片机的C/C++编译器

4.1.2 项目新建

先将EV41C56A开发板Debug USB端连接到电脑,然后打开MPLAB X IDE软件,Kit Window窗口能够显示开发板信息。同时Kit Window界面提供很多有关EV41C56A开发板的使用指南、示例程序等。

image.png

图:连接开发板

依次点击File→New Project,选择项目工程Microchip EmbeddedApplication Project (s),选择设备芯片PIC32CM5164LS00048,以及相应的开发板工具PIC32CM LS00 Curiosity Nano...,选择PIC32的编译器XC32,最后填写项目名称和路径。

image.png

图:新建项目

image.png

图:选择项目工程

image.png

图:选择设备芯片和工具

image.png

图:选择编译器

image.png

图:项目名称和路径

通常支持TrustZone 技术的 MCU(如 SAM L11, PIC32CM LS00 等)新建项目后,会自动生成包含“非安全工程(Non-Secure)”、“安全工程(Secure)”以及将它们组织在一起的“Group 工程”结构。

image.png

图:文件结构

4.1.3 基础任务工程项目配置

非安全工程下进入MCC,在Device Resources中将RTC、PTC、Touch Library三个组件添加到右侧Project Graph窗口中,一般会自动连线。

image.pngimage.png

图:添加组件

如果没有PTC、Touch Library组件,则应点击Device Resources旁的Content Manager,添加touch和touch_host_driver插件。

image.png

图:添加插件

组件添加好后,还需要对Clock时钟、Touch触摸、Pin引脚属性进行配置。

image.png

图:属性配置

Clock时钟属性界面可采用默认配置。

image.png

图:Clock时钟属性配置

Touch触摸属性界面进入后,Create标签添加1个触摸按键,Configure标签→Sensor Pins中,对Button 0的Y-Signals配置Y16(PA22)。

image.pngimage.png

图:Touch触摸属性配置

进入Pin引脚属性界面,配置PA15引脚为GPIO——Out,配置PA22引脚为PTC_X16/Y16——Analog。PA15作为数字量输出,控制LED;PA22作为触摸按键的模拟量输入。

image.png

图:Pin引脚属性配置

MCC配置完成后,点击Project Resources旁的Generate生成相应的项目代码。

image.png

图:生成配置代码

右键安全工程,Set as Main Proiect设置为主项目。在主项目的Source Files→main.c主程序中编写代码。

image.pngimage.png

图:main编写代码

4.1.4 进阶任务工程项目配置

进阶任务的工程项目MCC配置和基础任务主体流程类似,同样也是在非安全工程项目中配置MCC,在安全工程项目的main.c中编写程序。区别在于,进阶任务LED控制采用PWM方式。因此需要添加TCC0定时器组件,用于生成PWM信号。

image.png

点击System组件,在右侧的Configuration Options中打开Enable Secure SysTick,启动安全工程定时器,用于生成PWM信号。

image.png

此外,Pin引脚属性界面,配置PA15引脚为TCC0_WO5。

image.png

进阶任务的MCC配置完成后,同样点击Generate生成相应的项目代码。然后在安全工程项目的Source Files→main.c主程序中编写代码。

4.2 基础任务程序流程图

image.png

图:基础任务程序流程图

4.3 基础任务关键代码介绍

以下为基础任务主程序完整代码,并对代码进行详细解析。

//基础题目:【难度:⭐️】
//使用 EV41C56A 开发板板载触摸按键功能,实现以下逻辑:
//单次触摸 → 点亮板载LED
//再次触摸 → 关闭LED
//功能要求:
//正确初始化触摸通道
//LED 控制逻辑清晰,不出现误触发

uint8_t last_touch = 0;
uint8_t debounce_cnt = 0;

while (1) {
touch_process();
uint8_t touched = (get_sensor_state(0) & KEY_TOUCHED_MASK) ? 1 : 0;

if (touched != last_touch) {
debounce_cnt = 5; // 简单软件去抖计数
}

if (debounce_cnt > 0) {
debounce_cnt--;
if (debounce_cnt == 0 && touched == 1) {
GPIO_PA15_Toggle(); // 确认稳定触摸后翻转一次
}
}
last_touch = touched;
}

4.3.1 静态/局部变量定义(循环外部)

uint8_t last_touch = 0;
uint8_t debounce_cnt = 0;
  • last_touch:记录上一次循环采样得到的触摸状态(0 或 1),用于检测状态变化(边沿)。
  • debounce_cnt:去抖计数器,非零时表示正在去抖等待期间。

这两个变量必须定义在 while(1) 之外,否则每次循环都会重新初始化为 0,失去记忆作用。

4.3.2 触摸处理

touch_process();
  • 必须周期调用,更新内部触摸传感器的状态。具体实现取决于触摸库。

4.3.3 读取当前触摸状态(转换为布尔值)

uint8_t touched = (get_sensor_state(0) & KEY_TOUCHED_MASK) ? 1 : 0;
  • get_sensor_state(0) 返回传感器 0 的状态字,可能包含多个标志位(例如触摸、接近、压力等)。
  • & KEY_TOUCHED_MASK 只保留触摸标志位,其他位清零。例如 KEY_TOUCHED_MASK  0x01
  • 三元运算符 ? 1 : 0 将结果归一化为 0  1(尽管掩码后本身可能就是 0/1,但写得更明确)。

4.3.4 检测状态变化(开始去抖)

if (touched != last_touch) {
debounce_cnt = 5;
}
  • 如果当前读取的触摸状态与上一次记录的状态 不同,说明可能发生了按下或释放动作。
  • 此时不是立即响应,而是设置去抖计数器为 5(数值可根据实际情况调整,通常对应 5~20ms 的延时)。这个计数器将在后续循环中递减。

为什么不等稳定后再判断?
因为物理按键或触摸信号在边沿附近会有几次快速跳变。直接根据 touched != last_touch 翻转 LED 仍然会响应每次毛刺。计数器让系统等待一段时间,看状态是否真正稳定。

4.3.5 去抖计数与动作执行

if (debounce_cnt > 0) {
debounce_cnt--;
if (debounce_cnt == 0 && touched == 1) {
GPIO_PA15_Toggle();
}
}
  • 只要 debounce_cnt > 0,说明正处于去抖期间。每个循环周期:
    • 将计数器减 1。
    • 当计数器减到 0 时,表示去抖延时结束,此时再检查 最终稳定状态 touched 是否为 1(即触摸有效)。
    • 若稳定后确实是触摸按下,则执行一次 LED 翻转。

关键点

  • 去抖期间不会执行翻转,避免抖动引起的多次误触发。
  • 只有在去抖结束后且状态为按下,才真正动作。释放动作(touched == 0)通常不触发动作(除非你想让释放也触发,但一般不需要)。
  • 由于 debounce_cnt 只在状态变化时重置为 5,在抖动过程中(状态反复变化)每次变化都会重新重置计数器,因此只有状态保持稳定超过 5 个循环周期后,才会执行动作。这就实现了低通滤波。

4.3.6 更新上次状态

last_touch = touched;
  • 将本次读取的状态保存,供下次循环比较。注意无论是否去抖中,都应更新 last_touch,否则无法检测下一次变化。

4.4 进阶任务程序流程图

4.4.1 PWM_Init()程序流程图

PWM_Init()程序的功能主要是初始化并启动定时器 TCC0 的 PWM 输出,并获取 PWM 的周期值(最大占空比)。

主程序调用 TCC0_PWMInitialize()  TCC0_PWMStart() 完成硬件配置与启动。通过 TCC0_PWM24bitPeriodGet() 读取 PWM 周期,存入全局变量 pwm_period,后续所有亮度计算都以该值为基准。该函数仅在程序开始时执行一次。

image.png

图:进阶任务PWM_Init()程序流程图

4.4.2 SetPWMDuty(brightness)程序流程图

SetPWMDuty(brightness)程序的功能是根据亮度值换算并输出 PWM 信号,驱动 LED。

由于硬件采用低电平点亮 LED,因此实际占空比 = pwm_period - brightness_val增加了溢出保护:若计算结果大于 pwm_period,则将占空比限制为 0(即完全熄灭)。最后调用 TCC0_PWM24bitDutySet() 将换算后的占空比写入对应通道(CH1)。

image.png

图:进阶任务SetPWMDuty(brightness)序流程图

4.4.3 UpdateLED()程序流程图

UpdateLED()程序的功能是统一管理LED的亮灭状态,避免直接操作PWM导致逻辑混乱。

全局标志 led_on进行判断,若为 true,则调用 SetPWMDuty(brightness) 按当前亮度点亮 LED;若为 false,则调用 SetPWMDuty(0) 强制输出熄灭(此时 brightness 值无实际意义)。所有需要改变 LED 显示的地方(如开关灯、调光步进)都通过该函数实现,保证了输出的一致性。

image.png

图:进阶任务UpdateLED()程序流程图

4.4.4 进阶任务主程序流程图

主程序在完成触摸和PWM等外设初始化后,进入无限循环。每个循环周期都会读取触摸状态并与上一次状态比较,捕捉上升沿和下降沿。上升沿记录按下的起始计数值,下降沿则根据按住时长区分短按与长按:若按住时间低于阈值则为短按,此时翻转LED的开/关状态——关闭时保存当前亮度,开启时恢复上次记忆亮度,实现了开关不丢亮度的功能。

当按键持续按下且按住时长超过设定的长按阈值时,程序进入长按调光模式。若此时LED处于关闭状态,会先自动点亮并恢复记忆亮度;然后根据当前亮度自动确定调光方向(已达到最亮则转为变暗,否则变亮)。此后每隔固定循环次数,亮度值按照设定步长逐步增减,同时实时更新PWM输出,使LED平滑变化;一旦亮度触及最大值或最小值,方向自动反转,形成“灭→最亮→灭”的循环调光。松开按键后,长按模式退出,亮度停留在当前值。

整个逻辑依靠自由运行的loop_counter实现相对计时,所有亮度输出最终都通过UpdateLED函数统一控制,确保硬件输出与软件状态一致。这样既实现了直观的触摸交互,又避免了复杂的定时器占用。

image.png

图:进阶任务程序流程图

4.5 基础任务关键代码介绍

以下为基础任务主程序完整代码,并对代码进行详细解析。

/*******************************************************************************
Main Source File

Company:
Microchip Technology Inc.

File Name:
main.c

Summary:
This file contains the "main" function for a project.

Description:
This file contains the "main" function for a project. The
"main" function calls the "SYS_Initialize" function to initialize the state
machines of all modules in the system
*******************************************************************************/

//进阶题目:
//在基础题之上,实现触摸长按调节LED 亮度功能
//功能要求:
//通过 PWM 控制 LED 亮度
//短按:LED 开 / 关
//长按:连续调节亮度,并按照【亮度从“灭 → 最亮”线性变化;当到达最亮后,保持长按可实现“最亮 → 灭”反向变化】的逻辑循环
//亮度变化过程应平滑(无明显闪烁或跳变)

// *****************************************************************************
// Section: Included Files
// *****************************************************************************
#include <stddef.h> // Defines NULL
#include <stdbool.h> // Defines true
#include <stdlib.h> // Defines EXIT_FAILURE
#include "definitions.h" // SYS function prototypes

/* typedef for non-secure callback functions */
typedef void (*funcptr_void) (void) __attribute__((cmse_nonsecure_call));

// *****************************************************************************
// 用户配置常量(长按调光参数)
// *****************************************************************************
#define LONG_PRESS_CYCLES 20000 // 长按判定循环次数(约500ms,根据实际主循环速度调整)
#define PWM_STEP_CYCLES 500 // 亮度步进间隔循环次数(约10ms)
#define PWM_STEP 5 // 每次步进亮度变化量(0 ~ period)

// *****************************************************************************
// 全局变量
// *****************************************************************************
static uint32_t loop_counter = 0; // 主循环计数器(代替时间基准)
static bool led_on = false; // LED 开关状态
static uint32_t pwm_period; // PWM 周期值(最大占空比)
static uint32_t brightness = 0; // 当前亮度值(0=灭,period=最亮)
static uint32_t last_brightness = 0; // 关灯时记忆的亮度

// 长按调光状态
static uint32_t press_start_loop = 0;
static bool long_press_active = false;
static int8_t dim_dir = 1; // 1=变亮, -1=变暗
static uint32_t next_step_loop = 0;

// *****************************************************************************
// 初始化 PWM
// *****************************************************************************
static void PWM_Init(void)
{
TCC0_PWMInitialize();
TCC0_PWMStart();
pwm_period = TCC0_PWM24bitPeriodGet();
}

// *****************************************************************************
// 实际设置 PWM 输出(根据 LED 极性取反)
// 硬件:低电平点亮,因此实际占空比 = period - brightness
// *****************************************************************************
static void SetPWMDuty(uint32_t brightness_val)
{
uint32_t duty = pwm_period - brightness_val;
if (duty > pwm_period) duty = 0;
TCC0_PWM24bitDutySet(TCC0_CHANNEL1, duty);
}

// *****************************************************************************
// 更新 LED 输出(根据 led_on 和 brightness)
// *****************************************************************************
static void UpdateLED(void)
{
if (led_on) {
SetPWMDuty(brightness);
} else {
SetPWMDuty(0); // 关闭时 brightness 无意义,直接输出熄灭
}
}

// *****************************************************************************
// 主函数
// *****************************************************************************
int main ( void )
{
uint32_t msp_ns = *((uint32_t *)(TZ_START_NS));
volatile funcptr_void NonSecure_ResetHandler;

SYS_Initialize ( NULL );
SYSTICK_TimerStart();

// ========== 用户外设初始化 ==========
touch_init();
PWM_Init();

// 初始状态:LED 关闭,记忆中等亮度
led_on = false;
brightness = 0;
last_brightness = pwm_period / 2;
UpdateLED(); // 实际输出 pwm_period(熄灭)
// ==================================

if (msp_ns != 0xFFFFFFFF)
{
__TZ_set_MSP_NS(msp_ns);
NonSecure_ResetHandler = (funcptr_void)(*((uint32_t *)((TZ_START_NS) + 4U)));
NonSecure_ResetHandler();
}

static uint8_t last_touch = 0;
while ( true )
{
touch_process();
uint8_t now = (get_sensor_state(0) & KEY_TOUCHED_MASK) ? 1 : 0;

loop_counter++;

// 按下事件(上升沿)
if (now == 1 && last_touch == 0) {
press_start_loop = loop_counter;
}

// 释放事件(下降沿)
if (now == 0 && last_touch == 1) {
uint32_t duration = loop_counter - press_start_loop;

if (long_press_active) {
// 长按结束,退出调光模式
long_press_active = false;
}
else if (duration < LONG_PRESS_CYCLES) {
// 短按:翻转 LED 开关状态
if (led_on) {
// 关闭:记忆当前亮度,熄灭
last_brightness = brightness;
led_on = false;
UpdateLED();
} else {
// 开启:恢复记忆亮度
led_on = true;
brightness = (last_brightness == 0) ? (pwm_period / 2) : last_brightness;
UpdateLED();
}
}
}

// 长按检测
if (now == 1 && !long_press_active) {
if ((loop_counter - press_start_loop) >= LONG_PRESS_CYCLES) {
long_press_active = true;

// 若 LED 当前关闭,先开启并恢复记忆亮度
if (!led_on) {
led_on = true;
brightness = (last_brightness == 0) ? (pwm_period / 2) : last_brightness;
UpdateLED();
}

// 确定调光方向:当前亮度已达最亮则变暗,否则变亮
dim_dir = (brightness >= pwm_period) ? -1 : 1;
next_step_loop = loop_counter;
}
}

// 长按调光步进
if (long_press_active && (loop_counter - next_step_loop) >= PWM_STEP_CYCLES) {
next_step_loop = loop_counter;
int32_t new_bright = (int32_t)brightness + dim_dir * PWM_STEP;
if (new_bright >= (int32_t)pwm_period) {
new_bright = pwm_period;
dim_dir = -1;
} else if (new_bright <= 0) {
new_bright = 0;
dim_dir = 1;
}
brightness = (uint32_t)new_bright;
UpdateLED(); // 实时更新 PWM
}

last_touch = now;
}

return EXIT_FAILURE;
}

4.5.1 头文件包含与 TrustZone 回调定义

#include <stddef.h>
#include <stdbool.h>
#include <stdlib.h>
#include "definitions.h"

typedef void (*funcptr_void)(void) __attribute__((cmse_nonsecure_call));
  • 包含标准库和 Harmony 生成的设备相关头文件 definitions.h(其中声明了 TCC0、触摸、SysTick 等外设驱动接口)。
  • 定义非安全区函数指针类型,用于 TrustZone 环境下跳转到非安全区应用程序。

4.5.2 用户配置常量

#define LONG_PRESS_CYCLES   20000
#define PWM_STEP_CYCLES 500
#define PWM_STEP 5
  • LONG_PRESS_CYCLES:长按判定所需的主循环次数(对应约 500ms,具体取决于主循环执行速度)。
  • PWM_STEP_CYCLES:亮度步进间隔的循环次数(对应约 10ms)。
  • PWM_STEP:每次步进时亮度的变化量(占空比改变 5 级,保证调光平滑)。

这些参数不使用硬件定时器,而是利用主循环计数模拟时间基准。

4.5.3 全局变量

static uint32_t loop_counter = 0;
static bool led_on = false;
static uint32_t pwm_period;
static uint32_t brightness = 0;
static uint32_t last_brightness = 0;

static uint32_t press_start_loop = 0;
static bool long_press_active = false;
static int8_t dim_dir = 1;
static uint32_t next_step_loop = 0;
  • loop_counter:每次主循环递增,提供相对时间参考。
  • led_on:记录 LED 是否处于开启状态(用于短按开关)。
  • pwm_period:PWM 周期值(例如 2399),从 TCC0 寄存器读取。
  • brightness:当前亮度逻辑值(0 = 灭,pwm_period = 最亮)。
  • last_brightness:关灯时保存的亮度值,用于下次开灯恢复。
  • press_start_loop:记录按下时刻的 loop_counter 值。
  • long_press_active:标志是否正在长按调光模式中。
  • dim_dir:调光方向(1 = 变亮,-1 = 变暗)。
  • next_step_loop:下一次亮度步进应达到的 loop_counter 值。

4.5.4 PWM 辅助函数

static void PWM_Init(void)
static void SetPWMDuty(uint32_t brightness_val)
static void UpdateLED(void)
  • PWM_Init:调用 Harmony 生成的 TCC0 初始化函数、启动 PWM,并读取周期值。
  • SetPWMDuty:由于硬件 LED 低电平点亮,实际输出占空比 = pwm_period - brightness_val,实现逻辑值到硬件输出的转换。
  • UpdateLED:根据 led_on 标志决定是否输出当前亮度值(开启时调用 SetPWMDuty(brightness),关闭时调用 SetPWMDuty(0))。

4.5.5 主函数 – 初始化部分

int main(void)
{
// TrustZone 相关变量及跳转准备
SYS_Initialize(NULL);
SYSTICK_TimerStart();

touch_init();
PWM_Init();

led_on = false;
brightness = 0;
last_brightness = pwm_period / 2;
UpdateLED();

// TrustZone 非安全区跳转(如果存在)
}
  • 调用 Harmony 系统初始化、启动 SysTick(虽未使用其中断,但保留)。
  • 初始化触摸库和 PWM。
  • 设置初始状态:LED 关闭,记忆亮度为中等值(周期的一半)。
  • 如果芯片支持 TrustZone 且非安全区代码存在,则跳转执行;否则继续执行下面的主循环。

4.5.6 主循环 – 触摸事件处理与调光逻辑

主循环使用 while(1) 无限执行,核心步骤:

获取触摸状态并维护计数器

touch_process();
uint8_t now = (get_sensor_state(0) & KEY_TOUCHED_MASK) ? 1 : 0;
loop_counter++;
  • 刷新触摸传感器状态,读取传感器 0 的触摸标志位(KEY_TOUCHED_MASK 应在 definitions.h 中定义)。
  • 每次循环递增 loop_counter,用于模拟时间。

按下/释放事件检测

  • 上升沿now == 1 && last_touch == 0):记录按下时刻 press_start_loop
  • 下降沿now == 0 && last_touch == 1):
    • 若当前处于长按调光模式(long_press_active == true),则关闭长按标志(结束调光)。
    • 否则,如果按下持续时间小于长按阈值,判定为短按:翻转 led_on 状态并执行开关灯(关灯时记忆亮度,开灯时恢复记忆亮度)。

长按激活检测

if (now == 1 && !long_press_active) {
if ((loop_counter - press_start_loop) >= LONG_PRESS_CYCLES) {
long_press_active = true;
// 若 LED 关闭,先点亮并恢复记忆亮度
// 设置调光方向(当前最亮则变暗,否则变亮)
// 立即准备第一次步进
}
}
  • 在按键保持按下且尚未进入长按模式时,检测持续时间是否达到长按阈值。若达到,则激活长按调光模式,确保 LED 点亮,并初始化调光方向。

长按调光步进

if (long_press_active && (loop_counter - next_step_loop) >= PWM_STEP_CYCLES) {
next_step_loop = loop_counter;
// 亮度 = 当前亮度 + 方向 × 步长
// 边界处理:达到周期值或 0 时反转方向
brightness = new_bright;
UpdateLED();
}
  • 每隔 PWM_STEP_CYCLES 次循环,改变一次亮度值(增/减 PWM_STEP)。
  • 到达最亮或最暗时自动反向,实现来回扫描。
  • 实时调用 UpdateLED 刷新 PWM 占空比,保证亮度平滑变化。

状态更新

last_touch = now;
  • 保存本次触摸状态,用于下一轮边沿检测。

4.5.7 进阶任务程序总结

初始化

系统时钟、触摸、PWM,LED 初始关闭。

主循环

持续读取触摸状态。

检测短按 → 开关 LED(带亮度记忆)。

检测长按 → 进入调光模式,周期性改变亮度,边界自动反向。

松开按键后退出调光模式,亮度保持当前值。

特点

不依赖硬件中断,使用主循环计数模拟时间(简化实现)。

低电平点亮的 LED 极性在软件中取反处理。

亮度步进间隔短、步进量小,人眼感觉平滑。

代码结构清晰,仅使用几个静态变量,适合资源受限的嵌入式环境。

该程序完整实现了“短按开关 LED(记忆亮度)+ 长按连续调光(来回扫描)”的进阶需求。


五、功能展示图及说明

5.1 基础任务功能展示

基础任务短按触摸LED翻转.gif

图:基础任务短按触摸LED翻转

实现基础任务功能,短按触摸,实现LED翻转亮灭。

5.2 进阶任务功能展示

进阶任务长按触摸LED循环连续调光.gif

图:进阶任务长按触摸LED循环连续调光

实现进阶任务功能,长按触摸,通过PWM连续调节LED亮度,并按照【亮度从“ 最亮→ 灭→ 最亮”线性变化】的逻辑循环。而且LED亮度变化过程平滑,无明显闪烁或跳变。


六、项目中遇到的难题及解决方法

6.1 问题1:触摸信号抖动导致单次按压触发多次动作

问题描述
在基础任务中,短按一次触摸按钮,LED 有时会闪烁多次或状态翻转多次,而不是预期的单次切换。观察发现按钮按下或释放瞬间存在短暂的不稳定电平。

原因分析
触摸传感器(PTC)输出的数字信号在按下和释放的边界处可能存在多次跳变(抖动)。程序直接读取 get_sensor_state() 并在上升沿立即触发动作,抖动会被误判为多次有效触摸。

解决方法
在基础任务中引入 软件去抖 机制:

  • 记录上一次稳定状态 last_touch
  • 检测到原始值 raw  last_touch 不同时,启动去抖计数器 debounce_cnt(例如设为 5)。
  • 每个主循环计数器减 1,直到计数器归零且当前状态仍为按下,才执行有效的触摸动作。
  • 去抖期间即使状态再次变化,也会重置计数器,从而滤除毛刺。
    该方法不增加硬件成本,代码简单,有效保证单次触摸只响应一次。

6.2 问题2:LED 初始状态与程序逻辑相反

问题描述
程序初始化时设置 LED 为关闭状态(led_on = false, 占空比设为 0),但实际硬件上 LED 却是亮的。短按开/关逻辑也出现反向:关闭时 LED 亮,开启时 LED 灭。

原因分析
硬件电路设计为 低电平点亮 LED(LED 阳极接 VCC,阴极接 MCU 引脚)。而程序中直接使用 PWM 占空比值作为亮度控制:占空比越大,引脚高电平时间越长,LED 反而越暗甚至熄灭。因此逻辑上的“关闭”(占空比 0)实际导致引脚恒低,LED 全亮;“开启”(占空比非零)反而使引脚出现高电平,LED 变暗或灭。

解决方法
在软件中增加 极性转换:内部逻辑使用直观的亮度值 brightness(0 = 灭,pwm_period = 最亮),实际输出到 PWM 寄存器前取反:

c

uint32_t duty = pwm_period - brightness;
TCC0_PWM24bitDutySet(TCC0_CHANNEL1, duty);

同时,初始状态设置 brightness = 0,取反后输出为 pwm_period(引脚低电平),LED 熄灭。

6.3 问题3:短按时 LED 无法关闭,只能在最亮和记忆亮度之间切换

问题描述
短按触摸按钮后,LED 有时在最亮和某个中间亮度之间跳变,但从未真正熄灭。期望行为是:每次短按应切换 LED 的   

原因分析
原始代码中使用了单独的 led_on 标志和 brightness 变量,但在短按处理逻辑中,开灯时强制设置 brightness 为记忆值,关灯时却没有将 brightness 置为 0,且 UpdateLED() 内部判断 led_on 决定是否输出亮度值。当 led_on 为 false 时,实际调用 SetPWMDuty(0) 本应输出 0 占空比,但因极性转换公式导致输出 pwm_period(引脚低电平),LED 反而最亮。同时关灯时没有更新 brightness,导致下次开灯仍沿用之前值。

解决方法
重构 UpdateLED() 函数,使其完全基于 led_on  brightness 两个独立变量:

  • 开灯时:调用 SetPWMDuty(brightness)
  • 关灯时:调用 SetPWMDuty(0),并且将当前亮度保存到 last_brightness,但 不改变 brightness 的值(保留用于下次开灯恢复)。
    同时,确保极性转换公式正确:duty = pwm_period - brightness,当 brightness = 0  duty = pwm_period(熄灭),符合低电平点亮逻辑。

6.4 问题4:使用 SysTick 中断导致程序复杂,且与现有框架冲突

问题描述
最初的进阶任务尝试使用 SysTick 中断产生 1ms 时间基准,用于长按计时和调光步进。但在已有 TrustZone 安全框架和 Harmony 自动生成的代码中,SysTick_Handler 可能被多次定义,导致链接错误;同时中断服务函数增加了代码复杂度。

原因分析
Harmony 框架已经占用了 SysTick 用于系统心跳或延时(如 SYSTICK_TimerStart()),用户再添加同名中断处理函数会造成重复定义。此外,中断方式需要管理全局易变变量,增加了程序的不确定性。

解决方法
放弃使用硬件中断,改用 主循环计数 模拟时间基准。在主循环中递增 loop_counter 变量,所有延时和步进判断均基于循环次数的差值。通过实际测试调整 LONG_PRESS_CYCLES  PWM_STEP_CYCLES 宏,使得循环次数对应所需的毫秒数。这种方式无需额外中断,与 TrustZone 和 Harmony 完全兼容,且逻辑更简单直观。

6.5 问题5:长按时亮度变化不平滑,出现明显跳变或闪烁

问题描述
长按调光时,LED 亮度变化不是连续的,而是阶梯式跳跃,甚至人眼能感觉到闪烁,体验不佳。

原因分析
亮度步进间隔太长或步进量太大。初始参数中步进间隔约为 50ms,步进量为周期值的 1/20,导致每次变化幅度明显。同时 PWM 频率较低(例如几百赫兹)时也可能产生可见闪烁。

解决方法

  • 将步进间隔缩短至 10ms(PWM_STEP_CYCLES 对应 400 次循环,约 10ms)。
  • 步进量改为 10(占空比分辨率 256 级,周期值 2400 时变化量约 0.4% 每步)。
  • 确保 PWM 定时器频率设置在 20kHz 以上,使人眼无法察觉闪烁。
    经过调整,从最暗到最亮约需 2.4 秒,人眼感知为连续平滑的亮度变化,无闪烁或跳变。

6.6 问题6:MPLAB Harmony 配置外设时生成代码不完整或冲突

问题描述
在使用 MPLAB Harmony 配置器(MHC)为 PIC32CM 器件添加 TCC0 PWM 和触摸库时,生成的项目代码中有时会出现函数未定义、重复定义或缺少必要的初始化调用。例如,按照图形界面配置了 TCC0 的 PWM 通道,但生成的 plib_tcc0.c 中没有提供 TCC0_PWM24bitDutySet 函数,导致编译失败。

原因分析

  • Harmony 的代码生成依赖于正确的组件选择和引脚映射。如果未在 MHC 中明确将 TCC0 的某个通道绑定到具体引脚(如 PA15),生成器可能只提供基础的周期配置函数,而不生成占空比设置函数。
  • 同时启用触摸库(Touch Library)和 TCC0 PWM 时,两者可能都尝试占用同一个定时器资源或中断,导致符号冲突。
  • Harmony 生成的驱动文件往往需要用户手动调用初始化函数(如 TCC0_PWMInitialize),若遗漏则外设不工作。

解决方法

  • 在 MHC 中仔细检查引脚配置:确保将 TCC0 的 WO[1] 输出明确分配给 PA15,并在“Pin Settings”中验证复用功能正确。
  • 对于缺少的 API,可参考 TCC0 数据手册直接操作寄存器实现(例如使用 TCC0_REGS->TCC_CCBUF[1] = duty),或从 Harmony 的文档中确认正确的函数名(有时是 TCC0_PWM_DutySet)。
  • 避免外设资源冲突:触摸库通常使用 PTC 和内部振荡器,不占用 TCC0,但需确保两个组件的时钟配置一致。
  •  main.c 的系统初始化(SYS_Initialize)之后,显式调用 TCC0_PWMInitialize()TCC0_PWMStart() 以及 touch_init(),确保所有外设被正确启动。
  • 若生成代码仍不完整,可以尝试重新生成项目(选择“Clean and Generate”)或升级 Harmony 框架版本。

七、心得体会

通过本次基于PIC32CM LS00 Curiosity Nano+ Touch评估套件的嵌入式项目开发,不仅完成了从基础触摸开关到PWM调光进阶任务的完整实践,更在硬件理解、软件设计、调试技巧和项目管理等方面获得了宝贵的经验。

技术收获方面,深入掌握了PIC32CM系列微控制器的外设使用,特别是TCC0定时器的PWM模式配置和触摸控制器PTC的轮询驱动。通过极性转换、软件去抖和状态机设计,理解了如何将硬件特性(如低电平点亮的LED)与软件逻辑优雅地结合。同时,使用主循环计数器替代硬件中断来模拟时间基准,使得对嵌入式系统的实时性有了更灵活的认识——并非所有场景都需要精确的中断,有时简单的轮询反而更可靠、更易于维护。

开发经验上,深刻体会到阅读芯片数据手册和原理图的重要性。最初LED状态与逻辑相反的问题,正是由于忽略了原理图中的低电平有效连接;而触摸信号抖动则促使我加入了去抖算法。在MPLAB Harmony框架下,外设驱动的生成和集成并非完全自动化,有时需要手动补充缺失的API或调整引脚映射,这提醒我不能过度依赖图形配置工具,应当具备直接操作寄存器的能力。此外,TrustZone安全框架下的非安全区跳转逻辑也让我对Armv8-M的安全扩展有了初步实践。

项目感悟最深的是:一个优秀的嵌入式程序不仅需要功能正确,还要在简洁性和可维护性之间取得平衡。从最初冗长的长按调光代码,到后来重构为状态清晰、变量精简的版本,我明白了“写完代码只是开始,简化代码才是进阶”。同时,软硬件的紧密耦合要求开发者必须站在系统的高度思考问题——比如PWM极性取反不仅解决了显示问题,更统一了亮度逻辑;去抖处理虽小,却显著提升了用户体验。

展望未来,基于本次项目的基础,可以进一步扩展多通道PWM控制实现RGB混色,或者将亮度记忆存入EEPROM实现断电保存。也可以将触摸库与低功耗模式结合,开发电池供电的触摸调光产品。更重要的是,本次积累的状态机设计模式和事件驱动思想,可以迁移到其他单片机平台(如STM32、瑞萨等),让我在面对更复杂的嵌入式项目时更有信心。相信,扎实的外设驱动能力和清晰的项目架构思维,将成为我今后从事物联网、智能家居等领域的坚实基石。

最后,感谢以下组织和个人的支持:

  • 电子森林(eetree.cn):提供Funpack活动平台,让我有机会学习和实践
  • 得捷电子(Digi-Key):提供Microchip EV41C56A 开发板赞助
  • Microchip微芯科技:提供完善的开发板资料和丰富MPLAB示例程序

特别感谢Funpack活动的组织者,为我们提供了这么好的学习机会。


八、附件

8.1 完整代码文件

完整代码文件已打包,包含以下内容:

Funpack5_2/
├── Task_1_Basic/
│ ├── PIC32CM-touch/
│ └── PIC32CM-touch_secure/
├── Task_1_Advanced/
│ └── PIC32CM-touchPWM/
│ └── PIC32CM-touchPWM_secure/
└── ...

8.2 参考资料

  1. PIC32CM LS00 Curiosity Nano+ Touch 评估工具包
  2. https://www.eetree.cn/EV41C56A/index_v3.html
  3. https://github.com/Microchip-MPLAB-Harmony
  4. https://chat.deepseek.com/

8.3 项目链接

  • 项目展示https://www.eetree.cn/project/detail/7951
  • Funpack活动https://www.eetree.cn/page/activity/funpack-5-2
附件下载
Funpack5_2_基于EV41C56A实现触摸输入与PWM控制LED_项目代码.rar
团队介绍
个人
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号