Zephyr格式化输出-用户API和后端

本文说明Zephyr格式化输出使用API和对应后端。

在Zephyr中提供以下API提供格式化输出

  • printk: 内核调试信息打印
  • *printf:printf/fprintf/vfprintf/vprintf等标准C输出
  • shell_print: Shell系统打印
  • LOG_*: LOG_INF/LOG_DBG/LOG_ERR/LOG_WAR,LOG系统打印

以下分别说明其格式化处理函数及用什么后端输出,也可以直接跳到最后看总结。

prink 链接到标题

printk通常用于格式化输出内核调试信息的打印,为了方便我们通常也会在自己写的应用中调用该API进行打印输出。

格式化处理函数 链接到标题

printk源代码在lib/os/printk.c中通过调用vprintk完成

void vprintk(const char *fmt, va_list ap)
{
	struct out_context ctx = { 0 };

	cbvprintf(char_out, &ctx, fmt, ap);
}

vprintk使用cbvprintf完成字符串格式化,并同时使用char_out一个一个字符输出

static int char_out(int c, void *ctx_p)
{
	struct out_context *ctx = ctx_p;

	ctx->count++;
	return _char_out(c);
}

后端 链接到标题

char_out由__printk_hook_install注册的_char_out进行字符输出

void __printk_hook_install(int (*fn)(int))
{
	_char_out = fn;
}

__printk_hook_install会在zephyr console初始化的时候调用,设置入console的输出,根据平台硬件的不同zephyr的console支持UART,RAM, RTT, IPM等等。一般情况下我们默认会使用UART,也就是printk输出到串口。 console的代码在driver/console/下,串口的文件是uart_console.c 在其初始化函数uart_console_init中调用uart_console_hook_install进行printk hook注册

static void uart_console_hook_install(void)
{
#if defined(CONFIG_STDOUT_CONSOLE)
	__stdout_hook_install(console_out);
#endif
#if defined(CONFIG_PRINTK)
	__printk_hook_install(console_out);
#endif
}

*printf 链接到标题

Zephyr在lib/libc/minimal/source/stdout下实现了printf/fprintf/vfprintf/vprintf等标准C输出, 以在fprint.c中的printf进行说明

格式化处理函数 链接到标题

int printf(const char *ZRESTRICT format, ...)
{
	va_list vargs;
	int     r;

	va_start(vargs, format);
	r = cbvprintf(fputc, DESC(stdout), format, vargs);
	va_end(vargs);

	return r;
}

printf使用cbvprintf完成字符串格式化,并同时使用fputc一个一个字符输出, 在stdout_console.c

int fputc(int c, FILE *stream)
{
	return zephyr_fputc(c, stream);
}

注意,当Zephyr使用第三方libc时,例如newlib,库中的标准格式化输出函数将使用第三方库中格式化处理而不是使用cbvprintf

后端 链接到标题

zephyr_fputc实现为在z_impl_zephyr_fputc

int z_impl_zephyr_fputc(int c, FILE *stream)
{
	return (stream == stdout || stream == stderr) ? _stdout_hook(c) : EOF;
}

其hook函数由__stdout_hook_install注册

void __stdout_hook_install(int (*hook)(int))
{
	_stdout_hook = hook;
}

和printk一样其hook函数会在console driver中调用__stdout_hook_install进行注册(前面代码uart_console_hook_install可以看到)

shell_print 链接到标题

shell系统要控制自己的输入输出,当同为一个后端时,例如都在串口上,为了不被printk和printf干扰,建议使用shell_print进行打印。 shell_print是一个include\shell\shell.h中的宏,实际使用的是subsys\shell\shell.c中的shell_fprintf为简化说明,列出调用关系: shell_print->shell_fprintf->shell_vfprintf->z_shell_vfprintf->z_shell_fprintf_fmt

格式化处理函数 链接到标题

shell_fprintf.c中实现的z_shell_fprintf_fmt

void z_shell_fprintf_fmt(const struct shell_fprintf *sh_fprintf,
			 const char *fmt, va_list args)
{
	(void)cbvprintf(out_func, (void *)sh_fprintf, fmt, args);

	if (sh_fprintf->ctrl_blk->autoflush) {
		z_shell_fprintf_buffer_flush(sh_fprintf);
	}
}

z_shell_fprintf_fmt使用cbvprintf完成字符串格式化,并同时使用out_func一个一个字符输出。

