恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

STM32F103 AB双分区OTA升级方案:从Bootloader到Ymodem完整实战

  • 首页
  • 资讯中心
  • /
  • STM32F103 AB双分区OTA升级方案:从Bootloader到Ymodem完整实战

相关资讯

Selenium实战:百度识图上传图片自动化全流程与踩坑记录 2026/9/10 6:25:22
AI课程项目不翻车:从任务拆解到答辩展示的项目管理指南 2026/9/10 6:25:22
Hugo博客搭建完全指南:从域名到自动化部署与性能优化 2026/9/10 6:25:22

最新资讯

狼群算法在无人机对抗推演中的Matlab实现与参数调优
gRPC C++ 依赖接入与构建全指南:Bazel、CMake、pkg-config 三种集成路径详解
skills协议:智能体能力的声明式调度与执行机制
STM32F767驱动LAN8720A实现稳定TCP通信实战指南
Novanta MDI1PRD23B7-EQ CANopen步进电机深度实战指南
Apache Airflow 分区窗口方向控制:`Window.Direction.FORWARD` 与 `BACKWARD` 详解

今日推荐

AI搜索重构内容生态:企业从“流量争夺”转向“答案共建”
AI搜索的信任缺口:企业内容如何在答案时代自证可信
Spring Boot+Vue+Node.js售后服务系统开发实战

本周热门

超人会飞不算本事:系统稳定依赖清晰规则与边界设计
超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论
基于CNN的调制信号识别:MATLAB实现时频图分类实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

STM32F103 AB双分区OTA升级方案:从Bootloader到Ymodem完整实战

