基于MAX78000的“浮空输入”遥控笔方案
该项目使用了KiCad软件、Python、C、C#程序语言,实现了一个基于MAX78000平台的“浮空输入”遥控笔的设计,它的主要功能为:只要“书空”就能对电脑进行输入。同时也能完成聚光灯、数字激光、实体激光、音量增减、PPT翻页、视频快进快退、飞鼠等功能。
标签
嵌入式系统
AI
MAX78000
smallcracker
更新2024-01-10
哈尔滨工业大学
1722

项目介绍

出发点

人们对输入的便捷性追求是永无止境的。目前的计算机输入设备有键盘,鼠标,数位板,遥控笔,3D-鼠标等,其中,遥控笔因为其方便握持,能远程连接的特点,常用于 PPT 放映,视频播放控制等场景,在教育培训、宣传演讲等场景中有重要应用。我们计算开发一款功能更加强大的遥控笔。其核心特色功能是可以作为键盘使用,只需要“书空”就能输入对应的字符

FjwexLmZFujUJOLWnGPNZj0LARI4

项目特色

本项目特色在于通过“书空”就能输入文字,也就是用遥控笔在空中写一个“1”,就能在电脑中输入数字1。目前市面上的翻页笔所支持的手势种类相对较少,无法满足用户日益增长的多样化需求。 为了解决这一问题,我们计划发挥MAX78000拥有cnn加速模块的优势,利用人工智能技术,设计出一款支持更多手势输入的翻页笔。我将深入研究各种常见手势的含义和用途,并通过机器学习算法训练翻页笔识别和学习更多的手势模式

项目设计思路

遥控笔上和用户进行交互的装置有按钮、开关、惯性测量单元、激光头。MAX78000把开关、按钮的电平以及IMU的测量数据读进来,通过一系列逻辑,以及神经网络对IMU信号的分类,得到期望的控制信号。信号通过串口发送给esp32,再通过蓝牙转发给电脑,即可实现手势控制、手势输入的功能。同时我还使用 KiCad 设计了一个拓展板,让遥控笔握持起来更加舒适。

FoGnOhWEZEWr80RCIaJPAa8cde5h

效果展示

最终得到的作品如图所示:

FrRTDn52ZZkS9pePu113UxMzoPYd

使用手势输入数字的展示如图:

更多数字的书写见上方b站视频。此外还有手势截图、手势切换应用的展示:

当然,作为一个翻页笔,常规的聚光灯、数字激光、实体激光、音量增减功能、PPT翻页、视频快进快退功能、飞鼠功能肯定是有的

实现过程

1. 整体流程

FuwTuZqgQuXpshRn2aCizfrS_Fqp

2. 数据收集及数据集制作

2.1 单片机读取IMU数据 电脑收集数据

项目中需要用到 IMU,我选用了ICM20602 传感器。ICM20602芯片是一款集成了3轴陀螺仪和3轴加速度计的6轴运动跟踪芯片,封装为小巧的3毫米×3毫米×0.75毫米的16引脚LGA。选用该芯片的主要是看重了其性能较高,陀螺仪灵敏度误差仅为±1%,陀螺仪噪声为4 mdps/√Hz,加速度计噪声为100μg/√Hz。

该芯片通信端口包括I2C和高速SPI(最大10MHz)。这里我们采用SPI协议进行通讯。该芯片的读取寄存器分成两步,先向芯片发送要读取的地址,然后接受读取的信息。这里分别设计了写和读的过程:

mxc_spi_req_t request;
// 写入要读取的地址
request.txData = tx_data;
request.txLen = 1;
request.rxData = NULL;
request.rxLen = 0;
request.ssDeassert = 0;
MXC_SPI_MasterTransaction(&request);
// 读取返回的数据
request.txData = NULL;
request.txLen = 0;
request.rxData = rx_data;
request.rxLen = 128;
request.ssDeassert = 1;
MXC_SPI_MasterTransaction(&request);

因为读和写的过程是分开的,第一个读的过程结尾不要deassert。

