系统分区表

W801是平头哥内核的WIFI芯片。给到的SDK里面没有找到中断向量表重定向的函数,类似于STM32的NVIC_SetVectorTable,所以中断向量表只能给主程序用,那么BootLoader就没法弄了。干脆不用BootLoader了,直接划一个分区用来存放Updater代码,用于解压或差分计算新固件并复制到主分区(存在变砖的可能性)。

国产芯片就是资料不全,规格书里面写了Flash的寻址空间为0x0800 0000 ~ 0x0FFF FFFF,但是在SDK里面,链接脚本是从0x080d0400开始的,0x080d0400是在tools\w800\wconfig文件里面W800_RUN_ADDRESS选项定义的默认值:

但是把W800_RUN_ADDRESS改成0x08000000之后无法运行,而且下载的时候发现芯片的MAC地址值还被冲刷了,把W800_RUN_ADDRESS改成大于0x080d0400的地址值是可以运行的,可能是前面的一些空间是用作其它用途的(后面发现擦写Flash的时候,从0x08000000开始计算2MB之后的地址是没法写入的,代码里面做了地址范围判断)。所以这里只好按照0x080d0400之后的地址值作为Flash的起始地址开始分区,由于芯片内部Flash的最小擦除单位是sector,一个sector为4KB,为了方便擦写,取一个最近的4KB对齐地址0x080D1000作为起始地址,同时每个分区的大小也设置为4KB的整数倍。2MB Flash的有效范围是0x08000000到0x08200000,那么定义用户代码的有效地址范围为0x080D1000~0x08200000,一共1212KB。

分区表设计如下图:

最开始的800KB必须作为主程序Main APP,因为中断向量表固定在这个位置。紧跟64KB为Updater程序。Updater后面的32KB的Sub APP用于其它用途,然后是4KB存放OTA参数。最后312KB存放下载的OTA固件包(全量包、全量压缩包或差分包)。

OTA工作流程

Updater工程

Updater软件为单独的一个工程,直接拷贝原来的SDK软件,修改连接脚本:

......MEMORY
{I-SRAM : ORIGIN = 0x08199000 , LENGTH = 0x10000     /* I-SRAM  100KB */ D-SRAM : ORIGIN = 0x20000100 , LENGTH = 0x47f00     /* D-SRAM  288KB */V-SRAM : ORIGIN = 0x20000000 , LENGTH = 0x100       /* off-chip SRAM 8MB */
}......

I-SRAM是指令存储器,把它的起始地址修改成Updater分区的起始地址0x08199000,LENGTH为64KB。把wm_main.c里面的main函数直接改成:

int main(void)
{u32 value = 0;/*32K switch to use RC circuit & calibration*/tls_pmu_clk_select(0);/*Switch to DBG*/value = tls_reg_read32(HR_PMU_BK_REG);value &= ~(BIT(19));tls_reg_write32(HR_PMU_BK_REG, value);value = tls_reg_read32(HR_PMU_PS_CR);value &= ~(BIT(5));tls_reg_write32(HR_PMU_PS_CR, value);/*Close those not initialized clk except uart0,sdadc,gpio,rfcfg*/value = tls_reg_read32(HR_CLK_BASE_ADDR);value &= ~0x3fffff;value |= 0x1a02;tls_reg_write32(HR_CLK_BASE_ADDR, value);void disp_version_info(void);disp_version_info();tls_sys_clk_set(CPU_CLK_80M);UserMain(); // for updater proj, OS is not required, directly jump to UserMainwhile(1);return 0;
}

因为Updater不需要运行操作系统,只需要做一些解压、差分运算以及读取、擦除和写入数据到flash。另外没有在main函数里面添加Updater的核心代码,而是调用的UserMain函数,是因为编译系统将wm_main.c和其它系统层代码编译成SDK库,最后和app文件夹里面的应用层代码链接成可执行文件的,并且编译库make lib速度较慢,而Usermain函数是属于应用层的代码,直接编译更方便。

Main APP工程

由于官方没有给很好的Flash烧写工具,每次make flash或者make down好像都只能从固定地址处开始下载,没法下载文件到指定的地址,所以把Updater软件的可执行bin文件作为常量数组先放到Main APP的工程中,并指定该数组的存放段为 .updater_bin,下载Main APP的时候连带Updater一起下载进去(也可以在Main APP里面添加一个烧写功能,使用X-modem之类的通信协议下载数据到指定位置)。

