一、项目介绍
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® 技术、加密加速器与增强型触摸控制完美融合,是开发安全触摸应用的理想选择。

图: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)。

图:微控制器PIC32CM5164LS00048
安全特性:安全启动、ARM TrustZone®、加密加速器,为您的应用提供全面安全保障
触摸控制:增强型外设触摸控制器(PTC),支持按键、滑条、滚轮等多种触摸界面
超低功耗:SleepWalking 外设技术,实现极致低功耗运行,适合电池供电设备
2.2 Microchip EV41C56A开发板概述
PIC32CM LS00 Curiosity Nano+ Touch(EV41C56A)评估工具包是用于评估PIC32CM5164LS00048 MCU 的硬件平台。

图: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 评估工具包上的边缘引脚访问。

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


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

图:电源原理图

图:MCU原理图

图:调试器原理图
三、方案框图和项目设计思路
3.1 基础任务方案框图
基础题目:
使用 EV41C56A 开发板板载触摸按键功能,实现以下逻辑:
- 单次触摸 → 点亮板载LED
- 再次触摸 → 关闭LED
功能要求:
- 正确初始化触摸通道
- LED 控制逻辑清晰,不出现误触发
根据以上基础任务要求,确定方案框图。

图:基础任务方案框图
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为当前稳定状态。
- 记录上一次稳定触摸状态
- 去抖期间不会重复触发,有效滤除触摸信号的机械抖动和噪声。
- 初始化 GPIO(PA15 设为输出,初始高电平使 LED 灭)和触摸传感器(
3.3 进阶任务方案框图
在基础题之上,实现触摸长按调节LED 亮度功能
功能要求:
- 通过 PWM 控制 LED 亮度
- 短按:LED 开 / 关
- 长按:连续调节亮度,并按照【亮度从“灭 → 最亮”线性变化;当到达最亮后,保持长按可实现“最亮 → 灭”反向变化】的逻辑循环
- 亮度变化过程应平滑(无明显闪烁或跳变)
根据以上进阶任务要求,确定方案框图。


图:进阶任务方案框图
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开发板的使用指南、示例程序等。

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

图:新建项目

图:选择项目工程

图:选择设备芯片和工具

图:选择编译器

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

图:文件结构
4.1.3 基础任务工程项目配置
非安全工程下进入MCC,在Device Resources中将RTC、PTC、Touch Library三个组件添加到右侧Project Graph窗口中,一般会自动连线。


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

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

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

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


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

图:Pin引脚属性配置
MCC配置完成后,点击Project Resources旁的Generate生成相应的项目代码。

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


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

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

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

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

图:基础任务程序流程图
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,后续所有亮度计算都以该值为基准。该函数仅在程序开始时执行一次。

图:进阶任务PWM_Init()程序流程图
4.4.2 SetPWMDuty(brightness)程序流程图
SetPWMDuty(brightness)程序的功能是根据亮度值换算并输出 PWM 信号,驱动 LED。
由于硬件采用低电平点亮 LED,因此实际占空比 = pwm_period - brightness_val。增加了溢出保护:若计算结果大于 pwm_period,则将占空比限制为 0(即完全熄灭)。最后调用 TCC0_PWM24bitDutySet() 将换算后的占空比写入对应通道(CH1)。

图:进阶任务SetPWMDuty(brightness)程序流程图
4.4.3 UpdateLED()程序流程图
UpdateLED()程序的功能是统一管理LED的亮灭状态,避免直接操作PWM导致逻辑混乱。
对全局标志 led_on进行判断,若为 true,则调用 SetPWMDuty(brightness) 按当前亮度点亮 LED;若为 false,则调用 SetPWMDuty(0) 强制输出熄灭(此时 brightness 值无实际意义)。所有需要改变 LED 显示的地方(如开关灯、调光步进)都通过该函数实现,保证了输出的一致性。

图:进阶任务UpdateLED()程序流程图
4.4.4 进阶任务主程序流程图
主程序在完成触摸和PWM等外设初始化后,进入无限循环。每个循环周期都会读取触摸状态并与上一次状态比较,捕捉上升沿和下降沿。上升沿记录按下的起始计数值,下降沿则根据按住时长区分短按与长按:若按住时间低于阈值则为短按,此时翻转LED的开/关状态——关闭时保存当前亮度,开启时恢复上次记忆亮度,实现了开关不丢亮度的功能。
当按键持续按下且按住时长超过设定的长按阈值时,程序进入长按调光模式。若此时LED处于关闭状态,会先自动点亮并恢复记忆亮度;然后根据当前亮度自动确定调光方向(已达到最亮则转为变暗,否则变亮)。此后每隔固定循环次数,亮度值按照设定步长逐步增减,同时实时更新PWM输出,使LED平滑变化;一旦亮度触及最大值或最小值,方向自动反转,形成“灭→最亮→灭”的循环调光。松开按键后,长按模式退出,亮度停留在当前值。
整个逻辑依靠自由运行的loop_counter实现相对计时,所有亮度输出最终都通过UpdateLED函数统一控制,确保硬件输出与软件状态一致。这样既实现了直观的触摸交互,又避免了复杂的定时器占用。

图:进阶任务主程序流程图
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翻转
实现基础任务功能,短按触摸,实现LED翻转亮灭。
5.2 进阶任务功能展示

图:进阶任务长按触摸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 参考资料
- PIC32CM LS00 Curiosity Nano+ Touch 评估工具包
- https://www.eetree.cn/EV41C56A/index_v3.html
- https://github.com/Microchip-MPLAB-Harmony
- https://chat.deepseek.com/
8.3 项目链接
- 项目展示:https://www.eetree.cn/project/detail/7951
- Funpack活动:https://www.eetree.cn/page/activity/funpack-5-2