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

织女星开发板使用

Posted by Wang Chao on June 28, 2019

前言

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


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

/* Leverage LPIT0 to provide Systick */
void SystemSetupSystick(uint32_t tickRateHz, uint32_t intPriority)
{
    /* Init pit module */
    CLOCK_EnableClock(kCLOCK_Lpit0);

    /* Reset the timer channels and registers except the MCR register */
    SYSTICK_LPIT->MCR |= LPIT_MCR_SW_RST_MASK;
    SYSTICK_LPIT->MCR &= ~LPIT_MCR_SW_RST_MASK;

    /* Setup timer operation in debug and doze modes and enable the module */
    SYSTICK_LPIT->MCR = LPIT_MCR_DBG_EN_MASK | LPIT_MCR_DOZE_EN_MASK | LPIT_MCR_M_CEN_MASK;

    /* Set timer period for channel 0 */
    SYSTICK_LPIT->CHANNEL[SYSTICK_LPIT_CH].TVAL = (CLOCK_GetIpFreq(kCLOCK_Lpit0) / tickRateHz) - 1;

    /* Enable timer interrupts for channel 0 */
    SYSTICK_LPIT->MIER |= (1U << SYSTICK_LPIT_CH);

    /* Set interrupt priority. */
    EVENT_SetIRQPriority(SYSTICK_LPIT_IRQn, intPriority);

    /* Enable interrupt at the EVENT unit */
    EnableIRQ(SYSTICK_LPIT_IRQn);

    /* Start channel 0 */
    SYSTICK_LPIT->SETTEN |= (LPIT_SETTEN_SET_T_EN_0_MASK << SYSTICK_LPIT_CH);
}

void SystemClearSystickFlag(void)
{
    /* Channel 0. */
    SYSTICK_LPIT->MSR = (1U << SYSTICK_LPIT_CH);
}

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


#define SysTick_Handler LPIT0_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参考手册

delay.c文件


#include "delay.h"

static uint8_t  fac_us=0;							//us延时倍乘数
static uint16_t fac_ms=0;							//ms延时倍乘数,在ucos下,代表每个节拍的ms数

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;
}
//基于SysTick即LPIT0实现的延时微秒函数
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);		//清除计数器
}

//基于SysTick即LPIT0实现的延时毫秒函数
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);	//清除计数器
}

delay.h文件


#ifndef __DELAY_H__
#define __DELAY_H__

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

void Delay_Init(void);		//SysTick定时器,即LPIT0,时钟可设置
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

或微信扫码关注我的公众号