STM32量产前最后一步:Keil集成bin生成与boot+app合并DFU实战

发布时间:2026/10/30 14:30:52
STM32量产前最后一步:Keil集成bin生成与boot+app合并DFU实战 简介嵌入式固件开发中Bootloader与应用程序分离是IAP/OTA升级的基础而量产阶段往往需要把两者合成一个DFU包进行统一烧录。但Keil MDK默认只输出HEX文件无法直接得到BIN更无法按地址合并多段固件。文章从HEX与BIN的格式差异切入说明为何生产烧录与DFU升级都依赖BIN这种纯二进制流接着讲解STM32 Flash地址布局、扇区对齐原则以及DfuSe文件结构帮助理解固件合并的本质。随后给出在Keil中通过fromelf一键生成BIN的完整配置并提供一个Python合并脚本自动完成地址偏移检查、0xFF填充与合法性校验。最后演示使用DfuFileMgr将Boot和App封装为单个DFU文件并指出VTOR设置、GAP填充、工具兼容性等常见翻车点。适合正在开发STM32 IAP、OTA或产线批量烧录方案的嵌入式工程师让BootApp合并与DFU生成从手动碰运气变成可靠可重复的自动化流程。1. Keil 集成 bin 生成为什么 bootapp 合并成 DFU 是量产前的最后一道坎做 STM32 的 IAP 或 OTA 升级绕不开一个需求bootloader 和 app 要合成一个 DFU 包给产线批量烧录或者远程升级用。Keil 默认只生成 hex 文件不生成 bin更不会把两个工程拼成一个 DFU。很多同事第一次拿到“keil集成bin文件生成_bootapp合并_dfu生成”这类交付要求时第一反应是上网找现成脚本结果要么 fromelf 命令写错路径要么合并时地址对不上要么生成的 .dfu 文件在升级工具里打不开。这篇文章把这套流程完整拆开怎么在 Keil 里集成 bin 生成、怎么把 boot 和 app 按地址合并、怎么产出 DFU 文件以及每一步最常见的翻车点。适合正在做 IAP、OTA 或产线烧录方案的嵌入式工程师照着走能把三个工程串成一条自动化流水线。2. 原理先立住hex 到 bin、boot/app 地址布局与 DfuSe 文件结构2.1 Keil 默认产物是 hex为什么生产烧录和 DFU 升级都盯着 binKeil MDK 里默认勾选了 Create HEX File所以大部分工程师见到的第一个产物是 .hex。hex 文件是 Intel HEX 格式的文本每一行用 ASCII 字符记录地址、数据长度、记录类型和校验和比如:10000000 00 C0B40020 49A00008 ... F7。这种格式的好处是地址信息跟着每行走调试器、烧录器可以按地址逐条写入哪怕程序在 Flash 里分了几段也能正确处理。但它的坏处也很明显数据量比纯二进制大三倍左右而且解析依赖行首的地址不适合做简单的文件拼接和字节流传输。bin 文件是纯二进制从某个起始地址开始连续摆放没有地址字段长度就是数据本身。DFU 升级流程里MCU 侧的 DFU 固件通过 USB 接收的是字节流它自己决定把数据写到 Flash 的哪个地址所以它不认 hex 的文本记录只认 bin 这种“裸数据”。生产烧录也是同理编程器的上位机软件一般会让你填一个起始地址然后直接灌 bin。这就是为什么只要涉及 bootapp 合并、DFU 生成第一步永远是先把 hex 转成 bin。Keil 里生成 bin 不能靠勾选项得用 fromelf 命令行工具。fromelf 读取编译链接后生成的 axf 文件ARM 的 ELF 格式把里面的可加载段提取出来输出成 bin。这里有个容易混淆的点axf 和 .elf 的差别。如果你在工程里用了 GCC 工具链生成的是 .elffromelf 是 ARM 编译器的工具对 .elf 的兼容性很差常见报错是认不出输入文件格式。所以用 Keil 的标准 AC5/AC6 编译器生成的是 .axffromelf 才能正常处理。2.2 boot 和 app 的地址段划分合并前必须搞懂 Flash 布局boot 和 app 合并不是简单把两个文件接在一起而是按它们在 Flash 里的实际地址摆放。STM32 的 Flash 从 0x08000000 开始boot 通常占用起始段比如 0x08000000 到 0x0801000064KBapp 的起始地址必须避开 boot常见是 0x08010000也有按扇区边界设成 0x08020000 甚至 0x08080000 的。这个地址不是随便定的它由 boot 工程和 app 工程各自的分散加载文件sct 文件决定。合并脚本要做的就是把 boot.bin 放到偏移 0 的位置把 app.bin 放到偏移APP_ADDR - BOOT_ADDR的位置。如果 boot 占 64KBapp 起始是 0x08010000那偏移就是 0x10000中间如果没有其他数据就用 0xFF 填充。这里必须提扇区对齐STM32F4 的 Flash 前 4 个扇区是 16KB后面是 64KBSTM32F1 是 1KB/4KB 不等。如果 app 起始地址不是扇区边界擦除时会把 boot 的一部分一并抹掉。所以合并脚本里最好加一个对齐检查偏移值不能被扇区大小整除就直接报错。另外要注意 boot 和 app 之间的 gap。如果 app 放在 0x08040000而 boot 只有 64KB中间空出约 192KB。合并成单个 bin 时这些 gap 会以 0xFF 填充文件体积变大DFU 下载时间也变长。如果产线用全片烧录gap 填充是必须的如果只做 DFU 升级更优做法是让 DFU 文件里 boot 和 app 作为两个独立元素、各自带地址跳过 gap。这一点在第 5 章会详细展开。2.3 DfuSe 文件到底是什么DFU 包里的地址与元素DFU 是 USB Device Firmware Upgrade 协议的简称ST 在此基础上扩展出了 DfuSeDevice Firmware Upgrade ST Extended对应的文件后缀就是 .dfu。DfuSe 文件比 bin 多了一层包装它除了固件数据还记录了每个数据段的目标写入地址。这样 boot 和 app 可以放进同一个 .dfu 文件里DFU 升级工具在烧录时按文件里记录的地地址逐个写入不会乱套。DfuSe 文件的结构可以简单理解为文件头 镜像头 若干个 target 数据区。文件头最前面 8 字节是固定签名我用 Python 打开过很多次实际内容就是00 01 44 46 53 45 08 00其中44 46 53 45是 ASCII 码的 “DFSE”。镜像头里记录 target ID、alternate setting 编号和保留字段。每个 target 带一个目标地址和元素数量元素再记录数据长度和起始地址。末尾一般还有 16 字节的 DFU suffix包含 CRC32 校验值。这里有个实际经验要提醒不同版本的 ST DfuSe 工具对 target 结构的解析有细微差别早期工具对某些字段的容错能力差会导致“工具打不开文件”。所以我个人的做法是生成 DFU 用官方 DfuFileMgr检查文件只检查头部签名和数据长度不自己手工构造 DfuSe 的字节流。手工构造看着很酷但要兼容所有升级工具非常麻烦量产场合不值得赌。3. 在 Keil 里一键产出 binfromelf 命令与合并脚本落地3.1 在 Keil User 页签挂 fromelf一条命令让每次编译都出 bin先把 fromelf 命令跑通。打开 Keil在 Options for Target - User 页签里找到 After Build/Rebuild勾选 Run #1在输入框里填fromelf --bin --output.\Build\app.bin .\Build\app.axf这条命令把 Build 目录下的 app.axf 转成 app.bin。要点是 axf 路径必须正确。工程输出路径可以在 Options for Target - Output 页签的 Select Folder for Objects 里改默认是工程目录下的 .\Objects。如果填错路径build 后 fromelf 会直接报错“cannot open file”。更稳妥的写法是用 Keil 的替换变量把路径写活fromelf --bin --output!L.bin !L!L会被 Keil 自动展开为当前工程 axf 文件的完整路径不含扩展名所以无论工程名怎么改这行都不用动。输出文件会和 axf 在同一个目录文件名也一致。这里有个容易踩的坑fromelf 在 Keil 安装目录下AC5 和 AC6 的路径不同。AC5 编译器对应ARM\ARMCC\bin\fromelf.exeAC6 编译器对应ARM\ARMCLANG\bin\fromelf.exe。Keil 在 User 页签里执行命令时会自动把工具链的 bin 目录加入 PATH所以直接写fromelf一般能执行。如果你在命令行里手动跑 fromelf 却提示找不到命令就是 PATH 里没有对应路径把完整路径写进去即可。3.2 合并 bootappPython 脚本按地址拼接并检查扇区对齐生成 bin 之后合并脚本是整条流水线的核心。我习惯用一个 Python 脚本读入 boot.bin 和 app.bin按照两个工程的分散加载文件地址做拼接同时做几个关键检查。下面是一个可以直接用的版本#!/usr/bin/env python3 # merge_boot_app.py # 用法: # python merge_boot_app.py --boot boot.bin --app app.bin --out combined.bin # --boot-addr 0x08000000 --app-addr 0x08010000 import argparse import os DEFAULT_BOOT_ADDR 0x08000000 DEFAULT_APP_ADDR 0x08010000 # STM32F4 前 4 个扇区为 16KB此处按最小擦除单元检查 FLASH_SECTOR_SIZE 16 * 1024 def load_bin(path): with open(path, rb) as f: return bytearray(f.read()) def merge(boot_bin, app_bin, boot_addr, app_addr, out_path): offset app_addr - boot_addr if offset len(boot_bin): raise ValueError( f地址重叠: app 起始偏移 {offset:#x} 小于 boot 长度 {len(boot_bin):#x} ) if offset % FLASH_SECTOR_SIZE ! 0: raise ValueError( fapp 偏移 {offset:#x} 未按扇区对齐boot 会被擦除 ) total_size offset len(app_bin) image bytearray([0xFF] * total_size) image[0:len(boot_bin)] boot_bin image[offset:offset len(app_bin)] app_bin with open(out_path, wb) as f: f.write(image) print(fboot: {len(boot_bin):#x} bytes {boot_addr:#010x}) print(fapp: {len(app_bin):#x} bytes {app_addr:#010x}) print(fgap: {offset - len(boot_bin):#x} bytes (0xFF)) print(fmerged: {total_size:#x} bytes - {out_path}) def main(): parser argparse.ArgumentParser() parser.add_argument(--boot, requiredTrue) parser.add_argument(--app, requiredTrue) parser.add_argument(--out, requiredTrue) parser.add_argument(--boot-addr, typelambda x: int(x, 0), defaultDEFAULT_BOOT_ADDR) parser.add_argument(--app-addr, typelambda x: int(x, 0), defaultDEFAULT_APP_ADDR) args parser.parse_args() boot load_bin(args.boot) app load_bin(args.app) merge(boot, app, args.boot_addr, args.app_addr, args.out) if __name__ __main__: main()脚本逻辑分为三步。第一步通过offset app_addr - boot_addr计算出 app 相对 flash 起始地址的偏移这就是 app.bin 在合并文件里的摆放位置。第二步检查两个关键条件地址重叠和扇区对齐。地址重叠意味着 app 起始位置还没超过 boot 的长度合并产物会把 boot 尾巴覆盖掉这在物理上就不可能运行扇区对齐检查则是防止 app 起始地址落在某个 Flash 扇区中间导致升级时擦除扇区会误伤 boot。填充值用的是 0xFF对应 Flash 擦除后的默认状态这也是生产烧录全片时 Flash 里应有的值。bytearray 默认填 0x00所以这里先bytearray([0xFF] * total_size)再赋值是一开始写脚本最容易出错的地方。如果这里直接bytearray(total_size)最终合并文件里 gap 会全是 0x00烧录后 Flash 的未用区域内容异常虽然大部分程序跑起来不影响但做完整镜像比对时会不一致。3.3 把合并脚本串进 After Build三个工程一条线脚本单独跑没问题还不行得把它串进 Keil 的编译流程做到每次 build 完 app 自动生成合并固件。常见的做法是boot 工程先编译出 boot.bin然后 app 工程在 After Build 阶段调用合并脚本把新编译的 app.bin 和已有的 boot.bin 合并。这样平时只需要 Rebuild app 工程产物自动更新。在 app 工程的 User 页签After Build 的 Run #1 保留 fromelf 生成 binRun #2 填 Python 调用python D:\workspace\scripts\merge_boot_app.py --boot D:\workspace\boot\Build\boot.bin --app .\Build\app.bin --out .\Build\combined.binboot 路径建议写绝对路径app 路径可以写相对路径因为 Keil 执行命令时的工作目录就是工程文件所在目录。注意 Python 是否在系统 PATH 里如果没加就把python换成 Python 安装的完整路径。另一个细节每次改动 boot 工程后必须先 Rebuild boot 工程再 Rebuild app 工程否则合并用的 boot.bin 是旧版本。我习惯在脚本里加一个时间戳检查如果 boot.bin 的修改时间比 app.bin 晚就说明 boot 刚编译过这是正常的反过来如果 boot.bin 比 app.bin 老太多打印一句警告提示。代码写起来不复杂但能在流水线上省掉很多排查时间。4. 生成 DFU 包DfuFileMgr 操作步骤与产物验证4.1 用 DfuSe 工具把 boot 和 app 装进一个 DFU 文件合并出了 combined.bin 之后DFU 生成我用的是 ST 官方 DfuSe demo 里的 DfuFileMgr 工具。它不是命令行工具但胜在兼容性最好生成的 DfuSe 文件在 ST 的 DfuSeDemo、dfu-util、以及各家量产工具的兼容性都是最高的。操作步骤很固定打开 DfuFileMgr.exe菜单 File - Generate DFU File。在 Device 列表里选择目标芯片系列比如 STM32F407VG或者选 Global 让工具不绑定具体型号。点击 Add 按钮先加入 boot.bin此时地址栏填 0x08000000。再次点击 Add加入 app.bin地址填 0x08010000和前面脚本里 addr 保持一致。保存生成 xxx.dfu。生成过程中工具会把每个文件作为一个元素写进 DfuSe 结构。boot 元素记录起始地址 0x08000000app 元素记录起始地址 0x08010000升级时 DFU 固件会按元素顺序逐个写入。这里要注意一个概念区别DfuSe 里一个 target 可以包含多个元素每个元素自带地址也可以有多个 target每个 target 一个地址。DfuFileMgr 添加多个文件时默认是把它们作为同一个 target 下的多个元素这对 STM32 的 DFU 升级流程是常见且正确的形态。如果你只有一个 bin 文件需要生成 DFU也可以不加第二个文件直接保存。另一种替代路径是用 dfu-util 自带的一些脚本工具从 bin 生成标准 DFU 文件但纯 bin 转出的 DFU 只支持单 target、单元素放不了 bootapp。所以双固件场景DfuFileMgr 是我实际项目中主要采用的方式。4.2 生成 DFU 时要确认的三个参数目标地址、alternate setting 和 CRCDfuFileMgr 界面上看起来只是“添加文件、填地址、保存”但背后有三个参数直接决定 DFU 能不能被目标设备正确识别。第一个是目标地址它必须和 app 工程分散加载文件的 ROM 起始地址一致。曾见过有人把 app 的 DFU 地址填成 0x08000000结果升级时把 boot 覆盖了设备变砖。地址这块没有商量余地必须以工程的分散加载文件为准不能凭记忆填。第二个是 alternate setting。DFU 协议里设备可以暴露多个 alternates每个对应一个存储区域比如 TF-M 方案里 boot 和 app 可能放在不同 alternates。常规双固件方案里alternate 数量是 1boot 和 app 作为两个元素共用这个 alternate。如果你的 boot 工程在 DFU 固件里实现了多个 alternateDfuFileMgr 生成时要相应地把不同文件挂到不同 alternate 下否则下载时目标不匹配。第三个是 CRC。DfuSe 文件末尾的 suffix 带 CRC32 校验DfuFileMgr 在保存时会自动计算。但有些第三方工具或者手动修改过的 DFU 文件不更新 CRC升级工具下载到最后一个包时校验不过表现是“下载完成但校验失败”或者“设备跳出 DFU 模式但程序没写进去”。遇到这种问题别急着怀疑 Flash先用 DfuFileMgr 重新生成一次再看。4.3 校验脚本用 Python 读取 DFU 头确认 segment 信息DfuFileMgr 界面能直接看到每个元素的地址和大小但生产中往往需要脚本化校验尤其是把 DFU 文件交给产线之前。我写了下面这个简单的检查脚本只做文件头签名校验和基本字段打印不解析完整的 DfuSe 结构。它不能替代 DfuFileMgr 的打开检查但能在自动化流程里快速筛掉损坏文件。#!/usr/bin/env python3 # check_dfu.py # 用法: python check_dfu.py xxx.dfu import struct import sys def check_dfu(path): with open(path, rb) as f: data f.read() if len(data) 24: print(文件太小不是有效的 DfuSe 文件) return False # DfuSe 头: 00 01 D F S E 08 00 if data[0:2] ! b\x00\x01 or data[2:6] ! bDFSE: print(文件头签名错误) return False print(f文件大小: {len(data)} bytes) print(DfuSe 签名: OK) # 打印前 32 字节 hex方便人工核对 print(前 32 字节:, .join(f{b:02X} for b in data[:32])) # 从文件尾部读 suffix 长度字段如果有 suffix data[-16:] length_field, bcd_dfu struct.unpack(IH, suffix[12:18]) if length_field 16: print(DFU suffix: 存在 (16 bytes)) print(fbcdDFU: {bcd_dfu:#06x}) else: print(DFU suffix: 未检测到部分升级工具可能拒绝该文件) return True if __name__ __main__: if len(sys.argv) ! 2: print(用法: python check_dfu.py file.dfu) sys.exit(1) ok check_dfu(sys.argv[1]) sys.exit(0 if ok else 1)这段脚本没有深入解析每个 target因为 DfuSe 的多 target 结构在不同工具和版本下字段顺序有差异强行解析反而容易给出错误结论。它的价值在于确认文件头签名和文件大小正常确认 DFU suffix 存在防止把损坏的或者被截断的文件发给产线。产线工具如果打不开 DFU 文件先用这个脚本跑一遍能快速判断是文件本身的问题还是产线工具的配置问题。5. 避坑记录fromelf、VTOR、gap 填充与工具打不开的四个现场5.1 编译通过但烧录后首条指令就跑飞现象fromelf 成功生成了 bin下载到 Flash 后程序不运行硬件调试停在 HardFault或者 PC 指针跑到随机地址。 原因最常见的是 bin 的基地址填错了。fromelf --bin 生成的 bin 是纯数据它的偏移 0 对应分散加载文件里第一个 region 的起始地址。如果 app 工程分散加载文件规定 ROM 起始是 0x08000000而你在烧录工具里把下载地址填成了 0x08010000那么第一条指令栈顶指针会被写到错误位置MCU 上电直接跑飞。 解决明确“哪个工程的 bin 按哪个基地址烧”。boot.bin 基地址是 0x08000000app.bin 基地址是 APP_ADDR合并后的 combined.bin 基地址是 0x08000000。烧录工具里填的地址不是“从哪里读文件”而是“文件第一个字节写到哪”这两个概念经常被混淆。我把这条写进了团队烧录规范任何 bin 交付件必须附带烧录地址说明否则默认按 0x08000000 处理。5.2 app 跳转死机VTOR 没跟着 APP_ADDR 走现象boot 正常启动跳转指令执行后 app 不进 main或者一进中断就 HardFault。 原因Cortex-M3/M4 的中断向量表基地址由 VTOR 寄存器决定app 工程如果没有在启动阶段把 VTOR 设置成 APP_ADDR向量表就还留在 0x08000000。boot 跑在 0x08000000 时没问题跳转到 app 后一旦发生任何中断MCU 去 0x08000000 取中断向量拿到的还是 boot 的向量行为完全错乱。 解决app 工程里加一行SCB-VTOR APP_ADDR;放在 system_init 或 main 最开头必须在任何外设中断使能之前执行。同时注意跳转前关闭全局中断、复位外设状态常见写法是__disable_irq()后跳转app 初始化自己重新开启中断。这类问题合并脚本管不了只能在代码层面解决但每次做 bootapp 方案都应该把“跳转前关中断、app 里重设 VTOR”写成固定检查项。5.3 合并文件比预想大几倍gap 的 0xFF 与 DFU 多元素取舍现象boot 64KBapp 120KB但 combined.bin 生成了 512KB烧录时间明显变长。 原因app 起始地址 0x08040000 和 boot 之间空着约 192KB 的 gap合并脚本为了保持地址连续用 0xFF 把这些空区域都填满了。从 Flash 镜像角度看这没问题但 DFU 升级时会把这 192KB 的空数据也全量传输。 解决分场景处理。产线全片烧录combined.bin 是正确形态烧录器按地址写全片0xFF 区域擦除后本来也是 0xFF。DFU 远程升级就不要用合并 bin 生成 DFU而是直接把 boot.bin 和 app.bin 作为两个元素交给 DfuFileMgr让 DFU 文件里跳过 gap。还有一个折中方案如果 boot 固定不改DFU 里只打包 appboot 通过烧录器单独烧升级流程也只升级 app。具体取舍看产品需求但不要在没确认升级方式前就默认“合并 bin 越大越好”。5.4 DfuSe 工具打不开生成的 .dfu路径与格式细节现象生成的 .dfu 文件在 DfuSeDemo 里打开直接报 not valid DFU file但在 DfuFileMgr 里正常。 原因一是文件路径含中文或空格ST 的部分工具对非 ASCII 路径支持很差这一点在 Windows 下尤其明显二是 DFU 文件本身缺 suffix 或者 CRC 不对可能是别人手工改了文件内容后没重新生成。 解决先把整个工作目录改成纯英文路径这是玄学但成功率极高。其次用 check_dfu.py 看 suffix 是否存在缺失就重新用 DfuFileMgr 生成。最后如果还打不开换一个新版 DfuSe demo 包老版本工具对 DfuSe 新字段的容错差有时会直接把带保留位的文件拒掉。遇到工具兼容性问题记住一个原则以官方工具重新生成为准不要去手工修文件字节。6. 进阶把 CRC、版本号和回读验证塞进自动化流程6.1 合并时顺便算 CRC给固件留一个自检后门boot 跳转 app 之前经常要做完整性校验。常见做法是在 app.bin 尾部预留一小块区域存放 CRC32boot 在跳转前计算整个 app 区域的 CRC 和预留值比对。合并脚本可以顺手做这件事。下面是扩展思路的代码片段import zlib # 在 merge() 返回前调用 crc zlib.crc32(app_bin) 0xFFFFFFFF # 在 app.bin 末尾追加 8 字节: 4 字节 CRC 4 字节长度 app_with_crc app_bin struct.pack(II, crc, len(app_bin)) image[offset:offset len(app_with_crc)] app_with_crc注意 app 工程要在分散加载文件里预留这 8 字节的空间并且 boot 端校验时要按“整个 app 区域大小”来算不能只算到代码末尾。如果 app.bin 本身已经接近 Flash 分区上限加这 8 字节可能溢出所以预留位要从 app 区起始就规划好。这种 CRC 方案对生产烧录和一包到底的升级场景很实用代码侧只需要在 app 链接脚本里加一个空段。6.2 回读验证dfu-util 把烧进去的固件读回来比对生成 DFU 文件的最后一关是真正下载到设备上再回读比对。设备进入 DFU 模式后用 dfu-util 列出设备dfu-util -l确认设备列出后回读到本地文件dfu-util -a 0 -U readback.bin回读文件是从设备 Flash 里按完整区域读出的和原始 combined.bin 直接 cmp 一般对不上因为 DFU 模式回读往往包含整个 Flash 区域而原始文件里 0xFF 部分可能被设备擦除后保留 0xFF也有一部分区域是配置字或选项字节。我会把回读文件用脚本截取 boot 和 app 两个地址区间再和原始 boot.bin、app.bin 做逐字节比较通过才算这个 DFU 文件真的能用于量产。最初几次做这套流水线时我在 fromelf 路径和 gap 填充上各翻过一次车后来把检查点全部写进脚本里编译、合并、校验一条命令走完。这个习惯一直留到现在。DFU 生成这件事工具只是载体真正可靠的是地址一致性和可重复验证的流程。希望这篇能帮你在自己的 bootapp 方案里少踩几个坑。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询