×

带有PS3遥控器的ESP32上的Arduboy TV

消耗积分:2 | 格式:zip | 大小:1.86 MB | 2022-11-10

廉鼎琮

分享资料个

描述

目标

我着手做一个简单而省力的 Arduboy 游戏机。设计目标是

  • 便宜又简单
  • 电视输出
  • 不错的控件
  • 各种有趣的游戏

我现在对使用 ESP32 和 Arduboy 代码感到很自在,并认为这将是一个有趣的项目,可以推动自己学习一些新事物。

将目标一一确定

便宜又简单

KISS - 保持简单愚蠢。

几乎唯一要购买的大产品就是这个

TTGO T-Display ESP32,这个板实际上比我买它时很多没有显示器的板便宜,但几乎任何 ESP32 板都可以。

我一直在不断地构建控件,这一次我希望我可以利用 ESP32 中的一些额外硬件来实现这一目的。我选择了 PS3 控制器,任何无线通用 PS3 控制器都应该是完美的。

你还需要一些 RCA 插孔,我选择了母头,这样我就可以在电视和我的电脑之间快速轻松地来回运行,还需要一根公对公延长线,以便将背面也插入电视。 .. 最终我确实得到了一个 RCA 到 HDMI 转换器并将它连接到我的第二台显示器,我对设备非常满意,并且在闪烁和测试之间的周转时间更少。

电视输出

电视输出是我真正想了解更多的第一个也是最重要的事情。

我启动并运行了它,看到我从裸 ESP32 发送到电视的一些实际视频信号真的很酷。

未修改的代码存在一些问题,它实际上是为带有 PSRAM 的 ESP32 设计的,它将通常可用的 520kb 增加了另外 8mb!

问题是做颜色会占用大量空间,并且对于库和它的工作方式,它有非常严格的时间要求,所以你实际上使用了一个双缓冲区,这意味着当你绘制一个缓冲区(屏幕)时,您致力于更新另一个缓冲区。在完成对一个缓冲区的渲染时,将其换出,然后显示该缓冲区,然后开始绘制到备用缓冲区。这允许您在游戏处理下一帧时将一个缓冲区连续渲染到电视。

通过从彩色转换为黑白,我绕过了我所面临的大小限制,这使缓冲区从一个字节缩小了 8 倍......所以我们又回到了游戏中:)

生成电视信号的另一个有趣问题是,它需要满足非常严格的时序约束,而 ESP32 有 2 个内核,我通过将其中一个内核专门用于电视信号来解决这个问题。

我本可以走另一条路,得到一个带有 PSRAM 的 ESP32,大多数相机模块都有它们,因为它们经常咀嚼超过 520kb,尽管回到我最初的廉价和简单的目标,即没有额外内存的“裸”ESP32更容易掌握。

poYBAGNsUiyAP5S_AANg51OOrHg324.png
不太理想的设置
 
poYBAGNsUjqALbC0AASmdCqP7eo959.png
后来看起来好一点
 

不错的控件

我最初的选择是使用蓝牙,它实现了我的一些目标,它很好的控制,硬件内置在 ESP32 中(虽然需要一些软件黑客),而且因为我们正在做电视输出,所以它'当我们玩的时候,会给我们一些范围来打沙发。

我碰巧有一个废弃的 PS3 和 2 个控制器,所以在弄乱了一些原始的蓝牙外围设备之后,我决定看看使用这些。

PS3 控制器几乎是一个令人着迷的案例,从技术上讲,它使用蓝牙,但通过序列号有一个自定义配对过程。

我将我的 PS3 控制器连接到我的 PC 并使用 SixAxisPair 工具将其序列号设置为 01:02:03:04:05:06(我知道非常原始!)

我从这里得到了这个网站,这似乎是合法的....

pYYBAGNsUjyATyltAAAay6P7d00002.png
SixAxisTool 设置控制器新控制台
 