__attribute__((section(".updater_bin"))) const uint8_t updater_bin[1024*64] = {0x00, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, 0xD4, 0x85, 0x19, 0x08, ......

修改Main APP的链接脚本:

......MEMORY
{I-SRAM : ORIGIN = 0x080D1000 , LENGTH = 0x120000 /* I-SRAM  1M+128KB */ D-SRAM : ORIGIN = 0x20000100 , LENGTH = 0x47f00   /* D-SRAM  288KB */V-SRAM : ORIGIN = 0x20000000 , LENGTH = 0x100   /* off-chip SRAM 8MB */UPDATER : ORIGIN = 0x08199000 , LENGTH = 0x10000   /* updater */
}.......updater_bin :{KEEP(*main.o(.updater_bin))} > UPDATER......

链接脚本里面添加一个存储器UPDATER,起始地址为Updater分区的起始地址,大小64KB。再添加一个updater_bin段,Updater的bin文件数组就放在这个段里面。这样Main APP生成的bin文件大小就变成864KB了(800KB + 64KB),下载较慢。

这样在Main APP中就可以正常跳转到Updater中去了(注意,可执行文件的前256个字节为中断向量表,第一个中断向量为复位中断,所以向量表的第一个字为reset handler的地址):

#define OTA_PARAM_START_ADDRESS         0x081B1000
#define OTA_PARAM_LENGTH                (4 * 1024)#define OTA_PACKAGE_START_ADDRESS       0x081B2000
#define OTA_PACKAGE_LENGTH              (312 * 1024)#define OTA_UPDATER_START_ADDRESS       0x08199000
#define OTA_UPDATER_LENGTH              (64 * 1024)#define OTA_SUBAPP_START_ADDRESS         0x081B1400
#define OTA_SUBAPP_LENGTH               (32 * 1024)#define OTA_MAINAPP_START_ADDRESS        0x080D1000
#define OTA_MAINAPP_LENGTH              (800 * 1024)__attribute__((section(".updater_bin"))) const uint8_t updater_bin[1024*64] = {......
};void UserMain(void)
{uint32 *updater_start_addr = (uint32 *)OTA_UPDATER_START_ADDRESS;uint32 updater_reset_handler_addr = *updater_start_addr;LOGI("updater_bin addr:%.8X\n", (uint32)updater_bin);LOGI("updater_bin:%.2X %.2X %.2X %.2X\n", updater_bin[0], updater_bin[1], updater_bin[2], updater_bin[3]);LOGI("updater_start_addr:%.8X\n", (uint32)updater_start_addr);LOGI("updater_reset_handler_addr:%.8X\n", (uint32)updater_reset_handler_addr);void (*reset_handler)(void) = (void (*)(void))updater_reset_handler_addr;reset_handler();while (1) {tls_os_time_delay(5 * HZ);}
}

附:后面发现,Updater工程中设置了W800_RUN_ADDRESS为0x08199000之后,下载是从0x08199000处开始下载,这样Main APP下载就不会影响Updater代码了,两个工程可以独立下载不干扰,所以不需要再把Updater文件作为常量数组放在Main APP里面了。但是要注意,代码的执行地址也会变成W800_RUN_ADDRESS(可能0x08000000前面的一段空间是一个BootLoader,下载的时候把W800_RUN_ADDRESS传给它了,所以BootLoader启动应用程序的时候会跳转到W800_RUN_ADDRESS处运行并且设置了中断向量表地址也为W800_RUN_ADDRESS),所以单片机复位之后是会运行最后下载的代码的,那么需要先下载Updater再下载Main APP才行。

简单的HTTP服务器

创建本地HTTP服务器用于下载OTA固件包。参考链接:快速搭建一个简易的HTTP服务器用于文件分享与下载 - 灰信网(软件开发博客聚合)https://www.freesion.com/article/2710660021/

1、使用Python脚本创建http server:

import http.server
import socketserverPORT = 80Handler = http.server.SimpleHTTPRequestHandlerwith socketserver.TCPServer(("", PORT), Handler) as httpd:print("serving at port", PORT)httpd.serve_forever()

