FastBond4挑战部分-基于RDK X5与AM243x的工业EtherCAT实时IO控制系统
该项目使用了RDK X5与AM243x,实现了工业EtherCAT实时IO控制系统的设计,它的主要功能为:实现了数字量输入输出、蜂鸣器告警的硬实时控制,并通过网页端仪表盘实现远程监视与控制。
标签
EtherCAT
LP-AM243
RDK_X5
空耳-
更新2026-06-26
73

FastBond4-基于RDK X5与AM243x的工业EtherCAT实时IO控制系统

一、项目介绍

本项目构建了一套基于 EtherCAT 工业以太网的实时 IO 采集与控制系统。以地平线RDK X5边缘计算平台作为 EtherCAT 主站,TI AM243x-LP 作为 EtherCAT 从站 IO 节点,实现了数字量输入输出、蜂鸣器告警的硬实时控制,并通过网页端仪表盘实现远程监视与控制。

系统当前已实现功能:

  • 4路数字量输入 (DI):GPIO 直接读取,EtherCAT PDO 实时上传
  • 3路数字量输出 (DO):GPIO 直接输出 3.3V 电平
  • 1路无源蜂鸣器:软件模拟 PWM
  • 网页端 IO 控制面板:实时显示 DI 状态,远程控制 DO/蜂鸣器

规划扩展功能:

  • 4路 ±10V 模拟量采集 (ADC):基于 AM243x 片内 12-bit SAR ADC + 外部运放电路
  • FOC 电机控制:MP6536DU-LF-Z 三相栅极驱动器
  • 0-10V 模拟量输出 (DAC):MCP4728-E/UN 四通道 12-bit DAC,I2C 接口

二、使用到的硬件

硬件

角色

说明

RDK X5

EtherCAT 主站 + Web 服务器

8核A55, Ubuntu 22.04+RT内核, IgH Master

AM243x-LP

EtherCAT 从站 IO 节点

Cortex-R5F, PRU-ICSSG, Beckhoff SSC

无源蜂鸣器

告警输出

GPIO 直驱, 方波驱动

MP6536DU-LF-Z

FOC 驱动 (规划中)

三相栅极驱动器

MCP4728-E/UN

DAC (规划中)

4通道12-bit, I2C

7f79508aa1d18703e7b183b52336c797.jpg

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

3.1 系统架构

image.png

3.2 数据流

image.png

3.3 设计思路

为什么选择 EtherCAT?
工业 IO 控制要求毫秒级确定性延迟。EtherCAT 是主流工业实时以太网协议,具有以下优势:

  • 硬实时:帧处理在硬件(PRU-ICSSG)中完成,不受 CPU 负载影响
  • 确定性:每个周期 IO 刷新时间固定,从站间同步精度 <1μs
  • 单电缆:标准以太网物理层,一根网线走所有从站

为什么是双芯片架构?

  • RDK X5:运行 Linux + IgH Master,适合 Web 服务、Python 脚本、网络通信等非实时任务。8核 A55 提供足够的算力余量,BPU 可扩展 AI 推理。
  • AM243x-LP:Cortex-R5F 实时核心 + PRU-ICSSG,专为工业通信设计。片内多个 GPIO Bank 直接驱动 IO,无需外扩 IO 芯片。FreeRTOS 保证 IO 刷新在 EtherCAT 周期内完成。
  • 职责分离:Linux 侧处理人机交互和业务逻辑,R5F 侧专注硬实时 IO 刷新,两者通过 EtherCAT PDO 解耦。

模块化扩展
系统设计为可扩展架构:当前实现的 DI/DO/蜂鸣器验证了 EtherCAT 通信链路和 GPIO 驱动框架。后续添加 ADC/FOC/DAC 仅需:

  1. SysConfig 中使能对应外设(ADC/I2C/EPWM)
  2. 编写外设驱动并注册到 PDO 映射
  3. 在 APPL_Application 中调用驱动函数
    无需改动 EtherCAT 协议栈和应用层框架。

四、IO端口映射 (AM243x-LP)

信号

AM243x引脚

GPIO Bank

PDO映射

说明

DI0

PRG0_PRU1_GPO1