使用 Wifi 和蓝牙启动 ESP32 项目时要注意的一件事是库非常庞大!你炸掉大约 1Mb 的 ROM 只是为了获得 BT 和 Wifi 堆栈,我相信有更轻的可用,但不是我可以使用 Arduino。

完成此操作后,我的输入和输出开始工作了!

现在我的游戏在哪里!

各种各样的游戏

到目前为止,Arduboy 已经给我带来了一个有趣的世界,它是我放在面包板上的第一个真正的硬件,然后焊接到原型板上并构建了我自己的 ESP32 版本,最后现在制作了一个电视输出版本。我对我在这个生态系统中和周围的乐趣感到非常满意。我什至构建了一款令我引以为豪的游戏 Game Plug ArduRacer https://community.arduboy.com/t/arduracer-a-trackmania-type-time-trial-game/8850 ,它具有平滑滚动、放大功能起跑线和 10 个关卡为特色!

poYBAGNsUj6ALBm8AAALWk7BQEw205.png
介绍屏幕
 
poYBAGNsUkGARN3JAAAG_2rVzT0119.png
游戏截图
 

一个早期的控制台原型(错误的芯片,但我保证我有一个非常相似的带有按钮的 Arduino Pro Micro)。

pYYBAGNsUkuAMh6EAAQrcDszC_0363.png
我用 Arduino Pro Micro 和按钮拍了一张非常相似的照片,真正的共同点是拍摄时间
 

启动和运行它的实际过程非常有趣。

最初我尝试使用 Blinky 先生的图书馆

https://github.com/MrBlinky/Arduboy-homemade-package

这很棒,并且在不同的屏幕上为许多不同类型的 Arduboy 和 Arduino 的不同引脚做了诀窍。经过一堆错误的开始后,我找到了 ESPBoy https://www.espboy.com/的 ESP8266 转换

一旦我抓住了这个,我做的第一件事就是让它在它所构建的硬件上启动并运行,所以我抓住了一个 ESP8266、一个兼容的屏幕和面包板,一直在研究它,直到我启动并运行它(并且然后玩了一会儿游戏)

完成此操作后,接下来的步骤是更换处理器,进行了很多更改,但主要是删除和更改库。我将列出一些亮点

  • PROGMEM 不是 ESP32 上的东西,删除引用
  • avr/pgmspace.h 已移至 ESP32 上的 pgmspace.h
  • EEPROM不是很好,必须工作
  • 音调不起作用,不得不重新编写代码,它仍然有点hacky
  • 更改控件以使用 PS3 控制器
  • 将输出代码大幅更改为线程化并输出到电视
  • 因为需要线程,我不得不修改每个游戏的代码!

PROGMEM - 这有点简单,你需要做的就是#define PROGMEM 没有任何意义,等 viola,完成

poYBAGNsUk6AefIeAAAGOlU02HM714.png
假装不存在
 

avr/pgmspace.h - 我有点懒,在 Visual Studio Code 中对整个文件夹进行了完整的搜索和替换,这很快就解决了这个问题

Tones - 我写了一些非常适用于诅咒地下墓穴的 hacky 代码,我喜欢它!不幸的是,其他一些游戏也在为此苦苦挣扎……我应该稍后再研究一下

EEPROM - 我在游戏过程中运行良好,可以存储它,但实际上它需要一个完整的实现,我希望它知道哪个游戏正在运行并将 EEPROM 文件存储在 SPIFFS 分区上,这更像文件系统并允许文件,可能是与游戏名称匹配的格式,并将特定游戏 EEPROM 存储在 SPIFFS 上的文件中,因此它永远不会被覆盖。待定

PS3 控制器连接- PS3 库实际上有一个经常运行的通知检查,我只是将它的值存储在一些全局变量中,然后我可以在 Arduboy2Core::buttonsState 过程中使用它来设置值。

