58 KiB
bot4cj 项目深度分析文档
本文档对 bot4cj 项目进行全面、详细的分析,涵盖项目架构、每个模块的实现细节、算法原理、数据流、缺陷与改进建议。目标读者为零基础学习者。
目录
- 项目总览
- 项目架构与目录结构
- 核心数据流:从脚本到硬件
- 矩阵式脚本语言(.mt)详解
- 解析器模块详解
- 机器人结构模块详解
- 硬件驱动层详解
- 数学库详解
- 神经网络模块详解
- 卡尔曼滤波详解
- FFT 快速傅里叶变换详解
- CNN 卷积神经网络详解
- CUDA GPU 加速详解
- 线程与同步模块详解
- HTTP 服务器模块详解
- 辅助工具详解
- 构建系统详解
- 全局缺陷与改进建议
- 学习路线图
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...)都被封装为一个硬件变量,它们:
- 通过闭包持有真实的硬件对象(如
I2C_Orig、PCA9685、Servo) - 通过 JsonValue 进行统一接口通信(输入 JsonValue,输出 JsonValue)
- 有生命周期:
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() 流程:
- 从
@I2C矩阵中读取 name 和 path - 创建
I2C_Orig(path)对象(打开 I2C 设备文件) - 通过闭包将
I2C_Orig保存在things回调中 things支持 4 种模式:get_fd:获取文件描述符set_addr:设置从机地址set_val:写寄存器get_val:读寄存器
free回调中关闭 I2C 设备
MtVars_PCA9685(PCA9685 变量,src/r.cj:244-314)
用途: 封装 PCA9685 舵机控制板。
工厂函数流程:
- 从
@PCA9685矩阵读取 name、I2C 引用名、地址 - 从硬件变量字典中找到对应的 I2C 变量
- 通过 I2C 变量的
play({"mode": "get_fd"})获取文件描述符 - 创建
PCA9685(fd, addr)对象 things回调:接收{"channel": n, "angle": a},调用pca9685.setAngle(n, a)rise回调:调用pca9685.setup()初始化 PWM 频率
MtVars_MPU6050_BIND(MPU6050 变量,src/r.cj:188-241)
用途: 封装 MPU6050 陀螺仪/加速度计。
工厂函数流程:
- 从
@MPU6050_BIND矩阵读取参数 - 通过 I2C 变量获取文件描述符
- 创建
MPU6050(fd, addr)对象 rise回调:mpu6050.setup()+mpu6050.startDaemon()(启动采样线程)free回调:mpu6050.stopDaemon()+mpu6050.waitDaemon()(停止并等待线程)
MtVars_Servo(舵机变量,src/r.cj:320-373)
用途: 封装单个舵机,包含角度限制和偏移。
工厂函数流程:
- 从
@Servo矩阵读取 name、max、min、offset、controller、channel - 从字典中找到控制器(如 pca9685-0)
- 创建
Servo(max, min, offset, callback_setAngle)对象 callback_setAngle闭包:将角度通过控制器的play()发送给 PCA9685things回调:接收{"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)
用途: 封装扬声器,支持频率限制。
工厂函数流程:
- 从
@Speaker矩阵读取参数 - 创建
Spk(channels, sample_rate)对象 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):
对于 @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 矩阵的一列:
- 取列名(第 0 行),如
s1.A - 按
.分割为设备名和参数名 - 检查参数名是否在
@Alias中定义了别名,如有则替换 - 将数值加入对应设备的参数集合
7. 硬件驱动层详解
7.1 I2C 通信(src/hd/I2C.cj)
核心设计:直接调用 glibc 函数
项目没有使用自定义的 C 共享库来操作 I2C,而是直接在仓颉中通过 foreign func 声明并调用 Linux glibc 的原生函数:
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 操作流程:
openI2cDev(path)→ 打开/dev/i2c-N设备文件setI2cAddr(fd, addr)→ 通过ioctl设置从机地址writeI2cReg(fd, reg, val)→ 写入寄存器:先写寄存器地址,再写值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() 初始化流程:
- 加锁
- 设置 I2C 地址
- 写寄存器 0x00 = 0x20(开启地址自增,退出睡眠)
- 等待振荡器启动(5ms)
- 计算预分频值:
prescale = 25000000 / (4096 * 50) - 1 ≈ 100(50Hz PWM) - 写寄存器 0x00 = 0x10(进入睡眠模式,允许改频率)
- 写寄存器 0xFE = prescale(设置预分频值)
- 写寄存器 0x00 = 0x20(退出睡眠)
- 解锁
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() 初始化:
- 退出睡眠模式(写 PWR_MGMT_1 = 0x00)
- 设置陀螺仪量程 ±500°/s
- 设置加速度计量程 ±8g
- 等待 100ms 稳定
readSensor() 读取数据:
- 读取 6 个 16 位寄存器(加速度 X/Y/Z + 陀螺仪 X/Y/Z)
- 每个轴需要读高字节和低字节,组合为 16 位有符号整数
守护线程 mpu6050_main():
这是一个持续运行的线程,不断采样并滤波:
- 读取原始传感器数据
- 对 6 个轴分别进行卡尔曼滤波
- 打印滤波后的数据
- 睡眠 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)
当前状态: 仅实现了延时功能,实际发声被注释掉。
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 为值类型,支持运算符重载。
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 的多维推广。
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)。
数据结构:
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):
使用数值稳定的实现方式:
- 找到最大值
max_val - 计算
exp(x_i - max_val)避免指数溢出 - 除以总和归一化
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)
所有函数遵循统一模式:
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)
架构: 两层设计
- 底层 CUDA kernel:
_devAdd、_devSub、_devMul、_devMatDot等 - 上层 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 示例(向量加法):
__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]; ← 每个线程处理一个元素
}
}
调用方式:
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:
__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 互斥锁:
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)
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
请求格式:
{"mode": "list", "path": "/path/to/directory"}
响应:返回目录内容的 JSON 数组,每项包含:
name:文件名type:Directory / SymbolicLink / Regularwritable/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 运行脚本
# 构建
_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 | CUDACu4cj_Zero 注释说 n=10000 时被杀死 |
matrix.cu:74 可能是 GPU 内存不足 |
19. 学习路线图
第一阶段:理解脚本语言(1-2天)
- 阅读
scripts/test.mt,理解.mt脚本语法 - 对照
README.md中的语法说明 - 手动写一个简单的
.mt脚本
第二阶段:理解解析器(2-3天)
- 从
MatrixObject开始,理解数据结构 - 跟踪
MatrixParser.parse()的状态机逻辑 - 用纸笔画出状态转换图
第三阶段:理解硬件驱动(3-5天)
- 学习 I2C 通信原理
- 阅读
I2C.cj,理解open/ioctl/read/write系统调用 - 阅读
PCA9685.cj,理解 PWM 和舵机控制 - 阅读
MPU6050.cj,理解传感器读取和卡尔曼滤波
第四阶段:理解机器人结构(2-3天)
- 理解
MatrixVars_Hardware的闭包设计 - 跟踪一个完整的硬件变量创建流程(如 MtVars_Servo)
- 跟踪
run_drl_action()的执行流程
第五阶段:理解神经网络(3-5天)
- 学习矩阵运算(Matrix1 类)
- 学习激活函数(Af 枚举)
- 手动计算一次前向传播和反向传播
- 理解 Xavier 初始化
第六阶段:理解高级功能(选学)
- 卡尔曼滤波:从数学公式到代码实现
- FFT:理解蝶形运算
- CNN:理解卷积、池化、展平
- 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 | 深度强化学习 |