FrOF5_PrYUxGnhXt3NqleKRGTwqR

这样就能读到惯性传感器的测量数据了。一次读取的数据包括14个字节,分别为:

Fv-RlOWDYEhM6IW2y3h8aS1erfz5

之后我们手持遥控笔,做对应的动作。单片机通过串口将数据发送至电脑并保存。一个采集周期为1秒,采集到的数据 如图所示:

FpcIUW_o1oOWzdj0j3sKpWr6QLub

2.2 数据清洗 将数据制作成图片

为了便于进行网络移植,同时也为了便于可视化观察数据集,我将IMU读取到的数据通过Python脚本转化成了图片。这样我就可以方便地移植美信官方提供的各种图像分类的网络进行尝试,同时也能利用起来pytorch提供的便利的数据读取、数据增强的函数。

首先,我使用正则表达式对传感器数据进行提取

pattern = r'([0-9A-Z]{2} ){14}00 00 00 (?!00 00 )'

之后,因为原始的角速度信息为16bits,而cnn分类的输入为8bits,同时为了提升分类效果,需要对原始数据进行标准化。这个标准化过程在电脑上和单片机上都要进行,而且要实现同样的操作。为了照顾单片机的处理速度,我用移位操作代替了除以标准差的操作,设计了如下标准化过程

    @staticmethod
def process_matrix(matrix):
# 首先确保matrix是numpy数组
if not isinstance(matrix, np.ndarray):
matrix = np.array(matrix)
# 使用numpy的where函数根据条件进行加减操作
matrix = np.where(matrix > 32767, matrix-65536, matrix)
matrix = matrix/32
matrix = np.where(matrix > 127, 127, matrix)
matrix = np.where(matrix < -128, -128, matrix)
matrix = matrix+128
return matrix

这个过程在单片机上的实现为:

uint8_t nomilization(uint8_t h, uint8_t l, int16_t mean, int16_t bits) {
int16_t _16bit = (*(int16_t*)&h) << 8 | l;
_16bit = ((_16bit - mean) >> bits) + 128;
if (_16bit >= 255)
return 255;
if (_16bit <= 0)
return 0;
return (uint8_t)_16bit;
}

uint8_t gx = nomilization(rx_data[8], rx_data[9], 0, 5);
uint8_t gy = nomilization(rx_data[10], rx_data[11], 0, 5);
uint8_t gz = nomilization(rx_data[12], rx_data[13], 0, 5);

// 之后还要异或 0x80;

最后,我将IMU的数据转化成为了图像数据,具体的方法为将gx,gy,gz作为三个通道,从图像的左上角到右下角逐行填入,最终得到的数据集举例有:

FlgdFX0hFWMfl4_3CO6vjXAa_5fI

3. 模型设计及网络训练

因为单个数据的大小为3*6*6,在各种神经网络分类中属于较小的输入数据,同时也因为数据集较小,因此我设计了一个较小的神经网络来进行分类,具体结构如图:

FuiaaHeC0InWZgvcwnyEX854yus7

具体的模型设计文件为:

from torch import nn
import ai8x

class AI85motion(nn.Module):
def __init__(
self,
num_classes=3,
num_channels=3,
dimensions=(12, 12), # pylint: disable=unused-argument
bias=False,
**kwargs
):
super().__init__()
self.conv1 = ai8x.FusedConv2dBNReLU(num_channels, 16, 3, stride=1, padding=1, bias=bias, **kwargs)
self.conv2 = ai8x.FusedMaxPoolConv2dBNReLU(16, 9, 3, pool_size=2, pool_stride=2, stride=1, padding=0, bias=bias, **kwargs)

def forward(self, x): # pylint: disable=arguments-differ
"""Forward prop"""
x = self.conv1(x)
x = self.conv2(x)
x = x.view(x.size(0), -1)
return x

def ai85motion(pretrained=False, **kwargs):
assert not pretrained
return AI85motion(**kwargs)

models = [
{
'name': 'ai85motion',
'min_input': 1,
'dim': 2,
},
]

