<?xml version="1.0" encoding="UTF-8" ?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
	<channel>
		<title>Studio Untagged</title>
		<link>https://studiountagged.top</link>
		<description>Studio Untagged 的最新文章订阅源</description>
		<language>zh-cn</language>
		<lastBuildDate>Sun, 14 Jun 2026 12:34:42 GMT</lastBuildDate>
		
				<item>
					<title>Markdown 文档测试</title>
					<link>https://studiountagged.top/articles/test</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/test/</guid>
					<pubDate>Wed, 31 Dec 1969 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							Markdown 文档排版效果测试
A typography test for markdown document.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h1>1. 一级标题</h1>
<h2>1.1 二级标题</h2>
<h3>1.1.1 三级标题</h3>
<h2>2. 行内文本样式</h2>
<p>这是一个段落。滚滚长江东逝水，浪花淘尽英雄。是非成败转头空，青山依旧在，几度夕阳红。白发渔樵江渚上，惯看秋月春风。一壶浊酒喜相逢，古今多少事，都付笑谈中。</p>
<ul>
<li><strong>加粗 Bold</strong></li>
<li><em>斜体 Italic</em></li>
<li><em><strong>粗斜体 Bold Italic</strong></em></li>
<li><del>删除线 Strikethrough</del></li>
<li><u>下划线 Underline</u></li>
<li><code>行内代码 Inline Code</code></li>
<li><a href="https://example.com">超链接 Hyperlink</a></li>
<li>行内公式 Inline Formula $E = mc^2$</li>
<li><mark>文本高亮 Text Highlight</mark></li>
<li>下标 Subscript H<del>2</del>O</li>
<li>上标 Superscript x^2^</li>
<li><q>行内引用 Inline Quote</q></li>
</ul>
<h2>3. 引用与嵌套</h2>
<blockquote>
<p>这是一个块引用。</p>
<p>由于要测试背景引号的效果，所以在这里水字数多弄几行。是非成败转头空，青山依旧在，惯看秋月春风。一壶浊酒喜相逢，古今多少事，滚滚长江东逝水，浪花淘尽英雄。 几度夕阳红。白发渔樵江渚上，都付笑谈中。</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.</p>
<blockquote>
<p>这是一个嵌套引用。</p>
<ul>
<li>引用中的列表项</li>
<li>第二项</li>
</ul>
</blockquote>
</blockquote>
<blockquote>
<p>[!DANGER] 危险 DANGER
这是个表明危险的块引用。
This is a callout block to show danger.</p>
</blockquote>
<blockquote>
<p>[!WARNING] 警告 WARNING
这是个表明警告的块引用。
This is a callout block to show warnings.</p>
</blockquote>
<blockquote>
<p>[!SUCCESS] 成功 SUCCESS
这是个表明成功的块引用。
This is a callout block to show success.</p>
</blockquote>
<blockquote>
<p>[!TIP] 提示 TIP
这是个表明危提示的块引用。
This is a callout block to show tips.</p>
</blockquote>
<blockquote>
<p>[!NOTE] 注释 NOTE
这是个表明注释的块引用。
This is a callout block to show notes.</p>
</blockquote>
<blockquote>
<p>[!ABSTRACT] 概述 ABSTRACT
这是个表明概述的块引用。
This is a callout block to show abstract.</p>
</blockquote>
<blockquote>
<p>[!QUESTION] 疑问 QUESTION
这是个表明疑问的块引用。
This is a callout block to show questions.</p>
</blockquote>
<h2>4. 列表样式 List Style</h2>
<h3>无序列表</h3>
<ul>
<li>苹果</li>
<li>香蕉<ul>
<li>青香蕉</li>
<li>熟香蕉</li>
</ul>
</li>
<li>樱桃</li>
</ul>
<h3>有序列表</h3>
<ol>
<li>第一步</li>
<li>第二步<ol>
<li>子步骤 1</li>
<li>子步骤 2</li>
</ol>
</li>
</ol>
<h3>任务列表</h3>
<ul>
<li><input checked="" disabled="" type="checkbox"> 启动网站</li>
<li><input disabled="" type="checkbox"> 添加文章</li>
<li><input disabled="" type="checkbox"> 部署生产环境</li>
</ul>
<h2>5. 代码块 Code Block</h2>
<h3>行内代码 Inline Code</h3>
<p>请使用 <code>npm install</code> 安装依赖。</p>
<h3>块代码 Code Block</h3>
<pre><code class="language-js">function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet("World"));
</code></pre>
<h2>6. 表格测试</h2>
<table>
<thead>
<tr>
<th>编号</th>
<th>项目</th>
<th>说明</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>标题样式</td>
<td>多级标题支持</td>
</tr>
<tr>
<td>2</td>
<td>列表测试</td>
<td>有序/无序/任务</td>
</tr>
<tr>
<td>3</td>
<td>代码样式</td>
<td>高亮 &amp; 缩进</td>
</tr>
</tbody></table>
<h2>7. 图片</h2>
<h3>网络图片</h3>
<p><img src="https://studiountagged.top/articles/test/img/Tsunami_by_hokusai_19th_century.jpg" alt="示例图片"></p>
<h2>8. 分割线</h2>
<hr>
<h2>9. 数学公式（块级）</h2>
<p>$$
\int_{a}^{b} f(x) \mathrm{d} x = F(b) - F(a)
$$</p>
<h2>10. 脚注<a href="%E8%BF%99%E6%98%AF%E8%84%9A%E6%B3%A8%E7%9A%84%E5%86%85%E5%AE%B9%E3%80%82">^1</a></h2>
<p>这是包含脚注的文本。脚注位置如下方定义。</p>
<h2>11. 目录测试（TOC）</h2>
<blockquote>
<p>如果你的渲染器支持目录（例如<code>[toc]</code>或<code>[[toc]]</code>），可使用如下语法：</p>
</blockquote>
<p>[toc]</p>
<h2>12. 定义列表（支持的 Markdown 扩展）</h2>
<p>术语 1
: 定义内容 1</p>
<p>术语 2
: 定义内容 2，支持多行<br>  第二行内容</p>
<h2>13. Mermaid 流程图</h2>
<pre><code class="language-mermaid">flowchart TD
    A[Christmas] --&gt;|Get money| B(Go shopping)
    B --&gt; C{Let me think}
    C --&gt;|One| D[Laptop]
    C --&gt;|Two| E[iPhone]
    C --&gt;|Three| F[fa:fa-car Car]
</code></pre>
<h2>14. 总结</h2>
<p>这是一篇用于全面测试 Markdown 排版效果的文档。你可以：</p>
<ul>
<li>预览渲染样式</li>
<li>检查公式、脚注、引用渲染是否正确</li>
<li>验证高亮和主题切换是否影响文本可读性</li>
</ul>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>驱动 AY-3-8910（二）：简单频谱分析以及滤波电路设计</title>
					<link>https://studiountagged.top/articles/2026-04-26-Spectrum-Analysis-and-Filter-Design</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2026-04-26-Spectrum-Analysis-and-Filter-Design/</guid>
					<pubDate>Sat, 25 Apr 2026 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							对 AY-3-8910 的输出信号进行频谱分析，并设计一阶 RC 无源滤波器
