e2studio下printf重定向到串口:原理、实现与常见坑

发布时间:2026/10/22 3:05:51
e2studio下printf重定向到串口:原理、实现与常见坑 简介面向使用瑞萨RA系列单片机与e2studio开发环境的嵌入式工程师这是一份完整的printf、sprintf打印函数移植与实现示例工程。资源以瑞萨RA为主控平台展示如何在e2studio中配置UART串口并重定向标准输出使开发者能够通过printf快速输出调试信息提升开发与排错效率。压缩包共169个文件约1.3MB以c源码、h头文件为主体辅以e2studio生成的配置xml、链接脚本ld、makefile及工程元数据覆盖从初始化配置到编译链接的全流程便于直接导入或对照移植。目前已有1147人学习下载适合刚接触瑞萨RA、希望快速上手串口调试输出的单片机学习者与项目开发者。配套CSDN文字教程与B站视频讲解工程目录清晰、源码完整可帮助读者理解底层UART驱动与printf重定向机制并复用到实际项目中。1. 都在用printf调试为什么你的板子一亮串口就乱码在瑞萨 e2studio 里新建一个 RA 工程的第一个星期几乎所有开发者都会尝试在 main 后马上加一句 printf(hello\n)然后去串口助手等输出。结果往往是要么屏幕上什么都没有要么跳出一把乱码要么代码直接跑飞。不是我吓唬你printf 在 MCU 上不是语法问题而是没有一条真正能把字符送出去的电路。C 库的 printf 只负责格式化真正写完一个字符时它调用 fputc而 e2studio 默认环境里 fputc 指向的可能不是你的串口。我们要做的就是把这个钩子接到 UART 上从而让 printf 工作而 sprintf 则根本不需要硬件它只往一块内存数组里写格式化结果。这篇文章把两者一起讲透并给出可直接照抄的工程代码。2. printf和sprintf不是一回事重定向做的是底层钩子2.1 printf之所以能“打印”靠的是fputc这条小路我们平时说的“printf 打印到串口”其实在 ANSI C 标准里从来不是这么规定的。标准库的 printf 函数先解析格式字符串然后把生成的每个字符交给一个底层输出函数向外送。这个底层函数就是 fputc(int ch, FILE *f)其中 FILE *f 在 PC 上可能指向 stdout、文件或串口而在裸机环境下我们完全可以忽略它把它指向自定义外设。所以要让 printf 正常工作本质上只有一件事自己实现一个 fputc并保证每一个传入的字符都能从 MCU 发出去。常见做法一定不会在每个工程里重新发明格式化算法而是直接利用编译器自带的 C 库。在 e2studio 中默认的 printf 输出目标有两种选择一个是默认的“Semihosting”重定向到调试器另一个是用户自己写 fputc。如果没做任何处理printf 生成的字符会被调试器接管在源代码调试窗口里能看到但串口上没有。要改变它只需要在工程里定义同名 fputc 函数即可覆盖 C 库的默认符号。为了让这个重定向稳定可靠我一般会配合 FSP 提供的 UART 驱动来发送一个字节。瑞萨的异步串口驱动 R_SCI_UART_Write 不是“发完才返回”的而是把数据交给外设后立刻返回等发送完成会通过回调事件 UART_EVENT_TX_DATA_EMPTY 通知。因此我们需要一个标志位在调用发送前清零在回调里置一。下面是最小的 fputc 实现#include hal_data.h #include stdio.h volatile bool uart_tx_done true; void user_uart_callback(uart_callback_args_t *p_args) { if (p_args-event UART_EVENT_TX_DATA_EMPTY) { uart_tx_done true; } } int fputc(int ch, FILE *f) { (void)f; uint8_t c (uint8_t)ch; uart_tx_done false; R_SCI_UART_Write(g_uart0_ctrl, c, 1); while (!uart_tx_done) ; return ch; }“while (!uart_tx_done) ;”这句很容易让人担心死循环如果外设没触发回调程序会卡在那里。实际使用中这个等待是非常必要的因为发送下一字节前必须确认上一字节已经进入移位寄存器否则连续调用 R_SCI_UART_Write 会触发缓冲区冲突反而更容易翻车。注意将回调函数名与 FSP 配置里写的名字保持一致我这里用了 user_uart_callback你可以在 FSP 的 UART 配置页里任意命名。除了 fputcC 库还会把字符串输出交给 fputs(char *str, FILE *f)。如果不重定向它printf 中大量字符串会逐字符调用 fputc也可以跑只是慢。更高效的办法是同时提供 fputs 来实现整串发送后面会看到。对于绝大部分调试需求只实现 fputc 已经足够。这就是 printf 的唯一下层通路理解了它就理解为何 e2studio 下 printf 会“失灵”。2.2 sprintf只往内存里写不碰硬件但格式化规则同一套如果说 printf 的灵魂是“输出”那 sprintf 的灵魂是“格式化到内存”。它的签名是 int sprintf(char *buffer, const char *format, ...)把格式化结果写进 buffer然后在 buffer 末尾自动补 \0。它不依赖任何外设不需要重定向只要有一块合法的可写内存就能工作。很多开发者会把 printf 与 sprintf 混为一谈以为 sprintf 也需要底层发送钩子这是一个迷惑点。sprintf 的价值在于我们可以先把一整套日志数据组织成字符串再统一交给 UART 或 RTT 发送发送过程不受格式化时间干扰适合在中断上下文、RTOS 任务里使用。以下是一个最简单的使用示例char buf[80]; float temp 27.35f; uint16_t adc 1023; sprintf(buf, temp%.2f, adc%u\n, temp, adc);这里 %.2f 表示保留两位小数的浮点格式%u 表示无符号十进制。执行完后 buf 里就是 temp27.35, adc1023\n然后我们调用 uart_send_bytes(buf, strlen(buf)) 一次性发送即可。注意 sprintf 不会检查 buf 是否足够大这是后面的重点坑。在实际项目里我一般会同时保留 printf 和 sprintf启动阶段用 printf 做简单透传进入正式数据处理后用 sprintf 构造状态帧再用一块 DMA 队列发送避免阻塞主循环。这种方式既不浪费 CPU也不会有逐字符中断的噪音。在 e2studio 中sprintf 的实现直接由编译器 C 库提供。由于它不碰硬件所以不存在重定向一说但同样地如果编译器裁剪了 printf 系列函数中的浮点支持sprintf 也会受影响这是接下来实现章节要一并解决的事。用一张表可以看得更清楚对比点 printf sprintf 设备依赖 需要底层字符输出函数实现 只需要一块内存缓冲区 典型调用链路 printf - fputc/fputs - UART PHP sprintf直接写入字符数组 中断安全 阻塞在发送等待中不适合中断 格式化阶段无硬阻塞但发送环节要处理 性能开销 逐字符开销大尤其长字符串 一次性生成整串可配合DMA从这张表就能明白为什么很多工业级代码喜欢把 sprintf 当作日志骨架而不是依赖 printf。printf 适合初学者和简单验证工程化调试则需要更可控的字符串组装方式。3. 在e2studio里把printf接到串口FSP配置、重定向和实测3.1 FSP里给UART配一条可用通道把 printf 接到串口的第一步不是写代码而是先在 e2studio 的 FSP 配置窗口里布好一条 UART 通道。以 RA 系列 MCU 为例几乎是通用流程新建工程时选择好芯片型号后在项目树的“Stacks”页签下为某个外设单元比如系统里的 SCI0添加 r_sci_uart 驱动。这里要注意不同型号的引脚映射可能不同但 FSP 会弹出引脚冲突提示你只需要指定发送引脚与控制器的连接。我常用的配置表如下配置项典型值说明Baud Rate115200与串口助手一致Data Bits8常规设置ParityNone8-N-1模式Stop Bits1常规设置Interrupt Callbackuser_uart_callback回调函数名后面代码对齐Transmit Interrupt Priority选择优先级建议比主循环低的优先级设置完成后点击 Generate Project ContentFSP 会自动生成 g_uart0_ctrl 和相关的 API 头文件。这里强调一遍回调函数名不建议用默认的最好自定义成 user_uart_callback 这种一眼能认出的命名省得在代码里来回搜索。生成之后工程会自动包含 hal_data.h你可以在任意源文件里引用 g_uart0_ctrl 来调用 UART API。如果你买的开发板上有串口转 USB 芯片记得把 TX、RX 接对通常板上会印有字样。我记得第一次搭这些电路时就把 RX 和 TX 接反了结果 printf 发出来的东西全送到了对方嘴里串口助手当然什么都收不到。3.2 重定向fputc/fputs到R_SCI_UART_Write完成了 FSP 配置接下来才是核心的代码重定向。在工程中新建一个文件例如 debug_uart.c或者直接放在 hal_entry.c 里都可以。需要包含的头文件至少有 hal_data.h、stdio.h、stdbool.h。然后按照上一节的回调接口实现。为了避免每次发送都等待单个字符的传输完成而拖慢系统我习惯同时实现 fputs让 printf 在发送字符串时走整包发送。下面是我常用的全套重定向代码/* debug_uart.c */ #include hal_data.h #include stdio.h #include stdbool.h #include string.h static volatile bool uart_tx_busy false; void user_uart_callback(uart_callback_args_t *p_args) { if (p_args-event UART_EVENT_TX_DATA_EMPTY) { uart_tx_busy false; } } static int uart_send_byte(uint8_t c) { uart_tx_busy true; R_SCI_UART_Write(g_uart0_ctrl, c, 1); while (uart_tx_busy) ; return (int)c; } int fputc(int ch, FILE *f) { (void)f; return uart_send_byte((uint8_t)ch); } int fputs(const char *restrict ptr, FILE *restrict f) { size_t len strlen(ptr); (void)f; if (len 0) { uart_tx_busy true; R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)ptr, (uint16_t)len); while (uart_tx_busy) ; } return (int)len; }这段代码背后的逻辑很简单fputc 把单个字符包装成一次异步写回调把 busy 标志清零后等待循环退出。fputs 则一次性写入整个字符串避免每个字符都开启一次发送事务。UART 发送长度参数是 uint16_t因此字符串超过 65535 字节时不建议走这个函数一般日志场景不会遇到。需要留意的是fputs 与 fputc 同时存在时printf 会优先选择 fputs 处理字符串常量部分而格式化生成的非字符串部分比如数字转出来的字符仍走 fputc。因此两个函数必须都实现才能真正做到高效稳定。只实现 fputc 也能工作但输出长字符串时耗时明显。如果你在用某个没有回调版本的老驱动也可以用轮询式发送 API但瑞萨 FSP 的 r_sci_uart 层几乎都是异步风格不建议混用。若你的代码里出现多次调用造成的“上次发送未完成”把回调事件再加一个判断即可。另外回调函数名必须与 FSP 配置中填写的完全一致否则链接器找不到符号程序会直接执行 HardFault。3.3 实测printf和sprintf能不能同时用现在我们来写一个简单的验证函数放在 hal_entry.c 或者 main 函数里开机后周期打印一些信息同时用 sprintf 构造一串报文。这里展示的是一个最常见的组合用法#include stdio.h #include string.h #include hal_data.h void demo_debug(void) { static int cnt 0; char line[80]; /* 用 sprintf 先组一包 */ sprintf(line, cnt%d adc%04x temp%.2f\n, cnt, 0x3FF, 26.78f); printf(%s, line); /* 也可以直接 printf */ printf(running, cnt%d\n, cnt); }这段代码运行后串口助手会交替看到两行日志。sprintf 行不会受 printf 重定向影响因为它根本不走 fputc而 printf 行会经过 fputc/fputs 输出到同一 UART 通道。由于都在同一个串口上如果两个函数同时输出会有互相穿插的问题但这里的调用是顺序的不会出现竞争。有些场景里你想保留 printf 的“标准终端感”又想避免逐字符发送带来巨大延时可以在 fputs 里把字符串先存到一个环形缓冲区再由 DMA 或发送中断慢慢送。不过这意味着要额外考虑缓冲区的分配和backpressure对于调试阶段我更喜欢直接同步等待简单可靠。实测时可以先用 115200 波特率、8N1 打开串口助手。如果看到首字母缺失通常是因为先调用了 fprintf 或 printf 后的第一个字符被上次发送的残留状态卡住解决办法是在初始化后调用一次 uart_send_byte(0x00) 热热底子或者把 R_SCI_UART_Open 之后的发送等待循环逻辑再确认一次。还要注意 e2studio 生成的默认工程里main 函数前的 SystemInit 和 SystemCoreClockUpdate 会配置好时钟如果 UART 外设时钟没有打开R_SCI_UART_Write 永远不会触发回调现象就是 printf 毫无输出。4. 常见问题与排查乱码、卡死、%f不输出的五个场景4.1 串口全是乱码波特率对不上差一个字都翻车现象程序能跑printf 也确实执行了但串口助手里看到的是一串看不懂的字符有时连 ASCII 码都错位。原因99% 是两端波特率不一致。FSP 里的 Baud Rate 写的是 115200但串口助手里选成了 9600或者使用板载 USB 转串口芯片时驱动时钟与配置的时钟不一致。也有少部分情况是字符宽度不匹配比如 MCU 设置了 7 个数据位而串口助手是 8N1。解决第一步在 FSP 的 UART 配置页里看计算出来的“Actual Baud Rate”值与目标差异。第二步确保串口助手也选择同一数值并且确认没有流量控制。经验上9600 和 115200 最容易通过界面肉眼确认如果数据量大导致高速采样丢字节就降到 38400 或 57600。调试阶段优先级是能看懂数据不是追求速率。另外别忘记检查板载 USB 转串口芯片是否在设备管理器里被分配成了不同的 COM 口这种错误我至少犯过三次。4.2 printf执行完就死机底层发送没有等待完成现象第一次 printf 能出来第二次进去 while (!uart_tx_done) 后程序一直停住看门狗被饿死。原因这个循环在等回调置标志但回调函数没有触发。常见的有两种一是回调函数名写错FSP 里的 Callback 名称和 C 代码里的函数名不一致那么回调永远进不来另一个是发送缓冲区在 main 里被变量重新赋值或者回调事件不是 TX_DATA_EMPTY。解决先在回调里把能触发的事件全部打上断点看看是哪一条或者直接把回调里的事件判断条件去掉改成任何事件都置位 uart_tx_done验证是不是事件类型错。若确实没问题再检查是否每次都切换了发送长度大于 1 时 FSP 的内部状态。我的习惯是所有发送前强制 uart_tx_done false发送后等待置位避免残余状态干扰。还有一个隐蔽原因UART 驱动在发送前会检查底层 TX 是否为 busy如果你在开头没调用 R_SCI_UART_Open那么发送 API 会直接返回错误标志位自然永远不变。4.3 %f打出来是空或0.00浮点数格式串成了黑匣子现象sprintf 里用了 %.2f输出要么是 0.00要么什么都不出来但 %d %x 都正常。原因编译器提供的 printf 系列函数在默认配置下可能不启用浮点格式化支持。e2studio 工具链默认的 C 库可能为了节省体积把 float 相关的解析裁剪掉了另一个原因是优化等级对寄存器传递变量产生影响但我遇到最多的是前者。解决直接在 e2studio 的链接器配置或启动文件中打开 printf 的浮点支持选项。不同工具链的开关不一样但常见的是在“Properties - C/C Build - Settings - Linker”里增加一个 -u _printf_float 之类参数。如果你不想动链接器可以绕开 printf 的浮点格式化自己把浮点拆成整数和小数两个部分sprintf(buf, %d.%02d, (int)f, (int)(fabs(f*100))%100)。这样哪怕不支持 %f 也能打出两位小数。注意这种绕法会失去四舍五入的精度但调试够用。浮点支持打开后固件体积会增加几 KB这是正常代价。4.4 sprintf缓冲区溢出一串长日志把你内存踩了现象程序跑着跑着某些局部变量被改成奇怪的值或者在释放内存时崩溃。原因sprintf 不检查缓冲区大小。如果格式化的输出长度超过分配的数组长度它会把后续字符写到相邻内存里覆盖掉其他变量。尤其是在格式化数组、浮点数、十六进制长串时长度很容易估错。解决凡是长度可能变化的格式化输出一律改用 snprintfchar log[64]; snprintf(log, sizeof(log), %s %d, adc_value, 512);snprintf 会限制最大写入数并保证 \0 结尾。返回值是“如果空间足够会写入的字符数”当返回值 sizeof(log) 时说明日志要被截断我们可以补一条提示。这种做法在 MCU 上代价不大却能在早期就暴露日志超长的问题。调试阶段宁可用大一点的数组去换安全性也不要让 sprintf 把栈踩了。4.5 printf被优化掉代码存在但串口没有反应现象在调试器里看 printf 确实输出了字符串寄存器波形也显示 TX 引脚没有电平平移。尤其开了 -O2 优化后printf 函数体会直接内联但由于重定向的 fputc 没有被标记为可被调用编译器把整个 printf 判断为“无副作用”直接删掉了。原因C 库的 printf 本身不会被优化器认清如果 fputc 是用户自定义函数编译器可能认为它不产生可观测的输出从而在链接时把它当作无用代码裁剪。解决给 fputc/fputs 加上 volatile 机制确保每次调用都是外部可观察的。或者在工程里使用一个固定的全局计数器在 fputc 里对计数器自增让编译器无法移除调用。更简单的方法是在调试阶段关闭优化或者修改链接器脚本不让垃圾回收裁掉 fputc 符号。我一般会关闭优化来跑 printf 测试等确认日志没问题再开优化但会保留一个 log 开关来临时禁掉格式化。5. 进阶用法给日志模块加开关、加缓冲、选通道5.1 用宏把printf整体关掉上线不留尾巴在调试阶段 printf 满天飞很爽但发布固件时这些输出会占用串口、拖慢流程、还可能泄露信息。我一般会定义一组日志宏编译时直接掐掉低等级输出用可变参数宏把日志封装起来#define DEBUG_LEVEL_INFO 1 #define DEBUG_LEVEL_WARN 2 #define DEBUG_LEVEL_ERROR 3 #ifndef DEBUG_LEVEL #define DEBUG_LEVEL DEBUG_LEVEL_INFO #endif #define LOG(level, fmt, ...) \ do { \ if ((level) DEBUG_LEVEL) { \ char buf[128]; \ snprintf(buf, sizeof(buf), fmt, ##__VA_ARGS__); \ uart_send_str(buf, strlen(buf)); \ } \ } while(0) #define LOG_INFO(...) LOG(DEBUG_LEVEL_INFO, __VA_ARGS__) #define LOG_WARN(...) LOG(DEBUG_LEVEL_WARN, __VA_ARGS__) #define LOG_ERROR(...) LOG(DEBUG_LEVEL_ERROR, __VA_ARGS__)这里的##__VA_ARGS__语法是 GNU 扩展也适用于很多 ARM 编译器。它的作用是当没有变参时去掉前面多余的逗号。这样调用LOG_INFO(hello)不会编译失败。注意每个函数都使用了 snprintf比直接用 printf 多了一次字符串复制但换来了长度检查我认为是值得的。如果要彻底让 printf 不编译可以在宏里把等级判断写成预处理器条件#if (DEBUG_LEVEL DEBUG_LEVEL_ERROR) #define LOG_ERROR(...) LOG(DEBUG_LEVEL_ERROR, __VA_ARGS__) #else #define LOG_ERROR(...) ((void)0) #endif上线时把 DEBUG_LEVEL 设为 ERROR 或 -1日志宏就全部消失不会影响正常运行。这样还能隐藏业务细节避免被串口分析工具抓取通信协议。5.2 不用串口时改用RTT典型场景对比某些硬件上没有引出 UART或者串口被业务占用你要输出调试信息时可以改用 J-Link RTT。RTT 不需要占用额外引脚而是在 RAM 里开辟一块缓冲调试器通过 SWD 接口实时读取配合 e2studio 的 RTT Viewer 能看得非常舒适。下表对比两种方式的取舍关键点UART 输出RTT 输出硬件需求需要MCU引脚外设需要调试器SWD口无需占用串口引脚实时性由波特率和驱动决定存在发送阻塞时间由调试器轮询策略决定RAM开销可控制代码复杂度需要重定向fputc到UART需要初始化RTT控制块再重定向到RTT写函数环境依赖任何串口助手都可以必须安装J-Link驱动/RTT Viewer如果只是做性能测量RTT 比 UART 更有优势因为缓冲区写入几乎是 RAM 操作速度远高于串口移位。不过用 RTT 也要注意如果调试器卡死或断开RTT 缓冲区会一直增长需要设置合理容量并处理好覆盖策略。说实话日常调试我多数时候还是会接一根 TTL 转 USB毕竟串口助手可存档可回放离线分析更容易。5.3 在中断和RTOS里使用printf的注意事项很多开发者会在定时器中断或 UART 接收中断里加 printf这是比较危险的。printf 内部的格式化和逐字符阻塞发送会占用几十微秒到毫秒级的时间在中断上下文里这就是个“定时炸弹”轻则影响时序重则触发 NMI 或硬件错误。正确做法是中断里只把需要打印的数据塞进一个环形缓冲区回到主循环后统一处理或者使用 sprintf 生成字符串然后立刻发送但要把发送函数改成中断驱动不要在中断里等发送完成。我见过一个项目把 LOG_ERROR 放在串口接收中断里波特率 115200 时每个字符中断一次结果主循环根本没机会跑。后来把日志移到空闲任务里用队列传递问题才消失。这条是我最想让你记住的printf 不能直接进中断sprintf 可以进中断但它后面的发送函数必须非常快最好只往环形缓冲区写。在 RTOS 环境下多个任务同时调用 printf 会互相穿插。解决办法是给整个日志输出过程加一个互斥锁或者在宏内部用关中断做临界区保护。但关中断会屏蔽系统节拍所以更稳妥的是用一个独立的日志任务其他任务把日志请求放进队列。这样格式化、发送都在同一个任务上下文里执行不会出现字符交错。6. 一个调试习惯先sprintf构造完整字符串再一次性发送先说结论我现在的调试代码里已经基本不直接调 printf 了要么用宏要么手动 sprintf发送。因为 printf 带来的逐字符发送在长日志和高速打印时确实会拖慢程序尤其当你真的把某个变量每毫秒打一次整个任务循环就被卡住了。用 sprintf 把一条日志先组好再调用一个“裸发送函数”把整段字符串一次性交给 UART不但能避开 printf 的字符级等待还更容易在发送前做时间戳标记、切割、过滤。具体做法是这样的。在工程里保留一个极简发送接口void uart_send_str(const char *ptr, uint16_t len) { uart_tx_busy true; R_SCI_UART_Write(g_uart0_ctrl, (uint8_t *)ptr, len); while (uart_tx_busy) ; }然后所有日志都通过snprintf进数组再调用它。平时调试最常用的是这个套路char log_buf[96]; uint32_t uptime get_tick_ms(); snprintf(log_buf, sizeof(log_buf), [%07u ms] speed%d km/h, wtmp%.1f\n, uptime, speed, water_temp); uart_send_str(log_buf, (uint16_t)strlen(log_buf));这样有一个额外的好处在那一行里你可以随意调整字段顺序不会影响其他逻辑。比如临时想看某个中间变量直接加一个转换说明符编译后串口立刻能看到。至于会不会阻塞主循环其实和 printf 没本质区别但至少它是整包操作不会出现字符间被其他中断插入而撕成碎片的后果。如果你还想更快可以把 UART 发送改成 DMA 方式先把 log_buf 复制到不可变的缓冲区再触发 DMA 发送发送期间主循环可以干别的事。不过 DMA 会带来缓存同步问题调试阶段不建议先上等日志格式稳定后再考虑也不迟。至少我目前的经验是同步发送足够满足绝大多数调试场景而把队列和 DMA 留给正式通信协议。这段内容最后想分享一个刚踩过的坑我之前把 snprintf 的返回值和 strlen 混用结果在字符串超长时发送长度多了一个字节串口末尾续了 \0 导致后一帧错误。正确的习惯是发送前用 strlen 计算实际长度或者干脆把 snprintf 的返回值取与缓冲区大小的小值但这需要额外处理。后来我统一用“snprintf - strlen - uart_send_str”三步再没出过这类问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询