GPIO1

0x6000.1 bit0

GPIO直接输入

DI1

PRG0_PRU1_GPO2

GPIO1

0x6000.1 bit1

GPIO直接输入

DI2

PRG0_PRU1_GPO6

GPIO1

0x6000.1 bit2

GPIO直接输入

DI3

PRG0_PRU1_GPO11

GPIO1

0x6000.1 bit3

GPIO直接输入

DO0

MMC1_SDCD

GPIO1

0x7010.1 bit0

GPIO直接输出3.3V

DO1

MMC1_SDWP

GPIO1

0x7010.1 bit1

GPIO直接输出3.3V

DO2

PRG0_PRU1_GPO12

GPIO1

0x7010.1 bit2

GPIO直接输出3.3V

Buzzer

PRG0_PRU0_GPO12

GPIO1

0x7010.1 bit3

GPIO直接驱动

VREF_EN

PRG0_PRU0_GPO18

GPIO1

ADC偏置使能(预留)

PDO 结构:

  • TxPDO (从站→主站):0x6000 SubIdx 1, UINT8 — DI0-3 (bit0-3)
  • RxPDO (主站→从站):0x7010 SubIdx 1, UINT8 — DO0-2 (bit0-2), Buzzer (bit3)


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

5.1 使用的订单物料

物料

用途

DigiKey 链接

AD8591ARTZ-REEL7

ADC 偏置电压基准源 (1.65V)

AD8591ARTZ-REEL7

F1C1-322512-4R7M

电感

F1C1-322512-4R7M

5.2 原理图

5.2.1 ADC采集部分

  • 1.65V偏置电压
    AD8591ARTZ-REEL7电压跟随器输入前端是基准电压+分压电阻电路。 AZ431AN 是一个三端可调基准电压芯片,根据芯片典型电路运用,输出 2.5V 基准电压,即图中 R6 电阻左边电压为 2.5V。 使用 R6 和 R7 分压得到 1.65V 的电压,电压跟随器理想状态下是输入阻抗无穷大,输出阻抗等于 0,正向电压传输系数是 1,就是说电压跟随器的电压放大倍数恒小于且接近 1,输出电压也是 1.65V,提高带载能力。image.png
  • 运放供电
    采用MT3608L DCDC,它是一款高效率的2.5A 1.2MHz电流模式升压转换器,适用于小型低功耗应用。它具有2.2V至16V的输入电压范围,可调输出电压,内置80mΩ功率MOSFET,自动轻载脉冲频率调制模式,过压保护等功能。
    这里主要是给运放供电。image.png
  • 运放调理部分
    根据差分运算放大器典型电路image.png选用电阻使得: Rm = Rf , Rp = Rn那么: Vout = Vref + 𝑅𝑓 𝑅𝑛 ∙ (𝑉𝑝 - 𝑉𝑛) ,以 AIN1 输入端为例子,
    Rm = Rf = R151 = 3.24K , Rp = Rn = R150 = 20K并且,这里 Vn 直接连接到 GND,即 0V,所以: VOUT = 1.65+(3.24/20)*VAIN
    假设 AIN1 端子接入 10V 电压,那么 ADC0_AIN0 引脚电压应该是: Vout=1.65V+3.24/20*10V=3.27V
    另外, AIN1 端子接入-10V 电压,那么 ADC0_AIN0 引脚电压应该是: Vout=1.65V+3.24/20*-10V=0.03Vimage.png

5.2.2 DAC部分

  • I2C转DAC
    采用MCP4728-E/UN,他具有四个缓冲输出的12位电压输出数模转换器(DAC)。用于存储DAC代码和I²C地址位的板载非易失性存储器(EEPROM)。可选择内部或外部电压基准。image.png
  • 运放调理部分
    采用TLV2374 ,根据差分运算放大器典型电路 ,以输出端 AOUT1 为例子 ,AOUT1 = (10/3.24)*DAC1,可以实现0——10Vdac输出。image.png

5.2.3 DI DO部分

这里是直连芯片的,没有什么特别之处

image.png

5.2.4 FOC部分

