<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>My Blog</title><description>Hi♡~！Hi♡~♪</description><link>https://chaosnap.github.io/MyBlog/</link><language>en</language><templateTheme>Firefly</templateTheme><templateThemeVersion>6.3.2</templateThemeVersion><templateThemeUrl>https://github.com/CuteLeaf/Firefly</templateThemeUrl><lastBuildDate>August 20, 2026 at 08:07:33 AM</lastBuildDate><item><title>Balancing a Furuta Pendulum: From Plant Modelling to Cascade Control</title><link>https://chaosnap.github.io/MyBlog/posts/furuta-pendulum-cascade-control/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/furuta-pendulum-cascade-control/</guid><description>What I learned from modelling and controlling a rotary inverted pendulum using transfer functions, root-locus PID design, nonlinear Simulink simulation, and a cascaded PID-PD controller.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Balancing a Furuta Pendulum: From Plant Modelling to Cascade Control&lt;/h1&gt;
&lt;p&gt;The inverted pendulum is one of those control problems that looks simple in a diagram and becomes much more interesting as soon as the model is connected to a real actuator.&lt;/p&gt;
&lt;p&gt;In this project, I worked with a &lt;strong&gt;Furuta-type rotary inverted pendulum&lt;/strong&gt;. A motor drives a horizontal arm through a gearbox, while a pendulum is attached at the end of the arm. The primary objective is to keep the pendulum upright:&lt;/p&gt;
&lt;p&gt;$$
\alpha = 0
$$&lt;/p&gt;
&lt;p&gt;but doing that alone is not enough. The rotary arm angle $\theta$ also has to remain controlled.&lt;/p&gt;
&lt;p&gt;That second requirement became the most important lesson of the project.&lt;/p&gt;
&lt;p&gt;A single-loop controller could recover the pendulum angle after a disturbance, yet the arm itself slowly drifted away. The system only behaved the way I actually wanted after I introduced a &lt;strong&gt;cascade controller&lt;/strong&gt;: a fast inner loop for $\alpha$ and a slower outer loop for $\theta$.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./furuta-rig.webp&quot; alt=&quot;Furuta-type inverted pendulum rig&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The Furuta pendulum couples a rotary arm and an inverted pendulum through one motor-driven mechanical system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This project gave me a practical way to connect several control concepts that can otherwise feel separate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mechanical modelling
    -&amp;gt; transfer functions
    -&amp;gt; root locus
    -&amp;gt; PID design
    -&amp;gt; nonlinear simulation
    -&amp;gt; cascade control
    -&amp;gt; disturbance rejection
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Start with the Mechanical Model&lt;/h2&gt;
&lt;p&gt;Before designing a controller, I first had to determine the effective rotational inertia seen at the arm axis.&lt;/p&gt;
&lt;p&gt;The motor, tachometer, gears, encoder, potentiometer, and arm all contribute inertia, but the gear train reflects each inertia through the square of the gear ratio.&lt;/p&gt;
&lt;p&gt;For a generic gear pair,&lt;/p&gt;
&lt;h1&gt;$$
J_{\text{reflected}}&lt;/h1&gt;
&lt;p&gt;\left(
\frac{N_{\text{output}}}{N_{\text{input}}}
\right)^2 J
$$&lt;/p&gt;
&lt;p&gt;After referring the drivetrain components to the rotary axis and adding the arm inertia,&lt;/p&gt;
&lt;p&gt;$$
J_{\text{arm}} = \frac{mr^2}{3}
$$&lt;/p&gt;
&lt;p&gt;the total effective inertia in my model was approximately:&lt;/p&gt;
&lt;p&gt;$$
J \approx 0.00608\ \text{kg}\cdot\text{m}^2
$$&lt;/p&gt;
&lt;p&gt;This part of the project reminded me that controller design depends directly on mechanical assumptions.&lt;/p&gt;
&lt;p&gt;A root locus can look perfect for the wrong plant.&lt;/p&gt;
&lt;p&gt;So I now think of modelling error as something that should be treated with the same seriousness as tuning error.&lt;/p&gt;
&lt;h2&gt;From Motor Voltage to Pendulum Motion&lt;/h2&gt;
&lt;p&gt;The plant contains both the pendulum mechanics and the motor electrical dynamics.&lt;/p&gt;
&lt;p&gt;The motor torque depends on the applied voltage, armature resistance and inductance, motor constants, gearbox ratio, and back EMF.&lt;/p&gt;
&lt;p&gt;The resulting model gave me two useful transfer functions:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;motor voltage Va
    -&amp;gt; pendulum angle alpha
    -&amp;gt; rotary arm angle theta
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In MATLAB, I also rebuilt the transfer functions using &lt;code&gt;series&lt;/code&gt;, &lt;code&gt;feedback&lt;/code&gt;, and &lt;code&gt;minreal&lt;/code&gt; to check the algebraic derivation.&lt;/p&gt;
&lt;p&gt;That verification step was useful because complicated symbolic expressions are easy to get wrong.&lt;/p&gt;
&lt;p&gt;The normalized pendulum-angle plant was approximately:&lt;/p&gt;
&lt;h1&gt;$$
\frac{\alpha(s)}{V_a(s)}&lt;/h1&gt;
&lt;p&gt;\frac{3.801\times10^5s}
{s^4+1.444\times10^4s^3+2.123\times10^5s^2-1.002\times10^6s-9.286\times10^6}
$$&lt;/p&gt;
&lt;p&gt;The important part was not memorising the coefficients.&lt;/p&gt;
&lt;p&gt;It was establishing one consistent plant model that could be used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;root-locus design;&lt;/li&gt;
&lt;li&gt;MATLAB verification;&lt;/li&gt;
&lt;li&gt;Simulink simulation;&lt;/li&gt;
&lt;li&gt;later cascade-control analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Lesson 1 - Translate Time-Domain Requirements into Pole Regions&lt;/h2&gt;
&lt;p&gt;The inner controller was designed to regulate the pendulum angle $\alpha$.&lt;/p&gt;
&lt;p&gt;The required transient response was:&lt;/p&gt;
&lt;p&gt;$$
T_p \leq 0.2\ \text{s}
$$&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;$$
%OS \leq 20%
$$&lt;/p&gt;
&lt;p&gt;For a dominant second-order approximation,&lt;/p&gt;
&lt;p&gt;$$
T_p = \frac{\pi}{\omega_d}
$$&lt;/p&gt;
&lt;p&gt;so:&lt;/p&gt;
&lt;p&gt;$$
\omega_d \geq 5\pi
$$&lt;/p&gt;
&lt;p&gt;The overshoot requirement gives approximately:&lt;/p&gt;
&lt;p&gt;$$
\zeta \geq 0.456
$$&lt;/p&gt;
&lt;p&gt;Those two equations convert a time-domain specification into an allowed region of the $s$-plane.&lt;/p&gt;
&lt;p&gt;That was the part of root locus that became much more meaningful to me during this project.&lt;/p&gt;
&lt;p&gt;Instead of asking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Where should I put the poles?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I could ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Which pole locations correspond to the response I actually want?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./pid-root-locus.webp&quot; alt=&quot;Compensated root locus for the pendulum-angle loop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The root-locus design region was defined by the peak-time and damping-ratio requirements.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Lesson 2 - PID Design Is Easier to Understand as Pole-Zero Shaping&lt;/h2&gt;
&lt;p&gt;The inner loop used a PID compensator:&lt;/p&gt;
&lt;h1&gt;$$
PID_\alpha(s)&lt;/h1&gt;
&lt;p&gt;K_{P,\alpha}
+
K_{D,\alpha}s
+
\frac{K_{I,\alpha}}{s}
$$&lt;/p&gt;
&lt;p&gt;I selected two real zeros and an integrator.&lt;/p&gt;
&lt;p&gt;The final compensator was:&lt;/p&gt;
&lt;h1&gt;$$
PID_\alpha(s)&lt;/h1&gt;
&lt;p&gt;2.4\frac{(s+25)(s+8.5)}{s}
$$&lt;/p&gt;
&lt;p&gt;which gives:&lt;/p&gt;
&lt;p&gt;$$
K_{D,\alpha}=2.4
$$&lt;/p&gt;
&lt;p&gt;$$
K_{P,\alpha}=80.4
$$&lt;/p&gt;
&lt;p&gt;$$
K_{I,\alpha}=510
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./inner-loop-structure.webp&quot; alt=&quot;Inner-loop control structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The inner loop uses the pendulum-angle error to generate the motor-voltage command.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I found it more useful to think of this controller as a &lt;strong&gt;pole-zero shaping device&lt;/strong&gt; than as three independent gains.&lt;/p&gt;
&lt;p&gt;The integrator changes the low-frequency structure.&lt;/p&gt;
&lt;p&gt;The two zeros reshape the root locus.&lt;/p&gt;
&lt;p&gt;The overall gain then selects the closed-loop pole locations.&lt;/p&gt;
&lt;p&gt;That viewpoint made the tuning process much less arbitrary.&lt;/p&gt;
&lt;h2&gt;The Linear Controller Met the Original Specification&lt;/h2&gt;
&lt;p&gt;The closed-loop step response gave:&lt;/p&gt;
&lt;p&gt;$$
T_p = 0.0619\ \text{s}
$$&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;$$
%OS = 14.1389%
$$&lt;/p&gt;
&lt;p&gt;Both values satisfy the original requirements.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pid-step-response.webp&quot; alt=&quot;Closed-loop step response of the pendulum-angle controller&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The designed inner PID loop meets the peak-time and overshoot requirements in the linear model.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At this stage, it would be tempting to say that the controller design was finished.&lt;/p&gt;
&lt;p&gt;The nonlinear simulation showed why that conclusion would have been premature.&lt;/p&gt;
&lt;h2&gt;Lesson 3 - Stabilising the Pendulum Is Not the Same as Controlling the System&lt;/h2&gt;
&lt;p&gt;I implemented the PID controller in the nonlinear Simulink plant and applied a short voltage disturbance:&lt;/p&gt;
&lt;p&gt;$$
V_d(t)=u(t-1)-u(t-1.025)
$$&lt;/p&gt;
&lt;p&gt;The pendulum angle behaved well.&lt;/p&gt;
&lt;p&gt;After the disturbance, $\alpha$ returned close to zero.&lt;/p&gt;
&lt;p&gt;But the arm angle $\theta$ did not.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./single-loop-drift.webp&quot; alt=&quot;Single-loop nonlinear simulation showing arm-angle drift&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The inner PID loop recovers the pendulum angle, but the rotary arm position continues to drift because theta is not directly regulated.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This result was one of the most useful parts of the project.&lt;/p&gt;
&lt;p&gt;The controller was doing exactly what I had asked it to do.&lt;/p&gt;
&lt;p&gt;It was minimising:&lt;/p&gt;
&lt;p&gt;$$
e_\alpha(t)=\alpha_{\text{ref}}(t)-\alpha(t)
$$&lt;/p&gt;
&lt;p&gt;with:&lt;/p&gt;
&lt;p&gt;$$
\alpha_{\text{ref}}=0
$$&lt;/p&gt;
&lt;p&gt;There was no term in that loop asking the arm to return to its original position.&lt;/p&gt;
&lt;p&gt;So when the arm moved to recover the pendulum, nothing forced $\theta$ back to zero.&lt;/p&gt;
&lt;p&gt;This is a very general control lesson:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A controller cannot regulate a variable that never appears in its feedback objective.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The problem was not that the inner PID was badly tuned.&lt;/p&gt;
&lt;p&gt;The problem was that the control architecture was incomplete.&lt;/p&gt;
&lt;h2&gt;Lesson 4 - Cascade Control Solves a Structural Problem&lt;/h2&gt;
&lt;p&gt;The solution was to add a second loop.&lt;/p&gt;
&lt;p&gt;The inner loop remains responsible for keeping the pendulum upright.&lt;/p&gt;
&lt;p&gt;The outer loop regulates the rotary arm angle.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    R[&quot;Theta Reference&quot;] --&amp;gt; O[&quot;Outer PD&quot;]
    O --&amp;gt; A[&quot;Alpha Reference&quot;]
    A --&amp;gt; I[&quot;Inner PID&quot;]
    I --&amp;gt; P[&quot;Pendulum Plant&quot;]
    P --&amp;gt; AL[&quot;Alpha&quot;]
    AL --&amp;gt; ARM[&quot;Arm Dynamics&quot;]
    ARM --&amp;gt; TH[&quot;Theta&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important requirement is that the inner loop should be faster than the outer loop.&lt;/p&gt;
&lt;p&gt;The outer controller assumes that when it requests a small change in $\alpha_{\text{ref}}$, the inner controller can realise that request quickly enough.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cascade-structure.webp&quot; alt=&quot;Cascade control structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The outer theta loop generates the reference for the faster inner alpha loop.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This gave me a more practical understanding of hierarchical control.&lt;/p&gt;
&lt;p&gt;The two loops do not have equal responsibilities.&lt;/p&gt;
&lt;p&gt;The inner loop handles the fast unstable pendulum dynamics.&lt;/p&gt;
&lt;p&gt;The outer loop handles the slower arm-position objective.&lt;/p&gt;
&lt;h2&gt;Lesson 5 - The Outer Controller Should Respect the Inner-Loop Dynamics&lt;/h2&gt;
&lt;p&gt;For the outer loop, I used a PD compensator with a first-order derivative filter.&lt;/p&gt;
&lt;p&gt;The tuned controller was approximately:&lt;/p&gt;
&lt;p&gt;$$
PD_\theta(s)
\approx
-0.01735
\frac{1+3.1s}{1+0.15s}
$$&lt;/p&gt;
&lt;p&gt;An equivalent Simulink PID-block representation was:&lt;/p&gt;
&lt;p&gt;$$
P_\theta=-0.01735
$$&lt;/p&gt;
&lt;p&gt;$$
D_\theta=-0.05074
$$&lt;/p&gt;
&lt;p&gt;$$
N_\theta=6.57
$$&lt;/p&gt;
&lt;p&gt;The selected zero and pole were approximately:&lt;/p&gt;
&lt;p&gt;$$
z=-0.325
$$&lt;/p&gt;
&lt;p&gt;$$
p=-6.57
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./outer-root-locus.webp&quot; alt=&quot;Compensated root locus for the outer loop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The compensated outer-loop poles remain in the left-half plane, with the slowest pole dominating the arm response.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The dominant closed-loop pole was approximately:&lt;/p&gt;
&lt;p&gt;$$
s_1=-0.378
$$&lt;/p&gt;
&lt;p&gt;while the remaining poles were further into the left-half plane.&lt;/p&gt;
&lt;p&gt;That is exactly what I wanted from the cascade architecture: a visibly slower outer loop wrapped around a faster inner loop.&lt;/p&gt;
&lt;h2&gt;Lesson 6 - Stability Margins Are More Informative Than a Stable Pole Plot Alone&lt;/h2&gt;
&lt;p&gt;I also checked the compensated outer loop in the frequency domain.&lt;/p&gt;
&lt;p&gt;The resulting margins were approximately:&lt;/p&gt;
&lt;p&gt;$$
GM = 6.8\ \text{dB}
$$&lt;/p&gt;
&lt;p&gt;and&lt;/p&gt;
&lt;p&gt;$$
PM = 60^\circ
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./outer-bode.webp&quot; alt=&quot;Bode response of the tuned outer loop&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The outer-loop controller retains useful gain and phase margins after compensation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The poles tell me whether the nominal model is stable.&lt;/p&gt;
&lt;p&gt;The gain and phase margins give another view of how much uncertainty or additional phase lag the loop can tolerate before losing stability.&lt;/p&gt;
&lt;p&gt;That distinction became more important to me once the controller moved from a transfer-function diagram into a nonlinear simulation.&lt;/p&gt;
&lt;h2&gt;Lesson 7 - Non-Minimum-Phase Behaviour Can Look Wrong Even When It Is Correct&lt;/h2&gt;
&lt;p&gt;The outer-loop step response initially moved in the &lt;strong&gt;opposite direction&lt;/strong&gt; from the commanded arm motion.&lt;/p&gt;
&lt;p&gt;Then it reversed and moved toward the reference.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./cascade-recovery.webp&quot; alt=&quot;Cascade nonlinear response&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The cascade controller keeps both angles bounded and removes the long-term arm drift seen with the single-loop controller.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;My first instinct when seeing an inverse response like this is to suspect a sign error.&lt;/p&gt;
&lt;p&gt;In this case, the behaviour was expected from the non-minimum-phase nature of the inverted-pendulum system.&lt;/p&gt;
&lt;p&gt;This is an important debugging habit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An unintuitive transient is not automatically an implementation bug.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Before changing gains or signs, check whether the plant itself contains zeros or coupled dynamics that predict the behaviour.&lt;/p&gt;
&lt;h2&gt;Lesson 8 - A Good Inner Loop Makes the Outer Loop Possible&lt;/h2&gt;
&lt;p&gt;In the nonlinear cascade simulation, both variables remained bounded.&lt;/p&gt;
&lt;p&gt;The pendulum angle $\alpha$ moved during the transient and then returned close to zero.&lt;/p&gt;
&lt;p&gt;The rotary arm angle $\theta$ also returned to the intended value instead of continuing to drift.&lt;/p&gt;
&lt;p&gt;This is what the single-loop controller could not achieve.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Inner loop:
keep alpha near zero

Outer loop:
choose alpha_ref so theta goes where we want
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That is the key idea behind the architecture.&lt;/p&gt;
&lt;p&gt;The outer loop does not directly command motor voltage.&lt;/p&gt;
&lt;p&gt;It requests a pendulum-angle behaviour from the inner loop.&lt;/p&gt;
&lt;p&gt;That separation of objectives is what makes the cascade structure work.&lt;/p&gt;
&lt;h2&gt;Lesson 9 - Test More Than One Reference and More Than One Disturbance&lt;/h2&gt;
&lt;p&gt;I tested the nonlinear cascade controller with several cases:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10 degree theta reference, no disturbance
20 degree theta reference, no disturbance
20 degree theta reference, 0.5 V disturbance
20 degree theta reference, 2 V disturbance
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./reference-disturbance-tests.webp&quot; alt=&quot;Reference and disturbance test cases&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The same cascade controller was tested with different reference magnitudes and disturbance levels.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The 10-degree and 20-degree cases both showed the expected initial inverse response before $\theta$ moved toward the command.&lt;/p&gt;
&lt;p&gt;For the disturbance tests, the voltage disturbance was applied at:&lt;/p&gt;
&lt;p&gt;$$
t=1.25\ \text{s}
$$&lt;/p&gt;
&lt;p&gt;The 0.5 V case produced a relatively small additional transient.&lt;/p&gt;
&lt;p&gt;The 2 V case produced a more aggressive response, especially in $\alpha$, but both angles remained bounded and recovered.&lt;/p&gt;
&lt;p&gt;This is a much stronger validation than showing only one clean nominal step response.&lt;/p&gt;
&lt;p&gt;A controller that performs well on one operating point may still be fragile.&lt;/p&gt;
&lt;p&gt;Even simple variation in reference magnitude and disturbance size can reveal a lot.&lt;/p&gt;
&lt;h2&gt;What I Would Change in a Second Version&lt;/h2&gt;
&lt;h3&gt;1. Separate model validation from controller tuning&lt;/h3&gt;
&lt;p&gt;I would first validate the plant model against experimental data before spending too much time refining the compensator.&lt;/p&gt;
&lt;p&gt;A precise controller for an inaccurate plant is not necessarily useful.&lt;/p&gt;
&lt;h3&gt;2. Add actuator saturation explicitly&lt;/h3&gt;
&lt;p&gt;The linear design assumes the requested motor voltage is available.&lt;/p&gt;
&lt;p&gt;A practical implementation should include:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;voltage saturation
rate limits
anti-windup
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;especially because the inner loop contains integral action.&lt;/p&gt;
&lt;h3&gt;3. Quantify the cascade bandwidth separation&lt;/h3&gt;
&lt;p&gt;Instead of only reasoning qualitatively that the inner loop is faster, I would compare closed-loop bandwidths directly.&lt;/p&gt;
&lt;p&gt;A practical target could be expressed as:&lt;/p&gt;
&lt;p&gt;$$
\omega_{\text{BW,inner}}
\gg
\omega_{\text{BW,outer}}
$$&lt;/p&gt;
&lt;h3&gt;4. Compare linear and nonlinear responses systematically&lt;/h3&gt;
&lt;p&gt;The nonlinear model exposed behaviour that the simple linear step response did not.&lt;/p&gt;
&lt;p&gt;I would automate comparison across:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;linear model
nonlinear model
different disturbances
different references
parameter perturbations
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. Move toward state-space control&lt;/h3&gt;
&lt;p&gt;For a coupled system such as the Furuta pendulum, a state-space design would be a natural next step.&lt;/p&gt;
&lt;p&gt;It would allow the arm and pendulum states to be handled inside one multivariable framework instead of coordinating two SISO loops.&lt;/p&gt;
&lt;p&gt;The cascade design is very useful for understanding the system, but it also makes the coupling visible enough to motivate more advanced control methods.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The most important lesson from this project was not how to tune a PID controller.&lt;/p&gt;
&lt;p&gt;It was learning that &lt;strong&gt;control performance depends on choosing the correct architecture before choosing the gains&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The inner PID controller was successful by its own specification:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it met the peak-time requirement;&lt;/li&gt;
&lt;li&gt;it met the overshoot requirement;&lt;/li&gt;
&lt;li&gt;it recovered the pendulum after a disturbance.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And yet the complete system still had an undesirable behaviour: the rotary arm drifted.&lt;/p&gt;
&lt;p&gt;The cascade controller solved that because it added the missing control objective.&lt;/p&gt;
&lt;p&gt;That changed the way I think about feedback design.&lt;/p&gt;
&lt;p&gt;I now ask these questions before tuning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which variables actually need regulation?&lt;/li&gt;
&lt;li&gt;Which dynamics are fast and which are slow?&lt;/li&gt;
&lt;li&gt;What should each loop be responsible for?&lt;/li&gt;
&lt;li&gt;Does every important variable appear somewhere in the feedback architecture?&lt;/li&gt;
&lt;li&gt;Is an unexpected transient caused by the controller, or by the plant itself?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The project started as a PID and root-locus exercise.&lt;/p&gt;
&lt;p&gt;For me, it ended as a lesson in &lt;strong&gt;system-level control architecture&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A well-tuned controller can still solve the wrong problem. Good control design starts by deciding what the complete system must regulate.&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Building a Real-Time Embedded DAQ System: Lessons from Firmware, Sensors, and Integration</title><link>https://chaosnap.github.io/MyBlog/posts/embedded-daq-system-lessons/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/embedded-daq-system-lessons/</guid><description>What I learned while building an eight-channel embedded data acquisition system: component selection, sensor integration, firmware scheduling, UART protocols, and hardware-software integration.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a Real-Time Embedded DAQ System: Lessons from Firmware, Sensors, and Integration&lt;/h1&gt;
&lt;p&gt;Some embedded projects look simple when reduced to a block diagram: sample a few signals, process them on a microcontroller, and send the values to a PC. In practice, the difficult part is rarely a single peripheral or a single function. The real challenge is making &lt;strong&gt;hardware, firmware, timing, communication, and user-facing software behave like one system&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I learned this while working on an industrial-style multi-channel data acquisition system. The final prototype combined voltage acquisition, acceleration and temperature sensing, configurable alarms, non-volatile settings, a local LCD interface, and a Qt-based desktop application for visualization and logging.&lt;/p&gt;
&lt;p&gt;My main responsibility was the firmware side of the project, especially sensor integration, alarm control, EEPROM configuration, and communication with the GUI. Looking back, the most valuable part of the project was not learning another I2C register map or writing another UART interrupt routine. It was learning how to reason about the &lt;strong&gt;interfaces between modules&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./prototype.webp&quot; alt=&quot;Prototype of the embedded DAQ system&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An early proof-of-concept prototype used to bring the firmware, display, sensors, and communication path together.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;System at a Glance&lt;/h2&gt;
&lt;p&gt;The system was organized around eight logical channels:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Channels&lt;/th&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Main role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CH1-CH4&lt;/td&gt;
&lt;td&gt;Voltage&lt;/td&gt;
&lt;td&gt;Multi-channel external voltage acquisition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CH5-CH7&lt;/td&gt;
&lt;td&gt;Acceleration X/Y/Z&lt;/td&gt;
&lt;td&gt;Motion sensing through a 3-axis accelerometer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CH8&lt;/td&gt;
&lt;td&gt;Temperature&lt;/td&gt;
&lt;td&gt;Board/environment temperature monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Around these channels, the firmware also had to manage alarm thresholds, alarm modes, display updates, persistent configuration, and serial communication with the PC application.&lt;/p&gt;
&lt;p&gt;A simplified view of the architecture is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    V[Voltage Inputs CH1-CH4] --&amp;gt; AFE[Analog Front End]
    AFE --&amp;gt; ADC[Multi-Channel ADC]

    ACC[LIS3DH Accelerometer] --&amp;gt; MCU[ATmega328P Firmware]
    TMP[LM335 Temperature Sensor] --&amp;gt; MCU
    ADC --&amp;gt; MCU

    MCU --&amp;gt; LCD[Local LCD]
    MCU --&amp;gt; LED[Alarm LEDs]
    MCU --&amp;gt; EEPROM[EEPROM Configuration]
    MCU &amp;lt;--&amp;gt; UART[UART Protocol]
    UART &amp;lt;--&amp;gt; GUI[Qt GUI / Logger]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This diagram looks clean. The implementation was not.&lt;/p&gt;
&lt;p&gt;The system only became reliable after the boundaries between these blocks were treated as engineering contracts rather than informal assumptions.&lt;/p&gt;
&lt;h2&gt;Lesson 1 - Requirements Should Drive the Component Choice&lt;/h2&gt;
&lt;p&gt;One of the largest changes happened around the ADC.&lt;/p&gt;
&lt;p&gt;The early plan used a lower-cost ADC because it looked sufficient during initial planning. Once the full sampling requirements were considered, it became clear that the device could not satisfy the required combination of resolution, sampling rate, and simultaneous multi-channel acquisition.&lt;/p&gt;
&lt;p&gt;The design was therefore changed to an &lt;strong&gt;ADS8584S&lt;/strong&gt;, which supports simultaneous four-channel conversion and a much higher sampling rate.&lt;/p&gt;
&lt;p&gt;That change solved one problem but immediately created several new ones:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more complicated timing control;&lt;/li&gt;
&lt;li&gt;parallel or higher-throughput data handling;&lt;/li&gt;
&lt;li&gt;multi-channel synchronization;&lt;/li&gt;
&lt;li&gt;larger buffering requirements;&lt;/li&gt;
&lt;li&gt;additional firmware states;&lt;/li&gt;
&lt;li&gt;more integration work with the analog front end.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was an important lesson for me: &lt;strong&gt;a cheaper or simpler part is not actually simpler if it pushes complexity into the rest of the system&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A component decision should not be evaluated only by unit price or headline specifications. I now try to ask four questions much earlier:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Does it satisfy the worst-case requirement rather than the nominal case?&lt;/li&gt;
&lt;li&gt;What new firmware states does it introduce?&lt;/li&gt;
&lt;li&gt;What timing assumptions does it create?&lt;/li&gt;
&lt;li&gt;How difficult will it be to verify during integration?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In embedded design, the cost of changing a component late is often much larger than the cost difference between the components themselves.&lt;/p&gt;
&lt;h2&gt;Lesson 2 - The Firmware-to-GUI Interface Is an API&lt;/h2&gt;
&lt;p&gt;My firmware had to continuously exchange configuration and measurement data with the desktop application. At first, it is tempting to think of UART as simply &quot;printing values.&quot;&lt;/p&gt;
&lt;p&gt;That approach breaks very quickly once both sides become non-trivial.&lt;/p&gt;
&lt;p&gt;The GUI needed more than a number. For each channel, it needed context such as current value, alarm state, alarm mode, thresholds, and sensor metadata. The firmware also needed to receive commands for connection state, configuration updates, and logging control.&lt;/p&gt;
&lt;p&gt;I therefore treated the serial format as an explicit protocol.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./packet-format.webp&quot; alt=&quot;Firmware-to-GUI packet layout&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The communication format defined a fixed layout so the GUI could interpret each channel deterministically.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A simplified conceptual frame looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;DAT | input-range | CH1 | CH2 | CH3 | CH4 | CH5 | CH6 | CH7 | CH8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important part was not the delimiter itself. The important part was that &lt;strong&gt;both sides agreed on the meaning and order of every field&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The receive path also used defined command tags rather than arbitrary free-form text. Commands represented events such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;STA&amp;gt;    GUI connected
&amp;lt;END&amp;gt;    GUI disconnected
CFG...   configuration update
LOGS     start logging state
LOGE     end logging state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation made debugging much easier. When something failed, I could ask a precise question:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is the sensor value wrong, is the packet wrong, or is the GUI parser wrong?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Without a protocol contract, all three problems can look identical from the user interface.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./uart-parser.webp&quot; alt=&quot;UART receive and command parsing logic&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The UART receive path parses complete messages and converts them into system-level events.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;:::tip[What I would do differently]
If I rebuilt this system, I would define the protocol document before writing either the firmware parser or the GUI parser. I would also include an explicit protocol version and checksum from the first revision rather than treating them as later improvements.
:::&lt;/p&gt;
&lt;h2&gt;Lesson 3 - Sensor Integration Is About Data Meaning, Not Just I2C&lt;/h2&gt;
&lt;p&gt;The 3-axis accelerometer was one of the more useful examples of why &quot;the communication works&quot; does not mean &quot;the sensor works.&quot;&lt;/p&gt;
&lt;p&gt;The accelerometer used I2C. At the bus level, the task was straightforward:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;configure the device;&lt;/li&gt;
&lt;li&gt;select the output register;&lt;/li&gt;
&lt;li&gt;read six consecutive bytes;&lt;/li&gt;
&lt;li&gt;combine low and high bytes;&lt;/li&gt;
&lt;li&gt;convert the result into signed axis values.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;But several details mattered:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the output data rate;&lt;/li&gt;
&lt;li&gt;the selected measurement range;&lt;/li&gt;
&lt;li&gt;high-resolution mode;&lt;/li&gt;
&lt;li&gt;byte ordering;&lt;/li&gt;
&lt;li&gt;signed conversion;&lt;/li&gt;
&lt;li&gt;update timing;&lt;/li&gt;
&lt;li&gt;how the values were mapped to application channels.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A register transaction can be electrically correct and still produce useless application data.&lt;/p&gt;
&lt;p&gt;For this project, the accelerometer values became CH5, CH6, and CH7. That meant the sensor driver also had to fit the timing model and packet model used by the rest of the system.&lt;/p&gt;
&lt;p&gt;This changed how I think about drivers. A useful embedded driver is not only:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read_register();
write_register();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It should expose &lt;strong&gt;meaningful data at a predictable time&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;For example, the application layer should be able to think in terms of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;accel_update();
get_accel_x();
get_accel_y();
get_accel_z();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than repeatedly reconstructing low-level transactions.&lt;/p&gt;
&lt;p&gt;That boundary becomes increasingly important as a project grows.&lt;/p&gt;
&lt;h2&gt;Lesson 4 - &quot;Real-Time&quot; Often Starts with Scheduling Discipline&lt;/h2&gt;
&lt;p&gt;This system did not need a full RTOS to benefit from real-time thinking.&lt;/p&gt;
&lt;p&gt;Different tasks naturally operated at different rates:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;alarm checking needed relatively frequent updates;&lt;/li&gt;
&lt;li&gt;environmental and motion sensors could update more slowly;&lt;/li&gt;
&lt;li&gt;serial command parsing had to remain responsive;&lt;/li&gt;
&lt;li&gt;display and GUI transmission had their own update cadence.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A simplified main-loop structure looked conceptually like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;while (1) {
    parse_uart_commands();

    if (elapsed_ms(last_alarm_check) &amp;gt;= 10) {
        update_alarms();
        last_alarm_check = now_ms();
    }

    if (elapsed_ms(last_sensor_update) &amp;gt;= 100) {
        update_temperature();
        update_accelerometer();
        last_sensor_update = now_ms();
    }

    if (gui_connected) {
        transmit_latest_frame();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact implementation details are project-specific, but the principle is general:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do not let every module run whenever it wants.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Even a simple cooperative loop becomes much easier to reason about when each task has an expected period and bounded responsibility.&lt;/p&gt;
&lt;p&gt;This also made instrumentation easier. We could use a logic analyzer and serial output to check whether a sensor update or transaction was completing within the expected time.&lt;/p&gt;
&lt;p&gt;The lesson carried into my later work with RTOS-based and DMA-based systems: an RTOS does not automatically make a system real-time. &lt;strong&gt;Clear timing ownership does.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Lesson 5 - Configuration Persistence Changes the Product Experience&lt;/h2&gt;
&lt;p&gt;The alarm subsystem supported configurable thresholds and modes. Those settings were controlled from the GUI and stored in EEPROM.&lt;/p&gt;
&lt;p&gt;This sounds like a small feature, but it changed the character of the device.&lt;/p&gt;
&lt;p&gt;Without persistent configuration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;power on;&lt;/li&gt;
&lt;li&gt;connect GUI;&lt;/li&gt;
&lt;li&gt;re-enter settings;&lt;/li&gt;
&lt;li&gt;start using the system.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With EEPROM-backed configuration:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;power on;&lt;/li&gt;
&lt;li&gt;restore the previous known state;&lt;/li&gt;
&lt;li&gt;validate it;&lt;/li&gt;
&lt;li&gt;operate immediately.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I used a validity marker so startup code could distinguish between valid saved data and uninitialized EEPROM. If the configuration was invalid, the firmware fell back to defaults.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (eeprom_signature == CONFIG_VALID) {
    load_config_from_eeprom();
} else {
    load_default_config();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That pattern is simple, but it introduced an important product-level concept: &lt;strong&gt;startup state is part of the interface&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Embedded systems are often judged by what happens at the boundaries - boot, disconnect, reconnect, reset, sensor failure - rather than only by their steady-state behavior.&lt;/p&gt;
&lt;h2&gt;Lesson 6 - Hardware and Software Must Be Integrated Early&lt;/h2&gt;
&lt;p&gt;One of the biggest sources of wasted time in team projects is waiting until each subsystem is &quot;finished&quot; before integration.&lt;/p&gt;
&lt;p&gt;Our system had several independently developed parts:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;analog and digital hardware;&lt;/li&gt;
&lt;li&gt;firmware;&lt;/li&gt;
&lt;li&gt;the PC GUI;&lt;/li&gt;
&lt;li&gt;sensor modules;&lt;/li&gt;
&lt;li&gt;display and alarm logic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The PCB and the desktop interface were being developed in parallel with the firmware.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pcb-layout.webp&quot; alt=&quot;PCB layout used for the integrated system&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The PCB stage forced firmware assumptions to become concrete pin, voltage, interface, and timing decisions.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The GUI also made integration issues visible very quickly.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./gui-live-data.webp&quot; alt=&quot;Qt-based live data visualization&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The desktop interface provided real-time visualization, configuration, and data logging for the embedded system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;When the GUI displayed a wrong value, the root cause could be almost anywhere:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Sensor
  -&amp;gt; electrical interface
  -&amp;gt; driver
  -&amp;gt; conversion
  -&amp;gt; channel mapping
  -&amp;gt; packet formatting
  -&amp;gt; serial transport
  -&amp;gt; parser
  -&amp;gt; plot
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That debugging chain is why I now prefer &lt;strong&gt;vertical integration&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Instead of finishing every driver first, I would rather make one channel work end-to-end:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;one sensor -&amp;gt; one firmware path -&amp;gt; one packet field -&amp;gt; one GUI display
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then scale the pattern to the remaining channels.&lt;/p&gt;
&lt;p&gt;This produces a working reference path early and reduces the number of unknowns during debugging.&lt;/p&gt;
&lt;p&gt;:::important[The integration lesson]
A module is not finished when its unit test passes. It is finished when the next module can consume its output reliably.
:::&lt;/p&gt;
&lt;h2&gt;What I Would Improve in a Second Version&lt;/h2&gt;
&lt;p&gt;If I were to redesign the system today, I would keep the same modular direction but make several changes earlier:&lt;/p&gt;
&lt;h3&gt;1. Define protocol versioning from day one&lt;/h3&gt;
&lt;p&gt;A simple version field would make firmware and GUI compatibility much easier to manage.&lt;/p&gt;
&lt;h3&gt;2. Separate acquisition from presentation more aggressively&lt;/h3&gt;
&lt;p&gt;Sampling, alarm evaluation, packet generation, and LCD updates should consume a shared data model instead of depending on each other directly.&lt;/p&gt;
&lt;h3&gt;3. Add structured error reporting&lt;/h3&gt;
&lt;p&gt;Instead of only transmitting measurement frames, I would include explicit status flags for sensor faults, stale data, buffer overflow, configuration errors, and communication timeouts.&lt;/p&gt;
&lt;h3&gt;4. Build an automated protocol test harness&lt;/h3&gt;
&lt;p&gt;A Python script could emulate the GUI, replay command sequences, and validate outgoing frames without requiring the full desktop application.&lt;/p&gt;
&lt;h3&gt;5. Schedule integration checkpoints&lt;/h3&gt;
&lt;p&gt;A team integration session every week would have exposed interface mismatches earlier and reduced rework near the end of the project.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This project changed my understanding of embedded systems.&lt;/p&gt;
&lt;p&gt;Before building it, I tended to think about firmware mainly in terms of peripherals: ADC, I2C, UART, EEPROM, GPIO.&lt;/p&gt;
&lt;p&gt;After building it, I started thinking more in terms of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;interfaces;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;timing;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;state;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;data ownership;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;failure boundaries;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;integration contracts.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those concepts are less visible than a schematic or a code listing, but they are what allow a prototype to become a system.&lt;/p&gt;
&lt;p&gt;The most useful lesson I took away is simple:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Good embedded design is not about making every module individually clever. It is about making the boundaries between modules boring, predictable, and easy to debug.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is a principle I have kept using in later embedded, FPGA, and edge-computing projects.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Designing a Multiplierless ADFT on FPGA: Pipelining, Fixed-Point Arithmetic, and Spectrum Sensing</title><link>https://chaosnap.github.io/MyBlog/posts/fpga-adft-spectrum-sensing/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/fpga-adft-spectrum-sensing/</guid><description>What I learned while implementing an 8-point multiplierless Approximate DFT on FPGA, from MATLAB and HDL Coder verification to pipelining, fixed-point refinement, FPGA-in-the-loop testing, and standalone Nexys 4 deployment.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Designing a Multiplierless ADFT on FPGA: Pipelining, Fixed-Point Arithmetic, and Spectrum Sensing&lt;/h1&gt;
&lt;p&gt;One of the most useful FPGA exercises I worked through was implementing a small spectrum-sensing pipeline based on an &lt;strong&gt;8-point Approximate Discrete Fourier Transform (ADFT)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;At the algorithm level, the idea is attractive: instead of implementing a conventional DFT with a large number of complex multiplications, the approximate transform replaces its coefficients with structures that can be realised through &lt;strong&gt;addition, subtraction, and binary shifts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That sounds like a straightforward hardware optimisation.&lt;/p&gt;
&lt;p&gt;In practice, the more interesting part was everything that came after the equation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;deciding how to represent the arithmetic in fixed point;&lt;/li&gt;
&lt;li&gt;mapping the transform into a parallel datapath;&lt;/li&gt;
&lt;li&gt;balancing pipeline delays;&lt;/li&gt;
&lt;li&gt;verifying hardware against MATLAB;&lt;/li&gt;
&lt;li&gt;measuring the real critical path after synthesis and routing;&lt;/li&gt;
&lt;li&gt;integrating ROM, SIPO registers, power estimation, and display logic into a standalone FPGA system.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This project gave me a much clearer view of the difference between &lt;strong&gt;an algorithm that is hardware-friendly&lt;/strong&gt; and &lt;strong&gt;a hardware architecture that is actually well designed&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./adft-flow.webp&quot; alt=&quot;8-point ADFT signal-flow architecture&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The transform can be expressed as a butterfly-style network in which constant multiplications are replaced by add/subtract and shift operations.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Why Approximate the DFT at All?&lt;/h2&gt;
&lt;p&gt;For an $N$-point DFT,&lt;/p&gt;
&lt;p&gt;$$
X[k] = \sum_{n=0}^{N-1} x[n]e^{-j2\pi kn/N}
$$&lt;/p&gt;
&lt;p&gt;the straightforward implementation contains many complex multiplications.&lt;/p&gt;
&lt;p&gt;For only eight points, this is not a huge problem on a modern FPGA. But the exercise becomes interesting when the design goal changes from:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Can the FPGA calculate this?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;Can the transform be expressed using cheaper hardware primitives while preserving the spectral information that matters?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The ADFT architecture I implemented uses coefficient approximations that reduce the transform to combinations of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;addition
subtraction
right shifts
routing
registers
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{2}x = x \gg 1
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{4}x = x \gg 2
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{3}{4}x = x - (x \gg 2)
$$&lt;/p&gt;
&lt;p&gt;The main ADFT core therefore requires no general-purpose multipliers.&lt;/p&gt;
&lt;p&gt;That distinction is important. A constant coefficient that happens to be easy to express in binary is not really a &quot;multiplication problem&quot; anymore. It is an &lt;strong&gt;architecture problem&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Start with a Software Reference You Trust&lt;/h2&gt;
&lt;p&gt;Before building the HDL design, I generated several complex-valued test cases in MATLAB and compared the ADFT response with an FFT reference.&lt;/p&gt;
&lt;p&gt;The cases covered three useful spectral situations:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Case A - DC plus bin-aligned tones
Case B - off-bin tones with spectral leakage
Case C - multiple bin-aligned tones with unequal amplitudes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The per-bin energy metric was:&lt;/p&gt;
&lt;p&gt;$$
P_k = a_k^2 + b_k^2
$$&lt;/p&gt;
&lt;p&gt;where $a_k$ and $b_k$ are the real and imaginary components of each frequency bin.&lt;/p&gt;
&lt;p&gt;The point of the MATLAB stage was not to prove that an FFT works.&lt;/p&gt;
&lt;p&gt;It gave me a &lt;strong&gt;golden reference&lt;/strong&gt; that stayed unchanged while the hardware implementation evolved.&lt;/p&gt;
&lt;p&gt;Later, when something went wrong in Simulink or on the FPGA, I could ask:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is this an approximation effect, a fixed-point effect, a pipeline-alignment error, or a hardware integration error?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Without a software reference, those failure modes are difficult to separate.&lt;/p&gt;
&lt;h2&gt;Lesson 1 - A Hardware-Friendly Algorithm Still Needs a Hardware Architecture&lt;/h2&gt;
&lt;p&gt;The mathematical transform does not specify:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;where registers go;&lt;/li&gt;
&lt;li&gt;how wide intermediate values should be;&lt;/li&gt;
&lt;li&gt;when all outputs become valid;&lt;/li&gt;
&lt;li&gt;which paths must be delayed;&lt;/li&gt;
&lt;li&gt;how input samples are framed.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those decisions belong to the architecture.&lt;/p&gt;
&lt;p&gt;The HDL Coder interface used sixteen fixed-point inputs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8 real samples
8 imaginary samples
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and produced sixteen outputs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8 real frequency components
8 imaginary frequency components
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./hdl-interface.webp&quot; alt=&quot;HDL Coder interface for the ADFT core&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The explicit 16-input / 16-output mapping made it easier to compare the mathematical transform, Simulink model, and generated HDL.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I found this explicit mapping useful because it prevented the interface from hiding too much.&lt;/p&gt;
&lt;p&gt;Each signal corresponded to a specific part of the complex vector, so debugging a wrong output bin remained manageable.&lt;/p&gt;
&lt;p&gt;A simplified design flow was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    A[&quot;MATLAB Reference&quot;] --&amp;gt; B[&quot;Fixed-Point Inputs&quot;]
    B --&amp;gt; C[&quot;ADFT Butterfly Network&quot;]
    C --&amp;gt; D[&quot;Real and Imaginary Outputs&quot;]
    D --&amp;gt; E[&quot;Power per Bin&quot;]
    E --&amp;gt; F[&quot;Spectrum Classification&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I deliberately keep the Mermaid syntax simple here because it is also much more portable across renderers.&lt;/p&gt;
&lt;h2&gt;Lesson 2 - Pipelining Is Not Just Adding Registers&lt;/h2&gt;
&lt;p&gt;My first hardware version was functionally correct but had long combinational paths.&lt;/p&gt;
&lt;p&gt;Several add/subtract operations could be traversed within one clock period. That created a critical path dominated by chained arithmetic.&lt;/p&gt;
&lt;p&gt;The unoptimised critical-path estimate was about:&lt;/p&gt;
&lt;p&gt;$$
T_{\text{critical}} \approx 6.93\ \text{ns}
$$&lt;/p&gt;
&lt;p&gt;After pipelining and delay balancing, the post-implementation path was reduced to approximately:&lt;/p&gt;
&lt;p&gt;$$
T_{\text{critical}} \approx 3.13\ \text{ns}
$$&lt;/p&gt;
&lt;p&gt;The corresponding routed maximum clock frequency increased from roughly:&lt;/p&gt;
&lt;p&gt;$$
146.18\ \text{MHz}
$$&lt;/p&gt;
&lt;p&gt;to:&lt;/p&gt;
&lt;p&gt;$$
316.86\ \text{MHz}
$$&lt;/p&gt;
&lt;p&gt;The result was more than a 2x increase in achievable frequency without changing the transform arithmetic itself.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pipeline-comparison.webp&quot; alt=&quot;Unoptimised and pipelined ADFT architectures&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The arithmetic is similar in both designs; the important difference is how the combinational work is segmented by registers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The lesson was that pipelining has at least three separate jobs.&lt;/p&gt;
&lt;h3&gt;Breaking the critical path&lt;/h3&gt;
&lt;p&gt;A register prevents the next combinational stage from belonging to the same timing path.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adder -&amp;gt; adder -&amp;gt; shift -&amp;gt; adder -&amp;gt; register
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the architecture becomes closer to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adder -&amp;gt; register
shift -&amp;gt; register
adder -&amp;gt; register
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Balancing data arrival&lt;/h3&gt;
&lt;p&gt;This part is easier to underestimate.&lt;/p&gt;
&lt;p&gt;If one operand reaches an adder after two pipeline stages and another reaches it after three, the arithmetic is wrong even though every individual block is correct.&lt;/p&gt;
&lt;p&gt;So shorter paths need explicit delays.&lt;/p&gt;
&lt;h3&gt;Defining latency as part of the interface&lt;/h3&gt;
&lt;p&gt;Once the datapath is pipelined, output timing becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input frame
    ↓
pipeline latency
    ↓
valid output frame
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Latency is not an implementation detail anymore. Downstream logic has to understand it.&lt;/p&gt;
&lt;p&gt;:::important[Pipelining changes time, not the algorithm]
The numerical transformation can remain identical while the temporal behaviour of the module changes completely.
:::&lt;/p&gt;
&lt;h2&gt;Lesson 3 - Registers Are Cheap Only If You Remember What They Cost&lt;/h2&gt;
&lt;p&gt;The pipelined version retained the same basic arithmetic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;56 adders/subtractors
8 static shift operators
0 multipliers in the ADFT core
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but the register count increased substantially because of pipeline staging and delay balancing.&lt;/p&gt;
&lt;p&gt;That is exactly the expected trade-off:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Design choice&lt;/th&gt;
&lt;th&gt;Main effect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fewer registers&lt;/td&gt;
&lt;td&gt;Lower latency and fewer flip-flops, but longer critical paths&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More pipeline registers&lt;/td&gt;
&lt;td&gt;Higher clock rate and throughput, but more latency and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More parallel arithmetic&lt;/td&gt;
&lt;td&gt;Higher throughput, but more area&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Narrower fixed point&lt;/td&gt;
&lt;td&gt;Lower storage/routing cost, but more quantisation risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I used to think of pipelining mainly as a performance technique.&lt;/p&gt;
&lt;p&gt;This project made me think of it as a &lt;strong&gt;resource exchange&lt;/strong&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;spend registers and latency to buy timing margin.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That framing is much more useful when comparing architectures.&lt;/p&gt;
&lt;h2&gt;Lesson 4 - Fixed-Point Design Is Part of the Algorithm&lt;/h2&gt;
&lt;p&gt;The input samples were represented with a 16-bit fixed-point format using 12 fractional bits.&lt;/p&gt;
&lt;p&gt;The difficult part was not writing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Q1.12
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;in a block parameter.&lt;/p&gt;
&lt;p&gt;The difficult part was making sure intermediate arithmetic did not quietly destroy the spectrum.&lt;/p&gt;
&lt;p&gt;I considered several issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;growth after addition;&lt;/li&gt;
&lt;li&gt;temporary guard bits;&lt;/li&gt;
&lt;li&gt;truncation points;&lt;/li&gt;
&lt;li&gt;rounding mode;&lt;/li&gt;
&lt;li&gt;overflow behaviour;&lt;/li&gt;
&lt;li&gt;final output compatibility with MATLAB.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A useful model is:&lt;/p&gt;
&lt;p&gt;$$
x_{\text{fixed}} = \operatorname{round}(x \cdot 2^F)
$$&lt;/p&gt;
&lt;p&gt;where $F$ is the number of fractional bits.&lt;/p&gt;
&lt;p&gt;Increasing $F$ improves precision, but only until the total word length becomes the limiting factor.&lt;/p&gt;
&lt;p&gt;In my numerical sweep, the spectral mean-squared error decreased rapidly as fractional precision increased and reached approximately:&lt;/p&gt;
&lt;p&gt;$$
1.26 \times 10^{-11}
$$&lt;/p&gt;
&lt;p&gt;at 12 fractional bits.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fixed-point-mse.webp&quot; alt=&quot;Spectral MSE versus fixed-point fractional precision&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The precision sweep helped turn a word-length choice into a measurable engineering decision rather than a guess.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is one of the fixed-point habits I now value most:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Do not choose a format only because it &quot;looks precise enough.&quot; Sweep it against a reference and measure the error.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lesson 5 - FPGA-in-the-Loop Is More Useful Than I Expected&lt;/h2&gt;
&lt;p&gt;Simulation can confirm the Simulink model.&lt;/p&gt;
&lt;p&gt;Synthesis can confirm that the design maps to hardware.&lt;/p&gt;
&lt;p&gt;But neither alone proves that the generated FPGA implementation produces the exact sample stream expected by the software model.&lt;/p&gt;
&lt;p&gt;That is where FPGA-in-the-loop verification became useful.&lt;/p&gt;
&lt;p&gt;MATLAB supplied the fixed-point inputs, the Nexys 4 executed the ADFT hardware, and the returned outputs were compared with the delayed software reference.&lt;/p&gt;
&lt;p&gt;The error was evaluated as:&lt;/p&gt;
&lt;p&gt;$$
e[n] = y_{\text{FIL}}[n] - y_{\text{ref}}[n]
$$&lt;/p&gt;
&lt;p&gt;After compensating for the pipeline latency, the error remained zero across the verification window.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./fil-verification.webp&quot; alt=&quot;FPGA-in-the-loop verification&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Both baseline and optimised designs were compared against the same software reference after compensating for hardware latency.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The key phrase there is &lt;strong&gt;after compensating for latency&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Without aligning the streams, even a perfectly correct pipelined design appears wrong.&lt;/p&gt;
&lt;p&gt;That reinforced a recurring FPGA lesson for me:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Correct values at the wrong clock cycle are still incorrect hardware behaviour.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lesson 6 - Timing Reports Tell You More Than &quot;Pass&quot; or &quot;Fail&quot;&lt;/h2&gt;
&lt;p&gt;It is tempting to treat timing analysis as binary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;timing passed
timing failed
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the more useful question is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What physical structure created the critical path?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In the unoptimised version, the dominant path passed through several arithmetic operations before reaching a register.&lt;/p&gt;
&lt;p&gt;That told me exactly what to optimise.&lt;/p&gt;
&lt;p&gt;After pipelining, the longest path was reduced to roughly one arithmetic stage between registers.&lt;/p&gt;
&lt;p&gt;The main timing comparison was:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Unoptimised&lt;/th&gt;
&lt;th&gt;Optimised&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HDL critical-path estimate&lt;/td&gt;
&lt;td&gt;~6.93 ns&lt;/td&gt;
&lt;td&gt;~3.30 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-implementation delay&lt;/td&gt;
&lt;td&gt;~6.87 ns&lt;/td&gt;
&lt;td&gt;~3.13 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Post-implementation Fmax&lt;/td&gt;
&lt;td&gt;~146.18 MHz&lt;/td&gt;
&lt;td&gt;~316.86 MHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADFT multipliers&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ADFT arithmetic structure&lt;/td&gt;
&lt;td&gt;add/shift&lt;/td&gt;
&lt;td&gt;add/shift&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The important observation is that the frequency gain did not come from replacing the algorithm.&lt;/p&gt;
&lt;p&gt;It came from &lt;strong&gt;changing the spatial and temporal structure of the same arithmetic&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Lesson 7 - Verification Should Follow the Same Test Cases Across Every Layer&lt;/h2&gt;
&lt;p&gt;I reused the same representative spectra across:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MATLAB
Simulink functional simulation
FPGA-in-the-loop
standalone FPGA testing
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That consistency was extremely valuable.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./spectra-verification.webp&quot; alt=&quot;Hardware/software spectral verification cases&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Representative spectra were kept consistent across software and hardware verification.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Changing test signals at every stage makes each verification step individually interesting but weakens the end-to-end argument.&lt;/p&gt;
&lt;p&gt;Using the same cases lets you trace one expected behaviour through the entire toolchain:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;known input spectrum
      ↓
software ADFT
      ↓
fixed-point Simulink
      ↓
generated HDL
      ↓
FPGA-in-the-loop
      ↓
physical board
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a verification strategy I would reuse in future hardware projects.&lt;/p&gt;
&lt;h2&gt;Lesson 8 - A Standalone FPGA System Needs Much More Than the Compute Core&lt;/h2&gt;
&lt;p&gt;Once the ADFT itself worked, the final task was turning it into a standalone spectrum-sensing pipeline.&lt;/p&gt;
&lt;p&gt;The complete signal path was:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    ROM[&quot;Real and Imag ROM&quot;] --&amp;gt; SIPO[&quot;8-Sample SIPO&quot;]
    SIPO --&amp;gt; ADFT[&quot;Pipelined ADFT Core&quot;]
    ADFT --&amp;gt; PWR[&quot;Power Calculation&quot;]
    PWR --&amp;gt; CLASS[&quot;Low Mid High Classification&quot;]
    CLASS --&amp;gt; SSD[&quot;8-Digit SSD Driver&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ROM stored pre-generated complex samples.&lt;/p&gt;
&lt;p&gt;The SIPO registers converted sequential samples into the 8-sample parallel frame expected by the transform.&lt;/p&gt;
&lt;p&gt;The power block calculated:&lt;/p&gt;
&lt;p&gt;$$
P_k = a_k^2 + b_k^2
$$&lt;/p&gt;
&lt;p&gt;and then classified each bin relative to the maximum energy.&lt;/p&gt;
&lt;p&gt;The thresholds used were:&lt;/p&gt;
&lt;p&gt;$$
T_{\text{mid}} = 0.25P_{\max}
$$&lt;/p&gt;
&lt;p&gt;$$
T_{\text{high}} = 0.60P_{\max}
$$&lt;/p&gt;
&lt;p&gt;Finally, the eight seven-segment digits represented the eight frequency bins.&lt;/p&gt;
&lt;p&gt;Each digit used a simple visual encoding:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;top segment    -&amp;gt; High
middle segment -&amp;gt; Mid
bottom segment -&amp;gt; Low
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I like this architecture because every block has a narrow responsibility.&lt;/p&gt;
&lt;p&gt;The ADFT does not know anything about seven-segment displays.&lt;/p&gt;
&lt;p&gt;The display driver does not know anything about Fourier transforms.&lt;/p&gt;
&lt;p&gt;That separation made the standalone design much easier to debug.&lt;/p&gt;
&lt;h2&gt;Lesson 9 - The Peripheral Logic Can Be Small but Still Define the Product&lt;/h2&gt;
&lt;p&gt;The complete system used only a small fraction of the Artix-7 fabric.&lt;/p&gt;
&lt;p&gt;The final implementation occupied roughly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1896 LUTs       ~2.99%
1991 flip-flops ~1.57%
1 BRAM tile
16 DSP slices in the power-computation stage
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ADFT core itself remained multiplierless; the DSP slices in the complete design were associated with the later power calculation.&lt;/p&gt;
&lt;p&gt;This distinction matters.&lt;/p&gt;
&lt;p&gt;When describing an FPGA design, saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The system uses DSP slices&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;is not the same as saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The transform requires multipliers.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Architecture-level resource attribution is more informative than quoting only a top-level utilisation number.&lt;/p&gt;
&lt;h2&gt;On-Board Results Made the Design Feel Real&lt;/h2&gt;
&lt;p&gt;The final system ran on the Nexys 4 without MATLAB continuously controlling it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./onboard-results.webp&quot; alt=&quot;Standalone Nexys 4 spectrum-sensing results&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The eight seven-segment displays provide a coarse but immediate view of the relative energy distribution across the eight frequency bins.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The three test cases produced the expected Low/Mid/High patterns on the board.&lt;/p&gt;
&lt;p&gt;At that point the system had become:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ROM
 -&amp;gt; SIPO
 -&amp;gt; ADFT
 -&amp;gt; power
 -&amp;gt; classification
 -&amp;gt; display
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;with deterministic timing inside one FPGA clock domain.&lt;/p&gt;
&lt;p&gt;This final integration was important because a transform core that passes a unit test is not yet a useful embedded signal-processing system.&lt;/p&gt;
&lt;p&gt;The interfaces around it determine whether it can actually consume data and expose a meaningful result.&lt;/p&gt;
&lt;h2&gt;What I Would Change in a Second Version&lt;/h2&gt;
&lt;h3&gt;1. Add explicit valid/ready signalling&lt;/h3&gt;
&lt;p&gt;The laboratory version used a deterministic framing structure, but a more reusable core should expose interface-level timing explicitly.&lt;/p&gt;
&lt;p&gt;I would prefer something closer to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;input_valid
input_ready
output_valid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than relying only on known cycle counts.&lt;/p&gt;
&lt;h3&gt;2. Parameterise transform width and data format&lt;/h3&gt;
&lt;p&gt;Instead of fixing everything around one 8-point, 16-bit configuration, I would separate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;transform size
word length
fractional length
pipeline depth
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;into configurable parameters where practical.&lt;/p&gt;
&lt;h3&gt;3. Compare energy and power directly&lt;/h3&gt;
&lt;p&gt;The ADFT reduces arithmetic complexity, but a serious low-power study should measure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;dynamic power
static power
energy per frame
throughput per watt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than inferring efficiency from resource counts alone.&lt;/p&gt;
&lt;h3&gt;4. Stream real input data&lt;/h3&gt;
&lt;p&gt;The ROM-based test system is excellent for reproducibility.&lt;/p&gt;
&lt;p&gt;A natural next step would replace the ROM source with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ADC
RF front end
DMA/data stream
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;so that the design performs spectrum sensing on live samples.&lt;/p&gt;
&lt;h3&gt;5. Make the verification harness automatic&lt;/h3&gt;
&lt;p&gt;I would build one script that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;generates the MATLAB test vector;&lt;/li&gt;
&lt;li&gt;exports the fixed-point ROM data;&lt;/li&gt;
&lt;li&gt;runs HDL/FIL verification;&lt;/li&gt;
&lt;li&gt;compensates latency automatically;&lt;/li&gt;
&lt;li&gt;computes error metrics;&lt;/li&gt;
&lt;li&gt;produces the comparison figures.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That would make architecture iteration much faster.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This project changed how I think about FPGA optimisation.&lt;/p&gt;
&lt;p&gt;At first, the most interesting idea seemed to be the multiplierless ADFT itself.&lt;/p&gt;
&lt;p&gt;By the end, I found the architectural lessons more valuable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a hardware-friendly equation is only the beginning;&lt;/li&gt;
&lt;li&gt;pipelining is a deliberate exchange between timing, latency, and registers;&lt;/li&gt;
&lt;li&gt;delay balancing is just as important as inserting registers;&lt;/li&gt;
&lt;li&gt;fixed-point precision should be measured, not guessed;&lt;/li&gt;
&lt;li&gt;verification has to account for time as well as value;&lt;/li&gt;
&lt;li&gt;resource reports are most useful when interpreted by module;&lt;/li&gt;
&lt;li&gt;a compute core becomes useful only after it is integrated into a complete data path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The biggest lesson I kept is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;FPGA performance often improves not because the arithmetic changes, but because the same arithmetic is reorganised in space and time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That idea now influences how I look at DSP accelerators, hardware pipelines, and almost any architecture intended for real-time FPGA implementation.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Building a Wireless FreeRTOS Controller for a Remote Microscope</title><link>https://chaosnap.github.io/MyBlog/posts/freertos-wireless-remote-microscope/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/freertos-wireless-remote-microscope/</guid><description>Lessons from designing a wireless Remote Controlled Microscope controller with STM32, FreeRTOS, nRF24L01+, finite-state machines, queues, semaphores, Hamming-coded packets, and local diagnostic outputs.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a Wireless FreeRTOS Controller for a Remote Microscope&lt;/h1&gt;
&lt;p&gt;One of the projects that changed the way I think about embedded software was a &lt;strong&gt;wireless controller for a Remote Controlled Microscope (RCM)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The physical system was much larger than the microcontroller sitting on my desk. The RCM was based on an X-Carve-style CNC platform, with stepper motors controlling the microscope position over a roughly 200 mm by 200 mm work area. The controller had to select commands locally, package them into a wireless protocol, send them through an nRF24L01+ radio link, and keep several local displays and indicators synchronized with the current command state.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./rcm-machine.webp&quot; alt=&quot;Remote Controlled Microscope platform&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The RCM turns embedded commands into movement of a real three-axis physical system.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The project used a Nucleo-F429ZI, an MFS board, an RGB LED, and an nRF24L01+ transceiver. What made the project interesting was not any one peripheral. It was the need to combine &lt;strong&gt;FreeRTOS scheduling, finite-state machines, interrupt-driven inputs, wireless communication, packet encoding, and physical-system control&lt;/strong&gt; into one coherent architecture.&lt;/p&gt;
&lt;p&gt;I came away from this project with a much stronger appreciation for one idea:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In a larger embedded system, the hardest problem is usually not writing a driver. It is deciding which module is allowed to own which responsibility.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;The System Was a Chain of Responsibilities&lt;/h2&gt;
&lt;p&gt;At a high level, the controller had to turn physical input into a radio command.&lt;/p&gt;
&lt;p&gt;The path was conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Pushbuttons / switchbank / trimpot
        ↓
Command selection
        ↓
Command execution
        ↓
RCM state update
        ↓
Packet construction
        ↓
Hamming encoding
        ↓
TX queue
        ↓
nRF24L01+ radio
        ↓
Remote microscope
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the same time, the controller had local output responsibilities:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;current command
    ↓
7-segment display
LED bar opcode
RGB status
diagnostic LEDs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That meant the design was not a single loop anymore. It was a set of &lt;strong&gt;cooperating state machines and FreeRTOS tasks&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./system-flow.webp&quot; alt=&quot;Overall FreeRTOS system flow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;My top-level flow separated startup, button events, command processing, packet construction, and radio transmission.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Lesson 1 - FreeRTOS Is Most Useful When It Defines Ownership&lt;/h2&gt;
&lt;p&gt;It is easy to think that the reason to use an RTOS is simply to &quot;run several things at once.&quot;&lt;/p&gt;
&lt;p&gt;That is only part of the benefit.&lt;/p&gt;
&lt;p&gt;What I found more useful was that FreeRTOS encouraged me to decide which task owned each resource.&lt;/p&gt;
&lt;p&gt;For example, the radio transmitter should be responsible for the radio.&lt;/p&gt;
&lt;p&gt;The command-input module should not directly manipulate radio registers just because it knows that a packet needs to be sent.&lt;/p&gt;
&lt;p&gt;Instead, the cleaner relationship is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command task
    ↓
RCM system logic
    ↓
TX queue
    ↓
radio task
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a boundary.&lt;/p&gt;
&lt;p&gt;The command task knows &lt;strong&gt;what&lt;/strong&gt; should happen.&lt;/p&gt;
&lt;p&gt;The radio task knows &lt;strong&gt;how&lt;/strong&gt; to transmit it.&lt;/p&gt;
&lt;p&gt;That difference became increasingly important as the project grew.&lt;/p&gt;
&lt;p&gt;If every task can directly call every peripheral, then adding an RTOS can actually make the architecture harder to understand. Concurrency without ownership just creates more possible interactions.&lt;/p&gt;
&lt;h2&gt;Lesson 2 - Queues Are More Than Buffers&lt;/h2&gt;
&lt;p&gt;The TX queue was one of the most important architectural boundaries in the project.&lt;/p&gt;
&lt;p&gt;A packet producer could construct a command without caring whether the radio was currently idle, configuring its channel, waiting for the nRF24L01+ state machine, or loading a transmit FIFO.&lt;/p&gt;
&lt;p&gt;The producer only needed to place a valid packet into the queue.&lt;/p&gt;
&lt;p&gt;The radio side could then process packets at its own pace.&lt;/p&gt;
&lt;p&gt;That turns the queue into a contract:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Producer guarantee:
&quot;I will give you complete packets.&quot;

Consumer guarantee:
&quot;I will transmit them in the correct radio context.&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is much easier to reason about than a design where command code immediately starts an SPI transaction.&lt;/p&gt;
&lt;p&gt;It also makes failures easier to isolate.&lt;/p&gt;
&lt;p&gt;If no packet appears on air, I can ask:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Was the packet created?
Did it reach the queue?
Did the radio task dequeue it?
Did the nRF24L01+ enter TX mode?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each question belongs to a different layer.&lt;/p&gt;
&lt;h2&gt;Lesson 3 - The Radio Has Its Own State Machine&lt;/h2&gt;
&lt;p&gt;The nRF24L01+ is controlled through SPI and exposes control registers, TX/RX FIFOs, a CE control pin, and several radio states.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./nrf24-module.webp&quot; alt=&quot;nRF24L01+ radio module&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The nRF24L01+ provides the wireless link while the MCU controls configuration and data transfer over SPI.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The module operates at 2.4 GHz and supports data rates up to 2 Mbps. SPI is used both for configuration and for moving payload data into or out of the radio FIFOs.&lt;/p&gt;
&lt;p&gt;This is important because &quot;send packet&quot; is not a primitive physical action.&lt;/p&gt;
&lt;p&gt;The software has to coordinate:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;radio configuration
channel
address
FIFO state
CE control
TX state
return to idle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My radio-control FSM reflected that separation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./radio-command-fsm.webp&quot; alt=&quot;Radio command finite-state machine&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The radio path verifies configuration, handles JOIN ownership, and then processes command packets through dedicated states.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This project reinforced a useful rule for peripheral drivers:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If the hardware itself is stateful, pretending that it is a stateless function call usually makes the software worse.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A finite-state machine makes the legal transitions explicit.&lt;/p&gt;
&lt;h2&gt;Lesson 4 - JOIN Was Really an Ownership Protocol&lt;/h2&gt;
&lt;p&gt;Before normal control packets could be accepted, the controller had to send a &lt;code&gt;JOIN&lt;/code&gt; packet.&lt;/p&gt;
&lt;p&gt;At first, this looked like just another command.&lt;/p&gt;
&lt;p&gt;It is more useful to think of it as a distributed ownership mechanism.&lt;/p&gt;
&lt;p&gt;The rule is essentially:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;JOIN
  ↓
RCM associates control with this sender
  ↓
XYZ / ZOOM / BRIGHT commands become valid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Without a valid JOIN, the RCM rejects subsequent control packets.&lt;/p&gt;
&lt;p&gt;This solves an important physical-system problem: multiple users should not independently move the same microscope at the same time.&lt;/p&gt;
&lt;p&gt;That made the protocol feel much more real to me.&lt;/p&gt;
&lt;p&gt;The packet is not only carrying data. It is establishing &lt;strong&gt;permission to affect a physical machine&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;In a networked embedded system, protocol design and safety boundaries can become the same problem.&lt;/p&gt;
&lt;h2&gt;Lesson 5 - Packet Structure Should Be Stable Before the Tasks Become Complicated&lt;/h2&gt;
&lt;p&gt;The project used a fixed radio packet structure with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;packet type
sender address
payload string
padding
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A raw packet was 16 bytes.&lt;/p&gt;
&lt;p&gt;Each half-byte was then Hamming encoded, producing a 32-byte radio packet.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;16-byte raw packet
       ↓
split each byte into two nibbles
       ↓
Hamming encode each nibble
       ↓
32-byte encoded packet
       ↓
TX queue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The major packet types included:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;JOIN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Acquire control of the RCM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;XYZ&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set absolute X, Y and Z coordinates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ZOOM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Set/change microscope zoom&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;BRIGHT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Adjust image brightness&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For an XYZ packet, the coordinates are expressed as decimal ASCII fields. That makes the payload relatively easy to inspect during debugging.&lt;/p&gt;
&lt;p&gt;I liked the separation between &lt;strong&gt;semantic payload&lt;/strong&gt; and &lt;strong&gt;radio representation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The command layer can think in terms of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X = 100 mm
Y = 0 mm
Z = 0 mm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;while the packet layer is responsible for turning that meaning into the exact byte layout required on air.&lt;/p&gt;
&lt;p&gt;That boundary is worth protecting.&lt;/p&gt;
&lt;h2&gt;Lesson 6 - Error-Correcting Codes Change the Way You Think About Interfaces&lt;/h2&gt;
&lt;p&gt;Hamming encoding was another part that initially looked like a small implementation detail.&lt;/p&gt;
&lt;p&gt;It became more interesting when I thought about where it belongs.&lt;/p&gt;
&lt;p&gt;The command logic should not care that a nibble is being protected by an error-correcting code.&lt;/p&gt;
&lt;p&gt;Similarly, the radio driver should not need to understand that the payload represents an X coordinate.&lt;/p&gt;
&lt;p&gt;A cleaner stack is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RCM command
    ↓
raw protocol packet
    ↓
Hamming encoding
    ↓
radio transport
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each layer transforms the representation while preserving the meaning.&lt;/p&gt;
&lt;p&gt;This same idea appears in much larger communication systems.&lt;/p&gt;
&lt;p&gt;Once I started viewing it that way, embedded networking became less about &quot;sending bytes&quot; and more about &lt;strong&gt;building a protocol stack, even if the stack is small&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Lesson 7 - Interrupts Should Wake Work, Not Become the Work&lt;/h2&gt;
&lt;p&gt;Pushbuttons are asynchronous.&lt;/p&gt;
&lt;p&gt;The tempting approach is to detect a button interrupt and immediately perform the entire action inside the interrupt handler.&lt;/p&gt;
&lt;p&gt;That becomes dangerous when the action eventually includes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;state-machine updates
display changes
packet creation
Hamming encoding
queue operations
radio transmission
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The better design is to let the interrupt communicate an event to a task.&lt;/p&gt;
&lt;p&gt;My overall flow used FreeRTOS synchronization to move from a button event into task-level processing.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./button-fsm.webp&quot; alt=&quot;Button and task interaction&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Button events are translated into synchronization signals that drive the higher-level command state machine.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is one of the most important habits I took from the project.&lt;/p&gt;
&lt;p&gt;An ISR should ideally answer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;What happened?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and then wake the code that decides:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;What should the system do about it?&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Those are different responsibilities.&lt;/p&gt;
&lt;p&gt;Keeping that boundary clear improves latency, predictability, and maintainability.&lt;/p&gt;
&lt;h2&gt;Lesson 8 - A Command FSM Makes a Small User Interface Predictable&lt;/h2&gt;
&lt;p&gt;The local user interface had only a few physical inputs:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a command-selection pushbutton;&lt;/li&gt;
&lt;li&gt;an execute pushbutton;&lt;/li&gt;
&lt;li&gt;an 8-bit switchbank value;&lt;/li&gt;
&lt;li&gt;a trimpot for relative settings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But those inputs could control several behaviours:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X
Y
Z
ZOOM
BRIGHT
ORG
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A state machine was a natural way to map repeated CMD presses into a selected operation.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./command-task-flow.webp&quot; alt=&quot;Command task flow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The command task separates selecting an operation from executing it and constructing the corresponding packet.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is much cleaner than writing a long list of unrelated button checks.&lt;/p&gt;
&lt;p&gt;The state machine answers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;What command is currently selected?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The execute event answers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;When should the selected command take effect?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The switchbank or trimpot answers:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;What value should that command use?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Separating those three ideas reduced ambiguity.&lt;/p&gt;
&lt;h2&gt;Lesson 9 - Input Conversion Deserves Its Own Task Boundary&lt;/h2&gt;
&lt;p&gt;The switchbank provided an 8-bit input value.&lt;/p&gt;
&lt;p&gt;One of my task flows periodically read the GPIO state, converted the raw binary input to a decimal representation, formatted it for the rest of the system, and then returned to a periodic wait.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./switchbank-task.webp&quot; alt=&quot;Switchbank polling task&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The switchbank task turns raw GPIO state into a reusable numeric value rather than exposing pin-level details to command logic.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This may look like a small wrapper around GPIO.&lt;/p&gt;
&lt;p&gt;But it represents an important software boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hardware representation:
8 GPIO bits

application representation:
VALUE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The command FSM should care about &lt;code&gt;VALUE&lt;/code&gt;, not individual pins.&lt;/p&gt;
&lt;p&gt;That same principle scales to sensors, ADCs, network packets, and almost every embedded interface.&lt;/p&gt;
&lt;h2&gt;Lesson 10 - Local Indicators Are Extremely Valuable in Distributed Debugging&lt;/h2&gt;
&lt;p&gt;A wireless physical system can fail in many places.&lt;/p&gt;
&lt;p&gt;A command might be selected correctly but never executed.&lt;/p&gt;
&lt;p&gt;A packet might be created correctly but never transmitted.&lt;/p&gt;
&lt;p&gt;The radio might transmit but the RCM might reject the sender.&lt;/p&gt;
&lt;p&gt;Without local observability, all of these failures can look like:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&quot;The microscope did not move.&quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The project therefore used several local indicators:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MFS D1      -&amp;gt; packet transmission activity
MFS D2      -&amp;gt; pushbutton activity
LED bar     -&amp;gt; selected command opcode
7-segment   -&amp;gt; current command value
RGB LED     -&amp;gt; zoom/brightness direction or default state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;My output FSM coordinated those displays.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./output-fsm.webp&quot; alt=&quot;Output finite-state machine&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The local-output logic maps command state to the seven-segment display, LED bar, and RGB indicator.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This made me appreciate diagnostic outputs as part of architecture rather than decoration.&lt;/p&gt;
&lt;p&gt;A spare LED can sometimes save more debugging time than another hundred lines of logging code.&lt;/p&gt;
&lt;h2&gt;Lesson 11 - Absolute Motion Commands Simplify the Distributed State&lt;/h2&gt;
&lt;p&gt;The RCM uses an absolute coordinate frame for X, Y, and Z.&lt;/p&gt;
&lt;p&gt;The work area is approximately:&lt;/p&gt;
&lt;p&gt;$$
200\text{ mm} \times 200\text{ mm}
$$&lt;/p&gt;
&lt;p&gt;with an origin at the lower-left of the operating area.&lt;/p&gt;
&lt;p&gt;A command such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;XYZ10000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;describes a target state rather than a movement history.&lt;/p&gt;
&lt;p&gt;That has a useful property in a remote-control system.&lt;/p&gt;
&lt;p&gt;With relative commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;move +10
move +10
move -5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the sender and receiver must remain synchronized about the current position.&lt;/p&gt;
&lt;p&gt;With absolute commands:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go to X = 100
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;the desired final state is explicit.&lt;/p&gt;
&lt;p&gt;That does not eliminate every synchronization problem, but it reduces how much history has to be shared between the controller and the machine.&lt;/p&gt;
&lt;h2&gt;The Architecture Became Easier to Understand as Several FSMs&lt;/h2&gt;
&lt;p&gt;By the end of the project, I was not thinking in terms of one giant state machine.&lt;/p&gt;
&lt;p&gt;I had several smaller behavioural models:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Command FSM
    -&amp;gt; what the user is selecting

Radio FSM
    -&amp;gt; how packets reach the air

Output FSM
    -&amp;gt; what local feedback should be shown

RCM processing flow
    -&amp;gt; how received command types affect the machine

FreeRTOS synchronization
    -&amp;gt; how events move between those modules
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The receive-side flow also made the protocol layering visible.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./rcm-receive-flow.webp&quot; alt=&quot;RCM receive and command-processing flow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;JOIN, XYZ, ZOOM, and BRIGHT commands follow different paths but eventually converge on shared output and waiting states.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A single giant FSM would have created too many transitions.&lt;/p&gt;
&lt;p&gt;Multiple smaller FSMs allowed each module to describe one domain.&lt;/p&gt;
&lt;p&gt;This is a design pattern I still prefer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Use state machines to describe local behaviour, and use queues/events to connect the machines.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;What I Would Improve in a Second Version&lt;/h2&gt;
&lt;p&gt;If I rebuilt the controller now, I would keep the overall task separation but make several parts more explicit.&lt;/p&gt;
&lt;h3&gt;1. Add an application-level acknowledgement&lt;/h3&gt;
&lt;p&gt;The radio can indicate transport-level success, but for a physical system I would also like to know that the remote application accepted and applied the command.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;command
    ↓
radio transmission
    ↓
remote validation
    ↓
ACK with command sequence number
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That would make it easier to distinguish radio delivery from physical command acceptance.&lt;/p&gt;
&lt;h3&gt;2. Add sequence numbers&lt;/h3&gt;
&lt;p&gt;A sequence number would help detect:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;duplicates
stale packets
out-of-order application messages
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;especially if the protocol were expanded.&lt;/p&gt;
&lt;h3&gt;3. Centralise system state&lt;/h3&gt;
&lt;p&gt;I would keep one explicit RCM state model containing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;X
Y
Z
zoom
brightness
ownership status
last command
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and let display and packet-generation tasks consume snapshots or messages derived from it.&lt;/p&gt;
&lt;h3&gt;4. Make task interfaces more strongly typed&lt;/h3&gt;
&lt;p&gt;Instead of moving generic byte arrays between every layer, I would use small message structures internally and only serialize them at the packet boundary.&lt;/p&gt;
&lt;p&gt;That would make invalid states harder to represent.&lt;/p&gt;
&lt;h3&gt;5. Instrument queue and task timing&lt;/h3&gt;
&lt;p&gt;For a FreeRTOS system, I would add lightweight runtime measurements for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;queue depth
task execution time
radio service latency
command-to-transmit latency
stack high-water marks
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This would make performance tuning much more quantitative.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This project was a useful transition from peripheral-level embedded programming to &lt;strong&gt;system-level embedded architecture&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The nRF24L01+ driver mattered.&lt;/p&gt;
&lt;p&gt;The FreeRTOS APIs mattered.&lt;/p&gt;
&lt;p&gt;The Hamming encoder mattered.&lt;/p&gt;
&lt;p&gt;But the deeper lessons were about how those pieces should be connected.&lt;/p&gt;
&lt;p&gt;I learned to think more carefully about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;task ownership;&lt;/li&gt;
&lt;li&gt;queues as module boundaries;&lt;/li&gt;
&lt;li&gt;interrupts as event sources;&lt;/li&gt;
&lt;li&gt;protocol layers;&lt;/li&gt;
&lt;li&gt;finite-state machines;&lt;/li&gt;
&lt;li&gt;synchronization primitives;&lt;/li&gt;
&lt;li&gt;diagnostic observability;&lt;/li&gt;
&lt;li&gt;the difference between raw hardware values and application-level state.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The physical microscope made those decisions feel much more consequential.&lt;/p&gt;
&lt;p&gt;If a small LED demo has a race condition, an LED flashes incorrectly.&lt;/p&gt;
&lt;p&gt;If a distributed controller has a confused state model, a physical machine may move somewhere the operator did not intend.&lt;/p&gt;
&lt;p&gt;The biggest lesson I kept from the RCM project is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A reliable embedded system is built by controlling not only the hardware, but also the flow of responsibility between software components.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That idea has become one of the main ways I evaluate embedded architectures today.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Building Sokoban on an AVR: From Game Logic to Real-Time Peripheral Integration</title><link>https://chaosnap.github.io/MyBlog/posts/avr-sokoban-embedded-game/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/avr-sokoban-embedded-game/</guid><description>Lessons from implementing Sokoban on an ATmega324A with a 16x8 LED matrix, push buttons, UART terminal control, joystick input, a seven-segment display, and piezo-buzzer feedback.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building Sokoban on an AVR: From Game Logic to Real-Time Peripheral Integration&lt;/h1&gt;
&lt;p&gt;Sokoban is not a complicated game to explain.&lt;/p&gt;
&lt;p&gt;A player walks around a warehouse, pushes boxes, and tries to place every box on a target. There are no enemies, no physics engine, and no complicated graphics. That simplicity makes it a surprisingly useful embedded-systems project, because almost all of the difficulty comes from &lt;strong&gt;state, timing, I/O, and hardware interaction&lt;/strong&gt; rather than visual complexity.&lt;/p&gt;
&lt;p&gt;I built my version on an &lt;strong&gt;ATmega324A&lt;/strong&gt; in C. The game state was shown on a 16x8 LED matrix, while the same system also interacted with physical push buttons, a serial terminal, a joystick, a two-digit seven-segment display, and a piezo buzzer.&lt;/p&gt;
&lt;p&gt;What started as a small puzzle game became a compact exercise in embedded architecture.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./game-board.webp&quot; alt=&quot;Sokoban game-board representation&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The warehouse can be reduced to a small number of object types: player, walls, boxes, targets, and empty space.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The most useful lesson was that a game on a microcontroller is not really about drawing pixels. It is about keeping &lt;strong&gt;one consistent state&lt;/strong&gt; while many peripherals observe or modify it.&lt;/p&gt;
&lt;h2&gt;The Hardware Was Simple; the State Was Not&lt;/h2&gt;
&lt;p&gt;The project used a small warehouse map rendered on the LED matrix.&lt;/p&gt;
&lt;p&gt;At a logical level, the world only needs a few concepts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Wall
Target
Box
Player
Empty
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But some cells contain overlapping meaning.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;player + target
box + target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If I represented the screen only as colours, the implementation quickly became awkward. A green pixel could mean a player, a box on a target, or part of an animation.&lt;/p&gt;
&lt;p&gt;The better mental model is to separate &lt;strong&gt;game state&lt;/strong&gt; from &lt;strong&gt;render state&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    Input[&quot;Buttons / UART / Joystick&quot;] --&amp;gt; Logic[&quot;Movement and Collision Logic&quot;]
    Logic --&amp;gt; State[&quot;Game State&quot;]
    State --&amp;gt; Matrix[&quot;LED Matrix Renderer&quot;]
    State --&amp;gt; Terminal[&quot;Terminal Renderer&quot;]
    State --&amp;gt; SevenSeg[&quot;Step Counter&quot;]
    State --&amp;gt; Sound[&quot;Sound Events&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The LED matrix is only one view of the game.&lt;/p&gt;
&lt;p&gt;That distinction sounds obvious now, but it was one of the first embedded projects where I clearly saw why separating model and presentation matters.&lt;/p&gt;
&lt;h2&gt;Lesson 1 - Do Not Let Display State Become Game State&lt;/h2&gt;
&lt;p&gt;A tempting implementation is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (pixel_is_yellow(next_x, next_y)) {
    // wall
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It works until colours begin to carry multiple meanings.&lt;/p&gt;
&lt;p&gt;Targets may flash. A player can stand on a target. A box on a target uses a different colour. The terminal may use a different colour palette from the physical LED matrix.&lt;/p&gt;
&lt;p&gt;Instead, movement should operate on the logical warehouse representation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (is_wall(next_position)) {
    reject_move();
}
else if (has_box(next_position)) {
    try_push_box();
}
else {
    move_player();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rendering happens afterwards.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Game State
    ↓
render_led_matrix()
render_terminal()
update_seven_segment()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This has an important property:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A change in visual representation does not change the rules of the game.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That becomes especially valuable once animation and alternate displays are introduced.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./start-screen.webp&quot; alt=&quot;Start screen and initial board&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The same game can be represented through different output surfaces without changing the underlying state.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;Lesson 2 - One Movement Function Should Serve Every Input Device&lt;/h2&gt;
&lt;p&gt;The project supported movement from more than one interface.&lt;/p&gt;
&lt;p&gt;I used:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;physical push buttons;&lt;/li&gt;
&lt;li&gt;serial terminal input;&lt;/li&gt;
&lt;li&gt;a 2-axis joystick.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The terminal used conventional &lt;code&gt;W/A/S/D&lt;/code&gt; controls, while the buttons mapped directly to the four cardinal directions.&lt;/p&gt;
&lt;p&gt;At first, it is easy to write separate logic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;button right -&amp;gt; move right
terminal &apos;d&apos; -&amp;gt; move right
joystick east -&amp;gt; move right
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The problem appears when collision logic becomes more complicated.&lt;/p&gt;
&lt;p&gt;If each interface implements its own movement behaviour, eventually one input method handles a box differently from another.&lt;/p&gt;
&lt;p&gt;A better structure is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    B[&quot;Push Button&quot;] --&amp;gt; D[&quot;Direction Command&quot;]
    U[&quot;UART Character&quot;] --&amp;gt; D
    J[&quot;Joystick Position&quot;] --&amp;gt; D
    D --&amp;gt; M[&quot;move_player direction&quot;]
    M --&amp;gt; C{&quot;Collision&quot;}
    C --&amp;gt;|Wall| W[&quot;Reject&quot;]
    C --&amp;gt;|Box| P[&quot;Try Push&quot;]
    C --&amp;gt;|Free| V[&quot;Move&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Every frontend should translate physical input into a common command.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef enum {
    MOVE_UP,
    MOVE_DOWN,
    MOVE_LEFT,
    MOVE_RIGHT
} direction_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;handle_button_input();
handle_uart_input();
handle_joystick_input();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;can all eventually call the same game-logic function.&lt;/p&gt;
&lt;p&gt;This was one of the earliest projects where I learned a principle that I later found useful in much larger systems:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hardware interfaces should terminate at a clean software boundary.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lesson 3 - Sokoban Collision Logic Is a Small State Machine&lt;/h2&gt;
&lt;p&gt;Walking into empty space is easy.&lt;/p&gt;
&lt;p&gt;Pushing boxes is where the game becomes interesting.&lt;/p&gt;
&lt;p&gt;A requested movement can result in several different outcomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. Empty cell        -&amp;gt; player moves
2. Target            -&amp;gt; player moves onto target
3. Wall              -&amp;gt; movement rejected
4. Box + free space  -&amp;gt; box moves, then player moves
5. Box + wall        -&amp;gt; movement rejected
6. Box + box         -&amp;gt; movement rejected
7. Box + target      -&amp;gt; box becomes box-on-target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found it much easier to reason about this as a decision sequence rather than as scattered special cases.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TD
    A[&quot;Requested Move&quot;] --&amp;gt; B{&quot;Destination&quot;}
    B --&amp;gt;|Wall| X[&quot;Reject&quot;]
    B --&amp;gt;|Empty or Target| M[&quot;Move Player&quot;]
    B --&amp;gt;|Box| C{&quot;Cell Behind Box&quot;}
    C --&amp;gt;|Wall| X
    C --&amp;gt;|Box| X
    C --&amp;gt;|Empty or Target| P[&quot;Push Box&quot;]
    P --&amp;gt; M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./box-logic.webp&quot; alt=&quot;Examples of box movement and blocked moves&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A valid push, a blocked wall case, and a blocked box case can all be handled by the same movement-validation path.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;A useful detail is to avoid modifying the world until the move has been validated.&lt;/p&gt;
&lt;p&gt;In other words:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;calculate destination
calculate box destination if needed
validate entire move
commit changes
render
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;move player
discover collision
undo part of the move
fix display
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first approach produces much cleaner state transitions.&lt;/p&gt;
&lt;p&gt;It also makes step counting easier because only a &lt;strong&gt;valid committed move&lt;/strong&gt; should increment the count.&lt;/p&gt;
&lt;h2&gt;Lesson 4 - Wraparound Makes Coordinate Logic Worth Centralising&lt;/h2&gt;
&lt;p&gt;The game board allowed movement across the display boundary.&lt;/p&gt;
&lt;p&gt;A player walking off one side appears on the opposite side.&lt;/p&gt;
&lt;p&gt;For a board with width $W$ and height $H$:&lt;/p&gt;
&lt;p&gt;$$
x&apos; = (x + \Delta x + W) \bmod W
$$&lt;/p&gt;
&lt;p&gt;$$
y&apos; = (y + \Delta y + H) \bmod H
$$&lt;/p&gt;
&lt;p&gt;The extra &lt;code&gt;+W&lt;/code&gt; or &lt;code&gt;+H&lt;/code&gt; avoids a negative value before the modulo operation.&lt;/p&gt;
&lt;p&gt;This looks like a small implementation detail, but centralising it mattered because wrapping applies not only to the player.&lt;/p&gt;
&lt;p&gt;If a player pushes a box at an edge, the box&apos;s destination also has to be evaluated using the same coordinate rules.&lt;/p&gt;
&lt;p&gt;A helper such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position_t wrap_position(int x, int y);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;is safer than reproducing boundary conditions inside every collision case.&lt;/p&gt;
&lt;p&gt;Small helpers like this reduce the number of places where hardware-constrained C code can quietly go wrong.&lt;/p&gt;
&lt;h2&gt;Lesson 5 - Real-Time Does Not Mean &quot;Use Delay Everywhere&quot;&lt;/h2&gt;
&lt;p&gt;The player icon flashes.&lt;/p&gt;
&lt;p&gt;Targets can animate.&lt;/p&gt;
&lt;p&gt;The level timer updates.&lt;/p&gt;
&lt;p&gt;A buzzer may be producing a tone.&lt;/p&gt;
&lt;p&gt;The system must still accept input.&lt;/p&gt;
&lt;p&gt;This is where blocking delays become dangerous.&lt;/p&gt;
&lt;p&gt;A simple implementation such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;turn_player_on();
_delay_ms(200);
turn_player_off();
_delay_ms(200);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;makes the animation correct but the game unresponsive.&lt;/p&gt;
&lt;p&gt;The better pattern is to compare timestamps:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (now_ms - last_flash_ms &amp;gt;= FLASH_INTERVAL_MS) {
    player_visible = !player_visible;
    last_flash_ms = now_ms;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now the main loop can continue processing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;buttons
UART
joystick
game logic
display
sound
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;between animation events.&lt;/p&gt;
&lt;p&gt;The difference is fundamental:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Blocking timing:
do task -&amp;gt; wait -&amp;gt; continue

Non-blocking timing:
check time -&amp;gt; update if due -&amp;gt; continue immediately
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This project was one of my first practical examples of why embedded timing should usually be &lt;strong&gt;event-driven rather than delay-driven&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;:::important[Responsiveness is a system property]
A function can be individually correct and still make the system wrong if it blocks every other feature while it runs.
:::&lt;/p&gt;
&lt;h2&gt;Lesson 6 - Pause Is More Complicated Than Freezing the Main Loop&lt;/h2&gt;
&lt;p&gt;A pause feature looks trivial until timing state is involved.&lt;/p&gt;
&lt;p&gt;If the game pauses halfway through a 200 ms flash cycle, resuming should continue from that point rather than restarting the animation.&lt;/p&gt;
&lt;p&gt;Similarly, the level timer should exclude paused time.&lt;/p&gt;
&lt;p&gt;That means the program needs to distinguish between:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;wall-clock time
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;active gameplay time
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One simple model is:&lt;/p&gt;
&lt;h2&gt;$$
T_{\text{game}} =
T_{\text{now}}&lt;/h2&gt;
&lt;h2&gt;T_{\text{start}}&lt;/h2&gt;
&lt;p&gt;T_{\text{paused,total}}
$$&lt;/p&gt;
&lt;p&gt;When pausing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pause_start = now
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When resuming:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;total_paused += now - pause_start
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was a useful introduction to a broader real-time concept:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Time is part of program state.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Once timers, animations, and sound depend on elapsed time, &quot;pause&quot; is not merely a boolean.&lt;/p&gt;
&lt;h2&gt;Lesson 7 - Multiple Displays Should Read from the Same Source of Truth&lt;/h2&gt;
&lt;p&gt;The project had three very different output paths:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the 16x8 LED matrix;&lt;/li&gt;
&lt;li&gt;a serial terminal view;&lt;/li&gt;
&lt;li&gt;a two-digit seven-segment step counter.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The terminal was especially useful because it provided a second representation of the same game and doubled as a debugging interface.&lt;/p&gt;
&lt;p&gt;The correct architecture is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;update_game_state();

render_led_matrix();
render_terminal();
render_step_count();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the terminal renderer tries to infer state from the LED output buffer, the software layers become unnecessarily coupled.&lt;/p&gt;
&lt;p&gt;The terminal also taught me something useful about bandwidth.&lt;/p&gt;
&lt;p&gt;A serial interface is slow compared with writing memory. Redrawing the entire display every loop wastes bandwidth and can create visible latency.&lt;/p&gt;
&lt;p&gt;So the correct question is not:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Can I print the display?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How often does the display actually need to change?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That same reasoning applies to embedded dashboards, HMIs, and network telemetry.&lt;/p&gt;
&lt;h2&gt;Lesson 8 - Multiplexing a Seven-Segment Display Is a Timing Problem&lt;/h2&gt;
&lt;p&gt;The step counter used a two-digit seven-segment display.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./seven-segment.webp&quot; alt=&quot;Two-digit seven-segment step-count behaviour&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The visual output wraps at two decimal digits even though the internal step count remains larger.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Two digits are commonly multiplexed:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;show tens
disable
show ones
disable
repeat quickly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the refresh is too slow, the digits flicker.&lt;/p&gt;
&lt;p&gt;If both digits are switched incorrectly, ghosting appears.&lt;/p&gt;
&lt;p&gt;If display refreshing is performed with blocking code, gameplay responsiveness suffers.&lt;/p&gt;
&lt;p&gt;So even a tiny seven-segment display becomes another scheduled real-time task.&lt;/p&gt;
&lt;p&gt;The value displayed was only the lower two digits:&lt;/p&gt;
&lt;p&gt;$$
D = S \bmod 100
$$&lt;/p&gt;
&lt;p&gt;where $S$ is the full step count.&lt;/p&gt;
&lt;p&gt;Importantly, the internal step count should not itself wrap at 100 because the final game score may need the true number of steps.&lt;/p&gt;
&lt;p&gt;This is another example of the difference between:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;internal state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;display representation
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lesson 9 - Joystick Input Exposes Weak Movement Abstractions&lt;/h2&gt;
&lt;p&gt;Adding a joystick changed the movement problem.&lt;/p&gt;
&lt;p&gt;Cardinal movement is one step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;north
south
east
west
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But a diagonal joystick direction represents two orthogonal steps.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./joystick-control.webp&quot; alt=&quot;Diagonal joystick movement paths&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Two possible intermediate paths can produce the same diagonal result, so the intermediate collision state matters.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For example, north-east can be decomposed as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;east -&amp;gt; north
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;north -&amp;gt; east
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The intermediate square matters because a wall may block one ordering but not the other.&lt;/p&gt;
&lt;p&gt;The project rules also disallowed diagonal box pushes.&lt;/p&gt;
&lt;p&gt;This feature exposed whether movement logic was truly reusable.&lt;/p&gt;
&lt;p&gt;If the basic movement routine is clean, diagonal movement can reuse it.&lt;/p&gt;
&lt;p&gt;If movement and input handling are tangled together, the joystick feature forces a major rewrite.&lt;/p&gt;
&lt;p&gt;That was a useful architecture test.&lt;/p&gt;
&lt;p&gt;A feature that should be an extension becomes difficult when the original abstraction was wrong.&lt;/p&gt;
&lt;h2&gt;Lesson 10 - Sound Effects Must Be Asynchronous&lt;/h2&gt;
&lt;p&gt;The piezo buzzer added feedback for game events such as movement, invalid moves, box actions, startup, or game completion.&lt;/p&gt;
&lt;p&gt;The easiest implementation is also the worst:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;tone(440);
_delay_ms(100);
tone(660);
_delay_ms(100);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now input handling stops for 200 ms.&lt;/p&gt;
&lt;p&gt;A better sound engine keeps track of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;current tone
tone duration
sequence index
next update time
mute state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and advances the sequence from the normal event loop.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; Idle
    Idle --&amp;gt; Playing: sound event
    Playing --&amp;gt; Playing: next tone
    Playing --&amp;gt; Idle: sequence complete
    Playing --&amp;gt; Paused: game paused
    Paused --&amp;gt; Playing: resume
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important idea is not audio itself.&lt;/p&gt;
&lt;p&gt;It is that every new peripheral is another asynchronous activity competing for the MCU.&lt;/p&gt;
&lt;h2&gt;Level Completion Is a Good Test of State Consistency&lt;/h2&gt;
&lt;p&gt;A level is complete when every target contains a box.&lt;/p&gt;
&lt;p&gt;The final state should be internally consistent before the victory screen is shown.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./level-victory.webp&quot; alt=&quot;Solved Sokoban level&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;At level completion, every target contains a box and the remaining state can be used to calculate statistics.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The score combines step count and elapsed time:&lt;/p&gt;
&lt;h1&gt;$$
\text{Score}&lt;/h1&gt;
&lt;p&gt;\max(200-S,0)\times20
+
\max(1200-T,0)
$$&lt;/p&gt;
&lt;p&gt;where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$S$ is the full number of steps;&lt;/li&gt;
&lt;li&gt;$T$ is elapsed gameplay time in seconds.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This was another reason not to let UI representations replace the underlying state.&lt;/p&gt;
&lt;p&gt;The seven-segment display may show:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;03
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;but the real step count could be:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;103
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The gameplay model must keep the full value.&lt;/p&gt;
&lt;p&gt;The project also introduced a second level, which is a good reminder that level data should be treated as data rather than hard-coded behaviour.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./level-two.webp&quot; alt=&quot;Second Sokoban level&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A second layout uses the same engine but different initial world data.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Architecture I Would Use Today&lt;/h2&gt;
&lt;p&gt;If I rewrote the project now, I would structure it into four layers.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart TB
    HW[&quot;Hardware Drivers: SPI, UART, ADC, GPIO, Timer&quot;]
    IN[&quot;Device Services: Buttons, Joystick, Matrix, Sound&quot;]
    GAME[&quot;Game Engine: Movement, Boxes, Targets, Victory&quot;]
    APP[&quot;Application State: Start, Playing, Paused, Game Over&quot;]
    APP --&amp;gt; GAME
    GAME --&amp;gt; IN
    IN --&amp;gt; HW
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Hardware drivers&lt;/h3&gt;
&lt;p&gt;Responsible only for things such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SPI
UART
GPIO
ADC
timers
PWM
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Device services&lt;/h3&gt;
&lt;p&gt;Translate hardware into useful concepts:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read_joystick_direction()
button_pressed()
display_board()
set_step_count()
play_sound()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Game engine&lt;/h3&gt;
&lt;p&gt;Should know nothing about AVR registers.&lt;/p&gt;
&lt;p&gt;It should operate on:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;positions
walls
boxes
targets
moves
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Application state machine&lt;/h3&gt;
&lt;p&gt;Controls:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START
PLAYING
PAUSED
GAME_OVER
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This separation would make the game logic much easier to test on a PC without any hardware.&lt;/p&gt;
&lt;p&gt;:::tip[The test I now use for embedded architecture]
If a pure logic module cannot be compiled without including MCU register headers, hardware and application logic are probably too tightly coupled.
:::&lt;/p&gt;
&lt;h2&gt;What I Would Improve in a Second Version&lt;/h2&gt;
&lt;h3&gt;1. Use explicit event objects&lt;/h3&gt;
&lt;p&gt;Instead of output modules checking the game state independently, the game engine could emit events:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PLAYER_MOVED
MOVE_REJECTED
BOX_PUSHED
BOX_ON_TARGET
LEVEL_COMPLETE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The sound and UI systems could react without being embedded inside the movement code.&lt;/p&gt;
&lt;h3&gt;2. Create a single scheduler for periodic tasks&lt;/h3&gt;
&lt;p&gt;Rather than giving every feature its own ad-hoc timing checks, I would centralise periodic scheduling for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;player flashing
target animation
seven-segment multiplexing
sound sequencing
level-time refresh
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;3. Unit-test the movement engine on a computer&lt;/h3&gt;
&lt;p&gt;Collision rules are deterministic and do not require real hardware.&lt;/p&gt;
&lt;p&gt;I would create small board states and test cases such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;push box into wall -&amp;gt; rejected
push box onto target -&amp;gt; valid
walk through edge -&amp;gt; wrapped
diagonal joystick move into box -&amp;gt; rejected
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. Keep peripheral code extremely small&lt;/h3&gt;
&lt;p&gt;The more work that happens inside interrupt handlers and device drivers, the harder the whole game becomes to reason about.&lt;/p&gt;
&lt;h3&gt;5. Treat debugging interfaces as first-class features&lt;/h3&gt;
&lt;p&gt;The UART terminal was useful not only as a user interface but also as a debugging window into the system.&lt;/p&gt;
&lt;p&gt;In future embedded projects, I would deliberately design diagnostics instead of adding &lt;code&gt;printf()&lt;/code&gt; calls only when something breaks.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This project was small enough that I could understand almost every line of the system, but large enough to expose the problems that appear when multiple peripherals share one microcontroller.&lt;/p&gt;
&lt;p&gt;I started the project thinking mainly about C and AVR registers.&lt;/p&gt;
&lt;p&gt;I finished it thinking much more about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;state ownership;&lt;/li&gt;
&lt;li&gt;abstraction boundaries;&lt;/li&gt;
&lt;li&gt;non-blocking timing;&lt;/li&gt;
&lt;li&gt;reusable input paths;&lt;/li&gt;
&lt;li&gt;deterministic game rules;&lt;/li&gt;
&lt;li&gt;multi-display consistency;&lt;/li&gt;
&lt;li&gt;peripheral scheduling.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That shift mattered more than any individual feature.&lt;/p&gt;
&lt;p&gt;The LED matrix, joystick, terminal, seven-segment display, and buzzer were all useful, but the real engineering challenge was making them behave as &lt;strong&gt;one coherent system&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The lesson I kept from Sokoban is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;On a microcontroller, complexity rarely comes from one difficult algorithm. It comes from many simple things needing to remain correct at the same time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That idea has followed me into every larger embedded and real-time project I have worked on since.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Building a Multithreaded Chess Server in C: TCP, Threads, Pipes, and Stockfish</title><link>https://chaosnap.github.io/MyBlog/posts/multithreaded-chess-server-c/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/multithreaded-chess-server-c/</guid><description>Lessons from designing a networked chess service in C with asynchronous clients, one server thread per connection, shared game state, human matchmaking, and a single Stockfish process connected through pipes.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a Multithreaded Chess Server in C: TCP, Threads, Pipes, and Stockfish&lt;/h1&gt;
&lt;p&gt;A chess server is a surprisingly good systems-programming project.&lt;/p&gt;
&lt;p&gt;At first glance, the application sounds simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect a client
receive a move
check the move
send a response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The interesting part appears when the requirements become more realistic.&lt;/p&gt;
&lt;p&gt;Several clients may be connected at the same time. Some want to play the computer, while others want to be paired with another human. The chess engine is a separate process. Network communication is asynchronous. A human opponent can disconnect halfway through a game. The server has to retain game state, serialize access to shared resources, and continue running even when one connection disappears.&lt;/p&gt;
&lt;p&gt;That turns a chess application into a compact exercise in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TCP networking
POSIX threads
process creation
pipes and file descriptors
inter-process communication
mutual exclusion
protocol parsing
shared state
failure handling
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The main lesson I took from this project was not about chess.&lt;/p&gt;
&lt;p&gt;It was about &lt;strong&gt;where state should live when several independently executing parts of a program need to cooperate&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./server-architecture.webp&quot; alt=&quot;High-level architecture of the chess service&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Multiple TCP sessions are handled concurrently, while game state and the single chess-engine process are shared resources.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The Architecture Has Three Different Kinds of Concurrency&lt;/h2&gt;
&lt;p&gt;I found it useful to separate the problem into three concurrency domains.&lt;/p&gt;
&lt;h3&gt;1. Client-side asynchrony&lt;/h3&gt;
&lt;p&gt;The client has two independent input sources:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stdin
network socket
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A user may type a command while a server message is arriving.&lt;/p&gt;
&lt;h3&gt;2. Server-side client concurrency&lt;/h3&gt;
&lt;p&gt;Every connected client has an independent network session.&lt;/p&gt;
&lt;p&gt;The server therefore needs to make progress on many sockets at the same time.&lt;/p&gt;
&lt;h3&gt;3. Process-level concurrency&lt;/h3&gt;
&lt;p&gt;The chess engine is not a library call.&lt;/p&gt;
&lt;p&gt;Stockfish is a separate process with its own stdin and stdout, connected to the server with pipes.&lt;/p&gt;
&lt;p&gt;These look similar because all three involve &quot;multiple things happening&quot;, but they require different abstractions.&lt;/p&gt;
&lt;p&gt;That distinction helped me avoid treating every problem as simply another thread.&lt;/p&gt;
&lt;h2&gt;Lesson 1 - A Network Client Has to Listen in Two Directions&lt;/h2&gt;
&lt;p&gt;A blocking command-line client is easy to imagine:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;read user input
send request
wait for reply
repeat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That model assumes every message from the server is a direct reply to the most recent request.&lt;/p&gt;
&lt;p&gt;A multiplayer game breaks that assumption.&lt;/p&gt;
&lt;p&gt;If another human player makes a move, the server can send a message even though the local user did not just type anything. A game can also end because the other player resigns or disconnects.&lt;/p&gt;
&lt;p&gt;So the client really has two independent flows.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./async-client.webp&quot; alt=&quot;Asynchronous client structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;One path reads commands from &lt;code&gt;stdin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The other continuously reads messages from the TCP connection.&lt;/p&gt;
&lt;p&gt;The client itself should remain relatively thin. It does not need to understand the whole board. It only needs enough local state to decide whether commands such as &lt;code&gt;move&lt;/code&gt;, &lt;code&gt;hint&lt;/code&gt;, or &lt;code&gt;resign&lt;/code&gt; are currently meaningful.&lt;/p&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;game in progress?
my turn?
my colour?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The authoritative chess position belongs on the server side.&lt;/p&gt;
&lt;p&gt;This gave me a useful distributed-systems rule:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A client should store only the state it needs to provide its interface. The authoritative state should live where decisions are validated.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lesson 2 - Text Protocols Are Excellent for Debugging&lt;/h2&gt;
&lt;p&gt;The client and server communicate over TCP using a line-oriented text protocol.&lt;/p&gt;
&lt;p&gt;Typical messages can be thought of as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start computer white
start human either
move e2e4
hint best
hint all
board
resign
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and responses as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;started white
ok
moved e7e5
check
moves ...
gameover ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a student project, a text protocol has a major advantage: it is observable.&lt;/p&gt;
&lt;p&gt;I can connect with a simple terminal tool and manually send commands.&lt;/p&gt;
&lt;p&gt;That lets me test the server independently from my own client implementation.&lt;/p&gt;
&lt;p&gt;This is an underrated design property.&lt;/p&gt;
&lt;p&gt;When both sides of a protocol are being developed at the same time, a human-readable wire format makes it much easier to answer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Is the problem in the client, the server, or the protocol?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Binary protocols may be more compact, but debuggability has real engineering value.&lt;/p&gt;
&lt;h2&gt;Lesson 3 - One Thread Per Client Is Simple Until State Becomes Shared&lt;/h2&gt;
&lt;p&gt;The server accepts incoming TCP connections and creates one handler thread for each client.&lt;/p&gt;
&lt;p&gt;That is conceptually straightforward:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;accept connection
    ↓
spawn handler thread
    ↓
read command
process command
send response
repeat
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The difficulty appears when threads stop being independent.&lt;/p&gt;
&lt;p&gt;Two client threads may refer to the same human-vs-human game.&lt;/p&gt;
&lt;p&gt;Many client threads may need the same Stockfish process.&lt;/p&gt;
&lt;p&gt;Several threads may access the waiting-player list.&lt;/p&gt;
&lt;p&gt;At that point, the important question is no longer:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How many threads do I have?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It becomes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Which data can be touched by more than one thread?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;./shared-engine.webp&quot; alt=&quot;Shared engine access&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Every shared object needs a clearly defined synchronization policy.&lt;/p&gt;
&lt;p&gt;A large mutex around everything may be correct but unnecessarily restrictive.&lt;/p&gt;
&lt;p&gt;No mutex at all may work in simple tests and fail unpredictably under simultaneous connections.&lt;/p&gt;
&lt;p&gt;The architecture gets much easier to reason about when shared resources are listed explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connected-client registry
human-matchmaking pool
shared Game objects
Stockfish stdin/stdout transaction
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Lesson 4 - A Single Chess Engine Becomes a Serialized Service&lt;/h2&gt;
&lt;p&gt;One of the most interesting constraints was that the server uses &lt;strong&gt;one Stockfish process&lt;/strong&gt; even while supporting multiple clients and games.&lt;/p&gt;
&lt;p&gt;That means the engine cannot safely be treated as if every client thread owns its own private chess library.&lt;/p&gt;
&lt;p&gt;Suppose two threads do this at the same time:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Thread A:
position &amp;lt;game A&amp;gt;
go ...

Thread B:
position &amp;lt;game B&amp;gt;
go ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If those command sequences interleave, the response no longer has an unambiguous owner.&lt;/p&gt;
&lt;p&gt;The engine interaction therefore has to be treated as a transaction:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lock engine
set game context
send UCI command
read complete response
unlock engine
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The engine lock is not merely protecting a file descriptor.&lt;/p&gt;
&lt;p&gt;It protects the &lt;strong&gt;semantic integrity of a request-response conversation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That distinction matters.&lt;/p&gt;
&lt;p&gt;Many concurrency bugs happen because programmers lock individual writes but forget that a protocol operation consists of several ordered writes and reads.&lt;/p&gt;
&lt;h2&gt;Lesson 5 - Pipes Turn a Program into a Service&lt;/h2&gt;
&lt;p&gt;Stockfish runs as a child process.&lt;/p&gt;
&lt;p&gt;The server creates two pipes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server -&amp;gt; Stockfish stdin
Stockfish stdout -&amp;gt; server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then forks, redirects file descriptors in the child, and launches the engine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./stockfish-ipc.webp&quot; alt=&quot;Stockfish process and pipe structure&quot; /&gt;&lt;/p&gt;
&lt;p&gt;The startup sequence also has a small protocol of its own.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;start process
    ↓
isready
    ↓
readyok
    ↓
uci
    ↓
uciok
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This was one of the parts of the project I liked most because it connects several Unix concepts that can otherwise feel unrelated:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pipe()
fork()
dup / descriptor redirection
exec()
FILE* or descriptor I/O
process lifecycle
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result is effectively a local service boundary.&lt;/p&gt;
&lt;p&gt;Stockfish could have been a library, but using a process gives useful isolation:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the engine has its own address space;&lt;/li&gt;
&lt;li&gt;it can be replaced independently;&lt;/li&gt;
&lt;li&gt;communication is constrained to a documented protocol;&lt;/li&gt;
&lt;li&gt;engine termination can be detected through the pipe.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Lesson 6 - FEN Is a Better Game-State Boundary Than a Thread&lt;/h2&gt;
&lt;p&gt;A common mistake in threaded applications is to associate state too strongly with the thread currently executing.&lt;/p&gt;
&lt;p&gt;A thread is only an execution context.&lt;/p&gt;
&lt;p&gt;The chess game itself has a longer lifetime.&lt;/p&gt;
&lt;p&gt;A useful game object needs to know things such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;in progress or finished
white player
black player
current / final board state
whose turn is next
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The board state can be represented using &lt;strong&gt;FEN (Forsyth-Edwards Notation)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./game-state.webp&quot; alt=&quot;Relationship between clients, game state, and Stockfish&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This creates a useful separation:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client
    = communication endpoint

Thread
    = code currently handling the endpoint

Game
    = shared application state

FEN
    = serializable chess position
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For a human-vs-human game, two client threads can point at the same &lt;code&gt;Game&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Neither thread &lt;em&gt;is&lt;/em&gt; the game.&lt;/p&gt;
&lt;p&gt;That lesson generalizes well beyond chess.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In concurrent servers, long-lived domain state should usually be represented explicitly rather than hidden inside thread-local control flow.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;Lesson 7 - The Engine Can Be Reconstructed from State&lt;/h2&gt;
&lt;p&gt;Because the server stores the board as FEN, the single Stockfish instance can be switched between games.&lt;/p&gt;
&lt;p&gt;Before asking the engine a question, the server can reconstruct the relevant position:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;position fen &amp;lt;current FEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and then issue a query.&lt;/p&gt;
&lt;p&gt;This is what makes sharing one engine practical.&lt;/p&gt;
&lt;p&gt;The engine is not trusted as the only copy of the application state.&lt;/p&gt;
&lt;p&gt;Instead:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server game state
      ↓
reconstruct Stockfish context
      ↓
ask question
      ↓
parse response
      ↓
update server game state
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That architecture is much safer than allowing the child process to become an undocumented global state store.&lt;/p&gt;
&lt;p&gt;It also resembles how stateless backend services are designed: send enough context with each transaction that the service can answer correctly.&lt;/p&gt;
&lt;h2&gt;Lesson 8 - Move Validation Is a Pipeline, Not a Boolean Function&lt;/h2&gt;
&lt;p&gt;A move such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;e2e4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;looks like it should produce:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;valid / invalid
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the full server operation is richer.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./move-validation.webp&quot; alt=&quot;Move-validation pipeline&quot; /&gt;&lt;/p&gt;
&lt;p&gt;A useful conceptual sequence is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. load the current FEN
2. apply the proposed move in the engine context
3. inspect the resulting board
4. determine whether the state changed
5. count legal replies
6. determine check / checkmate / stalemate
7. if playing the computer, request its best response
8. update FEN
9. notify the relevant client or clients
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The UCI interface gives several tools for different parts of that process.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;position&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Defines the board state and optionally applies a candidate move.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;d&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Returns diagnostic board information, including state that can be used to recover the current FEN and detect check conditions.&lt;/p&gt;
&lt;h3&gt;&lt;code&gt;go perft 1&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Enumerates legal moves from the current position.&lt;/p&gt;
&lt;p&gt;The number of legal replies matters when distinguishing:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;check
checkmate
stalemate
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;go movetime ... depth ...&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Searches for a best move, which can be used for hints or the computer opponent.&lt;/p&gt;
&lt;p&gt;This part taught me that &quot;validation&quot; in a real application often crosses several subsystems.&lt;/p&gt;
&lt;p&gt;It is not necessarily one function call.&lt;/p&gt;
&lt;h2&gt;Lesson 9 - Matchmaking Is a Shared-State Problem&lt;/h2&gt;
&lt;p&gt;Computer games are comparatively easy because one client owns one game.&lt;/p&gt;
&lt;p&gt;Human matchmaking introduces a waiting pool.&lt;/p&gt;
&lt;p&gt;A player can request:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;white
black
either
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server has to find a compatible waiting client and create one shared game.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./human-matchmaking.webp&quot; alt=&quot;Human-player matchmaking&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This means matchmaking requires a data structure that exists outside any single client thread.&lt;/p&gt;
&lt;p&gt;Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Client A asks for white
    ↓
search waiting pool
    ↓
compatible black/either player?
    ↓
yes -&amp;gt; create game and notify both
no  -&amp;gt; record Client A as waiting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The subtle part is that another client thread may modify the same pool at the same time.&lt;/p&gt;
&lt;p&gt;So matchmaking is simultaneously:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;application logic
+
concurrency control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I found this to be a good example of why race conditions are often really &lt;strong&gt;domain-state races&lt;/strong&gt;, not just low-level memory races.&lt;/p&gt;
&lt;h2&gt;Lesson 10 - Disconnects Are Part of the Game Protocol&lt;/h2&gt;
&lt;p&gt;A socket closing is a transport event.&lt;/p&gt;
&lt;p&gt;In a multiplayer game, it also has application meaning.&lt;/p&gt;
&lt;p&gt;If one human player disconnects during a game, the other player needs to be told that the game is over.&lt;/p&gt;
&lt;p&gt;So cleanup is not simply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;close(fd);
return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server may need to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mark the game finished
notify the opponent
remove the client from matchmaking
release client resources
close the socket
terminate only that handler thread
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;./failure-lifecycle.webp&quot; alt=&quot;Failure and cleanup paths&quot; /&gt;&lt;/p&gt;
&lt;p&gt;This is another systems lesson that I found useful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Resource cleanup and application-state cleanup are not always the same thing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Closing the file descriptor handles the operating-system resource.&lt;/p&gt;
&lt;p&gt;It does not automatically repair the application state that referenced that connection.&lt;/p&gt;
&lt;h2&gt;Lesson 11 - SIGPIPE Is a Networking Design Issue, Not an Edge Case&lt;/h2&gt;
&lt;p&gt;Writing to a closed pipe or socket can generate &lt;code&gt;SIGPIPE&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the default action is allowed to terminate the process, one disconnected client could accidentally kill the whole server.&lt;/p&gt;
&lt;p&gt;Similarly, failure of the Stockfish pipe means something very different from failure of one client connection.&lt;/p&gt;
&lt;p&gt;The failure policy should reflect the ownership boundary:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client socket failure
    -&amp;gt; terminate one client session
    -&amp;gt; server continues

Stockfish process failure
    -&amp;gt; shared engine unavailable
    -&amp;gt; all games lose engine service
    -&amp;gt; notify clients and terminate server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is a clean example of &lt;strong&gt;failure domains&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The scope of recovery should match the scope of the failed resource.&lt;/p&gt;
&lt;h2&gt;Lesson 12 - Blocking Is Good When There Is Nothing to Do&lt;/h2&gt;
&lt;p&gt;Concurrent programs sometimes become complicated because developers try too hard to make everything continuously active.&lt;/p&gt;
&lt;p&gt;A server thread waiting for work should often simply block.&lt;/p&gt;
&lt;p&gt;Examples include:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;accept()
read()
fgets()
getline()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A blocked thread consumes almost no CPU while waiting for an event.&lt;/p&gt;
&lt;p&gt;The bad alternative is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;check socket
nothing there
check again
check again
sleep a little
check again
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Busy waiting adds latency, wastes CPU, and makes the server harder to reason about.&lt;/p&gt;
&lt;p&gt;This project reinforced an important Unix programming habit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If progress depends on external input, block on the object that will deliver that input.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;A Data Model I Would Use&lt;/h2&gt;
&lt;p&gt;If I were structuring the project again, I would make the major domain objects explicit.&lt;/p&gt;
&lt;h3&gt;Client&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;socket / FILE streams
selected game mode
requested colour
current Game pointer
connection state
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Game&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;white client or computer
black client or computer
FEN
in-progress flag
finished state
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Server&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;listener
client collection
waiting-player collection
engine connection
engine mutex
shared-state mutexes
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Engine&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;child PID
write stream
read stream
transaction lock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The exact C structures can vary.&lt;/p&gt;
&lt;p&gt;What matters is that ownership is visible from the model.&lt;/p&gt;
&lt;h2&gt;The Architecture I Would Prefer Today&lt;/h2&gt;
&lt;p&gt;I would still keep the overall thread-per-client model because it matches the scale of the problem well.&lt;/p&gt;
&lt;p&gt;But I would make a few design rules explicit.&lt;/p&gt;
&lt;h3&gt;Keep socket parsing outside game logic&lt;/h3&gt;
&lt;p&gt;A function that updates a chess game should not also be responsible for splitting TCP lines.&lt;/p&gt;
&lt;h3&gt;Keep UCI parsing behind an engine API&lt;/h3&gt;
&lt;p&gt;The rest of the server should ask questions such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;is this move valid?
what is the resulting FEN?
what are the legal moves?
what is the best move?
is the side in check?
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;rather than manually scanning Stockfish output everywhere.&lt;/p&gt;
&lt;h3&gt;Keep matchmaking operations atomic&lt;/h3&gt;
&lt;p&gt;Search + remove + create-game should be one protected state transition.&lt;/p&gt;
&lt;h3&gt;Keep lock scope understandable&lt;/h3&gt;
&lt;p&gt;The engine lock may need to cover a complete UCI transaction.&lt;/p&gt;
&lt;p&gt;A game-state lock should not be held while waiting unnecessarily on slow I/O unless the consistency requirement demands it.&lt;/p&gt;
&lt;h3&gt;Treat messages to human opponents as cross-thread communication&lt;/h3&gt;
&lt;p&gt;One client handler may need to send a notification to another client&apos;s socket.&lt;/p&gt;
&lt;p&gt;That should be an intentional capability of the data model, not an accidental global variable.&lt;/p&gt;
&lt;h2&gt;What I Would Improve in a Second Version&lt;/h2&gt;
&lt;h3&gt;1. Add a dedicated engine worker thread&lt;/h3&gt;
&lt;p&gt;Instead of allowing client threads to directly acquire the Stockfish lock, I would consider giving the engine its own request queue.&lt;/p&gt;
&lt;p&gt;The architecture becomes:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client threads
    ↓
engine-request queue
    ↓
single engine worker
    ↓
Stockfish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This serializes access naturally and makes the engine an active service rather than a locked resource.&lt;/p&gt;
&lt;h3&gt;2. Use typed internal requests&lt;/h3&gt;
&lt;p&gt;The network protocol is text.&lt;/p&gt;
&lt;p&gt;The internal server representation does not need to be.&lt;/p&gt;
&lt;p&gt;I would parse incoming messages immediately into structured requests such as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;START_GAME
BOARD_REQUEST
HINT_BEST
HINT_ALL
MOVE
RESIGN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That reduces repeated string handling inside domain logic.&lt;/p&gt;
&lt;h3&gt;3. Centralize game transitions&lt;/h3&gt;
&lt;p&gt;I would place all game lifecycle changes behind a small API:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;game_start()
game_apply_move()
game_resign()
game_disconnect()
game_finish()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That makes it harder for two different request handlers to update state inconsistently.&lt;/p&gt;
&lt;h3&gt;4. Add stronger runtime instrumentation&lt;/h3&gt;
&lt;p&gt;I would measure:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connected clients
waiting players
active games
engine request latency
commands per client
lock wait time
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Concurrency problems are much easier to diagnose when the program can explain what it is doing.&lt;/p&gt;
&lt;h3&gt;5. Test the server independently from the official client&lt;/h3&gt;
&lt;p&gt;Because the protocol is text based, I would use scripted raw TCP tests for:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;malformed commands
rapid reconnects
two clients moving at nearly the same time
disconnect during a game
engine failure
human colour matching
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That kind of testing is more effective than relying only on manual gameplay.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This project looked like a chess application, but most of the difficult reasoning had nothing to do with chess strategy.&lt;/p&gt;
&lt;p&gt;The real questions were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who owns the board state?&lt;/li&gt;
&lt;li&gt;How do several client threads share one engine?&lt;/li&gt;
&lt;li&gt;How should two human clients be paired?&lt;/li&gt;
&lt;li&gt;What must be protected by synchronization?&lt;/li&gt;
&lt;li&gt;Which failures affect one session and which affect the whole service?&lt;/li&gt;
&lt;li&gt;How do I preserve state when the executing thread changes?&lt;/li&gt;
&lt;li&gt;Where should protocol parsing stop and domain logic begin?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those questions appear in many real systems.&lt;/p&gt;
&lt;p&gt;A database server, build service, multiplayer game, hardware-control daemon, or model-serving backend can all have the same underlying shape:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;many clients
    ↓
concurrent request handling
    ↓
shared domain state
    ↓
serialized access to a limited resource
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The most useful lesson I kept from the project is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Concurrency becomes manageable when state ownership is explicit and communication boundaries are designed before the threads are added.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The threads themselves are not the architecture.&lt;/p&gt;
&lt;p&gt;The architecture is the set of rules that determines how those threads are allowed to interact.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Building a Real-Time Rhythm Game on STM32: Timing, DMA, LEDs, and GUI Integration</title><link>https://chaosnap.github.io/MyBlog/posts/tpmania-realtime-rhythm-game/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/tpmania-realtime-rhythm-game/</guid><description>Engineering lessons from building tpmania, a real-time STM32 rhythm game with a 16x4 LED matrix, arcade buttons, microSD assets, audio playback, and a cross-platform configuration GUI.</description><pubDate>Thu, 20 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Building a Real-Time Rhythm Game on STM32: Timing, DMA, LEDs, and GUI Integration&lt;/h1&gt;
&lt;p&gt;A rhythm game is a surprisingly good real-time systems project.&lt;/p&gt;
&lt;p&gt;On the surface, the idea is simple: play a song, light up the correct lane, detect a button press, and decide whether the player was early, late, or on time. Once everything runs on the same microcontroller, however, the problem becomes much more interesting. Audio must remain continuous, LEDs must update without flicker, button input must be timestamped accurately, files must be read from storage, and the user interface still has to feel responsive.&lt;/p&gt;
&lt;p&gt;That was the main engineering challenge behind &lt;strong&gt;tpmania&lt;/strong&gt;, a mini arcade rhythm game that our team built around an STM32-based embedded platform. The system combined a 16x4 addressable LED matrix, four illuminated arcade buttons, an OLED interface, microSD-based song and sequence storage, USB configuration, line-level audio, and a custom PCB.&lt;/p&gt;
&lt;p&gt;My main work was on the firmware and the PC-side configuration GUI. I implemented the LED-matrix frame pipeline and colour mapping, beat and half-beat progression, button LED cueing, scoring windows and combo logic, sequence parsing, microSD file management, OLED overlays, and the PyQt-based host interface. More importantly, I had to make all of these parts coexist without destroying the timing behaviour of the game.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./gameplay-hardware.webp&quot; alt=&quot;tpmania hardware during gameplay testing&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An intermediate hardware setup used to test the OLED menu, illuminated buttons, and LED-matrix timing together.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;The System Was Really Several Real-Time Pipelines&lt;/h2&gt;
&lt;p&gt;I found it useful to stop thinking about the project as &quot;one program&quot; and instead think about it as several pipelines sharing one MCU.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;flowchart LR
    SD[microSD&amp;lt;br/&amp;gt;.tsq + .wav] --&amp;gt; Parser[Sequence / File Parser]
    Parser --&amp;gt; Beat[Beat Scheduler]
    Beat --&amp;gt; LED[16x4 LED Matrix]
    Beat --&amp;gt; Score[Scoring Engine]
    BTN[Arcade Buttons] --&amp;gt; Score

    SD --&amp;gt; Audio[Audio Streaming]
    Audio --&amp;gt; DAC[PCM5102A / Line Out]

    OLED[OLED Menu] &amp;lt;--&amp;gt; Control[UI / Game State]
    Control --&amp;gt; Beat
    Control --&amp;gt; Audio

    GUI[PyQt GUI] &amp;lt;--&amp;gt; USB[USB CDC]
    USB &amp;lt;--&amp;gt; Control
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The game only feels correct if these pipelines agree on time.&lt;/p&gt;
&lt;p&gt;The LED matrix can be visually perfect and still feel wrong if it is a few tens of milliseconds away from the music. The button interrupt can be fast and still produce unfair scoring if it is compared against the wrong reference timestamp. A GUI can be responsive and still cause problems if configuration transfers block the embedded side at the wrong moment.&lt;/p&gt;
&lt;p&gt;That changed the way I approached the firmware: &lt;strong&gt;timing was not a feature added near the end; it was part of the architecture.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Lesson 1 - Represent Musical Time Explicitly&lt;/h2&gt;
&lt;p&gt;The basic timing relationship is simple:&lt;/p&gt;
&lt;p&gt;$$
T_{\text{beat}} = \frac{60}{\mathrm{BPM}}
$$&lt;/p&gt;
&lt;p&gt;and for a half-beat event:&lt;/p&gt;
&lt;p&gt;$$
T_{\text{half}} = \frac{30}{\mathrm{BPM}}
$$&lt;/p&gt;
&lt;p&gt;But a working game needs more than a beat period. It needs a consistent definition of where each expected event occurs, how long the hit window lasts, and how input timestamps are compared against those events.&lt;/p&gt;
&lt;p&gt;My scoring logic used configurable timing windows around the expected hit time. Conceptually:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;             early                        late
               &amp;lt;----------------------------&amp;gt;
------ MISS ------ GOOD ---- PERFECT ---- GOOD ------ MISS ------
                          expected hit
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of treating the tolerances as unrelated fixed numbers, I tied them to the beat period. This made the scoring behaviour much more consistent when moving between slower and faster songs.&lt;/p&gt;
&lt;p&gt;A simplified model is:&lt;/p&gt;
&lt;p&gt;$$
W_i = \alpha_i T_{\text{beat}}
$$&lt;/p&gt;
&lt;p&gt;where $W_i$ is a timing window and $\alpha_i$ is a configurable tolerance ratio.&lt;/p&gt;
&lt;p&gt;The embedded scheduler compares the button timestamp against the expected hit time:&lt;/p&gt;
&lt;p&gt;$$
e = t_{\text{press}} - t_{\text{expected}}
$$&lt;/p&gt;
&lt;p&gt;and classifies the result based on $|e|$.&lt;/p&gt;
&lt;p&gt;That may sound obvious, but this model became important once half-beat sequences were introduced. The initial project plan assumed single-beat events. Supporting half-beat content later forced changes across the sequence parser, the game state machine, the GUI, and the on-device interface.&lt;/p&gt;
&lt;p&gt;:::important[One timing change can propagate through the whole stack]
Changing the musical time model is not only a firmware change. It affects the file format, parser, visualisation, scoring rules, GUI configuration, and validation data.
:::&lt;/p&gt;
&lt;h2&gt;Lesson 2 - Offload Repetitive Timing Work Whenever Possible&lt;/h2&gt;
&lt;p&gt;The LED matrix used addressable LEDs, which are convenient but timing-sensitive. Updating them directly from blocking code would consume too much CPU time and could interfere with the rest of the game.&lt;/p&gt;
&lt;p&gt;The solution was to use a DMA-driven LED pipeline so that once the frame data had been prepared, hardware could handle much of the transfer while the CPU continued with game logic.&lt;/p&gt;
&lt;p&gt;Conceptually, the frame path looked like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Game state
   ↓
Lane / beat mapping
   ↓
RGB frame buffer
   ↓
PWM/serial waveform buffer
   ↓
DMA transfer
   ↓
WS2812 LED matrix
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The important lesson for me was not simply &quot;DMA is fast.&quot;&lt;/p&gt;
&lt;p&gt;The real advantage is that DMA makes execution &lt;strong&gt;more predictable&lt;/strong&gt;. Instead of a large CPU loop competing with input handling and display logic, the CPU prepares data and then lets the peripheral move it.&lt;/p&gt;
&lt;p&gt;This same principle appears throughout embedded systems:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;use DMA for repetitive data movement;&lt;/li&gt;
&lt;li&gt;let timers generate timing-critical events;&lt;/li&gt;
&lt;li&gt;use peripherals for waveform generation;&lt;/li&gt;
&lt;li&gt;keep interrupts short;&lt;/li&gt;
&lt;li&gt;let the main application deal with state and policy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The result is not only higher performance. It is a system that is easier to reason about.&lt;/p&gt;
&lt;h2&gt;Lesson 3 - Audio Problems Often Reveal Scheduling Problems&lt;/h2&gt;
&lt;p&gt;One of the most memorable integration issues was distorted audio during simultaneous OLED and LED activity.&lt;/p&gt;
&lt;p&gt;The audio pipeline streamed 16-bit PCM data from microSD storage, transferred samples over I2S/SAI to a PCM5102A DAC, and used a ping-pong buffer so one half could be played while the other half was refilled.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./system-overview.webp&quot; alt=&quot;Audio streaming and control flow&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The project used a double-buffered streaming pipeline so file access and playback could overlap.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The general pattern is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant SD as microSD
    participant CPU as CPU
    participant B0 as Buffer Half A
    participant B1 as Buffer Half B
    participant DMA as DMA / SAI
    participant DAC as PCM5102A

    CPU-&amp;gt;&amp;gt;SD: Read next PCM block
    SD--&amp;gt;&amp;gt;CPU: Samples
    CPU-&amp;gt;&amp;gt;B0: Fill A
    DMA-&amp;gt;&amp;gt;B0: Stream A
    DMA-&amp;gt;&amp;gt;DAC: I2S samples
    CPU-&amp;gt;&amp;gt;B1: Fill B
    DMA-&amp;gt;&amp;gt;B1: Stream B
    CPU-&amp;gt;&amp;gt;B0: Refill A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The architecture is sound, but the system can still fail if some unrelated interrupt blocks the CPU for too long.&lt;/p&gt;
&lt;p&gt;During integration, OLED and LED-related work could delay the audio path enough to produce audible distortion and pops. The oscilloscope became more useful than reading code because the failure was fundamentally temporal.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./audio-debug.webp&quot; alt=&quot;Oscilloscope used while debugging audio playback&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The audio path exposed scheduling and interrupt-latency problems that were difficult to see from source code alone.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This was a strong reminder that peripherals do not only compete for pins and buses. They also compete for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPU time;&lt;/li&gt;
&lt;li&gt;interrupt latency;&lt;/li&gt;
&lt;li&gt;memory bandwidth;&lt;/li&gt;
&lt;li&gt;bus access;&lt;/li&gt;
&lt;li&gt;buffer refill windows.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After this project, I became much more careful about putting expensive work inside interrupts. An interrupt should usually capture an event, update minimal state, and leave heavier processing to another execution context.&lt;/p&gt;
&lt;p&gt;:::tip[Debug the timing, not just the function]
When a subsystem works alone but fails during integration, check execution time, ISR duration, bus contention, and buffer deadlines before assuming the peripheral driver itself is wrong.
:::&lt;/p&gt;
&lt;h2&gt;Lesson 4 - A Rhythm Game Needs a Content Pipeline, Not Just Firmware&lt;/h2&gt;
&lt;p&gt;Another part I underestimated was content management.&lt;/p&gt;
&lt;p&gt;The game did not only need a &lt;code&gt;.wav&lt;/code&gt; file. It also needed a sequence describing the timing and lane information used by the LED matrix and scoring logic. This meant that song data became a small data pipeline:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Song metadata
      +
Audio (.wav)
      +
Sequence (.tsq)
      ↓
microSD
      ↓
Embedded parser
      ↓
Game scheduler
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The parser had to support both normal beats and half-beats, while the GUI needed to upload, delete, and manage the corresponding files.&lt;/p&gt;
&lt;p&gt;This is where the project started to feel less like a microcontroller exercise and more like a complete product.&lt;/p&gt;
&lt;p&gt;The microSD interface and file system were now part of the user experience. File naming, metadata consistency, parsing errors, and missing assets were no longer &quot;storage problems&quot; - they directly affected whether a song could be played.&lt;/p&gt;
&lt;p&gt;I learned to treat data formats as interfaces in the same way I treat UART or SPI.&lt;/p&gt;
&lt;p&gt;A sequence format should answer questions such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What is the version?&lt;/li&gt;
&lt;li&gt;How is BPM represented?&lt;/li&gt;
&lt;li&gt;Are timestamps absolute or beat-relative?&lt;/li&gt;
&lt;li&gt;How are half-beats encoded?&lt;/li&gt;
&lt;li&gt;What happens if a field is missing?&lt;/li&gt;
&lt;li&gt;Can old firmware reject a newer sequence safely?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I rebuilt the format today, I would introduce explicit versioning and stronger validation much earlier.&lt;/p&gt;
&lt;h2&gt;Lesson 5 - The PC GUI Was Part of the Embedded System&lt;/h2&gt;
&lt;p&gt;I also built a cross-platform configuration GUI in Python with PyQt.&lt;/p&gt;
&lt;p&gt;It communicated with the embedded system over USB CDC and provided controls for timing-window configuration, general settings, device status, and microSD content management.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./timing-gui.webp&quot; alt=&quot;Timing-window configuration in the PyQt GUI&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The host GUI exposed scoring tolerances and device configuration without requiring firmware recompilation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The timing page was especially useful because it converted what would otherwise be compile-time constants into tunable parameters.&lt;/p&gt;
&lt;p&gt;That made testing much faster.&lt;/p&gt;
&lt;p&gt;Instead of:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;change constant
→ rebuild firmware
→ flash MCU
→ replay song
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;I could do:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;adjust GUI
→ send configuration
→ replay song
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This matters in systems where the &quot;correct&quot; value is partly determined by human perception. A mathematically symmetric timing window may not necessarily feel fair when physical button latency, display delay, and audio latency are included.&lt;/p&gt;
&lt;p&gt;The GUI also handled the music library on the storage device.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./music-library.webp&quot; alt=&quot;Music library management interface&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The GUI provided a host-side workflow for managing game assets and synchronising them with the embedded device.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One design choice I liked was keeping the host GUI conceptually similar to the on-device menu. Users should not have to learn two completely different mental models for the same settings.&lt;/p&gt;
&lt;p&gt;This project made me appreciate that PC-side tooling is often an important part of embedded engineering. A good configuration and diagnostic interface can reduce firmware complexity, accelerate testing, and make hardware much easier to use.&lt;/p&gt;
&lt;h2&gt;Lesson 6 - Hardware-Software Co-Design Starts Before the PCB Is Finished&lt;/h2&gt;
&lt;p&gt;Although my main responsibility was firmware and GUI development, the custom PCB directly influenced software decisions.&lt;/p&gt;
&lt;p&gt;The final hardware included the STM32 microcontroller, microSD interface, OLED connection, button connectors, LED-matrix connection, programming interface, test points, and power-related circuitry.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./schematic-overview.webp&quot; alt=&quot;High-level schematic overview&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The schematic shows how the MCU, storage, display, buttons, LED matrix, test headers, and supporting hardware were organised.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The PCB artwork made the interfaces physical.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;./pcb-artwork.webp&quot; alt=&quot;PCB artwork&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The custom PCB turned firmware assumptions about pins, connectors, power rails, and peripherals into fixed hardware constraints.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is where hardware-software co-design becomes very concrete.&lt;/p&gt;
&lt;p&gt;A firmware decision such as &quot;I will use this timer channel for the LED output&quot; affects PCB routing and pin assignment. A hardware decision such as &quot;this connector is on this pin&quot; may determine which peripheral instance is available in firmware.&lt;/p&gt;
&lt;p&gt;I learned to review pin assignments, peripheral alternate functions, and timing-critical signals before layout is finalised. Waiting until the board arrives is too late to discover that two features depend on the same peripheral resource.&lt;/p&gt;
&lt;h2&gt;Validation: Real-Time Performance Has to Be Measured&lt;/h2&gt;
&lt;p&gt;The final system was tested across different songs and BPM values. According to our project validation, the final build maintained continuous playback without buffer underruns and achieved less than 2 ms latency between visual and audio events.&lt;/p&gt;
&lt;p&gt;That result mattered because a rhythm game is unusually sensitive to timing errors.&lt;/p&gt;
&lt;p&gt;For many applications, a few tens of milliseconds might be invisible. In a rhythm game, they can change whether the interaction feels responsive.&lt;/p&gt;
&lt;p&gt;I now think of validation in three layers:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Example question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Functional&lt;/td&gt;
&lt;td&gt;Did the correct lane light up?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporal&lt;/td&gt;
&lt;td&gt;Did it light up at the correct time?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Perceptual&lt;/td&gt;
&lt;td&gt;Did the game &lt;em&gt;feel&lt;/em&gt; synchronised to the player?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Passing the first layer is not enough for a real-time interactive system.&lt;/p&gt;
&lt;h2&gt;What I Would Change in a Second Version&lt;/h2&gt;
&lt;p&gt;The prototype worked, but there are several things I would redesign if I started again.&lt;/p&gt;
&lt;h3&gt;1. Introduce an RTOS once the task graph becomes large&lt;/h3&gt;
&lt;p&gt;The project could be managed with a structured scheduler, timers, callbacks, and carefully controlled tasks. As the number of asynchronous activities increases, however, an RTOS such as FreeRTOS or Zephyr would make ownership and task isolation clearer.&lt;/p&gt;
&lt;p&gt;I would separate at least:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Audio task
Game scheduler
Input task
LED rendering
Display/UI
Storage
USB/configuration
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The goal would not be to &quot;use an RTOS because it is more advanced.&quot; The goal would be to make deadlines and priorities explicit.&lt;/p&gt;
&lt;h3&gt;2. Build automated timing tests&lt;/h3&gt;
&lt;p&gt;Reference event timestamps were already useful for validation. I would extend that into an automated test harness that could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;load a known sequence;&lt;/li&gt;
&lt;li&gt;inject synthetic button events;&lt;/li&gt;
&lt;li&gt;verify scoring classifications;&lt;/li&gt;
&lt;li&gt;sweep across BPM values;&lt;/li&gt;
&lt;li&gt;detect timing regressions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. Define a stricter Git workflow&lt;/h3&gt;
&lt;p&gt;Multiple software environments created avoidable merge and synchronisation problems. A clearer branching strategy and automated checks would reduce integration risk.&lt;/p&gt;
&lt;h3&gt;4. Add protocol and file-format versioning&lt;/h3&gt;
&lt;p&gt;Both the USB configuration protocol and &lt;code&gt;.tsq&lt;/code&gt; files should have explicit versions so that incompatible firmware and tools fail safely rather than behaving unpredictably.&lt;/p&gt;
&lt;h3&gt;5. Instrument performance from the beginning&lt;/h3&gt;
&lt;p&gt;I would reserve debug GPIOs and lightweight timing counters for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ISR duration;&lt;/li&gt;
&lt;li&gt;frame-update time;&lt;/li&gt;
&lt;li&gt;buffer-refill time;&lt;/li&gt;
&lt;li&gt;input-to-score latency;&lt;/li&gt;
&lt;li&gt;audio deadline misses.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Instrumentation is much cheaper when designed in early.&lt;/p&gt;
&lt;h2&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;tpmania changed how I think about real-time embedded design.&lt;/p&gt;
&lt;p&gt;Before the project, I associated real-time systems mainly with fast interrupts and precise timers. After building an interactive system where audio, LEDs, storage, input, and GUI control all had to work simultaneously, I realised that real-time engineering is mostly about &lt;strong&gt;controlling interference between otherwise-correct subsystems&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The most important questions became:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Who owns this timing deadline?&lt;/li&gt;
&lt;li&gt;Can this operation block?&lt;/li&gt;
&lt;li&gt;What happens if the SD card is slow?&lt;/li&gt;
&lt;li&gt;How long can this interrupt run?&lt;/li&gt;
&lt;li&gt;Does this peripheral need DMA?&lt;/li&gt;
&lt;li&gt;Which data is shared between tasks?&lt;/li&gt;
&lt;li&gt;What happens when two features are active at the same time?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A feature can work perfectly in isolation and still fail at the system level.&lt;/p&gt;
&lt;p&gt;That is probably the biggest lesson I took from the project:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A real-time embedded system is not a collection of fast components. It is a collection of components whose timing relationships are deliberately designed.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For me, that shift - from writing peripheral code to designing timing relationships - was the most valuable part of building the game.&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>代码块示例</title><link>https://chaosnap.github.io/MyBlog/posts/code-examples/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/code-examples/</guid><description>使用表达性代码的代码块在 Markdown 中的外观。</description><pubDate>Fri, 03 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;在这里，我们将探索如何使用 &lt;a href=&quot;https://expressive-code.com/&quot;&gt;Expressive Code&lt;/a&gt; 展示代码块。提供的示例基于官方文档，您可以参考以获取更多详细信息。&lt;/p&gt;
&lt;h2&gt;表达性代码&lt;/h2&gt;
&lt;h3&gt;语法高亮&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/syntax-highlighting/&quot;&gt;语法高亮&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;常规语法高亮&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;此代码有语法高亮!&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;渲染 ANSI 转义序列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;ANSI colors:
- Regular: [31mRed[0m [32mGreen[0m [33mYellow[0m [34mBlue[0m [35mMagenta[0m [36mCyan[0m
- Bold:    [1;31mRed[0m [1;32mGreen[0m [1;33mYellow[0m [1;34mBlue[0m [1;35mMagenta[0m [1;36mCyan[0m
- Dimmed:  [2;31mRed[0m [2;32mGreen[0m [2;33mYellow[0m [2;34mBlue[0m [2;35mMagenta[0m [2;36mCyan[0m

256 colors (showing colors 160-177):
[38;5;160m160 [38;5;161m161 [38;5;162m162 [38;5;163m163 [38;5;164m164 [38;5;165m165[0m
[38;5;166m166 [38;5;167m167 [38;5;168m168 [38;5;169m169 [38;5;170m170 [38;5;171m171[0m
[38;5;172m172 [38;5;173m173 [38;5;174m174 [38;5;175m175 [38;5;176m176 [38;5;177m177[0m

Full RGB colors:
[38;2;34;139;34mForestGreen - RGB(34, 139, 34)[0m

Text formatting: [1mBold[0m [2mDimmed[0m [3mItalic[0m [4mUnderline[0m
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编辑器和终端框架&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/frames/&quot;&gt;编辑器和终端框架&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;代码编辑器框架&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;标题属性示例&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;!-- src/content/index.html --&amp;gt;
&amp;lt;div&amp;gt;文件名注释示例&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;终端框架&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;此终端框架没有标题&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;Write-Output &quot;这个有标题!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;覆盖框架类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;看，没有框架!&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;# 如果不覆盖，这将是一个终端框架
function Watch-Tail { Get-Content -Tail 20 -Wait $args }
New-Alias tail Watch-Tail
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;文本和行标记&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/text-markers/&quot;&gt;文本和行标记&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;标记整行和行范围&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 第1行 - 通过行号定位
// 第2行
// 第3行
// 第4行 - 通过行号定位
// 第5行
// 第6行
// 第7行 - 通过范围 &quot;7-8&quot; 定位
// 第8行 - 通过范围 &quot;7-8&quot; 定位
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;选择行标记类型 (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;此行标记为已删除&apos;)
  // 此行和下一行标记为已插入
  console.log(&apos;这是第二个插入行&apos;)

  return &apos;此行使用中性默认标记类型&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;为行标记添加标签&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}
  value={value}
  className={buttonClassName}
  disabled={disabled}
  active={active}
&amp;gt;
  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;在单独行上添加长标签&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// labeled-line-markers.jsx
&amp;lt;button
  role=&quot;button&quot;
  {...props}

  value={value}
  className={buttonClassName}

  disabled={disabled}
  active={active}
&amp;gt;

  {children &amp;amp;&amp;amp;
    !active &amp;amp;&amp;amp;
    (typeof children === &apos;string&apos; ? &amp;lt;span&amp;gt;{children}&amp;lt;/span&amp;gt; : children)}
&amp;lt;/button&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;使用类似 diff 的语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+此行将标记为已插入
-此行将标记为已删除
这是常规行
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;--- a/README.md
+++ b/README.md
@@ -1,3 +1,4 @@
+this is an actual diff file
-all contents will remain unmodified
 no whitespace will be removed either
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;结合语法高亮和类似 diff 的语法&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;  function thisIsJavaScript() {
    // 整个块都会以 JavaScript 高亮显示，
    // 并且我们仍然可以为其添加 diff 标记！
-   console.log(&apos;要删除的旧代码&apos;)
+   console.log(&apos;新的闪亮代码！&apos;)
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;标记行内的单独文本&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  // 标记行内的任何给定文本
  return &apos;支持给定文本的多个匹配项&apos;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;正则表达式&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;单词 yes 和 yep 将被标记。&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;转义正斜杠&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;echo &quot;Test&quot; &amp;gt; /home/test.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;选择内联标记类型 (mark, ins, del)&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;function demo() {
  console.log(&apos;这些是插入和删除的标记类型&apos;);
  // return 语句使用默认标记类型
  return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自动换行&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/key-features/word-wrap/&quot;&gt;自动换行&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;为每个块配置自动换行&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// 启用换行的示例
function getLongString() {
  return &apos;这是一个非常长的字符串，除非容器极宽，否则很可能无法适应可用空间&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// wrap=false 的示例
function getLongString() {
  return &apos;这是一个非常长的字符串，除非容器极宽，否则很可能无法适应可用空间&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;配置换行的缩进&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// preserveIndent 示例（默认启用）
function getLongString() {
  return &apos;这是一个非常长的字符串，除非容器极宽，否则很可能无法适应可用空间&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// preserveIndent=false 的示例
function getLongString() {
  return &apos;这是一个非常长的字符串，除非容器极宽，否则很可能无法适应可用空间&apos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;可折叠部分&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/collapsible-sections/&quot;&gt;可折叠部分&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 所有这些样板设置代码将被折叠
import { someBoilerplateEngine } from &apos;@example/some-boilerplate&apos;
import { evenMoreBoilerplate } from &apos;@example/even-more-boilerplate&apos;

const engine = someBoilerplateEngine(evenMoreBoilerplate())

// 这部分代码默认可见
engine.doSomething(1, 2, 3, calcFn)

function calcFn() {
  // 您可以有多个折叠部分
  const a = 1
  const b = 2
  const c = a + b

  // 这将保持可见
  console.log(`计算结果: ${a} + ${b} = ${c}`)
  return c
}

// 直到块末尾的所有代码将再次被折叠
engine.closeConnection()
engine.freeMemory()
engine.shutdown({ reason: &apos;示例样板代码结束&apos; })
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;行号&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://expressive-code.com/plugins/line-numbers/&quot;&gt;行号&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;为每个块显示行号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// 此代码块将显示行号
console.log(&apos;来自第2行的问候!&apos;)
console.log(&apos;我在第3行&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;// 此块禁用行号
console.log(&apos;你好?&apos;)
console.log(&apos;抱歉，你知道我在第几行吗?&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更改起始行号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;console.log(&apos;来自第5行的问候!&apos;)
console.log(&apos;我在第6行&apos;)
&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>KaTeX 数学公式示例</title><link>https://chaosnap.github.io/MyBlog/posts/katex-math-example/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/katex-math-example/</guid><description>展示 Firefly 主题对 KaTeX 数学公式的支持，包括行内公式、块级公式和复杂数学符号。</description><pubDate>Thu, 02 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文展示了 &lt;a href=&quot;https://github.com/CuteLeaf/Firefly&quot;&gt;Firefly&lt;/a&gt; 主题对 KaTeX 数学公式的渲染支持。&lt;/p&gt;
&lt;h2&gt;行内公式 (Inline)&lt;/h2&gt;
&lt;p&gt;行内公式使用单个 &lt;code&gt;$&lt;/code&gt; 符号包裹。&lt;/p&gt;
&lt;p&gt;例如：欧拉公式 $e^{i\pi} + 1 = 0$ 是数学中最优美的公式之一。&lt;/p&gt;
&lt;p&gt;质能方程 $E = mc^2$ 也是家喻户晓。&lt;/p&gt;
&lt;h2&gt;块级公式 (Block)&lt;/h2&gt;
&lt;p&gt;块级公式使用两个 &lt;code&gt;$$&lt;/code&gt; 符号包裹，会居中显示。&lt;/p&gt;
&lt;p&gt;$$
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
$$&lt;/p&gt;
&lt;p&gt;$$
x = \frac{-b \pm \sqrt{b^2 - 4ac}}{2a}
$$&lt;/p&gt;
&lt;h2&gt;复杂示例&lt;/h2&gt;
&lt;h3&gt;矩阵 (Matrices)&lt;/h3&gt;
&lt;p&gt;$$
\begin{pmatrix}
a &amp;amp; b \
c &amp;amp; d
\end{pmatrix}
\begin{pmatrix}
\alpha &amp;amp; \beta \
\gamma &amp;amp; \delta
\end{pmatrix} =
\begin{pmatrix}
a\alpha + b\gamma &amp;amp; a\beta + b\delta \
c\alpha + d\gamma &amp;amp; c\beta + d\delta
\end{pmatrix}
$$&lt;/p&gt;
&lt;h3&gt;极限与求和 (Limits and Sums)&lt;/h3&gt;
&lt;p&gt;$$
\sum_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6}
$$&lt;/p&gt;
&lt;p&gt;$$
\lim_{x \to 0} \frac{\sin x}{x} = 1
$$&lt;/p&gt;
&lt;h3&gt;麦克斯韦方程组 (Maxwell&apos;s Equations)&lt;/h3&gt;
&lt;p&gt;$$
\begin{aligned}
\nabla \cdot \mathbf{E} &amp;amp;= \frac{\rho}{\varepsilon_0} \
\nabla \cdot \mathbf{B} &amp;amp;= 0 \
\nabla \times \mathbf{E} &amp;amp;= -\frac{\partial \mathbf{B}}{\partial t} \
\nabla \times \mathbf{B} &amp;amp;= \mu_0\mathbf{J} + \mu_0\varepsilon_0\frac{\partial \mathbf{E}}{\partial t}
\end{aligned}
$$&lt;/p&gt;
&lt;h3&gt;化学方程式 (Chemical Equations)&lt;/h3&gt;
&lt;p&gt;$$
\ce{CH4 + 2O2 -&amp;gt; CO2 + 2H2O}
$$&lt;/p&gt;
&lt;h2&gt;更多符号&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;符号&lt;/th&gt;
&lt;th&gt;代码&lt;/th&gt;
&lt;th&gt;渲染结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Alpha&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\alpha&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\alpha$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Beta&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\beta&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\beta$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gamma&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\Gamma&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\Gamma$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pi&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\pi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\pi$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infinity&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\infty&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\infty$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Right Arrow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\rightarrow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\rightarrow$&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;&lt;code&gt;\partial&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;$\partial$&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;更多 KaTeX 语法请参考 &lt;a href=&quot;https://katex.org/docs/supported.html&quot;&gt;KaTeX Supported Functions&lt;/a&gt;。&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Markdown Mermaid 图表</title><link>https://chaosnap.github.io/MyBlog/posts/markdown-mermaid/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/markdown-mermaid/</guid><description>一个包含 Mermaid 的 Markdown 博客文章简单示例。</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Markdown 中 Mermaid 图表完整指南&lt;/h1&gt;
&lt;p&gt;本文演示如何在 Markdown 文档中使用 Mermaid 创建各种复杂图表，包括流程图、时序图、甘特图、类图和状态图。&lt;/p&gt;
&lt;h2&gt;流程图示例&lt;/h2&gt;
&lt;p&gt;流程图非常适合表示流程或算法步骤。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;graph TD
    A[开始] --&amp;gt; B{条件检查}
    B --&amp;gt;|是| C[处理步骤 1]
    B --&amp;gt;|否| D[处理步骤 2]
    C --&amp;gt; E[子过程]
    D --&amp;gt; E
    subgraph E [子过程详情]
        E1[子步骤 1] --&amp;gt; E2[子步骤 2]
        E2 --&amp;gt; E3[子步骤 3]
    end
    E --&amp;gt; F{另一个决策}
    F --&amp;gt;|选项 1| G[结果 1]
    F --&amp;gt;|选项 2| H[结果 2]
    F --&amp;gt;|选项 3| I[结果 3]
    G --&amp;gt; J[结束]
    H --&amp;gt; J
    I --&amp;gt; J
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;时序图示例&lt;/h2&gt;
&lt;p&gt;时序图显示对象之间随时间的交互。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    participant User as 用户
    participant WebApp as 网页应用
    participant Server as 服务器
    participant Database as 数据库

    User-&amp;gt;&amp;gt;WebApp: 提交登录请求
    WebApp-&amp;gt;&amp;gt;Server: 发送认证请求
    Server-&amp;gt;&amp;gt;Database: 查询用户凭据
    Database--&amp;gt;&amp;gt;Server: 返回用户数据
    Server--&amp;gt;&amp;gt;WebApp: 返回认证结果
    
    alt 认证成功
        WebApp-&amp;gt;&amp;gt;User: 显示欢迎页面
        WebApp-&amp;gt;&amp;gt;Server: 请求用户数据
        Server-&amp;gt;&amp;gt;Database: 获取用户偏好
        Database--&amp;gt;&amp;gt;Server: 返回偏好设置
        Server--&amp;gt;&amp;gt;WebApp: 返回用户数据
        WebApp-&amp;gt;&amp;gt;User: 加载个性化界面
    else 认证失败
        WebApp-&amp;gt;&amp;gt;User: 显示错误消息
        WebApp-&amp;gt;&amp;gt;User: 提示重新输入
    end
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;甘特图示例&lt;/h2&gt;
&lt;p&gt;甘特图非常适合显示项目进度和时间线。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gantt
    title 网站开发项目时间线
    dateFormat  YYYY-MM-DD
    axisFormat  %m/%d
    
    section 设计阶段
    需求分析      :a1, 2023-10-01, 7d
    UI设计                 :a2, after a1, 10d
    原型创建        :a3, after a2, 5d
    
    section 开发阶段
    前端开发      :b1, 2023-10-20, 15d
    后端开发       :b2, after a2, 18d
    数据库设计           :b3, after a1, 12d
    
    section 测试阶段
    单元测试              :c1, after b1, 8d
    集成测试       :c2, after b2, 10d
    用户验收测试   :c3, after c2, 7d
    
    section 部署
    生产环境部署     :d1, after c3, 3d
    发布                    :milestone, after d1, 0d
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;类图示例&lt;/h2&gt;
&lt;p&gt;类图显示系统的静态结构，包括类、属性、方法及其关系。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;classDiagram
    class User {
        +String username
        +String password
        +String email
        +Boolean active
        +login()
        +logout()
        +updateProfile()
    }
    
    class Article {
        +String title
        +String content
        +Date publishDate
        +Boolean published
        +publish()
        +edit()
        +delete()
    }
    
    class Comment {
        +String content
        +Date commentDate
        +addComment()
        +deleteComment()
    }
    
    class Category {
        +String name
        +String description
        +addArticle()
        +removeArticle()
    }
    
    User &quot;1&quot; -- &quot;*&quot; Article : 写作
    User &quot;1&quot; -- &quot;*&quot; Comment : 发表
    Article &quot;1&quot; -- &quot;*&quot; Comment : 拥有
    Article &quot;1&quot; -- &quot;*&quot; Category : 属于
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;状态图示例&lt;/h2&gt;
&lt;p&gt;状态图显示对象在其生命周期中经历的状态序列。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;stateDiagram-v2
    [*] --&amp;gt; 草稿
    
    草稿 --&amp;gt; 审核中 : 提交
    审核中 --&amp;gt; 草稿 : 拒绝
    审核中 --&amp;gt; 已批准 : 批准
    已批准 --&amp;gt; 已发布 : 发布
    已发布 --&amp;gt; 已归档 : 归档
    已发布 --&amp;gt; 草稿 : 撤回
    
    state 已发布 {
        [*] --&amp;gt; 活跃
        活跃 --&amp;gt; 隐藏 : 临时隐藏
        隐藏 --&amp;gt; 活跃 : 恢复
        活跃 --&amp;gt; [*]
        隐藏 --&amp;gt; [*]
    }
    
    已归档 --&amp;gt; [*]
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;饼图示例&lt;/h2&gt;
&lt;p&gt;饼图非常适合显示比例和百分比数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pie title 网站流量来源分析
    &quot;搜索引擎&quot; : 45.6
    &quot;直接访问&quot; : 30.1
    &quot;社交媒体&quot; : 15.3
    &quot;推荐链接&quot; : 6.4
    &quot;其他来源&quot; : 2.6
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;总结&lt;/h2&gt;
&lt;p&gt;Mermaid 是在 Markdown 文档中创建各种类型图表的强大工具。本文演示了如何使用流程图、时序图、甘特图、类图、状态图和饼图。这些图表可以帮助您更清晰地表达复杂的概念、流程和数据结构。&lt;/p&gt;
&lt;p&gt;要使用 Mermaid，只需在代码块中指定 mermaid 语言，并使用简洁的文本语法描述图表。Mermaid 会自动将这些描述转换为美观的可视化图表。&lt;/p&gt;
&lt;p&gt;尝试在您的下一篇技术博客文章或项目文档中使用 Mermaid 图表 - 它们将使您的内容更加专业且更易理解！&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>在文章中嵌入视频</title><link>https://chaosnap.github.io/MyBlog/posts/video/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/video/</guid><description>这篇文章演示如何在博客文章中嵌入视频。</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;只需从 YouTube 或其他平台复制嵌入代码，然后将其粘贴到 markdown 文件中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;---
title: 在文章中嵌入视频
published: 2023-10-19
// ...
---

&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;YouTube&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;https://www.youtube.com/embed/5gIf0_xpFPI?si=N1WTorLKL0uwLsU_&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&amp;gt;&amp;lt;/iframe&amp;gt;&lt;/p&gt;
&lt;h2&gt;Bilibili&lt;/h2&gt;
&lt;p&gt;&amp;lt;iframe width=&quot;100%&quot; height=&quot;468&quot; src=&quot;//player.bilibili.com/player.html?bvid=BV1fK4y1s7Qf&amp;amp;p=1&amp;amp;autoplay=0&quot; scrolling=&quot;no&quot; border=&quot;0&quot; frameborder=&quot;no&quot; framespacing=&quot;0&quot; allowfullscreen=&quot;true&quot; &amp;amp;autoplay=0&amp;gt; &amp;lt;/iframe&amp;gt;&lt;/p&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Markdown 扩展功能</title><link>https://chaosnap.github.io/MyBlog/posts/markdown-extended/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/markdown-extended/</guid><description>了解Markdown 功能</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;GitHub 仓库卡片&lt;/h2&gt;
&lt;p&gt;您可以添加链接到 GitHub 仓库的动态卡片，在页面加载时，仓库信息会从 GitHub API 获取。&lt;/p&gt;
&lt;p&gt;::github{repo=&quot;CuteLeaf/Firefly&quot;}&lt;/p&gt;
&lt;p&gt;使用代码 &lt;code&gt;::github{repo=&quot;CuteLeaf/Firefly&quot;}&lt;/code&gt; 创建 GitHub 仓库卡片。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;::github{repo=&quot;CuteLeaf/Firefly&quot;}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;提醒框&lt;/h2&gt;
&lt;p&gt;支持以下类型的提醒框：&lt;code&gt;note&lt;/code&gt; &lt;code&gt;tip&lt;/code&gt; &lt;code&gt;important&lt;/code&gt; &lt;code&gt;warning&lt;/code&gt; &lt;code&gt;caution&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;:::note
突出显示用户应该考虑的信息，即使在快速浏览时也是如此。
:::&lt;/p&gt;
&lt;p&gt;:::tip
可选信息，帮助用户更成功。
:::&lt;/p&gt;
&lt;p&gt;:::important
用户成功所必需的关键信息。
:::&lt;/p&gt;
&lt;p&gt;:::warning
由于潜在风险需要用户立即注意的关键内容。
:::&lt;/p&gt;
&lt;p&gt;:::caution
行动的负面潜在后果。
:::&lt;/p&gt;
&lt;h3&gt;基本语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;:::note
突出显示用户应该考虑的信息，即使在快速浏览时也是如此。
:::

:::tip
可选信息，帮助用户更成功。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;自定义标题&lt;/h3&gt;
&lt;p&gt;可以自定义提醒框的标题。&lt;/p&gt;
&lt;p&gt;:::note[我的自定义标题]
这是一个带有自定义标题的注释。
:::&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;:::note[我的自定义标题]
这是一个带有自定义标题的注释。
:::
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GitHub 语法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;[!TIP]
也支持 &lt;a href=&quot;https://github.com/orgs/community/discussions/16925&quot;&gt;GitHub 语法&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; [!NOTE]
&amp;gt; 也支持 GitHub 语法。

&amp;gt; [!TIP]
&amp;gt; 也支持 GitHub 语法。
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;剧透&lt;/h3&gt;
&lt;p&gt;您可以为文本添加剧透。文本也支持 &lt;strong&gt;Markdown&lt;/strong&gt; 语法。&lt;/p&gt;
&lt;p&gt;内容 :spoiler[被隐藏了 &lt;strong&gt;哈哈&lt;/strong&gt;]！&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;内容 :spoiler[被隐藏了 **哈哈**]！&lt;/code&gt;&lt;/pre&gt;
</content:encoded><author>GuanHua Yu</author></item><item><title>Markdown 教程</title><link>https://chaosnap.github.io/MyBlog/posts/markdown-tutorial/</link><guid isPermaLink="true">https://chaosnap.github.io/MyBlog/posts/markdown-tutorial/</guid><description>一个简明的 Markdown 博客示例。</description><pubDate>Wed, 01 Jan 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一个展示如何编写 Markdown 文件的示例。本文档汇总了核心语法与常见扩展（GFM）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#block-elements&quot;&gt;块级元素&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#paragraphs-and-line-breaks&quot;&gt;段落与换行&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#headers&quot;&gt;标题&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#blockquotes&quot;&gt;引用&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#lists&quot;&gt;列表&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#code-blocks&quot;&gt;代码块&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#horizontal-rules&quot;&gt;分割线&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#table&quot;&gt;表格&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#span-elements&quot;&gt;内联元素&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#links&quot;&gt;链接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#emphasis&quot;&gt;强调&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#code&quot;&gt;行内代码&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#images&quot;&gt;图片&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#strikethrough&quot;&gt;删除线&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#miscellaneous&quot;&gt;杂项&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;#automatic-links&quot;&gt;自动链接&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#backslash-escapes&quot;&gt;反斜杠转义&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;#inline-html&quot;&gt;内联 HTML&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;a id=&quot;block-elements&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;块级元素&lt;/h2&gt;
&lt;p&gt;&amp;lt;a id=&quot;paragraphs-and-line-breaks&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;段落与换行&lt;/h3&gt;
&lt;h4&gt;段落&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用一个或多个空行分隔段落。（仅包含&lt;strong&gt;空格&lt;/strong&gt;或&lt;strong&gt;制表符&lt;/strong&gt;的行也视为空行。）&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This will be
inline.

This is second paragraph.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This will be
inline.&lt;/p&gt;
&lt;p&gt;This is second paragraph.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;换行&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;br /&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在行末添加&lt;strong&gt;两个或更多空格&lt;/strong&gt;来产生换行。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This will be not
inline.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This will be not&lt;br /&gt;
inline.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;headers&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;标题&lt;/h3&gt;
&lt;p&gt;Markdown 支持两种标题样式：Setext 与 atx。&lt;/p&gt;
&lt;h4&gt;Setext&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;使用&lt;strong&gt;等号 (=)&lt;/strong&gt; 表示 &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;、使用&lt;strong&gt;短横线 (-)&lt;/strong&gt; 表示 &lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;，数量不限，作为“下划线”。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is an H1
=============
This is an H2
-------------
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;This is an H1&lt;/h1&gt;
&lt;h2&gt;This is an H2&lt;/h2&gt;
&lt;hr /&gt;
&lt;h4&gt;atx&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h2&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h3&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h4&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h5&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在行首使用 1-6 个&lt;strong&gt;井号 (#)&lt;/strong&gt;，对应 &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; 至 &lt;code&gt;&amp;lt;h6&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This is an H1
## This is an H2
###### This is an H6
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;This is an H1&lt;/h1&gt;
&lt;h2&gt;This is an H2&lt;/h2&gt;
&lt;h6&gt;This is an H6&lt;/h6&gt;
&lt;hr /&gt;
&lt;p&gt;可选：你可以在行尾“闭合” atx 标题。末尾的井号数量&lt;strong&gt;不必与&lt;/strong&gt;开头一致。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# This is an H1 #
## This is an H2 ##
### This is an H3 ######
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;This is an H1&lt;/h1&gt;
&lt;h2&gt;This is an H2&lt;/h2&gt;
&lt;h3&gt;This is an H3&lt;/h3&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;blockquotes&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;引用&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;blockquote&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Markdown 使用邮件风格的 &lt;strong&gt;&amp;gt;&lt;/strong&gt; 作为引用符号。若手动换行并在每行前加 &amp;gt;，显示效果最佳。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
&amp;gt; consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
&amp;gt; Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
&amp;gt;
&amp;gt; Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
&amp;gt; id sem consectetuer libero luctus adipiscing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.&lt;/p&gt;
&lt;p&gt;Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;Markdown 允许“偷懒”：在一个硬换行段落中，只在第一行前加 &amp;gt; 即可。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

&amp;gt; Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;引用可以嵌套（引用中的引用），通过增加 &amp;gt; 层级实现。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; This is the first level of quoting.
&amp;gt;
&amp;gt; &amp;gt; This is nested blockquote.
&amp;gt;
&amp;gt; Back to the first level.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;This is the first level of quoting.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is nested blockquote.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Back to the first level.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;引用内可包含其他 Markdown 元素，包括标题、列表与代码块。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;gt; ## This is a header.
&amp;gt;
&amp;gt; 1.   This is the first list item.
&amp;gt; 2.   This is the second list item.
&amp;gt;
&amp;gt; Here&apos;s some example code:
&amp;gt;
&amp;gt;     return shell_exec(&quot;echo $input | $markdown_script&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;h2&gt;This is a header.&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;This is the first list item.&lt;/li&gt;
&lt;li&gt;This is the second list item.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here&apos;s some example code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;return shell_exec(&quot;echo $input | $markdown_script&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;lists&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;列表&lt;/h3&gt;
&lt;p&gt;Markdown 支持有序（数字）与无序（圆点）列表。&lt;/p&gt;
&lt;h4&gt;无序列表&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;无序列表可使用 &lt;strong&gt;星号 (*)&lt;/strong&gt;、&lt;strong&gt;加号 (+)&lt;/strong&gt; 或 &lt;strong&gt;短横线 (-)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*   Red
*   Green
*   Blue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;Red&lt;/li&gt;
&lt;li&gt;Green&lt;/li&gt;
&lt;li&gt;Blue&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;等价于：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+   Red
+   Green
+   Blue
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或者：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-   Red
-   Green
-   Blue
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;有序列表&lt;/h4&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;ol&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;有序列表使用数字加英文句点：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.  Bird
2.  McHale
3.  Parish
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;Bird&lt;/li&gt;
&lt;li&gt;McHale&lt;/li&gt;
&lt;li&gt;Parish&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;注意：像下面这样可能会“意外触发”有序列表：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1986. What a great season.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ol&gt;
&lt;li&gt;What a great season.&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;p&gt;你可以用&lt;strong&gt;反斜杠转义 (\)&lt;/strong&gt; 句点：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1986\. What a great season.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;1986. What a great season.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;列表中的缩进内容&lt;/h4&gt;
&lt;h5&gt;列表项里的引用&lt;/h5&gt;
&lt;p&gt;在列表项内放置引用，需要将 &amp;gt; 符号整体缩进：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*   A list item with a blockquote:

    &amp;gt; This is a blockquote
    &amp;gt; inside a list item.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A list item with a blockquote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a blockquote
inside a list item.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;列表项里的代码块&lt;/h5&gt;
&lt;p&gt;在列表项内放置代码块，需要缩进两层——&lt;strong&gt;8 个空格&lt;/strong&gt;或&lt;strong&gt;两个 Tab&lt;/strong&gt;：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*   A list item with a code block:

        &amp;lt;code goes here&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A list item with a code block:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;code goes here&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h5&gt;嵌套列表&lt;/h5&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* A
  * A1
  * A2
* B
* C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;A
&lt;ul&gt;
&lt;li&gt;A1&lt;/li&gt;
&lt;li&gt;A2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;B&lt;/li&gt;
&lt;li&gt;C&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;code-blocks&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;代码块&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;pre&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;将代码块中的每行缩进至少&lt;strong&gt;4 个空格&lt;/strong&gt;或&lt;strong&gt;1 个制表符&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is a normal paragraph:

    This is a code block.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This is a normal paragraph:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is a code block.
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;代码块会一直持续，直到遇到未缩进的行（或文末）。&lt;/p&gt;
&lt;p&gt;在代码块内，&lt;strong&gt;与号 (&amp;amp;)&lt;/strong&gt; 和尖括号 &lt;strong&gt;(&amp;lt; &amp;gt;)&lt;/strong&gt; 会自动转为 HTML 实体。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    &amp;lt;div class=&quot;footer&quot;&amp;gt;
        &amp;amp;copy; 2004 Foo Corporation
    &amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;div class=&quot;footer&quot;&amp;gt;
    &amp;amp;copy; 2004 Foo Corporation
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;下文的“围栏代码块”和“语法高亮”属于扩展语法，你也可以用它们来书写代码块。&lt;/p&gt;
&lt;h4&gt;围栏代码块&lt;/h4&gt;
&lt;p&gt;使用成对的反引号围起来（如下所示），就不需要四空格缩进了。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Here&apos;s an example:

```
function test() {
  console.log(&quot;notice the blank line before this function?&quot;);
}
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Here&apos;s an example:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;function test() {
  console.log(&quot;notice the blank line before this function?&quot;);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h4&gt;语法高亮&lt;/h4&gt;
&lt;p&gt;在围栏代码块后添加可选的语言标识，即可启用语法高亮（参见支持语言列表）。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;```ruby
require &apos;redcarpet&apos;
markdown = Redcarpet.new(&quot;Hello World!&quot;)
puts markdown.to_html
```
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;pre&gt;&lt;code&gt;require &apos;redcarpet&apos;
markdown = Redcarpet.new(&quot;Hello World!&quot;)
puts markdown.to_html
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;horizontal-rules&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;分割线（水平线）&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;hr /&amp;gt;&lt;/code&gt;
一行中放置&lt;strong&gt;三个或以上的短横线 (-)、星号 (*) 或下划线 (_)&lt;/strong&gt;。符号之间允许有空格。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* * *
***
*****
- - -
---------------------------------------
___
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;table&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;表格&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这是扩展语法。&lt;/p&gt;
&lt;p&gt;用&lt;strong&gt;竖线 (|)&lt;/strong&gt; 分隔列，用&lt;strong&gt;短横线 (-)&lt;/strong&gt; 分隔表头，使用&lt;strong&gt;冒号 (:)&lt;/strong&gt; 指定对齐方式。&lt;/p&gt;
&lt;p&gt;两侧的&lt;strong&gt;竖线 (|)&lt;/strong&gt; 与对齐可选。用于表头分隔时，每列至少需要 &lt;strong&gt;3 个短横线&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;| Left | Center | Right |
|:-----|:------:|------:|
|aaa   |bbb     |ccc    |
|ddd   |eee     |fff    |

 A | B
---|---
123|456


A |B
--|--
12|45
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Left&lt;/th&gt;
&lt;th&gt;Center&lt;/th&gt;
&lt;th&gt;Right&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;aaa&lt;/td&gt;
&lt;td&gt;bbb&lt;/td&gt;
&lt;td&gt;ccc&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ddd&lt;/td&gt;
&lt;td&gt;eee&lt;/td&gt;
&lt;td&gt;fff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;123&lt;/td&gt;
&lt;td&gt;456&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;A&lt;/th&gt;
&lt;th&gt;B&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;span-elements&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;内联元素&lt;/h2&gt;
&lt;p&gt;&amp;lt;a id=&quot;links&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;链接&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Markdown 支持两种链接样式：行内链接与引用式链接。&lt;/p&gt;
&lt;h4&gt;行内链接&lt;/h4&gt;
&lt;p&gt;行内链接格式：&lt;code&gt;[文本](URL &quot;标题&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;标题可选。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is [an example](http://example.com/ &quot;Title&quot;) inline link.

[This link](http://example.net/) has no title attribute.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This is &lt;a href=&quot;http://example.com/&quot;&gt;an example&lt;/a&gt; inline link.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;http://example.net/&quot;&gt;This link&lt;/a&gt; has no title attribute.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;如果引用同一站点的本地资源，可以使用相对路径：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;See my [About](/about/) page for details.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;See my &lt;a href=&quot;/about/&quot;&gt;About&lt;/a&gt; page for details.&lt;/p&gt;
&lt;hr /&gt;
&lt;h4&gt;引用式链接&lt;/h4&gt;
&lt;p&gt;可以预定义链接引用。定义格式：&lt;code&gt;[id]: URL &quot;标题&quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;标题同样可选。引用时使用：&lt;code&gt;[文本][id]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[id]: http://example.com/  &quot;Optional Title Here&quot;
This is [an example][id] reference-style link.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This is &lt;a href=&quot;http://example.com/&quot;&gt;an example&lt;/a&gt; reference-style link.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方括号中包含链接标识（&lt;strong&gt;不区分大小写&lt;/strong&gt;，可在左侧缩进最多三格空格）；&lt;/li&gt;
&lt;li&gt;随后是冒号；&lt;/li&gt;
&lt;li&gt;再跟一个或多个空格（或 tab）；&lt;/li&gt;
&lt;li&gt;然后是链接 URL；&lt;/li&gt;
&lt;li&gt;URL 可选地用尖括号包裹；&lt;/li&gt;
&lt;li&gt;可选地跟随标题属性，用引号或圆括号包裹。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以下三种定义等价：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[foo]: http://example.com/  &quot;Optional Title Here&quot;
[foo]: http://example.com/  &apos;Optional Title Here&apos;
[foo]: http://example.com/  (Optional Title Here)
[foo]: &amp;lt;http://example.com/&amp;gt;  &quot;Optional Title Here&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果使用空的方括号，则链接文本本身会作为名称。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Google]: http://google.com/
[Google][]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;http://google.com/&quot;&gt;Google&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;emphasis&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;强调&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt;，&lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Markdown 使用 &lt;strong&gt;星号 (*)&lt;/strong&gt; 或 &lt;strong&gt;下划线 (_)&lt;/strong&gt; 表示强调。&lt;strong&gt;一个分隔符&lt;/strong&gt;对应 &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt;；&lt;strong&gt;两个分隔符&lt;/strong&gt;对应 &lt;code&gt;&amp;lt;strong&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;*single asterisks*

_single underscores_

**double asterisks**

__double underscores__
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;single asterisks&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;single underscores&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;double asterisks&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;double underscores&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;但如果两侧有空格，则会被视作普通字符而非强调语法。&lt;/p&gt;
&lt;p&gt;你可以使用反斜杠进行转义：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\*this text is surrounded by literal asterisks\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;*this text is surrounded by literal asterisks*&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;code&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;行内代码&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;code&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;用&lt;strong&gt;反引号 (`)&lt;/strong&gt; 包裹。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Use the `printf()` function.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Use the &lt;code&gt;printf()&lt;/code&gt; function.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;若行内代码中需要包含反引号字符，可使用&lt;strong&gt;多重反引号&lt;/strong&gt;作为定界符：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;``There is a literal backtick (`) here.``
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;code&gt;There is a literal backtick (`) here.&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;行内代码两侧的定界符允许包含空格（开头一个、结尾一个），方便在代码起始或结尾放置反引号字符：&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A single backtick in a code span: `` ` ``

A backtick-delimited string in a code span: `` `foo` ``
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;A single backtick in a code span: &lt;code&gt;`&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;A backtick-delimited string in a code span: &lt;code&gt;`foo`&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;images&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;图片&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;img /&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Markdown 的图片语法与链接类似，支持行内与引用两种方式。&lt;/p&gt;
&lt;h4&gt;行内图片&lt;/h4&gt;
&lt;p&gt;行内图片语法：&lt;code&gt;![替代文本](URL &quot;标题&quot;)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;标题可选。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;![Alt text](/path/to/img.jpg)

![Alt text](/path/to/img.jpg &quot;Optional title&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2024/08/20/5fszgXeOxmL3Wdv.webp&quot; alt=&quot;Alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2024/08/20/5fszgXeOxmL3Wdv.webp&quot; alt=&quot;Alt text&quot; title=&quot;Optional title&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;说明：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个感叹号 !；&lt;/li&gt;
&lt;li&gt;后接方括号，放置图片的替代文本；&lt;/li&gt;
&lt;li&gt;再接圆括号，内含图片 URL/路径，及可选的标题（引号包裹）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;引用式图片&lt;/h4&gt;
&lt;p&gt;引用式图片语法：&lt;code&gt;![替代文本][id]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[img id]: https://s2.loli.net/2024/08/20/5fszgXeOxmL3Wdv.webp  &quot;Optional title attribute&quot;
![Alt text][img id]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;img src=&quot;https://s2.loli.net/2024/08/20/5fszgXeOxmL3Wdv.webp&quot; alt=&quot;Alt text&quot; title=&quot;Optional title attribute&quot; /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;strikethrough&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;删除线&lt;/h3&gt;
&lt;p&gt;HTML 标签：&lt;code&gt;&amp;lt;del&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这是扩展语法。&lt;/p&gt;
&lt;p&gt;GFM 增加了删除线语法。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~~Mistaken text.~~
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;s&gt;Mistaken text.&lt;/s&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;miscellaneous&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;杂项&lt;/h2&gt;
&lt;p&gt;&amp;lt;a id=&quot;automatic-links&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;自动链接&lt;/h3&gt;
&lt;p&gt;Markdown 支持一种便捷写法来创建“自动链接”（URL 与邮箱地址）：只需用尖括号将其包住即可。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;http://example.com/&amp;gt;

&amp;lt;address@example.com&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a href=&quot;http://example.com/&quot;&gt;http://example.com/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;mailto:address@example.com&quot;&gt;address@example.com&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;GFM 会自动识别标准 URL 并转换为链接。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;https://github.com/emn178/markdown
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;https://github.com/emn178/markdown&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;a id=&quot;backslash-escapes&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h3&gt;反斜杠转义&lt;/h3&gt;
&lt;p&gt;Markdown 允许使用反斜杠来转义那些本用于 Markdown 语法的特殊字符，使其按字面显示。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\*literal asterisks\*
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;*literal asterisks*&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;以下字符可通过反斜杠转义以按字面量输出：&lt;/p&gt;
&lt;p&gt;Code:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;\   backslash
`   backtick
*   asterisk
_   underscore
{}  curly braces
[]  square brackets
()  parentheses
#   hash mark
+   plus sign
-   minus sign (hyphen)
.   dot
!   exclamation mark
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;a id=&quot;inline-html&quot;&amp;gt;&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;h2&gt;内联 HTML&lt;/h2&gt;
&lt;p&gt;对于 Markdown 语法未覆盖的标记，直接使用原生 HTML 即可。无需特别声明从 Markdown 切换到 HTML，直接写标签就行。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;This is a regular paragraph.

&amp;lt;table&amp;gt;
    &amp;lt;tr&amp;gt;
        &amp;lt;td&amp;gt;Foo&amp;lt;/td&amp;gt;
    &amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;

This is another regular paragraph.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;This is a regular paragraph.&lt;/p&gt;
&lt;p&gt;&amp;lt;table&amp;gt;
&amp;lt;tr&amp;gt;
&amp;lt;td&amp;gt;Foo&amp;lt;/td&amp;gt;
&amp;lt;/tr&amp;gt;
&amp;lt;/table&amp;gt;&lt;/p&gt;
&lt;p&gt;This is another regular paragraph.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;请注意：在&lt;strong&gt;块级 HTML 标签&lt;/strong&gt;内不会处理 Markdown 语法。&lt;/p&gt;
&lt;p&gt;与块级标签不同，在&lt;strong&gt;行内级标签&lt;/strong&gt;内会处理 Markdown 语法。&lt;/p&gt;
&lt;p&gt;代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;span&amp;gt;**Work**&amp;lt;/span&amp;gt;

&amp;lt;div&amp;gt;
    **No Work**
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;预览：&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;lt;span&amp;gt;&lt;strong&gt;Work&lt;/strong&gt;&amp;lt;/span&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;div&amp;gt;
&lt;strong&gt;No Work&lt;/strong&gt;
&amp;lt;/div&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
</content:encoded><author>emn178</author></item></channel></rss>