<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-CN"><id>https://blog.tyh123.top/atom.xml</id><subtitle>分享单片机、FPGA 开发, 和 PCB 设计经验, 记录嵌入式开发路上的成长点滴。</subtitle><link href="https://blog.tyh123.top/" rel="alternate" type="text/html"/><link href="https://blog.tyh123.top/atom.xml" rel="self" type="application/atom+xml"/><updated>2026-11-04T16:30:31.000Z</updated><author><name>tyh123</name><uri>https://blog.tyh123.top/</uri></author><entry>,<id>https://blog.tyh123.top/posts/2db062d7b9b8</id>,<title>[blog-MT-07] 博客主题页面更新啦, 浅浅介绍一下本次更新的内容吧</title>,<link href="https://blog.tyh123.top/posts/2db062d7b9b8" rel="alternate" type="text/html"/>,<published>2026-10-22T10:32:43.000Z</published>,<updated>2026-10-22T10:32:43.000Z</updated>,<summary type="text">主要更新内容 最明显的变化莫过于主题了，主题由原来的 Fuwari 换成了毛玻璃质感，感觉这样很简约又有高级感。 窄屏下的顶部导航栏会变成汉堡菜单，点击后从右侧…</summary>,<content type="html">&lt;h2 id=&quot;主要更新内容&quot;&gt;主要更新内容&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;最明显的变化莫过于主题了，主题由原来的 Fuwari 换成了毛玻璃质感，感觉这样很简约又有高级感。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;窄屏下的顶部导航栏会变成汉堡菜单，点击后从右侧弹出。
&lt;img alt=&quot;NavSideBar&quot; width=&quot;771&quot; height=&quot;845&quot; src=&quot;https://blog.tyh123.top/_astro/NavSidebar.Ot05qoY2_Z1Q8Bti.webp&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;文章搜索支持拼音输入
&lt;img alt=&quot;search_pinyin&quot; width=&quot;472&quot; height=&quot;138&quot; src=&quot;https://blog.tyh123.top/_astro/search_pinyin.CT5FafV-_NxRnt.webp&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;草稿文章在 &lt;code&gt;dev&lt;/code&gt; 环境下预览时, 会在顶部生成一个提示
&lt;img alt=&quot;draft_tip&quot; width=&quot;737&quot; height=&quot;255&quot; src=&quot;https://blog.tyh123.top/_astro/draft_tip.e7dH7RzY_Z1CTMnF.webp&quot;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;右侧的侧边栏增加了数据统计，评论数量、总访问量、访客数量 (为了防止对服务器造成压力，数据每 1 小时更新一次).&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;大致的新增内容就是这么多了, &lt;code&gt;99.99%&lt;/code&gt; 的内容都是使用 &lt;code&gt;deepseek v4 pro&lt;/code&gt; 对话生成的产物, 也有一些是用的 &lt;code&gt;deepseek v4 flash&lt;/code&gt;, 本人只对部分功能进行了细化和微调。&lt;/p&gt;
&lt;p&gt;(OS: 不得不感慨一下现在的 AI 真的是非常非常的强大啊 |´・ω・)ノ)&lt;/p&gt;

&lt;h2 id=&quot;关于开源&quot;&gt;关于开源&lt;/h2&gt;
&lt;p&gt;这个主题目前仍然在修改和添加一些小功能中, 待到时机合适和完善时会考虑开源.&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/7a651a2</id>,<title>[blog-MT-06] - 将 TTF 字体压缩至几百 KB 的 woff2 字体, 一套网页字体轻量化的完整解决方案</title>,<link href="https://blog.tyh123.top/posts/7a651a2" rel="alternate" type="text/html"/>,<published>2026-06-06T21:26:18.000Z</published>,<updated>2026-06-06T21:26:18.000Z</updated>,<summary type="text">TTF 格式的字体文件通常有十几 MB, 直接用在网站上会严重拖慢加载速度。本文记录了一套完整的字体优化流程: 从原始 TTF 字体出发, 通过子集化 (subsetting) 与 woff2 压缩, 最终输出体积仅为几百 KB 的网页字体。</summary>,<content type="html">&lt;p&gt;这篇解决方案适用于任何以 &lt;code&gt;Node.js&lt;/code&gt; 构建的站点，是一篇通用教程。&lt;/p&gt;

&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;今天我为博客更换了主要字体: 一款开源字体 &lt;a href=&quot;https://github.com/lxgw/LxgwWenKaiGB&quot;&gt;霞鹜文楷 GB&lt;/a&gt;。&lt;/p&gt;
&lt;a href=&quot;https://github.com/lxgw/LxgwWenKaiGB&quot;&gt;
                
                    &lt;img src=&quot;https://avatars.githubusercontent.com/u/51902309?v=4&quot;&gt;
                    
                        
                            lxgw
                            /
                            LxgwWenKaiGB
                        
                        An open-source Simplified Chinese font derived from Klee One.
                    
                
                
                1.6K36OFL-1.1
            &lt;/a&gt;
&lt;p&gt;然而下载下来的完整字体文件接近 25 MB。如果直接原封不动地用于博客, 加载速度会极其缓慢。于是我开始探索字体文件大小的优化方法。&lt;/p&gt;
&lt;p&gt;(图为原始 ttf 字体文件的大小)
&lt;img alt=&quot;raw-ttf&quot; width=&quot;654&quot; height=&quot;180&quot; src=&quot;https://blog.tyh123.top/_astro/raw-ttf.HjIC0fK9_2vc2Sb.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;优化方法&quot;&gt;优化方法&lt;/h2&gt;
&lt;p&gt;起初, 我尝试了最直接的方式: 利用在线工具将 TTF 格式转换为更适合网页的 woff2 格式。&lt;br&gt;
25 MB 的 TTF 转换后仍接近 10 MB。虽然体积已经减少了大约 60%, 但对一个网页来说, 10 MB 的字体资源依然过于庞大。&lt;/p&gt;
&lt;p&gt;(图为转换为 woff2 格式后的文件大小)
&lt;img alt=&quot;ttf-to-woff2-size&quot; width=&quot;624&quot; height=&quot;118&quot; src=&quot;https://blog.tyh123.top/_astro/ttf-to-woff2-size.BbCfASQQ_17J5Mf.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;随后, 借助 AI 工具检索资料后发现: &lt;strong&gt;字体文件中只有一小部分字符是网站实际会用到的&lt;/strong&gt;。因此, 我们可以先收集所有页面中用到的字符, 形成一个字符集合, 然后基于这个集合从原字体中生成一个「子集」 (subset) 。&lt;br&gt;
这样一来, 原本 10 MB 左右的 woff2 字体还能进一步压缩 (当然也可以直接从 TTF 生成子集) 。&lt;/p&gt;
&lt;h2 id=&quot;字体子集化过程&quot;&gt;字体子集化过程&lt;/h2&gt;
&lt;h3 id=&quot;step1-安装-node-工具&quot;&gt;Step.1 安装 Node 工具&lt;/h3&gt;
&lt;p&gt;使用以下命令安装字体子集化所需的依赖:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npm install -D subset-font glob jsdom&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;subset-font&lt;/strong&gt;: 字体子集化的主要工具&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;glob&lt;/strong&gt;: 用于遍历构建后生成的 HTML 文件&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;jsdom&lt;/strong&gt;: 从 HTML 页面中提取纯文本内容&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;step2-存放完整字体文件&quot;&gt;Step2. 存放完整字体文件&lt;/h3&gt;
&lt;p&gt;将原始 TTF 字体放在任意位置, 只要后续脚本能够加载到即可。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果你存放到了 &lt;code&gt;public&lt;/code&gt; 文件夹下, 并且在意构建后 &lt;code&gt;dist&lt;/code&gt; 文件夹的体积, 则需要额外的代码对完整字体进行清理。&lt;/li&gt;
&lt;li&gt;如果你存放到了 &lt;code&gt;src/assets&lt;/code&gt; 文件夹下, 并且在意构建后 &lt;code&gt;dist&lt;/code&gt; 文件夹的体积, 那么推荐你将其移动到其它路径。 因为 Astro 在构建时会为 &lt;code&gt;src/assets&lt;/code&gt; 路径下所有文件添加哈希值, 它可能是动态变化的, 而我目前不知道这个哈希值如何获取。因此不推荐放在 &lt;code&gt;src/assets&lt;/code&gt; 这个路径下。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我的存放位置是 &lt;code&gt;public/fonts&lt;/code&gt; 文件夹下, 目录结构如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;public/
    fonts/
        jetbrains-mono/
            *.ttf
        lxgw-wenkai-gb/
            *.ttf&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step3-创建构建后脚本&quot;&gt;Step3. 创建构建后脚本&lt;/h3&gt;
&lt;p&gt;该脚本会从构建好的页面 (即 &lt;code&gt;dist&lt;/code&gt; 文件夹中的所有 HTML) 收集所有字符, 然后根据原始完整字体生成对应的字体子集, 并输出为 &lt;code&gt;woff2&lt;/code&gt; 格式。
将脚本保存到项目根目录的 &lt;code&gt;scripts&lt;/code&gt; 文件夹中, 文件后缀为 &lt;code&gt;.mjs&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// scripts/subset-font.mjs
import fs from &apos;fs&apos;;
import path from &apos;path&apos;;
import crypto from &apos;crypto&apos;;
import { glob } from &apos;glob&apos;;
import { JSDOM } from &apos;jsdom&apos;;
import subsetFont from &apos;subset-font&apos;;

// 字体存放的根目录
const fontRoot = &quot;public/fonts&quot;;
// 构建后 dist 文件夹内的 fonts 目录, 用于额外清理
const distFontRoot = &quot;dist/fonts&quot;
// 字体子集的输出目录
const outputDir = &quot;dist/_astro/fonts&quot;;
// 字体类型配置, in: 字体的输入格式, out: 字体的输出格式
const fontTypeConfig = {in: &quot;ttf&quot;, out: &quot;woff2&quot;};

// font lists
const fontConfigs = [
    // family: font-family, 
    // file: 完整字体的文件地址, 相对于 fontRoot, 不要添加后缀名, 
    // weight: 字体的字重, 控制字体粗细, 
    // style: normal, 正常, italic, 斜体
    { family: &apos;Yozai&apos;               , file: &apos;yozai/Yozai-Regular&apos;                    , weight: 400, style: &quot;normal&quot;, },
    { family: &apos;LXGWWenKaiMonoGB&apos;    , file: &apos;lxgw-wenkai-gb/LXGWWenKaiMonoGB-Regular&apos;, weight: 400, style: &quot;normal&quot;, },
    { family: &apos;JetBrainsMono&apos;       , file: &apos;jetbrains-mono/JetBrainsMono-Regular&apos;   , weight: 400, style: &quot;normal&quot;, },
    { family: &apos;JetBrainsMono-Italic&apos;, file: &apos;jetbrains-mono/JetBrainsMono-Italic&apos;    , weight: 400, style: &quot;italic&quot;, },
];

// 收集 html 中的所有文本字符。
async function collectChars(dir) {
    const htmlFiles = await glob(`${dir}/**/*.html`);
    const charSet = new Set();
    for (const file of htmlFiles) {
        const html = fs.readFileSync(file, &apos;utf-8&apos;);
        const dom = new JSDOM(html);
        const text = dom.window.document.body?.textContent || &apos;&apos;;
        for (const c of text) charSet.add(c);
    }
    return [...charSet].join(&apos;&apos;);
}

// 计算最终字体子集的哈希值, 并保留前 16 位结果。
function contentHash(buffer) {
    return crypto.createHash(&apos;sha256&apos;).update(buffer).digest(&apos;hex&apos;).slice(0, 16);
}

// 主处理流程
async function run() {
    console.log(&apos;🔍 Collecting characters...&apos;);
    const chars = await collectChars(&apos;dist&apos;);
    if (!chars) {
        console.warn(&apos;⚠ An empty result is returned, skipping.&apos;);
        return;
    }
    console.log(`✅ Collected ${chars.length} characters.`);

    // 确保输出路径存在
    if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });

    const hashMap = {}; // family -&gt; hash

    for (const { family, file } of fontConfigs) {
        const inputPath = `${fontRoot}/${file}.${fontTypeConfig.in}`;
        if (!fs.existsSync(inputPath)) {
            console.error(`❌ Full font file is not exists: ${inputPath}`);
            process.exit(1);
        }
        const fontBuffer = fs.readFileSync(inputPath);

        console.log(`⏳ Generating subset font for &apos;${family}&apos; ...`);
        const subsetBuffer = await subsetFont(fontBuffer, chars, {
            targetFormat: `${fontTypeConfig.out}`,
            preserveNameTable: true,
        });

        const hash = contentHash(subsetBuffer);
        const outFile = path.join(outputDir, `${hash}.${fontTypeConfig.out}`);
        fs.writeFileSync(outFile, subsetBuffer);
        console.log(`   → ${hash}.${fontTypeConfig.out} (${(subsetBuffer.length / 1024).toFixed(1)} KB)`);

        hashMap[family] = hash;
    }

    // 替换 css 中的占位符
    console.log(&apos;🔄 Replacing placeholders in css...&apos;);
    const cssFiles = await glob(&apos;dist/**/*.css&apos;);
    for (const cssFile of cssFiles) {
        let content = fs.readFileSync(cssFile, &apos;utf-8&apos;);
        let replaced = false;
        for (const [family, hash] of Object.entries(hashMap)) {
            const placeholder = `__FONT_HASH_${family}__`;
            if (content.includes(placeholder)) {
                content = content.replaceAll(placeholder, `/_astro/fonts/${hash}.${fontTypeConfig.out}`);
                replaced = true;
            }
        }
        if (replaced) {
            fs.writeFileSync(cssFile, content);
            console.log(`   ✔ Updated: ${cssFile}`);
        }
    }

    // 额外操作: 清理 dist/fonts 文件夹
    if (distFontRoot &amp;#x26;&amp;#x26; fs.existsSync(&apos;dist/fonts&apos;)) {
        fs.rmSync(&apos;dist/fonts&apos;, { recursive: true, force: true });
        console.log(&apos;🗑 cleaned dist font&apos;);
    }

    console.log(&apos;✨ Font subsets generated done!&apos;);
}

run().catch(err =&gt; {
    console.error(err);
    process.exit(1);
});&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step4-修改-packagejson-构建命令&quot;&gt;Step4. 修改 &lt;code&gt;package.json&lt;/code&gt; 构建命令&lt;/h3&gt;
&lt;p&gt;修改后 &lt;code&gt;scripts&lt;/code&gt; 字段内容如下所示&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;scripts&quot;: {
        &quot;build&quot;: &quot;astro build &amp;#x26;&amp;#x26; node scripts/subset-font.mjs &amp;#x26;&amp;#x26; pagefind --site dist&quot;,
    },
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;node scripts/subset-font.mjs&lt;/code&gt; 为新增命令, 其余两条命令为 astro 构建命令 和 Fuwari 主题中的原有命令&lt;/p&gt;
&lt;p&gt;命令运行顺序: &lt;code&gt;astro build -&gt; node scripts/subset-font.mjs -&gt; pagefind --site dist&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;step5-在-astro-布局中动态切换字体路径&quot;&gt;Step5. 在 Astro 布局中动态切换字体路径&lt;/h3&gt;
&lt;p&gt;在 dev 模式下预览页面时需要使用完整字体来显示页面, 保证新输入的字符不会缺少字体。在这里使用端点 css 文件来实现这个功能, 这份 css 端点文件用于生成站点中所有用到的字体, 注意文件名格式必须是 &lt;code&gt;*.css.ts&lt;/code&gt; 格式。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// pages/fonts.css.ts
export async function GET() {
    const isDev = import.meta.env.DEV 

    // 完整字体和子集字体的格式
    const fullFontType = &quot;ttf&quot;;
    const subSetType = &quot;woff2&quot;;

    // 字体配置, 可直接从脚本中复制过来
    const fontWeights = [
        { family: &apos;Yozai&apos;               , file: &apos;yozai/Yozai-Regular&apos;                    , weight: 400, style: &quot;normal&quot;, },
        { family: &apos;LXGWWenKaiMonoGB&apos;    , file: &apos;lxgw-wenkai-gb/LXGWWenKaiMonoGB-Regular&apos;, weight: 400, style: &quot;normal&quot;, },
        { family: &apos;JetBrainsMono&apos;       , file: &apos;jetbrains-mono/JetBrainsMono-Regular&apos;   , weight: 400, style: &quot;normal&quot;, },
        { family: &apos;JetBrainsMono-Italic&apos;, file: &apos;jetbrains-mono/JetBrainsMono-Italic&apos;    , weight: 400, style: &quot;italic&quot;, },
    ];

    const fontFaces = fontWeights.map(({ family, file, weight, style }) =&gt; {
        const url = isDev
            ? `/fonts/${file}.${fullFontType}`
            : `__FONT_HASH_${family}__`;
        return `@font-face {
    font-family: &apos;${family}&apos;;
    src: url(&apos;${url}&apos;) format(&apos;${subSetType}&apos;);
    font-weight: ${weight};
    font-style: ${style};
    font-display: swap;
}`;
    }).join(&apos;\n&apos;);

    const css = `/* 自动生成的全局字体声明 */
${fontFaces}
/* (保留内容) 可以继续添加 css 样式 */

/* 应用全局字体 */
* {
    font-family: &apos;JetBrainsMono&apos;, &apos;JetBrainsMono-Italic&apos;, &apos;LXGWWenKaiMonoGB&apos;;
    line-height: 1.5rem
}
`;

    return new Response(css, {
        headers: { &apos;Content-Type&apos;: &apos;text/css&apos; },
    });
}&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;step6-功能验证&quot;&gt;Step6. 功能验证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;开发环境&lt;/strong&gt;: 运行 &lt;code&gt;npm run dev&lt;/code&gt;, 浏览器会加载完整的字体文件, 在这时可以自由输入汉字, 不会存在某一字符字体缺失的情况。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生产环境&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;运行 &lt;code&gt;npm run build&lt;/code&gt;, 脚本会自动扫描 dist 文件夹中所有 HTML 文档, 生成仅包含所需字符的字体子集。配合 woff2 压缩, 最终字体文件大小可以控制在 100–300 KB。&lt;/li&gt;
&lt;li&gt;相比于未子集化的 woff2 字体，体积优化了约 46 倍；相比于原始 TTF 字体，优化了约 116 倍。最终文件的输出路径可由 &lt;code&gt;scripts/subset-font.mjs&lt;/code&gt; 脚本中 &lt;code&gt;outputDir&lt;/code&gt; 这个参数设置。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图为生成好的子集字体文件, 可以看到文件大小来到了 203KB 和 235KB, 由于脚本中对文件进行了取哈希值操作, 所以文件名为 16 位哈希结果。
&lt;img alt=&quot;generated-subset-font&quot; width=&quot;703&quot; height=&quot;170&quot; src=&quot;https://blog.tyh123.top/_astro/generated-subset-font.CHjq_ScN_Z1gEFke.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;step7-应用-css-样式&quot;&gt;Step7. 应用 CSS 样式&lt;/h3&gt;
&lt;p&gt;在 &lt;code&gt;&amp;#x3C;head&gt;...&amp;#x3C;/head&gt;&lt;/code&gt; 标签中, 引用端点 css, 使网站能够使用定义的 &lt;code&gt;font-family&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;https://blog.tyh123.top/fonts.css&quot; /&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;补充说明&quot;&gt;补充说明&lt;/h2&gt;

&lt;p&gt;关于优化后的体积倍数计算:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原始 ttf 体积取 25MB&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在线工具转换后的 woff2 体积&lt;/strong&gt;: 取 10MB, 10/25*100%=40%, 即转换后 woff2 格式的字体文件相比于原始 ttf 压缩了将近 60%, 文件体积变为原始 ttf 文件体积的 40%。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;子集化 + woff2 最终体积&lt;/strong&gt;: 按截图取 203 KB 和 235 KB, 取中间值约 220 KB, 即 0.215 MB. 25MB/0.215MB ≈ 116 倍。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;进阶-从子集化到unicode-range-分片&quot;&gt;进阶: 从「子集化」到「unicode-range 分片」&lt;/h2&gt;
&lt;h3 id=&quot;问题的暴露评论区的动态文字&quot;&gt;问题的暴露：评论区的动态文字&lt;/h3&gt;
&lt;p&gt;上面这套子集化方案上线后一直很稳定, 直到我给博客接入评论区 (Artalk) 后才暴露了一个盲区。&lt;/p&gt;
&lt;p&gt;子集化的核心逻辑是「&lt;strong&gt;基于构建期已知的字符&lt;/strong&gt;」—— &lt;code&gt;subset-font.mjs&lt;/code&gt; 只扫描构建好的静态 HTML, 收集里面的文字。但 Artalk 评论区是&lt;strong&gt;页面加载完成后由前端脚本再渲染&lt;/strong&gt;的, 评论内容在构建时完全不可预知。&lt;/p&gt;
&lt;p&gt;于是出现这样的现象: 当访客的评论里出现了子集未收录的字符 (比如某个生僻字、特殊符号), 这个字符就没有对应的字形, 浏览器会回退到系统默认字体, 和周围的霞鹜文楷混排在一起, 观感十分割裂。&lt;/p&gt;
&lt;h3 id=&quot;两种方案的区别&quot;&gt;两种方案的区别&lt;/h3&gt;








































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;维度&lt;/th&gt;&lt;th&gt;子集化方案 (上文)&lt;/th&gt;&lt;th&gt;unicode-range 分片方案 (本章节)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;原理&lt;/td&gt;&lt;td&gt;收集静态页面用到的字符, 只保留这些字形&lt;/td&gt;&lt;td&gt;把完整字体按 Unicode 区间切成几百个小块&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;覆盖范围&lt;/td&gt;&lt;td&gt;仅构建期已知字符&lt;/td&gt;&lt;td&gt;完整字体的全部字形&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;动态文本&lt;/td&gt;&lt;td&gt;覆盖不到(回退到默认字体)&lt;/td&gt;&lt;td&gt;浏览器按需补加载&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;单个文件体积&lt;/td&gt;&lt;td&gt;极小 (100–300 KB)&lt;/td&gt;&lt;td&gt;每块几十 KB, 但块数多&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;实际加载量&lt;/td&gt;&lt;td&gt;一个子集一次性加载&lt;/td&gt;&lt;td&gt;只加载用到的字符所在的分片&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;最适合的字体&lt;/td&gt;&lt;td&gt;拉丁小字体&lt;/td&gt;&lt;td&gt;中文大字体&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;简单而言就是, 字体的子集化是「只装我用到的字」, 分片则是「全部切好、按需取件」。&lt;/p&gt;

&lt;h3 id=&quot;为什么换到分片方案&quot;&gt;为什么换到分片方案&lt;/h3&gt;
&lt;p&gt;其实两种方案并不冲突, 我在项目里是&lt;strong&gt;混合使用&lt;/strong&gt;的:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;拉丁字体 (JetBrains Mono)&lt;/strong&gt;: 体积本来就小, 而且评论里的字母、数字、符号永远都在 ASCII 范围内, 子集化一定覆盖得到 → &lt;strong&gt;继续用子集化&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;中文大字体 (霞鹜文楷 GB)&lt;/strong&gt;: 完整字体有上万个字形, 而且评论里的汉字是动态、不可预知的 → &lt;strong&gt;改用分片&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;分片后, 中文大字体被切成几百个几十 KB 的 woff2 分片, 每条 &lt;code&gt;@font-face&lt;/code&gt; 规则都带上了 &lt;code&gt;unicode-range&lt;/code&gt;。浏览器渲染某个字符时, 会自动去加载「含该字符的那几个分片」——包括后来才渲染进 DOM 的评论文字。评论里出现任何汉字, 都能被浏览器自动补加载, 彻底解决了回退问题。&lt;/p&gt;
&lt;h3 id=&quot;具体实现&quot;&gt;具体实现&lt;/h3&gt;
&lt;h4 id=&quot;step1-安装-cn-font-split&quot;&gt;Step1. 安装 cn-font-split&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;$ npm install -D cn-font-split&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/KonghaYao/cn-font-split&quot;&gt;cn-font-split&lt;/a&gt; 是「中文网字计划」使用的字体分包工具, Rust 内核, 分包速度极快。&lt;/p&gt;
&lt;h4 id=&quot;step2-创建分片脚本&quot;&gt;Step2. 创建分片脚本&lt;/h4&gt;
&lt;p&gt;将脚本保存到 &lt;code&gt;scripts/split-font.mjs&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// scripts/split-font.mjs
import fs from &apos;fs&apos;;
import path from &apos;path&apos;;
import { fontSplit } from &apos;cn-font-split&apos;;

// 需要分片的中文字体 (拉丁字体体积小, 继续走 subset-font.mjs 的内容子集即可)
const cjkFonts = [
    {
        family: &apos;LXGWWenKaiMonoGB&apos;, // 与 fonts.css.ts / 全局 font-family 保持一致
        file: &apos;public/fonts/lxgw-wenkai-gb/LXGWWenKaiMonoGB-Regular.ttf&apos;,
    },
];

const distRoot = &apos;dist/_astro/fonts&apos;;

async function run() {
    for (const { family, file } of cjkFonts) {
        if (!fs.existsSync(file)) {
            console.error(`❌ 字体文件不存在: ${file}`);
            process.exit(1);
        }

        const outDir = path.join(distRoot, family);
        const input = new Uint8Array(fs.readFileSync(file).buffer);

        console.log(`⏳ 分片字体 &apos;${family}&apos; ...`);
        await fontSplit({
            input,
            outDir,
            css: {
                fontFamily: family,
                fontWeight: &apos;400&apos;,
                fontStyle: &apos;normal&apos;,
                fontDisplay: &apos;swap&apos;,
                localFamily: [&apos;LXGW WenKai Mono GB&apos;],
            },
            chunkSize: 70 * 1024, // 单个分片目标大小 (~70KB)
            testHtml: false,
            reporter: false,
            silent: true,
        });

        // 清理辅助文件, 只保留分片 woff2 和 result.css
        for (const name of fs.readdirSync(outDir)) {
            if (!name.endsWith(&apos;.woff2&apos;) &amp;#x26;&amp;#x26; name !== &apos;result.css&apos;) {
                fs.rmSync(path.join(outDir, name), { force: true });
            }
        }

        console.log(`✅ 分片完成: ${family}`);
    }
}

run()
    .then(() =&gt; process.exit(0)) // 规避 cn-font-split Rust DLL 卸载时的访问冲突
    .catch((err) =&gt; {
        console.error(err);
        process.exit(1);
    });&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后, &lt;code&gt;dist/_astro/fonts/LXGWWenKaiMonoGB/&lt;/code&gt; 下会生成几百个 woff2 分片和一个 &lt;code&gt;result.css&lt;/code&gt; (内含大量带 &lt;code&gt;unicode-range&lt;/code&gt; 的 &lt;code&gt;@font-face&lt;/code&gt;)。&lt;/p&gt;
