- 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
1407 lines
58 KiB
Markdown
1407 lines
58 KiB
Markdown
# 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 | 深度强化学习 |
|