0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何实现RTOS上的微秒级延时设计呢?

strongerHuang 来源:MultiMCU EDU 2023-09-15 09:16 次阅读

通常RTOS系统滴答为1KHz,当然,也有100Hz,或者10KHz的情况。

1KHz时,系统延时最短为1ms,在实时控制中有些情况需要微秒(us)级延时,这该怎么办呢?

微秒级延时有两种实现思路:

一、着情提高系统时钟

二、使用 MCU高精度定时器

一、着情提高系统时钟

之所以说是“着情”提高的原因是:系统时钟越快,单位时间内的线程调度次数越多,也就是说花在调度的时间会大幅增加,这对线程的功能不利。真正做事的是线程函数,如果 CPU 会说话,过快的线程调度将会引起 CPU 的极度不满。线程是 CPU 具体要做的事,刚把 CPU 调过来做事,事没做完就拉跑做另一件事,CPU 会说:“傻瓜,疯了吗?不是让我做事的码,干嘛老是拉着我跑这跑那,就不能让我干完了再走码?!”

二、使用 MCU 片上外设定时器

一般 MCU 都会有片上高精度定时器外设,可以配置到 1us 精度。即然用定时器可以,那就用定时器呗,还写什么文章?当然不只是开启定时器这么简单,RTOS 要实现的是阻塞延时,任务进入延时要交出 CPU 使用权进入阻塞状态。在 RTOS 上用定时器躺平死等是无赖行为,睡眠让权才能实现良好的多线程调度。

虽然 us 级延时时间短,在一个线程处于延时中时另一个线程又要开始延时的情况发生概率不大。但是在多线程情况下延时依旧有可能发生重入,比如一个线程要延时 500us,刚过 100us 另一个线程就要延时 200us,这种情况不但发生了重入,还有“时间覆盖”(200us 覆盖了上一个线程剩余的 400us 里的时间段),这些情况也不是光靠一个硬件高精度定时器就能应对的。

多线程延时工况分析

先来看一张多线程延时工况图,如图所示:

3a5b5bf4-535e-11ee-a25d-92fbcf53809c.png

为了方便阅读以及接下来进一步的设计实现,在上图基础上加了一些注释,对多线程的工况进行更细致一点的描述,如图所示:

3a6829d8-535e-11ee-a25d-92fbcf53809c.png

为了更好说明,这里选用 Microsoft Azure RTOS ThreadX 做基础来实现这个设计。目的在于输出通用方法,具体选什么 RTOS 并不重要,是个多线程就行,比如:RT-ThreadFreeRTOS 等都可以。

图中的 A、B、C 和 High-precision Timer 是 4 个线程。其中 High-precision Timer 线程优先级最高,但不是定时回调的,而是被动触发。下面说说为什么 High-precision Timer 线程优先级要最高,以及如何被动触发。

我们知道线程中用 WAIT_FOREVER 方式等待信号量的时候,若信号量的值为 0 则线程会被挂起在这个信号量下。我们就利用这个特点来完成线程的“被动触发”,即:

1、信号量建立时初值为 0

2、在中断中释放一次信号量(即信号量值加 1)

这样中断发生后就能立刻唤醒挂起在该信号量下的线程,即完成了线程的被动触发。线程转为就绪态后,因其优先级最高,会立即抢占调度器得到执行。在 Hight-precision Timer 线程被信号量唤醒后,立即对延时时间到的线程进行 resume 操作,这样就完成了线程的 us 延时。

我们回看一下上面图中的 A、B、C 三个线程,每条线上都串了两个圈圈,每条线从上往下第一个圈是延时主动挂起,第二个圈是时间到后被 High-precision Timer 线程 resume 回来继续执行。

至此读图的方法基本说清楚了,如果要落实到代码,其实还有个“硬件定时器与 High-precision Timer 线程”的关系。图中标在 High-precision Timer 左边的标签是说:因为硬件定时器产生了中断,才使得 High-precision Timer 线程对延时时间到的线程进行 resume。上面说“被动触发”的时候有说到相关原理,其实上面图的最右边应该再放一列表示“硬件定时器”就更好理解原理了。没有放的原因是这里要考虑“可重入”,这个瓜有点多,一车装不下,装少了说不完善,装多了眼花缭乱,所以就没画“硬件定时器”这一列。

代码实现

为了实现上述设计的阻塞延时,代码要划分为四个部分: 一、 要配置一个 us 级定时器; 二、 要做一个 us 延时的函数接口; 三、 要有一个 High-precision Timer 线程; 四、 要有一个测试用 us 级的普通定时回调线程。 下面以 STM32 为例逐一上代码。

us 级定时器配置

1、 定时器初始化

这里直接使用 CubeMX 生成的函数最方便,一行不改,如下:


