59、QP-nano 实时框架深入解析

QP-nano 实时框架深入解析

1. 定时器事件处理

在处理定时器事件时,为了节省栈空间,临时变量 a (指向活动对象的指针)被声明为静态变量。活动对象指针从 ROM 数组 QF_active[] 中加载,该数组将活动对象的优先级映射到活动对象指针。

当滴答计数器不为零时,给定活动对象的时间事件正在运行。滴答计数器会递减并与零进行比较,当计数器达到零时,时间事件会自动解除。 QF_tick() 函数通过 QActive_postISR() 函数将 Q_TIMEOUT_SIG 事件发送给拥有该计数器的活动对象,因此 QF_tick() 只能在中断服务程序(ISR)上下文中调用。这个循环会对所有优先级大于零的活动对象继续执行。

2. 事件队列

在 QP - nano 中,每个活动对象都有自己的事件队列。队列由位于 QActive 结构内部的一个事件(所有活动对象都从该结构派生)和活动对象外部分配的事件环形缓冲区组成。

常量数组 QF_active[] 存储活动对象的“控制块”,即 QActiveCB 结构的实例。每个 QF_active[] 元素包含指向活动对象的指针 act 、指向环形缓冲区队列的指针 queue 以及环形缓冲区最后一个元素的索引 end ,这些元素在编译时为每个活动对象进行初始化。

QActive 结构存储在运行时会发生变化的事件队列元素。队列存储由外部用户分配的环形缓冲区队列和状态机内部存储的当前事件 evt 组成。所有发送到状态机的事件都必须经过“当前事件” evt 数据成员,这在图中用虚线表示。这个位于环形缓冲区之外的额外位置通过频繁绕过缓冲来优化队列操作,因为在大多数情况下,队列会在空和非空状态之间交替,且队列中一次只有一个事件。

在极端情况下,如果足够的队列深度仅为一个事件,整个环形缓冲区可以完全消除。此时,不分配环形缓冲区,使用 NULL 作为队列指针,使用零作为有效队列长度来初始化 QF_active[] 数组。

环形缓冲区的索引 head tail 以及 QF_active[] 中的 end 都是相对于队列指针的。这些索引管理着一个环形缓冲区队列,客户端必须预先分配一个连续的 QEvent 类型的事件数组。事件总是从缓冲区的 tail 索引处提取,新事件插入到 head 索引处,这对应于先进先出(FIFO)排队。提取事件时, tail 索引总是递减;插入事件时, head 索引也总是递减。 end 索引限制了 head tail 索引的范围,当它们达到零时,必须“环绕”到 end ,这会导致 head tail 索引围绕环形缓冲区逆时针移动。

下面是一个简单的 mermaid 流程图,展示事件队列的基本操作:

graph LR
    A[新事件到达] --> B{队列是否为空}
    B -- 是 --> C[直接复制到当前事件]
    B -- 否 --> D[插入到环形缓冲区头部]
    D --> E{头部索引是否为 0}
    E -- 是 --> F[头部索引环绕到 end]
    E -- 否 --> G[头部索引递减]
    C --> H[更新 QF_readySet_]
    F --> H
    G --> H
    H --> I[结束]
3. 就绪集(QF_readySet_)

QP - nano 包含一个协作式的“普通”内核和一个名为 QK - nano 的抢占式实时内核。为了在这两个内核中进行高效调度,QP - nano 使用一个单字节的 QF_readySet_ 来维护应用程序中所有活动对象事件队列的全局状态。

QF_readySet_ 是一个位掩码,代表系统中所有非空事件队列的“就绪集”。 QF_readySet_ 字节中的每一位对应一个活动对象。例如,当且仅当优先级为 n + 1 的活动对象的事件队列为非空时, QF_readySet_ 中的第 n 位为 1(位传统上从 0 开始编号,而 QP - nano 中的优先级从 1 开始编号)。

当向优先级为 p 的空队列发送事件时, QF_readySet_ 位掩码中的第 p - 1 位会被设置为 1;相反,当从优先级为 q 的队列中移除最后一个事件时, QF_readySet_ 位掩码中的第 q - 1 位会被清除。显然,对全局 QF_readySet_ 位掩码的所有操作都必须在临界区中进行。

