☰
STM32F407与MLX90640红外测温模块:从I2C驱动到LCD热像图完整实现
2026/10/28 9:13:39 网站建设 项目流程

简介:面向嵌入式系统与红外热成像开发者的完整工程方案,基于正点原子探索者STM32F407,集成MLX90640(32×24像素)红外传感器与LCD显示模块,通过双线性插值算法优化图像显示,可免修改代码适配2.8寸、3.5寸、4.3寸LCD,适合学习微控制器驱动、红外测温与图像处理的开发者。压缩包共206个文件,以56个h头文件、54个c源文件为核心,辅以o目标文件、crf编译信息、uvprojx工程文件及hex、axf等产物,整体6.89MB,目录按HARDWARE、SYSTEM、CORE、USER等模块组织,便于理解硬件接口与代码结构。资源涵盖从传感器驱动、数据处理到显示逻辑的完整实现,包含LCD多尺寸适配、双线性插值增强、STM32外设配置等关键代码,可据此搭建热成像系统或进行二次开发,已有2701人学习下载。

1. 用STM32F407把MLX90640变成一台热像仪:这个组合比你想的更实际

手头有一块正点原子探索者STM32F407开发板,再买一块MLX90640红外阵列模块,接一块LCD屏,就能拼出一台看得见温度分布的“简易热像仪”。MLX90640是一颗32×24像素的红外测温传感器,通过I2C输出物体表面温度;探索者F407主频168MHz,跑I2C和数学插值绰绰有余;LCD屏则负责把温度矩阵渲染成人眼友好的热像图。这个方案适合做电路板发热检测、非接触测温实验、甚至小型配电柜温度监控。成本远低于工业热像仪,而且I2C驱动、伪彩色映射、LCD刷屏这几个环节拆开来看都不复杂,踩坑主要集中在时序和数据处理上。

2. 硬件接线与I2C总线:先让传感器被看见,再谈图像

2.1 探索者开发板与MLX90640模块的引脚对应表

正点原子探索者STM32F407开发板上的I2C接口没有单独排针,常见做法是复用GPIO模拟I2C,或者直接用芯片的硬件I2C1(PB6/PB7)。我一般用软件模拟I2C,因为MLX90640的I2C时序要求并不苛刻,100kHz/400kHz都能跑,而且软件I2C可以任意切换引脚,调试起来方便。接线如下:

MLX90640模块引脚探索者开发板接口说明
VIN / VCC3.3V(或5V,需看模块供电范围)MLX90640核心芯片工作电压3.3V,很多模块带稳压芯片,可接5V
GNDGND共地必须接
SCLPB6(I2C1_SCL)软件模拟时可换任意GPIO
SDAPB7(I2C1_SDA)软件模拟时可换任意GPIO

有些成品模块还把SDA/SCL做成了带电平转换的版本,这时VIN接5V也没问题。但要注意:探索者板载的LCD接口占用了PB0~PB15等大量引脚,如果PB6/PB7和LCD接口冲突,请改用PE0/PE1之类的空闲引脚。建议先查阅探索者原理图,确认PB6/PB7没有复用冲突。

接线中最容易被忽略的是上拉电阻。MLX90640模块上一般已经贴了4.7kΩ或10kΩ上拉电阻到VIN,如果你的模块是裸传感器,需要在SCL/SDA上分别加一个4.7kΩ电阻到3.3V。上拉电阻太小(如1kΩ)会让信号沿太钝,太大会导致I2C通信不稳。正点原子探索者的3.3V电源能力足够,不建议把MLX90640的VIN接到板载变压器的5V输出后再经过模块稳压,直接3.3V最干净。

2.2 供电、上拉电阻与电平匹配的三个关键点

我遇到过几次“读回全0xFF”的问题,最后查下来都是供电纹波或者SCL/SDA电平异常。MLX90640内部模拟电路对电源噪声敏感,工作电流在10~20mA量级,但如果使用劣质USB供电,纹波可能叠加到高频噪声上,导致温度数据跳变。探索者开发板用DC005或者USB供电时,建议在MLX90640的VIN和GND之间加一颗100nF去耦电容,再加一颗10uF钽电容。