2、使用小工具软件MyWebServer,也很方便:

3、使用Node.js搭建一个简单的RESTful API服务器:

var express = require('express');
var app = express();
var fs = require("fs");app.get('/ota/query', function (req, res) {fs.readFile( __dirname + "/resource/" + "info.json", 'utf8', function (err, data) {console.log( data );res.end( data );});
})app.get('/ota/down/*', function (req, res) {console.log("req.params[0]:" + req.params[0]);fs.readFile( __dirname + "/resource/" + req.params[0], function (err, data) {res.end( data );});
})var server = app.listen(80, function () {
//   var host = server.address().addressvar host = "127.0.0.1"var port = server.address().portconsole.log("Server address: http://%s:%s", host, port)
})

OTA固件压缩和解压

使用的miniz库:Miniz is a lossless, high performance data compression library in a single source file that implements the zlib (RFC 1950) and Deflate (RFC 1951) compressed data format specification standards. It supports the most commonly used functions exported by the zlib library......

PC端可以对OTA固件进行压缩,单片机端进行解压。由于单片机端没有足够的空间进行一次性数据解压,所以PC端进行一次性数据压缩,单片机端进行分批次数据解压(解压比较耗内存,单片机端代码尽量将大数组用全局变量或静态变量定义,不然heap空间不够解压算法使用会导致解压失败),代码示例:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <termios.h>
#include "string.h"
#include "debug.h"
#include "event.h"#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include "miniz.h"typedef unsigned char uint8;
typedef unsigned short uint16;
typedef unsigned int uint;#define my_max(a,b) (((a) > (b)) ? (a) : (b))
#define my_min(a,b) (((a) < (b)) ? (a) : (b))#define BUF_SIZE (64 * 1024)static uint8 s_inbuf[BUF_SIZE];
static uint8 s_outbuf[BUF_SIZE];int my_uncompress(unsigned char *pDest, mz_ulong *pDest_len, const unsigned char *pSource, mz_ulong *pSource_len)
{mz_stream stream;int status;memset(&stream, 0, sizeof(stream));/* In case mz_ulong is 64-bits (argh I hate longs). */if ((*pSource_len | *pDest_len) > 0xFFFFFFFFU)return MZ_PARAM_ERROR;stream.next_in = s_inbuf;stream.avail_in = 0;stream.next_out = s_outbuf;stream.avail_out = BUF_SIZE;stream.total_out = 0;status = mz_inflateInit(&stream);if (status != MZ_OK)return status;uint remaining = *pSource_len;uint cursor = 0;uint total = 0;// LOGI("remaining: %d, cursor: %d\n", remaining, cursor);while (1) {// If input buffer is empty, read more bytes from input file.if (!stream.avail_in) {uint n = my_min(BUF_SIZE, remaining);LOGI("Reading %d bytes data...\n", n);memcpy(s_inbuf, pSource + cursor, n);// if (fread(s_inbuf, 1, n, pInfile) != n)// {//     LOGI("Failed reading from input file!\n");//     return EXIT_FAILURE;// }stream.next_in = s_inbuf;stream.avail_in = n;cursor += n;remaining -= n;}if (remaining) {status = mz_inflate(&stream, MZ_NO_FLUSH);} else {status = mz_inflate(&stream, Z_FINISH);// break;}LOGI("status: %d, remaining: %d, avail_out: %d\n", status, remaining, stream.avail_out);// If output buffer is full, or compression is done, write buffer to output file.if ((status == Z_STREAM_END) || (!stream.avail_out))    // "avail_out = 0" means output buff is full{uint n = BUF_SIZE - stream.avail_out;   // Valid data size is the length of output buff minus its remaining spaceLOGI("Writeing %d bytes of data...\n", n);// LOGI("n %d\n", n);// if (fwrite(s_outbuf, 1, n, pOutfile) != n)// {//     LOGI("Failed writing to output file!\n");//     return EXIT_FAILURE;// }memcpy(pDest + total, s_outbuf, n);total += n;stream.next_out = s_outbuf;stream.avail_out = BUF_SIZE;}if (status == Z_STREAM_END)break;else if (status != Z_OK){LOGI("deflate() failed with status %i!\n", status);// return EXIT_FAILURE;}usleep(10000);}LOGI("total %d\n", total);LOGI("stream.total_out %d\n", stream.total_out);*pDest_len = stream.total_out;return mz_inflateEnd(&stream);
}void miniz_test(uint8 *input, int input_len)
{int cmp_status;uLong src_len = (uLong)input_len;uLong cmp_len = compressBound(src_len);uLong uncomp_len = src_len;uint8 *pCmp, *pUncomp;LOGI("miniz.c version: %s\n", MZ_VERSION);do{// Allocate buffers to hold compressed and uncompressed data.pCmp = (mz_uint8 *)malloc((size_t)cmp_len);pUncomp = (mz_uint8 *)malloc((size_t)src_len);if ((!pCmp) || (!pUncomp)){LOGI("Out of memory!\n");return EXIT_FAILURE;}// ********************Compress********************// cmp_status = compress(pCmp, &cmp_len, (const unsigned char *)input, src_len);cmp_status = mz_compress2(pCmp, &cmp_len, (const unsigned char *)input, src_len, MZ_UBER_COMPRESSION);if (cmp_status != Z_OK){LOGI("compress() failed!\n");free(pCmp);free(pUncomp);return EXIT_FAILURE;}LOGI("Compressed from %u to %u bytes\n", (mz_uint32)src_len, (mz_uint32)cmp_len);// ********************Decompress********************// cmp_status = uncompress(pUncomp, &uncomp_len, pCmp, cmp_len);cmp_status = my_uncompress(pUncomp, &uncomp_len, pCmp, &cmp_len);if (cmp_status != Z_OK){LOGI("uncompress failed!\n");free(pCmp);free(pUncomp);return EXIT_FAILURE;}LOGI("Decompressed from %u to %u bytes\n", (mz_uint32)cmp_len, (mz_uint32)uncomp_len);// Ensure uncompress() returned the expected data.if ((uncomp_len != src_len) || (memcmp(pUncomp, input, (size_t)src_len))){LOGI("Decompression failed!\n");free(pCmp);free(pUncomp);return EXIT_FAILURE;}free(pCmp);free(pUncomp);} while (0);LOGI("Success.\n");return EXIT_SUCCESS;
}int main()
{LOGI("\n\n**********************%s, %s, %s**********************\n", __DATE__, __TIME__, VERSION);int fd_old, fd_new;fd_old  = open("old.bin", O_RDONLY);if(fd_old < 0){LOGE("open source image failed! ret(%d)\n", fd_old);return -1;}fd_new  = open("new.bin", O_RDONLY);if(fd_new < 0){LOGE("open source image failed! ret(%d)\n", fd_new);return -1;}lseek(fd_old, 0, SEEK_SET);lseek(fd_new, 0, SEEK_SET);uint8_t *fd_old_read_buf = malloc(1024 * 1024 * 2);uint8_t *fd_new_read_buf = malloc(1024 * 1024 * 2);uint8_t *fd_old_size = 0;uint8_t *fd_new_size = 0;fd_old_size = read(fd_old, fd_old_read_buf, 1024 * 1024 * 2);LOGI("fd_old read size:%d\n", fd_old_size);fd_new_size = read(fd_new, fd_new_read_buf, 1024 * 1024 * 2);LOGI("fd_new read size:%d\n", fd_new_size);miniz_test(fd_new_read_buf, fd_new_size);return 0;
}

Sub APP工程

SA(Sub APP)可以是一个和Updater一样的单独的工程,也可以整合到MA(Main APP)做一个内嵌的工程。后者是指MA和SA共存,可以同时运行,为了防止两个工程的内存使用重叠,在链接脚本里面把两个工程的RAM存储器空间分开,MA使用288KB的前256KB的RAM空间,SA使用最后32KB空间。另外还有一个很重要的,两个工程由于可以同时运行,栈空间就必须只有一个,规定使用MA的栈,运行SA的时候也是使用的MA的栈,所以在SA工程中不要设置栈指针。-R,每次MA有改动,SA需要重新编译。(勘误:由于wifi使用了RAM存储器最后面的一段内存空间,如下图,这段内存不是在C代码中创建的,而是使用了链接脚本中的__ram_end符号定义的,所以如果SA使用最后32KB的话,会被WiFi代码冲刷掉,因此这里我使用RAM的前32KB作为SA的内存空间,后面的给MA。)

使用RSA算法对固件签名

代码可以参考mbedtls库里面的参考代码 mbedtls/programs/pkey/rsa_sign.c mbedtls/programs/pkey/rsa_verify.c,或者W801给的SDK里面有RSA加解密和签名验证的demo代码demo/wm_rsa_demo.c(运行这个demo比较消耗栈空间)。这里我使用OpenSSL提供的指令进行测试:

1、计算固件test.bin的摘要,得到digest.txt文件,里面存放固件的hash值

tangquan@BDSHYF358758 MSYS ~/test
$ openssl sha1 test.bin > digest.txttangquan@BDSHYF358758 MSYS ~/test
$ cat digest.txt
SHA1(test.bin)= 5cb7698c44153c06b05847c95b55ec595bfcc4d6

2、创建私钥和公钥对:rsaprivatekey.pem,rsapublickey.pem

tangquan@BDSHYF358758 MSYS ~/test
$ openssl genrsa 1024 > rsaprivatekey.pem
Generating RSA private key, 1024 bit long modulus (2 primes)
.....+++++
.................................+++++
e is 65537 (0x010001)tangquan@BDSHYF358758 MSYS ~/test
$ cat rsaprivatekey.pem
-----BEGIN RSA PRIVATE KEY-----
MIICXwIBAAKBgQCfT3Jb9W4xZpALyiK4S0xFMsjltpXUy0r/9eIyrwraHV8RPeWv
......
Pb+ztFOm42PaNeR+9+R9kbTy575q9Z4lhauMEggfKLGu1wg=
-----END RSA PRIVATE KEY-----tangquan@BDSHYF358758 MSYS ~/test
$ openssl rsa -in rsaprivatekey.pem -pubout > rsapublickey.pem
writing RSA keytangquan@BDSHYF358758 MSYS ~/test
$ cat rsapublickey.pem
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCfT3Jb9W4xZpALyiK4S0xFMsjl
tpXUy0r/9eIyrwraHV8RPeWvn5+pv7nr2dQHbAnvcl46Fk+bfOe9kyriOvpAFkTj
wYbNu9DT5tnJkuhbCte46YVTvBnFSJSR83L+xo9l4WyYq+ZSiz26b11W4b+TLXLj
4cYTzCg1tXvGACr5JwIDAQAB
-----END PUBLIC KEY-----

3、使用私钥对摘要数据进行签名,得到signature.bin

tangquan@BDSHYF358758 MSYS ~/test
$ openssl rsautl -sign -inkey rsaprivatekey.pem -in digest.txt -out signature.bintangquan@BDSHYF358758 MSYS ~/test
$ ls
digest.txt  rsaprivatekey.pem  rsapublickey.pem  signature.bin  test.bin

4、使用公钥对签名进行验证,解码出来的结果和digest.txt文件内容一致,签名验证通过

tangquan@BDSHYF358758 MSYS ~/test
$ openssl rsautl -verify -inkey rsapublickey.pem -pubin -keyform PEM -in signature.bin
SHA1(test.bin)= 5cb7698c44153c06b05847c95b55ec595bfcc4d6

【嵌入式】基于平头哥内核的W801 WIFI SoC的OTA方案设计相关推荐

  1. 基于FPGA平台RISCV架构的SOC应用系统设计1

    基于FPGA平台RISCV架构的SOC应用系统设计1 ​ 本系列文章是参加第四届"复微杯"全国大学生电子设计大赛 FPGA 赛道的作品,该平台基于 RISCV,要求在 FPGA 平 ...

  2. 基于FPGA平台RISCV架构的SOC应用系统设计3

    基于FPGA平台RISCV架构的SOC应用系统设计3 ​ 本系列文章是参加第四届"复微杯"全国大学生电子设计大赛 FPGA 赛道的作品,该平台基于 RISCV,要求在 FPGA 平 ...

  3. iOS的那个漏洞并不只是影响苹果设备CVE-2017-6975 Google称影响了所有使用Broadcom Wi-Fi SoC的设备...

    数百万智能手机和智能设备,包括各个生产商制造的配备了Broadcom全系列Wi-Fi芯片的苹果iOS和安卓手机都可能遭受到OTA劫持攻击,且无需用户交互.绿盟科技发布了<Apple iOS WI ...

  4. 嵌入式系统Linux内核开发工程师必须掌握的三十道题

    嵌入式系统Linux内核开发工程师必须掌握的三十道题 如果你能正确回答以下问题并理解相关知识点原理,那么你就可以算得上是基本合格的Linux内核开发工程师,试试看! 1) Linux中主要有哪几种内核 ...

  5. YOCTO项目介绍:通过提供模版、工具和方法帮助开发者创建基于linux内核的定制系统

    目录 YOCTO项目介绍 配置内核 build配套 Yocto ,是一个开源社区.它通过提供模版.工具和方法帮助开发者创建基于linux内核的定制系统,支持ARM, PPC, MIPS, x86 (3 ...

  6. linux内核下网络驱动流程,基于Linux内核驱动的网络带宽测速方法与流程

    本发明涉及一种测速方法,尤其是一种网络带宽测速方法. 背景技术: :电信运营商为客户提供一定带宽的Internet接入:为了检验带宽是否达标,一般均由客户使用个人电脑在网页上直接测速.但是随着智能网关 ...

  7. 迅为4412开发板(全能版)V_4.0 ubuntu 在Android4.4.4的内核下移植wifi

    为4412全能版V_4.0 ubuntu 在Android4.4.4的内核下移植wifi 背景:由于厂家迅为的全能版的V4.0底板和V3.5底板的wifi部分有改动,所以需要重新移植wifi      ...

  8. 基于I.MX6UL平台的WIFI模块AP6214A 驱动移植

    基于I.MX6UL平台的WIFI模块AP6214A 驱动移植 IoT-6ULX简要介绍 IoT-6ULX,主要面向Internet Of Things应用,该产品集成了 ARM Cortex-A7 9 ...

  9. 基于webkit内核webos系统架构

    基于webkit内核webos系统架构 WebOS(Web-based Operating System)或者我们称为网络操作系统,是一种基于浏览器的虚拟的操作系统,用户通过浏览器可以在这个WebOS ...

最新文章

  1. 强行分类提取特征自编码网络例2
  2. 游戏型计算机的配置表,爽玩游戏型详细配置列表及推荐理由
  3. python知识点汇总百度文库_PYTHON知识点汇总.doc
  4. Android中最详细的焦点问题,从概念出发带你一点点分享(1)
  5. 小米汽车总部正式落户北京,注册地为北京经济技术开发区
  6. java猜单词游戏_9 java基础综合编程练习---猜单词游戏
  7. cocos2dx 3.x 快速创建项目
  8. CF 2A-Winner
  9. java socket调试工具_SocketToolV1.0-Java网络TCP调试助手
  10. 小型温控系统c语言程序,温度控制的PID算法的C语言程序
  11. html5 播放器 github,GitHub - Little-girl-lily/player: html5版本音乐播放器,支持iOS设备...
  12. 利用PS 调整 pdf清晰度
  13. python中的计数函数_Python Numpy统计函数
  14. docker网络端口映射
  15. centos 官网下载
  16. 电脑开机界面如何设置个性签名?
  17. SpringBoot爬虫
  18. TypeScript 学习资源合集
  19. 微信分享按钮隐藏、显示问题和注意事项
  20. 华为FreeBuds Pro2+和pro2区别哪个好

热门文章

  1. 计算机不联网怎么计时,电脑为什么在断网后仍能准确显示时间?
  2. 微分和导数的关系是什么?两者的几何意义有什么不同?为什么要定义微分 ?...
  3. Android开发常用代码
  4. InnoDB引擎Myslq数据库数据恢复
  5. java swing 跳转窗口_java swing 怎么实现点击按钮或者某个组件,跳转到另一个页面或者窗体?...
  6. 利用栈进行数制转换和括号匹配(C语言)
  7. Linux下MyCat的安装即使用
  8. 用 Python 快速追踪基金的收益情况(2022年4月更新) | Python财经实践
  9. PTA 一维数组 7-3 删除指定数据
  10. 生产者消费者模式解决强耦合问题