⚠️ 产品声明 / Product Disclaimer
非量产产品,仅供工程验证,不承诺符合 RoHS。 Non-mass-production product; for engineering verification only. RoHS compliance is not guaranteed.
Overview
The official base project already ships with the SHT3x driver (components/SHT3x) and completes initialization and periodic reads inside the sht3x_read_task task; the sht3x query tool was registered in Create an MCP Tool, Chapter 5. This chapter covers the wiring and a hands-on test of AI temperature/humidity queries.
SHT3x talks over an I²C bus (two wires: SDA/SCL). This board uses software-emulated I²C (the
Bsp/i2cdirectory), so any two GPIOs can serve as I²C; this project uses PB6 (SDA) / PB7 (SCL).
The official base project already ships with the SHT3x driver (components/SHT3x) — no need to copy anything. Only if you are starting from a bare project do you need to copy it from the SCBB library:
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\
The official base project already binds the I²C macros to the software I²C driver (Bsp/i2c). If you start from a bare project you must bind them manually; the code looks like this (verified):
/* axk_sht3x.h -- I2C operation function bindings */
#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 default I2C address
What bsp_i2c_##_func means: when the driver writes AXK_SHT3X_I2C_ACLL(start), it expands at compile time to bsp_i2c_start(). So the project must contain a software I²C BSP implementation (the Bsp/i2c directory — already implemented in this project):
/* Bsp/i2c/stm32f10x_bsp_i2c.h -- implemented in this project */
void bsp_i2c_init(void); // init (PB6/PB7 configured as open-drain outputs)
void bsp_i2c_start(void); // start condition
void bsp_i2c_stop(void); // stop condition
void bsp_i2c_send_ack(u8 ack); // send ACK
u8 bsp_i2c_wait_ack(void); // wait for ACK
void bsp_i2c_send_byte(u8 _dat); // send one byte
u8 bsp_i2c_read_byte(void); // read one byte
The official base project’s components/CMakeLists.txt already includes the SHT3x directory, and the sht3x_read_task task already implements initialization and periodic reads (0x2c06 mode, 10Hz). Starting from a bare project, the code looks like this:
/* Init: returns 0 on success */
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!"); }
/* Periodic read: 0x2c06 = periodic measurement mode, 10Hz, high repeatability */
double temperature = 0.0, humidity = 0.0;
res = axk_sht3x_read(0x2c06, &temperature, &humidity);
if (res == 0) {
g_temp = temperature; // keep it global for the OLED / MCP tool to use
g_hum = humidity;
} else {
log_error("sht3x read error: %d", res);
}
🧩 The temperature/humidity sensor on the 9Mod MCPBoard is already wired on board (SHT30/GXHT30C, broken out on a 4-pin header at the board edge) — no wiring needed. The table below is for identifying the pins, or for reference when adding an external SHT30 module.
| SHT30 Module | Connects to Board |
|---|---|
| SDA | PB6 |
| SCL | PB7 |
| VCC | 3.3V |
| GND | GND |
⚠️ Neighbors on the same I²C bus: the SHT3x (address 0x44) and the CH224 (address 0x22) share the PB6/PB7 software I²C bus; the addresses do not conflict, so they can coexist. But while debugging, both devices must be connected — otherwise the bus is disturbed by the state of the unconnected device and neither can be read.
The callback and registration of the sht3x query tool were completed in Create an MCP Tool, Chapter 5 (sht3x_query_set_handler reads temperature/humidity and replies with JSON). The core callback logic is below (for reference):
/* ---- SHT3x temperature/humidity query tool callback ---- */
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); // query = execute, reuse it directly
}
/* Register (inside StartDefaultTask, right after 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);
Don’t forget: in
emMCP_config.h,MCP_SERVER_TOOL_NUMBLE_MAXis already set to 7 (this project has 4 tools — enough, no change needed); if you use the manual JSON registration mode, the tool’s JSON must be included in themcp_tools_fmtstring (see Create an MCP Tool).
-
Build and flash; the serial log should show:
[INFO] sht3x init OK! -
Manual serial test (USART2, 115200):
{"role":"AI","msgType":"MCP","data":{"name":"sht3x","args":{}}}Expected reply:
mcp-responsive {"role":"MCU","msgType":"status","data":{"temperature":26.5,"humidity":45.2}} -
AI voice test: say “Xiao An, what’s the temperature now?” (小安,现在温度多少?) or “Check the temperature and humidity” (查询温湿度); the AI calls the sht3x tool and replies with the result by voice.
FAQ & Troubleshooting
🔧 The log keeps showing sht3x not driver / SHT3x ERR
Cause: ① wrong wiring ② wrong power supply ③ the bus is disturbed by the CH224
Fix: ① SDA→PB6, SCL→PB7 (reversing them is a guaranteed failure) ② connect the module's VCC to 3.3V (5V is also fine for modules with level shifting) ③ the CH224 must be connected too — on a shared I²C bus, one missing device can affect the other
🔧 Readings are always 0 or -1
Cause: I²C communication failure
Fix: check the bsp_i2c pin macros (whether GPIO_SDA/GPIO_SCL in the Bsp/i2c header are PB6/PB7); use an oscilloscope or logic analyzer to look at the SDA/SCL waveforms
🔧 The temperature/humidity reading is clearly wrong (e.g. 80 degrees)
Cause: ① the sensor is covered or close to a heat source ② poor contact in the wiring
Fix: move the sensor away from the heat source; reseat the Dupont wires; check that GND is a solid common ground
🔧 Temperature/humidity reads double or half
Cause: measurement mode or data parsing problem
Fix: use the standard axk_sht3x_read(0x2c06, ...) mode; make sure the part really is an SHT30 and not another model (SHT31/35 are compatible but calibrated slightly differently)
🔧 Only one of the two I²C devices (SHT3x + CH224) can be read
Cause: bus timing / pull-up problem
Fix: check that PB6/PB7 have external pull-up resistors (or that the modules include them); the two devices have different addresses (0x44 / 0x22) and do not conflict, so it is usually a wiring issue
🔧 The query tool replies with an error data:"false"
Cause: a momentary read failure (intermittent)
Fix: add a "retry once" path in the code; check that the sensor's power supply is stable
🔧 AI voice reply for the temperature/humidity query fails; the log shows read ok: {"temperature":,"humidity":} (the values are empty)
Cause: ⚠️ floating-point printf is not linked in (newlib-nano does not support %f by default)
Fix: see "Floating-Point printf Link Option" below; it affects every reply that uses %.1f (sht3x temperature/humidity, ch224 voltage)
🔧 Garbled logs / dropped bytes (e.g. Chinese text turns into ���, JSON fields are truncated)
Cause: several tasks print logs at the same time and HAL_UART_Transmit is not reentrant, so they corrupt each other
Fix: see the "Log Mutex" section below
🔧 The AI can't find the tool after adding sht3x
Cause: the tool limit is too low
Fix: keep MCP_SERVER_TOOL_NUMBLE_MAX at 7 (enough for 4 tools); if you add more custom tools, remember to +1 and rebuild
🔧 The temperature returned by the manual JSON test is a string, not a number
Cause: reply format problem
Fix: make sure snprintf produces {"temperature":26.5,"humidity":45.2} (numbers without quotes) so the AI can parse it correctly
Sensor Status Quick Check
After power-up, the serial log [INFO] sht3x init OK! means communication is normal, and [INFO] ch224 init OK! means the CH224 is normal. If both are fine but the reading is 0, check the Dupont-wire contact first — poor contact is by far the most common failure of this kind.
⚠️ Must Read: Floating-Point printf Link Option (root cause of empty %.1f output)
Symptom: the AI voice reply for the temperature/humidity query says "query failed", and the serial log shows [sht3x] read ok: {"temperature":,"humidity":} — the temperature and humidity values are empty. Screen/task logs (which use %d) display normally.
Root cause: the project uses newlib-nano (--specs=nano.specs), which by default does not include floating-point formatting code. The %.1f inside snprintf outputs an empty string, so the assembled JSON becomes {"temperature":,"humidity":} (invalid JSON); the AI side fails to parse it → the voice reply fails.
Fix: add -u _printf_float to the link options in the two CMake toolchain files:
# 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 (if using the STARM_HYBRID toolchain)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} --gcc-specs=nano.specs -u _printf_float")After rebuilding, FLASH grows by about 4~5KB (the floating-point formatting code) and %.1f outputs again.
Scope: every tool reply that uses floating-point formatting — in this project both sht3x (temperature/humidity) and ch224_voltage (voltage) use %.1f, so both are fixed at once.
⚠️ Must Read: Log Mutex (garbled logs from multiple tasks)
Symptom: serial logs come out garbled (a Chinese string like 查询温湿度 turns into 查���温湿度), fields are truncated ("msgType" becomes "msgT"), and two log lines are merged into one — yet the AI voice reply is fine (which means the data path is OK; only the log display is broken).
Root cause: log_printf is built on HAL_UART_Transmit (not reentrant). When defaultTask (handling messages) and sht3x_read_task (printing a temperature log every second) log at the same time, their bytes interleave and corrupt each other. The more frequently you log (e.g. on a 1.5Mbps debug port), the more obvious it gets.
Fix: give log_printf a static mutex (components/log/log.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;
/* guard the UART transmission with a mutex so competing tasks don't interleave log bytes */
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);
}
}Key points:
- Use
xSemaphoreCreateMutexStatic(static allocation), not the dynamic version — once the FreeRTOS heap is taken up by the WS2812 buffer, allocation may fail (see the memory notes in the LED Strip Case); log_printfmay only be called from task context (calling it inmain()before the scheduler starts is also safe, because a static mutex does not depend on the heap); never log from an interrupt (xSemaphoreTakewill crash in interrupt context);- If you just want a quick confirmation, temporarily delete the per-second log inside
sht3x_read_task; if the garbling disappears, that proves it was log contention.