4. 从任务级别发送事件(QActive_post())

QActive_post() 函数用于从一个活动对象向另一个活动对象发送事件,不能用于从 ISR 发送事件,因为它使用任务级别的中断锁定策略。

下面是 QActive_post() 函数的代码:

#if (Q_PARAM_SIZE != 0)
(1)
void QActive_post(QActive *me, QSignal sig, QParam par) {
#else
(2)
void QActive_post(QActive *me, QSignal sig) {
#endif
(3)
QF_INT_LOCK();
if (me->nUsed == (uint8_t)0) {
    /* is the queue empty? */
    ++me->nUsed;
    /* update number of events */
    (4)
    Q_SIG(me) = sig;
    /* deliver the event directly */
    #if (Q_PARAM_SIZE != 0)
    (5)
    Q_PAR(me) = par;
    #endif
    (6)
    QF_readySet_ |= Q_ROM_BYTE(l_pow2Lkup[me->prio]);
    /* set the bit */
    #ifdef QK_PREEMPTIVE
    (7)
    QK_schedule_();
    /* check for synchronous preemption */
    #endif
}
else {
    (8)
    QF_pCB_ = &QF_active[me->prio];
    /* the queue must be able to accept the event (cannot overflow) */
    (9)
    Q_ASSERT(me->nUsed <= Q_ROM_BYTE(QF_pCB_->end));
    ++me->nUsed;
    /* update number of events */
    /* insert event into the ring buffer (FIFO) */
    (10)
    ((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].sig = sig;
    #if (Q_PARAM_SIZE != 0)
    (11)
    ((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].par = par;
    #endif
    (12)
    if (me->head == (uint8_t)0) {
        (13)
        me->head = Q_ROM_BYTE(QF_pCB_->end);
        /* wrap the head */
    }
    (14)
    --me->head;
}
(15)
QF_INT_UNLOCK();
}

QActive_post() 函数的签名取决于是否配置了带参数的事件。任务级别的事件发送操作总是在任务级别的临界区中进行。当事件队列为空时,新事件会直接复制到状态机内部的当前事件中。对应活动对象优先级的位会在 QF_readySet_ 位掩码中被设置,常量查找表 l_pow2Lkup[] 按如下方式初始化: l_pow2Lkup[p] == (1 << (p - 1)) ,其中 p 为 1 到 8 的所有优先级。

当配置了抢占式 QK - nano 内核时,会调用抢占式调度器来处理潜在的同步抢占(当一个活动对象向更高优先级的任务发送事件时会发生同步抢占)。全局变量 QF_pCB_ 保存指向 ROM 中活动对象控制块 &QF_active[me->prio] 的指针,该变量仅在 QP - nano 函数内部局部使用,用于避免栈上加载临时变量。

断言确保队列能够接受这个事件,新事件会复制到环形缓冲区的 head 索引处。 head 索引会检查是否需要环绕,若需要则移动到缓冲区的末尾,并且 head 索引总是递减。最后,解锁中断以离开临界区。

5. 从 ISR 级别发送事件(QActive_postISR())

QActive_postISR() 函数用于从 ISR 向活动对象发送事件,不能用于从活动对象发送事件,因为它使用特定于 ISR 的临界区机制。

下面是 QActive_postISR() 函数的代码:

#if (Q_PARAM_SIZE != 0)
void QActive_postISR(QActive *me, QSignal sig, QParam par)
#else
void QActive_postISR(QActive *me, QSignal sig)
#endif
{
    (1)
    #ifdef QF_ISR_NEST
    (2)
    #ifdef QF_ISR_KEY_TYPE
    (3)
    QF_ISR_KEY_TYPE key;
    (4)
    QF_ISR_LOCK(key);
    #else
    (5)
    QF_INT_LOCK();
    #endif
    #endif
    if (me->nUsed == (uint8_t)0) {
        ++me->nUsed;
        /* update number of events */
        Q_SIG(me) = sig;
        /* deliver the event directly */
        #if (Q_PARAM_SIZE != 0)
        Q_PAR(me) = par;
        #endif
        QF_readySet_ |= Q_ROM_BYTE(l_pow2Lkup[me->prio]);
        /* set the bit */
    }
    else {
        QF_pCB_ = &QF_active[me->prio];
        /* the queue must be able to accept the event (cannot overflow) */
        Q_ASSERT(me->nUsed <= Q_ROM_BYTE(QF_pCB_->end));
        ++me->nUsed;
        /* update number of events */
        /* insert event into the ring buffer (FIFO) */
        ((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].sig = sig;
        #if (Q_PARAM_SIZE != 0)
        ((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[me->head].par = par;
        #endif
        if (me->head == (uint8_t)0) {
            me->head = Q_ROM_BYTE(QF_pCB_->end);
            /* wrap the head */
        }
        --me->head;
    }
    #ifdef QF_ISR_NEST
    #ifdef QF_ISR_KEY_TYPE
    QF_ISR_UNLOCK(key);
    #else
    QF_INT_UNLOCK();
    #endif
    #endif
}

只有在允许中断嵌套时才会锁定中断。如果定义了 QF_ISR_KEY_TYPE ,QP - nano 使用“保存和恢复中断状态”的高级策略,需要一个临时变量 key 来保存中断状态并锁定中断;如果未定义 QF_ISR_KEY_TYPE ,则使用“无条件中断解锁”的简单策略,与任务级别相同。

需要注意的是,ISR 级别的事件发送操作 QActive_postISR() 不会调用 QK - nano 调度器,因为任务永远不能同步抢占 ISR。

6. 协作式“普通”内核

默认情况下,QP - nano 使用简单的协作式“普通”调度器。下面是实现整个“普通”内核的 QF_run() 函数代码:

#ifndef QK_PREEMPTIVE
void QF_run(void) {
    (2)
    static uint8_t const Q_ROM Q_ROM_VAR log2Lkup[] = {
        0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4
    };
    (3)
    static uint8_t const Q_ROM Q_ROM_VAR invPow2Lkup[] = {
        0xFF, 0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F
    };
    (4)
    static QActive *a;
    /* declared static to save stack space */
    (5)
    static uint8_t p;
    /* declared static to save stack space */
    /* trigger initial transitions in all registered active objects... */
    (6)
    for (p = (uint8_t)1; p <= (uint8_t)QF_MAX_ACTIVE; ++p) {
        (7)
        a = (QActive *)Q_ROM_PTR(QF_active[p].act);
        (8)
        Q_ASSERT(a != (QActive *)0);
        /* QF_active[p] must be initialized */
        (9)
        a->prio = p;
        /* set the priority of the active object */
        #ifndef QF_FSM_ACTIVE
        (10)
        QHsm_init((QHsm *)a);
        /* take the initial transition in HSM */
        #else
        (11)
        QFsm_init((QFsm *)a);
        /* take the initial transition in FSM */
        #endif
    }
    (12)
    QF_onStartup();
    /* invoke startup callback */
    (13)
    for (;;) {
        /* the event loop of the vanilla kernel */
        (14)
        QF_INT_LOCK();
        (15)
        if (QF_readySet_ != (uint8_t)0) {
            #if (QF_MAX_ACTIVE > 4)
            (16)
            if ((QF_readySet_ & 0xF0) != 0U) {
                /* upper nibble used? */
                (17)
                p = (uint8_t)(Q_ROM_BYTE(log2Lkup[QF_readySet_ >> 4]) + 4);
            }
            else
                /* upper nibble of QF_readySet_ is zero */
            #endif
            {
                (18)
                p = Q_ROM_BYTE(log2Lkup[QF_readySet_]);
            }
            (19)
            QF_INT_UNLOCK();
            (20)
            a = (QActive *)Q_ROM_PTR(QF_active[p].act);
            #ifndef QF_FSM_ACTIVE
            (21)
            QHsm_dispatch((QHsm *)a);
            /* dispatch to HSM */
            #else
            (22)
            QFsm_dispatch((QFsm *)a);
            /* dispatch to FSM */
            #endif
            (23)
            QF_INT_LOCK();
            (24)
            if ((--a->nUsed) == (uint8_t)0) {
                /* queue becoming empty? */
                (25)
                QF_readySet_ &= Q_ROM_BYTE(invPow2Lkup[p]);/* clear the bit */
            }
            else {
                (26)
                QF_pCB_ = &QF_active[a->prio];
                (27)
                Q_SIG(a)=((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[a->tail].sig;
                #if (Q_PARAM_SIZE != 0)
                (28)
                Q_PAR(a)=((QEvent *)Q_ROM_PTR(QF_pCB_->queue))[a->tail].par;
                #endif
                (29)
                if (a->tail == (uint8_t)0) {
                    /* wrap around? */
                    (30)
                    a->tail = Q_ROM_BYTE(QF_pCB_->end);
                    /* wrap the tail */
                }
                (31)
                --a->tail;
            }
            (32)
            QF_INT_UNLOCK();
        }
        (33)
        else {
            (34)
            QF_onIdle();
            /* see NOTE01 */
        }
    }
}
#endif

协作式普通内核仅在未配置抢占式 QK - nano 内核时进行编译。常量数组 log2Lkup[] 是二进制对数(以 2 为底)查找表,用于快速确定位掩码中最显著的 1 位;常量数组 invPow2Lkup[] 是按位取反的 2 的幂查找表,用于屏蔽 QF_readSet_ 位掩码中的位。

临时变量 a p 被定义为静态变量以节省栈空间。 QF_run() 函数中的静态变量 p QF_tick() 中的 p 不同,因为这两个函数会并发执行。

通过一个 for 循环,从最低优先级的活动对象开始触发所有活动对象的初始转换。通常,活动对象应该按优先级顺序初始化,因为最低优先级的活动对象往往有最长的事件队列,这在活动对象从初始转换中相互发送事件时可能很重要。

活动对象指针从 ROM 中的活动对象控制块获取,断言确保 QF_active[] 数组已正确初始化,并初始化活动对象的内部优先级。根据是否配置了有限状态机(FSM)或层次状态机(HSM),触发相应的初始转换。

QF_onStartup() 回调函数用于配置和启动中断,该函数在应用程序级别(在 BSP 中)实现。

“普通”内核的事件循环开始,首先锁定中断以访问 QF_readySet_ 就绪集。如果就绪集不为空,内核需要处理一些事件。为了快速找到具有非空事件队列的最高优先级活动对象,使用二进制对数查找表。由于 QP - nano 中的 log2Lkup[] 查找表只能处理 0 到 15 的值,当活动对象数量大于查找表范围时,会先测试 QF_readySet_ 位掩码的高半字节。

如果高半字节不为零,将高半字节右移 4 位并应用 log2Lkup[] 查找表,然后加上 4 得到活动对象的优先级;否则,直接对低半字节应用 log2Lkup[] 查找表。解锁中断后,获取活动对象指针,并根据是否配置了 FSM 或 HSM 进行事件分发。

再次锁定中断以更新活动对象事件队列的状态,队列中的事件数量递减并与零比较。如果队列变为空,使用 invPow2Lkup[] 查找表清除 QF_readySet_ 位掩码中对应的位;否则,将下一个事件从环形缓冲区的 tail 索引处复制到状态机内部的当前事件中,检查 tail 索引是否需要环绕并递减。最后解锁中断。

当所有活动对象的事件队列都为空时,进入空闲条件,调用 QF_onIdle() 回调函数,该函数通常在应用程序级别(在 BSP 中)实现,用于让应用程序有机会将 MCU 置于低功耗睡眠模式。

7. 中断处理和空闲处理

在协作式“普通”内核下,中断处理与前后台架构一样简单,通常可以使用 C 编译器生成的 ISR。对于 QP - nano,需要在中断嵌套策略上保持一致。如果在 qpn_port.h 头文件中定义了宏 QF_ISR_NEST 来配置嵌套中断,在调用 QP - nano 函数 QActive_postISR() QF_tick() 之前需要一致地解锁中断。

空闲回调 QF_onIdle() 在 QP - nano 中的工作方式与完整版本的 QP 完全相同,可以参考相关示例为各种 CPU 定义此回调函数。

8. 抢占式实时内核(QK - nano)

QP - nano 包含一个名为 QK - nano 的抢占式实时内核,其工作方式与完整版本 QP 中的 QK 抢占式内核非常相似。在决定使用像 QK - nano 这样的抢占式内核之前,建议先了解相关知识。

需要注意的是,抢占式 QK - nano 内核比非抢占式普通内核对目标 CPU 和编译器的要求更高。一般来说,如果处理器和编译器满足以下要求,就可以使用 QK - nano:处理器支持能够容纳栈变量(不仅仅是返回地址)的硬件栈。

综上所述,QP - nano 提供了丰富的功能和灵活的配置选项,无论是协作式内核还是抢占式内核,都能满足不同场景的需求。在实际应用中,需要根据具体的硬件平台和应用需求来选择合适的内核和配置方式。

QP-nano 实时框架深入解析

9. 内核选择与应用场景分析

在实际应用中,选择合适的内核对于系统的性能和稳定性至关重要。下面通过一个表格来对比协作式“普通”内核和抢占式 QK - nano 内核的特点和适用场景:
| 内核类型 | 特点 | 适用场景 |
| — | — | — |
| 协作式“普通”内核 | 简单、实现成本低;任务按顺序执行,不会出现任务抢占;对硬件和编译器要求较低 | 对实时性要求不高、任务之间协作性强、硬件资源有限的场景,如一些简单的传感器数据采集系统 |
| 抢占式 QK - nano 内核 | 支持任务抢占,能更好地处理实时事件;可以提高系统的响应速度;对硬件和编译器要求较高 | 对实时性要求高、有紧急任务需要及时处理的场景,如工业自动化控制系统、航空航天设备等 |

10. 代码优化建议

为了提高 QP - nano 系统的性能和效率,可以从以下几个方面对代码进行优化:
- 栈空间优化 :如前面所述,将一些临时变量声明为静态变量可以节省栈空间。例如,在 QF_run() 函数中, a p 被定义为静态变量。在编写代码时,对于一些频繁使用且生命周期较长的临时变量,可以考虑使用静态变量。
- 查找表优化 :合理使用查找表可以提高代码的执行效率。例如, log2Lkup[] invPow2Lkup[] 查找表,通过预先计算好的值,避免了复杂的计算过程,加快了查找速度。在实际应用中,如果有类似的计算需求,可以考虑使用查找表来优化。
- 中断处理优化 :在中断处理中,尽量减少中断服务程序(ISR)的执行时间,避免在 ISR 中进行复杂的操作。可以将一些耗时的操作放到任务中处理,以保证系统的实时性。例如,在 ISR 中只进行简单的事件记录,然后在任务中进行具体的处理。

11. 常见问题及解决方法

在使用 QP - nano 过程中,可能会遇到一些常见问题,下面是一些问题及对应的解决方法:
| 问题 | 现象 | 解决方法 |
| — | — | — |
| 事件队列溢出 | 事件无法正常添加到队列中,系统出现异常 | 检查事件队列的长度,根据实际需求调整队列大小;优化事件处理逻辑,减少事件的产生 |
| 任务调度异常 | 任务无法按预期执行,出现任务阻塞或抢占异常 | 检查内核配置是否正确,确保选择了合适的内核类型;检查任务优先级设置,避免优先级冲突 |
| 中断处理异常 | 中断服务程序执行异常,系统响应不及时 | 检查中断嵌套策略是否正确,确保在调用相关函数时中断状态的一致性;优化中断服务程序的代码,减少执行时间 |

12. 总结与展望

QP - nano 是一个功能强大、灵活可配置的实时框架,它提供了协作式“普通”内核和抢占式 QK - nano 内核,满足了不同应用场景的需求。通过合理使用事件队列、就绪集等机制,QP - nano 可以高效地处理事件和任务调度。

在未来的开发中,随着硬件技术的不断发展,QP - nano 可以进一步优化以适应更复杂的应用场景。例如,可以考虑支持更多的硬件平台,提高系统的兼容性;优化内核算法,提高系统的实时性和性能。同时,对于开发者来说,需要深入理解 QP - nano 的原理和机制,根据具体需求进行合理的配置和优化,以充分发挥 QP - nano 的优势。

下面是一个 mermaid 流程图,展示了 QP - nano 系统的整体工作流程:

graph LR
    A[系统启动] --> B[初始化活动对象]
    B --> C[调用 QF_onStartup()]
    C --> D{是否配置 QK - nano 内核}
    D -- 是 --> E[使用 QK - nano 内核进行任务调度]
    D -- 否 --> F[使用协作式“普通”内核进行任务调度]
    E --> G[事件处理循环]
    F --> G
    G --> H{是否有新事件}
    H -- 是 --> I[处理新事件]
    H -- 否 --> J[调用 QF_onIdle()]
    I --> G
    J --> G

通过以上的分析和介绍,相信读者对 QP - nano 实时框架有了更深入的了解。在实际应用中,可以根据具体需求选择合适的内核和配置方式,同时注意代码优化和问题解决,以构建高效、稳定的实时系统。

潮汐研究作为海洋科学的关键分支,融合了物理海洋学、地理信息系统及水利工程等多领域知识。TMD2.05.zip是一套基于MATLAB环境开发的潮汐专用分析工具集,为科研人员与工程实践者提供系统化的潮汐建模与计算支持。该工具箱通过模块化设计实现了两大核心功能: 在交互界面设计方面,工具箱构建了图形化操作环境,有效降低了非专业用户的操作门槛。通过预设参数输入模块(涵盖地理坐标、时间序列、测站数据等),用户可自主配置模型运行条件。界面集成数据加载、参数调整、可视化呈现及流程控制等标准化组件,将复杂的数值运算过程转化为可交互的操作流程。 在潮汐预测模块中,工具箱整合了谐波分解法与潮流要素解析法等数学模型。这些算法能够解构潮汐观测数据,识别关键影响要素(包括K1、O1、M2等核心分潮),并生成不同时间尺度的潮汐预报。基于这些模型,研究者可精准推算特定海域的潮位变化周期与振幅特征,为海洋工程建设、港湾规划设计及海洋生态研究提供定量依据。 该工具集在实践中的应用方向包括: - **潮汐动力解析**:通过多站点观测数据比对,揭示区域主导潮汐成分的时空分布规律 - **数值模型构建**:基于历史观测序列建立潮汐动力学模型,实现潮汐现象的数字化重构与预测 - **工程影响量化**:在海岸开发项目中评估人工构筑物对自然潮汐节律的扰动效应 - **极端事件模拟**:建立风暴潮与天文潮耦合模型,提升海洋灾害预警的时空精度 工具箱以"TMD"为主程序包,内含完整的函数库与示例脚本。用户部署后可通过MATLAB平台调用相关模块,参照技术文档完成全流程操作。这套工具集将专业计算能力与人性化操作界面有机结合,形成了从数据输入到成果输出的完整研究链条,显著提升了潮汐研究的工程适用性与科研效率。 资源来源于网络分享,仅用于学习交流使用,请勿用于商业,如有侵权请联系我删除!
内容概要:本文围绕SSH安全连接配置在毕业设计中的实际应用展开,深入解析了SSH协议的核心功能,包括身份验证、数据加密和安全通道建立。文章重点介绍了SSH密钥对生成、高级配置优化(如自定义端口、密钥路径、心跳机制等),并通过Python结合Paramiko库实现自动化SSH连接与远程命令执行的完整案例,应用于智能家居控制系统项目中。代码层面详细剖析了密钥认证、连接参数设置、错误处理机制、命令执行流程及资源管理策略,并提出了安全增强建议,如主机密钥验证和连接池管理。此外,拓展了SSH在远程数据库访问、代码自动部署等场景的应用,展望了量子安全SSH、零信任架构集成、AI辅助安全监测及WebSSH技术的发展趋势。; 适合人群:具备基本Linux和网络基础知识,正在开展涉及远程通信或系统管理类毕业设计的学生,以及希望提升SSH实战能力的初级开发者; 使用场景及目标:①掌握SSH密钥认证与安全配置方法,构建可靠的远程开发环境;②在物联网、嵌入式系统等毕业项目中实现安全远程控制与自动化运维;③理解SSH底层机制并应用于实际工程问题; 阅读建议:学习过程中应结合文中代码实例进行实操演练,重点关注异常处理与安全性配置,在真实环境中逐步替换不安全策略(如AutoAddPolicy),并尝试扩展至更多应用场景。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值