通常情况下,实时操作系统(RTOS)的系统滴答频率为1KHz,当然也可以是100Hz或者10KHz。
在1KHz的情况下,系统的最小延时为1ms。然而,在实时控制中,有些情况需要微秒级(us)的延时,那么应该怎么办呢?
有两种实现微秒级延时的思路:
一、提高系统时钟频率
提高系统时钟频率可以实现更短的单位时间内线程调度次数,即调度频率增加。然而,过快的调度频率会导致线程调度时间的增加,对线程的功能产生不利影响。线程函数才是CPU真正需要处理的任务,如果CPU能够说话,过快的线程调度会引起CPU的不满。线程是CPU具体要做的事情,刚把CPU调过来做事情,还没做完就转去做其他事情,CPU会说:“傻瓜,你疯了吗?我不是被调度过来做事情的吗?为什么总是让我中途跳转到其他任务上去,难道不能等我把任务做完再切换吗?!”
二、使用MCU的高精度定时器
一般来说,MCU上都会有高精度定时器外设,可配置到1us的精度。既然定时器可以实现微秒级延时,那就直接使用定时器就好了,为什么还要写这个文章呢?当然,并不只是简单地启动定时器就可以。RTOS所要实现的是阻塞式延时,即任务在进入延时时要交出CPU使用权进入阻塞状态。在RTOS上,通过定时器进行死等是不可取的行为,只有通过让权睡眠才能实现良好的多线程调度。
尽管微秒级延时时间很短,在一个线程处于延时状态时,另一个线程也许不会立即开始延时。然而,在多线程的情况下,延时仍然可能出现重入的情况。例如,一个线程要延时500us,刚好过了100us,另一个线程也要延时200us,这种情况不仅发生了重入,还出现了”时间覆盖”(200us覆盖了上一个线程剩余的400us的时间段)。这些情况并不能仅凭一个硬件高精度定时器来处理。
多线程延时工况分析
先来看一张多线程延时工况图,如图所示:
为了方便阅读以及接下来进一步的设计实现,在上图基础上加了一些注释,对多线程的工况进行更细致一点的描述,如图所示:
为了更好说明,这里选用 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 接口》。
时间粒度测试
ThreadX 据说可以在 200MHz 的 MCU 上达到亚微秒级的上下文切换,Sugar 测试的时间粒度在 150us 时比较稳定。这并不是说 ThreadX 性能不好,而是 STM32F7 定时器一开加一关大约就要 30us,所以定时精度比 30us 更小时不要开关定时器,但这次我们的设计为了应对可能发生的重入情况,必须有定时器的开关才行。
怎么知道一开加一关要 30us 的,原因如图:
以上就是良许教程网为各位朋友分享的Linu系统相关内容。想要了解更多Linux相关知识记得关注公众号“良许Linux”,或扫描下方二维码进行关注,更多干货等着你 !