<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Aki&apos;s Blog</title><description>电气工程 · 嵌入式 · 控制</description><link>https://arkmostima.pages.dev/</link><language>zh_CN</language><item><title>典型低速高转矩大功率多相PMSM电机弱磁控制设计思路</title><link>https://arkmostima.pages.dev/posts/%E5%85%B8%E5%9E%8B%E4%BD%8E%E9%80%9F%E9%AB%98%E8%BD%AC%E7%9F%A9%E5%A4%A7%E5%8A%9F%E7%8E%87%E5%A4%9A%E7%9B%B8pmsm%E7%94%B5%E6%9C%BA%E5%BC%B1%E7%A3%81%E6%8E%A7%E5%88%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%85%B8%E5%9E%8B%E4%BD%8E%E9%80%9F%E9%AB%98%E8%BD%AC%E7%9F%A9%E5%A4%A7%E5%8A%9F%E7%8E%87%E5%A4%9A%E7%9B%B8pmsm%E7%94%B5%E6%9C%BA%E5%BC%B1%E7%A3%81%E6%8E%A7%E5%88%B6%E8%AE%BE%E8%AE%A1%E6%80%9D%E8%B7%AF/</guid><description>交流电机一般需要变频调速，而变频器作为电机的驱动源，受制于其物理硬件能力，其最大输出电流和电压是由上限的，这也是弱磁控制存在的根本原因。</description><pubDate>Wed, 13 May 2026 10:37:43 GMT</pubDate><content:encoded>&lt;h1&gt;弱磁控制的来源&lt;/h1&gt;
&lt;p&gt;交流电机一般需要变频调速，而变频器作为电机的驱动源，受制于其物理硬件能力，其最大输出电流和电压是由上限的，这也是弱磁控制存在的根本原因。&lt;/p&gt;
&lt;h3&gt;1. 电流极限约束（Current Limit Constraint）&lt;/h3&gt;
&lt;p&gt;逆变器内部功率半导体器件的最大允许发热量，以及电机定子绕组的热容量，共同决定了系统能够输出的最大定子电流幅值 $I_{max}$。&lt;/p&gt;
&lt;p&gt;在 $d-q$ 轴坐标系中，定子电流矢量 $i_s$ 必须满足：
$$i_d^2 + i_q^2 \le I_{max}^2$$
在 $i_d-i_q$ 平面上，这是一个以原点 $(0,0)$ 为圆心，$I_{max}$ 为半径的圆，称为&lt;strong&gt;电流极限圆&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;2. 电压极限约束（Voltage Limit Constraint）&lt;/h3&gt;
&lt;p&gt;逆变器能够输出的最大交流相电压基波幅值 $U_{max}$ 受限于直流母线电压 $U_{dc}$。&lt;/p&gt;
&lt;p&gt;稳态下的 PMSM $d-q$ 轴电压方程如下。为了突出高转速下的主要矛盾，通常忽略微小的定子电阻压降 $R_s i_s$。此时稳态电压方程简化为：
$$u_d \approx -\omega_e L_q i_q$$
$$u_q \approx \omega_e L_d i_d + \omega_e \psi_f$$&lt;/p&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\omega_e$ 为转子电角速度。&lt;/li&gt;
&lt;li&gt;$L_d, L_q$ 为直轴与交轴电感。&lt;/li&gt;
&lt;li&gt;$\psi_f$ 为永磁体产生的转子磁链。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定子电压矢量 $u_s$ 必须满足逆变器的电压极限：
$$u_d^2 + u_q^2 \le U_{max}^2$$&lt;/p&gt;
&lt;p&gt;将近似的 $u_d$ 和 $u_q$ 代入上式，并移项整理，我们得到电机控制中最著名的&lt;strong&gt;电压极限椭圆方程&lt;/strong&gt;：
$$\frac{(L_d i_d + \psi_f)^2}{(U_{max}/\omega_e)^2} + \frac{(L_q i_q)^2}{(U_{max}/\omega_e)^2} \le 1$$&lt;/p&gt;
&lt;p&gt;通过变换，写成更直观的形式：
$$(i_d + \frac{\psi_f}{L_d})^2 + (\frac{L_q}{L_d} i_q)^2 \le (\frac{U_{max}}{\omega_e L_d})^2$$&lt;/p&gt;
&lt;p&gt;这是一个很重要的式子，可以看出：在 $i_d-i_q$ 平面上，这是一个中心点位于 $(-\frac{\psi_f}{L_d}, 0)$ 的椭圆。
当母线电压 $U_{max}$ 固定时，&lt;strong&gt;随着转速 $\omega_e$ 的不断升高，这个电压极限椭圆的面积会不断收缩。&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;弱磁控制的原理&lt;/h1&gt;
&lt;p&gt;当电机从静止开始加速时（低速区），电压椭圆非常大，足以完全包住电流极限圆。此时系统不受电压限制，我们可以自由寻找能产生最大转矩的电流组合（MTPA的来源）。&lt;/p&gt;
&lt;h3&gt;基速的定义（Base Speed）&lt;/h3&gt;
&lt;p&gt;随着转速 $\omega_e$ 增加，电压椭圆不断收缩。当椭圆收缩到刚好与额定最大电流 $I_{max}$ 下的 MTPA（最大转矩电流比）工作点相交时，此时的转速被称为&lt;strong&gt;基速（$\omega_{base}$）&lt;/strong&gt;。这是在不改变控制策略下，电机能达到最高转速的理论边界。如果继续提高转速，逆变器将饱和，无法提供电机所需要的电压（电流），转速就会表现为波动剧烈的振荡曲线，或是持续跌落。&lt;/p&gt;
&lt;h3&gt;突破基速：引入负向 $d$ 轴电流&lt;/h3&gt;
&lt;p&gt;当要求转速 $\omega_e &amp;gt; \omega_{base}$ 时，原先的工作点已经暴露在收缩后的电压椭圆之“外”。如果强行要求逆变器输出该电流，会引发数学上的&lt;strong&gt;电压饱和&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;如何让工作点重新回到这颗“越来越小的椭圆”内部？
重新审视电压约束方程的直轴项：$(L_d i_d + \psi_f)^2$。
因为永磁体磁链 $\psi_f$ 是固定的正值常数，&lt;strong&gt;唯一的数学解就是令 $i_d$ 为负值（$i_d &amp;lt; 0$）。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;通过注入负的 $i_d$，定子绕组产生的去磁磁场抵消了部分永磁体磁场，使得等效的合成磁链变小，进而压低了反电动势（Back-EMF）。这相当于&lt;strong&gt;人为地将工作点向左推入了收缩后的电压椭圆内部&lt;/strong&gt;，从而换取了转速的进一步提升。这就是“弱磁控制”的数学本质。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;运行轨迹控制论 (Control Trajectories)&lt;/h2&gt;
&lt;p&gt;基于上述数学约束，电机的全速域控制本质是在 $i_d-i_q$ 平面上求解约束条件下的最优化问题（最大化转矩 $T_e$）：
$$T_e = \frac{3}{2} p_n [\psi_f i_q + (L_d - L_q) i_d i_q]$$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;区域 I：恒转矩区（$\omega_e \le \omega_{base}$）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;约束&lt;/strong&gt;：仅受电流圆限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：沿 &lt;strong&gt;MTPA（Maximum Torque Per Ampere）&lt;/strong&gt; 轨迹运行，追求系统最高效率。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;区域 II：弱磁区（$\omega_e &amp;gt; \omega_{base}$）&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;约束&lt;/strong&gt;：同时受电流圆与电压椭圆限制。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;策略&lt;/strong&gt;：工作点沿着电流极限圆（$i_d^2 + i_q^2 = I_{max}^2$）向左上方滑动。此时 $i_d$ 不断变负，$i_q$ 被迫减小，输出转矩与转速成反比下降，也称恒功率区。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;基速与MTPA曲线计算&lt;/h1&gt;
&lt;p&gt;以某台低速大功率推进 PMSM 为例，其参数为：&lt;/p&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;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;&lt;strong&gt;电机类型&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;六相 IPMSM&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;双Y型（双星形），相移 $30^\circ$，中性点隔离&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;额定功率&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$P_N$&lt;/td&gt;
&lt;td&gt;$3500$&lt;/td&gt;
&lt;td&gt;$\text{kW}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;额定电压&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$U_N$&lt;/td&gt;
&lt;td&gt;$660$&lt;/td&gt;
&lt;td&gt;$\text{V}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;额定转速&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$N_N$&lt;/td&gt;
&lt;td&gt;$889$&lt;/td&gt;
&lt;td&gt;$\text{RPM}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;极对数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$p_n$&lt;/td&gt;
&lt;td&gt;$6$&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;定子单相电阻&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$R_s$&lt;/td&gt;
&lt;td&gt;$0.00094$&lt;/td&gt;
&lt;td&gt;$\Omega$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;直轴电感&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$L_d$&lt;/td&gt;
&lt;td&gt;$0.243$&lt;/td&gt;
&lt;td&gt;$\text{mH}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;交轴电感&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$L_q$&lt;/td&gt;
&lt;td&gt;$0.47$&lt;/td&gt;
&lt;td&gt;$\text{mH}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;永磁体磁链&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$\psi_f$&lt;/td&gt;
&lt;td&gt;$0.88$&lt;/td&gt;
&lt;td&gt;$\text{Wb}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;逆变器限流阀值&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$I_{max}$&lt;/td&gt;
&lt;td&gt;$2500$&lt;/td&gt;
&lt;td&gt;$\text{A}$&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;首先，计算其基速和 MTPA 曲线。首先，计算其基速和 MTPA 曲线：&lt;/p&gt;
&lt;p&gt;在进行计算前，我们需要明确该系统在物理上的两个极值约束：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;最大相电压峰值 ($U_{max}$)&lt;/strong&gt;：直流母线电压为 1000 V，则逆变器输出相电压峰值为 $ 1000 / sqrt(3) $，即 $577 V$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最大定子电流峰值 ($I_{max}$)&lt;/strong&gt;：满载、额定转速情况下电机每相电流峰值约为$2300A$，因此逆变器硬件限幅设为 $2500\text{A}$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;MTPA 轨迹方程&lt;/strong&gt;：
为了在给定的定子电流幅值 $I_s$ 下获得最大转矩，定子电流在 $d-q$ 轴的最优分配满足以下关系：
$$i_d = \frac{\psi_f}{4(L_q - L_d)} - \sqrt{ \left( \frac{\psi_f}{4(L_q - L_d)} \right)^2 + \frac{i_q^2}{2} }$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;理论基速方程&lt;/strong&gt;：
当在最大电流 $I_{max}$ 下沿 MTPA 轨迹运行，随着转速升高，电压极限椭圆收缩，直到椭圆边界与该电流点恰好相交时，此时的机械转速即为基速 $N_{base}$：
$$U_{max} = \omega_{base} \sqrt{ (L_d i_{d_max} + \psi_f)^2 + (L_q i_{q_max})^2 }$$
$$N_{base} = \frac{\omega_{base} \times 60}{2\pi p_n}$$&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;clear; clc; close all;

% === 舰船大功率六相 IPMSM 参数 ===
Pn = 6;               % 极对数
Ld = 0.243e-3;        % d轴电感 (H)
Lq = 0.47e-3;         % q轴电感 (H)
psif = 0.88;          % 永磁体磁链 (Wb)
U_max = 577;        % 逆变器最大输出相电压峰值 (V)
I_max = 2500;         % 系统最大允许电流峰值 (A)

% === 1. 生成 MTPA 曲线数据 ===
% 设定电流幅值范围 Is: 从 0 到 2500A，取 100 个点
Is_vector = linspace(0, I_max, 100); 
id_mtpa_table = zeros(1, 100);
iq_mtpa_table = zeros(1, 100);
Te_mtpa_table = zeros(1, 100); % 记录对应的电磁转矩，方便你观察

for k = 1:100
    Is = Is_vector(k);
    
    if Is == 0
        id_mtpa_table(k) = 0;
        iq_mtpa_table(k) = 0;
        Te_mtpa_table(k) = 0;
        continue;
    end
    
    % 根据解析公式计算最优的 id
    % id = psif/(4*(Lq-Ld)) - sqrt( (psif/(4*(Lq-Ld)))^2 + Is^2/2 )
    term1 = psif / (4 * (Lq - Ld));
    id_opt = term1 - sqrt( term1^2 + (Is^2)/2 );
    
    % 因为求出来的是纯数学解，需限制其不能超出当前圆的半径
    if id_opt &amp;lt; -Is
        id_opt = -Is;
    end
    
    % 计算对应的 iq (必然为正)
    iq_opt = sqrt(Is^2 - id_opt^2);
    
    id_mtpa_table(k) = id_opt;
    iq_mtpa_table(k) = iq_opt;
    
    % 计算在该电流下产生的总电磁转矩
    % 六相双星通常视为两个独立的三相绕组，此处按总转矩公式计算（根据你采用的变换矩阵，系数可能为 3或1.5，此处采用标准单套三相等效公式，若是双星并行需乘以2或依据VSD调整，这里先给出理论相对值）
    Te_mtpa_table(k) = 1.5 * Pn * (psif * iq_opt + (Ld - Lq) * id_opt * iq_opt); 
end

% --- 绘图用于论文展示 ---
figure;
plot(Is_vector, id_mtpa_table, &apos;b-&apos;, &apos;LineWidth&apos;, 2); hold on;
plot(Is_vector, iq_mtpa_table, &apos;r-&apos;, &apos;LineWidth&apos;, 2);
grid on;
xlabel(&apos;速度环输出电流指令 Is (A)&apos;);
ylabel(&apos;d-q 轴电流分量 (A)&apos;);
title(&apos;MTPA 轨迹曲线&apos;);
legend(&apos;i_d^* (MTPA)&apos;, &apos;i_q^* (MTPA)&apos;);

disp(&apos;✅ MTPA 表格已生成。变量: Is_vector, id_mtpa_table, iq_mtpa_table 准备完毕。&apos;);

% === 2. 计算基速 ===
% 基速是指在最大电流 I_max 下，电压顶到 U_max 时的转速
% 提取 I_max 下对应的 id 和 iq
id_max = id_mtpa_table(end);
iq_max = iq_mtpa_table(end);

% 利用电压椭圆方程求解电角速度 omega_base
% U_max^2 = (omega * Ld * id + omega * psif)^2 + (omega * Lq * iq)^2
omega_base = U_max / sqrt( (Ld * id_max + psif)^2 + (Lq * iq_max)^2 );

% 换算为机械转速 (RPM)
N_base = (omega_base * 60) / (2 * pi * Pn);

fprintf(&apos;\n=== 基速计算结果 ===\n&apos;);
fprintf(&apos;在极限电流 %d A 下对应的 MTPA 指令:\n&apos;, I_max);
fprintf(&apos;id = %.2f A, iq = %.2f A\n&apos;, id_max, iq_max);
fprintf(&apos;理论基准电角速度: %.2f rad/s\n&apos;, omega_base);
fprintf(&apos;理论基准机械转速: %.2f RPM\n&apos;, N_base);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计算得到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;=== 基速计算结果 ===
在极限电流 2500 A 下对应的 MTPA 指令:
id = -1046.84 A, iq = 2270.27 A
理论基准电角速度: 466.49 rad/s
理论基准机械转速: 742.43 RPM
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;控制器&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/Controller.webp&quot; alt=&quot;控制器结构图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;速度外环与动态限幅模块 (Speed Loop &amp;amp; Dynamic Saturation)&lt;/h3&gt;
&lt;p&gt;位于模型最左侧，是整个控制系统的大脑，决定了电机要输出多少动力。&lt;/p&gt;
&lt;h3&gt;核心部件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sum 模块&lt;/strong&gt;：计算目标转速 &lt;code&gt;wm*&lt;/code&gt; 与实际转速 &lt;code&gt;speed&lt;/code&gt; 的误差。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed Controller (PI 控制器)&lt;/strong&gt;：带有外部饱和上限（External Upper Limit）输入的 PI 调节器。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;动态限幅计算逻辑&lt;/strong&gt;：由平方模块（$u^2$）、加减模块（$2500^2 - u^2$）和开方模块（$\sqrt{u}$）组成。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理与数学逻辑&lt;/h3&gt;
&lt;p&gt;速度环 PI 的原始任务是根据转速误差输出总电流指令幅值 $I_s$（模型中线标为 &lt;code&gt;iq_ref&lt;/code&gt;）。
本模块最精妙的设计在于&lt;strong&gt;动态限幅&lt;/strong&gt;。它实时采集弱磁控制器最终输出的 $d$ 轴电流指令 $i_{d_ref}$（通过标签 &lt;code&gt;-T-&lt;/code&gt; 引入），并计算剩余的 $q$ 轴电流可用空间：
$$i_{q_limit} = \sqrt{I_{max}^2 - i_{d_ref}^2} = \sqrt{2500^2 - i_{d_ref}^2}$$
计算结果被送入 PI 控制器的上限限幅端口。下限则被固定在 &lt;code&gt;-2500&lt;/code&gt;（&lt;code&gt;iqref_lowlimit&lt;/code&gt;）。&lt;/p&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;消除资源争夺&lt;/strong&gt;：在深弱磁区，弱磁环需要极大的负 $i_d$ 来压制电压。如果速度环不知道 $i_d$ 已经占用了大量额度，仍按 $2500\text{A}$ 去要求 $i_q$，总电流就会突破物理极限，引发非线性震荡。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;防积分饱和 (Anti-windup)&lt;/strong&gt;：通过将 $i_{q_limit}$ 动态喂给 PI 控制器，一旦电压受限导致 $i_d$ 激增，速度环的 PI 积分器会立刻感知到上限的收缩并停止积分，彻底消除了满载工况下的“拉风箱”式失控震荡。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;MTPA 查表解耦模块 (MTPA Lookup Tables)&lt;/h2&gt;
&lt;p&gt;位于模型中左侧，负责将速度环请求的“总力气”最优地分配给 $d$ 轴和 $q$ 轴。&lt;/p&gt;
&lt;h3&gt;核心部件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1-D T(u) &lt;code&gt;MTPA - Id&lt;/code&gt;&lt;/strong&gt;：一维查表模块，输入总电流幅值，输出最优负向 $i_d$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1-D T(u) &lt;code&gt;MTPA - Iq&lt;/code&gt;&lt;/strong&gt;：一维查表模块，输入总电流幅值，输出最优正向 $i_q$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理与数学逻辑&lt;/h3&gt;
&lt;p&gt;IPMSM 具有磁阻转矩（因为 $L_q &amp;gt; L_d$）。为了在定子电流发热最小的前提下输出最大转矩，必须满足电磁转矩对方程求极值的约束：
$$i_d = \frac{\psi_f}{4(L_q - L_d)} - \sqrt{ \left( \frac{\psi_f}{4(L_q - L_d)} \right)^2 + \frac{I_s^2}{2} }$$
模型通过离线计算好这条数学曲线，将输入 $I_s$ 瞬间映射为一对 &lt;code&gt;[id, iq]&lt;/code&gt; 矢量，并打包成 &lt;code&gt;idqRef&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;执行效率极高&lt;/strong&gt;：在线计算根号和非线性方程会极大消耗 DSP 算力，查表法（LUT）在工业软件中最稳定、最快。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;榨干电机效能&lt;/strong&gt;：在基速以下运行时，该模块确保电机全程利用磁阻转矩，用最小的电流（最高效率）输出所需推力。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;弱磁控制器与代数环斩断 (Field-Weakening Controller &amp;amp; Algebraic Loop Break)&lt;/h2&gt;
&lt;p&gt;位于模型正中央，是系统突破转速物理天花板的核心枢纽。&lt;/p&gt;
&lt;h3&gt;核心部件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PMSM Field-Weakening Controller&lt;/strong&gt;：弱磁核心算法模块。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Constant &lt;code&gt;1000/sqrt(3)&lt;/code&gt;&lt;/strong&gt;：设定逆变器最大相电压阈值 $V_{phMax} \approx 577.3\text{V}$。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;1/z&lt;/code&gt; (Unit Delay)&lt;/strong&gt;：极其关键的单位延迟模块，串联在电压反馈通路 &lt;code&gt;[udq]&lt;/code&gt; 上。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理与数学逻辑&lt;/h3&gt;
&lt;p&gt;弱磁块就像一个“监工”。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在低速区&lt;/strong&gt;：反馈回来的电压矢量模长 $\sqrt{u_d^2 + u_q^2}$ 小于 $577.3\text{V}$。监工不干预，直接把 MTPA 传来的 &lt;code&gt;idqRef&lt;/code&gt; 原封不动地通过，输出为 &lt;code&gt;idqRefFW&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;在高速深弱磁区&lt;/strong&gt;：当反馈电压即将突破 $577.3\text{V}$ 时，弱磁模块内部的 PI 调节器开始介入（可以根据模块内部的 Modulation Index Threshold 设定），强制在 MTPA 的基础上叠加更深的负 $i_d$（即以磁克磁），同时压低 $i_q$ 以保护总电流不超过极限。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unit Delay 的救命作用&lt;/strong&gt;：电流环输出的 $u_{dq}$ 如果瞬间无延迟地直接反馈给弱磁环，会在 Simulink 中形成&lt;strong&gt;无代数解的死循环（代数环）&lt;/strong&gt;，导致电压波形不稳定。加了 &lt;code&gt;1/z&lt;/code&gt; 后，强制用上一控制周期的电压来推算当前的弱磁量，符合物理真实情况，使系统数值求解极其稳定。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;平滑过渡&lt;/strong&gt;：使得电机能够从 MTPA 区无缝滑入弱磁区，在电压撞墙时依然能保持高转速巡航。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;电流内环模块 (Current Inner Loop)&lt;/h2&gt;
&lt;p&gt;位于模型最右侧，负责精准、快速地执行经过 MTPA 和弱磁裁定后的最终电流指令。&lt;/p&gt;
&lt;h3&gt;核心部件&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Sum 模块&lt;/strong&gt;：分别计算 $d$ 轴和 $q$ 轴的电流跟踪误差（$i_{ref} - i_{actual}$）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;d-axis / q-axis Controller (PI 控制器)&lt;/strong&gt;：两个高带宽的 PI 调节器。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理与数学逻辑&lt;/h3&gt;
&lt;p&gt;这是标准的基于 $d-q$ 旋转坐标系的矢量控制（FOC）内环。PI 控制器根据电流误差，计算出应该施加在电机端部的交直轴电压 $u_d$ 和 $u_q$：
$$u_d = K_p (i_{d_ref} - i_d) + K_i \int (i_{d_ref} - i_d) dt$$
$$u_q = K_p (i_{q_ref} - i_q) + K_i \int (i_{q_ref} - i_q) dt$$
这两个电压信号打包为 &lt;code&gt;[udq]&lt;/code&gt;，随后将送入 SVPWM 调制模块生成脉冲驱动逆变器桥臂。&lt;/p&gt;
&lt;h3&gt;效果&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;消除电磁惯性&lt;/strong&gt;：电机绕组具有极大的电感特性，直接给电压很难控制电流。电流内环通过高增益闭环，强制实际定子电流紧紧“咬”住指令波形，是整个推进系统具备高动态响应（突加负载不飞车）的基础保障。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;验证&lt;/h1&gt;
&lt;p&gt;为了对电机进行测试，在simulink中进行了以下操作。仿真开始后，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 5 s 内施加转速给定值至额定转速 889 rpm&lt;/li&gt;
&lt;li&gt;在第 6 s 突加至 50 % 额定转矩&lt;/li&gt;
&lt;li&gt;在第 11 s 突加至 100 % 额定转矩（即另加 50 % 的额定转矩）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/rotorspeed.webp&quot; alt=&quot;转速响应&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/current.webp&quot; alt=&quot;定子六相电流&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/Vmag.webp&quot; alt=&quot;电压饱和监测（黄色为Vmag，蓝色为Vphmax）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/FW.webp&quot; alt=&quot;弱磁控制器输出信号（黄色为id，蓝色为iq）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/idrefid.webp&quot; alt=&quot;id跟踪信号（黄色为实际id，蓝色为MTPA下id）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/iqrefiq.webp&quot; alt=&quot;iq跟踪信号（黄色为实际iq，蓝色为MTPA下iq）&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/pmsm/speedlimit.webp&quot; alt=&quot;速度环限幅信号（上限值）&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>十二相同步整流发电机实用参数的理论推导</title><link>https://arkmostima.pages.dev/posts/%E5%8D%81%E4%BA%8C%E7%9B%B8%E5%90%8C%E6%AD%A5%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E5%AE%9E%E7%94%A8%E5%8F%82%E6%95%B0%E7%9A%84%E7%90%86%E8%AE%BA%E6%8E%A8%E5%AF%BC/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%8D%81%E4%BA%8C%E7%9B%B8%E5%90%8C%E6%AD%A5%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E5%AE%9E%E7%94%A8%E5%8F%82%E6%95%B0%E7%9A%84%E7%90%86%E8%AE%BA%E6%8E%A8%E5%AF%BC/</guid><description>摘要：针对十二相同步整流发电机（采用四套星形联结且空间互移 15° 的电枢绕组架构），本文系统地论证了从绕组级微观电磁参数向系统级宏观实用参数转换的数学模型。推导过程严格基于多绕组磁动势叠加原理与多回路等效电路理论，通过对直轴与交轴磁路…</description><pubDate>Tue, 28 Apr 2026 07:44:51 GMT</pubDate><content:encoded>&lt;h1&gt;十二相同步整流发电机微观参数至实用参数的严谨推导过程&lt;/h1&gt;
&lt;p&gt;&lt;strong&gt;摘要&lt;/strong&gt;：针对十二相同步整流发电机（采用四套星形联结且空间互移 15° 的电枢绕组架构），本文系统地论证了从绕组级微观电磁参数向系统级宏观实用参数转换的数学模型。推导过程严格基于多绕组磁动势叠加原理与多回路等效电路理论，通过对直轴与交轴磁路分支的深入分析，给出了同步电抗、暂态电抗、次暂态电抗及各阶时间常数的精确合成公式，并代入实机参数进行了闭环验证。本研究为多相整流发电机组的精确建模、稳定性分析及励磁控制系统的精细化整定提供了完备的理论依据。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;1. 引言&lt;/h2&gt;
&lt;p&gt;在舰船中压直流（MVDC）综合电力系统中，十二相同步整流发电机通过多脉波整流技术提供高质量直流电源。该电机的定子结构特殊，由四套三相对称绕组组成，各套绕组在空间上依次移位 15°。在状态空间建模中，常使用定转子各回路的微观参数；但在工程设计与出厂试验中，则需使用宏观实用参数来表征整机特性。&lt;/p&gt;
&lt;h2&gt;2. 参数定义与物理含义&lt;/h2&gt;
&lt;p&gt;在推导开始前，首先对涉及的所有参数进行详细说明：&lt;/p&gt;
&lt;h3&gt;2.1 微观参数 (Micro Parameters)&lt;/h3&gt;
&lt;p&gt;这些参数描述单套绕组或转子单一回路的电磁特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x_{dy}$：&lt;strong&gt;直轴定子绕组自感电抗&lt;/strong&gt;，指单套三相绕组在直轴方向产生的自感电抗。&lt;/li&gt;
&lt;li&gt;$x_{dm1}, x_{dm2}$：&lt;strong&gt;直轴定子绕组间互感电抗&lt;/strong&gt;。$x_{dm1}$ 为相邻绕组间（空间夹角较小）的互感，$x_{dm2}$ 为相对绕组间（空间夹角较大）的互感。&lt;/li&gt;
&lt;li&gt;$x_{afdy}$：&lt;strong&gt;直轴励磁与电枢反应电抗&lt;/strong&gt;，表征定子单套绕组与转子励磁绕组间的电磁耦合强度。&lt;/li&gt;
&lt;li&gt;$x_{fd}$：&lt;strong&gt;励磁绕组自感电抗&lt;/strong&gt;，转子励磁绕组自身的标幺值自电感。&lt;/li&gt;
&lt;li&gt;$x_{kd}$：&lt;strong&gt;直轴阻尼绕组电抗&lt;/strong&gt;，直轴阻尼绕组自身的标幺值自电感。&lt;/li&gt;
&lt;li&gt;$r_{fd}, r_{kd}$：分别代表&lt;strong&gt;励磁绕组电阻&lt;/strong&gt;与&lt;strong&gt;直轴阻尼绕组电阻&lt;/strong&gt;。&lt;/li&gt;
&lt;li&gt;$\omega_s$：&lt;strong&gt;额定电角速度&lt;/strong&gt;。由发电机额定频率 $f$ 决定，即 $\omega_s = 2\pi f$。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;2.2 实用参数 (Macro Parameters)&lt;/h3&gt;
&lt;p&gt;这些参数描述整机作为统一物理实体的外部动态与稳态特性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$x_d$：&lt;strong&gt;直轴同步电抗&lt;/strong&gt;，反映整机在稳态运行时的直轴总磁阻特性。&lt;/li&gt;
&lt;li&gt;$x_d&apos;$：&lt;strong&gt;直轴暂态电抗&lt;/strong&gt;，反映短路初瞬间励磁回路起反应后的等效阻抗。&lt;/li&gt;
&lt;li&gt;$x_d&apos;&apos;$：&lt;strong&gt;直轴次暂态电抗&lt;/strong&gt;，反映短路初瞬间阻尼回路起反应后的极短时等效阻抗。&lt;/li&gt;
&lt;li&gt;$T_d&apos;, T_d&apos;&apos;$：分别代表&lt;strong&gt;直轴暂态与次暂态时间常数&lt;/strong&gt;，表征不同回路阻尼使得电流衰减的快慢。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;3. 宏观同步电抗 $x_d$ 的合成推导&lt;/h2&gt;
&lt;p&gt;对于十二相发电机，当四套绕组同时通入对称电流时，整机的直轴总磁动势由四套绕组的磁动势叠加而成。&lt;/p&gt;
&lt;h3&gt;3.1 推导公式&lt;/h3&gt;
&lt;p&gt;宏观同步电抗 $x_d$ 由单套绕组的自感及三套与之存在空间耦合的互感线性叠加：
$$x_d = x_{dy} + 2x_{dm1} + x_{dm2} \tag{1}$$&lt;/p&gt;
&lt;h3&gt;3.2 数值验证&lt;/h3&gt;
&lt;p&gt;代入实际机组微观参数：
$x_{dy} = 0.4721$, $x_{dm1} = 0.3503$, $x_{dm2} = 0.3358$。
$$x_d = 0.4721 + 2 \times 0.3503 + 0.3358 = 1.5085$$
该理论合成值与实测出厂参数完全自洽，证明了微观互感参数精确刻画了四套绕组间的空间耦合。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;4. 暂态与次暂态电抗的合成推导&lt;/h2&gt;
&lt;p&gt;在暂态与次暂态过程中，转子侧闭合回路依据楞次定律产生感应电流，阻止主磁通突变，导致磁路在数学上呈现为各漏抗分支的并联模式。&lt;/p&gt;
&lt;p&gt;由于四套定子绕组共享同一个转子磁路，需首先计算整机对转子的&lt;strong&gt;总电枢反应电抗&lt;/strong&gt; $X_{ad}$：
$$X_{ad} = 4 \times x_{afdy} \tag{2}$$
相应的整机&lt;strong&gt;总定子漏抗&lt;/strong&gt; $X_{l\Sigma}$ 定义为同步电抗剔除电枢反应的部分：
$$X_{l\Sigma} = x_d - X_{ad} \tag{3}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入参数计算&lt;/strong&gt;：
已知 $x_{afdy} = 0.3404$。
$$X_{ad} = 4 \times 0.3404 = 1.3616$$
$$X_{l\Sigma} = 1.5085 - 1.3616 = 0.1469$$&lt;/p&gt;
&lt;h3&gt;4.1 直轴暂态电抗 $x_d&apos;$&lt;/h3&gt;
&lt;p&gt;在暂态阶段，忽略阻尼绕组衰减，励磁回路呈现反作用。定义折算至整机定子侧的&lt;strong&gt;励磁绕组宏观漏抗&lt;/strong&gt; $X_{f\sigma}$：
$$X_{f\sigma} = 4(x_{fd} - x_{afdy}) \tag{4}$$
根据主磁路与励磁漏磁路并联的原理，暂态电抗合成公式为：
$$x_d&apos; = X_{l\Sigma} + \frac{X_{ad} \cdot X_{f\sigma}}{X_{ad} + X_{f\sigma}} \tag{5}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入参数计算&lt;/strong&gt;：
已知 $x_{fd} = 0.3836$。
$$X_{f\sigma} = 4(0.3836 - 0.3404) = 0.1728$$
$$x_d&apos; = 0.1469 + \frac{1.3616 \times 0.1728}{1.3616 + 0.1728} = 0.1469 + 0.1533 = 0.3002$$
该计算值 $0.3002$ 与出厂实测暂态电抗 $0.30378$ 误差极小，验证了励磁回路等效降阶逻辑的准确性。&lt;/p&gt;
&lt;h3&gt;4.2 直轴次暂态电抗 $x_d&apos;&apos;$&lt;/h3&gt;
&lt;p&gt;在次暂态阶段的极短时间内，阻尼绕组强效介入。定义折算至整机侧的&lt;strong&gt;阻尼绕组宏观漏抗&lt;/strong&gt; $X_{kd\sigma}$：
$$X_{kd\sigma} = 4(x_{kd} - x_{afdy}) \tag{6}$$
此时等效阻抗为三路并联（主磁路、励磁支路、阻尼支路），合成公式为：
$$x_d&apos;&apos; = X_{l\Sigma} + \frac{1}{\frac{1}{X_{ad}} + \frac{1}{X_{f\sigma}} + \frac{1}{X_{kd\sigma}}} \tag{7}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入参数计算&lt;/strong&gt;：
已知 $x_{kd} = 0.3545$。
$$X_{kd\sigma} = 4(0.3545 - 0.3404) = 0.0564$$
$$x_d&apos;&apos; = 0.1469 + \frac{1}{\frac{1}{1.3616} + \frac{1}{0.1728} + \frac{1}{0.0564}} = 0.1469 + 0.0412 = 0.1881$$
该次暂态计算值 $0.1881$ 与出厂实测值 $0.18855$ 高度吻合（误差低于 0.3%），精确描绘了短路极初期的阻抗特性。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;5. 时间常数与短路电流特性&lt;/h2&gt;
&lt;p&gt;实用时间常数决定了短路电流暂态与次暂态包络线的衰减速率。&lt;/p&gt;
&lt;h3&gt;5.1 暂态时间常数 $T_d&apos;$&lt;/h3&gt;
&lt;p&gt;首先定义由微观参数构成的励磁回路开路时间常数 $T_{d0}&apos;$：
$$T_{d0}&apos; = \frac{x_{fd}}{\omega_s \cdot r_{fd}} \tag{8}$$
将其通过电抗比例折算至短路工况，得到实用暂态时间常数：
$$T_d&apos; = T_{d0}&apos; \cdot \frac{x_d&apos;}{x_d} \tag{9}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入参数计算&lt;/strong&gt;：
已知额定频率 $f = 100$ Hz，$\omega_s = 2\pi \times 100 = 628.32$ rad/s，励磁电阻 $r_{fd} = 0.00022$。
$$T_{d0}&apos; = \frac{0.3836}{628.32 \times 0.00022} = 2.775 \text{ s}$$
$$T_d&apos; = 2.775 \times \frac{0.3002}{1.5085} = 0.552 \text{ s}$$&lt;/p&gt;
&lt;h3&gt;5.2 次暂态时间常数 $T_d&apos;&apos;$&lt;/h3&gt;
&lt;p&gt;对于十二相系统，阻尼绕组衰减受定子四套绕组影响，其开路时间常数 $T_{d0}&apos;&apos;$ 与短路时间常数 $T_d&apos;&apos;$ 需引入等效折算系数：
$$T_{d0}&apos;&apos; = \frac{x_{kd} - \frac{x_{afdy}^2}{x_{fd}}}{\omega_s \cdot r_{kd}} \tag{10}$$
$$T_d&apos;&apos; = T_{d0}&apos;&apos; \cdot \frac{x_d&apos;&apos;}{x_d&apos;} \times 4 \tag{11}$$&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;代入参数计算&lt;/strong&gt;：
已知直轴阻尼电阻 $r_{kd} = 0.0024$。
$$T_{d0}&apos;&apos; = \frac{0.3545 - \frac{0.3404^2}{0.3836}}{628.32 \times 0.0024} = 0.0347 \text{ s}$$
$$T_d&apos;&apos; = 0.0347 \times \frac{0.1881}{0.3002} \times 4 = 0.0870 \text{ s}$$
该计算值 $0.0870\text{ s}$ 逼近出厂值 $0.0906\text{ s}$，从物理本质上揭示了该电机阻尼漏抗较大导致短路衰减偏慢的特性。&lt;/p&gt;
&lt;h3&gt;5.3 整流侧短路电流峰值的理论约束&lt;/h3&gt;
&lt;p&gt;发电机交流侧产生包含直流偏移量的最大短路相电流 $i_{\varphi\max}$。当这 12 相交流电流进入采用 4Y 整流四并方式输出的整流桥后，其直流侧短路电流峰值 $i_{dcp}$ 受整流换相压降和多相叠加规律约束。&lt;/p&gt;
&lt;p&gt;该短路电流瞬态叠加理论解析式为：
$$i_{dcp} = \frac{1 + \cos(7.5^\circ - \theta_0)}{2} i_{\varphi\max} \times 4 \frac{\sin(7.5^\circ \times 4)}{4 \sin 7.5^\circ} \tag{12}$$&lt;/p&gt;
&lt;p&gt;由于短路发生时刻具有随机性，短路电流峰值与初始相位角 $\theta_0$ 高度相关（$0 \le \theta_0 \le 15^\circ$）。当相位角 $\theta_0 = 7.5^\circ$ 时，系数达到最大极限值，即：
$$i_{dcp\max} = 3.831 \cdot i_{\varphi\max} \tag{13}$$&lt;/p&gt;
&lt;p&gt;这一理论关系进一步证明了：由于四套整流桥的换相阻隔与并联叠加错位，十二相整流系统直流侧的最终短路峰值并非交流子暂态极值的 4 倍线性叠加，而是受 $3.831$ 这一最大系数制约。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;6. 结论&lt;/h2&gt;
&lt;p&gt;本文完整推导了十二相同步整流发电机从微观绕组参数到宏观实用参数的严密映射模型。通过全参数代入运算，理论推导出的宏观指标（如 $x_d=1.5085$, $x_d&apos;=0.3002$, $x_d&apos;&apos;=0.1881$）与机组出厂试验数据实现了高精度的闭环对标。该解析架构不仅是搭建十二相发电机状态空间矩阵的基石，更为大容量整流发电机励磁系统（AVR）的鲁棒性设计与限流保护整定提供了不可或缺的理论参数依据。&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;参考文献&lt;/strong&gt;：
马伟明, 王东. 多相整流发电机及其系统的分析[M]. 杭州: 浙江大学出版社, 2020.&lt;/p&gt;
</content:encoded></item><item><title>十二相同步整流发电机模型推导</title><link>https://arkmostima.pages.dev/posts/%E7%AE%80%E5%8C%96%E5%8D%81%E4%BA%8C%E7%9B%B8%E5%90%8C%E6%AD%A5%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E6%A8%A1%E5%9E%8B%E6%8E%A8%E5%AF%BC/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%AE%80%E5%8C%96%E5%8D%81%E4%BA%8C%E7%9B%B8%E5%90%8C%E6%AD%A5%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E6%A8%A1%E5%9E%8B%E6%8E%A8%E5%AF%BC/</guid><description>十二相同步发电机在定转子分解方面的步骤和三相同步发电机基本一致。如果单独考虑与定子直轴和交轴绕组想独立的零轴绕组，则在考虑$d$、$q$、$f$、$D$、$Q$五个绕组的电磁过渡过程及转子机械过渡过程时，会面临高阶模型的求解难题。实际工…</description><pubDate>Tue, 03 Jun 2025 07:12:51 GMT</pubDate><content:encoded>&lt;h2&gt;引言&lt;/h2&gt;
&lt;p&gt;十二相同步发电机在定转子分解方面的步骤和三相同步发电机基本一致。如果单独考虑与定子直轴和交轴绕组想独立的零轴绕组，则在考虑$d$、$q$、$f$、$D$、$Q$五个绕组的电磁过渡过程及转子机械过渡过程时，会面临高阶模型的求解难题。实际工程中通常对同步电机的数学模型作不同程度的简化。&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;$u_d$&lt;/td&gt;
&lt;td&gt;定子d轴电压&lt;/td&gt;
&lt;td&gt;V（伏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$u_q$&lt;/td&gt;
&lt;td&gt;定子q轴电压&lt;/td&gt;
&lt;td&gt;V（伏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$i_d$&lt;/td&gt;
&lt;td&gt;定子d轴电流&lt;/td&gt;
&lt;td&gt;A（安）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$i_q$&lt;/td&gt;
&lt;td&gt;定子q轴电流&lt;/td&gt;
&lt;td&gt;A（安）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$r_a$&lt;/td&gt;
&lt;td&gt;定子各相绕组电阻&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_d$&lt;/td&gt;
&lt;td&gt;直轴同步电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_q$&lt;/td&gt;
&lt;td&gt;交轴同步电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_d&apos;$&lt;/td&gt;
&lt;td&gt;直轴暂态电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_{ad}$&lt;/td&gt;
&lt;td&gt;直轴电枢反应电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_{aq}$&lt;/td&gt;
&lt;td&gt;交轴电枢反应电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$X_f$&lt;/td&gt;
&lt;td&gt;励磁绕组自感电抗&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$r_f$&lt;/td&gt;
&lt;td&gt;励磁绕组电阻&lt;/td&gt;
&lt;td&gt;Ω（欧姆）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$E_q$&lt;/td&gt;
&lt;td&gt;交轴空载电动势（稳态）&lt;/td&gt;
&lt;td&gt;V（伏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$E_q&apos;$&lt;/td&gt;
&lt;td&gt;交轴暂态电动势（状态变量）&lt;/td&gt;
&lt;td&gt;V（伏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$E_f$&lt;/td&gt;
&lt;td&gt;等效励磁电动势（励磁系统输入）&lt;/td&gt;
&lt;td&gt;V（伏）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$T_{d0}&apos;$&lt;/td&gt;
&lt;td&gt;d轴开路暂态时间常数&lt;/td&gt;
&lt;td&gt;s（秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$T_J$&lt;/td&gt;
&lt;td&gt;转子转动惯量对应时间常数&lt;/td&gt;
&lt;td&gt;s（秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$T_m$&lt;/td&gt;
&lt;td&gt;原动机提供的机械转矩&lt;/td&gt;
&lt;td&gt;Nm（牛·米）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$T_e$&lt;/td&gt;
&lt;td&gt;发电机电磁转矩&lt;/td&gt;
&lt;td&gt;Nm（牛·米）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\omega$&lt;/td&gt;
&lt;td&gt;电机相对同步角速度（机械量）&lt;/td&gt;
&lt;td&gt;p.u.（标幺值）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\delta$&lt;/td&gt;
&lt;td&gt;电机转子与同步旋转坐标系的夹角&lt;/td&gt;
&lt;td&gt;rad（弧度）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\Phi_d$&lt;/td&gt;
&lt;td&gt;d轴磁链&lt;/td&gt;
&lt;td&gt;Wb（韦伯）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\Phi_q$&lt;/td&gt;
&lt;td&gt;q轴磁链&lt;/td&gt;
&lt;td&gt;Wb（韦伯）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$\Phi_f$&lt;/td&gt;
&lt;td&gt;励磁绕组磁链&lt;/td&gt;
&lt;td&gt;Wb（韦伯）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$p$&lt;/td&gt;
&lt;td&gt;微分算符（$p = \frac{d}{dt}$）&lt;/td&gt;
&lt;td&gt;s⁻¹（每秒）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;前置条件&lt;/h2&gt;
&lt;p&gt;假定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;忽略定子直轴、交轴绕组的暂态过程，即定子电压方程中取$p\Phi_d=p\Phi_q=0$&lt;/li&gt;
&lt;li&gt;在定子电压方程中取$\omega \approx 1(p.u.)$。通常在速度变化不大的过渡过程中，该近似引起的误差较小&lt;/li&gt;
&lt;li&gt;忽略$D$、$Q$和等效交轴稳定绕组，其作用可在转子运动方程补入阻尼项来近似&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;推导&lt;/h2&gt;
&lt;h3&gt;引入实用变量&lt;/h3&gt;
&lt;p&gt;为了消去转子励磁绕组的变量$i_f$、$u_f$和$\Phi_f$，引入以下三个定子侧等效实用变量：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;定子励磁电动势$E_f$：
$$
E_f = X_{ad}\frac{u_f}{r_f}
$$&lt;/li&gt;
&lt;li&gt;交轴空载电动势$E_q$
$$
E_q = X_{ad}i_f
$$&lt;/li&gt;
&lt;li&gt;交轴瞬变电动势$E_q&apos;$
$$
E_q&apos; = \frac{X_{ad}}{X_f}\Phi_f
$$
也就是说$E_f$、$E_q$、$E_q&apos;$分别对应$u_f$、$i_f$、$\Phi_f$。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;稳态时，有$i_f = u_f / r_f$，所以(下标0表示稳态值):
$$
E_{f0} = E_{q0}
$$&lt;/p&gt;
&lt;p&gt;由Park方程，稳态时
$$
u_{q0} = E_{q0} - X_d i_{d0} - r_a i_{q0}
$$
从而
$$
E_{f0} = E_{q0} = u_{q0} + X_d i_{d0} + r_a i_{q0}
$$
根据上式可由稳态定子电量计算$E_{f0}$ 和 $E_{q0}$。&lt;/p&gt;
&lt;p&gt;根据$E_q&apos; = \frac{X_{ad}}{X_f}\Phi_f$，电机交轴瞬变电动势$E_q&apos;$和励磁绕组磁链成正比，由于暂态过程中磁链无法突变，假设扰动发生在$t=0$，则
$$
E_q&apos;|&lt;em&gt;{t=0} = E_q&apos;|&lt;/em&gt;{t=0&apos;}
$$
可根据稳态值来确定$E_q&apos;$在扰动发生时的初值，而由Park方程可知
$$
\Phi_{f0} = -X_{ad}i_{d0} + X_f i_{f0}
$$
等式两侧同乘$X_{ad}/X_f$，可得
$$
E_{q0}&apos; = \frac{X_{ad}}{X_f}\Phi_{f0} = - \frac{X_{ad}^2}{X_f}i_{d0} + X_{ad}i_{f0}
$$
根据$E_{f0} = E_{q0} = u_{q0} + X_d i_{d0} + r_a i_{q0}$，有
$$
X_{ad}i_{f0} = E_{q0} = u_{q0} + X_di_{d0} + r_a i_{q0}
$$
因为
$$
X_d&apos; = X_d - \frac{X_{ad}^2}{X_f}
$$
代入$E_{q0}&apos; = \frac{X_{ad}}{X_f}\Phi_{f0} = - \frac{X_{ad}^2}{X_f}i_{d0} + X_{ad}i_{f0}$，可得
$$
E_{q0}&apos; = u_{q0} + X_d&apos;i_{d0}+r_ai_{q0}
$$
该式可用于计算$E_q&apos;$在扰动时的初值，$E_{q0}&apos;$反映了瞬变初的交轴暂态电动势。&lt;/p&gt;
&lt;h3&gt;导出思路&lt;/h3&gt;
&lt;p&gt;思路如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对于Park方程，在忽略$D$、$Q$绕组（将相应方程及变量删去）后，尚有以下变量：$u_{dqf}$、$i_{dqf}$、$\Phi_{dqf}$及$\omega$、$\delta$、$T_m$。假设$u_f$和$T_m$为已知量（分别为励磁系统及原动机输出量），则有10个未知量。对应有直轴、交轴和励磁3个绕组的电压方程、磁链方程和2个转子运动方程，共计8个方程。若和直轴、交轴网络方程联立，则变量数和方程数平衡，可以求解。&lt;/li&gt;
&lt;li&gt;由Park方程推导三阶实用模型时，应保留定子变量$u_{dq}$和$i_{dq}$，而转子变量$u_f$、$i_f$和$\Phi_f$分别用$E_f$、$E_q$和$E_q&apos;$替代，然后再用3个磁链方程消去$\Phi_d$、$\Phi_q$及$i_f$（或$E_q$），从而在最终同步电机模型中保留$u_{dq}$、$i_{dq}$、$E_f$、$E_q&apos;$及$\omega$、$\delta$、$T_m$等变量，其中$E_q&apos;$、$\omega$、$\delta$为状态量，电机方程由3个电压方程和2个转子运动方程组成。当$E_f$和$T_m$已知时（对外表现为励磁系统和原动机系统输入），可联立求解方程。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;变量转换及消去&lt;/h3&gt;
&lt;p&gt;先消去$\Phi_d$、$\Phi_q$及$i_f$（$E_q$）所用表达式，将其用保留变量的函数来表示。&lt;/p&gt;
&lt;p&gt;已知直轴磁链方程为：
$$
\Phi_d = -X_di_d + X_{ad}i_f
$$
$$
\Phi_f = -X_{ad}i_d + X_{f}i_f
$$
将上式中的第二个式子等式两侧同乘$X_{ad}/X_f$，有
$$
E_q&apos; = -\frac{X_{ad}^2}{X_f}i_d + X_{ad}i_f
$$&lt;/p&gt;
&lt;p&gt;因为
$$
X_d&apos; = X_d - \frac{X_{ad}^2}{X_f}
$$
则上上个式子可表示为
$$
E_q&apos; = \Phi_d + X_d&apos;i_d
$$
或
$$
\Phi_d = E_q&apos; - X_d&apos;i_d
$$
上式即可用来消去$\Phi_d$。&lt;/p&gt;
&lt;p&gt;由$\Phi_d = -X_di_d + X_{ad}i_f$和$E_q = X_{ad}i_f$，可得
$$
\Phi_d = E_q - X_di_d
$$
将$\Phi_d = E_q&apos; - X_d&apos;i_d$代入上式，可得
$$
E_q&apos; - X_d&apos;i_d = E_q - X_di_d
$$
整理，上式即为
$$
E_q = E_q&apos; + (X_d - X_d&apos;)i_d
$$
该式可用于消去$i_f$($E_q$)。该式同时也是忽略$D$、$Q$绕组时用于表征$E_q$和$E_q&apos;$之间的一个重要关系。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：计及$D$、$Q$绕组暂态时，此关系不成立。&lt;/p&gt;
&lt;p&gt;$q$轴磁链方程为：
$$
\Phi_q = -X_qi_q
$$
该式可直接用来消去$\Phi_q$。&lt;/p&gt;
&lt;p&gt;至此，消去$\Phi_d$、$\Phi_q$和$i_f$($E_q$)的表达式均已得到，分别为：
$$
\Phi_d = E_q - X_di_d
$$
$$
E_q = E_q&apos; + (X_d - X_d&apos;)i_d
$$
$$
\Phi_q = -X_qi_q
$$&lt;/p&gt;
&lt;h3&gt;导出三阶模型&lt;/h3&gt;
&lt;p&gt;对原始电压方程，令$p\Phi_d=p\Phi_q=0, \omega \approx 1$，再将$\Phi_d = E_q - X_di_d$和$\Phi_q = -X_qi_q$代入，得
$$ \left{
\begin{aligned}
u_d = X_qi_q - r_ai_d \
u_q = E_q&apos; - X_d&apos;i_d - r_ai_q
\end{aligned}
\right.
$$
对于励磁绕组电压方程，将其改写为
$$
p\Phi_f = u_f - r_fi_f
$$
上式两侧同乘
$$
\frac{X_{ad}}{X_f}\times\frac{X_f}{r_f}
$$
由于$T_{d0}&apos;=X_f / r_f$，再由$E_f$、$E_q$和$E_q&apos;$的定义，可得
$$
T_{d0}&apos;pE_q&apos;=E_f - E_q
$$
将$E_q = E_q&apos; + (X_d - X_d&apos;)i_d$代入上式，消去$E_q$，有
$$
T_{d0}&apos;pE_q&apos; = E_f - E_q&apos; - (X_d - X_d&apos;)i_d
$$
上式即为转子励磁绕组的暂态方程。&lt;/p&gt;
&lt;p&gt;另对转子运动方程改造如下：&lt;/p&gt;
&lt;p&gt;根据
$$
T_J \frac{d\omega}{dt} = T_m - T_e = T_m - [E_q&apos;i_q - (X_d&apos; - X_q)i_di_q]
$$
消去$\Phi_d$和$\Phi_q$，得到
$$
T_J \frac{d\omega}{dt} = T_m - T_e = T_m - [E_q&apos;i_q - (X_d&apos; - X_q)i_di_q]
$$
另一运动方程不变，为
$$
\frac{d\delta}{dt} = \omega - 1
$$&lt;/p&gt;
&lt;p&gt;至此，以下四个式子构成了同步发电机的实用三阶模型：
$$ \left{
\begin{aligned}
u_d = X_qi_q - r_ai_d \
u_q = E_q&apos; - X_d&apos;i_d - r_ai_q
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;p&gt;$$
T_{d0}&apos;pE_q&apos; = E_f - E_q&apos; - (X_d - X_d&apos;)i_d
$$&lt;/p&gt;
&lt;p&gt;$$
T_J \frac{d\omega}{dt} = T_m - T_e = T_m - [E_q&apos;i_q - (X_d&apos; - X_q)i_di_q]
$$&lt;/p&gt;
&lt;p&gt;$$
\frac{d\delta}{dt} = \omega - 1
$$&lt;/p&gt;
&lt;h2&gt;推广至十二相同步发电机&lt;/h2&gt;
&lt;p&gt;十二相同步发电机的定子绕组在一定条件下可视为4个完全解耦的三相同步发电机定子绕组，每套Y绕组之间位移15°电角度。因此三相同步发电机模型和十二相同步发电机模型只在Park变换上有绕组数量和位移的差别，对于定转子分解的计算和分析则完全一致。&lt;/p&gt;
&lt;p&gt;对于和$E_q$，可以很简单的推导如下。首先有：
$$
T_{d0}&apos;pE_q&apos; = E_f - E_q&apos; - (X_d - X_d&apos;)i_d
$$&lt;/p&gt;
&lt;p&gt;移项为求解$E_q&apos;$形式，并$Laplace$变换至$s$域：
$$
E_q&apos; = \frac{1}{T_{d0}&apos;s+1}(E_f-(X_d - X_d&apos;)i_d)
$$&lt;/p&gt;
&lt;p&gt;对于转子运动方程，若原动机为柴油机，在考虑机组摩擦转矩阻尼系数$\lambda_f$的情况下，有
$$
T_m - T_e - \lambda_f \omega = T_J \frac{d\omega}{dt}
$$
$$
\omega = \frac{T_m - T_e}{T_J s + \lambda_f}
$$&lt;/p&gt;
&lt;h2&gt;Simulink模型&lt;/h2&gt;
&lt;p&gt;对于Simulink模型，机端电压$u_d$和$u_q$为结果量而非驱动量，实际的驱动量为发电机感应电动势$e_d$和$e_q$：
$$
e_d = p\Phi_d - \omega\Phi_q
$$
$$
e_q = p\Phi_q + \omega\Phi_d
$$
三阶实用模型中通常忽略定子暂态，即令$p\Phi_d=p\Phi_q=0$，于是有
$$
e_d \approx - \omega\Phi_q
$$
$$
e_q \approx \omega\Phi_d
$$
我们已经在之前的推导中得到$\Phi_d$和$\Phi_q$的实用参数表达式
$$
\Phi_d = E_q - X_di_d
$$
$$
\Phi_q = - X_qi_q
$$
代入$E_q = X_{ad}i_f$，有
$$
\Phi_d = X_{ad}i_f - X_di_d
$$
联立，有
$$
e_d = \omega X_qi_q
$$
$$
e_q = \omega X_{ad}i_f - \omega X_di_d
$$
上式可用于代数计算某一时刻的$e_d$和$e_q$，但无法反应系统特征。要获取系统特征，改写为以下式子：
$$
e_d = \omega X_qi_q
$$
$$
e_q = \omega E_q&apos; - \omega X_d&apos;i_d = \omega \frac{1}{T_{d0}&apos;s+1}(E_f-(X_d - X_d&apos;)i_d) - \omega X_d&apos;i_d
$$&lt;/p&gt;
&lt;h2&gt;励磁系统简化模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/dcdc/PID.webp&quot; alt=&quot;励磁系统简化模型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;对于励磁系统而言，不考虑饱和、剩磁和转速变化的影响时，主励磁机可简化为一阶惯性环节：
$$
G_{E}(s)=\frac{K_E}{T_{fde}s+1}
$$
式中，$T_{fde}$为励磁机励磁绕组时间常数，$K_E$为励磁机放大倍数。&lt;/p&gt;
&lt;p&gt;事实上，对于电力系统建模而言，励磁机的作用仅仅只是根据输入和参考量，来计算得到合适的$E_f$，因此无需对励磁机本身做非常精确的建模。任何一个电气量(比如$X_{fd}$)的细微差别都可能被控制器抵消。在这种情况下，仅需保证励磁系统结构和输出量正确即可，无需过分关注部分元器件的精确参数。&lt;/p&gt;
&lt;h2&gt;定转子分解模型&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/dcdc/4.webp&quot; alt=&quot;定转子分解&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;转子模型&lt;/h3&gt;
&lt;p&gt;计及转子稳定绕组的情况下，考虑转子磁链方程为
$$
\left{
\begin{aligned}
p\psi_{fd} &amp;amp;= \omega_b \left( u_{fd} - r_{fd} i_{fd} \right) \
p\psi_{kd} &amp;amp;= \omega_b \left( - r_{kd} i_{kd} \right) \
p\psi_{fq} &amp;amp;= \omega_b \left( - r_{fq} i_{fq} \right) \
p\psi_{kq} &amp;amp;= \omega_b \left( - r_{kq} i_{kq} \right)
\end{aligned}
\right.
$$
式中$\omega_b$为转速基值。标幺化时时间基值为$t_B = 1/\omega_B$，因此对时间的微分实际上为
$$
p=\frac{d}{dt^*} = \frac{d}{\omega_B t}
$$
由转子磁链方程
$$
\left{
\begin{aligned}
\psi_{fd} = \psi_{ad} + \psi_{fdl} \
\psi_{kd} = \psi_{ad} + \psi_{kdl} \
\psi_{fq} = \psi_{aq} + \psi_{fql} \
\psi_{kq} = \psi_{aq} + \psi_{kql}
\end{aligned}
\right.
$$
式中$\psi_{md}$和$\psi_{mq}$为直轴、交轴激磁磁链。&lt;/p&gt;
&lt;p&gt;上式可进一步化为适合计算的形式：
$$
\left{
\begin{aligned}
i_{fd} = x_{fdl}^{-1}(\psi_{fd} - \psi_{ad}) \
i_{kd} = x_{kdl}^{-1}(\psi_{kd} - \psi_{ad}) \
i_{fq} = x_{fql}^{-1}(\psi_{fq} - \psi_{aq}) \
i_{kq} = x_{kql}^{-1}(\psi_{kq} - \psi_{aq})
\end{aligned}
\right.
$$&lt;/p&gt;
&lt;p&gt;根据磁路基尔霍夫定理，直轴、交轴激磁磁链不计饱和时的计算公式为
$$
\left{
\begin{aligned}
\frac{\psi_{ad}}{x_{ad}&apos;} = -\Sigma i_{di} + \frac{\psi_{fd}}{x_{fdl}} + \frac{\psi_{kd}}{x_{kdl}} \
\frac{\psi_{aq}}{x_{aq}&apos;} = -\Sigma i_{qi} + \frac{\psi_{fq}}{x_{fql}} + \frac{\psi_{kq}}{x_{kql}}
\end{aligned}
\right.
$$
整理有
$$
\left{
\begin{aligned}
\psi_{ad} = x_{ad}&apos;(-(i_{d1}+i_{d2}+i_{d3}+i_{d4}) + \frac{\psi_{fd}}{x_{fdl}} + \frac{\psi_{kd}}{x_{kdl}}) \
\psi_{aq} = x_{aq}&apos;(-(i_{q1}+i_{q2}+i_{q3}+i_{q4}) + \frac{\psi_{fq}}{x_{fql}} + \frac{\psi_{kq}}{x_{kql}})
\end{aligned}
\right.
$$
式中$x_{ad}&apos;$和$x_{aq}&apos;$由等效感抗合成得来：
$$
\left{
\begin{aligned}
x_{ad}&apos; = x_{fdl} // x_{kdl} // x_{ad} \
x_{aq}&apos; = x_{fql} // x_{kql} // x_{aq}
\end{aligned}
\right.
$$
上面两个式子可以求解得到转子激磁磁链。&lt;/p&gt;
&lt;h3&gt;定子模型&lt;/h3&gt;
&lt;h4&gt;直轴部分&lt;/h4&gt;
&lt;p&gt;首先引入实用变量$x_d&apos;&apos;$:
$$
x_d&apos;&apos; = x_{ls} + x_{ad} // x_{fdl} // x_{kdl}
$$&lt;/p&gt;
&lt;p&gt;并且
$$
x_{fd} = x_{fdl} + x_{ad}, \quad x_{kd} = x_{kdl} + x_{ad}
$$&lt;/p&gt;
&lt;p&gt;定义$q$轴超瞬变电动势$E_q&apos;&apos;$，其物理意义时当$fd$绕组磁链为$\psi_{fd}$，$kd$绕组磁链为$\psi_{kd}$时，同步转速下的定子$q$轴开路电动势。&lt;/p&gt;
&lt;p&gt;根据叠加原理，同步转速下时，有：
$$
\psi_d = \frac{\psi_{fd}}{x_{fdl} + x_{ad} // x_{kdl}} \times \frac{x_{kdl}}{x_{ad} + x_{kdl}} \times x_{ad} + \frac{\psi_{kd}}{x_{kdl} + x_{ad} // x_{fdl}} \times \frac{x_{fdl}}{x_{ad} + x_{fdl}} \times x_{ad}
\
= \frac{x_{ad}}{x_{fd}x_{kd} - x_{ad}^2}(x_{kdl}\psi_{fd} + x_{fdl}\psi_{kd})
$$&lt;/p&gt;
&lt;p&gt;同步转速下电机定子开路时，$E_q&apos;&apos;$的值即等于$\psi_d$为
$$
E_q&apos;&apos; = \frac{x_{ad}}{x_{fd}x_{kd} - x_{ad}^2}(x_{kdl}\psi_{fd} + x_{fdl}\psi_{kd})
$$&lt;/p&gt;
&lt;p&gt;根据同步发电机五阶实用模型，$d$轴磁链表达式为
$$
\psi_d = E_q&apos;&apos; - X_d&apos;&apos;i_d \
= \frac{x_{ad}}{x_{fd}x_{kd} - x_{ad}^2}(x_{kdl}\psi_{fd} + x_{fdl}\psi_{kd}) - X_d&apos;&apos;i_d
$$&lt;/p&gt;
&lt;p&gt;考虑将上式等号右侧的分式项转换成带漏抗的形式。展开分母，有
$$
\begin{aligned}
x_{fd} \cdot x_{kd} - x_{ad}^2
&amp;amp;= (x_{fdl} + x_{ad})(x_{kdl} + x_{ad}) - x_{ad}^2 \
&amp;amp;= x_{fdl} x_{kdl} + x_{fdl} x_{ad} + x_{ad} x_{kdl} + x_{ad}^2 - x_{ad}^2 \
&amp;amp;= x_{fdl} x_{kdl} + x_{fdl} x_{ad} + x_{ad} x_{kdl}
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;因此原分式可以改写为
$$
\frac{x_{ad}}{x_{fd} \cdot x_{kd} - x_{ad}^2}
= \frac{x_{ad}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}}
$$&lt;/p&gt;
&lt;p&gt;因此原式子等号右侧可以改写为：
$$
\frac{x_{ad}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}} \cdot x_{kdl} \cdot \psi_{fd} + \frac{x_{ad}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}} \cdot x_{fdl} \cdot \psi_{kd}
$$&lt;/p&gt;
&lt;p&gt;我们恰好有
$$
x_{ad}&apos; = x_{ad} \parallel x_{fdl} \parallel x_{kdl}
= \frac{x_{ad} \cdot x_{fdl} \cdot x_{kdl}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}}
$$&lt;/p&gt;
&lt;h1&gt;显然能够得到以下关系式：
$$
\frac{x_{ad}}{x_{fd} \cdot x_{kd} - x_{ad}^2} \cdot x_{kdl}&lt;/h1&gt;
&lt;p&gt;\frac{x_{ad} \cdot x_{fdl} \cdot x_{kdl}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}}
=\frac{x_{ad}&apos;}{x_{fdl}} \cdot \frac{1}{x_{fdl}}\&lt;/p&gt;
&lt;h1&gt;\frac{x_{ad}}{x_{fd} \cdot x_{kd} - x_{ad}^2} \cdot x_{fdl}&lt;/h1&gt;
&lt;h1&gt;\frac{x_{ad} \cdot x_{fdl} \cdot x_{kdl}}{x_{fdl} x_{kdl} + x_{ad} x_{kdl} + x_{ad} x_{fdl}}&lt;/h1&gt;
&lt;p&gt;\frac{x_{ad}&apos;}{x_{fdl}} \cdot \frac{1}{x_{kdl}}
$$&lt;/p&gt;
&lt;p&gt;因此原式子可以改写为
$$
\psi_d = -x_d&apos;&apos;i_d + \frac{x_{ad}&apos;}{x_{fdl}}\psi_{fd} + \frac{x_{ad}&apos;}{x_{kdl}}\psi_{kd}
$$&lt;/p&gt;
&lt;h4&gt;交轴部分&lt;/h4&gt;
&lt;p&gt;同理，可得到
$$
\psi_q = -x_q&apos;&apos;i_q + \frac{x_{aq}&apos;}{x_{fql}}\psi_{fq} + \frac{x_{aq}&apos;}{x_{kql}}\psi_{kq}
$$&lt;/p&gt;
&lt;h4&gt;最终形式&lt;/h4&gt;
&lt;p&gt;联立直轴和交轴方程，得到
$$
\psi_d = -x_d&apos;&apos;i_d + \frac{x_{ad}&apos;}{x_{fdl}}\psi_{fd} + \frac{x_{ad}&apos;}{x_{kdl}}\psi_{kd} \
\psi_q = -x_q&apos;&apos;i_q + \frac{x_{aq}&apos;}{x_{fql}}\psi_{fq} + \frac{x_{aq}&apos;}{x_{kql}}\psi_{kq}
$$&lt;/p&gt;
&lt;p&gt;其中
$$
x_d&apos;&apos; = x_{ls} + x_{ad} // x_{fdl} // x_{kdl} = x_{ls} + x_{ad}&apos;\
x_q &apos;&apos; = x_{ls} + x_{aq} // x_{fql} // x_{kql} = x_{ls} + x_{aq}&apos;\
x_{ad}&apos; = x_{ad} // x_{fdl} // x_{kdl} \
x_{aq}&apos; = x_{aq} // x_{fql} // x_{kql} \
$$&lt;/p&gt;
&lt;p&gt;已知电压方程为
$$
u_d = p\psi_d - \omega\psi_q - r_ai_d \
u_q = p\psi_q + \omega\psi_d - r_ai_q \
$$&lt;/p&gt;
&lt;p&gt;将磁链方程代入到电压方程，可得
$$
\begin{aligned}
u_d = &amp;amp; - (x_d&apos;&apos; p + r_a) i_d + \omega x_q&apos;&apos; i_q&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\frac{x_{ad}&apos;}{x_{fdl}} p\psi_{fd} + \frac{x_{ad}&apos;}{x_{kdl}} p\psi_{kd}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\omega \frac{x_{aq}&apos;}{x_{fql}} \psi_{fq} - \omega \frac{x_{aq}&apos;}{x_{kql}} \psi_{kq} \
\
u_q = &amp;amp; - (x_q&apos;&apos; p + r_a) i_q - \omega x_d&apos;&apos; i_d&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\frac{x_{aq}&apos;}{x_{fql}} p\psi_{fq} + \frac{x_{aq}&apos;}{x_{kql}} p\psi_{kq}&lt;/li&gt;
&lt;li&gt;\omega \frac{x_{ad}&apos;}{x_{fdl}} \psi_{fd} + \omega \frac{x_{ad}&apos;}{x_{kdl}} \psi_{kd}
\end{aligned}
$$
将其改写为更简洁的形式：
$$
u_d = -x_d&apos;&apos;pi_d + \omega x_q&apos;&apos;i_q + (\frac{x_{ad}&apos;}{x_{fdl}} p\psi_{fd} + \frac{x_{ad}&apos;}{x_{kdl}} p\psi_{kd}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\omega \frac{x_{aq}&apos;}{x_{fql}} \psi_{fq} - \omega \frac{x_{aq}&apos;}{x_{kql}} \psi_{kq}) - r_ai_d \
u_q = -x_q&apos;&apos;pi_q - \omega x_d&apos;&apos;i_d + (\frac{x_{aq}&apos;}{x_{fql}} p\psi_{fq} + \frac{x_{aq}&apos;}{x_{kql}} p\psi_{kq}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\omega \frac{x_{ad}&apos;}{x_{fdl}} \psi_{fd} + \omega \frac{x_{ad}&apos;}{x_{kdl}} \psi_{kd}) - r_ai_q
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;括号内的式子实际上就是$e_d&apos;&apos;$和$e_q&apos;&apos;$：
$$
e_d&apos;&apos; = \frac{x_{ad}&apos;}{x_{fdl}} p\psi_{fd} + \frac{x_{ad}&apos;}{x_{kdl}} p\psi_{kd}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\omega \frac{x_{aq}&apos;}{x_{fql}} \psi_{fq} - \omega \frac{x_{aq}&apos;}{x_{kql}} \psi_{kq} \
e_q&apos;&apos; = \frac{x_{aq}&apos;}{x_{fql}} p\psi_{fq} + \frac{x_{aq}&apos;}{x_{kql}} p\psi_{kq}&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;\omega \frac{x_{ad}&apos;}{x_{fdl}} \psi_{fd} + \omega \frac{x_{ad}&apos;}{x_{kdl}} \psi_{kd} \
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;定子电压方程可简化为
$$
u_d = e_d&apos;&apos; -x_d&apos;&apos;pi_d + \omega x_q&apos;&apos;i_q - r_ai_d \
u_q = e_q&apos;&apos; -x_q&apos;&apos;pi_q - \omega x_d&apos;&apos;i_d - r_ai_q
$$&lt;/p&gt;
&lt;p&gt;等效三相电抗对称$(x_d&apos;&apos;=x_q&apos;&apos;)$的前提下，对上式作Park逆变换，有
$$
\left{
\begin{aligned}
u_a = e_a&apos;&apos; - L_d&apos;&apos;\frac{di_a}{dt} - r_ai_a \
u_b = e_b&apos;&apos; - L_d&apos;&apos;\frac{di_b}{dt} - r_ai_b \
u_c = e_c&apos;&apos; - L_d&apos;&apos;\frac{di_c}{dt} - r_ai_c\
\end{aligned}
\right.
$$
上式即为标准电动势-机端电压表达式。$L_d&apos;&apos;$和$r_a$可用Powergui电感和电阻模拟。&lt;/p&gt;
&lt;p&gt;等式成立需要满足条件$x_d&apos;&apos;=x_q&apos;&apos;$，通常情况下二者并不严格相等，为了解决此问题，&lt;a href=&quot;https://kns.cnki.net/kcms2/article/abstract?v=ygxn0A4FGUFrMu8OU4SE-aQ1qnFerUTdRCI6B5RmHPz_2j38BIpW-M9BOt1x_j5yye3DImxAfQoEd1BrgJ9H7KxJj1kC6S3SXK5unW3btYkjNI7HrnDiMbr7M20aVwDLjB42oycvxHEP9zMuFkn42cc8G2Rt_WnKMSAPOa05czWVmc8TTkzjUg==&amp;amp;uniplatform=NZKPT&amp;amp;language=CHS&quot;&gt;文献[40]&lt;/a&gt;提出了在$q$轴转子侧加一虚拟阻尼电阻的方法，从而令$x_d&apos;&apos;=x_q&apos;&apos;$，并且通过仿真验证了新添加的虚拟阻尼电阻并不会显著影响转子暂态性能。&lt;/p&gt;
&lt;h2&gt;励磁控制器参数设计&lt;/h2&gt;
&lt;p&gt;不考虑饱和、剩磁和转速变化的情况下，主励磁机可简化为一阶惯性环节
$$
G_E(s) = \frac{K_E}{T_{fde}s+1}
$$
式中$T_{fde}$为励磁机时间常数。&lt;/p&gt;
&lt;p&gt;励磁机内环（励磁电流内环）控制器传递函数为
$$
G_I(s) = K_{PI} + \frac{K_{II}}{s} = \frac{K_{PI}s+K_{II}}{s}
$$&lt;/p&gt;
&lt;p&gt;直流电压外环控制器传递函数为
$$
G_E(s) = K_{PE} + \frac{K_{IE}}{s} = \frac{K_{PE}s+K_{IE}}{s}
$$&lt;/p&gt;
&lt;p&gt;励磁机励磁内环控制系统开环传递函数为
$$
G_K(s) = \frac{2K_{II}(\frac{K_{PI}}{K_{II}}s+1)}{s}\frac{1}{T_{fde}s+1}
$$&lt;/p&gt;
&lt;p&gt;为简化设计，可选择内环控制器PI参数来对消励磁机惯性环节以降低系统阶数，即
$$
\frac{K_{PI}}{K_{II}} = T_{fde} = 0.06s
$$&lt;/p&gt;
</content:encoded></item><item><title>论文研读</title><link>https://arkmostima.pages.dev/posts/%E8%AE%BA%E6%96%87%E7%A0%94%E8%AF%BB/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E8%AE%BA%E6%96%87%E7%A0%94%E8%AF%BB/</guid><description>Awesome-GNN4TS GNN4TS包括四个基本维度： 预测 分类 异常检测 插补</description><pubDate>Sat, 24 May 2025 11:13:25 GMT</pubDate><content:encoded>&lt;h2&gt;综述文章&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/KimMeen/Awesome-GNN4TS&quot;&gt;Awesome-GNN4TS&lt;/a&gt;
GNN4TS包括四个基本维度：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;预测&lt;/li&gt;
&lt;li&gt;分类&lt;/li&gt;
&lt;li&gt;异常检测&lt;/li&gt;
&lt;li&gt;插补&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;异常检测（P10）&lt;/h3&gt;
&lt;p&gt;异常检测用于识别偏离正常数据生成过程的数据观测值。无监督前提下的传统方法包括：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于距离的方法&lt;/strong&gt;：异常点在特征空间中与其他点的“距离”明显较大，因此不属于某个“簇”或“群体”。典型方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;KNN&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;计算每个点与其最近的 k 个邻居的距离&lt;/li&gt;
&lt;li&gt;若该距离的平均值很大 → 该点可能是异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LOF&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;衡量一个点的密度与其邻域点的密度的比值&lt;/li&gt;
&lt;li&gt;低密度点（与邻居差异大）被视为异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;DBSCAN（密度聚类）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;离群点不属于任何高密度区域，即为异常&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该类方法计算开销高，对高维数据处理能力很差，并且无法获取时间序列的前后关系。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;基于分布的方法&lt;/strong&gt;：异常点是那些不符合总体概率分布假设的数据点。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高斯模型&lt;/li&gt;
&lt;li&gt;ARIMA残差检测&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该类方法依赖分布假设，对非高斯分布数据不稳健。&lt;/p&gt;
&lt;p&gt;深度学习下有两种主要策略用于无监督异常检测：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;重构&lt;/strong&gt;：模型学习如何重建原始输入序列，并使用重构误差来判断异常。考虑模型在训练中只输入正常数据，即其只会重建正常数据；当输入为异常序列时，模型无法准确重建它，因而重构误差变大。误差越大，越可能是异常。&lt;/p&gt;
&lt;p&gt;常见形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Autoencoder / Seq2Seq（LSTM 编码器-解码器）&lt;/li&gt;
&lt;li&gt;Graph Autoencoder（用于图结构数据）&lt;/li&gt;
&lt;li&gt;Variational Autoencoder (VAE)（支持概率输出）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;预测&lt;/strong&gt;：模型学习如何预测下一个时间点（或未来若干步），异常点表现为“预测误差高”的时间点。要求样本之间通常存在时序模式。如果为正常点，模型应当能够准确预测；如果预测结果严重偏离实际观测值，可能为异常数据。&lt;/p&gt;
&lt;p&gt;常见形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LSTM/GRU 预测模型&lt;/li&gt;
&lt;li&gt;Temporal Convolutional Network (TCN)&lt;/li&gt;
&lt;li&gt;Spatio-Temporal GNN（如 ST-GCN, GDN）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;通用方法&lt;/h3&gt;
&lt;p&gt;将异常检测视为一项无监督任务依赖于模型学习给定数据集的正态性概念，深度学习架构一般部署一个由主干模块和评分模块组成的二叉模块化框架：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主干模型用于拟合给定的训练数据（假定为正常值或包含极少的异常）&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;ul&gt;
&lt;li&gt;重构&lt;/li&gt;
&lt;li&gt;预测&lt;/li&gt;
&lt;li&gt;关系差异框架（relational discrepancy）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;重构&lt;/strong&gt;：框架的运行假设时，重构误差在正常时期较低，在异常时期较高，旨在将输入复制为输出，类似于自动编码器。一旦学习了数据结构，模型应该在正常时期很好地逼近输入，但在异常时期会遇到困难。然后，评分模型根据重构的输出计算差异分数，以识别异常事件。&lt;/p&gt;
&lt;p&gt;GNN与其他架构类型的一个关键区别在于主干重建期用STGNN实现。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;预测&lt;/strong&gt;：依赖假设为预测误差在正常时期应该较低，但在异常时期应该较高。主干模块被一个经过训练可以进行一步预测的图神经网络 (GNN) 预测器所取代。在部署期间，预测器进行一步预测，并将预测结果提供给评分器。评分器将预测结果与实际观测信号进行比较，以计算差异，例如绝对误差或均方误差。代表性架构为GDN。&lt;/p&gt;
&lt;p&gt;虽然预测差异框架与重建差异框架可能相似，但它们依赖于根本不同的原理和实施策略。重建差异框架将当前输入投射到潜在空间，尝试重建它，并在重建失败时识别异常。该方法在训练和推理过程中都使用当前输入数据，重点关注模型重建训练数据的能力，而不是样本外数据的能力。相反，预测差异框架使用历史数据预测当前值，并通过将预测与实际观测值进行比较来识别异常。重建侧重于复制当前输入，而预测则强调预测未来数据点并通过预测误差检测异常。&lt;/p&gt;
&lt;h3&gt;GDN（Graph Deviation Network）&lt;/h3&gt;
&lt;p&gt;旨在充分利用多变量时间序列中传感器之间的复杂关系，并指出了两个挑战：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同的传感器具有显著不同的行为（特征变量代表的物理含义不同）&lt;/li&gt;
&lt;li&gt;传感器之间的关系未知&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GDN方法包括四个部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;传感器嵌入(Embedding)，使用嵌入向量灵活捕捉每个传感器的独特特性&lt;/li&gt;
&lt;li&gt;图结构学习，学习传感器之间的关系&lt;/li&gt;
&lt;li&gt;基于图注意力机制预测每个传感器在其邻近传感器上的未来值&lt;/li&gt;
&lt;li&gt;图偏差评分&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;GDN的输入为：&lt;/p&gt;
&lt;p&gt;在每个时间刻度$t$，由$N$个传感器值$s_{train}$组成的N维向量，并在不同时刻进行组合，以构成时间序列数据。&lt;/p&gt;
&lt;p&gt;GDN的输出为：&lt;/p&gt;
&lt;p&gt;一组二进制标签，用于指示每个测试时间点是否为异常&lt;/p&gt;
</content:encoded></item><item><title>图神经网络学习笔记</title><link>https://arkmostima.pages.dev/posts/%E5%9B%BE%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%9B%BE%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/</guid><description>图是一种普遍存在的数据结构，也是描述复杂系统的通用语言。从最普遍的角度来看，图仅仅是对象（即节点）以及这些对象对之间一组交互（即边）的集合。例如，为了将社交网络编码为图，我们可以使用节点来表示个体，使用边来表示两个人是朋友。在生物学领域…</description><pubDate>Wed, 14 May 2025 13:34:54 GMT</pubDate><content:encoded>&lt;h1&gt;图(Graphs)&lt;/h1&gt;
&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;图是一种普遍存在的数据结构，也是描述复杂系统的通用语言。从最普遍的角度来看，图仅仅是&lt;em&gt;对象&lt;/em&gt;（即节点）以及这些对象对之间一组&lt;em&gt;交互&lt;/em&gt;（即边）的集合。例如，为了将社交网络编码为图，我们可以使用节点来表示个体，使用边来表示两个人是朋友。在生物学领域，我们可以用图中的节点来表示蛋白质，用边来表示各种生物相互作用，例如蛋白质之间的动力学相互作用。&lt;/p&gt;
&lt;p&gt;从数学角度，图可以由$G  = (V, E)$表征，由一组节点$V$和这些节点之间的一组边$E$定义。一般将从节点$u \in V$到$v \in V$的边表示为$(u, v) \in E$。在很多情况下，我们只关注简单的图，其中每对节点之间最多有一条边，节点与其自身之间没有边，并且所有边都是无向的。&lt;/p&gt;
&lt;p&gt;在数学角度，表示图的便捷方法是使用邻接矩阵。为了用邻接矩阵表示图，我们对图中的节点进行排序，使得每个节点都索引邻接矩阵中的特定行和列。然后，我们可以将边的存在表示为该矩阵中的元素：如果 $(u, v) \in E$，则 $A[u, v] = 1$，否则 $A[u, v] = 0$。如果图仅包含无向边，则 A 将是对称矩阵；但如果图是有向的（即边的方向很重要），则 A 不一定是对称的。某些图还可以包含加权边，其中邻接矩阵中的元素是任意实值，而不是 ${0, 1}$。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;异构图&lt;/strong&gt;：在异构图中，节点也具有类型（节点可以继续细分），这意味着我们可以将节点集划分为不相交的集合，其中 $V = V1 ∪ V2 ∪ ... ∪ Vk$，其中 $Vi ∩ Vj = ∅，∀i = j$。异构图中的边通常根据节点类型满足约束，最常见的约束是某些边仅连接特定类型的节点，即 $(u, τi, v) ∈ E → u ∈ Vj, v ∈ Vk$。例如，在异构生物医学图中，可能有一种类型的节点代表蛋白质，一种类型的节点代表药物，还有一种类型的节点代表疾病。表示“治疗”的边只会出现在药物节点和疾病节点之间。类似地，表示“多种药物副作用”的边只会出现在两个药物节点之间。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;多重图&lt;/strong&gt;：在多重图中，我们假设图可以分解为一组$ k $层。假设每个节点属于每个层，并且每个层对应一个唯一的关系，表示该层的层内边类型。我们还假设可以存在跨层连接同一节点的层间边类型。通过示例可以更好地理解多重图。例如，在多重交通网络中，每个节点可能代表一个城市，每个层可能代表一种不同的交通方式（例如，航空旅行或火车旅行）。层内边表示由不同交通方式连接的城市，而层间边表示在特定城市内切换交通方式的可能性。&lt;/p&gt;
&lt;h2&gt;图和机器学习&lt;/h2&gt;
&lt;h3&gt;节点分类&lt;/h3&gt;
&lt;p&gt;图的一个经典应用是根据少量标注，来从一批互联网用户中提取出bot账号。乍一看，节点分类似乎是标准监督分类的简单变体，但实际上它们之间存在重要区别。最重要的区别在于，图中的节点&lt;strong&gt;并非独立同分布 (i.i.d.)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;通常，在构建监督机器学习模型时，我们假设每个数据点在统计上与所有其他数据点独立；否则，我们可能需要对所有输入点之间的依赖关系进行建模。我们还假设数据点是同分布的；否则，我们无法保证我们的模型能够推广到新的数据点。&lt;/p&gt;
&lt;p&gt;节点分类完全打破了这一独立同分布假设。我们不是对一组独立同分布的数据点进行建模，而是对一组相互连接的节点进行建模。这种建模方式非常关注节点之间的依赖关系，期望明确的利用节点之间的连接。&lt;/p&gt;
&lt;h3&gt;关系预测&lt;/h3&gt;
&lt;p&gt;节点分类有助于根据节点与图中其他节点的关系推断其相关信息。但是，如果我们缺少这种关系信息，该怎么办呢？如果我们只知道特定细胞中存在的部分蛋白质相互作用，但想对缺失的相互作用做出准确的猜测，该怎么办？我们可以使用机器学习来推断图中节点之间的边吗？&lt;/p&gt;
&lt;p&gt;这项任务有很多名称，例如链接预测、图补全和关系推断，具体取决于具体的应用领域。我们在这里简称为关系预测。与节点分类一样，它是图数据中最流行的机器学习任务之一，并且在现实世界中有着无数的应用。&lt;/p&gt;
&lt;p&gt;关系预测的标准场景是，给定一组节点$V$以及这些节点之间的不完整的边集$E_{train}$，利用这部分信息推断出缺失的边。这项任务的复杂性在很大程度上取决于我们正在研究的图数据的类型。与节点分类类似，关系预测模糊了传统机器学习类别（通常被称为监督式和无监督式）的界限，并且它需要特定于图领域的归纳偏差。此外，与节点分类类似，关系预测也存在多种变体。&lt;/p&gt;
&lt;h3&gt;分类，回归和聚类&lt;/h3&gt;
&lt;p&gt;试图学习图数据，但不是对单个图的各个组成部分（即节点或边）进行预测，而是给定一个包含多个不同图的数据集，我们的目标是针对每个图进行独立的预测。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;类似于：通过学习多个工况下的数据，对每个工况下进行独立的预测&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;最关键的挑战：如何定义有用的特征，以考虑到每个数据点内的关系结构。&lt;/strong&gt;&lt;/p&gt;
&lt;h1&gt;背景和传统方法&lt;/h1&gt;
&lt;h2&gt;图数据和核方法(Kernel Methods)&lt;/h2&gt;
&lt;p&gt;图中有几个比较重要的要素：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;节点度(Node degree)：和节点关联的边(edge)的个数。通常是应用于节点级任务的传统机器学习模型中最具信息量的特征之一。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点中心性(Node centrality)：度只是衡量每个节点有多少个邻居，而特征向量中心性还考虑了节点邻居的重要性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;节点的特征向量中心性&lt;em&gt;e_u&lt;/em&gt;通常用一个递归关系来定义，即节点的中心性与其邻居节点的平均中心性成正比：
$$
e_u = \frac{1}{\lambda}A[u,v]e_v , \lambda = const
$$
将该方程改写为向量的形式，以$e$作为描述节点中心性的向量，这个递归关系定义了邻接矩阵的标准特征向量方程：
$$
\lambda e = A e
$$
根据$Perron-Frobenius$定理，中心度值向量 $e$ 由 $A$ 中最大特征值对应的特征向量给出。&lt;/p&gt;
&lt;p&gt;预印本第12页对表征节点中心性的不同指标做了综述。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;聚类系数(Clustering coefficient)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;节点度和节点中心度可能不足以区分开图中其他节点的特征，比如两个节点度和中心度都很相似的节点，可能也存在明显的差异。&lt;/p&gt;
&lt;p&gt;这种结构区别可以用聚类系数的变体来衡量，其是节点局部邻域中闭合三角形的比例。聚类系数的常用局部变体计算见13页&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;封闭的三角形和主题(closed triangles and motifs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另一种看待聚类系数的方式是计算每个节点局部邻域内闭合三角形的数量&lt;/p&gt;
&lt;h2&gt;图级特征和图核(Graph-level features and graph kernels)&lt;/h2&gt;
&lt;p&gt;暂不讨论&lt;/p&gt;
&lt;h2&gt;邻域重叠检测(Neighborhood Overlap Detection)&lt;/h2&gt;
&lt;p&gt;暂不讨论，电力系统中边的存在都是确定的&lt;/p&gt;
&lt;h1&gt;节点嵌入(Node embeddings)&lt;/h1&gt;
&lt;h1&gt;领域重构方法(Neighborhood Reconstruction methods)&lt;/h1&gt;
&lt;p&gt;这一部分关注的是学习节点嵌入的方法。这些方法的目标是将节点编码为&lt;strong&gt;低维向量&lt;/strong&gt;，这些向量概括了它们在图中的位置及其局部图邻域的结构。换句话说，我们希望将节点投影到一个潜在空间中，其中，该潜在空间中的几何关系对应于原始图或网络中的关系.&lt;/p&gt;
&lt;h2&gt;编码器-解码器视角(An Encoder-Decoder Perspective)&lt;/h2&gt;
&lt;p&gt;涉及两个关键操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;编码器模型将图中的每个节点映射到一个低维向量&lt;/li&gt;
&lt;li&gt;解码器模型获取低维节点嵌入，重建为原始图中每个节点邻域的信息。第30页&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;编码器(Encoder)&lt;/h3&gt;
&lt;p&gt;最简单的浅嵌入编码器：
$$
ENC: V -&amp;gt; R^d
$$&lt;/p&gt;
&lt;h3&gt;解码器(Decoder)&lt;/h3&gt;
&lt;h3&gt;优化编码器-解码器模型&lt;/h3&gt;
&lt;p&gt;为实现重建目标，标准做法是最小化一组训练节点对的经验重建损失：
$$
Loss = \Sigma l(DEC(z_u, z_v), S[u, v])
$$
$l$是一个损失函数，用于衡量解码后的相似度值和真实值之间的差异，可能是均方误差或交叉熵等。大多数方法使用随机梯度下降来最小化损失。&lt;/p&gt;
&lt;h3&gt;编码器-解码器方法概述&lt;/h3&gt;
&lt;p&gt;著名的浅层嵌入方法包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Lap. Eigenmaps&lt;/li&gt;
&lt;li&gt;Graph Fact.&lt;/li&gt;
&lt;li&gt;GraRep&lt;/li&gt;
&lt;li&gt;HOPE&lt;/li&gt;
&lt;li&gt;DeepWalk&lt;/li&gt;
&lt;li&gt;node2vec&lt;/li&gt;
&lt;/ul&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;损失函数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;来简洁地定义和比较不同的嵌入方法&lt;/p&gt;
&lt;h4&gt;基于矩阵分解的方法(Factorization-based approaches)&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;拉普拉斯矩阵&lt;/em&gt;的定义：
$$
L = D - A
$$
$D$是度矩阵，表示了每个节点的节点度；$A$是邻接矩阵，表示了连接关系&lt;/p&gt;
&lt;p&gt;从节点嵌入中解码局部邻域结构的挑战与重建图邻接矩阵中的条目密切相关。更一般地，我们可以将此任务视为使用矩阵分解来学习节点-节点相似性矩阵 S 的低维近似。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;拉普拉斯特征图(&lt;em&gt;Laplacian eigenmaps&lt;/em&gt;):最早也是最具影响力的基于因式分解的方法，根据节点嵌入的L2距离来定义解码器：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$$
DEC(z_u, z_v) = ||z_u - z_v ||^{2}_{2}
$$&lt;/p&gt;
&lt;p&gt;然后，损失函数根据图中的相似度对节点对进行加权：
$$
Loss = \Sigma DEC(z_u, z_v) * S[u, v]
$$&lt;/p&gt;
&lt;p&gt;这种方法非常直白，当应当非常相似的节点之间的嵌入过远时，损失函数会对没模型进行惩罚。假设嵌入$z_u$是$d$维的，那么损失函数的最优解应当由拉普拉斯矩阵的$d$个最小特征向量给出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内积法(&lt;em&gt;Inner Product Methods&lt;/em&gt;)：部分编码器采用内积方法：
$$
DEC(z_u, z_v) = z_u^Tz_v
$$
此处假设两个节点之间的相似度和他们嵌入的点积成正比。Graph Factorization、GraRep和HOPE均使用内积编码器。这三种方法都将内积编码器和以下均方误差相结合：
$$
Loss = \Sigma ||DEC(z_u, z_v) - S[u, v]||_2^2
$$
三种方法的主要区别在于如何定义式中的$S[u,v]$，即如何定义他们所使用的节点间相似性或邻域重叠的概念：&lt;/li&gt;
&lt;li&gt;GF方法直接使用邻接矩阵并设置$S$和$A$&lt;/li&gt;
&lt;li&gt;GraRep基于邻接矩阵的幂定义$S$&lt;/li&gt;
&lt;li&gt;HOPE支持通用的邻域重叠度量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上的方法都被称为矩阵分解方法，因为他们的损失函数都可以使用分解算法（如奇异值分解SVD）最小化。直观地说，这些方法的目标是为每个节点学习嵌入，使得学习到的嵌入向量之间的内积近似于节点相似度的某个确定性度量。&lt;/p&gt;
&lt;h4&gt;随机游走嵌入(&lt;em&gt;Random walk embeddings&lt;/em&gt;)&lt;/h4&gt;
&lt;p&gt;内积方法都采用了节点相似度的确定性度量，即将$S$定义为邻接矩阵的某个多项式函数，并对节点嵌入进行优化，使得$z_u^Tz_v  = S[u,v]$。&lt;/p&gt;
&lt;p&gt;随机游走嵌入方法将内积方法应用于邻域重叠的随机度量，创新之处在于对节点嵌入进行优化，使得如果两个节点倾向于在图上的短随机游走中同时出现，则二者具有相似的嵌入。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;DeepWalk&lt;/em&gt; &amp;amp; &lt;em&gt;node2vec&lt;/em&gt;：这两种方法和上述的矩阵分解方法类似，都使用了浅嵌入方法和内积解码器，关键区别在于他们如何定义节点相似性和邻域重构的概念，这些方法并非直接重构邻接矩阵$A$（或$A$的某个确定性函数），而是优化嵌入以编码随机游动的统计数据。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;从数学上讲，目标是学习嵌入，使以下公式得到大致满足：&lt;/p&gt;
&lt;p&gt;$$
DEC(z_u, z_v) = \frac{e^{z_u^Tz_v}}{\Sigma &lt;em&gt;{u_k}e^{z_u^Tz_v}} = p&lt;/em&gt;{G,T}(v|u)
$$&lt;/p&gt;
&lt;p&gt;其中$p_{G,T}(v|u)$是从$u$触发，在长度为$T$的随机游动中访问$v$的概率，$T$通常定义为2到10的范围。这个式子和上一小节中基于分解的$Loss$函数的关键区别在于，这个式子的相似性度量既是随机的，又是非对称的。&lt;/p&gt;
&lt;p&gt;为了训练随机游走嵌入，一般策略是使用上式的解码器，并最小化以下交叉熵损失：
$$
Loss = \Sigma_{(u,v)\in D} - log(DEC(z_u, z_v))
$$
这里用$D$表示随机游动的训练集，由从每个节点开始的随机游动采样生成。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LINE&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;图神经网络(Graph Neural Network)&lt;/h1&gt;
&lt;p&gt;GNN是一个用于定义基于图数据的深度神经网络的通用框架，核心思想是，我们希望生成实际上依赖于图结构以及我们可能拥有的任何特征信息的节点表示。&lt;/p&gt;
&lt;p&gt;开发用于图结构数据的复杂编码器的主要挑战在于，常用的深度学习Kit并不适用，比如CNN仅在Grid结构输入表现良好，RNN仅在序列上表现良好等。&lt;/p&gt;
&lt;h2&gt;神经信息传递&lt;/h2&gt;
&lt;p&gt;GNN的定义性特征在于其使用一种神经信息传递的形式，其中向量信息在节点间交换，并使用神经网络进行更新。&lt;/p&gt;
&lt;h3&gt;概览&lt;/h3&gt;
&lt;p&gt;在GNN的每次消息传递迭代中，每个节点$u \in V$对应的隐藏嵌入$h^{(k)}_u$会根据从u的邻居聚合的信息进行更新。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/GNN/1.webp&quot; alt=&quot;单个节点从其本地邻域聚合消息&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;节点特征&lt;/em&gt;：与本书第一部分讨论的浅嵌入方法不同，GNN 框架要求我们将节点特征$x_u$, $∀u ∈ V$
作为模型的输入。在许多图中，我们会有丰富的节点特征可供使用（例如，生物网络中的基因表达特征或社交网络中的文本特征）。但是，在没有节点特征可用的情况下，仍然有几种选择。一种选择是使用节点统计数据（例如 2.1 节中介绍的统计数据）来定义特征。另一种流行的方法是使用身份特征，我们将每个节点与一个独热指示器特征相关联，该特征唯一地标识该节点。&lt;/p&gt;
&lt;h3&gt;动机和直觉&lt;/h3&gt;
&lt;p&gt;GNN消息传递框架后的基本直觉很简单：在每次迭代中，每个节点都会聚合来自其本地邻域的信息，并且随着迭代的进行，每个节点嵌入都会包含越来越多的来自图更远范围的信息。$k$次迭代之后，每个节点嵌入都包含其$k$范围邻域的信息。&lt;/p&gt;
&lt;p&gt;问题是，这些节点嵌入实际上编码了什么样的信息。通常这种信息有两种形式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;图的结构信息：$k$次迭代后节点$u$的嵌入可能编码了$u$的$k$跳邻域内所有节点的节点度信息，这种信息在许多任务中都很有用，比如分子图。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;节点特征信息：$k$次迭代后每个节点的嵌入还编码了其$k$跳邻域内所有特征的信息，GNN的局部特征聚合行为类似于CNN中卷积核的行为。不同的是，CNN聚合的是图像中空间定义块的特征信息，而GNN基于局部图邻域聚合信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;基本GNN&lt;/h3&gt;
&lt;p&gt;基本的GNN消息传递可被定义为：
$$
h_u^{(k)} = \sigma(W_{self}^{(k)}h_u^{(k-1)} + W^{(k)}&lt;em&gt;{neighbor}\Sigma&lt;/em&gt;{v\in N(u)}h^{(k-1)}&lt;em&gt;v + b^{(k)})
$$
其中$W&lt;/em&gt;{self}^{(k)}$和$W^{(k)}_{neighbor}$为可训练参数矩阵，$\sigma$表示元素非线性（如&lt;code&gt;tanh&lt;/code&gt;和&lt;code&gt;ReLU&lt;/code&gt;）。$b^{(k)}$为偏置项，通常被忽略，但是对于实现高性能非常重要。&lt;/p&gt;
&lt;p&gt;基本GNN框架中的消息的传递类似于MLP或Elman RNN，因为他依赖于线性运算，然后进行单元素非线性运算：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对来自邻居节点的消息进行求和&lt;/li&gt;
&lt;li&gt;使用线性组合将邻居节点的信息与节点之前的嵌入进行组合&lt;/li&gt;
&lt;li&gt;应用元素非线性运算&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;带有自循环的消息传递&lt;/h3&gt;
&lt;p&gt;为了简化神经消息传递方法，通常会在输入图中添加自循环，并省略显式更新步骤。在这种方法中，我们将消息传递简单定义为：
$$
h_u^{(k)} = AGGREGATE({h_v^{k-1}})
$$&lt;/p&gt;
&lt;p&gt;这样做令聚合操作针对的是节点的邻居和节点本身，不再需要定义显式的更新函数，因为更新是通过聚合方法隐式定义的。这种方式可缓解过拟合，但也严重限制了GNN的表达能力，因为来自节点邻居的信息与来自节点本身的信息无法区分。&lt;/p&gt;
&lt;h2&gt;广义邻域聚合(&lt;em&gt;Generalized Neighborhood Aggregation&lt;/em&gt;)&lt;/h2&gt;
&lt;p&gt;基本GNN同MLP一样，可以通过多种方式进行改进和泛化以达到更好的性能。&lt;/p&gt;
&lt;h3&gt;邻域归一化&lt;/h3&gt;
&lt;p&gt;最基本的邻域聚合只是简单的对邻居嵌入求和，这样的一个问题是它可能不稳定，并且对节点高度敏感。&lt;/p&gt;
&lt;p&gt;解决这一问题的一个方法是简单的根据所涉及节点的度对聚合操作进行归一化，比如取平均值：
$$
m = \frac{\Sigma h_v}{|N(u)|}
$$&lt;/p&gt;
&lt;p&gt;研究表明其他归一化方法也可以达到很好的性能：
$$
m = \frac{\Sigma h_v}{\sqrt(|N(u)||N(v)|)}
$$&lt;/p&gt;
&lt;p&gt;例如，被引用太多次的论文可能对于推断图中的社区成员身份没有太大用处，因为这些论文可能在不同的子领域内被引用数千次（电力系统中的发电机也是这个道理）。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;是否要进行归一化？&lt;/em&gt;：
在使用 GNN 时，适当的归一化对于实现稳定且强大的性能至关重要。然而，需要注意的是，归一化也可能导致信息丢失。例如，归一化后，使用学习到的嵌入来区分不同度数的节点可能变得困难（甚至不可能），并且各种其他结构图特征可能会因归一化而变得模糊。因此，归一化的使用是一个特定于应用的问题。通常，在节点特征信息远比结构信息更有用，或者节点度数范围非常广，可能导致优化过程中不稳定的任务中，归一化最有帮助。&lt;/p&gt;
&lt;h4&gt;图卷积网络(GCN)&lt;/h4&gt;
&lt;p&gt;图卷积网络 (GCN)采用了对称归一化聚合以及自循环更新方法。GCN 模型将消息传递函数定义为：
$$
h_u^{(k)} = \sigma(W^{(k)}\Sigma\frac{h_v}{\sqrt{|N(u)||N(v)|}})
$$&lt;/p&gt;
&lt;h3&gt;Set Aggregators&lt;/h3&gt;
&lt;p&gt;邻域归一化可以成为提升 GNN 性能的有效工具，但我们能否做更多来改进 AGGREGATE 运算符？是否有比仅仅对邻域嵌入求和更复杂的方法？&lt;/p&gt;
&lt;p&gt;邻域聚合运算本质上是一个集合函数。给定一组邻域嵌入，我们必须将这个集合映射到一个向量。&lt;/p&gt;
&lt;h3&gt;边(Edge)特征和多关系图神经网络(GNN)&lt;/h3&gt;
&lt;p&gt;在许多应用中，所涉及的图是多关系的或异构的（例如知识图谱）。&lt;/p&gt;
&lt;h1&gt;GNN的实践&lt;/h1&gt;
&lt;h2&gt;应用的损失函数&lt;/h2&gt;
&lt;p&gt;在当前的绝大多数应用中，GNN 用于以下三个任务之一：节点分类、图分类或关系预测。如第一章所述，这些任务反映了大量的实际应用，例如预测用户在社交网络中是否为机器人（节点分类）、基于分子图结构的属性预测（图分类）以及在线平台中的内容推荐（关系预测）。&lt;/p&gt;
&lt;p&gt;在本节中，我们将简要描述如何将这些任务转化为 GNN 的具体损失函数，并讨论如何以无监督的方式对 GNN 进行预训练，以提高这些下游任务的性能。&lt;/p&gt;
&lt;p&gt;我们将使用$z_u \in R^d$表示GNN最后一层的节点嵌入输出，并使用$z_G \in R^d$表示池化函数输出的图级嵌入.&lt;/p&gt;
&lt;h3&gt;节点分类&lt;/h3&gt;
&lt;p&gt;GNN应用于此类节点分类任务的标准方法是采用全监督方式训练GNN，并使用Softmax分类函数和负对数似然损失来定义损失：
$$
Loss = \Sigma - log(Softmax(z_u, y_u))
$$
假设$y_u$是一个独热向量，表示训练节点$u$的类别。&lt;/p&gt;
&lt;p&gt;这种策略是以训练GNN的最常见的优化策略之一。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;监督，半监督，传导和归纳式&lt;/em&gt;：
节点分类设置通常被称为监督式和半监督式。应用这些术语时，一个重要因素是，在训练 GNN 期间是否以及如何使用不同的节点。通常，我们可以区分三种类型的节点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;训练节点集 Vtrain。这些节点包含在 GNN 消息传递操作中，并且它们也用于计算损失，&lt;/li&gt;
&lt;li&gt;除了训练节点之外，我们还可以有传导式测试节点 Vtrans。这些节点未标记，不用于损失计算，但这些节点及其关联边仍然参与 GNN 消息传递操作。换句话说，GNN 将在 GNN 消息传递操作期间为 u ∈ Vtrans 中的节点生成隐藏表示$h^{(k)}_u$。然而，这些节点的最终层嵌入 zu 将不会用于损失函数计算。&lt;/li&gt;
&lt;li&gt;最后，我们还将有归纳测试节点 Vind。这些节点在训练期间既不会用于损失计算，也不会用于 GNN 消息传递操作，这意味着这些节点及其所有边在 GNN 训练期间完全不可见。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;“半监督”一词适用于在直推测试节点上测试 GNN 的情况，因为在这种情况下，GNN 在训练期间会观察测试节点（但不观察它们的标签）。“归纳节点分类”一词用于区分测试节点及其所有关联边在训练期间完全不可见的情况。归纳节点分类的一个例子是在引文网络的一个子图上训练 GNN，然后在一个完全不相交的子图上对其进行测试。&lt;/p&gt;
&lt;h3&gt;图分类&lt;/h3&gt;
&lt;p&gt;与节点分类类似，图级分类应用作为基准测试任务也很受欢迎。历史上，核方法(Kernel Methods)在图分类中很流行，因此，一些早期最流行的图分类基准测试都改编自核方法文献。在这些任务中，通常使用softmax 分类损失。近年来，图神经网络 (GNN) 在涉及图数据的回归任务中也取得了成功，尤其是在基于图的分子表示预测分子特性（例如溶解度）的任务中。&lt;/p&gt;
&lt;h3&gt;关系预测&lt;/h3&gt;
&lt;h2&gt;效率问题和节点采样&lt;/h2&gt;
</content:encoded></item><item><title>基于Qt 6.9 为 ARM 设备交叉编译应用程序</title><link>https://arkmostima.pages.dev/posts/qt%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/qt%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91/</guid><description>如果接触过Linux驱动或应用开发，那么你一定有一个交叉编译器，用于在x64的机器上将c文件编译成可供arm架构设备执行的二进制版本。Qt也是这样，不过不同的是Qt基于C++运行，因此交叉编译器不再是xxx-gcc，而是xxx-g++或…</description><pubDate>Thu, 24 Apr 2025 09:34:45 GMT</pubDate><content:encoded>&lt;h3&gt;交叉编译器&lt;/h3&gt;
&lt;p&gt;如果接触过Linux驱动或应用开发，那么你一定有一个交叉编译器，用于在x64的机器上将c文件编译成可供arm架构设备执行的二进制版本。Qt也是这样，不过不同的是Qt基于C++运行，因此交叉编译器不再是&lt;code&gt;xxx-gcc&lt;/code&gt;，而是&lt;code&gt;xxx-g++&lt;/code&gt;或&lt;code&gt;xxx-gnu&lt;/code&gt;。以我的交叉编译器为例，编译C语言时我使用的是7.5.0版本的Linaro公司的&lt;code&gt;arm-linux-gnueabihf-gcc&lt;/code&gt;。同理，编译C++的就是&lt;code&gt;arm-linux-gnueabihf-g++&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;有一个办法可以用来快速确认编译器是否可用。在开发板的根目录下（或者别的随便什么位置）新建一个测试文件&lt;code&gt;test.cpp&lt;/code&gt;，输入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;
using namespace std;

int main()
{
  cout &amp;lt;&amp;lt; &quot;Hello World!&quot; &amp;lt;&amp;lt; endl; 
  return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在Ubuntu虚拟机中将其交叉编译为arm：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;arm-linux-gnueabihf-g++ test.cpp -o test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在开发板中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;./test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果你看到终端打印出了&quot;Hello World!&quot;，说明你的交叉编译器工作正常。&lt;/p&gt;
&lt;h3&gt;Qt环境配置&lt;/h3&gt;
&lt;p&gt;我们常说的“Qt&quot;其实由很多个东西组成，包括但不限于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Qt源码(source code)，比如 QtCore、QtWidgets、QtNetwork 等模块&lt;/li&gt;
&lt;li&gt;Qt Creator：Qt 官方的 IDE（集成开发环境），用来写代码、调试、构建 Qt 应用&lt;/li&gt;
&lt;li&gt;Qt UI designer：用于设计UI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;现在官方主推的安装Qt的方式是通过在线安装器安装，安装时一般会让你选择需要安装哪些组件。需要注意的是，安装器默认是不会安装Qt源码的，而交叉编译Qt给arm设备又必须需要Qt源码，因为Qt默认没有提供arm的二进制文件。因此我们需要从官网下载Qt源码，这里我选择6.9版本。&lt;/p&gt;
&lt;p&gt;在Qt5时代，可以只进行一次交叉编译。而Qt6则必须先用宿主机(Qt Host)编译一次以生成Host Build的Qt工具链用于后续的Qt tools构建，也就是必须基于x86_64先编译一次Qt的源码，生成一些Qt需要的宿主工具，比如moc和rcc。然后在这基础上，才能继续进行交叉编译。交叉编译时也需要向Cmake指定宿主工具的目录，否则交叉编译会失败。Qt5的编译步骤和Qt6差异较大，而关于Qt5的编译教程又有很多，这里就不展开了，主要讲Qt6.&lt;/p&gt;
&lt;p&gt;在浏览器中下载源码并拷贝至虚拟机：https://download.qt.io/official_releases/qt/6.9/6.9.0/single/qt-everywhere-src-6.9.0.tar.xz&lt;/p&gt;
&lt;p&gt;或者直接在虚拟机中&lt;code&gt;wget https://download.qt.io/official_releases/qt/6.9/6.9.0/single/qt-everywhere-src-6.9.0.tar.xz&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;下载完成后，解压（耗时较长），进入源码目录。&lt;/p&gt;
&lt;h3&gt;基于x86_64编译获得Qt Host&lt;/h3&gt;
&lt;p&gt;首先要构建Host。在Qt源码目录下新建一个&lt;code&gt;build-qt-host&lt;/code&gt;文件夹，用于存放编译结果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd ~/Qt_source_code/qt-everywhere-src-6.9.0
mkdir build-qt-host &amp;amp;&amp;amp; cd build-qt-host
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在终端中配置cmake：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cmake .. \
  -GNinja \
  -DCMAKE_BUILD_TYPE=Release \
  -DQT_BUILD_EXAMPLES=OFF \
  -DQT_BUILD_TESTS=OFF
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;执行编译：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ninja
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;默认会编译所有模块，共计需要编译一万多个文件，时间会很长（我用了将近三个小时）。此构建必须成功，否则得不到Qt需要的宿主工具。&lt;/p&gt;
&lt;p&gt;这里当然也可以只编译自己需要的模块，比如我只需要Core、Gui、Widgets、Network这些比较基础的模块，可以指定：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-DFEATURE_network=ON \
-DFEATURE_gui=ON \

......
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在大语言模型这么发达，完全可以让gpt或者deepseek写一个shell脚本，告诉他想要的模块，然后一键编译。这样编译的文件数量和时长会大大减少。&lt;/p&gt;
&lt;p&gt;比如这是我的shell文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# ======================
# Qt Host Build Script
# Author: ChatGPT
# ======================

set -e

# 设置变量
QT_SRC_DIR=~/Qt_source_code/qt-everywhere-src-6.9.0
QT_INSTALL_PREFIX=/usr/local/qt6-host
BUILD_DIR=$QT_SRC_DIR/build-qt-host

# 创建安装路径（如未存在）
sudo mkdir -p &quot;$QT_INSTALL_PREFIX&quot;
sudo chown $(whoami):$(whoami) &quot;$QT_INSTALL_PREFIX&quot;

# 清理旧构建目录
rm -rf &quot;$BUILD_DIR&quot;
mkdir -p &quot;$BUILD_DIR&quot;
cd &quot;$BUILD_DIR&quot;

# ========== Step 1: 构建 qtbase ==========
echo &quot;=== Configuring qtbase ===&quot;
cmake &quot;$QT_SRC_DIR/qtbase&quot; \
  -GNinja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_INSTALL_PREFIX=&quot;$QT_INSTALL_PREFIX&quot; \
  -DFEATURE_sql=ON \
  -DFEATURE_network=ON \
  -DFEATURE_gui=ON \
  -DFEATURE_widgets=ON \
  -DFEATURE_opengl=OFF \
  -DFEATURE_dbus=OFF \
  -DFEATURE_printsupport=OFF \
  -DQT_BUILD_EXAMPLES=OFF \
  -DQT_BUILD_TESTS=OFF

echo &quot;=== Building qtbase ===&quot;
ninja
echo &quot;=== Installing qtbase ===&quot;
ninja install

# ========== Step 2: 构建 qtcharts ==========
echo &quot;=== Configuring qtcharts ===&quot;
mkdir -p &quot;$QT_SRC_DIR/build-qtcharts-host&quot;
cd &quot;$QT_SRC_DIR/build-qtcharts-host&quot;

cmake &quot;$QT_SRC_DIR/qtcharts&quot; \
  -GNinja \
  -DCMAKE_BUILD_TYPE=Release \
  -DCMAKE_PREFIX_PATH=&quot;$QT_INSTALL_PREFIX&quot; \
  -DCMAKE_INSTALL_PREFIX=&quot;$QT_INSTALL_PREFIX&quot; \
  -DQT_BUILD_EXAMPLES=OFF \
  -DQT_BUILD_TESTS=OFF

echo &quot;=== Building qtcharts ===&quot;
ninja
echo &quot;=== Installing qtcharts ===&quot;
ninja install

echo &quot;✅ Qt Host Build Completed and Installed to $QT_INSTALL_PREFIX&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;交叉编译Qt&lt;/h3&gt;
&lt;p&gt;Qt6默认且推荐使用Cmake构建，和makefile相似，Cmake需要一个配置文件来向编译器告知交叉编译工具链。在源码目录下新建一个&lt;code&gt;arm-gnueabihf-toolchain.cmake&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# arm-gnueabihf-toolchain.cmake
SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_PROCESSOR arm)

SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# ARM根文件系统路径
SET(CMAKE_SYSROOT /home/aki/linux/nfs/NXP_rootfs/)

# ARM根文件系统路径
SET(CMAKE_FIND_ROOT_PATH /home/aki/linux/nfs/NXP_rootfs/)

SET(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
SET(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
SET(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
SET(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照注释，将arm的根文件路径替换成自己开发板的路径。&lt;/p&gt;
</content:encoded></item><item><title>Qt 获取网络时间</title><link>https://arkmostima.pages.dev/posts/qt%E8%8E%B7%E5%8F%96%E7%BD%91%E7%BB%9C%E6%97%B6%E9%97%B4/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/qt%E8%8E%B7%E5%8F%96%E7%BD%91%E7%BB%9C%E6%97%B6%E9%97%B4/</guid><description>url：http://quan.suning.com/getSysTime.do</description><pubDate>Thu, 24 Apr 2025 07:41:25 GMT</pubDate><content:encoded>&lt;h2&gt;苏宁易购时间API&lt;/h2&gt;
&lt;p&gt;url：http://quan.suning.com/getSysTime.do&lt;/p&gt;
&lt;p&gt;返回格式：json&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;sysTime2&quot;: &quot;2025-04-24 16:08:00&quot;,
	&quot;sysTime1&quot;: &quot;20250424160800&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Qt工程&lt;/h2&gt;
&lt;h3&gt;mainwindow.h&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include &amp;lt;QMainWindow&amp;gt;
#include &amp;lt;QNetworkAccessManager&amp;gt;
#include &amp;lt;QNetworkReply&amp;gt;
#include &amp;lt;QTimer&amp;gt;
#include &amp;lt;QLabel&amp;gt;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private slots:
    void fetchTime();
    void handleNetworkReply(QNetworkReply *reply);
    void updateLocalTime();

private:
    QNetworkAccessManager *manager;
    QTimer *timer;
    QTimer *updateTimeTimer;
    QLabel *label;
    int retryCount;
    int maxRetryCount = 5;
};
#endif // MAINWINDOW_H
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;mainwindow.cpp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;mainwindow.h&quot;
#include &amp;lt;QJsonDocument&amp;gt;
#include &amp;lt;QJsonObject&amp;gt;
#include &amp;lt;QDateTime&amp;gt;
#include &amp;lt;QTimeZone&amp;gt;
#include &amp;lt;QTimer&amp;gt;
#include &amp;lt;QDebug&amp;gt;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , manager(new QNetworkAccessManager(this))
    , timer(new QTimer(this))
    , updateTimeTimer(new QTimer(this))
    , retryCount(0)
{
    this-&amp;gt;setGeometry(0, 0, 800, 480);

    label = new QLabel(this);
    label-&amp;gt;setGeometry(400, 200, 200, 50);

    //绑定网络请求结束信号：网络请求管理器-&amp;gt;主GUI
    connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(handleNetworkReply(QNetworkReply*)));

    // 本地时间更新定时器，每秒更新时间
    connect(updateTimeTimer, &amp;amp;QTimer::timeout, this, &amp;amp;MainWindow::updateLocalTime);
    updateTimeTimer-&amp;gt;start(1000);

    // 网络时间同步定时器，每30秒请求一次网络时间
    timer-&amp;gt;start(30000);
    connect(timer, &amp;amp;QTimer::timeout, this, &amp;amp;MainWindow::fetchTime); //绑定定时器超时信号，定时器-&amp;gt;主GUI，槽函数：重新获取网络时间

    fetchTime();  // 启动时请求一次网络时间
}

MainWindow::~MainWindow() {}

//本地时间更新定时器回调
void MainWindow::updateLocalTime()
{
    QDateTime currentTime = QDateTime::currentDateTime();   //获取本地时间
    label-&amp;gt;setText(&quot;本地时间：&quot; + currentTime.toString(&quot;yyyy-MM-dd hh:mm:ss&quot;));
}

//获取网络时间
void MainWindow::fetchTime()
{
    QUrl url(&quot;http://quan.suning.com/getSysTime.do&quot;);   //苏宁易购时间API
    QNetworkRequest request(url);   //向url发送请求
    qDebug() &amp;lt;&amp;lt; &quot;发送请求：&quot; &amp;lt;&amp;lt; url;

    manager-&amp;gt;get(request);
}

void MainWindow::handleNetworkReply(QNetworkReply *reply)
{
    if (reply-&amp;gt;error() != QNetworkReply::NoError) {
        qDebug() &amp;lt;&amp;lt; &quot;网络错误：&quot; &amp;lt;&amp;lt; reply-&amp;gt;errorString();

        // 如果请求失败，且重试次数小于最大重试次数，进行重试
        if (retryCount &amp;lt; maxRetryCount) {
            retryCount++;
            qDebug() &amp;lt;&amp;lt; &quot;重试次数：&quot; &amp;lt;&amp;lt; retryCount;
            fetchTime();  // 重新请求
        } else {
            label-&amp;gt;setText(&quot;获取时间失败，已达到最大重试次数&quot;);
            qDebug() &amp;lt;&amp;lt; &quot;获取时间失败，已达到最大重试次数&quot;;
        }

        reply-&amp;gt;deleteLater();
        return;
    }

    //请求成功，读取全部响应数据
    QByteArray responseData = reply-&amp;gt;readAll();
    qDebug() &amp;lt;&amp;lt; &quot;收到响应：&quot; &amp;lt;&amp;lt; responseData;

    //将响应解析为json
    QJsonParseError parseError;
    QJsonDocument jsonDoc = QJsonDocument::fromJson(responseData, &amp;amp;parseError);
    if (parseError.error != QJsonParseError::NoError) {
        label-&amp;gt;setText(&quot;JSON解析失败：&quot; + parseError.errorString());
        qDebug() &amp;lt;&amp;lt; &quot;JSON解析错误：&quot; &amp;lt;&amp;lt; parseError.errorString();
        reply-&amp;gt;deleteLater();
        return;
    }

    //提取&quot;sysTime2&quot;时间字段并转换为QDateTime
    QJsonObject obj = jsonDoc.object();
    QString timeStr = obj.value(&quot;sysTime2&quot;).toString(); // &quot;2025-04-24 09:10:44&quot;
    QDateTime networkTime = QDateTime::fromString(timeStr, &quot;yyyy-MM-dd HH:mm:ss&quot;);

    // 获取当前本地时间
    QDateTime localTime = QDateTime::currentDateTime();

    // 计算误差
    qint64 timeDiff = networkTime.toMSecsSinceEpoch() - localTime.toMSecsSinceEpoch();
    qDebug() &amp;lt;&amp;lt; &quot;时间误差：&quot; &amp;lt;&amp;lt; timeDiff &amp;lt;&amp;lt; &quot;毫秒&quot;;

    // 如果误差大于2秒，则进行时间修正
    if (qAbs(timeDiff) &amp;gt; 2000) {
        qDebug() &amp;lt;&amp;lt; &quot;误差大于2秒，进行修正&quot;;
        // 修正本地时间
        label-&amp;gt;setText(&quot;本地时间已修正：&quot; + networkTime.toString(&quot;yyyy-MM-dd hh:mm:ss&quot;));
    }

    reply-&amp;gt;deleteLater();

    // 重置重试计数器
    retryCount = 0;
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Qt 通过继承QObject实现多线程</title><link>https://arkmostima.pages.dev/posts/qt%E5%A4%9A%E7%BA%BF%E7%A8%8B/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/qt%E5%A4%9A%E7%BA%BF%E7%A8%8B/</guid><description>最简单的多线程示例包含： MainWindow：主线程，负责GUI Worker：干活的对象 Worker::resultReady：子线程发出的信号 QThread：线程控制器 主线程 = 总公司，负责 UI 显示 QThread =…</description><pubDate>Wed, 23 Apr 2025 12:16:08 GMT</pubDate><content:encoded>&lt;h2&gt;Qt实现多线程&lt;/h2&gt;
&lt;p&gt;最简单的多线程示例包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MainWindow：主线程，负责GUI&lt;/li&gt;
&lt;li&gt;Worker：干活的对象&lt;/li&gt;
&lt;li&gt;Worker::resultReady：子线程发出的信号&lt;/li&gt;
&lt;li&gt;QThread：线程控制器&lt;/li&gt;
&lt;/ul&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;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;MainWindow&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QWidget&lt;/code&gt; 子类&lt;/td&gt;
&lt;td&gt;图形界面，主线程运行&lt;/td&gt;
&lt;td&gt;控制整个应用，接收子线程数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QThread&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Qt线程类&lt;/td&gt;
&lt;td&gt;提供线程上下文（事件循环）&lt;/td&gt;
&lt;td&gt;不是干活的，是“搬家工人 + 开个房间”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;QObject&lt;/code&gt; 子类&lt;/td&gt;
&lt;td&gt;真正干活的对象&lt;/td&gt;
&lt;td&gt;会被搬进 QThread 管理的线程空间&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QObject::moveToThread(QThread*)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;方法&lt;/td&gt;
&lt;td&gt;把对象“搬”进线程空间&lt;/td&gt;
&lt;td&gt;只有 QObject 的子类可以这么搬&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;QTimer&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;定时器&lt;/td&gt;
&lt;td&gt;定期调用 &lt;code&gt;Worker::timeout()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;运行在线程内，不阻塞主线程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;主线程 = 总公司，负责 UI 显示&lt;/li&gt;
&lt;li&gt;QThread = 新开了一个工厂的“独立办公室”&lt;/li&gt;
&lt;li&gt;Worker = 工人在新办公室干活&lt;/li&gt;
&lt;li&gt;moveToThread = 把工人搬去那边&lt;/li&gt;
&lt;li&gt;信号/槽 = 发邮件通知主公司，数据干活好了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────────┐              ┌──────────────────────┐
│ MainWindow (GUI) │◄────────────┤    Worker::resultReady
│     主线程        │             │        子线程信号       │
└──────────────────┘              └──────────────────────┘
         │                                  ▲
         │                                  │
         ▼                                  │
 ┌────────────┐     moveToThread()     ┌────────────┐
 │  QThread   │◄───────────────────────│  Worker     │
 │ 子线程控制器 │                      │ 干活的对象   │
 └────────────┘                       └────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Qt多线程中的重要概念是”事件循环“。每个线程可以拥有一个事件循环，通过&lt;code&gt;moveToThread()&lt;/code&gt; 把 &lt;code&gt;Worker&lt;/code&gt; 放进去，它就“生活在”那个线程中了。&lt;/p&gt;
&lt;p&gt;线程中的代码不能够直接访问GUI，会引起线程安全问题。只能通过信号将结果传回主线程。&lt;/p&gt;
&lt;h2&gt;项目示例&lt;/h2&gt;
&lt;h3&gt;mainwindow.cpp&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;mainwindow.h&quot;

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    this-&amp;gt;setGeometry(0, 0, 800, 480);
    pushButton1 = new QPushButton(this);
    pushButton2 = new QPushButton(this);

    pushButton1-&amp;gt;setGeometry(300, 200, 80, 40);
    pushButton2-&amp;gt;setGeometry(400, 200, 80, 40);

    pushButton1-&amp;gt;setText(&quot;开启线程&quot;);
    pushButton2-&amp;gt;setText(&quot;打断线程&quot;);

    /* 声明worker实例 */
    worker = new Worker;

    /* 将worker通过movetoThread()绑定至线程管理器 */
    worker-&amp;gt;moveToThread(&amp;amp;workerThread);

    /* 线程完成后销毁worker和workerThread */
    connect(&amp;amp;workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
    connect(&amp;amp;workerThread, SIGNAL(finished()), &amp;amp;workerThread, SLOT(deleteLater()));

    /* 主线程向worker发送开始信号，worker开始在新线程工作 */
    connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)));

    /* 主线程接收到worker发送来的信号，通知结果已准备好 */
    connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handlerResults(QString)));

    /* 点击按钮开始线程 */
    connect(pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1Clicked()));

    /* 点击按钮打断线程 */
    connect(pushButton2, SIGNAL(clicked()), this, SLOT(pushButton2Clicked()));
}

MainWindow::~MainWindow() {
    worker-&amp;gt;stopWork();
    workerThread.quit();

    if(workerThread.wait(2000)){
        qDebug() &amp;lt;&amp;lt; &quot;线程结束&quot; &amp;lt;&amp;lt; Qt::endl;
    }
}

/* 按钮1回调函数 */
void MainWindow::pushButton1Clicked()
{
    const QString str = &quot;正在运行&quot;;

    if (!workerThread.isRunning()){
        /* 开启线程 */
        workerThread.start();
    }

    /* 发送信号至线程，通知worker开始工作 */
    emit this-&amp;gt;startWork(str);
}

/* 按钮2回调函数 */
void MainWindow::pushButton2Clicked()
{
    if (workerThread.isRunning()){
        /* worker停止工作 */
        worker-&amp;gt;stopWork();
    }
}

void MainWindow::handlerResults(const QString &amp;amp; results)
{
    qDebug() &amp;lt;&amp;lt; &quot;线程的状态&quot; &amp;lt;&amp;lt; results &amp;lt;&amp;lt; Qt::endl;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;整个线程的创建步骤为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;线程对象与Worker对象创建&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;worker = new Worker;                       // 创建Worker对象
worker-&amp;gt;moveToThread(&amp;amp;workerThread);       // 将Worker对象绑定到线程
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;Worker对象：负责实际耗时操作的业务逻辑（需继承QObject）。&lt;/li&gt;
&lt;li&gt;moveToThread()：将Worker的事件循环绑定到workerThread线程。此后Worker的槽函数会在新线程中执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;线程生命周期管理&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;connect(&amp;amp;workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
connect(&amp;amp;workerThread, SIGNAL(finished()), &amp;amp;workerThread, SLOT(deleteLater()));
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;线程结束时自动清理资源：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当线程finished()时，触发worker-&amp;gt;deleteLater()，确保Worker对象在事件循环中安全删除。&lt;/li&gt;
&lt;li&gt;线程自身也通过deleteLater()自动销毁，避免内存泄漏。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;信号-槽通信机制
主线程通知Worker开始工作:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;跨线程通信：通过发射startWork信号，触发Worker的doWork1槽函数。&lt;/li&gt;
&lt;li&gt;队列连接（Queued Connection）：由于Worker位于不同线程，Qt自动使用队列连接，确保槽函数在目标线程执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Worker反馈结果到主线程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connect(worker, SIGNAL(resultReady(QString)), this, SLOT(handlerResults(QString)));
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;线程安全更新UI：Worker通过信号resultReady传递结果，主线程的handlerResults接收并处理（如更新界面），符合“UI操作必须在主线程”原则。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;线程启动与停止
启动线程:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::pushButton1Clicked() {
    if (!workerThread.isRunning()) {
        workerThread.start();     // 启动线程的事件循环
    }
    emit startWork(&quot;正在运行&quot;);    // 触发Worker开始工作
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;start()：启动线程的事件循环，等待处理任务。&lt;/li&gt;
&lt;li&gt;发射信号：通知Worker在新线程中执行doWork1。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;停止线程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MainWindow::pushButton2Clicked() {
    if (workerThread.isRunning()) {
        worker-&amp;gt;stopWork();      // 请求Worker停止工作
    }
}

MainWindow::~MainWindow() {
    worker-&amp;gt;stopWork();          // 停止Worker
    workerThread.quit();         // 结束事件循环
    workerThread.wait(2000);     // 等待线程退出
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;stopWork()：Worker内部应检查标志（如QAtomicInt）安全退出循环。&lt;/li&gt;
&lt;li&gt;quit() + wait()：优雅终止线程，quit()退出事件循环，wait()阻塞等待线程结束。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;线程执行流程&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;用户点击按钮1 → workerThread.start()启动线程。&lt;/li&gt;
&lt;li&gt;主线程发射startWork信号 → Worker的doWork1在新线程执行。&lt;/li&gt;
&lt;li&gt;Worker处理完成 → 发射resultReady信号 → 主线程更新UI。&lt;/li&gt;
&lt;li&gt;用户点击按钮2或窗口关闭 → 调用stopWork()终止任务，线程退出并清理资源。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux 进程与线程（三）线程安全和高级I/O</title><link>https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B3/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B3/</guid><description>对共享资源的访问进行保护 假设对于全局变量a，线程1和线程2都需要访问，a就是多个线程间的共享资源 保护的目的是解决数据一致性问题 当变量可以被多个线程修改和读取时，存在数据一致性问题，需要保证每个线程都能读取到有效且正确的值 数据一致…</description><pubDate>Tue, 22 Apr 2025 05:16:19 GMT</pubDate><content:encoded>&lt;h1&gt;线程同步&lt;/h1&gt;
&lt;h2&gt;线程同步的必要性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;对共享资源的访问进行保护
&lt;ul&gt;
&lt;li&gt;假设对于全局变量&lt;code&gt;a&lt;/code&gt;，线程1和线程2都需要访问，&lt;code&gt;a&lt;/code&gt;就是多个线程间的共享资源&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;保护的目的是解决数据一致性问题
&lt;ul&gt;
&lt;li&gt;当变量可以被多个线程修改和读取时，存在数据一致性问题，需要保证每个线程都能读取到有效且正确的值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;数据一致性问题的本质在于进程中的多个线程对共享资源的并发访问（同时访问）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;互斥锁(Mutex)&lt;/h2&gt;
&lt;p&gt;互斥锁（mutex）是一种同步机制，用于在某一时刻只允许一个线程访问共享资源。其它线程在锁被占用时只能阻塞等待，直到锁被释放。&lt;/p&gt;
&lt;p&gt;Linux中互斥锁用&lt;code&gt;pthread_mutex_t&lt;/code&gt;类型表示。&lt;/p&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;多个线程使用同一个文件描述符&lt;/li&gt;
&lt;li&gt;多个线程对同一硬件外设发起操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;互斥锁初始化&lt;/h3&gt;
&lt;p&gt;以下方法用于在定义mutex时就将其初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PTHREAD_MUTEX_INITIALIZER&lt;/code&gt;这个宏携带了mutex的默认属性。&lt;/p&gt;
&lt;p&gt;如果需要先定义互斥锁，再将其初始化，比如使用malloc()申请分配的互斥锁对象，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_t mutex;
pthread_mutex_init(&amp;amp;mutex, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;或：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_t *mutex = malloc(sizeof(pthread_mutex_t));
pthread_mutex_init(mutex, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;上锁和解锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 上锁 */
int pthread_mutex_lock(pthread_mutex_t *mutex);

/* 解锁 */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;互斥锁的属性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 销毁属性 */
int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

/* 初始化属性 */
int pthread_mutexattr_init(pthread_mutexattr_t *attr);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;条件变量(Condition Variable)&lt;/h2&gt;
&lt;p&gt;条件变量允许一个线程（或多个）阻塞等待某个条件成立，而另一个线程可以在条件满足时调用函数去唤醒等待线程。和互斥锁不同，条件变量本身不保护数据，它需要和互斥锁配合使用。&lt;/p&gt;
&lt;h3&gt;条件变量初始化&lt;/h3&gt;
&lt;p&gt;静态方式&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;动态方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_cond_init(&amp;amp;cond, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;唤醒线程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pthread_cond_signal(&amp;amp;cond);  // 唤醒一个等待的线程
pthread_cond_broadcast(&amp;amp;cond);  // 唤醒所有等待的线程
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这两个函数都可以向指定的条件变量发送信号，通知一个或多个正在等待的线程。&lt;/p&gt;
&lt;p&gt;例如在生产者-消费者模型中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (ret){
    exit(-1);
}

for ( ; ; ){
    pthread_mutex_lock(&amp;amp;mutex);
    g_avail++;
    pthread_mutex_unlock(&amp;amp;mutex);
    pthread_cond_signal(&amp;amp;cond); //唤醒处于等待状态的消费者线程
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;等待条件变量&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;pthread_cond_wait(&amp;amp;cond, &amp;amp;mutex)&lt;/code&gt;可用于当判断某个条件不满足时，将线程设置为阻塞等待状态。该函数形参包含一个互斥锁，因为条件的检测（通常需要访问共享资源）是在互斥锁的保护下进行的，即条件本身是由互斥锁保护的。&lt;/p&gt;
&lt;p&gt;条件变量本身并不保存状态信息，只是一种通讯机制，并不需要去深究cond里到底是什么东西。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pthread_mutex_lock(&amp;amp;mutex);        // 必须先加锁
while (条件不满足) {
    pthread_cond_wait(&amp;amp;cond, &amp;amp;mutex); // 自动释放 mutex，等条件满足后再自动加锁
}
pthread_mutex_unlock(&amp;amp;mutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自旋锁(Spinlock)&lt;/h2&gt;
&lt;p&gt;自旋锁是一种忙等（busy-wait）的锁：当一个线程想获取锁但锁被占用时，它不会休眠，而是死循环反复检查锁是否释放。而互斥锁在获取不到锁时会进行阻塞（休眠），频繁在唤醒和休眠状态间切换的对于CPU的开销很大，因此互斥锁的开销远大于自旋锁，自旋锁的效率远高于互斥锁。&lt;/p&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;中断上下文、内核态（不能睡眠的地方）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自旋锁不适合锁操作时间长的场景，比如文件读写、网络操作等。&lt;/p&gt;
&lt;h3&gt;自旋锁初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 销毁自旋锁 */
int pthread_spin_destroy(pthread_spinlock_t *lock);

/* 初始化自旋锁 */
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初始化时&lt;code&gt;pshared&lt;/code&gt;表示自旋锁的进程共享属性，可选：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_PROCESS_SHARED&lt;/code&gt;：共享自旋锁。该自旋锁可以在多个进程中的线程之间共享&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_PROCESS_PRIVATE&lt;/code&gt;：私有自旋锁。只有本进程内的线程才能够使用该自旋锁&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;上锁和解锁&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 加锁，未获取到锁则自旋 */
int pthread_spin_lock(pthread_spinlock_t *lock);
/* 加锁，未获取到锁则立即返回EBUSY错误 */
int pthread_spin_trylock(pthread_spinlock_t *lock);
/* 解锁 */
int pthread_spin_unlock(pthread_spinlock_t *lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;读写锁&lt;/h2&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;不加锁状态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一次只有一个线程可以占有写模式的读写锁，但是可以有多个线程同时占有读模式的读写锁：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当读写锁处于写加锁状态时，在这个锁被解锁之前，所有试图对这个锁进行加锁操作（不管是以读模式加锁还是以写模式加锁）的线程都会被阻塞&lt;/li&gt;
&lt;li&gt;当读写锁处于读加锁状态时，所有试图以读模式对它进行加锁的线程都可以加锁成功；但是任何以写模式对它进行加锁的线程都会被阻塞，直到所有持有读模式锁的线程释放它们的锁为止&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读写锁非常适合于对共享数据读的次数远大于写的次数的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当读写锁处于写模式加锁状态时，它所保护的数据可以被安全的修改，因为一次只有一个线程可以在写模式下拥有这个锁；&lt;/li&gt;
&lt;li&gt;当读写锁处于读模式加锁状态时，它所保护的数据就可以被多个获取读模式锁的线程读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以在应用程序当中，使用读写锁实现线程同步，当线程需要对共享数据进行读操作时，需要先获取读模式锁（对读模式锁进行加锁），当读取操作完成之后再释放读模式锁（对读模式锁进行解锁）；当线程需要对共享数据进行写操作时，需要先获取到写模式锁，当写操作完成之后再释放写模式锁。&lt;/p&gt;
&lt;h1&gt;高级I/O&lt;/h1&gt;
&lt;h2&gt;阻塞与非阻塞I/O&lt;/h2&gt;
&lt;p&gt;对于普通文件，总是以非阻塞的方式进行I/O操作。对于非普通文件，需要在&lt;code&gt;open()&lt;/code&gt;函数中为形参&lt;code&gt;flags&lt;/code&gt;指定&lt;code&gt;O_NONBLOCK&lt;/code&gt;标志，&lt;code&gt;open()&lt;/code&gt;调用成功后后续的I/O操作将以非阻塞方式进行。&lt;/p&gt;
&lt;p&gt;阻塞I/O下，如果文件无数据可读，I/O会将调用者的应用程序挂起，进入休眠阻塞状态，直到有数据可读时解除阻塞。阻塞时将会释放CPU资源。如果非阻塞I/O下读文件，将会不断地进行轮询，占用较高的CPU资源。&lt;/p&gt;
&lt;h2&gt;I/O多路复用(IO multiplexing)&lt;/h2&gt;
&lt;p&gt;I/O 多路复用的本质是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;使用一个线程或进程，同时监听多个文件描述符（fd），一旦某个 fd 就绪，就进行相应操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;select()&lt;/h3&gt;
&lt;p&gt;调用select()会一直阻塞，直到一个或多个文件描述符成为就绪态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;sys/select.h&amp;gt;

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参（不关心可以设置对应为NULL）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;readfds：用来是否可读的文件描述符集合&lt;/li&gt;
&lt;li&gt;writefds：用来检测是否可写的文件描述符集合&lt;/li&gt;
&lt;li&gt;exceptfds：用来检测异常情况是否发生的文件描述符集合&lt;/li&gt;
&lt;li&gt;timeout：阻塞时间上限，为NULL则一直阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;select()将阻塞，直到：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;readfds、writefds、exceptfds指定的文件描述符中至少有一个变为就绪态&lt;/li&gt;
&lt;li&gt;该调用被信号处理函数中断&lt;/li&gt;
&lt;li&gt;超时&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件描述符集合的所有操作都可以由以下四个宏完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;sys/select.h&amp;gt;

void FD_CLR(int fd, fd_set *set);   //将fd从fdset中移除
int FD_ISSET(int fd, fd_set *set);  //如果fd是fdset中的成员，则返回true，否则返回false
void FD_SET(int fd, fd_set *set);   //将fd添加到fdset中
void FD_ZERO(fd_set *set);          //将fdset初始化为空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select有三种可能的返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;返回-1，表示有错误发生&lt;/li&gt;
&lt;li&gt;返回0， 表示调用超时&lt;/li&gt;
&lt;li&gt;返回一个正整数&lt;code&gt;n&lt;/code&gt;，表示n个文件描述符已处于就绪态&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;       
#include &amp;lt;stdlib.h&amp;gt;      
#include &amp;lt;sys/types.h&amp;gt;   
#include &amp;lt;sys/stat.h&amp;gt;   
#include &amp;lt;fcntl.h&amp;gt;       
#include &amp;lt;unistd.h&amp;gt;      
#include &amp;lt;sys/select.h&amp;gt;  
#include &amp;lt;pthread.h&amp;gt;     

#define MOUSE   &quot;/dev/input/mouse0&quot;  // 定义鼠标设备文件路径

int main(void)
{
    char buf[100];       // 用于存放读取到的数据
    int fd, ret = 0, flag;
    fd_set rdfds;        // 读文件描述符集合，用于 select 检测是否可读
    int loops = 5;       // 控制循环次数，最多轮询 5 次

    // 打开鼠标设备为非阻塞模式
    fd = open(MOUSE, O_RDONLY | O_NONBLOCK);
    if (fd == -1){
        perror(&quot;open file error\n&quot;); // 打开失败就报错并退出
        exit(-1);
    }

    // 将标准输入（fd=0，即键盘）设置为非阻塞模式
    flag = fcntl(0, F_GETFL);    // 获取当前文件描述符的标志位
    flag |= O_NONBLOCK;          // 设置非阻塞标志
    fcntl(0, F_SETFL, flag);     // 更新回去

    while(loops--){
        FD_ZERO(&amp;amp;rdfds);         // 清空读文件描述符集合
        FD_SET(0, &amp;amp;rdfds);       // 把标准输入加入监听集合（键盘输入）
        FD_SET(fd, &amp;amp;rdfds);      // 把鼠标设备加入监听集合

        // select 监听两个设备是否可读，阻塞直到有事件发生
        // 注意：第一个参数要设置为最大 fd + 1
        ret = select(fd + 1, &amp;amp;rdfds, NULL, NULL, NULL);
        if (ret &amp;lt; 0){
            perror(&quot;select error\n&quot;); // 出错就退出
            goto out;
        }
        else if (ret == 0){
            fprintf(stderr, &quot;select timeout\n&quot;); // select 超时（本例不会超时，因为 timeout 是 NULL）
            continue;
        }

        // 检查标准输入（键盘）是否就绪
        if (FD_ISSET(0, &amp;amp;rdfds)){
            ret = read(0, buf, sizeof(buf));     // 从键盘读取输入
            if (ret &amp;gt; 0){
                printf(&quot;keyboard: successfully read &amp;lt;%d&amp;gt; bytes\n&quot;, ret);
            }
        }

        // 检查鼠标设备是否就绪
        if (FD_ISSET(fd, &amp;amp;rdfds)){
            ret = read(fd, buf, sizeof(buf));    // 从鼠标读取数据
            if (ret &amp;gt; 0){
                printf(&quot;mouse: successfully read &amp;lt;%d&amp;gt; bytes\n&quot;, ret);
            }
        }
    }

out:
    close(fd);    // 关闭鼠标文件
    exit(ret);    // 正常退出
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;poll()&lt;/h3&gt;
&lt;h2&gt;异步IO&lt;/h2&gt;
&lt;p&gt;异步IO能够在发起一个 I/O 请求后立即返回，等内核完成后再通知用户，不阻塞当前线程，非常适合高性能、响应敏感的场景。即“把事交给内核干，自己不等，等内核干完再来通知”。&lt;/p&gt;
&lt;p&gt;示例（异步读取文件）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;fcntl.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;aio.h&amp;gt;
#include &amp;lt;string.h&amp;gt;

int main() {
    int fd;
    struct aiocb cb; // AIO 控制块
    char buffer[100];

    // 打开文件
    fd = open(&quot;test.txt&quot;, O_RDONLY);
    if (fd &amp;lt; 0) {
        perror(&quot;open&quot;);
        exit(1);
    }

    // 清空控制块结构体
    memset(&amp;amp;cb, 0, sizeof(struct aiocb));

    cb.aio_fildes = fd;          // 设置文件描述符
    cb.aio_buf = buffer;         // 指定接收数据的缓冲区
    cb.aio_nbytes = sizeof(buffer); // 读取字节数
    cb.aio_offset = 0;           // 从文件开头读取

    // 发起异步读请求
    if (aio_read(&amp;amp;cb) &amp;lt; 0) {
        perror(&quot;aio_read&quot;);
        close(fd);
        exit(1);
    }

    // 等待读取完成
    while (aio_error(&amp;amp;cb) == EINPROGRESS) {
        printf(&quot;Reading in progress...\n&quot;);
        sleep(1);
    }

    // 检查结果
    if (aio_error(&amp;amp;cb) != 0) {
        perror(&quot;aio_error&quot;);
    } else {
        int ret = aio_return(&amp;amp;cb); // 获取实际读取的字节数
        printf(&quot;Read %d bytes: %.*s\n&quot;, ret, ret, buffer);
    }

    close(fd);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux 进程与线程（二）线程</title><link>https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B2/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B2/</guid><description>线程是参与系统调度的最小单位。它被包含在进程之中，是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流（或者说是执行路线、执行流），一个进程中可以创建多个线程，多个线程实现并发运行，每个线程执行不同的任务。譬如某应用程序设计…</description><pubDate>Mon, 21 Apr 2025 08:27:46 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;h3&gt;线程&lt;/h3&gt;
&lt;p&gt;线程是参与系统调度的最小单位。它被包含在进程之中，是进程中的实际运行单位。一个线程指的是进程中一个单一顺序的控制流（或者说是执行路线、执行流），一个进程中可以创建多个线程，多个线程实现并发运行，每个线程执行不同的任务。譬如某应用程序设计了两个需要并发运行的任务 task1 和 task2，可将两个不同的任务分别放置在两个线程中。RTOS中的任务就是线程的一种表现形式。&lt;/p&gt;
&lt;p&gt;线程是程序最基本的运行单位，而进程不能运行，真正运行的是进程中的线程。当启动应用程序后，系统就创建了一个进程，可以认为进程仅仅是一个容器，它包含了线程运行所需的数据结构、环境变量等信息。&lt;/p&gt;
&lt;p&gt;同一进程中的多个线程将共享该进程中的全部系统资源，如虚拟地址空间，文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈（call stack，我们称为线程栈），自己的寄存器环境（register context）、自己的线程本地存储（thread-local storage）。&lt;/p&gt;
&lt;p&gt;在多线程应用程序中，通常一个进程中包括了多个线程，每个线程都可以参与系统调度、被 CPU 执行，线程具有以下一些特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不单独存在，包含在进程中&lt;/li&gt;
&lt;li&gt;线程是参与系统调度的基本单位&lt;/li&gt;
&lt;li&gt;同一进程的多线程可并发执行，宏观上实现同时运行的效果&lt;/li&gt;
&lt;li&gt;同一进程的各个线程共享该进程拥有的资源：
&lt;ul&gt;
&lt;li&gt;所有线程具有相同的地址空间&lt;/li&gt;
&lt;li&gt;线程可访问该地址空间的每一个虚地址&lt;/li&gt;
&lt;li&gt;可访问进程所拥有的已打开文件、定时器和信号量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线程与进程的优劣&lt;/h3&gt;
&lt;p&gt;多进程编程的劣势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;内存空间不共享：每个进程有独立的虚拟地址空间，数据共享麻烦，必须通过 IPC（如管道、共享内存、消息队列等）&lt;/li&gt;
&lt;li&gt;创建开销大：使用 fork() 创建新进程代价高（复制页表、上下文等）。频繁创建/销毁进程会显著降低性能&lt;/li&gt;
&lt;li&gt;通信复杂：不同进程间通信（IPC）机制种类多、实现复杂，尤其是同步控制（例如多个进程同时写共享内存）容易出错&lt;/li&gt;
&lt;li&gt;切换成本高：进程上下文切换比线程慢很多，因为需要切换整个地址空间、页表等&lt;/li&gt;
&lt;li&gt;资源管理复杂：每个进程资源独立，协调管理（如关闭文件描述符、回收子进程）更复杂&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多线程编程的优势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;共享地址空间：线程共享进程的内存，通信和共享数据非常方便，比如共享变量、缓冲区&lt;/li&gt;
&lt;li&gt;创建/销毁成本小：创建线程用 pthread_create() 等，比 fork() 快很多，开销更小&lt;/li&gt;
&lt;li&gt;上下文切换快：线程切换只需切换寄存器、栈等，不涉及页表等操作，切换代价低&lt;/li&gt;
&lt;li&gt;通信简单：直接通过全局变量或堆内存通信，不需要复杂的 IPC 机制&lt;/li&gt;
&lt;li&gt;适合高并发任务：在 Web 服务器、实时系统中常见，用来处理高并发连接更高效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多线程编程的劣势：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据竞争：线程共享数据，必须使用互斥锁、读写锁等手段保护，稍不注意就出 bug&lt;/li&gt;
&lt;li&gt;调试困难：多线程问题（死锁、竞态条件）很难重现和排查&lt;/li&gt;
&lt;li&gt;一个线程崩溃可能导致整个进程崩溃&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;并发和并行&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;串行：一件事、一件事接着做
&lt;ul&gt;
&lt;li&gt;只用一口锅，先做完第一碗面，再做第二碗，最后做第三碗&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并发：交替做不同的事
&lt;ul&gt;
&lt;li&gt;烧水的时候准备下碗；水烧着时你不闲着；一会煮面，一会加料；只要锅没闲着，切来切去地忙&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;并行：同时做不同的事
&lt;ul&gt;
&lt;li&gt;3个厨师 + 3口锅，每人煮一碗面，互不干扰&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于单核处理器，只有一个执行单元，因此同时只能执行一条指令；对于多核处理器，可并行执行多条指令，譬如8核处理器可并行执行8条不同的指令。&lt;/p&gt;
&lt;h3&gt;线程ID&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

pthread_t pthread_self(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数返回当前线程的线程ID。&lt;/p&gt;
&lt;p&gt;还可以使用&lt;code&gt;pthread_equal()&lt;/code&gt;函数检查两个线程ID是否相等：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_equal(pthread_t t1, pthread_t t2);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果相等，返回非零值；否则返回0.&lt;/p&gt;
&lt;h3&gt;创建线程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;thread&lt;/code&gt;：函数成功返回时，创建成功的线程ID会保存在thread指向的内存中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;attr&lt;/code&gt;：定义了线程的各种属性，如果设置为NULL，表示所有属性设为默认值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;start_routine&lt;/code&gt;：新创建的线程从start_routine()函数开始执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;arg&lt;/code&gt;：要传递给start_routine()函数的参数，一般情况下需要将&lt;code&gt;arg&lt;/code&gt;指向一个全局或堆变量，即需要保证线程的生命周期中&lt;code&gt;arg&lt;/code&gt;指向的对象必须存在，否则会出现段错误。如果设置为NULL，表示不需要传入参数。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和进程创建时相同，一旦线程创建成功，新线程就会立即被加入到系统调度队列并开始执行，通常无法确定系统会首先调度哪一个线程来使用CPU资源（主线程或新创建的线程）。如果程序对不同线程的执行顺序有要求，就必须采用一些同步技术来实现。&lt;/p&gt;
&lt;p&gt;简单的创建线程的例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

static void *new_theard_start(void *arg)
{
    printf(&quot;新线程：进程ID&amp;lt;%d&amp;gt;，线程ID&amp;lt;%lu&amp;gt;\n&quot;, getpid(), pthread_self());
    return (void *)0;
}

int main(void)
{
    pthread_t tid;
    int ret;

    ret = pthread_create(&amp;amp;tid, NULL, new_theard_start, NULL);
    if (ret){
        fprintf(stderr, &quot;Error:%s\n&quot;, strerror(ret));
        exit(-1);
    }

    printf(&quot;主线程：进程ID&amp;lt;%d&amp;gt;，线程ID&amp;lt;%lu&amp;gt;\n&quot;, getpid(), pthread_self());
    sleep(1);
    exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;终止线程&lt;/h3&gt;
&lt;p&gt;线程可以通过以下方式终止：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程的start函数执行return并返回指定值，返回值就是线程的退出码&lt;/li&gt;
&lt;li&gt;线程调用&lt;code&gt;pthread_exit()&lt;/code&gt;函数&lt;/li&gt;
&lt;li&gt;调用&lt;code&gt;pthread_cancel()&lt;/code&gt;取消线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_exit()&lt;/code&gt;将终止调用它的线程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

void pthread_exit(void *retval);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参retval指定了线程的返回值，也就是线程的退出码。retval指向的内容不应分配在线程栈中，因为线程终止后将无法确认线程栈中的内存还是否有效。&lt;/p&gt;
&lt;h3&gt;回收线程&lt;/h3&gt;
&lt;p&gt;可调用&lt;code&gt;pthread_join()&lt;/code&gt;函数来&lt;strong&gt;阻塞&lt;/strong&gt;等待线程的终止，并获取线程的退出码，回收线程资源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_join(pthread_t thread, void **retval);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;retval&lt;/code&gt;：如果不为NULL，函数将目标线程的退出状态复制到&lt;code&gt;*retval&lt;/code&gt;；如果目标线程被cancel，则将&lt;code&gt;PTHREAD_CANCELED&lt;/code&gt;放在&lt;code&gt;*retval&lt;/code&gt;中；如果对目标线程的终止状态不感兴趣，可设置为NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_join()&lt;/code&gt;类似于针对进程的&lt;code&gt;waitpid()&lt;/code&gt;，但是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程之间关系是平等的。同一进程中的任意线程均可调用&lt;code&gt;pthread_join()&lt;/code&gt;来等待另一个线程的终止&lt;/li&gt;
&lt;li&gt;不能以非阻塞的方式调用&lt;code&gt;pthread_join()&lt;/code&gt;。&lt;code&gt;pthread_join()&lt;/code&gt;一旦被调用，对应线程即阻塞。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例子：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

static void *new_theard_start(void *arg)
{
    /* 线程创建2秒后即终止 */
    printf(&quot;New thread start\n&quot;);
    sleep(2);
    printf(&quot;New thread end\n&quot;);
    pthread_exit((void *)10);
}

int main(void)
{
    pthread_t tid;
    void *tret;
    int ret;

    ret = pthread_create(&amp;amp;tid, NULL, new_theard_start, NULL);
    if (ret){
        fprintf(stderr, &quot;Error:%s\n&quot;, strerror(ret));
        exit(-1);
    }

    /* 阻塞等待tid结束 */
    ret = pthread_join(tid, &amp;amp;tret);
    if (ret){
        fprintf(stderr, &quot;pthread join error: %s\n&quot;, strerror(ret));
        exit(-1);
    }
    printf(&quot;New thread end, code = %ld\n&quot;, (long)tret);

    exit(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;取消线程&lt;/h3&gt;
&lt;p&gt;取消线程特指某一个线程向另一个线程发送请求，要求其立即退出的操作。&lt;/p&gt;
&lt;h4&gt;取消一个线程&lt;/h4&gt;
&lt;p&gt;通过&lt;code&gt;pthread_cancel()&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_cancel(pthread_t thread);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数仅提出请求，不会等待目标线程退出，而是会立即返回。&lt;/p&gt;
&lt;h4&gt;取消状态和类型&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pthread_setcancelstate&lt;/code&gt;和&lt;code&gt;pthread_setcanceltype&lt;/code&gt;可用于设置线程的取消性状态和类型。&lt;code&gt;pthread_setcancelstate&lt;/code&gt;会将调用线程的取消性状态设置为state，并将线程之前的取消性状态保存在oldstate中。如果对旧状态不感兴趣，可设置为NULL。state必须是以下值之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CANCEL_ENABLE&lt;/code&gt;：线程可以取消，这是新创建的线程取消性状态的默认值，所以新建线程以及主线程默认都是可以取消的&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CANCEL_DISABLE&lt;/code&gt;：线程不可被取消，如果此类线程接收到取消请求，则会将请求挂起，直至线程的取消性状态变为 &lt;code&gt;PTHREAD_CANCEL_ENABLE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void *new_theard_start(void *arg)
{
    /* 设置线程为不可取消 */
    pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);

    for (; ;)
    {
        printf(&quot;new thread is running\r\n&quot;);
        sleep(2);
    }
    return (void *) 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果线程的取消性状态为ENABLE，那么其对取消请求的处理取决于线程的取消性类型，可通过&lt;code&gt;pthread_setcanceltype()&lt;/code&gt;来设置。type必须是以下值之一：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CANCEL_DEFERRED&lt;/code&gt;：取消请求到来时，线程还是继续运行，取消请求被挂起，直到线程到达某个取消点（cancellation point）为止，这是所有新建线程包括主线程默认的取消性类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CANCEL_ASYNCHRONOUS&lt;/code&gt;：很少使用。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;取消点&lt;/h4&gt;
&lt;p&gt;当线程可以取消并且canceltype设置为&lt;code&gt;PTHREAD_CANCEL_DEFERRED&lt;/code&gt;时，当收到其他线程通过&lt;code&gt;pthread_cancel()&lt;/code&gt;发送到来的取消请求时，仅当线程抵达某个取消点时，取消请求才会起作用。&lt;/p&gt;
&lt;p&gt;取消点的作用是更精确的设置响应取消请求的位置，未到达取消点时，已经到达的取消请求将不会被响应。这是系统的一种保护机制，用于保证取消请求不会影响到关键代码的执行。&lt;code&gt;man 7 pthreads&lt;/code&gt;可查询可用作取消点的函数。&lt;/p&gt;
&lt;h4&gt;可取消性的检测&lt;/h4&gt;
&lt;p&gt;如果线程执行的是一个无取消点的死循环，比如&lt;code&gt;while(1)&lt;/code&gt;，那么似乎这时候线程永远都不会响应外部的取消请求；除了线程自己主动退出，其他线程将无法通过向它发送取消请求而终止它。&lt;/p&gt;
&lt;p&gt;此时可以使用&lt;code&gt;pthread_testcancel&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

void pthread_testcancel(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数会产生一个取消点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void *new_theard_start(void *arg)
{
    printf(&quot;new thread is running\r\n&quot;);
    for (; ;){
        /* 人为产生取消点 */
        pthread_testcancel();
    }
    return (void *) 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;分离线程&lt;/h3&gt;
&lt;p&gt;默认情况下，当线程终止时，其它线程可以通过调用&lt;code&gt;pthread_join()&lt;/code&gt;获取其返回状态、回收线程资源。有时，代码并不关心线程的返回状态，也不打算join它，只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下，可以调用&lt;code&gt;pthread_detach()&lt;/code&gt;将指定线程进行分离，也就是分离线程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

int pthread_detach(pthread_t thread);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个线程可以将另一个线程分离，也可以将自己分离：&lt;code&gt;pthread_detach(pthread_self())&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;分离之后的线程无法通过&lt;code&gt;pthread_join&lt;/code&gt;获取终止状态，该过程不可逆，分离之后就无法恢复到之前的状态。处于分离状态的线程终止后能够自动回收线程资源。&lt;/p&gt;
&lt;p&gt;线程的分离状态通过&lt;code&gt;pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)&lt;/code&gt;设置，参数可选为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PTHREAD_CREATE_DETACHED&lt;/code&gt;：分离线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PTHREAD _CREATE_JOINABLE&lt;/code&gt;：非分离线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：如果一个线程被设置为分离线程，而该线程运行又非常快，很有可能在&lt;code&gt;pthread_create()&lt;/code&gt;返回之前该线程就终止了，终止之后它就可能将线程号和系统资源移交给其他线程，这样&lt;code&gt;pthread_create()&lt;/code&gt;可能就会返回错误的线程号。避免这种情况最常见的措施就是同步通知，比如在被创建的线程里调用&lt;code&gt;pthread_cond_timedwait()&lt;/code&gt;函数，让线程等待一会儿，留出足够的时间让
&lt;code&gt;pthread_create()&lt;/code&gt;返回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int pthread_cond_timedwait(pthread_cond_t *cond, 
                           pthread_mutex_t *mutex, 
                           const struct timespec *abstime);

&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cond&lt;/code&gt;：条件变量指针，是一个事件通知器，用于判断某种条件是否成立&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mutex&lt;/code&gt;：互斥锁指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;abstime&lt;/code&gt;：绝对时间，等待超时的截止时间，单位为秒+纳秒
注意：&lt;code&gt;pthread_cond_timedwait()&lt;/code&gt; 等待的是 “到指定时间点” 为止，而不是 “等待N秒”！&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_cond_timedwait()&lt;/code&gt;本质上是一个带锁的同步等待机制，目的是等待某个线程间的“条件”成立。它会阻塞线程，但可以通过事件唤醒，还可以设置超时时间，防止死锁。适合：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;等待某个事件的发生，如数据就绪或任务完成&lt;/li&gt;
&lt;li&gt;在一段时间内等待，而非无限等待&lt;/li&gt;
&lt;li&gt;希望在等待期间自动释放资源&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该函数一般不能用&lt;code&gt;sleep()&lt;/code&gt;和&lt;code&gt;wait()&lt;/code&gt;代替：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sleep()&lt;/code&gt;只是简单的挂起线程，无法被事件唤醒，也无法和条件变量配合使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wait()&lt;/code&gt;用于进程，不能用于线程同步&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应用示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;time.h&amp;gt;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

static void *thread_func(void *arg)
{
    printf(&quot;线程启动了（ID=%lu）\n&quot;, pthread_self());

    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &amp;amp;ts);
    ts.tv_sec += 1;  // 等待1秒钟

    pthread_mutex_lock(&amp;amp;mutex);
    // 等待条件变量1秒，让主线程有足够时间返回pthread_create()
    pthread_cond_timedwait(&amp;amp;cond, &amp;amp;mutex, &amp;amp;ts);
    pthread_mutex_unlock(&amp;amp;mutex);

    printf(&quot;线程准备退出\n&quot;);

    pthread_exit(NULL);
}

int main()
{
    pthread_t tid;
    pthread_attr_t attr;

    pthread_attr_init(&amp;amp;attr);
    pthread_attr_setdetachstate(&amp;amp;attr, PTHREAD_CREATE_DETACHED);  // 设置为分离状态

    if (pthread_create(&amp;amp;tid, &amp;amp;attr, thread_func, NULL) != 0) {
        perror(&quot;pthread_create error&quot;);
        exit(EXIT_FAILURE);
    }

    // 保证pthread_create返回前线程还在运行
    printf(&quot;主线程：pthread_create成功，线程ID=%lu\n&quot;, tid);

    pthread_attr_destroy(&amp;amp;attr);

    sleep(2);  // 等待子线程退出

    printf(&quot;主线程退出\n&quot;);
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;线程清理函数(Thread cleanup handler)&lt;/h3&gt;
&lt;p&gt;线程可通过函数 &lt;code&gt;pthread_cleanup_push()&lt;/code&gt;和 &lt;code&gt;pthread_cleanup_pop()&lt;/code&gt;分别负责向调用线程的清理函数栈中添加和移除清理函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;pthread.h&amp;gt;

void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;pthread_cleanup_push()&lt;/code&gt;用于注册一个清理函数&lt;code&gt;routine(arg)&lt;/code&gt;，&lt;code&gt;pthread_cleanup_pop()&lt;/code&gt;负责移除栈顶的cleanup handler，如果&lt;code&gt;excute != 0&lt;/code&gt;，则调用它。&lt;/p&gt;
&lt;p&gt;线程执行以下动作时，清理函数栈中的清理函数才会被执行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;pthread_exit()&lt;/code&gt;退出时&lt;/li&gt;
&lt;li&gt;响应取消请求时(&lt;code&gt;pthread_cancel&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;用非0参数调用&lt;code&gt;pthread_cleanup_pop()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;pthread_cleanup_push()&lt;/code&gt;和 &lt;code&gt;pthread_cleanup_pop()&lt;/code&gt;实际上是两个宏，必须成对出现，并在同一个函数作用域中使用。&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void cleanup(void *arg)
{
    printf(&quot;cleanup: %s\n&quot;, (char *)arg);
}

static void *new_thread_start(void *arg)
{
    printf(&quot;新线程--start run\n&quot;);
    pthread_cleanup_push(cleanup, &quot;第 1 次调用&quot;);
    pthread_cleanup_push(cleanup, &quot;第 2 次调用&quot;);
    pthread_cleanup_push(cleanup, &quot;第 3 次调用&quot;);

    pthread_cleanup_pop(1); //执行最顶层的清理函数
    printf(&quot;~~~~~~~~~~~~~~~~~\n&quot;);
    sleep(2);
    pthread_exit((void *)0); //线程终止

    /* 为了与 pthread_cleanup_push 配对 */
    pthread_cleanup_pop(0);
    pthread_cleanup_pop(0);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;线程属性&lt;/h3&gt;
&lt;p&gt;线程属性是&lt;code&gt;pthread_attr_t&lt;/code&gt;类型的，其在线程创建前被创建、初始化和修改，并在线程创建时通过形参传入线程。&lt;/p&gt;
&lt;h4&gt;初始化与销毁属性对象&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_init(pthread_attr_t *attr);&lt;/code&gt;&lt;br /&gt;
// 初始化线程属性对象&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_destroy(pthread_attr_t *attr);&lt;/code&gt;&lt;br /&gt;
// 销毁属性对象，释放资源&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设置线程是否可分离（joinable/detached）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);&lt;/code&gt;&lt;br /&gt;
// 设置线程分离属性（&lt;code&gt;PTHREAD_CREATE_JOINABLE&lt;/code&gt; 或 &lt;code&gt;PTHREAD_CREATE_DETACHED&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);&lt;/code&gt;&lt;br /&gt;
// 获取当前分离属性状态&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设置线程栈大小与起始地址&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);&lt;/code&gt;&lt;br /&gt;
// 设置线程栈大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);&lt;/code&gt;&lt;br /&gt;
// 获取线程栈大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);&lt;/code&gt;&lt;br /&gt;
// 设置线程栈地址和大小&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);&lt;/code&gt;&lt;br /&gt;
// 获取线程栈地址和大小&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设置线程调度策略和参数&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);&lt;/code&gt;&lt;br /&gt;
// 设置调度策略（如 &lt;code&gt;SCHED_FIFO&lt;/code&gt;, &lt;code&gt;SCHED_RR&lt;/code&gt;, &lt;code&gt;SCHED_OTHER&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy);&lt;/code&gt;&lt;br /&gt;
// 获取调度策略&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);&lt;/code&gt;&lt;br /&gt;
// 设置调度参数（如优先级）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);&lt;/code&gt;&lt;br /&gt;
// 获取调度参数&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设置调度继承属性&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setinheritsched(pthread_attr_t *attr, int inherit);&lt;/code&gt;&lt;br /&gt;
// 设置调度属性是否继承（&lt;code&gt;PTHREAD_INHERIT_SCHED&lt;/code&gt; 或 &lt;code&gt;PTHREAD_EXPLICIT_SCHED&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getinheritsched(const pthread_attr_t *attr, int *inherit);&lt;/code&gt;&lt;br /&gt;
// 获取调度继承属性&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h4&gt;设置线程作用域（系统级 / 进程级）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_setscope(pthread_attr_t *attr, int scope);&lt;/code&gt;&lt;br /&gt;
// 设置作用域（&lt;code&gt;PTHREAD_SCOPE_SYSTEM&lt;/code&gt; 或 &lt;code&gt;PTHREAD_SCOPE_PROCESS&lt;/code&gt;）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;int pthread_attr_getscope(const pthread_attr_t *attr, int *scope);&lt;/code&gt;&lt;br /&gt;
// 获取线程作用域&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;代码范例&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;pthread.h&amp;gt;
#include &amp;lt;string.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;

static void *thread_1_start(void *arg)
{
    size_t stacksize;

    /* 获取线程栈大小 */
    pthread_attr_getstacksize((pthread_attr_t *)arg, &amp;amp;stacksize);

    printf(&quot;Thread 1 start, stack size: %zu\n&quot;, stacksize);

    sleep(2);

    /* 退出线程 */
    pthread_exit(NULL);
}

int main()
{
    int ret;
    pthread_t thread_1;
    pthread_attr_t thread_1_attr;

    /* 初始化线程属性 */
    pthread_attr_init(&amp;amp;thread_1_attr);

    /* 设置线程为分离态 */
    pthread_attr_setdetachstate(&amp;amp;thread_1_attr, PTHREAD_CREATE_DETACHED);

    /* 设置线程栈大小 */
    ret = pthread_attr_setstacksize(&amp;amp;thread_1_attr, 88888);
    if (ret != 0){
        fprintf(stderr, &quot;pthread_attr_setstacksize failed: %s\n&quot;, strerror(ret));
        exit(-1);
    }

    /* 创建线程 */
    ret = pthread_create(&amp;amp;thread_1, &amp;amp;thread_1_attr, thread_1_start, &amp;amp;thread_1_attr);

    /* 销毁线程属性 */
    pthread_attr_destroy(&amp;amp;thread_1_attr);

    sleep(3);

    /* 退出线程 */
    pthread_exit(NULL);
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux RTL8188EUS Wifi驱动调试</title><link>https://arkmostima.pages.dev/posts/linuxwifi%E9%A9%B1%E5%8A%A8%E8%B0%83%E8%AF%95/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxwifi%E9%A9%B1%E5%8A%A8%E8%B0%83%E8%AF%95/</guid><description>正点原子提供的RTL8188EUS只能用在低版本Kernel，高版本Kernel会出现编译错误，比如我用的6.6.52版本的Kernel就无法编译正点原子提供的那几个Realtek的驱动文件。</description><pubDate>Thu, 17 Apr 2025 12:35:06 GMT</pubDate><content:encoded>&lt;p&gt;正点原子提供的RTL8188EUS只能用在低版本Kernel，高版本Kernel会出现编译错误，比如我用的6.6.52版本的Kernel就无法编译正点原子提供的那几个Realtek的驱动文件。&lt;/p&gt;
&lt;p&gt;尝试从网上找能够支持新版本Kernel的RTL8188EUS的驱动，Google出来第一位是这个：
https://github.com/aircrack-ng/rtl8188eus&lt;/p&gt;
&lt;p&gt;看了一圈，这个仓库有1k的star，并且最新支持到了6.12版本的Kernel，看上去很不错，下载下来，准备在虚拟机里交叉编译成arm版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export KDIR=~/linux/NXP_linux/linux-imx-lf-6.6.y

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- KSRC=$KDIR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的KDIR要设置成自己的Linux Kernel源码目录，不能用默认的，否则编译出来的是x86架构，没法在板子上用。&lt;/p&gt;
&lt;p&gt;正常状况下编译会得到8188eu.ko这个模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8188eu.ko     BUILD_FOR_NETHUNTER.md  dkms-remove.sh  modules.order   ReleaseNotes.pdf
8188eu.mod    built-in.a              hal             Module.symvers
8188eu.mod.c  core                    include         os_dep
8188eu.mod.o  dkms.conf               Kconfig         platform
8188eu.o      dkms-install.sh         Makefile        README.md
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把模块拷贝到根目录下/lib/modules/6.6.52中，重启开发板，尝试加载模块，Kernel报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[   77.296944] 8188eu: loading out-of-tree module taints kernel.
[   77.306670] 8188eu: Unknown symbol __cfg80211_alloc_event_skb (err -2)
[   77.316458] 8188eu: Unknown symbol wiphy_register (err -2)
[   77.325178] 8188eu: Unknown symbol cfg80211_remain_on_channel_expired (err -2)
[   77.335632] 8188eu: Unknown symbol cfg80211_vendor_cmd_reply (err -2)
[   77.345288] 8188eu: Unknown symbol cfg80211_del_sta_sinfo (err -2)
[   77.354656] 8188eu: Unknown symbol wiphy_unregister (err -2)
[   77.363749] 8188eu: Unknown symbol cfg80211_ch_switch_notify (err -2)
[   77.373440] 8188eu: Unknown symbol ieee80211_get_channel_khz (err -2)
[   77.383033] 8188eu: Unknown symbol cfg80211_ready_on_channel (err -2)
[   77.392592] 8188eu: Unknown symbol wiphy_free (err -2)
[   77.400814] 8188eu: Unknown symbol wiphy_new_nm (err -2)
[   77.409047] 8188eu: Unknown symbol cfg80211_get_bss (err -2)
[   77.417488] 8188eu: Unknown symbol ieee80211_freq_khz_to_channel (err -2)
[   77.427077] 8188eu: Unknown symbol __cfg80211_send_event_skb (err -2)
[   77.436316] 8188eu: Unknown symbol cfg80211_disconnected (err -2)
[   77.445217] 8188eu: Unknown symbol cfg80211_michael_mic_failure (err -2)
[   77.454688] 8188eu: Unknown symbol cfg80211_ibss_joined (err -2)
[   77.463463] 8188eu: Unknown symbol cfg80211_scan_done (err -2)
[   77.471772] 8188eu: Unknown symbol cfg80211_roamed (err -2)
[   77.479798] 8188eu: Unknown symbol cfg80211_put_bss (err -2)
[   77.487786] 8188eu: Unknown symbol cfg80211_connect_done (err -2)
[   77.496214] 8188eu: Unknown symbol cfg80211_unlink_bss (err -2)
[   77.504461] 8188eu: Unknown symbol cfg80211_inform_bss_frame_data (err -2)
[   77.513599] 8188eu: Unknown symbol cfg80211_new_sta (err -2)
[   77.521513] 8188eu: Unknown symbol __cfg80211_alloc_reply_skb (err -2)
[   77.530336] 8188eu: Unknown symbol cfg80211_rx_mgmt_ext (err -2)
[   77.538526] 8188eu: Unknown symbol wiphy_apply_custom_regulatory (err -2)
[   77.547557] 8188eu: Unknown symbol cfg80211_mgmt_tx_status_ext (err -2)
[   77.656978] 8188eu: Unknown symbol __cfg80211_alloc_event_skb (err -2)
[   77.665898] 8188eu: Unknown symbol wiphy_register (err -2)
[   77.673436] 8188eu: Unknown symbol cfg80211_remain_on_channel_expired (err -2)
[   77.682597] 8188eu: Unknown symbol cfg80211_vendor_cmd_reply (err -2)
[   77.691051] 8188eu: Unknown symbol cfg80211_del_sta_sinfo (err -2)
[   77.699152] 8188eu: Unknown symbol wiphy_unregister (err -2)
[   77.706787] 8188eu: Unknown symbol cfg80211_ch_switch_notify (err -2)
[   77.715234] 8188eu: Unknown symbol ieee80211_get_channel_khz (err -2)
[   77.723608] 8188eu: Unknown symbol cfg80211_ready_on_channel (err -2)
[   77.732000] 8188eu: Unknown symbol wiphy_free (err -2)
[   77.739088] 8188eu: Unknown symbol wiphy_new_nm (err -2)
[   77.746251] 8188eu: Unknown symbol cfg80211_get_bss (err -2)
[   77.753799] 8188eu: Unknown symbol ieee80211_freq_khz_to_channel (err -2)
[   77.762421] 8188eu: Unknown symbol __cfg80211_send_event_skb (err -2)
[   77.770861] 8188eu: Unknown symbol cfg80211_disconnected (err -2)
[   77.778911] 8188eu: Unknown symbol cfg80211_michael_mic_failure (err -2)
[   77.787562] 8188eu: Unknown symbol cfg80211_ibss_joined (err -2)
[   77.795674] 8188eu: Unknown symbol cfg80211_scan_done (err -2)
[   77.803474] 8188eu: Unknown symbol cfg80211_roamed (err -2)
[   77.810850] 8188eu: Unknown symbol cfg80211_put_bss (err -2)
[   77.818533] 8188eu: Unknown symbol cfg80211_connect_done (err -2)
[   77.826583] 8188eu: Unknown symbol cfg80211_unlink_bss (err -2)
[   77.834431] 8188eu: Unknown symbol cfg80211_inform_bss_frame_data (err -2)
[   77.843325] 8188eu: Unknown symbol cfg80211_new_sta (err -2)
[   77.850940] 8188eu: Unknown symbol __cfg80211_alloc_reply_skb (err -2)
[   77.859411] 8188eu: Unknown symbol cfg80211_rx_mgmt_ext (err -2)
[   77.867403] 8188eu: Unknown symbol wiphy_apply_custom_regulatory (err -2)
[   77.876210] 8188eu: Unknown symbol cfg80211_mgmt_tx_status_ext (err -2)
modprobe: can&apos;t load module 8188eu.ko (8188eu.ko): unknown symbol in module, or unknown parameter
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这一看就是CFG80211编译报错了，跑了一下&lt;code&gt;dmesg | grep cfg80211&lt;/code&gt;发现CFG80211根本没有加载，但是我之前明明配置好了开机自动加载的，不知道哪里把配置弄丢了，于是手动&lt;code&gt;depmod cfg80211&lt;/code&gt;，然后再&lt;code&gt;depmod 8188eu.ko&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/lib/modules/6.6.52 # modprobe  cfg80211.ko 
[   18.046491] cfg80211: Loading compiled-in X.509 certificates for regulatory database
[   18.123855] Loaded X.509 cert &apos;sforshee: 00b28ddf47aef9cea7&apos;
[   18.138554] Loaded X.509 cert &apos;wens: 61c038651aabdcf94bd0ac7ff06c7248db18c600&apos;
/lib/modules/6.6.52 # modprobe 8188eu.ko 
[   25.931742] 8188eu: loading out-of-tree module taints kernel.
[   25.959171] bFWReady == _FALSE call reset 8051...
[   26.008887] usbcore: registered new interface driver 8188eu
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再&lt;code&gt;ifconfig -a&lt;/code&gt;，可以看到WLAN0信息，驱动工作成功。&lt;/p&gt;
&lt;p&gt;关于cfg80211，之前为了避免出现cfg80211: failed to load regulatory.db的报错信息，参考&lt;a href=&quot;https://blog.csdn.net/wangfei19841001/article/details/141023775?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-141023775-blog-140298343.235%5Ev43%5Epc_blog_bottom_relevance_base3&amp;amp;spm=1001.2101.3001.4242.1&amp;amp;utm_relevant_index=2&quot;&gt;文章1&lt;/a&gt;和&lt;a href=&quot;https://blog.csdn.net/m0_61320035/article/details/140298343?spm=1001.2014.3001.5506&quot;&gt;文章2&lt;/a&gt;将CFG80211配置成了模块加载形式，也就是&lt;code&gt;CONFIG_CFG80211=m&lt;/code&gt;，不随Kernel启动。但是Kernel编译时会正常编译cfg80211.ko，位于&lt;code&gt;./net/wireless/cfg80211.ko&lt;/code&gt;，将它拷贝到&lt;code&gt;/lib/modules/6.6.52&lt;/code&gt;里手动modprobe就可以。&lt;/p&gt;
</content:encoded></item><item><title>Linux FlexCAN调试</title><link>https://arkmostima.pages.dev/posts/linuxflexcan%E8%B0%83%E8%AF%95/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxflexcan%E8%B0%83%E8%AF%95/</guid><description>按照手册来即可，注意Alpha版上Flexcan2节点的引脚被ecspi3复用，节点中要屏蔽掉flexcan2:</description><pubDate>Thu, 17 Apr 2025 06:40:07 GMT</pubDate><content:encoded>&lt;h2&gt;设备树&lt;/h2&gt;
&lt;p&gt;按照手册来即可，注意Alpha版上Flexcan2节点的引脚被ecspi3复用，节点中要屏蔽掉flexcan2:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Conflict with ECSPI3 */
&amp;amp;can2 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_flexcan2&amp;gt;;
	xceiver-supply = &amp;lt;&amp;amp;reg_can_3v3&amp;gt;;
	status = &quot;disabled&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原厂设备树节点中&lt;code&gt;xceiver-supply = &amp;lt;&amp;amp;reg_can_3v3&amp;gt;;&lt;/code&gt;这个属性可能会导致deferred probing，让CANprobe不出来，屏蔽掉这个属性就可以，完成后的节点如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;can1 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_flexcan1&amp;gt;;
	//xceiver-supply = &amp;lt;&amp;amp;reg_can_3v3&amp;gt;;
	status = &quot;okay&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完成后&lt;code&gt;ifconfig -a&lt;/code&gt;应该可以看得到CAN节点信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/ # ifconfig -a
can0      Link encap:UNSPEC  HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00  
          NOARP  MTU:16  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:10 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
          Interrupt:206 

eth0      Link encap:Ethernet  HWaddr EA:36:EF:87:91:0F  
          inet addr:192.168.1.103  Bcast:192.168.1.255  Mask:255.255.255.0
          inet6 addr: fe80::e836:efff:fe87:910f/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:3294 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1444 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:4472406 (4.2 MiB)  TX bytes:259612 (253.5 KiB)

eth1      Link encap:Ethernet  HWaddr C6:F2:80:26:D2:07  
          BROADCAST MULTICAST  MTU:1500  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

lo        Link encap:Local Loopback  
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

sit0      Link encap:IPv6-in-IPv4  
          NOARP  MTU:1480  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;iproute库编译&lt;/h2&gt;
&lt;p&gt;经过试验，新版本的iproute确实会出现找不到诸如libelf头文件等奇怪的情况，暂未深究原因。&lt;/p&gt;
&lt;p&gt;编译时可能报错：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;错误： 对‘setns’的静态声明出现在非静态声明之后
方法是打开iproute根目录/include/namespace.h文件，注释掉下面几行：&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;#ifndef HAVE_SETNS
static inline int setns(int fd, int nstype)
{
#ifdef __NR_setns
	return syscall(__NR_setns, fd, nstype);
#else
	errno = ENOSYS;
	return -1;
#endif
}
#endif /* HAVE_SETNS */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重新编译&lt;/p&gt;
</content:encoded></item><item><title>LinuxWM8960和声卡驱动</title><link>https://arkmostima.pages.dev/posts/linuxwm8960%E5%92%8C%E5%A3%B0%E5%8D%A1%E9%A9%B1%E5%8A%A8/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxwm8960%E5%92%8C%E5%A3%B0%E5%8D%A1%E9%A9%B1%E5%8A%A8/</guid><description>6.6.52版本Kernel中NXP已经默认在imx6ul-14x14-evk.dtsi中配置好了wm8960的节点，但是是配置在i2c2下，而正点原子阿尔法开发板中wm8960接在i2c1下，所以要把节点从i2c2移到i2c1：</description><pubDate>Wed, 16 Apr 2025 10:33:12 GMT</pubDate><content:encoded>&lt;h2&gt;设备树&lt;/h2&gt;
&lt;p&gt;6.6.52版本Kernel中NXP已经默认在imx6ul-14x14-evk.dtsi中配置好了wm8960的节点，但是是配置在i2c2下，而正点原子阿尔法开发板中wm8960接在i2c1下，所以要把节点从i2c2移到i2c1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;i2c1 {
	clock-frequency = &amp;lt;100000&amp;gt;;
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_i2c1&amp;gt;;
	status = &quot;okay&quot;;

    /* 省略其他节点 */

	codec: wm8960@1a {
		#sound-dai-cells = &amp;lt;0&amp;gt;;
		compatible = &quot;wlf,wm8960&quot;;
		reg = &amp;lt;0x1a&amp;gt;;
		wlf,shared-lrclk;
		wlf,hp-cfg = &amp;lt;3 2 3&amp;gt;;
		wlf,gpio-cfg = &amp;lt;1 3&amp;gt;;
		clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_SAI2&amp;gt;;
		clock-names = &quot;mclk&quot;;
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根节点下还需要创建一个sound节点，NXP默认也已经配置好了：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/ {
    /* 省略其他节点 */

    sound-wm8960 {
		compatible = &quot;fsl,imx-audio-wm8960&quot;;
		model = &quot;wm8960-audio&quot;;
		audio-cpu = &amp;lt;&amp;amp;sai2&amp;gt;;
		audio-codec = &amp;lt;&amp;amp;codec&amp;gt;;
		audio-asrc = &amp;lt;&amp;amp;asrc&amp;gt;;
		hp-det-gpio = &amp;lt;&amp;amp;gpio5 4 0&amp;gt;;
		audio-routing =
			&quot;Headphone Jack&quot;, &quot;HP_L&quot;,
			&quot;Headphone Jack&quot;, &quot;HP_R&quot;,
			&quot;Ext Spk&quot;, &quot;SPK_LP&quot;,
			&quot;Ext Spk&quot;, &quot;SPK_LN&quot;,
			&quot;Ext Spk&quot;, &quot;SPK_RP&quot;,
			&quot;Ext Spk&quot;, &quot;SPK_RN&quot;,
			&quot;LINPUT2&quot;, &quot;Mic Jack&quot;,
			&quot;LINPUT3&quot;, &quot;Mic Jack&quot;,
			&quot;RINPUT1&quot;, &quot;AMIC&quot;,
			&quot;RINPUT2&quot;, &quot;AMIC&quot;,
			&quot;Mic Jack&quot;, &quot;MICB&quot;,
			&quot;AMIC&quot;, &quot;MICB&quot;;
		status = &quot;okay&quot;;		/* must add this */
	};
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意最后的&lt;code&gt;status = &quot;okay&quot;&lt;/code&gt;必须要加，不然ALSA识别不到。&lt;/p&gt;
&lt;p&gt;Menuconfig里默认已经启用了wm8960在imx系列board上的区别，ALSA模拟OSS的API也已经关闭，保持默认即可。&lt;/p&gt;
&lt;h2&gt;alsa-lib &amp;amp; alsa-utils&lt;/h2&gt;
&lt;p&gt;照着教程来做就可以，不一样的地方在于，65.4.2节移植alsa-utils时，makefile可能会报错：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;mv: 无法获取&quot;t-ja.gmo&quot; 的文件状态(stat): 没有那个文件或目录
make[2]: *** [ja.gmo] 错误 1&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;而且报错不止一个文件，都位于&lt;code&gt;alsaconf/po/&lt;/code&gt;下，解决办法是自己创建一个空文件 &lt;code&gt;~/alsa-utils-xxx/alsaconf/po/t-ja.gmo&lt;/code&gt;，缺什么就添加什么，反正alsaconf这个功能暂时用不到。&lt;/p&gt;
&lt;h2&gt;aplay报错&lt;/h2&gt;
&lt;p&gt;按照教程执行aplay后kernel报错：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Playing WAVE &apos;RWR.wav&apos; : Signed 16 bit Little Endian, Rate 48000 Hz, Stereo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;imx-sdma 20ec000.dma-controller: sdma or sdma firmware not ready!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;fsl-sai 202c000.sai: ASoC: error at soc_component_trigger on 202c000.sai: -12&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;aplay: pcm_write:2178: write error: Input/output error&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;考虑到这四条消息是一起跳出来的，上网查了一圈ASoC错误很少有人提及，一般都是时钟配置有问题，要么就是硬件设计时的错误，但考虑到我直接用的是NXP原厂的设备树，并且板子是现成的开发板，基本可以排除硬件设计原因。因此我猜测问题是由sdma没初始化引起的，因为alsa会调用sdma来进行音频流的传输。&lt;/p&gt;
&lt;p&gt;参考这篇文章进行sdma的配置：&lt;a href=&quot;https://blog.csdn.net/m0_61320035/article/details/140298343?spm=1001.2014.3001.5506&quot;&gt;正点原子I.MAX6ULL kernel6.6移植遇到的问题&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;配置完成后，重新编译kernel，下载，再调用aplay，就能够成功播放了。&lt;/p&gt;
&lt;h2&gt;alsactl 提示no such file or directory&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/JasonTD/article/details/131927546&quot;&gt;参考文章&lt;/a&gt;
制作根文件系统的时候没有/var/lock的目录，alsactl默认会在/var/lock目录操作，所以就会失败。解决办法是&lt;code&gt;mkdir -p /var/lock&lt;/code&gt;&lt;/p&gt;
</content:encoded></item><item><title>Linux下GT911驱动移植</title><link>https://arkmostima.pages.dev/posts/linux%E4%B8%8Bgt911%E9%A9%B1%E5%8A%A8%E7%A7%BB%E6%A4%8D/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E4%B8%8Bgt911%E9%A9%B1%E5%8A%A8%E7%A7%BB%E6%A4%8D/</guid><description>Kernel版本：6.6.52</description><pubDate>Sun, 13 Apr 2025 01:24:35 GMT</pubDate><content:encoded>&lt;p&gt;Kernel版本：6.6.52&lt;/p&gt;
&lt;p&gt;开始前首先确认所用屏幕背面IC的丝印，到底是ft5426还是gt911，二者驱动不通用&lt;/p&gt;
&lt;h2&gt;客制化驱动&lt;/h2&gt;
&lt;p&gt;若使用正点原子提供的gt9147.c驱动:&lt;/p&gt;
&lt;h3&gt;设备树&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;i2c2 {
	gt911:gt911@14 {
 		compatible = &quot;goodix,gt911&quot;, &quot;goodix,gt9xx&quot;;
 		reg = &amp;lt;0x14&amp;gt;;
 		pinctrl-names = &quot;default&quot;;
 		pinctrl-0 = &amp;lt;&amp;amp;pinctrl_gt911&amp;gt;;
 		interrupt-parent = &amp;lt;&amp;amp;gpio1&amp;gt;;
 		interrupts = &amp;lt;9 0&amp;gt;;
 		reset-gpios = &amp;lt;&amp;amp;gpio5 9 GPIO_ACTIVE_LOW&amp;gt;;
		interrupt-gpios = &amp;lt;&amp;amp;gpio1 9 GPIO_ACTIVE_LOW&amp;gt;;
 		status = &quot;okay&quot;; 
 	};
};

&amp;amp;iomuxc {
	pinctrl_gt911: gt911grp {
	fsl,pins = &amp;lt;
		MX6UL_PAD_GPIO1_IO09__GPIO1_IO09 	0x79 /* TSC_INT */
		MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09  0x10B0
		&amp;gt;;
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;6.6.52版本的Kernel DT不需要把&lt;code&gt;MX6UL_PAD_SNVS_TAMPER9__GPIO5_IO09&lt;/code&gt;单独移到&lt;code&gt;iomuxc_snvs&lt;/code&gt;中&lt;/p&gt;
&lt;h3&gt;驱动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/i2c.h&amp;gt;
#include &amp;lt;linux/regmap.h&amp;gt;
#include &amp;lt;linux/gpio/consumer.h&amp;gt;
#include &amp;lt;linux/of_irq.h&amp;gt;
#include &amp;lt;linux/interrupt.h&amp;gt;
#include &amp;lt;linux/input.h&amp;gt;
#include &amp;lt;linux/input/mt.h&amp;gt;
#include &amp;lt;linux/debugfs.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/slab.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/input/mt.h&amp;gt;
#include &amp;lt;linux/input/touchscreen.h&amp;gt;
#include &amp;lt;linux/i2c.h&amp;gt;
#include &amp;lt;asm/unaligned.h&amp;gt;

#define GT_CTRL_REG 	        0X8040  
#define GT_MODSW_REG 	        0X804D  
#define GT_9xx_CFGS_REG 	    0X8047  
#define GT_1xx_CFGS_REG 	    0X8050  
#define GT_CHECK_REG 	        0X80FF  
#define GT_PID_REG 		        0X8140  

#define GT_GSTID_REG 	        0X814E  
#define GT_TP1_REG 		        0X814F  
#define GT_TP2_REG 		        0X8157	
#define GT_TP3_REG 		        0X815F  
#define GT_TP4_REG 		        0X8167  
#define GT_TP5_REG 		        0X816F	
#define MAX_SUPPORT_POINTS      5       

struct gt911_dev {
	int irq_pin,reset_pin;					
	int irqnum;								
	int irqtype;							
	int max_x;								
	int max_y; 								
	void *private_data;						
	struct input_dev *input;			
	struct i2c_client *client;			

};
struct gt911_dev gt911;

const u8 irq_table[] = {IRQ_TYPE_EDGE_RISING, IRQ_TYPE_EDGE_FALLING, IRQ_TYPE_LEVEL_LOW, IRQ_TYPE_LEVEL_HIGH};  /* 触发方式 */

/* reset gt911 */
static int gt911_ts_reset(struct i2c_client *client, struct gt911_dev *dev)
{
	int ret = 0;

    /* request RESET gpio */
	if (gpio_is_valid(dev-&amp;gt;reset_pin)) {  		
		ret = devm_gpio_request_one(&amp;amp;client-&amp;gt;dev,	
					dev-&amp;gt;reset_pin, GPIOF_OUT_INIT_HIGH,
					&quot;gt911 reset&quot;);
		if (ret) {
			return ret;
		}
	}

    /* request INT gpio */
	if (gpio_is_valid(dev-&amp;gt;irq_pin)) {  		
		ret = devm_gpio_request_one(&amp;amp;client-&amp;gt;dev,	
					dev-&amp;gt;irq_pin, GPIOF_OUT_INIT_HIGH,
					&quot;gt911 int&quot;);
		if (ret) {
			return ret;
		}
	}

    /* initialize gt911 follow datasheet */
    gpio_set_value(dev-&amp;gt;reset_pin, 0);
    msleep(10);
    gpio_set_value(dev-&amp;gt;reset_pin, 1); 
    msleep(10);
    gpio_set_value(dev-&amp;gt;irq_pin, 0);   
    msleep(50);
    gpio_direction_input(dev-&amp;gt;irq_pin); 

	return 0;
}

static int gt911_read_regs(struct gt911_dev *dev, u16 reg, u8 *buf, int len)
{
	int ret;
    u8 regdata[2];
	struct i2c_msg msg[2];
	struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;client;
    
    regdata[0] = reg &amp;gt;&amp;gt; 8;
    regdata[1] = reg &amp;amp; 0xFF;

	msg[0].addr = client-&amp;gt;addr;			
	msg[0].flags = !I2C_M_RD;			
	msg[0].buf = &amp;amp;regdata[0];			
	msg[0].len = 2;						

	msg[1].addr = client-&amp;gt;addr;			
	msg[1].flags = I2C_M_RD;			
	msg[1].buf = buf;					
	msg[1].len = len;					

	ret = i2c_transfer(client-&amp;gt;adapter, msg, 2);
	if(ret == 2) {
		ret = 0;
	} else {
		ret = -EREMOTEIO;
	}
	return ret;
}

static s32 gt911_write_regs(struct gt911_dev *dev, u16 reg, u8 *buf, u8 len)
{
	u8 b[256];
	struct i2c_msg msg;
	struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;client;
	
	b[0] = reg &amp;gt;&amp;gt; 8;			
    b[1] = reg &amp;amp; 0XFF;			
	memcpy(&amp;amp;b[2],buf,len);		

	msg.addr = client-&amp;gt;addr;	
	msg.flags = 0;				

	msg.buf = b;				
	msg.len = len + 2;			

	return i2c_transfer(client-&amp;gt;adapter, &amp;amp;msg, 1);
}

static irqreturn_t gt911_irq_handler(int irq, void *dev_id)
{
    int touch_num = 0;
    int input_x, input_y;
    int id = 0;
    int ret = 0;
    u8 data;
    u8 touch_data[5];
    struct gt911_dev *dev = dev_id;

    ret = gt911_read_regs(dev, GT_GSTID_REG, &amp;amp;data, 1);
    if (data == 0x00)  {     /* no touch data */
        goto fail;
    } else {                 /* calculate touch data */
        touch_num = data &amp;amp; 0x0f;
    }

    if(touch_num) {         
        gt911_read_regs(dev, GT_TP1_REG, touch_data, 5);
        id = touch_data[0] &amp;amp; 0x0F;
        if(id == 0) {
            input_x  = touch_data[1] | (touch_data[2] &amp;lt;&amp;lt; 8);
            input_y  = touch_data[3] | (touch_data[4] &amp;lt;&amp;lt; 8);

            input_mt_slot(dev-&amp;gt;input, id);
		    input_mt_report_slot_state(dev-&amp;gt;input, MT_TOOL_FINGER, true);
		    input_report_abs(dev-&amp;gt;input, ABS_MT_POSITION_X, input_x);
		    input_report_abs(dev-&amp;gt;input, ABS_MT_POSITION_Y, input_y);
        }
    } else if(touch_num == 0){                
        input_mt_slot(dev-&amp;gt;input, id);
        input_mt_report_slot_state(dev-&amp;gt;input, MT_TOOL_FINGER, false);
    }

	input_mt_report_pointer_emulation(dev-&amp;gt;input, true);
    input_sync(dev-&amp;gt;input);

    data = 0x00;                
    gt911_write_regs(dev, GT_GSTID_REG, &amp;amp;data, 1);

fail:
	return IRQ_HANDLED;
}

static int gt911_ts_irq(struct i2c_client *client, struct gt911_dev *dev)
{
	int ret = 0;
	
	/* requeset IRQ */
	ret = devm_request_threaded_irq(&amp;amp;client-&amp;gt;dev, client-&amp;gt;irq, NULL,
					gt911_irq_handler, irq_table[dev-&amp;gt;irqtype] | IRQF_ONESHOT,
					client-&amp;gt;name, &amp;amp;gt911);
	if (ret) {
		dev_err(&amp;amp;client-&amp;gt;dev, &quot;Unable to request touchscreen IRQ.\n&quot;);
		return ret;
	}

	return 0;
}

static int gt911_read_firmware(struct i2c_client *client, struct gt911_dev *dev)
{
	int ret = 0, version = 0;
	u16 id = 0;
	u8 data[7]={0};
	char id_str[5];
	ret = gt911_read_regs(dev, GT_PID_REG, data, 6);
	if (ret) {
		dev_err(&amp;amp;client-&amp;gt;dev, &quot;Unable to read PID.\n&quot;);
		return ret;
	}
	memcpy(id_str, data, 4);
	id_str[4] = 0;
    if (kstrtou16(id_str, 10, &amp;amp;id))
        id = 0x1001;
	version = get_unaligned_le16(&amp;amp;data[4]);
	dev_info(&amp;amp;client-&amp;gt;dev, &quot;ID %d, version: %04x\n&quot;, id, version);
	switch (id) {    
    case 1151:
    case 1158:
    case 5663:
    case 5688:    
        ret = gt911_read_regs(dev, GT_1xx_CFGS_REG, data, 7);  
		break;
    default:
        ret = gt911_read_regs(dev, GT_9xx_CFGS_REG, data, 7);
		break;
    }
	if (ret) {
		dev_err(&amp;amp;client-&amp;gt;dev, &quot;Unable to read Firmware.\n&quot;);
		return ret;
	}
	dev-&amp;gt;max_x = (data[2] &amp;lt;&amp;lt; 8) + data[1];
	dev-&amp;gt;max_y = (data[4] &amp;lt;&amp;lt; 8) + data[3];
	dev-&amp;gt;irqtype = data[6] &amp;amp; 0x3;
	printk(&quot;X_MAX: %d, Y_MAX: %d, TRIGGER: 0x%02x&quot;, dev-&amp;gt;max_x, dev-&amp;gt;max_y, dev-&amp;gt;irqtype);

	return 0;
}

static int gt911_probe(struct i2c_client *client)
{
    u8 data, ret;
    gt911.client = client;

	gt911.irq_pin = of_get_named_gpio(client-&amp;gt;dev.of_node, &quot;interrupt-gpios&quot;, 0);
	gt911.reset_pin = of_get_named_gpio(client-&amp;gt;dev.of_node, &quot;reset-gpios&quot;, 0);

	ret = gt911_ts_reset(client, &amp;amp;gt911);
	if(ret &amp;lt; 0) {
		goto fail;
    }

    data = 0x02;
    gt911_write_regs(&amp;amp;gt911, GT_CTRL_REG, &amp;amp;data, 1); 
    mdelay(100);
    data = 0x0;
    gt911_write_regs(&amp;amp;gt911, GT_CTRL_REG, &amp;amp;data, 1); 
    mdelay(100);
	
    
	ret = gt911_read_firmware(client, &amp;amp;gt911);
	if(ret != 0) {
		printk(&quot;Fail !!! check !!\r\n&quot;);
		goto fail;
    }
	
	gt911.input = devm_input_allocate_device(&amp;amp;client-&amp;gt;dev);
	if (!gt911.input) {
		ret = -ENOMEM;
		goto fail;
	}
	gt911.input-&amp;gt;name = client-&amp;gt;name;
	gt911.input-&amp;gt;id.bustype = BUS_I2C;
	gt911.input-&amp;gt;dev.parent = &amp;amp;client-&amp;gt;dev;

	__set_bit(EV_KEY, gt911.input-&amp;gt;evbit);
	__set_bit(EV_ABS, gt911.input-&amp;gt;evbit);
	__set_bit(BTN_TOUCH, gt911.input-&amp;gt;keybit);

	input_set_abs_params(gt911.input, ABS_X, 0, gt911.max_x, 0, 0);
	input_set_abs_params(gt911.input, ABS_Y, 0, gt911.max_y, 0, 0);
	input_set_abs_params(gt911.input, ABS_MT_POSITION_X,0, gt911.max_x, 0, 0);
	input_set_abs_params(gt911.input, ABS_MT_POSITION_Y,0, gt911.max_y, 0, 0);	     
	ret = input_mt_init_slots(gt911.input, MAX_SUPPORT_POINTS, 0);
	if (ret) {
		goto fail;
	}

	ret = input_register_device(gt911.input);
	if (ret)
		goto fail;

	ret = gt911_ts_irq(client, &amp;amp;gt911);
	if(ret &amp;lt; 0) {
		goto fail;
	}

	dev_info(&amp;amp;client-&amp;gt;dev, &quot;GT911 Probe OK\r\n&quot;);

    return 0;

fail:
	return ret;
}

static void gt911_remove(struct i2c_client *client)
{
    input_unregister_device(gt911.input);
}

const struct of_device_id gt911_of_match_table[] = {
    {.compatible = &quot;goodix,gt911&quot; },
    { }
};
	
struct i2c_driver gt911_i2c_driver = {
    .driver = {
        .name  = &quot;gt911&quot;,
        .owner = THIS_MODULE,
        .of_match_table = gt911_of_match_table,
    },
    .probe  = gt911_probe,
    .remove = gt911_remove,
};

module_i2c_driver(gt911_i2c_driver);

MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Kernel提供驱动&lt;/h2&gt;
&lt;p&gt;驱动文件为&lt;code&gt;/drivers/input/touchscreen/goodix.c&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;设备树&lt;/h3&gt;
&lt;p&gt;按照binding要求&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# SPDX-License-Identifier: GPL-2.0
%YAML 1.2
---
$id: http://devicetree.org/schemas/input/touchscreen/goodix.yaml#
$schema: http://devicetree.org/meta-schemas/core.yaml#

title: Goodix GT9xx series touchscreen controller

maintainers:
  - Dmitry Torokhov &amp;lt;dmitry.torokhov@gmail.com&amp;gt;

allOf:
  - $ref: touchscreen.yaml#

properties:
  compatible:
    enum:
      - goodix,gt1151
      - goodix,gt1158
      - goodix,gt5663
      - goodix,gt5688
      - goodix,gt911
      - goodix,gt9110
      - goodix,gt912
      - goodix,gt9147
      - goodix,gt917s
      - goodix,gt927
      - goodix,gt9271
      - goodix,gt928
      - goodix,gt9286
      - goodix,gt967

  reg:
    enum: [ 0x5d, 0x14 ]

  interrupts:
    maxItems: 1

  irq-gpios:
    description: GPIO pin used for IRQ. The driver uses the interrupt gpio pin
      as output to reset the device.
    maxItems: 1

  reset-gpios:
    maxItems: 1

  AVDD28-supply:
    description: Analog power supply regulator on AVDD28 pin

  VDDIO-supply:
    description: GPIO power supply regulator on VDDIO pin

  touchscreen-inverted-x: true
  touchscreen-inverted-y: true
  touchscreen-size-x: true
  touchscreen-size-y: true
  touchscreen-swapped-x-y: true

additionalProperties: false

required:
  - compatible
  - reg
  - interrupts

examples:
  - |
    i2c {
      #address-cells = &amp;lt;1&amp;gt;;
      #size-cells = &amp;lt;0&amp;gt;;
      gt928@5d {
        compatible = &quot;goodix,gt928&quot;;
        reg = &amp;lt;0x5d&amp;gt;;
        interrupt-parent = &amp;lt;&amp;amp;gpio&amp;gt;;
        interrupts = &amp;lt;0 0&amp;gt;;
        irq-gpios = &amp;lt;&amp;amp;gpio1 0 0&amp;gt;;
        reset-gpios = &amp;lt;&amp;amp;gpio1 1 0&amp;gt;;
      };
    };
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;compatible&lt;/code&gt;、&lt;code&gt;reg&lt;/code&gt;和&lt;code&gt;interrupts&lt;/code&gt;必须要有，其他的无所谓，修改设备树节点如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;i2c2 {
	gt911:gt911@14 {
 		compatible = &quot;goodix,gt911&quot;, &quot;goodix,gt9xx&quot;;
 		reg = &amp;lt;0x14&amp;gt;;
 		interrupt-parent = &amp;lt;&amp;amp;gpio1&amp;gt;;
 		interrupts = &amp;lt;9 0&amp;gt;;
 		reset-gpios = &amp;lt;&amp;amp;gpio5 9 GPIO_ACTIVE_HIGH&amp;gt;;
		irq-gpios = &amp;lt;&amp;amp;gpio1 9 GPIO_ACTIVE_HIGH&amp;gt;;
 		status = &quot;okay&quot;; 
 	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：这里的reset-gpio和irq-gpio有效电平均为高电平，不能设置成低电平，虽然硬件设计时INT和RST为外部上拉低电平有效。原因未知。&lt;/p&gt;
&lt;p&gt;驱动由于找不到本地的xxx_cfg.bin文件导致长时间等待（一般为64s）后再进行input注册的问题可以通过在goodix.c中Probe函数中修改判断条件解决：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 注释掉本地cfg检索，防止找不到cfg.bin时长时间等待 */
	 //if (ts-&amp;gt;load_cfg_from_disk) {
	 if (0){
		 /* update device config */
		 error = device_property_read_string(&amp;amp;client-&amp;gt;dev,
							 &quot;goodix,config-name&quot;,
							 &amp;amp;cfg_name);
		 if (!error)
			 snprintf(ts-&amp;gt;cfg_name, sizeof(ts-&amp;gt;cfg_name),
				  &quot;goodix/%s&quot;, cfg_name);
		 else
			 snprintf(ts-&amp;gt;cfg_name, sizeof(ts-&amp;gt;cfg_name),
				  &quot;goodix_%s_cfg.bin&quot;, ts-&amp;gt;id);
 
		 error = request_firmware_nowait(THIS_MODULE, true, ts-&amp;gt;cfg_name,
						 &amp;amp;client-&amp;gt;dev, GFP_KERNEL, ts,
						 goodix_config_cb);
		 if (error) {
			 dev_err(&amp;amp;client-&amp;gt;dev,
				 &quot;Failed to invoke firmware loader: %d\n&quot;,
				 error);
			 return error;
		 }
 
		 return 0;
	 } else {
		......
	 }
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux快速切换交叉编译器版本</title><link>https://arkmostima.pages.dev/posts/linux%E5%BF%AB%E9%80%9F%E5%88%87%E6%8D%A2%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E5%99%A8%E7%89%88%E6%9C%AC/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E5%BF%AB%E9%80%9F%E5%88%87%E6%8D%A2%E4%BA%A4%E5%8F%89%E7%BC%96%E8%AF%91%E5%99%A8%E7%89%88%E6%9C%AC/</guid><description>确保/usr/local/arm下有以下命令格式的交叉编译器文件夹：</description><pubDate>Sat, 12 Apr 2025 06:13:24 GMT</pubDate><content:encoded>&lt;p&gt;确保&lt;code&gt;/usr/local/arm&lt;/code&gt;下有以下命令格式的交叉编译器文件夹：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc-版本号-arm-linux-gnueabihf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gcc-7.5.0-arm-linux-gnueabihf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Shell文件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash

# 所有工具链的根目录
TOOLCHAIN_ROOT=&quot;/usr/local/arm&quot;

# 显示所有可用版本
function list_versions() {
    echo &quot;Available toolchain versions:&quot;
    for dir in &quot;$TOOLCHAIN_ROOT&quot;/gcc-*; do
        ver=$(basename &quot;$dir&quot; | sed &apos;s/^gcc-\(.*\)-arm.*/\1/&apos;)
        echo &quot;  - $ver&quot;
    done
}

# 显示当前交叉编译器版本
function current_version() {
    which arm-linux-gnueabihf-gcc &amp;gt;/dev/null 2&amp;gt;&amp;amp;1 || {
        echo &quot;arm-linux-gnueabihf-gcc not found in PATH&quot;
        return
    }

    echo -n &quot;Current compiler version: &quot;
    arm-linux-gnueabihf-gcc -v 2&amp;gt;&amp;amp;1 | grep &quot;gcc 版本&quot; | awk &apos;{print $3}&apos;
}

# 切换工具链
function switch_version() {
    local version=$1
    local match_dir=&quot;$TOOLCHAIN_ROOT/gcc-${version}-arm-linux-gnueabihf&quot;

    if [ ! -d &quot;$match_dir&quot; ]; then
        echo &quot;Error: Toolchain version $version not found in $TOOLCHAIN_ROOT&quot;
        list_versions
        return 1
    fi

    # 清除当前 PATH 中旧的 toolchain 目录
    export PATH=$(echo &quot;$PATH&quot; | tr &apos;:&apos; &apos;\n&apos; | grep -v &quot;$TOOLCHAIN_ROOT&quot; | paste -sd &apos;:&apos; -)
    # 加入新版本路径
    export PATH=&quot;$match_dir/bin:$PATH&quot;

    echo &quot;Switched to toolchain version $version&quot;
    current_version
}

# 参数处理
case &quot;$1&quot; in
    list)
        list_versions
        ;;
    current)
        current_version
        ;;
    switch)
        if [ -z &quot;$2&quot; ]; then
            echo &quot;Usage: source toolchain-switcher.sh switch &amp;lt;version&amp;gt;&quot;
        else
            switch_version &quot;$2&quot;
        fi
        ;;
    *)
        echo &quot;Usage:&quot;
        echo &quot;  source toolchain-switcher.sh list           # List available versions&quot;
        echo &quot;  source toolchain-switcher.sh current        # Show current version&quot;
        echo &quot;  source toolchain-switcher.sh switch 4.9.4   # Switch to version&quot;
        ;;
esac
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仅在当前终端中一次性有效&lt;/p&gt;
</content:encoded></item><item><title>FreeRTOS 任务、任务列表、任务切换和任务间通信</title><link>https://arkmostima.pages.dev/posts/rtos%E4%BB%BB%E5%8A%A1%E5%9F%BA%E7%A1%80/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/rtos%E4%BB%BB%E5%8A%A1%E5%9F%BA%E7%A1%80/</guid><description>运行态(Running) 任务当前正在 CPU 上执行 在单核系统中，同时只有一个任务处于运行态 任务只有在调度器（Scheduler）选择它执行时，才会进入运行态 就绪态（Ready） 任务已经具备执行条件，但由于 CPU 资源被其他…</description><pubDate>Wed, 02 Apr 2025 07:11:44 GMT</pubDate><content:encoded>&lt;h1&gt;任务&lt;/h1&gt;
&lt;h2&gt;任务状态&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;运行态(Running)
&lt;ul&gt;
&lt;li&gt;任务当前正在 CPU 上执行&lt;/li&gt;
&lt;li&gt;在单核系统中，同时只有一个任务处于运行态&lt;/li&gt;
&lt;li&gt;任务只有在调度器（Scheduler）选择它执行时，才会进入运行态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;就绪态（Ready）
&lt;ul&gt;
&lt;li&gt;任务已经具备执行条件，但由于 CPU 资源被其他更高优先级任务占用，它暂时无法运行&lt;/li&gt;
&lt;li&gt;任务处于就绪列表（Ready List），等待 FreeRTOS 调度它运行&lt;/li&gt;
&lt;li&gt;当更高优先级任务阻塞或时间片结束，调度器可能会让它进入运行态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;阻塞态（Blocked）
&lt;ul&gt;
&lt;li&gt;任务正在等待某个事件（如 &lt;code&gt;vTaskDelay&lt;/code&gt;、信号量、消息队列、事件组等），暂时无法运行&lt;/li&gt;
&lt;li&gt;任务在等待事件时会从就绪态转换到 阻塞态，避免占用 CPU 资源&lt;/li&gt;
&lt;li&gt;当等待的事件发生后（如 信号量释放、消息到达、延时时间结束），任务会转换回 就绪态&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;挂起态（Suspended）
&lt;ul&gt;
&lt;li&gt;任务被显式挂起（使用 &lt;code&gt;vTaskSuspend()&lt;/code&gt;），不会被调度执行&lt;/li&gt;
&lt;li&gt;任务不会自动恢复，必须调用 &lt;code&gt;vTaskResume()&lt;/code&gt; 或 &lt;code&gt;vTaskResumeFromISR()&lt;/code&gt; 才能恢复运行&lt;/li&gt;
&lt;li&gt;与阻塞态不同，挂起任务不会因为外部事件自动恢复&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/rtos/task_1.webp&quot; alt=&quot;task状态转换图&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;任务优先级&lt;/h2&gt;
&lt;p&gt;FreeRTOS 任务的优先级用一个整数表示，数值越大，优先级越高。默认情况下，FreeRTOS 的最低优先级是 0，最大优先级由 &lt;code&gt;configMAX_PRIORITIES&lt;/code&gt; 定义（通常在 &lt;code&gt;FreeRTOSConfig.h&lt;/code&gt; 中配置）。&lt;/p&gt;
&lt;h2&gt;任务调度方式&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;抢占式调度&lt;/li&gt;
&lt;li&gt;时间片调度&lt;/li&gt;
&lt;li&gt;协程式调度（已基本弃用）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;抢占式调度&lt;/h3&gt;
&lt;p&gt;即高优先级任务抢占低优先级任务。当高优先级任务进入就绪态，调度器会立刻&lt;em&gt;抢占&lt;/em&gt;低优先级任务，并切换到高优先级任务执行。只有当优先级高的任务发生阻塞或者被挂起，低优先级的任务才可以运行。&lt;/p&gt;
&lt;h3&gt;时间片调度&lt;/h3&gt;
&lt;p&gt;相同优先级的任务采用时间片轮转。若多个任务拥有相同优先级，FreeRTOS 默认使用时间片轮转调度，每个任务轮流执行一个时间片（依赖于 &lt;code&gt;configUSE_TIME_SLICING&lt;/code&gt;），即调度器会在每一次时间片之后切换任务，CPU轮流运行优先级相同的任务。&lt;/p&gt;
&lt;h3&gt;任务控制块（TCB，Task Control Block）&lt;/h3&gt;
&lt;p&gt;TCB为一结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct tskTaskControlBlock {
    volatile StackType_t *pxTopOfStack;  // 任务栈指针
    ListItem_t xStateListItem;          // 任务在就绪、阻塞等列表中的位置
    StackType_t *pxStack;               // 任务栈的起始地址
    char pcTaskName[configMAX_TASK_NAME_LEN]; // 任务名称
    UBaseType_t uxPriority;             // 任务优先级
    ...
} tskTCB;

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;任务栈&lt;/h3&gt;
&lt;p&gt;动态方式创建任务时，系统会自动从系统heap中分配一块内存作为任务的栈空间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BaseType_t xTaskCreate( TaskFunction_t pxTaskCode,
                        const char * const pcName,
                        const configSTACK_DEPTH_TYPE usStackDepth,
                        void * const pvParameters,
                        UBaseType_t uxPriority,
                        TaskHandle_t * const pxCreatedTask)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;usStackDepth&lt;/code&gt;即为栈大小，以字(32位)为单位（非字节）。&lt;/p&gt;
&lt;h2&gt;API函数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xTaskCreate()&lt;/code&gt;：动态创建任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xTaskCreateStatic()&lt;/code&gt;： 静态创建任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xTaskCreateRestricted()&lt;/code&gt;： 动态创建使用 MPU 限制的任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xTaskCreateRestrictedStatic()&lt;/code&gt;： 静态创建使用 MPU 限制的任务&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vTaskDelete()&lt;/code&gt;： 删除任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;xTaskCreate()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt;中需要将&lt;code&gt;configSUPPORT_DYNAMIC_ALLOCATION&lt;/code&gt;配置为&lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BaseType_t xTaskCreate(
            TaskFunction_t pxTaskCode,
            const char * const pcName,
            const configSTACK_DEPTH_TYPE usStackDepth,
            void * const pvParameters,
            UBaseType_t uxPriority,
            TaskHandle_t * const pxCreatedTask);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pxTaskCode&lt;/code&gt; 指向任务函数的指针&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pcName&lt;/code&gt; 任务名，最大长度为 configMAX_TASK_NAME_LEN&lt;/li&gt;
&lt;li&gt;&lt;code&gt;usStackDepth&lt;/code&gt; 任务堆栈大小，单位：字（注意，单位不是字节）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pvParameters&lt;/code&gt; 传递给任务函数的参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uxPriority&lt;/code&gt; 任务优先级，最大值为(configMAX_PRIORITIES-1)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pxCreatedTask&lt;/code&gt; 任务句柄，任务成功创建后，会返回任务句柄。任务句柄就
是任务的任务控制块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pdPASS&lt;/code&gt; 任务创建成功&lt;/li&gt;
&lt;li&gt;&lt;code&gt;errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY&lt;/code&gt; 内存不足，任务创建失败&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;xTaskCreateStatic()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt;中需要将&lt;code&gt;configSUPPORT_STATIC_ALLOCATION&lt;/code&gt;配置为&lt;code&gt;1&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TaskHandle_t xTaskCreateStatic(
           TaskFunction_t pxTaskCode,
           const char * const pcName,
           const uint32_t ulStackDepth,
           void * const pvParameters,
           UBaseType_t uxPriority,
           StackType_t * const puxStackBuffer,
           StaticTask_t * const pxTaskBuffer);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和动态分配基本一致，不同在于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;puxStackBuffer&lt;/code&gt; 任务栈指针，内存由用户分配提供&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pxTaskBuffer&lt;/code&gt; 任务控制块指针，内存由用户分配提供&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NULL&lt;/code&gt; 用户没有提供相应的内存，任务创建失败&lt;/li&gt;
&lt;li&gt;&lt;code&gt;其他值&lt;/code&gt; 任务句柄，任务创建成功&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;xTaskCreateRestricted()&lt;/h3&gt;
&lt;p&gt;用于使用动态的方式创建受 MPU 保护的任务，任务的任务控制块以及任务的栈空
间所需的内存，均由 FreeRTOS 从 FreeRTOS 管理的堆中分配，若使用此函数，需要将宏&lt;code&gt;configSUPPORT_DYNAMIC_ALLOCATION``和宏portUSING_MPU_WRAPPERS&lt;/code&gt; 同时配置为 &lt;code&gt;1&lt;/code&gt;。此函数创建的任务会立刻进入就绪态，由任务调度器调度运行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BaseType_t xTaskCreateRestricted(
            const TaskParameters_t * const pxTaskDefinition,
            TaskHandle_t * pxCreatedTask);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pxTaskDefinition&lt;/code&gt; 指向任务参数结构体的指针，建结构体中包含任务函数、任
务名、任务优先级等任务参数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pxCreadedTask&lt;/code&gt; 任务句柄，任务成功创建后，会返回任务句柄。任务句柄就
是任务的任务控制块&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;vTaskDelete()&lt;/h3&gt;
&lt;p&gt;数用于删除已被创建的任务，被删除的任务将被从就绪态任务列表、阻塞态任务列表、挂起态任务列表和事件列表中移除.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：空闲任务会负责释放被删除任务中由系统分配的内存，但是由用户在任务删除前申请的内存，则需要由用户在任务被删除前提前释放，否则将导致内存泄露。&lt;/p&gt;
&lt;p&gt;若使用此函数，需要在&lt;code&gt;FreeRTOSConfig.h&lt;/code&gt;文件中将宏&lt;code&gt;INCLUDE_vTaskDelete&lt;/code&gt;配置为 &lt;code&gt;1&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void vTaskDelete(TaskHandle_t xTaskToDelete);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;xTaskToDelete&lt;/code&gt; 待删除任务的任务句柄&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;vTaskSuspend()&lt;/h3&gt;
&lt;p&gt;挂起任务&lt;/p&gt;
&lt;h3&gt;vTaskResume()&lt;/h3&gt;
&lt;p&gt;恢复被挂起的任务
&lt;code&gt;vTaskResumeFromISR()&lt;/code&gt;可用于在中断中恢复被挂起的任务。&lt;/p&gt;
&lt;h2&gt;注意事项&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;FreeRTOS 任务函数不允许返回，因此&lt;code&gt;void task1(void *pvParameters)&lt;/code&gt;中&lt;code&gt;task&lt;/code&gt;的具体实现必须包含在&lt;code&gt;while(1)&lt;/code&gt;中，否则会触发异常（HardFault 或任务栈溢出）&lt;/li&gt;
&lt;li&gt;任务中的延时不能用裸机时的&lt;code&gt;delay_ms&lt;/code&gt;或&lt;code&gt;delay_us&lt;/code&gt;，要用&lt;code&gt;vTaskDelay()&lt;/code&gt;。该函数按照给定的滴答数延迟任务。任务保持阻塞的实际时间取决于滴答频率 。常量&lt;code&gt;portTICK_PERIOD_MS&lt;/code&gt;可用于根据滴答频率计算实际时间：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void vTaskFunction( void * pvParameters )
{
    /* Block for 500ms. */
    const TickType_t xDelay = 500 / portTICK_PERIOD_MS;

    for( ;; )
    {
        /* Simply toggle the LED every 500ms, blocking between each toggle. */
        vToggleLED();
        vTaskDelay( xDelay );
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数通过阻塞的方式来进行延时，因此&lt;code&gt;vTaskDelay()&lt;/code&gt;不能很好的控制周期性任务的频率，因为途径代码的路径和其他任务/中断会影响&lt;code&gt;vTaskDelay()&lt;/code&gt;被调用的频率，久而久之会影响周期性任务的触发。请参阅 &lt;code&gt;vTaskDelayUntil()&lt;/code&gt;，了解设计用于方便 固定频率执行的替代 API 函数。此函数指定调用任务应取消阻塞的绝对时间（而非相对时间）来实现这一点 。&lt;/p&gt;
&lt;h1&gt;列表(List)&lt;/h1&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;列表（List） 是一个非常核心的数据结构，主要用于管理各种内核对象的状态和优先级排序，比如就绪任务、延时任务、阻塞任务等。FreeRTOS 的调度器、延时机制、同步机制（如信号量）都依赖于列表来实现。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 双向链表 */
typedef struct xLIST_ITEM
{
    TickType_t xItemValue;
    struct xLIST_ITEM *pxNext;
    struct xLIST_ITEM *pxPrevious;
    void *pvOwner;
    void *pvContainer;
} ListItem_t;

typedef struct xLIST
{
    UBaseType_t uxNumberOfItems;
    ListItem_t *pxIndex;
    ListItem_t xListEnd;  // 哨兵节点，值最大
} List_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;作用&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;任务调度：就绪列表（Ready List）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;FreeRTOS 会根据任务优先级把就绪任务插入对应的列表（每个优先级一个 List）。&lt;/li&gt;
&lt;li&gt;调度器从最高优先级的列表中选出第一个任务来运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;任务延时：延时列表（Delay List）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;当任务调用 vTaskDelay() 等函数后，会被放入延时列表，列表按照时间排序（Tick 值）。&lt;/li&gt;
&lt;li&gt;每个系统节拍（tick）检查列表，时间到了就移回就绪列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;阻塞管理：等待列表（例如队列、信号量）&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;当任务在等待某些资源时（比如消息队列、信号量），会被挂入等待列表中。&lt;/li&gt;
&lt;li&gt;当资源可用时，从列表中唤醒一个或多个任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;API&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vListInitialise()&lt;/code&gt;: 初始化一个 List。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vListInitialiseItem()&lt;/code&gt;: 初始化一个 ListItem。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vListInsert()&lt;/code&gt;: 插入元素到列表中（按 xItemValue 排序）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vListInsertEnd()&lt;/code&gt;: 插入到列表末尾。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uxListRemove()&lt;/code&gt;: 从列表中删除一个项。&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;任务切换&lt;/h1&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;p&gt;OS内核中的任务切换有两种触发方式：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;应用程序通过SVC指令触发&lt;/li&gt;
&lt;li&gt;SysTick周期性中断
但是如果有IRQ请求在SysTick中断前产生，SysTick就可能抢占IRQ，导致IRQ被延迟处理，影响RTOS的实时性并导致Usage Fault.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;PendSV&lt;/h2&gt;
&lt;p&gt;PendSV 全称是 &quot;Pendable Service Call&quot;，是 Cortex-M 系列处理器（比如 STM32）特有的一个系统异常，用于延迟处理的一种软件中断。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Pend” 表示它是可以“挂起”的。&lt;/li&gt;
&lt;li&gt;它的优先级可以被设置为最低，确保只有当别的高优先级中断都处理完之后才会执行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PendSV 是 FreeRTOS 实现任务上下文切换（context switch）的关键机制，具体而言：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;执行任务切换：当 FreeRTOS 决定要从一个任务切换到另一个任务时（比如发生了任务阻塞、任务优先级发生变化等），不会立即切换，而是设置一个“请求切换任务”的标志，让 PendSV 异常触发，延迟到安全时机再做真正的上下文切换。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/rtos/pendsv_1.webp&quot; alt=&quot;PendSV中执行任务切换&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;保护上下文切换不被打断：PendSV 的优先级设置为最低，意味着它执行的时候不会打断别的中断，保证了上下文切换过程是原子且稳定的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;任务切换的本质&lt;/h2&gt;
&lt;p&gt;保存-&amp;gt;切换-&amp;gt;恢复-&amp;gt;跳转&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;__asm void xPortPendSVHandler( void )
{
    extern uxCriticalNesting;
    extern pxCurrentTCB;
    extern vTaskSwitchContext;

/* *INDENT-OFF* */
    PRESERVE8

    /* 从 PSP（进程堆栈指针）获取当前任务的栈顶指针到 r0，即当前任务栈的位置 */
    mrs r0, psp
    isb     /* 清洗流水线，确保指令顺序执行完成 */
    
    /* 加载TCB地址到R3，再取出TCB到R2 */
    ldr r3, =pxCurrentTCB
    ldr r2, [ r3 ]

    /* 检查是否启用了浮点协处理器（FPU） */
    /* 如果启用，将高位VFQ寄存器压栈，防止浮点任务之间干扰 */
    tst r14, #0x10
    it eq
    vstmdbeq r0!, {s16-s31}

    /* 保存核心寄存器 */
    stmdb r0!, {r4-r11, r14}

    /* 将新栈顶保存至TCB，更新栈顶 */
    str r0, [ r2 ]

    /* 1. 暂存R0、R3 */
    /* 2.设置basepri，屏蔽低优先级中断，防止上下文切换被打断 */
    /* 3.调用vTaskSwitchContext(),即调度器，更新TCB至新任务 */
    /* 4.取消中断屏蔽 */
    stmdb sp!, {r0, r3}
    mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY
    msr basepri, r0
    dsb
    isb
    bl vTaskSwitchContext
    mov r0, #0
    msr basepri, r0
    ldmia sp!, {r0, r3}

    /* 获取新任务的TCB至R1，再获取新任务的栈顶地址至R0 */
    ldr r1, [ r3 ]
    ldr r0, [ r1 ]

    /* 恢复核心寄存器 */
    ldmia r0!, {r4-r11, r14}

    /* 检查是否启用了浮点协处理器（FPU） */
    /* 如果启用，将高位VFQ寄存器出栈 */
    tst r14, #0x10
    it eq
    vldmiaeq r0!, {s16-s31}

    /* 设置新的堆栈指针 */
    msr psp, r0
    isb

    #ifdef WORKAROUND_PMU_CM001 /* XMC4000 specific errata */
        #if WORKAROUND_PMU_CM001 == 1
            push { r14 }
            pop { pc }
            nop
        #endif
    #endif

    /* 返回新任务的执行地址 */
    bx r14
/* *INDENT-ON* */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;任务间通信&lt;/h1&gt;
&lt;h2&gt;队列(Queue)&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;FIFO（先进先出）&lt;/li&gt;
&lt;li&gt;既能同步（阻塞），又能传数据&lt;/li&gt;
&lt;li&gt;任务和中断都可以用&lt;/li&gt;
&lt;li&gt;可设长度 N，缓存多个元素&lt;/li&gt;
&lt;/ul&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;UART/ADC驱动中，任务读取缓冲区数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即“不仅要通知，还要带点东西”：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    uint8_t cmd;
    uint16_t value;
} Message_t;

xQueueSend(xQueue, &amp;amp;msg, portMAX_DELAY);
xQueueReceive(xQueue, &amp;amp;msg, portMAX_DELAY);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;队列集(Queue Set)&lt;/h2&gt;
&lt;p&gt;多个队列的集合，用于处理单个队列只能传输同一类型数据的情况，可在多个队列/信号量之间选择哪个先有数据/事件发生，避免无效率的轮询。&lt;/p&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;一个任务等待多个外设的输入（例如：UART1 / UART2 / CAN 同时接收）&lt;/li&gt;
&lt;li&gt;管理多个任务发来的指令队列，集中处理&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即“有很多输入源，我想让一个任务来监听它们，并优先处理哪个先有数据”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xQueueSelectFromSet(xQueueSet, portMAX_DELAY); // 等哪个先来
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;信号量(Semaphore)&lt;/h2&gt;
&lt;p&gt;普通信号量分为Binary Semaphore和Counting Semaphore，都用于任务同步和中断通知，本身并不传输数据，类似于一个信号灯。&lt;/p&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断服务程序用 &lt;code&gt;xSemaphoreGiveFromISR()&lt;/code&gt; 通知任务去处理数据&lt;/li&gt;
&lt;li&gt;任务 A 通知任务 B 某件事完成&lt;/li&gt;
&lt;li&gt;控制允许 N 个任务并发访问某资源（计数型）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;中断通知任务下，二值信号量的工作机制为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务A等待某件事发生，于是它调用：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;xSemaphoreTake(xSemaphore, portMAX_DELAY);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并且任务A状态变为&lt;strong&gt;阻塞&lt;/strong&gt;，直到信号量更新或超时&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;某个中断发生后，ISR内调用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;xSemaphoreGiveFromISR(xSemaphore, &amp;amp;xHigherPriorityTaskWoken);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过信号量发出信号，告知任务A期所期望的事件已经到达&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;FreeRTOS检测到信号到达，任务A由阻塞变为就绪&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;把信号量看成门铃：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;任务按下 &lt;code&gt;Take()&lt;/code&gt; 就是“等门铃响”。&lt;/li&gt;
&lt;li&gt;中断调用 &lt;code&gt;Give()&lt;/code&gt; 就是“按门铃”。&lt;/li&gt;
&lt;li&gt;门铃一响，任务就醒了，去执行它的事情。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;互斥信号量(Mutex Semaphore)&lt;/h2&gt;
&lt;p&gt;保护共享资源，防止多个任务同时访问。互斥信号量基于信号量实现，但只能由拥有者释放，支持优先级继承（防止优先级反转）。&lt;/p&gt;
&lt;p&gt;常见用法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;多任务访问串口、SPI、LCD等，避免同一硬件被多任务使用&lt;/li&gt;
&lt;li&gt;避免数据竞争&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即“我只想让一个任务进入临界区，并防止资源冲突”&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xSemaphoreTake(xMutex, portMAX_DELAY);
LCD_Print(&quot;Hello&quot;);  // 临界区
xSemaphoreGive(xMutex);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对比&lt;/h2&gt;
&lt;p&gt;假设有个任务控制打印机，有多个任务要发打印请求：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;队列：多个任务传“打印任务结构体”给打印机任务&lt;/li&gt;
&lt;li&gt;队列集：多个来源（如 GUI、网络、中断）发任务 → 打印任务监听哪个先到&lt;/li&gt;
&lt;li&gt;信号量：中断来了发一个信号量通知任务“有数据了”&lt;/li&gt;
&lt;li&gt;互斥信号量：多个任务打印时共享串口 → 用互斥锁保护 printf()&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;任务与进程、线程&lt;/h1&gt;
&lt;p&gt;在 FreeRTOS 中，所谓的 &quot;任务（Task）&quot; 实际上是 线程（Thread） 的概念，而不是进程。FreeRTOS中不同任务共享全局变量、堆和BSS，每个任务只有自己的任务栈，并没有独立的地址空间。同时STM32系列大多没有MMU，创建的所有任务都是运行在同一个地址空间，多个任务共享堆、数据段和代码段，硬件上不支持多进程。进程级别的隔离需要 MMU（内存管理单元），这超出了大多数小 MCU 的硬件能力。&lt;/p&gt;
&lt;p&gt;作为补充，FreeRTOS任务栈中存储的内容包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;该任务运行时的局部变量&lt;/li&gt;
&lt;li&gt;函数调用时的返回地址&lt;/li&gt;
&lt;li&gt;寄存器上下文备份（为了调用任务后返回）&lt;/li&gt;
&lt;li&gt;中断返回现场保存&lt;/li&gt;
&lt;li&gt;浮点寄存器状态（若FPU启用）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;栈是私有的，作用是支撑任务自身运行，栈上不会有别的任务的东西；每次任务切换时，FreeRTOS 保存/恢复的就是当前任务的栈指针；就像 Linux 线程一样，虽然共享地址空间，但栈是各自独立的。&lt;/p&gt;
&lt;p&gt;比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void TaskA(void *pvParam) {
    int a = 42;   // 局部变量a在 TaskA 的任务栈中
    while (1) {
        printf(&quot;TaskA: %d\n&quot;, a);
        vTaskDelay(1000 / portTICK_PERIOD_MS);
    }
}

void TaskB(void *pvParam) {
    static int b = 100;  // 静态变量b在 .data 段，是共享的
    int c = 99;          // 局部变量c在 TaskB 的任务栈中
    while (1) {
        printf(&quot;TaskB: %d\n&quot;, c);
        vTaskDelay(500 / portTICK_PERIOD_MS);
    }
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux SPI驱动通用框架和编写细节</title><link>https://arkmostima.pages.dev/posts/linuxspi/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxspi/</guid><description>1. 追加节点： 需要确保UART2节点为disabled，因IO占用</description><pubDate>Wed, 02 Apr 2025 04:39:08 GMT</pubDate><content:encoded>&lt;h2&gt;设备树&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;追加节点：
需要确保UART2节点为&lt;code&gt;disabled&lt;/code&gt;，因IO占用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/* Note: to enable ecspi3, uart2 node in imx6ul-14x14-evk.dtsi must be disabled */
&amp;amp;ecspi3 {
	fsl,spi-num-chipselects = &amp;lt;1&amp;gt;;
	cs-gpios = &amp;lt;&amp;amp;gpio1 20 GPIO_ACTIVE_LOW&amp;gt;;
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_ecspi3&amp;gt;;
	status = &quot;okay&quot;;

	spidev: icm20608@0 {
		compatible = &quot;alientek,icm20608&quot;;
		spi-max-frequency = &amp;lt;8000000&amp;gt;;
		reg = &amp;lt;0&amp;gt;;
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;驱动&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;创建of_match_table，匹配设备树&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static const struct of_device_id icm20608_of_match[] = {
    { .compatible = &quot;alientek,icm20608&quot; },
    { /* Sential */ }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建&lt;code&gt;spi_driver&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static struct spi_driver icm20608_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = &quot;icm20608&quot;,
        .of_match_table = icm20608_of_match,
    },
    .probe = icm20608_probe,
    .remove = icm20608_remove,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;填充&lt;code&gt;probe&lt;/code&gt;和&lt;code&gt;remove&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int icm20608_probe(struct spi_device *spi)
{
    printk(&quot;SPI driver and DTS match OK\r\n&quot;);

    if (icm20608dev.major){
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    } else {
        alloc_chrdev_region(&amp;amp;icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
    }
    printk(&quot;device reg ok, major = %d\r\n&quot;, icm20608dev.major);

    icm20608dev.cdev.owner = THIS_MODULE;
    cdev_init(&amp;amp;icm20608dev.cdev, &amp;amp;icm20608_fops);
    cdev_add(&amp;amp;icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

    icm20608dev.class = class_create(ICM20608_NAME);

    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);

    /* SPI初始化 */
    spi-&amp;gt;mode = SPI_MODE_0; /* MODE0:CPOL = 0, CPHA = 0 */
    spi_setup(spi);
    icm20608dev.private_data = spi;     /* 保存spi_driver至结构体 */

    icm20608_reginit();
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;static void icm20608_remove(struct spi_device *spi)
{
    cdev_del(&amp;amp;icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);

    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;完善接收和发送函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    int ret = -1;
    struct spi_device *spi = (struct spi_device *)dev-&amp;gt;private_data;

    /* tx buffer */
    unsigned char *txdata = kmalloc(1, GFP_KERNEL);
    if (!txdata)
        return -ENOMEM;
    txdata[0] = reg | 0x80;

    /* rx buffer */
    /* 待接收数据长度为len，但SPI为全双工，第一次向从机发送准备读取消息时从机返回空数据，所以需要加一 */
    unsigned char *rxdata = kmalloc(len + 1, GFP_KERNEL);

    struct spi_transfer t = {
        .tx_buf = txdata,
        .rx_buf = rxdata,
        .len = len + 1,
    };

    struct spi_message m;
    spi_message_init(&amp;amp;m);
    spi_message_add_tail(&amp;amp;t, &amp;amp;m);    

    ret = spi_sync(spi, &amp;amp;m);
    if (ret){
        return ret;
    }
    
    memcpy(buf, &amp;amp;rxdata[1], len);

    return 0;
}

static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
    int ret = -1;
    struct spi_device *spi = (struct spi_device *)dev-&amp;gt;private_data;

    unsigned char *txdata = kmalloc(len + 1, GFP_KERNEL);
    txdata[0] = reg &amp;amp; 0x7F;
    memcpy(&amp;amp;txdata[1], buf, len);

    struct spi_transfer t = {
        .tx_buf = txdata,
        .len = len + 1,
    };

    struct spi_message m;

    spi_message_init(&amp;amp;m);
    spi_message_add_tail(&amp;amp;t, &amp;amp;m);    

    ret = spi_sync(spi, &amp;amp;m);
    if (ret){
        return ret;
    }
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：读取寄存器消息时不能将两条消息分开发送，因为ICM20608要求在读取数据时连续发送数据，即发送读取命令后立即接收从机返回的消息，CS片选信号在这期间始终拉低。如果分开两条信息发送，比如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    struct spi_transfer t[2] = {
        {
            .tx_buf = txdata,
            .len = 1,
        },
        {
            .rx_buf = rxdata,
            .len = len,
        },
    };

    ...

    spi_message_add_tail(&amp;amp;t[0], &amp;amp;m);
    spi_message_add_tail(&amp;amp;t[1], &amp;amp;m);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么做会导致SPI在发送&lt;code&gt;t[0]&lt;/code&gt;和&lt;code&gt;t[1]&lt;/code&gt;时在两条消息中间短暂拉高CS，导致SPI丢失数据。&lt;/p&gt;
&lt;h2&gt;完整驱动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/libata.h&amp;gt;   /* 新版kernel不再支持ide.h */
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;linux/semaphore.h&amp;gt;
#include &amp;lt;linux/timer.h&amp;gt;
#include &amp;lt;linux/i2c.h&amp;gt;
#include &amp;lt;linux/spi/spi.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/of_address.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;linux/platform_device.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;
#include &quot;icm20608reg.h&quot;

#define ICM20608_CNT    1
#define ICM20608_NAME   &quot;icm20608&quot;

struct icm20608_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int major;
    void *private_data;
    int cs_gpio;
	signed int gyro_x_adc;		/* 陀螺仪X轴原始值 	 */
	signed int gyro_y_adc;		/* 陀螺仪Y轴原始值		*/
	signed int gyro_z_adc;		/* 陀螺仪Z轴原始值 		*/
	signed int accel_x_adc;		/* 加速度计X轴原始值 	*/
	signed int accel_y_adc;		/* 加速度计Y轴原始值	*/
	signed int accel_z_adc;		/* 加速度计Z轴原始值 	*/
	signed int temp_adc;		/* 温度原始值 			*/
};

struct icm20608_dev icm20608dev;

static int icm20608_open(struct inode *inode, struct file *filp);
static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt);
static int icm20608_release(struct inode *inode, struct file *filp);
static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len);
static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len);
static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg);
static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value);
void icm20608_readdata(struct icm20608_dev *dev);
void icm20608_reginit(void);
static int icm20608_probe(struct spi_device *spi);
static void icm20608_remove(struct spi_device *spi);

static int icm20608_open(struct inode *inode, struct file *filp)
{
    filp-&amp;gt;private_data = &amp;amp;icm20608dev;
    return 0;
}

static ssize_t icm20608_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    signed int data[7];
    long err = 0;
    struct icm20608_dev *dev = filp-&amp;gt;private_data;

	icm20608_readdata(dev);
	data[0] = dev-&amp;gt;gyro_x_adc;
	data[1] = dev-&amp;gt;gyro_y_adc;
	data[2] = dev-&amp;gt;gyro_z_adc;
	data[3] = dev-&amp;gt;accel_x_adc;
	data[4] = dev-&amp;gt;accel_y_adc;
	data[5] = dev-&amp;gt;accel_z_adc;
	data[6] = dev-&amp;gt;temp_adc;
	err = copy_to_user(buf, data, sizeof(data));
	return 0;
}

static int icm20608_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static int icm20608_read_regs(struct icm20608_dev *dev, u8 reg, void *buf, int len)
{
    int ret = -1;
    struct spi_device *spi = (struct spi_device *)dev-&amp;gt;private_data;

    /* tx buffer */
    unsigned char *txdata = kmalloc(1, GFP_KERNEL);
    if (!txdata)
        return -ENOMEM;
    txdata[0] = reg | 0x80;

    /* rx buffer */
    /* 待接收数据长度为len，但SPI为全双工，第一次向从机发送准备读取消息时从机返回空数据，所以需要加一 */
    unsigned char *rxdata = kmalloc(len + 1, GFP_KERNEL);

    struct spi_transfer t = {
        .tx_buf = txdata,
        .rx_buf = rxdata,
        .len = len + 1,
    };

    struct spi_message m;
    spi_message_init(&amp;amp;m);
    spi_message_add_tail(&amp;amp;t, &amp;amp;m);    

    ret = spi_sync(spi, &amp;amp;m);
    if (ret){
        return ret;
    }
    
    memcpy(buf, &amp;amp;rxdata[1], len);

    return 0;
}

static s32 icm20608_write_regs(struct icm20608_dev *dev, u8 reg, u8 *buf, u8 len)
{
    int ret = -1;
    struct spi_device *spi = (struct spi_device *)dev-&amp;gt;private_data;

    unsigned char *txdata = kmalloc(len + 1, GFP_KERNEL);
    txdata[0] = reg &amp;amp; 0x7F;
    memcpy(&amp;amp;txdata[1], buf, len);

    struct spi_transfer t = {
        .tx_buf = txdata,
        .len = len + 1,
    };

    struct spi_message m;

    spi_message_init(&amp;amp;m);
    spi_message_add_tail(&amp;amp;t, &amp;amp;m);    

    ret = spi_sync(spi, &amp;amp;m);
    if (ret){
        return ret;
    }
    return 0;
}

static unsigned char icm20608_read_onereg(struct icm20608_dev *dev, u8 reg)
{
    u8 data = 0;
    icm20608_read_regs(dev, reg, &amp;amp;data, 1);
    return data;
}

static void icm20608_write_onereg(struct icm20608_dev *dev, u8 reg, u8 value)
{
    u8 buf = value;
    icm20608_write_regs(dev, reg, &amp;amp;buf, 1);
}

void icm20608_readdata(struct icm20608_dev *dev)
{
	unsigned char data[14] = { 0 };
	icm20608_read_regs(dev, ICM20_ACCEL_XOUT_H, data, 14);

	dev-&amp;gt;accel_x_adc = (signed short)((data[0] &amp;lt;&amp;lt; 8) | data[1]); 
	dev-&amp;gt;accel_y_adc = (signed short)((data[2] &amp;lt;&amp;lt; 8) | data[3]); 
	dev-&amp;gt;accel_z_adc = (signed short)((data[4] &amp;lt;&amp;lt; 8) | data[5]); 
	dev-&amp;gt;temp_adc    = (signed short)((data[6] &amp;lt;&amp;lt; 8) | data[7]); 
	dev-&amp;gt;gyro_x_adc  = (signed short)((data[8] &amp;lt;&amp;lt; 8) | data[9]); 
	dev-&amp;gt;gyro_y_adc  = (signed short)((data[10] &amp;lt;&amp;lt; 8) | data[11]);
	dev-&amp;gt;gyro_z_adc  = (signed short)((data[12] &amp;lt;&amp;lt; 8) | data[13]);
}

void icm20608_reginit(void)
{
	u8 value = 0;
	
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_PWR_MGMT_1, 0x80);
	mdelay(50);
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_PWR_MGMT_1, 0x01);
	mdelay(50);

	value = icm20608_read_onereg(&amp;amp;icm20608dev, ICM20_WHO_AM_I);
	printk(&quot;ICM20608 ID = %#X\r\n&quot;, value);	

	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_SMPLRT_DIV, 0x00); 	/* 输出速率是内部采样率					*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_GYRO_CONFIG, 0x18); 	/* 陀螺仪±2000dps量程 				*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_ACCEL_CONFIG, 0x18); 	/* 加速度计±16G量程 					*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_CONFIG, 0x04); 		/* 陀螺仪低通滤波BW=20Hz 				*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_ACCEL_CONFIG2, 0x04); /* 加速度计低通滤波BW=21.2Hz 			*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_PWR_MGMT_2, 0x00); 	/* 打开加速度计和陀螺仪所有轴 				*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_LP_MODE_CFG, 0x00); 	/* 关闭低功耗 						*/
	icm20608_write_onereg(&amp;amp;icm20608dev, ICM20_FIFO_EN, 0x00);		/* 关闭FIFO						*/
}

static const struct of_device_id icm20608_of_match[] = {
    { .compatible = &quot;alientek,icm20608&quot; },
    { /* Sential */ }
};

static struct file_operations icm20608_fops = {
    .owner = THIS_MODULE,
    .open = icm20608_open,
    .read = icm20608_read,
    .release = icm20608_release,
};

static int icm20608_probe(struct spi_device *spi)
{
    printk(&quot;SPI driver and DTS match OK\r\n&quot;);

    if (icm20608dev.major){
        icm20608dev.devid = MKDEV(icm20608dev.major, 0);
        register_chrdev_region(icm20608dev.devid, ICM20608_CNT, ICM20608_NAME);
    } else {
        alloc_chrdev_region(&amp;amp;icm20608dev.devid, 0, ICM20608_CNT, ICM20608_NAME);
        icm20608dev.major = MAJOR(icm20608dev.devid);
    }
    printk(&quot;device reg ok, major = %d\r\n&quot;, icm20608dev.major);

    icm20608dev.cdev.owner = THIS_MODULE;
    cdev_init(&amp;amp;icm20608dev.cdev, &amp;amp;icm20608_fops);
    cdev_add(&amp;amp;icm20608dev.cdev, icm20608dev.devid, ICM20608_CNT);

    icm20608dev.class = class_create(ICM20608_NAME);

    icm20608dev.device = device_create(icm20608dev.class, NULL, icm20608dev.devid, NULL, ICM20608_NAME);

    spi-&amp;gt;mode = SPI_MODE_0; /* MODE0:CPOL = 0, CPHA = 0 */
    spi_setup(spi);
    icm20608dev.private_data = spi;

    icm20608_reginit();
    return 0;
}

static void icm20608_remove(struct spi_device *spi)
{
    cdev_del(&amp;amp;icm20608dev.cdev);
    unregister_chrdev_region(icm20608dev.devid, ICM20608_CNT);

    device_destroy(icm20608dev.class, icm20608dev.devid);
    class_destroy(icm20608dev.class);
}

static struct spi_driver icm20608_driver = {
    .driver = {
        .owner = THIS_MODULE,
        .name = &quot;icm20608&quot;,
        .of_match_table = icm20608_of_match,
    },
    .probe = icm20608_probe,
    .remove = icm20608_remove,
};

module_spi_driver(icm20608_driver);

MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux I2C驱动通用框架和编写细节</title><link>https://arkmostima.pages.dev/posts/linuxi2c/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxi2c/</guid><description>I2C子系统采用分层设计： I2C核心层 提供总线协议实现、设备注册/注销、总线通信接口（如i2ctransfer） struct i2cadapter：抽象化的I2C控制器 struct i2cclient：连接到总线的I2C从机 管…</description><pubDate>Tue, 01 Apr 2025 05:40:02 GMT</pubDate><content:encoded>&lt;h2&gt;I2C子系统架构&lt;/h2&gt;
&lt;p&gt;I2C子系统采用分层设计：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I2C核心层
&lt;ul&gt;
&lt;li&gt;提供总线协议实现、设备注册/注销、总线通信接口（如&lt;code&gt;i2c_transfer&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct i2c_adapter&lt;/code&gt;：抽象化的I2C控制器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct i2c_client&lt;/code&gt;：连接到总线的I2C从机&lt;/li&gt;
&lt;li&gt;管理适配器和设备，提供用户空间接口(&lt;code&gt;/dev/i2c-*&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I2C总线驱动
&lt;ul&gt;
&lt;li&gt;控制物理I2C控制器硬件，实现底层时序和寄存器控制&lt;/li&gt;
&lt;li&gt;填充&lt;code&gt;struct i2c_algorithm&lt;/code&gt;，底层数据收发&lt;/li&gt;
&lt;li&gt;注册适配器(&lt;code&gt;i2c_add_adapter&lt;/code&gt;)，通常和平台设备驱动结合，依赖设备树&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;I2C设备驱动
&lt;ul&gt;
&lt;li&gt;驱动特定的I2C设备&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct i2c_driver&lt;/code&gt;：定义驱动名称、设备ID表、&lt;code&gt;probe&lt;/code&gt;和&lt;code&gt;remove&lt;/code&gt;方法&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct i2c_client&lt;/code&gt;：保存设备地址、适配器指针等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;设备树配置&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义I2C控制器节点，通常由SoC厂商提供&lt;/li&gt;
&lt;li&gt;在节点下添加I2C从设备，需保证一个从机地址只有一个设备，不能复用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;i2c1 {
	clock-frequency = &amp;lt;100000&amp;gt;;
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_i2c1&amp;gt;;
	status = &quot;okay&quot;;

	magnetometer@e {
		compatible = &quot;fsl,mag3110&quot;;
		reg = &amp;lt;0x0e&amp;gt;;
		vdd-supply = &amp;lt;&amp;amp;reg_peri_3v3&amp;gt;;
		vddio-supply = &amp;lt;&amp;amp;reg_peri_3v3&amp;gt;;
		position = &amp;lt;2&amp;gt;;
	};

/*
	fxls8471@1e {
		compatible = &quot;fsl,fxls8471&quot;;
		reg = &amp;lt;0x1e&amp;gt;;
		position = &amp;lt;0&amp;gt;;
		interrupt-parent = &amp;lt;&amp;amp;gpio5&amp;gt;;
		interrupts = &amp;lt;0 8&amp;gt;;
	}; */

    /* I2C从设备 */
	ap3216c@1e {
		compatible = &quot;alientek,ap3216c&quot;;
		reg = &amp;lt;0x1e&amp;gt;;
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;设备驱动&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义驱动结构体&lt;code&gt;i2c_driver&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = &quot;ap3216c&quot;,
        .of_match_table = ap3216c_of_match,
    },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现匹配表（兼容设备树），&lt;code&gt;{ /* Sential */ }&lt;/code&gt;必须添加&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static const struct of_device_id ap3216c_of_match[] = {
    {.compatible = &quot;alientek,ap3216c&quot;},
    { /* Sential */ }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现&lt;code&gt;probe&lt;/code&gt;函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int ap3216c_probe(struct i2c_client *client)
{
    printk(&quot;driver and device has been matched!\r\n&quot;);
    
    if (ap3216cdev.major){
        ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
        register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
    }else{
        alloc_chrdev_region(&amp;amp;ap3216cdev.devid, 0, 1, AP3216C_NAME);
        ap3216cdev.major = MAJOR(ap3216cdev.devid);
    }
    printk(&quot;reg ok, major = %d, minor = %d\r\n&quot;, ap3216cdev.major, 0);

    cdev_init(&amp;amp;ap3216cdev.cdev, &amp;amp;ap3216c_fops);
    cdev_add(&amp;amp;ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

    ap3216cdev.class = class_create(AP3216C_NAME);

    ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);

    ap3216cdev.private_data = client;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现数据传输
i2c的数据传输采用类似于“填充”的形式，具体为定义一个&lt;code&gt;i2c_msg&lt;/code&gt;类型的待发送消息&lt;code&gt;msg&lt;/code&gt;，然后填充&lt;code&gt;msg&lt;/code&gt;中的&lt;code&gt;addr&lt;/code&gt;(从机地址)、&lt;code&gt;flags&lt;/code&gt;(发送/接收标志位)、&lt;code&gt;buf&lt;/code&gt;(发送/接收缓冲区)和&lt;code&gt;len&lt;/code&gt;(数据长度)这四个成员变量，最后使用&lt;code&gt;i2c_transfer()&lt;/code&gt;函数进行发送。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;以接收数据为例，先发送目标寄存器地址，然后读取数据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;private_data;

    /* fill msg1, send addr signal */
    msg[0].addr = client-&amp;gt;addr;
    msg[0].flags = 0;   /* send data */
    msg[0].buf = &amp;amp;reg;  /* TX buffer */
    msg[0].len = 1;     

    /* fill msg2, read len byte data to val */
    msg[1].addr = client-&amp;gt;addr;
    msg[1].flags = I2C_M_RD;    /* read data */
    msg[1].buf = val;
    msg[1].len = len;

    /* send msg */
    ret = i2c_transfer(client-&amp;gt;adapter, msg, 2);
    if (ret == 2){
        ret = 0;
    }else{
        printk(&quot;i2c RD failed = %d reg = %06x led = %d\r\n&quot;, ret ,reg, len);
        ret = -EREMOTEIO;
    }
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：AP3216C要求写数据的时序为：
&lt;img src=&quot;/images/linux/i2c_1.webp&quot; alt=&quot;AP3216C Read Protocal&quot; /&gt;
即发送从机设备地址-&amp;gt;发送待读取寄存器地址-&amp;gt;重新发送从机设备地址-&amp;gt;从机输出待读取寄存器值。但是很显然上面的代码里面没有发送从机设备地址的这条&lt;code&gt;msg&lt;/code&gt;，这是因为linux底层会根据&lt;code&gt;msg.addr&lt;/code&gt;来自动处理发送设备地址的这一步操作，我们只需要处理&lt;em&gt;发送待读取寄存器地址&lt;/em&gt;和&lt;em&gt;从机输出待读取寄存器值&lt;/em&gt;这两步。&lt;/p&gt;
&lt;p&gt;写操作也是类似的：
&lt;img src=&quot;/images/linux/i2c_2.webp&quot; alt=&quot;AP3216C Write Protocal&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;private_data;

    b[0] = reg;     /* reg addr */
    memcpy(&amp;amp;b[1], buf, len);    /* copy tx data to b[1] */

    /* fill msg */
    msg.addr = client-&amp;gt;addr;
    msg.flags = 0;  /* send data */
    msg.buf = b;    /* tx buffer */
    msg.len = len + 1;

    /* transfer data */
    return i2c_transfer(client-&amp;gt;adapter, &amp;amp;msg, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux kernel会自动处理发送时序，而且发送数据时不需要再停下来重新发送一遍寄存器地址再等待读取，而是可以连着发送，因此代码把待发送的数据接到了设备地址&lt;code&gt;b[0]&lt;/code&gt;的后面，也就是&lt;code&gt;b[1]&lt;/code&gt;里。实际发送时把设备地址和待发送数据合到一条&lt;code&gt;msg&lt;/code&gt;里发送，长度也就是&lt;code&gt;len + 1&lt;/code&gt;，即发送数据长度（&lt;code&gt;len&lt;/code&gt;字节） + 寄存器地址长度（1字节）&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;注册驱动模块&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;module_i2c_driver(ap3216c_driver);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/libata.h&amp;gt;   /* 新版kernel不再支持ide.h */
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/wait.h&amp;gt;
#include &amp;lt;linux/poll.h&amp;gt;
#include &amp;lt;linux/of_address.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;
#include &amp;lt;linux/semaphore.h&amp;gt;
#include &amp;lt;linux/timer.h&amp;gt;
#include &amp;lt;linux/of_irq.h&amp;gt;
#include &amp;lt;linux/irq.h&amp;gt;
#include &amp;lt;linux/fcntl.h&amp;gt;
#include &amp;lt;linux/fs.h&amp;gt;
#include &amp;lt;linux/platform_device.h&amp;gt;
#include &amp;lt;linux/miscdevice.h&amp;gt;
#include &amp;lt;linux/input.h&amp;gt;
#include &amp;lt;linux/i2c.h&amp;gt;
#include &quot;ap3216creg.h&quot;

#define AP3216C_CNT     1
#define AP3216C_NAME    &quot;ap3216c&quot;

/* device dev */
struct ap3216c_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    struct device_node *nd;
    int major;
    void *private_data;
    unsigned short ir, als, ps;  /* ap3216c reg */
};

static struct ap3216c_dev ap3216cdev;

/* ap3216c read from multipy regs */
static int ap3216c_read_regs(struct ap3216c_dev *dev, u8 reg, void *val, int len)
{
    int ret;
    struct i2c_msg msg[2];
    struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;private_data;

    /* fill msg1, send addr signal */
    msg[0].addr = client-&amp;gt;addr;
    msg[0].flags = 0;   /* send data */
    msg[0].buf = &amp;amp;reg;  /* TX buffer */
    msg[0].len = 1;     

    /* fill msg2, read len byte data to val */
    msg[1].addr = client-&amp;gt;addr;
    msg[1].flags = I2C_M_RD;    /* read data */
    msg[1].buf = val;
    msg[1].len = len;

    /* send msg */
    ret = i2c_transfer(client-&amp;gt;adapter, msg, 2);
    if (ret == 2){
        ret = 0;
    }else{
        printk(&quot;i2c RD failed = %d reg = %06x led = %d\r\n&quot;, ret ,reg, len);
        ret = -EREMOTEIO;
    }
    return ret;
}

/* ap3216c write data to reg */
static s32 ap3216c_write_regs(struct ap3216c_dev *dev, u8 reg, u8 *buf, u8 len)
{
    u8 b[256];
    struct i2c_msg msg;
    struct i2c_client *client = (struct i2c_client *)dev-&amp;gt;private_data;

    b[0] = reg;     /* reg addr */
    memcpy(&amp;amp;b[1], buf, len);    /* copy tx data to b[1] */

    /* fill msg */
    msg.addr = client-&amp;gt;addr;
    msg.flags = 0;  /* send data */
    msg.buf = b;    /* tx buffer */
    msg.len = len + 1;

    /* transfer data */
    return i2c_transfer(client-&amp;gt;adapter, &amp;amp;msg, 1);
}

/* ap3216c read data from one reg */
static unsigned char ap3216c_read_reg(struct ap3216c_dev *dev, u8 reg)
{
    u8 data = 0;

    ap3216c_read_regs(dev, reg, &amp;amp;data, 1);
    return data;
}

/* ap3216c write data to one reg */
static void ap3216c_write_reg(struct ap3216c_dev *dev, u8 reg, u8 data)
{
    u8 buf = 0;
    buf = data;
    ap3216c_write_regs(dev, reg, &amp;amp;buf, 1);
}

/* read data from ap3216c reg */
void ap3216c_readdata(struct ap3216c_dev *dev)
{
    unsigned char i = 0;
    unsigned char buf[6];

    /* read six regs in cycle */
    for (i = 0; i &amp;lt; 6; i++)
    {
        buf[i] = ap3216c_read_reg(dev, AP3216C_IRDATALOW + i);
    }

    /* read IR */
    if ((buf[0] &amp;gt;&amp;gt; 7) == 0x1){  /* IR is invalid */
        dev-&amp;gt;ir = 0;
    }else{  /* read ir */
        dev-&amp;gt;ir = ((unsigned short)buf[1] &amp;lt;&amp;lt; 2) | (buf[0] &amp;amp; 0x03); /* IR*/
    }
    /* read ALS */
    dev-&amp;gt;als = ((unsigned short)buf[3] &amp;lt;&amp;lt; 8) | buf[2];

    /* read PS */
    if ((buf[4] &amp;gt;&amp;gt; 6) == 1){
        dev-&amp;gt;ps = 0;
    }else{
        dev-&amp;gt;ps = (((unsigned short)buf[5] &amp;amp; 0x3F) &amp;lt;&amp;lt; 4) | (buf[4] &amp;amp; 0x0F);
    }
}

/* ap3216c initialization */
static int ap3216c_open(struct inode *inode, struct file *filp)
{
    filp-&amp;gt;private_data = &amp;amp;ap3216cdev;

    ap3216c_write_reg(&amp;amp;ap3216cdev, AP3216C_SYSTEMCONG, 0x04);
    mdelay(50);
    ap3216c_write_reg(&amp;amp;ap3216cdev, AP3216C_SYSTEMCONG, 0x03);

    return 0;
}

static ssize_t ap3216c_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    short data[3];
    long err = 0;

    struct ap3216c_dev *dev = (struct ap3216c_dev *)filp-&amp;gt;private_data;

    ap3216c_readdata(dev);

    data[0] = dev-&amp;gt;ir;
    data[1] = dev-&amp;gt;als;
    data[2] = dev-&amp;gt;ps;
    err = copy_to_user(buf, data, sizeof(data));
    return 0;
} 

static int ap3216c_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static const struct file_operations ap3216c_fops = {
    .owner = THIS_MODULE,
    .open = ap3216c_open,
    .read = ap3216c_read,
    .release = ap3216c_release,
};

static int ap3216c_probe(struct i2c_client *client)
{
    printk(&quot;driver and device has been matched!\r\n&quot;);
    
    if (ap3216cdev.major){
        ap3216cdev.devid = MKDEV(ap3216cdev.major, 0);
        register_chrdev_region(ap3216cdev.devid, AP3216C_CNT, AP3216C_NAME);
    }else{
        alloc_chrdev_region(&amp;amp;ap3216cdev.devid, 0, 1, AP3216C_NAME);
        ap3216cdev.major = MAJOR(ap3216cdev.devid);
    }
    printk(&quot;reg ok, major = %d, minor = %d\r\n&quot;, ap3216cdev.major, 0);

    cdev_init(&amp;amp;ap3216cdev.cdev, &amp;amp;ap3216c_fops);
    cdev_add(&amp;amp;ap3216cdev.cdev, ap3216cdev.devid, AP3216C_CNT);

    ap3216cdev.class = class_create(AP3216C_NAME);

    ap3216cdev.device = device_create(ap3216cdev.class, NULL, ap3216cdev.devid, NULL, AP3216C_NAME);

    ap3216cdev.private_data = client;
    return 0;
}

static void ap3216c_remove(struct i2c_client *client)
{
    cdev_del(&amp;amp;ap3216cdev.cdev);
    unregister_chrdev_region(ap3216cdev.devid, AP3216C_CNT);

    device_destroy(ap3216cdev.class, ap3216cdev.devid);
    class_destroy(ap3216cdev.class);
}

static const struct of_device_id ap3216c_of_match[] = {
    {.compatible = &quot;alientek,ap3216c&quot;},
    { /* Sential */ }
};

static struct i2c_driver ap3216c_driver = {
    .probe = ap3216c_probe,
    .remove = ap3216c_remove,
    .driver = {
        .owner = THIS_MODULE,
        .name = &quot;ap3216c&quot;,
        .of_match_table = ap3216c_of_match,
    },
};

module_i2c_driver(ap3216c_driver);

MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux Input设备通用驱动框架</title><link>https://arkmostima.pages.dev/posts/linuxinput%E8%AE%BE%E5%A4%87/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxinput%E8%AE%BE%E5%A4%87/</guid><description>用于向kernel报告按键事件</description><pubDate>Mon, 31 Mar 2025 06:20:21 GMT</pubDate><content:encoded>&lt;h2&gt;Input子系统架构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;          +-----------------------+
          |    用户空间         |
          |-----------------------|
          | /dev/input/eventX      |  &amp;lt;-  应用程序通过读取这些设备文件获取输入事件
          +-----------------------+
                    ↑
          +-----------------------+
          |  Input 子系统      |  &amp;lt;-  提供统一的输入接口
          +-----------------------+
                    ↑
          +-----------------------+
          |  Input 驱动         |  &amp;lt;-  负责具体输入设备（如键盘、触摸屏）
          +-----------------------+
                    ↑
          +-----------------------+
          |  硬件（键盘/鼠标等）  |
          +-----------------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Input驱动核心数据结构&lt;/h2&gt;
&lt;h3&gt;struct input_dev (输入设备)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct input_dev {
    const char *name;        // 设备名称
    unsigned long evbit[EV_MAX];  // 支持的事件类型 (如键盘按键、鼠标移动)
    unsigned long keybit[KEY_MAX];  // 支持的按键
    unsigned long relbit[REL_MAX];  // 相对坐标事件（鼠标）
    unsigned long absbit[ABS_MAX];  // 绝对坐标事件（触摸屏）
    struct input_mt *mt;  // 多点触控支持
    int (*open)(struct input_dev *dev);   // 设备打开回调
    int (*close)(struct input_dev *dev);  // 设备关闭回调
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;struct input_event (输入事件)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct input_event {
    struct timeval time;  // 时间戳
    __u16 type;          // 事件类型 (EV_KEY, EV_REL, EV_ABS)
    __u16 code;          // 事件代码 (键值或坐标轴)
    __s32 value;         // 事件值 (按键按下/释放，坐标值等)
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;struct input_handler (输入处理程序)&lt;/h3&gt;
&lt;h4&gt;input_report_key&lt;/h4&gt;
&lt;p&gt;用于向kernel报告按键事件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void input_report_key(struct input_dev *dev, unsigned int code, int value);

code:按键的键值，如KEY_A表示A键
value: 1:按下，0:释放，2:保持按下
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;键值在&lt;code&gt;include/uapi/linux/input-event-codes.h&lt;/code&gt;中定义&lt;/p&gt;
&lt;h4&gt;input_sync()&lt;/h4&gt;
&lt;p&gt;用于同步事件，告知input子系统当前一批输入事件已经结束，可以传递给用户空间&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void input_sync(struct input_dev *dev);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Input设备注册&lt;/h2&gt;
&lt;h3&gt;注册输入设备&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;声明&lt;code&gt;input_dev&lt;/code&gt;
一般在设备结构体中设置&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct xxx_dev {
    ...
    struct input_dev *inputdev;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;申请设备&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;keyinputdev.inputdev = input_allocate_device();
keyinputdev.inputdev-&amp;gt;name = KEYINPUT_NAME;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;input_allocate_device()&lt;/code&gt;用于申请并初始化一个&lt;code&gt;input_dev&lt;/code&gt;结构体&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置事件类型&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;keyinputdev.inputdev-&amp;gt;evbit[0] = BIT_MASK(EV_KEY) | BIT_MASK(EV_REP);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;evbit&lt;/code&gt;代表设备支持的事件类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;BIT_MASK(EV_KEY)&lt;/code&gt;：支持按键事件&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BIT_MASK(EV_REP)&lt;/code&gt;：支持按键重复，长按时不断触发&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;绑定按键
告知input子系统这个设备支持某个按键&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;input_set_capability(keyinputdev.inputdev, EV_KEY, KEY_0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void input_set_capability(struct input_dev *dev, unsigned int type, unsigned int code);

type：事件类型
code：具体的按键/轴/开关代码，包括：
    EV_KEY 按键事件
    EV_REL 相对轴事件（鼠标移动、滚轮）
    EV_ABS 绝对轴事件（触摸屏、操纵杆）
    EV_SW  开关事件（外设插入检测） 
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注册设备&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    ret = input_register_device(keyinputdev.inputdev);
    if (ret) {
        printk(&quot;reg input device failed\r\n&quot;);
        return ret;
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;函数原型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int input_register_device(struct input_dev *dev);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;卸载时使用&lt;code&gt;input_unregister_device(keyinputdev.inputdev)&lt;/code&gt;和&lt;code&gt;input_free_device(keyinputdev.inputdev)&lt;/code&gt;释放input设备&lt;/p&gt;
&lt;h3&gt;触发输入事件&lt;/h3&gt;
</content:encoded></item><item><title>Linux 进程与线程（一）基本概念与信号</title><link>https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B1/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E7%BA%BF%E7%A8%8B1/</guid><description>进程：资源管理的最小单位 线程：程序执行的最小单位</description><pubDate>Mon, 31 Mar 2025 04:07:03 GMT</pubDate><content:encoded>&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;进程：资源管理的最小单位&lt;/li&gt;
&lt;li&gt;线程：程序执行的最小单位&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每个进程拥有自己的数据段、代码段和堆栈段。&lt;/p&gt;
&lt;p&gt;线程通常叫做轻型的进程，包含独立的栈和CPU寄存器状态。线程是进程的一条执行路径，每个线程共享其所附属进程的所有资源。&lt;/p&gt;
&lt;p&gt;线程和进程比起来很小，相对来说线程花费更少的CPU资源。&lt;/p&gt;
&lt;p&gt;更形象一点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程是资源的管理单位
&lt;ul&gt;
&lt;li&gt;进程就像一个工厂，里面有自己的设备（CPU、内存、文件、网络连接等）&lt;/li&gt;
&lt;li&gt;每个工厂（进程）都是独立的，工厂之间不能直接共享设备（但可以通过通信合作）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程是执行的基本单位
&lt;ul&gt;
&lt;li&gt;线程就像工人，他们在工厂（进程）里面工作，负责具体的任务&lt;/li&gt;
&lt;li&gt;一个工厂（进程）可以有多个工人（线程），他们一起工作，提高效率&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;线程共享进程资源
&lt;ul&gt;
&lt;li&gt;工厂里的工人（线程）共用工厂的资源，比如机器（内存、文件、网络等）&lt;/li&gt;
&lt;li&gt;但工人们有自己的工作台（栈），不会互相干扰自己的操作步骤&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;进程之间是隔离的，线程之间是共享的
&lt;ul&gt;
&lt;li&gt;两个工厂（进程）不能随便使用对方的设备（资源隔离）&lt;/li&gt;
&lt;li&gt;但同一个工厂里的工人（线程）可以直接使用工厂的设备（共享资源）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;多线程适合任务紧密相关，共享数据的场景；多进程适合任务独立，互不干扰的场景。&lt;/p&gt;
&lt;h2&gt;ULT和KLT&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;用户级线程（User-Level Thread, ULT）&lt;/li&gt;
&lt;li&gt;内核级线程（Kernel-Level Thread, KLT）&lt;/li&gt;
&lt;/ul&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;如果一个线程阻塞，整个进程都会挂起（一个选手倒下，整个队伍都停下）&lt;/li&gt;
&lt;li&gt;适合轻量级任务，但不适合I/O密集型任务&lt;/li&gt;
&lt;/ul&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;线程切换涉及内核态和用户态切换，开销较大&lt;/li&gt;
&lt;li&gt;适合多核CPU调度、多线程并发任务，如 I/O 密集型任务&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;信号&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;信号是事件发生时一个进程对另一个进程的通知机制，也可以被称作软件中断。大多数情况下进程无法预测信号到达的时间，因此信号提供了一种处理&lt;em&gt;异步事件&lt;/em&gt;的方法。&lt;/p&gt;
&lt;p&gt;信号可以由以下情况产生：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;硬件异常：如除数为0、数组访问越界等异常，硬件会在检测到异常后通知Kernel，由Kernel通知进程&lt;/li&gt;
&lt;li&gt;终端输入能够产生信号的特殊字符，如Ctrl+C和Ctrl+Z&lt;/li&gt;
&lt;li&gt;&lt;code&gt;kill()&lt;/code&gt;：此方法有限制，接收信号的进程和发送信号的进程的所有者必须相同，或发送信号的进程的所有者为root&lt;/li&gt;
&lt;li&gt;发生软件事件，如定时器超时、CPU时间超限、子进程退出等等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和RTOS相似，信号本质上就是一个int类型的数字编号，“传递信号”实际上就是传递一个整形变量。&lt;/p&gt;
&lt;h3&gt;信号的处理方式&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;忽略信号：信号到达进程后，进程直接忽略该信号，除了SIGKILL和SIGSTOP&lt;/li&gt;
&lt;li&gt;捕获信号：信号到达进程后，执行预先绑定后好的信号处理函数（回调函数）&lt;/li&gt;
&lt;li&gt;执行系统默认操作：对于大多数信号，系统的默认处理方式即为终止该进程&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;常见信号与默认行为&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;信号编号&lt;/th&gt;
&lt;th&gt;信号名称&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;th&gt;默认操作&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;SIGHUP&lt;/td&gt;
&lt;td&gt;终端挂起或控制进程终止&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SIGINT&lt;/td&gt;
&lt;td&gt;键盘中断 (Ctrl+C)&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;SIGQUIT&lt;/td&gt;
&lt;td&gt;键盘退出 (Ctrl+)&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SIGILL&lt;/td&gt;
&lt;td&gt;非法机器语言指令&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;SIGTRAP&lt;/td&gt;
&lt;td&gt;跟踪断点&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SIGABRT&lt;/td&gt;
&lt;td&gt;异常终止 (abort函数)&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;SIGBUS&lt;/td&gt;
&lt;td&gt;总线错误（某种内存访问错误）&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;SIGFPE&lt;/td&gt;
&lt;td&gt;浮点异常&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;SIGKILL&lt;/td&gt;
&lt;td&gt;强制终止&lt;/td&gt;
&lt;td&gt;终止（不可捕获）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;SIGUSR1&lt;/td&gt;
&lt;td&gt;用户自定义信号1&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;SIGSEGV&lt;/td&gt;
&lt;td&gt;段错误（无效内存引用）&lt;/td&gt;
&lt;td&gt;核心转储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;SIGUSR2&lt;/td&gt;
&lt;td&gt;用户自定义信号2&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;SIGPIPE&lt;/td&gt;
&lt;td&gt;管道破裂（写无读端）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;SIGALRM&lt;/td&gt;
&lt;td&gt;定时器超时&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;SIGTERM&lt;/td&gt;
&lt;td&gt;终止请求&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;td&gt;SIGCHLD&lt;/td&gt;
&lt;td&gt;子进程状态变化&lt;/td&gt;
&lt;td&gt;忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;SIGCONT&lt;/td&gt;
&lt;td&gt;继续执行&lt;/td&gt;
&lt;td&gt;忽略（可捕获）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;SIGSTOP&lt;/td&gt;
&lt;td&gt;停止（不可忽略、不可捕获）&lt;/td&gt;
&lt;td&gt;停止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;SIGTSTP&lt;/td&gt;
&lt;td&gt;终端停止信号 (Ctrl+Z)&lt;/td&gt;
&lt;td&gt;停止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;SIGTTIN&lt;/td&gt;
&lt;td&gt;后台进程请求读取终端&lt;/td&gt;
&lt;td&gt;停止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;SIGTTOU&lt;/td&gt;
&lt;td&gt;后台进程请求写终端&lt;/td&gt;
&lt;td&gt;停止&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&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;strong&gt;核心转储&lt;/strong&gt;：终止进程并生成 core 文件（用于调试）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;忽略&lt;/strong&gt;：信号被忽略，进程不受影响。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;停止&lt;/strong&gt;：暂停进程，等待 SIGCONT 恢复。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;进程处理信号&lt;/h3&gt;
&lt;h4&gt;signal()&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;signal()&lt;/code&gt;是Linux下设置信号处理方式最简单的接口，可设置信号的处理方式为捕获、忽略或默认操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;

typedef void (*sig_t)(int);
sig_t signal(int signum, sig_t handler);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signum&lt;/code&gt;：指定需要设置的信号，可用信号名（宏）或信号的数字编号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler&lt;/code&gt;：指向对应的信号处理函数，也可设置为&lt;code&gt;SIG_IGN&lt;/code&gt;（忽略）或&lt;code&gt;SIG_DFL&lt;/code&gt;（默认操作）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;sigaction()&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signum&lt;/code&gt;：待设置信号，除了SIGKILL和SIGSTOP&lt;/li&gt;
&lt;li&gt;&lt;code&gt;act&lt;/code&gt;：指向一个struct sigaction结构，描述了信号的处理方式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;oldact&lt;/code&gt;：信号之前的处理方式等信息通过该参数返回&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;sigaction结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t*, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sa_handler&lt;/code&gt;：指定的信号处理函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sa_sigaction&lt;/code&gt;：也用于指定信号处理函数，这是替代的信号处理函数，提供了更多的参数，与&lt;code&gt;sa_handler&lt;/code&gt;互斥&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sa_mask&lt;/code&gt;：定义了一组信号，当进程在执行由 sa_handler 所定义的信号处理函数之前，会先将这组信号添加到进程的信号掩码字段中，当进程执行完处理函数之后再恢复信号掩码，将这组信号从信号掩码字段中删除。当进程在执行信号处理函数期间，可能又收到了同样的信号或其它信号，从而打断当前信号处理函数的执行，类似中断嵌套；如果进程接收到了信号掩码中的这些信号，那么这个信号将会被阻塞暂时不能得到处理，直到这些信号从进程的信号掩码中移除。在信号处理函数调用时，进程会自动将当前处理的信号添加到信号掩码字段中，这样保证了在处理一个给定的信号时，如果此信号再次发生，那么它将会被阻塞。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sa_restorer&lt;/code&gt;：已过时&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sa_flags&lt;/code&gt;：标志位，用于控制信号的处理过程
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SA_NOCLDSTOP&lt;/code&gt;：子进程停止时（即当它们接收到SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU中的一种时）或恢复（即它们接收到 SIGCONT）时不会收到 SIGCHLD 信号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SA_NOCLDWAIT&lt;/code&gt;：子进程终止时不会将其转变为僵尸进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SA_NODEFER&lt;/code&gt;：不要阻塞从某个信号自身的信号处理函数中接收此信号。也就是说当进程此时正在执行某个信号的处理函数，默认情况下，进程会自动将该信号添加到进程的信号掩码字段中，从而在执行信号处理函数期间阻塞该信号，默认情况下，我们期望进程在处理一个信号时阻塞同种信号，否则引起一些竞态条件；如果设置SA_NODEFER 标志，则表示不对它进行阻塞。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SA_RESETHAND&lt;/code&gt;：信号的处理方式设置为系统默认操作&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SA_RESTART&lt;/code&gt;：被信号中断的系统调用在信号处理完成之后将自动重新发起&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SA_SIGINFO&lt;/code&gt;：如果设置了该标志，则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;sigsuspend()&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;sigsuspend()&lt;/code&gt;是&lt;code&gt;pause()&lt;/code&gt;的高级版本，当需要安全、精确地等待一个特定信号，而又不想错过它时，就该用 &lt;code&gt;sigsuspend()&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;signal.h&amp;gt;

int sigsuspend(const sigset_t *mask);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mask&lt;/code&gt;指向一个临时的信号屏蔽集（阻塞集）。&lt;code&gt;sigsuspend(&amp;amp;mask_set)&lt;/code&gt;实际上就意味着只有“要等的那个信号”能打断挂起、触发处理器，屏蔽其他所有不关心的信号。&lt;/p&gt;
&lt;p&gt;假设有两个进程，子进程执行完一个任务后，通过信号告诉父进程可以继续了。这时候你不能用 &lt;code&gt;pause()&lt;/code&gt;，因为你可能在设置好处理函数之前就错过了信号:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;父进程提前屏蔽 SIGUSR1（防止它在处理器没准备好时到来）&lt;/li&gt;
&lt;li&gt;安装信号处理器&lt;/li&gt;
&lt;li&gt;子进程用 kill(getppid(), SIGUSR1) 发信号&lt;/li&gt;
&lt;li&gt;父进程使用 sigsuspend() 临时取消屏蔽，挂起自己直到 SIGUSR1 到达&lt;/li&gt;
&lt;li&gt;信号处理器运行，sigsuspend() 返回，父进程继续执行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样做可以防止 race condition（竞争条件）—— 例如信号在 pause() 之前到来，结果永远挂起。&lt;/p&gt;
&lt;h4&gt;两种不同状态下信号的处理方式&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;程序启动
当一个应用程序刚启动，还未执行到&lt;code&gt;signal()&lt;/code&gt;处，或者程序中没有调用&lt;code&gt;signal()&lt;/code&gt;函数时，进程对所有信号的处理方式都设置为默认操作。这也就是为什么平时都可以使用Ctrl+C来终止一个进程。&lt;/li&gt;
&lt;li&gt;进程创建
当一个进程调用 fork()创建子进程时，其子进程将会继承父进程的信号处理方式，因为子进程在开始时复制了父进程的内存映像，所以信号捕获函数的地址在子进程中是有意义的。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;进程的内存布局&lt;/h2&gt;
&lt;h3&gt;C语言程序的内存布局&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;虚拟地址向上递增
 ┌──────────────────────────┐
 │  栈（Stack）             │ ← 局部变量、函数参数、返回地址
 ├──────────────────────────┤
 │  堆（Heap）             │ ← malloc / calloc / realloc 动态分配
 ├──────────────────────────┤
 │  BSS段（.bss）          │ ← 未初始化的全局变量 &amp;amp; 静态变量（全为0）
 ├──────────────────────────┤
 │  数据段（.data）        │ ← 初始化的全局变量 &amp;amp; 静态变量
 ├──────────────────────────┤
 │  代码段（.text）        │ ← 可执行代码、只读常量（有时包含.rodata）
 └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;进程的虚拟地址空间&lt;/h3&gt;
&lt;p&gt;Linux 为每个进程提供一个独立的虚拟地址空间，进程只能看到并访问自己的虚拟空间。实际的物理内存由内核通过页表映射维护：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程隔离，互不干扰，提升安全性&lt;/li&gt;
&lt;li&gt;可以实现内存保护、共享内存、内存映射等高级机制&lt;/li&gt;
&lt;li&gt;支持更大的地址空间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以64位Linux系统为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;   ┌────────────────────────────┐
   │     用户栈 (stack)         │ ← 地址高，向下增长
   ├────────────────────────────┤
   │     mmap 区域（匿名映射/共享库） │
   ├────────────────────────────┤
   │     堆 (heap)              │ ← 向上增长
   ├────────────────────────────┤
   │     BSS段（.bss）          │
   ├────────────────────────────┤
   │     数据段（.data）        │
   ├────────────────────────────┤
   │     代码段（.text）        │
   ├────────────────────────────┤
   │     NULL保留区（不可访问） │ ← 低地址，保护空指针访问
   └────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;x86下用户态虚拟地址空间上限为4GB，用户使用3GB，Kernel使用1GB。用户态程序操作的是虚拟地址，通过MMU（内存管理单元）由Kernel映射到真实的物理地址，用户态无法直接观测到物理地址。&lt;/p&gt;
&lt;h2&gt;创建子进程&lt;/h2&gt;
&lt;h3&gt;fork()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;unistd.h&amp;gt;

pid_t fork(void);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt;调用后，将存在两个进程：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;原进程（父进程）&lt;/li&gt;
&lt;li&gt;新创建的子进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;子进程实际上是父进程的一个副本，子进程拷贝了父进程的数据段、堆、栈，同时继承了父进程&lt;code&gt;fopen&lt;/code&gt;的文件描述符，并且两个进程并不共享这些存储空间（除了代码段，它是只读的，父子进程共享代码段，内存中只存在一份代码段数据），因此&lt;code&gt;fork()&lt;/code&gt;之后每个进程均可修改各自的栈数据和堆中的变量。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt; 本身在被调用一次之后，将会返回两次：一次在父进程中返回，一次在子进程中返回。父进程中&lt;code&gt;fork()&lt;/code&gt;返回新创建的子进程的PID，子进程中&lt;code&gt;fork()&lt;/code&gt;返回0。因此可以通过判断返回值的方式来判断是父进程还是子进程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pid_t pid = fork();

if (pid &amp;gt; 0) {
    // 父进程
} else if (pid == 0) {
    // 子进程
} else {
    // 错误处理
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面提到父进程和子进程会共享已经打开的文件标识符，那么这里就有两种可能的情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父进程先&lt;code&gt;fopen&lt;/code&gt;，再&lt;code&gt;fork()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个进程指向同一个文件标识符，并共享文件偏移量，也就是说子进程改变了文章偏移量之后，也会影响的父进程，反之亦然。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;父进程先&lt;code&gt;fork()&lt;/code&gt;，再各自&lt;code&gt;fopen()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;两个进程获取到的文件标识符不同，各自拥有自己的文件编译量，进程之间不会同步文件偏移量的修改结果，因此写入的数据会发生竞争（覆盖或缺失）&lt;/p&gt;
&lt;h3&gt;vfork()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;vfork()&lt;/code&gt;与 &lt;code&gt;fork()&lt;/code&gt;函数主要有以下两个区别：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;vfork()&lt;/code&gt;与 &lt;code&gt;fork()&lt;/code&gt;一样都创建了子进程，但 &lt;code&gt;vfork()&lt;/code&gt;函数并不会将父进程的地址空间完全复制到子进程中，因为子进程会立即调用 &lt;code&gt;exec&lt;/code&gt;（或&lt;code&gt;_exit&lt;/code&gt;），于是也就不会引用该地址空间的数据。不过在子进程调用 &lt;code&gt;exec&lt;/code&gt; 或 &lt;code&gt;_exit&lt;/code&gt; 之前，它在父进程的空间中运行、子进程共享父进程的内存。这种优化工作方式的实现会提高进程创建的效率；但如果子进程修改了父进程的数据（除了 &lt;code&gt;vfork&lt;/code&gt; 返回值的变量）、进行了函数调用、或者没有调用 &lt;code&gt;exec&lt;/code&gt; 或 &lt;code&gt;_exit&lt;/code&gt; 就返回将可能带来未知的结果。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;另一个区别在于，&lt;code&gt;vfork()&lt;/code&gt;保证子进程先运行，子进程调用 &lt;code&gt;exec&lt;/code&gt; 之后父进程才可能被调度运行。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际应尽量避免&lt;code&gt;vfork()&lt;/code&gt;来创建子进程，防止出现未知结果。&lt;/p&gt;
&lt;h2&gt;fork()之后的竞争条件&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;fork()&lt;/code&gt;之后父子进程都将被系统继续调度运行，此时无法确定父子两个进程谁先访问CPU，这导致两个进程谁先运行、谁后运行是不确定的（虽然大部分情况下都是父进程先执行）。这个时候可以通过某种进程间同步机制来实现，比如使用信号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &amp;lt;signal.h&amp;gt;
#include &amp;lt;unistd.h&amp;gt;
#include &amp;lt;sys/types.h&amp;gt;

static void sig_handler(int sig)
{
    printf(&quot;接收到信号\n&quot;);
}

int main(void)
{
    struct sigaction sig = {0};
    sigset_t wait_mask; /* 信号集 */

    sigemptyset(&amp;amp;wait_mask);    /* 初始化信号集 */

    sig.sa_handler = sig_handler;
    sig.sa_flags = 0;
    if (sigaction(SIGUSR1, &amp;amp;sig, NULL) == -1){
        perror(&quot;sigaction error&quot;);
        exit(-1);
    }

    switch (fork())
    {
    case -1:
        perror(&quot;fork error&quot;);
        exit(-1);

    case 0:
        printf(&quot;子进程开始执行\n&quot;);
        printf(&quot;子进程打印信息\n&quot;);
        printf(&quot;~~~~~~~~~~~~~~~\n&quot;);
        sleep(2);
        kill(getppid(), SIGUSR1);   /* 向父进程发送SIGUSR1信号 */
        _exit(0);
    
    default:
        /* 不屏蔽任何信号，等待信号到达 */
        if (sigsuspend(&amp;amp;wait_mask) != -1){
            exit(-1);
        }
        printf(&quot;父进程开始执行\n&quot;);
        printf(&quot;父进程打印信息\n&quot;);
        exit(0);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;监视子进程&lt;/h2&gt;
&lt;h3&gt;wait()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;sys/types.h&amp;gt;
#include &amp;lt;sys/wait.h&amp;gt;

pid_t wait(int *status);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;status&lt;/code&gt;：存放子进程终止时的状态信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;系统调用&lt;code&gt;wait()&lt;/code&gt;将执行如下动作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;调用&lt;code&gt;wait()&lt;/code&gt;函数，如果所有子进程都还在运行，则&lt;code&gt;wait()&lt;/code&gt;会一直阻塞等待，直到某个子进程终止&lt;/li&gt;
&lt;li&gt;如果进程调用&lt;code&gt;wait()&lt;/code&gt;，但是该进程没有子进程（该进程没有需要等待的子进程），那么&lt;code&gt;wait()&lt;/code&gt;将返回错误&lt;/li&gt;
&lt;li&gt;如果进程调用&lt;code&gt;wait()&lt;/code&gt;之前，其子进程已经有一个或多个终止了，那么调用&lt;code&gt;wait()&lt;/code&gt;将不会阻塞，而是会回收子进程的一些资源，即“收尸”。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;形参&lt;code&gt;status&lt;/code&gt;不为NULL的情况下，可通过以下宏来检查其参数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;WIFEXITED(status)&lt;/code&gt;：子进程正常终止，返回true&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WEXITSTATUS(status)&lt;/code&gt;：返回子进程退出状态&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux Platform驱动通用框架</title><link>https://arkmostima.pages.dev/posts/linuxplatform%E9%A9%B1%E5%8A%A8%E9%80%9A%E7%94%A8%E6%A1%86%E6%9E%B6/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linuxplatform%E9%A9%B1%E5%8A%A8%E9%80%9A%E7%94%A8%E6%A1%86%E6%9E%B6/</guid><description>Platform驱动需要读取设备树中的compatible信息用于ofmatchtable进行匹配：</description><pubDate>Fri, 28 Mar 2025 08:12:13 GMT</pubDate><content:encoded>&lt;h2&gt;设备树&lt;/h2&gt;
&lt;p&gt;Platform驱动需要读取设备树中的&lt;code&gt;compatible&lt;/code&gt;信息用于&lt;code&gt;of_match_table&lt;/code&gt;进行匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Custom LED pinctrl */
gpioled {
	#address-cells = &amp;lt;1&amp;gt;;
	#size-cells = &amp;lt;1&amp;gt;;
	compatible = &quot;custom-gpioled&quot;;
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_led&amp;gt;;
	led-gpio = &amp;lt;&amp;amp;gpio1 3 GPIO_ACTIVE_LOW&amp;gt;;
	status = &quot;okay&quot;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Platform驱动&lt;/h2&gt;
&lt;p&gt;Platform驱动本质还是传统的字符设备驱动、块设备驱动或网络设备驱动，只是套上了一张&quot;platform&quot;的皮，目的是使用驱动-&amp;gt;总线-&amp;gt;设备这个驱动框架来实现驱动的分离和分层&lt;/p&gt;
&lt;h3&gt;Platform设备声明和注册&lt;/h3&gt;
&lt;p&gt;需要手动定义一个&lt;code&gt;platform_driver&lt;/code&gt;类型的结构体变量，然后实现结构体中的各个成员变量，主要是匹配方法和probe函数。然后，在驱动文件的&lt;code&gt;xxx_open()&lt;/code&gt;函数中调用&lt;code&gt;platform_driver_register&lt;/code&gt;函数以注册该Platform设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct platform_driver led_driver = {
    .driver = {
        .name = &quot;imx6ul-led&quot;,   //Platform设备名称
        .of_match_table = led_of_match, //Platform匹配表
    },
    .probe = led_probe,     //Probe函数
    .remove = led_remove,   //驱动卸载函数
};
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;static int __init leddriver_init(void)
{
    return platform_driver_register(&amp;amp;led_driver);   //初始化设备时注册Platform设备
}

static void __exit leddriver_exit(void)
{
    platform_driver_unregister(&amp;amp;led_driver);        //关闭设备时卸载Platform设备
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Linux官方会使用&lt;code&gt;module_platform_driver(gpio_led_driver)&lt;/code&gt;来向Kernel注册Platform设备，这个宏展开之后为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int __init gpio_led_driver_init(void) 
{ 
    return platform_driver_register (&amp;amp;(gpio_led_driver)); 
} 
module_init(gpio_led_driver_init); 

static void __exit gpio_led_driver_exit(void) 
{ 
    platform_driver_unregister (&amp;amp;(gpio_led_driver)); 
} 
module_exit(gpio_led_driver_exit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这实际上就是标准的注册和删除Platform驱动的步骤，使用这个宏可以大大简化Platform设备的驱动代码。&lt;/p&gt;
&lt;h3&gt;设备匹配表&lt;/h3&gt;
&lt;p&gt;驱动中需要声明一个&lt;code&gt;of_device_id&lt;/code&gt;类别的匹配表，用于Platform从设备树查找对应设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct of_device_id {
    char    name[32];
    char    type[32];
    char    compatible[128];
    const void  *data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常都是使用&lt;code&gt;compatible&lt;/code&gt;属性和设备树节点中的&lt;code&gt;compatible&lt;/code&gt;值进行比对以进行匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* Platform匹配设备树 */
static const struct of_device_id led_of_match[] = {
    {.compatible = &quot;custom-gpioled&quot;},
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;probe函数&lt;/h3&gt;
&lt;p&gt;probe函数在驱动和设备匹配完成后执行，对于字符设备而言，原先在&lt;code&gt;xxx_open&lt;/code&gt;中的代码可以移到&lt;code&gt;probe&lt;/code&gt;函数中执行，即只在Platform驱动和设备匹配完成后执行，而不是在打开文件后就执行。probe函数的形参为已经声明并且注册过的&lt;code&gt;platform_device&lt;/code&gt;类型的结构体&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* platfrom函数的probe驱动，匹配后此函数执行 */
static int led_probe (struct platform_device *dev)
{
    printk(&quot;LED driver and device has beem matched!\r\n&quot;);

    if (leddev.major){
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
    }else{
        alloc_chrdev_region(&amp;amp;leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
        leddev.major = MAJOR(leddev.devid);
        leddev.minor = MINOR(leddev.devid);
    }
    printk(&quot;Device reg ok, major = %d, minor = %d\r\n&quot;, leddev.major, leddev.minor);

    leddev.cdev.owner = THIS_MODULE;
    cdev_init(&amp;amp;leddev.cdev, &amp;amp;led_fops);
    cdev_add(&amp;amp;leddev.cdev, leddev.devid, LEDDEV_CNT);

    leddev.class = class_create(LEDDEV_NAME);
    if (IS_ERR(leddev.class)){
        return PTR_ERR(leddev.class);
    }

    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
    if (IS_ERR(leddev.device)){
        return PTR_ERR(leddev.device);
    }

    leddev.nd = of_find_node_by_path(&quot;/gpioled&quot;);
    if (leddev.nd == NULL){
        printk(&quot;Unable find node\r\n&quot;);
        return -EINVAL;
    }

    leddev.led0_gpio = of_get_named_gpio(leddev.nd, &quot;led-gpio&quot;, 0);
    if (leddev.led0_gpio &amp;lt; 0){
        printk(&quot;unable find gpio\r\n&quot;);
        return -EINVAL;
    }

    gpio_request(leddev.led0_gpio, &quot;led0&quot;);
    gpio_direction_output(leddev.led0_gpio, 1);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整代码示例&lt;/h2&gt;
&lt;h3&gt;驱动&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/libata.h&amp;gt;   /* 新版kernel不再支持ide.h */
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/wait.h&amp;gt;
#include &amp;lt;linux/poll.h&amp;gt;
#include &amp;lt;linux/of_address.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;
#include &amp;lt;linux/semaphore.h&amp;gt;
#include &amp;lt;linux/timer.h&amp;gt;
#include &amp;lt;linux/of_irq.h&amp;gt;
#include &amp;lt;linux/irq.h&amp;gt;
#include &amp;lt;linux/fcntl.h&amp;gt;
#include &amp;lt;linux/fs.h&amp;gt;
#include &amp;lt;linux/platform_device.h&amp;gt;

#define LEDDEV_CNT      1
#define LEDDEV_NAME     &quot;dtsplatled&quot;
#define LEDOFF          0
#define LEDON           1

struct leddev_dev {
    dev_t   devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    int led0_gpio;
};

struct leddev_dev   leddev;

void led0_switch(u8 sta)
{
    switch (sta)
    {
    case LEDON:
        gpio_set_value(leddev.led0_gpio, 0);
        break;
    
    case LEDOFF:
        gpio_set_value(leddev.led0_gpio, 1);
        break;
    }
}

static int led_open(struct inode *inode, struct file *filp)
{
    filp-&amp;gt;private_data = (struct leddev_dev *)&amp;amp;leddev;
    return 0;
}

static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int ret;
    unsigned char databuf[2];
    unsigned char led_sta;

    ret = copy_from_user(databuf, buf, cnt);
    if (ret &amp;lt; 0){
        printk(&quot;copy from user failed\r\n&quot;);
        return -EFAULT;
    }

    led_sta = databuf[0];
    switch (led_sta)
    {
    case LEDON:
        led0_switch(LEDON);
        break;
    
    case LEDOFF:
        led0_switch(LEDOFF);
        break;
    }
    return 0;
}

static struct file_operations led_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .write = led_write,
};

/* platfrom函数的probe驱动，匹配后此函数执行 */
static int led_probe (struct platform_device *dev)
{
    printk(&quot;LED driver and device has beem matched!\r\n&quot;);

    if (leddev.major){
        leddev.devid = MKDEV(leddev.major, 0);
        register_chrdev_region(leddev.devid, LEDDEV_CNT, LEDDEV_NAME);
    }else{
        alloc_chrdev_region(&amp;amp;leddev.devid, 0, LEDDEV_CNT, LEDDEV_NAME);
        leddev.major = MAJOR(leddev.devid);
        leddev.minor = MINOR(leddev.devid);
    }
    printk(&quot;Device reg ok, major = %d, minor = %d\r\n&quot;, leddev.major, leddev.minor);

    leddev.cdev.owner = THIS_MODULE;
    cdev_init(&amp;amp;leddev.cdev, &amp;amp;led_fops);
    cdev_add(&amp;amp;leddev.cdev, leddev.devid, LEDDEV_CNT);

    leddev.class = class_create(LEDDEV_NAME);
    if (IS_ERR(leddev.class)){
        return PTR_ERR(leddev.class);
    }

    leddev.device = device_create(leddev.class, NULL, leddev.devid, NULL, LEDDEV_NAME);
    if (IS_ERR(leddev.device)){
        return PTR_ERR(leddev.device);
    }

    leddev.nd = of_find_node_by_path(&quot;/gpioled&quot;);
    if (leddev.nd == NULL){
        printk(&quot;Unable find node\r\n&quot;);
        return -EINVAL;
    }

    leddev.led0_gpio = of_get_named_gpio(leddev.nd, &quot;led-gpio&quot;, 0);
    if (leddev.led0_gpio &amp;lt; 0){
        printk(&quot;unable find gpio\r\n&quot;);
        return -EINVAL;
    }

    gpio_request(leddev.led0_gpio, &quot;led0&quot;);
    gpio_direction_output(leddev.led0_gpio, 1);

    return 0;
}

/* Platform驱动被移除时执行此函数 */
static int led_remove(struct platform_device *dev)
{
    gpio_set_value(leddev.led0_gpio, 1);

    cdev_del(&amp;amp;leddev.cdev);
    unregister_chrdev_region(leddev.devid, LEDDEV_CNT);
    device_destroy(leddev.class, leddev.devid);
    class_destroy(leddev.class);

    return 0;
}

/* Platform匹配设备树 */
static const struct of_device_id led_of_match[] = {
    {.compatible = &quot;custom-gpioled&quot;},
};

static struct platform_driver led_driver = {
    .driver = {
        .name = &quot;imx6ul-led&quot;,
        .of_match_table = led_of_match,
    },
    .probe = led_probe,
    .remove = led_remove,
};

static int __init leddriver_init(void)
{
    return platform_driver_register(&amp;amp;led_driver);
}

static void __exit leddriver_exit(void)
{
    platform_driver_unregister(&amp;amp;led_driver);
}

module_init(leddriver_init);
module_exit(leddriver_exit);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用程序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stdio.h&quot;
#include &quot;unistd.h&quot;
#include &quot;sys/types.h&quot;
#include &quot;sys/stat.h&quot;
#include &quot;fcntl.h&quot;
#include &quot;stdlib.h&quot;
#include &quot;string.h&quot;
#include &quot;errno.h&quot;
#include &quot;linux/ioctl.h&quot;
#include &quot;sys/select.h&quot;
#include &quot;sys/time.h&quot;
#include &quot;sys/poll.h&quot;
#include &quot;signal.h&quot;

#define LEDOFF  0
#define LEDON   1

int main(int argc, char *argv[])
{  
    int fd, ret;
    char *filename;
    unsigned char databuf[2];

    if (argc != 3){
        printf(&quot;error usage\r\n&quot;);

        return -1;
    }

    filename = argv[1];
    fd = open(filename, O_RDWR);
    if (fd &amp;lt; 0){
        printf(&quot;unable open file %s\r\n&quot;, argv[1]);
        return -1;
    }

    databuf[0] = atoi(argv[2]);
    ret = write(fd, databuf, sizeof(databuf));
    if (ret &amp;lt; 0){
        printf(&quot;LED control failed\r\n&quot;);
        close(fd);
        return -1;
    }

    ret = close(fd);
    if (ret &amp;lt; 0){
        printf(&quot;file %s unable close\r\n&quot;, filename);
        return -1;
    }
    
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;使用Linux原生LED驱动&lt;/h2&gt;
&lt;p&gt;Linux原生支持了通过GPIO控制LED的驱动，位于&lt;code&gt;/drivers/leds&lt;/code&gt;下。通常LED会作为系统运行指示灯（呼吸灯）或者硬盘工作指示灯进行工作。6.6.52版本Kernel下原生LED驱动支持默认是打开的，仅需要手动修改设备树。设备树的节点编写规则位于&lt;code&gt;/Documentation/devicetree/bindings/leds/leds-gpio.txt&lt;/code&gt;。添加以下节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;		dtsleds {   /* LED设备 */
			compatible = &quot;gpio-leds&quot;;

			led0 {
				label = &quot;red&quot;;  /* label属性，一般为LED灯的名字，用颜色区分 */
				gpios = &amp;lt;&amp;amp;gpio1 3 GPIO_ACTIVE_LOW&amp;gt;;
				default-state = &quot;on&quot;;  /* 默认打开 */
                linux,default-trigger = &quot;heartbeat&quot;;    /* 作为系统呼吸灯 */
			};
		};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编译设备树，LED即作为系统呼吸灯运行。&lt;/p&gt;
</content:encoded></item><item><title>Linux文件描述符</title><link>https://arkmostima.pages.dev/posts/linux%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E6%96%87%E4%BB%B6%E6%8F%8F%E8%BF%B0%E7%AC%A6/</guid><description>我们知道在Linux系统中一切皆可以看成是文件，文件又可分为：普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候，我们每操作一次就找一次名字，这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引，这样要操…</description><pubDate>Wed, 26 Mar 2025 13:56:52 GMT</pubDate><content:encoded>&lt;h2&gt;文件描述符(file descriptor)&lt;/h2&gt;
&lt;p&gt;我们知道在Linux系统中一切皆可以看成是文件，文件又可分为：普通文件、目录文件、链接文件和设备文件。在操作这些所谓的文件的时候，我们每操作一次就找一次名字，这会耗费大量的时间和效率。所以Linux中规定每一个文件对应一个索引，这样要操作文件的时候，我们直接找到索引就可以对其进行操作了。&lt;/p&gt;
&lt;p&gt;文件描述符（file descriptor）就是内核为了高效管理这些已经被打开的文件所创建的索引，其是一个非负整数（通常是小整数），用于指代被打开的文件，所有执行I/O操作的系统调用都通过文件描述符来实现。同时还规定系统刚刚启动的时候，0是标准输入，1是标准输出，2是标准错误。这意味着如果此时去打开一个新的文件，它的文件描述符会是3，再打开一个文件文件描述符就是4……&lt;/p&gt;
&lt;p&gt;Linux内核对所有打开的文件有一个文件描述符表格，里面存储了每个文件描述符作为索引与一个打开文件相对应的关系，简单理解就是下图这样一个数组，文件描述符（索引）就是文件描述符表这个数组的下标，数组的内容就是指向一个个打开的文件的指针。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/file_1.webp&quot; alt=&quot;文件指针&quot; /&gt;&lt;/p&gt;
&lt;p&gt;上面只是简单理解，实际上关于文件描述符，Linux内核维护了3个数据结构：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;进程级的文件描述符表&lt;/li&gt;
&lt;li&gt;系统级的打开文件描述符表&lt;/li&gt;
&lt;li&gt;文件系统的i-node表&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个 Linux 进程启动后，会在内核空间中创建一个 PCB 控制块，PCB 内部有一个文件描述符表（File descriptor table），记录着当前进程所有可用的文件描述符，也即当前进程所有打开的文件。进程级的描述符表的每一条记录了单个进程所使用的文件描述符的相关信息，进程之间相互独立，一个进程使用了文件描述符3，另一个进程也可以用3。除了进程级的文件描述符表，系统还需要维护另外两张表：打开文件表、i-node 表。这两张表存储了每个打开文件的打开文件句柄（open file handle）。一个打开文件句柄存储了与一个打开文件相关的全部信息。&lt;/p&gt;
&lt;p&gt;系统级的打开文件描述符表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前文件偏移量（调用read()和write()时更新，或使用lseek()直接修改）&lt;/li&gt;
&lt;li&gt;打开文件时的标识（open()的flags参数）&lt;/li&gt;
&lt;li&gt;文件访问模式（如调用open()时所设置的只读模式、只写模式或读写模式）&lt;/li&gt;
&lt;li&gt;与信号驱动相关的设置&lt;/li&gt;
&lt;li&gt;对该文件i-node对象的引用，即i-node 表指针&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;文件系统的i-node表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;文件类型（例如：常规文件、套接字或FIFO）和访问权限&lt;/li&gt;
&lt;li&gt;一个指针，指向该文件所持有的锁列表&lt;/li&gt;
&lt;li&gt;文件的各种属性，包括文件大小以及与不同类型操作相关的时间戳&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/file_1.webp&quot; alt=&quot;文件描述符、打开的文件句柄以及i-node之间的关系&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在进程 A 中，文件描述符 1 和 20 都指向了同一个打开文件表项，标号为 23（指向了打开文件表中下标为 23 的数组元素），这可能是通过调用 dup()、dup2()、fcntl() 或者对同一个文件多次调用了 open() 函数形成的。&lt;/li&gt;
&lt;li&gt;进程 A 的文件描述符 2 和进程 B 的文件描述符 2 都指向了同一个文件，这可能是在调用 fork() 后出现的（即进程 A、B 是父子进程关系），或者是不同的进程独自去调用 open() 函数打开了同一个文件，此时进程内部的描述符正好分配到与其他进程打开该文件的描述符一样。&lt;/li&gt;
&lt;li&gt;进程 A 的描述符 0 和进程 B 的描述符 3 分别指向不同的打开文件表项，但这些表项均指向 i-node 表的同一个条目（标号为 1976）；换言之，它们指向了同一个文件。发生这种情况是因为每个进程各自对同一个文件发起了 open() 调用。同一个进程两次打开同一个文件，也会发生类似情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这就说明：同一个进程的不同文件描述符可以指向同一个文件；不同进程可以拥有相同的文件描述符；不同进程的同一个文件描述符可以指向不同的文件（一般也是这样，除了 0、1、2 这三个特殊的文件）；不同进程的不同文件描述符也可以指向同一个文件。&lt;/p&gt;
</content:encoded></item><item><title>Linux阻塞、非阻塞与异步通知</title><link>https://arkmostima.pages.dev/posts/linux%E9%98%BB%E5%A1%9E%E9%9D%9E%E9%98%BB%E5%A1%9E%E4%B8%8E%E5%BC%82%E6%AD%A5%E9%80%9A%E7%9F%A5/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E9%98%BB%E5%A1%9E%E9%9D%9E%E9%98%BB%E5%A1%9E%E4%B8%8E%E5%BC%82%E6%AD%A5%E9%80%9A%E7%9F%A5/</guid><description>阻塞状态下应用程序从驱动读取函数时，若kernel回报设备不可用，应用程序对应的线程将会进入睡眠，等待设备可用后由设备通知应用程序并将其唤醒以继续执行。</description><pubDate>Wed, 26 Mar 2025 12:38:36 GMT</pubDate><content:encoded>&lt;h2&gt;阻塞IO&lt;/h2&gt;
&lt;p&gt;阻塞状态下应用程序从驱动读取函数时，若kernel回报设备不可用，应用程序对应的线程将会进入睡眠，等待设备可用后由设备通知应用程序并将其唤醒以继续执行。&lt;/p&gt;
&lt;p&gt;Linux依靠等待队列(wait queue)来进行进程同步，其允许进程在某个条件满足前进入睡眠，并在条件满足时由驱动进行唤醒，避免CPU忙等。&lt;/p&gt;
&lt;h3&gt;等待队列&lt;/h3&gt;
&lt;p&gt;基本的等待队列包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;事件(event)：通常是 硬件中断、数据可用 或 资源释放 等&lt;/li&gt;
&lt;li&gt;等待队列头（&lt;code&gt;wait_queue_head_t&lt;/code&gt;）：每个等待队列都有一个队列头，用于管理在此等待的进程&lt;/li&gt;
&lt;li&gt;等待队列项（&lt;code&gt;wait_queue_t&lt;/code&gt;）：表示等待队列中的一个进程，它包含进程的状态信息&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;等待队列头结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct __wait_queue_head {
    spinlock_t lock;         // 保护等待队列的自旋锁
    struct list_head task_list; 
} wait_queue_head_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然这是一个链表结构，&lt;code&gt;task_list&lt;/code&gt;为所有等待某事件发生的进程链表。等待队列头维护等待该事件的所有进程，通过自旋锁来保护对&lt;code&gt;task_list&lt;/code&gt;的访问以防止竞争条件。&lt;/p&gt;
&lt;p&gt;等待队列项：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct wait_queue_entry {
    unsigned int flags;
    struct task_struct *task;  // 进程的task_struct
    struct list_head entry;    // 连接到等待队列
} wait_queue_entry_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待队列项代表正在等待事件的进程，进程会被挂载至&lt;code&gt;task_list&lt;/code&gt;，进入睡眠。&lt;/p&gt;
&lt;p&gt;这里结构体中的&lt;code&gt;task_struct&lt;/code&gt;是Linux的进程描述符，该结构体会管理所有的进程，这是一个非常长而且很复杂的结构体。其中有一个重要的量&lt;code&gt;volatile long state&lt;/code&gt;，用于表示进程当前的状态，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;TASK_RUNNING&lt;/code&gt;：正在运行或准备运行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;：处于可被中断的睡眠状态，等待某个事件发生。此过程下进程可被信号唤醒&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_UNINTERRUPTIBLE&lt;/code&gt;：处于不可被中断的睡眠状态，等待某个事件发生。此过程下进程不可被信号唤醒，只能等待事件发生&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_STOPPED&lt;/code&gt;：进程停止执行&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_TRACED&lt;/code&gt;：进程正在被调试器追踪&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_DEAD&lt;/code&gt;：进程已终止，等待被释放&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_WAKEKILL&lt;/code&gt;：进程处于可被杀死的睡眠状态，即使在不可中断的睡眠中，也能被特定信号唤醒或终止&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_WAKING&lt;/code&gt;：进程正在从睡眠状态中被唤醒，尚未进入可运行队列&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TASK_NOLOAD&lt;/code&gt;：进程不应影响负载计算，通常用于内核线程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于等待队列，前三种状态比较常见。&lt;/p&gt;
&lt;h3&gt;等待队列的使用方式&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;定义和初始化等待队列头&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct xxx_dev {
    .......
    wait_queue_head_t r_wait;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;设定等待条件和事件（以按键为例）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    DECLARE_WAITQUEUE(wait, current);   /* 创建等待队列 */
    if (atomic_read(&amp;amp;dev-&amp;gt;releasekey) == 0){    /* 如果按键未按下 */
        add_wait_queue(&amp;amp;dev-&amp;gt;r_wait, &amp;amp;wait);    /* 添加当前线程至等待队列 */
        __set_current_state(TASK_INTERRUPTIBLE);    /* 切换进程模式至可中断睡眠状态 */
        schedule(); /* 让出CPU，等待被唤醒 */
        if (signal_pending(current)){   /* 如果有信号打断睡眠，退出 */
            ret = -ERESTARTSYS;
            goto wait_error;
        }
        __set_current_state(TASK_RUNNING);  /* 进程恢复运行 */
        remove_wait_queue(&amp;amp;dev-&amp;gt;r_wait, &amp;amp;wait); /* 从等待队列中移除当前线程 */
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DECLARE_WAITQUEUE(name, tsk)&lt;/code&gt;：用于创建等待队列，&lt;code&gt;name&lt;/code&gt;为队列名称，&lt;code&gt;tsk&lt;/code&gt;为队列所对应的进程，一般使用&lt;code&gt;current&lt;/code&gt;，表示当前线程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)&lt;/code&gt;：&lt;code&gt;q&lt;/code&gt;为等待队列要加入的等待队列头，&lt;code&gt;wait&lt;/code&gt;为要加入的等待队列项&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;设定唤醒条件（以按键为例）&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;    /* 唤醒进程 */
    if(atomic_read(&amp;amp;dev-&amp;gt;releasekey)){  /* 完成一次按键过程 */
        wake_up_interruptible(&amp;amp;dev-&amp;gt;r_wait);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此处可选&lt;code&gt;wake_up()&lt;/code&gt;和&lt;code&gt;wake_up_interruptible()&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wake_up()&lt;/code&gt;会唤醒所有在&lt;code&gt;r_wait&lt;/code&gt;所属队列中等待的进程&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wake_up_interruptible&lt;/code&gt;仅唤醒一个处于&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;状态的进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;唤醒后，进程将在被调度器选择后从&lt;code&gt;schedule()&lt;/code&gt;位置开始继续执行。&lt;/p&gt;
&lt;h3&gt;“信号”&lt;/h3&gt;
&lt;p&gt;上节提到在&lt;code&gt;wake_up_interruptible()&lt;/code&gt;下睡眠中的进程会被信号唤醒。信号是Linux的异步进程间通信机制，用于通知进程发生了某种事件，比如用户按下 Ctrl + C 终止进程，或者进程试图访问非法内存地址导致段错误（SIGSEGV）。&lt;/p&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;th&gt;默认处理方式&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGHUP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;终端挂起（用户退出 shell）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGINT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;用户终止进程（Ctrl + C）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGKILL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;立即终止进程（不能被捕获或忽略）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;11&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGSEGV&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;段错误（非法访问内存）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGTERM&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;终止进程（默认 &lt;code&gt;kill&lt;/code&gt; 发送）&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;17&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;SIGCHLD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;子进程结束&lt;/td&gt;
&lt;td&gt;忽略&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;如果处于&lt;code&gt;TASK_INTERRUPTIBLE&lt;/code&gt;状态下的进程接收到信号，其会提前返回并唤醒睡眠中的进程。在睡眠状态被信号中断的进程通常会检查是否有信号需要处理，如果有，则提前返回，避免无限等待。&lt;/p&gt;
&lt;h2&gt;非阻塞IO&lt;/h2&gt;
&lt;p&gt;非阻塞模式下若设备不可用或数据未准备好时，设备会向kernel返回错误码。应用程序会重新读取数据（或执行其他操作），如此循环，直到数据读取成功。&lt;/p&gt;
&lt;p&gt;非阻塞模式下读取数据时，需要修改打开文件的方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fd = open(&quot;/dev/xxx_dev&quot;, O_RDWR | O_NONBLOCK);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;O_NONBLOCK&lt;/code&gt;表示使用非阻塞模式。&lt;/p&gt;
&lt;h3&gt;轮询&lt;/h3&gt;
&lt;h4&gt;Select&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds,
           fd_set *exceptfds, 
           struct timeval *timeout)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;nfds&lt;/code&gt;：监视的文件描述符的数量。它是所有文件描述符中最大值加 1。即，nfds = max(fd1, fd2, ..., fdN) + 1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;readfds&lt;/code&gt;：待检测是否可读的文件描述符集合。用 FD_SET() 宏添加文件描述符，FD_ZERO() 初始化为空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;writefds&lt;/code&gt;：待检测是否可写的文件描述符集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;exceptfds&lt;/code&gt;：待检测是否发生异常的文件描述符集合&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timeout&lt;/code&gt;：指定等待时间的结构体。如果为 NULL，则会一直等待直到有事件发生；如果为零（timeout = {0, 0}），则非阻塞地返回&lt;/li&gt;
&lt;li&gt;返回值：可操作文件描述符个数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;readfds&lt;/code&gt;、&lt;code&gt;writefds&lt;/code&gt;和&lt;code&gt;exceptfds&lt;/code&gt;都为&lt;code&gt;fd_set&lt;/code&gt;类型，指向描述符集合，用于指明关心哪些描述符，以及描述符需要满足的条件。&lt;code&gt;readfds&lt;/code&gt;负责监视指定描述符集的读变化，即监视这些文件是否可读取，只要这些集合中有一个文件可以被读取，select就会返回大于0的值表示文件可读取。若没有文件可读取，则根据&lt;code&gt;timeout&lt;/code&gt;判断是否超时。&lt;code&gt;readfds&lt;/code&gt;可以设置为NULL，表示不关心文件的可读性变化。&lt;code&gt;writefds&lt;/code&gt;和&lt;code&gt;exceptfds&lt;/code&gt;的用法也类似。&lt;/p&gt;
&lt;p&gt;如果要从一个设备文件中读取数据，可以定义一个&lt;code&gt;fd_set&lt;/code&gt;变量，用于传递给&lt;code&gt;readfds&lt;/code&gt;。&lt;code&gt;fd_set&lt;/code&gt;可以通过这几个宏进行操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void FD_ZERO(fd_set *set)           /* 清零fd_set的所有位 */
void FD_SET(int fd, fd_set *set)    /* 将fd_set某个位置1 */
void FD_CLR(int fd, fd_set *set)    /* 将fd_set某个位置0（删除某个文件描述符） */
int FD_ISSET(int fd, fd_set *set)   /* 测试一个文件是否属于某个集合 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用select对某个设备驱动文件进行非阻塞访问的示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void main(void)
{
    int ret, fd;            //要监视的文件描述符
    fd_set readfds;         //读操作文件描述符集
    struct timeval timeout; //超时结构体

    fd = open(&quot;dev_xxx&quot;, O_RDWR | O_NONBLOCK);  //非阻塞访问

    FD_ZERO(&amp;amp;readfds);      //清除readfs
    FD_SET(fd, &amp;amp;readfds);   //将fd添加至readfds

    timeout.tv_sec = 0
    timeout.tv_usec = 500000;

    ret = select(fd + 1, &amp;amp;readfds, NULL, NULL, &amp;amp;timeout);   //仅监测文件是否可读
    switch (ret)
    {
        case 0:                     //超时
            printf(&quot;timeout\r\n&quot;);
            break;
        case -1:                    //错误
            printf(&quot;error\r\n&quot;);
            break;
        default:                    //可读取数据
            if (FD_ISSET(fd, &amp;amp;readfds)){    //判断是否为fd文件描述符
                //用read函数读取数据
            }
            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;poll&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int poll(struct pollfd *fds, 
         nfds_t nfds, 
         int timeout)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;fds&lt;/code&gt;：待监视的文件描述符集合以及要监视的事件，数组元素为&lt;code&gt;pollfd&lt;/code&gt;类型：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct pollfd {
    int     fd;         //文件描述符
    short   events;     //请求的事件
    short   revents;    //返回的事件
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;events为待监视事件，包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POLLIN 有数据可以读取。
POLLPRI 有紧急的数据需要读取。
POLLOUT 可以写数据。
POLLERR 指定的文件描述符发生错误。
POLLHUP 指定的文件描述符挂起。
POLLNVAL 无效的请求。
POLLRDNORM 等同于 POLLIN
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;nfds&lt;/code&gt;：poll函数要监视的文件描述符数量
&lt;code&gt;timeout&lt;/code&gt;：超时时间&lt;/p&gt;
&lt;p&gt;poll函数使用示例:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void main(void)
{
    int ret, fd;            //要监视的文件描述符
    struct pollfd fds;

    fd = open(&quot;dev_xxx&quot;, O_RDWR | O_NONBLOCK);  //非阻塞访问

    fds.fd = fd;
    fds.event = POLLIN;     //监视文件是否可以读取

    ret = poll(&amp;amp;fds, 1, 500);   //轮询文件是否可操作，超时时间500ms
    if (ret){               //可读取
        ...
    }else if (ret == 0){    //超时
        ...
    }else if (ret &amp;lt; 0){     //错误
        ...
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;驱动程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unsigned int imx6uirq_poll(struct file *filp, struct poll_table_struct *wait)
{
    unsigned int mask = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp-&amp;gt;private_data;

    poll_wait(filp, &amp;amp;dev-&amp;gt;r_wait, wait);    //挂载驱动至等待队列

    if (atomic_read(&amp;amp;dev-&amp;gt;releasekey)){
        mask = POLLIN | POLLRDNORM;         //返回，告知有数据可读
    }

    return mask;
}

static struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
    .poll = imx6uirq_poll,      //绑定poll至fops
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;epoll&lt;/h4&gt;
&lt;p&gt;epoll主要解决大并发问题，用于解决传统的select和poll会随着监听fd数量增加而出现效率低下的问题。&lt;/p&gt;
&lt;p&gt;使用epoll前应用程序要先用&lt;code&gt;epoll_create&lt;/code&gt;创建一个epoll句柄：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int epoll_create(int size)

return:
epoll句柄，-1表示创建失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;句柄创建完成后，用&lt;code&gt;epoll_ctl&lt;/code&gt;向其中要监视的文件描述符以及监视的事件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int epoll_ctl(int   epfd,
              int   op,
              int   fd,
              struct epoll_event *event)

epfd：要操作的 epoll 句柄

op：表示要对 epfd(epoll 句柄)进行的操作，可以设置为：
EPOLL_CTL_ADD 向 epfd 添加文件参数 fd 表示的描述符。
EPOLL_CTL_MOD 修改参数 fd 的 event 事件。
EPOLL_CTL_DEL 从 epfd 中删除 fd 描述符。

fd：要监视的文件描述符。

event：要监视的事件类型，为 epoll_event 结构体类型指针：

struct epoll_event {
        uint32_t events;   /* epoll 事件 */
        epoll_data_t data; /* 用户数据 */
};

events表示要监视的事件，可选：
EPOLLIN 有数据可以读取。
EPOLLOUT 可以写数据。
EPOLLPRI 有紧急的数据需要读取。
EPOLLERR 指定的文件描述符发生错误。
EPOLLHUP 指定的文件描述符挂起。
EPOLLET 设置 epoll 为边沿触发，默认触发模式为水平触发。
EPOLLONESHOT 一次性的监视，当监视完成以后还需要再次监视某个 fd，那么就需要将
fd 重新添加到 epoll 里面。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;设置完成后，调用&lt;code&gt;epoll_wait()&lt;/code&gt;等待事件发生：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int epoll_wait(int epfd,                    //要等待的poll
               struct epoll_event *events,  //指向epoll_event结构体的数组
               int maxevents,               //events数组大小
               int timeout)                 //超时时间，ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;异步通知&lt;/h2&gt;
&lt;p&gt;异步通知通过信号用类似于软中断的方式，由驱动程序自行通知应用程序令其响应。Linux的信号类型在&lt;code&gt;arch/xtebsa/include/uapi/asm/signal/h&lt;/code&gt;中定义。&lt;/p&gt;
&lt;p&gt;信号同样有回调函数（接收到信号后要执行的内容）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sighandler_t signal(int signum, sighandler_t handler)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;signum&lt;/code&gt;：信号类别&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler&lt;/code&gt;：信号处理函数&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;驱动处理信号&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;定义一个&lt;code&gt;fasync_struct&lt;/code&gt;类型的结构体指针：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct fasync_struct {
    dev_t devid;
    ......
    struct fasync_struct *async_queue;  /* 异步结构体 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化结构体指针：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int fasync_helper(int fd, struct file *filp, int on, struct fasync_struct **fapp)
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;新建&lt;code&gt;xxx_fasync&lt;/code&gt;回调函数以处理FASYNC标志，并绑定至&lt;code&gt;fops&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;满足信号发送条件时，调用&lt;code&gt;kill_fasync&lt;/code&gt;向应用程序发送信号：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void kill_fasync(struct fasync_struct **fp, int sig, int band)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;sig&lt;/code&gt;：待发送信号类型&lt;/li&gt;
&lt;li&gt;&lt;code&gt;band&lt;/code&gt;：可读时为&lt;code&gt;POLL_IN&lt;/code&gt;，可写时为&lt;code&gt;POLL_OUT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;驱动文件关闭时释放&lt;code&gt;fasync_struct&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;xxx_fasync(-1 ,filp, 0);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;xxx_fasync&lt;/code&gt;参考示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int xxx_fasync(int fd, struct file *filp, int on)
{
    struct xxx_dev *dev = (struct xxx_dev *)filp-&amp;gt;private_data;

    if (fasync_helper(fd, filp, on, &amp;amp;dev-&amp;gt;async_queue) &amp;lt; 0)
        return -EIO;
    return 0;
}

/* 满足条件时 */
void xxx()
{
    ....
    kill_fasync(&amp;amp;dev-&amp;gt;async_queue, SIGIO, POLL_IN); /* 向应用程序发送SIGIO（表示文件描述符准备就绪, 可以开始进行输入/输出操作）信号 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用程序处理信号&lt;/h3&gt;
&lt;p&gt;应用程序首先绑定处理函数至某个特定的信号量，然后调用&lt;code&gt;fcntl&lt;/code&gt;(操作文件描述符的属性)进行初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int fcntl (int __fd, int __cmd, ...);
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;    fcntl(fd, F_SETOWN, getpid());  /* 通知kernel当前进程的进程号 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;-&lt;code&gt;F_SETOWN&lt;/code&gt;：设置&lt;code&gt;fd&lt;/code&gt;的所有者，此处为当前进程(&lt;code&gt;getpid()&lt;/code&gt;返回当前进程的ID)&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    flags = fcntl(fd, F_GETFD);     /* 获取进程状态 */
    fcntl(fd, F_SETFL, flags | FASYNC); /* 为进程启用异步通知功能 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;F_GETFD&lt;/code&gt;：获取当前&lt;code&gt;fd&lt;/code&gt;的标志位，返回整型值，描述&lt;code&gt;fd&lt;/code&gt;属性信息（如文件描述符是否关闭，是否是异步 I/O 等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;F_SETFL&lt;/code&gt;：设置&lt;code&gt;fd&lt;/code&gt;的文件状态标志&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags | FASYNC&lt;/code&gt;：将&lt;code&gt;FASYNC&lt;/code&gt;标志添加进入现有的文件描述符标志，启用文件描述符的异步IO功能。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;完整过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int fd = 0;  /* 文件描述符 */

/* 信号处理函数 */
static void sigio_signal_func(int signum)
{
    int err = 0;
    unsigned int keyvalue = 0;

    err = read(fd, &amp;amp;keyvalue, sizeof(keyvalue));
    if (err &amp;lt; 0){
        printf(&quot;sigio error\r\n&quot;);
    }else{
        printf(&quot;sigio signal, key value = %d\r\n&quot;, keyvalue);
    }
}

int main(int argc, char *argv[])
{  
    .......

    /* 绑定信号处理函数至SIGIO信号 */
    signal(SIGIO, sigio_signal_func);   

    fcntl(fd, F_SETOWN, getpid());  /* 通知kernel当前进程的进程号 */
    flags = fcntl(fd, F_GETFD);     /* 获取进程状态 */
    fcntl(fd, F_SETFL, flags | FASYNC); /* 为进程启用异步通知功能 */

    while (1){
        sleep(2);
    }

    close(fd);
    return 0;
}

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux中断机制</title><link>https://arkmostima.pages.dev/posts/linux%E4%B8%AD%E6%96%AD%E6%9C%BA%E5%88%B6/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E4%B8%AD%E6%96%AD%E6%9C%BA%E5%88%B6/</guid><description>IMX6ULL和IMX6UL的中断控制器呈兼容关系，即IMX6ULL继承IMX6UL的GIC节点信息。在imx6ul.dtsi中有：</description><pubDate>Wed, 26 Mar 2025 06:56:52 GMT</pubDate><content:encoded>&lt;h2&gt;设备树中断信息节点&lt;/h2&gt;
&lt;p&gt;IMX6ULL和IMX6UL的中断控制器呈兼容关系，即IMX6ULL继承IMX6UL的GIC节点信息。在&lt;code&gt;imx6ul.dtsi&lt;/code&gt;中有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	intc: interrupt-controller@a01000 {
		compatible = &quot;arm,gic-400&quot;, &quot;arm,cortex-a7-gic&quot;;
        /* GIC_PPI：私有外设中断，非共享
           9:中断号为9
           GIC_CPU_MASK_SIMPLE(1):PPI只发送给CPU0处理
           IRQ_TYPE_LEVEL_HIGH：电平触发，高电平有效 */
		interrupts = &amp;lt;GIC_PPI 9 (GIC_CPU_MASK_SIMPLE(1) | IRQ_TYPE_LEVEL_HIGH)&amp;gt;;
        /* 中断描述需3个单元 */
		#interrupt-cells = &amp;lt;3&amp;gt;;
        /* 声明节点为中断控制器 */
		interrupt-controller;
        /* 父类为自身，说明是顶级GIC */
		interrupt-parent = &amp;lt;&amp;amp;intc&amp;gt;;
        /* GIC寄存器基地址和大小 */
		reg = &amp;lt;0x00a01000 0x1000&amp;gt;,  /* GIC分发器基地址 */
			  &amp;lt;0x00a02000 0x2000&amp;gt;,  /* GIC CPU接口基地址 */
			  &amp;lt;0x00a04000 0x2000&amp;gt;,  /* GIC虚拟CPU接口 */
			  &amp;lt;0x00a06000 0x2000&amp;gt;;  /* GIC虚拟分发器 */
	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的&lt;code&gt;interrupt-cells&lt;/code&gt;通常格式为&lt;code&gt;&amp;lt;中断类型 中断号 标志位&amp;gt;&lt;/code&gt;或&lt;code&gt;&amp;lt;中断号 标志位&amp;gt;&lt;/code&gt;，其中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;中断类型：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;0&lt;/code&gt;：SPI，共享外设中断&lt;/li&gt;
&lt;li&gt;&lt;code&gt;1&lt;/code&gt;：PPI，私有外设中断&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;中断号：设备中断ID，SPI中断号范围为0~987，PPI中断号范围为0~15&lt;/li&gt;
&lt;li&gt;标志位：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_NONE&lt;/code&gt;：Kernel不参与设置，跟随uboot&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_EDGE_RISING&lt;/code&gt;：上升沿触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_EDGE_FALLING&lt;/code&gt;：下降沿触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_EDGE_BOTH &lt;/code&gt;：双边沿触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_LEVEL_HIGH&lt;/code&gt;：高电平触发&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IRQ_TYPE_LEVEL_LOW&lt;/code&gt;：低电平触发&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GPIO也可以作为GIC：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;gpio5: gpio@20ac000 {
		compatible = &quot;fsl,imx6ul-gpio&quot;, &quot;fsl,imx35-gpio&quot;;
		reg = &amp;lt;0x020ac000 0x4000&amp;gt;;
        /* 共享中断，中断号74，高电平触发
           共享中断，中断号75，高电平触发 */
		interrupts = &amp;lt;GIC_SPI 74 IRQ_TYPE_LEVEL_HIGH&amp;gt;,
					 &amp;lt;GIC_SPI 75 IRQ_TYPE_LEVEL_HIGH&amp;gt;;
		clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_GPIO5&amp;gt;;
		gpio-controller;
		#gpio-cells = &amp;lt;2&amp;gt;;
		interrupt-controller;   /* 声明为中断控制器 */
		gpio-ranges = &amp;lt;&amp;amp;iomuxc 0 7 10&amp;gt;, &amp;lt;&amp;amp;iomuxc 10 5 2&amp;gt;;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中断需要在节点中配置，如&lt;code&gt;imx6ul-14x14-evk.dtsi&lt;/code&gt;中&lt;code&gt;i2c1&lt;/code&gt;节点下的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	fxls8471@1e {
		compatible = &quot;fsl,fxls8471&quot;;
		reg = &amp;lt;0x1e&amp;gt;;
		position = &amp;lt;0&amp;gt;;
		interrupt-parent = &amp;lt;&amp;amp;gpio5&amp;gt;;    /* GIC为GPIO5 */
		interrupts = &amp;lt;0 8&amp;gt;; /* 中断号为0，低电平触发 */
	};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单来说，设备树节点中描述中断的信息有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;#interrupt-cells&lt;/code&gt;：中断源cell个数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interrupt-controller&lt;/code&gt;：声明当前节点为中断控制器&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interrupts&lt;/code&gt;：指定中断号和中断触发模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;interrupt-parent&lt;/code&gt;：父中断(GIC)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;用法&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;修改设备树&lt;/li&gt;
&lt;li&gt;设备结构体中声明&lt;code&gt;irqreturn_t (*handler)(int, void *)&lt;/code&gt;(也可以不声明)和&lt;code&gt;int irqnum&lt;/code&gt;(存放中断号)&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;irq_of_parse_and_map&lt;/code&gt;向设备树申请中断信息&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;unsigned int irq_of_parse_and_map(struct device_node *node, int index);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数返回申请到的中断号。
4. 初始化时向kernel申请中断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
	    const char *name, void *dev)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;irq&lt;/code&gt;：中断号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;handler&lt;/code&gt;：中断服务函数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;：标志位，可选：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#define IRQF_TRIGGER_NONE	0x00000000
#define IRQF_TRIGGER_RISING	0x00000001
#define IRQF_TRIGGER_FALLING	0x00000002
#define IRQF_TRIGGER_HIGH	0x00000004
#define IRQF_TRIGGER_LOW	0x00000008
#define IRQF_TRIGGER_MASK	(IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
				 IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE	0x00000010
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;*name&lt;/code&gt;：生成中断的设备名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*dev&lt;/code&gt;：Cookie，传递设备结构体地址&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;设备注销时释放中断：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void *free_irq(unsigned int irq, void *dev);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;irq&lt;/code&gt;：中断号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*dev&lt;/code&gt;：Cookie，传递设备结构体地址&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;例程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/libata.h&amp;gt;   /* 新版kernel不再支持ide.h */
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/of_address.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;
#include &amp;lt;linux/semaphore.h&amp;gt;
#include &amp;lt;linux/timer.h&amp;gt;
#include &amp;lt;linux/of_irq.h&amp;gt;
#include &amp;lt;linux/irq.h&amp;gt;

#define IMX6UIRQ_CNT         1
#define IMX6UIRQ_NAME   &quot;imx6uirq&quot;
#define KEY0VALUE       0x01
#define INVAKEY         0xFF
#define KEY_NUM         1

struct irq_keydesc {
    int gpio;
    int irqnum;             /* 中断号 */
    unsigned char value;    /* 按键对应的键值 */
    char name[10];          
    irqreturn_t (*handler)(int, void *);    /* 中断服务函数 */
};

struct irq_led {
    int gpio;
};

struct imx6uirq_dev {
    dev_t devid;
    struct cdev cdev;
    struct class *class;
    struct device *device;
    int major;
    int minor;
    struct device_node *nd;
    atomic_t keyvalue;      /* 有效按键键值 */
    atomic_t releasekey;    /* 标记是否完成一次按键 */
    struct timer_list timer;
    struct irq_keydesc irqkeydesc; /* 按键描述结构体 */
    struct irq_led irqled;
};

struct imx6uirq_dev imx6uirq;

/* IRQ服务函数，延时10ms，消抖 */
static irqreturn_t key0_handler(int irq, void *dev_id)
{
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)dev_id;

    mod_timer(&amp;amp;dev-&amp;gt;timer, jiffies + msecs_to_jiffies(10)); /* 延时10ms，消抖 */
    return IRQ_RETVAL(IRQ_HANDLED);
}

/* 定时器服务函数，到期后再次读取按键值，如果还是按下表示按键有效 */
void timer_function(struct timer_list *t)
{
    unsigned char value;
    struct irq_keydesc *keydesc;
    struct imx6uirq_dev *dev = from_timer(dev, t, timer);

    keydesc = &amp;amp;dev-&amp;gt;irqkeydesc;

    value = gpio_get_value(keydesc-&amp;gt;gpio);  /* 读取IO电平 */
    if (value == 0){    /* 如果按下 */
        int sta = gpio_get_value(dev-&amp;gt;irqled.gpio);
        gpio_set_value(dev-&amp;gt;irqled.gpio, !sta);
        atomic_set(&amp;amp;dev-&amp;gt;keyvalue, keydesc-&amp;gt;value);
    }else{      /* 如果松开 */
        atomic_set(&amp;amp;dev-&amp;gt;keyvalue, 0x80 | keydesc-&amp;gt;value);
        atomic_set(&amp;amp;dev-&amp;gt;releasekey, 1);    /* 标记松开按键 */
    }
}

static int keyio_init(void)
{
    int ret = 0;

    imx6uirq.nd = of_find_node_by_path(&quot;/key&quot;);
    if (imx6uirq.nd == NULL){
        printk(&quot;find node failed\r\n&quot;);
        return -EINVAL;
    }

    imx6uirq.irqkeydesc.gpio = of_get_named_gpio(imx6uirq.nd, &quot;key-gpio&quot;, 0);
    if (imx6uirq.irqkeydesc.gpio &amp;lt; 0){
        printk(&quot;unable get key gpio\r\n&quot;);
        return -EINVAL;
    }

    imx6uirq.irqled.gpio = of_get_named_gpio(imx6uirq.nd, &quot;led-gpio&quot;, 0);
    if (imx6uirq.irqled.gpio &amp;lt; 0){
        printk(&quot;unable get led gpio\r\n&quot;);
        return -EINVAL;
    }

    sprintf(imx6uirq.irqkeydesc.name, &quot;KEY0&quot;);
    gpio_request(imx6uirq.irqkeydesc.gpio, imx6uirq.irqkeydesc.name);
    ret = gpio_direction_input(imx6uirq.irqkeydesc.gpio);
    if (ret &amp;lt; 0){
        printk(&quot;unable set key pin level\r\n&quot;);
        return ret;
    }
    imx6uirq.irqkeydesc.irqnum = irq_of_parse_and_map(imx6uirq.nd, 0);
    printk(&quot;key0: gpio = %d, irqnum = %d\r\n&quot;, imx6uirq.irqkeydesc.gpio, imx6uirq.irqkeydesc.irqnum);

    gpio_request(imx6uirq.irqled.gpio, &quot;LED&quot;);
    ret = gpio_direction_output(imx6uirq.irqled.gpio, 1);
    if (ret &amp;lt; 0){
        printk(&quot;unable set led pin level\r\n&quot;);
        return ret;
    }

    imx6uirq.irqkeydesc.handler = key0_handler;
    imx6uirq.irqkeydesc.value = KEY0VALUE;

    ret = request_irq(imx6uirq.irqkeydesc.irqnum, imx6uirq.irqkeydesc.handler, IRQF_TRIGGER_FALLING|IRQF_TRIGGER_RISING, imx6uirq.irqkeydesc.name, &amp;amp;imx6uirq);

    if (ret &amp;lt; 0){
        printk(&quot;IRQ request failed, error code: %d\r\n&quot;, ret);
    }

    timer_setup(&amp;amp;imx6uirq.timer, timer_function, 0);

    return 0;
}

static int imx6uirq_open(struct inode *inode, struct file *filp)
{
    filp-&amp;gt;private_data = &amp;amp;imx6uirq;
    return 0;
}

static ssize_t imx6uirq_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    int ret = 0;
    unsigned char keyvalue = 0;
    unsigned char releasekey = 0;
    struct imx6uirq_dev *dev = (struct imx6uirq_dev *)filp-&amp;gt;private_data;

    keyvalue = atomic_read(&amp;amp;dev-&amp;gt;keyvalue);
    releasekey = atomic_read(&amp;amp;dev-&amp;gt;releasekey);

    if (releasekey){
        if (keyvalue &amp;amp; 0x80){
            keyvalue &amp;amp;= ~(0x80);
            ret = copy_to_user(buf, &amp;amp;keyvalue, sizeof(keyvalue));
        }else{
            goto data_error;
        }
        atomic_set(&amp;amp;dev-&amp;gt;releasekey, 0);
    }else{
        goto data_error;
    }
    return 0;

data_error:
    return -EINVAL;
}

static struct file_operations imx6uirq_fops = {
    .owner = THIS_MODULE,
    .open = imx6uirq_open,
    .read = imx6uirq_read,
};

static int __init imx6uirq_init(void)
{
    if (imx6uirq.major){
        imx6uirq.devid = MKDEV(imx6uirq.major, 0);
        register_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
    }else{
        alloc_chrdev_region(&amp;amp;imx6uirq.devid, 0, IMX6UIRQ_CNT, IMX6UIRQ_NAME);
        imx6uirq.major = MAJOR(imx6uirq.devid);
        imx6uirq.minor = MINOR(imx6uirq.devid);
    }
    printk(&quot;KEY reg ok, major = %d, minor = %d\r\n&quot;, imx6uirq.major, imx6uirq.minor);

    cdev_init(&amp;amp;imx6uirq.cdev, &amp;amp;imx6uirq_fops);
    cdev_add(&amp;amp;imx6uirq.cdev, imx6uirq.devid, IMX6UIRQ_CNT);

    imx6uirq.class = class_create(IMX6UIRQ_NAME);
    if (IS_ERR(imx6uirq.class)){
        return PTR_ERR(imx6uirq.class);
    }

    imx6uirq.device = device_create(imx6uirq.class, NULL, imx6uirq.devid, NULL, IMX6UIRQ_NAME);
    if (IS_ERR(imx6uirq.device)){
        return PTR_ERR(imx6uirq.device);
    }

    atomic_set(&amp;amp;imx6uirq.keyvalue, INVAKEY);
    atomic_set(&amp;amp;imx6uirq.releasekey, 0);
    keyio_init();

    return 0;
}

static void __exit imx6uirq_exit(void)
{
    del_timer_sync(&amp;amp;imx6uirq.timer);

    free_irq(imx6uirq.irqkeydesc.irqnum, &amp;amp;imx6uirq);
    gpio_free(imx6uirq.irqkeydesc.gpio);

    cdev_del(&amp;amp;imx6uirq.cdev);
    unregister_chrdev_region(imx6uirq.devid, IMX6UIRQ_CNT);
    device_destroy(imx6uirq.class, imx6uirq.devid);
    class_destroy(imx6uirq.class);
}

module_init(imx6uirq_init);
module_exit(imx6uirq_exit);
MODULE_LICENSE(&quot;GPL&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux内核定时器</title><link>https://arkmostima.pages.dev/posts/linux%E5%86%85%E6%A0%B8%E5%AE%9A%E6%97%B6%E5%99%A8/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E5%86%85%E6%A0%B8%E5%AE%9A%E6%97%B6%E5%99%A8/</guid><description>新版本Kernel下定时器API得到了比较好的优化，以正点原子阿尔法基于4.4版本kernel的旧版API为例做对比：</description><pubDate>Wed, 26 Mar 2025 05:58:19 GMT</pubDate><content:encoded>&lt;p&gt;新版本Kernel下定时器API得到了比较好的优化，以正点原子阿尔法基于4.4版本kernel的旧版API为例做对比：&lt;/p&gt;
&lt;h2&gt;定时器回调函数：&lt;/h2&gt;
&lt;h3&gt;旧版：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void timer_function(unsigned long arg)
 {
 struct timer_dev *dev = (struct timer_dev *)arg;
 static int sta = 1;

 /* ....... */

 }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;新版：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void timer_function(struct timer_list *t)
{
	struct timer_dev *dev = from_timer(dev, t, timer);	/* 从定时器实例获取父级结构体 */
	static int sta = 1;

 /* ....... */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新版定时器回调函数形参直接为timer_list实例，而定时器父级结构体不再通过&lt;code&gt;arg&lt;/code&gt;获取，kernel提供了&lt;code&gt;from_timer&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define from_timer(var, callback_timer, timer_fieldname) \
	container_of(callback_timer, typeof(*var), timer_fieldname)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个宏自&lt;code&gt;container_of()&lt;/code&gt;扩展而来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * container_of - cast a member of a structure out to the containing structure
 * @ptr:	the pointer to the member.
 * @type:	the type of the container struct this is embedded in.
 * @member:	the name of the member within the struct.
 *
 * WARNING: any const qualifier of @ptr is lost.
 */
#define container_of(ptr, type, member) ({				\
	void *__mptr = (void *)(ptr);					\
	static_assert(__same_type(*(ptr), ((type *)0)-&amp;gt;member) ||	\
		      __same_type(*(ptr), void),			\
		      &quot;pointer type mismatch in container_of()&quot;);	\
	((type *)(__mptr - offsetof(type, member))); })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;container_of&lt;/code&gt;能从结构体成员指针推导出其父级结构体的指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ptr&lt;/code&gt;：指向结构体 &lt;code&gt;member&lt;/code&gt; 成员的指针。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;type&lt;/code&gt;：包含 &lt;code&gt;member&lt;/code&gt; 成员的结构体类型。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;member&lt;/code&gt;：&lt;code&gt;type&lt;/code&gt; 结构体中的成员变量名。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;扩展后，&lt;code&gt;from_timer&lt;/code&gt;能根据以下形参计算出其所属的父级结构体(这里为&lt;code&gt;timer_dev&lt;/code&gt;)的指针：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;var&lt;/code&gt;：目标结构体的变量（不是指针，而是变量名）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;callback_timer&lt;/code&gt;：指向 &lt;code&gt;timer_list&lt;/code&gt; 结构体的指针（定时器回调传入的参数）。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;timer_fieldname&lt;/code&gt;：&lt;code&gt;timer_list&lt;/code&gt; 在 &lt;code&gt;var&lt;/code&gt; 结构体中的字段名。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;定时器初始化函数&lt;/h2&gt;
&lt;h3&gt;旧版&lt;/h3&gt;
&lt;p&gt;旧版API比较复杂，还要手动设置结构体成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init_timer(&amp;amp;timerdev.timer);
timerdev.timer.function = timer_function;
timerdev.timer.data = (unsigned long)&amp;amp;timerdev;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;新版&lt;/h3&gt;
&lt;p&gt;Kernel提供&lt;code&gt;timer_setup()&lt;/code&gt;，这个宏实际上由&lt;code&gt;init_timer_key()&lt;/code&gt;函数扩展而来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define timer_setup(timer, callback, flags)			\
	__init_timer((timer), (callback), (flags))
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#define __init_timer(_timer, _fn, _flags)				\
	init_timer_key((_timer), (_fn), (_flags), NULL, NULL)
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;void init_timer_key(struct timer_list *timer, void (*func)(struct timer_list*), unsigned int flags, const char *name, struct lock_class_key *key)

usage: initialize a timer

Parameters:

struct timer_list *timer
the timer to be initialized

void (*func)(struct timer_list *)
timer callback function

unsigned int flags
timer flags

const char *name
name of the timer

struct lock_class_key *key
lockdep class key of the fake lock used for tracking timer sync lock dependencies

Description:

init_timer_key() must be done to a timer prior to calling any of the other timer functions.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上&lt;code&gt;timer_setup(&amp;amp;timerdev.timer, timer_function, 0)&lt;/code&gt;就等于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;init_timer_key(&amp;amp;timerdev.timer, timer_function, 0, NULL, NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的&lt;code&gt;timer_function&lt;/code&gt;必须使用&lt;code&gt;timer_list*&lt;/code&gt;类型作为形参，不能用旧版的&lt;code&gt;arg&lt;/code&gt;去传递。&lt;/p&gt;
&lt;h2&gt;从正点原子阿尔法第五十章示例代码修改而来的新代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/libata.h&amp;gt;   /* 新版kernel不再支持ide.h */
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;linux/of.h&amp;gt;
#include &amp;lt;linux/of_address.h&amp;gt;
#include &amp;lt;linux/of_gpio.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;
#include &amp;lt;linux/semaphore.h&amp;gt;
#include &amp;lt;linux/timer.h&amp;gt;

#define 	TIMER_CNT     	1
#define 	TIMER_NAME      &quot;timer&quot;
#define 	CLOSE_CMD     	(_IO(0xEF, 0x1))
#define 	OPEN_CMD 	    (_IO(0xEF, 0x2))
#define 	SETPERIOD_CMD  	(_IO(0xEF, 0x3))
#define 	LED_ON 	      	1
#define 	LED_OFF        	0

/* Timer device struct */
struct timer_dev{
 dev_t devid;			
 struct cdev cdev;		
 struct class *class;	
 struct device *device;	
 int major;				
 int minor;				
 struct device_node *nd;	
 int led_gpio;
 int timeperiod;			/* 定时器周期(ms) */
 struct timer_list timer;	/* 定时器 */
 spinlock_t lock;			/* 自旋锁 */
};

struct timer_dev timerdev;	/* 声明定时器实例 */

static int led_init(void)
{
	int ret = 0;

	/* 从设备树查找led节点 */
	timerdev.nd = of_find_node_by_path(&quot;/gpioled&quot;);
	if (timerdev.nd == NULL){
		return -EINVAL;
	}

	/* 从节点查找gpiopin */
	timerdev.led_gpio = of_get_named_gpio(timerdev.nd, &quot;led-gpio&quot;, 0);
	if (timerdev.led_gpio &amp;lt; 0){
		printk(&quot;Unable get led gpio\r\n&quot;);
		return -EINVAL;
	}

	/* 申请GPIO */
	gpio_request(timerdev.led_gpio, &quot;led&quot;);

	/* 上拉输出 */
	ret = gpio_direction_output(timerdev.led_gpio, 1);	
	if (ret &amp;lt; 0){
		printk(&quot;cannot set gpio\r\n&quot;);
		return ret;
	}

	return 0;
}

static int timer_open(struct inode *inode, struct file *filp)
{
	int ret = 0;
	filp-&amp;gt;private_data = &amp;amp;timerdev;	/* 设置私有变量以保存结构体 */

	timerdev.timeperiod = 1000;	/* 默认定时器周期1000ms */

	/* 初始化LED */
	ret = led_init();
	if (ret &amp;lt; 0){
		printk(&quot;led initialize failed\r\n&quot;);
		return ret;
	}

	return 0;
}

static long timer_unlocked_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
	struct timer_dev *dev = (struct timer_dev *)filp-&amp;gt;private_data;	/* 从fops获取保存的timerdev */
	int timerperiod;
	unsigned long flags;

	switch (cmd) {
		/* 关闭定时器 */
		case CLOSE_CMD:
			del_timer_sync(&amp;amp;dev-&amp;gt;timer);
			break;

		/* 打开定时器 */
		case OPEN_CMD:
			spin_lock_irqsave(&amp;amp;dev-&amp;gt;lock, flags);	/* 上锁 */
			timerperiod = dev-&amp;gt;timeperiod;	/* 保护状态下修改timerperiod为默认定时器周期 */
			spin_unlock_irqrestore(&amp;amp;dev-&amp;gt;lock, flags);	/* 解锁 */
			mod_timer(&amp;amp;dev-&amp;gt;timer, jiffies + msecs_to_jiffies(timerperiod));	/* 重启定时器 */
			break;

		/* 修改定时器周期 */
		case SETPERIOD_CMD:
			spin_lock_irqsave(&amp;amp;dev-&amp;gt;lock, flags);	/* 上锁 */
			dev-&amp;gt;timeperiod = arg;	/* 保护状态下从用户空间获取定时器周期并赋值 */
			spin_unlock_irqrestore(&amp;amp;dev-&amp;gt;lock, flags);	/* 解锁 */
			mod_timer(&amp;amp;dev-&amp;gt;timer, jiffies + msecs_to_jiffies(arg));	/* 重启定时器 */
			break;

		default:
			break;
	}
	return 0;
}
	
static struct file_operations timer_fops = {
	.owner = THIS_MODULE,
	.open = timer_open,
	.unlocked_ioctl = timer_unlocked_ioctl,
};

/* 定时器回调 */
void timer_function(struct timer_list *t)
{
	struct timer_dev *dev = from_timer(dev, t, timer);	/* 从定时器实例获取父级结构体 */
	static int sta = 1;
	int timerperiod;
	unsigned long flags;

	/* 翻转LED */
	sta = !sta;
	gpio_set_value(dev-&amp;gt;led_gpio, sta);

	spin_lock_irqsave(&amp;amp;dev-&amp;gt;lock, flags);	/* 上锁 */
	timerperiod = dev-&amp;gt;timeperiod;	/* 保护状态下获取当前定时器周期 */
	spin_unlock_irqrestore(&amp;amp;dev-&amp;gt;lock, flags);	/* 解锁 */
	mod_timer(&amp;amp;dev-&amp;gt;timer, jiffies + msecs_to_jiffies(dev-&amp;gt;timeperiod));	/* 重启定时器 */
}

static int __init timer_init(void)
{
	/* 初始化自旋锁 */
	spin_lock_init(&amp;amp;timerdev.lock);

	/* 注册设备 */
	if (timerdev.major){
		timerdev.devid = MKDEV(timerdev.major, 0);
		register_chrdev_region(timerdev.devid, TIMER_CNT, TIMER_NAME);
	}else{
		alloc_chrdev_region(&amp;amp;timerdev.devid, 0, 1, TIMER_NAME);
		timerdev.major = MAJOR(timerdev.devid);
		timerdev.minor = MINOR(timerdev.devid);
	}
	printk(&quot;Device reg ok, major = %d, minor = %d\r\n&quot;, timerdev.major, timerdev.minor);

	/* 配置CDEV*/
	timerdev.cdev.owner = THIS_MODULE;
	cdev_init(&amp;amp;timerdev.cdev, &amp;amp;timer_fops);
	cdev_add(&amp;amp;timerdev.cdev, timerdev.devid, TIMER_CNT);

	/* 配置CLASS */
	timerdev.class = class_create(TIMER_NAME);
	if (IS_ERR(timerdev.class)){
		return PTR_ERR(timerdev.class);
	}

	/* 配置DEVICE */
	timerdev.device = device_create(timerdev.class, NULL, timerdev.devid, NULL, TIMER_NAME);
	if (IS_ERR(timerdev.device))
	{
		return PTR_ERR(timerdev.device);
	}

	/* 初始化定时器 */
	timer_setup(&amp;amp;timerdev.timer, timer_function, 0);

	return 0;
}

static void __exit timer_exit(void)
{
	gpio_set_value(timerdev.led_gpio, 1);
	del_timer_sync(&amp;amp;timerdev.timer);

	cdev_del(&amp;amp;timerdev.cdev);
	unregister_chrdev_region(timerdev.devid, TIMER_CNT);

	device_destroy(timerdev.class, timerdev.devid);
	class_destroy(timerdev.class);
}

module_init(timer_init);
module_exit(timer_exit);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux并发与竞争</title><link>https://arkmostima.pages.dev/posts/linux%E5%B9%B6%E5%8F%91%E4%B8%8E%E7%AB%9E%E4%BA%89/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E5%B9%B6%E5%8F%91%E4%B8%8E%E7%AB%9E%E4%BA%89/</guid><description>原子操作能够在无锁条件下实现线程安全操作，避免多个线程同时修改变量导致数据竞争。比较适用于简单的计数或标志变量等小型共享数据。但是原子操作只能保证单个原子变量的原子性，不能保护复杂数据结构。并且虽然其不会导致线程阻塞，但可能导致活锁，即…</description><pubDate>Mon, 24 Mar 2025 08:36:02 GMT</pubDate><content:encoded>&lt;h2&gt;原子操作&lt;/h2&gt;
&lt;p&gt;原子操作能够在无锁条件下实现线程安全操作，避免多个线程同时修改变量导致数据竞争。比较适用于简单的计数或标志变量等小型共享数据。但是原子操作只能保证单个原子变量的原子性，不能保护复杂数据结构。并且虽然其不会导致线程阻塞，但可能导致活锁，即多个线程不断重试并竞争CPU资源。&lt;/p&gt;
&lt;p&gt;驱动中通过原子操作来保护进程的方式为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;在设备结构体中声明原子变量&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct gpioled_dev{
    ......
    atomic_t lock;   /* 原子变量 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;open函数中判断原子变量的值来检查目标设备有无被别的应用调用&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int led_open(...)
{
    if (!atomic_dec_and_test(&amp;amp;gpioled.lock)){
        atomic_inc(&amp;amp;gpioled.lock);
        return -EBUSY;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码的逻辑是，&lt;code&gt;atomic_dec_and_test&lt;/code&gt;将原子变量递减并检查其是否为0.如果为0，表示当前线程获得访问权限；如果递减后原子变量仍大于0，说明已经有其他线程持有权限，当前线程不能访问。如果当前线程可以访问，则将原子变量恢复原值，避免影响计数。&lt;/p&gt;
&lt;p&gt;在驱动文件关闭时，释放原子变量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int led_release(...)
{
    ......
    atomic_inc(&amp;amp;dev-&amp;gt;lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在驱动入口函数内，初始化原子变量为1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int __init led_init(void)
{
    ...

    atomic_set(&amp;amp;gpioled.lock, 1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;自旋锁(Spinlock)&lt;/h2&gt;
&lt;p&gt;这是一种轻量级的锁，通过不断循环检查锁的状态来等待资源，而非让线程进入睡眠。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线程在等待锁时会不断检查，不释放CPU时间&lt;/li&gt;
&lt;li&gt;适用于&lt;strong&gt;临界区执行时间短&lt;/strong&gt;的场景，否则浪费CPU资源&lt;/li&gt;
&lt;li&gt;需要显式加锁/解锁，适用于保护复杂的数据结构&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;自旋锁保护的是临界区，即&lt;code&gt;spin_lock_irqsave()&lt;/code&gt;到&lt;code&gt;spin_unlock_irqrestore()&lt;/code&gt;之间的
代码所操作的共享变量。&lt;/p&gt;
&lt;p&gt;自旋锁适用于临界区执行时间短或锁的持有时间短的情况，或多核CPU场景。操作自旋锁时，一般使用&lt;code&gt;spin_lock_irqsave&lt;/code&gt;和&lt;code&gt;spin_unlock_irqrestore&lt;/code&gt;函数，其会关闭中断，避免IRQ抢占锁以造成竞争问题。解锁时函数会一并恢复中断，防止中断永久关闭。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;函数声明：
	void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
函数功能：
	1. 保存本地中断状态
	2. 关闭本地中断
	3. 获取自旋锁
参数说明：
	lock：被定义且初始化过的锁；
	flags：保存本地中断状态；
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;驱动通过自旋锁来保护线程的方法：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设备结构体中声明设备状态和自旋锁：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;strcut gpioled_dev {
    ......
    int dev_stats; /* 设备状态，0：未使用；&amp;gt;0：已经被使用 */
    spinlock_t lock;   /* 自旋锁 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;open函数中检查锁的状态&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int led_open(...)
{
    ......
    spin_lock_irqsave(&amp;amp;gpioled.lock ,flags);    /* 上锁 */
    if (gpioled.dev_stats){     /* 如果设备已经被其他线程使用 */
        spin_unlock_irqrestore(&amp;amp;gpioled.lock, flags);   /* 解锁 */
        return -EBUSY;  /* 设备正忙 */
    }
    gpioled.dev_stats++;    /* 如果设备未打开，则更新设备状态为已经打开 */
    spin_unlock_irqrestore(&amp;amp;gpioled.lock, flags);   /* 解锁 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;release函数中更新锁的状态&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int led_release(...)
{
    ......
    spin_lock_irqsave(&amp;amp;dev-&amp;gt;lock, flags);   /* 上锁 */
    if (dev-&amp;gt;dev_stats){
        dev-&amp;gt;dev_stats--;   /* 更新设备状态至未使用 */
    }
    spin_unlock_irqrestore(&amp;amp;dev-&amp;gt;lock, flags);  /* 解锁 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;设备初始化时初始化自旋锁&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int __init led_init(void)
{
    ......
    spin_lock_init(&amp;amp;gpioled.lock);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;问题：为什么不在设备（线程）被使用后直接上锁，而是上锁后又解锁？ 这样不就防止其他线程访问了吗？&lt;/em&gt;
简单来说，锁的作用不是用来“锁住整个设备”，而是用来“保护对共享资源的访问”。
在 &lt;code&gt;led_open()&lt;/code&gt; 里，我们用自旋锁来保证检查 &amp;amp; 修改设备状态这段代码是原子的，而不是用来锁住整个设备的访问权限。&lt;/p&gt;
&lt;p&gt;假设我们在 &lt;code&gt;gpioled.dev_stats++&lt;/code&gt; 之后不解锁，而是让锁一直保持不释放，其他线程就无法再进入 &lt;code&gt;led_open()&lt;/code&gt;，但这带来了两个大问题：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;整个设备会被锁死，这个设备无法再被其他线程使用&lt;/li&gt;
&lt;li&gt;&lt;code&gt;led_release&lt;/code&gt;无法再获取锁，设备无法被释放，造成死锁。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;也就是说，锁保护的不是这个设备，而是设备中的某个量(这里就是&lt;code&gt;gpioled.dev_stats&lt;/code&gt;)，这个量用于检查该设备是否已经/正在被其他线程使用。&lt;/p&gt;
&lt;h2&gt;信号量(Semaphore)&lt;/h2&gt;
&lt;p&gt;和自旋锁不同，信号量可以阻塞线程，如果获取不到信号量，线程将会进入睡眠以释放CPU资源，适用于长时间访问资源的场景（如访问文件、操作设备等）。线程会进入睡眠这点涉及上下文切换，有一定的开销。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct semaphore sem;
sema_init(&amp;amp;sem, 1);  // 初始化信号量，初始值为1（类似互斥锁）

down(&amp;amp;sem);  // 🔒 获取信号量（如果已经被占用，当前线程会进入睡眠）
shared_resource++;  // 访问共享资源
up(&amp;amp;sem);    // 🔓 释放信号量
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;互斥体(Mutex)&lt;/h2&gt;
&lt;p&gt;和信号量不同，互斥体中只有一个线程能获得锁（信号量允许多个线程同时访问），其他线程会阻塞（睡眠）。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;结构体中声明互斥体&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct mutex lock;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;open函数中获取互斥体(&lt;code&gt;interruptible&lt;/code&gt;表示该函数可被信号打断)&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;if (mutexc_lock_interruptible(&amp;amp;gpioled.lock)){
    return -ERESTARTSYS;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;release时释放互斥锁&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mutex_unlock(&amp;amp;dev-&amp;gt;lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化设备时初始化互斥锁&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mutex_init(&amp;amp;gpioled.lock);
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;对比&lt;/h2&gt;
&lt;p&gt;假设你去银行🏦取钱：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;🔢 原子变量（Atomic）：银行门口有一个“当前排队人数”显示屏，每个人来都可以安全地加 1 或减 1，但不会控制谁去办业务（只适用于简单计数）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔄 自旋锁（Spinlock）：你去银行取钱，发现柜台有人，你站在那里等，直到轮到你（CPU 忙等）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🔢 信号量（Semaphore）：银行有多个柜台，你可以去任何空闲的柜台办理业务（多个线程同时访问）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;🛑 互斥体（Mutex）：你去银行取钱，发现柜台有人，你去等候区坐着，等柜台空了再去（线程睡眠）。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux字符设备驱动框架</title><link>https://arkmostima.pages.dev/posts/linux%E5%AD%97%E7%AC%A6%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E5%AD%97%E7%AC%A6%E8%AE%BE%E5%A4%87%E9%A9%B1%E5%8A%A8%E6%A1%86%E6%9E%B6/</guid><description>字符设备是 Linux 驱动中最基本的一类设备驱动，字符设备就是一个一个字节，按照字节流进行读写操作的设备，读写数据是分先后顺序的。最常见的点灯、按键、IIC、SPI，LCD 等等都是字符设备，这些设备的驱动就叫做字符设备驱动。</description><pubDate>Fri, 14 Mar 2025 06:02:25 GMT</pubDate><content:encoded>&lt;h2&gt;字符设备&lt;/h2&gt;
&lt;p&gt;字符设备是 Linux 驱动中最基本的一类设备驱动，字符设备就是一个一个字节，按照字节流进行读写操作的设备，读写数据是分先后顺序的。最常见的点灯、按键、IIC、SPI，LCD 等等都是字符设备，这些设备的驱动就叫做字符设备驱动。&lt;/p&gt;
&lt;h2&gt;字符设备驱动工作流程&lt;/h2&gt;
&lt;h3&gt;加载/卸载驱动模块&lt;/h3&gt;
&lt;p&gt;Linux的驱动有两种工作方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;驱动编译进kernel，kernel启动时自动运行驱动程序&lt;/li&gt;
&lt;li&gt;驱动编译为模块(&lt;code&gt;.ko&lt;/code&gt;)，kernel启动后调用&lt;code&gt;insmod&lt;/code&gt;加载驱动模块&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模块有加载和卸载两种操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;module_init(xxx_init);  //注册模块加载
module_exit(xxx_exit);  //注册模块卸载
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;module_init&lt;/code&gt;用于向Kernel注册一个模块加载函数，当&lt;code&gt;insmod&lt;/code&gt;时，该函数就会被Kernel调用。&lt;/p&gt;
&lt;p&gt;字符设备驱动的模块加载和卸载模板如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 驱动入口 */
static int __init xxx_init(void)
{
    /* 入口函数具体内容 */
    return 0;
}

/* 驱动出口 */
static void __exit xxx_exit(void)
{
    /* 出口函数具体内容 */
}

/* 将上面两个函数指定为驱动的入口和出口函数 */
module_init(xxx_init); 
module_exit(xxx_exit); 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;驱动编译完成后，Kernel有两种命令用于加载驱动模块：&lt;code&gt;insmod&lt;/code&gt;和&lt;code&gt;modprobe&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insmod&lt;/code&gt;是最简单的模块加载命令，用于加载指定的&lt;code&gt;.ko&lt;/code&gt;模块，比如&lt;code&gt;insmod drv.ko&lt;/code&gt;。但&lt;code&gt;insmod&lt;/code&gt;不能解决模块的依赖关系。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;modprobe&lt;/code&gt;可以自动分析模块的依赖关系，然后加载所有的依赖模块到Kernel中。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;模块卸载则使用&lt;code&gt;rmmod&lt;/code&gt;或&lt;code&gt;modprobe -r&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;推荐：加载使用&lt;code&gt;modprobe&lt;/code&gt;，卸载使用&lt;code&gt;rmmod&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;注册与注销&lt;/h3&gt;
&lt;p&gt;模块加载成功后需要注册字符设备，卸载模块时也需要注销相关字符设备：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline int register_chrdev(unsigned int major, const char *name, const struct file_operation *fops)
static inline void unregister_chrdev(unsigned int major, const char *name)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;register_chrdev&lt;/code&gt;用于注册字符设备，形参有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;major&lt;/code&gt;：主设备号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：设备名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fops&lt;/code&gt;：结构体&lt;code&gt;file_operations&lt;/code&gt;类型指针，指向操作函数集合变量&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;unregister_chrdev&lt;/code&gt;用于注册字符设备，形参有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;major&lt;/code&gt;：主设备号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：设备名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一半字符设备的注册在入口函数中进行，注销在出口函数中执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static struct file_operations test_fops;

static int __init xxx_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, &quot;chrtest&quot;, &amp;amp;test_ops);  /* 主设备号200，设备名&quot;chrtest&quot; */
    if (retvalue &amp;lt; 0)
    {
        error_handler();
    }

    return 0;
}

static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, &quot;chrtest&quot;);
}

/* 将个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;终端中&lt;code&gt;car /proc/devices&lt;/code&gt;可查看当前已被使用的设备号&lt;/p&gt;
&lt;h3&gt;实现设备的具体操作函数&lt;/h3&gt;
&lt;p&gt;在对&lt;code&gt;test_fops&lt;/code&gt;进行初始化之前，首先要分析需求，即&lt;code&gt;chrtest&lt;/code&gt;这个设备需要实现哪些功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;对&lt;code&gt;chrtest&lt;/code&gt;能够进行打开和关闭操作。这是最基本的要求&lt;/li&gt;
&lt;li&gt;对&lt;code&gt;chrtest&lt;/code&gt;进行读写操作。假设设备控制着一段缓冲区（内存），应用需要通过&lt;code&gt;read&lt;/code&gt;和&lt;code&gt;write&lt;/code&gt;对缓冲区进行读写操作，因此需要实现&lt;code&gt;file_operations&lt;/code&gt;中的&lt;code&gt;read&lt;/code&gt;和&lt;code&gt;write&lt;/code&gt;两个函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;修改代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 打开设备 */
static int chrtest_open(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 从设备读取 */
static ssize_t chrtest_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/* 向设备写数据 */
static ssize_t chrtest_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/* 关闭/释放设备 */
static int chrtest_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 初始化file_operations结构体 */
static struct file_operations test_fops = {
    .owner = THIS_MODULE, 
    .open = chrtest_open,
    .read = chrtest_read,
    .write = chrtest_write,
    .release = chrtest_release,
};

static int __init xxx_init(void)
{
    int retvalue = 0;

    /* 注册字符设备驱动 */
    retvalue = register_chrdev(200, &quot;chrtest&quot;, &amp;amp;test_ops);  /* 主设备号200，设备名&quot;chrtest&quot; */
    if (retvalue &amp;lt; 0)
    {
        error_handler();
    }

    return 0;
}

static void __exit xxx_exit(void)
{
    /* 注销字符设备驱动 */
    unregister_chrdev(200, &quot;chrtest&quot;);
}

/* 将个函数指定为驱动的入口和出口函数 */
module_init(xxx_init);
module_exit(xxx_exit);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的函数形参需要使用Linux驱动框架所规定的标准格式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;struct inode *inode&lt;/code&gt;：指向设备文件的 索引节点，描述了文件的元数据（如权限、文件类型等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;struct file *filp&lt;/code&gt;：表示 已打开的文件，包含文件的当前状态、访问模式等信息。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;char __user *buf&lt;/code&gt;：用户空间的缓冲区，内核需要将数据拷贝到这个缓冲区&lt;/li&gt;
&lt;li&gt;&lt;code&gt;size_t cnt&lt;/code&gt;：写入的字节数，表示用户请求写入的长度&lt;/li&gt;
&lt;li&gt;&lt;code&gt;loff_t *offt&lt;/code&gt;：文件偏移量，用于支持文件的随机访问&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;添加LICENSE和作者信息&lt;/h3&gt;
&lt;p&gt;LICENSE不添加编译会报错：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MODULE_LICENSE();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;设备号分配&lt;/h3&gt;
&lt;h4&gt;静态分配&lt;/h4&gt;
&lt;p&gt;WIP&lt;/p&gt;
&lt;h4&gt;动态分配&lt;/h4&gt;
&lt;p&gt;推荐使用动态分配设备号：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;dev&lt;/code&gt;：保存申请到的设备号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;baseminor&lt;/code&gt;：次设备号起始地址，alloc_chrdev_region 可以申请一段连续的多个设备号，这些设备号的主设备号一样，但是次设备号不同，次设备号以 baseminor 为起始地址地址开始递增。一般 baseminor 为 0，也就是说次设备号从 0 开始。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;：要申请的设备号数量&lt;/li&gt;
&lt;li&gt;&lt;code&gt;name&lt;/code&gt;：设备名&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;释放设备号函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void unregister_chrdev_region(dev_t from, unsigned count)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;from&lt;/code&gt;：要释放的设备号&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;：从&lt;code&gt;from&lt;/code&gt;开始要释放的设备号数量&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;cdev&lt;/h4&gt;
&lt;p&gt;cdev 是 Linux 内核中用于表示字符设备的一个结构体。它是 Linux 字符设备驱动中非常重要的一部分，主要用于管理字符设备的注册和文件操作的绑定。当注册一个字符设备时，cdev 会帮助内核处理与设备相关的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct cdev {
    struct kobject kobj;
    struct module *owner;
    const struct file_operations *ops;  // 设备的文件操作结构
    struct list_head list;  // 链接到系统设备列表
    dev_t dev;  // 设备号
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;cdev 的主要任务是将文件操作函数（例如：open、read、write 等）和设备进行关联。当设备被打开、读写或释放时，内核会通过 cdev 结构体找到相应的操作函数并调用它们。&lt;/p&gt;
&lt;p&gt;cdev应当按照顺序被使用：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;初始化&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;struct cdev chrdev_cdev;
cdev_init(&amp;amp;chrdev_cdev, &amp;amp;fops);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个操作可以通过&lt;code&gt;cdev_alloc()&lt;/code&gt;来一次性完成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct cdev *cdev_alloc(void)
{
	struct cdev *p = kzalloc(sizeof(struct cdev), GFP_KERNEL);
	if (p) {
		INIT_LIST_HEAD(&amp;amp;p-&amp;gt;list);
		kobject_init(&amp;amp;p-&amp;gt;kobj, &amp;amp;ktype_cdev_dynamic);
	}
	return p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注册字符设备：使用 &lt;code&gt;cdev_add&lt;/code&gt; 函数将其注册到内核。这样，内核就能够在收到对该设备的请求时，调用对应的文件操作函数。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;int ret = cdev_add(&amp;amp;chrdev_cdev, dev_num, 1);
if (ret &amp;lt; 0) {
    printk(&quot;Failed to add cdev\n&quot;);
    return ret;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;注销字符设备&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;cdev_del(&amp;amp;chrdev_cdev);
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;其他&lt;/h4&gt;
&lt;p&gt;驱动中还会用到&lt;code&gt;copy_to_user&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static inline long copy_to_user(void __user *to, const void *from, unsigned long n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该函数用于将内核空间中的数据复制到用户空间。&lt;/p&gt;
&lt;h2&gt;测试APP&lt;/h2&gt;
&lt;h3&gt;C库文件操作&lt;/h3&gt;
&lt;h4&gt;open()&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;int open(const char *pathname, int flags)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pathname&lt;/code&gt;:要打开的设备或者文件名&lt;/li&gt;
&lt;li&gt;&lt;code&gt;flags&lt;/code&gt;：文件打开模式，以下三种模式必选其一：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;O_RDONLY&lt;/code&gt; 只读模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_WRONLY&lt;/code&gt; 只写模式&lt;/li&gt;
&lt;li&gt;&lt;code&gt;O_RDWR&lt;/code&gt; 读写模式&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果文件打开成功的话返回文件的文件描述符.&lt;/p&gt;
&lt;h3&gt;read()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ssize_t read(int fd, void *buf, size_t count)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fd&lt;/code&gt;:要读取的文件描述符，读取文件之前要先用 open 函数打开文件，open 函数打开文件成功以后会得到文件描述符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buf&lt;/code&gt;:数据读取到此 buf 中&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;:要读取的数据长度，也就是字节数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;读取成功的话返回读取到的字节数；如果返回 0 表示读取到了文件末尾；如果返回负值，表示读取失败。&lt;/p&gt;
&lt;h3&gt;write()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;ssize_t write(int fd, const void *buf, size_t count);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;fd&lt;/code&gt;:要进行写操作的文件描述符，写文件之前要先用 open 函数打开文件，open 函数打开文件成功以后会得到文件描述符&lt;/li&gt;
&lt;li&gt;&lt;code&gt;buf&lt;/code&gt;:要写入的数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;count&lt;/code&gt;:要写入的数据长度，也就是字节数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;返回值：写入成功的话返回写入的字节数；如果返回 0 表示没有写入任何数据；如果返回
负值，表示写入失败&lt;/p&gt;
&lt;h3&gt;close()&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int close(int fd);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;0 表示关闭成功，负值表示关闭失败。&lt;/p&gt;
&lt;h2&gt;fops、cdev、class、device&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义 fops，提供 read/write/open/close/ioctl 操作&lt;/li&gt;
&lt;li&gt;定义 cdev，并绑定 fops&lt;/li&gt;
&lt;li&gt;注册 cdev，并分配 major/minor 设备号&lt;/li&gt;
&lt;li&gt;创建 class，供 udev 识别&lt;/li&gt;
&lt;li&gt;创建 device，在 &lt;code&gt;/dev/&lt;/code&gt; 下自动生成设备文件&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;完整流程&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;定义设备信息，用于注册设备至kernel&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static dev_t chrdev_devno;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;定义文件操作结构体，用于封装用于操作外设的功能函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static struct file_operations chrdev_fops = {
    .owner = THIS_MODULE,
    .open = chrdev_open,
    .read = chrdev_read,
    .write = chrdev_write,
    .release = chrdev_release,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;实现设备操作函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int chrdev_open(struct inode *inode, struct file *filp) {
    return 0;
}

static ssize_t chrdev_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt) {
    return 0;
}

static ssize_t chrdev_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt) {
    return 0;
}

static int chrdev_release(struct inode *inode, struct file *filp) {
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;初始化和清理&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;static int __init chrdev_init(void) {
    int ret;
    ret = alloc_chrdev_region(&amp;amp;chrdev_devno, 0, 1, &quot;chrdevbase&quot;);   //动态分配设备号
    if (ret &amp;lt; 0) {
        printk(&quot;Failed to register device\n&quot;);
        return ret;
    }
    cdev_init(&amp;amp;chrdev_cdev, &amp;amp;chrdev_fops);      //注册cdev
    ret = cdev_add(&amp;amp;chrdev_cdev, chrdev_devno, 1);      //添加cdev
    if (ret &amp;lt; 0) {
        unregister_chrdev_region(chrdev_devno, 1);
        printk(&quot;Failed to add cdev\n&quot;);
        return ret;
    }
    return 0;
}

static void __exit chrdev_exit(void) {
    cdev_del(&amp;amp;chrdev_cdev);     //销毁cdev
    unregister_chrdev_region(chrdev_devno, 1);      //注销设备号
}

module_init(hello_init);
module_exit(hello_exit);
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;创建节点。如果应用程序要想使用设备，还必须创建字符设备节点：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;mknod /dev/chrdev c 249 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后应用程序才能将设备号和具体的设备连接起来并使用。&lt;/p&gt;
&lt;p&gt;如果初始化中创建了&lt;code&gt;device&lt;/code&gt;和&lt;code&gt;class&lt;/code&gt;类，会自动生成节点。&lt;/p&gt;
&lt;h2&gt;LED点灯示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/* **************Includes************** */
#include &quot;linux/fs.h&quot;
#include &quot;linux/printk.h&quot;
#include &amp;lt;linux/types.h&amp;gt;
#include &amp;lt;linux/kernel.h&amp;gt;
#include &amp;lt;linux/delay.h&amp;gt;
#include &amp;lt;linux/ide.h&amp;gt;
#include &amp;lt;linux/init.h&amp;gt;
#include &amp;lt;linux/module.h&amp;gt;
#include &amp;lt;linux/errno.h&amp;gt;
#include &amp;lt;linux/gpio.h&amp;gt;
#include &amp;lt;linux/cdev.h&amp;gt;
#include &amp;lt;linux/device.h&amp;gt;
#include &amp;lt;asm/mach/map.h&amp;gt;
#include &amp;lt;asm/uaccess.h&amp;gt;
#include &amp;lt;asm/io.h&amp;gt;

/* **************Defines************** */
#define LED_DEVICE_CNT    1
#define LED_DEVICE_NAME   &quot;led_dev&quot;
#define LEDON   1
#define LEDOFF  0

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE            (0x020C406C)
#define SW_MUX_GPIO1_IO03_BASE    (0x020E0068)
#define SW_PAD_GPIO1_IO03_BASE    (0x020E02F4)
#define GPIO1_DR_BASE             (0x0209C000)
#define GPIO1_GDIR_BASE           (0x0209C004)

/* 寄存器虚拟地址 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* 设备结构体 */
struct led_dev{
  dev_t devid;
  struct cdev cdev;
  struct class *class;
  struct device *device;
  int major;
  int minor;
};

/* 声明实例 */
struct led_dev led;

/* LED切换开关函数 */
void led_switch(u8 status)
{
  u32 val = 0;

  if (status == LEDON){
    val = readl(GPIO1_DR);
    val &amp;amp;= ~(1 &amp;lt;&amp;lt; 3);
    writel(val, GPIO1_DR);
  }else if (status == LEDOFF){
    val = readl(GPIO1_DR);
    val |= (1 &amp;lt;&amp;lt; 3);
    writel(val, GPIO1_DR);
  }
}

/***********  fops结构体函数填充 **************/
static int led_open(struct inode *inode, struct file *filp)
{
  filp-&amp;gt;private_data = &amp;amp;led;  /* 设置私有变量 */
  return 0;
}

static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
  return 0;
}

static ssize_t led_write(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
  int retvalue;
  unsigned char databuf[1];
  unsigned char led_status;

  retvalue = copy_from_user(databuf, buf, cnt);
  if (retvalue &amp;lt; 0){
    printk(&quot;copy from user failed\r\n&quot;);
    return -EFAULT;
  }

  led_status = databuf[0];

  switch (led_status) {
    case LEDON:
      led_switch(LEDON);
      break;
    case LEDOFF:
      led_switch(LEDOFF);
      break;
  }

  return 0;
}

static int led_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/* fops结构体 */
static struct file_operations led_fops = {
  .owner = THIS_MODULE,
  .open = led_open,
  .read = led_read,
  .write = led_write,
  .release = led_release,
};

/* 设备初始化函数 */
static int __init led_init(void)
{
  int retvalue = 0;
  u32 val = 0;

  /* 获取寄存器虚拟地址 */
  IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
  SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
  SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
  GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
  GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

  /* 使能时钟 */
  val =readl(IMX6U_CCM_CCGR1);
  val &amp;amp;= ~(3 &amp;lt;&amp;lt; 26);
  val |= (3 &amp;lt;&amp;lt; 26);
  writel(val, IMX6U_CCM_CCGR1);

  /* 设置 GPIO1_IO03 复用 */
  writel(5, SW_MUX_GPIO1_IO03);

  /* 配置 GPIO1_IO03 */
  writel(0x10B0, SW_PAD_GPIO1_IO03);

  /* 设置 GPIO1_IO03 为输出 */
  val = readl(GPIO1_GDIR);
  val &amp;amp;= ~(1 &amp;lt;&amp;lt; 3);
  val |= (1 &amp;lt;&amp;lt; 3);
  writel(val, GPIO1_GDIR);

  /* 注册设备 */
  if (led.major){
    /* 如果事先指定了主设备号 */
    led.devid = MKDEV(led.major, 0);
    register_chrdev_region(led.devid, LED_DEVICE_CNT, LED_DEVICE_NAME);
  }else {
    /* 动态分配 */
    alloc_chrdev_region(&amp;amp;led.devid, 0, LED_DEVICE_CNT, LED_DEVICE_NAME);
    led.major = MAJOR(led.devid);
    led.minor = MINOR(led.devid);
  }
  printk(&quot;led register ok: major= %d, minor = %d\r\n&quot;, led.major, led.minor);

  /* 初始化Cdev */
  led.cdev.owner = THIS_MODULE;
  cdev_init(&amp;amp;led.cdev, &amp;amp;led_fops);

  /* 向内核注册cdev */
  retvalue = cdev_add(&amp;amp;led.cdev, led.devid, LED_DEVICE_CNT);
  if (retvalue &amp;lt; 0){
    printk(&quot;cdev add failed\r\n&quot;);
    return retvalue;
  }

  /* 创建设备类 */
  led.class = class_create(THIS_MODULE, LED_DEVICE_NAME);
  if (IS_ERR(led.class)){
    printk(&quot;class create failed\r\n&quot;);
    return PTR_ERR(led.class);
  }

  /* 创建设备实例 */
  led.device = device_create(led.class, NULL, led.devid, NULL, LED_DEVICE_NAME);
  if (IS_ERR(led.device)){
    printk(&quot;device create failed\r\n&quot;);
    return PTR_ERR(led.device);
  }

  return 0;
}

/* 设备出口函数 */
static void __exit led_exit(void)
{
  /* 释放寄存器映射 */
  iounmap(IMX6U_CCM_CCGR1);
  iounmap(SW_MUX_GPIO1_IO03);
  iounmap(SW_PAD_GPIO1_IO03);
  iounmap(GPIO1_DR);
  iounmap(GPIO1_GDIR);

  /* 销毁cdev，注销设备号 */
  cdev_del(&amp;amp;led.cdev);
  unregister_chrdev_region(led.devid, LED_DEVICE_CNT);

  /* 销毁设备类和设备实例 */
  device_destroy(led.class, led.devid);
  class_destroy(led.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE(&quot;GPL&quot;);
MODULE_AUTHOR(&quot;aki&quot;);

&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>IMX6ULL Linux内核移植</title><link>https://arkmostima.pages.dev/posts/linux%E5%86%85%E6%A0%B8%E7%A7%BB%E6%A4%8D/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E5%86%85%E6%A0%B8%E7%A7%BB%E6%A4%8D/</guid><description>关闭主机的Ubuntu的防火墙：</description><pubDate>Tue, 11 Mar 2025 09:08:23 GMT</pubDate><content:encoded>&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;h3&gt;网络设置&lt;/h3&gt;
&lt;p&gt;关闭主机的Ubuntu的防火墙：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo ufw disable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;确认VMWare使用桥接模式，主机、虚拟机和开发板三者处于同一网段，如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主机：192.168.1.100&lt;/li&gt;
&lt;li&gt;虚拟机：192.168.1.105&lt;/li&gt;
&lt;li&gt;开发板：192.168.1.103 (DHCP获取)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在uboot下，开发板ping虚拟机和主机、虚拟机ping主机应当都能ping通。未加载Linux内核前网卡尚未使能，虚拟机ping开发板应当ping不通。&lt;/p&gt;
&lt;h3&gt;TFTP搭建&lt;/h3&gt;
&lt;p&gt;TFTP常见用途：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;U-Boot 加载 Linux 内核、设备树和根文件系统&lt;/li&gt;
&lt;li&gt;NFS 结合 TFTP 进行网络启动&lt;/li&gt;
&lt;li&gt;固件更新&lt;/li&gt;
&lt;li&gt;开发调试&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装xinetd：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install xinetd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查&lt;code&gt;/etc/xinetd.conf&lt;/code&gt;是否存在，不存在则创建，修改内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Simple configuration file for xinetd
#
# Some defaults, and include /etc/xinetd.d/
defaults
{
# Please note that you need a log_type line to be able to use log_on_success
# and log_on_failure. The default is the following :
# log_type = SYSLOG daemon info
}
includedir /etc/xinetd.d
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建TFTP目录于&lt;code&gt;/home/aki/linux&lt;/code&gt;，然后&lt;code&gt;chmod 777&lt;/code&gt;该目录。&lt;/p&gt;
&lt;p&gt;安装tftp-hpa和tftpd-hpa：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install tftp-hpa tftpd-hpa
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完后，&lt;code&gt;sudo nvim /etc/default/tftpd-hpa&lt;/code&gt;，修改tftp目录为TFTP服务器工作目录：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/linux_1.webp&quot; alt=&quot;TFTP_DIRECTORY&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后&lt;code&gt;sudo nvim /etc/xinetd.d/tftp&lt;/code&gt;，添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;server tftp
 {
 socket_type = dgram
 wait = yes
 disable = no
 user = root
 protocol = udp
 server = /usr/sbin/in.tftpd
 server_args = -s /home/aki/linux/tftp -c
 #log_on_success += PID HOST DURATION
 #log_on_failure += HOST
 per_source = 11
 cps =100 2
 flags =IPv4
 }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存，重启TFTP服务：&lt;code&gt;sudo service tftpd-hpa restart&lt;/code&gt;，重启xinetd服务：&lt;code&gt;sudo service xinetd restart&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;NFS搭建&lt;/h3&gt;
&lt;p&gt;NFS主要用于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;嵌入式设备的根文件系统（RootFS）挂载：在开发阶段，NFS 服务器可以存放 根文件系统（RootFS），嵌入式设备通过网络直接挂载，而无需烧录到 Flash 或 SD 卡&lt;/li&gt;
&lt;li&gt;远程文件访问：如开发板访问PC共享目录&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;安装NFS服务：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo apt install nfs-kernel-server
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;新建NFS共享目录并&lt;code&gt;chmod 777&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo mkdir /home/aki/linux/nfs
sudo chmod 777 /home/aki/linux/nfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;安装完后，&lt;code&gt;sudo nvim /etc/exports&lt;/code&gt;，在最后添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/home/aki/linux/nfs *(rw,sync,no_root_squash)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;保存退出，重启NFS服务：&lt;code&gt;sudo /etc/init.d/nfs-kernel-server restart&lt;/code&gt;，确认共享目录已经配置完毕：&lt;code&gt;shouwmount -e&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;Linux镜像测试&lt;/h2&gt;
&lt;p&gt;先确认官方linux镜像是否可以。在linux根目录中，修改顶层Makefile，添加&lt;code&gt;ARCH&lt;/code&gt;和&lt;code&gt;CROSS_COMPILE&lt;/code&gt;变量，然后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make clean
make imx_v7_mfg_defconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make -j16
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;make完成后makefile会提示得到两个重要的文件：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;zImage：linux内核镜像，位于&lt;code&gt;/arch/arm/boot/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;imx6ull-14x14-evk.dtb：编译后的设备树文件，位于&lt;code&gt;/arch/arm/boot/dts/&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后，启动uboot，设置uboot中的env&lt;code&gt;bootargs&lt;/code&gt;为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;console=ttymxc0,115200 root=/dev/mmcblk 1p2 rootwait rw
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表明将从eMMC/SD卡的第2分区启动 Linux。然后，修改&lt;code&gt;bootcmd&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setenv bootcmd &apos;tftp 80800000 zImage;tftp 83000000 imx6ull-14x14-evk.dtb;bootz 80800000 - 83000000`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表明将通过TFTP从虚拟机拉取内核镜像和设备树并启动内核。然后，&lt;code&gt;boot&lt;/code&gt;，启动内核。&lt;/p&gt;
&lt;h2&gt;配置主频、EMMC和网络驱动&lt;/h2&gt;
&lt;h3&gt;主频设置&lt;/h3&gt;
&lt;p&gt;WIP&lt;/p&gt;
&lt;h3&gt;EMMC设置&lt;/h3&gt;
&lt;p&gt;6.6版本的Kernel已经默认禁用1.8V供电，同时自动启用了8位宽的EMMC总线，所以不用进行其他的设置。&lt;/p&gt;
&lt;h3&gt;网络驱动&lt;/h3&gt;
&lt;p&gt;和uboot中进行过的很像。已知NXP原版设备树中的SPI4节点中的两个引脚和SR8201F的冲突，因此打开&lt;code&gt;/arch/arm/boot/dts/nxp/imx/imx6ul-14x14-evk.dtsi&lt;/code&gt;这个底层设备树，找到&lt;code&gt;pinctrl_spi4: spi4grp&lt;/code&gt;这个节点，删掉包含&lt;code&gt;GPIO5 7&lt;/code&gt;和&lt;code&gt;GPIO5 8&lt;/code&gt;的两行。然后前往&lt;code&gt;SPI4&lt;/code&gt;的子节点，把status改成&lt;code&gt;disable&lt;/code&gt;，直接禁用掉SPI4。&lt;/p&gt;
&lt;p&gt;然后，修改pinctrl复用信息，来到&lt;code&gt;&amp;amp;iomuxc&lt;/code&gt;节点，在节点末尾新增两个&lt;code&gt;pinctrl group&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pinctrl_enet1_reset: enet1resetgrp {
    fsl,pins = &amp;lt;
        /* used for enet1 reset */
        MX6ULL_PAD_SNVS_TAMPER7__GPIO5_IO07 0x10B0 
    &amp;gt;;
};

pinctrl_enet2_reset: enet2resetgrp {
    fsl,pins = &amp;lt;
        /* used for enet2 reset */
        MX6ULL_PAD_SNVS_TAMPER8__GPIO5_IO08 0x10B0 
    &amp;gt;;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按照步骤还需要修改ENET1和ENET2的网络时钟引脚配置，但是6.6版本的kernel设备树已经添加了，就不用手动操作了。&lt;/p&gt;
&lt;p&gt;继续，在&lt;code&gt;fec1&lt;/code&gt;和&lt;code&gt;fec2&lt;/code&gt;节点里加入刚刚设置好的复位引脚的&lt;code&gt;pinctrl group&lt;/code&gt;信息：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;fec1 {
    .....
    pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet1
                 &amp;amp;pinctrl_enet1_reset&amp;gt;;
    .....
}

&amp;amp;fec2 {
    .....
    pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet2
                 &amp;amp;pinctrl_enet2_reset&amp;gt;;
    .....
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续，检查设备树&lt;code&gt;fec1&lt;/code&gt;和&lt;code&gt;fec2&lt;/code&gt;节点下&lt;code&gt;mdio&lt;/code&gt;子节点中的PHY地址是否正确。新版kernel默认都是设置好的。&lt;/p&gt;
&lt;p&gt;然后，在&lt;code&gt;defconfig&lt;/code&gt;里告知kernel使用realtek的PHY驱动：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_REALTEK_PHY=y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，根据SR8201F复位后至少延时150ms才能开始使用的要求，修改&lt;code&gt;drivers/net/ethernet/freescale/fec_main.c&lt;/code&gt;，在&lt;code&gt;static void fec_reset_phy()&lt;/code&gt;函数的末尾加入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;msleep(200);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;重新编译kernel，然后启动，通过&lt;code&gt;ifconfig&lt;/code&gt;观察网卡是否工作正常（正常情况下ETH1会自动启动）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ifconfig eth0 up    //启动ETH0网卡
ifconfig eth1 up    //启动ETH1网卡
ifconfig eth0 192.168.xx.xx
ifconfig eth1 192.168.xx.xx
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;构建根文件系统&lt;/h2&gt;
&lt;p&gt;使用busybox构建，在busybox根目录的顶层Makefile中添加&lt;code&gt;ARCH&lt;/code&gt;和&lt;code&gt;CORSS_COMPILE&lt;/code&gt;，然后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make defconfig
make menuconfig
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入图形化配置界面：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选中Settings-&amp;gt;Build static binary&lt;/li&gt;
&lt;li&gt;选中Setttings-&amp;gt;vi-style line editing commands&lt;/li&gt;
&lt;li&gt;取消选中Linux Module Utilities-&amp;gt;Simpilified modutils&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编译busybox时必须使用静态库，动态库会缺库文件导致dns解析没法正常使用。&lt;/p&gt;
&lt;p&gt;然后，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;make 
make install CONFIG_PREFIX=/home/aki/linux/nfs/rootfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：1.37.0版本busybox报告有一个bug，即：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;compilation
of tc.c fails with: 

networking/tc.c: In function ‘cbq_print_opt’:
networking/tc.c:236:27: error: ‘TCA_CBQ_MAX’ undeclared (first use in this
function); did you mean ‘TCA_CBS_MAX’?
  236 |         struct rtattr *tb[TCA_CBQ_MAX+1];
      |                           ^~~~~~~~~~~
      |                           TCA_CBS_MAX
————————————————
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和CBQ队列管理有关的大量宏会报错，解决方法是把&lt;code&gt;tc.c&lt;/code&gt;从&lt;code&gt;/busybox/networking&lt;/code&gt;文件夹里删除。&lt;/p&gt;
&lt;p&gt;然后，需要将编译器所包含的库文件全部打包到根目录下。&lt;/p&gt;
&lt;p&gt;使用NFS挂载时，bootargs要设成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setenv bootargs &apos;console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.105:/home/aki/linux/nfs/rootfs,vers=3,proto=tcp rw ip=192.168.1.103:192.168.1.105:192.168.1.1:255.255.255.0::eth0:off&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：这里通过&lt;code&gt;vers=3&lt;/code&gt;强制使用NFS v3版本，否则会挂载失败。&lt;/p&gt;
&lt;p&gt;如果碰到hotplug：can’t create /proc/sys/kernel/hotplug: nonexistent directory问题：&lt;a href=&quot;https://blog.csdn.net/m0_51149752/article/details/146395215?spm=1001.2014.3001.5502&quot;&gt;参见&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;fstab&lt;/code&gt;要加一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mdev  /dev  ramfs defaults 0 0
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>NXP 官方u-boot移植和启动过程</title><link>https://arkmostima.pages.dev/posts/uboot%E7%A7%BB%E6%A4%8D/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/uboot%E7%A7%BB%E6%A4%8D/</guid><description>Github 下载后拷贝tar.bz2至Linux，然后tar -vxjf</description><pubDate>Fri, 07 Mar 2025 05:58:29 GMT</pubDate><content:encoded>&lt;h2&gt;官方U-Boot下载&lt;/h2&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/nxp-imx/uboot-imx&quot;&gt;Github&lt;/a&gt;
下载后拷贝&lt;code&gt;tar.bz2&lt;/code&gt;至Linux，然后&lt;code&gt;tar -vxjf&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;U-boot工程目录&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/uboot_7.webp&quot; alt=&quot;编译后的uboot源码&quot; /&gt;
除了文件夹之外，还有一些文件：
&lt;img src=&quot;/images/linux/uboot_8.webp&quot; alt=&quot;编译后的uboot源码&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;验证defconfig&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;cd /configs/&lt;/code&gt;，确认存在有I.MX6ULL的配置文件：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/uboot_1.webp&quot; alt=&quot;mx6ull配置文件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;新版本uboot中&lt;code&gt;mx6ull_14x14_evk_defconfig&lt;/code&gt;用于传统的非安全启动（Non-Secure Boot）模式，而&lt;code&gt;mx6ull_14x14_evk_plugin_defconfig&lt;/code&gt;支持安全启动（Secure Boot），允许 U-Boot 在 ROM 加载阶段执行自定义代码，通常用于 HAB 安全启动初始化（如 DRAM 训练）。通常使用前者即可。&lt;/p&gt;
&lt;h2&gt;添加变量至顶层Makefile&lt;/h2&gt;
&lt;p&gt;在uboot根目录下的顶层Makefile中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ARCH=arm
CROSS_COMPILE=arm-linux-gnueabihf-
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;编写make用Shell，编译&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;touch mx6ull_14x14_evk.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#!/bin/bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- mx6ull_14x14_evk_emmc_defconfig
make V=1 ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;chmod 777 mx6ull_14x14_evk.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;./mx6ull_14x14_evk.sh&lt;/code&gt;，编译&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里用的uboot是NXP最新的分支&lt;code&gt;v2022.04&lt;/code&gt;，这个uboot版本比较新，如果用老版本的交叉编译器会报错，因为它不支持比较新的语法。这里使用的uboot、kernel和编译器的版本都为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;uboot：&lt;code&gt;v2022.04&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;kernel：&lt;code&gt;if 6.6y&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;arm-linux-gnueabihf-gcc: &lt;code&gt;7.5.0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;编译完成后，&lt;code&gt;imxdownload&lt;/code&gt;烧录至SD卡，设置开发板从SD卡启动。&lt;/p&gt;
&lt;p&gt;此时Uboot启动之后，由于LCD未做配置，LCD上是不会显示NXP的Logo的。下一步是修改LCD驱动。&lt;/p&gt;
&lt;h2&gt;修改LCD驱动&lt;/h2&gt;
&lt;p&gt;IO部分不用修改，官方和开发板一致，仅需修改参数。较老版本的LCD参数在&lt;code&gt;board/freescale/mx6ull_aki/mx6ull_14x14_evk_emmc.c&lt;/code&gt;的&lt;code&gt;display_info_t&lt;/code&gt;结构体里。但是新版本uboot全部把诸如LCD之类的外设配置参数全部移到了设备树中。并且，imx6ull是由imx6ul这块板子拓展而来的，二者的设备树也呈非常明显的层次关系，即先读imx6ul的设备树，再读imx6ull的，所以很多外设节点都在&lt;code&gt;/arch/arm/dts/imx6ul-14x14-evk.dtsi&lt;/code&gt;这个设备树文件中定义。&lt;/p&gt;
&lt;p&gt;打开该设备树文件，找到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;lcdif {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_lcdif_dat
		     &amp;amp;pinctrl_lcdif_ctrl&amp;gt;;

	display = &amp;lt;&amp;amp;display0&amp;gt;;
	status = &quot;okay&quot;;

	display0: display@0 {
		bits-per-pixel = &amp;lt;24&amp;gt;;
		bus-width = &amp;lt;24&amp;gt;;

		display-timings {
			native-mode = &amp;lt;&amp;amp;timing0&amp;gt;;
			timing0: timing0 {
			clock-frequency = &amp;lt;51200000&amp;gt;;	//像素时钟频率，Hz
			hactive = &amp;lt;1024&amp;gt;;		//水平可见像素数（水平分辨率）
			vactive = &amp;lt;600&amp;gt;;		//垂直可见像素数（垂直分辨率）
			hfront-porch = &amp;lt;160&amp;gt;;	//水平前肩，HFP
			hback-porch = &amp;lt;140&amp;gt;;	//水平后肩，HBP
			hsync-len = &amp;lt;20&amp;gt;;		//水平同步脉冲，HSPW
			vback-porch = &amp;lt;20&amp;gt;;		//水平后肩，VBP
			vfront-porch = &amp;lt;12&amp;gt;;	//水平前肩，VFP
			vsync-len = &amp;lt;3&amp;gt;;		//垂直同步脉冲，VSPW

			hsync-active = &amp;lt;0&amp;gt;;		//水平同步信号极性
			vsync-active = &amp;lt;0&amp;gt;;		//垂直同步信号极性
			de-active = &amp;lt;1&amp;gt;;		//数据使能，高电平有效
			pixelclk-active = &amp;lt;0&amp;gt;;	//下降沿采样
			};
		};
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按注释修改即可。新版设备树似乎没有定义LCD名字的地方，感觉不是那么在意设备名，毕竟都用设备树了。&lt;/p&gt;
&lt;h2&gt;联网&lt;/h2&gt;
&lt;h3&gt;主机部分&lt;/h3&gt;
&lt;p&gt;物理部分：路由器WAN连接校园网，LAN1接电脑，LAN2接开发板ENET2（网卡1）。&lt;/p&gt;
&lt;p&gt;VMware需开启桥接模式：
&lt;img src=&quot;/images/linux/uboot_5.webp&quot; alt=&quot;桥接模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;虚拟网络编辑器中一般会自动设置桥接网卡，不用手动设置。桥接模式下虚拟机和物理主机一样存在于局域网中，可以和主机相通，和互联网相通，和局域网中其它主机相通。&lt;/p&gt;
&lt;p&gt;配置成桥接模式后虚拟机的IP、网关、DNS、netmask等全部会自动配置。在虚拟机中&lt;code&gt;ifconfig&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ens33: flags=4163&amp;lt;UP,BROADCAST,RUNNING,MULTICAST&amp;gt;  mtu 1500
        inet 192.168.1.105  netmask 255.255.255.0  broadcast 192.168.1.255
        ether 00:0c:29:0d:4a:09  txqueuelen 1000  (以太网)
        RX packets 4642  bytes 4241810 (4.2 MB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 2980  bytes 400799 (400.7 KB)
        TX errors 0  dropped 5 overruns 0  carrier 0  collisions 0

lo: flags=73&amp;lt;UP,LOOPBACK,RUNNING&amp;gt;  mtu 65536
        inet 127.0.0.1  netmask 255.0.0.0
        inet6 ::1  prefixlen 128  scopeid 0x10&amp;lt;host&amp;gt;
        loop  txqueuelen 1000  (本地环回)
        RX packets 1298  bytes 110055 (110.0 KB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 1298  bytes 110055 (110.0 KB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时主机的IPv4地址为192.168.1.100，可以看到桥接模式已经生效，主机和虚拟机处于同一个网段下。&lt;/p&gt;
&lt;h3&gt;板级部分&lt;/h3&gt;
&lt;p&gt;官方的NXP IMX6ULL EVK开发板使用的不是LAN8720A这个IC，因此需要修改一系列设置。好在官方的板子和正点原子阿尔法开发板的PHY物理地址是一致的，这点从设备树里可以看到：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;fec1 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet1&amp;gt;;
	phy-mode = &quot;rmii&quot;;
	phy-handle = &amp;lt;&amp;amp;ethphy0&amp;gt;;
  phy-reset-gpios = &amp;lt;&amp;amp;gpio5 7 GPIO_ACTIVE_LOW&amp;gt;;
  phy-reset-duration = &amp;lt;100&amp;gt;;
  phy-reset-post-delay = &amp;lt;100&amp;gt;;
	status = &quot;disable&quot;;
};

&amp;amp;fec2 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet2&amp;gt;;
	phy-mode = &quot;rmii&quot;;
	phy-handle = &amp;lt;&amp;amp;ethphy1&amp;gt;;
  phy-reset-gpios = &amp;lt;&amp;amp;gpio5 8 GPIO_ACTIVE_LOW&amp;gt;;
  phy-reset-duration = &amp;lt;100&amp;gt;;
  phy-reset-post-delay = &amp;lt;100&amp;gt;;
	status = &quot;okay&quot;;

	mdio {
		#address-cells = &amp;lt;1&amp;gt;;
		#size-cells = &amp;lt;0&amp;gt;;

		ethphy0: ethernet-phy@2 {
			reg = &amp;lt;2&amp;gt;;	//PHY0物理地址为2
			micrel,led-mode = &amp;lt;1&amp;gt;;
			clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_ENET_REF&amp;gt;;
			clock-names = &quot;rmii-ref&quot;;
		};

		ethphy1: ethernet-phy@1 {
			reg = &amp;lt;1&amp;gt;;	//PHY1物理地址为1
			micrel,led-mode = &amp;lt;1&amp;gt;;
			clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_ENET2_REF&amp;gt;;
			clock-names = &quot;rmii-ref&quot;;
		};
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;初始化检查&lt;/h3&gt;
&lt;p&gt;检查&lt;code&gt;/common/board_r.c&lt;/code&gt;，其中从网络初始化入口&lt;code&gt;initr_net&lt;/code&gt;开始Uboot会进行网络设置，其中&lt;code&gt;initr_net&lt;/code&gt;会调用&lt;code&gt;eth_initialize()&lt;/code&gt;进行网络初始化，然后调用&lt;code&gt;reset_phy()&lt;/code&gt;对PHY进行复位，而&lt;code&gt;phy_init()&lt;/code&gt;中有以下条件宏语句：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int phy_init(void)
{
...
#ifdef CONFIG_PHY_MICREL_KSZ8XXX
    phy_micrel_ksz8xxx_init();
#endif 
#ifdef CONFIG_PHY_MICREL_KSZ90X1
    phy_micrel_ksz90x1_init();
#endif
...
#ifdef CONFIG_PHY_SMSC
    phy_smsc_init();
#endif
...
    genphy_init();
 
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然这是根据&lt;code&gt;defconfig&lt;/code&gt;里的键值来对应不同的PHY初始化函数。阿尔法开发板上的SR8201F是Realtek公司生产的，因此根据&lt;code&gt;phy_init&lt;/code&gt;，&lt;code&gt;defconfig&lt;/code&gt;里要设置&lt;code&gt;CONFIG_PHY_REALTEK=y&lt;/code&gt;才会触发&lt;code&gt;phy_realtek_init()&lt;/code&gt;这个函数。&lt;/p&gt;
&lt;p&gt;打开&lt;code&gt;/configs/imx6ull_14x14_evk_emmc_defconfig&lt;/code&gt;，删除或屏蔽：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_PHY_MICREL=y
CONFIG_PHY_MICREL_KSZ8XXX=y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_PHY_REALTEK=y
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（不过这里我一开始错误的改成了&lt;code&gt;CONFIG_PHY_SMSC=y&lt;/code&gt;，发现网络也能正常使用，不知道为什么）&lt;/p&gt;
&lt;h3&gt;配置fec复位管脚&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;fecmxc_probe&lt;/code&gt;函数中还调用了&lt;code&gt;fec_gpio_reset()&lt;/code&gt;来复位fec网卡：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if CONFIG_IS_ENABLED(DM_GPIO)
	fec_gpio_reset(priv);
#endif
	/* Reset chip. */
	writel(readl(&amp;amp;priv-&amp;gt;eth-&amp;gt;ecntrl) | FEC_ECNTRL_RESET,
	       &amp;amp;priv-&amp;gt;eth-&amp;gt;ecntrl);
	start = get_timer(0);
	while (readl(&amp;amp;priv-&amp;gt;eth-&amp;gt;ecntrl) &amp;amp; FEC_ECNTRL_RESET) {
		if (get_timer(start) &amp;gt; (CONFIG_SYS_HZ * 5)) {
			printf(&quot;FEC MXC: Timeout resetting chip\n&quot;);
			goto err_timeout;
		}
		udelay(10);
	}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而这里设备树会使用&lt;code&gt;fecmxc_of_to_plat&lt;/code&gt;这个OF函数来读取节点信息到驱动，该函数中有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#if CONFIG_IS_ENABLED(DM_GPIO)
	ret = gpio_request_by_name(dev, &quot;phy-reset-gpios&quot;, 0,
				   &amp;amp;priv-&amp;gt;phy_reset_gpio, GPIOD_IS_OUT);
	if (ret &amp;lt; 0)
		return 0; /* property is optional, don&apos;t return error! */

	priv-&amp;gt;reset_delay = dev_read_u32_default(dev, &quot;phy-reset-duration&quot;, 1);
	if (priv-&amp;gt;reset_delay &amp;gt; 1000) {
		printf(&quot;FEC MXC: phy reset duration should be &amp;lt;= 1000ms\n&quot;);
		/* property value wrong, use default value */
		priv-&amp;gt;reset_delay = 1;
	}

	priv-&amp;gt;reset_post_delay = dev_read_u32_default(dev,
						      &quot;phy-reset-post-delay&quot;,
						      0);
	if (priv-&amp;gt;reset_post_delay &amp;gt; 1000) {
		printf(&quot;FEC MXC: phy reset post delay should be &amp;lt;= 1000ms\n&quot;);
		/* property value wrong, use default value */
		priv-&amp;gt;reset_post_delay = 0;
	}
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到这里依赖节点的三个用于描述复位的值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;phy-reset-gpios&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;phy-reset-duration&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;phy-reset-post-delay&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;回到设备树，将这三个属性加进节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;amp;fec1 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet1&amp;gt;;
	phy-mode = &quot;rmii&quot;;
	phy-handle = &amp;lt;&amp;amp;ethphy0&amp;gt;;
    phy-reset-gpios = &amp;lt;&amp;amp;gpio5 7 GPIO_ACTIVE_LOW&amp;gt;;
    phy-reset-duration = &amp;lt;100&amp;gt;;
    phy-reset-post-delay = &amp;lt;100&amp;gt;;
	status = &quot;disable&quot;;
};

&amp;amp;fec2 {
	pinctrl-names = &quot;default&quot;;
	pinctrl-0 = &amp;lt;&amp;amp;pinctrl_enet2&amp;gt;;
	phy-mode = &quot;rmii&quot;;
	phy-handle = &amp;lt;&amp;amp;ethphy1&amp;gt;;
    phy-reset-gpios = &amp;lt;&amp;amp;gpio5 8 GPIO_ACTIVE_LOW&amp;gt;;
    phy-reset-duration = &amp;lt;100&amp;gt;;
    phy-reset-post-delay = &amp;lt;100&amp;gt;;
	status = &quot;okay&quot;;

	mdio {
		#address-cells = &amp;lt;1&amp;gt;;
		#size-cells = &amp;lt;0&amp;gt;;

		ethphy0: ethernet-phy@2 {
			reg = &amp;lt;2&amp;gt;;
			micrel,led-mode = &amp;lt;1&amp;gt;;
			clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_ENET_REF&amp;gt;;
			clock-names = &quot;rmii-ref&quot;;
		};

		ethphy1: ethernet-phy@1 {
			reg = &amp;lt;1&amp;gt;;
			micrel,led-mode = &amp;lt;1&amp;gt;;
			clocks = &amp;lt;&amp;amp;clks IMX6UL_CLK_ENET2_REF&amp;gt;;
			clock-names = &quot;rmii-ref&quot;;
		};
	};
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;GPIO5_IO07&lt;/code&gt;和&lt;code&gt;GPIO5_IO08&lt;/code&gt;会跟SPI4的两个IO冲突，这里直接把spi4 disable掉即可。通常uboot会自动加载fec2，为了方便起见，我把fec1也disable了。&lt;/p&gt;
&lt;p&gt;最后，还需要在&lt;code&gt;/drviers/net/phy&lt;/code&gt;中，找到&lt;code&gt;genphy_config_aneg&lt;/code&gt;函数，这个函数负责PHY自协商的完整配置，在初始化PHY时会调用（通过操作&lt;code&gt;BMCR_RESET&lt;/code&gt;位）。在函数中加入：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int genphy_config_aneg(struct phy_device *phydev)
{
	int result;
  
  /* Soft reset */
  phy_reset(phydev);
  mdelay(150);	//IC要求复位后延时150ms

  ......
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;uboot设置&lt;/h3&gt;
&lt;p&gt;在uboot内设置开发板网络配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;setenv ipaddr 192.168.1.55
setenv ethaddr b8:ae:1d:01:00:00
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0
setenv serverip 192.168.1.105   # Ubuntu IP
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;saveenv&lt;/code&gt;，保存变量。&lt;/p&gt;
&lt;p&gt;uboot支持自动配置物理地址，只需要在defconfig中加入：&lt;code&gt;CONFIG_NET_RANDOM_ETHADDR=y&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;如果对静态IP没有要求，或者想要简单一点，可以直接在Uboot内使用&lt;code&gt;DHCP&lt;/code&gt;命令。DHCP会自动申请可用IP，自动配置网关、DNS、子网掩码等。&lt;/p&gt;
&lt;p&gt;此时在Uboot内ping 虚拟机地址&lt;code&gt;192.168.1.105&lt;/code&gt;，应当是能Ping通的。为防止IP被复用，关闭虚拟机，再Ping一次虚拟机，如果无法Ping通，说明配置正确：
&lt;img src=&quot;/images/linux/uboot_6.webp&quot; alt=&quot;Ping测试&quot; /&gt;&lt;/p&gt;
&lt;p&gt;反过来，从虚拟机Ping开发板，肯定是Ping不通的（Ping通了说明有问题，这个IP被其他的什么东西复用了），因为此时Linux内核还未启动，网口不会被使能。&lt;/p&gt;
&lt;p&gt;此时，三台机器的IP地址分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主机：192.168.1.100&lt;/li&gt;
&lt;li&gt;虚拟机：192.168.1.105&lt;/li&gt;
&lt;li&gt;开发板：192.168.1.101(DHCP获取)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;uboot启动流程&lt;/h2&gt;
&lt;h3&gt;reset函数&lt;/h3&gt;
&lt;p&gt;uboot编译后会在根目录下生成链接脚本&lt;code&gt;u-boot.lds&lt;/code&gt;。其内容的前几行里有：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;OUTPUT_FORMAT(&quot;elf32-littlearm&quot;, &quot;elf32-littlearm&quot;, &quot;elf32-littlearm&quot;)
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
    ........
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然第3行的&lt;code&gt;ENTRY(_start)&lt;/code&gt;中的&lt;code&gt;_start&lt;/code&gt;是代码入口点，在&lt;code&gt;arch/arm/lib/vectors.S&lt;/code&gt;中定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 *  vectors - Generic ARM exception table code
 *
 *  Copyright (c) 1998	Dan Malek &amp;lt;dmalek@jlc.net&amp;gt;
 *  Copyright (c) 1999	Magnus Damm &amp;lt;kieraypc01.p.y.kie.era.ericsson.se&amp;gt;
 *  Copyright (c) 2000	Wolfgang Denk &amp;lt;wd@denx.de&amp;gt;
 *  Copyright (c) 2001	Alex Züpke &amp;lt;azu@sysgo.de&amp;gt;
 *  Copyright (c) 2001	Marius Gröger &amp;lt;mag@sysgo.de&amp;gt;
 *  Copyright (c) 2002	Alex Züpke &amp;lt;azu@sysgo.de&amp;gt;
 *  Copyright (c) 2002	Gary Jennejohn &amp;lt;garyj@denx.de&amp;gt;
 *  Copyright (c) 2002	Kyle Harris &amp;lt;kharris@nexus-tech.net&amp;gt;
 *
 * SPDX-License-Identifier:	GPL-2.0+
 */

#include &amp;lt;config.h&amp;gt;

/*
 *************************************************************************
 *
 * Symbol _start is referenced elsewhere, so make it global
 *
 *************************************************************************
 */

.globl _start

/*
 *************************************************************************
 *
 * Vectors have their own section so linker script can map them easily
 *
 *************************************************************************
 */

	.section &quot;.vectors&quot;, &quot;ax&quot;

/*
 *************************************************************************
 *
 * Exception vectors as described in ARM reference manuals
 *
 * Uses indirect branch to allow reaching handlers anywhere in memory.
 *
 *************************************************************************
 */

_start:

#ifdef CONFIG_SYS_DV_NOR_BOOT_CFG
	.word	CONFIG_SYS_DV_NOR_BOOT_CFG
#endif

	b	reset
	ldr	pc, _undefined_instruction
	ldr	pc, _software_interrupt
	ldr	pc, _prefetch_abort
	ldr	pc, _data_abort
	ldr	pc, _not_used
	ldr	pc, _irq
	ldr	pc, _fiq

.................

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然&lt;code&gt;_start&lt;/code&gt;后面就是中断向量表，&lt;code&gt;.section &quot;.vectors&quot;, &quot;ax&quot;&lt;/code&gt;表明此代码存放在&lt;code&gt;vectors&lt;/code&gt;里面，具体的地址可以在&lt;code&gt;u-boot.map&lt;/code&gt;中查询。&lt;code&gt;b reset&lt;/code&gt;跳转至reset函数内，该函数在&lt;code&gt;arch/arm/cpu/armv7/start.S&lt;/code&gt;里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;reset:
 /* Allow the board to save important registers */
 b save_boot_params
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reset&lt;/code&gt;函数紧接着又跳转到了&lt;code&gt;save_boot_params&lt;/code&gt;函数，该函数同样定义在&lt;code&gt;start.S&lt;/code&gt;里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENTRY(save_boot_params)
 b save_boot_params_ret @ back to my caller
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数又直接跳转到了&lt;code&gt;save_boot_params_ret&lt;/code&gt;函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;save_boot_params_ret:
 /*
 * disable interrupts (FIQ and IRQ), also set the cpu to SVC32 
 * mode, except if in HYP mode already
 */
 mrs r0, cpsr       @ 读取CPSR
 and r1, r0, #0x1f  @ 提取bit0 - bit4，用于设置Cortex-A7工作模式
 teq r1, #0x1a      @ 判断处理器是否处于HYP模式
 bicne r0, r0, #0x1f @ 若不处于HYP模式，清除模式位
 orrne r0, r0, #0x13 @ 进入SVC模式（特权）
 orr r0, r0, #0xc0   @ 关闭FIQ和IRQ
 msr cpsr,r0         @ 恢复CPSR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，继续执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /*
 * Setup vector:
 * (OMAP4 spl TEXT_BASE is not 32 byte aligned.
 * Continue to use ROM code vector only in OMAP4 spl)
 */
 #if !(defined(CONFIG_OMAP44XX) &amp;amp;&amp;amp; defined(CONFIG_SPL_BUILD))       
 /* Set V=0 in CP15 SCTLR register - for VBAR to point to vector */
 mrc p15, 0, r0, c1, c0, 0 @ 读取 CP15 SCTLR 寄存器
 bic r0, #CR_V @ V = 0
 mcr p15, 0, r0, c1, c0, 0 @ 写 CP15 SCTLR 寄存器，V清零，准备重定位向量表

 /* CP15 VBAR 寄存器中设置向量表地址 */
 ldr r0, =_start
 mcr p15, 0, r0, c12, c0, 0 @Set VBAR
 #endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续执行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* the mask ROM code should have PLL and others stable */
 #ifndef CONFIG_SKIP_LOWLEVEL_INIT
 bl cpu_init_cp15       @ 设置CP15相关内容
 bl cpu_init_crit       
 #endif

 bl _main
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这已经是跳转至main的最后一步。&lt;code&gt;cpu_init_crit&lt;/code&gt;定义如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ENTRY(cpu_init_crit)
 /*
 * Jump to board specific initialization...
 * The Mask ROM will have already initialized
 * basic memory. Go here to bump up clock rate and handle
 * wake up conditions.
 */
 b lowlevel_init @ go setup pll,mux,memory
 ENDPROC(cpu_init_crit)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注释已经写了，该函数即将要跳转到的&lt;code&gt;lowlevel_init&lt;/code&gt;会设置PLL、MUX和内存。&lt;/p&gt;
&lt;h3&gt;lowlevel_init&lt;/h3&gt;
&lt;p&gt;函数在&lt;code&gt;arch/arm/cpu/armv7/lowlevel_init.S&lt;/code&gt;中定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * A lowlevel_init function that sets up the stack to call a C function to
 * perform further init.
 *
 * (C) Copyright 2010
 * Texas Instruments, &amp;lt;www.ti.com&amp;gt;
 *
 * Author :
 *	Aneesh V	&amp;lt;aneesh@ti.com&amp;gt;
 *
 * SPDX-License-Identifier:	GPL-2.0+
 */

#include &amp;lt;asm-offsets.h&amp;gt;
#include &amp;lt;config.h&amp;gt;
#include &amp;lt;linux/linkage.h&amp;gt;

ENTRY(lowlevel_init)
	/*
	 * Setup a temporary stack. Global data is not available yet.
	 */
	ldr	sp, =CONFIG_SYS_INIT_SP_ADDR
	bic	sp, sp, #7 /* 8-byte alignment for ABI compliance */
#ifdef CONFIG_SPL_DM
	mov	r9, #0
#else
	/*
	 * Set up global data for boards that still need it. This will be
	 * removed soon.
	 */
#ifdef CONFIG_SPL_BUILD
	ldr	r9, =gdata
#else
	sub	sp, sp, #GD_SIZE
	bic	sp, sp, #7
	mov	r9, sp
#endif
#endif
	/*
	 * Save the old lr(passed in ip) and the current lr to stack
	 */
	push	{ip, lr}

	/*
	 * Call the very early init function. This should do only the
	 * absolute bare minimum to get started. It should not:
	 *
	 * - set up DRAM
	 * - use global_data
	 * - clear BSS
	 * - try to start a console
	 *
	 * For boards with SPL this should be empty since SPL can do all of
	 * this init in the SPL board_init_f() function which is called
	 * immediately after this.
	 */
	bl	s_init
	pop	{ip, pc}
ENDPROC(lowlevel_init)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第22行&lt;code&gt;ldr	sp, =CONFIG_SYS_INIT_SP_ADDR&lt;/code&gt;，将栈指针指向&lt;code&gt;CONFIG_SYS_INIT_SP_ADDR&lt;/code&gt;，这个宏在&lt;code&gt;include/configs/mx6ullevk.h&lt;/code&gt;里定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define CONFIG_SYS_INIT_RAM_ADDR IRAM_BASE_ADDR
#define CONFIG_SYS_INIT_RAM_SIZE IRAM_SIZE

#define CONFIG_SYS_INIT_SP_OFFSET \
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
#define CONFIG_SYS_INIT_SP_ADDR \
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;IRAM_BASE_ADDR&lt;/code&gt;和&lt;code&gt;IRAM_SIZE&lt;/code&gt;在&lt;code&gt;arch/arm/include/asm/arch-mx6/imx-regs.h&lt;/code&gt;中定义，实际上就是MX6ULL内部OCRAM的首地址和大小。这里&lt;code&gt;CONFIG_SYS_INIT_RAM_ADDR = IRAM_BASE_ADDR = 0x00900000&lt;/code&gt;，&lt;code&gt;CONFIG_SYS_INIT_RAM_SIZE = 0x00020000 =128KB&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;根据这两行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#define CONFIG_SYS_INIT_SP_OFFSET \
(CONFIG_SYS_INIT_RAM_SIZE - GENERATED_GBL_DATA_SIZE)
#define CONFIG_SYS_INIT_SP_ADDR \
(CONFIG_SYS_INIT_RAM_ADDR + CONFIG_SYS_INIT_SP_OFFSET)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;显然还需要知道&lt;code&gt;GENERATED_GBL_DATA_SIZE&lt;/code&gt;的值，在&lt;code&gt;include/generated/generic-asm-offsets.h&lt;/code&gt;中定义，大小为256.那么综上，&lt;code&gt;CONFIG_SYS_INIT_SP_ADDR&lt;/code&gt;的值如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;CONFIG_SYS_INIT_SP_OFFSET = 0x00020000 - 256 = 0x1FF00
CONFIG_SYS_INIT_SP_ADDR = 0x00900000 + 0x1FF00 = 0x0091FF00
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时栈指针SP指向0x0091FF00，这属于IMX6ULL的内部RAM。&lt;/p&gt;
&lt;p&gt;回到lowlevel_init.S，&lt;code&gt;bic	sp, sp, #7&lt;/code&gt;对栈指针作8字节对齐处理。第34行&lt;code&gt;sub	sp, sp, #GD_SIZE&lt;/code&gt;用栈指针减去&lt;code&gt;GD_SIZE&lt;/code&gt;，这个变量在&lt;code&gt;generic-asm-offsets.h&lt;/code&gt;中定义，大小为248；第35行&lt;code&gt;bic	sp, sp, #7&lt;/code&gt;，8字节对齐SP，此时&lt;code&gt;SP=0x0091FF00 - 248 = 0x0091FE08&lt;/code&gt;。第36行将SP保存至R9，第42行将ip和lr入栈，57行调用s_init函数，然后58行出栈ip和lr，并将lr赋给pc。&lt;/p&gt;
&lt;h3&gt;s_init&lt;/h3&gt;
&lt;p&gt;lowlevel_init在最后回调用s_init，定义在&lt;code&gt;arch/arm/cpu/armv7/mx6/soc.c&lt;/code&gt;中。对于MX6ULL而言，s_init()是个空函数（不满足触发要求）。至此lowlevel_init结束，返回cpu_init_crit，该函数也结束，最后返回save_boot_params_ret，紧接着就会执行_main函数。&lt;/p&gt;
&lt;h3&gt;_main&lt;/h3&gt;
&lt;p&gt;定义在&lt;code&gt;arch/arm/lib/crt0.S&lt;/code&gt;。总体而言非常长，总共调用了4个函数：&lt;code&gt;board_init_f&lt;/code&gt;、&lt;code&gt;relocate_code&lt;/code&gt;、&lt;code&gt;relocate_vectors&lt;/code&gt;、&lt;code&gt;board_init_r&lt;/code&gt;。&lt;/p&gt;
&lt;h4&gt;board_init_f&lt;/h4&gt;
&lt;p&gt;在&lt;code&gt;common/board_f.c&lt;/code&gt;中定义，该函数的工作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化外设，包括串口、定时器等，并打印一些消息&lt;/li&gt;
&lt;li&gt;初始化 gd 的各个成员变量，uboot 会将自己重定位到 DRAM 最后面的地址区域，也就是将自己拷贝到 DRAM 最后面的内存区域中。这么做的目的是给 Linux 腾出空间，防止 Linux kernel 覆盖掉 uboot，将 DRAM 前面的区域完整的空出来。在拷贝之前肯定要给 uboot 各部分分配好内存位置和大小，比如 gd 应该存放到哪个位置，malloc 内存池应该存放到哪个位置等等。这些信息都保存在 gd 的成员变量中，因此要对 gd 的这些成员变量做初始化。最终形成一个完整的内存map，在后面重定位 uboot 的时候就会用到这个内存map。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;该函数中包括非常关键的一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if (initcall_run_list(init_sequence_f))
hang();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;initcall_run_list&lt;/code&gt;会初始化一系列外设：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static init_fnc_t init_sequence_f[] = {
setup_mon_len,       //设置代码长度
initf_malloc,        //初始化malloc相关的成员变量
initf_console_record,   //无效，返回0
arch_cpu_init,          //基本的arch cpu依赖项初始化
initf_dm, 
arch_cpu_init_dm, 
mark_bootstage, 
board_early_init_f,     //初始化串口IO配置
timer_init,         //初始化Cortex-A7内核定时器，为Uboot提供时间(类似systick)
board_postclk_init,     //设置VDDSOC电压
get_clocks,             //获取SD卡外设时钟
env_init,               //初始化环境变量
init_baud_rate,         //初始化波特率
serial_init,            //初始化串口
console_init_f, 
display_options,        //串口打印信息
display_text_info,      //打印调试信息
print_cpuinfo,          //打印CPU信息
show_board_info,        //打印板子信息，调用checkboard()
INIT_FUNC_WATCHDOG_INIT     //无效
INIT_FUNC_WATCHDOG_RESET    //无效
init_func_i2c,          //初始化I2C
announce_dram_init,     
dram_init,              //设置gd-&amp;gt;ram_size
post_init_f,            //测试函数
INIT_FUNC_WATCHDOG_RESET
testdram,               //无效
INIT_FUNC_WATCHDOG_RESET
INIT_FUNC_WATCHDOG_RESET
/*
* Now that we have DRAM mapped and working, we can
* relocate the code and continue running from DRAM.
*
* Reserve memory at end of RAM for (top down in that order):
* - area that won&apos;t get touched by U-Boot and Linux (optional)
* - kernel log buffer
* - protected RAM
* - LCD framebuffer
* - monitor code
* - board info struct
*/
setup_dest_addr,        //设置gd-&amp;gt;ram_size(ram大小)，gd-&amp;gt;ram_top(ram最高地址)和gd-&amp;gt;relocaddr(重定位后最高地址)
reserve_round_4k,       //gd-&amp;gt;relocaddr 4字节对齐
reserve_mmu,            //留出MMU的TLB表位置
reserve_trace, 
reserve_uboot,          //留出重定位后uboot所占内存区域，大小由gd-&amp;gt;monlen指定，留出的空间需4KB对齐并设置gd-&amp;gt;start_addr_sp
reserve_malloc,         //留出malloc区域，由宏TOTAL_MALLOC_LEN定义(默认16MB)
reserve_board,          //留出bd_t内存区
setup_machine,          //无效
reserve_global_data,    //留出gd_t内存区域
reserve_fdt,            //无效
reserve_arch,           //空
reserve_stacks,         //留出栈空间，如果使能IRQ还需要留出IRQ相应的内存
setup_dram_config,      //设置DRAM，告知linux DRAM起始地址和大小
show_dram_config, 
display_new_sp,         //显示新的sp位置
INIT_FUNC_WATCHDOG_RESET
reloc_fdt,              //无效
setup_reloc,            //设置gd的其他成员变量
NULL,
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;至此，&lt;code&gt;board_init_f&lt;/code&gt;执行完成，最终的内存分配：
&lt;img src=&quot;/images/linux/uboot_9.webp&quot; alt=&quot;内存分配&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;relocate_code / relocate_vectors&lt;/h4&gt;
&lt;p&gt;用于拷贝代码和重定位向量表。&lt;/p&gt;
&lt;h4&gt;board_init_r&lt;/h4&gt;
&lt;p&gt;用于初始化高级外设，包括EMMC、LCD、BSP芯片、网络等。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;总的来说，uboot的汇编启动阶段包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;入口点：&lt;code&gt;_start&lt;/code&gt;(&lt;code&gt;arch/arm/lib/vectors.S&lt;/code&gt;)设置异常向量表&lt;/li&gt;
&lt;li&gt;CPU模式切换：进入SVC模式，禁用IRQ/FIQ&lt;/li&gt;
&lt;li&gt;关闭MMU/Cache，确保直接访问物理地址&lt;/li&gt;
&lt;li&gt;初始化栈指针&lt;/li&gt;
&lt;li&gt;lowlevel_init（板级特定）：
&lt;ul&gt;
&lt;li&gt;配置系统时钟&lt;/li&gt;
&lt;li&gt;DDR控制器初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;代码重定位：将U-Boot从加载地址（如SRAM）拷贝到DDR目标地址&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;C语言初始化阶段(&lt;code&gt;board_init_f&lt;/code&gt;)包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;全局数据结构初始化：&lt;code&gt;gd_t&lt;/code&gt;和&lt;code&gt;bd_t&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;低级驱动初始化：
&lt;ul&gt;
&lt;li&gt;串口、GPIO、MMU、内核定时器&lt;/li&gt;
&lt;li&gt;DDR&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;重定位准备
&lt;ul&gt;
&lt;li&gt;计算uboot在DDR中的最终位置，预留内核、DTB、initrd、代码空间&lt;/li&gt;
&lt;li&gt;执行重定位，更新代码地址相关引用(如全局变量指针)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;设备树预解析：若使用DTS，加载并校验DTB&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;重定位后主流程(&lt;code&gt;board_init_r&lt;/code&gt;)包括：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;高级外设初始化：
&lt;ul&gt;
&lt;li&gt;以太网、SD、USB、EEPROM等&lt;/li&gt;
&lt;li&gt;展开DTB，匹配驱动，设置硬件参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;环境变量加载：从存储介质读取&lt;code&gt;uEnv.txt&lt;/code&gt;或环境分区&lt;/li&gt;
&lt;li&gt;用户交互
&lt;ul&gt;
&lt;li&gt;检测输入&lt;/li&gt;
&lt;li&gt;加载zImage、设备树、initrd到指定内存地址&lt;/li&gt;
&lt;li&gt;准备加载内核&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>IMX6UL DDR3配置</title><link>https://arkmostima.pages.dev/posts/ddr3%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/ddr3%E9%85%8D%E7%BD%AE/</guid><description>RAM：Random-access memory，随机存储器，随时读写，速度快，掉电后数据丢失。内存条即为常见的RAM。ARM中栈、堆(malloc申请的内存)、全局区（.bss段、.data段）都存放在RAM中。 ROM：Read-o…</description><pubDate>Wed, 05 Mar 2025 13:28:29 GMT</pubDate><content:encoded>&lt;h2&gt;RAM和ROM&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;RAM：Random-access memory，随机存储器，随时读写，速度快，掉电后数据丢失。内存条即为常见的RAM。ARM中栈、堆(malloc申请的内存)、全局区（.bss段、.data段）都存放在RAM中。&lt;/li&gt;
&lt;li&gt;ROM：Read-only memory，只读存储器。容量大，速度慢，掉电后数据留存。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于IMX6U，256/512MB的DDR3为RAM，512MB的NANF Flash或8GB EMMC为ROM。&lt;/p&gt;
&lt;h2&gt;SRAM&lt;/h2&gt;
&lt;p&gt;Static Random-Access Memory，静态随机存储器。“静态”的意思是只要SRAM上电，SRAM里面的数据就会一直保存，直到SRAM掉电。为了随机读取任何一个地址空间内的数据，SRAM的地址线和数据线分离。&lt;/p&gt;
&lt;p&gt;以F103/F407常用的IS62WV51216 SRAM而言，其包括几个部分：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/sram_1.webp&quot; alt=&quot;IS62WV51216框图&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;地址线，用于寻址。地址线的根数代表着能够访问的地址大小，19根线就能访问2^19=524288=512KB的内存。该SRAM是16位宽的，一次访问两个字节，实际的可访问大小为512KB*2=1MB。&lt;/li&gt;
&lt;li&gt;数据线，用于传输数据。16位宽即有16根数据线，高低字节分开传输。&lt;/li&gt;
&lt;li&gt;控制线。包括：
&lt;ul&gt;
&lt;li&gt;CS1/CS2：片选信号&lt;/li&gt;
&lt;li&gt;OE：输出使能信号&lt;/li&gt;
&lt;li&gt;WE：写使能信号&lt;/li&gt;
&lt;li&gt;UB/LB：高字节/低字节控制信号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;SRAM最大的缺点就是成本高、价格高。&lt;/p&gt;
&lt;h2&gt;SDRAM&lt;/h2&gt;
&lt;p&gt;Synchronous Dynamic Random Access Memory，同步动态随机存储器。“同步”的意思是 SDRAM 工作需要时钟线，“动态”的意思是 SDRAM 中的数据需要不断的刷新来保证数据不会丢失，“随机”的意思就是可以读写任意地址的数据。相比SRAM，SDRAM读写速度较慢（要持续刷新），但是容量大，价格更低，功耗也更低，适合做大容量存储。&lt;/p&gt;
&lt;p&gt;SDRAM已经发展到了第四代：
1. SDRAM
2. DDR SDRAM
3. DDR2 SDRAM
4. DDR3 SDRAM
5. DDR4 SDRAM&lt;/p&gt;
&lt;p&gt;STM32F429/F767等都支持SDRAM。就以 STM32 开发板最常用的华邦 W9825G6KH 为例，W9825G6KH 是一款 16 位宽(数据位为 16 位)、32MB 的 SDRAM、速度一般为 133MHz、166MHz 或 200MHz：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/sram_2.webp&quot; alt=&quot;W9825G6KH框图&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;控制线
&lt;ul&gt;
&lt;li&gt;CLK：时钟线，SDRAM中“同步”二字表明其必定需要时钟线&lt;/li&gt;
&lt;li&gt;CKE：时钟使能信号线&lt;/li&gt;
&lt;li&gt;CS：片选信号&lt;/li&gt;
&lt;li&gt;RAS：行选通信号。与SRAM不同，SDRAM通过行、列来确定具体的存储区域，类似于“坐标”，因此有行地址和列地址之分&lt;/li&gt;
&lt;li&gt;CAS：列选通信号&lt;/li&gt;
&lt;li&gt;WE：写使能信号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A10地址线：除了地址线外，A10线还控制Auto-precharge（预充电）。SDRAM内部分为多个BANK，读写完成后如果要对同一个BANK中的另一行进行寻址，则必须将原来的有效行关闭，然后发送新的行/列地址。关闭现在正工作的行，打开新行的操作就叫做预充电。&lt;/li&gt;
&lt;li&gt;地址线：A0-A12，共13根，包含行地址和列地址。如W9825G6KH的A0-A8是列地址(9位)，A0-A12是行地址(13位)，可寻址范围为2^9*2^13=4194304B=4MB。 考虑到位宽为2，4MB * 2=8MB，共4个BANK，因此容量为32MB。&lt;/li&gt;
&lt;li&gt;BANK选择线：在一片 SDRAM 中因为技术、成本等原因，不可能做一个全容量的 BANK。而且，因为 SDRAM 的工作原理，单一的 BANK 会带来严重的寻址冲突，减低内存访问效率。为此，厂商在一片 SDRAM 中分割出多块 BANK，一般都是 2 的 n 次方，比如 2，4，8 等。如果有4个BANK，那么BANK选择则需要两根线，同理8个BANK就需要三根线。&lt;/li&gt;
&lt;li&gt;BANK区域&lt;/li&gt;
&lt;li&gt;数据线：16位宽，共16根数据线&lt;/li&gt;
&lt;li&gt;高低字节选择线&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;DDR&lt;/h2&gt;
&lt;p&gt;Double Data Rate SDRAM，双倍速率SDRAM。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DDR1:一个CLK周期内，在上升沿和下降沿各传输一次数据，该过程被称为prefetch（预取）&lt;/li&gt;
&lt;li&gt;DDR2：在DDR1的基础上将预取增加到了4bit&lt;/li&gt;
&lt;li&gt;DDR3：在DDR2的基础上将预取增加到了8bit，DDR3又细分为以下型号：
&lt;ul&gt;
&lt;li&gt;LPDDR3：低功耗，电压为1.2V&lt;/li&gt;
&lt;li&gt;DDR3：标压，电压为1.5V，台式机多使用该类&lt;/li&gt;
&lt;li&gt;DDR3L：低压，电压为1.35V，手机、嵌入式设备、笔记本等多使用该类&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;DDR3&lt;/h2&gt;
&lt;p&gt;以NT5CC256M16ER-EK为例：
&lt;img src=&quot;/images/linux/sram_3.webp&quot; alt=&quot;DDR3L框图&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;控制线
&lt;ul&gt;
&lt;li&gt;ODT：片上终端使能，使能和禁止片内终端电阻&lt;/li&gt;
&lt;li&gt;ZQ：输出驱动校准的外部参考引脚，应外接240欧电阻至VSSQ&lt;/li&gt;
&lt;li&gt;RESET：复位&lt;/li&gt;
&lt;li&gt;CKE：时钟使能&lt;/li&gt;
&lt;li&gt;A12：地址线，复用为BC引脚，在READ和WRITE命令期间会被采样以决定burst chop是否执行&lt;/li&gt;
&lt;li&gt;CK和CK#：时钟信号。DDR3使用差分时钟线，所有的控制和地址信号都会在CK的上升沿和CK#的下降沿交叉处被采集&lt;/li&gt;
&lt;li&gt;CS#：片选信号&lt;/li&gt;
&lt;li&gt;RAS#、CAS#、WE#：行选通，列选通和写使能信号&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;地址线：A0-A14，列地址为A0-A9共10根，行地址为A0-A14共15根，一个BANK大小即为2^10 * 2^15 * 2=64MB。IC共有8个BANK，加起来就是512MB。&lt;/li&gt;
&lt;li&gt;BANK选择线：8个BANK，需要3根选择线&lt;/li&gt;
&lt;li&gt;BANK区域&lt;/li&gt;
&lt;li&gt;数据线：16位宽，共需16根数据线&lt;/li&gt;
&lt;li&gt;数据选通引脚：DQS和DQS#，差分信号&lt;/li&gt;
&lt;li&gt;数据输入屏蔽引脚：DM&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;DDR3关键时间参数&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;传输速率：如1066MT/S、1600MT/S、1866MT/S 等&lt;/li&gt;
&lt;li&gt;tRCD参数：RAS-to-CAS Delay，即行寻址到列寻址之间的延迟。DDR的寻址流程是先指定 BANK 地址，然后再指定行地址，最后指定列地址确定最终要寻址的单元。BANK 地址和行地址是同时发出的，这个命令叫做“行激活”(Row Active)。行激活以后就发送列地址和具体的操作命令(读还是写)，这两个是同时发出的，因此一般也用“读/写命令”表示列寻址。在行有效(行激活)到读写命令发出的这段时间间隔叫做 tRCD。该参数在初始化DDR3时需要配置&lt;/li&gt;
&lt;li&gt;CL参数：CAS Latency，列地址选通潜伏期，列地址发出后，数据从存储单元到内存芯片IO接口上的时间。一般tRCD和CL大小一样&lt;/li&gt;
&lt;li&gt;AL参数：Additive Latency，前置CAS后读写操作并没有提前，依旧需要AL来保证足够的延迟（潜伏）期。AL和CL相加组成了RL（Read Latency）&lt;/li&gt;
&lt;li&gt;tRC参数：两个 ACTIVE 命令，或者 ACTIVE 命令到 REFRESH 命令之间的周期&lt;/li&gt;
&lt;li&gt;tRAS参数：ACTIVE 命令到 PRECHARGE 命令之间的最小时间，DDR3L 的数据手册同样也会给出此参数&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;I.MX6U MMDC控制器&lt;/h2&gt;
&lt;h3&gt;IO&lt;/h3&gt;
&lt;p&gt;MMDC 是 I.MX6U的内存控制器，是一个多模的 DDR 控制器，可以连接 16 位宽的 DDR3/DDR3L、16 位
宽的 LPDDR2，MMDC 是一个可配置、高性能的 DDR 控制器。MMDC 外设包含一个内核(MMDC_CORE)和 PHY(MMDC_PHY)，内核和 PHY 的功能如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MMDC内核：负责通过AXI接口与系统通信、DDR命令生成、DDR命令优化、读写数据路径&lt;/li&gt;
&lt;li&gt;MMDCPHY：负责时序调整和校准，保障数据在400MHz内被准确捕获&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为硬件要求严格，I.MX6U的DDR引脚没有复用：
&lt;img src=&quot;/images/linux/sram_4.webp&quot; alt=&quot;DDR信号IO&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;时钟源&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/sram_5.webp&quot; alt=&quot;MMDC时钟源&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;pre_periph2时钟选择器，由CBCMR的PRE_PERIPH2_CLK_SEL 位控制：
| PRE_PERIPH2_CLK_SEL(bit22:21) | 时钟源       |
| ----------------------------- | --------- |
| 00                             | PLL2      |
| 01                             | PLL2_PFD2 |
| 10                            | PLL2_PFD0 |
| 11                            | PLL4      |
先前的时钟配置已经将PLL2_PFD2设置为396MHz（约400MHz），因此DDR频率也为400MHz&lt;/li&gt;
&lt;li&gt;periph2_clk 时钟选择器，由 CBCDR 寄存器的 PERIPH2_CLK_SEL 位(bit26)控制，置0时选择pll2_main_clk作为periph2_clk的时钟源，因此&lt;code&gt;periph2_clk = PLL2_PFD0 = 396MHz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;分频器，由 CBCDR 寄存器的 FABRIC_MMDC_PODF 位(bit5:3)设置，0 - 7 对应1 - 8 分频，此处应设置1分频，即该位置0&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;DDR驱动&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;使用excel表生成inc文件&lt;/li&gt;
&lt;li&gt;加载inc至NXP DDR Test Tool，连接USB OTG线，设置MCU从USB启动，开始校准&lt;/li&gt;
&lt;li&gt;校准后的寄存器数据回填至inc文件，重新下载，进行压力测试&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>IMX6ULL外部中断配置</title><link>https://arkmostima.pages.dev/posts/imx6ull%E5%A4%96%E9%83%A8%E4%B8%AD%E6%96%AD%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/imx6ull%E5%A4%96%E9%83%A8%E4%B8%AD%E6%96%AD%E9%85%8D%E7%BD%AE/</guid><description>Cortex-A7的中断向量表共有8个异常中断，其中一个未使用，有效中断为7个：</description><pubDate>Tue, 04 Mar 2025 04:51:55 GMT</pubDate><content:encoded>&lt;h2&gt;Cortex-A7中断系统&lt;/h2&gt;
&lt;h3&gt;中断向量表&lt;/h3&gt;
&lt;p&gt;Cortex-A7的中断向量表共有8个异常中断，其中一个未使用，有效中断为7个：&lt;/p&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;0X00&lt;/td&gt;
&lt;td&gt;复位中断(Rest)&lt;/td&gt;
&lt;td&gt;特权模式(SVC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X04&lt;/td&gt;
&lt;td&gt;未定义指令中断(Undefined Instruction)&lt;/td&gt;
&lt;td&gt;未定义指令中止模式(Undef)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X08&lt;/td&gt;
&lt;td&gt;软中断(Software Interrupt,SWI)&lt;/td&gt;
&lt;td&gt;特权模式(SVC)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X0C&lt;/td&gt;
&lt;td&gt;指令预取中止中断(Prefetch Abort)&lt;/td&gt;
&lt;td&gt;中止模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X10&lt;/td&gt;
&lt;td&gt;数据访问中止中断(Data Abort)&lt;/td&gt;
&lt;td&gt;中止模式&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X14&lt;/td&gt;
&lt;td&gt;未使用(Not Used)&lt;/td&gt;
&lt;td&gt;未使用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X18&lt;/td&gt;
&lt;td&gt;IRQ 中断(IRQ Interrupt)&lt;/td&gt;
&lt;td&gt;外部中断模式(IRQ)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0X1C&lt;/td&gt;
&lt;td&gt;FIQ 中断(FIQ Interrupt)&lt;/td&gt;
&lt;td&gt;快速中断模式(FIQ)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;不同于Cortex-M在中断向量表中列出了所有的中断向量，Cortex-A将所有属于内核CPU的外部中断放在0x18的IRQ中断中，任意外部中断都会触发IRQ中断，然后在IRQ中断中通过读取寄存器的方式来判断具体的中断类型，然后做出相应处理。&lt;/p&gt;
&lt;h3&gt;汇编代码(启动文件&lt;code&gt;start.S&lt;/code&gt;)&lt;/h3&gt;
&lt;h4&gt;中断向量表&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;.global _start  				/* 全局标号 */

_start:
	ldr pc, =Reset_Handler		/* 复位中断 					*/	
	ldr pc, =Undefined_Handler	/* 未定义中断 					*/
	ldr pc, =SVC_Handler		/* SVC(Supervisor)中断 		*/
	ldr pc, =PrefAbort_Handler	/* 预取终止中断 					*/
	ldr pc, =DataAbort_Handler	/* 数据终止中断 					*/
	ldr	pc, =NotUsed_Handler	/* 未使用中断					*/
	ldr pc, =IRQ_Handler		/* IRQ中断 					*/
	ldr pc, =FIQ_Handler		/* FIQ(快速中断)未定义中断 			*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;指定中断发生后调用对应的中断复位函数。&lt;code&gt;pc&lt;/code&gt;为程序计数器，用来指出下一条指令在主存储器中的地址。中断发生后，主程序的下一条指令地址会自动更新为中断服务函数的地址。&lt;/p&gt;
&lt;h3&gt;GIC控制器&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/GIC_1.webp&quot; alt=&quot;GIC v2框图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;类似于STM32的NVIC，Cortex-A的中断控制器名字叫GIC，GIC负责处理输入的所有中断，然后生成一个IRQ信号上报给ARM内核。GIC将众多的中断源分为三类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPI，共享中断，常见外部中断均属于SPI中断&lt;/li&gt;
&lt;li&gt;PPI，私有中断，指定核心处理&lt;/li&gt;
&lt;li&gt;SGI，软件中断，通过向寄存器&lt;code&gt;GICR_SGIR&lt;/code&gt;写入数据触发，常用于多核通信&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中断ID&lt;/h3&gt;
&lt;p&gt;I.MX6UL的1020个中断ID分配如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ID0 - ID15：分配至SGI&lt;/li&gt;
&lt;li&gt;ID16 - ID31：分配至PPI&lt;/li&gt;
&lt;li&gt;ID32 - ID1019：分配至SPI&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I.MX6U的中断源由官方SDK在&lt;code&gt;MCIMX6Y2.h&lt;/code&gt;中给出，共有160个，每个中断源有独属的中断ID。&lt;/p&gt;
&lt;h3&gt;GIC逻辑分块&lt;/h3&gt;
&lt;p&gt;GIC架构分为两个逻辑块：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Distributor：分发器端，负责处理中断事件分发问题（中断事件该发送到哪个CPU Interface上去）。分发器收集所有的中断源，可控制每个中断的优先级，具体功能有：
&lt;ol&gt;
&lt;li&gt;全局中断使能控制&lt;/li&gt;
&lt;li&gt;控制每个中断的使能或关闭&lt;/li&gt;
&lt;li&gt;设置中断优先级&lt;/li&gt;
&lt;li&gt;设置每个中断的目标处理器列表&lt;/li&gt;
&lt;li&gt;设置每个外部中断的触发模式：电平触发或边沿触发&lt;/li&gt;
&lt;li&gt;设置每个中断属于组 0 还是组 1&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;CPU Interface：CPU接口端，具体功能有：
&lt;ol&gt;
&lt;li&gt;使能或者关闭发送到 CPU Core 的中断请求信号&lt;/li&gt;
&lt;li&gt;应答中断&lt;/li&gt;
&lt;li&gt;通知中断处理完成&lt;/li&gt;
&lt;li&gt;设置优先级掩码，通过掩码来设置哪些中断不需要上报给 CPU Core&lt;/li&gt;
&lt;li&gt;定义抢占策略&lt;/li&gt;
&lt;li&gt;当多个中断到来的时候，选择优先级最高的中断通知给 CPU Core&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;GIC控制器的所有寄存器在&lt;code&gt;core_ca7.h&lt;/code&gt;中声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * GIC寄存器描述结构体，
 * GIC分为分发器端和CPU接口端
 */
typedef struct
{
        uint32_t RESERVED0[1024];
  __IOM uint32_t D_CTLR;                 /*!&amp;lt; Offset: 0x1000 (R/W) Distributor Control Register */
  __IM  uint32_t D_TYPER;                /*!&amp;lt; Offset: 0x1004 (R/ )  Interrupt Controller Type Register */
  __IM  uint32_t D_IIDR;                 /*!&amp;lt; Offset: 0x1008 (R/ )  Distributor Implementer Identification Register */
        uint32_t RESERVED1[29];
  __IOM uint32_t D_IGROUPR[16];          /*!&amp;lt; Offset: 0x1080 - 0x0BC (R/W) Interrupt Group Registers */
        uint32_t RESERVED2[16];
  __IOM uint32_t D_ISENABLER[16];        /*!&amp;lt; Offset: 0x1100 - 0x13C (R/W) Interrupt Set-Enable Registers */
        uint32_t RESERVED3[16];
  __IOM uint32_t D_ICENABLER[16];        /*!&amp;lt; Offset: 0x1180 - 0x1BC (R/W) Interrupt Clear-Enable Registers */
        uint32_t RESERVED4[16];
  __IOM uint32_t D_ISPENDR[16];          /*!&amp;lt; Offset: 0x1200 - 0x23C (R/W) Interrupt Set-Pending Registers */
        uint32_t RESERVED5[16];
  __IOM uint32_t D_ICPENDR[16];          /*!&amp;lt; Offset: 0x1280 - 0x2BC (R/W) Interrupt Clear-Pending Registers */
        uint32_t RESERVED6[16];
  __IOM uint32_t D_ISACTIVER[16];        /*!&amp;lt; Offset: 0x1300 - 0x33C (R/W) Interrupt Set-Active Registers */
        uint32_t RESERVED7[16];
  __IOM uint32_t D_ICACTIVER[16];        /*!&amp;lt; Offset: 0x1380 - 0x3BC (R/W) Interrupt Clear-Active Registers */
        uint32_t RESERVED8[16];
  __IOM uint8_t  D_IPRIORITYR[512];      /*!&amp;lt; Offset: 0x1400 - 0x5FC (R/W) Interrupt Priority Registers */
        uint32_t RESERVED9[128];
  __IOM uint8_t  D_ITARGETSR[512];       /*!&amp;lt; Offset: 0x1800 - 0x9FC (R/W) Interrupt Targets Registers */
        uint32_t RESERVED10[128];
  __IOM uint32_t D_ICFGR[32];            /*!&amp;lt; Offset: 0x1C00 - 0xC7C (R/W) Interrupt configuration registers */
        uint32_t RESERVED11[32];
  __IM  uint32_t D_PPISR;                /*!&amp;lt; Offset: 0x1D00 (R/ ) Private Peripheral Interrupt Status Register */
  __IM  uint32_t D_SPISR[15];            /*!&amp;lt; Offset: 0x1D04 - 0xD3C (R/ ) Shared Peripheral Interrupt Status Registers */
        uint32_t RESERVED12[112];
  __OM  uint32_t D_SGIR;                 /*!&amp;lt; Offset: 0x1F00 ( /W) Software Generated Interrupt Register */
        uint32_t RESERVED13[3];
  __IOM uint8_t  D_CPENDSGIR[16];        /*!&amp;lt; Offset: 0x1F10 - 0xF1C (R/W) SGI Clear-Pending Registers */
  __IOM uint8_t  D_SPENDSGIR[16];        /*!&amp;lt; Offset: 0x1F20 - 0xF2C (R/W) SGI Set-Pending Registers */
        uint32_t RESERVED14[40];
  __IM  uint32_t D_PIDR4;                /*!&amp;lt; Offset: 0x1FD0 (R/ ) Peripheral ID4 Register */
  __IM  uint32_t D_PIDR5;                /*!&amp;lt; Offset: 0x1FD4 (R/ ) Peripheral ID5 Register */
  __IM  uint32_t D_PIDR6;                /*!&amp;lt; Offset: 0x1FD8 (R/ ) Peripheral ID6 Register */
  __IM  uint32_t D_PIDR7;                /*!&amp;lt; Offset: 0x1FDC (R/ ) Peripheral ID7 Register */
  __IM  uint32_t D_PIDR0;                /*!&amp;lt; Offset: 0x1FE0 (R/ ) Peripheral ID0 Register */
  __IM  uint32_t D_PIDR1;                /*!&amp;lt; Offset: 0x1FE4 (R/ ) Peripheral ID1 Register */
  __IM  uint32_t D_PIDR2;                /*!&amp;lt; Offset: 0x1FE8 (R/ ) Peripheral ID2 Register */
  __IM  uint32_t D_PIDR3;                /*!&amp;lt; Offset: 0x1FEC (R/ ) Peripheral ID3 Register */
  __IM  uint32_t D_CIDR0;                /*!&amp;lt; Offset: 0x1FF0 (R/ ) Component ID0 Register */
  __IM  uint32_t D_CIDR1;                /*!&amp;lt; Offset: 0x1FF4 (R/ ) Component ID1 Register */
  __IM  uint32_t D_CIDR2;                /*!&amp;lt; Offset: 0x1FF8 (R/ ) Component ID2 Register */
  __IM  uint32_t D_CIDR3;                /*!&amp;lt; Offset: 0x1FFC (R/ ) Component ID3 Register */

  __IOM uint32_t C_CTLR;                 /*!&amp;lt; Offset: 0x2000 (R/W) CPU Interface Control Register */
  __IOM uint32_t C_PMR;                  /*!&amp;lt; Offset: 0x2004 (R/W) Interrupt Priority Mask Register */
  __IOM uint32_t C_BPR;                  /*!&amp;lt; Offset: 0x2008 (R/W) Binary Point Register */
  __IM  uint32_t C_IAR;                  /*!&amp;lt; Offset: 0x200C (R/ ) Interrupt Acknowledge Register */
  __OM  uint32_t C_EOIR;                 /*!&amp;lt; Offset: 0x2010 ( /W) End Of Interrupt Register */
  __IM  uint32_t C_RPR;                  /*!&amp;lt; Offset: 0x2014 (R/ ) Running Priority Register */
  __IM  uint32_t C_HPPIR;                /*!&amp;lt; Offset: 0x2018 (R/ ) Highest Priority Pending Interrupt Register */
  __IOM uint32_t C_ABPR;                 /*!&amp;lt; Offset: 0x201C (R/W) Aliased Binary Point Register */
  __IM  uint32_t C_AIAR;                 /*!&amp;lt; Offset: 0x2020 (R/ ) Aliased Interrupt Acknowledge Register */
  __OM  uint32_t C_AEOIR;                /*!&amp;lt; Offset: 0x2024 ( /W) Aliased End Of Interrupt Register */
  __IM  uint32_t C_AHPPIR;               /*!&amp;lt; Offset: 0x2028 (R/ ) Aliased Highest Priority Pending Interrupt Register */
        uint32_t RESERVED15[41];
  __IOM uint32_t C_APR0;                 /*!&amp;lt; Offset: 0x20D0 (R/W) Active Priority Register */
        uint32_t RESERVED16[3];
  __IOM uint32_t C_NSAPR0;               /*!&amp;lt; Offset: 0x20E0 (R/W) Non-secure Active Priority Register */
        uint32_t RESERVED17[6];
  __IM  uint32_t C_IIDR;                 /*!&amp;lt; Offset: 0x20FC (R/ ) CPU Interface Identification Register */
        uint32_t RESERVED18[960];
  __OM  uint32_t C_DIR;                  /*!&amp;lt; Offset: 0x3000 ( /W) Deactivate Interrupt Register */
} GIC_Type;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不过，这里只给出了GIC各寄存器的偏移地址，基地址需要通过CP15协处理器获取。&lt;/p&gt;
&lt;h3&gt;CP15协处理器&lt;/h3&gt;
&lt;p&gt;CP15协处理器共有16个32位寄存器。对CP15协处理器的访问可通过下列指令完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MRC&lt;/strong&gt;：将CP15的寄存器数据读取到ARM寄存器中&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MCR&lt;/strong&gt;：将ARM寄存器数据写入到CP15寄存器中。格式：&lt;code&gt;MCR{cond} p15, &amp;lt;opc1&amp;gt;, &amp;lt;Rt&amp;gt;, &amp;lt;CRn&amp;gt;, &amp;lt;CRm&amp;gt;, &amp;lt;opc2&amp;gt;&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;cond：指令执行的条件码。若忽略，表示无条件执行&lt;/li&gt;
&lt;li&gt;opc1：CP15要执行的操作码&lt;/li&gt;
&lt;li&gt;Rt：ARM源寄存器&lt;/li&gt;
&lt;li&gt;CRn：CP15的目标寄存器&lt;/li&gt;
&lt;li&gt;CRm：CP15中附加的目标寄存器或源操作数寄存器。如不需要附加信息，该位应设置为&lt;code&gt;c0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;opc2：可选的CP15特定操作码，不需要时设置为0&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CP15的16个寄存器在被MRC和MCR指令访问时，指令中的参数搭配若不同，得到的寄存器含义也不同。以c0寄存器为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/GIC_2.webp&quot; alt=&quot;CP15 c0寄存器参数搭配&quot; /&gt;&lt;/p&gt;
&lt;p&gt;例如，当&lt;code&gt;opc1 = 0&lt;/code&gt;, &lt;code&gt;CRm = c0&lt;/code&gt;, &lt;code&gt;opc2 = 0&lt;/code&gt;时，此时c0是MIDR寄存器，即主ID寄存器，包含厂商编号、主版本号、架构代码等信息。&lt;/p&gt;
&lt;p&gt;对于c15寄存器:
&lt;img src=&quot;/images/linux/GIC_3.webp&quot; alt=&quot;CP15 c15寄存器参数搭配&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见GIC基地址保存在CBAR中，因此可通过&lt;code&gt;MRC p15, 4, r1, c15, c0, 0&lt;/code&gt;指令获取GIC基地址至r1中。现在就可以设置GIC的寄存器了，比如：读取当前中断ID，将ID保存在&lt;code&gt;GICC_IAR&lt;/code&gt;中，SDK中已经给出&lt;code&gt;GICC_IAR&lt;/code&gt;的偏移地址&lt;code&gt;__IM  uint32_t C_IAR;                  /*!&amp;lt; Offset: 0x200C (R/ ) Interrupt Acknowledge Register */&lt;/code&gt;为&lt;code&gt;0x200C&lt;/code&gt;，因此获取当前中断ID的汇编代码为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MRC p15, 5, r1, c15, c0 ,0  ;获取GIC基地址
ADD r1, r1, #0x2000         ;基地址+0x2000得到CPU接口端寄存器起始地址
LDR r0, [r1, #0xC]          ;读取GIC_IAR的值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;总结：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;c0寄存器可获取到处理器内核信息&lt;/li&gt;
&lt;li&gt;c1寄存器可使能或禁止MMU、I/D Cache&lt;/li&gt;
&lt;li&gt;c12寄存器可设置中断向量偏移&lt;/li&gt;
&lt;li&gt;c15可获取GIC基地址&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中断使能&lt;/h3&gt;
&lt;h4&gt;IRQ和FIQ总中断使能&lt;/h4&gt;
&lt;p&gt;汇编下支持以下快捷指令：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;指令&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cpsid i&lt;/td&gt;
&lt;td&gt;禁止IRQ中断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpsie i&lt;/td&gt;
&lt;td&gt;使能IRQ中断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpsid f&lt;/td&gt;
&lt;td&gt;禁止FIQ中断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cpsie f&lt;/td&gt;
&lt;td&gt;使能FIQ中断&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h4&gt;ID0 - ID1019 中断使能和禁止&lt;/h4&gt;
&lt;p&gt;由以下寄存器完成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GICD_ISENABLERn&lt;/code&gt;：&lt;code&gt;GICD_ISENABLER0&lt;/code&gt;的bit[15:0]对应ID15 - 0的SGI中断，bit[31:16]对应ID31 - 16的PPI中断&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GICD_ICENABLERn&lt;/code&gt;：&lt;code&gt;GICD_ISENABLER1&lt;/code&gt; - &lt;code&gt;GICD_ISENABLER15&lt;/code&gt;控制SPI中断&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;中断优先级&lt;/h3&gt;
&lt;h4&gt;优先级数量配置&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;GICC_PMR&lt;/code&gt;寄存器用于配置优先级数量，寄存器低8位有效。I.MX6U是Cortex-A7内核，支持32个优先级，因此&lt;code&gt;GICC_PMR&lt;/code&gt;设置为&lt;code&gt;0b11111000&lt;/code&gt;.&lt;/p&gt;
&lt;h4&gt;抢占优先级和子优先级位数配置&lt;/h4&gt;
&lt;p&gt;抢占优先级和子优先级各占多少位由寄存器&lt;code&gt;GICC_BPR&lt;/code&gt;决定，低3位有效，配置如下所示：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Binary Point&lt;/th&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;0&lt;/td&gt;
&lt;td&gt;[7:1]&lt;/td&gt;
&lt;td&gt;[0]&lt;/td&gt;
&lt;td&gt;7 级抢占优先级，1 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;[7:2]&lt;/td&gt;
&lt;td&gt;[1:0]&lt;/td&gt;
&lt;td&gt;6 级抢占优先级，2 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;[7:3]&lt;/td&gt;
&lt;td&gt;[2:0]&lt;/td&gt;
&lt;td&gt;5 级抢占优先级，3 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;[7:4]&lt;/td&gt;
&lt;td&gt;[3:0]&lt;/td&gt;
&lt;td&gt;4 级抢占优先级，4 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;[7:5]&lt;/td&gt;
&lt;td&gt;[4:0]&lt;/td&gt;
&lt;td&gt;3 级抢占优先级，5 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;[7:6]&lt;/td&gt;
&lt;td&gt;[5:0]&lt;/td&gt;
&lt;td&gt;2 级抢占优先级，6 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;[7:7]&lt;/td&gt;
&lt;td&gt;[6:0]&lt;/td&gt;
&lt;td&gt;1 级抢占优先级，7 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;无&lt;/td&gt;
&lt;td&gt;[7:0]&lt;/td&gt;
&lt;td&gt;0 级抢占优先级，8 级子优先级。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;一般而言所有的中断优先级位都配置为抢占优先级。如IMX6U的优先级位数为5（32个优先级），可设置Binary point为2，表示5个优先级位全部为抢占优先级。&lt;/p&gt;
&lt;h4&gt;优先级设置&lt;/h4&gt;
&lt;p&gt;某个中断ID的中断优先级设置由寄存器&lt;code&gt;GICD_IPRIORITYR&lt;/code&gt;完成，每个中断ID配有一个该寄存器，共512个。如果优先级个数为32，即使用&lt;code&gt;GICD_IPRIORITYR&lt;/code&gt;的bit[7:4]来设置优先级（实际优先级左移3位）。例如，要设置ID == 40中断优先级为5：&lt;code&gt;GICD_IPRIORITYR[40] = 5 &amp;lt;&amp;lt; 3&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;总结一下，中断优先级设置分三部分：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置&lt;code&gt;GICC_PMR&lt;/code&gt;，配置优先级个数，IMX6U为32级&lt;/li&gt;
&lt;li&gt;设置抢占优先级和子优先级位数，一般默认所有位数都为抢占优先级&lt;/li&gt;
&lt;li&gt;设置指定中断ID的优先级&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;复位中断服务函数&lt;/h3&gt;
&lt;p&gt;整个启动文件在设置完中断向量表后，要进行复位中断的设置，其分为以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置复位中断：关闭IRQ，然后关闭I/D Cache、MMU、对齐检测和分支预测&lt;/li&gt;
&lt;li&gt;进行中断向量表重映射&lt;/li&gt;
&lt;li&gt;设置IRQ模式、SYS模式和SVC模式下的栈指针，栈大小均为2MB&lt;/li&gt;
&lt;li&gt;打开IRQ中断&lt;/li&gt;
&lt;li&gt;跳转至main函数&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/* 复位中断 */	
Reset_Handler:

	cpsid i						/* 关闭全局中断 */

	/* 关闭I,DCache和MMU 
	 * 采取读-改-写的方式。
	 */
	mrc     p15, 0, r0, c1, c0, 0     /* 读取CP15的C1寄存器到R0中       		        	*/
    bic     r0,  r0, #(0x1 &amp;lt;&amp;lt; 12)     /* 清除C1寄存器的bit12位(I位)，关闭I Cache            	*/
    bic     r0,  r0, #(0x1 &amp;lt;&amp;lt;  2)     /* 清除C1寄存器的bit2(C位)，关闭D Cache    				*/
    bic     r0,  r0, #0x2             /* 清除C1寄存器的bit1(A位)，关闭对齐						*/
    bic     r0,  r0, #(0x1 &amp;lt;&amp;lt; 11)     /* 清除C1寄存器的bit11(Z位)，关闭分支预测					*/
    bic     r0,  r0, #0x1             /* 清除C1寄存器的bit0(M位)，关闭MMU				       	*/
    mcr     p15, 0, r0, c1, c0, 0     /* 将r0寄存器中的值写入到CP15的C1寄存器中	 				*/

#if 0
	/* 汇编版本设置中断向量表偏移 */
	ldr r0, =0X87800000

	dsb
	isb
	mcr p15, 0, r0, c12, c0, 0
	dsb
	isb
#endif
    
	/* 设置各个模式下的栈指针，
	 * 注意：IMX6UL的堆栈是向下增长的！
	 * 堆栈指针地址一定要是4字节地址对齐的！！！
	 * DDR范围:0X80000000~0X9FFFFFFF
	 */
	/* 进入IRQ模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零，也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x12 	/* r0或上0x13,表示使用IRQ模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0x80600000	/* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */

	/* 进入SYS模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零，也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x1f 	/* r0或上0x13,表示使用SYS模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0x80400000	/* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */

	/* 进入SVC模式 */
	mrs r0, cpsr
	bic r0, r0, #0x1f 	/* 将r0寄存器中的低5位清零，也就是cpsr的M0~M4 	*/
	orr r0, r0, #0x13 	/* r0或上0x13,表示使用SVC模式					*/
	msr cpsr, r0		/* 将r0 的数据写入到cpsr_c中 					*/
	ldr sp, =0X80200000	/* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */

	cpsie i				/* 打开全局中断 */
#if 0
	/* 使能IRQ中断 */
	mrs r0, cpsr		/* 读取cpsr寄存器值到r0中 			*/
	bic r0, r0, #0x80	/* 将r0寄存器中bit7清零，也就是CPSR中的I位清零，表示允许IRQ中断 */
	msr cpsr, r0		/* 将r0重新写入到cpsr中 			*/
#endif

	b main				/* 跳转到main函数 			 	*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;其他中断&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 未定义中断 */
Undefined_Handler:
	ldr r0, =Undefined_Handler
	bx r0

/* SVC中断 */
SVC_Handler:
	ldr r0, =SVC_Handler
	bx r0

/* 预取终止中断 */
PrefAbort_Handler:
	ldr r0, =PrefAbort_Handler	
	bx r0

/* 数据终止中断 */
DataAbort_Handler:
	ldr r0, =DataAbort_Handler
	bx r0

/* 未使用的中断 */
NotUsed_Handler:

	ldr r0, =NotUsed_Handler
	bx r0

    /* FIQ中断 */
FIQ_Handler:

	ldr r0, =FIQ_Handler	
	bx r0	
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;IRQ中断服务函数&lt;/h3&gt;
&lt;p&gt;所有的外部中断都会触发IRQ中断，因此IRQ中断服务函数的工作就是获取当前发生的中断ID以确定中断来源，然后根据不同的外部中断来进行不同的处理。这里会涉及到几个ARM中常用的寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP寄存器：内部程序调用暂存寄存器，子程序的连接text段中常使用该规则&lt;/li&gt;
&lt;li&gt;SP寄存器：栈指针寄存器，用于存储当前栈顶地址。程序执行过程中，栈是用来存储临时变量、函数调用返回地址等数据的重要数据结构，SP寄存器的值会随着栈的变化而变化&lt;/li&gt;
&lt;li&gt;LR寄存器：连接寄存器，程序跳转（子程序调用，中断跳转）后，arm自动在该寄存器中存入原程序（未跳转）的下一条指令的地址，也叫函数调用返回地址。当一个函数被调用时，LR寄存器会存储调用该函数的下一条指令的地址，当函数执行完毕后，程序会跳转到LR寄存器中存储的地址继续执行。&lt;/li&gt;
&lt;li&gt;PC寄存器：程序计数器，保存的是当前正在取指的指令的地址（arm采用2级流水线，因此是当前正在执行指令的地址+8）。PC寄存器是ARM中的程序计数器，用于存储下一条将要执行的指令的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，还有两个比较重要的状态寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPSR：程序状态寄存器，在任何处理器模式下被访问。它包含了条件标志位、中断禁止位、当前处理器模式标志以及其他的一些控制和状态位。CPSR在用户级编程时用于存储条件码。&lt;/li&gt;
&lt;li&gt;SPSR：程序状态保存寄存器，每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态，以便异常返回后恢复异常发生时的工作状态。当特定的异常中断发生时，这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时，可以用SPSR来恢复CPSR。由于用户模式和系统模式不是异常中断模式，所以他没有SPSR。当用户在用户模式或系统模式访问SPSR，将产生不可预知的后果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;二者常用于MRS或MSR指令,用于spsr中的值转移到寄存器或把寄存器的内容加载到spsr中，如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrs r0, spsr                /* 读取spsr寄存器 */
msr spsr_cxsf, r0            /* 恢复spsr */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IRQ中断服务函数的编写大致分为以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;进入IRQ模式，保存上下文。ARM在进入IRQ模式会自动切换LR和SPSR，但不会自动保存其他寄存器，因此需手动保存r0-r3和r12寄存器（后续操作可能修改）。自动切换时，LR保存被中断指令的下一条指令&lt;code&gt;-4&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/* IRQ中断 */
IRQ_Handler:
	push {lr}					/* 保存lr地址 */
	push {r0-r3, r12}			/* 保存r0-r3，r12寄存器 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;保存现场。SPSR存储被中断前CPU的模式状态，必须保存SPSR以正确恢复中断前的执行环境。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;	mrs r0, spsr				/* 读取spsr寄存器 */
	push {r0}					/* 保存spsr寄存器 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;获取GIC中断号。通过MRC向cp15读取寄存器以获取GIC基址，从而计算GICC_IAR地址并读取，读取后自动标记该中断为active状态，获取到的中断号&lt;code&gt;r0&lt;/code&gt;作为参数传递给C语言ISR。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;	mrc p15, 4, r1, c15, c0, 0 /* 读取CP15的C0寄存器内的值到R1寄存器中
								* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
								* Cortex-A7 Technical ReferenceManua.pdf P68 P138
								*/							
	add r1, r1, #0X2000			/* GIC基地址加0X2000，也就是GIC的CPU接口端基地址 */
	ldr r0, [r1, #0XC]			/* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器，
								 * GICC_IAR寄存器保存这当前发生中断的中断号，我们要根据
								 * 这个中断号来绝对调用哪个中断服务函数
								 */
	push {r0, r1}				/* 保存r0,r1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;切换至SVC模式并调用C函数。IAR模式栈空间有限，切换至SVC模式（特权模式）以获得更大的栈空间，并允许C函数处理更复杂的逻辑，如中断嵌套。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;	cps #0x13					/* 进入SVC模式，允许其他中断再次进去 */
	
	push {lr}					/* 保存SVC模式的lr寄存器 */
	ldr r2, =system_irqhandler	/* 加载C语言中断处理函数到r2寄存器中*/
	blx r2						/* 运行C语言中断处理函数，带有中断号参数，保存在R0寄存器中 */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;恢复模式并发送中断结束信号。向向GIC的End of Interrupt Register (GICC_EOIR) 写入中断号，告知中断控制器该中断已处理完毕。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;	pop {lr}					/* 执行完C语言中断服务函数，lr出栈 */
	cps #0x12					/* 进入IRQ模式 */
	pop {r0, r1}				
	str r0, [r1, #0X10]			/* 中断执行完成，写EOIR */
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;恢复中断前状态并返回。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里是一个很容易搞错的点。ARM使用三级流水线机制：取指-&amp;gt;译指-&amp;gt;执行，而我们总以执行位置作为参考点，因此PC永远是当前执行位置&lt;code&gt;+8&lt;/code&gt;；LR存放函数调用结束后返回继续执行的地址，也就是当前执行指令的下一条(&lt;code&gt;+4&lt;/code&gt;)。进入IRQ中断时，中断总是在执行一条指令后再进入，此时PC更新为&lt;code&gt;+12&lt;/code&gt;，相应的LR变为&lt;code&gt;+8&lt;/code&gt;，然后LR被入栈保存。如果中断结束后，直接将LR出栈，程序会从&lt;code&gt;+8&lt;/code&gt;处开始运行，那么&lt;code&gt;+4&lt;/code&gt;处的指令就直接被跳过了。因此，中断结束后将LR出栈时，要将LR&lt;code&gt;-4&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;用表格可以解释为：&lt;/p&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;状态A：中断前&lt;/th&gt;
&lt;th&gt;状态B：进入中断后&lt;/th&gt;
&lt;th&gt;状态C：中断恢复&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0x00&lt;/td&gt;
&lt;td&gt;MOV R1, R0&lt;/td&gt;
&lt;td&gt;准备执行&lt;/td&gt;
&lt;td&gt;已执行&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x04&lt;/td&gt;
&lt;td&gt;MOV R2, R1&lt;/td&gt;
&lt;td&gt;LR&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;LR（恢复为状态A时的PC值-4）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x08&lt;/td&gt;
&lt;td&gt;MOV R3, R2&lt;/td&gt;
&lt;td&gt;PC&lt;/td&gt;
&lt;td&gt;LR（已入栈）&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0x0c&lt;/td&gt;
&lt;td&gt;MOV R4 ,R3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;PC值&lt;/td&gt;
&lt;td&gt;0x08&lt;/td&gt;
&lt;td&gt;0x12&lt;/td&gt;
&lt;td&gt;由LR值决定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;LR值&lt;/td&gt;
&lt;td&gt;0x04&lt;/td&gt;
&lt;td&gt;0x08&lt;/td&gt;
&lt;td&gt;0x04&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;再通俗一点：PC始终指向当前执行指令+8，发生中断时，入栈保存的LR实际上是PC的地址，如果返回时将LR直接赋给PC，中间就跳过了一个指令，因此LR出栈后要&lt;code&gt;-4&lt;/code&gt;才能赋给PC。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;	pop {r0}						
	msr spsr_cxsf, r0			/* 恢复spsr */

	pop {r0-r3, r12}			/* r0-r3,r12出栈 */
	pop {lr}					/* lr出栈 */
	subs pc, lr, #4				/* 将计算后的地址（lr-4）赋给pc */	
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中断驱动文件（C函数）&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;中断初始化函数。向量表基地址设置为程序存放开始地址。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void int_init(void) {
    GIC_Init();                        // 初始化通用中断控制器，该函数由SDK提供
    system_irqtable_init();            // 初始化中断函数表
    __set_VBAR((uint32_t)0x87800000);  // 设置向量表基地址
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;中断向量表初始化。将所有中断初始化为default_irqhandler（死循环），强制开发者显式注册有效ISR，避免未处理中断导致不可控行为。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void system_irqtable_init(void) {
    unsigned int i = 0;
    irqNesting = 0;  // 重置嵌套计数器
    
    for(i = 0; i &amp;lt; NUMBER_OF_INT_VECTORS; i++) {
        system_register_irqhandler((IRQn_Type)i, default_irqhandler, NULL);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;中断注册函数。用于绑定中断服务函数和中断源。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void system_register_irqhandler(IRQn_Type irq, system_irq_handler_t handler, void *userParam) {
    irqTable[irq].irqHandler = handler;   // 注册处理函数
    irqTable[irq].userParam = userParam;  // 绑定用户参数
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;中断分发器。默认允许中断嵌套。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void system_irqhandler(unsigned int giccIar) {
    uint32_t intNum = giccIar &amp;amp; 0x3FFUL;  // 提取中断号
    
    /* 校验中断号有效性 */
    if ((intNum == 1023) || (intNum &amp;gt;= NUMBER_OF_INT_VECTORS)) {
        return;  // 非法中断号（1023伪中断或中断号不在表内）直接返回
    }
 
    irqNesting++;  // 嵌套计数增加
    
    /* 调用注册的ISR */
    irqTable[intNum].irqHandler(intNum, irqTable[intNum].userParam);
 
    irqNesting--;  // 嵌套计数减少
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;中断处理全流程&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;硬件触发：外设触发中断，GIC将中断请求发送至CPU。&lt;/li&gt;
&lt;li&gt;汇编入口：CPU跳转至IRQ_Handler，保存上下文并调用system_irqhandler(giccIar)。&lt;/li&gt;
&lt;li&gt;中断分发：
&lt;ul&gt;
&lt;li&gt;解析giccIar获取有效中断号&lt;/li&gt;
&lt;li&gt;查表irqTable[intNum]获取处理函数和参数&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;ISR执行：执行用户注册的函数（如uart_isr），处理具体中断任务。&lt;/li&gt;
&lt;li&gt;中断结束：汇编代码写GICC_EOIR通知GIC处理完成。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;示例（定时器按键消抖）&lt;/h2&gt;
&lt;p&gt;实现功能：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按下按键，进入外部中断，在中断中开启定时器&lt;/li&gt;
&lt;li&gt;定时器中断中完成消抖延时，中断周期即为延时时间。如果定时中断触发，表示消抖完成，执行按键处理函数&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;按键初始化&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;设置GPIO复用和Config&lt;/li&gt;
&lt;li&gt;填充gpio结构体，初始化gpio&lt;/li&gt;
&lt;li&gt;使能中断，注册ISR（绑定中断源和对应的ISR）&lt;/li&gt;
&lt;li&gt;初始化定时器&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;void filterkey_init(void)
{	
	gpio_pin_config_t key_config;
	
	/* 1、初始化IO复用 */
	IOMUXC_SetPinMux(IOMUXC_UART1_CTS_B_GPIO1_IO18,0);	/* 复用为GPIO1_IO18 */

	/* 2、、配置GPIO1_IO18的IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 11 默认22K上拉
	 *bit [13]: 1 pull功能
	 *bit [12]: 1 pull/keeper使能
	 *bit [11]: 0 关闭开路输出
	 *bit [7:6]: 10 速度100Mhz
	 *bit [5:3]: 000 关闭输出
	 *bit [0]: 0 低转换率
	 */
	IOMUXC_SetPinConfig(IOMUXC_UART1_CTS_B_GPIO1_IO18,0xF080);
	
	/* 3、初始化GPIO为中断 */
	key_config.direction = kGPIO_DigitalInput;          /* 输入 */
	key_config.interruptMode = kGPIO_IntFallingEdge;    /* 下降沿触发 */
	key_config.outputLogic = 1;
	gpio_init(GPIO1, 18, &amp;amp;key_config);

	GIC_EnableIRQ(GPIO1_Combined_16_31_IRQn); /* 使能GIC中对应的中断 */
	
	/* 注册中断服务函数 */
	system_register_irqhandler(GPIO1_Combined_16_31_IRQn, 
							   (system_irq_handler_t)gpio1_16_31_irqhandler, 
							   NULL);
	
	gpio_enableint(GPIO1, 18);		/* 使能GPIO1_IO18的中断功能 */

	filtertimer_init(66000000/100);	/* 初始化定时器,10ms */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;定时器初始化&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void filtertimer_init(unsigned int value)
{
	EPIT1-&amp;gt;CR = 0;	//先清零
	
	/*
     * CR寄存器:
     * bit25:24 01 时钟源选择Peripheral clock=66MHz
     * bit15:4  0  1分频
     * bit3:	1  当计数器到0的话从LR重新加载数值
     * bit2:	1  比较中断使能
     * bit1:    1  初始计数值来源于LR寄存器值
     * bit0:    0  先关闭EPIT1
     */
	EPIT1-&amp;gt;CR = (1&amp;lt;&amp;lt;24 | 1&amp;lt;&amp;lt;3 | 1&amp;lt;&amp;lt;2 | 1&amp;lt;&amp;lt;1);

	/* 计数值 */
	EPIT1-&amp;gt;LR = value;
	
	/* 比较寄存器，当计数器值和此寄存器值相等的话就会产生中断 */
	EPIT1-&amp;gt;CMPR	= 0;	
	
	GIC_EnableIRQ(EPIT1_IRQn);	/* 使能GIC中对应的中断 */
	
	/* 注册中断服务函数		    */
	system_register_irqhandler(EPIT1_IRQn, (system_irq_handler_t)filtertimer_irqhandler, NULL);	
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;GPIO ISR函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void gpio1_16_31_irqhandler(void)
{ 
	/* 开启定时器 */
	filtertimer_restart(66000000/100);

	/* 清除中断标志位 */
	gpio_clearintflags(GPIO1, 18);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;定时器 ISR函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void filtertimer_irqhandler(void)
{ 
	static unsigned char state = OFF;

	if(EPIT1-&amp;gt;SR &amp;amp; (1&amp;lt;&amp;lt;0)) 					/* 判断比较事件是否发生			*/
	{
		filtertimer_stop();					/* 关闭定时器 				*/
		if(gpio_pinread(GPIO1, 18) == 0)	/* KEY0 				*/
		{
			state = !state;
			beep_switch(state);				/* 反转蜂鸣器 				*/
		}
	}
		
	EPIT1-&amp;gt;SR |= 1&amp;lt;&amp;lt;0; 						/* 清除中断标志位 				*/
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;辅助函数&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void filtertimer_stop(void)
{
	EPIT1-&amp;gt;CR &amp;amp;= ~(1&amp;lt;&amp;lt;0);	/* 关闭定时器 */
}

void filtertimer_restart(unsigned int value)
{
	EPIT1-&amp;gt;CR &amp;amp;= ~(1&amp;lt;&amp;lt;0);	/* 先关闭定时器 */
	EPIT1-&amp;gt;LR = value;		/* 计数值 			*/
	EPIT1-&amp;gt;CR |= (1&amp;lt;&amp;lt;0);	/* 打开定时器 		*/
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>I.MX6ULL 时钟配置</title><link>https://arkmostima.pages.dev/posts/%E6%97%B6%E9%92%9F%E9%85%8D%E7%BD%AE/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%97%B6%E9%92%9F%E9%85%8D%E7%BD%AE/</guid><description>包括以下三个时钟来源： 1. 外部晶振，高速晶振频率24Mhz，低速晶振频率32.768KHz 2. 低压差分信号 (LVDS) I/O 端口，用于从外部获取时钟信号 3. 锁相环(PLL)，主时钟包含七个 PLL。其中两个 PLL 分…</description><pubDate>Mon, 03 Mar 2025 06:52:08 GMT</pubDate><content:encoded>&lt;h2&gt;时钟来源和生成&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_1.webp&quot; alt=&quot;时钟生成来源&quot; /&gt;
包括以下三个时钟来源：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;外部晶振，高速晶振频率24Mhz，低速晶振频率32.768KHz&lt;/li&gt;
&lt;li&gt;低压差分信号 (LVDS) I/O 端口，用于从外部获取时钟信号&lt;/li&gt;
&lt;li&gt;锁相环(PLL)，主时钟包含七个 PLL。其中两个 PLL 分别配备四个相位小数分频器 (PFD)，以生成额外的频率。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;七个PLL包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PLL1(ARM_PLL)，ARM内核时钟，最高可倍频至1.3GHz(但MCU最高仅支持到1.0GHz)&lt;/li&gt;
&lt;li&gt;PLL2(SYS_PLL/528_PLL)，固定倍频22倍，从外部高速晶振的24MHz倍频产生528MHz，自带四个PFD。通常528_PLL和4路PFD是MX6U内部系统总线的时钟源，如逻辑单元、DDR、NAND/NOR设备等&lt;/li&gt;
&lt;li&gt;PLL3(USB1_PLL)，USB PHY的第一个实例（USBPHY1，也称为 OTG PHY）一起使用。此 PLL 驱动四个 PFD（PLL3_PFD0...PLL3_PFD3）并以固定乘数 20 运行。这导致 VCO 频率为 480 MHz，振荡器为 24 MHz。主 PLL 输出及其 PFD 输出用作许多需要恒定频率的时钟根的输入，例如 UART 和其他串行接口、音频接口等&lt;/li&gt;
&lt;li&gt;PLL4(Audio_PLL)，用于生成具有标准音频频率的低抖动和高精度音频时钟。其频率范围为 650 MHz 至 1300 MHz，频率分辨率优于 1 Hz。该时钟主要用作串行音频接口的时钟和外部音频编解码器的参考时钟。&lt;/li&gt;
&lt;li&gt;PLL5(Video_PLL)，用于生成具有标准视频频率的低抖动和高精度视频时钟。PLL 振荡器频率范围为 650 MHz 至 1300 MHz，频率分辨率优于 1 Hz。该时钟主要用作显示和视频接口的时钟。&lt;/li&gt;
&lt;li&gt;PLL6(ENET_PLL)，固定(20+5/6)倍频，输出500MHz，用于生成以太网&lt;/li&gt;
&lt;li&gt;PLL7(USB2_PLL)，专门为 USB2 PHY（USBPHY2，也称为 OTG PHY）提供时钟，固定倍频 20，输出频率480MHz&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;CCM时钟树&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_2.webp&quot; alt=&quot;时钟树(1)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_3.webp&quot; alt=&quot;时钟树(2)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体参见手册第629页。&lt;/p&gt;
&lt;h2&gt;时钟配置&lt;/h2&gt;
&lt;p&gt;以ARM时钟配置为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_4.webp&quot; alt=&quot;ARM时钟&quot; /&gt;&lt;/p&gt;
&lt;p&gt;时钟从PLL1的996MHz开始，通过寄存器CCM_CACRR的ARM_PODF位进行分频，可选1/2/4/8分频，然后得到ARM时钟ARM_CLK_ROOT。灰色的/2分频并未实际效果。其中PLL1频率可通过寄存器CCM_ANALOG_PLL_ARMn设置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_5.webp&quot; alt=&quot;CCM_CACRR寄存器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_6.webp&quot; alt=&quot;CCM_ANALOG_PLL_ARMn寄存器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;修改PLL1时钟时要首先要切换内核时钟源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_7.webp&quot; alt=&quot;PLL1时钟源&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;PLL1最终输出时钟频率。&lt;/li&gt;
&lt;li&gt;选择器，用于选择pll1的时钟源，由CCM_CCSR的PLL1_SW_CLK_SEL位决定。默认为pll1_main_clk，但如果要调整pll1_main_clk频率，则首先应将pll_sw_clk切换至step_clk，调整完成后再切换回来。&lt;/li&gt;
&lt;li&gt;选择器，用于选择step_clk的时钟源，由CCM_CCSR的STEP_SEL位决定。默认为osc_clk，即外部高速晶振。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&quot;/images/linux/clock_8.webp&quot; alt=&quot;CCM_CCSR寄存器&quot; /&gt;&lt;/p&gt;
&lt;p&gt;至此，修改I.MX6U主频的步骤可以归纳为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;设置CCM_CCSR的STEP_SEL位，设置step_clk时钟源为外部24MHz高速晶振&lt;/li&gt;
&lt;li&gt;设置CCM_CCSR的PLL1_SW_CLK_SEL位，设置pll_sw_clk来源为step_clk=24Mhz&lt;/li&gt;
&lt;li&gt;设置CCM_ANALOG_PLL_ARMn，将pll1_main_clk设置为1056MHz&lt;/li&gt;
&lt;li&gt;设置CCM_CCSR的PLL1_SW_CLK_SEL位，将pll1_sw_clk时钟源切换回pll1_main_clk&lt;/li&gt;
&lt;li&gt;设置CCM_CACRR的ARM_PODF为2分频，完成分频设置&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;其他的时钟配置方式大同小异。&lt;/p&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @description	: 初始化系统时钟，设置系统时钟为792Mhz，并且设置PLL2和PLL3各个
 				  PFD时钟,所有的时钟频率均按照I.MX6U官方手册推荐的值.
 * @param 		: 无
 * @return 		: 无
 */
void imx6u_clkinit(void)
{
	unsigned int reg = 0;
	/* 1、设置ARM内核时钟为792MHz */
	/* 1.1、判断当前ARM内核是使用的那个时钟源启动的，正常情况下ARM内核是由pll1_sw_clk驱动的，而
	 *      pll1_sw_clk有两个来源：pll1_main_clk和tep_clk。
	 *      如果我们要让ARM内核跑到792M的话那必须选择pll1_main_clk作为pll1的时钟源。
	 *      如果我们要修改pll1_main_clk时钟的话就必须先将pll1_sw_clk从pll1_main_clk切换到step_clk,
	 *		当修改完pll1_main_clk以后在将pll1_sw_clk切换回pll1_main_clk。而step_clk的时钟源可以选择
	 * 		板子上的24MHz晶振。
	 */
	
	if((((CCM-&amp;gt;CCSR) &amp;gt;&amp;gt; 2) &amp;amp; 0x1 ) == 0) 	/* 当前pll1_sw_clk使用的pll1_main_clk*/
	{	
		CCM-&amp;gt;CCSR &amp;amp;= ~(1 &amp;lt;&amp;lt; 8);				/* 配置step_clk时钟源为24MH OSC */	
		CCM-&amp;gt;CCSR |= (1 &amp;lt;&amp;lt; 2);				/* 配置pll1_sw_clk时钟源为step_clk */
	}

	/* 1.2、设置pll1_main_clk为792MHz
	 *      因为pll1_sw_clk进ARM内核的时候会被二分频！
	 *      配置CCM_ANLOG-&amp;gt;PLL_ARM寄存器
	 *      bit13: 1 使能时钟输出
	 *      bit[6:0]: 66, 由公式：Fout = Fin * div_select / 2.0，792=24*div_select/2.0,
	 *              		得出：div_select=    66 
	 */
	CCM_ANALOG-&amp;gt;PLL_ARM = (1 &amp;lt;&amp;lt; 13) | ((66 &amp;lt;&amp;lt; 0) &amp;amp; 0X7F); 	/* 配置pll1_main_clk=792MHz */
	CCM-&amp;gt;CCSR &amp;amp;= ~(1 &amp;lt;&amp;lt; 2);									/* 将pll_sw_clk时钟重新切换回pll1_main_clk */
	CCM-&amp;gt;CACRR = 0;											/* ARM内核时钟为pll1_sw_clk/1=792/1=792Mhz */

	/* 2、设置PLL2(SYS PLL)各个PFD */
	reg = CCM_ANALOG-&amp;gt;PFD_528;
	reg &amp;amp;= ~(0X3F3F3F3F);		/* 清除原来的设置 						*/
	reg |= 32&amp;lt;&amp;lt;24;				/* PLL2_PFD3=528*18/32=297Mhz 	*/
	reg |= 24&amp;lt;&amp;lt;16;				/* PLL2_PFD2=528*18/24=396Mhz(DDR使用的时钟，最大400Mhz) */
	reg |= 16&amp;lt;&amp;lt;8;				/* PLL2_PFD1=528*18/16=594Mhz 	*/
	reg |= 27&amp;lt;&amp;lt;0;				/* PLL2_PFD0=528*18/27=352Mhz  	*/
	CCM_ANALOG-&amp;gt;PFD_528=reg;	/* 设置PLL2_PFD0~3 		 		*/

	/* 3、设置PLL3(USB1)各个PFD */
	reg = 0;					/* 清零   */
	reg = CCM_ANALOG-&amp;gt;PFD_480;
	reg &amp;amp;= ~(0X3F3F3F3F);		/* 清除原来的设置 							*/
	reg |= 19&amp;lt;&amp;lt;24;				/* PLL3_PFD3=480*18/19=454.74Mhz 	*/
	reg |= 17&amp;lt;&amp;lt;16;				/* PLL3_PFD2=480*18/17=508.24Mhz 	*/
	reg |= 16&amp;lt;&amp;lt;8;				/* PLL3_PFD1=480*18/16=540Mhz		*/
	reg |= 12&amp;lt;&amp;lt;0;				/* PLL3_PFD0=480*18/12=720Mhz	 	*/
	CCM_ANALOG-&amp;gt;PFD_480=reg;	/* 设置PLL3_PFD0~3 					*/	

	/* 4、设置AHB时钟 最小6Mhz， 最大132Mhz (boot rom自动设置好了可以不用设置)*/
	CCM-&amp;gt;CBCMR &amp;amp;= ~(3 &amp;lt;&amp;lt; 18); 	/* 清除设置*/ 
	CCM-&amp;gt;CBCMR |= (1 &amp;lt;&amp;lt; 18);	/* pre_periph_clk=PLL2_PFD2=396MHz */
	CCM-&amp;gt;CBCDR &amp;amp;= ~(1 &amp;lt;&amp;lt; 25);	/* periph_clk=pre_periph_clk=396MHz */
	while(CCM-&amp;gt;CDHIPR &amp;amp; (1 &amp;lt;&amp;lt; 5));/* 等待握手完成 */
		
	/* 修改AHB_PODF位的时候需要先禁止AHB_CLK_ROOT的输出，但是
	 * 我没有找到关闭AHB_CLK_ROOT输出的的寄存器，所以就没法设置。
	 * 下面设置AHB_PODF的代码仅供学习参考不能直接拿来使用！！
	 * 内部boot rom将AHB_PODF设置为了3分频，即使我们不设置AHB_PODF，
	 * AHB_ROOT_CLK也依旧等于396/3=132Mhz。
	 */
#if 0
	/* 要先关闭AHB_ROOT_CLK输出，否则时钟设置会出错 */
	CCM-&amp;gt;CBCDR &amp;amp;= ~(7 &amp;lt;&amp;lt; 10);	/* CBCDR的AHB_PODF清零 */
	CCM-&amp;gt;CBCDR |= 2 &amp;lt;&amp;lt; 10;		/* AHB_PODF 3分频，AHB_CLK_ROOT=132MHz */
	while(CCM-&amp;gt;CDHIPR &amp;amp; (1 &amp;lt;&amp;lt; 1));/
* 等待握手完成 */
#endif
	
	/* 5、设置IPG_CLK_ROOT最小3Mhz，最大66Mhz (boot rom自动设置好了可以不用设置)*/
	CCM-&amp;gt;CBCDR &amp;amp;= ~(3 &amp;lt;&amp;lt; 8);	/* CBCDR的IPG_PODF清零 */
	CCM-&amp;gt;CBCDR |= 1 &amp;lt;&amp;lt; 8;		/* IPG_PODF 2分频，IPG_CLK_ROOT=66MHz */
	
	/* 6、设置PERCLK_CLK_ROOT时钟 */
	CCM-&amp;gt;CSCMR1 &amp;amp;= ~(1 &amp;lt;&amp;lt; 6);	/* PERCLK_CLK_ROOT时钟源为IPG */
	CCM-&amp;gt;CSCMR1 &amp;amp;= ~(7 &amp;lt;&amp;lt; 0);	/* PERCLK_PODF位清零，即1分频 */
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Linux下规范性Makefile规则</title><link>https://arkmostima.pages.dev/posts/linux%E4%B8%8B%E8%A7%84%E8%8C%83%E6%80%A7makefile%E8%A7%84%E5%88%99/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/linux%E4%B8%8B%E8%A7%84%E8%8C%83%E6%80%A7makefile%E8%A7%84%E5%88%99/</guid><description>bsp clk (时钟驱动) bspclk.c bspclk.h delay (延时驱动) bspdelay.c bspdelay.h led (LED驱动) bspled.c bspled.h imx6ul cc.h (变量类型声明)…</description><pubDate>Sun, 02 Mar 2025 07:27:16 GMT</pubDate><content:encoded>&lt;h2&gt;文件目录&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;bsp
&lt;ul&gt;
&lt;li&gt;clk   (时钟驱动)
&lt;ul&gt;
&lt;li&gt;bsp_clk.c&lt;/li&gt;
&lt;li&gt;bsp_clk.h&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;delay (延时驱动)
&lt;ul&gt;
&lt;li&gt;bsp_delay.c&lt;/li&gt;
&lt;li&gt;bsp_delay.h&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;led   (LED驱动)
&lt;ul&gt;
&lt;li&gt;bsp_led.c&lt;/li&gt;
&lt;li&gt;bsp_led.h&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;imx6ul
&lt;ul&gt;
&lt;li&gt;cc.h  (变量类型声明)&lt;/li&gt;
&lt;li&gt;fsl_common.h  (NXP官方SDK通用宏定义文档)&lt;/li&gt;
&lt;li&gt;fsl_iomuxc.h  (NXP官方SDK寄存器地址定义文档)&lt;/li&gt;
&lt;li&gt;imx6ul.h      (常用头文件)&lt;/li&gt;
&lt;li&gt;MCIMX6Y2.h    (NXP官方SDK寄存器结构体和相关位定义文档)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;obj
.o文件目标地址&lt;/li&gt;
&lt;li&gt;project
&lt;ul&gt;
&lt;li&gt;main.c    (主函数)&lt;/li&gt;
&lt;li&gt;start.S   (启动文件)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Makefile&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;CROSS_COMPILE 	?= arm-linux-gnueabihf-
TARGET		  	?= bsp

CC 				:= $(CROSS_COMPILE)gcc
LD				:= $(CROSS_COMPILE)ld
OBJCOPY 		:= $(CROSS_COMPILE)objcopy
OBJDUMP 		:= $(CROSS_COMPILE)objdump

INCDIRS 		:= imx6ul \
				   bsp/clk \
				   bsp/led \
				   bsp/delay 
				   			   
SRCDIRS			:= project \
				   bsp/clk \
				   bsp/led \
				   bsp/delay 
				   
				   
INCLUDE			:= $(patsubst %, -I %, $(INCDIRS))

SFILES			:= $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))
CFILES			:= $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))

SFILENDIR		:= $(notdir  $(SFILES))
CFILENDIR		:= $(notdir  $(CFILES))

SOBJS			:= $(patsubst %, obj/%, $(SFILENDIR:.S=.o))
COBJS			:= $(patsubst %, obj/%, $(CFILENDIR:.c=.o))
OBJS			:= $(SOBJS) $(COBJS)

VPATH			:= $(SRCDIRS)

.PHONY: clean
	
$(TARGET).bin : $(OBJS)
	$(LD) -Timx6ul.lds -o $(TARGET).elf $^
	$(OBJCOPY) -O binary -S $(TARGET).elf $@
	$(OBJDUMP) -D -m arm $(TARGET).elf &amp;gt; $(TARGET).dis

$(SOBJS) : obj/%.o : %.S
	$(CC) -Wall -nostdlib -c -O2  $(INCLUDE) -o $@ $&amp;lt;

$(COBJS) : obj/%.o : %.c
	$(CC) -Wall -nostdlib -c -O2  $(INCLUDE) -o $@ $&amp;lt;
	
clean:
	rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)

&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;细节&lt;/h2&gt;
&lt;h3&gt;工具配置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;CROSS_COMPILE 	?= arm-linux-gnueabihf-     # 交叉编译工具前缀（ARM架构）
TARGET		  	?= bsp                      # 目标程序名称，默认输出文件名为 `bsp`

CC 				:= $(CROSS_COMPILE)gcc      # C 编译器
LD				:= $(CROSS_COMPILE)ld       # 链接器
OBJCOPY 		:= $(CROSS_COMPILE)objcopy  # 二进制转换工具
OBJDUMP 		:= $(CROSS_COMPILE)objdump  # 反汇编工具
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;目录与文件管理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;INCDIRS := imx6ul \            # 头文件搜索目录列表，&quot;\&quot;为换行符
           bsp/clk \
           bsp/led \
           bsp/delay

SRCDIRS := project \           # 源码搜索目录列表，&quot;\&quot;为换行符
           bsp/clk \
           bsp/led \
           bsp/delay

INCLUDE := $(patsubst %, -I %, $(INCDIRS))  # 将 INCDIRS 转换为 gcc 的 `-I` 选项
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;patsubst&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;函数用法：&lt;code&gt;$(patsubst &amp;lt;pattern&amp;gt;,&amp;lt;replacement&amp;gt;,&amp;lt;text&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;名称：模式字符串替换函数——patsubst。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;功能：查找&lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt;中的单词（单词以“空格”、“Tab”或“回车”“换行”分隔）是否符合模式&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;，如果匹配的话，则以&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;替换。这里，&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;可以包括通配符“%”，表示任意长度的字串。如果&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;中也包含“%”，那么&lt;code&gt;&amp;lt;replacement&amp;gt;&lt;/code&gt;中的这个“%”将是&lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt;中的那个“%”所代表的字串。（可以用“\”来转义，以“%”来表示真实含义的“%”字符）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;返回：函数返回被替换过后的字符串。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;示例：&lt;code&gt;$(patsubst %.c,%.o, a.c b.c)&lt;/code&gt;，把字串“a.c b.c”符合模式[%.c]的单词替换成[%.o]，返回结果是“a.o b.o”&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;源码文件收集&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;SFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.S))  # 所有 .S 汇编文件
CFILES := $(foreach dir, $(SRCDIRS), $(wildcard $(dir)/*.c))  # 所有 .c C 文件

SFILENDIR := $(notdir $(SFILES))  # 去除路径，保留文件名（如 `start.S`）
CFILENDIR := $(notdir $(CFILES))  # 去除路径，保留文件名（如 `main.c`）

SOBJS := $(patsubst %, obj/%, $(SFILENDIR:.S=.o))  # 汇编文件对应的 .o 文件（放在 obj 目录）
COBJS := $(patsubst %, obj/%, $(CFILENDIR:.c=.o))  # C 文件对应的 .o 文件（放在 obj 目录）
OBJS  := $(SOBJS) $(COBJS)                        # 所有目标文件

VPATH := $(SRCDIRS)  # 指定源码搜索路径，Make 会在这些目录中查找依赖文件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;foreach&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;用法：&lt;code&gt;$(foreach &amp;lt;var&amp;gt;, &amp;lt;list&amp;gt;, &amp;lt;expr&amp;gt;)&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;功能：遍历 &lt;code&gt;&amp;lt;list&amp;gt;&lt;/code&gt; 中的每个元素，将元素赋值给 &lt;code&gt;&amp;lt;var&amp;gt;&lt;/code&gt;，然后展开 &lt;code&gt;&amp;lt;expr&amp;gt;&lt;/code&gt;，最终将所有展开结果合并成一个空格分隔的字符串。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;示例中的用法：遍历 &lt;code&gt;SRCDIRS&lt;/code&gt; 中的每个目录，收集所有 &lt;code&gt;.S&lt;/code&gt; 和 &lt;code&gt;.c&lt;/code&gt; 文件的完整路径。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;dir&lt;/code&gt; 依次取 &lt;code&gt;SRCDIRS&lt;/code&gt; 中的值（如 &lt;code&gt;project&lt;/code&gt;, &lt;code&gt;bsp/clk&lt;/code&gt; 等）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(wildcard $(dir)/*.S)&lt;/code&gt; 展开为 &lt;code&gt;dir&lt;/code&gt; 目录下所有 &lt;code&gt;.S&lt;/code&gt; 文件的路径（如 &lt;code&gt;project/start.S, bsp/clk/clk.S&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;最终 &lt;code&gt;SFILES&lt;/code&gt; 是所有 &lt;code&gt;.S&lt;/code&gt; 文件的路径列表，&lt;code&gt;CFILES&lt;/code&gt; 是所有 &lt;code&gt;.c&lt;/code&gt; 文件的路径列表。&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;wildcard&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;用法：&lt;code&gt;$(wildcard &amp;lt;pattern&amp;gt;)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;功能：匹配符合 &lt;code&gt;&amp;lt;pattern&amp;gt;&lt;/code&gt; 的文件路径，支持通配符 * 和 ?。&lt;/li&gt;
&lt;li&gt;示例中的用法:在 &lt;code&gt;foreach&lt;/code&gt; 循环中，为每个 &lt;code&gt;dir&lt;/code&gt; 目录生成文件列表。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;生成二进制文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$(TARGET).bin : $(OBJS)                       # 依赖所有 .o 文件
	$(LD) -Timx6ul.lds -o $(TARGET).elf $^    # 1. 链接生成 ELF 文件
	$(OBJCOPY) -O binary -S $(TARGET).elf $@  # 2. 提取二进制文件
	$(OBJDUMP) -D -m arm $(TARGET).elf &amp;gt; $(TARGET).dis  # 3. 生成反汇编文件
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;编译规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;$(SOBJS) : obj/%.o : %.S      # 编译 .S 汇编文件（需预处理的汇编）
	$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $&amp;lt;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-nostdlib&lt;/code&gt;：不链接标准库，适用于裸机程序&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-O2&lt;/code&gt;：优化等级为2&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$(INCLUDE)&lt;/code&gt;：包含头文件目录的&lt;code&gt;-I&lt;/code&gt;选项&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;$(COBJS) : obj/%.o : %.c      # 编译 .c C 文件
	$(CC) -Wall -nostdlib -c -O2 $(INCLUDE) -o $@ $&amp;lt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;清理规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;clean:
	rm -rf $(TARGET).elf $(TARGET).dis $(TARGET).bin $(COBJS) $(SOBJS)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;完整流程：&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;收集源码路径：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SFILES = project/start.S bsp/clk/clk.S&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CFILES = project/main.c bsp/clk/clk.c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;提取文件名：
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SFILENDIR = start.S clk.S&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CFILENDIR = main.c clk.c&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;生成目标文件列表
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SOBJS = obj/start.o obj/clk.o&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COBJS = obj/main.o obj/clk.o&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OBJS = obj/start.o obj/clk.o obj/main.o obj/clk.o&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;编译时
&lt;ul&gt;
&lt;li&gt;Make 根据 &lt;code&gt;VPATH&lt;/code&gt; 在 &lt;code&gt;project&lt;/code&gt; 和 &lt;code&gt;bsp/clk&lt;/code&gt; 目录中查找 &lt;code&gt;start.S&lt;/code&gt; 和 &lt;code&gt;clk.c&lt;/code&gt; 等文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>LVGL发生各种卡死现象的原因分析和排故</title><link>https://arkmostima.pages.dev/posts/lvgl%E5%8F%91%E7%94%9F%E5%90%84%E7%A7%8D%E5%8D%A1%E6%AD%BB%E7%8E%B0%E8%B1%A1%E7%9A%84%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90%E5%92%8C%E6%8E%92%E6%95%85/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/lvgl%E5%8F%91%E7%94%9F%E5%90%84%E7%A7%8D%E5%8D%A1%E6%AD%BB%E7%8E%B0%E8%B1%A1%E7%9A%84%E5%8E%9F%E5%9B%A0%E5%88%86%E6%9E%90%E5%92%8C%E6%8E%92%E6%95%85/</guid><description>LVGL是一个在使用时非常容易产生卡死故障的GUI库，大多数卡死都发生在动态阶段，包括且不仅限于： 启动并初始化第一个页面时 在多个页面间来回切换 触发组件回调时（例如button或timer） 长时间运行后</description><pubDate>Thu, 20 Feb 2025 06:59:32 GMT</pubDate><content:encoded>&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;LVGL是一个在使用时非常容易产生卡死故障的GUI库，大多数卡死都发生在动态阶段，包括且不仅限于：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;启动并初始化第一个页面时&lt;/li&gt;
&lt;li&gt;在多个页面间来回切换&lt;/li&gt;
&lt;li&gt;触发组件回调时（例如button或timer）&lt;/li&gt;
&lt;li&gt;长时间运行后&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;本文所提供的解决方案就是我自己摸索出来的，不一定适用于所有人，也不一定是最标准的办法。网上关于LVGL的教程很多，包括官方文档，但是很多方法都是不怎么规范的。官方的Github issue中也有很多奇奇怪怪的问题，哪怕是LVGL的作者也无法解决或定位错误。但是LVGL已经迭代到了V9版本，大多数因为自带库问题导致的错误已经得到修复，由LVGL本身带来的致命错误已经很少，大多数错误都来源于用户自身的不规范代码或者是用法。&lt;/p&gt;
&lt;p&gt;如果你的LVGL例程发生了卡死，首先应当按照以下步骤尝试解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;更新LVGL版本至最新。同时，确保你使用的函数在最新的LVGL中仍然可用。LVGL库函数迭代的很快，且自从v8.3之后几乎每个小版本都有大改，v9之后更是删了一大批我认为很好用的功能（比如&lt;code&gt;lv_label_recolor()&lt;/code&gt;）。如果你抄了一个旧版本的函数并在新版本的lvgl环境下使用它，可能导致问题&lt;/li&gt;
&lt;li&gt;尝试在模拟器中复现代码，确认是否是LVGL库自身的问题&lt;/li&gt;
&lt;li&gt;如果LVGL工作在OS下，尝试在裸机中复现问题；如果裸机和OS下都发生问题，说明部分代码可能存在问题；如果裸机下工作正常，大概率是线程问题导致的LVGL错误。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果尝试过上述方法之后问题还没有解决，应考虑是用户代码造成的问题。&lt;/p&gt;
&lt;h2&gt;排故&lt;/h2&gt;
&lt;p&gt;在开始前，确保在&lt;code&gt;lv_conf.h&lt;/code&gt;中启用内存监视和CPU占用监视，具体为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*1: Enable system monitor component*/
/*1: Enable system monitor component*/
#define LV_USE_SYSMON   1
#if LV_USE_SYSMON
    /*Get the idle percentage. E.g. uint32_t my_get_idle(void);*/
    #define LV_SYSMON_GET_IDLE lv_timer_get_idle

    /*1: Show CPU usage and FPS count
     * Requires `LV_USE_SYSMON = 1`*/
    #define LV_USE_PERF_MONITOR 1       /* 启用CPU性能监视 */
    #if LV_USE_PERF_MONITOR
        #define LV_USE_PERF_MONITOR_POS LV_ALIGN_BOTTOM_RIGHT

        /*0: Displays performance data on the screen, 1: Prints performance data using log.*/
        #define LV_USE_PERF_MONITOR_LOG_MODE 0
    #endif

    /*1: Show the used memory and the memory fragmentation
     * Requires `LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN`
     * Requires `LV_USE_SYSMON = 1`*/
    #define LV_USE_MEM_MONITOR 1        /* 启用内存监视 */
    #if LV_USE_MEM_MONITOR
        #define LV_USE_MEM_MONITOR_POS LV_ALIGN_BOTTOM_LEFT
    #endif

#endif /*LV_USE_SYSMON*/
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;启动时卡死、白屏、花屏&lt;/h3&gt;
&lt;p&gt;非常常见的情况。首先在初始化后移除所有的对象创建函数，仅保留一个最基本的创建空白屏幕的函数，这么做是为了排除其他对象引起的干扰因素。&lt;/p&gt;
&lt;p&gt;然后，按照下列顺序检查：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;硬件问题。&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;确保屏幕和MCU硬件连接正确。最好的方式是先不跑LVGL，而是用一个最基本的填色函数来测试屏幕是否工作正常。&lt;/li&gt;
&lt;li&gt;确保供电满足要求&lt;/li&gt;
&lt;li&gt;确保时钟源正确配置&lt;/li&gt;
&lt;/ul&gt;
&lt;ol&gt;
&lt;li&gt;检查MCU启动文件中栈和堆大小是否设置正确。 LVGL正常运行至少需要以下条件：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;Flash/ROM: &amp;gt; 64 kB for the very essential components (&amp;gt; 180 kB is recommended)

RAM:
Static RAM usage: ~2 kB depending on the used features and object types

stack: &amp;gt; 2kB (&amp;gt; 8 kB is recommended)

Dynamic data (heap): &amp;gt; 2 KB (&amp;gt; 48 kB is recommended if using several objects).
Set by LV_MEM_SIZE in lv_conf.h.

Display buffer: &amp;gt; &quot;Horizontal resolution&quot; pixels (&amp;gt; 10 &quot;Horizontal resolution&quot; is recommended)

One frame buffer in the MCU or in an external display controller
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;检查&lt;code&gt;lv_conf.h&lt;/code&gt;中内存缓存地址及大小是否设置正确：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;#if LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN
    /*Size of the memory available for `lv_malloc()` in bytes (&amp;gt;= 2kB)*/
    #define LV_MEM_SIZE (300U * 1024U)          /*[bytes]*/

    /*Size of the memory expand for `lv_malloc()` in bytes*/
    #define LV_MEM_POOL_EXPAND_SIZE 0

    /*Set an address for the memory pool instead of allocating it as a normal array. Can be in external SRAM too.*/
    #define LV_MEM_ADR (0XC0000000+1024*600*2)     /*0: unused*/
    /*Instead of an address give a memory allocator that will be called to get a memory pool for LVGL. E.g. my_malloc*/
    #if LV_MEM_ADR == 0
        #undef LV_MEM_POOL_INCLUDE
        #undef LV_MEM_POOL_ALLOC
    #endif
#endif  /*LV_USE_STDLIB_MALLOC == LV_STDLIB_BUILTIN*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这里使用的是外部SDRAM作为lvgl的显存地址，大小为300KB。如果启动时白屏，优先考虑增加&lt;code&gt;LV_MEM_SIZE&lt;/code&gt;的大小，至少为64KB。如果增加至64KB还未能解决卡死的问题，如果原本现存位置是MCU内部SRAM，考虑将显存移至外部SDRAM；如果原本是外部SDRAM，考虑移动至内部SRAM。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查&lt;code&gt;lv_port_disp.c&lt;/code&gt;和&lt;code&gt;lv_port_indev.c&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;确认以下项设置正确：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;屏幕分辨率：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#ifndef MY_DISP_HOR_RES
    #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
    #define MY_DISP_HOR_RES    1024
#endif

#ifndef MY_DISP_VER_RES
    #warning Please define or replace the macro MY_DISP_VER_RES with the actual screen height, default value 240 is used for now.
    #define MY_DISP_VER_RES    600
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;屏幕分辨率如果比实际屏幕大，会导致花屏；如果比实际小，可能导致显示错位。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;disp_flush()&lt;/code&gt;是否在初始化后被正确调用&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;颜色深度是否选择正确，lvgl默认为RGB565&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;是否选择了正确的输入设备，没有用到的输入设备所对应的初始化代码要注释掉。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果LVGL初始化正确并进入了空白屏幕，那么至少屏幕左下角和右下角会分别显示内存监视和CPU监视的小窗口，且CPU监视窗口的数字应当是在动态变化的。&lt;/p&gt;
&lt;h3&gt;切换页面时卡死或花屏&lt;/h3&gt;
&lt;p&gt;通常认为，有两种切换屏幕的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过&lt;code&gt;lv_obj_clean&lt;/code&gt;或&lt;code&gt;lv_obj_del&lt;/code&gt;删除旧页面，然后&lt;code&gt;lv_scr_load&lt;/code&gt;创建新页面。比如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;        lv_obj_t *act_scr = lv_scr_act();
        lv_obj_del(act_scr);
        create_main_scr();        
        lv_scr_load(main_scr);
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;通过向页面添加或移除&lt;code&gt;LV_OBJ_FLAG_HIDDEN&lt;/code&gt;来控制页面的可视化，这么做需要事先一次性创建完所有可能需要显示的页面并添加flag，比如：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;        lv_obj_add_flag(main_screen, LV_OBJ_FLAG_HIDDEN); /* 隐藏页面 */
        lv_obj_clear_flag(main_screen, LV_OBJ_FLAG_HIDDEN); /* 显示页面 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;建议&lt;/em&gt;：&lt;strong&gt;不要&lt;/strong&gt;使用第二种通过控制&lt;code&gt;FLAG_HIDDEN&lt;/code&gt;来实现切换页面的方法，除非你真的确认第一种方法没法使用。因为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始化时一次性创建大量页面及其子类对象会极大的增加CPU和内存负担。应尽可能降低同时操作多个对象的可能性。&lt;/li&gt;
&lt;li&gt;浪费内存，造成内存泄漏，页面管理混乱。&lt;/li&gt;
&lt;li&gt;在LVGL v9及以上版本使用时，大概率导致花屏（很多例子），推测是多个对象叠加带来的内存问题。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果修改为第一种方法后还出现错误，参照以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;确保LVGL版本为最新。LVGL v9中&lt;code&gt;lv_scr_load()&lt;/code&gt;函数的执行步骤是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;获取当前活动屏幕和其关联的显示器对象&lt;/li&gt;
&lt;li&gt;判断当前屏幕是否为待切换的目标屏幕，如果是，退出函数&lt;/li&gt;
&lt;li&gt;判断是否有切换动画正在执行，如果有，立即切换至目标屏幕&lt;/li&gt;
&lt;li&gt;切换后，删除旧屏幕，更新&lt;code&gt;acr_scr&lt;/code&gt;指针至目标屏幕&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这里的切换逻辑已经写的非常合理了，如果还是有错误：&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保使用&lt;code&gt;lv_obj_del()&lt;/code&gt;而不是&lt;code&gt;lv_obj_clean()&lt;/code&gt;来删除旧屏幕。两个函数的功能有区别，&lt;code&gt;lv_obj_del()&lt;/code&gt;会立即删除对象本身和其子项，而&lt;code&gt;lv_obj_clean()&lt;/code&gt;仅删除目标对象的子项，而不删除目标对象本身，这可能造成内存泄漏。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;调换&lt;code&gt;lv_obj_del()&lt;/code&gt;和&lt;code&gt;lv_scr_load()&lt;/code&gt;的位置，即先创建新屏幕，再删除旧屏幕。之前有Github issue报告称在创建新屏幕前就删除旧屏幕可能导致内存池问题，lvgl会访问空指针导致hardfault。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保旧屏幕（包括其子项）和新屏幕（包括其子项）之间的对象依赖关系正确。具体一点来说：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;新屏幕必须已经被创建（父对象必须实际存在且为静态）&lt;/li&gt;
&lt;li&gt;新屏幕中的子项不能指向、包含或引用不存在的对象。这么说的意思是，如果你是在切换屏幕时才创建新屏幕中的对象，那么这个对象在切换屏幕前是不存在的。如果子项试图访问一个并不实际存在的对象，即访问一个空指针，那么LVGL就会卡死。举个例子：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static void btn_to_main_page_cb(lv_event_t *e){
if (lv_event_get_code(e) == LV_EVENT_CLICKED){
    lv_obj_t *act_scr = lv_scr_act();
    create_main_scr();        
    lv_scr_load(main_scr);  /* 切换至main_scr() *、`
    lv_obj_del(act_scr);
}
}

static void create_main_scr(){
    /* ..... */
    lv_label_set_text_fmt(Example_label);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个例子里，我即将切换到的目标屏幕中包含一个对&lt;code&gt;Example_label&lt;/code&gt;的操作，而&lt;code&gt;Example_label&lt;/code&gt;可能是在其他屏幕初始化函数中被创建的，那么必须确保在切换前&lt;code&gt;Example_label&lt;/code&gt;就已经存在。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果启用了定时器(lv_timer)，检查定时器创建位置和回调执行位置。假设&lt;code&gt;lv_timer_1&lt;/code&gt;用于实时更新&lt;code&gt;test_scr&lt;/code&gt;中的某个label组件，那么&lt;code&gt;lv_timer_1&lt;/code&gt;应当只在进入&lt;code&gt;test_scr&lt;/code&gt;后被创建，如果其他屏幕没有用到&lt;code&gt;lv_timer_1&lt;/code&gt;，那么&lt;code&gt;lv_timer_1&lt;/code&gt;在退出&lt;code&gt;test_scr&lt;/code&gt;前就应当被销毁（先销毁定时器，再删除旧屏幕），尤其是当&lt;code&gt;lv_timer_1&lt;/code&gt;的回调函数中包括对&lt;code&gt;test_scr&lt;/code&gt;中子对象的引用、指向和删除操作时，因为一旦退出&lt;code&gt;test_scr&lt;/code&gt;，回调函数所指向的对象就不再存在（被删除），即指向空指针，LVGL会立即卡死。&lt;/p&gt;
&lt;p&gt;推荐使用这种方式来管理定时器的创建：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void lvgl_create_timers_1(void)
{
// 如果存在旧的定时器对象
if (timer_1 != NULL) {
    lv_timer_del(timer_1);  // 删除旧的定时器
    timer_1 = NULL;         // 清空指针
}

// 创建新的定时器
timer_1 = lv_timer_create(lvgl_timer_1_cb, 500, NULL);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;如果到这里还没有解决问题，应使用IDE的debug功能进行单步调试，定位故障来源。下面是几个例子：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;函数卡死在&lt;code&gt;lv_tlsf.c&lt;/code&gt;中的某个函数：内存管理出现问题，极大可能是访问或释放了不存在的内存（空指针），确保正确处理了对象之间的依赖关系。&lt;/li&gt;
&lt;li&gt;函数卡死在&lt;code&gt;delay&lt;/code&gt;函数：函数停在这里是正常现象，这里的卡死指的是一直在delay中循环而不跳出，考虑时钟问题，可能的情况下降低屏幕时钟频率。&lt;/li&gt;
&lt;li&gt;函数跳转至&lt;code&gt;hardfault_handler()&lt;/code&gt;：非LVGL自身原因带来的问题，必定是用户的代码产生了错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;触发组件回调时卡死&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;不要在回调中使用&lt;code&gt;while&lt;/code&gt;、&lt;code&gt;for&lt;/code&gt;或长时间的延时&lt;/li&gt;
&lt;li&gt;不要在回调函数中访问已经被删除或无效的对象，参加上一小节中的第5条。这一条看似不起眼，但很多人都不会意识到这个问题&lt;/li&gt;
&lt;li&gt;LVGL不是线程安全的。如果在OS中使用LVGL，考虑为回调函数添加标志位或互斥锁，避免竞态条件&lt;/li&gt;
&lt;li&gt;尽可能地优化代码，不要嵌套过多的API或一次性创建大量对象，github issue中有因为这么干而造成栈溢出的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;长时间运行后卡死&lt;/h3&gt;
&lt;p&gt;99%是内存问题。确保创建新对象后删除旧对象并释放内存。&lt;/p&gt;
&lt;h2&gt;后话&lt;/h2&gt;
&lt;p&gt;最后，一些好习惯，能够高效的进行debug：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对LVGL申请的关键内存添加&lt;code&gt;static&lt;/code&gt;，尤其是各种缓冲区&lt;/li&gt;
&lt;li&gt;发生错误时，从父类到子类、从大到小、从前到后依次定位故障来源，着重关注“动态阶段”，也就是创建对象、删除对象、刷新对象等等的这些操作&lt;/li&gt;
&lt;li&gt;善用搜索功能和官方文档&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>C++类构造函数初始化列表</title><link>https://arkmostima.pages.dev/posts/c-%E7%B1%BB%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/c-%E7%B1%BB%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E5%88%9D%E5%A7%8B%E5%8C%96%E5%88%97%E8%A1%A8/</guid><description>使用初始化列表的构造函数显式的初始化类的成员：</description><pubDate>Tue, 18 Feb 2025 12:46:41 GMT</pubDate><content:encoded>&lt;p&gt;使用初始化列表的构造函数显式的初始化类的成员：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class example{
public:
    int a;
    float b;

    example(): a(0), b(8.8)
    {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没使用初始化列表的构造函数是对类的成员赋值，并没有进行显式的初始化:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class example{
public:
    int a;
    float b;

    example()
    {
        a = 0;
        b = 8.8;
    }
}
&lt;/code&gt;&lt;/pre&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;strong&gt;const成员或引用类型的成员&lt;/strong&gt;。因为 const 对象或引用类型只能初始化，不能对他们赋值。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;初始化数据成员与对数据成员赋值的含义是什么？有什么区别？&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;对于内置数据类型和复合类型（指针，引用），初始化过程在成员初始化列表和构造函数体内进行，在性能和结果上都是一样的；而对于用户定义类型（类类型），初始化结果相同，但是性能上存在很大的差别。因为类类型的数据成员对象在进入函数体前已经构造完成，也就是说在成员初始化列表处进行构造对象的工作，调用构造函数，在进入函数体之后，进行的是对已经构造好的类对象的赋值，又调用个拷贝赋值操作符才能完成（如果并未提供，则使用编译器提供的默认按成员赋值行为）。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：C++初始化类成员时，按照&lt;strong&gt;声明的顺序&lt;/strong&gt;初始化，而不是按照出现在初始化列表中的顺序。这意味着，即使你在初始化列表中改变了成员的初始化顺序，实际初始化时仍然会按照声明顺序进行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    int a;
    int b;

    // 构造函数
    MyClass(int x, int y) : b(x), a(y) {
        // 尽管初始化列表中 b 在 a 前面，但实际初始化顺序是 a 先于 b
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然这会带来潜在问题，比如如果初始化顺序依赖于其他成员的值，可能会导致未定义行为。例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    int a;
    int b;

    // 构造函数
    MyClass(int x) : b(x), a(b + 1) {
        // 这里 a 依赖于 b 的值，但由于 a 先初始化，b 还未初始化，导致未定义行为
    }
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要解决这个问题，正确做法是确保初始化顺序不依赖于其他成员的值，或者调整成员的声明顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;class MyClass {
public:
    int b;
    int a;

    // 构造函数
    MyClass(int x) : b(x), a(b + 1) {
        // 现在 b 先于 a 初始化，a 可以正确使用 b 的值
    }
};
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>适用于Cortex-A的常见GNU汇编语法</title><link>https://arkmostima.pages.dev/posts/%E9%80%82%E7%94%A8%E4%BA%8Ecortex-a%E7%9A%84%E5%B8%B8%E8%A7%81gnu%E6%B1%87%E7%BC%96%E8%AF%AD%E6%B3%95/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E9%80%82%E7%94%A8%E4%BA%8Ecortex-a%E7%9A%84%E5%B8%B8%E8%A7%81gnu%E6%B1%87%E7%BC%96%E8%AF%AD%E6%B3%95/</guid><description>GNU 汇编语法适用于所有的架构，并不是 ARM 独享的，GNU 汇编由一系列的语句组成，每行一条语句，每条语句有三个可选部分：label: instruction @ comment label：标号，表示地址位置 instructi…</description><pubDate>Tue, 18 Feb 2025 08:15:02 GMT</pubDate><content:encoded>&lt;h2&gt;语句&lt;/h2&gt;
&lt;p&gt;GNU 汇编语法适用于所有的架构，并不是 ARM 独享的，GNU 汇编由一系列的语句组成，每行一条语句，每条语句有三个可选部分：&lt;code&gt;label: instruction @ comment&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;label&lt;/code&gt;：标号，表示地址位置&lt;/li&gt;
&lt;li&gt;&lt;code&gt;instruction&lt;/code&gt;：指令，汇编指令或伪指令&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@ comment&lt;/code&gt;：注释&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;ARM中的指令、伪指令、伪操作、寄存器名等可以全部使用大写，也可以全部使用小写，但是不能大小写混用。&lt;/p&gt;
&lt;h2&gt;伪操作（伪指令）&lt;/h2&gt;
&lt;p&gt;用户可通过&lt;code&gt;.section&lt;/code&gt;伪操作来定义一个段，汇编系统预定义了一些段名，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.text&lt;/code&gt;：代码段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.data&lt;/code&gt;：初始化的数据段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.bss&lt;/code&gt;：未初始化的数据段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.rodata&lt;/code&gt;：只读数据段&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;.section&lt;/code&gt;也可用于自定义段，每个段以段名开始，以下一段名或文件结尾结束。如&lt;code&gt;.section .testsection @顶一个testsection段&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;汇编程序的默认入口标号是&lt;code&gt;_start&lt;/code&gt;，不过我们也可以在链接脚本中使用&lt;code&gt;ENTRY&lt;/code&gt;来指明其它的入口点，下面的代码就是使用&lt;code&gt;_start&lt;/code&gt;作为入口标号：
``
.global _start&lt;/p&gt;
&lt;p&gt;_start:
ldr r0, =0x12 @r0=0x12
``
代码中&lt;code&gt;.global&lt;/code&gt;是伪操作，表示&lt;code&gt;_start&lt;/code&gt;是一个全局标号（性质类似于C语言中的全局变量）。&lt;/p&gt;
&lt;p&gt;常见的伪指令有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.global&lt;/code&gt;：声明全局符号，如&lt;code&gt;.global main @声明main为全局符号&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.section&lt;/code&gt;：定义段&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.word&lt;/code&gt;：定义字数据&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.asciz&lt;/code&gt;：定义以null结尾的字符串，如&lt;code&gt;.asciz &quot;Hello World&quot;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.byte&lt;/code&gt;：定义单字节数据，比如&lt;code&gt;.byte 0x12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.short&lt;/code&gt;：定义双字节数据，比如&lt;code&gt;.short 0x1234&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.long&lt;/code&gt;：定义一个4字节数据，比如&lt;code&gt;.long 0x12345678&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.equ&lt;/code&gt;：赋值，如&lt;code&gt;.equ num, 0x12&lt;/code&gt;表示&lt;code&gt;num=0x12&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.align&lt;/code&gt;：数据字节对齐，&lt;code&gt;.align 4&lt;/code&gt;表示4字节对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.end&lt;/code&gt;：表示源文件结束&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;处理器内部数据传输指令&lt;/h2&gt;
&lt;p&gt;最常见的一类指令，包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将数据从一个寄存器传递到另外一个寄存器&lt;/li&gt;
&lt;li&gt;将数据从一个寄存器传递到特殊寄存器，如 CPSR 和 SPSR 寄存器&lt;/li&gt;
&lt;li&gt;将立即数传递到寄存器&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;MOV&lt;/h3&gt;
&lt;p&gt;将数据从一个寄存器拷贝到另一个寄存器，或将一个立即数传递到寄存器中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOV R0, R1      @寄存器R1中的数据传递给R0
MOV R0, #0x12   @立即数0x12传递给R0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MRS&lt;/h3&gt;
&lt;p&gt;将特殊寄存器（如CPSR和SPSR）中的数据传递给通用寄存器。&lt;em&gt;注意&lt;/em&gt;：读取特殊寄存器数据时只能使用&lt;code&gt;MRS&lt;/code&gt;指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MRS R0, CPSR
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;MSR&lt;/h3&gt;
&lt;p&gt;将普通寄存器的数据传递至特殊寄存器，即写入特殊寄存器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MSR CPSR, R0
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;存储器访问指令&lt;/h2&gt;
&lt;p&gt;ARM无法直接访问存储器（如RAM）。以I.MX6UL为例，其寄存器为RAM类型，汇编配置寄存器时需借助存储器访问指令，将待配置的值写入到Rx(x=0~12)寄存器中，然后借助存储器访问指令将Rx中的数据写入之MX6UL的寄存器中，读取亦同。&lt;/p&gt;
&lt;h3&gt;LDR&lt;/h3&gt;
&lt;p&gt;用于从存储器加载数据至寄存器Rx中，也可以将一个立即数加载到寄存器Rx中。加载立即数时，使用&lt;code&gt;=&lt;/code&gt;而不是&lt;code&gt;#&lt;/code&gt;，如&lt;code&gt;=0xFFFFFFFF&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在嵌入式开发中，LDR最常用的就是读取CPU的寄存器值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDR R0, =0x0209C004     @将寄存器地址0x0209C004加载到R0中，准备寻址
LDR R1, [R0]            @读取地址0x0209C004中的值到R1中
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;STR&lt;/h3&gt;
&lt;p&gt;与LDR相反，STR用于将数据写入到存储器中。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDR R0, =0x0209C004     @寄存器地址0x0209C004写入R0
LDR R1, =0x20000002     @待写入值0x20000002写入R1
STR R1, [R0]            @R1中的值写入R0中所保存的地址中
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;跳转指令&lt;/h2&gt;
&lt;h3&gt;B指令&lt;/h3&gt;
&lt;p&gt;B指令将PC寄存器的值设置为目标跳转地址，一旦执行B指令，ARM处理器就会立即跳转至指定的目标地址。如果待调用函数不会再返回到原来的执行处，就可以使用B指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_start:
    ldr sp, =0x80200000     @设置栈指针
    b main                  @跳转到main函数
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BL指令&lt;/h3&gt;
&lt;p&gt;相比B指令，BL指令在跳转之前会在寄存器 LR(R14) 中保存当前 PC 寄存器值，所以可以通过将 LR 寄存器中的值重新加载到 PC 中来继续从跳转之前的代码处运行，这是子程序调用一个基本但常用的手段，常见于各种中断服务程序。&lt;/p&gt;
&lt;p&gt;Cortex-A 处理器的 irq 中断服务函数都是汇编写的，主要用汇编来实现现场的保护和恢复、获取中断号等。但是具体的中断处理过程都是 C 函数，所以就会存在汇编中调用 C 函数的问题。而且当 C 语言版本的中断处理函数执行完成以后是需要返回到 irq 汇编中断服务函数，因为还要处理其他的工作，一般是恢复现场。这个时候就不能直接使用 B 指令了，因为 B 指令一旦跳转就再也不会回来了，这个时候要使用 BL 指令，示例代码如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;push {r0, r1}           @入栈，保存R0, R1
cps #0x13               @进入SVC模式，CPS指令用于直接修改CPSR寄存器的M[4:0]，让CPU进入不同的模式

bl system_irqhandler    @加载C语言中断处理函数至R2寄存器

cps #0x12               @进入IRQ模式
pop {r0, r1}            @恢复现场
str r0, {r1, 0#10}      @中断执行完成，写EOIR
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;ARM中常见的寄存器&lt;/h2&gt;
&lt;p&gt;几个ARM中常用的寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IP寄存器：内部程序调用暂存寄存器，子程序的连接text段中常使用该规则&lt;/li&gt;
&lt;li&gt;SP寄存器：栈指针寄存器，用于存储当前栈顶地址。程序执行过程中，栈是用来存储临时变量、函数调用返回地址等数据的重要数据结构，SP寄存器的值会随着栈的变化而变化&lt;/li&gt;
&lt;li&gt;LR寄存器：连接寄存器，程序跳转（子程序调用，中断跳转）后，arm自动在该寄存器中存入原程序（未跳转）的下一条指令的地址，也叫函数调用返回地址。当一个函数被调用时，LR寄存器会存储调用该函数的下一条指令的地址，当函数执行完毕后，程序会跳转到LR寄存器中存储的地址继续执行。&lt;/li&gt;
&lt;li&gt;PC寄存器：程序计数器，保存的是当前正在取指的指令的地址（arm采用2级流水线，因此是当前正在执行指令的地址+8）。PC寄存器是ARM中的程序计数器，用于存储下一条将要执行的指令的地址。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此外，还有两个比较重要的状态寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPSR：程序状态寄存器，在任何处理器模式下被访问。它包含了条件标志位、中断禁止位、当前处理器模式标志以及其他的一些控制和状态位。CPSR在用户级编程时用于存储条件码。&lt;/li&gt;
&lt;li&gt;SPSR：程序状态保存寄存器，每一种处理器模式下都有一个状态寄存器SPSR,SPSR用于保存CPSR的状态，以便异常返回后恢复异常发生时的工作状态。当特定的异常中断发生时，这个寄存器用于存放当前程序状态寄存器的内容。在异常中断退出时，可以用SPSR来恢复CPSR。由于用户模式和系统模式不是异常中断模式，所以他没有SPSR。当用户在用户模式或系统模式访问SPSR，将产生不可预知的后果。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;二者常用于MRS或MSR指令,用于spsr中的值转移到寄存器或把寄存器的内容加载到spsr中，如:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mrs r0, spsr                /* 读取spsr寄存器 */
msr spsr_cxsf, r0            /* 恢复spsr */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;ARM进入异常模式后，SPSR自动保存进入异常前的CPSR的值，以便异常返回后恢复异常发生时的工作状态。&lt;/p&gt;
&lt;h2&gt;LED点灯示例&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;.global _start  /* 全局标号 */

/*
 * 描述：	_start函数，程序从此函数开始执行此函数完成时钟使能、
 *		  GPIO初始化、最终控制GPIO输出低电平来点亮LED灯。
 */
_start:
	/* 例程代码 */
	/* 1、使能所有时钟 */
	ldr r0, =0X020C4068 	/* CCGR0 */
	ldr r1, =0XFFFFFFFF  
	str r1, [r0]		
	
	ldr r0, =0X020C406C  	/* CCGR1 */
	str r1, [r0]

	ldr r0, =0X020C4070  	/* CCGR2 */
	str r1, [r0]
	
	ldr r0, =0X020C4074  	/* CCGR3 */
	str r1, [r0]
	
	ldr r0, =0X020C4078  	/* CCGR4 */
	str r1, [r0]
	
	ldr r0, =0X020C407C  	/* CCGR5 */
	str r1, [r0]
	
	ldr r0, =0X020C4080  	/* CCGR6 */
	str r1, [r0]
	

	/* 2、设置GPIO1_IO03复用为GPIO1_IO03 */
	ldr r0, =0X020E0068	/* 将寄存器SW_MUX_GPIO1_IO03_BASE加载到r0中 */
	ldr r1, =0X5		/* 设置寄存器SW_MUX_GPIO1_IO03_BASE的MUX_MODE为5 */
	str r1,[r0]

	/* 3、配置GPIO1_IO03的IO属性	
	 *bit 16:0 HYS关闭
	 *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
    ldr r0, =0X020E02F4	/*寄存器SW_PAD_GPIO1_IO03_BASE */
    ldr r1, =0X10B0
    str r1,[r0]

	/* 4、设置GPIO1_IO03为输出 */
    ldr r0, =0X0209C004	/*寄存器GPIO1_GDIR */
    ldr r1, =0X0000008		
    str r1,[r0]

	/* 5、打开LED0
	 * 设置GPIO1_IO03输出低电平
	 */
	ldr r0, =0X0209C000	/*寄存器GPIO1_DR */
   ldr r1, =0		
   str r1,[r0]

/*
 * 描述：	loop死循环
 */
loop:
	b loop 				
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>意外修改/usr/sudo所属组和权限后的恢复方法</title><link>https://arkmostima.pages.dev/posts/%E6%84%8F%E5%A4%96%E4%BF%AE%E6%94%B9-usr-sudo%E6%89%80%E5%B1%9E%E7%BB%84%E5%92%8C%E6%9D%83%E9%99%90%E5%90%8E%E7%9A%84%E6%81%A2%E5%A4%8D%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%84%8F%E5%A4%96%E4%BF%AE%E6%94%B9-usr-sudo%E6%89%80%E5%B1%9E%E7%BB%84%E5%92%8C%E6%9D%83%E9%99%90%E5%90%8E%E7%9A%84%E6%81%A2%E5%A4%8D%E6%96%B9%E6%B3%95/</guid><description>使用arm-linux-gnueabihf-gcc交叉编译时，提示权限不足，尝试修改目标编译文件和sudo权限，意外对/usr/bin使用了sudo chmod 777 /usr/bin命令。</description><pubDate>Tue, 18 Feb 2025 01:05:09 GMT</pubDate><content:encoded>&lt;h2&gt;问题来源&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;arm-linux-gnueabihf-gcc&lt;/code&gt;交叉编译时，提示权限不足，尝试修改目标编译文件和sudo权限，意外对/usr/bin使用了&lt;code&gt;sudo chmod 777 /usr/bin&lt;/code&gt;命令。&lt;/p&gt;
&lt;h2&gt;Solution&lt;/h2&gt;
&lt;p&gt;在无法获得root用户名和密码的情况下，参考一下方式解决：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;重启电脑按住Shift不放进入grub，选择Ubuntu Advanced Options&lt;/li&gt;
&lt;li&gt;选择recovery mode，进入恢复模式&lt;/li&gt;
&lt;li&gt;Recovery menu中选择root，回车&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chown root:root   /usr/bin/sudo&lt;/code&gt;，&lt;code&gt;chmod 4755  /usr/bin/sudo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chown root:root   /usr/lib/sudo/sudoers.so&lt;/code&gt;，&lt;code&gt;chmod 4755  /usr/lib/sudo/sudoers.so&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reboot&lt;/code&gt;，重新进入系统&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;谨慎对待Linux中重要文件的权限组和所属用户组问题。&lt;/p&gt;
</content:encoded></item><item><title>Cpp中的虚函数与纯虚函数</title><link>https://arkmostima.pages.dev/posts/cpp%E4%B8%AD%E7%9A%84%E8%99%9A%E5%87%BD%E6%95%B0%E4%B8%8E%E7%BA%AF%E8%99%9A%E5%87%BD%E6%95%B0/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/cpp%E4%B8%AD%E7%9A%84%E8%99%9A%E5%87%BD%E6%95%B0%E4%B8%8E%E7%BA%AF%E8%99%9A%E5%87%BD%E6%95%B0/</guid><description>虚函数在基类中使用virtual关键字声明成员函数，并允许子类重写该函数，以提供特定于子类的实现。通俗一点的讲： 如果子类没有重写该函数（即提供虚函数的实现），将会自动调用基类的缺省虚函数实现 如果子类重写了该函数，则调用重写后的实现</description><pubDate>Sat, 15 Feb 2025 06:36:08 GMT</pubDate><content:encoded>&lt;h2&gt;虚函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
public:
	virtual void speak() {
		std::cout &amp;lt;&amp;lt; &quot;Animal speaks&quot; &amp;lt;&amp;lt; std::endl;
	}
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虚函数在基类中使用&lt;code&gt;virtual&lt;/code&gt;关键字声明成员函数，并允许子类重写该函数，以提供特定于子类的实现。通俗一点的讲：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果子类没有重写该函数（即提供虚函数的实现），将会自动调用基类的缺省虚函数实现&lt;/li&gt;
&lt;li&gt;如果子类重写了该函数，则调用重写后的实现&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建虚函数时，虚函数所在类在申请内存时会同时生成虚函数指针&lt;code&gt;vptr&lt;/code&gt;，该指针指向“虚函数表”&lt;code&gt;vtbl&lt;/code&gt;，表中数据为函数指针，存储了对应虚函数具体实现所对应的位置。&lt;/p&gt;
&lt;p&gt;因此，虚函数的实现过程是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;通过对象内存中的虚函数指针&lt;code&gt;vptr&lt;/code&gt;找到虚函数表&lt;code&gt;vtbl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过&lt;code&gt;vtbl&lt;/code&gt;中的函数指针找到对应虚函数的实现区域并进行调用&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

class Animal {
public:
    //声明虚函数
	virtual void speak() {
		std::cout &amp;lt;&amp;lt; &quot;Animal speaks&quot; &amp;lt;&amp;lt; std::endl;
	}
};

class Cat : public Animal {
public:
    //重写基类中声明的虚函数
	void speak() override {
		std::cout &amp;lt;&amp;lt; &quot;Meow&quot; &amp;lt;&amp;lt; std::endl;
	}
};

class Dog : public Animal {
public:
    //重写基类中声明的虚函数
	void speak() override {
		std::cout &amp;lt;&amp;lt; &quot;Bark&quot; &amp;lt;&amp;lt; std::endl;
	}
};

int main() {
	Animal* cat = new Cat();
	cat-&amp;gt;speak();   //这里的speak()来源是Cat类，而非Animal类

	Animal* dog = new Dog();
	dog-&amp;gt;speak();   //这里的speak()来源是Dog类，而非Animal类

	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由于&lt;code&gt;speak()&lt;/code&gt;为虚函数，&lt;code&gt;cat-&amp;gt;speak()&lt;/code&gt;会调用&lt;code&gt;Cat::speak()&lt;/code&gt;而非&lt;code&gt;Animal::speak()&lt;/code&gt;。因为虚函数是动态绑定（运行时多态）的，其根据对象的实际类型（Cat）来决定调用哪个版本的&lt;code&gt;speak()&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;纯虚函数&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;class Animal {
public:
	virtual void isalive() = 0; //纯虚函数
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在很多情况下，基类本身生成对象是不合情理的。例如，动物作为一个基类可以派生出老虎、孔雀等子类，但动物本身生成对象（“一个动物”是什么？）明显不合常理。而针对每种动物的方法又有所不同，此时需要使用多态特性，也就需要在基类中定义虚函数。&lt;/p&gt;
&lt;p&gt;纯虚函数是在基类中声明的虚函数，它要求任何派生类都要定义自己的实现方法，以实现多态性。实现了纯虚函数的子类，该纯虚函数在子类中就变成了虚函数。&lt;/p&gt;
&lt;p&gt;定义纯虚函数是为了实现一个&lt;strong&gt;接口&lt;/strong&gt;，用来规范派生类的行为，也即规范继承这个类的程序员必须实现这个函数。派生类仅仅只是继承函数的接口。纯虚函数的意义在于，让所有的类对象（主要是派生类对象）都可以执行纯虚函数的动作，但基类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者，“你必须提供一个纯虚函数的实现，但我不知道你会怎样实现它”。对于动物这个基类而言，它必须是某一个种类，可以是鸟、狗、猫，但不能什么都不是。这个“种类”就可以作为纯虚函数在基类&lt;code&gt;Animal&lt;/code&gt;中声明。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：含有纯虚函数的类称之为&lt;strong&gt;抽象类&lt;/strong&gt;，它不能生成对象（创建实例），只能创建它的派生类的实例。抽象类是一种特殊的类，它是为了抽象和设计的目的为建立的，它处于继承层次结构的较上层。抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中，由它来为派生类提供一个公共的根，派生类将具体实现在其基类中作为接口的操作。&lt;/p&gt;
&lt;p&gt;抽象类只能作为基类来使用，其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数，而只是继承基类的纯虚函数，则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现，则该派生类就不再是抽象类了，它是一个可以建立对象的具体的类。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;iostream&amp;gt;

class Animal {
public:
	virtual void speak() {
		std::cout &amp;lt;&amp;lt; &quot;Animal speaks&quot; &amp;lt;&amp;lt; std::endl;
	}

    //一个动物要么是活的，要么是死的，子类中必须定义这个属性，因此是纯虚函数
	virtual void isalive() = 0;
};

class Cat : public Animal {
public:
	void speak() override {
		std::cout &amp;lt;&amp;lt; &quot;Meow&quot; &amp;lt;&amp;lt; std::endl;
	}

    //重写基类Animal中的isalive()纯虚函数
	void isalive() override {
		std::cout &amp;lt;&amp;lt; &quot;Cat is alive&quot; &amp;lt;&amp;lt; std::endl;
	}
};

class Dog : public Animal {
public:
	void speak() override {
		std::cout &amp;lt;&amp;lt; &quot;Bark&quot; &amp;lt;&amp;lt; std::endl;
	}

    //重写基类Animal中的isalive()纯虚函数
	void isalive() override {
		std::cout &amp;lt;&amp;lt; &quot;Dog is alive&quot; &amp;lt;&amp;lt; std::endl;
	}
};

int main() {
	Animal* cat = new Cat();
	cat-&amp;gt;speak();
	cat-&amp;gt;isalive();

	Animal* dog = new Dog();
	dog-&amp;gt;speak();
	dog-&amp;gt;isalive();

	return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果这里使用&lt;code&gt;Animal* animal = new Animal()&lt;/code&gt;，编译器将报错，因为&lt;code&gt;Animal&lt;/code&gt;包含纯虚函数，已经是一个抽象类。&lt;/p&gt;
</content:encoded></item><item><title>LVGL9.2移植至正点原子阿波罗STM32F429</title><link>https://arkmostima.pages.dev/posts/lvgl92%E7%A7%BB%E6%A4%8D%E8%87%B3%E6%AD%A3%E7%82%B9%E5%8E%9F%E5%AD%90%E9%98%BF%E6%B3%A2%E7%BD%97stm32f429/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/lvgl92%E7%A7%BB%E6%A4%8D%E8%87%B3%E6%AD%A3%E7%82%B9%E5%8E%9F%E5%AD%90%E9%98%BF%E6%B3%A2%E7%BD%97stm32f429/</guid><description>参考文献：LVGL 移植到 STM32 通法 (https://www.cnblogs.com/Huae/p/18621614) 工程文件：Github</description><pubDate>Fri, 10 Jan 2025 09:48:39 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;参考文献&lt;/em&gt;：LVGL 移植到 STM32 通法 (https://www.cnblogs.com/Huae/p/18621614)
工程文件：&lt;a href=&quot;https://github.com/AkiChen891/LVGL_v9_blank&quot;&gt;Github&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;前言&lt;/h2&gt;
&lt;p&gt;LVGL（Light and Versatile Graphics Library）是一个开源的图形库，旨在为嵌入式系统提供高效、灵活的图形用户界面（GUI）解决方案。它具有小巧的内存占用和高性能的渲染能力，支持多种硬件平台，包括单片机、DSP 和 ARM 处理器等。LVGL 支持丰富的控件（如按钮、标签、图标、滚动条等），并提供强大的主题、样式和动画功能，使得开发者能够轻松设计出美观且响应迅速的界面。此外，LVGL 还支持触摸屏、图形加速（如 DMA2D）、多线程等特性，非常适合用于嵌入式应用程序，如智能家居、工业控制、医疗设备等领域。&lt;/p&gt;
&lt;p&gt;本文将介绍一下最新的LVGL9.2在正点原子阿波罗STMF429IGT6开发板上的移植。正点原子自己的移植教程还是几年前的8.2版本，LVGL在这个版本之后对很多API进行了优化和裁剪，导致正点原子的教程现在已经不是很好用了。但是万变不离其宗，LVGL的主架构没有变，只需要一点细微的改变就可以进行移植。&lt;/p&gt;
&lt;p&gt;我使用的屏幕是正点原子7寸RGB屏，分辨率为1024 * 600.编译器为IAR EW 9.6&lt;/p&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;LVGL9.2版本源码&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LVGL官方的文档&lt;/strong&gt;：&lt;a href=&quot;https://docs.lvgl.io/master/&quot;&gt;点击跳转&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我个人认为LVGL官方的文档非常重要，而且它甚至是有完整的官方授权的&lt;a href=&quot;https://lvgl.100ask.net/master/index.html&quot;&gt;中文版本&lt;/a&gt;的。大多数东西都可以在上面找到。&lt;/p&gt;
&lt;h2&gt;准备工作&lt;/h2&gt;
&lt;h3&gt;拷贝外设驱动&lt;/h3&gt;
&lt;p&gt;将所有和显示屏、触摸屏、SDRAM等有关的外设驱动加入工程。换句话说，你的工程需要做到在没有LVGL的环境下也能够通过最基本的显示函数在屏幕上显示东西的状态。如果你的工程在没有LVGL的环境下都没法通过正点原子例程里提供的函数显示东西，那显然是没有办法继续的。&lt;/p&gt;
&lt;h3&gt;拷贝LVGL库至工程文件夹&lt;/h3&gt;
&lt;p&gt;LVGL下载并解压之后，文件夹根目录内会包含很多文件夹及文件。在不需要demo或例程的情况下，我们只需要用到其中的这些文件/文件夹：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;examples文件夹中的porting文件夹&lt;/li&gt;
&lt;li&gt;src文件夹&lt;/li&gt;
&lt;li&gt;lv_conf_template.h&lt;/li&gt;
&lt;li&gt;lvgl.h&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在工程文件夹根目录创建一个lvgls文件夹：
&lt;img src=&quot;/images/lvgl/lvgl1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;然后：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;创建一个子文件夹，名为lvgl&lt;/li&gt;
&lt;li&gt;创建一个子文件夹，名为lvgl_app&lt;/li&gt;
&lt;li&gt;将之前提到的lv_conf_template.h重命名为lv_conf.h，移动到lvgls文件夹的根目录&lt;/li&gt;
&lt;li&gt;打开examples/porting文件夹，复制lv_port_disp_template.c、lv_port_disp_template.h、lv_port_indev_template.c、lv_port_indev_template.h四个文件到lvgls文件夹根目录，并且重命名四个文件，去掉&quot;_template&quot;。这是后面要用到的显示屏和触摸屏注册文件&lt;/li&gt;
&lt;li&gt;把src文件夹和lvgl.h复制到lvgls/lvgl文件夹中&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;现在的lvgls文件夹是这样的：
&lt;img src=&quot;/images/lvgl/lvgl2.webp&quot; alt=&quot;&quot; /&gt;
lvgl文件夹：
&lt;img src=&quot;/images/lvgl/lvgl3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;添加LVGL库文件至工程&lt;/h3&gt;
&lt;p&gt;将以下文件全部添加进入工程（可以一股脑全部加，也可以按照原本的文件结构自己慢慢加）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;src文件夹中的所有c文件，&lt;strong&gt;除了&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;draw中的nxp、renesas、sdl&lt;/li&gt;
&lt;li&gt;drivers文件夹&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;lv_port_disp.c和lv_port_indev.c&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;（千万别忘了src文件夹根目录下还有个lv_init.c）&lt;/p&gt;
&lt;p&gt;然后，添加头文件目录，到src为止就可以.&lt;/p&gt;
&lt;p&gt;我的添加完之后的工程结构长这个样子：
&lt;img src=&quot;/images/lvgl/lvgl4.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;修改Stack和Heap大小&lt;/h3&gt;
&lt;p&gt;LVGL需要至少2KB的Stack和Heap。
&lt;img src=&quot;/images/lvgl/lvgl5.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;代码修改&lt;/h2&gt;
&lt;h3&gt;定时器&lt;/h3&gt;
&lt;p&gt;LVGL需要一个tick源来为其提供心跳，这里使用基本定时器TIM6完成，PSC为10-1，ARR为6000-1.在tim.c中添加如下的回调函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  if (htim == &amp;amp;htim6)
  {
    lv_tick_inc(1);   /* LVGL systick */
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;别忘了在定时器初始化时打开TIM6的Time base，让定时器开始工作。&lt;/p&gt;
&lt;h3&gt;lv_conf.h&lt;/h3&gt;
&lt;p&gt;启用宏：&lt;code&gt;#if 1 /*Set it to &quot;1&quot; to enable content*/&lt;/code&gt;
检查以下关键信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/*Color depth: 1 (I1), 8 (L8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/#define LV_COLOR_DEPTH 16&lt;/code&gt;：颜色深度，默认为RGB565&lt;/li&gt;
&lt;li&gt;&lt;code&gt;#define LV_MEM_SIZE (64U * 1024U)          /*[bytes]*/&lt;/code&gt;缓冲区大小
，默认64KB&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其他的东西理论上你看不懂也没关系，可以暂时不动，日后可以对照着注释慢慢研究。&lt;/p&gt;
&lt;h3&gt;lv_port_disp.c&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;设置屏幕分辨率：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#ifndef MY_DISP_HOR_RES
    #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now.
    #define MY_DISP_HOR_RES    1024
#endif

#ifndef MY_DISP_VER_RES
    #warning Please define or replace the macro MY_DISP_VER_RES with the actual screen height, default value 240 is used for now.
    #define MY_DISP_VER_RES    600
#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意，这里必须根据自己使用的屏幕分辨率来设置！如果尺寸设置的比实际屏幕小会导致实际的图像占不满整个屏幕，如果设置的比实际大会导致花屏、撕裂或干脆无法显示&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置缓冲区&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;
void lv_port_disp_init(void)
{
    /*-------------------------
     * Initialize your display
     * -----------------------*/
    disp_init();

    /*------------------------------------
     * Create a display and set a flush_cb
     * -----------------------------------*/
    lv_display_t * disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
    lv_display_set_flush_cb(disp, disp_flush);

    /* Example 1
     * One buffer for partial rendering*/
    LV_ATTRIBUTE_MEM_ALIGN
    static uint8_t buf_1_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];            /*A buffer for 10 rows*/
    lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL);

    // /* Example 2
    //  * Two buffers for partial rendering
    //  * In flush_cb DMA or similar hardware should be used to update the display in the background.*/
    // LV_ATTRIBUTE_MEM_ALIGN
    // static uint8_t buf_2_1[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];

    // LV_ATTRIBUTE_MEM_ALIGN
    // static uint8_t buf_2_2[MY_DISP_HOR_RES * 10 * BYTE_PER_PIXEL];
    // lv_display_set_buffers(disp, buf_2_1, buf_2_2, sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL);

    // /* Example 3
    //  * Two buffers screen sized buffer for double buffering.
    //  * Both LV_DISPLAY_RENDER_MODE_DIRECT and LV_DISPLAY_RENDER_MODE_FULL works, see their comments*/
    // LV_ATTRIBUTE_MEM_ALIGN
    // static uint8_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL];

    // LV_ATTRIBUTE_MEM_ALIGN
    // static uint8_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES * BYTE_PER_PIXEL];
    // lv_display_set_buffers(disp, buf_3_1, buf_3_2, sizeof(buf_3_1), LV_DISPLAY_RENDER_MODE_DIRECT);

}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我这里设置的是单缓冲区，实际按照自己需求来就可以。如果不是干那些高刷新率的活，单缓冲足够用了。原例程提供了三个缓冲区方式，分别是单缓冲、双缓冲和全尺寸双缓冲，选择合适的一个之后注释掉其他的几个就可以。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显示设备初始化&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*Initialize your display and the required peripherals.*/
static void disp_init(void)
{
    /*You code here*/
    lcd_init();
    lcd_display_dir(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里添加屏幕的初始化函数。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;设置刷屏&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*Flush the content of the internal buffer the specific area on the display.
 *`px_map` contains the rendered image as raw pixel map and it should be copied to `area` on the display.
 *You can use DMA or any hardware acceleration to do this operation in the background but
 *&apos;lv_display_flush_ready()&apos; has to be called when it&apos;s finished.*/
static void disp_flush(lv_display_t * disp_drv, const lv_area_t * area, uint8_t * px_map)
{
    if(1) {
        /*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
        lcd_color_fill(area-&amp;gt;x1, area-&amp;gt;y1, area-&amp;gt;x2, area-&amp;gt;y2, (uint16_t *)px_map);

    }
    /*IMPORTANT!!!
     *Inform the graphics library that you are ready with the flushing*/
    lv_display_flush_ready(disp_drv);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用正点原子例程中的lcd_color_fill()函数替代原来默认的画点函数。&lt;/p&gt;
&lt;h3&gt;lv_port_indev.c&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;修改初始化函数，添加触摸屏的初始化函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;static void touchpad_init(void)
{
    /*Your code comes here*/
    tp_dev.init();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改触摸屏按下的识别动作&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
    /*Your code comes here*/
    tp_dev.scan(0);
    if (tp_dev.sta &amp;amp; TP_PRES_DOWN) {
        return true;
    }
    return false;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;修改坐标读取函数&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(int32_t * x, int32_t * y)
{
    /*Your code comes here*/

    (*x) = tp_dev.x[0];
    (*y) = tp_dev.y[0];
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;main.c&lt;/h3&gt;
&lt;p&gt;初始化一系列外设，对于lvgl相关的头文件则要按顺序初始化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  lv_init();
  lv_port_disp_init();
  lv_port_indev_init();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在while(1)主循环中添加time_handler&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    lv_timer_handler();
    delay_ms(5);
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;到这里为止，可以加一点自己的测试代码，然后编译烧录，LVGL就能正常显示了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/lvgl/lvgl6.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果碰到问题欢迎站内私信。&lt;/p&gt;
</content:encoded></item><item><title>数组中的解题技巧</title><link>https://arkmostima.pages.dev/posts/%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E8%A7%A3%E9%A2%98%E6%8A%80%E5%B7%A7/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%95%B0%E7%BB%84%E4%B8%AD%E7%9A%84%E8%A7%A3%E9%A2%98%E6%8A%80%E5%B7%A7/</guid><description>双指针能够将$O(n^2)$复杂度的算法降低为$O(n)$，并且常见于原地算法。两个指针能够在一个for循环内完成两个for循环的工作。</description><pubDate>Thu, 02 Jan 2025 12:53:30 GMT</pubDate><content:encoded>&lt;h2&gt;双指针&lt;/h2&gt;
&lt;p&gt;双指针能够将$O(n^2)$复杂度的算法降低为$O(n)$，并且常见于原地算法。两个指针能够在一个for循环内完成两个for循环的工作。&lt;/p&gt;
&lt;h3&gt;同向指针&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/move-zeroes&quot;&gt;283.移动零&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void moveZeroes(int* nums, int numsSize) {
    int slow = 0;

    for (int i = 0; i &amp;lt; numsSize; i++)
    {
        if (nums[i] != 0)       //不要判断nums[i] == 0，否则会很麻烦
        {
            nums[slow] = nums[i];
            slow++;
        }
    }

    for (int i = slow; i &amp;lt; numsSize; i++)
    {
        nums[i] = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在这里，slow作为慢指针，用于指示去掉0之后的“新元素”，而循环中的&lt;code&gt;i&lt;/code&gt;作为快指针，负责处理遍历。快指针走的比慢指针快，因此慢指针对元素做的原地处理不会和后面的快指针操作发生冲突。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-element&quot;&gt;27.移除元素&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int removeElement(int* nums, int numsSize, int val) {
    //int fast = 0;
    int slow = 0;

    for (int i = 0; i &amp;lt; numsSize; i++)
    {
        if (nums[i] != val)
        {
            nums[slow] = nums[i];
            slow++; 
        }
    }

    return slow;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路和上一题相同。主要判断条件都是“不等于”，因为slow要指向的对象是待储存元素（slow要重新在数组内为这些元素分配位置），而不是等于&lt;code&gt;val&lt;/code&gt;的元素。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/remove-duplicates-from-sorted-array&quot;&gt;26.删除有序数组中的重复项&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int removeDuplicates(int* nums, int numsSize) {
    int current_pos = 1;
    for (int i = 1; i &amp;lt; numsSize; i++)
    {
        if(nums[i] != nums[i-1])    
        {
            nums[current_pos] = nums[i];    
            current_pos++;
        }
    }

    return current_pos;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;思路也是一致的，判断出当前元素是否和前一个不一致，如果不一致说明这个元素要保留，slow就会指向这个元素。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/intersection-of-two-arrays/&quot;&gt;349.两个数组的交集&lt;/a&gt;
这题的分为两个部分：排序+双指针。双指针法只需要一次循环，前提是数组必须经过排序。排序完后的数组是升序的，两个数组分别使用两个指针从首位开始向后遍历并比较。以这两个数组为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[4,9,5]
[9,4,9,8,4]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;排序之后的结果为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[4,5,9]
[4,4,8,9,9]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个指针分别开始遍历，如果指针1指向值大于指针2指向值，说明指针2位置偏左，对其++；如果指针1指向值小于指针2指向值，则说明指针1偏左，对其++；如果指针1和指针2指向值大小相等，该值属于交集，提出并保存在result中。&lt;/p&gt;
&lt;p&gt;这个做法会带来重复存入的问题，比如上面的算例中，result会先后存入两个4。为了避免该情况发生，在存入时需要做过滤，条件为result中待存入位置的前一个数不能等于当前要存入的这个数（&lt;code&gt;result[k-1] != nums1[i]&lt;/code&gt;。如果待存入位置是result的第一位，则不用判断，直接存入&lt;code&gt;k == 0&lt;/code&gt;。因为两个数组事先已经排过序，所以这个判断逻辑一定是正确的，不存在漏判断的问题。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
 * Note: The returned array must be malloced, assume caller calls free().
 */
int compare(const void *a, const void *b){
    return *(int *)a - *(int *)b;
}

int* intersection(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) {
    qsort(nums1, nums1Size, sizeof(int), compare);
    qsort(nums2, nums2Size, sizeof(int), compare);
    int i = 0;
    int j = 0;
    int k = 0;
    int* result = malloc(sizeof(int) * (nums1Size &amp;lt; nums2Size ? nums1Size : nums2Size));

    while (i &amp;lt; nums1Size &amp;amp;&amp;amp; j &amp;lt; nums2Size){
        if (nums1[i] &amp;gt; nums2[j]){
            j++;
        }
        else if (nums1[i] &amp;lt; nums2[j]){
            i++;
        }
        else if (nums1[i] == nums2[j]){
            if (k == 0 || result[k - 1] != nums1[i]){
                result[k++] = nums1[i];
            }
            i++;
            j++;
        }
    }

    *returnSize = k;
    return result;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;排序&lt;/h2&gt;
&lt;h3&gt;冒泡排序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void bubble_sort(int* nums, int numsSize)
{
    for (int i = 0; i &amp;lt; numsSize - 1; i++)
    {
        for (int j = 0; j &amp;lt; numsSize - 1 - i; j++)
        {
            if (nums[j] &amp;gt; nums[j + 1])
            {
                int temp = nums[j];
                nums[j] = nums[j + 1];
                nums[j + 1] = temp;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;快速排序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;其他&lt;/h2&gt;
&lt;h3&gt;Boyer-Moore 投票算法（众数问题）&lt;/h3&gt;
&lt;p&gt;Boyer-Moore 投票算法是一种用于寻找**众数（Majority Element）**的高效算法。众数定义为一个数组中出现次数超过总长度一半的元素。该算法的时间复杂度为 O(n)，空间复杂度为 O(1)，非常高效。核心思想是利用计数器（count）维护当前候选元素的“优势”，如果某个元素在整个数组中出现次数超过一半，算法最终能锁定这个元素。&lt;/p&gt;
&lt;p&gt;步骤为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;候选阶段&lt;/strong&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从数组的第一个元素开始，假设它是众数候选人（&lt;code&gt;candidate&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;遍历数组，如果当前元素与候选人相同，增加计数器（&lt;code&gt;count++&lt;/code&gt;）；如果不同，减少计数器（&lt;code&gt;count--&lt;/code&gt;）。&lt;/li&gt;
&lt;li&gt;当计数器为零时，重新选择下一个元素作为新的候选人。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;确认阶段&lt;/strong&gt;（可选，视问题需求而定）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如果题目明确保证数组中一定有众数，则跳过此步骤，候选人即为众数。&lt;/li&gt;
&lt;li&gt;如果不保证众数存在，需要再遍历一次数组，验证候选人的出现次数是否超过数组的一半。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a href=&quot;https://leetcode.cn/problems/majority-element&quot;&gt;169.多数元素&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int majorityElement(int* nums, int numsSize) {
    int count = 0;      /* 计数器 */
    int candidate = 0;  /* 候选人 */

    for (int i = 0; i &amp;lt; numsSize; i++){
        if (count == 0){        /* 如果计数器为0，选中当前元素为候选人 */
            candidate = nums[i];
            count = 1;
        }else if (nums[i] == candidate){    /* 如果当前元素为候选人 */
            count++;
        }else{
            count--;
        }
    }

    return candidate;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>FLASH、EEPROM和FLASH模拟EEPROM</title><link>https://arkmostima.pages.dev/posts/flasheeprom%E5%92%8Cflash%E6%A8%A1%E6%8B%9Feeprom/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/flasheeprom%E5%92%8Cflash%E6%A8%A1%E6%8B%9Feeprom/</guid><description>FLASH存储器和EEPROM（电可擦可编程只读存储器）都属于非易失性存储器，但它们在工作原理、使用方式、应用场景等方面有一些区别。</description><pubDate>Thu, 02 Jan 2025 07:43:53 GMT</pubDate><content:encoded>&lt;h2&gt;FLASH和EEPROM&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;FLASH存储器&lt;/strong&gt;和&lt;strong&gt;EEPROM&lt;/strong&gt;（电可擦可编程只读存储器）都属于&lt;strong&gt;非易失性存储器&lt;/strong&gt;，但它们在工作原理、使用方式、应用场景等方面有一些区别。&lt;/p&gt;
&lt;h3&gt;&lt;strong&gt;FLASH存储器&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;FLASH&lt;/strong&gt;（闪存）是一种非易失性存储器，能够在没有电源的情况下保存数据。它的工作原理类似于EEPROM，但在数据擦除和写入的方式上有所不同。&lt;/p&gt;
&lt;h4&gt;特点：&lt;/h4&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;大容量&lt;/strong&gt;：FLASH存储器一般提供比EEPROM更大的存储容量，通常从几兆字节（MB）到几百兆字节（甚至几个GB）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;块擦除&lt;/strong&gt;：FLASH存储器以&lt;strong&gt;块&lt;/strong&gt;（通常为几KB到几MB）为单位进行擦除，擦除操作需要清空整个块，而不能像EEPROM那样按字节擦除。这使得FLASH的擦写次数比EEPROM更低，一般在10,000到100,000次之间。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;写入速度较快&lt;/strong&gt;：相比EEPROM，FLASH的写入速度更快，适用于需要快速写入的应用场景。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;应用广泛&lt;/strong&gt;：FLASH主要用于存储大容量数据，广泛应用于固态硬盘（SSD）、U盘、存储卡、智能手机、嵌入式设备等。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;编程和擦除&lt;/strong&gt;：FLASH可以通过电气方式编程和擦除，但擦除和编程过程比EEPROM要复杂得多，且无法像EEPROM那样轻松按字节更新数据。&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;类型：&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NAND Flash&lt;/strong&gt;：主要用于大容量存储，速度快，成本低，广泛用于移动存储设备（如U盘、SD卡、SSD等）。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NOR Flash&lt;/strong&gt;：速度较慢但可按字节进行读写，主要用于代码存储，适合嵌入式系统。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;strong&gt;EEPROM vs FLASH&lt;/strong&gt;&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;特性&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;EEPROM&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;FLASH&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;存储容量&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较小，一般为几KB到几MB&lt;/td&gt;
&lt;td&gt;较大，可以达到几GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;数据擦除方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;按字节擦除&lt;/td&gt;
&lt;td&gt;按块擦除（大块范围，一般几KB至几MB）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;写入速度&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较慢，尤其在单个字节写入时&lt;/td&gt;
&lt;td&gt;较快，适合大块数据写入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;擦写次数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较多，通常达到100,000次以上&lt;/td&gt;
&lt;td&gt;较少，通常为10,000到100,000次&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;应用场景&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;存储配置参数、序列号、校准数据等&lt;/td&gt;
&lt;td&gt;存储操作系统、应用程序、数据文件等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;价格&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;较贵，尤其在小容量时&lt;/td&gt;
&lt;td&gt;相对便宜，适用于大容量存储&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;EEPROM&lt;/strong&gt;通常用于存储少量的数据，比如设备配置、用户设置等，需要频繁的、字节级的读写操作。它的主要优点是可以单独字节擦写，但容量较小，写入速度较慢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FLASH&lt;/strong&gt;则适用于大容量的存储需求，广泛用于存储操作系统、应用程序、媒体文件等。由于其按块擦除的特性，写入速度较快，但擦写次数较少，适合大数据量的存储。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于嵌入式系统来说，&lt;strong&gt;EEPROM&lt;/strong&gt;适用于需要频繁更新的小量数据，而&lt;strong&gt;FLASH&lt;/strong&gt;则适合存储较大的程序或数据集。&lt;/p&gt;
&lt;h2&gt;STM32F429中的FLASH&lt;/h2&gt;
&lt;h3&gt;组织架构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash1.webp&quot; alt=&quot;MCU自带FLASH组织&quot; /&gt;
STM32F429 本身没有自带 EEPROM，但是 STM32F429 具有 IAP（在应用编程）功能，所以我们可以把它的 FLASH 当成 EEPROM 来使用。对于F429IGT6来说，其FLASH容量为1024KB。FLASH模块分为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;主存储器：该部分用来存放代码和数据常数（如 const 类型的数据）。分为两个 Bank，每个 Bank 分为 12 个扇区，前 4 个扇区为 16KB 大小，第五个扇区是 64KB 大小，剩下的 7 个扇区都是 128K大小，总共 1M 字节。两个 Bank 就是 2M 字节。不同容量的 STM32F429，拥有的扇区数不一样。以STM32F429IGT6为例，只有 Bank1，拥有 12 个扇区，1M 字节容量。主存储器的起始地址一般为 0X08000000，B0、B1 都接 GND 的时候，就是从0X08000000 开始运行代码的。&lt;/li&gt;
&lt;li&gt;系统存储器：主要用来存放 STM32F429的 bootloader代码，此代码在出厂时已经固化在 STM32F429 内，专门用来给主存储器下载代码。当 B0 接 V3.3，B1 接 GND 的时候，从该存储器启动（即进入串口下载模式）。&lt;/li&gt;
&lt;li&gt;OTP区域（一次性可编程区域）：共 528 字节，被分成两个部分，前面 512字节（32 字节为1块，分成16块），可以用来存储一些用户数据（一次性，写完一次，永远不可以擦除），后面 16 字节，用于锁定对应块。&lt;/li&gt;
&lt;li&gt;选项字节：用于配置读保护、BOR 级别、软件/硬件看门狗以及器件处于待机或停止模式下的复位。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在执行闪存写操作时，任何对闪存的读操作都会锁住总线，在写操作完成后读操作才能正
确地进行；即在进行写或擦除操作时，不能进行代码或数据的读取操作。&lt;/p&gt;
&lt;h3&gt;读取&lt;/h3&gt;
&lt;p&gt;为了准确读取 Flash 数据，必须根据 CPU 时钟 (HCLK) 频率和器件电源电压在 Flash 存取控制寄存器 (FLASH_ACR) 中正确地设置等待周期数 (LATENCY)。当电源电压低于 2.1V 时，必须关闭预取缓冲器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash2.webp&quot; alt=&quot;ACLK时钟频率和对应的FLASH等待周期&quot; /&gt;&lt;/p&gt;
&lt;p&gt;等待周期通过FLASH_ACR寄存器的LATENCY[2:0]三个位设置。&lt;/p&gt;
&lt;p&gt;STM32读取FLASH很简单，例如要从地址addr读取一个字（32位），只需要一句话：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Data = *(volatile uint32_t *)faddr;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将 faddr 强制转换为 &lt;code&gt;volatile uint32_t&lt;/code&gt; 指针，然后取该指针所指向的地址的值，即得到了 faddr 地址的值。类似的，将上面的 &lt;code&gt;volatile uint32_t&lt;/code&gt; 改为 &lt;code&gt;volatile uint8_t&lt;/code&gt;，即可读取指定地址的一个字节。此处的 &lt;code&gt;volatile&lt;/code&gt; 关键字告诉编译器该地址的内容可能随时改变，因此禁止编译器对该地址做任何优化。通常用于表示硬件寄存器或外设的内存地址，因为这些内容的值可能由外部事件（如外部硬件）改变。&lt;/p&gt;
&lt;h3&gt;编程和擦除&lt;/h3&gt;
&lt;p&gt;执行任何 Flash编程操作（擦除或编程）时，CPU时钟频率（HCLK）不能低于 1MHz。在对 STM32F429 的 Flash 执行写入或擦除操作期间，任何读取 Flash 的尝试都会导致总线阻塞。只有在完成编程操作后，才能正确处理读操作。这意味着，写/擦除操作进行期间不能从 Flash 中执行代码或数据获取操作。&lt;/p&gt;
&lt;p&gt;STM32F429 用户闪存的编程一般由 6 个 32 位寄存器控制，他们分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;FLASH 访问控制寄存器(FLASH_ACR)&lt;/li&gt;
&lt;li&gt;FLASH 秘钥寄存器(FLASH_KEYR)&lt;/li&gt;
&lt;li&gt;FLASH 选项秘钥寄存器(FLASH_OPTKEYR)&lt;/li&gt;
&lt;li&gt;FLASH 状态寄存器(FLASH_SR)&lt;/li&gt;
&lt;li&gt;FLASH 控制寄存器(FLASH_CR)&lt;/li&gt;
&lt;li&gt;FLASH 选项控制寄存器(FLASH_OPTCR)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;STM32F429复位后，FLASH 编程操作是被保护的，不能写入 FLASH_CR寄存器；通过写入特定的序列（&lt;code&gt;0X45670123&lt;/code&gt; 和 &lt;code&gt;0XCDEF89AB&lt;/code&gt;）到 FLASH_KEYR 寄存器才可解除写保护，只有在写保护被解除后，我们才能操作相关寄存器。&lt;/p&gt;
&lt;p&gt;FLASH_CR 的解锁序列为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;写 &lt;code&gt;0X45670123&lt;/code&gt; 到 FLASH_KEYR&lt;/li&gt;
&lt;li&gt;写 &lt;code&gt;0XCDEF89AB&lt;/code&gt; 到 FLASH_KEYR&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;通过这两个步骤，即可解锁 FLASH_CR，如果写入错误，那么 FLASH_CR 将被锁定，直到下次复位后才可以再次解锁。&lt;/p&gt;
&lt;p&gt;STM32F429 闪存的编程位数可以通过 FLASH_CR 的 PSIZE 字段配置，PSIZE 的设置必须
和电源电压匹配：
&lt;img src=&quot;/images/arm/flash3.webp&quot; alt=&quot;编程/擦除并行位数与电压关系&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当开发板电压为3.3V时，&lt;code&gt;PSIZE&lt;/code&gt;为&lt;code&gt;10&lt;/code&gt;，并行位数32位。也就是擦除或编程都需要以32位为基础进行。&lt;/p&gt;
&lt;p&gt;FLASH在编程时必须要求目标写入地址的FLASH已经被擦除（值为&lt;code&gt;0xFFFFFFFF&lt;/code&gt;），否则将无法写入。&lt;/p&gt;
&lt;p&gt;F429的FLASH标准编程步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查FLASH_CR中的BSY位，确保总线空闲&lt;/li&gt;
&lt;li&gt;将FLASH_CR寄存器的PG位置1，激活FLASH编程&lt;/li&gt;
&lt;li&gt;针对所需存储地址（主存储器块或 OTP 区域内）执行数据写入操作：
&lt;ul&gt;
&lt;li&gt;并行位数为x32时按字写入（PSIZE=02）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;等待BSY位清零&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：
- 编程前确保写入地址已经擦除
- 先解锁FLASH_CR后编程&lt;/p&gt;
&lt;p&gt;扇区擦除步骤如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;检查FLASH_CR的LOOK是否解锁，如果没有则先解锁&lt;/li&gt;
&lt;li&gt;检查FLASH_CR中的BSY位，确保总线空闲&lt;/li&gt;
&lt;li&gt;FLASH_CR的SER位置1，并从主存储块的12个扇区中选择要擦除的扇区（SNB）&lt;/li&gt;
&lt;li&gt;FLASH_CR寄存器中STRT位置1，触发擦除&lt;/li&gt;
&lt;li&gt;等待BSY位清零&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;寄存器&lt;/h3&gt;
&lt;h4&gt;FLASH_ACR - Flash访问控制寄存器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash4.webp&quot; alt=&quot;FLASH_ACR&quot; /&gt;
重点为LATENCY[2:0]位，应根据MCU电压和频率设置，否则死机。&lt;/p&gt;
&lt;h4&gt;FLASH_KEYR - Flash密钥寄存器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash5.webp&quot; alt=&quot;FLASH_KEYR&quot; /&gt;
用于解锁FLASH_CR，必须按顺序写入KEY1和KEY2&lt;/p&gt;
&lt;h4&gt;FLASH_CR - Flash控制寄存器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash6.webp&quot; alt=&quot;FLASH_CR&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;LOCK位：用于指示 FLASH_CR 寄存器是否被锁住，该位在检测到正确的解锁序列后，硬件将其清零。在一次不成功的解锁操作后，在下次系统复位之前，该位将不再改变。&lt;/li&gt;
&lt;li&gt;PG位：用于选择编程操作，在往 FLASH 写数据的时候，该位需要置 1。&lt;/li&gt;
&lt;li&gt;SER位：位用于选择扇区擦除操作，在扇区擦除的时候，需要将该位置 1。&lt;/li&gt;
&lt;li&gt;PSIZE[1:0]位：设置编程宽度，一般设置 PSIZE =2 即可（32 位）。&lt;/li&gt;
&lt;li&gt;STRT位：该位用于开始一次擦除操作。在该位写入 1 ，将执行一次擦除操作。&lt;/li&gt;
&lt;li&gt;SNB[3:0]位：用于选择要擦除的扇区编号，取值范围为 0~15。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;FLASH_SR - Flash状态寄存器&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/flash7.webp&quot; alt=&quot;FLASH_SR&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;BSY位：确认BANK是否正在执行编程操作&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;代码&lt;/h2&gt;
&lt;h3&gt;读取&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; /**
 * @brief       从指定地址读取一个字（32位）
 * @param       faddr   : 读取地址
 * @retval      读取到的数据（32位）
 */
uint32_t stmflash_read_word(uint32_t faddr)
{
    return *(volatile uint32_t *)faddr;
}

/**
* @brief 从指定地址开始读出指定长度的数据
* @param raddr : 起始地址
* @param pbuf : 数据指针
* @param length: 要读取的字(32)数,即 4 个字节的整数倍
* @retval 无
*/
void stmflash_read(uint32_t raddr, uint32_t *pbuf, uint32_t length)
{
    uint32_t i;

    for (i = 0; i &amp;lt; length; i++)
    {
        pbuf[i] = stmflash_read_word(raddr);
        raddr += 4;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;写入&lt;/h3&gt;
&lt;p&gt;写入相对来说复杂一点，首先要判断写入地址是否正确（不能超出FLASH地址范围，不能不是4的倍数），判断通过后，解锁FLASH，禁用数据缓存，然后判断待写入地址范围是否为&lt;code&gt;0xFFFFFFFF&lt;/code&gt;。如果是，表明该地址已经被擦除；如果不是，表明该地址需要先擦除，再写入。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void stmflash_write(uint32_t waddr, uint32_t *pbuf, uint32_t length)
{
    FLASH_EraseInitTypeDef flash_erase_init;
    HAL_StatusTypeDef flashstatus = HAL_OK;
    uint32_t sector_error = 0;
    uint32_t addrx = 0;
    uint32_t endaddr = 0;

    if (waddr &amp;lt; STM32_FLASH_BASE || waddr % 4 || waddr &amp;gt; (STM32_FLASH_BASE + STM32_FLASH_SIZE)) return;     /* 地址非法 */

    HAL_FLASH_Unlock();

    FLASH-&amp;gt;ACR &amp;amp;= ~(1 &amp;lt;&amp;lt; 10);       /* 禁用数据缓存 */
    addrx = waddr;                  /* 写入起始地址*/
    endaddr = waddr + length * 4;   /* 写入结束地址 */

    if (addrx &amp;lt; 0x1FFF0000)  /* 主存储区*/
        while (addrx &amp;lt; endaddr)
        {
            if (stmflash_read_word(addrx) != 0xFFFFFFFF)        /* 当前地址不为0xFFFFFFFF则表示需要擦除此地址 */
            {
                flash_erase_init.TypeErase = FLASH_TYPEERASE_SECTORS;
                flash_erase_init.Sector = stmflash_get_flash_sector(addrx);
                flash_erase_init.NbSectors = 1;
                flash_erase_init.VoltageRange = FLASH_VOLTAGE_RANGE_3;

                if (HAL_FLASHEx_Erase(&amp;amp;flash_erase_init, &amp;amp;sector_error) != HAL_OK) break;
            }
            else
            {
                addrx += 4;
            }
            FLASH_WaitForLastOperation(FLASH_WAITETIME);
        }
    flashstatus = FLASH_WaitForLastOperation(FLASH_WAITETIME);
    if (flashstatus == HAL_OK)
    {
        while (waddr &amp;lt; endaddr)
        {
            if (HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, waddr, *pbuf) != HAL_OK) break;
            waddr += 4;
            pbuf++;
        }
    }
    FLASH-&amp;gt;ACR |= 1 &amp;lt;&amp;lt; 10;     /* 启用数据缓存 */

    HAL_FLASH_Lock();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里有几个需要注意的点：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;擦除和写入期间必须禁用数据缓存，否则缓存器中的缓存内容和新的内容可以不一致导致bug。禁用数据缓存通过位操作&lt;code&gt;FLASH-&amp;gt;ACR &amp;amp;= ~(1 &amp;lt;&amp;lt; 10)&lt;/code&gt;实现，&lt;code&gt;1 &amp;lt;&amp;lt; 10&lt;/code&gt;表示&lt;code&gt;1&lt;/code&gt;左移10位，&lt;code&gt;~&lt;/code&gt;表示取反。&lt;/li&gt;
&lt;li&gt;擦除操作通过&lt;code&gt;HAL_FLASHEx_Erase()&lt;/code&gt;实现，HAL库对于该函数的定义为：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/**
  * @brief  Perform a mass erase or erase the specified FLASH memory sectors
  * @param[in]  pEraseInit pointer to an FLASH_EraseInitTypeDef structure that
  *         contains the configuration information for the erasing.
  *
  * @param[out]  SectorError pointer to variable  that
  *         contains the configuration information on faulty sector in case of error
  *         (0xFFFFFFFFU means that all the sectors have been correctly erased)
  *
  * @retval HAL Status
  */
HAL_StatusTypeDef HAL_FLASHEx_Erase(FLASH_EraseInitTypeDef *pEraseInit, uint32_t *SectorError)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参1为存储擦除配置的结构体，形参2为存储报错信息的变量。
结构体格式为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * @brief  FLASH Erase structure definition
  */
typedef struct
{
  uint32_t TypeErase;   /*!&amp;lt; Mass erase or sector Erase.
                             This parameter can be a value of @ref FLASHEx_Type_Erase */

  uint32_t Banks;       /*!&amp;lt; Select banks to erase when Mass erase is enabled.
                             This parameter must be a value of @ref FLASHEx_Banks */

  uint32_t Sector;      /*!&amp;lt; Initial FLASH sector to erase when Mass erase is disabled
                             This parameter must be a value of @ref FLASHEx_Sectors */

  uint32_t NbSectors;   /*!&amp;lt; Number of sectors to be erased.
                             This parameter must be a value between 1 and (max number of sectors - value of Initial sector)*/

  uint32_t VoltageRange;/*!&amp;lt; The device voltage range which defines the erase parallelism
                             This parameter must be a value of @ref FLASHEx_Voltage_Range */

} FLASH_EraseInitTypeDef;
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;全部擦除完成并等待&lt;code&gt;FLASH_WaitForLastOperation(FLASH_WAITETIME)&lt;/code&gt;传回&lt;code&gt;HAL_OK&lt;/code&gt;后，才可以开始进行。写入函数为&lt;code&gt;HAL_FLASH_Program&lt;/code&gt;：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;/**
  * @brief  Program byte, halfword, word or double word at a specified address
  * @param  TypeProgram  Indicate the way to program at a specified address.
  *                           This parameter can be a value of @ref FLASH_Type_Program
  * @param  Address  specifies the address to be programmed.
  * @param  Data specifies the data to be programmed
  *
  * @retval HAL_StatusTypeDef HAL Status
  */
HAL_StatusTypeDef HAL_FLASH_Program(uint32_t TypeProgram, uint32_t Address, uint64_t Data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;形参1用于指定写入的是字节（8位）、半个字（16位）、字（32位）或双字（64位）；形参2为写入地址，形参3为待写入数据。连续写入时，形参3一般设置为指针，每次循环对相应地址+1处理。
4. 写入完成后，启用数据缓存，FLASH上锁。&lt;/p&gt;
</content:encoded></item><item><title>CAN及其应用</title><link>https://arkmostima.pages.dev/posts/can%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/can%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8/</guid><description>CAN 总线由德国BOSCH公司开发，是一种多主控消息广播系统，其最大信号传输速率为1Mbps。与USB或以太网等传统网络不同，CAN 不会在中央总线主控的监督下从节点A向节点B点对点发送大量数据包。在CAN网络中，许多短消息（如温度或…</description><pubDate>Sat, 28 Dec 2024 09:07:58 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;CAN 总线由德国BOSCH公司开发，是一种多主控消息广播系统，其最大信号传输速率为1Mbps。与USB或以太网等传统网络不同，CAN 不会在中央总线主控的监督下从节点A向节点B点对点发送大量数据包。在CAN网络中，许多短消息（如温度或发动机转速）会广播到整个网络，从而确保系统每个节点的数据一致性。CAN 是国际标准化组织 (ISO) 定义的串行通信总线，最初是为汽车行业开发的，旨在用双线总线取代复杂的线束。该规范要求具有较高的抗电气干扰能力以及自我诊断和修复数据错误的能力。这些特性使得 CAN 在楼宇自动化、医疗和制造业等各种行业中广受欢迎。&lt;/p&gt;
&lt;p&gt;CAN具有以下特点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;多主控制&lt;/strong&gt;。在总线空闲时，所有单元都可以发送消息（多主控制），而两个以上的单元同时开始发送消息时，根据标识符（Identifier以下称为ID）决定优先级。ID并不是表示发送的目的地址，而是表示访问总线的消息的优先级。两个以上的单元同时开始发送消息时，对各消息ID的每个位进行逐个仲裁比较。仲裁获胜（被判定为优先级最高）的单元可继续发送消息，仲裁失利的单元则立刻停止发送而进行接收工作。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;系统的柔软性&lt;/strong&gt;。与总线相连的单元没有类似于“地址”的信息。因此在总线上增加单元时，连接在总线上的其它单元的软硬件及应用层都不需要改变。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;通信速度较快，通信距离远&lt;/strong&gt;。最高1Mbps（距离小于40M），最远可达10KM（速率低于5Kbps）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;具有错误检测、错误通知和错误恢复功能&lt;/strong&gt;。所有单元都可以检测错误（错误检测功能），检测出错误的单元会立即同时通知其他所有单元（错误通知功能），正在发送消息的单元一旦检测出错误，会强制结束当前的发送。强制结束发送的单元会不断反复地重新发送此消息直到成功发送为止（错误恢复功能）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;故障封闭功能&lt;/strong&gt;。CAN可以判断出错误的类型是总线上暂时的数据错误（如外部噪声等）还是持续的数据错误（如单元内部故障、驱动器故障、断线等）。由此功能，当总线上发生持续数据错误时，可将引起此故障的单元从总线上隔离出去。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;连接节点多&lt;/strong&gt;。CAN总线是可同时连接多个单元的总线。可连接的单元总数理论上是没有限制的。但实际上可连接的单元数受总线上的时间延迟及电气负载的限制。降低通信速度，可连接的单元数增加；提高通信速度，则可连接的单元数减少。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CAN 协议经过 ISO 标准化后有两个标准：ISO11898 标准（高速 CAN）和 ISO11519-2 标准（低速 CAN）。其中 ISO11898 是针对通信速率为 125Kbps~1Mbps 的高速通信标准，而ISO11519-2 是针对通信速率为 125Kbps 以下的低速通信标准。&lt;/p&gt;
&lt;p&gt;下文默认介绍ISO11898-2标准下的CAN，也就是高速CAN。&lt;/p&gt;
&lt;h2&gt;物理层&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN1.webp&quot; alt=&quot;高速CAN总线&quot; /&gt;
高速CAN总线呈现闭环结构，总共由两根线（CAN_High和CAN_Low）组成，总线两端各串联120Ω电阻，用于阻抗匹配。每个挂载节点拥有独立的CAN控制器（通常集成于MCU内）和CAN收发器（通常为外部IC）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN2.webp&quot; alt=&quot;高速CAN物理层特性&quot; /&gt;&lt;/p&gt;
&lt;p&gt;与RS485类似，CAN通过差分信号传输数据，以降低共模噪声的干扰，通过CAN总线上两根线之间的电位差来判断总线电平。总线电平分为显性（Dominant）电平和隐性（Recessive）电平。当总线上任何设备传输显性 (0) 时，高速 CAN 信号会将 CANH 线驱动至 3.5 V，将 CANL 线驱动至 1.5 V；而如果没有设备传输显性，则终端电阻会将两条线被动地返回到隐性 (1) 状态，标称差分电压为 0 V。（接收器将任何小于 0.5 V 的差分电压视为隐性。）显性差分电压的标称值为 2 V。显性共模电压 (CANH+CANL)/2 必须在共模的 1.5 至 3.5 V 范围内，而隐性共模电压必须在共模的 ±12V 范围内。&lt;/p&gt;
&lt;p&gt;也就是说，下文中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;显性电平：逻辑 0&lt;/li&gt;
&lt;li&gt;隐性电平：逻辑 1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可见，显性电平的“显性”具有“优先”的意味，即总线上只要有一个单元输出显性电平，总线上即为显性电平。而隐性电平的“隐性”具有“包容”的意味，只有所有的单元都输出隐性电平，总线上才为隐性电平。&lt;/p&gt;
&lt;h2&gt;协议&lt;/h2&gt;
&lt;p&gt;CAN协议通过5种类型的帧进行：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;数据帧：用于发送单元向接收单元传送数据&lt;/li&gt;
&lt;li&gt;遥控帧：用于接收单元向具有相同ID的发送单元请求数据&lt;/li&gt;
&lt;li&gt;错误帧：用于当检测出错误时向其它单元通知错误&lt;/li&gt;
&lt;li&gt;过载帧：用于接收单元通知其尚未做好接收准备&lt;/li&gt;
&lt;li&gt;间隔帧：用于将数据帧及遥控帧与前面的帧分离开来&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;数据帧和遥控帧还分为标准格式和扩展格式两种，标准格式的标识符（Identifier）为11位，扩展格式为29位。&lt;/p&gt;
&lt;h3&gt;标准格式下的数据帧&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN3.webp&quot; alt=&quot;高速CAN标准格式位标识&quot; /&gt;
&lt;img src=&quot;/images/arm/CAN4.webp&quot; alt=&quot;高速CAN标准格式位标识&quot; /&gt;
&lt;em&gt;注意&lt;/em&gt;：下方D表示显性电平（逻辑0），R表示隐性电平（逻辑1），D/R表示逻辑电平视配置而定。&lt;/p&gt;
&lt;p&gt;数据帧由7个段构成，分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;帧起始&lt;/li&gt;
&lt;li&gt;仲裁段：表示该帧优先级&lt;/li&gt;
&lt;li&gt;控制段：表示数据字节数和保留位&lt;/li&gt;
&lt;li&gt;数据段：数据内容，一帧一般可发送0-8个字节&lt;/li&gt;
&lt;li&gt;CRC段：校验帧传输正确性&lt;/li&gt;
&lt;li&gt;ACK段：确认是否正常接收&lt;/li&gt;
&lt;li&gt;帧结束&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;帧起始（1位）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;SOF：1位，显性电平，帧起始（Start of Frame），单个显性帧起始 (SOF) 位标记消息的开始，用于在空闲后同步总线上的节点。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;仲裁段（11位）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Identifier：11位（扩展下为29位），即标识符，又名仲裁段，标准CAN模式下为 11 位，用于确定消息的优先级。二进制值越低，优先级越高。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注意：无论是标准格式还是扩展格式，都禁止ID的高7位都为隐性，即禁止设定ID为1111111XXXX。&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RTR：1位，用于标识是否为远程帧（0：数据帧；1：远程帧）&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;控制段（6位）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;IDE：1位，标识符选择位（0：使用标准ID；1：使用扩展ID）&lt;/li&gt;
&lt;li&gt;r0：1位，显性电平，保留位（可能用于未来的标准修订）&lt;/li&gt;
&lt;li&gt;DLC：4位，标识当前正在传输的数据的字节数，高位在前&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：对于DLC位，通常只允许传输0-8，但接收方对DLC=9~15的情况并不视作错误&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;数据字节数&lt;/th&gt;
&lt;th&gt;DLC3&lt;/th&gt;
&lt;th&gt;DLC2&lt;/th&gt;
&lt;th&gt;DLC1&lt;/th&gt;
&lt;th&gt;DLC0&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;表中D和R实际上就是0和1.比如8个字节所对应的RDDD实际上就是二进制的1000.&lt;/p&gt;
&lt;h4&gt;数据段（0-64位）&lt;/h4&gt;
&lt;p&gt;数据段最多传输8字节数据，从最高位（MSB）开始输出。&lt;/p&gt;
&lt;h4&gt;CRC段（16位）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;CRC顺序：15位，循环冗余校验上一个传输的数据&lt;/li&gt;
&lt;li&gt;CRC界定符：1位，隐性电平，用于分割&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;CRC 顺序是根据多项式生成的 CRC 值，CRC 的计算范围包括帧起始、仲裁段、控制段、数据段。接收方以同样的算法计算 CRC 值并进行比较，不一致时会通报错误。&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;ACK段（2位）&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;ACK槽（ACK Slot）：1位&lt;/li&gt;
&lt;li&gt;ACK界定符：1位，隐性电平&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：对于ACK Slot，其工作方式为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对于发送单元：发送单元在ACK Slot发送1个隐性位&lt;/li&gt;
&lt;li&gt;对于接收单元：接收到正确信息的单元在ACK Slot发送显性位，通知发送单元正常接收结束。该动作称作“发送 ACK”或者“返回 ACK”。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;帧结束（7位）&lt;/h4&gt;
&lt;p&gt;表示帧结束，由7个隐性位构成。&lt;/p&gt;
&lt;h3&gt;错误帧&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;WIP&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;位时序&lt;/h2&gt;
&lt;p&gt;由发送单元在非同步的情况下发送的每秒钟的位数称为位速率。一个位可分为4段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步段（SS）&lt;/li&gt;
&lt;li&gt;传播时间段（PTS）&lt;/li&gt;
&lt;li&gt;相位缓冲段1（PBS1）&lt;/li&gt;
&lt;li&gt;相位缓冲段2（PBS2）
这些段又由可称为 Time Quantum（以下称为 Tq）的最小时间单位构成。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;1位分为4个段，每个段又由若干个Tq构成，这称为位时序。&lt;/p&gt;
&lt;p&gt;1位由多少个Tq构成、每个段又由多少个Tq构成等，可以任意设定位时序。通过设定位时序，多个单元可同时采样，也可任意设定采样点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/can5.webp&quot; alt=&quot;段及其作用&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN6.webp&quot; alt=&quot;1个位的构成&quot; /&gt;
图中的采样点是指读取总线电平并将其作为位值的点，位于PBS1结束处。&lt;/p&gt;
&lt;h2&gt;仲裁&lt;/h2&gt;
&lt;p&gt;CAN本质是半双工通信，因为总线上同一时刻只允许一个信号传输，ID优先级不够的信号会在仲裁时失去发送权。具体为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若总线空闲：最先开始发送消息的单元取得发送权&lt;/li&gt;
&lt;li&gt;若多个单元同时发送：各发送单元从仲裁段的第一位开始进行仲裁，连续输出显性电平（逻辑0）最多的单元可继续发送&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN7.webp&quot; alt=&quot;CAN总线仲裁过程&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;STM32F4的CAN控制器&lt;/h2&gt;
&lt;h3&gt;基本信息与标识符滤波器&lt;/h3&gt;
&lt;p&gt;STM32F4自带的是bxCAN，即基本扩展CAN，其支持CAN协议2.0A和2.0B。CAN2.0A只能处理标准数据帧，扩展帧的内容会识别错误；而CAN2.0B Active可以处理标准数据帧和扩展数据帧；CAN2.0B Passive只能处理标准数据帧，忽略扩展帧。&lt;/p&gt;
&lt;p&gt;F429自带两个CAN控制器：CAN1和CAN2.两个CAN分别拥有自己的发送邮箱和接收FIFO，但二者共用28个滤波器，其分配方式通过&lt;code&gt;CAN_FMR&lt;/code&gt;寄存器设置。滤波器用于选择性接收符合特定条件的 CAN 消息，通过对 CAN 报文的标识符（ID）进行筛选，只接收满足条件的报文，将不相关的报文丢弃。这可以避免 MCU 对所有接收到的 CAN 报文进行处理，从而提升系统效率。&lt;/p&gt;
&lt;p&gt;滤波器有两种工作模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标识符掩码模式（Identifier Mask Mode）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;通过掩码定义哪些位需要匹配。&lt;/li&gt;
&lt;li&gt;灵活性高，适合处理某些位通配的场景。&lt;/li&gt;
&lt;li&gt;掩码中的每一位可以设置为：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&quot;0&quot;：&lt;/strong&gt; 忽略对应位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&quot;1&quot;：&lt;/strong&gt; 匹配对应位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;例：目标只接收以&lt;code&gt;0x2A&lt;/code&gt;（&lt;code&gt;101010&lt;/code&gt;）为前六位的标识符，则配置过滤器的标识符为&lt;code&gt;0x2A0&lt;/code&gt;（&lt;code&gt;10101000000&lt;/code&gt;），掩码为&lt;code&gt;0xFC0&lt;/code&gt;（&lt;code&gt;11111100000&lt;/code&gt;），接收到的ID与掩码进行按位比较，只匹配掩码为1的位。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;标识符列表模式（Identifier List Mode）：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;直接指定一组标识符，只有匹配这些标识符的消息才会通过。&lt;/li&gt;
&lt;li&gt;适合明确的标识符匹配。&lt;/li&gt;
&lt;li&gt;例：指定&lt;code&gt;0x1A5&lt;/code&gt;为期望标识符，则只有接收到的ID等于&lt;code&gt;0x1A5&lt;/code&gt;时该消息才会被允许通过。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;发送&lt;/h3&gt;
&lt;p&gt;CAN 控制器有 3 个发送邮箱（Tx Mailbox），用于存储待发送的消息。发送时，将消息写入空闲邮箱，然后硬件根据仲裁机制将消息发送到总线上。仲裁失败的消息会被重新加入发送队列，直到成功发送。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN8.webp&quot; alt=&quot;CAN通过邮箱发送&quot; /&gt;&lt;/p&gt;
&lt;p&gt;正常发送流程为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;配置CAN发送帧，包括：
- 消息的标识符（11位或29位）
- 帧类型（数据帧或远程帧）
- 填充待发送数据
- 声明待发送数据长度&lt;/li&gt;
&lt;li&gt;检查邮箱状态
- 确保至少有一个发送邮箱空闲（&lt;code&gt;TME=1&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;邮箱请求发送
- 设置CAN_TIxR的TXRQ位为&lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;邮箱挂号
- &lt;code&gt;RQCP=0&lt;/code&gt;,&lt;code&gt;TXOK=0&lt;/code&gt;,&lt;code&gt;TME=0&lt;/code&gt;
- 等待成为最高优先级&lt;/li&gt;
&lt;li&gt;预定发送
- 已成为最高优先级
- 等待CAN总线空闲&lt;/li&gt;
&lt;li&gt;发送&lt;/li&gt;
&lt;li&gt;发送成功
- &lt;code&gt;RQCP=1&lt;/code&gt;,&lt;code&gt;TXOK=1&lt;/code&gt;,&lt;code&gt;TME=1&lt;/code&gt;
- 回到邮箱空闲状态&lt;/li&gt;
&lt;li&gt;发送失败
- &lt;code&gt;RQCP=1&lt;/code&gt;,&lt;code&gt;TXOK=0&lt;/code&gt;,&lt;code&gt;TME=1&lt;/code&gt;
- 回到邮箱空闲状态&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;接收&lt;/h3&gt;
&lt;p&gt;CAN接收到的有效报文，被存储在3级邮箱深度的FIFO中。FIFO完全由硬件来管理，从而节省了 CPU 的处理负荷，简化了软件并保证了数据的一致性。应用程序只能通过读取FIFO输出邮箱，来读取 FIFO 中最先收到的报文。&lt;/p&gt;
&lt;p&gt;CAN的接收有2个FIFO，我们每个滤波器组都可以设置其关联的 FIFO，通过 &lt;code&gt;CAN_FFA1R&lt;/code&gt; 的设置，可以将滤波器组关联到FIFOO/FIFO1.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;有效报文：指那些正确被接收的（直到EOF都没有错误）且通过了标识符过滤的报文。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN9.webp&quot; alt=&quot;CAN通过FIFO接收&quot; /&gt;&lt;/p&gt;
&lt;p&gt;FIFO接收到的报文数可以通过查询&lt;code&gt;CAN_RFxR&lt;/code&gt;的FMP寄存器得到。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：必须在FIFO溢出之前读出至少1个报文，否则下个报文到来时FIFO将溢出，导致最早收到的报文丢失。每读出1个报文，相应的挂号就减1，直到FIFO为空。&lt;/p&gt;
&lt;h3&gt;位时间特性&lt;/h3&gt;
&lt;p&gt;不同于原本的CAN协议，STM32把传播时间段（PTS）和相位缓冲段（PBS1）合并，所以STM32的CAN一个位只有3段：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;同步段（SYNC_SEG）&lt;/li&gt;
&lt;li&gt;时间段1（BS1）&lt;/li&gt;
&lt;li&gt;时间段2（BS2）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/CAN10.webp&quot; alt=&quot;STM32中的CAN位时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中给出了CAN波特率的计算公式，只需要知道CAN_BTR中BRP、TS1和TS2的设置就可以计算。&lt;/p&gt;
&lt;h3&gt;寄存器&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;WIP&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>STM32中的SPI</title><link>https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84spi/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84spi/</guid><description>SPI接口共有四个Pin: MISO（Master In / Slave Out）主设备数据输入，从设备数据输出。 MOSI（Master Out / Slave In）主设备数据输出，从设备数据输入。 SCLK（Serial Cloc…</description><pubDate>Tue, 17 Dec 2024 12:13:52 GMT</pubDate><content:encoded>&lt;h2&gt;SPI简介&lt;/h2&gt;
&lt;h3&gt;接口、工作原理与传输方式&lt;/h3&gt;
&lt;p&gt;SPI接口共有四个Pin:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MISO（Master In / Slave Out）主设备数据输入，从设备数据输出。&lt;/li&gt;
&lt;li&gt;MOSI（Master Out / Slave In）主设备数据输出，从设备数据输入。&lt;/li&gt;
&lt;li&gt;SCLK（Serial Clock）时钟信号，由主设备产生。&lt;/li&gt;
&lt;li&gt;CS（Chip Select）从设备片选信号，由主设备产生。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SPI的工作原理：在主机和从机都有一个串行移位寄存器，主机通过向它的 SPI 串行寄存器写入一个字节来发起一次传输。串行移位寄存器通过 MOSI 信号线将字节传送给从机，从机也将自己的串行移位寄存器中的内容通过 MISO 信号线返回给主机。这样，两个移位寄存器中
的内容就被交换。外设的写操作和读操作是同步完成的。如果只是进行写操作，主机只需忽略接收到的字节。反之，若主机要读取从机的一个字节，就必须发送一个空字节引发从机传输。&lt;/p&gt;
&lt;p&gt;SPI的传输方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;全双工：任何时刻，主机与从机之间都可以同时进行数据的发送和接收&lt;/li&gt;
&lt;li&gt;单工：数据传输是固定单向的&lt;/li&gt;
&lt;li&gt;半双工：允许数据在两个方向上传输，但同一时刻只能传输一个方向的数据（如IIC）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;SPI依据CPOL和CPHA的不同状态分成了4种模式。其中CPOL和CPHA分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CPOL（Clock Polarity，时钟极性）：定义为主从机没有数据传输时，SCL线的电平状态。假如空闲状态下SCL线为高电平，则&lt;code&gt;COPL=1&lt;/code&gt;；若为低电平，则&lt;code&gt;COPL=0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;CPHA（Clock Phase，时钟相位）：数据采样时刻，即在时钟信号的哪个边沿上数据被传输或采样。若&lt;code&gt;CPHA=0&lt;/code&gt;，则数据在时钟的第一个边沿（即时钟信号的上升沿或下降沿）采样，数据在第二个边沿（另一个相反的边沿）被传输；若&lt;code&gt;CPHA=1&lt;/code&gt;，则数据在时钟的第二个边沿（与CPHA = 0时不同）采样，数据在第一个边沿传输。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;按照CPOL和CPHA的不同，SPI的四种模式分别为：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;SPI工作模式&lt;/th&gt;
&lt;th&gt;CPOL&lt;/th&gt;
&lt;th&gt;CPHA&lt;/th&gt;
&lt;th&gt;SCL空闲状态&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;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;td&gt;上升沿&lt;/td&gt;
&lt;td&gt;奇数边沿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;LOW&lt;/td&gt;
&lt;td&gt;下降沿&lt;/td&gt;
&lt;td&gt;偶数边沿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;下降沿&lt;/td&gt;
&lt;td&gt;奇数边沿&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;HIGH&lt;/td&gt;
&lt;td&gt;上升沿&lt;/td&gt;
&lt;td&gt;偶数边沿&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;实际应用中，通常使用&lt;strong&gt;模式0&lt;/strong&gt;和&lt;strong&gt;模式3&lt;/strong&gt;，即都在奇数边沿采样。&lt;/p&gt;
&lt;h3&gt;SPI寄存器&lt;/h3&gt;
&lt;p&gt;SPI通常需要用到以下16位寄存器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SPI控制寄存器（SPI_CR1）：控制主设备模式选择，传输方向，数据格式，时钟极性、时钟相位和使能等。&lt;/li&gt;
&lt;li&gt;SPI状态寄存器（SPI_SR）：用于查询SPI当前状态&lt;/li&gt;
&lt;li&gt;SPI数据寄存器（SPI_DR）：双寄存器，包含发送缓存与接收缓存&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;NOR Flash （W25Q256）&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;NOR Flash 是一种非易失性存储器，广泛应用于嵌入式系统中，尤其是在需要快速读取数据的场景。它与 NAND Flash 的主要区别在于存储单元的结构和读写性能。NOR Flash 使用并行的地址和数据总线，其存储单元是基于 NOR 门（一个逻辑门）。这种结构使得它能够按字节（Byte）直接读取数据。由于 NOR Flash 支持按字节随机访问，因此在读取速度上优于 NAND Flash。它适合于存储固件、代码等经常读取的应用。其写入操作较慢，通常需要先将整个块（block）擦除后才能进行写入。每个单元的擦写次数有限，通常为数万到数十万次，因此在需要频繁写入的场景中，NOR Flash 的耐用性较差。NOR Flash 主要应用于嵌入式系统，如存储引导程序、固件（BIOS）等。它也广泛应用于汽车、智能设备、消费电子等领域，用作存储启动代码、配置数据等。对于低功耗设备，NOR Flash 适合用于电池供电且不频繁写入的应用。总的来说，NOR Flash 主要优势是其快速的读取能力，适合存储代码和配置等经常读取的内容，而写入和擦除操作较为缓慢。&lt;/p&gt;
&lt;p&gt;NOR与NAND在数据写入前都需要有&lt;strong&gt;擦除&lt;/strong&gt;操作。&lt;/p&gt;
&lt;p&gt;NM25Q128 支持标准的 SPI，还支持双输出/四输出的 SPI，最大 SPI 时钟可以到 104Mhz（双输出时相当于 208Mhz，四输出时相当于 416M）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/spi1.webp&quot; alt=&quot;W25Q256 IC引脚&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;CS：片选引脚，低电平有效&lt;/li&gt;
&lt;li&gt;DO：MISO引脚&lt;/li&gt;
&lt;li&gt;WP：写保护引脚。高电平时可读可写，低电平时只读&lt;/li&gt;
&lt;li&gt;DI：MOSI引脚&lt;/li&gt;
&lt;li&gt;CLK：串行时钟引脚&lt;/li&gt;
&lt;li&gt;HOLD：保持引脚，低电平有效&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对于W25Q256的存储架构而言，从大到小可以分为这么几级：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;256个块（Block），每个64K字节
&lt;ul&gt;
&lt;li&gt;每个块分为16个扇区（Sector），每个4K字节
&lt;ul&gt;
&lt;li&gt;每个扇区分为16个页（Page），每个256字节&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;W25Q256的最小擦除单位为一个扇区，也就是每次必须擦除4K个字节。这样我们需要给W25Q256开辟一个至少4K字节的缓存区，这样对SRAM要求比较高，要求芯片必须有4K以上的SRAM才能很好的操作。&lt;/p&gt;
&lt;h3&gt;状态寄存器（Status Register）&lt;/h3&gt;
&lt;p&gt;W25Q256拥有3个状态寄存器，分别为SR1，SR2和SR3.具体作用可参见手册7.1节。下面是比较重要的一些寄存器位：
&lt;img src=&quot;/images/arm/spi5.webp&quot; alt=&quot;SR1&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;S0：BUSY位，只读。置1表示设备忙（有操作正在进行），置0表示设备空闲，准备好接收其他指令。&lt;/li&gt;
&lt;li&gt;S1：写使能锁存器（WEL），只读。置1表示写使能，置0表示写禁用。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/spi6.webp&quot; alt=&quot;SR3&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;S17：ADP位。置0时，设备启动将会引导至3字节地址模式，该模式下若要访问128Mb以外的flash则必须启用附加地址寄存器（Extended Addr Reg）；置1时，设备启动引导至4字节地址模式。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;指令与工作时序&lt;/h3&gt;
&lt;h4&gt;写使能（Write Enable）- 06H&lt;/h4&gt;
&lt;p&gt;写使能指令用于将SR1的WEL位置1.该指令必须在所有页操作（Page Program）、扇区擦除、块擦除、芯片擦除和写入状态寄存器操作前调用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/spi6.webp&quot; alt=&quot;写使能指令时序&quot; /&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /**
* @brief IC 写使能 Write Enable
* @param NULL
* @retval NULL
*/
void w25q256_write_enable(void)
{
    NORFLASH_CS(0);     

    uint8_t code = 0x06;
    HAL_SPI_Transmit(&amp;amp;hspi5, (uint8_t *)&amp;amp;code, 1, 1000);

    NORFLASH_CS(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;写禁用（Write Disable） - 04H&lt;/h4&gt;
&lt;p&gt;该指令会将SR1-WEL置0.一般情况下，WEL位会在所有页操作（Page Program）、扇区擦除、块擦除、芯片擦除、写入状态寄存器操作和IC复位后自动置为0.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /**
* @brief IC 关闭写使能 Write Disable
* @param NULL
* @retval NULL
*/
void w25q256_write_disable(void)
{
    NORFLASH_CS(0);

    uint8_t code = 0x04;
    HAL_SPI_Transmit(&amp;amp;hspi5, (uint8_t *)&amp;amp;code, 1, 1000);

    NORFLASH_CS(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读取状态寄存器 - 05H/35H/15H&lt;/h4&gt;
&lt;p&gt;该指令用于读取状态寄存器中的值，&lt;code&gt;05H&lt;/code&gt;用于读取SR1，&lt;code&gt;35H&lt;/code&gt;读取SR2，&lt;code&gt;15H&lt;/code&gt;读取SR3.该指令可以在任何时候被调用，通过位操作用于确认BUSY位是否为0（&lt;code&gt;&amp;amp;0x01&lt;/code&gt;），以确认IC是否准备好接收另一条指令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* @brief 读取IC状态寄存器 Read Status Register
 * @note        SR-1
 *              BIT7  6   5   4   3   2   1   0
 *              SPR   RV  TB BP2 BP1 BP0 WEL BUSY
 *              SPR:默认为0，保护位，配合WP使用
 *              TB,BP2,BP1,BP0:FLASH区域写保护设置
 *              WEL:写使能锁定
 *              BUSY:忙标记为（1：忙；0：空闲）
 *              默认:0x00
 *
 *              SR-2
 *              BIT7  6   5   4   3   2   1   0
 *              SUS   CMP LB3 LB2 LB1 (R) QE  SRP1
 *
 *              SR-3
 *              BIT7      6    5    4   3   2   1   0
 *              HOLD/RST  DRV1 DRV0 (R) (R) WPS ADP ADS
* @param sr:状态寄存器编号
* @retval rxbyte：接收到的8位寄存器值
*/
uint8_t w25q256_read_status_reg(uint8_t sr)
{
    uint8_t rxbyte, readsr = 0;

    NORFLASH_CS(0);

    switch (sr)
    {
        case 1:
            readsr = 0x05;      /* SR1读指令 */
            break;
        
        case 2:
            readsr = 0x35;      /* SR2读指令 */
            break;
        
        case 3:
            readsr = 0x15;      /* SR3读指令 */
            break;
    }

    HAL_SPI_Transmit(&amp;amp;hspi5, (uint8_t *)&amp;amp;readsr, sizeof(readsr), 1000);   /* 主机发送对应指令 */
    HAL_SPI_Receive(&amp;amp;hspi5, (uint8_t *)&amp;amp;rxbyte, sizeof(rxbyte), 1000);

    NORFLASH_CS(1);

    return rxbyte;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/**
 * @brief       等待空闲
 * @param       NULL
 * @retval      NULL
 */
static void norflash_wait_busy(void)
{
    while ((w25q256_read_status_reg(1) &amp;amp; 0x01) == 0x01);               /* 等待BUSY位清空 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;进入/退出4字节地址模式 - B7H/E9H&lt;/h4&gt;
&lt;p&gt;4字节地址启用时，存入/读取数据时寻址地址为32位而非24位，可以访问128Mb以外的Flash。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
* @brief 进入4字节地址模式 Enter 4-Byte Address Mode
* @note 允许使用32位地址以访问128Mb以外区域
* @param NULL
* @retval NULL
*/
void w25q256_enter_4byte_mode(void)
{
    uint8_t code = 0xB7;

    NORFLASH_CS(0);
    HAL_SPI_Transmit(&amp;amp;hspi5, &amp;amp;code, 1, 1000);
    NORFLASH_CS(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt; /**
* @brief 退出4字节地址模式 Exit 4-Byte Address Mode
* @note 非4字节地址模式下要访问128Mb以外区域需要使用拓展地址寄存器（Extended Addr Reg）
* @param NULL
* @retval NULL
*/
void w25q256_exit_4byte_mode(void)
{
    uint8_t code = 0xE9;

    NORFLASH_CS(0);
    HAL_SPI_Transmit(&amp;amp;hspi5, &amp;amp;code, 1, 1000);
    NORFLASH_CS(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;读操作（32位地址模式） - 13H&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/spi2.webp&quot; alt=&quot;W25Q256 读操作（32位地址模式）时序&quot; /&gt;
读操作时序包含以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;拉低CS电平，激活片选&lt;/li&gt;
&lt;li&gt;MOSI发送13H，然后发送待读取32位地址&lt;/li&gt;
&lt;li&gt;从机在CLK下降沿从MISO发送对应地址的数据&lt;/li&gt;
&lt;li&gt;拉高CS电平，传输停止&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt; /**
* @brief 读取数据(32位地址模式) Read Data(4Byte Addr Mode)
* @note 参见datasheet 8.2.10
* @param pbuf:接收缓存区地址 
* @param addr：目标地址
* @param datalen：接收长度（字节）
* @retval NULL
*/
void w25q256_read_data_4byte_mode(uint8_t *pbuf, uint32_t addr, uint16_t datalen)
{
    uint8_t read_data_code = 0x13;

    NORFLASH_CS(0);
    HAL_SPI_Transmit(&amp;amp;hspi5, &amp;amp;read_data_code, 1, 1000);     /* 发送32位模式读指令 */
    HAL_SPI_Transmit(&amp;amp;hspi5, (uint8_t *) &amp;amp;addr, 4, 1000);   /* 发送32位目标内存地址 */
    HAL_SPI_Receive(&amp;amp;hspi5, pbuf, datalen, 1000);   /* 接收读取值 */
    NORFLASH_CS(1);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;页编程（Page Program） - 02H&lt;/h4&gt;
&lt;p&gt;&lt;em&gt;参见&lt;/em&gt;：datasheet 8.2.25
&lt;img src=&quot;/images/arm/spi3.webp&quot; alt=&quot;W25Q256 页写操作时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;页面编程指令允许在先前擦除的 (FFh) 内存位置对 1 字节到 256 字节（一页）的数据进行编程。必须先执行写使能指令，设备才会接受页面编程指令（WEL= 1）。&lt;/p&gt;
&lt;p&gt;如果要编程整个 256 字节页面，则最后一个地址字节（8 个最低有效地址位）应设置为 0。如果最后一个地址字节不为零，并且时钟数超过剩余页面长度，则寻址将绕回到页面的开头。在某些情况下，可以编程少于 256 个字节（部分页面），而不会对同一页面内的其他字节产生任何影响。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：执行部分页面编程的一个条件是时钟数不能超过剩余的页长度。如果向设备发送了超过 256 个字节，寻址将绕回到页面的开头并覆盖先前发送的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/spi4.webp&quot; alt=&quot;W25Q256 扇区擦除时序&quot; /&gt;
W25Q256扇区大小为4K字节，擦除扇区后所有扇区位全部置1，即扇区字节为&lt;code&gt;FFH&lt;/code&gt;。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主机发送“写使能”指令&lt;/li&gt;
&lt;li&gt;确定SPI总线状态：读取状态寄存器的BUSY位，等待BUSY位为0后，继续下一步&lt;/li&gt;
&lt;li&gt;主机拉低CS引脚&lt;/li&gt;
&lt;li&gt;主机从MOSI发送&lt;code&gt;20H&lt;/code&gt;到从机，然后发送24位扇区地址&lt;/li&gt;
&lt;li&gt;主机拉高CS引脚，读取寄存器状态，等待扇区擦除完成&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;W25Q256还有整个IC擦除的操作，只需发送&lt;code&gt;C7H&lt;/code&gt;到IC即可实现整片擦除。&lt;/p&gt;
</content:encoded></item><item><title>STM32中的I2C通信【AP3216C光传感器】</title><link>https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84i2c%E9%80%9A%E4%BF%A1ap3216c%E5%85%89%E4%BC%A0%E6%84%9F%E5%99%A8/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84i2c%E9%80%9A%E4%BF%A1ap3216c%E5%85%89%E4%BC%A0%E6%84%9F%E5%99%A8/</guid><description>AP3216C 是敦南科技推出的一款三合一环境传感器， 它包含了：数字环境光传感器（ALS）、接近传感器（PS）和一个红外 LED（IR）。该芯片通过 IIC 接口和 MCU 连接，并支持中断（INT）输出。AP3216C 的特点如下：…</description><pubDate>Fri, 13 Dec 2024 11:48:39 GMT</pubDate><content:encoded>&lt;h2&gt;AP3216C&lt;/h2&gt;
&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;AP3216C 是敦南科技推出的一款三合一环境传感器， 它包含了：数字环境光传感器（ALS）、接近传感器（PS）和一个红外 LED（IR）。该芯片通过 IIC 接口和 MCU 连接，并支持中断（INT）输出。AP3216C 的特点如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IIC 接口，支持高达 400KHz 通信速率&lt;/li&gt;
&lt;li&gt;支持多种工作模式（ALS、PS+IR、ALS+PS+IR 等）&lt;/li&gt;
&lt;li&gt;内置温度补偿电路&lt;/li&gt;
&lt;li&gt;工作温度支持-30~80℃&lt;/li&gt;
&lt;li&gt;环境光传感器具有 16 位分辨率&lt;/li&gt;
&lt;li&gt;接近传感器具有 10 位分辨率&lt;/li&gt;
&lt;li&gt;红外传感器具有 10 位分辨率&lt;/li&gt;
&lt;li&gt;超小封装（4.1&lt;em&gt;2.4&lt;/em&gt;1.35mm）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因为以上一些特性，AP3216C 被广泛应用于智能手机上面，用来检测光强度（自动背光控制），和接近开关控制（听筒靠近耳朵，手机自动灭屏功能）。&lt;/p&gt;
&lt;h3&gt;写寄存器&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/3216c1.webp&quot; alt=&quot;AP3216C写寄存器时序&quot; /&gt;
先发送 AP3216C 的地址（7 位，为&lt;code&gt;0X1E&lt;/code&gt;，左移一位后为 &lt;code&gt;0X3C&lt;/code&gt;），最低位 W=0 表示写数据，随后发送 8 位寄存器地址，最后发送 8 位寄存器值。其中：S，表示 IIC 起始信号；W，表示读/写标志位（W=0 表示写，W=1 表示读）；A，表示应答信号；P，表示 IIC 停止信号。&lt;/p&gt;
&lt;h3&gt;读寄存器&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/3216c2.webp&quot; alt=&quot;AP3216C寄存器说明&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：AP3216C的读取间隔至少需要大于112.5ms（一次ALS+PS+IR转换的时间）&lt;/p&gt;
&lt;h2&gt;工程&lt;/h2&gt;
&lt;h3&gt;ap3216c.h&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt; #include &quot;sys.h&quot;
 #include &quot;main.h&quot;

 #ifndef __AP3216C_H
 #define __AP3216C_H

 #define AP3216C_ADDR 0x3C  /* 原设备地址为0x1E，左移一位为0x3C*/

 #endif

 uint8_t ap3216c_init(void);
 uint8_t ap3216c_write_one_byte(uint8_t reg, uint8_t data);
 uint8_t ap3216c_read_one_byte(uint8_t reg);
 void ap3216c_read_data(uint16_t *ir, uint16_t *ps, uint16_t *als);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ap3216c.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;main.h&quot;
 #include &quot;sys.h&quot;
 #include &quot;ap3216c.h&quot;
 #include &quot;custom_i2c.h&quot;
 #include &quot;delay.h&quot;

 /**
* @brief 初始化 AP3216C
* @param 无
* @retval 返回值:0,初始化成功; 1,初始化失败
*/
uint8_t ap3216c_init(void)
{
    uint8_t temp = 0;
    iic_init();

    ap3216c_write_one_byte(0x00, 0x04);     /* 软复位AP3216C */
    delay_ms(50);                           /* IC复位至少需要11.5ms */
    ap3216c_write_one_byte(0x00, 0x03);     /* ALS+PS+IR功能激活 */
    temp = ap3216c_read_one_byte(0x00);     /* 读取刚刚写入的0x03指令 */
    if (temp == 0x03) return 0;             /* IC初始化成功 */
    else return 1;                          /* IC初始化失败 */
}

/**
* @brief AP3216C 写入一个字节
* @param reg : AP3216C目标寄存器地址
* @param data : 待写入数据
* @retval 0, 成功;
          1, 失败;
*/
uint8_t ap3216c_write_one_byte(uint8_t reg, uint8_t data)
{
    iic_start();
    
    iic_send_byte(AP3216C_ADDR | 0x00);     /* 发送设备地址，最低位为0，写命令 */

    if (iic_wait_ack())     /* 等待应答*/
    {
        iic_stop();         /* 等待应答失败则直接停机 */
        return 1;
    }

    iic_send_byte(reg);     /* 发送目标寄存器地址（指针）*/
    iic_wait_ack();         /* 等待应答 */
    iic_send_byte(data);    /* 发送待写入数据 */

    if (iic_wait_ack())     /* 等待应答*/
    {
        iic_stop();         /* 等待应答失败则直接停机 */
        return 1;
    }

    iic_stop();
    return 0;
}

/**
* @brief AP3216C 读取一个字节
* @param reg : 待读取寄存器地址
* @retval 读到的数据
*/
uint8_t ap3216c_read_one_byte(uint8_t reg)
{
    uint8_t result;

    iic_start();

    iic_send_byte(AP3216C_ADDR | 0x00);     /* 发送设备地址，最低位为0，写命令*/
    iic_wait_ack();

    iic_send_byte(reg);     /* 发送待读取寄存器地址 */
    iic_wait_ack();

    iic_start();            /* 准备开始读取，重新启动IIC以切换模式 */
    iic_send_byte(AP3216C_ADDR | 0x01);     /* 发送设备地址，最低位为1，读命令*/
    iic_wait_ack();
    result = iic_read_byte(0);              /* 读取一个字节，读完后发送NACK，表示读取结束 */
    iic_stop();
    
    return result;      /* 返回读取结果 */
}

/**
* @brief 读取 AP3216C 的数据
* @note 读取原始数据，包括 ALS,PS 和 IR
* 如果同时打开 ALS,IR+PS 的话两次数据读取的时间间隔要大于 112.5ms
* @param ir : IR 传感器值
* @param ps : PS 传感器值
* @param als : ALS 传感器值
* @retval 无
*/
void ap3216c_read_data(uint16_t *ir, uint16_t *ps, uint16_t *als)
{
    uint8_t rx_buf[6];      /* 读取缓冲区 */
    uint8_t i;

    for (i = 0; i &amp;lt; 6; i++) /* 循环6次以读取6个寄存器的值 */
    {
        rx_buf[i] = ap3216c_read_one_byte(0x0A + i);
    }

    /* 提取IR传感器值 */
    if (rx_buf[0] &amp;amp; 0x80)   /* 判断IR第7位是否为1 */
    {
        *ir = 0;            /* 第7位若为1则IR数据无效 */
    }
    else
    {
        /* rx_buf[1] &amp;lt;&amp;lt; 2：0x0B（IR高位）数据左移2位，给低位数据留出空余*/
        /* rx_buf[0] &amp;amp; 0x03：提取0x0A（IR低位）数据的第0位和第1位，也就是IR的最低2位*/
        /* 二者进行或运算得到IR的最终值 */
        *ir = ((uint16_t)rx_buf[1] &amp;lt;&amp;lt; 2) | (rx_buf[0] &amp;amp; 0x03);  
    }

    /* 提取ALS传感器值 */
    *als = rx_buf[2] | ((uint16_t)rx_buf[3] &amp;lt;&amp;lt; 8);

    /* 提取PS传感器值 */
    if (rx_buf[4] &amp;amp; 0x40)   /* 判断0x0E第6位是否为1*/
    {
        *ps = 0;            /* PS数据无效 */
    }
    else
    {
        /* (rx_buf[4] &amp;amp; 0x0F)：取0x0E的3:0位，也就是PS的低4位 */
        /* ((uint16_t)(rx_buf[5] &amp;amp; 0x3F) &amp;lt;&amp;lt; 4):取0x0F的5:0位然后左移4位，给低4位留位置 */
        /* 二者进行或运算得到PS的最终值 */
        *ps = ((uint16_t)(rx_buf[5] &amp;amp; 0x3F) &amp;lt;&amp;lt; 4) | (rx_buf[4] &amp;amp; 0x0F);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;main.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  /* USER CODE BEGIN 2 */
  while (ap3216c_init())    /* AP3216C初始化失败 */
  {
    uint8_t txbuf[] = &quot;ap3216c init failed!\n&quot;;
    HAL_UART_Transmit_IT(&amp;amp;huart1, txbuf, sizeof(txbuf));
    led_red(1);
  }
  /* USER CODE END 2 */
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;while (1)
  {
    /* USER CODE END WHILE */
    ap3216c_read_data(&amp;amp;ir, &amp;amp;ps, &amp;amp;als);

    uint8_t txbuf[23];

    sprintf(txbuf, &quot;ir:%d, ps:%d, als:%d\r\n&quot;, ir, ps, als);
    HAL_UART_Transmit_IT(&amp;amp;huart1, (uint8_t *)txbuf, sizeof(txbuf));

    led_green_toggle();
    delay_ms(120);    /* 延时120ms以保证AP3216C采样正确 */
    /* USER CODE BEGIN 3 */

  }
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;测试&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/3216c3.webp&quot; alt=&quot;串口打印结果&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>RNN：原理、组成与简单实现</title><link>https://arkmostima.pages.dev/posts/rnn%E5%8E%9F%E7%90%86%E7%BB%84%E6%88%90%E4%B8%8E%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/rnn%E5%8E%9F%E7%90%86%E7%BB%84%E6%88%90%E4%B8%8E%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0/</guid><description>循环神经网络（RNN）最早可以追溯到1980年代末，当时的研究者希望设计一种能够处理时间序列数据或具有时序依赖关系的数据的神经网络模型。RNN的设计灵感来自生物神经网络的工作原理，它模拟了大脑神经元的反馈机制，通过递归连接来捕捉数据中前…</description><pubDate>Thu, 12 Dec 2024 06:30:54 GMT</pubDate><content:encoded>&lt;h2&gt;传统RNN&lt;/h2&gt;
&lt;p&gt;循环神经网络（RNN）最早可以追溯到1980年代末，当时的研究者希望设计一种能够处理时间序列数据或具有时序依赖关系的数据的神经网络模型。RNN的设计灵感来自生物神经网络的工作原理，它模拟了大脑神经元的反馈机制，通过递归连接来捕捉数据中前后时刻的依赖关系。&lt;/p&gt;
&lt;h3&gt;历史&lt;/h3&gt;
&lt;p&gt;RNN的初期发展可追溯到1986年，David Rumelhart 和 Geoffrey Hinton 等人提出了反向传播算法，并在此基础上构建了简单的RNN模型。最初的RNN能够通过训练学习输入数据的时序模式。RNN 跟传统神经网络最大的区别在于每次都会将前一次的输出结果，带到下一次的隐藏层中，一起训练。&lt;/p&gt;
&lt;h3&gt;工作原理&lt;/h3&gt;
&lt;p&gt;以一个最简单的语序为例：用户输入了一句“what time is it”，首先需要对这句话进行分词：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/rnn1.webp&quot; alt=&quot;第一步：分词&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将分词结果按顺序输入RNN。首先输入“what”：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/rnn2.webp&quot; alt=&quot;第二步：输入&amp;quot;what&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;按顺序输入剩下的分词，第三步输入“time”。按照RNN的结构，输入&quot;time&quot;时，之前输入的&quot;what&quot;会对RNN的输出产生影响（隐藏层中有一半是黑色的）&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/rnn3.webp&quot; alt=&quot;第三步：输入&amp;quot;time&amp;quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;以此类推，每一个历史输入都会对未来输出产生影响，直观显示为每一个圆形隐藏层中都包含了之前所有历史输入所指代的颜色。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/rnn4.webp&quot; alt=&quot;第四步：历史输入会影响未来输出&quot; /&gt;&lt;/p&gt;
&lt;p&gt;最后需要输出结果（此处是判断这句话的意图）时，只需要输出最后一层的结果。&lt;/p&gt;
&lt;h3&gt;组成和基本结构&lt;/h3&gt;
&lt;p&gt;RNN的核心组成部分是&lt;strong&gt;循环结构&lt;/strong&gt;，它使得网络能够记住之前时刻的信息，并通过这种“记忆”来影响当前时刻的计算。RNN的基本结构可以通过以下几个部分来理解：&lt;/p&gt;
&lt;h4&gt;1. 输入层（Input Layer）&lt;/h4&gt;
&lt;p&gt;输入序列数据，通常表示为一个时间步的输入向量 $ x_t $。例如，在自然语言处理任务中，输入可以是一个词向量；在时间序列预测任务中，输入可以是某一时刻的传感器数据。&lt;/p&gt;
&lt;h4&gt;2. 隐层（Hidden Layer）&lt;/h4&gt;
&lt;p&gt;隐藏层由多个神经元组成，每个神经元的输出不仅受到当前输入的影响，还受到前一时刻隐层状态的影响。该层的计算过程可以表示为：
$$
h_t = f(W_{xh}x_t + W_{hh}h_{t-1} + b_h)
$$
其中，$ h_t $ 是当前时刻的隐层状态，$ x_t $ 是当前输入，$ h_{t-1} $ 是前一时刻的隐层状态，$ W_{xh} $ 和 $W_{hh} $ 是权重矩阵，$ b_h $ 是偏置项，$ f $ 是激活函数（如tanh或ReLU）。&lt;/p&gt;
&lt;h4&gt;3. 输出层（Output Layer）&lt;/h4&gt;
&lt;p&gt;输出层用于生成预测结果，通常可以表示为：
$$
y_t = g(W_{hy}h_t + b_y)
$$
其中，$ y_t $ 是当前时刻的输出，$ W_{hy} $ 是从隐层到输出的权重矩阵，$ b_y $ 是输出层的偏置项，$ g $ 是输出的激活函数（例如softmax或sigmoid，取决于任务）。&lt;/p&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;p&gt;RNN在时间序列数据处理中的优势是能够&lt;strong&gt;捕捉时间序列的动态变化和时序依赖关系&lt;/strong&gt;，因此它特别适用于处理和预测具有时序特征的数据。&lt;/p&gt;
&lt;p&gt;RNN的缺点也很明显，即短期的记忆影响较大，长期的记忆影响较小。若整个时间序列非常长，RNN甚至可能丢失很久之前的历史输入。同时，训练RNN也需要大量的成本，并且很容易带来梯度消失和梯度爆炸问题。这就引出了后来的变种：门控循环单元（GRU）与长短期记忆网络（LSTM）。&lt;/p&gt;
&lt;h2&gt;门控循环单元（GRU）&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;参考文献：Cho, K., Van Merriënboer, B., Bahdanau, D., &amp;amp; Bengio, Y. (2014). On the properties of neural machine translation: encoder-decoder approaches. arXiv preprint arXiv:1409.1259.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;来源&lt;/h3&gt;
&lt;p&gt;GRU被用于解决传统RNN的梯度异常问题。这种梯度异常问题在序列问题中是非常常见的，比如：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;早期观测值对预测所有未来观测值具有非常重要的意义。 考虑一个极端情况，其中第一个观测值包含一个校验和， 目标是在序列的末尾辨别校验和是否正确。 在这种情况下，第一个词元的影响至关重要。 我们希望有某些机制能够在一个记忆元里存储重要的早期信息。 如果没有这样的机制，我们将不得不给这个观测值指定一个非常大的梯度， 因为它会影响所有后续的观测值。&lt;/li&gt;
&lt;li&gt;一些特征或现象没有相关的观测值。 例如，在对网页内容进行情感分析时， 可能有一些辅助HTML代码与网页传达的情绪无关。 我们希望有一些机制来跳过隐状态表示中的此类词元。&lt;/li&gt;
&lt;li&gt;序列的各个部分之间存在逻辑中断。 例如，书的章节之间可能会有过渡存在， 或者证券的熊市和牛市之间可能会有过渡存在。 在这种情况下，最好有一种方法来重置我们的内部状态表示。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;工作原理&lt;/h3&gt;
&lt;p&gt;GRU与传统RNN的关键区别在于，GRU支持隐状态的门控。这意味着模型有专门的机制来确定应该何时更新隐状态， 以及应该何时重置隐状态。 这些机制是可学习的，并且能够解决了上面列出的问题。 例如，如果第一个序列数据变化现象非常重要（如发电机故障中匝间短路的那一瞬间）， 模型将学会在第一次观测之后不更新隐状态。同样，模型也可以学会跳过不相关的临时观测。最后，模型还将学会在需要的时候重置隐状态。&lt;/p&gt;
&lt;h4&gt;重置门（Reset Gate）&lt;/h4&gt;
&lt;p&gt;重置门用于决定当前时刻的输入数据在计算当前状态时对之前状态的影响程度。即它决定在计算新的候选隐藏状态时，前一时刻的隐层状态应该有多大的影响。&lt;/p&gt;
&lt;p&gt;公式表示为：
$$
r_t = \sigma(W_r x_t + U_r h_{t-1} + b_r)
$$
其中，$r_t$ 是重置门的输出，$W_r$ 和 $U_r$ 是权重矩阵，$x_t$ 是当前时刻的输入，$h_{t-1}$ 是前一时刻的隐藏状态，$b_r$ 是偏置项，$\sigma$ 是sigmoid激活函数。&lt;/p&gt;
&lt;p&gt;当 $r_t$ 接近0时，表示前一时刻的隐藏状态对当前时刻的影响很小，网络将“重置”之前的记忆。&lt;/p&gt;
&lt;h4&gt;更新门（Update Gate）&lt;/h4&gt;
&lt;p&gt;更新门控制着当前时刻的隐藏状态应该如何更新，它决定了当前时刻的输出应保留多少来自上一时刻的状态，多少来自当前输入的候选隐藏状态。更新门的值接近1时，意味着当前时刻的隐藏状态保留更多来自前一时刻的记忆；而接近0时，意味着更多的依赖于当前输入。&lt;/p&gt;
&lt;p&gt;公式表示为：
$$
z_t = \sigma(W_z x_t + U_z h_{t-1} + b_z)
$$
其中，$z_t$ 是更新门的输出，$W_z$ 和 $U_z$ 是权重矩阵，$x_t$ 是当前时刻的输入，$h_{t-1}$ 是前一时刻的隐藏状态，$b_z$ 是偏置项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/gru-1.svg&quot; alt=&quot;GRU中计算重置门和更新门&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;候选隐藏状态（Candidate Hidden State）&lt;/h4&gt;
&lt;p&gt;候选隐藏状态是GRU网络根据当前输入和重置门的作用计算出来的，表示网络在当前时刻希望更新的“记忆”。它是基于当前输入和前一时刻的记忆状态计算的。&lt;/p&gt;
&lt;p&gt;公式表示为：
$$
\tilde{h}&lt;em&gt;t = \tanh(W_h x_t + U_h (r_t \cdot h&lt;/em&gt;{t-1}) + b_h)
$$
其中，$\tilde{h}_t$ 是候选隐藏状态，$W_h$ 和 $U_h$ 是权重矩阵，$r_t$ 是重置门的输出，$\cdot$ 表示按元素相乘（Hadamard积），$\tanh$ 是tanh激活函数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/gru-2.svg&quot; alt=&quot;GRU中计算候选隐藏状态&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;最终隐藏状态（Final Hidden State）&lt;/h4&gt;
&lt;p&gt;最终的隐藏状态是当前时刻候选隐藏状态和上一时刻的隐藏状态的加权平均。加权系数由更新门 $z_t$ 控制，表示信息应该更新多少，保留多少。&lt;/p&gt;
&lt;p&gt;公式表示为：
$$
h_t = (1 - z_t) \cdot \tilde{h}&lt;em&gt;t + z_t \cdot h&lt;/em&gt;{t-1}
$$
其中，$h_t$ 是当前时刻的最终隐藏状态，$\tilde{h}&lt;em&gt;t$ 是候选隐藏状态，$z_t$ 是更新门，$h&lt;/em&gt;{t-1}$ 是前一时刻的隐藏状态。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/gru-3.svg&quot; alt=&quot;GRU中计算最终隐藏状态&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;简单应用&lt;/h3&gt;
&lt;p&gt;GRU十分适合用于时间序列数据的特征识别、提取和预测，尤其是较长时间内的时间序列。
下面是一个用于预测未来一段时间内天气状况的模型，其使用五个GRU层，用于对四个不同变量（最高温、最低温、降水量与风速）进行回归任务。数据集来自于&lt;a href=&quot;https://www.kaggle.com/datasets/ananthr1/weather-prediction&quot;&gt;Kaggle&lt;/a&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, GRU, Dense, Dropout
import tensorflow.keras.backend as K
import matplotlib.pyplot as plt

# 1. 加载CSV文件，读取日期数据，并转换为日期格式
df = pd.read_csv(&apos;seattle-weather.csv&apos;, parse_dates=[&apos;date&apos;])

# 2. 处理缺失值，使用前向填充（前一个有效值填充）
df.fillna(method=&apos;ffill&apos;, inplace=True)

# 3. 规范化数值特征
# 使用 MinMaxScaler 将数据缩放到 0 和 1 之间
scaler = MinMaxScaler()
scaled_features = [&apos;precipitation&apos;, &apos;temp_max&apos;, &apos;temp_min&apos;, &apos;wind&apos;]  # 需要规范化的特征
df[scaled_features] = scaler.fit_transform(df[scaled_features])

# 4. 创建时间序列数据：输入X是过去window_size天的特征，y是目标列（temp_max, temp_min, precipitation, wind）
def create_sequences(df, window_size, target_columns):
    X, y = [], []
    for i in range(len(df) - window_size):
        # 过去 window_size 天的数据作为输入
        X.append(df.iloc[i:i + window_size][scaled_features].values)
        # 当前天的目标特征作为输出
        y.append(df.iloc[i + window_size][target_columns].values)  # 多目标回归
    return np.array(X), np.array(y)

# 设置时间窗口为15天，目标列为气温、降水量和风速
window_size = 15
target_columns = [&apos;temp_max&apos;, &apos;temp_min&apos;, &apos;precipitation&apos;, &apos;wind&apos;]  # 目标列：温度、降水量和风速
X, y = create_sequences(df, window_size, target_columns)

# 5. 划分训练集和测试集，采用80%的数据用于训练，20%用于测试
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False)

# 6. 转换数据格式
X_train, y_train = np.array(X_train), np.array(y_train)
X_test, y_test = np.array(X_test), np.array(y_test)

# 将目标值数据类型转换为 float32，以便模型训练
y_train = y_train.astype(np.float32)
y_test = y_test.astype(np.float32)

# 输出训练集和测试集的形状，确保数据格式正确
print(&quot;X_train shape:&quot;, X_train.shape)
print(&quot;X_test shape:&quot;, X_test.shape)
print(&quot;y_train shape:&quot;, y_train.shape)
print(&quot;y_test shape:&quot;, y_test.shape)

# 7. 构建共享底层GRU网络 + 多目标回归模型

# 输入层，形状为 (window_size, 4)，即过去15天的4个特征
input_layer = Input(shape=(window_size, len(scaled_features)))

# 构建多层GRU网络，每层后面都添加Dropout层，以防止过拟合
x = GRU(256, return_sequences=True)(input_layer)  # 第1层GRU，返回所有时间步的输出
x = Dropout(0.2)(x)  # Dropout，丢弃20%的神经元

x = GRU(256, return_sequences=True)(x)  # 第2层
x = Dropout(0.2)(x)  # Dropout

x = GRU(128, return_sequences=True)(x)  # 第3层
x = Dropout(0.2)(x)  # Dropout

x = GRU(128, return_sequences=True)(x)  # 第4层
x = Dropout(0.2)(x)  # Dropout

x = GRU(64)(x)  # 第5层，只返回最后一个时间步的输出
x = Dropout(0.2)(x)  # Dropout

# 4个目标列的独立输出层，每个输出层预测一个目标
temp_max_output = Dense(1, name=&apos;temp_max&apos;)(x)  # 输出温度最大值
temp_min_output = Dense(1, name=&apos;temp_min&apos;)(x)  # 输出温度最小值
precipitation_output = Dense(1, name=&apos;precipitation&apos;)(x)  # 输出降水量
wind_output = Dense(1, name=&apos;wind&apos;)(x)  # 输出风速

# 定义模型，输入为输入层，输出为4个目标特征
model = Model(inputs=input_layer, outputs=[temp_max_output, temp_min_output, precipitation_output, wind_output])

# 8. 编译模型

# 定义加权损失函数
def weighted_loss(y_true, y_pred):
    # 为每个目标特征设置权重
    temp_max_weight = 1.0
    temp_min_weight = 1.0
    precipitation_weight = 10.0  # 为降水增加权重
    wind_weight = 50.0  # 为风速增加权重

    # 计算加权的损失
    loss = K.mean(K.square(y_true - y_pred), axis=-1)  # 计算均方误差
    weights = K.constant([temp_max_weight, temp_min_weight, precipitation_weight, wind_weight])

    return K.mean(loss * weights, axis=-1)  # 计算加权后的损失

# 编译模型，使用Adam优化器和均方误差作为损失函数
model.compile(optimizer=&apos;adam&apos;,
              loss=&apos;mean_squared_error&apos;,  # 对每个输出使用均方误差
              metrics={  # 为每个目标列定义MAE和MSE指标
                  &apos;temp_max&apos;: [&apos;mae&apos;, &apos;mse&apos;],         # 针对 temp_max 输出定义 MAE 和 MSE
                  &apos;temp_min&apos;: [&apos;mae&apos;, &apos;mse&apos;],         # 针对 temp_min 输出定义 MAE 和 MSE
                  &apos;precipitation&apos;: [&apos;mae&apos;, &apos;mse&apos;],    # 针对 precipitation 输出定义 MAE 和 MSE
                  &apos;wind&apos;: [&apos;mae&apos;, &apos;mse&apos;]              # 针对 wind 输出定义 MAE 和 MSE
              })

# 9. 训练模型
history = model.fit(X_train, [y_train[:, 0], y_train[:, 1], y_train[:, 2], y_train[:, 3]],
                    epochs=150, batch_size=32,  # 设置训练的轮次和每批次的样本数
                    validation_data=(X_test, [y_test[:, 0], y_test[:, 1], y_test[:, 2], y_test[:, 3]]))  # 验证集数据

# 10. 模型评估，返回损失值和每个目标特征的评估指标
results = model.evaluate(X_test, [y_test[:, 0], y_test[:, 1], y_test[:, 2], y_test[:, 3]])

# results 返回的是一个列表，包含了损失值和各个目标特征的MAE和MSE
test_loss = results[0]
test_mae_temp_max = results[2]  # temp_max 对应的 MAE
test_mae_temp_min = results[4]  # temp_min 对应的 MAE
test_mae_precipitation = results[6]  # precipitation 对应的 MAE
test_mae_wind = results[8]  # wind 对应的 MAE

# 输出评估结果
print(f&apos;Test Loss (MSE): {test_loss:.4f}&apos;)
print(f&apos;Test MAE for temp_max: {test_mae_temp_max:.4f}&apos;)
print(f&apos;Test MAE for temp_min: {test_mae_temp_min:.4f}&apos;)
print(f&apos;Test MAE for precipitation: {test_mae_precipitation:.4f}&apos;)
print(f&apos;Test MAE for wind: {test_mae_wind:.4f}&apos;)

# 11. 可视化训练过程
plt.figure(figsize=(12, 6))

# 绘制总体 Loss（MSE）
plt.subplot(1, 2, 1)
plt.plot(history.history[&apos;loss&apos;], label=&apos;Train Loss&apos;)
plt.plot(history.history[&apos;val_loss&apos;], label=&apos;Val Loss&apos;)
plt.title(&apos;Model Loss (MSE)&apos;)
plt.xlabel(&apos;Epochs&apos;)
plt.ylabel(&apos;Loss&apos;)
plt.legend()

# 绘制每个输出的 MAE
plt.subplot(1, 2, 2)
plt.plot(history.history[&apos;temp_max_mae&apos;], label=&apos;Train temp_max MAE&apos;)
plt.plot(history.history[&apos;val_temp_max_mae&apos;], label=&apos;Val temp_max MAE&apos;)
plt.plot(history.history[&apos;temp_min_mae&apos;], label=&apos;Train temp_min MAE&apos;)
plt.plot(history.history[&apos;val_temp_min_mae&apos;], label=&apos;Val temp_min MAE&apos;)
plt.plot(history.history[&apos;precipitation_mae&apos;], label=&apos;Train precipitation MAE&apos;)
plt.plot(history.history[&apos;val_precipitation_mae&apos;], label=&apos;Val precipitation MAE&apos;)
plt.plot(history.history[&apos;wind_mae&apos;], label=&apos;Train wind MAE&apos;)
plt.plot(history.history[&apos;val_wind_mae&apos;], label=&apos;Val wind MAE&apos;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个模型本质上执行了一个多变量回归任务，其中引入了weighted_loss函数，其通过为每个目标特征设置不同的权重，调整它们在总损失中的影响力，使得模型在训练时能够更加关注某些特征的预测准确性。这对于多目标回归任务尤为重要，特别是在特征的重要性或尺度差异较大的情况下，有助于提高模型的预测性能。&lt;/p&gt;
&lt;p&gt;模型在经过粗略调试之后输出了以下结果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/weather1.webp&quot; alt=&quot;MSE和MAE指标随迭代次数变化&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/DL/weather2.webp&quot; alt=&quot;预测结果与实际结果对比&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可以看出对于最高气温和最低气温，模型给出了非常好的结果。这也跟GRU的特性有关，因为其能尽可能多地捕捉到变量与时间的关系。而对于那些变化和时间不呈非常明显关系的变量————比如降水量和风速————模型给出的结果并不是非常尽人意，即使这两个变量已经被添加了相当高的权重。尤其是降水量，其突变次数和趋势明显高于其他几个变量，模型也无法做出比较好的计算结果。&lt;/p&gt;
&lt;p&gt;这只是一个非常简单的应用，如果结合其他方法，预测准确度应当能达到一个比较高的值。&lt;/p&gt;
</content:encoded></item><item><title>STM32中的I2C通信【24CXX EEPROM】</title><link>https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84i2c%E9%80%9A%E4%BF%A124cxx-eeprom/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/stm32%E4%B8%AD%E7%9A%84i2c%E9%80%9A%E4%BF%A124cxx-eeprom/</guid><description>I2C是由数据线SDA和时钟线SCL构成的串行总线，用于发送或接收数据。其中SDA用于传输数据。SCL用于同步时钟信号。I2C还有以下特征： 1. 主从架构 主机（Master）：控制通信过程，生成时钟信号并启动通信 从机（Slave）…</description><pubDate>Sat, 07 Dec 2024 12:41:46 GMT</pubDate><content:encoded>&lt;h2&gt;I2C简介&lt;/h2&gt;
&lt;p&gt;I2C是由数据线SDA和时钟线SCL构成的串行总线，用于发送或接收数据。其中SDA用于传输数据。SCL用于同步时钟信号。I2C还有以下特征：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主从架构
&lt;ul&gt;
&lt;li&gt;主机（Master）：控制通信过程，生成时钟信号并启动通信&lt;/li&gt;
&lt;li&gt;从机（Slave）：响应主机指令，按地址匹配参与通信&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;地址：每个从设备具有唯一的7位或10位地址，用于识别通信目标。主设备在通信开始时发送从设备地址，从设备响应后开始数据传输&lt;/li&gt;
&lt;li&gt;半双工通信： 数据线支持双向通信，但同一时刻只允许数据单向传输&lt;/li&gt;
&lt;li&gt;数据速率：
&lt;ul&gt;
&lt;li&gt;标准模式（Standard Mode）：最大速率100 kbps&lt;/li&gt;
&lt;li&gt;快速模式（Fast Mode）：最大速率400 kbps&lt;/li&gt;
&lt;li&gt;高速模式（High-Speed Mode）：最大速率3.4 Mbps&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;开漏设计：信号线通常采用开漏驱动，需要外部上拉电阻将信号线拉高到逻辑高电平。因此总线空闲时，SDA和DCL都为高电平。&lt;/li&gt;
&lt;li&gt;多设备连接：可以有多个具备 IIC 通信能力的设备挂载在上面，同时支持多个主机和多个从机，连接到总线的接口数量只由总线电容 400pF 的限制决定。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;I2C时序和读写操作&lt;/h2&gt;
&lt;h3&gt;时序信号&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/i2c1.webp&quot; alt=&quot;I2C总线时序图&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;起始信号：主机发出，为电平跳变信号而非恒电平信号。SCL为高电平期间，SDA由高电平跳变至低电平，总线被占用，准备数据传输&lt;/li&gt;
&lt;li&gt;停止信号：主机发出，为电平跳变信号而非恒电平信号。SCL为高电平期间，SDA由低电平跳变至高电平，总线空闲&lt;/li&gt;
&lt;li&gt;应答信号：发送器每发送一个字节，就在时钟脉冲 9 期间释放数据线，由接收器反馈一个应答信号。应答信号为低电平时，规定为有效应答位（ACK 简称应答位），表示接收器已经成功地接收了该字节。应答信号为高电平时，规定为非应答位（NACK），一般表示接收器接收该字节没有成功&lt;/li&gt;
&lt;li&gt;数据有效性：总线进行数据传送时，时钟信号为高电平期间，数据线上的数据必须保持稳定，只有在时钟线上的信号为低电平期间，数据线上的高电平或低电平状态才允许变化。数据在 SCL 的上升沿到来之前就需准备好。并在下降沿到来之前必须稳定&lt;/li&gt;
&lt;li&gt;数据传输：总线上传送的每一位数据都有一个时钟脉冲相对应（或同步控制），即在 SCL 串行时钟的配合下，在 SDA 上逐位地串行传送每一位数据。数据位的传输是边沿触发&lt;/li&gt;
&lt;li&gt;空闲状态：SDA 和 SCL两条信号线同时处于高电平时，规定为总线的空闲状态。此时各个器件的输出级场效应管均处在截止状态，即释放总线，由两条信号线各自的上拉电阻把电平拉高&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;写操作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/i2c2.webp&quot; alt=&quot;I2C写操作通讯&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主机发送起始信号，令总线上的所有从机等待接收数据&lt;/li&gt;
&lt;li&gt;主机发送从机地址+&apos;0&apos;（写操作）组成的8位数据。从机接收到地址后，比对该地址是否为本机地址。若为本机地址，从机发送应答信号&lt;/li&gt;
&lt;li&gt;主机向从机发送数据&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;读操作&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/i2c3.webp&quot; alt=&quot;I2C读操作通讯&quot; /&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;主机发送起始信号，令总线上的所有从机等待接收数据&lt;/li&gt;
&lt;li&gt;主机发送从机地址+&apos;1&apos;（读操作）组成的8位数据。从机接收到地址后，比对该地址是否为本机地址。若为本机地址，从机发送应答信号&lt;/li&gt;
&lt;li&gt;从机向主机发送数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若主机一直返回应答信号，那么从机可以一直发送数据，直到主机发送NACK信号为止。&lt;/p&gt;
&lt;h2&gt;24C02时序&lt;/h2&gt;
&lt;p&gt;24C02 是一个 2K bit 的串行 EEPROM 存储器，内部含有 256 个字节。在 24C02 里面还有一个 8 字节的页写缓冲器。该设备的通信方式为 IIC，通过其 SCL 和 SDA 与其他设备通信。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c021.webp&quot; alt=&quot;24C02引脚&quot; /&gt;&lt;/p&gt;
&lt;p&gt;WP为写保护引脚，高电平只读，低电平开放读写功能。24C02的设备地址共8位，包含不可编程部分和可编程部分，可编程部分根据硬件Pin A0、A1、A2决定；设备地址最后一位用于设置是读操作还是写操作。具体为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c022.webp&quot; alt=&quot;24C02设备地址格式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;本文中A0、A1、A2均接地，故24C02设备读操作地址为&lt;code&gt;0xA1&lt;/code&gt;，写操作地址为&lt;code&gt;0xA0&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;24C02读写操作&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c023.webp&quot; alt=&quot;24C02写时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;主机在 IIC 总线发送第 1 个字节的数据为24C02的设备地址&lt;code&gt;0xA0&lt;/code&gt;，用于寻找总线上的24C02，在获得24C02的应答信号之后，继续发送第 2 个字节数据，该字节数据是 24C02 的内存地址，再等到 24C02 的应答信号，主机继续发送第 3 字节数据，这里的数据即是写入在第 2 字节内存地址的数据。主机完成写操作后，可以发出停止信号，终止数据传输。这种写操作每次只能写入1字节数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c024.webp&quot; alt=&quot;24C02页写时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;写操作时，24C02可以使用页写时序，其和普通写时序的区别是页写时序只需要告知一次内存地址1，后面的数据会按照写入顺序存入内存地址2、内存地址3等，节省通信时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c025.webp&quot; alt=&quot;24C02读时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;24C02读取数据的过程是一个复合的时序，其中包含写时序和读时序。通常第一个通信过程为写时序，起始信号产生后，主机发送24C02设备地址&lt;code&gt;0xA0&lt;/code&gt;，获取从机应答信号后，接着发送需要读取的内存地址；在随后的读时序中，起始信号产生后，主机发送24C02设备地址&lt;code&gt;0xA1&lt;/code&gt;, 获取从机应答信号后，从机返回刚刚在写时序中传递的内存地址的数据，以字节为单位传输在总线上，假如主机获取数据后返回的是应答信号，那么从机会一直传输数据，当主机发出的是非应答信号并以停止信号发出为结束，从机结束传输。&lt;/p&gt;
&lt;h2&gt;软件模拟I2C&lt;/h2&gt;
&lt;p&gt;HAL库自带的硬件I2C函数比较复杂，多采用软件模拟I2C的方式操作GPIO以获得I2C时序。&lt;/p&gt;
&lt;h3&gt;custom_i2c.h（头文件）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
 ****************************************************************************************************
 * @file        custom_i2c.h
 * @author      Aki
 * @version     V1.0
 * @date        2024-12-11
 * @brief       软件IIC头文件
 ****************************************************************************************************
 */

#ifndef __MYIIC_H
#define __MYIIC_H

#include &quot;sys.h&quot;

/******************************************************************************************/
/* 引脚定义 */

#define IIC_SCL_GPIO_PORT               GPIOH
#define IIC_SCL_GPIO_PIN                GPIO_PIN_4
#define IIC_SCL_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   /* PH口时钟使能 */

#define IIC_SDA_GPIO_PORT               GPIOH
#define IIC_SDA_GPIO_PIN                GPIO_PIN_5
#define IIC_SDA_GPIO_CLK_ENABLE()       do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)   /* PH口时钟使能 */

/******************************************************************************************/
/* IO操作 */

#define IIC_SCL(x)        do{ x ? \
                              HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_SET) : \
                              HAL_GPIO_WritePin(IIC_SCL_GPIO_PORT, IIC_SCL_GPIO_PIN, GPIO_PIN_RESET); \
                          }while(0)       /* SCL */

#define IIC_SDA(x)        do{ x ? \
                              HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_SET) : \
                              HAL_GPIO_WritePin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN, GPIO_PIN_RESET); \
                          }while(0)       /* SDA */

#define IIC_READ_SDA     HAL_GPIO_ReadPin(IIC_SDA_GPIO_PORT, IIC_SDA_GPIO_PIN)        /* 读取SDA电平 */

/******************************************************************************************/

void iic_init(void);                        
void iic_start(void);                       
void iic_stop(void);                      
void iic_ack(void);                        
void iic_nack(void);                       
uint8_t iic_wait_ack(void);                 
void iic_send_byte(uint8_t data);          
uint8_t iic_read_byte(unsigned char ack);   

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里宏定义利用了两个小技巧：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;三目运算符：x ? ... : ... 是一个条件（三目）运算符。它的工作原理是：根据 x 的值（x 是一个表达式），判断执行哪一部分代码。如果 x 为 true（非零），执行冒号前的表达式；如果 x 为 false（零），执行冒号后的表达式。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;do { ... } while(0)&lt;/code&gt;：确保宏只执行一次且不会干扰外部代码结构的惯用法。这样做可以确保宏在使用时，代码不会因缺少括号而引发语法错误。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;custom_i2c.c&lt;/h3&gt;
&lt;h4&gt;初始化&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:IIC初始化
    @param:NULL
    @return:NULL
**/
void iic_init(void)
{
    GPIO_InitTypeDef gpio_init_struct;

    IIC_SCL_GPIO_CLK_ENABLE();  /* SCL引脚时钟使能 */
    IIC_SDA_GPIO_CLK_ENABLE();  /* SDA引脚时钟使能 */

    /* SCL */
    gpio_init_struct.Pin = IIC_SCL_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;    /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;            /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    HAL_GPIO_Init(IIC_SCL_GPIO_PORT, &amp;amp;gpio_init_struct);

    /* SDA */
    gpio_init_struct.Pin = IIC_SDA_GPIO_PIN;
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_OD;    /* 开漏输出 */
    HAL_GPIO_Init(IIC_SDA_GPIO_PORT, &amp;amp;gpio_init_struct);

    iic_stop();     /* 停止总线上所有设备 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里SDA线使用开漏输出，这意味着当SDA引脚输出低电平时，设备直接将线拉低（输出低电平）；而当设备输出高电平时，它并不直接将线拉高，而是将SDA引脚设置为高阻态（即不驱动该引脚。当一个设备希望发送数据时，如果它需要输出0（低电平），它会直接将SDA引脚拉低。当它需要发送1（高电平），它不会直接将SDA引脚拉高，而是让SDA引脚处于高阻态，让总线上的其他设备（如果有）通过拉高SDA线来实现高电平。这么做可以有效避免连接多个从机时发生电平冲突。STM32F429使用开漏模式时，必须外界上拉电阻。&lt;/p&gt;
&lt;h4&gt;延时&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:IIC延时函数
    @param:NULL
    @return:NULL
**/
static void iic_delay(void)
{
    delay_us(2);    /* 读写速度在250Khz以内 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为获得稳定的读写结果，需限制读写速度，此处通过延时2us的形式实现。&lt;/p&gt;
&lt;h4&gt;起始信号与停止信号&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:IIC起始信号
    @param:NULL
    @return:NULL
**/
void iic_start(void)
{
    IIC_SDA(1);
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(0);   /* START信号 */
    iic_delay();
    IIC_SCL(0);   /* 钳住总线，准备发送或接收数据（SCL只有低电平期间允许改变SDA状态） */
    iic_delay();
}

/** 
    @brief:IIC停止信号
    @param:NULL
    @return:NULL
**/
void iic_stop(void)
{
    IIC_SDA(0);     /* STOP信号 */
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SDA(1);     /* 总线结束信号 */
    iic_delay();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/i2c4.webp&quot; alt=&quot;START和STOP时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据IIC时序图，起始信号和停止信号分别为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;START：SCL高电平，SDA从1跳变至0&lt;/li&gt;
&lt;li&gt;STOP：SCL高电平，SDA从0跳变至1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上面的两个函数实现的就是这个功能，同时加入了延时来保证能得到稳定的电平。需要注意，停止信号发出时，SCL需要在SDA电平开始跳变前就保持高电平。&lt;/p&gt;
&lt;h4&gt;发送字节&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:IIC发送一个字节
    @param:data：要发送的数据
    @return:NULL
**/
void iic_send_byte(uint8_t data)
{
    uint8_t t;

    for (t = 0; t &amp;lt; 8; t++)
    {
        IIC_SDA((data &amp;amp; 0x80) &amp;gt;&amp;gt; 7);    /* 提取高位 */
        iic_delay();
        IIC_SCL(1);     /* 拉高SCL并维持高电平，开始发送 */
        iic_delay();
        IIC_SCL(0);     /* 发送结束，拉低SCL，准备下一位数据 */
        data &amp;lt;&amp;lt;= 1;     /* 左移1位，循环发送 */
    }
    IIC_SDA(1);     /* 发送完成，释放SDA线 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/i2c5.webp&quot; alt=&quot;I2C发送时序&quot; /&gt;&lt;/p&gt;
&lt;p&gt;发送字节时，由于一个时钟周期内I2C只能发送一位(bit)数据，因此发送需要循环8次，模拟8个时钟信号，才能把形参的8位数据都发送出去。也就是我们需要提取出形参的每一位并发送。这里使用了&lt;code&gt;(data &amp;amp; 0x80) &amp;gt;&amp;gt; 7&lt;/code&gt;的方式。&lt;code&gt;(data &amp;amp; 0x80)&lt;/code&gt;是将形参与&lt;code&gt;0x80&lt;/code&gt;，也就是&lt;code&gt;10000000&lt;/code&gt;进行与运算，然后右移7位，提取出形参的最高位。如果最高位是1，那么SDA就拉高；反之SDA则拉低。每个周期完成后，data左移1位，将原先是次高位的数移到最高位，重复这个过程8次。全部发送完成后，释放SDA线。&lt;/p&gt;
&lt;h4&gt;读取字节&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:IIC读取一个字节
    @param:ack: ack=1时，发送ack; ack=0时，发送nack
    @return:接收到的数据
**/
uint8_t iic_read_byte(uint8_t ack)
{
    uint8_t i, receive = 0x00;

    /* 1个字节共8位，单次接收1位，循环8次 */
    for (i = 0; i &amp;lt; 8; i++)
    {
        receive &amp;lt;&amp;lt;= 1;      /* 输出时高位先输出，因此接收时先收到的数据是高位，要左移 */
        IIC_SCL(1);         /* 拉高SCL，SDA准备接收 */
        iic_delay();

        if (IIC_READ_SDA)   /* 如果SDA为高电平 */
        {
            receive++;      /* 第0位（低位）置1 */
        }

        IIC_SCL(0);         /* 接收结束，拉低SCL */
        iic_delay();
    }

    if (!ack)
    {
        iic_nack();         /* 发送nACK信号 */
    }
    else
    {
        iic_ack();          /* 发送ACK信号 */
    }

    return receive;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在时序方面，I2C读取字节和写入字节是一致的。&lt;/p&gt;
&lt;h4&gt;应答信号（ACK）&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:等待应答信号
    @param:NULL
    @return:1=失败；0=成功
**/
uint8_t iic_wait_ack(void)
{
    uint8_t waittime = 0;
    uint8_t rack = 0;

    IIC_SDA(1);     /* 主机释放SDA（外部器件此时可拉低SDA电平） */
    iic_delay();
    IIC_SCL(1);     /* 拉高SCL，从机此时可返回ACK */
    iic_delay();

    while (IIC_READ_SDA)    /* 等待应答 */
    {
        waittime++;

        if (waittime &amp;gt; 250)
        {
            iic_stop();
            rack = 1;
            break;
        }
    }

    IIC_SCL(0);     /* SCL=0，结束ACK检查 */
    iic_delay();
    return rack;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等待应答信号一般用在写时序中，在&lt;code&gt;iic_send_byte&lt;/code&gt;后调用。当读取到SDA为低电平时，表示ACK信号；SDA为高电平则为NACK信号。若等待超时，则主机会直接发出停止信号。若正常接收到ACK信号，主机拉低SCL线，返回flag。这一段内容对应总线时序图中的红圈3，也就是脉冲9。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/** 
    @brief:产生ACK应答
    @param:NULL
    @return:NULL
    @note：ACK应答时，SDA拉低，SCL=0-&amp;gt;1-&amp;gt;0
**/
void iic_ack(void)
{
    IIC_SDA(0);     
    iic_delay();
    IIC_SCL(1);     
    iic_delay();
    IIC_SCL(0);     
    iic_delay();
    IIC_SDA(1);     
    iic_delay();
}

/** 
    @brief:不产生ACK应答（NACK）
    @param:NULL
    @return:NULL
    @note：NACK应答时，SDA拉高，SCL=0-&amp;gt;1-&amp;gt;0
**/
void iic_nack(void)
{
    IIC_SDA(1);
    iic_delay();
    IIC_SCL(1);
    iic_delay();
    IIC_SCL(0);
    iic_delay();
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上两个函数用于主机作为接收端时，在接收完数据后向从机返回ACK或NACK信号。二者对应的也是线时序图中的红圈3，也就是脉冲9。&lt;/p&gt;
&lt;h2&gt;24CXX EEPROM 驱动&lt;/h2&gt;
&lt;h3&gt;写操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
* @brief 向 AT24CXX 指定地址写入一个数据
* @param addr: 写入数据的目的地址
* @param data: 要写入的数据
* @retval 无
*/
void at24cxx_write_one_byte(uint16_t addr, uint8_t data)
{
    iic_start();       /* IIC起始信号 */

    if (EE_TYPE &amp;gt; AT24C16)  /* 容量大于24C16时分2个字节发送目标内存地址 */
    {
        iic_send_byte(0xA0);    /* 发送写命令，从机设备地址为0xA0，最低位为0表示写入 */
        iic_wait_ack();         /* 发送完一个字节后等待ACK */
        iic_send_byte(addr &amp;gt;&amp;gt; 8);   /* 发送高位内存地址 */
    }
    else
    {
        iic_send_byte(0xA0 + ((addr &amp;gt;&amp;gt; 8) &amp;lt;&amp;lt; 1));   /* 发送器件0xA0 + 高位a8/a9/a10地址，写数据 */
    }

    iic_wait_ack();
    iic_send_byte(addr % 256);  /* 发送低位内存地址 */
    iic_wait_ack();

    iic_send_byte(data);    /* 发送1个字节 */
    iic_wait_ack();
    iic_stop();
    delay_ms(10);       /* EEPROM写入速度慢，必须等待写入完成再写下一个 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;24CXX的写操作主要包含三个步骤：写入设备地址，写入目标内存地址，写入待传输数据。设备地址之前已经介绍过，高7位为固定地址，低1位为&lt;code&gt;&apos;0&apos;&lt;/code&gt;时表示写操作。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;若EEPROM容量大于24C16，比如24C32和24C64，EEPROM总的字节数分别为4096和8192，对应寻址线为12根和13根，即内存地址是12位和13位的，显然发送内存地址时就需要分高字节和低字节两次发送。&lt;/li&gt;
&lt;li&gt;若EEPROM容量小于或等于24C16，则寻址线最多为11根，内存地址最多是11位的。通过&lt;code&gt;iic_send_byte(0xA0 + ((addr &amp;gt;&amp;gt; 8) &amp;lt;&amp;lt; 1))&lt;/code&gt;，可以让主机下发读写命令时自带3位高地址，剩下的8位地址只需要合在一起在低位发送即可，也就是&lt;code&gt;iic_send_byte(addr % 256)&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种设备地址低位和内存地址高位相结合的设计可以看作是EEPROM和独特设计，用来节省资源。&lt;/p&gt;
&lt;h3&gt;读操作&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
* @brief 在 AT24CXX 指定地址写入一个数据
* @param addr: 写入数据的目的地址
* @param data: 要写入的数据
* @retval 无
*/
uint8_t at24cxx_read_one_byte(uint16_t addr)
{
    uint8_t temp = 0;
    iic_start();

    /* 根据不同的 24CXX 型号, 发送高位地址
    * 1, 24C16 以上的型号, 分 2 个字节发送地址
    * 2, 24C16 及以下的型号, 分 1 个低字节地址 + 占用器件地址的 bit1 ~ bit3 位
    * 用于表示高位地址, 最多 11 位地址
    * 对于 24C01/02, 其器件地址格式(8bit)为: 1 0 1 0 A2 A1 A0 R/W
    * 对于 24C04, 其器件地址格式(8bit)为: 1 0 1 0 A2 A1 a8 R/W
    * 对于 24C08, 其器件地址格式(8bit)为: 1 0 1 0 A2 a9 a8 R/W
    * 对于 24C16, 其器件地址格式(8bit)为: 1 0 1 0 a10 a9 a8 R/W
    * R/W : 读/写控制位 0,表示写; 1,表示读;
    * A0/A1/A2 : 对应器件的 1,2,3 引脚(只有 24C01/02/04/8 有这些脚)
    * a8/a9/a10: 对应存储整列的高位地址, 11bit 地址最多可以表示 2048 个位置,
    * 可以寻址 24C16 及以内的型号
    */

   if (EE_TYPE &amp;gt; AT24C16)       /* 24C16以上型号分2个字节发送地址 */
   {
        iic_send_byte(0xA0);    /* 最低位为0，表示写入*/
        iic_wait_ack();         /* 发送完一个字节后等待ACK信号 */
        iic_send_byte(addr &amp;gt;&amp;gt; 8);       /* 发送高字节地址 */
   }
   else
   {
        /* 发送器件0xA0 + 高位a8/a9/a10地址，写数据 */
        iic_send_byte(0xA0 + ((addr &amp;gt;&amp;gt; 8) &amp;lt;&amp;lt; 1));
   }

   iic_wait_ack();
   iic_send_byte(addr % 256);       /* 发送低位地址*/
   iic_wait_ack();

   iic_start();                     /* 重新发送起始信号*/
   iic_send_byte(0xA1);             /* 进入接收模式，最低位为1，表示读取*/
   iic_wait_ack();
   temp = iic_read_byte(0);         /* 接收一个字节数据 */
   iic_stop();

   return temp;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;iic_send_byte(addr % 256)&lt;/code&gt;用来提取内存地址的低8位。也可以用&lt;code&gt;addr &amp;amp;= 0x00FF&lt;/code&gt;来代替。&lt;/p&gt;
&lt;h3&gt;检测工作状态&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/**
* @brief 检查 AT24CXX 是否正常
* @note 检测原理: 在器件的末地址写入 0X55, 然后再读取, 
* 如果读取值为 0X55 则表示检测正常. 否则,则表示检测失败.
* @param 无
* @retval 检测结果
* 0: 检测成功
* 1: 检测失败
*/
uint8_t at24cxx_check(void)
{
    uint8_t temp;
    uint16_t addr = EE_TYPE;                /* 目标内存地址为EEPROM末地址 */
    temp = at24cxx_read_one_byte(addr);     /* 避免每次开机都写入*/

    if (temp == 0x55)                       /* 读取数据正常 */
    {
        return 0;
    }
    else                                    /* 第一次初始化时 */
    {
        at24cxx_write_one_byte(addr, 0x55); /* 先写入 */
        temp = at24cxx_read_one_byte(255);  /* 再读取 */
        if (temp == 0x55) return 0;
    }

    return 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种操作和RTC实验很类似，利用EEPROM掉电后内存不丢失的特性，固定在第一次写入时于内存末地址写入&lt;code&gt;0x55&lt;/code&gt;，然后再去读一下看看是否写入成功，以此来检测芯片是否正常工作。&lt;/p&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;p&gt;实现一个简单的应用：按下KEY1，向EEPROM的首地址写入字符串，写入成功串口打印“write ok”；按下KEY0，读取EEPROM首地址内容，串口打印读取结果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 2 */
  while (at24cxx_check())
  {
    led_red_toggle();
    delay_ms(500);
  }
  /* USER CODE END 2 */

/* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    uint8_t key;
    uint8_t writeok[] = &quot;write ok\n&quot;;
    uint8_t txbuf[] = &quot;mua\n&quot;;
    uint8_t rxbuf[5];
    uint8_t readok[15];

    key = key_scan(0);

    if (key == KEY1_pressed)    /* 如果KEY1被按下 */
    {
      at24cxx_write(0, txbuf, sizeof(txbuf));
      HAL_UART_Transmit_IT(&amp;amp;huart1, writeok, sizeof(writeok));
      delay_ms(50);
    }

    if (key == KEY0_pressed)    /* 如果KEY0被按下 */
    {
      at24cxx_read(0, rxbuf, sizeof(txbuf));
      //sprintf(readok, &quot;result is %d\n&quot;, rxbuf[0]);
      HAL_UART_Transmit_IT(&amp;amp;huart1, rxbuf, sizeof(rxbuf));
    }

    led_green(1);
    delay_ms(10);
  }
  /* USER CODE END 3 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/24c026.webp&quot; alt=&quot;串口打印结果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/AkiChen891/STM32F429-24C02-EEPROM-TEST&quot;&gt;Github项目地址&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;总的来说，通过I2C操作EEPROM时的要点有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;写入时：先写入设备地址+写操作指示符，然后写入目标内存地址，最后写入待写入数据&lt;/li&gt;
&lt;li&gt;读出时：起始信号，先写入设备地址+写操作指示符，从机ACK后写入目标内存地址；然后重新发出起始信号（&lt;strong&gt;因为需要从写模式切换到读模式&lt;/strong&gt;），写入设备地址+读操作指示符，从机ACK后读出待读数据。读取完毕后，主机发送NACK，提示从机读取结束&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>STM32F4 内部温度传感器（Internal Temperature Sensor）</title><link>https://arkmostima.pages.dev/posts/stm32%E5%86%85%E9%83%A8%E6%B8%A9%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%E9%87%87%E9%9B%86/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/stm32%E5%86%85%E9%83%A8%E6%B8%A9%E5%BA%A6%E4%BC%A0%E6%84%9F%E5%99%A8%E9%87%87%E9%9B%86/</guid><description>STM32F4系列MCU内置有温度传感器，用于采集片内温度，其绑定在ADC1的IN18通道，CubeMX中显示为&quot;Temperature sensor channel&quot;。</description><pubDate>Wed, 04 Dec 2024 07:43:45 GMT</pubDate><content:encoded>&lt;p&gt;STM32F4系列MCU内置有温度传感器，用于采集片内温度，其绑定在ADC1的IN18通道，CubeMX中显示为&quot;Temperature sensor channel&quot;。&lt;/p&gt;
&lt;h2&gt;ADC配置&lt;/h2&gt;
&lt;p&gt;ADC的结构体&lt;code&gt;hadc1&lt;/code&gt;配置如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.ScanConvMode = DISABLE;
  hadc1.Init.ContinuousConvMode = DISABLE;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.NbrOfConversion = 1;
  hadc1.Init.DMAContinuousRequests = DISABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClockPrescaler&lt;/code&gt;：预分频系数，默认为&lt;code&gt;PCLK2 / 4&lt;/code&gt;，此例为&lt;code&gt;90 / 4 = 22.5&lt;/code&gt;MHz&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Resolution&lt;/code&gt;：ADC分辨率，可选有 12 位、10 位、8 位和 6 位。分辨率越高，转换数据精度越高，转换时间也越长；反之分辨率越低，转换数据精度越低，转换时间也越
短。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ScanConvMode&lt;/code&gt;：是否使用扫描。多通道启用，单通道禁用。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ContinuousConvMode&lt;/code&gt;：启用为自动连续转换，禁用为单次转换。如果设置为单次转换，那么每次转换后都要人工介入才能开启新的转换&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DiscontinuousConvMode&lt;/code&gt;：配置是否使用不连续的采样模式。该参数只有在&lt;code&gt;ScanConvMode&lt;/code&gt;启用，且&lt;code&gt;ContinuousConvMode&lt;/code&gt;关闭的情况下才有效&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExternalTrigConv&lt;/code&gt;：外部触发方式选择。若为软件触发则外部触发会关闭。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ExternalTrigConvEdge&lt;/code&gt;：外部触发极性选择（禁止触发检测、上升沿检测、下降沿检测或均可）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DataAlign&lt;/code&gt;：数据对齐方式，默认右对齐&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NbrOfConversion&lt;/code&gt;：常规转换通道数目，范围1~16&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DMAContinuousRequests&lt;/code&gt;：指定DMA请求是否以一次性模式执行（达到转换次数时DMA传输停止）或在连续模式下执行（DMA传输无限制，无论转换数量如何）。在连续模式下执行时，DMA必须配置在循环（Circular）模式，否则DMA传输缓冲区被填满时将发生溢出。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EOCSelection&lt;/code&gt;：指定转换结束后是否产生EOS中断或事件参数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;STM32F429的ADC总转换时间可以通过下式计算：&lt;/p&gt;
&lt;p&gt;$$
T_{conversion} = T_{sampling} + 12 \times T_{adc}
$$&lt;/p&gt;
&lt;p&gt;对于每个ADC通道，采样时间可以通过ADC_SMPR1（用于通道10-18）或ADC_SMPR2（用于通道0-9）寄存器的相应位来配置。例如，通道0的采样时间是通过ADC_SMPR2的SMPR2[2:0]位设置。采样时间也可在&lt;code&gt;adc.c&lt;/code&gt;中用代码定义，通常默认为3个ADC周期（SMP=000），可获得最快的采样速度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; /** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
  */
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  sConfig.Rank = 1;
  sConfig.SamplingTime = ADC_SAMPLETIME_3CYCLES;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;12个周期由ADC输入时钟ADC_CLK决定，其由APB2（ 即PCLK2）分频产生，在&lt;code&gt;ClockPrescaler&lt;/code&gt;中可修改。&lt;/p&gt;
&lt;p&gt;若采样时间为3个ADC周期，则ADC转换总时间为15个ADC周期，APB2时钟频率为90MHz，PCLK2/4分频后为22.5MHz，那么转换时间就是0.6667us。&lt;/p&gt;
&lt;h2&gt;DMA配置&lt;/h2&gt;
&lt;p&gt;启用DMA2，ADC1转换后的原始数据存储到16位缓冲区Rx_Buf中。由于ADC分辨率为12位，即采回的数据为12位，正好能够被16位缓冲区存下，便于后续观察和管理内存中的采样值。如果用32位缓冲区，一个区域会存两个采样值，不方便查看和后续的转换。&lt;/p&gt;
&lt;h2&gt;采样&lt;/h2&gt;
&lt;p&gt;ADC直接采样会存在跳变或抖动，通常需要进行一定的软件滤波，最常见的方法是简单平均法。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    @ brief:ADC采样并取平均
    @ param:times:平均次数
    @ return:计算得到的平均值
*/
uint16_t adc1_get_average_value(uint8_t times)
{
  uint16_t temp_value = 0;
  uint8_t t;

  /* ADC 配置在单次模式，每次采样都需要手动重启ADC服务 */
  HAL_ADC_Start_DMA(&amp;amp;hadc1, (uint32_t *) Rx_Buf, Rx_Buf_Size);  //强制转换Rx_Buf为32位以满足形参限制

  /* 采样值取平均 */
  for (t = 0; t &amp;lt; times; t++)
  {
    temp_value = temp_value + HAL_ADC_GetValue(&amp;amp;hadc1);
    HAL_Delay(5);
  }

  /* 返回平均值 */
  return temp_value / times;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因分辨率为12位，ADC每次采样返回的数值即为12位无符号整型（十进制0~4095）。DMA将会传输每一次的采样值至缓冲区，为了能够更好的查看每一次返回的数值，该例程事先将缓冲区定义成了uint16_t类型，这样缓冲区数组的每一个单元正好能够存下一个采样值。在调用ADC采样函数时，只需要将其声明为uint32_t类型指针，这样DMA会按4个字节（32位）来传输数据。DMA本身并不关心传输的采样值到底是几位的，他只会读取4个字节的数据然后传递至内存，因此，每两个 uint16_t 数据（即 2 个采样值）会合并成一个 uint32_t，在内存中形成连续的数据块。&lt;/p&gt;
&lt;p&gt;以此类推，如果采样值为8位(uint8_t)，那么DMA每次将会打包4个采样值为一个uint32_t数值传回同样定义为uint8_t的缓冲区然后解包，缓冲区中的每个单元都显示一个采样值。&lt;/p&gt;
&lt;p&gt;然后进行温度的读取。温度传感器换算公式为（单位为摄氏度）：
$$
T = ((V_{sense}-V_{25})/AvgSlope) + 25
$$
式中参数取典型值，有$V_{25}=0.76$，$AvgSlope = 2.5mv/℃$
采集温度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    @ brief:采集温度
    @ param:NULL
    @ return:温度值（short）
*/
double read_temperature(void)
{
  uint16_t adc_value;
  double temperature; //返回温度为双精度值

  adc_value = adc1_get_average_value(10);

  temperature = ((float)adc_value * 3.3 / 4096.0 - 0.76) / 0.0025 + 25.0;

  return temperature;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;main.c&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  ic_temp = read_temperature();

  uint8_t txbuf[15];
  sprintf(txbuf, &quot;temp is: %.2f\n&quot;, ic_temp); //转换为字符串

  HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)txbuf, strlen(txbuf), 0xFFFF);  //串口轮询发送温度值
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;结果&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/temp1.webp&quot; alt=&quot;上位机接收&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/temp2.webp&quot; alt=&quot;内存监视结果&quot; /&gt;&lt;/p&gt;
</content:encoded></item><item><title>STM32 UART在循环DMA模式下接收大量不定字长数据并进行乒乓缓存</title><link>https://arkmostima.pages.dev/posts/stm32-uart%E6%8E%A5%E6%94%B6%E4%B8%8D%E5%9B%BA%E5%AE%9A%E5%AD%97%E8%8A%82%E6%95%B0%E6%8D%AE/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/stm32-uart%E6%8E%A5%E6%94%B6%E4%B8%8D%E5%9B%BA%E5%AE%9A%E5%AD%97%E8%8A%82%E6%95%B0%E6%8D%AE/</guid><description>串口(uart)是一种低速的串行异步通信，适用于低速通信场景，通常使用的波特率小于或等于115200bps。</description><pubDate>Mon, 25 Nov 2024 07:14:21 GMT</pubDate><content:encoded>&lt;p&gt;串口(uart)是一种低速的串行异步通信，适用于低速通信场景，通常使用的波特率小于或等于115200bps。&lt;/p&gt;
&lt;p&gt;对于小于或者等于115200bps波特率的，而且数据量不大的通信场景，一般没必要使用DMA，或者说使用DMA并未能充分发挥出DMA的作用。&lt;/p&gt;
&lt;p&gt;对于数量大，或者波特率提高时，必须使用DMA以释放CPU资源，因为高波特率可能带来这样的问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对于发送，使用循环发送，可能阻塞线程，需要消耗大量CPU资源“搬运”数据，浪费CPU&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于发送，使用中断发送，不会阻塞线程，但需浪费大量中断资源，CPU频繁响应中断；以115200bps波特率，1s传输11520字节，大约69us需响应一次中断，如波特率再提高，将消耗更多CPU资源&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;对于接收，如仍采用传统的中断模式接收，同样会因为频繁中断导致消耗大量CPU资源
因此，高波特率场景下，串口非常有必要使用DMA。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;中断轮询模式(IT)&lt;/h2&gt;
&lt;p&gt;使用&lt;code&gt;HAL_UARTEx_ReceiveToIdle_IT&lt;/code&gt;函数，该函数会持续接收数据，直到缓冲区溢出或触发空闲事件。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * @brief Receive an amount of data in interrupt mode till either the expected number of data is received or an IDLE event occurs.
  * @note   Reception is initiated by this function call. Further progress of reception is achieved thanks
  *         to UART interrupts raised by RXNE and IDLE events. Callback is called at end of reception indicating
  *         number of received data elements.
  * @note   When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M = 01),
  *         the received data is handled as a set of uint16_t. In this case, Size must indicate the number
  *         of uint16_t available through pData.
  * @param huart UART handle.
  * @param pData Pointer to data buffer (uint8_t or uint16_t data elements).
  * @param Size  Amount of data elements (uint8_t or uint16_t) to be received.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  HAL_StatusTypeDef status;

  /* 检查没有正在进行的接收过程 */
  if (huart-&amp;gt;RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* 接收数据，直到空闲事件发生*/
    huart-&amp;gt;ReceptionType = HAL_UART_RECEPTION_TOIDLE;
    huart-&amp;gt;RxEventType = HAL_UART_RXEVENT_TC;

    status =  UART_Start_Receive_IT(huart, pData, Size);

    /* 检查接收是否成功开始 */
    if (status == HAL_OK)
    {
      if (huart-&amp;gt;ReceptionType == HAL_UART_RECEPTION_TOIDLE)
      {
        __HAL_UART_CLEAR_IDLEFLAG(huart);
        ATOMIC_SET_BIT(huart-&amp;gt;Instance-&amp;gt;CR1, USART_CR1_IDLEIE);
      }
      else
      {
        /* 如果在开始接收时已有错误待处理，则可能已引发中断并导致接收中止 */
        status = HAL_ERROR;
      }
    }

    return status;
  }
  else
  {
    return HAL_BUSY;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;main.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;  /* USER CODE BEGIN 2 */
  HAL_UARTEx_ReceiveToIdle_IT(&amp;amp;huart1, (uint8_t *)RxBuffer, RX_BUFFER_SIZE);
  
  /* USER CODE END 2 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;usart.c&lt;/h3&gt;
&lt;p&gt;每次接收完数据（指溢出或空闲后）进入该回调，将缓冲区数据传回上位机，然后重新准备下一次接收&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 1 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  //uint16_t index = Size;
  //HAL_UARTEx_ReceiveToIdle_IT(&amp;amp;huart1, (uint8_t *)RxBuffer, RX_BUFFER_SIZE);
  HAL_UART_Transmit_IT(&amp;amp;huart1, (uint8_t *)RxBuffer, sizeof(RxBuffer));
  HAL_UARTEx_ReceiveToIdle_IT(&amp;amp;huart1, (uint8_t *)RxBuffer, RX_BUFFER_SIZE);
}
/* USER CODE END 1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;DMA模式（少量数据）&lt;/h2&gt;
&lt;p&gt;DMA绕过CPU进行数据传输（外设-内存），因此可以节省CPU资源。DMA分为正常（Normal）模式和循环(Circular)模式，一般大规模数据传输时使用循环模式。
使用&lt;code&gt; HAL_UARTEx_ReceiveToIdle_DMA&lt;/code&gt;函数，仅需在最开始调用一次。当接收完成（达到指定字节数）或触发Idle事件时接收停止。每次接收事件完成后触发&lt;code&gt;HAL_UARTEx_RxEventCallback&lt;/code&gt;中断回调。&lt;/p&gt;
&lt;p&gt;应用要求：输入一段文字，若MCU成功接收则回复“Wilco”。&lt;/p&gt;
&lt;h3&gt;main.c&lt;/h3&gt;
&lt;p&gt;在loop前调用一次以启动接收：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HAL_UARTEx_ReceiveToIdle_DMA(&amp;amp;huart1, (uint8_t *)RxBuffer, RX_BUFFER_SIZE);
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;usart.c&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 1 */
int count = 0;  //接收次数
uint16_t index = 0; //接收数据量
uint8_t Wilco[] = &quot;wilco\n&quot;;  //发送缓冲区

/*
  @name: UART接收中断回调函数
*/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  index = Size;
  count++;
  HAL_UART_Transmit_IT(&amp;amp;huart1, (uint8_t *)Wilco, sizeof(Wilco));   //中断轮询发送以免DMA冲突
/*   for (int i = 0; i &amp;lt; RX_BUFFER_SIZE; i++) 
  {
    RxBuffer[i] = 0;  // 清空接收缓冲区（可选）
  } */
  HAL_UARTEx_ReceiveToIdle_DMA(&amp;amp;huart1, (uint8_t *)RxBuffer, RX_BUFFER_SIZE); //重新开启接收
}
/* USER CODE END 1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/dma1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;DMA模式（大量数据）与乒乓缓存&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;参考文献&lt;/em&gt;：&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.eet-china.com/mp/a28491.html&quot;&gt;一个严谨的STM32串口DMA发送&amp;amp;接收（1.5Mbps波特率）机制&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://openatomworkshop.csdn.net/6744016a3a01316874d7817c.html?dp_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6OTAzMTMsImV4cCI6MTczMzczMzA5MywiaWF0IjoxNzMzMTI4MjkzLCJ1c2VybmFtZSI6Im0wXzUxMTQ5NzUyIn0.iiWrRqQwXcnrR4HqKAlmtbAKIr9u0HJiBW-IMTE2tQ0&amp;amp;spm=1001.2101.3001.6650.1&amp;amp;utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7Ebaidujs_baidulandingword%7Eactivity-1-131576165-blog-110628262.235%5Ev43%5Epc_blog_bottom_relevance_base1&amp;amp;depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7Ebaidujs_baidulandingword%7Eactivity-1-131576165-blog-110628262.235%5Ev43%5Epc_blog_bottom_relevance_base1&amp;amp;utm_relevant_index=2&quot;&gt;STM32 HAL 库实现乒乓缓存加空闲中断的串口 DMA 收发机制，轻松跑上 2M 波特率&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/MaJerle/stm32-usart-uart-dma-rx-tx&quot;&gt;MaJerla(Github)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;DMA在循环模式下工作时，如果在大规模传输数据时仍旧空闲中断（或传输完成中断）会有风险，因为当DMA传输数据完成，CPU介入开始拷贝DMA通道缓冲区数据时，如果此时UART继续有数据进来，DMA继续搬运数据到缓冲区，就有可能将数据覆盖，因为DMA数据搬运是不受CPU控制的，即使你关闭了CPU中断。&lt;/p&gt;
&lt;p&gt;因此严谨的做法需要建立双buffer，CPU和DMA各自使用一块内存交替访问，即&lt;strong&gt;乒乓缓存&lt;/strong&gt;，处理流程为：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DMA先将数据搬运到buf1，搬运完成通知CPU来拷贝buf1数据&lt;/li&gt;
&lt;li&gt;DMA将数据搬运到buf2，与CPU拷贝buf1数据不会冲突&lt;/li&gt;
&lt;li&gt;buf2数据搬运完成，通知CPU来拷贝buf2数据&lt;/li&gt;
&lt;li&gt;DMA继续开始拷贝新数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;STM32大多数型号不提供现成的双缓存机制，但提供“半满中断”，即数据搬运到buf大小的一半时，可以产生一个中断信号。基于这个机制，我们可以实现双缓存功能，只需将buf空间开辟大一点即可。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DMA将数据搬运完成buf的前一半时，触发“半满中断”事件，Callback中通知CPU来拷贝buf前半部分数据&lt;/li&gt;
&lt;li&gt;DMA继续将数据搬运到buf的后半部分，与CPU拷贝buf前半部数据不会冲突&lt;/li&gt;
&lt;li&gt;buf后半部分数据搬运完成，触发“溢满中断”，Callback通知CPU来拷贝buf后半部分数据&lt;/li&gt;
&lt;li&gt;DMA循环拷贝新数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;基于上述描述机制，DMA方式接收串口数据，有三种中断场景需要CPU去将buf数据拷贝到final中，分别是：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DMA通道buf溢满（传输完成）场景，触发满溢中断（&lt;code&gt;HAL_UARTEx_RxEventCallback&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;DMA通道buf半满场景，触发半满中断（&lt;code&gt;HAL_UART_RxHalfCpltCallback&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;串口空闲中断场景，触发空闲中断（&lt;code&gt;UART_FLAG_IDLE&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/rxhalf.webp&quot; alt=&quot;三种中断事件场景&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/rxhalf1.webp&quot; alt=&quot;数据处理流程图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;也就是说，代码总共需要考虑以下几种情况：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;数据量未达到半满，触发&lt;strong&gt;空闲中断&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据量达到半满，未达到满溢，先触发&lt;strong&gt;半满中断&lt;/strong&gt;，后触发&lt;strong&gt;空闲中断&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据量刚好达到满溢，先触发&lt;strong&gt;半满中断&lt;/strong&gt;，后触发&lt;strong&gt;满溢中断&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;数据量大于缓冲区长度，DMA循环覆盖溢出的字节&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;对于情况1：在空闲中断中拷贝全部数据&lt;/p&gt;
&lt;p&gt;对于情况2：在半满中断中通知CPU拷贝一半的数据，DMA继续接收剩下的数据，最后在空闲中断中拷贝剩下的数据&lt;/p&gt;
&lt;p&gt;对于情况3：在半满中断中通知CPU拷贝一半的数据，DMA继续接收剩下的数据，最后在满溢中断中拷贝剩下的一半数据&lt;/p&gt;
&lt;p&gt;对于情况4：综合处理&lt;/p&gt;
&lt;p&gt;下面这个代码经过试验不太好使，空闲中断和满溢中断似乎有冲突，导致接收完成后MCU进不去满溢中断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  /* USER CODE BEGIN USART1_IRQn 0 */
  if (__HAL_UART_GET_FLAG(&amp;amp;huart1, UART_FLAG_IDLE))
  {
    __HAL_UART_CLEAR_FLAG(&amp;amp;huart1, UART_FLAG_IDLE);
    HAL_UART_DMAStop(&amp;amp;huart1);
    uint32_t temp = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx);  //已接收的数据大小

    if (temp != 0)
    //如果接收到了数据
    {
      uint8_t txbuffer[] = &quot;rx ok in idle IT\n&quot;;
      HAL_UART_Transmit(&amp;amp;huart1, txbuffer, sizeof(txbuffer), 0xFFFF);

      if (rx_half_flag == 0)
      //如果半满中断尚未触发过（前半段）
      {
        for (int i = 0; i &amp;lt; temp; i++)
        {
          RX_final[final_index] = Rx_buffer[i];
          final_index++;
          HAL_UART_Receive_DMA(&amp;amp;huart1, Rx_buffer, RX_BUFFER_SIZE - temp);
        }
      }
      else if (rx_half_flag)
      //如果半满中断已触发过（后半段）
      {
        uint32_t temp_size = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx) - RX_BUFFER_SIZE / 2;
        for (int i = RX_BUFFER_SIZE / 2; i &amp;lt; RX_BUFFER_SIZE / 2 + temp_size; i++)
        {
          RX_final[final_index] = Rx_buffer[i];
          final_index++;
        }
        HAL_UART_Receive_DMA(&amp;amp;huart1, Rx_buffer, RX_BUFFER_SIZE/2 - temp_size);
      }
    }
    else if (temp == RX_BUFFER_SIZE)
    {
      HAL_UART_Receive_DMA(&amp;amp;huart1, Rx_buffer, RX_BUFFER_SIZE);
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 1 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if (huart-&amp;gt;Instance == USART1)
{
  rx_half_flag = 1;
  
  uint8_t sendbuffer[] = &quot;rxhalf IT\n&quot;;
  HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)sendbuffer, sizeof(sendbuffer), 0xFFFF);

  for (int i = 0; i &amp;lt; RX_BUFFER_SIZE / 2; i++)
  {
    RX_final[final_index] = Rx_buffer[i];
    final_index++;
  }

  HAL_UART_Receive_DMA(&amp;amp;huart1, (uint8_t *) Rx_buffer, RX_BUFFER_SIZE / 2);
}
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart-&amp;gt;Instance == USART1)
{
  uint8_t sendbuffer[] = &quot;rx ok\n&quot;;
  HAL_UART_Transmit(&amp;amp;huart1, (uint8_t *)sendbuffer, sizeof(sendbuffer), 0xFFFF);

  for (int i = RX_BUFFER_SIZE / 2; i &amp;lt; RX_BUFFER_SIZE; i++)
  {
    RX_final[final_index] = Rx_buffer[i];
    final_index++;
  }

  rx_half_flag = 0;
}
HAL_UART_Receive_DMA(&amp;amp;huart1, (uint8_t *) Rx_buffer, RX_BUFFER_SIZE);
}
/* USER CODE END 1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续调试，删掉除打印测试信息以外的所有功能性代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  /* USER CODE BEGIN USART1_IRQn 0 */
  uint32_t temp_size = 0;
  if (__HAL_UART_GET_FLAG(&amp;amp;huart1, UART_FLAG_IDLE))
  {
    __HAL_UART_CLEAR_IDLEFLAG(&amp;amp;huart1);
    HAL_UART_DMAStop(&amp;amp;huart1);
    temp_size = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx);  //已接收数据长度

    if (temp_size != 0)
    {
      uint8_t txbuf3[] = &quot;rxidle\n&quot;;
      HAL_UART_Transmit(&amp;amp;huart1, txbuf3, sizeof(txbuf3), 0xFFFF);
      
    }
  }
  /* USER CODE END USART1_IRQn 0 */

  /* USER CODE BEGIN 1 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart-&amp;gt;Instance == USART1)
  {
    uint8_t txbuf1[] = &quot;rxhalf\n&quot;;
    HAL_UART_Transmit(&amp;amp;huart1, txbuf1, sizeof(txbuf1), 0xFFFF);
  }
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart-&amp;gt;Instance == USART1)
  {
    uint8_t txbuf2[] = &quot;rxfull\n&quot;;
    HAL_UART_Transmit(&amp;amp;huart1, txbuf2, sizeof(txbuf2), 0xFFFF);
  }
}
/* USER CODE END 1 */

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正常情况下，接收缓冲区大小为10，当发送“12”（实际为&quot;12\r\n“四个字节）时，应只触发空闲中断；发送“123”时，触发半满中断和空闲中断；发送“12345678”时，触发半满中断和满溢中断。但测试发现，发送“12345678”共计十个字节数据，也就是预期接收数据大小等于缓冲区大小时，只能触发一次半满中断，满溢中断无法触发。若禁用空闲中断，发送“12345678”时，半满中断和溢满中断都能触发，初步判断是空闲中断的问题。&lt;/p&gt;
&lt;p&gt;由于DMA配置在循环模式，当接收数据大小等于缓冲区大小时，RX总线仍会处于空闲状态，因为即将到来的新的数据会覆盖掉环形队列的第一项，因此实际上NVIC会先触发空闲中断，而空闲中断中有这一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    temp_size = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx);  //已接收数据长度

    if (temp_size != 0)
    {
      uint8_t txbuf3[] = &quot;rxidle\n&quot;;
      HAL_UART_Transmit(&amp;amp;huart1, txbuf3, sizeof(txbuf3), 0xFFFF);
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当接收数据大小等于缓冲区大小时，&lt;code&gt;temp_size&lt;/code&gt;实际上等于0，导致程序在空闲中断中直接跳过了满溢判断，开始下一次接收。因此需要单独对&lt;code&gt;temp_size == 0&lt;/code&gt;这种情况进行处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  /* USER CODE BEGIN USART1_IRQn 0 */
  uint32_t temp_size = 0;
  if (__HAL_UART_GET_FLAG(&amp;amp;huart1, UART_FLAG_IDLE))
  {
    temp_size = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx); 
    idle_flag = 1;
    __HAL_UART_CLEAR_IDLEFLAG(&amp;amp;huart1);
    if (temp_size == 0) //满溢
    {
       //满溢回调
    }
    else if (temp_size != 0)  //非满溢
    {
      HAL_UART_DMAStop(&amp;amp;huart1);
      uint8_t txbuf3[] = &quot;rxidle\n&quot;;
      HAL_UART_Transmit(&amp;amp;huart1, txbuf3, sizeof(txbuf3), 0xFFFF);
      HAL_UART_Receive_DMA(&amp;amp;huart1, Rx_buffer, RX_BUFFER_SIZE);
    }
  }
  /* USER CODE END USART1_IRQn 0 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，空闲中断在数据填满缓冲区时不会进行任何操作而直接跳出。只有当数据未填满缓冲区时才会进行原先的操作（主要是DMAStop这一步）。到这里，三个中断触发的逻辑总算是理顺了，接下来只需要进行搬运数据的处理就可以了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;usart.c&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 1 */
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart-&amp;gt;Instance == USART1)
  {
    Rx_half_flag = 1; //半满标记
    uint8_t txbuf1[] = &quot;rxhalf\n&quot;;
    HAL_UART_Transmit(&amp;amp;huart1, txbuf1, sizeof(txbuf1), 0xFFFF);

    Rx_buffer_head = Rx_buffer_tail;
    Rx_buffer_tail = RX_BUFFER_SIZE / 2 - 1;

    for (int i = Rx_buffer_head; i &amp;lt;= Rx_buffer_tail; i++)
    {
      Rx_final[Rx_final_index] = Rx_buffer[i];
      Rx_final_index++;
    }
    
  }
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart-&amp;gt;Instance == USART1)
  {
    Rx_half_flag = 0; //清除半满标记

    uint8_t txbuf2[] = &quot;rxfull\n&quot;;
    HAL_UART_Transmit(&amp;amp;huart1, txbuf2, sizeof(txbuf2), 0xFFFF);

    Rx_buffer_head = Rx_buffer_tail + 1;
    Rx_buffer_tail = RX_BUFFER_SIZE - 1;

    for (int i = Rx_buffer_head; i &amp;lt;= Rx_buffer_tail; i++)
    {
      Rx_final[Rx_final_index] = Rx_buffer[i];
      Rx_final_index++;
    }
  }
}
/* USER CODE END 1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;中断服务函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN USART1_IRQn 0 */
  uint32_t temp_size = 0;
  if (__HAL_UART_GET_FLAG(&amp;amp;huart1, UART_FLAG_IDLE))
  {
    temp_size = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&amp;amp;hdma_usart1_rx); //本次接收到的数据大小
    //idle_flag = 1;
    __HAL_UART_CLEAR_IDLEFLAG(&amp;amp;huart1); //清除Idle标记
    if (temp_size == 0) //满溢
    {
       //满溢回调
    }
    else if (temp_size != 0)  //非满 ??
    {
      HAL_UART_DMAStop(&amp;amp;huart1);
      
      uint8_t txbuf3[] = &quot;rxidle\n&quot;;
      HAL_UART_Transmit(&amp;amp;huart1, txbuf3, sizeof(txbuf3), 0xFFFF);

      if (Rx_half_flag == 0)  //未半满，前半段触发空闲
      {
        Rx_buffer_head = Rx_buffer_tail;  
        Rx_buffer_tail = Rx_buffer_tail + temp_size - 1; 

        //拷贝数据
        for (int i = Rx_buffer_head; i &amp;lt;= Rx_buffer_tail; i++)
        {
          Rx_final[Rx_final_index] = Rx_buffer[i];
          Rx_final_index++;
        }  
      }
      else if (Rx_half_flag != 0) //已半满，后半段触发空闲
      {
        Rx_buffer_head = Rx_buffer_tail + 1;
        Rx_buffer_tail = Rx_buffer_tail + temp_size - RX_BUFFER_SIZE / 2;

        for (int i = Rx_buffer_head; i &amp;lt;= Rx_buffer_tail; i++)
        {
          Rx_final[Rx_final_index] = Rx_buffer[i];
          Rx_final_index++;
        }
      }
      temp_size = 0;
      Rx_buffer_head = 0;
      Rx_buffer_tail = 0;
      Rx_half_flag = 0;
      HAL_UART_Receive_DMA(&amp;amp;huart1,(uint8_t *) Rx_buffer, RX_BUFFER_SIZE);
    }
  }
  /* USER CODE END USART1_IRQn 0 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写到这里只处理了单次接收数据大小小于缓冲区长度的情况，可以发现代码量还是挺大的。尤其是这么写代码存在一个比较麻烦的逻辑：当DMA接收的数据量大于缓冲区大小&lt;code&gt;RX_BUFFER_SIZE&lt;/code&gt;时，由于DMA工作在循环模式，那么溢出的数据会被DMA重新放到缓冲区的开始部分，从而覆盖原有的数据。要处理这部分数据势必要引入比较复杂的判断机制，还要实时更新队首和队尾的指针，导致整个程序变得比较复杂。&lt;/p&gt;
&lt;p&gt;好在HAL库除了普通的&lt;code&gt;HAL_UART_Receive_DMA()&lt;/code&gt;和&lt;code&gt;HAL_UART_RxCpltCallback()&lt;/code&gt;外，HAL库还提供了&lt;code&gt;HAL_UARTEx_RxEventCallback&lt;/code&gt;回调。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/**
  * @brief  Reception Event Callback (Rx event notification called after use of advanced reception service).
  * @param  huart UART handle
  * @param  Size  Number of data available in application reception buffer (indicates a position in
  *               reception buffer until which, data are available)
  * @retval None
  */
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  /* Prevent unused argument(s) compilation warning */
  UNUSED(huart);
  UNUSED(Size);

  /* NOTE : This function should not be modified, when the callback is needed,
            the HAL_UARTEx_RxEventCallback can be implemented in the user file.
   */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该回调函数会在“advanced reception service”事件发生后触发，这里的所谓高级接收服务就包括之前需要分开判断的DMA半满中断、DMA满溢中断和空闲中断（实际上还有一个错误中断）。这三个中断触发后都会回调&lt;code&gt;HAL_UARTEx_RxEventCallback()&lt;/code&gt;函数。在拷贝数据时，无需再单独进行中断回调类型的判断。由于DMA工作不依赖CPU，因此在该函数内要做的就是将缓冲区内的数据拷贝至目标地址。注意这里的形参&lt;code&gt;Size&lt;/code&gt;表示缓冲区可用数据长度（从这个位置开始往后的位置都为空），而不是本次回调所接收的数据长度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/arm/rxhalf.webp&quot; alt=&quot;三种中断事件场景&quot; /&gt;&lt;/p&gt;
&lt;p&gt;再对照一下这张图，红色的部分就是&lt;code&gt;rx_size&lt;/code&gt;，其值等于&lt;code&gt;Size&lt;/code&gt;（缓冲区总的有效数据长度）减去&lt;code&gt;rx_buf_head&lt;/code&gt;（头指针）。当缓冲区溢出并循环存储新数据至缓冲区开头时，&lt;code&gt;Size&lt;/code&gt;会同步更新。&lt;/p&gt;
&lt;p&gt;原先的三个中断中的代码可以合到一个中实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* USER CODE BEGIN 1 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
  uint8_t txbuf[] = &quot;rx done\n&quot;;
  HAL_UART_Transmit_IT(&amp;amp;huart1, txbuf, sizeof(txbuf));

  static uint8_t rx_buf_head = 0;
  static uint8_t rx_size; //待处理数据长度
 
  rx_size = Size - rx_buf_head;

  for (uint16_t i = 0; i &amp;lt; rx_size; i++)
  {
      RxFinal[final_index++] = RxBuf[(rx_buf_head + i) % RxBufSize]; // 环形缓冲处理
      if (final_index &amp;gt;= RxFinalSize) final_index = 0; // 避免 RxFinal 溢出
  }

  rx_buf_head = rx_buf_head + rx_size;

  if (rx_buf_head &amp;gt;= RxBufSize) rx_buf_head = 0;
}
/* USER CODE END 1 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么写就无需判断到底是哪个中断触发的回调，只需要通过&lt;code&gt;Size&lt;/code&gt;和头指针位置计算本次接收到的数据长度，然后按照队列逻辑按次序拷贝即可，因DMA循环而引起的溢出可通过取模操作来处理。&lt;/p&gt;
&lt;p&gt;验证：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;输入“12345678\r\n”:&lt;img src=&quot;/images/arm/test1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;输入“123456789abcdefghijklmn”：&lt;img src=&quot;/images/arm/test2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;先输入&quot;123&quot;，再输入&quot;1234567“，再输入&quot;12&quot;，再输入&quot;123456789”：&lt;img src=&quot;/images/arm/test3.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;验证成功。&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/AkiChen891/STM32F429-UART-DMA-Ping-Pong-Cache&quot;&gt;Github项目地址&lt;/a&gt;&lt;/p&gt;
</content:encoded></item><item><title>线性系统理论期末考点</title><link>https://arkmostima.pages.dev/posts/%E7%BA%BF%E6%80%A7%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E6%9C%9F%E6%9C%AB%E8%80%83%E7%82%B9/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%BA%BF%E6%80%A7%E7%B3%BB%E7%BB%9F%E7%90%86%E8%AE%BA%E6%9C%9F%E6%9C%AB%E8%80%83%E7%82%B9/</guid><description>单模矩阵与单模变换 互质性与互质性判据（秩判据） 既约性与既约性判据（行次系数矩阵或列次系数矩阵） 非既约矩阵的既约化 Smith标准形 左MFD与右MFD的求法 真与严真的性质与判据 有理分式判别法（分子次数小于等于为真，小于分母次数…</description><pubDate>Tue, 22 Oct 2024 14:00:04 GMT</pubDate><content:encoded>&lt;h2&gt;第七章 数学基础&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;单模矩阵与单模变换&lt;/li&gt;
&lt;li&gt;互质性与互质性判据（秩判据）&lt;/li&gt;
&lt;li&gt;既约性与既约性判据（行次系数矩阵或列次系数矩阵）&lt;/li&gt;
&lt;li&gt;非既约矩阵的既约化&lt;/li&gt;
&lt;li&gt;Smith标准形&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第八章 矩阵分式描述（MFD）&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;左MFD与右MFD的求法&lt;/li&gt;
&lt;li&gt;真与严真的性质与判据
&lt;ul&gt;
&lt;li&gt;有理分式判别法（分子次数小于等于为真，小于分母次数为严真）&lt;/li&gt;
&lt;li&gt;$G(s)$阵判别法（s趋向无穷时$G(s)$等于常阵为真，等于零为严真）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;列既约&lt;/strong&gt;右MFD列次系数判别法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;行既约&lt;/strong&gt;左MFD行次系数判别法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;从非真MFD导出严真MFD（多项式除法导出有理分式部分并求出新的$R(s)$作为$N(s)$）&lt;/li&gt;
&lt;li&gt;不可简约MFD的性质与判据（互质性判据，判断有无同时降秩的$s$值）&lt;/li&gt;
&lt;li&gt;由可简约MFD求不可简约MFD的算法（以右MFD为例）
&lt;ul&gt;
&lt;li&gt;求gcrd表为$R(s)$&lt;/li&gt;
&lt;li&gt;求$R(s)$的逆，表为$R^{-1}(s)$&lt;/li&gt;
&lt;li&gt;$D(s)$和$N(s)$分别右乘$R^{-1}(s)$得到新的$D(s)$和$N(s)$&lt;/li&gt;
&lt;li&gt;组合，解毕&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第九章  传递函数矩阵的结构特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;史密斯-麦克米伦形的形式与导出
&lt;ul&gt;
&lt;li&gt;求出所有元有理分式的最小公分母，表为$d(s)$&lt;/li&gt;
&lt;li&gt;分子矩阵化为Smith标准形&lt;/li&gt;
&lt;li&gt;消去公因子，解毕&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;M(s)的基本特性
&lt;ul&gt;
&lt;li&gt;唯一性&lt;/li&gt;
&lt;li&gt;非保真性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有限零点和有限极点
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Rosenbrock&lt;/em&gt;零极点定义（基于M(s)的判别法，只适用于有限复平面）&lt;/li&gt;
&lt;li&gt;推论性定义
&lt;ul&gt;
&lt;li&gt;基于&lt;strong&gt;不可简约&lt;/strong&gt;MFD：极点令$detD(s)$等于0，零点令$N(s)$降秩&lt;/li&gt;
&lt;li&gt;基于状态空间方程：极点令$det(sI-A)=0$，零点令系统矩阵降秩&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;零点直观解释与其阻塞定理（书P479）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;结构指数
&lt;ul&gt;
&lt;li&gt;定义（书P480），来自于零极点集合，基于S-M形求解&lt;/li&gt;
&lt;li&gt;含义（书P481）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;无穷远处的零点和极点
&lt;ul&gt;
&lt;li&gt;思路：引入$s=\lambda^{-1}$，化为以$\lambda$为变量的有理分式矩阵$H(\lambda)$，求出其S-M形，然后按照有限零极点定义求解&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;评价值
&lt;ul&gt;
&lt;li&gt;有限复平面的评价值求法（书P486）&lt;/li&gt;
&lt;li&gt;由评价值构造S-M形（书P489）&lt;/li&gt;
&lt;li&gt;无穷远处评价值求法（分母次数-分子次数，书P490）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;S-M形的合成表达式（书P492）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;易错考点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;零极点推论性定义都基于&lt;strong&gt;不可简约MFD&lt;/strong&gt;，如果题目给出的是可简约MFD，需要转化为不可简约MFD&lt;/li&gt;
&lt;li&gt;根据$G(s)$求评价值时，直接求各阶子式，子式中包含的分式所对应的$s$即为所求的零极点集合，不需要求$M(s)$再求零极点&lt;/li&gt;
&lt;li&gt;$M(s)$中对角线各元素必须满足整除性，具体为分子次数随位置增加越来越大，分母次数越来越小&lt;/li&gt;
&lt;li&gt;求解$M(s)$过程中导出$N(s)$时，分母需首一化&lt;/li&gt;
&lt;li&gt;根据零点阻塞定理求对应的$x_0$和$u(t)$时，先由$Cx_0=0$确认非零初始状态$x_0$，然后由$(sI-A)x_0+Bu_0=0$求$u_0$&lt;/li&gt;
&lt;li&gt;$s$在无穷远处的评价值即为对应的有理分式的分母次数减去分子次数&lt;/li&gt;
&lt;li&gt;$s$在无穷远处的零点和极点即为$\lambda=1/s$矩阵在$\lambda=0$处的零点和极点&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十章 传递函数矩阵的状态空间实现&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实现的定义与属性（唯一性、维数、最小实现）
&lt;ul&gt;
&lt;li&gt;$G(s)=C(sI-A)^{-1}B$&lt;/li&gt;
&lt;li&gt;实现维数：$dim(A)$&lt;/li&gt;
&lt;li&gt;最小实现：所有实现中维数最小&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;标量传递函数/传函矩阵的标准实现（要求$G(s)$严真）
&lt;ul&gt;
&lt;li&gt;能控标准形实现（书P528）
&lt;ul&gt;
&lt;li&gt;将$G(s)$表示为$\frac{P(s)}{d(s)}$的形式，并全部展开&lt;/li&gt;
&lt;li&gt;$P(s)$按$s^n$系数拆开为$P_0(s)$、$P_1(s)$...&lt;/li&gt;
&lt;li&gt;类似于标量传递函数能控形求解方式进行组合&lt;/li&gt;
&lt;li&gt;能控形实现一般不保证能观性&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;能观标准形实现（书P530）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;MFD的标准实现（要求MFD严真）
&lt;ul&gt;
&lt;li&gt;控制器形实现&lt;/li&gt;
&lt;li&gt;观测器形实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;易错考点&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;控制器形和观测器形都要求MFD为&lt;strong&gt;严真&lt;/strong&gt;，如果给出的MFD非严真，则需求其严真部分的MFD。在分离出$Q(s)$和$G_{sp}(s)$后，考虑$G_{sp}(s)=R(s)\times D^{-1}(s)$，有$R(s)=G_{sp}(s)\times D(s)$&lt;/li&gt;
&lt;li&gt;若MFD为不可简约，则该MFD的实现必为最小实现&lt;/li&gt;
&lt;li&gt;最小实现的形式不唯一但必定代数等价&lt;/li&gt;
&lt;li&gt;若题目要求判断是否为实现，则通过$G(s)=C(sI-A)^{-1}B$判断&lt;/li&gt;
&lt;li&gt;如果要求判断是否为最小实现
&lt;ul&gt;
&lt;li&gt;若已知状态空间方程，判断其是否同时满足能控且能观&lt;/li&gt;
&lt;li&gt;若已知MFD，判断其是否为不可简约&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;给定MFD的控制器形实现，判断实现维数时，所求维数即为$dim(A_c)=deg det D(s)$&lt;/li&gt;
&lt;li&gt;控制器形求解步骤
&lt;ol&gt;
&lt;li&gt;确认MFD为右严真MFD，如非严真，将其严真化&lt;/li&gt;
&lt;li&gt;写出每行的列次数&lt;/li&gt;
&lt;li&gt;写出$D_{hc}$、$D_{lc}$、$N_{lc}$&lt;/li&gt;
&lt;li&gt;写出$A^{0}_c$、$B^{0}_c$&lt;/li&gt;
&lt;li&gt;由$A_c=A^{0}&lt;em&gt;c-B^{0}&lt;em&gt;c\times D^{-1}&lt;/em&gt;{hc}\times D&lt;/em&gt;{lc}$、$B_c=B^{0}&lt;em&gt;c\times D^{-1}&lt;/em&gt;{hc}$、$C_c=N_{lc}$&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十一章 PMD&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;PMD的形式
&lt;ul&gt;
&lt;li&gt;同传递函数矩阵$G(s)$的关系
&lt;ul&gt;
&lt;li&gt;$G(s)=R^{-1}(s)P(s)Q(s)+W(s)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;同状态空间描述的关系
&lt;ul&gt;
&lt;li&gt;$P(s)=sI-A,Q(s)=B,R(s)=C$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;同&lt;strong&gt;严真&lt;/strong&gt;MFD的关系
&lt;ul&gt;
&lt;li&gt;右MFD：$P(s)=D(s),Q(s)=I,R(s)=N(s)$&lt;/li&gt;
&lt;li&gt;左MFD：$P(s)=D_L(s),Q(s)=N_L(s),R(s)=I$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不可简约PMD
&lt;ul&gt;
&lt;li&gt;判断不可简约条件
&lt;ul&gt;
&lt;li&gt;$P(s),Q(s)$左互质&lt;/li&gt;
&lt;li&gt;$P(s),R(s)$右互质&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;不可简约PMD的构造（不可简约MFD构造的组合）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;PMD的观测器形实现
&lt;ol&gt;
&lt;li&gt;由PMD化左MFD&lt;/li&gt;
&lt;li&gt;判断左MFD行既约，后判断是否严真&lt;/li&gt;
&lt;li&gt;构造左MFD观测器形实现&lt;/li&gt;
&lt;li&gt;转化为PMD观测器形实现（书P579）&lt;/li&gt;
&lt;li&gt;判断是否为最小实现（PMD不可简约，MFD不可简约，完全能控和能观）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;PMD的极点和零点
&lt;ul&gt;
&lt;li&gt;PMD的极点（$detP(s)=0$）&lt;/li&gt;
&lt;li&gt;PMD的传输零点（系统矩阵降秩）&lt;/li&gt;
&lt;li&gt;输入解耦零点（PQ行降秩）&lt;/li&gt;
&lt;li&gt;输出解耦零点（PR列降秩）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统矩阵&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;第十二章 线性时不变控制系统的复频域分析&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;并联系统
&lt;ul&gt;
&lt;li&gt;完全能控 == $D_1(s)$和$D_2(s)$左互质&lt;/li&gt;
&lt;li&gt;完全能观 == $D_{L1}(s)$和$D_{L2}(s)$右互质&lt;/li&gt;
&lt;li&gt;$G_1(s)$和$G_2(s)$无公共极点可推出$S_P$完全能控和完全能观&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;串联系统
&lt;ul&gt;
&lt;li&gt;完全能控 == $D_2(s)$和$N_1(s)$左互质&lt;/li&gt;
&lt;li&gt;完全能观 == $D_{L1}(s)$和$N_{L2}(s)$右互质&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;输出反馈系统的能观/能控性（书P622）
&lt;ul&gt;
&lt;li&gt;完全能控 == $S_{12}$完全能控&lt;/li&gt;
&lt;li&gt;完全能观 == $S_{21}$完全能观&lt;/li&gt;
&lt;li&gt;若$S_2$为常阵，则$S_1$能控能观等价于整个系统能控能观&lt;/li&gt;
&lt;li&gt;$G_F(s)=G_1(s)[I+G_2(s)G_1(s)]^{-1}=[I+G_1(s)G_2(s)]^{-1}G_2(s)$&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;直接输出反馈系统的稳定性分析
&lt;ul&gt;
&lt;li&gt;系统BIBO稳定 == 系统渐进稳定&lt;/li&gt;
&lt;li&gt;系统以有理分式矩阵表征时
&lt;ul&gt;
&lt;li&gt;系统渐进/BIBO稳定 == $\Delta_1(s)det[I+G_1(s)]=0$的根全部具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统以不可简约右MFD表征时
&lt;ul&gt;
&lt;li&gt;系统渐进/BIBO稳定 == $\Delta_1(s)det[D_1(s)+N_1(s)]=0$的根全部具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;系统以不可简约左MFD表征时
&lt;ul&gt;
&lt;li&gt;系统渐进/BIBO稳定 == $\Delta_1(s)det[D_{L1}(s)+N_{L1}(s)]=0$的根全部具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;带补偿器的输出反馈系统的稳定性
&lt;ul&gt;
&lt;li&gt;渐进稳定与BIBO稳定的等价条件
&lt;ul&gt;
&lt;li&gt;若$S_{12}$能控，$S_{21}$能观，二者等价&lt;/li&gt;
&lt;li&gt;若不满足上一个条件，只能通过渐进稳定推出BIBO稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$G_1(s)$和$G_2(s)$均以有理分式矩阵表征
&lt;ul&gt;
&lt;li&gt;渐进稳定 == $\Delta_1(s)\Delta_2(s)det[I+G_1(s)G_2(s)]=0$的根全部具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$G_1(s)$和$G_2(s)$以不可简约左MFD和右MFD表征
&lt;ul&gt;
&lt;li&gt;渐进稳定 == $det[D_{L1}(s)D_2(s)+N_{L1}(s)N_2(s)]=0$的根均具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;$G_1(s)$和$G_2(s)$以不可简约右MFD和左MFD表征
&lt;ul&gt;
&lt;li&gt;渐进稳定 == $det[D_{L2}(s)D_1(s)+N_{L2}(s)N_1(s)]=0$的根均具有负实部&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;特殊情形
&lt;ul&gt;
&lt;li&gt;若反馈通路为常阵，则系统渐近稳定==BIBO稳定&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>【矩阵论】矩阵分解</title><link>https://arkmostima.pages.dev/posts/%E7%9F%A9%E9%98%B5%E8%AE%BA%E7%9F%A9%E9%98%B5%E5%88%86%E8%A7%A3/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%9F%A9%E9%98%B5%E8%AE%BA%E7%9F%A9%E9%98%B5%E5%88%86%E8%A7%A3/</guid><description>将一个矩阵分解为两个较小矩阵的乘积，并且这两个矩阵的秩与原矩阵相同。满秩分解的目的是把矩阵表示为两个更简单矩阵的乘积，同时保持原矩阵的秩。 任何非零矩阵都存在满秩分解 满秩分解可分为行满秩分解和列满秩分解 满秩分解不唯一</description><pubDate>Sun, 20 Oct 2024 04:11:18 GMT</pubDate><content:encoded>&lt;h2&gt;满秩分解(Full Rank Factorization)&lt;/h2&gt;
&lt;p&gt;将一个矩阵分解为两个较小矩阵的乘积，并且这两个矩阵的秩与原矩阵相同。满秩分解的目的是把矩阵表示为两个更简单矩阵的乘积，同时保持原矩阵的秩。&lt;/p&gt;
&lt;h3&gt;前置定理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;任何非零矩阵都存在满秩分解&lt;/li&gt;
&lt;li&gt;满秩分解可分为&lt;strong&gt;行满秩分解&lt;/strong&gt;和&lt;strong&gt;列满秩分解&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;满秩分解不唯一&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;行满秩分解步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对矩阵$A$进行行变换，化为行最简形$\hat A$，即每一行的第一个非零元素为1，并且是其所在的列的唯一一个非零元素&lt;/li&gt;
&lt;li&gt;标记每一个非零行的第一个非零元素所在的列，此处假设为1，2，3&lt;/li&gt;
&lt;li&gt;$B$为$A$的第1，2，3列，$C$为$\hat A$的第1，2，3行&lt;/li&gt;
&lt;li&gt;$A=BC$即为行满秩分解&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;列满秩分解步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;对矩阵$A$进行列变换，化为列最简形$\hat A$，即每一列的第一个非零元素为1，并且是其所在的行的唯一一个非零元素&lt;/li&gt;
&lt;li&gt;标记每一个非零列的第一个非零元素所在的行，此处假设为1，2，3&lt;/li&gt;
&lt;li&gt;$C$为$A$的第1，2，3行，$B$为$\hat A$的第1，2，3列&lt;/li&gt;
&lt;li&gt;$A=BC$即为列满秩分解&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;UR分解&lt;/h2&gt;
&lt;p&gt;也被称为QR分解或正交三角分解。&lt;/p&gt;
&lt;h3&gt;前置定理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;若$A$为列满秩矩阵，则$A$可以唯一地被分解为$A=UR$，其中$U$为列满秩矩阵，$R$为正线上三角阵。&lt;/li&gt;
&lt;li&gt;UR分解是一种特殊的满秩分解。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;令$A=[\alpha_1,\alpha_2,\alpha_3]$&lt;/li&gt;
&lt;li&gt;将$[\alpha_1,\alpha_2,\alpha_3]$正交化、单位化，重新组合，即得到$U$&lt;/li&gt;
&lt;li&gt;根据$A=UR$，有$R=U^H A$，求出$R$&lt;/li&gt;
&lt;li&gt;UR分解即为$A=UR$&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;奇异值分解&lt;/h2&gt;
&lt;p&gt;奇异值分解的形式为：&lt;/p&gt;
&lt;p&gt;$$
A=U
\begin{bmatrix}
\Delta &amp;amp; 0 \
0 &amp;amp; 0&lt;br /&gt;
\end{bmatrix}
V^H
$$&lt;/p&gt;
&lt;h3&gt;前置定理&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;$A^H A$和$AA^H$均为半正定，特征值均为非负实数&lt;/li&gt;
&lt;li&gt;$A^H A$和$AA^H$的非零特征值相同&lt;/li&gt;
&lt;li&gt;奇异值分解通常不唯一&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;步骤（方法一）&lt;/h3&gt;
&lt;p&gt;该方法由$AA^H$求$U_1$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求出$AA^H$的全部非零特征值，由大到小排列，并以此为序写出奇异值矩阵$\Delta$&lt;/li&gt;
&lt;li&gt;求出$AA^H$的所有特征值对应的特征向量，正交化加单位化后组合，得到矩阵$U$&lt;/li&gt;
&lt;li&gt;设$AA^H$有$n$个非零奇异值，则取出$U$的前$n$列，组合得到$U_1$&lt;/li&gt;
&lt;li&gt;由$V_1=A^HU_1\Delta^{-H}$，求得$V_1$&lt;/li&gt;
&lt;li&gt;扩展$V_1$至$V$，其中新添加的列向量$V_2$应与$V_1$正交化、单位化&lt;/li&gt;
&lt;li&gt;解毕&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;步骤（方法二）&lt;/h3&gt;
&lt;p&gt;该方法由$A^HA$求$V_1$。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求出$A^HA$的全部非零特征值，由大到小排列，并以此为序写出奇异值矩阵$\Delta$&lt;/li&gt;
&lt;li&gt;求出$A^HA$的所有特征值对应的特征向量，正交化加单位化后组合，得到矩阵$V$&lt;/li&gt;
&lt;li&gt;设$A^HA$有$n$个非零奇异值，则取出$V$的前$n$列，组合得到$V_1$&lt;/li&gt;
&lt;li&gt;由$U_1 = AV_1\Delta^{-1}$，求得$U_1$&lt;/li&gt;
&lt;li&gt;扩展$U_1$至$U$，其中新添加的列向量$U_2$应与$U_1$正交化、单位化&lt;/li&gt;
&lt;li&gt;解毕&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;谱分解&lt;/h2&gt;
&lt;p&gt;谱分解不唯一。&lt;/p&gt;
&lt;h3&gt;步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;求特征值及其所对应的特征向量&lt;/li&gt;
&lt;li&gt;无需进行正交化或单位化，组合特征向量，得到矩阵$P$&lt;/li&gt;
&lt;li&gt;求$P^{-1}$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若特征值无重根：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;按照特征值次序$i$，依次取$P$的第$i$列和$P^{-1}$的第$i$列，二者相乘，得到$H_i$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;若特征值有重根：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;按照特征值次序$i$，若某特征值有$n$重根，则在取列和行时一次性取$n$次，然后二者相乘，得到$H_i$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;组合，证毕&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
</content:encoded></item><item><title>数据结构笔记【2】</title><link>https://arkmostima.pages.dev/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%AC%94%E8%AE%B02/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%AC%94%E8%AE%B02/</guid><description>将线性表的个元素分布在存储器的不同存储块，称为结点，通过地址或指针建立元素之间的联系。</description><pubDate>Fri, 11 Oct 2024 12:10:10 GMT</pubDate><content:encoded>&lt;h2&gt;链式存储&lt;/h2&gt;
&lt;h3&gt;线性表之单链表&lt;/h3&gt;
&lt;p&gt;将线性表的个元素分布在存储器的不同存储块，称为结点，通过地址或指针建立元素之间的联系。&lt;/p&gt;
&lt;p&gt;结点分为data域和next域，next域是一个指针，指向$a_i$的直接后继$a_{i+1}$的所在结点。next为NULL时表明该元素为链表最后一个元素。&lt;/p&gt;
&lt;p&gt;头结点：data不重要，专门用来标记第一个元素的结点地址&lt;/p&gt;
&lt;p&gt;结点类型描述:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct node
{
    data_t data;    //数据域
    struct node *next;  //结点的后继指针域
}listnode, *linklist;   //linklist 是一个指向链表的指针类型，用于操作链表的头指针
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以有&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;listnode A; // 定义一个结点
linklist p = &amp;amp;A;  // 定义一个链表头指针，并指向 A 结点
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;获取$a_i$:  p-&amp;gt;data;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;获取$a_{i+1}$:  p-&amp;gt;next-&amp;gt;data;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;若指针p的值为NULL，则其不指向任何结点，此时取p-&amp;gt;data或p-&amp;gt;next是错误的&lt;/p&gt;
&lt;h4&gt;单链表的遍历&lt;/h4&gt;
&lt;p&gt;线性存储中，遍历基于表尾标识符last完成，索引$i$从0循环到last视为遍历一次。&lt;/p&gt;
&lt;p&gt;链式存储中，节点之间由next进行后继连接，所以遍历应当从头节点开始，由&lt;code&gt;head = head-&amp;gt;next&lt;/code&gt;方式进行索引（指针）更新&lt;/p&gt;
&lt;p&gt;示例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    * @brief: 遍历链表并打印其中的元素
    * @param: H: 指向链表的指针
    * @return: 遍历成功返回0，失败返回-1
*/
int list_show(linklist H)
{
    linklist p;

    if (H == NULL) // 如果链表为空，直接返回 NULL
    {
        printf(&quot;H is NULL\n&quot;);
        return -1;
    }

    p = H-&amp;gt;next; //p 指向头结点的下一个节点 注意：这里不是线性存储，节点之间通过指针连接，不能写成p=H+1
    while (p != NULL)
    {
        printf(&quot;%d &quot;,p-&amp;gt;data);
        p = p-&amp;gt;next;  //p指向下一个节点
    }

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;单链表获取指定位置的节点地址&lt;/h4&gt;
&lt;p&gt;头节点只有Next域，没有data域；尾节点只有data域，Next域为NULL。&lt;/p&gt;
&lt;p&gt;list_get()函数遍历到指定链表的pos位置，遍历到之后返回最后一个节点的指针。注意遍历可以有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;从头节点开始，初始化p = head,i = -1，遍历到i &amp;lt; pos时结束，此时的pos是包含头节点在内的位置，比如链表{1,2,6,3}，元素2的位置是2，不是1；&lt;/li&gt;
&lt;li&gt;从头节点之后的第一个结点开始，初始化p = head-&amp;gt;next; i = 0，遍历到i &amp;lt; pos时结束，此时的pos是不包含头节点在内的位置，比如链表{1,2,6,3}，元素2的位置是1；&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;/*
    * @brief: 获取链表中指定位置的节点地址
    * @param: H: 指向链表的指针
    * @param: pos: 指定的位置
    * @return: 插入成功返回找到的结点，失败返回NULL
*/
linklist list_get(linklist H, int pos)
{
    linklist p; // 声明一个指向链表节点的指针，用于遍历链表
    int i = -1; // 用于记录遍历位置，初始化为-1以考虑头结点

    if (H == NULL) // 如果链表为空，直接返回 NULL
    {
        printf(&quot;H is NULL\n&quot;);
        return NULL;
    }

    if (pos == -1) // 如果 pos == -1，返回头结点
    {
        return H;
    }

    p = H; // 从头结点的下一个节点开始遍历
    while (i &amp;lt; pos) // 当 i 小于 pos 时继续循环
    {
        p = p-&amp;gt;next; // 移动到下一个节点
        if (p == NULL)  // 如果链表到达了末尾，且还没找到 pos，返回 NULL
        {
            printf(&quot;pos is invalid\n&quot;);
            return NULL;
        }
        i++; // 记录当前遍历到的节点编号
    }

    return p; // 返回找到的节点
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;单链表的其他操作&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &quot;linklist.h&quot;

/*
    * @brief: 创建一个空的链式线性表
    * @param: 无
    * @return: 返回链表的头指针
*/
linklist list_create()
{
    linklist head = (linklist)malloc(sizeof(listnode));

    if (head == NULL)
    {
        printf(&quot;memory allocation failed\n&quot;);
        return NULL;
    }
    head-&amp;gt;next = NULL;  // 初始化头结点的指针域为空，表示链表为空
    return head;
}

/*
    * @brief: 插入元素到链表中
    * @param: head: 指向链表头结点的指针
    * @param: value: 要插入的值
    * @param: pos：插入位置，新的节点会放置在该位置之后
    * @return: 插入成功返回0，失败返回-1
*/
int list_insert(linklist head, data_t value, int pos)
{
    if (head == NULL) return -1;  // 检查链表是否存在

    // 如果要在第 0 位插入节点（头结点之后）
    if (pos == 0)
    {
        linklist new_node = (linklist)malloc(sizeof(listnode));   //为新节点申请内存 
        if (new_node == NULL)
        {
            printf(&quot;memory allocation failed\n&quot;);
            return -1;
        }

        new_node-&amp;gt;data = value;   // 新节点的数据域赋值
        new_node-&amp;gt;next = head-&amp;gt;next;  // 新节点指向头结点后的第一个节点
        head-&amp;gt;next = new_node;  // 头结点的next指向新插入的节点
        return 0;
    }

    // 否则查找 pos-1 位置的前驱节点
    linklist prev_node = list_get(head, pos-1);
    if (prev_node == NULL)
    {
        printf(&quot;Invalid position\n&quot;);
        return -1;
    }

    // 为新节点申请内存
    linklist new_node = (linklist)malloc(sizeof(listnode));   
    if (new_node == NULL)
    {
        printf(&quot;memory allocation failed\n&quot;);
        return -1;
    }

    new_node-&amp;gt;data = value;  // 设置新节点的数据
    new_node-&amp;gt;next = prev_node-&amp;gt;next;  // 新节点指向原链表在插入位置后的节点
    prev_node-&amp;gt;next = new_node;  // 前驱节点替换为新插入的节点

    return 0;
}

/*
    * @brief: 获取链表中指定位置的节点地址
    * @param: H: 指向链表的指针
    * @param: pos: 指定的位置
    * @return: 插入成功返回找到的结点，失败返回NULL
*/
linklist list_get(linklist H, int pos)
{
    linklist p; // 声明一个指向链表节点的指针，用于遍历链表
    int i = -1; // 用于记录遍历位置，初始化为-1以考虑头结点

    if (H == NULL) // 如果链表为空，直接返回 NULL
    {
        printf(&quot;H is NULL\n&quot;);
        return NULL;
    }

    if (pos == -1) // 如果 pos == -1，返回头结点
    {
        return H;
    }

    p = H; // 从头结点开始遍历
    while (i &amp;lt; pos) // 当 i 小于 pos 时继续循环
    {
        p = p-&amp;gt;next; // 移动到下一个节点
        if (p == NULL)  // 如果链表到达了末尾，且还没找到 pos，返回 NULL
        {
            printf(&quot;pos is invalid\n&quot;);
            return NULL;
        }
        i++; // 记录当前遍历到的节点编号
    }

    return p; // 返回找到的节点
}

/*
    * @brief: 遍历链表并打印其中的元素
    * @param: H: 指向链表的指针
    * @return: 遍历成功返回0，失败返回-1
*/
int list_show(linklist H)
{
    linklist p;

    if (H == NULL) // 如果链表为空，直接返回 NULL
    {
        printf(&quot;H is NULL\n&quot;);
        return -1;
    }

    p = H-&amp;gt;next; //p 指向头结点的下一个节点 注意：这里不是线性存储，节点之间通过指针连接，不能写成p=H+1
    while (p != NULL)
    {
        printf(&quot;%d &quot;,p-&amp;gt;data);
        p = p-&amp;gt;next;  //p指向下一个节点
    }

    return 0;
}

/*
    * @brief: 遍历链表并查找指定的值
    * @param: H: 指向链表的指针
    * @return: 查找成功返回0，失败返回-1
*/
int list_search(linklist H, data_t value)
{
    if (H == NULL) // 如果链表为空，直接返回 NULL
    {
        printf(&quot;H is NULL\n&quot;);
        return -1;
    }

    linklist p = H-&amp;gt;next;
    int i = 0; //初始化节点索引为-1

    while (p != NULL)
    {
        if (p-&amp;gt;data == value)
        {
            printf(&quot;search ok, pos is %d\n&quot;,i);
        }
        i++;
        p = p-&amp;gt;next;   //更新指针，指向下一个节点
    }

    return 0;
}

/*
    * @brief: 合并两个链表，然后存储到新链表中
    * @param: H1: 指向链表1的指针
    * @param: H2: 指向链表2的指针
    * @return: 返回表3
*/
linklist list_merge(linklist H1, linklist H2)
{
    if (H1 == NULL || H2 == NULL) 
    {
        printf(&quot;H1 or H2 is null\n&quot;);
        return NULL;
    }
    
    linklist H3 = list_create();    //创建新表

    //拷贝H1至H3
    linklist TempPtr1 = H1-&amp;gt;next;   //声明TempPtr指向H1的下一个节点，跳过头节点
    int count1 = 0;
    while (TempPtr1 != NULL)
    {
        list_insert(H3, TempPtr1-&amp;gt;data, count1);    //向H3插入H1的data
        TempPtr1 = TempPtr1-&amp;gt;next;  //更新指针
        count1++;   //更新计数器
    } 

    linklist TempPtr2 = H2-&amp;gt;next;
    int count2 = 0;
    while (TempPtr2 != NULL)
    {
        list_insert(H3, TempPtr2-&amp;gt;data, count1 + count2);
        TempPtr2 = TempPtr2-&amp;gt;next;
        count2++;
    }

    return H3;
}

/*
    * @brief: 删除两个链表中的重复部分，将剩余元素存在新表中
    * @param: H1: 指向链表1的指针
    * @param: H2: 指向链表2的指针
    * @return: 返回表3
    * @note: 仅适用于小链表，大链表可以用哈希表
*/
linklist list_purge(linklist H1, linklist H2)
{
    if (H1 == NULL || H2 == NULL) 
    {
        printf(&quot;H1 or H2 is null\n&quot;);
        return NULL;
    }

    linklist H3 = list_create();

    int is_common = 0;  //初始化标记，09为不重复，1为重复

    linklist TempPtr1 = H1-&amp;gt;next;   //初始化Ptr1指向H1的第一个有效节点
    while (TempPtr1 != NULL)
    {
        linklist TempPtr2 = H2-&amp;gt;next;   // 每次遍历TempPtr1时，重置TempPtr2，否则TempPtr2一旦遍历到NULL后不会返回
        while (TempPtr2 != NULL)
        {
            if (TempPtr1-&amp;gt;data == TempPtr2-&amp;gt;data)
            {
                printf(&quot;common elements:%d\n&quot;,TempPtr2-&amp;gt;data);
                
                is_common = 1;
                list_insert(H3, TempPtr2-&amp;gt;data, 0);
            }
            TempPtr2 = TempPtr2-&amp;gt;next;  //更新TempPtr2
        }

        if (is_common == 0)  // 如果不是重复元素，插入到H3中
        {
            list_insert(H3, TempPtr1-&amp;gt;data, 0);
        }
        TempPtr1 = TempPtr1-&amp;gt;next;  //更新TempPtr1
    }

    return H3;
}

/*
    * @brief: 删除指定位置的节点
    * @param: H: 指向链表的指针
    * @param: pos：待删除结点的位置
    * @return: 成功返回0，失败返回-1
*/
int list_delete(linklist H, int pos)
{
    if (H == NULL) 
    {
        printf(&quot;no such linklist\n&quot;);
        return -1;
    }

    // 如果删除的位置是第一个节点
    if (pos == 1)
    {
        linklist TempPtr = H-&amp;gt;next; // 保存下一个节点
        free(H); // 释放头节点
        H = TempPtr; // 更新头指针
        return 0;
    }

    // 获取待删除节点的前一个节点
    linklist Prev_Node = list_get(H, pos - 1);
    if (Prev_Node == NULL || Prev_Node-&amp;gt;next == NULL)
    {
        printf(&quot;invalid position\n&quot;);
        return -1;
    }

    // 获取待删除的节点
    linklist TempPtr = Prev_Node-&amp;gt;next;
    Prev_Node-&amp;gt;next = TempPtr-&amp;gt;next; // 连接前后节点
    free(TempPtr); // 释放被删除的节点

    return 0;
}

/*
    * @brief: 删除整个链表并释放内存
    * @param: H: 指向链表的指针
    * @return: 成功返回0，失败返回-1
*/
int list_free(linklist H)
{
    if (H == NULL)
    {
        printf(&quot;no such linklist\n&quot;);
        return -1;
    }

    linklist TempPtr = H;

    while (H != NULL)
    {
        TempPtr = H;    // 保存当前节点
        H = H-&amp;gt;next;    // 更新链表头指针为下一个节点
        free(TempPtr);  // 释放当前节点
    }

    return 0;
}

/*
    * @brief: 倒置链表
    * @param: H: 指向链表的指针
    * @return: 成功返回0，失败返回-1
    * @note: 将节点拿出放到
*/
int list_reverse(linklist H)
{
    if (H == NULL)
    {
        printf(&quot;no such linklist\n&quot;);
        return -1;
    }

    if (H-&amp;gt;next == NULL)
    {
        printf(&quot;linklist is empty\n&quot;);
        return -1;
    }

    //链表一分为二
    linklist p = H-&amp;gt;next-&amp;gt;next;
    H-&amp;gt;next-&amp;gt;next = NULL;

    
    while (p != NULL)
    {
        linklist q = p; // 暂存当前节点
        p = p-&amp;gt;next;    // 移动到下一个节点
        q-&amp;gt;next = H-&amp;gt;next; // 将当前节点插入到链表头部（头插法）
        H-&amp;gt;next = q; // 更新头节点为新插入的节点
    }

    return 0;
}

/*
    * @brief: 求链表相邻两节点data值之和为最大的第一结点的指针
    * @param: H: 指向链表的指针
    * @return: 成功返回指针，失败返回NULL
*/
int list_getmax(linklist H)
{
    if (H == NULL)
    {
        printf(&quot;no such linklist\n&quot;);
        return -1;
    }

    if (H-&amp;gt;next == NULL)
    {
        printf(&quot;linklist is empty\n&quot;);
        return -1;
    }

    int sum_new, max = 0;
    linklist p = H-&amp;gt;next;
    linklist max_p = H-&amp;gt;next;
    int sum = p-&amp;gt;data + p-&amp;gt;next-&amp;gt;data;

    while (p != NULL &amp;amp;&amp;amp; p-&amp;gt;next != NULL)
    {
        sum_new = p-&amp;gt;data + p-&amp;gt;next-&amp;gt;data;
        
        if (sum_new &amp;gt;= max)
        {
            max = sum_new;
            max_p = p;
        }

        p = p-&amp;gt;next;
    }
    //printf(&quot;max sum is %d&quot;,max);
    return max;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;栈(Stack)&lt;/h2&gt;
&lt;p&gt;和微机原理中栈的原理相同&lt;/p&gt;
&lt;p&gt;注意：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;先进后出&lt;/li&gt;
&lt;li&gt;注意栈顶指针的更新&lt;/li&gt;
&lt;/ul&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;递归&lt;/li&gt;
&lt;li&gt;二叉树遍历&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &quot;linear_stack.h&quot;
#include &amp;lt;string.h&amp;gt;

/*
    @brief: 创建新的栈
    @param: len: 栈的长度
    @return: 指向栈的指针
*/
sqstack *stack_create(int len)
{
    if (len &amp;lt; 0)
    {
        printf(&quot;len is illegal\n&quot;);
        return NULL;
    }

    sqstack *ss;

    //申请用于放置结构体的内存
    ss = (sqstack *)malloc(sizeof(sqstack));   
    if (ss == NULL) {
        printf(&quot;Failed to allocate memory for stack.\n&quot;);
        return NULL;
    }

    //申请用于放置栈内数据的内存
    ss-&amp;gt;data = (data_t *)malloc(sizeof(data_t) * len);
    if (ss-&amp;gt;data == NULL) {
        printf(&quot;Failed to allocate memory for stack data.\n&quot;);
        free(ss);  
        return NULL;
    }

    memset(ss-&amp;gt;data, 0, len*sizeof(data_t));
    ss-&amp;gt;top = -1;
    ss-&amp;gt;maxlen = len;

    return ss;
}

/*
    @brief: 入栈
    @param: s：对应栈指针 value：待入栈值
    @return: 成功返回0，失败返回-1
*/
int stack_push(sqstack *s, data_t value)
{
    if (s == NULL)
    {
        printf(&quot;no such stack\n&quot;);
        return -1;
    }
    if (s-&amp;gt;top == s-&amp;gt;maxlen - 1)
    {
        printf(&quot;stack is full\n&quot;);
        return -1;
    }

    s-&amp;gt;top++;
    s-&amp;gt;data[s-&amp;gt;top] = value;

    printf(&quot;stack push ok\n&quot;);
    return 0;
}

/*
    @brief: 判断栈是否为空
    @param: s：对应栈指针
    @return: 栈为空返回-1，非空返回0，函数非法返回-2
*/
int stack_empty(sqstack *s)
{
    if (s == NULL)
    {
        printf(&quot;no such stack\n&quot;);
        return -2;
    }

    if (s-&amp;gt;top == -1)
    {
        printf(&quot;stack is empty\n&quot;);
        return -1;
    }
    else
    {
        return 0;
    }
}

/*
    @brief: 判断栈是否为满
    @param: s：对应栈指针
    @return: 栈为满返回-1，非满返回0，函数非法返回-2
*/
int stack_full(sqstack *s)
{
    if (s == NULL)
    {
        printf(&quot;no such stack\n&quot;);
        return -2;
    }

    if (s-&amp;gt;top == s-&amp;gt;maxlen - 1)
    {
        printf(&quot;stack is full\n&quot;);
        return -1;
    }
    else
    {
        return 0;
    }
}

/*
    @brief: 数据出栈
    @param: s：对应栈指针
    @return: 返回出栈值，如果栈为空或出错则返回-999
*/
data_t stack_pop(sqstack *s)
{
    if (s == NULL)
    {
        printf(&quot;Stack not initialized\n&quot;);
        return -999;  
    }

    if (s-&amp;gt;top == -1)
    {
        printf(&quot;Stack is empty\n&quot;);
        return -999;  
    }
     
    data_t temp = s-&amp;gt;data[s-&amp;gt;top];
    s-&amp;gt;top--;
    return temp;
}

/*
    @brief: 清除栈内所有元素
    @param: s：对应栈指针
    @return: 成功返回0，失败返回-1
*/
int stack_clear(sqstack *s)
{
    if (s == NULL)
    {
        printf(&quot;Stack not initialized\n&quot;);
        return -1;  
    }

    if (s-&amp;gt;top == -1)
    {
        return 0;  
    }

    //一般的栈清空操作只会重置 top，但不释放内存
    s-&amp;gt;top = -1;
    //free(s-&amp;gt;data);
    //s-&amp;gt;data = NULL;

    return 0;
}

/*
    @brief: 释放整个栈的空间
    @param: s：对应栈指针
    @return: 成功返回0，失败返回-1
*/
int stack_free(sqstack *s)
{
    if (s == NULL)
    {
        printf(&quot;Stack not initialized\n&quot;);
        return -1;  
    }

    free(s-&amp;gt;data);
    s-&amp;gt;data = NULL;
    free(s);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;队列(Queue)&lt;/h2&gt;
&lt;h3&gt;基于数组的队列&lt;/h3&gt;
&lt;p&gt;等同于现实生活中的“排队”，元素从列尾存入，列首取出&lt;/p&gt;
&lt;p&gt;双端队列：列首和列尾都可以进行存入和取出；或解释为：同一个队列共享列首和列尾&lt;/p&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;IO请求管理（确保系统资源的有序访问）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;队列结构体一般为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef int data_t;
#define N 64
typedef struct 
{
    data_t data[N];
    int front, rear;    //rear为队尾后一个元素的下标
}sequeue_t;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中&lt;code&gt;rear&lt;/code&gt;一般指示队尾元素的下一个位置，也就是下一个要入队的元素位置。用于处理队列为空或只有一个元素时的情况。头尾位置重合时，队列即为空。
初始&lt;code&gt;front, rear = 0;&lt;/code&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出队列：&lt;code&gt;x = sq[front++]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;入队列：&lt;code&gt;sq[rear++] = x;&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当队列中前几个元素出队时，会造成这些位置为空，从而浪费队列中的可存储空间。此时可以使用&lt;strong&gt;循环队列&lt;/strong&gt;。
为区别空队和满队，满队元素个数比数组元素个数少一个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    @brief: 创建空队列
    @param: 无
    @return: 指向队列的指针
*/
sequeue queue_create()
{
    sequeue qq = (struct queue*)malloc(sizeof(struct queue));

    if (qq == NULL)
    {
        printf(&quot;malloc failed\n&quot;);
        return NULL;
    }

    qq-&amp;gt;front = 0;
    qq-&amp;gt;rear = 0;

    memset(qq-&amp;gt;data, 0, sizeof(qq-&amp;gt;data));

    return qq;
}

/*
    @brief: 遍历并打印队列
    @param: q：队列指针
    @return: 成功返回0，失败返回-1
*/
int queue_print(sequeue q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == q-&amp;gt;rear)
    {
        printf(&quot;queue is empty&quot;);
        return -1;
    }

    for (int i = q-&amp;gt;front; i &amp;lt; q-&amp;gt;rear; i++)
    {
        printf(&quot;%d &quot;, q-&amp;gt;data[i]);
    }

    return 0;
}

/*
    @brief: 元素入队
    @param: q：队列指针 data：待入队元素
    @return: 成功返回0，失败返回-1
*/
int enqueue(sequeue q, data_t data)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if ((q-&amp;gt;rear + 1) % N == q-&amp;gt;front)
    {
        printf(&quot;queue is full\n&quot;);
        return -1;
    }

    q-&amp;gt;data[q-&amp;gt;rear] = data;
    q-&amp;gt;rear = (q-&amp;gt;rear + 1) % N;    // rear 指针移动到队列边界时，通过取余使其返回到数组开头，实现循环

    return 0;
}

/*
    @brief: 元素出队
    @param: q：队列指针
    @return: 成功返回出队元素，失败返回-1
    @note: 出队元素为队首的第一个元素
*/
int dequeue(sequeue q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == q-&amp;gt;rear)
    {
        printf(&quot;queue is empty&quot;);
        return -1;
    }

    int temp = q-&amp;gt;data[q-&amp;gt;front];
    q-&amp;gt;front = (q-&amp;gt;front + 1) % N;

    return temp;
}

/*
    @brief: 清空队列内元素，但不释放内存
    @param: q：队列指针
    @return: 成功返回0，失败返回-1
    @note: 
*/
int queue_clear(sequeue q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    q-&amp;gt;front = 0;
    q-&amp;gt;rear = 0;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;基于链式存储的队列&lt;/h3&gt;
&lt;p&gt;插入操作在队尾进行，删除操作在队首进行，由队首指针和队尾指针控制队列操作&lt;/p&gt;
&lt;p&gt;结构体需要定义两个：节点结构体 和 队列结构体&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef int data_t;

typedef struct node
{
    data_t data;
    struct node* next;
}*linklist;

typedef struct queue
{
    linklist front;
    linklist rear;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;初始化：创建空队列，front和rear指针都为空&lt;/li&gt;
&lt;li&gt;入队：在队尾新增一个节点，并更新当前队尾节点的next指针&lt;/li&gt;
&lt;li&gt;出队：更新front指针到待出队节点的下一个节点，然后删除原有队首节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与线性表不同的是，链式队列中rear指针一般指向最后队列的一个节点，而非最后一个节点的下一个节点。&lt;/p&gt;
&lt;p&gt;判断链式队列是否为空的条件只有一个：&lt;code&gt;front == NULL, rear == NULL&lt;/code&gt;，不能使用&lt;code&gt;front==rear&lt;/code&gt;判断，因为这里&lt;code&gt;front&lt;/code&gt;和&lt;code&gt;rear&lt;/code&gt;都是指针，并且&lt;code&gt;rear&lt;/code&gt;指向的是最后一个节点，而非线性队列中的指向最后一个位置的下一个位置（待插入位置）。假设队伍中只有一个元素，这时候&lt;code&gt;front&lt;/code&gt;和&lt;code&gt;rear&lt;/code&gt;二者相同，但队列不为空。&lt;/p&gt;
&lt;h4&gt;创建新的链式队列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*
    @ brief：创建并初始化新的链式队列
    @param：无
    @return：成功指向新队列的指针，失败返回NULL
*/
struct queue* queue_create()
{
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));

    if (q == NULL)
    {
        printf(&quot;malloc failed\n&quot;);
        return NULL;
    }

    q-&amp;gt;front = NULL;
    q-&amp;gt;rear = NULL;

    return q;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;为队列申请内存，暂时先不申请节点的，因为这是一个空队列，目前还没有元素&lt;/li&gt;
&lt;li&gt;因为队列中没有节点，front和rear指针都为NULL&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;遍历队列&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*
    @ brief：遍历链式队列并打印所有元素
    @param：q：队列指针
    @return：成功返回0，失败返回-1
*/
int queue_print(struct queue* q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == NULL)
    {
        printf(&quot;queue is empty\n&quot;);
        return -1;
    }

    struct node* temp = q-&amp;gt;front;

    while (temp != NULL)
    {
        printf(&quot;%d&quot;, temp-&amp;gt;data);
        temp = temp-&amp;gt;next;
    }

    printf(&quot;\n&quot;);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;在遍历时要用临时指针temp来代替front，如果直接用front指针的话，遍历一次之后front就回不去了，整个队列会被破坏&lt;/li&gt;
&lt;li&gt;尽量不要在遍历前用temp存储front指针，用front指针遍历完之后再front = temp这种形式返回回去的这种方法，在多线程的时候可能会有问题&lt;/li&gt;
&lt;li&gt;遍历结束的条件是当temp指针走到NULL之后，表明从队首节点到队尾节点全都扫描到了，而不是&lt;code&gt;temp(front)=rear&lt;/code&gt;或者&lt;code&gt;temp(front)=rear-&amp;gt;next&lt;/code&gt;，因为链式队列中rear指针指向的是最后一个（队尾）节点&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;其他&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;/*
    @ brief：创建并初始化新的链式队列
    @param：无
    @return：成功指向新队列的指针，失败返回NULL
*/
struct queue* queue_create()
{
    struct queue* q = (struct queue*)malloc(sizeof(struct queue));

    if (q == NULL)
    {
        printf(&quot;malloc failed\n&quot;);
        return NULL;
    }

    q-&amp;gt;front = NULL;
    q-&amp;gt;rear = NULL;

    return q;
}

/*
    @ brief：遍历链式队列并打印所有元素
    @param：q：队列指针
    @return：成功返回0，失败返回-1
*/
int queue_print(struct queue* q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == NULL)
    {
        printf(&quot;queue is empty\n&quot;);
        return -1;
    }

    struct node* temp = q-&amp;gt;front;

    while (temp != NULL)
    {
        printf(&quot;%d &quot;, temp-&amp;gt;data);
        temp = temp-&amp;gt;next;
    }

    printf(&quot;\n&quot;);

    return 0;
}

/*
    @ brief：元素入队
    @param：q：队列指针 val：待入队元素值
    @return：成功返回0，失败返回-1
*/
int enqueue(struct queue* q, data_t val)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }
    struct node* newnode = (struct node*)malloc(sizeof(struct node));

    newnode-&amp;gt;data = val;
    newnode-&amp;gt;next = NULL;

    //判断原队列是否为空
    if (q-&amp;gt;rear == NULL)
    {
        q-&amp;gt;front = newnode;
        q-&amp;gt;rear = newnode;
    }
    else
    {  
        q-&amp;gt;rear-&amp;gt;next = newnode;
        q-&amp;gt;rear = newnode;
    }

    return 0;
}

/*
    @ brief：元素出队
    @param：q：队列指针
    @return：成功返回0，失败返回-1
*/
int dequeue(struct queue* q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == NULL)
    {
        printf(&quot;queue is empty\n&quot;);
        return -1;
    }

    struct node* temp_node = q-&amp;gt;front;
    int temp = temp_node-&amp;gt;data;
    q-&amp;gt;front = q-&amp;gt;front-&amp;gt;next;

    // 如果队列为空，则需要更新 rear 为 NULL
    if (q-&amp;gt;front == NULL)
    {
        q-&amp;gt;rear = NULL;
    }

    free(temp_node);

    return temp;
}

/*
    @ brief：清空队列并删除对应节点
    @param：q：队列指针
    @return：成功返回0，失败返回-1
*/
int queue_clear(struct queue* q)
{
    if (q == NULL)
    {
        printf(&quot;no such queue\n&quot;);
        return -1;
    }

    if (q-&amp;gt;front == NULL)
    {
        return 0;
    }

    //临时节点，用于储存待删除的节点而不影响后续节点的遍历
    struct node* temp;  
    
    while (q-&amp;gt;front != NULL)
    {
        temp = q-&amp;gt;front;
        q-&amp;gt;front = q-&amp;gt;front-&amp;gt;next;
        free(temp);
    }

    q-&amp;gt;rear = NULL;

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;球钟问题（队列和栈的应用）&lt;/h2&gt;
&lt;p&gt;球钟问题（Ball Clock Problem）是一种经典的算法和数据结构题目，主要用于考察队列和栈的结合运用。问题的基本情景是：
假设有一个球钟系统，它通过小球来计时。系统中有 3 个轨道，每个轨道容量不同：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分钟轨道（Min Track）：最多可容纳 4 个小球。&lt;/li&gt;
&lt;li&gt;五分钟轨道（Five-Min Track）：最多可容纳 11 个小球。&lt;/li&gt;
&lt;li&gt;小时轨道（Hour Track）：最多可容纳 11 个小球。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;运作机制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;初始状态下有一组编号为 1 到 n 的小球，n 是球的总数，这些小球依次通过队列进行处理。&lt;/li&gt;
&lt;li&gt;每分钟，第一个小球从队列中取出，放到分钟轨道上。&lt;/li&gt;
&lt;li&gt;当分钟轨道满（即放入第5个小球时），轨道中的 4 个小球被依次放回队列，保持原来的顺序，而第 5 个小球则进入五分钟轨道。&lt;/li&gt;
&lt;li&gt;当五分钟轨道满（即放入第12个小球时），轨道中的 11 个小球被依次放回队列，第 12 个小球进入小时轨道。&lt;/li&gt;
&lt;li&gt;当小时轨道满（即放入第12个小球时），轨道中的 11 个小球被依次放回队列，第 12 个小球也回到队列的末尾。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;问题的一个常见考点是计算在给定的初始状态下，经过多少分钟后，小球队列的顺序会恢复到最初的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;stdlib.h&amp;gt;
#include &quot;queue.h&quot;
#include &quot;linear_stack.h&quot;

#define N 27

int main()
{
    struct queue* main_queue = queue_create();
    struct stack* minute = stack_create(4);
    struct stack* five_minute = stack_create(11);
    struct stack* hour = stack_create(11);

    int time = 0;

    //初始化主队列中的27个小球
    for (int i = 1; i &amp;lt;= N; i++)
    {
        enqueue(main_queue, i);
    }

    while (!stack_full(hour))   //如果小时轨道未满，继续
    {
        while (!stack_full(five_minute))    //如果五分钟轨道未满，继续
        {
            while (!stack_full(minute))     //如果分钟轨道未满，继续
            {
                int ball = dequeue(main_queue);     //从主队列中获取小球
                stack_push(minute, ball);           // 放入分钟栈中
                time++;

                if (stack_full(minute))             //如果分钟栈满了
                {
                    int count_min = 0;
                    while (count_min &amp;lt; 4)    
                    {
                        int min_top_ball = stack_pop(minute);   //从分钟栈中弹出一个小球
                        enqueue(main_queue, min_top_ball);      //该小球送入主队列
                        count_min++;
                    }

                    stack_push(five_minute, ball);          //将一个小球推入五分钟栈
                }
            }
            if (stack_full(five_minute))    //如果五分钟栈满了
            {
                int count_five_min = 0;
                while (count_five_min &amp;lt; 11)  
                {
                    int five_min_top_ball = stack_pop(five_minute);     //从五分钟栈中弹出一个小球
                    enqueue(main_queue, five_min_top_ball);     //送回主队列
                    count_five_min++;
                }

                stack_push(hour, dequeue(main_queue));
            }
        }

        if (stack_full(hour))
        {
            int count_hour = 0;
            while (count_hour &amp;lt; 11)
            {
                int hour_top_ball = stack_pop(hour);
                enqueue(main_queue, hour_top_ball);
                count_hour++;
            }
        }
    }

    printf(&quot;钟表恢复到初始状态所需时间: %d 分钟\n&quot;, time);

    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>数据结构笔记【1】</title><link>https://arkmostima.pages.dev/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%AC%94%E8%AE%B01/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E7%AC%94%E8%AE%B01/</guid><description>包含： 集合 线性结构（一对一），如线性表、栈、队列 树形结构 一个对多个 图状结构 多个对多个 顺序存储：按照逻辑顺序存放于连续空间 链式存储（重点）：放到存储区的不同位置，用地址（指针）方式建立逻辑上的联系 索引存储：建立附加的索引…</description><pubDate>Thu, 10 Oct 2024 12:00:17 GMT</pubDate><content:encoded>&lt;h3&gt;数据的三种结构&lt;/h3&gt;
&lt;h4&gt;逻辑结构&lt;/h4&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;树形结构 一个对多个&lt;/li&gt;
&lt;li&gt;图状结构 多个对多个&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;存储结构&lt;/h4&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;索引存储：建立附加的索引表（电话簿）&lt;/li&gt;
&lt;li&gt;散列存储：依据数据元素的特殊字段（关键字key）计算元素的存放地址，然后按地址存放&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;线性表&lt;/h3&gt;
&lt;p&gt;线性表是包含若干数据元素的一个线性序列，记为
$$
L=(a_0,...,a_{i-1},a_i,a_{i+1},...,a_{n-1})
$$&lt;/p&gt;
&lt;p&gt;$L$为表名，$a_i$为数据元素，$n$为表长，$n&amp;gt;0$时表为非空表，否则为空表&lt;/p&gt;
&lt;p&gt;线性表可以用二元组形式描述
$$
L=(D,R)
$$
即线性表$L$包含数据元素集合$D$和关系集合$R$&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关系符&amp;lt;$a_i$,$a_{i+1}$&amp;gt;为有序对&lt;/li&gt;
&lt;li&gt;表示任意两个相邻元素之间的先后次序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$E.g.$ 有顺序表L={1,2,3,4,5,6}，若使用$L=(D,R)$表示，则$D={1,2,3,4,5,6}$,$R=&amp;lt;1,2&amp;gt;,&amp;lt;2,3&amp;gt;,...$&lt;/p&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;其余元素仅有一个直接前驱和直接后继&lt;/li&gt;
&lt;/ul&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;存储密度高&lt;/li&gt;
&lt;li&gt;对表的插入和删除等运算的时间复杂度高&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C语言中，可借助一维数组类型来表述线性表的顺序存储结构&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
    * @brief:创建新线性表
    * @param:无
    * @return:线性表指针(sqlink类型)，申请失败返回NULL
*/
sqlink list_create()
{
    sqlink L;   //声明sqlink类型的表L
    L = (sqlink)malloc(sizeof(sqlist));  // 为空表申请内存，返回分配的内存地址
    
    //判断内存是否申请成功
    if (L == NULL)
    {
        printf(&quot;memory allocation failed\n&quot;);
        return NULL;
    }

    memset(L, 0, sizeof(sqlist));   //L指向的sqlist结构体的内存清零
    L-&amp;gt;last = -1;   //表示线性表为空

    return L;
}

/*
    * @brief:清除线性表内的元素，全部置为零
    * @param: L:指向表的指针
    * @return:成功返回0，失败返回-1
*/
int list_clear(sqlink L)
{
    //判断是否是空表
    if (L == NULL)
    {
        return -1;
    }

    memset(L, 0, sizeof(sqlist));   //L指向的sqlist结构体的内存清零
    L-&amp;gt;last = -1;   //last置为-1，表示线性表为空 

    return 0;
}

/*
    * @brief:判断线性表是否为空表
    * @param: L:指向线性表的指针
    * @return:空表返回1，非空表返回0
*/
int list_empty(sqlink L)
{
    //判断是否为空表
    if (L-&amp;gt;last == -1)
    {
        return 1;
    }
    else
    {
        return 0;
    }
}

/*
    * @brief:求线性表的长度(有效元素个数)
    * @param: L:指向线性表的指针
    * @return:表不存在返回-1，否则返回长度
*/
int list_length(sqlink L)
{
    if (L == NULL)
    {
        return -1;
    }
    return (L-&amp;gt;last + 1);   //长度为表尾下标加1
}

/*
    * @brief:向线性表内插入数值，插入后原位置向后的所有元素后移一位
    * @param: L:指向线性表的指针 
    * @param: value:待插入值 
    * @param: pos:插入位置
    * @return:插入成功返回0，失败返回-1
*/
int list_insert(sqlink L, data_t value, int pos)
{
    if (L == NULL) return -1;   //表不存在返回-1
    //判断线性表是否满
    if (L-&amp;gt;last == N - 1)
    {
        printf(&quot;table is full\n&quot;);
        return -1;
    }
    //检查插入位置是否正确，应属于[0,last]，如果是空表则不检查
    if (pos &amp;lt; 0 || pos &amp;gt;= L-&amp;gt;last + 1)
    {
        if (L-&amp;gt;last != -1) 
        {
            printf(&quot;pos is illegal\n&quot;);
            return -1;
        }

    }
    //向后移动原有元素，从最后一个元素开始
    for (int i = L-&amp;gt;last; i &amp;gt;= pos; i--)
    {
        L-&amp;gt;data[i+1] = L-&amp;gt;data[i];
    }
    //更新插入值和last
    L-&amp;gt;data[pos] = value;
    L-&amp;gt;last++;

    return 0;    
}

/*
    * @brief:遍历并打印线性表内元素
    * @param: L:指向线性表的指针
    * @return:成功返回0，失败返回-1
*/
int list_show(sqlink L)
{
    if (L == NULL) return -1;   //判断是否为有效表
    if (L-&amp;gt;last == -1) printf(&quot;table is empty\n&quot;);  //判断是否为空表
    //遍历并打印
    for (int i = 0; i &amp;lt;= L-&amp;gt;last; i++)  //注意是小于等于，不然不显示表尾
    {
        printf(&quot;%d &quot;,L-&amp;gt;data[i]);
    }

    return 0;
}

/*
    * @brief:删除全表，包括创建表时所分配的内存
    * @param: L:指向线性表的指针
    * @return:表不存在返回-1，成功返回0
*/
int list_delete(sqlink L)
{
    if (L == NULL) return -1;
    free(L);    //释放为表L申请的内存
    L = NULL;   //标记表L为无效表
    return 0;
}

/*
    * @brief:删除表中某一位置的元素
    * @param: L：指向线性表的指针
    * @param: pos：待删除元素位置
    * @return:删除成功返回0，失败返回-1
*/
int list_delete_single(sqlink L, int pos)
{
    if (L == NULL) return -1;   //判断是否为无效表

    //判断pos是否位于有效范围内 应为[0,last]
    if (pos &amp;lt; 0 || pos &amp;gt; L-&amp;gt;last)
    {
        printf(&quot;pos is illegal\n&quot;);
        return -1;
    }

    //[pos+1,last]区间内已有元素前移
    for (int i = pos + 1; i &amp;lt; L-&amp;gt;last + 1; i++)
    {
       L-&amp;gt;data[i-1] = L-&amp;gt;data[i];   //数据前移，自动覆盖
    }

    //更新last
    L-&amp;gt;last--;   

    return 0;
}

/*
    * @brief:合并两个线性表，L2全表元素置于L1元素之后
    * @param: L1：指向待合并线性表L1的指针
    * @param: L1：指向待合并线性表L2的指针
    * @return:合并成功返回0，失败返回-1
*/
int list_merge(sqlink L1, sqlink L2)
{
    int count = 0;
    if (L1 == NULL || L2 == NULL) return -1;

    //判断合并后L1是否越界
    if (L1-&amp;gt;last + 1 + L2-&amp;gt;last + 1 &amp;gt; N) printf(&quot;table is too big\n&quot;);

    //L2的第i位加到L1的last+1+i位置，总共传递L2-&amp;gt;last个数
    for (int i = 0; i &amp;lt;= L2-&amp;gt;last; i++)
    {
        L1-&amp;gt;data[L1-&amp;gt;last+1+i] = L2-&amp;gt;data[i];
        count++;    //计数
    }

    //更新L1-&amp;gt;last为原last值+传递完成的元素个数
    L1-&amp;gt;last = L1-&amp;gt;last + count;

    return 0;
}

/*
    * @brief:查询线性表中是否存在某一元素
    * @param: L：指向待查询线性表L的指针
    * @param: value：待查询值
    * @return:值对应的元素存在返回1，不成功返回-1
*/
int list_locate(sqlink L, data_t value)
{
    int i = 0;

    //搜索L中是否有元素等于value
    for (i = 0 ; i &amp;lt;=L-&amp;gt;last; i++)
    {
        if (L-&amp;gt;data[i] == value) return 1;
    }

    return -1;
}

/*
    * @brief:查找L1和L2中是否有相同的元素，将不同的元素存于新表L3
    * @param: L1：指向待合并线性表L1的指针
    * @param: L2：指向待合并线性表L2的指针
    * @return:合并成功返回0，失败返回-1
*/
sqlink list_mergetonewtable(sqlink L1, sqlink L2)
{
    sqlink L3 = list_create();  //创建新表L3
    int i = 0;
    int ret;

    if (L1 == NULL || L2 == NULL) 
    {
        printf(&quot;no such table&quot;);
        return NULL;
    }
    //遍历L2内元素
    while (i &amp;lt;= L2-&amp;gt;last)
    {
        ret = list_locate(L1, L2-&amp;gt;data[i]);   //查找L1中是否有L2中的第i个元素，即判断是否重复
        //某元素不重复则向L3插入
        if (ret == -1)
        {
            list_insert(L3, L2-&amp;gt;data[i], L3-&amp;gt;last+1);   //按顺序向L3插入非重复值
        }
        i++;    //更新索引
    }
    return L3;
}

/*
    * @brief:清除线性表内的重复元素，清除后所有元素前移补空
    * @param: L：指向待合并线性表L1的指针
    * @return:操作成功返回0，失败返回-1
*/
int list_purge(sqlink L)
{
    if (L == NULL) return -1;

    for (int i = 0; i &amp;lt;= L-&amp;gt;last; i++)  //遍历线性表
    {
        for (int j = i + 1; j &amp;lt;= L-&amp;gt;last; j++)  //从索引i+1开始，如果i+1及之后的元素和data[i]相等，判断为重复
        {
            if (L-&amp;gt;data[i] == L-&amp;gt;data[j])
            {
                printf(&quot;The repeated elements are:%d\n&quot;,L-&amp;gt;data[i]);
                list_delete_single(L, j);   //删除重复的第j个元素
                j--;    //回退索引，防止跳过未检查的元素
            }
        }
    }
    return 0;
} 
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;线性表的顺序存储缺点&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;要求系统提供一大片连续存储空间&lt;/li&gt;
&lt;li&gt;插入、删除等运算需要遍历整个内存，运算耗时，且元素可能在存储器中成片移动&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>十二相同步整流发电机的数学模型</title><link>https://arkmostima.pages.dev/posts/%E5%8D%81%E4%BA%8C%E7%9B%B8%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E7%9A%84%E6%95%B0%E5%AD%A6%E6%A8%A1%E5%9E%8B/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%8D%81%E4%BA%8C%E7%9B%B8%E6%95%B4%E6%B5%81%E5%8F%91%E7%94%B5%E6%9C%BA%E7%9A%84%E6%95%B0%E5%AD%A6%E6%A8%A1%E5%9E%8B/</guid><description>与常规同步发电机相比，多相整流发电机除了定子结构有差异，其转子也会有一定的区别。为了改善其运行稳定性,在常规同步发电机转子设置的励磁绕组($fd$)、d轴阻尼绕组($kd$)和q轴阳阻尼绕组($kq$)三套绕组基础上，还会布置q轴稳定绕…</description><pubDate>Fri, 13 Sep 2024 03:41:02 GMT</pubDate><content:encoded>&lt;h2&gt;假设条件与正方向&lt;/h2&gt;
&lt;p&gt;与常规同步发电机相比，多相整流发电机除了定子结构有差异，其转子也会有一定的区别。为了改善其运行稳定性,在常规同步发电机转子设置的励磁绕组($fd$)、d轴阻尼绕组($kd$)和q轴阳阻尼绕组($kq$)三套绕组基础上，还会布置q轴稳定绕组($fq$)。为了简化分析过程，并确保一定的分析精度，对十二相发电机的物理状态作如下基本假设:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;忽略铁心材料的饱和、磁滞及涡流影响，不计导线的集肤效应；&lt;/li&gt;
&lt;li&gt;忽略空间谐波磁场的影响，气隙磁场按正弦分布；&lt;/li&gt;
&lt;li&gt;忽略定、转子齿槽影响，认为定子和转子表面光滑；&lt;/li&gt;
&lt;li&gt;将转子上的阻尼回路看作两组等效的阻尼绕组，即$D$轴阻尼绕组和$Q$轴阻尼绕组。&lt;/li&gt;
&lt;/ul&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;转子旋转正方向为逆时针方向，$q$轴正方向领先$d$轴正方向90°电角度。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/12/4-1.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/12/4-2.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;abc坐标系下的基本方程&lt;/h2&gt;
&lt;p&gt;磁链参数矩阵为：&lt;/p&gt;
&lt;p&gt;$$
\Psi_{abc}=
\begin{bmatrix}
\Psi_{a1} &amp;amp; \Psi_{b1} &amp;amp; \Psi_{c1} &amp;amp; \Psi_{a2} &amp;amp; \Psi_{b2} &amp;amp; \Psi_{c2} &amp;amp; \Psi_{a3} &amp;amp; \Psi_{b3} &amp;amp; \Psi_{c3} &amp;amp; \Psi_{a4} &amp;amp; \Psi_{b4} &amp;amp; \Psi_{c4} &amp;amp; \Psi_{fd} &amp;amp; \Psi_{kd} &amp;amp; \Psi_{fq} &amp;amp; \Psi_{kq}
\end{bmatrix}^T
$$
式中前12项下标为12个相位的标号，后四项中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\Psi_{fd}$：励磁绕组磁链&lt;/li&gt;
&lt;li&gt;$\Psi_{kd}$：直轴阻尼绕组磁链&lt;/li&gt;
&lt;li&gt;$\Psi_{fq}$：交轴稳定绕组磁链&lt;/li&gt;
&lt;li&gt;$\Psi_{kq}$：交轴阻尼绕组磁链&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;电压参数矩阵为
$$
u_{abc}=
\begin{bmatrix}
u_{a1} &amp;amp; u_{b1} &amp;amp; u_{c1} &amp;amp; u_{a2} &amp;amp; u_{b2} &amp;amp; u_{c2} &amp;amp; u_{a3} &amp;amp; u_{b3} &amp;amp; u_{c3} &amp;amp; u_{a4} &amp;amp; u_{b4} &amp;amp; u_{c4} &amp;amp; u_{fd} &amp;amp; u_{kd} &amp;amp; u_{fq} &amp;amp; u_{kq}
\end{bmatrix}^T
$$&lt;/p&gt;
&lt;p&gt;电流参数矩阵为
$$
i_{abc}=
\begin{bmatrix}
i_{a1} &amp;amp; i_{b1} &amp;amp; i_{c1} &amp;amp; i_{a2} &amp;amp; i_{b2} &amp;amp; i_{c2} &amp;amp; i_{a3} &amp;amp; i_{b3} &amp;amp; i_{c3} &amp;amp; i_{a4} &amp;amp; i_{b4} &amp;amp; i_{c4} &amp;amp; i_{fd} &amp;amp; i_{kd} &amp;amp; i_{fq} &amp;amp; i_{kq}
\end{bmatrix}^T
$$&lt;/p&gt;
&lt;p&gt;因直轴阻尼绕组、交轴阻尼绕组和交轴稳定绕组均短接，有$u_{kd}=u_{kq}=u_{fq}=0$。&lt;/p&gt;
&lt;p&gt;相应的磁链方程和电压方程为
$$
\Psi_{abc}=L_{abci}i_{abc} \
u_{abc}=p\Psi_{abc}-R_{abc}i_{abc}
$$
式中
$$
L_{abc}=
\begin{bmatrix}
L_{11} &amp;amp; L_{12} &amp;amp; L_{13} &amp;amp; L_{14} &amp;amp; L_{1r} \
L_{21} &amp;amp; L_{22} &amp;amp; L_{23} &amp;amp; L_{24} &amp;amp; L_{2r} \
L_{31} &amp;amp; L_{32} &amp;amp; L_{33} &amp;amp; L_{34} &amp;amp; L_{3r} \
L_{41} &amp;amp; L_{42} &amp;amp; L_{43} &amp;amp; L_{44} &amp;amp; L_{4r} \
-L_{1r}^T &amp;amp; -L_{2r}^T &amp;amp; -L_{3r}^T &amp;amp; -L_{4r}^T &amp;amp; L_{rr} \
\end{bmatrix}
$$
式中$L_{ij},i=1,2,3,4;i=1,2,3,4$为分块矩阵，具体为
$$
L_{ij}=
\begin{bmatrix}
-L_{aiaj} &amp;amp; -L_{aibj} &amp;amp; -L_{aicj} \
-L_{biaj} &amp;amp; -L_{bibj} &amp;amp; -L_{bicj} \
-L_{ciaj} &amp;amp; -L_{cibj} &amp;amp; -L_{cicj} \
\end{bmatrix}
$$
矩阵中$-L_{aiaj}$表示$ai$相电枢绕组与$aj$相电枢绕组之间的互感/自感。因正方向中规定正方向的定子电流产生负的磁链，所以所有电感均带负号。&lt;/p&gt;
&lt;p&gt;同理，式中$L_{ir},i=1,2,3,4$也为分块矩阵，具体为
$$
L_{ir}=
\begin{bmatrix}
L_{aifd} &amp;amp; L_{aikd} &amp;amp; L_{aifq} &amp;amp; L_{aikq} \
L_{bifd} &amp;amp; L_{bikd} &amp;amp; L_{bifq} &amp;amp; L_{bikq} \
L_{cifd} &amp;amp; L_{cikd} &amp;amp; L_{cifq} &amp;amp; L_{cikq} \
\end{bmatrix}
$$
矩阵中$L_{aifd}$表示电枢$ai$相绕组与转子励磁绕组的互感，其余同理。因正方向中规定正方向的转子电流产生正的磁链，所以所有电感均为正。&lt;/p&gt;
&lt;p&gt;同理，式中$L_{rr}$表示转子各绕组之间的自感/互感，具体为
$$
L_{rr}=
\begin{bmatrix}
L_{fd} &amp;amp; L_{fdkd} &amp;amp; 0 &amp;amp; 0 \
L_{fdkd} &amp;amp; L_{kd} &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; L_{fq} &amp;amp; L_{fqkq} \
0 &amp;amp; 0 &amp;amp; L_{fqkq} &amp;amp; L_{kq} \
\end{bmatrix}
$$
矩阵中$L_{fd}$为励磁绕组的自感，$L_{fdkd}$为励磁绕组与直轴阻尼绕组的互感，其余同理。因为励磁绕组与交轴阻尼绕组、励磁绕组与交轴稳定绕组、直轴阻尼绕组与交轴阻尼绕组、直轴阻尼绕组与交轴稳定绕组的电角度夹角均为90$\degree$，所以有$L_{fdkq}=L_{fdfq}=L_{kdkq}=L_{kdfq}=0$。&lt;/p&gt;
&lt;p&gt;电感矩阵中各量的详细表达式略。&lt;/p&gt;
&lt;h2&gt;dq0坐标系下的基本方程&lt;/h2&gt;
&lt;h3&gt;变换矩阵&lt;/h3&gt;
&lt;p&gt;应用推广的三相电机Park变换，取变换矩阵为
$$
C^{abc}&lt;em&gt;{dq0}(\theta)=
\begin{bmatrix}
C&lt;/em&gt;{11} &amp;amp; &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; C_{22} &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; C_{33} &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; C_{44} &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; &amp;amp; I \
\end{bmatrix}
$$
式中$I$为$4\times 4$的单位矩阵，其对应的变换对象为转子部分参数，显然该部分无需进行Park变换。&lt;/p&gt;
&lt;p&gt;式中，
$$
C_{ii}=
\frac{2}{3}
\begin{bmatrix}
\cos[\theta-(i-1)15\degree] &amp;amp; \cos[\theta-120\degree-(i-1)15\degree] &amp;amp; \cos[\theta+120\degree-(i-1)15\degree] \
-\sin[\theta-(i-1)15\degree] &amp;amp; -\sin[\theta-120\degree-(i-1)15\degree] &amp;amp; -\sin[\theta+120\degree-(i-1)15\degree] \
\frac{1}{2} &amp;amp; \frac{1}{2} &amp;amp; \frac{1}{2}\
\end{bmatrix}
$$&lt;/p&gt;
&lt;h3&gt;电压方程&lt;/h3&gt;
&lt;h1&gt;展开dq0坐标系下的&lt;strong&gt;有名值&lt;/strong&gt;电压方程式得
$$
\begin{bmatrix}
u_{d1} \
u_{q1} \
u_{01} \
u_{d2} \
u_{q2} \
u_{02} \
u_{d3} \
u_{q3} \
u_{03} \
u_{d4} \
u_{q4} \
u_{04} \
u_{fd} \
u_{kd} \
u_{fq} \
u_{kq} \
\end{bmatrix}&lt;/h1&gt;
&lt;p&gt;p
\begin{bmatrix}
\Psi_{d1} \
\Psi_{q1} \
\Psi_{01} \
\Psi_{d2} \
\Psi_{q2} \
\Psi_{02} \
\Psi_{d3} \
\Psi_{q3} \
\Psi_{03} \
\Psi_{d4} \
\Psi_{q4} \
\Psi_{04} \
\Psi_{fd} \
\Psi_{kd} \
\Psi_{fq} \
\Psi_{kq} \
\end{bmatrix}
+
\begin{bmatrix}
-\omega \Psi_{q1} \
\omega \Psi_{d1} \
0 \
-\omega \Psi_{q2} \
\omega \Psi_{d2} \
0 \
-\omega \Psi_{q3} \
\omega \Psi_{d3} \
0 \
-\omega \Psi_{q4} \
\omega \Psi_{d4} \
0 \
0 \
0 \
0 \
0 \
\end{bmatrix}
+
\begin{bmatrix}&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;r_s i_{d1} \
-r_s i_{q1} \
-r_s i_{01} \&lt;/li&gt;
&lt;li&gt;r_s i_{d2} \
-r_s i_{q2} \
-r_s i_{02} \&lt;/li&gt;
&lt;li&gt;r_s i_{d3} \
-r_s i_{q3} \
-r_s i_{03} \&lt;/li&gt;
&lt;li&gt;r_s i_{d4} \
-r_s i_{q4} \
-r_s i_{04} \
r_{fd} i_{fd}\
r_{fq} i_{fq}\
r_{kd} i_{kd}\
r_{kq} i_{kq} \
\end{bmatrix}
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;考虑基值选取中有$\omega_B$的存在，将电压方程定子侧的第一个矩阵（磁链阵）除以之，可得$x_{ad}$基值系统下的&lt;strong&gt;标幺值&lt;/strong&gt;定子电压方程：
$$&lt;/p&gt;
&lt;p&gt;$$&lt;/p&gt;
</content:encoded></item><item><title>机器学习基础</title><link>https://arkmostima.pages.dev/posts/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%9F%BA%E7%A1%80/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0%E5%9F%BA%E7%A1%80/</guid><description>Machine Learning can be divided to : supervised learning unsupervised learning</description><pubDate>Mon, 09 Sep 2024 12:41:28 GMT</pubDate><content:encoded>&lt;p&gt;Machine Learning can be divided to :
- supervised learning
- unsupervised learning&lt;/p&gt;
&lt;h2&gt;Supervised learning&lt;/h2&gt;
&lt;p&gt;Needs data set with labels&lt;/p&gt;
&lt;p&gt;TO: predict unknown future output&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Classfication Problem (discrete)&lt;/strong&gt;: to predict a discrete output (say yes or no), e.g Cancer benign or malignant diagnosis&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Regression Problem (Continous)&lt;/strong&gt;: to predict a specific number, e.g Stock Price Prediction&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;During a linear regression problem we have to solve a minimization problem, to minimize the difference between $x$ and $h(y)$.&lt;/p&gt;
&lt;h3&gt;Regression Problem&lt;/h3&gt;
&lt;p&gt;Training set + learning algorithm -&amp;gt; generate hypothesis function $h$&lt;/p&gt;
&lt;p&gt;$h$ takes input $x$ (e.g. size of house) and output $y$ (e.g. estimated selling price)&lt;/p&gt;
&lt;p&gt;Cost Function:&lt;/p&gt;
&lt;p&gt;$$
J(\theta_{0},\theta_{1})=\frac{1}{2m} \Sigma (h_{\theta}(x)-y)^2
$$&lt;/p&gt;
&lt;p&gt;Sigma from 1 to m (m equal to sample size)&lt;/p&gt;
&lt;h3&gt;Gradient Descent&lt;/h3&gt;
&lt;p&gt;repeat until convergence:
$$
\theta_j  := \theta_j -\alpha\frac{\partial}{\partial \theta_j}J(\theta_0 , \theta_1)
$$
(for $j$ = 0 and $j$ = 1)
:= means denote assignment
$\alpha$:learning rate, basically controls how big is a step when descent
$\theta_0$ and $\theta_1$ have to be updated simultaneously&lt;/p&gt;
&lt;h3&gt;Linear Regression Algorithm&lt;/h3&gt;
&lt;p&gt;TO: Apply gradient descent algorithm to minimize squared error cost function&lt;/p&gt;
&lt;p&gt;&quot;Batch&quot; gradient descent: each step of gradient descend uses all training examples&lt;/p&gt;
&lt;h3&gt;Multiple features (variables)&lt;/h3&gt;
&lt;p&gt;Hypothesis:$h_\theta (x) = \theta_T x = \theta_0 x_0 + \theta_1 x_1 ... + \theta_n x_n$
Parameters:$\theta_0,\theta_1,...,\theta_n$
Cost function:
$$
J(\theta_0,\theta_1,...,\theta_n) = \frac{1}{2m}\Sigma (h_\theta(x^{(i)})-y^{(i)})^2
$$
Sigma from i=1 to m&lt;/p&gt;
&lt;p&gt;Gradient descent:
repeat fellow:
$$
\theta_j  := \theta_j -\alpha\frac{\partial}{\partial \theta_j}J(\theta_0 , \theta_1)
$$
(simutaneously update for every $j$)&lt;/p&gt;
&lt;p&gt;As a result new algorithm will be like fellow:&lt;/p&gt;
&lt;p&gt;$$
\theta_j := \theta_j - \alpha \frac{1}{m} \Sigma (h_\theta(x^{(i)})-y^{(i)})^2 x_j^{(i)}
$$
(simultaneously update $\theta_j$ for $j=0,1，...,n$)
E.g from a data set with three features it may like follows:&lt;/p&gt;
&lt;p&gt;$$
\theta_0 := \theta_0 - \alpha \frac{1}{m} \Sigma (h_\theta(x^{(i)})-y^{(i)})^2 x_0^{(i)}$$
$$
\theta_1 := \theta_1 - \alpha \frac{1}{m} \Sigma (h_\theta(x^{(i)})-y^{(i)})^2 x_1^{(i)}$$
$$
\theta_2 := \theta_2 - \alpha \frac{1}{m} \Sigma (h_\theta(x^{(i)})-y^{(i)})^2 x_2^{(i)}
$$&lt;/p&gt;
&lt;h3&gt;Feature Scaling &amp;amp; Mean normalization&lt;/h3&gt;
&lt;p&gt;Idea:make sure features are on a similiar scale.&lt;/p&gt;
&lt;p&gt;E.g. $x_1$ = size (0-2000 feet^2) while $x_2$ = number of bedrooms (1-5)&lt;/p&gt;
&lt;h3&gt;Learning rate&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;if $\alpha$ is too small: slow convergence&lt;/li&gt;
&lt;li&gt;if $\alpha$ is too large: $J(\theta)$ may not decrease on every iteration; may not converge&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Make sure gradient descent is working correctly.Final goal is converge the cost function.&lt;/p&gt;
&lt;h3&gt;Features and Polynomial Regression&lt;/h3&gt;
&lt;p&gt;E.g. Housing prices prediction
$$
h_\theta (x) = \theta_0 + \theta_1 \times frontage + \theta_2 \times depth
$$
Clearly frontage is the first feature $x_1$ and depth is the second feature $x_2$&lt;/p&gt;
&lt;p&gt;To decide which feature is the most important factor to the housing price, thereafter a polynomial regression can be taken:
$$
h_\theta (x) = \theta_0 + \theta_1 \times frontage + \theta_2 \times depth^2
$$
this formula can be written as:
$$
h_\theta (x) = \theta_0 + \theta_1 \times x_1 + \theta_2 \times x_2^2
$$
In this case, feature scaling is becoming increasingly important to get them comparable.&lt;/p&gt;
&lt;h3&gt;Classification Problem (Discrete)&lt;/h3&gt;
&lt;h4&gt;Sigmoid Function&lt;/h4&gt;
&lt;p&gt;Want outputs 0 or 1&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sigmoid Function(Logistic Function)&lt;/strong&gt;:
$$
g(z)= \frac{1}{1+e^{-z}}
$$&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/ML/Sfunc.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;while $z$ can be written as vector($\vec{w}$ is weights and $x$ is feature):
$$
z = \vec{w}x + b
$$&lt;/p&gt;
&lt;h4&gt;Decision Boundary&lt;/h4&gt;
&lt;p&gt;$$
f_{\vec{w},b}(x) = g(\vec{w}x+b) = \frac{1}{1+e^{-(\vec{w}x+b)}}=P(y=1|x;\vec{w},b)
$$&lt;/p&gt;
&lt;p&gt;Clearly the decision boundary is the threshold value of $f_{\vec{w},b}(x)$.&lt;/p&gt;
&lt;p&gt;$x$ is also can be replaced by $\vec{x}$ if there are multiple features.&lt;/p&gt;
&lt;p&gt;Clearly the descision boundary is
$$
\vec{w}x+b=0
$$&lt;/p&gt;
&lt;h4&gt;Cost Funciton&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/ML/logcost.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Since MSE under logistic regression is a non-convex function, using MSE as cost function index may get &quot;stuck&quot; at the inflection point of the function, causing error.&lt;/p&gt;
&lt;p&gt;Target:create or select a new cost function to make it convex.&lt;/p&gt;
&lt;p&gt;The loss function of logistic regression uses the log-likelihood loss function, also known as the cross-entropy loss function, which is used to measure the difference between the model prediction and the true label. The loss function is defined as:&lt;/p&gt;
&lt;p&gt;$$
L(\theta) = - \frac{1}{m} \sum_{i=1}^{m} \left[ y^{(i)} \log h_{\theta}(x^{(i)}) + (1 - y^{(i)}) \log (1 - h_{\theta}(x^{(i)})) \right]
$$
while&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$m$:sample amounts&lt;/li&gt;
&lt;li&gt;$y^{(i)}$: true label (0 or 1) of sample $i$&lt;/li&gt;
&lt;li&gt;$h_{\theta}(x^{(i)})$: The model predicts the probability of sample $i$, i.e. $P(y=1|x^{(i)})$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The goal of this loss function is to minimize the gap between the predicted and actual labels.&lt;/p&gt;
&lt;h4&gt;Training Logistic regression&lt;/h4&gt;
&lt;p&gt;Use gradient descent to minimize the cost function $J(\vec{w},b)$.&lt;/p&gt;
&lt;p&gt;we have
$$
\frac{\partial}{\partial w_j} J(\vec{w},b) = \frac{1}{m} \sum_{i=1}^{m} \left[ f_{\vec{w},b}(\vec{x}^{(i)}-y^{(i)})\right]x_j^{(i)}
$$
and
$$
\frac{\partial}{\partial b} J(\vec{w},b) = \frac{1}{m} \sum_{i=1}^{m} \left[ f_{\vec{w},b}(\vec{x}^{(i)}-y^{(i)})\right]
$$
what we need to do is update them simultaneously.&lt;/p&gt;
&lt;h2&gt;Unsupervised learning&lt;/h2&gt;
&lt;p&gt;Needs data set without labels&lt;/p&gt;
&lt;p&gt;TO: Automatically discover the internal patterns or structures of data, such as dividing data into different clusters, revealing the inherent laws of data, or simplifying data representation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt; : Extracting vocals from audio&lt;/p&gt;
&lt;h2&gt;决策树(Decision Tree)&lt;/h2&gt;
&lt;p&gt;E.g.：判断动物是否为猫
&lt;img src=&quot;/images/ML/DT.webp&quot; alt=&quot;&quot; /&gt;
决策树训练步骤&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认第一个决策节点(Node)后对样本进行分类 例如：待检测动物的耳朵是圆的还是尖的&lt;/li&gt;
&lt;li&gt;确认剩下的节点&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;需要解决的问题&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何确定每个节点所用来进行分类的特征(Feature)？如：是根据耳朵类型进行分类还是根据体型进行分类&lt;/li&gt;
&lt;li&gt;什么时候停止继续分类（split）？
&lt;ul&gt;
&lt;li&gt;某节点能够做到100%的分类时（如有猫DNA的动物一定是猫，不可能是其他）&lt;/li&gt;
&lt;li&gt;某节点之后决策树溢出&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;继续进行决策所带来的提升低于阈值&lt;/li&gt;
&lt;li&gt;节点中的样本数量过少&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>以TPS5430为例进行DC-DC选型与设计</title><link>https://arkmostima.pages.dev/posts/%E4%BB%A5tps5430%E4%B8%BA%E4%BE%8B%E8%BF%9B%E8%A1%8Cdc-dc%E9%80%89%E5%9E%8B%E4%B8%8E%E8%AE%BE%E8%AE%A1/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E4%BB%A5tps5430%E4%B8%BA%E4%BE%8B%E8%BF%9B%E8%A1%8Cdc-dc%E9%80%89%E5%9E%8B%E4%B8%8E%E8%AE%BE%E8%AE%A1/</guid><description>原文来自德州仪器（TI）：TPS5430 Datasheet</description><pubDate>Sun, 18 Aug 2024 16:49:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;原文来自德州仪器（TI）&lt;/em&gt;：&lt;a href=&quot;https://www.ti.com.cn/product/cn/TPS5430?bm-verify=AAQAAAAJ_____9dYxHNK53iaDtVqyxPQxRcsrzMOM-c86S6ivd470tCxI7v2y_3iLaQxnR81kWkBePuyKvYKyC-Kf15mbkeVGi8nAGHJ0TZ3ENp-6GgUlTcvMy_qdx-euoFt1ZFv6JxkzW-Dd3OlaLFZQaxAsFpiD3EHrvgSZyrsjhPFZukuNU6g3lsGi53NSBPq24SEFjJVkxhff7hPbsSkYNRWKgMWat5cOPLkdhhwYkWjjWZCYxQ19lVbHzjbGrfD90mIptd-Xo2P9gBYm2ZBpeDhZ5g5dlGNhcbA7Z9fEjLipdCzrhXmW1HXWL3ZbWTD&quot;&gt;TPS5430 Datasheet&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;本文将以德州仪器（TI）TPS5430 降压DC-DC转换器为例，剖析手册中的参数与技术指标，并进行选型方面的介绍，同时简要介绍高频DC-DC设计相关内容。&lt;/p&gt;
&lt;h2&gt;特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;宽输入电压范围：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TPS5430：5.5V 至 36V&lt;/li&gt;
&lt;li&gt;TPS5431：5.5V 至 23V&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;高达 3A 的连续（4A 峰值）输出电流&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过 100mΩ 集成式 MOSFET 开关实现高达 95%的高效率&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;宽输出电压范围：可调节为低至 1.22V，初始精度为 1.5%&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部补偿可最大限度减少外部器件数量&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;适用于小型滤波器尺寸的固定 500kHz 开关频率&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;通过输入电压前馈改进线路调整和瞬态响应&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;系统受过流限制、过压保护和热关断的保护&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;–40°C 至 125°C 的工作结温范围&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;采用小型热增强型 8 引脚 SO PowerPAD™ 集成电路封装&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用 TPS5430 并借助 WEBENCH® Power Designer 创建定制设计&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;应用&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;消费类：机顶盒、DVD 显示屏、LCD 显示屏&lt;/li&gt;
&lt;li&gt;工业用和车载音频电源&lt;/li&gt;
&lt;li&gt;电池充电器、大功率 LED 电源&lt;/li&gt;
&lt;li&gt;12V 和 24V 分布式电源系统&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;说明&lt;/h2&gt;
&lt;p&gt;TPS543x 是一款高输出电流 PWM 转换器，集成了低电阻、高侧 N 沟道 MOSFET。具有所列的特性的基板上还包括高性能电压误差放大器（可在瞬态条件下提供高稳压精度）、欠压锁定电路（用于防止在输入电压达到 5.5V 前启动）、内部设置的慢启动电路（用于限制浪涌电流）以及电压前馈电路（用于改进瞬态响应）。通过使用 ENA 引脚，关断电源电流通常可减少到15μA。其他特性包括高电平有效使能端、过流限制、过压保护和热关断。为降低设计复杂性并减少外部元件数量，对 TPS543x 反馈环路进行内部补偿。TPS5431可采用高达 23V 的电源轨运行。TPS5430 可调节多种电源，包括 24V 总线。&lt;/p&gt;
&lt;p&gt;TPS543x 器件采用热增强型且易于使用的 8 引脚 SOIC PowerPAD 集成电路封装。TI 提供评估模块和Designer 软件工具，协助快速实现高性能电源设计，满足迫切的设备开发周期要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/dcdc/1.webp&quot; alt=&quot;简化原理图与效率-输出电流关系&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有关&lt;em&gt;效率&lt;/em&gt;：DC-DC转换器的效率（Efficiency）表示输入功率中有多少被成功转换为输出功率，而非被损耗。它通常以百分比表示。效率高意味着大部分能量都被有效转换，只有少量能量因开关损耗、导通损耗、磁损耗等形式而被消耗为热量。DC-DC 转换器的效率通常会随着输入电压、输出电流、负载条件等变化而有所不同。一般来说，DC-DC 转换器的效率可以达到 85% 至 95% 甚至更高。较高的效率在电源设计中至关重要，因为它直接影响系统的功耗和热管理设计。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;引脚配置和功能&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;/images/dcdc/2.webp&quot; alt=&quot;引脚配置和功能&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;应当注意&lt;/em&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;VSENSE&lt;/strong&gt;：反馈回路对噪声敏感，应当在PCB设计时进行特殊处理。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ENA&lt;/strong&gt;：与其他型号的IC不同，TPS5430的使能在悬空（Float）时开启，低于0.5V即外部拉低时关闭。不过经验表明ENA被外部上拉时也能够开启使能。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PH&lt;/strong&gt;：即输出端。IC内部集成有N-MOSFET，其源极（S）即连接至PH引脚。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;规格&lt;/h2&gt;
&lt;h3&gt;绝对最大额定值&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/dcdc/3.webp&quot; alt=&quot;绝对最大额定值&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;应当注意&lt;/em&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;所有输入引脚禁止反接，即使设计有外部反接保护电路。&lt;/li&gt;
&lt;li&gt;ENA引脚上拉后输入不得超过7V。&lt;/li&gt;
&lt;li&gt;接近 VIN 引脚的绝对最大额定值可能会导致 PH 引脚上的电压超过绝对最大额定值。&lt;/li&gt;
&lt;li&gt;IC输出端可能会在瞬态出现负电压脉冲，其一般由高di/dt与寄生电感共同作用引起。参见：&lt;a href=&quot;https://www.novosns.com/technical-articles-163&quot;&gt;如何解决负压脉冲&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;表中的“拉电流”原文为“Source Current”，通常指的是FET向负载提供电流的能力。在开关模式中，FET在导通时从电源“拉”电流并传输到负载。当MOSFET处于关闭状态时，PH 引脚会呈现高阻抗状态。表中 PH 漏电流指的是在这种高阻抗状态下，PH 引脚仍然有少量的电流从引脚流出或流入（一般是流出）。手册中标注的 10 $\mu A$ 漏电流，表示在开关管关闭时，PH 引脚由于漏电效应产生的微小电流。这种电流通常很小，不会对系统性能造成显著影响，但在某些对精度要求较高的应用中，这些小电流可能需要考虑。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3&gt;ESD Ratings&lt;/h3&gt;
&lt;p&gt;静电放电测试项，此处略。&lt;/p&gt;
</content:encoded></item><item><title>电机驱动类PCB布局的最佳实践</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E9%A9%B1%E5%8A%A8%E7%B1%BBpcb%E5%B8%83%E5%B1%80%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E9%A9%B1%E5%8A%A8%E7%B1%BBpcb%E5%B8%83%E5%B1%80%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5/</guid><description>原文转自德州仪器（TI）Application Note</description><pubDate>Wed, 14 Aug 2024 06:05:09 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;原文转自德州仪器（TI）Application Note&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;简介&lt;/h1&gt;
&lt;p&gt;电机驱动系统的 PCB 设计并非易事，需要特殊的考虑和技术才能实现最佳性能。电源效率、高速开关频率、低噪声抖动和紧凑的电路板设计是设计人员在布局电机驱动系统时必须考虑的几个主要因素。德州仪器的 DRV 设备非常适合此类系统，因为它们高度集成且配备了保护电路。本应用报告的目标是强调使用 DRV 设备时电机驱动布局的主要因素，并为高性能解决方案提供最佳实践指南，以减少热应力、优化效率并最大限度地降低电机驱动应用中的噪音。&lt;/p&gt;
&lt;h1&gt;接地优化&lt;/h1&gt;
&lt;p&gt;任何良好的接地方案的目标都是为 IC 及其周围电路提供稳定的参考（Reference），而不受噪声和其他振荡的影响。本节介绍不同的接地技术、接地的常见挑战、使用接地平面的最佳方式以及双层板的接地注意事项。&lt;/p&gt;
&lt;h2&gt;常用术语/连接方式&lt;/h2&gt;
&lt;p&gt;本节中使用的术语定义如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;单点接地（Single Point）&lt;/strong&gt;：在单点分布中，所有参考点都源自同一源，从而确保每个负载都有自己的不间断接地路径（见图 1-1 右）。建议将此连接用于电源分布线。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;星形接地（Star Ground）&lt;/strong&gt;：在星形接地分布中，所有参考点都位于中心；但是，源可能不集中。此方法平衡了所有资源之间的公共阻抗（图 1-1 左）。建议将此连接用于信号线。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-1.webp&quot; alt=&quot;图1-1 星形接地与单点接地的功率分布&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;分区（Partitioning）&lt;/strong&gt;：在使用分区接地方案的PCB中，数字、模拟和高功率信号有自己独立的区域（见图 1-2）。这种分离不是数字和模拟接地的物理分区。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;网格化（Grid）&lt;/strong&gt;：网格化使整个电路板上的接地路径连续，以确保每个信号都有一条返回源的路径（见图 1-3）。这种做法涉及对元件放置、接地填充、过孔放置和走线路径进行细微更改，以最大限度地减少返回地面的路径。网格化有效地创建了一个更加互连的接地平面，可以降低噪声并降低电源和负载之间的阻抗。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-2.webp&quot; alt=&quot;图1-2 数字-模拟地分离与地平面分区&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图 1-3 显示了使用接地网格实现有效接地平面的示例。在布局中实施网格化所做的更改很小，这表明小的改动可以产生大影响。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-3.webp&quot; alt=&quot;图1-3 将地面填充和走线网格化以形成接地平面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在图 1-3 的示例中，布局 A 和布局 B 是顶层和底层，只留下接地填充、接地走线和前后层之间的过孔。图 1-3 中的布局 C 是电路板接地布线的简单棒状图。每根棒状图或腿代表接地导体的路径。大多数走线仅在一端连接。移除大多数单端走线后，图 1-3 中的布局 D 显示了整个电路板上的接地布线方式；布线上任意两点之间只有一条路径。&lt;/p&gt;
&lt;p&gt;图 1-3 中的布局 E、布局 F、布局 G 和布局 H 显示了为实现网格接地而修改的设计。在图 1-3 中的布局 E 和布局 F 中添加了一些走线（以实心黑色显示）并移动了几何图形（由箭头指示）。图 1-3 中的布局 G 显示了修改后的接地棒状图。两端连接的完整走线形成更完整的导体。比较图 1-3 中的布局 H 和布局 D，网格接地已创建了广泛的互连网络，从而创建了所需的网格。其效果几乎与实际接地平面一样有效。&lt;/p&gt;
&lt;h2&gt;使用地平面&lt;/h2&gt;
&lt;p&gt;在 4 层板或更大的 2 层板设计中，建议使用接地平面。将 PCB 的一层作为连续接地平面可使每个信号具有最短的返回路径，并减少耦合和干扰。建议通过仔细布线信号线来尽量减少接地平面的不连续性，并将过孔彼此分开以防止平面中断。有关过孔放置的更多信息，请参阅下文。&lt;/p&gt;
&lt;p&gt;大多数 DRV 设备都有一个用作接地的导热焊盘，并使用接地铜来散热。图 1-4 显示了公共和分离接地平面的良好布局示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-4.webp&quot; alt=&quot;图1-4 公共接地平面与分离接地平面&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：如果设计是空间受限的 2 层电路板，则接地平面并不总是可行的。在这种情况下，PCB 布局变得更加重要。必须小心谨慎，确保高电流路径远离敏感信号。PCB 的噪声部分（例如功率级 FET、自举电路和电荷泵）通常包含高噪声和纹波，应与此类信号隔离。&lt;/p&gt;
&lt;h2&gt;常见问题&lt;/h2&gt;
&lt;h3&gt;电容和电感耦合&lt;/h3&gt;
&lt;p&gt;当两条走线平行延伸时，即使距离很短，也可能发生电容或电感耦合。当一条走线的上升沿导致另一条走线的上升沿时，就会发生电容耦合。当一条走线具有上升沿或下降沿，而第二条走线具有耦合的下降沿或上升沿时，就会发生电感耦合。电容耦合比电感耦合更频繁地发生。&lt;/p&gt;
&lt;p&gt;耦合的严重程度取决于走线的长度、开关频率、电压变化和走线之间的距离。为了减少电容耦合，请将嘈杂的信号走线远离重要的数字和模拟信号。尝试在接地平面上布线。&lt;/p&gt;
&lt;p&gt;在带有预驱动器的开关应用中，必须特别注意确保预驱动器的高电流承载接地平面与 IC 其余部分的接地平面物理隔离。这两个地可以在星形点或单点接地位置连接，如上一节所述。&lt;/p&gt;
&lt;h3&gt;共模噪声和差模噪声&lt;/h3&gt;
&lt;p&gt;差模噪声沿着迹线传播到接收设备，然后通过返回路径返回到源，从而导致两条迹线之间产生差分电压。当信号和返回路径上都产生电压时，就会发生共模噪声，这是由于共用阻抗上的电压降引起的。地弹就是共模噪声的一个例子。通过确保所有返回源的路径都是宽、短且阻抗低的迹线，可以大大降低发生此问题的可能性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-5.webp&quot; alt=&quot;图1-5 差模噪声与共模噪声&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;考虑EMC&lt;/h3&gt;
&lt;p&gt;电磁兼容性 (EMC) 主要取决于布局和组件之间的电气连接。&lt;/p&gt;
&lt;p&gt;每个信号的返回路径必须从资源流向信号源，从而产生电流环路。该线路环路会形成一个天线，可以辐射电磁能量，该能量由电流幅度、信号的重复频率和电流环路的几何面积决定。建议尽量减少这些电流环路以获得最佳 EMC 性能，图 1-6 显示了常见的电流环路类型。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/1-6.webp&quot; alt=&quot;图1-6 系统中的电流路径&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图 1-6 中的电源线形成环路 A–C–D–B 和 A–E–F–B。系统运行所需的能量由这些线路传导。&lt;/p&gt;
&lt;p&gt;环路 L-M-F-D、N-Q-P-F 和 G-H-J-K 由信号和控制形成。如果不考虑系统外部的线路，这些线路所包围的面积通常很小。但是，必须在高频下考虑这些线路，因为它们通常会传输会影响 EMC 性能的信号。&lt;/p&gt;
&lt;p&gt;当连接器、接头或其他组件破坏接地平面时，也会形成电流环路。这会导致开关电流的高频分量在电路板周围传播得更远，从而有效地形成一个大环路。这种情况也可能发生在过孔中。&lt;/p&gt;
&lt;h1&gt;热效应&lt;/h1&gt;
&lt;p&gt;电机驱动器并非理想设备，在实际应用中，部分功率会以热量的形式在内部耗散。必须在驱动器损坏之前处理转换为热量的能量。正确的 PCB 设计可以有效消除效率低下产生的热量，并使设备保持在推荐温度。&lt;/p&gt;
&lt;h2&gt;PCB 传导和对流&lt;/h2&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;导热 Pad&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以这三条路径为例，导热焊盘是热量从设备传出的最有效路径，其次是封装材料，最后是键合线。导热焊盘集成电路封装中使用的技术从芯片到外部铜平面创建了一条低热阻路径。因此，导热焊盘可以有效地将大量热量从芯片传导出去。驱动器下方的导热焊盘应足够大，以覆盖导热焊盘的整个区域，并且仍然包括 PCB 其他部分的大表面积。导热焊盘还应紧密结合到底部接地平面，并在导热焊盘正下方放置几个导热过孔。图 2-1 显示了设备芯片中产生的热量使用的出口路径示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/2-1.webp&quot; alt=&quot;图2-1 导热焊盘封装的横截面以及由此产生的热传递&quot; /&gt;&lt;/p&gt;
&lt;p&gt;将顶部和底部接地平面连接到驱动器的导热焊盘可显著提高 PCB 设计中的散热量。因此，在布局中应将这些平面做得尽可能大。&lt;/p&gt;
&lt;h2&gt;连续Top-Layer导热焊盘&lt;/h2&gt;
&lt;p&gt;将导热焊盘连接到实心铜平面是创建驱动器芯片产生的热量出口路径的重要要求。为了使热量从设备中流出，铜平面必须从导热焊盘连续到电路板上的其他区域。最佳做法是包括一条从驱动器下方的铜填充到宽阔的高表面积平面的宽出口路径。如果这些平面被中断，热量的出口路径就会收缩，从而增加热阻。热阻的增加会导致导热焊盘和同一平面上更宽的表面积之间的温差更大。图 2-2 显示了驱动器下方收缩和连续接地浇注引起的温升示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/2-2.webp&quot; alt=&quot;图2-2 破碎地与连续地浇注热图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在驱动器下方放置连续的铜导热焊盘对于设备高效冷却非常重要。
将宽路径整合到高表面积平面可使驱动器导热焊盘和环境空气温度之间的热阻保持在最低水平。&lt;/p&gt;
&lt;h2&gt;铜厚 （Copper Thickness）&lt;/h2&gt;
&lt;p&gt;虽然连续、宽阔的平面会降低热阻，但平面上的铜厚度也是 PCB 热性能的关键考虑因素。通过增加 PCB 上的铜镀层厚度，平面的有效热阻会降低。下式可计算铜厚度与平面面积之间的关系：&lt;/p&gt;
&lt;p&gt;$$
\theta_{cu}=(\frac{1}{\lambda_{cu}}\times length ) / Area
$$&lt;/p&gt;
&lt;p&gt;假设长度和宽度为 1 厘米，镀层厚度为 1 盎司 (0.0035 厘米)，则与驱动器横向连接的铜平面的近似热阻可按照下式计算：&lt;/p&gt;
&lt;p&gt;$$
\theta_{cu}=(\frac{1}{\lambda_{cu}}\times length ) / Area = (25 ℃ \quad cm/W \times 1 cm) / 1 cm \times 0.0035 cm =71.4 ℃/W
$$&lt;/p&gt;
&lt;p&gt;如果铜厚度加倍，相同尺寸平面的热阻就会减半。连接到驱动器的接地平面上的铜厚度越厚，有助于高效地将热量从设备传导到周围空气中，而不会导致电路板上出现明显的温差。&lt;/p&gt;
&lt;h2&gt;散热过孔连接方式（Thermal via Connections）&lt;/h2&gt;
&lt;p&gt;散热过孔应将顶层和底层连接在一起，以便热量可以从 IC 散发到两层。散热过孔不应使用&lt;em&gt;热风焊盘&lt;/em&gt;（Thermal Relief，即十字花焊盘，为了防止因散热过快而导致的虚焊，在电源和地的过孔采用十字花的工艺连接，减少了接触面积，降低了散热速度，方便焊接）连接，因为热量从顶层通过通孔流到底层的路径受到限制。这种热流路径的收缩导致通孔周围顶层剩余部分的温度升高。直接连接通孔可使通孔和铜层之间的热阻尽可能低。散热通孔应与内部接地平面连接，并在镀通孔的整个圆周周围进行完整连接。不要用焊料掩模覆盖通孔，否则会导致过多的空洞。图 2-3 显示了散热通孔和直接连接通孔之间的温差。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;参见&lt;/em&gt;：&lt;a href=&quot;https://ee.ofweek.com/2021-01/ART-11000-2800-30482578.html&quot;&gt;如何使引脚接地为十字焊盘，但过孔为完全接地?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/2-3.webp&quot; alt=&quot;图2-3 热风焊盘（右）与直接过孔连接（左）热力图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;散热焊盘接头将平面与过孔或组件电连接起来，但它们会减少组件或过孔与平面之间的热流。这样做的目的是让烙铁或回流炉只加热组件并确保可靠的焊接连接。这种方法对于不需要过孔在平面之间进行热传导的应用非常有效。然而，电源应用（例如电机驱动器）要求将这些过孔直接粘合到平面上，以实现层间的最佳热性能。&lt;/p&gt;
&lt;h2&gt;散热过孔宽度 （Thermal Via Width）&lt;/h2&gt;
&lt;p&gt;虽然导热焊盘在芯片和 PCB 顶部接地平面之间提供了低阻抗热路径，但应考虑连接顶部和底部接地平面的过孔的热阻抗。&lt;strong&gt;德州仪器建议在导热焊盘正下方设置直径为 20 mil 的导热过孔，孔径为 8 mil。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;减小散热孔的直径或增加孔的尺寸会增加热阻。建议的 8 mil 孔尺寸和 20 mil 直径需要从散热焊盘到底层的最小焊料芯吸，并将过孔的热阻保持在最低水平。图 2-4 显示了推荐的过孔放置位置。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/2-4.webp&quot; alt=&quot;图2-4 散热过孔放置&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;PCB散热设计小结&lt;/h2&gt;
&lt;p&gt;总结一下热设计，电机驱动器系统中热设计的主要考虑因素如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;导热焊盘（Thermal Pad）连接是器件芯片导热的最有效途径&lt;/li&gt;
&lt;li&gt;使用从导热焊盘到接地平面的连续顶层灌注&lt;/li&gt;
&lt;li&gt;尽可能使用 1.5 或 2 盎司铜厚&lt;/li&gt;
&lt;li&gt;使用直接连接热通孔&lt;/li&gt;
&lt;li&gt;使用 8 mil x 20 mil 的导热过孔（Via）尺寸，以避免过多的焊料渗入&lt;/li&gt;
&lt;li&gt;将热通孔分组为阵列，以最小化平面之间的热阻&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;过孔（Vias）&lt;/h1&gt;
&lt;p&gt;PCB 上的通孔在电路板的不同层上具有两个位于相应位置的焊盘，它们通过穿过电路板的孔进行电连接。该孔通过电镀制成导电的。有几种类型的通孔，例如盲孔、埋孔和热孔。对于电机驱动器 PCB 设计，重点是普通通孔和热孔。&lt;/p&gt;
&lt;p&gt;通孔经常用于信号轨道和电源轨道的 PCB 布线。对于信号连接，电流很小（微安到毫安），一个或两个通孔可能足以将信号路由到另一层。对于电源连接，可以在电源或接地迹线上添加多通孔或“通孔拼接”，以确保层间以及电源和接地平面之间的低阻抗连接。还可以添加多通孔以将设备产生的热量散发到其他电路板层，如第上一节所述。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/3-1.webp&quot; alt=&quot;图3-1 底部铜平面的过孔连接&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;过孔电流承载量（Via Current Capacity）&lt;/h2&gt;
&lt;p&gt;在电机驱动器 PCB 设计中，多通孔通常用于层间高电流连接。提供适当的通孔尺寸和数量以实现低电阻和长期可靠性非常重要。通常，通孔的直径至少应为走线的长度。在使用铜平面作为走线的情况下，多个通孔应位于电流进入或离开元件引脚的位置附近。&lt;/p&gt;
&lt;p&gt;下表列出了按照 IPC-2152 标准，1 盎司 PCB 在温度升高 10°C 时不同通孔直径的电流容量。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Via 	 Diameter&lt;/th&gt;
&lt;th&gt;Current Capacity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;6  	    mil&lt;/td&gt;
&lt;td&gt;0.2       A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8  	   mil&lt;/td&gt;
&lt;td&gt;0.55      A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10 	   mil&lt;/td&gt;
&lt;td&gt;0.81      A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;12 	    mil&lt;/td&gt;
&lt;td&gt;0.84      A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;16 	   mil&lt;/td&gt;
&lt;td&gt;1.1       A&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;过孔布局建议&lt;/h2&gt;
&lt;h3&gt;多过孔布局&lt;/h3&gt;
&lt;p&gt;多通孔对于低寄生接地和大电流连接非常有用。图 3-2、图 3-3 和图 3-4 显示了多通孔放置在电路板设计的不同位置的示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/3-2.webp&quot; alt=&quot;图3-2 用于去耦接地的多通孔&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/3-3.webp&quot; alt=&quot;图3-3 用于电流检测接地的多通孔&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/3-4.webp&quot; alt=&quot;图3-4 用于大电流连接的多通孔&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;过孔放置&lt;/h3&gt;
&lt;p&gt;尽管过孔很小，但它们会占用 PCB 和接地平面上的空间。将多个过孔布线在一起会在平面上产生间隙，并影响电流和接地回路。良好的过孔布局可为所有信号创建充足的返回路径。图 3-5 显示了如何避免过孔造成不必要的接地平面分裂的示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/3-5.webp&quot; alt=&quot;图3-5 避免过孔分裂接地平面&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;通用布线技巧&lt;/h1&gt;
&lt;p&gt;在进行电机驱动器 PCB 设计时，请遵循以下通用布线技术：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使栅极驱动器走线尽可能宽且长度尽可能短。对于至少 1 盎司铜，从 20 密耳的走线宽度开始，如果大电流需要，则走线宽度可以更大。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-1.webp&quot; alt=&quot;图4-1 DRV8323xEVM 栅极信号线&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高端FET栅极的信号走线应当尽可能靠近开关节点走线，以尽量减少电感、环路面积以及 dv/dt 开关引起噪声的可能性。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-2.webp&quot; alt=&quot;图4-2 平行栅极信号线&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不要使用直角走线。走线中的 90 度弯曲会起到阻抗的作用，并可能导致电流反射。当电机的相位切换时，急弯可能会引发电磁干扰 (EMI) 问题。圆形弯曲是理想的，但在实际设计中可能不切实际。拐角布线的最佳做法是使用钝角。图 4-3 显示了走线中不同角度的示例。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-3.webp&quot; alt=&quot;图4-3 正确的走线角度&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将过孔过渡到焊盘，特别是将输出引脚上的细线过渡到粗线。泪滴（Teardrop）技术可降低信号转换的热应力。该技术还可避免线的开裂，并使线在机械上更坚固。泪滴技术适用于从小信号过渡到通孔焊盘的情况。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-4.webp&quot; alt=&quot;图4-4 过孔至焊盘的过渡&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;绕物体布线时，应以平行对的方式布线，以避免因分叉布线而导致的差分阻抗和不连续性。这种方法对于电流检测放大器的信号非常重要。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-5.webp&quot; alt=&quot;图4-5 平行走线&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将无源元件（例如源匹配电阻或交流耦合电容）放置在信号路径内，并将它们并排放置。平行放置元件可产生更宽的走线间距。不建议交错放置元件，因为这会造成狭窄的区域。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-6.webp&quot; alt=&quot;图4-6 推荐的元器件放置&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将电路的模拟部分和数字部分分开接地是抑制噪声的最简单、最有效的方法之一。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/4-7.webp&quot; alt=&quot;图4-7 数字地与模拟地的隔离&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;Bulk电容和旁路电容的放置&lt;/h1&gt;
&lt;h2&gt;Bulk电容放置&lt;/h2&gt;
&lt;p&gt;在电机驱动系统设计中，Bulk电容器（大容量电容器）可最大限度地减少低频电流瞬变的影响，并存储电荷以提供电机驱动器切换时所需的大电流。选择Bulk电容器时，请考虑电机系统所需的最高电流、电源电压纹波和电机类型。&lt;/p&gt;
&lt;p&gt;使用Bulk电解电容帮助从通过电机绕组驱动的电流中获取低频、高值电流。这些电容器通常大于 10 μF，具体取决于应用要求。&lt;/p&gt;
&lt;p&gt;将所有Bulk电容器放置在电源模块或电路板的电源入口点附近。TI 建议每个Bulk电容器都有多个通孔将焊盘连接到相应的电源层。TI 还建议所有Bulk电容器都具有低等效串联电阻 (ESR)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-1.webp&quot; alt=&quot;图5-1 Bulk电容多过孔放置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-2.webp&quot; alt=&quot;图5-2 Bulk电容和旁路电容放置&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;电荷泵电容器（Charge Pump Capacitor）和自举电容&lt;/h2&gt;
&lt;p&gt;TI 的大多数电机驱动设备 (DRVxx) 都使用电荷泵或自举电容器来完全切换高侧 N 沟道 MOSFET 的栅极。将这些电容器放置在尽可能靠近电机驱动设备的位置。在图 5-3 中，C4 电容器是用于从 VM 到 VCP 引脚的电荷泵输出的电容，而 C7 电容器是用于电荷泵切换节点的电容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-3.webp&quot; alt=&quot;图5-3 电荷泵电容放置&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;旁路（Bypass）/解耦（Decoupling）电容放置&lt;/h2&gt;
&lt;h3&gt;靠近电源&lt;/h3&gt;
&lt;p&gt;旁路电容用于将高频噪声最小化，以减少进入 DRV 器件电源引脚的噪声。TI 建议将电容尽可能靠近器件的电源输入引脚和接地引脚。如果旁路电容和器件之间的走线长度没有最小化，它们可能会在旁路电容要过滤的高频下产生电感。走线电感增加的阻抗可能导致电源引脚处的电压或电流产生振铃，从而导致 EMI 并影响数字或模拟电路的性能。最佳做法是将值较小的电容尽可能靠近器件放置，以最大限度地减少走线电感的影响。将值较大的电容连接到值较小的电容之后，因为随着电容值的增加，电感变得更小。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-4.webp&quot; alt=&quot;图5-4 靠近设备的解耦电容&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如上文所示，使用的过孔越多，阻抗越低。TI 强烈建议在电源层和接地层使用多个过孔。将过孔直接放置在电容器的安装焊盘上是一种有效的方法，可以最大限度地减少布线面积，同时仍能实现电流流动布线。请遵循以下旁路电容器指南：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不要在旁路电容器和有源器件之间使用过孔。可视化高频电流流动并尽可能减少高频电流环路。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;确保旁路电容器与有源元件位于同一层，以获得最佳效果。不要在旁路电容器引脚和 IC 电源或接地引脚之间放置过孔。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将过孔布线到旁路电容器中，然后再布线到有源元件中。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用最多的过孔和最宽的走线以获得最佳布局。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;旁路电容器越近越好（小于 0.5 厘米，0.2 英寸）。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不要使用大于 3:1 的长宽比。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-5.webp&quot; alt=&quot;图5-5 旁路电容的放置&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;靠近功率级（Power Stage）&lt;/h3&gt;
&lt;p&gt;对于功率级上的旁路电容，请使用小型陶瓷电容器来衰减由 MOSFET 和其他寄生电容切换引起的高频电流。这些电容器的电容值通常小于 10 μF，具体取决于应用要求。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-6.webp&quot; alt=&quot;图5-6 设置有旁路电容的H桥&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;靠近开关电流源（Switch Current Source）&lt;/h3&gt;
&lt;p&gt;正确布局和放置这些电容器对于确保其有效性至关重要。电容和开关电流源之间的任何额外寄生电感都会降低其效果。理想情况下，将电容器放置在尽可能靠近开关电流源的位置，在本例中为电机和 MOSFET。图 5-7 显示了基于上一个原理图示例的示例布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-7.webp&quot; alt=&quot;图5-7 旁路电容的放置&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;靠近电流测量运放&lt;/h3&gt;
&lt;p&gt;对于带有集成电流检测放大器 (CSA) 的设备，TI 建议将额外的去耦电容尽可能靠近检测引脚放置，并使用大约 1 nF 的值。图 5-8 显示了去耦电容 C12、C13 和 C17。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-8.webp&quot; alt=&quot;图5-8 SNx和SPx布局&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;靠近稳压器（Voltage Regulators）&lt;/h3&gt;
&lt;p&gt;对于带有稳压器的器件，请将电容器尽可能靠近引脚放置。尽量减少接地引脚的接地回路。例如，图 5-9 显示 C18 电容器尽可能靠近 DVDD 稳压器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/5-9.webp&quot; alt=&quot;图5-9 DVDD的接地回路&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;MOSFET的放置与功率级布线&lt;/h1&gt;
&lt;p&gt;栅极驱动器和功率 MOSFET 的放置对于预驱动器电机驱动解决方案的正确功能和最佳性能至关重要。对于带有集成 MOSFET 的电机驱动器，例如 DRV8870、DRV8313、DRV10987、DRV10983-Q1 和 DRV8873-Q1，正确的布线已在内部完成。对于栅极驱动器，例如 DRV8701、DRV8304、DRV8306、DRV8323、DRV8343-Q1 和 DRV8353，仔细规划 PCB 的布局和功率 MOSFET 的放置非常重要。以下部分介绍了一些常见的 MOSFET 拓扑，并介绍了使用常见电机驱动架构的基本布局示例。&lt;/p&gt;
&lt;h2&gt;常见功率 MOSFET 封装&lt;/h2&gt;
&lt;p&gt;本节介绍一些常见的 N 沟道功率 MOSFET 封装类型。大多数
功率 MOSFET 都有这四种选项之一。通过了解封装类型、尺寸和引脚排列，可以更好地设计 PCB 以获得最佳解决方案。&lt;/p&gt;
&lt;p&gt;图 6-1 中的符号代表 N 沟道功率 MOSFET。虽然 MOSFET 是一种四端器件，具有源极、栅极、漏极和体极，但体极通常位于源极端子。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-1.webp&quot; alt=&quot;图6-1 N-MOSFET符号&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MOSFET 封装与功率级和散热直接相关。不同的封装具有不同的布线规则。&lt;/p&gt;
&lt;h3&gt;DPAK&lt;/h3&gt;
&lt;p&gt;图 6-2 所示的 DPAK (SOT-252) 封装是业界最常用的封装之一。此封装在尺寸和性能之间实现了折衷。DPAK 封装通常用于高功率 MOSFET 和稳压器。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-2.webp&quot; alt=&quot;图6-2 DPAK封装&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;D2PAK&lt;/h3&gt;
&lt;p&gt;D2PAK（SOT-252）封装（见图 6-3）是 DPAK 封装的较大版本，可以提供更好的散热效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-3.webp&quot; alt=&quot;图6-3 D2PAK封装&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;TO-220&lt;/h3&gt;
&lt;p&gt;TO-220 封装（见图 6-4）是一种通孔 MOSFET 封装。直立片可用于安装散热器。使用这种封装的缺点是它需要更多空间，并且通常比表面贴装封装（例如 DPAK 和 D2PAK 封装）更高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-4.webp&quot; alt=&quot;图6-4 TO-220封装&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;8-Pin SON&lt;/h3&gt;
&lt;p&gt;8 引脚 SON 封装（见图 6-5）是最常见的无引线封装。此封装提供了一种解决方案，可实现最小的电路板空间和最佳性能。大多数引线封装具有相似的引脚排列。8 引脚 SON 封装的独特之处在于，与引线封装（TO-252 和 TO-220）相比，栅极引脚位于相反的一侧。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-5.webp&quot; alt=&quot;图6-5 8-Pin SON 封装&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;MOSFET布局设置&lt;/h2&gt;
&lt;p&gt;图 6-6 和图 6-7 显示了两种典型配置的常见 MOSFET 位置和布局；分别为半桥堆叠和半桥并排，适用于有引线和无引线封装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-6.webp&quot; alt=&quot;图6-6 半桥堆叠&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-7.webp&quot; alt=&quot;图6-7 半桥并排&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这些半桥布局可以重复用于多个半桥拓扑，包括 H 桥（两个半桥）、逆变器（三个半桥）和双 H 桥（四个半桥）。&lt;/p&gt;
&lt;h2&gt;功率级布局设计&lt;/h2&gt;
&lt;p&gt;在为功率 MOSFET 选择正确的位置后，下一步是确保布线正确。由于 MOSFET 用于高功率、电机驱动开关应用，因此设计对非理想布局引入的寄生效应很敏感。本节介绍了一些需要布线的关键信号以及管理这些信号的最佳实践。功率级如图 6-8 所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-8.webp&quot; alt=&quot;图6-8 带开关节点（红圈）的半桥功率级&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;开关节点（Switch Node）&lt;/h3&gt;
&lt;p&gt;开关节点是高端 MOSFET 的源极引脚和低端 MOSFET 的漏极引脚之间的连接，如图 6-10 所示。此节点是最终连接到负载（在本应用中为电机）的网络。开关节点是半桥配置中要路由的最关键信号，因为此网络上的信号具有高频、大电流特性。图 6-8 所示的电路具有由 PCB 和功率 MOSFET 引起的许多非理想寄生效应。图 6-9 显示了其中一些主要寄生效应，它们是开关节点振铃现象的主要原因。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-9.webp&quot; alt=&quot;图6-9 半桥寄生效应&quot; /&gt;&lt;/p&gt;
&lt;p&gt;开关节点振铃是由于 PCB 和功率 MOSFET 的寄生效应导致的开关节点上的 LC 振荡。开关节点振铃会导致 EMI 并产生过冲和下冲电压，这可能会违反 MOSFET 漏极-源极电压和栅极驱动器引脚的绝对最大额定值。它还会降低功率级的效率。&lt;/p&gt;
&lt;p&gt;可以通过外部措施和系统调整（降低斜率、外部缓冲器等）来解决开关节点振铃问题，但基本合理的布局可以解决许多主要问题。图 6-10 中的布局示例显示了最小化高端 MOSFET 源极和低端 MOSFET 漏极之间电感的设计。最佳做法是最小化铜平面连接的长度并最大化其宽度，并使用具有最小寄生电感的 MOSFET 封装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-10.webp&quot; alt=&quot;图6-10 开关节点布局样例&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;大电流循环路径（High-Current Loop Paths）&lt;/h3&gt;
&lt;p&gt;由于电机应用依赖于高开关电流，因此最小化高电流路径的总环路电感至关重要。最小化该电感可最大程度地减少电压纹波和噪声，并可减少对额外旁路电容的需求。&lt;/p&gt;
&lt;p&gt;在电机系统中，高电流环路从电源的正极开始，经过高端功率 MOSFET，经过电机绕组，经过相反的低端 MOSFET，再回到电源的负极。图 6-11 以 H 桥示例显示了此流程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-11.webp&quot; alt=&quot;图6-11 大电流循环路径样例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;应通过以下方式最小化高电流环路路径：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;对整个高电流环路使用正确的走线宽度。增加走线宽度可降低寄生电感。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;使用正确的布局来最小化元件之间的距离。减少走线长度可降低寄生电感。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;尽量减少高电流路径中的层跳转数量，并在使用高电流走线跳转层时使用正确的通孔尺寸和数量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;图 6-12 显示的是优化大电流环路的示例布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-12.webp&quot; alt=&quot;图6-12 大电流循环路径Layout样例&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;VDRAIN 检测引脚&lt;/h3&gt;
&lt;p&gt;VDRAIN 引脚用于感测高端 MOSFET 漏极电压。具有 VDRAIN 引脚的电机驱动设备必须经过一定的布线才能获得最佳性能。由于电压供应首先到达高端 MOSFET 的漏极，因此 VDRAIN 引脚的布线对于布局至关重要。VDRAIN
引脚为电压供应 (VM) 提供 Kelvin 连接，从而可以在发生过流事件时监控高端 MOSFET 的 VDS 电压（见图 6-13）。由于功率级中的 VM 连接通常由大铜平面和宽走线组成以支持所需的电流，因此平面的额外电感和压降可能会影响 VDS 测量精度。因此，将 VDRAIN 引脚通过一条走线直接布线到外部功率 MOSFET 的漏极。TI 建议在漏极附近使用 Net Tie 以尽量减少可能导致错误 OCP 故障的额外电感（见图 6-14）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-13.webp&quot; alt=&quot;图6-13 VDRAIN Kelvin 连接&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/6-14.webp&quot; alt=&quot;图6-14 Net Tie 高端MOSFET的漏级&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;有关 Kelvin 连接，参见&lt;/em&gt;：&lt;a href=&quot;https://www.testandmeasurementtips.com/the-basics-of-kelvin-connections-faq/&quot;&gt;The basics of Kelvin connections&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Net Tie 是一种在PCB设计中将两个或多个不同的网络在特定位置进行连接的技术，通常用于管理不同网络之间的连接关系。它是一种特殊的器件，形式上可以表现为一小段连接铜皮、过孔或其他类似的结构。有关更多 ，参见&lt;/em&gt;：&lt;a href=&quot;https://smtnet.com/library/files/upload/NetTies-and-How-to-Use-Them.pdf&quot;&gt;Net Ties and How to Use Them&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;电流检测放大器布线（Current Sense Amplifier Routing）&lt;/h1&gt;
&lt;p&gt;TI 的各种电机驱动器都包含具有内置电流感应功能的器件，其中大多数使用外部分流电阻作为测量源。将电流感应放大器与驱动器结合使用为电机接口提供了一体化解决方案，并允许以更低的成本实现更高质量的电流感应。图 7-1 显示了所有可用的电流感应拓扑。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-1.webp&quot; alt=&quot;图7-1 电流检测拓扑&quot; /&gt;&lt;/p&gt;
&lt;p&gt;这些设备内置的集成电流检测放大器 (CSA) 通常分为三类，每类都有各自的优点。以下将介绍这些类别。&lt;/p&gt;
&lt;h2&gt;单高端电流分流器（Single High-Side Current Shunt）&lt;/h2&gt;
&lt;p&gt;图 7-2 显示了高端（High-Side）电流检测电路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-2.webp&quot; alt=&quot;图7-2 高端电流检测&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用高侧电流检测的好处包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;直接测量电源电流&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;可以检测负载短路&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;不受地（GND）干扰&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;使用高端电流检测的缺点是它需要更多的共模电压。&lt;/p&gt;
&lt;h2&gt;单低端电流分流器（Single Low-Side Current Shunt）&lt;/h2&gt;
&lt;p&gt;图 7-3 显示了低端电流分流电路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-3.webp&quot; alt=&quot;图7-3 低端电流检测&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用低侧电流分流器的好处是它需要的共模电压较低。&lt;/p&gt;
&lt;p&gt;使用低侧电流分流器的缺点是它更容易受到接地噪声的影响，并且无法检测接地短路。&lt;/p&gt;
&lt;h2&gt;两相和三相电流分流放大器（Two-Phase and Three-Phase Current Shunt Amplifiers）&lt;/h2&gt;
&lt;p&gt;图 7-4 显示了具有两相和三相 CSA 的电路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-4.webp&quot; alt=&quot;图7-4 两相和三相 CSA &quot; /&gt;&lt;/p&gt;
&lt;p&gt;两相和三相 CSA 对电路板布局有利，因为它们对共模电压的要求较低。它们还可以单独测量每个通道，因此可用于更复杂的控制方案，例如磁场定向控制。&lt;/p&gt;
&lt;p&gt;在电路板布局中使用两相和三相 CSA 的缺点包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;对接地噪声的敏感性更高&lt;/li&gt;
&lt;li&gt;无法检测接地短路&lt;/li&gt;
&lt;li&gt;可能需要更多软件来实现总系统电流&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;器件选型&lt;/h2&gt;
&lt;p&gt;在选择检测电阻（精密电阻）时，需要在精度和功耗之间进行权衡。由于功率级中的大电流会流过检测电阻，因此所选电阻值必须很小，以将功耗保持在最低水平。对于大电流系统，电阻值通常以 mΩ 为单位。例如，驱动 20 A 电流并使用 1 mΩ 检测电阻的系统将从该电阻上耗散 400 mW。在这种情况下，CSA 的输入仅接收 20 mV 的信号。增加电阻值可以提高信噪比，但也会增加功耗。&lt;/p&gt;
&lt;p&gt;CSA 的性能参数也必须考虑。在针对系统中的最坏情况电流进行设计时，所选的分流电阻应防止电流检测输入引脚上的电压高于 CSA 的绝对最大额定值。在正常运行期间，此电压必须保持在差分电压范围的指定参数范围内。要选择检测电阻，请参阅器件数据表。&lt;/p&gt;
&lt;p&gt;对于使用外部增益电阻的器件（例如 DRV3201-Q1），请选择具有高精度的组件。组件不匹配会导致整个系统的共模和差模增益发生很大变化。&lt;/p&gt;
&lt;h2&gt;器件放置（Placement）&lt;/h2&gt;
&lt;p&gt;检测电阻的放置位置应与功率级的组件一致，以最大限度地减少走线阻抗。分流电阻也应放置在靠近 CSA 连接的位置，以降低在电路板上其他走线上耦合的可能性。&lt;/p&gt;
&lt;p&gt;对于高端电流检测，分流电阻应靠近电源和高端 MOSFET 源极之间的星点。对于使用外部增益电阻的高端电流检测设备（例如 DRV3205-Q1），分压器中的第一个电阻应放置在最靠近分流电阻的位置。其余组件应放置在最靠近设备的位置。&lt;/p&gt;
&lt;p&gt;对于低端电流检测，分流电阻应位于低端 MOSFET 的源极和功率级的星点接地连接之间。&lt;/p&gt;
&lt;p&gt;对于在两个或三个单独相位上具有分流电阻的系统，分流电阻应放置在相应低端 MOSFET 的源极和星点接地连接之间。&lt;/p&gt;
&lt;h2&gt;布线（Routing）&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;必须使用差分对来路由检测信号&lt;/strong&gt;。在差分对中，两个信号在布局中紧密耦合，并且走线必须从分流器或检测电阻器平行延伸到 IC 输入端的 CSA。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-5.webp&quot; alt=&quot;图7-5 CSA 布线 &quot; /&gt;&lt;/p&gt;
&lt;h2&gt;实用工具（Net Tie 和 差分对）&lt;/h2&gt;
&lt;p&gt;许多现代 CAD 工具都实现了可帮助布局工程师正确布线 PCB 的功能。本节中介绍的功能是 Altium Designer 工具的一部分；但是，许多其他工具也具有类似的功能。&lt;/p&gt;
&lt;p&gt;在初始阶段布线 PCB 时，有助于显示哪些组件布线到哪里的指南在检测电阻布线的情况下可能会产生误导。在低侧分流电阻的情况下，负输入可以直接接地，而正输入可以直接连接到低侧源极引脚。为避免这种情况，请在设备和分流电阻之间放置一个 Net Tie，以便设计人员可以在放置期间而不是布线期间放置布线限制。图 7-6 显示了 Net Tie 放置的示例。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-6.webp&quot; alt=&quot;图7-6 Net Tie 放置样例&quot; /&gt;&lt;/p&gt;
&lt;p&gt;使用差分对的布局技术通过并行布线两个紧密耦合的信号来降低共模噪声。TI 建议使用差分对将信号从分流电阻布线到 CSA。为了进一步降低噪声耦合，请勿将噪声敏感的走线与噪声（开关）信号并行布线。&lt;/p&gt;
&lt;h2&gt;输入和输出过滤（Input and Output Filters）&lt;/h2&gt;
&lt;p&gt;感测放大器的输入和输出滤波器尽可能靠近感测放大器。这种放置方式可确保设备接收到的不需要的噪声被限制在滤波器和感测放大器之间。图 7-7 显示了输入滤波器（C40、R30 和 R31）的示例布局。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-7.webp&quot; alt=&quot;图7-7 输入和输出过滤原理图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/siva/7-8.webp&quot; alt=&quot;图7-8 输入和输出过滤Layout&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;CSA 小结&lt;/h2&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;使用 Net Ties 和差分布线工具&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>简单H桥电机驱动电路分析及其隔离设计</title><link>https://arkmostima.pages.dev/posts/%E7%AE%80%E5%8D%95h%E6%A1%A5%E7%94%B5%E6%9C%BA%E9%A9%B1%E5%8A%A8%E7%94%B5%E8%B7%AF%E5%88%86%E6%9E%90%E5%8F%8A%E5%85%B6%E9%9A%94%E7%A6%BB%E8%AE%BE%E8%AE%A1/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%AE%80%E5%8D%95h%E6%A1%A5%E7%94%B5%E6%9C%BA%E9%A9%B1%E5%8A%A8%E7%94%B5%E8%B7%AF%E5%88%86%E6%9E%90%E5%8F%8A%E5%85%B6%E9%9A%94%E7%A6%BB%E8%AE%BE%E8%AE%A1/</guid><description>H桥是一种相当简单的电路。它包含四个独立控制的MOSFET，用作引导电流流过负载（通常是电感负载，例如电机）的开关元件。</description><pubDate>Mon, 12 Aug 2024 15:42:32 GMT</pubDate><content:encoded>&lt;h1&gt;H桥驱动电路（H-Bridge Circuit）&lt;/h1&gt;
&lt;p&gt;H桥是一种相当简单的电路。它包含四个独立控制的MOSFET，用作引导电流流过负载（通常是电感负载，例如电机）的开关元件。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/1.webp&quot; alt=&quot;H桥电路拓扑图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;具体的工作方式不再赘述，本文主要分析一些比较细节的点。&lt;/p&gt;
&lt;h2&gt;MOSFET中的寄生二极管与外部续流二极管&lt;/h2&gt;
&lt;p&gt;在N沟道MOSFET中，源极和漏极通常是N型区域，而基区是P型材料。这些区域之间形成了两个PN结，其中一个是在源极与基区之间，另一个是在漏极与基区之间。由于在MOSFET中，源极和基区通常是短接的，因此源极-基区的PN结不会起作用，而漏极-基区的PN结则形成了寄生二极管。该现象在大多数功率MOSFET中都会出现。这也就是为什么在一些仿真软件中，MOSFET的拓扑符号后会有一个反向并联的二极管标识。&lt;/p&gt;
&lt;p&gt;作为典型的感性负载，电机在电流变化时会产生反电动势。当H桥中的MOSFET切换状态时，电流的变化会导致电感负载产生一个高电压尖峰。这种电压尖峰可能会反向施加在MOSFET上，可能导致MOSFET损坏或造成不稳定的电路操作。&lt;/p&gt;
&lt;p&gt;为了保护MOSFET和其他电路元件，在每个MOSFET的漏极和源极之间通常会并联一个续流二极管。续流二极管的作用主要有以下几点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;提供电流续流路径&lt;/strong&gt;：当MOSFET关闭时，感性负载的电流无法立即停止，续流二极管为该电流提供了一个通路。这样可以避免因电流中断导致的高电压尖峰，从而保护MOSFET和其他电路元件。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;保护MOSFET&lt;/strong&gt;：续流二极管可以限制感性负载产生的反电动势，避免这些电压尖峰直接加在MOSFET上，防止MOSFET因过压而损坏。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;稳定电路操作&lt;/strong&gt;：在高频开关操作中，续流二极管可以减少电路中的电磁干扰（EMI）和噪声，提高电路的稳定性和可靠性。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MOSFET自带的寄生二极管在电路中确实可以在某些情况下起到续流二极管的作用，特别是在开关断开的瞬间，它可以为电感性负载提供电流续流的路径。然而，由于寄生二极管的特性不如专门设计的续流二极管（例如肖特基二极管），它的导通电压较高，反向恢复时间较长，所以在实际电路设计中，寄生二极管往往不能完全替代续流二极管。因此，如果对续流二极管有严格的要求，通常会在电路中另外并联一个性能更好的二极管。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/2.webp&quot; alt=&quot;一个H桥上臂MOSFET两端并联的续流二极管&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;电流衰减模式&lt;/h2&gt;
&lt;p&gt;电机是感性负载，电流不能突变。若要使电机减速，在断开电机两端所加的电压时，电压由工作值瞬间衰减至零，根据电磁感应定律，电机会根据电压变化率（$du/dt$）的大小而产生反向电动势。当电压变化率很大时，这个反电动势很有可能损坏MOSFET。因此想让电机停下，除了断开供电，还要形成一个续流的回路，释放掉电机上的能量。这就是刚刚提到过的续流二极管的作用。&lt;/p&gt;
&lt;p&gt;在减速时的电流再生过程中，H桥通常可以工作在两个模式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;快速衰减（Fast Decay Mode）&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;慢速衰减（Slow Decay Mode）&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：“衰减”指的是流过电机线圈（电感器）的电流，与电机的行为没有直接关系。不要认为快速衰减会使电机迅速停止。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/3.webp&quot; alt=&quot;不同衰减模式下的电流回路示意图&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;快速衰减模式（Fast Decay Mode）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/4.webp&quot; alt=&quot;快速衰减模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该模式下，四个MOSFET全部关断，反电动势带来的反向感应电流通过Q2、Q3的寄生二极管（续流二极管）流动，此时反电动势带来的感应电流方向与电源电压方向相反，线圈电流迅速衰减。&lt;/p&gt;
&lt;p&gt;为避免同一侧的上下臂MOSFET同时导通而造成击穿，应当采取机制。具体为，添加一个令所有FET开关均处于关闭状态的时间段（被称为&lt;strong&gt;死区时间&lt;/strong&gt;）。在此期间，允许激励FET开关切换到关闭状态，存在电感负载的电流由寄生二极管或外部续流肖特基二极管承载。&lt;/p&gt;
&lt;p&gt;快速衰减模式下电流虽然能够迅速降低到零，但电机速度会因惯性而逐渐减小，对外呈现“滑动”式的减速。&lt;/p&gt;
&lt;h3&gt;慢衰减模式（Slow Decay Mode）&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/5.webp&quot; alt=&quot;慢衰减模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该模式下，Q1、Q3关断，Q2、Q4导通，感应电流以循环方式在流经Q2和Q4时降至零。在这种情况下，电流在流过电感阻抗和两个FET的导通阻抗时以热量的形式耗散。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：虽然慢速衰减通常描述为两个低侧FET开关打开、两个高侧FET开关关闭，但相同现象可通过启用两个高侧 FET 开关、同时禁用两个低侧FET开关来实现。也就是说，衰减回路可以为上臂，也可以为下臂。部分IC支持通过相应的输入信号来配置。&lt;/p&gt;
&lt;p&gt;在直流电机上，慢速衰减模式会令电机绕组短路，进而形成反电动势短路，使得转子以极快速度停止旋转。&lt;/p&gt;
&lt;h3&gt;混合衰减（Mixed Decay Mode）&lt;/h3&gt;
&lt;p&gt;在混合衰减再循环模式中，电流衰减至零的速度快于慢速衰减方法，但慢于快速衰减方法。实现此技术的方法是，协调FET开关的开/关时间，在固定时间内处于快速衰减模式，然后在剩余时间内处于慢速衰减模式。系统保持在快速衰减模式与慢速衰减模式中的时间比例被称为混合衰减百分比。&lt;/p&gt;
&lt;p&gt;混合衰减对于步进电机驱动很有意义，但最重要的是，它对微步进驱动极有意义。在微步进时，将保持一定波形以获得出色的运动质量。例如，通常会通过正弦波（电流曲线）来驱动步进电机绕组。也可以在相同范围内使用三角形和菱形。&lt;/p&gt;
&lt;p&gt;有关混合衰减的内容将另作介绍。&lt;/p&gt;
&lt;h2&gt;MOSFET高低端驱动与自举电路&lt;/h2&gt;
&lt;p&gt;在H桥电路中，MOSFET通常分为高端（High-Side）和低端（Low-Side）两个部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;高端MOSFET（High-Side MOSFET）&lt;/strong&gt;： 连接在电源和负载之间的MOSFET。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;低端MOSFET（Low-Side MOSFET）&lt;/strong&gt;： 连接在负载和地之间的MOSFET。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;已知MOSFET导通需要满足&lt;/p&gt;
&lt;p&gt;$$
V_{GS}&amp;gt;V_{GS(th)}
$$&lt;/p&gt;
&lt;p&gt;对于低端MOSFET，源极通常接地，因此提高驱动电压使得$V_{GS}&amp;gt;V_{GS(th)}$比较容易实现。而对于高端MOSFET，源极与负载相连，且源极电位通常高于地电位，因此对栅极直接驱动需要一个比电源电压更高的电压来导通MOSFET。这就需要特殊的高端驱动电路来生成这个较高的栅极驱动电压。&lt;/p&gt;
&lt;p&gt;自举电路是常用的一种解决方案，用于生成高端MOSFET所需的高电压驱动信号。它通常包括一个二极管和一个电容器（自举电容），配合驱动芯片工作。&lt;/p&gt;
&lt;p&gt;以半桥为例，当Q2导通时，Q1的源极被拉到地电位（低电平）。此时，自举电容C1通过二极管充电（此处所加电压V一般由LDO架构电源输出）。此时，自举电容的电压接近于电源电压V。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/6.webp&quot; alt=&quot;自举电容充电&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当Q2断开时，SW位置电位不再为地电位，而自举电容C1两端储存了大小为V的电压，A点电压变为$V_{SW}+V$，很容易就能够驱动高端MOSFET。此时由C1提供上管驱动阶段所需的所有电流。此时自举二极管（一般为肖特基二极管）处于反向截止状态，将自举压降限制在自举回路内，防止其回流至上一级从而损坏电路。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/7.webp&quot; alt=&quot;自举电容放电&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参见：&lt;a href=&quot;https://blog.csdn.net/qq_44897194/article/details/107397079?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-107397079-blog-110673237.235%5Ev43%5Epc_blog_bottom_relevance_base6&amp;amp;spm=1001.2101.3001.4242.1&amp;amp;utm_relevant_index=3&quot;&gt;H桥自举电路分析&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;自举电容持续放电将会导致高端MOSFET的栅极对地电压持续减小，直至其等于V。因此要让高端MOSFET持续导通，需要令自举电容不断充放电，或是增加自举电容容量、更换IC或在设计中避免持续导通需求。由于MOSFET的特性，自举电路在增加栅源电压的同时，还可令MOSFET的导通电阻减小，从而减少发热损耗。&lt;/p&gt;
&lt;h2&gt;H桥驱动控制&lt;/h2&gt;
&lt;p&gt;对MOSFET栅极引入PWM控制，可实现对电机转动方向和速度的调节。PWM的占空比对应电机转速，占空比越大，平均等效输出电压越大，转速越快。通常的PWM频率在10Khz至20KHz之间。若频率太低，电机转速会过低，同时容易出现高噪声或高频机械振动；频率太高，则MOSFET开关损耗带来的影响会变大，降低系统效率。&lt;/p&gt;
&lt;p&gt;根据不同桥臂的PWM控制方式，大致上可分为三种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;受限单极模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;单极模式&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;双极模式&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;受限单极模式&lt;/h3&gt;
&lt;p&gt;该模式下，电机电枢驱动电压极性是单一的，优点是控制电路较为简单，缺点是电机不能刹车，不能进行能耗制动，在负载超过设定速度时不能提供向力矩。调速静差大，调速性能很差，稳定性也不好。当PWM高电平时，电机两端分别为VM和GND，有电流流过，电机产生扭矩，进行转动。当PWM低电平时，电机两端未导通，不形成闭合回路，没有电流流过，此时电机的转动没有力来维持，难以控制。&lt;/p&gt;
&lt;h3&gt;单极模式&lt;/h3&gt;
&lt;p&gt;单极模式引入互补PWM波（PWMN），其一般由MCU高级定时器生成，如STM32F429的TIM1、TIM8。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/8.webp&quot; alt=&quot;PWM单极模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;相比受限单极模式，单极模式控制在Q1信号为低电平时，由互补PWM提供，对Q2施加高电平信号，令Q2打开。此时Q2、电机、Q4形成闭合回路。电机中仍然有电流流过，虽然电流会减小，但是仍会产生磁场，仍会产生力矩来继续控制电机转动。该模式优点是启动快，能加速、刹车、能耗制动、能量反馈，调速性能虽然不如双极模式好，但是相差不多，电机特性也较好。在负载超速时，也能提供反向力矩。&lt;/p&gt;
&lt;h3&gt;双极模式&lt;/h3&gt;
&lt;p&gt;双极模式下电枢电压极性是正负交替的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/9.webp&quot; alt=&quot;PWM双极模式&quot; /&gt;&lt;/p&gt;
&lt;p&gt;图中，PWM1、PWM2均由定时器输出，PW1N、PW2N为他们的互补PWM通道。PWM1和PWM2周期相同，占空比相同，极性相反，使得对角线上的两个MOSFET能够同时导通，同时关断。最终电机的转动方向由PWM和PWM1共同决定，若A的PWM占空比高于50%则电流从左至右，若低于50%则从右至左。该模式在继承单极模式优点的基础下，在速度快要接近于0时也可以提供一定的力矩，加减速性能更好，因此正反转的性能优于单极模式。控制效果好。缺点是控制更复杂，工作时4个MOS都在工作，功耗大。&lt;/p&gt;
&lt;h3&gt;死区时间&lt;/h3&gt;
&lt;p&gt;死区时间（Dead Time）是指在切换两个互补的MOSFET时，为了避免同时导通而导致短路或直通故障，在关断一个MOSFET和导通另一个MOSFET之间插入的延迟时间。死区时间提供了一个缓冲，确保一个MOSFET完全关闭后，另一个才开始导通，从而避免直通故障。&lt;/p&gt;
&lt;p&gt;MOSFET的导通和关断不是瞬时的，而是需要一定的时间。这包括MOSFET栅极充电和放电的时间。不同类型的MOSFET具有不同的开关速度（开通时间和关断时间）。如果死区时间设置得太短，可能不足以覆盖MOSFET的开关时间，导致直通故障。如果死区时间设置得太长，则会降低电路的效率。因为在死区时间内，两对互补的MOSFET都处于关闭状态，没有电流通过负载，导致功率损失和电机或负载的性能下降。&lt;/p&gt;
&lt;p&gt;MOSFET的开关速度与栅极电荷（Qgs、Qgd）和栅极驱动电流有关。死区时间应与这些参数相匹配。较低的栅极电荷通常意味着更快的开关速度，可以使用更短的死区时间。另一方面，高侧和低侧MOSFET的不同Rds(on)值也会影响开关速度，因此在设计死区时间时需要考虑。一些驱动IC会根据MOSFET的特性自动调整死区时间，以优化性能和保护MOSFET。&lt;/p&gt;
&lt;h3&gt;程序代码（以STM32 HAL库为例）&lt;/h3&gt;
&lt;p&gt;以IRS21867S方案为例：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/H/10.webp&quot; alt=&quot;IRS21867S硬件方案&quot; /&gt;&lt;/p&gt;
&lt;p&gt;MCU为STM32F1，PWM由TIM1通道3生成，PWM-N由TIM1通道3的互补通道生成；PWM1由TIM1通道2生成，PWM1-N由TIM1通道2的互补通道生成。PWM信号从对应的复用GPIO管脚连接至驱动IC。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;模式&lt;/th&gt;
&lt;th&gt;Q1（左上）栅极信号&lt;/th&gt;
&lt;th&gt;Q2（右上）栅极信号&lt;/th&gt;
&lt;th&gt;Q3（左下）栅极信号&lt;/th&gt;
&lt;th&gt;Q4（右下）栅极信号&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;受限单极模式 (正转)&lt;/td&gt;
&lt;td&gt;PWM&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;ON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;受限单极模式 (反转)&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;PWM&lt;/td&gt;
&lt;td&gt;ON&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单极模式（正转）&lt;/td&gt;
&lt;td&gt;PWM&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;PWM-N&lt;/td&gt;
&lt;td&gt;ON&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单极模式（反转）&lt;/td&gt;
&lt;td&gt;OFF&lt;/td&gt;
&lt;td&gt;PWM&lt;/td&gt;
&lt;td&gt;ON&lt;/td&gt;
&lt;td&gt;PWM-N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;双极模式&lt;/td&gt;
&lt;td&gt;PWM1&lt;/td&gt;
&lt;td&gt;PWM2&lt;/td&gt;
&lt;td&gt;PWM1-N&lt;/td&gt;
&lt;td&gt;PWM2-N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;根据该真值表，即可通过修改对应的TIM通道的PWM极性来配置电机工作模式。&lt;/p&gt;
&lt;p&gt;为方便，可自定义工作模式函数。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：下面的Set_Motor_Mode()函数仅设置了定时器状态。在受限单极模式和单极模式下，修改Set_Motor_Mode()函数后，一定要记得根据上表修改对应桥臂MOSFET的栅极信号以保证其处于恒导通状态，对电枢电流提供回路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 电机工作模式设置（定时器状态）
void Set_Motor_Mode(uint8_t mode)
{
    switch (mode)
    {
    case 0: // 受限单极模式（正转）
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        break;

    case 1: // 受限单极模式（反转）
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        break;

    case 2: // 单极模式（正转）
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Start(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        break;

    case 3: // 单极模式（反转）
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Start(&amp;amp;htim1, TIM_CHANNEL_3);
        break;

    case 4: // 双极模式
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Start(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Start(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Start(&amp;amp;htim1, TIM_CHANNEL_3);
        break;

    case 5: // 关闭输出
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_2);
        HAL_TIM_PWM_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        HAL_TIMEx_PWMN_Stop(&amp;amp;htim1, TIM_CHANNEL_3);
        break;            

    default:
        // 处理非法模式
        break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置TIM1：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void MX_TIM1_Init(void)
{
    TIM_ClockConfigTypeDef sClockSourceConfig = {0};
    TIM_OC_InitTypeDef sConfigOC = {0};
    TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig = {0};

    htim1.Instance = TIM1;
    htim1.Init.Prescaler = (uint32_t)((SystemCoreClock / 2) / 20000) - 1; //预分频寄存器写入
    htim1.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim1.Init.Period = 1000 - 1; // 自动重载寄存器写入
    htim1.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim1.Init.RepetitionCounter = 0;
    htim1.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    HAL_TIM_Base_Init(&amp;amp;htim1);

    sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
    HAL_TIM_ConfigClockSource(&amp;amp;htim1, &amp;amp;sClockSourceConfig);

    HAL_TIM_PWM_Init(&amp;amp;htim1);

    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 500; // 初始占空比50%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCNPolarity = TIM_OCNPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    sConfigOC.OCIdleState = TIM_OCIDLESTATE_RESET;
    sConfigOC.OCNIdleState = TIM_OCNIDLESTATE_RESET;

    HAL_TIM_PWM_ConfigChannel(&amp;amp;htim1, &amp;amp;sConfigOC, TIM_CHANNEL_2);
    HAL_TIM_PWM_ConfigChannel(&amp;amp;htim1, &amp;amp;sConfigOC, TIM_CHANNEL_3);

    //死区设置
    sBreakDeadTimeConfig.OffStateRunMode = TIM_OSSR_DISABLE;
    sBreakDeadTimeConfig.OffStateIDLEMode = TIM_OSSI_DISABLE;
    sBreakDeadTimeConfig.LockLevel = TIM_LOCKLEVEL_OFF;
    sBreakDeadTimeConfig.DeadTime = 100;
    sBreakDeadTimeConfig.BreakState = TIM_BREAK_DISABLE;
    sBreakDeadTimeConfig.BreakPolarity = TIM_BREAKPOLARITY_HIGH;
    sBreakDeadTimeConfig.AutomaticOutput = TIM_AUTOMATICOUTPUT_DISABLE;
    HAL_TIMEx_ConfigBreakDeadTime(&amp;amp;htim1, &amp;amp;sBreakDeadTimeConfig);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;定时器触发更新事件时间间隔为
$$
T_{out}=\frac{(ARR+1)\times(PSC+1)}{f_{clk}}
$$&lt;/p&gt;
&lt;p&gt;可按照该式自定义PWM频率。如要调整初始占空比，可通过改变Pulse参数值（范围为0到Period）来实现。&lt;/p&gt;
&lt;p&gt;PWM输出函数（以单极模式为例）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void set_pwm(int speed)
{
    // 处理速度的正负，正值表示正转，负值表示反转
    if (speed &amp;gt; 0)
    {
        Set_Motor_Mode(2);  // 正转

        *******此处要记得将Q4栅极信号拉高*******

    }
    else if (speed &amp;lt; 0)
    {
        Set_Motor_Mode(3);  // 反转

        *******此处要记得将Q3栅极信号拉高*******

        speed = -speed; // 将速度值取绝对值
    }
    else
    {
        Set_Motor_Mode(5);  // 停止
        return;
    }

    // 限制最低速度，确保电机能够启动
    int min_speed = 10; // 定义最低速度
    if (speed &amp;lt; min_speed)
    {
        speed = min_speed;
    }

    // 将速度值映射到PWM占空比范围
    speed = 700 - speed; // 这里700是基准值，越接近700，PWM占空比越小

    TIM1-&amp;gt;CCR2 = speed; // 设置TIM1通道2的PWM占空比
    TIM1-&amp;gt;CCR3 = speed; // 设置TIM1通道3的PWM占空比
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void)
{
    HAL_Init();
    SystemClock_Config();

    // 初始化TIM1
    MX_TIM1_Init();

    // 选择电机的工作模式
    Set_Motor_Mode(???); 
    
    // 主循环
    while (1)
    {
        // 在这里可以通过修改占空比来控制电机速度
        set_pwm(???);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上是一个最简单的能让电机在开环下转起来的代码。如果要实现动态控制、动态调速，需要加入控制环节，另作介绍。&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】同步电机突然三相短路分析</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E7%AA%81%E7%84%B6%E4%B8%89%E7%9B%B8%E7%9F%AD%E8%B7%AF%E5%88%86%E6%9E%90/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E7%AA%81%E7%84%B6%E4%B8%89%E7%9B%B8%E7%9F%AD%E8%B7%AF%E5%88%86%E6%9E%90/</guid><description>在电力系统实际发生的故障中，不对称短路占的比例较大，对称短路的故障所占比例虽较小，但其后果比较严重。根据国家标准规定，同步发电机必须能承受空载电压等于105%额定电压下的突然三相对称短路。本节直接利用之前导出的同步电机动态方程式，求解空…</description><pubDate>Thu, 08 Aug 2024 04:02:14 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;在电力系统实际发生的故障中，不对称短路占的比例较大，对称短路的故障所占比例虽较小，但其后果比较严重。根据国家标准规定，同步发电机必须能承受空载电压等于105%额定电压下的突然三相对称短路。本节直接利用之前导出的同步电机动态方程式，求解空载时突然三相对称短路后的短路电流和电磁转矩。&lt;/p&gt;
&lt;p&gt;假定在突然短路过程中转速为同步转速，即$\omega=1$。这个假定在分析最大突然短路电流和最大短路转矩时是可以满足工程要求的。由于转速恒定而且已知，因此，只需求解动态方程式中的电压方程式，无需考虑转矩过渡时的转子运动方程式。此时电压方程式是一组线性常系数的微分方程式，为了求解方便，可以应用叠加原理和拉氏变换的方法。&lt;/p&gt;
&lt;p&gt;根据叠加原理，实际突然短路可以看成是下面两种工况的叠加：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电机突然短路前的稳定运行工况&lt;/li&gt;
&lt;li&gt;在零初始状态下电枢端突然加上与电机短路前大小相等、方向相反的电压时的运行工况&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;实际突然短路问题的求解就转化成了短路前的稳态运行工况和零初始值时电枢端突加反向电压的瞬态问题的求解，这样，将使计算大为简化。下面分别就电枢突然短路后的电枢电流、转子电流及电磁转矩进行讨论。&lt;/p&gt;
&lt;h2&gt;空载突然短路后的电枢电流&lt;/h2&gt;
&lt;p&gt;假设同步电机在三项短路前空载运行，其稳定运行工况之前已经有分析过，其端电压为&lt;/p&gt;
&lt;p&gt;$$
u_d=u_{d0}=0 \
u_q=u_{q0}=E_m
$$&lt;/p&gt;
&lt;p&gt;根据叠加原理，突然三相短路后的电枢电流$i_d$、$i_q$，将由原来的稳态电流$i_{d0}$、$i_{q0}$和突然加电压$-u_{d0}$和$-u_{q0}$引起的瞬态电流$i_d&apos;$、$i_q&apos;$相加而成，即&lt;/p&gt;
&lt;p&gt;$$
i_d=i_{d0}+i_d&apos; \
i_q=i_{q0}+i_q&apos;
$$&lt;/p&gt;
&lt;p&gt;下面将分别求解电流$i_{d0}$、$i_{q0}$、$i_d&apos;$、$i_q&apos;$和$i_d$、$i_q$，并将其返回至abc坐标系统。&lt;/p&gt;
&lt;h3&gt;短路前的稳态电流$i_{d0}$、$i_{q0}$&lt;/h3&gt;
&lt;p&gt;同步电机运行在空载状态，电枢绕组开路，三相电枢绕组电流均为零，因而&lt;/p&gt;
&lt;p&gt;$$
i_{d0}=i_{q0}=0
$$&lt;/p&gt;
&lt;h3&gt;突然加上$-u_{d0}$、$-u_{q0}$时所产生的电流$i_d$、$i_q$&lt;/h3&gt;
&lt;p&gt;电枢d、q绕组端分别突然加上$-u_{d0}$、$-u_{q0}$时，电压方程式为&lt;/p&gt;
&lt;p&gt;$$
-u_{d0}=0=p\psi_d&apos;-\psi_q&apos;+r_si_d&apos; \
-u_{q0}=-E_m=p\psi_q&apos;+\psi_d&apos;+r_si_q&apos;
$$&lt;/p&gt;
&lt;p&gt;不计励磁调节作用，在突然短路时$u_f$为恒定值，而该恒定励磁电压的作用已经在稳态运行分析时考虑过。当电枢端突加电压$-u_{d0}$、$-u_{q0}$时，励磁绕组相当于短路回路，即$u_f=0$，因而相应的电枢磁链为&lt;/p&gt;
&lt;p&gt;$$
\psi_d&apos;=x_d(p)i_d&apos; \
\psi_q&apos;=x_q(p)i_q&apos;
$$&lt;/p&gt;
&lt;p&gt;将电枢磁链式代入电压方程式，得到电压与阻抗、电流的关系式。当电枢端突加电压时，各绕组的电流、磁链初值为零，故经由$Laplace$变换后，其运算方程为&lt;/p&gt;
&lt;p&gt;$$
0=[r_s+px_d(p)]I_d(p)-x_q(p)I_q(p) \
-\frac{E_m}{p}=[r_s+px_q(p)]I_q(p)+x_d(p)I_d(p)
$$&lt;/p&gt;
&lt;p&gt;解出电流$I_d(p)$如下&lt;/p&gt;
&lt;p&gt;$$
I_d(p)=-\frac{E_m}{p}\times\frac{x_q(p)}{r_s^2+r_s[x_d(p)+x_q(p)]p+x_d(p)x_q(p)p^2+x_d(p)x_q(p)}
$$&lt;/p&gt;
&lt;p&gt;或&lt;/p&gt;
&lt;p&gt;$$
I_d(p)=-\frac{E_m}{p}\times\frac{1}{p^2+r_s[\frac{1}{x_d(p)}+\frac{1}{x_q(p)}]p+1+\frac{r_s^2}{x_d(p)x_q(p)}}\times\frac{1}{x_d(p)}
$$&lt;/p&gt;
&lt;p&gt;略去$r_s^2$项，中括号内有$x_d(p)$和$x_q(p)$的项中，将转子电阻$r_f$、$r_D$、$r_Q$略去，这样$x_d(p)$、$x_q(p)$可分别简化为直轴、交轴次暂态（超瞬变）电抗$x_d&apos;&apos;$、$x_q&apos;&apos;$，于是有&lt;/p&gt;
&lt;p&gt;$$
I_d(p)=-\frac{E_m}{p(p^2+2\alpha p+1)x_d(p)}
$$&lt;/p&gt;
&lt;p&gt;式中&lt;/p&gt;
&lt;p&gt;$$
\alpha = \frac{r_s}{2}(\frac{1}{x_d&apos;&apos;}+\frac{1}{x_q&apos;&apos;})=\frac{1}{T_a}
$$&lt;/p&gt;
&lt;p&gt;根据运算电抗章节中导出的$x_d(p)$的倒数形式&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{x_d(p)}=\frac{1}{x_d}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})\frac{T_d&apos;p}{T_d&apos;p+1}+(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_d&apos;})\frac{T_d&apos;&apos;p}{T_d&apos;&apos;p+1}
$$&lt;/p&gt;
&lt;p&gt;代入原式，有&lt;/p&gt;
&lt;p&gt;$$
I_d(p)=-\frac{E_m}{p(p^2+2\alpha p +1)}[\frac{1}{x_d}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})\frac{T_d&apos;p}{T_d&apos;p+1}+(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_d&apos;})\frac{T_d&apos;&apos;p}{T_d&apos;&apos;p+1}]
$$&lt;/p&gt;
&lt;p&gt;同理可解出象函数$I_q(p)$为&lt;/p&gt;
&lt;p&gt;$$\begin{aligned}I_{q}(p)&amp;amp;=- \frac{E_{m}}{p}\times\frac{r_{s}+px_{d}(p)}{r_{s}^{2}+r_{s}[x_{d}(p)+x_{q}(p)]p+x_{d}(p)x_{q}(p)p^{2}+x_{d}(p)x_{q}(p)}\&amp;amp;=- \frac{E_{m}}{p}\times\frac{r_{s}+px_{d}(p)}{p^{2}+r_{s}\Big[\frac{1}{x_{d}(p)}+\frac{1}{x_{q}(p)}\Big]p+1+\frac{r_{s}^{2}}{x_{d}(p)x_{q}(p)}}\times\frac{1}{x_{d}(p)x_{q}(p)}\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;分母中采用与导出$I_d(p)$时同样的简化方法，并根据运算电抗章节中导出的$x_q(p)$的倒数形式&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{x_q(p)}=\frac{1}{x_q}+(\frac{1}{x_q&apos;&apos;}-\frac{1}{x_q})\frac{T_q&apos;&apos;p}{1+T_q&apos;&apos;p}
$$&lt;/p&gt;
&lt;p&gt;代入，得&lt;/p&gt;
&lt;p&gt;$$
I_q(p)=-\frac{E_m}{(p^2+2\alpha p +1)}[\frac{1}{x_q}+(\frac{1}{x_q&apos;&apos;}-\frac{1}{x_q})\frac{T_q&apos;&apos;p}{1+T_q&apos;&apos;p}]
$$&lt;/p&gt;
&lt;p&gt;将象函数$I_d(p)$和$I_q(p)$使用$Laplace$反变换至原函数$i_d&apos;$和$i_q&apos;$。由于同步电机中$\frac{1}{T_d}$、$\frac{1}{T_d&apos;}$、$\frac{1}{T_d&apos;&apos;}$、$\frac{1}{T_q&apos;&apos;}$均远小于1，反变换后的结果为&lt;/p&gt;
&lt;p&gt;$$
i_d&apos;=-E_m[(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_d&apos;})e^{-t/T_d&apos;&apos;}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})e^{-t/T_d&apos;}+\frac{1}{x_d}]+\frac{E_m}{x_d&apos;&apos;}e^{-t\cos t/T_a}\
$$&lt;/p&gt;
&lt;p&gt;$$
i_q&apos;=-\frac{E_m}{x_q&apos;&apos;}e^{-t \sin t/T_a}
$$&lt;/p&gt;
&lt;h3&gt;空载突然短路后的实际电流$i_d$、$i_q$&lt;/h3&gt;
&lt;p&gt;根据上面分析，实际的空载突然三相短路电流可应用叠加原理，将空载稳态运行时的电流与突加$-u_{d0}$、$-u_{q0}$时的电流相加，由于空载时电枢电流为零，所以突然短路后的电流用dq0坐标系统表示为&lt;/p&gt;
&lt;p&gt;$$
i_d=i_{d0}+i_d&apos;=i_d&apos; \
i_q=i_{q0}+i_q&apos;=i_q&apos;
$$&lt;/p&gt;
&lt;h3&gt;空载突然短路后的电流$i_a$、$i_b$、$i_c$&lt;/h3&gt;
&lt;p&gt;假定$t=0$时，d轴和q轴得夹角为$\theta_0$，则$\theta=t+\theta_0$。将$i_d&apos;$和$i_q&apos;$二式返回至abc坐标系统，有&lt;/p&gt;
&lt;p&gt;$$
i_a=-[(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_d&apos;})e^{-t/T_d&apos;&apos;}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})e^{-t/T_d&apos;}+\frac{1}{x_d}]E_m\cos(t+\theta_0)+\frac{E_m}{2}e^{-1/T_a}[(\frac{1}{x_d&apos;&apos;}+\frac{1}{x_q&apos;&apos;})\cos \theta_0+(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_q&apos;&apos;})\cos (2t+\theta_0)]
$$&lt;/p&gt;
&lt;p&gt;把上式中的$\theta_0$分别用$(\theta_0-120\degree)$、$(\theta_0+120\degree)$替换，即可得到$i_b$、$i_c$的表达式。&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;以上是从解方程式得到的突然三相短路后的实际定子绕组电流。由$i_a$的表达式可知，突然三相短路后，定子各相短路电流由两大部分构成，即基频的周期分量、非周期及2倍频分量，从物理意义上来理解，当定子绕组突然三相短路后，由于转子磁场的旋转及定子绕组中产生相应的三相稳态基频电流，将引起定子及转子各组中的磁链的突变。为了保持各闭合回路中的磁链不发生突变,定子绕组和转子阻尼和励磁绕组中就产生了非周期电流。由于转子以同步速度旋转，转子阻尼和励磁绕组中的非周期电流会在定子绕组中产生相应的基频周期电流分量，即超瞬变（次暂态）分量和瞬变（暂态）分量。由于转子绕组中存在电阻，突然短路在转子绕组中引起的非周期电流将逐步衰减至零，其阻尼绕组和励磁绕组的衰减时间常数分别为$T_d&apos;&apos;$和$T_d&apos;$。由转子绕组中的非周期电流在定子绕组中感生的超瞬变分量和瞬变分量电流亦按同样的时间常数衰减。同样地，定子绕组中的非周期电流会在转子绕组中产生相应的基频周期分量电流，但由于转子不对称，这个转子绕组中的基频电流所产生的磁场相对转子而言可分为两个转向相反，转速均为同步转速$\omega_1$的分量，其中与转子转向相同的旋转磁场对定子的转速为$2\omega_1$，它将在定子绕组中引起倍频的周期分量。从$i_a$的表达式中也可看出，当转子的磁路和电路完全对称，即$x_d&apos;&apos;=x_q&apos;$时，这个倍频的周期分量将不存在。与转子转向相反的旋转磁场对定子的转速为零，对定子非周期电流产生的磁场是一个去磁作用。&lt;/p&gt;
&lt;p&gt;由于定子绕组存在电阻，定子绕组中的非周期电流和倍频电流将以定子绕组的时间常数$T_a$衰减至零。&lt;/p&gt;
&lt;p&gt;从上面分析也可以看出，由于绕组轴线选取的不同，在d、q、0坐标系统中，电流$i_d$、$i_q$的非周期分量相当于a、b、c坐标系统中电流$i_a$、$i_b$、$i_c$的基频周期分量，而电流$i_d$及$i_q$的基频周期分量，则相当于电流$i_a$、$i_b$、$i_c$的非周期分量和倍频的周期分量。&lt;/p&gt;
&lt;h2&gt;空载突然短路后的转子绕组电流&lt;/h2&gt;
&lt;p&gt;突然三相短路后，电机转子绕组中电流的计算方法与计算电枢绕组电流一致，采用叠加原理将实际的突然短路电流看成是以下两种工况的叠加：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电机突然短路前稳定运行工况的电流&lt;/li&gt;
&lt;li&gt;在零初始状态下，电枢端突然加上与短路前大小相等、方向相反的电压时运行工况的电流&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;求原来稳态三相对称运行时的转子电流$i_{f0}$、$i_{D0}$、$i_{Q0}$&lt;/li&gt;
&lt;li&gt;求突然在电枢端加上电压$-u_{d0}$、$-u_{q0}$引起的转子电流$i_f&apos;$、$i_D&apos;$、$i_Q&apos;$&lt;/li&gt;
&lt;li&gt;突然三相短路后转子的实际电流为（1）、（2）所求电流的叠加，即
$$
i_f=i_{f0}+i_f&apos;\
i_D=i_{D0}+i_D&apos;\
i_Q=i_{Q0}+i_Q&apos;\
$$&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面分三步求解转子电流：&lt;/p&gt;
&lt;h3&gt;求稳态三相对称运行时转子电流&lt;/h3&gt;
&lt;p&gt;根据第二节空载运行的分析可知，稳态运行时励磁绕组电流，等于外加励磁电压在励磁绕组中产生的稳态直流电流，即&lt;/p&gt;
&lt;p&gt;$$
i_{f0}=\frac{u_f}{r_f}
$$&lt;/p&gt;
&lt;p&gt;且稳态三相对称运行时阻尼绕组内电流为零，即&lt;/p&gt;
&lt;p&gt;$$
i_{D0}=i_{Q0}=0
$$&lt;/p&gt;
&lt;h3&gt;求突然加上电压$-u_{d0}$、$-u_{q0}$引起的转子电流&lt;/h3&gt;
</content:encoded></item><item><title>【电机瞬态分析】凸级同步电机对称稳态运行分析</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%87%B8%E7%BA%A7%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E5%AF%B9%E7%A7%B0%E7%A8%B3%E6%80%81%E8%BF%90%E8%A1%8C%E5%88%86%E6%9E%90/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%87%B8%E7%BA%A7%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E5%AF%B9%E7%A7%B0%E7%A8%B3%E6%80%81%E8%BF%90%E8%A1%8C%E5%88%86%E6%9E%90/</guid><description>电机学中已经详细讨论了同步电机对称稳态运行问题，这里用之前导出的d、q、0坐标系统的电压方程式，对这一运行方式进行分析，很容易求得稳态运行时的有功功率、无功功率及电磁转矩。</description><pubDate>Mon, 05 Aug 2024 05:43:45 GMT</pubDate><content:encoded>&lt;p&gt;电机学中已经详细讨论了同步电机对称稳态运行问题，这里用之前导出的d、q、0坐标系统的电压方程式，对这一运行方式进行分析，很容易求得稳态运行时的有功功率、无功功率及电磁转矩。&lt;/p&gt;
&lt;h2&gt;空载运行&lt;/h2&gt;
&lt;p&gt;空载稳态运行时，显然同步电机的转速只能为同步转速，即角速度（标幺值）$\omega=1$；励磁绕组端外加直流电压为$u_f$，电枢绕组开路，三相电枢绕组电流$i_a$、$i_b$、$i_c$均为零，转换为d、q、0坐标系统后，$i_d$、$i_q$、$i_0$亦都为零。由于转速恒定，不存在转矩过渡过程，故该情况下无需求解转矩方程式，只需求解电压方程式，即可求得发电机空载运行时由励磁电流、磁链在发电机电枢绕组中产生的空载电压。&lt;/p&gt;
&lt;p&gt;首先，求出励磁电流$i_f$。根据标幺值形式下的同步电机电压方程式&lt;/p&gt;
&lt;p&gt;$$
U^*=L^&lt;em&gt;pI^&lt;/em&gt;+\omega^*G^&lt;em&gt;I^&lt;/em&gt;+R^&lt;em&gt;I^&lt;/em&gt;
$$&lt;/p&gt;
&lt;p&gt;式中
$$
U^&lt;em&gt;=[U_{dq0}^&lt;/em&gt; \quad U_{fDQ}^&lt;em&gt;]^T\
$$
$$
I^&lt;/em&gt;=[I_{dq0}^* \quad I_{fDQ}^&lt;em&gt;]^T
$$
$$
R^&lt;/em&gt;=
\begin{bmatrix}
R_{abc}^* &amp;amp; 0\
0 &amp;amp; R_{fDQ}^*
\end{bmatrix}
$$
$$
\begin{array}{c}
L^{&lt;em&gt;}=\left[\begin{array}{cccccc}
x_{\mathrm{d}}^&lt;/em&gt; &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{ad}}^{&lt;em&gt;} &amp;amp; x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 \
0 &amp;amp; x_{\mathrm{q}}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{a q}^{&lt;/em&gt;} \
0 &amp;amp; 0 &amp;amp; x_{0}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{f}}^{&lt;em&gt;} &amp;amp; x_{a d}^{&lt;/em&gt;} &amp;amp; 0 \
x_{a d}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{ad}^{&lt;/em&gt;} &amp;amp; x_{\mathrm{D}}^{&lt;em&gt;} &amp;amp; 0 \
0 &amp;amp; x_{\mathrm{aq}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{Q}}^{*}
\end{array}\right]&lt;/p&gt;
&lt;p&gt;\&lt;/p&gt;
&lt;p&gt;G^{&lt;em&gt;}=\left[\begin{array}{cccccc}
0 &amp;amp; -x_{\mathrm{q}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; -x_{a q}^{&lt;em&gt;} \
x_{d}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{ad}}^{&lt;em&gt;} &amp;amp; x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0
\end{array}\right]
\end{array}
$$&lt;/p&gt;
&lt;p&gt;考虑$i_d$、$i_q$为零，可得&lt;/p&gt;
&lt;p&gt;$$
u_f=p(x_fi_f+x_{ad}i_D) + r_fi_f \
0=p(x_{ad}i_f+x_Di_D)+r_Di_D \
0=px_Qi_Q+r_Qi_Q
$$&lt;/p&gt;
&lt;p&gt;励磁绕组外加电源为直流电压，且现讨论的是稳态运行问题，所以励磁绕组电流为恒定直流，转子各绕组交链的磁链也为恒指，变压器电动势$p\psi$为零，这时励磁绕组的电压方程式为&lt;/p&gt;
&lt;p&gt;$$
u_f=r_fi_f
$$&lt;/p&gt;
&lt;p&gt;励磁绕组的电流为&lt;/p&gt;
&lt;p&gt;$$
i_f = \frac{u_f}{r_f}
$$&lt;/p&gt;
&lt;p&gt;阻尼绕组为短路回路，无端口电压，此时与阻尼绕组交链的磁链为恒值，无变压器电动势，显然阻尼绕组内电流为零，即&lt;/p&gt;
&lt;p&gt;$$
i_D=i_Q=0
$$&lt;/p&gt;
&lt;p&gt;其次，解出空载时电枢绕组的端电压$u_d$、$u_q$，根据同步电机电压方程式中电枢绕组部分，有&lt;/p&gt;
&lt;p&gt;$$
u_d=p\psi_d-\omega\psi_q+r_ai_d\
u_q=p\psi_q+\omega\psi_d+r_ai_q
$$&lt;/p&gt;
&lt;p&gt;上式右边第一项$p\psi_d$、$p\psi_q$为零，由于电枢绕组电流$i_d$、$i_q$和阻尼绕组电流都为零，故$\psi_d=x_{ad}i_f=$恒值，$\psi_q=0$，故其变化率$p\psi$也为零，第三项电枢电流引起的电阻压降亦为零。考虑转子角速度$\omega=1$，可将上式改写为&lt;/p&gt;
&lt;p&gt;$$
u_d = 0 \
u_q = \psi_d = x_{ad}i_f = x_{ad}\frac{u_f}{i_f}=E_m
$$&lt;/p&gt;
&lt;p&gt;式中，$E_m$为空载电动势幅值。$u_q =E_m$是一个重要的关系。&lt;/p&gt;
&lt;p&gt;最后，将电枢绕组在dq0坐标系统下得到的空载电压$u_d$、$u_q$返回到abc坐标系统，稳态运行时，可任意选择时间的起始点，一般取d轴和a轴重合的瞬间作为时间起点，即$\theta_0=0$，这样就有$\theta=\omega t + \theta_0 = t$，将$u_d$、$u_q$逆变换后可得三相空载电压为&lt;/p&gt;
&lt;p&gt;$$
u_a = u_d \cos\theta - u_q\sin\theta = -E_m\sin t \
u_b = u_d \cos (\theta - 120 \degree) - u_q \sin (\theta - 120 \degree) = -E_m\sin (t-120 \degree)\
u_c = u_d \cos (\theta + 120 \degree) - u_q \sin (\theta + 120 \degree)=-E_m\sin (t+120 \degree)
$$&lt;/p&gt;
&lt;p&gt;综合上式可以看出，稳态运行时，在abc坐标系统中，电枢绕组的空载电压为按额定频率变换的正弦量，而在dq0坐标系统中则变为恒定的直流量。由于绕组轴线的变换，使绕组电动势的频率也发生了变换，这也可以从电机变换前后的物理模型中清楚的看到。&lt;/p&gt;
&lt;h2&gt;负载运行&lt;/h2&gt;
&lt;p&gt;同步电机带负载运行时，端电压与空载电动势之间会形成一定的相位差。假定端电压滞后于空载电动势的相角为$\delta$，即为&lt;strong&gt;功角&lt;/strong&gt;。则电机端电压的瞬时值可导出为&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix}
u_a \ u_b \ u_c
\end{bmatrix}=
-U_m
\begin{bmatrix}
\sin (t - \delta) \
\sin (t - \delta - 120 \degree) \
\sin (t - \delta + 120 \degree)
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;式中，$U_m$为同步电机端电压幅值。&lt;/p&gt;
&lt;p&gt;将abc坐标系统的电枢绕组电压变换至dq0坐标系统，则$u_d$、$u_q$、$u_0$为&lt;/p&gt;
&lt;p&gt;$$
\begin{bmatrix}
u_d \ u_q \ u_0
\end{bmatrix}=C
\begin{bmatrix}
u_a \ u_b \ u_c
\end{bmatrix}=
\begin{bmatrix}
U_m \sin \delta \
U_m \cos \delta \
0 \
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;稳态运行时，端电压幅值$U_m$及功角$\delta$皆为恒值，所以$u_d$和$u_q$亦为恒值。同样地，对称稳态运行时，三相电流$i_a$、$i_b$、$i_c$以及电枢绕组电流$i_d$、$i_q$都为恒定直流且零序电流$i_0$为零。根据同步电机磁链方程式，可求得d、q绕组的磁链为&lt;/p&gt;
&lt;p&gt;$$
\psi_d = x_di_d + x_{ad}i_f = x_di_d + x_{ad}\frac{u_f}{r_f} = x_di_d + E_m \
\psi_q = x_q i_q
$$&lt;/p&gt;
&lt;p&gt;根据上式，当电流$i_d$、$i_q$为恒值时，磁链亦为恒值。将上式代入电压方程式，并考虑同步转速下$\omega = 1$，有&lt;/p&gt;
&lt;p&gt;$$
u_d = -\psi_q + r_ai_d = -x_qi_q + r_si_d \
u_q = \psi_d + r_ai_q = E_m + x_di_d + r_si_q \
$$&lt;/p&gt;
&lt;p&gt;此处有$r_s=r_a$。三相对称绕组中每一相的电阻都是一致的，为表示方便，下面将用$r_s$代替定子电阻。&lt;/p&gt;
&lt;p&gt;当已知电压$u_d$、$u_q$、$u_f$及阻抗参数时，可解出电枢绕组电流$i_d$、$i_q$如下&lt;/p&gt;
&lt;p&gt;$$
i_d = \frac{r_su_d-x_q(E_m-u_q)}{r_s^2+x_dx_q}=\frac{r_sU_m\sin\delta-x_q(E_m-U_m\cos\delta)}{r_s^2+x_dx_q} \
i_q = \frac{-x_du_d-r_s(E_m-u_q)}{r_s^2+x_dx_q}=\frac{-x_du_m\sin\delta-r_s(E_m-U_m\cos\delta)}{r_s^2+x_dx_q}
$$&lt;/p&gt;
&lt;p&gt;实际中，同步电机定子电阻$r_s$的值远小于电抗$x_d$和$x_q$的值。当略去定子电阻$r_s$时，上式可化简为&lt;/p&gt;
&lt;p&gt;$$
i_d = -\frac{E_m-u_q}{x_d}=-\frac{E_m-U_m\cos\delta}{x_d} \
i_q = - \frac{u_d}{x_q} = - \frac{U_m\sin\delta}{x_q}
$$&lt;/p&gt;
&lt;p&gt;从上面求解过程也可看出，当三相对称稳态运行时，变换到d、q、0坐标系统下的电枢绕组电压、电流均为直流。因此，将原三相稳态交流电路的问题转变成了稳态直流电路的问题，即将求解复代数方程式转变为求解实代数方程式，使计算大为简化，显示了dq0坐标系统在解三相对称稳态运行问题时的优越性。&lt;/p&gt;
&lt;p&gt;同步电机对称稳态运行时的磁链、电流、电压求出后，就可进一步分析同步发电机的有功功率、无功功率与电磁转矩。&lt;/p&gt;
&lt;h3&gt;有功功率&lt;/h3&gt;
&lt;p&gt;同步发电机电枢端的有功功率，只需将之前求出的电压和电流代入三相功率标幺值公式$P=u_di_d+u_qi_q+2u_0i_0$中即可得到，此时考虑定子电阻$r_s$，为&lt;/p&gt;
&lt;p&gt;$$
P=u_di_d+u_qi_q \
= U_m\sin\delta\frac{r_sU_m\sin\delta-x_q(E_m-U_m\cos\delta)}{r_s^2+x_dx_q}+U_m\cos\delta\frac{-x_dU_m\sin\delta-r_s(E_m-U_m\cos\delta)}{r_s^2+x_dx_q}\
= \frac{-1}{r_s^2+x_dx_q}[-r_sU_m^2+E_mU_m(x_q\sin\delta+r_s\cos\delta)+\frac{U_m^2}{2}(x_d-x_q)\sin2\delta]
$$&lt;/p&gt;
&lt;p&gt;若略去$r_s$，则&lt;/p&gt;
&lt;p&gt;$$
P=-[\frac{E_mU_m}{x_d}\sin\delta+\frac{U_m^2}{2}(\frac{1}{x_q}-\frac{1}{x_d})\sin2\delta]
$$&lt;/p&gt;
&lt;p&gt;对于隐级同步电机，因其$x_d=x_q$，则有&lt;/p&gt;
&lt;p&gt;$$
P=-\frac{E_mU_m}{x_d}\sin\delta
$$&lt;/p&gt;
&lt;p&gt;从上面公式可看出，有功功率前面均有负号，这是由于按电动机惯例选取的正方向。因此，负功率意味着电枢端不是输入而是输出电功率，即运行在发电机状态。这也说明正方向的假设是可以人为选定的。按电动机惯例选取正方向，同样可以分析发电机的运行状态。本文中功率基值为三相电机的额定容量，即$P_b=3U_NI_N$，因此，用标么值表示的功率在形式上与电机学中不一致，但返回到有名值后就无差别了。&lt;/p&gt;
&lt;h3&gt;无功功率&lt;/h3&gt;
&lt;p&gt;电机的视在功率$S$与有功功率$P$和无功功率$Q$的关系为&lt;/p&gt;
&lt;p&gt;$$
Q= \sqrt {S^2-P^2}
$$&lt;/p&gt;
&lt;p&gt;式中$S$为标幺值。&lt;/p&gt;
&lt;p&gt;三相视在功率的有名值为$3UI$，则有&lt;/p&gt;
&lt;p&gt;$$
S=\frac{3UI}{P_b}=\frac{\frac{3}{2}U_mI_m}{\frac{3}{2}U_bI_b}=U_mI_m
$$&lt;/p&gt;
&lt;p&gt;式中$U_m$和$I_m$分别为用标幺值表示的电枢绕组端电压和电流的幅值，它们与d、q轴电枢绕组的端电压和电流的关系分别为&lt;/p&gt;
&lt;p&gt;$$
U_m=\sqrt{u_d^2+u_q^2},I_m=\sqrt{i_d^2+i_q^2}
$$&lt;/p&gt;
&lt;p&gt;将视在功率$S$与有功功率$P$代入上式，可得无功功率$Q$的标幺值为&lt;/p&gt;
&lt;p&gt;$$
Q=\sqrt{(u_d^2+u_q^2)(i_d^2+i_q^2)-(u_di_d+u_qi_q)^2}=u_qi_d-u_di_q
$$&lt;/p&gt;
&lt;p&gt;代入电压和电流，有&lt;/p&gt;
&lt;p&gt;$$
Q=\frac{-1}{r_s^2+x_dx_q}[E_mU_m(x_q\cos\delta-r_s\sin\delta)-\frac{U_m^2}{2}(x_d+x_q)+\frac{U_m^2}{2}(x_d-x_q)\sin2\delta]
$$&lt;/p&gt;
&lt;p&gt;略去$r_s$时，有&lt;/p&gt;
&lt;p&gt;$$
Q=-[\frac{E_mU_m}{x_d}\cos\delta-\frac{U_m^2}{2}(\frac{1}{x_q}+\frac{1}{x_d})+\frac{U_m^2}{2}(\frac{1}{x_q}-\frac{1}{x_d})\sin2\delta]
$$&lt;/p&gt;
&lt;p&gt;若为隐级同步电机，因$x_d=x_q$，则&lt;/p&gt;
&lt;p&gt;$$
Q=-(\frac{E_mU_m}{x_d}\cos\delta-\frac{U_m^2}{x_d})
$$&lt;/p&gt;
&lt;h3&gt;电磁转矩&lt;/h3&gt;
&lt;p&gt;将磁链式和电流式代入电磁转矩公式，有&lt;/p&gt;
&lt;p&gt;$$
T_{em}=i_q\psi_d-i_d\psi_q=-\frac{r_s(r_s^2+x_q^2)E_m^2}{(r_s^2+x_dx_q)^2}-\frac{E_mU_m}{(r_s^2+x_dx_q)^2} \
\times [(r_sx_d\sin\delta-x_q^2\cos\delta)2r_s+(x_dx_q-r_s^2)(r_s\cos\delta+x_q\sin\delta)] \&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;\frac{(x_d-x_q)U_m^2}{(r_s^2+x_dx_q)^2}[(x_d\sin^2\delta-x_q\cos^2\delta)r_s+\frac{1}{2}(x_dx_q-r_s^2)\sin2\delta]
$$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;上式中，等号左侧电磁转矩符号为负，说明该转矩方向与假定正方向相反，为制动转矩。&lt;/p&gt;
&lt;p&gt;上式中等号右侧的第一项称为短路转矩，为电机稳态短路$(U_m=0)$时的转矩。短路时电枢电流$i_{dk}$、$i_{qk}$为&lt;/p&gt;
&lt;p&gt;$$
i_{dk}=-\frac{x_qE_m}{r_s^2+x_dx_q},i_{qk}=-\frac{r_sE_m}{r_s^2+x_dx_q}
$$&lt;/p&gt;
&lt;p&gt;因此，电枢短路电流所引起的电枢电阻损耗为&lt;/p&gt;
&lt;p&gt;$$
i_{dk}^2r_s+i_{qk}^2r_s=I_{km}^2r_s=\frac{(r_s^2+x_q^2)E_m^2}{(r_s^2+x_dx_q)}r_s
$$&lt;/p&gt;
&lt;p&gt;因$r_s^2 &amp;lt;&amp;lt; x_q^2$，且$r_s^2 &amp;lt;&amp;lt; x_dx_q$，可略去$r_s$的二次项，改写上式为&lt;/p&gt;
&lt;p&gt;$$
T_k=-\frac{(r_s^2+x_q^2)E_m^2}{(r_s^2+x_dx_q)^2}r_s \approx -(\frac{E_m}{x_d})^2r_s=-I_{km}^2r_s
$$&lt;/p&gt;
&lt;p&gt;因此，短路转矩$T_k$仅与励磁绕组电流产生的空载电动势$E_m$有关，而与端电压$U_m$无关。当略去电枢绕组电阻$r_s$时，该转矩为零。&lt;/p&gt;
&lt;p&gt;转矩公式中等号右侧的第二项为同步转矩$T_s$，它不仅与励磁电流产生的空载电动势$E_m$有关，还和电枢绕组的端电压$U_m$有关，其对应基本电磁功率部分。当略去$r_s$时，$T_s$变为&lt;/p&gt;
&lt;p&gt;$$
T_s = \frac{E_mU_m}{x_d}\sin\delta
$$&lt;/p&gt;
&lt;p&gt;转矩公式中等号右侧的第三项为磁阻转矩$T_r$，只有当直轴和交轴磁阻不等$(x_d \neq x_q)$时，该转矩才存在，且只与$U_m$有关。其对应电磁功率中的磁阻功率部分。当略去$r_s$时，$T_r$变为&lt;/p&gt;
&lt;p&gt;$$
T_r = - \frac{U_m^2}{2}(\frac{1}{x_q}-\frac{1}{x_d})\sin2\delta
$$&lt;/p&gt;
&lt;p&gt;当略去电枢电阻时，稳态对称情况下总的电磁转矩为&lt;/p&gt;
&lt;p&gt;$$
T_{em}=-\frac{E_mU_m}{x_d}\sin\delta-\frac{U_m^2}{2}(\frac{1}{x_q}-\frac{1}{x_d})\sin2\delta
$$&lt;/p&gt;
&lt;p&gt;可以看出，略去电枢电阻后，且直轴和交轴磁阻相等时，用标幺值表示的电磁转矩式（上式）与电枢端有功功率式完全一致。与电磁转矩相对应的电磁功率在不考虑电枢电阻损耗时，就是电枢端的有功功率。&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】交流电机综合动态方程式</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E7%BB%BC%E5%90%88%E5%8A%A8%E6%80%81%E6%96%B9%E7%A8%8B%E5%BC%8F/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E7%BB%BC%E5%90%88%E5%8A%A8%E6%80%81%E6%96%B9%E7%A8%8B%E5%BC%8F/</guid><description>在实际运行中，不论电机受到电磁方面或是机械方面的扰动，都可以使电机的转矩失去平衡，导致发生机电过渡过程。在这种情况下，既有电机的电流、磁链、电磁转矩等电磁量的变化，又有电机的转速、机械转矩等机械量的变化，两者是相互影响的。之前几个小节所…</description><pubDate>Fri, 02 Aug 2024 12:22:36 GMT</pubDate><content:encoded>&lt;h3&gt;简介&lt;/h3&gt;
&lt;p&gt;在实际运行中，不论电机受到电磁方面或是机械方面的扰动，都可以使电机的转矩失去平衡，导致发生机电过渡过程。在这种情况下，既有电机的电流、磁链、电磁转矩等电磁量的变化，又有电机的转速、机械转矩等机械量的变化，两者是相互影响的。之前几个小节所导出的电压方程式和转子运动方程式，正确地反映了电机的电磁、机械等量的内在联系和相互制约的关系。因此，一般情况下应该联立求解电压方程式和转子运动方程式。&lt;/p&gt;
&lt;p&gt;当转速不是恒值而是变量时，电压方程式中的$\omega \psi$为两个变量的乘积，转矩方程式中电磁转矩$T_{em}=i_q\psi_d-i_d\psi_q$，亦是两个变量的乘积，因此，转换为d、q、0坐标系统的同步电机电压方程式和转矩方程式，均为非线性微分方程式。一般来说，无法采用解析法而需采用数值法求解。&lt;/p&gt;
&lt;p&gt;将凸极同步电机的电压方程式和转子运动方程式合并，可得凸极同步电机的动态方程式。以该方程式为基础，根据隐极同步电机和感应电机的实际情况做适当简化，可分别得到他们各自的动态方程式。&lt;/p&gt;
&lt;h3&gt;凸级同步电机动态方程式&lt;/h3&gt;
&lt;p&gt;令机械阻尼转矩为&lt;/p&gt;
&lt;p&gt;$$
T_D=k_p\frac{d\theta}{dt}=k_D \omega$$
式中$k_D$为机械阻尼系数。&lt;/p&gt;
&lt;p&gt;则转子运动方程式为
$$
H\frac{d\omega}{dt}=T_{em}-T_L-T_D=i_q\psi_d-i_d\psi_q-T_L-k_D\omega\
=x_di_qi_d+x_{ad}i_qi_f+x_{ad}i_qi_D-x_qi_di_q-x_{aq}i_di_Q-T_L-k_D\omega
$$
合并电压方程式和上式（转矩方程式），可得凸级同步电机的动态方程为
$$
\begin{bmatrix}
u_d \ u_q \ u_0 \ u_f \ 0 \ 0 \T_L \ 0
\end{bmatrix}=
\begin{bmatrix}
x_d &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{ad} &amp;amp; x_{ad} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; x_q &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{aq} &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; x_0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
x_{ad} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_f &amp;amp; x_{ad} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
x_{ad} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{ad} &amp;amp; x_D &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
0 &amp;amp; x_{aq} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_Q &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; -H &amp;amp; 0\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 1\
\end{bmatrix}p
\begin{bmatrix}
i_d \ i_q \ i_0 \ i_f \ i_D\ i_Q\ \omega\\theta
\end{bmatrix}
+
\begin{bmatrix}
r_s &amp;amp; -\omega x_q &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; -\omega x_{aq} &amp;amp; 0 &amp;amp; 0 \
\omega x_d &amp;amp; r_s &amp;amp; 0 &amp;amp; \omega x_{ad} &amp;amp; \omega x_{ad} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; r_s &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_f &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_D &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_Q &amp;amp; 0 &amp;amp; 0\
x_di_q &amp;amp; -x_qi_d &amp;amp; 0 &amp;amp; x_{ad}i_q &amp;amp; x_{ad}i_q &amp;amp; -x_{aq}i_d &amp;amp; -k_D &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; -1 &amp;amp; 0\
\end{bmatrix}
\begin{bmatrix}
i_d \ i_q \ i_0 \ i_f \ i_D\ i_Q\ \omega\\theta
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;显然这是一组非线性微分方程，直接求解较为困难,求解实际问题时，可根据求解问题的条件，从工程角度出发，做一些适当的简化。&lt;/p&gt;
&lt;p&gt;当转子转速$\omega$为恒值，且己知时，对这类问题的求解就无需求解转子运动方程式，只需单独求解电压方程式，这时方程式是一组线性常系数微分方程式，可用解析法求解。对于变速问题，由于$\omega$是变量，必须联立求解电压方程式和转子运动方程式，这时方程式是一组非线性微分方程式。一般情况下，只能将上式改写为状态方程式，采用数值法求近似解。&lt;/p&gt;
&lt;h3&gt;隐级同步电机动态方程式&lt;/h3&gt;
&lt;p&gt;隐级同步电机为柱形转子，直轴与交轴磁导相同，所以有
$$
x_{ad}=x_{aq},x_d=x_q
$$
按照此关系对凸级同步电机动态方程中的参数作修改后即可得到隐级同步电机的动态方程式。&lt;/p&gt;
&lt;h3&gt;感应电机动态方程式&lt;/h3&gt;
&lt;p&gt;感应电机中定子三相绕组为对称，转子为圆柱形，且无励磁绕组，而转子不论是绕线型还是鼠笼型都为对称绕组，所以感应电机中定子、转子的磁路和电路均为对称，因而直轴和交轴电枢反应电抗相等，即$x_{ad}=x_{aq}=x_m$。电枢绕组中$x_d=x_q=x_{ss}=x_{sl}+x_m$，原转子绕组中直轴和交轴阻尼绕组参数应该为现转子绕组的相应电抗$x_{rr}$和电阻$r_r$，即
$$
x_D=x_Q=x_{rr}=x_{rl}+x_m,r_D=r_Q=r_r
$$
将参数代入原凸级同步电机的动态方程式，并将式中的第四行、第四列去掉，转子直轴阻尼绕组电流$i_D$改为转子直轴绕组电流$i_{dr}$，交轴阻尼绕组电流$i_Q$改为转子交轴绕组电流$i_{qr}$，即可得到感应电机动态方程式。&lt;/p&gt;
&lt;p&gt;由于感应电机定子、转子磁路、电路均对称，转子位置改变时磁路的磁导不变，故坐标变换时，只要绕组轴线相对静止，就可以使电感系数变为常系数，这样绕组轴线既可选在定子方面也可选在转子方面。&lt;/p&gt;
</content:encoded></item><item><title>晶振及其典型应用与设计参数</title><link>https://arkmostima.pages.dev/posts/%E6%99%B6%E6%8C%AF%E5%8F%8A%E5%85%B6%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8%E4%B8%8E%E8%AE%BE%E8%AE%A1%E5%8F%82%E6%95%B0/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E6%99%B6%E6%8C%AF%E5%8F%8A%E5%85%B6%E5%85%B8%E5%9E%8B%E5%BA%94%E7%94%A8%E4%B8%8E%E8%AE%BE%E8%AE%A1%E5%8F%82%E6%95%B0/</guid><description>晶振可以分为两类： 有源晶振(Oscillator) 无源晶振(Crystal)</description><pubDate>Tue, 30 Jul 2024 16:58:44 GMT</pubDate><content:encoded>&lt;h3&gt;有源晶振(Oscillator)和无源晶振(Crystal)&lt;/h3&gt;
&lt;p&gt;晶振可以分为两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有源晶振(Oscillator)&lt;/li&gt;
&lt;li&gt;无源晶振(Crystal)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;有源晶振(Oscillator)&lt;/h4&gt;
&lt;p&gt;又被称为石英晶体振荡器（简写OSC或XO）。&lt;/p&gt;
&lt;p&gt;有源晶振包含&lt;em&gt;晶体振荡器&lt;/em&gt;和&lt;em&gt;外围集成电路&lt;/em&gt;两个部分，工程上多集成于同一封装内，以4pin贴片形式出现，体积较大，引脚分别为VCC、GND、OUT和NC。&lt;/p&gt;
&lt;p&gt;有源晶振不需要使用MCU的内部振荡器，信号稳定，质量较好，而且连接方式比较简单（仍需设置电源滤波，通常使用一个电容和电感构成滤波网络，输出端用一个小阻值的电阻过滤信号），不需要复杂的配置电路。&lt;/p&gt;
&lt;p&gt;适用场景：有成本和功耗要求，时钟信号精度、稳定性要求不高，IC内部有晶振时钟电路。&lt;/p&gt;
&lt;p&gt;有源晶振原始输出波形一般为&lt;strong&gt;方波&lt;/strong&gt;（若封装内已集成整形电路）。实际观测时因示波器带宽限制可能无法看到方波而只能看到正弦波，因方波的傅里叶分解结果为基频和奇数次谐波叠加，若带宽不够，很容易只能看到高频方波的低频谐波分量，即只显示正弦波。参见：&lt;a href=&quot;https://blog.csdn.net/qq_29350001/article/details/50680676&quot;&gt;有源晶振和无源晶振的输出波形&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/1.webp&quot; alt=&quot;有源晶振原始输出波形&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;无源晶振(Crystal)&lt;/h4&gt;
&lt;p&gt;又被称为石英晶体（Xtal）。&lt;/p&gt;
&lt;p&gt;无源晶振是有2个引脚的无极性元件，需要借助于时钟电路才能产生振荡信号，自身无法起振。其信号质量较差，通常需要精确匹配外围电路（用于信号匹配的RLC元件），更换不同频率的晶体时周边配置电路也需要做相应的调整。&lt;/p&gt;
&lt;p&gt;适用场景：时钟要求高，IC内部无晶振时钟电路。&lt;/p&gt;
&lt;p&gt;无源晶振原始输出波形为&lt;strong&gt;正弦波&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/2.webp&quot; alt=&quot;无源晶振原始输出波形&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;无源晶振的分析模型&lt;/h3&gt;
&lt;h4&gt;电气模型&lt;/h4&gt;
&lt;p&gt;在电气网络中，石英晶体可以转换成一组RLC等效电路，以利分析。这一电路模型有两个频率接近但特性不同的共振点：&lt;strong&gt;低阻抗的串联共振点&lt;/strong&gt;与&lt;strong&gt;高阻抗的并联共振点&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/3.webp&quot; alt=&quot;无源晶振电路符号与等效电路&quot; /&gt;&lt;/p&gt;
&lt;p&gt;$Laplace$变换后，该等效电路网络的阻抗可以写成以下形式：
$$
Z(s)=(\frac{1}{sC_1}+sL_1+R_1)//(\frac{1}{sC_0})
$$
或
$$
Z(s)=\frac{s^2+s\frac{r_1}{L_1}+\omega_s^2}{(sC_0)[s^2+s\frac{R_1}{L_1}+\omega_p^2]}
$$
$$
\omega_s = 1/(\sqrt{L_1C_1})
$$
$$
\omega_p = \sqrt{\frac{C_1+C_0}{L_1C_1C_0}}=\omega_s \sqrt{1+\frac{C_1}{C_0}} \approx \omega_s(1+\frac{C_1}{2C_0}) (C_0 &amp;gt;&amp;gt; C_1)
$$
式中$s=j\omega$，$\omega_s$为串联共振频率，$\omega_p$为并联共振频率，单位均为$rad/s$。&lt;/p&gt;
&lt;p&gt;在晶体两端并联上额外的并联电容器会使并联后的整体共振频率降低，因此，石英晶体厂商在制作并测量石英晶体的并联共振频率时，会在特定的并联电容值（称为负载电容）下进行测试。如使用较小的电容值，振荡频率会比规格高，反之比规格低。这一特性也可以用来微调振荡频率。&lt;/p&gt;
&lt;h4&gt;共振模式&lt;/h4&gt;
&lt;p&gt;石英晶体提供了两种共振模式，由$C_1$与$L_1$构成的串联共振，与由$C_0$、$C_1$与$L_1$构成的并联共振。&lt;/p&gt;
&lt;p&gt;对于一般的MHz级石英晶体而言，串联共振频率一般会比并联共振频率低若干KHz。频率在30MHz以下的石英晶体，通常工作时的频率处于串联共振频率与并联共振频率之间，此时石英晶体呈现电感性阻抗。因为，外部电路上的电容会把电路的振荡频率拉低一些。在设计石英晶体振荡电路时，也应令电路上的杂散电容与外加电容合计値与晶体厂商使用的负载电容值相同，振荡频率才会准确符合厂商的规格。&lt;/p&gt;
&lt;p&gt;频率在30MHz以上（到200MHz）的石英晶体，通常工作于串联共振模式，工作时的阻抗处于最低点，相当于$R_s$。此种晶体通常标示串联电阻（&amp;lt; 100 $\Omega$）而非并联负载电容。为了达到高的振荡频率，石英晶体会振荡在它的一个谐波频率上，此谐波频率是基频的整数倍。因为偶数次谐波会使得晶体内电场互相抵消，只有奇数次谐波可以利用，例如3倍、5倍、与7倍的泛音晶体。要达到所要的振荡频率，振荡电路上会加入额外的电容器与电感器，以选择出所需的频率。&lt;/p&gt;
&lt;h4&gt;温度效应&lt;/h4&gt;
&lt;p&gt;石英晶体的频率特性取决于形状或切割方式。音叉型晶体通常会切割成温度特性是以25℃为中心的抛物线。这意味着，音叉晶体振荡器在室温下产生的共振频率接近其目标频率，当温度或增加或减少时频率都会降低。频率-温度曲线为抛物线的常见32.768千赫音叉晶体的温度系数是负百万分之0.04/摄氏度²。
$$
f=f_0[1-0.04ppm(T-T_0)^2]
$$
也就是说，如不考虑制作误差，以这种石英晶体控制频率的时钟，如运作在比室温低10°C的环境下，每年会比运作在室温下慢2分钟；如运作在比室温低摄氏20°C的环境下，则每年会比运作在室温下慢8分钟。&lt;/p&gt;
&lt;h3&gt;无源晶振典型电路及其参数&lt;/h3&gt;
&lt;h4&gt;皮尔斯晶体振荡器（Pierce Crystal Oscillator）&lt;/h4&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/4.webp&quot; alt=&quot;Pierce Crystal Oscillator&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此电路中晶体决定了振荡频率，并以串联谐振频率工作，$f_s$在输出和输入之间提供低阻抗路径。谐振时存在180度相移，因而反馈为正。输出正弦波的幅度限制在MOSFET漏极端子的最大电压范围内。电阻器$R_1$控制反馈量和晶体驱动量，而射频扼流圈RFC上的电压在每个周期内反转。&lt;/p&gt;
&lt;p&gt;大多数数字时钟、手表和计时器都以某种形式使用皮尔斯振荡器，因为它可以使用最少的组件来实现。&lt;/p&gt;
&lt;h4&gt;MCU振荡器&lt;/h4&gt;
&lt;p&gt;MCU电路中，皮尔斯振荡器常以另一种较为简单的方式出现。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/5.webp&quot; alt=&quot;Pierce Crystal Oscillator (MCU)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$C_{L1}$、$C_{L2}$：外部负载电容&lt;/li&gt;
&lt;li&gt;$R_{EXT}$：阻尼电阻，用于调节激励电流&lt;/li&gt;
&lt;li&gt;$R_F$：反相器反馈电阻，强制反相器工作在线性放大区，用于改善起振。多数MCU会内置该电阻。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;MCU振荡器还有另一种更简洁的形式：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/6.webp&quot; alt=&quot;External Oscillator (MCU)&quot; /&gt;&lt;/p&gt;
&lt;p&gt;该电路中省去了$R_{EXT}$。&lt;/p&gt;
&lt;h4&gt;负载电容($C_L$)&lt;/h4&gt;
&lt;p&gt;负载电容的标称值通常由晶振制造商提供，标示为$C_L$。实际电路中的负载电容是并联在晶振两端的两个电容$C_{L1}$、$C_{L2}$，以及晶振内部电容以及布线电容（寄生电容）共同作用的结果，其计算公式为
$$
C_L=\frac{C_1 \times C_2}{C_1 + C_2}+C_{s}
$$&lt;/p&gt;
&lt;p&gt;式中$C_{s}$是电路中其他杂散电容（包括晶振引脚电容和 PCB 布线电容），大小通常在3-5$pF$。&lt;/p&gt;
&lt;p&gt;实际应用中，选择负载电容的步骤为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;查阅手册，确定晶振的标称负载电容 $C_L$。&lt;/li&gt;
&lt;li&gt;根据经验或者实际测量估算电路板的杂散电容，一般经验值为3-10$pF$。&lt;/li&gt;
&lt;li&gt;根据公式选择合适的$C_{L1}$、$C_{L2}$，使其满足标称负载电容的要求。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;振荡频率(Oscillation frequency)&lt;/h4&gt;
&lt;p&gt;振动频率是指与晶体谐振器一起工作的振荡电路的实际频率。振动频率由晶体谐振器决定，并受MCU、外部负载电容、PCB杂散电容等的影响。
$$
f_{osc}=f_{L}=f_{r}[\frac{C_1}{2(C_0+C_s)}+1]
$$
式中$f_{L}$为负载共振频率，$f_{r}$为共振频率。
&lt;em&gt;注意&lt;/em&gt;：不能用示波器探头、万用表笔等测试工具直接探测晶振输入、输出端，因为示波器探头、万用表笔均具有寄生电容特性，测试时将改变整个负载电容的等效值，测试出的数据不准，同时也会改变晶体的ESR，甚至会出现晶振停振现象。&lt;/p&gt;
&lt;p&gt;参见：&lt;a href=&quot;https://www.murata.com.cn/zh-cn/products/timingdevice/crystalu/overview/basic/frequency&quot;&gt;如何测量振荡频率&lt;/a&gt;&lt;/p&gt;
&lt;h4&gt;激励功率 (Drive Level)和$R_{EXT}$&lt;/h4&gt;
&lt;p&gt;激励功率是指施加在晶振上的功率，通常以微瓦 ($\mu W$) 为单位。它表示晶振在工作时所消耗的能量。激励功率对于晶振的可靠性和寿命至关重要，过高的激励功率可能会导致晶振过热或损坏，过低的激励功率则可能导致晶振无法正常启动或工作不稳定。一般都按手册设计。&lt;/p&gt;
&lt;p&gt;激励功率的观测和调节可基于两种方法进行：&lt;/p&gt;
&lt;h5&gt;电流法&lt;/h5&gt;
&lt;p&gt;根据
$$
DL=I_{RMS}^2 \times ESR
$$
参见：&lt;a href=&quot;https://www.murata.com.cn/zh-cn/products/timingdevice/crystalu/overview/basic/drivelevel&quot;&gt;How to Measure the Drive Level&lt;/a&gt;&lt;/p&gt;
&lt;h5&gt;电压法&lt;/h5&gt;
&lt;p&gt;此处略。&lt;/p&gt;
&lt;p&gt;$R_{EXT}$用于调节激励功率。&lt;/p&gt;
&lt;h4&gt;静态电容（Static Capacitance）&lt;/h4&gt;
&lt;p&gt;静态电容即为并联电容（Parallel Capacitance,$C_0$），是指晶振在不振荡时其两端的电容。这一参数是晶振的固有特性之一，通常由制造商在晶振的规格书中给出。其在不施加任何电压或不进行振荡时测得。它反映了晶体材料和电极结构的固有特性。&lt;/p&gt;
&lt;h4&gt;温漂&lt;/h4&gt;
&lt;p&gt;温漂是指晶振的振荡频率随温度变化而发生的偏移。温漂通常用 ppm/°C（百万分之几每摄氏度）来表示，它是晶振频率稳定性的重要指标之一。&lt;/p&gt;
&lt;p&gt;温漂受以下因素影响：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;材料特性。晶体材料的温度系数是影响温漂的主要因素。石英晶体的温度系数相对较低，但仍会随温度变化而导致频率漂移。&lt;/li&gt;
&lt;li&gt;晶体切割方式。不同的晶体切割方式（如 AT 切割、BT 切割等）对温漂有不同的影响。AT 切割的石英晶体在常用温度范围内具有较好的频率稳定性。&lt;/li&gt;
&lt;li&gt;电路设计。晶振电路中的元件（如负载电容、放大器等）对温度的敏感度也会影响整体的频率温漂。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;应对方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;选用温补晶振（TCXO），通过内部的温度补偿电路，修正因温度变化引起的频率漂移，从而提高晶振的频率稳定性。&lt;/li&gt;
&lt;li&gt;选用恒温控制晶振（OCXO），通过内置的恒温控制系统，将晶振保持在恒定温度下工作，极大地减少温漂影响。&lt;/li&gt;
&lt;li&gt;在温度相对稳定的环境中使用晶振，尽量避免极端温度变化。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;品质因数（Quality Factor，Q值）&lt;/h4&gt;
&lt;p&gt;品质因数$Q$是无量纲的参数，用来表示振荡系统的损耗情况。具体来说，它是储存在晶振中的能量与每周期损耗能量的比值。对于晶振，$Q$值越高，表示其能量损耗越小，振荡的稳定性和纯度越高。
$$
Q=\frac{f_0}{\Delta f}
$$
式中$f_0$为晶振谐振频率，$\Delta f$为频率响应的带宽（即晶振在其谐振频率处的响应下降到其最大值的$1/\sqrt 2$处的频率间隔）。&lt;/p&gt;
&lt;p&gt;高$Q$值晶振起振后的频率稳定性和噪声均较低，但其需要较长的起振时间以达到稳定。&lt;/p&gt;
&lt;h3&gt;典型应用&lt;/h3&gt;
&lt;p&gt;STM32F429IRTx需要在OSC_IN和OSC_OUT引脚外接25MHz晶振以供系统高速时钟源HSE，同时通过内部锁相环（PLL）进行倍频。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/7.webp&quot; alt=&quot;外接的25MHz高频时钟源&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/crystal/8.webp&quot; alt=&quot;使用的YXC X322525MOB4SI型无源贴片晶振&quot; /&gt;&lt;/p&gt;
&lt;p&gt;手册推荐负载电容为10$pF$或20$pF$，选取10$pF$。&lt;/p&gt;
&lt;p&gt;根据工程设计经验，一般取手册值-2或-3$pF$后，再乘以2，作为目标负载电容值。此处即为
$$
C_L&apos;=(C_L-2)\times2=(10-2)\times2=16pF
$$
根据
$$
C_L&apos;=\frac{C_1 \times C_2}{C_1 + C_2}+C_{s}
$$
$C_s$经验值为5$pF$，所以可解得$C_1=C_2=22pF$。&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】交流电机的运算电抗、等效电路、电磁转矩</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E7%9A%84%E8%BF%90%E7%AE%97%E7%94%B5%E6%8A%97%E7%AD%89%E6%95%88%E7%94%B5%E8%B7%AF%E7%94%B5%E7%A3%81%E8%BD%AC%E7%9F%A9/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E7%9A%84%E8%BF%90%E7%AE%97%E7%94%B5%E6%8A%97%E7%AD%89%E6%95%88%E7%94%B5%E8%B7%AF%E7%94%B5%E7%A3%81%E8%BD%AC%E7%9F%A9/</guid><description>在进行瞬态分析时，亦可根据导出的方程式画出相应的等效电路，这样比较直观也便于记忆。一般情况下，最关心的是电枢绕组各量的变化，即从电枢绕组的端点看，能反映瞬态方程式的等效电路和等效阻抗。通过从a、b、c坐标系统到d、q、0坐标系统的变换，…</description><pubDate>Fri, 26 Jul 2024 17:23:26 GMT</pubDate><content:encoded>&lt;h2&gt;同步电机等效运算电路&lt;/h2&gt;
&lt;p&gt;在进行瞬态分析时，亦可根据导出的方程式画出相应的等效电路，这样比较直观也便于记忆。一般情况下，最关心的是电枢绕组各量的变化，即从电枢绕组的端点看，能反映瞬态方程式的等效电路和等效阻抗。通过从a、b、c坐标系统到d、q、0坐标系统的变换，将电感系数从变系数变为了常系数，并通过各基值的选取，使互感系数的标么值可逆，且$L_{ad}=M_{af}=M_{aD}=x_{ad}$，这样就在电路上无联系的几个绕组具备了等效为一个电路的基本条件。当转子转速$\omega$=恒值时，电压方程式为常系数线性微分方程组，可采用拉氏变换将时域的微分方程转换为复频域的代数方程求解。&lt;/p&gt;
&lt;p&gt;下面就转子上只有励磁绕组和同时存在励磁和阻尼绕组两种情况，分别对电枢直轴链和交轴磁链的等效运算电路进行讨论。&lt;/p&gt;
&lt;h3&gt;转子上只有励磁绕组时的等效运算电路&lt;/h3&gt;
&lt;p&gt;该情况下，根据上篇文章的内容，由标幺值下交流电机方程式推导可得（磁链关系代入电压方程式），直轴电枢磁链方程和励磁绕组电压方程为
$$
\psi_d=x_di_d+x_{ad}i_f
$$
$$
u_f=px_{ad}i_d+(r_f+px_f)i_f
$$
式中$p$为时间微分算子。可见各变量为时间的函数，相应的方程为微分方程。当电流$i_d、i_f$的初值为零时，拉氏变换后方程的形式不变，但$p$为一代数复变量，各变量是以$p$为变量的变换函数，如变量$\psi、i、u$经拉氏变换后分别用$\psi(p)、I(p)、U(p)$表示，以示区别。如此改写后的磁链和电压方程式的变换函数为
$$
u_d(p)=x_dI_d(p)+x_{ad}I_f(p)
$$
$$
U_f(p)=px_{ad}I_d(p)+(r_f+px_f)I_f(p)
$$
&lt;em&gt;注意&lt;/em&gt;：此处的$p$在代数学意义上可视为拉普拉斯变换算子$s$。$U(p)$即等同于$U(s)$，从$U(t)$经$Laplace$变换而来。&lt;/p&gt;
&lt;p&gt;用$p$除等式$U_f(p)$两端，并考虑到$x_d=x_l+x_{ad}$，$x_f=x_{fl}+x_{ad}$，整理后可得
$$
\psi_d(p)=x_lI_d(p)+x_{ad}[I_d(p)+I_f(p)]
$$
$$
\frac{U_f(p)}{p}=x_{ad}[I_d(p)+I_f(p)]+(x_{fl}+\frac{r_f}{p})I_f(p)
$$
根据上式即可画出相应的直轴等效运算电路，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/dzhoudengxiao.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在多数情况下只需研究定子上的各变量，因而可以将等式中的转子电流项$I_f(p)$消去，以获得更加简明的关系式。应用戴维南定理，根据图(a)，得到开路电压$G(p)U_f(p)$及短路阻抗$x_d(p)$，即
$$
\psi_d(p)=G(p)U_f(p)+x_d(p)I_d(p)
$$
式中
$$
G_(p)=\frac{x_{ad}}{r_f+x_fp}
$$
$$
x_d(p)=x_l+\frac{1}{\frac{1}{x_{ad}+\frac{1}{x_{fl}+\frac{r_f}{p}}}}
$$
式中$G_(p)$称为励磁电压对直轴磁链的传递函数，$x_d(p)$称为直轴运算电抗。二者都是具有恒系数的运算式，和电机瞬时转速无关，因而可以用来研究任意转速下电机的各种运行工况。&lt;/p&gt;
&lt;h3&gt;转子上有励磁绕组、D轴阻尼绕组和Q轴阻尼绕组&lt;/h3&gt;
&lt;h4&gt;直轴等效运算电路&lt;/h4&gt;
&lt;p&gt;当转子直轴上除励磁绕组外还装有短路的阻尼绕组，且当各绕组电流初值为零时，可得到直轴电枢绕组磁链方程和励磁绕组、直轴阻尼绕组电压方程式的变换函数方程为
$$
\psi_d(p)=x_dI_d(p)+x_{ad}I_f(p)+x_{ad}I_D(p)
$$
$$
U_f(p)=px_{ad}I_d(p)+(r_f+px_f)I_f(p)+px_{ad}I_D(p)
$$
$$
0=px_{ad}I_d(p)+px_{ad}I_f(p)+(r_D+px_D)I_D(p)
$$
用$p$除以等式两侧，考虑$x_d=x_l+x_{ad}$，$x_f=x_{fl}+x_{ad}$，$x_D=x_{Dl}+x_{ad}$，整理后有
$$
\psi_d(p)=x_lI_d(p)+x_{ad}[I_d(p)+I_f(p)+I_D(p)]
$$
$$
\frac{U_f(p)}{p}=x_{ad}[I_d(p)+I_f(p)+I_D(p)]+(x_{fl}+\frac{r_f}{p})I_f(p)
$$
$$
0=x_{ad}[I_d(p)+I_f(p)+I_D(p)]+(x_{Dl}+\frac{r_D}{p})I_D(p)
$$
根据上式，导出得有阻尼绕组时得直轴等效运算电路&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/zunidzhoudengxiao.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据戴维南定理，可得开路电压$G(p)U_f(p)$及短路阻抗$x_d(p)$，则$\psi_d(p)$为
$$
\psi_d(p)=G(p)U_f(p)+x_d(p)I_d(p)
$$
其中
$$
G(p)=\frac{(x_Dx_{ad}-x_{ad}^2)p+x_{ad}r_D}{(x_fx_D-x_{ad}^2)p^2+(x_fr_D+x_Dr_f)p+r_fr_D}
$$
$$
x_d(p)=x_l+\frac{1}{\frac{1}{x_{ad}}+\frac{1}{x_{fl}+\frac{r_f}{p}}+\frac{1}{x_{Dl}+\frac{r_D}{p}}}
$$&lt;/p&gt;
&lt;h4&gt;交轴等效运算电路&lt;/h4&gt;
&lt;p&gt;转子交轴上有短路的阻尼绕组，且各绕组电流初值为零时，可得到交轴电枢绕组磁链方程和交轴阻尼绕组电压方程式的变换函数为
$$
\psi_q(p)=x_qI_q(p)+x_{aq}I_Q(p)
$$
$$
0=x_{aq}I_q(p)+(x_Q+\frac{r_Q}{p})I_Q(p)
$$
类似于直轴情况，考虑$x_q=x_l+x_{aq}$，$x_Q=x_{Ql}+x_{aq}$，消去交轴阻尼绕组电流后，有
$$
\psi_q(p)=x_q(p)I_q(p)
$$
其中
$$
x_q(p)=x_l+\frac{1}{\frac{1}{x_{aq}}+\frac{1}{x_{Ql}+\frac{r_Q}{p}}}
$$
式中$x_q(p)$为交轴运算电抗。交轴等效运算电路如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/zuniqzhoudengxiao.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;运算电抗特性&lt;/h2&gt;
&lt;h3&gt;同步电抗、瞬变（暂态）电抗和超瞬变（次暂态）电抗&lt;/h3&gt;
&lt;p&gt;根据上节内容，直轴、交轴运算电抗的等效电路可导出为下图（未消去转子电流）。
&lt;img src=&quot;/images/1.2/tu25.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据拉氏变换终值定理，$p$趋于零时，$t$趋于无穷大，即同步电机稳态运行所呈现的同步电抗。图中$p$趋于零时，其值相当于转子各绕组均断开时的电抗值，即
$$
\lim_{p \to 0}x_d(p)=x_l+x_{ad}=x_d
$$
$$
\lim_{p \to 0}x_q(p)=x_l+x_{aq}=x_q
$$
根据拉氏变换初值定理，若转子上无阻尼绕组，根据等效电路，可得
$$
\lim_{p \to \infin}x_d(p)=x_l+\frac{1}{\frac{1}{x_{ad}}+\frac{1}{x_{fl}}}=x&apos;&lt;em&gt;{d}
$$
$x&apos;&lt;/em&gt;{d}$即为同步电机的&lt;strong&gt;直轴暂态电抗&lt;/strong&gt;，等效电路如图2-6(a)所示。&lt;/p&gt;
&lt;p&gt;若转子上有阻尼绕组，根据等效电路，有
$$
\lim_{p \to \infin}x_d(p)=x_l+\frac{1}{\frac{1}{x_{ad}}+\frac{1}{x_{fl}}+\frac{1}{x_{Dl}}}=x&apos;&apos;_{d}
$$&lt;/p&gt;
&lt;p&gt;$x&apos;&apos;_{d}$即为同步电机的&lt;strong&gt;直轴次暂态电抗&lt;/strong&gt;，等效电路如图2-6(b)所示。&lt;/p&gt;
&lt;p&gt;同理，在交轴方向，根据等效电路，有
$$
\lim_{p \to \infin}x_q(p)=x_l+\frac{1}{\frac{1}{x_{aq}}+\frac{1}{x_{Ql}}}=x&apos;&apos;&lt;em&gt;{q}
$$
$x&apos;&apos;&lt;/em&gt;{q}$即为同步电机的&lt;strong&gt;交轴次暂态电抗&lt;/strong&gt;，等效电路如图2-6(c)所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu26.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见，暂态电抗和次暂态电抗分别是无阻尼绕组和有阻尼绕组同步电机在瞬态的初瞬间，从电枢绕组端看进去的电抗。该数值在研究同步电机运行时非常有用。&lt;/p&gt;
&lt;h3&gt;以时间常数形式来表示的运算电抗&lt;/h3&gt;
&lt;p&gt;实际应用中，运算电抗$x_d(p)$和$x_q(p)$常用励磁绕组和阻尼绕组的时间常数来表示。&lt;/p&gt;
&lt;p&gt;当转子仅有励磁绕组而无阻尼绕组时，有
$$
x_d(p)=x_d-\frac{x_{ad}^2p}{r_f+x_fp}=x_d[\frac{r_f+(x_f-x_{ad}^2/x_d)}{r_f+x_fp}]=x_d(\frac{r_f+x_f&apos;p}{r_f+x_fp})=x_d(\frac{1+T_f&apos;p}{1+T_fp})
$$
式中，$x_f&apos;$为电枢绕组短路时从励磁绕组端口看进去的瞬态电抗，$T_f&apos;=x_f&apos;/r_f$为励磁绕组瞬态时间常数。$T_f=x_f/r_f$为励磁绕组时间常数。&lt;/p&gt;
&lt;p&gt;电枢绕组短路时，从励磁绕组端口看进去的瞬态电抗$x_f&apos;$的等效电路如图2-7所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu27.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当转子仅有励磁绕组时，&lt;strong&gt;其时间常数$T_f&apos;$就是电枢直轴瞬变电流衰减的时间常数$T_d&apos;$&lt;/strong&gt;，即$T_f&apos;=T_d&apos;$，且$T_f=T_{d0}$。&lt;em&gt;注意&lt;/em&gt;：此处时间常数单位均用时间的标幺值表示。&lt;/p&gt;
&lt;p&gt;求解瞬变电流时常使用运算电抗的倒数形式，有
$$
\frac{1}{x_d(p)}=\frac{1}{x_d} \times \frac{1+T_{d0}}{1+T_d&apos;p}
$$
将该式展开为部分分式，并考虑到$x_d&apos;=\frac{T_d&apos;}{T_{d0}}x_d$，有
$$
\frac{1}{x_d(p)}=\frac{1}{x_d}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})\frac{T_d&apos;p}{1+T_d&apos;p}
$$
该形式在由复数域函数变换返回时域的原函数时非常方便。&lt;/p&gt;
&lt;p&gt;同理可推导，转子上除励磁绕组外，直轴上还装有阻尼绕组时的直轴运算电抗$x_d(p)$的时间常数表达式为&lt;/p&gt;
&lt;p&gt;$$
x_d(p)=x_d[\frac{\sigma_{fD}&apos; T_f&apos; T_D&apos; p^2+(T_f&apos;T_D&apos;)p+1}{\sigma_{fD}T_f T_Dp^2+(T_f+T_D)p+1}]
$$&lt;/p&gt;
&lt;p&gt;式中，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T_D$为直轴阻尼绕组本身的时间常数。$T_D=x_D/r_D$&lt;/li&gt;
&lt;li&gt;$T_D&apos;$为励磁绕组开路、直轴电枢绕组短路时，直轴阻尼绕组的时间常数（瞬态时间常数），$T_D&apos;=x_D&apos;/r_D$。&lt;/li&gt;
&lt;li&gt;$T_D&apos;$计算式中$x_D&apos;=x_{Dl}+\frac{1}{\frac{1}{x_l}+\frac{1}{x_{ad}}}=x_{Dl}+x_{ad}&apos;$，为该情况下直轴绕组的等效电抗，$x_D&apos;$等效电路如图2-8所示。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu28.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$\sigma_{fD}$为电枢绕组&lt;strong&gt;开路&lt;/strong&gt;时，励磁绕组和直轴阻尼绕组间的漏磁系数，$\sigma_{fD}=1-{x_{ad}^2}/{x_fx_D}$&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$\sigma_{fD}&apos;$为电枢绕组&lt;strong&gt;短路&lt;/strong&gt;时，励磁绕组和直轴阻尼绕组间的漏磁系数，$\sigma_{fD}&apos;=1-{x_{ad}&apos;^2}/{x_f&apos;x_D&apos;}$&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;漏磁系数反映绕组间电磁耦合的漏磁情况。当完全耦合无漏磁时，上述漏磁系数为零。实际上每个绕组总存在着只与各自绕组交链的漏磁链和相应的漏电感，因此，漏磁系数不会为零，其大小反映了漏磁的大小。&lt;/p&gt;
&lt;p&gt;转子上除励磁绕组外，直轴上还装有阻尼绕组时的直轴运算电抗$x_d(p)$的时间常数表达式可继续分解为$p$的因式形式：
$$
x_d(p)=\frac{(T_d&apos;p+1)(T_d&apos;&apos;p+1)}{(T_{d0}&apos;p+1)(T_{d0}&apos;&apos;p+1)}x_d
$$
联立，时间常数由下列$p$的二次方程式确定：
$$
(T_{d0}&apos;p+1)(T_{d0}&apos;&apos;p+1)=\sigma_{fD}T_fT_Dp^2+(T_f+T_D)p+1
$$
$$
(T_d&apos;p+1)(T_d&apos;&apos;p+1)=\sigma_{fD}&apos;T_f&apos;T_D&apos;p^2+(T_f&apos;+T_D&apos;)p+1
$$
式中
$$
T_{d0}&apos;=\frac{2\sigma_{fD}T_fT_D}{(1-q)(T_f+T_D)}=\frac{1}{2}(1+q)(T_f+T_D)\approx T_f+T_D
$$
$$
T_{d0}&apos;&apos;=\frac{2\sigma_{fD}T_fT_D}{(1+q)(T_f+T_D)} \approx  \frac{\sigma_{fD}T_fT_D}{(T_f+T_D)}
$$
$$
T_d&apos;=\frac{2\sigma_{fD}&apos;T_f&apos;T_D&apos;}{(1-q&apos;)(T_f&apos;+T_D&apos;)}=\frac{1}{2}(1+q&apos;)(T_f&apos;+T_D&apos;)\approx T_f&apos;+T_D&apos;
$$
$$
T_d&apos;&apos;=\frac{2\sigma_{fD}&apos;T_f&apos;T_D&apos;}{(1+q&apos;)(T_f&apos;+T_D&apos;)} \approx  \frac{\sigma_{fD}&apos;T_f&apos;T_D&apos;}{(T_f&apos;+T_D&apos;)}
$$
式中$q\approx1$，$q&apos;\approx1$。&lt;/p&gt;
&lt;p&gt;上式中的$T_{d0}&apos;$、$T_{d0}&apos;&apos;$、$T_d&apos;$、$T_d&apos;&apos;$是同步电机主要的四个时间常数，其准确值计算由上式给出。但以上计算非常繁琐，工程计算角度可进一步简化。考虑阻尼绕组电阻$r_D$标幺值远大于励磁绕组电阻$r_f$的标幺值，而相应的电抗$x_D$、$x_f$的标幺值相近，$x_D&apos;$、$x_f&apos;$的标幺值亦相近，因此在直轴阻尼绕组和励磁绕组的时间常数中，$T_D$远小于$T_f$，且$T_D&apos;$远小于$T_f&apos;$。因而之前的计算式可进一步简化为
$$
T_{d0}&apos;=T_f+T_D \approx T_f = T_{d0}
$$
$$
T_{d0}&apos;&apos; \approx \frac{\sigma_{fD}T_fT_D}{(T_f+T_D)} \approx \sigma_{fD}T_D
$$
$$
T_d&apos;=T_f&apos;+T_D&apos; \approx T_f&apos;
$$
$$
T_d&apos;&apos; \approx \frac{\sigma_{fD}&apos;T_f&apos;T_D&apos;}{(T_f&apos;+T_D&apos;)} \approx \sigma_{fD}&apos;T_D&apos;
$$
进一步可导出
$$
T_{d0}&apos;&apos; \approx \sigma_{fD}T_D=(1-\frac{x_{ad}^2}{x_fx_D})\frac{x_D}{r_D}=(x_{Dl}+\frac{1}{\frac{1}{x_{ad}}+\frac{1}{x_{fl}}})\frac{1}{r_D}
$$
$$
T_d&apos;&apos; \approx \sigma_{fD}&apos;T_D&apos;=(1-\frac{x_{ad}&apos;^2}{x_f&apos;x_D&apos;})\frac{x_D&apos;}{r_D}=(x_D&apos;-\frac{x_{ad}&apos;^2}{x_f&apos;})\frac{1}{r_D}=(x_{Dl}+\frac{1}{\frac{1}{x_{ad}}+\frac{1}{x_l}+\frac{1}{x_{fl}}})\frac{1}{r_D}
$$
当直轴设置有阻尼绕组时，从以上对直轴运算电抗$x_d(p)$导出的时间常数的准确计算式可看出，$T_{d0}&apos;$、$T_{d0}&apos;&apos;$、$T_d&apos;$、$T_d&apos;&apos;$四个时间常数均与励磁绕组和阻尼绕组两个绕组的时间常数有关，因为这两个绕组均放置在转子直轴上，二者之间存在类似于变压器一、二次绕组间的电磁能量传递关系。与变压器不同，同步电机中时间常数$T_D$远小于$T_f$，且$T_D&apos;$远小于$T_f&apos;$，因而可简化，简化后的等效电路见图2-9.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu29.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中，$T_{d0}&apos;$、$T_d&apos;$是电枢绕组分别为开路、短路时励磁绕组的时间常数，二者与阻尼绕组无关；而$T_{d0}&apos;&apos;$、$T_d&apos;&apos;$是励磁绕组短路，电枢绕组分别为开路、短路时直轴阻尼绕组的时间常数。&lt;/p&gt;
&lt;p&gt;为方便逆变换，可导出$x_d(p)$的倒数形式为&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{x_d(p)}=\frac{1}{x_d}+(\frac{1}{x_d&apos;}-\frac{1}{x_d})\frac{T_d&apos;p}{T_d&apos;p+1}+(\frac{1}{x_d&apos;&apos;}-\frac{1}{x_d&apos;})\frac{T_d&apos;&apos;p}{T_d&apos;&apos;p+1}
$$&lt;/p&gt;
&lt;p&gt;交轴运算电抗$x_q(p)$的表达式亦可用时间常数表示为
$$
x_q(p)=x_q-\frac{x_{aq}^2p}{r_Q+x_Qp}=x_q(\frac{T_q&apos;&apos;p+1}{T_{q0}&apos;&apos;p+1})
$$
式中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$T_{q0}&apos;&apos;$为交轴阻尼绕组本身的时间常数，$T_{q0}&apos;&apos;=x_Q/r_Q$&lt;/li&gt;
&lt;li&gt;$T_q&apos;&apos;$为考虑到交轴电枢绕组短路时，交轴阻尼绕组的时间常数，$T_q&apos;&apos;=\frac{1}{r_Q}(x_{Ql}+x_{aq}x_l/(x_{aq}+x_l))$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;$T_{q0}&apos;&apos;$和$T_q&apos;&apos;$所对应的等效电路如图2-10所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu210.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;为方便逆变换，可导出$x_q(p)$的倒数形式为&lt;/p&gt;
&lt;p&gt;$$
\frac{1}{x_q(p)}=\frac{1}{x_q}+(\frac{1}{x_q&apos;&apos;}-\frac{1}{x_q})\frac{T_q&apos;&apos;p}{1+T_q&apos;&apos;p}
$$&lt;/p&gt;
&lt;h2&gt;同步电机的等效电路&lt;/h2&gt;
&lt;p&gt;上文已经通过电枢磁链$\psi_d(p)$、$\psi_q(p)$的运算方程式，导出了等效运算电路及相应的运算电抗，在时域中也可画出类似于变压器和感应电机的同步电机等效电路。为获得直轴上各绕组的等效电路，将标幺值表示的直轴磁链方程式代入电压方程式，有
$$
\begin{bmatrix}
u_d\u_f\0
\end{bmatrix}=p
\begin{bmatrix}
x_d &amp;amp; x_{ad} &amp;amp; x_{ad} \
x_{ad} &amp;amp; x_f &amp;amp; x_{ad} \
x_{ad} &amp;amp; x_{ad} &amp;amp; x_D
\end{bmatrix}
\begin{bmatrix}
i_d \ i_f \ i_D
\end{bmatrix}+
\begin{bmatrix}
r_a &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; r_f &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; r_D \
\end{bmatrix}
\begin{bmatrix}
i_d \ i_f \ i_D
\end{bmatrix}+
\begin{bmatrix}
-\omega \psi_q \ 0 \ 0
\end{bmatrix}
$$
相应的直轴等效电路如图2-11所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu211.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;同理可列出交轴电压方程式为
$$
\begin{bmatrix}
u_q \ 0
\end{bmatrix}=p
\begin{bmatrix}
x_q &amp;amp; x_{aq} \
x_{aq} &amp;amp; x_Q
\end{bmatrix}
\begin{bmatrix}
i_q \ i_Q
\end{bmatrix}+
\begin{bmatrix}
r_a &amp;amp; 0 \
0 &amp;amp; r_Q \
\end{bmatrix}
\begin{bmatrix}
i_q \ i_Q
\end{bmatrix}+
\begin{bmatrix}
\omega \psi_d \ 0
\end{bmatrix}
$$
相应的交轴等效电路如图2-12所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/tu212.webp&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然，方程式与等效电路图是对应的，从方程式可画出等效电路，反过来，根据等效电路图，利用基尔霍夫电压定律也可写出各绕组回路的电压方程式。&lt;/p&gt;
&lt;p&gt;根据前面分析的结果，分析隐极同步电机时，只要令上面等效电路中$x_{ad}=x_{aq}$，即可使用；分析感应电机时，再将励磁回路取消即可。&lt;/p&gt;
&lt;p&gt;下面就图2-11、2-12所示的同步电机等效电路图与通常电机学中感应电机等效电路图的异同比较如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&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;/li&gt;
&lt;li&gt;
&lt;p&gt;在频率折算上，稳态感应电机的等效电路是将旋转的转子绕组通过频率折算,把实际转子绕组的频率由$f_2=sf_1$转换为定子绕组频率$f_1$，而同步电机的等效电路是将静止的定子绕组轴线通过坐标变换变为与转子同步旋转的d、q两相绕组轴线，亦即将电枢绕组中频率为$f_1$的交流量转换为频率为零的直流量。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;同步电机的功率及电磁转矩&lt;/h2&gt;
&lt;p&gt;有名值表示三相同步电机电枢端的瞬时输入功率为
$$
P=u_ai_a+u_bi_b+u_ci_c
$$
标幺值系统中功率基值为
$$
P_b=3U_NI_N=\frac{3}{2}U_bI_b
$$
因此标幺值表示时电机电枢端瞬时三相输入功率为
$$
P^&lt;em&gt;=\frac{2}{3}(u_a^&lt;em&gt;i_a^&lt;/em&gt;+u_b^&lt;em&gt;i_b^&lt;/em&gt;+u_c^&lt;em&gt;i_c^&lt;/em&gt;)
$$
对上式中变量进行Park变换后，dq0坐标系中三相输入功率的标幺值为
$$
P^&lt;/em&gt;=u_d^&lt;em&gt;i_d^&lt;/em&gt;+u_q^&lt;em&gt;i_q^&lt;/em&gt;+2u_0^&lt;em&gt;i_0^&lt;/em&gt;
$$
dq0坐标系下因定子绕组由原先的“静止绕组”转化为“伪静止绕组”，绕组电感系数由时变系数转为常系数，因而先前在相坐标系统中导出的电磁转矩公式已经不再适用。下面以能量守恒原理为基础重新推导适用于这种绕组的电磁转矩表达式。&lt;/p&gt;
&lt;p&gt;由于零轴电流所产生的气隙合成磁场为零，仅产生与电枢绕组交链的漏磁场，因而不参与电磁功率的转换，故在推导电磁转矩时可以不考虑零轴分量。&lt;/p&gt;
&lt;p&gt;由dq0坐标系中三相输入功率的标幺值可知，零轴对应的输入功率为$2u_0i_0$，当不考虑电枢零轴分量时，从各端点输入的电功率瞬时值的总和为
$$
I&apos;^TU&apos;=u_di_d+u_qi_q+u_fi_f
$$
式中
$$
I&apos;^T=
\begin{bmatrix}
i_d &amp;amp; i_q &amp;amp; i_f &amp;amp; i_D &amp;amp; i_Q
\end{bmatrix}
$$
$$
U&apos;= \begin{bmatrix}
u_d &amp;amp; u_q &amp;amp; u_f &amp;amp; 0 &amp;amp; 0
\end{bmatrix}^T
$$
根据标幺值系统下电压方程式，可导出不考虑电枢零轴分量的电压方程式为
$$
U&apos;=L&apos;pI&apos;+\omega G&apos;I&apos;+R&apos;I&apos;
$$
式中&apos;上标表示该矩阵未考虑零轴分量，其可以分别从相应的原始矩阵中除去零轴分量的第三行以及第三列得到。如该矩阵方程左乘以电流的转置矩阵$I&apos;^T$，则功率方程为
$$
I&apos;^TU&apos;=I&apos;^TL&apos;pI&apos;+I&apos;^T\omega G&apos;I&apos;+I&apos;^TR&apos;I&apos;
$$
方程的左边是从各端点输入的总电功率，右边由二项组成：第一项为电流和磁链变化率的乘积，即$I&apos;^TL&apos;pI&apos;=I&apos;^Tp\Psi&apos;$，其代表了定、转子绕组储存磁场能量的变化率；第三项$I&apos;^TR&apos;I&apos;$代表了定、转子绕组的电阻损耗。根据功平衡关系可知，第二项$I&apos;^T\omega G&apos;I&apos;$是跨过气隙由电功率转为机械功率的电磁功率。用转速$\omega$除它，即得到用标么值表示的电磁转矩，即
$$
T_{em}=\frac{I&apos;^T\omega G&apos;I&apos;}{\omega}=I&apos;^T G&apos;I&apos;
$$
矩阵$G$中第三行和第三列元素均为0，所以有
$$
I&apos;^T G&apos;I&apos;=I^TGI
$$
即
$$
T_{em}=I^TGI
$$
该式适用于“伪静止绕组”电机。将电流矩阵$I$和矩阵$G$代入上式，可得三相同步电机的电磁转矩为
$$
T_{em}=i_q\psi_d-i_d\psi_q
$$&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】交流电机在相坐标系统中的状态方程式、电磁转矩和转子运动方程式</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E5%9C%A8%E7%9B%B8%E5%9D%90%E6%A0%87%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%8A%B6%E6%80%81%E6%96%B9%E7%A8%8B%E5%BC%8F%E7%94%B5%E7%A3%81%E8%BD%AC%E7%9F%A9%E5%92%8C%E8%BD%AC%E5%AD%90%E8%BF%90%E5%8A%A8%E6%96%B9%E7%A8%8B%E5%BC%8F/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E5%9C%A8%E7%9B%B8%E5%9D%90%E6%A0%87%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%8A%B6%E6%80%81%E6%96%B9%E7%A8%8B%E5%BC%8F%E7%94%B5%E7%A3%81%E8%BD%AC%E7%9F%A9%E5%92%8C%E8%BD%AC%E5%AD%90%E8%BF%90%E5%8A%A8%E6%96%B9%E7%A8%8B%E5%BC%8F/</guid><description>将先前得出的磁链方程式代入电压方程式，即可得到交流电机在相坐标系统中的电压方程式：</description><pubDate>Tue, 23 Jul 2024 16:04:14 GMT</pubDate><content:encoded>&lt;h3&gt;交流电机在相坐标系统中的状态方程式&lt;/h3&gt;
&lt;p&gt;将先前得出的磁链方程式代入电压方程式，即可得到交流电机在相坐标系统中的电压方程式：
$$
\begin{bmatrix}
u_a \
u_b \
u_c \
u_f \
u_D \
u_Q
\end{bmatrix}=
p
\begin{bmatrix}
L_{aa} &amp;amp; M_{ab} &amp;amp; M_{ac} &amp;amp; M_{af} &amp;amp; M_{aD} &amp;amp; M_{aQ}\
M_{ba} &amp;amp; L_{bb} &amp;amp; M_{bc} &amp;amp; M_{bf} &amp;amp; M_{bD} &amp;amp; M_{bQ}\
M_{ca} &amp;amp; M_{cb} &amp;amp; L_{cc} &amp;amp; M_{cf} &amp;amp; M_{cD} &amp;amp; M_{cQ}\
M_{fa} &amp;amp; M_{fb} &amp;amp; M_{fc} &amp;amp; L_{f}  &amp;amp; M_{fD} &amp;amp; 0 \
M_{Da} &amp;amp; M_{Db} &amp;amp; M_{Dc} &amp;amp; M_{Df} &amp;amp; L_{D} &amp;amp; 0  \
M_{Qa} &amp;amp; M_{Qb} &amp;amp; M_{Qc} &amp;amp; 0 &amp;amp; 0 &amp;amp; L_{Q}\
\end{bmatrix}
\begin{bmatrix}
i_a\
i_b\
i_c\
i_f\
i_D\
i_Q
\end{bmatrix}+
\begin{bmatrix}
r_a &amp;amp; &amp;amp; &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; r_b &amp;amp; &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; r_c &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; r_f &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; &amp;amp; r_D &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; &amp;amp; &amp;amp; r_Q \
\end{bmatrix}
\begin{bmatrix}
i_a\
i_b\
i_c\
i_f\
i_D\
i_Q
\end{bmatrix}
$$
即
$$
U=p(LI)+RI
$$
矩阵中各绕组的自感系数和互感系数在之前的文章中已有推导。&lt;/p&gt;
&lt;p&gt;这是一组表述电机基本电磁关系的&lt;em&gt;时变&lt;/em&gt;系数的微分方程组，在已知电感系数和电阻的情况下，利用这组方程就可以求解&lt;strong&gt;恒速&lt;/strong&gt;运转时电机的瞬态和稳态运行问题。由于定子绕组电感系数和定、转子绕组间的互感系数均为位置角$\theta$的函数，即为时间的函数，因此，在求解上述电压方程式时,不能采用解析方法,而需采用数值解法。&lt;/p&gt;
&lt;p&gt;用数值法求解微分方程组,可将式$U=p(LI)+RI$改写为状态方程组。状态方程是一组一阶的微分方程式。在电机分析中利用状态变量法时，一般可选取各绕组回路中的电流或磁链作为状态变量，这些变量都是不能突变的量。&lt;em&gt;此处的更多内容请参阅《线性控制理论》。&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;若选取定子三相电流$i_a$、$i_b$、$i_c$，及转子绕组电流$i_f$、$i_D$、$i_Q$为状态变量，则电压方程式可改写为：
$$
U=pLI+RI=(pL)I+LpI+RI
$$
式中$pL$为电感系数矩阵的一阶导数。&lt;/p&gt;
&lt;p&gt;用状态方程组的标准形式将上式表示如下
$$
\dot I = AI + BU
$$
式中
$$
A=-L^{-1}[D+R]\
D=pL\
B=L^{-1}
$$
当需要考虑电感系数的高次谐波项时，由于定子绕组的自感系数和互感系数，及定、转子绕组间的互感系数均为位置角$\theta$的无穷三角级数，系数矩阵$L$及其导数$D$的计算工作量很大，用手工计算是不可能的，因而随着计算机技术的发展和广泛应用，可利用计算机求解上述状态方程组的数值解。&lt;/p&gt;
&lt;p&gt;相坐标系统下导出的凸极同步电机的电压方程式，只要作相应的变化，就可适用于隐极同步电机和感应电机。隐极同步电机与凸极同步电机的差别是隐极同步电机的气隙为均匀的。感应电机与隐极同步电机相比，定子结构都是相同的，气隙也是均匀的，只是感应电机转子绕组中没有励磁绕组，感应电机的短路转子绕组相当于同步电机的阻尼绕组。所以从凸极同步电机入手推导的方程式，只需作很小的变动，就可以方便地用于隐极同步电机和感应电机。&lt;/p&gt;
&lt;p&gt;相坐标系统下交流电机的电压方程式的特点是可以考虑空间气隙磁场谐波，适用于计算磁场谐波很强的电机。利用这些方程，可求解&lt;strong&gt;恒速&lt;/strong&gt;运转时的电机瞬态和稳态运行问题。如果再加上电机的电磁转矩和转子运动方程，电机变速运转时的机电过渡过程也可求解。下节将对电机的电磁转矩和转子运动方程加以讨论。&lt;/p&gt;
&lt;h3&gt;交流电机电磁转矩和转子运动方程式&lt;/h3&gt;
&lt;p&gt;电磁转矩是电机进行机电能量转换的关键量，也是表征电机性能的重要量。而转子运动方程是根据牛顿定律写出的作用在电机转子上的转矩之间的平衡方程式，当考虑转子转速变化时，除上述电压方程式外，尚需增加转子运动方程式联立求解。下面先讨论交流电机的电磁转矩。&lt;/p&gt;
&lt;p&gt;设交流电机的定子绕组有$m$个回路，以下标&quot;$sm$&quot;表示定子第$m$个回路；转子绕组有$n$个回路，以下标&quot;$rn$&quot;表示定子第$n$个回路。假设磁路为线性，则电机的总磁场能量为
$$
W_{m}=\frac{1}{2}(\psi_{s1}i_{s1}+\psi_{s2}i_{s2}+...+\psi_{sm}i_{sm}+\psi_{r1}i_{r1}+\psi_{r2}i_{r2}+...+\psi_{rn}i_{rn})
$$
式中各回路的磁链分别为
$$
\psi_{s1}=L_{s1}i_{s1}+...+M_{s1sm}i_{sm}+M_{s1r1}i_{r1}+...+M_{s1rn}i_{rn}\
...\
\psi_{sm}=M_{sms1}i_{s1}+...+L_{sm}i_{sm}+M_{smr1}i_{r1}+...+M_{smrn}i_{rn}\
\psi_{r1}=M_{r1s1}i_{r1}+...+M_{r1sm}i_{sm}+L_{r1}i_{r1}+...+M_{r1rn}i_{rn}\
...\
\psi_{rn}=M_{rns1}i_{s1}+...+M_{rnsm}i_{sm}+M_{rnr1}i_{r1}+...+L_{rn}i_{rn}
$$&lt;/p&gt;
&lt;p&gt;若改写为矩阵形式，可表示为
$$
W_m=\frac{1}{2}I^TLI
$$&lt;/p&gt;
&lt;p&gt;根据&lt;em&gt;虚位移定理&lt;/em&gt;，当各回路电流保持不变时，有
$$
f=\frac{\partial W_m}{\partial g}|i=const
$$
式中$f$为广义力，$g$为广义位移。&lt;/p&gt;
&lt;p&gt;若视广义位移为转子绕组参考轴线领先于定子绕组参考轴线的机械角度$\gamma$，则广义力即为电机的电磁转矩$T_{em}$，有
$$
T_{em}=\frac{\partial W_m}{\partial \gamma}=\frac{\partial (\frac{1}{2}I^TLI)}{\partial \gamma}=\frac{1}{2}I^T\frac{\partial L}{\partial \gamma}I
$$&lt;/p&gt;
&lt;p&gt;电机极对数为$p$，$\theta$为转子绕组参考轴线领先于定子绕组参考轴线的电角度，$\gamma$为同步电机d轴领先于a轴的位置角（机械角度），则有
$$
\gamma = \theta / p
$$
电磁转矩可继续改写为
$$
T_{em}=\frac{p}{2}I^T\frac{\partial L}{\partial \theta}I
$$
在建立转子运动方程时，按电动机惯例选用电磁转矩的正方向，它的正方向与转子正常旋转方向一致。如下图；&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/zhuanzifangcheng.webp&quot; alt=&quot;建立转子运动方程用图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;其中$T_{em}$为电动机的电磁转矩，是驱动转矩；$T_m$为负载的机械转矩，是制动转矩；剩余转矩$T_{em}-T_m$则为加速转矩。由此可得转子运动方程为
$$
J\frac{d\Omega}{dt}=T_{em}-T_m
$$
式中$J$为转动惯量($kgm^2$)，$\Omega$为转子机械角速度，$\Omega=\frac{d\gamma}{dt}$。&lt;/p&gt;
&lt;p&gt;电机中常用电弧度来表示角度，将上式改写为：
$$
J\frac{1}{p} \times \frac{d\omega}{dt}=J\frac{1}{p} \times \frac{d^2\theta}{dt^2}=T_{em}-T_m
$$
式中$\omega$为转子转速($rad/s$)，$\omega = \frac{d\theta}{dt}$。&lt;/p&gt;
&lt;p&gt;根据电力拖动内容，若转动部分飞轮转矩为$GD^2$，工程常用单位为$t·m^2$，则
$$
J=\frac{1}{4}GD^2 \times 10^3 kgm^2
$$
联立转化为状态方程为
$$
\frac{d\omega}{dt}=\frac{p}{J}(T_{em}-T_m)\
\frac{d\theta}{dt}=\omega
$$
当需要求解电机变速问题，即$\omega$不为常系数时，需通过联立求解两个状态方程式（上节和本节）得到各状态变量。此时$\theta \neq \omega t + \theta_0$，需通过上式中的第二式解出$\theta$。&lt;/p&gt;
&lt;h3&gt;转子运动方程式的标幺值形式&lt;/h3&gt;
&lt;p&gt;工程实践中，转子运动方程式常用标幺值形式。转矩的基值一般选为
$$
T_b=\frac{S_N \times 10^3}{\frac{2\pi n}{60}}
$$
式中$S_N$为电机额定视在功率($kVA$)，$n$为电机同步转速($r/min$)。&lt;/p&gt;
&lt;p&gt;时间基值一般选为$\tau_b=\frac{1}{2\pi f}$，即为电机转过一个电弧度所需的时间。用转矩基值除原式的两侧，可得
$$
H\frac{d^2\theta}{dt^2}=H\frac{d\omega^&lt;em&gt;}{dt^&lt;/em&gt;}=(T_{em}^&lt;em&gt;-T_m^&lt;/em&gt;)
$$
其中$H$被称为惯性常数，可导出为
$$
H=\frac{1}{p} \times \frac{GD^2}{S_N}2\pi^3f^2(\frac{n}{60})=2\pi^3f\times \frac{GD^2}{S_N}(\frac{n}{60})^2
$$
$H$和时间标幺值之间有简单的关系：
$$
dt^&lt;em&gt;=\frac{Hd\omega^&lt;/em&gt;}{T_{em}^&lt;em&gt;-T_m^&lt;/em&gt;}
$$
假设剩余转矩$T_{em}^&lt;em&gt;-T_m^&lt;/em&gt;=1$，电机转速从零上升至同步转速($\omega^&lt;em&gt;=1$)所需的时间为$\tau$，则
$$
\tau = \int_{0}^{\tau}dt^&lt;/em&gt;=\int_{0}^{1}Hd\omega^*=H
$$
可见，在原来静止的电机转子上加上标幺值为1的剩余转矩后，转速由零上升至同步转速的时间标幺值等于$H$。&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】同步电机标幺值系统</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E6%A0%87%E5%B9%BA%E5%80%BC%E7%B3%BB%E7%BB%9F/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E5%90%8C%E6%AD%A5%E7%94%B5%E6%9C%BA%E6%A0%87%E5%B9%BA%E5%80%BC%E7%B3%BB%E7%BB%9F/</guid><description>电气工程领域中，因采用有名值时不同容量电机参数之间不便于比较，故通常的工程计算中多采用标幺值系统。</description><pubDate>Sat, 20 Jul 2024 16:09:23 GMT</pubDate><content:encoded>&lt;h3&gt;标幺值系统&lt;/h3&gt;
&lt;p&gt;电气工程领域中，因采用有名值时不同容量电机参数之间不便于比较，故通常的工程计算中多采用标幺值系统。&lt;/p&gt;
&lt;p&gt;标幺值的定义为：标幺值=有名值/基值。其中基值与有名值的单位应当相同。&lt;/p&gt;
&lt;p&gt;要采用标幺值系统，则必须首先解决基值的选取问题。电机分析最常用的一套基值系统选取原则为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使采用标幺值后电机各方程式的形式与采用有名值时的相同&lt;/li&gt;
&lt;li&gt;用标幺值表示的电枢与转子绕组间的互感系数是可逆的&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;显然电机中各物理量的量纲都可以表示为电压、电流和时间三个量纲的组合，因此只要确定这三个变量的基值，就可推广确定电机运行中所有变量的标幺值基值。&lt;/p&gt;
&lt;h3&gt;定子各变量基值的选取&lt;/h3&gt;
&lt;p&gt;瞬态分析时各变量均采用瞬时值形式，故电压、电流基值常采用其额定值的幅值，而不使用其有效值。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定子电流基值$I_b$选取定子相电流额定值的幅值&lt;/li&gt;
&lt;li&gt;定子电压基值$U_b$选取定子相电压额定值的幅值&lt;/li&gt;
&lt;li&gt;时间基值$\tau_b=1/\omega_N=1/2{(\pi f_N)}(s)$，即在额定频率$f_N$下经过一个电弧度所需的时间。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;以上三个变量均为独立地选取其基值，为保证方程式形式在使用标幺值系统时保持不变，以下变量的基值需要根据量纲关系，基于上述三个基本变量的基值进行导出。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;阻抗基值：$Z_b=\frac{U_b}{I_b}$  $(\Omega=V/A)$&lt;/li&gt;
&lt;li&gt;角频率基值 $\omega_b=\frac{1}{\tau_b}$ $(rad/s)$&lt;/li&gt;
&lt;li&gt;电感基值 $L_b=\frac{Z_b}{\omega_b}=\frac{U_b}{I_b}\tau_b$ $(H=Vs/A)$&lt;/li&gt;
&lt;li&gt;磁链基值 $\psi_b=L_bI_b=U_b\tau_b$ $(Wb=Vs)$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;此处电感基值的推导基于$Z_L=j\omega L$。&lt;/p&gt;
&lt;p&gt;以下基值按惯用方法选取：功率基值取三相&lt;strong&gt;额定&lt;/strong&gt;视在功率，即&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;功率基值 $P_b=3U_NI_N=\frac{3}{2}U_{Nm}I_{Nm}=\frac{3}{2}U_bI_b$ $(VA)$ （注意$U_b$和$I_b$均为幅值而非有效值，所以等式第三项要除以两次根号2）&lt;/li&gt;
&lt;li&gt;转矩基值 $T_b = \frac{P_b}{\Omega_N}=\frac{P_b}{\omega_b}p$ $(Nm)$ 式中$\Omega_N$为额定机械角速度，单位$rad/s$；$p$为磁极对数。
（注意此处有$\omega_b=p\Omega_N$，即同步电机电气角频率基值等于电机额定机械角速度乘以极对数。对于单极电机，额定机械角速度等于电气角频率；对于多极电机，电气角频率一般数倍于额定机械角速度。）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;转子各量基值的选取&lt;/h3&gt;
&lt;p&gt;转子的时间基值$\tau_b$与定子一致，关键是要确定转子各绕组电压、电流基值。下面讨论常用的可逆的互感系数和$x_{ad}$基值系统的转子基值选取方法。&lt;/p&gt;
&lt;p&gt;假定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$I_{fb}$、$U_{fb}$、$\psi_{fb}$为励磁绕组的电流基值、电压基值和磁链基值；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$I_{Db}$、$U_{Db}$、$\psi_{Db}$为直轴阻尼绕组的电流基值、电压基值和磁链基值；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$I_{Qb}$、$U_{Qb}$、$\psi_{Qb}$为交轴阻尼绕组的电流基值、电压基值和磁链基值。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;令定子电流基值及电压基值与转子绕组相应量的基值的比值为
$$
k_{if}=\frac{I_b}{I_{fb}},k_{iD}=\frac{I_b}{I_{Db}}.k_{iQ}=\frac{I_b}{I_{Qb}}\
k_{uf}=\frac{U_b}{U_{fb}},k_{uD}=\frac{U_b}{U_{Db}}.k_{uQ}=\frac{U_b}{U_{Qb}}
$$&lt;/p&gt;
&lt;p&gt;由$\psi=LI$和$L_b=\frac{Z_b}{\omega_b}=\frac{U_b}{I_b}\tau_b$，可推导得$\psi_b=U_b\tau_b$。所以定子、转子磁链基值比与电压基值比相同，即：
$$
k_{uf}=\frac{\psi_b}{\psi_{fb}},k_{uD}=\frac{\psi_b}{\psi_{Db}},k_{uQ}=\frac{\psi_b}{\psi_{Qb}}
$$
用标幺值表示的磁链方程式可推导为：
$$
\left[\begin{array}{l}
\psi_{d}^* \
\psi_{f}^* \
\psi_{D}^*
\end{array}\right]=\left[\begin{array}{ccc}
L_d^* &amp;amp; M_{af}^* &amp;amp; M_{aD}^&lt;em&gt;\
M_{fa}^&lt;/em&gt; &amp;amp; L_f^* &amp;amp; M_{fD}^&lt;em&gt;\
M_{Da}^&lt;/em&gt; &amp;amp; M_{Df}^* &amp;amp; L_D^*
\end{array}\right]\left[\begin{array}{l}
i_{d}^* \
i_{f}^* \
i_{D}^*
\end{array}\right]
$$
上式中各自感系数和互感系数的标幺值可推导如下：
$$
M_{af}^&lt;em&gt;=\frac{M_{af0}}{L_b}\frac{1}{k_{if}}\
M_{fa}^&lt;/em&gt;=\frac{\frac{3}{2}M_{af0}}{L_b}k_{uf}\
L_{d}^&lt;em&gt;=\frac{L_d}{L_b}\
M_{aD}^&lt;/em&gt;=\frac{M_{aD0}}{L_b}\frac{1}{k_{iD}}\
M_{Da}^&lt;em&gt;=\frac{\frac{3}{2}M_{aD0}}{L_b}k_{uD}\
L_f^&lt;/em&gt;=\frac{L_f}{L_b}\frac{k_{uf}}{k_{if}}\
L_D^&lt;em&gt;=\frac{L_D}{L_b}\frac{k_{uD}}{k_{iD}}\
M_{fD}^&lt;/em&gt;=\frac{M_{fD}}{L_b}\frac{k_{uf}}{k_{iD}}\
M_{Df}^*=\frac{M_{Df}}{L_b}\frac{k_{uD}}{k_{if}}
$$&lt;/p&gt;
&lt;p&gt;要使以上各式中的互感系数可逆，即要求
$$
M_{af}^&lt;em&gt;=M_{fa}^&lt;/em&gt;,M_{aD}^&lt;em&gt;=M_{Da}^&lt;/em&gt;,M_{fD}^&lt;em&gt;=M_{Df}^&lt;/em&gt;
$$
显然要令上式成立，则必须在标幺值系统中选取合适的转子电流基值和电压基值，即：
$$
k_{uf}k_{if}=\frac{2}{3},
k_{uD}k_{iD}=\frac{2}{3},
k_{uQ}k_{iQ}=\frac{2}{3}
$$
因此，要获得可逆的互感系数，&lt;strong&gt;定、转子各绕组的电压基值比与电流基值比的乘积需等于2/3&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;显然这个条件很容满足，这说明在定子各量基值已经选定并要求互感系数可逆的条件下，转子方面的电流基值和电压基值的选取方式仍然很多，但$k_i$和$k_u$中则只能自由选定其中一个，另一个则由2/3法则确定。&lt;/p&gt;
&lt;p&gt;工程上常用$x_{ad}$基值系统来确定转子方面的变量基值。该系统中，电枢d轴绕组自感中对应主磁场部分（$L_{ad}$）的标幺值、电枢d轴绕组与励磁绕组互感系数$M_{af}$的标幺值以及电枢d轴绕组与直轴阻尼绕组互感系数$M_{aD}$的标幺值相等，即
$$
L_{ad}^&lt;em&gt;=M_{af}^&lt;/em&gt;=M_{aD}^*
$$&lt;/p&gt;
&lt;p&gt;将等式代入，很容易得到：
$$
L_{ad}I_b=M_{af0}I_{fb}
$$
可见$x_{ad}$基值系统中励磁电流基值选取的规则是：励磁电流基值所产生的通过气隙的主磁场与电枢d绕组的互感磁链应与电枢d绕组通过基值定子电流所产生的通过气隙的自感磁链相等。&lt;/p&gt;
&lt;p&gt;励磁电流基值$I_{fb}$也可通过实验求得。将上式两边同乘以额定角频率$\omega_N$，得
$$
\omega_N M_{af0} I_{fb}=\omega_N L_{ad}I_b=x_{ad}I_{Nm}
$$
该式表明基值励磁电流在定子各相绕组中将感生出有名值幅值为$x_{ad}I_{Nm}$的空载电动势。因此以下实验可确定励磁电流基值：同步电机以额定同步转速旋转，定子各绕组开路，励磁绕组通以电流，各项绕组中产生的基波空载（不计饱和）电压幅值为$x_{ad}I_b=x_{ad}I_{Nm}$,此时的励磁电流值即为励磁电流基值。&lt;/p&gt;
&lt;p&gt;按照该种方法选定转子电流基值后，可确定转子电压基值。原来不相等的一些电感系数还可以变为相等：
$$
L_{ad}^&lt;em&gt;=M_{af}^&lt;/em&gt;=M_{fa}^&lt;em&gt;=M_{aD}^&lt;/em&gt;=M_{Da}^&lt;em&gt;\
L_{aq}^&lt;/em&gt;=M_{aQ}^&lt;em&gt;=M_{Qa}^&lt;/em&gt;\
M_{fD}^&lt;em&gt;=M_{Df}^&lt;/em&gt;=\frac{M_{fD}}{L_b} \times \frac{2}{3}(\frac{I_{fb}I_{Db}}{I_b^2})\
L_f^&lt;em&gt;=\frac{L_f}{L_b} \times \frac{2}{3}(\frac{I_{fb}}{I_b})^2\
L_D^&lt;/em&gt;=\frac{L_D}{L_b} \times \frac{2}{3}(\frac{I_{Db}}{I_b})^2\
L_Q^*=\frac{L_Q}{L_b} \times \frac{2}{3}(\frac{I_{Qb}}{I_b})^2
$$&lt;/p&gt;
&lt;p&gt;转子变量标幺值的折算也可以作以下理解，即先将转子方面的参数折合到定子方面，然后再采用定子方面的基值求其标幺值，非常类似于变压器中高低压两侧的标幺值折算方法。如：
$$
L_f^*=\frac{L_fk_{uf}}{L_bk_{if}}
$$
可以看作是励磁绕组自感$L_f$乘以$k_{uf}/k_{if}$折合到定子方面，再除以定子电感基值$L_b$。&lt;/p&gt;
&lt;p&gt;励磁绕组与直轴阻尼绕组间的互感系数标幺值$M_{Df}^&lt;em&gt;=M_{fD}^&lt;/em&gt;$，在不考虑只与它们两个绕组共同交链的不通过气隙的漏磁链时，则有$M_{Df}^&lt;em&gt;=M_{fD}^&lt;/em&gt;=L_{ad}^*$。一般情况下工程计算中都会作该简化，此时对电枢电流计算带来的误差可忽略，对转子电流计算会带来一定的误差。&lt;/p&gt;
&lt;h1&gt;将$M_{Df}^&lt;em&gt;=M_{fD}^&lt;/em&gt;=L_{ad}^&lt;em&gt;$代入磁链方程式中，并考虑电感标幺值与额定频率下相应电抗的标幺值在数值上相等，即$x^&lt;/em&gt;=\frac{\omega_N L}{\omega_b L_b}=L^&lt;em&gt;$，因此磁链方程式可以改写为：
$$
\begin{bmatrix}
\psi_d^&lt;/em&gt; \
\psi_f^* \
\psi_D^*
\end{bmatrix}&lt;/h1&gt;
&lt;h1&gt;\begin{bmatrix}
x_d^* &amp;amp; x_{ad}^* &amp;amp; x_{ad}^* \
x_{ad}^* &amp;amp; x_f^* &amp;amp; x_{ad}^* \
x_{ad}^* &amp;amp; x_{ad}^* &amp;amp; x_D^*
\end{bmatrix}
\begin{bmatrix}
i_d^* \
i_f^* \
i_D^*
\end{bmatrix}
$$
同理有
$$
\begin{bmatrix}
\psi_q^* \
\psi_Q^*
\end{bmatrix}&lt;/h1&gt;
&lt;p&gt;\begin{bmatrix}
x_q^* &amp;amp; x_{aq}^*  \
x_{aq}^* &amp;amp; x_Q^*  \
\end{bmatrix}
\begin{bmatrix}
i_q^* \
i_Q^*
\end{bmatrix}
$$
其中
$$
\begin{aligned}
x_d^* &amp;amp;= x_l^* + x_{ad}^* \
x_q^* &amp;amp;= x_l^* + x_{aq}^* \
x_f^* &amp;amp;= x_{fl}^* + x_{ad}^* \
x_D^* &amp;amp;= x_{Dl}^* + x_{ad}^* \
x_Q^* &amp;amp;= x_{Ql}^* + x_{aq}^*
\end{aligned}
$$
励磁绕组电压基值可由以下方式确定。&lt;/p&gt;
&lt;p&gt;因为
$$
k_{uf}k_{if}=\frac{U_b}{U_{fb}}\times \frac{I_b}{I_{fb}}=\frac{2}{3}
$$
故
$$
U_{fb}=\frac{3}{2}\times\frac{I_b}{I_{fb}}U_b
$$&lt;/p&gt;
&lt;p&gt;励磁绕组阻抗基值的取法与电枢阻抗相似，为励磁绕组电压、电流基值的比，即
$$
Z_{fb}=\frac{U_{fb}}{I_{fb}}=\frac{3}{2}(\frac{I_b}{I_{fb}})^2Z_b
$$
同样方法可求出直轴、交轴阻尼绕组的电压基值和阻抗基值。&lt;/p&gt;
&lt;h3&gt;交流电机基本方程式的标幺值形式&lt;/h3&gt;
&lt;p&gt;现在，就可以把原有的dq坐标系下的交流电机基本方程式改写为基于$x_{ad}$基值系统的标幺值形式。&lt;/p&gt;
&lt;p&gt;定、转子电压方程标幺值的形式与有名值形式下的相同，即
$$
\begin{bmatrix}
u_d^* \
u_q^* \
u_0^* \
u_f^* \
u_D^* \
u_Q^*
\end{bmatrix}
=p
\begin{bmatrix}
\psi_d^* \
\psi_q^* \
\psi_0^* \
\psi_f^* \
\psi_D^* \
\psi_Q^*
\end{bmatrix}
+
\begin{bmatrix}
r_a^* &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; r_a^* &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; r_a^* &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_f^* &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_D^* &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_Q^*
\end{bmatrix}
\begin{bmatrix}
i_d^* \
i_q^* \
i_0^* \
i_f^* \
i_D^* \
i_Q^*
\end{bmatrix}
+
\begin{bmatrix}
-\omega^* \psi_q^* \
\omega^* \psi_d^* \
0 \
0 \
0 \
0
\end{bmatrix}
$$
将磁链方程式代入，整理可得
$$
U^&lt;em&gt;=L^&lt;em&gt;pI^&lt;/em&gt;+\omega^&lt;em&gt;G^&lt;em&gt;I^&lt;/em&gt;+R^&lt;em&gt;I^&lt;/em&gt;
$$
式中
$$
U^&lt;/em&gt;=[U_{dq0}^&lt;/em&gt; \quad U_{fDQ}^&lt;em&gt;]^T\
$$
$$
I^&lt;/em&gt;=[I_{dq0}^* \quad I_{fDQ}^&lt;em&gt;]^T
$$
$$
R^&lt;/em&gt;=
\begin{bmatrix}
R_{abc}^* &amp;amp; 0\
0 &amp;amp; R_{fDQ}^*
\end{bmatrix}
$$
$$
\begin{array}{c}
L^{&lt;em&gt;}=\left[\begin{array}{cccccc}
x_{\mathrm{d}}^&lt;/em&gt; &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{ad}}^{&lt;em&gt;} &amp;amp; x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 \
0 &amp;amp; x_{\mathrm{q}}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{a q}^{&lt;/em&gt;} \
0 &amp;amp; 0 &amp;amp; x_{0}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{f}}^{&lt;em&gt;} &amp;amp; x_{a d}^{&lt;/em&gt;} &amp;amp; 0 \
x_{a d}^{&lt;em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{ad}^{&lt;/em&gt;} &amp;amp; x_{\mathrm{D}}^{&lt;em&gt;} &amp;amp; 0 \
0 &amp;amp; x_{\mathrm{aq}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{Q}}^{*}
\end{array}\right]&lt;/p&gt;
&lt;p&gt;\&lt;/p&gt;
&lt;p&gt;G^{&lt;em&gt;}=\left[\begin{array}{cccccc}
0 &amp;amp; -x_{\mathrm{q}}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; -x_{a q}^{&lt;em&gt;} \
x_{d}^{&lt;/em&gt;} &amp;amp; 0 &amp;amp; 0 &amp;amp; x_{\mathrm{ad}}^{&lt;em&gt;} &amp;amp; x_{\mathrm{ad}}^{&lt;/em&gt;} &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0
\end{array}\right]
\end{array}
$$&lt;/p&gt;
&lt;p&gt;以上$G^&lt;em&gt;$为电压方程中角速度$\omega^&lt;/em&gt;$项的系数矩阵，反映了旋转电动势的项的系数大小，仅在电枢电压、$u_d^&lt;em&gt;$、$u_q^&lt;/em&gt;$对应的行里有元素。&lt;/p&gt;
&lt;p&gt;可以看出，通过线性变换后，原来定子绕组a、b、c三相的各变量已被新的d、q、0坐标系统中各变量所取代。a、b、c三相绕组是实际存在的三个回路，其绕组轴线为静止的，且在空间上互差$2\pi/3$电弧度。变换后的d、q、0坐标系统中各变量是d、q、0三个绕组回路的变量，其中d、q绕组的轴线与转子一道旋转，且d轴与转子直轴轴线(励磁绕组轴线)相重合，d、q轴在空间上相差$\pi/2$电弧度。零轴绕组在电方面是独立的，与其他绕组无电磁耦合关系。&lt;/p&gt;
&lt;p&gt;从这以后，在一般不加说明的情况下，电机各量均采用标么值，并将表示标么值的“ * ”符号省去。&lt;/p&gt;
</content:encoded></item><item><title>【电机瞬态分析】交流电机方程式（dq0坐标系）</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E6%96%B9%E7%A8%8B%E5%BC%8Fdq0%E5%9D%90%E6%A0%87%E7%B3%BB/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E6%96%B9%E7%A8%8B%E5%BC%8Fdq0%E5%9D%90%E6%A0%87%E7%B3%BB/</guid><description>上一篇文章对三相凸级同步电机在a、b、c坐标系下的电压方程式和磁链方程式，并且给出了定子和转子绕组之间的电感系数。在需要考虑气隙谐波磁场时，定子绕组的自感系数、互感系数，以及定子绕组和转子绕组之间的互感系数均是关于转子位置角$\thet…</description><pubDate>Sun, 14 Jul 2024 05:08:23 GMT</pubDate><content:encoded>&lt;h3&gt;概述&lt;/h3&gt;
&lt;p&gt;上一篇文章对三相凸级同步电机在a、b、c坐标系下的电压方程式和磁链方程式，并且给出了定子和转子绕组之间的电感系数。在需要考虑气隙谐波磁场时，定子绕组的自感系数、互感系数，以及定子绕组和转子绕组之间的互感系数均是关于转子位置角$\theta$的无穷级数。虽然工程中可以忽略高次项进行近似计算，但计算过程仍是非常复杂的。&lt;/p&gt;
&lt;p&gt;在仅考虑气隙磁场基波成分的前提下，通过Park变换，用dq0坐标系下的新变量替代原abc相坐标系下的变量，可使定子绕组的自感系数和互感系数又时变系数变为常系数。&lt;/p&gt;
&lt;h3&gt;交流电机在dq0坐标系中的电压方程式和磁链方程式&lt;/h3&gt;
&lt;h4&gt;相坐标系至dq0坐标系的变换&lt;/h4&gt;
&lt;p&gt;由线性代数知识，当满秩线性变换时，新变量与原变量间具有单值的对应关系。此时，变换矩阵C（中间量）应为满秩。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;满秩矩阵的充分必要条件是其行列式的值不为0。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;在此变换下，线性变换和线性反变换式可写为：
$$
Y = CX \
X = C^{-1}Y
$$
其中$C^{-1}$是$C$的逆矩阵。&lt;/p&gt;
&lt;p&gt;电机工程中最常用的坐标系即为dq0坐标系，即将原来静止的定子绕组a、b、c绕组变换至与转子同角速度旋转的d、q轴线及独立的零轴线代替，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/raozuzhouxian.webp&quot; alt=&quot;电机绕组轴线位置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然在使用dq0坐标系时，由于d、q轴随着转子旋转而与转子轴线呈相对静止，电枢绕组的自感和互感系数变为常数。零轴绕组在电磁方面为独立轴，与其他绕组无耦合关系。&lt;/p&gt;
&lt;p&gt;以电枢电流为例，变换后的新变量$i_d$，$i_q$，$i_0$与原变量$i_a$，$i_b$，$i_c$关系为：
$$
\left[\begin{array}{l}
i_{d} \
i_{q} \
i_{0}
\end{array}\right]=\frac{2}{3}\left[\begin{array}{ccc}
\cos \theta &amp;amp; \cos \left(\theta-\frac{2 \pi}{3}\right) &amp;amp; \cos \left(\theta+\frac{2 \pi}{3}\right) \
-\sin \theta &amp;amp; -\sin \left(\theta-\frac{2 \pi}{3}\right) &amp;amp; -\sin \left(\theta+\frac{2 \pi}{3}\right) \
\frac{1}{2} &amp;amp; \frac{1}{2} &amp;amp; \frac{1}{2}
\end{array}\right]\left[\begin{array}{l}
i_{a} \
i_{b} \
i_{c}
\end{array}\right]
$$
式中第三个新变量$i_0$与$i_a$，$i_b$，$i_c$的关系为
$$
i_0 = \frac{1}{3}(i_a+i_b+i_c)
$$
$i_0$被称为定子绕组电流的零轴分量，为瞬时值。&lt;/p&gt;
&lt;p&gt;显然$i_0$与电机各项绕组连接方式有关：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;有中线连接时，$i_0$实际为电机中线电流的1/3&lt;/li&gt;
&lt;li&gt;无中线连接时。$i_0$为零&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后续分析同步电机及其系统时，若将电流分量变换至dq0坐标系，且原相坐标系中的电压、磁链等量也要同步变换至dq0坐标系，其变换矩阵$C$与电流变量的变换矩阵相同。&lt;/p&gt;
&lt;h4&gt;磁链方程式（dq0坐标系）&lt;/h4&gt;
&lt;p&gt;将相坐标系中的磁链方程式转化为dq0坐标系中的磁链方程式，其过程如下：
$$
\left[\begin{array}{l}
\Psi_{dq0} \
\Psi_{fDQ}
\end{array}\right]=\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{l}
\Psi_{abc} \
\Psi_{fDQ}
\end{array}\right]=\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{cc}
L_{ss} &amp;amp; L_{sr} \
L_{rs} &amp;amp; L_{rr}
\end{array}\right]\left[\begin{array}{l}
I_{abc} \
I_{fDQ}
\end{array}\right]
$$
式中，$\Psi_{dq0}$为电机电枢d、q、0三轴绕组的磁链矩阵表达式，$\Psi_{dq0}=[\phi_d \quad \phi_q \quad \phi_0]^{T}$；$\Psi_{fDQ}$为电机电枢f、D、Q三轴绕组的磁链矩阵表达式，$\Psi_{fDQ}=[\phi_f \quad \phi_D \quad \phi_Q]^{T}$，$E$为单位矩阵。&lt;/p&gt;
&lt;p&gt;联立上述各式，可得计算后的结果为：
$$
\left[\begin{array}{l}
\psi_{d} \
\psi_{q} \
\psi_{0} \
\psi_{f} \
\psi_{D} \
\psi_{Q}
\end{array}\right]=\left[\begin{array}{cccccc}
L_d &amp;amp; 0 &amp;amp; 0 &amp;amp; M_{af0} &amp;amp; M_{aD0} &amp;amp; 0\
0 &amp;amp; L_q &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; M_{aQ0}\
0 &amp;amp; 0 &amp;amp; L_0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0\
\frac{3}{2}M_{af0} &amp;amp; 0 &amp;amp; 0 &amp;amp; L_f &amp;amp; M_{fD} &amp;amp; 0\
\frac{3}{2}M_{aD0} &amp;amp; 0 &amp;amp; 0 &amp;amp; M_{Df} &amp;amp; L_{D} &amp;amp; 0\
0 &amp;amp; \frac{3}{2}M_{aQ0} &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; L_Q
\end{array}\right]\left[\begin{array}{l}
i_{d} \
i_{q} \
i_{0} \
i_{f} \
i_{D} \
i_{Q}
\end{array}\right]
$$
其中
$$
L_d = L_{aal}+M_{abl}+\frac{3}{2}L_{mad}=L_{leak} + L_{ad} \
L_q = L_{aal}+M_{abl}+\frac{3}{2}L_{maq}=L_{leak} + L_{aq} \
L_0 = L_{aal} - 2M_{abl}
$$
式中，$L_{aal}$为电枢a相绕组自漏感系数($l$意为$leak$)，$M_{abl}$为电枢a相和b相绕组之间的互漏感系数，$L_{aad}$为电枢a相绕组自感系数在d轴上的分量，$L_{aaq}$为电枢a相绕组自感系数在q轴上的分量。$M_{af0}$为定子a轴与励磁绕组d轴重合时所具有的最大互感系数，$M_{aD0}$为定子a轴与阻尼绕组D轴重合时所具有的最大互感系数，$M_{aQ0}$为定子a轴与阻尼绕组Q轴重合时所具有的最大互感系数，下标中的“0”表示该互感系数计算时仅考虑了基波分量。&lt;/p&gt;
&lt;p&gt;由上式可看出：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;$L_d$和$L_q$分别是假想的电枢d轴绕组和电枢q轴绕组的自感系数。它包括不通过气隙的漏磁链所对应的自漏感系数$L_{leak}=L_{aal}+M_{abl}$以及通过气隙的主磁场所对应的电感系数$L_{ad}=\frac{3}{2}L_{aad}$以及$L_{aq}=\frac{3}{2}L_{aaq}$。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;$L_0$是变换后假想的电枢零轴绕组的自感系数，其仅由漏感系数组成。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;电枢绕组从abc相绕组轴线转换为与转子同速旋转的d、q轴以及独立的零轴后，电枢绕组的自感系数及互感系数均由时变系数变为与$\theta$角无关的常数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;坐标变换使具有相互电磁耦合的abc相绕组变为没有耦合关系的假想的电枢d轴、q轴绕组，达到了解耦的目的。对于电机控制系统，该操作可获得更良好的控制性能。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;电压方程式（dq0坐标系）&lt;/h4&gt;
&lt;p&gt;dq0坐标系中的电压方程式可采用同样的方式从相坐标系统下的电压方程式转换而来，即
$$
\left[\begin{array}{l}
U_{dq0} \
U_{fDQ}
\end{array}\right]=\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{l}
U_{abc} \
U_{fDQ}
\end{array}\right]=\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{l}
p\Psi_{abc} \
p\Psi_{fDQ}
\end{array}\right]+\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{cc}
R_{abc} &amp;amp; 0 \
0 &amp;amp; R_{fDQ}
\end{array}\right]\left[\begin{array}{l}
I_{abc} \
I_{fDQ}
\end{array}\right]
$$&lt;/p&gt;
&lt;p&gt;式中，$U_{dq0}$为电枢d、q、0绕组轴端的电压矩阵，$U_{dq0}=[u_d \quad u_q \quad u_0]^{T}$。&lt;/p&gt;
&lt;p&gt;上式中等式右侧的第一项：
$$
\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{l}
p\Psi_{abc} \
p\Psi_{fDQ}
\end{array}\right]=\left[\begin{array}{cc}
C &amp;amp; (p \quad \Psi_{abc}) \
p &amp;amp; \Psi_{fDQ}
\end{array}\right]
$$
根据磁链的Park变换：
$$
\Psi_{dq0} = C \Psi_{abc}
$$
故有
$$
p \Psi_{dq0} = (pC)\Psi_{abc}+C(p \quad \Psi_{abc})
$$
可得
$$
C(p \quad \Psi_{abc}) = p\Psi_{dq0} - (pC)\Psi_{abc}
$$
可以证明其中
$$
-(pC)\Psi_{abc} = -{pC}C^{-1}\Psi_{dq0}=\omega \left[\begin{array}{ccc}
0 &amp;amp; -1 &amp;amp; 0 \
1 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp;0
\end{array}\right]\left[\begin{array}{l}
\phi_d\
\phi_q\
\phi_0
\end{array}\right]=\left[\begin{array}{c}
-\omega\psi_q\
\omega\psi_d\
0
\end{array}\right]
$$
式中$\omega$为转子的瞬时转速，$\omega = p \theta$。&lt;/p&gt;
&lt;h1&gt;转换式等式右侧的第二项（即电阻压降）可以写成：
$$
\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{cc}
R_{abc} &amp;amp; 0 \
0 &amp;amp; R_{fDQ}
\end{array}\right]\left[\begin{array}{c}
I_{abc} \
I_{fDQ}
\end{array}\right]=
\left[\begin{array}{cc}
C &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{cc}
R_{abc} &amp;amp; 0 \
0 &amp;amp; R_{fDQ}
\end{array}\right]\left[\begin{array}{cc}
C^{-1} &amp;amp; 0 \
0 &amp;amp; E
\end{array}\right]\left[\begin{array}{c}
I_{dq0} \
I_{fDQ}
\end{array}\right]&lt;/h1&gt;
&lt;p&gt;\left[\begin{array}{cc}
R_{abc} &amp;amp; 0 \
0 &amp;amp; R_{fDQ}
\end{array}\right]\left[\begin{array}{c}
I_{dq0} \
I_{fDQ}
\end{array}\right]
$$
联立上述各式，可导出dq0坐标系统下的电压方程式：
$$
\begin{bmatrix}
u_d \
u_q \
u_0 \
u_f \
u_D \
u_Q
\end{bmatrix}
=p
\begin{bmatrix}
\psi_d \
\psi_q \
\psi_0 \
\psi_f \
\psi_D \
\psi_Q
\end{bmatrix}
+
\begin{bmatrix}
r_a &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; r_a &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; r_a &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_f &amp;amp; 0 &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_D &amp;amp; 0 \
0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; 0 &amp;amp; r_Q
\end{bmatrix}
\begin{bmatrix}
i_d \
i_q \
i_0 \
i_f \
i_D \
i_Q
\end{bmatrix}
+
\begin{bmatrix}
-\omega \psi_q \
\omega \psi_d \
0 \
0 \
0 \
0
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;根据上式，显然假想的电枢d、q轴绕组的电压方程中，与a、b、c绕组的“静止绕组”相比，除$p\psi$仅表示变压器电动势外，另有正比于角速度的运动电动势$\omega\psi$项存在。&lt;/p&gt;
&lt;p&gt;在dq0坐标系统中，因重新选取了电枢绕组的轴线，使得d、q轴线以$\omega$转速旋转，而构成d、q绕组的线圈元件仍为静止，因而线圈元件与磁链之间存在相对运动，产生了运动电动势$\omega\psi$。这种绕组轴线与构成该绕组的线圈元件间存在相对运动的绕组称为“伪静止绕组”。&lt;/p&gt;
&lt;h4&gt;同步电机的派克（Park）方程&lt;/h4&gt;
&lt;p&gt;派克变换(Park Transform)实际是以克拉克变换(Clark Transform，静止dq坐标系变换)推广而来。恒定幅值条件下的Clark线性变换矩阵即下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/park1.webp&quot; alt=&quot;恒定幅值Clark变换矩阵&quot; /&gt;&lt;/p&gt;
&lt;p&gt;式中，矩阵前的常系数$\frac{2}{3}$用于保证在变换后，变量的幅值仍保持不变。&lt;/p&gt;
&lt;p&gt;若需要进行恒功率Clark变换，变换矩阵可被替换为：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.2/park2.webp&quot; alt=&quot;恒定功率Clark变换矩阵&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当Clark变换被推广至旋转dq坐标系（即包含转子位置角$\theta$作为变量）时，结合三角恒等变换，可导出Park变换方程。相关推导参见&lt;a href=&quot;https://blog.csdn.net/weixin_44462280/article/details/110290942&quot;&gt;《Clark变换与Park变换的推导》&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Park方程为定常系数微分方程，在同步电机分析中使用该方程可以通过解析方法求解&lt;strong&gt;恒速&lt;/strong&gt;运转时电机的瞬态和稳态运行问题。&lt;/p&gt;
&lt;p&gt;上述方程中各变量都是有名值,用有名值的好处是物理念明确,单位的量纲清楚,但在实际计算中往往有所不便,因此，分析计算电机的许多问题时，经常采用标么值。&lt;/p&gt;
&lt;h3&gt;dq0坐标系统与双反应理论&lt;/h3&gt;
&lt;p&gt;电机学中已经介绍过，双反应理论是由Andre Blondel提出的一种解释同步电机内部磁场分布的方法。该理论假设同步电机的电枢磁动势（MMF）可以分解为两个独立的分量：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;d轴反应：与转子磁链对齐的磁动势分量，主要影响电机的励磁电流。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;q轴反应：与d轴垂直的磁动势分量，主要影响电机的转矩电流。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;双反应理论认为这两个反应是独立的，并且可以分别计算，从而简化了电机的分析。&lt;/p&gt;
&lt;p&gt;dq0坐标系统与双反应理论在概念和应用上具有很大的统一性。具体表现如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;坐标对齐：在dq0坐标系统中，d轴和q轴分别对应双反应理论中的d轴反应和q轴反应。这使得电机的磁动势和电流可以通过这两个独立的分量来表示。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;数学简化：两者都旨在简化电机的数学分析。dq0坐标系统将交流变量转换为直流变量，而双反应理论将复杂的三相系统分解为两个独立的轴。这两种方法结合使用，可以大大简化电机的动态建模和控制设计。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;独立性假设：双反应理论假设d轴反应和q轴反应是独立的，这在dq0坐标系统中也得到了体现。dq0系统中的电压和电流方程可以分别在d轴和q轴上进行解耦分析。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同步电机的dq0坐标系统和双反应理论都为简化和深入理解电机内部的物理现象提供了工具。dq0坐标系统通过数学变换将电机的时变问题转换为静态问题，而双反应理论则通过物理解释将电机的磁动势分解为两个独立分量。两者在概念和实际应用上具有很强的统一性，结合使用能够更好地分析和控制同步电机。&lt;/p&gt;
</content:encoded></item><item><title>将基于Hexo的个人网站提交至Google收录</title><link>https://arkmostima.pages.dev/posts/%E5%B0%86%E5%9F%BA%E4%BA%8Ehexo%E7%9A%84%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E6%8F%90%E4%BA%A4%E8%87%B3google%E6%94%B6%E5%BD%95/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%B0%86%E5%9F%BA%E4%BA%8Ehexo%E7%9A%84%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99%E6%8F%90%E4%BA%A4%E8%87%B3google%E6%94%B6%E5%BD%95/</guid><description>Hexo博客生成并部署后，无论是部署至第三方平台（如Github Pages），还是部署至私有服务器然后定向至个人域名，都需要人工向搜索引擎提交收录。简单来说这一过程就是把该网站的内容和地址“告诉”搜索引擎，让搜索引擎“认识”你。这样当…</description><pubDate>Sat, 13 Jul 2024 08:04:58 GMT</pubDate><content:encoded>&lt;h2&gt;简介&lt;/h2&gt;
&lt;p&gt;Hexo博客生成并部署后，无论是部署至第三方平台（如Github Pages），还是部署至私有服务器然后定向至个人域名，都需要人工向搜索引擎提交收录。简单来说这一过程就是把该网站的内容和地址“告诉”搜索引擎，让搜索引擎“认识”你。这样当下次有人在搜索引擎中搜索与你博客有关联的内容时，搜索引擎就能够发现你了。&lt;/p&gt;
&lt;p&gt;我们以Google为例，要完成这个过程，通常需要以下步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;确认博客是否被收录&lt;/li&gt;
&lt;li&gt;验证网站所有者&lt;/li&gt;
&lt;li&gt;生成并提交站点地图（sitemap）&lt;/li&gt;
&lt;li&gt;等待收录&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;一、确认博客是否被收录&lt;/h3&gt;
&lt;p&gt;以本网站为例，打开Google，输入&lt;code&gt;site:akichen891.github.io&lt;/code&gt;
&lt;img src=&quot;/images/sitemap/sousuo.webp&quot; alt=&quot;搜索结果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果Google提示“找不到相符的内容”，则说明你的网站尚未被Google收录。如果网站已经被收录，Google会自动查询和网站有关的内容。&lt;/p&gt;
&lt;h3&gt;二、验证网站所有者&lt;/h3&gt;
&lt;p&gt;只有向搜索引擎添加并认证网站，证明自己是该域名的拥有者和管理者后，搜索引擎可以快速批量添加该域名下的所有子站点，而无需一一验证。认证完成后还可以使用Google Console查看并管理网站的访问统计等、&lt;/p&gt;
&lt;p&gt;要进行验证，首先前往&lt;a href=&quot;https://www.google.com/webmasters/tools/home?hl=zh-CN&quot;&gt;Google Console&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Google Console会要求选择资源类型。
&lt;img src=&quot;/images/sitemap/tianjia.webp&quot; alt=&quot;选择资源类型&quot; /&gt;&lt;/p&gt;
&lt;p&gt;选择“网址前缀”，在输入框中输入带&lt;code&gt;http://&lt;/code&gt;或&lt;code&gt;https://&lt;/code&gt;前缀的完整主域名，然后点击“继续”。&lt;/p&gt;
&lt;p&gt;Google提供五种验证方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;HTML文件&lt;/li&gt;
&lt;li&gt;HTML标记&lt;/li&gt;
&lt;li&gt;Google Analytics&lt;/li&gt;
&lt;li&gt;Google跟踪代码管理器&lt;/li&gt;
&lt;li&gt;域名提供商&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这几种方法依次为从简到难。这里只介绍头两种。&lt;/p&gt;
&lt;h4&gt;HTML文件验证&lt;/h4&gt;
&lt;p&gt;这是最简单的一种验证方法，具体步骤Google也写的很清楚。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/sitemap/1.webp&quot; alt=&quot;HTML文件验证&quot; /&gt;&lt;/p&gt;
&lt;p&gt;点击下载这个&lt;code&gt;.html&lt;/code&gt;文件，对于Hexo，将该文件放在Hexo根目录下&lt;code&gt;/source&lt;/code&gt;文件中。然后&lt;code&gt;hexo g -d&lt;/code&gt;，等待部署。当你能够在浏览器中输入&lt;code&gt;https://example.blog/googleXXXXXXX.html&lt;/code&gt;并成功跳转时，说明该文件已经添加成功。&lt;/p&gt;
&lt;p&gt;回到Google Console，点击验证，正常情况下Google Console很快就会提示验证成功。&lt;/p&gt;
&lt;p&gt;如果使用该方法却一直提示验证失败，可以用第二种方法。&lt;/p&gt;
&lt;h4&gt;HTML标记验证&lt;/h4&gt;
&lt;p&gt;这种方法和第一种方法非常类似，只不过将验证文件变成了一个html元标记。
&lt;img src=&quot;/images/sitemap/2.webp&quot; alt=&quot;HTML标记验证&quot; /&gt;&lt;/p&gt;
&lt;p&gt;按照要求，复制元标记。对于Hexo博客，这个所谓的“首页”文件的位置可能有所不同。对于Fluid主题，该文件即为&lt;code&gt;\node_modules\hexo-theme-fluid\layout\_partials&lt;/code&gt;目录下的&lt;code&gt;head.ejs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;按照Google的要求，在&lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt;中粘贴该元标记。
&lt;img src=&quot;/images/sitemap/3.webp&quot; alt=&quot;HTML标记验证&quot; /&gt;&lt;/p&gt;
&lt;p&gt;之后，&lt;code&gt;hexo clean&lt;/code&gt;，然后&lt;code&gt;hexo g -d&lt;/code&gt;，等待部署。部署完成后在Google Console中点击验证。&lt;/p&gt;
&lt;h2&gt;三、站点地图(Sitemap)&lt;/h2&gt;
&lt;h3&gt;什么是站点地图？&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;站点地图是网站上的一个文件，可以让 Google 知道应该了解网站上的哪些网页。如果您使用了网站托管服务（比如 Squarespace 或 Wix），这些服务可能会为您管理站点地图，这样的话，您就无需创建自己的站点地图，也无需使用此报告。若想了解站点地图的相关信息，请搜索您的托管服务提供商。如果您的网站规模较小（少于 100 个网页），并且您可以通过追踪首页上的一个或多个链接到达网站上的任何网页，则可能无需使用站点地图或此报告。在这种情况下，只需请求将您的首页编入索引即可（除非您使用的是上述网站托管服务，在这种情况下您无需执行任何操作）。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;为Hexo生成站点地图&lt;/h3&gt;
&lt;p&gt;为启用站点地图，首先要在Hexo根目录中Git Bash来安装插件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install hexo-generator-sitemap --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;strong&gt;站点配置文件&lt;/strong&gt;（不是主题配置文件！）&lt;code&gt;_config.yml&lt;/code&gt;中添加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Auto Generate sitemap
sitemap:
path: sitemap.xml
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后&lt;code&gt;hexo g -d&lt;/code&gt;，若&lt;code&gt;/public&lt;/code&gt;文件夹下生成了&lt;code&gt;sitemap.xml&lt;/code&gt;，并且浏览器能够访问&lt;code&gt;https://example.blog/sitemap.xml&lt;/code&gt;，说明创建成功。&lt;/p&gt;
&lt;h3&gt;向Google提交站点地图&lt;/h3&gt;
&lt;p&gt;在Google Console中，左侧栏选择“站点地图”，在“添加新的站点地图”中，输入你的站点地图网址，比如&lt;code&gt;sitemap.xml&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/sitemap/4.webp&quot; alt=&quot;添加站点地图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;提交成功后，Google会在“已提交的站点地图”中显示该sitemap。
&lt;img src=&quot;/images/sitemap/5.webp&quot; alt=&quot;已提交的站点地图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;索引需要一段时间编制并生效。通常情况下，48小时内Google即可完成收录。&lt;/p&gt;
</content:encoded></item><item><title>在Hexo中启用新Markdown解释器并配置Katex</title><link>https://arkmostima.pages.dev/posts/%E5%9C%A8hexo%E4%B8%AD%E5%90%AF%E7%94%A8%E6%96%B0markdown%E8%A7%A3%E9%87%8A%E5%99%A8%E5%B9%B6%E9%85%8D%E7%BD%AEkatex/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E5%9C%A8hexo%E4%B8%AD%E5%90%AF%E7%94%A8%E6%96%B0markdown%E8%A7%A3%E9%87%8A%E5%99%A8%E5%B9%B6%E9%85%8D%E7%BD%AEkatex/</guid><description>在 Hexo 中，如果默认的 Markdown 编译器无法正确渲染 LaTeX 公式，可以考虑使用支持 LaTeX 公式的 Markdown 编译器插件。常用的包括： hexo-renderer-pandoc hexo-renderer…</description><pubDate>Thu, 11 Jul 2024 15:12:54 GMT</pubDate><content:encoded>&lt;h2&gt;Markdown编译器&lt;/h2&gt;
&lt;p&gt;在 Hexo 中，如果默认的 Markdown 编译器无法正确渲染 LaTeX 公式，可以考虑使用支持 LaTeX 公式的 Markdown 编译器插件。常用的包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;hexo-renderer-pandoc&lt;/li&gt;
&lt;li&gt;hexo-renderer-markdown-it&lt;/li&gt;
&lt;li&gt;hexo-filter-mathjax&lt;/li&gt;
&lt;li&gt;hexo-renderer-markdown-it-plus&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;推荐使用&lt;a href=&quot;https://github.com/CHENXCHEN/hexo-renderer-markdown-it-plus&quot;&gt;hexo-renderer-markdown-it-plus&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;hexo-renderer-markdown-it-plus 安装与配置&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;卸载旧的渲染器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;npm uninstall hexo-renderer-marked --save
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;安装&lt;code&gt;hexo-renderer-markdown-it-plus&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在Hexo根目录中打开Git Bash，然后：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install hexo-renderer-markdown-it-plus --save
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;配置&lt;code&gt;_config.yml&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;添加以下配置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;markdown:
  render:
    engine: markdown-it-plus
    options:
      html: true
      xhtmlOut: true
      breaks: true
      linkify: true
      typographer: true
      quotes: &apos;“”‘’&apos;
      plugins:
        - plugin:
            name: markdown-it-katex
            options:
              throwOnError: false
              errorColor: &apos;#cc0000&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;引入KaTeX的CSS&lt;/h2&gt;
&lt;p&gt;为了避免公式错位问题，需要确保正确引入 KaTeX 的 CSS 文件。在主题布局文件（通常是 themes/your-theme/layout/_partial/head.ejs）中添加以下代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;link rel=&quot;stylesheet&quot; href=&quot;https://cdn.jsdelivr.net/npm/katex@0.13.11/dist/katex.min.css&quot;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>【电机瞬态分析】交流电机方程式（相坐标）</title><link>https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E6%96%B9%E7%A8%8B%E5%BC%8F%E7%9B%B8%E5%9D%90%E6%A0%87/</link><guid isPermaLink="true">https://arkmostima.pages.dev/posts/%E7%94%B5%E6%9C%BA%E7%9E%AC%E6%80%81%E5%88%86%E6%9E%90%E4%BA%A4%E6%B5%81%E7%94%B5%E6%9C%BA%E6%96%B9%E7%A8%8B%E5%BC%8F%E7%9B%B8%E5%9D%90%E6%A0%87/</guid><description>根据惯例，认为一个线圈轴线的正方向极为该线圈产生磁场轴线的正方向。若按照右手螺旋定律来确定电流和电压的正方向，即认为二者符合电动机惯例，则该回路磁链方程式可列写为：</description><pubDate>Thu, 11 Jul 2024 10:27:35 GMT</pubDate><content:encoded>&lt;h2&gt;凸级同步电机在相坐标系统中的电压方程式&lt;/h2&gt;
&lt;h3&gt;正方向规定中的电动机惯例与发电机惯例&lt;/h3&gt;
&lt;h4&gt;电动机惯例&lt;/h4&gt;
&lt;p&gt;磁链定义式为：
$$
\lambda = N \phi
$$
其中$N$为线圈匝数。&lt;/p&gt;
&lt;p&gt;根据惯例，认为一个线圈轴线的正方向极为该线圈产生磁场轴线的正方向。若按照右手螺旋定律来确定电流和电压的正方向，即认为二者符合电动机惯例，则该回路磁链方程式可列写为：
$$
\psi_k = N \phi = L_kI_k
$$
其中$L$为线圈电感，$I$为线圈中的电流。&lt;/p&gt;
&lt;p&gt;根据电磁感应定理和楞次定理，当磁链$\psi$正向增长时，感生电动势必定是阻止磁链增长的，这也就意味着在等效电路中，电压源电位升高的方向与电流正方向相反。等效回路如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/Fig3.webp&quot; alt=&quot;图1 电动机惯例下回路压、流正方向&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据基尔霍夫第二定律，此时等效回路的电压方程式为：
$$
u_k = p \psi + i_k r_k
$$
其中,$p$为微分算子，即$\frac{d}{dt}$。$\psi$为主磁路磁链。&lt;/p&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;在电动机运行时，电流从定子绕组流入，电枢电流方向与定子磁场方向相同或相近，电动机产生电磁转矩，带动机械负载运行。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;发电机惯例&lt;/h4&gt;
&lt;p&gt;如果保持电流的正方向规定规则不变，仍将产生正向磁链的电流方向规定为电流正方向，仅改变电压正方向的规定，令二者符合发电机惯例，则回路电压正方向与电流正方向的关系改为下图所示：&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/Fig1.webp&quot; alt=&quot;图2 发电机惯例下回路压、流正方向&quot; /&gt;&lt;/p&gt;
&lt;p&gt;根据基尔霍夫第二定律，回路电压方程式为：
$$
u_k = -p \psi - i_k r_k
$$&lt;/p&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;在发电机运行时，外部机械力驱动转子旋转，定子绕组中的电流流出，定子绕组中的电流方向与定子磁场方向相反或相对，发电机将机械能转换为电能输出到电网或负载。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;凸级同步电机的电压方程式&lt;/h3&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;定子三相绕组对称，转子结构分别对直轴和交轴对称。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;由于假定磁路为线性，可应用叠加原理，即某绕组交链的总磁链等于各绕组电流分别产生并与之交链的磁链的代数和。实际电机饱和的影响，可根据电机的运行条件，近似地采用选择适当参数值的方法加以修正。&lt;/p&gt;
&lt;p&gt;开始分析前应当规定各变量的正方向，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/Fig2.webp&quot; alt=&quot;图3 凸级同步电机内部变量正方向&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然图中a、b、c轴线分别表示定子三相绕组实际轴线的对应正方向，即各相绕组磁链正方向。以及为基础，规定产生正向磁链的电流方向为电流正方向。电压正方向与电流正方向的关系按照电动机惯例确定。&lt;/p&gt;
&lt;h4&gt;定子绕组&lt;/h4&gt;
&lt;p&gt;按照电动机惯例，根据基尔霍夫第二定律，定子三相绕组的电压方程式可分别根据定子绕组回路列写为：
$$
\left.\begin{array}{l}
u_{\mathrm{a}}=p \psi_{\mathrm{a}}+r_{s} i_{\mathrm{a}} \
u_{\mathrm{b}}=p \psi_{\mathrm{b}}+r_{\mathrm{s}} i_{\mathrm{b}} \
u_{\mathrm{c}}=p \psi_{\mathrm{c}}+r_{\mathrm{s}} i_{\mathrm{c}}
\end{array}\right}
$$&lt;/p&gt;
&lt;h4&gt;励磁绕组&lt;/h4&gt;
&lt;p&gt;根据电动机惯例，励磁绕组电压方程为：
$$
u_f = -p \psi_f - i_f r_f
$$&lt;/p&gt;
&lt;h4&gt;阻尼绕组&lt;/h4&gt;
&lt;p&gt;阻尼绕组广泛应用于电力系统中需要高稳定性的同步电机，如发电机和大型工业电动机。其主要目的是保证电机在启动、停止以及负载变化时的平稳运行，防止系统失步和振荡。&lt;/p&gt;
&lt;p&gt;阻尼绕组一般由安装在磁极极靴上的阻尼条和阻尼环组成，阻尼条由阻尼环部分或全部短接，其分布对于直轴或交轴而言通常都是对称的。其结构、安装位置和简单示意图如下所示。&lt;/p&gt;
&lt;p&gt;&amp;lt;p align=center&amp;gt;
&amp;lt;img src=/images/1.1/zuniraozu.webp width=50% title=阻尼绕组安装位置&amp;gt;
&amp;lt;/p&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/zuniraozu2.webp&quot; alt=&quot;阻尼绕组简单示意图&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见阻尼条构成的回路是网状的，为了方便起见，通常电流回路的选取均对称于直轴或交轴。上图表示每个磁极有四根阻尼条的阻尼绕组，其中回路1d、2d对称于d轴，回路1q、2q对称于q轴，阻尼条中流过的实际电流即为相应的d轴和q轴回路电流的代数和。&lt;/p&gt;
&lt;p&gt;当只考虑气隙磁场（定、转子合成磁场）基波时，若基于以上规则划分阻尼绕组电流回路，则d轴回路和q轴回路在空间中恰好相差90°的电角度，而转子结构显然相对于d轴或q轴又是对称的，因此在d轴回路和q轴回路之间不产生互感，进而可把实际的阻尼绕组看成是两组假想的阻尼绕组，即直轴阻尼绕组和交轴阻尼绕组。&lt;/p&gt;
&lt;p&gt;若按照实际情况单独计算每一根阻尼条的电流和磁链关系，可进一步给出每一根阻尼条的实际电流，但是计算量非常大。在不需研究阻尼绕组内部情况的条件下，可将实际的直轴及轴的多个回路的阻尼绕组分别用一个直轴及交轴的等效阻尼统组D和Q来代替,从而大为简化分析计算工作。这样虽会给计算带来一些误差，但在工程计算中是允许的，也是常用的一种方法。简化的直轴及交轴等效阻尼绕组的电压方程式为：&lt;/p&gt;
&lt;p&gt;$$
\left.\begin{array}{l}
0 = p \psi_D + r_D i_D \
0 = p \psi_Q + r_Q i_Q
\end{array}\right}
$$&lt;/p&gt;
&lt;h4&gt;联立&lt;/h4&gt;
&lt;p&gt;联立定子绕组、励磁绕组和阻尼绕组的电压方程式，用矩阵形式表示为：
$$
U = p \Psi + R I
$$
式中
$$
U = [u_a \quad u_b \quad u_c  \quad u_f \quad 0 \quad 0]^{T}
$$&lt;/p&gt;
&lt;p&gt;$$
I = [i_a \quad i_b \quad i_c  \quad i_f \quad i_D \quad i_Q]^{T}
$$&lt;/p&gt;
&lt;p&gt;$$
\Psi = [\psi_a \quad \psi_b \quad \psi_c  \quad \psi_f \quad \psi_D \quad \psi_Q]^{T}
$$&lt;/p&gt;
&lt;p&gt;$$
R =
\begin{bmatrix}
r_a &amp;amp; &amp;amp; &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; r_b &amp;amp; &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; r_c &amp;amp; &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; r_f &amp;amp; &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; &amp;amp; r_D &amp;amp; \
&amp;amp; &amp;amp; &amp;amp; &amp;amp; &amp;amp; r_Q \
\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;由上式，每个电压方程式均由两项构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与绕组交链的主磁链随时间变化而产生的感应电动势$p\psi$&lt;/li&gt;
&lt;li&gt;电流通过绕组电阻产生的压降$ri$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在上述的分析中，无论是定子绕组、励磁绕组还是阻尼绕组，不管其本身是旋转还是停止，其绕组的轴线与构成该绕组的线圈元件之间永远保持相对静止。这类绕组也被称为“静止绕组” 。&lt;/p&gt;
&lt;h2&gt;凸级同步电机在相坐标系统中的磁链方程式&lt;/h2&gt;
&lt;p&gt;上文导出的电压方程式中，与每个绕组交链的主磁链是以下两个部分的代数和：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;绕组自身通过变化电流产生的磁通（自感）&lt;/li&gt;
&lt;li&gt;与该绕组相交链的互感磁链（互感）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这些磁链与电流及电感系数的关系用矩阵形式表示为：
$$\begin{bmatrix}\psi_\mathrm{a}\\psi_\mathrm{b}\\psi_\mathrm{c}\\psi_\mathrm{f}\\psi_\mathrm{D}\\psi_\mathrm{Q}\end{bmatrix}=\begin{bmatrix}L_\mathrm{aa}&amp;amp;M_\mathrm{ab}&amp;amp;M_\mathrm{ac}&amp;amp;M_\mathrm{af}&amp;amp;M_\mathrm{aD}&amp;amp;M_\mathrm{aQ}\M_\mathrm{ba}&amp;amp;L_\mathrm{bb}&amp;amp;M_\mathrm{bc}&amp;amp;M_\mathrm{bf}&amp;amp;M_\mathrm{bD}&amp;amp;M_\mathrm{bQ}\M_\mathrm{ca}&amp;amp;M_\mathrm{cb}&amp;amp;L_\mathrm{cc}&amp;amp;M_d&amp;amp;M_\mathrm{cD}&amp;amp;M_\mathrm{cQ}\M_\mathrm{fa}&amp;amp;M_\mathrm{fb}&amp;amp;M_\mathrm{fc}&amp;amp;L_\mathrm{f}&amp;amp;M_\mathrm{fD}&amp;amp;0\M_\mathrm{Da}&amp;amp;M_\mathrm{Db}&amp;amp;M_\mathrm{Dc}&amp;amp;M_\mathrm{Df}&amp;amp;L_\mathrm{D}&amp;amp;0\M_\mathrm{Qa}&amp;amp;M_\mathrm{Qb}&amp;amp;M_\mathrm{Qc}&amp;amp;0&amp;amp;0&amp;amp;L_\mathrm{Q}\end{bmatrix}\begin{bmatrix}i_\mathrm{a}\i_\mathrm{b}\i_\mathrm{c}\i_\mathrm{f}\i_\mathrm{D}\i_\mathrm{Q}\end{bmatrix}$$&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：矩阵中的公式仍可通过$\lambda = LI$导出，这里的$L$既可以是自感系数也可以是互感系数，其表示意义即为自感磁链或互感磁链。该矩阵中的互感系数也是可逆的。&lt;/p&gt;
&lt;p&gt;上式可改写为：
$$
\begin{bmatrix}\Psi_{abc}\\Psi_{fDQ}\end{bmatrix}=\begin{bmatrix}L_{ss}&amp;amp;L_{sr}\L_{rs} &amp;amp; L_{rr}\end{bmatrix}\begin{bmatrix}I_{abc}\I_{fDQ}\end{bmatrix}
$$&lt;/p&gt;
&lt;p&gt;其中，$L_{ss}、L_{sr}$为定、转子自感系数矩阵，$L_{sr}、L_{rs}$为定、转子互感系数矩阵。&lt;/p&gt;
&lt;p&gt;显然，由于交轴等效阻尼绕组Q与直轴等效阻尼绕组D，还有励磁绕组$f$的轴线在空间中相差90°电角度，因此二者互感系数为0。这就是为什么矩阵中$M_{fQ}、M_{DQ}、M_{Qf}、M_{QD}$四者均等于0。&lt;/p&gt;
&lt;h2&gt;凸级同步电机的自感系数与互感系数&lt;/h2&gt;
&lt;h3&gt;磁动势、磁导、自感系数、互感系数&lt;/h3&gt;
&lt;p&gt;回顾一下磁导的概念：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;磁导（magnetic conductance,$\mu$）是衡量磁性材料对磁场线的导通能力的物理量，它类似于电导在电路中的作用。磁导越大，表示材料对磁场线的导通能力越强。具体来说，磁导是磁通量与磁动势的比值，反映了材料在磁场中的导磁性能。磁导越大，说明该部分对磁通量的阻碍越小，即导磁能力越强，磁阻（magnetic reluctance）越小。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;磁路中的磁导定义为：$\Lambda = \frac{\Phi}{F}$，其中$\Lambda$为磁导，单位安培每韦伯（$A/Wb$），$\Phi$为磁通量，单位韦伯（$Wb$），$F$为磁动势，单位安培($A$)。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;已经自感系数定义为：
$$
L = \frac{N\Phi}{I}
$$
磁导定义为：
$$
\Lambda = \frac{\Phi}{F} = \frac{1}{R_m}
$$&lt;/p&gt;
&lt;p&gt;根据磁路欧姆定律：
$$
F = \Phi R_m = NI
$$
式中$F$为作用于该磁路上的磁动势，$\Phi$为磁路内磁通量，$R_m$为磁阻。&lt;/p&gt;
&lt;p&gt;联立以上式子，可导出：
$$
L = N^2 \Lambda
$$&lt;/p&gt;
&lt;p&gt;该方法也可被推广至分析两个线圈间互感系数的关系。&lt;/p&gt;
&lt;p&gt;由以上推导可知：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;线圈自感系数为线圈内通过单位电流所产生的自感磁链，其大小与该线圈匝数的平方（$N^2$）及自感磁通所经磁路磁导($\mu$)的乘积成正比&lt;/li&gt;
&lt;li&gt;线圈1和线圈2间的互感系数为线圈1内通过单位电流所产生的与线圈2相交链的互感磁链，其大小与两线圈匝数的乘积（$N_1N_2$）成正比，并与互感磁通所经磁路的磁导($\mu$)成正比&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;凸级同步电机在转子旋转时，显然部分绕组之间的相对空间位置以及所对应磁路的磁导随转子位置变化而变化，因此相应的自感系数$L$或互感系数$M$将是转子位置角$\theta$的函数，即时变函数。&lt;/p&gt;
&lt;h3&gt;定子绕组自感系数&lt;/h3&gt;
&lt;p&gt;首先分析定子a相绕组的自感系数$L_{aa}$。当绕组匝数确定时，$L_{aa}$的大小主要取决于对应磁路磁导的大小。当a相绕组中轴线与磁极直轴（d轴）轴线重合，即$\theta=0$时，a相绕组磁场对应磁路的磁导$\Lambda$最大，因而在此位置自感系数达到最大值$L_{aad}$；当转子旋转至$\theta = \pi /2$电弧度时，a相绕组轴线与转子q轴重合，对应磁路的磁导最小，自感系数为最小值$L_{aaq}$。当转子旋转至$\theta = \pi$电弧度时，对应磁路的磁导$\Lambda$和自感系数$L$又达到最大值。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/azigan.webp&quot; alt=&quot;a相绕组自感系数&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然a相绕组自感系数随$\theta$的角的变化可以用余弦级数表示为：
$$
L_{aa}=L_{aa0}+L_{aa2}cos{2\theta}+L_{aa4}cos{4\theta}+...
$$&lt;/p&gt;
&lt;p&gt;可见用该种方式计算绕组自感系数非常复杂。就工程设计而言，一般只需考虑气隙的基波磁动势和气隙的基波磁场。&lt;/p&gt;
&lt;p&gt;仅考虑隙的基波磁动势和气隙的基波磁场时，a相绕组自感系数$L_{aa}$由以下二者构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不通过气隙的漏磁场所对应的漏电感$L_{aal}$&lt;/li&gt;
&lt;li&gt;通过气隙的主合成磁场对应的主电感$L_{aam}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;漏电感$L_{aal}$一般仅和漏磁场的分布情况以及磁路性质有关，与转子空间位置无关，为一常数。主电感$L_{aam}$是关于转子位置角$\theta$的函数。&lt;/p&gt;
&lt;p&gt;令a相绕组有效匝数为$W$，由磁动势定义式，绕组线圈通过单位电流所产生的基波磁动势为$F_{am}=W$，轴线为a轴。将该磁动势分解至磁导$\Lambda$为恒值的d轴和q轴上，即$F_{amd}=Wcos\theta$, $F_{amq}=-Wsin\theta$。令d轴和q轴的磁导分别为$\Lambda_d$和$\Lambda_q$，则a相磁动势在d轴和q轴产生的基波磁通大小分别为：
$$
\phi_d=F_{amd}\Lambda_d=W\Lambda_d cos\theta\
\phi_q=F_{amq}\Lambda_q=-W\Lambda_q sin\theta
$$&lt;/p&gt;
&lt;p&gt;二者与a相绕组交链的自感磁链等于相绕组的有效匝数$W$与d轴、q轴磁通在a轴上的分量乘积的代数和：
$$
\Psi_{aam}=W(\phi_d cos\theta-\phi_q sin\theta)=W^2\Lambda_d cos^2{\theta}+W^2\Lambda_q sin^2{\theta}
$$&lt;/p&gt;
&lt;p&gt;a相绕组通过单位电流时产生的主电感的表达式为：
$$
L_{aam}=L_{aad}cos^2{\theta}+L_{aaq}sin^2{\theta}=\frac{1}{2}(L_{aad}+L_{aaq})+\frac{1}{2}(L_{aad}-L_{aaq})cos{2\theta}
$$&lt;/p&gt;
&lt;p&gt;a相绕组的自感系数$L_{aa}$为主电感$L_{aam}$和漏电感$L_{aal}$的代数和，即
$$
L_{aa} = L_{aam} + L_{aal} = L_{aa0}+L_{aa2}cos{2\theta}
$$
同理
$$
L_{bb} = L_{aa0}+L_{aa2}cos{2(\theta-\frac{2\pi}{3})}\
L_{cc} = L_{aa0}+L_{aa2}cos{2(\theta+\frac{2\pi}{3})}
$$
式中
$$
L_{aa0} = L_{aal}+\frac{1}{2}(L_{aad}+L_{aaq})\
L_{aa2} = \frac{1}{2}(L_{aad}-L_{aaq})
$$&lt;/p&gt;
&lt;p&gt;显然，当只考虑空间基波磁场时，凸级同步电机三相定子绕组二点自感系数只有常数项和转子位置角$\theta$的二次项，其他高次谐波项不存在，给计算带来很大的方便。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;注意&lt;/em&gt;：隐级同步电机和感应电机由于具有均匀分布的气隙，故各项绕组的自感系数与转子位置角$\theta$无关，为一常数。&lt;/p&gt;
&lt;h3&gt;定子两相绕组间的互感系数&lt;/h3&gt;
&lt;p&gt;定子三相绕组结构相同，但绕组轴线在空间上互差$2\pi/3$电弧度。a相绕组通过单位电流所产生的与b相绕组相交链的互感磁链，此为a、b相的互感系数$M_{ab}$。&lt;/p&gt;
&lt;p&gt;互感系数也由两部分构成：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;与不通过气隙的漏磁通对应的互漏感系数$M_{abl}$&lt;/li&gt;
&lt;li&gt;与通过气隙的主磁通对应的互感系数$M_{abm}$&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;与自感系数类似，互漏感系数$M_{abl}$为一常值，互感系数$M_{abm}$随转子位置变化而变化。&lt;/p&gt;
&lt;p&gt;上文已经求出仅考虑空间基波磁场分布下a相绕组单位电流产生的直轴磁通$\phi_d$和交轴磁通$\phi_q$。它们与b相绕组相交链的互感磁链，等于相绕组有效匝数$W$与它们在b轴上的磁通量分量乘积的代数和：
$$
\begin{aligned}
\psi_{\mathrm{abm}} &amp;amp; =W\left[\phi_{d} \cos \left(\theta-\frac{2 \pi}{3}\right)\right]-W\left[\phi_{q} \sin \left(\theta-\frac{2 \pi}{3}\right)\right] \
&amp;amp; =W^{2} \Lambda_{d} \cos \theta \cos \left(\theta-\frac{2 \pi}{3}\right)+W^{2} \Lambda_{q} \sin \theta \sin \left(\theta-\frac{2 \pi}{3}\right)
\end{aligned}
$$&lt;/p&gt;
&lt;p&gt;所以a相绕组通过单位电路时产生的通过气隙的磁通与b轴绕组相交链的互感磁链，即a、b相间的互感系数为：
$$
M_{abm}=L_{aad}cos\theta cos(\theta-\frac{2\pi}{3})+L_{aaq}sin\theta sin(\theta-\frac{2\pi}{3})\
=-\frac{L_{aad}+L_{aaq}}{4}+\frac{L_{aad}-L_{aaq}}{2}cos(2\theta-\frac{2\pi}{3})
$$&lt;/p&gt;
&lt;p&gt;a相和b相间的互感系数$M_{ab}$应为互漏电感系数$-M_{abl}$和互电感系数$M_{abm}$之和，即
$$
M_{ab}=-M_{abl}-\frac{L_{aad}+L_{aaq}}{4}+\frac{L_{aad}-L_{aaq}}{2}cos(2\theta-\frac{2\pi}{3})\
= - M_{ab0}+L_{aa2}cos{2(\theta + \frac{2\pi}{3})}
$$&lt;/p&gt;
&lt;p&gt;同理&lt;/p&gt;
&lt;p&gt;$$
M_{bc}=- M_{ab0}+L_{aa2}cos{2\theta}\
M_{ca}=- M_{ab0}+L_{aa2}cos{2(\theta-\frac{2\pi}{3})}
$$&lt;/p&gt;
&lt;p&gt;式中$M_{ab0} = M_{abl}+\frac{L_{aad}+L_{aaq}}{4}$&lt;/p&gt;
&lt;p&gt;由于三相绕组轴线间夹角互为120°电角度，其值大于90°，因此其互感系数均为负值。定子绕组b相和c相间的互感系数$M_{bc}$随转子位置的变化曲线如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/1.1/bchugan.webp&quot; alt=&quot;b、c相绕组间的互感系数&quot; /&gt;&lt;/p&gt;
&lt;p&gt;可见，$\theta = 0$时，b相和c相的互感系数的绝对值最小；$\theta = \pi /2$时，互感系数绝对值最大，这与自感系数的变化规律是相反的。&lt;/p&gt;
&lt;h3&gt;定子绕组和转子绕组间的互感系数&lt;/h3&gt;
&lt;h4&gt;定子绕组和励磁绕组间的互感系数&lt;/h4&gt;
&lt;p&gt;励磁绕组通过单位电流时所产生的与定子a相绕组交链的互感磁链即为定子绕组与励磁绕组间的互感系数$M_{af}$。互感磁链遵循以下规律：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当励磁绕组d轴与a相绕组轴线重合时，互感磁链为最大；&lt;/li&gt;
&lt;li&gt;转子d轴旋转90°电角度后，两轴线正交，互感磁链为0；&lt;/li&gt;
&lt;li&gt;当转子d轴旋转180°电角度时，互感磁链为负的最大值；&lt;/li&gt;
&lt;li&gt;当转子d轴旋转270°电角度时，两轴线正交，互感磁链为0；&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因此，一般情况下有：
$$
M_{af} = M_{fa} = M_{af0}cos\theta + M_{af3}cos{3\theta}+...
$$&lt;/p&gt;
&lt;p&gt;同理可推广至b相和c相：
$$
M_{bf} = M_{fb} = M_{af0}cos(\theta-\frac{2\pi}{3}) + M_{af3}cos(\theta-\frac{2\pi}{3})+...\
M_{cf} = M_{fc} = M_{af0}cos(\theta+\frac{2\pi}{3}) + M_{af3}cos(\theta+\frac{2\pi}{3})+...
$$&lt;/p&gt;
&lt;p&gt;仅考虑基波磁通时，显然互感系数只有基波分量，即：
$$
M_{af} = M_{fa} = M_{af0}cos\theta \
M_{bf} = M_{fb} = M_{af0}cos(\theta-\frac{2\pi}{3})\
M_{cf} = M_{fc} = M_{af0}cos(\theta+\frac{2\pi}{3})
$$
式中$M_{af0}$为a轴与励磁绕组d轴重合时所具有的最大互感系数。下标中的“0”表示该互感系数计算时仅考虑了基波分量。&lt;/p&gt;
&lt;p&gt;$M_{af}$随$\theta$的关系如下图所示。
&lt;img src=&quot;/images/1.1/maf.webp&quot; alt=&quot;定子a相绕组与励磁绕组间的互感系数&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;定子绕组与直轴阻尼绕组间的互感系数&lt;/h4&gt;
&lt;p&gt;直轴阻尼绕组与励磁绕组轴线重合，故定子绕组与直轴阻尼绕组间的互感系数变化规律可由定子绕组与励磁绕组的互感系数变化规律推广得来（仅考虑基波分量）：
$$
M_{aD} = M_{Da} = M_{aD0}cos\theta \
M_{bD} = M_{Db} = M_{aD0}cos(\theta-\frac{2\pi}{3})\
M_{cD} = M_{Dc} = M_{aD0}cos(\theta+\frac{2\pi}{3})
$$
式中$M_{aD0}$为定子绕组a轴与直轴阻尼绕组D轴重合时所具有的最大互感系数。&lt;/p&gt;
&lt;h4&gt;定子绕组与交轴阻尼绕组间的互感系数&lt;/h4&gt;
&lt;p&gt;显然交轴提前直轴90°电角度，以$\theta + \frac{\pi}{2}$代换上小节公式中的$\theta$角，并考虑直轴和交轴的磁导不同，推导得（仅考虑基波分量）：
$$
M_{aQ} = M_{Qa} = M_{aQ0}cos\theta \
M_{bQ} = M_{Qb} = M_{aQ0}cos(\theta-\frac{2\pi}{3})\
M_{cQ} = M_{Qc} = M_{aQ0}cos(\theta+\frac{2\pi}{3})
$$
式中$M_{aQ0}$为定子绕组a轴与交轴阻尼绕组Q轴重合时所具有的最大互感系数。&lt;/p&gt;
&lt;h4&gt;转子绕组的自感系数&lt;/h4&gt;
&lt;p&gt;转子绕组通过单位电流时产生的自感磁链即为转子绕组的自感系数。由于定子铁心为圆环形，转子上各绕组产生的自感磁链大小与转子位置无关，均为恒值。&lt;/p&gt;
&lt;p&gt;励磁绕组自感系数由两部分组成，即
$$
L_f = L_{fl} + L_{fm}
$$
式中 $L_{fl}$对应励磁绕组漏磁通的漏电感系数，$L_{fm}$对应励磁绕组气隙磁通的主电感系数。&lt;/p&gt;
&lt;p&gt;直轴阻尼绕组和交轴阻尼绕组的自感系数分别为：&lt;/p&gt;
&lt;p&gt;$$
L_D = L_{Dl} + L_{Dm} \&lt;/p&gt;
&lt;p&gt;L_Q = L_{Ql} + L_{Qm}
$$&lt;/p&gt;
&lt;p&gt;式中 $L_{Dl}$和$L_{Ql}$对应于D、Q绕组漏磁通的漏电感系数，$L_{Dm}$和$L_{Qm}$对应于D、Q绕组气隙磁通的主电感系数。&lt;/p&gt;
&lt;h4&gt;转子绕组间的互感系数&lt;/h4&gt;
&lt;p&gt;因转子分别对直轴和交轴对称，且两轴正交，故交轴阻尼绕组Q与直轴阻尼绕组D、励磁绕组$f$间的互感系数为0.绕组D与绕组$f$同在直轴上且式中相对静止，有
$$
M_{fD}=M_{Df}=M_{fDl}+M_{fDm}
$$&lt;/p&gt;
&lt;h3&gt;小结&lt;/h3&gt;
&lt;p&gt;以上讨论的凸极同步电机电感系数，从变化规律上看，可分为三种类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;定子绕组的自感系数和互感系数。其变化周期为$\pi$。引起这类电感系数变化的原因是转子直轴和交轴的磁导不同,当电机转子为圆柱形时,如隐极同步电机及感应电机，转子直轴和交轴的磁导相同，则电感系数$L_{aad}=L_{aaq}$，定子绕组的自感系数和互感系数均为常数，与转子位置角$\theta$无关。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;定子与转子绕组间的互感系数。其变化周期为2$\pi$。引起这类电感系数变化的原因是定子绕组轴线与转子绕组轴线间有相对运动，导致互感磁链周期性变化。因此，即使在隐极同步电机和感应电机中，这类互感系数仍是转子位置角$\theta$的函数。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;转子绕组的自感系数与互感系数。由于定子铁心为圆环形，对转子直轴或交轴而言，磁导是不变的，且转子绕组轴线间无相对运动。上述引起电感系数变化的两个原因均不存在，故转子绕组的自感系数与互感系数均为常数。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item></channel></rss>