一、任务活动介绍
本次参与的活动为“WeDesign第7期:纳芯微传感器 + MCU芯片/评估板”,WeDesign活动是硬禾学堂发起的“一起设计、一起体验”活动。本项目选择了纳芯微温度传感器 NST461-DQNR和绝压传感器 NSPAD1N200DR04,任务要求如下:
- 制作电路模块,读取NST461-DQNR的本地温度和远程温度,读取NSPAD1N200DR04的压力值,并使用串口软件完成温度曲线图绘制。
- 使用12指神探查看I2C的数据波形,与规格书的波形比较,并结合实际数据分析,完成报告书。
二、项目描述




本项目考虑在获取了温度和大气压强数据基础上,实现更加实用的应用。正好前段时间,家里购置了鱼缸和小金鱼,对于我这类养鱼新手,需要更精准的数据而不是经验。
因此,为了更好的监测养鱼环境数据,包括温度、水体温度、大气压强、参考水体溶氧量数据,以指导是否进行水体加热、水体降温、水体加氧操作。
该项目在活动基本任务基础上,增加如下功能:
- 水体温度测量:利用三极管制作远端温度头,并进行防水封装,实现水体温度测量;
- 溶氧量标准计算:利用环境温度、水体温度、大气压强,参考《中华人民共和国国家环境保护标准》和《中华人民共和国国家标准—水质、溶解氧的测定》,理论计算水体溶氧量。
- 直观数据展示:通过屏幕直观展示上述数值、并通过图案标识。
三、芯片选型
- 温度传感器 NST461-DQNR:
NST461-DQNR 是一款高精度且低功耗的数字温度传感器,基于 CMOS 工艺晶体管 PN 结的温度效应,分辨率高达 0.0625°C。除了具有高精度的本地温度测量能力外,该传感器还支持通过外部晶体管进行远程温度测量。其远程测量功能主要通过外部低成本晶体管或二极管实现。
- 绝压传感器 NSPAD1N200DR04:
NSPAD1N200DR04 是一款经过精确校准的绝对压力传感器,采用汽车级专用集成电路(ASIC)对 MEMS 传感器元件进行校准和补偿,能够将 10千帕到40万帕 的压力信号转换为 SPI/I2C 输出信号。
- 主控SEEED XIAO ESP32-C6:
ESP32-C6 是一款基于两个 32 位 RISC-V 处理器 构建的高性能主控芯片。该芯片具有 512KB SRAM 和 4 MB Flash,为物联网控制场景提供了丰富的编程空间和强大的处理能力。无线协议栈支持以下技术:2.4 GHz WiFi 6,Bluetooth® 5.3,Zigbee,Thread (802.15.4)。
四、方案框图与设计思路
根据芯片手册,使用I2C读取传感器数据,通过SPI将显示数据发送到LCD显示。
在主控ESP32-C6中,使用ESP-IDF+Freertos进行开发,保证多任务的实时执行。
额外的,主控还进行传感器数据读取与滤波、溶氧量计算和LVGL显示。

五、电路设计
- 原理图
项目使用ESP32-C6作为主控,通过I2C接口连接两个传感器,其中I2C需要使用外部电阻进行上拉保证信号读取稳定性。使用SPI接口连接LCD屏幕,控制LCD进行显示,额外的,通过PWM+NMOS控制LCD的背光亮度。

- PCB布局
PCB设计上,根据圆形LCD屏幕的尺寸,将PCB外框设置为圆形。
将两个传感器放置在边缘,尤其是温度传感器。
其中温度传感器附近不进行铺铜,减少因其他元件导热影响温度测量。


- PCB实物
由于两个传感器芯片很小,且引脚没有独立引出,因此最好通过热风枪或加热板进行焊接。
焊接完成贴片电阻之后,焊接排针排母,测量是否存在短路等情况。
最后插上ESP32C6模块即可。