电平匹配方面,MLX90640的I2C引脚耐压是3.6V,如果开发板上有5V I2C设备,千万不能把两者直接挂在一起。探索者F407的GPIO是3.3V TTL兼容,所以和MLX90640之间不需要电平转换,但注意不要解开模块上有些板子自带的5V逻辑跳线。若使用探索者板上的硬件I2C外设(PB6/PB7),还需要注意I2C外设的容忍电压是5V,但输出开漏要靠外部上拉,板上是否已经存在上拉要看原理图。

调试I2C时,我会先在板上找一个空闲GPIO接成软件I2C,写一个扫描设备地址的程序,把MLX90640的7位地址0x33(左移一位后写入0x66)发出去。如果扫描不到设备,最先怀疑的不是代码,而是接线和上拉。用万用表量SCL/SDA对地电压,正常待机时应为高电平(约3.3V),如果为低,说明上拉电阻没焊好或者芯片被拉低。

2.3 用示波器或逻辑分析仪验证I2C有没有通

正点原子很多开发者有DM40示波器,用它抓I2C波形非常方便。没有的话,也可以用一个简易的逻辑分析器。验证步骤如下:

  1. 将逻辑分析仪CH0接到SCL,CH1接到SDA,GND共地。
  2. 设置采样率不低于1MHz(SPI/I2C调试建议4MHz以上)。
  3. 在探索者上运行一个最简单的I2C写命令:向MLX90640的0x8000寄存器写0x0000(软复位)。
  4. 观察波形中是否有ACK信号:SCL第9个脉冲时SDA被拉低,说明设备应答正常。

如果看到波形有毛刺,可以在SCL/SDA线上各串一个220Ω电阻再进模块,抑制反射。注意:软件模拟I2C时,如果GPIO配置成推挽输出而非开漏输出,也容易产生信号竞争。开漏输出 + 外部上拉是标准做法,探索者的GPIO配置需要将GPIO_Mode设为GPIO_Mode_OUT_OD,否则I2C波形可能扭曲。

这一步看起来简单,但能省掉后面大量排错时间。我习惯把“扫描到0x33”作为一个工程里程碑:只有这个节点过了,才继续写MLX90640的寄存器去读温度。

3. MLX90640的驱动移植:读RAM、算温度、处理坏像素

3.1 先搞懂MLX90640的存储结构:RAM、EEPROM、寄存器

MLX90640内部有三块关键存储区:RAM(0x2400~0x27FF)存放当前帧的768个像素的16位原始ADC值;EEPROM(0x2400? 这里要小心,实际上EEPROM地址在0x2400之后的一段区域,通常由驱动库定义)存放出厂校准系数;控制寄存器(0x8000等)用于设置帧率、测量模式、软复位等。RAM区每帧会更新,EEPROM里的数据是出厂时烧录的,不需要改动。

温度计算不能用原始ADC值直接当温度,必须先读取EEPROM中的热敏电阻(Ta)补偿系数、像素灵敏度、以及Kta/Kv等校准参数,再按照Melexis官方给出的公式计算物体温度。这个公式推导较长,但它的核心思想是:每个像素的ADC值减去偏移后,除以灵敏度,同时修正环境温度对芯片基底的影响。早年调试时,我图省事直接用ADC值映射成颜色,结果图像完全看不出温度层次,后来才明白必须做完整的校准计算。

在工程实现上,最稳的做法是移植MLX90640官方驱动库中的MLX90640_CalculateTo函数。这个函数需要传入EEPROM数组和RAM数组,输出一个24×32的浮点温度矩阵。如果你不想用官方库,也得按数据手册一步步实现公式。移植时注意官方库依赖“CtrlConfig”等寄存器的最低两位(sub-page模式),必须在初始化时把控制寄存器配成连续测量模式,并选择交叉模式(interleaved)还是滚动模式(chess pattern)。探索者F407的Flash有1MB,容量充足,官方库加上原子LCD驱动不会挤占空间,所以直接完整移植是比较省心的路线。

3.2 读取一帧温度数据的I2C代码骨架

在探索者工程里新建mlx90640.c,核心读取流程是:先读EEPROM一次,然后循环读RAM并计算温度。下面这段是读取一帧RAM数据的关键代码(软件I2C为例):

