FastBond4挑战部分-基于RDK X5 的多核异构工业危险行为实时检测与联动控制系统
该项目使用了RDK X5 与多传感器融合,实现了一款面向工业场景的“行为-环境融合智能终端”的设计,它的主要功能为:对工业环境中的操作行为与周边环境参数进行实时感知、融合分析,并提供智能决策支持。
标签
CAN
EtherCAT
RDK X5
YOLOV8
陌--
更新2026-06-09
178

FastBond4挑战部分-基于RDK X5 的多核异构工业危险行为实时检测与联动控制系统

一、所选主题和项目介绍

本项目聚焦于 人工智能在嵌入式系统中的应用,以地平线RDK X5边缘计算平台为核心,构建了一套面向工业现场的 AI视觉安全监控与联动控制系统。系统深度融合了 计算机视觉(图像识别)、多传感器感知实时工业以太网控制 ,设计并实现了一套“视觉AI检测+环境感知+实时IO控制”三位一体的边缘计算系统。实现对跌倒、火灾等典型危险行为的智能识别与自主保护性响应,为工业自动化提供了高可靠、低延迟的边缘AI解决方案。

项目完整链路为:USB摄像头接入RDK X5,运行YOLOv8火灾/跌倒检测模型,推理结果实时推流至板载ZLMediaKit流媒体服务器(亦可通过Socket.IO直接推送JPEG帧至网页);同时,RDK X5通过CAN总线接收STM32F103节点采集的温湿度(SHT30、HDC1080)、气压(BMP280)、光照强度(OPT3001)及MPU6050、QMI8658A、LSM6DSMTR三组六轴姿态数据,所有数据通过Web后端实时展示在网页仪表盘上。RDK X5还部署了IgH EtherCAT主站协议栈,通过以太网接口与EtherCAT从站通信,网页端可实时查看两路数字输入状态(DI0/DI1),当AI模块判断发生跌倒或火灾时,系统自动通过EtherCAT下发指令,驱动从站上两个LED分别点亮实现就地报警,并在网页上弹出最高优先级报警提示。整个系统实现了检测、推流、数据显示、设备控制与联动的闭环,具有一定的工程实用价值。

二、使用到的所有硬件介绍

1. 地平线RDK X5(核心主控)
基于BPU计算架构的AI开发板,4核CPU,板载10TOPs算力BPU,拥有千兆以太网、USB3.0、CAN、WiFi等丰富接口。本项目中它承担AI推理、ZLM流媒体服务器(可选)、EtherCAT主站、CAN数据汇集和Web服务器五大核心角色。

2. 自制环境感知节点(MCU: STM32F103RCT6)
主控为Cortex-M3架构,256KB Flash,48KB RAM。外围挂载I2C1总线上的MPU6050、QMI8658A、LSM6DSMTR三款六轴传感器,以及I2C2总线上的SHT30、BMP280、OPT3001、HDC1080环境传感器。板载CAN收发器TJA1051,通过CAN总线以100ms周期上报环境数据帧与姿态帧。

3. 自制EtherCAT从站节点(MCU: STM32G474RET6 + LAN9252)
Cortex-M4F内核,内置高分辨率定时器。从站芯片使用Microchip LAN9252,通过SPI与G474交互,实现EtherCAT从站控制器功能。板上引出两颗红色/绿色高亮LED用于报警指示。在从站程序中,利用SSC工具生成的从站协议栈,实现了标准CoE对象字典,便于主站映射控制。

4. 其他配件
USB免驱高清摄像头(1080P/30fps)。微信图片_20260609114936_352_13.jpg

三、方案框图和项目设计思路介绍

image.png

设计思路

  • AI与流媒体一体化:利用RDK X5的BPU加速推理,搭配板载硬件视频编码能力,将检测框渲染后的视频流直接推送到同机部署的ZLMediaKit,避免数据搬运延迟,降低网络负载。
  • 多协议高实时性通信:环境数据采集为低频、多传感器数据,选择CAN总线(500kbps)兼顾可靠性与成本;IO控制要求毫秒级确定性延时,采用EtherCAT保证硬实时。
  • 模块化解耦:AI检测、环境采集、IO控制三个功能由独立硬件实现,仅通过RDK X5软件进行逻辑耦合,任意模块异常不影响其他子系统基本安全逻辑。
  • 网页端集中管理:采用Flask-SocketIO后端+WebSocket,将环境数据、报警信息、AI识别结果推送给前端,前端可以实时监视EtherCAT输入输出状态。