static int out_func(int c, void *ctx)
{
	const struct shell_fprintf *sh_fprintf;
	const struct shell *shell;

	sh_fprintf = (const struct shell_fprintf *)ctx;
	shell = (const struct shell *)sh_fprintf->user_ctx;

	if ((shell->shell_flag == SHELL_FLAG_OLF_CRLF) && (c == '\n')) {
		(void)out_func('\r', ctx);
	}
	//装入buffer
	sh_fprintf->buffer[sh_fprintf->ctrl_blk->buffer_cnt] = (uint8_t)c;
	sh_fprintf->ctrl_blk->buffer_cnt++;

	//装满后才进行真正的flush写到后端
	if (sh_fprintf->ctrl_blk->buffer_cnt == sh_fprintf->buffer_size) {
		z_shell_fprintf_buffer_flush(sh_fprintf);
	}

	return 0;
}

void z_shell_fprintf_buffer_flush(const struct shell_fprintf *sh_fprintf)
{
	//写到后端
	sh_fprintf->fwrite(sh_fprintf->user_ctx, sh_fprintf->buffer,
			   sh_fprintf->ctrl_blk->buffer_cnt);
	sh_fprintf->ctrl_blk->buffer_cnt = 0;
}

sh_fprintf->fwriteSHELL_DEFINE->Z_SHELL_FPRINTF_DEFINE注册函数z_shell_print_stream最后会调用到z_shell_write