#define MLX90640_I2C_ADDR (0x33 << 1) // 7位地址0x33,左移1位成为8位写/读地址 #define MLX90640_RAM_START 0x2400 #define MLX90640_RAM_SIZE 768 // 32 * 24 像素 // 连续读取RAM区数据(16位寄存器地址,数据也是16位) uint8_t MLX90640_ReadRAM(uint16_t *buf, uint16_t startAddr, uint16_t len) { uint16_t addr = startAddr; uint8_t tmp[2]; for (uint16_t i = 0; i < len; i++) { // 软件I2C写寄存器地址:高字节在前 I2C_Start(); I2C_SendByte(MLX90640_I2C_ADDR | 0); // 写地址 if (!I2C_GetAck()) return 1; I2C_SendByte(addr >> 8); I2C_GetAck(); I2C_SendByte(addr & 0xFF); I2C_GetAck(); // 重复起始,切入读模式 I2C_Start(); I2C_SendByte(MLX90640_I2C_ADDR | 1); // 读地址 I2C_GetAck(); tmp[0] = I2C_RecvByte(); // 读取高字节 I2C_SendAck(0); // 继续读 tmp[1] = I2C_RecvByte(); // 读取低字节 I2C_SendAck(1); // 最后读完后发送NACK I2C_Stop(); buf[i] = (tmp[0] << 8) | tmp[1]; addr++; } return 0; }

这段代码有几个参数需要说明。MLX90640_RAM_SIZE是768,对应32列×24行;每次读一个16位数据需要先写寄存器地址再读两字节,所以整帧RAM读完要发768次I2C事务,在100kHz下耗时大约20ms,能跑到20帧/秒左右。如果你用硬件I2C加DMA,可以一次读128字节数据块,效率高很多,但代码也更复杂。一般来说,MLX90640帧率调到8Hz或16Hz,用上述软件I2C足够。

需要注意:软件I2C的时序中,I2C_SendAck(0)表示主机在第9个时钟拉低SDA应答,I2C_SendAck(1)表示最后读完后不拉低SDA(NACK)。如果写反,最后一字节会读错。另外,MLX90640的RAM地址在读取过程中会自动递增吗?数据手册说明并非自动递增,每次读一个寄存器都必须写地址,所以上面的循环写法是正确的,不能偷懒只写一次地址。

3.3 帧同步与坏像素:为什么图像会出现雪花和死点

MLX90640有两种输出模式:连续测量和单次测量。默认上电后是连续测量,内部用“子页(sub-page)”机制把一帧分成两个子帧,每个子帧包含一半像素。状态寄存器0x8000的bit3/bit2标识当前子帧,只有读取同一子帧的数据并组合,才能得到完整一帧。官方库的做法是:读取状态寄存器,等待子页翻转后再读取RAM。如果不做帧同步,你会看到画面上下两半错开,或者在刷新时温度跳变。

我在探索者上刚开始偷懒,只看RAM数据就计算,结果温度图像像被撕开一样,上半部分和下半部分相差很大。后来加上了帧同步逻辑:

// 等待子页变化,确保读到完整一帧新数据 while (1) { uint16_t status = MLX90640_ReadReg(0x8000); // 读状态寄存器 if ((status & 0x0002) != last_subpage) { // bit1表示子页状态 last_subpage = status & 0x0002; break; } } MLX90640_ReadRAM(mlx_ram, MLX90640_RAM_START, MLX90640_RAM_SIZE);

这里的关键是bit1(有的手册标注为sub-page bit),判断前后两次状态是否变化。如果连续两次读到相同子页,说明数据没更新,继续等待。这个等待超时要加一个计数器,比如超过1000次循环就退出,否则在故障环境下程序会死等,看门狗都救不了。

坏像素问题也要处理。MLX90640出厂时会在EEPROM里记录坏点坐标和替代方式,官方库在MLX90640_SetResolution等初始化函数里会读取坏点列表,并用邻近像素插值修复。如果你只用最简驱动而忽略坏点,热像图上会出现固定位置的黑点或白点——那不是真实温度,而是传感器坏像素。探索者例程里一般不会帮你做这步,需要自己去驱动里调用坏点修正逻辑。

4. 从温度矩阵到LCD热像图:插值、伪彩色和DMA刷新

4.1 温度到RGB565:伪彩色映射表怎么生成

MLX90640算出来的是浮点温度矩阵,比如float temp[24][32]。LCD屏上要显示成热像图,需要把温度值转换成颜色。常见做法是定义一条“彩虹色映射表”:低温用蓝色(或黑色),中温用绿色/黄色,高温用红色/白色。这样人眼能直观分辨热点。