/**
  * @brief TIM9 Initialization Function
  * @param None
  * @retval None
  */
static void MX_TIM9_Init(void)
{


  /* USER CODE BEGIN TIM9_Init 0 */


  /* USER CODE END TIM9_Init 0 */


  TIM_ClockConfigTypeDef sClockSourceConfig = {0};


  /* USER CODE BEGIN TIM9_Init 1 */


  /* USER CODE END TIM9_Init 1 */
  htim9.Instance = TIM9;
  htim9.Init.Prescaler = 215;
  htim9.Init.CounterMode = TIM_COUNTERMODE_UP;
  htim9.Init.Period = 65535;
  htim9.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
  htim9.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
  if (HAL_TIM_Base_Init(&htim9) != HAL_OK)
  {
    Error_Handler();
  }
  sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
  if (HAL_TIM_ConfigClockSource(&htim9, &sClockSourceConfig) != HAL_OK)
  {
    Error_Handler();
  }
  /* USER CODE BEGIN TIM9_Init 2 */


  /* USER CODE END TIM9_Init 2 */


}

由于我们要使用定时器的定时中断,所以要对 NVIC 设置一下,这部分代码 CubeMX 生成在另一个文件下,为了调用方便将之与上面的初始化函数合至一处,如下:


void bsp_InitHardTimer(void)
{
    __HAL_RCC_TIM9_CLK_ENABLE();
    HAL_NVIC_SetPriority(TIM1_BRK_TIM9_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(TIM1_BRK_TIM9_IRQn);
    MX_TIM9_Init();
}

注意,这里调到初始化函数就完了,不要开启定时器,按照设计定时器是需要延时的线程在调用延时函数时才打开的。

2、 打开定时器的函数


void bsp_DelayUS(uint32_t n)
{
    n = (n<=30) ? n : (n-30);
    HAL_TIM_Base_Stop_IT(&htim9);
    htim9.Instance->CNT = htim9.Init.Period - n;
    HAL_TIM_Base_Start_IT(&htim9);
}

这里注意是“先关闭再打开”,上面提到了“时间覆盖”的情况下做延时,就必须先关闭正在延时中的定时器。

3、 定时器中断函数


/**
  * @brief This function handles TIM1 break interrupt and TIM9 global interrupt.
  */
void TIM1_BRK_TIM9_IRQHandler(void)
{
  /* USER CODE BEGIN TIM1_BRK_TIM9_IRQn 0 */


  /* USER CODE END TIM1_BRK_TIM9_IRQn 0 */
  HAL_TIM_IRQHandler(&htim9);
  /* USER CODE BEGIN TIM1_BRK_TIM9_IRQn 1 */
  tx_semaphore_put(&tx_semaphore_delay_us);
  HAL_TIM_Base_Stop_IT(&htim9);
  /* USER CODE END TIM1_BRK_TIM9_IRQn 1 */
}

这里调用了 Microsoft Azure RTOS ThreadX 释放信号量的 API tx_semaphore_put(),信号量在初始化时建立(省略了建立信号量的代码)。

us 延时的函数接口


TX_THREAD       *thread_delay_us;


UINT  tx_thread_sleep_us(ULONG timer_ticks)
{
    TX_THREAD_GET_CURRENT(thread_delay_us)
    bsp_DelayUS(timer_ticks); 
    tx_thread_suspend(thread_delay_us);
    return TX_SUCCESS;
}

这里定义了一个全局变量 thread_delay_us,用 TX_THREAD_GET_CURRENT() 获取调用 us 延时的线程,在打开定时器后将线程通过 tx_thread_suspend() 挂起。

High-precision Timer 线程


extern TX_THREAD*      thread_delay_us;


UINT status;
void threadx_task_delay_us_run(ULONG thread_input)
{
    (void)thread_input;


    while(1){
        tx_semaphore_get(&tx_semaphore_delay_us, TX_WAIT_FOREVER);
        if(thread_delay_us){
            status = tx_thread_resume(thread_delay_us);
        }
    }
}

这里同样省略了线程的建立过程,给出了线程主体:与信号量 tx_semaphore_delay_us 一起完成线程的被动触发,以及对 thread_delay_us 线程的 resume。

测试用 us 级的普通定时回调线程


#include "pthread.h"


VOID    *pthread_test_entry(VOID *pthread1_input)
{
    while(1) 
    {
        //print_task_information();
        uint64_t now = get_timestamp_us();
        tx_thread_sleep_us(100);
        printf("delay_us: %lld
", get_timestamp_us() - now);
    }
}

这是以 posix 接口 API 建立的线程,对 posix 有兴趣的可以看下文章《Azure RTOS ThreadX 的 posix 接口》。

时间粒度测试

3a8002a6-535e-11ee-a25d-92fbcf53809c.png

3aa0f402-535e-11ee-a25d-92fbcf53809c.png

ThreadX 据说可以在 200MHz 的 MCU 上达到亚微秒级的上下文切换,Sugar 测试的时间粒度在 150us 时比较稳定。这并不是说 ThreadX 性能不好,而是 STM32F7 定时器一开加一关大约就要 30us,所以定时精度比 30us 更小时不要开关定时器,但这次我们的设计为了应对可能发生的重入情况,必须有定时器的开关才行。

怎么知道一开加一关要 30us 的,原因如图:

3ab84814-535e-11ee-a25d-92fbcf53809c.png







审核编辑:刘清

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • STM32
    +关注

    关注

    2240

    文章

    10674

    浏览量

    348809
  • 定时器
    +关注

    关注

    23

    文章

    3148

    浏览量

    112046
  • 触发器
    +关注

    关注

    14

    文章

    1681

    浏览量

    60409
  • RTOS
    +关注

    关注

    20

    文章

    776

    浏览量

    118798
  • 定时器中断
    +关注

    关注

    0

    文章

    46

    浏览量

    11026

原文标题:RTOS 上微秒级延时方案

文章出处:【微信号:strongerHuang,微信公众号:strongerHuang】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    微秒us延时指令

    怎么做微秒us延时
    发表于 12-26 20:37

    【STM32L476 Nucleo试用体验】SysTick微秒延时函数的实现

    延时函数不采用和微秒函数相同的实现方法,是因为毫秒延时有的时候
    发表于 10-03 11:51

    请问stm32 ns延时程序怎么实现

    纳秒的stm32延时程序应该怎么实现
    发表于 03-13 06:31

    请问怎么在ucosII中实现微秒延时

    1、ucosii中怎么实现微秒延时,OSTimeDly();对ticks进行计数,我的计数是1ms一次,OSTimeDlyHMSM()
    发表于 07-01 04:35

    STM32 HAL库微秒延时

    STM32HAL库微秒(μs)延时
    发表于 08-24 07:19

    在MCU编程中STM32延时函数如何去实现

    【STM32笔记】[sub]STM32 延时函数的实现在MCU编程中,微秒延时和毫秒延时使用最为频繁,在
    发表于 08-24 07:55

    如何去实现STM32定时器US延时

    STM32定时器可分为哪几类?STM32定时器的结构是由哪些部分组成的?如何去实现STM32定时器US延时
    发表于 11-09 06:30

    如何用SysTick系统定时器写一个微秒延时函数

    SysTick是什么意思?SysTick系统定时器有哪些作用?如何用SysTick系统定时器写一个微秒延时函数
    发表于 11-24 07:15

    如何利用SysTick实现微秒延时函数?

    怎么实现STM32 HAL库微秒延时函数? 如何利用SysTick实现微秒延时函数?
    发表于 11-25 08:06

    怎么实现STM32CubeIDE HAL库微秒us的延时Delay?

    怎么实现STM32CubeIDE HAL库微秒us的延时Delay?
    发表于 11-25 07:40

    HAL库微秒延时实现

    目录前言一、代码和使用二、使用和验证1.引入头文件2.初始化3.使用和验证总结前言接触HAL库差不多两年了,一直苦于HAL库没有自带微秒延时,网上的前辈们给出的解决方案要么是改写
    发表于 01-20 07:49

    用C语言实现,精确微秒级的延时资料下载

    电子发烧友网为你提供用C语言实现,精确微秒级的延时资料下载的电子资料下载,更有其他相关的电路图、源代码、课件教程、中文资料、英文资料、参考设计、用户指南、解决方案等资料,希望可以帮助到广大的电子工程师们。
    发表于 04-17 08:53 8次下载
    用C语言<b class='flag-5'>实现</b>,精确<b class='flag-5'>微秒</b>级的<b class='flag-5'>延时</b>资料下载

    华大HC32-(04)-微秒级us延时测试

    华大HC32-(04)-微秒级us延时测试
    发表于 11-22 19:51 10次下载
    华大HC32-(04)-<b class='flag-5'>微秒</b>级us<b class='flag-5'>延时</b>测试

    STM32HAL库微秒延时(μs)

    STM32HAL库微秒(μs)延时
    发表于 01-18 10:39 48次下载
    STM32HAL库<b class='flag-5'>微秒</b><b class='flag-5'>延时</b>(μs)

    STM32如何使用定时器实现微秒(us)级延时

    STM32如何使用定时器实现微秒(us)级延时? 在STM32微控制器中,可以使用定时器实现微秒延时
    的头像 发表于 11-06 11:05 3056次阅读