Skip to content

概述

官方基础工程已预置 SHT3x 驱动(components/SHT3x)并在 sht3x_read_task 任务中完成初始化与周期读取;sht3x 查询工具已在创建 MCP 工具第 5 章注册。本章接线并实测 AI 查询温湿度。

SHT3x 走 I²C 总线(SDA/SCL 两根线),开发板使用软件模拟 I²C(Bsp/i2c 目录),任意两个 GPIO 即可当 I²C 用,本项目使用 PB6(SDA)/PB7(SCL)。


🎯本页目标使用官方预置的 SHT3x 驱动(软件 I²C),新增查询工具,实现 AI 查询环境温湿度。
🧰前置条件① 完成 [创建 MCP 工具](./create-mcp-tool-win) ② SHT30 模块(地址 0x44,板上已焊则无需接线)。
🔗相关章节[OLED 显示](./oled-display-win) 章节已在前方掌握屏幕显示,本章温湿度数据可直接用于屏幕显示。
确认官方已预置 SHT3x 模块

官方基础工程已预置 SHT3x 驱动(components/SHT3x),无需复制。若从零工程起步,才需要从 SCBB 库复制:

New-Item -ItemType Directory -Force components\SHT3x
Copy-Item AiPi-SCBB\SHT3x\axk_sht3x.c components\SHT3x\
Copy-Item AiPi-SCBB\SHT3x\axk_sht3x.h components\SHT3x\
确认 I²C 绑定(官方已配好)

官方基础工程已将 I²C 宏绑定到软 I²C 驱动(Bsp/i2c)。从零工程起步时需要手动绑定,写法如下(已验证):

/* axk_sht3x.h —— I2C 操作函数绑定 */
#if __has_include("stm32f10x_bsp_i2c.h")
#include "stm32f10x_bsp_i2c.h"
#define AXK_SHT3X_DELAY_MS(x) delay_ms(x)
#define AXK_SHT3X_I2C_ACLL(_func, ...) bsp_i2c_##_func(__VA_ARGS__)
#else
#error "Please include the appropriate I2C header for AXK_SHT3X."
#endif

#define AXK_SHT3X_ADDRESS 0x44   // SHT3x 默认 I2C 地址

bsp_i2c_##_func 的含义:驱动里写 AXK_SHT3X_I2C_ACLL(start),编译时展开为 bsp_i2c_start()。因此工程中必须有软 I²C 的 BSP 实现(Bsp/i2c 目录,本项目已实现):

/* Bsp/i2c/stm32f10x_bsp_i2c.h —— 本项目已实现 */
void bsp_i2c_init(void);          // 初始化(PB6/PB7 配为开漏输出)
void bsp_i2c_start(void);         // 起始条件
void bsp_i2c_stop(void);          // 停止条件
void bsp_i2c_send_ack(u8 ack);    // 发送应答
u8   bsp_i2c_wait_ack(void);      // 等待应答
void bsp_i2c_send_byte(u8 _dat);  // 发送一个字节
u8   bsp_i2c_read_byte(void);     // 读取一个字节
确认官方已初始化并周期读取

官方基础工程 components/CMakeLists.txt 已包含 SHT3x 目录;sht3x_read_task 任务已实现初始化与周期读取(0x2c06 模式,10Hz)。从零工程起步时的写法如下:

/* 初始化:返回 0 表示成功 */
uint8_t res = axk_sht3x_init();
if (res != 0) { log_error("sht3x not driver"); sht30_is_init = false; }
else          { sht30_is_init = true;  log_info("sht3x init OK!"); }

/* 周期读取:0x2c06 = 周期测量模式,10Hz,高重复性 */
double temperature = 0.0, humidity = 0.0;
res = axk_sht3x_read(0x2c06, &temperature, &humidity);
if (res == 0) {
  g_temp = temperature;   // 保存全局,供 OLED / MCP 工具使用
  g_hum  = humidity;
} else {
  log_error("sht3x read error: %d", res);
}
接线

🧩 九章板温湿度传感器已板载接好(SHT30/GXHT30C,板边 4Pin 排针引出),无需接线。下表供对照认识引脚,或扩展外部 SHT30 模块时参考。

SHT30 模块 接开发板
SDA PB6
SCL PB7
VCC 3.3V
GND GND

