EtherCAT的igh学习与研究(一)

简述

IgH EtherCAT Master 是 Linux 平台上一款开源的 EtherCAT 主站实现,广泛应用于工业自动化领域。本项目详细记录了 IgH 主站的启动流程、配置方法、多主站部署以及时钟同步等核心技术的学习心得 。

主要内容

  • 启动流程分析(systemd / SysV init 两种方式)

  • 配置文件详解

  • 内核模块加载原理

  • 多主站配置与冗余

  • DC 时钟同步

  • 故障排查与性能调试

技术特性

  • ✅ 支持单主站/多主站配置

  • ✅ 支持双网卡冗余备份

  • ✅ 支持 DC 时钟同步

  • ✅ 兼容多种网卡驱动

  • ✅ 支持 EOE、COE、FOE等等

环境要求

  • Linux 内核

  • IgH EtherCAT 主站源码

  • 兼容的网卡设备

资源参考

  • IgH EtherCAT Master 官方源码

  • EtherCAT 技术规范

正文

目标平台 : RK3576 ARM64 IgH 版本 : 1.5.2 内核版本: Linux 6.12

一、启动流程

1.1 启动流程概述

1.1.1 整体架构图

EtherCAT 主站的启动是一个涉及用户空间和内核空间的多层协作过程。

核心要点:

  1. 用户空间配置驱动内核行为 - 通过配置文件传递 MAC 地址等参数

  2. 双模块协作 - ec_master.ko 提供框架,ec_xxx.ko 驱动硬件

  3. 完全旁路 Linux 网络栈 - 实现微秒级实时响应

  4. DMA 零拷贝 - 主站与驱动直接共享内存

1.2 配置文件详解

1.2.1 /etc/ethercat.conf - 主配置文件(systemd 用)

文件路径: /usr/local/etc/ethercat.conf(构建产物) → /etc/ethercat.conf(实际使用)

文件作用:

  • 定义主站使用的网卡设备(通过 MAC 地址)

  • 配置设备驱动模块

  • 配置 EOE(EtherCAT over Ethernet)接口

  • 主要供 ethercatctl 脚本使用

配置项 说明 示例值 是否必需
MASTER0_DEVICE 主站 0 的网卡 MAC 地址 c4:83:4f:27:30:5b 必需
MASTER1_DEVICE 主站 1 的网卡 MAC 地址 (可选) 可选
MASTER0_BACKUP 主站 0 的备份设备 MAC 地址 (可选,用于冗余) 可选
DEVICE_MODULES 网卡驱动模块名称 dwmac-rk/igb/generic 必需
EOE_INTERFACES EOE 虚拟接口 eoe0s20 可选
LINK_DEVICES 启动时 UP 的网卡 eth0 推荐

1.2.2 /etc/sysconfig/ethercat - 模块参数配置(SysV 用)

文件路径: /etc/sysconfig/ethercat

文件作用:

  • 为内核模块传递参数

  • 定义系统启动时加载的模块和行为

  • 主要供 init.d/ethercat 脚本使用

1.2.3 /etc/init.d/ethercat - SysV init 启动脚本

文件路径: /etc/init.d/ethercat

文件作用:

  • 传统的 Linux SysV init 启动脚本

  • 负责加载/卸载内核模块

  • 管理系统启动 (stop/start/restart/status) 流程

启动流程分析

以下是基于提供的Mermaid语法生成的流程图文本描述及解释:

流程图解析

复制代码
graph TB
    START[开始] --> R1[读取配置]
    R1 --> R2[解析 MAC 地址]
    R2 --> R3[设置网口 UP]
    R3 --> R4[加载 ec_master 模块]
    R4 --> R5[卸载原网卡驱动]
    R5 --> R6[加载 EtherCAT 驱动]
    R6 --> R7[创建设备节点]
    R7 --> END[完成]

流程说明

该流程图描述了一个典型的EtherCAT主站初始化过程。从开始到完成的7个关键步骤按顺序执行,每个步骤依赖前一个步骤的输出。

关键步骤说明

读取配置:从配置文件中获取必要的参数,包括网络接口、MAC地址等。

解析MAC地址:将配置中的MAC地址字符串转换为驱动可识别的格式。

设置网口UP:通过系统命令激活指定的网络接口。