四、原理图和PCB展示及介绍

本项目自制了两款PCB:环境采集板和EtherCAT从站板。两款板子各打样一版,最终完成稳定版本。

环境采集板

使用的订单物料如下:

F1C1-252012-1R5M Signal Transformer | 固定电感器 - DigiKey

PTS815SJM250SMTRLFS C&K | 触摸开关 - DigiKey

原理图:

最小系统部分

image.png

电源+CAN+RS485部分image.png

六轴陀螺仪部分

image.png

环境传感器部分

image.png

PCB展示

image.png

渲染图及实物图

image.png

image.png

EtherCAT IO板

原理图介绍

  • 主控部分
    采用STM32G474CEU6作为主控c02898cf904f4bb9adc25023b68a26ab.png
  • 电源部分
    • 24V->5V采用RY8411,它具有4.5-40V宽输入范围 - 最大1A输出电流 - 过压/过流保护2ffe9de67c1d41c689c116263f102cce.png
    • 5V->3.3V采用XC6210B332MRc7dc6db646d04494be1aaa470e87d00a.png
  • ST_LINK_V2.1
    采用stm32f103cbu6为主控4eaf56e68206467a9e1dd9d0956c32b0.png
  • CANFD
    采用NXP的TJA1051TK/3/1收发器5ed58f7a80c04fecac31f6c64d235839.png
  • RS485
    采用SIT3088ETK90c166675386432f91878555fe32808d.png
  • 拓展IO
  • 2ef5f710b0634114b90f02a04772159e-1780970748678-19.png
  • LAN9252
  • image.png
  • 网口及电
  • image-20260609100634790.png
  • image-20260609100651288.png

PCB

stm32g474板卡

image-20260609100821448.png

LAN9252板卡

image-20260609100748443.png

实物图

image-20260609101035062.png

五、 YOLOV8模型训练与转换部署

为实现高精度的跌倒与火灾检测,我针对性地训练了 YOLOv8 模型,并完成从 PC 端到 RDK X5 边缘端的完整迁移。

YOLOv8 的官方仓库地址: https://github.com/ultralytics/ultralytics.git

整体流程

image.png

5.1 数据集构建

我们收集了约2000张图片,其中跌倒图片800张,火灾图片1200张。使用 LabelImg 工具进行标注,按 8:2 随机划分为训练集(1600张)和验证集(400张),并编写符合 YOLO 格式的 fire_fall.yaml 配置文件,定义三类检测目标:Fire、Smoke、fall。

fire_fall.yaml文件

path: ../datasets/fire_fall  
train: images/train
val: images/val
test:
names:
0: Fire
1: Smoke
2: fall

5.2 模型训练

基于 Ultralytics 官方仓库,使用预训练权重 yolov8n.pt 进行迁移学习。训练配置为:120 epochs,输入尺寸 640×640,batch size 16。训练完成后,最佳模型保存为 best.pt,并在 PC 端验证了正确的检测效果。

训练结果

由于数据集太少,结果不是太好。

164712zh3f7gfm86076lag.png.thumb.jpg

164717picgeipsscfic2gc.png.thumb.jpg

164722w7gr411r1h16l04z.png.thumb.jpg

PC端测试结果

model = YOLO('runs/detect/fire_fall/weights/best.pt')
results = model('test_images/', save=True)

image.png

5.3 面向地平线 BPU 的模型导出