⚠️ 同一条 I²C 总线上的邻居:SHT3x(地址 0x44)与 CH224(地址 0x22)共用 PB6/PB7 软 I²C 总线,地址不冲突可共存。但调试时两个设备都要接好,否则总线被未接设备的状态干扰,两个都读不到。

确认 sht3x 工具已注册(第 5 章已完成)

sht3x 查询工具的回调与注册已在创建 MCP 工具第 5 章完成(sht3x_query_set_handler 读取温湿度并以 JSON 回执)。工具回调核心逻辑如下(对照理解):

/* ---- SHT3x 温湿度查询工具回调 ---- */
static void sht3x_query_set_handler(void *arg) {
  (void)arg;
  log_info("[sht3x] query called");
  double temp = 0.0, hum = 0.0;
  if (axk_sht3x_read(0x2c06, &temp, &hum) == 0) {
    char rsp[64];
    snprintf(rsp, sizeof(rsp), "{\"temperature\":%.1f,\"humidity\":%.1f}", temp, hum);
    log_info("[sht3x] read ok: %s", rsp);
    emMCP_ResponseValue(rsp);
  } else {
    log_error("[sht3x] read failed");
    emMCP_ResponseValue(emMCP_CTRL_ERROR);
  }
}
static void sht3x_query_check_handler(void *arg) {
  sht3x_query_set_handler(arg);   // 查询 = 执行,直接复用
}

/* 注册(StartDefaultTask 中,紧跟 relay 之后) */
memset(&t, 0, sizeof(t));
t.name = "sht3x";
t.description = "温湿度查询工具,返回当前温度和湿度";
t.setRequestHandler = sht3x_query_set_handler;
t.checkRequestHandler = sht3x_query_check_handler;
emMCP_AddToolToToolList(&t);

别忘了:emMCP_config.hMCP_SERVER_TOOL_NUMBLE_MAX 已设为 7(本项目 4 个工具,足够,无需修改);若使用手动 JSON 注册模式,需在 mcp_tools_fmt 字符串中包含该工具的 JSON(见 创建 MCP 工具)。

测试
  1. 编译烧录,串口日志应出现:

    [INFO] sht3x init OK!
    
  2. 串口手动测试(USART2,115200):

    {"role":"AI","msgType":"MCP","data":{"name":"sht3x","args":{}}}
    

    预期回执:

    mcp-responsive {"role":"MCU","msgType":"status","data":{"temperature":26.5,"humidity":45.2}}
    
  3. AI 语音测试:说 “小安,现在温度多少?”“查询温湿度”,AI 调用 sht3x 工具后语音回复结果。


常见问题与踩坑提示

🔧 日志一直 sht3x not driver / SHT3x ERR
原因:① 接线错 ② 供电错 ③ 总线被 CH224 干扰
解决:① SDA→PB6、SCL→PB7(接反必挂)② 模块 VCC 接 3.3V(带电平转换的也可 5V)③ CH224 也必须接好,同一 I²C 总线少一个设备都可能影响另一个

🔧 读数一直是 0 或 -1
原因:I²C 通信失败
解决:检查 bsp_i2c 的引脚宏(Bsp/i2c 头文件里 GPIO_SDA/GPIO_SCL 是否为 PB6/PB7);用示波器/逻辑分析仪看 SDA/SCL 波形

🔧 读出的温度湿度值明显不对(如 80 度)
原因:① 传感器被遮挡/靠近热源 ② 接线接触不良
解决:传感器离开热源;重新插拔杜邦线;检查 GND 是否稳定共地

🔧 温度湿度翻倍或一半
原因:测量模式/数据解析问题
解决:用 axk_sht3x_read(0x2c06, ...) 标准模式;确认是 SHT30 而非其他型号(SHT31/35 兼容但校准略有差异)

🔧 两个 I²C 设备(SHT3x + CH224)只有一个能读
原因:总线时序/上拉问题
解决:确认 PB6/PB7 外部有上拉电阻(或模块自带);两个设备地址不同(0x44 / 0x22),不会冲突,通常是接线

🔧 查询工具回执报错 data:"false"
原因:读取瞬间失败(偶发)
解决:代码里加"重试一次"逻辑;检查传感器电源是否稳定