加载ec_master模块:加载EtherCAT主站核心模块,提供基础通信能力。

卸载原网卡驱动:移除系统原有的网络驱动以避免冲突。

加载EtherCAT驱动:加载专用的EtherCAT协议栈驱动。

创建设备节点:在/dev目录下生成EtherCAT设备文件供应用程序访问。

关键代码片段:

复制代码
# 读取配置
ETHERCAT_CONFIG=/etc/sysconfig/ethercat
. ${ETHERCAT_CONFIG}

# 解析 MAC 地址
while true; do
    DEVICE=$(eval echo "\${MASTER${MASTER_INDEX}_DEVICE}")
    if [ -z "${DEVICE}" ]; then break; fi
    parse_mac_address ${DEVICE}
    DEVICES=${DEVICES}${MAC}
done

# 加载主站模块
modprobe ec_master main_devices=${DEVICES} backup_devices=${BACKUPS}

# 替换网卡驱动
for MODULE in ${DEVICE_MODULES}; do
    rmmod ${MODULE}
    modprobe ec_${MODULE}
done

1.2.4 /usr/local/sbin/ethercatctl - systemd 启动脚本

文件路径: /usr/local/sbin/ethercatctl

文件作用:

  • systemd 环境下被调用的启动脚本

  • 读取 /etc/ethercat.conf 配置文件

  • 与 init.d 脚本功能类似

与 init.d/ethercat 的对比

差异点 ethercatctl init.d/ethercat
配置文件 /etc/ethercat.conf /etc/sysconfig/ethercat
模块参数传递 main_devices= main_devices=
驱动替换逻辑 相同 相同

1.2.5 ethercat.service - systemd 服务单元

文件路径: /usr/local/lib/systemd/system/ethercat.service

文件作用:

  • 定义 systemd 服务单元

  • 指定启动/停止命令

  • 设置依赖关系

文件内容:

复制代码
[Unit]
Description=EtherCAT Master Kernel Modules

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/ethercatctl start
ExecStop=/usr/local/sbin/ethercatctl stop

[Install]
WantedBy=multi-user.target

关键参数说明

参数 说明
Type oneshot 服务只执行一次就退出
RemainAfterExit yes 退出后仍保持活跃状态
ExecStart ethercatctl start 启动时执行的命令
WantedBy multi-user.target 开机自启目标

1.3 两种启动方式对比

IgH EtherCAT Master 支持两种启动方式:systemdSysV init

1.3.1 方式一:systemd 启动

EtherCAT 系统启动流程解析

以下是基于 Mermaid 语法生成的 EtherCAT 系统启动流程图解析:

复制代码
graph TB
    A[系统启动] --> B[systemd 初始化]
    B --> C[读取 ethercat.service]
    C --> D[systemctl start ethercat]
    D --> E[读取/etc/ethercat.conf]
    E --> F[解析配置变量]
    F --> G[modprobe ec_master]
    G --> H[注册/dev/EtherCAT*]
    H --> I[modprobe ec_dwmac-rk]
    I --> J[设备就绪 Phase:Idle]

流程步骤说明

systemd 初始化阶段

Linux 系统启动后由 systemd 接管初始化过程,读取服务单元文件(如 ethercat.service)配置。

服务启动阶段

通过 systemctl 命令触发 ethercat 服务启动,服务管理器会按照单元文件定义的顺序加载依赖项和执行启动命令。

配置文件处理

系统读取 /etc/ethercat.conf 主配置文件,解析其中定义的网络接口、主站参数等关键变量。

内核模块加载

动态加载 ec_master 主站模块,该模块负责创建字符设备文件(如 /dev/EtherCAT0)并提供用户空间接口。

从站驱动加载

加载特定网卡驱动模块(如示例中的 ec_dwmac-rk),该步骤可能因硬件差异而不同,需匹配实际使用的以太网控制器。

状态转换

主站完成初始化后进入空闲(Idle)状态,等待后续的拓扑扫描和状态机控制指令。此时可通过 ethercat 命令行工具查询状态。

关键文件说明

  • ethercat.service:定义服务启动顺序、依赖关系和 ExecStart 命令
  • ethercat.conf:包含 MASTER0_DEVICE、DEVICE_MODULES 等硬件相关配置
  • ec_master.ko:主站核心模块,实现 EtherCAT 协议栈和主站功能