Perform a spectral analysis of the AY-3-8910 output signals and design a first-order passive RC filter.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h2>前言</h2>
<p>在上一篇文章[[驱动 AY-3-8910（一）]]中，已经成功驱动了 AY-3-8910，并让它能够播放指定音量和时长的音调。在这一篇文章中，将介绍频谱分析，以及简单滤波电路设计等内容。</p>
<h2>频谱分析</h2>
<p>在上一篇文章中，芯片输出信号是直接驱动耳机的，这种未经优化的信号听起来非常刺耳，尤其在高音区。<strong>相比之下，</strong> 同样是高音，为什么像施坦威这类名琴发出的声音，反而显得清脆悦耳、毫无噪感呢？</p>
<p>要回答这个问题，就得先了解声音的基本特征。钢琴弹奏一个 C4 的音，和小提琴拉奏一个 C4 的音，这两者的音高和音量完全一致，就算蒙住眼睛，也能轻易分辨出是哪个乐器发出的。因为它们的<strong>音色 (Timbre)</strong> 有显著不同。音色的本质是什么？为什么不同的乐器会有不同的音色？</p>
<p>以钢琴为例，按下一个键时，会联动一个小木槌敲击琴弦振动发声。如果去看钢琴声音的波形图，放大之后就会发现并非简单的正弦波，而是一种更加复杂的振动模式。因为除了琴弦振动本身带来的正弦波以外，还会有诸如琴弦材质、小木槌的摩擦、腔体的共振乃至空气湿度等等因素影响着最终波形的塑造。</p>
<p>在这个过程中，琴弦本身的振动就是<strong>基频 (Fundamental Fre­quency)</strong>。基频决定了我们听到的音高；除了基频以外的其他所有频率分量就构成了<strong>泛音 (Overtone)</strong>。在泛音中，基频的整数倍 $2f, 3f, 4f$ 等为<strong>谐音 (Harmonics)</strong>，这是乐器音色的主要决定部分。除此之外还有一些非谐波的成分，例如小木槌的摩擦、腔体共振等等，这些共同构成了钢琴丰富且复杂的音色。而正是因为不同乐器的发声结构千奇百异，才会有独属于它们自己的音色。</p>
<p>![[img/Instrument_Waveforms.avif|不同乐器发出的波形。]]</p>
<h3>傅里叶变换</h3>
<p>知道了构成音色的到底是什么以后，该如何对其进行分析？法国数学家傅里叶 (Joseph Fourier) 揭示了一个深刻的真理：<strong>任何复杂的信号波形，本质上都可以分解为无数个不同频率正弦波的线性叠加</strong>。这一数学工具便是名垂青史的<strong>傅里叶变换 (Fourier Transform, FT)</strong>。以钢琴弹奏出的某个音符为例，宏观上看，它表现为一种极度复杂的振动模式；这种复杂的波形通过 FT 后会被分解为一系列正弦分量，背后的频率构成便一目了然。这一原理不仅是声学的基础，更是现代通信、图像处理及信号分析等领域的灵魂。</p>
<p>录音时的电平图像、示波器上显示的波形等，记录的都是物理量随时间流逝的状态。横坐标是时间，纵坐标是物理量的值。这种以时间为参考来衡量动态世界的方式被称为<strong>时域 (Time Domain)</strong> 分析。而通过傅立叶变换，得以从另一个完全不同的视角去审视信号——这就是<strong>频域 (Frequency Domain)</strong> 分析。前者好比整首曲子的录音，后者就是这首曲子的乐谱。</p>
<p>接下来的讲解来自于 <a href="https://youtu.be/spUNpyF58BY?si=koIXNLcFVXmvn_89">3Blue1Brown</a> 和 <a href="https://youtu.be/nmgFG7PUHfo?si=ZxoRL4f8_F1_nRN4">Veritasium</a> 的视频，我非常推荐去观看原视频。此处仅做总结，加上一部分自己的感悟。</p>
<p>从数学上讲，连续信号的傅里叶变换通过积分将时域函数 $f(t)$ 映射为频域函数 $F(\omega)$：
$$F(\omega) = \int_{-\infty}^{\infty}f(t)e^{-j\omega t}\mathrm{d}t$$</p>
<p>这个公式乍一看很吓人，不妨拆开来看看。公式中的 $j$ 就是复数单位 $i$. 在复平面中，横坐标代表实部，纵坐标代表虚部。这就意味着当一个矢量乘以 $i$ 后方向会和原矢量垂直。</p>
<p>![[img/Complex_Plane.avif|矢量 $z_1=2+3i$，乘以 $i$ 后得到新的矢量 $z_2=-3+2i$。$z_2$ 与 $z_1$ 的夹角正好是 90°.]]</p>
<p>假设某一函数反映了平面内某一点的位置随时间变化的关系：
$$
\frac{ \text{d} }{ \text{d}t }e^{it}=i\cdot e^{it}
$$</p>
<p>把等式左侧看作速度矢量，右侧看作位置矢量。由于右侧比左侧多了一个旋转因子 $i$，速度矢量的方向将会永远垂直于位置矢量。随着时间 $t$ 的增长，将在复平面上画出一个圆。这便引出了数学中最伟大的公式——<strong>欧拉公式 (Euler's Formula)</strong>：
$$
e^{it} = \cos(t)+i\sin(t)
$$</p>
<p>如果要改变绘制这个圆的速度，需引入频率参数 $\omega$ 改变速度矢量的虚部 $\omega i\cdot t$。如果再为 $e$ 的幂加上实部变成 $(\sigma+\omega i)t$，此时速度矢量在水平方向上也有了分量，随时间变化就能够绘制出螺旋线。</p>
<p>![[img/Complex_Plane_2.avif|绘制的轨迹随 $e$ 的幂的变化。$a$ 是矢量的实部，决定着图像随时间的增益或衰减；$b$ 是虚部，决定着图像的震荡速度。]]</p>
<p>将旋转基底乘以原始信号函数 $f(t)$，在视觉上这就产生了一种类似于将原信号「缠绕」在单位圆上的效果。在这个缠绕图像中，质心的位移正是频率提取的关键。从物理上看，质心的位置取决于图像分布的均衡程度。计算质心的方法是取若干个样本点，并计算它们位置的平均值。而当样本点的个数趋于无穷时，就变成了定积分。当缠绕频率与信号内在频率相同时，质心会猛然偏离原点，偏移距离和方向便反映了该频率的强度与相位。</p>
<p>![[img/Complex_Plane_3.avif|蓝色曲线为函数 $g(t)=\cos(2\pi\cdot 2t)+1.5$ 在单位圆上缠绕的效果。$f$ 是缠绕频率，红点是质心。观察质心轨迹可以发现，当 $f=2$（即等于 $g(t)$ 的频率时）时，质心在 $x$ 轴上的位移最大；而其他频率时仅在原点附近轻微振荡（忽略质心的初始位置，因为函数向上偏移了）。在偏移达到最大的同时，绕成的图像形成了帕斯卡蜗线。]]</p>
<p>为了方便解释，故采用了质心这一思想。而傅里叶变换就不需要对图形的每个采样点取平均值。换个说法，最大位移就指向了原函数的频率。</p>
<p>![[img/Complex_Plane_4.avif|紫色的轨迹即质心随 $f$ 的变化在 $x$ 轴上的偏移量。忽略初始的位置变化，峰值出现的位置正好等于 $g(t)$ 的频率。]]</p>
<p>傅里叶变换要求信号必须是收敛的，即必须衰减得足够快，在 $t\to\infty$ 内能够收敛到 0 或一个常数上，以及能量是有限的。换句话说，信号是绝对可积的：
$$
\int_{-\infty}^{\infty}|f(t)|dt &lt; \infty
$$</p>
<p>以及平方可积的：
$$
\int_{-\infty}^{\infty}|f(t)|^2dt &lt; \infty
$$</p>
<p>而如果信号是永恒不变的，在尝试计算前文提到的质心的时候，积分的结果就会发散，从而无法反映出正确的结果。直流和阶跃是两种很重要的信号，然而它们并不满足绝对可积的条件。这时候就要引入一个衰减因子，也就是前文中提到的在 $e$ 的幂中的实部 $\sigma$，强行让信号收敛。而这就是<strong>拉普拉斯变换 (Laplace Transform)</strong>：
$$
F(s)=\int_{0}^{\infty}f(t)e^{-st}\text{d}t
$$</p>
<p>其中，拉普拉斯算子 $s = \sigma + j\omega$. FT 可以近似地看作 LT 在特定条件下的一种特殊情况。</p>
<p>在工程中通常不会对时间跨度这么长的信号进行分析，而是从中截取一段有限的时间区间 $[0, T]$ 进行分析，就像只对屏幕上窗口内展示的信号进行分析，原信号 $f(t)$ 乘以一个<strong>窗函数 (Window Function)</strong>。在这段区间以外的信号都会强制变成 0.</p>
<p>然而，这种截断并非没有代价。如果截取的区间未能恰好包含信号频率的完整周期时，窗口的首尾两端就会发生剧烈的跳变。这种时域上的不连续性就会造成很多本不应该出现的频率分量，导致频谱能量从主频向四周漫延。这便是所谓的<strong>频谱泄露 (Spectral Leakage)</strong>。频率主峰附近会隆起许多本不属于信号的<strong>旁瓣 (Sidelobes)</strong>。</p>
<p>![[img/Spectural_Leakage.avif|被截断的信号。首尾处出现跳变。<a href="https://www.ni.com/docs/en-US/bundle/ni-scope/page/spectral-leakage.html?srsltid=AfmBOoq5OivBKvnMMr_DuP6qir8p-0K53IeUuNjueGeUjKG-izoBPIyl">NI</a>]]</p>
<p>为了抑制这种副作用，信号应该「淡入淡出」。通过使用加权窗，窗口中心区域的信号获得更高的权重，而让两侧边缘的信号平滑地衰减至零。这种柔和的过渡能够有效平复截断处的跳变，从而压制旁瓣能量。常见的窗函数如下，此处暂时不做过多解释：</p>
<p>![[img/Rectangular_Window.svg|矩形窗 (Rectangular Window)。]]</p>
<p>![[img/Hann_Window.svg|汉恩窗 (Hann Window)。]]</p>
<p>![[img/Hamming_Window.svg|汉明窗 (Hamming Window). 与汉恩窗类似，窗口两端留有约 0.08 的台阶，不归零。]]</p>
<p>![[img/Blackman_Window.svg|布莱克曼窗 (Blackman Window)。]]</p>
<p>![[img/Triangular_Window.svg|三角窗 (Triangle Window)。]]</p>
<p>![[img/Flat_Top_Window.svg|平顶窗 (Flat Window)。]]</p>
<p>读者可能已经注意到了，频率相同时的图形颇有意思，此处粗略介绍一下。假设信号是一个简谐波加上直流偏量：$g(t)=B+A\sin(2\pi f_0t)$. 缠绕的过程本质上是将时间 $t$ 映射为极坐标中的角度 $\theta$. 如果以频率 $f$ 进行旋转，那么在 $t$ 时刻，旋转过的总角度（弧度）为：
$$
\theta = 2\pi ft
$$</p>
<p>可得时间 $t$ 与角度 $\theta$ 的关系：
$$
t=\frac{\theta}{2\pi f}
$$</p>
<p>代入 $g(t)$ 作为极径 $r$：
$$
\begin{align}
r(\theta) &amp;= g\left( \frac{\theta}{2\pi f} \right)=B+A\sin\left( 2\pi f_0\cdot \frac{\theta}{2\pi f} \right) \
&amp;= B+Asin(\frac{f}{f_0}​​\theta)
\end{align}
$$</p>
<p>而当 $f=f_0$ 时就会变成：
$$
r(θ)=B+Asin(θ)
$$</p>
<p>这正是<strong>帕斯卡蜗线 (Limaçon of Pascal)</strong> 的标准方程。而当 $B=A$ 时就是大名鼎鼎的<strong>心脏线 (Cardioid)</strong>。</p>
<p>![[img/Cardioid.avif|当 $B=A$ 时，帕斯卡蜗线呈尖点型，即心脏线。]]</p>
<p>回到正题。现实生活中各种信号变化并非无限精细，同时仪器输出的波形看似光滑，也会存在不可分辨的最小单位。所以最终得到的是离散、有限的信号。此时连续傅里叶变换不再适用，取而代之的是<strong>离散傅里叶变换 (Discrete Fourier Transform, DFT)</strong>。对于长度为 $N$ 的序列 $x[n]$，对其 DFT 的公式为：
$$
X[k] = \sum_{n=0}^{N-1}x[n]\cdot e^{-j\frac{2\pi}{N}nk}, k=0, 1, 2, \dots, N-1
$$
离散的视角下，圆周不能平滑地旋转，而是像那种一次走一格的时钟。在 $e$ 的幂中，如果有 $N$ 个采样点，那么就需要将圆弧分成 $N$ 份，也就是 $\tfrac{2\pi}{N}$. 而 $n$ 就表示当前是第几个采样点。$k$ 就是缠绕频率。</p>
<p>DFT 可以看作一个矩阵乘法 $y=Wx$，其中 $W$ 是 $n\times n$ 的范德蒙德矩阵：
$$
\begin{bmatrix} 
X[0] \ X[1] \ \vdots \ X[n-1] 
\end{bmatrix} = 
\begin{bmatrix} 
\omega^0 &amp; \omega^0 &amp; \dots &amp; \omega^0 \
\omega^0 &amp; \omega^1 &amp; \dots &amp; \omega^{n-1} \
\vdots &amp; \vdots &amp; \ddots &amp; \vdots \
\omega^0 &amp; \omega^{n-1} &amp; \dots &amp; \omega^{(n-1)(n-1)}
\end{bmatrix}
\begin{bmatrix} 
x[0] \ x[1] \ \vdots \ x[n-1] 
\end{bmatrix}
$$</p>
<p>为了得到单个点 $X[k]$，需要从 0 到 N-1 进行求和，其中涉及到 N 次复数乘法与 N-1 次复数加法，复杂度就是 $O(n)$；而整个序列中含有 N 个点，需要将上述过程重复 N 次，总复杂度就是 $O(N^2)$. 对于高采样率的信号计算量会变得非常巨大，无法实时分析。</p>
<p><strong>快速傅里叶变换 (Fast Fourier Transform, FFT)</strong> 则优化了算法的复杂度。这个算法曾经被数学家吉尔伯特·斯特朗描述为「我们一生中最重要的数值算法」。库利-图基算法是最常见的 FFT 算法。除此之外还有质因子算法 (Prime-Factor Algorithm) 等等，此处按下不表。</p>
<p>库利-图基算法靠两个手段优化。第一个是旋转因子的周期性与对称性。由于旋转因子 $W^{nk}<em>N = e^{-j\frac{2\pi}{N}nk}$ 本质上就是圆上的点，因此具备两个特性：对称性 $W^{k+N/2}</em>{N} = -W^{k}<em>{N}$ 和周期性 $W^{k+N}</em>{N} = W^k_N$. 这意味着很多复杂的乘法运算结果其实是一样的，或者只需变个符号。</p>
<p>而第二个是递归拆分。将一个长度为 $N$ 的大问题，拆分成两个长度为 $N/2$ 的小问题。对于长度为 $N$ 的序列 $x[n]$，分成偶数索引 $x_{2m} = x_0, x_2, \dots, x_{N-2}$ 和奇数索引 $x_{2m+1} = x_1, x_3, \dots, x_{N-1}$ 两组。分别计算这两组的 DFT。 利用旋转因子的特性，将这两个小结果组合成最终的结果。这个过程会一直递归下去，直到拆成只有 2 个点的小单元。此时算法的复杂度就降到了 $O(N \log_2 N)$.</p>
<p>得益于 FFT 的高效，它不仅统治了频率分析领域，更成为了数字信号处理的核心底座。例如，计算机中极为耗时的<strong>卷积 (Convolution)</strong> 运算，在频域下只需进行简单的乘法即可完成。这种运算在图像、音频处理，以及深度学习（例如卷积神经网络）等领域都有大量运用，以后有机会深入研究。</p>
<h2>滤波器</h2>
<p>前面的频谱分析结果揭示了方波在高频处有频率分量的分布，而这也是导致听感刺耳的「罪魁祸首」。因此，需要将这些高频的成分过滤掉，实现这个功能的电路就被称为<strong>滤波器 (Filter)</strong>。滤波器有很多种，如果根据频率响应来划分，通低频、阻高频的就是<strong>低通滤波器 (Low-pass Filter, LPF)</strong>，反之就是<strong>高通滤波器 (High-pass Filter, HPF)</strong>；还有针对特定频段的<strong>带通 (Band-pass Filter, BPF)</strong> 和<strong>带阻 (Band-stop Filter, BSF)</strong> 滤波器。</p>
<p>![[img/Type_Of_Filters.avif|滤波器的主要类型。从上到下，从左至右依次为低通、高通、带通、带阻滤波器。图像的横坐标为频率，纵坐标为信号的强度或功率。<a href="https://www.allaboutcircuits.com/video-tutorials/op-amp-applications-band-pass-and-band-reject-active-filters/">All About Circuit</a>]]</p>
<p>除此之外，有些滤波器结构比较简单，可能只包含电阻、电容或电感这几种器件，仅起滤波作用，这类就被称为无源滤波器；而有的含有运算放大器、晶体管等元件，除了滤波还可以对信号提供增益，这类就被称为有源滤波器。在本篇文章中，将着重介绍最简单的 RC 无源低通滤波器。</p>
<p>![[img/RC_Filter.avif|最基本的 RC 低通滤波器电路。]]</p>
<p>这个电路的结构非常简单。如图所示，将一个电阻 R1 与信号输出串联，电容器 C1 与负载 RL 并联，就可以产生 RC 低通响应。电容器本质上就是由两块导体板与中间的绝缘介质（电介质）构成的。在直流电路中，当电容器接入电路中时，电极的电压会因为电子的聚集而不断上升，直到等于电源电压，电流停止流动，所以此时表现可近似看作「断路」。换句话说，对电流的<strong>阻抗 (Impedance)</strong>，或<strong>容抗 (Capacitive reactance)</strong> 很大。</p>
<p>而在交流电路中，由于电流方向不断反转，电压方向正向时，电容板充电；当电压方向反向时，电容板放电，并向相反方向充电。电流并没有实际流过绝缘介质，但电流在电路中不断地来回流动，就像在电路中「通过」了一样；而频率越高，电容器充放电的速度也越快，内部介质来不及建立稳态电压，电流在外部电路中来回流动的速率也越高。从电路外部来看，这等效于电容器的阻抗越小。总结来说，电容的一个重要性质就是「通高频，阻低频」。容抗的计算公式如下，也可以看出其大小与频率成反比：
$$W
X_C = \frac{1}{2\pi fC}=\frac{1}{\omega C}
$$</p>
<p>当输入低频信号时，容抗相对于电阻的阻抗更高，因此电容上的分压降低；当输入高频信号时，容抗相对于电阻的较低，这意味着电阻上的分压降低，这样就较少的电压传输到负载。因此，低频通高频阻。而反过来，如果想要实现高通滤波，R1 和 C1 的位置就需要对调一下。这样，低频分量就会更多地分压在 C1 上，而高频分量更多地分压在 R1 上。</p>
<p>额外补充一点，除了电容器以外，<strong>电感器 (Inductor)</strong> 也有着与之相似的电气特性，只是两者恰好相反，即「通低频阻高频」。从物理本质上看，电感器通常由导线绕制成的线圈构成。当电流流经线圈时，会在其周围激发出磁场。如果电流 $i$ 的大小或方向发生变化，穿过线圈的磁通量 $\Phi$ 也会随之改变。根据<strong>法拉第电磁感应定律 (Faraday's Law of Induction)</strong>，当线圈不动而磁场变化时，穿过回路的磁通量也发生变化，由此在回路中激发的感生电动势 $\mathscr{E}$ 的大小与穿过线圈的磁通量变化率成正比。其数学表达式为：
$$
\mathscr{E} = -n\frac{\mathrm{d}\Phi}{\mathrm{d}t}
$$</p>
<p>其中，$n$ 是线圈的匝数。公式中的负号就是<strong>楞次定理 (Lenz's Law)</strong> 的体现，它表明感生电动势的方向始终倾向于阻碍原磁通量的变化。这种性质在电路中表现为对交流信号的阻碍作用，称为<strong>感抗 (Inductive Reactance)</strong>。感抗的计算公式如下：
$$
X_L = 2\pi fL=\omega L
$$</p>
<p>可以看出，感抗的大小与信号频率 $f$ 成正比。在低频或直流（$f=0$）情况下，感抗极小，电感可近似看作导线；而在高频情况下，感抗显著增大，从而实现对高频噪声的有效滤除。</p>
<p>![[img/RL_filter.avif|RL 滤波器。左侧为低通滤波器，右侧为高通滤波器。]]</p>
<p>回到正题。「高频」和「低频」是个相当宽泛的概念。要落地到电路设计，必须计算出具体数值。滤波器不会引起显著衰减的频率范围称为<strong>通带</strong>，反之则称为<strong>阻带</strong>。模拟滤波器（如 RC 低通滤波器）在滤波并不会像一堵墙一样瞬间把信号切断，而是像一个陡坡，让信号强度从通带逐渐衰减到阻带。这意味着我们很难在曲线上找到一个「绝对」的阻塞起点。在工程实践中，通常将信号强度衰减至输入端 -3 dB 处的频率定义为<strong>截止频率 (Cutoff Frequency)</strong>。这里使用<strong>分贝 (Decibel, dB)</strong> 来定量描述信号功率的损耗，这种对数单位在衡量系统增益或衰减时比线性数值更加直观。为什么偏偏是 -3 dB 呢？由这个单位的计算方法就能得出：
$$
L_{\mathrm{dB}} = 10 \log_{10} {\left( \frac{P_1}{P_0} \right)}
$$</p>
<p>$P_0$ 是输入功率，$P_1$ 是输出功率。当 $\tfrac{P_1}{P_0} = \tfrac{1}{2}$ 时，$L_{\mathrm{dB}} \approx -3.01 \text{ dB}$，正好对应信号功率衰减到最大功率的一半。在该点上，电阻的阻值等于电容的容抗，即：
$$R = X_C$$</p>
<p>代入容抗的计算公式，即可得到截止频率：
$$f = \frac{1}{2\pi RC}$$</p>
<h2>信号采样与分析</h2>
<p>为了对芯片输出的音频信号进行频域分析，需要准确采样信号。这里我用一块 USB 声卡配合 Adobe Audition 和示波器进行录制与分析。</p>
<p>![[img/Oscilloscope.avif|最近购入了一台普源 DHO814 四通道示波器，12bit 储存深度，最高 1.25GSa/s 的采样率。]]</p>
<p>操作时有个关键的技术细节，信号必须接在声卡的 <span style="color: #6cace4">LINE IN</span>（线路输入）接口上，而不是 <span style="color: #e68699">MIC IN</span>（麦克风输入），因为两者的电平定义完全不同。</p>
<p>与带有预放大功能的 MIC IN 不同，LINE IN 专为高电平信号设计。大多数的 PSG 芯片输出信号都会直接用于驱动小型的模拟电路或扬声器，它们通常在 0 V 到 5 V 之间做单极性跳变，峰峰值 $V_{p-p}$ 能达到 2 V 甚至 4 V。然而，消费级声卡预期的输入电平应该是 -10 dBV。这个单位是以 1 V 为基准参考的电压值。根据换算公式：
$$
\text{dBV} = 20\cdot\log_{10}\left(\frac{V}{1.000}\right)
$$</p>
<p>可以计算出对应的有效值电压约为 $0.316\text{ V}_\text{RMS}$.</p>
<p>$\text{ V}_\text{RMS}$ 指的是均方根 (Root Mean Square) 值，是衡量随时间变化的电压有效性的核心指标。比如正弦波为的有效值约等于峰值电压的 0.707 倍（即 $\tfrac{1}{ \sqrt{2} }$）；而理想方波信号的有效值就等于它的峰值。由此可见，芯片输出的原始信号强度远超过声卡的额定承受范围，直接输入会导致严重<strong>削波失真 (Clipping)</strong>，也就是在电平表上看到的「爆音」。</p>
<p>为了安全且完整地采集信号，需要设计一个简单的调理电路。芯片输出通常携带 2.5 V 左右的直流偏置，这会偏置声卡的输入级。首先串联一个电容器，它在电路中起到了<strong>高通滤波器</strong>的作用，只允许变化的音频成分通过，阻断直流成分。</p>
<p>随后再引入大电阻分压网络来进一步衰减信号强度，将 5 V 的 TTL 电平降压至 0.3 V 左右。这里就涉及到<strong>阻抗匹配 (Impedance Matching)</strong> 的概念。信号传输可以简化为一个串联分压模型：</p>
<p>![[img/Series_Module.avif|信号传输可以简化为一个串联分压电路。]]
其公式为：
$$
V_{in} = V_{src}\cdot\frac{ Z_{in} }{ Z_{out}+Z_{in} }
$$</p>
<p>其中，$V_{src}$ 和 $Z_{out}$ 为信号内部的理想电压和输出端的阻抗（源阻抗），而 $Z_{in}$ 和 $V_{in}$ 为输入端的阻抗（负载阻抗）和声卡实际接收到的电压。为了让声卡尽可能完整的接收到芯片产生的电压信号，即让 $V_{in}$ 尽可能接近 $V_{src}$，那么 $Z_{in}$ 应该尽可能大。当 $Z_{in}\to+\infty$ 时，$V_{in}=V_{src}$. 通俗点说，输出阻抗越低，信号源的「推力」就越强，信号也就越不容易在传输过程中损耗。</p>
<p>为了观察滤波器的实际效果，先录制一段未处理的音符 C4（注意音量大小）：</p>
<pre><code class="language-cpp">note.PlayNoteEvent(500, NOTE_C4, 15);
</code></pre>
<p>![[audio/unfiltered.wav]]</p>
<p>![[img/Waveform_Unfiltered.avif|未经处理的音频信号的波形。]]</p>
<p>随后，在信号输出端与地之间并联了一个 10nF 的陶瓷电容。这个元件与前级的限流电阻共同构成了一个一阶低通滤波器。在示波器上可以明显观察到，原本陡峭的方波边缘变得圆润了——这意味着高频谐波被削弱，信号在频域上的能量分布发生了显著变化。经过滤波的音频听上去要更「闷」一点。</p>
<p>![[audio/filtered.wav]]</p>
<p>![[img/Waveform_Filtered.avif|在信号输出端与地之间并联了一个电容。可以看到方波的上升沿顶端和下降沿底端形成了圆角。]]</p>
<p>由于输出端的内阻未知，可以通过逆向工程来获取系统的关键参数。利用示波器捕捉信号的阶跃响应。实测显示，信号的上升沿时间（10%-90%）$t_r$ 为 40 μs 左右。由于示波器的输入阻抗很大（1 MΩ），远大于芯片的输出内阻，其并联效应对波形的影响可以忽略不计。对于一阶 RC 滤波器，上升时间 $t_r$ 与时间常数 $\tau=RC$ 满足如下关系：
$$
t_r = 2.2\cdot \tau
$$</p>
<p>可得芯片的输出内阻约为 1.82 kΩ. 再代入截止频率的计算公式 $f_c=\tfrac{1}{2\pi RC}$ 可以推导出：
$$
f_c\approx\frac{0.35}{t_r}
$$</p>
<p>得截止频率 $f_c\approx8.75\text{ kHz}$. 这个结果和 Audition 中频谱分析展示的结果非常接近。可以看到，信号在 8 kHz 附近恰好产生了约 -3 dB 的衰减。</p>
<p>受限于<strong>奈奎斯特采样定律 (Nyquist Sampling Theorem)</strong>，以常见的音频采样率 44.1 kHz 为准，声卡中的抗混叠滤波器会强制切除 22 kHz 以上的所有能量。因此在接近 20 kHz 时开始急速衰减，并在 22 kHz 之后衰减到 -100 dB 以下。</p>
<p>![[img/Freq_Analysis.avif|Audition 中对波形进行频率分析后的对比图，中红色曲线代表滤波前，蓝色曲线则是滤波后的效果。]]</p>
<p>在频域的视角下，方波本质上是由无数正弦波叠加而成的复合信号。对于一个振幅为 $A$，频率为 $f_0$ 的理想方波，其傅里叶级数展开式为：
$$
f(t)=\frac{4A}{\pi}\sum_{n=1, 3, 5, \dots}^{\infty}\frac{1}{n}\sin(2\pi nf_0t)
$$</p>
<p>这个公式揭示了方波的三个本质：</p>
<ol>
<li>只包含奇次谐波：方波只包含基波（$f_0$）及其奇数倍频率（$3f_0, 5f_0, 7f_0 \dots$）；</li>
<li>振幅衰减：谐波次数越高，能量越弱。第 $n$ 次谐波的振幅是基波的 $1/n$；</li>
<li>相位对齐：所有的正弦波在零点同时出发，通过叠加抵消，最终在时域上构建出平整的顶部和陡峭的边缘。</li>
</ol>
<p>![[img/Square_Wave_FFT.avif|利用示波器上 FFT 运算得到的图像。为了让波峰更清晰，垂直刻度采用了 V<sub>RMS</sub>.]]</p>
<p>![[img/Fourier_Series_And_Transform.avif|函数 $s6(x)$（红色曲线）是 6 个谐波相关正弦波（蓝色曲线）的傅里叶级数求和结果。其傅里叶变换&nbsp;$S(f)$ 是一种频域表示，揭示了这些叠加正弦波的振幅分布。<a href="https://en.wikipedia.org/wiki/Fourier_series">Wikipedia</a>]]</p>
<p>在试图用有限项谐波拟合这一理想模型时，在跳变的边缘处总会出现一圈无法消除的振铃。这种现象被称为<strong>吉布斯现象 (Gibbs Phenomenon)</strong>。即便将谐波项数增加到更多，边缘过冲的幅度依然会锁定在 9% 左右。</p>
<p>现实世界中不存在绝对完美的方波。受限于系统的带宽限制，高频成分在传输中必然被截断，导致波形转角处不可避免地出现圆角或过冲。这也是为什么示波器捕捉到的信号，永远无法呈现出理论模型中那种绝对锐利的直角。</p>
<p>![[img/Gibbs_Phenomenon.avif|从上至下依次为使用 5, 25, 125 次谐波对方波进行函数逼近的效果。<a href="https://en.wikipedia.org/wiki/Gibbs_phenomenon">Wikipedia</a>]]</p>
<h2>总结</h2>
<p>这篇文章主要侧重于理论和电路上的知识。讲的东西已经够多了，从傅里叶变换到滤波器设计，最后再到信号采样与分析，虽然只是稍作介绍，途中也学到了不少新知识。此后将开始尝试解析 Tracker Music 的文件格式，并写出能够播放它们的驱动程序。对于一些有更多轨道的 chiptune，可能会考虑使用性能更强大的单片机，如 ESP32 等。</p>
<h2>扩展阅读</h2>
<ol>
<li>Fourier transform. <em>Wikipedia</em>. <a href="https://en.wikipedia.org/wiki/Fourier_transform">https://en.wikipedia.org/wiki/Fourier_transform</a></li>
<li>Fourier series. <em>Wikipedia</em>. <a href="https://en.wikipedia.org/wiki/Fourier_series">https://en.wikipedia.org/wiki/Fourier_series</a></li>
<li>Fast fourier transform. <em>Wikipedia</em>. <a href="https://en.wikipedia.org/wiki/Fast_fourier_transform">https://en.wikipedia.org/wiki/Fast_fourier_transform</a></li>
<li>Window function. <em>Wikipedia</em>. <a href="https://en.wikipedia.org/wiki/Window_function">https://en.wikipedia.org/wiki/Window_function</a></li>
<li>Spectral leakage. <em>Wikipedia</em>. <a href="https://en.wikipedia.org/wiki/Spectral_leakage">https://en.wikipedia.org/wiki/Spectral_leakage</a></li>
</ol>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>驱动 LCD12864 ST7920（二）：图像的显示</title>
					<link>https://studiountagged.top/articles/2025-10-27-LCD12864-ST7920-Disp-Img</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-10-27-LCD12864-ST7920-Disp-Img/</guid>
					<pubDate>Sun, 26 Oct 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							在 LCD 12864 ST7920 上显示图像。
Display an image on LCD12864 ST7920.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h2>前言</h2>
<p>在<a href="/articles/2025-09-11-LCD12864-ST7920/">上一篇文章</a>中，已经实现了驱动这块屏幕的基本函数，使用的主要是字符模式，即往显存中写入内建字库的字符地址。而在本文章中，则会使用它的图形模式来实现各种显示效果。</p>
<h2>硬件</h2>
<p>如果要显示图片之类的内容，则必须进入图形模式，这样单独控制每个像素点的亮灭。那么，要怎么才能进入图形模式呢？根据手册上的介绍，功能设置指令的第 2 位<code>RE</code>低电平为基本指令集，高电平为扩展指令集。而图形模式则仅能在启用了扩展指令集的时候才能被切换。启用之后，还需要将 bit1 的<code>G</code>切换为高电平。这样一来，就能启用图形模式了。请注意，bit4 的<code>DL</code>，bit2 的<code>RE</code>以及 bit1 的<code>G</code>不能同时设置。先要改变<code>DL</code>或<code>G</code>，才能再是<code>RE</code>。要输入的指令如下：</p>
<table>
<thead>
<tr>
<th><span>指令</span><span>Instruction</span></th>
<th align="center">RS</th>
<th align="center">RW</th>
<th align="center">D7</th>
<th align="center">D6</th>
<th align="center">D5</th>
<th align="center">D4</th>
<th align="center">D3</th>
<th align="center">D2</th>
<th align="center">D1</th>
<th align="center">D0</th>
</tr>
</thead>
<tbody><tr>
<td><span>功能设置</span><span>Function Set</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
</tr>
</tbody></table>
<p>这样，通过调用之前写好的函数<code>FuncSet()</code>即可切换成图形模式：</p>
<pre><code class="language-c">// Function Set command
enum DataLength &nbsp; &nbsp; { DL_8BIT &nbsp; = 1, DL_4BIT &nbsp; &nbsp;= 0 };
enum InstructionSet { EXTENDED &nbsp;= 1, BASIC &nbsp; &nbsp; &nbsp;= 0 };
enum GraphicMode &nbsp; &nbsp;{ GRAPHIC &nbsp; = 1, CHAR &nbsp; &nbsp; &nbsp; = 0 };

FuncSet(DL_8BIT, EXTENDED, GRAPHIC);
</code></pre>
<h2>软件</h2>
<p>那么，屏幕是如何显示图像的呢？ST7920 通过 GDRAM (Graphic Display RAM) 储存图像数据。但是内存与像素点的对应关系略有些复杂。总的来说，ST7920 是以 16 位（即两个字节）为最小单位进行存储和寻址的，对应控制屏幕上一个宽 16 px，高 1 px 的区域。而屏幕的数据位宽度只有 8 bit（一个字节），因此，每个地址需要进行两次写入操作。对于一个大小为 128 × 64 px 的屏幕，所有的像素点并非独立寻址，而是被划分成 $\frac{128}{16} \times \frac{64}{1} = 8 \times 64$ 个这样的小块。</p>
<p>![[img/LCD12864_Display_1.avif|屏幕上的像素点是以宽 16 px，高 1 px 的小块形式储存在 GDRAM 中的。每个小块都有唯一的地址。]]</p>
<p>ST7920 有个特殊之处，即整个屏幕的地址并非连续的，而是被拆分成两个独立区域。请注意，不同型号的屏幕拆分方式可能会有差异。笔者使用的这块屏幕是拆分成了上下两个区域，每个区域的大小为 128 × 32 px。有些屏幕会拆分成左右两块 64 × 64 px 大小的区域。因此实际操作时请务必查清楚手册。在这里面，上半屏的水平地址从<code>0x80</code>开始，递增至<code>0x87</code>；下半屏从<code>0x88</code>开始，递增至<code>0x8F</code>。而每个水平地址中又包含 32 个垂直地址，都是从<code>0x80</code>开始，<code>0x9F</code>结束。也就是说，定位一个 chunk 需要依次发送两条命令用来在 GDRAM 中定位。</p>
<p>![[img/LCD12864_Display_2.avif|屏幕每一小块的地址示意图。屏幕被分为上下两个部分，垂直地址从 0x80 开始递增至 0x9F，而位于同一行小块的列地址不变。水平地址上半部分从 0x80 开始，下半从 0x88 开始。]]</p>
<p>这些小块在 GDRAM 中有唯一的字节地址。当我们要显示图像时，只需先通过指令指定要操作的 GDRAM 地址，然后向该地址<strong>写入</strong>对应的数据，即可控制这些像素点的亮灭。根据手册，寻址操作需要依次输入两条命令分别设置垂直和水平地址：</p>
<table>
<thead>
<tr>
<th><span>指令</span><span>Instruction</span></th>
<th align="center">RS</th>
<th align="center">RW</th>
<th align="center">D7</th>
<th align="center">D6</th>
<th align="center">D5</th>
<th align="center">D4</th>
<th align="center">D3</th>
<th align="center">D2</th>
<th align="center">D1</th>
<th align="center">D0</th>
</tr>
</thead>
<tbody><tr>
<td><span>设置垂直 GDRAM 地址</span><span>Set Verticle GDRAM Addr</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>AC5</code></td>
<td align="center"><code>AC4</code></td>
<td align="center"><code>AC3</code></td>
<td align="center"><code>AC2</code></td>
<td align="center"><code>AC1</code></td>
<td align="center"><code>AC0</code></td>
</tr>
<tr>
<td><span>设置水平 GDRAM 地址</span><span>Set Horizontal GDRAM Addr</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>AC3</code></td>
<td align="center"><code>AC2</code></td>
<td align="center"><code>AC1</code></td>
<td align="center"><code>AC0</code></td>
</tr>
</tbody></table>
<p><code>ACx</code>就是地址计数器（Address Counter）。由此也可以看出，垂直地址的范围为<code>0x00-0x3F</code>，水平地址的范围为<code>0x00-0x0F</code>。现在就可以定义函数<code>SetGraphicAddr()</code>，传入小块所在的行和列的地址，以此来寻址：</p>
<pre><code class="language-c">// Set the Graphic RAM (GDRAM) Address
void SetGraphicAddr(uint8_t x, uint8_t y) {
&nbsp; &nbsp; // Set vertical address
&nbsp; &nbsp; WriteByte(INST, 0x80 | (y &amp; 0x3F), 10);
&nbsp; &nbsp; // Set horizontal address
&nbsp; &nbsp; WriteByte(INST, 0x80 | (x &amp; 0x0F), 10);
}
</code></pre>
<p>指令码中的<code>0x80</code>即将 bit7 设为<code>1</code>。随后，将<code>y</code>与<code>0x3F</code>取<code>&amp;</code>按位与运算。为什么要在此处进行这个运算呢？这种方法叫做<strong>位掩码（Bit Mask）</strong>，即选取要对哪些位进行操作。要操作的位就设为<code>1</code>，无需操作的就设为<code>0</code>。由于垂直地址的范围为<code>0x00-0x3F</code>，因此只需要对 bit0 - bit5 这 6 位进行操作。这样就算传入错误的数据，也不会向硬件发送无效地址。而对于<code>x</code>也是一样的，选取 bit0 - bit3 这 4 位进行操作。这样就可以准确地设定好要写入数据的地址了。</p>
<p>除了上面提到的内容以外，目前容易混淆的点还有如下几个：</p>
<ol>
<li>先写入的是高字节还是低字节？</li>
<li>先填充完高字节/低字节以后，另一半的数据要怎么写入？</li>
<li>指针会在完成一个水平地址的写入以后，自动指向下一个地址吗？</li>
</ol>
<p>这些问题其实手册上已经回答了，但是为了能够更加清晰地展示，笔者写了如下的测试程序：</p>
<pre><code class="language-c">void setup() {
&nbsp; &nbsp; Serial.begin(9600);
&nbsp; &nbsp; LCD_Reset();
&nbsp; &nbsp; LCD_Initialize();

&nbsp; &nbsp; LCD_FuncSet(DL_8BIT, EXTENDED, GRAPHIC);
&nbsp; &nbsp; Serial.println("Start Test...");

&nbsp; &nbsp; LCD_ClearGdram();
&nbsp; &nbsp; delay(1000);

&nbsp; &nbsp; // Phase 1: Phase 1: Higher byte test on addrX 0x80
&nbsp; &nbsp; LCD_SetGdramAddr(0x80, 0x80);

&nbsp; &nbsp; // Write first byte 0xFF
&nbsp; &nbsp; LCD_WriteByte(DATA, 0xFF, 100);
&nbsp; &nbsp; Serial.println("Phase 1: Written 1st Byte to X=0. Expect: Pixels 0-7 ON.");
&nbsp; &nbsp; delay(2000);

&nbsp; &nbsp; // Phase 2: Lower byte test on addrX 0x80
&nbsp; &nbsp; LCD_WriteByte(DATA, 0xF0, 100);
&nbsp; &nbsp; Serial.println("Phase 2: Written 2nd Byte to X=0. Expect: Pixels 8-15 show 11110000.");
&nbsp; &nbsp; delay(2000);

&nbsp; &nbsp; // Phase 3: Address automatically increment
&nbsp; &nbsp; LCD_WriteByte(DATA, 0xAA, 100); // 10101010
&nbsp; &nbsp; LCD_WriteByte(DATA, 0x55, 100); // 01010101
&nbsp; &nbsp; Serial.println("Phase 3: Written 2 Bytes to X=1 (Auto-increment). Expect: Pixels 16-31 show pattern.");
}
</code></pre>
<ul>
<li><strong>第一阶段</strong> 测试水平地址的高位。这会让X = 0 到 X = 7 处的像素全部点亮。</li>
</ul>
<p>![[img/Phase_1.avif|测试的第一阶段。X = 0 到 X = 7 处的像素全部点亮。]]</p>
<ul>
<li><strong>第二阶段</strong> 测试水平地址的低位。不再重新设置地址，而是接着写入。写入的数据是<code>0xF0</code>，可以看到 X = 8 到 X = 11 处的像素被点亮。</li>
</ul>
<p>![[img/Phase_2.avif|测试的第二阶段。X = 8 到 X = 11 处的像素被点亮。]]</p>
<ul>
<li><strong>第三阶段</strong> 测试地址自动递增。在不设置新地址的前提下，继续写入新的数据。可以看到屏幕上 X = 16 到 X = 23 处像素以<code>10101010</code>的方式点亮；X = 24 到 X = 31 处像素以<code>01010101</code>的方式点亮。</li>
</ul>
<p>![[img/Phase_3.avif|测试的第三阶段。X = 16 到 X = 23 处像素以<code>10101010</code>的方式点亮；X = 24 到 X = 31 处像素以<code>01010101</code>的方式点亮。]]</p>
<p>对于第一个问题，阶段二输入<code>1111 0000</code>，而结果是 X = 8 到 X = 11 处的像素，也就是这个字节内靠左的部分被点亮。这就可以说明像素是按照**大端序（Big Endian）**的方式组织的。</p>
<p>在这里插一嘴计算机中字节储存（Endianness），这是个基础又关键的概念，它定义了多字节数据在存储或传输时的排列顺序。这个名字的来源很有意思，源自英国作家乔纳森·斯威夫创作的长篇小说《格列佛游记》中小人国的居民因争论该从大端还是小端敲开鸡蛋而分成两派，引发了一场荒诞的战争，后来这个典故被计算机科学家 Danny Cohen 在他的论文《论圣战与求和 <em>On Holy Wars and a Plea for Peace</em>》中借用，用来命名多字节数据存储时的分歧，这也就是大端序和小端序的来历。[^1]简单来说，大端序就是一个数的最低位存在最高位的地址上，而小端序则恰恰相反。例如对于一个这样的整数<code>0xAABBCCDD</code>：</p>
<table>
<thead>
<tr>
<th><span>字节地址</span><span>Byte Address</span></th>
<th align="center">0</th>
<th align="center">1</th>
<th align="center">2</th>
<th align="center">3</th>
</tr>
</thead>
<tbody><tr>
<td><span>小端序</span><span>Small Endian</span></td>
<td align="center"><code>0xDD</code></td>
<td align="center"><code>0xCC</code></td>
<td align="center"><code>0xBB</code></td>
<td align="center"><code>0xAA</code></td>
</tr>
<tr>
<td><span>大端序</span><span>Big Endian</span></td>
<td align="center"><code>0xAA</code></td>
<td align="center"><code>0xBB</code></td>
<td align="center"><code>0xCC</code></td>
<td align="center"><code>0xDD</code></td>
</tr>
</tbody></table>
<p>回到正题，通过测试的结果，我们可以得出剩下的问题的答案：
r</p>
<ol>
<li>每个水平地址要写入两次，写入低八位时无需设置新的地址；</li>
<li>写完一个水平地址中的数据后，指针会自动指向下一地址。</li>
</ol>
<p>在研究清楚了屏幕的显示原理以后，接下来就是图像的数据。目前有很多将位图转化为二进制数据的取模软件，用什么软件就随意。在这个项目中，我是用的 <a href="https://javl.github.io/image2cpp/">image2cpp</a>这个项目，可以直接在浏览器中运行，无需额外安装，而且可以直接生成适用于 Arduino 的数据结构。笔者绘制了工作室 Logo 的像素画版本：</p>
<p>![[img/Studio_Untagged_Logo.avif|像素画版本的 Logo。尽可能地保留了原 Logo 的特色。]]</p>
<p>在 image2cpp 中打开要转换的位图以后，Image Settings 中还有一些设置。具体情况取决于想要的效果。由于位图是黑色背景的，因此要在这个设置里面将 Background color 设为 Black。同时在 Output 中也要将 Code output format 设为 Arduino Code, single bitmap。点击最下方的 Generate Code 按钮后就可以了。</p>
<p>其实严格来说，这类数据最好储存在 SD 卡或者 ROM 中，避免占用单片机过多的空间。关于如何烧录、读取 EEPROM，使用 SD 卡模块，以后会有文章进行介绍。由于篇幅限制，在本篇文章中将直接将这部分数据放在头文件<code>logo_image.h</code>中。生成的内容大致如下：</p>
<pre><code class="language-c">const unsigned char epd_bitmap_Sprite_0002 [] PROGMEM = {
&nbsp; &nbsp; 0x00, 0x00, 0x00, 0x00, 0x00, 
&nbsp; &nbsp; ...
};
</code></pre>
<p>这一坨东西就是用来存放位图数据的数组。诸位读者可能注意到了，这个数组后面还有个<code>PROGMEM</code>的关键字。这是什么意思呢？Arduino 上有几种不同类型的内存，这些内存读写速度以及容量各不相同，各司其职：</p>
<ul>
<li>闪存（Flash），读写较快，存储编译后的程序代码和<code>const</code>常量；</li>
<li>静态随机存取存储器（SRAM），读写速度最快的，用于储存程序运行时创建和修改的变量、对象实例、堆栈等；</li>
<li>电可擦除可编程只读存储器（EEPROM），读写最慢，负责储存永久性配置参数等。</li>
</ul>
<p>由于 SRAM 的容量有限，例如在 Arduino Mega 2560 上仅为 8 KB，因此储存了图像数据的数组不应占用 SRAM 的空间。而 Flash 的容量是最大的，有 256 KB，因此可以将数组存放在此处。<code>PROGMEM</code>的作用正是将数组存放到 Flash 中。如果要读取数据，方式会和 SRAM 略有不同。这点会稍后提到。</p>
<p>如图所示，每行的地址从<code>0x80</code>开始，按照垂直向下的方向递增到<code>0x9F</code>，一共 32 行。而同一行里面的小块共用同一个列地址，即<code>0x80</code>。其他的列也是如此。在下半部分的第一列中，其列地址为<code>0x88</code>。在写入数据时应将两半部分分开处理。</p>
<pre><code class="language-c">void Lcd_PrintImg(const unsigned char* bitmap) {
	// The upper part
	for (uint8_t i = 0; i &lt; 32; i++) {
		SetGraphicAddr(0x80, 0x80 + i);
		for (uint8_t j = 0; j &lt; 16; j++) {
			uint8_t data = pgm_read_byte(bitmap++);
			WriteByte(DATA, data, 10);
		}
	}

	// The lower part
	for (uint8_t i = 0; i &lt; 32; i++) {
		SetGraphicAddr(0x88, 0x80 + i);
		for (uint8_t j = 0; j &lt; 16; j++) {
			uint8_t data = pgm_read_byte(bitmap++);
			WriteByte(DATA, data, 10);
		}
	}
}
</code></pre>
<p>为了读取储存在 Flash 中的图像数据，须使用<code>pgm_read_byte()</code>。这个函数位于<code>&lt;avr/pgmspace.h&gt;</code>中。传参是一个地址。因此调用的时候传入<code>Lcd_PrintImg(epd_bitmap_Sprite_0002)</code>即可；而<code>bitmap++</code>的意思则是，当自增运算符<code>++</code>后置时，会先将<code>bitmap</code>当前的值传递给<code>pgm_read_byte()</code> 函数。函数执行完毕以后，才会对<code>bitmap</code>进行自增操作，这样就指针就自动指向了数组中下一字节的地址。</p>
<h2>最终效果</h2>
<p>![[img/Logo_LCD.avif|效果相当不错。]]</p>
<p>[^1]: <a href="https://en.wikipedia.org/wiki/Endianness">Endianness - Wikipedia</a></p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>独立游戏开发笔记（一）：序</title>
					<link>https://studiountagged.top/articles/2025-10-17-Indie-Game-Dev-Prologue</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-10-17-Indie-Game-Dev-Prologue/</guid>
					<pubDate>Thu, 16 Oct 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							独立游戏开发项目概述。
An overview of the indie game development project.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>早在中学时期，我和 Roy 就有制作独立游戏的想法。不过由于当时由于学业等原因迟迟未能动工，而且制作游戏所需的各个方面的技能，例如编程、配乐、美术等等更是毫无经验，当时只是在纸上记录了一些剧情、世界观等方面的设定；现在从中学的学业压力解放以后，终于有空来好好研究这方面的技术。</p>
<p>最开始的时候，游戏是打算通过 Unity 来制作的。不过才开始没学多久，Unity 就捅出了 Unity Runtime Fee 这个「惊天」大篓子——虽然以我们当时（也包括写这篇文章的时候）从游戏中获得的收益水平尚未到达费用征收范围，但是考虑到 Unity 高层对此次事件的态度，以及造成的负面影响等等方面，我们最终还是放弃了它。市面上还有其他不少相当优秀的引擎，比如 Unreal，不过对于我们的游戏来说简直就是大炮轰蚊子；Godot 也因为这次事件获得了很多关注。不过在本项目中，我们最终还是决定基于 SDL3 手搓开发游戏引擎以及游戏。选择 SDL3 的原因是因为：</p>
<ol>
<li>SDL3 足够成熟，市面上有不少基于它的项目，教程和论坛资源也很丰富；</li>
<li>很多游戏引擎的组件，从渲染到场景、摄像机等等都需要涉及到很多图形学知识，而这些内容现在就需要自己写，在此过程中就可以更好地学习和运用这些知识；</li>
<li>SDL3 让调用底层图形库流程变得更简单，无需写一大堆代码只为绘制一个三角形；</li>
<li>SDL3 是跨平台的，这样移植到其他平台上的开发工作就可以大大简化；</li>
<li>SDL3 免费开源，无需担心哪一天会突然冒出个什么什么 Fee🤣；</li>
</ol>
<p>本系列将以我的视角为主，Roy 作为联合作者来记录从零开始的开发中学习到的各种技术，以及自己的理解。以 SDL3 为主，同时也会包含 Godot；除此之外，还会有游戏的一些构想、开发时的一些幕后花絮等等。由于游戏引擎的内容涉及到方方面面，很难完全做到面面俱到。因此若有遗疏错漏还请见谅。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>驱动 AY-3-8910（一）</title>
					<link>https://studiountagged.top/articles/2025-09-28-AY-3-8910</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-09-28-AY-3-8910/</guid>
					<pubDate>Sat, 27 Sep 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							通过 Arduino 驱动 AY-3-8910。
Driving AY-3-8910 by Arduino.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h2>前言 Foreword</h2>
<p>早在计算机技术不甚发达的时候，想让电脑播放音频或者音乐是件很困难的事情。因为当时的数字采样技术技术并不成熟。如果追究音质，采样后的文件体积将变得十分庞大。这对于那个「寸金难买寸内存」的年代来说，直接在软件里使用简直是不可能的事情。最开始可能只有蜂鸣器发出的单调的滴滴答答。直到后来，才出现了只保存乐谱，剩下发出声音的工作交给电路的 Tracker Music 和 MIDI。这也就是电子音乐以及各种效果器、合成器等的起点。</p>
<p><a href="https://en.wikipedia.org/wiki/General_Instrument_AY-3-8910">AY-3-8910</a> 是一款由<a href="https://en.wikipedia.org/wiki/General_Instrument">通用仪器</a> (General Instrument, GI) 公司生产的 8-bit 可编程声音发生器 (Programmable Sound Generator, PSG)，内置三个通道，也就是说可以同时播放三种声音，同时还有一个噪声发生器以及一个<a href="https://en.wikipedia.org/wiki/Envelope_(music)">包络</a>发生器。在上个世纪 80 年代，广泛用于各种街机，例如 KONAMI 的 Gyruss；以及家用电脑，如 ZX Spectrum 中。[^1]</p>
<p>这个项目是以 <a href="https://github.com/internalregister/AY-3-8910/tree/master">InternalRegister</a> 的项目为基础改编而成。AY-3-8910 的手册请参见<a href="https://f.rdw.se/AY-3-8910-datasheet.pdf">此链接</a>。</p>
<h2>硬件准备 Hardware Preparation</h2>
<p>AY-3-8910 的元件引脚及功能如下表：</p>
<table>
<thead>
<tr>
<th align="center"><span>引脚</span><span>Pins</span></th>
<th align="center"><span>符号</span><span>Symbol</span></th>
<th align="center"><span>输入输出</span><span>IO</span></th>
<th align="center"><span>电平</span><span>Level</span></th>
<th><span>描述</span><span>Description</span></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>1</code></td>
<td align="center"><code>VSS</code></td>
<td align="center">-</td>
<td align="center">0V</td>
<td>接地</td>
</tr>
<tr>
<td align="center"><code>2</code></td>
<td align="center"><code>NC</code></td>
<td align="center">-</td>
<td align="center">-</td>
<td>悬空</td>
</tr>
<tr>
<td align="center"><code>3</code></td>
<td align="center"><code>ANALOG CHANNEL B</code></td>
<td align="center">O</td>
<td align="center">-</td>
<td>模拟信号输出通道 B</td>
</tr>
<tr>
<td align="center"><code>4</code></td>
<td align="center"><code>ANALOG CHANNEL A</code></td>
<td align="center">O</td>
<td align="center">-</td>
<td>模拟信号输出通道 A</td>
</tr>
<tr>
<td align="center"><code>5</code></td>
<td align="center"><code>NC</code></td>
<td align="center">-</td>
<td align="center">-</td>
<td>悬空</td>
</tr>
<tr>
<td align="center"><code>6-13</code></td>
<td align="center"><code>IOB7-IOB0</code></td>
<td align="center">I / O</td>
<td align="center">L / H</td>
<td>IO 接口 A</td>
</tr>
<tr>
<td align="center"><code>14-21</code></td>
<td align="center"><code>IOA7-IOA0</code></td>
<td align="center">I / O</td>
<td align="center">L / H</td>
<td>IO 接口 B</td>
</tr>
<tr>
<td align="center"><code>22</code></td>
<td align="center"><code>CLOCK</code></td>
<td align="center">I</td>
<td align="center">-</td>
<td>时钟信号</td>
</tr>
<tr>
<td align="center"><code>23</code></td>
<td align="center"><code style="text-decoration: overline">RESET</code></td>
<td align="center">I</td>
<td align="center">L</td>
<td>重置，低电平有效</td>
</tr>
<tr>
<td align="center"><code>24</code></td>
<td align="center"><code>A9</code></td>
<td align="center">I</td>
<td align="center">L / H</td>
<td>地址 9</td>
</tr>
<tr>
<td align="center"><code>25</code></td>
<td align="center"><code>A8</code></td>
<td align="center">I</td>
<td align="center">L / H</td>
<td>地址 8</td>
</tr>
<tr>
<td align="center"><code>26, 39</code></td>
<td align="center"><code>TEST 2, TEST 1</code></td>
<td align="center">-</td>
<td align="center">-</td>
<td>测试用引脚，悬空</td>
</tr>
<tr>
<td align="center"><code>27</code></td>
<td align="center"><code>BDIR</code></td>
<td align="center">I</td>
<td align="center">L / H</td>
<td>总线方向</td>
</tr>
<tr>
<td align="center"><code>28</code></td>
<td align="center"><code>BC2</code></td>
<td align="center">I</td>
<td align="center">L / H</td>
<td>总线控制 2</td>
</tr>
<tr>
<td align="center"><code>29</code></td>
<td align="center"><code>BC1</code></td>
<td align="center">I</td>
<td align="center">L / H</td>
<td>总线控制 1</td>
</tr>
<tr>
<td align="center"><code>30-37</code></td>
<td align="center"><code>DA7-DA0</code></td>
<td align="center">I / O / 高阻抗</td>
<td align="center">L / H</td>
<td>数据 / 地址总线 0-7</td>
</tr>
<tr>
<td align="center"><code>38</code></td>
<td align="center"><code>ANALOG CHANNEL C</code></td>
<td align="center">O</td>
<td align="center">-</td>
<td>模拟信号输出通道 C</td>
</tr>
<tr>
<td align="center"><code>40</code></td>
<td align="center"><code>VCC</code></td>
<td align="center">-</td>
<td align="center">+5V</td>
<td>+5V 电源</td>
</tr>
</tbody></table>
<p>一共 40 个引脚，看起来貌似相当复杂，有点不知从何下手。但是仔细观察就会发现，IO 接口占了很大一部分。这些接口是用来与外部设备通信的，并不会影响到音频的功能，一般也很少用到。这也就是为什么这款芯片的亲戚 AY-3-8912 砍掉了一半的 IO 接口。本项目中不会有这种需求，因此全部悬空即可。除此之外，还有几个引脚标注了<code>NC</code>和<code>TEST</code>，这些也可以直接悬空。</p>
<h3>输入引脚 Input Pins</h3>
<p>剩下的引脚才是真正要用到的。电源、接地和 8 位的数据 / 地址总线自不用说。先来看用于输入的引脚。第 22 号引脚<code>CLOCK</code>是<strong>时钟信号输入</strong>。芯片若要发生波形，就必须借助时钟信号这种有周期性的方波信号来完成控制时序、调制波形等操作。时钟信号可以通过<a href="https://en.wikipedia.org/wiki/Crystal_oscillator">晶振</a>来提供；不过在本项目中则会使用 Arduino 来生成。具体的生成方法将在本文后面进行详细介绍。</p>
<p>第 23 号引脚<code>RESET</code>是<strong>复位信号输入</strong>，低电平有效。复位时会将芯片所有的寄存器置 0。</p>
<p>第 24 和 25 号引脚<code>A9</code>, <code>A8</code>是地址线。这两个地址线的作用是什么呢？<code>DA0-DA7</code>这 8 个引脚既可以作输入数据，也可以输入地址。当作为地址线使用的时候，最多可以被映射成 $2^{8} = 256$ 个地址。而如果再加上这两根额外的地址线，就可以映射成 $2^{10} = 1024$ 个地址。这个在有很多 IO 设备，或者同时使用很多块这种芯片时可以扩展地址的识别范围。简单来说，就是选择要用的芯片。不过说了这么多，本项目其实并不会用上。因此只需将<code>A9</code>接地，<code>A8</code>接电源即可。</p>
<p>接下来的第 27，28，29 号引脚<code>BDIR</code>, <code>BC2</code>, <code>BC1</code>引脚用于控制总线。虽然三个引脚一共有 8 种电平的组合，手册给出了一个简化的版本，即保持<code>BC2</code>为高电平，剩下的就交给<code>BDIR</code>和<code>BC1</code>控制。芯片生成的音调、音量以及噪声和包络发生都要通过向寄存器内写入数据才能控制，而在这之前还得先选择锁存器的地址。这一点在后面也会详细介绍。</p>
<h3>输出引脚 Output Pins</h3>
<p>OK，输入的部分介绍完了，接下来就是输出的部分。AY-3-8910 提供了三个通道输出，分别为第 3，4 以及 38 号引脚的 B，A，C 模拟输出。这些引脚内置了数模转换器，因此可以不需要再外接其他的转换电路。输出的功率可以直接带动入耳式的耳机，不过可以加个简单的滤波或者功放模块。</p>
<p>![[img/AY-3-8910.avif|在 Proteus 中绘制的电路图。输出部分的滤波电路基于 <a href="https://github.com/GadgetReboot/AY-3-8910/tree/main">GadgetReboot</a> 的设计。虽然图中用的 PSG 芯片标注的是 YM2149，和 AY-3-8910 相比，唯一的区别就在于第<code>26</code>号引脚变成了<code>SEL</code>。不过根据手册上的描述，这个引脚也可以直接悬空。因此这两个芯片是完全兼容的。]]</p>
<p>![[img/Connection.avif|实物连接图。]]</p>
<h2>程序编写 Programming</h2>
<h3>生成时钟信号 Generating Clock Signal</h3>
<p>笔者使用的 Arduino Mega 2560 采用的是基于 AVR 内核的 ATmega2560 处理器。在 AVR 内核中，一共有 3 个计时器，两个 8-bit (Timer 0 和 Timer 2)，以及一个 16-bit (Timer 1) 分辨率的计时器，用于实现与时间有关的操作时，如生成时钟信号、定时中断、频率测量以及 PWM 等。如果要生成 2 MHz 的时钟信号，基本的思路是这样的：Arduino Mega 2560 的时钟频率为 16 MHz，通过设置内部的计数器，让它数到某个数时执行翻转输出引脚上的电平的操作，同时清零为下半个周期做准备。而在这个过程中，将用到如下寄存器：</p>
<table>
<thead>
<tr>
<th align="center"><span>寄存器</span><span>Register</span></th>
<th><span>功能</span><span>Function</span></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>TCNT</code></td>
<td>计时器 / 计数器寄存器Timer / Counter Register</td>
</tr>
<tr>
<td align="center"><code>TCCR</code></td>
<td>计时器 / 计数器控制寄存器Timer / Counter Control Register</td>
</tr>
<tr>
<td align="center"><code>OCR</code></td>
<td>输出比较寄存器Output Compare Register</td>
</tr>
</tbody></table>
<p>其中用来进行计时或计数的就是<code>TCNT1</code>，而控制计数器工作模式的就是<code>TCCR1A</code>和<code>TCCR1B</code>。<code>OCR1A</code>就是设置计数器从 0 开始要数到的目标数。<code>TCCR1A</code>和<code>TCCR1B</code>是怎么配置计数器的工作模式呢？这两个寄存器一共有 8 个控制位，每个控制位的功能如下：</p>
<table>
<thead>
<tr>
<th align="center"><span>位</span><span>Bit</span></th>
<th align="center"><span>TCCR1A</span></th>
<th><span>描述</span><span>Description</span></th>
<th align="center"><span>TCCR1B</span></th>
<th><span>描述</span><span>Description</span></th>
</tr>
</thead>
<tbody><tr>
<td align="center"><code>7</code></td>
<td align="center"><code>COM1A1</code></td>
<td>Compare Output Mode for Channel A</td>
<td align="center"><code>ICNC1</code></td>
<td>Input Capture Noise Canceler</td>
</tr>
<tr>
<td align="center"><code>6</code></td>
<td align="center"><code>COM1A0</code></td>
<td></td>
<td align="center"><code>ICES</code></td>
<td>Input Capture Edge Select</td>
</tr>
<tr>
<td align="center"><code>5</code></td>
<td align="center"><code>COM1B1</code></td>
<td>Compare Output Mode for Channel B</td>
<td align="center">-</td>
<td></td>
</tr>
<tr>
<td align="center"><code>4</code></td>
<td align="center"><code>COM1B0</code></td>
<td></td>
<td align="center"><code>WGM13</code></td>
<td></td>
</tr>
<tr>
<td align="center"><code>3</code></td>
<td align="center"><code>COM1C1</code></td>
<td>Compare Output Mode for Channel C</td>
<td align="center"><code>WGM12</code></td>
<td></td>
</tr>
<tr>
<td align="center"><code>2</code></td>
<td align="center"><code>COM1C0</code></td>
<td></td>
<td align="center"><code>CS12</code></td>
<td>Clock Select</td>
</tr>
<tr>
<td align="center"><code>1</code></td>
<td align="center"><code>WGM11</code></td>
<td>Wave Generation Mode</td>
<td align="center"><code>CS11</code></td>
<td></td>
</tr>
<tr>
<td align="center"><code>0</code></td>
<td align="center"><code>WGM10</code></td>
<td></td>
<td align="center"><code>CS10</code></td>
<td></td>
</tr>
</tbody></table>
<p>我们首先来看<code>WGM10</code>到<code>WGM13</code>这 4 位。这是用来控制<strong>波形生成模式</strong>的，一共有 16 种模式，其中大部分都是 PWM。在前文中提到过，计时的本质就是一个累加的计数器，因此我们要选择的模式是 CTC (Clear Timer on Compare Match) 模式，即计数器的值与目标数匹配时清零，重新开始。</p>
<p>![[img/WGM.avif|波形生成模式控制位描述。]]</p>
<p>诸位读者可能注意到了，表格中的模式 4 和 12 的行为都是 CTC，而唯一不同的地方在于 TOP 那一列。模式 4 为<code>OCRnA</code>，模式 12 为<code>ICRn</code>。这两个有什么区别呢？简单来说，TOP 的意思就是计数器的终点值。<code>OCR</code>是<strong>输出比较寄存器 (Output Compare Register)</strong>。这个寄存器会被写入一个特定的值来指定计数的终点；而<code>ICR</code>是<strong>输入捕获寄存器 (Input Capture Register)</strong>。当外部的输入信号发生变化时，这个寄存器会立马记录下此时计数器的值作为计数的终点。打个比方，<code>OCR</code>就像是计时器，按照人为设定好的时间来周期性地计时；而<code>ICR</code>则相当于秒表，测量外部事件发生的时间。时钟信号是一种周期性信号，因此应该选择模式 4，将<code>WGM12</code>这一位设置为<code>1</code>，其他的<code>WGM</code>位设为<code>0</code>。</p>
<p>接着再来看看<code>COM1A1</code>和<code>COM1A0</code>这两位。这两位用于<strong>比较输出控制</strong>，控制当计数器数到终点时，对应的输出引脚该怎么办。在比较输出模式，非 PWM的情况下，手册上给出了如下几种输出行为：</p>
<p>![[img/Compare_Output_Mode.avif|比较输出模式，非 PWM。]]</p>
<p>当<code>COM1A1 = 0, COM1A0 = 1</code>时，输出行为就是当匹配时翻转<code>OC1A / OC1B / OC1C</code>上的电平，这正是生成方波的关键。</p>
<p><code>CS10-CS12</code>这 3 位用于选择时钟，里面预设了很多档位的分频 (prescaler)。如果以处理器 16 MHz 的时钟信号为基准，虽然在 8 分频的情况下正好是 $16 \div 8 = 2 \mathrm{MHz}$，不过由于真正的输出频率还得取决于计数器，因此不分频，将<code>CS10</code>设为<code>1</code>，其余位为<code>0</code>。</p>
<p>![[img/Clock_Select.avif|时钟选择位描述。]]</p>
<p>时钟信号将在<code>OC1A</code>引脚上输出。根据 Arduino 的引脚图，<code>D11</code>引脚的<span style="color: #00959C">青色</span>背景文字标注着<code>OCA1</code>。请注意，虽然基于 AVR 核心的处理器，例如 ATmega328P 等，也有相同的用于计时的寄存器，不过对于引脚的映射会略有不同。如果使用的是其他型号的开发板，请务必确认引脚的映射关系。</p>
<p>![[img/Arduino_Mega_2560_Pinout_1.avif|Arduino Mega 2560 的引脚图。请注意青色背景与橘红色条纹背景标注之间的关系。]]</p>
<p>最后一步就是设定计数器的终点<code>OCR1</code>的值。这个值该怎么计算呢？诸位读者不妨跟着下面的过程推导一下。首先，每个脉冲的时长为：</p>
<p>$$
T_{tick} = \frac{1}{f_{CLK}}
$$</p>
<p>由于 Timer 1 是从 0 开始计数的，所以总共<code>OCR1A + 1</code>个计数周期。每次<code>OC1A</code>翻转所需时间：</p>
<p>$$
T_{toggle} = (\mathrm{OCR1A} + 1) \cdot T_{tick} = \frac{\mathrm{OCR1A} + 1}{ f_{CLK} }
$$</p>
<p>而一个完整的方波周期需要两次翻转，意味着需要经历两个计数周期：</p>
<p>$$
T_{period} = 2 \cdot T_{toggle} = 2 \cdot (\mathrm{OCR1A} + 1) \cdot \frac{1}{f_{CLK}}
$$</p>
<p>最终输出公式：</p>
<p>$$
f_{out} = \frac{1}{T_{period}} = \frac{f_{CLK}}{2 \cdot (\mathrm{OCR1A} + 1)}
$$</p>
<p>当 $f_{out} = 2 \ \mathrm{MHz}, \ f_{CLK} = 16 \ \mathrm{MHz}$ 时，代入可解得 $\mathrm{OCR1A} = 3$。</p>
<p>其实说了这么多，要写的代码也就五行。因为这些涉及到了相当底层的操作，笔者也是边学边写，因此会尽可能记录得详细一点。</p>
<pre><code class="language-cpp">#define CLOCKOUT 11

TCCR1A = bit(COM1A0);
TCCR1B = bit(WGM12) | bit(CS10);
OCR1A = 3;
pinMode(CLOCKOUT, OUTPUT);
</code></pre>
<h3>驱动芯片 Driving the Chip</h3>
<p>能够生成稳定的时钟信号以后，接下来就是驱动这块芯片了。这块芯片的各种功能都是通过写寄存器实现的。</p>
<p>![[img/PSG_Register_Array.avif|芯片的寄存器阵列。]]</p>
<p>由于这块芯片是寄存器地址和数据共用，因此得先通过<code>BDIR</code>和<code>BC1</code>来指定总线的读写方向：</p>
<table>
<thead>
<tr>
<th><code>BDIR</code></th>
<th><code>BC2</code></th>
<th><code>BC1</code></th>
<th><span>PSG 功能</span><span>PSG Function</span></th>
</tr>
</thead>
<tbody><tr>
<td><code>0</code></td>
<td><code>1</code></td>
<td><code>0</code></td>
<td>非活动 INACTIVE</td>
</tr>
<tr>
<td><code>0</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
<td>从 PSG 读取 READ FROM PSG</td>
</tr>
<tr>
<td><code>1</code></td>
<td><code>1</code></td>
<td><code>0</code></td>
<td>向 PSG 写入 WRITE TO PSG</td>
</tr>
<tr>
<td><code>1</code></td>
<td><code>1</code></td>
<td><code>1</code></td>
<td>设置寄存器地址 LATCH ADDRESS</td>
</tr>
</tbody></table>
<p>那么，首先定义一个函数<code>SetPsgMode()</code>来切换模式：</p>
<pre><code class="language-cpp">enum PSGMode { INACTIVE, READ, WRITE, LATCH };

// Set the PSG function
void SetPsgMode(enum PSGMode mode) {
&nbsp; &nbsp; switch (mode) {
&nbsp; &nbsp; &nbsp; &nbsp; case INACTIVE:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BC1, LOW);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BDIR, LOW);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;

&nbsp; &nbsp; &nbsp; &nbsp; case READ:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BC1, HIGH);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BDIR, LOW);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;

&nbsp; &nbsp; &nbsp; &nbsp; case WRITE:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BC1, LOW);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BDIR, HIGH);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break;

