织女星开发板RISC-V内核实现微秒级精确延时

织女星开发板使用

Posted by Wang Chao on June 28, 2019

前言

收到VEGA织女星开发板也有一段时间了,好久没玩了,想驱动个OLED屏,但是首先要实现IIC协议,而实现IIC协议,最基本的就是需要一个精确的延时函数,所以研究了一下如何来写一个精确的延时函数。众所周知,ARM Cortex-M内核都有一个24位的SysTick系统节拍定时器,它是一个简易的周期定时器,用于提供时基,多为操作系统所使用。RV32M1的RISC-V内核RI5CY也有一个SysTick定时器,只不过它不属于内核,而是使用的一个外部通用定时器,即LPIT0( low power periodic interval timer)定时器的通道0来实现的。

system_RV32M1_ri5cy.c文件的SysTick定时器:


/* Use LIPT0 channel 0 for systick. */
#define SYSTICK_LPIT LPIT0
#define SYSTICK_LPIT_CH 0
#define SYSTICK_LPIT_IRQn LPIT0_IRQn

system_RV32M1_ri5cy.h文件中的SysTick中断服务函数:


#define SysTick_Handler LPIT0_IRQHandler

system_RV32M1_zero_riscy.c文件中的SysTick定时:


/* Use LIPT1 channel 0 for systick. */
#define SYSTICK_LPIT LPIT1
#define SYSTICK_LPIT_CH 0
#define SYSTICK_LPIT_IRQn LPIT1_IRQn

system_RV32M1_zero_riscy.h文件中的SysTick中断服务函数:

/
#define SysTick_Handler LPIT1_IRQHandler

关于LPIT0

LPIT0的每个通道都包含一个32位的计数器,加载计数值之后开始倒数,当倒数到0时,中断标志位被置1,通过向中断标志位写1来清除中断。为了尽量减少执行函数所消耗的时间,delay延时函数的采用了直接操作寄存器的方式来实现。通过阅读RV32M1参考手册【Chapter 50 Low Power Interrupt Timer (LPIT)】章节,和fsl_lpit.h库函数的实现,我们可以了解到以下几个寄存器的功能:

寄存器名称 全称 读/写 含义
TVAL Timer Value Register 读/写 设置定时器初值
CVAL Current Timer Value 只读 获取当前计数值
SETEN Set Timer Enable Register 读写 定时器使能控制
CLRTEN Clear Timer Enable Register 只写 清除计数值
MCR Module Control Register 读写 定时器时钟使能控制
MSR Module Status Register 读写 溢出中断标志,写1清除中断

通过上面参考手册相关寄存器的介绍,我们有两种方式来获取定时器是否溢出:

  • 获取当前计数值是否为0,即CVAL寄存器的值
  • 获取寄存器状态是否溢出,即MSR寄存器的值。

这几个寄存器都是32位的,具体每一位的含义,可以查阅RV32M1参考手册

ZERO核的SysTick定时器

虽然同样是属于RISC-V内核,ZERO核与RI5CY核使用的SysTick定时器不同,

  • ZERO : LPIT1_CH0
  • RI5CY: LPIT0_CH0

可以通过预编译指令来进行条件编译,官方的Demo工程通过使用不同的宏定义来区分两个工程。

  • ZERO核宏定义:CPU_RV32M1_zero_riscy
  • RI5CY核宏定义:CPU_RV32M1_ri5cy

delay.c文件


#include "delay.h"

/*
 * ZERO : LPIT1_CH0
 * RI5CY: LPIT0_CH0
 * */

static uint8_t  fac_us=0;
static uint16_t fac_ms=0;

#if defined(CPU_RV32M1_zero_riscy)

/*
 * RISC_V ZERO 使用 LPIT1_CH0作为SysTick,与RI5CY不同
 * */

void Delay_Init(void)
{
	CLOCK_SetIpSrc(kCLOCK_Lpit1, kCLOCK_IpSrcFircAsync);	//设置定时器时钟48MHz
	LOG("LPIT1时钟: %ld \r\n", CLOCK_GetIpFreq(kCLOCK_Lpit1));	//输出LPIT0时钟

	CLOCK_EnableClock(kCLOCK_Lpit1);	//使能时钟
	LPIT_Reset(LPIT1);					//复位定时器
	LPIT1->MCR = LPIT_MCR_M_CEN_MASK;	//使能定时器

	fac_us = CLOCK_GetIpFreq(kCLOCK_Lpit1)/1000000;
	fac_ms = fac_us*1000;
}

void Delay_us(uint32_t Nus)
{
	LPIT1->CHANNEL[kLPIT_Chnl_0].TVAL =  48 * Nus - 1;					//加载时间
	LPIT1->SETTEN |= (LPIT_SETTEN_SET_T_EN_0_MASK << kLPIT_Chnl_0);		//启动定时器
	while(LPIT1->CHANNEL[kLPIT_Chnl_0].CVAL);							//等待计数值到0
//	while((LPIT1->MSR & 0x0001) != 0x01);								//等待溢出
//	LPIT0->MSR |= (1U << kLPIT_Chnl_0);									//写1,清除中断
	LPIT1->CLRTEN |= (LPIT_CLRTEN_CLR_T_EN_0_MASK << kLPIT_Chnl_0);		//清除计数器
}

