<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>云朵实验室</title><description>CloudSir的个人博客</description><link>https://blog.cloudsir.top/</link><language>zh_CN</language><item><title>一文带你搞懂直流电机驱动（新版）</title><link>https://blog.cloudsir.top/posts/how-to-use-motor-v2/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/how-to-use-motor-v2/</guid><description>用 5 个互动实验拆解直流电机的转向、H 桥、PWM 和 L298N，并给出 Arduino 与 STM32 完整代码。</description><pubDate>Fri, 28 Aug 2026 00:27:00 GMT</pubDate><content:encoded>&lt;p&gt;import MotorVoltageLab from &apos;./MotorVoltageLab.tsx&apos;;
import HBridgeLab from &apos;./HBridgeLab.tsx&apos;;
import DriverTruthTableLab from &apos;./DriverTruthTableLab.tsx&apos;;
import PwmControlLab from &apos;./PwmControlLab.tsx&apos;;
import L298WiringLab from &apos;./L298WiringLab.tsx&apos;;
import &apos;../../../components/interactive-lab/interactive-lab.css&apos;;
import &apos;./motor-lab.css&apos;;&lt;/p&gt;
&lt;p&gt;:::tip
旧版文章也发布在 &lt;a href=&quot;https://blog.csdn.net/qq_34802028/article/details/122431195&quot;&gt;CSDN&lt;/a&gt; 和本站的&lt;a href=&quot;/posts/how-to-use-motor/&quot;&gt;《一文带你搞懂电机驱动模块》&lt;/a&gt;。本版修正代码，并增加了互动实验。
:::&lt;/p&gt;
&lt;p&gt;单片机 GPIO 只能输出控制电平，无法承担电机电流。电机启动、堵转和换向时会吸取大电流；线圈断电时还会产生反向电动势。&lt;/p&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;承受电压与电流，把逻辑电平转换成功率输出。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对象是&lt;strong&gt;有刷直流电机&lt;/strong&gt;。调节 5 个实验，把一个 &lt;code&gt;-100% ~ +100%&lt;/code&gt; 的速度命令依次对应到 H 桥电流、PWM 波形、L298N 接线和单片机代码。&lt;/p&gt;
&lt;h2&gt;电压决定方向，也影响转速&lt;/h2&gt;
&lt;p&gt;直流电机通电后开始旋转。对调两根电机线，电流方向和转向一起反转。&lt;/p&gt;
&lt;p&gt;保持电源和负载不变，提高电机端的有效电压，稳定转速会随之升高。同一电压下，负载增加会拉低转速并抬高电流。&lt;/p&gt;
&lt;p&gt;&amp;lt;MotorVoltageLab client:visible /&amp;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;/li&gt;
&lt;/ol&gt;
&lt;p&gt;手动对调电机线可以换向。H 桥让单片机用电平信号完成同样的操作。&lt;/p&gt;
&lt;h2&gt;H 桥：用四个开关改变电流方向&lt;/h2&gt;
&lt;p&gt;四个开关分列电机两侧，电机位于中间，电路形状像字母 H，因此得名 &lt;strong&gt;H 桥（H-Bridge）&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;一组对角开关控制正转，另一组控制反转。停止方式分为两种：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自由滑行（coast）&lt;/strong&gt;：断开电机，让转子靠惯性停下；&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速制动（brake）&lt;/strong&gt;：把电机两端接到相同电位，让旋转中的电机产生制动力。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;HBridgeLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;实验用机械开关表示电流路径。驱动芯片使用双极型晶体管或 MOSFET，并配置栅极或基极驱动、互锁、死区、过温保护和续流路径。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一桥臂的上管和下管不能同时导通。&lt;/strong&gt; 电流一旦绕过电机从电源流向地，就会发生桥臂直通。芯片保护只能降低损坏概率，控制逻辑仍需排除这个状态。&lt;/p&gt;
&lt;h2&gt;从 H 桥到电机驱动模块&lt;/h2&gt;
&lt;p&gt;驱动芯片接收 &lt;code&gt;IN1&lt;/code&gt;、&lt;code&gt;IN2&lt;/code&gt;、&lt;code&gt;ENA&lt;/code&gt; 等逻辑信号，再控制内部的四个功率开关。&lt;/p&gt;
&lt;p&gt;以 L298 的 A 路 H 桥为例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;IN1&lt;/code&gt;、&lt;code&gt;IN2&lt;/code&gt; 决定两个输出端的电位；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ENA&lt;/code&gt; 为高时，输入命令生效；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ENA&lt;/code&gt; 为低时，整座桥被关闭，电机自由滑行。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;DriverTruthTableLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.st.com/resource/en/datasheet/l298.pdf&quot;&gt;ST L298 数据手册&lt;/a&gt; 给出了双向直流电机真值表：&lt;code&gt;ENA=1&lt;/code&gt; 时，两个输入不同对应正转或反转，两个输入相同对应快速制动；&lt;code&gt;ENA=0&lt;/code&gt; 对应自由滑行。&lt;/p&gt;
&lt;p&gt;这张表&lt;strong&gt;只适用于 L298&lt;/strong&gt;。其他驱动器可能使用 &lt;code&gt;AIN1/AIN2&lt;/code&gt;、&lt;code&gt;PH/EN&lt;/code&gt; 或 &lt;code&gt;STBY&lt;/code&gt;，制动和休眠状态也有差异。换芯片时请按新器件的真值表改接线和代码。&lt;/p&gt;
&lt;h2&gt;PWM：用开关时间调节出力&lt;/h2&gt;
&lt;p&gt;定时器让 GPIO 在高、低电平之间快速切换。37% 占空比表示每个周期有 37% 的时间导通：&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;p&gt;电机线圈和转子的机械惯性会平滑这些开关脉冲。占空比升高后，电机获得的有效驱动随之增加。&lt;/p&gt;
&lt;p&gt;&amp;lt;PwmControlLab client:visible /&amp;gt;&lt;/p&gt;
&lt;h3&gt;推荐的三线控制&lt;/h3&gt;
&lt;p&gt;后文代码采用三线控制：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ENA&lt;/code&gt; 接定时器 PWM；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IN1&lt;/code&gt;、&lt;code&gt;IN2&lt;/code&gt; 接普通 GPIO；&lt;/li&gt;
&lt;li&gt;正负号决定 &lt;code&gt;IN1/IN2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;绝对值决定 &lt;code&gt;ENA&lt;/code&gt; 占空比。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方向 GPIO 和 PWM 各管一件事。反转时无需换算 PWM 比较值，方向与占空比可以分别核对。&lt;/p&gt;
&lt;h3&gt;两线 PWM + DIR 的占空比换算&lt;/h3&gt;
&lt;p&gt;如果为了节省一个控制引脚，把 &lt;code&gt;IN1&lt;/code&gt; 接 PWM、&lt;code&gt;IN2&lt;/code&gt; 接 DIR：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DIR=0&lt;/code&gt; 时，PWM 高电平区间产生正向电压；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DIR=1&lt;/code&gt; 时，PWM &lt;strong&gt;低电平区间&lt;/strong&gt;才产生反向电压。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同样输出 70% 时，正转写入 70% 比较值，反转要写入 30%。这种接法还会改变 PWM 关闭阶段的制动或滑行状态。初次接线可先采用三线方案。&lt;/p&gt;
&lt;h3&gt;选择 PWM 频率&lt;/h3&gt;
&lt;p&gt;低频 PWM 可能让电机发出啸叫，并加大电流和转矩波动。提高频率可以避开部分可听范围，同时也会增加驱动器的开关损耗。老器件还可能限制最高开关频率。&lt;/p&gt;
&lt;p&gt;示例使用 &lt;strong&gt;10 kHz&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;/li&gt;
&lt;li&gt;定时器时钟和所需占空比分辨率。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;L298N 实战接线&lt;/h2&gt;
&lt;p&gt;L298 内含两座 H 桥，可控制两台有刷直流电机或一台双极步进电机。接线示例只用 A 路：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ENA&lt;/code&gt;、&lt;code&gt;IN1&lt;/code&gt;、&lt;code&gt;IN2&lt;/code&gt; 控制 &lt;code&gt;OUT1&lt;/code&gt;、&lt;code&gt;OUT2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;B 路对应 &lt;code&gt;ENB&lt;/code&gt;、&lt;code&gt;IN3&lt;/code&gt;、&lt;code&gt;IN4&lt;/code&gt; 和 &lt;code&gt;OUT3&lt;/code&gt;、&lt;code&gt;OUT4&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;L298WiringLab client:visible /&amp;gt;&lt;/p&gt;
&lt;h3&gt;上电前检查&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;给电机准备独立电源。&lt;/strong&gt; 开发板的 5 V 引脚无法承担启动或堵转电流。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单片机 GND 与驱动模块 GND 共地。&lt;/strong&gt; 否则逻辑电平没有共同参考。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ENA 跳帽状态与接法一致。&lt;/strong&gt; 使用 PWM 调速时，拔掉把 ENA 固定拉高的跳帽，再接 PWM 引脚。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;确认模块的 5 V 逻辑供电方式。&lt;/strong&gt; 各家 L298N 模块使用不同的稳压器、跳帽和丝印，请按手中模块的说明接线。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;先架空负载并限流。&lt;/strong&gt; 第一次测试从小占空比开始，先验证方向，再装回机械结构。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;原厂 L298 把逻辑高电平门限最低值定为 2.3 V，因此 STM32F103 的 3.3 V GPIO 满足要求。成品模块还包含外围电路，接线前请检查模块原理图。&lt;/p&gt;
&lt;h2&gt;Arduino Uno 完整示例&lt;/h2&gt;
&lt;p&gt;接线约定：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;D11 → ENA&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D10 → IN1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;D9 → IN2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;Arduino GND 与 L298N GND 共地。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;#include &amp;lt;Arduino.h&amp;gt;

constexpr uint8_t MOTOR_ENA = 11;
constexpr uint8_t MOTOR_IN1 = 10;
constexpr uint8_t MOTOR_IN2 = 9;

// speed 范围：-255 ~ +255
// 正数正转，负数反转，0 自由滑行停止。
void motorSetSpeed(int16_t speed) {
  speed = constrain(speed, -255, 255);

  if (speed == 0) {
    analogWrite(MOTOR_ENA, 0);
    digitalWrite(MOTOR_IN1, LOW);
    digitalWrite(MOTOR_IN2, LOW);
    return;
  }

  if (speed &amp;gt; 0) {
    digitalWrite(MOTOR_IN1, HIGH);
    digitalWrite(MOTOR_IN2, LOW);
  } else {
    digitalWrite(MOTOR_IN1, LOW);
    digitalWrite(MOTOR_IN2, HIGH);
  }

  analogWrite(MOTOR_ENA, abs(speed));
}

void setup() {
  pinMode(MOTOR_ENA, OUTPUT);
  pinMode(MOTOR_IN1, OUTPUT);
  pinMode(MOTOR_IN2, OUTPUT);
  motorSetSpeed(0);
}

void loop() {
  // 约 2.5 秒内逐渐加速到正向全速。
  for (int16_t speed = 0; speed &amp;lt;= 255; ++speed) {
    motorSetSpeed(speed);
    delay(10);
  }

  // 先减到零，再换向，避免满速直接反转。
  for (int16_t speed = 255; speed &amp;gt;= 0; --speed) {
    motorSetSpeed(speed);
    delay(10);
  }
  delay(300);

  for (int16_t speed = 0; speed &amp;gt;= -255; --speed) {
    motorSetSpeed(speed);
    delay(10);
  }

  for (int16_t speed = -255; speed &amp;lt;= 0; ++speed) {
    motorSetSpeed(speed);
    delay(10);
  }
  delay(800);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;analogWrite()&lt;/code&gt; 使用开发板预设的 PWM 频率，Arduino Uno 的默认值并非 10 kHz。产品若对噪声或转矩纹波有要求，请配置定时器。&lt;/p&gt;
&lt;h2&gt;STM32F103 标准外设库完整示例&lt;/h2&gt;
&lt;p&gt;示例配置使用 72 MHz 系统时钟：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PA6 / TIM3_CH1 → ENA&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PB0 → IN1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PB1 → IN2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIM3&lt;/code&gt; 计数周期为 7200 个计数，对应 10 kHz PWM。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;speed&lt;/code&gt; 的范围为 &lt;code&gt;-1000 ~ +1000&lt;/code&gt;，命令步进为 0.1%。定时器周期决定 PWM 的有效分辨率。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;stm32f10x.h&quot;

#define MOTOR_PWM_TOP 7200U
#define MOTOR_IN1_PORT GPIOB
#define MOTOR_IN1_PIN  GPIO_Pin_0
#define MOTOR_IN2_PORT GPIOB
#define MOTOR_IN2_PIN  GPIO_Pin_1

static void Motor_GPIO_Init(void)
{
    GPIO_InitTypeDef gpio;

    RCC_APB2PeriphClockCmd(
        RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB,
        ENABLE
    );

    // PA6 = TIM3_CH1 = ENA PWM
    gpio.GPIO_Pin = GPIO_Pin_6;
    gpio.GPIO_Mode = GPIO_Mode_AF_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &amp;amp;gpio);

    // PB0、PB1 = IN1、IN2
    gpio.GPIO_Pin = MOTOR_IN1_PIN | MOTOR_IN2_PIN;
    gpio.GPIO_Mode = GPIO_Mode_Out_PP;
    gpio.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOB, &amp;amp;gpio);

    GPIO_ResetBits(GPIOB, MOTOR_IN1_PIN | MOTOR_IN2_PIN);
}

static void Motor_PWM_Init(void)
{
    TIM_TimeBaseInitTypeDef timer;
    TIM_OCInitTypeDef pwm;

    RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);

    // TIM3 时钟 72 MHz：72 MHz / 7200 = 10 kHz
    timer.TIM_Prescaler = 0;
    timer.TIM_CounterMode = TIM_CounterMode_Up;
    timer.TIM_Period = MOTOR_PWM_TOP - 1U;
    timer.TIM_ClockDivision = TIM_CKD_DIV1;
    timer.TIM_RepetitionCounter = 0;
    TIM_TimeBaseInit(TIM3, &amp;amp;timer);

    pwm.TIM_OCMode = TIM_OCMode_PWM1;
    pwm.TIM_OutputState = TIM_OutputState_Enable;
    pwm.TIM_Pulse = 0;
    pwm.TIM_OCPolarity = TIM_OCPolarity_High;
    TIM_OC1Init(TIM3, &amp;amp;pwm);
    TIM_OC1PreloadConfig(TIM3, TIM_OCPreload_Enable);
    TIM_ARRPreloadConfig(TIM3, ENABLE);

    TIM_Cmd(TIM3, ENABLE);
}