&nbsp; &nbsp; &nbsp; &nbsp; case LATCH:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BC1, HIGH);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(BDIR, HIGH);
&nbsp; &nbsp; }
}
</code></pre>
<p>按照手册上的描述，写入数据的流程应该是这样的：非活动 → 写入地址 → 非活动 → 写入数据 → 非活动。而<code>DA0-DA7</code>上写入数据的脉冲宽度应在 500 到 10,000 ns 之间。由此就构成了用于写入的函数<code>WriteByte()</code>：</p>
<pre><code class="language-cpp">// Write data into the specified register
void WriteByte(uint8_t reg, uint8_t data) {
&nbsp; &nbsp; SetPsgMode(LATCH);
&nbsp; &nbsp; PORTF = reg;
&nbsp; &nbsp; delayMicroseconds(10);
&nbsp; &nbsp; SetPsgMode(INACTIVE);

&nbsp; &nbsp; SetPsgMode(WRITE);
&nbsp; &nbsp; PORTF = data;
&nbsp; &nbsp; delayMicroseconds(10);
&nbsp; &nbsp; SetPsgMode(INACTIVE);
}
</code></pre>
<p>这块芯片有 A，B，C 三个通道，而每个通道可以独立开关，或者控制音调 (Tone，或周期 Tone Period)、音量 (Volume，或振幅 Amplitude) 以及包络 (Envelope) 形状、周期；同时，也可以选择一个独立的噪声发生器和其中一个通道混合在一起输出。在本篇文章中暂时不会用到噪声和包络，前者常用于模仿鼓点或者爆炸等音效，后者则会改变输出音符的效果等。目前的任务是能够成功驱动这块芯片，并播放出简单的音符，譬如在通道 A 上播放 C4-B4 这七个音。</p>
<p>先看通道 A 的输出是怎么控制的。首先，<code>R0</code>和<code>R1</code>这两个寄存器用来调整输出的音调。<code>R0</code>用 8 位来微调 (Fine Tune)，再加上<code>R1</code>的前 4 位粗调 (Coarse Tune)，总共 12 位。其实和处理器输出时钟信号的原理如出一辙，这里的这 12 位本质上也是个计数器。计数器将从给定的值开始倒数，直到倒数到零时翻转输出引脚上的电平，并重新开始。那么，要传入寄存器的值该如何计算呢？手册上已经很贴心地给出了公式：</p>
<p>$$
f_{T} = \frac{f_{CLK}}{16 \cdot \mathrm{TonePeriod}}
$$</p>
<p>其中，$f_{CLK}$ 是时钟频率。由于芯片内部还有个 16 分频器，所以要除以 16；$f_T$ 是要生成的频率，$\mathrm{TonePeriod}$ 是要传入寄存器的值。例如，如果想生成 C4 (262 Hz) 这个音调的话，代入解得 $\mathrm{TonePeriod} \approx 477$。</p>
<p>网上有很多地方给出了音名和频率的对照关系表，手册上也有基于 1.78977 MHz 时钟频率计算出的应该写入寄存器的值。基于此可以生成一个头文件专门储存音名和要写入寄存器的值的对应关系。这样的好处就是每次要用什么音符直接调用它的名字即可，不仅可读性更高，而且可以比储存音名和频率的对应关系这种方案快一些，因为不需要每次再去计算要写入的值。不过，如果使用了不同的频率，最终输出会略有移调(其实也并无大碍，因为我们听音乐本质上听的是音程关系而非频率)。</p>
<p>芯片是通过 12 位来控制音调的，低 8 位要写入<code>R0</code>，高 4 位要写入<code>R1</code>，因此可以先定义一个 16 位的变量<code>uint16_t tone</code>。而要取它的低 8 位和高 4 位，可以配合逻辑与和右移操作实现，最后再强制类型转为为 8 位的数据。而音量则通过<code>R10</code>寄存器来控制，其中 bit0 - bit4 为音量的值(0-15)，bit5 为静音。最后，再通过 <code>delay()</code>来控制音符播放的时间：</p>
<pre><code class="language-cpp">// Play the notes according to the specified parameters
void PlayNoteEvent(int duration, uint16_t tone, uint8_t volume) {
&nbsp; &nbsp; uint8_t r0 = (uint8_t) tone &amp; 0xFF; // The value of Register 0
&nbsp; &nbsp; uint8_t r1 = (uint8_t) (tone &gt;&gt; 8) &amp; 0x0F; // The value of Register 1

&nbsp; &nbsp; WriteByte(0, r0 &amp; 0xFF);
&nbsp; &nbsp; WriteByte(1, r1 &amp; 0x0F);
&nbsp; &nbsp; WriteByte(8, volume);

&nbsp; &nbsp; delay(duration);
}
</code></pre>
<p>初始化并启用通道 A 的输出。在<code>R7</code>寄存器中，bit0 就是控制通道 A 是否启用，低电平有效，因此将其设为<code>0</code>，其他设为<code>1</code>即可：</p>
<pre><code class="language-cpp">WriteByte(7, 0b00111110); // Enable channel A tone output
</code></pre>
<h3>拓展：封装类 Encapsulating Classes</h3>
<p>在此前的文章<a href="/articles/2025-09-11-LCD12864-ST7920/">驱动 LCD12864 ST7920(一)</a>中，是将整个驱动过程，划分成一个个的小步骤完成的。这个就是<a href="https://en.m.wikipedia.org/wiki/Procedural_programming">面向过程编程 (Procedural Programming)</a>。这种编程方式的核心就是函数。例如，假设现在我们要完成「把大象放进冰箱里」，一共三步，即打开冰箱门、把大象放进去以及关闭冰箱门。在面向过程编程中，这三个步骤就会被封装成三个函数，然后由主函数依次调用。这种编程方式在早期的编程语言 例如 C，BASIC，Pascal 中尤为常见。</p>
<p>而在本项目中，将使用另外一种思维方式，即<a href="https://en.m.wikipedia.org/wiki/Object-oriented_programming">面向对象编程 (Object-Oriented Programming)</a>。比起前者的将一个问题拆分为很多很多的步骤，这种则是将问题的各种属性、方法等封装在一起，抽象成一个<strong>类 (Class)</strong>。例如，在使用 C# 开发的游戏中，如果要控制角色的行为，通常都会通过它们的类实例化成一个可供操作的<strong>对象 (Object)</strong>。这个对象里面将包含各种譬如生命值、攻击力、防御力等属性，以及移动、攻击、受伤等方法。这样，当角色移动或受伤时，只需通过调用这个对象里面的方法，就可以控制角色的坐标或生命值。</p>
<p>这种编程方式的好处就是在一些大型项目中可以选择只暴露必须的接口，避免了函数过多导致的混乱；而且类也可以被继承，便于扩展和复用，例如在 Unity 中，脚本都会默认继承自一个叫做<code>MonoBehaviour</code>的类，这个类里面就已经预先封装好了比如初始化、每帧调用等方法。这里我不会展开太多，不过以后应该也会有独立游戏开发笔记的文章。回到正题，现代有很多编程语言都是面向对象的，例如 C++，C# 等；这也是 C++ 和 C 的主要区别之一。</p>
<p>那么该如何将目前写的代码改为面向对象的形式呢？首先在头文件<code>ay38910.h</code>中通过<code>class</code>声明<code>AY38910Player</code>这个类。这个将用于描述所有播放器共有的属性和行为。类里面的各种变量或方法统称为<strong>成员 (Member)</strong>。</p>
<p>面向对象编程中有个很重要的概念就是<strong>动态成员 (Dynamic Member)</strong> 和 <strong>静态成员 (Static Member)</strong>。那么，这两个究竟是什么意思呢？举个例子，由于 AY-3-8910 这块芯片有三个通道，因此每个通道播放的音符、持续的时间、音量等会不一样。这些会变化的内容就被称为动态成员。而静态成员就指那些每个对象共享的、不依赖于对象的。例如默认的 BPM 就可以看作是一个静态成员变量；而设置 BPM 的方法就是一个静态成员函数。</p>
<p>在一个类里面，可以通过<code>public</code>和<code>private</code>来设置权限，选择哪些成员是可以公开访问，哪些只能由同一个类的成员访问。例如，假设我不希望暴露<code>WriteByte</code>这个方法：</p>
<pre><code class="language-cpp">// ay38910.h