一种简单的映射是把温度范围线性映射到0~255整数,然后用查表法得到RGB565值。RGB565是16位色:高5位红色,中6位绿色,低5位蓝色。正点原子的LCD屏大部分是RGB565格式,一个像素占2字节。下面生成映射表:

#define TEMP_MIN 20 // 显示范围下限,单位0.01℃ 这里用整数摄氏度 #define TEMP_MAX 80 // 显示范围上限 #define COLOR_TAB_SIZE 256 static uint16_t color_tab[COLOR_TAB_SIZE]; // 温度转RGB565查表项:低温蓝色,中温绿黄,高温红白 void BuildColorTable(uint16_t *tab, uint16_t temp_min, uint16_t temp_max) { for (int i = 0; i < COLOR_TAB_SIZE; i++) { // 分成三段渐变:蓝->青->绿->黄->红 if (i < 64) { tab[i] = RGB565(0, 0, i * 4); // 深蓝到亮蓝 } else if (i < 128) { tab[i] = RGB565(0, (i - 64) * 4, 128); // 蓝到绿 } else if (i < 192) { tab[i] = RGB565((i - 128) * 4, 255, 0); // 绿到红 } else { tab[i] = RGB565(255, 255, (i - 192) * 4); // 红到黄白 } } }

映射时,先限制温度范围:t < temp_min取索引0,t > temp_max取255,中间(t - temp_min) * 255 / (temp_max - temp_min)得到索引。这个范围设置很关键:如果你在室内测温,温度范围20~40℃就够;但测烙铁头时,要扩大到20~300℃,否则图像在低温区间全是蓝色,热点却过曝。我是建议把温度范围做成板级可调参数,用按键加减,这样在不同场景下不用重新编译。

4.2 双线性插值:把32×24拉大到128×96的常用做法

32×24像素直接放大到LCD上,每个温度点对应一个很大的色块,颗粒感严重。常见做法是双线性插值,让过渡平滑。目标分辨率可以是96×72、128×96或192×144,看LCD性能和刷新速度。探索者F407跑168MHz,128×96插值只需要计算约1.2万个输出像素,每个做几次乘法和加法,CPU负载很小。

双线性插值的本质:对于目标像素坐标(x, y),映射到源温度图的浮点坐标(fx, fy),取四个相邻原始像素加权平均。下面是一个适用于整幅图的优化版,核心在查表代替浮点除法:

// temp_src[24][32] 源温度,temp_out[dest_rows][dest_cols] 插值后的温度 // 这里只展示单通道循环的核心逻辑 void TempBilinear(float *src, uint16_t src_cols, uint16_t src_rows, float *dst, uint16_t dst_cols, uint16_t dst_rows) { for (uint16_t yy = 0; yy < dst_rows; yy++) { float fy = (float)yy * (src_rows - 1) / (dst_rows - 1); uint16_t y0 = (uint16_t)fy; uint16_t y1 = y0 + 1; if (y1 >= src_rows) y1 = y0; float lam_y = fy - y0; for (uint16_t xx = 0; xx < dst_cols; xx++) { float fx = (float)xx * (src_cols - 1) / (dst_cols - 1); uint16_t x0 = (uint16_t)fx; uint16_t x1 = x0 + 1; if (x1 >= src_cols) x1 = x0; float lam_x = fx - x0; float v00 = src[y0 * src_cols + x0]; float v01 = src[y0 * src_cols + x1]; float v10 = src[y1 * src_cols + x0]; float v11 = src[y1 * src_cols + x1]; float v = (v00 * (1 - lam_x) + v01 * lam_x) * (1 - lam_y) + (v10 * (1 - lam_x) + v11 * lam_x) * lam_y; dst[yy * dst_cols + xx] = v; } } }

注意这个代码是浮点运算,虽然F407有FPU,但直接浮点算1.2万像素也要不少时间。我在实际项目里会改成定点:将温度扩大100倍存为int16,然后插值只用整数乘除,速度提升明显。对于追求极致性能的使用者,可以把内层循环写成查表,或者用DSP库。但是说实话,128×96的插值在最坏情况下也就不到2ms,不值得过度优化。

