一. 简介
微秒级延时在许多通过软件实现的自定义通信协议中具有不可或缺的作用,本文介绍使用 S32K3 的 PIT 和内部 Systick 模块实现微妙级别的延时函数实现。
二. PIT 实现延时函数
定义如下结构体类型,在 pit_config.h 中:
#ifndef __PIT_CONFIG
#define __PIT_CONFIG
#include "Pit_Ip.h"
#define PIT_DELAY 1
#define ENABEL_PIT_INT 0
#define likely(x) __builtin_expect(!!(x), 1)
#define unlikely(x) __builtin_expect(!!(x), 0)
typedef struct
{
uint32 pit_num;
uint32 pit_channel;
uint32 pit_period;
uint32 delay_count;
uint32 delay_time;
}pit_config_t;
void pit_init(volatile pit_config_t * pit_config);
#if PIT_DELAY
void pit_delay_us(uint32 time_us);
#endif
uint16 Siul2_Dio_Ip_Rev_Bit_16_self(uint16 value);
#endif
结构体初始化函数如下,用于设定 PIT 初始化,开启 PIT 计时。在 pit_config.c 中:
#include "pit_config.h"
void pit_get_default_config(volatile pit_config_t * pit_config)
{
pit_config->pit_num = 0U;
pit_config->pit_channel = 0U;
pit_config->pit_period = 40; /* equivalent to 1us */
pit_config->delay_count = 0U;
pit_config->delay_time = 1U;
}
void pit_init(volatile pit_config_t * pit_config)
{
pit_get_default_config(pit_config);
switch(pit_config->pit_num)
{
case 0:
/* Initialize PIT instance 0 - Channel 0 */
Pit_Ip_Init(pit_config->pit_num, &PIT_0_InitConfig_PB_myVS);
#if ENABEL_PIT_INT
/* set PIT 0 interrupt */
IntCtrl_Ip_Init(&IntCtrlConfig_0);
IntCtrl_Ip_EnableIrq(PIT0_IRQn);
#endif
/* Initialize channel 0 */
Pit_Ip_InitChannel(pit_config->pit_num, PIT_0_CH_0);
break;
default: break;
}
/* Enable channel interrupt */
#if ENABEL_PIT_INT
Pit_Ip_EnableChannelInterrupt(pit_config->pit_num, pit_config->pit_channel);
#endif
/* Start channel*/
Pit_Ip_StartChannel(pit_config->pit_num, pit_config->pit_channel, pit_config->pit_period);
}
/* Reverse bit order in each halfword independently */
uint16 Siul2_Dio_Ip_Rev_Bit_16_self(uint16 value)
{
uint8 i;
uint16 ret = 0U;
for (i = 0U; i < 8U; i++)
{
ret |= (uint16)((((value >> i) & 1U) << (15U - i)) | (((value << i) & 0x8000U) >> (15U - i)));
}
return ret;
}
#if PIT_DELAY
void pit_delay_us(uint32 time_us)
{
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL &= ~0x1; //100ns~150ns
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL |= 0x1; //100ns~150ns
/*40Mhz means a clock cycle of 25ns. 1000000000/25=40000000 equal to 1s*/
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].LDVAL = (40U*time_us - 21U); //50ns
__asm("DSB");
/*below two ways seem to be the seem effort*/
#if 1
while(1)
{
if(unlikely(!((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].CVAL))
{
break;
}
}
#else
while(((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].CVAL);
#endif
}
#endif
PIT 延时函数如下,通过直接读写寄存器可以实现快速精确的定时设置:
① 关闭定时;
② 重新开启定时;
③ 通过设定 LDVAL 的重装载值,设置 PIT 的定时时间;
④ 通过 while 等待 PIT 计数值为 0。
#if PIT_DELAY
void pit_delay_us(uint32 time_us)
{
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL &= ~0x1; //100ns~150ns
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL |= 0x1; //100ns~150ns
/*40Mhz means a clock cycle of 25ns. 1000000000/25=40000000 equal to 1s*/
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].LDVAL = (40U*time_us - 21U); //50ns
__asm("DSB");
/*below two ways seem to be the seem effort*/
#if 1
while(1)
{
if(unlikely(!((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].CVAL))
{
break;
}
}
#else
while(((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].CVAL);
#endif
}
#endif
三. Systick 实现延时
-
通过调用 SDK 底层驱动实现:
外设配置如下:
延时函数如下:
void OsIfDelay(uint32 timeoutUs)
{
uint32 curTime = 0u;
uint32 endTime = 0u;
uint32 timeoutCnt = 0u;
curTime = OsIf_GetCounter(OSIF_COUNTER_SYSTEM);
timeoutCnt = OsIf_MicrosToTicks(timeoutUs * 1u, OSIF_COUNTER_SYSTEM);
while(1)
{
endTime += OsIf_GetElapsed(&curTime, OSIF_COUNTER_SYSTEM);
if(timeoutCnt <= endTime)
{
break;
}
}
}
-
通过重新配置 Systick 寄存器实现:
void sys_tick_config(uint32 interval)
{
uint32_t sysfreq = 160000000U;
uint32_t reload_vlaue = 0;
reload_vlaue = (sysfreq / 1000000 * interval) ; // SystemCoreClock = 500000000UL
if ((reload_vlaue - 1UL) > 0xFFFFFFUL)
{
reload_vlaue = 0xFFFFFFUL;
}
S32_SysTick->RVR = (reload_vlaue & 0xFFFFFFUL) - 1;
NVIC_SetPriority(SysTick_IRQn, (1<<4) - 1);
S32_SysTick->CVR = 0;
S32_SysTick->CSRr |= S32_SysTick_CSR_TICKINT(0)
| S32_SysTick_CSR_ENABLE(1)
| S32_SysTick_CSR_CLKSOURCE(1);//禁用 SysTick 中断,使能 SysTick,时钟源选择内核时钟
}
void sys_delay_us( __IO uint32_t us) //us 1 per 10us
{
S32_SysTick->CSRr |= 1;
uint32_t i;
for (i=0; i<us; i++)
{
while ( !((S32_SysTick->CSRr)&(1<<16)) );
}
S32_SysTick->CSRr &=~ 1;
}
四. 应用代码
#include "Pit_Ip.h"
#include "Clock_Ip.h"
#include "IntCtrl_Ip.h"
#include "Siul2_Port_Ip.h"
#include "Siul2_Dio_Ip.h"
#include "nvic.h"
#define SYS_DELAY 0
#define SYS_OS_DELAY 0
#define SUSPEND_TEST 0
#define FOR_LOOP_TEST 0
volatile uint32 pit_config_mem[5] = {0}; /*array to create the space of struct*/
/**
* @brief Main function of the example
* @details Initialize the used drivers and uses the Pit
* and Dio drivers to toggle a LED periodically
*/
int main (void)
{
uint32 led_pin = 0;
/* Initialize Clock */
Clock_Ip_Init(&Clock_Ip_aClockConfig[0]);
/* Initialize Pin */
Siul2_Port_Ip_Init(NUM_OF_CONFIGURED_PINS_PortContainer_0_BOARD_InitPeripherals, g_pin_mux_InitConfigArr_PortContainer_0_BOARD_InitPeripherals);
led_pin = Siul2_Dio_Ip_Rev_Bit_16_self((1UL << LED_PIN));
#if PIT_DELAY
pit_init((volatile pit_config_t *)pit_config_mem);
#endif
#if SYS_OS_DELAY
OsIf_Init(NULL);
#endif
#if SYS_DELAY
sys_tick_config(1);
#endif
/* Waiting for Interrupt occurred */
while (1)
{
#if SUSPEND_TEST
SuspendAllInterrupts(); /*approximately take 50ns-100ns, different with different code*/
#endif
#if FOR_LOOP_TEST
for(int i = 0; i < 2; i++) /*approximately take none time for the start of loop*/
{
#endif
LED_PORT->PGPDO ^= led_pin; //150ns~200ns
#if SYS_OS_DELAY
OsIfDelay(1000);
#endif
#if SYS_DELAY
sys_delay_us(1);
#endif
#if PIT_DELAY
pit_delay_us(1);
#if FOR_LOOP_TEST
}
#endif
#if SUSPEND_TEST
ResumeAllInterrupts(); /*approximately take 50ns-100ns, different with different code*/
#endif
#endif
}
return 0;
}
#ifdef __cplusplus
}
#endif
五. 关键问题探讨
-
Gpio 翻转问题
通过函数调用,空跑 IO 翻转时间 250kHz,无法满足测量条件。
通过 LED_PORT->PGPDO ^= led_pin; 直接写寄存器,省去了函数调用所用的时间,实测空跑翻转可以到达 2.86MHz,方可满足 1us 延时时间测量的条件。
-
中断开销
开启 PIT 中断将导致代码频繁进入中断,影响 pit_delay_us 的时间读取,因此延时函数的实现需要关闭 PIT 中断。
-
函数执行速度
函数的执行速度,受到编译器优化的影响,提高函数运行速度可以采用精简代码的手段对代码进行优化,优化的 pit_delay_us 函数可以做到较 systick 更加精确的 us 级别的延时。
在执行定时之前需要执行如下的三条语句,三条语句耗时 500ns:
LED_PORT->PGPDO ^= led_pin; //150ns~200ns
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL &= ~0x1; //100ns~150ns
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].TCTRL |= 0x1; //100ns~150ns
在设定定时时间时要减去 20 * 25 ns = 500 ns 的偏差,此偏差消除可以根据需要更改。
/*40Mhz means a clock cycle of 25ns. 1000000000/25=40000000 equal to 1s*/
((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].LDVAL = (40U*time_us - 21U);
在等待计时完成时要执行一下 DSB 指令确保寄存器写入及时生效,参考 https://blog.youkuaiyun.com/tilblackout/article/details/132030663:
__asm("DSB");
在面对判断语句时通过 likely 与 unlikely 指令可优化分支预测,参考 https://zhuanlan.zhihu.com/p/357434227:
if(unlikely(!((PIT_Type *)IP_PIT_0_BASE)->TIMER[0].CVAL))
{
break;
}