电视输出- Arduboy 有它自己的帧缓冲区,理想情况下我会使用它来避免代码重复和复制,但它的水平条纹格式有点奇怪。缓冲区模仿您一次写入几个水平像素的屏幕的方式,这使得我的电视输出代码很难使用,所以我有一个过程,我把它放入 Arduboy 输出代码中,而不是输出到屏幕,它准备缓冲区并使用锁处理缓冲区的交换。有一个线程一直在运行,只是使用当前设置的输出缓冲区进行 TV 输出,当它们准备好时,它将获取更改。

修改每个游戏的代码- 这实际上非常有趣,因为它意味着源代码兼容我想找到一种修改每个 Arduboy 游戏的方法,我研究了很多解决方案。但让我先设置前提。

我需要为线程添加初始化代码,电视输出代码对于它生成的 NTSC 信号非常敏感,所以我希望它本身有一个完整的核心。默认情况下,Wifi 和蓝牙代码也在默认使用的一个核心上运行。这对电视输出来说是有问题的,所以我希望它在不同的核心上。

第一次尝试- 手动修改游戏这是一件很痛苦的事情,并且不能对所有游戏进行维护或扩展。

第二次尝试- 用我自己的替换主 ino 文件,同时将 ino 重命名为 mytvgame.cpp 或类似的,我能够通过一两个游戏摆脱这个问题,但由于一些原因而出现了一些问题。Ino 文件就像一个全局命名空间,可以按照您喜欢的任何顺序定义函数(就像您在导入的头文件中指定它们一样),但是当我以编程方式生成头文件时,我开始遇到更多错误。这是一个单一的源文件,一旦它们被复制到 CPP 文件中,它实际上就能够从我的 Arduino INO 文件https://fossil-scm.org/home/doc/trunk/src/makeheaders.html制作标题。

第三次尝试- 实际上我需要的只是将 setup() 和 loop() 重命名为其他名称,然后创建自己的启动和循环方法来调用它们!

我最近在 Python 上玩得很开心,并决定在其中编写脚本。首先,浏览所有 GAMES 目录,寻找与文件夹名称匹配的 a.ino 文件(这是我从未理解的 Arduino IDE 限制,但谢谢!)

找到文件后,请进行备份(始终进行备份!)。然后复制到我愿意修改的文件,我运行了一些更改

pYYBAGNsUlCAAlLCAAF0B_Ghe2M742.png
 

最后,一旦完成所有磨机更改,它会使用我的新设置和循环创建一个新的 ino 文件,一旦完成,它就会调用重命名的游戏设置和循环方法

poYBAGNsUlKAEZtlAAA6EA8jmhE072.png
 

我的设置运行游戏设置代码并为游戏逻辑循环设置线程。

我几乎可以在这里做任何我想做的事情,目前它只是尝试进行无线更新,但以后可能会添加一个 flash 菜单,如果我们有 SD 卡,你可以在设备上玩多个游戏

你会看到循环只是一种延迟方法。它不需要做任何 gameLogicLoop 线程负责运行原始循环代码的事情。

其中一个不错的功能是实际上使其可多次运行(有点幂等),因此每次运行它时,它都会修改原始文件而不是已经更改的文件,这对快速迭代有很大帮助。

从这里到哪里

如果您有兴趣四处寻找,可以查看我的所有代码

https://github.com/tonym128/ESP32_Arduboy

这个项目目前都在 tvout 分支下

我已经在 BSides Cape Town 2019 Badge 上运行了它,我在这里详细介绍了徽章项目本身的软件。

ESP32 电子纸显示器

现在有了带有 PS3 控制器的电视输出,我认为这是最简单、最快捷的方式来开始这一点并获得一些乐趣。

我希你在我的旅程中发现了一些有趣的地方,如果你想聊聊这方面的任何事情,请给我留言,我很乐意参与。

在那之前,我会玩一些该死的地下墓穴。

 

 


声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.lene-v.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'带有PS3遥控器的ESP32上的Arduboy TV',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"//www.lene-v.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);