STM32F103+RS485土壤温湿度采集与Modbus-RTU驱动实战
发布时间:2026/11/2 2:50:43 锦皓数字建站

简介面向STM32初学者与嵌入式入门者的RS485通信实践文档围绕STM32F103微控制器的UART接口展开讲解半双工总线中DE数据使能线的收发切换、波特率配置与GPIO控制思路。内容以土壤温湿度传感器为实际对象给出RS485_Init、RS485_Send_Data、RS485_Receive_Data等函数的完整代码并说明主函数中发送读取指令、延时等待、接收响应、解析数据并在TFTLCD屏显示温湿度的流程同时涉及中断优先级设置、接收缓冲区与宏定义等工程细节。压缩包共1个docx文件约1.22MB为图文与代码混合的笔记式整理便于边看边对照移植。已有2629人学习适合想打通STM32串口外设与工业现场总线连接、积累驱动编写与排错经验的读者参考。1. 一块 F103 加一根两芯线这套采集链路到底解决什么问题手头一块 STM32F103ZET6 开发板、一个四线制土壤温湿度探头探头引出 VCC/GND 和 A/B 两根差分线中间夹一颗 485 收发器——这套组合在大棚环境采集、机柜温湿度监测里出镜率极高也是大多数人第一次把 stm32 rs485 的 485通讯方式原理真正跑通的场景。它不涉及点灯和串口打印这种玩具级实验而是把「USART 外设 半双工方向控制 一帧查询报文」三件事焊成一条完整链路MCU 发 8 个字节问探头要数据探头回 9 个字节MCU 拆出温湿度显示到 TFTLCD 上。文档里给出的main.c、rs485.c、rs485.h就是这条链路的最小可跑版本硬件上只需要三根信号线加电源。它适合手里已经有 F103 板子和 485 传感器、卡在「发得出去收不回来」或者「收到了但算出来的数不对」这两类人也适合第一次接触 Modbus-RTU 帧格式、想找个能对照的参考实现的人。2. 从 USART2 到 A/B 差分线硬件层先把电气关系捋顺先讲清楚一个前提STM32 的 USART 外设本身只会说 TTL 电平的单端信号它不知道什么叫 RS485。真正把 0/1 变成 A/B 两根线上的压差是外面那颗收发器干的活。这一步理不顺后面代码写得再漂亮也是空转。2.1 收发器做了什么TTL 电平与差分总线的翻译官收发器内部就两个方向DI 引脚收 MCU 的 TX 信号转成 A/B 之间的差分电压推上总线总线上的差分电压经过比较器还原成 0/1从 RO 引脚送回 MCU 的 RX。RS485 规定 A-B 压差大于 200mV 判为逻辑 1小于 -200mV 判为逻辑 0中间是死区。这个压差判据就是它抗干扰能力的来源——总线上叠加的共模噪声会同时抬升 A 和 B差值不变判决结果就不变。标准器件的共模输入范围通常在 -7V 到 12V 之间所以两端设备地电位差个几伏还能正常通信这是 RS232 做不到的。代价是半双工。同一时刻总线上只能有一个设备在推电流其余设备必须把自己的输出级置成高阻否则两个设备同时驱动 A/B 就是硬碰硬的短路。收发器上为此有两个控制脚DEDriver Enable高有效和 /REReceiver Enable低有效。绝大多数电路把这两个脚短接在一起用一个 GPIO 控制高电平发送、低电平接收。这套设计在代码里的体现就是那个RS485_TX_EN宏它不是一个普通的状态变量而是直接决定总线归属权的开关。提示选型上3.3V 供电的收发器要和 3.3V 的 F103 直连如果用的是 5V 器件RO 输出给 MCU 的 RX 是高电平 5V虽然 F103 多数引脚标称 5V 容忍但 DE/RE 的控制电平要单独确认阈值别想当然。2.2 引脚与接线PA2/PA3/PG3 三个脚位不能错F103ZET6 的 USART2 固定挂在 PA2TX和 PA3RX上这是复用功能不是随便重映射的。文档里的初始化代码把 PA2 配成复用推挽、PA3 配成浮空输入方向控制脚用的是 GPIOG 的 Pin3。把这张表抄下来贴在工作台前面接线时逐个对信号STM32 引脚GPIO 模式接到收发器备注USART2_TXPA2复用推挽 AF_PP50MHzDI时钟挂在 APB2USART2_RXPA3浮空输入 IN_FLOATINGRO建议外部上拉 10k方向控制PG3推挽输出 Out_PP50MHzDE 与 /RE 短接高电平发送电源3.3V 或 5V—VCC按探头铭牌来地GND—GND必须与探头共地时钟使能这里有个容易忽略的点GPIOA 和 GPIOG 都在 APB2 总线上USART2 挂在 APB1 上两条总线的时钟要分别开。代码里RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOG|RCC_APB2Periph_GPIOA, ENABLE)和RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE)是两句独立的调用漏掉任何一句的表现都是「编译通过、运行时引脚毫无反应」排查时先回头数这两行。现场接线还有三条硬规矩A 接 A、B 接 B接反了近距离可能还能通、拉长必挂两端设备必须共地只接 A/B 不接地共模电压漂到超出范围就时通时断总线长度超过三十米或者波特率上到 115200末端要并 120Ω 终端电阻。这几条在实验室桌面上全都体现不出来一上现场就集中爆发。2.3 方向切换时序等 TC 标志而不是 TXE这是整套代码里最容易写错、也最难通过现象定位的地方。发送函数里那个while(USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET)等的不是「数据写进寄存器」而是「最后一个字节的最后一位已经从移位寄存器移出去了」。区别在哪TXE发送数据寄存器空置位时数据只是从 DR 搬到了移位寄存器线上还在一个 bit 一个 bit 地吐。这时候如果把 DE 拉低收发器立刻退出驱动状态最后那一个字节就被拦腰截断。9600 波特率下一个字节约 1.04ms截断的后果是传感器收到一帧 CRC 错误的报文直接丢弃不回——现象就是「偶尔能通、大部分时候没反应」非常像接触不良实际是时序问题。反过来接收切换也有讲究。发完最后一个字节、TC 置位后立刻拉低 DE收发器进入接收态需要一点建立时间同时总线上最后一个停止位可能还没完全结束所以代码里在拉低之前垫了delay_ms(2)。这个 2ms 是经验值9600 下够用波特率上去了可以缩短但别省成 0。注意方向脚上电默认必须是低电平接收态。如果复位瞬间 GPIO 处于浮空或者高电平收发器会短暂抢占总线多节点系统里这一下就可能把别人正在发的帧打坏。初始化函数末尾那句RS485_TX_EN0不是可有可无的收尾。3. 驱动代码逐函数拆初始化、发送、接收三个动作的边界硬件理顺之后代码其实就三个动作。但每个动作都有自己的边界条件写成「能跑」和写成「稳定跑」差的就是这些边界。3.1 RS485_Init 里的四件事这个函数表面上是初始化串口实际干了四件互相独立的事任何一件漏了都会以不同形式报错。第一件是时钟前一小节已经说过。第二件是 GPIO 模式配置注意 PA3 的注释写的是「模拟输入」但代码用的是GPIO_Mode_IN_FLOATING以代码为准——注释是复制粘贴留下来的历史包袱别被带偏。第三件是 USART2 的参数波特率 9600、8 位字长、1 位停止位、无校验、无硬件流控、收发双开。第四件是 NVIC 配置优先级分组在 main 里设成了NVIC_PriorityGroup_22 位抢占 2 位响应USART2 中断抢占优先级 3、子优先级 2。参数表列一下改的时候对着改参数取值依据波特率9600传感器出厂默认改了探头也得跟着改字长8 位Modbus-RTU 固定 8N1 或 8E1停止位1 位同上校验无若探头是 8E1这里要改成USART_Parity_Even流控无485 靠 DE 控方向不走 RTS/CTS中断RXNE 使能每收到一个字节进一次中断EN_USART2_RX这个宏是中断接收的总开关定义为 1 才会在USART_ITConfig里开 RXNE 中断。调试阶段如果只想验证发送是否正常把它改成 0 可以让接收缓冲区一直为空排除接收侧的干扰。3.2 发送函数循环里那个 while 到底在等什么RS485_Send_Data的逻辑是拉高 DE 进发送态逐字节写 DR每写一个之前等一次 TC全部写完后最后再等一次 TC清接收计数拉低 DE 回接收态。void RS485_Send_Data(u8 *buf, u8 len) { u8 t; RS485_TX_EN 1; /* 抢占总线切入发送态 */ for (t 0; t len; t) { while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); USART_SendData(USART2, buf[t]); /* 写 DR 触发发送 */ } while (USART_GetFlagStatus(USART2, USART_FLAG_TC) RESET); /* 等最后一个字节彻底走完 */ RS485_RX_CNT 0; /* 清掉发送期间可能收到的杂散字节 */ RS485_TX_EN 0; /* 释放总线回到接收态 */ }循环内那个while在第一次进入时等的是「上一次发送完成」因为在初始化里已经USART_ClearFlag(USART2, USART_FLAG_TC)清过标志第一次会立刻通过。真正起作用的是循环外那个while它保证 DE 拉低之前移位寄存器已经空。RS485_RX_CNT 0这一句很多人会漏后果是发送期间收发器可能因为总线上的反射收到一两个杂散字节接收计数被污染下一次判帧就会多算长度。参数只有一个len注意它是u8最大 255。请求帧固定 8 字节不会溢出但如果哪天要发长报文这个类型得改成u16。3.3 接收中断攒字节 10ms 收敛判帧尾接收分两半。中断里只做一件事把 DR 里的字节搬进RS485_RX_BUF计数加一越界就回绕或者丢弃。void USART2_IRQHandler(void) { u8 res; if (USART_GetITStatus(USART2, USART_IT_RXNE) ! RESET) { res USART_ReceiveData(USART2); /* 读 DR 同时清 RXNE */ if (RS485_RX_CNT 64) /* 缓冲区 64 字节防越界 */ { RS485_RX_BUF[RS485_RX_CNT] res; } } USART_ClearFlag(USART2, USART_FLAG_TC); }这里必须强调中断里不要做任何发送动作也不要用delay_ms。中断服务函数执行时间越长主循环被抢占得越久而且如果延时依赖的 SysTick 优先级低于 USART2会在中断里死等直接卡死。原文那份参考实现里在中断里做了「收到什么回发什么」的回显还夹了两次毫秒级延时这是导致总线自激和偶发卡死的高发点实际项目里必须删掉。接收的另一半在主循环里靠「两次采样长度不变」来判帧结束void RS485_Receive_Data(u8 *buf, u8 *len) { u8 rxlen RS485_RX_CNT; u8 i 0; *len 0; delay_ms(10); /* 帧内字符间隔远小于 10ms等它收敛 */ if (rxlen rxlen RS485_RX_CNT) /* 两次长度一致说明这一帧发完了 */ { for (i 0; i rxlen; i) buf[i] RS485_RX_BUF[i]; *len RS485_RX_CNT; RS485_RX_CNT 0; /* 清计数准备下一帧 */ } }10ms 这个值的依据是 Modbus-RTU 的帧间隔定义帧与帧之间至少静默 3.5 个字符时间。9600 波特率下 1 个字符是 11 位、约 1.15ms3.5 字符约 4ms取 10ms 留了一倍余量。代价是这个函数是阻塞的主循环调用它就会停 10ms如果还要跑按键扫描或者屏幕刷新得改成状态机版本最后一章会给出写法。提示rxlen是u8RS485_RX_CNT也是u8两者在中断和主循环之间共享读写顺序有讲究。先读长度再延时再比对如果写成先延时后读第一次读到的就已经是被覆盖后的值判帧永远失败。3.4 主循环的节奏300ms 采样与 3s 刷新把三个函数串起来的主循环节奏是这样的发 8 字节请求 → 等 300ms → 收数据 → 有数据就解析显示 → 等 3000ms 进入下一轮。while (1) { RS485_Send_Data(TR_Read, 8); /* 发查询指令 */ delay_ms(300); /* 给探头留出响应和传输时间 */ RS485_Receive_Data(rs485buf, len); if (len) { if (len 9) len 9; /* 帧长上限保护 */ /* 解析与显示见下一章 */ } delay_ms(3000); /* 采样周期 3s土壤量变化慢够用 */ }300ms 的等待不是拍脑袋9600 下 9 字节响应帧的传输时间约 9.4ms加上探头内部 ADC 采样和固件处理常见规格在 50~200ms 之间。给 300ms 是留余量。3s 的周期取决于被测对象的变化速率土壤温湿度这种慢变量 3s 绰绰有余如果换成振动或者流量得压到几十毫秒那时候阻塞式写法就撑不住了。4. 报文层Modbus-RTU 请求帧拆到字节温湿度怎么算出来代码跑通只说明电气和时序没问题数据对不对全看报文层。这一层出错的表现往往不是「收不到」而是「收到了但数值明显不合理」比收不到更难查。4.1 八个字节的请求帧逐个字段对照TR_Read[8] {0x01, 0x03, 0x00, 0x06, 0x00, 0x02, 0xC4, 0x38}这一行是整份代码里信息密度最高的地方一个字节都不能随便改。偏移值含义改动影响00x01从站地址多节点时必须唯一10x03功能码读保持寄存器换成 0x04 读输入寄存器看探头手册20x00起始寄存器高字节起始地址 0x000630x06起始寄存器低字节即第 7 个寄存器40x00寄存器数量高字节读 2 个寄存器50x02寄存器数量低字节即 4 个字节数据60xC4CRC 低字节前 6 字节算出70x38CRC 高字节Modbus 规定低字节在前功能码 0x03 是「读保持寄存器」这是 Modbus 里最常用的读命令。寄存器地址 0x0006 和数量 0x0002 这两个值不是通用的不同厂家的探头把温湿度放在不同寄存器上有的在 0x0000有的在 0x0012。拿到新探头第一件事是翻手册确认这两个字段改完必须重算 CRC手工改完不重算 CRC 是最常见的「发出去没反应」原因。4.2 响应帧结构与 CRC 自校验正常响应是 9 个字节结构跟请求帧对应地址 功能码 字节数 4 字节数据 2 字节 CRC。异常响应只有 5 个字节功能码的最高位置 1比如 0x83后面跟一个异常码常见的是 0x02非法数据地址和 0x03非法数据值这两个码基本等于在告诉你「寄存器地址写错了」。CRC 校验必须做。总线上的干扰、方向切换时被截断的半个字节、多节点抢占总线造成的碰撞全都会产生「长度看着对、内容已经烂了」的帧。不做校验直接拿去算屏幕上就会时不时跳出一个离谱的温湿度值。/* Modbus-RTU CRC16多项式 0xA001 反向初值 0xFFFF */ u16 CRC16_Modbus(u8 *buf, u8 len) { u16 crc 0xFFFF; u8 i, j; for (i 0; i len; i) { crc ^ buf[i]; for (j 0; j 8; j) { if (crc 0x0001) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; /* 低字节在前高字节在后 */ }这个函数算出来的crc低字节要跟帧里的倒数第二个字节比、高字节跟最后一个字节比。用请求帧验证一下前 6 字节算出来是 0x38C4所以帧尾是0xC4, 0x38跟常量数组对得上说明算法实现正确。以后改任何字段都可以拿这个函数现算现填。4.3 湿度除以 10、温度除以 100 的缩放来源原始数据是 4 个字节按高字节在前拼成两个 16 位整数u16 raw_h (u16)(rs485buf[3] 8) | rs485buf[4]; /* 湿度原始值 */ u16 raw_t (u16)(rs485buf[5] 8) | rs485buf[6]; /* 温度原始值 */为什么要缩放Modbus 寄存器只能存整数传感器固件为了保留小数位会把真实值乘一个系数再存进去。土壤湿度常见的是乘 10单位 0.1%RH温度常见的是乘 10 或者乘 100单位 0.1℃ 或 0.01℃。原文代码里WD(rs485buf[3]*256rs485buf[4])/100和SD(rs485buf[5]*256rs485buf[6])/10用的是 100 和 10 两个不同系数这说明这颗探头的湿度分辨率是 0.01、温度是 0.1。这里有个必须点出来的坑WD和SD在原文里声明成u8。u8的取值范围是 0~255湿度 0~100 勉强塞得下但小数位全丢了温度如果乘了 10、范围到 -400~800赋值给u8会直接截断回绕零下温度显示成正数、高温显示成个位数全乱。正确做法是用s16存原始值、float或者放大十倍的s16做显示s16 raw_t (s16)((rs485buf[5] 8) | rs485buf[6]); /* 有符号兼容零下 */ float temp_c raw_t / 10.0f; /* 按手册系数改 */温度字段是不是有符号也要看手册。有的厂家用补码表示负数有的用最高位做符号位加偏移量还有的把零下映射到 0x0000 以上的正数区间。这三种编码方式算出来的结果完全不同拿一个新探头先用冰水混合物0℃和室温各测一次对照原始十六进制值反推系数比看手册快。4.4 把解析封装成带校验的函数把上面几步收拢成一个函数返回值表示解析是否成功主循环只负责显示/* 返回 0 成功-1 帧长不对-2 CRC 错-3 异常响应 */ int Parse_SoilFrame(u8 *frame, u8 len, float *humi, float *temp) { u16 crc_calc, crc_recv; if (len ! 9) return -1; /* 正常响应固定 9 字节 */ if (frame[1] 0x80) return -3; /* 功能码最高位异常响应 */ crc_calc CRC16_Modbus(frame, 7); /* 前 7 字节参与校验 */ crc_recv (u16)frame[7] | ((u16)frame[8] 8); /* 低字节在前 */ if (crc_calc ! crc_recv) return -2; *humi ((s16)((frame[3] 8) | frame[4])) / 100.0f; *temp ((s16)((frame[5] 8) | frame[6])) / 10.0f; return 0; }三个参数frame是接收缓冲区len是实际收到的字节数后两个是输出。返回值设计成负数是常见做法主循环里可以据此决定是重发、报警还是显示上一次的有效值。crc_recv的拼接顺序容易写反记住 Modbus 是低字节在前帧里第 8 个字节是低字节。提示(s16)((frame[3] 8) | frame[4])这里必须显式转s16再赋值如果先赋给u16再转负数的位模式在两步之间不会变但中间变量类型写错会让编译器插入不同的符号扩展结果差 65536。这种 bug 只在零下温度出现夏天测不出来。5. 避坑与排查五个现场翻车记录这一章列的每一条都是实打实踩过的共同特点是「代码看起来没问题但就是不工作」。按现象、原因、解决三段式记下来下次遇到能省半天。5.1 方向脚宏定义和初始化端口对不上现象发送函数返回了串口助手在总线上也测到波形但传感器毫无反应偶尔又能通一次。原因头文件里#define RS485_TX_EN PDout(7)初始化函数里配的却是 GPIOG 的 Pin3。宏操作的端口和实际接线的端口不是同一个DE 永远拉不高收发器一直处于接收态MCU 的 TX 信号根本没上总线。示波器探 MCU 的 PA2 有波形探 A/B 什么都没有就是这个问题。解决宏定义、GPIO 初始化、原理图三处对齐同一个端口和引脚。改完在发送函数里加一个临时的翻转测试——拉高 DE 后直接读回GPIO_ReadOutputDataBit确认电平真的变了再往下走。5.2 中断里做回显总线自己跟自己打架现象单节点测试时好时坏接上第二个节点后彻底不通偶尔程序卡死不响应。原因参考实现的中断服务函数里做了「收到一个字节就切发送态把它发回去」的处理还夹了delay_ms(1)和delay_ms(2)。收到的每一个字节都会触发一次回发两个节点同时这么干就是互相碰撞中断里调用毫秒延时如果 SysTick 优先级低于 USART2延时永远等不到直接死锁。解决中断里只做「搬字节 计数」不发送、不延时、不解析。发送动作全部放到主循环由应用层决定什么时候发、发什么。中断服务函数的执行时间控制在几微秒量级。5.3 u8 变量存温度数值被截断现象湿度显示基本正常温度偶尔显示成个位数或者负数冬天尤其明显。原因WD和SD声明成u8而温度原始值缩放后可能超过 255 或者为负。u8赋值时的截断是按位取的不是饱和256 变成 0、-1 变成 255显示出来的数完全对不上。解决原始值用s16显示值用float或者放大十倍的s16。改完之后把LCD_ShowxNum换成能显示小数点和负号的LCD_ShowString先格式化成字符串再打别用只支持无符号整数的显示函数硬凑。5.4 A/B 接反、没共地、缺终端电阻现象桌面上三十厘米的跳线通信正常接到现场二十米线缆上就时通时断早上能通中午不通。原因三个硬件问题叠在一起。A/B 接反在短线、低速率下因为反射和容性耦合可能还能勉强判对不共地导致两端共模电压随负载变化漂移超出收发器的 -7~12V 范围就误判长线末端不接 120Ω信号反射叠加在边沿上接收端采到的电平忽高忽低。解决先短接法确认 A/B 极性不确定就交换一次试通了就是接反补一根地线两端设备的地直接连末端并 120Ω 电阻中间节点不接。三件事做完还不行再考虑降波特率到 4800 试一次降速能通说明是信号完整性问题而不是协议问题。5.5 长度截断直接显示不做 CRC 校验现象大部分时候数值正常每隔几分钟跳出一个明显不合理的值比如湿度 400%、温度 120℃。原因主循环里if(len 9) len 9之后直接取rs485buf[3..6]算温湿度既没验证len 9也没校验 CRC。干扰造成的半帧、两帧粘连、方向切换截断的残帧长度可能恰好凑够 9内容却是错的。解决加长度判断和 CRC 校验校验失败就丢弃这一帧、沿用上一次的有效值并累计一个错误计数。错误计数持续增长说明硬件有问题偶尔一次说明是噪声两种情况的处理策略不一样。6. 从单点到多点轮询状态机与现场验证手法单节点跑通之后真正的考验是多节点轮询。十个探头挂在同一条总线上每个地址轮流问一遍一轮下来几百毫秒主循环里再塞阻塞延时就会互相拖死。这时候要把第 3 章那套阻塞收发改成状态机。typedef enum { ST_IDLE, ST_WAIT_ACK, ST_NEXT } rs_state_t; static rs_state_t st ST_IDLE; static u8 node 1; /* 当前轮询地址1~10 */ static u32 t_send 0; void RS485_Poll_Task(void) { switch (st) { case ST_IDLE: TR_Read[0] node; /* 换从站地址 */ /* 地址变了必须重算 CRC否则从站直接丢弃 */ { u16 c CRC16_Modbus(TR_Read, 6); TR_Read[6] c 0xFF; TR_Read[7] (c 8) 0xFF; } RS485_Send_Data(TR_Read, 8); t_send GetTickCount(); /* 记录发送时刻 */ st ST_WAIT_ACK; break; case ST_WAIT_ACK: if (RS485_RX_CNT) /* 收到任何字节就算有响应 */ { /* 交给解析函数成功则更新该节点的缓存 */ st ST_NEXT; } else if (GetTickCount() - t_send 200) /* 200ms 超时判定掉线 */ { Mark_Node_Offline(node); /* 标记离线界面置灰 */ st ST_NEXT; } break; case ST_NEXT: node (node 10) ? 1 : node 1; /* 轮转到下一个地址 */ st ST_IDLE; break; } }这段的关键在于「地址变了要重算 CRC」和「超时要能跳过去」。前者漏了的表现是第一个节点通、后面全不通后者漏了的表现是拔掉一个探头整条总线卡死。GetTickCount用 SysTick 的毫秒计数实现两行代码的事比delay_ms阻塞强太多。现场验证我一般按这三步走顺序不能反步骤动作通过判据1断开探头只留 MCU 和收发器示波器看 A/B发送时能看到差分波形幅度 1.5V2接单个探头串口助手抓原始十六进制收到 9 字节CRC 校验通过3接全部节点跑轮询十分钟无超时累积各节点数据连续第一步能把 MCU 侧和总线侧的问题切开第二步确认协议层第三步才是系统级验证。跳过第一步直接上全系统出问题时根本分不清是代码、接线还是干扰。最后说一个我自己的习惯每换一款新传感器我都会先用串口助手手动发一次请求帧把响应的十六进制原样抄在纸上标出哪两个字节是湿度、哪两个是温度、系数是多少然后再动代码。这个动作花五分钟能省掉后面反复改系数、怀疑类型、重烧固件的几个小时。那年冬天一个探头温度一直显示 6548℃ 折腾到半夜最后发现是符号位没处理从那以后每次上电第一件事就是拿冰袋和体温测两个点验证缩放系数强制走一遍才敢接下一级。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。