发布时间:2026/9/10 6:30:22
STM32F103 AB双分区OTA升级方案:从Bootloader到Ymodem完整实战 STM32F103一颗卖了十几年的Cortex-M3单片机到现在依然是无数产品的中枢。我接触过的不少项目里它还在稳定跑着产线逻辑、通信网关和各类变流器控制。但这两年有个问题越来越绕不开——产品要联网、要迭代、要能远程修复bug。尤其当你需要给一批已经铺到现场的F103设备升级固件时拿着J-Link挨个刷的办法根本就行不通。所以我决定把这套基于STM32F103的AB双分区OTA方案完整复现出来从零开始不跳步。这套方案不依赖昂贵芯片不用外挂Flash当然要挂也能挂只用STM32F103自带的Flash和串口就能跑通。AB分区的核心价值就是安全升级失败还能回滚永远有一个能跑的固件在兜底。本文适合正在做产品化、需要给设备加空中升级能力的嵌入式工程师也适合刚接触Bootloader、想搞清楚AB分区和OTA完整链路的学生。我会从方案怎么选、Flash怎么分一直到Bootloader怎么写、App怎么改、上位机怎么传一条线拉通讲完最后再分享我复现过程中踩过的坑。1. 整体方案设计为什么F103也能做AB分区OTA1.1 先搞清楚AB分区到底解决什么问题很多开发者第一次接触OTA第一反应是“我只要一个Bootloader把新固件下载到某个地址然后跳过去不就行了”。这种做法在小型产品里确实常见但它有一个致命的隐患如果在下载过程中断电、传输中断、或者新固件本身就有bug设备就变砖了。而AB分区也叫A/B Slot核心思想是把Flash划出两个独立的固件区——A区和B区。当前运行的是A区升级时就把新固件完整写入B区写完校验通过后下一次启动由Bootloader切换到B区。如果B区跑不起来Bootloader自动回退到A区。这个机制在汽车电子和Android系统里已经很成熟但很多嵌入式开发者一听到AB分区第一反应是“Flash不够用”。这种顾虑有道理但对STM32F103来说其实取决于你选哪个型号。比如STM32F103C8T6是64KB Flash做AB分区确实紧张但STM32F103RCT6有256KBSTM32F103ZET6有512KB做AB分区绰绰有余。我的复现项目选用的是STM32F103RCT6256KB Flash把整个方案跑通同时也会给出小容量芯片怎么选的建议。1.2 为什么选串口/Ymodem作为传输方式F103没有以太网控制器也没有USB High-Speed最常用的通信外设是USART、CAN、SPI和I2C。我在这个项目里选择USART加Ymodem协议作为OTA传输通道原因有三个。第一几乎每个F103产品都会引出至少一路串口不少产品本身就带RS232或RS485接口复用一路作为升级通道零成本。第二Ymodem协议有CRC校验、有批传输、有文件结束标志实现复杂度适中和F103的64KB到512KB Flash搭配非常合适。第三调试方便——用电脑上的SecureCRT、XShell、或者自己写的PyQt上位机都能直接发文件不需要额外硬件。当然如果未来产品升级同一套Bootloader思路也能平滑迁移到CAN FD、以太网甚至无线模块比如LoRa、4G模组上。我在设计通信层时做了抽象上层只管收数据、写Flash底层是串口还是其他外设只影响这一层的实现而已。1.3 双Bank、自适应擦写与断电保护STM32F103的Flash有扇区Sector概念但不同型号扇区大小不一样。F103RCT6的256KB Flash从0x08000000开始前4页每页1KB然后是1页4KB之后是1页16KB再往后每页64KB。这个结构对AB分区划分影响很大因为擦除操作必须整页擦你不可能只擦一个扇区里的某几个字节。设计分区表时必须先把Flash的物理页边界摸清楚再决定Bootloader、A区、B区的起始地址和大小否则擦写逻辑会很别扭。断电保护也是这个方案的重点。如果新固件写到一半掉电B区残留一个不完整固件下一次启动时Bootloader的CRC校验会失败直接回退到A区。A区没动过设备还能正常运行等下次有机会再重新升级。这种机制在实践中非常有用——用过单分区方案的朋友应该深有体会升级过程中一旦断电设备就彻底没反应了只能开壳、接J-Link、救砖。关于是否需要均衡磨损也就是AB区轮流写入的磨损控制我在这个项目里没有做。原因是F103的Flash擦写寿命标称是1万次对于大多数设备来说一年升级几次到几十次足够用很多年。但如果你的产品升级非常频繁比如每周一次那建议考虑动态切换起始分区、写坏块标记这是后话本文不做展开。2. Flash分区设计与Bootloader核心实现2.1 我的分区表256KB Flash这么分最省心我做嵌入式有一个习惯设计任何系统前先把资源地图画出来。F103RCT6的256KB Flash地址从0x08000000到0x0803FFFF。经过反复权衡我最终使用的分区方案如下分区起始地址大小内容Bootloader0x0800000032KB启动、校验、升级逻辑App_A0x0800800096KB当前稳定运行固件App_B0x0802000096KB备用固件/新固件接收区参数区0x080380008KB启动标志、升级计数器、版本信息预留0x0803A00024KB用户日志、出厂校准数据等提示分区时必须保证每个分区起始地址对齐到Flash的页边界。F103RCT6的Flash从0x08000000开始前16KB是4个1KB页加1个4KB页加1个16KB页再往后每页64KB。所以0x08008000正好落在64KB页的起始处这是合法的页对齐地址。对齐的意义在于擦除时不会误伤其他分区的数据。App区选择96KB是基于我的实际项目需求——一个带FreeRTOS、FatFS、Modbus协议栈、LCD驱动的固件编译出来大约70KB留出约25%余量。如果你的固件更大可以适当缩小Bootloader到24KB或者压缩参数区。如果是C8T6的64KB Flash最多只能给A区20KB、B区20KB、Bootloader16KB这要求固件必须精简有的项目甚至得去掉RTOS才能塞下。2.2 Bootloader要干的三件事校验、跳转、升级Bootloader的功能可以拆成三个核心环节启动检查、固件跳转、升级接收。启动检查的逻辑是上电后先读参数区的启动标志如果标志位表示“新固件已就绪”则对新固件区执行CRC32校验校验通过更新标志位跳转校验失败清除标志位跳转到另一个分区。如果标志位是“无升级请求”直接跳转到默认运行分区。为了可靠启动标志我使用了双字冗余——同一个标志写两次在参数区读取时两次一致才有效防止Flash写入过程中的bit翻转。固件跳转的核心代码不长但有一个关键点非常容易踩坑。跳转前必须把全局中断关掉把SysTick停在当前状态然后把MSP主堆栈指针设置为App分区首地址的前4字节内容再把PC指针设置为App分区首地址的后4字节内容。F103的App固件中断向量表默认是在0x08000000但你App烧在0x08008000必须把SCB-VTOR设置为0x08008000否则中断向量表对不上一进中断就HardFault。跳转代码参考如下typedef void (*pFunction)(void); void jump_to_app(uint32_t app_addr) { uint32_t app_sp *(volatile uint32_t *)app_addr; uint32_t app_pc *(volatile uint32_t *)(app_addr 4); pFunction app_entry; __disable_irq(); SysTick-CTRL 0; SysTick-LOAD 0; SysTick-VAL 0; SCB-VTOR app_addr; app_entry (pFunction)app_pc; __set_MSP(app_sp); app_entry(); }提示跳转前不仅要去初始化外设。我在实际调试中发现如果Bootloader里初始化过DMA、串口或定时器跳转前必须将这些外设复位否则App启动时会遇到外设状态异常。最省事的做法是跳转前调用RCC-AHBRSTR、RCC-APB1RSTR、RCC-APB2RSTR把涉及到的外设全部复位一遍。2.3 固件校验CRC32一定要算对固件校验我选择了CRC32。STM32F103内部自带一个CRC外设硬件算起来很快但它固定使用多项式0x04C11DB7初值为0xFFFFFFFF输出不取反。如果你上位机用标准CRC32比如zlib的crc32算出来的结果和硬件CRC外设不一致直接校验失败。这个问题我调试了一下午才找到原因后来干脆不用硬件CRC改成在Bootloader里软件实现标准CRC32和上位机保持一致。这样也方便以后换芯片平台代码直接复制。软件CRC32实现网上很多我使用的是查表法256项表速度很快。对一个96KB的固件区做全量CRC校验在72MHz主频下大约耗时200ms到300ms完全可接受。校验时机放在启动时做一次全量校验而不是只校验几个标志字这能确保固件区数据的完整性——有时候Flash内容会因为写Flash时的电压不稳发生个别字节错误只有全量校验才能发现。升级流程放在Bootloader里做接收一包数据、写入Flash、继续接收直到收完整个文件做一次最终CRC校验。注意F103擦写Flash期间CPU会暂停所以串口接收需要用中断或DMA把数据先缓存到RAM。我在实现时开了双缓冲一个缓冲在写Flash另一个缓冲在接收串口数据这样整个升级过程可以做到无缝衔接。缓冲区大小设定为2KB和Ymodem的128字节包或者1KB包都能对齐。3. App端改造与固件生成链接脚本是第一道关3.1 修改Linker Script让App跑在0x08008000很多第一次做OTA的朋友写好了Bootloader跳转也正常但App一跑就HardFault问题多半出在链接脚本没有改。你在Keil里新建工程时默认的Linker脚本把Flash起始地址设为0x08000000大小设为256KB。如果App烧在0x08008000却仍然按0x08000000链接中断向量表和代码里的绝对地址引用全部错位当然跑不起来。以Keil MDK为例修改方法有两种。我推荐直接改分散加载文件.sct在Options for Target - Linker - Edit里把加载域和运行域的起始地址改成0x08008000大小改成0x0001800096KB。改完之后重新编译用生成的.axf或.hex文件转成.bin就能看到bin文件大小能反映App实际占用空间。如果你用的是IAR或者GCC思路一样——改链接脚本的FLASH起始地址和长度。改完链接脚本后App里的中断向量表也要搬。STM32F103在上电时会从0x08000000读取初始SP和PC但如果你是Bootloader跳转到AppBootloader已经运行过了中断向量表还在0x08000000不重映射的话App里的任何中断串口、定时器、GPIO外部中断都会跑飞。解决方法是App启动早期调用SCB-VTOR 0x08008000;注意SCB-VTOR是Cortex-M3内核的标准寄存器F103上可以直接用。但有些从STM32F1移植到F0或L0系列的朋友会发现没有这个寄存器那是Cortex-M0的差异需要特殊处理。本文基于F103直接用VTOR即可。3.2 App要通知Bootloader“我活着”AB分区比单分区多了一个关键机制运行确认。Bootloader跳转到App后App需要在一定时间内比如10秒往参数区写入一个“运行正常”标志。如果App因为自身bug启动失败、卡死在初始化阶段Bootloader在下一次启动时会发现这个分区没有被确认过就会自动回退到另一个分区。这个机制的实现要点是参数区专门分出一个字节作为运行确认位。Bootloader跳转前把确认位清零App启动后完成关键初始化再置位确认位Bootloader下次上电时检查确认位如果当前运行分区没有确认就切换启动另一个分区。为了保险我把确认位也做了双写冗余防止单字节翻转导致误判。有一个细节需要注意App启动到确认位置位之间的时间不能太长否则Bootloader会以为App没跑起来。一般放在系统时钟初始化完成、Flash读写校验正常、关键外设初始化完成之后就可以置位了。如果RTOS或其他慢速外设初始化放在后面不影响这个确认的时效性。3.3 生成bin文件并管理版本号Keil MDK生成.hex文件很方便但Bootloader里做OTA最好用.bin文件因为它没有地址信息纯粹是数据流便于按偏移写入。Keil里在After Build的User页签添加一条命令fromelf --bin --output .\Output\app.bin .\Output\app.axf构建后自动生成app.bin。上位机读取这个bin加上一个自定义的文件头魔数、版本号、固件大小、CRC32再按Ymodem协议发送。文件头设计如下字段大小说明魔数4字节0xAA55A55A用于识别合法固件包版本号2字节主版本/次版本Bootloader可据此判断是否需要升级固件大小4字节有效固件字节数CRC324字节全固件数据的CRC32校验值固件数据N字节实际固件内容App固件里维护一个全局版本号变量或者是编译时自动生成的git commit号。Bootloader在升级前读取包头版本号和当前运行分区记录里的版本号比较同版本不发。这样能避免用户反复升级同一个固件浪费Flash擦写寿命。4. 上位机与Ymodem通信从0到1写一个能用的下载工具4.1 Ymodem协议要点Ymodem协议是从Xmodem扩展来的支持文件名传输和批处理。它的基本传输流程是发送方先发一个128字节的块块0包含文件名和文件大小接收方收到后回复ACK然后发送方按128字节或1024字节的块依次发送数据文件结束后发送EOT接收方回复ACK后再发送一个全0块表示结束。我选择在Bootloader里实现Ymodem接收端因为Ymodem的CRC校验足够强大而且有成熟的协议流程。实现过程不算复杂但几个细节必须处理对块号是1到255循环的块号0用于文件头每个块用CRC16校验多项式0x1021初值0x0000这在Ymodem协议里叫CRC16-CCITT接收方收到NACK表示要求重发收到ACK表示正确接收。4.2 一个基于Python的上位机实现我在项目里用Python写了一个简易版OTA上位机核心功能就是通过串口发送bin文件到设备。PyQt版本的可视化界面留到以后分享这里先给一个命令行版本方便大家跑通流程。依赖只有pyserial和tqdm安装没什么坑。import serial import time import struct import zlib from tqdm import tqdm def crc16_ccitt(data): crc 0x0000 for byte in data: crc ^ byte 8 for _ in range(8): crc ((crc 1) ^ 0x1021) 0xFFFF if crc 0x8000 else (crc 1) 0xFFFF return crc def build_frame(seq, payload): frame b\x01 bytes([seq 0xFF, (256 - seq) 0xFF]) payload frame struct.pack(H, crc16_ccitt(payload)) return frame def send_ota(port, baudrate, bin_path): with open(bin_path, rb) as f: fw_data f.read() header b\xAA\x55\xA5\x5A struct.pack(H, 0x0100) \ struct.pack(I, len(fw_data)) struct.pack(I, zlib.crc32(fw_data)) total_data header fw_data ser serial.Serial(port, baudrate, timeout1) time.sleep(0.2) # 发送C握手字符等待Bootloader应答 ser.write(bC) time.sleep(0.2) resp ser.read(1) if resp ! bC: print(bootloader not ready) return # 发送块0文件名和大小 filename bin_path.split(/)[-1].encode() zero_payload filename b\x00 str(len(total_data)).encode() zero_payload b\x00 * (128 - len(zero_payload)) ser.write(build_frame(0, zero_payload)) resp ser.read(2) if resp[:1] ! b\x06: print(header ack failed) return ser.write(b\x06) # 再ACK time.sleep(0.1) # 按1024字节发送数据块 seq 1 for offset in tqdm(range(0, len(total_data), 1024), descOTA): chunk total_data[offset:offset 1024] if len(chunk) 1024: chunk b\x00 * (1024 - len(chunk)) ser.write(build_frame(seq, chunk)) resp ser.read(1) if resp b\x15: ser.write(build_frame(seq, chunk)) # 重发 resp ser.read(1) seq 1 # 发送EOT结束 ser.write(b\x04) time.sleep(0.2) ser.write(b\x04) time.sleep(0.2) ser.close() print(OTA done)这个脚本对Windows和Linux都适用唯一要注意的是串口号写法Windows是COM3Linux是/dev/ttyUSB0。实际传一个70KB的固件在115200波特率下大约耗时15秒到20秒比J-Link烧录慢不少但胜在不需要打开外壳。4.3 Bootloader端Ymodem接收的注意点Bootloader端实现Ymodem接收时我遇到过最麻烦的一个问题是握手。Ymodem协议规定接收方要先发一个字符‘C’0x43表示具备CRC校验能力。但有些上位机工具比如SecureCRT发送Ymodem前会先等接收方发‘C’。我调试时抓串口数据才发现双方都在等对方先说话造成假死。解决方法是Bootloader端上电后不管有没有升级请求先主动延时100ms再发一个‘C’如果没有收到数据再发一个‘C’连续发三次后进入超时跳转逻辑。这样上位机就不会蒙圈。另外串口中断接收时每收到一个字节后要判断帧边界Ymodem帧头是SOH0x01表示128字节块STX0x02表示1024字节块根据帧头类型决定后续接收长度不能固定按1024字节接收否则会把SOH后的控制字节当作数据。5. 三套方案对比AB分区、回滚指针、双Bank5.1 为什么最终选了AB而不是回滚指针在嵌入式OTA设计里除了AB分区还有一种常见方案回滚指针。这个方案的核心是只保留一个App区升级时把旧固件备份到一个独立备份区升级失败时用备份区恢复。听起来也不错但它有两个痛点一是备份区的Flash开销和AB分区几乎一样并没有省Flash二是恢复到备份区的动作需要较长时间设备在恢复期间无法对外服务如果这时候断电系统可能处于中间状态变砖风险更高。AB分区切换只需要Bootloader修改几个标志字节几乎瞬时完成安全性更好。做产品不能只看“能不能升级”还要看“升级失败怎么办”。AB分区在Flash利用率上略低但换来了“永远有一个可启动系统”的确定性。这个确定性对产线、对用户、对售后都很重要——不用开壳不用返厂远程就能自救。5.2 STM32F103有没有硬件双Bank有些新系列MCU比如STM32F7、H7支持硬件双Bank可以一边跑程序一边擦写另一个Bank。但STM32F103不支持它只有单一Flash阵列。所以在F103上做AB分区必须要软件分区并且在升级App_B时CPU需要从App_A执行因为Flash在写期间无法同时取指。如果你把Bootloader也放到App_A前面那么升级App_B时Bootloader可以先执行升级流程App_A只是待命。但如果Bootloader的空间已经很小升级逻辑比较复杂也可以把升级逻辑放到App_A的一部分区域这叫“A升级B、B升级A”的交互模式。我目前的方案是升级逻辑全部放在Bootloader里App只管运行和运行确认结构更清爽也方便长期维护。5.3 小容量F103怎么办裁剪策略如果你的项目用的是STM32F103C8T664KB Flash做AB分区确实只能算“硬扛”但也不是完全不行。最极端的分配方式是Bootloader 16KB、A区20KB、B区20KB、参数区4KB、剩4KB备用。这样你的App固件必须控制在20KB以内意味着不能跑太重的RTOS驱动也得精简LCD字库要放到外部存储器。我在一个简单的温控器项目上验证过这种裁剪方案固件压缩到18KB左右功能上能接受。如果项目复杂我的建议是优先换芯片比如F103RCT6或者F103RET6成本差异很小但开发体验完全不是一个量级。特别是如果固件用到浮点运算、文件系统、加密算法96KB分区刚刚好64KB会一直处于“删了加、加了删”的尴尬状态。6. 常见问题与排查技巧实录6.1 跳转后立刻HardFault这个坑我栽了三次跳转到App后立刻进入HardFault原因至少有三种栈指针不对、向量表偏移没设置、App在编译时链接地址没改。我栽得最深的一次是前两个都对但App里用了微库MicroLIB初始化时访问了位于0x08000000的默认向量表结果跳飞。排查思路是用调试器在HardFault_Handler里打点查看PC寄存器的值和LR寄存器的值如果PC值是0xFFFFFFxx或0x08000000开头的低地址基本就是向量表或栈指针问题。我在Bootloader的跳转函数里加了一行防御性代码跳转前检查app_sp是否落在0x20000000到0x20010000范围内F103RCT6的RAM范围如果超出直接判定App固件无效不跳转。这个检查成本极低但能挡住80%的错误固件包。6.2 OTA传输到一半失败如何断点续传有一次我在测试时发现升级到50%左右串口通信忽然卡住超时后Bootloader重启设备还是老固件在运行。问题出在Bootloader接收缓冲区溢出。我原来只准备了1KB的接收缓冲但Ymodem一次最大发1024字节加上帧头和CRC校验正好超过1KB导致最后几个字节丢包。后来把缓冲区改成2KB并采用双缓冲切换写Flash的同时继续收下一包问题就解决了。关于断点续传如果传输中途失败Bootloader应该允许上位机从失败点重新发起整个升级流程而不是要求设备必须回到出厂状态。因为AB分区里有另一个分区在正常运行重复升级失败不会影响设备功能。我实现的方式是Bootloader在参数区记录已接收字节数上位机如果检测到中断重新握手后Bootloader读到断点继续从断点接收。这个功能在弱网、易干扰的工业现场非常实用。6.3 启动时CRC校验为什么莫名其妙失败CRC校验失败的典型案例是固件升级完成CRC刚才明明校验通过了但设备重启后Bootloader再校验一遍却失败。排查下来发现是升级完成后、设备重启前App往参数区写了运行确认标志但写Flash时擦除页的操作覆盖了一小段固件区数据。原因就是分区表起始地址和Flash页边界没有对齐擦除范围越界了。所以分区表设计时必须画好页边界图App_B结束地址到参数区起始地址之间要留出至少一个页的空白。不能为了省空间把分区紧密贴合否则擦除参数区时会连带擦掉固件区尾部的数据这样的bug在实验室环境里很难复现但到现场用户手里几乎必炸。6.4 快速排查速查表现象可能原因解决方案跳转后PC值异常App链接地址未改检查.sct分散加载文件进不了串口中断SCB-VTOR没设置或设置值错误App启动早期设置VTORYmodem握手失败双方都在等待对方发CBootloader主动延时后发C连续发3次升级到一半卡死缓冲区太小或单缓冲冲突改用双缓冲Buffer加大到2KB重启后CRC失败分区表未对齐页边界重新检查Flash分区对齐升级后不能回滚运行确认位未实现加运行确认标志与回退逻辑擦写Flash时中断丢失串口中断优先级配置不当提中断优先级擦除前缓存数据7. 扩展方向从串口OTA到无线OTA串口OTA跑通之后这套架构的通信层是可以替换的。我在另一个项目中把串口驱动换成了一颗SX1268 LoRa模块的SPI驱动AIr接口上跑的还是Ymodem帧结构只改了物理层和数据链路层的适配。Bootloader的Flash写入、分区切换、校验逻辑一行没动。这就是分层设计的好处。如果是ESP32之类的Wi-Fi模组OTA的速度和体验会好很多。但STM32F103在很多工业设备里依然占有重要地位——它的稳定性、成熟度、外设丰富程度依然是很多产品经理的首选。给F103加上AB分区OTA能力相当于给这些老设备续上了远程维护的生命线。我个人实际操作中的最深体会是做OTA不只是写代码更重要的是把“升级失败也能活”这套状态机想清楚。AB分区真正的价值不在平时升级成功那一秒而在升级失败、断电、串线、手滑这些意外发生的时候——你还能保住设备。这套方案我从设计到跑通前后大概花了一个周末加三个晚上其中一半时间都在和Flash扇区边界以及CRC变体过不去。如果你照着这份教程做应该会顺利很多。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号