
前面的文章都在讲如何选型、部署、排障现成的工业传感器。但当你面对一个定制场景——比如为特定型号的独立配电柜设计一套嵌入式温湿度监测模块,需要自己画电路、写固件、搭服务端——事情就完全不一样了。
这不是"买一个传感器接上网线"的问题,而是:
本文从硬件电路设计到服务器数据接收程序,完整拆解一个独立配电柜 RJ45 温湿度监测终端的开发过程。

┌──────────────────────────────────────────────────────────────────┐
│ 温湿度监测终端 PCB │
│ │
│ ┌─────────┐ I2C ┌──────────┐ SPI ┌──────────────┐ │
│ │ SHT3x │─────────►│ STM32F4 │──────────►│ W5500 │ │
│ │ 温湿度 │ │ (主控) │ │ 以太网PHY │ │
│ └─────────┘ │ │ └──────┬───────┘ │
│ │ │ │ │
│ ┌─────────┐ UART │ │ I2C │ │
│ │ 调试/ │◄────────►│ │◄─────────┐ │ │
│ │ 配置 │ └──────────┘ │ │ │
│ └─────────┘ │ ┌────▼────┐ │
│ └─►│ EEPROM │ │
│ ┌────────────────────────────────────────────┐ └─────────┘ │
│ │ 电源模块 │ │
│ │ 24VDC → 3.3V (DC-DC隔离 + LDO) │ │
│ │ TVS + EMI滤波 + 浪涌保护 │ │
│ └────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ RJ45 (带集成变压器/LED) │ │
│ │ LINK LED / ACT LED │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
│
以太网线 (Cat5e SF/UTP)
│
┌────────▼────────┐
│ 交换机 │
│ (工业级/带光口) │
└────────┬────────┘
│
┌────────▼────────┐
│ 服务器 │
│ (数据接收程序) │
└─────────────────┘器件 | 型号 | 选型理由 |
|---|---|---|
MCU | STM32F407VGT6 | Cortex-M4, 168MHz, 192KB RAM, 1MB Flash, 丰富外设 |
温湿度传感器 | Sensirion SHT35 | ±1.5%RH / ±0.1℃, I2C, 工业级 |
以太网控制器 | W5500 | 硬件 TCP/IP 协议栈, SPI 接口, 8个独立socket |
RJ45 插座 | HanRun HR911105A | 集成变压器, 状态LED, 符合 IEEE 802.3 |
隔离电源 | B0505S-1WR3 | 5V→5V 隔离 DC-DC, 1500VDC 隔离 |
非隔离降压 | MP2359 | 24V→5V, 2A, 同步降压 |
LDO | AMS1117-3.3 | 5V→3.3V, 给 MCU 和传感器供电 |
EEPROM | AT24C64 | 64Kbit, 存储配置和校准参数 |
TVS | SMAJ24A | 24V 系统瞬态抑制 |
ESD 保护 | SRV05-4 | RJ45 差分线 ESD 保护 |
配电柜内 24VDC 供电,但环境恶劣(前文已分析过瞬态尖峰问题),电源设计必须层层防御:
24VDC 输入
│
├───[保险丝 2A]───[TVS SMAJ24A]───[共模电感]───[π型滤波]──┐
│ │
│ ┌─────────▼────────┐
│ │ MP2359 │
│ │ 24V → 5V/2A │
│ └────────┬────────┘
│ │
│ ┌────────▼────────┐
│ │ B0505S-1WR3 │
│ │ 隔离 DC-DC │
│ │ 5V → 5V 隔离 │
│ └────────┬────────┘
│ │
│ ┌────────▼────────┐
│ │ AMS1117-3.3 │
│ │ 5V → 3.3V │
│ └────────┬────────┘
│ │
│ ┌──────────────────┼──────────┐
│ │ │ │
│ STM32F4 W5500 SHT35 │
│ 3.3V 3.3V 3.3V │设计要点:
层级 | 作用 | 参数 |
|---|---|---|
TVS | 吸收瞬态高压尖峰 | 击穿电压 26.7V, 钳位 38.9V |
共模电感 | 抑制共模干扰 | 100Ω @ 100MHz |
π型滤波 | 滤除高频噪声 | 10μH + 10μF × 2 |
隔离 DC-DC | 电气隔离,阻断地环路 | 1500VDC 隔离电压 |
LDO | 低压差稳压,低噪声 | 压差 1V, 纹波 < 50mV |

