格式化输出-用户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系统要控制自己的输入输出,当同为一个后端时,例如都在串口上,为了不被 printkprintf 干扰,建议使用 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同时在多线程或中断中存在时会相互干扰,使用时需要留意。