常见调试方法

检查服务状态:
systemctl status ethercat

查看内核模块加载:
lsmod | grep ec_

验证设备节点:
ls -l /dev/EtherCAT*

查看启动日志:
journalctl -u ethercat -b

启动命令:

bash 复制代码
systemctl start ethercat      # 启动
systemctl stop ethercat       # 停止
systemctl restart ethercat   # 重启
systemctl status ethercat    # 状态
systemctl enable ethercat    # 开机自启

1.3.2 方式二:SysV init 启动

以下是针对 EtherCAT 启动流程的流程图代码(Graph TB 语法),可直接用于 Mermaid 工具渲染:

graph TB A[系统启动] --> B[进入运行级别 3] B --> C[执行/etc/rc3.d/] C --> D[S99ethercat 链接] D --> E[执行 init.d/ethercat] E --> F[读取/etc/sysconfig/ethercat] F --> G[解析配置变量] G --> H[modprobe ec_master] H --> I[注册设备] I --> J[modprobe ec_dwmac-rk] J --> K[设备就绪]

流程节点说明

  • 系统启动:Linux 内核完成初始化后进入用户空间
  • 运行级别 3:多用户命令行模式(无图形界面)
  • /etc/rc3.d/:该运行级别对应的服务脚本目录
  • S99ethercat:EtherCAT 服务的软链接(数字99表示启动顺序)
  • init.d/ethercat:实际的 EtherCAT 初始化脚本
  • /etc/sysconfig/ethercat:主配置文件(路径可能因发行版而异)
  • ec_master:EtherCAT 主站内核模块
  • ec_dwmac-rk:特定网卡驱动模块(示例为 Rockchip DW MAC 驱动)

关键配置建议

  1. 确保 /etc/sysconfig/ethercat 包含正确的 MAC 地址配置:

    复制代码
    MASTER0_DEVICE="00:0a:35:00:01:02"
  2. 模块加载顺序可通过 ETHERCAT_MODULE_ORDER 变量调整:

    复制代码
    ETHERCAT_MODULE_ORDER="ec_master ec_dwmac-rk"

调试方法

  • 检查模块加载状态:

    bash 复制代码
    lsmod | grep ec_
  • 查看系统日志:

    bash 复制代码
    journalctl -u ethercat

启动命令:

bash 复制代码
/etc/init.d/ethercat start      # 启动
/etc/init.d/ethercat stop       # 停止
/etc/init.d/ethercat restart    # 重启
/etc/init.d/ethercat status     # 状态
chkconfig --add ethercat        # 开机自启

1.3.3 启动方式判别机制

系统如何知道使用哪种启动方式?

系统不会自动判别 ,而是由用户选择的安装和配置方式决定:

判别流程

步骤 用户操作 系统行为 结果
1. 安装 用户选择安装 systemd 服务或 SysV 脚本 安装对应的服务文件 决定使用哪种启动方式
2. 配置 用户创建对应的配置文件 读取相应配置文件 systemd 读 /etc/ethercat.conf SysV 读 /etc/sysconfig/ethercat
3. 启动 用户执行对应的启动命令 执行相应的启动脚本 加载内核模块
4. 开机自启 用户设置对应的自启服务 在对应运行级别创建链接 systemd 或 SysV 管理

用户配置选择

选择 systemd 启动(推荐现代系统)

适用系统

  • Ubuntu 16.04+

  • Debian 8+

  • CentOS 7+

  • openSUSE 12+

配置步骤

bash 复制代码
# 1. 安装 systemd 服务文件
sudo cp /usr/local/lib/systemd/system/ethercat.service /etc/systemd/system/

# 2. 创建配置文件 /etc/ethercat.conf
sudo nano /etc/ethercat.conf

# 3. 配置内容
MASTER0_DEVICE="c4:83:4f:27:30:5b"
DEVICE_MODULES="dwmac-rk"
LINK_DEVICES="eth0"

# 4. 重新加载 systemd 配置
sudo systemctl daemon-reload

# 5. 设置开机自启
sudo systemctl enable ethercat

# 6. 启动服务
sudo systemctl start ethercat

# 7. 查看状态
systemctl status ethercat

判别标志

  • ✅ 存在文件:/etc/systemd/system/ethercat.service

  • ✅ 存在文件:/etc/ethercat.conf

  • ✅ 使用命令:systemctl 管理

