目次
最初: はじめに
一般的な組み込み製品の設計では、コスト、消費電力などの理由により、選択した MCU は基本的にリソースが制限されており、内部のタイマーの数はさらに制限されています。当社のソフトウェア設計では、パルス出力、キー検出、LCD 画面カット遅延など、さまざまなタイミング要件が存在することがよくあります.すべてのタイミング ビジネスに対してハードウェア タイマーを開くことは不可能です.まず、ハードウェア リソースが不足している可能性があります。第 2 に、ソフトウェアがハードウェア プラットフォームに過度に依存するようになり、移植性が低下します。
ソフトウェアタイマーがある場合、すべてのタイミングサービスはソフトウェアタイマーに依存します。これにより、ハードウェアリソースが節約されるだけでなく、移植中にソフトウェアタイマーとハードウェア関連部品を変更するだけでよく、他の部品を移動する必要はありません。
ソフトウェア タイマーの実装:
1. 構造体配列によるソフトウェアタイマーの実現
構造体配列の方が単純で分かりやすいですし、その後の連結リストの実装に比べて他にメリットはありません。
実装方法を紹介します. 構造体配列に開始フラグとタイミング期間を定義し, 一つはカウント値 count. この 3 つの変数は最も基本的な 3 つの変数です. その他は操作など自分で補うことができます. mode 、コールバック関数ポインタなど さらに、各構造体配列はタイマーであり、事前に構造体配列のサイズを定義する必要があります。
定義が完了したら、タイマーを開始するときに対応する配列に開始フラグを設定し、ハードウェア ティック割り込みサービス関数で、すべての構造体配列の開始フラグが設定されているかどうかを確認し、現在の開始が見つかったときに設定されている場合は、この配列の期間とカウントを比較し、等しい場合はタイマー タイム アップを意味し、そうでない場合は ++ をカウントし、他の配列の開始フラグをチェックして無限ループにします。
この方法の欠点は非常に明白です, つまり, ハードウェアティック割り込みサービス関数では, すべての配列をポーリングする必要があります. ソフトウェアのビジネス要件が20のタイミングタスクである場合, ソフトウェアの実装で20の配列を定義する必要があります.タイマー スペースの浪費は他の追随を許しません. 重要なのは、ハードウェアティックによってポーリングされる配列が多いほど、特定の配列の実行に時間がかかることです. 将来、50 または 100 のタイミング要件がある場合、タイミングは非常に不正確であること。
2. リンクリストによるソフトウェアタイマーの実現
構造体配列を使用して上記のソフトウェア タイマーを実現することにはさまざまな欠点があるため、改善案を提案します。分析後、ほとんどのタイミング サービスでは、多くの場合、特定の期間に 1 回だけスケジュールする必要があります。つまり、タイマーがタイミングを開始し、タイミングを終了します。タイミングとオフのタイミング. 開始フラグを使用して決定するだけでよいのですが、配列で実装する方法では、タイマーをオフにしても、つまり開始フラグを削除します. タイマーは実行されませんが、配列のスペースは減少しません. ハードウェアティックは、すべての配列をポーリングする必要があります.
そのため、リンク リストを使用してソフトウェア タイマーを実装し、ハードウェア ティックのすべてのノードをポーリングし、タイマーがオンになったときにノードを追加し、タイマーがオフになったときにノードを削除し、必要なノードのみがタイミングを計る対象は現時点でポーリングされるため、タイミングの正確性が大幅に保証されます。
さらに、ユーザーは、タイミングが合ったときにハードウェア ティックで直接実行するか、ハードウェア ティックで組み込みフラグを設定し、while ループで実行のためにキューに入れるかを選択できます。これにより、不正確な問題を効果的に解決できます。パルス出力の必要性など、重要なビジネスのタイミング タイミングと正確なビジネス。
2 番目: リンクされたリストの実装
H ファイル:
/**
* sfor_timer_list.h
* 链表实现的软件定时器库
*/
#ifndef __SOFT_TIMER_LIST_H
#define __SOFT_TIMER_LIST_H
/**
* 硬件中断tick
*/
#define TIMER_HARD_TICK 100U //ms,硬件tick取决于硬件定时中断时间
#define TIMER_200MS_TICK (200U/TIMER_HARD_TICK) //TIMER_HARD_TICK * (2) = 100mS
#define TIMER_SEC_TICK (1000U/TIMER_HARD_TICK) //TIMER_HARD_TICK * (20) = 1S
/**
* 定时模式选择
*/
typedef enum
{
ONCE_MODE, /* 单次定时模式,即超时后自动关闭定时器 */
CONTINUE_MODE, /* 持续定时模式,只要开启除非手动关闭否则永不停歇 */
DEFINE_NUM_MODE, /* 定义次数的模式,运行指定的次数后关闭定时器 */
}TimerTimingModeType;
/**
* 定时超时后运行的回调函数可以选择在中断直接运行或者挂起任务轮询执行
* 只要在定时需求准确的时候才建议选择中断模式执行,类似无磁传感器脉冲测量
* 像一些超时判断类的应用以轮询的方式进行执行
* 中断执行模式越多,其他定时器越不准,毕竟中断允许占时间,查询其他定时器时
* 会有延时
*/
typedef enum
{
RUN_IN_LOOP_MODE, /* 轮询执行模式 */
RUN_IN_INTERRUPT_MODE, /* 中断实时执行模式 */
}TimerRunModeType;
/**
* 软件定时器基本类型
*/
typedef struct SoftTimer
{
unsigned long counter; /* 计数 */
unsigned long duration; /* 定时时长 */
unsigned long run_num; /* 自定义的定时次数 */
BOOL start_flag; /* 启动标志 */
BOOL loop_flag; /* 轮询标志 */
TimerRunModeType run_mode;
TimerTimingModeType timing_mode;
void (*callback_function)(void); /* 回调函数 */
struct SoftTimer *next;
}SoftTimer;
/*
* 初始化软件定时器的硬件tick
*/
extern void soft_timer_tick_init(void);
/*
* 创建一个只运行一次的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
extern void creat_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void));
/*
* 创建永远运行的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
extern void creat_continue_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void));
/*
* 创建指定次数运行的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* run_num:要定时的次数,比如1就是定时1次,5就是定时5次以后自动关闭定时器
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
extern void creat_limit_num_soft_timer(SoftTimer *p, TimerRunModeType mode,unsigned long run_num, unsigned long duration, void(*timeout_handler)(void));
/*
* 重启指定的单次软件定时器
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
extern void restart_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void));
/**
* 删除一个软件定时器
*/
extern void stop_timer(SoftTimer *p);
/**
* 系统main循环进程,用于执行轮询模式的回调函数
*/
extern void soft_timer_main_loop(void);
/**
* 此函数为tick中断服务函数,需要挂载在外部硬件定时器上
* 因此软件定时器的定时精度由此函数挂载的硬件定时时间决定,
* 比如此函数挂载在定时50ms的外部定时器上,那么定时dutation
* 为20时定时时间就是20*50ms=1S
*/
extern void system_tick_IrqHandler(void);
#endif /* !1__SOFT_TIMER_LIST_H */
C ファイル:
/**
* sfor_timer_list.c
* 链表实现的软件定时器库
*/
#define NULL ((void *)0)
typedef enum {FALSE = 0, TRUE = !FALSE} BOOL;
#include "meter_include.h" //包含用户的硬件定时器初始化函数
#include "soft_timer_list.h"
/**
* 软件定时器内部变量
*/
static SoftTimer *head_point = NULL;
static struct SoftTimer *creat_node(SoftTimer *node);
static char delete_node(SoftTimer *node);
static BOOL is_node_already_creat(SoftTimer *node);
/**
* 初始化软件定时器的硬件tick
*/
void soft_timer_tick_init(void)
{
R_IT_Create(); /* 由用户初始化一个硬件定时器,当前tick 100ms */
R_IT_Start();
}
/**
* 系统main循环进程,用于执行轮询模式的回调函数
*/
void soft_timer_main_loop(void)
{
struct SoftTimer *p1 = head_point;
while (p1 != NULL) //下一个节点如果不为空
{
if(p1->loop_flag == TRUE)
{
p1->loop_flag= FALSE;
p1->callback_function();
if(p1->start_flag != TRUE)
delete_node(p1); /* 如果定时器被删除就删除节点 */
}
/* 寻找下一个有意义的节点 */
p1 = p1->next;
}
}
/*
* 创建一个只运行一次的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
void creat_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void))
{
if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;
p->start_flag = TRUE;
p->counter = 0;
p->loop_flag = FALSE;
p->duration = duration;
if(mode == RUN_IN_LOOP_MODE)
p->run_mode = RUN_IN_LOOP_MODE;
else
p->run_mode = RUN_IN_INTERRUPT_MODE;
p->callback_function = timeout_handler;
p->timing_mode = ONCE_MODE;
p->run_num = 0; /* 只有在自定义运行次数的情况下此值才有效 */
head_point = creat_node(p);
}
/*
* 创建永远运行的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行(除非实在必要)还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
void creat_continue_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void))
{
if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;
p->start_flag = TRUE;
p->counter = 0;
p->loop_flag = FALSE;
p->duration = duration;
if(mode == RUN_IN_LOOP_MODE)
p->run_mode = RUN_IN_LOOP_MODE;
else
p->run_mode = RUN_IN_INTERRUPT_MODE;
p->callback_function = timeout_handler;
p->timing_mode = CONTINUE_MODE;
p->run_num = 0; /* 只有在自定义运行次数的情况下此值才有效 */
head_point = creat_node(p);
}
/*
* 创建指定次数运行的软件定时器并立刻开始计时
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* run_num:要定时的次数,比如1就是定时1次,5就是定时5次以后自动关闭定时器
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
void creat_limit_num_soft_timer(SoftTimer *p, TimerRunModeType mode,unsigned long run_num, unsigned long duration, void(*timeout_handler)(void))
{
if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;
p->start_flag = TRUE;
p->counter = 0;
p->loop_flag = FALSE;
p->duration = duration;
if(mode == RUN_IN_LOOP_MODE)
p->run_mode = RUN_IN_LOOP_MODE;
else
p->run_mode = RUN_IN_INTERRUPT_MODE;
p->callback_function = timeout_handler;
p->timing_mode = DEFINE_NUM_MODE;
p->run_num = run_num; /* 只有在自定义运行次数的情况下此值才有效 */
head_point = creat_node(p);
}
/*
* 重启指定的单次软件定时器
* 参数表:p: 定时器结构体指针,由用户创建
* mode: 选择运行模式,可选定时器到了之后是直接在tick中断内执行还是置起标志在while循环内轮询执行
* duration: 要计时的时长,单位为硬件中断的tick
* timeout_handler: 定时到了之后要执行的函数指针
* return:无
*/
void restart_single_soft_timer(SoftTimer *p, TimerRunModeType mode, unsigned long duration, void(*timeout_handler)(void))
{
if ((p == NULL)||(timeout_handler == NULL) || duration == 0) return;
p->start_flag = TRUE;
p->counter = 0;
p->loop_flag = FALSE;
p->duration = duration;
if(mode == RUN_IN_LOOP_MODE)
p->run_mode = RUN_IN_LOOP_MODE;
else
p->run_mode = RUN_IN_INTERRUPT_MODE;
p->callback_function = timeout_handler;
p->timing_mode = ONCE_MODE;
p->run_num = 0; /* 只有在自定义运行次数的情况下此值才有效 */
if (is_node_already_creat(p) != TRUE) /* 若之前的节点已被删除就重新创建 */
head_point = creat_node(p);
}
/**
* 封装后给用户使用
*/
void stop_timer(SoftTimer *p)
{
if (p != NULL) {
p->counter = 0;
p->start_flag = FALSE;
delete_node(p);
}
}
static struct SoftTimer *creat_node(SoftTimer *node)
{
struct SoftTimer *p1; //p1保存当前需要检查的节点的地址
if(node == NULL)
return head_point;
if(is_node_already_creat(node)!=FALSE) {
delete_node(node); /* 当节点已经存在的时候在这里选择退出还是删除后重新创建,目前重新创建 */
}
//当头节点为空时,将传入的节点作为头节点,返回头节点
if (head_point == NULL) {
head_point = node;
node->next = NULL;
return head_point;
}
p1 = head_point;
while(p1->next != NULL)
{
p1 = p1->next; //后移一个节点
}
if(p1->next == NULL) //将该节点插入链表的末尾
{
p1->next = node;
node->next = NULL;
}
else
{
}
return head_point;
}
static char delete_node(SoftTimer *node)
{
struct SoftTimer *p1; //p1保存当前需要检查的节点的地址
struct SoftTimer *temp;
if(node == NULL)
return 1;
p1 = head_point;
if(node == head_point) {
head_point = head_point->next; /* 如果要删除头指针,就将头指针后移 */
} else {
while (p1 != NULL) /*头节点如果不为空 */
{
temp = p1; /* 记录当前节点 */
p1 = p1->next; /* 检索的是下一个节点 */
if (p1 == NULL) {
return 1;
}
if (p1 == node) {
temp->next = p1->next; /* 删除此节点 */
return 0;
}
}
}
return 0;
}
static BOOL is_node_already_creat(SoftTimer *node)
{
struct SoftTimer *p1; //p1保存当前需要检查的节点的地址
if(node == NULL)
return FALSE;
p1 = head_point;
while(p1 != NULL)
{
if(p1 == node)
return TRUE;
p1 = p1->next; //后移一个节点
}
return FALSE;
}
/**
* 此函数为tick中断服务函数,需要挂载在外部硬件定时器上
* 因此软件定时器的定时精度由此函数挂载的硬件定时时间决定,
* 比如此函数挂载在定时50ms的外部定时器上,那么定时dutation
* 为20时定时时间就是20*50ms=1S
*/
void system_tick_IrqHandler(void)
{
struct SoftTimer *p1 = head_point;
close_global_ir(); //关闭中断,根据硬件平台修改
while (p1 != NULL) //下一个节点如果不为空
{
if(p1->start_flag != FALSE) /* 判断当前定时器是否被开启 */
{
if(++p1->counter >= p1->duration) /* 判断当前计时有没有到达 */
{
switch (p1->timing_mode)
{
case ONCE_MODE:
p1->start_flag = FALSE;
break;
case CONTINUE_MODE:
break;
case DEFINE_NUM_MODE:
if (p1->run_num > 0)
{
if (--p1->run_num == 0)
{
p1->start_flag = FALSE;
}
}
default:
break;
}
if(p1->run_mode == RUN_IN_INTERRUPT_MODE)
{
p1->callback_function(); /* 中断内直接运行回调函数,用于实时性比较高的程序 */
if(p1->start_flag != TRUE)
delete_node(p1);
}
else
p1->loop_flag = TRUE;
p1->counter = 0;
}
}
/* 寻找下一个有意义的节点 */
p1 = p1->next;
}
open_global_ir(); //打开中断,根据硬件平台修改
}
3番目: 構造の実装
最後に、参照用に構造体配列で実装されたソフトウェア タイマーを添付します。
H ファイル:
/**
* sfor_timer_array.h
* 数组实现的软件定时器库
* 一个软件定时器解决整个项目中所有的定时需求,回调函数可根据应用
* 自动切换中断实时操作或者不实时的轮询操作,可以有效解决硬件资源
* 不足或者软件定时器定时不准的问题,定时误差就几个C语言语句,倘若
* 配置最大10个软件定时器,误差就是最多10个for循环的时间
*/
#ifndef __SOFT_TIMER_ARRAY_H
#define __SOFT_TIMER_ARRAY_H
/**
* 定义最大的可用的软件定时器数量
* 理论上可以无限大,但是数量越大定时误差越大,所以用几个开几个
* 误差在于轮询检测所有定时器,定时器越多轮询到自己的定时器就越慢,
* 此外,数量增多亦会带来空间增大
*/
#define MAX_TIMER_NUM 10
/**
* 定时模式选择
*/
typedef enum
{
ONCE_MODE, /* 单次定时模式,即超时后自动关闭定时器 */
CONTINUE_MODE, /* 持续定时模式,只要开启除非手动关闭否则永不停歇 */
DEFINE_NUM_MODE, /* 定义次数的模式,运行指定的次数后关闭定时器 */
}TimerTimingModeType;
/**
* 定时超时后运行的回调函数可以选择在中断直接运行或者挂起任务轮询执行
* 只要在定时需求准确的时候才建议选择中断模式执行,类似无磁传感器脉冲测量
* 像一些超时判断类的应用以轮询的方式进行执行
* 中断执行模式越多,其他定时器越不准,毕竟中断允许占时间,查询其他定时器时
* 会有延时
*/
typedef enum
{
RUN_IN_LOOP_MODE, /* 轮询执行模式 */
RUN_IN_INTERRUPT_MODE, /* 中断实时执行模式 */
}TimerRunModeType;
/**
* 软件定时器基本类型
*/
typedef struct
{
unsigned long counter; /* 计数 */
unsigned long duration; /* 定时时长 */
unsigned long run_num; /* 自定义的定时次数 */
unsigned char start_flag; /* 启动标志 */
unsigned char loop_flag; /* 轮询标志 */
TimerRunModeType run_mode;
TimerTimingModeType timing_mode;
void (*callback_function)(void); /* 回调函数 */
}SoftTimer;
/**
* 删除一个软件定时器
*/
extern void stop_timer(void (*callback_function)(void));
/*
* 创建一个软件定时器并立刻开始计时
* return:没有空闲定时器时返回1,创建成功时返回0
*/
extern char soft_timer_start(TimerTimingModeType timing_mode,
TimerRunModeType run_mode,
unsigned long run_num,
unsigned long duration,
void (*callback_function)(void));
/**
* 系统main循环进程,用于执行轮询模式的回调函数
*/
extern void soft_timer_main_loop(void);
/**
* 此函数为tick中断服务函数,需要挂载在外部硬件定时器上
* 因此软件定时器的定时精度由此函数挂载的硬件定时时间决定,
* 比如此函数挂载在定时50ms的外部定时器上,那么定时dutation
* 为20时定时时间就是20*50ms=1S
*/
extern void system_tick_IrqHandler(void);
#endif /* !1__SOFT_TIMER_LIB_H */
C ファイル:
/**
* sfor_timer_array.c
* 数组实现的软件定时器库
* 一个软件定时器解决整个项目中所有的定时需求,回调函数可根据应用
* 自动切换中断实时操作或者不实时的轮询操作,可以解决硬件资源
* 不足或者软件定时器定时不准的问题
*/
#include "soft_timer_array.h"
/**
* 软件定时器内部变量
*/
static SoftTimer soft_timer[MAX_TIMER_NUM];
/*
* 创建一个软件定时器并立刻开始计时
* return:没有空闲定时器时返回1,创建成功时返回0
*/
char soft_timer_start(TimerTimingModeType timing_mode, TimerRunModeType run_mode, unsigned long run_num, unsigned long duration,
void (*callback_function)(void))
{
unsigned char i = 0;
if(!callback_function) return 1;
stop_timer(callback_function); /* 先判断有没有一样的定时器,有的话先删除 */
for(i =0; i<MAX_TIMER_NUM;i++)
{
if(soft_timer[i].start_flag == 0) /* 查询空闲的定时器 */
{
soft_timer[i].start_flag = 1;
soft_timer[i].counter = 0;
soft_timer[i].loop_flag = 0;
soft_timer[i].duration = duration;
soft_timer[i].run_mode = run_mode;
soft_timer[i].callback_function = callback_function;
if ((soft_timer[i].timing_mode = timing_mode) == DEFINE_NUM_MODE)
soft_timer[i].run_num = run_num; /* 只有在自定义运行次数的情况下此值才有效 */
break;
}
if (i == MAX_TIMER_NUM ) /* 没有空闲定时器 */
return 1;
}
return 0;
}
/**
* 删除一个软件定时器
*/
void stop_timer(void (*callback_function)(void))
{
unsigned char i;
for(i = 0; i <MAX_TIMER_NUM; i++)
{
if(soft_timer[i].callback_function == callback_function)
{
soft_timer[i].start_flag = 0;
soft_timer[i].loop_flag = 0;
}
}
}
/**
* 系统main循环进程,用于执行轮询模式的回调函数
*/
void soft_timer_main_loop(void)
{
unsigned char i;
for(i = 0; i <MAX_TIMER_NUM; i++) /* 轮询执行回调函数 */
{
if(soft_timer[i].loop_flag == 1)
{
soft_timer[i].loop_flag= 0;
soft_timer[i].callback_function();
// return; /* 运行一次任务后就退出去等跑一轮后运行下个程序,防止任务堆积在一起运行 */
}
}
}
/**
* 此函数为tick中断服务函数,需要挂载在外部硬件定时器上
* 因此软件定时器的定时精度由此函数挂载的硬件定时时间决定,
* 比如此函数挂载在定时50ms的外部定时器上,那么定时dutation
* 为20时定时时间就是20*50ms=1S
*/
void system_tick_IrqHandler(void)
{
unsigned char i = 0;
for(i =0 ;i <MAX_TIMER_NUM; i++)
{
if(soft_timer[i].start_flag !=0 ) /* 判断当前定时器是否被开启 */
{
if(++soft_timer[i].counter >= soft_timer[i].duration) /* 判断当前计时有没有到达 */
{
switch (soft_timer[i].timing_mode)
{
case ONCE_MODE:
soft_timer[i].start_flag = 0;
break;
case CONTINUE_MODE:
break;
case DEFINE_NUM_MODE:
if (soft_timer[i].run_num > 0)
{
if (--soft_timer[i].run_num == 0)
{
soft_timer[i].start_flag = 0;
}
}
default:
break;
}
if(soft_timer[i].run_mode == RUN_IN_INTERRUPT_MODE)
soft_timer[i].callback_function(); /* 中断内直接运行回调函数,用于实时性比较高的程序 */
else
soft_timer[i].loop_flag = 1;
soft_timer[i].counter = 0;
}
// else
// {
// soft_timer[i].counter++;
// }
}
}
}