训练使用美信官方提供的框架。根据训练的需要,设置了训练的策略:policies/schedule-motion.yaml

---
lr_schedulers:
training_lr:
class: MultiStepLR
milestones: [10, 20, 30]
gamma: 0.25

policies:
- lr_scheduler:
instance_name: training_lr
starting_epoch: 0
ending_epoch: 40
frequency: 1

以及量化训练的策略:policies/qat_motion.yaml

---
start_epoch: 20
shift_quantile: 0.985
weight_bits: 8

可以查看:Analog Devices AI (github.com)更加深入地了解关于框架的使用方法 。之后输入参数,即可开始训练:

python train.py --epochs 40 --optimizer Adam --lr 0.001 --wd 0 --compress policies/schedule-motion.yaml --model ai85motion --dataset motion --device MAX78000 --batch-size 128 --print-freq 10 --validation-split 0 --qat-policy policies/qat_motion.yaml --use-bias "$@" --data ./data/motion --enable-tensorboard

训练的过程为:

(ai8x) smallcracker@LAPTOP-DC0VFSJJ:~/code/max78000-ai/ai8x-training$ python train.py --epochs 40 --optimizer Adam --lr 0.001 --wd 0 --compress policies/schedule-motion.yaml --model ai85motion --dataset motion --device MAX78000 --batch-size 128 --print-freq 10 --validation-split 0 --qat-policy policies/qat_motion.yaml --use-bias "$@" --data ./data/motion --enable-tensorboard
Configuring device: MAX78000, simulate=False.
Log file for this run: /home/smallcracker/code/max78000-ai/ai8x-training/logs/2023.12.17-011122/2023.12.17-011122.log


---
Logging to TensorBoard - remember to execute the server:
tensorboard --logdir='./logs'

{'start_epoch': 20, 'shift_quantile': 0.985, 'weight_bits': 8}
Optimizer Type: <class 'torch.optim.adam.Adam'>
Optimizer Args: {'lr': 0.001, 'betas': (0.9, 0.999), 'eps': 1e-08, 'weight_decay': 0.0, 'amsgrad': False}
Dataset sizes:
training=3124
validation=716
test=716
Reading compression schedule from: policies/schedule-motion.yaml


Training epoch: 3124 samples (128 per mini-batch)
Epoch: [0][ 10/ 25] Overall Loss 2.135687 Objective Loss 2.135687 LR 0.001000 Time 0.164879Epoch: [0][ 20/ 25] Overall Loss 2.070709 Objective Loss 2.070709 LR 0.001000 Time 0.109497Epoch: [0][ 25/ 25] Overall Loss 2.043880 Objective Loss 2.043880 Top1 73.888889 Top5 95.000000 LR 0.001000 Time 0.097627--- validate (epoch=0)-----------
716 samples (128 per mini-batch)
Epoch: [0][ 6/ 6] Loss 2.160456 Top1 61.452514 Top5 93.296089==> Top1: 61.453 Top5: 93.296 Loss: 2.160

==> Best [Top1: 61.453 Top5: 93.296 Sparsity:0.00 Params: 1728 on epoch: 0]
Saving checkpoint to: logs/2023.12.17-011122/checkpoint.pth.tar


Training epoch: 3124 samples (128 per mini-batch)
Epoch: [1][ 10/ 25] Overall Loss 1.893476 Objective Loss 1.893476 LR 0.001000 Time 0.071982Epoch: [1][ 20/ 25] Overall Loss 1.868204 Objective Loss 1.868204 LR 0.001000 Time 0.055311Epoch: [1][ 25/ 25] Overall Loss 1.859942 Objective Loss 1.859942 Top1 80.000000 Top5 95.555556 LR 0.001000 Time 0.052230--- validate (epoch=1)-----------
716 samples (128 per mini-batch)
Epoch: [1][ 6/ 6] Loss 2.035450 Top1 78.491620 Top5 96.368715==> Top1: 78.492 Top5: 96.369 Loss: 2.035