🔧 AI 查询温湿度播报失败,日志里 read ok: {"temperature":,"humidity":}(数值是空的)原因:⚠️ 浮点 printf 未链接(newlib-nano 默认不支持 %f解决:见下方"浮点 printf 链接选项",影响所有用 %.1f 的回执(sht3x 温湿度、ch224 电压)

🔧 日志乱码/丢字节(如中文变 ���、JSON 字段残缺)
原因:多任务同时打日志,HAL_UART_Transmit 非重入互相打坏
解决:见下方"日志互斥锁"说明

🔧 加了 sht3x 工具后 AI 找不到
原因:工具上限不够
解决MCP_SERVER_TOOL_NUMBLE_MAX 保持 7(4 个工具足够);若自定义更多工具记得 +1 并重新编译

🔧 手动 JSON 测试返回的温度是字符串不是数字
原因:回执格式问题
解决:确认 snprintf 生成的是 {"temperature":26.5,"humidity":45.2}(数字无引号),AI 才能正确解析

传感器状态速查

上电后串口日志 [INFO] sht3x init OK! 表示通信正常;[INFO] ch224 init OK! 表示 CH224 正常。两者均正常但读数为 0 时,优先排查杜邦线接触不良——这是最常见的接触类故障。


⚠️ 必读:浮点 printf 链接选项(%.1f 输出为空的根因)

现象:AI 查询温湿度播报"查询失败";串口日志显示 [sht3x] read ok: {"temperature":,"humidity":}——温湿度数值是空的。屏幕/任务日志(用 %d)显示正常。

根因:工程使用 newlib-nano--specs=nano.specs),它默认不包含浮点格式化代码snprintf 里的 %.1f 直接输出空字符串,拼出的 JSON 变成 {"temperature":,"humidity":}(非法 JSON),AI 端解析失败 → 播报失败。

修复:在两个 CMake 工具链文件里给链接选项加 -u _printf_float

cmake
# cmake/gcc-arm-none-eabi.cmake
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --specs=nano.specs -u _printf_float")

# cmake/starm-clang.cmake(如使用 STARM_HYBRID 工具链)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --gcc-specs=nano.specs -u _printf_float")

重新编译后 FLASH 会增加约 4~5KB(浮点格式化代码),%.1f 恢复输出。

影响范围:所有用浮点格式化的工具回执——本项目里 sht3x(温湿度)、ch224_voltage(电压)都用 %.1f,一并修复。


⚠️ 必读:日志互斥锁(多任务日志乱码)

现象:串口日志中文乱码(查询温湿度 变成 查���温湿度)、字段残缺("msgType""msgT")、两行日志拼在一起——但 AI 播报是正常的(说明数据链路没问题,只是日志显示坏了)。

根因log_printf 底层是 HAL_UART_Transmit非重入)。defaultTask(处理消息)和 sht3x_read_task(每秒打温度日志)同时打日志时,字节互相穿插打坏。日志打印得越频繁(如 1.5Mbps 调试口),越明显。

修复:给 log_printf静态互斥锁components/log/log.c):

c
#include "FreeRTOS.h"
#include "semphr.h"

static StaticSemaphore_t g_log_mutex_buf;
static SemaphoreHandle_t g_log_mutex = NULL;

void log_printf(const char *format, ...) {
  char buffer[LOG_BUFFER_SIZE];
  va_list args;
  va_start(args, format);
  int len = vsnprintf(buffer, LOG_BUFFER_SIZE, format, args);
  va_end(args);
  if (len <= 0) return;

  /* 互斥保护串口发送,避免多任务竞争导致日志字节交错 */
  if (g_log_mutex == NULL) {
    g_log_mutex = xSemaphoreCreateMutexStatic(&g_log_mutex_buf);
  }
  if (g_log_mutex != NULL &&
      xSemaphoreTake(g_log_mutex, pdMS_TO_TICKS(20)) == pdTRUE) {
    HAL_UART_Transmit(&huart1, (uint8_t *)buffer, len, 100);
    xSemaphoreGive(g_log_mutex);
  }
}

要点:

  • xSemaphoreCreateMutexStatic(静态分配),不要用动态版——FreeRTOS 堆被 WS2812 缓冲占用后可能分配失败(见灯带案例内存说明);
  • log_printf 只能在任务上下文调用(main() 里调度器启动前调用也安全,因为静态锁不依赖堆);不要在中断里打日志xSemaphoreTake 在中断上下文会崩);
  • 若只想快速验证,可先临时删掉 sht3x_read_task 里的每秒日志,确认乱码消失即证明是日志竞争。

下一步

Released under the MIT License. Build Time 2026-08-07 22:59:19