title: rtthread定时器的实现
date: 2020-10-25 15:57:01
tags: rtthread


每当线程需要延时的时候,就初始化 remaining_tick 需要延时的时间,然后将线程挂起,这里的挂起只是将线程在线程就绪优先级组中对应的位清 0,并不会将线程从线程优先级表(即就绪列表)中删除。当每次时基中断(SysTick 中断)来临时,就扫描就绪列表中的每个线程的 remaining_tick,如果 remaining_tick 大于 0 则递减一次,然后判断 remaining_tick 是否为 0,如果为 0 则表示延时时间到,将该线程就绪(即将线程在线程就绪优先级组中对应的位置位),然后等待系统下一次调度。这种延时的缺点是,在每个时基中断中需要对所有线程都扫描一遍,费时,优点是容易理解。之所以先这样讲解是为了慢慢地过度到 RT-Thread 定时器的讲解。

在 RT-Thread 中,每个线程都内置一个定时器,当线程需要延时的时候,则先将线程挂起,然后内置的定时器就会启动,并且将定时器插入到一个全局的系统定时器列表rt_timer_list,这个全局的系统定时器列表维护着一条双向链表,每个节点代表了正在延时的线程的定时器,节点按照延时时间大小做升序排列。当每次时基中断(SysTick 中断)来临时,就扫描系统定时器列表的第一个定时器,看看延时时间是否到,如果到则让该定时器对应的线程就绪,如果延时时间不到,则退出扫描,因为定时器节点是按照延时时间升序排列的,第一个定时器延时时间不到期的话,那后面的定时器延时时间自然不到期。比起第一种方法,这种方法就大大缩短了寻找延时到期的线程的时间。

定时器实现

系统定时器列表

在 RT-Thread 中,定义了一个全局的系统定时器列表,当线程需要延时时,就先把线程挂起,然后线程内置的定时器将线程挂起到这个系统定时器列表中,系统定时器列表维护着一条双向链表,节点按照定时器的延时时间的大小做升序排列。

redef.h

#ifndef RT_TIMER_SKIP_LIST_LEVEL
#define RT_TIMER_SKIP_LIST_LEVEL          1
#endif

timer.c

/* hard timer list */
static rt_list_t rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL];

数组只有一个成员。

系统定时器列表初始化

void rt_system_timer_init(void)
{int i;for (i = 0; i < sizeof(rt_timer_list) / sizeof(rt_timer_list[0]); i++){rt_list_init(rt_timer_list + i);}
}

即初始化数组每个成员节点的nextprev两个指向自己本身。

定时器结构体定义

rtdef.h

/*** 定时器结构体*/
struct rt_timer
{struct rt_object parent;                         /* 从 rt_object 继承 */rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];void (*timeout_func)(void *parameter);           /* 超时函数 */void            *parameter;                      /* 超时函数形参 */rt_tick_t        init_tick;                      /* 定时器实际需要延时的时间 */rt_tick_t        timeout_tick;                   /* 定时器实际超时时的系统节拍数 */
};
typedef struct rt_timer *rt_timer_t;

定时器也属内核对象,也会在结构体包含内核对象类型的成员。

row[RT_TIMER_SKIP_LIST_LEVEL]定时器自身的节点,通过该节点可以实现将定时器插入到系统定

时器列表。

(*timeout_func)(void *parameter)定时器超时函数,定时器到期会调用相应的超时函数。

timeout_tick:我们知道系统定义了一个全局的系统时基计数器 rt_tick(在 clock.c 中定义),每产生一次系统时基中断(即 SysTick 中断)时,rt_tick 计数加一。假设线程要延时 10 个 tick,即 init_tick 等于 10,此时 rt_tick 等于 2,那么 timeout_tick 就等于 10 加 2 等于 12,当 rt_tick 递增到 12 的时候,线程延时到期,这个就是 timeout_tick 的实际含义。

线程控制块内置定时器

rtdef.h