插值后,再把目标矩阵映射到颜色索引,生成LCD的RGB565缓冲。这一整步要注意数据类型:温度缓冲如果定义为float,占4字节;如果定义为int16_t(0.01℃分辨率),则一个像素占2字节。从RAM读取到温度计算再到插值,最好统一用int16_t,避免浮点和整型之间反复转换导致精度丢失。

4.3 探索者LCD上刷新图像:LTDC配置与DMA2D搬运

探索者开发板板载的LCD接口是RGB565并口,常用两种驱动:一种是老型号的FSMC接口TFT(如2.8寸、3.5寸),另一种是RGB接口(LTDC)驱动的4.3寸/7寸屏。如果你的屏是LTDC接口,刷新热像图有两种思路:

第一种思路:在LTDC的Framebuffer里直接填色。探索者的LCD例程会分配一个显存数组(比如lcdbuf[800*480]),你需要把热像图的像素拷贝到显存对应区域。用DMA2D做快速拷贝,代码类似:

// 将热像图缓冲 image_buf 拷贝到LCD显存指定区域,128*96像素 void LCD_ShowImage_DMA2D(uint16_t *dest, uint16_t *src, uint16_t w, uint16_t h) { // DMA2D配置为内存到内存拷贝,16位像素 DMA2D_HandleTypeDef hdma2d; hdma2d.Instance = DMA2D; hdma2d.Init.Mode = DMA2D_M2M; // 内存到内存 hdma2d.Init.ColorMode = DMA2D_RGB565; // 16位色 hdma2d.Init.OutputOffset = 0; hdma2d.LayerCfg[1].InputOffset = 0; hdma2d.LayerCfg[1].InputColorMode = DMA2D_INPUT_RGB565; hdma2d.LayerCfg[1].AlphaMode = DMA2D_NO_MODIF_ALPHA; HAL_DMA2D_Init(&hdma2d); dma2d_handle = &hdma2d; // 启动拷贝,等待完成 while (HAL_DMA2D_Start_IT(&hdma2d, (uint32_t)src, (uint32_t)dest, w, h) != HAL_OK); // 轮询完成标志,或用中断 while (__HAL_DMA2D_GET_FLAG(&hdma2d, DMA2D_FLAG_TC) == RESET); }

第二种思路:如果用FSMC接口的LCD,因为没有硬件图层,刷新必须整屏逐点写,速度慢。这时可以开一个中间缓冲,把整个热像区域写好后再一次性用批量写函数输出。正点原子LCD驱动里有LCD_Fast_DrawPoint或LCD_Color_Fill函数,适合做矩形填充。但请记住:不要每一像素调用一次LCD_DrawPoint,那样光函数调用开销就吃掉大部分时间。

需要特别注意的是LTDC的刷新时机:不要在LTDC正在扫描某一行时修改显存,否则会出现撕裂。探索者的LCD控制器扫描频率一般是60Hz,你的图像刷新可能刚好落在扫描到中间的时刻。解决办法是使用两个缓冲交替显示(ping-pong buffer),一帧写入后台缓冲,等VSync中断时切换显示地址。虽然这个方案在探索者上需要多分配一块显存(比如768×1024×2? 实际上显存本来就够大,但你要确保LTDC能访问这块内存),但撕裂问题值得提前设计。

5. 避坑与排查:黑屏、花屏、温度漂移的五个血泪经验

5.1 现象:I2C读回全0xFFFF或0x0000,设备扫描不到

原因通常是接线错误、上拉电阻缺失、地址左移出问题。MLX90640的7位地址是0x33,写地址应该为0x66,读地址为0x67。很多新人直接把0x33当作8位地址发给I2C,设备不响应。解决:确认地址左移一位;用万用表测SCL/SDA电压是否为高;如果SCL为低,说明有设备拉死总线,可能上电时序不对,断电重启。另外,探索者的PB6/PB7有可能被板载其他外设占用,例如音频编解码器,导致电平被拉低。换空闲引脚(如PE0/PE1)可以解决。

5.2 现象:LCD屏幕有图像但像蒙了一层雾,或者颜色完全错乱