SHT35 通过 I2C 接口与 MCU 通信:
SHT35 引脚:
VDD ─── 3.3V (10μF + 100nF 去耦)
GND ─── GND
SCL ─── PB6 (STM32 I2C1_SCL) ─── 4.7kΩ 上拉到 3.3V
SDA ─── PB7 (STM32 I2C1_SDA) ─── 4.7kΩ 上拉到 3.3V
SCL ─── 串联 100Ω 电阻 (抑制振铃)
SDA ─── 串联 100Ω 电阻
VDD ─── 串联磁珠 (抑制高频噪声)PCB 布局要点:
W5500 + RJ45(集成变压器)方案:
W5500 侧:
TXP/TXN ─── 差分对 ─── RJ45 (HR911105A)
RXP/RXN ─── 差分对 ─── RJ45
LED0 ─── LINK LED (绿色)
LED1 ─── ACT LED (黄色)
RJ45 侧:
中心抽头 (CT) ─── 3.3V (通过 1μF + 2.2μH 偏置)
屏蔽壳 ─── GND (通过 1nF/2kV 电容 + 1MΩ 电阻并联)关键设计:
项目 | 要求 |
|---|---|
差分阻抗 | 100Ω ± 10% |
差分对长度匹配 | < 5mil |
远离噪声源 | 与 DC-DC、晶振间距 > 5mm |
ESD 保护 | RJ45 差分线加 SRV05-4 |
变压器隔离 | RJ45 内置变压器 ≥ 1500V |
SPI 连接方式:
STM32F4 W5500
PA4 (SPI1_NSS) ── CS
PA5 (SPI1_SCK) ── SCLK
PA6 (SPI1_MISO) ─ MISO
PA7 (SPI1_MOSI) ─ MOSI
PC4 ── INT (中断输出,低有效)
PC5 ── RST (硬件复位)┌────────────────────────────────────────────┐
│ 应用层 │
│ ├── 传感器采集任务 (5s周期) │
│ ├── 网络通信任务 (事件驱动 + 周期发送) │
│ ├── 配置管理任务 (EEPROM读写) │
│ └── 看门狗任务 (1s喂狗) │
├────────────────────────────────────────────┤
│ W5500 驱动层 │
│ ├── SPI 读写封装 │
│ ├── Socket 管理 (8个socket) │
│ └── 中断处理 │
├────────────────────────────────────────────┤
│ TCP/IP 协议栈 (W5500 硬件栈) │
│ ├── TCP Server/Client │
│ ├── UDP │
│ └── ICMP (Ping) │
├────────────────────────────────────────────┤
│ 硬件抽象层 (HAL) │
│ ├── GPIO / SPI / I2C / UART │
│ ├── 时钟 / 定时器 │
│ └── Flash / EEPROM │
└────────────────────────────────────────────┘#include "sht3x.h"
#include "i2c.h"
#define SHT3x_ADDR 0x44 << 1
#define POLYNOMIAL 0x31
static uint8_t crc8(const uint8_t *data, int len) {
uint8_t crc = 0xFF;
for (int i = 0; i < len; i++) {
crc ^= data[i];
for (int j = 0; j < 8; j++) {
if (crc & 0x80)
crc = (crc << 1) ^ POLYNOMIAL;
else
crc <<= 1;
}
}
return crc;
}
int sht3x_read(float *temp, float *humi) {
uint8_t cmd[2] = {0x2C, 0x06}; // 高精度测量命令
uint8_t buf[6];
// 发送测量命令
if (HAL_I2C_Master_Transmit(&hi2c1, SHT3x_ADDR, cmd, 2, 100) != HAL_OK)
return -1;
HAL_Delay(15); // 等待测量完成 (高精度模式最长 15ms)
// 读取结果
if (HAL_I2C_Master_Receive(&hi2c1, SHT3x_ADDR, buf, 6, 100) != HAL_OK)
return -1;
// CRC 校验
if (crc8(buf, 2) != buf[2] || crc8(buf + 3, 2) != buf[5])
return -2; // CRC 错误
// 转换
int16_t raw_temp = (buf[0] << 8) | buf[1];
int16_t raw_humi = (buf[3] << 8) | buf[4];
*temp = -45.0f + 175.0f * raw_temp / 65535.0f;
*humi = 100.0f * raw_humi / 65535.0f;
return 0;
}#include "w5500.h"
#include "socket.h"
#define SERVER_IP {10, 20, 30, 100}
#define SERVER_PORT 9000
#define SENSOR_PORT 5000
static uint8_t tx_buf[512];
static uint8_t rx_buf[512];
void network_init(void) {
uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x01, 0x02, 0x03};
uint8_t ip[4] = {10, 20, 30, 41};
uint8_t sn[4] = {255, 255, 255, 0};
uint8_t gw[4] = {10, 20, 30, 1};
uint8_t dns[4] = {8, 8, 8, 8};
w5500_init(); // SPI 初始化 + 硬件复位
setSHAR(mac); // 设置 MAC 地址
setSIPR(ip); // IP 地址
setSUBR(sn); // 子网掩码
setGAR(gw); // 网关
}
int send_env_data(float temp, float humi, uint32_t seq) {
uint8_t sock = 0; // 使用 socket 0
// 如果 socket 未打开,先打开
if (getSn_SR(sock) != SOCK_ESTABLISHED) {
socket(sock, Sn_MR_TCP, SENSOR_PORT, 0);
connect(sock, SERVER_IP, SERVER_PORT);
// 等待连接建立(带超时)
uint32_t start = HAL_GetTick();
while (getSn_SR(sock) != SOCK_ESTABLISHED) {
if (HAL_GetTick() - start > 5000) {
close(sock);
return -1; // 连接超时
}
}
}
// 构造报文
uint8_t *p = tx_buf;
*p++ = 0xAA; // 帧头
*p++ = 0x55;
*p++ = 0x01; // 协议版本
memcpy(p, &seq, 4); p += 4; // 序列号
memcpy(p, &temp, 4); p += 4; // 温度 (float)
memcpy(p, &humi, 4); p += 4; // 湿度 (float)
uint16_t crc = calc_crc16(tx_buf, p - tx_buf);
memcpy(p, &crc, 2); p += 2;
int len = p - tx_buf;
// 发送
int sent = send(sock, tx_buf, len);
if (sent < 0) {
close(sock);
return -2; // 发送失败
}
return 0;
}int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_SPI1_Init();
MX_I2C1_Init();
MX_TIM2_Init();
network_init();
load_config_from_eeprom(); // 加载设备配置
uint32_t last_sample = 0;
uint32_t seq = 0;
float temp, humi;
while (1) {
uint32_t now = HAL_GetTick();
// 每 5 秒采集一次
if (now - last_sample >= 5000) {
last_sample = now;
if (sht3x_read(&temp, &humi) == 0) {
send_env_data(temp, humi, seq++);
}
}
// 喂看门狗
HAL_IWDG_Refresh(&hiwdg);
// 处理 W5500 中断
if (w5500_interrupt_pending()) {
w5500_process_interrupt();
}
HAL_Delay(10);
}
}目标 | 说明 |
|---|---|
高并发 | 支持 1000+ 终端同时连接 |
数据完整 | 不丢包、不重复 |
协议解析 | 解析自定义二进制协议 |
存储 | 写入时序数据库 |
告警 | 越限实时告警 |
设备管理 | 在线状态、设备注册 |
方案对比:
Python asyncio → 开发快,性能中等,适合 500 以内连接
Go net → 开发中等,性能高,适合 1000+ 连接 ← 选择
C epoll → 开发慢,性能最高,维护成本高
Node.js → 不适合 CPU 密集型解析package main
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
"github.com/influxdata/influxdb-client-go/v2"
)
// 报文结构
type EnvPacket struct {
Header uint16 // 0xAA55
Version uint8 // 协议版本
Seq uint32 // 序列号
Temp float32 // 温度
Humi float32 // 湿度
CRC uint16 // CRC16
}
// 设备状态
type Device struct {
ID string
Addr net.Addr
LastSeen time.Time
Seq uint32
Connected bool
mu sync.Mutex
}
var (
devices = make(map[string]*Device)
devicesMu sync.RWMutex
influxClient influxdb2.Client
)
func handleConnection(conn net.Conn) {
defer conn.Close()
remoteAddr := conn.RemoteAddr().String()
deviceID := extractDeviceID(remoteAddr) // 从 IP 或证书提取
// 注册设备
dev := registerDevice(deviceID, conn.RemoteAddr())
defer dev.setOffline()
// 设置读取超时
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil {
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
// 超时,检查心跳
if time.Since(dev.LastSeen) > 30*time.Second {
fmt.Printf("[%s] Heartbeat timeout\n", deviceID)
return
}
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
continue
}
fmt.Printf("[%s] Read error: %v\n", deviceID, err)
return
}
// 解析报文
pkt, err := parsePacket(buf[:n])
if err != nil {
fmt.Printf("[%s] Parse error: %v\n", deviceID, err)
continue
}
// 更新设备状态
dev.mu.Lock()
dev.Seq = pkt.Seq
dev.LastSeen = time.Now()
dev.mu.Unlock()
// 写入时序数据库
writeToInflux(deviceID, pkt)
// 检查告警
checkAlarm(deviceID, pkt)
// 发送 ACK
sendAck(conn, pkt.Seq)
}
}
func parsePacket(data []byte) (*EnvPacket, error) {
if len(data) < 15 { // 最小报文长度
return nil, fmt.Errorf("packet too short: %d bytes", len(data))
}
pkt := &EnvPacket{}
// 解析帧头
pkt.Header = binary.BigEndian.Uint16(data[0:2])
if pkt.Header != 0xAA55 {
return nil, fmt.Errorf("invalid header: 0x%04X", pkt.Header)
}
pkt.Version = data[2]
pkt.Seq = binary.BigEndian.Uint32(data[3:7])
pkt.Temp = float32frombytes(data[7:11])
pkt.Humi = float32frombytes(data[11:15])
// CRC 校验
if len(data) >= 17 {
pkt.CRC = binary.BigEndian.Uint16(data[15:17])
calcCRC := crc16(data[:15])
if calcCRC != pkt.CRC {
return nil, fmt.Errorf("CRC mismatch: calc=0x%04X, recv=0x%04X",
calcCRC, pkt.CRC)
}
}
return pkt, nil
}
func writeToInflux(deviceID string, pkt *EnvPacket) {
writeAPI := influxClient.WriteAPI("org", "bucket")
p := influxdb2.NewPoint(
"env_data",
map[string]string{"device_id": deviceID},
map[string]interface{}{
"temp": pkt.Temp,
"humi": pkt.Humi,
"seq": pkt.Seq,
},
time.Now(),
)
writeAPI.WritePoint(p)
}
func checkAlarm(deviceID string, pkt *EnvPacket) {
if pkt.Temp > 40.0 {
triggerAlarm(deviceID, "HIGH_TEMP", pkt.Temp)
}
if pkt.Humi > 80.0 {
triggerAlarm(deviceID, "HIGH_HUMI", pkt.Humi)
}
}
func main() {
// 初始化 InfluxDB
influxClient = influxdb2.NewClient("http://localhost:8086", "token")
listener, err := net.Listen("tcp", ":9000")
if err != nil {
panic(err)
}
defer listener.Close()
fmt.Println("Server listening on :9000")
for {
conn, err := listener.Accept()
if err != nil {
fmt.Printf("Accept error: %v\n", err)
continue
}
go handleConnection(conn)
}
}func registerDevice(deviceID string, addr net.Addr) *Device {
devicesMu.Lock()
defer devicesMu.Unlock()
if dev, ok := devices[deviceID]; ok {
dev.mu.Lock()
dev.Addr = addr
dev.Connected = true
dev.LastSeen = time.Now()
dev.mu.Unlock()
fmt.Printf("[%s] Reconnected from %s\n", deviceID, addr)
return dev
}
dev := &Device{
ID: deviceID,
Addr: addr,
Connected: true,
LastSeen: time.Time{},
}
devices[deviceID] = dev
fmt.Printf("[%s] New device registered from %s\n", deviceID, addr)
return dev
}
func (d *Device) setOffline() {
d.mu.Lock()
d.Connected = false
d.mu.Unlock()
fmt.Printf("[%s] Device offline\n", d.ID)
}
// 后台心跳检测
func heartbeatMonitor() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
now := time.Now()
devicesMu.RLock()
for _, dev := range devices {
dev.mu.Lock()
if dev.Connected && now.Sub(dev.LastSeen) > 30*time.Second {
fmt.Printf("[%s] Heartbeat timeout, marking offline\n", dev.ID)
dev.Connected = false
}
dev.mu.Unlock()
}
devicesMu.RUnlock()
}
}// CRC16-CCITT
func crc16(data []byte) uint16 {
crc := uint16(0xFFFF)
for _, b := range data {
crc ^= uint16(b) << 8
for i := 0; i < 8; i++ {
if crc & 0x8000 != 0 {
crc = (crc << 1) ^ 0x1021
} else {
crc <<= 1
}
}
}
return crc & 0xFFFF
}
// 序列号去重
type Deduplicator struct {
seen map[string]map[uint32]time.Time
mu sync.Mutex
}
func (d *Deduplicator) isDuplicate(deviceID string, seq uint32) bool {
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.seen[deviceID]; !ok {
d.seen[deviceID] = make(map[uint32]time.Time)
}
if _, ok := d.seen[deviceID][seq]; ok {
return true // 重复
}
d.seen[deviceID][seq] = time.Now()
return false
}□ PCB 焊接完成,功能测试通过
□ 外壳组装,防护等级验证
□ MAC 地址烧录,IP 地址配置
□ 传感器校准(与标准仪器比对)
□ 上电测试,通信正常
□ 安装到配电柜指定位置
□ 走线完成,屏蔽接地
□ 服务器添加设备记录
□ 验收测试□ Go 程序编译,交叉编译支持目标平台
□ systemd 服务配置(自动重启)
□ 防火墙开放 9000 端口
□ InfluxDB 数据库初始化
□ Grafana 仪表盘配置
□ 告警规则配置
□ 日志轮转配置指标 | 告警阈值 |
|---|---|
在线设备数 | < 预期数量的 95% |
数据接收速率 | 突降 > 50% |
报文解析失败率 | > 1% |
设备心跳超时 | 连续 3 次 |
数据库写入延迟 | > 1s |
从硬件电路到服务器程序,完整开发一套 RJ45 温湿度监测终端,核心经验:
关键词:RJ45传感器,硬件电路设计,STM32,W5500,SHT35,以太网,TCP服务器,Go语言,时序数据库,配电柜监测
标签:#RJ45 #硬件设计 #STM32 #W5500 #温湿度传感器 #嵌入式开发 #TCP服务器 #Go语言 #配电柜 #环境监测
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。