struct rt_thread
{/* rt 对象 */char        name[RT_NAME_MAX];    /* 对象的名字 */rt_uint8_t  type;                 /* 对象类型 */rt_uint8_t  flags;                /* 对象的状态 */rt_list_t   list;                 /* 对象的列表节点 */rt_list_t   tlist;                /* 线程链表节点 */void        *sp;               /* 线程栈指针 */void        *entry;                  /* 线程入口地址 */void        *parameter;             /* 线程形参 */    void        *stack_addr;          /* 线程起始地址 */rt_uint32_t stack_size;           /* 线程栈大小,单位为字节 */rt_ubase_t  remaining_tick;       /* 用于实现阻塞延时 */rt_uint8_t  current_priority;     /* 当前优先级 */rt_uint8_t  init_priority;        /* 初始优先级 */rt_uint32_t number_mask;          /* 当前优先级掩码 */rt_err_t    error;                /* 错误码 */rt_uint8_t  stat;                 /* 线程的状态 */struct rt_timer thread_timer;     /* 内置的线程定时器 */
};
typedef struct rt_thread *rt_thread_t;

定时器初始化函数

timer.c

/*** 该函数用于初始化一个定时器,通常该函数用于初始化一个静态的定时器** @param timer 静态定时器对象* @param name 定时器的名字* @param timeout 超时函数* @param parameter 超时函数形参* @param time 定时器的超时时间* @param flag 定时器的标志*/
void rt_timer_init(rt_timer_t  timer,const char *name,void (*timeout)(void *parameter),void       *parameter,rt_tick_t   time,rt_uint8_t  flag)
{/* 定时器对象初始化 */rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);/* 定时器初始化 */_rt_timer_init(timer, timeout, parameter, time, flag);
}static void _rt_timer_init(rt_timer_t timer,void (*timeout)(void *parameter),void      *parameter,rt_tick_t  time,rt_uint8_t flag)
{int i;/* 设置标志 */timer->parent.flag  = flag;/* 先设置为非激活态 */timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;timer->timeout_func = timeout;timer->parameter    = parameter;/* 初始化 定时器实际超时时的系统节拍数 */timer->timeout_tick = 0;/* 初始化 定时器需要超时的节拍数 */timer->init_tick    = time;/* 初始化定时器的内置节点 */for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++){rt_list_init(&(timer->row[i]));}
}

定时器的标志位:

rtdef.h

/*** 时钟 & 定时器 宏定义*/
#define RT_TIMER_FLAG_DEACTIVATED       0x0     /* 定时器没有激活 */
#define RT_TIMER_FLAG_ACTIVATED         0x1     /* 定时器已经激活 */
#define RT_TIMER_FLAG_ONE_SHOT          0x0     /* 单次定时 */
#define RT_TIMER_FLAG_PERIODIC          0x2     /* 周期定时 */#define RT_TIMER_FLAG_HARD_TIMER        0x0     /* 硬件定时器,定时器回调函数在 tick isr中调用 */#define RT_TIMER_FLAG_SOFT_TIMER        0x4     /* 软件定时器,定时器回调函数在定时器线程中调用 */

定时器相关函数

删除函数

rt_inline void _rt_timer_remove(rt_timer_t timer)
{int i;for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++){rt_list_remove(&timer->row[i]);}
}

停止函数

实现的算法主要分成两步,先将定时器从系统定时器列表删除,然后改变定时器的状态为非 active 即可

/*** 该函数将停止一个定时器** @param timer 将要被停止的定时器** @return 操作状态, RT_EOK on OK, -RT_ERROR on error*/
rt_err_t rt_timer_stop(rt_timer_t timer)
{register rt_base_t level;/* 只有active的定时器才能被停止,否则退出返回错误码 */if (!(timer->parent.flag & RT_TIMER_FLAG_ACTIVATED))return -RT_ERROR;/* 关中断 */level = rt_hw_interrupt_disable();/* 将定时器从定时器列表删除 */_rt_timer_remove(timer);/* 开中断 */rt_hw_interrupt_enable(level);/* 改变定时器的状态为非active */timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;return RT_EOK;
}

定时器控制函数

根据不同的形参来设置定时器的状态和初始时间值

/*** 该函数将获取或者设置定时器的一些选项* * @param timer 将要被设置或者获取的定时器* @param cmd 控制命令* @param arg 形参** @return RT_EOK*/
rt_err_t rt_timer_control(rt_timer_t timer, int cmd, void *arg)
{switch (cmd){case RT_TIMER_CTRL_GET_TIME:*(rt_tick_t *)arg = timer->init_tick;break;case RT_TIMER_CTRL_SET_TIME:timer->init_tick = *(rt_tick_t *)arg;break;case RT_TIMER_CTRL_SET_ONESHOT:timer->parent.flag &= ~RT_TIMER_FLAG_PERIODIC;break;case RT_TIMER_CTRL_SET_PERIODIC:timer->parent.flag |= RT_TIMER_FLAG_PERIODIC;break;}return RT_EOK;
}