选择 SysV init 启动(旧系统或特殊需求)

适用系统

  • Ubuntu 14.04 及更早版本

  • Debian 7 及更早版本

  • CentOS 6 及更早版本

  • 其他不支持 systemd 的系统

配置步骤

bash 复制代码
# 1. 安装 init.d 脚本
sudo cp /usr/local/etc/init.d/ethercat /etc/init.d/
sudo chmod +x /etc/init.d/ethercat

# 2. 创建配置文件 /etc/sysconfig/ethercat
sudo nano /etc/sysconfig/ethercat

# 3. 配置内容
MAIN_DEVICES="c4:83:4f:27:30:5b"
DEVICE_MODULES="dwmac-rk"
LINK_DEVICES="eth0"

# 4. 设置开机自启(根据系统选择)
# CentOS/RedHat:
sudo chkconfig --add ethercat
sudo chkconfig ethercat on

# Debian/Ubuntu:
sudo update-rc.d ethercat defaults

# 5. 启动服务
sudo /etc/init.d/ethercat start

# 6. 查看状态
sudo /etc/init.d/ethercat status

判别标志

  • ✅ 存在文件:/etc/init.d/ethercat

  • ✅ 存在文件:/etc/sysconfig/ethercat

  • ✅ 使用命令:/etc/init.d/ethercat 管理

1.4 内核模块加载原理

1.4.1 模块参数传递机制

源码位置 : master/module.c

参数定义:

bash 复制代码
// 最大主站数量
#define MAX_MASTERS 32

// 参数变量定义
static char *main_devices[MAX_MASTERS];      // 主设备 MAC 地址数组
static unsigned int master_count;             // 主站数量
static char *backup_devices[MAX_MASTERS];     // 备份设备 MAC 地址数组
static unsigned int backup_count;             // 备份设备数量

#ifdef EC_EOE
char *eoe_interfaces[MAX_EOE];                // EOE 接口数组
unsigned int eoe_count;                       // EOE 接口数量
bool eoe_autocreate = 1;                      // EOE 自动创建
#endif

static unsigned int debug_level;              // 调试级别
unsigned long pcap_size;                      // Pcap 缓冲区大小

参数传递流程:

bash 复制代码
用户空间执行:modprobe ec_master main_devices=00:11:22:33:44:55
           ↓
内核模块加载:ec_init_module()
           ↓
解析参数:ec_mac_parse() 将 MAC 地址字符串转换为 uint8_t 数组
           ↓
存储到全局变量:macs[MAX_MASTERS][2][ETH_ALEN]
           ↓
初始化主站:ec_master_init(&masters[i], ...)

MAC 地址解析函数 (ec_mac_parse):

bash 复制代码
static int ec_mac_parse(uint8_t *mac, const char *mac_str, int is_backup)
{
    // 解析格式:XX:XX:XX:XX:XX:XX
    // 转换为 6 字节数组
    // 验证格式正确性
    // 存储到 macs[i][is_backup ? 1 : 0]
}

1.4.2 设备节点创建过程

源码位置 : master/module.c, master/cdev.c, master/master.c

设备节点创建流程:

第一步:分配设备号 (ec_init_module)

bash 复制代码
/ 根据 master_count 分配字符设备号范围
if (master_count) {
    if (alloc_chrdev_region(&device_number, 0, master_count, "EtherCAT")) {
        EC_ERR("Failed to obtain device number(s)!\n");
        ret = -EBUSY;
        goto out_return;
    }
}

第二步:创建设备类 (ec_init_module)

bash 复制代码
/ 创建 /sys/class/EtherCAT 设备类
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 4, 0)
class = class_create(THIS_MODULE, "EtherCAT");
#else
class = class_create("EtherCAT");
#endif

第三步:初始化主站 (ec_master_init 循环调用)

bash 复制代码
for (i = 0; i < master_count; i++) {
    ret = ec_master_init(&masters[i], i, macs[i][0], macs[i][1],
                device_number, class, debug_level);
    if (ret)
        goto out_free_masters;
}

第四步:创建字符设备和设备节点 (ec_master_init)

bash 复制代码
// 初始化字符设备
ret = ec_cdev_init(&master->cdev, master, 
                   MKDEV(MAJOR(dev_num), master->index), 1);

