Files
blackbird/docs/reports/bot4cj_analysis_20260525_00.md
T
qing a7a78d1424 docs: aggregate project documentation under docs/ with unified index
- Move all per-module security audit reports to docs/security/
- Move code review logs to docs/reviews/, analysis reports to docs/reports/
- Add docs/INDEX.md as single entry point for all project documentation
- Add documentation management conventions section to README.md
- Clean up empty per-module docs/ directories
2026-08-03 13:21:48 +08:00

1407 lines
58 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<MatrixObject>
▼
┌─────────────┐
│ RobotStruct │ 从 MatrixObject 列表构建硬件变量字典
│ (src/r.cj) │ HashMap<String, MatrixVars_Hardware>
└──────┬──────┘
│
├── 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<ArrayList<String>> ← 二维字符串矩阵
}
```
**关键方法:**
| 方法 | 功能 | 零基础解释 |
| ---------------------- | ---------------- | ---------------------------------------------------- |
| `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<MatrixObject>` 序列化回文本格式。
**格式:**
```
@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<T>(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<Unit>, count: UIntNative): IntNative
foreign func write(fd: Int32, buf: CPointer<Unit>, 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<FP32>`,行优先存储。
```
___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<T> {
data: Array<T> ← 一维扁平存储
dims: Array<Int64> ← 维度列表(形状)
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<Matrix1> ← 本次输入
prev_raw_inputs: Option<Matrix1> ← 激活前的输出(z = Wx + b)
prev_outputs: Option<Matrix1> ← 激活后的输出(a = f(z))
// 梯度缓存
weights_grad: Option<Matrix1> ← 权重梯度
bias_grad: Option<Matrix1> ← 偏置梯度
}
```
**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<T>` | 可选类型 | 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<T>` | 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 | 深度强化学习 |