Rockchip RK3588 - Rockchip Linux Recovery recovery源码分析
目录
----------------------------------------------------------------------------------------------------------------------------
开发板 :NanoPC-T4
开发板
eMMC
:16GB
LPDDR3
:4GB
显示屏 :15.6
英寸HDMI
接口显示屏
uboot
:2017.09
linux
:4.19
----------------------------------------------------------------------------------------------------------------------------
在《Rockchip RK3588 - Rockchip Linux Recovery updateEngine
源码分析》中我们对updateEngine
源码进行了深入分析,并提到updateEngine
升级命令由两部分组成;
normal
系统下的升级:升级recovery
分区,并修改misc
分区,重启系统;recovery
系统下的升级:系统重启之后,会根据misc
分区存放的字段来判断将要引导的系统是normal
系统还是recovery
系统,这里会进入到recovery
系统,进行剩余分区的升级;
其中normal
系统下的升级是通过updateEngine
可执行程序完成的;
// 1. 在normal系统,updateEngine升级recovery分区;升级完函数就返回了,并不会升级其它分区
main(argc, argv)
MiscUpdate(image_url, partition, save_path)
// 对需要执行的升级命令打标记(这里标记了parameter、recovery升级命令)
RK_ota_set_partition(0x040000)
// 进行parameter、recovery分区升级
RK_ota_start(handle_upgrade_callback, handle_print_callback)
// 往misc偏移16k位置写入recovery信息,这样系统重启后会进入recovery系统执行剩余分区的升级
set_bootloader_message(&msg)
// 2. 触发系统重启
system(" echo b > /proc/sysrq-trigger ")
而recovery
系统下的升级,是通过recovery
可执行程序实现,具体命令如下:
/usr/bin/recovery --update_package=/userdata/update.img
recovery
二进制bin
程序部会根据编译配置调用updateEngine
或者rkupdate
进行升级,本章的目标就是对recovery
源码进行分析,实验基于《Rockchip RK3399
- 从零开始制作recovery
系统》中移植的recovery系统
。
一、recovery
目标分析
1.1 目标recovery
定位到<Rockchip Linux SDK>/external/recovery
目录下的Makefile
文件,找到目标recovery
;
OBJ = recovery.o \
default_recovery_ui.o \
rktools.o \
roots.o \
bootloader.o \
safe_iop.o \
strlcpy.o \
strlcat.o \
rkupdate.o \
sdboot.o \
usbboot.o \
mtdutils/mounts.o \
mtdutils/mtdutils.o \
mtdutils/rk29.o \
minzip/DirUtil.o \
update_engine/log.o
ifdef RecoveryNoUi
OBJ += noui.o # 不走这里
else
OBJ += ui.o\ # 走这里
minzip/Hash.o \
minzip/SysUtil.o \
minzip/Zip.o \
minui/events.o \
minui/graphics.o \
minui/resources.o \
minui/graphics_drm.o
endif
CFLAGS += -I$(PROJECT_DIR) -I/usr/include -I/usr/include/libdrm/ -lc -DUSE_UPDATEENGINE=ON
ifdef RecoveryNoUi
CFLAGS += -lpthread -lbz2 # 不走这里
else
CFLAGS += -lz -lpng -ldrm -lpthread -lcurl -lcrypto -lbz2 # 走这里
endif
$(PROM): $(OBJ)
$(CC) -o $(PROM) $(OBJ) $(CFLAGS)
可以看到目标recovery
是由若干个.o
文件通过aarch64-buildroot-linux-gnu-gcc
编译器链接生成的可执行文件。
而.o
文件实际上是由.c
文件通过aarch64-buildroot-linux-gnu-gcc
编译器编译生成的。
# build in buildroot, it need change work directory
recovery_version:
cd $(PROJECT_DIR)/../../../../../external/recovery && \
COMMIT_HASH=$$(git rev-parse --verify --short HEAD) && \
GIT_COMMIT_TIME=$$(git log -1 --format=%cd --date=format:%y%m%d) && \
GIT_DIRTY=$$(git diff-index --quiet HEAD -- || echo "-dirty") && \
commit_info=-g$${COMMIT_HASH}-$${GIT_COMMIT_TIME}$${GIT_DIRTY} && \
cd $(PROJECT_DIR) && \
echo "#define GIT_COMMIT_INFO $${commit_info}" > recovery_autogenerate.h
%.o: %.cpp
$(CC) -c $< -o $@ $(CFLAGS)
%.o: %.c recovery_version
$(CC) -c $< -o $@ $(CFLAGS)
其中:%.o
表示所有以.o
结尾的文件作为目标文件,%.cpp
表示所有以.c
结尾的文件作为依赖文件。
1.2 编译recovery
在《Rockchip RK3588 - Rockchip Linux SDK Buildroot
文件系统构建》中介绍过buildroot
编译命令;
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make -j8
或者使用如下命令单独编译recovery
软件包:
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make recovery-dirclean
root@ubuntu:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot$ sudo make recovery
其中recovery
编译日志如下:
>>> recovery develop Syncing from source dir /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/../external/recovery
rsync -au --chmod=u=rwX,go=rX --exclude .svn --exclude .git --exclude .hg --exclude .bzr --exclude CVS /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/../external/recovery/ /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop
>>> recovery develop Configuring
>>> recovery develop Building
PATH="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin:/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/sbin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin" /usr/bin/make -C /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop CC="/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc" CFLAGS="-D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON"
make[1]: 进入目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop”
cd /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/../../../../../external/recovery && \
COMMIT_HASH=$(git rev-parse --verify --short HEAD) && \
GIT_COMMIT_TIME=$(git log -1 --format=%cd --date=format:%y%m%d) && \
GIT_DIRTY=$(git diff-index --quiet HEAD -- || echo "-dirty") && \
commit_info=-g${COMMIT_HASH}-${GIT_COMMIT_TIME}${GIT_DIRTY} && \
cd /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop && \
echo "#define GIT_COMMIT_INFO ${commit_info}" > recovery_autogenerate.h
/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc -c recovery.c -o recovery.o -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON
......
/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/bin/aarch64-buildroot-linux-gnu-gcc -o updateEngine mtdutils/mounts.o mtdutils/mtdutils.o mtdutils/rk29.o update_engine/rkbootloader.o update_engine/download.o update_engine/flash_image.o update_engine/log.o update_engine/main.o update_engine/md5sum.o update_engine/rkimage.o update_engine/rktools.o update_engine/rkboot.o update_engine/crc.o update_engine/update.o update_engine/do_patch.o -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -D_FILE_OFFSET_BITS=64 -Os -g0 -D_FORTIFY_SOURCE=1 -I. -fPIC -lpthread -lcurl -lssl -lcrypto -lbz2 -lpng -ldrm -lz -lm -I/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/host/aarch64-buildroot-linux-gnu/sysroot/usr/include/libdrm -DUSE_UPDATEENGINE=ON -DSUCCESSFUL_BOOT=ON
make[1]: 离开目录“/work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop”
>>> recovery develop Installing to target
/usr/bin/install -D -m 755 /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/recovery /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/usr/bin/
mkdir -p /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/res/images
cp /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/res/images/* /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/res/images/
/usr/bin/install -D -m 755 /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/build/recovery-develop/updateEngine /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/usr/bin/
/usr/bin/install -D -m 755 package/rockchip/recovery//S40recovery /work/sambashare/rk3588/armsom/armsom-rk3588-bsp/buildroot/output/rockchip_rk3588_recovery/target/etc/init.d/S40recovery
二、recovery
源码分析
recovery
程序的入口为recovery.c
文件。recovery.c
代码比较长,如下所示:
View Code
int main(int argc, char **argv)
{
bool bSDBoot = false;
bool bUDiskBoot = false;
const char *sdupdate_package = NULL;
const char *usbupdate_package = NULL;
int previous_runs = 0;
const char *send_intent = NULL;
const char *update_package = NULL;
const char *encrypted_fs_mode = NULL;
int wipe_data = 0;
int wipe_all = 0;
int pcba_test = 0; // add for pcba test
int toggle_secure_fs = 0;
int arg;
bool isrkdebug = false;
int log_level = LOG_DEBUG;
encrypted_fs_info encrypted_fs_data;
struct timeval start_time, end_time;
long long elapsed_time;
gettimeofday(&start_time, NULL);
// 2.1 获取命令行参数:该函数的目的是从多个来源获取命令行参数,并将这些参数存储在argv数组中,同时更新misc分区
get_args(&argc, &argv);
strcpy(systemFlag, "false");
// 2.2 解析命令行参数:解析命令行参数
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 'p':
previous_runs = atoi(optarg);
break;
case 's':
send_intent = optarg;
break;
case 'u':
update_package = optarg;
break;
case 'w':
wipe_data = 1;
break;
case 'a':
wipe_all = 1;
break;
case 'e':
encrypted_fs_mode = optarg;
toggle_secure_fs = 1;
break;
case 't':
ui_show_text(1);
break;
case 'f':
pcba_test = 1;
break; // add for pcba test
case 'r':
isrkdebug = true;
break;
case 'i':
gr_set_rotate(atoi(optarg));
break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
// 2.3 重定位标准输出和标准错误,并输出版本信息
time_t start = time(NULL);
if ((access("/.rkdebug", F_OK) != 0) && (isrkdebug != true)) {
// If these fail, there's not really anywhere to complain...
if (freopen(TEMPORARY_LOG_FILE, "a", stdout) == NULL) {
LOGW("freopen stdout error");
}
setbuf(stdout, NULL);
if (freopen(TEMPORARY_LOG_FILE, "a", stderr) == NULL) {
LOGE("freopen stderr error");
}
setbuf(stderr, NULL);
}
printf("\n");
printf("*********************************************************\n");
printf(" ROCKCHIP recovery system \n");
printf("*********************************************************\n");
printf("**** version : %s ****\n", recovery_version);
LOGI("Starting recovery on %s\n", ctime(&start));
while (access(coldboot_done, F_OK) != 0) {
LOGI("coldboot not done, wait...\n");
sleep(1);
}
#ifndef RecoveryNoUi
LOGI("Recovery System have UI defined.\n");
#endif
// 2.4 ui相关
ui_init();
ui_set_background(BACKGROUND_ICON_INSTALLING);
// 2.5 获取/etc/fstab中定义的系统启动时要挂在的文件系统,同时将要挂载的文件系统数量保存到全局变量num_volumes
load_volume_table();
// 2.6 获取获取MMC、SD、SDIO等类型的设备节点路径
setFlashPoint();
// 2.7 启动方式判定
bSDBoot = is_boot_from_SD();
bUDiskBoot = is_boot_from_udisk();
if (bSDBoot || bUDiskBoot) {
char imageFile[64] = {0};
if (bSDBoot) {
if (is_sdcard_update()) {
strlcpy(imageFile, EX_SDCARD_ROOT, sizeof(imageFile));
strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
if (access(imageFile, F_OK) == 0) {
sdupdate_package = strdup(imageFile);
bSDBootUpdate = true;
ui_show_text(1);
LOGI("sdupdate_package = %s\n", sdupdate_package);
}
}
}
if (bUDiskBoot) {
if (is_udisk_update()) {
strlcpy(imageFile, EX_UDISK_ROOT, sizeof(imageFile));
strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
if (access(imageFile, F_OK) == 0) {
usbupdate_package = strdup(imageFile);
bUdiskUpdate = true;
ui_show_text(1);
LOGI("usbupdate_package = %s\n", usbupdate_package);
}
}
}
}
// 函数什么也没有做,直接返回0
device_recovery_start();
// 输出命令:LOG_INFO: Command: "recovery" "--update_package=/userdata/update.img"
LOGI("Command:");
for (arg = 0; arg < argc; arg++) {
printf(" \"%s\"", argv[arg]);
}
printf("\n");
// update_package="/userdata/update.img"
if (update_package) {
// For backwards compatibility on the cache partition only, if
// we're given an old 'root' path "CACHE:foo", change it to
// "/cache/foo". 不会进入
if (strncmp(update_package, "CACHE:", 6) == 0) {
int len = strlen(update_package) + 10;
char* modified_path = malloc(len);
strlcpy(modified_path, "/cache/", len);
strlcat(modified_path, update_package + 6, len);
LOGI("(replacing path \"%s\" with \"%s\")\n",
update_package, modified_path);
update_package = modified_path;
}
}
printf("\n");
int status = INSTALL_SUCCESS;
// 不会进入
if (toggle_secure_fs) {
......
} else if (update_package != NULL) { // 进入 2.8 升级固件检测
int i, ret = 0;
const char* binary = "/usr/bin/rkupdate";
// 确保/oem、/userdata路径没有被挂载
rockchip_partition_check();
for (i = 0; i < 5; i++) {
// 确保/userdata/update.img已经被挂载
if (!ensure_path_mounted(update_package)) {
LOGI("mounted %s Success.\n", update_package);
break;
}
LOGW("mounted %s Failed. retry %d\n", update_package, i + 1);
sleep(1);
}
if (i != 5) {
LOGI(">>>rkflash will update from %s\n", update_package);
#ifdef USE_RKUPDATE
status = do_rk_update(binary, update_package);
#endif
#ifdef USE_UPDATEENGINE // 走这里 2.9 执行剩余分区的升级功能
const char* updateEnginebin = "/usr/bin/updateEngine";
status = do_rk_updateEngine(updateEnginebin, update_package);
#endif
if (status == INSTALL_SUCCESS) {
strcpy(systemFlag, update_package);
/* update success, delete update.img. */
if (access(update_package, F_OK) == 0)
remove(update_package);
ui_print("update.img images success!\n");
} else {
ui_print("update.img images failed!\n");
}
} else {
LOGE("mounted %s Failed.\n", update_package);
ui_print("mounted %s Failed.\n", update_package);
}
if (status != INSTALL_SUCCESS) ui_print("Installation aborted.\n");
ui_print("update.img Installation done.\n");
//ui_show_text(0);
} else if (sdupdate_package != NULL) {
......
} else if (usbupdate_package != NULL) {
.......
} else if (wipe_data) {
.......
} else if (wipe_all) {
.......
} else if (pcba_test) {
//pcba test todo...
printf("------------------ pcba test start -------------\n");
exit(EXIT_SUCCESS); //exit recovery bin directly, not start pcba here, in rkLanuch.sh
return 0;
} else {
if (argc == 1) { // No command specified
if (!bSDBootUpdate && !bUdiskUpdate && ui_text_visible())
prompt_and_wait();
finish_recovery(NULL);
reboot(RB_AUTOBOOT);
return 0;
}
status = INSTALL_ERROR; // No command specified
}
if (status != INSTALL_SUCCESS) ui_set_background(BACKGROUND_ICON_ERROR);
if (status != INSTALL_SUCCESS) {
LOGE("\n Install fail! \n");
if (!bSDBootUpdate && !bUdiskUpdate && ui_text_visible())
prompt_and_wait();
}
if (sdupdate_package != NULL && bSDBootUpdate) {
.......
} else if (usbupdate_package && bUdiskUpdate) {
.......
}
// Otherwise, get ready to boot the main system...
// 2.10 进行一些收尾工作
finish_recovery(send_intent);
gettimeofday(&end_time, NULL);
elapsed_time = (end_time.tv_sec - start_time.tv_sec) * 1000LL +
(end_time.tv_usec - start_time.tv_usec) / 1000LL;
LOGI("recovery usage time:%lld ms\n", elapsed_time);
ui_print("Rebooting...\n");
LOGI("Reboot...\n");
ui_show_text(0);
fflush(stdout);
sync();
// 系统重启
reboot(RB_AUTOBOOT);
return EXIT_SUCCESS;
}
2.1 获取命令行参数
首先调用get_args
函数,该函数的目的是从多个来源获取命令行参数,并将这些参数存储在 argv
数组中,同时更新misc
分区;
// command line args come from, in decreasing precedence:
// - the actual command line
// - the bootloader control block (one per line, after "recovery")
// - the contents of COMMAND_FILE (one per line)
static void
get_args(int *argc, char ***argv)
{
// 1. misc偏移16k位置存放的是recovery信息,因此recovery系统重启后会加载recovery信息
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
get_bootloader_message(&boot); // this may fail, leaving a zeroed structure
// 输出命令:Boot command: boot-recovery
if (boot.command[0] != 0 && boot.command[0] != 255) {
LOGI("Boot command: %.*s\n", (int)sizeof(boot.command), boot.command);
}
// 输出状态
if (boot.status[0] != 0 && boot.status[0] != 255) {
LOGI("Boot status: %.*s\n", (int)sizeof(boot.status), boot.status);
}
// --- if arguments weren't supplied, look in the bootloader control block
// 2. 如果argc小于或等于1(即没有提供命令行参数),函数会尝试从boot.recovery中提取参数
if (*argc <= 1) {
// 首先确保boot.recovery字符串以空字符结尾,然后使用strtok将boot.recovery字符串分割成以换行符\n为界的多个部分
// \n对应的ASCII值是0x0a
// boot.recovery="recovery\n--update_package=/userdata/update.img\n"
boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
// arg=”recovery”
// boot.recovery="recovery\0--update_package=/userdata/update.img\n"
const char *arg = strtok(boot.recovery, "\n");
// 满足条件
if (arg != NULL && !strcmp(arg, "recovery")) {
*argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
// 保存参数arg
(*argv)[0] = strdup(arg);
for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
// 获取下一个子字符串
// arg=”--update_package=/userdata/update.img”
// boot.recovery="recovery\0--update_package=/userdata/update.img\0"
if ((arg = strtok(NULL, "\n")) == NULL) break;
(*argv)[*argc] = strdup(arg);
}
LOGI("Got arguments from boot message\n");
} else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery);
}
}
// --- if that doesn't work, try the command file
if (*argc <= 1) {
......
}
// --> write the arguments we have back into the bootloader control block
// always boot into recovery after this (until finish_recovery() is called)
// 3. 设置参数
// 设置boot.command="boot-recovery"
// 设置boot.recovery="recovery"
strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
int i;
// 合并参数,最后boot.recovery="recovery\n--update_package=/userdata/update.img\n"
for (i = 1; i < *argc; ++i) {
strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
strlcat(boot.recovery, "\n", sizeof(boot.recovery));
}
// 往misc偏移16k位置写入recovery信息
set_bootloader_message(&boot);
}
有关struct bootloader_message
参考《Rockchip RK3588 - Rockchip Linux Recovery updateEngine
源码分析》第4.4节。比如misc
分区偏移16k
的数据如下:
10000000: 62 6f 6f 74 2d 72 65 63 6f 76 65 72 79 00 00 00 boot-recovery...
10000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10000040: 72 65 63 6f 76 65 72 79 0a 2d 2d 75 70 64 61 74 recovery.--updat
10000050: 65 5f 70 61 63 6b 61 67 65 3d 2f 75 73 65 72 64 e_package=/userd
10000060: 61 74 61 2f 75 70 64 61 74 65 2e 69 6d 67 0a 00 ata/update.img..
10000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
10000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
......
10000340: 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
......
2.2 解析命令行参数
由于recovery
是通过传参来实现固件升级的,因此不难猜出recovery
是基于命令行参数的程序。
接着使用了getopt_long
函数来解析命令行参数,并根据参数的不同执行相应的操作。
static const struct option OPTIONS[] = {
{ "send_intent", required_argument, NULL, 's' },
{ "update_package", required_argument, NULL, 'u' },
{ "wipe_data", no_argument, NULL, 'w' },
{ "wipe_all", no_argument, NULL, 'a' },
{ "set_encrypted_filesystems", required_argument, NULL, 'e' },
{ "show_text", no_argument, NULL, 't' },
{ "factory_pcba_test", no_argument, NULL, 'f' },
{ "rkdebug", no_argument, NULL, 'r'},
{ "ui_rotation", required_argument, NULL, 'i'},
{ NULL, 0, NULL, 0 },
};
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 'p':
previous_runs = atoi(optarg);
break;
case 's':
send_intent = optarg;
break;
case 'u': // --update_package=/userdata/update.img
update_package = optarg;
break;
case 'w':
wipe_data = 1;
break;
case 'a':
wipe_all = 1;
break;
case 'e':
encrypted_fs_mode = optarg;
toggle_secure_fs = 1;
break;
case 't':
ui_show_text(1);
break;
case 'f':
pcba_test = 1;
break; // add for pcba test
case 'r':
isrkdebug = true;
break;
case 'i':
gr_set_rotate(atoi(optarg));
break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
getopt_long
这是一个用于获取命令行选项的函数。它可以处理长选项,并与短选项配合使用。
argc
和argv
是标准的命令行参数;OPTIONS
是定义选项的字符串或结构体(通常包含短选项及其参数信息);NULL
是指不使用长选项的返回值。
由于(*argv)[0]="recovery"
,(*argv)[1]="--update_package=/userdata/update.img"
,因此会进入u
分支,设置update_package="/userdata/update.img"
。
2.3 输出版本信息
此外如果创建了/.rkdebug
文件,则将将标准输出和标准错误重定向到一个临时日志文件/tmp/recovery.log
,,以便进行调试和记录;
if ((access("/.rkdebug", F_OK) != 0) && (isrkdebug != true)) {
// If these fail, there's not really anywhere to complain...
if (freopen(TEMPORARY_LOG_FILE, "a", stdout) == NULL) {
LOGW("freopen stdout error");
}
setbuf(stdout, NULL);
if (freopen(TEMPORARY_LOG_FILE, "a", stderr) == NULL) {
LOGE("freopen stderr error");
}
setbuf(stderr, NULL);
}
接着main
函数输出当前recovery
版本信息,比如:
*********************************************************
ROCKCHIP recovery system
*********************************************************
**** version : V1.0.1-g28f720bc5-240524-dirty ****
LOG_INFO: Starting recovery on Sun Oct 6 09:04:09 2024
如果没有配置RecoveryNoUi
,输出:
LOG_INFO: Recovery System have UI defined.
2.4 ui
相关
2.4.1 ui_init
ui_init
函数定义在ui.c
文件;
void ui_init(void)
{
int ret = 0;
ret = gr_init();
if (ret)
return;
ev_init((ev_callback)input_callback, NULL);
text_col = text_row = 0;
text_rows = gr_fb_height() / CHAR_HEIGHT;
if (text_rows > MAX_ROWS) text_rows = MAX_ROWS;
text_top = 1;
text_cols = gr_fb_width() / CHAR_WIDTH;
if (text_cols > MAX_COLS - 1) text_cols = MAX_COLS - 1;
int i;
for (i = 0; BITMAPS[i].name != NULL; ++i) {
int result = res_create_display_surface(BITMAPS[i].name, BITMAPS[i].surface);
if (result < 0) {
if (result == -2) {
LOGI("Bitmap %s missing header\n", BITMAPS[i].name);
} else {
LOGE("Missing bitmap %s\n(Code %d)\n", BITMAPS[i].name, result);
}
*BITMAPS[i].surface = NULL;
}
}
pthread_t t;
pthread_create(&t, NULL, progress_thread, NULL);
pthread_create(&t, NULL, input_thread, NULL);
}
2.4.2 ui_set_background
ui_set_background
函数定义在ui.c
文件;
void ui_set_background(int icon)
{
if (!gr_draw)
return;
pthread_mutex_lock(&gUpdateMutex);
gCurrentIcon = gBackgroundIcon[icon];
update_screen_locked();
pthread_mutex_unlock(&gUpdateMutex);
}
2.5 加载/etc/fstab
load_volume_table
函数用于获取/etc/fstab
中定义的系统启动时要挂在的文件系统,同时将要挂载的文件系统数量保存到全局变量num_volumes
;
rk3399 login: recovery filesystem table
=========================
0 (null) /tmp ramdisk (null) (null) (null)
1 /dev/root / ext2 rw,noauto 0 1
2 proc /proc proc defaults 0 0
3 devpts /dev/pts devpts defaults,gid=5,mode=620,ptmxmode=0666 0 0
4 tmpfs /dev/shm tmpfs mode=1777 0 0
5 tmpfs /tmp tmpfs mode=1777 0 0
6 tmpfs /run tmpfs mode=0755,nosuid,nodev 0 0
7 sysfs /sys sysfs defaults 0 0
8 configfs /sys/kernel/config configfs defaults 0 0
9 debugfs /sys/kernel/debug debugfs defaults 0 0
load_volume_table
函数定义在roots.c
:
void load_volume_table()
{
int alloc = 2;
// 保存块设备信息
device_volumes = malloc(alloc * sizeof(Volume));
// Insert an entry for /tmp, which is the ramdisk and is always mounted.
device_volumes[0].mount_point = "/tmp";
device_volumes[0].fs_type = "ramdisk";
device_volumes[0].device = NULL;
device_volumes[0].device2 = NULL;
device_volumes[0].option = NULL;
device_volumes[0].dump = NULL;
device_volumes[0].pass = NULL;
num_volumes = 1;
// 打开/etc/fstab文件
FILE* fstab = fopen("/etc/fstab", "r");
if (fstab == NULL) {
LOGE("failed to open /etc/fstab (%d)\n", errno);
return;
}
char buffer[1024];
char file_system[1024];
char mount_point[1024];
char fs_type[1024];
char option[1024];
char dump[1024];
char pass[1024];
char device[1024];
int i;
// 从文件中读取一行
while (fgets(buffer, sizeof(buffer) - 1, fstab)) {
// 将读取的行数据依次解析到file_system、mount_point、fs_type、option等
i = sscanf(buffer, "%s %s %s %s %s %s", file_system,
mount_point, fs_type, option, dump, pass);
if (file_system[0] == '#') continue;
//printf("load_volume_table file_system:%s, mount_point:%s, fs_type:%s, option:%s, dump:%s, pass:%s\n", file_system, mount_point, fs_type, option, dump, pass);
/* HACK: Convert *LABEL to "by-name" symlink */
// 如果配置的LABEL=xxx,则设置device="/dev/block/by-name/xxxx"
if (strstr(file_system, "LABEL="))
snprintf(device, sizeof(device), "/dev/block/by-name/%s",
strstr(file_system, "LABEL=") + strlen("LABEL="));
else
// 负责直接设置device=file_system
strcpy(device, file_system);
// 一行6个参数
if (i == 6) {
// 动态扩容
while (num_volumes >= alloc) {
alloc *= 2;
device_volumes = realloc(device_volumes, alloc * sizeof(Volume));
}
device_volumes[num_volumes].mount_point = strdup(mount_point);
device_volumes[num_volumes].fs_type = strdup(fs_type);
device_volumes[num_volumes].option = strdup(option);
device_volumes[num_volumes].dump = strdup(dump);
device_volumes[num_volumes].pass = strdup(pass);
device_volumes[num_volumes].device = strdup(device);;
device_volumes[num_volumes].device2 = NULL;
++num_volumes;
} else {
LOGE("skipping malformed recovery.fstab line: %s\n", buffer);
}
}
fclose(fstab);
printf("recovery filesystem table\n");
printf("=========================\n");
for (i = 0; i < num_volumes; ++i) {
Volume* v = &device_volumes[i];
printf(" %d %s %s %s %s %s %s\n", i, v->device, v->mount_point, v->fs_type, v->option, v->dump, v->pass);
}
printf("\n");
}
2.6 setFlashPoint
setFlashPoint
函数定义在rktools.c
,用于获取MMC
、SD
、SDIO
等类型的设备节点路径;
void setFlashPoint()
{
// 判断是不是MTD设备(Nor/Nand Flash设备),对于SD、eMMC不属于MTD设备
if (!isMtdDevice()) // 进入
// 等待设备节点/dev/block/by-name/misc可用
wait_for_device(MISC_PARTITION_NAME_BLOCK);
// 通过读取/sys/bus/mmc/devices/目录中的条目来查找SD或eMMC设备,并更新result_point
init_sd_emmc_point();
// 设置环境变量emmc_point_name="/dev/mmcblk2"
setenv(EMMC_POINT_NAME, result_point[MMC], 1);
// 判断SD类型设备是否存在,设备节点为/dev/mmcblk0
if (access(result_point[SD], F_OK) == 0)
// 设置环境变量sd_point_name_2
setenv(SD_POINT_NAME_2, result_point[SD], 1);
// /dev/mmcblk0p1
char name_t[22];
if (strlen(result_point[SD]) > 0) {
strcpy(name_t, result_point[SD]);
strcat(name_t, "p1");
}
if (access(name_t, F_OK) == 0)
// 设置环境变量sd_point_name
setenv(SD_POINT_NAME, name_t, 1);
// emmc_point_name
LOGI("emmc_point is %s\n", getenv(EMMC_POINT_NAME));
// sd_point_name
LOGI("sd_point is %s\n", getenv(SD_POINT_NAME));
// sd_point_name_2
LOGI("sd_point_2 is %s\n", getenv(SD_POINT_NAME_2));
}
函数执行完,输出信息大致如下;
LOG_INFO: devices is not MTD.
LOG_INFO: emmc_point is /dev/mmcblk2 # SDIO类型设备,在这里对应的是板载eMMC
LOG_INFO: sd_point is (null)
LOG_INFO: sd_point_2 is (null)
2.6.1 wait_for_device
wait_for_device
用于 确保某个文件(例如设备节点,挂载点或配置文件)可用;
static void wait_for_device(const char* fn)
{
int tries = 0;
int ret;
struct stat buf;
do {
++tries;
// 检查文件状态
ret = stat(fn, &buf);
// 回非零值,表示路径不可用
if (ret) {
LOGI("stat %s try %d: %s\n", fn, tries, strerror(errno));
sleep(1);
}
} while (ret && tries < 10);
if (ret) {
LOGI("failed to stat %s\n", fn);
}
}
2.6.2 init_sd_emmc_point
init_sd_emmc_point
通过读取/sys/bus/mmc/devices/
目录中的条目来查找SD
或eMMC
设备,并更新result_point
;
static const char *point_items[] = {
"/dev/mmcblk0", // MMC类型设备对应的设备节点
"/dev/mmcblk1", // SD类型设备对应的设备节点
"/dev/mmcblk2", // SDIO类型设备对应的设备节点
"/dev/mmcblk3", // SDcombo类型设备对应的设备节点
};
enum type {
MMC, // MMC类型
SD, // SD类型
SDIO, // SDIO类型
SDcombo, // .....
};
static const char *typeName[] = {
"MMC",
"SD",
"SDIO",
"SDcombo",
};
/**
* 设置flash 节点
*/
static char result_point[4][20] = {'\0'}; //0-->MMC, 1-->SD, 2-->SDIO, 3-->SDcombo
// 读取dir目录下的filename文件
int readFile(DIR* dir, char* filename)
{
char name[30] = {'\0'};
int i;
// 构建文件路径:mmc0:0001/type
// 构建文件路径:mmc1:0001/type
// 构建文件路径:mmc2:0001/type
strcpy(name, filename);
strcat(name, "/type");
// 打开文件
int fd = openat(dirfd(dir), name, O_RDONLY);
if (fd == -1) {
LOGE("Error: openat %s error %s.\n", name, strerror(errno));
return -1;
}
// 读取文件内容到resultBuf中
char resultBuf[10] = {'\0'};
if (read(fd, resultBuf, sizeof(resultBuf)) < 1) {
return -1;
}
// 将换行符替换为字符串结束符
for (i = 0; i < strlen(resultBuf); i++) {
if (resultBuf[i] == '\n') {
resultBuf[i] = '\0';
break;
}
}
// 比较读取的内容与预定义的类型名称typeName,如果匹配则返回索引
for (i = 0; i < 4; i++) {
if (strcmp(typeName[i], resultBuf) == 0) {
//printf("type is %s.\n", typeName[i]);
return i;
}
}
LOGE("Error:no found type!\n");
return -1;
}
// 通过读取/sys/bus/mmc/devices/目录中的条目来查找SD或eMMC设备
void init_sd_emmc_point()
{
DIR* dir = opendir("/sys/bus/mmc/devices/");
// 目录存在
if (dir != NULL) {
struct dirent* de;
// 循环读取目录中的每个条目,比如mmc0:0001、mmc1:0001、mmc2:0001
while ((de = readdir(dir))) {
// 排除当前目录 (.) 和父目录 (..)
if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0 )
continue;
//if (de->d_type == 4) //dir
// printf("dir name : %s \n", de->d_name);
// 检查目录项名称是否以mmc开头,以确定是否是mmc设备。
if (strncmp(de->d_name, "mmc", 3) == 0) {
//printf("find mmc is %s.\n", de->d_name);
char flag = de->d_name[3];
int ret = -1;
// 读取dir目录下的mmc0:0001/type文件,获取设备类型
ret = readFile(dir, de->d_name);
// 设备类型存在
if (ret != -1) {
// mmc0:0001/type -> result_point[SDIO]="/dev/mmcblk2"
// mmc1:0001/type -> result_point[SD]="/dev/mmcblk1"
// mmc2:0001/type -> result_point[MMC]="/dev/mmcblk0"
strcpy(result_point[ret], point_items[flag - '0']);
} else {
// 无效
strcpy(result_point[ret], "");
}
}
}
}
closedir(dir);
}
以NanoPC-T4
开发板为例(未接入SD
卡):
pi@NanoPC-T4:~$ ls -l /sys/bus/mmc/devices
total 0
lrwxrwxrwx 1 root root 0 Oct 6 14:41 mmc0:0001 -> ../../../devices/platform/fe310000.mmc/mmc_host/mmc0/mmc0:0001
lrwxrwxrwx 1 root root 0 Oct 6 14:41 mmc2:0001 -> ../../../devices/platform/fe330000.sdhci/mmc_host/mmc2/mmc2:0001
pi@NanoPC-T4:~$ cat /sys/bus/mmc/devices/mmc0\:0001/type
SDIO
pi@NanoPC-T4:~$ cat /sys/bus/mmc/devices/mmc2\:0001/type
MMC
函数执行完毕:
result_point[MMC]="/dev/mmcblk0"
result_point[SD]=""
result_point[SDIO]="/dev/mmcblk2"
result_point[SDcombo]=""
如果插入SD
卡:
pi@NanoPC-T4:~$ ls -l /sys/bus/mmc/devices
total 0
lrwxrwxrwx 1 root root 0 Oct 6 14:41 mmc0:0001 -> ../../../devices/platform/fe310000.mmc/mmc_host/mmc0/mmc0:0001
lrwxrwxrwx 1 root root 0 Oct 6 15:02 mmc1:0001 -> ../../../devices/platform/fe320000.mmc/mmc_host/mmc1/mmc1:0001
lrwxrwxrwx 1 root root 0 Oct 6 14:41 mmc2:0001 -> ../../../devices/platform/fe330000.sdhci/mmc_host/mmc2/mmc2:0001
pi@NanoPC-T4:~$ cat /sys/bus/mmc/devices/mmc1\:0001/type
SD
2.7 启动方式判定
接着通过读取命令行信息判断启动方式,用于检测是否从SD
/USB
启动;
bSDBoot = is_boot_from_SD();
bUDiskBoot = is_boot_from_udisk();
if (bSDBoot || bUDiskBoot) {
char imageFile[64] = {0};
if (bSDBoot) {
if (is_sdcard_update()) {
strlcpy(imageFile, EX_SDCARD_ROOT, sizeof(imageFile));
strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
if (access(imageFile, F_OK) == 0) {
sdupdate_package = strdup(imageFile);
bSDBootUpdate = true;
ui_show_text(1);
LOGI("sdupdate_package = %s\n", sdupdate_package);
}
}
}
if (bUDiskBoot) {
if (is_udisk_update()) {
strlcpy(imageFile, EX_UDISK_ROOT, sizeof(imageFile));
strlcat(imageFile, "/sdupdate.img", sizeof(imageFile));
if (access(imageFile, F_OK) == 0) {
usbupdate_package = strdup(imageFile);
bUdiskUpdate = true;
ui_show_text(1);
LOGI("usbupdate_package = %s\n", usbupdate_package);
}
}
}
}
由于我们采用的eMMC
启动方式,所以输出如下日志:
LOG_INFO: read cmdline
LOG_INFO: >>> Boot from non-SDcard
LOG_INFO: read cmdline
LOG_INFO: >>> Boot from non-U-Disk
2.7.1 is_boot_from_SD
is_boot_from_SD
函数定义在sdboot.c
:
bool is_boot_from_SD(void)
{
bool bSDBoot = false;
char param[1024];
int fd, ret;
char *s = NULL;
LOGI("read cmdline\n");
memset(param, 0, 1024);
// 读取命令行
fd = open("/proc/cmdline", O_RDONLY);
ret = read(fd, (char*)param, 1024);
// 查找sdfwupdate字符串
s = strstr(param, "sdfwupdate");
if (s != NULL) {
bSDBoot = true;
LOGI(">>> Boot from SDcard\n");
} else {
bSDBoot = false;
LOGI(">>> Boot from non-SDcard\n");
}
close(fd);
return bSDBoot;
}
2.7.2 is_boot_from_udisk
is_boot_from_udisk
函数定义在usbboot.c
:
bool is_boot_from_udisk(void)
{
bool bUDisk = false;
char param[1024];
int fd, ret;
char *s = NULL;
LOGI("read cmdline\n");
memset(param, 0, 1024);
// 读取命令行
fd = open("/proc/cmdline", O_RDONLY);
ret = read(fd, (char*)param, 1024);
// 查找usbfwupdate字符串
s = strstr(param, "usbfwupdate");
if (s != NULL) {
bUDisk = true;
LOGI(">>> Boot from U-Disk\n");
} else {
bUDisk = false;
LOGI(">>> Boot from non-U-Disk\n");
}
close(fd);
return bUDisk;
}
2.8 升级固件检测
接着就是升级固件的检测,这里包含两步检测:
-
确保
/oem
、/userdata
路径没有被挂载; -
确保升级固件路径
/userdata/update.img
已经挂载;
源码如下:
int i, ret = 0;
const char* binary = "/usr/bin/rkupdate";
// 检测oem、userdata分区是否挂载
rockchip_partition_check();
for (i = 0; i < 5; i++) {
//update_package="/userdata/update.im", 函数内部确保/userdata路径已经挂载
if (!ensure_path_mounted(update_package)) {
LOGI("mounted %s Success.\n", update_package);
break;
}
LOGW("mounted %s Failed. retry %d\n", update_package, i + 1);
sleep(1);
}
比如我们在使用NanoPC-T4
开发板进行测试的时候,并没有在/etc/fstab
中配置/oem
、/userdata
路径的挂载信息;因此会输出如下信息:
# 输出下面这个错误是因为我们没有在/etc/fstab中配置/oem这个路径的挂载信息
LOG_ERROR: unknown volume for path [/oem]
LOG_ERROR:
=== umount oem fail ===
# 输出下面这个错误是因为我们没有在/etc/fstab中配置/userdata这个路径的挂载信息
LOG_ERROR: unknown volume for path [/userdata]
LOG_ERROR:
=== umount userdata fail ===
LOG_INFO: check userdata/oem partition success ...
# /userdata路径挂载成功
LOG_INFO: mounted /userdata/update.img Success.
我们采用了另外一种方式,即在制作的recovery
系统的/etc/inittab
文件中配置了oem
、userdata
分区的的挂载命令;
# 用于挂载oem、userdata分区(如果分区存在的话)
::sysinit:/bin/mount -t ext4 -r /dev/block/by-name/oem /oem
::sysinit:/bin/mount -t ext4 /dev/block/by-name/userdata /userdata
所以后面在使用updateEngine
命令进行剩余分区升级的时候,是可以找到升级固件/userdata/update.img
的。
2.8.1 rockchip_partition_check
rockchip_partition_check
函数用于确保/oem
、/userdata
路径没有被挂载;
// rockchip partition check (e.g: oem/userdata....)
static void
rockchip_partition_check()
{
// 确保/oem路径没有被挂载
if (ensure_path_unmounted("/oem") != 0)
LOGE("\n === umount oem fail === \n");
// 确保/userdata路径没有被挂载
if (ensure_path_unmounted("/userdata") != 0)
LOGE("\n === umount userdata fail === \n");
ui_print("check userdata/oem partition success ...\n");
LOGI("check userdata/oem partition success ...\n");
}
2.8.2 ensure_path_unmounted
ensure_path_unmounted
函数定义在roots.c
文件中,该函数用于确保路径path
没有被挂载。这里有一点需要注意:
- 如果输入
path="/oem"
,则判定的实际挂载点mount_point="/oem"
; - 如果输入
path="/userdata/update.img
,则判定的实际挂载点mount_point="/userdata"
; - 如果输入
path="/userdata/recovery/last_log
,则判定的实际挂载点mount_point="/userdata/recovery"
;
亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。
日期 | 姓名 | 金额 |
---|---|---|
2023-09-06 | *源 | 19 |
2023-09-11 | *朝科 | 88 |
2023-09-21 | *号 | 5 |
2023-09-16 | *真 | 60 |
2023-10-26 | *通 | 9.9 |
2023-11-04 | *慎 | 0.66 |
2023-11-24 | *恩 | 0.01 |
2023-12-30 | I*B | 1 |
2024-01-28 | *兴 | 20 |
2024-02-01 | QYing | 20 |
2024-02-11 | *督 | 6 |
2024-02-18 | 一*x | 1 |
2024-02-20 | c*l | 18.88 |
2024-01-01 | *I | 5 |
2024-04-08 | *程 | 150 |
2024-04-18 | *超 | 20 |
2024-04-26 | .*V | 30 |
2024-05-08 | D*W | 5 |
2024-05-29 | *辉 | 20 |
2024-05-30 | *雄 | 10 |
2024-06-08 | *: | 10 |
2024-06-23 | 小狮子 | 666 |
2024-06-28 | *s | 6.66 |
2024-06-29 | *炼 | 1 |
2024-06-30 | *! | 1 |
2024-07-08 | *方 | 20 |
2024-07-18 | A*1 | 6.66 |
2024-07-31 | *北 | 12 |
2024-08-13 | *基 | 1 |
2024-08-23 | n*s | 2 |
2024-09-02 | *源 | 50 |
2024-09-04 | *J | 2 |
2024-09-06 | *强 | 8.8 |
2024-09-09 | *波 | 1 |
2024-09-10 | *口 | 1 |
2024-09-10 | *波 | 1 |
2024-09-12 | *波 | 10 |
2024-09-18 | *明 | 1.68 |
2024-09-26 | B*h | 10 |
2024-09-30 | 岁 | 10 |
2024-10-02 | M*i | 1 |
2024-10-14 | *朋 | 10 |
2024-10-22 | *海 | 10 |
2024-10-23 | *南 | 10 |
2024-10-26 | *节 | 6.66 |
2024-10-27 | *o | 5 |
2024-10-28 | W*F | 6.66 |
2024-10-29 | R*n | 6.66 |
2024-11-02 | *球 | 6 |
2024-11-021 | *鑫 | 6.66 |
2024-11-25 | *沙 | 5 |
2024-11-29 | C*n | 2.88 |

【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· [翻译] 为什么 Tracebit 用 C# 开发
· 腾讯ima接入deepseek-r1,借用别人脑子用用成真了~
· Deepseek官网太卡,教你白嫖阿里云的Deepseek-R1满血版
· DeepSeek崛起:程序员“饭碗”被抢,还是职业进化新起点?
· RFID实践——.NET IoT程序读取高频RFID卡/标签