From 9cebbd2bc76bf4f95c87ab68aa99171345d43ff7 Mon Sep 17 00:00:00 2001 From: coffee Date: Wed, 1 Apr 2026 15:14:31 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E9=87=8D=E6=94=B9=E5=AE=9A=E6=97=B6?= =?UTF-8?q?=E5=99=A8=202.=20=E5=A2=9E=E5=8A=A0HList?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/HDProtocolServer.h | 8 + include/HList.h | 108 ++++++++++++++ include/HTimer.h | 50 ++++--- src/HDProtocolServer.c | 18 +++ src/HList.c | 128 ++++++++++++++++ src/HTimer.c | 289 ++++++++++++++++--------------------- 6 files changed, 409 insertions(+), 192 deletions(-) create mode 100644 include/HList.h create mode 100644 src/HList.c diff --git a/include/HDProtocolServer.h b/include/HDProtocolServer.h index 4cb07fd..c3a4a5f 100644 --- a/include/HDProtocolServer.h +++ b/include/HDProtocolServer.h @@ -140,6 +140,14 @@ uint8_t HDProtocolCurrSend(const void *data, int len); */ void HDProtocolSendPort(uint8_t dst, const void *data, int len); +/** + * @brief 直接根据目标数据口发送(不等待回送的特殊接口) + * @param dst 需要往那个目标数据口发送 + * @param data 发送数据 + * @param len 发送数据长度 + */ +void HDProtocolDirectSendPort(uint8_t dst, const void *data, int len); + /** * @brief 临时路由发送(用于发送目标端口的数据后, 解析端临时更改目标端口, 通过 HDProtocolGetDest 获取目标端口) * @param src 发送源端口 diff --git a/include/HList.h b/include/HList.h new file mode 100644 index 0000000..e25f0b0 --- /dev/null +++ b/include/HList.h @@ -0,0 +1,108 @@ +/** + * 日期: 2026-04-01 + * 作者: coffee + * 描述: 侵入式双向链表基础组件 + * 用法类似内核 list, 节点嵌入到用户结构体中 + */ + +#ifndef _H_LIST_H_ +#define _H_LIST_H_ + +#include +#include + + +typedef struct HList { + struct HList *next; + struct HList *prev; +} HList; + + +#define HLIST_HEAD_INIT(name) { &(name), &(name) } + +#define HLIST_HEAD(name) HList name = HLIST_HEAD_INIT(name) + +#define HLIST_CONTAINER_OF(ptr, type, member) \ + ((type *)((uint8_t *)(ptr) - offsetof(type, member))) + +#define HLIST_ENTRY(ptr, type, member) \ + HLIST_CONTAINER_OF(ptr, type, member) + +#define HLIST_FIRST_ENTRY(head, type, member) \ + HLIST_ENTRY((head)->next, type, member) + +#define HLIST_LAST_ENTRY(head, type, member) \ + HLIST_ENTRY((head)->prev, type, member) + +#define HLIST_FOR_EACH(pos, head) \ + for ((pos) = (head)->next; (pos) != (head); (pos) = (pos)->next) + +#define HLIST_FOR_EACH_TYPE(pos, head) \ + for (HList *(pos) = (head)->next; (pos) != (head); (pos) = (pos)->next) + +#define HLIST_FOR_EACH_SAFE(pos, n, head) \ + for ((pos) = (head)->next, (n) = (pos)->next; (pos) != (head); \ + (pos) = (n), (n) = (pos)->next) + +#define HLIST_FOR_EACH_SAFE_TYPE(pos, n, head) \ + for (HList *(pos) = (head)->next, (n) = (pos)->next; (pos) != (head); \ + (pos) = (n), (n) = (pos)->next) + + +/** + * @brief 初始化链表头或节点 + */ +void HListInit(HList *list); + +/** + * @brief 判断节点是否已经独立初始化 + */ +uint8_t HListIsSelf(const HList *list); + +/** + * @brief 判断链表是否为空 + */ +uint8_t HListEmpty(const HList *head); + +/** + * @brief 获取链表节点数量 + */ +size_t HListLen(const HList *head); + +/** + * @brief 头插 + */ +void HListAdd(HList *node, HList *head); + +/** + * @brief 尾插 + */ +void HListAddTail(HList *node, HList *head); + +/** + * @brief 删除节点, 删除后节点会重新初始化成独立状态 + */ +void HListDel(HList *node); + +/** + * @brief 替换节点 + */ +void HListReplace(HList *oldNode, HList *newNode); + +/** + * @brief 移动节点到目标链表头部 + */ +void HListMove(HList *node, HList *head); + +/** + * @brief 移动节点到目标链表尾部 + */ +void HListMoveTail(HList *node, HList *head); + +/** + * @brief 弹出头节点 + * @return 成功返回节点指针, 空链表返回 NULL + */ +HList *HListPop(HList *head); + +#endif // _H_LIST_H_ diff --git a/include/HTimer.h b/include/HTimer.h index 7cec9f9..41fede3 100644 --- a/include/HTimer.h +++ b/include/HTimer.h @@ -10,15 +10,11 @@ #include - -// 定时器注册最大数量 -#ifndef HTIMER_REGISTER_MAX -#define HTIMER_REGISTER_MAX (5) -#endif +#include "HList.h" // 无效值 #ifndef HTIMER_INVALID -#define HTIMER_INVALID -1 +#define HTIMER_INVALID (NULL) #endif // 检查添加定时任务超出限制后进入死循环, 方便调试 @@ -38,21 +34,28 @@ typedef enum } eHTimerFlags; typedef void (*HTimerCallType)(); -typedef int16_t HTimer_t; +typedef void * HTimer_t; ///< 注册的定时器信息, 定义为数组, 数量由外部控制 typedef struct HTimerInfo { uint32_t flags : 1; ///< 定时器标志, eTimerFlags - volatile uint32_t enable: 2; ///< 定时器使能 - uint32_t curr : 1; ///< 当前回调者 - uint32_t duration : 28; ///< 定时触发时长, 毫秒为计数单元 + uint32_t duration : 31; ///< 定时触发时长, 毫秒为计数单元 uint32_t lastTime; ///< 上次触发时间 HTimerCallType call; ///< 定时触发函数 #if HTIMER_USE_USERDATA long userData; ///< 用户数据 #endif + HList node; ///< 定时器节点 + uint8_t id; ///< 定时器ID } HTimerInfo; +typedef struct TimeRegisterInfo { + HList workNode; ///< 工作节点 + HList idleNode; ///< 空闲节点 +#if HTIMER_USE_USERDATA + HList *currNode; ///< 当前节点 +#endif +} TimeRegisterInfo; ///< 初始化毫秒定时器, 需要传递获取毫秒的函数 void HTimerInitMs(uint32_t (*func)(void)); @@ -61,7 +64,14 @@ void HTimerInitMs(uint32_t (*func)(void)); uint32_t HTimerGetMs(); /** - * @brief 注册定时器信息 + * @brief 注册定时器注册列表信息内存(外部提供数组) + * @param info 定时器注册信息 + * @param len 定时器个数 + */ +void HTimerInitRegister(TimeRegisterInfo *info, uint8_t len); + +/** + * @brief 注册定时器信息(外部提供定时器数组) * @param id 定时器ID * @param info 定时器信息, 由外部提供定时器数组 * @param infoLen 定时器信息长度 @@ -69,24 +79,15 @@ uint32_t HTimerGetMs(); */ uint8_t HTimerRegisterTimerInfo(uint8_t id, HTimerInfo *info, uint16_t infoLen); -///< 移除定时器信息 -void HTimerRemoveRegister(uint8_t id); - ///< 运行定时器可执行任务, 需要在主循环中调用 void HTimerRun(uint8_t id); -///< 添加一个定时任务, 添加失败返回 HTIMER_INVALID, ms不超过24比特 +///< 添加一个定时任务, 添加失败返回 HTIMER_INVALID, ms不超过31比特 HTimer_t HTimerAdd(uint8_t id, uint32_t ms, HTimerCallType call, eHTimerFlags flags); ///< 移除一个定时任务 void HTimerRemove(HTimer_t index); -///< 在定时器回调任务中获取当前定时器ID -uint8_t HTimerGetCurrentId(); - -///< 在定时器回调任务中获取当前定时器调用者索引, 不存在返回 HTIMER_INVALID -HTimer_t HTimerGetCurrentCaller(uint8_t id); - #if HTIMER_USE_USERDATA ///< 设置定时器用户数据 void HTimerSetUserData(HTimer_t index, long data); @@ -94,12 +95,13 @@ void HTimerSetUserData(HTimer_t index, long data); ///< 获取定时器用户数据, 不存在返回 0 long HTimerGetUserData(HTimer_t index); -///< 定时器回调时, 获取对应的定时器用户数据, 不存在返回 0 -long HTimerGetCurrCallUserData(); +///< 获取当前调用定时器的用户数据 +long HTimerGetCurrCallUserData(uint8_t id); + #else static inline void HTimerSetUserData(HTimer_t index, long data) {} static inline long HTimerGetUserData(HTimer_t index) { return 0; } -static inline long HTimerGetCurrCallUserData() { return 0; } +static inline long HTimerGetCurrCallUserData(uint8_t id) { return 0; } #endif #endif //__H_TIMER_H__ diff --git a/src/HDProtocolServer.c b/src/HDProtocolServer.c index 694c3cd..f859aa6 100644 --- a/src/HDProtocolServer.c +++ b/src/HDProtocolServer.c @@ -301,6 +301,24 @@ void HDProtocolSendPort(uint8_t dst, const void *data, int len) sInfo.writeCall(&sInfo.info[dst], (const uint8_t *)data, len); } +void HDProtocolDirectSendPort(uint8_t dst, const void *data, int len) +{ + if (data == NULL || len == 0) { + return; + } + + if (sInfo.writeCall == NULL || sInfo.info == NULL) { + return; + } + + if (dst >= sInfo.infoLen) { + LogE("index[%d] is out of range[%d]", dst, sInfo.infoLen); + return; + } + + sInfo.writeCall(&sInfo.info[dst], (const uint8_t *)data, len); +} + void HDProtocolTmpSend(uint8_t src, uint8_t feedbackRoute, const void *data, int len, uint8_t needReadCount, uint8_t waitCount) { if (data == NULL || len == 0) { diff --git a/src/HList.c b/src/HList.c new file mode 100644 index 0000000..163f5b5 --- /dev/null +++ b/src/HList.c @@ -0,0 +1,128 @@ +/** + * 日期: 2026-04-01 + * 作者: coffee + * 描述: 侵入式双向链表实现 + */ + +#include "HList.h" + + +static void __HListAdd(HList *node, HList *prev, HList *next) { + next->prev = node; + node->next = next; + node->prev = prev; + prev->next = node; +} + +static void __HListDel(HList *prev, HList *next) { + next->prev = prev; + prev->next = next; +} + +void HListInit(HList *list) { + if (list == NULL) { + return; + } + + list->next = list; + list->prev = list; +} + +uint8_t HListIsSelf(const HList *list) { + if (list == NULL) { + return 0; + } + + return (list->next == list && list->prev == list) ? 1u : 0u; +} + +uint8_t HListEmpty(const HList *head) { + if (head == NULL) { + return 1; + } + + return (head->next == head) ? 1u : 0u; +} + +size_t HListLen(const HList *head) { + size_t len = 0; + const HList *pos; + + if (head == NULL) { + return 0; + } + + for (pos = head->next; pos != head; pos = pos->next) { + ++len; + } + + return len; +} + +void HListAdd(HList *node, HList *head) { + if (node == NULL || head == NULL) { + return; + } + + __HListAdd(node, head, head->next); +} + +void HListAddTail(HList *node, HList *head) { + if (node == NULL || head == NULL) { + return; + } + + __HListAdd(node, head->prev, head); +} + +void HListDel(HList *node) { + if (node == NULL || node->next == NULL || node->prev == NULL) { + return; + } + + __HListDel(node->prev, node->next); + HListInit(node); +} + +void HListReplace(HList *oldNode, HList *newNode) { + if (oldNode == NULL || newNode == NULL || + oldNode->next == NULL || oldNode->prev == NULL) { + return; + } + + newNode->next = oldNode->next; + newNode->next->prev = newNode; + newNode->prev = oldNode->prev; + newNode->prev->next = newNode; + HListInit(oldNode); +} + +void HListMove(HList *node, HList *head) { + if (node == NULL || head == NULL) { + return; + } + + HListDel(node); + HListAdd(node, head); +} + +void HListMoveTail(HList *node, HList *head) { + if (node == NULL || head == NULL) { + return; + } + + HListDel(node); + HListAddTail(node, head); +} + +HList *HListPop(HList *head) { + HList *node; + + if (HListEmpty(head)) { + return NULL; + } + + node = head->next; + HListDel(node); + return node; +} diff --git a/src/HTimer.c b/src/HTimer.c index bc04684..ba29320 100644 --- a/src/HTimer.c +++ b/src/HTimer.c @@ -11,90 +11,102 @@ static uint32_t (*GetCurrentMs)(void); -#define CHECK_VALUE (0x0FFFFFFF) -#define CREATE_INDEX(id, index) ((id << 8) | index) -#define GET_ID(index) ((index >> 8) & 0xFF) -#define GET_INDEX(index) (index & 0xFF) +#define CHECK_VALUE (0x7FFFFFFF) -struct __attribute__((packed)) TimeRegisterInfo { - uint16_t enable : 1; ///< 定时器使能 - uint16_t curr : 1; ///< 当前定时器 - uint16_t len : 14; ///< 定时器个数 - HTimerInfo *timers; ///< 定时器 +struct __attribute__((packed)) TimerInfo { + TimeRegisterInfo *info; + uint8_t infoLen; }; -static struct TimeRegisterInfo sTimeRegisters[HTIMER_REGISTER_MAX]; +static struct TimerInfo sInfo; - -static void CallTimer(uint8_t id, int16_t index) { - if (id >= HTIMER_REGISTER_MAX) { +static void InsertWorkNode(uint8_t id, HList *node) { + if (id >= sInfo.infoLen) { LogD("error id[%d]", id); return ; } - if (index < 0 || index >= sTimeRegisters[id].len) { - LogD("error index[%d], len[%d]", index, sTimeRegisters[id].len); + HTimerInfo *info = HLIST_ENTRY(node, HTimerInfo, node); + const uint32_t triggerTime = info->lastTime + info->duration; + HList *entry = NULL; + + HLIST_FOR_EACH(entry, &sInfo.info[id].workNode) { + HTimerInfo *nodeInfo = HLIST_ENTRY(entry, HTimerInfo, node); + if ((nodeInfo->lastTime + nodeInfo->duration) < triggerTime) { + continue; + } + + HListAddTail(node, entry); + return; + } + + HListAddTail(node, entry); +} + +static void CallTimer(uint8_t id, HList *node) { + if (id >= sInfo.infoLen) { + LogD("error id[%d]", id); return ; } - sTimeRegisters[id].curr = 1; - sTimeRegisters[id].timers[index].curr = 1; - if (sTimeRegisters[id].timers[index].flags == kHTimerOnce) { - sTimeRegisters[id].timers[index].enable = 0; +#if HTIMER_USE_USERDATA + sInfo.info[id].currNode = node; +#endif + HTimerInfo *info = HLIST_ENTRY(node, HTimerInfo, node); + if (info->call) { + info->call(); + } +#if HTIMER_USE_USERDATA + sInfo.info[id].currNode = NULL; +#endif + + info->lastTime = GetCurrentMs(); + if (!HListIsSelf(node)) { + return; } - sTimeRegisters[id].timers[index].call(); - sTimeRegisters[id].curr = 0; - sTimeRegisters[id].timers[index].curr = 0; - sTimeRegisters[id].timers[index].lastTime = GetCurrentMs(); + // 如果是单次的, 直接移动到空闲队列, 循环的, 按触发时间重新插入工作队列 + if (info->flags == kHTimerOnce) { + HListAddTail(node, &sInfo.info[id].idleNode); + } else { + InsertWorkNode(id, node); + } } -static int16_t AddTimerData(uint8_t id, uint32_t duration, HTimerCallType call, eHTimerFlags flags) { +static HList * AddTimerData(uint8_t id, uint32_t duration, HTimerCallType call, eHTimerFlags flags) { if (!GetCurrentMs) { LogD("GetCurrentMs not init"); return HTIMER_INVALID; } - if (id >= HTIMER_REGISTER_MAX) { + if (id >= sInfo.infoLen) { LogD("error id[%d]", id); return HTIMER_INVALID; } - if (sTimeRegisters[id].enable == 0) { - LogD("not enable, id[%d]", id); - return HTIMER_INVALID; - } - if ((duration & ~CHECK_VALUE) != 0) { LogD("duration overflow, duration[%d]", duration); return HTIMER_INVALID; } - for (uint16_t i = 0; i < sTimeRegisters[id].len; ++i) { - if (sTimeRegisters[id].timers[i].enable) { - continue; - } - - // 防中断导致数据重叠 - sTimeRegisters[id].timers[i].enable = 1; - if (sTimeRegisters[id].timers[i].enable != 1) { - continue; - } - - sTimeRegisters[id].timers[i].enable = 2; - sTimeRegisters[id].timers[i].duration = duration; - sTimeRegisters[id].timers[i].lastTime = GetCurrentMs(); - sTimeRegisters[id].timers[i].call = call; - sTimeRegisters[id].timers[i].flags = flags; -#if HTIMER_USE_USERDATA - sTimeRegisters[id].timers[i].userData = 0; -#endif - return CREATE_INDEX(id, i); + HList *node = HListPop(&sInfo.info[id].idleNode); + if (node == NULL) { + LogD("timers full, id[%d], duration[%d], flags[%d]", id, duration, flags); + return HTIMER_INVALID; } - LogD("timers full, id[%d], duration[%d], flags[%d]", id, duration, flags); - return HTIMER_INVALID; + HTimerInfo *info = HLIST_ENTRY(node, HTimerInfo, node); + info->duration = duration; + info->lastTime = GetCurrentMs(); + info->call = call; + info->flags = flags; +#if HTIMER_USE_USERDATA + info->userData = 0; +#endif + + InsertWorkNode(id, node); + return node; } void HTimerInitMs(uint32_t (*func)(void)) { @@ -109,32 +121,30 @@ uint32_t HTimerGetMs() { return GetCurrentMs(); } -uint8_t HTimerRegisterTimerInfo(uint8_t id, HTimerInfo *info, uint16_t infoLen) { - if (id >= HTIMER_REGISTER_MAX) { - LogD("error id[%d], max[%d]", id, HTIMER_REGISTER_MAX); - return 0; +void HTimerInitRegister(TimeRegisterInfo *info, uint8_t len) +{ + sInfo.info = info; + sInfo.infoLen = len; + for (uint8_t i = 0; i < len; ++i) { + HListInit(&info[i].workNode); + HListInit(&info[i].idleNode); } - - if (sTimeRegisters[id].enable == 1) { - LogD("id[%d] already register", id); - return 0; - } - - memset(info, 0, infoLen); - sTimeRegisters[id].timers = info; - sTimeRegisters[id].len = infoLen; - sTimeRegisters[id].enable = 1; - return 1; } -void HTimerRemoveRegister(uint8_t id) { - if (id >= HTIMER_REGISTER_MAX) { - return ; +uint8_t HTimerRegisterTimerInfo(uint8_t id, HTimerInfo *info, uint16_t infoLen) { + if (id >= sInfo.infoLen) { + LogD("error id[%d], max[%d]", id, sInfo.infoLen); + return 0; } - sTimeRegisters[id].enable = 0; - sTimeRegisters[id].len = 0; - sTimeRegisters[id].timers = 0; + memset(info, 0, sizeof(*info) * infoLen); + for (uint16_t i = 0; i < infoLen; ++i) { + info[i].id = id; + HListInit(&info[i].node); + HListAddTail(&info[i].node, &sInfo.info[id].idleNode); + } + + return 1; } void HTimerRun(uint8_t id) { @@ -142,38 +152,33 @@ void HTimerRun(uint8_t id) { return ; } - if (id >= HTIMER_REGISTER_MAX) { + if (id >= sInfo.infoLen) { return ; } - if (sTimeRegisters[id].enable == 0) { + // 检查是否有任务 + if (HListEmpty(&sInfo.info[id].workNode)) { return ; } - if (sTimeRegisters[id].curr == 1) { - return ; - } - - // 没必要每次都扫描, 当时间更新时再检查 - static uint32_t lastMs[HTIMER_REGISTER_MAX]; - if (lastMs[id] == GetCurrentMs()) { - return ; - } - lastMs[id] = GetCurrentMs(); - - for (uint16_t i = 0; i < sTimeRegisters[id].len; ++i) { - if (sTimeRegisters[id].timers[i].enable == 0) { - continue; - } - - // 这里每次都获取最新时间是因为执行任务期间可能会导致时间变化 - uint32_t diff = GetCurrentMs() - sTimeRegisters[id].timers[i].lastTime; - uint32_t timeDuration = sTimeRegisters[id].timers[i].duration; + HList readyNode; + HListInit(&readyNode); + // 工作队列按触发时间有序, 只需要连续取出队头的就绪任务 + while (!HListEmpty(&sInfo.info[id].workNode)) { + HTimerInfo *info = HLIST_FIRST_ENTRY(&sInfo.info[id].workNode, HTimerInfo, node); + uint32_t diff = GetCurrentMs() - info->lastTime; + uint32_t timeDuration = info->duration; if (diff < timeDuration) { - continue; + break; } - CallTimer(id, i); + HListMoveTail(&info->node, &readyNode); + } + + // 执行就绪队列 + while (!HListEmpty(&readyNode)) { + HList *node = HListPop(&readyNode); + CallTimer(id, node); } } @@ -194,47 +199,9 @@ void HTimerRemove(HTimer_t index) { return ; } - const uint8_t id = GET_ID(index); - index = GET_INDEX(index); - if (id >= HTIMER_REGISTER_MAX) { - LogD("error id[%d]", id); - return ; - } - - if (index < 0 || index >= sTimeRegisters[id].len) { - LogD("error index[%d]", index); - return ; - } - - sTimeRegisters[id].timers[index].enable = 0; -} - -uint8_t HTimerGetCurrentId() { - for (uint8_t i = 0; i < HTIMER_REGISTER_MAX; ++i) { - if (sTimeRegisters[i].curr == 1) { - return i; - } - } - - return HTIMER_INVALID; -} - -HTimer_t HTimerGetCurrentCaller(uint8_t id) { - if (id >= HTIMER_REGISTER_MAX) { - return HTIMER_INVALID; - } - - if (sTimeRegisters[id].curr == 0) { - return HTIMER_INVALID; - } - - for (uint16_t i = 0; i < sTimeRegisters[id].len; ++i) { - if (sTimeRegisters[id].timers[i].curr == 1) { - return CREATE_INDEX(id, i); - } - } - - return HTIMER_INVALID; + HTimerInfo *info = HLIST_ENTRY(index, HTimerInfo, node); + HListDel(&info->node); + HListAddTail(&info->node, &sInfo.info[info->id].idleNode); } #if HTIMER_USE_USERDATA @@ -244,19 +211,8 @@ void HTimerSetUserData(HTimer_t index, long data) return ; } - const uint8_t id = GET_ID(index); - index = GET_INDEX(index); - if (id >= HTIMER_REGISTER_MAX) { - LogD("error id[%d]", id); - return ; - } - - if (index < 0 || index >= sTimeRegisters[id].len) { - LogD("error index[%d]", index); - return ; - } - - sTimeRegisters[id].timers[index].userData = data; + HTimerInfo *info = HLIST_ENTRY(index, HTimerInfo, node); + info->userData = data; } long HTimerGetUserData(HTimer_t index) @@ -265,25 +221,22 @@ long HTimerGetUserData(HTimer_t index) return 0; } - const uint8_t id = GET_ID(index); - index = GET_INDEX(index); - if (id >= HTIMER_REGISTER_MAX) { - LogD("error id[%d]", id); - return 0; - } - - if (index < 0 || index >= sTimeRegisters[id].len) { - LogD("error index[%d]", index); - return 0; - } - - return sTimeRegisters[id].timers[index].userData; + HTimerInfo *info = HLIST_ENTRY(index, HTimerInfo, node); + return info->userData; } -long HTimerGetCurrCallUserData() +long HTimerGetCurrCallUserData(uint8_t id) { - HTimer_t index = HTimerGetCurrentCaller(HTimerGetCurrentId()); - return HTimerGetUserData(index); -} -#endif + if (id >= sInfo.infoLen) { + return 0; + } + if (sInfo.info[id].currNode == NULL) { + return 0; + } + + HTimerInfo *info = HLIST_ENTRY(sInfo.info[id].currNode, HTimerInfo, node); + return info->userData; +} + +#endif // HTIMER_USE_USERDATA