采用MP6536DU-LF-Z 3通道半桥驱动IC,用于驱动三相无刷直流电机。 MP6536具有低电流关断模式、待机模式、输入欠压保护、电流限制、热关断和故障标志信号输出等特性。驱动器的所有通道都可与标准逻辑信号接口。 MP6536采用40引脚、5mmx5mm的QFN封装。image.png

电流环采用了两个INA240

image.png

5.2.5 其他拓展部分

Booster Pack Header接口

image.png

WS2812及buzzer

image.png

5.3 PCB

image.png

pcb渲染图

image.png

实物图:

image.png

六、IgH EtherCAT Master 移植到 RDK X5

6.1 启用实时内核

# 切换 RT 内核
sudo sed -i 's/imagefile="Image"/imagefile="Image-rt"/' /boot/boot.cmd
cd /boot && sudo mkimage -C none -A arm -T script -d boot.cmd boot.scr
sudo reboot

验证实时性:

sudo apt-get install rt-tests
sudo cyclictest -p80 -t4 -i1000 -l100000
# 结果: 最大延迟 ~62us, 典型延迟 ~24us

image.png

6.2 交叉编译 IgH

宿主机 Ubuntu 22.04,ARM64 工具链:

#安装环境依赖
sudo apt-get install -y build-essential make cmake libpcre3 libpcre3-dev bc bison \
                      flex python3-numpy python3-pip mtd-utils zlib1g-dev debootstrap \
                      libdata-hexdumper-perl libncurses5-dev zip qemu-user-static \
                       curl repo git liblz4-tool apt-cacher-ng libssl-dev checkpolicy autoconf \
                      android-sdk-libsparse-utils mtools parted dosfstools udev rsync device-tree-compiler u-boot-tools ccache

#安装交叉编译工具链
curl -fO http://archive.d-robotics.cc/toolchain/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu.tar.xz
sudo tar -xvf gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu.tar.xz -C /opt
export CROSS_COMPILE=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-

准备匹配的内核源码:

# 源码下载
repo init -u git@github.com:D-Robotics/x5-manifest.git -b main && repo sync
# 将标准内核源码复制为 kernel-rt
cp -rf source/kernel source/kernel-rt
# 进入该目录并打上 RT 补丁(补丁文件就在当前目录下)
cd source/kernel-rt && patch -p1 < ../../patch-6.1.83-rt28.patch
cd ../..   # 回到 SDK 根目录
export KERNEL_SRC=~/RDK/rdk_x5_source/source/kernel-rt
cd $KERNEL_SRC

# 在 RDK X5 板端执行以下命令,将配置导出为纯文本文件:
zcat /proc/config.gz > ~/kernel_config_rt
# 在宿主机$KERNEL_SRC目录下创建.config文件,将配置文件替换
cp ~/kernel_config_rt .config  
# 生成编译外部模块所需的辅助工具和头文件
make ARCH=arm64 CROSS_COMPILE=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- oldconfig
make ARCH=arm64 CROSS_COMPILE=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu- modules_prepare
# 这一步会生成fixdep、modpost等工具,并确保所有头文件与当前配置一致。

编译安装 IgH EtherCAT Master:

# 克隆代码
git clone https://gitlab.com/etherlab.org/ethercat.git && cd ethercat && git checkout stable-1.5
# 清理并生成 configure 脚本
make clean
./bootstrap
# 交叉编译配置(关键参数)
./configure \
   --prefix=/opt/etherlab \
   --host=aarch64-none-linux-gnu \
   CC=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-gcc \
   CXX=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-g++ \
   --with-linux-dir=$KERNEL_SRC \
   --enable-generic=yes \
   --enable-stmmac=no \
   --enable-8139too=no \
   --enable-e100=no \
   --enable-e1000=no \
   --enable-e1000e=no \
   --enable-igb=no \
   --enable-usb=no \
   --disable-eoe
# 交叉编译
export PATH=/opt/gcc-arm-11.2-2022.02-x86_64-aarch64-none-linux-gnu/bin:$PATH
export CROSS_COMPILE=aarch64-none-linux-gnu-
export ARCH=arm64

# 编译内核模块(ec_master.ko, ec_generic.ko)
make ARCH=arm64 modules