class AY38910Player {
	public:
		void SetPsgMode(enum PSGMode mode);
		void Initialize();
		void PlayNoteEvent(int duration, uint16_t tone, uint8_t volume);
	
	private:
		void WriteByte(uint8_t reg, uint8_t data);
}
</code></pre>
<p>这样，<code>WriteByte()</code>就只能通过由公开的方法间接访问了，避免了因为写入了错误的数据而直接无法工作。</p>
<p>不过这还没完。如果要在外部实现类里面的成员函数，例如<code>ay38910.cpp</code>，则需要通过作用域解析运算符<code>::</code>，来明确它们所属的作用域。例如：</p>
<pre><code class="language-cpp">// ay38910.cpp

#include "ay38910.h"
void AY38910Player::SetPsgMode(enum PSGMode mode) {}
void AY38910Player::WriteByte(uint8_t reg, uint8_t data) {}
</code></pre>
<p>目前，还只是定义了类以及实现方法。对于静态成员，可以直接通过<code>::</code>访问，比如<code>AY38910Player::defaultBpm</code>。而对于动态成员，则必须先<strong>实例化 (Instantiation)</strong> 成对象。打个比方，类就相当于一份蓝图，描述了某物的结构和行为，没有实际数据；而对象就是根据蓝图制造出来的机械或建筑，有实际功能，会在内存中分配空间储存成员变量。</p>
<p>一般实例化对象有两个方法。方法一：栈实例化。这是最简单、也最常用的方法。<strong>栈 (Stack)</strong> 是指程序运行时在内存中开辟的一小块固定的空间，用来取存一些局部和临时数据：</p>
<pre><code class="language-cpp">AY38910Player player;
</code></pre>
<p>现在，<code>player</code>就是一个<code>AY38910Player</code>对象。要访问这个对象里的成员，就可以通过成员访问运算符<code>.</code>：</p>
<pre><code class="language-cpp">player.SetPsgMode(INACTIVE);
</code></pre>
<p>栈会自动管理内存分配，一般用于局部变量、函数参数、栈实例化的对象等。而如果有一个巨大的数据容器，比如储存整首曲子的信息时，就不推荐使用栈，因为它的空间有限，过大的数据可能会造成栈溢出(Stack Overflow)。因此，对于这样的数据，或者是生命周期更长的，要使用方法二来实例化。</p>
<p>方法二：堆实例化。与栈不同的是，<strong>堆 (Heap)</strong> 是一块更大一点的空间，由操作系统动态分配。使用的时候必须得手动通过<code>new</code>分配，以及最后用<code>delete</code>释放。在分配时，<code>new</code>操作符返回的是一个指向为这个对象开辟的内存空间的地址。这也是和栈实例化不同的一点，因为后者可以直接访问。这样，就需要用指针通过成员访问运算符<code>-&gt;</code>来调用对象里的方法：</p>
<pre><code class="language-cpp">AY38910Player* playerPtr = new AY38910Player();
playerPtr -&gt; SetModePsg(INACTIVE);
delete playerPtr;
</code></pre>
<p>那么，为什么堆需要手动释放内存呢？由于栈是位于一块固定的区域，而且里面的分配情况都会自动管理。例如在函数结束以后，临时数据等所占用的空间就会被自动释放；而通过<code>new</code>创建对象时，如果没有手动释放内存，那么程序在运行过程中就丢失了对这块堆内存的控制权，既不能被访问、也不能被重新分配给其他程序，而这就是<strong>内存泄漏 (Memory Leak)</strong>。所以，请务必记得在最后要释放内存。</p>
<p>不过，在 C++ 11 的标准中，提供了<strong>智能指针 (Smart Pointer)</strong><code>std::unique_ptr</code>[^2]，包含在头文件<code>&lt;memory&gt;</code>中。可以通过如下方式来创建：</p>
<pre><code class="language-cpp">std::unique_ptr&lt;AY38910Player&gt; playerPtr = 
	std::make_unique&lt;AY38910Player&gt;();
</code></pre>
<p>通过这种方式封装的数据，其占用的内存将在函数销毁或超出作用域（即一对大括号包裹的区域）时自动释放。这是一个更好的 C++ 实践，因为它更安全，能防止某些异常场景下的内存泄漏，而且更高效。关于这点将在下一篇文章中进行更详细的介绍。</p>
<h2>后记 Epilogue</h2>
<p>为了驱动这块芯片，这个过程说不上一帆风顺，总会出些电路或者软件上的小毛病。光是调试就花费了不少时间。不过，当听到从扬声器里播放出预期的音符时的那种喜悦很快就将调试时的种种阴翳一扫而空。这个过程中也学到了很多很多的新知识。不仅仅是硬件和编程上的，关于如何采集芯片的输出信号、改编曲子、解析 Tracker Music 文件结构等，就涉及到了信号处理、乐理等内容了。在下篇文章中，我将会介绍如何制作一个简单的 Tracker Music 播放器。</p>
<p>[^1]: <a href="https://en.wikipedia.org/wiki/General_Instrument_AY-3-8910">General Instrument AY-3-8910 - Wikipedia</a>
[^2]: <a href="https://cppreference.cn/w/cpp/memory/unique_ptr">std::unique_ptr - cppreference.cn - C++参考手册</a></p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>驱动 LCD12864 ST7920（一）</title>
					<link>https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/</guid>
					<pubDate>Wed, 10 Sep 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							通过 Arduino 驱动 LCD12864 ST7920。