相关宏定义

#define RT_TIMER_CTRL_SET_TIME          0x0     /* 设置定时器定时时间 */
#define RT_TIMER_CTRL_GET_TIME          0x1     /* 获取定时器定时时间 */
#define RT_TIMER_CTRL_SET_ONESHOT       0x2     /* 修改定时器为一次定时 */
#define RT_TIMER_CTRL_SET_PERIODIC      0x3     /* 修改定时器为周期定时 */

*启动函数

/*** 启动定时器** @param timer 将要启动的定时器** @return 操作状态, RT_EOK on OK, -RT_ERROR on error*/
rt_err_t rt_timer_start(rt_timer_t timer)
{unsigned int row_lvl = 0;rt_list_t *timer_list;register rt_base_t level;rt_list_t *row_head[RT_TIMER_SKIP_LIST_LEVEL];unsigned int tst_nr;static unsigned int random_nr;/* 关中断 */level = rt_hw_interrupt_disable();/* 将定时器从系统定时器列表移除 */_rt_timer_remove(timer);/* 改变定时器的状态为非active */timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;/* 开中断 */rt_hw_interrupt_enable(level);/* 获取 timeout tick,最大的timeout tick 不能大于 RT_TICK_MAX/2 */timer->timeout_tick = rt_tick_get() + timer->init_tick;/* 关中断 */level = rt_hw_interrupt_disable();/* 将定时器插入到定时器列表 *//* 获取系统定时器列表根节点地址,rt_timer_list是一个全局变量 */timer_list = rt_timer_list;/* 获取系统定时器列表第一条链表根节点地址 */row_head[0]  = &timer_list[0];/* 因为RT_TIMER_SKIP_LIST_LEVEL等于1,这个循环只会执行一次 */for (row_lvl = 0; row_lvl < RT_TIMER_SKIP_LIST_LEVEL; row_lvl++){/* 列表不为空,当没有定时器被插入到系统定时器列表时,该循环不执行 */for (; row_head[row_lvl] != timer_list[row_lvl].prev; row_head[row_lvl]  = row_head[row_lvl]->next){struct rt_timer *t;/* 获取定时器列表节点地址 */rt_list_t *p = row_head[row_lvl]->next;/* 根据节点地址获取父结构的指针 */t = rt_list_entry(p,                 /* 节点地址 */ struct rt_timer,   /* 节点所在父结构的数据类型 */row[row_lvl]);     /* 节点在父结构中叫什么,即名字 *//* 两个定时器的超时时间相同,则继续在定时器列表中寻找下一个节点 */if ((t->timeout_tick - timer->timeout_tick) == 0){continue;}/* 两个定时器的超时时间相同,则继续在定时器列表中寻找下一个节点 */else if ((t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2){break;}}/* 条件不会成真,不会被执行 */if (row_lvl != RT_TIMER_SKIP_LIST_LEVEL - 1){row_head[row_lvl + 1] = row_head[row_lvl] + 1; }            }/* random_nr是一个静态变量,用于记录启动了多少个定时器 */random_nr++;tst_nr = random_nr;/* 将定时器插入到系统定时器列表 */rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - 1],       /* 双向列表根节点地址 */&(timer->row[RT_TIMER_SKIP_LIST_LEVEL - 1])); /* 要被插入的节点的地址 *//* RT_TIMER_SKIP_LIST_LEVEL 等于1,该for循环永远不会执行 */for (row_lvl = 2; row_lvl <= RT_TIMER_SKIP_LIST_LEVEL; row_lvl++){if (!(tst_nr & RT_TIMER_SKIP_LIST_MASK))rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - row_lvl],&(timer->row[RT_TIMER_SKIP_LIST_LEVEL - row_lvl]));elsebreak;tst_nr >>= (RT_TIMER_SKIP_LIST_MASK + 1) >> 1;}/* 设置定时器标志位为激活态 */timer->parent.flag |= RT_TIMER_FLAG_ACTIVATED;/* 开中断 */rt_hw_interrupt_enable(level);return -RT_EOK;
}