void Delay_ms(uint32_t Nms)
{
	LPIT1->CHANNEL[kLPIT_Chnl_0].TVAL = Nms * fac_ms  - 1;			//加载时间
	LPIT1->SETTEN |= (LPIT_SETTEN_SET_T_EN_0_MASK << kLPIT_Chnl_0);	//启动定时器
	while(LPIT1->CHANNEL[kLPIT_Chnl_0].CVAL);						//等待计数到0
//	while((LPIT1->MSR & 0x0001) != 0x0001);							//等待产生中断
//	LPIT0->MSR |= (1U << kLPIT_Chnl_0);								//向中断标志位写1,清除中断
	LPIT1->CLRTEN |= (LPIT_CLRTEN_CLR_T_EN_0_MASK << kLPIT_Chnl_0);	//清除计数器
}

#elif defined(CPU_RV32M1_ri5cy)

/*
 * RISC_V RI5CY 使用 LPIT0_CH0作为SysTick,与ZERO不同
 * */

void Delay_Init(void)
{
	CLOCK_SetIpSrc(kCLOCK_Lpit0, kCLOCK_IpSrcFircAsync);	//设置定时器时钟48MHz
	LOG("LPIT0时钟: %ld \r\n", CLOCK_GetIpFreq(kCLOCK_Lpit0));	//输出LPIT0时钟

	CLOCK_EnableClock(kCLOCK_Lpit0);	//使能时钟
	LPIT_Reset(LPIT0);					//复位定时器
	LPIT0->MCR = LPIT_MCR_M_CEN_MASK;	//使能定时器

	fac_us = CLOCK_GetIpFreq(kCLOCK_Lpit0)/1000000;
	fac_ms = fac_us*1000;
}

void Delay_us(uint32_t Nus)
{
	LPIT0->CHANNEL[kLPIT_Chnl_0].TVAL =  48 * Nus - 1;					//加载时间
	LPIT0->SETTEN |= (LPIT_SETTEN_SET_T_EN_0_MASK << kLPIT_Chnl_0);		//启动定时器
	while(LPIT0->CHANNEL[kLPIT_Chnl_0].CVAL);							//等待计数值到0
//	while((LPIT0->MSR & 0x0001) != 0x01);								//等待溢出
//	LPIT0->MSR |= (1U << kLPIT_Chnl_0);									//写1,清除中断
	LPIT0->CLRTEN |= (LPIT_CLRTEN_CLR_T_EN_0_MASK << kLPIT_Chnl_0);		//清除计数器
}

void Delay_ms(uint32_t Nms)
{
	LPIT0->CHANNEL[kLPIT_Chnl_0].TVAL = Nms * fac_ms  - 1;			//加载时间
	LPIT0->SETTEN |= (LPIT_SETTEN_SET_T_EN_0_MASK << kLPIT_Chnl_0);	//启动定时器
	while(LPIT0->CHANNEL[kLPIT_Chnl_0].CVAL);						//等待计数到0
//	while((LPIT0->MSR & 0x0001) != 0x0001);							//等待产生中断
//	LPIT0->MSR |= (1U << kLPIT_Chnl_0);								//向中断标志位写1,清除中断
	LPIT0->CLRTEN |= (LPIT_CLRTEN_CLR_T_EN_0_MASK << kLPIT_Chnl_0);	//清除计数器
}

#endif

delay.h文件

#ifndef __DELAY_H__
#define __DELAY_H__

#include "fsl_lpit.h"
#include "fsl_debug_console.h"
#include "sys.h"

/*
 * ZERO : LPIT1_CH0
 * RI5CY: LPIT0_CH0
 * */

void Delay_Init(void);
void Delay_ms(uint32_t Nms);
void Delay_us(uint32_t Nus);

#endif

实际验证


...

#include "delay.h"
...

int main(void)
{
    ...
    Delay_Init();
    ...
    while(1)
    {
        GPIOA->PTOR = 1 << 24;	//寄存器方式操作,减小误差
        Delay_ms(100);	//延时微秒函数验证
//  	Delay_us(5);	//延时微秒函数验证              
    }
}


通过实际示波器测试,发现Delay_us微秒级延时函数,无论延时多少时间都有2us左右的误差,不知道是这为什么,不过实现IIC协议驱动OLED屏并没有什么影响。

驱动IIC接口OLED

  • 社区首页的LOGO图片

  • OLED实际显示效果:

总结

既然精确延时函数实现了,那么各种协议的传感器、显示模块、通信模块的驱动都可以轻松实现了,希望分享的本篇帖子能给社区的朋友一些帮助,也希望大家能多多发帖,互相交流学习。

参考资料

历史精选


  • 个人博客:www.wangchaochao.top
  • 微信公众号:mcu149