在嵌入式开发中,PWM(脉冲宽度调制)技术是实现LED亮度控制、电机调速等功能的基石。nRF52832作为Nordic Semiconductor推出的高性能蓝牙SoC,其内置的硬件PWM模块凭借EasyDMA和灵活的解码器模式,能够实现无CPU干预的精密控制。本文将带您深入探索如何利用这些硬件特性,打造一个专业级的LED呼吸灯方案。
在嵌入式领域,PWM实现方式主要分为软件模拟和硬件模块两种。软件PWM通过定时器中断手动翻转GPIO实现,虽然灵活但存在明显缺陷:
相比之下,nRF52832的硬件PWM模块具有以下优势:
| 特性 | 硬件PWM | 软件PWM |
|---|---|---|
| CPU占用 | 接近0% | 随频率升高而增加 |
| 精度 | 16位 | 通常8-12位 |
| 多通道同步 | 支持 | 难以实现 |
| 波形复杂度 | 支持任意序列 | 简单波形 |
| 功耗 | 极低 | 较高 |
特别是其内置的EasyDMA功能,可以直接从RAM读取PWM参数序列,实现真正的"设置后不管"操作模式。这对于需要精确控制且要求低功耗的物联网设备尤为重要。
nRF52832提供了三个独立的PWM模块(PWM0、PWM1、PWM2),每个模块包含4个输出通道,共计12路PWM输出。这些模块共享以下核心特性:
关键寄存器组包括:
c复制typedef struct {
__IO uint32_t ENABLE; // 模块使能
__IO uint32_t MODE; // 计数模式
__IO uint32_t COUNTERTOP; // 计数上限
__IO uint32_t PRESCALER; // 时钟分频
__IO uint32_t DECODER; // 解码模式配置
__IO uint32_t LOOP; // 循环控制
struct {
__IO uint32_t PTR; // 序列指针
__IO uint32_t CNT; // 序列长度
__IO uint32_t REFRESH; // 刷新计数
__IO uint32_t ENDDELAY; // 结束延迟
} SEQ[2]; // 序列寄存器组
__IO uint32_t PSEL.OUT[4]; // 输出引脚选择
} NRF_PWM_Type;
要实现平滑的LED呼吸效果,我们需要创建一个亮度渐变序列。Common模式是最适合的选择,因为它允许所有通道共享相同的占空比序列,同时保持独立的极性控制。
首先确保LED正确连接,假设我们使用PWM0通道0驱动LED:
c复制#define LED_PIN 17
void pwm_init(void) {
// 配置PWM输出引脚
NRF_PWM0->PSEL.OUT[0] = (LED_PIN << PWM_PSEL_OUT_PIN_Pos) |
(PWM_PSEL_OUT_CONNECT_Connected << PWM_PSEL_OUT_CONNECT_Pos);
// 使能PWM模块
NRF_PWM0->ENABLE = (PWM_ENABLE_ENABLE_Enabled << PWM_ENABLE_ENABLE_Pos);
// 配置为向上计数模式
NRF_PWM0->MODE = (PWM_MODE_UPDOWN_Up << PWM_MODE_UPDOWN_Pos);
// 设置时钟分频为125kHz (16MHz/128)
NRF_PWM0->PRESCALER = (PWM_PRESCALER_PRESCALER_DIV_128 << PWM_PRESCALER_PRESCALER_Pos);
// 设置计数器上限为25000 (200ms周期 @125kHz)
NRF_PWM0->COUNTERTOP = (25000 << PWM_COUNTERTOP_COUNTERTOP_Pos);
// 配置为Common解码模式,自动刷新
NRF_PWM0->DECODER = (PWM_DECODER_LOAD_Common << PWM_DECODER_LOAD_Pos) |
(PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
}
呼吸灯效果本质上是一个亮度从0%到100%再到0%的循环变化过程。我们可以用数学函数生成平滑的亮度曲线:
c复制#define SEQUENCE_LENGTH 50 // 序列长度
#define MAX_DUTY_CYCLE 25000 // 最大占空比(COUNTERTOP)
static uint16_t breathe_sequence[SEQUENCE_LENGTH];
void generate_breathe_sequence(void) {
for (int i = 0; i < SEQUENCE_LENGTH; i++) {
// 使用正弦函数创建平滑过渡
float radians = (2 * M_PI * i) / SEQUENCE_LENGTH;
float factor = (1 - cosf(radians)) / 2; // 0到1之间变化
breathe_sequence[i] = (uint16_t)(MAX_DUTY_CYCLE * factor);
}
}
生成序列后,我们需要配置EasyDMA相关寄存器:
c复制void start_pwm_sequence(void) {
// 设置序列指针
NRF_PWM0->SEQ[0].PTR = ((uint32_t)(breathe_sequence) << PWM_SEQ_PTR_PTR_Pos);
// 设置序列长度
NRF_PWM0->SEQ[0].CNT = (SEQUENCE_LENGTH << PWM_SEQ_CNT_CNT_Pos);
// 设置刷新和延迟参数
NRF_PWM0->SEQ[0].REFRESH = 0;
NRF_PWM0->SEQ[0].ENDDELAY = 0;
// 启动序列播放
NRF_PWM0->TASKS_SEQSTART[0] = 1;
}
提示:为了获得更流畅的效果,可以适当增加序列长度(如100-200点),但这会占用更多RAM。需要在效果和资源消耗间取得平衡。
如果需要同时控制多个LED(如RGB LED),Grouped模式是更好的选择。它允许将4个通道分为两组,每组共享相同的占空比设置。
c复制void pwm_grouped_init(void) {
// 配置四个输出通道
NRF_PWM0->PSEL.OUT[0] = (RED_PIN << PWM_PSEL_OUT_PIN_Pos) | PWM_PSEL_OUT_CONNECT_Connected;
NRF_PWM0->PSEL.OUT[1] = (GREEN_PIN << PWM_PSEL_OUT_PIN_Pos) | PWM_PSEL_OUT_CONNECT_Connected;
NRF_PWM0->PSEL.OUT[2] = (BLUE_PIN << PWM_PSEL_OUT_PIN_Pos) | PWM_PSEL_OUT_CONNECT_Connected;
NRF_PWM0->PSEL.OUT[3] = PWM_PSEL_OUT_CONNECT_Disconnected; // 未使用
// 其他配置与Common模式类似
// ...
// 关键区别:设置为Grouped解码模式
NRF_PWM0->DECODER = (PWM_DECODER_LOAD_Grouped << PWM_DECODER_LOAD_Pos) |
(PWM_DECODER_MODE_RefreshCount << PWM_DECODER_MODE_Pos);
}
在Grouped模式下,每个序列点包含两个16位值,分别控制通道0/1和通道2/3:
c复制typedef struct {
uint16_t group0; // 通道0和1的占空比
uint16_t group1; // 通道2和3的占空比
} pwm_grouped_value_t;
#define RGB_SEQUENCE_LENGTH 50
static pwm_grouped_value_t rgb_sequence[RGB_SEQUENCE_LENGTH];
void generate_rgb_breathe_sequence(void) {
for (int i = 0; i < RGB_SEQUENCE_LENGTH; i++) {
float phase = (2 * M_PI * i) / RGB_SEQUENCE_LENGTH;
// 红色通道:相位0
float red = (1 - cosf(phase)) / 2;
// 绿色通道:相位2π/3
float green = (1 - cosf(phase + 2*M_PI/3)) / 2;
// 蓝色通道:相位4π/3
float blue = (1 - cosf(phase + 4*M_PI/3)) / 2;
rgb_sequence[i].group0 = (uint16_t)(MAX_DUTY_CYCLE * red); // 通道0(红)
rgb_sequence[i].group1 = (uint16_t)(MAX_DUTY_CYCLE * green); // 通道1(绿)
// 通道2(蓝)需要通过另一个序列点控制
}
}
在实际产品中,功耗是需要重点考虑的因素。以下是几个优化技巧:
时钟分频选择:
动态关闭未使用模块:
c复制// 当不需要PWM时
NRF_PWM0->ENABLE = (PWM_ENABLE_ENABLE_Disabled << PWM_ENABLE_ENABLE_Pos);
利用PPI系统:
RAM使用优化:
c复制// PPI配置示例:使用定时器自动启动PWM序列
NRF_PPI->CH[0].EEP = (uint32_t)&NRF_TIMER0->EVENTS_COMPARE[0];
NRF_PPI->CH[0].TEP = (uint32_t)&NRF_PWM0->TASKS_SEQSTART[0];
NRF_PPI->CHENSET = (1 << 0);
通过合理利用nRF52832的硬件PWM特性,开发者可以创建出既高效又节能的LED控制方案,为物联网设备增添精美的视觉效果而不牺牲电池寿命。