cover

16ch Temperature & Humidity

January 5, 2024

实验室需要在狭小空间内采集多路温湿度,耗时约一周工时,打样两次。效率还有提升空间。

    元件选型

    • 简单任务快速开发:选择 Seeeduino XIAO 作为主控,Arduino 编程。具有一个 I2C 端口。
    • 小型集成传感器:SHT30-DIS,2.5x2.5mm 集成温湿度传感器、ADC、I2C 通讯,最高采样率 10 mps。范围 0~100%RH、-40~125 deg,误差 2%RH、0.2 deg,重复性 0.08%RH、0.04 deg,飘移 0.25%RH/yr、0.03 deg/yr。
    • I2C 多路复用:TCA9548A,4x4mm QFN。1-8 双向转换开关,可设置为 8 个地址,可连接 64 个同名设备。
    • 数据传输:16 通道 1.1mm 三芯屏蔽线传输数据和供电,经复用开关汇于主控处理为物理量,通过 USB 串口输出。
    seeed-studio-xiao

    PCB 设计

    • 精简设计:周围元件种类仅 4 种:2.2k 主上拉电阻、10k 从上拉电阻、1uF 主解耦、100nF 从解耦。
    • 轻薄:传感器基板使用 0.11mm FPC ,总高度 1mm。
    PCBFPC

    主控程序

    • 初始化:软重置所有通道的传感器。
    • 定时运行:每经过 1000 系统毫秒运行一次采样。
    • 单次拉取模式:发送请求至传感器后等待采样(15ms)回复 6 byte 数据。
    • CRC-8 校验:4 byte 数据通过 2 byte 校验确认传输完整性。
    // 16ch Temp&Hum by WANG Yanqi
    // Created 4 Jan 2024
    
    #include <Wire.h>
    
    const long interval = 1000; 
    const uint8_t addr_hub[] = {0x70, 0x72};
    const uint8_t addr_sen = 0x44;
    const int size_hub = sizeof(addr_hub)/sizeof(addr_hub[0]);
    unsigned long previousMillis = 0;
    unsigned long currentMillis = 0;
    uint8_t data[6];
    uint8_t data_crc[2];
    float temp;
    float hum;
    
    float fun_temp(float raw);
    float fun_hum(float raw);
    uint8_t crc8(const uint8_t* data, size_t length);
    
    void setup() {
      Wire.begin();
      Serial.begin(9600);
      delay(5000);
      // Go through all the hubs
      for (int i = 0; i < size_hub; i++) {
        // Go through 8 ch. in a hub
        for (uint8_t ch = 0b00000001; ch != 0; ch = ch << 1) {
          // Open the ch.
          Wire.beginTransmission(addr_hub[i]);
          Wire.write(ch);
          Wire.endTransmission();
          // Soft reset sen.
          Wire.beginTransmission(addr_sen);
          Wire.write(0x30);
          Wire.write(0xA2);
          Wire.endTransmission();
          delay(100);
        }
        // Close all the ch.
        Wire.beginTransmission(addr_hub[i]);
        Wire.write(0);
        Wire.endTransmission();
      }
    }
    
    void loop() {
      // Get the current number of milliseconds since the program started
      currentMillis = millis();
      if (currentMillis - previousMillis >= interval) {
        // Go through all the hubs
        for (int i = 0; i < size_hub; i++) {
          // Go through 8 ch. in a hub
          for (uint8_t ch = 0b00000001; ch != 0; ch = ch << 1) {
            // Open the ch.
            Wire.beginTransmission(addr_hub[i]);
            Wire.write(ch);
            Wire.endTransmission();
            read_sen(addr_sen, data);
            // Check temp.
            data_crc[0] = data[0];
            data_crc[1] = data[1];
            if (crc8(data_crc, sizeof(data_crc)) == data[2]) {
              temp = fun_temp((data[0] << 8) | data[1]);
            } else {
              temp = NAN;
            }
            // Check hum.
            data_crc[0] = data[3];
            data_crc[1] = data[4];
            if (crc8(data_crc, sizeof(data_crc)) == data[5]) {
              hum = fun_hum((data[3] << 8) | data[4]);
            } else {
              hum = NAN;
            }
            // Print data
            Serial.print(temp);
            Serial.print(",");
            Serial.print(hum);
            if (i != size_hub-1 || ch != 0b10000000) {
              Serial.print(";");
            }
          }
          // Close all the ch.
          Wire.beginTransmission(addr_hub[i]);
          Wire.write(0);
          Wire.endTransmission();
        }
        Serial.print("\n");
        previousMillis = currentMillis;
      }
    }
    
    void read_sen(uint8_t addr, uint8_t* data) {
      // Clock stretching, high repeatability
      Wire.beginTransmission(addr);
      Wire.write(0x2C);
      Wire.write(0x06);
      Wire.endTransmission();
      Wire.requestFrom(addr, 6);
      for (int i = 0; i < 6; i++) {
          data[i] = Wire.read();
      }
    }
    
    uint8_t crc8(const uint8_t* data, size_t length) {
      // Start with 0xFF for initialization
      uint8_t crc = 0xFF;
      for (size_t i = 0; i < length; ++i) {
        crc ^= data[i];
        // Loop over each bit
        for (uint8_t j = 8; j; --j) {
          // If the uppermost bit is 1...
          if (crc & 0x80) { 
            // ... shift left and XOR with the polynomial
            crc = (crc << 1) ^ 0x31; 
          } else {
            // Otherwise, just shift left
            crc <<= 1; 
          }
        }
      }
      // No final XOR
      return crc;
    }
    
    float fun_temp(float raw) {
      float temp = -45.0 + 175.0 * raw / 65535;
      return temp;
    }
    
    float fun_hum(float raw) {
      float hum = 100.0 * raw / 65535;
      return hum;
    }
    

    上位程序

    • 创建图形界面:使用 matplotlib 的 pyplot、widgets 实时显示数据和按钮。
    • 读取数据:等待串口输出一行,记录主机时间戳,存入数组。
    • 存储数据:关闭窗口后,将所有数据以 CSV 格式保存。
    # pip install pyserial matplotlib pandas
    
    import serial
    import matplotlib.pyplot as plt
    import pandas as pd
    from matplotlib.widgets import Button
    from datetime import datetime
    
    max_points = 20  # Number of points to display on the graph
    data = {
        'Timestamp': [],
        **{f'Temp{i+1}': [] for i in range(16)},
        **{f'Hum{i+1}': [] for i in range(16)}
    }
    
    # Initialize plots
    plt.ion()
    fig, (ax_temp, ax_hum) = plt.subplots(2, 1, figsize=(15, 10))
    
    # Set plot titles and labels
    ax_temp.set_title('Temperature')
    ax_temp.set_xlabel('Time')
    ax_temp.set_ylabel('Temperature (°C)')
    
    ax_hum.set_title('Humidity')
    ax_hum.set_xlabel('Time')
    ax_hum.set_ylabel('Humidity (%)')
    
    # Global flag to control the data collection
    collecting_data = False
    exit_flag = False
    
    # Button callback functions
    def start_button_callback(event):
        global collecting_data
        collecting_data = True
    
    def exit_button_callback(event):
        global exit_flag
        exit_flag = True
    
    # Create the buttons
    ax_start_button = plt.axes([0.7, 0.05, 0.1, 0.075])
    ax_exit_button = plt.axes([0.81, 0.05, 0.1, 0.075])
    
    start_button = Button(ax_start_button, 'Start')
    start_button.on_clicked(start_button_callback)
    
    exit_button = Button(ax_exit_button, 'Exit')
    exit_button.on_clicked(exit_button_callback)
    
    # Display the plot window
    plt.show(block=False)
    
    # Wait here until 'Start' button is pressed
    while not collecting_data:
        plt.pause(0.1)  # Use a short pause to handle UI events
        if exit_flag:
            plt.close(fig)
            raise SystemExit
    
    # Main loop
    try:
        # Set up the serial connection (adjust the COM port as needed)
        ser = serial.Serial('/dev/cu.usbmodem14101', 9600, timeout=1)
        while not exit_flag:
            line = ser.readline().decode('utf-8').strip()
            if line:
                try:
                    # Split the line into groups and then into temperature and humidity values
                    groups = line.split(';')
                    if len(groups) == 16:
                        timestamp = datetime.now().strftime('%H:%M:%S')
                        data['Timestamp'].append(timestamp)
                        temps = []
                        hums = []
                        for i, group in enumerate(groups):
                            temp, hum = map(float, group.split(','))
                            temps.append(temp)
                            hums.append(hum)
                            data[f'Temp{i+1}'].append(temp)
                            data[f'Hum{i+1}'].append(hum)
    
                        # Update temperature graph with latest 20 points
                        ax_temp.clear()
                        ax_temp.set_title('Temperature')
                        ax_temp.set_xlabel('Time')
                        ax_temp.set_ylabel('Temperature (°C)')
                        for i in range(16):
                            ax_temp.plot(data['Timestamp'][-max_points:], data[f'Temp{i+1}'][-max_points:], label=f'Temp{i+1}')
    
                        # Update humidity graph with latest 20 points
                        ax_hum.clear()
                        ax_hum.set_title('Humidity')
                        ax_hum.set_xlabel('Time')
                        ax_hum.set_ylabel('Humidity (%)')
                        for i in range(16):
                            ax_hum.plot(data['Timestamp'][-max_points:], data[f'Hum{i+1}'][-max_points:], label=f'Hum{i+1}')
    
                        plt.pause(0.5)
                except ValueError as e:
                    print(f"Could not convert data to float: {line}, error: {e}")
    except KeyboardInterrupt:
        plt.ioff()
    finally:
        # Save the data to a CSV file
        df = pd.DataFrame(data)
        csv_filename = 'arduino_data.csv'
        df.to_csv(csv_filename, index=False)
        print(f"Data saved to '{csv_filename}'")
        ser.close()
    

    python_plot