// 创建 /dev/EtherCAT0, /dev/EtherCAT1, ...
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 26)
master->class_device = class_device_create(class, NULL,
    MKDEV(MAJOR(dev_num), master->index), NULL, "EtherCAT%u", index);
#else
master->class_device = device_create(class, NULL,
    MKDEV(MAJOR(dev_num), master->index), NULL, "EtherCAT%u", index);
#endif

字符设备操作 (master/cdev.c):

bash 复制代码
// 字符设备文件操作结构
static const struct file_operations eccdev_fops = {
    .owner = THIS_MODULE,
    .open = eccdev_open,
    .release = eccdev_release,
    .read = eccdev_read,
    .write = eccdev_write,
    .unlocked_ioctl = eccdev_ioctl,
    .mmap = eccdev_mmap,
    .fasync = eccdev_fasync,
};

// 字符设备初始化
int ec_cdev_init(ec_cdev_t *cdev, ec_master_t *master, dev_t dev_num)
{
    cdev->master = master;
    cdev_init(&cdev->cdev, &eccdev_fops);
    cdev->cdev.owner = THIS_MODULE;
    
    // 添加字符设备到内核
    ret = cdev_add(&cdev->cdev, dev_num, 1);
    
    return ret;
}

完整创建流程:

flowchart TD A[ec_init_module] --> B[alloc_chrdev_region\n获取主设备号] B --> C[class_create\n创建 /sys/class/EtherCAT] C --> D[ec_master_init 循环\nmaster_count 次] D --> E[ec_cdev_init\ncdev_add 添加字符设备] D --> F[device_create\n创建/dev/EtherCAT0/1...] E --> G[最终结果] F --> G G --> H[设备号:主设备号 + 索引] G --> I[设备节点:/dev/EtherCAT0/1/2...] G --> J[设备类:/sys/class/EtherCAT/...]

设备号计算:

bash 复制代码
// 主设备号从 alloc_chrdev_region 获取
// 次设备号 = master index (0, 1, 2...)
dev_t dev_num = MKDEV(MAJOR(device_number), master->index);

// 示例:
// master 0 → /dev/EtherCAT0 (主设备号:250, 次设备号:0)
// master 1 → /dev/EtherCAT1 (主设备号:250, 次设备号:1)
// master 2 → /dev/EtherCAT2 (主设备号:250, 次设备号:2)

1.4.3 网卡驱动替换原理

这是 EtherCAT 能够实现实时通信的关键机制。

常规驱动 vs EtherCAT 驱动 :

驱动替换流程:

卸载原驱动并加载 EtherCAT 驱动

卸载原有的网络驱动模块(如 igbdwmac-rk),替换为 EtherCAT 专用驱动模块(如 ec_igbec_dwmac-rk)。通过 rmmod 命令移除原驱动,再使用 modprobe 加载 EtherCAT 驱动模块。

初始化硬件配置

加载驱动后,需初始化硬件相关参数,包括 DMA 控制器、中断处理机制和寄存器配置。确保硬件处于兼容 EtherCAT 协议的状态,例如调整时钟同步、PHY 模式等底层设置。

配置 DMA 与缓冲区

为高效数据传输分配 DMA 缓冲区,并配置描述符链表。描述符需明确指向物理内存地址,以支持主站与从站间的实时数据交换。通常需要设置环形缓冲区和双缓冲机制以降低延迟。

注册设备到 EtherCAT 主站

调用 ecdev_offer 函数将驱动注册到 EtherCAT 主站。此步骤会建立主站与从站的通信链路,并传递设备能力信息(如支持的同步模式、缓冲区大小等)。

主站状态切换

主站检测到从站后,状态从 IDLE 过渡到 OPERATION。此时开始周期性的数据交换,驱动需正确处理同步信号(如 DC 同步)和过程数据帧的收发。状态切换可能触发中断,需在驱动中实现相应的状态机处理逻辑。

代码示例(关键片段):

c 复制代码
// 注册 EtherCAT 设备
ec_device_t *ecdev = ecdev_offer(netdev, ec_poll, THIS_MODULE);
if (!ecdev) {
    printk("Failed to offer device\n");
    return -ENODEV;
}

// 状态变化回调
static void ec_state_change(void *priv, ec_device_state_t state) {
    if (state == EC_DEVICE_OPERATION) {
        printk("Enter OPERATION state\n");
    }
}

