当前位置: 首页 > news >正文

SoC仿真环境中自定义printf函数的实现

博主在做SoC芯片级验证编写c语言测试激励的时候发现printf函数都被一个自定义的打印函数给替代了,所以就学习了一下这些函数的实现过程及为什么要在SoC仿真环境中自定义打印函数而不是直接调用c标准库的printf函数。下面先介绍我做过的一个arm核和一个riscv核两个SoC项目中自定义printf函数的实现方式。

1.arm核SoC仿真环境中的自定义a_printf函数

a_printf 函数使用了可变参数(...)来模拟标准库的 printf 功能,其中va_list,va_endva_start是处理​​可变参数函数​​(类似printf)的核心宏,用于访问不确定数量的函数参数。

va_list​:声明一个指针,用于遍历可变参数列表

va_start​:初始化va_list,使其指向​第一个可变参数​​

va_end :​​​​用于清理 va_list 指针,结束可变参数的访问​​,防止内存或资源泄漏(必须和 va_start 配对使用)

a_printf函数的核心是将格式化逻辑委托给 vprintfmt 函数(下面会讲),并通过强制转换的 putchar_custom 回调函数实现​​输出目标定制​(例如输出到仿真器控制台、串口或内存缓冲区)。

void a_printf(const char* fmt, ...) 
{// 1. 初始化可变参数列表va_list ap;       // 定义可变参数指针(通常实现为char*)va_start(ap, fmt); // 将ap指向fmt之后的第一个参数(通过栈指针或寄存器定位)// 2. 调用核心格式化引擎// 参数说明://   (void*)putchar_custom - 将字符输出函数强制转换为泛型指针//   0                     - 输出设备的上下文(未使用)//   fmt                   - 格式化字符串//   ap                    - 可变参数列表vprintfmt((void*)putchar_custom, 0, fmt, ap);// 3. 清理可变参数列表va_end(ap); 
}

 vprintfmt 函数实现了一个​​轻量级格式化输出引擎​​,其核心功能是解析类似printf的格式字符串(如%d%s),通过回调函数putch逐字符输出结果,支持整数(十进制/十六进制/八进制)、字符、字符串等基础格式化,并允许控制对齐、填充、宽度等格式,专为​​嵌入式或仿真环境​​设计,避免了标准库printf的资源开销和硬件依赖,可直接重定向输出到自定义设备(如仿真器控制台或内存映射IO),vprintfmt 函数代码比较复杂,大致了解作用就行。

