Nordic 日志
Nordic 日志
本文为本人粗略研究Nordic所作笔记,只研究了在GUNC中的代码
编译器环境为 GUNC
本文中用到的知识点:
- C语言中# 和##的用法
- 字符串的拼接
- ANSI颜色控制码
- C语言中attribute属性的定义与用法
1、日志中的定义
1.1、日志信息结构体
typedef struct
{
//模块名字,字符串,由##方式拼接而成
const char * p_module_name; ///< Module or instance name.
//模块颜色,ANSI控制码
uint8_t info_color_id; ///< Color code of info messages.
uint8_t debug_color_id; ///< Color code of debug messages.
//编译等级
nrf_log_severity_t compiled_lvl; ///< Compiled highest severity level.
nrf_log_severity_t initial_lvl; ///< Severity level for given module or instance set on backend initialization.
} nrf_log_module_const_data_t;
1.2、通用宏
#define NRF_LOG_ITEM_DATA(_name) CONCAT_3(m_nrf_log_,_name,_logs_data)
//宏拼接
#define CONCAT_2(p1, p2) CONCAT_2_(p1, p2)
#define CONCAT_2_(p1, p2) p1##p2
//转字符串
#define STRINGIFY_(val) #val
#define STRINGIFY(val) STRINGIFY_(val)
#define NRF_LOG_CONST_SECTION_NAME(_module_name) CONCAT_2(log_const_data_,_module_name)
#define _CONST const
1.3、日志注册宏
//定义了一个const修饰的结构体变量
#define NRF_LOG_INTERNAL_CONST_ITEM_REGISTER( \
_name, _str_name, _info_color, _debug_color, _initial_lvl, _compiled_lvl) \
NRF_SECTION_ITEM_REGISTER(NRF_LOG_CONST_SECTION_NAME(_name), \
_CONST nrf_log_module_const_data_t NRF_LOG_ITEM_DATA_CONST(_name)) = { \
.p_module_name = _str_name, \
.info_color_id = (_info_color), \
.debug_color_id = (_debug_color), \
.compiled_lvl = (nrf_log_severity_t)(_compiled_lvl), \
.initial_lvl = (nrf_log_severity_t)(_initial_lvl), \
}
#define NRF_LOG_INTERNAL_ITEM_REGISTER( \
_name, _str_name, _info_color, _debug_color, _initial_lvl, _compiled_lvl) \
NRF_LOG_INTERNAL_CONST_ITEM_REGISTER(_name, \
_str_name, \
_info_color, \
_debug_color, \
_initial_lvl, \
_compiled_lvl)
#define NRF_LOG_INTERNAL_MODULE_REGISTER() \
NRF_LOG_INTERNAL_ITEM_REGISTER(NRF_LOG_MODULE_NAME, \
STRINGIFY(NRF_LOG_MODULE_NAME), \
NRF_LOG_INFO_COLOR, \
NRF_LOG_DEBUG_COLOR, \
NRF_LOG_INITIAL_LEVEL, \
COMPILED_LOG_LEVEL)
#define NRF_LOG_MODULE_REGISTER() NRF_LOG_INTERNAL_MODULE_REGISTER()
1.4、打印宏
//日志打印的最终走向,
#define LOG_INTERNAL_X(N, ...) CONCAT_2(LOG_INTERNAL_, N) (__VA_ARGS__)
//日志的打印接入口
// NUM_VA_ARGS_LESS_1宏为一个数值,该数值为可变参数目减一
#define LOG_INTERNAL(type, ...) LOG_INTERNAL_X(NUM_VA_ARGS_LESS_1( __VA_ARGS__), type, __VA_ARGS__)
#define NRF_LOG_INTERNAL_MODULE(level, level_id, ...) \
if (NRF_LOG_ENABLED && (NRF_LOG_LEVEL >= level) && \
(level <= NRF_LOG_DEFAULT_LEVEL)) \
{ \
if (NRF_LOG_FILTER >= level) \
{ \
LOG_INTERNAL(LOG_SEVERITY_MOD_ID(level_id), __VA_ARGS__); \
} \
}
#define NRF_LOG_INTERNAL_INFO(...) \
NRF_LOG_INTERNAL_MODULE(NRF_LOG_SEVERITY_INFO, NRF_LOG_SEVERITY_INFO, __VA_ARGS__)
#define NRF_LOG_INFO(...) NRF_LOG_INTERNAL_INFO( __VA_ARGS__)
1.5、其他宏
#define NRF_SECTION_START_ADDR(section_name) &CONCAT_2(__start_, section_name)
//该宏找到了变量的起始地址,来确定该模块具体的信息
#define NRF_LOG_MODULE_ID_POS 16
#define NRF_LOG_MODULE_ID_GET_CONST(addr) (((uint32_t)(addr) - \
(uint32_t)NRF_SECTION_START_ADDR(log_const_data)) / \
sizeof(nrf_log_module_const_data_t))
#define NRF_LOG_ITEM_DATA_CONST(_name) CONCAT_2(NRF_LOG_ITEM_DATA(_name),_const)
#define NRF_LOG_MODULE_ID NRF_LOG_MODULE_ID_GET_CONST(&NRF_LOG_ITEM_DATA_CONST(NRF_LOG_MODULE_NAME))
#define LOG_SEVERITY_MOD_ID(severity) ((severity) | NRF_LOG_MODULE_ID << NRF_LOG_MODULE_ID_POS)
//ANSI控制码中的颜色
// <0=> Default
// <1=> Black
// <2=> Red
// <3=> Green
// <4=> Yellow
// <5=> Blue
// <6=> Magenta
// <7=> Cyan
// <8=> White
#ifndef NRF_LOG_COLOR_DEFAULT
#define NRF_LOG_COLOR_DEFAULT 0 //默认颜色
#endif
//日志等级
// <0=> Off
// <1=> Error
// <2=> Warning
// <3=> Info
// <4=> Debug
#ifndef NRF_LOG_DEFAULT_LEVEL
#define NRF_LOG_DEFAULT_LEVEL 3 //默认为Info
#endif
#ifndef NRF_LOG_LEVEL
#define NRF_LOG_LEVEL NRF_LOG_DEFAULT_LEVEL
#endif
#ifndef NRF_LOG_INITIAL_LEVEL
#define NRF_LOG_INITIAL_LEVEL NRF_LOG_LEVEL
#endif
#ifndef NRF_LOG_INFO_COLOR
#define NRF_LOG_INFO_COLOR NRF_LOG_COLOR_DEFAULT
#endif
#ifndef NRF_LOG_DEBUG_COLOR
#define NRF_LOG_DEBUG_COLOR NRF_LOG_COLOR_DEFAULT
#endif
#define COMPILED_LOG_LEVEL NRF_LOG_LEVEL //实际用到的值
// <0=> Off
// <1=> Error
// <2=> Warning
// <3=> Info
// <4=> Debug
#ifndef NRF_LOG_DEFAULT_LEVEL
#define NRF_LOG_DEFAULT_LEVEL 3
#endif
#define NRF_LOG_FILTER NRF_LOG_SEVERITY_DEBUG
#ifndef NRF_LOG_USES_COLORS
#define NRF_LOG_USES_COLORS 0
#endif
#ifndef NRF_LOG_USES_TIMESTAMP
#define NRF_LOG_USES_TIMESTAMP 0
#endif
#define NRF_SECTION_DEF(section_name, data_type) \
extern data_type * CONCAT_2(__start_, section_name); \
extern void * CONCAT_2(__stop_, section_name)
1.6、适配不同编译链
#if defined(__CC_ARM) //ARM编译链
#define NRF_SECTION_ITEM_REGISTER(section_name, section_var) \
section_var __attribute__ ((section(STRINGIFY(section_name)))) __attribute__((used))
#elif defined(__GNUC__)
#define NRF_SECTION_ITEM_REGISTER(section_name, section_var) \
section_var __attribute__ ((section("." STRINGIFY(section_name)))) __attribute__((used))
//实际为GUNC编译链
#elif defined(__ICCARM__)
#define NRF_SECTION_ITEM_REGISTER(section_name, section_var) \
__root section_var @ STRINGIFY(section_name)
#endif
1.7、项目信息
<ProgramSection alignment="4" keep="Yes" load="Yes" name=".log_const_data" inputsections="*(SORT(.log_const_data*))" address_symbol="__start_log_const_data" end_symbol="__stop_log_const_data" />
1.8、具体函数
#define LOG_INTERNAL_1(type, str, arg0) \
/*lint -save -e571*/nrf_log_frontend_std_1(type, str, (uint32_t)(arg0))/*lint -restore*/
void nrf_log_frontend_std_1(uint32_t severity_mid,
char const * const p_str,
uint32_t val0)
{
uint32_t args[] = {val0};
std_n(severity_mid, p_str, args, ARRAY_SIZE(args));
}
2、 日志调用
2.1 日志初始化
2.1.1 相关宏
#define NRF_LOG_INIT(...) NRF_LOG_INTERNAL_INIT(__VA_ARGS__)
#define NRF_LOG_INTERNAL_INIT(...) \
nrf_log_init(GET_VA_ARG_1(__VA_ARGS__), \
GET_VA_ARG_1(GET_ARGS_AFTER_1(__VA_ARGS__, LOG_TIMESTAMP_DEFAULT_FREQUENCY)))
//用来确保参数必为一个参数
#define GET_VA_ARG_1(...) GET_VA_ARG_1_(__VA_ARGS__, ) // Make sure that also for 1 argument it works
#define GET_VA_ARG_1_(a1, ...) a1
//使用第二个参数
#define GET_ARGS_AFTER_1(...) GET_ARGS_AFTER_1_(__VA_ARGS__, ) // Make sure that also for 1 argument it works
#define GET_ARGS_AFTER_1_(a1, ...) __VA_ARGS__
//确认时钟频率,用做时间戳打印
#define LOG_TIMESTAMP_DEFAULT_FREQUENCY ((NRF_LOG_TIMESTAMP_DEFAULT_FREQUENCY == 0) ? \
(NRF_LOG_LFCLK_FREQ/(APP_TIMER_CONFIG_RTC_FREQUENCY + 1)) : \
NRF_LOG_TIMESTAMP_DEFAULT_FREQUENCY)
2.1.2 宏替换分析
NRF_LOG_INIT(NULL); //主函数调用宏
——–>
nrf_log_init(NULL,GET_VA_ARG_1(GET_ARGS_AFTER_1(NULL, LOG_TIMESTAMP_DEFAULT_FREQUENCY))) //宏用来确定至少只有一个参数
——–>
nrf_log_init(NULL,LOG_TIMESTAMP_DEFAULT_FREQUENCY) //第一个传参为NULL,代表打印不带时间戳
2.2 日志结构体定义
2.2.1 模块中的日志定义
使用NRF打印的函数将定义有下列宏
//文件开始包含
#define NRF_LOG_MODULE_NAME moliam
NRF_LOG_MODULE_REGISTER();
2.2.2 宏替换分析
NRF_LOG_MODULE_REGISTER();
——–> NRF_LOG_INTERNAL_ITEM_REGISTER(moliam,#moliam,NRF_LOG_INFO_COLOR,NRF_LOG_DEBUG_COLOR,NRF_LOG_INITIAL_LEVEL,COMPILED_LOG_LEVEL);
——–>
NRF_LOG_INTERNAL_ITEM_REGISTER(moliam,”moliam”,0,0,3,3) ;
——–>
NRF_LOG_INTERNAL_CONST_ITEM_REGISTER(moliam,”moliam”,0,0,3,3) ;
——–> NRF_SECTION_ITEM_REGISTER(NRF_LOG_CONST_SECTION_NAME(moliam),const nrf_log_module_const_data_t NRF_LOG_ITEM_DATA_CONST(moliam)) = {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
};
——–> NRF_SECTION_ITEM_REGISTER(NRF_LOG_CONST_SECTION_NAME(moliam),const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const ) = {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
};
——–>
NRF_SECTION_ITEM_REGISTER(log_const_data_moliam , const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const) = {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
};
——–>
const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const attribute ((section(“.” “log_const_data_moliam”))) attribute((used))= {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
};
——–>
const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const attribute ((section(“.” “log_const_data_moliam”))) attribute((used)) = {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
};
——–>
const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const attribute ((section(“.log_const_data_moliam”))) attribute((used)) = {
.p_module_name = “moliam”,
.info_color_id = 0,
.debug_color_id = 0,
.compiled_lvl = 3,
.initial_lvl = 3,
}; //“a” “b” 会被编译器认为”ab” 多用来换行,以及字符串拼接
//整体替换
——–>
const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const attribute ((section(“.log_const_data_moliam”))) attribute((used)) = {
.p_module_name = “moliam”,
.info_color_id = NRF_LOG_INFO_COLOR,
.debug_color_id = NRF_LOG_DEBUG_COLOR,
.compiled_lvl = COMPILED_LOG_LEVEL,
.initial_lvl = NRF_LOG_INITIAL_LEVEL,
} ;
——–>
const nrf_log_module_const_data_t m_nrf_log_moliam_logs_data_const attribute ((section(“.log_const_data_moliam”))) attribute((used)) = {
.p_module_name = “moliam”,
.info_color_id = NRF_LOG_INFO_COLOR,
.debug_color_id = NRF_LOG_DEBUG_COLOR,
.compiled_lvl = (COMPILED_LOG_LEVEL),
.initial_lvl = (NRF_LOG_INITIAL_LEVEL),
} ;
2.2.3 Tips
——–> 该宏定义了一个常量,且该常量的ram被固定分配在被命名为<.log_const_data_moliam>的地址空间(实际地址由编译器分配) 所有的该类型变量全分配在了地址相近的ram中,这也是后面可通过 LOG_SEVERITY_MOD_ID(level_id) 能够找到模块数据地址的真正原因,而不需要外部 extern 去声明**attribute**((used))说明该变量需要保存,不提示变量不使用的警告信息,即使模块中没有使用
2.3 调用日志
以NRF_LOG_INFO为例
2.3.1 宏替换分析
NRF_LOG_INFO(“moliam is %s”, “handsome”);
——–>
NRF_LOG_INTERNAL_INFO(“moliam is %s”, “handsome”)
——–>
NRF_LOG_INTERNAL_MODULE(3,3,”moliam is %s”, “handsome”) //NRF_LOG_SEVERITY_INFO 日志等级为3
——–>
if(NRF_LOG_ENABLED && (NRF_LOG_LEVEL >= 3) && (3 <= 3) ){
if(4 >= 3){
LOG_INTERNAL(LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”);
}
} //这一步是等级过滤,低优先级的会被过滤掉不进行打印
——–>
LOG_INTERNAL(LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”); //过滤后
——–>
LOG_INTERNAL(((3) | NRF_LOG_MODULE_ID << NRF_LOG_MODULE_ID_POS) , “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | NRF_LOG_MODULE_ID << 16) , “moliam is %s”, “handsome”); // << 的优先级比 | 大一些
——–>
LOG_INTERNAL((3 | NRF_LOG_MODULE_ID_GET_CONST ( & NRF_LOG_ITEM_DATA_CONST(NRF_LOG_MODULE_NAME)) << 16) , “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | NRF_LOG_MODULE_ID_GET_CONST (&NRF_LOG_ITEM_DATA_CONST(moliam)) << 16) , “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | NRF_LOG_MODULE_ID_GET_CONST (&m_nrf_log_moliam_logs_data_const) << 16) , “moliam is %s”, “handsome”);
//这个变量在上边已被定义
——–>
LOG_INTERNAL((3 | (((uint32_t)(&m_nrf_log_moliam_logs_data_const) - \
(uint32_t)NRF_SECTION_START_ADDR(log_const_data)) / sizeof(nrf_log_module_const_data_t)) << 16) , “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | (((uint32_t)(&m_nrf_log_moliam_logs_data_const) - \
(uint32_t)&__start_log_const_data) / sizeof(nrf_log_module_const_data_t)) << 16), “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | (((uint32_t)(&m_nrf_log_moliam_logs_data_const) - (uint32_t)&__start_log_const_data) / sizeof(nrf_log_module_const_data_t)) << 16), “moliam is %s”, “handsome”);
——–>
LOG_INTERNAL((3 | (((uint32_t)(&m_nrf_log_moliam_logs_data_const) - (uint32_t)&__start_log_const_data) / 8) << 16), “moliam is %s”, “handsome”);
// __start_log_const_data 在前面已说过,是GUNC编译链实现的(ses开发环境用的是开源编译器GCC,linux编译器也使用此编译链)
// ARM编译链(keil 开发环境)和__ICCARM__编译链(IAR开发环境)实现不一样
//该宏是为了确定具体模块定义的地址,然后进行取值,了解了功能,再将LOG_SEVERITY_MOD_ID(3) 换回来
——–>
LOG_INTERNAL(LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”); //过滤后
——–>
LOG_INTERNAL_X(NUM_VA_ARGS_LESS_1( “moliam is %s”, “handsome”), LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”);
//NUM_VA_ARGS_LESS_1 这个宏brief上写的是获取可变参数的数量 我看不懂,但我大受震撼 宏名就是参数值少1 ,是把第一个参数当成格式化输出
//NUM_VA_ARGS_LESS_1(“%p”,”aaa”) = 1 ; NUM_VA_ARGS_LESS_1(“%p”) = 0 //将第一个参数作为字符串,后面的才算数
//其实这个地方已经和标准库的格式控制符有些不一致了,比如说 %.*s 则需要两个参数,但是会被此宏给分解掉 虽说arg依然是2 这也是%.*s 会被NRF_LOG 只打印 s 这个字符的真正原因
——–>
LOG_INTERNAL_X(1,LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”); //可变参数量为1(handsome)
//通过可变参数目的方式,再将宏中的X具体化 此例具体化为1
——–>
LOG_INTERNAL_1(LOG_SEVERITY_MOD_ID(3), “moliam is %s”, “handsome”); //最大定义到LOG_INTERNAL_6 这也就是NRF最大只能由6个可变参的原因
——–> nrf_log_frontend_std_1(LOG_SEVERITY_MOD_ID(3),”moliam is %s”, (uint32_t)”handsome”); //在此处变成了函数
——–>
uint32_t args[] = {val0};
std_n(LOG_SEVERITY_MOD_ID(3),”moliam is %s”,args,1);
//只有一个参数 LOG_SEVERITY_MOD_ID(3) 用来索引moliam的module_id 且
//std_n 在nrf_log_frontend.c 中有定义
——–>
此时并没有将数据转换为 moliam is handsome 只是将first这个地址放入了全局变量中 如果没有autoflush 此宏就执行完毕了,日志信息都在 m_log_data 中,而wr_idx ++ , m_buffer_mask 则是通过掩码防止溢出 NRF_LOG_BUFSIZE 的手段
// 可以仔细分析一下这种防溢出,这个方式在uart_fifo中也有使用
// 掩码为NRF_LOG_BUFSIZE - 1 每次 fifo[(wr_idx ++) & mask] 可保证超出fifo自动从0开始 但是wr_idx 却是递增!就可以直接用 wr_idx - rd_idx 获取长度,也可以判断出是否超出 NRF_LOG_BUFSIZE,此方式也有一个弊端,长度必须是2的指数
2.4 日志输出分析
在调用NRF_LOG_INFO()之后,并没有进行输出,而是通过NRF_LOG_FLUSH()进行输出。
这也是如果使用官方移植的FreeRTOS如果空闲钩子为1时,为什么功耗维持在7mA左右的原因。
就是因为官方在FreeRTOS的空闲任务一直执行此函数,然后导致的芯片没有休眠。
解决这个问题的办法就是,自己找个地方调用该函数,而将钩子设置为1
NRF_LOG_FLUSH();
——–>
在函数 nrf_log_frontend_dequeue 中使用 nrf_memobj_write函数 将数据放入结构体 具体元素nrf_log_internal.h 313 - 356 行有定义 severity 元素为携带的等级 3
——–>
通过 m_log_data.p_backend_head->p_api->put() 这个函数指针进行发送 而此函数指针对于RTT输出和串口输出有以下定义
RTT的输出函数
const nrf_log_backend_api_t nrf_log_backend_rtt_api = {
.put = nrf_log_backend_rtt_put,
.flush = nrf_log_backend_rtt_flush,
.panic_set = nrf_log_backend_rtt_panic_set,
};
串口的输出函数
const nrf_log_backend_api_t nrf_log_backend_uart_api = {
.put = nrf_log_backend_uart_put,
.flush = nrf_log_backend_uart_flush,
.panic_set = nrf_log_backend_uart_panic_set,
};
最终都会调用 nrf_log_backend_serial_put() 函数,
—->
颜色在该函数中定义 params.use_colors = NRF_LOG_USES_COLORS; 并作为传参传入 nrf_log_std_entry_process
—->
再调用 nrf_log_std_entry_process 该函数中 p_ctx 即为打印的真正字符串
nrf_log_std_entry_process 函数 执行功能如下:
{
//该函数是整理需要放入到用户字符串前面的数据 (有红色的drop在此处定义 终于知道如果打印过多的话,为什么会有红色drop)
// 输出:颜色控制 + 时间戳 + 模块信息头
// 1、颜色
// nrf_fprintf(p_ctx, “%s”, m_colors[nrf_log_color_id_get( p_params->module_id, p_params->severity)]);
// static const char * m_colors 这个颜色中根据Module_id来获取
// 而颜色字符串为 ANSI 控制码 https://blog.csdn.net/weixin_44110772/article/details/105860997 // 函数中判断了 NRF_LOG_USES_COLORS 如果不为 0(sdk_config.h 中有定义),则使用m_colors[NRF_LOG_USES_COLORS] 进行颜色打印
// 如若为0 则使用 NRF_LOG_MODULE_REGISTER()中的颜色参数
// 2、时间戳
// 获取uint32_t 的时间戳 是NRF_LOG_INIT()函数中的第一个参数 NRF_LOG_USES_TIMESTAMP 为1时需要传入
// NRF_LOG_STR_FORMATTER_TIMESTAMP_FORMAT_ENABLED 为1时 为格式化的时间戳 // nrf_fprintf(p_ctx, “[%02d:%02d:%02d.%03d,%03d] “, hours, mins, seconds, ms, us);
// 3、模块信息头
// 填充完毕颜色则 fmt(“<%s> %s: “,severity_names,module_name) 变成了打印头
moliam :prefix_process(p_params, p_ctx); //格式化字符串,此时才将handsome真正的放入到了 “moliam is %s” 此时是Nordic实现的可变参
//在此之前都是用的nrf_log_header_t 这个结构体进行存储日志相关数据 nrf_log_internal.h 313 - 356 行所显示的进行存储,一行为4字节
nrf_fprintf(p_ctx)
//该函数是整理需要放入到用户字符串后面的数据
// 其中有ANSI 控制码 还有换行使用的\r\n
postfix_process(p_params, p_ctx, false);
}
——>
字符串组成 为”\x1B[0m[12:00:43.002,005]
moliam: moliam is handsome” 显示为 默认颜色 终端显示为 [12:00:43.002,005]
moliam: moliam is handsome
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!