在嵌入式实训课程的要求下,我们需要完成一个和嵌入式硬件搭边的作品设计。鉴于我对stm32不算熟悉,但手里有一块树莓派和一只6自由度机械臂,而且在一个无聊的深夜,我机缘巧合之下用牙签和胶带粘合出了一只轮子,并成功绑定在舵机转轴上。因此我决定,组装一个小车。
早在几个月之前开发web网站时,我就在考虑做语音指令控制的功能。然而当时觉得过于高级,对我技术可行性不高,只是一个妄想,故一直未有尝试。但最近一番折腾之下略有了解,我决定把语音控制功能加到小车上去,变成“智能小车”。
每每看见别人的光彩夺目,便为自己的大学旅途深感羞愧,仿佛光阴虚过,学问毫无,碌碌于一次次考试的应付。然而,理论学习脱离了综合性的实践,它就找不着自己的位置,人就没有自己的方向。
实现一个支持客户端按键、语音输入两种控制方式的智能小车。并在制作过程中,体会这些零散的课程与技术,是如何支撑起一个综合性的系统,它们各自作为怎样的角色,相互之间又如何地协作。
在后文相应部分也有一些说明,这里做下汇总。
本作品以树莓派和总线舵机为硬件基础,通过手工DIV组装,最终实现的是一个网络远程控制的智能小车。提供了一个手机客户端的操作界面,可以直接控制小车进行前进、后退、转弯等等操作。同时提供了语音控制,让小车完成用户要求的操作。当然,这些操作需要是被预定义好的。
小车主体的制作材料如下:树莓派3B,总线舵机两个,牙签、胶带、橡皮筋若干,小木块两个,可乐瓶,水果网套。
除了轮子有用到胶带粘合。其它部分都只是通过橡皮筋绑定,未来拆卸也会很方便。通过恰当的绑定方式,它具有“哈尔的移动城堡”一样,稍显混乱却具有弹性恢复力的稳定结构。
一共是四层结构。
使用牙签骨架,圈上柔软的纸巾胎面。对于轮子的圆度问题,加上了裁剪的塑料瓶。对于抓地力问题,加上了泡沫圈。使用胶带和橡皮筋多层绑定。
每个舵机是单轴的,小车使用两个舵机横向绑定。
使用单纯的橡皮筋力学绑定,缝隙夹住纸块以填充。整体结构具有弹性稳定性和恢复力。
制作的轮子使用胶带和橡皮筋的双层绑定,固定在舵机的转轴上。
两轮装置必然产生平衡问题。因此在前后加入支撑结构,将牙签用橡皮筋以特殊方式绑在小木块上,受到地面向上支持力时,具有回弹能力。
成品如下。
电子硬件包括:树莓派3B,树莓派扩展板,电源,总线舵机两个,相应总线两根。通过总线将控制板与舵机连上即可。
树莓派介绍
树莓派介绍RaspberryPi,中文名为“树莓派”,简写为RPi,或RasPi/RPI,是一款只有信用卡大小的计算机。它是一款基于ARM的微型电脑主板,可连接键盘、鼠标和网线,同时拥有视频模拟信号的电视输出接口和HDMI高清视频输出接口。
扩展板介绍
此款树莓派扩展板是由杭州众灵科技有限公司研发的一款集PWM舵机控制(标准舵机和9g小舵机控制)、本店总线设备控制(总线舵机,总线马达等)、传感器连接,手柄和红外控制的控制器。接口丰富,功能强大。
总线舵机介绍
传统PWM舵机是通过单片机发送PWM信号控制舵机转动,总线舵机是舵机内部带有一个主控芯片,内部已完成PWM信号控制。只需要通过串口发送字符串指令即可控制舵机。舵机内部的芯片也可以检测舵机的工作状态,所以通过串口也可以读取舵机的角度,切换工作状态。
舵机参数
舵机提供的串口指令(节选)如下。
连接在总线上面的每个舵机可以分别独立接收和执行串口指令,独立运动。每个舵机有自己的id,通过指定id,就可以单独操作总线上的某个舵机。
我这里的舵机设置的id分别是左轮子3号,右轮子1号。当然,这个是可以在0~254自由设置的。
此外需要设置舵机的工作模式,这款总线舵机一共有8种工作模式。这里我采用的是模式7(马达模式360度定圈顺时针模式)和模式8(逆时针)。注意硬件连接时两个舵机的朝向是相反的,因此在小车整体向前运动时,左轮舵机采用模式7,则同时右轮舵机需要采用模式8。
classCar:'''提供小车基本动作的封装。'''leftId=3rightId=1leftForwardMod=7#左轮(id=3):7前8后|右轮(id=2):8前7后......舵机控制指令封装。
以下代码将串口种使用的字符串指令中,我们需要用到的部分,封装成了函数。这样我们就方便地通过参数进行调用。轮子需要能够向前、向后运动,因此每次运动时需要先发送工作模式设置指令(控制顺时针、逆时针),然后再发送旋转指令。
classCmds:'''舵机控制指令封装'''@staticmethoddefwheel_mod_cmd(id:int=255,mod:int=1):'''左轮(id=3):7前8后|右轮(id=2):8前7后'''returnf'#{id:03d}PMOD{mod}!'@staticmethoddefwheel_move_cmd(id:int=255,pwm:int=1700,time:int=1):returnf'#{id:03d}P{pwm:04d}T{time:04d}!'@staticmethoddefstop(id:int=255):returnf'#{id:03d}PDST!'小车动作封装。
我接下来将舵机作为小车的轮子,进行更抽象的封装。下面包含了move和stop两个动作,进行传入参数的解析,并向串口发送相应的指令。
classCar:'''提供小车基本动作的封装。''' ...@staticmethoddefmod_reverse(mod:int):return8ifmod==7else7@staticmethoddefmove(forward=True,left=True,pwm:int=1700,t:int=1,excute=True):'''一个轮子的一次移动@excute:False则不执行,仅仅返回命令字符串'''whell_id=Car.leftIdifleftelseCar.rightIdmod_id=Car.leftForwardModifnotforward:mod_id+=1ifnotleft:mod_id=Car.mod_reverse(mod_id)cmd1=Cmds.wheel_mod_cmd(id=whell_id,mod=mod_id)cmd2=Cmds.wheel_move_cmd(id=whell_id,pwm=pwm,time=t)ifexcute:#否则仅仅返回命令myUart.uart_send_str(cmd1)time.sleep(0.4)#否则可能不转myUart.uart_send_str(cmd2)returnf'{cmd1}{cmd2}'@staticmethoddefstop(id:int=255):cmd=Cmds.stop(id=id)myUart.uart_send_str(cmd)returncmd...4.2.3双轮控制模式单轮分别控制,理论上可以让小车做出非常灵活的动作,但是这对操作者提出了一定的要求——是有难度的。你有可能半天总在原地打转。
因此我决定进一步封装双轮同时控制。我们可以直接在前面单轮控制的基础上封装,并不复杂。但是,如果简单地顺序调用两次前面的move指令,在实际操作时两个轮子动作之间会产生明显的延迟。
总线舵机支持动作组操作,将可以同时执行的多条指令连接起来,加上“{}”,就可以叠加同时控制多个舵机。比如:{G0000#000P1602T1000!#001P2500T0000!#002P1500T1000!}。
因此在前面的move函数中设置了excute参数,可以在调用时并不实际执行而仅返回相应的指令字符串。于是我们可以在move_double函数中进行进一步的解析和组合,达到双轮同时运动的效果。
classCar:'''提供小车基本动作的封装。'''......@staticmethoddefmove_double(forward=True,pwml:int=1700,pwmr:int=1700,t:int=1,turn_left:bool=None):'''两只轮子一起动,使用动作组。(可以差速转弯)'''#单向运动|原地转圈ifturn_leftisNone:fl=fr=forwardelifturn_left:fl,fr=False,Trueelse:fl,fr=True,Falsecmdl=Car.move(forward=fl,left=True,pwm=pwml,t=t,excute=False).split()cmdr=Car.move(forward=fr,left=False,pwm=pwmr,t=t,excute=False).split()group_mod='{'+cmdl[0]+cmdr[0]+'}'group_move='{'+cmdl[1]+cmdr[1]+'}'myUart.uart_send_str(group_mod)time.sleep(0.4)myUart.uart_send_str(group_move)returnf'{group_mod}{group_move}'4.2.4动作序列模式很自然地,将一些预定义动作的连续执行封装起来,我们就可以在一次调用中让小车进行丰富的动作,比如:S形走位。然而并不能简单地连续把它们调用一遍。因为舵机的指令是打断式的,即:后面的指令并不会等待前面的执行完。
classCarShow:'''小车的组合动作展示'''@staticmethoddefS_move():'''S形状走位'''groups=[{'forward':True,'pwml':1600,'pwmr':1600,'t':1},{'forward':True,'pwml':2000,'pwmr':2000,'t':1,'turn_left':False},{'forward':True,'pwml':1800,'pwmr':2500,'t':4},{'forward':True,'pwml':2500,'pwmr':1800,'t':4},{'forward':True,'pwml':2500,'pwmr':2500,'t':1,'turn_left':True},]forgingroups:Car.move_double(**g)time.sleep(g['t'])4.3无线控制4.3.1网络结构在本次实训中,我打算使用局域网无线通信,这样可以方便地将电脑、手机和树莓派三者拉到同一局域网中。而且实现简单,只需要将某一设备作为热点即可。电脑、手机、树莓派都可以作为热点,但只有将手机作为热点时,电脑和树莓派是可以访问互联网的,查找资料和调试更加方便。
(注意:如果将电脑连接手机热点,然后将树莓派连电脑热点,此时树莓派和手机并不在同一局域网。电脑、手机创建热点时,会分别创建一个独立的局域网。)
综上,将手机作为热点,树莓派和电脑连接它。
但问题是,手机通常并不能查看连接到它的设备的ip地址(仅仅显示mac地址)。我们可以使用nmap工具,进行局域网活动设备扫描。
运行命令如下,图中192.168.43.1是作为热点的手机;在电脑命令行运行ipconfig可以获取电脑ip,这里是192.168.43.166。因此,下面的192.168.43.91即为树莓派ip地址。
我选择了将树莓派作为服务器,收到请求后即可本地调用相应的动作函数,更为方便。服务器程序使用python语言,flask框架编写。服务器和客户端主要采用流行的json数据格式通信。
代码较长不全部贴出,详见附件。通常只需要对数据进行简单处理后,调用运动模块中相应类方法即可。(下面的INPI变量用于调试,在配置文件config.py中设置。因为在你的电脑上调试程序时,没法真正执行对应的动作控制指令。
app.run(debug=True,host="0.0.0.0",port=5000)4.3.3客户端设计我采用浏览器作为客户端。无论手机还是电脑,通常都会有浏览器。
这部分涉及的主要是一些web技术,如flask配套的jinja2模板引擎,jquery.js。使用javascript语言在用户操作时,向服务器发起相应的请求。作为小车的控制面板,相比按钮,图标的风格会更加适洽,因此用到了remixicon图标库。以及,使用bootstrap进行一些布局控制。
在手机端横屏界面效果如下。
下面是一段javascript代码示例,用于小车的整体前进或后退操作。
$('.run').on('click',function(){varforward=true;if($(this).hasClass('backward')){forward=false;}fetch('/car_double',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({forward:forward,pwml:left_pwm+1500,pwmr:right_pwm+1500,time:run_time})}).then(response=>response.json()).then(data=>{printc(data)})})4.4带语义理解的语音控制常见的语音控制方式,是关键词识别。然而,如果我们身边可以语音控制的设备躲起来,或者具有丰富的操作和指令。那么,记住繁杂的命令词会是我们沉重的负担。
为此本作中采用了两级识别结构,1)语音识别,从音频到说话内容的字符串;2)语义匹配,从字符串到小车能执行的命令词。
下面是语音识别中,整体的数据流程。更多的细节在后文介绍。
本作采用浏览器客户端,现在许多浏览器已经提供了本地的识别接口,通过js调用即可。运行在浏览器本地。
将该模型部署在电脑本地,可以由客户端发起附带音频文件的请求,获取识别结果。
但由于我电脑算力受限,且配置gpu时遇到了障碍,只能运行较小的模型版本(base,141MB)。识别速度较稳定,在2s左右,但准确率相对低一些。
基于上述情况,我在客户端中实现了两种识别方式的并发。采用最快的那一个识别结果。(如果有一个又快有准的模型,则不必这样。)
如图,使用全局变量RACEID进行识别任务的同步控制。当一种识别方式返回结果时,将RACEID加一,那么另一种识别方式迟到的结果将不再被采用,以避免小车执行重复动作。
前面的语音输入已经可以通过识别命令词,实现对小车的控制。是的,在简单的实验作品中,这已经足够了。但随着智能技术的持续渗透,我们身边会有越来越多的智能设备。如果对每个设备,以及设备的每个功能,我们都需要记住它具体的命令词——这无疑是一份巨大的负担。
而当今的大模型,为模糊指令的识别提供了可能。需要小车前进时,你可以说“往前走”,“向前跑”,而不必再纠结具体的指令词。
我使用prompt模板如下,可以传入语音识别阶段得到用户说话内容content,和小车预定义的支持指令集results两个参数。返回与content匹配的相应指令。
如果可以将文本到舵机控制指令的环节直接打通,通过用户要求自动生成相应的舵机指令序列,它将会像一个真正的智能小车。然而基于对其技术难度的评估,在这次作品中并未实现。
因为涉及实际硬件的控制,一键将所有用例跑一遍的测试方式不很方便。下面采用交互的方式,运行在树莓派的终端*(我是通过vscode插件远程连接了)*,控制轮子的实际运动。终端打印了舵机执行的指令。这里不便展现出实际的运动效果。
控制正常。
下面是在电脑浏览器,测试语音控制模块。首先启动程序。
#在树莓派的项目目录python3app_pi.py#在电脑stt模型目录python3start.py可以看到每次语音输入后,控制台有4行显示。前3行是识别的内容,第4行是匹配到的指令。下图中“前进。”、“广(往)前跑。”、“快跑!”都成功匹配到了指令“前进”。
这次测试中都是采纳了stt模型的识别结果,WebSpeech确实会速度不怎么稳定。
这次实训一共3周,然而前2周因为时而要准备期末考试,为避免挂科,实在没法安心搞实训。但最后一周时成功集中精神,也让人久违地体会到了动手制作的乐趣。
大学的最后一次实训课了,日后回头一看,应该也不算空空如也。