/*** 格式化输出核心函数(类似标准库的vprintf)* @param putch  字符输出回调函数,参数为(int字符, void*用户数据)* @param putdat 传递给putch的额外数据* @param fmt    格式字符串(如"%d %s")* @param ap     可变参数列表*/
static void vprintfmt(void (*putch)(int, void*), void *putdat, const char *fmt, va_list ap)
{register const char* p;         // 临时字符串指针const char* last_fmt;           // 记录格式字符串回溯位置register int ch, err;           // 当前字符和错误码(寄存器优化)unsigned long long num;         // 存储数值参数int base, lflag, width, precision, altflag; // 格式控制参数char padc;                      // 填充字符(空格或0)// 主循环:处理整个格式字符串while (1) {// 处理普通字符(非'%'部分)while ((ch = *(unsigned char *) fmt) != '%') {if (ch == '\0') return;     // 遇到字符串结束符则退出fmt++;                      // 移动格式字符串指针putch(ch, putdat);          // 输出当前字符}fmt++; // 跳过'%'字符/* 开始处理格式化指令(如%-08d中的符号、宽度等)*/last_fmt = fmt;               // 记录当前位置(用于错误回溯)padc = ' ';                   // 默认填充空格width = -1;                   // 默认无宽度限制precision = -1;               // 默认无精度限制lflag = 0;                    // 默认非long类型altflag = 0;                  // 默认无替代格式(如0x前缀)reswitch:// 解析格式控制字符switch (ch = *(unsigned char *) fmt++) {/* 标志位处理('-', '0', '#'等)*/case '-': padc = '-'; goto reswitch; // 左对齐case '0': padc = '0'; goto reswitch; // 用0填充case '#': altflag = 1; goto reswitch; // 替代格式(如0x)/* 宽度处理(数字或*)*/case '1'...'9': // 数字宽度(如%10d)for (precision = 0; ; ++fmt) {precision = precision * 10 + ch - '0';ch = *fmt;if (ch < '0' || ch > '9') break;}goto process_precision;case '*': // 动态宽度(如%*d)precision = va_arg(ap, int);goto process_precision;case '.': // 精度开始标记if (width < 0) width = 0;goto reswitch;/* 类型长度修饰(l)*/case 'l':lflag++; // long/long long标志goto reswitch;/* 具体数据类型处理 */case 'c': // 字符(%c)putch(va_arg(ap, int), putdat);break;case 's': // 字符串(%s)if ((p = va_arg(ap, char *)) == NULL)p = "(null)"; // NULL保护// 处理对齐和填充if (width > 0 && padc != '-') // 右侧填充for (width -= strnlen(p, precision); width > 0; width--)putch(padc, putdat);// 输出字符串内容for (; (ch = *p) != '\0' && (precision < 0 || --precision >= 0); p++)putch(ch, putdat);// 左侧填充(左对齐时)for (; width > 0; width--)putch(' ', putdat);break;/* 数值类型处理 */case 'd': // 有符号十进制(%d)num = getint(&ap, lflag);if ((long long)num < 0) { // 处理负数putch('-', putdat);num = -(long long)num;}base = 10;goto signed_number;case 'u': // 无符号十进制(%u)base = 10;goto unsigned_number;case 'o': // 八进制(%o)base = 8;goto unsigned_number;case 'p': // 指针(%p)lflag = 1; // 强制long类型putch('0', putdat); // 输出0x前缀putch('x', putdat);/* 继续执行x的case */case 'x': // 十六进制(%x/%X)base = 16;unsigned_number:num = getuint(&ap, lflag); // 获取无符号数signed_number:printnum(putch, putdat, num, base, width, padc); // 实际数字打印break;case '%': // 转义%%输出%putch(ch, putdat);break;default: // 未知格式指令putch('%', putdat); // 原样输出%fmt = last_fmt;     // 回溯到格式开始位置break;}}
}

 2.riscv核SoC仿真环境中的自定义tb_printf函数

tb_printf函数和上面的a_printf函数类似,代码如下

int tb_printf(char *fmt, ...)
{char    buf[1024];  // 输出缓冲区(栈空间,固定1024字节)char    *p;         // 缓冲区指针va_list args;       // 可变参数列表int     n = 0;      // 返回值(当前未使用,始终返回0)/ /初始化可变参数列表 va_start(args, fmt); // 将 args 指向第一个可变参数// 格式化字符串处理 // 将格式化后的字符串写入 buf,返回实际长度(未使用返回值)ee_vsprintf(buf, fmt, args); //清理可变参数列表va_end(args); // 结束可变参数访问//准备输出p = buf; // 让指针指向格式化后的字符串起始位置//调用汇编级输出函数// 将缓冲区内容通过底层硬件接口输出(如串口/UART)asm_print(p);return n; // 返回0
}

其中调用的asm_print是使用riscv汇编函数写的,代码如下可供参考

.globl asm_print                //伪指令​​,用于声明一个符号(通常是函数或变量名)为​​全局可见​​的,使得该符号可以被其他文件或模块访问
asm_print:nop                     // 对齐或调试占位
asm_contex_save:ASM_CONTEX_SAVE()       // 保存调用者寄存器上下文mv      s1, a0          // s1 = 字符串起始地址(a0为第一个参数)
asm_print_body:lw      s2, 0x0(s1)     // 加载32位字到s2li      s3, chip_base | print_addr  // s3 = 硬件输出地址sw      s2, 0x0(s3)     // 将32位字写入硬件addi    s1, s1, 4       // 指针后移4字节andi    s4, s2, 0xff    // 检查字节0(位7:0)beqz    s4, asm_print_endsrli    s2, s2, 8       // 右移8位andi    s4, s2, 0xff    // 检查字节1(位15:8)beqz    s4, asm_print_endsrli    s2, s2, 8       // 右移8位andi    s4, s2, 0xff    // 检查字节2(位23:16)beqz    s4, asm_print_endsrli    s2, s2, 8       // 右移8位andi    s4, s2, 0xff    // 检查字节3(位31:24)beqz    s4, asm_print_endj       asm_print_body  // 继续循环asm_print_end:nop                     // 对齐或调试占位
asm_contex_restore:ASM_CONTEX_RESTORE()    // 恢复调用者寄存器ret                     // 函数返回

