PX4/Pixhawk---uORB深入理解和应用
The Instructions of uORB
『PX4/Pixhawk』
『软件体系结构』
『uORB』
『主题发布』
『主题订阅』
1 简介
1.1 PX4/Pixhawk的软件体系结构
PX4/Pixhawk的软件体系结构主要被分为四个层次,这可以让我们更好的理解PX4/Pixhawk的软件架构和运作:
- 应用程序的API:这个接口提供给应用程序开发人员,此API旨在尽可能的精简、扁平及隐藏其复杂性。
- 应用程序框架: 这是为操作基础飞行控制的默认程序集(节点)。
- 库: 这一层包含了所有的系统库和基本交通控制的函数。
- 操作系统: 最后一层提供硬件驱动程序,网络,UAVCAN和故障安全系统。
uORB(Micro Object Request Broker,微对象请求代理器)是PX4/Pixhawk系统中非常重要且关键的一个模块,它肩负了整个系统的数据传输任务,所有的传感器数据、GPS、PPM信号等都要从芯片获取后通过uORB进行传输到各个模块进行计算处理。实际上uORB是一套跨「进程
」 的IPC通讯模块。在Pixhawk中, 所有的功能被独立以进程模块为单位进行实现并工作。而进程间的数据交互就由为重要,必须要能够符合实时、有序的特点。
Pixhawk使用的是NuttX实时ARM系统,uORB实际上是多个进程打开同一个设备文件,进程间通过此文件节点进行数据交互和共享。进程通过命名的「总线
」交换的消息称之为「主题
」(topic),在Pixhawk 中,一个主题仅包含一种消息类型,通俗点就是数据类型。每个进程可以「订阅
」或者「发布
」主题,可以存在多个发布者,或者一个进程可以订阅多个主题,但是一条总线上始终只有一条消息。
1.2 PX4/Pixhawk应用程序框架
应用层中操作基础飞行的应用之间都是隔离的,这样提供了一种安保模式,以确保基础操作独立的高级别系统状态的稳定性。而沟通它们的就是uORB。
2 uORB文件夹说明
2.1 uORB文件夹结构
2.2 文件/目录说明
topics : 系统通用接口定义的标准主题,比如电池电量转态、GPS的位置参数等
module.mk : uORB模块makefile文件
objects_common.cpp: 通用接口标准主题定义集合,如添加新主题在这里定义
ORBMap.hpp : 对象请求器节点链表管理(驱动节点)
ORBSet.hpp : 对象请求器节点管理(非驱动节点)
Publication.cpp : 在不同的发布中遍历使用
Publication.hpp : 在不同的发布中遍历使用
Subscription.cpp : 在不同的订阅中遍历使用
Subscription.hpp : 在不同的订阅中遍历使用
uORB.cpp : uORB的实现
uORB.h : uORB头文件
uORBCommon.hpp : uORB公共部分变量定义实现
uORBCommunicator.hpp : 远程订阅的接口实现,实现了对不同的通信通道管理,如添加/移除订阅者,可以基于TCP/IP或fastRPC;传递给通信链路的实现,以提供在信道上接收消息的回调。
uORBDevices.hpp :
uORBDevices_nuttx.cpp : 节点操作,close,open,read,write
uORBDevices_nuttx.hpp :
uORBDevices_posix.cpp :
uORBDevices_posix.hpp :
uORBMain.cpp : uORB入口
uORBManager.hpp : uORB功能函数实现头文件
uORBManager_nuttx.cpp : uORB功能函数实现(Nuttx)
uORBManager_posix.cpp : uORB功能函数实现(Posix)
uORBTest_UnitTest.cpp : uORB测试
uORBTest_UnitTest.hpp : uORB测试头文件,包括主题定义和声明等
uORBUtiles.cpp :
uORBUtiles.hpp :
3 常用函数功能解析
int poll(struct pollfd fds[], nfds_t nfds, int timeout)
功能:监控文件描述符(多个);
说明:timemout=0,poll()函数立即返回而不阻塞;timeout=INFTIM(-1),poll()会一直阻塞下去,直到检测到return > 0;
参数:fds:struct pollfd结构类型的数组;nfds:用于标记数组fds中的结构体元素的总数量;timeout:是poll函数调用阻塞的时间,单位:毫秒;
返回值:>0:数组fds中准备好读、写或出错状态的那些socket描述符的总数量;==0:poll()函数会阻塞timeout所指定的毫秒时间长度之后返回;-1:poll函数调用失败;同时会自动设置全局变量errno;
int orb_subscribe(const struct orb_metadata *meta)
功能:订阅主题(topic);
说明:即使订阅的主题没有被公告,但是也能订阅成功;但是在这种情况下,却得不到数据,直到主题被公告;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;
返回值:错误则返回ERROR;成功则返回一个可以读取数据、更新话题的句柄;如果待订阅的主题没有定义或声明则会返回-1,然后会将errno赋值为ENOENT;
eg:int fd = orb_subscribe(ORB_ID(topicName));
int orb_copy(const struct orb_metadata *meta, int handle, void *buffer)
功能:从订阅的主题中获取数据并将数据保存到buffer中;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;handle:订阅主题返回的句柄;buffer:从主题中获取的数据;
返回值:返回OK表示获取数据成功,错误返回ERROR;否则则有根据的去设置errno;
eg:struct sensor_combined_s raw;orb_copy(ORB_ID(sensor_combined), sensor_sub_fd, &raw);
orb_advert_t orb_advertise(const struct orb_metadata *meta, const void *data)
功能:公告发布者的主题;
说明:在发布主题之前是必须的;否则订阅者虽然能订阅,但是得不到数据;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;data:指向一个已被初始化,发布者要发布的数据存储变量的指针;
返回值:错误则返回ERROR;成功则返回一个可以发布主题的句柄;如果待发布的主题没有定义或声明则会返回-1,然后会将errno赋值为ENOENT;
eg:struct vehicle_attitude_s att;memset(&att, 0, sizeof(att));int att_pub_fd = orb_advertise(ORB_ID(vehicle_attitude), &att);
int orb_publish(const struct orb_metadata *meta, orb_advert_t handle, const void *data)
功能:发布新数据到主题;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;handle:orb_advertise函数返回的句柄;data:指向待发布数据的指针;
返回值:OK表示成功;错误返回ERROR;否则则有根据的去设置errno;
eg: orb_publish(ORB_ID(vehicle_attitude), att_pub_fd, &att);
int orb_set_interval(int handle, unsigned interval)
功能:设置订阅的最小时间间隔;
说明:如果设置了,则在这间隔内发布的数据将订阅不到;需要注意的是,设置后,第一次的数据订阅还是由起初设置的频率来获取,
参数:handle:orb_subscribe函数返回的句柄;interval:间隔时间,单位ms;
返回值:OK表示成功;错误返回ERROR;否则则有根据的去设置errno;
eg:orb_set_interval(sensor_sub_fd, 1000);
orb_advert_t orb_advertise_multi(const struct orb_metadata *meta, const void *data, int *instance, int priority)
功能:设备/驱动器的多个实例实现公告,利用此函数可以注册多个类似的驱动程序;
说明:例如在飞行器中有多个相同的传感器,那他们的数据类型则类似,不必要注册几个不同的话题;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;data:指向一个已被初始化,发布者要发布的数据存储变量的指针;instance:整型指针,指向实例的ID(从0开始);priority:实例的优先级。如果用户订阅多个实例,优先级的设定可以使用户使用优先级高的最优数据源;
返回值:错误则返回ERROR;成功则返回一个可以发布主题的句柄;如果待发布的主题没有定义或声明则会返回-1,然后会将errno赋值为ENOENT;
eg:struct orb_test t;t.val = 0;int instance0;orb_advert_t pfd0 = orb_advertise_multi(ORB_ID(orb_multitest), &t, &instance0, ORB_PRIO_MAX);
int orb_subscribe_multi(const struct orb_metadata *meta, unsigned instance)
功能:订阅主题(topic);
说明:通过实例的ID索引来确定是主题的哪个实例;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;instance:主题实例ID;实例ID=0与orb_subscribe()实现相同;
返回值:错误则返回ERROR;成功则返回一个可以读取数据、更新话题的句柄;如果待订阅的主题没有定义或声明则会返回-1,然后会将errno赋值为ENOENT;
eg:int sfd1 = orb_subscribe_multi(ORB_ID(orb_multitest), 1);
int orb_unsubscribe(int handle)
功能:取消订阅主题;
参数:handle:主题句柄;
返回值:OK表示成功;错误返回ERROR;否则则有根据的去设置errno;
eg:ret = orb_unsubscribe(handle);
int orb_check(int handle, bool *updated)
功能:订阅者可以用来检查一个主题在发布者上一次更新数据后,有没有订阅者调用过ob_copy来接收、处理过;
说明:如果主题在在被公告前就有人订阅,那么这个API将返回“not-updated”直到主题被公告。可以不用poll,只用这个函数实现数据的获取。
参数:handle:主题句柄;updated:如果当最后一次更新的数据被获取了,检测到并设置updated为ture;
返回值:OK表示检测成功;错误返回ERROR;否则则有根据的去设置errno;
eg:if (PX4_OK != orb_check(sfd, &updated)) {return printf("check(1) failed");}if (updated) {return printf("spurious updated flag");}//orbool updated;struct random_integer_data rd;/* check to see whether the topic has updated since the last time we read it */orb_check(topic_handle, &updated);if (updated) {/* make a local copy of the updated data structure */orb_copy(ORB_ID(random_integer), topic_handle, &rd);printf("Random integer is now %d\n", rd.r);}
int orb_stat(int handle, uint64_t *time)
功能:订阅者可以用来检查一个主题最后的发布时间;
参数:handle:主题句柄;time:存放主题最后发布的时间;0表示该主题没有发布或公告;
返回值:OK表示检测成功;错误返回ERROR;否则则有根据的去设置errno;
eg:ret = orb_stat(handle,time);
int orb_exists(const struct orb_metadata *meta, int instance)
功能:检测一个主题是否存在;
参数:meta:uORB元对象,可以认为是主题id,一般是通过ORB_ID(主题名)来赋值;instance:ORB 实例ID;
返回值:OK表示检测成功;错误返回ERROR;否则则有根据的去设置errno;
eg:ret = orb_exists(ORB_ID(vehicle_attitude),0);
int orb_priority(int handle, int *priority)
功能:获取主题优先级别;
参数:handle:主题句柄;priority:存放获取的优先级别;
返回值:OK表示检测成功;错误返回ERROR;否则则有根据的去设置errno;
eg:ret = orb_priority(handle,&priority);
4 例程
4.1 例程前准备工作
archives已编译完成(注:2015/10/6号后改为cmake编译系统,不再需要编译archives了);
添加一个新的模块
- 在Firmware/src/modules中添加一个新的文件夹,命名为px4_simple_app
- 在px4_simple_app文件夹中创建module.mk文件,并输入以下内容:
- MODULE_COMMAND = px4_simple_app
- SRCS = px4_simple_app.c
- 在px4_simple_app文件夹中创建px4_simple_app.c文件
/*** @file px4_simple_app.c* Minimal application example for PX4 autopilot.*/#include <nuttx/config.h>
#include <stdio.h>
#include <errno.h>__EXPORT int px4_simple_app_main(int argc, char *argv[]);int px4_simple_app_main(int argc, char *argv[])
{printf("Hello Sky!\n");return OK;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 注册新添加的应用到NuttShell中,并编译上传
- Firmware/makefiles/config_px4fmu-v2_default.mk文件中添加如下内容:
- MODULES += modules/px4_simple_app
- 编译
- make clean
- make px4fmu-v2_default
- 上传到板子中
- make upload px4fmu-v2_default
- Firmware/makefiles/config_px4fmu-v2_default.mk文件中添加如下内容:
- 在QGC 中的Terminal(终端)中运行新应用
- nsh > px4_simple_app
接下来的代码修改均是基于此应用。
4.2 订阅主题
sensor_combined
主题是官方提供的通用接口标准主题。
/*** @file px4_simple_app.c* Minimal application example for PX4 autopilot*/#include <nuttx/config.h>
#include <unistd.h>
#include <stdio.h>
#include <poll.h>#include <uORB/uORB.h>
#include <uORB/topics/sensor_combined.h>__EXPORT int px4_simple_app_main(int argc, char *argv[]);int px4_simple_app_main(int argc, char *argv[])
{printf("Hello Sky!\n");/*订阅sensor_combined 主题*/int sensor_sub_fd = orb_subscribe(ORB_ID(sensor_combined));/*一个应用可以等待多个主题,在这里只等待一个主题*/struct pollfd fds[] = {{ .fd = sensor_sub_fd, .events = POLLIN },/* 这里可以添加更多的文件描述符;* { .fd = other_sub_fd, .events = POLLIN },*/};int error_counter = 0;while (true) {/*poll函数调用阻塞的时间为1s*/int poll_ret = poll(fds, 1, 1000);/*处理poll返回的结果 */if (poll_ret == 0) {/* 这表示时间溢出了,在1s内没有获取到发布者的数据 */printf("[px4_simple_app] Got no data within a second\n");} else if (poll_ret < 0) {/* 出现问题 */if (error_counter < 10 || error_counter % 50 == 0) {/* use a counter to prevent flooding (and slowing us down) */printf("[px4_simple_app] ERROR return value from poll(): %d\n", poll_ret);}error_counter++;} else {if (fds[0].revents & POLLIN) {/*从文件描述符中获取订阅的数据*/struct sensor_combined_s raw;/* copy sensors raw data into local buffer */orb_copy(ORB_ID(sensor_combined), sensor_sub_fd, &raw);printf("[px4_simple_app] Accelerometer:\t%8.4f\t%8.4f\t%8.4f\n",(double)raw.accelerometer_m_s2[0],(double)raw.accelerometer_m_s2[1],(double)raw.accelerometer_m_s2[2]);}/* 如果有更多的文件描述符,可以这样:* if (fds[1..n].revents & POLLIN) {}*/}}return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
测试需要在QGC终端启动uORB和初始化该传感器,最后运行应用:nsh > uorb startnsh > sh /etc/init.d/rc.sensorsnsh > px4_simple_app &
4.3 订阅和发布主题
sensor_combined
主题是官方提供的通用接口标准主题。
vehicle_attitude
主题是官方提供的通用接口标准主题。
程序流程图如下:
/*** @file px4_simple_app.c* Minimal application example for PX4 autopilot*/#include <nuttx/config.h>
#include <unistd.h>
#include <stdio.h>
#include <poll.h>#include <uORB/uORB.h>
#include <uORB/topics/sensor_combined.h>
#include <uORB/topics/vehicle_attitude.h>__EXPORT int px4_simple_app_main(int argc, char *argv[]);int px4_simple_app_main(int argc, char *argv[])
{printf("Hello Sky!\n");/* 订阅 sensor_combined 主题 */int sensor_sub_fd = orb_subscribe(ORB_ID(sensor_combined));orb_set_interval(sensor_sub_fd, 1000);/* 公告 attitude 主题 */struct vehicle_attitude_s att;memset(&att, 0, sizeof(att));int att_pub_fd = orb_advertise(ORB_ID(vehicle_attitude), &att);/*一个应用可以等待多个主题,在这里只等待一个主题*/struct pollfd fds[] = {{ .fd = sensor_sub_fd, .events = POLLIN },/* there could be more file descriptors here, in the form like:* { .fd = other_sub_fd, .events = POLLIN },*/};int error_counter = 0;while (true) {/* wait for sensor update of 1 file descriptor for 1000 ms (1 second) */int poll_ret = poll(fds, 1, 1000);/* handle the poll result */if (poll_ret == 0) {/* this means none of our providers is giving us data */printf("[px4_simple_app] Got no data within a second\n");} else if (poll_ret < 0) {/* this is seriously bad - should be an emergency */if (error_counter < 10 || error_counter % 50 == 0) {/* use a counter to prevent flooding (and slowing us down) */printf("[px4_simple_app] ERROR return value from poll(): %d\n", poll_ret);}error_counter++;} else {if (fds[0].revents & POLLIN) {/* obtained data for the first file descriptor */struct sensor_combined_s raw;/* copy sensors raw data into local buffer */orb_copy(ORB_ID(sensor_combined), sensor_sub_fd, &raw);printf("[px4_simple_app] Accelerometer:\t%8.4f\t%8.4f\t%8.4f\n",(double)raw.accelerometer_m_s2[0],(double)raw.accelerometer_m_s2[1],(double)raw.accelerometer_m_s2[2]);/* 赋值 att 并且发布这些数据给其他的应用 */att.roll = raw.accelerometer_m_s2[0];att.pitch = raw.accelerometer_m_s2[1];att.yaw = raw.accelerometer_m_s2[2];orb_publish(ORB_ID(vehicle_attitude), att_pub_fd, &att);}/* there could be more file descriptors here, in the form like:* if (fds[1..n].revents & POLLIN) {}*/}}return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
4.4 创建自己的主题
官方提供的通用接口标准主题都放在了topics文件夹下了。如果要定义我们自己的主题,比如我们新添加了超声波传感器,为了将超声波传感器的数据发布出去给其他需要的应用订阅,那么久需要创建我们的主题了。
- 主题头文件(mytopic.h)
- ORB_DECLARE(myTopicName);//声明一个主题
- 定义一个存放发布数据的结构体;
- 主题源文件(mytopic.c)
- ORB_DEFINE(myTopicName);//定义一个主题
- 初始化发布数据
- 公告主题
- 发布主题数据
mytopic.h
/* 声明自定义主题,名字可以自定义,不过最好具有一定的意义,如下为随机产生整数数据 */
ORB_DECLARE(random_integer);/* 定义要发布的数据结构体 */
struct random_integer_data {int r;
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
mytopic_publish.c
#include <topic.h>/* 定义主题 */
ORB_DEFINE(random_integer);/* 待发布的主题句柄 */
static int topic_handle;int init()
{/* 随机产生一个数初始化数据结构体 */struct random_integer_data rd = { .r = random(), };/* 公告主题 */topic_handle = orb_advertise(ORB_ID(random_integer), &rd);
}int update_topic()
{/* 产生新的数据 */struct random_integer_data rd = { .r = random(), };/* 发布主题,更新数据 */orb_publish(ORB_ID(random_integer), topic_handle, &rd);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
对于订阅者来说,就可以参考主题「4.2 订阅例程
」了。不过这里还是提供下简单处理例程:
mytopic_subscriber.c
#include <topic.h>/* 订阅主题的句柄*/
static int topic_handle;int init()
{/* 订阅主题 */topic_handle = orb_subscribe(ORB_ID(random_integer));
}void check_topic()
{bool updated;struct random_integer_data rd;/* check to see whether the topic has updated since the last time we read it */orb_check(topic_handle, &updated);if (updated) {/* make a local copy of the updated data structure */orb_copy(ORB_ID(random_integer), topic_handle, &rd);printf("Random integer is now %d\n", rd.r);}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
5 参考资料
http://www.pixhawk.com/start?id=zh/dev/px4_simple_app
http://www.pixhawk.com/dev/shared_object_communication
http://blog.arm.so/armteg/pixhawk/183-0503.html
http://pixhawk.org/start?id=dev/software_architecture
http://www.pixhawk.com/dev/add_uorb_topic?s[]=objects&s[]=common
PX4/Pixhawk---uORB深入理解和应用相关推荐
- mahony 互补滤波器
by luoshi006 上接[互补滤波器],继续学习互补滤波.... 参考: Keeping a Good Attitude: A Quaternion-Based Orientation Filt ...
- PX4/Pixhawk---uORB深入理解和应用(最新版)
1 简介 ps:第1章简介是参考 uORB深入理解和应用 1.1 PX4/Pixhawk的软件体系结构 PX4/Pixhawk的软件体系结构主要被分为四个层次,这可以让我们更好的理解PX4/Pixha ...
- PX4姿态解算源码原理理解
PX4源码原理理解一.主要参考资料链接:1.1 取PX4源码一小部分姿态解算来进行讲解姿态解算源码中文注释:https://blog.csdn.net/zouxu634866/article/deta ...
- Pixhawk原生固件PX4之常用函数解读
PX4Firmware 经常有人将Pixhawk.PX4.APM还有ArduPilot弄混.这里首先还是简要说明一下: Pixhawk是飞控硬件平台,PX4和ArduPilot都是开源的可以烧写到Pi ...
- Pixhawk系统架构介绍
前段时间导师叫我做扑翼无人机,工程上需要实现的,能够通过程控飞起来,感觉难度挺大.先从研究PX4开始,打算一步步理解透整个PX4的框架,机型的适配.旋翼.固定翼的姿态控制,新机型的添加等等.不知道能不 ...
- PX4项目学习::(一)项目架构
目录 序:项目整体嵌入式架构: 无人机方面做嵌入式编写的飞控总结 1软件: 2硬件: 1 PX4/Pixhawk的软件体系结构 2.下图所示为PX4的算法顶层架构. 左边是整个飞控系统的控制功能实现 ...
- 基于PX4的地面无人车避障系统及路径规划研究
基于PX4的地面无人车避障系统及路径规划研究 人工智能技术与咨询 来源:<动力系统与控制> ,作者姜琼阁等 关键词: 地面无人车:避障:PX4: 摘要: 地面无人车避障及路径规划是指,无人 ...
- PX4代码解析(1)
前言 做pixhawk飞控有一段时间了,但在学习过程中遇到许多困难,目前网上找不到比较完整的PX4学习笔记,我打算结合自己理解,写写自己对PX4源码的理解,不一定对,只是希望与各位大佬交流交流,同时梳 ...
- 关于Arduino、树莓派和 Pixhawk微处理器对比分析
摘要:Arduino是一款基于微控制器(单片机)的电子开发板,它可以运行一些相对比较简单的应用程序.包含硬件(各种型号的Arduino板)和软件(Arduino IDE).树莓派(RaspberryP ...
最新文章
- 微信小程序页面之间数据传递
- python 发送邮件解决所有乱码问题
- java中的wait和sleep_java中wait和sleep的区别
- CCCC L2 007 家庭房产 并查集
- 《30天自制操作系统》前言、目录、样章欢迎阅读!
- C++基础复习——C++语言数据类型
- 流量节省模式 Android,这三种方法让你节省更多手机上网流量
- junit4进行单元测试
- 蓝桥杯第七届省赛JAVA真题----剪邮票
- 火星首座NFT虚拟房屋以超过50万美元价格售出
- MFC下debug改成release版本出现问题及解决办法
- java大学实用教程(第四版)_Java大学实用教程(第4版)
- linux 服务器 硬盘测试工具,linux硬盘检测工具:Smartmontools使用指南
- 量子计算机物理原理,量子信息物理原理
- CImage不失真缩放显示图片
- 深度报道 | 瀚高软件CTO郑晓军:以开源之路发展国产数据库符合市场规律
- 英语二作文模板及范文
- 数据科学入门前需要知道的10件事
- VSCode取消注释斜体
- 【086】微博切九图-图片在线切分四份或九份