<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[tanejo's blog]]></title><description><![CDATA[欢迎光临]]></description><link>https://tanejo.cn</link><image><url>https://tanejo.cn/api/v2/objects/avatar/mh0vkm4vduichjzhlk.jpg</url><title>tanejo&apos;s blog</title><link>https://tanejo.cn</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Thu, 07 May 2026 09:10:34 GMT</lastBuildDate><atom:link href="https://tanejo.cn/feed" rel="self" type="application/rss+xml"/><pubDate>Thu, 07 May 2026 09:10:34 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[2026年CIMC西门子杯工业嵌入式系统开发赛项讲解整理]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105056173.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105248951.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105121417.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105301836.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105315787.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105147121.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20260314105226110.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/competition/2026-cimc-siemens-cup-industrial-embedded-systems-development">https://tanejo.cn/posts/competition/2026-cimc-siemens-cup-industrial-embedded-systems-development</a></blockquote><div><blockquote><p>说明：本文整理自官方直播讲解，原视频配有PPT，录制从中途开始，前段少部分内容不完整。</p></blockquote>
<hr/><h2 id="">一、软件功能需求（续）</h2><p><img src="https://img.661231.xyz/image-20260314105056173.png" alt="image-20260314105056173"/></p><h3 id="8">第8条：串口收发与报文解析</h3><p>能够利用串口实现数据的收发和报文解析功能。</p><h3 id="9gd32">第9条：低功耗设计（GD32休眠功能）</h3><p>利用GD32的休眠功能实现低功耗设计。</p><p><strong>具体要求：</strong></p><ul><li>在供电电源与开发板之间串联一个电流表</li><li>通过观察进入低功耗睡眠前后的电流变化，判断是否成功进入低功耗模式</li><li>需要自行设计：在什么条件下进入低功耗、如何唤醒（定时唤醒 / 外部中断唤醒 / 其他方式），有一定的自由设计空间</li></ul><h3 id="10rtc">第10条：RTC时钟</h3><p>利用RTC实现时钟功能（与去年无变化）。</p><h3 id="11tf--flash">第11条：TF卡 / 外部Flash读写</h3><p>通过TF卡和外部Flash实现数据的读写（与去年无变化）。</p><h3 id="12bootloader">第12条：Bootloader基础功能（今年新增）</h3><p>实现Bootloader的基础功能，即在不使用SWD仿真器的情况下，通过固件下装方式实现板载系统升级（如OTA升级、上位机升级等）。</p><p><strong>需要考虑的细节：</strong></p><ul><li>升级文件的校验（文件头校验、版本号比对）</li><li>系统版本号的管理</li><li>升级前将配置参数保存到Flash，升级后恢复</li></ul><hr/><h2 id="">二、硬件部分需求</h2><p>硬件部分要求使用<strong>嘉立创EDA</strong>完成电路设计、打样、制作和调试，共两类电路板：</p><h3 id="">电源板</h3><ul><li>输入：18V～36V</li><li>输出：5V（为控制板供电）</li><li>设计一个稳定可靠的降压电源电路</li></ul><h3 id="pt100">PT100变送器采样板</h3><ul><li>用于温度检测</li><li>需要完成PT100输入输出曲线的拟合校验（建议用冷水/温水/热水三瓶矿泉水进行测试）</li><li>最终输出按规约给定的浮点数格式，并能解析还原为温度值</li></ul><p><strong>两块板均需完成打板打样制作。</strong></p><p><img src="https://img.661231.xyz/image-20260314105248951.png" alt="image-20260314105248951"/></p><hr/><h2 id="">三、芯片要求</h2><ul><li>两块硬件板均要求使用<strong>兆易创新</strong>指定的两款芯片（具体型号4月中旬官网公告）</li><li><strong>购买新版开发板（4月发布第二版）</strong>：随板附配两款样片各3片</li><li><strong>使用去年旧板的同学</strong>：可通过以下渠道单独购买：
<ul><li>官方嵌入式购买平台（4月中旬上架竞赛资源包，按芯片成本价出售，各3片）</li><li>自行联系供应商或其他渠道购买</li></ul></li><li>对购买渠道无限制，只要求使用指定型号芯片</li></ul><hr/><h2 id="">四、初赛时间节点</h2><table><thead><tr><th> 时间            </th><th> 事项                                                         </th></tr></thead><tbody><tr><td> 5月初（上旬）   </td><td> 硬件设计题目发布，大家有约一个月时间完成设计、打样、调试     </td></tr><tr><td> 5月31日         </td><td> 报名截止（<strong>强烈建议提前报名</strong>，见下方说明）                 </td></tr><tr><td> 6月5日 晚8点    </td><td> 初赛题目和方案模板发布                                       </td></tr><tr><td> 6月5日～6月8日  </td><td> 嵌入式软件开发时间                                           </td></tr><tr><td> 6月8日 晚8点    </td><td> <strong>嵌入式程序和功能演示视频录制截止</strong>（8点后的内容不计入评分） </td></tr><tr><td> 6月8日 23:59    </td><td> 所有材料上传截止（建议提前上传，错峰提交，不要卡点）         </td></tr><tr><td> 6月8日～6月11日 </td><td> 系统方案撰写 + 答辩介绍视频制作                              </td></tr><tr><td> 6月11日 中午    </td><td> 系统方案和答辩视频提交截止                                   </td></tr></tbody></table><h3 id="">关于报名时间的提醒</h3><p>去年有同学5月29、30日才报名，导致后续时间极为紧张。PCB打板周期：</p><ul><li>嘉立创加急：12～24小时（加急费较贵）</li><li>正常打板：3～4天 + 发货1～2天</li></ul><p>如果打板后发现问题还需重新设计，时间会非常紧。<strong>建议尽早确定参赛并提前准备。</strong></p><p><img src="https://img.661231.xyz/image-20260314105121417.png" alt="image-20260314105121417"/></p><hr/><h2 id="">五、评分机制</h2><h3 id="">初赛总分构成</h3><pre class=""><code class="">初赛总成绩 = 实施过程总分（100分）× 60% + 技术文档与答辩视频（40%）
</code></pre>
<ul><li><strong>实施过程总分</strong>：嵌入式软件占60%，硬件开发占40%（暂定，具体比例待专家组确认）</li><li><strong>技术文档与答辩视频</strong>：合计占总成绩40%</li></ul><h3 id="">功能完成度系数机制</h3><p>为防止&quot;任务完成少但文档写得好&quot;的情况，设立了<strong>功能完成度系数</strong>：</p><ul><li>前面实施部分完成了多少比例，文档和答辩部分的得分会乘以对应的完成度系数</li><li>不能靠堆砌文档弥补功能缺失</li></ul><h3 id="">评审机制</h3><p>采用<strong>盲评机制</strong>，材料分配给不同专家评审，防止因泄露学校/姓名信息影响评分公正性。</p><p><img src="https://img.661231.xyz/image-20260314105301836.png" alt="image-20260314105301836"/>
<img src="https://img.661231.xyz/image-20260314105315787.png" alt="image-20260314105315787"/></p><hr/><h2 id="">六、功能演示要求</h2><h3 id="">上位机评分流程</h3><ul><li>赛前官方提供上位机软件（预设好报文）</li><li>操作流程：点击上位机按钮 → 报文下发给嵌入式程序 → 嵌入式解析报文并组装上行报文 → 上位机解析返回报文并显示 → 自动生成评分文件</li><li>需提交：<strong>上位机自动生成的评分文件</strong>（记录完整操作过程和数据）</li></ul><h3 id="">视频录制要求</h3><ul><li>全程使用<strong>腾讯会议</strong>录制</li><li>视频大小不超过<strong>500MB</strong>（腾讯会议录制约300MB左右）</li><li>具体录制要求赛前另行发布</li><li>视频中需清晰展示核心指标，例如：
<ul><li>低功耗模式下的电流变化</li><li>PT100测温（将传感器放入不同温度的水瓶中）</li><li>OLED显示内容（<strong>字体不要太小</strong>，建议两行显示，确保画面清晰可读）</li></ul></li></ul><h3 id="">评分截止说明</h3><p>6月8日晚8点整为功能演示截止时间，评分文件中8点之后的操作记录<strong>不计入评分</strong>。</p><p><img src="https://img.661231.xyz/image-20260314105147121.png"/></p><p><img src="https://img.661231.xyz/image-20260314105226110.png" alt="image-20260314105226110"/></p><hr/><h2 id="">七、材料提交方式</h2><ul><li>使用官方上位机软件（同时也是材料上传工具）</li><li>赛前会发放队伍编号和密钥，登录后按类别上传对应文件</li><li>系统有<strong>自动重命名机制</strong>，务必在正确的文件类别下上传对应文件，否则命名错误导致文件无法识别</li></ul><hr/><h2 id="">八、去年常见失分点（重点参考）</h2><h3 id="1-">1. 未按要求提交材料</h3><ul><li>队伍编号填写错误</li><li>文件命名不符合要求</li><li>后果：专家收到的文件夹为空，无法评审，直接0分</li></ul><h3 id="2-">2. 演示视频录制不清晰</h3><ul><li>OLED字体过小（做成4行，字看不清）</li><li>画面模糊、角度偏斜、有反光</li><li>建议：OLED显示两行即可，录制时确保画面清晰正对</li></ul><h3 id="3-">3. 代码查重（重灾区）</h3><p>以下情况均会被判定为代码完全一致：</p><ul><li>逻辑应用层代码一模一样，未做任何修改</li><li>仅修改了注释、调换了函数顺序</li><li>仅修改了变量名（如改成 <code>variable1</code>、<code>a</code>、<code>b</code> 等无意义命名）</li><li>添加了大量无效混淆代码（如永远不会执行的 <code>else</code> 分支）</li></ul><blockquote><p>正常情况下，同一个人自己写两遍代码都不会完全一样，如果两份代码逻辑结构、变量命名、括号位置完全相同，必然会被判定为抄袭。</p></blockquote>
<h3 id="4-ai">4. 报告AI生成</h3><ul><li>去年大量报告被专家判定为AI生成，AI率过高</li><li>报告的意义：训练逻辑思维、分析问题和解决问题的能力、学术写作能力</li><li>AI生成的内容缺乏个人深度思考，且往往包含错误，如果没有相应知识储备，无法有效审查和修改</li><li><strong>必须自己撰写报告</strong>，这也是比赛考核的重要组成部分</li></ul><h3 id="5-">5. 答辩视频出现个人信息</h3><ul><li>视频中出现姓名、学校名称、学校LOGO等信息，会被扣分</li><li>PPT不要使用带有学校标识的模板，也不要在开场介绍中报出姓名</li></ul><hr/><h2 id="">九、学习资源</h2><ul><li><strong>学习通</strong>和<strong>B站</strong>平台持续对参赛者开放，可先学习去年已上线的课程内容</li><li>今年4月起，将逐步更新今年的课程体系</li><li>课程内容从零基础到进阶，与企业真实需求衔接</li></ul><hr/><p><em>以上为工业嵌入式系统开发赛项的全部讲解内容，后续QA环节问题另行整理。</em></p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/competition/2026-cimc-siemens-cup-industrial-embedded-systems-development#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/competition/2026-cimc-siemens-cup-industrial-embedded-systems-development</link><guid isPermaLink="true">https://tanejo.cn/posts/competition/2026-cimc-siemens-cup-industrial-embedded-systems-development</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Sat, 14 Mar 2026 03:03:00 GMT</pubDate></item><item><title><![CDATA[sscanf 的常见陷阱与安全使用]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/DevNotes/common-traps-and-safe-use-of-sscanf">https://tanejo.cn/posts/DevNotes/common-traps-and-safe-use-of-sscanf</a></blockquote><div><p><code>sscanf</code> 是 C 语言中一个非常方便的函数，它可以从字符串中轻松地提取格式化的数据。然而，它的方便也伴随着一些非常隐蔽的陷阱。如果你不了解它的工作机制，很容易写出看似正常但实际存在严重漏洞的代码，尤其是在嵌入式和单片机开发中，这类问题常常导致程序卡死或行为异常。</p><p>本文档旨在总结 <code>sscanf</code> 最核心的陷阱，并提供更安全、更健壮的替代方案。</p><h2 id="">陷阱一：最经典的“部分匹配”陷阱</h2><p><strong>核心误解</strong>：认为 <code>sscanf</code> 返回值大于0就代表整个格式串都匹配成功了。</p><p><strong>事实</strong>：<code>sscanf</code> 在遇到第一个不匹配的字符时就会立即停止，并返回它在停止前已经成功赋值的变量数量。</p>
<h3 id="">场景重现</h3><p>假设你的代码如下，你的输入是 <code>&quot;LED1:OFF&quot;</code>：</p><pre class="language-c lang-c"><code class="language-c lang-c">uint8_t led_temp = 0;
if (sscanf(cmd, &quot;LED%hhu:ON&quot;, &amp;led_temp) == 1) {
    // 你以为只有 &quot;LED1:ON&quot; 才能进入这里
    // 但 &quot;LED1:OFF&quot; 也进来了！
    printf(&quot;LED %u Turned Up\n&quot;, led_temp); 
}
</code></pre><h3 id="">分解过程：</h3><p><code>sscanf</code> 在匹配 <code>&quot;LED%hhu:ON&quot;</code> 时，它的内心活动是这样的：</p><ol start="1"><li><code>L</code>, <code>E</code>, <code>D</code> -&gt; 匹配成功。</li><li><code>%hhu</code> -&gt; 匹配到数字 <code>1</code>，并成功赋值给 <code>led_temp</code>。<strong>（成功赋值数量：1）</strong></li><li><code>:</code> -&gt; 匹配成功。</li><li><code>O</code> -&gt; 匹配成功。</li><li><code>N</code> -&gt; 期望是&#x27;N&#x27;，但输入是&#x27;F&#x27;。<strong>不匹配！立即停止！</strong></li></ol><p><strong>最终结果</strong>：<code>sscanf</code> 在停止前成功赋值了 <code>1</code> 个变量 (<code>led_temp</code>)，所以它的返回值是 <code>1</code>。<code>if (1 == 1)</code> 条件成立，程序错误地进入了处理 &quot;ON&quot; 的逻辑。</p><h2 id="">陷阱二：数值溢出陷阱</h2><p><code>sscanf</code> 在处理超出变量范围的数值时，并不会告诉你出错了。
<strong>核心误解</strong>：认为 <code>sscanf</code> 会自动处理数值范围问题。
<strong>事实</strong>：<code>sscanf</code> 不会进行范围检查。如果提供的数字超出了变量类型的存储范围，它会发生“回绕”（wraparound），得到一个完全错误的值，但函数返回值依然是 <code>1</code>，让你误以为转换成功。</p><h3 id="">场景重现</h3><p>假设 <code>led_temp</code> 是 <code>uint8_t</code> (范围 0-255)，输入是 <code>&quot;LED999:ON&quot;</code>。</p><pre class="language-c lang-c"><code class="language-c lang-c">uint8_t led_temp = 0; // 只能存 0-255
if (sscanf(&quot;LED999:ON&quot;, &quot;LED%hhu:ON&quot;, &amp;led_temp) == 1) {
    // 程序会进入这里
    // 但 led_temp 的值是 231 (999 % 256)
    // 而不是 999！
    printf(&quot;Value: %u\n&quot;, led_temp); 
}
</code></pre><p>这是一个非常危险的逻辑 Bug，因为程序在处理一个错误的数据，但它自己却毫不知情。</p><h2 id="">如何安全地解析字符串？</h2><p>既然 <code>sscanf</code> 有这么多问题，我们应该如何编写健壮的解析代码呢？</p><h3 id="">方案一：先验证，再解析</h3><p>将“验证字符串格式”和“提取数字”这两个步骤分开。</p><pre class="language-c lang-c"><code class="language-c lang-c">#include &lt;string.h&gt;

void process_command(const char* cmd) {
    uint8_t led_temp = 0;
    
    // 1. 先用更严格的字符串函数判断命令类型
    if (strstr(cmd, &quot;:ON&quot;) != NULL) {
        // 2. 确认是ON命令后，再用sscanf提取数字，此时更安全
        if (sscanf(cmd, &quot;LED%hhu:ON&quot;, &amp;led_temp) == 1) {
            printf(&quot;ON command for LED %u\n&quot;, led_temp);
        }
    } else if (strstr(cmd, &quot;:OFF&quot;) != NULL) {
        if (sscanf(cmd, &quot;LED%hhu:OFF&quot;, &amp;led_temp) == 1) {
            printf(&quot;OFF command for LED %u\n&quot;, led_temp);
        }
    }
}
</code></pre><p><strong>优点</strong>：代码逻辑清晰，易于阅读和维护。</p><h3 id="-strtol-">方案二：使用 <code>strtol</code> 系列函数</h3><p>对于任何严肃的项目，尤其是处理外部输入时，<code>strtol</code> (<code>string to long</code>) 和 <code>strtoul</code> (<code>string to unsigned long</code>) 是最佳选择。它们提供了精细的错误检查机制。</p><pre class="language-c lang-c"><code class="language-c lang-c">#include &lt;stdlib.h&gt;
#include &lt;errno.h&gt;
#include &lt;limits.h&gt;

void process_command_robust(const char* cmd) {
    // 假设命令是 &quot;LED123:ON&quot;
    if (strncmp(cmd, &quot;LED&quot;, 3) != 0) return;

    const char* num_start = cmd + 3;
    char* end_ptr; // 将指向数字之后第一个字符的地址

    errno = 0; // 清空错误码
    unsigned long value = strtoul(num_start, &amp;end_ptr, 10);

    // 检查1：数字后面是否有垃圾字符？
    // 正确的格式下，end_ptr应该指向 &quot;:&quot;
    if (end_ptr == num_start || *end_ptr != &#x27;:&#x27;) {
        printf(&quot;Error: Invalid format.\n&quot;);
        return;
    }
    
    // 检查2：是否溢出？
    if (errno == ERANGE || value &gt; UCHAR_MAX) { // UCHAR_MAX == 255
        printf(&quot;Error: Number out of range.\n&quot;);
        return;
    }
    
    // 所有检查通过，现在才是安全的值
    uint8_t led_temp = (uint8_t)value;

    // 最后检查命令是ON还是OFF
    if (strcmp(end_ptr, &quot;:ON&quot;) == 0) {
        printf(&quot;Robust ON for LED %u\n&quot;, led_temp);
    } else if (strcmp(end_ptr, &quot;:OFF&quot;) == 0) {
        printf(&quot;Robust OFF for LED %u\n&quot;, led_temp);
    }
}
</code></pre>
<p><strong>优点</strong>：极致安全。能检查溢出、能检查非法格式、能检查多余字符。</p><h2 id="">总结与建议</h2><table><thead><tr><th> 何时使用？                           </th><th> 推荐函数                                        </th><th> 理由                                                 </th></tr></thead><tbody><tr><td> 处理<strong>外部输入</strong>（串口、网络、用户） </td><td> <code>strtol</code>系列 / 先验证再解析                     </td><td> <strong>绝对不要信任外部输入！</strong> 必须做到最严格的检查。    </td></tr><tr><td> 解析内部<strong>格式固定的</strong>简单字符串     </td><td> <code>sscanf</code>                                        </td><td> 在确保输入格式100%可信的情况下，用它图个方便也无妨。 </td></tr><tr><td> 解析有多种固定格式的命令             </td><td> 先用 <code>strstr</code>/<code>strcmp</code> 验证，再用 <code>sscanf</code> 解析 </td><td> 代码可读性高，逻辑清晰，是安全和方便的平衡点。       </td></tr></tbody></table><p><strong>最后的忠告：永远不要高估 <code>sscanf</code> 的能力，也永远不要低估用户输入“惊喜”数据的能力。</strong></p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/DevNotes/common-traps-and-safe-use-of-sscanf#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/DevNotes/common-traps-and-safe-use-of-sscanf</link><guid isPermaLink="true">https://tanejo.cn/posts/DevNotes/common-traps-and-safe-use-of-sscanf</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Mon, 22 Sep 2025 10:58:58 GMT</pubDate></item><item><title><![CDATA[[DEBUG] DMA+空闲中断的“伪中断”问题]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/image-20250916212502245.png"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/DevNotes/debug-dma-plus-space-interrupt-problem">https://tanejo.cn/posts/DevNotes/debug-dma-plus-space-interrupt-problem</a></blockquote><div><h2 id="dma">程序逻辑错误：DMA+空闲中断的“伪中断”问题</h2><p><strong>问题</strong>：USART1 和 USART3 的 DMA 接收功能在刚上电时无效，必须手动按一下复位键后才能正常工作。</p><p><strong>分析</strong>：这是“DMA+空闲中断”方案中一个典型的“竞态条件”问题。原因如下：</p><ul><li>STM32 一上电，其 RX 引脚就已经接收到了来自电脑串口工具的“空闲”高电平。</li><li>当初始化代码执行到使能“空闲中断”的那一刻，USART 外设立即检测到这个已经存在的空闲状态，产生了一次“伪”的空闲中断。</li><li>这次中断被错误地处理了（因为 DMA 还没收到任何数据），导致系统错过了对第一帧真实数据的响应。</li><li>手动复位时，时序略有不同，规避了这个问题。</li></ul><p><strong>解决方案</strong>：在启动 DMA 接收、使能空闲中断之前，强制清除一次 USART 的空闲标志位。代码实现为：</p><pre class="language-c lang-c"><code class="language-c lang-c">__HAL_UART_CLEAR_IDLEFLAG(&amp;huartX); // 清除标志
HAL_UARTEx_ReceiveToIdle_DMA(&amp;huartX, ...); // 启动DMA接收
</code></pre>
<h2 id="">代码结构优化——初始化与业务逻辑分离</h2><p><strong>问题分析</strong>：即使加入了清除标志位的代码，放在 <code>Init</code> 函数里效果依然不理想。其深层原因是：</p><ul><li>在 <code>main</code> 函数中，<code>MX_USART1_UART_Init</code> 执行完后，程序继续执行了其他外设的初始化 (<code>MX_I2C2_Init</code>, <code>MX_USART3_UART_Init</code> 等)。</li><li>在一个不稳定的、动态的初始化序列中途提前启动一个需要持续运行的后台任务（如 UART DMA），很容易被后续其他外设的初始化动作（如重设 DMA 通道、改变时钟、修改中断优先级等）所干扰，导致失败。</li></ul><p><strong>解决方案</strong>：</p><ol start="1"><li>移除<code>MX_..._Init()</code> 函数中所有业务启动相关的代码 (如 <code>HAL_UARTEx_ReceiveToIdle_DMA</code>)，让它只负责纯粹的硬件参数配置。</li><li>将业务启动代码，连同修复方案，统一迁移到 <code>main</code> 函数的 <code>/* USER CODE BEGIN 2 */</code> 区域。</li></ol><p><strong>结果</strong>：确保了所有硬件外设都配置完毕，系统进入稳定状态后，才启动 UART DMA 接收等应用层任务。程序行为变得可靠且符合预期。</p><img src="https://img.661231.xyz/image-20250916212502245.png" alt="image-20250916212502245"/><p>更新:要在两个串口外设初始化之前加一句  <code>HAL_Delay(500);</code> 延迟函数，否则USART3的DMA接收也可能失效.</p><h2 id="">总结</h2><ol start="1"><li><strong>警惕时序和竞态条件</strong>：中断、DMA 等异步操作极易出现时序问题。“上电异常，复位正常”是典型信号。</li><li><strong>分离硬件配置与业务启动</strong>：这是最重要的原则。
<ul><li><strong>配置</strong>：让 <code>MX_..._Init</code> 函数只做一件事——根据 CubeMX 的设置配置好硬件寄存器，让硬件处于“待命”状态。</li><li><strong>启动</strong>：在 <code>main</code> 函数中，等待所有硬件配置完成后，再按业务逻辑顺序启动它们（比如开启 DMA、启动定时器 PWM 等）。</li></ul></li></ol><p>遵循第2点原则，可以避免 90% 以上难以排查的嵌入式系统初始化问题，也是代码走向专业和稳健的必经之路。</p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/DevNotes/debug-dma-plus-space-interrupt-problem#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/DevNotes/debug-dma-plus-space-interrupt-problem</link><guid isPermaLink="true">https://tanejo.cn/posts/DevNotes/debug-dma-plus-space-interrupt-problem</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Tue, 16 Sep 2025 11:32:00 GMT</pubDate></item><item><title><![CDATA[[AIGC] Miniconda 常用命令速查手册]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/Sharing/aigc-miniconda-commands-reference-guide">https://tanejo.cn/posts/Sharing/aigc-miniconda-commands-reference-guide</a></blockquote><div><p>最近学习视觉测控，需要用到Python，用AI生成一些常用命令，方便查询</p><hr/><p>Miniconda 是一个轻量级的 Conda 安装程序。它包含了 Conda、Python、它们所依赖的包，以及少量其他有用的包。这个速查手册旨在帮助你快速找到并使用最核心、最常用的 Miniconda 命令。</p><h2 id="-environments">环境管理 (Environments)</h2><p>管理独立隔离的开发环境是 Conda 最强大的功能之一。</p><h3 id="">创建新环境</h3><ul><li><p>创建一个包含特定 Python 版本的环境：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda create --name myenv python=3.9
</code></pre>
<p><em><code>myenv</code></em> 是你的环境名称，<em><code>python=3.9</code></em> 是你希望安装的 Python 版本。</p></li><li><p>创建一个包含特定软件包的空环境：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda create --name myenv numpy pandas
</code></pre>
</li><li><p>完整克隆一个已存在的环境：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda create --name newenv --clone oldenv
</code></pre>
</li></ul><h3 id="">激活与退出环境</h3><ul><li><p>激活环境 (Windows):</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda activate myenv
</code></pre>
</li><li><p>激活环境 (macOS / Linux):</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">source activate myenv
</code></pre>
<p>或者更通用的：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda activate myenv
</code></pre>
</li><li><p>退出当前环境，返回 base 环境：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda deactivate
</code></pre>
</li></ul><h3 id="">查看环境</h3><ul><li><p>列出所有已创建的环境：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda env list
</code></pre>
<p>或者</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda info --envs
</code></pre>
<p>当前激活的环境会有一个 <code>*</code> 标记。</p></li></ul><h3 id="">删除环境</h3><ul><li><p>删除指定名称的环境及其所有包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda env remove --name myenv
</code></pre>
</li></ul><h2 id="-packages">包管理 (Packages)</h2><p>在激活的环境中，你可以轻松管理软件包。</p><h3 id="">安装包</h3><ul><li><p>在当前环境中安装包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda install numpy
</code></pre>
</li><li><p>安装多个包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda install numpy pandas matplotlib
</code></pre>
</li><li><p>安装指定版本的包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda install numpy=1.20.3
</code></pre>
</li><li><p>从特定的渠道 (channel) 安装包 (例如 anaconda, conda-forge)：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda install -c conda-forge beautifulsoup4
</code></pre>
</li></ul><h3 id="">查看已安装的包</h3><ul><li><p>列出当前环境中所有已安装的包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda list
</code></pre>
</li><li><p>搜索可用的包版本：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda search beautifulsoup4
</code></pre>
</li></ul><h3 id="">更新包</h3><ul><li><p>更新当前环境中的单个包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda update numpy
</code></pre>
</li><li><p>更新当前环境中所有可更新的包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda update --all
</code></pre>
</li></ul><h3 id="">删除包</h3><ul><li><p>从当前环境中删除一个包：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda remove numpy
</code></pre>
</li></ul><h2 id="conda-">Conda 自身管理</h2><ul><li><p>更新 Conda 到最新版本：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda update -n base -c defaults conda
</code></pre>
<p>或者一个更简洁的命令：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda update conda
</code></pre>
</li><li><p>查看 Conda 版本：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda --version
</code></pre>
<p>或者</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda info
</code></pre>
</li></ul><h2 id="-channels">渠道管理 (Channels)</h2><p>Channels 是 Conda 用来查找和安装包的远程仓库。</p><ul><li><p>查看当前配置的渠道列表：</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda config --show channels
</code></pre>
</li><li><p>添加新的渠道 (例如，添加 <code>conda-forge</code> 并设为最高优先级):</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda config --add channels conda-forge
</code></pre>
</li><li><p>移除指定的渠道:</p><pre class="language-bash lang-bash"><code class="language-bash lang-bash">conda config --remove channels conda-forge
</code></pre>
</li></ul><hr/><p><strong>小贴士</strong>: 将最常用的渠道（如 <code>conda-forge</code>）置于 <code>defaults</code> 之上，可以帮助你安装到更多更新、更全面的社区维护的包。使用 <code>--add</code> 会将新渠道添加到列表顶部，使其具有最高优先级。</p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/Sharing/aigc-miniconda-commands-reference-guide#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/Sharing/aigc-miniconda-commands-reference-guide</link><guid isPermaLink="true">https://tanejo.cn/posts/Sharing/aigc-miniconda-commands-reference-guide</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Thu, 07 Aug 2025 02:24:33 GMT</pubDate></item><item><title><![CDATA[数据包处理——初探状态机思想]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/DevNotes/L-StateMachine">https://tanejo.cn/posts/DevNotes/L-StateMachine</a></blockquote><div><h2 id="">前言</h2><p>暑假在家闲来无事，翻出一个闲置许久的 HC-05 蓝牙模块，正好拿来玩玩。稍微查了一下，它说白了就是一个无线串口桥接工具，用手机上的蓝牙调试软件连接后，实际通信效果和有线串口没啥区别，没什么需要额外学的。</p><p>想起之前学习上挖过的坑，计划学习数据解包/封包还没有填上，正好借机会学习一下。</p><h2 id="">蓝牙模块</h2><p>我使用的是立创天空星STM32F407VGT6，将 HC-05 的 TXD , RXD 连接到单片机的 USART3 对应引脚，其逻辑电平3.3V，使用5V电压驱动。串口方案使用了DMA + 空闲中断 + 环形缓存区。</p><p>当EN/KEY引脚处于<strong>低电平</strong>或<strong>悬空</strong>状态时，HC-05处于默认模式（数据模式），手机使用蓝牙调试软件连接 HC-05 发送数据， HC-05 自动会转发数据到单片机。</p><p>当EN/KEY引脚被拉至<strong>高电平</strong>时，HC-05模块将进入AT指令模式。此模式允许用户通过串口向模块发送特定的“AT指令”，以配置模块的各种参数。</p><h2 id="">自定义数据包</h2><p>一个设计良好的数据包通常包含以下部分</p><table><thead><tr><th> 组成部分                     </th><th> 别称/俗称          </th><th> 功能与作用                                                   </th></tr></thead><tbody><tr><td> <strong>1. 包头 (Header)</strong>         </td><td> 同步头, 帧头       </td><td> 一串固定的、独特的字节序列，用于标识一个数据包的开始。       </td></tr><tr><td> <strong>2. 地址/ID (Address)</strong>     </td><td> 源/目标地址        </td><td> 在多设备通信总线（如RS485）上，用来指定该数据包是发给谁的，或者来自谁。在简单的点对点通信中可以省略。 </td></tr><tr><td> <strong>3. 命令/功能码 (Command)</strong> </td><td> 功能码, CMD        </td><td> 数据包的核心，告诉接收方“要做什么事”，例如“设置LED”、“读取温度”、“返回心跳”。 </td></tr><tr><td> <strong>4. 数据长度 (Length)</strong>     </td><td> LEN                </td><td> 明确指出<strong>有效载荷 (Payload)</strong> 的长度。这是实现变长数据包的关键，接收方根据此长度来确定需要读取多少字节的数据。 </td></tr><tr><td> <strong>5. 有效载荷 (Payload)</strong>    </td><td> 数据, Data         </td><td> 数据包中真正承载变化信息的部分。例如，要设置LED的状态值，要发送的传感器读数等。如果某个命令不需要额外数据（如“查询”），则此部分可以为空。 </td></tr><tr><td> <strong>6. 校验码 (Checksum)</strong>     </td><td> CRC, 校验和        </td><td> 通过特定算法（累加和、CRC等）对数据包的关键内容进行计算得出的一个“指纹”。接收方用同样的算法计算一遍，如果结果与接收到的校验码一致，就说明数据很可能没问题。 </td></tr><tr><td> <strong>7. 包尾 (Footer)</strong>         </td><td> 结束符, Terminator </td><td> 一个固定的字节序列，用于标识数据包的结束。在有“数据长度”字段的协议中，包尾不是必需的，但有时会用作额外的校验层。在一些文本协议中，换行符<code>\r\n</code>就扮演了包尾的角色。 </td></tr></tbody></table>
<p>为了练习，设计了一个简单情景，通过手机与蓝牙模块通信，控制开发版上6个LED。</p><p>自定义的数据包格式如下:</p><table><thead><tr><th> 字段 (Field)          </th><th> 字节数 (Bytes)   </th><th> 值 (Value)         </th><th> 描述 (Description)                                           </th></tr></thead><tbody><tr><td> 包头 (Header)    </td><td> 2                </td><td> <code>0xAA 0x55</code> (固定) </td><td> 标识一个数据帧的开始，接收方以此作为数据同步的标志。         </td></tr><tr><td> 命令码 (Command </td><td> 1                </td><td> <code>0x00</code> ~ <code>0xFF</code>    </td><td> 定义了该数据包的功能和意图。                                 </td></tr><tr><td> 数据长度 (Length) </td><td> 1                </td><td> <code>0x00</code> ~ <code>0xFF</code>    </td><td> 指示紧随其后的<strong>数据 (Data)</strong>字段的字节长度。如果数据字段为空，则此值为0。 </td></tr><tr><td> 数据 (Data)       </td><td> N (由Length决定) </td><td> 任意值             </td><td> 数据包的有效载荷，即真正需要传输的信息。                     </td></tr><tr><td> 校验和 (Checksum) </td><td> 1                </td><td> <code>0x00</code> ~ <code>0xFF</code>    </td><td> 从<strong>命令码</strong>到<strong>数据</strong>字段最后一个字节的所有字节的累加和，结果截断为最低8位。 </td></tr></tbody></table>
<p><strong>A. 设置LED状态 (CMD: <code>0x01</code>)</strong></p><ul><li>方向: 手机/上位机 -&gt; 开发板</li><li>功能: 控制开发板上6个LED的亮灭状态。</li><li>数据字段 (Data):
<ul><li>长度 (Length): 1字节。</li><li>内容: 一个8位字节，其中低6位（Bit 0 ~ Bit 5）分别对应6个LED的状态。<code>1</code>代表点亮，<code>0</code>代表熄灭。</li></ul></li><li>示例: 点亮LED1和LED3 (<code>0b00000101</code> = <code>0x05</code>)。
<ul><li>完整数据包**: <code>AA 55 01 01 05 07</code></li><li>校验和: <code>0x01 + 0x01 + 0x05 = 0x07</code></li></ul></li></ul><p><strong>B. 查询LED状态 (CMD: <code>0x02</code>)</strong></p><ul><li>方向 手机/上位机 -&gt; 开发板</li><li>功能: 请求开发板返回当前所有LED的状态。</li><li>数据字段 (Data):
<ul><li>长度 (Length): 0字节。数据字段为空。</li></ul></li><li>示例: 发送查询命令。
<ul><li>完整数据包: <code>AA 55 02 00 02</code></li><li>校验和: <code>0x02 + 0x00 = 0x02</code></li></ul></li></ul><p><strong>C. 响应LED状态 (CMD: <code>0x82</code>)</strong></p><ul><li>方向: 开发板 -&gt; 手机/上位机</li><li>功能: 作为对<code>0x02</code>查询命令的回复，上报当前LED的状态。
<ul><li>设计说明: 响应命令码通常在请求命令码的基础上将最高位置为1（即 <code>请求码 + 0x80</code>），这是一种常见的协议设计模式。</li></ul></li><li>数据字段 (Data):
<ul><li>长度 (Length): 1字节。</li><li>内容: 与<code>0x01</code>命令的数据格式完全相同，表示当前LED的实际状态。</li></ul></li><li>示例: 回复当前LED1和LED3亮着的状态。
<ul><li>完整数据包: <code>AA 55 82 01 05 88</code></li><li>校验和: <code>0x82 + 0x01 + 0x05 = 0x88</code></li></ul></li></ul><h2 id="">数据包处理</h2><h3 id="">为什么要用状态机？</h3><p>根据上面的设计，确定我们的数据包格式如下</p><p>| 包头1 (0xAA) | 包头2 (0x55) | 命令 (1字节) | 长度 (1字节) | 数据 (N字节) | 校验和 (1字节) |</p><p>按照直觉，我可能会写出这样的“面条代码”</p><pre class="language-c lang-c"><code class="language-c lang-c">// 伪代码
void process_data(uint8_t* data, int len) {
    int i = 0;
    if (data[i] == 0xAA) {
        i++;
        if (data[i] == 0x55) {
            i++;
            uint8_t cmd = data[i];
            i++;
            uint8_t data_len = data[i];
            i++;
            // ...一堆嵌套的 if-else...
        }
    }
}
</code></pre>
<p>这种代码的执行流程是线性的、过程化的，根据一系列条件判断来决定下一步操作，其坏处显而易见：层层嵌套，逻辑混乱，非常容易出错，而且极难扩展。</p><p>显然，这种“一条路走到黑”的面向过程写法，在处理带有上下文逻辑的流式数据时，已经力不从心。</p><p>那么，我们来分析一下这种混乱的根源是什么？</p><p>根本原因在于，我们的解析逻辑<strong>不仅取决于当前读到的字节，更依赖于一个隐藏的“上下文”</strong>，也就是“目前读到了数据包的哪个部分”。</p><p>在上面的代码里，试图用一个<code>i++</code>和代码的嵌套层次来被人为地“记住”这个上下文，但效果显然不理想。</p><p>既然解析过程存在不同的“上下文”，为何不换一种思维模式呢？我们可以把这些上下文明确地定义出来，变成一个个清晰的<strong>“状态”</strong>。</p><p>比如：</p><ul><li><code>等待包头1的状态</code></li><li><code>等待包头2的状态</code></li><li><code>等待命令字的状态</code></li><li>...等等</li></ul><p>如果程序能在这些明确的“状态”之间进行切换，代码结构就会清晰许多了。</p><p>这正是接下来要介绍的核心思想——<strong>有限状态机（Finite State Machine, FSM）</strong>。</p><p>状态机是一种强大的设计模式，简单来说，状态机思想的核心，就是把一个复杂的逻辑过程，拆分成有限个、互不重叠的“状态”，并明确定义在什么条件下（事件），从一个状态切换到另一个状态。通过它，我们可以将上面那团乱麻似的<code>if-else</code>，重构为结构清晰、易于维护的代码。它天然地适合用来处理像数据包解析这样，行为依赖于历史步骤的场景。</p>
<h2 id="">状态机实现</h2><h3 id="1-">1. 定义状态/命令</h3><p>用一个枚举类型 <code>enum</code> 定义所有可能的状态。初始状态为 <code>STATE_HEADER1</code> 等待包头1 (0xAA)。</p><pre class="language-c lang-c"><code class="language-c lang-c">typedef enum {
    STATE_HEADER1,      // 等待包头1 (0xAA)
    STATE_HEADER2,      // 等待包头2 (0x55)
    STATE_CMD,          // 等待命令字
    STATE_LEN,          // 等待数据长度
    STATE_DATA,         // 接收数据
    STATE_CHECKSUM      // 等待校验和
} ParserState;

static ParserState current_state = STATE_HEADER1; // 初始状态
</code></pre>
<p>定义协议命令常量。</p><pre class="language-c lang-c"><code class="language-c lang-c">#define PACKET_HEADER_1         0xAA
#define PACKET_HEADER_2         0x55
#define CMD_SET_LED             0x01
#define CMD_QUERY_LED           0x02
#define CMD_RESPONSE_LED        0x82
</code></pre>
<h3 id="2-">2. 状态机核心</h3><p>状态机核心在于 <code>parse_byte(uint8_t byte)</code> 函数。它的工作模式是<strong>逐字节驱动</strong>的</p><p>串口功能已将数据存入环形缓存区，并开始逐字节处理数据。</p><pre class="language-c lang-c"><code class="language-c lang-c">/**
 * @brief 蓝牙数据处理任务，调用状态机解析器
 */
void BT_Task()
{
    uint8_t temp_buffer[BT_PROCESS_BUFFER_SIZE]; // 用于临时存储从环形缓冲区读取的数据
    rt_size_t read_len;

    // 从环形缓冲区中读取数据
    read_len = rt_ringbuffer_get(&amp;uart3_rx_ringbuffer, temp_buffer, BT_PROCESS_BUFFER_SIZE);

    if (read_len &gt; 0)
    {
        // 逐个字节地送入解析器
        for (rt_size_t i = 0; i &lt; read_len; i++)
        {
            parse_byte(temp_buffer[i]);
        }
    }
}
</code></pre>
<pre class="language-c lang-c"><code class="language-c lang-c">void parse_byte(uint8_t byte)
{
    switch (current_state)
    {
        // 状态1: 等待 0xAA
        case STATE_HEADER1:
            if (byte == 0xAA) {
                current_state = STATE_HEADER2; // 收到，进入下一状态
            }
            break;

        // 状态2: 等待 0x55
        case STATE_HEADER2:
            if (byte == 0x55) {
                current_state = STATE_CMD; // 收到，进入下一状态
            } else {
                current_state = STATE_HEADER1; // 错误，重置回初始状态
            }
            break;

        // 状态3: 接收命令
        case STATE_CMD:
            packet.cmd = byte;
            current_state = STATE_LEN;
            break;
        // 状态4: 接收长度
        case STATE_WAIT_LEN:
            packet_len = byte;
            if (packet_len &gt; 0)
            {
                data_index = 0;
                parser_state = STATE_WAIT_DATA;
            }
            else // 如果数据长度为0
            {
                parser_state = STATE_WAIT_CHECKSUM;
            }
            break;
        // 状态5: 接收数据
        case STATE_WAIT_DATA:
            packet_buffer[data_index++] = byte;
            if (data_index &gt;= packet_len)
            {
                parser_state = STATE_WAIT_CHECKSUM;
            }
            break;
        // 状态6: 校验和验证
        case STATE_WAIT_CHECKSUM:
        {
            uint8_t checksum = 0;
            checksum += packet_cmd;
            checksum += packet_len;
            for (int i = 0; i &lt; packet_len; i++)
            {
                checksum += packet_buffer[i];
            }

            if (checksum == byte) // 校验成功
            {
                // 一个完整的数据包接收并校验成功！
                handle_packet();
            }
            else
            {
                // 校验失败，可以在此打印错误信息
                my_printf(&amp;huart1, &quot;Checksum error!\n&quot;);
            }
            // 无论成功与否，都重置状态机，准备接收下一个包
            parser_state = STATE_WAIT_HEADER_1;
            break;

    }
}

</code></pre>
<p>它每次只处理一个字节，然后根据 <code>current_state</code>（当前状态）来决定下一步做什么。</p><p><code>switch-case</code> 结构是实现状态机的绝佳方式，每个 <code>case</code> 就是一个独立的状态处理逻辑。</p><p><strong>初始状态 (<code>STATE_WAIT_HEADER_1</code>)</strong>:</p><ul><li>状态机启动后，它唯一的目标就是等待协议的第一个包头 <code>0xAA</code>。</li><li>一旦接收到 <code>0xAA</code>，它就完成了第一个任务，并将状态切换到 <code>STATE_WAIT_HEADER_2</code>。</li></ul><p><strong>等待第二个包头 (<code>STATE_WAIT_HEADER_2</code>)</strong>:</p><ul><li>此时，期望接收到第二个包头 <code>0x55</code>。</li><li>如果成功接收到 <code>0x55</code>，说明包头匹配成功，状态切换到 <code>STATE_WAIT_CMD</code>，准备接收命令。</li><li>如果接收到的不是 <code>0x55</code>，说明这是一个错误的包（可能只是数据中恰好包含了<code>0xAA</code>），会立刻<strong>重置状态机</strong>回到 <code>STATE_WAIT_HEADER_1</code>。</li></ul><p><strong>接收命令和长度 (<code>STATE_WAIT_CMD</code>, <code>STATE_WAIT_LEN</code>)</strong>:</p><ul><li>将字节存入相应的变量 (<code>packet_cmd</code>, <code>packet_len</code>)，然后无条件地切换到下一个状态。</li><li>如果 <code>packet_len</code> 为 0，会直接跳过数据接收阶段，进入校验和状态 <code>STATE_WAIT_CHECKSUM</code>。</li></ul><p><strong>接收数据 (<code>STATE_WAIT_DATA</code>)</strong>:</p><ul><li>这是一个“循环”状态。它会一直留在这个状态，直到接收满 <code>packet_len</code> 个字节的数据。</li><li><code>data_index</code> 变量在这里起到了计数器的作用。每接收一个字节，<code>data_index</code> 就加一。</li><li>当 <code>data_index</code> 等于 <code>packet_len</code> 时，表示数据部分接收完毕，状态切换到 <code>STATE_WAIT_CHECKSUM</code>。</li></ul><p><strong>校验和验证 (<code>STATE_WAIT_CHECKSUM</code>)</strong>:</p><ul><li>根据已接收的 <code>packet_cmd</code>、<code>packet_len</code> 和 <code>packet_buffer</code> 中的数据，重新计算一遍校验和。</li><li>将计算出的 <code>checksum</code> 与接收到的最后一个字节 <code>byte</code>进行比较。</li><li>如果相等，<strong>校验通过！</strong> 调用 <code>handle_packet()</code> 函数处理这个完整、正确的数据包。</li><li>如果不相等，说明数据在传输过程中发生了错误。</li><li>此时一个数据包的生命周期就结束了。状态机<strong>重置回 <code>STATE_WAIT_HEADER_1</code></strong>，等待下一个数据包的到来。</li></ul><h3 id="3-">3. 数据处理</h3><p>当状态机成功接收并校验完一个完整的数据包后（在 <code>STATE_CHECKSUM</code> 状态完成），它会调用 <code>handle_packet()</code> 函数来执行真正的业务逻辑。</p><pre class="language-c lang-c"><code class="language-c lang-c">void handle_packet(void)
{
    switch (packet_cmd)
    {
        case CMD_SET_LED:
        {
            uint8_t led_status_data = packet_buffer[0]; // 数据在packet_buffer的第一个字节
            set_led_status(led_status_data); // 调用LED控制函数
            
            // 可以在这里回显调试信息
            my_printf(&amp;huart1, &quot;CMD_SET_LED received, status: 0x%02X\n&quot;, led_status_data);
            
            break;
        }

        case CMD_QUERY_LED:
        {
            uint8_t current_led_status = get_led_status(); // 获取当前LED状态
            send_response_packet(CMD_RESPONSE_LED, &amp;current_led_status, 1); // 将状态打包回复
            
            // 调试信息
            my_printf(&amp;huart1, &quot;CMD_QUERY_LED received, responding with status: 0x%02X\n&quot;,current_led_status);
            break;
        }
        
        default:
            my_printf(&amp;huart1, &quot;Unknown CMD: 0x%02X\n&quot;, packet_cmd);
            break;
    }
}
</code></pre>
<h3 id="">改进</h3><p>对于更复杂的、安全性要求更高的场景，可以作以下改进：</p><ol start="1"><li><p>增加超时机制</p><p>如果只接受到半个包（例如，只发了包头和命令），状态机会永远停在中间状态（如 <code>STATE_WAIT_LEN</code>），无法自动复位。
增加一个超时定时器。在进入 <code>STATE_WAIT_HEADER_2</code> 时记录一下当前系统时间（例如 <code>HAL_GetTick()</code>）。在 <code>BT_Task</code> 的循环中，检查当前时间与记录时间的差值。如果超过了一个预设的阈值，就强制将 <code>parser_state</code> 复位到 <code>STATE_WAIT_HEADER_1</code>。</p></li><li><p>数据长度校验</p><p>如果错误地发送了一串超长的数据，超出缓存区发生<strong>溢出</strong>，导致数据丢失，程序出错。
在 <code>STATE_WAIT_LEN</code> 状态接收到 <code>packet_len</code> 后，立刻进行检查<code>if (packet_len &gt; BT_PROCESS_BUFFER_SIZE) </code>，若长度错误，重置状态机。</p></li></ol><h2 id="">总结</h2><p>有机会填上了“数据包解包/封包”这个之前一直想学却没深入的坑。</p><p>这次学习让我豁然开朗。“面条式” <code>if-else</code> 嵌套写法，在处理流式数据时会变得逻辑混乱、难以维护。</p><p>状态机的实现不仅让代码变得易于理解和扩展，并且很自然地实现了<strong>解析逻辑与业务逻辑的解耦</strong>。</p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/DevNotes/L-StateMachine#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/DevNotes/L-StateMachine</link><guid isPermaLink="true">https://tanejo.cn/posts/DevNotes/L-StateMachine</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Tue, 05 Aug 2025 03:52:59 GMT</pubDate></item><item><title><![CDATA[ST7735S 驱动的 LCD 屏幕开发笔记]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/tb_image_share_1753267810042_trans2.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20250723223834510.png"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20250724093005863_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20250724093030696_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/image-20250724093303453_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/IMG_20250712_165121_trans3.webp"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/DevNotes/st7735s-driver-lcd-screen-development-notes">https://tanejo.cn/posts/DevNotes/st7735s-driver-lcd-screen-development-notes</a></blockquote><div><p>ST7735S 是一款由台湾晶宏科技（Sitronix Technology Corp.）生产的单芯片 TFT LCD 驱动控制器。它被广泛应用于各种需要彩色显示功能的中小型电子设备中，以其高性价比、低功耗和易于集成的特点而备受工程师和爱好者的青睐。</p><h2 id="">数据手册</h2><p><a href="https://img.661231.xyz/st7735s-datasheet-v1.4.pdf">ST7735S 数据手册</a></p><h2 id="">屏幕模块</h2><p>我使用的是一块来自淘宝上购买的 1.8 寸，128*160 分辨率，SPI 协议的 TFT_LCD 屏幕模块，店铺链接 <a href="https://m.tb.cn/h.hPwnsUdpm0EdiwP">台湾铼宝[OLED]</a></p><p><img src="https://img.661231.xyz/tb_image_share_1753267810042_trans2.webp" alt="屏幕模块实物图"/></p><h3 id="">产品参数</h3><table><thead><tr><th style="text-align:left"> 项目 </th><th style="text-align:left"> 参数 </th></tr></thead><tbody><tr><td style="text-align:left"> 尺寸 </td><td style="text-align:left"> 1.8 英寸 TFT LCD </td></tr><tr><td style="text-align:left"> 分辨率 </td><td style="text-align:left"> 128RGB * 160 Dot-matrix </td></tr><tr><td style="text-align:left"> 通信接口 </td><td style="text-align:left"> SPI-4 wire interface </td></tr><tr><td style="text-align:left"> 驱动芯片 </td><td style="text-align:left"> ST7735S </td></tr><tr><td style="text-align:left"> 颜色 </td><td style="text-align:left"> 全彩 </td></tr><tr><td style="text-align:left"> 外形尺寸 </td><td style="text-align:left"> 34.00 x 56 x 3.65 (mm) </td></tr><tr><td style="text-align:left"> 显示区域 </td><td style="text-align:left"> 28.03 (W) * 35.04 (H) </td></tr><tr><td style="text-align:left"> 像素尺寸 </td><td style="text-align:left"> 0.219 (W) * 0.219 (H) </td></tr><tr><td style="text-align:left"> 工作电压 </td><td style="text-align:left"> 3.3V </td></tr><tr><td style="text-align:left"> 工作电流 </td><td style="text-align:left"> 30mA </td></tr><tr><td style="text-align:left"> 管脚数量 </td><td style="text-align:left"> 8Pin（2.54mm 间距排针） </td></tr><tr><td style="text-align:left"> 视角方向 </td><td style="text-align:left"> 12 点方向 </td></tr><tr><td style="text-align:left"> 工作温度 </td><td style="text-align:left"> -20 度到 70 度 </td></tr></tbody></table><h3 id="">接口定义</h3><table><thead><tr><th style="text-align:left"> 序号 </th><th style="text-align:left"> 符号 </th><th style="text-align:left"> 说明 </th></tr></thead><tbody><tr><td style="text-align:left"> 1 </td><td style="text-align:left"> GND </td><td style="text-align:left"> 电源地 </td></tr><tr><td style="text-align:left"> 2 </td><td style="text-align:left"> VCC </td><td style="text-align:left"> 电源正 3.3V </td></tr><tr><td style="text-align:left"> 3 </td><td style="text-align:left"> SCL </td><td style="text-align:left"> SPI 时钟线（与单片机 SPI_CLK 对接） </td></tr><tr><td style="text-align:left"> 4 </td><td style="text-align:left"> SDA </td><td style="text-align:left"> SPI 数据线（与单片机硬件 SPI_MOSI 对接） </td></tr><tr><td style="text-align:left"> 5 </td><td style="text-align:left"> RES </td><td style="text-align:left"> 复位管脚 </td></tr><tr><td style="text-align:left"> 6 </td><td style="text-align:left"> DC </td><td style="text-align:left"> 数据 / 命令选择引脚 </td></tr><tr><td style="text-align:left"> 7 </td><td style="text-align:left"> CS </td><td style="text-align:left"> SPI 片选 </td></tr><tr><td style="text-align:left"> 8 </td><td style="text-align:left"> BL </td><td style="text-align:left"> 背光控制开关，默认可不接 </td></tr></tbody></table>
<p>以上信息来自 <strong>台湾铼宝[OLED]</strong></p><hr/><h2 id="st7735-">ST7735 驱动库</h2><p>使用 <a href="https://github.com/afiskon">afiskon</a> 大佬的开源库 <a href="https://github.com/afiskon/stm32-st7735">stm32-st7735</a>，该库将 Adafruit 的 ST7735 库移植到了 STM32 平台上。
作者原文链接：<a href="https://eax.me/stm32-st7735/">https://eax.me/stm32-st7735/</a>，在使用之前推荐阅读。</p><p><img src="https://img.661231.xyz/image-20250723223834510.png" alt="开源库架构示意图"/></p><h3 id="">使用说明</h3><p>在 <code>st7735.h</code> 文件中，修改 SPI 句柄以及 GPIO 引脚定义：</p><p><img src="https://img.661231.xyz/image-20250724093005863_trans.webp" alt="引脚配置定义"/></p><p>在该文件中，作者提供了一些屏幕参数预设，可以根据你的屏幕规格取消对应宏定义的注释：</p><p><img src="https://img.661231.xyz/image-20250724093030696_trans.webp" alt="屏幕参数预设选择"/></p><p>对于我购买的这款屏幕，配置如下：</p><p><img src="https://img.661231.xyz/image-20250724093303453_trans.webp" alt="当前屏幕的具体配置"/></p>
<p>该宏定义表示当前使用的是 160×128 分辨率的屏幕。</p><pre class="language-c lang-c"><code class="language-c lang-c">#define ST7735_IS_160X128 1
</code></pre>
<p>定义屏幕的实际显示区域尺寸。</p><pre class="language-c lang-c"><code class="language-c lang-c">#define ST7735_WIDTH  128
#define ST7735_HEIGHT 160
</code></pre>
<p>定义显示内容的起始偏移量，坐标原点为左上角。</p><pre class="language-c lang-c"><code class="language-c lang-c">#define ST7735_XSTART 2
#define ST7735_YSTART 1
</code></pre>
<p>此宏定义了屏幕的旋转方向和颜色顺序：</p><pre class="language-c lang-c"><code class="language-c lang-c">#define ST7735_ROTATION (ST7735_MADCTL_MX | ST7735_MADCTL_MY | ST7735_MADCTL_RGB)
</code></pre><ul><li><strong>ST7735<em>MADCTL</em>MX</strong>: 启用 X 轴镜像</li><li><strong>ST7735<em>MADCTL</em>MY</strong>: 启用 Y 轴镜像</li><li><strong>ST7735<em>MADCTL</em>RGB</strong>: 设置 RGB 颜色顺序</li></ul><h3 id="">驱动函数说明</h3><h4 id="">初始化函数</h4><p><code>void ST7735_Init(void)</code></p><ul><li><strong>功能</strong>：初始化 ST7735 LCD 控制器。</li><li><strong>说明</strong>：必须在调用其他显示函数前执行。</li></ul><h4 id="">绘制像素点函数</h4><p><code>void ST7735_DrawPixel(uint16_t x, uint16_t y, uint16_t color)</code></p><ul><li><strong>参数</strong>：<code>x</code>, <code>y</code> 为坐标，<code>color</code> 为 16 位 RGB565 颜色。</li></ul><h4 id="">快速绘制填充矩形函数</h4><p><code>void ST7735_FillRectangleFast(uint16_t x, uint16_t y, uint16_t w, uint16_t h, uint16_t color)</code></p><ul><li><strong>说明</strong>：通过预准备整行数据传输，比普通填充函数更高效。</li></ul><h4 id="">文本显示函数</h4><p><code>void ST7735_WriteString(uint16_t x, uint16_t y, const char* str, FontDef font, uint16_t color, uint16_t bgcolor)</code></p><ul><li><strong>说明</strong>：支持自动换行，字体可在 <code>fonts.h</code> 中选择。</li></ul><h4 id="">反转颜色函数</h4><p><code>void ST7735_InvertColors(bool invert)</code></p><ul><li><strong>功能</strong>：开启或关闭显示颜色反转。</li></ul><h4 id="">设置伽马值函数</h4><p><code>void ST7735_SetGamma(uint8_t gamma)</code></p><ul><li><strong>功能</strong>：调整显示的亮度和对比度。</li></ul><hr/><h2 id="">显示效果</h2><p>图中是我使用 <code>ST7735_DrawImage()</code> 函数将一张 128*160 的图片显示到 LCD 屏幕上的实拍效果。</p><p>推荐图片取模软件：<a href="https://www.bilibili.com/video/BV1Qj411H7Ww/">【硬件图片取模软件！支持彩色/阈值调整】</a></p><p><img src="https://img.661231.xyz/IMG_20250712_165121_trans3.webp" alt="显示效果实拍"/></p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/DevNotes/st7735s-driver-lcd-screen-development-notes#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/DevNotes/st7735s-driver-lcd-screen-development-notes</link><guid isPermaLink="true">https://tanejo.cn/posts/DevNotes/st7735s-driver-lcd-screen-development-notes</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Sat, 26 Jul 2025 07:14:14 GMT</pubDate></item><item><title><![CDATA[Github学生认证申请]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/github-edu-0_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/github-edu-1_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/github-edu-2_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/github-edu-3_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/github-edu-4_trans.webp"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/Sharing/github-edu-cert">https://tanejo.cn/posts/Sharing/github-edu-cert</a></blockquote><div><p><strong>本文章非教程，仅分享我的申请过程，以供参考</strong></p><h1 id="1">1.前期准备</h1><h2 id="11-examplexxxxeducnmailtoexamplexxxxeducn">1.1 申请教育邮箱(<a href="mailto:example@xxxx.edu.cn">example@xxxx.edu.cn</a>)</h2><p>使用教育邮箱申请学生认证通过率会大大提高。
我所在的学校在教务系统中开放了教育邮箱的申请，且申请门槛较低，在校就读期间可以免费使用。
拿到教育邮箱后，将你的教育邮箱绑定到你的 Github 账户。<a href="https://github.com/settings/emails">绑定邮箱</a></p><h2 id="12-">1.2 添加付款信息</h2><p>在Github主页，点击头像→Settings→Billing and plans→Payment information 或 <a href="https://github.com/settings/billing/payment_information">直达链接</a>
添加个人付款信息并保存（姓名要和之后提交的证明资料中一致)</p><h2 id="13-">1.3 添加双重验证方式</h2><p>在Settings→Password and authentication→Two-factor authentication中 或 点击此链接<a href="https://github.com/settings/security">双重验证</a>
方式不作赘述，若需教程请自行搜索。</p><h2 id="14-">1.4 关闭网络代理，设置位置权限</h2><p>一定要关闭代理，尽量使用校园网，并允许浏览器获取你的位置信息，Github会进行定位，以辅助验证。</p><h1 id="2">2.开始申请</h1><h2 id="21--github-education-httpseducationgithubcom">2.1 进入 Github Education <a href="https://education.github.com/">直达链接</a></h2><p><img src="https://img.661231.xyz/github-edu-0_trans.webp" alt="github-edu-0" height="842" width="1528"/></p><p>点击&quot; Join Github Education → &quot;跳转至申请页面
<img src="https://img.661231.xyz/github-edu-1_trans.webp" alt="github-edu-1" height="763" width="1548"/></p><p>选择&quot;Student&quot; 并下拉页面至&quot;Application&quot;
<img src="https://img.661231.xyz/github-edu-2_trans.webp" alt="github-edu-2" height="597" width="668"/></p><p>此时如果你正确绑定了你的教育邮箱，会高亮提示以下字段</p><blockquote><p>You have verified the email address <strong><a href="mailto:example@xxxx.edu.cn">example@xxxx.edu.cn</a></strong> on your GitHub account. That academic domain is associated with the school <strong>xxxxxxx University</strong>.</p></blockquote>
<p>系统会匹配你的大学名称，并自动填写（若没有自动填充，请自行填写大学的官方英文名称，系统会有补全）
<img src="https://img.661231.xyz/github-edu-3_trans.webp" alt="github-edu-3" height="209" width="730"/></p><p>确认自己的大学名称无误后，点击&quot;continue&quot;进入下一步。</p><h2 id="22-">2.2 提交证明资料</h2><p><img src="https://img.661231.xyz/github-edu-4_trans.webp" alt="github-edu-4"/></p><p>根据其它成功案例和个人经验，总结出的可选的证明资料有:</p><ul><li>学信网教育部学籍在线验证报告</li><li>带有学校名称，学校公章（如有），个人信息及有效期的学生证</li><li>带有学校名称，入学日期和学校公章的录取通知书</li><li>其他认证材料(未知)</li></ul><p>上传后,点击绿色&quot;Process my application&quot;按钮提交申请</p><p>此时已经完成Github学生认证申请，接下来请等待审核，结果将在<a href="https://education.github.com/discount_requests/application">GitHub Benefits application</a>显示并发送至你的邮箱中。</p><p>我的申请过程比较顺利，我选择了使用录取通知书进行验证，仅上传了原件照片，未进行翻译，就很快通过了。
若你的申请被驳回，Github会给你发送一封申请失败的邮件，按照里面的提示进行调整后再试。
可以试试在原件照片旁附上英文翻译等等。</p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/Sharing/github-edu-cert#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/Sharing/github-edu-cert</link><guid isPermaLink="true">https://tanejo.cn/posts/Sharing/github-edu-cert</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Wed, 23 Oct 2024 16:30:17 GMT</pubDate></item><item><title><![CDATA[一次维权]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/SliceOfLife/my-rights-claim">https://tanejo.cn/posts/SliceOfLife/my-rights-claim</a></blockquote><div><p>一个月之前在去哪儿网上预订了两晚酒店，结果昨天傍晚酒店方电联我，告知酒店因下暴雨被淹，不能入住，想让我主动取消，我当即就表达了不满，现在附近的订酒店均涨价许多，如果我取消，要自己承担涨价部分，酒店方在电话里居然还说“我们这边取消是要扣钱的，您取消我们没有损失”，这句话让我很恼火，要我当老好人吃亏，我肯定不能接受。
挂断电话，联系了平台维权，平台处理的很快，10分钟不到给我回了电话，告知我酒店确实不能接待，给我提出了一个赔偿方案:将原订单退款，并提供最高108元的差价补偿，也就是让我自己重新订酒店，新酒店订单价-原订单价 不超108元，都可以赔付。
我浏览了附近酒店的现价，发现这108元还是不能弥补涨价部分的损失，我没有同意上述方案，客服在10分钟后再次来电，这次补偿金额来到了最多400元，足以弥补差价，我最终接受了这个补偿方案。</p>
<p>《‌中华人民共和国民法典》第五百八十二条规定：履行不符合约定的，应当按照当事人的约定承担违约责任。对违约责任没有约定或者约定不明确，依据本法第五百一十条的规定仍不能确定的，受损害方根据标的的性质以及损失的大小，可以合理选择请求对方承担修理、重作、更换、退货、减少价款或者报酬等违约责任。根据以上规定，如果酒店未能按照约定提供服务，应承担违约责任。消费者可以要求酒店按照实际损失进行赔偿，包括但不限于额外住宿费用、交通费用等。
在这种情况下，一定要联系平台维权，平台提出的赔偿方案肯定有商量的余地，可以尽量争取一下更多的赔偿。</p><p>去哪儿网也对我这次赔偿提出了一些限定条件，需要注意一下</p><ol start="1"><li>所谓&quot;三项&quot;一致，即订房人，电话号码，订房城市</li><li>订房日期一致</li><li>需提交发票和订单信息截图</li></ol></div><p style="text-align:right"><a href="https://tanejo.cn/posts/SliceOfLife/my-rights-claim#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/SliceOfLife/my-rights-claim</link><guid isPermaLink="true">https://tanejo.cn/posts/SliceOfLife/my-rights-claim</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Wed, 28 Aug 2024 11:00:15 GMT</pubDate></item><item><title><![CDATA[距离黑神话:悟空正式发售还有七天，先来跑下官方性能测试工具]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/wukong-benchmark-page_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/wukong-benchmark.gif"/><link rel="preload" as="image" href="https://img.661231.xyz/wukong-benchmark-1_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/wukong-benchmark-2_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/wukong-benchmark-3_trans.webp"/><link rel="preload" as="image" href="https://img.661231.xyz/battle-scene_trans.webp"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/Game/blm-benchmark">https://tanejo.cn/posts/Game/blm-benchmark</a></blockquote><div><h2 id="-">黑神话：悟空 性能测试工具</h2><h2 id="black-myth-wukong-benchmark-tool">Black Myth: Wukong Benchmark Tool</h2><p>在09:51 GMT+8（北京时间），游戏科学在Steam发布了黑神话：悟空 性能测试工具</p><p><img src="https://img.661231.xyz/wukong-benchmark-page_trans.webp" alt="黑神话：悟空 性能测试工具" height="563" width="1064"/></p><p>打开工具，屏幕映出“游戏科学”的 LOGO ，紧接着是&quot;悟空&quot;（怡宝）[doge] 的游戏艺术 LOGO，下面写着“按任意键开始”。
明明打开之前知道只是“性能测试工具”，但看到开始界面就好像马上能玩到备受期待的国产3A《黑神话：悟空》，让我感到一阵激动。</p><p><img src="https://img.661231.xyz/wukong-benchmark.gif" alt="wukong-benchmark.gif" height="720" width="1280"/></p><p>该工具通过实时渲染一段游戏内场景来测试 PC 硬件性能，不得不说游戏科学的这个做法十分聪明，在发售前一周放出性能测试工具，让迫不及待等待游戏发售的玩家们提前体验到游戏的视觉盛宴，同时也为游戏科学提供了丰富的样本数据来进行发布前的最终优化调整。当然，性能测试工具的发布肯定会吸引一波流量，毕竟玩家们都想知道自己的电脑是否能流畅运行《黑神话：悟空》，在我来看，这也是一次不错的营销。</p><h2 id="">性能测试</h2><p>游戏科学放出的《黑神话：悟空》实机演示已经证明了其在虚幻5的加持下的优秀游戏画面，但要注意的是，如果优化做得糟糕，那么玩家也一定不会买账。</p><p>话不多说，先来看看我的跑分。</p><p><strong><em>测试平台：华硕天选5Pro 锐龙版</em></strong></p><p><strong><em>CPU：AMD Ryzen 9 7940HX</em></strong></p><p><strong><em>GPU：NVIDIA GeForce RTX 4060 Laptop GPU</em></strong></p><p><strong><em>RAM：32GB DDR5 5200</em></strong></p><p><img src="https://img.661231.xyz/wukong-benchmark-1_trans.webp" alt="1080P 推荐画质（全高）开启DLSS 超采样分辨率67（质量档）" height="1080" width="1920"/></p>
<p>这大概率就是我游玩正式版时的画面设置了，下面我想测试一下我能接受的底线，也就是稳定60fps游玩的情况，能达到什么样的画面水平，如下图</p><hr/><p><img src="https://img.661231.xyz/wukong-benchmark-2_trans.webp" alt="2560*1600(2.5k) 超高画质预设 开启DLSS 超采样分辨率45（性能档）" height="1080" width="1920"/></p>
<hr/><p><img src="https://img.661231.xyz/wukong-benchmark-3_trans.webp" alt="2560*1600(2.5k) 电影级画质预设 开启DLSS 超采样分辨率40（性能档）"/></p>
<hr/><p>在超高画质预设与电影级画质预设下，我都是经过多次测试，调整超采样分辨率，以满足帧数在60fps左右。
dlss开到性能档，在细节方面的确会有所丢失，但对我这个不追求画质的人来说，是满意的。</p><p>但是，不能高兴的过早，官方这次放出的性能测试工具仅提供了一个跑分场景，大多是对水、树、建筑的渲染，有少量人物。
并没有出现之前pv中的特效较多的战斗场景，所以游戏真正的性能表现，我们只能在正式版发布之后看到了。</p><p><img src="https://img.661231.xyz/battle-scene_trans.webp" alt="battle-scene"/></p><h2 id="">小结</h2><p>在写这篇文章时，距离《黑神话：悟空》发售仅剩六天多一点，我第一次注意到这款国产3a是在2020年8月20日，是黑神话悟空在B站发布第一个游戏实机演示的日子，这款游戏给了包括我的所有玩家一个国产游戏崛起的希望，《黑神话：悟空》真正触动我的点在于中国故事，它给世人展现了中华文化的丰富底蕴，它架起了跨文化交流的桥梁，不仅继承了传统，还创新地表达了中国文化。
之后游戏科学放出的每个实机演示也都火爆全网，总能给人惊喜，导致人们对它期望越来越高，甚至有人给它提前安排了<strong>Game of the Year</strong>的头衔，这无疑体现了《黑神话：悟空》的魅力，但有了2077的前车之鉴，我们应该学会理性一点，<strong>期望越大，失望越大</strong>这句话不无道理。</p><p>8月20日，让我们共同见证《黑神话：悟空》的问世。</p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/Game/blm-benchmark#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/Game/blm-benchmark</link><guid isPermaLink="true">https://tanejo.cn/posts/Game/blm-benchmark</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Mon, 12 Aug 2024 16:18:15 GMT</pubDate></item><item><title><![CDATA[网站图床方案:Cloudflare R2 + PicGo]]></title><description><![CDATA[<link rel="preload" as="image" href="https://img.661231.xyz/picgo-s3_trans2.webp"/><div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://tanejo.cn/posts/WebsiteBuilding/cloudflare-r2-picgo-image-hosting">https://tanejo.cn/posts/WebsiteBuilding/cloudflare-r2-picgo-image-hosting</a></blockquote><div><h2 id="">前言</h2><p>关于图床方案我尝试过如下几种，均搭配PicGo使用</p><ol start="1"><li>免费图床网站，如<a href="https://sm.ms/">sm.ms</a>，虽然PicGo插件支持，但管理略麻烦，且文件安全无保障</li><li>七牛云对象储存，每月免费10GB空间，缺点是仅提供临时域名且自定义域名需备案。在使用过程中碰到过图片无法正常显示等小问题，总的来说需操心的地方较多，遂放弃。</li><li>Github图床+jsDelivr CDN，国内访问速度较慢，若使用国内优化CDN增加成本</li><li>阿里/腾讯等国内服务商对象存储，速度有保障，管理方便，缺点当然是收费...</li></ol><h2 id="cloudflare-r2--picgo">Cloudflare R2 + PicGo</h2><p>来自赛博菩萨Cloudflare的对象存储服务,Free计划中有每月10GB的存储容量。
作为个人博客的图床，其空间完全够用，且大厂服务数据安全有保障。
可使用自定义域名，无需备案。
通过cf cdn加速，国内用户使用速度虽不如国内服务商，但经测试还是合格的。
而且Cloudflare R2兼容S3 API，在PicGo中安装Amazon S3插件，可完美兼容。
<img src="https://img.661231.xyz/picgo-s3_trans2.webp" alt="picgo-s3" height="450" width="800"/></p></div><p style="text-align:right"><a href="https://tanejo.cn/posts/WebsiteBuilding/cloudflare-r2-picgo-image-hosting#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://tanejo.cn/posts/WebsiteBuilding/cloudflare-r2-picgo-image-hosting</link><guid isPermaLink="true">https://tanejo.cn/posts/WebsiteBuilding/cloudflare-r2-picgo-image-hosting</guid><dc:creator><![CDATA[tanejo]]></dc:creator><pubDate>Mon, 12 Aug 2024 14:17:50 GMT</pubDate></item></channel></rss>