一个初始化好的空的系统定时器列表示意图,只有一个成员

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mEZjpQI7-1608216004386)(https://i.loli.net/2020/10/25/BneYF4PWR3TScXj.png)]

假设现在有3个定时器要启动。

定时器1: timeout_tick=4

定时器2: timeout_tick=2

定时器3: timeout_tick=3

插入定时器1(timeout_tick=4)

  1. 将定时器1节点从系统定时器移除。(相当于将节点取下晾衣架)
  2. 将定时器1状态为非active。
  3. 获取定时器1的超时节拍。(定时器超时节拍 = 系统节拍 + 定时器延时节拍)
  4. 获取系统定时器列表根节点地址到timer_list
  5. 获取链表第一个节点的地址到row_head[0] 。(row_head[0] = &timer_list[0];
  6. 应为链表为空,所以不执行for循环。
  7. 将定时器1插入系统定时器列表根节点。
  8. 将定时器1状态为active。

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PCYQA4Gu-1608216004394)(https://i.loli.net/2020/10/25/kzacUWSnIOo4Mb3.png)]

插入定时器2(timeout_tick=2)

前五步一样。

  1. 此时链表不为空,执行for循环。
  2. 获取定时器1的节点地址给 p
  3. 根据定时器1的节点地址p得出定时器1的地址给t
  4. 较定时器1(t) 和定时器2(timer) 超时时间大小。如果时间相同则在定时器列表中寻找下一个节点。如果定时器1(t)大,跳出for循环。即将定时器2节点插入到定时器1前面。(超时时间大的在后面)
  5. 后面几步一样。

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wT9lozaw-1608216004396)(https://i.loli.net/2020/10/25/hBITaoXnKcv1yEl.png)]

插入定时器3(timeout_tick=3)

前五步一样。

  1. 第一次执行for循环,t(定时器2_timeout_tick=2) < timer(定时器3_timeout_tick=3)。
  2. row_head[row_lvl] = row_head[row_lvl]->next。执行第二次for循环。
  3. 此时t(定时器1_timeout_tick=4) > timer(定时器3_timeout_tick=3)。
  4. 定时器3节点插入到定时器1节点之前。

如图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tjk17mOV-1608216004399)(https://i.loli.net/2020/10/25/iV6qvufY9FzWwJb.png)]

扫描函数

用于扫描系统定时器列表,查询定时器的延时是否到期,如果到期则让对应的线程就绪。

/*** 该函数用于扫描系统定时器列表,当有超时事件发生时* 就调用对应的超时函数** @note 该函数在操作系统定时器中断中被调用*/
void rt_timer_check(void)
{struct rt_timer *t;rt_tick_t current_tick;register rt_base_t level;/* 获取系统时基计数器rt_tick的值 */current_tick = rt_tick_get();/* 关中断 */level = rt_hw_interrupt_disable();/* 系统定时器列表不为空,则扫描定时器列表 */while (!rt_list_isempty(&rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1])){/* 获取第一个节点定时器的地址 */t = rt_list_entry(rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,   /* 节点地址 */struct rt_timer,                                    /* 节点所在的父结构的数据类型 */ row[RT_TIMER_SKIP_LIST_LEVEL - 1]);                 /* 节点在父结构的成员名 */if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2){/* 先将定时器从定时器列表移除 */_rt_timer_remove(t);/* 调用超时函数 */t->timeout_func(t->parameter);/* 重新获取 rt_tick */current_tick = rt_tick_get();/* 周期定时器 */if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&(t->parent.flag & RT_TIMER_FLAG_ACTIVATED)){/* 启动定时器 */t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;rt_timer_start(t);}/* 单次定时器 */else{/* 停止定时器 */t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;}}elsebreak;}/* 开中断 */rt_hw_interrupt_enable(level);
}

通过判断current_tick - t->timeout_tick > 0 判断定时器是否到期。到期移除定时器并调用超时函数。最后判断是否为周期定时器。

超时函数:

thread.c

