<?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[会飞的猪的小屋]]></title><description><![CDATA[哈喽~欢迎光临]]></description><link>https://blog.flyyingpiggy2020.cn</link><image><url>https://blog.flyyingpiggy2020.cn/innei.svg</url><title>会飞的猪的小屋</title><link>https://blog.flyyingpiggy2020.cn</link></image><generator>Shiro (https://github.com/Innei/Shiro)</generator><lastBuildDate>Sun, 23 Aug 2026 20:56:18 GMT</lastBuildDate><atom:link href="https://blog.flyyingpiggy2020.cn/feed" rel="self" type="application/rss+xml"/><pubDate>Sun, 23 Aug 2026 20:56:18 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[[esp32] Captive Portal]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-captive-portal">https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-captive-portal</a></blockquote><div><h1 id="esp32-captive-portal-">ESP32 Captive Portal 自动弹出原理</h1><h2 id="1-">1. 功能概述</h2><p>手机连接 ESP32 的 SoftAP 后，Android 或 iOS 会自动弹出配网页面。</p><p>这个页面并不是 ESP32 主动命令手机打开的，而是手机操作系统检测到当前 Wi-Fi 存在 Captive Portal 后，主动打开系统配网窗口。</p><p>当前实现结合了以下三种机制：</p><ol start="1"><li>DHCP Option 114 声明配网页面地址。</li><li>DNS 劫持将所有域名解析到 ESP32。</li><li>HTTP Server 将未知路径重定向到配网页面。</li></ol><h2 id="2--wi-fi-">2. 手机连接 Wi-Fi 后的行为</h2><p>手机连接 Wi-Fi 后，通过 DHCP 获取以下网络参数：</p><ul><li>IP 地址</li><li>子网掩码</li><li>默认网关</li><li>DNS Server</li><li>Captive Portal 地址</li></ul><p>ESP32 SoftAP 通常分配：</p><pre class="language-text lang-text"><code class="language-text lang-text">手机地址：192.168.4.2
网关地址：192.168.4.1
DNS 地址：192.168.4.1
</code></pre>
<p>ESP32 还通过 DHCP Option 114 告诉手机：</p><pre class="language-text lang-text"><code class="language-text lang-text">Captive Portal URL = http://192.168.4.1/
</code></pre>
<p>支持 DHCP Option 114 的操作系统可以直接知道配网页面的地址。</p><h2 id="3-">3. 操作系统联网检测</h2><p>手机连接 Wi-Fi 后，会主动访问操作系统预设的 HTTP 地址，判断当前网络是否能够访问互联网。</p><p>常见检测地址如下：</p><pre class="language-text lang-text"><code class="language-text lang-text">Android:
http://connectivitycheck.gstatic.com/generate_204

iOS:
http://captive.apple.com/hotspot-detect.html

Windows:
http://www.msftconnecttest.com/connecttest.txt
</code></pre>
<p>操作系统期望收到特定响应：</p><table><thead><tr><th> 系统 </th><th> 预期响应 </th></tr></thead><tbody><tr><td> Android </td><td> HTTP 204 No Content </td></tr><tr><td> iOS </td><td> 包含固定 <code>Success</code> 内容的页面 </td></tr><tr><td> Windows </td><td> 固定的文本内容 </td></tr></tbody></table><p>如果收到预期响应，系统认为网络可以正常访问互联网。</p><p>如果收到重定向、配网页面或其他内容，系统会认为当前网络需要进行网页登录认证。</p><h2 id="4-dns-">4. DNS 劫持</h2><p>ESP32 在 UDP 53 端口运行 DNS Server。</p><p>DNS Server 会将客户端发来的所有 IPv4 A 记录查询都解析为 SoftAP 地址：</p><pre class="language-text lang-text"><code class="language-text lang-text">任意域名 -&gt; 192.168.4.1
</code></pre>
<p>例如手机查询：</p><pre class="language-text lang-text"><code class="language-text lang-text">connectivitycheck.gstatic.com
</code></pre>
<p>ESP32 返回：</p><pre class="language-text lang-text"><code class="language-text lang-text">connectivitycheck.gstatic.com -&gt; 192.168.4.1
</code></pre>
<p>因此手机原本发往互联网的联网检测请求，最终会发送到 ESP32 的 HTTP Server。</p><h2 id="5-http-">5. HTTP 重定向</h2><p>手机可能访问以下路径：</p><pre class="language-text lang-text"><code class="language-text lang-text">/generate_204
/hotspot-detect.html
/connecttest.txt
</code></pre>
<p>ESP32 当前只提供根路径：</p><pre class="language-text lang-text"><code class="language-text lang-text">/
</code></pre>
<p>当 HTTP Server 收到未注册的路径时，返回：</p><pre class="language-http lang-http"><code class="language-http lang-http">HTTP/1.1 303 See Other
Location: /
</code></pre>
<p>手机随后访问：</p><pre class="language-text lang-text"><code class="language-text lang-text">http://192.168.4.1/
</code></pre>
<p>ESP32 返回完整的配网页面。</p><h2 id="6-">6. 完整流程</h2><pre class="language-text lang-text"><code class="language-text lang-text">手机连接 ESP32 SoftAP
        |
        v