# 编译用户空间工具和库
make ARCH=arm64

# 安装到临时目录并打包
mkdir -p ethercat_install
make install DESTDIR=/home/kone/RDK/ethercat/ethercat_install

tar -czf ethercat_arm64.tar.gz -C ~/ethercat_install .

6.3 板端部署与启动

# 解压安装
sudo tar -xzf ethercat_arm64.tar.gz -C /opt/
# 1.内核模块(ec_master.ko, ec_generic.ko)
sudo mkdir -p /lib/modules/$(uname -r)/ethercat
sudo cp /opt/etherlab/lib/modules/ec_master.ko /lib/modules/$(uname -r)/ethercat/
sudo cp /opt/etherlab/lib/modules/ec_generic.ko /lib/modules/$(uname -r)/ethercat/

# 更新模块依赖
sudo depmod -a

echo "/opt/etherlab/lib" | sudo tee /etc/ld.so.conf.d/etherlab.conf && sudo ldconfig
echo 'export PATH=/opt/etherlab/bin:/opt/etherlab/sbin:$PATH' >> ~/.bashrc && source ~/.bashrc

# 启动主站
ETH0_MAC=$(cat /sys/class/net/eth0/address)
sudo modprobe ec_master main_devices="$ETH0_MAC" && sudo modprobe ec_generic
ethercat slaves   # 应显示 AM243x 从站

七、EtherCAT从站软件设计

7.1 SysConfig 配置

// example.syscfg — GPIO 引脚分配
const gpio3 = gpio.addInstance(); gpio3.$name="CONFIG_DI0"; gpio3.pinDir="INPUT";
  gpio3.GPIO.$assign="GPIO1"; gpio3.GPIO.gpioPin.$assign="PRG0_PRU1_GPO1";
const gpio4 = gpio.addInstance(); gpio4.$name="CONFIG_DI1"; gpio4.pinDir="INPUT";
  gpio4.GPIO.$assign="GPIO1"; gpio4.GPIO.gpioPin.$assign="PRG0_PRU1_GPO2";
const gpio5 = gpio.addInstance(); gpio5.$name="CONFIG_DI2"; gpio5.pinDir="INPUT";
  gpio5.GPIO.$assign="GPIO1"; gpio5.GPIO.gpioPin.$assign="PRG0_PRU1_GPO6";
const gpio6 = gpio.addInstance(); gpio6.$name="CONFIG_DI3"; gpio6.pinDir="INPUT";
  gpio6.GPIO.$assign="GPIO1"; gpio6.GPIO.gpioPin.$assign="PRG0_PRU1_GPO11";
const gpio7 = gpio.addInstance(); gpio7.$name="CONFIG_DO0"; gpio7.pinDir="OUTPUT";
  gpio7.GPIO.$assign="GPIO1"; gpio7.GPIO.gpioPin.$assign="MMC1_SDCD";
const gpio8 = gpio.addInstance(); gpio8.$name="CONFIG_DO1"; gpio8.pinDir="OUTPUT";
  gpio8.GPIO.$assign="GPIO1"; gpio8.GPIO.gpioPin.$assign="MMC1_SDWP";
const gpio9 = gpio.addInstance(); gpio9.$name="CONFIG_DO2"; gpio9.pinDir="OUTPUT";
  gpio9.GPIO.$assign="GPIO1"; gpio9.GPIO.gpioPin.$assign="PRG0_PRU1_GPO12";
const gpio11= gpio.addInstance(); gpio11.$name="CONFIG_BUZZER"; gpio11.pinDir="OUTPUT";
  gpio11.GPIO.$assign="GPIO1"; gpio11.GPIO.gpioPin.$assign="PRG0_PRU0_GPO12";
const gpio12= gpio.addInstance(); gpio12.$name="CONFIG_VREF_EN"; gpio12.pinDir="OUTPUT";
  gpio12.GPIO.$assign="GPIO1"; gpio12.GPIO.gpioPin.$assign="PRG0_PRU0_GPO18";

7.2 软件架构

