作者:Gao Jun, Nordic Semiconductor
什么是PPI和DPPI?
PPI,英文全称是Programmable Peripheral Interconnect;
DPPI,英文全称是Distributed Programmable Peripheral Interconnect。
PPI是nRF52②系列芯片的一个外设,从nRF53③系列开始,使用DPPI代替了PPI。PPI和DPPI的设计目的是相同的,都提供了一种机制,让某个外设的事件可以触发另一个外设的任务,让一个外设直接控制另一个外设成为可能,从而减少了CPU的参与,和DMA有异曲同工之效。
比如,我们可以用timer定时器去触发一个ADC采样,这样就不需要CPU执行指令去触发采样了。所谓编程(Programmable),就是指可以通过配置寄存器,将不同外设的事件和任务建立关联。
nRF52 DK
什么是事件(event)和任务(task)?
说到PPI和DPPI就绕不过事件和任务。那么这个事件和任务又是什么呢?学习过MCU的同学都知道外设(Peripheral)都会有与之对应的控制寄存器和状态寄存器,而我们Nordic的控制寄存器和状态寄存器的设计有些与众不同。
我们将控制寄存器的控制逻辑做了细分,将每一个控制逻辑都具体成了一个任务,一个任务就对应一个任务寄存器。同样,将状态寄存器的状态也做了细分,将每一个不同的状态都具体成了一个事件,一个事件就对应一个事件寄存器。
以UARTE模块为例,它的task和event寄存器如下:
什么是通道(Channel)?
通道可以想象成一个管道,管道的输入端关联event寄存器,管道的输出端关联task寄存器。管道上有开关,当开关被打开时(channel enable),event寄存器如果有信号,就能触发管道另一头的task寄存器,从而触发task寄存器对应的控制逻辑。
PPI和DPPI的主要区别是:通道与event寄存器和task寄存器建立关联的机制不同。PPI的机制决定了一个通道只能关联一个event和一个task,即只能一对一(一个event触发一个task),利用fork机制可以做到一对二(一个event触发两个task)。
DPPI的一个通道可以和多个task和event做关联,能做到多对多(多个event触发多个task)。另外,通道还有自己的管理机制,还可以分组管理。PPI和DPPI的通道管理机制是相同的。
PPI框图
EEP是event end point的缩写;TEP是task end point的缩写。每一个通道都有自己的EEP和TEP寄存器。
CH[i].EEP是一个配置寄存器,负责存储event register 地址,使外设的event与通道i(CH[i])建立关联。
CH[i].TEP也是一个配置寄存器,负责存储task register 地址,使外设的task与通道i (CH[i])建立关联。
PPI允许一个event或task和多个通道建立关联。即不同的通道里可以写入同一个event或task地址。但一个通道只能和一个event建立关联,因为某个通道i对应的CH[i].EEP里只能写入一个地址。Task比较特殊,因为有FORK[i].TEP的存在,所以CH[i].TEP和FORK[i].TEP可以写入两个不同的task register地址,实现一对二。FORK功能只存在于PPI,DPPI没有FORK功能。
接入同一个通道的event和task可以形成通路,为event触发task逻辑创造条件。
CHEN, CHENSET, CHENCLR和CHG[m]则是用来管理这些通道(enable and disable the channels)是否使能。
0-19通道可以任意配置;20-31通道的task和event是固定的,不能配置。
PPI的EEP和TEP寄存器:
CH[n].EEP (n=0..19)
Address offset: 0x510 + (n × 0x8)
Channel n event endpoint
CH[n].TEP (n=0..19)
Address offset: 0x514 + (n × 0x8)
Channel n task endpoint
FORK[n].TEP (n=0..19, 20..31)
Address offset: 0x910 + (n × 0x4)
Channel n task endpoint
DPPI框图
所有外围设备都含有一系列的任务订阅寄存器和事件发布寄存器(每个task寄存器对应一个订阅( subscribe)寄存器;每个event寄存器对应一个发布(publish)寄存器)。(订阅和发布寄存器取代了PPI中EEP和TEP寄存器的功能)
发布和订阅的对象是通道(channel)
同一个通道可以被不同的事件发布,被不同的任务订阅,达到多对多的效果。
以下是ADC模块的task,event,subscribe task,publish event寄存器。
通道管理和组(group)
PPI和DPPI对通道的管理方式基本相同
通过通道管理寄存器,每一个通道可以被单独使能或是禁用,相关寄存器如下:
CHENSET
Address offset: 0x504
Channel enable set register
Note: Read: Reads value of CH[i] field in CHEN register
CHENCLR
Address offset: 0x508
Channel enable clear register
Note: Read: Reads value of CH[i] field in CHEN register
CHENSET和CHENCLR寄存器用于设置通道,如果要读取通道的状态,需要读取CHEN寄存器。
CHEN
Address offset: 0x500
Channel enable register
不同的通道可以分配到一个组里。一共支持6个组。CHG[n](n=0..5)寄存器有32bit,每一个bit位对应一个通道号,如果相应的bit位被置“1”,那么这个通道就被分配到了当前的组中。
CHG[n] (n=0..5)
Address offset: 0x800 + (n x 0x4)
Channel group n
Note: Writes to this register are ignored if either SUBSCRIBE_CHG[n].EN or SUBSCRIBE_CHG[n].DIS are enabled
每一个组都有两个task寄存器,一个task(TASKS_CHG[n].EN)控制组使能,另一个task (TASKS_CHG[n].DIS)控制组禁用。有了task,那么它们也可以通过PPI/DPPI的机制,被别的event触发。当TASKS_CHG[n].EN被触发,同一组内所有的通道被使能;当TASKS_CHG[n].DIS被触发,同一组内所有的通道被禁用。
TASKS_CHG[n].EN (n=0..5)
Address offset: 0x000 + (n x 0x8)
Enable channel group n
TASKS_CHG[n].DIS (n=0..5)
Address offset: 0x004 + (n x 0x8)
Disable channel group n
PPI和DPPI的驱动资源及使用流程
PPI 驱动位于modules\hal\nordic\nrfx\drivers\src\nrfx_ppi.c;DPPI驱动位于modules\hal\nordic\nrfx\drivers\src\nrfx_dppi.c
为了兼容PPI和DPPI,还设计了辅助层(Helper layer:modules\hal\nordic\nrfx\helpers\nrfx_gppi.h) 提供 PPI 和 DPPI 驱动的通用功能。辅助层直接调用PPI和DPPI的驱动程序,使用辅助层时,需要考虑到PPI 和 DPPI 接口在应用时的明显差异。
PPI和DPPI的使用流程相同,都很简单:
1)分配(申请)通道
2)对于PPI,通道两端分别关联task和event;对于DPPI,task(Subscribe)和event(Publish)端关联通道
3)使能通道
PPI和DPPI配置代码示例
1)Timer COMPARE0 EVENT产生的时候触发ADC采样。按下button1启动timer,端口输出低电平,按下button2停止timer,端口输出高电平。下面列举了PPI多通道,以及FORK使用的配置代码。
status = nrfx_ppi_channel_alloc(&m_timer_saadc_ppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
status = nrfx_ppi_channel_assign(m_timer_saadc_ppi_channel,
nrfx_timer_event_address_get(&m_sample_timer, NRF_TIMER_EVENT_COMPARE0),
nrf_saadc_task_address_get(NRF_SAADC, NRF_SAADC_TASK_SAMPLE));
NRFX_ASSERT(status == NRFX_SUCCESS);
//button1 start timer and output low
status = nrfx_ppi_channel_alloc(&m_button0_timer_ppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
status = nrfx_ppi_channel_assign(m_button0_timer_ppi_channel,
nrfx_gpiote_in_event_addr_get(INPUT_PIN_0),
nrf_timer_task_address_get(m_sample_timer.p_reg, NRF_TIMER_TASK_START));
NRFX_ASSERT(status == NRFX_SUCCESS);
status = nrfx_ppi_channel_fork_assign(m_button0_timer_ppi_channel,
nrfx_gpiote_clr_task_addr_get(OUTPUT_PIN));
NRFX_ASSERT(status == NRFX_SUCCESS);
//button2 stop timer and output high
status = nrfx_ppi_channel_alloc(&m_button1_timer_ppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
status = nrfx_ppi_channel_assign(m_button1_timer_ppi_channel,
nrfx_gpiote_in_event_addr_get(INPUT_PIN_1),
nrf_timer_task_address_get(m_sample_timer.p_reg, NRF_TIMER_TASK_STOP));
NRFX_ASSERT(status == NRFX_SUCCESS);
status = nrfx_ppi_channel_fork_assign(m_button1_timer_ppi_channel,
nrfx_gpiote_set_task_addr_get(OUTPUT_PIN));
NRFX_ASSERT(status == NRFX_SUCCESS);
2)Timer COMPARE0 EVENT产生的时候触发ADC采样。按下button1启动timer,端口输出低电平,按下button2停止timer,端口输出高电平。下面列举了DPPI多通道使用的配置代码。
status = nrfx_dppi_channel_alloc(&m_timer_saadc_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
nrf_timer_publish_set(m_sample_timer.p_reg, NRF_TIMER_EVENT_COMPARE0,
m_timer_saadc_dppi_channel);
nrf_saadc_subscribe_set(NRF_SAADC, NRF_SAADC_TASK_SAMPLE, m_timer_saadc_dppi_channel);
//button1 start timer and output low
status = nrfx_dppi_channel_alloc(&m_button0_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
nrf_gpiote_publish_set(NRF_GPIOTE, nrfx_gpiote_in_event_get(INPUT_PIN_0),
m_button0_timer_dppi_channel);
nrf_gpiote_subscribe_set(NRF_GPIOTE,nrfx_gpiote_clr_task_addr_get(OUTPUT_PIN),m_button0_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
nrf_timer_subscribe_set(m_sample_timer.p_reg, NRF_TIMER_TASK_START, m_button0_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
//button2 stop timer and output high
status = nrfx_dppi_channel_alloc(&m_button1_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
nrf_gpiote_publish_set(NRF_GPIOTE, nrfx_gpiote_in_event_get(INPUT_PIN_1),
m_button1_timer_dppi_channel);
nrf_gpiote_subscribe_set(NRF_GPIOTE,nrfx_gpiote_set_task_addr_get(OUTPUT_PIN),m_button1_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
nrf_timer_subscribe_set(m_sample_timer.p_reg, NRF_TIMER_TASK_STOP, m_button1_timer_dppi_channel);
NRFX_ASSERT(status == NRFX_SUCCESS);
3)组(group)的使用代码示例。使用timer,gpiote,dppi输出固定周期的波形。只有在COMPARE2 EVENT发生的时候,将输出电平置高;在COMPARE3 EVENT发生的时候,将输出电平置低。第一次产生COMPARE1 EVENT的时候,触发了timer clear的task, timer重新开始计数,那么COMPARE0 EVENT就不会发生了。COMPARE1 EVENT还触发了group disable task。
那么,下一个COMPARE1 EVENT就不会触发timer clear task了,因为这个channel被disable了。COMPARE1 EVENT发生后, COMPARE0 EVENT会接着发生。在COMPARE0 EVENT产生的中断处理里面,通过group的控制函数,又将channel使能了。那么,COMPARE1 EVENT,又能触发了timer clear task,循环上面的动作。输出波形如下:
Timer设置:
/*
//2 3 1 2 3 1 0 2 3 1 2 3 1 0
__ __ __ __ __ __ __ __
_| |__| |______| |__| |______| |__| |______| |__| |______
2 3 1 3 1 0 3 1 3 1 0 3 1 3 1 0 3 1 3 1 0
2 2 2 2 2 2 2
*/
static void timer_init(void)
{
nrfx_timer_config_t timer_config = NRFX_TIMER_DEFAULT_CONFIG;
timer_config.frequency = NRF_TIMER_FREQ_125kHz;
timer_config.bit_width = NRF_TIMER_BIT_WIDTH_32;
nrfx_timer_init(&timer, &timer_config, timer_handler);
nrfx_timer_compare(&timer, NRF_TIMER_CC_CHANNEL0, PERIOD, true);
nrf_timer_shorts_enable(timer.p_reg, NRF_TIMER_SHORT_COMPARE0_CLEAR_MASK);
nrfx_timer_compare(&timer, NRF_TIMER_CC_CHANNEL1, SHORT_PERIOD, true);
nrfx_timer_compare(&timer, NRF_TIMER_CC_CHANNEL2, OFFSET, false);
nrfx_timer_compare(&timer, NRF_TIMER_CC_CHANNEL3, OFFSET + PULSE_WIDTH, false);
IRQ_CONNECT(TIMER0_IRQn, NRFX_TIMER_DEFAULT_CONFIG_IRQ_PRIORITY - 1,
nrfx_timer_0_irq_handler, NULL, 0);
configure_one_time_clear();
nrfx_timer_enable(&timer);
}
//NRF_TIMER_EVENT_COMPARE1触发中断,每次会发生;
//触发task,则是一次不允许,一次允许。触发task的时候,会清timer
//timer清了COMPARE0就不会发生了。下次,task不触发时,compare0就会发生,然后使能task触发。
static void timer_handler(nrf_timer_event_t event_type, void *p_context)
{
if (event_type == NRF_TIMER_EVENT_COMPARE0) {
printk("0");
//group里面只有一个通道的情况下,下面两个函数的功能是等价的。
nrfx_dppi_group_enable(group_ch);
//nrfx_dppi_channel_enable(channel_ch);
}
if (event_type == NRF_TIMER_EVENT_COMPARE1) {
printk("1");
}
}
DPPI分配组,分配通道,关联通道配置COMPARE1 EVENT触发timer clear,加入组,配置COMPARE1 EVENT触发group disable task。然后使能通道。
static void configure_one_time_clear(void)
{
nrfx_dppi_group_alloc(&group_ch);
nrfx_dppi_channel_alloc(&channel_ch);
nrf_timer_publish_set(timer.p_reg, NRF_TIMER_EVENT_COMPARE1, channel_ch);
nrf_timer_subscribe_set(timer.p_reg, NRF_TIMER_TASK_CLEAR, channel_ch);
nrfx_dppi_channel_include_in_group(channel_ch, group_ch);
//影响下次event触发task,把通道disable了。
nrf_dppi_subscribe_set(NRF_DPPIC, nrf_dppi_group_disable_task_get(group_ch), channel_ch);
nrfx_dppi_channel_enable(channel_ch);
}
DPPI分配通道,配置COMPARE2 EVENT触发output ‘H’,使能通道;DPPI分配通道,配置COMPARE3 EVENT触发output ‘L’,使能通道。
nrfx_gpiote_out_init(OUTPUT_PIN, &out_config);
nrfx_gpiote_out_task_enable(OUTPUT_PIN);
uint8_t channel;
nrfx_dppi_channel_alloc(&channel);
nrf_timer_publish_set(timer.p_reg, NRF_TIMER_EVENT_COMPARE2, channel);
nrf_gpiote_subscribe_set(
NRF_GPIOTE,
nrfx_gpiote_set_task_get(OUTPUT_PIN),
channel);
nrfx_dppi_channel_enable(channel);
nrfx_dppi_channel_alloc(&channel);
nrf_timer_publish_set(timer.p_reg, NRF_TIMER_EVENT_COMPARE3, channel);
nrf_gpiote_subscribe_set(
NRF_GPIOTE,
nrfx_gpiote_clr_task_get(OUTPUT_PIN),
channel);
nrfx_dppi_channel_enable(channel);
中文官网:www.nordicsemi.cn
英文官网:www.nordicsemi.com
微信公众号:nordicsemi
https://devzone.nordicsemi.com
北京分公司: +86 010 8438 2767
上海分公司: +86 21 6330 0620
深圳分公司: +86 755 8322 0147
sales.cn@nordicsemi.no
点击“阅读原文”进入Nordic半导体中文官网