通过 DHCP 获取 IP、网关和 DNS
        |
        v
DHCP Option 114 声明配网页面地址
        |
        v
手机发起系统联网检测
        |
        v
查询 connectivitycheck.gstatic.com 等域名
        |
        v
ESP32 DNS 返回 192.168.4.1
        |
        v
联网检测请求到达 ESP32 HTTP Server
        |
        v
未知路径返回 HTTP 303
        |
        v
重定向到根路径 /
        |
        v
ESP32 返回配网页面
        |
        v
系统判断当前 Wi-Fi 存在 Captive Portal
        |
        v
Android/iOS 自动弹出系统配网窗口
</code></pre>
<h2 id="7-">7. 各模块职责</h2><h3 id="dhcp-server">DHCP Server</h3><p>负责向客户端分配网络参数，并通过 Option 114 声明配网页面：</p><pre class="language-text lang-text"><code class="language-text lang-text">http://192.168.4.1/
</code></pre>
<h3 id="captive-dns-server">Captive DNS Server</h3><p>监听 UDP 53 端口，将所有 A 记录查询解析为：</p><pre class="language-text lang-text"><code class="language-text lang-text">192.168.4.1
</code></pre>
<h3 id="http-server">HTTP Server</h3><p>负责：</p><ul><li>返回配网页面。</li><li>处理手机的联网检测请求。</li><li>将未知路径重定向到 <code>/</code>。</li></ul><h3 id="androidios">Android/iOS</h3><p>负责：</p><ul><li>主动检测网络连通性。</li><li>判断是否存在 Captive Portal。</li><li>打开系统配网窗口。</li><li>配网完成后重新检查互联网连接。</li></ul><h2 id="8--http--https">8. 为什么使用 HTTP 而不是 HTTPS</h2><p>Captive Portal 主要依赖 HTTP 联网检测。</p><p>如果手机访问：</p><pre class="language-text lang-text"><code class="language-text lang-text">https://example.com
</code></pre>
<p>浏览器要求服务器提供与 <code>example.com</code> 匹配的 TLS 证书。</p><p>ESP32 无法提供任意网站的合法证书，因此 HTTPS 请求会产生证书错误，不能透明重定向到配网页面。</p><p>因此可以认为：</p><pre class="language-text lang-text"><code class="language-text lang-text">普通 HTTP 请求可以被引导到配网页面。
HTTPS 请求不能可靠劫持和重定向。
</code></pre>
<h2 id="9-">9. 配网页面不一定始终自动弹出</h2><p>是否自动弹出最终由手机操作系统决定，以下情况可能影响弹窗：</p><ul><li>手机启用了 Private DNS、DoH 或 DoT。</li><li>手机缓存了之前的 DNS 结果。</li><li>手机自动切换到蜂窝网络。</li><li>操作系统厂商修改了联网检测机制。</li><li>手机已经记住了该 Wi-Fi 的检测结果。</li><li>浏览器访问的是 HTTPS 或启用了 HSTS 的网站。</li></ul><p>即使系统没有自动弹窗，用户仍然可以手动访问：</p><pre class="language-text lang-text"><code class="language-text lang-text">http://192.168.4.1/
</code></pre>
<p>进入配网页面。</p><h2 id="10-">10. 当前实现总结</h2><p>当前 ESP32 Captive Portal 使用：</p><pre class="language-text lang-text"><code class="language-text lang-text">SoftAP
+ DHCP Option 114
+ 通配 DNS 解析
+ HTTP 303 重定向
+ HTML 配网页面
</code></pre>
<p>其本质是：</p><blockquote><p>ESP32 将手机的联网检测请求引导到本地 HTTP Server，操作系统发现响应不符合互联网连通性检测要求，因此判定该 Wi-Fi 需要认证，并自动打开配网页面。</p></blockquote></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-captive-portal#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-captive-portal</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-captive-portal</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Fri, 14 Aug 2026 09:04:07 GMT</pubDate></item><item><title><![CDATA[[esp32] http server]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-http-server">https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-http-server</a></blockquote><div><p>esp32上搭建http server 利用 web_server加上SoftAP进行路由配网。</p><p>httpd_register_uri_handler 这个函数可以用于注册http方法。</p><p>此函数入参为代表了HTTP_GET这个方法就会进入handler这个函数</p><pre class="language-c lang-c"><code class="language-c lang-c">static const httpd_uri_t s_root_uri = {
    .uri = &quot;/&quot;,
    .method = HTTP_GET,
    .handler = http_server_root_get_handler,
    .user_ctx = NULL,
};
</code></pre>
<p>在这个函数的resp中返回html格式的应答。</p><p><code>html</code>可以 通过CmakeLists中<code>EMBED_FILES</code>添加：</p><pre class="language-text lang-text"><code class="language-text lang-text">idf_component_register(
    SRCS
        ...
    INCLUDE_DIRS
        ...
    EMBED_FILES
        &quot;http/web/index.html&quot;
)
</code></pre>
<p>通过这种方式可以访问：</p><pre class="language-c lang-c"><code class="language-c lang-c">extern const uint8_t s_index_html_start[] asm(&quot;_binary_index_html_start&quot;);
extern const uint8_t s_index_html_end[] asm(&quot;_binary_index_html_end&quot;);
</code></pre>
<p>更加复杂点的，可以做一个小型文件系统，放入带有目录格式的网页代码</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-http-server#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-http-server</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/smart_home/esp32-http-server</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Fri, 14 Aug 2026 08:45:30 GMT</pubDate></item><item><title><![CDATA[[docker] 查看mqtt信息]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/debug/docker-mqtt-info">https://blog.flyyingpiggy2020.cn/posts/debug/docker-mqtt-info</a></blockquote><div><p><code>dofc-mqtt</code>是我开的一个<code>containers</code>的名称</p><p><code>docker logs -f --tail=100 dofc-mqtt</code>这条命令可以查看日志</p><p><code>docker exec -it dofc-mqtt mosquitto_sub -h 127.0.0.1 -p 1883 -t &quot;dofc/device/dofc-motor-3C0F02C9B78C/config/state&quot; -v</code> 可以查看对应topic的报文</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/debug/docker-mqtt-info#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/debug/docker-mqtt-info</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/debug/docker-mqtt-info</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Thu, 06 Aug 2026 05:11:54 GMT</pubDate></item><item><title><![CDATA[外骨骼遇到的问题-自激震荡]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/other/exoskeleton-self-excited-oscillation-problems">https://blog.flyyingpiggy2020.cn/posts/other/exoskeleton-self-excited-oscillation-problems</a></blockquote><div><blockquote><p>The use of large assistive torques is particularly problematic because the residual motion created by the device can be registered as user motion thus creating an unstable oscillation. This poses safety problems to those with poor balance.</p></blockquote>
<p>在很多篇文章里都提到了，当辅助力矩过大时，外骨骼自身的震荡被识别成使用者自身的运动，从而产生不稳定的震荡。</p><p>之前我自己鼓捣的方法，就是无法克服这个问题。</p><p>但是看论文里面很多都说自己能克服这个问题，我抄袭过两篇的做法，实际无法复现。。不知道是因为我自己的水平问题，还是说论文本身作者就有点不诚实，改了数据。</p><hr/><p>在网友帮助下，得到了一篇文章，解决了力矩如何输出的问题。这个算法在力矩大的时候仍然会震荡。但是论文上有说明，这个方法是最后是收敛的。</p><p>然后我配合了启动停止的识别，在停止时随着时间衰减力矩增益系数，解决了这个问题。</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/other/exoskeleton-self-excited-oscillation-problems#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/other/exoskeleton-self-excited-oscillation-problems</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/other/exoskeleton-self-excited-oscillation-problems</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Mon, 03 Aug 2026 07:35:36 GMT</pubDate></item><item><title><![CDATA[加速度传感器获取位移]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/other/accelerometer-displacement-measurement">https://blog.flyyingpiggy2020.cn/posts/other/accelerometer-displacement-measurement</a></blockquote><div><p>如何使用加速度传感器获取位移？</p><p>首先要先把数据从<code>本体坐标系</code>转换到<code>世界坐标系</code></p><p>转换完毕之后，对<code>世界加速度</code>做处理，去掉Z轴上恒定的重力加速度得到<code>线性加速度</code></p><p>静止状态下，<code>线性加速度</code>应该为0。</p><p><code>线性加速度</code>积分得到速度，再对速度做积分得到位移。</p><p>这个过程中由于进行了二次积分，原来加速度数据中很小的残差，进行二次积分过后会导致很大的差异。此时你得到的数据，在50cm的尺子上测试，大概误差会在50%以上的误差。</p><p>这个时候假设加速度残差是恒定的，用 <code>0.5 * v_end * T</code>  来修正最终误差，其中<code>v_end</code>为末速度。</p><p>进行修正之后，误差在20%之间，也就是说移动50cm，可能是55cm，也可能是60cm。</p><p>接下来我也不知道如何去优化了，对这个结果最终不满意，不过我观察数据也不知道更准确的思路了。</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/other/accelerometer-displacement-measurement#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/other/accelerometer-displacement-measurement</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/other/accelerometer-displacement-measurement</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Tue, 28 Jul 2026 07:18:50 GMT</pubDate></item><item><title><![CDATA[[git] git hook]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/debug/git-git_hook">https://blog.flyyingpiggy2020.cn/posts/debug/git-git_hook</a></blockquote><div><pre class="language-bash lang-bash"><code class="language-bash lang-bash">
#!/bin/sh
#pre-commit
set -eu

