最近有个项目,需要通过NRF52832的IO实时输出不同频率的方波来驱动蜂鸣器,但是因为NRF52832自带的硬件PWM初始化时就决定了频率,动态可调的只有占空比,如果需要调整频率需要重新初始化PWM,故选择了通过硬件Timer通过PPI与GPIO连接来输出方波的方式达成这个目地.

首先,介绍下NRF52的PPI:

1.概念
PPI是可编程外设互连(Programmable Peripheral Interconnect)的缩写
2.作用
提供一个硬件通道,将不同外设的事件和任务"连接"在一起,当事件产生时硬件自动触发事件"连接"的任务
3.优点
PPI机制的设计,使得被"连接"的任务由硬件自动触发完成,省去了原本CPU需要参与的步骤。这一方面降低了CPU的负荷,另一方面保证了产生事件到执行任务的实时性。

4.实现原理
1)nRF52832共有32个通道(编号为0~31),其中,有12个通道(通道20~31)已经被预编程,剩余20个通道(通道0~19)是用户可编程的;
2)每个PPI通道由3个端点寄存器组成,1个EEP(Event End-Point:事件端点)和2个TEP(Task End-Point:任务端点,次级任务端点)寄存器;
3)任务端点和次级任务端点同时被触发;
4)使用PPI连接外设事件和外设任务的时候,将外设事件寄存器的地址写入到PPI通道的EEP,将外设任务寄存器的地址写入到PPI通道的TEP,最后使能该PPI通道实现事件和任务间的连接;

5.PPI组
nRF52832共用6个PPI组CHG[0]~CHG[5],PPI通道可以加入PPI组中集中管理,实现同时使能或禁用PPI通道的功能。
注意:当一个PPI通道同时属于两个PPI组时,如果使能一个PPI组禁用另一个PPI组,则使能作用优先执行。

这个功能需要include的几个头文件:

  • nrf_drv_timer.h
  • nrf_drv_ppi.h
  • nrf_drv_gpiote.h

相关的初始化代码:

const nrf_drv_timer_t TIMER_Audio = NRF_DRV_TIMER_INSTANCE(3);
    nrf_ppi_channel_t PpiChannel;
    uint32_t CompareEvtAddr;
    uint32_t GpioteTaskAddr;
    ret_code_t ErrCode = NRF_SUCCESS;

    // 配置Timer
    nrf_drv_timer_config_t TimerCfg = NRF_DRV_TIMER_DEFAULT_CONFIG;
    TimerCfg.frequency = NRF_TIMER_FREQ_16MHz;
    TimerCfg.mode = NRF_TIMER_MODE_TIMER;
    TimerCfg.bit_width = NRF_TIMER_BIT_WIDTH_16;
    TimerCfg.interrupt_priority = APP_IRQ_PRIORITY_LOWEST;

    // 初始化Timer
    ErrCode = nrf_drv_timer_init(&TIMER_Audio, &TimerCfg, timer_audio_event_handler);
    APP_ERROR_CHECK(ErrCode);

    // 将IO配置成输出任务为翻转IO,默认低电平
    nrf_drv_gpiote_out_config_t gpiote_config = GPIOTE_CONFIG_OUT_TASK_TOGGLE(false);
    ErrCode = nrf_drv_gpiote_out_init(SINGAL_DIN_PIN, &gpiote_config);
    APP_ERROR_CHECK(ErrCode);
    
    // 申请一个PPI通道
    ErrCode = nrf_drv_ppi_channel_alloc(&PpiChannel);
    APP_ERROR_CHECK(ErrCode);

    // 获取Timer的事件地址
    CompareEvtAddr = nrf_drv_timer_event_address_get(&TIMER_Audio, NRF_TIMER_EVENT_COMPARE0);

    // 获取刚刚设置的那个PIN的输出任务地址
    GpioteTaskAddr = nrf_drv_gpiote_out_task_addr_get(SINGAL_DIN_PIN);

    // 通过申请的PPI通道连接刚刚的事件地址与任务地址
    ErrCode = nrf_drv_ppi_channel_assign(PpiChannel, CompareEvtAddr, GpioteTaskAddr);
    APP_ERROR_CHECK(ErrCode);

    // 使能PPI通道
    ErrCode = nrf_drv_ppi_channel_enable(PpiChannel);
    APP_ERROR_CHECK(ErrCode);

    // 使能GPIOTE的输出任务
    nrf_drv_gpiote_out_task_enable(SINGAL_DIN_PIN);

在播放音频过程中如需要修改频率,则可以调用以下函数:

void audio_tone(uint16_t usFreq)
 {
     if (usFreq)
     {
         // 设置Timer的比较值,设置不使能中断
         nrf_drv_timer_extended_compare(&TIMER_Audio, NRF_TIMER_CC_CHANNEL0, nrf_drv_timer_us_to_ticks(&TIMER_Audio, (1000 * 1000 / usFreq / 2)),NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK, false);
         // 使能定时器
         if (nrf_drv_timer_is_enabled(&TIMER_Audio) == false)
         {
             nrf_drv_timer_enable(&TIMER_Audio);
         }
     }
     else
     {
         // 停止PWM
         nrf_drv_timer_disable(&TIMER_Audio);
     }
 }


一个电子工程师的自我修养