六、软件设计与关键代码
- 软件设计流程
程序首先进行初始化,包括LCD、LVGL、I2C、NST461的初始化,并创建多个任务。
NST461任务:周期读取本地和远端温度数据,由于跳变严重,对数据进行了均值滤波;随后通过LVGL显示数据,并通过消息队列发布数据;
NSPAD1N任务:周期读取气压数据,并通过LVGL显示;同样通过消息队列发布数据;
溶氧量计算任务:通过消息队列等待温度和气压数据,通过插值计算得到溶氧量并显示;同时对于异常数据,进行警报。

- 关键代码
- NST461设置远端温度偏移值:由于远端温度使用三极管,不同的三极管具有不同的特性,需要根据实际情况设置偏移值。计算得到后,写入对应地址即可,如下:
esp_err_t nst461_set_offset_factor()
{
// 根据测量结果,选取的三极管温差为+13.0℃,需要减去这么多
// 因为是减去,需要使用~进行取反
// 比例系统选取1不变
// -13.0 = -13 + (0)*0.0625
data_wr[0] = ~0x0D;
data_wr[1] = ~0x00;
i2c_bus_write_bytes(handler, NST461_OFFSET_H_ADDR, 1, data_wr);
i2c_bus_write_bytes(handler, NST461_OFFSET_L_ADDR, 1, data_wr + 1);
return ESP_OK;
}
b. NST461温度读取并滤波:读取到本地和远端数据后,进行滤波防止温度跳变严重。
esp_err_t nst461_get_senser_dat(float *lt, float *rt)
{
// 读取本地温度
uint8_t ltemp[2];
if (i2c_bus_read_bytes(handler, 0x00, 1, ltemp) != ESP_OK)
return ESP_FAIL;
if (i2c_bus_read_bytes(handler, 0x15, 1, ltemp + 1) != ESP_OK)
return ESP_FAIL;
float local_temp = (float)ltemp[0] + (float)(ltemp[1] >> 4) * 0.0625;
// 读取远程温度
uint8_t rtemp[2];
if (i2c_bus_read_bytes(handler, 0x01, 1, rtemp) != ESP_OK)
return ESP_FAIL;
if (i2c_bus_read_bytes(handler, 0x10, 1, rtemp + 1) != ESP_OK)
return ESP_FAIL;
float remote_temp = (float)rtemp[0] + (float)(rtemp[1] >> 4) * 0.0625;
// 应用移动平均滤波器
local_temp = moving_average_filter(local_temp, &local_temp_filter);
remote_temp = moving_average_filter(remote_temp, &remote_temp_filter);
// 记录结果
ESP_LOGI(NST461_TAG, "Local Temp: %.2f C, Remote Temp: %.2f C", local_temp, remote_temp);
// 通过指针返回结果
*lt = local_temp;
*rt = remote_temp;
return ESP_OK; // 成功时返回ESP_OK
}
c. NSPAD1N读取气压数据:读取到原始数据后,需要通过计算得到准确的气压值。
/*——— 读取压力(kPa)———*/
esp_err_t nspad1n_get_senser_dat(float *pressure)
{
if (pressure == NULL) {
ESP_LOGE(NSPAD1N_TAG, "pressure out ptr is null");
return ESP_ERR_INVALID_ARG;
}
if (handler == NULL) {
ESP_LOGE(NSPAD1N_TAG, "I2C handler not initialized");
return ESP_FAIL;
}
/* 触发一次测量(如果芯片需要) */
data_wr[0] = 0x0A; // 具体启动命令以手册为准
esp_err_t err = i2c_bus_write_bytes(handler, NSPAD1N_REG_CMD, 1, data_wr);
if (err != ESP_OK) {
ESP_LOGE(NSPAD1N_TAG, "write CMD failed: %d", err);
return err;
}
/* 轮询等待就绪 */
uint8_t retries = NSPAD1N_POLL_RETRIES;
while (retries--) {
err = i2c_bus_read_bytes(handler, NSPAD1N_REG_CMD, 1, data_wr);
if (err != ESP_OK) {
ESP_LOGE(NSPAD1N_TAG, "read STATUS failed: %d", err);
return err;
}
if ((data_wr[0] & NSPAD1N_STATUS_READY) == NSPAD1N_STATUS_READY) {
break;
}
vTaskDelay(pdMS_TO_TICKS(NSPAD1N_POLL_DELAY_MS));
}
if (retries == 0) {
ESP_LOGE(NSPAD1N_TAG, "read timeout");
return ESP_ERR_TIMEOUT;
}
/* 读取 24bit 原始压力数据 */
uint8_t raw[3];
err = i2c_bus_read_bytes(handler, NSPAD1N_REG_PRESS_MSB, 3, raw);
if (err != ESP_OK) {
ESP_LOGE(NSPAD1N_TAG, "read pressure failed: %d", err);
return err;
}
/* 组装为 24 位并做符号扩展到 int32 */
uint32_t u24 = ((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | (uint32_t)raw[2];
int32_t s24 = (u24 & 0x00800000U) ? (int32_t)(u24 | 0xFF000000U) : (int32_t)u24;
/* 标定换算(按手册/实测调整 A、B) */
const float A = 231.250021f;
const float B = -8.125010f;
const float FS = 8388607.0f; // 2^23 - 1
*pressure = A * ((float)s24 / FS) + B;
ESP_LOGI(NSPAD1N_TAG, "P: %.1f kPa", *pressure);
return ESP_OK;
}
d. 溶氧量计算:等待温度和气压数据后,通过插值的方式计算得到理论的氧溶解度。
// 任务:计算溶氧量
void oxygen_task(void *arg)
{
static char sdat[50];
// 标准大气压101.325 kPa下的氧溶解度,温度0-40°,
const float Rou_O2[41] = {
14.62,
14.22, 13.83, 13.46, 13.11, 12.77, 12.45, 12.14, 11.84, 11.56, 11.29,
11.03, 10.78, 10.54, 10.31, 10.08, 9.87, 9.66, 9.47, 9.28, 9.09,
8.91, 8.73, 8.56, 8.42, 8.26, 8.11, 7.97, 7.83, 7.69, 7.56,
7.43, 7.30, 7.18, 7.07, 6.95, 6.84, 6.73, 6.63, 6.53, 6.43
};
// 饱和水蒸汽压力,温度0-40°
const float p_w[41] = {
0.61,
0.66, 0.71, 0.76, 0.81, 0.87, 0.93, 1.00, 1.07, 1.15, 1.23,
1.31, 1.40, 1.49, 1.60, 1.71, 1.81, 1.93, 2.07, 2.20, 2.81,
2.99, 3.17, 3.36, 3.56, 3.77, 4.00, 4.24, 4.49, 4.76, 5.02,
5.32, 5.62, 5.94, 6.28, 6.62, 6.98, 2.81, 2.99, 3.17, 7.37
};
// 计算公式,不同温度和气压下的氧溶解度
// Rou = Rou_O2[t] * (p - p_w[t]) / (101.325 - p_w[t])
// 其中t是温度,p是当前压强
float t = 20; // 设置目标温度
float p = 101.325; // 设置当前压力
while(1)
{
xQueueReceive(pressure_queue, &p, portMAX_DELAY);
xQueueReceive(temp_queue, &t, portMAX_DELAY);
// 确保温度在0到40度之间
if(t < 0) t = 0;
if(t > 39) t = 39;
// 获取温度下的低点和高点
int t_floor = (int)t; // 向下取整的温度值
int t_ceil = t_floor + 1; // 向上取整的温度值
// 进行线性插值计算氧溶解度
float Rou_O2_interp = Rou_O2[t_floor] + (t - t_floor) * (Rou_O2[t_ceil] - Rou_O2[t_floor]);
// 进行线性插值计算水蒸气压力
float p_w_interp = p_w[t_floor] + (t - t_floor) * (p_w[t_ceil] - p_w[t_floor]);
// 计算最终的氧溶解度(氧气浓度)
float Rou = Rou_O2_interp * (p - p_w_interp) / (101.325 - p_w_interp);
// 显示在屏幕上
snprintf(sdat, 50, "%.1f mg/L", Rou);
lv_label_set_text(ui_Lo2, sdat);
lv_slider_set_value(ui_So2, (int16_t)(Rou*10), LV_ANIM_ON);
}
}
七、波形展示——使用带屏12指神探
- 12指神探接线示意图
PCB中引出了电源引脚、I2C的时钟线和数据线引脚。如图,连接带屏12指神探到对应的引脚上,连接关系如下:
12指神探 <---> PCB
GND <---> GND
5V0 <---> 5V
D2-CH0 <---> SDA
D3-CH1 <---> SCL

- I2C通信波形
通过硬禾的带屏12指神探,连接I2C引脚,使用PulseView软件,测量读取两款传感器过程中的引脚数据,对比波形与数据。
- NST461的从机地址为0x4C,以读取0x00地址数据为例:发送从机地址(0x4C<<1 + 0)-> 等待ACK成功 -> 并写入0x00寄存器地址;发送从机地址(0x4C<<1 + 1) -> 等待ACK成功 -> 读取到从机返回的数据。具体如下:

图:读取从机地址0x4C,寄存器地址0x00的1Byte数据
b. 以同样的方式,分别读取寄存器地址0x15;0x01;0x10的数据,波形如下:

图:读取从机地址0x4C,寄存器地址0x15的1Byte数据

图:读取从机地址0x4C,寄存器地址0x01的1Byte数据

图:读取从机地址0x4C,寄存器地址0x10的1Byte数据
c. 对于NSPAD1N,从机地址为0x7F。为了读取大气压数据,首先需要触发测量。先写入地址NSPAD1N_REG_CMD 0x30数据0x0A,触发一次测量;然后轮询等待就绪,期间读取同个地址,并检查就绪位NSPAD1N_STATUS_READY 0x02是否为1。波形数据如下:

图:写入从机地址0x7F,寄存器地址0x30的1Byte数据0x0A

图:读取从机地址0x7F,寄存器地址0x30的1Byte数据
d. 待上述有数据后,连续读取地址NSPAD1N_REG_PRESS_MSB 0x06的3个数据,后通过换算即可得到正确的数据。波形如下:

图:读取从机地址0x7F,寄存器地址0x06的连续3Bytes数据
- 传感器数据波形显示
通过串口工具,将本地温度、远端温度、大气压强和理论氧溶解量进行显示。现象如下:
a. 测量过程中,将远端三极管放在了热水中,可以看到远端温度t迅速上升到60;拿出来后温度下降。
b. 气压保持在101.7kPa;
c. 本地温度保持在29℃左右;
d. 温度上升将导致水体溶氧量下降,温度下降将导致水体溶氧量上升。

八、实物演示
- 零件组成:① 远端测温头(连接一个三极管SS8050,并通过热缩管密封防水);② 主控SEEED XIAO ESP32-C6和制作的PCB底板(上面有两个传感器和一个圆形LCD屏幕);③ 3D打印的外壳。

- 组装完成后,将远端测温头放置在空气中进行测量:本地温度30.5℃(因LCD热量传导导致温度较高),远端温度28.8℃,大气压强101.7kPa,计算的溶氧量7.8mg/L。

- 将远端测温头放置在热水中测量:此时远端温度升高至52.8℃,并且溶氧量降低至6.6mg/L。

九、难点与解决
- 难点:利用温度传感器 NST461-DQNR测量远端温度时,虽然有显示但是温度差异巨大。
解决:查看手册,其中有offset地址和factor地址,用于调整PN结特性的偏移量和变化比例,通过测量所选三极管的输出,得到准确的数值后写入,此时温度正常显示。
- 难点:使用LVGL,显示颜色总是无法调整到与预期的一致。
解决:对于LCD显示,需要确定两个参数:颜色顺序(RGB/BGR等)和字节顺序(是否需要高低字节交换),分别在LCD驱动和LVGL配置文件中进行设置,即可正常显示。
十、总结
- 通过本次活动,了解到了温度测量方法,即利用PN结随温度的特性进行测量,是一个从理论到实际应用的典型;
- 本次尝试了ESP-IDF和LVGL,边学边开发,很有收获;
- 此次项目不仅是简单的读取温湿度,更是针对实际需求如室内淡水养鱼,输出显示溶氧量,具备一定的实际用途。
- 最后感谢主办方和芯片提供商!
