国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > php开源 > 综合技术 > u-boot串口和stdio、console初始化及相关操作详解<三>

u-boot串口和stdio、console初始化及相关操作详解<三>

来源:程序员人生   发布时间:2016-10-31 11:06:21 阅读次数:3033次
console是构建在stdio之上的,console的初始化是board_r中最后扫尾的操作。
console的初始化函数console_init_r在common/console.c中实现:
int console_init_r(void) { char *stdinname, *stdoutname, *stderrname; struct stdio_dev *inputdev = NULL, *outputdev = NULL, *errdev = NULL; #ifdef CONFIG_CONSOLE_MUX int iomux_err = 0; #endif /* set default handlers at first */ gd->jt->getc = serial_getc; gd->jt->tstc = serial_tstc; gd->jt->putc = serial_putc; gd->jt->puts = serial_puts; gd->jt->printf = serial_printf; /*--------------------------以上为代码段1--------------------------------------------*/ /* stdin stdout and stderr are in environment */ /* scan for it */ stdinname = getenv("stdin"); stdoutname = getenv("stdout"); stderrname = getenv("stderr"); //setenv stdout serial,vga标准输出被重载,如果u-boot中环境变量stdou被设定,那末stdout就被重定位 if (OVERWRITE_CONSOLE == 0) { /* if not overwritten by config switch */ 这里OVERWRITE_CONSOLE值为1 inputdev = search_device(DEV_FLAGS_INPUT, stdinname); outputdev = search_device(DEV_FLAGS_OUTPUT, stdoutname); errdev = search_device(DEV_FLAGS_OUTPUT, stderrname); #ifdef CONFIG_CONSOLE_MUX //如setenv stdout serial,vga iomux_err = iomux_doenv(stdin, stdinname); iomux_err += iomux_doenv(stdout, stdoutname); iomux_err += iomux_doenv(stderr, stderrname); if (!iomux_err) /* Successful, so skip all the code below. */ goto done; #endif } /*--------------------------以上为代码段2--------------------------------------------*/ /* if the devices are overwritten or not found, use default device */ if (inputdev == NULL) { inputdev = search_device(DEV_FLAGS_INPUT, "serial"); } if (outputdev == NULL) { outputdev = search_device(DEV_FLAGS_OUTPUT, "serial"); } if (errdev == NULL) { errdev = search_device(DEV_FLAGS_OUTPUT, "serial"); } /*--------------------------以上为代码段3--------------------------------------------*/ /* Initializes output console first */ if (outputdev != NULL) { /* need to set a console if not done above. */ console_doenv(stdout, outputdev); } if (errdev != NULL) { /* need to set a console if not done above. */ console_doenv(stderr, errdev); } if (inputdev != NULL) { /* need to set a console if not done above. */ console_doenv(stdin, inputdev); } /*--------------------------以上为代码段4--------------------------------------------*/ #ifdef CONFIG_CONSOLE_MUX done: #endif #ifndef CONFIG_SYS_CONSOLE_INFO_QUIET /*defined*/ stdio_print_current_devices(); #endif /* CONFIG_SYS_CONSOLE_INFO_QUIET */ #ifdef CONFIG_SYS_CONSOLE_ENV_OVERWRITE /*no defined*/ /* set the environment variables (will overwrite previous env settings) */ for (i = 0; i < 3; i++) { setenv(stdio_names[i], stdio_devices[i]->name); } #endif /* CONFIG_SYS_CONSOLE_ENV_OVERWRITE */ /*defined*/ gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */ print_pre_console_buffer(PRE_CONSOLE_FLUSHPOINT2_EVERYTHING_BUT_SERIAL); /*--------------------------以上为代码段5--------------------------------------------*/ return 0; }
上述程序按其实现的功能可分为5部份,为了便于分析,我们下面仅以stdout装备为例,逐渐进行讨论:
1. gd->jt初始化------代码段1
gd->jt->getc = serial_getc; gd->jt->tstc = serial_tstc; ....
上述的代码段设置jt操作的默许函数为串口相干函数。关于gd->jt所包括函数的使用,我们将在后续的章节中讨论。 
2.stdin, stdout, stderr装备被环境变量中的设定值重定位---代码段2  
取决于OVERWRITE_CONSOLE配置,当其被配置为0时,标准输入输出装备将使用环境变量stdin,stdout,stderr中的设定值。
代码段2包括的代码段以下:
stdinname =getenv("stdin"); stdoutname = getenv("stdout"); stderrname = getenv("stderr"); /*setenv stdout serial,vga标准输出被重载,如果u-boot中环境变量stdou被设定,那末stdout就被重定位*/ if (OVERWRITE_CONSOLE == 0) { /* if not overwritten by config switch */ /* OVERWRITE_CONSOLE或为宏定义,或为函数返回值,这里为返回值1*/ inputdev = search_device(DEV_FLAGS_INPUT, stdinname); outputdev = search_device(DEV_FLAGS_OUTPUT, stdoutname); errdev = search_device(DEV_FLAGS_OUTPUT, stderrname); #ifdef CONFIG_CONSOLE_MUX //如setenv stdout serial,vga iomux_err = iomux_doenv(stdin, stdinname); iomux_err += iomux_doenv(stdout, stdoutname); iomux_err += iomux_doenv(stderr, stderrname); if (!iomux_err) /* Successful, so skip all the code below. */ goto done; #endif
如我们曾在u-boot中履行命令:
=>setenv stdout vga

=>setenv stdout serial, vga
(注意:只有在定义了CONFIG_CONSOLE_MUX了时,才能将多个装备赋值给stdio相干的环境变量,否则履行上述命令u-boot会返回毛病信息。)
然后履行saevenv命令保存环境变量,重启u-boot,代码履行到这里,如果OVERWRITE_CONSOLE的值为0,代码继续向下履行,
针对环境变量的设定值,有两种情况,我们以stdout装备为例,分析其处理流程。
如环境变量stdout的值为"vga",stdout设定为单1的stdio装备vga,search_device(DEV_FLAGS_OUTPUT, stdoutname)的返回值
outputdev为非空,且指向装备名为"vga"的装备。
如环境变量stdout的值为"serial,vga ",则search_device(DEV_FLAGS_OUTPUT, stdoutname)找不到装备名为"serial,vga"的装备,
返回值outputdev为NULL。
接下来的宏CONFIG_CONSOLE_MUX处理上述某1stdio包括多路装备的情况,如输出信息同时输出到多个装备的情况。
上述代码中search_device语句放在iomux_doenv以后应当更公道。即:
stdoutname = getenv("stdout"); /*setenv stdout serial,vga标准输出被重载,如果u-boot中环境变量stdou被设定,那末stdout就被重定位*/ ... if (OVERWRITE_CONSOLE == 0) { /* if not overwritten by config switch */ /* OVERWRITE_CONSOLE或为宏定义,或为函数返回值,这里为返回值1*/ #ifdef CONFIG_CONSOLE_MUX //如setenv stdout serial,vga iomux_err = iomux_doenv(stdin, stdinname); ... if (!iomux_err) /* Successful, so skip all the code below. */ goto done; #endif inputdev = search_device(DEV_FLAGS_INPUT, stdinname); ...
这样,当定义了CONFIG_CONSOLE_MUX时,如果iomux_doenv的履行没有毛病,那末跳过search_device。如果有毛病,则进1步履行search_device段代码。
如果没有定义CONFIG_CONSOLE_MUX,则直接履行search_device。原来的程序流程安排中,当定义了CONFIG_CONSOLE_MUX时,会先履行那些search_device代码,后续履行iomux_doenv后,大多数情况下iomux_doenv的履行不会有毛病,这样直接goto done,search_device的返回值根本用不到,这时候先履行的search_device有些过剩。对照原程序流程安排,更改后的代码逻辑性比较强,履行效力提高,但可读性有点差。
这里还要强调的是:
标志OVERWRITE_CONSOLE(或是宏,或是函数的返回值)决定着标准输入输出装备是不是使用非易失性存储器存储的环境变量stdin,stdout,stderr中的设定值,即是不是被后者重载。如果OVERWRITE_CONSOLE 的值为0,那末u-boot启动后,stdio装备将使用重载值。否则,会使用默许的serial装备(见代码段3)。
当在u-boot中履行命令setenv stdout ...时,其类似于linux中履行了的输入输出重定位命令">"。该命令也是立即生效的。但setenv stdout ...并未调用代码段2。而是调用了某些回调函数,进行了stdio重定位。所以,u-boot命令setenv stdout ...强调的是stdio的重定位到指定的装备。
要注意辨别stdio被环境变量重载和stdio重定位的区分。 
下面我们重点分析iomux_doenv函数。
iomux_doenv函数包括的代码比较多,下面删除注解和1些返回值的判断处理,且只保存了stdout分支,且按实现功能将其分为3段,其大致的处理框架以下:
#ifdef CONFIG_CONSOLE_MUX /* This tries to preserve the old list if an error occurs. */ int iomux_doenv(const int console, const char *arg) { char *console_args, *temp, **start; int i, j, k, io_flag, cs_idx, repeat; struct stdio_dev *dev; struct stdio_dev **cons_set; console_args = strdup(arg); ... i = 0; temp = console_args; for (;;) { temp = strchr(temp, ','); if (temp != NULL) { i++; temp++; continue; } /* There's always one entry more than the number of commas. */ i++; break; } start = (char **)malloc(i * sizeof(char *)); ... /* setenv stdout serial,vga 几个用逗号分隔的参数*/ i = 0; start[0] = console_args; for (;;) { temp = strchr(start[i++], ','); if (temp == NULL) break; *temp = '\0'; start[i] = temp + 1; } /*start是1个指向字符串的指针数组。这里start[0]指向serial, start[1]指向vga*/ /*--------------------------以上为代码段2.1 --------------------------------------------*/ cons_set = (struct stdio_dev **)calloc(i, sizeof(struct stdio_dev *)); /*...cons_set检查,出错返回1*/ switch (console) { case stdout: io_flag = DEV_FLAGS_OUTPUT; break; default: /*...释放资源start,console_args,cons_set*/ return 1; } cs_idx = 0; for (j = 0; j < i; j++) { dev = search_device(io_flag, start[j]); if (dev == NULL) continue; repeat = 0; for (k = 0; k < cs_idx; k++) { if (dev == cons_set[k]) { repeat++; break; } } if (repeat) continue; if (console_assign(console, start[j]) < 0) continue; cons_set[cs_idx++] = dev; } /*--------------------------以上为代码段2.2 --------------------------------------------*/ free(console_args); free(start); /* failed to set any console */ if (cs_idx == 0) { free(cons_set); return 1; } else { console_devices[console] = (struct stdio_dev **)realloc(console_devices[console], cs_idx * sizeof(struct stdio_dev *)); if (console_devices[console] == NULL) { free(cons_set); return 1; } memcpy(console_devices[console], cons_set, cs_idx * sizeof(struct stdio_dev *)); cd_count[console] = cs_idx; } free(cons_set); /*--------------------------以上为代码段2.3 --------------------------------------------*/ return 0; } #endif /* CONFIG_CONSOLE_MUX */
上述程序中的stdin,stdout,stderr在include/common.h中定义:
#define stdin 0 #define stdout 1 #define stderr 2 #define MAX_FILES 3
讨论上述代码之前,首先要强调的是,只有定义了CONFIG_CONSOLE_MUX,才会有函数iomux_doenv的定义和实现。
否则,不会使用此函数。
代码段2.1
这里主要处理stdout包括多个装备的情况,而单个装备可看做多个装备的特例。多个装备的装备名使用逗号分开,就像我们在上面的代码段2中讨论的1样,当stdio被环境变量的设定值重载时,可能包括的多个装备的装备名用逗号分开,如环境变量stdout的值为“ serial,vga”,该段代码终究将这些装备名字符串的首址存入start[i]字符串指针数组中,i为用逗号隔开的装备名个数。
代码段2.2
首先利用上述start[i]字符串指针数组指向的装备名查找在全局装备表devs中查找此前已注册的stdio装备,并去掉装备重复(如设定stdout环境变量为serial,vga,serial),然后调用console_assign:
int console_assign(int file, const char *devname) { int flag; struct stdio_dev *dev; /* Check for valid file */ switch (file) { case stdin: flag = DEV_FLAGS_INPUT; break; case stdout: case stderr: flag = DEV_FLAGS_OUTPUT; break; default: return ⑴; } /* Check for valid device name */ dev = search_device(flag, devname); if (dev) return console_setfile(file, dev); return ⑴; }
为了此处的讨论尽量清晰简单,search_device函数我们放在后面分析。
在履行console_assign之前,已调用过search_device获得了装备指针,而后调用的console_assign中又履行了1遍search_device,然后调用了onsole_setfile,查找装备的操作有些冗余,为什么不在iomux_doenv中直接调用onsole_setfile,来代替console_assign呢?
对外部利用程序来讲,只关心和知道装备名,所以调用console_assign,利用装备装备名查找到对应的装备,然后再调用onsole_setfile。这是比较公道的。console_assign是文件console.c中开放给外部程序的唯1stdio分配操作的函数接口。onsole_setfile则是console.c中的静态函数。所以针对外部文件中的函数iomux_doenv相干的stdio分配操作,调用了console_assign ,即便有些代码冗余,就其程序架构上公道性,该冗余还是能容忍的。
下面分析函数console_setfile:
static int console_setfile(int file, struct stdio_dev * dev) { int error = 0; if (dev == NULL) return ⑴; switch (file) { case stdin: case stdout: case stderr: /* Start new device */ if (dev->start) { error = dev->start(dev); /* If it's not started dont use it */ if (error < 0) break; } /* Assign the new device (leaving the existing one started) */ stdio_devices[file] = dev; /* * Update monitor functions * (to use the console stuff by other applications) */ switch (file) { case stdin: gd->jt->getc = getc; ... break; case stdout: ... gd->jt->printf = printf; break; } break; default: /* Invalid file ID */ error = ⑴; } return error; }
首先尝试启动入口参数中的stdio装备。需要注意的是,在前面"stdio_add_devices"1节stdio装备的注册中,只是填充了相干的结构体,如果其后没有被使用(如serial就曾被使用了),就还未真正实际启动被注册的装备。而这里,为console分配stdio装备时,就要启动它(实际是初始化该硬件装备),由于接下就要使用该硬件完成stdio实际的硬件输入输出操作。上述dev->start代码中,1旦启动失败(有可能已启动,或硬件本身的缘由),函数console_setfile就立即返回,返回值为0。console_setfile的上层函数也返回0,这样就回到代码段2.2,但接下来还是会填充cons_set,但不会在函数console_setfile中接着填充下面的stdio_devices。
另外还有1种情况,如video装备,在video注册为stdio装备时,并未填充start函数,那末,start为NULL,即该装备无需有启动操作便可使用,那末这里,该装备也会填充到stdio_devices中。
接下来函数console_setfile将上述查找到的装备终究存储在全局变量stdio_devices中。如上所述,此装备是被成功启动或可用的装备,
stdio_devices在common/stdio.c中定义为:
struct stdio_dev *stdio_devices[] = { NULL, NULL, NULL };
它是1个装备指针数组,该数组有3个成员,即stdin,stdout,stderr,代表当前正在使用的stdio装备。所以,如果是stdout装备,该装备的结构体首址存入到stdio_devices[1]中。其他类此。当被重载后stdio为多个装备,如stdout环境变量设定为serial,vga,屡次调用的console_assign也会屡次履行console_setfile,如果两装备都有效,且能被成功启动,那末就会对同1个stdio_devices[stdout]进行赋值,可以看到stdio_devices[1]的值终究为装备名为"vga"的装备,第1个serial装备会被覆盖掉。即这时候只使用环境变量设定值的最后1个可被启动的有效装备。
另外在后续的信息输出的stdout使用时,我们可以看到printf调用了fputs,fputs又调用了console_puts,这时候,根据CONFIG_CONSOLE_MUX的定义,console_puts有两处实现,当定义了CONFIG_CONSOLE_MUX,即stdout装备可多路输出, console_puts使用console_devices数组中的装备进行输入输出的相干操作;
当没有定义了CONFIG_CONSOLE_MUX时,直接调用stdio_devices[file]->puts(stdio_devices[file], s)。但明显,当定义了CONFIG_CONSOLE_MUX时,后续的输入输出操作中几近不会使用到stdio_devices,此中情况下也被填充,其意义不是很大。
其实stdio_devices变量主要用在CONFIG_CONSOLE_MUX未定义的情况下。此时,最后的输入输出操作会使用该变量中存储的装备来完成。
README.iomux中有:
It should be possible to specify any device which console_assign()
finds acceptable, but the code has only been tested with serial and
nc.

程序最后将更新gd->jt函数列表。

代码段2.3

该段代码主要实现:
将上面代码段查找到的装备存储到全局变量console_devices[console]中,其装备个数存储到全局变量cd_count[console]中。
这里的console即stdin,stdout,stderr常量之1。当这3者之1具有多个stdio装备时,console_devices[console]会保存这多个装备,且用cd_count[console]来记录装备个数。如环境变量stdout的值为serial,vga,那末console_devices[1]指向的struct stdio_dev结构体指针数组中会包括两个指针,分别指向serial和vga装备对应的结构体地址。
cd_count[1]为console_devices[1]指向的数组的长度,这里值为2。
我们可以在终究的输出函数console_puts实现中看到console_devices和cd_count的使用:

static void console_puts(int file, const char *s) { int i; struct stdio_dev *dev; for (i = 0; i < cd_count[file]; i++) { dev = console_devices[file][i]; if (dev->puts != NULL) dev->puts(dev, s); } }
函数iomux_doenv总结:
该函数填充了以下的全局变量:
stdio_devices[3]
console_devices[3]
cd_count[3]
stdio_devices[3]在common/stdio.c中定义。还未被使用过。
console_devices、cd_count在common/console.c中定义为:
static struct stdio_dev *tstcdev; struct stdio_dev **console_devices[MAX_FILES]; int cd_count[MAX_FILES];
其中的MAX_FILES在 include/common.h中定义为3,即stdin,stdout,stderr。
console_devices包括控制台所用的struct stdio_dev装备,控制台装备包括标准输入,输出和毛病装备。而每项标准装备会有包括多个struct stdio_dev装备的情况。如控制台同时输出到串口和液晶。cd_count[...]的值这类所多个包括struct stdio_dev装备的计数。如console_devices[1]是指向stdout装备数组的指针,而cd_count[1]是stdout装备所包括的个数。这里的1即stdout。其他类此。
stdio_devices[...]则包括了当前的标准输入输出和出错装备。如 stdio_devices[0]为当前标准输入装备,
stdio_devices[1]为当前标准输出, stdio_devices[2]为当前标准出错。
当某项标准stdio装备包括多个装备时,只是使用了环境变量相对应设定中的最后1项,
比如我们设定了:
setenv stdout serial,vga
那末stdio_devices[1]的值是装备名为"vga"的装备。
3. 查找装备---代码段3
前面的处理包括了以下的情况:
a). OVERWRITE_CONSOLE != 0,即stdio装备没有被环境变量重载。无路是不是定义了CONFIG_CONSOLE_MUX ,
     这将直接履行到代码段3
b). OVERWRITE_CONSOLE == 0, 即stdio装备被环境变量重载,且定义了CONFIG_CONSOLE_MUX,
     stdio装备环境变量中包括的装备无效或stdio装备console注册失败,也将履行代码段3。
     否则如果注册成功,跳过代码段3⑷。
所以,只要程序履行到了代码段3,都将使用串口(serial)作为默许的stdio装备。
下面我们具体分析该代码段所包括函数的具体实现。以stdout装备为例,简化后代码段3以下:
if (outputdev == NULL) { outputdev = search_device(DEV_FLAGS_OUTPUT, "serial"); }
search_device函数履行装备查找,其输入参数DEV_FLAGS_OUTPUT为stdin,stdout,stderr对应的3者之1。
另外一个参数为装备名,即根据装备名来查找装备。
search_device函数的在common/console.c中实现以下:
struct stdio_dev *search_device(int flags, const char *name) { struct stdio_dev *dev; dev = stdio_get_by_name(name); if (dev && (dev->flags & flags)) return dev; return ((void *)0); }
函数stdio_get_by_name在common/stdio.c中实现:
struct stdio_dev* stdio_get_by_name(const char *name) { struct list_head *pos; struct stdio_dev *dev; if(!name) return NULL; list_for_each(pos, &(devs.list)) { dev = list_entry(pos, struct stdio_dev, list); if(strcmp(dev->name, name) == 0) return dev; } return NULL; }
在前面"stdio_add_devices"函数讨论的1节中,所有注册的stdio装备使用全局变量devs.list链表串接起来,stdio_get_by_name函数就是在此链表中查找名字为涵参name的stdio装备。我们继续跟踪list_entry,可以看到其定义为:
#define list_entry(ptr, type, member) \ container_of(ptr, type, member)
container_of和linux驱动中用法1致,也是通过结构体成员来查找结构体本身的首址。我们在前面的"stdio_add_devices" 1节中也曾提及过,devs.list的链表成员只是struct stdio_dev结构体的成员变量list,而非struct stdio_dev结构体变量本身。所以要使用container_of查找描写stdio装备的struct stdio_dev变量本身。stdio_get_by_name履行完返回到search_device后,如果找到装备,还会核对查找到的装备属性标志是不是和输入参数的标志1致,装备属性标志在该装备注册时设置。
4. 注册stdio装备到console中---代码段4
我们要注意代码履行到代码段4的前述情况:
使用串口serial作为唯1的默许stdio装备进行console注册。
针对CONFIG_CONSOLE_MUX定义与否,函数console_doenv有两处实现。当没有定义CONFIG_CONSOLE_MUX时,则console_doenv的实现为:
static inline void console_doenv(int file, struct stdio_dev *dev) { console_setfile(file, dev); }
函数console_setfile将上述查找到的装备终究存储在全局变量stdio_devices中。这我们在上面代码段2.2已讨论过。终究将默许的serial装备赋值到stdio_devices中去。
综上所述,当没有定义CONFIG_CONSOLE_MUX,装备是不会注册到console的装备描写变量console_devices中去的。当定义了CONFIG_CONSOLE_MUX时,函数console_doenv则直接调用common/Iomux.c中的iomux_doenv。后续操作和上述代码段2.x中的操作1致,只不过这里的装备为单1装备serial,会将serial注册到console中去。不知是不是是1种代码过渡,截止到u-boot⑵016.3,个人觉得还是将stdio和console混淆处理的模糊不清。
5.函数的尾端程序处理---代码段5
后续的程序包括:
gd->flags |= GD_FLG_DEVINIT; /* device initialization completed */ print_pre_console_buffer(PRE_CONSOLE_FLUSHPOINT2_EVERYTHING_BUT_SERIAL);
由于CONFIG_PRE_CONSOLE_BUFFER没有定义,print_pre_console_buffer为空函数。
gd->flags是1个非常重要的定义,当履行gd->flags |= GD_FLG_DEVINIT后,代表此时console控制台已的前戏准备工作已完成,console控制台已可用。
gd->flags会被console.c文件中的函数屡次判标GD_FLG_DEVINIT使用。 这些函数包括getc,tstc,puts,on_console。
如最经常使用的信息输出函数printf调用了这其中的puts。下面以puts函数为例,分析变量gd->flags使用及意义:
void puts(const char *s) { ... if(!gd->have_console) return pre_console_puts(s); if (gd->flags & GD_FLG_DEVINIT) { /* Send to the standard output */ fputs(stdout, s); } else { /* Send directly to the handler */ pre_console_puts(s); serial_puts(s); } }
上述代码中,如果gd->flags & GD_FLG_DEVINIT为真时,将使用fputs履行信息输出,fputs定义为:
void fputs(int file,constchar*s) { if (file < MAX_FILES) console_puts(file, s); }
函数console_puts我们在代码段2.2中粗略提及过,其具体实现以下:
static void console_puts(int file, const char *s) { int i; struct stdio_dev *dev; for (i = 0; i < cd_count[file]; i++) { dev = console_devices[file][i]; if (dev->puts != NULL) dev->puts(dev, s); } }

可见,gd->flags & GD_FLG_DEVINIT为真时,终究将使用console_devices中注册过的控制台函数履行相干操作。也即是,履行了代表gd->flags |= GD_FLG_DEVINIT后,gd->flags & GD_FLG_DEVINIT为真 ,代表console控制台中的相干操作函数可用了。否则使用默许的串口输出函数serial_puts。

斟酌到这样1种情况,我们在上述代码段5的语句

gd->flags |= GD_FLG_DEVINIT;
之前的printf输出信息,将会使用默许的串口输出函数serial_puts,该函数在board_f阶段被注册且其后续可用。而该代码段以后的程序,所使用的printf,都将使用该节讨论的console控制台输出函数。

gd->flags |= GD_FLG_DEVINIT语句制造了1个这样的分水岭。


那末在stdio和serial结构图的基础上,加上console,3者之间的结构总图以下:









生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生