echo &quot;Running project formatter...&quot;

if command -v pwsh &gt;/dev/null 2&gt;&amp;1; then
    pwsh -NoProfile -File &quot;tools/format_code.ps1&quot;
elif command -v powershell.exe &gt;/dev/null 2&gt;&amp;1; then
    powershell.exe -NoProfile -ExecutionPolicy Bypass -File &quot;tools/format_code.ps1&quot;
elif command -v powershell &gt;/dev/null 2&gt;&amp;1; then
    powershell -NoProfile -ExecutionPolicy Bypass -File &quot;tools/format_code.ps1&quot;
else
    echo &quot;PowerShell was not found. Run tools/format_code.ps1 manually before committing.&quot;
    exit 1
fi

if ! git diff --quiet --; then
    echo &quot;&quot;
    echo &quot;Formatting changed files or unstaged changes are present.&quot;
    echo &quot;Review the diff, stage the formatted files, then commit again:&quot;
    echo &quot;&quot;
    echo &quot;  git diff&quot;
    echo &quot;  git add &lt;files&gt;&quot;
    echo &quot;  git commit&quot;
    echo &quot;&quot;
    git diff --name-only --
    exit 1
fi

echo &quot;Formatting check passed.&quot;
</code></pre>
<p>使用<code>git hook</code>可以在git操作的各个阶段进行操作。</p><p>例如我这里的<code>pre-commit</code>可以在<code>commit</code>提交之前执行脚本。</p><p>这里执行了一次<code>clang-format</code>，判断代码是否格式化。</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/debug/git-git_hook#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/debug/git-git_hook</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/debug/git-git_hook</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Wed, 08 Jul 2026 02:56:51 GMT</pubDate></item><item><title><![CDATA[等比数列求公式]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/basic/sum_of_geo_seq">https://blog.flyyingpiggy2020.cn/posts/basic/sum_of_geo_seq</a></blockquote><div><p>在复习卷积时候用到的，已经忘记了。</p><p>等比数列的求和公式主要分为公比 $q \neq 1$ 和 $q = 1$ 两种情况。</p><p>常规求和公式（从第 1 项加到第 n 项）</p><p>假设等比数列的首项是 $a<em>1$，公比是 $q$，前 $n$ 项的和记为 $S</em>n$：</p><p>当 $q \neq 1$ 时：</p><p>$S<em>n = \frac{a</em>1(1 - q^n)}{1 - q}$</p><p>或者写成：</p><p>$S<em>n = \frac{a</em>1 - a_n q}{1 - q}$</p><p>当 $q = 1$ 时：</p><p>$S<em>n = n \cdot a</em>1$</p><p>从 $n=0$ 加到 $n=k$ 的情况</p><p>你提到的“最后是 $q^{k+1}$ 吗？”这个直觉非常准确。在最终的求和公式里确实会出现 $q^{k+1}$，但我们需要稍微区分一下“加式的最后一项”和“公式里的指数”。</p><p>假设通项公式为 $a \cdot q^n$，我们从 $n=0$ 开始累加到 $n=k$：</p><p>$S = a \cdot q^0 + a \cdot q^1 + a \cdot q^2 + \dots + a \cdot q^k$</p><p>这里有两个关键细节：</p><p>累加的最后一项： 是 $a \cdot q^k$，并不是 $q^{k+1}$。</p><p>总项数： 从 $0$ 数到 $k$，总共有 $k+1$ 项（这是一个常见的“差一错误”陷阱）。</p><p>根据等比数列的求和原理，公式中的指数代表的是总项数。将“首项为 $a$”、“项数为 $k+1$”代入求和公式中（假设 $q \neq 1$），最终结果为：</p><p>$S = \frac{a(1 - q^{k+1})}{1 - q}$</p><p>总结： 你的理解在公式推导的层面上是完全正确的！如果你从 $n=0$ 累加到 $n=k$，求和公式的分子中必然包含 $q^{k+1}$。</p><hr/><p>这是一道关于离散时间序列卷积的经典信号与系统题目。我们可以通过离散卷积的定义公式来分步求解。</p><p>已知两个序列：</p><p>$x[n] = (\frac{1}{2})^n u[n] + 2^n u[-n]$</p><p>$h[n] = u[n]$</p><p>目标是求卷积 $y[n] = x[n] * h[n]$。</p><ol start="1"><li>设立卷积和公式</li></ol><p>根据离散时间卷积的定义：</p><p>$y[n] = \sum_{k=-\infty}^{\infty} x[k] h[n-k]$</p><p>将 $x[n]$ 和 $h[n]$ 代入公式中：</p><p>$y[n] = \sum_{k=-\infty}^{\infty} \left[ (\frac{1}{2})^k u[k] + 2^k u[-k] \right] u[n-k]$</p><p>利用求和的线性性质，我们可以将其分为两部分分别求解：</p><p>$y[n] = y<em>1[n] + y</em>2[n]$</p><p>其中：</p><p>$y<em>1[n] = \sum</em>{k=-\infty}^{\infty} (\frac{1}{2})^k u[k] u[n-k]$</p><p>$y<em>2[n] = \sum</em>{k=-\infty}^{\infty} 2^k u[-k] u[n-k]$</p><ol start="3"><li>求解 $y_1[n]$</li></ol><p>分析阶跃函数确定的求和区间：</p><p>$u[k] = 1$ 要求 $k \ge 0$</p><p>$u[n-k] = 1$ 要求 $n-k \ge 0$，即 $k \le n$</p><p>因此， $y<em>1[n]$ 的非零区间要求同时满足 $k \ge 0$ 且 $k \le n$。这说明只有当 $n \ge 0$ 时求和才有意义（此时 $0 \le k \le n$）。如果 $n &lt; 0$，区间不重叠，$y</em>1[n] = 0$。</p><p>当 $n \ge 0$ 时，利用等比数列求和公式：</p><p>$y<em>1[n] = \sum</em>{k=0}^{n} (\frac{1}{2})^k = \frac{1 - (1/2)^{n+1}}{1 - 1/2} = 2 \left[ 1 - (\frac{1}{2})^{n+1} \right] = 2 - (\frac{1}{2})^n$</p><p>所以，$y_1[n]$ 可以写为：</p><p>$y_1[n] = \left[ 2 - (\frac{1}{2})^n \right] u[n]$</p><ol start="4"><li>求解 $y_2[n]$</li></ol><p>分析阶跃函数确定的求和区间：</p><p>$u[-k] = 1$ 要求 $-k \ge 0$，即 $k \le 0$</p><p>$u[n-k] = 1$ 要求 $n-k \ge 0$，即 $k \le n$</p><p>因此，$y_2[n]$ 的求和区间取决于 $n$ 与 $0$ 的大小关系：</p><p>情况 A：当 $n \ge 0$ 时</p><p>求和区间的上限受限于 $k \le 0$（因为 $k$ 必须同时小于等于 $0$ 且小于等于 $n$，此时 $0$ 更小）。</p><p>$y<em>2[n] = \sum</em>{k=-\infty}^{0} 2^k$</p><p>令 $m = -k$，则求和变为从 $0$ 到 $\infty$：</p><p>$y<em>2[n] = \sum</em>{m=0}^{\infty} 2^{-m} = \sum_{m=0}^{\infty} (\frac{1}{2})^m = \frac{1}{1 - 1/2} = 2$</p><p>情况 B：当 $n &lt; 0$ 时</p><p>求和区间的上限受限于 $k \le n$（因为此时 $n$ 比 $0$ 小）。</p><p>$y<em>2[n] = \sum</em>{k=-\infty}^{n} 2^k$</p><p>令 $m = n-k$，当 $k=n$ 时 $m=0$；当 $k \to -\infty$ 时 $m \to \infty$。</p><p>$y<em>2[n] = \sum</em>{m=0}^{\infty} 2^{n-m} = 2^n \sum_{m=0}^{\infty} (\frac{1}{2})^m = 2^n \left( \frac{1}{1 - 1/2} \right) = 2^{n+1}$</p><p>综合以上两种情况，$y_2[n]$ 可以分段表示为：</p><p>$y_2[n] = \begin{cases} 2, &amp; n \ge 0 \ 2^{n+1}, &amp; n &lt; 0 \end{cases}$</p><p>或者用阶跃函数写成：$y_2[n] = 2u[n] + 2^{n+1}u[-n-1]$。</p><ol start="5"><li>组合最终结果</li></ol><p>将 $y<em>1[n]$ 和 $y</em>2[n]$ 相加 $y[n] = y<em>1[n] + y</em>2[n]$：</p><p>当 $n \ge 0$ 时：</p><p>$y[n] = \left( 2 - (\frac{1}{2})^n \right) + 2 = 4 - (\frac{1}{2})^n$</p><p>当 $n &lt; 0$ 时：</p><p>$y[n] = 0 + 2^{n+1} = 2^{n+1}$</p><p>最终答案：</p><p>你可以将结果写成分段函数的形式：</p><p>$y[n] = \begin{cases} 4 - (\frac{1}{2})^n, &amp; n \ge 0 \ 2^{n+1}, &amp; n &lt; 0 \end{cases}$</p><p>或者合并写成使用单位阶跃函数的闭式表达式：</p><p>$y[n] = \left[ 4 - (\frac{1}{2})^n \right] u[n] + 2^{n+1} u[-n-1]$</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/basic/sum_of_geo_seq#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/basic/sum_of_geo_seq</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/basic/sum_of_geo_seq</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Wed, 08 Jul 2026 00:15:53 GMT</pubDate></item><item><title><![CDATA[at device源码阅读]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/at-device">https://blog.flyyingpiggy2020.cn/posts/rtthread/at-device</a></blockquote><div><p>AT设备你们是如何解析的呢？笔者第一次使用at设备，是一个4G模块，大夏龙雀的L511-2。</p><p>看了新公司的同事的代码，这颗模块实现MQTT的方式，是直接用了AT+MQTT的方式来下发。</p><p>我打算换一个架构，之前学习的是socket做网络应用，我想把AT设备封装成socket接口。</p><p>这部分代码就不自己造轮子了，据我了解rtthread直接有at socket，今天就花一天时间阅读一下它的源码。</p><p>这里得分为两部分，一个是<strong>AT协议的解析框架</strong>。第二个是<strong>如何把AT指令封装成socket</strong>。</p><p>我们先来看第一个。</p><h1 id="at">AT协议解析框架</h1><h2 id="">底层收发接口</h2><p>最底层的发送接口使用一个<code>weak</code>定义的函数<code>at_utils_send</code>实现：</p><pre class="language-c lang-c"><code class="language-c lang-c">
rt_weak rt_size_t at_utils_send(rt_device_t dev,
                                rt_off_t    pos,
                                const void *buffer,
                                rt_size_t   size)
{
    return rt_device_write(dev, pos, buffer, size);
}