void Motor_Init(void)
{
    Motor_GPIO_Init();
    Motor_PWM_Init();
    TIM_SetCompare1(TIM3, 0);
}

// speed 范围：-1000 ~ +1000
void Motor_SetSpeed(int16_t speed)
{
    uint32_t pulse;

    if (speed &amp;gt; 1000) speed = 1000;
    if (speed &amp;lt; -1000) speed = -1000;

    if (speed == 0) {
        TIM_SetCompare1(TIM3, 0);  // ENA 持续为低：自由滑行
        GPIO_ResetBits(GPIOB, MOTOR_IN1_PIN | MOTOR_IN2_PIN);
        return;
    }

    // 先关闭 ENA，再改变方向脚，避免切换瞬间产生错误状态。
    TIM_SetCompare1(TIM3, 0);

    if (speed &amp;gt; 0) {
        GPIO_SetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN);
        GPIO_ResetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN);
        pulse = ((uint32_t)speed * MOTOR_PWM_TOP) / 1000U;
    } else {
        GPIO_ResetBits(MOTOR_IN1_PORT, MOTOR_IN1_PIN);
        GPIO_SetBits(MOTOR_IN2_PORT, MOTOR_IN2_PIN);
        pulse = ((uint32_t)(-speed) * MOTOR_PWM_TOP) / 1000U;
    }

    TIM_SetCompare1(TIM3, pulse);
}

static void Delay(volatile uint32_t count)
{
    while (count-- != 0U) {
        __NOP();
    }
}