原因很可能是伪彩色映射表的数据格式不对。正点原子LCD例程里常用的颜色值是RGB565,而你如果直接用了RGB888的数值,在16位色下会丢色,看起来偏绿或偏暗。解决:确认颜色宏定义是RGB565(R,G,B)而不是0x00RRGGBB。另外,探索者有些屏(尤其7寸RGB屏)内部颜色顺序是BGR565,这时需要把RGB转换成BGR再写入,否则红色和蓝色互换,热像图的高温区域变成蓝色,傍晚调试容易误判为低温。

5.3 现象:温度数据高得离谱,比如人体35℃测出来变成50℃

这通常是环境温度补偿没做对。MLX90640计算物体温度时需要知道芯片基底温度Ta,这个值存在热敏电阻通道,必须从RAM的768个像素中的某个特定位置读取(官方库会处理)。如果你只取了像素数据而忽略了Ta补偿,计算出来的温度会随开机时间漂移。解决:按官方公式完整计算,至少包含Ta和Kta修正。另一个常见原因是帧率设置太高(比如64Hz),而I2C速度太低导致数据读取跟不上,读到新旧混合帧,温度跳变。把帧率降到8Hz试试。

5.4 现象:图像上半部分和下半部分错位,或者像有两帧叠加

这是没有做帧同步导致的。我调试探索者工程时,因为LCD刷新比较快,差不多10ms就刷新一次,而MLX90640在16Hz模式下每帧要62.5ms才能更新完,导致每次读取RAM时,RAM区的上半区是新的子页,下半区还是旧子页。解决:必须按3.3节的方法等待状态寄存器的子页翻转,再启动一次完整的RAM读取。另外,如果读取过程中花了太长时间(超过一个帧周期),下一次读取时子页又变了,尽量保证一帧RAM读取时间小于帧周期的60%,也就是在16Hz下要小于37ms,软件I2C读768个字节能做到约20ms,属于安全范围。

5.5 现象:LCD刷新闪烁,而且字体显示有毛边

这个要从三层看:第一,LTDC显存没有对齐;第二,图像缓冲和LCD扫描重叠;第三,写入字体时用的是叠加模式,透明度配置错误。解决方法:正点原子例程里推荐显存数组放在32位对齐的地址上,可以用__attribute__((aligned(32)));或者使用DMA2D的Alpha模式做文字叠加。如果显示中文温度单位“℃”乱码,不是编码问题,而是字库里的点阵数据是B/W格式,需要转成RGB565的字体背景,或者先擦除背景再写字。LCD亮度方面,如果感觉图像整体发白,可以把背光PWM调到合适的占空比,不要直接降到最低,否则MLX90640的伪彩色低亮区域会看不清。

6. 把热像图做得更实用:温度标签、帧率控制和实时刷新技巧

当你能稳定显示MLX90640的伪彩色热像图后,下一步是让它更像一个可用工具。我第一个推荐做的是“十字光标+中心点温度显示”:用LTDC的第二个图层画一个十字线,然后读取对应像素温度值,用正点原子LCD的字体显示在屏幕左上角,比如“36.5℃”。注意字体的绘制不要占用主循环太多时间,建议先把整行字符串准备好,一次性写入,而不是逐字调用。若要在热像图上标记最高温度点,遍历一遍目标温度矩阵找最大值即可,在坐标转换时别忘了缩放系数:源温度是32×24,插值后是128×96,光标位置需要除以比例因子。

第二个技巧是控制帧率。MLX90640在8Hz模式下图像最稳定,但人眼感觉卡,16Hz又容易读帧超时。我一般设成8Hz输出,但LCD上保留上一帧插值结果,再叠加一个透明度动画指示刷新状态。这样做能缓解主观卡顿感,又不会让I2C忙不过来。如果以后换更高性能的MCU,还可以把插值结果缓存成定点数组,用DMA2D直接搬运,效果更丝滑。

最后一个技巧是校准温度范围。工业检测场景里,你关心的不是整幅图的绝对温度,而是“目标区域比环境高多少”。我习惯在启动时读取周围环境温度均值作为基线,显示时用“当前温度 - 基线”作为伪彩色映射的输入,这样热点更醒目。基线可以是MLX90640自带的Ta,或者取画面边缘区域的平均值。这种差分显示在检测发热电阻、MOS管短路时非常有价值。正点原子DM40示波器可以用来观察I2C时序在长跑后是否出现毛刺,若发现上升沿变缓,就要检查上拉电阻和线缆长度。这些细节都是血泪换来的,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询