// 代码
</code></pre>
<p>它内部调用<code>rt_device_write</code>，如果我们物理接口使用的是<code>device io</code>框架，那么可以直接使用。</p><h2 id="atclient-">at_client 初始化</h2><p>根据语义，主动的被称为Client，被动的被称为Server。在我们的应用场景中往往模块被当成Server，而调用模块的MCU当成Client。如果你是要自己做一个模块，那就是实现Server。</p><p>在rtthread的代码中，提供了<code>at_client.c</code>和<code>at_server.c</code>这样子两个文件。</p><p>作为使用模块的用户，这里启用<code>at_client.c</code></p><p>它支持多实例，用单向链表管理多个client。<code>g_at_client_list</code>就是链表，在里面挂载了一个个节点<code>at_client_t</code>。</p>
<details><summary>在参数初始化代码中我发现了有趣的一点，多个线程可以使用同一个entry函数</summary><pre class="language-c lang-c"><code class="language-c lang-c">
unsigned int at_client_num = rt_slist_len(&amp;g_at_client_list);
rt_snprintf(name, RT_NAME_MAX, &quot;%s%d&quot;, AT_CLIENT_THREAD_NAME, at_client_num);
client-&gt;parser = rt_thread_create(name,
                                      (void (*)(void *parameter))ient_parsercl,
                                      client,
                                      1024 + 512,
                                      RT_THREAD_PRIORITY_MAX / 3 - 1,
                                      5);