main.c → freertos_main()ethercat_slave_beckhoff_ssc_demo_main()
└── common_main()
      ├── Drivers_open()       # SysConfig 生成的时钟/PinMux/外设初始化
      ├── Board_driversOpen()
      ├── tiesc_socEvmInit()   # 板级初始化
      └── task1()               # EtherCAT 主循环任务
            └── APPL_Application()   # 每周期调用一次

7.3 关键代码

tiesc_gpio.h / tiesc_gpio.c — GPIO 驱动层:

// DI 读取 (4通道位操作)
uint8_t tiesc_readDi(void) {
   uint8_t val = 0;
   if(GPIO_pinRead(CONFIG_DI0_BASE_ADDR, CONFIG_DI0_PIN)) val |= 0x01;
   if(GPIO_pinRead(CONFIG_DI1_BASE_ADDR, CONFIG_DI1_PIN)) val |= 0x02;
   if(GPIO_pinRead(CONFIG_DI2_BASE_ADDR, CONFIG_DI2_PIN)) val |= 0x04;
   if(GPIO_pinRead(CONFIG_DI3_BASE_ADDR, CONFIG_DI3_PIN)) val |= 0x08;
   return val;
}

// DO 输出 (3通道 + 蜂鸣器)
void tiesc_writeDo(uint8_t val) {
   if(val&0x01) GPIO_pinWriteHigh(CONFIG_DO0_BASE_ADDR, CONFIG_DO0_PIN);
   else         GPIO_pinWriteLow (CONFIG_DO0_BASE_ADDR, CONFIG_DO0_PIN);
   // DO1 (bit1), DO2 (bit2) 同理

   // 无源蜂鸣器 (bit3): 软件翻转产生 ~5kHz 方波
   static int buzToggle = 0;
   if(val & 0x08) { buzToggle ^= 1; tiesc_setBuzzer(buzToggle); }
   else           { buzToggle = 0;  tiesc_setBuzzer(0); }
}

tiescappl.c — EtherCAT 应用层 IO 同步:

void APPL_Application(void) {
   uint8_t LED = sDOOutputs.LEDs;  // 从 RxPDO 读取主站指令
   appState = sDO1Outputs.Cmd;

   tiesc_writeDo(LED & 0x07);       // DO0-2 直驱
   if(LED & 0x08) { /* 蜂鸣器 PWM */ }

   sDIInputs.switchs = tiesc_readDi();  // 读 DI 并写入 TxPDO
}

7.4 运行时序

EtherCAT Cycle (~1ms):
RxPDO(SM2): DO[7:0] ← 主站
  → APPL_OutputMapping()LED 变量
  → tiesc_writeDo(LED)GPIO 输出

TxPDO(SM3): DI[7:0] → 主站
  → tiesc_readDi() → sDIInputs.switchs
  → APPL_InputMapping()ESC 内存

八、RDK X5 端控制

8.1 命令行控制

# 读 DI (状态查询)
ethercat upload -p0 -t uint8 0x6000 1

# 写 DO+蜂鸣器
ethercat download -p0 -t uint8 0x7010 1 0x01  # DO0 ON
ethercat download -p0 -t uint8 0x7010 1 0x08  # Buzzer ON
ethercat download -p0 -t uint8 0x7010 1 0x09  # DO0 + Buzzer
ethercat download -p0 -t uint8 0x7010 1 0x00  # All OFF

# 进入 OP 模式
ethercat state -p0 OP

8.2 Python 控制脚本

import subprocess, re

def read_di(slave=0):
   out = subprocess.run(["ethercat", "upload", "-p", str(slave),
       "-t", "uint8", "0x6000", "1"], capture_output=True, text=True)
   m = re.search(r'0x([0-9a-fA-F]+)', out.stdout)
   return int(m.group(1), 16) if m else 0

def write_do(val, slave=0):
   subprocess.run(["ethercat", "download", "-p", str(slave),
       "-t", "uint8", "0x7010", "1", f"0x{val:02X}"])

# 示例:读 DI 并打印
di = read_di()
print(f"DI0={di>>0&1} DI1={di>>1&1} DI2={di>>2&1} DI3={di>>3&1}")

8.3 网页 IO 控制面板