Driving LCD12864 ST7920 by Arduino.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h2>前言</h2>
<p>继上次的成功驱动了 LCD1602 以后，这几天又入手了一块 LCD12864。之所以叫 LCD12864，是因为它的屏幕是由 128 * 64 大小的点阵构成的。本文章将以学习笔记加个人想法的形式，记录使用 Arduino Mega 2560 驱动它的过程，因此很多内容会写得相当细致。同样，为了更好地理解底层的工作原理，这个项目将不会借助譬如 U8glib/U8g2 这样的库，而是会通过自己编写控制 LCD12864 的各种命令和函数。</p>
<h2>硬件准备</h2>
<p>第一步还是一样的，弄清楚每个引脚的功能。不过需要注意的是，LCD12864 有很多变种，这些变种通常在引脚以及其功能上会有差异。笔者使用的这款驱动芯片是 ST7920。常见的芯片种类与差异如下表（由 Grok 汇总）[^1][^2][^3][^4]：</p>
<table>
<thead>
<tr>
<th><span>芯片型号</span><span>Chip Model</span></th>
<th>ST7920</th>
<th>ST7565</th>
<th>T6963C</th>
<th>KS0108</th>
</tr>
</thead>
<tbody><tr>
<td>制造商</td>
<td>Sitronix</td>
<td>Sitronix</td>
<td>Toshiba</td>
<td>Samsung</td>
</tr>
<tr>
<td>电压范围</td>
<td>2.7V ~ 5.5V (兼容 3.3V/5V)</td>
<td>1.8V ~ 3.3V (低压优先)</td>
<td>4.5V ~ 5.5V (需 5V)</td>
<td>4.5V ~ 5.5V (需 5V)</td>
</tr>
<tr>
<td>接口类型</td>
<td>并行 8-bit / 串行 (SPI-like，写-only)</td>
<td>并行 8-bit / 串行 (SPI，写-only)</td>
<td>并行 8-bit (需外部时钟)</td>
<td>并行 8-bit (多芯片页选择)</td>
</tr>
<tr>
<td>内置 RAM</td>
<td>是 (256×32 像素，约 1Kb，支持滚动)</td>
<td>是 (约 1Kb，支持读写)</td>
<td>否 (需外部 SRAM，灵活扩展)</td>
<td>是 (每个芯片 64×64 像素，总 1Kb，支持读写)</td>
</tr>
<tr>
<td>内存组织</td>
<td>水平字节 (8 像素/字节，类似您的图像模式：上半屏 0x80，下半屏 0x88)</td>
<td>垂直字节 (1 列 8 像素/字节，需转换数据)</td>
<td>水平字节 (灵活，8/16 像素/字节)</td>
<td>垂直字节 (分 2 页 64×64，需页切换)</td>
</tr>
<tr>
<td>速度</td>
<td>较慢 (串行模式 ~100kHz，适合简单图形)</td>
<td>较快 (SPI 模式支持高频，适合动态显示)</td>
<td>中等 (并行快，但外部 RAM 延迟)</td>
<td>慢 (页切换开销大，串行需缓冲)</td>
</tr>
<tr>
<td>特殊功能</td>
<td>兼容 HD44780 字符模式 (8×32 字符)，内置电荷泵 (无需外部 7660)；支持 CGRAM (自定义字符)</td>
<td>支持灰度 (部分变体)，RGB 背光兼容；低功耗，适合电池设备</td>
<td>支持更大分辨率 (至 240×128)；文本/图形混合，需外部字体 RAM</td>
<td>简单页模式 (2 芯片并联实现 128×64)；支持读操作 (易调试)</td>
</tr>
<tr>
<td>引脚数</td>
<td>约 16-20 (RS, E, RST, PSB 选接口)</td>
<td>约 11-16 (SCLK, SID, CS, A0)</td>
<td>约 24 (DB0-7, FS, CE 等，多引脚)</td>
<td>约 16 (DB0-7, CS1/CS2 页选, R/W)</td>
</tr>
<tr>
<td>优缺点</td>
<td>优点：兼容字符模式，易移植；缺点：串行写-only (需 MCU 缓冲</td>
<td>优点：低压串行，速度快；缺点：垂直内存需数据转置</td>
<td>优点：扩展性强；缺点：需外部 RAM，复杂</td>
<td>优点：支持读写，简单；缺点：页切换慢，不兼容串行</td>
</tr>
<tr>
<td>成本/可用性</td>
<td>低 (~5-10元)，常见于中国模块</td>
<td>低 (~5-10元)，Raspberry Pi 友好</td>
<td>中等 (~10-20元)，工业级</td>
<td>低 (~5元)，老款但易得</td>
</tr>
<tr>
<td>库/兼容</td>
<td>U8g2/ST7920</td>
<td>U8g2/ST7565，需调整垂直布局</td>
<td>U8g2/T6963C，需配置外部 RAM</td>
<td>U8g2/KS0108，支持但速度慢</td>
</tr>
</tbody></table>
<p>LCD12864 ST7920 的元件引脚及功能如下表：</p>
<table>
<thead>
<tr>
<th align="center"><span>引脚</span>Pins<span></span></th>
<th align="center"><span>符号</span><span>Symbol</span></th>
<th align="center"><span>电平</span><span>Level</span></th>
<th><span>描述</span><span>Description</span></th>
</tr>
</thead>
<tbody><tr>
<td align="center">1</td>
<td align="center"><code>VDD</code></td>
<td align="center">-</td>
<td>接地</td>
</tr>
<tr>
<td align="center">2</td>
<td align="center"><code>VSS</code></td>
<td align="center">-</td>
<td>电源正极</td>
</tr>
<tr>
<td align="center">3</td>
<td align="center"><code>V0</code></td>
<td align="center">-</td>
<td>对比度调节</td>
</tr>
<tr>
<td align="center">4</td>
<td align="center"><code>RS</code></td>
<td align="center">L / H</td>
<td>寄存器选择，L：<strong>指令</strong>寄存器；H：<strong>数据</strong>寄存器</td>
</tr>
<tr>
<td align="center">5</td>
<td align="center"><code>R/W</code></td>
<td align="center">L / H</td>
<td>读写控制，L：<strong>写入</strong>；H：<strong>读取</strong></td>
</tr>
<tr>
<td align="center">6</td>
<td align="center"><code>E</code></td>
<td align="center">H</td>
<td>使能信号，高电平有效</td>
</tr>
<tr>
<td align="center">7</td>
<td align="center"><code>DB0</code></td>
<td align="center">L / H</td>
<td>数据位 0</td>
</tr>
<tr>
<td align="center">8</td>
<td align="center"><code>DB1</code></td>
<td align="center">L / H</td>
<td>数据位 1</td>
</tr>
<tr>
<td align="center">9</td>
<td align="center"><code>DB2</code></td>
<td align="center">L / H</td>
<td>数据位 2</td>
</tr>
<tr>
<td align="center">10</td>
<td align="center"><code>DB3</code></td>
<td align="center">L / H</td>
<td>数据位 3</td>
</tr>
<tr>
<td align="center">11</td>
<td align="center"><code>DB4</code></td>
<td align="center">L / H</td>
<td>数据位 4</td>
</tr>
<tr>
<td align="center">12</td>
<td align="center"><code>DB5</code></td>
<td align="center">L / H</td>
<td>数据位 5</td>
</tr>
<tr>
<td align="center">13</td>
<td align="center"><code>DB6</code></td>
<td align="center">L / H</td>
<td>数据位 6</td>
</tr>
<tr>
<td align="center">14</td>
<td align="center"><code>DB7</code></td>
<td align="center">L / H</td>
<td>数据位 7</td>
</tr>
<tr>
<td align="center">15</td>
<td align="center"><code>PSB</code></td>
<td align="center">L / H</td>
<td>接口选择，L：串行；H：8 / 4 位并行</td>
</tr>
<tr>
<td align="center">16</td>
<td align="center"><code>NC</code></td>
<td align="center">-</td>
<td>悬空</td>
</tr>
<tr>
<td align="center">17</td>
<td align="center"><code>RESET</code></td>
<td align="center">L / H</td>
<td>复位，低电平有效</td>
</tr>
<tr>
<td align="center">18</td>
<td align="center"><code>VOUT</code></td>
<td align="center">-</td>
<td>-10V 负电源输出</td>
</tr>
<tr>
<td align="center">19</td>
<td align="center"><code>LEDA</code></td>
<td align="center">-</td>
<td>背光正极</td>
</tr>
<tr>
<td align="center">20</td>
<td align="center"><code>LEDK</code></td>
<td align="center">-</td>
<td>背光负极</td>
</tr>
</tbody></table>
<p>可以看到，与 LCD1602 相比，两者的在引脚上的安排是极为相似的。需要额外注意的是，在本项目中将直接使用 8 位并行的方式传输数据，因此需要将<code>PSG</code>引脚设为高电平；而用于提供 -10V 电源输出的 <code>VOUT</code>，原本应该是通过一个<a href="https://en.wikipedia.org/wiki/Potentiometer">电位器</a>接电源，同时输出端接<code>V0</code>，通过调整电阻以调节屏幕的对比度；但是有的模块上可能已经附带了用于调整的电位器，因此就无需额外连接电路。</p>
<p>在本项目中，读取功能不会使用，因此<code>RW</code>直接拉低。<code>RS</code>，<code>E</code>和<code>RESET</code>分别接 Arduino 的 2，3，4 引脚。</p>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/LCD12864_Back.avif" alt="LCD 的背面。RV1 是用于调整对比度的电位器。"></p>
<p>在大多数项目中，会使用<code>pinMode()</code>函数来控制引脚的数据方向，<code>pinOut()</code>函数来设置引脚的输出电平。除了这个方法以外，Arduino 的每个引脚都由三个 8 位寄存器控制，通常以 B，C，D 等字母命名，对应不同的端口：</p>
<ol>
<li><code>DDRx, Data Direction Register</code>控制引脚的数据方向。0 为<strong>输入</strong>，1 为<strong>输出</strong>。</li>
<li><code>PORTx, Port Output Register</code>控制着该端口引脚上的输出电平。0 为<strong>高电平</strong>，1 为<strong>低电平</strong>。</li>
<li><code>PINx, Port Input Register</code>读取输入引脚的状态。如果引脚是高电平，则3相应的位是 1；反之则为 0。[^5]</li>
</ol>
<p>虽然官方手册上并不推荐这种做法，但它的速度比通过<code>for</code>循环逐位写入快得多；而且这种底层操作对于深入理解硬件也大有裨益。不同的型号的芯片对于各个引脚的映射不一样，所以在使用时请务必先通过官方给出的 Pinout 确认各个引脚所属的端口。笔者使用的是 Arduino Mega 2560，采用的处理器是 ATmega2560。根据官方给出的引脚图，<span style="color: #f49d26;">橘黄色</span>背景的文字就是单片机的端口，而相同端口的引脚就可以直接使用寄存器来读写。</p>
<p>![[img/Arduino_Mega_2560_Pinout_1.avif|Arduino Mega 2560 的引脚图。橘黄色背景的文字是该引脚的端口编号。<a href="https://docs.arduino.cc/hardware/mega-2560/">Arduino</a>]]</p>
<p>若要直接使用<code>0-7</code>号引脚作为数据接口，由于<code>0</code>号和<code>1</code>号引脚有编程的串行通信和调试的用途，直接将它们作为 <a href="https://en.wikipedia.org/wiki/General-purpose_input/output">GPIO</a>可能会干扰下载或调试，因此不行；同时这些引脚也并不属于同一个端口，用寄存器来控制的话很麻烦。在这个项目中，笔者选择了<code>A0-A7</code>作为数据接口，因为它们可以直接使用<code>PORTF</code>和<code>DDRF</code>来控制；而且排成一排，可以直接插排线，看起来就整洁很多。虽然板子上这几个引脚旁标注了 ANALOG IN 的字样，这并不妨碍它们作为 GPIO 使用。</p>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/LCD12864_Fritzing.avif" alt="在 Fritzing 中绘制的电路。"></p>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/Connection.avif" alt="实物连接图。买了杜邦线和面包板用硬芯线是个正确的决定。"></p>
<h2>程序编写</h2>
<p>硬件的准备工作完成了以后，接下来就是软件层面的事情了。LCD12864 的初始化流程和 LCD1602 的非常相似。在等待电源电压稳定以后，两者都要经过功能设置 → 显示控制 → 清屏 → 输入模式这几步。指令格式如下：</p>
<table>
<thead>
<tr>
<th><span>指令</span><span>Instruction</span></th>
<th align="center">RS</th>
<th align="center">R/W</th>
<th align="center">D7</th>
<th align="center">D6</th>
<th align="center">D5</th>
<th align="center">D4</th>
<th align="center">D3</th>
<th align="center">D2</th>
<th align="center">D1</th>
<th align="center">D0</th>
</tr>
</thead>
<tbody><tr>
<td><span>功能设置</span><span>Function Set</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>DL</code></td>
<td align="center"><code>X</code></td>
<td align="center"><code>RE</code></td>
<td align="center"><code>G</code></td>
<td align="center"><code>X</code></td>
</tr>
<tr>
<td><span>显示控制</span><span>Display Control</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>D</code></td>
<td align="center"><code>C</code></td>
<td align="center"><code>B</code></td>
</tr>
<tr>
<td><span>清屏</span><span>Clear Screen</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
</tr>
<tr>
<td><span>输入模式</span><span>Entry Mode Set</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>I/D</code></td>
<td align="center"><code>S</code></td>
</tr>
</tbody></table>
<p>注：<code>X</code>表示该位可以为任意值，不会造成影响。功能设置指令的<code>G</code>仅有在启用扩展指令集的情况下才能使用。</p>
<p>第一条是功能设置 (Function Set) 指令。<code>DL</code>是用来选择数据位的宽度的。低电平是 <strong>4 位</strong>，高电平是 <strong>8 位</strong>；而<code>RE</code>用来选择指令集。低电平是<strong>基本指令集</strong>，高电平是<strong>扩展指令集</strong>。本项目将使用 8 位并行模式通信，因此<code>DL = 1</code>。扩展指令集在后期实现显示图像等功能的时候会派上用场，不过现在的任务是先在屏幕上显示内置字库，因此使用基本指令集即可，<code>RE = 0</code>。</p>
<p>第二条是显示控制 (Display ON/OFF Control) 指令。<code>D</code>, <code>C</code>, <code>B</code>这三位分别对应着<strong>显示</strong>，<strong>光标</strong>以及<strong>字符闪烁</strong>。高电平为启用，低电平为关闭。</p>
<p>第三条是清屏 (Clear Screen) 指令。这个很简单，没有需要设置的参数。</p>
<p>最后一条是输入模式 (Entry Mode Set) 指令。<code>I/D</code>控制光标的移动方向，高电平为向右移动（递增），低电平为向左移动（递减）。<code>S</code>控制全屏移动。低电平时禁用，高电平时则根据<code>I/D</code>来进行全屏移动。例如，当<code>S</code>启用时，<code>I/D</code>为高电平则向右移动，反之则向左移动。</p>
<p>笔者采用的指令如下：</p>
<table>
<thead>
<tr>
<th><span>指令</span><span>Instruction</span></th>
<th align="center">RS</th>
<th align="center">R/W</th>
<th align="center">D7</th>
<th align="center">D6</th>
<th align="center">D5</th>
<th align="center">D4</th>
<th align="center">D3</th>
<th align="center">D2</th>
<th align="center">D1</th>
<th align="center">D0</th>
</tr>
</thead>
<tbody><tr>
<td><span>功能设置</span><span>Function Set</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
</tr>
<tr>
<td><span>显示控制</span><span>Display Control</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
</tr>
<tr>
<td><span>清屏</span><span>Clear Screen</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
</tr>
<tr>
<td><span>输入模式</span><span>Entry Mode Set</span></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>0</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>1</code></td>
<td align="center"><code>0</code></td>
</tr>
</tbody></table>
<p>OK，决定好要使用什么指令以后，接下来就是编程。上次驱动 LCD1602 时，是将这些指令的二进制数据直接保存在变量里。这样虽然调用时方便，但是若想使用不同的设置，还得手动更改变量的值，很不直观。因此这次将使用最近学会的一个好方法。</p>
<p>这个方法即自定义头文件。里面将<strong>声明 (Declaration)</strong> 一些函数。头文件取名为<code>ST7920.h</code>。初始化的流程一共要用到四条指令，现在就先声明四个函数用来专门控制：</p>
<pre><code class="language-c">// ST7920.H
#ifndef ST7920_H
#define ST7920_H

void FuncSet();
void DispCtrl();
void ClrScreen();
void EntryMode();

#endif
</code></pre>
<p>诸位读者想必已经发现了：除了要声明的函数以外，还有三条语句：<code>#ifndef</code>，<code>#define</code>和<code>#endif</code>。它们是什么意思呢？</p>
<p>这三条是 C/C++ 的<a href="https://cppreference.cn/w/c/preprocessor">预处理器指令 (Preprocessor Directives)</a>。这类指令将用于控制编译器如何编译文件或编译哪些部分[^6]。上述代码中出现的<code>#ifndef</code>，<code>#define</code>和<code>#endif</code>，是用于防止头文件被重复包含，通常成对出现。</p>
<p>那么为什么需要防止头文件被重复包含呢？简单来说，我们在一个源文件通过<code>#include</code>包含某个头文件时，就相当于直接将头文件里的内容复制粘贴在了代码里。假设有两个源文件<code>file1.c</code>和<code>file2.c</code>同时包含了<code>common.h</code>这个头文件，那么在编译时就相当于<code>common.h</code>中的内容被<strong>重复定义</strong>，这时编译器就会报错。<code>#ifndef</code>相当于一个<code>if ()</code>语句，会检测一个特定的<a href="https://en.wikipedia.org/wiki/Macro_(computer_science)">宏</a>，通常来说是头文件的文件名，例如<code>COMMON_H</code>，有没有被定义。如果没有定义，就执行接下来的<code>#define</code>定义这个宏，以及接下来的其他代码；而如果被定义了，那么接下来所有的代码都不会被执行，直到<code>#endif</code>结束。</p>
<p>除此之外，这三条指令也可以凝缩成为一条<code>#pragma once</code>。这个更简洁，同时也不用担心宏名冲突的问题。虽然是非标准的，不过目前大部分的编译器都支持这条命令，除非是那种特别特别老旧的版本。头文件也应尽量减少相互包含，避免形成循环依赖，正确的做法是在<code>.c</code>或<code>.cpp</code>的源文件中包含。</p>
<p>确定好要使用的函数以后，接下来就是要传入的参数。这些参数都可以视作布尔类型。但是如果在调用的时候直接写<code>DispCtrl(true, false, false)</code>的话也很不直观。因此可以通过<strong>枚举</strong><code>enum</code>来赋予这些参数一个有意义的名称。首先，根据每条指令要用到的参数，以这些参数为基准创建一个枚举类型，里面就可以列举出所有可能的情况。情况不外乎<code>0</code>和<code>1</code>两种，而这两种情况就可以分别取个易懂的名字：</p>
<pre><code class="language-c">// Enums
// Entry Mode Set command
enum AddrContCtrl   { ADDR_INC = 1, ADDR_DEC  	= 0 };
enum DispShiftCtrl  { SHIFT_ON = 1, SHIFT_OFF 	= 0 };

// Function Set command
enum DataLength     { DL_8BIT  	= 1, DL_4BIT 	= 0 };
enum InstructionSet { EXTENDED 	= 1, BASIC    	= 0 };
enum GraphicMode    { GRAPHIC  	= 1, CHAR    	= 0 };

// Display Control command
enum DispSwitch   	{ DISP_ON   = 1, DISP_OFF   = 0 };
enum CursorSwitch 	{ CURSOR_ON = 1, CURSOR_OFF = 0 };
enum CharBlink    	{ BLINK_ON  = 1, BLINK_OFF  = 0 };

// Write Byte command
enum RegSelect 		{ DATA = 1, INST = 0 };
</code></pre>
<p>之前声明的函数就可以这样设定形参 (Formal Parameters)：</p>
<pre><code class="language-c">void FuncSet(enum DataLength DL, enum InstructionSet RE, enum GraphicMode G);
void DispCtrl(enum DispSwitch D, enum CursorSwitch C, enum CharBlink B);
void ClrScreen();
void EntryMode(enum AddrContCtrl ID, enum DispShiftCtrl S);
</code></pre>
<p>现在，就可以这样来设置要发送的指令，例如：</p>
<pre><code class="language-c">FuncSet(DL_8BIT, BASIC, CHAR);
</code></pre>
<p>意思就相当一目了然了，即 8 位数据模式，基础指令集，字符模式。</p>
<p>不过，这还只是声明好了函数。函数功能的实现，或者<strong>定义 (Definition)</strong> 应该放在<code>.c</code>或<code>.cpp</code>的源文件中实现，例如再新建一个<code>st7920.c</code>的文件。</p>
<p>由于<code>PORTF</code>一次性传递的是一个字节的数据，因此需要进行一些位操作来更改这一字节内某一位的值。这一过程可以通过<strong>按位或 (Bitwise OR)</strong> 以及 <strong>位移 (Bitwise Shift)</strong> 来完成。例如，在<code>FuncSet()</code>中，需要将 bit4 和 bit2 分别设为<code>1</code>和<code>0</code>。读取到传入的参数以后，分别将这两个向高位左移 4 位和 2 位，随后再通过按位或把值赋给这一位，即：</p>
<pre><code class="language-c">uint8_t cmd = 0b00100000 | (DL &lt;&lt; 4) | (RE &lt;&lt; 2);
</code></pre>
<p>例如当<code>DL = 1, RE = 0</code>时：</p>
<pre><code>   0b00100000
   0b00010000 (1 &lt;&lt; 4 = 0b00010000)
OR 0b00000000
--------------
   0b00110000
</code></pre>
<p>最终的计算结果就是要传输的指令。</p>
<p>最后，除了这些和 LCD 功能有关的指令以外，还得有个负责将它们写入的函数。要写入的内容分为指令和数据两种，同时还得控制写完后的延迟的时间，以确保正确写入。根据这些需求，就可以声明：</p>
<pre><code class="language-c">enum RegSelect { DATA = 1, INST = 0 };
void WriteByte(enum RegSelect rs, uint8_t data, int DelayUs)
</code></pre>
<p>第一个参数<code>rs</code>是选择要写入的寄存器类型，分为指令和数据两种寄存器。这个参数就直接控制着<code>RS</code>引脚上的电平。<code>data</code>即要写入的内容，通过<code>uint8_t</code>定义了一个大小为 8-bit 的形参。最后是要延迟的微秒数。根据手册可得，大部分的写指令都能在几百纳秒内完成，因此为了提升效率，一般延迟 10μs 左右就足够了，不过清屏的时间可以留得略久一点。除此之外，还得控制使能<code>E</code>引脚上的电平。这个引脚的功能可以理解为「发送数据」，高电平有效。这个函数的实现如下：</p>
<pre><code class="language-c">void WriteByte(enum RegSelect rs, uint8_t data, int delayUs) {
	// Set the mode of RS
	// true for data mode
	// false for instruction mode
	if (rs == true)
		digitalWrite(RS, HIGH);
	else
		digitalWrite(RS, LOW);
	   
	// Write the data the the data pins
	PORTF = data;

	// Generate a rising edge on pin Enable
	digitalWrite(E, HIGH);
	delayMicroseconds(10);
	digitalWrite(E, LOW);

	// Delay for delayMs for component ready
	delayMicroseconds(delayUs);
}
</code></pre>
<h2>正式点亮</h2>
<p>终于，所有最基本的函数已经写好了，现在只需要在主文件中包含自定义的头文件即可调用这些函数：</p>
<pre><code class="language-c">// LCD128+4.ino

#include "ST7920.h"

void setup() {
	pinMode(RST, OUTPUT); // Intialize RST pin (default HIGH)
	pinMode(E, OUTPUT); // Intialize E pin (default LOW)
	pinMode(RS, OUTPUT); // Intialize RS pin (default LOW)

	DDRF = 0xFF; &nbsp;// Set data direction output
	PORTF = 0x00; // Intialize pin output

	DispClr();
	FuncSet(DL_8BIT, BASIC, CHAR);
	DispCtrl(DISP_ON, CURSOR_OFF, BLINK_OFF);
	DispClr();
	EntryMode(ADDR_INC, SHIFT_OFF);
}
</code></pre>
<p>这样就完成了屏幕的初始化流程（其实这些函数也可以打包在一起，放在一个用于初始化的函数中）。而此时如果想在屏幕上输出字符，可以直接通过<code>WriteByte()</code>，选择数据模式，数据就填要打印的 ASCII 字符即可。如果想输出字符串，可以定义一个<code>char</code>类型的指针，并且在每次写完数据后向后移动一位。在这里我写了个函数专门用于输出字符串：</p>
<pre><code class="language-c">void LcdPrint(const char *text) {
&nbsp; &nbsp; while (*text) {
&nbsp; &nbsp; &nbsp; &nbsp; WriteByte(DATA, *text, 10);
&nbsp; &nbsp; &nbsp; &nbsp; text++;
&nbsp; &nbsp; }
}

LcdPrint("Hello World!");
</code></pre>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/Hello_World.avif" alt="在屏幕上输出 Hello World!"></p>
<p>而如果此时直接输入中文字符串：</p>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/Mojibake.avif" alt="输入的原字符串是「你好！」，结果屏幕上出现的是乱码。这种展开属于是意料之外，情理之中了。"></p>
<p>出现<a href="https://en.wikipedia.org/wiki/Mojibake">乱码 (Mojibake)</a> 的原因很简单。西文部分由于是兼容 ASCII 码的，所以无论怎么样都没关系，能被正确解码；但是中文就不一样了。屏幕采用的编码是基于 <a href="https://en.wikipedia.org/wiki/GB_2312">GB2312</a> 的双字节编码；而代码默认采用的是 UTF-8。缺少字符间的映射关系的话就会导致乱码。这部分的原理比较复杂，而且偏离了本文的主题，在此先按下不表。代替方案就是先通过一些工具，例如 Python 有个encode()`函数，将返回一串字节串，这时再将编码输入函数就可以正确显示：</p>
<pre><code class="language-python">("你好！").encode("GB2312") # Return b'\xc4\xe3\xba\xc3\xa3\xa1'
</code></pre>
<p><img src="https://studiountagged.top/articles/2025-09-11-LCD12864-ST7920/img/Hanzi.avif" alt="正确显示的字符串。"></p>
<h2>后记</h2>
<p>这篇文章篇幅虽然很长，但内容还是相当基础的，只用到了这块屏幕的最基础的功能。在后面的文章中，我将会侧重于它的图形模式，显示自定义字形，以及开发一些用于绘图的函数。</p>
<hr>
<p>[^1]: ST7920 Datasheet(PDF) - Sitronix Technology Co., Ltd. <a href="https://www.alldatasheet.com/datasheet-pdf/pdf/326219/SITRONIX/ST7920.html">https://www.alldatasheet.com/datasheet-pdf/pdf/326219/SITRONIX/ST7920.html</a>
[^2]: ST7565 Datasheet(PDF) - Sitronix Technology Co., Ltd. <a href="https://www.alldatasheet.com/datasheet-pdf/pdf/326240/SITRONIX/ST7565.html">https://www.alldatasheet.com/datasheet-pdf/pdf/326240/SITRONIX/ST7565.html</a>
[^3]: T6963C Datasheet(PDF) - Toshiba Semiconductor. <a href="https://www.alldatasheet.com/datasheet-pdf/pdf/31129/TOSHIBA/T6963C.html">https://www.alldatasheet.com/datasheet-pdf/pdf/31129/TOSHIBA/T6963C.html</a>
[^4]: KS0108B Datasheet(PDF) - Samsung semiconductor. <a href="https://www.alldatasheet.com/datasheet-pdf/pdf/37323/SAMSUNG/KS0108B.html">https://www.alldatasheet.com/datasheet-pdf/pdf/37323/SAMSUNG/KS0108B.html</a>
[^5]: Arduino - PortManipulation | Arduino Documentation. <a href="https://docs.arduino.cc/retired/hacking/software/PortManipulation/">https://docs.arduino.cc/retired/hacking/software/PortManipulation/</a>
[^6]: C 预处理器 | 菜鸟教程. <a href="https://www.runoob.com/cprogramming/c-preprocessors.html">https://www.runoob.com/cprogramming/c-preprocessors.html</a></p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>温柔的幻境：从崩铁 × Fate 联动看「开拓之道」的精神内核</title>
					<link>https://studiountagged.top/articles/2025-07-22-A-Soft-Illusion</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-07-22-A-Soft-Illusion/</guid>
					<pubDate>Mon, 21 Jul 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							关于联动剧情启发带来的启示
The enlightment brought by the collab story.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>《崩坏：星穹铁道》与《Fate UBW》联动这件事，对我来说其实没什么太大的波澜。我之前虽然也看过《Fate/stay night》的动画，也相当喜欢 Saber，有段时间甚至拿她当过壁纸，但那已经是很久很久以前的事了，很多设定已经记不太清了，有些在联动剧情中玩的梗也未必能够及时 get 到。抛开这些不谈，本次联动活动的剧情总体上还是以「欢愉」为主基调的，不过有一段令我印象深刻，故打算写一篇杂谈，来分享分享我个人的看法。</p>
<p>这段剧情概括一下，作为 Caster 的「音符小姐」为了拖住众人前进的脚步，将众人拖进了她所营造的幻境中。这个幻境所展现的正是众人心中的理想乡的模样，以此试图让众人忘却心中的使命，永远留在这个幻境之中。在这个幻境之中，我们的开拓者来到了另一条 if 线中——即如果当时没有踏上星穹列车，而是留在空间站会怎么样。在这条分支，开拓者变成了一个平平无奇的科员，每天都奔波于各种琐事，仿佛自己的一生能一眼从头看到尾。</p>
<p>对我而言，这段剧情是颇具警醒意味的。这不禁与《月亮与六便士》书中的那位画家形成了某种呼应。我很久以前读到这本书时，并不知道它为什么称得上是名著；然而在过完这段剧情以后，这层联想才使我在一定程度上理解了为什么主人公宁愿放弃自己的妻儿和稳定的工作，转而选择当一个漂泊不定、穷困潦倒的画家。开拓者在幻象中逐渐对日复一日的重复工作感到厌倦，而内心也在嘶声力竭地挣扎着，试图摆脱现状。我想，书中的画家在一定程度上和开拓者是一样的；又或许大家都一样。谁人年少时不曾拥有一腔热血，希望能够闯出一番天地、有所作为？然而总是会被现实毫不留情地当头一棒，被各种鸡毛蒜皮的小事束手缚脚，最后蓦然回首才发现自己已经垂垂老矣，只能碌碌无为地度过一生。</p>
<p>诚然，踏出第一步无疑需要巨大的勇气，无人知道前方究竟会有什么风险。这似乎是个两难的抉择；然而如果因此失去了前进的动力，最后也只会在遗忘中死去。对于整个世界来说，每个人刚诞生的时候都是如此渺小，平平无奇，与游戏中的 NPC 并无二异；然而总有些人能够脱颖而出，在人群中发出耀眼的光芒，甚至改写历史的进程。人类之所以能够向前发展，不是因为其聪明才智，或是周密计划；而是那些不甘平庸的灵魂，不肯放弃挣扎与思考。开拓者所踏上的开拓之道，不光光是对群星的开拓，也是对自身的开拓。我想，这就是策划想要表达的吧，虽然未来未知，但要永远不忘初心，永远胸怀热血，正如乔布斯的名言 <q>Stay hungry. Stay foolish.</q></p>
<p>音符小姐所构建的幻象并非空穴来风，现实中也存在着无痛的「理想乡」。如今我们处于一个信息爆炸的时代，快节奏的生活和娱乐的泛滥使得人们逐渐失去了批判思考与自我意识。尼尔·波兹曼对电视的诞生进行了批判，而他的这些话放在半个世纪后的今天仍然适用。赫胥黎的《美丽新世界》正在成为现实。而当所有人当沉湎于感官刺激和娱乐中，人类发展就已经停滞了，因为我们心甘情愿地住进了我们自己所营造的牢房之中；而历史上一切的伟大壮举和新思想都是源于苦难的。理想乡之所以令人动摇，并非是因为它是虚构的；反之，它一直就存在于现实生活中，而且触手可及。那是一种不痛不痒、不需要承担风险的生活，是所有烦恼与责任都被温水煮开的日常。相比之下，现实太难了，难得让人想逃。在这一点上，中国几千年前的一位大哲学家就已经提出了准确的观点：「生于忧患，死于安乐」。在这个娱乐至死的时代，是时候该慢下来，好好听听自己内心的声音了。「现在我真的拥有我想拥有的吗？」「还记得当初自己究竟是要干什么吗？」切忌被各种鸡毛蒜皮的小事遮蔽耳目。</p>
<p>哲学上有三大著名的发问：「我是谁」「我从哪里来」「我到哪里去」。在我看来，这些问题可以被分别解读为对自己的定位，自己的初心以及自己真正想要追求的东西。我因为高考完填错志愿被分配到了一个自己极其讨厌的专业，而且每天都还得学着自己讨厌的课程。别人口中所谓的「美好的大学时光」对我而言只不过是高中之后的另一个牢笼。对于专业课一次次的抵触和逃避带来的自然是堆成山的挂科和重修。而这次的剧情，使我意识到，一直逃避、一直沉浸在自己的世界中，恐怕将来的处境会变得和游戏中音符小姐所期望的那样。我开始认真思索这三个问题。也许我现在并不能给出答案，但这个过程将会让我能够重新听清忽视已久的内心的声音。</p>
<p>在《赛博朋克 2077》中德克斯特曾在车上问 V 的那个问题：「无名小卒？还是名扬天下？」，现在看来，我会坚定地选择后者。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>新增对 RSS 订阅的支持</title>
					<link>https://studiountagged.top/articles/2025-07-04-RSS-Available</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-07-04-RSS-Available/</guid>
					<pubDate>Thu, 03 Jul 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							可以通过各种主流 RSS 阅览器对本站文章进行订阅。
Subscribe to the articles on this site through various mainstream RSS readers.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>目前本站已支持对 RSS 订阅的支持🎉。RSS (Really Simple Syndication) 是一种很棒的信息获取方式。通过 RSS，你可以在自己喜欢的阅读器中统一管理和阅读订阅的内容，不会错过任何更新，也不用担心算法推荐的干扰。在这里感谢 <a href="https://quarkpixel.github.io/">QuarkPixel</a> 给我提供的灵感。订阅方法如下：</p>
<ol>
<li><p>通过浏览器插件进行订阅。以 RSSHub Radar 为例，它可以自动检测到当前页面上的 RSS。</p>
</li>
<li><p>或者点击侧栏或页面最底部处橙色的 RSS 订阅 的链接；该链接将会直接导向<code>rss.xml</code>。你只需要将这个 XML 文件的链接复制粘贴进 RSS 阅览器中即可完成订阅。</p>
</li>
</ol>
<p>目前支持的订阅格式为 RSS 2.0。欢迎大家订阅！</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>What’s the “fi” you see on the books</title>
					<link>https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/</guid>
					<pubDate>Sun, 25 May 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							排印之美 Ep. 01
The Beauty of Typography Ep. 01
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><div class="youtube-video-container">
    <iframe width="560" height="315" src="https://www.youtube.com/embed/5RA6tvWzk7Q?si=qM37ryFvBdv1iDBZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div>

<h2>Video Transcripts</h2>
<blockquote>
<p>[!NOTE] 注释
简体中文版的视频文案请参阅<a href="/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/">此文章</a>。</p>
</blockquote>
<p>Have you ever noticed, in some books, magazines or newspapers, and even on screens, you can always find these kinds of linked characters? So, what are these things? Are they some sort of printing or rendering errors How were these things created? And what's the purpose of their usage?</p>
<p>Greetings, I’m Steven. This series will introduce some knowledge of typeface designing and typography to you. Okay, let's begin!</p>
<p>These kinds of linked characters named Ligatures in typography. Ligature from Latin <em>ligātus</em> means “to tie, bind”. A ligature occurs where two or more letters are joined to form a single glyph.</p>
<p>Let’s date back to the ages when typesetting was the dominant printing method. Take a look at this metal type, I’ll flip it first for the demonstration. The letter sits in a rectangular space we call the bounding box. The width of this box includes the width of the glyph and side bearings. These side bearings help to keep the spacing between the letters looking just right in the ideal situation.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Metrics.avif" alt="The bounding box, character width and side bearings of a metal type"></p>
<p>However, certain letters such as f or T, A, V, can cause these huge visual gaps between the letters and reducing the readability because of their shape. Allowing a portion of the glyph to extend beyond the body can be a solution. And the overhanging part is called <em>kern</em>. But this can cause the collision between the adjacent letters, making them impossible to align neatly.</p>
<p>To solve this problem, the designers cast frequently used letter combinations, such as fi, fl, as individual metal type pieces. This practice gives rise to the ligatures. It not only enhanced visual harmony but also simplified the typesetting workflows. It quickly became popular and endures in modern digital design.</p>
<p>In contemporary typography, ligatures had evolved their original role of eliminating unsightly collisions and improving readability. They now serve as stylistic design elements. Powered by OpenType technology, designers can create not only standard ligatures but also various ligature functions—all meeting sophisticated typographic needs. Beyond simply connecting letters, modern ligatures do the opposite: enable subtle adjustments to adjacent glyphs, preventing visual clashing while maintaining cohesive word shapes.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/liga_and_dlig.avif" alt="Standard ligatures (liga) &amp; Discretionary ligatures (dlig) in EB Garamond">
<img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Cambria_ligature.avif" alt="Please pay attention to the highlighted parts. The fi ligature in Cambria separated instead connecting"></p>
<p>In many modern programming fonts, more and more type foundries are highlighting their support for various OpenType features as a selling point. One common approach is to alter the glyphs of frequently repeated symbols to improve the distinction between similar-looking content or to make repeated characters easier to count. Some fonts also enhance the readability of symbol combinations—for example, by rendering exclamation mark plus equal sign != as a widened, double-width not-equals sign <code>!=</code> for clearer recognition.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Fira_Code_Ligatures.png" alt="Various ligatures in Fira Code"></p>
<p>The ligatures mentioned above originate from typographic practices, where their function is purely aesthetic and technical in font design and typesetting, without altering the semantic meaning of the text. These are therefore termed&nbsp;<em>typographic ligatures</em>.</p>
<p>In contrast, another category exists, <em>orthographic ligatures</em>.&nbsp;These are not mere connected forms of two letters, nor can they be mechanically split back into their original components. Instead, they evolve into entirely new letters. The most iconic example is the letter&nbsp;W, whose name reflects its origin: called&nbsp;Double-U&nbsp;in English and&nbsp;Double-V&nbsp;in French. The origin of this letter won’t be discussed here today. So, if you are interested in this series, please leave a like and click subscribe. Comment below and let me know your ideas.</p>
<p>Another notable case is the German&nbsp;<em>eszett</em> (ß) or <em>Sharp S</em>. Although the name eszett, phonetically suggests a combination of&nbsp;Es&nbsp;and&nbsp;Zett, it is often written as a ligature of the&nbsp;long s (ſ)&nbsp;and&nbsp;round s (s). Tracing its roots, this character emerged from the Late Medieval and Early Modern German&nbsp;digraph ⟨Sz⟩, which in Gothic typography was stylized as a long s followed by a tailed z (ſʒ). So why is it written in combination of longs s plus round s today?</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Blackletters.avif" alt="Various glyph shapes of ß in Blackletter typefaces"></p>
<p>Between the 3rd and 8th centuries <span style="font-variant-caps: small-caps;">AD</span>, a significant phonetic shift occurred in Germanic languages known as the&nbsp;<em>High German Consonant Shift</em>. During this period, the West Germanic dialects that underwent the shift gradually evolved into Old High German, distinguishing themselves from non-shifted variants. The Proto-West Germanic voiceless stops /p/, /t/, and /k/ shifted to fricatives or affricates like /t͡s/, /p͡f/, and /x/.</p>
<p>However, due to the continued use of the Latin alphabet for written German, this led to inconsistencies such as multiple sounds represented by a single letter or multiple spellings for a single sound, with letter usage largely dictated by personal preference. In the original alphabet, the letter ⟨z⟩ represented both /t͡s/ and /s/. By the 13th century, ⟨s⟩ gradually became voiced as /z/, necessitating a distinction. Consequently, /s/ began to be denoted by ⟨ss⟩ or the digraph ⟨sz⟩. In Western typography, the ligature ſs (long s + round s) had long existed, but initially served purely as a typographic convention.</p>
<p>The pivotal moment came in July 1903, when the Leipzig Typographic Society officially adopted the <em>Sulzbacher</em> form as the foundational design for the Eszett (ß). This marked ß’s formal entry into the German alphabet. Finally, in 2017, the German Orthographic Council standardized the uppercase form, cementing ß’s definitive role in German orthography.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Leipzig.avif" alt="„Zeitschrift für Deutschlands Buchbrucker Steindrucker und Verwandte Gewerbe“. Nr. 27, July 1903. In the announcement at the lower part of this page specified the shape of ß"></p>
<p>In contrast, the status of the digraph ⟨Ĳ⟩ in Dutch remains unresolved. Although the Dutch Language Union and many dictionaries recommend using ⟨i⟩ + ⟨j⟩ directly, it often behaves as an orthographic ligature in practice: when capitalizing initials, the entire ⟨ĳ⟩ is converted to uppercase; in Dutch crossword puzzles, they occupy a single square; on some typewriters, ⟨Ĳ⟩ has a dedicated key; The sign of Rijksmuseum designs it in this form; and many fonts design ⟨ĳ⟩ as a ligature. Unicode code points <code>U+0132</code> and <code>U+0133</code> explicitly name its uppercase and lowercase forms as <em>ligatures</em>. These factors undeniably reinforce the grapheme’s unified identity.</p>
<p>The evolution of ligatures is not limited to forming standalone letters. Over the course of history, frequently used letter combinations have gradually transformed into symbolic forms. A prime example is <em>ampersand</em> (&amp;), which signifies “and” and originates from the Latin et. Similarly, the number sign evolved from the Latin abbreviation <em>libra pondo</em> (℔). Additionally, currency symbols, alchemical symbols, and astrological signs also trace their origins to letter ligatures.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Ampersand.avif" alt="The evolution of Ampersand (&amp;)">
<img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Libra_pondo_abbreviation_newton.avif" alt="The abbreviation of libra pondo wriiten by Issac Newton"></p>
<p>Ligatures are not exclusive to Latin scripts. Greek manuscript traditions included numerous ligatures, and influenced by this practice, early Greek printed works preserved such stylistic conventions. Today, Unicode still encodes ligatures like ⟨ϗ⟩<code>U+03D7</code>and ⟨ϛ⟩<code>U+03DB</code>. Cyrillic orthography also features many orthographic ligatures, most of which are included in Unicode. In Arabic, ligatures are indispensable—proper text rendering on screens requires extensive OpenType features. In East Asia, historically vertical Japanese writing employed ligatures or combined characters, though only a few are encoded today, such as hiragana <em>yori</em>「ゟ」<code>U+309F</code> and katakana <em>koto</em>「ヿ」<code>U+30FF</code>.</p>
<p><img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Greek_Manuscripts.avif" alt="The manuscript of the Greek lectionary shows that it contains numerous ligatures.">
<img src="https://studiountagged.top/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/img/Cyrillic_Ligatures.avif" alt="There's also a lot of orthographic ligatures in Cyrillic script"></p>
<p>Let us return to the opening question:&nbsp;<em>What purpose do these ligatures serve?</em> Beyond mere aesthetic pursuit, they are akin to DNA’s double helix—typographic and orthographic ligatures twist, soar, and waltz through time, etching their intricate dance into the annals of history. Through them, we glimpse the evolution of human language itself.</p>
<p>Though many typographic ligatures faded with the rise of phototypesetting and desktop publishing in the 20th century, OpenType rekindled their dance. Now, in the quiet spaces between letters, in the breath of every word, these humble symbols thread together the continuum of human expression—silent yet sovereign, they carry forward the unbroken story of who we are.</p>
<h2>References</h2>
<h3>Books &amp; Manuscripts</h3>
<ul>
<li>Aristotle. <a href="https://ub.unibas.ch/cmsdata/spezialkataloge/gg/higg0127.html$0"><em>Nicomachaen Ethics</em></a>. Basel: Johannes Oporin und Eusebius Episcopius, 1566.</li>
<li><a href="https://cudl.lib.cam.ac.uk/view/MS-ADD-00679/1$0">Greek Lectionary</a>. Cambridge University Library, MS Add. 679.</li>
<li>Arrighi, Ludovico degli. <a href="https://archive.org/details/laoperinadiludou00arri/page/n5/mode/2up$0"><em>La operina di Ludouico Vicentino, da imparare di scriuere littera cancellarescha</em></a>, 1524.</li>
<li><em>Zeitschrift für Deutschlands Buchdrucker Steindrucker und Verwandte Gewerbe</em>, 1903.</li>
</ul>
<h3>Articles &amp; Columns</h3>
<ul>
<li>Ralf Hermann. <a href="https://typography.guru/journal/whats-a-ligature/$0">“Typographic Myth Busting: What’s a Ligature, Anyway?”</a>. <em>Typography.guru</em>, Nov 20, 2012.</li>
<li>AMB. <a href="https://blog.as.uky.edu/thebhlog/?p=91$0">“The High German Sound Shift”</a>. <em>The Bʰlog</em>, Apr 28, 2014.</li>
<li>Zui. <a href="https://thelanguagecloset.com/2022/11/05/the-story-of-eszett-s/">“The Story of Eszett (ß)”</a>. <em>The Language Closet</em>, Nov 5, 2022.</li>
<li>Siphercase. <a href="https://vistudium.top/2023/02/16/e04_ligatures/$0">“万『字』皆可『连』 (影片文案)”</a>. <em>遠見齋</em>, 2023.</li>
</ul>
<h3>Wikipedia &amp; Wiktionary Entries</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Ligature_(writing)$0">“Ligature (writing)”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/%C3%9F$0">“ß”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/High_German_consonant_shift">“High German consonant shift”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/IJ_(digraph)$0">“IJ (digraph)”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Ampersand">“Ampersand”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Number_sign">“Number sign”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Currency_symbol">“Currency symbol”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Alchemical_symbol">“Alchemical symbol”</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wiktionary.org/wiki/ligature">“ligature”</a>. <em>Wiktionary</em>, 2025.</li>
<li><a href="https://ja.wikipedia.org/wiki/%E5%90%88%E7%95%A5%E4%BB%AE%E5%90%8D$0">“合略仮名”</a>. <em>Wikipedia</em>, 2025.</li>
</ul>
<h3>Documentations &amp; Forums</h3>
<ul>
<li>FontForge Project. <a href="https://fontforge.org/docs/tutorial/editexample4.html#creating-a-ligature$0">“6.2. Creating a Ligature”</a>. <em>FontForge Documentation</em>.</li>
<li>Topp-Thema: <a href="https://www.typografie.info/3/topic/16219-topp-thema-eszett-als-gro%C3%9Fbuchstabe/$0">“Eszett als Großbuchstabe”</a>. <em>Typografie.info</em>, Mar 8, 2003.</li>
<li>Fira Code. <a href="https://github.com/tonsky/FiraCode$0">“Fira Code: free monospaced font with programming ligatures”</a>.</li>
<li><a href="https://web.archive.org/web/20170706162042/http://www.rechtschreibrat.com/DOX/rfdr_Regeln_2017.pdf$0">“Regeln und Wörterverzeichnis”</a>. <em>Deutsche Rechtschreibung</em>, 2017.</li>
</ul>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>书本上的 fi 究竟是什么？</title>
					<link>https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/</guid>
					<pubDate>Sat, 24 May 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							排印之美 Ep. 01
The Beauty of Typography Ep. 01
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><div class="youtube-video-container">
    <iframe width="560" height="315" src="https://www.youtube.com/embed/5RA6tvWzk7Q?si=qM37ryFvBdv1iDBZ" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""></iframe>
</div>

<h2>视频文案</h2>
<blockquote>
<p>[!NOTE] Note
The English version video transcript is available at <a href="/articles/2025-05-26-The-Beauty-of-the-Typography-Ep01-EN/">here</a>.</p>
</blockquote>
<p>不知道你是否注意过，在一些书籍、杂志、报纸，乃至屏幕上，总能找到这样连起来的字符？所以，这究竟是什么？是某种印刷或者显示错误吗？它们又是怎么被创造出来的？用途又是什么？大家好，我是 Steven。本系列将会向你介绍一些字体设计以及排印的知识。OK，步入正题。</p>
<p>这类连起来的字符其实在排印里面称作「连字」，并非印刷或显示错误。Ligature 源于拉丁语 <em>ligātus</em>，意为「连接，结合」。当两个或多个字母组合在一起形成一个单一的字形时，就会出现连字。</p>
<p>回溯至铅字还是主要印刷方式的时代。观察这个铅字，为了方便演示，我先将它镜像翻转一下。字母坐在我们称之为「字身框」的矩形方框中。方框的宽度包含了字形的宽度以及两侧的侧边距。在理想情况下，这些侧边距有助于保持字母之间的间距，使它们看起来恰到好处。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Metrics.avif" alt="铅字的字身框、字符宽度和侧边距"></p>
<p>但有时候，某些字母例如 f，或者 T, A, V，因为字形的原因，会导致视觉上这些巨大空白的出现，降低可读性。把字面的一部分伸出字身框是个解决办法。这些伸出去的部分就叫 kern。但这也会导致伸出的部分会和其他字母发生碰撞，根本无法和其他字母并列放置。同时也不利于收纳，铅字使用的时间一长也会导致字面发生更大程度的磨损。</p>
<p>为了解决这个问题，设计师们将常用的字母组合设计成单独的活字。连字也就应运而生。由于这种设计不仅使得版面整齐美观，而且也使拣字和排版更加便利，因此在那个时代很快就蔚然成风，直到今天。</p>
<p>在如今的语义下，连字的功能可以分为两大类：除了消除字符相连时可能会出现的难看的黑块，优化阅读体验的标准连字以外，还有一种就是更具有装饰性的设计，即自由连字。自由连字的使用并非必须，但由于它们大多都起源于活字印刷乃至手抄本的时代，因此酌情使用更能为设计作品添加一份古典的气息。在 OpenType 技术的加持下，除了标准连字以外，设计师还可以设计各式各样的连字功能，以满足更复杂的排版需求。连字除了将字「连」起来，也可以反其道而行之，将前后字符微调特意隔开，避免发生粘连。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/liga_and_dlig.avif" alt="EB Garamond 中的标准连字和自由连字">
<img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Cambria_ligature.avif" alt="请仔细观察图中高亮的部分。在 Cambria 中，fi 连字反其道而行之，并没有将字「连」起来，而是隔开"></p>
<p>在现代的一些编程字体中，也能看到越来越多的厂商开始以支持各种 OpenType 特性作为宣传点。最常见的就是对一些重复出现的符号改变字形，以便于区分相近的内容，或者使重复的符号更容易计数。也可以使一些符号组合更容易辨认，例如将 != 渲染成两倍宽的不等号 <code>!=</code>。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Fira_Code_Ligatures.png" alt="Fira Code 中设计的各种连字"></p>
<p>上面提到的连字是出现于排版印刷中的，它们功能仅在字体设计和排印上才能体现，而不会对原文的意义产生影响，因此也被称为<strong>排印学连字</strong>。而还有另外一种连字，这种连字不单单是将两个字母连写起来，也不是简单地将其拆开成原始字母就能作为替换，而是由此诞生了一个新的字母，称为<strong>正字法连字</strong>。</p>
<p>最有名的一个例子便是 <strong>W</strong>。根据它的读法就可见一斑，英语里叫 Double-U，法语中叫 Double-V。关于这个字母的起源，今天在这视频中就暂时不做过多阐述。如果你对本系列感兴趣的话，请点点赞，关注一下，在评论区留言让我知道你的想法。</p>
<p>此外还有德语中的 <strong>eszett</strong>。虽然依据读音是 Es+Zett 的形式，但却经常写作长 s (ſ) + 圆 s (s) 的正字法连字。寻其本源，这个字母最开始起源于中世纪晚期和现代早期德语的二合字母 ⟨Sz⟩，在 Blackletter 中就写成了 ſ + 带尾 z(ʒ) 的形式。既然如此，为什么它现在又会写成 ſ + s 的形式呢？</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Blackletters.avif" alt="Eszett 在各种 Blackletter 字体中的字形"></p>
<p>在公元 3 至 8 世纪的时候，德语的语音发生了比较重要的变化，即高地德语辅音推移。在这期间，发生了推移的西日耳曼语逐渐形成了旧高地德语，区别于其他未发生推移的变体。原本西日耳曼语中的清塞音 /p/, /t/ 和 /k/ 演变成了擦音或塞擦音 /t͡s/, /p͡f/ 和 /x/。然而，由于当时的德语主要使用的还是拉丁字母，因此就出现了一字多音或一音多字的混乱局面。怎么使用这些字母全凭使用者的喜好。</p>
<p>在原来的字母表中，字母 ⟨z⟩ 既表 /t͡s/，又表 /s/。而到了 13 世纪，字母 ⟨s⟩ 逐渐浊化，开始发 /z/ 的音。为了区分，/s/ 就开始由 ⟨ss⟩ 或二合字母 ⟨sz⟩ 表记。在西文排印中，ſs 这个连字其实在就存在了，只是当时仅仅只是作为一个排印学连字存在。</p>
<p>而后来在 1903 年 7 月莱比锡字体排印学会发布的公告上，选用了一种名为 Sulz­bacher 式的字形作为 Eszett 的字形基础。自此，Eszett 就正式作为一个字母进入了德语。2017 年，德语正字法协会正式将 Eszett 的大写纳入了德语正字法。Eszett 在德语中的用法从此就尘埃落定。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Leipzig.avif" alt="1903 年 7 月第 27 期的《面向德国图书印刷商、平版印刷商及相关行业的杂志（机翻）》。在这一页下半部分的公告中规范了 ß 的字形"></p>
<p>而与之相对的，荷兰语中的二合字母 ⟨Ĳ⟩ 至今依然悬而未决。尽管荷兰语联盟和不少字典，都推荐这个字母现在应该直接使用 I + J 的形式，然而不少情况下它的表现都像是一个正字法连字。在首字母大写时，整个 ⟨ĳ⟩ 都会变成大写的形式；在荷兰语的纵横字谜中，它们也会被填入同一个格子；在一些打字机上，这个字母也分配到了单独的键位；阿姆斯特丹国家博物馆的标志将其设计成这种形式；在不少字体中，⟨ĳ⟩ 也被设计成了连字的形式。Unicode 码位<code>U+0132</code>和<code>U+0133</code>也将其大写和小写的形式称为 ligature。这些无疑又在强调这个字母的整体性。</p>
<p>形成单独的字母并非连字唯一的演化方向。一些常用的字母组合也在历史的长河中慢慢进化成了符号的样式。与号（&amp;）便是绝佳的证明。这个符号表示「与，和」的意思，而它就来源于拉丁语中的 <em>et</em>；而井号（#）最开始也是来自拉丁语 <em>libra pondo</em> 的缩写。除此之外，不少国家的货币符号，炼金术符号，天体符号等，都是字母连字的产物。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Ampersand.avif" alt="与号（&amp;）的演化">
<img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Libra_pondo_abbreviation_newton.avif" alt="由艾萨克·牛顿书写的 libra pondo 的缩写"></p>
<p>连字也并非仅局限于拉丁字：希腊字母的手抄本中就包含了大量的连字，受其影响，早期的希腊语出版物中也保留了这种风格。如今，你还可以在 Unicode 中找到 ⟨ϗ⟩ 和 ⟨ϛ⟩ 的码位；在西里尔字母中，也存在着不少正字法连字，而且大多都被收录进了 Unicode；而在阿拉伯语中更是不可或缺，在屏幕上正确渲染文本需要涉及到大量的 OpenType 特性。在亚洲地区，曾经直排的日文也拥有不少连字或合字，如今被收录的只有平假名「ゟ」<code>U+309F</code>和片假名「ヿ」<code>U+30FF</code>，等等。</p>
<p><img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Greek_Manuscripts.avif" alt="希腊语字典的手抄本，可以看到其中包含了大量的连字">
<img src="https://studiountagged.top/articles/2025-05-25-The-Beauty-of-the-Typography-Ep01/img/Cyrillic_Ligatures.avif" alt="西里尔字母中也有大量的正字法连字"></p>
<p>让我们回到最开始的问题。「这些连字有什么用处」。我想，这不仅仅是人们在美学上的尝试，除此之外，如同 DNA 的双螺旋结构，排印学和正字法的连字互相缠绕、纷飞、共舞，在历史的长河中留下了浓墨重彩的一笔，而我们也能以此得以窥见人类语言发展的历史。</p>
<p>不少排印学连字在 20 世纪兴起的照排以及桌面出版中逐渐没落，而 OpenType 的出现则再次让它们重焕生机。这些小小的符号，在字里行间、一呼一吸之间，承上启下人类的文明。</p>
<h2>参考链接</h2>
<h3>书籍及手稿</h3>
<ul>
<li>Aristotle. <a href="https://ub.unibas.ch/cmsdata/spezialkataloge/gg/higg0127.html$0"><em>Nicomachaen Ethics</em></a>. Basel: Johannes Oporin und Eusebius Episcopius, 1566.</li>
<li><a href="https://cudl.lib.cam.ac.uk/view/MS-ADD-00679/1$0">Greek Lectionary</a>. Cambridge University Library, MS Add. 679.</li>
<li>Arrighi, Ludovico degli. <a href="https://archive.org/details/laoperinadiludou00arri/page/n5/mode/2up$0"><em>La operina di Ludouico Vicentino, da imparare di scriuere littera cancellarescha</em></a>, 1524.</li>
<li><em>Zeitschrift für Deutschlands Buchdrucker Steindrucker und Verwandte Gewerbe</em>, 1903.</li>
</ul>
<h3>文章及专栏</h3>
<ul>
<li>Ralf Hermann. <a href="https://typography.guru/journal/whats-a-ligature/$0">"Typographic Myth Busting: What’s a Ligature, Anyway?"</a>. <em>Typography.guru</em>, Nov 20, 2012.</li>
<li>AMB. <a href="https://blog.as.uky.edu/thebhlog/?p=91$0">"The High German Sound Shift"</a>. <em>The Bʰlog</em>, Apr 28, 2014.</li>
<li>Zui. <a href="https://thelanguagecloset.com/2022/11/05/the-story-of-eszett-s/">"The Story of Eszett (ß)"</a>. <em>The Language Closet</em>, Nov 5, 2022.</li>
<li>Siphercase. <a href="https://vistudium.top/2023/02/16/e04_ligatures/$0">"万『字』皆可『连』 (影片文案)"</a>. <em>遠見齋</em>, 2023.</li>
</ul>
<h3>维基百科及维基词典</h3>
<ul>
<li><a href="https://en.wikipedia.org/wiki/Ligature_(writing)$0">"Ligature (writing)"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/%C3%9F$0">"ß"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/High_German_consonant_shift">"High German consonant shift"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/IJ_(digraph)$0">"IJ (digraph)"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Ampersand">"Ampersand"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Number_sign">"Number sign"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Currency_symbol">"Currency symbol"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wikipedia.org/wiki/Alchemical_symbol">"Alchemical symbol"</a>. <em>Wikipedia</em>, 2025.</li>
<li><a href="https://en.wiktionary.org/wiki/ligature">"ligature"</a>. <em>Wiktionary</em>, 2025.</li>
<li><a href="https://ja.wikipedia.org/wiki/%E5%90%88%E7%95%A5%E4%BB%AE%E5%90%8D$0">"合略仮名"</a>. <em>Wikipedia</em>, 2025.</li>
</ul>
<h3>文档及论坛</h3>
<ul>
<li>FontForge Project. <a href="https://fontforge.org/docs/tutorial/editexample4.html#creating-a-ligature$0">"6.2. Creating a Ligature"</a>. <em>FontForge Documentation</em>.</li>
<li>Topp-Thema: <a href="https://www.typografie.info/3/topic/16219-topp-thema-eszett-als-gro%C3%9Fbuchstabe/$0">"Eszett als Großbuchstabe"</a>. <em>Typografie.info</em>, Mar 8, 2003.</li>
<li>Fira Code. <a href="https://github.com/tonsky/FiraCode$0">"Fira Code: free monospaced font with programming ligatures"</a>.</li>
<li><a href="https://web.archive.org/web/20170706162042/http://www.rechtschreibrat.com/DOX/rfdr_Regeln_2017.pdf$0">"Regeln und Wörterverzeichnis"</a>. <em>Deutsche Rechtschreibung</em>, 2017.</li>
</ul>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>通过 Arduino 驱动 LCD1602（一）</title>
					<link>https://studiountagged.top/articles/2025-04-30-LCD1602</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-04-30-LCD1602/</guid>
					<pubDate>Tue, 29 Apr 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							从底层控制 LCD1602 的显示
Control the display of LCD1602 from the bottom layer
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><h2>前言</h2>
<p>为什么突然想使用 Arduino 来控制 LCD 1602？原因很简单，因为现在正在学习数字电路，下个学期又要接触单片机。所以一半是为了预习，另一半也只是纯粹为了好玩，而且手头上正好也有这么一个元件，就开始了折腾。</p>
<p>我最早是受到了 <a href="https://www.youtube.com/watch?v=hZRL8luuPb8">The 8-Bit Guy</a> 和 <a href="https://www.youtube.com/watch?v=FY3zTUaykVo&amp;t=414s">Ben Eater</a> 的视频的启发。观看这两位的视频后，觉得 LCD 1602 的引脚功能很好理解，于是便买了一个回来。得益于批量生产，这种元件的价格已经相当廉价且皮实耐造， 20 块左右便到手了。</p>
<h2>元件功能</h2>
<p>首要的任务便是弄清楚每个引脚的具体功能，然后再连接到 Arduino 上面。引脚的功能如下表：</p>
<table>
    <thead>
        <tr>
            <th><span>引脚</span><span>Pins</span></th>
            <th><span>符号</span><span>Symbol</span></th>
            <th><span>电平</span><span>Level</span></th>
            <th style="text-align: left"><span>描述</span><span>Description</span></th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>1</td>
            <td><code>GND</code></td>
            <td>0V</td>
            <td>接地</td>
        </tr>
        <tr>
            <td>2</td>
            <td><code>VCC</code></td>
            <td>5.0V</td>
            <td>正极</td>
        </tr>
        <tr>
            <td>3</td>
            <td><code>VO</code></td>
            <td>（可变）</td>
            <td>对比度调节</td>
        </tr>
        <tr>
            <td>4</td>
            <td><code>RS</code></td>
            <td>0/1</td>
            <td>寄存器选择，低电平为<strong>指令</strong>寄存器，高电平为<strong>数据</strong>寄存器</td>
        </tr>
        <tr>
            <td>5</td>
            <td><code>R/W</code></td>
            <td>0/1</td>
            <td>读写控制，低电平为<strong>写入</strong>，高电平为<strong>读取</strong></td>
        </tr>
        <tr>
            <td>6</td>
            <td><code>E</code></td>
            <td>1, 1 → 0</td>
            <td>使能，脉冲信号触发</td>
        </tr>
        <tr>
            <td>7</td>
            <td><code>DB0</code></td>
            <td>0/1</td>
            <td>数据位 0</td>
        </tr>
        <tr>
            <td>8</td>
            <td><code>DB1</code></td>
            <td>0/1</td>
            <td>数据位 1</td>
        </tr>
        <tr>
            <td>9</td>
            <td><code>DB2</code></td>
            <td>0/1</td>
            <td>数据位 2</td>
        </tr>
        <tr>
            <td>10</td>
            <td><code>DB3</code></td>
            <td>0/1</td>
            <td>数据位 3</td>
        </tr>
        <tr>
            <td>11</td>
            <td><code>DB4</code></td>
            <td>0/1</td>
            <td>数据位 4</td>
        </tr>
        <tr>
            <td>12</td>
            <td><code>DB5</code></td>
            <td>0/1</td>
            <td>数据位 5</td>
        </tr>
        <tr>
            <td>13</td>
            <td><code>DB6</code></td>
            <td>0/1</td>
            <td>数据位 6</td>
        </tr>
        <tr>
            <td>14</td>
            <td><code>DB7</code></td>
            <td>0/1</td>
            <td>数据位 7</td>
        </tr>
        <tr>
            <td>15</td>
            <td><code>A</code></td>
            <td>-</td>
            <td>背光正极</td>
        </tr>
        <tr>
            <td>16</td>
            <td><code>K</code></td>
            <td>-</td>
            <td>背光负极</td>
        </tr>
    </tbody>
</table>

<p>1 号和 2 号引脚就是元件的接地和正极，后者就接 Arduino 的 +5V 电源引脚；3 号<code>V0</code>为对比度，需要通过一个电位器连接正极和负极来调整。而 15 和 16 号引脚<code>A</code>和<code>K</code>为背光的正极（Anode）和负极（Cathode），为了避免电流可能过大，因此使用了一个 220Ω 的电阻连接到电源上。</p>
<p>由于目前仅测试 LCD 1602 的显示功能，所以 5 号读写控制引脚就直接接地。4 号寄存器选择和 6 号使能分别接 Arduino 的 12，11 号引脚。剩下的数据位就很好办了，直接按从小到大的顺序接 9~2 号引脚。</p>
<p><img src="https://studiountagged.top/articles/2025-04-30-LCD1602/img/Sketch.avif" alt="在 TinkerCAD 中绘制的电路">
<img src="https://studiountagged.top/articles/2025-04-30-LCD1602/img/IMG_20250430_195609.avif" alt="实物连接图"></p>
<p>由于我并没有排线，在实物连接的时候，只能临时通过以购买的杜邦线和跳线来接线，所以看起来就比较凌乱。而且当时不慎将数据位的连接顺序弄反了，还花了一些时间来排错。不过幸运的是，一番折腾之后屏幕终于点亮了。接下来就是写代码使其运行了。</p>
<h2>程序编写</h2>
<p>Arduino 提供了库<code>LiquidCrystal.h</code>用来驱动这类屏幕。然而我并不打算直接使用这个库中预先封装好的各种功能。因为希望能够从更底层开始控制，最终目的是通过 MOS 6502 搭建的 8 位计算机，运行汇编代码并将结果输出到这种屏幕上😎。</p>
<p>为了使其能够正常显示，第一步就是初始化屏幕（Initialize）。根据手册，在 8 位总线模式下的初始化过程如图所示：</p>
<p><img src="https://studiountagged.top/articles/2025-04-30-LCD1602/img/Initializing_Process.avif" alt="LCD 1602 在 8 位数据模式下的初始化流程图"></p>
<p>根据手册，为了确保 LCD 能够正确识别并进入 8 位总线模式，建议重复三次<code>0x30</code>指令，并每次发送前后都要稍作等待。不过为了简单起见，我就只采用发送一次的方案，信号持续时间 5ms。接下来的几条指令分别是设置显示行数为单/双、字体尺寸、开启显示、清屏、以及输入模式设定。</p>
<p><img src="https://studiountagged.top/articles/2025-04-30-LCD1602/img/Instruction_Table.avif" alt="LCD1602 指令表"></p>
<p>我采用的设置如下：</p>
<table>
    <tbody><tr>
        <th rowspan="2"><span>指令</span><span>Instruction</span></th>
        <th colspan="10"><span>指令码</span><span>Instruction Code</span></th>
        <th rowspan="2" style="text-align: left"><span>描述</span><span>Description</span></th>
    </tr>
    <tr>
        <th><code>RS</code></th>
        <th><code>R/W</code></th>
        <th><code>D7</code></th>
        <th><code>D6</code></th>
        <th><code>D5</code></th>
        <th><code>D4</code></th>
        <th><code>D3</code></th>
        <th><code>D2</code></th>
        <th><code>D1</code></th>
        <th><code>D0</code></th>
    </tr>
    <tr>
        <td><span>功能设置</span><span>Function Set</span></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>1</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>1</code></td>
        <td><code>1</code></td>
        <td><code>1</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td>
			<ul>
				<li>8 位总线模式</li>
				<li>双行显示</li>
				<li>5 * 8 字体大小</li>
			</ul>
		</td>
    </tr>
    <tr>
        <td><span>显示控制</span><span>Display ON/OFF Control</span></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>1</code></td>
        <td><code>1</code></td>
        <td><code>1</code></td>
        <td><code>1</code></td>
        <td>
			<ul>
				<li>开启显示</li>
				<li>显示光标</li>
				<li>光标闪烁</li>
			</ul>
		</td>
    </tr>
    <tr>
        <td><span>清屏</span><span>Clear Screen</span></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>1</code></td>
        <td>
			<ul>
				<li>清屏</li>
			</ul>
		</td>
    </tr>
    <tr>
        <td><span>输入模式</span><span>Entry Mode Set</span></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td><code>0</code></td>
        <td style="border-right: 2px solid #bfbfbf;"><code>0</code></td>
        <td><code>0</code></td>
        <td><code>1</code></td>
        <td><code>1</code></td>
        <td><code>0</code></td>
        <td>
			<ul>
				<li>光标向右移动（增加）</li>
				<li>整个显示内容不移动</li>
			</ul>
		</td>
    </tr>
</tbody></table>

<p>OK，现在该发送什么指令已经确定了；接下来就是写代码。</p>
<pre><code class="language-c">// LCD1602Tesing.ino

#define RS 12 &nbsp;// RS 使用引脚 12
#define E 11 &nbsp; // E 使用引脚 11

// 数据位
const int dataPins[8] = { 9, 8, 7, 6, 5, 4, 3, 2 };
</code></pre>
<p>首先定义好 Arduino 上要使用的引脚。</p>
<pre><code class="language-c">// 指令
const int funcSet   = 0b00111100; // 8 位总线模式，双行显示，5 * 8 字体大小
const int dispCtrl  = 0b00001111; // 开启显示，隐藏光标，光标不闪烁
const int entryMode = 0b00000110; // 光标向右移动（增加），整个显示内容不移动
const int cls       = 0b00000001; // 清屏
</code></pre>
<p>接下来定义要发送的指令。</p>
<p>随后便是重中之重。该如何把这些指令发送到各个引脚上呢？首先要明确写入指令的过程。由于这四条都是指令而非数据，所以<code>RS</code>应设为低电平。</p>
<p>其次就是按位发送指令。而既然提到了按位，逻辑运算肯定是少不了了。这点可以通过<code>for</code>循环从低位引脚开始，通过右移运算依次移动数据，并将该数据的最右侧的一位与数字<code>1</code>进行按位与<code>&amp;</code>运算，<code>0 &amp; 1 =0, 1 &amp; 1 = 1</code>。这样就可以确保结果只会输出一位二进制数据。</p>
<p>最后，设置好各个引脚的电平以后就要在使能引脚上发送一个脉冲以送入数据。根据时序图，一个有效的脉冲时间组成为上升、下降沿时间 $t_{R}, t_{F}$ 加上脉冲宽度（即高电平的时间）$t_{W}$ ，可得 $t=t_{R}+t_{F}+t_{W}$ 。由于上升、下降沿的时间短到几乎可以忽略不计，所以只要保证 $t\ge230ns$ 就足够了。 代码如下：</p>
<pre><code class="language-c">// 将数据写入引脚
void writeByte(bool rs, byte data, int delayMs) {
&nbsp; &nbsp; // 设置 RS 的电平
&nbsp; &nbsp; // 高电平数据模式，低电平指令模式
&nbsp; &nbsp; if (rs)
&nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(RS, HIGH);
&nbsp; &nbsp; else
&nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(RS, LOW);

&nbsp; &nbsp; // 将数据写入数据位
&nbsp; &nbsp; for (int i = 0; i &lt; 8; i++) {
&nbsp; &nbsp; &nbsp; &nbsp; digitalWrite(dataPins[i], (data &gt;&gt; i) &amp; 1);
&nbsp; &nbsp; }

&nbsp; &nbsp; // 在使能引脚上生成脉冲
&nbsp; &nbsp; digitalWrite(E, HIGH);
&nbsp; &nbsp; delay(1);
&nbsp; &nbsp; digitalWrite(E, LOW);

&nbsp; &nbsp; // 延迟，确保各个功能执行完毕
&nbsp; &nbsp; delay(delayMs);
}
</code></pre>
<p>这个函数将传入三个参数：<code>rs</code>为布尔值，选择寄存器的模式；<code>byte</code>是<code>Arduino.h</code>中定义的数据类型，其本质就是无符号 8 位整数类型，将传入一个字节大小的命令；整数<code>delayMs</code>为延迟的毫秒数。<code>digitalWrite()</code>也是在<code>Arduino.h</code>中定义的函数，传入的参数为引脚编号和电平值，可为<code>HIGH</code>或者<code>LOW</code>。这样，就可以直接调用这个函数来发送数据了。主程序如下：</p>
<pre><code class="language-c">void setup() {
&nbsp; &nbsp; Serial.begin(9600);

&nbsp; &nbsp; // 设置引脚为输出模式
&nbsp; &nbsp; pinMode(RS, OUTPUT);
&nbsp; &nbsp; pinMode(E, OUTPUT);
&nbsp; &nbsp; 
&nbsp; &nbsp; // 设置数据引脚为输出模式
&nbsp; &nbsp; for (int i = 2; i &lt;= 9; i++)
&nbsp; &nbsp; &nbsp; &nbsp; pinMode(i, OUTPUT);

&nbsp; &nbsp; // 写入数据
&nbsp; &nbsp; writeByte(false, funcSet,   5); // 功能设置
&nbsp; &nbsp; writeByte(false, dispCtrl,  1); // 显示控制
&nbsp; &nbsp; writeByte(false, cls,       1); // 清屏
&nbsp; &nbsp; writeByte(false, entryMode, 1); // 输入模式
}

void loop() {

}
</code></pre>
<p>至此，关于 LCD 1602 的初始化工作就完成了。LCD 1602 在启动时第一排会有一排白色的方块，而如果电路连接以及指令发送正确的话，成功初始化以后是看不见这行方块的。由于现在只是完成初始化了，还没有发送任何要输出的内容，所以屏幕还是一块空白，只有光标在闪烁。关于如何使其显示字符、自定义字符，以及更多复杂操作，将在后续文章中继续进行介绍。</p>
<h2>后记</h2>
<p>由于笔者这个学期学校里有数字电路的课程，以及下个学期即将到来的单片机，加上平时会观看一些 8-bit 计算机的视频，而网上也有不少通过面包板搭建这样的计算机的视频，自然而然萌生了兴趣；因此便决定趁早开始研究这些东西。笔者并不是那种坐在教室里光听理论的类型，更喜欢的方式是先动手，再根据实际情况中遇到的问题去查资料。整个过程受益颇多，也相信这是未来更深一步学习譬如汇编，以及实现通过 MOS 6502 构建一台能变成的计算机的终极目标的重要基础。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>网络信息安全的初窥门径</title>
					<link>https://studiountagged.top/articles/2025-03-26-A-Peek-to-the-Cyber-Security</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2025-03-26-A-Peek-to-the-Cyber-Security/</guid>
					<pubDate>Tue, 25 Mar 2025 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							初窥网络攻击并对其进行防御的经历。
A peek to and defend against cyber attacks. 
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>前几日笔者的朋友，也即本工作室的另一成员 Roy 在聊天软件中向我发送了如下图片：</p>
<p><img src="https://studiountagged.top/articles/2025-03-26-A-Peek-to-the-Cyber-Security/img/lastb-log.avif" alt="lastb 的日志，截图自 Termux"></p>
<p>在搭建本网站的工作中，我主要负责的是前端的设计。因此对于服务器以及后端的各种技术不甚了解。最开始，我并不知道这些文字是什么意思。后来一番询问后才得知，这是服务器登陆失败的记录。当时大吃一惊，因为这些记录密密麻麻的有很多行，几乎每天每时每刻都会有陌生 IP 尝试登陆服务器。</p>
<p>后来经过一番查询才得知，这种行为其实是非常常见的。攻击者会通过一些自动化的扫描工具，例如 masscan, zmap 等对全网进行扫描，以试图找到那些开放了 SSH 端口的服务器，并通过暴力破解尝试登陆。如果服务器采用了弱密码，后果将不堪设想。</p>
<p>虽然本工作室服务器的 root 用户采用的是随机生成的密码，若要暴力破解得花上相当长的时间，不过这也不能代表能就此高枕无忧。为了谨慎起见，我又到网上去了解了一些基本的网络安全知识，并学习了几种应对这类现象的常见防御手段。</p>
<p>第一点便是有针对性地封锁 IP。通过命令<code>lastb | awk '{print $3}' | sort | uniq -c | sort -nr | head -20</code>，可以筛选并排序出 lastb 记录里失败次数排名前 20 的 IP 地址。而<code>fail2ban</code>则会自动监控 SSH 登陆失败的记录，例如相同 IP 连续登陆失败 5 次，对其进行临时封禁。</p>
<p><img src="https://studiountagged.top/articles/2025-03-26-A-Peek-to-the-Cyber-Security/img/Ranking.avif" alt="排名前 20 的攻击者。第一列数字为尝试的次数。"></p>
<p><img src="https://studiountagged.top/articles/2025-03-26-A-Peek-to-the-Cyber-Security/img/fail2ban.avif" alt="在配置好 fail2ban 之后很快就发挥了它的作用。在写这篇文章的时候仍然有攻击者在尝试登陆服务器，而 fail2ban 不负所望地封禁了这些 IP。"></p>
<p>第二种方法是更换 SSH 使用的端口。SSH 默认会使用端口 22，而切换成其他的端口就可以大大减小被自动化扫描的几率。</p>
<p>第三种是在 SSH 连接时采用密钥的方式认证，而非密码。比起传统的密码，这种方式安全性更强。首先可以通过命令<code>ssh-keygen -t rsa -b 4096</code>来生成密钥对，也即是<code>id_rsa.pub（公钥）</code>和<code>id_rsa（私钥）</code>这两个文件。公钥就相当于家里用的密码锁，而私钥就相当于打开这把锁的钥匙。密码锁大家都能看到，但没有正确的钥匙就无法打开。在登陆服务器时，服务器会通过公钥加密一串随机的信息，要求私钥持有者解密这串信息。唯有正确的私钥持有者才能解密出正确的信息，服务器才会允许登陆。</p>
<p>在升级了我们的防御措施之后，我也开始对那些“名列前茅”的 IP 产生了兴趣。又是一番学习后，得知通过 WHOIS 查询可以得知有关该域名的诸多信息。例如，我在<code>lastb</code>记录中发现了有几条 IP 的登陆失败次数完全一致，而且IP地址也高度相似。当时便怀疑攻击者是否是使用了代理服务器或者 VPN 。而在查询后验证了这一猜想，攻击者的确是通过代理服务器发动了分布式攻击。除此之外，通过 ipinfo 还可以进一步查询到该IP精确到街道级的信息。而这一查才发现，攻击者真是来自五湖四海，全球各地。而从登陆失败用的用户名中也可见一斑，其中不乏例如 muhamed 和 ahmed 这类极具地域特色的名字。</p>
<p>这次的经历得以让我对网络安全这一领域初窥门径。以前偶尔会在新闻上听说各式各样某服务器用户数据泄露等等新闻。当时只觉得挺遥远。而现在，这类事件就近在眼前，也促使我开始了解这一领域的一些基础知识。这仅仅是冰山一角，后续随着网站的规模越来越大，要面临的安全问题肯定也不止如此，比如 XSS 注入，DDoS 等等。这篇文章就当作是记录我在这条路上的学习历程和心得感悟吧。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>幻象像素衬线体的幕后故事</title>
					<link>https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/</guid>
					<pubDate>Fri, 24 May 2024 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							关于踏上字体设计最开始的旅程的故事。
Behind the scenes - the story of how the font design journey began.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>「幻象像素衬线体」最早是在 2023 年 4 月 16 日的时候发布于 <a href="https://steven-liu.itch.io">itch.io</a> 的。不过当时并未为其撰写任何文章，最多也就项目主页寥寥数字的介绍。时隔一年，现在终于有些许闲暇时光，便决定为它写点什么，以此来纪念当初制作这款字体时的经历。这篇文章不会花太大的篇幅在介绍字体的特点上，而是会类似于一篇回忆录。所以大家大可当作幕后花絮或者个人访谈来阅读。</p>
<h2>开发过程</h2>
<p>我最开始入坑字体设计的契机是因为独立游戏开发。这款游戏是采用像素画风的。当时为了测试游戏内文本，便找了几款已有的中文像素字体。然而我对这些字体内置的西文字形不甚满意，于是逐渐萌生了自己开发的想法。</p>
<p>在网上搜索了一大圈如何开发后，意外得知了一个叫做 <a href="https://fontstruct.com">FontStruct</a> 的网站。这个网站界面简洁，上手简单、操作便捷（而且还有一点我特别喜欢的是，如果长时间未保存，页面上 Save 这个按钮便会疯狂闪烁，生怕你看不见）。于是，我的字体设计之路便由此开始。从此，这个网站就成了我最常访问的网站。</p>
<p><img src="https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/img/FontStruct.avif" alt="FontStruct 网站"></p>
<p>其次就是 <a href="https://fonts.adobe.com">Adobe Fonts</a>。对于当时对西文书法有些初步了解的我来说，每次参考都能发现一些平时忽略的细节，也逐渐学会了如何从不同的角度欣赏那些经典的作品。</p>
<p>我参考得最多的字体便是 Adobe Garamond, Trajan 以及 Noto Serif。前两者是我最喜欢的西文字体，在后来 Illusion Book 的开发中也是重要参考对象。</p>
<p>Garamond 作为金属活字时代的代表之一，其既兼有人情味和古朴的温润感，同时还能适应现代设计需要。这也是个人在众多数字复刻版本中最喜欢的一版；而 Trajan 这款源自古罗马碑文上的字体，则为后续几千年西文字体的发展奠定了基础。我尤其喜欢这款字体里面 Q 的设计，它那长长的尾巴可以把人迷得神魂颠倒。Noto Serif 的极细字重则在一定程度上帮助我剔除了每个字母外在的筋肉，揭示了内在的骨架。</p>
<p>开发这款字体正好是高三的时候。因此这件事成了每天枯燥、繁重学业以外的小小的放松时光。大部分的开发工作都是在班级的多媒体设备上完成的，有时候还得和班主任和年级组长「打游击战」；或者在实在无法碰到电脑的情况下，先暂时在格子纸上构思一下。这种模式从 2023 年 1 月份开始，一直到 4 月份高考前夕，才完成四种样式的全部设计工作。当时发布的时候也匆匆忙忙，并不知道要怎么宣传，或者说根本无暇宣传，直接在 itch.io 上点了 Publish 按钮就算完事儿了。宣传图也是在高考结束后才制作的。其实现在想来那段时光也挺有意思的，只可惜现在已经回不去了（悲）。</p>
<p><img src="https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/img/Manuscript.avif" alt="当时的部分设计手稿"></p>
<h2>遇到的问题</h2>
<p>由于当时刚刚接触字体设计，很多基础概念包括字体度量、字偶距等等名词术语看得晕头转向，更别说直接使用 FontForge 这种界面看起来就对新手很不友好的软件了。所以当初发布这款字体时是直接拿 FontStruct 生成的版本。然而为了易于上手，它牺牲了很多更进一步的设置。因此最后导出的版本字体度量数据不正确，用起来有问题。</p>
<p><img src="https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/img/ExportSample.avif" alt="由 FontStruct 直接导出的字体。字体度量的数据很明显亟须修正"></p>
<p>除此之外，这个字体家族一共有包括 Regular 在内的四种样式。而 FontStruct 的算法会根据当前字符的大小，自动计算每个单元格的尺寸，这反而导致该字体家族里单元格大小不统一的现象。</p>
<p><img src="https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/img/FontMetrics.avif" alt="两款字体的单元格尺寸在 FontForge 中的测量结果。左侧字体为 Bold 字重，右侧的为 Regular 字重。单元格的边长明显不一致"></p>
<p><img src="https://studiountagged.top/articles/2024-09-18-The-Story-Behind-Illusion-Serif/img/E-mailReply.avif" alt="向作者咨询后得到的回复"></p>
<p>当时在「艰难」地阅读 FontForge 的文档时，得知了一种叫做「标准连字」的 OpenType 特性。这种特性的运用在日常生活中很常见，但在此之前我并不是很清楚这是怎么来的，甚至一度认为是印刷错误导致的。虽然这种一对一的替换规则虽然现在看来再容易理解不过了，然而当时我花了不少时间研究了一堆网页和文章。于是这个便成了第一个我所熟知的特性。这也就是为什么该字体仅包含了两种连字特性。</p>
<p>然而这些功能给我带来的苦恼远不止四处翻找文章那么简单。我兴冲冲地生成了第一批样张后放到 Illustrator 中进行测试，却发现怎么都启用不了。最开始以为是自己有什么地方弄错了，在一遍遍的检查以后，最终都无功而返。这种问题直到我开发 Illusion Book 的时候才真正得到解决——缺少了一个名为<code>aalt</code>的特性。这个特性是我在 ｢解剖｣ 其他字体的时候才得知的。最初不解其意，｢访问全部变体 Access All Alternates｣ 这个名字也叫人看得云里雾里。后来翻阅微软讲排印的手册才得知，所有的替换规则都必须先在这里「登记」一下，排版软件才能正确读取。这也就是为什么我要在 B 站上发讲这些玩意儿的视频。现在正在准备在保留原有字形的基础上，重新绘制轮廓，并修正度量数据以及高级排版功能的信息。</p>
<h2>后记</h2>
<p>以现在的眼光看来，这个绝对算不上一份完美的作品，无论是从字形上还是技术上都有许多瑕疵。然而，当初对着一个字母反反复复斟酌的心情；以及辛辛苦苦地把 26 个英文字母全都画完，然后再将它们排成句子的时候的那种感觉，我想，应该是自己对字体设计这份热爱的根本源泉。在高考下的教育环境无疑会对学生们自由发展产生一定的扼制；但对我来说，能发掘出新的爱好，也算是意外之喜了。</p>
<p>借由这款字体，我从此便正式入坑了字体设计这一领域，也结识了不少同好。<a href="https://hanshuyuri.itch.io/">HanshuYuri</a> 曾对粗体的字重赞赏有加，并激发他开发了另一款字体（他曾经尝试与我交流，但因在 B 站上发的图文里写错了联络方式这一乌龙，导致迟迟未能联系上）。还是那句老生常谈的话，但是发自内心：感谢大家对我的支持，你们的支持是我前进的动力！</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>幻象像素衬线体 Illusion Serif</title>
					<link>https://studiountagged.top/articles/2024-09-18-Illusion-Serif</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2024-09-18-Illusion-Serif/</guid>
					<pubDate>Fri, 24 May 2024 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							像素字体设计的首个作品。
The first work in pixel typefaces designing.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p><img src="https://studiountagged.top/articles/2024-09-18-Illusion-Serif/img/1.avif" alt="">
<img src="https://studiountagged.top/articles/2024-09-18-Illusion-Serif/img/2.avif" alt="">
<img src="https://studiountagged.top/articles/2024-09-18-Illusion-Serif/img/3.avif" alt=""></p>
<p>开源的像素风格衬线体，目前支持拉丁语增补，拉丁语扩充-A，部分希腊语和科普特语以及部分西里尔文。该字体家族包含常规和粗体两种字重，常规和斜体两种样式。支持例如 fi, ffi, fl, ffl 等连字以及 ct, st, sp 的自由连字。</p>
<h2>技术指标</h2>
<p>压缩包中将包含 OTF, TTF, WOFF 和 WOFF2 四种格式的字体。与此同时还包含了 README 和 LICENSE 两则说明文件。</p>
<p>该字体的 UPM 值为 1200。若想取得最佳显示效果，字号应基于 12px 的大小进行整数倍的缩放。</p>
<p>请注意，由于本字体在字距调整时并未完全采用整数倍单元格大小的数值，这可能导致在某些情况下字体不能正确地渲染。在允许的情况下，请关闭任何的抗锯齿设置以及过滤模式，并将字距微调和追踪皆设为 0。</p>
<h2>授权及获取方式</h2>
<p>该字体采用的是 SIL 开源字体协议。</p>
<p>你可以在直接在本页面上或 <a href="https://steven-liu.itch.io/illusionserif">itch.io</a> 上下载已经封装好的字体文件，也可以直接在 GitHub 上下载该字体的 FontForge 工程文件。</p>
<h2>鸣谢</h2>
<p>整个字体家族的开发是在高三这一年中完成的。首先得感谢我的老师们，他们虽然并未对此字体的制作做出贡献，不过正是因为有了他们的悉心指导，我才能在高考中超越自我，考取了一个能让自己满意的成绩。</p>
<p>感谢我的父母。他们在这一年里作为我的坚强后盾，帮我顺利地度过了高考难关。他们也一如既往地支持着我的爱好。</p>
<p>感谢 <a href="https://hanshuyuri.itch.io/">HanshuYuri</a> 对我的肯定。他对此字体的粗体赞赏有加，我们俩人因此相识。没有什么比自己的作品受到他人赞赏这一点更令人振奋了，而且还找到了同好。</p>
<p>感谢我的挚友 Roy, Heinrich 和摇曳对我在呈现这款字体效果方面的协助，他们能不厌其烦的听我在这方面的「高谈阔论」，并提出适当的建议。</p>
<p>关于此字体开发的幕后故事，请阅读<a href="/articles/2024-09-18-The-Story-Behind-Illusion-Serif">此文章</a>。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>幻象像素书体 Illusion Book</title>
					<link>https://studiountagged.top/articles/2024-06-01-Illusion-Book</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2024-06-01-Illusion-Book/</guid>
					<pubDate>Fri, 31 May 2024 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							一款尝试在像素世界中寻铅字时代的温润的字体。
A font tries to find the warmth of the typeface era in a world of pixels.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p>「幻象像素书体」是继幻象像素衬线体后，独立设计的第二款衬线风格的像素字体。经过半年多的设计与打磨后，完成了以西文为主的支持。目前其他例如中日韩的字符集仍在开发中。</p>
<h2>设计</h2>
<p>两者在风格上很相似，甚至有些字母的字形是完全一致的。因为它们的设计理念都是希望能够在像素风格上，展现衬线体所特有的优雅，同时还要有高可读性，适用于大段文本。</p>
<p>然而，这款字体最显著的变化便是缩小了字怀。小巧的字怀使文字的排列更加紧致，灰度更加均匀，同时不失节奏感。</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/1.avif" alt="Illusion Book 和 Illusion Serif 的对比"></p>
<p>字形也并未设计成等宽的样式，因为有些字母强行拉宽会显得十分拖沓；而有的强行压缩则又会拥挤不堪。在设计时大量参考了传统的旧式字体，例如罗马大写体，对每个大写字母的宽度进行了分类，并以此为依据进行设计。</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/2.avif" alt="大写字母的设计"></p>
<p>在比例上，整套字体基于 12px 的尺寸进行设计。升部的高度为 9px，降部的高度为 3px。大写字母的高度被特意设计成比升部矮一个像素；而 6px 高的小写字母既不显得过于狭长，也不矮胖。</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/3.avif" alt="字体度量"></p>
<h2>高级排版特性</h2>
<p>虽然看上去是像素的，但并不是老旧的像素字体格式，所有的「像素」其实都是由矢量轮廓描摹出来的，除此之外，它还配备了丰富的高级排版武器，即 OpenType 特性。在专业的平面设计和排版软件（比如 Adobe InDesign 和 Illustrator）以及网页开发中，它能够让文字以更加灵活的方式呈现。</p>
<p>比如，按照西方排版惯例，自动将一些字母连写的合字（liga­ture）功能：</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/4.avif" alt="连字特性"></p>
<p>以及为了配合小写字母的旧式数字（old-style figures）：</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/5.avif" alt="旧式数字"></p>
<p>还有小型大写字母（small cap­itals）：</p>
<p><img src="https://studiountagged.top/articles/2024-06-01-Illusion-Book/img/5.avif" alt="小型大写字母"></p>
<p>除了字形以外，字符间距也经过了仔细推敲。为了美观，在制作这款字体时毅然放弃了以整数倍单元格宽度进行调整的思路，就是为了避免在某些情况下会出现「不调整看上去不太对劲，调整了又更不对劲」的尴尬地步。以 1/4 单元格宽度为基础，对大量的字偶对进行了手动微调。</p>
<h2>技术指标</h2>
<p>压缩包中将包含 OTF, TTF, WOFF 和 WOFF2 四种格式的字体。与此同时还包含了 README 和 LICENSE 两则说明文件。</p>
<p>该字体的 UPM 值为 1200。若想取得最佳显示效果，字号应基于 12px 的大小进行整数倍的缩放。</p>
<p>请注意，由于本字体在字距调整时并未完全采用整数倍单元格大小的数值，这可能导致在某些情况下字体不能正确地渲染。在允许的情况下，请关闭任何的抗锯齿设置以及过滤模式，并将字距微调和追踪皆设为 0。</p>
<h2>授权及获取方式</h2>
<p>该字体采用的是 <a href="https://openfontlicense.org/">SIL 开源字体协议</a>。</p>
<p>你可以在直接在本页面上或 <a href="https://steven-liu.itch.io/illusion-book">itch.io</a> 上下载已经封装好的字体文件，也可以直接在 <a href="https://github.com/StevenLZH/IllusionBook">GitHub</a> 上下载该字体的 FontForge 工程文件。</p>
<h2>鸣谢</h2>
<p>感谢 Roy, Heinrich 和摇曳对我在呈现这款字体效果方面的协助，特别感谢 Roy 在技术支持方面的帮助；同时，也要感谢 <a href="https://hanshuyuri.itch.io/">HanshuYuri</a> 对字体的宝贵建议，他的像素字体也非常出色。没有他们的支持，这款字体可能无法达到目前的效果。</p>
<p>感谢我的家人对我的爱好的支持。他们开明包容的态度是我走上这条道路的基础；</p>
<p>感谢 B 站上我的粉丝们对我的认可，以及 QQ 群“像素字体工房”的各路大佬分享的技术与心得。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			

				<item>
					<title>幻象像素书体 Illusion Book - 正式发布！</title>
					<link>https://studiountagged.top/articles/2024-05-25-The-Release-of-Illusion-Book</link>
					<guid isPermaLink="true">https://studiountagged.top/articles/2024-05-25-The-Release-of-Illusion-Book/</guid>
					<pubDate>Fri, 24 May 2024 16:00:00 GMT</pubDate>
					<author>Steven Liu</author>
					
					<description>
						<![CDATA[
							一款尝试在像素世界中寻铅字时代的温润的字体。
A font tries to find the warmth of the typeface era in a world of pixels.
							
						]]>
					</description>
					<content:encoded>
						<![CDATA[
							<html><head></head><body><p><img src="https://studiountagged.top/articles/2024-05-25-The-Release-of-Illusion-Book/img/A_Study_In_Scarlet.avif" alt="">
<img src="https://studiountagged.top/articles/2024-05-25-The-Release-of-Illusion-Book/img/Inscription.avif" alt="">
<img src="https://studiountagged.top/articles/2024-05-25-The-Release-of-Illusion-Book/img/Text-06-06.avif" alt=""></p>
<p>Illusion&nbsp;Book 幻象像素书体 是一款基于 12*12 px 大小的像素字体。该字体希望能够在像素风格上，展现衬线体所特有的优雅，同时还要有高可读性，适用于大段文本。</p>
<p>在经过差不多半年时间的设计与打磨之后，这款字体终于在今天正式发布了！立即下载并体验吧！</p>
<p>该字体采用了 <a href="https://openfontlicense.org/">SIL 开源字体协议</a>，你可以免费将其用于个人，非商业乃至商业目的。</p>
<p>字体文件可以戳本页面内的下载按钮；也可以参见 <a href="https://steven-liu.itch.io/illusion-book">itch.io</a> 项目主页；字体的 FontForge 工程文件也已公开至 <a href="https://github.com/StevenLZH/IllusionBook">GitHub</a>。</p>
<p>如果你喜欢这款字体的话，请在 <a href="https://afdian.com/a/steven-liu">爱发电</a> 或 <a href="https://patreon.com/StevenLiu">Patreon</a> 上支持我吧！</p>
<p>更多信息请阅读 <a href="/articles/2024-06-01-Illusion-Book">此文章</a>。</p>
</body></html>
						]]>
					</content:encoded>
				</item>
			
	</channel>
</rss>