asm_print函数 通过32位字批量写入方式将字符串内容输出到指定的硬件地址(如串口,UART)。其特点是每次读取4字节数据写入硬件,并逐字节检测NULL终止符;通过保存/恢复寄存器上下文保证函数安全性,适用于嵌入式系统或内核早期的低层调试输出。

在实际使用中虽然tb_printf函数可以带参数打印信息,而asm_print函数仅能打印字符串,但asm_print函数仿真速度很快,所以我一般用asm_print函数。

3.为什么SoC仿真要自定义printf?​

  1. ​硬件无关性​
    标准printf依赖UART/OS驱动,仿真环境可能没有真实硬件。自定义函数直接输出到仿真器控制台或日志。

  2. ​节省资源​
    标准库printf代码大(含浮点等),自定义版可精简到几百字节,适合Flash/ROM受限的嵌入式场景。

  3. ​确定性输出​
    仿真需要严格时序同步,标准printf可能有缓冲延迟,自定义函数确保即时输出。

  4. ​扩展功能​
    可添加仿真专用功能,如自动打时间戳、非标准格式(%r打印寄存器值)。

  5. ​裸机兼容性​
    无OS环境(如Bootloader)可能无C库支持,自定义printf不依赖运行时。

  6. ​安全可靠​
    避免标准库潜在的缓冲区溢出,自定义实现可严格限制输出长度,防止崩溃。

相关文章:

  • 怎么用企业网站做营销seo排名影响因素主要有
  • 做鞋的贸易公司网站怎么做好报个电脑培训班要多少钱
  • 新疆建设厅证件查询网站贵阳做网络推广的公司
  • 外包公司做网站西安优化外包
  • 响应式网站如何做的怎么弄一个自己的网址
  • 有做兼职的网站吗个人网页制作
  • 微算法科技融合Grover算法与统一哈希函数的混合经典-量子算法技术,可在多领域高效提升文本处理效率
  • nt!CcFlushCache函数分析之nt!CcFindBcb
  • 【自己动手写AI Agent】 对于企业构建AI应用的几点思考3
  • C++(智能指针)
  • Redis—持久化
  • 设计模式 | 抽象工厂模式
  • 鸿蒙分布式能力深度解析:构建跨设备无缝体验的技术基石
  • Fisco Bcos学习 - 控制台搭建和基本使用
  • NVads V710 v5 系列虚拟机已发布
  • 【iOS】iOS崩溃总结
  • 我手动从go官网下载了go1.16.15linux安装包,我该如何做,才能使得vscode仍能通过右下角来管理这个go版本
  • python学习笔记(深度学习)
  • 新能源汽车电池类型差异分析
  • 英飞凌高性能BMS解决方案助力汽车电动化
  • 亚远景-ASPICE与ISO 26262:汽车安全与软件质量的协同
  • 【世纪龙科技】新能源汽车VR虚拟体验展示馆-解锁认知新维度
  • 打造属于你的AI智能体,从数据开始 —— 使用 Bright Data MCP+Trae快速构建垂直智能体
  • React Native【实战范例】账号管理(含转换分组列表数据的封装,分组折叠的实现,账号的增删改查,表单校验等)
  • React + Umi(Umijs/Max) 搭建项目及配置
  • 大塘至浦北高速分布式光伏项目,让‘交通走廊’变身‘绿色能源带’