RDK X5 的 BPU 对模型结构有特定要求。我们修改了 ultralytics/nn/modules/head.py 中的 Detect 类,使 forward 方法在导出模式下输出 6 个独立的 NHWC 张量(每个尺度分别输出 bbox 和 cls),而非默认的合并输出。随后使用 model.export(format='onnx', opset=11) 导出 ONNX 模型,并用 Netron 验证了输出结构。

  1. 修改检测头 (head.py):核心任务是找到ultralytics/nn/modules/head.py中的Detect类,修改其forward方法,以在self.export为True时,输出 6 个独立的 NHWC 张量(BBox 和 Cls 分开,且每个尺度独立)。
     def forward(self, x: list[torch.Tensor]):
          """Concatenates and returns predicted bounding boxes and class probabilities."""
          # ---------- RDK X5 导出分支 ----------
          if self.export:
              outputs = []
              for i in range(self.nl):
                  bbox = self.cv2[i](x[i]) # (bs, 4*reg_max, h, w)
                  cls = self.cv3[i](x[i]) # (bs, nc, h, w)
                  # 输出 NHWC 布局(如果后处理需要 NCHW,可注释掉这两行)
                  bbox = bbox.permute(0, 2, 3, 1).contiguous()
                  cls = cls.permute(0, 2, 3, 1).contiguous()
                  outputs.append(bbox)
                  outputs.append(cls)
              return outputs
          # ---------- 原始训练/推理代码 ----------
          preds = self.forward_head(x, **self.one2many)
          if self.end2end:
              x_detach = [xi.detach() for xi in x]
              one2one = self.forward_head(x_detach, **self.one2one)
              preds = {"one2many": preds, "one2one": one2one}
          if self.training:
              return preds
          y = self._inference(preds["one2one"] if self.end2end else preds)
          if self.end2end:
              y = self.postprocess(y.permute(0, 2, 1))
          return y if self.export else (y, preds)
  2. 执行导出:使用修改后的 Ultralytics 代码执行导出。
    from ultralytics import YOLO
    model = YOLO('best.pt')
    model.export(format='onnx', imgsz=640, opset=11)
  3. 导出验证:使用 Netron 打开 ONNX 模型,确认输出是预期的 6 个独立张量。image.png

5.4 地平线工具链转换

在地平线提供的 Docker 环境中,使用 hb_mapper checker 检查 ONNX 模型兼容性,编写 fire_fall.yaml 配置文件,指定 NV12 输入、数据归一化参数、校准数据集路径等信息。通过 hb_mapper makertbin 完成量化与编译,最终生成板端可推理的 fire_fall.bin 文件。

  1. 启动Docker
    export version=v1.2.8
    export ai_toolchain_package_path=/home/kone/RDK_X5/OE/horizon_x5_open_explorer_v1.2.8-py310_20240926
    export dataset_path=/home/kone/RDK_X5/OE/dataset  

    # 启动docker镜像
    sudo docker run -it --rm \
      -v "$ai_toolchain_package_path":/open_explorer \
      -v "$dataset_path":/data \
      openexplorer/ai_toolchain_ubuntu_20_x5_cpu:v1.2.8-py310
  2. 模型检查 (hb_mapper checker)
    在正式开始前,先用 hb_mapper checker命令检查 ONNX 模型的兼容性,避免有不支持的算子导致后续转换卡住。image.png
    hb_mapper checker --model-type onnx --march bayes-e --model fire_fall.onnx
  3. 准备校准数据和配置文件
    修复文件名(批量重命名)在Docker 容器内,进入校准图片目录,执行以下命令,把括号全部删掉:
    for f in *; do
      new=$(echo "$f" | tr -d '()')
      [ "$f" != "$new" ] && mv "$f" "$new"
    done
    ls   # 检查是否还有特殊字符
    准备配置文件
    model_parameters:
    onnx_model: 'fire_fall.onnx'
    march: "bayes-e"
    working_dir: 'model_output'               # 转换产物的输出目录
    output_model_file_prefix: 'fire_fall'

    input_parameters:
    input_type_rt: 'nv12'
    input_type_train: 'rgb'
    input_layout_train: 'NCHW'
    norm_type: 'data_scale'
    scale_value: 0.003921568627451

    calibration_parameters:
    cal_data_dir: './calibration_data'  
    cal_data_type: 'float32'
    preprocess_on: True                 # 工具链自动完成 resize 和格式转换
    calibration_type: 'default'

    compiler_parameters:
    compile_mode: 'latency'
    optimize_level: 'O3'
    执行
    hb_mapper makertbin --model-type onnx --config fire_fall.yam
    image.png
    生成的 fire_fall.bin 即为板端可用模型。


六、调试软件、软件流程图和关键代码介绍

6.1 软件流程图

image.png

6.2 关键代码示例

CAN数据接收(小端字节序)