基于 Python HTTP Server + 命令行 ethercat 工具实现,前端采用深色科幻风格 UI 设计:

  • 左侧 DI 面板:4个绿色圆形指示灯,实时 200ms 轮询显示 DI 状态
  • 右侧 DO 面板:3个 DO 开关按钮 + 1个橙色蜂鸣器按钮,点击即下发 EtherCAT SDO 指令
  • 粒子动画背景:Canvas 深色粒子网络动效
#!/usr/bin/env python3
"""EtherCAT IO Web Control Panel"""

import subprocess, json, http.server, urllib.parse, re

SLAVE_POS = 0
OBJ_DI = "0x6000"
OBJ_DO = "0x7010"

def ecat_cmd(cmd):
  try:
      r = subprocess.run(["ethercat"] + cmd.split(), capture_output=True, text=True, timeout=3)
      return r.stdout.strip()
  except: return None

def read_di():
  out = ecat_cmd(f"upload -p {SLAVE_POS} -t uint8 {OBJ_DI} 1")
  if out:
      m = re.search(r'0x([0-9a-fA-F]+)', out)
      if m: return int(m.group(1), 16)
  return 0

def write_do(val):
  ecat_cmd(f"download -p {SLAVE_POS} -t uint8 {OBJ_DO} 1 0x{val:02X}")

class Handler(http.server.BaseHTTPRequestHandler):
  def do_GET(self):
      path = urllib.parse.urlparse(self.path).path
      if path == "/api/di":
          val = read_di()
          self.send_json({"di": [bool(val>>i & 1) for i in range(8)]})
      elif path == "/":
          self.send_html()
      else: self.send_error(404)

  def do_POST(self):
      path = urllib.parse.urlparse(self.path).path
      length = int(self.headers.get("Content-Length", 0))
      body = urllib.parse.parse_qs(self.rfile.read(length).decode()) if length else {}
      if path == "/api/do":
          val = int(body.get("val", ["0"])[0], 16)
          write_do(val)
          self.send_json({"ok": True})

  def send_json(self, data):
      self.send_response(200)
      self.send_header("Content-Type", "application/json")
      self.end_headers()
      self.wfile.write(json.dumps(data).encode())

  def send_html(self):
      h = HTML
      self.send_response(200)
      self.send_header("Content-Type", "text/html; charset=utf-8")
      self.end_headers()
      self.wfile.write(h.encode())