</code></pre></details><p>之前没有接触过这种写法，不同入参同样的entry，又学到了一招。</p><p>初始化函数为：<code>int at_client_init(const char *dev_name, rt_size_t recv_bufsz, rt_size_t send_bufsz)</code></p><p>这里的入参有3个，第一个<code>dev_name</code>也就是输出物理层设备的名称。</p><p>我看到代码里的写法，应该是一个输出设备只能注册一个client。不存在一个串口，挂载多个client这种现象。我仔细想一下，好像确实是这样，这个抽象是合理的。一个串口挂载两个设备就乱套了。必须物理层和AT设备是一对一的。</p><p>整体<code>at_client_init</code>过程不是很复杂，完整代码我就不贴了，不然文章就太长了。</p><p>它里面，首先根据<code>dev_name</code>判断设备是否注册，防止重复注册。</p><p>然后malloc申请一个长度为<code> sizeof(struct at_client) + recv_bufsz + send_bufsz</code>内存。</p><p>这种做法貌似在RTOS代码里面满常见的。只申请一次，其中控制句柄放在这个内存的最开始。<code>client = rt_malloc(sizeof(struct at_client) + recv_bufsz + send_bufsz);</code>然后再对这段内存做分配。而不是申请三次</p><pre class="language-c lang-c"><code class="language-c lang-c">//申请一次，后续分配这段内存
client = rt_malloc(sizeof(struct at_client) + recv_bufsz + send_bufsz);
...
client-&gt;recv_bufsz    = recv_bufsz;
client-&gt;recv_line_buf = ((char *)client) + sizeof(struct at_client);