def can_recv(sock, timeout=0.5):
  sock.settimeout(timeout)
  try:
      raw = sock.recv(CAN_MTU)
      if len(raw) >= 5:
          can_id, dlc = struct.unpack('<IB', raw[:5])
          can_id &= 0x1FFFFFFF
          dlc &= 0x0F
          payload = raw[8:8+dlc]
          return can_id, payload
  except socket.timeout:
      pass
  return None, None


EtherCAT LED控制

def set_led(led_index, state):
  val = 0xFFFF if state else 0x0000
  subprocess.run([
      "ethercat", "download", "--type", "uint16", "-p0",
      f"0x7000", str(led_index + 1), f"0x{val:04X}"
  ], check=False)


AI报警去抖逻辑

if current_fire or current_smoke:
  fire_count += 1
else:
  fire_count = 0
if current_fall:
  fall_count += 1
else:
  fall_count = 0
fire_alarm = fire_count >= ALARM_THRESH
fall_alarm = fall_count >= ALARM_THRESH


WebSocket实时推送

socketio.emit('env_data', env)
socketio.emit('mpu_data', mpu)
socketio.emit('led_state', {'led0': current_led0, 'led1': current_led1})


6.3 前端仪表盘

前端采用深色科幻风格,基于Chart.js绘制三组IMU姿态实时曲线,利用Canvas绘制六个圆形环境数据仪表盘,每个仪表盘上方标注传感器型号(如SHT30 Temp、BMP280 Press等)。EtherCAT IO状态通过LED圆点直观显示,报警事件以脉冲动画突出。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>RDK X5 Industrial Safety Monitor</title>
  <style>
      :root { --bg: #0a0a0f; --panel: #111122; --border: #1e1e3f; --text: #ccc; --accent: #00ffcc; --fire: #ff3b3b; --fall: #ff8c00; }
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font-family: 'Segoe UI', sans-serif; background: var(--bg); color: var(--text); overflow-x: hidden; }
      #particles { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 0; }
      .container { position: relative; z-index: 1; display: flex; flex-wrap: wrap; padding: 15px; gap: 15px; }
      .video-box { flex: 2; min-width: 500px; background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 10px; box-shadow: 0 0 20px rgba(0,255,204,0.1); }
      .video-box h3 { color: var(--accent); margin-bottom: 10px; font-weight: 300; letter-spacing: 2px; }
      .video-box img { width: 100%; border-radius: 8px; border: 1px solid var(--border); }
      .panel { flex: 1; min-width: 320px; display: flex; flex-direction: column; gap: 15px; }
      .card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 15px; box-shadow: 0 0 15px rgba(0,255,204,0.05); }
      .card h4 { color: var(--accent); margin-bottom: 10px; font-weight: 400; text-transform: uppercase; letter-spacing: 1px; }
      .env-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
      .gauge { text-align: center; }
      .gauge-source { font-size: 0.65rem; color: #888; margin-bottom: 2px; text-transform: uppercase; }
      .gauge canvas { width: 70px; height: 70px; }
      .gauge-label { font-size: 0.8rem; margin-top: 2px; color: #aaa; }
      .alarm { padding: 12px; margin: 5px 0; border-radius: 8px; font-weight: bold; display: none; animation: pulse 1s infinite; }
      @keyframes pulse { 0% { opacity: 1; } 50% { opacity: 0.6; } 100% { opacity: 1; } }
      .fire-alarm { background: var(--fire); color: white; }
      .fall-alarm { background: var(--fall); color: white; }
      .led-dot { transition: background 0.2s; }
      .chart-container { height: 150px; margin: 10px 0; }
      canvas { max-width: 100%; }
  </style>
</head>
<body>
  <canvas id="particles"></canvas>
  <div class="container">
      <div class="video-box">
          <h3>Live Camera</h3>
          <img id="video" src="" alt="Video Stream">
      </div>
      <div class="panel">
          <div class="card">
              <h4>Environment</h4>
              <div class="env-grid">
                  <div class="gauge">
                      <div class="gauge-source">SHT30 Temp</div>
                      <canvas id="gauge_temp"></canvas>
                      <div class="gauge-label" id="temp_label">--&deg;C</div>
                  </div>
                  <div class="gauge">
                      <div class="gauge-source">SHT30 Hum</div>
                      <canvas id="gauge_hum"></canvas>
                      <div class="gauge-label" id="hum_label">--%</div>
                  </div>
                  <div class="gauge">
                      <div class="gauge-source">BMP280 Press</div>
                      <canvas id="gauge_press"></canvas>
                      <div class="gauge-label" id="press_label">--hPa</div>
                  </div>
                  <div class="gauge">
                      <div class="gauge-source">OPT3001 Light</div>
                      <canvas id="gauge_lux"></canvas>
                      <div class="gauge-label" id="lux_label">--lux</div>
                  </div>
                  <div class="gauge">
                      <div class="gauge-source">HDC1080 Temp</div>
                      <canvas id="gauge_hdc_temp"></canvas>
                      <div class="gauge-label" id="hdc_temp_label">--&deg;C</div>
                  </div>
                  <div class="gauge">
                      <div class="gauge-source">HDC1080 Hum</div>
                      <canvas id="gauge_hdc_hum"></canvas>
                      <div class="gauge-label" id="hdc_hum_label">--%</div>
                  </div>
              </div>
          </div>
          <div class="card">
              <h4>Alarms</h4>
              <div id="fire_alarm" class="alarm fire-alarm">FIRE!</div>
              <div id="fall_alarm" class="alarm fall-alarm">FALL DETECTED!</div>
          </div>
          <div class="card">
              <h4>EtherCAT IO</h4>
              <div style="display:flex; align-items:center; gap:15px; flex-wrap:wrap;">
                  <div style="flex:1; background:#1a1a2e; padding:10px; border-radius:8px;">
                      <div style="margin-bottom:8px;">Output LEDs</div>
                      <div>
                          <span class="led-dot" id="led0_state" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle;"></span> LED0 (Fire)
                          <span class="led-dot" id="led1_state" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle; margin-left:15px;"></span> LED1 (Fall)
                      </div>
                  </div>
                  <div style="flex:1; background:#1a1a2e; padding:10px; border-radius:8px;">
                      <div style="margin-bottom:8px;">Input DI</div>
                      <div>
                          <span class="led-dot" id="di0_led" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle;"></span> DI0
                          <span class="led-dot" id="di1_led" style="display:inline-block; width:14px; height:14px; border-radius:50%; background:#444; vertical-align:middle; margin-left:15px;"></span> DI1
                      </div>
                      <div style="font-family:monospace; margin-top:6px; font-size:0.85rem;" id="io_raw">Overall: 0x--</div>
                  </div>
              </div>
          </div>
      </div>
  </div>
  <div class="container" style="padding-top:0;">
      <div class="card" style="width:100%;"><h4>MPU6050 Attitude</h4><div class="chart-container"><canvas id="mpu_chart"></canvas></div></div>
      <div class="card" style="width:100%;"><h4>QMI8658A Attitude</h4><div class="chart-container"><canvas id="qmi_chart"></canvas></div></div>
      <div class="card" style="width:100%;"><h4>LSM6DSMTR Attitude</h4><div class="chart-container"><canvas id="lsm_chart"></canvas></div></div>
  </div>
  <script src="https://cdn.socket.io/4.5.0/socket.io.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
  <script>
      // Particle background
      const canvas = document.getElementById('particles');
      const ctx = canvas.getContext('2d');
      canvas.width = window.innerWidth;
      canvas.height = window.innerHeight;
      const particles = [];
      for (let i = 0; i < 80; i++) {
          particles.push({ x: Math.random() * canvas.width, y: Math.random() * canvas.height, r: Math.random() * 2 + 1, dx: (Math.random() - 0.5) * 0.5, dy: (Math.random() - 0.5) * 0.5 });
      }
      function drawParticles() {
          ctx.clearRect(0, 0, canvas.width, canvas.height);
          ctx.fillStyle = 'rgba(0,255,204,0.2)';
          for (let p of particles) {
              ctx.beginPath();
              ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);
              ctx.fill();
              p.x += p.dx;
              p.y += p.dy;
              if (p.x < 0 || p.x > canvas.width) p.dx *= -1;
              if (p.y < 0 || p.y > canvas.height) p.dy *= -1;
          }
          requestAnimationFrame(drawParticles);
      }
      drawParticles();
      window.addEventListener('resize', () => { canvas.width = window.innerWidth; canvas.height = window.innerHeight; });

      // Gauges
      function drawGauge(id, value, max, label, unit) {
          const c = document.getElementById(id);
          if (!c) return;
          const w = c.width = 70, h = c.height = 70;
          const gctx = c.getContext('2d');
          gctx.clearRect(0,0,w,h);
          const centerX = w/2, centerY = h/2, radius = 28;
          const start = Math.PI * 0.75, end = Math.PI * 2.25;
          const percent = Math.min(value / max, 1);
          gctx.beginPath();
          gctx.arc(centerX, centerY, radius, start, end);
          gctx.strokeStyle = '#333';
          gctx.lineWidth = 6;
          gctx.stroke();
          gctx.beginPath();
          gctx.arc(centerX, centerY, radius, start, start + (end - start) * percent);
          gctx.strokeStyle = '#00ffcc';
          gctx.lineWidth = 6;
          gctx.stroke();
          gctx.fillStyle = '#fff';
          gctx.font = 'bold 12px sans-serif';
          gctx.textAlign = 'center';
          gctx.fillText(parseFloat(value).toFixed(1), centerX, centerY + 4);
          gctx.font = '9px sans-serif';
          gctx.fillText(unit, centerX, centerY + 16);
          document.getElementById(label).textContent = value.toFixed(1) + ' ' + unit;
      }

      // Charts
      function createLineChart(canvasId) {
          const ctx = document.getElementById(canvasId).getContext('2d');
          return new Chart(ctx, {
              type: 'line',
              data: {
                  labels: [],
                  datasets: [
                      { label: 'ax', data: [], borderColor: '#ff6384', pointRadius: 0, borderWidth: 1 },
                      { label: 'ay', data: [], borderColor: '#36a2eb', pointRadius: 0, borderWidth: 1 },
                      { label: 'az', data: [], borderColor: '#ffce56', pointRadius: 0, borderWidth: 1 },
                      { label: 'gx', data: [], borderColor: '#4bc0c0', pointRadius: 0, borderWidth: 1 },
                      { label: 'gy', data: [], borderColor: '#9966ff', pointRadius: 0, borderWidth: 1 },
                      { label: 'gz', data: [], borderColor: '#ff9f40', pointRadius: 0, borderWidth: 1 }
                  ]
              },
              options: {
                  responsive: true, maintainAspectRatio: false, animation: false,
                  scales: { x: { display: false }, y: { grid: { color: '#2a2a4a' }, ticks: { color: '#ccc' } } },
                  plugins: { legend: { labels: { color: '#aaa', usePointStyle: true } } }
              }
          });
      }
      const mpuChart = createLineChart('mpu_chart');
      const qmiChart = createLineChart('qmi_chart');
      const lsmChart = createLineChart('lsm_chart');
      const MAX_POINTS = 50;
      function updateChart(chart, dataObj) {
          chart.data.labels.push('');
          chart.data.datasets[0].data.push(dataObj.ax);
          chart.data.datasets[1].data.push(dataObj.ay);
          chart.data.datasets[2].data.push(dataObj.az);
          chart.data.datasets[3].data.push(dataObj.gx);
          chart.data.datasets[4].data.push(dataObj.gy);
          chart.data.datasets[5].data.push(dataObj.gz);
          if (chart.data.labels.length > MAX_POINTS) {
              chart.data.labels.shift();
              chart.data.datasets.forEach(ds => ds.data.shift());
          }
          chart.update();
      }

      // Socket.IO
      const socket = io();
      socket.on('video_frame', (data) => {
          document.getElementById('video').src = 'data:image/jpeg;base64,' + data.img;
      });
      socket.on('env_data', (data) => {
          drawGauge('gauge_temp', data.sht_temp, 50, 'temp_label', 'degC');
          drawGauge('gauge_hum', data.sht_hum, 100, 'hum_label', '%');
          drawGauge('gauge_press', data.bmp_press/100, 1100, 'press_label', 'hPa');
          drawGauge('gauge_lux', data.opt_lux, 1000, 'lux_label', 'lux');
          drawGauge('gauge_hdc_temp', data.hdc_temp, 50, 'hdc_temp_label', 'degC');
          drawGauge('gauge_hdc_hum', data.hdc_hum, 100, 'hdc_hum_label', '%');
      });
      socket.on('mpu_data', (data) => { updateChart(mpuChart, data); });
      socket.on('qmi_data', (data) => { updateChart(qmiChart, data); });
      socket.on('lsm_data', (data) => { updateChart(lsmChart, data); });
      socket.on('detection', (data) => {
          document.getElementById('fire_alarm').style.display = data.fire ? 'block' : 'none';
          document.getElementById('fall_alarm').style.display = data.fall ? 'block' : 'none';
      });
      socket.on('led_state', (data) => {
          document.getElementById('led0_state').style.background = data.led0 ? '#ff4d4d' : '#444';
          document.getElementById('led1_state').style.background = data.led1 ? '#ff8c00' : '#444';
      });
      socket.on('io_state', (data) => {
          document.getElementById('di0_led').style.background = data.di0 ? '#00ffcc' : '#444';
          document.getElementById('di1_led').style.background = data.di1 ? '#00ffcc' : '#444';
          document.getElementById('io_raw').textContent = 'Overall: 0x' + data.raw.toString(16).toUpperCase().padStart(4, '0');
      });
  </script>
</body>
</html>


七、硬件功能展示图及说明

  1. 整体接线图:RDK X5与两块自研板卡连接,摄像头置于模拟厂房场景微信图片_20260609114936_352_13.jpg
  2. 网页仪表盘:左侧显示实时视频(带有检测框标注,分别用红色框标火灾、蓝色框标人体),右侧以仪表和曲线展示温湿度、光照、MPU6050翻滚角image.pngimage.png
  3. 报警展示:触发跌倒后,网页弹出“FALL DETECTED!”红色横幅,视频画面叠加“FALL ALERT”文字;同时EtherCAT从站的红色LED点亮。火灾触发时蓝色LED点亮,网页弹出“FIRE!”警告。image.png
  4. IO状态:网页上实时显示DI0、DI1的高低电平(绿色圆点表示高电平,灰色表示低电平),以及整体十六进制输入状态字。
  5. 数据推送:通过手机浏览器访问同一网络下的网页,所有数据与视频流畅显示,延迟<300ms。image.pngimage.png

八、设计中遇到的难题和解决方法

1. EtherCAT主站无法启动
现象:执行ethercat slaves时报“Failed to open master device /dev/EtherCAT0”。
解决:检查内核模块是否加载,执行sudo modprobe ec_master && sudo modprobe ec_generic,并配置/etc/ethercat.conf绑定对应网卡MAC,重启ethercat服务后设备文件出现,主站正常工作。

2. CAN总线数据字节序错误
现象:RDK X5端接收到的CAN ID异常(0x1010000),数据无法解析。
解决:RDK X5采用ARM64小端架构,而代码最初使用大端解析,改为struct.unpack('<IB', raw[:5])后数据完全正确,温度、姿态均实时更新。

3. Flask模板编码错误
现象:网页返回500 Internal Server Error,错误日志提示UnicodeDecodeError
解决:index.html文件内混入了非UTF-8字符。重新在终端用cat命令生成纯ASCII的HTML文件,彻底解决编码问题。

九、心得体会

这个项目让我深刻体会到了“软件定义硬件”在边缘计算中的价值。RDK X5强大的AI算力和丰富的接口使我们可以用一块板子替代过去“工控机+AI加速卡+网关”的组合,但同时也对软件架构设计提出了极高要求。多协议、多任务并发下,合理分配CPU资源、严格区分实时与非实时线程是系统稳定运行的基石。





软硬件
元器件
STM32F103RCT6
主流高性能系列,Arm Cortex-M3 MCU,具有256 KB Flash存储器,72 MHz CPU,电机控制,USB和CAN
LAN9252I/ML
集成电路以太网控制器 10/100 64引脚四方扁平无引线封装
STM32G474CEU6
带DSP和FPU的170MHz主流ARM Cortex-M4 MCU,具有512 KB Flash存储器、数学加速器、高分辨率定时器和高模拟电平集成
电路图
附件下载
00_CAN_MODBUS.zip
环境采集节点代码
CODE.7z
RDK X5代码+YOLOV8模型
ProPrj_09_LAN9252_2026-06-09.epro2
ProPrj_STM32F103RCT6_MODBUS_2026-06-09.epro2
ProPrj_STM32g474-color-board_2026-06-09.epro2
ECAT_IO.tar.xz
stm32g474+lan9252代码
团队介绍
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号