# bot4cj 项目深度分析文档 > 本文档对 bot4cj 项目进行全面、详细的分析,涵盖项目架构、每个模块的实现细节、算法原理、数据流、缺陷与改进建议。目标读者为零基础学习者。 --- ## 目录 1. [项目总览](#1-项目总览) 2. [项目架构与目录结构](#2-项目架构与目录结构) 3. [核心数据流:从脚本到硬件](#3-核心数据流从脚本到硬件) 4. [矩阵式脚本语言(.mt)详解](#4-矩阵式脚本语言mt详解) 5. [解析器模块详解](#5-解析器模块详解) 6. [机器人结构模块详解](#6-机器人结构模块详解) 7. [硬件驱动层详解](#7-硬件驱动层详解) 8. [数学库详解](#8-数学库详解) 9. [神经网络模块详解](#9-神经网络模块详解) 10. [卡尔曼滤波详解](#10-卡尔曼滤波详解) 11. [FFT 快速傅里叶变换详解](#11-fft-快速傅里叶变换详解) 12. [CNN 卷积神经网络详解](#12-cnn-卷积神经网络详解) 13. [CUDA GPU 加速详解](#13-cuda-gpu-加速详解) 14. [线程与同步模块详解](#14-线程与同步模块详解) 15. [HTTP 服务器模块详解](#15-http-服务器模块详解) 16. [辅助工具详解](#16-辅助工具详解) 17. [构建系统详解](#17-构建系统详解) 18. [全局缺陷与改进建议](#18-全局缺陷与改进建议) 19. [学习路线图](#19-学习路线图) --- ## 1. 项目总览 ### 1.1 项目是什么 **bot4cj** 是一个**机器人硬件脚本解析器**,基于仓颉(Cangjie)编程语言和 Linux I2C 接口。它的核心思想是: > 用一种自定义的"矩阵式脚本语言"来**描述机器人的硬件构成和动作序列**,然后解析并驱动真实硬件执行这些动作。 简单类比:如果把机器人比作一个演员,那么 `.mt` 脚本就是剧本,`bot4cj` 就是导演——它读剧本、安排演员(硬件)、指挥表演(执行动作)。 ### 1.2 项目能做什么 | 功能 | 说明 | | ---------------------------- | ------------------------------------------- | | 解析矩阵式脚本 | 读取`.mt` 文件,解析出硬件定义和动作序列 | | 驱动 PCA9685 舵机控制板 | 通过 I2C 控制最多 16 路舵机 | | 读取 MPU6050 陀螺仪/加速度计 | 实时获取姿态数据,带卡尔曼滤波 | | 控制扬声器发声 | 以声音频率替代延时(或同时发声和延时) | | HTTP 服务器模式 | 通过网络接口远程提交脚本并执行 | | 神经网络推理与训练 | 全连接前馈网络,支持反向传播训练 | | 深度强化学习微调 | 用 DRL 对机器人动作进行微调(旧版,已注释) | | CNN 卷积神经网络 | 用于视觉识别(C++ 实现,通过 FFI 调用) | | FFT 频谱分析 | 对信号进行快速傅里叶变换 | | CUDA GPU 加速 | 矩阵运算和激活函数可在 GPU 上执行 | ### 1.3 技术栈 | 层级 | 技术 | 用途 | | ------ | --------------- | ---------------------------------- | | 应用层 | 仓颉 (Cangjie) | 主程序、解析器、机器人结构、数学库 | | 系统层 | C/C++ | I2C 驱动、矩阵运算、FFT、CNN、音频 | | 加速层 | CUDA | GPU 矩阵运算、GPU 激活函数 | | 脚本层 | .mt 矩阵式脚本 | 硬件定义和动作描述 | | 前端层 | HTML + Tailwind | Web 管理界面 | | 工具层 | Python | 图片标注工具、API 测试 | --- ## 2. 项目架构与目录结构 ``` bot4cj/ ├── src/ # 源代码根目录 │ ├── main.cj # 程序入口:命令行解析、分支选择 │ ├── P.cj # 解析器:MatrixObject、MatrixParser、MatrixSaver │ ├── r.cj # 机器人结构:RobotStruct、硬件变量类 │ ├── math/ # 数学库 │ │ ├── Matrix.cj # FP32 结构体 + Matrix1 矩阵类(仓颉原生) │ │ ├── Tensor.cj # 张量类(多维数组) │ │ ├── nn.cj # 神经网络层 Nn1 + 训练/推理函数 │ │ ├── nns.cj # 多层神经网络训练/推理(旧版,已注释) │ │ ├── Af.cj # 激活函数枚举:Sigmoid/ReLU/Leaky_ReLU/Tanh/Softmax │ │ ├── Ini.cj # 权重初始化:Xavier 正态/均匀 │ │ ├── kalman.cj # 一维卡尔曼滤波器(仓颉实现) │ │ └── fft.cj # FFT 接口(已注释,改用 C 实现) │ ├── hd/ # 硬件驱动 │ │ ├── I2C.cj # I2C 通信:直接调用 glibc + 互斥锁系统 │ │ ├── PCA9685.cj # PCA9685 舵机驱动板控制 │ │ ├── MPU6050.cj # MPU6050 陀螺仪/加速度计 + 守护线程 │ │ ├── Servo.cj # 舵机类:角度限制 + 偏移 + 回调 │ │ └── snd.cj # 扬声器类(目前仅延时占位) │ ├── th/ # 线程与同步 │ │ ├── mutex.cj # pthread 互斥锁封装 │ │ └── pthread.cj # pthread 线程封装 │ ├── cpp/ # C/C++ 共享库源码 │ │ ├── I2C.c # 旧版 I2C 驱动(使用 i2c-smbus) │ │ ├── Matrix.cpp + .hh # C++ 模板矩阵类 + C 接口封装 │ │ ├── kalman.c + .h # 旧版卡尔曼滤波器(C 实现) │ │ ├── fft1.c # 基-2 FFT 算法(纯 C) │ │ ├── fft.cpp + .h # FFT 封装层(C++ → C 接口) │ │ ├── snd.cpp # 音频播放(Windows WaveAPI / Linux ALSA) │ │ ├── bot4cj.h # C 头文件:I2C 常量和函数声明 │ │ └── cnn/ # CNN 卷积神经网络 │ │ ├── cnn_i.cpp # CNN C 接口封装层 │ │ ├── conv.cpp + .h # 卷积层 │ │ ├── pooling.cpp + .h # 池化层(最大/平均) │ │ ├── ga_pooling.cpp + .h # 全局平均池化 │ │ ├── flatten.cpp + .h # 展平层 │ │ └── imgl.cpp + .h # 图像加载器 │ ├── cuda/ # CUDA GPU 加速 │ │ ├── matrix.cu # GPU 矩阵运算 + 激活函数 │ │ └── error.cuh # CUDA 错误检查宏 │ └── py/ │ └── labeled.py # 图片标注工具(Python + Tkinter) ├── scripts/ # 动作脚本 │ ├── test.mt # 新版矩阵式脚本(蜘蛛机器人) │ ├── std.bot # 旧版脚本(站立动作) │ ├── test.bot # 旧版脚本(行走动作) │ └── ... # 其他旧版脚本 ├── public/ │ └── index.html # Web 前端页面 ├── test/ │ └── test_api_fs.py # 文件系统 API 测试 ├── Makefile # C/C++ 共享库构建 ├── cjpm.toml # 仓颉包管理配置 ├── _run.sh / _set.sh # 运行和构建脚本 └── cangjie.md # 仓颉语言参考文档 ``` --- ## 3. 核心数据流:从脚本到硬件 这是理解整个项目最关键的部分。以下是数据从脚本文件到硬件执行的完整流程: ``` ┌─────────────┐ │ .mt 脚本文件 │ 例如:scripts/test.mt └──────┬──────┘ │ 文件读取 (InputStream) ▼ ┌─────────────┐ │ MatrixParser│ 逐字符解析,输出 MatrixObject 对象 │ (src/P.cj) │ 每个 @name...@ 块变成一个 MatrixObject └──────┬──────┘ │ LinkedList ▼ ┌─────────────┐ │ RobotStruct │ 从 MatrixObject 列表构建硬件变量字典 │ (src/r.cj) │ HashMap └──────┬──────┘ │ ├── createAliasVars() 从 @Alias 矩阵创建别名变量 ├── createI2CVars() 从 @I2C 矩阵创建 I2C 设备变量 ├── createMPU6050Bind() 从 @MPU6050_BIND 矩阵创建陀螺仪变量 ├── createPCA9685Vars() 从 @PCA9685 矩阵创建舵机控制板变量 ├── createServoVars() 从 @Servo 矩阵创建舵机变量 └── createSpeakerVars() 从 @Speaker 矩阵创建扬声器变量 │ ▼ ┌─────────────┐ │ active() │ 调用所有硬件变量的 build() 方法(初始化硬件) └──────┬──────┘ │ ▼ ┌─────────────┐ │ run() │ 逐行执行 @Action 矩阵中的动作 │ run_drl_ │ 每行解析出各硬件的参数,调用 play() 执行 │ action() │ └──────┬──────┘ │ ▼ ┌─────────────┐ │ deactive() │ 以倒序调用所有硬件变量的 destroy() 方法(释放资源) └─────────────┘ ``` ### 关键概念:MatrixVars_Hardware(硬件变量) 这是整个项目的核心抽象。每种硬件(I2C、PCA9685、Servo、Speaker...)都被封装为一个**硬件变量**,它们: 1. **通过闭包持有真实的硬件对象**(如 `I2C_Orig`、`PCA9685`、`Servo`) 2. **通过 JsonValue 进行统一接口通信**(输入 JsonValue,输出 JsonValue) 3. **有生命周期**:`build()` 初始化,`play()` 执行,`destroy()` 释放 这种设计使得 `@Action` 矩阵中的每一列(如 `s1.A`)都能通过统一的 `play()` 接口驱动对应的硬件。 --- ## 4. 矩阵式脚本语言(.mt)详解 ### 4.1 语法规则 矩阵式脚本是一种**声明式、表格型**的领域特定语言(DSL),用于描述机器人的硬件构成和动作序列。 **基本结构:** ``` @MatrixName ← 矩阵开始,@ 后跟名称 col1 col2 col3; ← 第 0 行:列名声明 val1 val2 val3; ← 第 1 行起:数据行 val4 - val6; ← "-" 表示沿用上一行同列的值 @ ``` **注释:** 以 `#` 开头到行末 **占位符 `-`:** 在数据行中,`-` 表示"与上一行同一列的值相同"。这是为了减少重复书写。 ### 4.2 各矩阵对象的含义 | 矩阵名 | 用途 | 关键列 | | --------------- | ----------------- | ------------------------------------------------- | | `@Alias` | 定义参数别名 | name, alias | | `@I2C` | 定义 I2C 总线设备 | name, path | | `@PCA9685` | 定义舵机控制板 | name, I2C, addr | | `@MPU6050_BIND` | 定义陀螺仪 | name, I2C, addr | | `@Speaker` | 定义扬声器 | name, channels, sample_rate, volumn, max_f, min_f | | `@Servo` | 定义舵机 | name, max, min, offset, controller, channel | | `@Action` | 定义动作序列 | 格式为`硬件名.参数名`,如 `s1.A` | ### 4.3 @Action 矩阵详解 `@Action` 是最核心的矩阵,它定义了机器人的动作序列: ``` @Action s1.A s2.A s3.A ... spk.F spk.delay; ← 列名:硬件变量.参数名 45 - 45 ... 10 200; ← 第1行:站立姿态 90 - - ... - 20; ← 第2行:左后方抬起 ... @ ``` - **列名格式**:`硬件变量名.参数名`,如 `s1.A` 表示舵机 s1 的角度(Angle) - **参数别名**:`@Alias` 中定义了 `A → angle`、`F → frequency`,所以 `s1.A` 实际传给 s1 的是 `angle` 参数 - **执行方式**:逐行执行,每行同时设置所有硬件的参数值 ### 4.4 蜘蛛机器人动作示例解读 以 `scripts/test.mt` 为例,这是一个 8 足蜘蛛机器人的行走步态: ``` @Action s1.A s2.A s3.A s4.A s5.A s6.A s7.A s8.A s9.A s10.A spk.F spk.delay; 45 - 45 - 45 - 45 - 90 90 10 200; # 站立 90 - - - - - - - - - - 20; # 左后方抬起 45 - - - - - - - - - 0 500; # 恢复站立 - - 90 - - - 45 - - - - 20; # 右后方抬起 ... @ ``` **解读:** - s1-s8 是 8 个舵机(4 条腿,每腿 2 个关节) - s9-s10 是云台舵机 - spk.F 是扬声器频率,spk.delay 是延时 - 第 1 行:所有舵机设为 45°/90°,这是**站立姿态** - 第 2 行:s1 设为 90°,其余不变,这是**左后方腿抬起** - 频率 0 + 延时 500ms = 静默等待 500ms - 频率 10 + 延时 200ms = 发出 10Hz 声音 200ms(同时作为延时) --- ## 5. 解析器模块详解 ### 5.1 MatrixObject 类(src/P.cj:11-97) **用途:** 表示解析后的一个矩阵对象,对应脚本中 `@name ... @` 之间的内容。 **数据结构:** ``` MatrixObject { name: String ← 矩阵名称(如 "I2C"、"Servo") values: ArrayList> ← 二维字符串矩阵 } ``` **关键方法:** | 方法 | 功能 | 零基础解释 | | ---------------------- | ---------------- | ---------------------------------------------------- | | `hasName()` | 是否已命名 | 检查这个矩阵有没有名字(@ 后面的字符串) | | `[](row, col)` | 取元素 | 像数组一样用下标访问,如`obj[0, 1]` 取第 0 行第 1 列 | | `indexOfCol(row, key)` | 在某行中查找列号 | 在第 row 行中找值为 key 的列,返回列号 | | `addRune(c)` | 添加字符到缓冲区 | 一个字符一个字符地积累,最终组成一个完整的元素 | | `submitName()` | 提交名字 | 把缓冲区里的字符拼成名字字符串 | | `submitItem()` | 提交元素 | 把缓冲区里的字符拼成一个矩阵元素 | | `submitLine(count)` | 提交一行 | 检查当前行的列数是否与之前一致,不一致则报错 | | `save(output)` | 储存对象 | 将矩阵对象写回文件流 | **设计要点:** 矩阵的元素是 `String` 类型,不是数值。这是因为矩阵既包含数值(如 `0x40`),也包含字符串引用(如 `pca9685-0`)。数值解析延迟到使用时进行。 ### 5.2 MatrixParser 类(src/P.cj:112-216) **用途:** 将输入流(文件或标准输入)逐字符解析为 `MatrixObject` 对象。 **算法:有限状态机(FSM)** 解析器是一个典型的**有限状态机**,通过一组布尔标志位来跟踪当前状态: | 标志 | 含义 | | ------ | ---------------------------------- | | `flag` | 是否遇到结束的`@` | | `nl` | 是否处于新行起始位置 | | `cm` | 是否处于注释中 | | `nm` | 是否正在解析名称行(@ 后的第一行) | **状态转换逻辑(`_next` 方法):** ``` 输入字符 c ├── 如果在注释中 (cm=true) │ ├── c == '\n' → 注释结束,nl=true, cm=false │ └── 其他 → 忽略 ├── 如果在新行起始 (nl=true) │ ├── c 是空白 → 维持现状 │ ├── c == '\n' → 空行,行号+1 │ ├── c == '@' 且未命名 → 开始名称行,nm=true │ ├── c == '@' 且已命名 → 结束标志,flag=true │ └── 其他 → 内容字符,加入缓冲区 ├── 如果不在新行 (nl=false) │ ├── 在名称行且遇空白 → 提交名字 │ ├── 遇空白/制表符 → 提交当前元素 │ ├── c == ';' → 提交元素并提交行 │ └── 其他 → 加入缓冲区 ``` **循环不变式(在代码注释中声明):** - **初始化:** `input` 是一个动作脚本的输入流 - **保持:** 每次循环后,`input` 剩余部分仍是未解析的脚本 - **终止:** 当输入流为空或遇到结束 `@` 时,循环终止 ### 5.3 MatrixSaver 类(src/P.cj:221-231) **用途:** 将 `LinkedList` 序列化回文本格式。 **格式:** ``` @MatrixName elem elem elem ; elem elem elem ; @ @NextMatrix ... @ ``` ### 5.4 异常体系(src/P.cj:239-260) | 异常类 | 含义 | | ---------------- | ------------------------------ | | `ParseExcept` | 解析异常基类 | | `BufOverTheLine` | 缓冲区过长(单元素字符数超限) | | `DoesNotSync` | 矩阵行没对齐(列数不一致) | | `UnknowExcept` | 未知异常兜底 | --- ## 6. 机器人结构模块详解 ### 6.1 MatrixVars_Hardware 基类(src/r.cj:22-89) **这是整个硬件抽象的核心。** 所有硬件变量都继承自这个类。 **设计思想:** ``` MatrixVars_Hardware(基类) ├── things: (JsonValue) -> JsonValue ← 事件回调(核心!) ├── rise: Option<() -> Unit> ← 初始化回调 ├── free: Option<() -> Unit> ← 释放回调 ├── play(input) → things(input) ← 执行动作 ├── build() → rise() ← 初始化硬件 └── destroy() → free() ← 释放硬件 ``` **为什么用 JsonValue 通信?** 因为不同硬件需要不同的参数: - PCA9685 需要 `{"channel": 0, "angle": 90.0}` - I2C 需要 `{"mode": "set_addr", "addr": 0x40}` - Servo 需要 `{"angle": 180.0}` - Speaker 需要 `{"frequency": 1000.0, "delay": 400}` 用 JsonValue 作为统一的数据交换格式,避免为每种硬件写不同的接口。 **静态工具方法:** | 方法 | 功能 | | --------------------------- | -------------------------------- | | `getMt(obj, row, col)` | 获取矩阵元素,`-` 自动沿用上一行 | | `parseMt(obj, row, col)` | 获取并解析为数值类型 T | `parseMt` 的 `-` 处理逻辑: - 如果当前值是 `-` 且行号 > 1:递归取上一行同列的值 - 如果当前值是 `-` 且行号 == 1:返回 0(首行的 `-` 默认为 0) ### 6.2 各硬件变量类 #### MtVars_Alias(别名变量,src/r.cj:92-115) **用途:** 定义参数别名,如 `A → angle`、`F → frequency`。 **工作方式:** `play()` 时直接返回别名对应的字符串。 #### MtVars_I2C(I2C 变量,src/r.cj:118-185) **用途:** 封装 I2C 设备的打开、关闭、读写操作。 **工厂函数 `Factory()` 流程:** 1. 从 `@I2C` 矩阵中读取 name 和 path 2. 创建 `I2C_Orig(path)` 对象(打开 I2C 设备文件) 3. 通过闭包将 `I2C_Orig` 保存在 `things` 回调中 4. `things` 支持 4 种模式: - `get_fd`:获取文件描述符 - `set_addr`:设置从机地址 - `set_val`:写寄存器 - `get_val`:读寄存器 5. `free` 回调中关闭 I2C 设备 #### MtVars_PCA9685(PCA9685 变量,src/r.cj:244-314) **用途:** 封装 PCA9685 舵机控制板。 **工厂函数流程:** 1. 从 `@PCA9685` 矩阵读取 name、I2C 引用名、地址 2. 从硬件变量字典中找到对应的 I2C 变量 3. 通过 I2C 变量的 `play({"mode": "get_fd"})` 获取文件描述符 4. 创建 `PCA9685(fd, addr)` 对象 5. `things` 回调:接收 `{"channel": n, "angle": a}`,调用 `pca9685.setAngle(n, a)` 6. `rise` 回调:调用 `pca9685.setup()` 初始化 PWM 频率 #### MtVars_MPU6050_BIND(MPU6050 变量,src/r.cj:188-241) **用途:** 封装 MPU6050 陀螺仪/加速度计。 **工厂函数流程:** 1. 从 `@MPU6050_BIND` 矩阵读取参数 2. 通过 I2C 变量获取文件描述符 3. 创建 `MPU6050(fd, addr)` 对象 4. `rise` 回调:`mpu6050.setup()` + `mpu6050.startDaemon()`(启动采样线程) 5. `free` 回调:`mpu6050.stopDaemon()` + `mpu6050.waitDaemon()`(停止并等待线程) #### MtVars_Servo(舵机变量,src/r.cj:320-373) **用途:** 封装单个舵机,包含角度限制和偏移。 **工厂函数流程:** 1. 从 `@Servo` 矩阵读取 name、max、min、offset、controller、channel 2. 从字典中找到控制器(如 pca9685-0) 3. 创建 `Servo(max, min, offset, callback_setAngle)` 对象 4. `callback_setAngle` 闭包:将角度通过控制器的 `play()` 发送给 PCA9685 5. `things` 回调:接收 `{"angle": a}`,调用 `servo.setAngle(a)` **Servo.setAngle() 的逻辑(src/hd/Servo.cj:18-24):** ``` 角度 = 输入角度 + 偏移值(offset) 如果 角度 > 最大值(max) → 角度 = max 如果 角度 < 最小值(min) → 角度 = min 调用回调函数设置角度 ``` #### MtVars_Speaker(扬声器变量,src/r.cj:380-426) **用途:** 封装扬声器,支持频率限制。 **工厂函数流程:** 1. 从 `@Speaker` 矩阵读取参数 2. 创建 `Spk(channels, sample_rate)` 对象 3. `things` 回调: - 接收 `{"frequency": f, "delay": d}` - 频率限制在 `[min_f, max_f]` 范围内 - 调用 `spk.play(f, volume, d)` ### 6.3 RobotStruct 类(src/r.cj:435-626) **用途:** 机器人的完整结构,从脚本构建并执行。 **核心流程:** ``` RobotStruct(input) ├── readMatrix(input) 解析所有矩阵对象 └── createHardVars() 创建硬件变量字典 ├── createAliasVars() ├── createI2CVars() ├── createMPU6050Bind() ├── createPCA9685Vars() ├── createServoVars() └── createSpeakerVars() active() 初始化所有硬件 run() → run_drl_action() 执行动作 deactive() 倒序释放所有硬件 ``` **run_drl_action() 的执行逻辑(src/r.cj:578-596):** ```python 对于 @Action 矩阵的每一行 i (从1开始): 创建空的 JsonObject: set 对于每一列 j: 解析列名 "s1.A" → 设备名="s1", 参数名="A" 检查参数名是否是别名(如 A → angle) 获取该列的数值(处理 "-" 占位符) 将参数加入 set: set["s1"]["angle"] = 45.0 对于 set 中的每个设备: 调用 M[设备名].play(参数) ← 驱动硬件! ``` **parse_action_matrixObject() 详解(src/r.cj:599-625):** 这个方法解析 `@Action` 矩阵的一列: 1. 取列名(第 0 行),如 `s1.A` 2. 按 `.` 分割为设备名和参数名 3. 检查参数名是否在 `@Alias` 中定义了别名,如有则替换 4. 将数值加入对应设备的参数集合 --- ## 7. 硬件驱动层详解 ### 7.1 I2C 通信(src/hd/I2C.cj) **核心设计:直接调用 glibc 函数** 项目没有使用自定义的 C 共享库来操作 I2C,而是直接在仓颉中通过 `foreign func` 声明并调用 Linux glibc 的原生函数: ```cangjie foreign func open(path: CString, flag: Int32, ...): Int32 foreign func close(fd: Int32): Int32 foreign func ioctl(fd: Int32, addr: UInt64, ...): Int32 foreign func read(fd: Int32, buf: CPointer, count: UIntNative): IntNative foreign func write(fd: Int32, buf: CPointer, count: UIntNative): IntNative ``` **I2C 操作流程:** 1. `openI2cDev(path)` → 打开 `/dev/i2c-N` 设备文件 2. `setI2cAddr(fd, addr)` → 通过 `ioctl` 设置从机地址 3. `writeI2cReg(fd, reg, val)` → 写入寄存器:先写寄存器地址,再写值 4. `readI2cReg(fd, reg)` → 读取寄存器:先写寄存器地址,再读值 ### 7.2 I2C 互斥锁系统(src/hd/I2C.cj:99-243) **为什么需要锁?** I2C 总线是共享资源。当多个线程(如 MPU6050 采样线程和主线程)同时访问同一个 I2C 设备时,必须保证操作的原子性。 **锁系统架构:** ``` MAIN_LOCK (主锁) ← 保护 LOCKS 数组的操作 └── LOCKS[0..19] (锁具数组) ← 每个 I2C 设备一个锁 └── I2C_Lock { i2c_fd: Int32 ← 绑定的 I2C 设备文件句柄 lock: CPointer ← pthread_mutex_t 指针 is_active: Bool ← 是否激活 } ``` **操作流程:** | 函数 | 功能 | | --------------------- | -------------------------------------------------------------------------- | | `I2cLock_create(fd)` | 为 I2C 设备创建锁:获取主锁 → 找空位 → 初始化互斥锁 → 释放主锁 | | `I2cLock_destroy(fd)` | 销毁锁:获取主锁 → 找到对应锁 → 销毁互斥锁 → 如无活跃锁则释放整个锁系统 | | `I2cLock_lock(fd)` | 加锁:获取主锁 → 找到对应锁 → 对该锁加锁 → 释放主锁 | | `I2cLock_unlock(fd)` | 解锁:获取主锁 → 找到对应锁 → 对该锁解锁 → 释放主锁 | ### 7.3 PCA9685 舵机控制板(src/hd/PCA9685.cj) **PCA9685 是什么?** 一个通过 I2C 控制的 16 通道 PWM 信号发生器,常用于驱动舵机。 **setup() 初始化流程:** 1. 加锁 2. 设置 I2C 地址 3. 写寄存器 0x00 = 0x20(开启地址自增,退出睡眠) 4. 等待振荡器启动(5ms) 5. 计算预分频值:`prescale = 25000000 / (4096 * 50) - 1 ≈ 100`(50Hz PWM) 6. 写寄存器 0x00 = 0x10(进入睡眠模式,允许改频率) 7. 写寄存器 0xFE = prescale(设置预分频值) 8. 写寄存器 0x00 = 0x20(退出睡眠) 9. 解锁 **setAngle() 角度转 PWM:** ``` 脉宽 = 0.5 + (2.5 - 0.5) * (angle / 180.0) ← 0°→0.5ms, 180°→2.5ms OFF值 = 脉宽 * 4096 / 20 ← 20ms 周期, 12位分辨率 setPWM(channel, ON=0, OFF=计算值) ``` ### 7.4 MPU6050 陀螺仪/加速度计(src/hd/MPU6050.cj) **MPU6050 是什么?** 一个 6 轴运动跟踪设备,包含 3 轴加速度计和 3 轴陀螺仪。 **setup() 初始化:** 1. 退出睡眠模式(写 PWR_MGMT_1 = 0x00) 2. 设置陀螺仪量程 ±500°/s 3. 设置加速度计量程 ±8g 4. 等待 100ms 稳定 **readSensor() 读取数据:** - 读取 6 个 16 位寄存器(加速度 X/Y/Z + 陀螺仪 X/Y/Z) - 每个轴需要读高字节和低字节,组合为 16 位有符号整数 **守护线程 mpu6050_main():** 这是一个持续运行的线程,不断采样并滤波: 1. 读取原始传感器数据 2. 对 6 个轴分别进行卡尔曼滤波 3. 打印滤波后的数据 4. 睡眠 10ms ### 7.5 Servo 舵机类(src/hd/Servo.cj) **设计:** 通过回调函数实现与控制器的解耦。 ``` Servo { max: Float32 ← 最大角度限制 min: Float32 ← 最小角度限制 offset: Float32 ← 角度偏移(校准用) callback_setAngle ← 设置角度的回调函数 } ``` `setAngle(angle)` 的处理顺序:加偏移 → 限制范围 → 调用回调 ### 7.6 Spk 扬声器类(src/hd/snd.cj) **当前状态:** 仅实现了延时功能,实际发声被注释掉。 ```cangjie public func play(frequency, volume, durations_ms): Bool { // 实际发声被注释 sleep(Duration.millisecond * Int64(durations_ms)) // 仅延时 return true } ``` **C++ 实现的音频播放(src/cpp/snd.cpp)** 是完整的,支持: - **Windows:** 使用 WaveAPI(`waveOutOpen`/`waveOutWrite`)+ 回调通知 - **Linux:** 使用 ALSA(`snd_pcm_open`/`snd_pcm_writei`) - 生成正弦波数据,带 5ms 淡入淡出避免咔嗒声 - 支持多声道、可调采样率和音量 --- ## 8. 数学库详解 ### 8.1 FP32 结构体(src/math/Matrix.cj:29-55) **用途:** 包装 `Float32` 为值类型,支持运算符重载。 ```cangjie public struct FP32 { private var val: Float32 // 运算符重载:+、-、*、/(支持 FP32 和 Float32 两种参数) } ``` **为什么需要 FP32?** 仓颉的 `Float32` 是内置类型,不能直接重载运算符。通过包装为 `struct FP32`,可以定义 `+`、`-`、`*`、`/` 运算符,使矩阵运算代码更简洁。 ### 8.2 Matrix1 矩阵类(src/math/Matrix.cj:73-291) **用途:** 仓颉原生实现的二维浮点矩阵,是神经网络的基础。 **数据布局:** 一维数组 `Array`,行优先存储。 ``` ___col___ / \ +---------+ \ | 0 1 2 3 | | row - 第一维度 | 4 5 6 7 | | +---------+ / arr[row * col + col] ← 元素访问公式 ``` **支持的运算:** | 运算 | 符号 | 含义 | 形状要求 | | -------- | ----------- | ------------------------ | -------------- | | 矩阵加法 | `A + B` | 逐元素相加 | 同形状 | | 广播加法 | `A + s` | 每个元素加标量 | - | | 矩阵减法 | `A - B` | 逐元素相减 | 同形状 | | 广播减法 | `A - s` | 每个元素减标量 | - | | 逐元素乘 | `A * B` | 逐元素相乘(Hadamard积) | 同形状 | | 广播乘法 | `A * s` | 每个元素乘标量 | - | | 逐元素除 | `A / B` | 逐元素相除 | 同形状 | | 广播除法 | `A / s` | 每个元素除标量 | - | | 矩阵乘法 | `A.dot(B)` | 矩阵点积(MatMul) | A.col == B.row | | 转置 | `A.T()` | 行列互换 | - | | 克隆 | `A.clone()` | 深拷贝 | - | **矩阵乘法算法(dot):** 这是最核心的运算,三重循环实现: ``` 对于结果矩阵的每个元素 (i, j): sum = 0 对于 k in 0..A.col: sum += A[i, k] * B[k, j] 结果[i, j] = sum ``` 时间复杂度:O(m × n × k),其中 A 是 m×k,B 是 k×n。 ### 8.3 Tensor 张量类(src/math/Tensor.cj) **用途:** 多维数组的泛型实现,是 Matrix1 的多维推广。 ```cangjie class Tensor { data: Array ← 一维扁平存储 dims: Array ← 维度列表(形状) size: Int64 ← 总元素数 } ``` **oneByOne() 方法:** 逐元素运算,接受另一个张量和一个二元函数,要求两个张量形状相同。 **当前状态:** 基础框架,功能有限,未在项目中实际使用。 --- ## 9. 神经网络模块详解 ### 9.1 Nn1 全连接层(src/math/nn.cj:20-99) **用途:** 神经网络的一个全连接层(Dense Layer / Fully Connected Layer)。 **数据结构:** ```cangjie class Nn1 { inputno: Int64 ← 输入维度 outputno: Int64 ← 输出维度 weights: Matrix1 ← 权重矩阵 (inputno, outputno) bias: Matrix1 ← 偏置向量 (1, outputno) learning_rate: FP32 ← 学习率 f: Af ← 激活函数 // 前向传播缓存 prev_inputs: Option ← 本次输入 prev_raw_inputs: Option ← 激活前的输出(z = Wx + b) prev_outputs: Option ← 激活后的输出(a = f(z)) // 梯度缓存 weights_grad: Option ← 权重梯度 bias_grad: Option ← 偏置梯度 } ``` **forward() 前向传播(src/math/nn.cj:53-61):** ``` z = x · weights + bias ← 矩阵乘法 + 广播加法 a = f(z) ← 激活函数 缓存 x, z, a 返回 a ``` 数学公式:**a = f(Wx + b)** **backward() 反向传播(src/math/nn.cj:64-72):** ``` δ = errs * f'(z) ← 误差 × 激活函数导数(逐元素乘) weights_grad = x^T · δ ← 权重梯度 bias_grad = δ ← 偏置梯度 errs_up = δ · W^T ← 传给上一层的误差 返回 errs_up ``` 数学推导: - 设损失为 L,本层输出为 a = f(z),z = Wx + b - δ = ∂L/∂z = (∂L/∂a) · f'(z) = errs · f'(z) - ∂L/∂W = x^T · δ - ∂L/∂b = δ - ∂L/∂x = δ · W^T **update() 权重更新(src/math/nn.cj:76-79):** ``` weights = weights + weights_grad * learning_rate * discount bias = bias + bias_grad * learning_rate * discount ``` 注意:这里是**加号**,不是减号。这意味着 `weights_grad` 已经包含了负号(因为 `errs = t - res`,即目标减预测),或者学习率本身为负值。实际上,`errs = t - res` 确实是 `(target - prediction)`,而梯度下降应该是 `weights -= lr * grad`,所以这里用加号是**正确的**——因为 `grad` 已经是 `(t - res)` 方向的,加上去就是往目标方向移动。 ### 9.2 Af 激活函数枚举(src/math/Af.cj) **支持的激活函数:** | 函数 | 公式 | 导数 | 用途 | | ---------- | ---------------------------- | ------------------------- | ---------------------------- | | None | f(x) = x | f'(x) = 1 | 恒等变换 | | Sigmoid | f(x) = 1/(1+e^(-x)) | f'(x) = f(x)(1-f(x)) | 二分类输出层 | | ReLU | f(x) = max(0, x) | f'(x) = 1 if x>0, else 0 | 隐藏层(解决梯度消失) | | Leaky_ReLU | f(x) = x if x>0, else αx | f'(x) = 1 if x>0, else α | 隐藏层(解决 ReLU 死亡问题) | | Tanh | f(x) = tanh(x) | f'(x) = 1 - tanh²(x) | 隐藏层 | | Softmax | f(x_i) = e^(x_i) / Σe^(x_j) | 雅可比矩阵 | 多分类输出层 | **Softmax 实现(src/math/Af.cj:209-241):** 使用**数值稳定**的实现方式: 1. 找到最大值 `max_val` 2. 计算 `exp(x_i - max_val)` 避免指数溢出 3. 除以总和归一化 **Softmax 雅可比矩阵(src/math/Af.cj:246-271):** ``` J[i][j] = y_i * (1 - y_i) 如果 i == j(对角线) J[i][j] = -y_i * y_j 如果 i != j(非对角线) ``` ### 9.3 Ini 权重初始化(src/math/Ini.cj) **Xavier 正态初始化:** ``` σ = sqrt(2 / (fan_in + fan_out)) 权重 ~ N(0, σ²) ``` **Xavier 均匀初始化(Glorot):** ``` limit = sqrt(6 / (fan_in + fan_out)) 权重 ~ U(-limit, limit) ``` **原理:** Xavier 初始化的目的是让每一层的输出方差与输入方差相同,避免梯度消失或爆炸。推导基于 Tanh 激活函数的线性近似。 ### 9.4 训练函数 train1()(src/math/nns.cj:40-75) **完整训练流程:** ``` 1. 正向传播:逐层计算 forward 2. 计算误差:errs = target - output 3. 反向传播:从最后一层到第一层,逐层计算 backward 4. 梯度更新:逐层调用 update 5. 计算 MSE 损失:loss = 0.5 * (t - y)² ``` **与门测试 prepare_andDoor()(src/math/nns.cj:11-21):** 随机生成两个 0/1 输入,目标输出为同或(XNOR): - (0,0) → 1 - (1,1) → 1 - (0,1) → 0 - (1,0) → 0 --- ## 10. 卡尔曼滤波详解 ### 10.1 什么是卡尔曼滤波 卡尔曼滤波是一种**最优估计算法**,用于从含有噪声的测量数据中估计系统的真实状态。 **直觉理解:** 想象你在开车,GPS 给出位置但有误差,车速表给出速度也有误差。卡尔曼滤波就是综合这两个不完美的信息源,给出最优的位置和速度估计。 ### 10.2 一维卡尔曼滤波器(src/math/kalman.cj) **5 个核心公式:** | 步骤 | 公式 | 含义 | | -------------- | ----------------------------------- | ------------------------------------- | | 预测状态 | X_mid = A · X_last | 根据上一次最优估计预测当前状态 | | 预测协方差 | P_mid = A · P_last + Q | 预测的不确定性增大(+系统噪声Q) | | 计算卡尔曼增益 | kg = P_mid / (P_mid + R) | 测量越可靠(R小),增益越大,越相信测量 | | 更新状态 | X_now = X_mid + kg · (dat - X_mid) | 用测量值修正预测值 | | 更新协方差 | P_now = (1 - kg) · P_mid | 估计的不确定性减小 | **参数含义:** | 参数 | 值 | 含义 | | ---- | --- | -------------------------------------- | | A | 1 | 状态转移矩阵(恒等,因为假设状态不变) | | B | 0 | 控制输入矩阵(无控制输入) | | H | 1 | 观测矩阵(直接观测) | | Q | 20 | 系统噪声协方差(越大越信任测量值) | | R | 200 | 测量噪声协方差(越大越信任模型预测) | **在 MPU6050 中的应用:** 为加速度计和陀螺仪的每个轴各创建一个卡尔曼滤波器,持续滤波以获得平滑的姿态数据。 --- ## 11. FFT 快速傅里叶变换详解 ### 11.1 什么是 FFT 快速傅里叶变换(FFT)是一种高效计算离散傅里叶变换(DFT)的算法。它将时域信号转换为频域表示,让你看到信号中包含哪些频率成分。 **DFT 公式:** ``` X[k] = Σ x[n] · e^(-j·2π·k·n/N) 对于 k = 0, 1, ..., N-1 ``` ### 11.2 基-2 FFT 实现(src/cpp/fft1.c) **算法:Cooley-Tukey 基-2 FFT** **核心思想:分治法** - 将 N 点 DFT 分解为两个 N/2 点 DFT(偶数项和奇数项) - 递归分解直到 1 点 DFT(直接返回) - 时间复杂度从 O(N²) 降低到 O(N·log N) **关键函数:** | 函数 | 功能 | | -------------------- | ------------------------------------------ | | `initial()` | 初始化旋转因子 W^k = e^(-j·2π·k/N) | | `fft1(A, ifft)` | 复序列基-2 FFT(ifft=1 正变换,-1 逆变换) | | `fft1r2(A, B, ifft)` | 双实序列 FFT(同时变换两个实序列) | | `fft1r1(A, ifft)` | 单实序列 FFT(利用对称性优化) | | `inverseq(i, imax)` | 比特逆序(用于蝶形运算的地址重排) | **蝶形运算:** ``` 对于每一级 l: 对于每一组 j: 对于每个蝶形 k: t = A[jl1k] · W[ij] ← 旋转因子乘法 A[jl1k] = A[jl0k] - t ← 蝶形运算上支 A[jl0k] = A[jl0k] + t ← 蝶形运算下支 ``` ### 11.3 FFT 封装层(src/cpp/fft.cpp) 提供 C 接口给仓颉调用: - `gen_wave()` → 生成 10Hz 正弦波测试数据 - `start_fft(M)` → 对矩阵 M 执行 FFT,返回频谱幅度 --- ## 12. CNN 卷积神经网络详解 ### 12.1 CNN 架构 CNN 模块完全用 C++ 实现,通过 C 接口(`extern "C"`)暴露给仓颉调用。 **包含的层:** | 层 | 文件 | 功能 | | -------------------- | ---------------- | ------------------------------- | | ConvLayer | conv.cpp/h | 2D 卷积层 + 反向传播 + 参数更新 | | PoolingLayer | pooling.cpp/h | 最大/平均池化 | | GlobalAveragePooling | ga_pooling.cpp/h | 全局平均池化 | | FlattenLayer | flatten.cpp/h | 展平(4D → 2D) | | DynamicFlattenLayer | flatten.cpp/h | 动态形状展平 | | ImageLoader | imgl.cpp/h | 图像加载 + 归一化 | ### 12.2 C 接口设计(src/cpp/cnn/cnn_i.cpp) 所有函数遵循统一模式: ```c void* cnn_create_xxx_layer(...) ← 创建层 void cnn_destroy_xxx_layer(void*) ← 销毁层 float* cnn_xxx_forward(void*, ...) ← 前向传播 float* cnn_xxx_backward(void*, ...) ← 反向传播 void cnn_xxx_update_parameters(void*, lr, ...) ← 更新参数 ``` **内存管理:** 输出数组通过 `new float[]` 分配,调用方负责 `delete[]`。 **错误处理:** 使用 `thread_local` 错误缓冲区 + `cnn_get_last_error()` 查询。 ### 12.3 卷积层 ConvLayer **前向传播:** ``` 对于每个输出通道 oc: 对于每个空间位置 (oh, ow): sum = bias[oc] 对于每个输入通道 ic: 对于每个核位置 (kh, kw): sum += weight[oc,ic,kh,kw] * input[ic,oh*stride+kh,ow*stride+kw] output[oc,oh,ow] = sum ``` **支持特性:** - 任意 stride 和 padding - Leaky ReLU 激活(内置,negative_slope 参数) - SGD + Momentum 优化器 - Adam 优化器 ### 12.4 图像加载器 ImageLoader **功能:** - 加载单张图片或批量图片 - 支持通道数、尺寸调整 - 归一化:[0,1] 归一化 或 mean/std 归一化 - 自动计算数据集统计量(mean/std) - 从目录批量加载 --- ## 13. CUDA GPU 加速详解 ### 13.1 CUDA 矩阵运算(src/cuda/matrix.cu) **架构:** 两层设计 1. **底层 CUDA kernel**:`_devAdd`、`_devSub`、`_devMul`、`_devMatDot` 等 2. **上层 C 接口**:`Cu4cj_Add`、`Cu4cj_Sub`、`Cu4cj_MatDot` 等 **内存管理:** | 函数 | 功能 | | ----------------------- | ------------------------------- | | `Cu4cj_Orig(n)` | 在 GPU 上分配 n 个 float 的内存 | | `Cu4cj_Zero(n)` | 在 GPU 上分配并清零 | | `Cu4cj_ToDev(h, n)` | 主机 → 设备拷贝 | | `Cu4cj_ToHost(d, h, n)` | 设备 → 主机拷贝 | | `Cu4cj_Free(d)` | 释放 GPU 内存 | **CUDA kernel 示例(向量加法):** ```cuda __global__ void _devAdd(float* d1, float* d2, float* r, int64_t n) { int i = blockIdx.x * blockDim.x + threadIdx.x; ← 计算全局线程ID if (i < n) { r[i] = d1[i] + d2[i]; ← 每个线程处理一个元素 } } ``` **调用方式:** ```c Cu4cj_Add(d1, d2, d_r, n) { _devAdd<<<(n + 255) / 256, 256>>>(d1, d2, d_r, n); // ↑ grid大小 ↑ block大小 // 每个block 256个线程,共 ceil(n/256) 个block } ``` **矩阵乘法 kernel:** ```cuda __global__ void _devMatDot(float* d1, float* d2, float* r, int64_t m, int64_t n, int64_t k) { int i = blockIdx.x * blockDim.x + threadIdx.x; int j = blockIdx.y * blockDim.y + threadIdx.y; if (i < m && j < k) { float sum = 0.0f; for (int p = 0; p < n; p++) { sum += d1[i * n + p] * d2[p * k + j]; } r[i * k + j] = sum; } } ``` 使用 2D 线程块 (16×16),每个线程计算结果矩阵的一个元素。 ### 13.2 GPU 激活函数 所有激活函数都在 GPU 上实现: - `_devSigmoid` / `_devSigmoidDerivative` - `_devReLU` / `_devReLUDerivative` - `_devLeakyReLU` / `_devLeakyReLU_Derivative` - `_devTanh` / `_devTanhDerivative` ### 13.3 C++ Matrix 类(CUDA 版) 在 `matrix.cu` 中还定义了一个 C++ `Matrix` 类,数据存储在 GPU 上,运算通过 CUDA kernel 执行。对外暴露与 CPU 版相同的 C 接口,使得仓颉代码无需修改即可切换 CPU/GPU。 --- ## 14. 线程与同步模块详解 ### 14.1 mutex 互斥锁(src/th/mutex.cj) 直接封装 pthread 互斥锁: ```cangjie foreign func pthread_mutex_init(mutex, attr): Int32 foreign func pthread_mutex_destroy(mutex): Int32 foreign func pthread_mutex_lock(mutex): Int32 foreign func pthread_mutex_unlock(mutex): Int32 ``` **关键常量:** `SIZE_PTHREAD_MUTEX_TYPE_BYTE = 48`(ARM64 上 pthread_mutex_t 的大小) **辅助函数:** - `_mutex_create()` → malloc 分配 48 字节 + 返回指针 - `_mutex_free(p)` → free 释放 - `_mutex_init(mutex)` → pthread_mutex_init - `_mutex_destroy(mutex)` → pthread_mutex_destroy - `_mutex_lock(mutex)` / `_mutex_unlock(mutex)` → 加锁/解锁 ### 14.2 pthread 线程(src/th/pthread.cj) ```cangjie public class Pth { private var pthread: UInt64 = 0 init(_main, args) → pthread_create(...) ← 创建线程 join(retval) → pthread_join(...) ← 等待线程结束 close() → pthread_cancel(...) ← 取消线程 } ``` --- ## 15. HTTP 服务器模块详解 ### 15.1 服务器配置(src/main.cj:192-294) **端点:** | 路径 | 方法 | 功能 | | ----------------- | ---- | ------------------------ | | `/api/v2/bot_run` | POST | 接收动作脚本并执行 | | `/` | GET | 返回前端页面 | | `/api/v1/fs` | POST | 文件系统操作(列表目录) | ### 15.2 文件系统 API 请求格式: ```json {"mode": "list", "path": "/path/to/directory"} ``` 响应:返回目录内容的 JSON 数组,每项包含: - `name`:文件名 - `type`:Directory / SymbolicLink / Regular - `writable` / `readable`:权限 - `size`:文件大小(仅普通文件) --- ## 16. 辅助工具详解 ### 16.1 图片标注工具(src/py/labeled.py) **用途:** 用于 CNN 训练数据的标注,手动在图片上画矩形框。 **技术栈:** Python + Tkinter + PIL + OpenCV **功能:** - 打开图片、缩放、平移 - 绘制矩形标注框 - 保存/加载标注(百分比坐标格式:top right bottom left) - 删除/编辑标注 ### 16.2 前端页面(public/index.html) 一个科技感的展示页面,使用 Tailwind CSS,支持暗色模式、移动端适配。 ### 16.3 API 测试(test/test_api_fs.py) 测试文件系统 API 的 Python 脚本,支持不同路径格式。 --- ## 17. 构建系统详解 ### 17.1 Makefile 构建 C/C++ 共享库: | 目标 | 源文件 | 输出 | 用途 | | -------------- | ---------------------- | -------- | ---------------------------- | | `libi2c4cj.so` | I2C.c | I2C 驱动 | 旧版 I2C(已弃用) | | `libsnd4cj.so` | snd.cpp | 音频播放 | Windows WaveAPI / Linux ALSA | | `libmt4cj.so` | Matrix.cpp / matrix.cu | 矩阵运算 | CPU 或 GPU 版本 | | `libfft4cj.so` | fft1.c + fft.cpp | FFT | 快速傅里叶变换 | | `libcnn4cj.so` | cnn_i.cpp + ... | CNN | 卷积神经网络 | **MODE 变量:** `MODE=cpu` 使用 C++ Matrix,`MODE=cuda` 使用 CUDA Matrix。 ### 17.2 cjpm.toml 仓颉包管理配置: - 目标平台:`aarch64-unknown-linux-gnu`(树莓派 ARM64) - 编译选项:`-l c -l pthread`(链接 C 库和 pthread) - 不支持 Windows(因为依赖 glibc) ### 17.3 运行脚本 ```bash # 构建 _set.sh → cjpm build # 运行(设置大栈空间) _run.sh → export cjStackSize=10000kb && ./target/release/bin/main $@ ``` --- ## 18. 全局缺陷与改进建议 ### 18.1 严重缺陷 | 编号 | 缺陷 | 位置 | 详细说明 | 严重程度 | | ---- | ----------------------------------------- | ------------------- | -------------------------------------------------------------------------------------- | ------------ | | D1 | **析构器调用时间不确定** | r.cj:28 | 仓颉的 GC 不保证析构器调用时间,I2C 文件描述符可能泄漏 | 高 | | D2 | **扬声器仅延时未发声** | hd/snd.cj:17 | `play()` 只 sleep,C++ 实现的 `play_frequency()` 未被调用 | 高 | | D3 | **MPU6050 线程无法安全退出** | hd/MPU6050.cj:115 | `mpu6050_main()` 是无限循环,`pthread_cancel` 可能在不安全点中断 | 高 | | D4 | **I2C 锁系统线性扫描** | hd/I2C.cj:129 | 每次加锁/解锁都遍历 20 个元素的数组,O(n) 复杂度 | 中 | | D5 | **xavierUniformInit 每个元素新建 Random** | math/Ini.cj:30 | 循环内每次`DateTime.now().nanosecond` + `Random()`,种子可能重复 | 中 | | D6 | **Softmax 雅可比矩阵反向传播未正确使用** | math/Af.cj:246 | `softmax_jacobian` 返回 n×n 矩阵,但 `backward` 调用者期望与输入同形状 | 高 | | D7 | **旧版 Nn 反向传播有错误** | math/nn.cj:186 | 旧版代码注释中标注 "This is Wrong" | 中(已弃用) | | D8 | **tanh 导数输入错误** | math/Af.cj:194-206 | `tanh_dot` 接收的是激活后的输出 y,但计算 `1 - x*x` 用的是原始输入 x,应改为 `1 - y*y` | 高 | | D9 | **I2C 读寄存器返回 Int8 而非 UInt8** | hd/I2C.cj:81 | `readI2cReg` 返回 `Int8`,负值寄存器数据会出错 | 中 | | D10 | **MPU6050 ADDR_0 和 ADDR_1 相同** | hd/MPU6050.cj:14-15 | 两个地址常量都是 0x68,ADDR_1 应为 0x69 | 中 | | D11 | **矩阵列数不一致时的错误信息不清晰** | P.cj:93 | `DoesNotSync` 异常只报行号,不报期望列数和实际列数 | 低 | ### 18.2 设计缺陷 | 编号 | 缺陷 | 详细说明 | 建议 | | ---- | -------------------------- | ------------------------------------------------------- | -------------------------- | | D12 | **JsonValue 通信开销大** | 每次硬件操作都要构造/解析 JSON,对于高频 I2C 操作性能差 | 对高频路径提供直接调用接口 | | D13 | **硬件变量创建顺序硬编码** | `createHardVars()` 中顺序固定,无法处理依赖关系变化 | 使用依赖图自动排序 | | D14 | **无 batch 训练支持** | 神经网络每次只训练一个样本 | 增加 batch 维度 | | D15 | **无 Adam 优化器** | 仓颉版 Nn1 只有 SGD | 实现 Adam 优化器 | | D16 | **Tensor 类未使用** | Tensor 类已实现但未集成到神经网络中 | 统一 Matrix1 和 Tensor | | D17 | **FFT 未集成** | C++ FFT 已实现但仓颉接口被注释 | 恢复 FFT 接口 | | D18 | **CNN 未集成** | C++ CNN 已实现但仓颉端无调用代码 | 添加仓颉端 CNN 接口 | | D19 | **DRL 代码全部注释** | 深度强化学习微调功能被注释 | 重构后重新启用 | | D20 | **无单元测试** | 数学库、解析器等无自动化测试 | 添加测试框架 | ### 18.3 安全缺陷 | 编号 | 缺陷 | 详细说明 | | ---- | ----------------------- | ------------------------------------------------- | | D21 | **文件系统 API 无认证** | `/api/v1/fs` 可列出任意目录内容,无权限检查 | | D22 | **HTTP 服务器无认证** | `/api/v2/bot_run` 可执行任意脚本,无认证 | | D23 | **路径遍历风险** | 文件系统 API 接受用户输入的路径,可能遍历敏感目录 | ### 18.4 代码质量问题 | 编号 | 问题 | 位置 | | ---- | --------------------------------------------------- | --------------------------------------------------------------------------------------------- | | D24 | 大量注释掉的旧代码 | P.cj(300+ 行注释)、nn.cj、nns.cj、main.cj | | D25 | `print(s)` 调试输出未移除 | P.cj:147 | | D26 | `FIXME` 和 `TODO` 标记散布各处 | 全项目约 30+ 处 | | D27 | C++ Matrix 的`get()` 索引方式与仓颉版不同 | Matrix.cpp:24 用`col * row + row`,Matrix.cj:91 用 `row * col + col` | | D28 | CUDA 转置函数`Cu4cj_Transport` 分配内存大小可能错误 | matrix.cu:330 用`Cu4cj_Zero(c * r * sizeof(float))`,但 `Cu4cj_Zero` 参数是元素个数不是字节数 | | D29 | CUDA`Cu4cj_Zero` 注释说 n=10000 时被杀死 | matrix.cu:74 可能是 GPU 内存不足 | --- ## 19. 学习路线图 ### 第一阶段:理解脚本语言(1-2天) 1. 阅读 `scripts/test.mt`,理解 `.mt` 脚本语法 2. 对照 `README.md` 中的语法说明 3. 手动写一个简单的 `.mt` 脚本 ### 第二阶段:理解解析器(2-3天) 1. 从 `MatrixObject` 开始,理解数据结构 2. 跟踪 `MatrixParser.parse()` 的状态机逻辑 3. 用纸笔画出状态转换图 ### 第三阶段:理解硬件驱动(3-5天) 1. 学习 I2C 通信原理 2. 阅读 `I2C.cj`,理解 `open`/`ioctl`/`read`/`write` 系统调用 3. 阅读 `PCA9685.cj`,理解 PWM 和舵机控制 4. 阅读 `MPU6050.cj`,理解传感器读取和卡尔曼滤波 ### 第四阶段:理解机器人结构(2-3天) 1. 理解 `MatrixVars_Hardware` 的闭包设计 2. 跟踪一个完整的硬件变量创建流程(如 MtVars_Servo) 3. 跟踪 `run_drl_action()` 的执行流程 ### 第五阶段:理解神经网络(3-5天) 1. 学习矩阵运算(Matrix1 类) 2. 学习激活函数(Af 枚举) 3. 手动计算一次前向传播和反向传播 4. 理解 Xavier 初始化 ### 第六阶段:理解高级功能(选学) 1. 卡尔曼滤波:从数学公式到代码实现 2. FFT:理解蝶形运算 3. CNN:理解卷积、池化、展平 4. CUDA:理解 GPU 编程模型 --- ## 附录 A:仓颉语言关键语法速查 | 语法 | 含义 | 对应语言 | | -------------------------- | ----------- | ------------------------- | | `package foo` | 声明包 | Java package | | `import std.io.*` | 导入 | Java import | | `class A <: B` | 继承 | Java extends | | `open class` | 允许继承 | Java 非 final | | `let / var` | 不可变/可变 | Kotlin val/var | | `Option` | 可选类型 | Java Optional | | `Some(v) / None` | 有值/无值 | Optional.of/present | | `if (let Some(v) <- expr)` | if-let 解构 | Swift if-let | | `match(expr) { case ... }` | 模式匹配 | Kotlin when / Rust match | | `foreign func` | FFI 声明 | Java native / Rust extern | | `unsafe { }` | 不安全块 | Rust unsafe | | `CPointer` | C 指针 | Rust *mut T | | `@C` | C 兼容注解 | 使结构体内存布局兼容 C | | `operator func []` | 运算符重载 | C++ operator[] | | `mut prop` | 可变属性 | Kotlin mutable property | | `init()` | 构造函数 | Java constructor | | `~init()` | 终结器 | Java finalize | | `extend A { }` | 扩展方法 | Kotlin extension | --- ## 附录 B:术语表 | 术语 | 英文 | 含义 | | ---------- | --------------------------- | ------------------------------------ | | 舵机 | Servo | 一种可以精确控制角度的电机 | | I2C | Inter-Integrated Circuit | 一种两线制串行总线协议 | | PWM | Pulse Width Modulation | 脉宽调制,通过改变脉冲宽度控制输出 | | 陀螺仪 | Gyroscope | 测量角速度的传感器 | | 加速度计 | Accelerometer | 测量加速度的传感器 | | 前向传播 | Forward Propagation | 神经网络从输入到输出的计算过程 | | 反向传播 | Backpropagation | 神经网络从输出到输入的梯度计算过程 | | 梯度下降 | Gradient Descent | 沿梯度反方向更新参数以最小化损失 | | 学习率 | Learning Rate | 每次参数更新的步长 | | 损失函数 | Loss Function | 衡量预测与真实值差距的函数 | | 激活函数 | Activation Function | 引入非线性的函数(如 ReLU、Sigmoid) | | 卡尔曼增益 | Kalman Gain | 决定多大程度上相信测量值 vs 预测值 | | 协方差 | Covariance | 衡量不确定性的量 | | 傅里叶变换 | Fourier Transform | 将信号从时域转换到频域 | | 蝶形运算 | Butterfly Operation | FFT 中的基本计算单元 | | 卷积 | Convolution | 滑动窗口加权求和的运算 | | 池化 | Pooling | 降采样操作(取最大值或平均值) | | 展平 | Flatten | 将多维数据变为一维 | | FFI | Foreign Function Interface | 调用其他语言函数的接口 | | DRL | Deep Reinforcement Learning | 深度强化学习 |