client-&gt;send_bufsz = send_bufsz;
client-&gt;send_buf   = ((char *)client) + sizeof(struct at_client) + client-&gt;recv_bufsz;
...
</code></pre>
<p>最后把<code>client</code>添加到<code>list</code>里面，注册串口RX的回调之类的。
<code>rt_device_set_rx_indicate(client-&gt;device, at_client_rx_ind);</code></p><p>这个回调函数在串口收到数据后会被触发，里面是发送了事件标志组，去通知解析。</p><p><code>rt_event_send(&amp;client-&gt;event, at_client_rx_notice_event);</code></p>
<h2 id="">解析数据</h2><p>通过<code>at_client_rx_notice_event</code>事件来触发读取。把读到的数据放到自己的<code>recv_line_buff</code>中</p><pre class="language-c lang-c"><code class="language-c lang-c">

if (event &amp; at_client_rx_notice_event)
{
    while (RT_EOK == at_client_getchar(client, &amp;ch))
    {
        if (client-&gt;recv_line_len &lt; client-&gt;recv_bufsz)
        {
            client-&gt;recv_line_buf[client-&gt;recv_line_len++] = ch;
        }
        else
        {
            is_full = RT_TRUE;
        }

        /* is newline or URC data */
        client-&gt;urc = get_urc_obj(client);
        if (client-&gt;urc != RT_NULL || (ch == &#x27;\n&#x27; &amp;&amp; last_ch == &#x27;\r&#x27;)
            || (client-&gt;end_sign != 0 &amp;&amp; ch == client-&gt;end_sign))
        {
            if (is_full)
            {
                LOG_E(&quot;read line failed. The line data length is out of buffer size(%d)!&quot;, client-&gt;recv_bufsz);
                rt_memset(client-&gt;recv_line_buf, 0x00, client-&gt;recv_bufsz);
                client-&gt;recv_line_len = 0;
                return -RT_EFULL;
            }

            /* Since the buffer state is uncertain, we proactively clear it; the overhead is negligible. */
            rt_event_send(&amp;client-&gt;event, at_client_rx_notice_event);
            goto __next;
        }
        last_ch = ch;
    }
}
</code></pre>
<p>这里判断了<code>urc</code></p><p>所谓urc就是指AT指令中，模块主动给MCU上报的信息，例如发送<code>AT+CSQ</code>收到的应答如下：</p><pre class="language-powershell lang-powershell"><code class="language-powershell lang-powershell">+CSQ: 20,99
OK
</code></pre>
<p>这种时候就需要解析应答的内容。</p><p>因为不同模块的应答格式不同，所以这个框架中采用了注册的方法，注册一个<code>urc_table</code>，然后这里是收到应答后去匹配，看看是不是<code>urc</code>的内容。</p>
<p><code>resp</code>是应答包。在AT协议中需要<code>OK</code>,<code>ERROR</code>,<code>FAIL</code>来判断发送命令的知行情况。</p><p>伪代码如下：</p>
<pre class="language-C lang-C"><code class="language-C lang-C">/**
 * Waiting for connection to external devices.
 *
 * @param client current AT client object
 * @param timeout millisecond for timeout
 *
 * @return 0 : success
 *        -2 : timeout
 *        -5 : no memory
 */
int at_client_obj_wait_connect(at_client_t client, rt_uint32_t timeout)
{
    rt_err_t      result     = RT_EOK;
    at_response_t resp       = RT_NULL;
    rt_tick_t     start_time = 0;

    if (client == RT_NULL)
    {
        LOG_E(&quot;input AT client object is NULL, please create or get AT Client object!&quot;);
        return -RT_ERROR;
    }

    resp = at_create_resp(64, 0, rt_tick_from_millisecond(300));
    if (resp == RT_NULL)
    {
        LOG_E(&quot;no memory for AT client(%s) response object.&quot;, client-&gt;device-&gt;parent.name);
        return -RT_ENOMEM;
    }

    start_time = rt_tick_get();

    while (1)
    {
        /* Check whether it is timeout */
        if (rt_tick_get() - start_time &gt; rt_tick_from_millisecond(timeout))
        {
            LOG_E(&quot;wait AT client(%s) connect timeout(%d tick).&quot;, client-&gt;device-&gt;parent.name, timeout);
            result = -RT_ETIMEOUT;
            break;
        }

        if (at_obj_exec_cmd(client, resp, &quot;AT&quot;) == RT_EOK)
        {
            break;
        }
    }

    at_delete_resp(resp);
    return result;
}

</code></pre>
<p>先是创建<code>resp</code>，然后调用<code>at_obj_exec_cmd</code>等到回复应答。</p>
<h1 id="socket">封装socket</h1></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/at-device#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/rtthread/at-device</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/rtthread/at-device</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Thu, 02 Jul 2026 02:40:19 GMT</pubDate></item><item><title><![CDATA[scons]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/scons">https://blog.flyyingpiggy2020.cn/posts/rtthread/scons</a></blockquote><div><p><code>scons --cdb build/compile_commands.json</code>这行命令可以用来用于clangd生成智能提示的json。</p><p><code>scons --target=mkd5</code>生成MDK工程。MDK工程的.c和.h文件添加通过编辑Sconsript文件实现。</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/scons#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/rtthread/scons</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/rtthread/scons</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Tue, 09 Jun 2026 06:58:49 GMT</pubDate></item><item><title><![CDATA[serial_v1和serial_v2的差别]]></title><description><![CDATA[<div><blockquote>该渲染由 Shiro API 生成，可能存在排版问题，最佳体验请前往：<a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/serial-v1-serial-v2">https://blog.flyyingpiggy2020.cn/posts/rtthread/serial-v1-serial-v2</a></blockquote><div><p><code>RT_USING_SRIAL_V1</code>和<code>RT_USING_SRIAL_V2</code>是 RT-Thread串口驱动的两个版本</p><p>定义在</p><p><a href="https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/serial/Kconfig#L7-L15">https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/serial/Kconfig#L7-L15</a></p><pre class="language-text lang-text"><code class="language-text lang-text">choice RT_USING_SERIAL_VERSION
    prompt &quot;Choice Serial version&quot;
    default RT_USING_SERIAL_V1

    config RT_USING_SERIAL_V1
        bool &quot;RT_USING_SERIAL_V1&quot;

    config RT_USING_SERIAL_V2
        bool &quot;RT_USING_SERIAL_V2&quot;
endchoice
</code></pre>
<p>SCons会根据这个选择不同的源码</p><p><a href="https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/serial/SConscript#L14-L17">https://github.com/RT-Thread/rt-thread/blob/master/components/drivers/serial/SConscript#L14-L17</a></p><pre class="language-text lang-text"><code class="language-text lang-text">if GetDepend([&#x27;RT_USING_SERIAL_V2&#x27;]):
    src += [&#x27;dev_serial_v2.c&#x27;]
else:
    src += [&#x27;dev_serial.c&#x27;]
</code></pre>
<p><strong>V1 和 V2 的主要区别：</strong></p><table><thead><tr><th> 项目 </th><th> Serial V1 </th><th> Serial V2 </th></tr></thead><tbody><tr><td> RT-Thread serial 核心文件 </td><td> dev_serial.c </td><td> dev<em>serial</em>v2.c </td></tr><tr><td> STM32 UART 驱动 </td><td> drv_usart.c </td><td> drv<em>usart</em>v2.c </td></tr><tr><td> 默认选择 </td><td> 是 </td><td> 否 </td></tr><tr><td> RX buffer </td><td> 全局 RT<em>SERIAL</em>RB_BUFSZ </td><td> 每个 UART 单独配置 </td></tr><tr><td> TX buffer </td><td> 较弱 </td><td> 每个 UART 单独配置 </td></tr><tr><td> buffer 策略 </td><td> 较传统 </td><td> 支持 drop / overwrite </td></tr><tr><td> Kconfig 可配置粒度 </td><td> 较粗 </td><td> 更细 </td></tr><tr><td> DMA 支持 </td><td> 有 </td><td> 更系统化 </td></tr><tr><td> 适合新项目 </td><td> 一般 </td><td> 更推荐 </td></tr></tbody></table><p><strong>BYPASS功能</strong></p><p>这个功能是RT-Thread serial 驱动里面的一个串口数据拦截机制。</p><p>开启 bypass 之后，串口的数据在被 rt_device_read()之前，会经过bypass pipe。</p><p>这个功能可以让某些模块提前看到串口字节。</p></div><p style="text-align:right"><a href="https://blog.flyyingpiggy2020.cn/posts/rtthread/serial-v1-serial-v2#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://blog.flyyingpiggy2020.cn/posts/rtthread/serial-v1-serial-v2</link><guid isPermaLink="true">https://blog.flyyingpiggy2020.cn/posts/rtthread/serial-v1-serial-v2</guid><dc:creator><![CDATA[Lu Xianfan]]></dc:creator><pubDate>Tue, 09 Jun 2026 00:37:35 GMT</pubDate></item></channel></rss>