良许Linux教程网 干货合集 RTOS 上微秒级延时方案

RTOS 上微秒级延时方案

通常情况下,实时操作系统(RTOS)的系统滴答频率为1KHz,当然也可以是100Hz或者10KHz。

在1KHz的情况下,系统的最小延时为1ms。然而,在实时控制中,有些情况需要微秒级(us)的延时,那么应该怎么办呢?

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

一、提高系统时钟频率

提高系统时钟频率可以实现更短的单位时间内线程调度次数,即调度频率增加。然而,过快的调度频率会导致线程调度时间的增加,对线程的功能产生不利影响。线程函数才是CPU真正需要处理的任务,如果CPU能够说话,过快的线程调度会引起CPU的不满。线程是CPU具体要做的事情,刚把CPU调过来做事情,还没做完就转去做其他事情,CPU会说:“傻瓜,你疯了吗?我不是被调度过来做事情的吗?为什么总是让我中途跳转到其他任务上去,难道不能等我把任务做完再切换吗?!”

二、使用MCU的高精度定时器

一般来说,MCU上都会有高精度定时器外设,可配置到1us的精度。既然定时器可以实现微秒级延时,那就直接使用定时器就好了,为什么还要写这个文章呢?当然,并不只是简单地启动定时器就可以。RTOS所要实现的是阻塞式延时,即任务在进入延时时要交出CPU使用权进入阻塞状态。在RTOS上,通过定时器进行死等是不可取的行为,只有通过让权睡眠才能实现良好的多线程调度。

尽管微秒级延时时间很短,在一个线程处于延时状态时,另一个线程也许不会立即开始延时。然而,在多线程的情况下,延时仍然可能出现重入的情况。例如,一个线程要延时500us,刚好过了100us,另一个线程也要延时200us,这种情况不仅发生了重入,还出现了”时间覆盖”(200us覆盖了上一个线程剩余的400us的时间段)。这些情况并不能仅凭一个硬件高精度定时器来处理。

多线程延时工况分析

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

image-20231022220320842
image-20231022220320842

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

image-20231022220328590
image-20231022220328590

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

图中的 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 = (nCNT = 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\r\n", get_timestamp_us() - now);
    }
}

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

时间粒度测试

image-20231022220444153
image-20231022220444153
image-20231022220452155
image-20231022220452155

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

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

image-20231022220500310
image-20231022220500310

以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !

137e00002230ad9f26e78-265x300
本文由 良许Linux教程网 发布,可自由转载、引用,但需署名作者且注明文章出处。如转载至微信公众号,请在文末添加作者公众号二维码。
良许

作者: 良许

良许,世界500强企业Linux开发工程师,公众号【良许Linux】的作者,全网拥有超30W粉丝。个人标签:创业者,CSDN学院讲师,副业达人,流量玩家,摄影爱好者。
上一篇
下一篇

发表评论

联系我们

联系我们

公众号:良许Linux

在线咨询: QQ交谈

邮箱: yychuyu@163.com

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部