&lt;h4 id=&quot;step3-修改原有子集化脚本&quot;&gt;Step3. 修改原有子集化脚本&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;scripts/subset-font.mjs&lt;/code&gt; 的 &lt;code&gt;fontConfigs&lt;/code&gt; 里移除中文大字体, 只保留拉丁字体:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fontConfigs = [
    // 中文大字体 LXGWWenKaiMonoGB 改由 scripts/split-font.mjs 分片, 不在这里子集化。
    { family: &apos;JetBrainsMono&apos;        , file: &apos;jetbrains-mono/JetBrainsMono-Regular&apos; , weight: 400, style: &quot;normal&quot;, },
    { family: &apos;JetBrainsMono-Italic&apos; , file: &apos;jetbrains-mono/JetBrainsMono-Italic&apos;  , weight: 400, style: &quot;italic&quot;, },
];&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step4-修改端点-css&quot;&gt;Step4. 修改端点 css&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;pages/fonts.css.ts&lt;/code&gt; 里, 中文大字体只在 &lt;code&gt;dev&lt;/code&gt; 环境下用完整的 TTF 字体, &lt;code&gt;prod&lt;/code&gt; 环境下不再生成占位符:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const fontWeights = [
    ...(isDev
        ? [{ family: &apos;LXGWWenKaiMonoGB&apos;, file: &apos;lxgw-wenkai-gb/LXGWWenKaiMonoGB-Regular&apos;, weight: 400, style: &quot;normal&quot; }]
        : []),
    { family: &apos;JetBrainsMono&apos;       , file: &apos;jetbrains-mono/JetBrainsMono-Regular&apos;  , weight: 400, style: &quot;normal&quot;, },
    { family: &apos;JetBrainsMono-Italic&apos;, file: &apos;jetbrains-mono/JetBrainsMono-Italic&apos;   , weight: 400, style: &quot;italic&quot;, },
];&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step5-在布局中引入分片-css&quot;&gt;Step5. 在布局中引入分片 CSS&lt;/h4&gt;
&lt;p&gt;在 &lt;code&gt;Layout.astro&lt;/code&gt; 的 &lt;code&gt;&amp;#x3C;head&gt;&lt;/code&gt; 中, 于 &lt;code&gt;/fonts.css&lt;/code&gt; 之后引入分片 CSS (只在生产环境引入):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;https://blog.tyh123.top/fonts.css&quot; /&gt;
{import.meta.env.PROD &amp;#x26;&amp;#x26; &amp;#x3C;link rel=&quot;stylesheet&quot; href=&quot;https://blog.tyh123.top/_astro/fonts/LXGWWenKaiMonoGB/result.css&quot; /&gt;}&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;step6-修改构建命令&quot;&gt;Step6. 修改构建命令&lt;/h4&gt;
&lt;p&gt;在原有构建命令里插入分片脚本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &quot;scripts&quot;: {
        &quot;build&quot;: &quot;astro build &amp;#x26;&amp;#x26; node scripts/split-font.mjs &amp;#x26;&amp;#x26; node scripts/subset-font.mjs&quot;
    }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;命令运行顺序: &lt;code&gt;astro build -&gt; node scripts/split-font.mjs -&gt; node scripts/subset-font.mjs&lt;/code&gt;&lt;/p&gt;
&lt;h3 id=&quot;三个需要注意的坑&quot;&gt;三个需要注意的坑&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. 二进制不随 npm 包分发, 需要放行 postinstall&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;cn-font-split 的 FFI 二进制 (&lt;code&gt;libffi-*.dll&lt;/code&gt; / &lt;code&gt;.so&lt;/code&gt;) 不随 npm 包分发, 靠安装时的 postinstall 脚本从 GitHub 下载。而 pnpm 默认会&lt;strong&gt;阻止&lt;/strong&gt;依赖的 postinstall 脚本, 需要在 &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; 里显式放行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# pnpm 版本 11+
allowBuilds:
  cn-font-split: true

# pnpm 版本 10
onlyBuiltDependencies:
  - cn-font-split&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则构建时加载二进制会报 &lt;code&gt;cannot open shared object file: No such file or directory&lt;/code&gt; (Linux) 或 &lt;code&gt;win32=193&lt;/code&gt; (Windows)。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. 国内网络下载二进制可能被截断&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;postinstall 从 GitHub 下载二进制, 国内直连极慢, 下载可能被截断 (DLL 只剩约 1.9MB), 运行时加载报 &lt;code&gt;win32=193 (不是有效的 Win32 应用程序)&lt;/code&gt;。用镜像重新下载即可:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;set CN_FONT_SPLIT_GH_HOST=https://ik.imagekit.io/github
npx cn-font-split i default&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Windows 下进程退出时的访问冲突&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;cn-font-split 在 Windows 上退出进程时, Rust DLL 的卸载阶段会触发访问冲突 (退出码 &lt;code&gt;0xC0000005&lt;/code&gt;)。分片本身其实已经完成, 只需在脚本末尾用 &lt;code&gt;process.exit(0)&lt;/code&gt; 显式退出即可规避 (见上面的 split-font.mjs 代码)。&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="字体优化"/>,<category term="woff2"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/1e78a05c</id>,<title>[blog-MT-05] 使用 Obsidian 来编写笔记</title>,<link href="https://blog.tyh123.top/posts/1e78a05c" rel="alternate" type="text/html"/>,<published>2026-06-05T09:13:26.000Z</published>,<updated>2026-11-04T16:30:31.000Z</updated>,<summary type="text">记录把 Obsidian 配置成博客写作环境的完整过程: 直接打开 src 目录作为仓库, 用 CSS 片段隐藏与写作无关的工程目录, 并把文章图片从 public 迁到 src/assets, 让 Obsidian 预览和 Vite 构建都能正常显示图片。</summary>,<content type="html">&lt;h2 id=&quot;为什么用-obsidian-写笔记&quot;&gt;为什么用 Obsidian 写笔记&lt;/h2&gt;
&lt;p&gt;博客的文章都是一个个本地 Markdown 文件, 用 Obsidian 直接打开项目目录来写作, 可以省掉「在编辑器里写一遍, 再去浏览器里预览」的来回切换:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接打开本地文件夹作为仓库, 不需要导入或转换;&lt;/li&gt;
&lt;li&gt;原生支持 Markdown、代码块与相对路径图片, 和博客的写作格式一致;&lt;/li&gt;
&lt;li&gt;可以用 CSS 片段调整界面, 把与写作无关的工程目录隐藏掉。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面记录一下具体的配置过程。&lt;/p&gt;
&lt;h2 id=&quot;准备工作&quot;&gt;准备工作&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;下文的 &lt;code&gt;[ROOT]&lt;/code&gt; 指项目根目录 (即包含 &lt;code&gt;package.json&lt;/code&gt; 的那一层)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;创建-obsidian-仓库&quot;&gt;创建 Obsidian 仓库&lt;/h3&gt;
&lt;p&gt;在 Obsidian 中选择「打开本地仓库」, 然后选中项目的 &lt;code&gt;src&lt;/code&gt; 目录即可:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;open_folder&quot; width=&quot;800&quot; height=&quot;650&quot; src=&quot;https://blog.tyh123.top/_astro/obsidian-open-folder.CwXBsrP2_Z1QIvKx.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;打开之后, 左侧会列出 &lt;code&gt;src&lt;/code&gt; 下的所有子目录:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;obsidian_file_explorer&quot; width=&quot;813&quot; height=&quot;547&quot; src=&quot;https://blog.tyh123.top/_astro/obsidian-file-explorer.CrUE1Lbw_ZfD34p.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;隐藏与写作无关的目录&quot;&gt;隐藏与写作无关的目录&lt;/h3&gt;
&lt;p&gt;写作时其实只关心 &lt;code&gt;assets&lt;/code&gt; 和 &lt;code&gt;contents&lt;/code&gt; 两个目录 (图中红框圈出来的部分), 其余目录 (&lt;code&gt;components&lt;/code&gt;、&lt;code&gt;layouts&lt;/code&gt;、&lt;code&gt;objects&lt;/code&gt; 等) 都可以隐藏。把下面这段 CSS 保存到 &lt;code&gt;.obsidian/snippets/&lt;/code&gt; 目录下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.nav-folder:has([data-path=&quot;components&quot;]),
.nav-folder:has([data-path=&quot;icons&quot;]),
.nav-folder:has([data-path=&quot;layouts&quot;]),
.nav-folder:has([data-path=&quot;loaders&quot;]),
.nav-folder:has([data-path=&quot;objects&quot;]),
.nav-folder:has([data-path=&quot;pages&quot;]),
.nav-folder:has([data-path=&quot;styles&quot;]) {
  display: none;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存后, 到 &lt;code&gt;设置 -&gt; 外观 -&gt; CSS 代码片段&lt;/code&gt; 里启用即可。找不到 &lt;code&gt;.obsidian/snippets&lt;/code&gt; 目录时, 可以在同一设置页点右上角的「打开目录」:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;obsidian-open-cfg-1&quot; width=&quot;739&quot; height=&quot;523&quot; src=&quot;https://blog.tyh123.top/_astro/obsidian-open-cfg-1.Cyajfzjt_ZD2uoa.webp&quot;&gt;
&lt;img alt=&quot;obsidian-open-cfg-2&quot; width=&quot;739&quot; height=&quot;523&quot; src=&quot;https://blog.tyh123.top/_astro/obsidian-open-cfg-2.mvOkx3GC_Zuv44u.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;启用后, 左侧的文件树会清爽很多:&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;obsidian-file-explorer-2&quot; width=&quot;326&quot; height=&quot;563&quot; src=&quot;https://blog.tyh123.top/_astro/obsidian-file-explorer-2.CQIWyv3K_Z1arWkd.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;把文章图片迁移到-assets-目录&quot;&gt;把文章图片迁移到 assets 目录&lt;/h3&gt;
&lt;p&gt;之前文章的图片都放在 &lt;code&gt;[ROOT]/public&lt;/code&gt; 下, 而 &lt;code&gt;public&lt;/code&gt; 里的文件会被原样复制到站点根目录, 所以 Markdown 里应该按站点根路径引用:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/img/posts/blog/05/xxx.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果写成带 &lt;code&gt;public&lt;/code&gt; 前缀的 &lt;code&gt;/public/img/posts/blog/05/xxx.png&lt;/code&gt;, Vite 会报警告, 提示去掉 &lt;code&gt;public&lt;/code&gt; 前缀。&lt;/p&gt;
&lt;p&gt;但站点根路径 (&lt;code&gt;/img/...&lt;/code&gt;) 在 Obsidian 里是找不到的 —— Obsidian 按本地文件系统的相对路径解析图片, 预览时会直接断图。&lt;/p&gt;
&lt;p&gt;把图片迁到 &lt;code&gt;[ROOT]/src/assets&lt;/code&gt; 下就能同时满足两边: Markdown 用相对路径引用, Obsidian 能正常预览, 构建时 Vite 也会正常处理这些图片资源。&lt;/p&gt;
&lt;p&gt;相对路径按文章所在目录的层级向上推:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;../../../(有多少层就向上推多少层)/assets/.../xxx.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以这篇为例, 文章在 &lt;code&gt;src/contents/posts/博客维护日记/&lt;/code&gt; 下, 向上三层到 &lt;code&gt;src/&lt;/code&gt;, 图片在 &lt;code&gt;src/assets/img/posts/blog/05/&lt;/code&gt; 下, 所以写成:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;../../../assets/img/posts/blog/05/obsidian-open-folder.png&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样图片在 Obsidian 里能正常显示, 构建出来的 HTML 页面也不受影响。&lt;/p&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用 Obsidian 打开项目的 &lt;code&gt;src&lt;/code&gt; 目录, 直接复用现有的 Markdown 文件;&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;.obsidian/snippets&lt;/code&gt; 里放一段 CSS, 隐藏与写作无关的工程目录;&lt;/li&gt;
&lt;li&gt;图片统一放在 &lt;code&gt;src/assets&lt;/code&gt; 下, Markdown 用相对路径引用, Obsidian 预览和 Vite 构建都能正常工作。&lt;/li&gt;
&lt;/ul&gt;</content>,<category term="博客维护日记"/>,<category term="Obsidian"/>,<category term="笔记"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/dda32b50</id>,<title>ETH-05 以太网 IP 协议介绍以及 Verilog 实现代码</title>,<link href="https://blog.tyh123.top/posts/dda32b50" rel="alternate" type="text/html"/>,<published>2026-06-04T16:51:35.000Z</published>,<updated>2026-06-04T16:51:35.000Z</updated>,<summary type="text">介绍 IP 协议的作用与 IPv4 首部各字段, 说明首部校验和的计算方法, 并给出 FPGA 中用 Verilog 实现 IP 数据报收发的代码。</summary>,<content type="html">&lt;h2 id=&quot;ip-协议简介&quot;&gt;IP 协议简介&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;IP 协议&lt;/strong&gt;（Internet Protocol）是互联网的核心网络层协议。它负责&lt;strong&gt;为数据包分配源(Source) IP 地址&lt;/strong&gt; 和&lt;strong&gt;目标 (Destination) IP 地址&lt;/strong&gt;, 并通过路由功能在不同网络节点间转发数据。它采用“尽力而为”的不可靠、无连接传输模式, 不保证数据包顺序或可靠性。而保证数据报顺序或可靠性的任务是由其上层协议 (如 TCP) 处理的。当前主要的 IP 协议版本为 IPv4 和 IPv6。&lt;/p&gt;
&lt;p&gt;IP 协议目前常用的上层协议如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ICMP&lt;/strong&gt;: 全称: &lt;strong&gt;I&lt;/strong&gt;nternet &lt;strong&gt;C&lt;/strong&gt;ontrol &lt;strong&gt;M&lt;/strong&gt;essage &lt;strong&gt;P&lt;/strong&gt;rotocol, 互联网控制报文协议。主要作用是帮助管理和控制网络通信, 提供网络诊断功能, 以及报告错误信息和网络是否可达等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UDP&lt;/strong&gt;: 全称: &lt;strong&gt;U&lt;/strong&gt;ser &lt;strong&gt;D&lt;/strong&gt;atagram &lt;strong&gt;P&lt;/strong&gt;rotocol, 用户数据协议。这是一种无连接、简单高效的传输层协议。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP&lt;/strong&gt;: 全称: &lt;strong&gt;T&lt;/strong&gt;ransmission &lt;strong&gt;C&lt;/strong&gt;ontrol &lt;strong&gt;P&lt;/strong&gt;rotocol, 传输控制协议。是一种面向连接的、可靠的字节流传输协议, 广泛应用于互联网数据传输。
以上三种协议都是基于 &lt;strong&gt;IP 协议&lt;/strong&gt; 运行的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ip-协议格式&quot;&gt;IP 协议格式&lt;/h2&gt;
&lt;h3 id=&quot;mac-帧&quot;&gt;MAC 帧&lt;/h3&gt;
&lt;p&gt;&lt;img alt=&quot;MAC-FRAME&quot; width=&quot;1083&quot; height=&quot;214&quot; src=&quot;https://blog.tyh123.top/_astro/mac-frame.B5JQGXFT_1J3A1t.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;ip-首部格式&quot;&gt;IP 首部格式&lt;/h3&gt;
&lt;p&gt;&lt;img width=&quot;941&quot; height=&quot;386&quot; src=&quot;https://blog.tyh123.top/_astro/ip-format.CNRpnP15_ZgA7vr.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;版本 (4 bits)&lt;/strong&gt;: IP 版本号, IPv4 = 4, IPv6 = 6.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首部长度 (4 bits)&lt;/strong&gt;: 以 32bit (4 字节) 为一个单位的首部长度, 首部长度一般填 5, 即 20 字节的长度.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;服务类型 (8 bits)&lt;/strong&gt;: 默认为 0 就行, 用于一般服务.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;总长度 (16 bits)&lt;/strong&gt;: &lt;strong&gt;整个 IP 数据报的长度&lt;/strong&gt; (首部 + 数据段), 以&lt;strong&gt;字节&lt;/strong&gt;为单位.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标识 (16 bits)&lt;/strong&gt;: 标识符, 用于分片重组&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;标志 (3 bits)&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;bit2: 保留位&lt;/li&gt;
&lt;li&gt;bit1: 填 1 表示禁止分片, 填 0 表示允许分片&lt;/li&gt;
&lt;li&gt;bit0: 若为分片包的最后一个包就填0, 否则为 1&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;片偏移 (13 bits)&lt;/strong&gt;: 用于接收端重组报文&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;生存时间 (8 bits)&lt;/strong&gt;: 英文简称 &lt;strong&gt;TTL&lt;/strong&gt;(&lt;strong&gt;T&lt;/strong&gt;ime &lt;strong&gt;T&lt;/strong&gt;o &lt;strong&gt;L&lt;/strong&gt;ive), 指 IP 报文在网络传输中可以通过网络交换机的次数, 若超过这个次数则交换机会将本次的 IP 报丢弃。作用是防止 IP 数据报在网络中长时间传输导致网络瘫痪。常用值: 64/128&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议 (4 bits)&lt;/strong&gt;: 上层协议 (ICMP=1, TCP=6, UDP=17)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;首部校验和&lt;/strong&gt;: 首部校验和, 计算方式在下文, 快捷链接: &lt;a href=&quot;#%E9%A6%96%E9%83%A8%E6%A0%A1%E9%AA%8C%E5%92%8C%E8%AE%A1%E7%AE%97%E6%96%B9%E6%B3%95&quot;&gt;[首部校验和计算方法]&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;源 IP 地址&lt;/strong&gt;: 发送方的 IP 地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的 IP 地址&lt;/strong&gt;: 接收方的 IP 地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可选字段&lt;/strong&gt;: 可变, 0-40 字节的长度, 需对齐至 32 位 (4字节) 边界。但在现代网络通信中这段内容几乎用不到。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;负载数据&lt;/strong&gt;: 用户传输的负载数据, ICMP、UDP、TCP 的首部数据和负载数据均在这段内容中。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;首部校验和计算方法&quot;&gt;首部校验和计算方法&lt;/h3&gt;
&lt;p&gt;IP 首部校验和用于检测 IP 数据报首部在传输过程中是否出现错误。它只覆盖 IP 首部（不包括数据部分）, 且采用 &lt;strong&gt;16 位反码求和&lt;/strong&gt; 算法。&lt;/p&gt;
&lt;p&gt;假设我们的 IP 首部数据经过填充后如下图所示。
&lt;img width=&quot;940&quot; height=&quot;455&quot; src=&quot;https://blog.tyh123.top/_astro/ip-header-filled.BC2SlAcc_Z1rIhq7.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;计算前的准备&quot;&gt;计算前的准备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;将校验和字段清零&lt;/strong&gt;: 在计算校验和之前, IP 首部中的 “首部校验和” 字段 (16 位) 必须设置为 &lt;code&gt;0x0000&lt;/code&gt;, 保证此段不参与计算。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;将所有数据按照每 16-bit(2 字节) 为划分为一个半字(Half-Word)&lt;/strong&gt;: 将整个 IP 首部 (通常是 20 字节, 不含选项, 或更多带选项的情况) 按每 2 字节为一个单位, 拆分成若干个 &lt;strong&gt;半字&lt;/strong&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;计算步骤&quot;&gt;计算步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;累加所有的半字&lt;/strong&gt;: 将所有这些 16 位数值进行普通的&lt;strong&gt;二进制加法&lt;/strong&gt; (允许进位到 17 位或更高, 并非取模值)。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以下是计算示例 (箭头所指的值为上一步骤的计算结果):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x4500 + 0x003C = 0x453C
-&gt; 0x453C + 0x0000 = 0x453C
-&gt; 0x453C + 0x4000 = 0x853C
-&gt; 0x853C + 0x4001 = 0xC53D
-&gt; 0xC53D + 0x0000 = 0xC53D
-&gt; 0xC53D + 0xC0A8 = 0x185E5   (产生进位, 结果是 0x185E5, 即高 16 位 = 0x0001, 低 16 位 = 0x85E5)
-&gt; 0x185E5 + 0x0166 = 0x1874B   (高 16 位 = 0x0001, 低 16 位 = 0x874B)
-&gt; 0x1874B + 0xC0A8 = 0x247F3   (高 16 位 = 0x0002, 低 16 位 = 0x47F3)
-&gt; 0x247F3 + 0x010A = 0x248FD   (高 16 位 = 0x0002, 低 16 位 = 0x48FD)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最终累加结果为 &lt;code&gt;0x2_48FD&lt;/code&gt; (即高 16 位 &lt;code&gt;0x0002&lt;/code&gt;, 低 16 位 &lt;code&gt;0x48FD&lt;/code&gt;)。&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;strong&gt;处理进位&lt;/strong&gt;: 如果高 16 位不为 0, 则将其与低 16 位相加, 重复此步骤直到高 16 位结果为 0。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;0x0002 + 0x48FD = 0x48FF&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时高 16 位为 &lt;code&gt;0x0000&lt;/code&gt;, 停止计算。中间结果 &lt;code&gt;0x48FF&lt;/code&gt; 称为 &lt;strong&gt;反码求和&lt;/strong&gt; 的中间值。&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;&lt;strong&gt;按位取反&lt;/strong&gt;: 将最终的 16 位中间值 &lt;code&gt;0x48FF&lt;/code&gt; 按位取反: &lt;code&gt;~0x48FF = 0xB700&lt;/code&gt;, 由此我们得到了最终的校验和 &lt;code&gt;0xB700&lt;/code&gt;, 这个值就是需要填入 IP 首部校验和字段的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;为什么要折叠进位&quot;&gt;为什么要折叠进位？&lt;/h3&gt;
&lt;p&gt;普通的加法会产生进位, 而反码求和算法通过把进位加回到低位, 保证了最终结果是一个 16 位数, 并且这种操作等价于 &lt;strong&gt;以 65535 为模的补码加法&lt;/strong&gt;。&lt;br&gt;
这样设计的好处是: 接收方可以用同样的方法计算 (包括校验和字段在内), 如果最终结果是 &lt;code&gt;0xFFFF&lt;/code&gt;, 则说明首部无错。&lt;/p&gt;
&lt;h3 id=&quot;接收方的验证方法&quot;&gt;接收方的验证方法&lt;/h3&gt;
&lt;p&gt;接收方收到 IP 数据报后, 也将整个首部 (包含校验和字段在内)  &lt;strong&gt;用相同的方法进行 16 位反码求和&lt;/strong&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若首部没有错误, 计算得到的最终结果应该是 &lt;code&gt;0xFFFF&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;如果不是 &lt;code&gt;0xFFFF&lt;/code&gt;, 则接收方应该丢弃这个数据报 (或通知上层)。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下面是&lt;strong&gt;接收方&lt;/strong&gt;计算示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x4500 + 0x003C = 0x453C
-&gt; 0x453C + 0x0000 = 0x453C
-&gt; 0x453C + 0x4000 = 0x853C
-&gt; 0x853C + 0x4001 = 0xC53D
-&gt; 0xC53D + 0xB700 = 0x17C3D   (0xB700: 在接收方参与和计算, 高 16 位 = 0x0001, 低 16 位 = 0x7C3D)
-&gt; 0x17C3D + 0xC0A8 = 0x23CE5  (高 16 位 = 0x0002, 低 16 位 = 0x3CE5)
-&gt; 0x23CE5 + 0x0166 = 0x23E4B  (高 16 位 = 0x0002, 低 16 位 = 0x3E4B)
-&gt; 0x23E4B + 0xC0A8 = 0x2FEF3  (高 16 位 = 0x0002, 低 16 位 = 0xFEF3)
-&gt; 0x2FEF3 + 0x010A = 0x2FFFD  (高 16 位 = 0x0002, 低 16 位 = 0xFFFD)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将高 16 位结果和低 16 位结果相加取和得到最终结果 &lt;code&gt;0xFFFF&lt;/code&gt;, 表明首部数据没有错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x0002 + 0xFFFD = 0xFFFF&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;实现代码&quot;&gt;实现代码&lt;/h2&gt;
&lt;h3 id=&quot;ip-解包模块&quot;&gt;IP 解包模块&lt;/h3&gt;
&lt;p&gt;其工作方式同样是流式握手控制协议。从这部分开始的模块有两个握手接口, 一个是与上游的网络解包模块接口握手来接收包含 IP 首部和负载的数据, 模块负责从中分离 IP 首部并解析; 另外一个握手接口是与下游模块 (通常是 IP 协议的上层协议模块, 如 UDP、TCP) 握手发送数据, 将 IP 数据段中的负载数据发送给下游模块。&lt;/p&gt;
&lt;p&gt;这里偷懒了没做接收方的数据校验😜。有能力的朋友可以自己做一下, 后面有时间的话我再实现吧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module ip_decap(
    input   wire            clk                 ,   //  [I] [     ] Module clock
    input   wire            rst_n               ,   //  [I] [     ] Module reset signal, active-low

    input   wire    [15:00] eth_type            ,   //  [I] [15:00] received ethertype
    input   wire    [31:00] board_ip            ,   //  [I] [31:00] board ip address, for comparison whith received destination ip address
    
    // upstream: ethernet pack decapsulation
    input   wire            decap_busy          ,   //  [I] [     ] decapsulation module busy state input
    input   wire    [07:00] s_axis_rd           ,   //  [I] [07:00] axis receive data input from decapsulation module
    input   wire            s_axis_rdv          ,   //  [I] [     ] axis receive data valid input from decapsulation module
    input   wire            s_axis_rend         ,   //  [I] [     ] axis receive tend input from decapsulation module
    output  reg             s_axis_rrdy         ,   //  [O] [     ] axis receive ready signal

    // Received IP payload output, to downstream: tcp/udp/icmp
    input   wire            s_ip_axis_rrdy      ,   //  [I] [     ] IP downstream rx ready
    output  reg     [07:00] s_ip_axis_rd        ,   //  [O] [07:00] IP data received output
    output  reg             s_ip_axis_rdv       ,   //  [O] [     ] IP data valid output, active-high
    output  reg             s_ip_axis_rend      ,   //  [O] [     ] IP data output end

    // IP Header output
    output  reg             ip_valid            ,   //  [O] [     ] Active-high only received destination ip address is board-ip
    output  reg     [03:00] ip_version          ,   //  [O] [03:00] IP version, 4&apos;b4: ipv4, 4&apos;b6: ipv6
    output  reg     [03:00] ip_header_len       ,   //  [O] [03:00] IP header length, In units of 32 bits (4 bytes).
    output  reg     [07:00] ip_tos              ,   //  [O] [07:00] IP type of service
    output  reg     [15:00] ip_total_len        ,   //  [O] [15:00] IP total length (include payload)
    output  reg     [15:00] ip_id               ,   //  [O] [15:00] IP identification
    output  reg     [02:00] ip_flag             ,   //  [O] [02:00] IP Flag, bit2: reserved, bit1: 1&apos;b1 is allowing fragmentation, else disallowing. bit0: 1&apos;b1 is more frame, 1&apos;b0 is the last frame.
    output  reg     [12:00] ip_fragment_offset  ,   //  [O] [12:00] IP fragment offset
    output  reg     [07:00] ip_ttl              ,   //  [O] [07:00] IP time to live
    output  reg     [07:00] ip_ul_protocol      ,   //  [O] [07:00] IP upper-layer protocol (ICMP = 8&apos;d1, TCP=8&apos;d6, UDp=8&apos;d17)
    output  reg     [15:00] ip_checksum         ,   //  [O] [15:00] IP checksum
    output  reg     [31:00] ip_src_addr         ,   //  [O] [31:00] Source ip address
    output  reg     [31:00] ip_dst_addr             //  [O] [31:00] Destination ip address
);

//================================================================================
// Local Parameter Declarations
//================================================================================
localparam  IP_ETH_TYPE =   16&apos;h0800    ;

// state defines
localparam  ST_IDLE         =   3&apos;d0    ;
localparam  ST_IP_HEADER    =   3&apos;d1    ;
localparam  ST_IP_PAYLOAD   =   3&apos;d2    ;
localparam  ST_IP_END       =   3&apos;d3    ;

//================================================================================
// Register Declarations
//================================================================================
reg [03:00] state           ;
reg [15:00] bytes_rx_cnt    ; 

//================================================================================
// Wire Declarations
//================================================================================

//================================================================================
// Assign Declarations
//================================================================================

//================================================================================
// implements
//================================================================================

//================================================================================
// MAIN CODE
//================================================================================
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state               &amp;#x3C;= ST_IDLE  ;
        bytes_rx_cnt        &amp;#x3C;=  16&apos;d0   ;
        ip_version          &amp;#x3C;=  4&apos;b0    ;
        ip_header_len       &amp;#x3C;=  4&apos;b0    ;
        ip_tos              &amp;#x3C;=  8&apos;b0    ;
        ip_total_len        &amp;#x3C;=  16&apos;b0   ;
        ip_id               &amp;#x3C;=  16&apos;b0   ;
        ip_flag             &amp;#x3C;=  3&apos;b0    ;
        ip_fragment_offset  &amp;#x3C;=  13&apos;b0   ;
        ip_ttl              &amp;#x3C;=  8&apos;b0    ;
        ip_ul_protocol      &amp;#x3C;=  8&apos;b0    ;
        ip_checksum         &amp;#x3C;=  16&apos;b0   ;
        ip_src_addr         &amp;#x3C;=  32&apos;b0   ;
        ip_dst_addr         &amp;#x3C;=  32&apos;b0   ;

        s_axis_rrdy         &amp;#x3C;=  1&apos;b0    ;
        s_ip_axis_rend      &amp;#x3C;=  1&apos;b0    ;
        s_ip_axis_rd        &amp;#x3C;=  8&apos;b0    ;
        s_ip_axis_rdv       &amp;#x3C;=  1&apos;b0    ;
        ip_valid            &amp;#x3C;=  1&apos;b0    ;
    end else begin
        case (state)
            ST_IDLE: begin
                bytes_rx_cnt        &amp;#x3C;=  16&apos;d0   ;

                s_axis_rrdy         &amp;#x3C;=  1&apos;b1    ;
                s_ip_axis_rend      &amp;#x3C;=  1&apos;b0    ;
                s_ip_axis_rd        &amp;#x3C;=  8&apos;b0    ;
                s_ip_axis_rdv       &amp;#x3C;=  1&apos;b0    ;
                ip_valid            &amp;#x3C;=  1&apos;b0    ;
                
                if (
                    eth_type == IP_ETH_TYPE &amp;#x26;&amp;#x26;      //  upstream received ip packet
                    s_axis_rdv == 1&apos;b1
                ) begin
                    state   &amp;#x3C;=  ST_IP_HEADER                ;

                    ip_version      &amp;#x3C;=  s_axis_rd[7:4]      ;   // pre receive 1 bytes
                    ip_header_len   &amp;#x3C;=  s_axis_rd[3:0]      ;
                    bytes_rx_cnt    &amp;#x3C;= bytes_rx_cnt + 1&apos;b1  ;
                end else begin
                    state   &amp;#x3C;=  ST_IDLE                     ;
                end
            end

            ST_IP_HEADER: begin
                bytes_rx_cnt &amp;#x3C;= bytes_rx_cnt + 1&apos;b1 ;
                case (bytes_rx_cnt) 
                    16&apos;d1: ip_tos               &amp;#x3C;=  s_axis_rd       ;
                    16&apos;d2: ip_total_len[15:8]   &amp;#x3C;=  s_axis_rd       ;
                    16&apos;d3: ip_total_len[07:0]   &amp;#x3C;=  s_axis_rd       ;
                    16&apos;d4: ip_id[15:08]         &amp;#x3C;=  s_axis_rd       ;
                    16&apos;d5: ip_id[07:00]         &amp;#x3C;=  s_axis_rd       ;

                    16&apos;d6: begin
                        ip_flag                 &amp;#x3C;=  s_axis_rd[7:4]  ;
                        ip_fragment_offset[12:8]&amp;#x3C;=  s_axis_rd[4:0]  ;
                    end

                    16&apos;d7: ip_fragment_offset[7:0]  &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d8: ip_ttl                   &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d9: ip_ul_protocol           &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d10: ip_checksum[15:08]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d11: ip_checksum[07:00]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d12: ip_src_addr[31:24]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d13: ip_src_addr[23:16]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d14: ip_src_addr[15:08]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d15: ip_src_addr[07:00]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d16: ip_dst_addr[31:24]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d17: ip_dst_addr[23:16]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d18: ip_dst_addr[15:08]      &amp;#x3C;=  s_axis_rd   ;
                    16&apos;d19: ip_dst_addr[07:00]      &amp;#x3C;=  s_axis_rd   ;

                    default: begin  //  26.4.8 add: dest ip check
                        if (ip_dst_addr == board_ip) begin
                            state           &amp;#x3C;=  ST_IP_PAYLOAD   ;   // 1if received dst ip is board ip, receive payload
                            ip_valid        &amp;#x3C;=  1&apos;b1            ;
                            s_ip_axis_rdv   &amp;#x3C;=  1&apos;b1            ;
                            s_ip_axis_rd    &amp;#x3C;=  s_axis_rd       ;   //  pre-fill 1 byte
                        end else begin
                            state           &amp;#x3C;= ST_IP_END        ;   // or else, drop this packet
                            ip_valid        &amp;#x3C;=  1&apos;b0            ;
                        end
                    end
                endcase
            end

            ST_IP_PAYLOAD: begin
                s_ip_axis_rd        &amp;#x3C;=  s_axis_rd   ;
                if (s_axis_rdv == 1&apos;b0 ) begin
                    s_ip_axis_rd    &amp;#x3C;=  8&apos;b0    ;
                    s_ip_axis_rdv   &amp;#x3C;=  1&apos;b0    ;
                    s_ip_axis_rend  &amp;#x3C;=  1&apos;b1    ;
                    state           &amp;#x3C;=  ST_IP_END   ;
                end
            end

            ST_IP_END: begin
                s_ip_axis_rend  &amp;#x3C;=  1&apos;b0    ;
                state           &amp;#x3C;=  ST_IDLE ;
            end
        endcase
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ip-封包模块&quot;&gt;IP 封包模块&lt;/h3&gt;
&lt;p&gt;同样是两个握手接口, 一个接口负责接收来自上游模块 (UDP、TCP模块) 的数据, 一个接口用来将封装好的数据发送到下游模块 (以太网封包模块)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module ip_encap (
    input   wire            clk             ,   //  [I] [     ] Module clock
    input   wire            rst_n           ,   //  [I] [     ] Module reset signal, active-low
    
    // downstream: ethernet pack encapsulation
    input   wire            encap_busy      ,   //  [I] [     ] encapsulation module busy state input, we only send packet when this signal is 1&apos;b0    
    output  reg     [07:00] s_axis_td       ,   //  [O] [07:00] axis send data output to encapsulation module
    output  reg             s_axis_tdv      ,   //  [O] [     ] axis send data valid output to encapsulation module
    output  wire            s_axis_tend     ,   //  [O] [     ] axis send tend output to encapsulation module
    input   wire            s_axis_trdy     ,   //  [I] [     ] axis send ready signal

    // upstream: tcp/udp/icmp tx output
    input   wire    [07:00] s_ip_axis_td    ,   //  [I] [07:00] IP data AXI-Stream input
    input   wire            s_ip_axis_tdv   ,   //  [I] [     ] IP data send valid, active-high
    input   wire            s_ip_axis_tend  ,   //  [I] [     ] IP data send end
    output  reg             s_ip_axis_trdy  ,   //  [O] [     ] IP data send ready

    // IP header
    input   wire    [15:00] data_length     ,   //  [I] [15:00] Data segment length in bytes
    input   wire    [07:00] ip_ttl          ,   //  [I] [07:00] Time to live.
    input   wire    [07:00] ip_ul_protocol  ,   //  [I] [07:00] Upper-layer protocol, (ICMP=8&apos;d1, TCP=8&apos;d6, UDP=8&apos;d17)
    input   wire    [31:00] ip_src_ip       ,   //  [I] [31:00] Source ip address
    input   wire    [31:00] ip_dst_ip       ,   //  [I] [31:00] Destination ip address

    // States output
    output  reg             ip_tx_err_busy      //  [O] [     ] TX error when dowmstream encapsulation module is busy, at this time, upstream must stop transmission.
);

//================================================================================
// Local Parameter Declarations
//================================================================================
localparam  IP_VERSION          =   4&apos;h4        ;   // ipv4
localparam  IP_HEADER_LENGTH    =   4&apos;h5        ;   // header length 5, 20 bytes in total.
localparam  IP_TOS              =   8&apos;h00       ;

// state defines
localparam  ST_IDLE             =   4&apos;b0        ;
localparam  ST_CHECKSUM         =   4&apos;d1        ;
localparam  ST_CHECKSUM_CHECK   =   4&apos;d2        ;
localparam  ST_IP_TX_TRIG       =   4&apos;d3        ;
localparam  ST_IP_PAYLOAD       =   4&apos;d4        ;
localparam  ST_IP_END           =   4&apos;d5        ;

//================================================================================
// Register Declarations
//================================================================================
reg     [03:00] state               ;
reg     [31:00] ip_header   [4:0]   ;   //  ip header: 20 bytes
reg     [04:00] ip_header_index     ;   //  for checksum and header transmission
reg     [31:00] checksum_buffer     ;
reg     [15:00] cnt                 ;   //  a counter

//================================================================================
// Wire Declarations
//================================================================================
wire    [15:00] total_length    ;   // total ip packet length, include header and data segment

//================================================================================
// Assign Declarations
//================================================================================
assign  total_length    =   ((IP_HEADER_LENGTH*4) + data_length)    ;
assign  s_axis_tend     =   s_ip_axis_tend                          ;

//================================================================================
// implements
//================================================================================

//================================================================================
// MAIN CODE
//================================================================================
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state           &amp;#x3C;=  ST_IDLE ;
        cnt             &amp;#x3C;=  16&apos;d0   ;

        ip_header[0]    &amp;#x3C;=  32&apos;b0   ;
        ip_header[1]    &amp;#x3C;=  32&apos;b0   ;
        ip_header[2]    &amp;#x3C;=  32&apos;b0   ;
        ip_header[3]    &amp;#x3C;=  32&apos;b0   ;
        ip_header[4]    &amp;#x3C;=  32&apos;b0   ;
        ip_header_index &amp;#x3C;=  5&apos;d00   ;
        checksum_buffer &amp;#x3C;=  32&apos;b0   ;

        s_axis_tdv      &amp;#x3C;=  1&apos;b0    ;
        s_axis_td       &amp;#x3C;=  8&apos;b0    ;
        s_ip_axis_trdy  &amp;#x3C;=  1&apos;b0    ;

        ip_tx_err_busy  &amp;#x3C;=  1&apos;b0    ;
    end else begin
        case(state)
            ST_IDLE: begin
                s_ip_axis_trdy  &amp;#x3C;=  1&apos;b0    ;
                if (encap_busy == 1&apos;b1) begin
                    ip_tx_err_busy  &amp;#x3C;=  1&apos;b1    ;
                end else if (s_ip_axis_tdv == 1&apos;b1) begin
                    ip_tx_err_busy  &amp;#x3C;=  1&apos;b0    ;
                    // pre fill buffer, in order to calculate checksum.
                    ip_header[0]        &amp;#x3C;=  {IP_VERSION, IP_HEADER_LENGTH, IP_TOS, total_length};
                    ip_header[1][31:16] &amp;#x3C;=  ip_header[1][31:16] + 1&apos;b1  ;   //  id, auto increment by 1
                    ip_header[1][15:13] &amp;#x3C;=  3&apos;b010                      ;   //  No fragment
                    ip_header[1][12:00] &amp;#x3C;=  13&apos;d00                      ;   //  Fragment offset
                    ip_header[2][31:24] &amp;#x3C;=  ip_ttl                      ;   //  TTL
                    ip_header[2][23:16] &amp;#x3C;=  ip_ul_protocol              ;   //  Upper-layer protocol, (ICMP=8&apos;d1, TCP=8&apos;d6, UDP=8&apos;d17)
                    ip_header[2][15:00] &amp;#x3C;=  16&apos;h0000                    ;   //  IP header checksum, temporarily is 0
                    ip_header[3]        &amp;#x3C;=  ip_src_ip                   ;   //  Source ip address
                    ip_header[4]        &amp;#x3C;=  ip_dst_ip                   ;   //  Destination ip address

                    state   &amp;#x3C;=  ST_CHECKSUM ;
                end
            end

            ST_CHECKSUM: begin
                checksum_buffer &amp;#x3C;=  ip_header[0][31:16] + ip_header[0][15:00] +
                                    ip_header[1][31:16] + ip_header[1][15:00] +
                                    ip_header[2][31:16] + ip_header[2][15:00] +
                                    ip_header[3][31:16] + ip_header[3][15:00] +
                                    ip_header[4][31:16] + ip_header[4][15:00] ;
                state   &amp;#x3C;=  ST_CHECKSUM_CHECK   ;
            end

            ST_CHECKSUM_CHECK: begin
                if (checksum_buffer[31:16] == 16&apos;h0000) begin           // Accumulate until the high 16 bits are 0.
                    ip_header[2][15:00] &amp;#x3C;=  ~checksum_buffer[15:00] ;   //  The final result is bitwise negated.
                    state               &amp;#x3C;=  ST_IP_TX_TRIG           ;   //  Goto next state

                    s_axis_tdv          &amp;#x3C;=  1&apos;b1                    ;   //  downstream: frame encapsulation tx valid, then wait trdy signal
                    // s_axis_td           &amp;#x3C;=  ip_header[0][31:24]     ;
                    // cnt                 &amp;#x3C;=  15&apos;d1                   ;
                end else begin
                    checksum_buffer &amp;#x3C;=  checksum_buffer[31:16] + checksum_buffer[15:00];
                end
            end

            ST_IP_TX_TRIG: begin
                if (s_axis_trdy == 1&apos;b1) begin
                    cnt         &amp;#x3C;=  cnt + 1&apos;b1          ;
                    s_axis_td   &amp;#x3C;=  ip_header[cnt[15:2]][((3 - cnt[1:0])*8)+:8];
                    if (cnt == IP_HEADER_LENGTH*4 - 1) begin
                        cnt         &amp;#x3C;=  16&apos;d0           ;
                        state       &amp;#x3C;=  ST_IP_PAYLOAD   ;
                        // s_axis_td   &amp;#x3C;=  s_ip_axis_td    ;
                    end else if (cnt == IP_HEADER_LENGTH*4 - 2) begin
                        s_ip_axis_trdy  &amp;#x3C;=  1&apos;b1            ;
                    end
                end
            end

            ST_IP_PAYLOAD: begin
                s_axis_td           &amp;#x3C;=  s_ip_axis_td    ;
                if (s_ip_axis_tend == 1&apos;b1) begin
                    s_axis_tdv      &amp;#x3C;=  1&apos;b0            ;
                    s_axis_td           &amp;#x3C;=  8&apos;b00       ;
                    s_ip_axis_trdy      &amp;#x3C;=  1&apos;b0        ;
                    state           &amp;#x3C;=  ST_IP_END       ;
                end
            end

            ST_IP_END: begin
                state               &amp;#x3C;=  ST_IDLE             ;
            end
        endcase
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;ip-协议顶层&quot;&gt;IP 协议顶层&lt;/h3&gt;
&lt;p&gt;这个模块作为一个顶层例化了封包和解包两个子模块, 方便在其它上层模块中进行调用。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module ip(
    input   wire            tx_clk                      ,   //  [I] [     ] Module tx clock
    input   wire            rx_clk                      ,   //  [I] [     ] Module rx clock
    input   wire            rst_n                       ,   //  [I] [     ] Module reset signal, active-low

    input   wire    [15:00] ip_rx_eth_type              ,   //  [I] [15:00] received ethertype
    input   wire    [31:00] board_ip                    ,   //  [I] [31:00] board ip address, for comparison whith received destination ip address

    // ip encap
    // downstream: ethernet pack encapsulation
    input   wire            encap_busy                  ,   //  [I] [     ] encapsulation module busy state input, we only send packet when this signal is 1&apos;b0
    output  wire    [07:00] s_axis_td                   ,   //  [O] [07:00] axis send data output to encapsulation module
    output  wire            s_axis_tdv                  ,   //  [O] [     ] axis send data valid output to encapsulation module
    output  wire            s_axis_tend                 ,   //  [O] [     ] axis send tend output to encapsulation module
    input   wire            s_axis_trdy                 ,   //  [I] [     ] axis send ready signal

    // IP payload output, to downstream: tcp/udp/icmp
    output  wire            s_ip_axis_trdy              ,   //  [I] [     ] IP downstream rx ready
    input   wire    [07:00] s_ip_axis_td                ,   //  [O] [07:00] IP data AXI-Stream output
    input   wire            s_ip_axis_tdv               ,   //  [O] [     ] IP data output valid, active-high
    input   wire            s_ip_axis_tend              ,   //  [O] [     ] IP data output end

    input   wire    [15:00] ip_encap_data_length        ,   //  [I] [15:00] Data segment length in bytes
    input   wire    [07:00] ip_encap_ttl                ,   //  [I] [07:00] Time to live.
    input   wire    [07:00] ip_encap_ul_protocol        ,   //  [I] [07:00] Upper-layer protocol, (ICMP=8&apos;d1, TCP=8&apos;d6, UDP=8&apos;d17)
    input   wire    [31:00] ip_encap_src_ip             ,   //  [I] [31:00] Source ip address
    input   wire    [31:00] ip_encap_dst_ip             ,   //  [I] [31:00] Destination ip address

    output  wire            ip_encap_tx_err_busy        ,   //  [O] [     ] TX error when dowmstream encapsulation module is busy, at this time, upstream must stop transmission.

    // ip decap
    // upstream: ethernet pack decapsulation
    input   wire            decap_busy                  ,   //  [I] [     ] decapsulation module busy state input
    input   wire    [07:00] s_axis_rd                   ,   //  [I] [07:00] axis receive data input from decapsulation module
    input   wire            s_axis_rdv                  ,   //  [I] [     ] axis receive data valid input from decapsulation module
    input   wire            s_axis_rend                 ,   //  [I] [     ] axis receive tend input from decapsulation module
    output  wire            s_axis_rrdy                 ,   //  [O] [     ] axis receive ready signal

    input   wire            s_ip_axis_rrdy              ,   //  [I] [     ] IP downstream rx ready
    output  wire    [07:00] s_ip_axis_rd                ,   //  [O] [07:00] IP data received output
    output  wire            s_ip_axis_rdv               ,   //  [O] [     ] IP data valid output, active-high
    output  wire            s_ip_axis_rend              ,   //  [O] [     ] IP data output end

    // output received ip header
    output  wire            ip_valid                    ,   //  [O] [     ] Active-high only received destination ip address is board-ip
    output  wire    [03:00] ip_decap_version            ,   //  [O] [03:00] IP version, 4&apos;b4: ipv4, 4&apos;b6: ipv6
    output  wire    [03:00] ip_decap_header_len         ,   //  [O] [03:00] IP header length, In units of 32 bits (4 bytes).
    output  wire    [07:00] ip_decap_tos                ,   //  [O] [07:00] IP type of service
    output  wire    [15:00] ip_decap_total_len          ,   //  [O] [15:00] IP total length (include payload)
    output  wire    [15:00] ip_decap_id                 ,   //  [O] [15:00] IP identification
    output  wire    [02:00] ip_decap_flag               ,   //  [O] [02:00] IP Flag, bit2: reserved, bit1: 1&apos;b1 is allowing fragmentation, else disallowing. bit0: 1&apos;b1 is more frame, 1&apos;b0 is the last frame.
    output  wire    [12:00] ip_decap_fragment_offset    ,   //  [O] [12:00] IP fragment offset
    output  wire    [07:00] ip_decap_ttl                ,   //  [O] [07:00] IP time to live
    output  wire    [07:00] ip_decap_ul_protocol        ,   //  [O] [07:00] IP upper-layer protocol (ICMP = 8&apos;d1, TCP=8&apos;d6, UDp=8&apos;d17)
    output  wire    [15:00] ip_decap_checksum           ,   //  [O] [15:00] IP checksum
    output  wire    [31:00] ip_decap_src_addr           ,   //  [O] [31:00] Source ip address
    output  wire    [31:00] ip_decap_dst_addr               //  [O] [31:00] Destination ip address
);

ip_encap ip_encap_inst (
    .clk                (   tx_clk                      ),  //  [I] [     ] Module clock
    .rst_n              (   rst_n                       ),  //  [I] [     ] Module reset signal, active-low
    
    // downstream: ethernet pack encapsulation
    .encap_busy         (   encap_busy                  ),  //  [I] [     ] encapsulation module busy state input, we only send packet when this signal is 1&apos;b0
    .s_axis_td          (   s_axis_td                   ),  //  [O] [07:00] axis send data output to encapsulation module
    .s_axis_tdv         (   s_axis_tdv                  ),  //  [O] [     ] axis send data valid output to encapsulation module
    .s_axis_tend        (   s_axis_tend                 ),  //  [O] [     ] axis send tend output to encapsulation module
    .s_axis_trdy        (   s_axis_trdy                 ),  //  [I] [     ] axis send ready signal
    
    // upstream: tcp/udp/icmp tx output
    .s_ip_axis_td       (   s_ip_axis_td                ),  //  [I] [07:00] IP data AXI-Stream input
    .s_ip_axis_tdv      (   s_ip_axis_tdv               ),  //  [I] [     ] IP data receive valid, active-high
    .s_ip_axis_tend     (   s_ip_axis_tend              ),  //  [I] [     ] IP data receive end
    .s_ip_axis_trdy     (   s_ip_axis_trdy              ),  //  [O] [     ] IP data receive ready

    // IP header
    .data_length        (   ip_encap_data_length        ),  //  [I] [15:00] Data segment length in bytes
    .ip_ttl             (   ip_encap_ttl                ),  //  [I] [07:00] Time to live.
    .ip_ul_protocol     (   ip_encap_ul_protocol        ),  //  [I] [07:00] Upper-layer protocol, (ICMP=8&apos;d1, TCP=8&apos;d6, UDP=8&apos;d17)
    .ip_src_ip          (   ip_encap_src_ip             ),  //  [I] [31:00] Source ip address
    .ip_dst_ip          (   ip_encap_dst_ip             ),  //  [I] [31:00] Destination ip address

    // States output
    .ip_tx_err_busy     (   ip_encap_tx_err_busy        )   //  [O] [     ] TX error when dowmstream encapsulation module is busy, at this time, upstream must stop transmission.
);

ip_decap ip_decap_inst (
    .clk                (   rx_clk                      ),  //  [I] [     ] Module clock
    .rst_n              (   rst_n                       ),  //  [I] [     ] Module reset signal, active-low
    .eth_type           (   ip_rx_eth_type              ),  //  [I] [15:00] received ethertype
    .board_ip           (   board_ip                    ),  //  [I] [31:00] board ip address, for comparison whith received destination ip address
    // upstream: ethernet pack decapsulation
    .decap_busy         (   decap_busy                  ),  //  [I] [     ] decapsulation module busy state input
    .s_axis_rd          (   s_axis_rd                   ),  //  [I] [07:00] axis receive data input from decapsulation module
    .s_axis_rdv         (   s_axis_rdv                  ),  //  [I] [     ] axis receive data valid input from decapsulation module
    .s_axis_rend        (   s_axis_rend                 ),  //  [I] [     ] axis receive tend input from decapsulation module
    .s_axis_rrdy        (   s_axis_rrdy                 ),  //  [O] [     ] axis receive ready signal

    // IP payload output, to downstream: tcp/udp/icmp
    .s_ip_axis_rrdy     (   s_ip_axis_rrdy              ),  //  [I] [     ] IP downstream rx ready
    .s_ip_axis_rd       (   s_ip_axis_rd                ),  //  [O] [07:00] IP data AXI-Stream output
    .s_ip_axis_rdv      (   s_ip_axis_rdv               ),  //  [O] [     ] IP data output valid, active-high
    .s_ip_axis_rend     (   s_ip_axis_rend              ),  //  [O] [     ] IP data output end

    // output received ip header
    .ip_valid           (   ip_valid                    ),  //  [O] [     ] Active-high only received destination ip address is board-ip
    .ip_version         (   ip_decap_version            ),  //  [O] [03:00] IP version, 4&apos;b4: ipv4, 4&apos;b6: ipv6
    .ip_header_len      (   ip_decap_header_len         ),  //  [O] [03:00] IP header length, In units of 32 bits (4 bytes).
    .ip_tos             (   ip_decap_tos                ),  //  [O] [07:00] IP type of service
    .ip_total_len       (   ip_decap_total_len          ),  //  [O] [15:00] IP total length (include payload)
    .ip_id              (   ip_decap_id                 ),  //  [O] [15:00] IP identification
    .ip_flag            (   ip_decap_flag               ),  //  [O] [02:00] IP Flag, bit2: reserved, bit1: 1&apos;b1 is allowing fragmentation, else disallowing. bit0: 1&apos;b1 is more frame, 1&apos;b0 is the last frame.
    .ip_fragment_offset (   ip_decap_fragment_offset    ),  //  [O] [12:00] IP fragment offset
    .ip_ttl             (   ip_decap_ttl                ),  //  [O] [07:00] IP time to live
    .ip_ul_protocol     (   ip_decap_ul_protocol        ),  //  [O] [07:00] IP upper-layer protocol (ICMP = 8&apos;d1, TCP=8&apos;d6, UDp=8&apos;d17)
    .ip_checksum        (   ip_decap_checksum           ),  //  [O] [15:00] IP checksum
    .ip_src_addr        (   ip_decap_src_addr           ),  //  [O] [31:00] Source ip address
    .ip_dst_addr        (   ip_decap_dst_addr           )   //  [O] [31:00] Destination ip address
);

endmodule&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/70562cef</id>,<title>[blog-MT-04] - 用 Umami 为博客搭建数据统计服务</title>,<link href="https://blog.tyh123.top/posts/70562cef" rel="alternate" type="text/html"/>,<published>2026-05-29T14:42:31.000Z</published>,<updated>2026-05-29T14:42:31.000Z</updated>,<summary type="text">记录用 Docker 部署 Umami 数据统计服务的过程, 包括修改 docker-compose 配置、初始化数据库、进入后台并接入博客统计脚本。</summary>,<content type="html">&lt;h2 id=&quot;需要做的工作&quot;&gt;需要做的工作&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;拥有一台云服务器&lt;/li&gt;
&lt;li&gt;拥有一个已经部署好的 umami 实例，本篇会介绍如何部署 umami 实例&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;部署-umami-实例&quot;&gt;部署 umami 实例&lt;/h2&gt;
&lt;p&gt;这里采用 docker 安装，在这之前你需要在云服务器上安装好 &lt;code&gt;docker&lt;/code&gt; 服务，推荐使用&lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fwww.bt.cn%2Fnew%2Findex.html&quot;&gt;宝塔面板&lt;/a&gt;来一键安装 docker 服务，本篇基于宝塔面板来介绍。&lt;/p&gt;
&lt;p&gt;首先需要将 umami 的 github 仓库克隆下来&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ git clone https://github.com/umami-software/umami.git&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后进入刚刚克隆好的仓库目录下，找到 &lt;code&gt;docker-compose.yml&lt;/code&gt;, 修改数据库的名字&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;change-doker-compose&quot; width=&quot;903&quot; height=&quot;671&quot; src=&quot;https://blog.tyh123.top/_astro/change-doker-compose.Di1EqmfZ_Z1fFsYU.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;以下内容不修改也是可以的，但是为了安全起见建议修改&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ports&lt;/strong&gt;: umami 实例的端口映射，建议修改一下&lt;strong&gt;宿主机&lt;/strong&gt;的端口(冒号前面的)，不要用默认的端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DATABASE_URL&lt;/strong&gt;: 格式为 &lt;code&gt;postgresql://用户名:密码@db:端口号/数据库名&lt;/code&gt; 这里的用户名、密码、数据库名对应 &lt;code&gt;db&lt;/code&gt; 中 的 &lt;code&gt;POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB&lt;/code&gt; 这三个值，端口号目前我不知道怎么改，如果有知道的大佬可以在评论区评论一下（&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;APP_SECRET&lt;/strong&gt;: 改成一串随机的字符串&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB&lt;/strong&gt;: 在上面已经提到了，是数据库的用户名、密码、数据库名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;改完配置之后运行下面的命令:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ docker compose up -d&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后在宝塔面板的 Docker 服务中就可以看到我们刚刚创建好的两个容器了，一个是 umami，另外一个是数据库。
&lt;img alt=&quot;docker-containers&quot; width=&quot;1638&quot; height=&quot;264&quot; src=&quot;https://blog.tyh123.top/_astro/docker-containers.KRyMFFPz_Z29jun0.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;进入-umami-后台&quot;&gt;进入 umami 后台&lt;/h2&gt;
&lt;p&gt;部署完成后，可以访问 &lt;code&gt;http://服务器公网ip:端口(宿主机)&lt;/code&gt; 进入 umami 后台了，默认用户名和密码为 &lt;code&gt;admin&lt;/code&gt; 和 &lt;code&gt;umami&lt;/code&gt;,&lt;/p&gt;

&lt;p&gt;也可以使用反向代理将域名解析到你的服务器 ip 上去掉端口，之前有介绍过这里就不说了。&lt;/p&gt;

&lt;h3 id=&quot;创建网站&quot;&gt;创建网站&lt;/h3&gt;
&lt;p&gt;在 umami 后台中依次点击 &lt;code&gt;网站 -&gt; 添加网站&lt;/code&gt;，之后输入网站名称和网站的 &lt;strong&gt;域名(不带 &lt;code&gt;https://&lt;/code&gt;)&lt;/strong&gt; 就好了&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;add website&quot; width=&quot;1773&quot; height=&quot;680&quot; src=&quot;https://blog.tyh123.top/_astro/add-website.BDDesLaR_yzyHL.webp&quot;&gt;&lt;/p&gt;

&lt;p&gt;初次进入界面时语言可能是英文，这时可以点击左下角 &lt;code&gt;admin -&gt; Language -&gt; 往下滑找到中文&lt;/code&gt; 就可以改成中文了。&lt;/p&gt;

&lt;h3 id=&quot;开始统计数据&quot;&gt;开始统计数据&lt;/h3&gt;
&lt;p&gt;点击右侧的 “编辑” 图标
&lt;img alt=&quot;edit website&quot; width=&quot;1232&quot; height=&quot;126&quot; src=&quot;https://blog.tyh123.top/_astro/edit-website.7KVCCt4H_Z1zi6xM.webp&quot;&gt;
找到 “跟踪代码” 块, 将这部分代码复制到 &lt;code&gt;&amp;#x3C;header&gt; ... &amp;#x3C;/header&gt;&lt;/code&gt; 标签页内即可开始跟踪数据。
&lt;img alt=&quot;Tracking code&quot; width=&quot;844&quot; height=&quot;238&quot; src=&quot;https://blog.tyh123.top/_astro/tracking-code.DIwKwqq5_ZR3nF3.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;分享数据&quot;&gt;分享数据&lt;/h3&gt;
&lt;p&gt;在编辑网站页面找到 “分享” 块，点击添加，然后选择你要分享的数据，选择好后点击保存会生成一个分享链接，将这个链接放置到你的博客网站上，这样访客就可以直接查看到你的网站统计数据，而无需登录到 umami 后台，确保数据安全。&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="umami"/>,<category term="数据统计"/>,<category term="Docker"/>,<category term="建站"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/435c3f95</id>,<title>[blog-MT-03] - 通过域名访问宝塔面板，并设置反向代理来移除端口号</title>,<link href="https://blog.tyh123.top/posts/435c3f95" rel="alternate" type="text/html"/>,<published>2026-05-27T21:34:08.000Z</published>,<updated>2026-05-27T21:34:08.000Z</updated>,<summary type="text">记录把域名解析到宝塔面板, 并通过反向代理去掉访问端口号的配置过程, 同时整理 SSL 证书申请与浏览器缓存导致的踩坑记录。</summary>,<content type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;为了给博客搭建评论系统，我购买了一台云服务器来托管评论，选用的评论系统是 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fartalk.js.org%2F&quot;&gt;Artalk&lt;/a&gt;, 在折腾服务器的过程中，自然少不了配置宝塔面板。但通过 IP + 端口 的方式访问面实在是不太优雅。因此在这篇记录中，我会介绍如何将域名解析到宝塔面板，并通过反向代理去掉端口号，实现更简洁的访问方式。&lt;/p&gt;
&lt;h2 id=&quot;为域名添加-dns-解析记录&quot;&gt;为域名添加 DNS 解析记录&lt;/h2&gt;
&lt;p&gt;添加记录很简单，我这里使用的是 cloudflare dns 解析，添加一个 A 记录解析到云服务器的公网 ip 即可。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;ADD RECORD&quot; width=&quot;1101&quot; height=&quot;266&quot; src=&quot;https://blog.tyh123.top/_astro/add-a-type-record.Cgq6AQhy_Z1BuUNq.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;宝塔面板设置&quot;&gt;宝塔面板设置&lt;/h2&gt;
&lt;p&gt;首先来到 &lt;code&gt;网站 -&gt; 反向代理&lt;/code&gt; 页面，然后点击添加反代。
&lt;img alt=&quot;ADD REVERSE PROXY&quot; width=&quot;1430&quot; height=&quot;747&quot; src=&quot;https://blog.tyh123.top/_astro/add-reverse-proxy.3Q0-mVbr_1zWoo.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后配置内容如下图所示
&lt;img alt=&quot;PROXY SETTINGS&quot; width=&quot;691&quot; height=&quot;666&quot; src=&quot;https://blog.tyh123.top/_astro/reverse-proxy-settings.DVh7b39P_hbx1N.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URL 地址就是 &lt;code&gt;127.0.0.1:[宝塔端口]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;发送域名不用管，就用 &lt;code&gt;$http_host&lt;/code&gt; 就可以，这部分在你填写 URL 地址时就已经填好了&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;踩坑记录&quot;&gt;踩坑记录&lt;/h2&gt;
&lt;h3 id=&quot;设置好后浏览器依旧显示不安全的网站但是证书有效&quot;&gt;&lt;strong&gt;设置好后，浏览器依旧显示不安全的网站，但是证书有效&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;这是因为浏览器缓存的原因，清除浏览记录，使用 inprivate 页面访问，或者换个设备尝试一下或许就正常了。我当时就是因为这个原因卡了有几个小时，结果居然就是这么个离谱的原因……&lt;/p&gt;
&lt;h3 id=&quot;设置好后访问提示-invalid-ssl-certificate&quot;&gt;&lt;strong&gt;设置好后，访问提示 Invalid SSL Certificate&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;img alt=&quot;invalid-ssl&quot; width=&quot;825&quot; height=&quot;177&quot; src=&quot;https://blog.tyh123.top/_astro/invalid-ssl.qKjo4IaA_Z1eeS9T.webp&quot;&gt;
这是因为没有配置 ssl 证书，可以在宝塔中申请一个 ssl 证书，之后就可以正常访问了。&lt;/p&gt;
&lt;p&gt;或者使用 CF 的源端服务器证书，有效期 15 年。下面介绍 CF 源端服务器证书申请方式：&lt;/p&gt;
&lt;p&gt;依次进入 &lt;code&gt;域名(Domain) -&gt; 概览(Overview) -&gt; 你的域名 -&gt; SSL/TLS -&gt; 源服务器 -&gt; 创建证书&lt;/code&gt;
&lt;img alt=&quot;alt text&quot; width=&quot;1861&quot; height=&quot;911&quot; src=&quot;https://blog.tyh123.top/_astro/apply-origin-ssl.CPAd5FGu_Z1WP1LM.webp&quot;&gt;
创建后会显示出证书和密钥，复制下来保存好，然后将 SSL 证书上传到宝塔即可。
&lt;img alt=&quot;alt text&quot; width=&quot;873&quot; height=&quot;626&quot; src=&quot;https://blog.tyh123.top/_astro/baota-ssl-settings.4zJlbt9w_2kJ65B.webp&quot;&gt;
生成好的证书是统配的，以后再需要创建反代或者站点的话直接使用同一个证书就行，很方便。&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="宝塔面板"/>,<category term="反向代理"/>,<category term="SSL"/>,<category term="建站"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/9942d232</id>,<title>[blog-MT-02] - 使用 cloudflare pages 托管博客网页的静态资源</title>,<link href="https://blog.tyh123.top/posts/9942d232" rel="alternate" type="text/html"/>,<published>2026-05-20T09:43:19.000Z</published>,<updated>2026-05-20T09:43:19.000Z</updated>,<summary type="text">介绍如何用 Cloudflare Pages 托管博客静态资源, 从导入 Git 仓库、配置构建设置到自动部署与访问域名绑定的完整步骤。</summary>,<content type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;由于 github pages 在国内的访问速度实在是不稳定，在查询资料的时候发现 cloudflare 可以提供静态资源部署服务，并且访问速度还是不错的。所以本篇文章基于此而诞生。&lt;/p&gt;
&lt;h2 id=&quot;部署步骤&quot;&gt;部署步骤&lt;/h2&gt;
&lt;p&gt;在这之前请注册 cloudflare 账号。关于账号注册本篇不再赘述，跟着提示一步步走即可，或者直接使用 github 账号登陆，登陆后验证一下邮箱就行了。&lt;/p&gt;
&lt;h3 id=&quot;创建页面&quot;&gt;创建页面&lt;/h3&gt;
&lt;p&gt;在注册好并成功登录之后，我们会来到如图所示的界面，然后根据图中箭头所示进入对应的页面。
&lt;img alt=&quot;goto_pages&quot; width=&quot;1265&quot; height=&quot;936&quot; src=&quot;https://blog.tyh123.top/_astro/goto_cloud_flare_pages.BrPYfi6j_1x7MXY.webp&quot;&gt;
来到这个页面之后，我们点击右上角的 Create Application
&lt;img width=&quot;1265&quot; height=&quot;936&quot; src=&quot;https://blog.tyh123.top/_astro/works_pages_create_app._6Jub18m_Z1GMLw3.webp&quot;&gt;
然后就来到了这个页面，注意要选择下方的 Get started
&lt;img width=&quot;988&quot; height=&quot;595&quot; src=&quot;https://blog.tyh123.top/_astro/pages_create_app.Mr4RJJ_e_ZaIF3b.webp&quot;&gt;
选择从 git 导入
&lt;img width=&quot;837&quot; height=&quot;448&quot; src=&quot;https://blog.tyh123.top/_astro/get_started_page.Cpp8D-go_1QhXag.webp&quot;&gt;
选择你的 hexo &lt;strong&gt;源码仓库&lt;/strong&gt;，即包含文章 markdown 源码的仓库。 (注意：不是 github pages 仓库，你可以前往我图中的这个仓库查看一下：&lt;a href=&quot;https://github.com/ed333a/hexo_repo&quot;&gt;ed333a/hexo_repo&lt;/a&gt;)选择完毕后点击右下角的 Begining setup 蓝色按钮。
&lt;img width=&quot;1041&quot; height=&quot;880&quot; src=&quot;https://blog.tyh123.top/_astro/sel_repo.U6Yr17Rz_2mNK6B.webp&quot;&gt;
之后我们会来到这个页面，相关选项已在图中表示，配置好后往下翻找到 Save and Deploy 蓝色按钮，点击保存后开始自动部署。
&lt;img width=&quot;1052&quot; height=&quot;820&quot; src=&quot;https://blog.tyh123.top/_astro/pages_setup.Ce97AXRB_ZTFGHp.webp&quot;&gt;
之后自动部署好的静态网页将可以通过 &lt;code&gt;hexo-repo-e1a.pages.dev&lt;/code&gt; 这个链接访问。在这之前我已经创建好了一个静态资源网页，它的地址是 &lt;code&gt;hexo-repo-1qi.pages.dev&lt;/code&gt;。&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="Cloudflare Pages"/>,<category term="建站"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/f1ef2aaa</id>,<title>[blog-MT-01] - 使用 Astro/Hexo 框架的博客建站过程</title>,<link href="https://blog.tyh123.top/posts/f1ef2aaa" rel="alternate" type="text/html"/>,<published>2026-05-19T22:33:31.000Z</published>,<updated>2026-11-04T15:29:13.000Z</updated>,<summary type="text">记录从配置 Node.js 环境到安装 Hexo、初始化项目并生成静态页面的建站过程, 同时说明与 Astro 建站的差异和注意事项。</summary>,<content type="html">&lt;p&gt;注意: 文章中关于使用 hexo 框架建站的内容已不再适用于本站, 本站采用 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fastro.build%2F&quot;&gt;Astro&lt;/a&gt; 框架搭建。相关内容仅作参考, 请勿直接用于 Astro 框架的博客网站。&lt;/p&gt;

&lt;h2 id=&quot;关于-hexo&quot;&gt;关于 Hexo&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Hexo&lt;/strong&gt; 是一个基于 Node.js 编写的快速、简洁且高效的&lt;strong&gt;博客框架&lt;/strong&gt;。它通过解析 Markdown 格式的源文件，结合主题模板，一键生成静态网页资源；用户可以将这些静态资源文件部署到如 Github Pages 的静态资源托管服务站点上，无需后端环境或数据库，即可拥有高性能、易于维护的个人博客站点。&lt;/p&gt;
&lt;h2 id=&quot;配置-nodejs-环境&quot;&gt;配置 Node.js 环境&lt;/h2&gt;
&lt;p&gt;在安装 Hexo 之前，首先要配置 Node.js 环境，前往 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fnodejs.cn%2Fdownload%2F&quot;&gt;Node.js 中文网下载页面&lt;/a&gt; 下载适合你当前所使用的系统的 Node.js 安装包。&lt;/p&gt;

&lt;p&gt;使用 Windows 10 或更高版本的系统时，通常建议 &lt;strong&gt;直接下载安装包 [Windows 安装包(.msi)]&lt;/strong&gt; 进行安装，以免因 PowerShell 执行策略阻止未签名脚本运行，造成 npm 脚本无法正常执行。&lt;/p&gt;

&lt;h2 id=&quot;安装-hexo&quot;&gt;安装 Hexo&lt;/h2&gt;
&lt;p&gt;安装好 Node.js 后，使用以下命令来全局安装 hexo&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npm install hexo-cli -g&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完成后，在控制台中输入以下命令后，控制台中应出现 hexo-cli 的安装版本&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo -v&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img width=&quot;320&quot; height=&quot;47&quot; src=&quot;https://blog.tyh123.top/_astro/cmd_hexo_v.Cd8tDws5_G6CBK.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;在此次安装过程中，我的控制台中提示了以下内容，这种情况一般是在安装 Node.js 时没有配置好系统变量。将 &lt;code&gt;%appdata%/npm&lt;/code&gt; 这个路径添加到系统变量中即可 (复制到文件资源管理器中的地址栏，然后获得绝对路径)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo: 无法将 “hexo” 项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写，如果包括路径，请确保路径正确，然后再试一次。&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;初始化-hexo-目录&quot;&gt;初始化 Hexo 目录&lt;/h2&gt;
&lt;p&gt;完成 Hexo 的安装后，在&lt;strong&gt;适当的位置&lt;/strong&gt;创建一个文件夹，用来存放 hexo 的项目文件。
&lt;img width=&quot;694&quot; height=&quot;191&quot; src=&quot;https://blog.tyh123.top/_astro/hexo_project_folder.lmRiHadN_C7G0o.webp&quot;&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;这里的文件夹仅作为演示使用，无须严格按照图中所示的路径创建，你可以依照自己的文件管理习惯自己创建&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;进入这个文件夹&lt;/strong&gt;，然后执行以下命令&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo init&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后 Hexo 将会在文件夹下初始化一些必要文件，如图所示。
&lt;img width=&quot;726&quot; height=&quot;410&quot; src=&quot;https://blog.tyh123.top/_astro/hexo_project_init.-QbeuEyg_Z2eLwjP.webp&quot;&gt;&lt;/p&gt;

&lt;p&gt;Hexo 初始化时会从 Github 克隆一个模板仓库，有时受限于网络波动等因素，你可能无法完成初始化，此时请考虑&lt;strong&gt;使用网络代理&lt;/strong&gt;或其它方式完成初始化。&lt;/p&gt;

&lt;p&gt;初始化完成后，使用以下命令安装 Node.js 模块&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ npm install&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，你已完成 Hexo 建站的所有前置条件，使用以下命令即可开启一个本地服务器用来预览你的 Hexo 页面&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo s&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在浏览器中输入控制台输出的地址 &lt;code&gt;localhost:4000&lt;/code&gt; 即可预览你的 hexo 页面
&lt;img width=&quot;1170&quot; height=&quot;856&quot; src=&quot;https://blog.tyh123.top/_astro/hexo_page_hello_world.CIjZ5Bsx_1D5ts3.webp&quot;&gt;&lt;/p&gt;

&lt;p&gt;当默认端口被占用时，你可以在命令后面增加参数 &lt;code&gt;-p [端口号]&lt;/code&gt; 来指定一个未占用的端口。&lt;/p&gt;
&lt;p&gt;命令如下&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo s -p 4001&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;生成静态资源&quot;&gt;生成静态资源&lt;/h2&gt;
&lt;p&gt;使用以下命令可以生成部署到 Github Pages 的静态资源&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ hexo g&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成好的静态资源位于 &lt;code&gt;public&lt;/code&gt; 文件夹下&lt;/p&gt;</content>,<category term="博客维护日记"/>,<category term="hexo"/>,<category term="Astro"/>,<category term="建站"/>,<category term="经验分享"/>,<category term="web"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/52a1f482</id>,<title>FPGA SPI 通信</title>,<link href="https://blog.tyh123.top/posts/52a1f482" rel="alternate" type="text/html"/>,<published>2026-04-19T14:38:34.000Z</published>,<updated>2026-04-19T14:38:34.000Z</updated>,<summary type="text">SPI 是典型的同步串行通信接口, 本文从四线式与三线式接线讲起, 说明 SCLK、MOSI、MISO、CS 信号的作用和主从通信时序, 并记录 FPGA 上的 Verilog 实现思路。</summary>,<content type="html">&lt;h2 id=&quot;简介&quot;&gt;简介&lt;/h2&gt;
&lt;p&gt;在之前讲 ${post 96d23a91} 的时候，有讲到过&lt;strong&gt;串口通信是异步通信&lt;/strong&gt;，而 SPI 通信是一个&lt;strong&gt;典型的同步通信&lt;/strong&gt;。它需要&lt;strong&gt;主设备驱动时钟信号线&lt;/strong&gt;，所有数据位的发送和接收都在时钟的边沿触发，收发双方不再需要各自校准波特率，也不用起始位/停止位等操作，因此相比于异步的串口通信，这种通信方式效率更高，时序更稳定。缺点则是占用了更多的管脚，在 PCB 的布局布线上相比于串口复杂一些。&lt;/p&gt;
&lt;h2 id=&quot;标准-spi四线式-接口&quot;&gt;标准 SPI(四线式) 接口&lt;/h2&gt;
&lt;p&gt;标准 SPI 接口采用的是四线式接线方式，有 SCLK、MOSI、MISO 以及 SS#/CS# 四根信号线。这种方式的 SPI 接口支持全双工通信，即同时接收和发送。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;SCK/SCLK&lt;/strong&gt;：SPI 的串行时钟，由主设备 (Master) 驱动。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MOSI&lt;/strong&gt;：全称：Master Out Slave In，是标准 SPI 的一根数据线，由&lt;strong&gt;主机输出&lt;/strong&gt;，从机输入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MISO&lt;/strong&gt;：全称：Master In Slave Out，是标准 SPI 的一根数据线，由&lt;strong&gt;从机输出&lt;/strong&gt;，主机输入&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SS#/CS#&lt;/strong&gt;：全称：Slave Select/Chip Select 由&lt;strong&gt;主机驱动的片选信号线&lt;/strong&gt;，用于多设备共享一个 SPI 总线时，选定指定设备有效的信号。&lt;strong&gt;该信号低电平有效&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;关于片选信号线：从机设备片选信号线为高电平时，不会接收来自 MOSI 数据线上的数据，同时设备本身也不会输出任何数据。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;下面是一张多设备共享 SPI 通信的连接方式图
&lt;img alt=&quot;SPI 一主多从接线方式&quot; width=&quot;648&quot; height=&quot;365&quot; src=&quot;https://blog.tyh123.top/_astro/spi_wiring.BOU4TkGs_4OM1S.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;半双工-spi-三线式-通信&quot;&gt;半双工 SPI (三线式) 通信&lt;/h2&gt;
&lt;p&gt;该种接线方式将数据接口 (MISO、MOSI) 整合成了一个双向数据接口 (SDIO)，不是标准的 SPI 形式，这种 SPI 通信&lt;strong&gt;只支持半双工通信&lt;/strong&gt;，但这是一种&lt;strong&gt;常用和被广泛支持的变体&lt;/strong&gt;，通常称为 “三线 SPI” 或 “半双工 SPI”。三线式 SPI 通信同样支持多设备共享 SDIO 总线。&lt;/p&gt;
&lt;p&gt;用一张表格来对比四线式 SPI 和 三线式 SPI 的区别：&lt;/p&gt;






























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;对比项&lt;/th&gt;&lt;th&gt;标准 SPI (四线式)&lt;/th&gt;&lt;th&gt;三线 SPI (四线式)&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;信号线&lt;/td&gt;&lt;td&gt;&lt;strong&gt;4根&lt;/strong&gt;，SCLK, MOSI, &lt;strong&gt;MISO&lt;/strong&gt;, CS&lt;/td&gt;&lt;td&gt;&lt;strong&gt;3根&lt;/strong&gt;：SCLK, &lt;strong&gt;SDIO&lt;/strong&gt;, CS&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;数据线&lt;/td&gt;&lt;td&gt;&lt;strong&gt;两根独立&lt;/strong&gt;：发送 (MOSI) 和接收 (MISO) 分开&lt;/td&gt;&lt;td&gt;&lt;strong&gt;一根共用&lt;/strong&gt;：发送和接收都通过同一条 SDIO 线&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;传输模式&lt;/td&gt;&lt;td&gt;&lt;strong&gt;全双工&lt;/strong&gt;：可同时发送和接收&lt;/td&gt;&lt;td&gt;&lt;strong&gt;半双工&lt;/strong&gt;：不能同时进行发送和接收，同一时间只能进行一种操作。&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;引脚数量&lt;/td&gt;&lt;td&gt;较多&lt;/td&gt;&lt;td&gt;较少，节省I/O资源&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;关于 SDIO 如何切换数据方向&lt;/strong&gt;：这个要看具体的芯片数据手册，每款芯片的数据手册对于换向的时机都是不一样的。如我调试时的某款 ADC 芯片 SDIO 通信时序如下 (&lt;strong&gt;高位先发&lt;/strong&gt;)：&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;SPI-SDIO通信时序&quot; width=&quot;816&quot; height=&quot;199&quot; src=&quot;https://blog.tyh123.top/_astro/spi_sdio_timing.B_ndvs5R_2dtA8t.webp&quot;&gt;&lt;/p&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;bit&lt;/th&gt;&lt;th&gt;功能&lt;/th&gt;&lt;th&gt;功能介绍&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;23&lt;/td&gt;&lt;td&gt;R/W#&lt;/td&gt;&lt;td&gt;读写控制位，读操作时写 1，写操作时写 0&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;[22:08]&lt;/td&gt;&lt;td&gt;A0 to A14&lt;/td&gt;&lt;td&gt;15 位寄存器地址&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;[07:00]&lt;/td&gt;&lt;td&gt;Do to D7&lt;/td&gt;&lt;td&gt;7 位数据，读操作时为向寄存器写入该数据，写操作时为读回指定地址的寄存器数据，&lt;strong&gt;读操作时此时的 SDIO 方向为输入&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;spi-的四种通信模式&quot;&gt;SPI 的四种通信模式&lt;/h2&gt;
&lt;p&gt;由 &lt;strong&gt;CPOL(时钟极性)&lt;/strong&gt; 和 &lt;strong&gt;CPHA(时钟相位)&lt;/strong&gt; 控制&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CPOL(时钟极性)&lt;/strong&gt;: 控制 &lt;strong&gt;SCLK&lt;/strong&gt; 电平在空闲时的状态
&lt;ul&gt;
&lt;li&gt;若 &lt;strong&gt;CPOL=0&lt;/strong&gt; 则当 SPI 总线&lt;strong&gt;空闲时&lt;/strong&gt;, SCLK 处于&lt;strong&gt;低电平&lt;/strong&gt;状态&lt;/li&gt;
&lt;li&gt;若 &lt;strong&gt;CPOL=1&lt;/strong&gt; 则当 SPI 总线&lt;strong&gt;空闲时&lt;/strong&gt;, SCLK 处于&lt;strong&gt;高电平&lt;/strong&gt;状态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPHA(时钟相位)&lt;/strong&gt;: 控制数据的采样边沿
&lt;ul&gt;
&lt;li&gt;若 &lt;strong&gt;CPHA=0&lt;/strong&gt; 则在时钟信号的&lt;strong&gt;第一个跳变沿&lt;/strong&gt; (通常是&lt;strong&gt;上升沿&lt;/strong&gt;) 进行数据采样&lt;/li&gt;
&lt;li&gt;若 &lt;strong&gt;CPHA=1&lt;/strong&gt; 则在时钟信号的&lt;strong&gt;第二个跳变沿&lt;/strong&gt; (通常是&lt;strong&gt;下降沿&lt;/strong&gt;) 进行数据采样&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;



































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;模式&lt;/th&gt;&lt;th&gt;CPOL&lt;br&gt;(时钟极性)&lt;/th&gt;&lt;th&gt;CPHA&lt;br&gt;(时钟相位)&lt;/th&gt;&lt;th&gt;说明&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;空闲时低电平, 上升沿采集&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;空闲时低电平, 下降沿采集&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;2&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;空闲时高电平, 上升沿采集&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;3&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;空闲时高电平, 下降沿采集&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;spi-模块参数以及端口定义&quot;&gt;SPI 模块参数以及端口定义&lt;/h2&gt;

































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;参数&lt;/th&gt;&lt;th&gt;功能描述&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;DATA_WIDTH&lt;/td&gt;&lt;td&gt;数据位宽&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CPOL&lt;/td&gt;&lt;td&gt;时钟极性&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CPHA&lt;/td&gt;&lt;td&gt;时钟相位&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;DLK_DIV&lt;/td&gt;&lt;td&gt;SCLK 的时钟分频系数&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;MOSI_IDLE_STATE&lt;/td&gt;&lt;td&gt;MOSI 信号线在空闲时的状态&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;CONTINUOUS_CLK&lt;/td&gt;&lt;td&gt;是否产生连续的时钟信号，若设置为 1’b0，则表示时钟仅在 SPI 活跃时产生。若设置为 1’b1，则表示时钟信号始终产生&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;端口&lt;/th&gt;&lt;th&gt;I/O&lt;/th&gt;&lt;th&gt;作用&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;clk&lt;/td&gt;&lt;td&gt;I&lt;/td&gt;&lt;td&gt;模块的时钟输入&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;rst_n&lt;/td&gt;&lt;td&gt;I&lt;/td&gt;&lt;td&gt;模块复位信号，低电平有效&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;dat_tx[N-1:0]&lt;/td&gt;&lt;td&gt;I&lt;/td&gt;&lt;td&gt;要通过 SPI 总线发送的数据（N 为数据位宽）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;dat_rx[N-1:0]&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;由从机接收到的数据（N 为数据位宽）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;start_op&lt;/td&gt;&lt;td&gt;I&lt;/td&gt;&lt;td&gt;一个时钟周期的脉冲信号，用于开始 SPI 通信&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spi_end&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;一个时钟周期的脉冲信号，表示 SPI 发送或接收完成&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spi_sclk&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;spi 的时钟信号线&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spi_cs_n&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;spi 的片选信号线&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spi_miso&lt;/td&gt;&lt;td&gt;I&lt;/td&gt;&lt;td&gt;SPI 的一根数据线，由&lt;strong&gt;从机输出&lt;/strong&gt;，主机输入&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;spi_mosi&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;SPI 的一根数据线，由&lt;strong&gt;主机输出&lt;/strong&gt;，从机输入&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;data_bit_cnt&lt;/td&gt;&lt;td&gt;O&lt;/td&gt;&lt;td&gt;发送的比特位计数器，用于在其顶层实现 SDIO 通信&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;module spi_master #(
    parameter                       DATA_WIDTH          = 4&apos;d8             , // data width
    parameter                       CPOL                = 1&apos;b0              , // Clock polarity
    parameter                       CPHA                = 1&apos;b0              , // Clock phase
    parameter                       CLK_DIV             = 8&apos;d10             , // input clk division, used to generate SCLK.
    parameter                       MOSI_IDLE_STATE     = 1&apos;b0              , // when module goes to idle. the MOSI line logic level will be set to this value  
    parameter                       CONTINUOUS_CLK      = 1&apos;b0                // when set to 1&apos;b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1&apos;b0.
)(
    input                           clk                                     , // module clock
    input                           rst_n                                   , // module reset signal, active low
    
    input       [DATA_WIDTH-1:0]    dat_tx                                  , // the data to be transmitted to slave device
    output  reg [DATA_WIDTH-1:0]    dat_rx                                  , // the data received from the slave device
    input                           start_op                                , // pulse, trigger transfer dat operation

    output  reg                     spi_end                                 , // signal is high when transferring data
    
    // PHY signals      
    output                          spi_sclk                                , // serial clock
    output  reg                     spi_cs_n                                , // chip select signal, active low
    input                           spi_miso                                , // master in slave out
    output  reg                     spi_mosi                                , // master out slave in
    output  reg [7:0           ]    data_bit_cnt                              // bit transmit counter
);
// ...

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;串行时钟的产生&quot;&gt;串行时钟的产生&lt;/h2&gt;
&lt;p&gt;通过计数器产生 &lt;code&gt;sclk_inv&lt;/code&gt; 信号，该信号为一个时钟周期的脉冲信号，每一次脉冲信号都代表着 SCLK 信号将会在下一个时钟周期跳变一次。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// REGION_HEADER------------------------------------------------------------------------------------
reg                         spi_sclk_inv                ; // This signal is a pulse signal with a clk period, and on its rising edge, the SCLK signal will invert once.
reg                         spi_sclk_internal           ; // internal serial clock divied from module clock

// generate spi_sclk_inv signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        clk_cnt         &amp;#x3C;=  8&apos;d0                    ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b0                   ;
    end
    else if (clk_cnt == CLK_DIV - 1&apos;b1) begin
        clk_cnt         &amp;#x3C;=  8&apos;d0                    ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b1                   ;
    end
    else if (clk_cnt == CLK_DIV[7:1] - 1&apos;b1) begin
        clk_cnt         &amp;#x3C;=  clk_cnt + 1&apos;b1          ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b1                   ;
    end
    else begin
        clk_cnt         &amp;#x3C;=  clk_cnt + 1&apos;b1          ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b0                   ;
    end
end

// generate spi_sclk_internal signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_sclk_internal   &amp;#x3C;=  1&apos;b0                ;
    end else if (spi_sclk_inv) begin
        spi_sclk_internal   &amp;#x3C;=  !spi_sclk_internal  ;
    end else begin
        spi_sclk_internal   &amp;#x3C;=   spi_sclk_internal  ;
    end
end

// assign continouous clock
generate
    if (CONTINUOUS_CLK == 1&apos;b1)
        assign spi_sclk = spi_sclk_internal ^ CPOL                                  ;
    else
        assign spi_sclk = (spi_cs_n == 1&apos;b1) ? CPOL : (spi_sclk_internal ^ CPOL)    ;
endgenerate
// REGION_FOOTER------------------------------------------------------------------------------------&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;时钟边沿的判断&quot;&gt;时钟边沿的判断&lt;/h2&gt;
&lt;p&gt;根据内部的 &lt;code&gt;sclk_internal&lt;/code&gt; 信号，即可判断上升沿和下降沿。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;sclk_internal&lt;/code&gt; 是&lt;strong&gt;高电平&lt;/strong&gt;且 &lt;code&gt;spi_sclk_inv&lt;/code&gt; 产生了脉冲信号的时候，表示 &lt;code&gt;sclk_internal&lt;/code&gt; 将会&lt;strong&gt;在下个时钟周期跳变到低电平&lt;/strong&gt;，此时则为 &lt;code&gt;sclk_internal&lt;/code&gt; 的下降沿。&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;sclk_internal&lt;/code&gt; 是&lt;strong&gt;低电平&lt;/strong&gt;且 &lt;code&gt;spi_sclk_inv&lt;/code&gt; 产生了脉冲信号的时候，表示 &lt;code&gt;sclk_internal&lt;/code&gt; 将会&lt;strong&gt;在下个时钟周期跳变到高电平&lt;/strong&gt;，此时则为 &lt;code&gt;sclk_internal&lt;/code&gt; 的上升沿。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;update_edge&lt;/code&gt; 和 &lt;code&gt;sample_edge&lt;/code&gt; 则表示 SPI 数据的更新边沿和采样边沿，依照 CPHA 参数的设置在上升沿或下降沿更新/采样数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wire    spi_sclk_negedge    =   spi_sclk_internal &amp;#x26; spi_sclk_inv                ; // pulse signal, indicates SCLK negedge
wire    spi_sclk_posedge    =  !spi_sclk_internal &amp;#x26; spi_sclk_inv                ;
wire    update_edge         = (CPHA == 0) ? spi_sclk_negedge : spi_sclk_posedge ; // Select update and sample edges based on CPHA
wire    sample_edge         = (CPHA == 0) ? spi_sclk_posedge : spi_sclk_negedge ;&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;完整的-spi-代码-以及搭建仿真测试&quot;&gt;完整的 SPI 代码, 以及搭建仿真测试&lt;/h2&gt;
&lt;h3 id=&quot;完整代码&quot;&gt;完整代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module spi_master #(
    parameter                       DATA_WIDTH          = 5&apos;d24             , // data width, [1bit RW ctrl, 15 bits register addr, 8 bits data]
    parameter                       CPOL                = 1&apos;b0              , // Clock polarity
    parameter                       CPHA                = 1&apos;b0              , // Clock phase
    parameter                       CLK_DIV             = 8&apos;d10             , // input clk division, used to generate SCLK.
    parameter                       MOSI_IDLE_STATE     = 1&apos;b0              , // when module goes to idle. the MOSI line logic level will be set to this value   
    parameter                       CONTINUOUS_CLK      = 1&apos;b0                // when set to 1&apos;b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1&apos;b0.
)(
    input                           clk                                     , // module clock
    input                           rst_n                                   , // module reset signal, active low

    input       [DATA_WIDTH-1:0]    dat_tx                                  , // the data to be transmitted to slave device
    output  reg [DATA_WIDTH-1:0]    dat_rx                                  , // the data received from the slave device

    input                           start_op                                , // pulse, trigger transfer dat operation
    output  reg                     spi_end                                 , // signal is high when transferring data

    // PHY signals      
    output                          spi_sclk                                , // serial clock
    output  reg                     spi_cs_n                                , // chip select signal, active low
    input                           spi_miso                                , // master in slave out
    output  reg                     spi_mosi                                , // master out slave in
    output  reg [7:0           ]    data_bit_cnt                              // bit transmit counter
);

// NOTE_HEADER--------------------------------------------------------------------------------------
// SPI Modes | CPOL             | CPHA          | Note
//           | (Clock Polarity) | (Clock Phase) | 
// ==========+==================+===============+===================================================
//         0 |                0 |             0 | Clock low  level when idle, data sampled on rising  edge
//         1 |                0 |             1 | Clock low  level when idle, data sampled on falling edge
//         2 |                1 |             0 | Clock high level when idle, data sampled on rising  edge
//         3 |                1 |             1 | Clock high level when idle, data sampled on falling edge
// NOTE_FOOTER--------------------------------------------------------------------------------------

//================================================================================
// local parameter declarations
//================================================================================

//================================================================================
// reg declarations
//================================================================================
reg [DATA_WIDTH-1:0 ]       data_tx_buffer              ; // tx data buffer, shift out to mosi
reg [DATA_WIDTH-1:0 ]       data_rx_buffer              ; // rx data buffer, shift in from miso
reg [7:0            ]       clk_cnt                     ; // a counter for frequency division

reg                         spi_sclk_inv                ; // This signal is a pulse signal with a clk period, and on its rising edge, the SCLK signal will invert once.
reg                         spi_sclk_internal           ; // internal serial clock divied from module clock

reg                         trans_end_d                 ;
reg                         trans_start                 ;
reg                         spi_start_op_d0             ;
reg                         spi_start_op_d1             ;
reg                         spi_busy                    ; // internal busy signal
//================================================================================
// wire declarations
//================================================================================
wire    spi_sclk_negedge    =   spi_sclk_internal &amp;#x26; spi_sclk_inv                ; // pulse signal, indicates SCLK negedge
wire    spi_sclk_posedge    =  !spi_sclk_internal &amp;#x26; spi_sclk_inv                ;
wire    update_edge         = (CPHA == 0) ? spi_sclk_negedge : spi_sclk_posedge ; // Select update and sample edges based on CPHA
wire    sample_edge         = (CPHA == 0) ? spi_sclk_posedge : spi_sclk_negedge ;

wire    spi_start_op_pulse                                          ;

//================================================================================
// assign declarations
//================================================================================
assign  spi_start_op_pulse  =   !spi_start_op_d0 &amp;#x26; spi_start_op_d1  ;

//================================================================================
// MAIN CODE
//================================================================================

// sync start_op signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_start_op_d0 &amp;#x3C;=  1&apos;b0            ;
        spi_start_op_d1 &amp;#x3C;=  1&apos;b0            ;
    end else begin
        spi_start_op_d0 &amp;#x3C;=  start_op        ;
        spi_start_op_d1 &amp;#x3C;=  spi_start_op_d0 ;
    end
end

// spi busy indicator
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_busy    &amp;#x3C;=  1&apos;b0                ;
    end else if (spi_start_op_pulse) begin
        spi_busy    &amp;#x3C;=  1&apos;b1                ;
    end else if (spi_end) begin
        spi_busy    &amp;#x3C;=  1&apos;b0                ;
    end else begin
        spi_busy    &amp;#x3C;=  spi_busy            ;
    end
end

// REGION_HEADER------------------------------------------------------------------------------------
// generate spi_sclk_inv signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        clk_cnt         &amp;#x3C;=  8&apos;d0                    ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b0                   ;
    end
    else if (clk_cnt == CLK_DIV - 1&apos;b1) begin
        clk_cnt         &amp;#x3C;=  8&apos;d0                    ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b1                   ;
    end
    else if (clk_cnt == CLK_DIV[7:1] - 1&apos;b1) begin
        clk_cnt         &amp;#x3C;=  clk_cnt + 1&apos;b1          ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b1                   ;
    end
    else begin
        clk_cnt         &amp;#x3C;=  clk_cnt + 1&apos;b1          ;
        spi_sclk_inv     &amp;#x3C;=  1&apos;b0                   ;
    end
end

// generate spi_sclk_internal signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_sclk_internal   &amp;#x3C;=  1&apos;b0                ;
    end else if (spi_sclk_inv) begin
        spi_sclk_internal   &amp;#x3C;=  !spi_sclk_internal  ;
    end else begin
        spi_sclk_internal   &amp;#x3C;=   spi_sclk_internal  ;
    end
end

// assign continouous clock
generate
    if (CONTINUOUS_CLK == 1&apos;b1)
        assign spi_sclk = spi_sclk_internal ^ CPOL                                  ;
    else
        assign spi_sclk = (spi_cs_n == 1&apos;b1) ? CPOL : (spi_sclk_internal ^ CPOL)    ;
endgenerate
// REGION_FOOTER------------------------------------------------------------------------------------

// count data bit
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_bit_cnt    &amp;#x3C;=  8&apos;d0                            ;
    end 
    else if (
        spi_busy        &amp;#x26;&amp;#x26; 
        update_edge     &amp;#x26;&amp;#x26; 
        (data_bit_cnt  &amp;#x3C; DATA_WIDTH)
    ) begin
        data_bit_cnt    &amp;#x3C;=  data_bit_cnt + 1&apos;b1             ;
    end else if (update_edge &amp;#x26;&amp;#x26; data_bit_cnt == DATA_WIDTH) begin
        data_bit_cnt    &amp;#x3C;=  8&apos;d0                            ;
    end else begin
        data_bit_cnt    &amp;#x3C;=  data_bit_cnt                    ;
    end
end

// send data
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_mosi        &amp;#x3C;=  MOSI_IDLE_STATE                 ;
        data_tx_buffer  &amp;#x3C;=  {DATA_WIDTH{1&apos;b0}}              ;
    end else if (spi_start_op_pulse) begin
        data_tx_buffer  &amp;#x3C;=  dat_tx                          ;
    end 
    else if (
        spi_busy                        &amp;#x26;&amp;#x26;
        update_edge                     &amp;#x26;&amp;#x26;
        (data_bit_cnt &amp;#x3C; DATA_WIDTH)
    ) begin
        spi_mosi        &amp;#x3C;=  data_tx_buffer[DATA_WIDTH-1]    ;   // output data, MSB First
        data_tx_buffer  &amp;#x3C;=  {                                   // left-shift data
            data_tx_buffer[DATA_WIDTH-2:0]  ,
            data_tx_buffer[DATA_WIDTH-1]     
        };
    end else if (
        spi_busy                        &amp;#x26;&amp;#x26;
        update_edge                     &amp;#x26;&amp;#x26;
        (data_bit_cnt &gt;= DATA_WIDTH)
    ) begin
        spi_mosi        &amp;#x3C;=  MOSI_IDLE_STATE                 ;
        data_tx_buffer  &amp;#x3C;=  {DATA_WIDTH{1&apos;b0}}              ;
    end
end

// receive data
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_rx_buffer      &amp;#x3C;=  {DATA_WIDTH{1&apos;b0}}  ;
    end else if (spi_cs_n == 1&apos;b0 &amp;#x26;&amp;#x26; sample_edge) begin
        data_rx_buffer[0]   &amp;#x3C;=  spi_miso            ;   // sample data
        data_rx_buffer[DATA_WIDTH-1:1] &amp;#x3C;= data_rx_buffer[DATA_WIDTH-2:0];   // right-shif in sampled data to buffer
    end else begin
        data_rx_buffer      &amp;#x3C;=  data_rx_buffer      ;
    end
end

// move data from data_rx_buffer to data_rx output and generate spi_end signal 
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_end &amp;#x3C;=  1&apos;b0                ;
        dat_rx  &amp;#x3C;=  {DATA_WIDTH{1&apos;b0}}  ;
    end else if (update_edge &amp;#x26;&amp;#x26; (data_bit_cnt == DATA_WIDTH)) begin
        spi_end &amp;#x3C;=  1&apos;b1                ;
        dat_rx  &amp;#x3C;=  data_rx_buffer      ;
    end else begin
        spi_end &amp;#x3C;=  1&apos;b0                ;
    end
end

always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        trans_end_d &amp;#x3C;=  1&apos;b0            ;
    end else begin
        trans_end_d &amp;#x3C;=  spi_end         ;
    end
end

always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        trans_start &amp;#x3C;=  1&apos;b0            ;
    end else if (spi_busy &amp;#x26; update_edge) begin
        trans_start &amp;#x3C;=  1&apos;b1            ;
    end else begin
        trans_start &amp;#x3C;=  1&apos;b0            ;
    end
end

always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        spi_cs_n &amp;#x3C;=  1&apos;b1               ;
    end else if (trans_start) begin
        spi_cs_n &amp;#x3C;=  1&apos;b0               ;
    end else if (trans_end_d) begin // used a extended version to meet the timing requirements(clock to enable low time) of some device
        spi_cs_n &amp;#x3C;=  1&apos;b1               ;
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;仿真测试平台&quot;&gt;仿真测试平台&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;`timescale 1ns/1ps

module spi_master_tb;

// 产生 100MHz 的时钟
localparam CLOCK_PERIOD = 10;

reg clk;
initial clk = 0;
always #(CLOCK_PERIOD/2) clk=~clk;

reg             rst_n           ;
reg     [23:0]  dat_tx          ;
wire    [23:0]  dat_rx          ;
reg             start_op        ;
wire            spi_end         ;
wire            spi_sclk        ;
wire            spi_cs_n        ;
reg             spi_miso        ;
wire            spi_mosi        ;
wire    [ 7:0]  data_bit_cnt    ;

initial begin
    rst_n       &amp;#x3C;=  1&apos;b0        ;
    dat_tx      &amp;#x3C;=  24&apos;h79BC    ;   //  待发送数据
    start_op    &amp;#x3C;=  1&apos;b0        ;
    spi_miso    &amp;#x3C;=  1&apos;b0        ;
    #(CLOCK_PERIOD*10)
    rst_n       &amp;#x3C;=  1&apos;b1        ;   //  释放复位信号
    #(CLOCK_PERIOD*10)
    start_op    &amp;#x3C;=  1&apos;b1        ;   //  等待 10 个时钟周期后触发发送信号
    #CLOCK_PERIOD
    start_op    &amp;#x3C;=  1&apos;b0        ;   //  释放发送信号

end

//  实例例化
spi_master # (
    .DATA_WIDTH         (   8&apos;d24   ),
    .CPOL               (   1&apos;b0    ),
    .CPHA               (   1&apos;b0    ),
    .CLK_DIV            (   8&apos;d10   ),  // 输入的 100MHz 时钟进行 10 分频, 得到 10MHz 的 SPI 时钟
    .MOSI_IDLE_STATE    (   1&apos;b0    ), 
    .CONTINUOUS_CLK     (   1&apos;b0    )
) dut (
    .clk                (   clk             ),
    .rst_n              (   rst_n           ),
    .dat_tx             (   dat_tx          ),
    .dat_rx             (   dat_rx          ),
    .start_op           (   start_op        ),
    .spi_end            (   spi_end         ),
    .spi_sclk           (   spi_sclk        ),
    .spi_cs_n           (   spi_cs_n        ),
    .spi_miso           (   spi_miso        ),
    .spi_mosi           (   spi_mosi        ),
    .data_bit_cnt       (   data_bit_cnt    )
);
    
endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;仿真波形图&quot;&gt;仿真波形图&lt;/h3&gt;
&lt;p&gt;完整的仿真波形图如下:
&lt;img alt=&quot;modelsim_full_wave&quot; width=&quot;1680&quot; height=&quot;509&quot; src=&quot;https://blog.tyh123.top/_astro/modelsim_full_wave.BdJlAzLI_ZEhc2.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;实现三线式-spi-通信&quot;&gt;实现三线式 SPI 通信&lt;/h2&gt;
&lt;p&gt;SDIO 通信完全不需要从头开始编写代码，以上方提供的 &lt;code&gt;spi_master&lt;/code&gt; 模块的代码作为基础，将其作为一个子模块封装到顶层模块，再用 IOBUF 原语实现一个双向 IO 即可。&lt;/p&gt;
&lt;p&gt;完整代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module sdio_interface (
    input   wire            clk             ,   //  模块的时钟信号
    input   wire            rst_n           ,   //  模块的复位信号，低电平有效
    
    input   wire            rh_wl           ,   //  读写控制信号, 读为高电平，写为低电平
    input   wire            sdio_start_op   ,   //  操作使能的脉冲信号
    input   wire    [15:00] register_addr   ,   //  16 位寄存器地址
    input   wire    [07:00] dat_tx          ,   //  要发送的数据
    output  wire    [07:00] dat_rx          ,   //  从 sdio 接口读取到的 8 位寄存器数据
    
    output  wire            spi_sclk        ,   //  串行时钟信号
    output  wire            spi_cs_n        ,   //  片选信号
    inout   wire            sdio            ,   //  双向 IO 端口
);

// 例化 spi_master 模块
wire            spi_mosi    ;
wire            spi_miso    ;
wire    [23:00] dat_rx_t    ;   //  将 spi_master 模块接收到的 24 位数据暂存到此处，我们只需要低八位的寄存器数据
wire    [07:00] data_bit_cnt;
wire    sdio_dir = (data_bit_cnt &amp;#x3C;= 16) ? 1&apos;b0 : rh_wl; // 前 16 位：读写控制位和寄存器地址，固定为输出方向，低 8 位根据读写控制来调整方向
assign  dat_rx  =   dat_rx_t[07:00] ;   //  只取出低八位数据
spi_master # (
    .DATA_WIDTH             (   24                              ),  // data width, [1bit RW ctrl, 15 bits register addr, 8 bits data]
    .CPOL                   (   1&apos;b0                            ),  // Clock polarity
    .CPHA                   (   1&apos;b0                            ),  // Clock phase
    .CLK_DIV                (   8&apos;d10                           ),  // input clk division, used to generate SCLK.
    .MOSI_IDLE_STATE        (   1&apos;b0                            ),  // when module goes to idle. the MOSI line logic level will be set to this value   
    .CONTINUOUS_CLK         (   1&apos;b0                            )   // when set to 1&apos;b1, the SCLK line will continouous or only toggle when cs is low logic if it sets to 1&apos;b0.
) spi_master (                   
    .clk                    (   clk                             ),  // [I] [      ] module clock
    .rst_n                  (   rst_n                           ),  // [I] [      ] module reset signal, active-low

    .dat_tx                 (   {rh_wl, register_addr, dat_tx}  ),  // [I] [DW-1:0] the data to be transmitted to slave device
    .dat_rx                 (   dat_rx                          ),  // [O] [DW-1:0] the data received from the slave device

    .start_op               (   sdio_start_op                   ),  // [I] [      ] pulse, trigger SPI transfer
    .spi_end                (   spi_end                         ),  // [O] [      ] busy is high level when transferring data

    .spi_sclk               (   spi_sclk                        ),  // [O] [   0:0] serial clock
    .spi_cs_n               (   spi_cs_n                        ),  // [O] [   0:0] chip select signal, active low
    .spi_mosi               (   spi_mosi                        ),  // [I] [   0:0] master in slave out
    .spi_miso               (   spi_miso                        ),  // [O] [   0:0] master out slave in
    .data_bit_cnt           (   data_bit_cnt                    )   // [O] [   7:0] bit transmit counter
);

// 例化 IOBUF 原语
IOBUF # (
    .DRIVE          (   12              ),
    .IBUF_LOW_PWR   (   &quot;TRUE&quot;          ),
    .IOSTANDARD     (   &quot;DEFAULT&quot;       ),
    .SLEW           (   &quot;SLOW&quot;          )
) ms14d2600_spi_iobuf (
    .O              (   spi_miso        ),  //  IOBUF 原语的输出端口，接到 spi_miso 中
    .IO             (   sdio            ),  //  IOBUF 原语的双向端口，输出到模块端口
    .I              (   spi_mosi        ),  //  IOBUF 原语的输入端口，接到 spi_mosi 中
    .T              (   sdio_dir        )   //  IOBUF 原语的方向控制端口，low: output, high: input
);

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我实现的三线式 SPI 通信适用于我上面提到的某款 ADC 芯片通信时序，对于 sdio 方向的更改时机可以通过修改代码中第 21 行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wire    sdio_dir = (data_bit_cnt &amp;#x3C;= 16) ? 1&apos;b0 : rh_wl; // 前 16 位：读写控制位和寄存器地址，固定为输出方向，低 8 位根据读写控制来调整方向&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将判断条件设置为你需要的值即可。&lt;/p&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/3723985a</id>,<title>FPGA 实现以太网 PHY 芯片配置接口 —— MDIO 的学习与应用</title>,<link href="https://blog.tyh123.top/posts/3723985a" rel="alternate" type="text/html"/>,<published>2026-04-18T15:18:25.000Z</published>,<updated>2026-04-18T15:18:25.000Z</updated>,<summary type="text">MDIO 是以太网 PHY 芯片的寄存器配置接口。本文介绍 MDC/MDIO 接线、读写帧格式与时序细节, 并给出 FPGA 中配置 PHY 芯片的 Verilog 实现。</summary>,<content type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;MDIO 接口是配置以太网 PHY 芯片片上寄存器的一种通用接口，几乎所有的以太网 PHY 芯片使用的都是 MDIO 接口来配置寄存器。&lt;/p&gt;
&lt;h2 id=&quot;接线方式&quot;&gt;接线方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;ETH_MDC: 时钟信号线，频率不大于 12.5MHz&lt;/li&gt;
&lt;li&gt;ETH_MDIO: 双向数据线
主从式、半双工通信，所有通信都是由主设备发起，从设备被动响应（和 IIC 类似）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;数据帧格式&quot;&gt;数据帧格式&lt;/h2&gt;
&lt;h3 id=&quot;读数据&quot;&gt;读数据&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Preamble&lt;/th&gt;&lt;th&gt;ST&lt;/th&gt;&lt;th&gt;OP&lt;/th&gt;&lt;th&gt;PHYAD&lt;/th&gt;&lt;th&gt;REGAD&lt;/th&gt;&lt;th&gt;TA&lt;/th&gt;&lt;th&gt;DATA&lt;/th&gt;&lt;th&gt;IDLE&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1…1&lt;/td&gt;&lt;td&gt;01&lt;/td&gt;&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AAAAA&lt;/td&gt;&lt;td&gt;RRRRR&lt;/td&gt;&lt;td&gt;&lt;strong&gt;Z0&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;DDDDDDDDDDDDDDDD&lt;/td&gt;&lt;td&gt;Z&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;Preamble: 前导码，由主设备发送的&lt;strong&gt;连续的 32 个逻辑 1&lt;/strong&gt;，用于与从设备建立同步&lt;/li&gt;
&lt;li&gt;ST: 数据帧的开始，固定为 2位数据 &lt;strong&gt;01&lt;/strong&gt;，代表有效数据的起始位置&lt;/li&gt;
&lt;li&gt;OP: 操作码，定义操作类型，&lt;strong&gt;01：写操作，10：读操作&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;PHYAD：&lt;strong&gt;5 位 PHY 芯片地址&lt;/strong&gt;，用于选择总线上的目标 PHY 芯片&lt;/li&gt;
&lt;li&gt;REGAD：&lt;strong&gt;片上寄存器地址&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;TA：转向域，2 位的空闲周期，用于读写方向切换；写操作：主设备持续驱动总线输出 “10”; 读操作：第一个时钟周期 MDIO 程高阻态 (主、从设备均释放总线),  在第二个时钟周期下，从设备将接管 MDIO 总线，开始驱动 MDIO 输出数据。&lt;/li&gt;
&lt;li&gt;DATA：16 位即将要写入或读取的数据&lt;/li&gt;
&lt;li&gt;IDLE：空闲状态位，帧结束后 MDIO 总线拉高进入高阻态
表中的 Z 高阻态，在外部上拉电阻的情况下最终信号体现为高电平&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在读数据操作时，&lt;strong&gt;直到 TA 阶段之前，数均由主设备(FPGA)驱动&lt;/strong&gt;，TA 阶段第&lt;strong&gt;一个时钟周期&lt;/strong&gt;主设备控制 MDIO 为高阻态，释放总线，从&lt;strong&gt;第二个时钟周期&lt;/strong&gt;开始，&lt;strong&gt;从设备接管 MDIO&lt;/strong&gt;，数据为低电平，表示数据将在&lt;strong&gt;下一个时钟周期&lt;/strong&gt;开始发送。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img alt=&quot;MDIO 读数据时序图&quot; width=&quot;1717&quot; height=&quot;453&quot; src=&quot;https://blog.tyh123.top/_astro/mdio_reading.DEVO3gUr_ZLen9t.webp&quot;&gt;
&lt;img alt=&quot;MDIO 读数据时序图-note&quot; width=&quot;1421&quot; height=&quot;431&quot; src=&quot;https://blog.tyh123.top/_astro/mdio_reading_note.BtMKeyq7_1oiHy8.webp&quot;&gt;&lt;/p&gt;
&lt;h3 id=&quot;写数据&quot;&gt;写数据&lt;/h3&gt;

























&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Preamble&lt;/th&gt;&lt;th&gt;ST&lt;/th&gt;&lt;th&gt;OP&lt;/th&gt;&lt;th&gt;PHYAD&lt;/th&gt;&lt;th&gt;REGAD&lt;/th&gt;&lt;th&gt;TA&lt;/th&gt;&lt;th&gt;DATA&lt;/th&gt;&lt;th&gt;IDLE&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;1…1&lt;/td&gt;&lt;td&gt;01&lt;/td&gt;&lt;td&gt;&lt;strong&gt;01&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;AAAAA&lt;/td&gt;&lt;td&gt;RRRRR&lt;/td&gt;&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;DDDDDDDDDDDDDDDD&lt;/td&gt;&lt;td&gt;Z&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;在写数据时，MDIO 总线&lt;strong&gt;全程由主设备控制&lt;/strong&gt;
&lt;img alt=&quot;MDIO 写数据时序图&quot; width=&quot;1776&quot; height=&quot;392&quot; src=&quot;https://blog.tyh123.top/_astro/mdio_writing.DA0yeF3-_ZbihVB.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;实现代码&quot;&gt;实现代码&lt;/h2&gt;
&lt;h3 id=&quot;双向-io-口的驱动&quot;&gt;双向 IO 口的驱动&lt;/h3&gt;
&lt;p&gt;在这里使用 &lt;code&gt;IOBUF&lt;/code&gt; 原语驱动双向 IO 口。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IOBUF # (
    .DRIVE          (   12              ),
    .IBUF_LOW_PWR   (   &quot;TRUE&quot;          ),
    .IOSTANDARD     (   &quot;DEFAULT&quot;       ),
    .SLEW           (   &quot;SLOW&quot;          )
) mdio_obuf (
    .O              (   eth_mdio_in     ),
    .IO             (   eth_mdio        ),
    .I              (   eth_mdio_out    ),
    .T              (   eth_mdio_dir    )   //  1&apos;b1: input, 1&apos;b0: output
);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者使用三态门进行逻辑判断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module eth_mdio (
	//...
	inout eth_mdio; // 双向 io 口
	//...
);

wire eth_mdio_in ; // MDIO 从机输入的比特位数据
wire eth_mdio_out; // MDIO 主机输出的比特位数据
reg  eth_mdio_dir; // 控制 MDIO 的输入/输出方向, 1&apos;b1: 输入, 1&apos;b0:输出

assign eth_mdio = eth_mdio_dir ? eth_mdio_in : eth_mdio_out; // 三态门根据内部的 dir 信号控制端口的输入输出方向

//... 其它代码 ... 如时钟信号、驱动 eth_mdio_dir 的逻辑等等&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;时钟信号的产生&quot;&gt;时钟信号的产生&lt;/h3&gt;
&lt;p&gt;和之前有一篇讲 ${post 52a1f482} 文章中的代码类似，也是产生一个双边沿的脉冲信号，然后根据这个双边沿脉冲信号去翻转 MDC 信号就得到了驱动时钟。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// MDC inv Generation, inv pulse signal is active-high when MDC rising edge and falling edge
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_mdc_cnt &amp;#x3C;=  32&apos;d0               ;
        eth_mdc_inv &amp;#x3C;=   1&apos;b0               ;
    end
    else if (eth_mdc_cnt == (CLK_DIV - 1&apos;d1)) begin         // MDC falling edge
        eth_mdc_cnt &amp;#x3C;=  8&apos;d0                ;
        eth_mdc_inv &amp;#x3C;=  1&apos;b1                ;
    end
    else if (eth_mdc_cnt == (CLK_DIV[7:1] - 1&apos;d1)) begin    // MDC rising  edge
        eth_mdc_cnt &amp;#x3C;=  eth_mdc_cnt + 1&apos;d1  ;
        eth_mdc_inv &amp;#x3C;=   1&apos;b1               ;
    end
    else begin
        eth_mdc_cnt &amp;#x3C;=  eth_mdc_cnt + 1&apos;d1  ;
        eth_mdc_inv &amp;#x3C;=  1&apos;b0                ;
    end
end

// generate MDC
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_mdc     &amp;#x3C;=  1&apos;b0                ;
    end else if (eth_mdc_inv) begin
        eth_mdc     &amp;#x3C;=  ~eth_mdc            ;
    end else begin
        eth_mdc     &amp;#x3C;=   eth_mdc            ;
    end
end&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;上升沿和下降沿的判断&quot;&gt;上升沿和下降沿的判断&lt;/h3&gt;
&lt;p&gt;判断逻辑如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;eth_mdc&lt;/code&gt; 为&lt;strong&gt;高电平&lt;/strong&gt;并且产生了 &lt;code&gt;eth_mdc_inv&lt;/code&gt; 的脉冲信号，表示 &lt;code&gt;eth_mdc&lt;/code&gt; 将会在下一个时钟周期&lt;strong&gt;跳变到低电平&lt;/strong&gt;，此时则为 &lt;code&gt;eth_mdc&lt;/code&gt; 的下降沿&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;eth_mdc&lt;/code&gt; 为&lt;strong&gt;低电平&lt;/strong&gt;并且产生了 &lt;code&gt;eth_mdc_inv&lt;/code&gt; 的脉冲信号，表示 &lt;code&gt;eth_mdc&lt;/code&gt; 将会在下一个时钟周期&lt;strong&gt;跳变到高电平&lt;/strong&gt;，此时则为 &lt;code&gt;eth_mdc&lt;/code&gt; 的上升沿&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;wire eth_mdc_negedge =  eth_mdc &amp;#x26; eth_mdc_inv;   // indicates MDC negedge
wire eth_mdc_posedge = !eth_mdc &amp;#x26; eth_mdc_inv;   // indicates MDC posedge&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;完整的-mdio-驱动代码&quot;&gt;完整的 MDIO 驱动代码&lt;/h3&gt;
&lt;p&gt;已折叠部分代码 (点击即可展开)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module mdio_dri # (
    parameter   CLK_DIV = 8&apos;d10                 // for MDC generation, The max frequency of MDC is 12.500MHz
)(
    input                   clk             ,   // [ I] [    ] module clock, 100.000MHz is recommended
    input                   rst_n           ,   // [ I] [    ] reset signal, active-low
    input                   eth_start_op    ,   // [ I] [    ] start MDIO configuration pulse

    input   wire            rh_wl           ,   // [ I] [    ] high level for read, and low level for write.
    input   wire    [ 4:0]  phy_addr        ,   // [ I] [ 4:0] 5-bit phy address
    input   wire    [ 4:0]  reg_addr        ,   // [ I] [ 4:0] 5-bit register address 
    input   wire    [15:0]  dat_tx          ,   // [ I] [15:0] 16-bit tx data
    output  reg     [15:0]  dat_rx          ,   // [ O] [15:0] 16-bit rx data

    output  reg             eth_end         ,   // [ O] [    ] pulse signal when transmit done, active-high
    output  reg             eth_module_busy ,   // [ O] [    ] module busy siganl
    output  reg             eth_mdc         ,   // [ O] [    ] serial clock
    inout   wire            eth_mdio            // [IO] [    ] data port 
);
//================================================================================
// Local Parameter Declarations
//================================================================================
// 32-bit preamble  , 2-bit ST      , 2-bit OP  , 
//  5-bit PHYAD     , 5-bit REGAD   , 2-bit TA  ,
// 16-bit DATA      , Then higi-impedance IDLE state.
localparam  DAT_WIDTH       =   7&apos;d64       ;

//================================================================================
// Register Declarations
//================================================================================
reg [7:0]   eth_mdc_cnt     ;
reg         eth_mdc_inv     ;

reg         eth_start_op_d0 ;
reg         eth_start_op_d1 ;

reg [ 7:0]  data_bit_cnt    ;

reg [31:0]  dat_tx_buffer   ;
reg [15:0]  dat_rx_buffer   ;

reg         eth_mdio_out    ; 
//================================================================================
// Wire Declarations
//================================================================================
wire        eth_mdio_in                                                 ;   // mdio data in
wire        eth_mdio_dir                                                ;

wire        eth_start_op_pulse  =  !eth_start_op_d0 &amp;#x26; eth_start_op_d1   ;   
wire        eth_mdc_negedge     =   eth_mdc &amp;#x26; eth_mdc_inv               ;   // indicates MDC negedge
wire        eth_mdc_posedge     =  !eth_mdc &amp;#x26; eth_mdc_inv               ;   // indicates MDC posedge

//================================================================================
// Assign Declarations
//================================================================================
// always output until we transmit/receive 16-bit data stage
assign  eth_mdio_dir    =   (data_bit_cnt &amp;#x3C;= (8&apos;d48)) ? 1&apos;b0 : rh_wl;

//================================================================================
// implements
//================================================================================
IOBUF # (
    .DRIVE          (   12              ),
    .IBUF_LOW_PWR   (   &quot;TRUE&quot;          ),
    .IOSTANDARD     (   &quot;DEFAULT&quot;       ),
    .SLEW           (   &quot;SLOW&quot;          )
) mdio_obuf (
    .O              (   eth_mdio_in     ),
    .IO             (   eth_mdio        ),
    .I              (   eth_mdio_out    ),
    .T              (   eth_mdio_dir    )   //  1&apos;b1: input, 1&apos;b0: output
);

//================================================================================
// MAIN CODE
//================================================================================
// sync eth_start_op signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_start_op_d0 &amp;#x3C;=  1&apos;b0            ;
        eth_start_op_d1 &amp;#x3C;=  1&apos;b0            ;
    end else begin
        eth_start_op_d0 &amp;#x3C;=  eth_start_op    ;
        eth_start_op_d1 &amp;#x3C;=  eth_start_op_d0 ;
    end
end

// module busy indicator
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_module_busy &amp;#x3C;=  1&apos;b0            ;
    end else if (eth_start_op_pulse) begin
        eth_module_busy &amp;#x3C;=  1&apos;b1            ;
    end else if (eth_end) begin
        eth_module_busy &amp;#x3C;=  1&apos;b0            ;
    end else begin
        eth_module_busy &amp;#x3C;=  eth_module_busy ;
    end
end

// MDC inv Generation, inv pulse signal is active-high when MDC rising edge and falling edge
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_mdc_cnt &amp;#x3C;=  32&apos;d0               ;
        eth_mdc_inv &amp;#x3C;=   1&apos;b0               ;
    end
    else if (eth_mdc_cnt == (CLK_DIV - 1&apos;d1)) begin         // MDC falling edge
        eth_mdc_cnt &amp;#x3C;=  8&apos;d0                ;
        eth_mdc_inv &amp;#x3C;=  1&apos;b1                ;
    end
    else if (eth_mdc_cnt == (CLK_DIV[7:1] - 1&apos;d1)) begin    // MDC rising  edge
        eth_mdc_cnt &amp;#x3C;=  eth_mdc_cnt + 1&apos;d1  ;
        eth_mdc_inv &amp;#x3C;=   1&apos;b1               ;
    end
    else begin
        eth_mdc_cnt &amp;#x3C;=  eth_mdc_cnt + 1&apos;d1  ;
        eth_mdc_inv &amp;#x3C;=  1&apos;b0                ;
    end
end

// generate MDC
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_mdc     &amp;#x3C;=  1&apos;b0                ;
    end else if (eth_mdc_inv) begin
        eth_mdc     &amp;#x3C;=  ~eth_mdc            ;
    end else begin
        eth_mdc     &amp;#x3C;=   eth_mdc            ;
    end
end

// count data bit
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_bit_cnt    &amp;#x3C;=  8&apos;d0            ;
    end else if (
        eth_module_busy     &amp;#x26;&amp;#x26;
        eth_mdc_negedge     &amp;#x26;&amp;#x26;
        (data_bit_cnt &amp;#x3C; DAT_WIDTH)
    ) begin
        data_bit_cnt &amp;#x3C;=  data_bit_cnt + 1&apos;b1;
    end else if (eth_mdc_negedge &amp;#x26;&amp;#x26; data_bit_cnt == DAT_WIDTH) begin
        data_bit_cnt    &amp;#x3C;=  8&apos;d0            ;
    end else begin
        data_bit_cnt    &amp;#x3C;=  data_bit_cnt    ;
    end
end

// send data
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_mdio_out    &amp;#x3C;=  1&apos;b0            ;
        dat_tx_buffer   &amp;#x3C;=  32&apos;b0           ;
    end else if (eth_start_op_pulse) begin 
        dat_tx_buffer   &amp;#x3C;=  {
            2&apos;b01                       ,   // start of frame
            rh_wl                       ,   // Operation code bit 1
            !rh_wl                      ,   // Operation code bit 2
            phy_addr                    ,   // PHY address
            reg_addr                    ,   // Register address
            (rh_wl) ?  2&apos;b00  : 2&apos;b10   ,   // Turnaround
            (rh_wl) ? 16&apos;b0000: dat_tx      // 16-bit data
        };
    end else if (
        eth_module_busy     &amp;#x26;&amp;#x26;
        eth_mdc_negedge     &amp;#x26;&amp;#x26;
        (data_bit_cnt &amp;#x3C; 8&apos;d32)                      
    ) begin
        eth_mdio_out    &amp;#x3C;=  1&apos;b1                ;   // 32-bit preamble
    end else if (
        eth_module_busy     &amp;#x26;&amp;#x26;
        eth_mdc_negedge     &amp;#x26;&amp;#x26;
        (data_bit_cnt &amp;#x3C; DAT_WIDTH)
    ) begin
        eth_mdio_out    &amp;#x3C;=  dat_tx_buffer[31]   ;   // MSB First
        dat_tx_buffer   &amp;#x3C;=  {
            dat_tx_buffer[30:0],
            dat_tx_buffer[  31]
        };
    end else if (
        eth_module_busy     &amp;#x26;&amp;#x26;
        eth_mdc_negedge     &amp;#x26;&amp;#x26;
        (data_bit_cnt &gt;= DAT_WIDTH)
    ) begin
        eth_mdio_out    &amp;#x3C;=  1&apos;b0                ;
        dat_tx_buffer   &amp;#x3C;=  {32{1&apos;b0}}          ;
    end
end

// receive data
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        dat_rx_buffer       &amp;#x3C;=  16&apos;b0               ;
    end else if (eth_mdio_dir &amp;#x26;&amp;#x26; eth_mdc_posedge) begin
        dat_rx_buffer[0]    &amp;#x3C;=  eth_mdio_in         ;
        dat_rx_buffer[15:1] &amp;#x3C;=  dat_rx_buffer[14:0] ;
    end else begin
        dat_rx_buffer       &amp;#x3C;=  dat_rx_buffer       ;
    end
end

// move data from dat_rx_buffer to dat_rx output and generate eth_end signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        eth_end     &amp;#x3C;=      1&apos;b0                    ;
        dat_rx      &amp;#x3C;=      16&apos;b0                   ;
    end else if (eth_mdc_negedge &amp;#x26;&amp;#x26; (data_bit_cnt == DAT_WIDTH)) begin
        eth_end     &amp;#x3C;=      1&apos;b1                    ;
        dat_rx      &amp;#x3C;=      dat_rx_buffer           ;
    end else begin
        eth_end     &amp;#x3C;=      1&apos;b0                    ;
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/96d23a91</id>,<title>FPGA 串口通信</title>,<link href="https://blog.tyh123.top/posts/96d23a91" rel="alternate" type="text/html"/>,<published>2026-04-17T14:08:17.000Z</published>,<updated>2026-11-04T14:51:35.000Z</updated>,<summary type="text">从 UART 的异步通信原理出发, 介绍 TXD/RXD 交叉接线、起始位与停止位、奇偶校验和波特率, 并给出 FPGA 串口发送与接收模块的 Verilog 实现。</summary>,<content type="html">&lt;h2 id=&quot;简介&quot;&gt;简介&lt;/h2&gt;
&lt;p&gt;串口，即 UART(全称 Universal Asynchronous Receiver/Transmitter，中文正式名称为通用异步收发器)，是采用串行通信方式的接口。串行通信将字节数据&lt;strong&gt;按比特在一条数据线上逐个传输&lt;/strong&gt;，其特点是线路简单，但传输速度较慢s&lt;/p&gt;
&lt;p&gt;对于传输速度要求不高的场合，如工业控制、嵌入式开发等领域，串口通信是常客。&lt;/p&gt;
&lt;h2 id=&quot;串口通信方式&quot;&gt;串口通信方式&lt;/h2&gt;
&lt;p&gt;UART 通信需要两根信号线来实现，分别是 TXD 和 RXD。TXD 用来发送数据；RXD 用来接收数据。在发送数据时，将并行的 8 比特数据按比特在一条数据线上逐个传输。在接收数据时，将接收到的串行数据按比特解串成并行数据。&lt;strong&gt;发送时低位先发。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;需要注意的是，两个串口设备通信时，不能把各自的 TXD 和 RXD 直接对应相连（即 TXD 接 TXD、RXD 接 RXD），而应该交叉连接：一个设备的 TXD 接另一个设备的 RXD，同时这个设备的 RXD 接另一个设备的 TXD。具体连接方式如下图所示。
&lt;img alt=&quot;串口接线方式&quot; width=&quot;597&quot; height=&quot;340&quot; src=&quot;https://blog.tyh123.top/_astro/wiring.DOuZqeBF_ZP1RQS.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;串口通信数据格式&quot;&gt;串口通信数据格式&lt;/h2&gt;
&lt;p&gt;如下图所示，下图为&lt;strong&gt;一个字节数据 (字符帧)&lt;/strong&gt; 的传输格式
&lt;img alt=&quot;异步串口通信数据格式图&quot; width=&quot;465&quot; height=&quot;124&quot; src=&quot;https://blog.tyh123.top/_astro/data_format.CRqHYKet_2hBEgw.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;起始位&lt;/strong&gt;：标志着一帧数据的开始，该位&lt;strong&gt;固定为 0&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据位&lt;/strong&gt;：一帧数据中的&lt;strong&gt;有效数据&lt;/strong&gt;，在串口设置中可配置为 5、6、7、8 位&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;校验位&lt;/strong&gt;：分为&lt;strong&gt;奇校验&lt;/strong&gt;和&lt;strong&gt;偶校验&lt;/strong&gt;。为了使得整体 1 的个数为对应的奇数/偶数，在该位补齐对应的 0 或 1，例图如下：
&lt;ul&gt;
&lt;li&gt;&lt;img alt=&quot;串口校验位-奇校验图例&quot; width=&quot;609&quot; height=&quot;343&quot; src=&quot;https://blog.tyh123.top/_astro/parity_odd.CQywkbMz_1hT68P.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;串口校验位-偶校验图例&quot; width=&quot;599&quot; height=&quot;357&quot; src=&quot;https://blog.tyh123.top/_astro/parity_even.B6e6YkUe_Z1aA6Eb.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;总结一下就是：奇偶校验是为了让传输的数据（包含校验位）中 1 的个数为奇数/偶数。如果传输字节中 1 的个数为 偶数/奇数，则校验位的数据为 1，否则为 0。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停止位&lt;/strong&gt;：标志着一个数据帧的结束。该位&lt;strong&gt;固定为 1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;波特率&quot;&gt;波特率&lt;/h2&gt;
&lt;p&gt;串口通信中，我们用波特率表示数据传输的快慢。波特率表示每秒传输了多少个二进制位（bit）。通信双方各自使用本地时钟源，通过分频得到所需的波特率时钟信号。这种不需要额外传输时钟信号的方式，就是异步通信——其本质是双方各自依赖本地时钟，依靠起始位来完成字符同步。&lt;/p&gt;
&lt;p&gt;常见的波特率有 9600、19200 以及 115200 等。波特率是人为规定的，理论上你可以设置任何速度的波特率，只要通信双方波特率一致，就可以正常通信。但实际硬件和软件通常只支持有限个常用值，这受限于系统时钟的分频：分频只能得到某些特定的数值，例如前面提到的常见波特率。此外，常见的操作系统和串口软件通常也只在常用波特率列表中提供选择，支持手动输入任意波特率的情况并不多。&lt;/p&gt;
&lt;h2 id=&quot;实现代码&quot;&gt;实现代码&lt;/h2&gt;
&lt;h3 id=&quot;串口发送模块&quot;&gt;串口发送模块&lt;/h3&gt;
&lt;p&gt;该模块负责串口数据的发送，已折叠部分代码 (点击折叠内容即可打开)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module uart_tx # (
    parameter   CLOCK_FREQ  =   32&apos;d100_000_000 ,   //  输入时钟频率 (200MHz)
    parameter   BAUD_RATE   =   32&apos;d115200      ,   //  波特率
    parameter   DATA_BITS   =   8               ,   //  数据位
    parameter   PARITY      =   &quot;ODD&quot;           ,   //  校验方式，有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
    parameter   STOP_BITS   =   &quot;1&quot;                 //  停止位: 1, 1.5, 2，输入其它值则默认为 1 停止位
) (
    input                   clk     ,   //  [I][     ] 输入的模块时钟，推荐 100.000 MHz
    input                   rst_n   ,   //  [I][     ] 模块复位信号，低电平有效

    input   wire            tx_en   ,   //  [I][     ] 发送使能信号，高脉冲有效
    input   wire    [07:00] tx_data ,   //  [I][07:00] 要发送的数据
    
    output  reg             tx_busy ,   //  [O][     ] 发送机忙信号
    output  reg             txd         //  [O][     ] TXD 信号线
);
    
localparam  BAUD_CNT_MAX        =   CLOCK_FREQ / BAUD_RATE  ;   //  计数器的最大计数值
localparam  BAUD_CNT_MID        =   BAUD_CNT_MAX / 2        ;   //  计数器值的中点
localparam  HAS_PARITY          =   (PARITY != &quot;NONE&quot;)      ;   //  是否使用了校验位
localparam  STOP_BIT_INDEX      =   DATA_BITS + (HAS_PARITY ? 1 : 0) + 1 ;  // 停止位在 bit_cnt 中的位置

// 根据停止位长度计算停止位结束时刻的 baud_cnt 计数值
// 1   -&gt; 在停止位中点（BAUD_CNT_MAX/2 - 1）结束
// 1.5 -&gt; 在停止位中点后继续 1 个位周期（BAUD_CNT_MAX + BAUD_CNT_MAX/2 - 1）
// 2   -&gt; 在停止位中点后继续 1.5 个位周期（2*BAUD_CNT_MAX - 1）
// 为了保持代码一致，减 1 的操作在 always 语句中进行
localparam  STOP_BITS1_CNT      =   BAUD_CNT_MAX/2                  ;   //  停止位为 1   时计数器的值
localparam  STOP_BITS1_5_CNT    =   BAUD_CNT_MAX + BAUD_CNT_MAX/2   ;   //  停止位为 1.5 时计数器的值
localparam  STOP_BITS2_CNT      =   (2 * BAUD_CNT_MAX)              ;   //  停止位为 2   时计数器的值

localparam  STOP_END_CNT        =   (STOP_BITS == &quot;1&quot;  ) ? (STOP_BITS1_CNT  ) :
                                    (STOP_BITS == &quot;1.5&quot;) ? (STOP_BITS1_5_CNT) :
                                    (STOP_BITS == &quot;2&quot;  ) ? (STOP_BITS2_CNT  ) :
                                     STOP_BITS1_CNT                             ;   // 停止位计数器的值，默认按停止位 1 处理

reg [31:00] baud_cnt    ;
reg [03:00] bit_cnt     ;
reg [07:00] data_reg    ;

wire        parity_bit  ;

// 校验位计算，使用的是缩位异或运算
// 缩位异或运算用来检查数据中 1 的个数是否为奇数，对其计算结果取反后可用于检查数据中 1 的个数是否为偶数
assign      parity_bit  =  (PARITY == &quot;ODD&quot;  ) ? ~(^tx_data[DATA_BITS-1:0]) :
                           (PARITY == &quot;EVEN&quot; ) ?   ^tx_data[DATA_BITS-1:0]  :
                           (PARITY == &quot;MARK&quot; ) ? 1&apos;b1 :
                           (PARITY == &quot;SPACE&quot;) ? 1&apos;b0 :
                           (PARITY == &quot;NONE&quot; ) ? 1&apos;b0 :
                           1&apos;bz;

// 判断校验位的参数是否正确
initial begin
    if (
        !((PARITY == &quot;ODD&quot;  ) ||
          (PARITY == &quot;EVEN&quot; ) ||
          (PARITY == &quot;MARK&quot; ) ||
          (PARITY == &quot;SPACE&quot;) ||
          (PARITY == &quot;NONE&quot; ))
    ) begin
        $error(&quot;Unknown PARITY value: [%s], for parameter PARITY, its value must be one of these values: ODD,EVEN,MARK,SPACE or NONE.&quot;, PARITY);
    end
end

// 打拍同步 tx_en 信号，检测上升沿
reg [2:0] tx_en_sr;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) tx_en_sr &amp;#x3C;= 3&apos;b111;
    else        tx_en_sr &amp;#x3C;= {tx_en_sr[1:0], tx_en};
end
wire tx_en_rising_edge = (tx_en_sr[2:1] == 2&apos;b01); // 上升降沿检测

//  当 tx_en 检测到上升沿时, 寄存输入的并行数据, 并拉高 busy 信号
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        data_reg    &amp;#x3C;=  8&apos;b0        ;
        tx_busy     &amp;#x3C;=  1&apos;b0        ;
    end else if (tx_en_rising_edge) begin
        data_reg    &amp;#x3C;=  tx_data     ;   //  检测到上升沿, 寄存输入的并行数据
        tx_busy     &amp;#x3C;=  1&apos;b1        ;   //  拉高 busy 信号
    end else if (bit_cnt == STOP_BIT_INDEX &amp;#x26;&amp;#x26; baud_cnt == STOP_END_CNT - 1&apos;b1) begin
        data_reg    &amp;#x3C;=  8&apos;b0        ;   //  计数到停止位时清空发送数据寄存器
        tx_busy     &amp;#x3C;=  1&apos;b0        ;   //  拉低 busy 信号
    end else begin
        data_reg    &amp;#x3C;=  data_reg    ;
        tx_busy     &amp;#x3C;=  tx_busy     ;
    end
end

//  波特率计数器
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        baud_cnt    &amp;#x3C;=  16&apos;d0           ;
    end else if (tx_busy) begin 
        if(bit_cnt == STOP_BIT_INDEX) begin             //  停止位阶段
            if(baud_cnt &amp;#x3C; STOP_END_CNT)         
                baud_cnt &amp;#x3C;= baud_cnt + 1&apos;b1     ;       //  计数器计数到结束时刻
            else
                baud_cnt &amp;#x3C;= 16&apos;d0               ;       // 到达停止位结束时刻，清零（随后rx_flag变低）
        end else begin
            if (baud_cnt &amp;#x3C; BAUD_CNT_MAX - 1&apos;b1) begin   //  正常阶段，非停止位正常循环计数
                baud_cnt    &amp;#x3C;=  baud_cnt + 1&apos;b1 ;       //  接收过程时计数器循环计数
            end else begin
                baud_cnt &amp;#x3C;= 16&apos;d0               ;       // 完成了一个计数周期后清零
            end
        end
    end else begin
        baud_cnt    &amp;#x3C;=  16&apos;d0                   ;       //  空闲状态清零
    end
end

//  接收数据计数器
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bit_cnt         &amp;#x3C;=  4&apos;d0            ;
    end else if (tx_en_rising_edge) begin 
        bit_cnt         &amp;#x3C;=  4&apos;d0            ;
    end else if (tx_busy) begin 
        if (bit_cnt &amp;#x3C; STOP_BIT_INDEX) begin
            if(baud_cnt == BAUD_CNT_MAX - 1&apos;b1) begin  // 每个位周期结束时自增
                bit_cnt &amp;#x3C;=  bit_cnt + 1&apos;b1  ;
            end else begin
                bit_cnt &amp;#x3C;=  bit_cnt         ;
            end
        end else begin      // 已到达停止位索引，保持不变，等待 rx_flag 拉低
            bit_cnt     &amp;#x3C;=  bit_cnt         ;
        end
    end else begin
        bit_cnt &amp;#x3C;= 4&apos;d0 ;   //  空闲状态清零
    end
end

//  根据 bit_cnt 来发送数据到 txd 端口
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) 
        txd &amp;#x3C;=  1&apos;b1    ;
    else if(tx_busy) begin                          // 系统发送忙状态时
        if(baud_cnt == BAUD_CNT_MID - 1&apos;b1) begin   // 判断 baud_cnt 是否计数到数据位的中间
            if (bit_cnt == 0) begin
                txd &amp;#x3C;=  1&apos;b0                ;   //  起始位
            end else if((bit_cnt &gt;= 1) &amp;#x26;&amp;#x26; (bit_cnt &amp;#x3C;= DATA_BITS)) begin
                txd &amp;#x3C;=  data_reg[bit_cnt-1] ;   //  由低到高位发送数据
            end else if (HAS_PARITY == 1&apos;b1 &amp;#x26;&amp;#x26; bit_cnt == STOP_BIT_INDEX - 1) begin
                txd &amp;#x3C;=  parity_bit          ;   //  发送校验位
            end
        end else begin
            txd &amp;#x3C;=  txd ;
        end
    end else begin
        txd &amp;#x3C;=  1&apos;b1    ;   // 空闲时清零（不保留残余）
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;串口接收模块&quot;&gt;串口接收模块&lt;/h3&gt;
&lt;p&gt;该模块负责串口数据的接收，已折叠部分代码 (点击折叠内容即可打开)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module uart_rx # (
    parameter   CLOCK_FREQ  =   32&apos;d100_000_000 ,   //  输入时钟频率 (200MHz)
    parameter   BAUD_RATE   =   32&apos;d115200      ,   //  波特率
    parameter   DATA_BITS   =   8               ,   //  数据位
    parameter   PARITY      =   &quot;ODD&quot;           ,   //  校验方式，有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
    parameter   STOP_BITS   =   &quot;1&quot;                 //  停止位: 1, 1.5, 2，输入其它值则默认为 1 停止位
) (
    input                   clk     ,   //  [I][     ] 模块时钟
    input                   rst_n   ,   //  [I][     ] 复位信号，低有效

    input   wire            rxd     ,   //  [I][     ] RXD 信号线
    
    output  reg     [07:00] rx_data ,   //  [O][07:00] 接收到的数据
    output  reg             rx_done ,   //  [O][     ] 接收完成标志（高脉冲）
    output  reg             rx_err      //  [O][     ] 错误标志（校验错误或帧错误）
);

localparam  BAUD_CNT_MAX        =   CLOCK_FREQ / BAUD_RATE  ;   //  计数器的最大计数值
localparam  BAUD_CNT_MID        =   BAUD_CNT_MAX / 2        ;   //  计数器值的中点
localparam  HAS_PARITY          =   (PARITY != &quot;NONE&quot;)      ;   //  是否使用了校验位
localparam  STOP_BIT_INDEX      =   DATA_BITS + (HAS_PARITY ? 1 : 0) + 1 ;  // 停止位在 bit_cnt 中的位置

// 根据停止位长度计算停止位结束时刻的 baud_cnt 计数值
// 1   -&gt; 在停止位中点（BAUD_CNT_MAX/2 - 1）结束
// 1.5 -&gt; 在停止位中点后继续 1 个位周期（BAUD_CNT_MAX + BAUD_CNT_MAX/2 - 1）
// 2   -&gt; 在停止位中点后继续 1.5 个位周期（2*BAUD_CNT_MAX - 1）
// 为了保持代码一致，减 1 的操作在 always 语句中进行
localparam  STOP_BITS1_CNT      =   BAUD_CNT_MAX/2                  ;   //  停止位为 1   时计数器的值
localparam  STOP_BITS1_5_CNT    =   BAUD_CNT_MAX + BAUD_CNT_MAX/2   ;   //  停止位为 1.5 时计数器的值
localparam  STOP_BITS2_CNT      =   (2 * BAUD_CNT_MAX)              ;   //  停止位为 2   时计数器的值

localparam  STOP_END_CNT        =   (STOP_BITS == &quot;1&quot;  ) ? (STOP_BITS1_CNT  ) :
                                    (STOP_BITS == &quot;1.5&quot;) ? (STOP_BITS1_5_CNT) :
                                    (STOP_BITS == &quot;2&quot;  ) ? (STOP_BITS2_CNT  ) :
                                     STOP_BITS1_CNT                             ;   // 停止位计数器的值，默认按停止位 1 处理

reg [31:00] baud_cnt    ;
reg [03:00] bit_cnt     ;
reg [07:00] rx_reg      ;
reg         rx_parity   ;   //  接收到的校验位

// 打拍同步 rxd 信号，检测下降沿
reg [2:0] rxd_sync;
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) rxd_sync &amp;#x3C;= 3&apos;b111;
    else        rxd_sync &amp;#x3C;= {rxd_sync[1:0], rxd};
end
wire rxd_fall = (rxd_sync[2:1] == 2&apos;b10); // 下降沿检测
wire rxd_s    = rxd_sync[2];              // 同步后的电平

// 校验计算逻辑
wire    parity_check;
assign  parity_check    =  (PARITY == &quot;ODD&quot;  ) ? ~(^rx_reg[DATA_BITS-1:0]) :
                           (PARITY == &quot;EVEN&quot; ) ?   ^rx_reg[DATA_BITS-1:0]  :
                           (PARITY == &quot;MARK&quot; ) ? 1&apos;b1 :
                           (PARITY == &quot;SPACE&quot;) ? 1&apos;b0 :
                           (PARITY == &quot;NONE&quot; ) ? 1&apos;b0 :
                           1&apos;bz;
initial begin
    if (
        !((PARITY == &quot;ODD&quot;  ) ||
          (PARITY == &quot;EVEN&quot; ) ||
          (PARITY == &quot;MARK&quot; ) ||
          (PARITY == &quot;SPACE&quot;) ||
          (PARITY == &quot;NONE&quot; ))
    ) begin
        $error(&quot;Unknown PARITY value: [%s], for parameter PARITY, its value must be one of these values: ODD,EVEN,MARK,SPACE or NONE.&quot;, PARITY);
    end
end

// 接收标志指示器
reg     rx_flag;
wire    start_en = rxd_fall &amp;#x26; ~rx_flag;
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_flag &amp;#x3C;=  1&apos;b0    ;
    end else if (start_en) begin    //  检测到起始位
        rx_flag &amp;#x3C;=  1&apos;b1    ;       //  接收过程中，标志信号 rx_flag 拉高
        // 根据停止位长度，在指定时刻结束接收
    end else if (bit_cnt == STOP_BIT_INDEX &amp;#x26;&amp;#x26; baud_cnt == STOP_END_CNT - 1&apos;b1) begin
        rx_flag &amp;#x3C;=  1&apos;b0    ;
    end else begin
        rx_flag &amp;#x3C;= rx_flag  ;
    end
end

//  波特率计数器
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        baud_cnt    &amp;#x3C;=  16&apos;d0           ;
    end else if (rx_flag) begin 
        if(bit_cnt == STOP_BIT_INDEX) begin             //  停止位阶段
            if(baud_cnt &amp;#x3C; STOP_END_CNT)         
                baud_cnt &amp;#x3C;= baud_cnt + 1&apos;b1     ;       //  计数器计数到结束时刻
            else
                baud_cnt &amp;#x3C;= 16&apos;d0               ;       // 到达停止位结束时刻，清零（随后rx_flag变低）
        end else begin
            if (baud_cnt &amp;#x3C; BAUD_CNT_MAX - 1&apos;b1) begin   //  正常阶段，非停止位正常循环计数
                baud_cnt    &amp;#x3C;=  baud_cnt + 1&apos;b1 ;       //  接收过程时计数器循环计数
            end else begin
                baud_cnt &amp;#x3C;= 16&apos;d0               ;       // 完成了一个计数周期后清零
            end
        end
    end else begin
        baud_cnt    &amp;#x3C;=  16&apos;d0                   ;       //  空闲状态清零
    end
end

//  接收数据计数器
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        bit_cnt         &amp;#x3C;=  4&apos;d0            ;
    end else if (rx_flag) begin 
        if (bit_cnt &amp;#x3C; STOP_BIT_INDEX) begin
            if(baud_cnt == BAUD_CNT_MAX - 1&apos;b1) begin  // 每个位周期结束时自增
                bit_cnt &amp;#x3C;=  bit_cnt + 1&apos;b1  ;
            end else begin
                bit_cnt &amp;#x3C;=  bit_cnt         ;
            end
        end else begin      // 已到达停止位索引，保持不变，等待 rx_flag 拉低
            bit_cnt     &amp;#x3C;=  bit_cnt         ;
        end
    end else begin
        bit_cnt &amp;#x3C;= 4&apos;d0 ;   //  空闲状态清零
    end
end

//  根据 bit_cnt 来寄存 rxd 端口数据
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        rx_reg      &amp;#x3C;= 8&apos;b0                     ;
        rx_parity   &amp;#x3C;=  1&apos;b0                    ;
    end else if(rx_flag) begin                          // 系统处于接收过程时
        if(baud_cnt == BAUD_CNT_MID - 1&apos;b1) begin   // 判断 baud_cnt 是否计数到数据位的中间
            if((bit_cnt &gt;= 1) &amp;#x26;&amp;#x26; (bit_cnt &amp;#x3C;= DATA_BITS)) begin
                rx_reg[bit_cnt-1]   &amp;#x3C;= rxd_s    ;   //  数据位采样 (按位索引写入)
            end else if (HAS_PARITY == 1&apos;b1 &amp;#x26;&amp;#x26; bit_cnt == STOP_BIT_INDEX - 1&apos;b1) begin
                rx_parity           &amp;#x3C;=  rxd_s   ;   //  采样校验位
            end
        end else begin
            rx_reg &amp;#x3C;= rx_reg;
        end
    end else begin
        rx_reg &amp;#x3C;= 8&apos;b0;   // 空闲时清零（不保留残余）
    end
end

//  校验位判断, 若校验位不通过则将 rx_err 置 1
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        rx_err  &amp;#x3C;=  1&apos;b0    ;
    end else if ((HAS_PARITY == 1&apos;b1) &amp;#x26;&amp;#x26; (rx_parity == parity_check)) begin
        rx_err  &amp;#x3C;=  1&apos;b0    ;
    end else begin
        rx_err  &amp;#x3C;=  1&apos;b1    ;
    end
end

//  赋值接收完成信号和接收到的数据
always @(posedge clk or negedge rst_n) begin
    if(!rst_n) begin
        rx_done &amp;#x3C;= 1&apos;b0;
        rx_data &amp;#x3C;= 8&apos;b0;
    end
    // 在停止位中点采样完毕时输出数据
    else if((bit_cnt == STOP_BIT_INDEX) &amp;#x26;&amp;#x26; (baud_cnt == BAUD_CNT_MID - 1&apos;b1)) begin
        rx_done &amp;#x3C;= 1&apos;b1     ;  // 拉高接收完成信号
        rx_data &amp;#x3C;= rx_reg   ;  // 并对UART接收到的数据进行赋值
    end    
    else begin
        rx_done &amp;#x3C;= 1&apos;b0     ;
        rx_data &amp;#x3C;= rx_data  ;
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;封装后的串口顶层&quot;&gt;封装后的串口顶层&lt;/h3&gt;
&lt;p&gt;为了方便使用，直接将发送模块和接收模块作为两个子模块封装到一个顶层模块中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module uart # (
    parameter               CLOCK_FREQ  =   32&apos;d100_000_000 ,   //  输入时钟频率 (200MHz)
    parameter               BAUD_RATE   =   32&apos;d115200      ,   //  波特率
    parameter               DATA_BITS   =   8               ,   //  数据位
    parameter   [8*8-1:0]   PARITY      =   &quot;ODD&quot;           ,   //  校验方式，有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
    parameter   [8*8-1:0]   STOP_BITS   =   &quot;1&quot;                 //  停止位: 1, 1.5, 2，输入其它值则默认为 1 停止位
) (
    input                   clk     ,
    input                   rst_n   ,

    // TX 接口
    input   wire            tx_en   ,
    input   wire    [07:00] tx_data ,
    output  wire            tx_busy ,
    output  wire            txd     ,

    // RX 接口
    input   wire            rxd     ,
    output  wire    [07:00] rx_data ,
    output  wire            rx_done ,
    output  wire            rx_err
);

// 实例化发送模块
uart_tx #(
    .CLOCK_FREQ (CLOCK_FREQ ),
    .BAUD_RATE  (BAUD_RATE  ),
    .DATA_BITS  (DATA_BITS  ),
    .PARITY     (PARITY     ),
    .STOP_BITS  (STOP_BITS  )
) uart_tx_inst (
    .clk        (clk        ),
    .rst_n      (rst_n      ),
    .tx_en      (tx_en      ),
    .tx_data    (tx_data    ),
    .tx_busy    (tx_busy    ),
    .txd        (txd        )
);

// 实例化接收模块
uart_rx #(
    .CLOCK_FREQ (CLOCK_FREQ ),
    .BAUD_RATE  (BAUD_RATE  ),
    .DATA_BITS  (DATA_BITS  ),
    .PARITY     (PARITY     ),
    .STOP_BITS  (STOP_BITS  )
) uart_rx_inst (
    .clk        (clk        ),
    .rst_n      (rst_n      ),
    .rxd        (rxd        ),
    .rx_data    (rx_data    ),
    .rx_done    (rx_done    ),
    .rx_err     (rx_err     )
);

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;回环测试工程顶层代码&quot;&gt;回环测试(工程顶层)代码&lt;/h3&gt;
&lt;p&gt;在这里我使用的开发板型号为 &lt;code&gt;XC7K325TFFG900-2&lt;/code&gt;，时钟输入为差分信号输入&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module uart_loopback_top # (
    parameter   CLOCK_FREQ  =   32&apos;d200_000_000 ,   //  输入时钟频率 (200MHz)
    parameter   BAUD_RATE   =   32&apos;d115200      ,   //  波特率
    parameter   DATA_BITS   =   8               ,   //  数据位
    parameter   PARITY      =   &quot;NONE&quot;           ,   //  校验方式，有 ODD(奇校验)、EVEN(偶校验)、SPACE(始终为 0)、MARK(始终为 1)、NONE(无校验)
    parameter   STOP_BITS   =   &quot;1&quot;                 //  停止位: 1, 1.5, 2，输入其它值则默认为 1 停止位
) (
    input                   sys_clk_p,              // 200MHz 差分时钟 P 端
    input                   sys_clk_n,              // 200MHz 差分时钟 N 端
    input                   rst_n   ,               // 复位信号（低有效）

    // 硬件引脚连接到串口芯片
    input                   uart_rxd,               // UART 接收引脚
    output                  uart_txd,               // UART 发送引脚
    
    // 连接到 LED 的状态指示
    output                  led_rx_done,
    output                  led_err
);

// 差分时钟输入转单端时钟输出
wire clk_200m_single;
IBUFDS ibufds_inst (
    .I  (   sys_clk_p       ),
    .IB (   sys_clk_n       ),
    .O  (   clk_200m_single )
);

// 例化串口模块
wire            tx_en   ;
wire    [07:00] tx_data ;
wire    [07:00] rx_data ;
wire            tx_busy ;
wire            rx_done ;
uart # (
    .CLOCK_FREQ (CLOCK_FREQ ),
    .BAUD_RATE  (BAUD_RATE  ),
    .DATA_BITS  (DATA_BITS  ),
    .PARITY     (PARITY     ),
    .STOP_BITS  (STOP_BITS  )
) uart_inst (
    .clk        (   clk_200m_single ),
    .rst_n      (   rst_n           ),

    // TX interface
    .tx_en      (   tx_en           ),
    .tx_data    (   tx_data         ),
    .tx_busy    (   tx_busy         ),
    .txd        (   uart_txd        ),

    // RX interface
    .rxd        (   uart_rxd        ),
    .rx_data    (   rx_data         ),
    .rx_done    (   rx_done         ),
    .rx_err     (   led_err         )
);

assign tx_data      = rx_data; // 回环测试：发送的数据为接收到的数据
assign tx_en        = rx_done; // 回环测试：发送使能信号为接收完成信号
assign led_rx_done  = rx_done; // 将接收完成信号输出到板载的一颗 led 上

endmodule&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/424a6d95</id>,<title>ETH-04 ARP 通信协议简介及实现代码</title>,<link href="https://blog.tyh123.top/posts/424a6d95" rel="alternate" type="text/html"/>,<published>2026-04-16T11:25:17.000Z</published>,<updated>2026-04-16T11:25:17.000Z</updated>,<summary type="text">ARP 协议用于根据 IP 地址查找对应的 MAC 地址。本文介绍 ARP 请求与应答流程、报文格式, 并给出 FPGA 上 Verilog 实现 ARP 通信的代码。</summary>,<content type="html">&lt;h3 id=&quot;arp-协议简介&quot;&gt;ARP 协议简介&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;ARP（Address Resolution Protocol，地址解析协议）&lt;/strong&gt; 是一种根据 IP 地址 (逻辑地址) 获取 MAC 地址 (物理地址) 的 TCP/IP 协议。&lt;/p&gt;
&lt;p&gt;在以太网环境中，设备之间的通信&lt;strong&gt;依赖于 MAC 地址&lt;/strong&gt;，但上层应用通常&lt;strong&gt;只知道目标设备的 IP 地址&lt;/strong&gt;。ARP 协议通过 “一问一答” 的机制，解决了 “&lt;strong&gt;已知 IP 地址，如何找到对应 MAC 地址&lt;/strong&gt;” 的问题，确保数据能够正确封装并送达目标设备。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;ARP 协议图&quot; width=&quot;563&quot; height=&quot;161&quot; src=&quot;https://blog.tyh123.top/_astro/arp_block_diagram.C4nKPj7-_Z1enKQW.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;其工作过程简述如下：主机 A 发送数据时，按照以太网帧格式封装，但将目标 MAC 地址设置为&lt;strong&gt;广播地址（FF:FF:FF:FF:FF:FF）&lt;/strong&gt;。这样，局域网内所有主机都会收到该数据包。只有 IP 地址与目标匹配的主机 B 会响应，将自己的 MAC 地址发送给主机 A，从而完成地址解析。其他非对应 IP 的主机则自动忽略该广播包。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;补充说明：TCP/IP 协议簇&lt;/strong&gt;&lt;br&gt;
TCP/IP 是互联网的核心通信协议，它并非单一的协议，而是一个协议簇 (协议集合) ，定义了设备之间如何联网和通信。其名称来源于其中最著名的两个协议：&lt;strong&gt;TCP&lt;/strong&gt; (传输控制协议) 和 &lt;strong&gt;IP&lt;/strong&gt; (互联网协议)。ARP 正是这个协议簇中的一员。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;arp-请求&quot;&gt;ARP 请求&lt;/h3&gt;
&lt;p&gt;当主机需要与目标 IP 通信，但不知道目标 IP 的 MAC 地址时，会构造 ARP 请求报文，并&lt;strong&gt;以广播的方式（目标 MAC 地址为 FF:FF:FF:FF:FF:FF）发送到整个局域网&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;strong&gt;广播发送&lt;/strong&gt;，局域网内所有的设备都会收到这个数据包，但&lt;strong&gt;只有目标 IP 地址匹配的设备才会处理&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;arp-应答&quot;&gt;ARP 应答&lt;/h3&gt;
&lt;p&gt;当目标设备收到 ARP 请求后，发现目标 IP 与自己的 IP 匹配，会构造 ARP 应答报文，并&lt;strong&gt;以单播的方式直接回复给请求方。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;特点：&lt;strong&gt;单播发送&lt;/strong&gt;，同时目标设备也会将请求方的 IP-MAC 映射记录到自己的 ARP 缓存中。&lt;/p&gt;
&lt;h3 id=&quot;arp-协议数据格式&quot;&gt;ARP 协议数据格式&lt;/h3&gt;
&lt;h4 id=&quot;以太网帧格式&quot;&gt;以太网帧格式&lt;/h4&gt;
&lt;p&gt;ARP 协议的以太网帧数据格式如下图所示，ARP 数据位于以太网数据帧中的数据段部分。
&lt;img alt=&quot;ARP 协议 MAC 帧&quot; width=&quot;519&quot; height=&quot;137&quot; src=&quot;https://blog.tyh123.top/_astro/arp_format_mac.BgmihKCT_qRqQ8.webp&quot;&gt;&lt;/p&gt;
&lt;h4 id=&quot;arp-数据报格式&quot;&gt;ARP 数据报格式&lt;/h4&gt;
&lt;p&gt;ARP 数据报位于以太网帧中的数据段中，它的格式如下图所示。
&lt;img alt=&quot;ARP 数据报格式&quot; width=&quot;1226&quot; height=&quot;127&quot; src=&quot;https://blog.tyh123.top/_astro/arp_format.mcYCkApd_ZjVeUa.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;硬件类型&lt;/strong&gt;：指定底层网络硬件类型。对于以太网，值为 &lt;strong&gt;1 &lt;code&gt;(0x0001)&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议类型&lt;/strong&gt;：要映射的协议地址类型，&lt;strong&gt;ARP协议的上层协议为IP协议&lt;/strong&gt;，因此该协议类型为IP协议，其值为 0x0800。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;硬件地址长度&lt;/strong&gt;：硬件地址（MAC地址）的长度，以字节为单位。对于以太网上 IP 地址的 ARP 请求或者应答来说，该值为6。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议地址长度&lt;/strong&gt;：IP 地址的长度，以字节为单位。对于以太网上 IP 地址的 ARP 请求或者应答来说，该值为4。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;操作码 (OP)&lt;/strong&gt;：用于表示该数据包为 ARP 请求或者 ARP 应答。1 表示 ARP 请求，2 表示ARP 应答。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;源 MAC 地址&lt;/strong&gt;：&lt;strong&gt;发送端&lt;/strong&gt;的 MAC 地址，即硬件地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;源 IP 地址&lt;/strong&gt;： &lt;strong&gt;发送端&lt;/strong&gt;的 IP 地址，即协议地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的 MAC 地址&lt;/strong&gt;：&lt;strong&gt;接收端&lt;/strong&gt;的 MAC 地址，即硬件地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的 IP 地址&lt;/strong&gt;： &lt;strong&gt;接收端&lt;/strong&gt;的 IP 地址，即协议地址&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;实现代码&quot;&gt;实现代码&lt;/h3&gt;
&lt;p&gt;基于之前文章 &lt;a href=&quot;ETH-03%20%E4%BB%A5%E5%A4%AA%E7%BD%91%E5%B8%A7%E7%BB%93%E6%9E%84%E4%BB%8B%E7%BB%8D%E4%BB%A5%E5%8F%8A%20Verilog%20%E5%AE%9E%E7%8E%B0%E4%BB%A3%E7%A0%81.md&quot;&gt;ETH-03 以太网帧结构介绍以及 Verilog 实现代码&lt;/a&gt; 中的以太网帧封包模块，我们只需要向模块中发送数据段的 ARP 数据即可。&lt;/p&gt;
&lt;h4 id=&quot;arp-发送模块&quot;&gt;ARP 发送模块&lt;/h4&gt;
&lt;p&gt;只需要发送 ARP 数据段的内容给 以太网封包模块即可。&lt;/p&gt;
&lt;p&gt;模块工作过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上游控制模块为端口&lt;code&gt;arp_tx_en&lt;/code&gt; 产生一个高电平脉冲。之后模块将在端口 &lt;code&gt;s_axis_tdv&lt;/code&gt; 产生持续的高电平信号，给下游的以太网封包模块。&lt;/li&gt;
&lt;li&gt;模块等待下游以太网封包模块拉高 &lt;code&gt;s_axis_trdy&lt;/code&gt; 信号，之后开始发送 ARP 数据。&lt;/li&gt;
&lt;li&gt;发送到最后一个字节的 ARP 数据时，产生 &lt;code&gt;s_axis_tend&lt;/code&gt; 信号。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整的实现代码如下：
已折叠部分代码&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module arp_tx (
    input   wire            clk             ,   //  [I] [     ] module clock
    input   wire            rst_n           ,   //  [I] [     ] module reset, active-low
    input   wire            arp_tx_en       ,   //  [I] [     ] ARP TX enable signal, active-high pulse signal
    input   wire            arp_tx_type     ,   //  [I] [     ] ARP TX type, 1&apos;b0: ARP req, 1&apos;b1: ARP ack

    input   wire    [47:0]  tx_src_mac      ,   //  [I] [47:00] Source MAC address to be send
    input   wire    [47:0]  tx_dst_mac      ,   //  [I] [47:00] Destination MAC address to be send
    input   wire    [31:0]  tx_src_ip       ,   //  [I] [31:00] Source IP address to be send
    input   wire    [31:0]  tx_dst_ip       ,   //  [I] [31:00] Destination IP address to be send

    // downstream: ethernet pack encapsulation
    input   wire            encap_busy      ,   //  [I] [     ] encapsulation module busy state input, we only send arp packet when this signal is 1&apos;b0    
    output  reg     [7:0]   s_axis_td       ,   //  [O] [07:00] axis send data output to encapsulation module
    output  reg             s_axis_tdv      ,   //  [O] [     ] axis send data valid output to encapsulation module
    output  reg             s_axis_tend     ,   //  [O] [     ] axis send tend output to encapsulation module
    input   wire            s_axis_trdy         //  [I] [     ] axis send ready signal
);

//================================================================================
// Local Parameter Declarations
//================================================================================
// states define
localparam  ST_IDLE         =   2&apos;d0        ;
localparam  ST_ARP_DATA     =   2&apos;d1        ;
localparam  ST_ARP_END      =   2&apos;d2        ;

// local parameters
localparam  ETH_TYPE        =   16&apos;h0806    ;
localparam  HD_TYPE         =   16&apos;h0001    ;
localparam  PROTOCOL_TYPE   =   16&apos;h0800    ;
localparam  MAC_ADD_LEN     =   8&apos;h06       ;
localparam  IP_ADD_LEN      =   8&apos;h04       ;
localparam  OP_CODE         =   16&apos;h0001    ;

localparam  PREAMBLE_WORD   =   8&apos;h55       ;
localparam  SFD_WORD        =   8&apos;hd5       ;

//================================================================================
// Register Declarations
//================================================================================
reg [1:0]   state               ;
reg [7:0]   arp_data    [27:0]  ;
reg [4:0]   arp_cnt             ;

reg [2:0]   arp_tx_en_sr        ;
reg         arp_tx_en_latch     ;

//================================================================================
// Wire Declarations
//================================================================================
wire        arp_tx_en_pulse     ;

//================================================================================
// Assign Declarations
//================================================================================
assign  arp_tx_en_pulse = {arp_tx_en_sr[0] &amp;#x26; !arp_tx_en_sr[1]};

//================================================================================
// MAIN CODE
//================================================================================

// sync arp_tx_en signal
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        arp_tx_en_sr &amp;#x3C;= 3&apos;b000  ;
    end else begin
        arp_tx_en_sr &amp;#x3C;= {arp_tx_en_sr[1:0], arp_tx_en}  ;
    end
end

always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        arp_data[00]    &amp;#x3C;=  HD_TYPE[15:08]      ;
        arp_data[01]    &amp;#x3C;=  HD_TYPE[07:00]      ;
        arp_data[02]    &amp;#x3C;=  PROTOCOL_TYPE[15:08];
        arp_data[03]    &amp;#x3C;=  PROTOCOL_TYPE[07:00];
        arp_data[04]    &amp;#x3C;=  MAC_ADD_LEN         ;
        arp_data[05]    &amp;#x3C;=  IP_ADD_LEN          ;
        arp_data[06]    &amp;#x3C;=  OP_CODE[15:08]      ;
        arp_data[07]    &amp;#x3C;=  OP_CODE[07:00]      ;
        arp_data[08]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[09]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[10]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[11]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[12]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[13]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[14]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[15]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[16]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[17]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[18]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[19]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[20]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[21]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[22]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[23]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[24]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[25]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[26]    &amp;#x3C;=  8&apos;b00   ;
        arp_data[27]    &amp;#x3C;=  8&apos;b00   ;

        s_axis_tdv      &amp;#x3C;=  1&apos;b0    ;
        arp_tx_en_latch &amp;#x3C;=  1&apos;b0    ;
        s_axis_tend     &amp;#x3C;=  1&apos;b0    ;
        s_axis_td       &amp;#x3C;=  8&apos;b0    ;
        arp_cnt         &amp;#x3C;=  5&apos;d0    ;
        state           &amp;#x3C;=  ST_IDLE ;
    end else begin
        if (arp_tx_en_pulse) arp_tx_en_latch  &amp;#x3C;=  1&apos;b1        ;

        case (state)
            ST_IDLE: begin
                s_axis_tend &amp;#x3C;=  1&apos;b0    ;
                s_axis_td   &amp;#x3C;=  8&apos;b0    ;
                arp_cnt     &amp;#x3C;=  5&apos;d0    ;
                s_axis_tdv  &amp;#x3C;=  1&apos;b0    ;
                if ((arp_tx_en_latch == 1&apos;b1) &amp;#x26;&amp;#x26; (encap_busy == 1&apos;b0)) begin
                    arp_tx_en_latch &amp;#x3C;=  1&apos;b0                ;
                    arp_data[08]    &amp;#x3C;=  tx_src_mac[47:40]   ;
                    arp_data[09]    &amp;#x3C;=  tx_src_mac[39:32]   ;
                    arp_data[10]    &amp;#x3C;=  tx_src_mac[31:24]   ;
                    arp_data[11]    &amp;#x3C;=  tx_src_mac[23:16]   ;
                    arp_data[12]    &amp;#x3C;=  tx_src_mac[15:08]   ;
                    arp_data[13]    &amp;#x3C;=  tx_src_mac[07:00]   ;
                    arp_data[14]    &amp;#x3C;=  tx_src_ip [31:24]   ;
                    arp_data[15]    &amp;#x3C;=  tx_src_ip [23:16]   ;
                    arp_data[16]    &amp;#x3C;=  tx_src_ip [15:08]   ;
                    arp_data[17]    &amp;#x3C;=  tx_src_ip [07:00]   ;
                    arp_data[18]    &amp;#x3C;=  tx_dst_mac[47:40]   ;
                    arp_data[19]    &amp;#x3C;=  tx_dst_mac[39:32]   ;
                    arp_data[20]    &amp;#x3C;=  tx_dst_mac[31:24]   ;
                    arp_data[21]    &amp;#x3C;=  tx_dst_mac[23:16]   ;
                    arp_data[22]    &amp;#x3C;=  tx_dst_mac[15:08]   ;
                    arp_data[23]    &amp;#x3C;=  tx_dst_mac[07:00]   ;
                    arp_data[24]    &amp;#x3C;=  tx_dst_ip [31:24]   ;
                    arp_data[25]    &amp;#x3C;=  tx_dst_ip [23:16]   ;
                    arp_data[26]    &amp;#x3C;=  tx_dst_ip [15:08]   ;
                    arp_data[27]    &amp;#x3C;=  tx_dst_ip [07:00]   ;

                    if (arp_tx_type == 1&apos;b0) begin
                        arp_data[7]     &amp;#x3C;=  8&apos;h01   ;
                        arp_data[18]    &amp;#x3C;=  8&apos;h00   ;
                        arp_data[19]    &amp;#x3C;=  8&apos;h00   ;
                        arp_data[20]    &amp;#x3C;=  8&apos;h00   ;
                        arp_data[21]    &amp;#x3C;=  8&apos;h00   ;
                        arp_data[22]    &amp;#x3C;=  8&apos;h00   ;
                        arp_data[23]    &amp;#x3C;=  8&apos;h00   ;
                    end else begin
                        arp_data[7]     &amp;#x3C;=  8&apos;h02   ;
                    end

                    state           &amp;#x3C;=  ST_ARP_DATA     ;
                    s_axis_tdv      &amp;#x3C;=  1&apos;b1            ;
                end
            end

            ST_ARP_DATA: begin
                if (s_axis_trdy == 1&apos;b1) begin
                    s_axis_td   &amp;#x3C;=  arp_data[arp_cnt]   ;
                    arp_cnt     &amp;#x3C;=  arp_cnt + 1&apos;b1      ;
                    if (arp_cnt == 5&apos;d27) begin
                        s_axis_tend &amp;#x3C;=  1&apos;b1            ;
                        state       &amp;#x3C;=  ST_ARP_END      ;
                    end
                end
            end

            ST_ARP_END: begin
                s_axis_tend &amp;#x3C;=  1&apos;b0    ;
                s_axis_td   &amp;#x3C;=  8&apos;d0    ;
                s_axis_tdv  &amp;#x3C;=  1&apos;b0    ;
                arp_cnt     &amp;#x3C;=  5&apos;d0    ;
                state       &amp;#x3C;=  ST_IDLE ;
            end
        endcase
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;arp-接收模块&quot;&gt;ARP 接收模块&lt;/h4&gt;
&lt;p&gt;接收模块同样从以太网帧解包模块中接收负载数据，负载数据即为 ARP 数据内容。&lt;/p&gt;
&lt;p&gt;模块工作过程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从上游以太网帧解包模块中等待 &lt;code&gt;s_axis_rdv&lt;/code&gt; 信号，表明负载数据有效。&lt;/li&gt;
&lt;li&gt;开始按字节计数，将接收到的数据寄存&lt;/li&gt;
&lt;li&gt;判断目的 IP 地址是否为板卡 IP 地址，如果是板卡的 IP 地址，则将寄存的源 MAC 地址输出到模块的端口，方便其它模块调用，否则将接收到的数据丢弃。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;module arp_rx (
    input   wire            clk             ,   //  [I] [     ] Module clock
    input   wire            rst_n           ,   //  [I] [     ] Module reset signal, active-low

    input   wire    [15:00] eth_type        ,   //  [I] [15:00] received ethertype
    output  reg     [47:00] src_mac         ,   //  [O] [47:00] Received source MAC address
    output  reg     [31:00] src_ip          ,   //  [O] [31:00] Received source IP address
    input   wire    [31:00] board_ip        ,   //  [I] [31:00] Input board IP address, for comparison with received destination IP address

    // upstream: ethernet pack decapsulation
    input   wire            decap_busy      ,   //  [I] [     ] decapsulation module busy state input
    input   wire    [7:0]   s_axis_rd       ,   //  [I] [07:00] axis receive data input from decapsulation module
    input   wire            s_axis_rdv      ,   //  [I] [     ] axis receive data valid input from decapsulation module
    input   wire            s_axis_rend     ,   //  [I] [     ] axis receive tend input from decapsulation module
    output  reg             s_axis_rrdy     ,   //  [O] [     ] axis receive ready signal
    
    output  reg             arp_rx_done     ,   //  [O] [     ] ARP receive done signal, active-high
    output  reg             arp_rx_type         //  [O] [     ] ARP receive type, 1&apos;b1: ack, 1&apos;b0: req
);

//================================================================================
// Local Parameter Declarations
//================================================================================
localparam  ST_IDLE         =   2&apos;b00   ;
localparam  ST_ARP_DATA     =   2&apos;b01   ;
localparam  ST_ARP_END      =   2&apos;b10   ;

localparam  ETH_TYPE_ARP    =   16&apos;h0806;

//================================================================================
// Register Declarations
//================================================================================
reg [47:00] dst_mac_t       ;
reg [31:00] dst_ip_t        ;
reg [47:00] src_mac_t       ;
reg [31:00] src_ip_t        ;
reg [15:00] op_data         ;

reg [01:00] state           ;
reg [04:00] data_bytes_cnt  ;

//================================================================================
// MAIN CODE
//================================================================================
always  @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state           &amp;#x3C;=  ST_IDLE ;
        data_bytes_cnt  &amp;#x3C;=  5&apos;b0    ;
        s_axis_rrdy     &amp;#x3C;=  1&apos;b0    ;
        op_data         &amp;#x3C;=  16&apos;b0   ;
        dst_mac_t       &amp;#x3C;=  48&apos;b0   ;
        dst_ip_t        &amp;#x3C;=  32&apos;b0   ;
        src_mac_t       &amp;#x3C;=  48&apos;b0   ;
        src_ip_t        &amp;#x3C;=  32&apos;b0   ;
        src_mac         &amp;#x3C;=  48&apos;b0   ;
        src_ip          &amp;#x3C;=  32&apos;b0   ;
        arp_rx_done     &amp;#x3C;=  1&apos;b0    ;
        arp_rx_type     &amp;#x3C;=  1&apos;b0    ;
    end else begin 
        case(state) 
            ST_IDLE: begin
                data_bytes_cnt  &amp;#x3C;=  5&apos;b0        ;
                s_axis_rrdy     &amp;#x3C;=  1&apos;b1        ;
                arp_rx_done     &amp;#x3C;=  1&apos;b0        ;
                
                if (
                    eth_type    ==  ETH_TYPE_ARP &amp;#x26;&amp;#x26;
                    s_axis_rdv  ==  1&apos;b1        
                ) begin
                    state       &amp;#x3C;=  ST_ARP_DATA ;
                    data_bytes_cnt  &amp;#x3C;=  data_bytes_cnt + 1&apos;b1   ;
                end else begin
                    state       &amp;#x3C;=  ST_IDLE     ;
                end
            end

            ST_ARP_DATA: begin
                data_bytes_cnt  &amp;#x3C;=  data_bytes_cnt + 1&apos;b1           ;
                if (data_bytes_cnt &gt;= 5&apos;d6 &amp;#x26;&amp;#x26; data_bytes_cnt &amp;#x3C; 5&apos;d8) begin
                    op_data     &amp;#x3C;=  {op_data[7:0], s_axis_rd}       ;                   // OP code
                end else if (data_bytes_cnt &gt;= 5&apos;d8  &amp;#x26;&amp;#x26; data_bytes_cnt &amp;#x3C; 5&apos;d14) begin   
                    src_mac_t   &amp;#x3C;=  {src_mac_t[39:0], s_axis_rd}    ;                   // So urce MAC address
                end else if (data_bytes_cnt &gt;= 5&apos;d14 &amp;#x26;&amp;#x26; data_bytes_cnt &amp;#x3C; 5&apos;d18) begin
                    src_ip_t    &amp;#x3C;=  {src_ip_t[23:0], s_axis_rd}     ;                   // Source IP address
                end else if (data_bytes_cnt &gt;= 5&apos;d18 &amp;#x26;&amp;#x26; data_bytes_cnt &amp;#x3C; 5&apos;d24) begin
                    dst_mac_t   &amp;#x3C;=  {dst_mac_t[39:0], s_axis_rd}    ;                   // Destination MAC address
                end else if (data_bytes_cnt &gt;= 5&apos;d24 &amp;#x26;&amp;#x26; data_bytes_cnt &amp;#x3C; 5&apos;d28) begin
                    dst_ip_t    &amp;#x3C;=  {dst_ip_t[23:0], s_axis_rd}     ;                   // Destination IP address
                end else if (data_bytes_cnt &gt;= 5&apos;d28) begin
                    if (
                        dst_ip_t    == board_ip &amp;#x26;&amp;#x26; 
                        ((op_data == 16&apos;d1) || (op_data == 16&apos;d2))
                    ) begin
                        arp_rx_done &amp;#x3C;=  1&apos;b1        ;
                        src_mac     &amp;#x3C;=  src_mac_t   ;
                        src_ip      &amp;#x3C;=  src_ip_t    ;
                        dst_mac_t   &amp;#x3C;=  48&apos;b0       ;
                        dst_ip_t    &amp;#x3C;=  32&apos;b0       ;
                        src_mac_t   &amp;#x3C;=  48&apos;b0       ;
                        src_ip_t    &amp;#x3C;=  32&apos;b0       ;
                        if (op_data == 16&apos;d1) begin
                            arp_rx_type &amp;#x3C;= 1&apos;b0     ;   //  arp request
                        end else begin
                            arp_rx_type &amp;#x3C;= 1&apos;b1     ;   //  arp ack
                        end
                        state   &amp;#x3C;=  ST_ARP_END      ;
                    end else begin
                        state   &amp;#x3C;= ST_ARP_END       ;
                    end
                end
            end

            ST_ARP_END: begin
                s_axis_rrdy     &amp;#x3C;=  1&apos;b0            ;
                if (decap_busy == 1&apos;b0) begin
                    state       &amp;#x3C;=  ST_IDLE         ;
                end
            end
        endcase
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h4 id=&quot;arp-模块&quot;&gt;ARP 模块&lt;/h4&gt;
&lt;p&gt;这个模块是将两个模块进行整合封装到一个顶层，方便后续的例化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module arp (
    input                   rst_n       ,   //  [I] [     ] Module reset signal, active-low
    input                   gmii_tx_clk ,   //  [I] [     ] GMII TX clock
    input                   gmii_rx_clk ,   //  [I] [     ] GMII RX clock
    
    input   wire    [47:00] board_mac   ,   //  [I] [47:00] Input board MAC address, for comparison with received destination MAC address.
    input   wire    [31:00] board_ip    ,   //  [I] [31:00] Input board IP address, for comparison with received destination IP address.

    input   wire            decap_busy  ,   //  [I] [     ] Upstream decapsulation module busy state input
    input   wire            encap_busy  ,   //  [I] [     ] Downstream encapsulation module busy state input

    // arp rx
    input   wire    [07:00] s_axis_rd   ,   //  [I] [07:00] axis receive data input from decapsulation module
    input   wire            s_axis_rdv  ,   //  [I] [     ] axis receive data valid input from decapsulation module
    input   wire            s_axis_rend ,   //  [I] [     ] axis receive tend input from decapsulation module
    output  wire            s_axis_rrdy ,   //  [O] [     ] axis receive ready signal
    output  wire            arp_rx_done ,   //  [O] [     ] ARP receive done signal, active-high
    output  wire            arp_rx_type ,   //  [O] [     ] ARP receive type, 1&apos;b1: ack, 1&apos;b0: req
    input   wire    [15:00] eth_type    ,   //  [I] [15:00] received ethertype
    output  wire    [47:00] rx_src_mac  ,   //  [O] [47:00] Received source MAC address
    output  wire    [31:00] rx_src_ip   ,   //  [O] [31:00] Received source IP address

    // arp tx
    input   wire            arp_tx_en   ,   //  [I] [     ] ARP TX enable signal, active-high pulse signal
    input   wire            arp_tx_type ,   //  [I] [     ] ARP TX type, 1&apos;b0: ARP req, 1&apos;b1: ARP ack
    input   wire    [47:00] tx_dst_mac  ,   //  [I] [47:00] Destination MAC address to be send
    input   wire    [31:00] tx_dst_ip   ,   //  [I] [31:00] Destination IP address to be send
    output  wire    [07:00] s_axis_td   ,   //  [O] [07:00] axis send data output to encapsulation module
    output  wire            s_axis_tdv  ,   //  [O] [     ] axis send data valid output to encapsulation module
    output  wire            s_axis_tend ,   //  [O] [     ] axis send tend output to encapsulation module
    input   wire            s_axis_trdy     //  [I] [     ] axis send ready signal
);

arp_rx arp_rx_inst (
    .clk            (   gmii_rx_clk ),  //  [I] [     ] Module clock
    .rst_n          (   rst_n       ),  //  [I] [     ] Module reset signal, active-low
    
    .eth_type       (   eth_type    ),  //  [I] [15:00] received ethertype
    .src_mac        (   rx_src_mac  ),  //  [O] [47:00] Received source MAC address
    .src_ip         (   rx_src_ip   ),  //  [O] [31:00] Received source IP address
    .board_ip       (   board_ip    ),  //  [I] [31:00] Input board IP address, for comparison with received destination IP address

    .decap_busy     (   decap_busy  ),  //  [I] [     ] decapsulation module busy state input
    .s_axis_rd      (   s_axis_rd   ),  //  [I] [07:00] axis receive data input from decapsulation module
    .s_axis_rdv     (   s_axis_rdv  ),  //  [I] [     ] axis receive data valid input from decapsulation module
    .s_axis_rend    (   s_axis_rend ),  //  [I] [     ] axis receive tend input from decapsulation module
    .s_axis_rrdy    (   s_axis_rrdy ),  //  [O] [     ] axis receive ready signal
    .arp_rx_done    (   arp_rx_done ),  //  [O] [     ] ARP receive done signal, active-high
    .arp_rx_type    (   arp_rx_type )   //  [O] [     ] ARP receive type, 1&apos;b1: ack, 1&apos;b0: req
);

arp_tx arp_tx_inst (
    .clk            (   gmii_tx_clk ),  //  [I] [     ] Module clock
    .rst_n          (   rst_n       ),  //  [I] [     ] Module reset signal, active-low

    .tx_src_mac     (   board_mac   ),  //  [I] [47:00] Source MAC address to be send
    .tx_src_ip      (   board_ip    ),  //  [I] [31:00] Source IP address to be send
    .tx_dst_mac     (   tx_dst_mac  ),  //  [I] [47:00] Destination MAC address to be send
    .tx_dst_ip      (   tx_dst_ip   ),  //  [I] [31:00] Destination IP address to be send
    
    .encap_busy     (   encap_busy  ),  //  [I] [     ] encapsulation module busy state input, we only send arp packet when this signal is 1&apos;b0    
    .s_axis_td      (   s_axis_td   ),  //  [O] [07:00] axis send data output to encapsulation module
    .s_axis_tdv     (   s_axis_tdv  ),  //  [O] [     ] axis send data valid output to encapsulation module
    .s_axis_tend    (   s_axis_tend ),  //  [O] [     ] axis send tend output to encapsulation module
    .s_axis_trdy    (   s_axis_trdy ),  //  [I] [     ] axis send ready signal
    .arp_tx_en      (   arp_tx_en   ),  //  [I] [     ] ARP TX enable signal, active-high pulse signal
    .arp_tx_type    (   arp_tx_type )   //  [I] [     ] ARP TX type, 1&apos;b0: ARP req, 1&apos;b1: ARP ack
);&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/6ec21651</id>,<title>ETH-03 以太网帧结构介绍以及 Verilog 实现代码</title>,<link href="https://blog.tyh123.top/posts/6ec21651" rel="alternate" type="text/html"/>,<published>2026-04-15T10:47:26.000Z</published>,<updated>2026-04-15T10:47:26.000Z</updated>,<summary type="text">介绍以太网帧的完整结构, 包括前导码、SFD、目的与源 MAC 地址、长度与类型、数据段、FCS 和帧间隙, 并附上 Verilog 收发实现代码。</summary>,<content type="html">&lt;h2 id=&quot;什么是以太网帧&quot;&gt;什么是以太网帧&lt;/h2&gt;
&lt;p&gt;以太网帧是指在网络中&lt;strong&gt;传输数据的基本单位&lt;/strong&gt;。以太网帧按照一定的格式组成了以太网数据包。简单来讲，它类似于 “数据包裹(就像快递一样)“，包裹中包含了 “寄件人 (发送方 MAC 地址)”、“收件人 (接收方 MAC 地址)” 和 “包裹内容 (有效数据)“。以太网帧指的就是这个数据包本身。&lt;/p&gt;
&lt;p&gt;理解以太网帧的组成是我们开始学习以太网 ARP、ICMP、UDP 等上层协议的基础。&lt;/p&gt;
&lt;h2 id=&quot;以太网帧的组成-格式&quot;&gt;以太网帧的组成 (格式)&lt;/h2&gt;
&lt;p&gt;数据格式如下图所示。
&lt;img alt=&quot;以太网数据帧结构&quot; width=&quot;1160&quot; height=&quot;120&quot; src=&quot;https://blog.tyh123.top/_astro/ethII_frame_format.3wQDIkNc_oDDAU.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;前导码 (Preamble)&lt;/strong&gt;：固定的 &lt;strong&gt;7 字节 0x55&lt;/strong&gt;，二进制表现为 0 和 1 交替。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;帧起始界定符 (SFD)&lt;/strong&gt;：全称：Start Frame Delimiter。固定的 &lt;strong&gt;1 字节 0xD5&lt;/strong&gt;，二进制表现为 &lt;code&gt;1101_1010&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目的 MAC 地址&lt;/strong&gt;：即接收端的物理 MAC 地址，占用 6 个字节，MAC 地址从应用上可分为以下三种类型
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;单播地址&lt;/strong&gt;：第一个字节的最低位为 0，比如 &lt;code&gt;00:00:00:11:11:11&lt;/code&gt;一般用于标志唯一的设备&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;组播地址&lt;/strong&gt;：第一个字节的最低位为 1，比如&lt;code&gt;01:00:00:11:11:11&lt;/code&gt;一般用于标志同属一组的多个设备&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广播地址&lt;/strong&gt;：所有的 48 bit 全部为 1，即&lt;code&gt;FF:FF:FF:FF:FF:FF&lt;/code&gt;，它用于标志同一网段中的所有设备&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;源 MAC 地址&lt;/strong&gt;：即发送端的 MAC 地址，占用 6 个字节&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;长度/类型&lt;/strong&gt;：当这两个字节的值小于 1500 (对应十六进制 0x05DC) 时，代表该以太网数据包中的数据段长度；大于 1500 (对应十六进制 0x05DC) 时，代表以太网中的数据属于某个 上层协议。例如，&lt;strong&gt;0x0800 代表 IP 协议 (国际协议)&lt;/strong&gt;、&lt;strong&gt;0x0806 代表 ARP 协议 (地址解析协议)&lt;/strong&gt; 等，其余内容会在 &lt;a href=&quot;#%E4%BB%A5%E5%A4%AA%E7%BD%91%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B&quot;&gt;以太网数据类型&lt;/a&gt; 这个表中列出&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据段&lt;/strong&gt;：长度最小46个字节，最大1500个字节。
&lt;ul&gt;
&lt;li&gt;最大值 1500 称为以太网的&lt;strong&gt;最大传输单元（MTU，Maximum Transmission Unit）&lt;/strong&gt;，之所以限制最大传输单元是因为在多个计算机的数据帧排队等待传输时，如果某个数据帧太大的话，接收这一帧的数据所带来的时间开销将会变高，从而导致体验变差。另外还要考虑网络I/O控制器缓存区资源以及网络最大的承载能力等因素，因此最大传输单元是由各种综合因素决定的。&lt;/li&gt;
&lt;li&gt;为了避免增加额外的配置，通常以太网的有效数据字段都会小于1500个字节，现在电脑的配置已经足够接收巨型帧，巨型帧可以超过 1500 个字节。但是，要成功使用巨型帧，网络中的所有设备（包括交换机、路由器和主机）都必须支持巨型帧，并且 MTU 的大小必须一致。如果网络中的任何设备不支持巨型帧，或者 MTU 的大小不一致，那么数据包可能会被丢弃或者分片，从而影响网络性能。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;帧检验序列（FCS）&lt;/strong&gt;：全称：Frame Check Sequence。&lt;strong&gt;4 个字节的循环冗余校验码 (通常是CRC-32 校验)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CRC 数据校验从目的 MAC 地址开始计算，直到数据段最后一个数据结束&lt;/strong&gt;。通用的CRC标准有 CRC-8、CRC-16、CRC-32、CRC-CCIT，其中&lt;strong&gt;在网络通信系统中应用最广泛的是 CRC-32 标准&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;帧间隙（IFG）&lt;/strong&gt;：全称：Interpack Gap。&lt;strong&gt;即两帧之间的时间间隔，最小为 96bit time。&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;IFG 的最小值是 96 bit time ，即在&lt;strong&gt;媒介中发送 96 位原始数据所需要的时间&lt;/strong&gt;，在不同媒介中 IFG 的最小值是不一样的。不管 10M/100M/1000M 的以太网，两帧之间最少要有 96bit time，IFG 的最少间隔时间计算方法如下：
&lt;ul&gt;
&lt;li&gt;10 Mbps 最小时间是 &lt;code&gt;96*100ns = 9600ns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;100 Mbps 最小时间是 &lt;code&gt;96*10ns = 960ns&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;1000 Mbps 最小时间是 &lt;code&gt;96*1ns = 96ns&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;以太网数据类型&quot;&gt;以太网数据类型&lt;/h2&gt;
&lt;p&gt;在以太网数据包结构中，2 字节类型字段的内容如下表，其中加粗的类型为常用类型。&lt;/p&gt;





















































































































































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;&lt;strong&gt;Ethertype   &lt;br&gt;( 十六进制 )&lt;/strong&gt;&lt;/th&gt;&lt;th&gt;&lt;strong&gt;协议&lt;/strong&gt;&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;0x0800&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;网际协议（IP）&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;0x0806&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;&lt;strong&gt;地址解析协议（ARP ： Address Resolution Protocol）&lt;/strong&gt;&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0000 - 0x05DC&lt;/td&gt;&lt;td&gt;IEEE 802.3 长度&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0101 – 0x01FF&lt;/td&gt;&lt;td&gt;实验&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0600&lt;/td&gt;&lt;td&gt;XEROX NS IDP&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0660, 0x0661&lt;/td&gt;&lt;td&gt;DLOG&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0801&lt;/td&gt;&lt;td&gt;X.75 Internet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0802&lt;/td&gt;&lt;td&gt;NBS Internet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0803&lt;/td&gt;&lt;td&gt;ECMA Internet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0804&lt;/td&gt;&lt;td&gt;Chaosnet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0805&lt;/td&gt;&lt;td&gt;X.25 Level 3&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x0808&lt;/td&gt;&lt;td&gt;帧中继 ARP （Frame Relay ARP） [RFC1701]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x6559&lt;/td&gt;&lt;td&gt;原始帧中继（Raw Frame Relay） [RFC1701]&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8035&lt;/td&gt;&lt;td&gt;动态 DARP （DRARP：Dynamic RARP）&lt;br&gt;反向地址解析协议（RARP：Reverse Address Resolution Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8037&lt;/td&gt;&lt;td&gt;Novell Netware IPX&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x809B&lt;/td&gt;&lt;td&gt;EtherTalk&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x80D5&lt;/td&gt;&lt;td&gt;IBM SNA Services over Ethernet&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x 80F 3&lt;/td&gt;&lt;td&gt;AppleTalk 地址解析协议（AARP：AppleTalk Address Resolution Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8100&lt;/td&gt;&lt;td&gt;以太网自动保护开关（EAPS：Ethernet Automatic Protection Switching）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8137&lt;/td&gt;&lt;td&gt;因特网包交换（IPX：Internet Packet Exchange）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x 814C&lt;/td&gt;&lt;td&gt;简单网络管理协议（SNMP：Simple Network Management Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x86DD&lt;/td&gt;&lt;td&gt;网际协议v6 （IPv6，Internet Protocol version 6）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x880B&lt;/td&gt;&lt;td&gt;点对点协议（PPP：Point-to-Point Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x 880C&lt;/td&gt;&lt;td&gt;通用交换管理协议（GSMP：General Switch Management Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8847&lt;/td&gt;&lt;td&gt;多协议标签交换（单播） MPLS：Multi-Protocol Label Switching [unicast]）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8848&lt;/td&gt;&lt;td&gt;多协议标签交换（组播）（MPLS, Multi-Protocol Label Switching [multicast]）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8863&lt;/td&gt;&lt;td&gt;以太网上的 PPP（发现阶段）（PPPoE：PPP Over Ethernet [Discovery Stage]）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8864&lt;/td&gt;&lt;td&gt;以太网上的 PPP（PPP 会话阶段） （PPPoE，PPP Over Ethernet[PPP Session Stage]）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x88BB&lt;/td&gt;&lt;td&gt;轻量级访问点协议（LWAPP：Light Weight Access Point Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x88CC&lt;/td&gt;&lt;td&gt;链接层发现协议（LLDP：Link Layer Discovery Protocol）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x8E88&lt;/td&gt;&lt;td&gt;局域网上的 EAP（EAPOL：EAP over LAN）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x9000&lt;/td&gt;&lt;td&gt;配置测试协议（Loopback）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x9100&lt;/td&gt;&lt;td&gt;VLAN 标签协议标识符（VLAN Tag Protocol Identifier）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0x9200&lt;/td&gt;&lt;td&gt;VLAN 标签协议标识符（VLAN Tag Protocol Identifier）&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;0xFFFF&lt;/td&gt;&lt;td&gt;保留&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;
&lt;h2 id=&quot;实现代码&quot;&gt;实现代码&lt;/h2&gt;
&lt;h3 id=&quot;以太网帧封包模块&quot;&gt;以太网帧封包模块&lt;/h3&gt;
&lt;p&gt;该模块负责将来自上层协议的数据，如 ARP、UDP 协议 (包含上层协议的数据头) 等打包成标准的以太网帧，通过 GMII 接口输出。&lt;/p&gt;
&lt;p&gt;模块工作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上层协议模块将端口&lt;code&gt;s_axis_tdv&lt;/code&gt; 拉高，此时封包模块开始发送前导码、SFD、目的 MAC 地址 、源 MAC 地址以及以太网帧类型。&lt;/li&gt;
&lt;li&gt;当前导内容发送完成后，&lt;code&gt;s_axis_trdy&lt;/code&gt; 端口会被拉高，此时可以通过  &lt;code&gt;s_axis_td&lt;/code&gt; 端口写入负载数据。&lt;/li&gt;
&lt;li&gt;当负载数据写入最后一个字节时，上层协议模块给端口 &lt;code&gt;s_axis_tend&lt;/code&gt; 一个时钟周期的脉冲信号，结束发送。&lt;/li&gt;
&lt;li&gt;需要注意的是，上层协议模块给端口 &lt;code&gt;s_axis_tend&lt;/code&gt; 产生脉冲后，封包模块并不会立即结束发送，而是继续发送 FCS 和等待帧间隙。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;module_busy&lt;/code&gt; 信号在发送数据时 (从前导码开始，到 IFG 结束) 始终为高电平状态。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// NOTE_HEADER--------------------------------------------------------------------------------------
// Useage of this module:
//  - Port s_axis_tdv set to 1&apos;b1
//  - Wait s_axis_rdy goes to 1&apos;b1, write data to port s_axis_td
//  - When data write done, give an pulse signal to s_axis_tend
// NOTE_FOOTER--------------------------------------------------------------------------------------

module eth_frame_encap # (
    parameter   USE_DEBUG   =   1&apos;b0        
) (
    input   wire            rst_n           ,   //  [I] [    ] reset signal, active-low

    // user input interface, (AXI-Stream)
    input   wire    [ 7:0]  s_axis_td       ,   //  [I] [ 7:0] tx data input
    input   wire            s_axis_tdv      ,   //  [I] [    ] tx data valid
    input   wire            s_axis_tend     ,   //  [I] [    ] the last data input,
    output  reg             s_axis_trdy     ,   //  [O] [    ] tx data ready signal, high logic when module can send data

    input   wire    [47:0]  src_mac         ,   //  [I] [47:0] Source MAC address to be send
    input   wire    [47:0]  dst_mac         ,   //  [I] [47:0] Destination MAC address to be send
    input   wire    [15:0]  eth_type        ,   //  [I] [15:0] Ethernet type (or data length, but we don&apos;t need this), 
                                                //             when its value is less than 1500(Decimal, 0x05DC in hex), it refers to IEEE 802.3 data length (we don&apos;t need this)
                                                //             when its value is bigger than 1500(Decimal, 0x05DC in hex), it refers to ethernet type
    output  wire            module_busy     ,   //  [O] [    ] module busy signal when sending data, active-high
    
    // GMII output interface
    input   wire            gmii_txc        ,   //  [I]
    output  reg     [7:0]   gmii_txd        ,   //  [O]
    output  reg             gmii_tx_en          //  [O]
);

//================================================================================
// Local Parameter Declarations
//================================================================================
localparam  PADDING_DATA    =   8&apos;h00       ;   //  If data segment length is less than 46, padding this data until its length reach to 46.
localparam  PREAMBLE_WORD   =   8&apos;h55       ;
localparam  SFD_WORD        =   8&apos;hD5       ;
localparam  MIN_FRAME_LEN   =   8&apos;d64       ;   //  Minimum frame length in bytes, includes 6 bytes dest_mac, 6 bytes src_mac, 2 bytes type/length, minum 46 bytes data segment and 4 bytes FCS
localparam  HEADER_LENGTH   =   8&apos;d14       ;   //  Ethernet pack header length in bytes, 6 bytes dest_mac, 6 bytes src_mac and 2 bytes type/length
localparam  CRC_LENGTH      =   8&apos;d4        ;   //  CRC data length in bytes
localparam  MIN_PAYLOAD     =   8&apos;d46       ;   //  Minimum payload length
localparam  MAX_PAYLOAD     =   16&apos;d1500    ;   //  Maximum payload length
localparam  IFG_CNT         =   4&apos;d12       ;

// states defines
localparam  ST_IDLE         =   4&apos;d0    ;   //  IDLE state
localparam  ST_PREAMBLE     =   4&apos;d1    ;   //  State of sending preamble code, 7 bytes of 0x55
localparam  ST_SFD          =   4&apos;d2    ;   //  State of sending SFD, Start Frame Delimiter, 1 byte of 0xd5
localparam  ST_DST_MAC      =   4&apos;d3    ;   //  State of sending destination MAC Address
localparam  ST_SRC_MAC      =   4&apos;d4    ;   //  State of sending source MAC Address
localparam  ST_ETH_TYPE     =   4&apos;d5    ;   //  State of sending Ethernet type
localparam  ST_PAYLOAD      =   4&apos;d6    ;   //  State of sending payload, the data content of this packet
localparam  ST_PADDING      =   4&apos;d7    ;   //  State of padding data, if data content length is less than 46, then fill data.
localparam  ST_CRC          =   4&apos;d8    ;   //  State of sending CRC result
localparam  ST_IFG          =   4&apos;d9    ;   //  State of Interpacket GAP, 10Mbps min: 9600ns, 100Mbps min: 960ns, 1000Mbps min: 96ns

//================================================================================
// Register Declarations
//================================================================================
reg     [ 3:0]  state           ;
reg     [15:0]  byte_cnt        ;   // counter
reg     [15:0]  payload_len     ;   
reg     [ 3:0]  ifg_cnt         ;   //  IFG counter
reg     [ 7:0]  crc_data_in     ;
reg             crc_en          ;
reg             crc_clr         ;

//================================================================================
// Wire Declarations
//================================================================================
wire    [31:0]  crc_inv         ;
wire    [31:0]  crc_next_inv    ;

//================================================================================
// Assign Declarations
//================================================================================
assign  module_busy =   (state == ST_IDLE) ? 1&apos;b0 : 1&apos;b1    ;

//================================================================================
// implements
//================================================================================
crc32_d8 crc32_d8_inst (
    .clk            (   gmii_txc        ),  // [I] [    ] Module clock
    .rst_n          (   rst_n           ),  // [I] [    ] Reset signal, active-low
    .data           (   crc_data_in     ),  // [I] [ 7:0] The 8-bit data to be validated
    .crc_en         (   crc_en          ),  // [I] [    ] CRC enable
    .crc_clr        (   crc_clr         ),  // [I] [    ] CRC Result Clear
    .crc_next_inv   (   crc_next_inv    ),  // [O] [31:0] CRC data
    .crc_inv        (   crc_inv         )   // [O] [31:0] Inversed crc data
);

//================================================================================
// MAIN CODE
//================================================================================
generate
    if (USE_DEBUG == 1&apos;b1) begin: ila_frame_encap
        ila_frame_encap ila_frame_encap_inst (
            .clk    (   gmii_txc    ),
            .probe0 (   state       ),  //  4b
            .probe1 (   byte_cnt    ),  //  16b
            .probe2 (   payload_len ),  //  16b
            .probe3 (   gmii_txd    ),  //  8b
            .probe4 (   gmii_tx_en  )   //  1b
        );
    end
endgenerate

always  @(posedge gmii_txc or negedge rst_n) begin
    if (!rst_n) begin
        state       &amp;#x3C;=  ST_IDLE ;
        crc_en      &amp;#x3C;=  1&apos;b0    ;
        crc_clr     &amp;#x3C;=  1&apos;b1    ;
        crc_data_in &amp;#x3C;=  8&apos;b0    ;
        s_axis_trdy &amp;#x3C;=  1&apos;b0    ;
        byte_cnt    &amp;#x3C;=  16&apos;b0   ;
        ifg_cnt     &amp;#x3C;=  4&apos;b0    ;
        gmii_txd    &amp;#x3C;=  8&apos;b0    ;
        gmii_tx_en  &amp;#x3C;=  1&apos;d0    ;
        payload_len &amp;#x3C;=  16&apos;d0   ;
    end else begin
        case(state)
            ST_IDLE: begin
                payload_len &amp;#x3C;=  16&apos;d0       ;
                crc_en      &amp;#x3C;=  1&apos;b0        ;
                crc_clr     &amp;#x3C;=  1&apos;b1        ;
                crc_data_in &amp;#x3C;=  8&apos;b0        ;
                byte_cnt    &amp;#x3C;=  16&apos;d0       ;
                ifg_cnt     &amp;#x3C;=  4&apos;d0        ;
                gmii_txd    &amp;#x3C;=  8&apos;b0        ;
                gmii_tx_en  &amp;#x3C;=  1&apos;b0        ;
                s_axis_trdy &amp;#x3C;=  1&apos;b0        ;   //  Stop receive upstream data
                if (s_axis_tdv) begin
                    state   &amp;#x3C;=  ST_PREAMBLE ;
                end
            end

            ST_PREAMBLE: begin
                s_axis_trdy     &amp;#x3C;=  1&apos;b0            ;
                crc_clr         &amp;#x3C;=  1&apos;b1            ;
                crc_en          &amp;#x3C;=  1&apos;b0            ;
                gmii_txd        &amp;#x3C;=  PREAMBLE_WORD   ;
                gmii_tx_en      &amp;#x3C;=  1&apos;b1            ;

                if (byte_cnt == 7-1) begin
                    state       &amp;#x3C;= ST_SFD           ;
                    byte_cnt    &amp;#x3C;=  16&apos;d0           ;
                end else begin
                    byte_cnt        &amp;#x3C;=  byte_cnt + 1&apos;b1 ;
                end
                // ... Preamble does not participate in CRC
            end

            ST_SFD: begin
                crc_clr     &amp;#x3C;=  1&apos;b1                    ;
                crc_en      &amp;#x3C;=  1&apos;b0                    ;

                s_axis_trdy &amp;#x3C;=  1&apos;b0                    ;
                state       &amp;#x3C;=  ST_DST_MAC              ;
                gmii_txd    &amp;#x3C;=  SFD_WORD                ;
                byte_cnt    &amp;#x3C;=  16&apos;d0                   ;
                // ... SFD does not participate in CRC
            end

            ST_DST_MAC: begin                               //  Send destination MAC address
                crc_clr     &amp;#x3C;=  1&apos;b0                    ;
                crc_en      &amp;#x3C;=  1&apos;b1                    ;   //  enable crc
                s_axis_trdy &amp;#x3C;=  1&apos;b0                    ;
                
                case (byte_cnt)
                    0: begin gmii_txd &amp;#x3C;= dst_mac[47:40] ; crc_data_in &amp;#x3C;=  dst_mac[47:40]; end
                    1: begin gmii_txd &amp;#x3C;= dst_mac[39:32] ; crc_data_in &amp;#x3C;=  dst_mac[39:32]; end
                    2: begin gmii_txd &amp;#x3C;= dst_mac[31:24] ; crc_data_in &amp;#x3C;=  dst_mac[31:24]; end
                    3: begin gmii_txd &amp;#x3C;= dst_mac[23:16] ; crc_data_in &amp;#x3C;=  dst_mac[23:16]; end
                    4: begin gmii_txd &amp;#x3C;= dst_mac[15:08] ; crc_data_in &amp;#x3C;=  dst_mac[15:08]; end
                    5: begin gmii_txd &amp;#x3C;= dst_mac[07:00] ; crc_data_in &amp;#x3C;=  dst_mac[07:00]; end
                endcase

                // turn to next state
                if (byte_cnt == 6 - 1) begin
                    state       &amp;#x3C;= ST_SRC_MAC           ;
                    byte_cnt    &amp;#x3C;=  16&apos;d0               ;
                end else begin
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                end
            end

            ST_SRC_MAC: begin                               //  Send destination MAC address
                crc_clr     &amp;#x3C;=  1&apos;b0                    ;
                crc_en      &amp;#x3C;=  1&apos;b1                    ;   //  enable crc
                s_axis_trdy &amp;#x3C;=  1&apos;b0                    ;
                
                case (byte_cnt)
                    0: begin gmii_txd &amp;#x3C;= src_mac[47:40] ; crc_data_in &amp;#x3C;=  src_mac[47:40]; end
                    1: begin gmii_txd &amp;#x3C;= src_mac[39:32] ; crc_data_in &amp;#x3C;=  src_mac[39:32]; end
                    2: begin gmii_txd &amp;#x3C;= src_mac[31:24] ; crc_data_in &amp;#x3C;=  src_mac[31:24]; end
                    3: begin gmii_txd &amp;#x3C;= src_mac[23:16] ; crc_data_in &amp;#x3C;=  src_mac[23:16]; end
                    4: begin gmii_txd &amp;#x3C;= src_mac[15:08] ; crc_data_in &amp;#x3C;=  src_mac[15:08]; end
                    5: begin gmii_txd &amp;#x3C;= src_mac[07:00] ; crc_data_in &amp;#x3C;=  src_mac[07:00]; end
                endcase

                // turn to next state
                if (byte_cnt == 6 - 1) begin
                    state       &amp;#x3C;= ST_ETH_TYPE          ;
                    byte_cnt    &amp;#x3C;=  16&apos;d0               ;
                end else begin
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                end
            end

            ST_ETH_TYPE: begin
                crc_clr     &amp;#x3C;=  1&apos;b0                    ;
                crc_en      &amp;#x3C;=  1&apos;b1                    ;   //  enable crc
                s_axis_trdy &amp;#x3C;=  1&apos;b0                    ;

                case (byte_cnt)
                    0: begin 
                        gmii_txd    &amp;#x3C;= eth_type[15:08]  ; 
                        crc_data_in &amp;#x3C;= eth_type[15:08]  ;
                        s_axis_trdy &amp;#x3C;=  1&apos;b1            ;   //  we can receive user data at next clock period
                    end
                    1: begin 
                        gmii_txd    &amp;#x3C;= eth_type[07:00]  ; 
                        crc_data_in &amp;#x3C;= eth_type[07:00]  ;
                    end
                endcase

                // turn to next state
                if (byte_cnt == 2 - 1) begin
                    s_axis_trdy &amp;#x3C;=  1&apos;b1                ;   //  we can receive user data at next clock period
                    state       &amp;#x3C;= ST_PAYLOAD           ;
                end else begin
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                end
            end

            ST_PAYLOAD: begin                               // axi-stream data in at this time.
                if (s_axis_tdv &amp;#x26;&amp;#x26; s_axis_trdy) begin
                    s_axis_trdy &amp;#x3C;=  1&apos;b1                ;
                    crc_clr     &amp;#x3C;=  1&apos;b0                ;
                    crc_en      &amp;#x3C;=  1&apos;b1                ;

                    gmii_txd    &amp;#x3C;=  s_axis_td           ;
                    crc_data_in &amp;#x3C;=  s_axis_td           ;
                    payload_len &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                end

                // turn to next state
                if (    
                    s_axis_tdv      == 1&apos;b1 &amp;#x26;&amp;#x26; 
                    s_axis_trdy     == 1&apos;b1 &amp;#x26;&amp;#x26; 
                    s_axis_tend     == 1&apos;b1
                ) begin

                    if (payload_len &amp;#x3C; MIN_PAYLOAD) begin
                        s_axis_trdy &amp;#x3C;=  1&apos;b0            ;   //  Stop receive upstream data
                        state       &amp;#x3C;=  ST_PADDING      ;
                        byte_cnt    &amp;#x3C;=  16&apos;d0           ;
                    end else begin
                        s_axis_trdy &amp;#x3C;=  1&apos;b0            ;   //  Stop receive upstream data
                        state       &amp;#x3C;=  ST_CRC          ;
                        byte_cnt    &amp;#x3C;=  16&apos;d0           ;
                    end
                end
            end

            ST_PADDING: begin                               //  if data in is less than MIN_PAYLOAD, padding data
                crc_clr         &amp;#x3C;=  1&apos;b0                ;
                crc_en          &amp;#x3C;=  1&apos;b1                ;
                s_axis_trdy     &amp;#x3C;=  1&apos;b0                ;   //  Stop receive upstream data
                
                // turn to next state
                if (byte_cnt == MIN_PAYLOAD - payload_len) begin
                    state       &amp;#x3C;=  ST_CRC              ;
                    byte_cnt    &amp;#x3C;=  16&apos;d0               ;
                end else begin
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                    gmii_txd    &amp;#x3C;=  PADDING_DATA        ;
                    crc_data_in &amp;#x3C;=  PADDING_DATA        ;
                end
            end

            ST_CRC: begin
                s_axis_trdy     &amp;#x3C;=  1&apos;b0                ;   //  Stop receive upstream data
                crc_en          &amp;#x3C;=  1&apos;b0                ;
                case(byte_cnt)
                    0: gmii_txd &amp;#x3C;=  crc_next_inv[07:00] ;
                    1: gmii_txd &amp;#x3C;=  crc_inv[15:08]      ;
                    2: gmii_txd &amp;#x3C;=  crc_inv[23:16]      ;
                    3: gmii_txd &amp;#x3C;=  crc_inv[31:24]      ;
                endcase
                if (byte_cnt == 4 - 1) begin
                    state       &amp;#x3C;=  ST_IFG              ;
                end else begin
                    byte_cnt    &amp;#x3C;=  byte_cnt + 1&apos;b1     ;
                end
            end

            ST_IFG: begin
                ifg_cnt     &amp;#x3C;=  ifg_cnt + 1&apos;b1          ;
                crc_clr     &amp;#x3C;=  1&apos;b1                    ;
                gmii_tx_en  &amp;#x3C;=  1&apos;b0                    ;
                if (ifg_cnt &gt;= (IFG_CNT - 1)) begin
                    state   &amp;#x3C;=  ST_IDLE                 ;
                end
            end

            default: state  &amp;#x3C;= ST_IDLE;
        endcase
    end
end

endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;以太网帧解包模块&quot;&gt;以太网帧解包模块&lt;/h3&gt;
&lt;p&gt;该模块负责将以太网帧进行解包，从中分离出负载数据供下游模块使用。&lt;/p&gt;
&lt;p&gt;模块工作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;模块接收来自网络中的以太网帧，当接收到以太网帧的前导码时，模块开始工作，拉高 &lt;code&gt;module_busy&lt;/code&gt; 信号。&lt;/li&gt;
&lt;li&gt;之后就开始接收前导码、起始帧界定符、目的 MAC、源 MAC 以及以太网类型。&lt;/li&gt;
&lt;li&gt;接收完毕后比对接收到的目的 MAC 是否与板卡上的 MAC 一致，若一致则进入接收负载数据阶段。否则直接丢弃这个数据包。&lt;/li&gt;
&lt;li&gt;接收负载阶段，&lt;code&gt;s_axis_rdv&lt;/code&gt; 信号将会被拉高，直到负载阶段结束。负载阶段结束时 &lt;code&gt;s_axis_rend&lt;/code&gt; 会产生一个时钟周期的高电平脉冲信号。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了能够完整的接收 FCS(帧校验序列)，模块中的做法制是制作一个 4 字节的缓冲区，将来自 &lt;code&gt;gmii_rxd&lt;/code&gt; 的数据延迟 4 个字节发送到 &lt;code&gt;s_axis_rd&lt;/code&gt; 端口上，这样直到 FCS 阶段结束，缓冲区内的 4 字节数据刚好就是接收到的 FCS 数据。&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;// NOTE_HEADER--------------------------------------------------------------------------------------
// HOW TO USE:
//  1. Set s_axis_rrdy to 1&apos;b1, for enable receive data from RGMII interface.
//  2. Wait s_axis_rdv signal goes to 1&apos;b1
//  3. Downstream receive data from s_axis_rd.
//  4. Wait active-high pulse signal from port s_axis_rend  
//  5. Receive finished
// NOTE_FOOTER--------------------------------------------------------------------------------------

module eth_frame_decap (
    input   wire            rst_n           ,   //  [I] [    ] reset signal, active-low

    // module output interface, (it likes AXI-Stream)
    output  reg     [7:0]   s_axis_rd       ,   //  [O] [ 7:0] rx data output
    output  reg             s_axis_rdv      ,   //  [O] [    ] rx data valid
    output  reg             s_axis_rend     ,   //  [O] [    ] the last data output
    input   wire            s_axis_rrdy     ,   //  [I] [    ] downstream ready to receive data

    input   wire    [47:0]  board_mac       ,   //  [I] [47:0] Board MAC address, for comparison with the received destination MAC address
    output  reg     [47:0]  src_mac         ,   //  [O] [47:0] Received source MAC address
    output  reg     [15:0]  eth_type        ,   //  [O] [15:0] Received ethernet type (or data length, but we don&apos;t need this), 
                                                //             when its value is less than 1500(Decimal, 0x05DC in hex), it refers to IEEE 802.3 data length (we don&apos;t need this)
                                                //             when its value is bigger than 1500(Decimal, 0x05DC in hex), it refers to ethernet type
    output  wire            module_busy     ,   //  [O] [    ] module busy signal when receiving data, active-high
    output  reg             rx_frame_valid  ,   //  [O] [    ] active-high if crc validation passed
    output  reg             rx_frame_error  ,   //  [O] [    ] active-high when some error occurred on receive data, or CRC is not passed
    output  reg     [15:0]  rx_payload_len  ,   //  [O] [15:0] Received payload length
    
    // GMII input interface
    input   wire            gmii_rxc        ,   //  [I] [    ] GMII RX Clock
    input   wire            gmii_rx_dv      ,   //  [I] [    ] GMII RX Data valid
    input   wire    [ 7:0]  gmii_rxd            //  [I] [    ] GMII RX data
);

//================================================================================
// Local Parameter Declarations
//================================================================================
localparam  MIN_FRAME_LEN   =   8&apos;d64   ;   //  Minimum frame length in bytes, includes 6 bytes dest_mac, 6 bytes src_mac, 2 bytes type/length, minum 46 bytes data segment and 4 bytes FCS
localparam  CRC_LENGTH      =   8&apos;d4    ;
localparam  HEADER_LENGTH   =   8&apos;d14   ;   //  Ethernet pack header length in bytes, 6 bytes dest_mac, 6 bytes src_mac and 2 bytes type/length
localparam  PREAMBLE_WORD   =   8&apos;h55   ;
localparam  SFD_WORD        =   8&apos;hD5   ;

// states
localparam  ST_IDLE         =   4&apos;d0    ;   //  IDLE state
localparam  ST_PREAMBLE     =   4&apos;d1    ;   //  State of receiving preamble code, 7 bytes of 0x55
localparam  ST_SFD          =   4&apos;d2    ;   //  State of receiving SFD, Start Frame Delimiter, 1 byte of 0xd5
localparam  ST_HEADER       =   4&apos;d3    ;   //  State of receiving header, includes destination MAC, src MAC, and ethertype
localparam  ST_PAYLOAD      =   4&apos;d4    ;   //  State of receiving payload, the data content of this packet
localparam  ST_CRC          =   4&apos;d5    ;   //  State of receiving CRC result
localparam  ST_ERR          =   4&apos;d6    ;   //  State of receiving error, drop frame until end
localparam  ST_DROP         =   4&apos;d7    ;   //  State of drop frame   

//================================================================================
// Register Declarations
//================================================================================
reg     [ 3:0]  state                   ;
reg     [ 7:0]  header_buf      [13:0]  ;   //  Ethernet pack header buffer, includes 6 bytes src MAC address, 6 bytes dst MAC address and 2 bytes ethtype. 
reg     [47:0]  recv_dst_mac            ;   //  Received destination MAC address
reg     [47:0]  recv_src_mac            ;   //  Received source MAC address
reg     [16:0]  recv_eth_type           ;   //  Received ethertype

// crc
reg     [ 7:0]  crc_data_in             ;
reg             crc_en                  ;
reg             crc_clr                 ;

// output ctrl (4-byte delay)
reg     [31:0]  delay_buf               ;
reg     [ 2:0]  delay_cnt               ;
reg     [15:0]  bytes_rec_cnt           ;   //  Total bytes received (Preamble and SFD is not included)
reg             payload_active          ;   //  indicator indicates that payload is received, not an error

//================================================================================
// Wire Declarations
//================================================================================
wire    [31:0]  crc_inv                 ;
wire    [31:0]  crc_next_inv            ;

//================================================================================
// Assign Declarations
//================================================================================
assign  module_busy =   (state == ST_IDLE) ? 1&apos;b0 : 1&apos;b1                    ;

//================================================================================
// implements
//================================================================================
crc32_d8 crc32_d8_inst (
    .clk            (   gmii_rxc        ),  // [I] [    ] Module clock
    .rst_n          (   rst_n           ),  // [I] [    ] Reset signal, active-low
    .data           (   crc_data_in     ),  // [I] [ 7:0] The 8-bit data to be validated
    .crc_en         (   crc_en          ),  // [I] [    ] CRC enable
    .crc_clr        (   crc_clr         ),  // [I] [    ] CRC Result Clear
    .crc_next_inv   (   crc_next_inv    ),  // [O] [31:0] CRC data
    .crc_inv        (   crc_inv         )   // [O] [31:0] Inversed crc data
);

//================================================================================
// MAIN CODE
//================================================================================
always  @(posedge gmii_rxc or negedge rst_n) begin
    if (!rst_n) begin
        state                       &amp;#x3C;=  ST_IDLE                 ;
        rx_payload_len              &amp;#x3C;=  16&apos;d0                   ;
        bytes_rec_cnt               &amp;#x3C;=  16&apos;d0                   ;
        crc_data_in                 &amp;#x3C;=  8&apos;d0                    ;
        crc_en                      &amp;#x3C;=  1&apos;b0                    ;
        crc_clr                     &amp;#x3C;=  1&apos;b1                    ;

        delay_buf                   &amp;#x3C;=  32&apos;d0                   ;
        delay_cnt                   &amp;#x3C;=  3&apos;d0                    ;

        header_buf[00]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[01]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[02]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[03]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[04]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[05]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[06]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[07]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[08]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[09]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[10]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[11]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[12]              &amp;#x3C;=  8&apos;h0                    ;
        header_buf[13]              &amp;#x3C;=  8&apos;h0                    ;

        recv_dst_mac                &amp;#x3C;=  48&apos;h0                   ;
        recv_src_mac                &amp;#x3C;=  48&apos;h0                   ;
        recv_eth_type               &amp;#x3C;=  16&apos;b0                   ;
        src_mac                     &amp;#x3C;=  48&apos;h0                   ;
        eth_type                    &amp;#x3C;=  16&apos;b0                   ;

        payload_active              &amp;#x3C;=  1&apos;b0                    ;
        s_axis_rd                   &amp;#x3C;=  8&apos;b0                    ;
        s_axis_rdv                  &amp;#x3C;=  1&apos;b0                    ;
        s_axis_rend                 &amp;#x3C;=  1&apos;b0                    ;
        rx_frame_valid              &amp;#x3C;=  1&apos;b0                    ;
        rx_frame_error               &amp;#x3C;=  1&apos;b0                    ;
    end else begin
        case (state)
            ST_IDLE: begin
                bytes_rec_cnt       &amp;#x3C;=  16&apos;d0                   ;
                crc_en              &amp;#x3C;=  1&apos;b0                    ;
                delay_cnt           &amp;#x3C;=  3&apos;d0                    ;
                delay_buf           &amp;#x3C;=  32&apos;d0                   ;
                rx_frame_error       &amp;#x3C;=  1&apos;b0                    ;
                s_axis_rdv          &amp;#x3C;=  1&apos;b0                    ;
                s_axis_rend         &amp;#x3C;=  1&apos;b0                    ;
                if (gmii_rx_dv == 1&apos;b1 &amp;#x26;&amp;#x26; gmii_rxd == PREAMBLE_WORD) begin
                    state           &amp;#x3C;=  ST_PREAMBLE             ;
                    bytes_rec_cnt   &amp;#x3C;=  bytes_rec_cnt + 1&apos;b1    ;
                end
            end

            // receive 7 bytes preamble (Standard Ethernet is 7 bytes 0x55)
            ST_PREAMBLE: begin
                if ((gmii_rx_dv == 1&apos;b1) &amp;#x26;&amp;#x26; (gmii_rxd == PREAMBLE_WORD)) begin
                    bytes_rec_cnt   &amp;#x3C;=  bytes_rec_cnt + 1&apos;b1    ;
                    if (bytes_rec_cnt == 8&apos;d7 - 1) begin
                        state       &amp;#x3C;=  ST_SFD                  ;
                    end
                end else begin
                    state           &amp;#x3C;= ST_ERR                   ;
                end

                // ... Preamble does not participate in CRC
            end

            // receive 1 byte SFD
            ST_SFD: begin
                if ((gmii_rx_dv == 1&apos;b1) &amp;#x26;&amp;#x26; (gmii_rxd == SFD_WORD)) begin
                    state                   &amp;#x3C;=  ST_HEADER       ;
                    bytes_rec_cnt           &amp;#x3C;=  16&apos;d0           ;
                    crc_data_in             &amp;#x3C;=  8&apos;d0            ;
                    crc_clr                 &amp;#x3C;=  1&apos;b1            ;
                end else begin
                    state                   &amp;#x3C;=  ST_ERR          ;
                end
                
                // ... SFD does not participate in CRC
            end

            ST_HEADER: begin
                if (gmii_rx_dv == 1&apos;b1) begin
                    delay_buf   &amp;#x3C;=  {delay_buf[23:0], gmii_rxd};

                    if (delay_cnt &amp;#x3C; 4) begin                        //  do 4 bytes delay
                        delay_cnt   &amp;#x3C;=  delay_cnt + 1   ;
                    end else begin
                        crc_en      &amp;#x3C;=  1&apos;b1            ;
                        crc_clr     &amp;#x3C;=  1&apos;b0            ;
                        crc_data_in &amp;#x3C;=  delay_buf[31:24];

                        // receive header
                        header_buf[bytes_rec_cnt]   &amp;#x3C;=  delay_buf[31:24]        ;
                        bytes_rec_cnt               &amp;#x3C;=  bytes_rec_cnt + 1&apos;b1    ;

                        // receive header finish
                        if (bytes_rec_cnt == HEADER_LENGTH - 1) begin
                            recv_dst_mac  &amp;#x3C;= {header_buf[0], header_buf[1], header_buf[2], header_buf[3], header_buf[4], header_buf[5]};
                            recv_src_mac  &amp;#x3C;= {header_buf[6], header_buf[7], header_buf[8], header_buf[9], header_buf[10], header_buf[11]};
                            recv_eth_type &amp;#x3C;= {header_buf[12], delay_buf[31:24]};

                            if(({header_buf[0], header_buf[1], header_buf[2], header_buf[3], header_buf[4], header_buf[5]} == board_mac) || 
                            ({header_buf[0], header_buf[1], header_buf[2], header_buf[3], header_buf[4], header_buf[5]} == 48&apos;hFF_FF_FF_FF_FF_FF)) begin
                                // MAC address is match
                                state           &amp;#x3C;=  ST_PAYLOAD      ;
                                rx_payload_len  &amp;#x3C;=  16&apos;d0           ;
                                bytes_rec_cnt   &amp;#x3C;=  16&apos;d0           ;
                                delay_cnt       &amp;#x3C;=  2&apos;d0            ;

                                // output src MAC and ethertype to upstream
                                src_mac         &amp;#x3C;=  recv_src_mac    ;
                                eth_type        &amp;#x3C;=  {header_buf[12], delay_buf[31:24]};
                            end else begin
                                // MAC address is not match, drop this pack directly.
                                state           &amp;#x3C;=  ST_DROP         ;
                            end
                        end
                    end
                end else begin
                    state                   &amp;#x3C;=  ST_ERR          ;
                end
            end 

            ST_PAYLOAD: begin
                if (gmii_rx_dv) begin
                    bytes_rec_cnt   &amp;#x3C;=  bytes_rec_cnt + 1&apos;b1    ;

                    // crc
                    crc_en          &amp;#x3C;=  1&apos;b1                    ;
                    crc_clr         &amp;#x3C;=  1&apos;b0                    ;
                    delay_buf       &amp;#x3C;=  {delay_buf[23:0], gmii_rxd};
                    crc_data_in     &amp;#x3C;=  delay_buf[31:24]        ;

                    s_axis_rdv  &amp;#x3C;=  1&apos;b1                        ;
                    if (s_axis_rrdy == 1&apos;b1) begin                      //  Dowmstream is ready to receive data
                        s_axis_rd       &amp;#x3C;=  delay_buf[31:24]        ;
                        rx_payload_len  &amp;#x3C;=  rx_payload_len + 1&apos;b1   ;   //  record payload length (FCS is not included)
                        payload_active  &amp;#x3C;=  1&apos;b1                    ;
                    end else begin
                        state       &amp;#x3C;=  ST_DROP             ;   //  Downstream is not ready to receive data, drop this frame
                    end
                end else begin
                    s_axis_rdv      &amp;#x3C;=  1&apos;b0                ;
                    s_axis_rd       &amp;#x3C;=  8&apos;b0                ;
                    s_axis_rend     &amp;#x3C;=  1&apos;b1                ; // send end pulse
                    // gmii_rx_dv goes to 1&apos;b0, frame end, mark s_axis_rend sig
                    if (payload_active == 1&apos;b1) begin
                        payload_active  &amp;#x3C;=  1&apos;b0                ;
                        state           &amp;#x3C;=  ST_CRC              ;
                    end
                end
            end

            ST_CRC: begin
                crc_en          &amp;#x3C;=  1&apos;b0                ;
                s_axis_rend     &amp;#x3C;=  1&apos;b0                ;
                if (crc_inv == delay_buf) begin
                    rx_frame_valid  &amp;#x3C;=  1&apos;b1            ;
                    state           &amp;#x3C;=  ST_IDLE         ;
                end else begin
                    rx_frame_valid  &amp;#x3C;=  1&apos;b0            ;
                    state           &amp;#x3C;=  ST_ERR          ;
                end
            end

            ST_ERR: begin      
                rx_frame_error  &amp;#x3C;=  1&apos;b1                ;
                state           &amp;#x3C;=  ST_DROP             ;
            end

            ST_DROP: begin
                if(gmii_rx_dv == 1&apos;b0) begin                //  Drop this frame until frame end.
                    state   &amp;#x3C;=  ST_IDLE                ;
                end
            end
        endcase
    end
end
    
endmodule&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;以太网帧模块&quot;&gt;以太网帧模块&lt;/h3&gt;
&lt;p&gt;这个模块的作用是将两个模块进行整合封装到一个顶层，方便后续的例化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module eth_frame # (
    parameter   USE_DEBUG   =   1&apos;b0
) (
    input   wire            rst_n           ,

    input   wire    [47:00] board_mac       ,   //  [I] [47:0] Board MAC address, for comparison with the received destination MAC address
    input   wire    [47:00] dst_mac         ,   //  [I] [47:0] Destination MAC address to be send

    // frame decap AXI-Stream output
    output  wire    [07:00] s_axis_rd       ,   //  [O] [ 7:0] rx data output
    output  wire            s_axis_rdv      ,   //  [O] [    ] rx data valid
    output  wire            s_axis_rend     ,   //  [O] [    ] the last data output
    input   wire            s_axis_rrdy     ,   //  [I] [    ] downstream ready to receive data

    output  wire    [15:00] rx_eth_type     ,   //  [O] [15:0] Received ethernet type
    output  wire            decap_busy      ,   //  [O] [    ] decapsulation busy signal when receiving data, active-high
    output  wire            rx_frame_valid  ,   //  [O] [    ] active-high if crc validation passed
    output  wire            rx_frame_error  ,   //  [O] [    ] active-high when some error occurred on receive data, or CRC is not passed
    output  wire    [15:0]  rx_payload_len  ,   //  [O] [15:0] Received payload length

    // gmii rx interface
    input   wire            gmii_rxc        ,   //  [I] [    ] GMII RX Clock
    input   wire            gmii_rx_dv      ,   //  [I] [    ] GMII RX Data valid
    input   wire    [07:00] gmii_rxd        ,   //  [I] [07:0] GMII RX data

    // frame encap AXI-Stream input
    input   wire    [07:00] s_axis_td       ,   //  [I] [ 7:0] tx data input
    input   wire            s_axis_tdv      ,   //  [I] [    ] tx data valid
    input   wire            s_axis_tend     ,   //  [I] [    ] the last data input,
    output  wire            s_axis_trdy     ,   //  [O] [    ] tx data ready signal, high logic when module can send data
    
    input   wire    [15:00] tx_eth_type     ,   //  [I] [15:0] Ethernet type
    output  wire            encap_busy      ,   //  [O] [    ] encapsulation busy signal when sending data, active-high

    // gmii TX interface
    input   wire            gmii_txc        ,   //  [I] [    ] clock input
    output  wire    [07:00] gmii_txd        ,   //  [O] [07:0]
    output  wire            gmii_tx_en          //  [O] [    ]  
); 

eth_frame_decap eth_frame_decap_inst (
    .rst_n  (rst_n),

    // AXI-Stream output
    .s_axis_rd      (   s_axis_rd       ),  //  [O] [ 7:0] rx data output
    .s_axis_rdv     (   s_axis_rdv      ),  //  [O] [    ] rx data valid
    .s_axis_rend    (   s_axis_rend     ),  //  [O] [    ] the last data output
    .s_axis_rrdy    (   s_axis_rrdy     ),  //  [I] [    ] downstream ready to receive data

    .board_mac      (   board_mac       ),  //  [I] [47:0] Board MAC address, for comparison with the received destination MAC address
    .src_mac        (                   ),  //  [O] [47:0] Received source MAC address, unused, 
    .eth_type       (   rx_eth_type     ),  //  [O] [15:0] Received ethernet type
    .module_busy    (   decap_busy      ),  //  [O] [    ] module busy signal when receiving data, active-high
    .rx_frame_valid (   rx_frame_valid  ),  //  [O] [    ] active-high if crc validation passed
    .rx_frame_error (   rx_frame_error  ),  //  [O] [    ] active-high when some error occurred on receive data, or CRC is not passed
    .rx_payload_len (   rx_payload_len  ),  //  [O] [15:0] Received payload length

    .gmii_rxc       (   gmii_rxc        ),  //  [I] [    ] GMII RX Clock
    .gmii_rx_dv     (   gmii_rx_dv      ),  //  [I] [    ] GMII RX Data valid
    .gmii_rxd       (   gmii_rxd        )   //  [I] [07:0] GMII RX data
);

eth_frame_encap # (
    .USE_DEBUG      (   USE_DEBUG       )
) eth_frame_encap_inst(
    .rst_n          (   rst_n           ),  //  [I] [    ] reset signal, active-low

    // user input interface, (AXI-Stream)
    .s_axis_td      (   s_axis_td       ),  //  [I] [ 7:0] tx data input
    .s_axis_tdv     (   s_axis_tdv      ),  //  [I] [    ] tx data valid
    .s_axis_tend    (   s_axis_tend     ),  //  [I] [    ] the last data input,
    .s_axis_trdy    (   s_axis_trdy     ),  //  [O] [    ] tx data ready signal, high logic when module can send data

    .src_mac        (   board_mac       ),  //  [I] [47:0] Source MAC address to be send
    .dst_mac        (   dst_mac         ),  //  [I] [47:0] Destination MAC address to be send
    .eth_type       (   tx_eth_type     ),  //  [I] [15:0] Ethernet type
    .module_busy    (   encap_busy      ),  //  [O] [    ] module busy signal when sending data, active-high
    
    // GMII interface
    .gmii_txc       (   gmii_txc        ),  //  [I] [    ] clock input
    .gmii_txd       (   gmii_txd        ),  //  [O] [07:0]
    .gmii_tx_en     (   gmii_tx_en      )   //  [O] [    ]
);&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/db8d0357</id>,<title>ETH-02 RGMII 接口和 GMII 接口的互相转换</title>,<link href="https://blog.tyh123.top/posts/db8d0357" rel="alternate" type="text/html"/>,<published>2026-04-14T14:26:09.000Z</published>,<updated>2026-04-14T14:26:09.000Z</updated>,<summary type="text">RGMII 采用双边沿传输数据, FPGA 只采上升沿会丢数据。本文说明如何用 Xilinx IDDR 与 ODDR 原语完成 RGMII 与 GMII 之间的互相转换。</summary>,<content type="html">&lt;p&gt;在 ${post 4a0bb95d} 中我讲到了 RGMII 接口和 GMII 的两种数据接口。本篇则围绕这两种数据接口的互相转换展开。&lt;/p&gt;
&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;在 FPGA 中，数据通常只在时钟的上升沿被采集。对于采用双边沿数据有效的 RGMII 接口而言，若仅使用上升沿进行采样，就会丢失下降沿所对应的数据&lt;/strong&gt;。同理，FPGA 在输出数据时，也只能在上升沿发生变化。为解决这一问题，Xilinx 提供了 &lt;code&gt;IDDR&lt;/code&gt; 和 &lt;code&gt;ODDR&lt;/code&gt; 两个原语，分别用于接收和发送双边沿数据。&lt;/p&gt;

&lt;p&gt;下方所有配图均来自 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fdocs.amd.com%2Fv%2Fu%2Fen-US%2Fug471_7Series_SelectIO&quot;&gt;[7Series FPGAs SelectIO Resources User Guide (UG471)]&lt;/a&gt;, 你可以打开 DOCNav 来搜索对应的文档名称&lt;/p&gt;

&lt;h2 id=&quot;iddr-input-double-data-rate&quot;&gt;IDDR (Input Double Data Rate)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;IDDR&lt;/code&gt; 的作用是将 RGMII 接口中的&lt;strong&gt;双边沿数据&lt;/strong&gt; (DDR) 转换为 FPGA 内部可以处理的&lt;strong&gt;单边沿数据&lt;/strong&gt;，&lt;code&gt;IDDR&lt;/code&gt; 将双边沿变化的数据通过寄存器输出到了 Q1/Q2 两个端口上，分别代表上升沿的数据和下降沿的数据。&lt;/p&gt;
&lt;p&gt;下面是 IDDR 原语的语言模板&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IDDR #(
  .DDR_CLK_EDGE (   &quot;OPPOSITE_EDGE&quot; ), // 数据对齐参数，有 &quot;OPPOSITE_EDGE&quot;, &quot;SAME_EDGE&quot; or &quot;SAME_EDGE_PIPELINED&quot; 
  .INIT_Q1      (   1&apos;b0            ), // Q1 的初始值: 1&apos;b0 or 1&apos;b1
  .INIT_Q2      (   1&apos;b0            ), // Q2 的初始值: 1&apos;b0 or 1&apos;b1
  .SRTYPE       (   &quot;SYNC&quot;          ) // Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
) IDDR_inst (
  .Q1   (   Q1  ),  // 1-bit 时钟上升沿输出的值
  .Q2   (   Q2  ),  // 1-bit 时钟下降沿输出的值
  .C    (   C   ),  // 1-bit 时钟输入
  .CE   (   CE  ),  // 1-bit 时钟使能信号
  .D    (   D   ),  // 1-bit DDR 信号输入
  .R    (   R   ),  // 1-bit 复位信号
  .S    (   S   )   // 1-bit 置位信号，优先级低于复位信号
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;参数介绍&quot;&gt;参数介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DDR_CLK_EDGE&lt;/strong&gt;：数据对齐方式，该参数将在下文介绍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INIT_Q1/Q2&lt;/strong&gt;：初始化 Q1、Q2 的初始值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SRTYPE&lt;/strong&gt;：配置复位/置位信号为同步/异步。建议是使用同步复位驱动 R/S，以避免亚稳态的产生。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;端口介绍&quot;&gt;端口介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Q1/Q2&lt;/strong&gt;：数据的输出端口，Q1 输出时钟上升沿的数据，Q2 输出时钟下降沿的数据。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;：时钟的输入端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CE&lt;/strong&gt;：时钟的使能信号，高电平有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;D&lt;/strong&gt;：DDR 信号的输入端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R/S&lt;/strong&gt;：复位/置位信号，只能使用其中之一 (置1)，或全部不使用 (置 0)。复位信号使得 Q1/Q2 输出 0，置位信号使得 Q1/Q2 输出 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;iddr-数据对齐方式&quot;&gt;IDDR 数据对齐方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OPPOSITE_EDGE&lt;/strong&gt;：数据对齐的基本模式，该模式实现了基本的 DDR 功能，&lt;strong&gt;但是输出的数据 Q1/Q2 在单个时钟周期内不能同时有效&lt;/strong&gt;，对逻辑设计不友好，已经较少使用。下图为该模式下的时序图例。&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;IDDR_CLK_EDGE_OPPOSITE_EDGE&quot; width=&quot;1287&quot; height=&quot;541&quot; src=&quot;https://blog.tyh123.top/_astro/IDDR_CLK_EDGE_OPPOSITE_EDGE.BvBqiiY7_27JcFF.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAME_EDGE&lt;/strong&gt;：数据在同一个时钟沿提供给内部逻辑，能有效避免时序冲突。缺点是&lt;strong&gt;两个数据之间有一个时钟周期的延迟差异&lt;/strong&gt;，在逻辑上需要处理 “错位”。下图为该模式下的时序图例。&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;IDDR_CLK_EDGE_SAME_EDGE&quot; width=&quot;1338&quot; height=&quot;490&quot; src=&quot;https://blog.tyh123.top/_astro/IDDR_CLK_EDGE_SAME_EDGE.Db-v3CZN_Z1P62Jm.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAME_EDGE_PIPELINED&lt;/strong&gt;：完美的数据对齐，两个输出 Q1/Q2 在&lt;strong&gt;相同的钟沿同时有效&lt;/strong&gt;。代价则是引入了额外的延时，通常是 2 个时钟周期。下图为该模式下的时序图例。&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;IDDR_CLK_EDGE_SAME_EDGE_PIPELINED&quot; width=&quot;1319&quot; height=&quot;534&quot; src=&quot;https://blog.tyh123.top/_astro/IDDR_CLK_EDGE_SAME_EDGE_PIPELINED.DrVkaZHO_1ihjHE.webp&quot;&gt;
通过时序图我们可以看到 &lt;code&gt;IDDR&lt;/code&gt; 原语在 &lt;code&gt;SAME_EDGE_PIPELINED&lt;/code&gt; 对齐模式下满足了我们的数据对齐要求，所以在这里我们使用 &lt;code&gt;SAME_EDGE_PIPELINED&lt;/code&gt; 对齐方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;oddr-output-double-data-rate&quot;&gt;ODDR (Output Double Data Rate)&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ODDR&lt;/code&gt; 的作用正好与 &lt;code&gt;IDDR&lt;/code&gt; 是相反的，它通过在&lt;strong&gt;时钟的上升沿更新 &lt;code&gt;Q1/Q2&lt;/code&gt; 的值&lt;/strong&gt;，之后输出双边沿变化的 DDR 信号。&lt;/p&gt;
&lt;p&gt;下面是 ODDR 的语言模板&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ODDR #(
  .DDR_CLK_EDGE (&quot;OPPOSITE_EDGE&quot;),  // &quot;OPPOSITE_EDGE&quot; or &quot;SAME_EDGE&quot; 
  .INIT         (1&apos;b0           ),  // Initial value of Q: 1&apos;b0 or 1&apos;b1
  .SRTYPE       (&quot;SYNC&quot;         )   // Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
) ODDR_inst (
  .Q            (   Q           ),  // 1-bit DDR output
  .C            (   C           ),  // 1-bit clock input
  .CE           (   CE          ),  // 1-bit clock enable input
  .D1           (   D1          ),  // 1-bit data input (positive edge)
  .D2           (   D2          ),  // 1-bit data input (negative edge)
  .R            (   R           ),  // 1-bit reset
  .S            (   S           )   // 1-bit set
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;参数介绍-1&quot;&gt;参数介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DDR_CLK_EDGE&lt;/strong&gt;：数据对齐方式，该参数将在下文介绍。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;INIT&lt;/strong&gt;：初始化端口 Q 的初始值。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SRTYPE&lt;/strong&gt;：配置复位/置位信号为同步/异步。建议是使用同步复位驱动 R/S，以避免亚稳态的产生。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;端口介绍-1&quot;&gt;端口介绍&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;D1/D2&lt;/strong&gt;：数据的输入端口，D1 将会被输出到时钟的上升沿，D2 将会被输出到时钟的下降沿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C&lt;/strong&gt;：时钟的输入端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CE&lt;/strong&gt;：时钟的使能信号，高电平有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Q&lt;/strong&gt;：DDR 信号的输出端口。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;R/S&lt;/strong&gt;：复位/置位信号，只能使用其中之一 (置1)，或全部不使用 (置 0)。复位信号使得 Q1/Q2 输出置 0，置位信号使得 Q1/Q2 输出置 1。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;oddr-数据对齐方式&quot;&gt;ODDR 数据对齐方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OPPOSITE_EDGE&lt;/strong&gt;：在该模式下时钟的两个边沿都用于从 FPGA 逻辑捕获数据，从而实现两倍数据的吞吐量。这种方式要求内部逻辑&lt;strong&gt;必须提供两个数据流&lt;/strong&gt;：D1 数据流&lt;strong&gt;与上升沿对齐&lt;/strong&gt;，D2 数据流&lt;strong&gt;与下降沿对齐&lt;/strong&gt;, (或提前半个周期来保持数据的稳定)。该方式通常要求使用下降沿的触发器来驱动，容易导致时序违例。该模式的时序图例如下。&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;ODDR_CLK_EDGE_OPPOSITE_EDGE&quot; width=&quot;948&quot; height=&quot;541&quot; src=&quot;https://blog.tyh123.top/_astro/ODDR_CLK_EDGE_OPPOSITE_EDGE.KUlO-quN_Z4IiAH.webp&quot;&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SAME_EDGE&lt;/strong&gt;：在该模式下，D1/D2 都可以由上升沿触发的逻辑产生，DDR 内部会自动把 D2 对齐到下降沿进行输出，从而避免了跨边沿的约束，时序宽松，设计更加简单。该模式的图例如下。&lt;/li&gt;
&lt;li&gt;&lt;img alt=&quot;ODDR_CLK_EDGE_SAME_EDGE&quot; width=&quot;950&quot; height=&quot;534&quot; src=&quot;https://blog.tyh123.top/_astro/ODDR_CLK_EDGE_SAME_EDGE.DrJLOk7i_Z2pMey3.webp&quot;&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;tx_ctrl-的转换&quot;&gt;TX_CTRL 的转换&lt;/h2&gt;
&lt;p&gt;在 RGMII 接口中，&lt;code&gt;TX_CTL&lt;/code&gt;是一个集成了 GMII 接口中 &lt;code&gt;TX_EN&lt;/code&gt;(发送使能) 和 &lt;code&gt;TX_ER&lt;/code&gt;(发送错误) 功能的复合信号，通过将这两个信号编码在了同一个时钟周期内，传递了两个控制信息。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;TX_CTL&lt;/code&gt; 采用的同样也是&lt;strong&gt;双边沿采样 (DDR)&lt;/strong&gt;，配合发送时钟 &lt;code&gt;TXC&lt;/code&gt; 一起传输。它的编码规则如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;上升沿&lt;/strong&gt;：传输 &lt;code&gt;TX_EN&lt;/code&gt; 信号，表示该时钟周期内的数据是否有效。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;下降沿&lt;/strong&gt;：传输 &lt;code&gt;TX_EN ^ TX_ER&lt;/code&gt;，即 &lt;code&gt;TX_EN ^ TX_ER&lt;/code&gt; 的&lt;strong&gt;异或 (XOR) 结果&lt;/strong&gt;。
这种编码方式定义了四种明确的状态：&lt;/li&gt;
&lt;/ul&gt;













































&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;组合场景&lt;/th&gt;&lt;th&gt;GMII: TX_EN&lt;/th&gt;&lt;th&gt;GMII_TX_ER&lt;/th&gt;&lt;th&gt;RGMII: TX_CTL&lt;br&gt;(上升沿)&lt;/th&gt;&lt;th&gt;RGMII: TX_CTL&lt;br&gt;(下降沿)&lt;/th&gt;&lt;th&gt;状态描述&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;正常帧间隙&lt;br&gt;(空闲)&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;空闲状态。链路无数据传输，&lt;code&gt;TX_CTL&lt;/code&gt;保持低电平&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;正常帧传输&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;&lt;strong&gt;正常数据传输&lt;/strong&gt;。表示当前时钟周期的 8 位数据全部有效&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;&lt;strong&gt;数据错误&lt;/strong&gt;&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;&lt;strong&gt;发送错误&lt;/strong&gt;。指示当前传输帧中存在错误，通常由MAC层发起&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;保留/异常&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;0&lt;/td&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;无效的组合，实际过程中用不到&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;

&lt;p&gt;在标准操作中，大多数应用只需要关注&lt;code&gt;正常帧传输&lt;/code&gt;（上升沿=1，下降沿=1）和&lt;code&gt;数据错误&lt;/code&gt;（上升沿=1，下降沿=0）这两种情况。&lt;/p&gt;

&lt;p&gt;关于 &lt;code&gt;TX_CTL&lt;/code&gt; 信号的产生方式，代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ODDR #(
    .DDR_CLK_EDGE   (   &quot;SAME_EDGE&quot;     ),  //  &quot;OPPOSITE_EDGE&quot; or &quot;SAME_EDGE&quot; 
    .INIT           (   1&apos;b0            ),  //  Initial value of Q: 1&apos;b0 or 1&apos;b1
    .SRTYPE         (   &quot;SYNC&quot;          )   //  Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
) rgmii_tdv_oddr_inst (
    .Q              (   rgmii_tx_ctl    ),  //  1-bit DDR output
    .C              (   gmii_txc        ),  //  1-bit clock input
    .CE             (   1&apos;b1            ),  //  1-bit clock enable input
    .D1             (   gmii_tx_en      ),  //  1-bit data input (positive edge)
    .D2             (   gmii_tx_en      ),  //  1-bit data input (negative edge) 实际上发送时 tx_ctl 信号一直都是拉高的，所以下降沿也接到了 gmii_tx_en 信号上。
    .R              (   1&apos;b0            ),  //  1-bit reset
    .S              (   1&apos;b0            )   //  1-bit set
);&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id=&quot;rgmii-接口与-gmii-接口互相转换的代码实现&quot;&gt;RGMII 接口与 GMII 接口互相转换的代码实现&lt;/h2&gt;
&lt;h3 id=&quot;gmii-转-rgmii&quot;&gt;GMII 转 RGMII&lt;/h3&gt;
&lt;p&gt;首先来实现 GMII 转 RGMII 接口，这样 FPGA 侧的逻辑就可以通过 GMII 接口发送，再由该模块转换成 RGMII 接口的数据传递到 PHY 芯片了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module rgmii_tx (
    // GMII
    input   wire            gmii_txc        ,   //  [I] [   ] gmii rx clock
    input   wire            gmii_tx_en      ,   //  [I] [   ] gmii rx control
    input   wire    [7:0]   gmii_txd        ,   //  [I] [7:0] gmii rx data

    // RGMII
    output  wire            rgmii_txc       ,   //  [O] [   ] rgmii rx clock
    output  wire            rgmii_tx_ctl    ,   //  [O] [   ] rgmii rx control
    output  wire    [3:0]   rgmii_txd           //  [O] [3:0] rgmii rx data
);

// RGMII 接口 tx_ctl 信号的产生
ODDR #(
    .DDR_CLK_EDGE   (   &quot;SAME_EDGE&quot;     ),  //  &quot;OPPOSITE_EDGE&quot; or &quot;SAME_EDGE&quot; 
    .INIT           (   1&apos;b0            ),  //  Initial value of Q: 1&apos;b0 or 1&apos;b1
    .SRTYPE         (   &quot;SYNC&quot;          )   //  Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
) rgmii_tdv_oddr_inst (
    .Q              (   rgmii_tx_ctl    ),  //  1-bit DDR output
    .C              (   gmii_txc        ),  //  1-bit clock input
    .CE             (   1&apos;b1            ),  //  1-bit clock enable input
    .D1             (   gmii_tx_en      ),  //  1-bit data input (positive edge)
    .D2             (   gmii_tx_en      ),  //  1-bit data input (negative edge)
    .R              (   1&apos;b0            ),  //  1-bit reset
    .S              (   1&apos;b0            )   //  1-bit set
);

// generate 语句为每一条数据线都生成 ODDR 原语
genvar i;
generate 
    for (i=0; i&amp;#x3C;4; i=i+1) 
    begin: txd_bus
        ODDR #(
            .DDR_CLK_EDGE  (    &quot;SAME_EDGE&quot;     ),  //  &quot;OPPOSITE_EDGE&quot; or &quot;SAME_EDGE&quot; 
            .INIT          (    1&apos;b0            ),  //  Initial value of Q: 1&apos;b0 or 1&apos;b1
            .SRTYPE        (    &quot;SYNC&quot;          )   //  Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
        ) rgmii_txd_oddr_inst (
            .Q             (    rgmii_txd[i]    ),  //  1-bit DDR output
            .C             (    gmii_txc        ),  //  1-bit clock input
            .CE            (    1&apos;b1            ),  //  1-bit clock enable input
            .D1            (    gmii_txd[i  ]   ),  //  1-bit data input (positive edge)
            .D2            (    gmii_txd[i+4]   ),  //  1-bit data input (negative edge) 在 RGMII 接口的要求中，时钟的上升沿发送高 4 位，下降沿发送低 4 位。
            .R             (    1&apos;b0            ),  //  1-bit reset
            .S             (    1&apos;b0            )   //  1-bit set
        );   
    end
endgenerate

// 使用 ODDR 原语生成 rgmii_txc 来匹配延迟
ODDR #(
    .DDR_CLK_EDGE   (   &quot;SAME_EDGE&quot;     ),  //  &quot;OPPOSITE_EDGE&quot; or &quot;SAME_EDGE&quot; 
    .INIT           (   1&apos;b0            ),  //  Initial value of Q: 1&apos;b0 or 1&apos;b1
    .SRTYPE         (   &quot;SYNC&quot;          )   //  Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
) rgmii_clk_oddr_inst (
    .Q              (   rgmii_txc       ),  //  1-bit DDR output
    .C              (   gmii_txc        ),  //  1-bit clock input
    .CE             (   1&apos;b1            ),  //  1-bit clock enable input
    .D1             (   1&apos;b1            ),  //  1-bit data input (positive edge)
    .D2             (   1&apos;b0            ),  //  1-bit data input (negative edge)
    .R              (   1&apos;b0            ),  //  1-bit reset
    .S              (   1&apos;b0            )   //  1-bit set
);&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;rgmii-转-gmii&quot;&gt;RGMII 转 GMII&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;module rgmii_rx (
    // RGMII
    input   wire            rgmii_rxc       ,   //  [I] [   ] rgmii rx clock
    input   wire            rgmii_rx_ctl    ,   //  [I] [   ] rgmii rx control
    input   wire    [3:0]   rgmii_rxd       ,   //  [I] [3:0] rgmii rx data

    // GMII
    output  wire            gmii_rxc        ,   //  [O] [   ] gmii rx clock
    output  wire            gmii_rx_dv      ,   //  [O] [   ] gmii rx data valid
    output  wire    [7:0]   gmii_rxd            //  [O] [7:0] gmii rx data
);

wire            rgmii_rxc_bufio     ;
wire            rgmii_rxc_bufg      ;
wire    [1:0]   gmii_rx_dv_t        ;
assign gmii_rxc     = rgmii_rxc_bufg                    ;
assign gmii_rx_dv   = gmii_rx_dv_t[0] &amp;#x26; gmii_rx_dv_t[1] ;

BUFG BUFG_inst (
    .I              (   rgmii_rxc               ),
    .O              (   rgmii_rxc_bufg          )
);

BUFIO BUFIO_inst (
    .I              (   rgmii_rxc               ),
    .O              (   rgmii_rxc_bufio         )
);

genvar i;
generate
    for (i = 0; i &amp;#x3C; 4; i = i+1)
    begin: rxd_bus
        // input delay
        IDDR #(
            .DDR_CLK_EDGE   (   &quot;SAME_EDGE_PIPELINED&quot;   ),  //  &quot;OPPOSITE_EDGE&quot;, &quot;SAME_EDGE&quot; or &quot;SAME_EDGE_PIPELINED&quot; 
            .INIT_Q1        (   1&apos;b0                    ),  //  Initial value of Q1: 1&apos;b0 or 1&apos;b1
            .INIT_Q2        (   1&apos;b0                    ),  //  Initial value of Q2: 1&apos;b0 or 1&apos;b1
            .SRTYPE         (   &quot;SYNC&quot;                  )   //  Set/Reset type: &quot;SYNC&quot; or &quot;ASYNC&quot; 
        ) gmii_rxd_iddr (
            .Q1             (   gmii_rxd[i  ]           ),  // [O] 1-bit output for positive edge of clock
            .Q2             (   gmii_rxd[i+4]           ),  // [O] 1-bit output for negative edge of clock
            .C              (   rgmii_rxc_bufio         ),  // [I] 1-bit clock input
            .CE             (   1&apos;b1                    ),  // [I] 1-bit clock enable input
            .D              (   rgmii_rxd[i]            ),  // [I] 1-bit DDR data input
            .R              (   1&apos;b0                    ),  // [I] 1-bit reset
            .S              (   1&apos;b0                    )   // [I] 1-bit set
        );
    end
endgenerate&lt;/code&gt;&lt;/pre&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/4a0bb95d</id>,<title>ETH-01 初识以太网的硬件组成</title>,<link href="https://blog.tyh123.top/posts/4a0bb95d" rel="alternate" type="text/html"/>,<published>2026-04-13T15:14:06.000Z</published>,<updated>2026-04-13T15:14:06.000Z</updated>,<summary type="text">从 MAC 与 PHY 的分工讲起, 梳理以太网通信的硬件组成, 并对比 GMII 与 RGMII 接口的引脚数量、时钟频率和适用场景。</summary>,<content type="html">&lt;h2 id=&quot;以太网通信的硬件组成&quot;&gt;以太网通信的硬件组成&lt;/h2&gt;
&lt;p&gt;下面是一张硬件组成图例
&lt;img alt=&quot;以太网通信的硬件组成&quot; width=&quot;897&quot; height=&quot;279&quot; src=&quot;https://blog.tyh123.top/_astro/ethernet_hardware_components.DI6vd7rZ_Z27I8cj.webp&quot;&gt;
主要由 &lt;strong&gt;MAC (Media Access Control，媒体访问控制器)&lt;/strong&gt; 和 &lt;strong&gt;PHY(Physical Layer，物理层接口)&lt;/strong&gt; 两大部分组成。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MAC&lt;/strong&gt;：通常由嵌入式设备（如单片机、FPGA 等）实现，负责数据的打包、寻址、CRC 校验等逻辑控制功能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PHY&lt;/strong&gt;：一般作为一颗&lt;strong&gt;独立的板载芯片&lt;/strong&gt;，负责网络自协商、数据编解码等物理层相关工作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通信设备之间通过 RJ45 接口进行连接。需要注意的是，&lt;strong&gt;接口本身不具备通信功能&lt;/strong&gt;，仅起到连接信号的作用。&lt;/p&gt;
&lt;h2 id=&quot;rgmii-和-gmii-接口&quot;&gt;RGMII 和 GMII 接口&lt;/h2&gt;
&lt;p&gt;RGMII 和 GMII 都是连接 MAC 层与 PHY 层的芯片间接口，主要区别在于引脚数量和传输效率。&lt;/p&gt;
&lt;h3 id=&quot;gmii-gigabit-media-independent-interface&quot;&gt;GMII (Gigabit Media Independent Interface)&lt;/h3&gt;
&lt;p&gt;GMII 是千兆以太网接口的完整实现，也是早期 MII 接口的升级版（MII 最高支持 100Mbps）。
&lt;img alt=&quot;GMII 接口&quot; width=&quot;823&quot; height=&quot;550&quot; src=&quot;https://blog.tyh123.top/_astro/gmii_interface.CVTv6O9A_Z1lixgs.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据宽度&lt;/strong&gt;：8 位（发送和接收各 8 条数据线）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟频率&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;1000 Mbps 模式：125 MHz&lt;/li&gt;
&lt;li&gt;100 Mbps 模式：25 MHz&lt;/li&gt;
&lt;li&gt;10 Mbps 模式：2.5 MHz&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键信号&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TXD[7:0]&lt;/code&gt;（发送数据）、&lt;code&gt;RXD[7:0]&lt;/code&gt;（接收数据）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TX_CLK&lt;/code&gt; / &lt;code&gt;RX_CLK&lt;/code&gt;（时钟，由 PHY 提供或从数据中恢复）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TX_EN&lt;/code&gt;（发送使能）、&lt;code&gt;RX_DV&lt;/code&gt;（接收数据有效）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TX_ER&lt;/code&gt; / &lt;code&gt;RX_ER&lt;/code&gt;（错误指示）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CRS&lt;/code&gt;（载波侦听）、&lt;code&gt;COL&lt;/code&gt;（冲突检测，仅半双工用）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：时序简单，兼容性好，支持全双工和半双工。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：引脚数量多，约 &lt;strong&gt;24 根&lt;/strong&gt;，PCB 布线复杂，成本高。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：早期千兆交换芯片、FPGA 开发板、对时序要求极高的场合。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：GMII 的发送时钟 &lt;code&gt;TX_CLK&lt;/code&gt; 可能来自 MAC，也可能来自 PHY（取决于模式），这一点与 MII 略有差异。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;rgmii-reduced-gmii&quot;&gt;RGMII (Reduced GMII)&lt;/h3&gt;
&lt;p&gt;RGMII 是为了解决 GMII 引脚过多问题而设计的&lt;strong&gt;精简版&lt;/strong&gt;接口，是目前大多数 SoC、交换芯片和 PHY 芯片的首选。它采用的是 &lt;strong&gt;DDR(Double Data Rate)&lt;/strong&gt; 模式传输的数据，即时钟的上升沿和下降沿均为有效数据。
&lt;img alt=&quot;RGMII 接口&quot; width=&quot;1083&quot; height=&quot;724&quot; src=&quot;https://blog.tyh123.top/_astro/rgmii_interface.DawD1_A8_Z1ewII1.webp&quot;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;数据宽度&lt;/strong&gt;：4 位（发送和接收各 4 条数据线）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;时钟频率&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;1000 Mbps 模式：125 MHz（双沿采样，等效 250 Mbps per pin）&lt;/li&gt;
&lt;li&gt;100 Mbps 模式：25 MHz（双沿采样，等效 50 Mbps per pin）&lt;/li&gt;
&lt;li&gt;10 Mbps 模式：2.5 MHz（双沿采样，等效 5 Mbps per pin）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;核心技术&lt;/strong&gt;：&lt;strong&gt;DDR(Double Data Rate)——时钟的上升沿和下降沿均传输数据。其中上升沿传递数据的高 4 位，下降沿传递数据的低 4 位&lt;/strong&gt;。所以 4 位数据线 x 双边沿 x 125MHz = 1000Mbps。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;关键信号&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TXD[3:0]&lt;/code&gt;（发送数据）、&lt;code&gt;RXD[3:0]&lt;/code&gt;（接收数据）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TX_CLK&lt;/code&gt;（125MHz 时钟，由 MAC 或 PHY 提供）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RX_CLK&lt;/code&gt;（125MHz 时钟，由 PHY 恢复）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TX_CTL&lt;/code&gt;（发送控制，上升沿 = TX_EN，下降沿 = TX_ER 或其它编码）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RX_CTL&lt;/code&gt;（接收控制，上升沿 = RX_DV，下降沿 = RX_ER）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;优点&lt;/strong&gt;：引脚数少，约 &lt;strong&gt;12 根&lt;/strong&gt;，仅为 GMII 的一半，PCB 布局更轻松。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;缺点&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;时序要求严格，因为用到了双沿和信号编码。&lt;/li&gt;
&lt;li&gt;通常需要 PCB 上做&lt;strong&gt;等长走线&lt;/strong&gt;，并且 MAC/PHY 内部要有时钟延迟调整功能（一般通过 RGMII TX/RX Delay 配置）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用场景&lt;/strong&gt;：绝大多数现代嵌入式处理器、以太网交换机芯片、路由器、网卡。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;实现以太网通信的模块框图-udp-通信&quot;&gt;实现以太网通信的模块框图 (UDP 通信)&lt;/h2&gt;
&lt;p&gt;模块以实现 UDP 回环测试为目标，整体模块框图如下图所示。
接下来的系列文章根据以下模块框图逐步推进。
&lt;img alt=&quot;UDP 通信模块框图&quot; width=&quot;1046&quot; height=&quot;686&quot; src=&quot;https://blog.tyh123.top/_astro/udp_module_diagram.BTgSHcOi_Z1UdWjf.webp&quot;&gt;
&lt;strong&gt;注意&lt;/strong&gt;：&lt;code&gt;Protocol SW&lt;/code&gt; 模块的作用是在 ARP 协议与 IP 协议之间进行切换。若将 ARP 协议和 IP 协议直接连接到 MAC 发送层，会&lt;strong&gt;导致多重驱动问题&lt;/strong&gt;，这在 FPGA 设计中是不允许的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;简单来说，&lt;code&gt;Protocol SW&lt;/code&gt; 模块正是&lt;strong&gt;为了避免因多重驱动而引发时序违例&lt;/strong&gt;所设计的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;以太网的硬件设计离不开 MAC 与 PHY 的协同工作，而 GMII 与 RGMII 作为两者之间的主流接口，分别在高性能与高集成度之间提供了不同的选择。GMII 以完整的 8 位数据线和简单的时序控制，适合对时序要求严苛的早期设备或开发平台；RGMII 则通过 DDR 技术和引脚精简，成为当前嵌入式系统中最常用的接口标准。理解这些接口的特性，有助于在实际项目中进行合理的芯片选型和 PCB 设计，从而在性能、成本与布线复杂度之间取得平衡。&lt;/p&gt;</content>,<category term="FPGA"/>,<category term="Verilog"/>,<category term="嵌入式"/>,<category term="通信接口"/>,<category term="以太网"/>,<category term="经验分享"/>,  </entry>,<entry>,<id>https://blog.tyh123.top/posts/29f8f7f6</id>,<title>关于点阵字库数据的生成, 以及对应偏移地址的计算方法</title>,<link href="https://blog.tyh123.top/posts/29f8f7f6" rel="alternate" type="text/html"/>,<published>2026-03-17T09:18:06.000Z</published>,<updated>2026-03-17T09:18:06.000Z</updated>,<summary type="text">记录点阵字库数据的生成方法, 以及把字库按偏移地址烧录到 W25Q 系列 Flash 芯片的完整流程, 包含 GBK 编码计算与程序实现思路。</summary>,<content type="html">&lt;h2 id=&quot;前言&quot;&gt;前言&lt;/h2&gt;
&lt;p&gt;本文旨在提供思路和方法，同时作为一篇开发笔记分享给大家。本文中所有的软件资源、代码等可以在 Gitee 仓库: &lt;a&gt;Stm32_HZK&lt;/a&gt; 中获取。&lt;/p&gt;
&lt;p&gt;本文中所使用的 Flash 芯片型号为 华邦(WINBOND) 生产的 W25Q 系列的芯片，它是一款&lt;strong&gt;非易失性存储芯片&lt;/strong&gt;，通信方式支持有 SPI, Dual SPI, QUAD SPI 这三种方式。 支持的最大时钟频率为  133MHz。&lt;/p&gt;
&lt;h2 id=&quot;gbk-汉字字符集的生成&quot;&gt;GBK 汉字字符集的生成&lt;/h2&gt;
&lt;h3 id=&quot;什么是-gbk-汉字字符集&quot;&gt;什么是 GBK 汉字字符集？&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;GBK 汉字字符集&lt;/strong&gt; 是一种用于表示中文字符的字符编码。它包含了所有的中文字符，每个中文字符都有一个唯一的编码值。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;通俗的来讲就是所有汉字的一个集合，这个集合按照先后顺序被排列组合起来，就形成了 GBK 汉字字符集。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;gbk-字符集的编码格式&quot;&gt;GBK 字符集的编码格式&lt;/h3&gt;
&lt;p&gt;与 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fascii.org.cn%2F&quot;&gt;ASCII 码&lt;/a&gt; 不同的是，GBK 编码&lt;strong&gt;是一个 16 位的编码&lt;/strong&gt;，每个汉字&lt;strong&gt;需要 2 个字节&lt;/strong&gt;来表示。GBK 编码的双字节结构，具体分为:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区码 （高字节）&lt;/strong&gt;: 范围 0x81-0xFE&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位码 （低字节）&lt;/strong&gt;: 范围 0x40-0xFE（0x7F 除外）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以，经过计算后可以得出&lt;strong&gt;GBK 理论编码位置有 23940 个&lt;/strong&gt;。计算过程如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区码（高字节）长度&lt;/strong&gt;: &lt;code&gt;0x81-0xFE+0x01=0x7E&lt;/code&gt;，加上 1 则是算上起始位置。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;位码（低字节）长度&lt;/strong&gt;: &lt;strong&gt;又称为每个区的字码数量&lt;/strong&gt; &lt;code&gt;0x40-0xFE-0x01+0x01=0xBE&lt;/code&gt;，减去的 1 是被除掉的 0x7F，加上的 1 则是算上起始位置。&lt;/li&gt;
&lt;li&gt;所以总的可用的编码位置一共有: &lt;code&gt;0x7E*0xBE=0x5D84&lt;/code&gt; 个，十进制下为 &lt;code&gt;23940&lt;/code&gt; 个。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;举个例子说明为什么计算结果要加 1，比如有 &lt;code&gt;2 3 4 5 6&lt;/code&gt; 这几个数字，那么数字的数量就是 &lt;code&gt;(6-2)+1=5&lt;/code&gt;，小学数学问题，但是防止不懂还是提一下(doge)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&quot;为什么要生成它&quot;&gt;为什么要生成它？&lt;/h3&gt;
&lt;p&gt;在这之前，我尝试在网络上搜索现有的 GBK 汉字字符集，但是搜索到的都是已经收录到 GBK 字符集的字库文件，而我需要的是一个完整的 GBK 字符集文件，包含所有理论上的中文字符。所以我就打算自己生成一个 GBK 汉字字符集文件。&lt;/p&gt;
&lt;h3 id=&quot;生成-gbk-汉字字符集&quot;&gt;生成 GBK 汉字字符集&lt;/h3&gt;
&lt;p&gt;本次使用 python 生成 GBK 汉字字符集。代码如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 生成的索引文件会被保存到当前目录下的 GBK_INDEX.txt 中
file = open(&quot;GBK_INDEX.txt&quot;, &quot;wb&quot;)

highByte = 0x81 # 高字节起始位置
lowByte = 0x40  # 低字节起始位置

# 文件头写入 ascii 码
# 也可以把这部分放到下一部分中，在文件尾写入 ascii 码
asciiCode = 20 # ascii 码起始字符位置
while True:
    asciiCode += 1
    if asciiCode &gt; 0x7E: # 终止字符位置为 0x7E
        break
    file.write(bytes([asciiCode]))

while True:
    # 跳过 0x7F（GBK 编码中该位码无效）
    if lowByte == 0x7F:
        lowByte += 1
        continue
    
    # 写入当前高低字节
    file.write(bytes([highByte, lowByte]))

    # 低位字节自增
    lowByte += 1

    # 检查低位是否溢出，即达到最大值 0xFE
    if lowByte &gt; 0xFE:
        # 重置低位并增加高位
        lowByte = 0x40
        highByte += 1

        # 检查高位是否达到终止条件
        if highByte &gt; 0xFE:
            break

file.close()&lt;/code&gt;&lt;/pre&gt;

&lt;h2 id=&quot;生成点阵字库&quot;&gt;生成点阵字库&lt;/h2&gt;
&lt;p&gt;将我们生成好的 GBK 汉字字符集导入到 PCtoLCD2002 软件中去生成我们的点阵字库数据就可以了。图中配置的是每个中文字符的字模数据为 16x16 大小，占用 32 个字节。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;首先打开 PCtoLCD2002 软件，对应的软件资源位于 &lt;a href=&quot;https://gitee.com/Stm32_HZK/Stm32_HZK&quot;&gt;Stm32/_HZK&lt;/a&gt; 仓库中 &lt;code&gt;PCtoLCD2002 完美版&lt;/code&gt; 文件夹内。&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;导入大量文本&lt;/code&gt; 按钮，选择 &lt;code&gt;打开文本文件&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;选择我们之前生成的 GBK_INDEX.txt 文件&lt;/li&gt;
&lt;li&gt;点击 &lt;code&gt;开始生成&lt;/code&gt; 按钮，选择好保存路径后，等待软件生成完成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img alt=&quot;generate_index_data&quot; width=&quot;1167&quot; height=&quot;933&quot; src=&quot;https://blog.tyh123.top/_astro/generate_index_data.Cis63GTi_1gXnm1.webp&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;计算汉字字模数据的偏移地址&quot;&gt;计算汉字字模数据的偏移地址&lt;/h2&gt;
&lt;p&gt;在这之前，我们已经生成了&lt;strong&gt;连续的 &lt;code&gt;23940&lt;/code&gt; 个 GBK 编码的中文字符&lt;/strong&gt;，并且对每个字符都取模生成了对应的字模数据，每个中文字符的字模数据都是 32 个字节。所以在生成的字库数据中，每个&lt;strong&gt;汉字的字模数据都是连续存储的&lt;/strong&gt;，我们需要根据汉字的编码值来计算出它的字模数据在 Flash 中的偏移地址。&lt;/p&gt;
&lt;p&gt;以汉字 “啊” 为例子，它的 GBK 编码为 &lt;code&gt;0xB0A1&lt;/code&gt;，根据 GBK 编码的规则，计算过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它的区码为 &lt;code&gt;0xB0&lt;/code&gt;，由于区码的起始位置为 &lt;code&gt;0x81&lt;/code&gt;，&lt;strong&gt;所以区码偏移为 &lt;code&gt;0xB0-0x81=0x2F&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它的位码为 &lt;code&gt;0xA1&lt;/code&gt;，由于位码的起始位置为 &lt;code&gt;0x40&lt;/code&gt;，并且它的值大于了 &lt;code&gt;0x7F&lt;/code&gt;，要在最终的结果上减去 1，&lt;strong&gt;所以位码偏移为 &lt;code&gt;0xA1-0x40-0x01=0x60&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;将汉字的&lt;strong&gt;区码偏移和位码长度&lt;/strong&gt;相乘，再加上位码偏移，就得到了&lt;strong&gt;汉字偏移量&lt;/strong&gt;：&lt;code&gt;0x2F*0xBE+0x60=0x2342&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;再将&lt;strong&gt;汉字偏移量&lt;/strong&gt;乘以每个中文字符的字模数据大小 32 字节，就得到了&lt;strong&gt;汉字字模数据在 Flash 中的偏移地址&lt;/strong&gt;：&lt;code&gt;0x2342*0x20=0x046840&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;h3 id=&quot;小结&quot;&gt;小结&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;若位码小于等于 0x7F，则地址计算为:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;地址 = (区码 - 0x81) * 0xBE + (位码 - 0x40) * 32&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;若位码大于 0x7F，则地址计算为:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;地址 = (区码 - 0x81) * 0xBE + (位码 - 0x40 - 0x01) * 32&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;其中，0x81 是区码的起始位置，0xBE 是每个区的字码数量，0x40 是位码的起始位置，0x01 是为了修正位码的偏移量。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;验证计算结果&quot;&gt;验证计算结果&lt;/h2&gt;

&lt;p&gt;使用 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fhexed.it%2F&quot;&gt;十六进制编辑器&lt;/a&gt; 打开我们生成的字库文件，按下 &lt;code&gt;Ctrl+G&lt;/code&gt; 输入 &lt;code&gt;0x046840&lt;/code&gt; 这个位置，之后按下回车跳转到这个位置，从这个位置开始，连续读取 32 个字节，就是汉字 “啊” 的字模数据了。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;goto_address1&quot; width=&quot;1080&quot; height=&quot;936&quot; src=&quot;https://blog.tyh123.top/_astro/goto_address1.CllTaKzF_Z16zOM7.webp&quot;&gt;&lt;/p&gt;
&lt;p&gt;选中这些数据，把它复制下来，粘贴到 &lt;a href=&quot;https://blog.tyh123.top/go?url=https%3A%2F%2Fwww.23bei.com%2Ftool%2F553.html&quot;&gt;字模验证工具网站&lt;/a&gt; 中 (注意选择对应的取模参数，数据排列和取模方式)，从图中可以看到我们的字模数据是正确的。&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;font_data_validation&quot; width=&quot;1080&quot; height=&quot;936&quot; src=&quot;https://blog.tyh123.top/_astro/font_data_validation.C410GNhO_1O0oWm.webp&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;刚刚验证的是汉字 “啊” 的字模数据，它的位码大于了 0x7F，验证的是 &lt;a href=&quot;#%E5%B0%8F%E7%BB%93&quot;&gt;小结&lt;/a&gt; 中 “若位码大于 0x7F” 这一部分的计算结果。下面以汉字 “丢” 为例子验证 “位码小于 0x7F” 这一部分的计算结果。
汉字 “丢” 的 GBK 编码为 &lt;code&gt;0x8147&lt;/code&gt;，根据 GBK 编码的规则，计算过程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;它的区码为 &lt;code&gt;0x81&lt;/code&gt;，由于区码的起始位置为 &lt;code&gt;0x81&lt;/code&gt;，&lt;strong&gt;所以区码偏移为 &lt;code&gt;0x81-0x81=0x00&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;它的位码为 &lt;code&gt;0x47&lt;/code&gt;，由于位码的起始位置为 &lt;code&gt;0x40&lt;/code&gt;，并且它的值小于等于 0x7F，&lt;strong&gt;所以位码偏移为 &lt;code&gt;0x47-0x40=0x07&lt;/code&gt;&lt;/strong&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以它的偏移地址为 &lt;code&gt;0x00*0xBE+0x07*0x20=0xE0&lt;/code&gt;。
跳转到 &lt;code&gt;0xE0&lt;/code&gt; 这个位置就可以看到汉字 “丢” 的字模数据了。在这里不再赘述验证过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;烧录字库数据到-flash-芯片内&quot;&gt;烧录字库数据到 Flash 芯片内&lt;/h2&gt;
&lt;p&gt;这部分内容你可以用任何方式包括但不限于 ST-LINK、J-LINK、SWD 调试器、以及最基本的串口烧录(配合 Flash 驱动程序)。在这里就不详细展开说明了。&lt;/p&gt;</content>,<category term="STM32"/>,<category term="嵌入式"/>,<category term="字库"/>,<category term="经验分享"/>,  </entry></feed>