HTML = r'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>EtherCAT IO Panel</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#0a0a1a;color:#ccc;font-family:'Segoe UI',system-ui,sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:center;overflow:hidden}
.bg{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0}
.card{position:relative;z-index:1;background:rgba(18,18,45,0.88);backdrop-filter:blur(24px);border:1px solid rgba(100,140,255,0.18);border-radius:26px;padding:38px 48px;box-shadow:0 0 100px rgba(80,120,255,0.1)}
h1{font-size:30px;font-weight:700;letter-spacing:3px;margin-bottom:6px;background:linear-gradient(135deg,#6af,#c6f);-webkit-background-clip:text;-webkit-text-fill-color:transparent}
.sub{font-size:13px;color:#556;margin-bottom:30px;letter-spacing:1px}
.row{display:flex;gap:20px;flex-wrap:wrap;justify-content:center}
.sec{flex:1;min-width:210px;background:rgba(255,255,255,0.025);border-radius:18px;padding:22px 24px;border:1px solid rgba(255,255,255,0.05)}
.sec h2{font-size:14px;text-transform:uppercase;letter-spacing:3px;margin-bottom:18px;color:#778}
.io{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;margin:5px 0;border-radius:10px;background:rgba(255,255,255,0.018);transition:.2s}
.io:hover{background:rgba(255,255,255,0.05)}
.name{font-size:14px;font-weight:500;letter-spacing:1px}
.dot{width:14px;height:14px;border-radius:50%;border:2px solid #333;transition:.15s}
.dot.on{background:#0f6;border-color:#0f6;box-shadow:0 0 14px #0f6}
.btn{width:66px;height:36px;border:none;border-radius:18px;font-size:13px;font-weight:700;cursor:pointer;letter-spacing:1px;transition:.25s}
.btn.off{background:rgba(255,255,255,0.05);color:#445}
.btn.on{background:linear-gradient(135deg,#05f,#6af);color:#fff;box-shadow:0 0 20px rgba(0,80,255,0.35)}
.btn.buzz{background:linear-gradient(135deg,#f60,#f90);color:#fff;box-shadow:0 0 20px rgba(255,100,0,0.35)}
.btn:active{transform:scale(0.9)}
.bar{margin-top:22px;padding:10px 18px;border-radius:10px;background:rgba(0,0,0,0.3);font-size:12px;color:#556;display:flex;justify-content:space-between}
.live{width:8px;height:8px;border-radius:50%;display:inline-block;margin-right:6px;background:#0f6;box-shadow:0 0 8px #0f6;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.2}}
</style>
</head>
<body>
<div class="bg"><canvas id="c"></canvas></div>
<div class="card">
<h1>&#x26A1; Slave IO Panel</h1>
<div class="sub">4x DI &middot; 3x DO &middot; Buzzer</div>
<div class="row">
<div class="sec"><h2>&#x2B07; Inputs</h2><div id="di"></div></div>
<div class="sec"><h2>&#x2B06; Outputs</h2><div id="do"></div></div>
</div>
<div class="bar"><span><span class="live"></span>Live</span><span id="ts">--</span><span>200ms</span></div>
</div>
<script>
var $ = function(id){return document.getElementById(id)};
var E = function(t,c,a){var e=document.createElement(t);if(c)e.className=c;if(a)for(var k in a)e[k]=a[k];return e};

['DI0','DI1','DI2','DI3'].forEach(function(n,i){
  var d = E('div','io');
  d.append(E('span','name',{textContent:n}));
  d.append(E('div','dot',{id:'di-'+i}));
  $('di').append(d);
});
$('di').append(E('span','',{textContent:'(DI4-7 unused)',style:'font-size:11px;color:#333'}));

var st = 0, buzz = false;
['DO0','DO1','DO2'].forEach(function(n,i){
  var d = E('div','io');
  d.append(E('span','name',{textContent:n}));
  var b = E('button','btn off',{textContent:'OFF'});
  b.onclick = (function(idx){return function(){st ^= (1<<idx); apply()}})(i);
  d.append(b); $('do').append(d);
});
(function(){
  var d = E('div','io');
  d.append(E('span','name',{textContent:'Buzzer'}));
  var b = E('button','btn off',{textContent:'OFF',id:'buzzer'});
  b.onclick = function(){buzz = !buzz; apply()};
  d.append(b); $('do').append(d);
})();

function apply(){
  var v = st | (buzz ? 0x08 : 0);
  var x = new XMLHttpRequest();
  x.open('POST','/api/do');
  x.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
  x.send('val=' + v.toString(16));
  upUI();
}
function upUI(){
  for(var i=0;i<3;i++){
      var b = $('do').children[i].children[1];
      b.className = 'btn ' + ((st>>i)&1 ? 'on' : 'off');
      b.textContent = (st>>i)&1 ? 'ON' : 'OFF';
  }
  var bz = $('buzzer');
  bz.className = 'btn ' + (buzz ? 'buzz' : 'off');
  bz.textContent = buzz ? 'ON' : 'OFF';
}
function poll(){
  fetch('/api/di').then(function(r){return r.json()}).then(function(d){
      d.di.forEach(function(v,i){var e=$('di-'+i);if(e)e.className='dot'+(v?' on':'')});
      $('ts').textContent = new Date().toLocaleTimeString();
  }).catch(function(){});
}
setInterval(poll,200); poll(); upUI();

// particle background
(function(){
var c=$('c'),x=c.getContext('2d'),W,H,p=[];
function R(){W=c.width=innerWidth;H=c.height=innerHeight}R();window.onresize=R;
for(var i=0;i<80;i++)p.push({x:Math.random()*W,y:Math.random()*H,r:.8+Math.random()*2,vx:.4-Math.random()*.8,vy:.4-Math.random()*.8});
function A(){
  x.clearRect(0,0,W,H);
  for(var a=0;a<p.length;a++){p[a].x+=p[a].vx;p[a].y+=p[a].vy;if(p[a].x<0)p[a].x=W;if(p[a].x>W)p[a].x=0;if(p[a].y<0)p[a].y=H;if(p[a].y>H)p[a].y=0;x.beginPath();x.arc(p[a].x,p[a].y,p[a].r,0,6.28);x.fillStyle='rgba(100,150,255,.14)';x.fill()}
  for(var i=0;i<p.length;i++)for(var j=i+1;j<p.length;j++){
      var dx=p[i].x-p[j].x,dy=p[i].y-p[j].y,d=dx*dx+dy*dy;
      if(d<12000){x.beginPath();x.moveTo(p[i].x,p[i].y);x.lineTo(p[j].x,p[j].y);x.strokeStyle='rgba(100,150,255,'+(.06-d/200000)+')';x.stroke()}
  }
  requestAnimationFrame(A);
}
A();
})();
</script>
</body>
</html>'''

if __name__ == "__main__":
  PORT = 8888
  print(f"\n [EtherCAT IO Panel] Slave #{SLAVE_POS}")
  print(f" http://localhost:{PORT}\n")
  http.server.HTTPServer(("0.0.0.0", PORT), Handler).serve_forever()

运行方式:

sudo python3 ecat_web.py   # 监听 8888 端口
# 浏览器打开 http://<RDK_X5_IP>:8888

九、规划扩展功能

9.1 4路 ±10V 模拟量采集 (ADC)

利用 AM243x 片内 10-bit SAR ADC (ADC0),外部信号调理电路:

  • 偏置电压:1.65V (AD8591 基准源,GPIO 使能)
  • 运放电路:Rin=3.24kΩ, Rfb=20kΩ, 分压 R1=2.94kΩ, R2=3.48kΩ
  • 输入范围:-10V → ADC 0.016V, +10V → ADC 1.756V
  • ADC 配置:4通道单次转换, 16样本平均, FIFO0 阈值等待

9.2 FOC 电机控制 (MP6536DU-LF-Z)

  • 三相栅极驱动器,通过 PWM 信号控制无刷电机
  • 需要 AM243x EPWM 模块生成 6路互补 PWM
  • 电流采样反馈闭环控制

9.3 0-10V 模拟量输出 (MCP4728-E/UN)

  • 四通道 12-bit DAC,I2C 接口
  • AM243x I2C0 总线控制
  • 输出电压范围 0-10V (需外部运放增益)

十、问题与解决

问题

原因

解决方法

IgH扫描不到从站

PRU固件未加载

确认串口输出 "SYNC0/SYNC1 task started"

内核模块vermagic不匹配

源码/配置与板端不一致

必须用板端 zcat /proc/config.gz 作为 .config

IO控制网页中文乱码

编码问题

全部改为英文,兼容所有浏览器

十一、功能展示

首先就是硬件连接图

a4edccced3d1335c375ee05a14121155.jpg

网页控制面板

image.png

IO控制演示

当镊子短接DI1时

image.png

测量DO1输出

image.png

十二、心得体会

本项目实现了从 EtherCAT 主站协议栈交叉编译部署到从站 IO 驱动的完整工业以太网链路。在实践中深刻体会到了几点:

  1. SysConfig 是 MCU+ SDK 的灵魂:引脚分配、时钟使能、外设初始化全部由 SysConfig 自动生成,手动操作极易出错。添加 ADC 模块时必须同步修改
  2. 跨平台交叉编译的版本一致性至关重要:IgH 内核模块编译必须使用与板端完全匹配的内核源码和 .config,否则 vermagic 不匹配导致模块加载失败。
软硬件
元器件
MP6536DU-LF-Z
集成电路半桥驱动器 5.5安培 40引脚QFN封装
MCP4728-E/UN
12位四路输出DAC,带EEPROM和I2C™接口
AD8591ARTZ-REEL7
单路、CMOS单电源轨到轨输入/输出运算放大器,具有±250 mA输出电流和省电关断模式
电路图
附件下载
lp_am243x_code.zip
ProPrj_LP-AM243 IO AD采集_2026-06-26.epro2
团队介绍
kone
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号