2.8.1、uboot命令体系基础
2.8.1.1、使用uboot命令
x210 # help tftp
tftpboot [loadAddress] [[hostIPaddr:]bootfilename]
x210 # help ping
ping pingAddress
2.8.1.2、uboot命令体系实现代码在哪里
2.8.1.3、每个命令对应一个函数
2.8.1.4、命令参数以argc&argv传给函数
//Command.h
/*
* Monitor Command Table 监控命令表
* uboot命令表结构体
*/
//定义了一个命令表结构体,里面包含
//命令名字、接收参数个数、是否可重复执行、命令所对应的函数指针、
//短帮助信息、长帮助信息、命令自动补全函数
struct cmd_tbl_s {
char *name; /* Command Name(命令名称,字符串格式。) */
int maxargs; /* maximum number of arguments(命令最多可以接收多少个参数) */
int repeatable; /* autorepeat allowed?(指示这个命令是否可重复执行。) */
/* Implementation function(实现功能) */
//函数指针,命令对应的函数的函数指针,将来执行这个命令的函数时使用这个函数指针来调用。
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short)(命令的短帮助信息。) */
#ifdef CFG_LONGHELP
char *help; /* Help message (long)(命令的长帮助信息。) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
//函数指针,指向这个命令的自动补全的函数。
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
//Command.c
/*
* Use puts() instead of printf() to avoid printf buffer overflow
* for long help messages
* 用puts()代替printf(),以避免长帮助消息的printf缓冲区溢出
*/
/*
* 函数功能:对应命令help背后的函数,实现help功能
*
* 参数:
* cmd_tbl_t * cmdtp:命令结构体指针
* int flag:标志
* int argc:输入的参数
* char *argv[]:输入的字符
*
* 返回值:
*/
int do_help (cmd_tbl_t * cmdtp, int flag, int argc, char *argv[])
{
int i;
int rcode = 0;
if (argc == 1) { /*show list of commands 显示命令列表*/ //仅仅只有命令,没有参数
//uboot中命令总个数,当只有一个help命令时,需要打印所有的命令
int cmd_items = &__u_boot_cmd_end -
&__u_boot_cmd_start; /* pointer arith! 指针*/
//定义了一个指针数组,其是一个数组,数组中存放的元素时指针,该指针指向的为命令结构体
cmd_tbl_t *cmd_array[cmd_items];
int i, j, swaps;
/* Make array of commands from .uboot_cmd section */
//从创建命令数组。uboot_cmd部分
cmdtp = &__u_boot_cmd_start; //cmdtp中记录uboot中命令的起始地址
//利用for循环,依次将uboot中所有命令表结构体复制到结构体指针数组
for (i = 0; i < cmd_items; i++) {
cmd_array[i] = cmdtp++;
}
/* Sort command list (trivial bubble sort) 排序命令列表(普通气泡排序)*/
//按照命令的名字进行冒泡排序
for (i = cmd_items - 1; i > 0; --i) {
swaps = 0;
for (j = 0; j < i; ++j) {
if (strcmp (cmd_array[j]->name,
cmd_array[j + 1]->name) > 0) {
cmd_tbl_t *tmp;
tmp = cmd_array[j];
cmd_array[j] = cmd_array[j + 1];
cmd_array[j + 1] = tmp;
++swaps;
}
}
if (!swaps) //swaps:交换次数为0;跳出死循环
break;
}
/* print short help (usage) 打印简短帮助(用法)*/
for (i = 0; i < cmd_items; i++) {
//const char *usage:usage是一个字符指针,usage本身可以改变,但是其指向的字符不可以改变
const char *usage = cmd_array[i]->usage;
/* allow user abort 允许用户中止*/
if (ctrlc ())
return 1;
if (usage == NULL)
continue; //结束本次循环执行下一循环
puts (usage);
}
return 0;
}
/*
* command help (long version)
* 命令帮助(长版本)
* 此时输入的命令参数大于1
*/
//argc大于1,输入了多少个命令。就执行多少次循环
for (i = 1; i < argc; ++i) {
if ((cmdtp = find_cmd (argv[i])) != NULL) { //如果查找到命令。将此命令的地址复制给cmdtp
#ifdef CFG_LONGHELP //CFG_LONGHELP:定义,可以打印长帮主信息
/* found - print (long) help info */
//找到-打印(长)帮助信息
puts (cmdtp->name); //打印命令
putc (' '); //空格
if (cmdtp->help) {
puts (cmdtp->help); //打印长帮助信息
} else {
puts ("- No help available.\n");
rcode = 1;
}
putc ('\n'); //换行符
#else /* no long help available 不再有帮助*/
if (cmdtp->usage)
puts (cmdtp->usage);
#endif /* CFG_LONGHELP */
} else {
printf ("Unknown command '%s' - try 'help'"
" without arguments for list of all"
" known commands\n\n", argv[i]
);
rcode = 1;
}
}
return rcode;
}
/* Sort command list (trivial bubble sort) 排序命令列表(普通气泡排序)*/
//按照命令的名字进行冒泡排序
for (i = cmd_items - 1; i > 0; --i) {
swaps = 0;
for (j = 0; j < i; ++j) {
if (strcmp (cmd_array[j]->name,
cmd_array[j + 1]->name) > 0) {
cmd_tbl_t *tmp;
tmp = cmd_array[j];
cmd_array[j] = cmd_array[j + 1];
cmd_array[j + 1] = tmp;
++swaps;
}
}
if (!swaps) //swaps:交换次数为0;跳出死循环
break;
}
2.8.2、uboot命令解析和执行过程分析
2.8.2.1、从main_loop说起
//Board.c
/* main_loop() can return to retry autoboot, if so just run it again. */
//main_loop()可以返回重试自动启动,如果是这样,就再次运行它
for (;;) { //死循环
main_loop (); //接收命令、解析命令、执行命令的死循环
2.8.2.2、run_command函数详解
/****************************************************************************
* returns:
* 1 - command executed, repeatable
* 0 - command executed but not repeatable, interrupted commands are
* always considered not repeatable
* -1 - not executed (unrecognized, bootd recursion or too many args)
* (If cmd is NULL or "" or longer than CFG_CBSIZE-1 it is
* considered unrecognized)
*
* WARNING:
*
* We must create a temporary copy of the command since the command we get
* may be the result from getenv(), which returns a pointer directly to
* the environment data, which may change magicly when the command we run
* creates or modifies environment variables (like "bootp" does).
*/
/****************************************************************************
* 返回值:
* 1-执行命令,可重复
* 0-已执行但不可重复的命令,中断的命令总是被认为不可重复
* -1-未执行(无法识别、bootd递归或参数太多)(如果cmd为空或""或长于CFG_CBSIZE-1,则视为无法识别)
*
* 警告:
*
* 我们必须创建该命令的临时副本,因为我们得到的命令可能是getenv()的结果,它直接返回一
* 个指向环境数据的指针,当我们运行的命令创建或修改环境变量时(就像“bootp”所做的那样),环境数据可能会发生变化。
*/
//run_command函数就是用来执行命令的函数。
int run_command (const char *cmd, int flag)
{
cmd_tbl_t *cmdtp;
char cmdbuf[CFG_CBSIZE]; /* working copy of cmd (cmd的工作副本)*/
char *token; /* start of token in cmdbuf(cmdbuf中令牌的开始) */
char *sep; /* end of token (separator) in cmdbuf (cmdbuf中的标记结束(分隔符))*/
char finaltoken[CFG_CBSIZE];
char *str = cmdbuf; //命令缓冲区指针
char *argv[CFG_MAXARGS + 1]; /* NULL terminated 空终止*/
int argc, inquotes;
int repeatable = 1;
int rc = 0;
#ifdef DEBUG_PARSER
printf ("[RUN_COMMAND] cmd[%p]=\"", cmd);
puts (cmd ? cmd : "NULL"); /* use puts - string may be loooong */
puts ("\"\n");
#endif
clear_ctrlc(); /* forget any previous Control C 忘记任何以前的控制C*/
if (!cmd || !*cmd) { //命令为空
return -1; /* empty command 空命令*/
}
if (strlen(cmd) >= CFG_CBSIZE) { //命令太长或者参数过多
puts ("## Command too long!\n");
return -1;
}
strcpy (cmdbuf, cmd); //strcpy把含有'\0'结束符的字符串复制到另一个地址空间,返回值的类型为char*
/* Process separators and check for invalid
* repeatable commands
* 处理分隔符并检查无效的可重复命令
*/
#ifdef DEBUG_PARSER
printf ("[PROCESS_SEPARATORS] %s\n", cmd);
#endif
while (*str) { //结束标志:*str为空时
/*
* Find separator, or string end
* Allow simple escape of ';' by writing "\;"
* 查找分隔符或字符串结尾允许简单转义“;”通过书写“\”;
*/
for (inquotes = 0, sep = str; *sep; sep++) {
if ((*sep=='\'') &&
(*(sep-1) != '\\'))
inquotes=!inquotes;
if (!inquotes &&
(*sep == ';') && /* separator 分隔符*/
( sep != str) && /* past string start (过去字符串开始)*/
(*(sep-1) != '\\')) /* and NOT escaped */
break;
}
/*
* Limit the token to data between separators
* 将标记限制为分隔符之间的数据
*/
token = str;
if (*sep) {
str = sep + 1; /* start of command for next pass */
*sep = '\0';
}
else
str = sep; /* no more commands for next pass */
#ifdef DEBUG_PARSER
printf ("token: \"%s\"\n", token);
#endif
/* find macros in this token and replace them */
process_macros (token, finaltoken);
/* Extract arguments */
//parse_line函数把"md 30000000 10"解析成argv[0]=md, argv[1]=30000000 argv[2]=10;
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
/* Look up command in command table */
//命令集中查找命令。
//find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
/* found - check max args */
if (argc > cmdtp->maxargs) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
/* found - check max args */
if (argc > cmdtp->maxargs) {
printf ("Usage:\n%s\n", cmdtp->usage);
rc = -1;
continue;
}
/***************************************************************************
* find command table entry for a command
* 查找命令的命令表条目
*/
//命令集中查找命令。
//find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp; //命令结构体指针
//__u_boot_cmd_start:此在链接脚本定义,其代表命令集的首地址
//cmdtp_temp:此指针指向了命令集的首地址,类型为cmd_tbl_t
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value (初始值)*/
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
* 有些命令允许长度修饰符(如“CP . b”);仅比较命令名,直到第一个点。
*/
//strchr(cmd, '.')),可以查找字符串s中首次出现字符.的位置
//返回首次出现c的位置的指针,返回的地址是字符串在内存中随机分配的地址再加上你所搜索的字符在字符串位置,
//如果s中不存在c则返回NULL。
//如果p==NULL,则命令中没有.,len为cmd的长度
//如果p!=NULL,则命令中有.,len为p - cmd
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
//int strncmp(stringstr1,charstr2,intlen)
//功 能:比较字符串的前N个字符
// 如果两个字符串相等的话,strcnmp将返回0。
//cmd:参数
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name)) //cmdtp->name:命令集中的命令名称
return cmdtp; /* full match(满匹配) */ //返回
cmdtp_temp = cmdtp; /* abbreviated command ? (缩写命令)*/
n_found++;
}
}
if (n_found == 1) { /* exactly one match (正好一个匹配)*/
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
2.8.2.3、关键点分析
//Main.c
/* Extract arguments 提取参数*/
//parse_line函数把"md 30000000 10"解析成argv[0]=md, argv[1]=30000000 argv[2]=10;
if ((argc = parse_line (finaltoken, argv)) == 0) {
rc = -1; /* no command at all */
continue;
}
/****************************************************************************/
/* Extract arguments 提取参数*/
//parse_line函数把"md 30000000 10"解析成argv[0]=md, argv[1]=30000000 argv[2]=10;
int parse_line (char *line, char *argv[])
{
int nargs = 0;
#ifdef DEBUG_PARSER
printf ("parse_line: \"%s\"\n", line);
#endif
while (nargs < CFG_MAXARGS) {
/* skip any white space 跳过任何空白*/
//也就是跳过命令前面的空白
while ((*line == ' ') || (*line == '\t')) {
++line;
}
if (*line == '\0') { /* end of line, no more args(行尾,没有参数) */
argv[nargs] = NULL;
#ifdef DEBUG_PARSER
printf ("parse_line: nargs=%d\n", nargs);
#endif
return (nargs);
}
argv[nargs++] = line; /* begin of argument string (参数字符串的开头)*/
/* find end of string(查找字符串的结尾) */
while (*line && (*line != ' ') && (*line != '\t')) {
++line;
}
if (*line == '\0') { /* end of line, no more args */
argv[nargs] = NULL;
#ifdef DEBUG_PARSER
printf ("parse_line: nargs=%d\n", nargs);
#endif
return (nargs);
}
*line++ = '\0'; /* terminate current arg */
}
printf ("** Too many args (max. %d) **\n", CFG_MAXARGS);
#ifdef DEBUG_PARSER
printf ("parse_line: nargs=%d\n", nargs);
#endif
return (nargs);
}
/****************************************************************************/
//Main.c
/* Look up command in command table */
//命令集中查找命令。
//find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
if ((cmdtp = find_cmd(argv[0])) == NULL) {
printf ("Unknown command '%s' - try 'help'\n", argv[0]);
rc = -1; /* give up after bad command */
continue;
}
//Comm
and.c
/***************************************************************************
* find command table entry for a command
* 查找命令的命令表条目
*/
//命令集中查找命令。
//find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp;
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value (初始值)*/
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
* 有些命令允许长度修饰符(如“CP . b”);仅比较命令名,直到第一个点。
*/
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name))
return cmdtp; /* full match */
cmdtp_temp = cmdtp; /* abbreviated command ? */
n_found++;
}
}
if (n_found == 1) { /* exactly one match */
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
/* OK - call function to do the command (确定-调用函数执行命令)*/
if ((cmdtp->cmd) (cmdtp, flag, argc, argv) != 0) {
rc = -1;
}
/*
* Monitor Command Table
*/
struct cmd_tbl_s {
char *name; /* Command Name(命令名字) */
int maxargs; /* maximum number of arguments(参数的最大个数) */
int repeatable; /* autorepeat allowed?(是否允许自动重复) */
/* Implementation function(实现功能) */
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short)(用法信息) */
#ifdef CFG_LONGHELP
char *help; /* Help message (long)(帮助信息) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
//对参数进行自动完成
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
2.8.3、uboot如何处理命令集1
2.8.3.1、可能的管理方式
2.8.3.2、命令结构体cmd_tbl_t
/*
* Monitor Command Table 监控命令表
* uboot命令表结构体
*/
//定义了一个命令表结构体,里面包含
//命令名字、接收参数个数、是否可重复执行、命令所对应的函数指针、
//短帮助信息、长帮助信息、命令自动补全函数
struct cmd_tbl_s {
char *name; /* Command Name(命令名称,字符串格式。) */
int maxargs; /* maximum number of arguments(命令最多可以接收多少个参数) */
int repeatable; /* autorepeat allowed?(指示这个命令是否可重复执行。) */
/* Implementation function(实现功能) */
//函数指针,命令对应的函数的函数指针,将来执行这个命令的函数时使用这个函数指针来调用。
int (*cmd)(struct cmd_tbl_s *, int, int, char *[]);
char *usage; /* Usage message (short)(命令的短帮助信息。) */
#ifdef CFG_LONGHELP
char *help; /* Help message (long)(命令的长帮助信息。) */
#endif
#ifdef CONFIG_AUTO_COMPLETE
/* do auto completion on the arguments */
//函数指针,指向这个命令的自动补全的函数。
int (*complete)(int argc, char *argv[], char last_char, int maxv, char *cmdv[]);
#endif
};
2.8.3.3、uboot实现命令管理的思路
2.8.4、uboot如何处理命令集2
2.8.4.1、uboot命令定义具体实现分析
//Command.h
//U_BOOT_CMD(name,maxargs,rep,cmd,usage,help):我们定义的宏
//cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}:将来要替换的
//cmd_tbl_t:命令结构体类型
//__u_boot_cmd_##name:结构体变量的名字
//##:在gcc中是一种扩展语法,连字符。这里面的name就是传参的第一个name,其将会被括号里面的#name替换
//#define Struct_Section __attribute__ ((unused,section (".u_boot_cmd")))
//__attribute__:属性,给其贴标签,所谓的段
//.u_boot_cmd:用户自定义段
#define U_BOOT_CMD(name,maxargs,rep,cmd,usage,help) \
cmd_tbl_t __u_boot_cmd_##name Struct_Section = {#name, maxargs, rep, cmd, usage, help}
//链接脚本
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) } //自定义数据段段
__u_boot_cmd_end = .;
2.8.4.2、find_cmd函数详解
/***************************************************************************
* find command table entry for a command
* 查找命令的命令表条目
*/
//命令集中查找命令。
//find_cmd(argv[0])函数去uboot的命令集合当中搜索有没有argv[0]这个命令,
cmd_tbl_t *find_cmd (const char *cmd)
{
cmd_tbl_t *cmdtp;
//__u_boot_cmd_start:此在链接脚本定义,其代表命令集的首地址
//cmdtp_temp:此指针指向了命令集的首地址,类型为cmd_tbl_t
cmd_tbl_t *cmdtp_temp = &__u_boot_cmd_start; /*Init value (初始值)*/
const char *p;
int len;
int n_found = 0;
/*
* Some commands allow length modifiers (like "cp.b");
* compare command name only until first dot.
* 有些命令允许长度修饰符(如“CP . b”);仅比较命令名,直到第一个点。
*/
//strchr(cmd, '.')),可以查找字符串s中首次出现字符.的位置
//返回首次出现c的位置的指针,返回的地址是字符串在内存中随机分配的地址再加上你所搜索的字符在字符串位置,
//如果s中不存在c则返回NULL。
//如果p==NULL,则命令中没有.,len为cmd的长度
//如果p!=NULL,则命令中有.,len为p - cmd
len = ((p = strchr(cmd, '.')) == NULL) ? strlen (cmd) : (p - cmd);
for (cmdtp = &__u_boot_cmd_start;
cmdtp != &__u_boot_cmd_end;
cmdtp++) {
//int strncmp(stringstr1,charstr2,intlen)
//功 能:比较字符串的前N个字符
// 如果两个字符串相等的话,strcnmp将返回0。
//cmd:参数
if (strncmp (cmd, cmdtp->name, len) == 0) {
if (len == strlen (cmdtp->name)) //cmdtp->name:命令集中的命令名称
return cmdtp; /* full match(满匹配) */ //返回
cmdtp_temp = cmdtp; /* abbreviated command ? (缩写命令)*/
n_found++;
}
}
if (n_found == 1) { /* exactly one match (正好一个匹配)*/
return cmdtp_temp;
}
return NULL; /* not found or ambiguous command */
}
2.8.4.3、U_BOOT_CMD宏详解
2.8.4.4、命令举例:version命令
2.8.5、uboot中增加自定义命令
2.8.5.1、在已有的c文件中直接添加命令
//command.c在此目录下
root@xfj-virtual-machine:~/x210v3_bsp/uboot/common# pwd
/root/x210v3_bsp/uboot/common
//xfj添加mycmd
//开始
int
do_mycmd (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
printf ("\n%s\n", "this is a test for add cmd to uboot.");
return 0;
}
U_BOOT_CMD(
mycmd, 1, 1, do_mycmd,
"mycmd - usage of my cmd\n",
"long help of mycmd\n"
);
//结束
//烧录
//1.插上SD卡
//2.可移动设备--链接
//3.
root@xfj-virtual-machine:~/x210v3_bsp/uboot# ls /dev/sdb
//4.
root@xfj-virtual-machine:~/x210v3_bsp/uboot# cd sd_fusing/
root@xfj-virtual-machine:~/x210v3_bsp/uboot/sd_fusing# ls
C110-EVT1-mkbl1.c c110.signedBL1_bin Makefile mkbl1 sd_fdisk sd_fdisk.c sd_fusing2.sh sd_fusing.sh
root@xfj-virtual-machine:~/x210v3_bsp/uboot/sd_fusing# ./sd_fusing.sh /dev/sdb
//徐富军添加mycmd
//开始
int
do_mycmd (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
int i = 0;
printf ("\n%s\n", "this is a test for add cmd to uboot.");
printf( "argc = %d\n", argc );
for( i=0; i < argc; i++ )
{
printf( "argv[%d] = %s\n", i, argv[i] );
}
return 0;
}
U_BOOT_CMD(
mycmd, 3, 1, do_mycmd,
"mycmd - usage of my cmd\n",
"long help of mycmd\n"
);
//结束
2.8.5.2、自建一个c文件并添加命令
//1.
root@xfj-virtual-machine:~/x210v3_bsp/uboot/common# pwd
/root/x210v3_bsp/uboot/common
//2.
root@xfj-virtual-machine:~/x210v3_bsp/uboot/common# touch cmd_aston.c
#include <common.h> //头文件一定要包含
#include <command.h>
//徐富军添加aston
//开始
int
do_aston (cmd_tbl_t *cmdtp, int flag, int argc, char *argv[])
{
int i = 0;
printf ("\n%s\n", "this is a test for add cmd to uboot.");
printf( "argc = %d\n", argc );
for( i=0; i < argc; i++ )
{
printf( "argv[%d] = %s\n", i, argv[i] );
}
return 0;
}
U_BOOT_CMD(
aston, 3, 1, do_aston,
"aston - usage of my aston\n",
"long help of aston\n"
);
//结束
root@xfj-virtual-machine:~/x210v3_bsp/uboot/common# pwd
/root/x210v3_bsp/uboot/common
root@xfj-virtual-machine:~/x210v3_bsp/uboot/common# vim Makefile
COBJS-y += aston.o
//CONFIG_CMD_AMBAPP:若被定义了,则添加;否则不添加
COBJS-$(CONFIG_CMD_AMBAPP) += cmd_ambapp.o