==> Best [Top1: 78.492 Top5: 96.369 Sparsity:0.00 Params: 1728 on epoch: 1]
Saving checkpoint to: logs/2023.12.17-011122/checkpoint.pth.tar

。。。中间过程省略

Training epoch: 3124 samples (128 per mini-batch)
Epoch: [7][ 10/ 25] Overall Loss 1.665525 Objective Loss 1.665525 LR 0.001000 Time 0.071485Epoch: [7][ 20/ 25] Overall Loss 1.654179 Objective Loss 1.654179 LR 0.001000 Time 0.055514Epoch: [7][ 25/ 25] Overall Loss 1.656868 Objective Loss 1.656868 Top1 90.000000 Top5 98.333333 LR 0.001000 Time 0.052205--- validate (epoch=7)-----------
716 samples (128 per mini-batch)
Epoch: [7][ 6/ 6] Loss 1.563096 Top1 90.642458 Top5 96.648045==> Top1: 90.642 Top5: 96.648 Loss: 1.563

==> Best [Top1: 90.782 Top5: 96.788 Sparsity:0.00 Params: 1728 on epoch: 6]
Saving checkpoint to: logs/2023.12.17-011122/checkpoint.pth.tar
...
Training epoch: 3124 samples (128 per mini-batch)
Epoch: [39][ 10/ 25] Overall Loss 1.549194 Objective Loss 1.549194 LR 0.000016 Time 0.071183Epoch: [39][ 20/ 25] Overall Loss 1.553316 Objective Loss 1.553316 LR 0.000016 Time 0.055679Epoch: [39][ 25/ 25] Overall Loss 1.551925 Objective Loss 1.551925 Top1 91.111111 Top5 97.777778 LR 0.000016 Time 0.052187--- validate (epoch=39)-----------
716 samples (128 per mini-batch)
Epoch: [39][ 6/ 6] Loss 1.526219 Top1 91.201117 Top5 96.648045==> Top1: 91.201 Top5: 96.648 Loss: 1.526

==> Best [Top1: 91.341 Top5: 97.067 Sparsity:0.00 Params: 1728 on epoch: 33]
Saving checkpoint to: logs/2023.12.17-011229/qat_checkpoint.pth.tar
--- test ---------------------
716 samples (128 per mini-batch)
Test: [ 6/ 6] Loss 1.526273 Top1 91.201117 Top5 96.648045==> Top1: 91.201 Top5: 96.648 Loss: 1.526

最终训练的结果为:

训练集表现:Top1: 91.341 Top5: 97.067

测试集表现:Top1: 91.201 Top5: 96.648

无论是训练集还是测试集,Top1和Top5的准确率都相当高。这表明模型在分类任务上表现良好,能够有效地识别和区分不同的类别。训练集和测试集的准确率非常接近,这表明模型过拟合或欠拟合的问题不明显

4. 模型量化和部署

因为MAX78000硬件的限制,模型的权重最大只能为8bits,PC端训练出的神经网络需要经过量化才能进行部署。美信提供的synthesis工程可以帮助我们完成量化和部署任务,我们需要准备好一个networks/motion.yaml文件,用以对网络结构以及部署细节进行描述。在这个文件的编写可以参考美信的官方文档:MaximAI_Documentation/Guides/YAML Quickstart.md at master · MaximIntegratedAI/MaximAI_Documentation (github.com)

arch: ai85motion1
dataset: motion

layers:
- out_offset: 0x2000
processors: 0x0000000000000007
operation: conv2d
kernel_size: 3x3
pad: 1
activate: ReLU
data_format: HWC
streaming: false
- max_pool: 2
pool_stride: 2
pad: 0
operation: conv2d
kernel_size: 3x3
activate: ReLU
out_offset: 0x0000
processors: 0x0000ffff00000000

量化过程为:

(ai8x) smallcracker@LAPTOP-DC0VFSJJ:~/code/max78000-ai/ai8x-synthesis$ python quantize.py trained/best_without_bn.pth.tar trained/motion-q.pth.tar --device MAX78000 -v -c networks/motion.yaml "$@"
Configuring device: MAX78000
Reading networks/motion.yaml to configure network...
Converting checkpoint file trained/best_without_bn.pth.tar to trained/motion-q.pth.tar