/*** 线程超时函数* 当线程延时到期或者等待的资源可用或者超时时,该函数会被调用** @param parameter 超时函数的形参*/
void rt_thread_timeout(void *parameter)
{struct rt_thread *thread;thread = (struct rt_thread *)parameter;/* 设置错误码为超时 */thread->error = -RT_ETIMEOUT;/* 将线程从挂起列表中删除 */rt_list_remove(&(thread->tlist));/* 将线程插入到就绪列表 */rt_schedule_insert_thread(thread);/* 系统调度 */rt_schedule();
}
  1. 设置线程错误码为超时。
  2. 将线程从挂起列表中删除,前提是线程在等待某些资源而被挂起到挂起列表,如果只是延时到期,则这个只是空操作。
  3. 将线程就绪。执行系统调度。

修改代码

线程初始化函数

rt_err_t rt_thread_init(struct rt_thread *thread,const char       *name,void (*entry)(void *parameter),void             *parameter,void             *stack_start,rt_uint32_t       stack_size,rt_uint8_t        priority)
{/* 线程对象初始化 *//* 线程结构体开头部分的成员就是rt_object_t类型 */rt_object_init((rt_object_t)thread, RT_Object_Class_Thread, name);rt_list_init(&(thread->tlist));thread->entry = (void *)entry;thread->parameter = parameter;thread->stack_addr = stack_start;thread->stack_size = stack_size;/* 初始化线程栈,并返回线程栈指针 */thread->sp = (void *)rt_hw_stack_init( thread->entry, thread->parameter,(void *)((char *)thread->stack_addr + thread->stack_size - 4) );thread->init_priority    = priority;thread->current_priority = priority;thread->number_mask = 0;/* 错误码和状态 */thread->error = RT_EOK;thread->stat  = RT_THREAD_INIT;/* 初始化线程定时器 */rt_timer_init(&(thread->thread_timer),     /* 静态定时器对象 */thread->name,                /* 定时器的名字,直接使用的是线程的名字 */rt_thread_timeout,           /* 超时函数 */thread,                      /* 超时函数形参 */0,                           /* 延时时间 */RT_TIMER_FLAG_ONE_SHOT);     /* 定时器的标志 */return RT_EOK;
}

线程延时函数

rt_err_t rt_thread_delay(rt_tick_t tick)
{return rt_thread_sleep(tick);
}
/*** 该函数将让当前线程睡眠一段时间,单位为tick* * @param tick 睡眠时间,单位为tick** @return RT_EOK*/
rt_err_t rt_thread_sleep(rt_tick_t tick)
{register rt_base_t temp;struct rt_thread *thread;/* 关中断 */temp = rt_hw_interrupt_disable();/* 获取当前线程的线程控制块 */thread = rt_current_thread;/* 挂起线程 */rt_thread_suspend(thread);/* 设置线程定时器的超时时间 */rt_timer_control(&(thread->thread_timer), RT_TIMER_CTRL_SET_TIME, &tick);/* 启动定时器 */rt_timer_start(&(thread->thread_timer));/* 开中断 */rt_hw_interrupt_enable(temp);/* 执行系统调度 */rt_schedule();return RT_EOK;
}
/*** 该函数用于挂起指定的线程* @param thread 要被挂起的线程** @return 操作状态, RT_EOK on OK, -RT_ERROR on error** @note 如果挂起的是线程自身,在调用该函数后,* 必须调用rt_schedule()进行系统调度* */
rt_err_t rt_thread_suspend(rt_thread_t thread)
{register rt_base_t temp;/* 只有就绪的线程才能被挂起,否则退出返回错误码 */if ((thread->stat & RT_THREAD_STAT_MASK) != RT_THREAD_READY){return -RT_ERROR;}/* 关中断 */temp = rt_hw_interrupt_disable();/* 改变线程状态 */thread->stat = RT_THREAD_SUSPEND;/* 将线程从就绪列表删除 */rt_schedule_remove_thread(thread);/* 停止线程定时器 */rt_timer_stop(&(thread->thread_timer));/* 开中断 */rt_hw_interrupt_enable(temp);return RT_EOK;
}

修改系统时基更新函数

void rt_tick_increase(void)
{/* 系统时基计数器加1操作,rt_tick是一个全局变量 */++ rt_tick;/* 扫描系统定时器列表 */rt_timer_check();
}

