文章目录
- 1. 引言
- 2. 核心概念
- 2.1 什么是 PWM
- 2.2 nRF52840 的 PWM 资源概览
- 2.3 三种实现方案对比
- 3. 实战示例
- 3.1 方案一:low_power_pwm(低功耗优先)
- 3.2 方案二:pwm_library(性能优先)
- 3.3 方案三:pwm_driver(均衡之选)
- 3.4 选型决策流程
- 4. 常见问题
- 4.1 为什么 low_power_pwm 和 pwm_library 都只能输出 2 路?
- 4.2 需要 8 路 PWM 该如何实现?
- 4.3 低功耗模式下 PWM 还能工作吗?
- 4.4 PWM 输出异常排查
- 1. 引脚冲突
- 2. 时钟配置错误
- 3. 占空比设置无效
- 5. 总结
1. 引言
在嵌入式开发中,PWM(脉冲宽度调制)是控制电机转速、LED 亮度、蜂鸣器频率等场景的核心技术。nRF52840 作为 Nordic 推出的低功耗蓝牙 SoC,提供了多种 PWM 实现途径,但不同方案在功耗、资源占用和输出通道数上差异显著。
本文将从 nRF52840 的 PWM 资源出发,系统对比三种主流实现方案,并通过实战示例帮助读者快速选型与落地。
2. 核心概念
2.1 什么是 PWM
PWM 通过调节方波信号的占空比(高电平时间占整个周期的比例)来模拟模拟输出。占空比越高,等效输出电压越高,从而实现对负载的精细控制。
2.2 nRF52840 的 PWM 资源概览
nRF52840 内置 3 组 PWM 外设模块,每组最多支持 4 路 PWM 输出,总计最多 12 路。此外,芯片还提供低功耗定时器(如 RTC)和通用硬件定时器(如 TIMER),它们也能产生 PWM,但通道数和功耗特性各不相同。
2.3 三种实现方案对比
| 方案 | 底层资源 | 功耗 | 单定时器/模块通道数 | 适用场景 |
|---|---|---|---|---|
| low_power_pwm | 低功耗定时器(RTC) | 低 | 2 路 | 对功耗敏感、通道需求少的场景 |
| pwm_library | 硬件定时器(TIMER) | 高 | 2 路 | 对实时性要求高、可接受较高功耗 |
| pwm_driver | PWM 外设模块 | 中等 | 4 路 | 需要多通道输出、功耗与性能均衡 |
3. 实战示例
在动手编写代码之前,先了解 nRF52840 上常用的 PWM 引脚分配,有助于避免引脚冲突并快速选型。下表列出了 6 个典型引脚及其对应的 PWM 资源:
| 引脚号 | 功能 | 所属 PWM 模块/定时器 | 适用方案 |
|---|---|---|---|
| P0.06 | LED 亮度控制 | PWM0 通道 0 | pwm_driver |
| P0.08 | 电机调速 | TIMER0 比较输出 0 | pwm_library |
| P0.11 | 蜂鸣器驱动 | PWM1 通道 1 | pwm_driver |
| P0.13 | 舵机控制 | PWM2 通道 2 | pwm_driver |
| P0.15 | 低功耗呼吸灯 | RTC 比较输出 0 | low_power_pwm |
| P0.17 | 风扇调速 | TIMER1 比较输出 1 | pwm_library |
| P0.20 | RGB LED 绿色通道 | PWM0 通道 3 | pwm_driver |
| P0.24 | 低功耗指示灯 | RTC 比较输出 1 | low_power_pwm |
说明:实际引脚功能需结合具体开发板原理图与 Nordic 引脚复用配置(GPIO 的 P0/P1 端口)确认,上表为常见典型分配示例。
3.1 方案一:low_power_pwm(低功耗优先)
#include"nrfx.h"#include"nrf_gpio.h"#include"nrfx_gpiote.h"#include"nrfx_rtc.h"#include"nrfx_ppi.h"/* low_power_pwm:使用 RTC + PPI + GPIOTE 实现低功耗 PWM 输出 */#defineLOW_PWM_RTC_ID2#defineLOW_PWM_PINNRF_GPIO_PIN_MAP(0,15)#defineLOW_PWM_PERIOD_TICKS1000U/* 32.768 kHz 下约 32.77 Hz,适合低功耗 LED 呼吸灯 */staticconstnrfx_rtc_tlow_pwm_rtc=NRFX_RTC_INSTANCE(LOW_PWM_RTC_ID);staticnrf_ppi_channel_tlow_pwm_ppi_set;staticnrf_ppi_channel_tlow_pwm_ppi_clr;staticnrf_ppi_channel_tlow_pwm_ppi_period;/* 将 0-100 的占空比转换为 RTC 比较计数值 */staticuint32_tlow_pwm_percent_to_ticks(uint8_tpercent){if(percent>100U){percent=100U;}return(LOW_PWM_PERIOD_TICKS*percent)/100U;}voidlow_power_pwm_init(void){nrfx_err_terr;/* 1. 配置输出引脚为 GPIOTE 任务模式,初始输出低电平 */nrfx_gpiote_out_config_tout_cfg={.action=NRF_GPIOTE_POLARITY_TOGGLE,.init_state=NRF_GPIOTE_INITIAL_VALUE_LOW,.task_pin=true,};err=nrfx_gpiote_out_init(LOW_PWM_PIN,&out_cfg);if(err!=NRFX_SUCCESS){return;}/* 2. 初始化 RTC,使用 32.768 kHz 低功耗时钟 */nrfx_rtc_config_trtc_cfg=NRFX_RTC_DEFAULT_CONFIG;rtc_cfg.prescaler=0;err=nrfx_rtc_init(&low_pwm_rtc,&rtc_cfg,NULL);if(err!=NRFX_SUCCESS){return;}/* 3. 分配并配置 PPI 通道: CC0 事件 -> GPIOTE SET:置位引脚,开始高电平 CC1 事件 -> GPIOTE CLR:清除引脚,结束高电平 CC2 事件 -> RTC CLEAR:清除计数器,开始下一个周期 */nrfx_ppi_channel_alloc(&low_pwm_ppi_set);nrfx_ppi_channel_alloc(&low_pwm_ppi_clr);nrfx_ppi_channel_alloc(&low_pwm_ppi_period);nrfx_ppi_channel_assign(low_pwm_ppi_set,nrfx_rtc_event_address_get(&low_pwm_rtc,NRF_RTC_EVENT_COMPARE_0),nrfx_gpiote_set_task_addr_get(LOW_PWM_PIN));nrfx_ppi_channel_assign(low_pwm_ppi_clr,nrfx_rtc_event_address_get(&low_pwm_rtc,NRF_RTC_EVENT_COMPARE_1),nrfx_gpiote_clr_task_addr_get(LOW_PWM_PIN));nrfx_ppi_channel_assign(low_pwm_ppi_period,nrfx_rtc_event_address_get(&low_pwm_rtc,NRF_RTC_EVENT_COMPARE_2),nrfx_rtc_task_address_get(&low_pwm_rtc,NRF_RTC_TASK_CLEAR));nrfx_ppi_channel_enable(low_pwm_ppi_set);nrfx_ppi_channel_enable(low_pwm_ppi_clr);nrfx_ppi_channel_enable(low_pwm_ppi_period);/* 4. 设置比较值并启动 RTC */nrfx_rtc_counter_clear(&low_pwm_rtc);nrfx_rtc_cc_set(&low_pwm_rtc,NRF_RTC_CC_CHANNEL_0,0,false);nrfx_rtc_cc_set(&low_pwm_rtc,NRF_RTC_CC_CHANNEL_1,0,false);nrfx_rtc_cc_set(&low_pwm_rtc,NRF_RTC_CC_CHANNEL_2,LOW_PWM_PERIOD_TICKS,false);nrfx_rtc_enable(&low_pwm_rtc);}voidlow_power_pwm_set_duty(uint8_tduty_percent){/* 仅更新比较值即可,PPI 会在下一个周期自动生效 */uint32_tduty_ticks=low_pwm_percent_to_ticks(duty_percent);nrfx_rtc_cc_set(&low_pwm_rtc,NRF_RTC_CC_CHANNEL_1,duty_ticks,false);}intmain(void){low_power_pwm_init();low_power_pwm_set_duty(50);/* 50% 占空比输出 */while(1){__WFE();/* 进入低功耗等待,PWM 由 RTC 和 PPI 自动维持 */}}特点:功耗最低,适合电池供电的常驻场景;但单定时器仅支持 2 路,且频率精度受限于低功耗时钟源。
3.2 方案二:pwm_library(性能优先)
#include"nrfx.h"#include"nrf_gpio.h"#include"nrfx_gpiote.h"#include"nrfx_timer.h"#include"nrfx_ppi.h"/* pwm_library:使用 TIMER + PPI + GPIOTE 实现高频 PWM 输出 */#defineHPWM_TIMER_ID0#defineHPWM_PINNRF_GPIO_PIN_MAP(0,8)#defineHPWM_TIMER_FREQ_HZ16000000UL/* TIMER 运行频率 16 MHz */#defineHPWM_TARGET_FREQ_HZ20000UL/* 目标 PWM 频率 20 kHz */#defineHPWM_PERIOD_TICKS(HPWM_TIMER_FREQ_HZ/HPWM_TARGET_FREQ_HZ)/* 800 */staticconstnrfx_timer_thpwm_timer=NRFX_TIMER_INSTANCE(HPWM_TIMER_ID);staticnrf_ppi_channel_thpwm_ppi_set;staticnrf_ppi_channel_thpwm_ppi_clr;staticnrf_ppi_channel_thpwm_ppi_period;/* 将 0-100 的占空比转换为定时器比较计数值 */staticuint32_thpwm_percent_to_ticks(uint8_tpercent){if(percent>100U){percent=100U;}return(HPWM_PERIOD_TICKS*percent)/100U;}voidpwm_library_init(void){nrfx_err_terr;/* 1. 配置输出引脚为 GPIOTE 任务模式,初始输出低电平 */nrfx_gpiote_out_config_tout_cfg={.action=NRF_GPIOTE_POLARITY_TOGGLE,.init_state=NRF_GPIOTE_INITIAL_VALUE_LOW,.task_pin=true,};err=nrfx_gpiote_out_init(HPWM_PIN,&out_cfg);if(err!=NRFX_SUCCESS){return;}/* 2. 初始化 TIMER0 为定时器模式,16 MHz、16 位计数 */nrfx_timer_config_ttimer_cfg=NRFX_TIMER_DEFAULT_CONFIG;timer_cfg.frequency=NRF_TIMER_FREQ_16MHz;timer_cfg.mode=NRF_TIMER_MODE_TIMER;timer_cfg.bit_width=NRF_TIMER_BIT_WIDTH_16;err=nrfx_timer_init(&hpwm_timer,&timer_cfg,NULL);if(err!=NRFX_SUCCESS){return;}/* 3. 分配并配置 PPI 通道: CC0 事件 -> GPIOTE SET:置位引脚 CC1 事件 -> GPIOTE CLR:清除引脚 CC2 事件 -> TIMER CLEAR:清除定时器,开始下一个周期 */nrfx_ppi_channel_alloc(&hpwm_ppi_set);nrfx_ppi_channel_alloc(&hpwm_ppi_clr);nrfx_ppi_channel_alloc(&hpwm_ppi_period);nrfx_ppi_channel_assign(hpwm_ppi_set,nrfx_timer_event_address_get(&hpwm_timer,NRF_TIMER_EVENT_COMPARE0),nrfx_gpiote_set_task_addr_get(HPWM_PIN));nrfx_ppi_channel_assign(hpwm_ppi_clr,nrfx_timer_event_address_get(&hpwm_timer,NRF_TIMER_EVENT_COMPARE1),nrfx_gpiote_clr_task_addr_get(HPWM_PIN));nrfx_ppi_channel_assign(hpwm_ppi_period,nrfx_timer_event_address_get(&hpwm_timer,NRF_TIMER_EVENT_COMPARE2),nrfx_timer_task_address_get(&hpwm_timer,NRF_TIMER_TASK_CLEAR));nrfx_ppi_channel_enable(hpwm_ppi_set);nrfx_ppi_channel_enable(hpwm_ppi_clr);nrfx_ppi_channel_enable(hpwm_ppi_period);/* 4. 设置比较值并启动 TIMER */nrfx_timer_clear(&hpwm_timer);nrfx_timer_compare(&hpwm_timer,NRF_TIMER_CC_CHANNEL0,0,false);nrfx_timer_compare(&hpwm_timer,NRF_TIMER_CC_CHANNEL1,0,false);nrfx_timer_compare(&hpwm_timer,NRF_TIMER_CC_CHANNEL2,HPWM_PERIOD_TICKS,false);nrfx_timer_enable(&hpwm_timer);}voidpwm_library_set_duty(uint8_tduty_percent){/* 仅更新比较值即可,PPI 会在下一个周期自动生效 */uint32_tduty_ticks=hpwm_percent_to_ticks(duty_percent);nrfx_timer_compare(&hpwm_timer,NRF_TIMER_CC_CHANNEL1,duty_ticks,false);}intmain(void){pwm_library_init();pwm_library_set_duty(75);/* 75% 占空比输出 */while(1){__WFE();/* 等待中断或低功耗事件 */}}特点:硬件定时器精度高、实时性强,适合电机控制等高频场景;但功耗较高,单定时器同样仅支持 2 路。
3.3 方案三:pwm_driver(均衡之选)
#include"nrfx.h"#include"nrf_gpio.h"#include"nrfx_pwm.h"/* pwm_driver:使用 nRF52840 的 PWM 外设直接输出 4 路 PWM */#definePWM_DRV_PIN0NRF_GPIO_PIN_MAP(0,6)#definePWM_DRV_PIN1NRF_GPIO_PIN_MAP(0,8)#definePWM_DRV_PIN2NRF_GPIO_PIN_MAP(0,11)#definePWM_DRV_PIN3NRF_GPIO_PIN_MAP(0,13)#definePWM_DRV_TOP_VALUE1000U/* top 值 1000,便于按 0.1% 粒度设置占空比 */staticnrfx_pwm_tpwm0=NRFX_PWM_INSTANCE(0);staticnrf_pwm_values_common_tpwm_seq_values[NRF_PWM_CHANNEL_COUNT]={0};staticnrf_pwm_sequence_tpwm_seq;voidpwm_driver_init(void){nrfx_err_terr;/* 1. 配置 PWM0 的四路输出引脚 */nrfx_pwm_config_tcfg=NRFX_PWM_DEFAULT_CONFIG(PWM_DRV_PIN0,PWM_DRV_PIN1,PWM_DRV_PIN2,PWM_DRV_PIN3);/* 2. 设置 1 MHz 计数时钟和 top 值,PWM 频率约为 1 kHz */cfg.base_clock=NRF_PWM_CLK_1MHz;cfg.top_value=PWM_DRV_TOP_VALUE;err=nrfx_pwm_init(&pwm0,&cfg,NULL);if(err!=NRFX_SUCCESS){return;}/* 3. 初始化 PWM 播放序列,后续通过更新数组值改变各通道占空比 */pwm_seq.values.p_common=pwm_seq_values;pwm_seq.length=NRF_PWM_VALUES_LENGTH(pwm_seq_values);pwm_seq.repeats=0;pwm_seq.end_delay=0;}/* 设置指定通道的占空比,duty_percent 范围 0-100 */voidpwm_driver_set_channel(uint8_tchannel,uint8_tduty_percent){if(channel>=NRF_PWM_CHANNEL_COUNT){return;}if(duty_percent>100U){duty_percent=100U;}pwm_seq_values[channel]=(PWM_DRV_TOP_VALUE*duty_percent)/100U;/* 使用 LOOP 标志循环播放,修改后立即重新加载序列 */nrfx_pwm_simple_playback(&pwm0,&pwm_seq,1,NRFX_PWM_FLAG_LOOP);}intmain(void){pwm_driver_init();pwm_driver_set_channel(0,25);/* 通道 0:25% 占空比 */pwm_driver_set_channel(1,50);/* 通道 1:50% 占空比 */pwm_driver_set_channel(2,75);/* 通道 2:75% 占空比 */pwm_driver_set_channel(3,100);/* 通道 3:100% 占空比 */while(1){__WFE();/* PWM 外设由硬件自动输出,无需 CPU 持续干预 */}}特点:nRF52840 有 3 组 PWM 外设模块,每组最多 4 路,总计最多 12 路输出;功耗中等,是多数项目的首选方案。
3.4 选型决策流程
4. 常见问题
4.1 为什么 low_power_pwm 和 pwm_library 都只能输出 2 路?
因为这两种方案都基于定时器实现,而 nRF52840 的每个定时器(无论是低功耗还是硬件定时器)在标准配置下最多只能产生 2 路比较输出。
4.2 需要 8 路 PWM 该如何实现?
可以使用 2 组 PWM 外设模块(每组 4 路),或 1 组 PWM 模块 + 1 个定时器组合。注意引脚分配和模块资源冲突。
4.3 低功耗模式下 PWM 还能工作吗?
low_power_pwm 基于低功耗定时器,在 System ON 低功耗模式下仍可运行;而 pwm_library 依赖的高频硬件定时器通常需要更高的工作模式,功耗随之上升。
4.4 PWM 输出异常排查
在实际调试中,PWM 输出异常大多来自硬件配置、时钟设置或更新流程。下面列出三类高频问题及排查步骤。
1. 引脚冲突
现象:波形输出到错误引脚、多路 PWM 互相干扰,或 GPIO 状态异常。
排查步骤:
- 检查目标引脚是否被其他外设复用,例如 SPI、UART、I2C、QSPI,确认对应
PSEL寄存器配置。 - 对照开发板原理图,确认代码中的引脚号与实际物理连接一致。
- 多路 PWM 同时使用时,确认各通道没有重复占用同一个比较输出或同一个 GPIO。
代码检查点:
// 检查引脚是否已被外设占用if(NRF_P0->PIN_CNF[PWM_PIN]&(GPIO_PIN_CNF_MCUSEL_Msk)){// 该引脚已被外设占用,需要释放或更换引脚}2. 时钟配置错误
现象:PWM 频率偏离预期,波形抖动,或 low_power_pwm 输出不稳定。
排查步骤:
- 确认当前方案使用的时钟源:
pwm_driver依赖 HFCLK,low_power_pwm依赖 LFCLK。 - 检查是否已启动对应时钟,并等待启动完成事件,避免在时钟稳定前配置 PWM。
- 若使用内部 RC 振荡器作为 LFCLK,注意校准时钟精度,否则低频输出可能漂移。
代码检查点:
// 启动 LFCLK 并等待稳定NRF_CLOCK->EVENTS_LFCLKSTARTED=0;NRF_CLOCK->TASKS_LFCLKSTART=1;while(!NRF_CLOCK->EVENTS_LFCLKSTARTED){}3. 占空比设置无效
现象:写入占空比后没有变化,输出一直为 0 或 100%,或占空比与设定值不符。
排查步骤:
- 确认占空比单位:不同方案可能使用百分比、0-255、0-4095 等不同量程。
- 检查是否在 PWM 启动后才修改比较值,或修改后未触发对应的更新任务。
- 使用
pwm_driver时,确认top值设置合理,实际占空比由compare/top计算得出。
代码检查点:
// 修改占空比后,确保触发 PWM 外设更新if(NRF_PWM0->EVENTS_SEQEND[0]){NRF_PWM0->EVENTS_SEQEND[0]=0;NRF_PWM0->TASKS_SEQSTART[0]=1;}小结:先确认硬件连接与引脚复用,再核对时钟源是否启动,最后检查占空比量程与更新触发条件。多数 PWM 输出异常都可以沿这三条路径快速定位。
5. 总结
nRF52840 的三种 PWM 方案各有侧重:low_power_pwm适合功耗敏感、通道少的场景;pwm_library适合高频高精度控制;pwm_driver凭借 3 组模块、每组 4 路的资源,在通道数与功耗之间取得最佳平衡。实际项目中,建议优先评估通道数量与功耗预算,再结合实时性要求做出选择。