Model keys (state_dict):
conv1.output_shift, conv1.weight_bits, conv1.bias_bits, conv1.quantize_activation, conv1.adjust_output_shift, conv1.shift_quantile, conv1.op.weight, conv1.op.bias, conv2.output_shift, conv2.weight_bits, conv2.bias_bits, conv2.quantize_activation, conv2.adjust_output_shift, conv2.shift_quantile, conv2.op.weight, conv2.op.bias
conv1.op.weight avg_max: 0.19751033 max: 0.28141353 mean: -0.0034502496 factor: [256.] bits: 8
conv1.op.bias avg_max: 0.008856256 max: 0.055651106 mean: -0.008856256 factor: [256.] bits: 8
conv2.op.weight avg_max: 0.24153727 max: 0.30837783 mean: -0.002213747 factor: [64.] bits: 8
conv2.op.bias avg_max: 0.11054543 max: 1.1653148 mean: 0.11054543 factor: [64.] bits: 8

量化后我们还可以检查网络的分类效果:

Configuring device: MAX78000, simulate=False.
Log file for this run: /home/smallcracker/code/max78000-ai/ai8x-training/logs/2023.12.15-161130/2023.12.15-161130.log
{'start_epoch': 10, 'weight_bits': 8}
=> loading checkpoint ./best_without_bn.pth.tar
=> Checkpoint contents:
+----------------------+-------------+-------------+
| Key | Type | Value |
|----------------------+-------------+-------------|
| arch | str | ai85motion1 |
| compression_sched | dict | |
| epoch | int | 13 |
| extras | dict | |
| optimizer_state_dict | dict | |
| optimizer_type | type | Adam |
| state_dict | OrderedDict | |
+----------------------+-------------+-------------+

=> Checkpoint['extras'] contents:
+--------------+--------+---------+
| Key | Type | Value |
|--------------+--------+---------|
| best_epoch | int | 13 |
| best_mAP | int | 0 |
| best_top1 | float | 91.3408 |
| current_mAP | int | 0 |
| current_top1 | float | 91.3408 |
+--------------+--------+---------+

Loaded compression schedule from checkpoint (epoch 13)
=> loaded 'state_dict' from checkpoint './best_without_bn.pth.tar'
Optimizer Type: <class 'torch.optim.sgd.SGD'>
Optimizer Args: {'lr': 0.1, 'momentum': 0.9, 'dampening': 0, 'weight_decay': 0.0001, 'nesterov': False}
Dataset sizes:
test=716
--- test ---------------------
716 samples (256 per mini-batch)
Test: [ 3/ 3] Loss 1.531021 Top1 91.340782 Top5 97.486034
==> Top1: 91.341 Top5: 97.486 Loss: 1.531

==> Confusion:
[[74 0 0 1 0 0 0 0 1]
[ 0 74 3 2 0 0 0 1 0]
[ 0 0 79 0 0 0 0 1 0]
[ 0 0 2 72 0 0 1 1 4]
[ 1 0 0 1 64 0 0 0 14]
[ 0 0 0 0 1 75 0 1 3]
[ 0 0 0 0 0 0 72 0 8]
[ 0 0 0 0 0 0 0 76 4]
[ 0 12 0 0 0 0 0 0 68]]

由最后的混淆矩阵可见量化之后模型的分类效果依然很优秀。生成C语言脚本的过程为:

(ai8x) smallcracker@LAPTOP-DC0VFSJJ:~/code/max78000-ai/ai8x-synthesis$ python ai8xize.py --test-dir "sdk/Examples/MAX78000/CNN" --prefix motion --checkpoint-file trained/motion-q.pth.tar --config-file networks/motion.yaml --overwrite --device MAX78000 --timer 0 --display-checkpoint --verbose --compact-data --mexpress --sample-input 'motion.npy' --boost 2.5 "$@"
WARNING: Cannot check for updates - no local repository.
Configuring device: MAX78000
Reading networks/motion.yaml to configure network...
WARNING: Cannot run "yamllint" linter to check networks/motion.yaml
Reading trained/motion-q.pth.tar to configure network weights...
Checkpoint for epoch 11, model ai85motion1 - weight and bias data:
InCh OutCh Weights Quant Shift Min Max Size Key Bias Quant Min Max Size Key
3 16 (48, 3, 3) 8 -1 -67 72 432 conv1.op.weight (16,) 8 -14 13 16 conv1.op.bias 16 9 (144, 3, 3) 8 1 -18 20 1296 conv2.op.weight (9,) 8 -19 75 9 conv2.op.biasTOTAL: 2 parameter layers, 1,753 parameters, 1,753 bytes
Configuring data set: motion.
motion...
NOTICE: --overwrite specified, writing to sdk/Examples/MAX78000/CNN/motion even though it exists.
Arranging weights... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
Storing weights... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%
Creating network... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 100%

之后可以在sdk/Examples/MAX78000/CNN/motion文件夹找到生成的C工程,可以直接编译烧入单片机进行测试。

FqsCxC67NTLqYyPUgYbasfdLAIIp

根据输出层对每一类的输出,我们可以比较得到单片机的预测结果,在上面的演示中,可以看到单片机能够准确地对手势进行分类。

5. 使用逻辑设计

当单片机上电后,用户在电脑上连接esp32的蓝牙。如果之前连接过这个设备,则这个过程可以自动完成。

之后我们需要打开一个上位机程序,这个程序用来实现聚光灯数字激光功能,如果不打开这个上位机程序,则聚光灯和数字激光功能无法使用,但是除此以外的其他功能的使用不受影响。这个上位机程序由C#语言编写,设计成了便携版,因此点击即用,不必安装。打开后自动运行在后台,不会给PPT讲解的过程带来干扰。这个功能实现的主要原理是上位机软件接受某一个快捷键之后开启聚光灯或者数字激光光点功能,我的遥控笔只负责模拟键盘发送这个快捷键。所以说如果大家没有我这个遥控笔,也是可以在键盘上开关聚光灯,数字激光。下面是上位机程序的核心代码:

public MouseForm()
{
Proc = HookCallback;

StartPosition = FormStartPosition.Manual;
Rectangle vs = SystemInformation.VirtualScreen;
Location = vs.Location + new Size(1, 1);
Size = vs.Size - new Size(2, 2);
FormBorderStyle = FormBorderStyle.None;
ShowInTaskbar = false;

const int WH_MOUSE_LL = 14;

using (Process process = Process.GetCurrentProcess())
using (ProcessModule module = process.MainModule)
{
Hook = SetWindowsHookEx(WH_MOUSE_LL, Proc, GetModuleHandle(module.ModuleName), 0);
}

const int DQTYPE_THREAD_CURRENT = 2;
const int DQTAT_COM_STA = 2;

DispatcherQueueOptions options = new DispatcherQueueOptions
{
dwSize = Marshal.SizeOf(typeof(DispatcherQueueOptions)),
threadType = DQTYPE_THREAD_CURRENT,
apartmentType = DQTAT_COM_STA
};
CreateDispatcherQueueController(options, out object controller);
}

private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
const int WM_MOUSEMOVE = 0x0200;

if (nCode >= 0 && (int)wParam == WM_MOUSEMOVE)
{
Point position = PointToClient(Cursor.Position);
MouseVisual.Offset = new Vector3(position.X, position.Y, 0);
}

return CallNextHookEx(Hook, nCode, wParam, lParam);
}

之后,使用板子上的开关和按键对聚光灯、数字激光、实体激光、音量增减、PPT翻页、视频快进快退功能进行控制。这里实现的主要原理是MAX78000单片机捕获到用户的输入,然后编码到一个控制命令中。控制命令通过串口发送给esp32。esp32上实现了一个简单的蓝牙HID协议栈,模仿键鼠功能。控制命令中各个部分对应着不同的功能,触发这些功能时,esp32模仿键鼠向电脑发送对应的指令。所以,所有能够由键鼠完成的指令,都是可以通过这个遥控笔实现。

