描述
<div class="document">
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">* 1、项目功能介绍</h3>
<hr class="horizontal-splitline normal-bold-2">
<p>家属学校做实验需要一个CO2检测仪,本来准备网上买个,但是网上的检测仪不满足培养皿的尺寸需求(要尽可能的小、扁),并且要支持历史数据导出。闲来无事,就做一个吧。</p>
<p> </p>
<h3 id="2实现功能">主要功能</h3>
<h4 id="21-空气质量监测">1 空气质量监测</h4>
<p>主要是eCO2(当量CO2,不是真实CO2)和TVOC(总挥发性有机化合物,主要是颗粒物、H2等)。真实CO2需要买一些工业级的传感器,消费级的模块一般测的都是eCO2,eCO2一般也是根据TVOC等来计算的。</p>
<h4 id="22-温湿度监测">2 温湿度监测</h4>
<p>这个可有可无,但是温度可以用来补偿eCO2的计算过程。</p>
<h4 id="23-屏幕显示">3 屏幕显示</h4>
<p>要能够切换显示内容,比如查看数据、系统运行信息显示、图标显示等。</p>
<h4 id="24-数据存储及导出">4 数据存储及导出</h4>
<p>这个是比较核心的,要实现数据持久化,考虑存储在flash里。<br>数据导出考虑wifi ap模式,访问网站进行文件下载。</p>
<h4>5 时间同步</h4>
<p>数据的时间属性是肯定要的,否则只要断电了,等一会下载下来的数据就不知道是啥时候的了。。这就需要RTC模块进行时间同步,一直连wifi同步也不太现实,因为实验室可能没有网,而且一直连着也增加功耗。</p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*2、项目属性</h3>
<hr class="horizontal-splitline normal-bold-2">
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">本项目未曾参加其他比赛、课题答辩等,也是首次公开、原创。感兴趣的可以一起交流学习~</p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </h3>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">* 3、开源协议</h3>
<hr class="horizontal-splitline normal-bold-2">
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">本项目完全开源,遵从GPL3.0开源协议。</p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*4、硬件部分</h3>
<hr class="horizontal-splitline normal-bold-2">
<p>主控使用ESP32S3N16R8,16M flash,8M PSRAM。</p>
<p> </p>
<h3 id="31-气体传感器的选择sgp30-vs-sgp40">4.1 气体传感器的选择:SGP30 VS SGP40</h3>
<p>网上关于SGP40的资料不多,能找到的就是Arduino里的Adafruit_SGP40库。买了之后才发现SGP40好像只能测VOC,不能测CO2。看了<a href="https://www.waveshare.net/wiki/SGP40_VOC_Sensor" target="_blank">微雪的介绍</a>以及deepseek回答,它一般是用来测空气质量的,即只有VOC输出,如果有不对请指出。<br>SGP30可以直接用内置算法获取eCO2。但是SGP40没有内置算法直接读出eCO2,如果真的要这个数据只能用经验公式从VOC Index(VOC指数值)算出eCO2,DeepSeek给出的经验公式如下:</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-comment">// VOC→eCO₂转换(含温湿度动态修正)</span>
<span class="hljs-function"><span class="hljs-type">float</span> <span class="hljs-title">transformVocToEco2</span><span class="hljs-params">(<span class="hljs-type">int32_t</span> vocIndex, <span class="hljs-type">float</span> temp, <span class="hljs-type">float</span> rh)</span> </span>{
<span class="hljs-comment">/* 动态参数(基于长期逆向工程):
* k1: 基础转换系数
* k2: 温度修正权重
* k3: 湿度非线性因子 */</span>
<span class="hljs-type">float</span> k1 = <span class="hljs-number">0.75</span>;
<span class="hljs-type">float</span> k2 = <span class="hljs-number">0.005</span> * (<span class="hljs-number">25.0</span> - temp); <span class="hljs-comment">// 温度补偿</span>
<span class="hljs-type">float</span> k3 = <span class="hljs-number">1.0</span> + (rh - <span class="hljs-number">40.0</span>) * <span class="hljs-number">0.0025</span>; <span class="hljs-comment">// 30-70%RH区间优化</span>
<span class="hljs-comment">// 核心转换公式(Sensirion未公开)只是粗略的</span>
<span class="hljs-type">float</span> baseEco2 = <span class="hljs-number">410.0</span> + k1 * (vocIndex - <span class="hljs-number">120.0</span>); <span class="hljs-comment">// 注意基线120非100!</span>
<span class="hljs-keyword">return</span> baseEco2 * k3 + k2;<br>}</code></pre>
<p>这样相比SGP30多了算法的不确定性,索性直接买SGP30吧。。。真的要上精度的后面考虑买工业级。</p>
<p> </p>
<h3 id="32-温湿度传感器">4.2 温湿度传感器</h3>
<p>DeepSeek给出的结论是,AHT20模块性价比比较高。建议直接买AHT20+BMP280的模块,比分开买便宜,还可以顺便测下大气压。</p>
<p> </p>
<h3 id="33-显示屏">4.3 显示屏</h3>
<p>0.96寸OLED,配合U8g2库使用应该简单的,但是要注意OLED几个孔的位置尺寸,每家可能不一样的,和画的PCB预留位置要匹配,这个可能会影响到后期安装固定。</p>
<p> </p>
<h3 id="34-rtc">4.4 RTC</h3>
<p>DS1302不用多说,但是板子尺寸有限,要使用SOP贴片。同时纽扣电池(CR2032)使用外挂而不是板载的电池底座,外挂在PCB上部空间有效节约了PCB尺寸。</p>
<p> </p>
<h3 id="35-pcb设计">4.5 PCB设计</h3>
<p>手头有一块3.7v2000mah(603048)的航模电池,就以这个主供电电源作为最小尺寸进行PCB设计。<br><img src="//image.lceda.cn/pullimage/ed9WUd34a5ItwpCV07cFhljYdYLAUrW9efvpggLO.png"><br>尺寸60*35mm。<br>因为空间狭小,且反面不放置元器件(主要是方便热焊台焊接),这里有几个注意点:</p>
<ol>
<li>ESP32芯片底下直接暴力开大孔散热(因为后续发现wifi ap会发热)</li>
<li>主电池接口要往里一点,这样接插件不会超出PCB板尺寸</li>
<li>DS1302的晶振要远离ESP32天线和电源,我放在了板子边缘不受干扰。并且布线尽量对称等长,周围包地。</li>
<li>OLED放置在ESP32上方,要注意下面2个定位孔的位置。我是打算这里焊接铜柱的,所以周边元件要注意不要摆放太近。画了2个焊盘以焊接牢固。</li>
</ol>
<p>整个PCB电路是比较简单的,都是常见的电路,网上资料很多。</p>
<p><img src="//image.lceda.cn/pullimage/PMYFQcedkU5sInaJMzCtkuSIH3NqHT6ItKZIDyZE.png"></p>
<p>几个重点电路设计:</p>
<p> </p>
<h4 id="351-ldo">4.5.1 LDO</h4>
<p>me6211c33的压降:<br><img src="//image.lceda.cn/pullimage/34GEHSyDnFcovVggSj6oXWGWQyWohHVGg3RrWNys.png"><br>ams1117-3.3的压降:<br><img src="//image.lceda.cn/pullimage/FUImKnWFPdejCa8Eshm4h3d2suyd8u5mOu4hLl4n.png"><br>我使用的是3.7v的锂电池,考虑到3.3v的系统供电,我使用me6211。ams1117的压降都在1v以上,me6211可以控制在0.小几伏。</p>
<p> </p>
<h4 id="352-电源切换">4.5.2 电源切换</h4>
<p>我以前的设计很简单:<br><img src="//image.lceda.cn/pullimage/3E7Q540UGo17e12EbvXrpfWuqoia6mDUVsbKyq3n.png"><br>通过一个单刀双掷开关控制V_IN的来源(另一边V_USB默认是接V_BAT充电的),但是需要手动切换比较麻烦。所以想设计一个自动切换电路,要求插上USB时,无论有没有锂电池都自动使用USB电源(也给锂电池充电),没有USB通过开关控制锂电池的供电。<br><img src="//image.lceda.cn/pullimage/u5746HksV30R1fkpCYsyuzPvloBn4hkN4CLhXZQH.png" width="549" height="318"><br>这里我放了2个0欧的电阻,防止这个切换电路不生效就不使用它了。<br>电路参考:<br><a href="https://www.bilibili.com/video/BV1QDjBzDEiW/" target="_blank">https://www.bilibili.com/video/BV1QDjBzDEiW/</a><br><a href="https://www.bilibili.com/video/BV1Qb421B7c6/" target="_blank">https://www.bilibili.com/video/BV1Qb421B7c6/</a><br><a href="https://www.bilibili.com/video/BV1e9MizhEJY/" target="_blank">https://www.bilibili.com/video/BV1e9MizhEJY/</a></p>
<p>通过一个P沟道MOSFET充当开关,二极管防止V_IN倒灌,但是二极管会有压降,考虑到对后面LDO电路的影响,尽可能压降小一点。所以选用了B5815W。<br><img src="//image.lceda.cn/pullimage/8QWlVDj5YKyH421t9PI0WYMlqgATzb8tmalwgsHy.png"><br>压降只有0.5v左右,USB电压一般在4.7v以上,也满足me6211的最小压差,问题不大。</p>
<p> </p>
<h3 id="36-外壳">4.6 外壳</h3>
<p>外壳尺寸紧贴PCB尺寸,分为上下2部分。设计原则就是尽可能的薄。<br>下壳用m2热熔螺母固定,主要放置锂电池,市面小的3.7v锂电池厚度最小也是8mm左右,可压缩的空间不多。<br>上壳后端的主要影响因素就是OLED和SGP30传感器的高度,目前设计的高度为6mm,也是考虑了板子散热。<br>整个高度为22mm。另外还设计了一个小按钮部件。</p>
<p> </p>
<p><img src="//image.lceda.cn/pullimage/7zz2B2FKwuBTcdy8N6UnfJdZD3y7tb1J3dNLucRH.png"></p>
<p> </p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"><img src="//image.lceda.cn/pullimage/8bXqU70fd22Nt2YphVziw6iLLNdbjBydZvQYDaUM.png"></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt pap-left-indent-1.6em" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*5、软件部分</h3>
<hr class="horizontal-splitline normal-bold-2">
<p>列出所有用到的第三方arduino库:</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs">Adafruit_SGP30
Adafruit_AHTX0
Adafruit_BMP280
U8g2lib
OneButton
WiFiManager
arduino-ds1302(只能github下载)
ESPAsyncWebServer
AsyncTCP
</code></pre>
<p> </p>
<p>先亮出<a href="https://gitee.com/CharlesPu/paqm" target="_blank">我的代码</a>。</p>
<p> </p>
<h3 id="41-u8g2库移植">5.1 U8g2库移植</h3>
<p>初始化时ssd1306和ssd1315是兼容的,都用1306的初始化函数:<br><code>U8G2_SSD1306_128X64_NONAME_F_HW_I2C</code>,这个函数底层默认用的中的Wire,即i2c1,如果想用Wire1,可以使用<code>U8G2_SSD1306_128X64_NONAME_F_2ND_HW_I2C</code>来初始化。以上的初始化函数都可以传入SCL和SDA pin来自定义管脚。</p>
<p> </p>
<h3 id="42-数据持久化">5.2 数据持久化</h3>
<p>有2个方案,都是在16M的flash里操作:</p>
<ol>
<li>nvs分区-perefrence库</li>
<li>spiffs分区-SPIFFS或LittleFS库</li>
</ol>
<p>一开始用的是perefrence库,kv形式的存储,好处是结构简单,读取某个key时直接方便,缺点是需要自己管理key,容易出现数据残留和不同步,同时nvs大小无法调整,一般在20k左右。<br>后来改为LittleFS库,优点是文件形式存储数据,操作api简单,还可以修改分区表调整大小,最大可以充满16M剩下的区域,缺点是查找某个内容时需要全部遍历文件。</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-keyword">typedef</span> <span class="hljs-keyword">struct</span>
{
<span class="hljs-type">uint16_t</span> tvoc;
<span class="hljs-type">uint16_t</span> eco2;
<span class="hljs-type">float</span> temperature;
<span class="hljs-type">float</span> humidity;
Timestamp_t timestamp;
} PersistenceData_t;
</code></pre>
<p>这是我要存储的数据结构,打印到LittleFS里一行是20个字节左右(因为是字符串),每分钟需要存储一条,30天下来大概1M不到。考虑到下载速度和现实需求,就暂定最多存储30天吧。<br>在LittleFS里采用csv格式存储,格式如下:<br><img src="//image.lceda.cn/pullimage/tPlqAZRT1AcfATNJk0UU5SXY6nMSUvd4RiU3k61l.png"><br>每天一个文件(以日期命名),每个文件中时间从老到新逐行添加。</p>
<p> </p>
<h3 id="43-wifi模式管理">5.3 wifi模式管理</h3>
<p>由于需要精确时间,RTC模块需要从云端同步时间,所以至少使用初期就要连接下wifi。<br>而后数据需要连接板子自身的热点进行网站访问下载。综上,需要在2个模式之间切换。</p>
<p> </p>
<h4 id="431-sta模式连上wifi">5.3.1 STA模式(连上wifi)</h4>
<p>直接用wifimanager库进行wifi配置,简单高效。<br>目前判断下flash里有没有采集数据,如果空则认定为第一次使用,需要配网同步时间。当然也可以记录系统启动次数,第一次启动则进行配网。<br>阅读wifimanager源码,先是ap模式供连接热点,然后访问网站配置wifi,配置成功后退出ap模式进入sta模式,并保持sta连接wifi。<br>这里我接入wifi后,等同步时间等系统配置后,就直接退出sta模式(因为不需要做其他事情了)。</p>
<p> </p>
<h4 id="432-ap模式开启热点">5.3.2 AP模式(开启热点)</h4>
<p>有2种方案:</p>
<ol>
<li><strong>wifi ap热点一直开启</strong><br>这种方案下wifi耗电可能会有点高,并且会增加板子的持续发热量,当然我也试过降低发射功率,从默认的20dBm降低到8dBm,但是效果不明显。</li>
<li><strong>通过按键开启</strong><br>双击按键才能开启,这种方式比较省电,但是会增加使用者的心智成本,并且要写好wifi启停的代码,不能造成资源泄露。</li>
</ol>
<p>考虑到目前用的是2000mAh的大容量锂电池,以上2种模式其实都可以,看使用者需求。</p>
<p> </p>
<h3 id="44-按钮功能及屏幕显示">5.4 按钮功能及屏幕显示</h3>
<p>开机动画之后判断是否需要同步网络时间,若需要则开启wifimanager等待配网同步,完成后或不需要配网则直接进入传感器初始化动画,等待sgp30前15s的初始数据正常。<br>默认显示页面为eCO2、TVOC、温度、湿度、气压5个数值。</p>
<ul>
<li>单击右侧边按键可以切换eCO2、TVOC、温度的柱状图或者点状图显示;</li>
<li>双击按钮可以进入wifi ap模式,供链接热点下载数据使用(如果wifi不是默认开启的话);</li>
<li>三击按钮可以手动进入wifimanager配网模式,从网络同步时间;</li>
<li>长按按钮可以清除所有数据及配网信息,即出厂初始化。</li>
</ul>
<p> </p>
<h3 id="45-低功耗">5.5 低功耗</h3>
<p>实际使用中发现,2000mAh的电池24小时都顶不住,尝试了以下的省电方法:</p>
<ol>
<li>wifi从常开改成按键按需启动</li>
<li>oled屏幕30s内无动作就息屏</li>
<li>去掉温湿度传感器</li>
<li>定时存储数据间隔拉长</li>
</ol>
<p>均无果。其中OLED屏幕定时息屏会多几个小时的工作时间,稍微有些改善吧。<br>没办法,只能一个个找到底哪个模块耗电量大。最后发现loop函数为空,初始化后也会耗电量大。当我发现注释掉SGP传感器初始化之后耗电量明显变长。遂想寻找SGP30的低功耗模式。<br>通过阅读SGP30的官方手册发现有个sleep mode的电流很低。<br><img src="//image.lceda.cn/pullimage/XinJIjZR2RswTxhJmFSg89cfnG0kBc1qTqtwSuMI.png"><br>这里写到:</p>
<p>The measurement mode is activated by sending an “Init_air_quality” or “Measure_raw_signal” command.</p>
<p>即调用IAQinit函数初始化之后就处于measurement mode。</p>
<p>The sleep mode is activated after power-up or after a soft reset.</p>
<p>即soft reset之后可处于sleep mode。</p>
<p>两个mode电流消耗相差近25000倍!赶紧修改了主流程逻辑,即在持久化数据的时候才IAQinit启动SGP30,之后直接soft reset进入sleep模式。但是这里遇到了一个坑,后面说。</p>
<p> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*6、遇到的坑</h3>
<hr class="horizontal-splitline normal-bold-2">
<h3 id="51-web-server阻塞">6.1 web server阻塞</h3>
<p>ap模式下web网站本来是用的<code>WebServer</code>库,其处理loop函数(<code>server.handleClient()</code>)需要放到主函数循环执行,后来发现很多情况下该函数内部出现了阻塞调用的情形,卡在这里大概有2s。定位无果。<br>后来改用async web server,使用<code>ESPAsyncWebServer</code>库,主函数不需要阻塞调用,使用api上也和同步的类似。</p>
<p> </p>
<h3 id="52-下载zip文件">6.2 下载zip文件</h3>
<p>原本使用的是perefrence库,所有数据都在一个命名空间下,下载的时候实时组装成cvs文件,只要组装成一个cvs文件即可。<br>但是改用LittleFS库之后,文件以日期为单位已经生成好,如果要下载全量的话需要打包压缩成一个zip文件下载。<br>这里如果需要控制下载量防止太慢,也可以前端提供一个日期区间选择按钮,比如最多下载7天的,时间关系我先没做这个功能。<br>所以摆在面前的一个问题就是打包成zip文件进行传输。<br>我也没有去细细研究zip格式,就用Trae写了一段代码,调试了几下竟然成了。。</p>
<p> </p>
<h3 id="53-loop中的ticker计数值">6.3 loop()中的Ticker计数值</h3>
<p>在使用<code>Ticker</code>库时,一般会写一些定时任务,一般的写法是在定时回调函数中修改变量值,在loop()中读取变量值来感知是否定时到了。<br>但是如果loop()中多次调用定时器的计数值会不一样,建议使用中间变量存下loop_cnt。</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">loop</span><span class="hljs-params">()</span>
</span>{
<span class="hljs-keyword">if</span> (ticker_1ms_cnt% <span class="hljs-number">1000</span> == <span class="hljs-number">0</span>) <span class="hljs-comment">// 1s</span>
{
<span class="hljs-comment">// ...</span>
}
<span class="hljs-comment">// 代码执行到这里的时候定时器如果运行,ticker_1ms_cnt可能发生变化,就不能在一次循环中既处理1s的定时任务也处理1min的定时任务了,也许因为ticker_1ms_cnt的变化下面的1min的这个任务永远不会运行</span>
<span class="hljs-keyword">if</span> (ticker_1ms_cnt% <span class="hljs-number">60000</span> == <span class="hljs-number">0</span>) <span class="hljs-comment">// 1min</span>
{
<span class="hljs-comment">// ...</span>
}
}
<span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">ticker_cb_1ms</span><span class="hljs-params">()</span>
</span>{
ticker_1ms_cnt++;
}
</code></pre>
<p>注意上面2个不同的地方读取ticker_1ms_cnt值可能是不一样的。可以用局部变量来规避。</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-function"><span class="hljs-type">void</span> <span class="hljs-title">loop</span><span class="hljs-params">()</span>
</span>{
<span class="hljs-type">uint32_t</span> ticker_1ms_cnt_tmp = ticker_1ms_cnt; <span class="hljs-comment">// 防止后面处理过程中ticker_1ms_cnt变化</span>
<span class="hljs-keyword">if</span> (ticker_1ms_cnt_tmp % <span class="hljs-number">1000</span> == <span class="hljs-number">0</span>) <span class="hljs-comment">// 1s</span>
{
<span class="hljs-comment">// ...</span>
}
<span class="hljs-keyword">if</span> (ticker_1ms_cnt_tmp % <span class="hljs-number">60000</span> == <span class="hljs-number">0</span>) <span class="hljs-comment">// 1min</span>
{
<span class="hljs-comment">// ...</span>
}
}
</code></pre>
<h3> </h3>
<h3 id="54-sgp30模块发热">6.4 SGP30模块发热</h3>
<p>在使用了一段时间的SGP30模块之后,发现其在发热。本来以为是板子整体在发热,带到了SGP30模块上了,但是我每个模块的功能代码挨个注释掉排查时,发现是SGP30功能模块的发热。</p>
<p>首先看硬件。</p>
<p>相关代码注释掉,只把模块插上去,发现不发热,说明电路没问题。</p>
<p>再看软件。</p>
<p>SGP30代码分初始化和循环读取数据2部分,我发现只要初始化了,模块就开始发热了。<br>本来以为可能I2C总线冲突啥的造成电路发热(OLED、SGP30、AHT20、BMP280都用的一个I2C总线,但是大家的地址不同),就挨个组合不同模块代码,发现只要SGP30代码存在就会发热,期间也换了一个新的SGP30模块也是一样的问题。<br>基本可以确定是SGP30本身模块特性的问题。</p>
<p>我使用的是Adafruit_SGP30库,初始化代码如下:</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-function">boolean <span class="hljs-title">Adafruit_SGP30::begin</span><span class="hljs-params">(TwoWire *theWire, boolean initSensor)</span> </span>{
<span class="hljs-keyword">if</span> (i2c_dev) {
<span class="hljs-keyword">delete</span> i2c_dev; <span class="hljs-comment">// remove old interface</span>
}
i2c_dev = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Adafruit_I2CDevice</span>(SGP30_I2CADDR_DEFAULT, theWire);
<span class="hljs-keyword">if</span> (!i2c_dev-><span class="hljs-built_in">begin</span>()) {
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}
<span class="hljs-type">uint8_t</span> command[<span class="hljs-number">2</span>];
command[<span class="hljs-number">0</span>] = <span class="hljs-number">0x36</span>;
command[<span class="hljs-number">1</span>] = <span class="hljs-number">0x82</span>;
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">readWordFromCommand</span>(command, <span class="hljs-number">2</span>, <span class="hljs-number">10</span>, serialnumber, <span class="hljs-number">3</span>))
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
<span class="hljs-type">uint16_t</span> featureset;
command[<span class="hljs-number">0</span>] = <span class="hljs-number">0x20</span>;
command[<span class="hljs-number">1</span>] = <span class="hljs-number">0x2F</span>;
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">readWordFromCommand</span>(command, <span class="hljs-number">2</span>, <span class="hljs-number">10</span>, &featureset, <span class="hljs-number">1</span>))
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
<span class="hljs-comment">// Serial.print("Featureset 0x"); Serial.println(featureset, HEX);</span>
<span class="hljs-keyword">if</span> ((featureset & <span class="hljs-number">0xF0</span>) != SGP30_FEATURESET)
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
<span class="hljs-keyword">if</span> (initSensor) {
<span class="hljs-keyword">if</span> (!<span class="hljs-built_in">IAQinit</span>())
<span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>发现在<code>IAQinit()</code>初始化后就开始发热了,问了DeepSeek说这个函数会启动内部加热器,但是初始化流程不合理,要使用Sensirion官方库,我就改用官方库的代码试了下发现有一点点改善,但还是有逐渐发热。<br>关于DeepSeek提到的加热器,查看官方芯片手册,没有对加热器做过多说明,但是芯片架构图里明确有heater controller存在。<br><img src="//image.lceda.cn/pullimage/ucgdwHq2qahlQamjX30GAwrBdpHxJN7HzsMyGRgJ.png"></p>
<p>所以应该是芯片自身特性,就不管了。。<br>但是这里存在一个问题:我的使用场景下,发热对环境影响不是很大,但是实验室环境下还是要避免传感器本身的发热,所以将来需要通过更换更好更精准的传感器来解决。</p>
<p> </p>
<h3 id="55-littlefs无法加载">6.5 littlefs无法加载</h3>
<p>在做第二个模块的时候,发现下载代码报错:</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.006</span> -> <span class="hljs-built_in">E</span> (<span class="hljs-number">1386</span>) esp_littlefs: ./components/esp_littlefs/src/littlefs/lfs.c:<span class="hljs-number">1225</span>:error: Corrupted dir pair at {<span class="hljs-number">0x0</span>, <span class="hljs-number">0x1</span>}
<span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.006</span> ->
<span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.006</span> -> <span class="hljs-built_in">E</span> (<span class="hljs-number">1386</span>) esp_littlefs: mount failed, (<span class="hljs-number">-84</span>)
<span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.006</span> -> <span class="hljs-built_in">E</span> (<span class="hljs-number">1389</span>) esp_littlefs: Failed to initialize LittleFS
<span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.045</span> -> <span class="hljs-built_in">E</span> (<span class="hljs-number">1420</span>) esp_littlefs: Failed to format filesystem
<span class="hljs-number">09</span>:<span class="hljs-number">58</span>:<span class="hljs-number">19.045</span> -> [E][persistence.cpp:<span class="hljs-number">345</span>][init] LittleFS Mount Failed
</code></pre>
<p>本来以为第一次下载代码或者擦除flash后第一次启动会这样,但是重启了还是一样的报错,选择了不同的partition分区也没用,擦除flash也是这样。<br>后来在网上看到个<a href="https://blog.csdn.net/qq_61692089/article/details/135064611" target="_blank">帖子</a>,提到了Arduino Flash Mode选项可能存在问题,我就试了下,结果改成了DIO 80MHz就行了。。顺便学习了下这个Flash Mode什么意思。<br>参考博客<a href="https://blog.csdn.net/tianizimark/article/details/124663902" target="_blank">1</a>、<a href="https://www.elecfans.com/zt/1328274/" target="_blank">2</a>。<br>源码中其实就是不同spi通信模式:</p>
<ul>
<li>SPI_FLASH_SLOWRD 标准SPI,对时钟速度有限制(速率较慢)</li>
<li>SPI_FLASH_FASTRD 标准SPI</li>
<li>SPI_FLASH_DOUT 双线SPI(Dual SPI)只在数据阶段使用两根数据线通信</li>
<li>SPI_FLASH_DIO 双线SPI(Dual SPI)地址、数据阶段都使用两根数据线通信</li>
<li>SPI_FLASH_QOUT 四线SPI(Qual SPI)只在数据阶段使用四根数据线通信</li>
<li>SPI_FLASH_QIO 四线SPI(Qual SPI)地址、数据阶段使用四根数据线通信</li>
<li>SPI_FLASH_OPI_STR 四线SPI(Qual SPI)命令、地址、数据阶段(所有阶段)都使用四根数据线通信,并且一个时钟传输一位</li>
<li>SPI_FLASH_OPI_DTR 四线SPI(Qual SPI)命令、地址、数据阶段(所有阶段)都使用四根数据线通信,并且一个时钟传输两位<br>其中DIO兼容性最强,一般不会出错。这么看来可能是第二个esp32模组有些许不同。<br>但其实定位到这里我也有点怀疑是我焊接的spi管脚可能存在问题,比如模组位置没有摆正导致相邻引脚短路干涉(之前遇到过),不过模组上方oled已经焊死在PCB上了,不好拆了。。就此作罢。<br>后开又发现了另一个问题,PSRAM不能打开,否则会出现不停重启现象,更怀疑可能是spi相关引脚焊接的问题了。。</li>
</ul>
<p> </p>
<h3 id="56-adafruit_sgp30库的softreset函数不生效总是失败">6.6 Adafruit_SGP30库的softReset()函数不生效,总是失败</h3>
<p>先看官方手册:<br><img src="//image.lceda.cn/pullimage/ITJxl3WnGotcsC7c6zlHcixD9bALgRGFPRHzTWfu.png"><img src="//image.lceda.cn/pullimage/gX2TM9SPdTvEwIQ7QQLWuIKh4wxhWSo0cDnr3WWN.png"><br>简单说就是I2C广播,地址是0x00,而指令只有1个字节0x06。再看代码:</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-function">boolean <span class="hljs-title">Adafruit_SGP30::softReset</span><span class="hljs-params">(<span class="hljs-type">void</span>)</span> </span>{
<span class="hljs-type">uint8_t</span> command[<span class="hljs-number">2</span>];
command[<span class="hljs-number">0</span>] = <span class="hljs-number">0x00</span>;
command[<span class="hljs-number">1</span>] = <span class="hljs-number">0x06</span>;
<span class="hljs-keyword">return</span> <span class="hljs-built_in">readWordFromCommand</span>(command, <span class="hljs-number">2</span>, <span class="hljs-number">10</span>);
}
</code></pre>
<p>跟踪下去就能发现,这里函数把指令变成了0x0006 2个字节,而底层addr地址并没有改变(还是0x58的设备地址,并不是0x00广播地址)。<br>所以要重新写个自己的soft reset函数。</p>
<pre class="highlighter-hljs"><code class="language-cpp highlighter-hljs hljs"><span class="hljs-function">boolean <span class="hljs-title">GS_SGP30_soft_reset</span><span class="hljs-params">()</span>
</span>{
Wire.<span class="hljs-built_in">beginTransmission</span>(<span class="hljs-number">0x00</span>); <span class="hljs-comment">// General Call地址</span>
<span class="hljs-comment">// 发送复位命令 0x06</span>
Wire.<span class="hljs-built_in">write</span>(<span class="hljs-number">0x06</span>);
<span class="hljs-type">uint8_t</span> result = Wire.<span class="hljs-built_in">endTransmission</span>();
<span class="hljs-comment">// 等待复位完成(SGP30需要10ms初始化)</span>
<span class="hljs-built_in">delay</span>(<span class="hljs-number">10</span>);
<span class="hljs-keyword">return</span> (result == <span class="hljs-number">0</span>);
}</code></pre>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt pap-left-indent-1.6em" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*7、BOM清单</h3>
<hr class="horizontal-splitline normal-bold-2">
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">无法上传或者同步,官方也没有教程。。重新保存也没用,这个bug还没修复好像。。先贴上链接。</p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">https://oshwhub.com/meinschatz/air-quality-monitor-based-on-esp32</p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"><span style="color: #95a5a6; font-size: 14px;">注:项目涉及的BOM清单。在<span style="text-decoration: underline;"><a href="https://lceda.cn/editor" target="_blank">嘉立创EDA</a> </span>生成/上传设计文件后,BOM将自动生成至项目详情;建议包括型号、品牌、名称、封装、采购渠道、用途等内容。具体内容和形式应以表达清楚项目构成为准。 </span></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">*8、大赛LOGO验证</h3>
<hr class="horizontal-splitline normal-bold-2">
<p style="line-height: 1.8;">实际工作环境:</p>
<p style="line-height: 1.8;"><img src="//image.lceda.cn/pullimage/gvIC5K2I1C7jHTzh192WaS6mJbyyvtBdg2H5YCUK.jpeg" width="457" height="1014"><img src="//image.lceda.cn/pullimage/TkRqvoHT6hSOh4x3JN8vxmsa4v1uMgEOSsJc2ZZQ.jpeg" width="644" height="290"></p>
<p style="line-height: 1.8;"> </p>
<p style="line-height: 1.8;"><img src="//image.lceda.cn/pullimage/r3PrvV2VFAfv9CIZz60Xq1HVU5S8WIdxjaRY3uxm.png"></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
<h3 class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;">* 9、演示您的项目并录制成视频上传</h3>
<hr class="horizontal-splitline normal-bold-2">
<p style="line-height: 1.8;"> </p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"><span style="color: #95a5a6; font-size: 14px;">视频要求:请横屏拍摄,分辨率不低于1280×720,格式Mp4/Mov,单个视频大小限100M内;</span></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"><span style="color: #95a5a6; font-size: 14px;">视频标题:立创电赛:{项目名称}-{视频模块名称};如立创电赛:《自动驾驶》-团队介绍。</span></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"><span style="font-size: 14px;"><a href="/posts/42551e8f2f2548cabc1c36626a42da94" target="_blank">前往查看更多详情 ></a></span></p>
<p class="paragraph text-align-type-left pap-line-1.3 pap-line-rule-auto pap-spacing-before-3pt pap-spacing-after-3pt" style="line-height: 1.8;"> </p>
</div>
评论(0)