资讯动态

STM32 F103C8T6 驱动 AHT10 温湿度传感器:I2C 时序与数据换算详解

发布时间:2026/10/13 17:45:25 来源:尧图企业网站定制
简介面向嵌入式开发者与STM32初学者的AHT10温湿度传感器驱动学习资源基于STM32 F103C8T6平台覆盖I2C接口配置、通信时序、数据解析及低功耗管理要点适合在环境监测、智能家居等项目中进行传感器集成与调试。压缩包共227个文件以c源码、h头文件、o目标文件及d依赖文件为主体另含工程配置、汇编启动文件、hex烧录文件与调试相关文件可支撑从源码阅读、编译构建到下载验证的完整流程包体大小6.12MB。资源已有1100余人学习工程基于标准外设库搭建用户可通过i2c.c等驱动代码快速理解传感器命令序列也可参考USART等外设联动实现便于在此基础上继续扩展功能。1. AHT10 与 F103C8T6 的第一次照面别急着读数据拿到一份标题为「STM32 F103C8T6学习笔记13AHT10温湿度传感器模块.rar」的资源包我第一反应不是解压而是先确认你当前处在哪个阶段。AHT10 是一颗 I2C 接口的温湿度传感器价格便宜、精度够用但很多人在 F103C8T6 上把它跑起来后遇到的第一组数永远是 0xFF 或者一个恒定不变的旧值。这不是传感器坏了也不是芯片翻车而是上电时序、测量等待时间和数据换算这三件事没对齐。这篇笔记会把从接线、底层 I2C 时序、驱动函数到温湿度换算公式完整走一遍适合正在用 F103C8T6 做环境监测、小家电或毕设项目、需要自己手动移植传感器驱动的开发者。2. 读懂 AHT10 的 I2C 通信机制地址、命令与硬件接线2.1 一条 I2C 总线上只有两个角色AHT10 的地址与应答逻辑AHT10 的工作电压是 2.2V 到 3.6V湿度分辨率 0.024%RH温度分辨率 0.01°C典型精度大约在 ±2%RH 和 ±0.3°C。对大多数环境监测场景来说这个精度足够日常使用。它的接口是标准的 I2C7 位设备地址固定为 0x38这个地址在出厂时已经固化不能像某些 EEPROM 那样通过引脚改成 0x39 或 0x3A。也就是说如果你打算在一条 I2C 总线上挂两颗 AHT10它们会互相争抢后面的章节会提到对应的处理办法。关于地址还有一个非常容易踩的写法差异当主机向 AHT10 发送数据时I2C 总线上的写地址是0x38 1 0x70读地址是0x71。我在工程里习惯直接用 0x70 和 0x71因为我的模拟 I2C 发送函数要求传入完整的 8 位地址字节。但有些例程的 I2C 发送函数会自动做一次移位运算如果你再额外左移一次设备就永远不会有 ACK 回应。移植代码前先确认你用的是“7 位地址 自动移位”还是“8 位地址直接发送”。AHT10 上有几条核心命令上电后需要发初始化命令 0xE1后面跟两个字节 0x08、0x00每次测量前需要发触发命令 0xAC后面跟 0x33、0x00如果工作异常可以发软复位命令 0xBA 让它回到初始状态状态寄存器则可以由 0x71 读取。下面这张表是常用命令速查命令名称命令字节后续参数使用时机初始化校准0xE10x08, 0x00每次上电后执行一次触发测量0xAC0x33, 0x00每次读取数据前执行软复位0xBA无数据异常或卡死时恢复读取状态0x71无可查询忙状态与校准状态2.2 硬件接线PB6/PB7 与 4.7k 上拉电阻AHT10 模块通常引出四个引脚VCC、GND、SCL、SDA。SCL 是时钟线SDA 是数据线。我在 F103C8T6 上最常见的接法是让 SCL 接 PB6、SDA 接 PB7这两条引脚默认就是 I2C1 的位置后续如果要用硬件 I2C 也能直接复用。如果你用的是其他引脚只要不是被 JTAG 占用的特殊引脚GPIO 配置改成对应端口即可。AHT10 模块引脚F103C8T6 引脚说明VCC3.3V不能接 5V超过 3.6V 会损坏传感器GNDGND共地必须保证SCLPB6时钟线开漏输出SDAPB7数据线开漏输出VCC 与 GND 之间100nF 电容尽量靠近模块引脚放置我一般会在 VCC 和 GND 之间加一个 100nF 的陶瓷电容做电源滤波减少电源纹波对湿度数据的干扰。I2C 总线是漏极开路结构SDA 和 SCL 必须有上拉电阻才能工作典型值是 4.7kΩ 到 3.3V。市面的大多数 AHT10 模块已经板载了上拉电阻但如果你用的是裸芯片或者自己画的转接板千万别忘了上拉。没有上拉电阻时总线空闲时 SDA 电平不确定读回来的数据大概率全是 0xFF。2.3 为什么我优先选 GPIO 模拟 I2C而不是 F103 的硬件外设F103C8T6 内部有硬件 I2C 外设理论上配置好后能自动处理起始条件、停止条件、应答信号看起来比 GPIO 模拟更省事。但在实际项目中F1 系列的硬件 I2C 有几个让人头疼的地方总线忙标志位容易被异常时序卡住事件标志位的等待逻辑在中断优先级配置不当时容易死循环排查问题时 I2C 状态寄存器像黑匣子一样看不透。对 AHT10 这种几十毫秒才读一次的传感器GPIO 模拟方式的 CPU 占用完全可以忽略。模拟 I2C 的最大优势是时序透明每一个时钟沿、每一位数据、每一次应答都能用逻辑分析仪或直接在调试器里看到。传感器驱动出问题时你能准确说出是卡在 START 阶段、SCL 没拉高还是从机没有 ACK而不是对着硬件状态寄存器猜。所以这篇笔记后续代码全部使用 GPIO 模拟 I2C这套底层代码在任何 STM32 系列上都能二次复用。如果以后你执意要用硬件 I2C至少应该先用模拟方式验证传感器是好的再做外设移植。3. 动手移植驱动先写 I2C 总线层代码3.1 引脚初始化开漏输出让“读”不用切模式很多网上例程在模拟 I2C 时会把 SDA 在输入模式和输出模式之间反复切换这种做法虽然能用但代码看起来乱还容易漏掉方向切换导致的时序问题。我习惯把 SCL 和 SDA 都初始化为开漏输出初始状态下 SDA 输出高电平也就是“释放总线”。当 AHT10 需要拉低 SDA 应答时它直接把电平拉低主控代码读取引脚电平就能拿到 ACK 或数据位。/* 初始化 I2C 引脚PB6SCL, PB7SDA */ void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_6 | GPIO_PIN_7; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_OD; /* 开漏输出 */ GPIO_InitStruct.Pull GPIO_PULLUP; /* 内部上拉 外部上拉 */ GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; /* 高速模式利于沿变化 */ HAL_GPIO_Init(GPIOB, GPIO_InitStruct); /* 初始状态SCL 和 SDA 都释放 */ HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6 | GPIO_PIN_7, GPIO_PIN_SET); }这里把模式配置成GPIO_MODE_OUTPUT_OD开漏输出配合外部上拉SDA 写 1 时实际上是把引脚置为高阻从机才能拉低总线通信写 0 时引脚被主动拉低。相比推挽输出开漏模式不会出现主控强行拉高 SDA 导致从机无法应答的情况。内部上拉只是兜底强度不够长线传输时仍然建议在模块端保留 4.7kΩ 外部上拉。3.2 四个时序函数Start、Stop、SendByte 与等待 ACKI2C 通信的底层就四个动作起始条件、停止条件、发送字节、等待应答。起始条件是在 SCL 为高电平期间把 SDA 拉低停止条件是在 SCL 为高电平期间把 SDA 拉高。数据位传输要求 SCL 低电平时 SDA 变化SCL 高电平时 SDA 保持稳定。/* 操作宏方便后续替换到其他引脚 */ #define I2C_SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define I2C_SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define I2C_SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define I2C_SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define I2C_SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7) /* 总线延时72MHz 主频下空循环大约 1~2us */ static void I2C_Delay(void) { volatile uint8_t i 30; while (i--); } /* 起始条件SCL 高电平期间 SDA 拉低 */ static void I2C_Start(void) { I2C_SDA_H(); I2C_SCL_H(); I2C_Delay(); I2C_SDA_L(); I2C_Delay(); I2C_SCL_L(); I2C_Delay(); } /* 停止条件SCL 高电平期间 SDA 拉高 */ static void I2C_Stop(void) { I2C_SDA_L(); I2C_SCL_H(); I2C_Delay(); I2C_SDA_H(); I2C_Delay(); }I2C_Delay里的循环次数不是一个固定真理它取决于编译优化等级和实际频率。如果你发现传感器偶尔无应答可以把循环次数往大调如果接逻辑分析仪看到波形占空比明显高于 50%可以往小调。AHT10 支持 100kHz 和 400kHz 速率的 I2C空循环 30 次左右换算下来接近几百千赫兹实际使用中只要从机能正常 ACK慢一点反而更稳定。发送字节时每次先把要发送的位放到 SDA然后拉高 SCL 让从机采样再拉低 SCL 准备下一位。高位先发这是 I2C 的固定协议格式。/* 向总线发送一个字节 */ static void I2C_SendByte(uint8_t dat) { for (uint8_t i 0; i 8; i) { if (dat 0x80) I2C_SDA_H(); else I2C_SDA_L(); dat 1; I2C_Delay(); I2C_SCL_H(); I2C_Delay(); I2C_SCL_L(); I2C_Delay(); } } /* 等待从机应答返回 0 表示有 ACK返回 1 表示 NACK */ static uint8_t I2C_WaitAck(void) { uint8_t ack; I2C_SDA_H(); I2C_Delay(); I2C_SCL_H(); I2C_Delay(); ack I2C_SDA_READ(); I2C_SCL_L(); I2C_Delay(); return ack; }注意I2C_WaitAck的返回值约定从机正常应答时会把 SDA 拉低所以读到 0 表示 ACK读到 1 表示 NACK。后面的驱动代码里我用err | I2C_WaitAck()来累积错误标志AC K 时 err 保持 0。网络上一部分例程的返回值定义是相反的移植时一定要统一否则判断条件全反代码跑起来会出现“明明连接正常却一直报错”的迷惑现象。3.3 ReadByte 与 ACK/NACK 收尾技巧读取从机数据时主控释放 SDA在每个 SCL 高电平期间读取引脚状态。读完 8 位后主控需要主动发送一个 ACK 或 NACK。两设备之间读多字节时主控对除了最后一个字节以外的所有字节都要回 ACK告诉从机“继续发”读到最后一个字节时回 NACK告诉从机“可以停了”。/* 读取一个字节ack1 表示主控回 ACKack0 表示主控回 NACK */ static uint8_t I2C_ReadByte(uint8_t ack) { uint8_t dat 0; I2C_SDA_H(); for (uint8_t i 0; i 8; i) { I2C_SCL_H(); I2C_Delay(); dat (dat 1) | I2C_SDA_READ(); I2C_SCL_L(); I2C_Delay(); } if (ack) I2C_SDA_L(); else I2C_SDA_H(); I2C_SCL_H(); I2C_Delay(); I2C_SCL_L(); I2C_Delay(); I2C_SDA_H(); return dat; }这段代码里的 ACK/NACK 时序同样要遵守“SCL 低电平时改变 SDASCL 高电平时数据稳定”。读完 8 位数据后 SCL 停在低电平此时把 SDA 设置为对应电平再拉高 SCL 产生一个应答时钟最后拉低 SCL 并释放 SDA。AHT10 的数据帧固定为 6 个字节所以读取时的 ack 参数按“前 5 个字节传 1、最后一个字节传 0”来填。这个习惯要从底层养成后面 AHT10 读取函数里会直接调用。4. 读取与换算从原始 6 字节到温湿度数值4.1 上电初始化0xE1 规定的两次握手AHT10 冷启动后不能马上测量。常见做法是系统上电后先延时 50ms让传感器电源稳定做一次软复位再发送初始化命令。软复位命令是 0xBA发送后等待 20ms。初始化命令是 0xE1、0x08、0x00其中 0x08 表示开启内部校准0x00 是保留字节。这组命令必须在同一次 I2C 事务里连续发送不能拆成三次单独写操作。#define AHT10_ADDR_W 0x70 /* 写地址 */ #define AHT10_ADDR_R 0x71 /* 读地址 */ /* AHT10 软复位 */ uint8_t AHT10_SoftReset(void) { uint8_t err 0; I2C_Start(); I2C_SendByte(AHT10_ADDR_W); err | I2C_WaitAck(); I2C_SendByte(0xBA); err | I2C_WaitAck(); I2C_Stop(); HAL_Delay(20); return err; } /* AHT10 初始化返回 0 表示成功 */ uint8_t AHT10_Init(void) { uint8_t err 0; HAL_Delay(50); err | AHT10_SoftReset(); I2C_Start(); I2C_SendByte(AHT10_ADDR_W); err | I2C_WaitAck(); I2C_SendByte(0xE1); err | I2C_WaitAck(); I2C_SendByte(0x08); err | I2C_WaitAck(); I2C_SendByte(0x00); err | I2C_WaitAck(); I2C_Stop(); HAL_Delay(10); return err; }有些模块刚上电时直接读数会返回 0%RH 和 -50°C这不是传感器坏了而是没有执行初始化校准。初始化完成后再等 10ms才开始第一次测量。这里的软复位不是必须的但能作为兜底如果你的程序曾经让 AHT10 进入异常状态复位后重新初始化比重新断电可靠得多。所有 I2C 错误都用按位或累加任何一位出错最终返回非零主循环可以捕获到。4.2 触发测量与读取 6 字节延时比状态轮询更省心AHT10 属于“单次触发型”传感器主机发送 0xAC、0x33、0x00 后传感器才开始内部测量测量时间标称 80ms 左右。触发完成后主机需要重新发起一次读操作连续读取 6 个字节。触发命令和读取命令是两次独立的 I2C 事务中间必须等待足够时间。/* 触发一次测量 */ uint8_t AHT10_TriggerMeasure(void) { uint8_t err 0; I2C_Start(); I2C_SendByte(AHT10_ADDR_W); err | I2C_WaitAck(); I2C_SendByte(0xAC); err | I2C_WaitAck(); I2C_SendByte(0x33); err | I2C_WaitAck(); I2C_SendByte(0x00); err | I2C_WaitAck(); I2C_Stop(); return err; } /* 读取 6 字节原始数据 */ uint8_t AHT10_ReadRaw(uint8_t buf[6]) { uint8_t err 0; I2C_Start(); I2C_SendByte(AHT10_ADDR_R); err | I2C_WaitAck(); for (uint8_t i 0; i 6; i) { if (i 5) buf[i] I2C_ReadByte(0); else buf[i] I2C_ReadByte(1); } I2C_Stop(); return err; }读回的第一个字节 buf[0] 是状态字节它的最高位 bit7 在忙碌时为 1测量完成后变为 0bit3 为 1 表示校准已经完成。最常见的失败场景是触发测量后只等了 20ms 就开始读此时 buf[0] 最高位还是 1后面数据也都是旧值。我一般触发后固定等待 100ms必要时调到 150ms留足余量。有些资料里推荐轮询状态寄存器但固定延时对绝大多数场景足够代码也更简单。4.3 从原始字节到物理量20 位换算公式AHT10 的湿度数据和温度数据都是 20 位无符号数存储方式是从第 1 字节到第 5 字节按位拼接。湿度占 buf[1] 的完整 8 位、buf[2] 的完整 8 位、buf[3] 的高 4 位温度占 buf[3] 的低 4 位、buf[4] 的完整 8 位、buf[5] 的完整 8 位。很多新手把 buf[0] 状态字节也算进湿度结果湿度值直接放大 1 倍怎么算都不对。float AHT10_GetHumidity(uint8_t buf[6]) { uint32_t hum_raw; hum_raw ((uint32_t)buf[1] 12) | ((uint32_t)buf[2] 4) | ((uint32_t)buf[3] 4); return hum_raw * 100.0f / 1048576.0f; } float AHT10_GetTemperature(uint8_t buf[6]) { uint32_t temp_raw; temp_raw ((uint32_t)(buf[3] 0x0F) 16) | ((uint32_t)buf[4] 8) | (uint32_t)buf[5]; return temp_raw * 200.0f / 1048576.0f - 50.0f; }湿度公式里的 1048576 是 2 的 20 次方对应传感器的 20 位 ADC 量程乘以 100 得到 0%RH 到 100%RH。温度公式乘以 200 再减 50对应 -50°C 到 150°C 的物理量程。如果温度算出来总是 -50°C说明原始值全部是 0大概率是初始化失败或等待时间不够如果湿度算出来总是接近 0%RH优先检查 buf 索引是否错位。4.4 一个可复制的主循环把驱动串起来驱动函数写好后主循环就非常简单。每次读取前触发测量延时 100ms读回 6 字节再判断状态字节最高位。如果最高位为 1说明数据还没准备好这次值直接丢弃。两次测量之间我建议至少间隔 1 秒。AHT10 不是连续采样型传感器你触发一次它才工作一次间隔太短只是浪费总线时间并放大数据波动。int main(void) { uint8_t raw[6]; float temp, hum; HAL_Init(); I2C_GPIO_Init(); AHT10_Init(); while (1) { if (AHT10_TriggerMeasure() 0) { HAL_Delay(100); if (AHT10_ReadRaw(raw) 0) { if ((raw[0] 0x80) 0) { hum AHT10_GetHumidity(raw); temp AHT10_GetTemperature(raw); printf(Temp: %.2f C, Hum: %.2f %%RH\r\n, temp, hum); } } } HAL_Delay(1000); } }这里的printf需要你的工程里已经移植了串口重定向。如果暂时没有串口可以把 raw 数组原样发出来在 PC 端用串口助手看十六进制值。调试阶段先确认原始字节能变化再确认换算后的物理量合理不要一上来就追求滤波和精度。第一次跑通后把读到的数据放到房间、冰箱保鲜层和手边哈气测试观察数据是否随环境明显变化这一步能过滤掉一半以上的玄学问题。5. 常见问题防坑指南为什么数据看起来像“没有了”5.1 读数全是 0xFF设备永远不回 ACK现象读回的 6 字节全部是 0xFF或者 I2C_WaitAck 每次都返回 1调用初始化函数后 err 一直非零。原因这是最常见的接线或配置问题。SDA 和 SCL 是漏极开路结构如果总线上没有上拉电阻空闲时 SDA 引脚电平不确定主控读到的一直是高电平所以全是 0xFF。另一种常见原因是 GPIO 被配置成了推挽输出主控把 SDA 拉高后从机无法把它拉低ACK 永远发不出来。还有一种情况是 SDA 和 SCL 接反了或者 VCC 接到了 5V 导致传感器不工作。解决先用万用表量 SDA 和 SCL 对地电压空闲状态应该都在 3.3V 左右。如果 SDA 只有 0V检查上拉电阻是否焊上如果 SDA 是 3.3V 但通信还是失败检查引脚模式是否为开漏输出。再写一个最简单的 I2C 地址扫描函数发一次起始条件、发 0x70、等待 ACK看返回值。注意你的发送函数是否已经对地址做左移重复移位也会导致无 ACK。5.2 温湿度读数恒定不变数值还特别规律现象每次读取都是 25.4°C、55.3%RH或者一直显示 100°C 和 0%RH 这种极端值重启后依然不变。原因大概率是读取到的不是新测量结果。AHT10 只有收到 0xAC 触发命令后才更新内部数据如果主循环没有触发读回来的是上次残留数据。更隐蔽的情况是触发了但延时不足 80ms 就发起读操作传感器还处于忙状态返回的仍然是旧数据。还有一类原因是 buf 索引错位把状态字节当作湿度高位换算结果当然恒定为某个固定值。解决把触发延时从 100ms 改成 200ms 再试将读回的 6 个原始字节用十六进制打印出来手动观察两次读到的字节序列有没有变化。如果 buf[0] 最高位一直为 1说明你总是在传感器忙碌时读取需要加大延时。如果原始数据在变但换算结果不变检查 4.3 节里的位拼接代码特别是 buf[3] 的低 4 位归属。5.3 湿度不断跳变温度却很稳定现象温度读数几乎不变湿度在真实值附近上下跳动短时间波动超过 ±5%RH。原因湿度数据本身容易受气流、电源纹波和测量频率影响。AHT10 的湿敏电容对环境影响敏感人呼一口气、手从旁边掠过都会让瞬时值波动。如果电源纹波太大湿度原始值的跳变也会更明显。测量间隔太短时数据没有足够时间沉淀表现出来就是数值乱跳。解决在 VCC 与 GND 之间补一个 100nF 电容采样间隔拉长到 1 秒以上。软件上做 5 到 10 次滑动平均能明显压低高频抖动。同时注意传感器不要紧靠单片机、开关电源这类发热源否则温度读数是“器件附近”的温度而不是环境温度。判断是否正常可以用密封袋放一张湿纸巾等 10 分钟看湿度是否上升这个方法比对着空气瞎猜可靠得多。5.4 硬件 I2C 外设挂死时先怀疑状态机现象程序卡在等待硬件 I2C 事件标志的位置或者运行一段时间后 I2C 再也不应答必须整板复位才能恢复。原因F103 的硬件 I2C 在异常总线时序下总线忙标志位可能没有被正确清除主控状态机陷入一个无法退出的状态。这个问题和具体时序、中断抢占都有关系排查成本高。如果是在学习阶段遇到最稳妥的办法是暂时换回 GPIO 模拟 I2C先把传感器本身的时序跑通。解决坚持用硬件 I2C 的话给所有等待事件标志的循环加超时退出超时后调用外设复位函数。不要用无限等待的写法否则一次异常就让整个系统锁死。我的习惯是先模拟 I2C 验证模块确认传感器没问题后再考虑硬件外设的移植。如果你只是在做环境监测模拟 I2C 完全可以长期使用不必追求硬件外设的效率。6. 进阶滤波、多传感器接入与可信度验证单点 AHT10 驱动跑通只是开始真正要把它用进产品还需要做数据滤波和可信度验证。模拟 I2C 的代码在读取速度上不快但每次读回来的原始数据连续做滑动平均能明显改善湿度抖动。简单做法是在主循环里保存最近 N 次的值取平均后再输出。这个功能用数组加取模索引就能实现几十行代码不复杂。注意第一次启动时数组还没填满直接求平均会偏低可以等缓存区满后再启用滤波。AHT10 的地址固定为 0x38所以同一条 I2C 总线上不能并联两颗同样的传感器。我的经验是如果只需要两个点直接用两路 GPIO 模拟 I2C分别占用不同的引脚对如果数量更多可以考虑用多路 I2C 开关芯片通过切换通道解决冲突。不要在软件里试图“伪装地址”I2C 从机设备地址是硬件决定的AHT10 不支持动态地址修改硬写只会让你陷入无休止的 ACK 失败排查。最后说可信度验证的方法。找一个稳定的环境做参照比如把传感器和一台手持温湿度计放在同一位置记录十分钟内的两组数据温差超过 1°C 或湿度差超过 5%RH 就要检查传感器老化或贴片虚焊。水汽验证可以用湿润纸巾温度验证可以用手捏住传感器。这些方法都很土但在没有恒温恒湿箱的项目阶段是最直接有效的检测手段。我自己移植每一颗新传感器时习惯先把“单次触发、打印原始字节”跑通确认数据能变再做滤波、单位换算和流程封装。这个习惯帮我省掉了太多看起来像玄学的 I2C 通信问题。AHT10 的驱动难度不在于命令多而在于你能否把一个完整的时序链路走完。希望这篇笔记能帮你少踩一次初始化顺序的坑如果你第一次读到的数据还是 0xFF请按第 5 章的方法一步步排查多数情况下是上拉电阻或地址移位的问题不是芯片坏了。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