生成控制指令、发送控制指令部分的代码为:

void buttonTimerHandler() {
MXC_TMR_ClearFlags(MXC_TMR2);
esp32_uart_data[0] &= 0b00000011;

timerCount++;
if (timerCount % 2) {
LED_Toggle(LED1);
}

// laser control
if (MXC_GPIO_InGet(laserSwitchInput.port, laserSwitchInput.mask)) {
uint32_t state = MXC_GPIO_OutGet(laserOutput.port, 0xffff);
state |= laserOutput.mask;
MXC_GPIO_OutSet(laserOutput.port, state);
} else {
uint32_t state = MXC_GPIO_OutGet(laserOutput.port, 0xffff);
state &= (~laserOutput.mask);
MXC_GPIO_OutClr(laserOutput.port, laserOutput.mask);
}

// focus light control
if (MXC_GPIO_InGet(focusLightSwitch.port, focusLightSwitch.mask)) {
esp32_uart_data[0] |= 0b10;
} else {
esp32_uart_data[0] &= 0b11111101;
}

// high light control
if (MXC_GPIO_InGet(highLighterSwitch.port, highLighterSwitch.mask)) {
esp32_uart_data[0] |= 0b1;
} else {
esp32_uart_data[0] &= 0b11111110;
}

// volumn bottun
if (MXC_GPIO_InGet(volumeButton.port, volumeButton.mask)) {
if (volumeButtonState == 1) {
if (timerCount - volumeButtonLastUp <7) {
// shart press
esp32_uart_data[0] |= 0b1000;
} else {
// long press
esp32_uart_data[0] |= 0b100;
}
}
volumeButtonState = 0;
} else {
LED_Toggle(LED2);
if (volumeButtonState == 0) {
volumeButtonLastUp = timerCount;
volumeButtonState = 1;
}
}

// button
if (MXC_GPIO_InGet(leftRightPageButton.port, leftRightPageButton.mask)) {
if (leftRightButtonState == 1) {
if (timerCount - leftRightButtonLastUp < 7) {
// shart press
esp32_uart_data[0] |= 0b100000;
} else {
// long press
esp32_uart_data[0] |= 0b10000;
}
}
leftRightButtonState = 0;
} else {
LED_Toggle(LED2);
if (leftRightButtonState == 0) {
leftRightButtonLastUp = timerCount;
leftRightButtonState = 1;
}
}

if (rx_data[8] > 0x70 && rx_data[8] < 0x90 &&
timerCount - modeSwitch > 200) {
flyMode = 1 - flyMode;
modeSwitch = timerCount;
}
if (flyMode) {
esp32_uart_data[0] |= 0b10000000;
esp32_uart_data[2] = rx_data[12];
esp32_uart_data[1] = rx_data[8];
} else {
if (number_to_send != 0) {
esp32_uart_data[3] = max_predict_class;
number_to_send = 0;
}
}

esp32_write_req.txData = esp32_uart_data;
esp32_write_req.txLen = 4;
esp32_write_req.rxLen = 0;
esp32_write_req.callback = NULL;
MXC_UART_Transaction(&esp32_write_req);
turnPageFlag = 0;
esp32_uart_data[3] = 8;
}

MAX78000和esp32的通讯协议约定如下:

Fnbs3pnTfd_ftq4un0jn_fSOKLxl

此外,为了方便移动激光光点和聚光灯,我还实现了飞鼠功能,通过上下左右摆动遥控笔即可控制光标的移动。不过,这个功能显然和手势控制的功能相冲突,两个功能可以通过快速向下摆动一下遥控笔来进行切换。

手势分类的任务需要载入陀螺仪的数据,当遥控笔关闭飞鼠模式时,如果遥控笔收到略大幅度的摆动,则认为用户已经开始输入手势,此时开始采集0.9秒共计36次数据,加载入神经网络加速器,开始进行分类。