int main(void)
{
    int16_t speed;

    Motor_Init();

    while (1) {
        for (speed = 0; speed &amp;lt;= 1000; speed += 10) {
            Motor_SetSpeed(speed);
            Delay(50000);
        }
        for (speed = 1000; speed &amp;gt;= 0; speed -= 10) {
            Motor_SetSpeed(speed);
            Delay(50000);
        }

        Delay(500000);  // 确认减速到零后再反转

        for (speed = 0; speed &amp;gt;= -1000; speed -= 10) {
            Motor_SetSpeed(speed);
            Delay(50000);
        }
        for (speed = -1000; speed &amp;lt;= 0; speed += 10) {
            Motor_SetSpeed(speed);
            Delay(50000);
        }

        Delay(500000);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空循环延时只负责放慢演示，无法提供精确定时。项目代码应改用 SysTick、硬件定时器或 RTOS 延时，并用状态机管理加减速和换向等待。&lt;/p&gt;
&lt;h2&gt;STM32F103 HAL 库完整示例&lt;/h2&gt;
&lt;p&gt;HAL 版本沿用同一组引脚。先在 STM32CubeMX 中完成以下配置：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把 &lt;code&gt;PA6&lt;/code&gt; 配为 &lt;code&gt;TIM3_CH1 / PWM Generation CH1&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;把 &lt;code&gt;PB0&lt;/code&gt;、&lt;code&gt;PB1&lt;/code&gt; 配为推挽输出，标签分别设为 &lt;code&gt;MOTOR_IN1&lt;/code&gt;、&lt;code&gt;MOTOR_IN2&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;若 TIM3 时钟为 72 MHz，设置 &lt;code&gt;Prescaler = 0&lt;/code&gt;、&lt;code&gt;Counter Period = 7199&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;PWM 极性选 High，初始 Pulse 设为 0。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;CubeMX 生成时钟、GPIO 和 &lt;code&gt;MX_TIM3_Init()&lt;/code&gt; 后，把以下驱动代码加入工程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#ifndef MOTOR_H
#define MOTOR_H

#include &quot;main.h&quot;

void Motor_Init(void);
void Motor_SetSpeed(int16_t speed);

#endif
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;motor.h&quot;
#include &quot;tim.h&quot;

#define MOTOR_PWM_TOP 7200U

void Motor_Init(void)
{
    HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
    __HAL_TIM_SET_COMPARE(&amp;amp;htim3, TIM_CHANNEL_1, 0);
    HAL_TIM_PWM_Start(&amp;amp;htim3, TIM_CHANNEL_1);
}

// speed 范围：-1000 ~ +1000
void Motor_SetSpeed(int16_t speed)
{
    uint32_t pulse;

    if (speed &amp;gt; 1000) speed = 1000;
    if (speed &amp;lt; -1000) speed = -1000;

    if (speed == 0) {
        __HAL_TIM_SET_COMPARE(&amp;amp;htim3, TIM_CHANNEL_1, 0);
        HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
        return;
    }

    // 先关闭 ENA，再更新方向。
    __HAL_TIM_SET_COMPARE(&amp;amp;htim3, TIM_CHANNEL_1, 0);

    if (speed &amp;gt; 0) {
        HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_SET);
        HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_RESET);
        pulse = ((uint32_t)speed * MOTOR_PWM_TOP) / 1000U;
    } else {
        HAL_GPIO_WritePin(MOTOR_IN1_GPIO_Port, MOTOR_IN1_Pin, GPIO_PIN_RESET);
        HAL_GPIO_WritePin(MOTOR_IN2_GPIO_Port, MOTOR_IN2_Pin, GPIO_PIN_SET);
        pulse = ((uint32_t)(-speed) * MOTOR_PWM_TOP) / 1000U;
    }

    __HAL_TIM_SET_COMPARE(&amp;amp;htim3, TIM_CHANNEL_1, pulse);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 CubeMX 生成的 &lt;code&gt;main.c&lt;/code&gt; 中调用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#include &quot;motor.h&quot;

int main(void)
{
    int16_t speed;

    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_TIM3_Init();
    Motor_Init();

    while (1) {
        for (speed = 0; speed &amp;lt;= 1000; speed += 10) {
            Motor_SetSpeed(speed);
            HAL_Delay(10);
        }
        for (speed = 1000; speed &amp;gt;= 0; speed -= 10) {
            Motor_SetSpeed(speed);
            HAL_Delay(10);
        }

        HAL_Delay(300);

        for (speed = 0; speed &amp;gt;= -1000; speed -= 10) {
            Motor_SetSpeed(speed);
            HAL_Delay(10);
        }
        for (speed = -1000; speed &amp;lt;= 0; speed += 10) {
            Motor_SetSpeed(speed);
            HAL_Delay(10);
        }

        HAL_Delay(800);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;APB1 分频和系统时钟会改变 TIM3 的计数时钟。请在 CubeMX 的 Clock Configuration 中确认定时器时钟，再计算 Prescaler 和 Period。&lt;/p&gt;
&lt;h2&gt;五个工程风险&lt;/h2&gt;
&lt;h3&gt;满速直接反转&lt;/h3&gt;
&lt;p&gt;电机高速旋转时施加反向电压，会产生制动电流和反向电流。换向流程应按顺序执行：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;逐步减小 PWM；&lt;/li&gt;
&lt;li&gt;等待转速下降，必要时读取编码器；&lt;/li&gt;
&lt;li&gt;关闭 ENA；&lt;/li&gt;
&lt;li&gt;改变方向；&lt;/li&gt;
&lt;li&gt;再逐步增加 PWM。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;示例代码先减速到零再反转。&lt;code&gt;Motor_SetSpeed()&lt;/code&gt; 不读取转速，系统需要通过等待时间或编码器判断电机是否停稳。&lt;/p&gt;
&lt;h3&gt;只看额定电流，不看堵转电流&lt;/h3&gt;
&lt;p&gt;电机轴卡住后，反电动势消失，电流会远高于正常运行值。选择驱动器和电源时，请使用数据手册中的堵转电流，或通过限流测试取得实测值。空载电流不能用于功率选型。&lt;/p&gt;
&lt;h3&gt;忽略驱动器压降与散热&lt;/h3&gt;
&lt;p&gt;L298 使用双极型晶体管。原厂数据手册给出的桥总饱和压降在 1 A 条件下典型值约为 1.8 V，最坏条件更高。驱动器把这部分压降转化为热量，电机得到的电压也随之降低。&lt;/p&gt;
&lt;p&gt;L298N 可用于教学和已有模块维护。低压、电池供电或高效率设计可选择现代 MOSFET 驱动器。&lt;/p&gt;
&lt;h3&gt;忽略感性负载的续流路径&lt;/h3&gt;
&lt;p&gt;开关关断后，电机线圈仍会维持电流，因此电路需要续流路径。常见 L298N 模块已安装续流二极管；裸 L298 芯片和自制功率板需要按数据手册配置外部二极管与电源退耦电容。&lt;/p&gt;
&lt;h3&gt;把保护电路当成正常工作方式&lt;/h3&gt;
&lt;p&gt;过温保护、保险丝和限流电源会降低损坏概率。设计者仍需保证接线正确，留出电流余量，并处理软件状态。保护反复触发时，请检查负载、散热和换向流程。&lt;/p&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;li&gt;保持 &lt;code&gt;ENA=0&lt;/code&gt; 上电，检查没有异常发热；&lt;/li&gt;
&lt;li&gt;架空电机或卸下机械负载；&lt;/li&gt;
&lt;li&gt;给 10%～20% 占空比，只验证一个方向；&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;h2&gt;小结&lt;/h2&gt;
&lt;p&gt;GPIO 发送命令，驱动模块向电机提供功率。H 桥控制正转、反转、快速制动和自由滑行，PWM 控制有效出力。有符号速度命令进入驱动函数后，正负号选择方向，绝对值转换为占空比。&lt;/p&gt;
&lt;p&gt;拿到一款新驱动器时，先从数据手册确认方向引脚、PWM 引脚和关闭状态，再检查堵转电流、压降、散热、共地和换向流程。&lt;/p&gt;
</content:encoded></item><item><title>相机是怎么识别出颜色的：聊聊 CFA 阵列与 Demosaic</title><link>https://blog.cloudsir.top/posts/how-debayer-reconstructs-color-v2/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/how-debayer-reconstructs-color-v2/</guid><description>通过逐像素演示讲解 CFA 滤色阵列与 Demosaic 去马赛克插值，并分析伪色与摩尔纹的成因。</description><pubDate>Thu, 27 Aug 2026 12:20:00 GMT</pubDate><content:encoded>&lt;p&gt;import BayerPatternLab from &apos;./BayerPatternLab.tsx&apos;;
import RawPipelineLab from &apos;./RawPipelineLab.tsx&apos;;
import PixelWalkthroughLab from &apos;./PixelWalkthroughLab.tsx&apos;;
import AlgorithmExplorerLab from &apos;./AlgorithmExplorerLab.tsx&apos;;
import MoireVsFalseColorLab from &apos;./MoireVsFalseColorLab.tsx&apos;;
import DebayerQuickCheck from &apos;./DebayerQuickCheck.tsx&apos;;
import &apos;./debayer-v2.css&apos;;
import &apos;../../../components/interactive-lab/interactive-lab.css&apos;;&lt;/p&gt;
&lt;p&gt;传感器记录的 RAW 图像不像一张完整彩色照片。Bayer 传感器上的每个感光位置只测量红、绿、蓝中的一个通道，其余两个通道需要根据周围样本估计。&lt;/p&gt;
&lt;p&gt;把这张单通道马赛克重建成 RGB 图像的过程叫 &lt;strong&gt;Demosaic&lt;/strong&gt;（去马赛克，demosaicing）。由于最常见的 CFA 是拜耳阵列，工程上这一步也常被叫作 &lt;strong&gt;Debayer&lt;/strong&gt;，两种说法指的是同一件事。你在最终照片里看到的每个彩色像素，通常包含一个实测通道和两个估计通道，本文后面两种名称都会出现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文讨论线性 RAW 与常见 Bayer CFA。组件使用确定性合成场景，目的是说明坐标、邻域和采样限制。手机与相机中的实现还会结合噪声模型、坏点修复、饱和检测和硬件优化。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;拜耳阵列：一个位置只测一种颜色&lt;/h2&gt;
&lt;p&gt;硅感光单元主要测量到达它的光强。工程师在感光单元上方覆盖 &lt;strong&gt;CFA（Color Filter Array，彩色滤光阵列）&lt;/strong&gt;，让不同位置分别测量红光、绿光或蓝光。&lt;/p&gt;
&lt;p&gt;Bayer 阵列是最常见的一种 CFA，以 2×2 单元重复。RGGB 的基本单元是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R G
G B
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四种常见 Bayer Pattern 使用相同的颜色比例，只改变坐标原点处的相位。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;2×2 单元&lt;/th&gt;
&lt;th&gt;左上角通道&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RGGB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;R G / G B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BGGR&lt;/td&gt;
&lt;td&gt;&lt;code&gt;B G / G R&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GRBG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;G R / B G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GBRG&lt;/td&gt;
&lt;td&gt;&lt;code&gt;G B / R G&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;组件同时加入 RYYB 和 X‑Trans 作为对照。RYYB 使用黄色滤光位置，X‑Trans 使用 6×6 重复单元；它们不是 RGGB 的坐标相位变体，后续重建也需要匹配各自的采样结构。&lt;/p&gt;
&lt;p&gt;&amp;lt;BayerPatternLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;绿色占一半采样位置。人眼的亮度敏感度以及自然图像中的大量空间细节都与绿色附近的响应关系密切，提高绿色采样密度能在颜色信息与空间细节之间取得实用平衡。&lt;/p&gt;
&lt;p&gt;Pattern 描述坐标相位，并不代表四种去马赛克算法。裁掉一行、旋转图像或读错元数据，都可能改变坐标原点与 Pattern 的对应关系。&lt;/p&gt;
&lt;h2&gt;为什么需要 Debayer&lt;/h2&gt;
&lt;p&gt;以 RGGB 为例，一张 2400 万像素的 RAW 矩阵包含 2400 万个数值。每个数值只属于 R、G、B 中的一个通道，并非 2400 万组完整 RGB。&lt;/p&gt;
&lt;p&gt;Debayer 需要完成三件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据坐标与 Pattern 判断每个 RAW 数字属于哪个通道。&lt;/li&gt;
&lt;li&gt;保留当前位置的实测值。&lt;/li&gt;
&lt;li&gt;从邻近同色样本估计另外两个通道。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;下面的 NumPy 代码把 RGGB RAW 拆成三个稀疏平面。代码使用 mask 记录有效位置，避免把真实的黑色数值 0 误认为“没有样本”。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import numpy as np


def split_rggb(raw):
    &quot;&quot;&quot;把 RGGB RAW 拆成稀疏通道与有效位置 mask。&quot;&quot;&quot;
    rows, columns = raw.shape
    red_mask = np.zeros((rows, columns), dtype=bool)
    green_mask = np.zeros_like(red_mask)
    blue_mask = np.zeros_like(red_mask)

    red_mask[0::2, 0::2] = True
    green_mask[0::2, 1::2] = True
    green_mask[1::2, 0::2] = True
    blue_mask[1::2, 1::2] = True

    return (
        np.where(red_mask, raw, 0.0),
        np.where(green_mask, raw, 0.0),
        np.where(blue_mask, raw, 0.0),
        (red_mask, green_mask, blue_mask),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Debayer 位于 RAW 处理管线的前段。点选下面任一阶段，可以看到它处理什么，以及顺序为什么重要。&lt;/p&gt;
&lt;p&gt;&amp;lt;RawPipelineLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;具体产品可能调整或联合执行部分步骤，但插值应在线性域中完成。Gamma 或强 Tone Curve 会改变平均值的物理意义。&lt;/p&gt;
&lt;h2&gt;插值算法：从复制到方向判断&lt;/h2&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;典型问题&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;最近邻&lt;/td&gt;
&lt;td&gt;复制距离最近的同色样本&lt;/td&gt;
&lt;td&gt;色块、锯齿、通道错位&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;双线性&lt;/td&gt;
&lt;td&gt;对符合位置规则的同色邻居取平均&lt;/td&gt;
&lt;td&gt;边缘变软、假色、拉链纹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;教学版方向感知&lt;/td&gt;
&lt;td&gt;比较水平与垂直变化，沿较平滑方向估计 G&lt;/td&gt;
&lt;td&gt;方向判断会受噪声、斜线和重复纹理影响&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工业级算法&lt;/td&gt;
&lt;td&gt;使用更大邻域、色差模型、统计估计或学习方法&lt;/td&gt;
&lt;td&gt;计算与存储成本更高&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;完整的 Malvar-He-Cutler 算法使用固定的 5×5 线性滤波核。本文组件中的第三种方法只演示“先判断方向，再决定怎样平均”的思路，因此标为&lt;strong&gt;教学版方向感知&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;双线性怎样选择邻居&lt;/h3&gt;
&lt;p&gt;双线性 Debayer 不会把周围八个数字混在一起平均。目标位置与目标通道共同决定邻域形状。&lt;/p&gt;
&lt;p&gt;在 RGGB 的 R 位置，G 通常来自上、下、左、右四个绿色样本：&lt;/p&gt;
&lt;p&gt;$$
\hat G_{i,j}=
\frac{G_{i-1,j}+G_{i+1,j}+G_{i,j-1}+G_{i,j+1}}{4}
$$&lt;/p&gt;
&lt;p&gt;B 来自四个对角蓝色样本：&lt;/p&gt;
&lt;p&gt;$$
\hat B_{i,j}=
\frac{B_{i-1,j-1}+B_{i-1,j+1}+B_{i+1,j-1}+B_{i+1,j+1}}{4}
$$&lt;/p&gt;
&lt;p&gt;G 位置缺失的 R 与 B 分别来自水平或垂直方向，方向取决于当前 G 所在的 CFA 相位。&lt;/p&gt;
&lt;h2&gt;逐像素看完整计算过程&lt;/h2&gt;
&lt;p&gt;整张图的 Debayer，就是同一套局部计算在各个坐标上重复执行。选择下面任意像素，再按步骤查看两个缺失通道从哪些位置取得样本。&lt;/p&gt;
&lt;p&gt;&amp;lt;PixelWalkthroughLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;图像边缘没有完整邻域。实验允许选择最外圈像素，并使用镜像填充：越过边界的坐标映射回图内。生产实现也可能复制边缘、缩小有效输出区域，或为边界设计专用滤波核。算法和测试代码必须采用同一种策略。&lt;/p&gt;
&lt;p&gt;最近邻适合作为速度基线。双线性能得到更平滑的结果，但它假设局部颜色平滑变化。黑色栏杆与亮天空之间不满足这个假设，跨边缘平均会把两个物体混在一起。&lt;/p&gt;
&lt;h2&gt;同一份 RAW，不同算法会得到什么&lt;/h2&gt;
&lt;p&gt;算法差异在高对比边缘和高频纹理上更容易观察。下面保持场景、Pattern 和显示位置不变，只切换重建方法。&lt;/p&gt;
&lt;p&gt;&amp;lt;AlgorithmExplorerLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;小房子场景适合检查轮廓、窗框和屋顶。高频斜纹接近 CFA 的采样极限，此时误差同时来自采样丢失与插值估计。&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;不同噪声强度、饱和区域和 Pattern 相位下是否稳定。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;假色、拉链纹与摩尔纹&lt;/h2&gt;
&lt;p&gt;Debayer 常见的三类问题外观相近，成因并不相同。&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;假色（False color）&lt;/td&gt;
&lt;td&gt;中性细节旁出现错误颜色&lt;/td&gt;
&lt;td&gt;各通道采样位置不同，插值把空间变化解释成颜色变化&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;拉链纹（Zippering）&lt;/td&gt;
&lt;td&gt;边缘出现交替明暗或彩色小齿&lt;/td&gt;
&lt;td&gt;邻域选择或方向判断在边缘两侧反复切换&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;摩尔纹（Moiré）&lt;/td&gt;
&lt;td&gt;细密纹理变成大尺度波纹&lt;/td&gt;
&lt;td&gt;场景频率超过像素或 CFA 的采样能力&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;镜头色差、运动、传感器串扰和过强锐化也会形成彩色边缘。排查时应比较线性 RAW、不同 Debayer 结果和未锐化版本。&lt;/p&gt;
&lt;h2&gt;摩尔纹不等于伪色&lt;/h2&gt;
&lt;p&gt;摩尔纹属于亮度或结构混叠。灰度传感器也会产生摩尔纹。Bayer 传感器在不同位置采样 R、G、B，高频混叠再叠加通道相位差，可能产生额外伪色。&lt;/p&gt;
&lt;p&gt;&amp;lt;MoireVsFalseColorLab client:visible /&amp;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;像素偏移通过多次曝光获得更完整的颜色采样，但要求场景和相机保持稳定；&lt;/li&gt;
&lt;li&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;strong&gt;数据范围&lt;/strong&gt;：减黑电平后转换到 &lt;code&gt;float32&lt;/code&gt;，避免无符号整数差值下溢。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;饱和样本&lt;/strong&gt;：饱和数值不能提供可靠的颜色比例。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;坏点与噪声&lt;/strong&gt;：插值会把异常样本传播到周围输出像素。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pattern 与裁剪&lt;/strong&gt;：裁掉奇数行列、旋转或翻转后要同步更新 CFA 相位。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;边界策略&lt;/strong&gt;：镜像、复制或有效区域裁剪必须与测试参考一致。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;验证场景&lt;/strong&gt;：同时覆盖平坦色块、斜边、细纹、噪声、饱和区和四种 Pattern。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算法输出需要保留中间精度。不要为了让每一步“看起来合法”而反复裁剪到 0 到 1；应在管线明确要求时处理越界值。&lt;/p&gt;
&lt;h2&gt;知识小测&lt;/h2&gt;
&lt;p&gt;用五道题检查 CFA、插值、采样限制和方向判断。&lt;/p&gt;
&lt;p&gt;&amp;lt;DebayerQuickCheck client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;Debayer 的核心限制来自输入数据：一个位置只实测一个通道，算法只能根据空间相关性估计缺失信息。邻域、边界、处理域和 Pattern 相位共同决定重建结果。&lt;/p&gt;
</content:encoded></item><item><title>Ghostty 配置</title><link>https://blog.cloudsir.top/posts/ghostty/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/ghostty/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Wed, 26 Aug 2026 22:11:00 GMT</pubDate><content:encoded>&lt;pre&gt;&lt;code&gt;# Config generated by Ghostty Config

window-height = 24
window-width = 96
# custom-shader = ~/.config/ghostty/shaders/bettercrt.glsl
custom-shader = ~/.config/ghostty/shaders/cursor_smear.glsl
# custom-shader = ~/.config/ghostty/shaders/cursor_blaze.glsl

font-size = 17
# Nerd Font（tmux 状态栏图标需要，系统已安装 SFMono Nerd Font）
font-family = &quot;SFMono Nerd Font&quot;
macos-titlebar-style = tabs
macos-titlebar-proxy-icon = hidden
window-show-tab-bar = never
background-opacity = 0.8
background-blur = true
background-blur-radius = 11

theme = Catppuccin Mocha
background = #1e1e2e
foreground = #cdd6f4
selection-background = #585b70
selection-foreground = #cdd6f4
cursor-color = #f5e0dc
cursor-text = #1e1e2e
palette = 0=#45475a
palette = 1=#f38ba8
palette = 2=#a6e3a1
palette = 3=#f9e2af
palette = 4=#89b4fa
palette = 5=#f5c2e7
palette = 6=#94e2d5
palette = 7=#a6adc8
palette = 8=#585b70
palette = 9=#f37799
palette = 10=#89d88b
palette = 11=#ebd391
palette = 12=#74a8fc
palette = 13=#f2aede
palette = 14=#6bd7ca
palette = 15=#bac2de

window-padding-y = 10
unfocused-split-opacity = 0.89
split-divider-color = #ffffff

macos-option-as-alt = true
macos-window-shadow = false
# 左右内边距（x轴）
window-padding-x = 12

# 上下内边距（y轴）
window-padding-y = 12
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>摄影集 · 蓝</title><link>https://blog.cloudsir.top/posts/blue-portfolio-white/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/blue-portfolio-white/</guid><description>在白色展墙与纸面留白中观看九张蓝色摄影。</description><pubDate>Wed, 26 Aug 2026 13:13:00 GMT</pubDate><content:encoded>&lt;p&gt;import PhotoEssayV2Frame from &quot;@components/photo/essay-v2/PhotoEssayV2Frame.astro&quot;;
import PhotoEssayV2Grid from &quot;@components/photo/essay-v2/PhotoEssayV2Grid.astro&quot;;
import PhotoEssayV2Pair from &quot;@components/photo/essay-v2/PhotoEssayV2Pair.astro&quot;;
import PhotoEssayV2Spread from &quot;@components/photo/essay-v2/PhotoEssayV2Spread.astro&quot;;
import boat from &quot;../../../assets/blue/boat.jpg&quot;;
import water from &quot;../../../assets/blue/water.jpg&quot;;
import gull from &quot;../../../assets/blue/gull.jpg&quot;;
import flock from &quot;../../../assets/blue/flock.jpg&quot;;
import bubbles from &quot;../../../assets/blue/bubbles.jpg&quot;;
import blossomMoon from &quot;../../../assets/blue/blossom-moon.jpg&quot;;
import windfarm from &quot;../../../assets/blue/windfarm.jpg&quot;;
import moonBuilding from &quot;../../../assets/blue/moon-building.jpg&quot;;&lt;/p&gt;
&lt;p&gt;&amp;lt;PhotoEssayV2Grid
columns={3}
aspect=&quot;portrait&quot;
caption=&quot;薄雾压低了海平线。一艘船向远处移动，水纹和贴浪的海鸥，让蓝色第一次有了尺度。&quot;
captionAlign=&quot;center&quot;
index={2}
label=&quot;海面上的三段连续画面&quot;
photos={[
{ src: boat, alt: &quot;薄雾海面上的一艘游艇&quot; },
{ src: water, alt: &quot;蓝色海面的细密波纹&quot; },
{ src: gull, alt: &quot;贴着海面飞行的海鸥与倒影&quot; },
]}
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;PhotoEssayV2Spread
index={3}
lead={{ src: flock, alt: &quot;蓝色天空中的迁徙鸟群&quot;, position: &quot;center 52%&quot; }}
aside={{ src: bubbles, alt: &quot;蓝色天空中的两只透明泡泡&quot; }}
caption=&quot;鸟群把天空划出方向；两个泡泡停在另一侧，轻得像一段尚未落下的呼吸。&quot;
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;PhotoEssayV2Pair
index={4}
align=&quot;center&quot;
aspect=&quot;portrait&quot;
caption=&quot;天色没有变暗，月亮却已经出现。它先落在花枝旁，随后被城市的边缘重新框住。&quot;
captionAlign=&quot;center&quot;
photos={[
{ src: blossomMoon, alt: &quot;蓝天下的白色花朵与白昼月亮&quot; },
{ src: moonBuilding, alt: &quot;城市建筑边缘与蓝色天空中的月亮&quot;, position: &quot;left center&quot; },
]}
/&amp;gt;&lt;/p&gt;
&lt;p&gt;&amp;lt;PhotoEssayV2Frame photo={windfarm} alt=&quot;海上滩涂与成排的白色风力发电机&quot; caption=&quot;潮水退去以后，岸线露出细密的纹理。风机沿着远处排开，蓝色在这里慢慢落回地面。&quot; captionAlign=&quot;center&quot; size=&quot;wide&quot; align=&quot;center&quot; aspect=&quot;panorama&quot; position=&quot;center 68%&quot; index={5} /&amp;gt;&lt;/p&gt;
</content:encoded></item><item><title>LUT 是怎么改变颜色的？从一条曲线走进 RGB 立方体</title><link>https://blog.cloudsir.top/posts/how-lut-changes-color/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/how-lut-changes-color/</guid><description>用互动实验和 Python 代码解释 1D LUT、RGB 立方体、三线性插值与四面体插值。</description><pubDate>Mon, 24 Aug 2026 03:10:00 GMT</pubDate><content:encoded>&lt;p&gt;import LutPipelineOverview from &apos;./LutPipelineOverview.tsx&apos;;
import Lut1DExplorer from &apos;./Lut1DExplorer.tsx&apos;;
import RgbCurveMixer from &apos;./RgbCurveMixer.tsx&apos;;
import LutCubeExplorer from &apos;./LutCubeExplorer.tsx&apos;;
import LutCellCornerPicker from &apos;./LutCellCornerPicker.tsx&apos;;
import InterpolationComparator from &apos;./InterpolationComparator.tsx&apos;;
import LutResolutionLab from &apos;./LutResolutionLab.tsx&apos;;
import &apos;./lut-playground.css&apos;;&lt;/p&gt;
&lt;p&gt;剪辑软件给 Log 视频套用 LUT 后，画面的明暗和颜色随之改变。LUT 执行的核心操作是&lt;strong&gt;查表&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;LUT 是 Look-Up Table，也就是查找表。它保存一组输入与输出的对应关系。输入落在两个样本之间时，程序用&lt;strong&gt;插值&lt;/strong&gt;估算输出。&lt;/p&gt;
&lt;p&gt;1D LUT 在一条线上选择两个邻居，3D LUT 在 RGB 立方体中选择网格顶点。两者都需要坐标、邻居和权重。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;文中的 RGB 均归一化到 $[0,1]$。亮度单位、显示设备和完整色彩管理不在讨论范围内。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;LUT 保存输入与输出&lt;/h2&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;0.00&lt;/td&gt;
&lt;td&gt;0.00&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.25&lt;/td&gt;
&lt;td&gt;0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.50&lt;/td&gt;
&lt;td&gt;0.55&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.75&lt;/td&gt;
&lt;td&gt;0.86&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;td&gt;1.00&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;输入为 &lt;code&gt;0.50&lt;/code&gt; 时，程序读取第三行并输出 &lt;code&gt;0.55&lt;/code&gt;。数组查询开销低，同一张表也便于软件和设备交换。&lt;/p&gt;
&lt;p&gt;一条色彩管线会先把输入映射到 LUT 定义域，再用曲线分配明暗精度，随后交给 3D LUT 处理通道间的颜色关系。&lt;/p&gt;
&lt;p&gt;&amp;lt;LutPipelineOverview client:idle /&amp;gt;&lt;/p&gt;
&lt;p&gt;各阶段处理不同任务：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Domain（定义域）&lt;/strong&gt; 回答“输入数字落在哪个范围”。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;1D LUT / Shaper LUT&lt;/strong&gt; 擅长重新分配亮部与暗部的精度。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;3D LUT&lt;/strong&gt; 让三个颜色通道互相影响，完成更复杂的色彩变换。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LUT 不记录输入数值的色彩空间或物理意义。&lt;code&gt;0.5&lt;/code&gt; 可以表示 sRGB 中间调，也可以表示 Log 编码中的另一档曝光。输入编码与 LUT 预期不一致时，程序会查询错误位置。&lt;/p&gt;
&lt;h2&gt;1D LUT：先从一条亮度曲线开始&lt;/h2&gt;
&lt;p&gt;一维 LUT 像一把标有刻度的尺子。数组下标对应输入位置，数组值对应输出。&lt;/p&gt;
&lt;p&gt;这 5 个数字构成一张 1D LUT：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;lut = [0.00, 0.18, 0.55, 0.86, 1.00]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;五个采样点记录一条连续曲线的离散位置。增加采样点可以记录更多曲线细节；采样过少会产生可见台阶。&lt;/p&gt;
&lt;p&gt;&amp;lt;Lut1DExplorer client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;把组件中的 LUT 大小设为 4，再选择“最近邻”。输入跨过相邻样本的中点后，输出才会跳到下一档，实线因此呈阶梯状。&lt;/p&gt;
&lt;p&gt;要找到输入在数组中的位置，先把 $[0,1]$ 映射到数组下标范围：&lt;/p&gt;
&lt;p&gt;$$
p=x(N-1)
$$&lt;/p&gt;
&lt;p&gt;其中 $x$ 是输入，$N$ 是 LUT 长度。假设 LUT 有 5 项，输入是 &lt;code&gt;0.6&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;value = 0.6
size = 5
position = value * (size - 1)

print(position)  # 2.4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;位置 &lt;code&gt;2.4&lt;/code&gt; 落在第 2 项与第 3 项之间。插值根据它与两侧样本的距离计算输出。&lt;/p&gt;
&lt;h2&gt;一维线性插值按距离分配权重&lt;/h2&gt;
&lt;p&gt;上午 10 点测得 18℃，11 点测得 22℃。线性插值用连接两个样本的直线，把 10:30 估为 20℃。这个结果只描述两个已知点之间的线性近似。&lt;/p&gt;
&lt;p&gt;线性插值公式为：&lt;/p&gt;
&lt;p&gt;$$
y=(1-t)y_0+ty_1
$$&lt;/p&gt;
&lt;p&gt;$t$ 表示从左端走到右端的比例：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$t=0$：位于左端，结果是 $y_0$；&lt;/li&gt;
&lt;li&gt;$t=0.5$：位于中间，两边各占一半；&lt;/li&gt;
&lt;li&gt;$t=1$：到达右端，结果是 $y_1$。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;用 Python 写出来只有一行核心计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def lerp(left, right, fraction):
    &quot;&quot;&quot;在 left 和 right 之间进行线性插值。&quot;&quot;&quot;
    return left * (1.0 - fraction) + right * fraction


result = lerp(0.4, 0.8, 0.25)
print(result)  # 0.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的 1D LUT 采样还要处理索引与边界：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import math


def sample_lut_1d(lut, value):
    &quot;&quot;&quot;使用线性插值采样一张输入范围为 [0, 1] 的 1D LUT。&quot;&quot;&quot;
    value = max(0.0, min(1.0, value))
    position = value * (len(lut) - 1)

    left_index = math.floor(position)
    right_index = min(left_index + 1, len(lut) - 1)
    fraction = position - left_index

    return lerp(
        lut[left_index],
        lut[right_index],
        fraction,
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采样函数需要处理两个边界：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;函数先把输入限制在 $[0,1]$，避免访问数组边界之外；&lt;/li&gt;
&lt;li&gt;输入等于 &lt;code&gt;1.0&lt;/code&gt; 时，右侧索引停在最后一项。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;需要保留超范围高光时，请按 LUT 格式和色彩管线约定选择裁剪、外推或预映射，不要直接使用示例中的 &lt;code&gt;clamp&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;三条 1D 曲线无法识别颜色组合&lt;/h2&gt;
&lt;p&gt;红、绿、蓝三个通道可以各用一张 1D LUT：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def apply_rgb_curves(color, red_lut, green_lut, blue_lut):
    red, green, blue = color
    return (
        sample_lut_1d(red_lut, red),
        sample_lut_1d(green_lut, green),
        sample_lut_1d(blue_lut, blue),
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方法适合 Gamma、对比度和通道增益。每个输出通道只读取对应的输入通道，无法判断三个通道组成了什么颜色。&lt;/p&gt;
&lt;p&gt;&amp;lt;RgbCurveMixer client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;假设调色规则只针对“绿色和蓝色高、红色低”的青色区域。蓝色曲线读不到绿色输入；抬高蓝色曲线会同时影响天空、紫色和肤色阴影等含蓝色区域。&lt;/p&gt;
&lt;p&gt;3D LUT 同时读取 R、G、B，因而能针对特定通道组合修改颜色。&lt;/p&gt;
&lt;h2&gt;3D LUT 把 RGB 作为三维坐标&lt;/h2&gt;
&lt;p&gt;把 R、G、B 看成三条互相垂直的轴：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;沿 R 轴走，红色增加；&lt;/li&gt;
&lt;li&gt;沿 G 轴走，绿色增加；&lt;/li&gt;
&lt;li&gt;沿 B 轴走，蓝色增加。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;黑色 &lt;code&gt;(0, 0, 0)&lt;/code&gt; 和白色 &lt;code&gt;(1, 1, 1)&lt;/code&gt; 位于立方体的一组对角。红、绿、蓝、青、品红、黄位于其余角点。每组 RGB 数值对应立方体中的一个坐标。&lt;/p&gt;
&lt;p&gt;&amp;lt;LutCubeExplorer client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;17³ LUT&lt;/code&gt; 每条边放置 17 个采样点，共保存：&lt;/p&gt;
&lt;p&gt;$$
17^3=4913
$$&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;$17^3$&lt;/td&gt;
&lt;td&gt;4,913&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$33^3$&lt;/td&gt;
&lt;td&gt;35,937&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;$65^3$&lt;/td&gt;
&lt;td&gt;274,625&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;输入颜色很少落在网格点上。它位于某个小立方体内，程序从单元的 8 个顶点估算当前位置的输出。&lt;/p&gt;
&lt;h3&gt;定位单元的 8 个顶点&lt;/h3&gt;
&lt;p&gt;先只看 R 轴。假设输入颜色的红色分量是 $r$，LUT 每条边有 $N$ 个采样点，那么它在 R 轴上的数组位置是：&lt;/p&gt;
&lt;p&gt;$$
p_r=r(N-1)
$$&lt;/p&gt;
&lt;p&gt;对这个位置向下取整，得到左边的网格索引 $r_0$；右边的索引就是 $r_1=r_0+1$。G、B 两个方向也做同样的计算，于是得到三组索引：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R 方向：r0 或 r1
G 方向：g0 或 g1
B 方向：b0 或 b1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每个方向各有低侧和高侧两个索引，三个方向组合出 $2^3=8$ 个顶点：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;名称&lt;/th&gt;
&lt;th&gt;R 索引&lt;/th&gt;
&lt;th&gt;G 索引&lt;/th&gt;
&lt;th&gt;B 索引&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c100&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c010&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c110&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b0&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c101&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c011&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;c111&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;r1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;g1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;b1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;c&lt;/code&gt; 表示 LUT 中存储的输出颜色，后三位依次对应 R、G、B。数字 &lt;code&gt;0&lt;/code&gt; 选择低侧索引，&lt;code&gt;1&lt;/code&gt; 选择高侧索引。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;c100&lt;/code&gt; 表示当前单元中 R 选择高侧 &lt;code&gt;r1&lt;/code&gt;，G 和 B 选择低侧 &lt;code&gt;g0&lt;/code&gt;、&lt;code&gt;b0&lt;/code&gt;。程序用这三个索引从 LUT 读取一个输出颜色。&lt;/p&gt;
&lt;p&gt;&amp;lt;LutCellCornerPicker client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;以下 Python 代码把输入颜色转换为 8 个顶点。代码假设 &lt;code&gt;.cube&lt;/code&gt; 数据已展开为一维列表，并以 &lt;strong&gt;R 方向变化最快&lt;/strong&gt;的顺序存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import math


def grid_coordinate(value, size):
    &quot;&quot;&quot;返回低侧索引、高侧索引，以及输入在两者之间的比例。&quot;&quot;&quot;
    value = max(0.0, min(1.0, value))
    position = value * (size - 1)

    low = math.floor(position)
    high = min(low + 1, size - 1)
    fraction = position - low
    return low, high, fraction


def lut_index(red_index, green_index, blue_index, size):
    &quot;&quot;&quot;将三维坐标转换为一维数组下标，R 方向变化最快。&quot;&quot;&quot;
    return (blue_index * size + green_index) * size + red_index


def read_lut(lut, size, red_index, green_index, blue_index):
    index = lut_index(
        red_index,
        green_index,
        blue_index,
        size,
    )
    return lut[index]


def get_cell_corners(lut, size, color):
    red, green, blue = color

    r0, r1, fr = grid_coordinate(red, size)
    g0, g1, fg = grid_coordinate(green, size)
    b0, b1, fb = grid_coordinate(blue, size)

    corners = {
        &quot;000&quot;: read_lut(lut, size, r0, g0, b0),
        &quot;100&quot;: read_lut(lut, size, r1, g0, b0),
        &quot;010&quot;: read_lut(lut, size, r0, g1, b0),
        &quot;110&quot;: read_lut(lut, size, r1, g1, b0),
        &quot;001&quot;: read_lut(lut, size, r0, g0, b1),
        &quot;101&quot;: read_lut(lut, size, r1, g0, b1),
        &quot;011&quot;: read_lut(lut, size, r0, g1, b1),
        &quot;111&quot;: read_lut(lut, size, r1, g1, b1),
    }

    fractions = (fr, fg, fb)
    return corners, fractions
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例如，在 &lt;code&gt;17³ LUT&lt;/code&gt; 中输入 &lt;code&gt;(0.62, 0.38, 0.74)&lt;/code&gt;，三个方向的位置分别是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r0, r1, fr = grid_coordinate(0.62, 17)
g0, g1, fg = grid_coordinate(0.38, 17)
b0, b1, fb = grid_coordinate(0.74, 17)

print(r0, r1, fr)  # 9  10  0.92
print(g0, g1, fg)  # 6   7   0.08
print(b0, b1, fb)  # 11  12  0.84
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;c000&lt;/code&gt; 对应 LUT 坐标 &lt;code&gt;(9, 6, 11)&lt;/code&gt;，&lt;code&gt;c100&lt;/code&gt; 对应 &lt;code&gt;(10, 6, 11)&lt;/code&gt;，对角的 &lt;code&gt;c111&lt;/code&gt; 对应 &lt;code&gt;(10, 7, 12)&lt;/code&gt;。其余五个顶点使用另外五组高低索引组合。&lt;/p&gt;
&lt;h2&gt;三线性插值执行三轮 lerp&lt;/h2&gt;
&lt;p&gt;三线性插值沿 R、G、B 三个方向依次执行一维线性插值。&lt;/p&gt;
&lt;p&gt;设输入点在当前网格单元内的局部位置为 $(f_r,f_g,f_b)$，每个分量都位于 $[0,1]$：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;沿 R 方向对 8 个角点两两插值，得到 4 个颜色；&lt;/li&gt;
&lt;li&gt;沿 G 方向把 4 个颜色变成 2 个；&lt;/li&gt;
&lt;li&gt;沿 B 方向把最后 2 个颜色变成 1 个。&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;def lerp_color(left, right, fraction):
    return tuple(
        lerp(a, b, fraction)
        for a, b in zip(left, right)
    )


def trilinear_from_corners(corners, fr, fg, fb):
    &quot;&quot;&quot;corners 使用 c000、c100……c111 命名。&quot;&quot;&quot;
    c00 = lerp_color(corners[&quot;000&quot;], corners[&quot;100&quot;], fr)
    c10 = lerp_color(corners[&quot;010&quot;], corners[&quot;110&quot;], fr)
    c01 = lerp_color(corners[&quot;001&quot;], corners[&quot;101&quot;], fr)
    c11 = lerp_color(corners[&quot;011&quot;], corners[&quot;111&quot;], fr)

    c0 = lerp_color(c00, c10, fg)
    c1 = lerp_color(c01, c11, fg)

    return lerp_color(c0, c1, fb)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调用时，先用上一节的函数取得角点和局部位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;corners, (fr, fg, fb) = get_cell_corners(
    lut,
    size=17,
    color=(0.62, 0.38, 0.74),
)

output = trilinear_from_corners(corners, fr, fg, fb)
print(output)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三线性插值使用单元的 8 个顶点。GPU 可以用 3D 纹理采样执行这组计算，因此实时图形管线常用这种方法。&lt;/p&gt;
&lt;h2&gt;四面体插值：把立方体切成六块&lt;/h2&gt;
&lt;p&gt;另一个常见方案是把一个立方体沿对角线切成 6 个四面体。输入点落在哪个四面体，就只使用那一块的 4 个顶点。&lt;/p&gt;
&lt;p&gt;程序比较 $f_r$、$f_g$、$f_b$ 的大小顺序，以确定输入点所在的四面体。例如：&lt;/p&gt;
&lt;p&gt;$$
f_r\ge f_g\ge f_b
$$&lt;/p&gt;
&lt;p&gt;意味着输入沿 R 方向走得最远，沿 B 方向走得最少。此时使用的顶点是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;000 → 100 → 110 → 111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应的四个权重是：&lt;/p&gt;
&lt;p&gt;$$
1-f_r,\quad f_r-f_g,\quad f_g-f_b,\quad f_b
$$&lt;/p&gt;
&lt;p&gt;四个权重均不小于零，且总和为 1。以下 Python 代码列出六种顺序，便于逐项核对顶点和权重：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def add_weighted(result, color, weight):
    for channel in range(3):
        result[channel] += color[channel] * weight


def tetrahedral_from_corners(corners, fr, fg, fb):
    if fr &amp;gt;= fg &amp;gt;= fb:
        names = (&quot;000&quot;, &quot;100&quot;, &quot;110&quot;, &quot;111&quot;)
        weights = (1 - fr, fr - fg, fg - fb, fb)
    elif fr &amp;gt;= fb &amp;gt;= fg:
        names = (&quot;000&quot;, &quot;100&quot;, &quot;101&quot;, &quot;111&quot;)
        weights = (1 - fr, fr - fb, fb - fg, fg)
    elif fb &amp;gt;= fr &amp;gt;= fg:
        names = (&quot;000&quot;, &quot;001&quot;, &quot;101&quot;, &quot;111&quot;)
        weights = (1 - fb, fb - fr, fr - fg, fg)
    elif fb &amp;gt;= fg &amp;gt;= fr:
        names = (&quot;000&quot;, &quot;001&quot;, &quot;011&quot;, &quot;111&quot;)
        weights = (1 - fb, fb - fg, fg - fr, fr)
    elif fg &amp;gt;= fb &amp;gt;= fr:
        names = (&quot;000&quot;, &quot;010&quot;, &quot;011&quot;, &quot;111&quot;)
        weights = (1 - fg, fg - fb, fb - fr, fr)
    else:  # fg &amp;gt;= fr &amp;gt;= fb
        names = (&quot;000&quot;, &quot;010&quot;, &quot;110&quot;, &quot;111&quot;)
        weights = (1 - fg, fg - fr, fr - fb, fb)

    result = [0.0, 0.0, 0.0]
    for name, weight in zip(names, weights):
        add_weighted(result, corners[name], weight)

    return tuple(result)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四面体插值在每个四面体内部执行线性组合。输入跨过分界面后，程序改用相邻四面体。输出在边界上连续，导数可能不连续。&lt;/p&gt;
&lt;h2&gt;比较三线性与四面体插值&lt;/h2&gt;
&lt;p&gt;插值误差指插值结果与原始连续变换之间的差。脱离原始变换，无法判断哪种方法更准确。LUT 只保存了该变换的一组离散样本。&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;参与的单元顶点&lt;/td&gt;
&lt;td&gt;8 个&lt;/td&gt;
&lt;td&gt;4 个&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单元内部模型&lt;/td&gt;
&lt;td&gt;三线性&lt;/td&gt;
&lt;td&gt;分片线性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结果连续性&lt;/td&gt;
&lt;td&gt;连续&lt;/td&gt;
&lt;td&gt;连续&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实现特点&lt;/td&gt;
&lt;td&gt;可映射到 3D 纹理&lt;/td&gt;
&lt;td&gt;需要判断所在四面体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;常见场景&lt;/td&gt;
&lt;td&gt;GPU 实时渲染&lt;/td&gt;
&lt;td&gt;调色与 CPU 色彩处理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;高分辨率 LUT 采样平滑变换时，两种方法会得到接近的结果。降低 LUT 分辨率或增加变换曲率会放大差异。&lt;/p&gt;
&lt;p&gt;调色管线常用四面体插值。它采用分片线性模型，能否接近原始函数取决于 LUT 内容、分辨率和输入位置。&lt;/p&gt;
&lt;p&gt;&amp;lt;InterpolationComparator client:visible /&amp;gt;&lt;/p&gt;
&lt;h2&gt;分清 LUT 分辨率、位深与插值&lt;/h2&gt;
&lt;p&gt;渐变断层可能来自 LUT 采样、输出量化或插值误差。三者需要分别检查。&lt;/p&gt;
&lt;h3&gt;LUT 分辨率&lt;/h3&gt;
&lt;p&gt;LUT 分辨率描述网格密度。&lt;code&gt;33³&lt;/code&gt; 比 &lt;code&gt;17³&lt;/code&gt; 保存更多中间颜色样本，因而能记录曲率更高的变换。&lt;/p&gt;
&lt;h3&gt;图像或输出位深&lt;/h3&gt;
&lt;p&gt;8-bit 每通道提供 256 个编码级别，10-bit 提供 1024 个。LUT 插值无法消除低位深量化产生的色带。&lt;/p&gt;
&lt;h3&gt;插值方式&lt;/h3&gt;
&lt;p&gt;最近邻、三线性和四面体决定如何估算 LUT 样本之间的输出。近似误差与输出量化是两个误差来源。&lt;/p&gt;
&lt;p&gt;抖动（dithering）加入受控噪声，把规则色带打散成颗粒。它不会恢复量化丢失的细节，但能减弱色带的视觉边界。&lt;/p&gt;
&lt;p&gt;&amp;lt;LutResolutionLab client:visible /&amp;gt;&lt;/p&gt;
&lt;p&gt;Log 编码和高动态范围输入常在 3D LUT 前使用 Shaper LUT。Shaper 重新分配数值区间，让 3D 网格在重点明暗区域获得更多采样点。&lt;/p&gt;
&lt;h2&gt;&lt;code&gt;.cube&lt;/code&gt; 文件里保存了什么&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;.cube&lt;/code&gt; 文件头可以包含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;TITLE &quot;Example Look&quot;
DOMAIN_MIN 0.0 0.0 0.0
DOMAIN_MAX 1.0 1.0 1.0
LUT_3D_SIZE 33
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DOMAIN_MIN&lt;/code&gt; 与 &lt;code&gt;DOMAIN_MAX&lt;/code&gt; 定义输入范围；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LUT_1D_SIZE&lt;/code&gt; 或 &lt;code&gt;LUT_3D_SIZE&lt;/code&gt; 定义表格尺寸；&lt;/li&gt;
&lt;li&gt;后续各行保存输出 RGB 三元组。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;一个只处理“纯 3D LUT”的教学版简化解析器如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;def load_cube(path):
    size = None
    domain_min = (0.0, 0.0, 0.0)
    domain_max = (1.0, 1.0, 1.0)
    values = []

    with open(path, encoding=&quot;utf-8&quot;) as cube_file:
        for raw_line in cube_file:
            line = raw_line.strip()

            if not line or line.startswith(&quot;#&quot;):
                continue

            parts = line.split()
            keyword = parts[0]

            if keyword == &quot;LUT_3D_SIZE&quot;:
                size = int(parts[1])
            elif keyword == &quot;DOMAIN_MIN&quot;:
                domain_min = tuple(map(float, parts[1:4]))
            elif keyword == &quot;DOMAIN_MAX&quot;:
                domain_max = tuple(map(float, parts[1:4]))
            elif keyword not in {&quot;TITLE&quot;, &quot;LUT_1D_SIZE&quot;}:
                values.append(tuple(map(float, parts[:3])))

    if size is None:
        raise ValueError(&quot;文件中没有 LUT_3D_SIZE&quot;)
    if len(values) != size ** 3:
        raise ValueError(&quot;3D LUT 数据量与声明的尺寸不一致&quot;)

    return {
        &quot;size&quot;: size,
        &quot;domain_min&quot;: domain_min,
        &quot;domain_max&quot;: domain_max,
        &quot;values&quot;: values,
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码只解析教学示例。生产文件可能同时包含 1D 与 3D 数据，并使用不同的空白、注释和数值范围。解析器还需确认三维数据的遍历顺序。把“R 变化最快”读成“B 变化最快”不会改变数组长度，却会让颜色错位。&lt;/p&gt;
&lt;h2&gt;五项工程检查&lt;/h2&gt;
&lt;h3&gt;1. 色彩空间或传递函数不匹配&lt;/h3&gt;
&lt;p&gt;把 sRGB 输入送入为 LogC3 设计的 LUT，或把带 Gamma 的输入送入线性 RGB LUT，都会查询错误位置。插值算法无法修正输入编码错误。&lt;/p&gt;
&lt;h3&gt;2. 过早裁剪&lt;/h3&gt;
&lt;p&gt;浮点颜色管线可以保留小于 0 或大于 1 的中间值。过早 &lt;code&gt;clamp&lt;/code&gt; 会丢失高光或负值信息。裁剪位置由 LUT 定义域和后续流程决定。&lt;/p&gt;
&lt;h3&gt;3. 轴顺序与数组展开顺序错误&lt;/h3&gt;
&lt;p&gt;程序会把三维数组展开为一维内存。前文的 &lt;code&gt;lut_index()&lt;/code&gt; 假设 R 变化最快；文件若采用另一种顺序，8 个下标仍合法，但会指向另一组颜色。文件规范、解析器和 GPU 纹理上传代码必须使用同一顺序。&lt;/p&gt;
&lt;h3&gt;4. 把 LUT 当成完整色彩管理&lt;/h3&gt;
&lt;p&gt;LUT 保存固定映射，不携带完整的观察环境、显示设备、白点和场景信息。ACES、OCIO、ICC 等色彩管理系统可以调用 LUT，也会处理 LUT 之外的上下文。&lt;/p&gt;
&lt;h3&gt;5. 只在漂亮样片上测试&lt;/h3&gt;
&lt;p&gt;测试样片应包含：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;黑白点是否发生裁剪，中性灰是否偏色；&lt;/li&gt;
&lt;li&gt;RGB 和 CMY 六个高饱和颜色；&lt;/li&gt;
&lt;li&gt;接近网格边界的输入；&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DOMAIN_MIN&lt;/code&gt;、&lt;code&gt;DOMAIN_MAX&lt;/code&gt; 与恰好等于 &lt;code&gt;1.0&lt;/code&gt; 的输入；&lt;/li&gt;
&lt;li&gt;两种插值结果的最大误差出现在哪里。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;选择 LUT 与插值方法&lt;/h2&gt;
&lt;p&gt;亮度、Gamma 和独立通道曲线可以使用 1D LUT。&lt;/p&gt;
&lt;p&gt;色相旋转、选择性色彩和通道耦合变换需要 3D LUT。&lt;/p&gt;
&lt;p&gt;在实现 3D LUT 时：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GPU 提供 3D 纹理采样或项目要求实时处理时，可以先用三线性插值；&lt;/li&gt;
&lt;li&gt;需要匹配采用四面体插值的调色软件或处理管线，应明确实现四面体插值；&lt;/li&gt;
&lt;li&gt;变换曲率高时，提高 LUT 分辨率或加入 Shaper；&lt;/li&gt;
&lt;li&gt;需要可靠复现时，必须记录输入色彩空间、定义域、插值方法和超范围处理方式。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;LUT 保存离散的输入输出样本，插值估算样本之间的结果。1D LUT 在一条线上读取两个邻居；3D LUT 在 RGB 立方体中定位一个网格单元。三线性使用单元的 8 个顶点，四面体插值使用输入所在四面体的 4 个顶点。&lt;/p&gt;
&lt;p&gt;实现和调试 LUT 时，依次核对输入坐标、相邻样本和插值权重。&lt;/p&gt;
</content:encoded></item><item><title>使用Gitee + Webhook自动化部署网站</title><link>https://blog.cloudsir.top/posts/push-blog-by-gitee-webhook/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/push-blog-by-gitee-webhook/</guid><description>主要利用了 Gitee 的 Webhook 功能，当推送代码到 Gitee 时，Gitee会发送一条 POST 请求到指定的地址。</description><pubDate>Sun, 11 Jan 2026 05:01:00 GMT</pubDate><content:encoded>&lt;h2&gt;TLDR&lt;/h2&gt;
&lt;p&gt;:::note
主要利用了 Gitee 的 Webhook 功能，当推送代码到 Gitee 时，Gitee会发送一条 POST 请求到指定的地址，只需要在服务器端编写响应程序即可实现自动化部署，流程图如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
	participant 开发者
	participant Gitee
	participant 个人服务器
	
	开发者-&amp;gt;&amp;gt;Gitee: 设置 Webhook 地址与 SecretKey
	开发者-&amp;gt;&amp;gt;Gitee: git push
	Gitee-&amp;gt;&amp;gt;个人服务器: 触发 Webhook （POST请求）
	个人服务器-&amp;gt;&amp;gt;个人服务器: 验证 token
	个人服务器-&amp;gt;&amp;gt;Gitee: 验证结果
	个人服务器-&amp;gt;&amp;gt;个人服务器: 开始部署
	个人服务器-&amp;gt;&amp;gt;开发者: 部署结果通知（飞书或微信）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;h2&gt;步骤&lt;/h2&gt;
&lt;h3&gt;1. 编写在服务器自动部署项目的程序&lt;/h3&gt;
&lt;p&gt;主要流程是收到 Gitee 发出的 POST 请求后，编译项目并将输出的文件复制到网站根目录。这里的代码仅使用于使用 &lt;code&gt;pnpm&lt;/code&gt; 进行构建的前端项目，如果是其他的项目，需要对 &lt;code&gt;deploy()&lt;/code&gt; 函数进行修改。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import os
import shutil
import subprocess
from flask import Flask, request, jsonify, after_this_request
import filecmp
import multiprocessing
import time
import hmac
import hashlib
import base64
import os


# 配置项

# 项目工作目录
WORKING_DIR = &quot;/www/wwwroot/CLoudSirLab-Blog/blog-cloud-site&quot; 
# 网站部署目录
DEPLOY_DIR = &quot;/www/wwwroot/cloudsir.top&quot;
# 默认TOKEN
DEFAULT_SECRET_TOKEN = &quot;test&quot;

DEPLOYING = False
process_handler = None
retry = 0

def verify_signature(timestamp, sign):
    &quot;&quot;&quot;
    验证签名函数
    :param timestamp: 时间戳（毫秒）
    :param sign: 需要验证的签名
    :return: bool，表示签名是否有效
    &quot;&quot;&quot;
    # 从环境变量中获取密钥
    secret = os.getenv(&apos;SECRET_KEY&apos;, DEFAULT_SECRET_TOKEN)  # 如果环境变量未设置，则使用默认值
    secret_enc = secret.encode(&apos;utf-8&apos;)

    # 构造签名字符串
    string_to_sign = f&apos;{timestamp}\n{secret}&apos;
    string_to_sign_enc = string_to_sign.encode(&apos;utf-8&apos;)

    # 生成 HMAC 签名
    hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest()
    expected_sign = base64.b64encode(hmac_code).decode(&apos;utf-8&apos;)

    print(&quot;---HMAC---&quot;)
    print(&quot;timestamp: &quot;, timestamp)
    print(&quot;secret: &quot;, secret)
    print(&quot;sign: &quot;, sign)
    print(&quot;expected_sign: &quot;, expected_sign)
    print(&quot;---HMAC---&quot;)

    # 比较传入的签名和生成的签名
    return hmac.compare_digest(sign, expected_sign)


def deploy():
    global DEPLOYING
    global retry

    try:
        print(&quot;拉取最新代码&quot;)
        # 拉取最新代码
        subprocess.run([&quot;git&quot;, &quot;pull&quot;], check=True)
        
        print(&quot;开始编译&quot;)
        # 编译项目
        build_result = subprocess.run([&quot;npx&quot;, &quot;pnpm&quot;, &quot;run&quot;, &quot;build&quot;], check=True, capture_output=True, text=True)

        if build_result.returncode == 0:
            print(&quot;开始部署&quot;)

            # 将文件复制到部署目录
            dist_path = os.path.join(WORKING_DIR, &quot;dist&quot;)
            deploy_path = DEPLOY_DIR
            sync_directories(dist_path, deploy_path)
            
            # 发送消息通知
            notify_me(&quot;Deployment completed successfully&quot;)
            DEPLOYING = False
        else:
            print(&quot;部署失败&quot;)
            # 部署失败
            error_message = build_result.stderr
            DEPLOYING = False
            
            print(error_message)
            
            if retry &amp;gt; 0:
                print(&quot;重新部署&quot;)
                retry = retry - 1
                deploy()
            else:
                notify_me(f&quot;Deployment failed: {error_message}&quot;)
            
    except subprocess.CalledProcessError as e:
        error_message = str(e)
        DEPLOYING = False
        print(error_message)
        if retry &amp;gt; 0:
            print(&quot;重新部署&quot;)
            retry = retry - 1
            deploy()
        else:
            notify_me(f&quot;Deployment failed: {error_message}&quot;)

# 通知函数（可以改造成飞书推送、微信推送）
def notify_me(s):
    print(s)

app = Flask(__name__)

@app.route(&apos;/&apos;):
def test():
    return &quot;ok&quot;

@app.route(&apos;/webhook&apos;, methods=[&apos;POST&apos;])
def webhook():
    global DEPLOYING
    global process_handler
    global retry
    
    # 获取请求头
    event_type = request.headers.get(&apos;X-Git-Oschina-Event&apos;)
    token = request.headers.get(&apos;X-Gitee-Token&apos;)
    timestamp = request.headers.get(&apos;X-Gitee-Timestamp&apos;)
    
    print(request.headers)

    # 逐个判断是否都存在
    if not event_type:
        return jsonify({&quot;error&quot;: &quot;event_type is missing&quot;}), 400
    if not token:
        return jsonify({&quot;error&quot;: &quot;token is missing&quot;}), 400
    if not timestamp:
        return jsonify({&quot;error&quot;: &quot;timestamp is missing&quot;}), 400

    # 验证时间戳1分钟有效期
    timestamp_is_valid = time.time() - (int(timestamp) / 1000) &amp;lt; 60  
    if not timestamp_is_valid:
        return jsonify({&quot;error&quot;: &quot;Timestamp is expired&quot;}), 401

    # 验证token
    if not verify_signature(timestamp, token):
        print(&quot;Invalid signature&quot;)
        return jsonify({&quot;error&quot;: &quot;Invalid signature&quot;}), 401
    
    # 验证事件类型
    if event_type == &apos;Push Hook&apos;:
        print(&quot;Push Hook&quot;)
    elif event_type == &apos;push_hooks&apos;:
        return jsonify({&quot;message&quot;: &quot;Token is ok&quot;}), 200
    else:
        return jsonify({&quot;error&quot;: &quot;Invalid event type&quot;}), 400
    
    # 切换工作目录
    os.chdir(WORKING_DIR)
    
    DEPLOYING = True

    if (process_handler is not None) and (process_handler.is_alive()):
        print(&quot;检测到正在运行的部署进程，尝试终止...&quot;)
        process_handler.terminate()
        process_handler.join()  # 等待进程终止
        print(&quot;旧部署进程已终止&quot;)
    
    print(&quot;开始部署&quot;)
    retry = 3
    process_handler = multiprocessing.Process(target=deploy)
    process_handler.start()
 
    return jsonify({&quot;message&quot;: &quot;Start Deploying&quot;}), 200

def sync_directories(src, dst):
    &quot;&quot;&quot;
    同步两个目录，确保 dst 目录与 src 目录一致，只更新变化的文件，删除多余的文件。
    &quot;&quot;&quot;
    # 遍历 src 目录，更新或添加文件
    for root, dirs, files in os.walk(src):
        relative_path = os.path.relpath(root, src)
        target_path = os.path.join(dst, relative_path)

        # 确保目标目录存在
        if not os.path.exists(target_path):
            os.makedirs(target_path)

        for file in files:
            src_file = os.path.join(root, file)
            dst_file = os.path.join(target_path, file)

            # 如果目标文件不存在或内容不同，则更新文件
            if not os.path.exists(dst_file) or not filecmp.cmp(src_file, dst_file, shallow=False):
                shutil.copy2(src_file, dst_file)

    # 遍历 dst 目录，删除多余的文件和目录
    for root, dirs, files in os.walk(dst, topdown=False):
        relative_path = os.path.relpath(root, dst)
        src_path = os.path.join(src, relative_path)

        for file in files:
            dst_file = os.path.join(root, file)
            src_file = os.path.join(src_path, file)

            # 如果目标文件在 src 中不存在，则删除
            if not os.path.exists(src_file) and not src_file.endswith(&quot;.user.ini&quot;):
                os.remove(dst_file) 

        for dir in dirs:
            dst_dir = os.path.join(root, dir)
            src_dir = os.path.join(src_path, dir)

            # 如果目标目录在 src 中不存在，则删除
            if not os.path.exists(src_dir):
                shutil.rmtree(dst_dir)

if __name__ == &apos;__main__&apos;:
    app.run(host=&apos;0.0.0.0&apos;, port=9999)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2. 将服务器的公钥粘贴到 Gitee 中&lt;/h3&gt;
&lt;p&gt;在服务器上执行下面的代码获取公钥，尽量不要使用 root 用户进行部署（这里选择的是 &lt;code&gt;www&lt;/code&gt; 用户）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cat /home/www/.ssh/id_rsa.pub
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;将公钥粘贴在 Gitee 对应项目的 &lt;code&gt;部署公钥&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Pasted%20image%2020260111142953.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;3. 在服务器上下载项目代码并编译&lt;/h3&gt;
&lt;p&gt;将项目代码下载到服务器。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -u www git clone &amp;lt;项目ssh地址&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;初步编译一下项目，确保项目可以在服务器上正常编译。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 使用pnpm安装依赖库
sudo -u www npx pnpm install

# 编译一下，确保编译过程正常
sudo -u www npx pnpm run build
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4. 在服务器上启动部署程序&lt;/h3&gt;
&lt;p&gt;:::note
运行前需要安装 flask 库。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip3 install  flask
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;:::&lt;/p&gt;
&lt;p&gt;在服务器上以 &lt;code&gt;www&lt;/code&gt; 用户的身份运行部署程序。
注意设置一个 &lt;code&gt;SECRET_KEY&lt;/code&gt; ，Gitee 发送部署请求时需要携带该参数（可以随意设置，这里设置的是 &lt;code&gt;test&lt;/code&gt;）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo -u www SECRET_KEY=test python auto_deploy.py &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运行后执行下面的命令测试一下，如果部署成功，将输出 &lt;code&gt;ok&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl 127.0.0.1:9999
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;5. 设置 Webhook&lt;/h3&gt;
&lt;p&gt;进入 Gitee 的项目，点击 管理-&amp;gt; webhooks -&amp;gt; 添加Webhook&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Pasted%20image%2020260111140935.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;输入基础信息并保存。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Pasted%20image%2020260111144611.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;如果配置无误，点击 Webhook 页面的 &lt;code&gt;测试&lt;/code&gt; 按钮，将显示 &lt;code&gt;{&quot;message&quot;:&quot;Token is ok&quot;}&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Pasted%20image%2020260111144850.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6. 开始使用&lt;/h3&gt;
&lt;p&gt;在使用 &lt;code&gt;git push&lt;/code&gt; 上传代码后，稍等片刻，即可看到网站已更新。&lt;/p&gt;
</content:encoded></item><item><title>嵌入式学习资料整理</title><link>https://blog.cloudsir.top/posts/embedded-tourist-collections/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/embedded-tourist-collections/</guid><description>私人收藏整理的嵌入式学习资料，不定期会更新。</description><pubDate>Thu, 08 Jan 2026 12:01:00 GMT</pubDate><content:encoded>&lt;p&gt;:::note
私人收藏整理的嵌入式学习资料，不定期会更新。
:::&lt;/p&gt;
&lt;h2&gt;1. MCU 基础开发&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.embedfire.com/motor/motor_tutorial/zh/latest/index.html&quot;&gt;[野火]电机应用开发实战指南 — [野火]电机应用开发实战指南—基于STM32 文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://doc.embedfire.com/mcu/stm32/f103/hal_general/zh/latest/index.html&quot;&gt;[野火]STM32 HAL库开发实战指南——基于F103系列开发板 — [野火]STM32 HAL库开发实战指南——基于F103系列开发板文档&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href=&quot;https://baud-dance.com/&quot;&gt;波特律动&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 物联网开发&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.embedfire.com/net/lwip/zh/latest/index.html&quot;&gt;[野火]LwIP应用开发实战指南—基于STM32 — [野火]LwIP应用开发实战指南—基于野火STM32 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;3. RTOS 开发&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://rtos.100ask.org/zh/FreeRTOS/DShanMCU-F103/&quot;&gt;百问网《FreeRTOS入门与工程实践-基于STM32F103》教程-基于DShanMCU-103(STM32F103) | 百问网&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;4. Linux 开发&lt;/h2&gt;
&lt;h3&gt;4.1. 应用层开发&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;基于 C
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.embedfire.com/linux/stm32mp1/linux_base/zh/latest/index.html&quot;&gt;[野火]Linux基础与应用开发实战指南——基于STM32MP157开发板 — [野火]Linux基础与应用开发实战指南——基于STM32MP157开发板文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;基于 Python
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.embedfire.com/lubancat/python/zh/latest/index.html&quot;&gt;[野火]Python应用开发实战指南——基于LubanCat-i.MX6ULL-MP157开发板 — [野火]Python应用开发实战指南——基于LubanCat-i.MX6ULL-MP157开发板文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.2. 驱动开发&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.embedfire.com/linux/stm32mp1/driver/zh/latest/index.html#&quot;&gt;[野火]嵌入式Linux驱动开发实战指南——基于STM32MP157开发板 — [野火]嵌入式Linux驱动开发实战指南——基于STM32MP157开发板 文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4.3. 系统编译&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://doc.embedfire.com/lubancat/build_and_deploy/zh/latest/index.html#&quot;&gt;[野火]嵌入式Linux镜像构建与部署——基于LubanCat-i.MX6ULL开发板 — [野火]嵌入式Linux镜像构建与部署——基于LubanCat-i.MX6ULL开发板文档&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>如何将 flomo 笔记导入到 Obsidian</title><link>https://blog.cloudsir.top/posts/flomo-2-obsidian/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/flomo-2-obsidian/</guid><pubDate>Sat, 20 Dec 2025 08:12:00 GMT</pubDate><content:encoded>&lt;p&gt;视频教程: https://www.bilibili.com/video/BV1exqzBbE8U/&lt;/p&gt;
&lt;h2&gt;导入&lt;/h2&gt;
&lt;h3&gt;说明&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;支持图片&lt;/li&gt;
&lt;li&gt;支持 flomo 的特殊格式：加粗、列表、下划线等&lt;/li&gt;
&lt;li&gt;注意：&lt;strong&gt;不支持引用&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;操作步骤&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;安装 BeautifulSoup4 依赖库&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;pip install beautifulsoup4 -i https://pypi.tuna.tsinghua.edu.cn/simple
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&amp;lt;a href=&quot;/resources/flomo_to_md.py&quot;  target=&quot;_blank&quot; &amp;gt;下载导入脚本 (Python)&amp;lt;/a&amp;gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;将 &lt;code&gt;flomo_to_md.py&lt;/code&gt; 文件复制到解压后的文件夹中&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在解压后的文件夹中执行脚本&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;python flomo_to_md.py
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;删除笔记&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;// flomo自动删除无图片卡片脚本 - 优化版（每10次后重新获取网页信息）
// 该脚本会循环查找每个卡片，判断是否包含图片，如果没有则删除该卡片

let scanCount = 0;
const maxScansBeforeRefresh = 15;

function deleteMemosWithoutImages() {
  console.log(`开始第 ${scanCount + 1} 次扫描...`);
  
  // 查找memo卡片
  const memoCards = document.querySelectorAll(&apos;div[data-slug].memo, div[data-v-1abaecf2][data-slug]&apos;);
  
  console.log(`找到 ${memoCards.length} 个卡片`);
  
  if (memoCards.length === 0) {
    console.log(&apos;未找到卡片，停止处理&apos;);
    return;
  }

  let processedThisScan = 0;
  
  // 遍历每个卡片
  for (let i = 0; i &amp;lt; memoCards.length; i++) {
    const card = memoCards[i];

    console.log(`卡片 ${i + 1} 没有图片，准备删除...`);
    
    // 查找并点击卡片内的菜单按钮
    const menuTrigger = card.querySelector(&apos;[data-v-1abaecf2] .menu-trigger-content&apos;) ||
                        card.querySelector(&apos;.menu-trigger-content&apos;) ||
                        card.querySelector(&apos;svg.lucide-icon.lucide.lucide-ellipsis&apos;);
    
    if (menuTrigger) {
      const triggerElement = menuTrigger.closest(&apos;.menu-trigger-content&apos;) || menuTrigger;
      triggerElement.click();
      
      // 等待0.5秒让菜单展开
      setTimeout(() =&amp;gt; {
        // 查找删除按钮并点击
        const deleteButton = findDeleteButton();
        if (deleteButton) {
          deleteButton.click();
          console.log(&apos;点击了删除按钮&apos;);
        } else {
          console.log(&apos;未找到删除按钮&apos;);
        }
      }, 100);
    } else {
      console.log(&apos;未找到菜单按钮&apos;);
    }
    
    processedThisScan++;
    // 每次只处理一个卡片，然后等待页面更新
    break; 
  }
  
  if (processedThisScan === 0) {
    console.log(&apos;当前页面没有找到需要删除的卡片&apos;);
    return;
  }
  
  scanCount++;
  
  // 检查是否达到重新获取网页信息的次数
  if (scanCount &amp;gt;= maxScansBeforeRefresh) {
    console.log(`已达到 ${maxScansBeforeRefresh} 次扫描，重置计数器并继续...`);
    scanCount = 0; // 重置计数器
  }
  
  // 设置定时器，等待页面更新后继续处理
  setTimeout(() =&amp;gt; {
    deleteMemosWithoutImages();
  }, 2000); // 等待2秒后继续
}

// 查找删除按钮的函数
function findDeleteButton() {
  // 尝试多种可能的选择器
  let deleteButton = document.querySelector(&apos;[data-v-6226e620] .menu-item.danger.submenu-persistent .menu-item-content&apos;);
  
  if (!deleteButton) {
    // 查找文本为&quot;删除&quot;的菜单项
    const allMenuItems = document.querySelectorAll(&apos;.menu-item-content&apos;);
    for (let item of allMenuItems) {
      if (item.textContent &amp;amp;&amp;amp; item.textContent.trim() === &apos;删除&apos;) {
        deleteButton = item;
        break;
      }
    }
  }
  
  if (!deleteButton) {
    // 查找包含&quot;删除&quot;文本的按钮
    const dangerItems = document.querySelectorAll(&apos;.menu-item.danger&apos;);
    for (let item of dangerItems) {
      if (item.textContent &amp;amp;&amp;amp; item.textContent.includes(&apos;删除&apos;)) {
        deleteButton = item.querySelector(&apos;.menu-item-content&apos;) || item;
        break;
      }
    }
  }
  
  return deleteButton;
}

// 查找确认按钮的函数
function findConfirmButton() {
  // 尝试多种可能的选择器
  let confirmButton = document.querySelector(&apos;.el-message-box__btns .el-button--primary&apos;);
  
  if (!confirmButton) {
    // 查找文本包含&quot;确定&quot;或&quot;确认&quot;的按钮
    const allButtons = document.querySelectorAll(&apos;button, .el-button&apos;);
    for (let btn of allButtons) {
      const text = btn.textContent ? btn.textContent.trim() : &apos;&apos;;
      if ((text === &apos;确定&apos; || text === &apos;确认&apos; || text === &apos;是&apos;) &amp;amp;&amp;amp; 
          (btn.classList.contains(&apos;el-button--primary&apos;) || 
           btn.classList.contains(&apos;el-button--danger&apos;) ||
           btn.type === &apos;button&apos; || btn.type === &apos;submit&apos;)) {
        confirmButton = btn;
        break;
      }
    }
  }
  
  if (!confirmButton) {
    // 最后的备选方案：查找主要操作按钮
    confirmButton = document.querySelector(&apos;.el-button--primary, button[type=&quot;submit&quot;], .confirm-button&apos;);
  }
  
  return confirmButton;
}

// 启动函数
deleteMemosWithoutImages();
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python连接MQTT服务器</title><link>https://blog.cloudsir.top/posts/python-paho-mqtt/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/python-paho-mqtt/</guid><pubDate>Fri, 14 Nov 2025 23:11:00 GMT</pubDate><content:encoded>&lt;h2&gt;Getting Started&lt;/h2&gt;
&lt;p&gt;Python 连接 MQTT 需要先安装 MQTT 客户端库，&lt;code&gt;paho-mqtt&lt;/code&gt; 在这里是一个不错的选择。&lt;/p&gt;
&lt;h3&gt;安装 &lt;code&gt;paho-mqtt&lt;/code&gt;&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;pip install paho-mqtt
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;连接 MQTT 服务器&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import paho.mqtt.client as mqtt
import time

# MQTT服务器配置
broker = &quot;test.mosquitto.org&quot;  # 公共测试服务器
port = 1883
topic = &quot;test/topic/cloudsir&quot;

# 连接回调
def on_connect(client, userdata, flags, reason_code, properties):
    print(&quot;连接结果: &quot; + str(reason_code))
    client.subscribe(topic)
    client.publish(topic, &quot;Hello MQTT&quot;, qos=1, retain=False)

# 接收到消息的回调
def on_message(client, userdata, msg):
    print(f&quot;收到消息: {msg.topic} -&amp;gt; {msg.payload.decode()}&quot;)

# 创建客户端实例
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=&quot;test-client&quot;)

# 设置账户名和密码
# client.username_pw_set(&quot;your_username&quot;, &quot;your_password&quot;)

# 绑定回调函数
client.on_connect = on_connect
client.on_message = on_message

# 连接
client.connect(broker, port, keepalive=60)

# 启动后台消息循环线程
client.loop_start()

# 50s后关闭连接
time.sleep(60)
client.loop_stop()
client.disconnect()
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;连接 wss&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;paho-mqtt&lt;/code&gt; 默认创建的 MQTT 连接的是基于 TCP 的，如果要连接 &lt;code&gt;ws&lt;/code&gt; 或者 &lt;code&gt;wss&lt;/code&gt;，需要在创建客户端时指定 &lt;code&gt;transport&lt;/code&gt; 参数为 &lt;code&gt;websockets&lt;/code&gt; .&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;client = mqtt_client.Client(mqtt.CallbackAPIVersion.VERSION2, 
						client_id= clientId, 
						transport=&quot;websockets&quot;)  # 指定连接协议
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Python Flask的基础使用</title><link>https://blog.cloudsir.top/posts/python-flask/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/python-flask/</guid><description>A simple example of a Markdown blog post.</description><pubDate>Fri, 07 Nov 2025 22:11:00 GMT</pubDate><content:encoded>&lt;h2&gt;1. Hello world&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# flask Hello world
from flask import Flask,request

app = Flask(&apos;app&apos;)

@app.route(&apos;/&apos;)    # @app.get(&apos;/&apos;)
def main():
    return &apos;Hello World!&apos;

@app.route(&apos;/&amp;lt;name&amp;gt;&apos;)
def hello(name):
    if age := request.args.get(&apos;age&apos;):
        return &apos;Hello {}, 你{}岁!&apos;.format(name, age)
    return &quot;请求参数错误&quot;

app.run(debug=True, port=9999, host=&quot;0.0.0.0&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;如果需要外部访问，需要将 host 设置为 0.0.0.0 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;2. 获取全部参数&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&amp;lt;del&amp;gt;&lt;a href=&quot;https://blog.csdn.net/qq_41061717/article/details/106139899&quot;&gt;request属性&lt;/a&gt;&amp;lt;/del&amp;gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&quot;https://blog.csdn.net/whatday/article/details/106301387&quot;&gt;设置header&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://www.cnblogs.com/DEJAVU888/p/15479576.html&quot;&gt;DAY 213 Flask 1.1.2：request中存储参数的属性(form、args、data、json、files、values)使用简介 - DEJAVU_ERIC - 博客园&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;3. WSGI 方式启动&lt;/h2&gt;
&lt;p&gt;[[WSGI]]&lt;/p&gt;
&lt;h2&gt;4. 内部接口调用&lt;/h2&gt;
&lt;p&gt;flask内部接口调用&lt;/p&gt;
&lt;p&gt;https://deepinout.com/flask/flask-questions/45_flask_how_to_set_request_args_with_flask_test_client.html&lt;/p&gt;
&lt;p&gt;不会走 http 协议，而是内部的接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 可以是 post 等 http 方法
res = app.test_client().get(&quot;/api&quot;)

res.json  # 返回内容
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;Flask
(逐行显示输出) https://flask.net.cn/patterns/streaming.html&lt;/p&gt;
&lt;h2&gt;5. SSE&lt;/h2&gt;
&lt;p&gt;直接用Response包装迭代器函数即可&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;from flask import Flask, render_template, Response
import random
import time

@app.route(&apos;/generate_numbers&apos;)
def generate_numbers():
    def generate():
        for _ in range(5):
            number = random.randint(1, 100)  # 生成随机数字
            yield f&quot;data: {number}\n\n&quot;  # 使用 Server-Sent Events (SSE) 格式发送数据
            time.sleep(0.5)  # 每隔 0.5 秒生成一个数字
        
        yield &quot;data: done\n\n&quot;  # 发送结束信号
    
        headers = {
            &apos;Content-Type&apos;: &apos;text/event-stream&apos;,
            &apos;Cache-Control&apos;: &apos;no-cache&apos;,
            &apos;Connection&apos;: &apos;keep-alive&apos;,
        }
        
    return Response(generate(), headers=headers)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;done&lt;/code&gt; 并不是 Server-Sent Events (SSE) 协议中规定的关键词或信号。它只是我们在实现中自定义的一个标识符，用于通知前端“数据发送完成”,在前端代码中，我们通过判断 &lt;code&gt;event.data === &apos;done&apos;&lt;/code&gt; 来决定何时关闭连接。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;前端&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 创建 EventSource 连接
const eventSource = new EventSource(&apos;/generate_numbers&apos;);

eventSource.onmessage = (event) =&amp;gt; {
    if (event.data === &apos;done&apos;) {
        // 请求完成，关闭连接并恢复按钮状态
        eventSource.close();
        console.log(&apos;Generation complete&apos;);
    } else {
        // 显示生成的数字
        console.log(&apos;Generated number:&apos;, event.data);
    }
};

eventSource.onerror = (error) =&amp;gt; {
    console.error(&apos;Error:&apos;, error);
    eventSource.close();

    console.log(&apos;Generation failed&apos;);
};
           
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 参考资料&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/asdq2w3d/article/details/131432889&quot;&gt;flask ：stream_with_context流内容 + 前端请求_var source: any =-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://blog.csdn.net/qq_38463737/article/details/126163498&quot;&gt;ptyhon flask SSE 浏览器和服务器实时通信-例子实时推送随机数到前端画echart曲线图-CSDN博客&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>一文带你搞懂电机驱动模块</title><link>https://blog.cloudsir.top/posts/how-to-use-motor/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/how-to-use-motor/</guid><description>电机驱动的使用</description><pubDate>Thu, 06 Nov 2025 14:11:00 GMT</pubDate><content:encoded>&lt;p&gt;CSDN同步发布：&lt;a href=&quot;https://blog.csdn.net/qq_34802028/article/details/122431195&quot;&gt;【嵌入式&amp;amp;单片机】一文带你搞懂电机驱动模块_云朵先生_的博客-CSDN博客_电机驱动模块&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;1. 基本原理&lt;/h2&gt;
&lt;h3&gt;1.1 控制电机转速&lt;/h3&gt;
&lt;p&gt;在电机两端施加电压，电机就会旋转，而且电压越大，转速越快&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/013fc7d4b3de777086380f93accaa1eb.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;1.2 控制电机旋转方向&lt;/h3&gt;
&lt;p&gt;通正向电压电机正转，反向电压电机反转&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/39221f53ca47eb6708c7e8f75f5f3022.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;2. H桥驱动&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;attachment/94250d4f032b7a379617fe507cb99b20.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;2.1 &lt;code&gt;H桥驱动&lt;/code&gt;名称的由来&lt;/h3&gt;
&lt;p&gt;在上图中，四个开关和电机构成了一个驱动电路，因为形状像字母“H”，所以称作 &lt;code&gt;H桥驱动&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;2.2 H桥驱动控制电机旋转方向&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;在本篇博客中，当电机两端的电压方向为从左到右时，电机的旋转方向为正向.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;attachment/b3cb2c8be20b0eac8bac840ff061ade0.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.2.1 如上图，闭合开关 S1 和 S4，电机两端的电压方向为从左到右，电机正转&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/d8f5d35ee715f339f47fc7be69995e5a.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;2.2.1 如上图，闭合开关 S1 和 S4，电机两端的电压方向为从右到左，电机反转&lt;/p&gt;
&lt;h3&gt;2.3 H桥驱动&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;attachment/42b1d7eb52251cffae574cc763452e13.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在实际的应用中，并不会使用开关去驱动电机，而是一种类似开关的元器件--&lt;code&gt;MOS管&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;MOS管的导通条件比较繁琐，我们可以简单的认为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;接入高电平，MOS管导通&lt;/li&gt;
&lt;li&gt;接入低电平，MOS管断开&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;attachment/1da954b2b6a3640246c3211d7d87d528.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;所以，实际应用中的 &lt;code&gt;H桥驱动&lt;/code&gt; 是下面的样子&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/06c335151a0df7c18ffc5f637aa20e1c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;3. 电机驱动模块&lt;/h2&gt;
&lt;h3&gt;3.1 两个控制引脚的电机驱动&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;attachment/bf40ea2d730c7b167b8fc8735e8973d2.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;当 IN1 = 0，IN2 = 1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;M2 和 M5 导通，电机两端的电压方向为从左到右，电机正转&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 IN1 = 1，IN2 = 0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;M3 和 M4 导通，电机两端的电压方向为从左到右，电机正转&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 IN1 = 0，IN2 = 0&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;M2、M3、M4、M5 全部断开，电机不转&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;当 IN1 = 1，IN2 = 1&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;M2、M3、M4、M5 全部导通，电机不转（电机驱动模块中s包含保护电路，所以在这种情况下并不会发生短路）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.2 三个控制引脚的电机驱动&lt;/h3&gt;
&lt;p&gt;三个控制引脚的电机驱动是在 IN1、IN2 控制脚的基础上，增加了一个控制 IN1 和 IN2 通断的管脚 ENA（如下图）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/ae8ff854354a9dec388f6a6d3c141875.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 ENA = 1
&lt;ul&gt;
&lt;li&gt;控制方式与 [3.1](#3.1 两个控制引脚的电机驱动) 的控制方式相同&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;当 ENA = 0
&lt;ul&gt;
&lt;li&gt;电机停止转动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3.3 电机驱动模块的组成&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;attachment/f353aa435b022a072b0237280c342d8b.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;电机驱动模块除了内部的 &lt;code&gt;H桥驱动&lt;/code&gt; ，还由 保护电路、上下拉电阻、MOS管驱动芯片（单片机不能直接控制MOS管） 等组成。&lt;/p&gt;
&lt;h2&gt;4. 使用单片机控制电机驱动&lt;/h2&gt;
&lt;h3&gt;4.1 三个控制引脚的电机驱动&lt;/h3&gt;
&lt;h4&gt;4.1.1 接线&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;ENA &amp;lt;-&amp;gt; PWM引脚&lt;/li&gt;
&lt;li&gt;IN1 &amp;lt;-&amp;gt; 普通IO口&lt;/li&gt;
&lt;li&gt;IN2 &amp;lt;-&amp;gt; 普通IO口&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;attachment/7d574e9a53e1fae5651c40a4eef094f6.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h4&gt;4.1.2 控制方式&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;控制转速&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;调节 ENA 引脚 PWM 的占空比&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 PWM = 0，电机停止转动&lt;/li&gt;
&lt;li&gt;当 PWM = 100%，电机转速达到最大&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;控制转向&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;请参考 [3.1](#3.1 两个控制引脚的电机驱动)&lt;/p&gt;
&lt;h3&gt;4.2 两个控制引脚的电机驱动&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;Tip：三个控制引脚的电机驱动也可以使用下面的控制方式，但是需要将其中的 ENA脚 接 VCC。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;4.2.1 接线方式1 - 两路PWM&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;4.2.1.1 接线&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IN1 &amp;lt;-&amp;gt; PWM1&lt;/li&gt;
&lt;li&gt;IN2 &amp;lt;-&amp;gt; PWM2&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;要保证 PWM1 和 PWM2 的频率相同（通常为 10k HZ），频率与电机驱动模块和电机的规格有关，具体控制频率需要参考相关规格书。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&quot;attachment/b1df695f75f64a49b534248b1e8772cd.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4.2.1.2 控制方式 1&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PWM1 占空比&lt;/code&gt; &amp;gt; &lt;code&gt;PWM2 占空比&lt;/code&gt;（如上图）
&lt;ul&gt;
&lt;li&gt;电机正转&lt;/li&gt;
&lt;li&gt;电机的转速与 &lt;code&gt;PWM1 占空比&lt;/code&gt; - &lt;code&gt;PWM2 占空比&lt;/code&gt; 有关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PWM1 占空比&lt;/code&gt; &amp;lt; &lt;code&gt;PWM2 占空比&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;电机反转&lt;/li&gt;
&lt;li&gt;电机的转速与 &lt;code&gt;PWM1 占空比&lt;/code&gt; - &lt;code&gt;PWM2 占空比&lt;/code&gt; 有关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PWM1 占空比&lt;/code&gt; = &lt;code&gt;PWM2 占空比&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;电机停止转动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4.2.1.3 控制方式 2&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PWM1 占空比&lt;/code&gt; = 0，&lt;code&gt;PWM2 占空比&lt;/code&gt; 不为 0
&lt;ul&gt;
&lt;li&gt;电机反转&lt;/li&gt;
&lt;li&gt;电机的转速与 &lt;code&gt;PWM2 占空比&lt;/code&gt; 有关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PWM2 占空比&lt;/code&gt; = 0，&lt;code&gt;PWM1 占空比&lt;/code&gt; 不为 0
&lt;ul&gt;
&lt;li&gt;电机正转&lt;/li&gt;
&lt;li&gt;电机的转速与 &lt;code&gt;PWM1 占空比&lt;/code&gt; 有关&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PWM2 占空比&lt;/code&gt; = &lt;code&gt;PWM1 占空比&lt;/code&gt;
&lt;ul&gt;
&lt;li&gt;电机停止转动&lt;/li&gt;
&lt;li&gt;建议占空比为 0 或 100%&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;4.2.2 :star2: 接线方式2 - 一路PWM，一路 GPIO&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;4.2.2.1 接线&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;IN1 &amp;lt;-&amp;gt; PWM (通常为 10k HZ)&lt;/li&gt;
&lt;li&gt;IN2 &amp;lt;-&amp;gt; DIR (普通GPIO引脚)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4.2.2.2 控制方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/98c68298f12a0a53287214b68487e0ef.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DIR = 0（见上图）
&lt;ul&gt;
&lt;li&gt;电机正转&lt;/li&gt;
&lt;li&gt;转速与 PWM 的正向占空比有关
&lt;ul&gt;
&lt;li&gt;PWM = 100% 时，电机正向转速最大&lt;/li&gt;
&lt;li&gt;PWM = 0 时，电机停止转动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;attachment/f204dd73f7fbae6953f03b9b4347bf9c.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;DIR = 1 （见上图）
&lt;ul&gt;
&lt;li&gt;电机反转&lt;/li&gt;
&lt;li&gt;转速与 PWM 的反向占空比有关
&lt;ul&gt;
&lt;li&gt;PWM = 0 时，电机反向转速最大&lt;/li&gt;
&lt;li&gt;PWM = 100% 时，电机停止转动&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 实战 - 使用 Arduino 控制电机&lt;/h2&gt;
&lt;p&gt;使用的电机驱动是 L298N&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/082e4660f272be3623f5b3e0f4f7b4f9.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;它的原理图如下&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;attachment/4de892a1c49f06c46f1638272fc30f99.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;L29N是一个双路驱动，它可以同时驱动两个电机&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ENA，IN1，IN2 控制左边的电机&lt;/li&gt;
&lt;li&gt;ENB，IN2，IN3 控制右边的电机&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用的控制方式是 [4.1](#4.1 三个控制引脚的电机驱动) 的控制方式。&lt;/p&gt;
&lt;p&gt;接线如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ENA &amp;lt;-&amp;gt; D11 （注意：一定要接PWM引脚）&lt;/li&gt;
&lt;li&gt;IN1 &amp;lt;-&amp;gt; D10&lt;/li&gt;
&lt;li&gt;IN2 &amp;lt;-&amp;gt; D9&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;代码：&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/*
 * @Author: CloudSir
 * @Github: https://github.com/cloudsir
 * @Date: 2022-01-11 12:52:48
 * @LastEditTime: 2022-01-11 12:58:13
 * @LastEditors: CloudSir
 * @Description: Arduino 控制电机
 */

#define ENA 11
#define IN1 10
#define IN2 9

// speed的范围 -255 ~ 255
void setMotorSpeed(int speed)
{
    if(speed &amp;gt; 0)
    {
        // 如果速度大于0，则正转
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, LOW);
    }
    else (speed &amp;lt; 0)
    {
        // 如果速度小于0，则反转
        digitalWrite(IN1, LOW);
        digitalWrite(IN2, HIGH);
    }
    else
    {
        // 如果速度等于0，则停止转动
        digitalWrite(IN1, HIGH);
        digitalWrite(IN2, HIGH);      
    }

    analogWrite(ENA, abs(speed));
}

void setup()
{
    pinMode(IN1, OUTPUT);
    pinMode(IN2, OUTPUT);
}

void loop()
{
    int i = 0;

    // 电机正向旋转，速度逐渐增加
    for(i = 0; i &amp;lt;= 255; i++)
    {
        setMotorSpeed(i);
        delay(100);
    }

    // 电机正向旋转，速度逐渐减小
    for(i = 255; i &amp;gt;= 0; i--)
    {
        setMotorSpeed(i);
        delay(100);
    }

    // 电机反向旋转，速度逐渐增加
    for(i = 0; i &amp;lt;= 255; i++)
    {
        setMotorSpeed(-i);
        delay(100);
    }

    // 电机反向旋转，速度逐渐减小
    for(i = 255; i &amp;gt;= 0; i--)
    {
        setMotorSpeed(-i);
        delay(100);
    }

}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;6. 实战 - 使用 STM32F103 控制电机&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[4.2.2](#4.2.2 :star2: 方式2 - 一路PWM，一路 GPIO) 中控制电机驱动的方式最常用，但它的逻辑有些难理解，我们将以这种控制方式为例。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;6.1 接线&lt;/h3&gt;
&lt;p&gt;使用的驱动是 [章节5.](#5. 实战 - 使用 Arduino 控制电机) 中的 L298N&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ENA &amp;lt;-&amp;gt; VCC (改装为2线驱动)&lt;/li&gt;
&lt;li&gt;IN1 &amp;lt;-&amp;gt; DIR (任意一个GPIO口)&lt;/li&gt;
&lt;li&gt;IN2 &amp;lt;-&amp;gt; PWM (任意一个PWM输出口)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;6.2 PWM设置&lt;/h3&gt;
&lt;p&gt;首先对 STM32 的PWM模式进行设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;定时器为&lt;code&gt;向上计数&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;定时器频率设置为 10k HZ&lt;/li&gt;
&lt;li&gt;PWM模式为 &lt;code&gt;模式1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;PWM有效电平为&lt;code&gt;高电平&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;名词规定：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;maxPulse&lt;/code&gt;：定时器的最大计数值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;cnt&lt;/code&gt;：当前计数器值&lt;/li&gt;
&lt;li&gt;&lt;code&gt;duty&lt;/code&gt; ：占空比 ，&lt;code&gt;duty&lt;/code&gt; = &lt;code&gt;pulse&lt;/code&gt; / &lt;code&gt;maxPulse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pulse&lt;/code&gt;：比较寄存器的值&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时在一个PWM周期内，&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 &lt;code&gt;cnt&lt;/code&gt; &amp;lt; &lt;code&gt;pulse&lt;/code&gt;, PWM 引脚输出高电平&lt;/li&gt;
&lt;li&gt;当 &lt;code&gt;cnt&lt;/code&gt; &amp;gt;= &lt;code&gt;pulse&lt;/code&gt;, PWM 引脚输出低电平&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;attachment/b3f49045b19e9f37cf1af0ac0d5a1d44.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;h3&gt;6.3 原理分析&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;attachment/c900d15820c32a96c20f7046884b5488.png&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;
&lt;p&gt;参考 [4.2.2](#4.2.2 :star2: 方式2 - 一路PWM，一路 GPIO) 的控制方式，结合上图：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当 DIR = 0 时，电机正转，电机的转速取决于正向占空比 ，此时 &lt;code&gt;duty&lt;/code&gt; = &lt;code&gt;pulse&lt;/code&gt; / &lt;code&gt;maxPulse&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;当 DIR = 1 时，电机反转，电机的转速取决于反向占空比，此时 &lt;code&gt;duty&lt;/code&gt; = (&lt;code&gt;maxPulse&lt;/code&gt; - &lt;code&gt;pulse&lt;/code&gt;) / &lt;code&gt;maxPulse&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以可以写出下列 &lt;code&gt;伪代码&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// speed最大值是maxPulse, 最小值是 -maxPulse
// speed为正数时正转，为负数时反转
void setSpeed(int speed)
{
    // 限幅
    if(speed &amp;gt; maxPulse)
        speed = maxPulse;
    else if(speed &amp;lt; -maxPulse)
        speed = -maxPulse;
    
    
    if (speed &amp;gt; 0)
    {
        // 正转
        DIR = 0;
        pulse = speed;
        setPulse(pulse);
    }
    else if (pulse &amp;lt; 0)
    {
        // 反转
        DIR = 1;
        pulse = maxPulse + speed; // 这时speed是负数
        setPulse(pulse);
    }
    else
    {
        // 停止转动
        DIR = 1;
        pulse = maxPulse;
        setPulse(pulse);
    }
}

&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6.4 代码&lt;/h3&gt;
&lt;p&gt;明白了原理后，不难写出上面的代码，所以这里先不给出代码了。&lt;/p&gt;
</content:encoded></item><item><title>使用 eqmx 搭建MQTT Broker</title><link>https://blog.cloudsir.top/posts/use-eqmx-create-mqtt-broker/</link><guid isPermaLink="true">https://blog.cloudsir.top/posts/use-eqmx-create-mqtt-broker/</guid><pubDate>Wed, 15 Jan 2025 00:01:00 GMT</pubDate><content:encoded>&lt;h2&gt;启动&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;# 不带控制台输出信息启动
$ emqx start

# 进入emqx控制台，关闭终端程序即结束
$ emqx console
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;浏览器访问：&lt;/p&gt;
&lt;p&gt;http://localhost:18083&lt;/p&gt;
&lt;p&gt;默认账户：admin 密码：public&lt;/p&gt;
&lt;h2&gt;停止&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;$ emqx stop
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item></channel></rss>