rtthread定时器的实现相关推荐

  1. rt-thread的定时器管理源码分析

    1 前言 rt-thread可以采用软件定时器或硬件定时器来实现定时器管理的,所谓软件定时器是指由操作系统提供的一类系统接口,它构建在硬件定时器基础之上,使系统能够提供不受数目限制的定时器服务.而硬件 ...

  2. RTT学习笔记3-时钟定时器管理

    基本知识 RT-Thread 中,时钟节拍的长度可以根据 RT_TICK_PER_SECOND 的定义来调整,等于 1/RT_TICK_PER_SECOND 秒 获取时钟节拍 rt_tick_t rt ...

  3. rtthread工业使用_rtthread使用总结

    RT-Thread 中,实际上线程并不存在运行状态,就绪状态和运行状态是等同的. 若某线程运行完毕,系统将自动删除线程:自动执行 rt_thread_exit() 函数,先将该线程从系统就绪队列中删除 ...

  4. RT_Thread_软件定时器

    1.定时器分类 1.1.硬件定时器(MCU提供) 精度很高,可以达到纳秒级别,并且是中断触发方式. 由外部晶振提供给芯片输入时钟,到达设定时间值后芯片中断控制器产生时钟中断. 1.2.软件定时器(OS ...

  5. RT-Thread-学习分析(详细版)

    RT-Thread简介–>>>下载资料来源于RT-Thread文档中心 RT-Thread API参考手册 3.1.1 具体包括以下部分: 内核层:RT-Thread 内核,是 RT ...

  6. RT-Thread 软件定时器(学习笔记)

    本文参考自[野火EmbedFire]<RT-Thread内核实现与应用开发实战--基于STM32>,仅作为个人学习笔记.更详细的内容和步骤请查看原文(可到野火资料下载中心下载) 文章目录 ...

  7. rtthread studio与正点原子apollo(3)--硬件定时器HTIMER

    rtthread studio与正点原子apollo[3]--硬件定时器HTIMER 前言 一.软件定时器和硬件定时器? 二.HTIMER使用详解 1.RT-Thread studio配置 2.功能代 ...

  8. RT-thread内核之进程间通信

    一.进程间通信机制 rt-thread操作系统的IPC(Inter-Process Communication,进程间同步与通信)包含有中断锁.调度器锁.信号量.互斥锁.事件.邮箱.消息队列.其中前5 ...

  9. rt-thread端口时钟使能_(2)RTThread启动过程分析

    点击上方蓝字,关注微联智控工作室 可点击右上角的 -,分享这篇文章 在一些不使用操作系统的单片机软件工程里面,除了汇编启动文件之外,普遍认为程序入口就是main函数,很多程序代码都是从main函数开始 ...

最新文章

  1. 三星下一代手机芯片由AI来设计,EDA行业老大提供技术
  2. struct_config.xml中action/set-property标签的用法
  3. camel java_与Java EE和Camel的轻量级集成
  4. 常用的JPA标记 (转)
  5. php 的超全局数组,PHP超全局数组(Superglobals)介绍
  6. Python数据结构————二叉查找树的实现
  7. xhr请求python_Python爬虫进阶必备!关于某电商网站的加密请求头!
  8. VS中读取NMEA数据进行定位精度分析
  9. 记录一下gitHub跑项目的步骤
  10. tipask 修改,临时的(暂没进行很好的全面考虑,为上线用)
  11. 2021大三学习机器学习课程手杖之机器学习基本概念的理解
  12. ssm基于jsp高校选课系统毕业设计源码291627
  13. 转专业计算机常见的名词解释,迎鲜肉第9弹|大学生活必知名词解释
  14. 天使投资人徐小平:最爱理性狂热创业者
  15. Vue2/3 脚手架搭建
  16. GitHub Android Libraries Top 100 简介
  17. 国富论总结(第一卷 1-3 章)
  18. Docker 使用镜像
  19. SQL创建一个学生信息表
  20. 如何在iPhone上设置和使用Siri

热门文章

  1. 下载的PPT模板有水印怎么去除?
  2. 怎么下载淘宝主的视频
  3. 小米集团2020全球校园招聘正式开启!
  4. mac系统卸载亚信安全助手
  5. markdown实现表格内换行
  6. centos linux 快捷键,centos 快捷键大全
  7. Unity Transform与Hierachy
  8. 双边滤波器cv2.bilateralFilter
  9. smobiler仿自如app筛选页面
  10. 复杂项目的版本管理及git分支管理建议