void ContinuousTimerHandler() {
if (cnn_buf == 1)
return;
LED_On(LED2);

MXC_TMR_ClearFlags(CONT_TIMER);
icm20602_update();
uint8_t gx = nomilization(rx_data[8], rx_data[9], 0, 5);
uint8_t gy = nomilization(rx_data[10], rx_data[11], 0, 5);
uint8_t gz = nomilization(rx_data[12], rx_data[13], 0, 5);
cnn_input[fill_idx] = 0;
cnn_input[fill_idx] = ((gz << 16) | (gy << 8) | gx) ^ 0x00808080;

fill_idx++;
if (fill_idx == 36) {
NVIC_DisableIRQ(TMR1_IRQn);
fill_idx = 0;
memcpy32((uint32_t*)0x50400000, cnn_input, 144);
cnn_buf = 1;
}
}

目前,我训练了9种手势,分别是:逆时针画圈,前后摆手,3,W,顺时针画圆,&,类似8的动作,7,空闲状态(乱动),其中最后一个用来防止误触发一些指令。整个MAX78000工作的流程图为:

Fq04iTh0Z45VxG_D5nk7Y_cPmIj4

遇到的问题

在目前的使用过程中,会出现误触的现象,即单片机将随意移动的信号误识别为某类手势,然后给电脑发送控制信号,同时也有将某类手势识别为空闲状态(乱动)的信号,从而不能发出想要的命令。未来考虑中的解决方案有:

  1. 更换载入cnn模块数据的条件。目前当陀螺仪数据有较大幅度波动时认为使用者在做手势,后续考虑加入一个触摸按键,只有当使用者触摸特定位置时才会向cnn模块中载入数据进行分类、
  2. 优化网络,增加空闲状态数据集容量。当网络能够更好地区分无用手势和设定手势时,才能根本上防止误触发的现象。

同时,因为开发时间有限,训练集中的数字种类还很少,因此只能实现了几种数字的识别。未来如果能够收集更多种类的数字、字母甚至是汉字,遥控笔能够识别的动作将更多,功能也将更为强大,充分发挥出MAX78000芯片的潜能。

总结与感受

本次项目中,我第一次实践了从模型采集到模型部署的整个流程。美信公司为开发过程提供了完整的工程示例和开发说明,不仅为我的项目开发带来了极大的便利,更让我学到了很多关于神经网络的知识。

整个项目给我印象最为深刻的点是MAX78000芯片运行网络推理的速度非常之快,可以达到毫秒级推理。我是控制专业的学生,当前控制系统难于直接嵌入神经网络的一个原因正是网络推理速度无法满足实时性的要求。因此,这块板卡让我看到了未来神经网络进入实时控制系统的希望。未来,我希望能够基于更多的人工智能微控制器,开展更多关于神经网络应用方向的探索。

最后,感谢硬禾学堂举办此次比赛,感谢交流群里工作人员的付出,感谢各位大佬的答疑解惑,希望未来还能有机会参与硬禾学堂的更多活动。

在附件中包括包含:01 MAX78000收集数据的程序 02 制作数据集的程序以及收集的数据 03 训练相关文件 04 量化、转化成c语言工程相关文件 05 运行在翻页笔上的MAX78000程序 06 运行在翻页笔上的ESP32程序 07 运行在电脑上的上位机程序 以及使用KiCad软件绘制的项目符号库、封装库、原理图及pcb图。希望能给想要复刻的小伙伴带来一些帮助!

附件下载
project_resources.zip
包含:01 MAX78000收集数据的程序 02 制作数据集的程序,内含收集的数据 03 训练相关文件 04 量化、转化成c语言工程相关文件 05 运行在翻页笔上的MAX78000程序 06 运行在翻页笔上的ESP32程序 07 运行在电脑上的上位机程序
pcb工程.zip
使用KiCad软件绘制的项目符号库、封装库、原理图及pcb图
团队介绍
许明辉 哈尔滨工业大学本科生
评论
0 / 100
查看更多
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号