源码分析 (devices/generic.c):

bash 复制代码
// Generic 驱动设备注册
int __init ec_gen_init_module(void)
{
    struct list_head descs;
    struct net_device *netdev;
    ec_gen_interface_desc_t *desc, *next;

    INIT_LIST_HEAD(&generic_devices);
    INIT_LIST_HEAD(&descs);

    // 遍历所有网络设备
    rcu_read_lock();
    for_each_netdev_rcu(&init_net, netdev) {
        if (netdev->type != ARPHRD_ETHER)
            continue;
        
        // 创建接口描述
        desc = kmalloc(sizeof(ec_gen_interface_desc_t), GFP_ATOMIC);
        strncpy(desc->name, netdev->name, IFNAMSIZ);
        desc->netdev = netdev;
        desc->ifindex = netdev->ifindex;
        memcpy(desc->dev_addr, netdev->dev_addr, ETH_ALEN);
        list_add_tail(&desc->list, &descs);
    }
    rcu_read_unlock();

    // 向主站注册设备
    list_for_each_entry_safe(desc, next, &descs) {
        ret = offer_device(desc);  // 关键:注册到 EtherCAT 主站
        if (ret)
            goto out_err;
        kfree(desc);
    }
    return ret;
}

设备注册到主站 (master/module.c):

bash 复制代码
// 用户空间请求主站
ec_master_t *ecrt_request_master_err(unsigned int master_index)
{
    ec_master_t *master = &masters[master_index];

    // 检查主站是否空闲
    if (master->phase != EC_IDLE) {
        EC_MASTER_ERR(master, "Master still waiting for devices!\n");
        return ERR_PTR(-ENODEV);
    }

    // 获取所有设备的模块引用
    for (dev_idx = 0; dev_idx < ec_master_num_devices(master); dev_idx++) {
        ec_device_t *device = &master->devices[dev_idx];
        if (!try_module_get(device->module)) {
            EC_MASTER_ERR(master, "Device module is unloading!\n");
            return ERR_PTR(-ENODEV);
        }
    }

    // 进入操作阶段
    if (ec_master_enter_operation_phase(master)) {
        EC_MASTER_ERR(master, "Failed to enter OPERATION phase!\n");
        return ERR_PTR(-EIO);
    }

    return master;  // 成功返回
}

关键要点:

  1. 驱动替换必要性:

    • 原网卡驱动使用标准 Linux 网络栈,延迟不可控

    • EtherCAT 驱动直接操作硬件,实现确定性延迟

  2. 设备注册机制:

    • 驱动通过 ecdev_offer() 向主站注册

    • 主站收集所有配置的设备

    • 用户空间调用 ecrt_request_master() 完成绑定

  3. DMA 零拷贝:

    • 用户空间内存通过 mmap 映射到内核

    • DMA 直接在用户空间缓冲区读写

    • 避免内核态到用户空间的数据拷贝

  4. 实时性保证:

    • 旁路网络协议栈

    • 优先级继承锁 (rtmutex)

    • 硬件时间戳支持

1.4.4 完整启动时序图

1.4.4.1 系统启动流程

1.4.4.2 关键函数调用栈


1.4.4.3 主站状态机
1.4.4.4 设备注册时序

未完待续

相关推荐
xian_wwq2 小时前
【学习笔记】GB/T 20986-2023 详解,10 类网络安全事件分类
笔记·学习·web安全
鱼鳞_2 小时前
Java学习笔记_Day27(Stream流)
java·笔记·学习
_李小白2 小时前
【OSG学习笔记】Day 42: OSG 动态场景安全修改
笔记·学习·安全
H_老邪2 小时前
Docker 学习之路-从入门到放弃:7
学习·docker·容器
头疼的程序员2 小时前
计算机网络:自顶向下方法(第七版)第八章 学习分享(四)
学习·计算机网络
m0_677904842 小时前
K8s学习
java·学习·kubernetes
|_⊙2 小时前
红黑树 (C++)
开发语言·c++·学习
ByteCraze3 小时前
手写高性能虚拟列表(详解!!!)
javascript·学习
扣脑壳的FPGAer3 小时前
数字信号处理学习笔记--Chapter 1.3 常系数线性差分方程
笔记·学习·信号处理