void z_shell_write(const struct shell *shell, const void *data,
		 size_t length)
{
	__ASSERT_NO_MSG(shell && data);

	size_t offset = 0;
	size_t tmp_cnt;

	while (length) {
		int err = shell->iface->api->write(shell->iface,
				&((const uint8_t *) data)[offset], length,
				&tmp_cnt);
		(void)err;
		__ASSERT_NO_MSG(err == 0);
		__ASSERT_NO_MSG(length >= tmp_cnt);
		offset += tmp_cnt;
		length -= tmp_cnt;
		if (tmp_cnt == 0 &&
		    (shell->ctx->state != SHELL_STATE_PANIC_MODE_ACTIVE)) {
			shell_pend_on_txdone(shell);
		}
	}

这里的shell->iface->api->write就是shell的后端write

后端 链接到标题

shell的后端的所有实现都放在subsys/shell/backends下,支持uart, rtt, telnet, dummy,当选择串口作为后端时shell_print将输出到串口,串口后端实现的代码是shell_uart.c

const struct shell_transport_api shell_uart_transport_api = {
	.init = init,
	.uninit = uninit,
	.enable = enable,
	.write = write,
	.read = read,
#ifdef CONFIG_MCUMGR_SMP_SHELL
	.update = update,
#endif /* CONFIG_MCUMGR_SMP_SHELL */
};

static int write(const struct shell_transport *transport,
		 const void *data, size_t length, size_t *cnt)
{
	const struct shell_uart *sh_uart = (struct shell_uart *)transport->ctx;
	const uint8_t *data8 = (const uint8_t *)data;

	//使用串口直接输出
		for (size_t i = 0; i < length; i++) {
			uart_poll_out(sh_uart->ctrl_blk->dev, data8[i]);
		}

		*cnt = length;

		sh_uart->ctrl_blk->handler(SHELL_TRANSPORT_EVT_TX_RDY,
					   sh_uart->ctrl_blk->context);


	return 0;
}

LOG_*: 链接到标题

LOG_INF/LOG_DBG/LOG_ERR/LOG_WAR是LOG系统打印,Zephyr提供这些格式化打印接口方便过滤和控制打印。 其调用关系可简化为: LOG_\*->Z_LOG->Z_LOG2-Z_LOG_MSG2_CREATE->Z_LOG_MSG2_CREATE2 Z_LOG_MSG2_CREATE2终于会根据配置的不同调用z_log_msg2_runtime_createZ_LOG_MSG2_SIMPLE_CREATEZ_LOG_MSG2_STACK_CREATE

格式化处理函数 链接到标题

动态生成 链接到标题

z_log_msg2_runtime_create->z_log_msg2_runtime_vcreate->z_impl_z_log_msg2_runtime_vcreate

void z_impl_z_log_msg2_runtime_vcreate(uint8_t domain_id, const void *source,
				uint8_t level, const void *data, size_t dlen,
				const char *fmt, va_list ap)
{
	int plen;

	if (fmt) {
		va_list ap2;

		va_copy(ap2, ap);
		plen = cbvprintf_package(NULL, Z_LOG_MSG2_ALIGN_OFFSET, 0,
					 fmt, ap2);
		__ASSERT_NO_MSG(plen >= 0);
		va_end(ap2);
	} else {
		plen = 0;
	}

	size_t msg_wlen = Z_LOG_MSG2_ALIGNED_WLEN(plen, dlen);
	struct log_msg2 *msg;
	struct log_msg2_desc desc =
		Z_LOG_MSG_DESC_INITIALIZER(domain_id, level, plen, dlen);

	if (IS_ENABLED(CONFIG_LOG2_MODE_IMMEDIATE)) {
		msg = alloca(msg_wlen * sizeof(int));
	} else {
		msg = z_log_msg2_alloc(msg_wlen);
	}

	if (msg && fmt) {
		plen = cbvprintf_package(msg->data, (size_t)plen, 0, fmt, ap);
		__ASSERT_NO_MSG(plen >= 0);
	}

	z_log_msg2_finalize(msg, source, desc, data);
}

使用cbvprintf_package打包格式化,使用z_log_msg2_finalize对打包后的数据进行输出

静态生成时 链接到标题

Z_LOG_MSG2_SIMPLE_CREATE先使用CBPRINTF_STATIC_PACKAGE打包格式化,再使用z_log_msg2_finalize对打包后的数据进行输出 Z_LOG_MSG2_STACK_CREATE先使用CBPRINTF_STATIC_PACKAGE打包格式化,再通过z_log_msg2_static_create->z_impl_z_log_msg2_static_create->z_log_msg2_finalize对打包后的数据进行输出

后端 链接到标题

z_log_msg2_finalize只是将cbvprintf_packageCBPRINTF_STATIC_PACKAGE打包后的数据送到log core, log core会将包送给后端进行显示。 log的backend实现文件放到subsys\logging\中以名字为log_backend_*.c, log系统的backend可以根据硬件平台的不同选择uart, rtt, swo, fs, net等等。其中uart实现在log_backend_uart.c 显示的执行流程是process->log_output_msg2_process简化如下

void log_output_msg2_process(const struct log_output *output,
			     struct log_msg2 *msg, uint32_t flags)
{
	//读取包数据
	uint8_t *data = log_msg2_get_package(msg, &len);
	
	if (len) {
		int err = cbpprintf(raw_string ? cr_out_func :  out_func,
				    (void *)output, data);

		(void)err;
		__ASSERT_NO_MSG(err >= 0);
	}
	
	//使用cbpprintf解析包数据,并使用out_func输出
	if (len) {
		int err = cbpprintf(raw_string ? cr_out_func :  out_func,
				    (void *)output, data);

		(void)err;
		__ASSERT_NO_MSG(err >= 0);
	}
}

out_func实现在log_output.c中,收到字符会先放到buffer,达到一定量后调用log_output_flush->buffer_write->(output->func)进行输出 output使用的是LOG_OUTPUT_DEFINE(log_output_uart, char_out, uart_output_buf, sizeof(uart_output_buf)); uart的char_out实现如下

static int char_out(uint8_t *data, size_t length, void *ctx)
{
	ARG_UNUSED(ctx);
	int err;

	if (IS_ENABLED(CONFIG_LOG_BACKEND_UART_OUTPUT_DICTIONARY_HEX)) {
		dict_char_out_hex(data, length);
		return length;
	}

	if (!IS_ENABLED(CONFIG_LOG_BACKEND_UART_ASYNC) || in_panic || !use_async) {
		for (size_t i = 0; i < length; i++) {
			uart_poll_out(uart_dev, data[i]);
		}

		return length;
	}

	err = uart_tx(uart_dev, data, length, SYS_FOREVER_US);
	__ASSERT_NO_MSG(err == 0);

	err = k_sem_take(&sem, K_FOREVER);
	__ASSERT_NO_MSG(err == 0);

	(void)err;

	return length;
}

可以看到是使用的串口驱动直接输出。

总结 链接到标题

printk: 使用cbvprintf完成字符串格式化,输出由console决定 *printf: 当使用zephyr自己的minilibc时,使用cbvprintf完成字符串格式化,输出由console决定 shell_print: 使用cbvprintf完成字符串格式化,输出由shell自己配置的后端决定 LOG_*:使用cbvprintf_packageCBPRINTF_STATIC_PACKAGE打包格式化字符串,由cbpprintf根据包数据完成字符串格化,输出由log自己配置的后端决定

当console和shell后端还有log后端都选择为串口时,由于大家最后都是通过串口驱动输出,以上4类格式化API同时在多线程或中断中存在时会相互干扰,使用时需要留意。