<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Shaw 的小屋</title><description>随手记，随手看</description><link>https://me.622168.xyz/</link><language>zh_CN</language><item><title>课程地图</title><link>https://me.622168.xyz/posts/freertos/00-course-map/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/00-course-map/</guid><description>韦东山 FreeRTOS 入门与工程实践课的全 46 集目录，以及每集对应到本教程哪一章。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;课程&lt;/td&gt;
&lt;td&gt;FreeRTOS 入门与工程实践：由浅入深带你学习 FreeRTOS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;DShanMCU-F103，STM32F103 + HAL 库&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;工具&lt;/td&gt;
&lt;td&gt;STM32CubeMX、Keil（MDK-ARM）、ST-Link 下载器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1Jw411i7Fz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;规模&lt;/td&gt;
&lt;td&gt;46 个分P / 约 748 分钟（12.5 小时）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;先修&lt;/td&gt;
&lt;td&gt;C 语言、STM32 GPIO/串口/中断、能独立建立一个 HAL 工程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;前两集是后来追加的宣传视频；真正的课程正文从 p3 的硬件介绍开始。分P编号按当前 B 站页面计算。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;这门课的特点&lt;/h2&gt;
&lt;p&gt;这门课不是“背 API”路线，而是先摆出一个能运行但存在缺陷的做法，再引出 FreeRTOS 的正确机制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;轮询忙等 → 浪费 CPU
共享变量裸读写 → 竞态
高优先级任务被低优先级任务拖住 → 优先级反转
一个消费者从队列取走数据 → 其他任务收不到同一份数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后半程把这些问题放进两个连续项目里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;游戏机项目
  红外接收、旋转编码器、按键 → 输入任务
  游戏逻辑                         → 处理任务
  屏幕 / OLED                      → 显示任务
  蜂鸣器 / 音乐                    → 声音任务

车辆协同项目
  遥控、姿态、里程计等输入 → 队列 / 队列集
  中央逻辑                 → 分发任务
  电机、灯光、显示          → 信号量与事件组协调
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;学习路线&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;第 1 阶段：先把工程跑起来
  p3 ~ p8   硬件、模块、CubeMX、FreeRTOS 工程

第 2 阶段：理解任务为什么能切换
  p9 ~ p17  多任务、ARM、栈与堆、源码、内存管理

第 3 阶段：学会任务管理
  p18 ~ p26 创建、参数、栈、优先级、状态、调度、空闲任务、Delay

第 4 阶段：解决任务之间的问题
  p27 ~ p40 同步、互斥、通信、队列、队列集、游戏机

第 5 阶段：用内核对象组织工程
  p41 ~ p46 信号量、互斥量、优先级反转、事件组、车辆协同
&lt;/code&gt;&lt;/pre&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;th&gt;对应章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p1&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;百问网所有课程_链接与学习路线&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p2&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;韦东山1对1嵌入式私教课&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p3&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;td&gt;[0-1] 课程配套硬件介绍&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p4&lt;/td&gt;
&lt;td&gt;20.7&lt;/td&gt;
&lt;td&gt;[1-1] 单片机工程师的最具性价比升级路线 RTOS&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p5&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;[1-2] 讲师介绍 课程风格 项目展示&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p6&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;[2-1] 开发板使用&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p7&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;td&gt;[2-2] 模块使用说明与 STM32CubeMX 配置&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p8&lt;/td&gt;
&lt;td&gt;20.3&lt;/td&gt;
&lt;td&gt;[2-3] 自己创建 FreeRTOS 工程&lt;/td&gt;
&lt;td&gt;课程导览、硬件准备与工程搭建&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p9&lt;/td&gt;
&lt;td&gt;13.0&lt;/td&gt;
&lt;td&gt;[3-1] 创建第 1 个多任务程序&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p10&lt;/td&gt;
&lt;td&gt;15.0&lt;/td&gt;
&lt;td&gt;[3-2-1] ARM 架构简明教程：硬件架构与汇编指令&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p11&lt;/td&gt;
&lt;td&gt;17.2&lt;/td&gt;
&lt;td&gt;[3-2-2] ARM 架构简明教程：汇编实例&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p12&lt;/td&gt;
&lt;td&gt;21.9&lt;/td&gt;
&lt;td&gt;[3-3-1] 堆的概念&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p13&lt;/td&gt;
&lt;td&gt;22.8&lt;/td&gt;
&lt;td&gt;[3-3-2] 栈的概念：函数调用&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p14&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;[3-3-3] 栈的概念：局部变量&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p15&lt;/td&gt;
&lt;td&gt;16.0&lt;/td&gt;
&lt;td&gt;[3-3-4] 栈的概念：RTOS 如何使用栈&lt;/td&gt;
&lt;td&gt;第一个多任务程序与底层基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p16&lt;/td&gt;
&lt;td&gt;11.6&lt;/td&gt;
&lt;td&gt;[4-1] FreeRTOS 源码概述&lt;/td&gt;
&lt;td&gt;FreeRTOS 源码与内存管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p17&lt;/td&gt;
&lt;td&gt;11.5&lt;/td&gt;
&lt;td&gt;[4-2] 内存管理&lt;/td&gt;
&lt;td&gt;FreeRTOS 源码与内存管理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p18&lt;/td&gt;
&lt;td&gt;33.0&lt;/td&gt;
&lt;td&gt;[5-1-1] 创建任务：声光色影&lt;/td&gt;
&lt;td&gt;任务的创建、参数与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p19&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;[5-1-2] 创建任务：估算栈大小&lt;/td&gt;
&lt;td&gt;任务的创建、参数与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p20&lt;/td&gt;
&lt;td&gt;22.0&lt;/td&gt;
&lt;td&gt;[5-2] 创建任务：使用任务参数&lt;/td&gt;
&lt;td&gt;任务的创建、参数与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p21&lt;/td&gt;
&lt;td&gt;14.1&lt;/td&gt;
&lt;td&gt;[5-3] 删除任务：用遥控器控制音乐&lt;/td&gt;
&lt;td&gt;任务的创建、参数与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p22&lt;/td&gt;
&lt;td&gt;4.6&lt;/td&gt;
&lt;td&gt;[5-4] 优先级与阻塞：改善播放效果&lt;/td&gt;
&lt;td&gt;优先级、任务状态与调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p23&lt;/td&gt;
&lt;td&gt;17.3&lt;/td&gt;
&lt;td&gt;[5-5-1] 任务状态：改进播放控制&lt;/td&gt;
&lt;td&gt;优先级、任务状态与调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p24&lt;/td&gt;
&lt;td&gt;33.3&lt;/td&gt;
&lt;td&gt;[5-5-2] 任务管理与调度&lt;/td&gt;
&lt;td&gt;优先级、任务状态与调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p25&lt;/td&gt;
&lt;td&gt;14.4&lt;/td&gt;
&lt;td&gt;[5-5-3] 空闲任务&lt;/td&gt;
&lt;td&gt;优先级、任务状态与调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p26&lt;/td&gt;
&lt;td&gt;16.6&lt;/td&gt;
&lt;td&gt;[5-6] 两个 Delay 函数&lt;/td&gt;
&lt;td&gt;优先级、任务状态与调度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p27&lt;/td&gt;
&lt;td&gt;15.8&lt;/td&gt;
&lt;td&gt;[6-1] 同步互斥与通信：有缺陷的同步示例&lt;/td&gt;
&lt;td&gt;同步、互斥与通信总览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p28&lt;/td&gt;
&lt;td&gt;15.9&lt;/td&gt;
&lt;td&gt;[6-2] 同步互斥与通信：有缺陷的互斥示例&lt;/td&gt;
&lt;td&gt;同步、互斥与通信总览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p29&lt;/td&gt;
&lt;td&gt;4.3&lt;/td&gt;
&lt;td&gt;[6-3] 同步互斥与通信：FreeRTOS 提供的方法&lt;/td&gt;
&lt;td&gt;同步、互斥与通信总览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p30&lt;/td&gt;
&lt;td&gt;11.3&lt;/td&gt;
&lt;td&gt;[7] 游戏机项目说明&lt;/td&gt;
&lt;td&gt;同步、互斥与通信总览&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p31&lt;/td&gt;
&lt;td&gt;17.3&lt;/td&gt;
&lt;td&gt;[8-1-1] 数据传输的方法：环形 buffer&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p32&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;[8-1-2] 数据传输的方法：队列的本质&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p33&lt;/td&gt;
&lt;td&gt;12.2&lt;/td&gt;
&lt;td&gt;[8-2-1] 队列实验：多设备玩游戏（思路）&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p34&lt;/td&gt;
&lt;td&gt;21.4&lt;/td&gt;
&lt;td&gt;[8-2-2] 队列实验：多设备玩游戏（红外改造）&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p35&lt;/td&gt;
&lt;td&gt;30.4&lt;/td&gt;
&lt;td&gt;[8-2-3] 队列实验：多设备玩游戏（旋转编码器）&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p36&lt;/td&gt;
&lt;td&gt;4.8&lt;/td&gt;
&lt;td&gt;[8-2-4] 勘误：解决旋转编码器不好用的问题&lt;/td&gt;
&lt;td&gt;队列、环形缓冲与多设备输入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p37&lt;/td&gt;
&lt;td&gt;17.0&lt;/td&gt;
&lt;td&gt;[8-3-1] 队列集实验：改进程序框架（思路）&lt;/td&gt;
&lt;td&gt;队列集、姿态控制与赛车游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p38&lt;/td&gt;
&lt;td&gt;40.5&lt;/td&gt;
&lt;td&gt;[8-3-2] 队列集实验：改进程序框架（编程）&lt;/td&gt;
&lt;td&gt;队列集、姿态控制与赛车游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p39&lt;/td&gt;
&lt;td&gt;32.4&lt;/td&gt;
&lt;td&gt;[8-3-3] 队列集实验：增加姿态控制&lt;/td&gt;
&lt;td&gt;队列集、姿态控制与赛车游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p40&lt;/td&gt;
&lt;td&gt;46.5&lt;/td&gt;
&lt;td&gt;[8-4] 队列实验：分发数据给多个任务（赛车游戏）&lt;/td&gt;
&lt;td&gt;队列集、姿态控制与赛车游戏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p41&lt;/td&gt;
&lt;td&gt;10.5&lt;/td&gt;
&lt;td&gt;[9-1] 信号量的本质&lt;/td&gt;
&lt;td&gt;信号量、互斥量与优先级反转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p42&lt;/td&gt;
&lt;td&gt;16.2&lt;/td&gt;
&lt;td&gt;[9-2-1] 信号量实验：控制车辆运行&lt;/td&gt;
&lt;td&gt;信号量、互斥量与优先级反转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p43&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;[9-2-2] 信号量实验：优先级反转&lt;/td&gt;
&lt;td&gt;信号量、互斥量与优先级反转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p44&lt;/td&gt;
&lt;td&gt;17.7&lt;/td&gt;
&lt;td&gt;[9-3] 互斥量：领导临时提拔你（解决优先级反转）&lt;/td&gt;
&lt;td&gt;信号量、互斥量与优先级反转&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p45&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;[10-1] 事件组的本质&lt;/td&gt;
&lt;td&gt;事件组与车辆协同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p46&lt;/td&gt;
&lt;td&gt;13.7&lt;/td&gt;
&lt;td&gt;[10-2] 事件组实验：车辆协同&lt;/td&gt;
&lt;td&gt;事件组与车辆协同&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;API 速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;目标&lt;/th&gt;
&lt;th&gt;常用 API&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;code&gt;xTaskCreate&lt;/code&gt; / &lt;code&gt;xTaskCreateStatic&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;栈深度是 &lt;code&gt;StackType_t&lt;/code&gt; 的数量，不是字节数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除任务&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vTaskDelete&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;动态任务被删除后，空闲任务回收它的 TCB 和栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;延时&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vTaskDelay&lt;/code&gt; / &lt;code&gt;vTaskDelayUntil&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;前者是“再等 N 个 tick”，后者是“固定周期”&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xQueueCreate&lt;/code&gt; / &lt;code&gt;xQueueSend&lt;/code&gt; / &lt;code&gt;xQueueReceive&lt;/code&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;code&gt;xQueueCreateSet&lt;/code&gt; / &lt;code&gt;xQueueAddToSet&lt;/code&gt; / &lt;code&gt;xQueueSelectFromSet&lt;/code&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;code&gt;xSemaphoreCreateBinary&lt;/code&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;code&gt;xSemaphoreCreateCounting&lt;/code&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;code&gt;xSemaphoreCreateMutex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;有优先级继承，专用于互斥，不应在 ISR 中 Give&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;事件组&lt;/td&gt;
&lt;td&gt;&lt;code&gt;xEventGroupWaitBits&lt;/code&gt; / &lt;code&gt;xEventGroupSetBits&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;一个位表示一个事件，可等任意位或全部位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;优先级&lt;/td&gt;
&lt;td&gt;数值越大，逻辑优先级越高；&lt;code&gt;tskIDLE_PRIORITY&lt;/code&gt; 通常是 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tick&lt;/td&gt;
&lt;td&gt;FreeRTOS 的时间单位，常用 1 tick = 1 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Block&lt;/td&gt;
&lt;td&gt;阻塞态任务不参与调度，超时或条件满足后回到就绪态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;就绪&lt;/td&gt;
&lt;td&gt;任务具备运行条件，只是尚未拿到 CPU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;挂起&lt;/td&gt;
&lt;td&gt;任务被移出调度器，不会因为超时自动恢复&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ISR 版本&lt;/td&gt;
&lt;td&gt;中断里只能调用带 &lt;code&gt;FromISR&lt;/code&gt; 的接口，并正确传递 &lt;code&gt;pxHigherPriorityTaskWoken&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;怎么用这份笔记&lt;/h2&gt;
&lt;p&gt;每一章对应视频里的一段。建议的阅读顺序不是从 API 表开始，而是先看“为什么需要它”，再看 API 和实验，最后看“坑”。&lt;/p&gt;
&lt;p&gt;动手时至少复现四个节点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 声光色影四个任务同时跑：确认调度器真的在切换
② 用遥控器创建、暂停、删除音乐任务：走一遍任务的四种状态
③ 把红外键值从写环形缓冲区改成写队列：确认生产者和消费者解耦
④ 三个任务抢一张票：亲手制造优先级反转，再用互斥量解决
&lt;/code&gt;&lt;/pre&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;code&gt;vTaskDelay(100)&lt;/code&gt; 不是“绝对延时到某个时刻”，它表示当前时刻之后再等 100 个 tick。&lt;/li&gt;
&lt;li&gt;队列满、队列空都有超时参数。传 &lt;code&gt;0&lt;/code&gt; 是不等待，传 &lt;code&gt;portMAX_DELAY&lt;/code&gt; 才是永久等待。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xQueueSendFromISR&lt;/code&gt; 不能调用普通任务版；忘记处理 &lt;code&gt;pxHigherPriorityTaskWoken&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>FreeRTOS 源码与内存管理</title><link>https://me.622168.xyz/posts/freertos/03-source-and-memory/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/03-source-and-memory/</guid><description>内核源码怎么组织，heap_1 到 heap_5 差在哪，3072 字节的堆够不够用。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p16（11.6 分钟）、p17（11.5 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;源码目录&lt;/h2&gt;
&lt;p&gt;FreeRTOS 源码不用一次读完，先建立一张地图：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Source/
├── tasks.c          任务、调度、延时、状态
├── list.c           内核链表，任务队列的基础
├── queue.c          队列、队列集、信号量、互斥量
├── timers.c         软件定时器
├── event_groups.c   事件组
├── include/         API 与内核结构声明
└── portable/        芯片和编译器相关实现
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;portable/&lt;/code&gt; 是最容易出问题的地方，里面同时有编译器相关（RVDS、GCC、IAR）和芯片相关（Cortex-M3、M4）的代码，选错目录编译或链接阶段就会直接报错。这类问题不该靠改内核源码绕过去。&lt;/p&gt;
&lt;p&gt;&lt;em&gt;p16 这一集没有抓到字幕，上面这部分是按 FreeRTOS 官方源码结构整理的，不是视频里的原话。&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;堆就是一个数组&lt;/h2&gt;
&lt;p&gt;CubeMX 的中间件配置里，内存分配有「动态分配」和「静态分配」两个选项，堆大小是 3072 字节，管理方案选 heap_4。把动态分配禁掉之后，配置文件里就不再去编译 heap_4 那个文件了。&lt;/p&gt;
&lt;p&gt;堆在代码里没什么神秘的东西，就是一个数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;uint8_t ucHeap[configTOTAL_HEAP_SIZE];
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个全局静态数组，大小由配置宏决定（这里是 3072），用数组巧妙地表示一整块空闲内存。宏开得非常大时编译会报错。&lt;/p&gt;
&lt;h2&gt;heap_1 到 heap_5&lt;/h2&gt;
&lt;p&gt;五个文件都在 &lt;code&gt;Third-party/FreeRTOS/Source/portable/MemMang&lt;/code&gt; 下面，每个都实现了同名函数（&lt;code&gt;pvPortMalloc&lt;/code&gt; 之类），所以同一时间只能用其中一个。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heap_1&lt;/code&gt; 只分配、不回收，跟前面手写的最简 &lt;code&gt;my_malloc&lt;/code&gt; 是一个路子。只分配不回收的应用用它是&lt;strong&gt;一点空间都不浪费&lt;/strong&gt;——要 100 字节就给 100 字节，没有头部开销。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heap_2&lt;/code&gt; 能分配也能释放，但不合并相邻的空闲块，碎片问题很严重：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分配 100（头部 + 100）、分配 50（头部 + 50）
释放 buffer1、释放 buffer2
两块明明相邻，却不合并
此时想分配 120 → 失败
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;heap_3&lt;/code&gt; 只是去调用标准库函数（malloc/free）。嵌入式里一般不用标准库，所以这个方案也很少用。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heap_4&lt;/code&gt; 在 heap_2 基础上加了合并：释放 100 和 50 之后发现相邻就合并，再分配 120 就没问题了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;heap_5&lt;/code&gt; 支持分隔的内存。硬件上有 CPU、片内内存、Flash，可能还有片外 RAM，程序只用了其中一小块，剩下的都能当堆。heap_5 用空闲链表管理这些离散的堆，链表头指向第一个堆、再指向第二个，所以要先告诉它一共有几个离散堆，并把每块内存的起止地址和大小初始化进链表。&lt;/p&gt;
&lt;p&gt;选哪个：一般用 heap_4，有多块内存用 heap_5，heap_2 很少用，heap_1 更少用。&lt;/p&gt;
&lt;h2&gt;heap_4 用起来要注意什么&lt;/h2&gt;
&lt;p&gt;初始化堆的私有函数是 &lt;code&gt;prvHeapInit&lt;/code&gt;，但不需要手工调用——第一次调 &lt;code&gt;pvPortMalloc&lt;/code&gt; 的时候会自动调。&lt;/p&gt;
&lt;p&gt;分配和释放就是 &lt;code&gt;pvPortMalloc&lt;/code&gt; / &lt;code&gt;vPortFree&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;想知道 3072 够不够，跑一段时间后调这两个函数看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xPortGetFreeHeapSize()             当前还剩多少空闲内存
xPortGetMinimumEverFreeHeapSize()  整个运行过程中空闲容量的最小值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个值更有用。如果它已经接近个位数或十位数，说明系统曾经紧张到这个程度，该把堆调大了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;pvPortMalloc&lt;/code&gt; 还留了个口子：用户可以自定义一个钩子函数，分配失败的时候被调用，用来打印调试信息。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;configTOTAL_HEAP_SIZE&lt;/code&gt; 是 FreeRTOS 堆的大小，不是芯片全部 RAM。&lt;/li&gt;
&lt;li&gt;动态创建的任务、队列、信号量都从这个堆里拿内存。堆不够时表现为创建失败，不是运行出错。&lt;/li&gt;
&lt;li&gt;只看 &lt;code&gt;xPortGetFreeHeapSize()&lt;/code&gt; 会误判。刚启动时剩得多不代表跑起来够用，要看历史最小值。&lt;/li&gt;
&lt;li&gt;五份 heap_x 文件只能挑一个编进工程，同时编进去会撞同名函数。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>课程导览、硬件准备与工程搭建</title><link>https://me.622168.xyz/posts/freertos/01-course-setup/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/01-course-setup/</guid><description>从 DShanMCU-F103 的硬件和模块开始，走完 STM32CubeMX 配置、FreeRTOS 中间件启用和第一次编译下载。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p1 ~ p8（约 77 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;这门课在讲什么&lt;/h2&gt;
&lt;p&gt;全名《FreeRTOS 快速入门与工程实践》，百问网出品，分两部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;快速入门    API 怎么用——怎么创建任务、怎么用队列
工程实践    先摆出场景和问题，再讲用 FreeRTOS 的哪个功能去解决
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二部分才是主线。整门课写了 30 多个程序，但它们不是 30 个独立例子，而是&lt;strong&gt;三个项目被反复改进&lt;/strong&gt;的结果：音乐播放、打砖块、汽车游戏。打砖块是核心，最终框架会用到队列、信号量、互斥量、事件组；全课就是一步步提问题、一步步改代码，最后把这个框架搭出来。后两个游戏来自开源项目 nWatch。&lt;/p&gt;
&lt;p&gt;讲法上有个特点：理论部分不念 PPT，用绘图板现场画图；实践部分先讲思路，再从第一条指令开始现场写、现场调试，不给现成代码。模块驱动只讲接口怎么调，不讲内部实现——重点是 RTOS，不是 HAL 库开发。&lt;/p&gt;
&lt;h2&gt;硬件平台&lt;/h2&gt;
&lt;p&gt;中间是一块 &lt;strong&gt;STM32F103C8T6&lt;/strong&gt; 核心板，外面套一块黑色 12 边形底板，模块都插在底板上，配 ST-Link 下载器。软件是 STM32CubeMX + Keil。&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;用到哪&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;显示&lt;/td&gt;
&lt;td&gt;OLED（I2C1，PB6/PB7）&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;姿态传感器&lt;/td&gt;
&lt;td&gt;摇晃控制、姿态控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出&lt;/td&gt;
&lt;td&gt;LED（PC13）、无源蜂鸣器、全彩 LED、直流电机&lt;/td&gt;
&lt;td&gt;状态提示、音乐、动作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两个配置上的取舍：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debug 接口选 SWD，不选 JTAG。&lt;/strong&gt; JTAG 会占掉很多引脚，换 SWD 能省出 3 条，底板上正好用它们控制全彩 LED 或直流电机。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;时基选 TIM4。&lt;/strong&gt; 因为 TIM1、TIM2、TIM3 都被模块占用了。&lt;/p&gt;
&lt;h2&gt;三个项目&lt;/h2&gt;
&lt;p&gt;音乐播放是第 9 个程序，接无源蜂鸣器，按三角符号播放或停止。&lt;/p&gt;
&lt;p&gt;打砖块是核心。挡板可以用三种方式控制：摇晃板子、红外遥控器、旋转编码器。球碰到边缘会发声。&lt;/p&gt;
&lt;p&gt;汽车游戏是第 17 个程序，遥控器上 1/2/3 键控制。&lt;/p&gt;
&lt;h2&gt;自己建一个 FreeRTOS 工程&lt;/h2&gt;
&lt;p&gt;配套模板能直接用，自己从零建只是给想折腾的人。步骤记下来是因为坑都藏在细节里。&lt;/p&gt;
&lt;p&gt;CubeMX 里：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 新建工程，选 STM32F103C8T6（列表里有两个相近型号，选第一个）
② 时钟源使能 HSE 外部晶振；System Core → Debug 选 SWD；时基选 TIM4
③ 时钟树填 72，回车
④ Middleware → FreeRTOS，接口选 CMSIS_V2
   其余保持默认：可抢占开着、tick 频率 1000、heap_4、堆大小默认（3072 字节）
⑤ 启动后默认已有一个 defaultTask，名字、栈、优先级都保持默认
⑥ Project Manager：工程名 01_FreeRTOS_my，IDE 选 MDK-ARM
   Code Generator 勾上「把用到的库拷贝进工程」
⑦ 生成 → 编译 → 下载
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 ④ 步选的是 &lt;strong&gt;CMSIS_V2&lt;/strong&gt;，所以生成出来的是 &lt;code&gt;MX_FREERTOS_Init()&lt;/code&gt; + &lt;code&gt;osKernelStart()&lt;/code&gt; 和默认任务 &lt;code&gt;defaultTask&lt;/code&gt;，不是裸的 &lt;code&gt;xTaskCreate&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;第 ⑦ 步下载完什么都不会发生——工程里还没有任何操作硬件的代码，这是正常的。&lt;/p&gt;
&lt;h2&gt;加 LED 和 OLED&lt;/h2&gt;
&lt;p&gt;LED 在 PC13。CubeMX 里配成 GPIO_Output、推挽输出、不用内部上下拉，生成后 &lt;code&gt;MX_GPIO_Init&lt;/code&gt; 里已经配好了。&lt;/p&gt;
&lt;p&gt;驱动文件从模板复制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;driver_led     两个文件，放到工程 driver/ 目录
driver_timer   driver_led 里用了延时，要一起加
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工程里新建 group（F2 改名 DShanMCU-F103），把文件加进去。编译报「找不到头文件」就去 Options → C/C++ → Include Paths 加上头文件目录。&lt;/p&gt;
&lt;p&gt;然后在 defaultTask 里 &lt;code&gt;#include &quot;driver_led.h&quot;&lt;/code&gt;，在 &lt;code&gt;USER CODE BEGIN&lt;/code&gt; 区调 &lt;code&gt;led_test()&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;OLED 走 I2C1、PB6/PB7。重新打开 CubeMX 的办法是双击工程里的 &lt;code&gt;.ioc&lt;/code&gt; 文件，把 I2C1 使能，引脚会自动选上，参数保持默认。&lt;/p&gt;
&lt;p&gt;驱动要复制三样：&lt;code&gt;driver_oled&lt;/code&gt;、&lt;code&gt;driver_lcd&lt;/code&gt;，还有字库文件。&lt;code&gt;driver_lcd.c&lt;/code&gt; 只是对具体屏幕的封装，以后换屏只改它，上层统一用 &lt;code&gt;driver_lcd.h&lt;/code&gt; 的函数。字库被其他文件包含，不需要在工程里手动添加；编译会有一堆警告，能用。&lt;/p&gt;
&lt;p&gt;通用流程就四步：CubeMX 加配置 → 拷代码到工程 → 工程里添加文件 → 调用它的测试函数。&lt;/p&gt;
&lt;h2&gt;Reset and Run 这个坑&lt;/h2&gt;
&lt;p&gt;第一次烧写 LED 程序后灯不闪。不是程序错，是 Flash Download 里的 &lt;strong&gt;Reset and Run 被取消了&lt;/strong&gt;，芯片下载完没自动复位运行。重新上电就正常。&lt;/p&gt;
&lt;p&gt;麻烦的是：每次重新生成代码，这个勾选都会掉一次，要重新去 Debug → Settings → Flash Download 里勾上。不知道这件事，很容易把「没反应」当成程序写错了。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;模块驱动不能拿来直接用。以 OLED 为例，驱动内部发命令实际是调 I2C1 控制器的发送函数，所以必须先让 CubeMX 把 I2C1 和引脚初始化好。&lt;/li&gt;
&lt;li&gt;每次重新生成代码后 Reset and Run 会掉，烧写后没反应先查这里。&lt;/li&gt;
&lt;li&gt;工程放在中文目录里，Keil 退出调试时会报错，挪到非中文路径就好。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MX_FREERTOS_Init()&lt;/code&gt; 里的 defaultTask 不是摆设，后面第二个任务是在它之外手动建的，别顺手删掉。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>任务的创建、参数与删除</title><link>https://me.622168.xyz/posts/freertos/04-task-create-delete/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/04-task-create-delete/</guid><description>从声光色影实验出发，掌握 xTaskCreate 的参数、任务参数传递、栈估算，以及用遥控器删除音乐的完整流程。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p18 ~ p21（约 79 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;声光色影&lt;/h2&gt;
&lt;p&gt;配套源码第 5 个程序。四个任务，名字就说明了各自干什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;声   无源蜂鸣器放《孤勇者》
光   LED 闪烁
色   全彩 LED 渐变调色
影   LCD 显示遥控器键值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外还有一个默认任务负责红外遥控接收。烧写后能听到音乐、看到灯闪和变色、按遥控器屏幕有反应。&lt;/p&gt;
&lt;p&gt;任务要三样东西：函数（做什么）、栈（每个任务独享）、优先级（不是必需的，但决定谁先跑）。优先级可以这么理解：妈妈在喂饭，厨房着火了，她会放下碗先去灭火——火比喂饭急。任务在内核里用 TCB（Task Control Block）结构体串成链表来管理。&lt;/p&gt;
&lt;h2&gt;动态创建和静态创建&lt;/h2&gt;
&lt;p&gt;两套创建函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xTaskCreate        栈和 TCB 都由内核动态分配
xTaskCreateStatic  栈 buffer 和 TCB 都由用户提供
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;参数是同一套：函数指针、任务名、栈深度、参数、优先级、句柄输出。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;usStackDepth&lt;/code&gt; 的单位是 word，不是字节。传 128 表示 128×4 = 512 字节。静态版也要传栈大小，但那个值只是告诉函数「你给我的这块 buffer 有多大」。&lt;/p&gt;
&lt;p&gt;有个坑：静态版的返回值是句柄，不是 &lt;code&gt;BaseType_t&lt;/code&gt;，所以不能写 &lt;code&gt;if (xTaskCreateStatic(...) == pdPASS)&lt;/code&gt;。第一版就是这么写然后编译报错的，正确做法是把返回的句柄存起来。&lt;/p&gt;
&lt;p&gt;实验里的分工：声音任务用 &lt;code&gt;xTaskCreate&lt;/code&gt;，名字 &lt;code&gt;sound_task&lt;/code&gt;，栈 128，参数 NULL，优先级 &lt;code&gt;tskIDLE_PRIORITY&lt;/code&gt; + normal。光和色两个任务用 &lt;code&gt;xTaskCreateStatic&lt;/code&gt;，各自定义：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static StackType_t light_task_stack[128];
static StaticTask_t light_task_tcb;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 &lt;code&gt;StackType_t&lt;/code&gt; 这种 32 位类型声明数组，就不用手动乘 4 了。&lt;/p&gt;
&lt;p&gt;播放函数单独放在新建的 &lt;code&gt;music.c&lt;/code&gt; 里，从开源仓库「孤勇者 无源蜂鸣器 STM32」搬过来，用 HAL 定时器设频率、占空比 50%，延时用 &lt;code&gt;vTaskDelay&lt;/code&gt;。蜂鸣器要接无源那只——贴白纸的是有源的。&lt;/p&gt;
&lt;p&gt;跑起来有个现象：四个任务一起跑，音乐明显卡顿迟钝；把其他任务注释掉只留音乐任务，声音立刻流畅。这个问题留到后面解决。&lt;/p&gt;
&lt;h2&gt;栈要开多大&lt;/h2&gt;
&lt;p&gt;栈里放三样东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 返回地址和其他寄存器
② 局部变量
③ 任务被切换时保存的现场（16 个寄存器 × 4 = 64 字节）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;局部变量多少看代码，大数组、大 buffer 都很吃栈；返回地址和寄存器看函数调用深度。&lt;/p&gt;
&lt;p&gt;估算的办法是找最复杂的那条调用链。每级函数最多保存 9 个「被调用者寄存器」——R4 ~ R11 共 8 个加 LR，9 × 4 = 每级 36 字节。反汇编里实际看到的是每级保存 5~6 个，36 是理论上限。所以 5 级调用大约 5 × 36。&lt;/p&gt;
&lt;p&gt;这里有个容易搞错的地方：用栈最多的不一定是调用链最深的那个。如果某个比较浅的函数里定义了 1000 字节的局部 buffer，这条短链反而最耗栈。要找的是「局部变量最多的函数」。&lt;/p&gt;
&lt;p&gt;现场保存那部分基本恒定 64 字节。&lt;/p&gt;
&lt;p&gt;以音乐播放为例：假设 4 层调用，4 × 36 = 144 字节；局部变量约 32 字节（含一个 18 字节的结构体）；现场 64 字节；加起来约 250 字节。给的栈是 128 word = 512 字节，比 250 大，粗估够用。这只是粗算，精确办法（栈水位）后面再说。&lt;/p&gt;
&lt;h2&gt;用参数区分任务&lt;/h2&gt;
&lt;p&gt;第 6 个程序。同一个函数创建多个任务，靠参数区分行为。原理是：即使执行的是同一个函数，每个任务有自己的栈，函数里的局部变量各有各的副本。只有访问全局变量时才需要互斥。&lt;/p&gt;
&lt;p&gt;实验是同一个 &lt;code&gt;lcd_print_task(void *param)&lt;/code&gt; 创建三个任务，在屏幕不同位置打印：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;typedef struct {
    int8_t x;
    int8_t y;
    char name[16];      /* 屏幕每行最多 16 个字符 */
} task_print_info;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三个 static 全局结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;task1_info = {0, 0, &quot;Task1&quot;};
task2_info = {0, 3, &quot;Task2&quot;};
task3_info = {0, 6, &quot;Task3&quot;};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建时分别作为 &lt;code&gt;pvParameters&lt;/code&gt; 传进去，函数里再转回来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;task_print_info *p = (task_print_info *)param;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印逻辑有点绕：&lt;code&gt;lcd_print_string(x, y, p-&amp;gt;name)&lt;/code&gt; 会返回打印了多少个字符，拿到长度 len 之后再打印「:」，位置要加上 len，然后在同一行打印局部变量 &lt;code&gt;count&lt;/code&gt;（从 0 开始，各任务各自累加）。&lt;code&gt;count&lt;/code&gt; 是局部变量，在各任务的栈里，所以三个任务的数字不同步。&lt;/p&gt;
&lt;p&gt;LCD 的临时保护用了最土的办法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int lcd_can_use = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用之前判断，用时清 0，用完置 1。&lt;/p&gt;
&lt;p&gt;现象很有意思：最初只有 task3 在打印，task1 和 task2 完全没有输出。原因是 task3 在耗时的打印段中间被切换的概率最大，切换时它已经把 &lt;code&gt;lcd_can_use&lt;/code&gt; 清成 0，其他任务进来看到 0 就不打印了；而 task3 恢复之后没被立刻切走，马上又进循环把 0 又清一遍，于是 task1、task2 永远拿不到 LCD。&lt;/p&gt;
&lt;p&gt;解决办法是打印之后加 &lt;code&gt;mdelay(500)&lt;/code&gt;，让切换点大概率落在延时里——这时候它没占着 LCD，别人就有机会用。&lt;/p&gt;
&lt;p&gt;另外，因为默认任务被屏蔽了，要在 main 里创建任务之前自己初始化 LCD 并清屏。&lt;/p&gt;
&lt;p&gt;这一集留了两个问题：为什么用全局变量保护 LCD 不可靠？为什么最后创建的 task3 先运行？&lt;/p&gt;
&lt;h2&gt;删除任务&lt;/h2&gt;
&lt;p&gt;第 7 个程序 &lt;code&gt;07deltask&lt;/code&gt;。用遥控器控制音乐：三角键（play，键值 A8）创建音乐任务，红色电源键（power，键值 A2）删除音乐任务。红外读取函数返回 0 表示读到了键值，键值放在 &lt;code&gt;data&lt;/code&gt; 里。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vTaskDelete(TaskHandle_t xTaskToDelete);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删别的任务要传句柄，自杀传 NULL。&lt;/p&gt;
&lt;p&gt;逻辑上要注意句柄的初值是 NULL：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;收到 play  且句柄为 NULL    → 创建任务
收到 power 且句柄非 NULL    → 删除任务，删完把句柄置回 NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个条件都不加，就会重复创建或者删一个不存在的任务。&lt;/p&gt;
&lt;p&gt;现象：删除任务之后蜂鸣器保持一个音调，变成噪音。因为频率已经设好了，任务没了，没人去停它。改两处：打印前用 &lt;code&gt;lcd_clear_line&lt;/code&gt; 清行，避免旧字符残留；删除之前先调驱动里的停止蜂鸣器函数。&lt;/p&gt;
&lt;p&gt;这一集的重点是两条讲评：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;频繁创建、删除任务会反复动态分配和释放内存，容易产生内存碎片，
多次执行之后可能就分配不到内存了。

vTaskDelete 是直接终止任务，任务来不及做清理工作。
蜂鸣器停不下来就是例子。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以一般不推荐随手删任务，更常见的做法是让任务自己读遥控器、自己停止并做清理。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;usStackDepth&lt;/code&gt; 是 word 数不是字节数，STM32 上 128 表示 512 字节。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xTaskCreateStatic&lt;/code&gt; 返回的是句柄，拿它跟 &lt;code&gt;pdPASS&lt;/code&gt; 比较会编译报错。&lt;/li&gt;
&lt;li&gt;估算栈别只找最深的调用链，局部变量多的浅函数可能更吃栈。&lt;/li&gt;
&lt;li&gt;任务被删除前该停的外设要先停（蜂鸣器、电机），&lt;code&gt;vTaskDelete&lt;/code&gt; 不会帮你收尾。&lt;/li&gt;
&lt;li&gt;用全局变量保护共享资源不可靠，这一段只是演示问题，不是解决方案。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>第一个多任务程序与底层基础</title><link>https://me.622168.xyz/posts/freertos/02-first-task-and-memory/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/02-first-task-and-memory/</guid><description>从两个死循环任务讲起，穿过 ARM 汇编、堆和栈，理解 FreeRTOS 为什么能给每个任务切出独立的运行现场。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p9 ~ p15（约 116 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;第一个多任务程序&lt;/h2&gt;
&lt;p&gt;模板里 &lt;code&gt;driver_led&lt;/code&gt; 是死循环，&lt;code&gt;oled_test&lt;/code&gt; 也是死循环。两个死循环要同时跑，只能上 RTOS。&lt;/p&gt;
&lt;p&gt;CubeMX 生成的工程默认已经建了一个任务——&lt;code&gt;osThreadNew&lt;/code&gt; 创建的那个，里面跑 LCD 初始化加 LCD 测试。所以只需要再手动建第二个。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;osThreadNew&lt;/code&gt; 属于 CMSIS-RTOS，是一层统一接口。嵌入式里 FreeRTOS、RT-Thread、RTX 创建任务的函数各不相同（FreeRTOS 是 &lt;code&gt;xTaskCreate&lt;/code&gt;，RT-Thread 是 &lt;code&gt;rt_thread_create&lt;/code&gt;），于是抽出一层接口，app 只调 &lt;code&gt;osThreadNew&lt;/code&gt;，底层按不同 OS 转发。它内部判断之后，要么调 &lt;code&gt;xTaskCreateStatic&lt;/code&gt;，要么调 &lt;code&gt;xTaskCreate&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;换成原生写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static void my_task(void *argument)
{
    (void)argument;
    for (;;) {
        /* led_test(); */
    }
}

xTaskCreate(my_task,           /* 任务函数 */
            &quot;My first task&quot;,   /* 名字 */
            128,               /* 栈深度 */
            NULL,              /* 参数，本任务不需要 */
            osPriorityNormal,  /* 优先级 */
            NULL);             /* 句柄，先不用 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个参数的值是抄来的：栈深度参考默认任务的属性写 128（它那边显示的是 128×4），优先级直接用了 &lt;code&gt;osPriorityNormal&lt;/code&gt;。进 &lt;code&gt;osThreadNew&lt;/code&gt; 内部看，它把属性里的优先级取出来、做几个判断之后一路往下直接用，没做转换，所以原生函数里也能填这个宏。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; FreeRTOS 原生优先级是 0 ~ &lt;code&gt;configMAX_PRIORITIES&lt;/code&gt;−1，&lt;code&gt;osPriorityNormal&lt;/code&gt; 对应哪个数值没有明说。&lt;/p&gt;
&lt;p&gt;跑起来的效果：屏幕上数字不断累加，LED 不断闪烁。两个死循环同时运行，这就是多任务。&lt;/p&gt;
&lt;h2&gt;从 C 到汇编&lt;/h2&gt;
&lt;p&gt;创建任务要指定栈大小，函数内部按这个值分配栈；要理解栈，得先知道处理器怎么工作。&lt;/p&gt;
&lt;p&gt;组装电脑是主板插 CPU、内存条、硬盘。单片机是 SoC，一颗芯片里集成 CPU、内存和 Flash。Flash 类比硬盘存程序，CPU 跑程序，运行时用内存。&lt;/p&gt;
&lt;p&gt;有个前提必须记住：&lt;strong&gt;内存没有计算功能&lt;/strong&gt;。访问内存只有「写进去」和「读出来」两种操作，所有计算都在 CPU 内部完成。ARM 是精简指令集（RISC），CPU 对内存就只有读、写两条指令。&lt;/p&gt;
&lt;p&gt;所以 &lt;code&gt;A = A + B&lt;/code&gt; 这样一行 C 代码，拆成汇编是四步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 把 A 读进 CPU
② 把 B 读进 CPU
③ CPU 内部算 A + B
④ 结果写回内存里 A 的位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;汇编指令存在 Flash，变量值存在内存，CPU 从 Flash 取指令来执行。&lt;/p&gt;
&lt;p&gt;CPU 内部有寄存器 R0 ~ R15 和一个计算单元（字幕里的「202122~25」是 R0/R1/R2…R15 的误识别），另外还有程序状态寄存器 PSR。上面那个例子里，A 读进 R0，B 读进 R1，新值放回 R0，最后把 R0 写回内存。&lt;/p&gt;
&lt;p&gt;后三个寄存器有专门的名字：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R13 = SP   栈指针，保存栈的地址
R14 = LR   链接寄存器，保存返回地址（A 调 B，B 执行完要回到 A）
R15 = PC   程序计数器，往它写一个函数地址，CPU 就跳过去执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据传输类指令和 memcpy 一样有三个要素：源、目的、长度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LDR / LDRB / LDRH   读 4 字节 / 1 字节 / 2 字节
STR / STRB / STRH   写 4 字节 / 1 字节 / 2 字节
ADD  R0, R1, R2     R0 = R1 + R2
SUB  R0, R1, R2     R0 = R1 − R2
CMP  R0, R1         比较，结果存进 PSR
B    addr           直接跳过去
BL   addr           先记返回地址，再跳过去
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;LDR R0, [R1, #4]&lt;/code&gt; 的意思是从「R1 的值 + 4」这个地址读 4 字节存进 R0，寄存器可以换，偏移也可以改。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BL&lt;/code&gt; 分两步：先把 LR 设成返回地址（下一条指令的地址），再把 PC 设成被调函数的地址，于是跳过去执行。&lt;/p&gt;
&lt;p&gt;一共七条就够用了：LDR、STR、ADD、SUB、CMP、B、BL，读写指令各带 B、H 变种。&lt;/p&gt;
&lt;h2&gt;反汇编里看函数调用&lt;/h2&gt;
&lt;p&gt;写一个 C 加法函数，里面加 &lt;code&gt;volatile&lt;/code&gt; 故意阻止编译器优化，再加三个 &lt;code&gt;volatile&lt;/code&gt; 局部变量。然后在 Keil 的 User 标签里配一条编译后命令，用 &lt;strong&gt;fromelf&lt;/strong&gt; 把链接输出的 &lt;code&gt;.axf&lt;/code&gt; 转成 &lt;code&gt;.dis&lt;/code&gt; 反汇编文件。&lt;/p&gt;
&lt;p&gt;反汇编有三列：地址、机器码、汇编码。烧进芯片的只有机器码，汇编码只是编译器翻译给人看的。搜函数名时要注意区分大小写。&lt;/p&gt;
&lt;p&gt;调用之前先准备两个参数：第一个参数放 R0，第二个放 R1（&lt;code&gt;MOV R1, #1&lt;/code&gt;）。反汇编里能看到 &lt;code&gt;R0 = R4&lt;/code&gt;（R4 就是变量 count），然后 &lt;code&gt;BL&lt;/code&gt; 跳进函数。函数算完的新值放在 R0 返回，调用方再把 R0 写回 R4——这就是 &lt;code&gt;count = ADD(count, 1)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;PUSH&lt;/code&gt; 本质上是写内存，是 &lt;code&gt;STR&lt;/code&gt; 的变种，把多个寄存器写进栈并调整 SP。假设初始 &lt;code&gt;SP = A&lt;/code&gt;，高地址在上、低地址在下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUSH {LR, R1, R0}
  → 写 LR，SP 下移
  → 写 R1，SP 下移
  → 写 R0，SP 下移
  最后 SP = A − 12    三个寄存器占 12 字节
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以&lt;strong&gt;栈是向低地址方向生长的&lt;/strong&gt;。函数体内部还能看到 &lt;code&gt;SUB SP, SP, #4&lt;/code&gt; 再空出 4 字节、&lt;code&gt;LDRD&lt;/code&gt; 一次从 &lt;code&gt;SP+4&lt;/code&gt; 读 8 字节、&lt;code&gt;STR R0, [SP, #0]&lt;/code&gt; 把结果存进 SP+0——那就是局部变量的位置。&lt;code&gt;POP&lt;/code&gt; 反向恢复，低地址对应低编号寄存器，最后把返回地址弹回 PC，PC 一被赋值就回到了调用点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 这里 PUSH 写的是 LR、R1、R0 三个寄存器，但 POP 字幕里出现了四个寄存器，前后不一致，以 PUSH 的三个为准。&lt;/p&gt;
&lt;p&gt;一个看着挺唬人的 C 函数，拆开就是读内存、写内存、加减、跳转。&lt;/p&gt;
&lt;h2&gt;堆&lt;/h2&gt;
&lt;p&gt;堆就是一块空闲内存，从里面取一部分用，用完还回去。&lt;/p&gt;
&lt;p&gt;最简的堆实现是定义一个数组当空闲内存：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;char heap_buffer[1024];
int pos = 0;

void *my_malloc(int size)
{
    void *p = &amp;amp;heap_buffer[pos];
    pos += size;
    return p;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用一个整数 &lt;code&gt;pos&lt;/code&gt; 记空闲位置，分配一次就往后挪一次。这种写法没法实现 free——分配了 buffer(100) 和 buffer2(100)，再 &lt;code&gt;my_free(buffer)&lt;/code&gt;，&lt;code&gt;my_free&lt;/code&gt; 只拿到一个地址，不知道要释放多大。所以 &lt;code&gt;my_free&lt;/code&gt; 只能先留空。&lt;/p&gt;
&lt;p&gt;在 Keil 里点 Debug 选 Use Simulator，打断点单步看：第一次分配返回的是 &lt;code&gt;heap_buffer&lt;/code&gt; 的首地址 0x20000004，填 &lt;code&gt;for(i=0;i&amp;lt;26;i++) buffer[i]=&apos;A&apos;+i;&lt;/code&gt; 之后能看到内容。退出调试时报错，原因是工程放在中文目录，复制到非中文目录就正常了。&lt;/p&gt;
&lt;p&gt;真正的堆管理要在头部记账：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分配 100 字节，实际分配「头部 + 100 字节」
返回的指针指向头部之后
头部里存 size = 100
free 时用指针减掉头部大小找到头部，读出 size
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构体大概长这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct head {
    int size;
    struct head *next_free;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分配 100、50、100 三块，再释放中间那块，就会出现隔开的空闲区域，所以必须用链表把空闲块串起来。全局链表头初始 &lt;code&gt;size = 0&lt;/code&gt;，&lt;code&gt;next_free&lt;/code&gt; 指向第一个头部；第一个头部 &lt;code&gt;size = 1024&lt;/code&gt;，表示从这里开始有 1024 字节空闲。&lt;/p&gt;
&lt;p&gt;分配 100 时，从链表头找一块够大的，从 1024 里挖走 108（100 加 8 字节头部），剩余头部 &lt;code&gt;size = 1024 − 108&lt;/code&gt;。再分配 50，需要找 size 超过 58 的块。释放就把空闲块挂回链表。申请时从头找第一块，不够找第二块，再不够就失败——这是首次适应。&lt;/p&gt;
&lt;p&gt;堆管理有很多种实现，这里演示的是最常见的一种。头部 8 字节按 32 位算就是 size(4) + next_free(4)。&lt;/p&gt;
&lt;h2&gt;栈&lt;/h2&gt;
&lt;p&gt;栈也是一块内存，但 CPU 的 SP 寄存器指着它。函数调用、局部变量、任务切换时保存现场，都靠它。每个任务都有自己的栈。&lt;/p&gt;
&lt;p&gt;写一条 main → A → B → C 的调用链，加 &lt;code&gt;volatile&lt;/code&gt; 防优化，编译后用同样的 fromelf 命令生成反汇编。&lt;/p&gt;
&lt;p&gt;函数调用的本质就是 &lt;code&gt;BL&lt;/code&gt; 指令。main 调 A 时，LR 里存的是 A 执行完要返回的地址；A 里面调 B，又用 &lt;code&gt;BL&lt;/code&gt; 把 LR 覆盖成新的返回地址。原来那个返回地址就丢了。&lt;/p&gt;
&lt;p&gt;解决办法是在破坏 LR 之前，先用 &lt;code&gt;PUSH&lt;/code&gt; 把 LR 连同其他寄存器存进栈。所以每个 C 函数入口一般都会保存 LR。简单的、不调别的函数的函数可能不保存，取决于优化等级；但只要函数内部还会用 &lt;code&gt;BL&lt;/code&gt;，就必定保存。LR 只能存在栈里。&lt;/p&gt;
&lt;p&gt;栈帧的变化过程（设初始 &lt;code&gt;SP = A&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;main 入口    PUSH {R3~R6, LR}      SP = SP − 20   这块是 main 的栈
main 调 A    A 入口 PUSH {R0, LR}  SP = SP − 8    这块是 A 的栈
A 调 B       B 入口 PUSH {LR, R0}                 这块是 B 的栈
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; main 入口那句字幕给的是 &lt;code&gt;SP − 20&lt;/code&gt;，但四个寄存器只占 16 字节，可能还包含了栈对齐或者第五个寄存器。&lt;/p&gt;
&lt;p&gt;关键的一点：&lt;strong&gt;C 函数的栈不接在 B 下面，而是复用 B 回收的空间&lt;/strong&gt;。B 执行完 &lt;code&gt;POP&lt;/code&gt; 出栈，SP 回到调用 B 之前的位置，然后才调用 C，C 的栈从这个位置重新分配。&lt;/p&gt;
&lt;p&gt;所以每个 C 函数入口就干两件事：划分自己的栈，把 LR 和其他必要寄存器存进去。&lt;/p&gt;
&lt;h2&gt;RTOS 为什么要给每个任务一个栈&lt;/h2&gt;
&lt;p&gt;函数 B 把变量加 2 返回新值。建两个任务，代码几乎一样，都是死循环调 B 累加，但 Task A 的 &lt;code&gt;count&lt;/code&gt; 初值是 0，Task B 是 100。&lt;/p&gt;
&lt;p&gt;运行过程是 A 跑一阵、定时器中断切换、B 跑一阵、再切回 A，切换时机无法控制。切换时做的事就是保存当前任务现场、恢复另一个任务现场。&lt;/p&gt;
&lt;p&gt;现场指的是被切换那一瞬间的所有寄存器。设想 Task A 刚在 B 函数里算出 &lt;code&gt;R0 = 0 + 2 = 2&lt;/code&gt;，还没用上这个结果就被切走了。为了不浪费算好的值（这次是 R0，别的时候可能是 R1、R2），内核干脆&lt;strong&gt;把除 SP 之外的所有寄存器都保存下来&lt;/strong&gt;——它不知道什么时候会被切，也不知道哪个寄存器里有重要数据。&lt;/p&gt;
&lt;p&gt;保存的位置是任务自己的栈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Task A 运行中被定时器中断切换
  → 切换瞬间的所有寄存器存进 Task A 的栈（包括 PC：我执行到哪了）
  → 保存完，SP 指向这里
  → 这个 SP 要记进 Task A 的结构体（TCB）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以后要恢复 A，先找到 A 的结构体，从里面取出 SP 赋给硬件，再把保存的寄存器值倒回去。顺序上先恢复普通寄存器，最后恢复 PC：把 2 写回 R0，其他值各归各位，最后把 &lt;code&gt;0x0800017A&lt;/code&gt; 写进 PC，CPU 从那个地址接着跑。R0 里还是之前算好的 2，程序正确续上。&lt;/p&gt;
&lt;p&gt;一个人同时看语文和数学两本书，脑子只有一个。语文读到第 100 页累了，在第 100 页夹个书签改看数学；数学读到 200 页再夹一个，翻回语文第 100 页接着读。夹书签就是保存现场，从书签处接着读就是恢复现场。&lt;/p&gt;
&lt;p&gt;每个任务都得有自己的栈，原因有三条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;调用关系不同   每个任务的调用链不一样，B 函数执行完返回到哪，A 和 B 两个任务各不相同
局部变量不同   两个任务都调 B 函数，但初值 0 和 100 不一样，这些变量各自存在自己的栈里
现场不同       A 的现场存 A 的栈，B 的现场存 B 的栈
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;反汇编里搜函数名区分大小写，搜不到先看这一点。&lt;/li&gt;
&lt;li&gt;工程放在中文目录，Keil 退出调试会报错。&lt;/li&gt;
&lt;li&gt;只分配不回收的 &lt;code&gt;my_malloc&lt;/code&gt; 不能配 &lt;code&gt;free&lt;/code&gt;：&lt;code&gt;free&lt;/code&gt; 只拿到一个地址，不知道要释放多大，所以头部里必须存 size。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUSH&lt;/code&gt; 之后 SP 是减小的，栈向低地址生长。看反汇编时别把方向弄反。&lt;/li&gt;
&lt;li&gt;内核切换保存的是「除 SP 外的所有寄存器」，不是只保存 R0~R3。少存一个，恢复出来的现场就是错的。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>优先级、任务状态与调度</title><link>https://me.622168.xyz/posts/freertos/05-priority-states-scheduling/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/05-priority-states-scheduling/</guid><description>用音乐任务改善实验讲清抢占、时间片、阻塞、挂起、空闲任务和两个 Delay 函数的真实差别。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p22 ~ p26（约 86 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;只提高优先级会更糟&lt;/h2&gt;
&lt;p&gt;第 8 个程序，在第 7 个基础上只改两处：把音乐播放任务的优先级提高（&lt;code&gt;tskIDLE_PRIORITY&lt;/code&gt; + normal 再加 1，也就是 24 → 25），把音乐任务里的 &lt;code&gt;mdelay&lt;/code&gt; 换成 &lt;code&gt;vTaskDelay&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;只改优先级、不改延时的现象出乎意料：音乐很流畅，但 LED 不闪了（摄像头能拍到，人眼看不出），全彩 LED 停止变化，按遥控器想停音乐也没反应。&lt;/p&gt;
&lt;p&gt;原因在这个最高优先级任务一直在死循环里跑 &lt;code&gt;mdelay&lt;/code&gt;，而 &lt;code&gt;mdelay&lt;/code&gt; 本身就是个死循环（不断读定时器时间），任务从不主动放弃 CPU，于是独占了处理器，其他任务永远得不到运行。&lt;/p&gt;
&lt;p&gt;把 &lt;code&gt;mdelay&lt;/code&gt; 换成 &lt;code&gt;vTaskDelay&lt;/code&gt; 之后一切正常：音乐不卡顿，LED 继续闪、颜色继续变、遥控器也能控制音乐播放和停止。因为 &lt;code&gt;vTaskDelay&lt;/code&gt; 让任务进入阻塞，延时期间不参与调度，主动把 CPU 让了出来。&lt;/p&gt;
&lt;p&gt;所以改善播放效果靠两件事：提高优先级，加上运行中主动放弃 CPU。少一样都不行。&lt;/p&gt;
&lt;h2&gt;四种任务状态&lt;/h2&gt;
&lt;p&gt;第 9 个程序，在第 8 个基础上加了暂停和恢复：第一次按 play 创建任务，之后每次按 play 在暂停与恢复之间切换，按 power 删除任务。&lt;/p&gt;
&lt;p&gt;四种状态还是用喂饭那套类比：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Running     正在执行
Ready       随时可以执行
Blocked     在等某个事件或时刻（等同事回消息）
Suspended   单纯不想干（累了）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;任务创建出来一定是 Ready；被切出去之后回到 Ready；调 &lt;code&gt;vTaskDelay&lt;/code&gt; 是等某个时刻，进入 Blocked。&lt;/p&gt;
&lt;p&gt;进入挂起只有两条路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;自己调用 vTaskSuspend(NULL)      能自己调用，说明它此刻正在 Running
别人调用 vTaskSuspend(handle)    此时它可能在 Ready，也可能在 Blocked
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恢复用 &lt;code&gt;vTaskResume(handle)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;代码上要加一个标志记录音乐任务是不是在跑。play 键的处理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;没有任务     → 创建，标志置 1
已有任务     → 标志为 1 就 vTaskSuspend(句柄) 并清 0
               标志为 0 就 vTaskResume(句柄) 并置 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;power 键仍然是 &lt;code&gt;vTaskDelete&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现象跟删除那次一样：暂停时蜂鸣器保持一个音调，因为没人停它。暂停和恢复时都要先停止蜂鸣器，同时在屏幕上打印 &lt;code&gt;suspend&lt;/code&gt; / &lt;code&gt;resume&lt;/code&gt;（打印前先清行）。改完之后，暂停后再按 play 能从上次暂停的位置继续播放。&lt;/p&gt;
&lt;p&gt;完整的链路是这样一圈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;创建 → Ready → Running → vTaskDelay → Blocked
     → 被默认任务 vTaskSuspend → Suspended
     → vTaskResume → Ready → Running → ……
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;调度就是链表&lt;/h2&gt;
&lt;p&gt;第 10 个程序之前先把原理讲清楚：任务管理与调度的本质是链表。&lt;/p&gt;
&lt;p&gt;调度的原则可以这样推出来：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 同优先级任务轮流运行（时间片）
② 最高优先级的任务先运行
③ 高优先级任务一旦就绪就马上运行，抢占当前任务
④ 高优先级任务不让出 CPU，低优先级就永远跑不了
⑤ 多个最高优先级任务之间交叉执行
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;配置上，&lt;code&gt;configMAX_PRIORITIES = 56&lt;/code&gt;，也就是优先级 0 ~ 55。就绪链表是个数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pxReadyTasksLists[]     56 项，第 N 项存放优先级为 N 且处于 Ready/Running 的任务
tskIDLE_PRIORITY        0
priority normal         24
pxCurrentTCB            指向当前要运行的任务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建任务时，按优先级把新任务挂到对应的就绪链表；如果新任务的优先级更高，就更新 &lt;code&gt;pxCurrentTCB&lt;/code&gt;。所以后创建的 color_led 会取代先创建的那个成为「最高优先级就绪任务」——这正好回答了前面「为什么最后创建的 task3 先运行」。&lt;/p&gt;
&lt;p&gt;启动调度器时会创建空闲任务，优先级 0，比其他任务都低，&lt;code&gt;pxCurrentTCB&lt;/code&gt; 仍然指向 color_led，所以最后创建的 color_led 先运行。&lt;/p&gt;
&lt;p&gt;tick 中断的频率由 &lt;code&gt;configTICK_RATE_HZ = 1000&lt;/code&gt; 决定，也就是每 1ms 一次。中断里做三件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 计数累加
② 检查 delay 链表里的任务有没有到期，到期就移回就绪链表
③ 发起一次调度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;调度就是从高到低遍历就绪链表，找到第一个非空的，按记录的下标（&lt;code&gt;pxIndex&lt;/code&gt;）取出「下一个」任务，这样同优先级的任务才能公平轮转。&lt;/p&gt;
&lt;p&gt;音乐任务的优先级是 25，一旦创建立刻抢占——图例里任务 1 才跑了 0.1ms 也被换下去。&lt;code&gt;vTaskDelay(2)&lt;/code&gt; 的参数单位是 tick，任务会被从就绪链表移到 &lt;code&gt;xDelayedTaskList&lt;/code&gt;（有 1、2 两条，用来处理 tick 溢出）；&lt;code&gt;vTaskSuspend&lt;/code&gt; 把它移到挂起链表；&lt;code&gt;vTaskResume&lt;/code&gt; 再移回就绪链表。所有状态切换，落到代码里都是链表的增删。&lt;/p&gt;
&lt;h2&gt;空闲任务&lt;/h2&gt;
&lt;p&gt;任务函数不能直接返回。创建任务时初始化栈会伪造一个 LR，让函数返回时跳到一个错误处理函数 &lt;code&gt;prvTaskExitError&lt;/code&gt;，里面关中断然后死循环。任务切换依赖 tick 中断，中断一关，所有任务都跑不了，系统直接死机。&lt;/p&gt;
&lt;p&gt;实验很直观：让任务 &lt;code&gt;for&lt;/code&gt; 循环 10 次之后返回（不调 &lt;code&gt;vTaskDelete&lt;/code&gt;），结果是闪 10 次后死机，按遥控器屏幕毫无反应。在循环后面加上 &lt;code&gt;vTaskDelete(NULL)&lt;/code&gt;，一切正常。&lt;/p&gt;
&lt;p&gt;任务退出只有两条路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;自杀    vTaskDelete(NULL)
他杀    别的任务传句柄调用 vTaskDelete
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;收尸的规则是：A 杀 B 由 A 收拾；自杀的没人收尸，由空闲任务收。收尸就是释放动态分配的 TCB 和栈。&lt;/p&gt;
&lt;p&gt;空闲任务优先级是 0，启动调度器时创建。风险在于：如果其他任务都不主动放弃 CPU，空闲任务永远轮不到，就没法帮自杀的任务收尸；一直创建、自杀下去会把内存耗光。所以习惯上要做到事件驱动；就算写死循环，延时也要用 &lt;code&gt;vTaskDelay&lt;/code&gt; 这类阻塞函数，而不是 &lt;code&gt;mdelay&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;空闲任务有两个作用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 释放被删除任务的内存
② 保证系统永远有一个可运行任务（其他任务全阻塞时至少它还能跑）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它永远处于就绪态，要么在运行要么在等待，永不阻塞。空闲任务的函数里会检查等待终止的任务并做清理，还会调用空闲钩子 &lt;code&gt;vApplicationIdleHook&lt;/code&gt;，可以在里面打印信息、统计运行状态。&lt;/p&gt;
&lt;h2&gt;两个 Delay&lt;/h2&gt;
&lt;p&gt;第 11 个程序，基于第 6 个程序。两个 delay 的单位都是 tick；tick 中断每隔固定时间来一次，这个间隔就是 tick。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vTaskDelay(5)&lt;/code&gt; 表示从调用那一刻起等 5 次 tick 中断才变就绪，之后能不能立刻运行，要看有没有同优先级或更高优先级的就绪任务。&lt;/p&gt;
&lt;p&gt;它只保证「函数从进入到出来 ≥ 5 个 tick」，不保证周期。时间轴上看：&lt;code&gt;do_something&lt;/code&gt; 第一次跑了 1ms、第二次 10ms、第三次 5ms，启动间隔就变成大约 6ms、15ms 这样参差不齐。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;vTaskDelayUntil(&amp;amp;last_time, 15)&lt;/code&gt; 才能保证固定周期：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;循环开始前    last_time = xTaskGetTickCount()     设为 T1
执行          do_something()
调用          vTaskDelayUntil(&amp;amp;last_time, 15)      阻塞到 T1+15，即 T2
唤醒时        函数内部把 last_time 更新为 T2，任务进入就绪态
下一次        再阻塞到 T2+15
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不管 &lt;code&gt;do_something&lt;/code&gt; 耗时多久，T1 → T2 → T3 的间隔恒为 15 个 tick。关键是起始时间要提前取好并传进去，之后函数会自己更新它。&lt;/p&gt;
&lt;p&gt;实验里在 &lt;code&gt;do_something&lt;/code&gt; 中用 &lt;code&gt;count % 30 * 3&lt;/code&gt; 之类造一个长度随机的死循环，测量进出消耗的时间。&lt;code&gt;vTaskDelay&lt;/code&gt; 版打印约 499ms（设的是 500ms，很精确）；&lt;code&gt;vTaskDelayUntil&lt;/code&gt; 版因为前面随机延时不同，打印出 378、380 这样不等的值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 字幕把 API 读成「s task delay until / x task delay until」，正确名字是 &lt;code&gt;vTaskDelayUntil&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;只提高优先级不加阻塞，高优先级任务会独占 CPU，其他任务全部饿死。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mdelay&lt;/code&gt; 是死循环读定时器，不会让出 CPU；任务里要用 &lt;code&gt;vTaskDelay&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;任务函数不能返回，返回就是关中断死机。要退出就调 &lt;code&gt;vTaskDelete(NULL)&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;挂起的任务不会因为超时自动恢复，只能靠 &lt;code&gt;vTaskResume&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vTaskDelay&lt;/code&gt; 不保证周期，需要固定周期就用 &lt;code&gt;vTaskDelayUntil&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>同步、互斥与通信总览</title><link>https://me.622168.xyz/posts/freertos/06-sync-mutex-communication/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/06-sync-mutex-communication/</guid><description>从两个有缺陷的实验出发，分清同步、互斥、通信的边界，并认识队列、信号量、互斥量和事件组在游戏机项目中的分工。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p27 ~ p30（约 47 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;用厕所理解同步和互斥&lt;/h2&gt;
&lt;p&gt;同一时间只能一个人用厕所，这叫互斥。「我等你用完再用」，这是同步操作。互斥通常是用同步实现的。&lt;/p&gt;
&lt;p&gt;换成办公室的场景：同事 A 写完报表，经理 B 才能拿去汇报——B 有依赖，必须放慢脚步等，这是同步。A 已经在用会议室了，B 就算是领导也得等，于是 B 说「你用完提醒我一下」——这就是用同步实现互斥。&lt;/p&gt;
&lt;p&gt;代码里的模型是这样：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A 先抢到函数，用厕所
B 再调用，发现有人用 → 进入阻塞
A 用完 → 唤醒 B
B 从断点继续
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;被保护的「厕所」叫临界资源。&lt;/p&gt;
&lt;h2&gt;第一个缺陷：忙等浪费 CPU&lt;/h2&gt;
&lt;p&gt;把第 6 个程序改成第 12 个程序。任务一算一个大数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int32_t i;
for (i = 0; i &amp;lt; 10000000; i++) {
    sum += i;
}
计算结束 = 1;
vTaskDelete(NULL);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;算完把全局标志「计算结束」置 1，然后自杀（本来想用 delay，改成了 delete）。任务二用 &lt;code&gt;while (计算结束 == 0);&lt;/code&gt; 死等，等到之后打印 sum 和耗时。&lt;/p&gt;
&lt;p&gt;现象是任务二一直卡死。单步看汇编，发现它就在两条指令上循环：编译器第一次把变量读进 CPU 寄存器之后，循环里一直判断那个寄存器，读的是旧值，从来没重新读内存。加 &lt;code&gt;volatile&lt;/code&gt; 就好了。&lt;/p&gt;
&lt;p&gt;数值太大变成负数，改用十六进制（&lt;code&gt;0x&lt;/code&gt; 前缀）打印。耗时按纳秒算出来约 2 秒，改成毫秒（除以 10^6）得到 2500ms。&lt;/p&gt;
&lt;p&gt;真正的发现在这里：任务 B 的死等本身也在耗 CPU。两个任务同优先级、各跑 1ms 交替，B 大部分时间都在跑那个没意义的死循环。把 B 去掉、或者让它阻塞，A 只需要大约一半时间，&lt;strong&gt;1.2 秒&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;验证一下：让 B 先 &lt;code&gt;vTaskDelay(3000)&lt;/code&gt;（3 秒 = 3000 tick），A 果然约 1.2 秒完成。&lt;/p&gt;
&lt;p&gt;所以全局变量做同步有效率问题。正确做法是 A 算完之后唤醒 B，让等待的任务真正阻塞、不参与调度。&lt;/p&gt;
&lt;h2&gt;第二个缺陷：判断和设置不是原子操作&lt;/h2&gt;
&lt;p&gt;第 6 个程序创建三个任务，都调用同一个函数往屏幕上显示信息。屏幕是临界资源，同一时间只能一个任务访问。&lt;/p&gt;
&lt;p&gt;原因是这个函数通过 I2C 访问硬件，一次 I2C 操作要发 start 信号、发设备地址、读写数据、发停止信号。不加保护的话：任务 A 刚发完 start 就被切出去，切到 B，B 发了 start 和设备地址之后又被切走，于是 I2C 上出现「两个 start 之后才发设备地址」，时序全乱，数据错乱。&lt;/p&gt;
&lt;p&gt;有缺陷的写法是用一个全局变量保护：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static int b_can_user = 1;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 A 运行到第 108 行（此时变量 = 1）被切换，还没把变量清零；B 运行也成功通过了判断。B 用 LCD 之前确实把变量清零了，但没用，因为 A 下次是从断点继续的。再假设 B 用到一半被切，轮到 A，A 又清一次变量，A 也能用 LCD——A、B 同时用了 LCD。&lt;/p&gt;
&lt;p&gt;这个切换点必须落在「判断变量」和「设置变量」之间，恰好发生 tick 中断的概率极低，所以绝大多数情况不出问题，但程序跑成千上万次就可能踩中。本质是：判断与设置不是原子操作，两个动作之间的时间太长。&lt;/p&gt;
&lt;p&gt;文档里有个改进版：一进来先把变量减一。初始值 1，减到 0 表示之前没人用，可以用 LCD；B 再执行发现已经等于 0，减一下变成 1，条件不成立，用不了。&lt;/p&gt;
&lt;p&gt;但 &lt;code&gt;b_can_user--&lt;/code&gt; 会被编译成三条汇编：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 读变量到寄存器
② 寄存器减一
③ 写回内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 执行到 ① 之后被切（r0 = 1），B 读到变量 = 1、减到 0 后开始用 LCD，用到一半被切；A 恢复时现场里 r0 还是 1，减一得 0 写回，变量又变成 0，A 的判断也成立——A 也用上 LCD 了。所以减一也杜绝不了，根因是「读—改—写」这个过程可以被打断。&lt;/p&gt;
&lt;p&gt;两个解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 关中断
   判断 + 修改变量之后再开中断。关中断期间不会发生任务切换。
   如果关中断之后发现 LCD 已被占用，就开中断、返回失败。
② 用 FreeRTOS 提供的函数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关中断的不足在于：A 在打印的时候，B 不断尝试、不断失败（每次返回 −1）又变回死等，CPU 利用率很低。期望的效果是 B 用不上 LCD 就阻塞，A 用完再唤醒 B——这要靠后面的信号量和互斥量。&lt;/p&gt;
&lt;h2&gt;FreeRTOS 提供了什么&lt;/h2&gt;
&lt;p&gt;通信本身很简单：第 12 个程序就是 A 把结果放全局变量、B 去读。问题在于 A 写一半没写完 B 就来读，会读到错误的值。&lt;/p&gt;
&lt;p&gt;所以「怎么通信」不重要，重要的是保证通信结果正确，这需要互斥；除此之外还要高效率，比如写一个很大的数组时，读方应该阻塞等待。&lt;/p&gt;
&lt;p&gt;FreeRTOS 的思路是：先用互斥保证正确性，再提供阻塞和唤醒机制提高效率。四个工具先混个脸熟：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;队列      传送带/流水线。生产者往上放产品，消费者从上面取，
          先进先出，先生产先取。一个对多个、多个对一个都行。

事件组    一个整数，每一位代表一种事件。生产者把某位置 1 表示事件完成；
          消费者可以等某一个、某几个事件，或者「若干事件中任意一个」。多对多。

信号量    里面放的不是商品，是一个计数值。手工作坊做饺子：产出一个 +1，
          买走一个 −1。计数值限制成只有 1 或 0，信号量就变成互斥量，
          用来保护临界资源。用互斥量会引入优先级反转，
          于是又有优先级继承来解决。

任务通知  多对一。多个任务通知某一个任务，可以传一个数字，
          也可以只通知「某事件发生了」。
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;游戏机项目&lt;/h2&gt;
&lt;p&gt;游戏来自开源的 STM32 手表项目 Nwatch &lt;code&gt;[待确认]&lt;/code&gt;，里面有三个游戏，官方源码已经移植到 DShanMCU-F103。&lt;/p&gt;
&lt;p&gt;开发板资料的「程序源码/freertos」下面有三个程序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 裸机程序，只有一个游戏
② FreeRTOS 程序
③ 完整的 Nwatch（裸机）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完整的那个按中间三角切菜单、左右键移动菜单、遥控器控制挡球板。第二个程序功能简陋、只支持遥控器、没有音效，后面就在它基础上改。&lt;/p&gt;
&lt;p&gt;游戏的核心是画位图和刷位图。OLED 是 128×64 点阵，位图数据按「一个字节等于 8 行像素，低位在上、高位在下」组织：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0x60 = 0110 0000
0x70 = 0111 0000
1 点亮，0 熄灭，12 列拼出挡球板
球是 0x03 = 0000 0011
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;汽车更复杂一点，0x40、0x02、0x01 这些，一行数据对应 8 行像素，整组数据搬到 OLED 显存就显示出来了。&lt;/p&gt;
&lt;p&gt;game1 的主循环是这样跑的：先隐藏球（在球的 XY 处写全零改显存），再通过 I2C 把显存刷到 OLED；移动球之后判断有没有撞砖块（撞了就把砖块位置清零并刷新）、撞墙壁（X 方向加速度反向）、撞挡球板，然后在新的位置画球。&lt;/p&gt;
&lt;p&gt;改造目标（第 34 个程序）要接的模块：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;红外接收     遥控器左右键控制挡球板
无源蜂鸣器   外壳没有白纸的是无源，有白纸的是有源，只有无源才能发游戏音
温湿度模块   接它就要拔掉 DS18B20，两者只能同时接一个
陀螺仪       姿态控制
旋转编码器   左旋/右旋控制挡球板
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接好之后，晃动板子、遥控器、旋转编码器都能控制挡球板，球撞边会发声。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 红外接收头和旋转编码器的具体型号与接线，字幕里没有给出。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;全局变量做同步，等的一方如果在死循环里轮询，会白占 CPU，把对方拖慢一倍。&lt;/li&gt;
&lt;li&gt;编译器可能把变量缓存进寄存器，轮询标志位时要加 &lt;code&gt;volatile&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;「判断 + 设置」两个动作不是原子的，中间随时可能被切换打断。&lt;/li&gt;
&lt;li&gt;减一那种写法也救不了，因为 &lt;code&gt;--&lt;/code&gt; 本身就是读、改、写三条汇编。&lt;/li&gt;
&lt;li&gt;关中断能解决问题，但等待方会反复失败重试，效率很低。真正需要的是阻塞和唤醒。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>队列、环形缓冲与多设备输入</title><link>https://me.622168.xyz/posts/freertos/07-queue-game-input/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/07-queue-game-input/</guid><description>从环形缓冲区讲到 FreeRTOS 队列，完成红外、旋转编码器和按键共同驱动游戏机的输入链路。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p31 ~ p36（约 103 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;环形 buffer&lt;/h2&gt;
&lt;p&gt;任务 A、B 都是函数，和裸机不一样，可以「同时」执行。传数据有三种方法，先看环形缓冲区。&lt;/p&gt;
&lt;p&gt;用全局变量或结构体传数据的毛病：一个变量只传一个数据，结构体也只传一个结构体本身，数量只有 1；没有保护措施，也没有阻塞唤醒，效率低；还可能传错。&lt;/p&gt;
&lt;p&gt;传错的例子很典型：结构体 &lt;code&gt;{int x; int y;}&lt;/code&gt; 作为全局变量，A 把 X 改成 1、Y 改成 2。如果 A 改完 X 就被切出去，B 拿到的是新 X 配旧 Y，数据就是错的。&lt;/p&gt;
&lt;p&gt;环形缓冲区的本质就是一个数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int buffer[8];
读位置 R = 0;
写位置 W = 0;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判空很简单：&lt;code&gt;R == W&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;写的时候先判未满，&lt;code&gt;buffer[W] = 值&lt;/code&gt;，W 累加，溢出到 8 就归 0。读的时候 &lt;code&gt;R != W&lt;/code&gt; 表示有数据，&lt;code&gt;value = buffer[R]&lt;/code&gt;，R 累加，越界归 0。&lt;/p&gt;
&lt;p&gt;麻烦的是判满。写位置追上读位置的时候是满，但这和「空」的状态撞了，所以牺牲一个空位——W 指向 7 的时候就不写这一格，认为已经满了，也就是 8 格最多只写 7 个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;next_w = W + 1;
if (next_w == 8) next_w = 0;
判满   next_w == R
未满   next_w != R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写函数里就是「下一个写位置不等于读位置就可以写」。&lt;/p&gt;
&lt;p&gt;那为什么不干脆加一个计数变量 &lt;code&gt;int number&lt;/code&gt;？可以，但没有保护会出问题。&lt;/p&gt;
&lt;p&gt;有缺陷的写法是这样：除了 buffer 和读写位置再加一个 number。写的时候 number != 8 表示未满，填数据、number 累加；读的时候 number != 0 表示有数据，读数据、number 减减。&lt;/p&gt;
&lt;p&gt;问题在于 number 是全局变量，生产者只管累加，消费者只管减减，两个人都要改它。&lt;code&gt;number++&lt;/code&gt; 会被拆成三条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 读入寄存器
② 寄存器累加
③ 写回内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 number = 10，A 想累加成 11。A 执行到 ① 被切走（r0 = 10），B 读到数据把 number 从 10 减到 9（buffer 里剩 9 个），切回 A，r0 还是 10、累加得 11 写回。本该是 10 → 11 → 10，结果停在 11，个数错了。&lt;/p&gt;
&lt;p&gt;结论很明确：不要加这种被双方共改的计数值。改用读写位置判断——生产者只改写位置，消费者只改读位置，没有变量被同时访问，就不会出这个问题。&lt;/p&gt;
&lt;p&gt;如果只有两个任务，也不关心阻塞唤醒和效率，可以用这种环形缓冲区。切记别加计数值。&lt;/p&gt;
&lt;h2&gt;队列的本质&lt;/h2&gt;
&lt;p&gt;队列内部读写数据用的就是环形缓冲区，只是在它上面加了保护措施和阻塞唤醒机制。正因为有保护，内部才敢同时用「读位置、写位置、数据个数」三个量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;写队列   把数据拷进去，计数累加
读队列   把数据取出来，计数减一
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果队列只改计数、不传数据，就成了信号量；把计数上限限制为 1，就成了互斥量。三者本质都是队列。&lt;/p&gt;
&lt;p&gt;操作队列三步：创建、写、读。阻塞和超时可以用流水线来理解：工人 A 往流水线上放产品，工人 B 从上面取。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;流水线上没产品     B 眯一会儿（阻塞），但定了闹钟（超时）
A 放入产品         敲一下流水线把 B 震醒（唤醒）
A 一直不放         闹钟响（tick 中断超时唤醒）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对应到程序里，读队列函数是带 timeout 的。有产品立即返回；没产品就阻塞，比如阻塞 30 分钟，第 15 分钟 A 写队列并唤醒它；如果 A 一直不写，tick 中断不断产生、判断时间到了把它唤醒。写队列也一样，队列可能已经满了（它就是个环形缓冲区），满了可以立即返回错误，也可以阻塞睡一会儿，由取走产品的同事敲流水线（有空位了）或者 tick 超时来唤醒。&lt;/p&gt;
&lt;p&gt;队列的本质是三样东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 一个环形 buffer
② send list      想写但队列满而阻塞的发送者
③ receiver list  想读但队列空而阻塞的接收者
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A 写队列的时候并不知道该唤醒谁，它只是「敲流水线」，链表里记着 B 呢。&lt;/p&gt;
&lt;p&gt;以任务 B 为例看状态流转：B 创建后处于就绪态，挂在优先级 10 的那条就绪链表上。B 运行到读队列，队列没数据而且它愿意等，就把自己从就绪链表移除，放进队列的 receiver list；同时因为指定了超时时间，还被挂到延时链表上。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;情况一  A 写队列 → 数据存进环形 buffer → 去队列的链表看，非空就取第一个任务唤醒
        （把 B 移出 receiver list，放回就绪链表）→ B 拿到数据继续处理

情况二  B 一直阻塞，没人写 → tick 中断反复判断延时链表里的任务是否超时
        → 超时就把它从链表删除、移入就绪链表
        → B 运行后从函数返回一个错误值（没取到数据，是超时被唤醒的）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;知道内部是怎么实现的，用起来心里更有底。&lt;/p&gt;
&lt;h2&gt;改成队列：红外遥控&lt;/h2&gt;
&lt;p&gt;改成第 13 个程序。整个框架里用户任务只有两个：球任务（&lt;code&gt;game1task&lt;/code&gt;）和挡球板任务。球任务的主循环每次让球移到下一个位置，判断撞墙、撞砖块、撞挡球板——这部分基本不用改。要改的是挡球板任务。&lt;/p&gt;
&lt;p&gt;挡球板任务通过一个函数取红外键值，这个函数读的是一个环形缓冲区（驱动在 &lt;code&gt;driver/103&lt;/code&gt; 的红外遥控器部分）。谁往这个 buffer 里写？红外接收中断服务程序，解析出数据之后写进去。&lt;/p&gt;
&lt;p&gt;环形 buffer 没有阻塞唤醒，任务只能一直循环尝试读，读不到再读，无阻塞、低效率。&lt;/p&gt;
&lt;p&gt;改造思路：把这个函数改成读队列 A。红外中断解析出数据后不再写环形缓冲区，改写队列 A。&lt;/p&gt;
&lt;p&gt;为什么还要加旋转编码器？红外遥控器一个按键的最长数据是「引导码 + 数据」，耗时 &lt;strong&gt;85ms&lt;/strong&gt;（接近 100ms），反应快的人会觉得非常迟钝，玩游戏很难受，经常要长按左右键挡球板才快速移动。旋转编码器快得多。&lt;/p&gt;
&lt;p&gt;旋转编码器的驱动也是在中断里解析的（判断逆时针还是顺时针）。可以让它也写同一个队列，但这里刻意让它先写队列 B，再由一个「旋转编码器任务」读队列 B、处理之后写队列 A。&lt;/p&gt;
&lt;p&gt;为什么要多这么一个任务？简单的事可以在中断里做，复杂的事必须放到任务里——中断里执行太久会卡住其他中断，拖低系统性能。&lt;/p&gt;
&lt;p&gt;编码器这块要处理的是：转动产生中断，即使只中断一次，信息量也不一样。驱动在中断里记录了 count（转一下 +1 或 −1）和速度（数值变化的速度），&lt;strong&gt;速度才是关键&lt;/strong&gt;。目标是转得慢就慢慢调整挡球板，转得快即使只中断一次也要让挡球板多移几格。&lt;/p&gt;
&lt;p&gt;所以要做两件事：分辨速度；按速度决定往队列 A 写多少个数据——快就多写，慢就少写，数据个数决定挡球板移动速度。程序里每次调整 X 都是加 3 或减 3，值固定；队列里数据多，就能每次循环都取到，移动更快。&lt;/p&gt;
&lt;p&gt;把处理放到任务里只是个例子，真正要引出的是「中断里写队列」和「任务里写队列」用的函数不一样。改用队列之后，读不到数据会阻塞，阻塞时其他任务能跑，CPU 效率提高了。&lt;/p&gt;
&lt;h2&gt;红外改造的实现&lt;/h2&gt;
&lt;p&gt;第 13 个程序，用队列传数据。效果是左右两键控制挡球板，插上蜂鸣器还能一边听音乐一边玩。&lt;/p&gt;
&lt;p&gt;三步：创建队列、红外中断写队列、挡球板任务读队列（原来是读环形缓冲区）。&lt;/p&gt;
&lt;p&gt;创建队列可以用动态分配 &lt;code&gt;xQueueCreate&lt;/code&gt;，也可以用静态分配——静态版要提供两个 buffer：环形缓冲区本身加队列结构体。在 game1 里创建，定义全局的 &lt;code&gt;QueueHandle_t&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xQueueCreate(队列元素个数, 每个元素大小)
xQueueCreate(10, sizeof(struct input_data))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;元素用结构体，在 &lt;code&gt;game_type.h&lt;/code&gt; 里定义，包含 device 和 value 两项，都是 32 位。为精简代码没判返回值。&lt;/p&gt;
&lt;p&gt;读用 &lt;code&gt;xQueueReceive&lt;/code&gt;，返回 &lt;code&gt;pdPASS&lt;/code&gt; 表示读到了。定义一个 &lt;code&gt;struct input_data idata&lt;/code&gt; 传地址进去，timeout 用「永远等待」（&lt;code&gt;portMAX_DELAY&lt;/code&gt;），一直等到有数据为止。为了不改动后面的代码，取 &lt;code&gt;data = idata.value&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;写在红外接收中断回调里，把原来写环形缓冲区改成写队列。队列在 game1 里创建，中断文件里要 &lt;code&gt;extern&lt;/code&gt; 声明。&lt;code&gt;xQueueSend&lt;/code&gt; 传入数据地址，不等待，队列满就直接返回错误——中断里不允许等待。解析出有用数据的地方：&lt;code&gt;d.device = data[0]&lt;/code&gt;、&lt;code&gt;d.value = data[2]&lt;/code&gt;，然后写队列。&lt;/p&gt;
&lt;p&gt;调试过程踩了两个坑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;编译报错「不认识 Q」     → 要包含 queue.h
input_data 未定义        → 它在 game_type.h 里，复制过来并添加头文件路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;烧写后一按键就卡死、单步跑飞。原因是写队列用了 &lt;code&gt;xQueueSend&lt;/code&gt;，但这句在中断里，必须改成 &lt;code&gt;xQueueSendFromISR&lt;/code&gt;（另一个写队列的地方也要加这个后缀）。改完就能用遥控器控制挡球板了。&lt;/p&gt;
&lt;p&gt;最后验证音乐：main 里原先没有启动默认任务，而音乐播放任务是在默认任务里创建的。现在把 music 任务打开，播放果然很流畅，同时还能用遥控器控制，实验目的达到。&lt;/p&gt;
&lt;h2&gt;旋转编码器&lt;/h2&gt;
&lt;p&gt;在第 13 个程序基础上改出第 14 个程序，加入旋转编码器控制，比红外灵敏得多。&lt;/p&gt;
&lt;p&gt;结构是：中断写队列 B → 旋转编码器任务读队列 B、处理之后写队列 A。为什么不像红外那样直接写队列 A？因为原始数据要先运算才能变成控制挡球板的动作，这里假设这个转换很耗时（不一定，但为了引出新技术），所以放到任务里做。&lt;/p&gt;
&lt;p&gt;队列 B 刻意改用静态创建 &lt;code&gt;xQueueCreateStatic&lt;/code&gt;，长度取 10，要提供两个 buffer：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;static ... rotary_buf[10];      存放 10 个结构体的 data buffer
static struct ... ;             队列结构体 buffer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据结构体 &lt;code&gt;rotary_data&lt;/code&gt; 有两项：&lt;code&gt;int count&lt;/code&gt;（当前计数，变大还是变小）和 &lt;code&gt;speed&lt;/code&gt;（速度）。&lt;/p&gt;
&lt;p&gt;写源头在旋转编码器驱动里（仿照红外接收驱动写，要包含三个头文件）。中断回调里用 extern 取到队列，构造 &lt;code&gt;rotary_data&lt;/code&gt;（count = 计数值、speed = 速度）写进队列 B。&lt;/p&gt;
&lt;p&gt;任务里循环做三件事：读队列 B（无限等待，只有读到才处理）、处理数据、写队列 A。写队列 A 用 &lt;code&gt;xQueueSend&lt;/code&gt;、不阻塞，而且应该放在循环里——转得快就写多次，慢就写一两次。&lt;/p&gt;
&lt;p&gt;处理逻辑看 &lt;code&gt;rdata.speed&lt;/code&gt; 判断方向：负数向左转，正数向右转。小于 0 就把 left 置 1，否则置 0，速度取绝对值。速度分级一开始写的是 &lt;code&gt;if (速度 &amp;gt; 100) count = 3; else count = 1;&lt;/code&gt;，又试过 &lt;code&gt;count = 速度 / 10&lt;/code&gt;，如果算出来是 0 至少上报一次。然后 &lt;code&gt;for (i = 0; i &amp;lt; count; i++)&lt;/code&gt; 往队列 A 写，&lt;code&gt;idata.device&lt;/code&gt; 随便写 1，&lt;code&gt;idata.value&lt;/code&gt; 按左/右取相应键值（这里暂时借用红外键值，有点别扭，先调通）。&lt;/p&gt;
&lt;p&gt;现象是编码器能移动、比红外快，红外也还能用，目的达到。但程序很怪异——处理结果跟红外挂钩，因为队列 A 沿用的还是红外的数据格式。&lt;/p&gt;
&lt;p&gt;于是改造队列 A：value 只关心「左/右」，不关心来自红外的哪个键。在 game 里定义宏（value 取 无/left/right），让红外和编码器都转换到这套值，跟红外解耦。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;红外中断里   左键 = 0x10，右键 = 0x90
             等于 0x10 则 value = left，等于 0x90 则 value = right
编码器任务里 左取 left，否则取 right
挡球板任务   把原来按键函数设置的变量改成直接取 data.value
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;红外还有重复码：重复码表示左还是右，要看上一次的值。用 &lt;code&gt;static int32_t last_value&lt;/code&gt; 记录上次的 value，重复码时上报上一次的按键。&lt;/p&gt;
&lt;p&gt;编译时括号和逗号有错，改掉之后遥控器又能用了。这时编码器「太飘忽」，时快时慢不好控制。把 &lt;code&gt;count = 速度 / 10&lt;/code&gt; 改成分级限制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;速度 &amp;gt; 100  → count = 4
速度 &amp;gt; 50   → count = 2
否则        → count = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不让 count 太大。再烧写，控制很漂亮——想快就快转，想准就慢下来，既快又准。&lt;/p&gt;
&lt;h2&gt;勘误：旋转编码器不好用&lt;/h2&gt;
&lt;p&gt;很多同学反馈旋转编码器不好用，现象是一直想往左滑，它就是不动。这一集是录完全部课程之后补录的，后续源码按它改。&lt;/p&gt;
&lt;p&gt;问题出在旋转编码器代码里的延时，它根本消除不了抖动。用波形看：如果跳变时间小于 2ms，后面确实会稳定；但如果这段跳变时间大于 2ms，你只是在那里等，等够 2ms 之后，后面那些抖动跳变的波形仍然会被当成有效脉冲。&lt;/p&gt;
&lt;p&gt;改法是不用延时，改用时间差判抖：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;当前中断时间 − 上次中断时间 &amp;lt; 2ms  → 判为抖动，直接返回不处理
                            ≥ 2ms  → 往下处理
同时把当前中断时间记录下来，更新上次中断时间戳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;2ms = 2000 微秒 = 2 000 000 纳秒。&lt;/p&gt;
&lt;p&gt;第二处：算速度的时候要做除法，如果时间足够大，除出来的值可能是 0。结果为 0 时给一个较小的速度值（比如 1），避免出现 0 速度。&lt;/p&gt;
&lt;p&gt;改进就这两点：防抖，以及速度为 0 时给最小值。烧写后滑动顺畅，不再出现怎么滑都不动。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;环形缓冲区不要加被生产者和消费者共改的计数值，&lt;code&gt;number++&lt;/code&gt; 的读—改—写会被打断。&lt;/li&gt;
&lt;li&gt;判满只能靠牺牲一个空位：8 格的 buffer 最多写 7 个。&lt;/li&gt;
&lt;li&gt;中断里写队列必须用 &lt;code&gt;xQueueSendFromISR&lt;/code&gt;，用 &lt;code&gt;xQueueSend&lt;/code&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 时给个最小值。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>队列集、姿态控制与赛车游戏</title><link>https://me.622168.xyz/posts/freertos/08-queue-set-racing/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/08-queue-set-racing/</guid><description>当一个任务要同时等多个来源时，队列集如何工作；以及赛车项目中如何把数据分发到多个任务。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p37 ~ p40（约 136 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;第 14 个程序的毛病&lt;/h2&gt;
&lt;p&gt;红外遥控的中断函数解析出按键值之后，直接把按键翻译成「游戏左移/右移」再写队列——硬件驱动里掺进了游戏业务。以后不做游戏机、改成电视机遥控，这段代码就不能用了，得重写。&lt;/p&gt;
&lt;p&gt;对比一下旋转编码器的驱动：它只把「硬件相关的数据」写进自己的队列，由上层任务决定怎么用，这才纯粹。&lt;/p&gt;
&lt;p&gt;但「一个硬件一个任务」也有代价：每加一个设备（比如 MPU6050）就多一个任务、多一份任务栈，内存浪费很大。&lt;/p&gt;
&lt;h2&gt;一个任务读多个队列&lt;/h2&gt;
&lt;p&gt;改进后的框架是这样：底层三个硬件驱动各自在中断里把硬件数据写进自己的队列（队列 1/2/3，跟业务无关），上面只留一个 InputTask，负责读多个队列、解析、转成业务数据（游戏控制）、再写挡球板队列。以后加硬件，只需要在这一层加一个判断分支。&lt;/p&gt;
&lt;p&gt;一个任务读多个队列有两种办法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;轮询&lt;/strong&gt;：依次 &lt;code&gt;xQueueReceive&lt;/code&gt; 队列 1、队列 2、队列 3。读队列 1 的时候超时只能设 0——一旦设长超时（比如 30 分钟），队列 2、3 就会被推迟处理。结果就是任务永远不阻塞、空转烧 CPU。理论上能及时读到三个队列，效果不好。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;队列集&lt;/strong&gt;：队列集本身也是个队列，只不过里面装的是队列句柄。若 A 队列深 2、B 队列深 3，队列集大小就是 2 + 3 = 5（最坏情况两个队列都写满，集里就有 5 个句柄）。用起来四步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 创建队列 A、B
② 创建队列集 S
③ 把 A、B 加入 S          xQueueAddToSet
④ 任务里读 S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内部机制值得记一下：往队列 A 写数据的时候，写队列函数会判断 A 是不是属于某个队列集，属于就顺便把 A 的句柄也写进队列集。所以 InputTask 读队列集就能立刻拿到「有数据的那个队列的句柄」，再拿句柄去读该队列拿到真正的数据——读两次，先读句柄，再读数据。不管有多少个队列，InputTask 只读队列集就够了。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 这一集字幕里的「169」「平板队列」识别不清。&lt;/p&gt;
&lt;h2&gt;把框架改干净&lt;/h2&gt;
&lt;p&gt;第 15 个程序，复制第 14 个改。&lt;/p&gt;
&lt;p&gt;驱动层要改三处：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;红外驱动不再把键值翻译成游戏键，原原本本上报硬件数据
  原来的 input_data 改名成红外数据结构体，放到红外驱动的头文件里
  键值用 IR_KEY_POWER 这样的宏代替裸数字
  中断里得到重复码就上报重复码，解析出数据就上报 data0 / Value 等原始值，
  不再记录「上一次的值」——那是上层的事

在红外驱动的初始化函数里创建自己的队列
  static，长度用宏 IR_Q_LEN = 10，元素大小就是结构体大小
  用静态创建 xQueueCreateStatic，buffer 一并搬过来

驱动不暴露 static 变量，改成提供 Get 函数返回句柄
  game 里定义全局变量 xq_ir、xq_encoder，调这些函数取得句柄
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;旋转编码器同理：把原来放在游戏文件里的结构体挪回编码器头文件，队列改在编码器驱动里创建。&lt;/p&gt;
&lt;p&gt;上层（game 文件）做四件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;创建队列集 xQueueCreateSet(长度)
  长度必须 ≥ N + M（两个队列都写满时的句柄总数）
  = IR_Q_LEN + 编码器长度 = 20

创建输入任务 InputTask

把两个队列 xQueueAddToSet 加入队列集

InputTask 循环：
  xQueueSelectFromSet（超时用 portMAX_DELAY，因为没数据就没法处理）
  → 得到句柄
  → 等于红外句柄就调处理红外函数，否则如果等于编码器句柄就调处理编码器函数
  → 处理函数把硬件数据转成游戏控制数据，写挡球板队列
  → 处理函数里 xQueueReceive 超时用 0（「肯定会读到」）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;红外处理里按键等于左就是 left、等于右就是 right，其他无效；重复按键沿用上一次的值（用 static 记）。编码器处理按速度决定 for 循环上报多少个数据。&lt;/p&gt;
&lt;p&gt;编译和调试踩了四个坑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;队列集函数未定义
  → 要配 configUSE_QUEUE_SETS = 1
  → CubeMX 里没有队列集配置项，直接改 FreeRTOS 源文件在重新生成后会被恢复成 0
  → 要放在配置文件的 USER CODE 区加 define

CubeMX 重新生成会恢复被注释掉的默认任务
  → 改成 vTaskDelete(NULL) 让它自杀

堆不够，挡球板不显示
  → 从默认值调大到 8000（多了一个队列和一个队列集）

遥控器不生效
  → 调试发现上报的是原始数据，应该上报转换后的游戏数据
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现象和第 14 个程序一致，只是框架更漂亮。&lt;/p&gt;
&lt;h2&gt;加上姿态控制&lt;/h2&gt;
&lt;p&gt;第 16 个程序，用 MPU6050 加姿态控制：摇晃板子控制挡球板，遥控器和旋转编码器仍然可用。这一集专注 FreeRTOS，MPU6050 怎么取数、怎么算角度不细讲。&lt;/p&gt;
&lt;p&gt;MPU6050 的接口是 I2C，读出 XYZ 加速度和角速度之后解析成结构体，这里只解析 X 方向的角度。&lt;/p&gt;
&lt;p&gt;先做个实验验证角度基准：只留默认任务，在默认任务的死循环里每 100ms（&lt;code&gt;vTaskDelay&lt;/code&gt;）读一次，用 LCD 打印 X 方向的角度。结果是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;水平放置    ≈ 90°
左边高      &amp;lt; 90°
左边低      &amp;gt; 90°
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;据此就能判断左右倾斜了。&lt;/p&gt;
&lt;p&gt;思路分三步：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① MPU6050 没有接中断
   I2C 很慢，即使将来有中断，中断里也不该读 I2C
   应该只唤醒任务，由任务去读 I2C

② 任务里 while 循环：
   读 I2C → 解析 → 写自己的队列（写队列时会自动把句柄写进队列集）
   → vTaskDelay，延迟 50ms
   （不能老是读，会影响别的程序）

③ 把这个队列加进队列集；InputTask 里增加「句柄等于 MPU6050 队列」的分支
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码细节：驱动注册函数里创建 MPU6050 队列（宏 &lt;code&gt;MPU6050_Q_LEN = 10&lt;/code&gt;），并提供函数返回句柄。硬件初始化不放注册函数里，而是放在 FreeRTOS 入口直接初始化——因为注册函数什么时候运行不确定，而 game 任务需要马上拿到队列。&lt;/p&gt;
&lt;p&gt;任务里读数据的代码照搬测试代码，只传 X 方向变量，其余传 NULL；解析后用 &lt;code&gt;xQueueSend&lt;/code&gt; 写队列（任务里写，超时 0）。&lt;/p&gt;
&lt;p&gt;game 里取句柄、加入队列集（队列集长度相应加大）、创建 MPU6050Task、InputTask 增加分支；处理函数读队列得到角度：大于 90° 挡球板左移（value = left），小于 90° 右移，等于 90° 不动。&lt;/p&gt;
&lt;p&gt;这一节有两个 bug 值得记。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;摇不动&lt;/strong&gt;。数据源确实在写队列，但没唤醒 InputTask。原因是任务创建顺序：先建 game 任务、紧接着建 MPU6050Task；6050 任务每 50ms 写一次，此时队列还没被加入队列集，很快就写满了；等 game 任务运行到 &lt;code&gt;xQueueAddToSet&lt;/code&gt; 的时候队列已经满了，之后再也写不进队列，也就不会再写队列集。修正办法是先让 game 任务把队列加进队列集，再创建 MPU6050Task。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;玩久了出现两个挡球板（残影）&lt;/strong&gt;。MPU6050 和屏幕都走 I2C，没有互斥。临时办法是把屏幕刷新函数用的静态标志变量提为全局，MPU6050 那边用 extern 引用，等于 1 表示别人正在用 I2C 就死循环等待，用完置 0。这里只是用全局变量凑合，并不可靠，讲互斥量之前先这么用着。&lt;/p&gt;
&lt;h2&gt;赛车游戏：把数据分发给多个任务&lt;/h2&gt;
&lt;p&gt;第 17 个程序，基于第 16 个改，是个简陋版赛车游戏，也是后面讲信号量、互斥量的基础。&lt;/p&gt;
&lt;p&gt;效果是遥控器 1/2/3 分别控制三辆小车右移，移到最右就不再动。&lt;/p&gt;
&lt;p&gt;素材来自开发板配套资料：05 程序源码 / 02 FreeRTOS 参考源码 / DShanMCU-F103，解压最后一个程序，取里面 watch 目录的 &lt;code&gt;game2&lt;/code&gt; 加进工程，只保留汽车位图和路标位图，其余删掉。&lt;/p&gt;
&lt;p&gt;显示上的细节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;汽车位图 car image   每行 15 字节，宽 15、高 16（2 行）
                    清除一辆汽车用 30 个 0 覆盖，所以 clean_image 取 30、初值 0
路标位图             宽 8、高 1（只用一行数据）
LCD 分辨率           64×128
绘制用 draw bitmap   （XY 坐标、位图、宽、高、是否反转、offsetY）
车道                 路标 Y 坐标 16、33、50（间隔 17）
                     每行路标画 8 段（128÷8=16 段会首尾相连不好看，只画 8 段）
                     X = 16 × Z，Z &amp;lt; 8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先写个测试程序 &lt;code&gt;car test&lt;/code&gt;：初始化显存 → 画一辆车（0,0，15×16）→ 画一个路标（X=0, Y=16）→ 死循环。验证之后再画三辆车（X 都从 0 起，Y 累加）。&lt;/p&gt;
&lt;p&gt;分发数据是这一节的核心。红外中断解析出键值之后只写一个队列，但有三个 car task。普通队列一个消费者取走就没了，另外两个任务拿不到，所以不能用「一个队列多消费者」。&lt;/p&gt;
&lt;p&gt;方案是让驱动层分发，同一个键值写三次队列（三辆车各一条队列）。&lt;/p&gt;
&lt;p&gt;半成品的写法是分发函数 &lt;code&gt;dispatch_key&lt;/code&gt; 里直接写 car1、car2、car3 三个队列，代码丑陋、和汽车业务强耦合，加一条队列就要改代码。&lt;/p&gt;
&lt;p&gt;改进版是提供注册函数 &lt;code&gt;Register&lt;/code&gt;：任务把自己的队列句柄传进来，驱动用一个数组（&lt;code&gt;xq[10]&lt;/code&gt;）记录，配一个 &lt;code&gt;static int count&lt;/code&gt;；分发的时候 for 循环遍历 count 逐条写队列。驱动自己也创建一个默认队列并注册进去。&lt;/p&gt;
&lt;p&gt;任务那边：先创建自己的队列（长度 10）→ 注册 → 读队列（永久等待）→ 按按键值控制小车。&lt;/p&gt;
&lt;p&gt;汽车任务的结构体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct car {
    int x;
    int y;
    int key_control_key;
    /* 队列 */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三辆全局变量 cars，键用 IRK1/IRK2/IRK3，坐标 (0,0)/(0,17)/(0,34)。手工创建三个任务而不用 for 循环，因为以后各任务的优先级要不一样。&lt;/p&gt;
&lt;p&gt;移动的逻辑：先隐藏旧车（同位置画 &lt;code&gt;clean_image&lt;/code&gt;）→ 调整位置（X 每次加 20，按五六次就能到最右；越界就钳到 128 − 15 = 113）→ 重新显示。条件是位置小于 X 分辨率(128) − 车宽(15) 才移动。&lt;/p&gt;
&lt;p&gt;一开始就要把三辆车显示出来，否则按下按键才出现，很别扭。&lt;/p&gt;
&lt;p&gt;完成后把 &lt;code&gt;car test&lt;/code&gt; 改名 &lt;code&gt;car game&lt;/code&gt;，main 里去掉测试代码，改调创建任务的函数。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;驱动里不要掺业务逻辑，硬件数据原样上报，翻译交给上层任务。&lt;/li&gt;
&lt;li&gt;轮询读多个队列时超时只能设 0，设长了后面的队列会被推迟。&lt;/li&gt;
&lt;li&gt;队列集长度要 ≥ 各成员队列长度之和，两个深 10 的队列就要 20。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;configUSE_QUEUE_SETS&lt;/code&gt; 要放在 USER CODE 区，直接改源文件会被 CubeMX 重新生成覆盖。&lt;/li&gt;
&lt;li&gt;队列要先加入队列集，再创建往它写数据的任务，否则队列先写满就再也进不了集合。&lt;/li&gt;
&lt;li&gt;堆不够的表现是画面不显示，不是报错，多一个队列和队列集就要把堆调大。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>信号量、互斥量与优先级反转</title><link>https://me.622168.xyz/posts/freertos/09-semaphore-mutex/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/09-semaphore-mutex/</guid><description>用车辆运行控制和优先级反转实验，区分二值信号量、计数信号量与互斥量，并理解优先级继承为什么能救场。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p41 ~ p44（约 54 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;信号量的本质&lt;/h2&gt;
&lt;p&gt;信号量的本质是一个队列，只是不涉及数据的真正传输，只做「数据个数」的加减统计。名字拆开看：信号是提醒作用，量是计数作用。&lt;/p&gt;
&lt;p&gt;用「三辆车想进城」来讲：进城要先到售票中心买票，票就是信号量，票的张数就是「量」。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Take（取票）  想进城先 Take。有票就计数减 1，拿票进城；
              没票可以在售票处等（阻塞，把自己挂进等待链表）

Give（放票）  领导打电话来放票，计数累加，并唤醒等待者；
              每放一张票只唤醒一个等待者。
              Give 永不阻塞——放到上限后直接返回失败，不等待。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结构上和常规队列对比着看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;常规队列   环形缓冲区 + 读位置 + 写位置 + 计数值 + 发送者链表 + 接收者链表

信号量     把队列的结构挪过来复用，但没有环形缓冲区、没有读写位置、
           没有发送者链表，多了一个 max value（最大计数值），
           对应队列的「深度」
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作上也能一一对应：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;写队列 send    拷贝数据 + count 累加 + 唤醒等数据的人
Give           不拷贝数据 + count 累加 + 唤醒等票的人

读队列 receive 拷贝数据 + count 减减 + 唤醒等写的人
Take           有票就 count 减减，不唤醒任何人（买票的顾客不会去叫醒领导）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;把票数限定为 1，同一时间就只能有一辆车进城——这就是互斥。&lt;/p&gt;
&lt;h2&gt;控制车辆运行&lt;/h2&gt;
&lt;p&gt;在第 17 个程序（遥控器 1/2/3 控制三辆车）基础上改出第 18 个程序：去掉按键读取和判断，一上来就显示汽车、隐藏原位置、调坐标重新显示，然后 &lt;code&gt;vTaskDelay(50ms)&lt;/code&gt;，到达最右边时任务退出（自杀）。三辆车没有任何约束，几乎同时从左冲到右。&lt;/p&gt;
&lt;p&gt;第 19 个程序加一个「票站」信号量，初始值 = 2，于是只有两辆车能拿到票跑完全程。两种信号量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 计数型：可以指定最大值和初始值 */
SemaphoreHandle_t ticket = xSemaphoreCreateCounting(max, init);

/* 二值型：不需要参数，取值只有 0/1，创建时初始值就是 0 */
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 计数信号量的最大值，字幕里说得自相矛盾（「写三也好……不要写三」），只能确定必须大于等于初始值 2。&lt;/p&gt;
&lt;p&gt;汽车任务显示完自己的形状之后，先 &lt;code&gt;xSemaphoreTake(ticket, xTicksToWait)&lt;/code&gt; 才能移动；超时值可以传 tick 数，也可以传 &lt;code&gt;portMAX_DELAY&lt;/code&gt;（永远等待）。到达最右边时 &lt;code&gt;xSemaphoreGive&lt;/code&gt; 释放，把票还给票站并唤醒下一辆。&lt;/p&gt;
&lt;p&gt;第 20 个程序把初始值改成 1，同一时间就只有一辆车能跑，跑完释放再轮到下一辆。&lt;/p&gt;
&lt;p&gt;唤醒顺序有个规则：等待链表里高优先级任务可以「后来先到」插到队首；同优先级按调用 Take 的时刻先后排；释放时取链表第一个运行。&lt;/p&gt;
&lt;p&gt;二值信号量要手动 Give 一次置 1：直接创建出来值是 0，三辆车全都拿不到；得故意 Give 一次把值设成 1，再 Give 就无效了（最大值是 1，后面几次不起作用）。这时候一车跑到终点释放、二车跑、三车跑，依次进行。&lt;/p&gt;
&lt;h2&gt;优先级反转&lt;/h2&gt;
&lt;p&gt;计数型和二值信号量都可能出现优先级反转：正常是高优先级先跑，反转之后低优先级先跑、高优先级反而跑不了。&lt;/p&gt;
&lt;p&gt;成因分三步（信号量初始值 = 1）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 最低优先级的任务先被创建、先运行，Take 拿到信号量
② 中优先级任务随后创建，优先级更高，会长时间运行
③ 最高优先级任务最后创建，也想 Take 但失败（票已经被低优先级拿走了），阻塞等待
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结果是 3 号（最高）被 2 号（中）阻塞，这就是优先级反转。关键在中优先级任务：它不拿信号量，却一直占着 CPU，把持票的低优先级任务挤下去；低优先级不释放，高优先级就永远等。&lt;/p&gt;
&lt;p&gt;在第 20 个程序基础上改出第 21 个程序，创建三个任务、优先级依次提高：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;任务 1（低）  一进来 Take 信号量 → 移动汽车 → 到最右 Give 释放

任务 2（中）  复制任务 1，但不 Take 也不 Give；
              先 vTaskDelay 让它稍后再跑，然后把移动里的 vTaskDelay 换成忙等
              （一直查询时间、不进入阻塞状态），全程不放弃 CPU

任务 3（高）  最后启动，延时 2 秒后再 Take；拿到就跑到最右并 Give
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现象：任务 1 先跑，任务 2 一跑，任务 1 和任务 3 都动不了；任务 1 跑到终点自杀之后任务 2 才能跑，任务 1 释放信号量之后任务 3 才能跑——任务 3 反而最后运行。再改一下：让任务 2 一直不自杀、不放弃 CPU，任务 1 和任务 3 就都被卡死（任务 3 虽然有最高优先级，但信号量被任务 1 占着，任务 1 跑不了也就不会释放）。&lt;/p&gt;
&lt;p&gt;生活里可以这么理解：学校机房有台超算，学生按指纹先用上了；主任带人来参观嫌吵，让先别用，堵着不走；校长有十万火急的事想用超算，却被学生占了先机，只能干等——学生被主任卡住迟迟不归还，校长越来越火。&lt;/p&gt;
&lt;h2&gt;互斥量：领导临时提拔你&lt;/h2&gt;
&lt;p&gt;用互斥量（MUTEX）解决优先级反转。第 22 个程序，在第 21 个基础上改。先用常规信号量的时候，最高优先级的第三辆车被中间那辆车卡住跑不了；把相关代码换成互斥量之后，第三辆车能启动，第一辆车到终点后第三辆车就可以启动。&lt;/p&gt;
&lt;p&gt;机制可以这样讲。校长要等学生用完超算，但学生被参观的主任卡住了。于是校长在等待的时候临时「提拔」学生，把学生的级别提升到跟自己一样，学生就能压过主任，立刻继续用超算；学生用完、归还超算使用权之后，再乖乖恢复自己原来的级别——但它已经把校长唤醒了，校长马上用上超算，主任拦不住校长。&lt;/p&gt;
&lt;p&gt;时间轴：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 学生获得超算
② 主任带人参观
③ 校长想用，但超算被占，临时把学生提拔到校长同级，学生立刻续用
④ 学生用完，恢复自己的级别，同时唤醒校长
⑤ 校长用完
⑥ 才轮到主任继续参观
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是优先级继承：学生继承了校长的优先级，释放互斥量之后恢复成原来的优先级。&lt;/p&gt;
&lt;p&gt;互斥量是信号量的一种变种，额外实现了优先级继承和优先级恢复。创建函数是 &lt;code&gt;xSemaphoreCreateMutex()&lt;/code&gt;，没有参数，返回 &lt;code&gt;SemaphoreHandle_t&lt;/code&gt;。跟进源码可以看到，创建时数值先是 0，初始化时被 +1 提升为 1，也就是初始值为 1。&lt;/p&gt;
&lt;p&gt;换互斥量的过程中程序一度卡死。原因是任务 2 正在刷新 LCD，最高优先级的任务 3 抢占进来之后忙等一个标志变量变成 0，而任务 2 还没把它清零、又跑不了，于是任务 3 死循环。这暴露了「用全局变量保护临界资源」的隐患。&lt;/p&gt;
&lt;p&gt;顺手就把 LCD 和 MPU6050 共用的 I2C 总线改成用互斥量保护：在 &lt;code&gt;lcd.c&lt;/code&gt; 里创建 &lt;code&gt;LCD_I2C_Mutex&lt;/code&gt;，实现成一对函数 &lt;code&gt;LCD_GetI2C()&lt;/code&gt; / &lt;code&gt;LCD_PutI2C()&lt;/code&gt;（一开始叫 wait/release，后来改成 get/put 更对称）。访问 LCD、MPU6050 读写 I2C 之前先 Get，用完 Put，Get 的时候一直等待（阻塞当前任务）。同时补上 HAL 头文件，修掉 &lt;code&gt;lcd.c&lt;/code&gt; 里漏写的分号。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 互斥量变量名的字幕识别不清。&lt;/p&gt;
&lt;p&gt;最终现象：任务 1 先跑，被任务 2 抢占，之后任务 1 和任务 2 交叉运行（看起来像同时跑，其实是切换很快）——因为任务 1 虽然被提升了优先级，但过程中调用了 &lt;code&gt;vTaskDelay&lt;/code&gt; 进入阻塞，任务 2 才有机会跑，时间一到任务 1 又抢回来。任务 1 到终点释放，唤醒任务 3；任务 3 跑完自杀；任务 2 继续跑。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;二值信号量创建出来是 0，要手动 Give 一次才能用，再 Give 无效。&lt;/li&gt;
&lt;li&gt;计数信号量的最大值必须大于等于初始值。&lt;/li&gt;
&lt;li&gt;优先级反转的元凶是中优先级任务，它不碰信号量却一直占 CPU。&lt;/li&gt;
&lt;li&gt;互斥量有优先级继承，信号量没有；要保护临界资源就别拿信号量凑合。&lt;/li&gt;
&lt;li&gt;用全局变量保护临界资源迟早出事，I2C 这种共享总线应该用互斥量。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>事件组与车辆协同</title><link>https://me.622168.xyz/posts/freertos/10-event-group-vehicle/</link><guid isPermaLink="true">https://me.622168.xyz/posts/freertos/10-event-group-vehicle/</guid><description>一个写入只能唤醒一个任务，要同时通知多个任务就得靠事件组；用三辆车演示广播、等待「或」和等待「与」。</description><pubDate>Tue, 29 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p45 ~ p46（约 22 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;事件组的本质&lt;/h2&gt;
&lt;p&gt;队列、信号量、互斥量都有一个共同的局限：一个写入、一个量，只能唤醒一个任务。如果想「一个任务写数据、唤醒多个任务」，就需要广播机制，也就是事件组（Event Group）。&lt;/p&gt;
&lt;p&gt;事件组也是一个结构体，里面有几样东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;一个整数        高八位不用，其余每一位代表一个事件，含义由应用程序定义
                bit0 = 1 表示 0 号事件发生了，bit1 = 0 表示对应事件没发生

一个等待链表    挂进去一个个「正在等待某些事件」的任务

每个等待任务自己的结构体里也有一个整数
                bit0 = 1 表示它想等 0 号事件，bit3 = 1 表示想等 3 号事件
                也就是「我想等哪些事件」

与/或的关系     用高八位里的某些位表示
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;[待确认]&lt;/code&gt; 标准 FreeRTOS 是用 &lt;code&gt;xEventGroupWaitBits&lt;/code&gt; 的 &lt;code&gt;xWaitForAllBits&lt;/code&gt; 参数表达与/或，字幕里关于「高八位编码与/或」的说法前后比较模糊，可能是个教学简化模型。&lt;/p&gt;
&lt;p&gt;广播的原理举个例子就清楚了。事件组初值全 0：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;任务 A 想等 0 号或 3 号事件
任务 B 想等 0 号或 7 号事件
两个任务都进等待链表

任务 C 写 bit0、bit1、bit2  → 遍历链表，A、B 都不满足，不唤醒
任务 C 写 bit7 = 1          → 遍历链表，A 不满足，B 满足（它在等 7 号事件）→ 唤醒 B
任务 C 写 bit0              → A（等 0 号或 3 号）满足，B（等 0 号或 7 号）满足 → 两个都唤醒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写事件的时候会遍历整个等待链表、逐个判断条件是否满足、满足就唤醒——这就是广播。&lt;/p&gt;
&lt;h2&gt;三辆车演示广播、或、与&lt;/h2&gt;
&lt;p&gt;在第 22 个程序基础上改，做出第 23、24、25 三个程序，分别演示广播、等待「或」、等待「与」。&lt;/p&gt;
&lt;p&gt;创建用动态分配函数 &lt;code&gt;xEventGroupCreate()&lt;/code&gt;，定义一个全局事件组变量。原先的互斥量代码注释掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 23 个程序（广播）&lt;/strong&gt;：汽车一任务不再 Take 信号量，一直运行，到最右用 &lt;code&gt;xEventGroupSetBits(xEventCar, bit0)&lt;/code&gt; 把 bit0 置 1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;xEventGroupWaitBits(xEventCar,
                    bit0,                 /* 等哪些位 */
                    xClearOnExit,         /* 退出前清除所等到的位 */
                    xWaitForAllBits,      /* 与还是或 */
                    xTicksToWait);        /* 超时 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;汽车二、三改成调用它：&lt;code&gt;xClearOnExit = pdTRUE&lt;/code&gt; 表示退出前清除所等到的位（下次可以继续等新事件）；只等一个位的时候与/或随便写；超时传「一直等待」。二、三原来释放信号量的代码删掉。&lt;/p&gt;
&lt;p&gt;现象：任务 1 优先级最低，一跑就让车往右走（二、三都在等事件）；任务 1 到终点置位，唤醒二、三，优先级高的任务 3 先跑。所以顺序是一到终点、三第二到、二最后到。把二、三的优先级改成一致，就能看到两车同时运行，这就是广播的效果。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 24 个程序（「或」）&lt;/strong&gt;：任务 1 到最右置 bit0；任务 2 不等待（它是事件发生者），随机延迟后到最右，自杀之前置 bit1；任务 3 用 &lt;code&gt;xEventGroupWaitBits&lt;/code&gt; 等 &lt;code&gt;bit0 | bit1&lt;/code&gt;，&lt;code&gt;xWaitForAllBits = pdFALSE&lt;/code&gt;（或的关系），于是任意一辆到站都能唤醒第三辆。改任务 2 的延迟可以控制谁先到：延迟改小则二先到、三先跑、一后跑；延迟加大 100 则一先到、三立即被唤醒，三一跑二就跑不了了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;第 25 个程序（「与」）&lt;/strong&gt;：把任务 3 的 &lt;code&gt;xWaitForAllBits&lt;/code&gt; 改成 &lt;code&gt;pdTRUE&lt;/code&gt;，要 bit0 和 bit1 都发生才退出，也就是一、二两车都到终点之后，第三辆车才启动。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;事件组适合表达「事件发生了」，不适合搬运数据。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xClearOnExit&lt;/code&gt; 传 &lt;code&gt;pdTRUE&lt;/code&gt; 才能反复等同一批事件；不清除的话位一直是 1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;xWaitForAllBits&lt;/code&gt; 是 &lt;code&gt;pdFALSE&lt;/code&gt; 表示等任意一个，&lt;code&gt;pdTRUE&lt;/code&gt; 表示全都要满足。&lt;/li&gt;
&lt;li&gt;被唤醒的任务之间仍然按优先级竞争，想看到「同时运行」得把优先级设成一样。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>课程地图</title><link>https://me.622168.xyz/posts/data-structure/00-course-map/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/00-course-map/</guid><description>Harsha Suryanarayana 数据结构课的全 42 集目录，以及每集对应到本教程哪一章。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;strong&gt;Harsha Suryanarayana&lt;/strong&gt;（mycodeschool），中文配音：外影译坊&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1Jr4RemEHY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;规模&lt;/td&gt;
&lt;td&gt;42 集 / 约 564 分钟（9.4 小时）/ 平均 13 分钟一集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;先修&lt;/td&gt;
&lt;td&gt;C 语言指针 + 动态内存分配&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;定位&lt;/td&gt;
&lt;td&gt;用 C/C++ 手写实现每一种数据结构，不是调库&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;这套课的特点&lt;/h2&gt;
&lt;p&gt;讲课方式是「边画图边写代码」，每一集基本都是：先画结构图讲清思路 → 再一行行写 C/C++ 代码 → 最后讲复杂度和边界情况。&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;高度定义&lt;/td&gt;
&lt;td&gt;用「边数」定义：空树高度 = −1，叶节点高度 = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;深度定义&lt;/td&gt;
&lt;td&gt;根节点深度 = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数组下标&lt;/td&gt;
&lt;td&gt;0-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链表位置&lt;/td&gt;
&lt;td&gt;1-based（长度为 n 的链表最大合法插入位置是 n+1）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;head 是什么&lt;/td&gt;
&lt;td&gt;指向头节点的指针，不是头节点本身&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NULL&lt;/td&gt;
&lt;td&gt;就是地址 0，用来标记链表末尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;这套课的字幕是 AI 识别的，同音字错极多（「店主」=电阻、「试播器」=示波器这类）。本笔记已按上下文还原，凡是识别不清或口误自相矛盾的地方，都用 &lt;code&gt;[待确认]&lt;/code&gt; 标出来了。&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;th&gt;对应章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p1&lt;/td&gt;
&lt;td&gt;6.3&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;p2&lt;/td&gt;
&lt;td&gt;12.6&lt;/td&gt;
&lt;td&gt;2.抽象数据类型：ADT&lt;/td&gt;
&lt;td&gt;导论与抽象数据类型&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p3&lt;/td&gt;
&lt;td&gt;16.6&lt;/td&gt;
&lt;td&gt;3.链表：基本介绍&lt;/td&gt;
&lt;td&gt;链表（一）基础与数组对比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p4&lt;/td&gt;
&lt;td&gt;11.8&lt;/td&gt;
&lt;td&gt;4.数组 vs 链表&lt;/td&gt;
&lt;td&gt;链表（一）基础与数组对比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p5&lt;/td&gt;
&lt;td&gt;13.4&lt;/td&gt;
&lt;td&gt;5.链表：C_C++实现&lt;/td&gt;
&lt;td&gt;链表（二）C 实现与头部插入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p6&lt;/td&gt;
&lt;td&gt;12.3&lt;/td&gt;
&lt;td&gt;6.链表：头部插入一个节点&lt;/td&gt;
&lt;td&gt;链表（二）C 实现与头部插入&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p7&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;td&gt;7.链表：任意位置插入一个节点&lt;/td&gt;
&lt;td&gt;链表（三）任意位置插入与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p8&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;8.链表：任意位置删除一个节点&lt;/td&gt;
&lt;td&gt;链表（三）任意位置插入与删除&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p9&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;td&gt;9.链表：反转一个链表 ( 迭代方式实现 )&lt;/td&gt;
&lt;td&gt;链表（四）反转与递归&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p10&lt;/td&gt;
&lt;td&gt;13.8&lt;/td&gt;
&lt;td&gt;10.链表：打印一个链表 ( 递归方式实现 )&lt;/td&gt;
&lt;td&gt;链表（四）反转与递归&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p11&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;11.链表：反转一个链表 ( 递归方式实现 )&lt;/td&gt;
&lt;td&gt;链表（四）反转与递归&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p12&lt;/td&gt;
&lt;td&gt;6.9&lt;/td&gt;
&lt;td&gt;12.链表：双向链表的介绍&lt;/td&gt;
&lt;td&gt;双向链表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p13&lt;/td&gt;
&lt;td&gt;14.8&lt;/td&gt;
&lt;td&gt;13.链表：双向链表的实现&lt;/td&gt;
&lt;td&gt;双向链表&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p14&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;14.栈：基本介绍&lt;/td&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p15&lt;/td&gt;
&lt;td&gt;12.7&lt;/td&gt;
&lt;td&gt;15.栈：使用数组实现一个栈&lt;/td&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p16&lt;/td&gt;
&lt;td&gt;10.6&lt;/td&gt;
&lt;td&gt;16.栈：使用链表实现一个栈&lt;/td&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p17&lt;/td&gt;
&lt;td&gt;15.8&lt;/td&gt;
&lt;td&gt;17.栈：反转一个字符串或者反转一个链表 ( 使用栈来实现 )&lt;/td&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p18&lt;/td&gt;
&lt;td&gt;13.7&lt;/td&gt;
&lt;td&gt;18.栈：检查括号的匹配性 ( 使用栈来实现 )&lt;/td&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p19&lt;/td&gt;
&lt;td&gt;13.1&lt;/td&gt;
&lt;td&gt;19.栈：中缀, 前缀, 后缀的基本概念&lt;/td&gt;
&lt;td&gt;栈的应用 前缀 中缀 后缀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p20&lt;/td&gt;
&lt;td&gt;13.6&lt;/td&gt;
&lt;td&gt;20.栈：前缀, 后缀表达式求值 ( 使用栈来实现 )&lt;/td&gt;
&lt;td&gt;栈的应用 前缀 中缀 后缀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p21&lt;/td&gt;
&lt;td&gt;17.6&lt;/td&gt;
&lt;td&gt;21.栈：中缀到后缀表达式的转换 ( 使用栈来实现 )&lt;/td&gt;
&lt;td&gt;栈的应用 前缀 中缀 后缀&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p22&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;22.队列：基本介绍&lt;/td&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p23&lt;/td&gt;
&lt;td&gt;14.4&lt;/td&gt;
&lt;td&gt;23.队列：使用数组实现一个队列&lt;/td&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p24&lt;/td&gt;
&lt;td&gt;13.7&lt;/td&gt;
&lt;td&gt;24.队列：使用链表实现一个队列&lt;/td&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p25&lt;/td&gt;
&lt;td&gt;15.2&lt;/td&gt;
&lt;td&gt;25.树：基本介绍&lt;/td&gt;
&lt;td&gt;树与二叉树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p26&lt;/td&gt;
&lt;td&gt;15.7&lt;/td&gt;
&lt;td&gt;26.树：二叉树&lt;/td&gt;
&lt;td&gt;树与二叉树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p27&lt;/td&gt;
&lt;td&gt;18.7&lt;/td&gt;
&lt;td&gt;27.树：二叉搜索树&lt;/td&gt;
&lt;td&gt;二叉搜索树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p28&lt;/td&gt;
&lt;td&gt;17.9&lt;/td&gt;
&lt;td&gt;28.树：二叉搜索树 (C_C++实现)&lt;/td&gt;
&lt;td&gt;二叉搜索树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p29&lt;/td&gt;
&lt;td&gt;12.6&lt;/td&gt;
&lt;td&gt;29.树：二叉搜索树 (C_C++实现) - 内存中的栈与堆详解&lt;/td&gt;
&lt;td&gt;内存中的栈与堆&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p30&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;td&gt;30.树：二叉搜索树 - 查找最小值和最大值&lt;/td&gt;
&lt;td&gt;BST 查找最值与二叉树的高度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p31&lt;/td&gt;
&lt;td&gt;6.9&lt;/td&gt;
&lt;td&gt;31.树：二叉树的高度&lt;/td&gt;
&lt;td&gt;BST 查找最值与二叉树的高度&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p32&lt;/td&gt;
&lt;td&gt;11.4&lt;/td&gt;
&lt;td&gt;32.树：二叉树的遍历 - 广度优先 vs 深度优先&lt;/td&gt;
&lt;td&gt;二叉树的遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p33&lt;/td&gt;
&lt;td&gt;10.9&lt;/td&gt;
&lt;td&gt;33.树：二叉树的层次遍历&lt;/td&gt;
&lt;td&gt;二叉树的遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p34&lt;/td&gt;
&lt;td&gt;13.9&lt;/td&gt;
&lt;td&gt;34.树：二叉树的前序, 中序, 后序遍历&lt;/td&gt;
&lt;td&gt;二叉树的遍历&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p35&lt;/td&gt;
&lt;td&gt;15.9&lt;/td&gt;
&lt;td&gt;35.树：判断是否为二叉搜索树&lt;/td&gt;
&lt;td&gt;判断 BST 与删除节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p36&lt;/td&gt;
&lt;td&gt;17.8&lt;/td&gt;
&lt;td&gt;36.树：二叉搜索树中删除一个节点&lt;/td&gt;
&lt;td&gt;判断 BST 与删除节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p37&lt;/td&gt;
&lt;td&gt;17.3&lt;/td&gt;
&lt;td&gt;37.树：二叉搜索树的中序后继节点&lt;/td&gt;
&lt;td&gt;判断 BST 与删除节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p38&lt;/td&gt;
&lt;td&gt;16.1&lt;/td&gt;
&lt;td&gt;38.图：基本介绍&lt;/td&gt;
&lt;td&gt;图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p39&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;td&gt;39.图：图的一些基本属性&lt;/td&gt;
&lt;td&gt;图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p40&lt;/td&gt;
&lt;td&gt;13.2&lt;/td&gt;
&lt;td&gt;40.图：图的表示法（第1部分：边列表）&lt;/td&gt;
&lt;td&gt;图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p41&lt;/td&gt;
&lt;td&gt;14.2&lt;/td&gt;
&lt;td&gt;41.图：图的表示法（第2部分：邻接矩阵）&lt;/td&gt;
&lt;td&gt;图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p42&lt;/td&gt;
&lt;td&gt;26.8&lt;/td&gt;
&lt;td&gt;42.图：图的表示法（第3部分：邻接表）&lt;/td&gt;
&lt;td&gt;图&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;数据结构全貌&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;线性结构
├── 数组      连续内存，随机访问 O(1)，插入删除 O(n)
├── 链表      离散内存 + 指针，插入删除 O(1)（已知位置时），访问 O(n)
│   └── 双向链表  多一个 prev 指针，能反向走，单指针即可删除
├── 栈        单侧开口，LIFO，push/pop 都是 O(1)
└── 队列      两侧开口，FIFO，enqueue/dequeue 都是 O(1)

非线性结构
├── 树        层级结构，N 个节点恰好 N−1 条边
│   └── 二叉搜索树  平均 O(log N)，最坏 O(N)（退化成链表）
└── 图        没有支配连接的规则，树只是图的特例
    ├── 边列表    O(|V|+|E|) 空间，但找邻居 O(|E|)
    ├── 邻接矩阵  O(V²) 空间，判断相连 O(1)，适合密集图
    └── 邻接表    O(|V|+|E|) 空间，适合稀疏图
&lt;/code&gt;&lt;/pre&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;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;O(1)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;链表&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(1)（头部）&lt;/td&gt;
&lt;td&gt;O(1)（已知前驱）&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;双向链表&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)（单指针即可）&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;栈&lt;/td&gt;
&lt;td&gt;O(1)（栈顶）&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;队列&lt;/td&gt;
&lt;td&gt;O(1)（队头）&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BST（平衡）&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BST（最坏）&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&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;p&gt;这套课的笔记里「坑」那部分特别值得看。边界情况本来就容易错，连课上写的代码也会漏（比如删除函数忘了 &lt;code&gt;return&lt;/code&gt;），这些都是真实会犯的错。&lt;/p&gt;
&lt;p&gt;建议动手顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 链表三章（p3-p13）—— 把指针彻底搞明白，后面全靠这个
② 栈和队列（p14-p24）—— 实现简单，但边界条件最容易错
③ 树（p25-p37）—— 重点是递归，尤其是插入为什么必须接住返回值
④ 图（p38-p42）—— 重点理解三种表示法的取舍
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>导论与抽象数据类型</title><link>https://me.622168.xyz/posts/data-structure/01-introduction-adt/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/01-introduction-adt/</guid><description>数据结构在解决什么问题，以及 ADT 为什么只讲「能做什么」不讲「怎么做」——附数组实现动态列表的完整代价分析。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p1（6 分钟）、p2（13 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;数据结构是什么&lt;/h2&gt;
&lt;p&gt;正式定义：数据结构是在计算机中存储和组织数据、以使数据得以有效利用的一种方式。&lt;/p&gt;
&lt;p&gt;核心命题：数据如何存储、组织、整合，决定了软件效率。机器算力再强，结构不对就不高效。&lt;/p&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;/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;以表格组织&lt;/td&gt;
&lt;td&gt;便于汇总和提取信息&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;结论：不同种类的数据需要不同种类的结构来组织。&lt;/p&gt;
&lt;h2&gt;研究数据结构的两种途径&lt;/h2&gt;
&lt;p&gt;这条主线全课反复回扣。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 作为数学 / 逻辑模型  →  只看抽象视角、高层特性和操作，不关心实现
                          →  这就是「抽象数据类型 ADT」
② 作为实现            →  具体类型；同一种语言可以有多种方式实现同一个 ADT
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个类比：电视机。抽象视图是「可开可关、能接收卫星信号并播放音视频的电子设备」，不必操心内部电路怎么嵌、哪家公司生产。&lt;/p&gt;
&lt;h2&gt;ADT 是什么&lt;/h2&gt;
&lt;p&gt;正式定义：对数据和操作的定义，但不存在实现。&lt;/p&gt;
&lt;p&gt;也就是说，ADT 里没有任何实现细节。&lt;/p&gt;
&lt;h3&gt;例子：列表 ADT&lt;/h3&gt;
&lt;p&gt;定义一个叫「列表」的东西：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;能存给定数据类型的元素
能按位置读取元素
能修改特定位置的元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;高级语言里的数组，已经是对这类 ADT 的具体实现。&lt;/p&gt;
&lt;h3&gt;课程方法论（三点，贯穿全系列）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 研究逻辑视图
② 研究能做哪些操作
③ 研究这些操作的代价（主要从时间方面考量）
—— 然后才是语言中的实现
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;列表 ADT 的两种形态&lt;/h2&gt;
&lt;h3&gt;静态列表&lt;/h3&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;int A[n]&lt;/code&gt;，元素是 A[0]、A[1]…（下标从 0 开始）。&lt;/p&gt;
&lt;h3&gt;动态列表（6 条特性）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 空列表时称「空列表」，大小为 0
② 可向列表任意位置插入元素
③ 可从已有列表移除元素
④ 可统计元素数量
⑤ 可在特定位置读取并修改元素
⑥ 创建时可指定数据类型（整数列表 / 字符串 / 浮点…）
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;用数组实现动态列表&lt;/h2&gt;
&lt;p&gt;做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;声明一个非常大的数组（定一个最大尺寸）
另设一个变量 N 标记「数组中列表的末尾」
&lt;/code&gt;&lt;/pre&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;N 的初值&lt;/td&gt;
&lt;td&gt;−1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;列表元素个数&lt;/td&gt;
&lt;td&gt;N + 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判定为空&lt;/td&gt;
&lt;td&gt;N == −1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;为什么 N 初值是 −1&lt;/strong&gt;：因为可能的最低索引是 0，所以 N = −1 时列表在任何时候都为空。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;/tr&gt;
&lt;tr&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;每次插入/删除后 N 都要调整。&lt;/p&gt;
&lt;p&gt;演示序列：插入 2 → 插入 4 → 在末尾插入 5；再调用移除函数传索引 0 移除元素 2。&lt;/p&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;p&gt;扩容策略：新数组大小 = 旧数组的 2 倍。&lt;/p&gt;
&lt;p&gt;至于为什么这是最佳策略，这一节没有讲。所以这里不要自作主张补「因为均摊 O(1)」——那是后面的内容。&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;按索引读/写&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;数组元素在连续内存中，用「内存块起始地址 + 元素索引」就能算出地址并直接访问&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;特定位置插入&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;最坏情况在首位插入，要把所有元素向右移，时间与列表长度成正比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除一个元素&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;所花时间与列表当前规模成正比&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;末尾添加&lt;/td&gt;
&lt;td&gt;不是 O(1)&lt;/td&gt;
&lt;td&gt;未满时 O(1)；已满时 O(n)（建新数组并复制全部元素）→ 最坏情况下仍是 O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;最后一行容易记错&lt;/strong&gt;：很多人以为「末尾追加是 O(1)」，其实最坏情况是 O(n)。&lt;/p&gt;
&lt;h2&gt;对数组实现动态列表的总评&lt;/h2&gt;
&lt;p&gt;三条代价：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 中间插入/删除代价高昂
② 列表频繁增大缩小 → 一再创建新数组、反复复制元素
③ 很多时候数组中有很大一部分未被使用，那里的内存毫无用处
   → 内存效率不高
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 ③ 条是引出链表的动机。注意动机是内存效率，不是访问速度——这个链条在 p2 结尾 → p3 故事 → p4 对比三集里是连贯的。&lt;/p&gt;
&lt;p&gt;引子：「我们有一种能很好地利用内存的数据结构，就是链表。」&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;N 的初值是 −1，不是 0；元素个数是 N+1，判空是 N == −1。&lt;/li&gt;
&lt;li&gt;扩容是翻倍，但没解释为什么，别替它补「均摊 O(1)」。&lt;/li&gt;
&lt;li&gt;末尾添加最坏是 O(n)，不是 O(1)；数组无法原地扩展，只能新建 + 复制。&lt;/li&gt;
&lt;li&gt;ADT 不含任何实现细节，只有「数据和操作的定义」。&lt;/li&gt;
&lt;li&gt;下标从 0 开始（数组/动态列表这套约定），后面讲链表时会换成 1-based，两套别混。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>链表（一）基础与数组对比</title><link>https://me.622168.xyz/posts/data-structure/02-linked-list-basics/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/02-linked-list-basics/</guid><description>从一段地址演算讲清数组的局限，以及链表和数组在访问、内存、插入三个维度上的完整对比。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p3（17 分钟）、p4（12 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;从内存讲起&lt;/h2&gt;
&lt;p&gt;用一个「程序员阿尔伯特与内存管理器」的故事讲数组的局限。&lt;/p&gt;
&lt;h3&gt;内存模型约定&lt;/h3&gt;
&lt;p&gt;把内存从左往右水平绘制，每格 1 字节，地址随向右递增（200 → 201 → 202 …）。&lt;/p&gt;
&lt;p&gt;自上而下还是从左到右关系不大，都只是看待内存的逻辑方式。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int X&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 字节（典型架构 int = 4 字节），变量 X 在地址 217&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;int A[4]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;数组总是一段连续内存，分配起始地址 201 到结束地址 216&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;访问 &lt;code&gt;A[3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;其地址 = 213&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;「一块内存的地址 = 该内存块中第一个字节的地址。」&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;访问 A[3] 的地址是由「内存块起始地址 + 位置」算出来的，所以访问任意元素时间恒定，与数组规模无关。&lt;/p&gt;
&lt;p&gt;想加第 5 个元素 → 请求扩展同一内存块 → 内存管理器拒绝（旁边已被其他变量占用）。只能新建内存块 + 复制 + 释放旧块。&lt;/p&gt;
&lt;p&gt;结论：数组要么浪费未使用部分，要么再次新建 + 复制。&lt;/p&gt;
&lt;h2&gt;链表的核心思想&lt;/h2&gt;
&lt;p&gt;不再要一大片连续区域，而是一次只申请一个元素所需的内存。&lt;/p&gt;
&lt;p&gt;四个整数的列表分别申请：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6 @ 204
5 @ 217
4 @ 232
2 @ 242
&lt;/code&gt;&lt;/pre&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;一部分存数据（值）
另一部分存「下一块的地址」
&lt;/code&gt;&lt;/pre&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;204 块&lt;/td&gt;
&lt;td&gt;存 217&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;217 块&lt;/td&gt;
&lt;td&gt;存 232&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;232 块&lt;/td&gt;
&lt;td&gt;存 242&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;242 块（最后一块）&lt;/td&gt;
&lt;td&gt;填 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;为什么填 0：零是无效地址，零可被用来标记这是列表的末尾。&lt;/p&gt;
&lt;h3&gt;C 语言定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int data;
    struct node *link;    /* 也可叫 next */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;内存占用：int 4 字节 + 指针 4 字节 = 每个节点 8 字节。&lt;/p&gt;
&lt;p&gt;这块 8 字节空间称为一个节点。&lt;/p&gt;
&lt;h3&gt;逻辑视图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;6 ──→ 5 ──→ 4 ──→ 2 ──→ NULL
&lt;/code&gt;&lt;/pre&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&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;遍历方式&lt;/h3&gt;
&lt;p&gt;只能从头部开始，问第一个「下一个的地址」，再问第二个……&lt;/p&gt;
&lt;p&gt;这就像玩寻宝游戏：先找第一个人拿第二个人的地址，再去第二个人拿第三个人的地址。&lt;/p&gt;
&lt;h3&gt;插入示例&lt;/h3&gt;
&lt;p&gt;在末尾加数字 3：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 先单独创建节点（3 @ 252）
② 把 252 填进「值为 2 的节点」的地址部分
③ 3 这个节点的地址部分置为空
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组 vs 链表&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;地址(i) = base + i × 每个元素字节数
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体演算：int 数组，base = 200，int 占 4 字节：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;地址(i) = 200 + 4i
索引 0  →  200
索引 6  →  200 + 6×4 = 224
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间复杂度 &lt;strong&gt;O(1)&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;链表：数据不在连续内存，唯一掌握的信息是首节点地址。&lt;/p&gt;
&lt;p&gt;要访问某位置必须从头逐个走。最坏要遍历全部节点，平均会遍历 N/2 个元素 → 平均情况 O(n)。&lt;/p&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;7 个整数的数组里只存了 3 个整数，其余 4 个位置未使用
7 个整数的数组 = 7 × 4 = 28 字节
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;链表：不存在未使用的内存（一次只为一个节点申请），但指针变量带来的额外内存需求不可忽视。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;3 节点链表 = 8 × 3 = 24 字节
再加一个元素：数组只多用 1 个位置（+4 字节）
              链表要多建 1 个节点（+8 字节）→ 32 字节
&lt;/code&gt;&lt;/pre&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;数据是 int（4 字节），7 个元素&lt;/td&gt;
&lt;td&gt;28 字节&lt;/td&gt;
&lt;td&gt;3 节点 24 字节 / 4 节点 32 字节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;数据是 16 字节的复杂类型，7 个元素&lt;/td&gt;
&lt;td&gt;112 字节&lt;/td&gt;
&lt;td&gt;4 节点 80 字节&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&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;这或许是一种罕见现象，但确实是一种可能性，也是链表得分之处。&lt;/p&gt;
&lt;p&gt;数组的额外成本：大小固定，一旦填满就必须新建更大数组 + 复制，这也是链表不存在的一项成本。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;开头插入&lt;/td&gt;
&lt;td&gt;每个元素朝高索引移一位 → O(n)&lt;/td&gt;
&lt;td&gt;创建新节点 + 调整头指针和新节点链接，不依赖列表大小 → &lt;strong&gt;O(1)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;末尾插入&lt;/td&gt;
&lt;td&gt;未满 O(1)；已满要新建数组并复制全部 → O(n)&lt;/td&gt;
&lt;td&gt;要遍历整个链表再创建节点、调整链接 → O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中间第 i 个位置&lt;/td&gt;
&lt;td&gt;平均要移 N/2 个元素 → O(n)&lt;/td&gt;
&lt;td&gt;也要遍历到该位置（虽然不用移动元素）→ 平均 O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;最容易写反的是「末尾插入」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;数组末尾：未满 O(1)   ← 数组快
链表末尾：O(n)        ← 链表慢（要走到尾巴）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;删除一个元素同样存在这三种情形，复杂度也相同。&lt;/p&gt;
&lt;h3&gt;哪个更容易实现&lt;/h3&gt;
&lt;p&gt;数组显然容易得多。&lt;/p&gt;
&lt;p&gt;链表的实现尤其在 C/C++ 中更容易出现段错误（segfault）和内存泄漏，处理链表要格外小心。&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;访问元素&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;td&gt;O(n)&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;需要连续大块&lt;/td&gt;
&lt;td&gt;✓ 可用小块&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开头插入&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;末尾插入&lt;/td&gt;
&lt;td&gt;未满 O(1) ✓ / 满 O(n)&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中间插入&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)&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;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;「链表插入是 O(1)」是错的。这里的插入是 O(n)，因为要遍历到特定位置；定位是 O(n)，链接调整本身不费时。只有头部插入才是 O(1)。&lt;/li&gt;
&lt;li&gt;末尾插入：数组未满 O(1)，链表 O(n)，这两个最容易写反。&lt;/li&gt;
&lt;li&gt;指针的额外内存不可忽视，小数据时链表反而更费内存。&lt;/li&gt;
&lt;li&gt;NULL 就是地址 0；链表唯一的标识是头节点地址，丢了头指针整条链就找不回来了。&lt;/li&gt;
&lt;li&gt;链表在 C/C++ 里更容易段错误和内存泄漏，不是「更高级所以更好」。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>课程地图</title><link>https://me.622168.xyz/posts/hardware/00-course-map/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/00-course-map/</guid><description>凡亿教育硬件工程师零基础入门课的全 127 集目录，以及每集对应到本教程哪一章。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;地址&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BV1gHSyY3E6q&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;规模&lt;/td&gt;
&lt;td&gt;127 集 / 约 1268 分钟（21 小时）/ 平均 10 分钟一集&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;从元器件一路讲到运放、开关电源、PCB 设计&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这套课最值钱的地方是实物演示&lt;/strong&gt;：当着你面烧电阻、烧 LED、炸电解电容、把 MOS 管击穿，再用热成像仪给你看温度。这些破坏性实验课本上没有，但对建立直觉极其重要。&lt;/p&gt;
&lt;h2&gt;三段结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;p1  ~ p20    元器件入门一：电阻、电容
p21 ~ p105   元器件与电路进阶：电感、二极管、三极管、MOS、比较器、运放、电源
p106 ~ p127  实战：纯硬件循迹小车（原理图 → PCB → 打样）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前 20 集把最基础的两个元件吃透，中段 85 集是模电主干，最后 22 集是完整的工程流程。&lt;strong&gt;能跟完最后 22 集，就具备了独立画一块板子的能力。&lt;/strong&gt;&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;th&gt;对应章节&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p1&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;硬件工程师完全零基础入门课程介绍&lt;/td&gt;
&lt;td&gt;课程地图&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p2&lt;/td&gt;
&lt;td&gt;15.8&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;p3&lt;/td&gt;
&lt;td&gt;17.5&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;p4&lt;/td&gt;
&lt;td&gt;15.3&lt;/td&gt;
&lt;td&gt;3.电阻的阻值识别&lt;/td&gt;
&lt;td&gt;电阻的阻值识别与功率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p5&lt;/td&gt;
&lt;td&gt;12.4&lt;/td&gt;
&lt;td&gt;4.电阻的功率&lt;/td&gt;
&lt;td&gt;电阻的阻值识别与功率&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p6&lt;/td&gt;
&lt;td&gt;20.0&lt;/td&gt;
&lt;td&gt;5.电阻的限流作用&lt;/td&gt;
&lt;td&gt;电阻的三大功能：限流 分流 分压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p7&lt;/td&gt;
&lt;td&gt;15.2&lt;/td&gt;
&lt;td&gt;6.电阻的分流功能&lt;/td&gt;
&lt;td&gt;电阻的三大功能：限流 分流 分压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p8&lt;/td&gt;
&lt;td&gt;12.2&lt;/td&gt;
&lt;td&gt;7.电阻的分压功能&lt;/td&gt;
&lt;td&gt;电阻的三大功能：限流 分流 分压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p9&lt;/td&gt;
&lt;td&gt;14.5&lt;/td&gt;
&lt;td&gt;8.电容是什么&lt;/td&gt;
&lt;td&gt;电容基础与常见特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p10&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;9.常见电容特性&lt;/td&gt;
&lt;td&gt;电容基础与常见特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p11&lt;/td&gt;
&lt;td&gt;19.3&lt;/td&gt;
&lt;td&gt;10.电容两端电压不能发生突变特性&lt;/td&gt;
&lt;td&gt;电容两端电压不能突变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p12&lt;/td&gt;
&lt;td&gt;18.8&lt;/td&gt;
&lt;td&gt;课程答疑_电容两端电压不能突变&lt;/td&gt;
&lt;td&gt;电容两端电压不能突变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p13&lt;/td&gt;
&lt;td&gt;17.9&lt;/td&gt;
&lt;td&gt;11.利用电容的储能特性实现上电延时&lt;/td&gt;
&lt;td&gt;电容的延时应用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p14&lt;/td&gt;
&lt;td&gt;6.0&lt;/td&gt;
&lt;td&gt;12.电容的储能特性实现断电延时&lt;/td&gt;
&lt;td&gt;电容的延时应用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p15&lt;/td&gt;
&lt;td&gt;19.0&lt;/td&gt;
&lt;td&gt;13.电容的稳压功能和滤波功能&lt;/td&gt;
&lt;td&gt;电容的稳压与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p16&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;14.容抗的计算公式&lt;/td&gt;
&lt;td&gt;容抗与低通滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p17&lt;/td&gt;
&lt;td&gt;13.8&lt;/td&gt;
&lt;td&gt;15.低通滤波电路的基本作用&lt;/td&gt;
&lt;td&gt;容抗与低通滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p18&lt;/td&gt;
&lt;td&gt;14.7&lt;/td&gt;
&lt;td&gt;16.低通滤波电路的本质原理&lt;/td&gt;
&lt;td&gt;容抗与低通滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p19&lt;/td&gt;
&lt;td&gt;10.9&lt;/td&gt;
&lt;td&gt;17.高通滤波电路工作原理&lt;/td&gt;
&lt;td&gt;高通滤波电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p20&lt;/td&gt;
&lt;td&gt;3.1&lt;/td&gt;
&lt;td&gt;18.高通滤波电路常见应用场景&lt;/td&gt;
&lt;td&gt;高通滤波电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p21&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;硬件进阶课程简介&lt;/td&gt;
&lt;td&gt;—（进阶课程简介 / 炸板记录）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p22&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;硬件线下班炸板记录片&lt;/td&gt;
&lt;td&gt;—（进阶课程简介 / 炸板记录）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p23&lt;/td&gt;
&lt;td&gt;6.3&lt;/td&gt;
&lt;td&gt;19.电感是什么&lt;/td&gt;
&lt;td&gt;电感基础与基本特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p24&lt;/td&gt;
&lt;td&gt;11.5&lt;/td&gt;
&lt;td&gt;20.电感的基本特性&lt;/td&gt;
&lt;td&gt;电感基础与基本特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p25&lt;/td&gt;
&lt;td&gt;8.0&lt;/td&gt;
&lt;td&gt;21.电感只限制电流的变化速度但不改变电流最大值&lt;/td&gt;
&lt;td&gt;电感基础与基本特性&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p26&lt;/td&gt;
&lt;td&gt;21.4&lt;/td&gt;
&lt;td&gt;22.电感回路的电阻突然变很大会怎样&lt;/td&gt;
&lt;td&gt;电感回路、感抗与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p27&lt;/td&gt;
&lt;td&gt;4.6&lt;/td&gt;
&lt;td&gt;23.电感的感抗计算公式&lt;/td&gt;
&lt;td&gt;电感回路、感抗与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p28&lt;/td&gt;
&lt;td&gt;15.0&lt;/td&gt;
&lt;td&gt;24.电感的低通滤波原理&lt;/td&gt;
&lt;td&gt;电感回路、感抗与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p29&lt;/td&gt;
&lt;td&gt;10.8&lt;/td&gt;
&lt;td&gt;25.电感的高通滤波原理&lt;/td&gt;
&lt;td&gt;电感回路、感抗与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p30&lt;/td&gt;
&lt;td&gt;16.5&lt;/td&gt;
&lt;td&gt;26. LC低通滤波原理&lt;/td&gt;
&lt;td&gt;电感回路、感抗与滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p31&lt;/td&gt;
&lt;td&gt;21.3&lt;/td&gt;
&lt;td&gt;27.二极管的基本结构&lt;/td&gt;
&lt;td&gt;二极管基础与整流电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p32&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;28.二极管的基本功能&lt;/td&gt;
&lt;td&gt;二极管基础与整流电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p33&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;29.二极管的漏电特性&lt;/td&gt;
&lt;td&gt;二极管基础与整流电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p34&lt;/td&gt;
&lt;td&gt;9.3&lt;/td&gt;
&lt;td&gt;30.半波整流电路工作原理(峰值检波电路)&lt;/td&gt;
&lt;td&gt;二极管基础与整流电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p35&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;31.全波整流电路工作原理&lt;/td&gt;
&lt;td&gt;二极管基础与整流电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p36&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;td&gt;32.二极管的钳位功能&lt;/td&gt;
&lt;td&gt;二极管的钳位、方向控制与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p37&lt;/td&gt;
&lt;td&gt;8.6&lt;/td&gt;
&lt;td&gt;33.二极管控制电路的电流方向&lt;/td&gt;
&lt;td&gt;二极管的钳位、方向控制与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p38&lt;/td&gt;
&lt;td&gt;8.4&lt;/td&gt;
&lt;td&gt;34.二极管的最大反向耐压&lt;/td&gt;
&lt;td&gt;二极管的钳位、方向控制与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p39&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;35.稳压二极管的基本功能&lt;/td&gt;
&lt;td&gt;稳压二极管&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p40&lt;/td&gt;
&lt;td&gt;7.8&lt;/td&gt;
&lt;td&gt;36.三极管的基本结构&lt;/td&gt;
&lt;td&gt;三极管的基本结构与开关功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p41&lt;/td&gt;
&lt;td&gt;7.1&lt;/td&gt;
&lt;td&gt;37.三极管的开关功能&lt;/td&gt;
&lt;td&gt;三极管的基本结构与开关功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p42&lt;/td&gt;
&lt;td&gt;17.8&lt;/td&gt;
&lt;td&gt;38.三极管开关电路设计注意事项及计算方法&lt;/td&gt;
&lt;td&gt;三极管开关电路设计与计算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p43&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;td&gt;39.电子开关什么时候用P管？什么时候用N管？&lt;/td&gt;
&lt;td&gt;N 管与 P 管的选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p44&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;td&gt;40.P型三极管开关电路计算方法&lt;/td&gt;
&lt;td&gt;N 管与 P 管的选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p45&lt;/td&gt;
&lt;td&gt;3.3&lt;/td&gt;
&lt;td&gt;41.N型三极管和P型三极管的特性对比&lt;/td&gt;
&lt;td&gt;N 管与 P 管的选择&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p46&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;42.三极管的输出反向功能&lt;/td&gt;
&lt;td&gt;三极管的反相、下拉电阻与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p47&lt;/td&gt;
&lt;td&gt;9.6&lt;/td&gt;
&lt;td&gt;43.三极管的基极下拉电阻的重要性&lt;/td&gt;
&lt;td&gt;三极管的反相、下拉电阻与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p48&lt;/td&gt;
&lt;td&gt;9.8&lt;/td&gt;
&lt;td&gt;44.三极管的最大耐压&lt;/td&gt;
&lt;td&gt;三极管的反相、下拉电阻与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p49&lt;/td&gt;
&lt;td&gt;7.6&lt;/td&gt;
&lt;td&gt;45.自动出水水龙头工作原理_上&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p50&lt;/td&gt;
&lt;td&gt;15.4&lt;/td&gt;
&lt;td&gt;46.自动出水水龙头工作原理_下&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p51&lt;/td&gt;
&lt;td&gt;4.1&lt;/td&gt;
&lt;td&gt;47.自动出水水龙头电路设计注意事项&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p52&lt;/td&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;td&gt;48.比较器为什么要加上拉电阻&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p53&lt;/td&gt;
&lt;td&gt;12.1&lt;/td&gt;
&lt;td&gt;49.温控风扇工作原理&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p54&lt;/td&gt;
&lt;td&gt;12.6&lt;/td&gt;
&lt;td&gt;50.人体感应灯工作原理&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p55&lt;/td&gt;
&lt;td&gt;6.5&lt;/td&gt;
&lt;td&gt;51.光控灯工作原理&lt;/td&gt;
&lt;td&gt;实战模块：水龙头 温控风扇 人体感应灯 光控灯&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p56&lt;/td&gt;
&lt;td&gt;10.4&lt;/td&gt;
&lt;td&gt;52.MOS管的基本结构&lt;/td&gt;
&lt;td&gt;MOS 管的基本结构与导通条件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p57&lt;/td&gt;
&lt;td&gt;5.7&lt;/td&gt;
&lt;td&gt;53.MOS的导通条件&lt;/td&gt;
&lt;td&gt;MOS 管的基本结构与导通条件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p58&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;54.MOS开关电路的设计方法&lt;/td&gt;
&lt;td&gt;MOS 开关电路设计与寄生参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p59&lt;/td&gt;
&lt;td&gt;14.5&lt;/td&gt;
&lt;td&gt;55.MOS的寄生电容问题&lt;/td&gt;
&lt;td&gt;MOS 开关电路设计与寄生参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p60&lt;/td&gt;
&lt;td&gt;12.5&lt;/td&gt;
&lt;td&gt;56.MOS的寄生二极管一定不能忽略&lt;/td&gt;
&lt;td&gt;MOS 开关电路设计与寄生参数&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p61&lt;/td&gt;
&lt;td&gt;23.3&lt;/td&gt;
&lt;td&gt;57.Nmos管做电源开关时的注意事项&lt;/td&gt;
&lt;td&gt;NMOS 与 PMOS 电源开关设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p62&lt;/td&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;td&gt;58.Pmos的电源开关设计方法&lt;/td&gt;
&lt;td&gt;NMOS 与 PMOS 电源开关设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p63&lt;/td&gt;
&lt;td&gt;5.1&lt;/td&gt;
&lt;td&gt;59.Pmos防反接电路的工作原理&lt;/td&gt;
&lt;td&gt;NMOS 与 PMOS 电源开关设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p64&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;td&gt;60.MOS管的电流可以在DS之间双向流通&lt;/td&gt;
&lt;td&gt;NMOS 与 PMOS 电源开关设计&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p65&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;61.如何抵消MOS的寄生体二极管&lt;/td&gt;
&lt;td&gt;MOS 的体二极管、Vgs 与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p66&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;td&gt;62.MOS管的导通电流与Vgs的关系&lt;/td&gt;
&lt;td&gt;MOS 的体二极管、Vgs 与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p67&lt;/td&gt;
&lt;td&gt;9.3&lt;/td&gt;
&lt;td&gt;63.MOS管的耐压&lt;/td&gt;
&lt;td&gt;MOS 的体二极管、Vgs 与耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p68&lt;/td&gt;
&lt;td&gt;2.6&lt;/td&gt;
&lt;td&gt;64.比较器是什么&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p69&lt;/td&gt;
&lt;td&gt;8.7&lt;/td&gt;
&lt;td&gt;65.比较器什么时候要加上拉电阻&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p70&lt;/td&gt;
&lt;td&gt;1.6&lt;/td&gt;
&lt;td&gt;66.比较器OD和OC输出结构的区别&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p71&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;td&gt;67.比较器上拉电阻的使用注意事项&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p72&lt;/td&gt;
&lt;td&gt;5.6&lt;/td&gt;
&lt;td&gt;68.比较器的输入信号范围&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p73&lt;/td&gt;
&lt;td&gt;13.6&lt;/td&gt;
&lt;td&gt;69.如何解决比较器的颤振问题&lt;/td&gt;
&lt;td&gt;比较器&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p74&lt;/td&gt;
&lt;td&gt;3.5&lt;/td&gt;
&lt;td&gt;70.运放的常见作用&lt;/td&gt;
&lt;td&gt;运放基础与虚短虚断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p75&lt;/td&gt;
&lt;td&gt;16.9&lt;/td&gt;
&lt;td&gt;71.运放虚短的根本原因&lt;/td&gt;
&lt;td&gt;运放基础与虚短虚断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p76&lt;/td&gt;
&lt;td&gt;10.5&lt;/td&gt;
&lt;td&gt;72.运放虚断的根本原因&lt;/td&gt;
&lt;td&gt;运放基础与虚短虚断&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p77&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;73.同相放大电路的推导&lt;/td&gt;
&lt;td&gt;四种放大电路推导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p78&lt;/td&gt;
&lt;td&gt;5.1&lt;/td&gt;
&lt;td&gt;74.反相放大电路的推导&lt;/td&gt;
&lt;td&gt;四种放大电路推导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p79&lt;/td&gt;
&lt;td&gt;10.0&lt;/td&gt;
&lt;td&gt;75.单电源反相放大电路的推导&lt;/td&gt;
&lt;td&gt;四种放大电路推导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p80&lt;/td&gt;
&lt;td&gt;10.8&lt;/td&gt;
&lt;td&gt;76.差分放大电路的推导&lt;/td&gt;
&lt;td&gt;四种放大电路推导&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p81&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;77.低端电流采样原理&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p82&lt;/td&gt;
&lt;td&gt;16.7&lt;/td&gt;
&lt;td&gt;78.低端电流采样电路的注意事项及解决方法&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p83&lt;/td&gt;
&lt;td&gt;6.4&lt;/td&gt;
&lt;td&gt;79.高端电流采样原理&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p84&lt;/td&gt;
&lt;td&gt;13.0&lt;/td&gt;
&lt;td&gt;80.仪表放大器_高端电流采样原理&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p85&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;81.仪表放大器_带偏置差分放大电路的公式推导&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p86&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;82.仪表放大器做高端电流检测的注意事项&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p87&lt;/td&gt;
&lt;td&gt;8.2&lt;/td&gt;
&lt;td&gt;83.跨阻放大电路工作原理&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p88&lt;/td&gt;
&lt;td&gt;6.8&lt;/td&gt;
&lt;td&gt;84.为什么运放的输入端要做电阻匹配&lt;/td&gt;
&lt;td&gt;电流采样电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p89&lt;/td&gt;
&lt;td&gt;11.0&lt;/td&gt;
&lt;td&gt;85.恒流源电路工作原理&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p90&lt;/td&gt;
&lt;td&gt;6.2&lt;/td&gt;
&lt;td&gt;86.恒流源电路的设计注意事项1&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p91&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;87.恒流源电路的设计注意事项2&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p92&lt;/td&gt;
&lt;td&gt;13.1&lt;/td&gt;
&lt;td&gt;88.高端反馈恒流源电路工作原理&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p93&lt;/td&gt;
&lt;td&gt;8.3&lt;/td&gt;
&lt;td&gt;89.如何解决恒流源自激振荡问题&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p94&lt;/td&gt;
&lt;td&gt;11.0&lt;/td&gt;
&lt;td&gt;90.高端反馈恒流源电路工作原理方案二&lt;/td&gt;
&lt;td&gt;恒流源电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95&lt;/td&gt;
&lt;td&gt;9.0&lt;/td&gt;
&lt;td&gt;91.直流稳压电源工作原理_上&lt;/td&gt;
&lt;td&gt;直流稳压电源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p96&lt;/td&gt;
&lt;td&gt;4.4&lt;/td&gt;
&lt;td&gt;92.直流稳压电源工作原理_下&lt;/td&gt;
&lt;td&gt;直流稳压电源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p97&lt;/td&gt;
&lt;td&gt;11.3&lt;/td&gt;
&lt;td&gt;93.反馈型和非反馈型直流稳压电源的优缺点&lt;/td&gt;
&lt;td&gt;直流稳压电源&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p98&lt;/td&gt;
&lt;td&gt;7.4&lt;/td&gt;
&lt;td&gt;94.最原始的开关电源电路结构&lt;/td&gt;
&lt;td&gt;开关电源：BUCK 与 BOOST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p99&lt;/td&gt;
&lt;td&gt;12.0&lt;/td&gt;
&lt;td&gt;95.BUCK拓扑是如何设计出来的&lt;/td&gt;
&lt;td&gt;开关电源：BUCK 与 BOOST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p100&lt;/td&gt;
&lt;td&gt;4.9&lt;/td&gt;
&lt;td&gt;96.续流二极管为什么可以解决电感感生的高压问题&lt;/td&gt;
&lt;td&gt;开关电源：BUCK 与 BOOST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p101&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;97.开关电源控制芯片与拓扑结构的等效&lt;/td&gt;
&lt;td&gt;开关电源：BUCK 与 BOOST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p102&lt;/td&gt;
&lt;td&gt;9.7&lt;/td&gt;
&lt;td&gt;98.BOOST的工作原理&lt;/td&gt;
&lt;td&gt;开关电源：BUCK 与 BOOST&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p103&lt;/td&gt;
&lt;td&gt;4.2&lt;/td&gt;
&lt;td&gt;99.电感的饱和电流怎么选&lt;/td&gt;
&lt;td&gt;电感饱和电流与电荷泵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p104&lt;/td&gt;
&lt;td&gt;7.3&lt;/td&gt;
&lt;td&gt;100.电荷泵升压工作原理&lt;/td&gt;
&lt;td&gt;电感饱和电流与电荷泵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p105&lt;/td&gt;
&lt;td&gt;3.2&lt;/td&gt;
&lt;td&gt;101.电荷泵产生负压的工作原理&lt;/td&gt;
&lt;td&gt;电感饱和电流与电荷泵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p106&lt;/td&gt;
&lt;td&gt;8.9&lt;/td&gt;
&lt;td&gt;小白进阶转折点_开启硬件工程师实战之旅&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p107&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;102.纯硬件循迹小车工作原理&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p108&lt;/td&gt;
&lt;td&gt;9.7&lt;/td&gt;
&lt;td&gt;103.纯硬件循迹小车_设计电机驱动电路&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p109&lt;/td&gt;
&lt;td&gt;8.1&lt;/td&gt;
&lt;td&gt;104.纯硬件寻迹小车_黑线检测原理&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p110&lt;/td&gt;
&lt;td&gt;7.0&lt;/td&gt;
&lt;td&gt;105.纯硬件循迹小车_控制电路基本逻辑&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p111&lt;/td&gt;
&lt;td&gt;9.9&lt;/td&gt;
&lt;td&gt;106.纯硬件循迹小车_设计电机控制电路&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p112&lt;/td&gt;
&lt;td&gt;16.3&lt;/td&gt;
&lt;td&gt;107.纯硬件循迹小车_改善电机驱动电路&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p113&lt;/td&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;td&gt;108.电机驱动电路中储能电容的作用&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p114&lt;/td&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;109.纯硬件循迹小车_完善电机驱动电路&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p115&lt;/td&gt;
&lt;td&gt;14.3&lt;/td&gt;
&lt;td&gt;110.纯硬件循迹小车_设计电源电路&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p116&lt;/td&gt;
&lt;td&gt;5.2&lt;/td&gt;
&lt;td&gt;111.纯硬件循迹小车_电源电路参数计算&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p117&lt;/td&gt;
&lt;td&gt;4.7&lt;/td&gt;
&lt;td&gt;112.纯硬件寻迹小车_完善电路设计&lt;/td&gt;
&lt;td&gt;实战项目：纯硬件循迹小车&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p118&lt;/td&gt;
&lt;td&gt;9.7&lt;/td&gt;
&lt;td&gt;113.绘制单部件原理图封装&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p119&lt;/td&gt;
&lt;td&gt;10.5&lt;/td&gt;
&lt;td&gt;114.绘制多部件原理图封装&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p120&lt;/td&gt;
&lt;td&gt;20.4&lt;/td&gt;
&lt;td&gt;115.绘制PCB封装&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p121&lt;/td&gt;
&lt;td&gt;5.0&lt;/td&gt;
&lt;td&gt;116.原理图导入PCB&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p122&lt;/td&gt;
&lt;td&gt;10.8&lt;/td&gt;
&lt;td&gt;117.设计PCB机械结构及边框&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p123&lt;/td&gt;
&lt;td&gt;9.3&lt;/td&gt;
&lt;td&gt;118.关键器件定位&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p124&lt;/td&gt;
&lt;td&gt;20.9&lt;/td&gt;
&lt;td&gt;119.元器件布局&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p125&lt;/td&gt;
&lt;td&gt;13.3&lt;/td&gt;
&lt;td&gt;120.元器件layout&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p126&lt;/td&gt;
&lt;td&gt;12.1&lt;/td&gt;
&lt;td&gt;121.铺铜&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p127&lt;/td&gt;
&lt;td&gt;10.3&lt;/td&gt;
&lt;td&gt;122.PCB检查&lt;/td&gt;
&lt;td&gt;PCB 设计全流程&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;p&gt;硬件和数据结构不一样的地方：光看没用，必须动手。这门课每个结论都是从实物上演示出来的，看到「实操」「仿真」的地方一定要跟着做一遍。尤其是这几集，建议自己复现：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;p5 烧电阻（感受额定功率）&lt;/li&gt;
&lt;li&gt;p9 炸电解电容（感受极性接反的后果，注意安全）&lt;/li&gt;
&lt;li&gt;p67 MOS 管击穿（感受栅极有多脆）&lt;/li&gt;
&lt;li&gt;p106 之后 循迹小车（完整工程流程）&lt;/li&gt;
&lt;/ul&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;code&gt;U = IR&lt;/code&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;电流不能突变，通直隔交&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;反向击穿时电压不变&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;MOS 管&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;高增益差分放大&lt;/td&gt;
&lt;td&gt;放大、采样、恒流源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;电容和电感是镜像的一对：电容管电压，电感管电流；电容隔直通交，电感通直隔交。把这一对对照着记，比单独背两个快得多。&lt;/p&gt;
&lt;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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;欧姆定律、分压分流、RC/RL 电路&lt;/td&gt;
&lt;td&gt;电路分析基础&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;二极管、三极管、MOS、比较器、运放&lt;/td&gt;
&lt;td&gt;模拟电子技术（模电）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;BUCK / BOOST / 电荷泵&lt;/td&gt;
&lt;td&gt;电力电子技术&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原理图、PCB 设计&lt;/td&gt;
&lt;td&gt;实际工程技能，课本不教&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;这是模电的前置课。&lt;/strong&gt; 模电课本一上来就是二极管、三极管、放大电路。如果连 PN 结为什么单向导电、三极管为什么能放大都说不清，后面全是背公式。&lt;/p&gt;
</content:encoded></item><item><title>电路设计安全要点</title><link>https://me.622168.xyz/posts/hardware/01-safety/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/01-safety/</guid><description>零基础学硬件最该先记住的一集：真正电人的是电流不是电压，以及为什么不要碰市电。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p2（15.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;一句话&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;零基础不要碰市电。&lt;/strong&gt; 老老实实玩 30V 以下的低压直流，高压的犯错成本太高。&lt;/p&gt;
&lt;h2&gt;入门该买什么&lt;/h2&gt;
&lt;p&gt;花一两百块买一台可调直流稳压电源，淘宝搜「直流稳压电源」，输出选 0~30V。&lt;/p&gt;
&lt;p&gt;它干的事就是把 220V 交流转成你能随便摸的低压直流。有了它，整个入门阶段都不会碰到市电。&lt;/p&gt;
&lt;h2&gt;真正危险的是电流，不是电压&lt;/h2&gt;
&lt;p&gt;这是最该记住的一句。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;有电压但没有电流流过人体，就没事。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;比如老电工「带电操作」换灯泡，站在绝缘梯上，手摸到 220V 火线也没事——梯子对大地绝缘，没有构成回路，他身上有电压但没有电流。&lt;/p&gt;
&lt;p&gt;反过来，只要构成了回路，低压也能出事。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;交流&lt;/td&gt;
&lt;td&gt;36V&lt;/td&gt;
&lt;td&gt;10mA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直流&lt;/td&gt;
&lt;td&gt;约 50V&lt;/td&gt;
&lt;td&gt;约 50mA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;直流其实到 50V 也不会有危险，但玩低压的话 30V 电源已经够用。&lt;/p&gt;
&lt;h2&gt;万一要用身体试电&lt;/h2&gt;
&lt;p&gt;有个演示（自称是错误示范）：直流 5V、甚至调到 30V，两只手摸输出端都没事。&lt;/p&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;p&gt;为什么用手背：触电时肌肉会收缩。用手背碰，收缩方向是缩回来；用手掌抓，会握住导线，被电麻痹后甩不掉，旁边没人就可能一直烧下去。&lt;/p&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;h2&gt;一个反直觉的坑&lt;/h2&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;h2&gt;另一个坑：开关可能切的是零线&lt;/h2&gt;
&lt;p&gt;家里开关如果接错，切断的是零线而不是火线，那么开关断开后，线上仍然有 220V。&lt;/p&gt;
&lt;p&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;li&gt;别人带电作业时别去扶梯子、别碰人；开关也不一定切的是火线，断电先用万用表确认。&lt;/li&gt;
&lt;li&gt;调试时板子别对着自己（大电容、MOS 管这些会炸），对地或对天都行；大电容断电后还存电，碰之前先放电。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电容基础与常见特性</title><link>https://me.622168.xyz/posts/hardware/05-capacitor-basics/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/05-capacitor-basics/</guid><description>电容的结构、极性、种类，以及炸电容实验——25V 的电容到底能扛到多少伏。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p9（14.5 分钟）、p10（6 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;电容是什么&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;两块不连通的导体 + 中间的绝缘材料。&lt;/strong&gt;&lt;/p&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;p&gt;面积越大、介电常数越高，容量越大。&lt;/p&gt;
&lt;h3&gt;一个类比&lt;/h3&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;ul&gt;
&lt;li&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;/li&gt;
&lt;li&gt;长脚正、短脚负&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;正负极绝对不能接反，接反会炸。也不能超过耐压值。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;实验一：反接爆炸&lt;/h2&gt;
&lt;p&gt;手上 100µF / 25V 电解电容，反接，逐步升压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5.3V      还没炸
继续升     电流慢慢变大 → 外壳发烫（摸到至少 45~50℃）
十几伏     炸，外壳直接飞出来
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;低电压反接不会立刻坏，长时间工作会慢慢自然损坏；反向耐压高时才一下子炸。&lt;/p&gt;
&lt;p&gt;内部机理：内部发生化学反应产生气体 → 气体膨胀 → 把电容炸开。&lt;/p&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;/li&gt;
&lt;li&gt;调试电路尽量戴防护&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;实验二：正接超压&lt;/h2&gt;
&lt;p&gt;同一个 25V/100µF 电容正接，从 12V 往上调：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;26V       电流仍为 0
继续升     电流有一点点变化又跳回去
40 多伏    电流持续上升
随后       短路（炸）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个结论：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;厂家耐压留有余量，标 25V 不是到 25V 就立刻炸&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电流开始自己慢慢变大 = 准备要炸了&lt;/strong&gt;（这是最有用的判断规则）&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;有机会可以自己炸一次电容，建立对危险性的直观认识。&lt;/p&gt;
&lt;p&gt;注意量级：这只是 100µF，炸起来已经像一个鞭炮。以后用到 mF 甚至 1F 级电容，爆炸威力完全不是一个量级。&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;电解电容&lt;/td&gt;
&lt;td&gt;有方向、不能反接、不能超耐压&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CBB 电容&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;/tr&gt;
&lt;tr&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MLCC（贴片陶瓷电容）&lt;/td&gt;
&lt;td&gt;见下&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;MLCC 的关键参数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;ESR 非常低，能提供的瞬态电流能力相对较大&lt;/li&gt;
&lt;li&gt;ESL 也很低，&lt;strong&gt;ESL 才是决定高频特性的主要因素&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;容量一般（体积小，做不到特别大）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;决定电容高频特性的是 ESL，不是 ESR。&lt;/strong&gt; ESR 影响的是损耗和纹波，ESL 影响的是「这个电容在高频下还像不像个电容」。&lt;/p&gt;
&lt;h3&gt;电解电容的寿命&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;内部电解液会挥发，温度越高挥发越快，寿命越短。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;而且温度升高时寿命衰减曲线非常陡——网上流传的「电解电容温度每升 10℃ 寿命减半」的经验说法就来源于此。&lt;/p&gt;
&lt;p&gt;这就是为什么电源板上最常坏的就是电解电容。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;固态电解电容&lt;/td&gt;
&lt;td&gt;好&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;选型结论&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;要高电压 + 大容量  →  电解电容（MLCC 容量做不大）
要低 ESR / 高频好  →  MLCC
要无极性 / 高耐压  →  CBB
要温度特性好      →  固态电解
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;选器件前必须先了解该器件的工作特性，方向对了才选得到合适的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;电解电容接反会炸，长脚正、色带负，上电前必须检查；耐压要留余量，实际工作电压别超过额定的 70%~80%。&lt;/li&gt;
&lt;li&gt;25V 的电容不会在 25V 立刻炸，但会持续受损；电流开始自己变大就是快炸了。&lt;/li&gt;
&lt;li&gt;杂牌电容的失效方式不可控（该从防爆槽炸的却整个爆开）。&lt;/li&gt;
&lt;li&gt;决定高频特性的是 ESL 不是 ESR；电解电容的寿命由温度决定，温度高一点寿命掉很多。&lt;/li&gt;
&lt;li&gt;调试时别把板子对着脸，电容越大炸得越狠，1F 级能造成严重伤害。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>欧姆定律</title><link>https://me.622168.xyz/posts/hardware/02-ohms-law/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/02-ohms-law/</guid><description>实战电路里 90% 以上的电阻都是用它算出来的，以及为什么万用表一串进去电流就变了。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;对应 p3，17 分钟&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;公式&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;U = I × R
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;R = U/I&lt;/code&gt; 和 &lt;code&gt;I = U/R&lt;/code&gt; 是同一个式子的变形。实战里 90% 以上的电阻都靠它算。&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;电压&lt;/td&gt;
&lt;td&gt;220V&lt;/td&gt;
&lt;td&gt;3.3 ~ 24V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电阻&lt;/td&gt;
&lt;td&gt;1Ω 量级&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;千欧以上&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电流&lt;/td&gt;
&lt;td&gt;几百安&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;毫安&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;常见直流电压规格：24 / 12 / 15 / 5 / 3.3 / 2.5 / 1.8 / 0.85V（越新的芯片核心电压越低）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5V / 1KΩ = 5mA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;5mA 就是很典型的工作电流。脑子里先有这个量级，后面算限流电阻才不容易离谱——&lt;strong&gt;算出几百安培肯定是错的&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;万用表&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;测电阻&lt;/strong&gt;：档位选比待测值大的一档（测 10Ω 就选 200 档）。COM 孔接黑笔，VΩ 那个孔接红笔。&lt;/p&gt;
&lt;p&gt;手上这个标着 10Ω 的，实测 &lt;strong&gt;9.9Ω&lt;/strong&gt;。元件都有偏差，这是常态。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;测电流&lt;/strong&gt;：表笔&lt;strong&gt;串联进回路&lt;/strong&gt;，红笔接电阻另一端，黑笔接电源负。上电前先打回电流档。档位选略大于预计电流的（预计 100mA 就选 200mA 档）；不知道工作电流就先打 20A 档，读数小再往小调。&lt;/p&gt;
&lt;h2&gt;两个实验&lt;/h2&gt;
&lt;h3&gt;电源显示的数不能当真&lt;/h3&gt;
&lt;p&gt;10Ω 加 1V，理论 100mA，电源只显示 &lt;strong&gt;95mA&lt;/strong&gt;。&lt;/p&gt;
&lt;h3&gt;万用表串进去，电流变了&lt;/h3&gt;
&lt;p&gt;串入万用表后电流只剩 &lt;strong&gt;77mA&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;理论：1V / 10Ω = 100mA
实际：77mA  →  总阻值 ≈ 1V / 0.077A ≈ 13Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;多出来的约 3Ω 就是万用表内阻。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以被测电阻必须远大于万用表内阻，读数才可信。&lt;/strong&gt; 测 10Ω 时内阻占了 23%，误差没法看；测 10KΩ 时内阻占比可以忽略，就准了。&lt;/p&gt;
&lt;h2&gt;顺带说个方法&lt;/h2&gt;
&lt;p&gt;读数对不上先别怀疑元件。当时的推理是：&lt;code&gt;I = U/R&lt;/code&gt;，R 是量过的 9.9Ω，U 是 1V，接线就三根不可能接错——那只能是&lt;strong&gt;回路里多了东西&lt;/strong&gt;。多的是什么？刚串进去的那只表。&lt;/p&gt;
&lt;p&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;li&gt;&lt;code&gt;U = IR&lt;/code&gt; 里的 R 是你要算的那个电阻，不是电源内阻。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电容两端电压不能突变</title><link>https://me.622168.xyz/posts/hardware/06-capacitor-voltage-cannot-jump/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/06-capacitor-voltage-cannot-jump/</guid><description>电容最重要的一条性质，以及一个能把复杂电路瞬间看穿的等效方法：电压为 0 时把电容当导线。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p11（19.3 分钟）、p12（18.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;这是电容的地基&lt;/h2&gt;
&lt;p&gt;电容功能很多（储能、滤波、耦合、旁路），但&lt;strong&gt;归根结底都基于「电容两端电压不能突变」这一特性&lt;/strong&gt;。&lt;/p&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;设两端为 A 电极、B 电极：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A 相对 B 的电压不会突变。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;原因：改变电容两端电压必须充放电，而充放电需要时间，所以不可能突变。&lt;/p&gt;
&lt;h3&gt;对比演示&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;5V + 两个 1K 电阻分压：开关一闭合，中点电压瞬间从 0 跳到 2.5V，这就是突变&lt;/li&gt;
&lt;li&gt;5V + 1K 电阻 + 电容：同一点电压缓慢上升，没有突变&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;最容易错的一点&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;两端相对电压不能突变，但两端各自的（对地）电压可以同时突变。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这句话反直觉，但极其重要。&lt;/p&gt;
&lt;h3&gt;分析：5V 电源 + 1K 电阻 + 电容&lt;/h3&gt;
&lt;p&gt;初始：两个开关都断开，电容无电压，A 点 = B 点 = 0V。&lt;/p&gt;
&lt;p&gt;情况一：闭合 S2（电源经电阻给电容充电）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① A 点经开关直接接到 5V  →  A 点立刻跳到 5V
② 此时电容两端电压为 0（A 与 B 等电位）
③ 所以 B 点也跟着从 0 跳到 5V        ← 两点同时突变
④ 随后电容开始充电：A 保持 5V，B 从 5V 慢慢下降到 0V
⑤ 充满后：A = 5V、B = 0V，电容上有 5V 压降
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 ①②③ 的顺序：A 先跳，然后 B 被「拽」着一起跳。跳完之后电容两端压差仍然是 0，所以没有违反「相对电压不能突变」。&lt;/p&gt;
&lt;p&gt;情况二：断开 S2、闭合 S3（把 B 点接到 5V）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① B 点突然从 0 变成 5V
② 电容两端电压会同时突变 → A 点也跟着往上抬 5V  →  A 点变成 10V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;A 点出现 10V，比电源 5V 还高&lt;/strong&gt;——这就是自举/泵升的雏形，后面 MOS 管驱动会用。&lt;/p&gt;
&lt;h3&gt;实物演示&lt;/h3&gt;
&lt;p&gt;一个电阻 + 一个电容，用便携式示波器两个探头分别测电容上端、下端：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;电源正极短接到电阻上端（模拟 S2 闭合）：下端（绿色线）立刻跳上去，然后慢慢下降，符合分析&lt;/li&gt;
&lt;li&gt;上端（黄色线）保持 3V 不变；把电源改接到上端时，黄色线跟着往上跳，绿色线也跳到 3V&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;示波器上有个坑：两个通道档位不同，会导致波形看起来像「短路在一起」，要把两通道档位调成一致再比较。&lt;/p&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;p&gt;答疑部分是学生提问，但里面藏着最实用的一个技巧。&lt;/p&gt;
&lt;h3&gt;核心方法&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;分析上电瞬间的电压波形时，只要电容两端电压为 0，就可以把电容当成一根导线（等效短路）来简化电路。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;严格前提：只有当电容两端没有压差（= 0V）时才可以这样等效。如果电容上已经有 1V，就不能当成导线。&lt;/p&gt;
&lt;h3&gt;三个问题的解答&lt;/h3&gt;
&lt;h4&gt;问题 1：电阻在电源底端&lt;/h4&gt;
&lt;p&gt;开关闭合瞬间，A 通道和 B 通道都瞬间跳到 5V。&lt;/p&gt;
&lt;p&gt;用等效法看：把电容当导线后，两点是同一根导线，5V 自然同时到两点，秒懂。&lt;/p&gt;
&lt;h4&gt;问题 2：电阻换到电源上方&lt;/h4&gt;
&lt;p&gt;开关闭合瞬间，电容下端维持 0V；随着充电，上端从 0 慢慢升到 5V。&lt;/p&gt;
&lt;h4&gt;问题 3：电路里完全没有串联电阻（只有开关）&lt;/h4&gt;
&lt;p&gt;电容下端依旧维持 0V，不会出现「GND 从 0 变 5V 又掉下来」的现象。&lt;/p&gt;
&lt;p&gt;原因：开关本身有内阻（例：10mΩ）。没有外部电阻时，开关内阻就不能忽略了，电路本质上仍等价于「有电阻」的电路。&lt;/p&gt;
&lt;h3&gt;另一种会让 GND 被抬起来的情况（和 PCB 走线有关）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;PCB 上的 GND 铜线本身也有电阻&lt;/strong&gt;（呼应电阻课：所有导体都有电阻）。&lt;/p&gt;
&lt;p&gt;如果 GND 走线画得非常细/非常差（假设也是 10mΩ），那么「开关 + 走线」总电阻就是 10mΩ。&lt;/p&gt;
&lt;p&gt;此时开关闭合瞬间，电容（等效导线）与这两个电阻构成分压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5V 经两个 10mΩ 分压  →  两点都变成 2.5V
随后一端升到 5V、一端降到 0V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是「地弹（ground bounce）」的成因，也是为什么大电流回路的 GND 走线要粗。&lt;/p&gt;
&lt;p&gt;仿真验证：需要提高仿真精度并把初始条件设为 0，才能看到这个瞬态。&lt;/p&gt;
&lt;h2&gt;拓展应用：为什么电源输入端要配 NTC 热敏电阻&lt;/h2&gt;
&lt;p&gt;这个拓展把「电压不能突变」直接连到了实际设计。&lt;/p&gt;
&lt;p&gt;现象：电源输入端常接大电容（例 1mF，甚至 1F 法拉电容）。上电瞬间电容未充电 = 短路，&lt;strong&gt;相当于把电源瞬间短路一下&lt;/strong&gt;，直到电容电压充到电源电压（例 12V）才解除。&lt;/p&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;/li&gt;
&lt;/ol&gt;
&lt;p&gt;解决：在输入回路串电感或 NTC 热敏电阻抑制上电脉冲电流。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NTC（负温度系数热敏电阻）：冷态电阻大 → 限制上电浪涌；电流流过发热后电阻变小 → 正常工作时损耗小。&lt;strong&gt;限流和低损耗靠温度自动切换，这就是它的巧妙之处。&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;仿真中即使串入 10mΩ 电阻，脉冲电流依旧非常大——&lt;strong&gt;说明这个浪涌是真实且严重的问题，不是理论上的小毛病。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;「相对电压不能突变」不等于「两端对地电压不能变」，两端可以同时跳。&lt;/li&gt;
&lt;li&gt;等效成导线的前提是电容两端压差为 0，有压差就不能等效。&lt;/li&gt;
&lt;li&gt;开关、导线、GND 铜箔都有电阻，电流大、走线细的时候不能忽略，会造成地弹。&lt;/li&gt;
&lt;li&gt;上电瞬间大电容 = 短路，会产生巨大浪涌，要串 NTC 或电感。&lt;/li&gt;
&lt;li&gt;示波器两个通道档位要一致才能正确比较波形；仿真看瞬态必须把初始条件设为 0 并提高精度，否则看不到。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电阻的阻值识别与功率</title><link>https://me.622168.xyz/posts/hardware/03-resistor-value-power/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/03-resistor-value-power/</guid><description>色环和丝印怎么读，以及为什么算完阻值还得算功率——附烧电阻实验的温度数据。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p4（15.3 分钟）、p5（12.4 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;前提：所有元器件都是非理想器件&lt;/h2&gt;
&lt;p&gt;买 10Ω 实测可能是 9.9Ω 或 10.1Ω。前面量过的 10Ω 电阻就是 9.9Ω，这次又碰到一个实测 9.7Ω 的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;微小偏差可能影响后期参数配置、放大电路的检测精度。&lt;/strong&gt; 不是所有场合都能忽略。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10%&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;最常见（普通色环电阻默认就是 5%）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;td&gt;一般精密电路&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0.1% / 0.001%&lt;/td&gt;
&lt;td&gt;仪器、基准&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;精度越高越贵，按场景选，不要无脑买高精度。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;插件电阻：色环怎么读&lt;/h2&gt;
&lt;p&gt;两类：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4 色环 = 前 3 环读数 + 第 4 环精度&lt;/li&gt;
&lt;li&gt;5 色环 = 前 4 环读数 + 第 5 环精度&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;先辨认方向&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;读数的几环间距近，精度环与它们间距明显大一点。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;方向搞反了读出来的值会差很多，先看间距。&lt;/p&gt;
&lt;h3&gt;读数规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;数值 = 前面几环组成的数字 × 10^N
（N = 紧邻精度环的那一环的数值）
&lt;/code&gt;&lt;/pre&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;黑&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;棕&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;红&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;黄&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;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;金&lt;/td&gt;
&lt;td&gt;作乘数时 = 0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;例子&lt;/h3&gt;
&lt;p&gt;5 色环：黄-紫-黑-黄 → &lt;code&gt;470 × 10⁴ = 4,700,000Ω&lt;/code&gt; = 4.7MΩ&lt;/p&gt;
&lt;p&gt;4 色环：红-红-黑 → &lt;code&gt;22 × 10⁰&lt;/code&gt; = 22Ω&lt;/p&gt;
&lt;p&gt;实物：棕-黑-黑-金-棕 → &lt;code&gt;100 × 0.1&lt;/code&gt; = 10Ω&lt;/p&gt;
&lt;p&gt;金色作乘数时是 ×0.1。按 ×0.1 算出来正好 10Ω，和实物标称一致，这个是对的。&lt;/p&gt;
&lt;h3&gt;速算法：数小数点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;3 个零 → K
再 3 个零 → M
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;4700000&lt;/code&gt; → 先划掉 3 个零得 &lt;code&gt;4700K&lt;/code&gt; → 再划掉 3 个零得 &lt;code&gt;4.7M&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;贴片电阻：丝印怎么读&lt;/h2&gt;
&lt;h3&gt;整数型（3 位或 4 位数字）&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;最后一位是 10 的 N 次方，前面数字直接照抄。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;103  →  10 × 10³  =  10KΩ
1003 →  100 × 10³ =  100KΩ      （4 位数，精度更高）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;带小数点型：字母 R 代表小数点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5R6   =  5.6Ω
R100  =  0.1Ω
30R9  =  30.9Ω
&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;h2&gt;电阻的功率&lt;/h2&gt;
&lt;h3&gt;公式（三个等价形式）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;P = U × I
P = I² × R
P = U² / R
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;核心结论&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;算出电阻值之后，不能随便选一个电阻。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;每个电阻都有额定功率。电流流过电阻会发热，实际损耗功率不能超过额定功率。&lt;/p&gt;
&lt;h3&gt;实验一：烧掉一个 1/4W 电阻&lt;/h3&gt;
&lt;p&gt;手上 10Ω 电阻（实测 9.7Ω），加 5V：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;P = U² / R = 25 / 10 = 2.5W
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个电阻是 1/4W（0.25W），实际损耗是额定的 10 倍。&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;电压提到 10V：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;P = 100 / 10 = 10W
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一接上立刻冒烟发光，随后电流变 0——内部金属薄膜被烧断了。&lt;/p&gt;
&lt;h3&gt;实验三：大功率电阻不会立刻坏，但会很烫&lt;/h3&gt;
&lt;p&gt;换成 2W / 10Ω 电阻，仍加 5V（P = 2.5W &amp;gt; 2W）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不冒烟，电流 0.5A 正常&lt;/li&gt;
&lt;li&gt;但用热成像仪看到温度飙到 150+℃，还在持续上升，预计平衡在 200℃ 左右&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;200℃ 已经可以熔锡，甚至能点着东西。有人用「电源 + 电阻」点过蚊香。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;结论：超额定功率不一定立刻坏，但发热极其严重。&lt;/strong&gt; 不冒烟不等于没问题。&lt;/p&gt;
&lt;h3&gt;选型规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 按实际电流/电压算出损耗功率 P
② 选额定功率 &amp;gt; P 的电阻
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工程上一般还要留余量（实际用 50%~70% 的额定值比较稳妥）。&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0402 贴片&lt;/td&gt;
&lt;td&gt;1/16W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0603 贴片&lt;/td&gt;
&lt;td&gt;1/10W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;0805 贴片&lt;/td&gt;
&lt;td&gt;1/8W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1206 贴片&lt;/td&gt;
&lt;td&gt;1/4W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直插小个头&lt;/td&gt;
&lt;td&gt;1/4W（最常见）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;直插大个头&lt;/td&gt;
&lt;td&gt;1/2W、1W、2W&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;带散热的水泥电阻&lt;/td&gt;
&lt;td&gt;5W ~ 100W+&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;色环先看间距判方向，读反了值差几个数量级。&lt;/li&gt;
&lt;li&gt;金色作乘数是 ×0.1，不是 ×10；贴片 &lt;code&gt;103&lt;/code&gt; 是 10KΩ 不是 103Ω，最后一位是 10 的次方。&lt;/li&gt;
&lt;li&gt;算完阻值必须算功率，这是新手最容易漏的一步，漏了就是冒烟。&lt;/li&gt;
&lt;li&gt;不冒烟 ≠ 没问题：2W 电阻吃 2.5W 时表面看着正常，实际已经 200℃。&lt;/li&gt;
&lt;li&gt;精度按需选，不要无脑上 0.1%；元器件都有偏差（10Ω 实测 9.7~10.1Ω 都正常），设计时按最坏情况留余量。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电阻的三大功能 限流 分流 分压</title><link>https://me.622168.xyz/posts/hardware/04-resistor-three-functions/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/04-resistor-three-functions/</guid><description>限流电阻怎么算、为什么并联十个 100Ω 而不是十个 10Ω、以及分压公式为什么和初中课本不一样。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p6（20 分钟）、p7（15 分钟）、p8（12 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实战电路里 60% 以上的电阻都是按限流算出来的&lt;/strong&gt;，复杂电路都是这些小原理组合而成。&lt;/p&gt;
&lt;h2&gt;限流&lt;/h2&gt;
&lt;h3&gt;什么时候需要&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;供电电压 &amp;gt; 负载工作电压&lt;/strong&gt;时，直连会损坏负载。&lt;/p&gt;
&lt;h3&gt;实验：LED 直连电源&lt;/h3&gt;
&lt;p&gt;红色 LED，逐步升压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.4V        不亮
1.5~1.7V    微微有光
1.9V        很亮，很璀璨
3.3V        视频里看着还亮，实际肉眼可见亮度慢慢变暗，手摸很热
4.4~4.95V   越来越暗
再往上      直接烧掉，完全灭掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：供电电压大于负载工作电压，负载必然异常。大得多就直接坏，只大一点可能不至于马上坏，但会持续受损。&lt;/p&gt;
&lt;p&gt;LED 参数：常见工作电压 1.6V ~ 3V；长脚为正、短脚为负。&lt;/p&gt;
&lt;h3&gt;限流电阻计算&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;R = U_R / I

其中 U_R = VCC − V_LED
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;最关键的一点：U 取的是「电阻两端的电压」，不是电源电压。&lt;/strong&gt; 这是初学者最常搞错的地方。&lt;/p&gt;
&lt;p&gt;回路关系：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VCC = V_R + V_LED          （串联分压）
I_电阻 = I_LED              （串联电流处处相等）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;算例&lt;/h3&gt;
&lt;p&gt;例 1：&lt;code&gt;VCC = 3.3V&lt;/code&gt;，&lt;code&gt;V_LED = 2V&lt;/code&gt;，LED 工作电流取 &lt;code&gt;5mA&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U_R = 3.3 − 2 = 1.3V
R = 1.3V / 5mA = 260Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例 2：&lt;code&gt;VCC = 8V&lt;/code&gt;，&lt;code&gt;V_LED = 2V&lt;/code&gt;，&lt;code&gt;I = 5mA&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U_R = 8 − 2 = 6V
R = 6V / 5mA = 1.2KΩ
&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;p&gt;不用先把 mA 换算成 A：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1.3 ÷ 5 = 0.26K = 260Ω
6 ÷ 5 = 1.2K
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个技巧能省掉大量换算错误。&lt;/p&gt;
&lt;h3&gt;实物验证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;8V 直连绿色 LED → 点上去闪一下立刻烧掉&lt;/li&gt;
&lt;li&gt;串上 1.5K（手上没有 1.2K，取最接近的值——实际生产不会有理想阻值）→ 3.1V 时还没亮（电阻有压降、电流被限制），一路拧到 8.3V 不烧&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算出来 1.2K，实际用 1.5K，电路照样工作。&lt;strong&gt;算出来的值只是个起点，最终要选标准系列里最接近的。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;分流&lt;/h2&gt;
&lt;h3&gt;负载电流大的时候&lt;/h3&gt;
&lt;p&gt;例：电机 &lt;code&gt;5V / 500mA&lt;/code&gt;，电源 10V。&lt;/p&gt;
&lt;h3&gt;先看单电阻限流的账&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;R = (10 − 5) / 0.5 = 10Ω
P = 5V × 0.5A = 2.5W          ← 电流全走这一个电阻
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实验一：电机直连&lt;/h3&gt;
&lt;p&gt;10V 直接点电机 → 电流接近 1A，电机发烫，闻到金属烧焦味（像空锅干烧的味道），长时间会烧毁。&lt;/p&gt;
&lt;h3&gt;实验二：串一个 0.25W 的 10Ω&lt;/h3&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;pre&gt;&lt;code&gt;总功耗 2.5W ÷ 每个小电阻 0.25W = 需要 10 个
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键约束&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;并联后的总阻值必须仍然等于 10Ω。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;✓ 正确：10 个 100Ω 并联  →  100/10 = 10Ω
✗ 错误：10 个 10Ω 并联   →  10/10 = 1Ω   →  限不住电流了！
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;并联是把阻值变小，所以单个电阻的阻值要相应放大。这一步很多人漏掉。&lt;/p&gt;
&lt;h3&gt;实物验证&lt;/h3&gt;
&lt;p&gt;10 个 100Ω 并联后直接怼到电机上（未焊接）：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不再冒烟，能正常工作&lt;/li&gt;
&lt;li&gt;热成像：电机 62℃、电阻 30 多℃&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;电阻仍会温热，电机依然烫手（电机电压仍偏高）。&lt;/p&gt;
&lt;h2&gt;分压&lt;/h2&gt;
&lt;h3&gt;限流和分压是一回事&lt;/h3&gt;
&lt;p&gt;回顾：限流电阻本质上就是帮负载分担掉一部分电压，即分压。&lt;/p&gt;
&lt;h3&gt;分压解法&lt;/h3&gt;
&lt;p&gt;当没有大功率电阻、也不能并联分流时：&lt;/p&gt;
&lt;p&gt;约束分析：不能加并联支路 → 电阻上的 &lt;code&gt;I&lt;/code&gt; 不变 = 0.5A；要让 &lt;code&gt;P &amp;lt; 0.25W&lt;/code&gt;，只能减小 U。&lt;/p&gt;
&lt;p&gt;做法：串联多个电阻。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;串得越多 → 每个电阻分到的压降越小 → 每个电阻的功耗越小 → 都不超额定
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：串联 10 个 1Ω（总阻值还是 10Ω）分担 5V，每个电阻压降约 0.5V。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每个电阻功耗 = 0.5V × 0.5A = 0.25W   ← 刚好压线
&lt;/code&gt;&lt;/pre&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;放大（100Ω）&lt;/td&gt;
&lt;td&gt;缩小（1Ω）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;总阻值&lt;/td&gt;
&lt;td&gt;保持 10Ω&lt;/td&gt;
&lt;td&gt;保持 10Ω&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每个元件的电流&lt;/td&gt;
&lt;td&gt;变小&lt;/td&gt;
&lt;td&gt;不变&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;每个元件的电压&lt;/td&gt;
&lt;td&gt;不变&lt;/td&gt;
&lt;td&gt;变小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两者都是为了把「单个元件的功耗」降到额定以下，但走的路完全不同。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;一条路走不通就换一条路，这是电路设计的拓展思维。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;分压做参考电压（最常用）&lt;/h3&gt;
&lt;p&gt;两个电阻串联，中间节点电压必然小于电源电压。例：&lt;code&gt;VCC = 5V&lt;/code&gt;、上 1K、下 1K → 中点 2.5V。&lt;/p&gt;
&lt;h3&gt;分压公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;V_R = V2 + (V1 − V2) × R2 / (R1 + R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;R1&lt;/code&gt; 在 &lt;code&gt;V1&lt;/code&gt; 侧，&lt;code&gt;R2&lt;/code&gt; 在 &lt;code&gt;V2&lt;/code&gt; 侧&lt;/li&gt;
&lt;li&gt;&lt;code&gt;V_R&lt;/code&gt; 取 &lt;code&gt;R2&lt;/code&gt; 两端的电压&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;为什么和初中课本的公式不一样&lt;/h3&gt;
&lt;p&gt;课本公式默认底部接 GND，即 &lt;code&gt;V2 = 0&lt;/code&gt;，代入后化简为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V_R = V1 × R2 / (R1 + R2)      ← 课本版，只在底部接地时成立
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;但实际电路中 &lt;code&gt;V2&lt;/code&gt; 经常不等于 0&lt;/strong&gt;，所以完整式才是通用的。&lt;/p&gt;
&lt;h3&gt;数值验证&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;V1&lt;/th&gt;
&lt;th&gt;V2&lt;/th&gt;
&lt;th&gt;R1&lt;/th&gt;
&lt;th&gt;R2&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10V&lt;/td&gt;
&lt;td&gt;2V&lt;/td&gt;
&lt;td&gt;1K&lt;/td&gt;
&lt;td&gt;2K&lt;/td&gt;
&lt;td&gt;7.3333V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10V&lt;/td&gt;
&lt;td&gt;2V&lt;/td&gt;
&lt;td&gt;10K&lt;/td&gt;
&lt;td&gt;2K&lt;/td&gt;
&lt;td&gt;3.3333V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;表格计算和仿真互验，结果一致（7.33V）。&lt;/p&gt;
&lt;p&gt;我第一次填表算出来是 4.666V，原因是表格公式写反了（分子取成了 R1）。改过来才对上。&lt;/p&gt;
&lt;p&gt;用配套表格填参数即可，不用手推。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;p9 开头专门纠正了这个公式错误，配套表格也同步改了。&lt;strong&gt;这种地方谁都可能写错，算完一定要用数值回代验证。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;三者的关系&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;供电电压 &amp;gt; 负载电压
    ↓
加一个电阻把多余的电压吃掉  ← 这就是限流
    ↓
但电阻会发热，功率不能超额定
    ↓
    ├─ 想保住总阻值，又要降单个功耗 → 并联分流
    └─ 不能加支路，就串联分压
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;三个功能本质上是同一个问题的三种解法。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;限流公式里的 U 是电阻两端的电压，不是电源电压；&lt;code&gt;伏 ÷ 毫安 = 千欧&lt;/code&gt;，别把 mA 换成 A 再算。&lt;/li&gt;
&lt;li&gt;并联时单个阻值要放大（10 个 10Ω 并联变成 1Ω，限流就失效了）；串联分压时单个阻值要缩小，每个电阻的功耗要单独验算。&lt;/li&gt;
&lt;li&gt;分压公式的通用形式带 V2，课本那个简化版只在底部接地时成立；算完用数值回代验证一遍，公式写反这种事谁都会犯。&lt;/li&gt;
&lt;li&gt;算出来的阻值要选标准系列，1.2K 买不到就买 1.5K，电路照样工作。&lt;/li&gt;
&lt;li&gt;限流电阻会消耗功率，大电流场合用它限流很不划算，应该改电源电压。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>容抗与低通滤波</title><link>https://me.622168.xyz/posts/hardware/09-capacitive-reactance-lowpass/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/09-capacitive-reactance-lowpass/</guid><description>容抗公式怎么来的、为什么电容隔直通交、截止频率和 -3dB 是什么、低通滤波的两种理解方式。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p16（6.4 分钟）、p17（13.8 分钟）、p18（14.7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;容抗&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Xc = 1 / (2πfC)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;Xc&lt;/code&gt; 容抗，单位欧姆；&lt;code&gt;f&lt;/code&gt; 频率，单位 Hz；&lt;code&gt;C&lt;/code&gt; 容量，单位 F。&lt;/p&gt;
&lt;p&gt;两条结论：频率 f 越高，容抗越小；频率一定时，电容 C 越大，容抗越小。&lt;/p&gt;
&lt;h3&gt;直流不通、交流通&lt;/h3&gt;
&lt;p&gt;直流 f = 0，容抗无穷大，相当于断路，直流过不来。交流 f ≠ 0，容抗有具体值，和后级负载分压，容值取合适就能通过。&lt;/p&gt;
&lt;p&gt;「直流不通」的本质不是「电容是断路」。电容物理上确实是断开的，但真正的解释是容抗趋于无穷大——这个视角分析交流电路时更有用。&lt;/p&gt;
&lt;p&gt;从电流角度看，交流电源直接并联电容，测电源工作电流 I：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f 越高 → 容抗越小 → I 越大
C 越大 → 容抗越小 → I 越大
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真：换成 DC 电源（f = 0）12V，电流只有 12pA（来自仿真模型的漏电，约等于没有）；改成交流电源，出现交变电流；幅值不变、频率改成 10K，电流明显变大；频率幅值不变、电容 1µF → 100µF，电流进一步增大。&lt;/p&gt;
&lt;h2&gt;低通滤波&lt;/h2&gt;
&lt;p&gt;频率低时能通过，频率高时通过后有衰减，越高衰减越严重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vin ──[ R ]──┬── Vout
             │
            ─┴─ C
             │
            GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;R 在前、C 在后，输出取电容两端的电压。&lt;/p&gt;
&lt;p&gt;截止频率：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fT = 1 / (2πRC)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;由 R 和 C 共同决定。这个公式和容抗公式长得像，但不是一回事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Xc = 1/(2πfC)&lt;/code&gt; —— 给定 f 和 C，算阻抗&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fT = 1/(2πRC)&lt;/code&gt; —— 给定 R 和 C，算截止频率&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;截止频率的定义：输入信号频率等于截止频率时，输出信号衰减 −3dB，也就是衰减成自身的 0.707 倍。&lt;/p&gt;
&lt;p&gt;例：输入 1Vpp、1KHz 的正弦波，电路 fT = 1KHz，输出频率仍是 1KHz，但峰峰值变成 ±0.707V。&lt;/p&gt;
&lt;p&gt;仿真：R = 1K、C = 1µF 时 fT 不是 1K；换成 159Ω + 1µF，&lt;code&gt;fT = 1/(2π×159×1µF) ≈ 1000Hz&lt;/code&gt;，容抗公式倒过来用就能算，配套表格可以直接算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1Vpp / 1KHz   →  707mV（= 0.707 倍）
1Vpp / 10KHz  →  只剩 ±100mV（明显衰减）
1Vpp / 100Hz  →  约等于 1Vpp（几乎不衰减）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是「低通」的含义：低频原样通过，高频被砍掉。&lt;/p&gt;
&lt;h3&gt;典型应用：滤高频噪声&lt;/h3&gt;
&lt;p&gt;实际电路里，1KHz 的有用信号上多多少少会叠上高频噪声（外界电压抖动、处理不当等）。加一个 RC 低通就能滤掉：白色是纯净的 1Vpp / 1KHz 正弦波，红色是叠了 100KHz / 0.1Vpp 噪声后的波形（毛刺明显），经 RC 低通后输出又变回很漂亮的正弦波。&lt;/p&gt;
&lt;p&gt;反过来，要滤除低频噪声，就得用高通滤波电路。&lt;/p&gt;
&lt;h2&gt;低通滤波的本质原理&lt;/h2&gt;
&lt;p&gt;有两种理解方式。&lt;/p&gt;
&lt;p&gt;一是阻抗分压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vout = Xc / (Xc + R) × Vin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;f 越高，&lt;code&gt;Xc&lt;/code&gt; 越小，电容分到的电压越低，输出越小；f 越低，&lt;code&gt;Xc&lt;/code&gt; 越大，电容分到的电压越大，输出越大。就是把 RC 当成一个分压电路，只不过其中一个电阻换成了随频率变化的容抗。&lt;/p&gt;
&lt;p&gt;二是物理本质，也就是电容的充放电。R 和 C 一旦确定，电容的充放电速度就确定了（由 RC 决定）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;直流                → 电压慢慢上升，最终等于电源电压，输出等于输入
交流变化远快于充放电 → 跟不上，信号到最大值时电容电压还升不上去，输出衰减
交流频率很低        → 跟得上，电容电压贴着输入信号走，输出 ≈ 输入
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根本原因：电容的充放电速度跟不上输入信号的变化速度。输入变化越快，电容上得到的电压越小，衰减越多。&lt;/p&gt;
&lt;p&gt;实物演示：信号源 + 一个 R + 一个 C，黄色探头测输入、绿色探头测输出。输入频率很高时绿色变成一条直线（衰减到极小，垂直档位调到最小也只能看到一点点浮动）；50Hz 时绿色开始出现波形；10Hz 时绿色波形更高。&lt;strong&gt;输入频率越低，输出幅值越高。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;波形上能直接看到电容电压滞后于输入信号：输入已经升到最大值，电容还在慢慢上升；等电容电压追平输入信号，下一刻输入就低于电容电压，电容开始放电。&lt;/p&gt;
&lt;p&gt;本质理解不了就用阻抗分压去理解，两种理解等价，选顺手的那个。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Xc = 1/(2πfC)&lt;/code&gt; 和 &lt;code&gt;fT = 1/(2πRC)&lt;/code&gt; 别混，一个算阻抗一个算截止频率。&lt;/li&gt;
&lt;li&gt;直流 f = 0 时容抗无穷大，「隔直」是这个原因，不是「电容物理断开」这么简单。&lt;/li&gt;
&lt;li&gt;截止频率处输出是输入的 0.707 倍，不是 0.5 倍，也不是完全通不过。&lt;/li&gt;
&lt;li&gt;低通里 R 在前 C 在后，输出取电容两端；接反就变成高通。&lt;/li&gt;
&lt;li&gt;衰减是渐变的，过了截止频率不是完全没了——10 倍频率时还有约 100mV（0.1 倍）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电容的稳压与滤波</title><link>https://me.622168.xyz/posts/hardware/08-capacitor-regulation-filtering/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/08-capacitor-regulation-filtering/</guid><description>电压跌落是怎么发生的，以及为什么芯片旁边非要加一堆电容。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p15（19 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;总纲&lt;/h2&gt;
&lt;p&gt;电容的滤波作用本质上就是稳定电压。不管是低频储能滤波还是高频噪声滤波，最终目的都是稳定电容两端的电压。&lt;/p&gt;
&lt;p&gt;所以「为什么要给芯片 VCC/GND 加一堆电容」，答案就是为了稳住 VCC 上的电压。&lt;/p&gt;
&lt;h2&gt;电压跌落是怎么发生的&lt;/h2&gt;
&lt;p&gt;芯片工作电压 3.3V，供电的同时还带一个负载（比如一个较大的器件 B）。稳态下电源流出 10mA，全部给芯片。&lt;/p&gt;
&lt;p&gt;某一时刻负载开关闭合，理论电流要从 10mA 突变到 1.01A（负载要 1A）。但电源本身有反应时间，反馈环路得先响应；电源离芯片又远，供电线有长度、有电感。所以电流不可能瞬间提上去。&lt;/p&gt;
&lt;p&gt;供电不足，3.3V 就掉下去了，等电源反应过来再慢慢升回 3.3V。这就是电压跌落（voltage droop）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;电压跌落很容易让芯片工作异常——死机、复位。&lt;/strong&gt; 嵌入式里最常见的疑难杂症之一就是：代码没问题、逻辑没问题，偶尔就是复位，八成是电源跌落。&lt;/p&gt;
&lt;h2&gt;电容为什么能救&lt;/h2&gt;
&lt;p&gt;电容等效成一个小电池，能瞬间释放自身能量、维持两端电压不突变，把跌落幅度压得很小（还是掉一点，但幅度小得多）。&lt;/p&gt;
&lt;p&gt;电容不需要反应时间，它就在芯片旁边。电源远、慢，电容近、快，这就是去耦电容存在的意义。&lt;/p&gt;
&lt;h2&gt;两个演示&lt;/h2&gt;
&lt;p&gt;无电容：&lt;code&gt;V1 = 3.3V&lt;/code&gt;（实操用 3V）供电，两个 1K 电阻分压，中点 1.5V（3.3V 时是 1.65V），电容初始无电。用一根线把电容下端（等效开关）短到 GND，中点电压瞬间被拖到接近 0V，之后慢慢充电回升。这就是电压跌落，后级真接了电路大概率会死机。&lt;/p&gt;
&lt;p&gt;有电容：在中点并联一个大电容，同样操作。仍会有一个向下的尖峰，但跌落幅度小很多，不会掉到 0。芯片旁边为什么非要加一堆电容，原因就在这。&lt;/p&gt;
&lt;h2&gt;抓波形的几个要点&lt;/h2&gt;
&lt;p&gt;这几个地方我都卡过：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;触发类型要选对（这里选 CHB）&lt;/li&gt;
&lt;li&gt;触发阈值要设在 1.5V 以下——分压点是 1.5V 而且会往下掉，阈值设成 1.5V 就触发不到&lt;/li&gt;
&lt;li&gt;每次触发前先给电容放电，否则没反应&lt;/li&gt;
&lt;li&gt;时间档位要调精确&lt;/li&gt;
&lt;li&gt;手不能抖，一抖波形就很难看&lt;/li&gt;
&lt;li&gt;电容负极必须用线接到 GND，不能默认已经接了&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;去耦电容的工程实践&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每个芯片电源脚旁边放一个 100nF（0.1µF），就近提供瞬态电流&lt;/li&gt;
&lt;li&gt;电源入口放大容量电解电容（10µF~1000µF），提供大能量缓冲&lt;/li&gt;
&lt;li&gt;大小电容并联用，大电容管低频、小电容管高频（ESL 不同）&lt;/li&gt;
&lt;li&gt;电容尽量靠近芯片电源脚，走线电感会削弱效果&lt;/li&gt;
&lt;li&gt;电源脚旁边还要配 10µF 左右的储能电容，应对较大的电流突变&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大电容不能替代小电容：大电容的 ESL 大，高频时表现得像电感，反应不过来，高频瞬态得靠小容量的 MLCC。小电容也不能替代大电容：存的能量太少，撑不住大的电流突变。两个都要有。&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;li&gt;示波器触发阈值要设在信号会经过的电平上，设错根本抓不到。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>高通滤波电路</title><link>https://me.622168.xyz/posts/hardware/10-highpass-filter/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/10-highpass-filter/</guid><description>只把低通的输出位置换一下，就变成了高通——以及工频干扰该怎么处理。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p19（10.9 分钟）、p20（3.1 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;应用背景&lt;/h2&gt;
&lt;p&gt;有用信号是高频，但上面叠了低频噪声（比如 50Hz 工频干扰），表现为信号的峰峰值来回抖动。要把低频噪声剥掉，就得用高通滤波。&lt;/p&gt;
&lt;h2&gt;先说清楚「高频/低频」指什么&lt;/h2&gt;
&lt;p&gt;这里说的高频/低频是相对于该 RC 电路截止频率的相对概念，不是常说的「大于 20MHz 算高频、低于 2MHz 算低频」。&lt;/p&gt;
&lt;p&gt;对 1KHz 截止频率的电路来说，100Hz 就是低频，10KHz 就是高频。&lt;strong&gt;永远相对于 fT 而言。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;电路结构&lt;/h2&gt;
&lt;p&gt;和低通只差一个元件位置：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;低通：Vin ──[ R ]──┬── Vout       高通：Vin ──[ C ]──┬── Vout
                   │                                │
                  ─┴─ C                            [ R ]
                   │                                │
                  GND                              GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;低通是 R 在前、C 在后，输出取电容两端；高通是 C 在前、R 在后，输出取电阻两端。&lt;/p&gt;
&lt;p&gt;原理是一样的：电阻上的电压 = 输入信号 − 电容上的电压。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出电容 → 低通（取「电容上留住的」，高频留不住）
输出电阻 → 高通（取「电容上丢掉的」，高频丢不掉）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;截止频率公式完全相同，都是 &lt;code&gt;fT = 1 / (2πRC)&lt;/code&gt;，只是输出对象变了。&lt;/p&gt;
&lt;h2&gt;仿真验证&lt;/h2&gt;
&lt;p&gt;用 159Ω + 1µF（&lt;code&gt;fT ≈ 1KHz&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1KHz / 1Vpp（= 截止频率） →  707mV，= 输入的 0.707 倍（和低通一致）
10KHz                    →  约等于输入，高频通过
100Hz                    →  严重衰减，低频被砍掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 1KHz 那行：低通和高通在截止频率处输出幅度完全一样（都是 0.707 倍），这是必然的——两者输出的和就是输入。&lt;/p&gt;
&lt;h2&gt;叠加噪声演示&lt;/h2&gt;
&lt;p&gt;有用信号 100KHz，叠上 50Hz / 1V 低频噪声：纯 100KHz 时峰峰值是一条直线（很漂亮）；叠上 50Hz 后峰峰值来回变化；经高通滤波后红色输出变平了，50Hz 低频噪声被衰减掉。&lt;/p&gt;
&lt;h2&gt;常见应用场景&lt;/h2&gt;
&lt;p&gt;典型的噪声源是工频干扰（50Hz 市电干扰）。电源端隔离没做好，很容易把工频干扰叠到信号上。&lt;/p&gt;
&lt;p&gt;示波器本身也会引入工频干扰：探头夹子没夹好、没有共地就去测信号，本来干净的信号在示波器上就变成带 50Hz 干扰的波形。这是很常见的自欺欺人——以为是电路有问题，其实是测量方法有问题。&lt;/p&gt;
&lt;p&gt;处理顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;1. 从根源解决：该隔离的隔离好，该接地/共地的做好，从本质上把噪声去掉
2. 实在改不了，再在电路里加高通滤波把它滤掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;先找根源，再加滤波。加滤波是掩盖问题，不是解决问题。&lt;/p&gt;
&lt;p&gt;如果是共模噪声，要用共模滤波——在两条输入线（例如差分输入）上各加电容、电阻，组成共模高频滤波。具体拓扑这里不展开，道理都一样。&lt;/p&gt;
&lt;h2&gt;低通 vs 高通&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;元件顺序&lt;/td&gt;
&lt;td&gt;R → C&lt;/td&gt;
&lt;td&gt;C → R&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;高频&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;code&gt;1/(2πRC)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1/(2πRC)&lt;/code&gt;（相同）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;fT 处输出&lt;/td&gt;
&lt;td&gt;0.707 倍&lt;/td&gt;
&lt;td&gt;0.707 倍（相同）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;用途&lt;/td&gt;
&lt;td&gt;滤高频噪声、平滑信号&lt;/td&gt;
&lt;td&gt;滤工频干扰、隔直流&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;记忆&lt;/td&gt;
&lt;td&gt;留电容上的&lt;/td&gt;
&lt;td&gt;留电阻上的&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;记法：电容隔直通交，电容上留下的就是直流/低频，所以输出取电容的是低通。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;「高频/低频」是相对 fT 的，不是绝对频率，别拿射频的标准套。&lt;/li&gt;
&lt;li&gt;高通就是 C 在前 R 在后，和低通只差元件顺序，别记反。&lt;/li&gt;
&lt;li&gt;fT 处两种滤波输出都是 0.707 倍，这是必然结果，不是巧合。&lt;/li&gt;
&lt;li&gt;先找噪声根源再加滤波。工频干扰多半是隔离/接地没做好，加滤波只是掩盖。&lt;/li&gt;
&lt;li&gt;高通滤波会滤掉直流分量，信号里要保留直流偏置就不能用高通。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电容的延时应用</title><link>https://me.622168.xyz/posts/hardware/07-capacitor-delay/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/07-capacitor-delay/</guid><description>时间常数 τ 怎么用，上电延时和断电延时两个经典电路，以及选型时为什么先定电容再调电阻。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p13（17.9 分钟）、p14（6 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;电容 = 小电池&lt;/h2&gt;
&lt;p&gt;可以储能。&lt;strong&gt;充电速度由两个因素决定&lt;/strong&gt;：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;充电电流（由串联的电阻 R 决定）&lt;/li&gt;
&lt;li&gt;电容容量 C&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;时间常数 τ&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;τ = R × C
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单位是秒。&lt;strong&gt;τ 是描述充放电快慢的核心参数。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;充电进度表（以 VCC = 10V 为例）&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;10V 对应&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;63%&lt;/td&gt;
&lt;td&gt;6.3V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2τ&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;td&gt;8.6V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3τ&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;9.5V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4τ&lt;/td&gt;
&lt;td&gt;99.8%&lt;/td&gt;
&lt;td&gt;9.98V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5τ&lt;/td&gt;
&lt;td&gt;99%&lt;/td&gt;
&lt;td&gt;9.9V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;例：&lt;code&gt;R = 1K&lt;/code&gt;、&lt;code&gt;C = 1µF&lt;/code&gt; → &lt;code&gt;τ = 1000 × 1×10⁻⁶ = 1ms&lt;/code&gt;，即 1ms 时充到 6.3V。&lt;/p&gt;
&lt;p&gt;经验规则：3~5 个 τ 就认为电容已充满（3τ 以上就认为电容电压 ≈ 电源电压）。&lt;/p&gt;
&lt;h2&gt;应用一：上电延时（芯片使能）&lt;/h2&gt;
&lt;h3&gt;电路&lt;/h3&gt;
&lt;p&gt;芯片有 EN（使能）引脚，接高电平芯片才开始工作。&lt;/p&gt;
&lt;p&gt;设 EN 阈值为 2V，供电 3.3V。用 RC 给上电时间做延时——&lt;strong&gt;只要设好 R、C，就能控制电压从 0 升到 2V 的时间点&lt;/strong&gt;，从而任意控制上电延迟。&lt;/p&gt;
&lt;h3&gt;设计实例：目标延时 10ms&lt;/h3&gt;
&lt;p&gt;已知：充电电压 3.3V，目标阈值 2V，初值 0。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;先试 10K + 1µF   →  差一点
调成 11K + 1µF    →  ≈ 10ms  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;选型顺序&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;先定电容，再调电阻。&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为什么：电容的标准容量很少，是 1µF → 2.2µF → 3.3µF → 4.7µF 这样的档位，没有 1.1µF、1.51µF 这种值；而电阻的阻值档位密得多，可以慢慢调。&lt;/p&gt;
&lt;p&gt;如果反过来先定电阻再挑电容，会发现自己想要的电容值买不到。&lt;/p&gt;
&lt;h3&gt;仿真验证&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;充电电压 3.3V：从 0 充到 2V 约 10ms&lt;/li&gt;
&lt;li&gt;充电电压 12V：只要 2ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;充电电压越高，达到阈值越快。&lt;/strong&gt; 表格和仿真一致。&lt;/p&gt;
&lt;h3&gt;实物演示&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;10K + 10µF&lt;/code&gt;，3V 充电到 2V，理论 109ms。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;τ = 10K × 10µF = 100ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示波器设置：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;时间档位调到 50ms 或 100ms&lt;/li&gt;
&lt;li&gt;垂直档位 2V/格&lt;/li&gt;
&lt;li&gt;先把触发调好、时间档位调好，再上电&lt;/li&gt;
&lt;li&gt;用手水平测量功能量时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;实测约 116ms&lt;/strong&gt;，与理论 109ms 很接近（差异来自测量点选取和手抖）。&lt;/p&gt;
&lt;h2&gt;应用二：断电延时&lt;/h2&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;电容充满电后相当于电池，&lt;strong&gt;电源撤走后可以继续对外放电&lt;/strong&gt;，使后级电路慢慢关断而不是立刻断电。&lt;/p&gt;
&lt;h3&gt;对比&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;电源 + 开关 + LED：开关闭合 LED 立刻亮，断开立刻灭&lt;/li&gt;
&lt;li&gt;上面再并一个电容：开关闭合电容储能；断开后 LED 慢慢灭，持续一小段时间&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;电容越大，关断延时越长；电容越小，时间越短。&lt;/p&gt;
&lt;h3&gt;实际意义&lt;/h3&gt;
&lt;p&gt;这里的 LED 可以代表一整块电路板。电容越大，关断延时越久，类比手机电池容量越大待机越久。&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;利用的是&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;芯片使能时序、上电复位&lt;/td&gt;
&lt;td&gt;掉电保存数据、平稳关机&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;时间由&lt;/td&gt;
&lt;td&gt;τ = RC&lt;/td&gt;
&lt;td&gt;τ = RC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;τ = R × C&lt;/code&gt;，注意单位：R 用欧姆、C 用法拉，结果才是秒。&lt;code&gt;1K × 1µF = 1ms&lt;/code&gt; 要能秒算。&lt;/li&gt;
&lt;li&gt;3~5τ 才算充满，用 1τ 估时间会严重偏小。&lt;/li&gt;
&lt;li&gt;先定电容再调电阻——电容标准值稀疏，电阻密。&lt;/li&gt;
&lt;li&gt;充电电压会影响达到阈值的时间，改电压要重新算。&lt;/li&gt;
&lt;li&gt;示波器要先把触发和时间档调好再上电；延时时间还受电容精度和漏电流影响，电解电容容差 ±20%，做精确定时要选好电容或改用其他方案。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电感基础与基本特性</title><link>https://me.622168.xyz/posts/hardware/11-inductor-basics/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/11-inductor-basics/</guid><description>电感的镜像特性「电流不能突变」，以及一个必须记牢的边界：电感只改变速度，不改变电流最大值。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p23（6.3 分钟）、p24（11.5 分钟）、p25（8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;电感是什么&lt;/h2&gt;
&lt;p&gt;结构就是一根导线加一个磁性材料（磁芯）。&lt;/p&gt;
&lt;p&gt;实物有两种形态：裸露线圈型能直接看到线圈绕在黑色磁芯上；贴片型/屏蔽型看不到线圈，外形方方正正，带屏蔽罩，底部有两个贴片焊盘，一般黑色或灰色。&lt;/p&gt;
&lt;p&gt;磁芯的作用是增强/控制电感量。任意线圈本身就是电感，但裸线圈电感量很小；加了磁性材料就能在小体积下做到较大电感量。&lt;/p&gt;
&lt;p&gt;量级参考：绕 10 圈约 1µH，剪成 3~4 圈约 0.3µH，剪成一条直线仍是电感，但非常非常小。&lt;/p&gt;
&lt;p&gt;三个「所有」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;所有两块不连通的导体  →  都是一个电容
任何一根导线          →  都有电感（只是很小）
任何非超导导线        →  都有电阻
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以导线 = 很小的电阻 + 很小的电感。掌握这一点有助于分析非理想状态下的电感和电容——走线长度、粗细为什么会影响电路性能，答案就在这里。&lt;/p&gt;
&lt;p&gt;生活里的电感：电机（磁芯 + 线圈）、喇叭（大线圈 + 大磁铁）。所有由线圈组成的器件都是电感。&lt;/p&gt;
&lt;h2&gt;电感的基本特性&lt;/h2&gt;
&lt;p&gt;流过电感的电流不能突变。这是电感唯一的基本特性，它的全部电路功能都源于此。&lt;/p&gt;
&lt;p&gt;对比演示：5V 电源 + 两个 10kΩ 电阻串联分压。纯电阻电路里，开关一闭合电流立刻从 0 跳到 &lt;code&gt;5V / 20kΩ = 0.25mA&lt;/code&gt;，断开瞬间立刻回 0；串入电感后，电流从 0 慢慢上升到 0.25mA 才变平（曲线类似电容充电）。分压点电压也一样：纯电阻瞬间跳到 2.5V，带电感的一路慢慢上升。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;电感会抑制电路中电流的上升速度，下降速度同样被抑制。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实物演示：两个 10kΩ 电阻 + 中间一个电感，示波器两路共地。黄通道（A）接纯电阻分压点，立刻升到 2.5V；绿通道接带电感那一路，慢慢上升。原因就是电感的电流不能突变。&lt;/p&gt;
&lt;h2&gt;必须记牢的边界&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;电感只限制电流的变化速度，不改变电路的电流最大值。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;5V + 10Ω
不加电感：I = 5 / 10 = 0.5A
加电感后：I 最终仍等于 0.5A，只是从 0 升到 0.5A 需要一段时间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电感线圈自身电阻会影响一点点，但基本影响不大。&lt;/p&gt;
&lt;p&gt;仿真：5V、10Ω、电感 10µH，两个开关 + 两个电流探头。白线（纯电阻）瞬间从 0 跳到 500mA；红线（带电感）慢慢上升，最终与白线重合。&lt;strong&gt;「最终重合」是重点。&lt;/strong&gt; 电感不是限流元件，它是缓变元件。&lt;/p&gt;
&lt;h2&gt;电感 vs 电容：一对镜像&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;不能突变的量&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;断路（隔直）&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;&lt;code&gt;Xc = 1/(2πfC)&lt;/code&gt;，f↑ → 阻抗↓&lt;/td&gt;
&lt;td&gt;&lt;code&gt;XL = 2πfL&lt;/code&gt;，f↑ → 阻抗↑&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;把这两列对照着记，比单独背两个快得多。后面所有 RC/RL/LC 滤波电路，都是这一对镜像的排列组合。&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;li&gt;磁芯会饱和，饱和后电感量骤降、电流暴涨。&lt;/li&gt;
&lt;li&gt;电感的电流不能突变，意味着断开回路会产生高压——这是电感最危险也最有用的性质。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电感回路、感抗与滤波</title><link>https://me.622168.xyz/posts/hardware/12-inductor-reactance-filter/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/12-inductor-reactance-filter/</guid><description>断开电感回路为什么会打出几百伏、感抗怎么算、LR 滤波和 RC 滤波该怎么选、LC 谐振为什么会放大。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p26（21.4 分钟）、p27（4.6 分钟）、p28（15 分钟）、p29（10.8 分钟）、p30（16.5 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;断开电感回路会打出高压&lt;/h2&gt;
&lt;p&gt;电路：5V + 10Ω + 电感 + 开关。开关闭合，电流从 0 慢慢升到 0.5A，然后突然断开。&lt;/p&gt;
&lt;p&gt;电感电流不能突变——上升不能突变，下降也不能突变。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 开关断开后，电感回路「没有了」
② 但电感电流仍在 0.5A（不能突变）
③ 电感会强制把自身储存的电流能量释放出来，强行让回路继续流过电流
④ 电流跑到开关两端的寄生电容上
   （机械开关 = 两块不连通的导体 = 一个电容，可以储电荷）
⑤ 寄生电容非常小 → 两端产生很高电压
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;开关断开瞬间两端会产生高压，轻轻松松几百伏，电感很大时甚至几千、上万伏。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;机理上，开关断开可以看作电阻变成无穷大。电感为了维持电流不突变，可以感生出无穷大的电压去维持电流。&lt;/p&gt;
&lt;p&gt;数值演算：断开后等效电阻为 100kΩ 时，电感为维持 0.5A 需要感生&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;U = I × R = 0.5A × 100kΩ = 50kV
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;等效电阻 10kΩ 时感生 5000V，100kΩ 时 50kV。仿真里示波器档位开到 10kV/格 才看得到尖峰，读数为正负十千伏。&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;p&gt;解决办法是加一个二极管做续流回路：开关闭合时电流按原路径走，仍从 0 慢慢升到 0.5A；开关断开后原回路消失，但电流「发现」二极管这条新回路，不会突然变化、不产生高压，慢慢降到 0。&lt;/p&gt;
&lt;p&gt;实物验证：无续流二极管时产生的是负压，需用负压触发，测得 Vpp = 163V；示波器采样率较低，实际电压肯定比 163V 更高。有续流二极管时，断开瞬间电压往下跳，随后被二极管钳位，最终约等于 0.7V。&lt;/p&gt;
&lt;p&gt;不建议自己在家这样玩，很容易把示波器打坏。非要玩就把电阻调大、电感减小。&lt;/p&gt;
&lt;h2&gt;感抗&lt;/h2&gt;
&lt;p&gt;电感与电容一样是储能器件，不损耗；但它对交流电有阻碍作用，这个阻碍叫感抗。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;XL = 2πfL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电感固定时，频率越高感抗越大；频率一定时，电感越大感抗越大。方向和容抗正好相反：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;容抗 Xc = 1/(2πfC)   →  f↑ 则 Xc↓
感抗 XL = 2πfL       →  f↑ 则 XL↑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真：交流电源 + 电感，1kHz / 1Vpp / 1mH，电流峰峰值约 300mA。增大电感，感抗变大、电流减小；电感减到 100µH，感抗变小、电流变大很多；提高频率，感抗变大、电流减小；频率改 100Hz，感抗变小、电流变大。&lt;/p&gt;
&lt;p&gt;计算示例：&lt;code&gt;f = 1kHz&lt;/code&gt;、&lt;code&gt;L = 100µH&lt;/code&gt; → &lt;code&gt;XL = 2π × 1000 × 100×10⁻⁶ = 0.628Ω&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;LR 低通滤波&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Vin ──[ L ]──┬── Vout
             │
            [ R ]
             │
            GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从电阻两端输出。原理是利用电感电流不能突变的特性，抑制输入信号向后级提供的电流。用阻抗分压看：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vout = R / (XL + R) × Vin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;频率越高，&lt;code&gt;XL&lt;/code&gt; 越大，&lt;code&gt;Vout&lt;/code&gt; 越小。&lt;/p&gt;
&lt;p&gt;截止频率：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;f = R / (2πL)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（由感抗公式转换而来）&lt;/p&gt;
&lt;p&gt;例：滤波电阻 100Ω + 滤波电感 100µH → 截止频率 159kHz。&lt;/p&gt;
&lt;p&gt;仿真：100Hz（远低于截止）时输入输出完全重合；1MHz（远高于截止）时输出幅值小很多；159kHz（= 截止频率）时 706.743mV ≈ 0.707 倍。&lt;strong&gt;低通不是高频完全不通过，而是有很大的衰减。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;应用是滤除有用信号上叠加的高频噪声。例：有用信号 1kHz，上面叠着 500kHz 开关电源开关噪声，用低通滤掉。&lt;/p&gt;
&lt;h3&gt;LR 低通 vs RC 低通&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;RC 低通&lt;/th&gt;
&lt;th&gt;LR 低通&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;只能电流很小&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;工程惯例：信号线上的高频噪声用 RC 滤；电源端一般用电感做低通滤波（很少用电阻+电容）。电感分差模、共模两种。&lt;/p&gt;
&lt;p&gt;例外：运放芯片工作电流小，它的电源端有时也用 RC 低通滤电源噪声。&lt;/p&gt;
&lt;p&gt;两者原理相同，区别只在于一个利用「电容电压不能突变」，一个利用「电感电流不能突变」。&lt;/p&gt;
&lt;h2&gt;LR 高通滤波&lt;/h2&gt;
&lt;p&gt;和低通刚好相反：高频通过，低频衰减。原理和低通完全一样，只是输出位置不同——低通取电阻两端，高通取电感两端。&lt;/p&gt;
&lt;p&gt;截止频率公式相同，还是 &lt;code&gt;f = R / (2πL)&lt;/code&gt;。仿真里把电感和电阻调换位置，元件值没变，截止频率依旧是 159kHz，输出依旧等于输入的 0.707 倍：1MHz 时输出与输入基本贴在一起（高通）；1kHz / 10kHz 时衰减很多。&lt;/p&gt;
&lt;p&gt;应用是滤除高频信号上叠加的低频噪声。例：有用信号 500kHz + 50Hz 工频干扰（波形表现为 500kHz 信号上下缓慢起伏），经高通滤波后输出平直。&lt;/p&gt;
&lt;h3&gt;一个必须注意的坑&lt;/h3&gt;
&lt;p&gt;LR 高通以电感作为输出，而电感底部接 GND，所以这个电路不能存在直流偏置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;标准交流信号以 GND 为基准（高 +1V、低 −1V）→ 能正常工作
若交流信号叠加在 1V 直流偏置上（高 2V、低 0V）
  → 直流就是 0Hz，也是低频分量
  → 会被电感短路掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直流偏置是信号固有的一部分，不是噪声，按道理不能滤掉，但用电感高通滤波时会被一起滤掉。仿真里加 1V 偏置后输入变为 0~2V，经 LR 高通滤波后 1V 偏置没了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电感 LR 高通滤波  →  只能滤「没有直流偏置」的信号
有直流偏置        →  改用「电容高通滤波」
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;LC 低通滤波&lt;/h2&gt;
&lt;p&gt;LC 低通的滤波效果比 RC / LR 更好。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LC 低通：Vout = XC / (XC + XL) × Vin
RC 低通：Vout = XC / (XC + R)  × Vin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;RC 里随频率变化的量只有 XC（频率升高 XC 减小），分压变化量小、衰减慢；LC 里有两个——XL 随频率升高而变大，XC 变小，分压变化量大、衰减快。&lt;/p&gt;
&lt;p&gt;专业说法：LC 低通是二阶低通滤波电路，LR / RC 是一阶低通滤波电路。&lt;/p&gt;
&lt;p&gt;为公平起见把截止/谐振频率设成一样：LC 用 L = 10µH、C = 10µF，谐振频率 15.9kHz；RC 用 R = 1kΩ、C = 0.01µF（10nF），截止频率 15.9kHz。&lt;/p&gt;
&lt;p&gt;仿真现象：1kHz（都低于 15.9k）时两者输出基本重叠，但 LC 会多出一个斜坡/谐振；20kHz 时两者都有衰减，LC 衰减到更小幅值，但 LC 回路产生谐振导致输出偏高；50kHz（远离谐振频率）时谐振放大效果变得很小，LC 的滤波效果明显优于 RC。&lt;/p&gt;
&lt;p&gt;加一个 0.1Ω 小电阻可基本消除斜坡。&lt;/p&gt;
&lt;h3&gt;核心坑&lt;/h3&gt;
&lt;p&gt;LC 低通电路也叫 LC 谐振回路，在谐振点（即截止频率处）不但不衰减，反而会放大。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LC 回路里的电阻&lt;/th&gt;
&lt;th&gt;谐振增益&lt;/th&gt;
&lt;th&gt;输出（输入 1Vpp）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0.1Ω&lt;/td&gt;
&lt;td&gt;放大 10 倍&lt;/td&gt;
&lt;td&gt;10Vpp（测得 ±10V）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1Ω&lt;/td&gt;
&lt;td&gt;约 1 倍&lt;/td&gt;
&lt;td&gt;刚好等于输入&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;LC 回路中电阻越小，谐振点的放大越明显。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以用 LC 电路做低通滤波时，一定要在 LC 网络上预留一个电阻，限制谐振点的放大倍数，否则很容易出问题。&lt;/strong&gt; 这是电源滤波设计里最容易翻车的地方——本来想滤掉高频噪声，结果在某个频率上把噪声放大了 10 倍。&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;th&gt;特点&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RC 低通&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;LR 低通&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;LR 高通&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;LC 低通&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;/tbody&gt;
&lt;/table&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;电源端滤波用电感（LR/LC），信号线滤波用 RC——因为电阻耗能。&lt;/li&gt;
&lt;li&gt;LR 高通会滤掉直流偏置，有偏置的信号要用 RC 高通。&lt;/li&gt;
&lt;li&gt;LC 低通在谐振点会放大而不是衰减，必须留一个阻尼电阻。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二极管基础与整流电路</title><link>https://me.622168.xyz/posts/hardware/13-diode-basics-rectifier/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/13-diode-basics-rectifier/</guid><description>导通压降不是恒定值、为什么二极管不能直接并联电压源、半波与全波整流。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p31（21.3 分钟）、p32（7.1 分钟）、p33（8.6 分钟）、p34（9.3 分钟）、p35（6.5 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;二极管是什么&lt;/h2&gt;
&lt;p&gt;材料分三类：绝缘体、导体、半导体。半导体介于两者之间——通了电不一定有电流，只有满足某些条件才有电流。&lt;/p&gt;
&lt;p&gt;掺杂 P 型材料得到 P 型半导体，掺杂 N 型材料得到 N 型半导体，P 型与 N 型结合就是 PN 结。&lt;strong&gt;PN 结就叫二极管&lt;/strong&gt;（最简单的晶体管）。&lt;/p&gt;
&lt;p&gt;外壳只是 PN 结的「衣服」，真正的二极管封装在里面。用火机烧掉外壳、刷掉灰，能看到一个发金属光泽的小东西，那才是真实二极管形状。&lt;/p&gt;
&lt;p&gt;外壳上灰色色环（色条）一端是负极，另一端是正极。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;普通二极管只能从正极流向负极（单向导通）。&lt;/strong&gt; 稳压二极管工作在击穿区，击穿时电流方向相反（后面讲）。&lt;/p&gt;
&lt;h2&gt;导通压降不是恒定值&lt;/h2&gt;
&lt;p&gt;硅管 0.5 ~ 0.7V，锗管约 0.3V（现在较少用），肖特基更低。&lt;/p&gt;
&lt;p&gt;实测（1N4007）：电源接入后电流 5mA，压降显示 0.67V，万用表测得 0.69V。电流从 5 → 6 → 7 → 8 → 9mA 逐渐调高，压降越来越高。&lt;strong&gt;不能因为规格书写 0.6V 就认为恒为 0.6V。&lt;/strong&gt; 原因是二极管不是纯二极管模型，内部有寄生电阻（ESR），电流越大压降越大。&lt;/p&gt;
&lt;p&gt;实测（肖特基）：同样 5mA 电流下压降只有 0.24V，电流增大压降依旧变大。&lt;/p&gt;
&lt;p&gt;反接实验：把电源反过来接，二极管不导通，电源设 5V 时万用表测得 4.99V（相当于断开）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;只要导通，两端就约等于导通压降。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;不能直接用电压源并联二极管&lt;/h2&gt;
&lt;p&gt;二极管两端不能直接接大于其导通压降的电压。实验中能直接并联，是因为把直流稳压电源的输出电流限流到只有 5mA。&lt;/p&gt;
&lt;p&gt;从特性曲线看（横轴 &lt;code&gt;VD&lt;/code&gt;、纵轴 &lt;code&gt;Id&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VD &amp;lt; 0.6V      →  Id = 0
VD 到 0.6V     → 开始有电流
VD 再上升一点点 → Id 变化非常大（非线性）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 0.6V 压降的二极管直接接 1V，电流可能直接到几百毫安。&lt;/p&gt;
&lt;p&gt;从寄生电阻看（简单粗暴）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;设导通压降恒定 0.6V，电源 1V
→ 剩下 0.4V 全加在寄生电阻（二极管 ESR + 走线电阻）上
→ 若总寄生电阻只有 0.1Ω：0.4V / 0.1Ω = 4A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实测（1N4007，电源限流调到最大 5.2A）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0.7V              →  约 2mA
0.8V（只升 0.1V）  →  直接跳到 50mA
再升 0.1V          →  0.3A
再提高             →  1A 多
1V                →  1.4A，实测温度 167℃
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;不能直接用电压源与二极管并联，否则要么二极管烧了，要么电源不正常。&lt;/strong&gt; 二极管尽量工作在 10mA 左右；大功率封装可持续电流很大且不发热。&lt;/p&gt;
&lt;h2&gt;三个电路对比&lt;/h2&gt;
&lt;p&gt;电源均为 5V，二极管压降按 0.7V 算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二极管正向串联（上二极管下电阻）：导通 → 5 − 0.7             → 4.3V（实测 4.35V）
二极管反接：不导通 → I = 0 → VR = I×R = 0                   → 0V（与 R 大小无关）
二极管与电阻调换位置（仍反接）：不导通 → I = 0 → 电阻压降 0  → 5 − 0 = 5V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第三种容易分析不出来，套 &lt;code&gt;VR = I×R&lt;/code&gt; 就行。&lt;/p&gt;
&lt;p&gt;换成肖特基 1N5819（压降 0.3V）：该点电压 &lt;code&gt;= 5 − 0.3 = 4.7V&lt;/code&gt;，实测 4.78V。&lt;/p&gt;
&lt;h2&gt;漏电特性&lt;/h2&gt;
&lt;p&gt;二极管反向截止 ≠ 完全关断，仍会有微弱漏电流。推广到所有半导体材料都一样——只要器件上有耐压，即使处于不工作/不导通状态，依旧有漏电流，只是大小不同。&lt;/p&gt;
&lt;p&gt;肖特基的取舍：普通二极管导通压降 0.7V、反向漏电小、开关速度慢、耐压高；肖特基导通压降 0.3V、反向漏电大、开关速度快、耐压低。&lt;/p&gt;
&lt;p&gt;所以「导通压降只有 0.3V」不代表肖特基在所有场景都好用：对漏电流要求高的场景，肖特基不适用。&lt;/p&gt;
&lt;p&gt;实测：1N5819（肖特基）反接、5V、电阻 2kΩ，&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电阻上压降 = 3mV
漏电流 = 3mV / 2kΩ = 1.5µA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对人来说很小，但对静态工作电路或负载非常精确的场合，这个漏电流不能忽略。&lt;/p&gt;
&lt;p&gt;1N4007（普通）：同样条件下测不到电压（0），继续把电压提高到 40 多伏，依旧没有可测漏电流。1N4007 反向耐压可达 1000V，1N5819 耐压约 50V。&lt;/p&gt;
&lt;p&gt;做项目时某些场景要求漏电流很小，若只看「导通压降低」就选肖特基，可能把能量全漏掉。&lt;/p&gt;
&lt;h2&gt;半波整流&lt;/h2&gt;
&lt;p&gt;利用单向导通特性，把交流电变成直流电。&lt;/p&gt;
&lt;p&gt;电路：交流输入 + 二极管 + 电阻 + 电容（先忽略后级负载）。电阻作用是限制电容充电速度（为实验方便）。&lt;/p&gt;
&lt;p&gt;工作过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 初始电容两端压降 = 0
② 输入信号接进来就给后级充电，因有限流电阻，电容电压慢慢上升
③ 二极管压降的存在：输入到 0.7V 时电容才开始充；
   当电容电压与输入电压相差小于 0.7V 时停止充电
④ 输入继续下降到不满足导通压降条件 → 不再给电容充电
⑤ 后面没有负载 → 电容不充不放 → 电容电压保持不变
⑥ 下一个周期输入重新升到比电容电压高 0.7V 时继续充电，逐周期抬高
⑦ 最终充到「输入最大值 − 0.7V」时停止，之后保持一个纯直流电压
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只有加负载才会往下掉。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;二极管 + 电容就能把交流整流成直流。&lt;/strong&gt; 也叫峰值检波功能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电容峰值电压 ≈ Vin_max − 0.7V（二极管压降）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实物演示波形：第二个周期起台阶式上升（充一下 → 保持 → 再充），最后输出峰值比交流峰值小一点点，之后是一条直线。&lt;/p&gt;
&lt;h2&gt;全波整流&lt;/h2&gt;
&lt;p&gt;在半波基础上多加了几个二极管，构成电桥（4 个二极管）。分析方法是先忽略电容，把电容当电阻看，分析负载电阻两端电压与输入波形。&lt;/p&gt;
&lt;p&gt;正半周（上正下负）：电流从上端出发，经过其中两个二极管，流过负载，回到电源负极。回到电源处有两个分支——负载过来的那一点电压 V1 已经比另一点 V2 小（后级有压降），所以另一支路的二极管不可能导通，电流只能走回负极。输出波形跟随输入上升/下降，但比输入小两个二极管的压降，即从 1.4V 处才开始上升。&lt;/p&gt;
&lt;p&gt;负半周（下正上负）：电流方向反过来，走另两个二极管，同样从负载上端下来，负载上依旧是正电压，最大值同样比输入小 1.4V。&lt;/p&gt;
&lt;p&gt;经过电桥后，负半周被「折过去」，正半波和负半波都用到了，所以叫全波整流。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出永远比输入小 1.4V（两个二极管压降）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;加电容后，电容储能，波形变成「冲到最高 → 慢慢放电 → 又冲上去 → 又慢慢放电」的锯齿状。&lt;/p&gt;
&lt;h2&gt;半波 vs 全波&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;二极管数量&lt;/td&gt;
&lt;td&gt;1 个&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;0.7V&lt;/td&gt;
&lt;td&gt;1.4V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出频率&lt;/td&gt;
&lt;td&gt;与输入相同&lt;/td&gt;
&lt;td&gt;输入的 2 倍&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;效率&lt;/td&gt;
&lt;td&gt;低&lt;/td&gt;
&lt;td&gt;高&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;纹波&lt;/td&gt;
&lt;td&gt;大&lt;/td&gt;
&lt;td&gt;小&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;全波效率高，代价是多两个二极管和 1.4V 的压降。低压整流（比如 5V 系统）用桥式会损失 1.4V，这时候更常用肖特基桥（每个 0.3V，共 0.6V）或者同步整流。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;导通压降不是恒定值，电流越大压降越高，因为有 ESR。&lt;/li&gt;
&lt;li&gt;不能把二极管直接并到电压源上，会瞬间烧毁（1V 时就能到 1.4A、167℃）。&lt;/li&gt;
&lt;li&gt;反向截止有漏电流，肖特基的漏电明显比普通二极管大，别只看导通压降选型，耐压也要看。&lt;/li&gt;
&lt;li&gt;全波整流损失 1.4V，低压系统要注意。&lt;/li&gt;
&lt;li&gt;整流后的直流靠电容维持，没负载时电容不放电，加了负载才有纹波。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二极管的钳位、方向控制与耐压</title><link>https://me.622168.xyz/posts/hardware/14-diode-clamp-direction-voltage/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/14-diode-clamp-direction-voltage/</guid><description>钳位电路怎么保护芯片、反接保护为什么不能接在 GND 端、以及耐压标称的余量到底有多少。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p36（10.3 分钟）、p37（8.6 分钟）、p38（8.4 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;钳位功能&lt;/h2&gt;
&lt;p&gt;二极管导通后两端压降约等于其额定导通电压（硅管 0.7V）。电流越大压降会稍大一点点，但基本在 0.7V 左右。&lt;/p&gt;
&lt;p&gt;演示：直流 5V 电源 + 两个 10kΩ 电阻分压。不加二极管时分压点是 2.5V；在其中一个电阻上并联一个二极管后，分压点变成 0.55V（约等于该二极管的导通压降）。&lt;/p&gt;
&lt;p&gt;原理：二极管导通后，该电阻与二极管并联，该电阻上的电压就被钳位在二极管的导通压降上。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;钳位的前提是外部电阻分压值大于二极管的导通压降。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;分压 = 1V  &amp;gt; 0.7V  →  二极管导通，把 1V 拉到 0.7V   ✓
分压 = 0.3V &amp;lt; 0.7V →  二极管不导通，没有钳位作用    ✗
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用：电路防护&lt;/h3&gt;
&lt;p&gt;实例 1，反激开关电源里的运放输入保护：误差放大器的输出信号反馈线上接一个二极管到 3.3V。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;正常时该点电压约 10V，经两个电阻分压后应等于运放的基准 1.2V
若电路异常（焊接焊反、电容焊反、器件出错）
  → 10V 会直接跑到运放输入端
  → 运放供电只有 3.3V  →  很容易使运放工作异常甚至打坏运放

加二极管后：
  该点电压升到约 4V 时二极管导通（3.3V + 0.7V）
  → 该点电压永远只能比 3.3V 高 0.7V  →  保护运放输入端口
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例 2：同一信号线接二极管到 GND，可保证该点电压不低于 −0.7V，同样起保护作用。&lt;/p&gt;
&lt;p&gt;识别规则：&lt;strong&gt;只要看到一根信号线上挂着两个二极管——一个接 3.3V、一个接 GND——作用就是防护。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;实例 3：信号线上加稳压管也是防护，但利用的是稳压管的反向击穿功能，而非二极管正向导通。&lt;/p&gt;
&lt;h2&gt;控制电流方向 / 反接保护&lt;/h2&gt;
&lt;p&gt;直流有刷电机从上到下加电流是正转，从下到上则反转。有些场景只允许正转、不允许反转，有两种做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;机械防呆设计：把接口做成特殊形状（正极长、负极短），反接插不进去 —— 缺点是比较贵
用二极管：回路中串一个二极管，接反时不导通、电机不转 —— 缺点是有 0.7V 压降
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这叫反接保护。&lt;/p&gt;
&lt;p&gt;实物演示：黑色线接负、褐色线接正时电机旋转，反接也转但方向相反；在褐色线（正端）串一个二极管后，正常接法电机正常转，电源反接电机就不转了。&lt;/p&gt;
&lt;p&gt;常见的两种方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 机械结构防呆接口（反着插不进去）
② 电源端串一个二极管：一端接 VCC，另一端接 GND，接反则二极管不导通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意：二极管不一定非要放电源端，放 GND 端也能防反接。但一般不建议放在 GND——会让电路的接地点不等于 0（实际等于 0.7V）；而且二极管导通压降随工作电流变化（电流越大压降越大），会对 GND 产生波动，影响电路工作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用二极管做反接保护时，建议接到电源电压（VCC）端，不要接到 GND。&lt;/strong&gt; 因为 GND 是全电路的参考点，非常关键；VCC 处理起来相对好处理一点点。&lt;/p&gt;
&lt;h2&gt;最大反向耐压&lt;/h2&gt;
&lt;p&gt;二极管反向耐压不是无穷大，有范围。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：最大反向耐压标 30V → 反向电压不能超过 30V
若一端加 45V、另一端接 0V，二极管承受 45V 反向电压 → 很可能击穿
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;正向没有「最大正向电压」——正向导通后就是其额定压降值。正向需要注意的反而是电流不能超过最大电流，否则发烫、发热损坏。&lt;/p&gt;
&lt;p&gt;厂家的耐压标称有余量：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;标 30V  →  实际可能加到 35V 才开始有微微电流（开始击穿，但尚未损坏）
          此时把电压降下来，不会坏
继续升到 45V  →  可能完全击穿，不可恢复
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实验用 1N5819（最大反向耐压约 45V）：一个二极管 + 10Ω 电阻，反接，万用表调到 200V 档。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;20 多伏        →  电流一直为 0，二极管两端承受 22.3V
23 ~ 27V       →  仍然没有电流
44V            →  开始出现漏电流
46 ~ 50V       →  电流又往上上升一点点
把电压拧下来    →  二极管依旧正常（说明刚刚已经过压但没坏）
突然拧到约 60V  →  瞬间击穿；因电阻只有 10Ω，60V 加进去的瞬间电阻被烧断冒光
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;拆下用万用表蜂鸣档测量，蜂鸣器响，说明二极管已损坏（短路）。&lt;/p&gt;
&lt;p&gt;那个细节值得记一下：二极管击穿后变成短路，于是 60V 几乎全加在 10Ω 电阻上，&lt;code&gt;60²/10 = 360W&lt;/code&gt;，电阻瞬间烧断。这就是「一个器件坏了会连锁烧掉别的器件」的典型例子。&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;整流&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;串联在电源输入端（不要串在 GND）&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;并联在信号线与 VCC/GND 之间&lt;/td&gt;
&lt;td&gt;限制电压范围，保护芯片&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;识别口诀：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;信号线上挂两个二极管（一个到 VCC、一个到 GND）  →  防护钳位
感性负载两端反并联一个二极管                    →  续流
电源入口串一个二极管                            →  防反接
四个二极管组成桥                                →  整流
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;钳位的前提是分压值大于导通压降，小于就不起作用。&lt;/li&gt;
&lt;li&gt;反接保护的二极管不要串在 GND 端，会把地电位抬高 0.7V 并随电流波动。&lt;/li&gt;
&lt;li&gt;反向耐压标称有余量，但余量不是让你用的，设计要按标称值留 1.5~2 倍余量。&lt;/li&gt;
&lt;li&gt;反向电压超限会击穿，击穿后变成短路，会连带烧毁限流电阻。&lt;/li&gt;
&lt;li&gt;二极管损坏后用蜂鸣档能测出来（短路会响），排查时常用。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>稳压二极管</title><link>https://me.622168.xyz/posts/hardware/15-zener-diode/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/15-zener-diode/</guid><description>唯一一种「故意让它击穿」的二极管——稳压管怎么用、工作电流取多少、为什么必须反接。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p39（9.9 分钟）&lt;/em&gt;&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;正向&lt;/td&gt;
&lt;td&gt;导通，0.7V&lt;/td&gt;
&lt;td&gt;导通，约 0.8V&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;正向&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;整流、防反接、续流、钳位&lt;/td&gt;
&lt;td&gt;稳压、钳位&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;稳压管是唯一一种「故意让它工作在击穿状态」的器件。&lt;/p&gt;
&lt;h2&gt;基本行为&lt;/h2&gt;
&lt;p&gt;击穿之后，两端电压等于一个额定值，也就是稳压值。拿 5.6V 的稳压管试：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电源 3V（小于 5.6V） → 未击穿，断开状态，该点 3V
电源 7V（超过 5.6V） → 击穿，但不烧，该点 5.6V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意 5.6V 并不是非常稳定：电流越大，电压会慢慢升高，但依旧约等于 5.6V。我一开始把这个值当成死的，实测才知道它会跟着电流往上跑。&lt;/p&gt;
&lt;h2&gt;必须串联限流电阻&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;      ┌─── R ───┐
      │         │
     V+        ─┴─  稳压管（反接！）
      │         │
      └─────────┴──── GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;没有限流电阻，稳压管会烧。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;工作电流怎么取&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;功耗 P = I × Vz  →  会发热
热量超过额定功率 →  稳压管损坏
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电流不能太大，超过额定功率会烧；也尽量别太小，太小不稳定。&lt;/p&gt;
&lt;p&gt;建议普通稳压管工作电流约 5mA（不同管子不一样，大点小点问题不大）。输入电压很高又想稳定工作，就加大限流电阻，把电流降下来。&lt;/p&gt;
&lt;h2&gt;实物演示&lt;/h2&gt;
&lt;p&gt;电阻 + 5.6V 稳压管。&lt;/p&gt;
&lt;h3&gt;极性识别&lt;/h3&gt;
&lt;p&gt;黑色边对应 N 极，无黑色边是 P 极。&lt;/p&gt;
&lt;p&gt;稳压管工作在反向击穿状态，所以必须反接（一端接电源，一端接 GND）。&lt;/p&gt;
&lt;h3&gt;实测过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5V（小于稳压值）         → 两端 5V，断开状态
5.2→5.3→5.4→5.5→5.6V    → 跟着上升，还没击穿
5.6V                    → 停止上升；输入 5.7V 时两端仍保持 5.6V，已击穿且正常工作
6.4 / 6.5V              → 5.64V，电流约 6mA，稳压中
8.8V                    → 5.7V，稳压值开始偏移，且会发热
继续调高                 → 稳压值更高，超过额定功率可能损坏
&lt;/code&gt;&lt;/pre&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;压降约 0.8V，比普通硅管的 0.7V 略高&lt;/li&gt;
&lt;li&gt;减小电压仍约 0.8V&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;h3&gt;简易稳压&lt;/h3&gt;
&lt;p&gt;给电路提供一个粗略稳定的电压。只适合小电流场合，大电流要用三端稳压器（如 7805、AMS1117）。&lt;/p&gt;
&lt;h3&gt;钳位保护&lt;/h3&gt;
&lt;p&gt;和普通二极管钳位类似，但稳压管钳位的是「反向击穿电压」。好处是可以钳到比 VCC 高得多的电压，比如用 12V 稳压管保护 12V 回路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;二极管钳位：VCC + 0.7V，受电源限制，精度差，用来保护芯片 IO
稳压管钳位：任意稳压值，精度好，限制在某个特定电压
&lt;/code&gt;&lt;/pre&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;工作电流取 5mA 左右，太小不稳定，太大烧管子&lt;/li&gt;
&lt;li&gt;稳压值不是固定的，输入越高、电流越大越往上移&lt;/li&gt;
&lt;li&gt;只适合小电流，大电流上三端稳压器。别忘额定功率：5.6V 配 5mA 是 28mW，1/4W 够用；到 50mA 就是 280mW，超了&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>三极管的基本结构与开关功能</title><link>https://me.622168.xyz/posts/hardware/16-transistor-structure-switch/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/16-transistor-structure-switch/</guid><description>三极管本质是一个电子开关，以及为什么不能拿两个二极管拼出一个三极管。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p40（7 分钟）、p41（7.1 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;一句话&lt;/h2&gt;
&lt;p&gt;三极管简单说就是一个电子开关。&lt;/p&gt;
&lt;p&gt;零基础阶段只把它当开关用。放大功能确实存在，但比较复杂，这里不展开。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;先回顾：P 型半导体 + N 型半导体 = PN 结 = 二极管。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N 型三极管：上 N、中 P、下 N → NPN，看成两个二极管背靠背
P 型三极管：上 P、中 N、下 P → PNP，看成两个二极管面对面
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么两个二极管拼不出来&lt;/h3&gt;
&lt;p&gt;外部拿两个二极管背靠背或面对面接起来，没有三极管功能，必须是&lt;strong&gt;在芯片内部形成&lt;/strong&gt;这种结构才算。我一开始也觉得接起来就能用，其实不行。&lt;/p&gt;
&lt;p&gt;原因是三极管的基极区域做得极薄，两个独立二极管中间的 P 区或 N 区太厚，载流子过不去。能放大靠的是「薄」这个工艺，不是靠「结构像」。&lt;/p&gt;
&lt;h2&gt;三个电极&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;基极   B   控制端
集电极 C   输出端
发射极 E   公共端
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;P 型、N 型三极管都有这三个电极，区别只是排列组合不同。&lt;/p&gt;
&lt;h3&gt;实物怎么认&lt;/h3&gt;
&lt;p&gt;外壳叫封装，可以理解成「芯片的衣服」。&lt;/p&gt;
&lt;p&gt;同一个封装里可能是三极管，也可能是 MOS 管或其他芯片结构，不能看外形判断，要看芯片手册。&lt;/p&gt;
&lt;h3&gt;万用表验证&lt;/h3&gt;
&lt;p&gt;我拿一个 NPN 三极管量过：万用表打二极管档，红笔接中间极、黑笔接另一极，量到 0.6V 压降；换另一侧同样有二极管。&lt;/p&gt;
&lt;p&gt;也就是说，中间极与旁边两极之间各有一个二极管（两个 PN 结）。这是三极管内部结构最直接的证据。&lt;/p&gt;
&lt;h2&gt;开关功能&lt;/h2&gt;
&lt;h3&gt;为什么需要它&lt;/h3&gt;
&lt;p&gt;一个 LED，供电 5V，想控制亮灭。人用机械开关按下去 LED 亮、松开灭。&lt;/p&gt;
&lt;p&gt;但电路要自动化就必然有主控芯片 MCU，而 MCU 没法「伸手按机械开关」，所以必须加一个电子开关。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;用电信号控制的开关，就是三极管。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;对应关系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;机械开关的 A、B 两点   →  三极管的 C、E
按下（短路）           →  导通
松开（断开）           →  截止
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;导通条件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;B、E 之间流过 IB 电流  →  C、E 导通（闭合），流过 IC 电流
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;典型电路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5V ──[ LED ]──[ 限流电阻 ]── C
                             │  NPN
        MCU ──[ 基极电阻 RB ]── B
                             E
                             │
                            GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;工作过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MCU 输出高电平（5V 或 3.3V）
  → 经基极电阻 → 流过 BE  →  产生 IB
  → CE 闭合  →  流过 IC  →  驱动 LED
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实物演示&lt;/h3&gt;
&lt;p&gt;上电后控制线悬空不接，灯不亮；把基极电阻另一端接到 5V，灯立刻亮。&lt;/p&gt;
&lt;p&gt;实际电路中那根控制线要接到主控芯片。&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;控制方式&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;无穷大&lt;/td&gt;
&lt;td&gt;有漏电&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;闭合时的压降&lt;/td&gt;
&lt;td&gt;0（理想）&lt;/td&gt;
&lt;td&gt;0.3V 左右（饱和压降）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;「多了一个放大状态」是后面所有坑的根源&lt;/strong&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;中间极和两侧各有 0.6~0.7V 压降，这是判断三极管、辨认引脚的基本方法&lt;/li&gt;
&lt;li&gt;基极必须串电阻，直接加高电平会烧掉 BE 结&lt;/li&gt;
&lt;li&gt;做开关时必须让它进饱和区，卡在放大状态会烧&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>三极管开关电路设计与计算</title><link>https://me.622168.xyz/posts/hardware/17-transistor-switch-design/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/17-transistor-switch-design/</guid><description>基极电阻为什么不能随便取 1K/10K，以及两个电阻对比实验的温度数据。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p42（17.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;基极电阻 RB 的作用&lt;/h2&gt;
&lt;p&gt;限流，限制 IB。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;RB 越大  →  IB 越小
RB 越小  →  IB 越大
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个电阻很关键，取值算错会直接烧三极管。&lt;/p&gt;
&lt;h2&gt;网上那套「经验法则」是错的&lt;/h2&gt;
&lt;p&gt;网上「大神」说 RB 随便取 1K 或 10K。这是错的。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;负载电流小于 100mA      → 一般不出大问题
几百 mA、1~2A 甚至更高  → 绝对不能这么取：负载不能正常工作，三极管很烫甚至烧掉
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么那套经验能用：他可能一直做小功率电路，1K/10K 确实没事。&lt;/p&gt;
&lt;p&gt;换成电机就不一样了。实验对象是电机（5V 时正常工作电流约 500mA），不用 LED——LED 只有几毫安，看不出效果。&lt;/p&gt;
&lt;p&gt;预先备两个电阻：510Ω 和 10K。&lt;/p&gt;
&lt;p&gt;所以 RB 一定按负载算，不要用经验法则。&lt;/p&gt;
&lt;h2&gt;对照实验&lt;/h2&gt;
&lt;p&gt;接线：三极管发射极 → 电源负端；集电极 → 电机一端；电机另一端 → 5V；基极经电阻接 5V 触发。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;实验&lt;/th&gt;
&lt;th&gt;RB&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;10K&lt;/td&gt;
&lt;td&gt;电机压根不转，但电流有 100 多 mA；热像仪测三极管温度飙升到 116℃&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;实验 2&lt;/td&gt;
&lt;td&gt;510Ω&lt;/td&gt;
&lt;td&gt;电机转起来，电流 500 多 mA，三极管温度只有 40 多度&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;同一个电路，只换了一个电阻，一个 116℃ 一个 40℃。我原以为 10K 只是让电机转得慢一点，没想到是压根不转还发烫。&lt;/p&gt;
&lt;h3&gt;实验 1 为什么会这样&lt;/h3&gt;
&lt;p&gt;三极管进入了放大状态（第三个状态）。&lt;/p&gt;
&lt;p&gt;理想开关只有开/关，但三极管存在「开了但没完全开、关了又没完全关」的放大状态。&lt;/p&gt;
&lt;p&gt;放大状态下三极管相当于一个大电阻，功耗全落在它自己身上：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;P = VCE × IC
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;VCE 很大（可能几伏），IC 还有 100mA → 发热严重，而电机拿不到足够电压，所以不转。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;放大状态一定不能带大电流负载。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;怎么判断三极管在哪个状态&lt;/h2&gt;
&lt;h3&gt;看 VCE&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;VCE = VCC           → 截止（断开）
VCE ≤ 0.3V          → 饱和导通（完全开通）
0.3V &amp;lt; VCE &amp;lt; VCC    → 放大
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;做开关用时，VCE 必须接近 0.3V。&lt;/p&gt;
&lt;h3&gt;看电流&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;IB = 0          → 截止
IB × β = IC     → 临界
IB × β &amp;gt; IC     → 饱和导通  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;做开关时必须保证 &lt;code&gt;IB × β &amp;gt; IC&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;RB 的计算步骤&lt;/h2&gt;
&lt;p&gt;以 500mA 电机为例。&lt;/p&gt;
&lt;h3&gt;第一步：求 IC 最大值&lt;/h3&gt;
&lt;p&gt;IC 最大值 = 负载工作时的最大电流，查负载的数据手册 / 产品信息。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电机 5V 时最大 500mA
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第二步：求 IB 最小值&lt;/h3&gt;
&lt;p&gt;饱和条件 &lt;code&gt;IB × β &amp;gt; IC&lt;/code&gt;，所以 &lt;code&gt;IB &amp;gt; IC / β&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;β 区间很大，通常 50 ~ 300（甚至 350 或更高），难以确定。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;计算时默认 β = 100
IB &amp;gt; 500mA / 100 = 5mA
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么取 100 而不是按实际 β 算：β 离散性极大，同一型号可能差几倍，还会随温度变化。取一个偏小的值（100），算出来的 IB 偏大，能保证一定饱和。&lt;/p&gt;
&lt;h3&gt;第三步：算 RB&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;RB = (VCC − VBE) / IB
   = (5V − 0.7V) / 5mA
   = 4.3V / 5mA
   = 0.86K
   = 860Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;分子是 &lt;code&gt;VCC − VBE&lt;/code&gt;，不是 &lt;code&gt;VCC&lt;/code&gt;，因为 BE 结本身要吃掉 0.7V。&lt;/p&gt;
&lt;h3&gt;第四步：确定实际取值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;860Ω 是 RB 的最大值
实际取值必须小于 860Ω
取太大就会进入放大状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按标准系列，510Ω 是合适的选择（实验 2 用的就是它，温度只有 40 多度）。&lt;/p&gt;
&lt;h2&gt;计算流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;① 查负载最大工作电流 → IC
② IB = IC / 100
③ RB = (VCC − 0.7V) / IB
④ 选一个比算出来的值小的标准电阻
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;不要抄网上的 1K/10K，大电流负载下会烧三极管&lt;/li&gt;
&lt;li&gt;β 按 100 算，不要按规格书典型值算，否则 IB 可能不够&lt;/li&gt;
&lt;li&gt;分子是 &lt;code&gt;VCC − 0.7V&lt;/code&gt;，不是 &lt;code&gt;VCC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;算出来的是最大值，实际要取更小的标准电阻&lt;/li&gt;
&lt;li&gt;做开关必须让 VCE ≤ 0.3V，进放大状态就是发热 + 负载拿不到电压（电机不转），两件事同一个原因；算完最好用热成像或手摸确认温度，异常就是没进饱和区&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>N 管与 P 管的选择</title><link>https://me.622168.xyz/posts/hardware/18-npn-pnp-selection/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/18-npn-pnp-selection/</guid><description>什么时候用 N 管、什么时候用 P 管——这一集的结论必须记准，后面 MOS 管完全沿用。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p43（17.8 分钟）、p44（2.6 分钟）、p45（3.3 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;问题的由来&lt;/h2&gt;
&lt;p&gt;N 型三极管放在负载底部，控制的是负载的接地线（负极）。&lt;/p&gt;
&lt;p&gt;但很多产品需要控制负载的电源线（高边）。&lt;/p&gt;
&lt;p&gt;那能不能把 N 管放到上面用？我一开始觉得应该可以，实测不行。&lt;/p&gt;
&lt;h2&gt;把 N 管放到高边会怎样&lt;/h2&gt;
&lt;h3&gt;原理分析&lt;/h3&gt;
&lt;p&gt;N 管导通的前提是 VBE = 二极管压降 ≈ 0.7V。&lt;/p&gt;
&lt;p&gt;以发射极作输出时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出点最高只能到 VCC − 0.7 = 4.3V（5V 供电时）
CE 之间至少存在 0.7V 压差
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;流过较大电流时，发热非常严重。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;P = 0.7V × 0.5A = 0.35W   ← 全部消耗在三极管上
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实测&lt;/h3&gt;
&lt;p&gt;N 管发射极输出，基极电阻取 1K（故意取大以便观察效果）：&lt;/p&gt;
&lt;p&gt;电机能转，但三极管温度飙到 116℃ → 120℃。&lt;/p&gt;
&lt;h2&gt;换成 P 管&lt;/h2&gt;
&lt;p&gt;P 管电流方向与 N 管相反：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IB 从发射极流向基极
输出从集电极取
→ BE 与负载之间没有直接联系
→ VBE 压降不影响负载
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以完全避免上述问题。&lt;/p&gt;
&lt;h3&gt;P 管实测&lt;/h3&gt;
&lt;p&gt;PNP 管，发射极接 5V，集电极接电机，电机另一端接 GND，基极经电阻接地触发：&lt;/p&gt;
&lt;p&gt;电机转，三极管温度只有 50 多度（因电阻取了 1K，偏大；取 510Ω 可压到 40 多度）。&lt;/p&gt;
&lt;h2&gt;核心结论&lt;/h2&gt;
&lt;p&gt;P 管用来控制负载的电源端，充当「电源开关」（高边开关）。&lt;/p&gt;
&lt;p&gt;N 管一般接到负载的负端 / 底部，控制负载接地，是「负极开关」（低边开关）。&lt;/p&gt;
&lt;p&gt;这条结论在后面 MOS 管部分会完全沿用，是最重要的选型规则之一。&lt;/p&gt;
&lt;h3&gt;例外情况&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;功率较小的场景，N 管也可以直接以发射极输出（0.7V 压差产生的功耗可以接受）&lt;/li&gt;
&lt;li&gt;用自举电路等特殊结构也能避免发热，但成本会变得非常高&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;导通逻辑&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;              N 管              P 管
导通条件      B 比 E 高 0.7V    E 比 B 高 0.7V
输入          高电平导通        低电平导通
关断          低电平            高电平
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;p&gt;N 管：BE 之间就是一个二极管，要流过 IB 必须 B 点比 E 点高 0.7V，前提是输入端来高电平。&lt;/p&gt;
&lt;p&gt;P 管：结构是 PNP，发射极是 P、基极是 N，BE 之间的二极管方向从发射极指向基极；要流过 IB 必须发射极比基极高 0.7V，前提是基极变成低电平（如接 0V），发射极 5V，才有电流流下来。&lt;/p&gt;
&lt;p&gt;若输入信号从低变高（基极也变 5V），发射极 5V 与基极 5V 无压差，不会有 IB 电流，P 管不导通。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;N 型三极管和 P 型三极管的工作逻辑刚好相反。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;P 管的 RB 计算&lt;/h2&gt;
&lt;p&gt;和 N 管完全一样。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① IC 最大值 500mA（电机）
② 饱和条件 IB × β &amp;gt; IC，β 取 100 → IB &amp;gt; 5mA
③ RB = (5V − 0.7V) / 5mA = 860Ω（最大值）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;区别只在于导通逻辑反了。&lt;/p&gt;
&lt;h2&gt;两种开关的电路对照&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;N 管（低边开关）                 P 管（高边开关）
                                
   VCC                              VCC
    │                                │
  [负载]                          ┌──┴──┐
    │                             │  E  │
    ├─── C                        │ PNP │
    │                             │  C  │
    │ B ──[ RB ]── MCU            └──┬──┘
    │ E                              │
   GND                             [负载]
                                     │
                                    GND
                                  B ──[ RB ]── MCU
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;N 管低边&lt;/th&gt;
&lt;th&gt;P 管高边&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;高电平&lt;/td&gt;
&lt;td&gt;低电平&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;抗干扰电阻&lt;/td&gt;
&lt;td&gt;下拉（B-E 之间）&lt;/td&gt;
&lt;td&gt;上拉（B-E 之间）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;压降影响&lt;/td&gt;
&lt;td&gt;有 0.3V 饱和压降&lt;/td&gt;
&lt;td&gt;有 0.3V 饱和压降&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;选型决策&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;负载可以一端接地吗？
    │
    ├─ 可以（大多数情况）  →  用 N 管（低边开关）
    │                        便宜、性能好、驱动简单
    │
    └─ 不可以（负载必须接电源端）→  用 P 管（高边开关）
                                   导通逻辑相反，注意别搞错
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;N 管放高边会严重发热（0.7V × 负载电流全在三极管上），实测能到 120℃&lt;/li&gt;
&lt;li&gt;P 管控制电源端，N 管控制接地端，这条在 MOS 管部分完全沿用&lt;/li&gt;
&lt;li&gt;两者导通逻辑相反：N 管高电平通，P 管低电平通&lt;/li&gt;
&lt;li&gt;P 管加&lt;strong&gt;上拉&lt;/strong&gt;电阻，N 管加&lt;strong&gt;下拉&lt;/strong&gt;电阻，都接在 B-E 之间&lt;/li&gt;
&lt;li&gt;P 管的 RB 算法和 N 管一样，别以为换个管子公式就变了；另外 P 管可选型号比 N 管少、价格更贵、性能也略差，能用 N 管就用 N 管&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>三极管的反相、下拉电阻与耐压</title><link>https://me.622168.xyz/posts/hardware/19-transistor-invert-pulldown-voltage/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/19-transistor-invert-pulldown-voltage/</guid><description>集电极输出为什么是反相的、基极下拉电阻为什么「非常重要」、以及三个耐压里该关注哪个。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p46（10 分钟）、p47（9.6 分钟）、p48（9.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;输出反相&lt;/h2&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;以集电极作输出时，输出与输入逻辑反向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输入 0（低电平）→ 三极管断开 → 输出高（1）
输入 1（高电平）→ 三极管闭合 → 输出低（0）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;断开时：I = 0  →  电阻上压降 = 0  →  该点电压 = 5V  →  高电平
导通时：相当于把该点短路到 GND（不是真短路，最小压降 0.3V 或更小）→ 低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「不是真短路」这点要注意：导通时集电极对地有 0.3V 左右的饱和压降，所以输出低电平其实是 0.3V 而不是 0V。大多数数字电路能接受，精密场合要注意。&lt;/p&gt;
&lt;h3&gt;应用实例&lt;/h3&gt;
&lt;p&gt;客户的产品能输出高低电平控制信号，要求「低电平工作、高电平不工作」。&lt;/p&gt;
&lt;p&gt;方案一，用 P 管，最直接：低电平时有电流，P 管导通，工作；高电平时无电流，不工作。逻辑刚好正确。&lt;/p&gt;
&lt;p&gt;方案二，手上只有 N 管时（比如 P 管用完了）：加一个 N 管先做一次反向，再去驱动后级的负载开关。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;低电平：第一级不导通 → 后级有电流 → 负载工作 ✓
高电平：第一级导通 → 该点被拉到约 0~0.3V → 后级不满足导通条件 → 负载不工作 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;利用反相特性做电平转换，是三极管电路的常见套路。&lt;/p&gt;
&lt;p&gt;方案三，单管省成本，但有风险。N 管发射极接信号端、基极接 VCC、集电极接负载。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;低电平时 VCC 有电流过来 → 工作
高电平时高电平 = VCC → 无压差 → 断开 → 不工作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;风险 1：要求输出高电平电压必须等于 VCC（VCC = 5V 时该点高电平也要 5V）；或把输出端口设为开漏（开路）输出模式，此时不必输出高电平。&lt;/p&gt;
&lt;p&gt;风险 2：即使设成开漏输出，输出低电平时 IB 电流和负载电流都流进芯片内部，负载电流大就可能烧芯片。&lt;/p&gt;
&lt;p&gt;限制：只能用在小电流场景，毫安级别；芯片吸收电流一般几毫安，最大不超过十几到二十几毫安。&lt;/p&gt;
&lt;h2&gt;基极下拉电阻&lt;/h2&gt;
&lt;p&gt;前面几个例子都漏了基极下拉电阻，电路特别容易受干扰。我一开始也没在意它，直到手一碰灯就亮。&lt;/p&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;LED 控制电路上电正常，然后：手一碰基极，灯就亮；手放开就灭；按住时灯一直闪。&lt;/p&gt;
&lt;h3&gt;原因：工频干扰&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;人体在 220V 电器周围会被感应出电荷（感应静电）
这些感应电荷带有 220V 交流电信息，频率为 50Hz
手按下时人体不断感应出 50Hz 电荷，经人体形成 IB 回路
→ 干扰灯的工作
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是常说的「工频干扰」。&lt;/p&gt;
&lt;p&gt;类似的还有：机器机壳或电路处于浮空悬空状态，人一摸，机器数据就不停跳。&lt;/p&gt;
&lt;h3&gt;解决方法&lt;/h3&gt;
&lt;p&gt;在 B、E 之间加一个电阻，也就是下拉电阻。&lt;/p&gt;
&lt;p&gt;加电阻后手摸灯不亮了，电路正常工作（点一下亮、放开灭）。&lt;/p&gt;
&lt;h3&gt;为什么有效&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;人体感应出的静电荷能量非常微弱
下拉电阻为 VBE 分担小功率噪声，为静电 / 外部干扰提供释放回路
噪声能量太小，在电阻上建立不起 0.7V 电压
→ 三极管不会导通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键在「建立不起 0.7V」：下拉电阻和 BE 结构成分压，噪声能量太弱，分到 BE 结上的电压达不到 0.7V 的导通门槛。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;只要用三极管做开关，N 管底部一定要加下拉电阻；P 管不是加下拉，而是加上拉电阻。&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N 管：下拉，接在 B 与 E 之间（B 到 GND）
P 管：上拉，接在 B 与 E 之间（B 到 VCC）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;P 管演示：不加电阻时手摸灯会亮会闪；加上上拉电阻后手摸不亮。原理同上——静电荷从电阻走，不经过 VBE。&lt;/p&gt;
&lt;h3&gt;取值&lt;/h3&gt;
&lt;p&gt;通常 10KΩ 左右。太小会增加驱动负担，太大泄放太慢。&lt;/p&gt;
&lt;h2&gt;最大耐压&lt;/h2&gt;
&lt;h3&gt;断开状态也不是完全没电流&lt;/h3&gt;
&lt;p&gt;很多人以为断开状态就没有电流。实际上当 VCC 大到一定程度，三极管承受不住，会出现漏电流。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;出现小电流时及时撤掉电压，三极管一般还能恢复，是安全的&lt;/li&gt;
&lt;li&gt;长时间维持高压不撤，很可能被击穿&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;实测（SS8050）&lt;/h3&gt;
&lt;p&gt;VCE 耐压一般 30 多伏（35V 或 45V，不同厂家略有区别）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;15V      → 电流一点都没有
20 多伏  → 电流微微抖动，说明已比较接近
37~40V   → 电流出现频次变高
60 多伏  → 也不至于炸，只是会有一些漏电流
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三个耐压的大小顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① VBE 反向耐压  最小   ← 最脆弱
② VCE 反向耐压  其次
③ VCB 反向耐压  最高（可能达到 50V）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;VBE 反向耐压实测&lt;/h3&gt;
&lt;p&gt;正极接发射极、负极接基极，电源从 0 慢慢往上加：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;加到约 10V 就出现电流 → 已击穿（还没烧）
温度 48℃ → 100 多度
电压再高会烧起来
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关机前要先把电源调到最低，否则一上电就炸。&lt;/p&gt;
&lt;p&gt;半导体的失效方式：不会炸，但会烧起来冒很大的烟，非常臭。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;三极管的 VBE 绝对不能承受反向电压，非常容易烧。&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;实际设计该关注哪个&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;VBE  一般不关注   正常接法、电路中没有多个电源时不会承受反向电压；除非电路里有多个电源
VCE  最需要关注   三极管关断时 VCE 就等于 VCC，所以要求 VCE 耐压 &amp;gt; VCC，
                  一般要求大于 VCC 的 1.5 倍以上甚至 2 倍
VCB  一般不关注   VCB 耐压比 VCE 高；真达到 VCB 时 VCE 早就击穿了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;补充一点：达到耐压值不一定就坏（如加到 60V 也没事），只是漏电率变大、工作不稳定。&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;基极电阻 RB&lt;/td&gt;
&lt;td&gt;MCU 与 B 之间&lt;/td&gt;
&lt;td&gt;限流，按 &lt;code&gt;RB = (VCC−0.7)/IB&lt;/code&gt; 算&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;下拉/上拉电阻&lt;/td&gt;
&lt;td&gt;B 与 E 之间&lt;/td&gt;
&lt;td&gt;抗工频干扰，N 管下拉、P 管上拉&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;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;集电极输出是反相的，需要同相逻辑就用两级或者换 P 管&lt;/li&gt;
&lt;li&gt;导通时输出低电平是 0.3V 不是 0V，精密场合要注意&lt;/li&gt;
&lt;li&gt;单管省成本方案有烧芯片的风险，只能用于毫安级小电流&lt;/li&gt;
&lt;li&gt;N 管必须加下拉、P 管必须加上拉，漏了就会被工频干扰；下拉电阻的作用是「让噪声建立不起 0.7V」，不是「把噪声滤掉」&lt;/li&gt;
&lt;li&gt;VBE 反向耐压最小（约 10V），绝对不能反接；VCE 耐压要大于 VCC 的 1.5~2 倍，这是选型主要依据；断开状态也有漏电流，超压后越来越严重&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>实战模块 水龙头 温控风扇 人体感应灯 光控灯</title><link>https://me.622168.xyz/posts/hardware/20-practical-modules/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/20-practical-modules/</guid><description>四个真实产品的电路原理，以及比较器为什么要加上拉电阻。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p49（7.6 分钟）、p50（15.4 分钟）、p51（4.1 分钟）、p52（4.7 分钟）、p53（12.1 分钟）、p54（12.6 分钟）、p55（6.5 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;四个模块的结构都是同一套：传感器 + 比较器 + 三极管开关 + 负载。&lt;/p&gt;
&lt;h2&gt;比较器基础&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;V+   同向输入端（正）
V−   反向输入端（负）
输出  一个输出端

V+ &amp;gt; V−  →  输出高电平
V+ &amp;lt; V−  →  输出低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;举例：反向端加参考电压 VREF，同向端加输入信号 VIN。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VIN &amp;gt; VREF  →  输出高
VIN &amp;lt; VREF  →  输出低
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;比较器为什么要加上拉电阻&lt;/h2&gt;
&lt;p&gt;前提：不是所有比较器都能主动输出高电平和低电平，要看内部结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;推挽输出：内部有两个开关，一个接 VCC、一个接 GND → 可以主动输出高低电平
开漏输出（开集电极输出）：没有上管，只有一个下管接 GND → 只能把信号线拉低，不能拉高
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;开漏输出的解法：外部上拉电阻&lt;/h3&gt;
&lt;p&gt;把信号线上拉到 VCC。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开关断开时：后级接收器件作为输入是高阻态（可看成非常大的电阻到地，例：1MΩ）
           与上拉电阻（例：1K）分压 → 该点约等于 5V = VCC  → 高电平
开关闭合时：信号线被比较器内部开关短路到 GND → 该点电压变为 0V → 低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那个 1MΩ 要注意：高阻态不是断路，是「很大的电阻」。所以上拉电阻必须远小于后级输入阻抗，分压才能拉得上去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开漏输出结构  →  一定要加上拉电阻
推挽结构      →  可以不加
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但有些推挽电路也会加上拉电阻，作用是加大电路输出电流的能力。&lt;/p&gt;
&lt;h2&gt;自动出水水龙头&lt;/h2&gt;
&lt;p&gt;整体结构：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;红外发射管 + 红外接收管  →  比较器  →  N 管开关  →  电机（水泵）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;红外接收二极管的特性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;接法：反向接法（P 端接 GND、N 端接电源端，与普通二极管相反）
无红外信号：视为断路（断开）
有红外信号：导通，流过一定电流
电流大小：与红外光亮度成正比
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;电路推导&lt;/h3&gt;
&lt;p&gt;电路是 &lt;code&gt;5V → 串电阻 R → 红外接收二极管 → GND&lt;/code&gt;，取分压点电压。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V1 = VCC − R × IR
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;假设 &lt;code&gt;IR = 10μA&lt;/code&gt;，&lt;code&gt;R&lt;/code&gt; 取 100K：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10μA × 1K = 10mV
10mV × 100 = 1V
即 100K × 10μA = 1V

有红外：V1 = 5 − 1 = 4V
无红外：IR = 0 → V1 = 5 − 0 = 5V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考电压&lt;/h3&gt;
&lt;p&gt;两个电阻分压：45K 接 5V、5K 接 GND。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VREF = 5V × 5K / (45K + 5K) = 5 × 0.1 = 0.5V ...
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我算了两遍才对上：按分压公式是 &lt;code&gt;5 × 5/(45+5) = 0.5V&lt;/code&gt;，但结果要的是 4.5V。&lt;/p&gt;
&lt;p&gt;也就是说，取的是上端电阻的电压（45K 上的压降），即 &lt;code&gt;5 − 0.5 = 4.5V&lt;/code&gt;。分压点 4.5V 接到比较器同向端（V+），红外接收分压点接到反向端（V−）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;分压点的取法要注意：45K 在上、5K 在下时，中间点电压是 &lt;code&gt;5 × 5/50 = 0.5V&lt;/code&gt;。要得到 4.5V，应该理解成取 45K 两端的压降，或者实际电路是 5K 在上、45K 在下。结果按 4.5V 反推电路即可。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;逻辑&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;有红外：V− = 4V，V+ = 4.5V → 输出高电平 5V → 三极管导通 → 电机转
无红外：V− = 5V，V+ = 4.5V → 输出低电平 0V → 三极管断开 → 电机不转
&lt;/code&gt;&lt;/pre&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;h3&gt;扩展演示&lt;/h3&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;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;实测：供电 5V，示波器直接读到 VPP = 18V。&lt;/p&gt;
&lt;p&gt;18V 已经算偏小了（该电机功率本来就不大），用更精密的示波器应该能测到更高电压。&lt;/p&gt;
&lt;p&gt;原理：电感为保持电流不变，理论上可以感生出无穷大的电压。&lt;/p&gt;
&lt;p&gt;后果：三极管承受不住这么高的电压就会被击穿。&lt;/p&gt;
&lt;p&gt;正确做法：在电机两端加一个续流二极管。&lt;/p&gt;
&lt;h2&gt;温控风扇&lt;/h2&gt;
&lt;p&gt;电路是&lt;strong&gt;比较器 + P 管开关电路&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;T &amp;gt; 36℃  →  风扇转
T ≤ 36℃  →  不转
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若要两个不同阈值（如 36℃ 开、30℃ 关），需要加回差（迟滞）比较器；这里简化成单一阈值开关。&lt;/p&gt;
&lt;p&gt;参考电压：反向端加 VREF，1K 接 5V + 1K 接 GND 分压 → 2.5V。&lt;/p&gt;
&lt;h3&gt;热敏电阻&lt;/h3&gt;
&lt;p&gt;相当于一个可变电阻，阻值随温度变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;正温度系数：温度越高，电阻越大
负温度系数：温度越高，电阻越小
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;选型推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电机转的前提是比较器输出低电平
→ 需要 V+ &amp;lt; V−
→ 需要「温度越高、V+ 越小」
→ 负温度系数热敏电阻应放在 V+ 分压的上端
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段推理是从目标反推元件选型和位置的典型例子。&lt;/p&gt;
&lt;h3&gt;具体数值&lt;/h3&gt;
&lt;p&gt;固定电阻取 1K。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;25℃     热敏 10K        分压点约 4.5V   V+ &amp;gt; V−（2.5V）  输出高电平  电机不转
36℃     热敏 1K         分压点 2.5V     临界             —          —
36.11℃  热敏略小于 1K    分压点 2.499V   V+ &amp;lt; V−          输出低电平  电机转
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实物对应&lt;/h3&gt;
&lt;p&gt;模块 → 一个电阻 → P 管（P 管要加上拉电阻）→ 电机接 VCC。&lt;/p&gt;
&lt;p&gt;这个电路也没有加反向续流二极管，正常应再加一个。&lt;/p&gt;
&lt;h3&gt;应用&lt;/h3&gt;
&lt;p&gt;电脑 CPU 散热风扇。CPU 里的温控风扇用 PWM 调制，可实现温度越高转速越快。&lt;/p&gt;
&lt;h2&gt;人体感应灯&lt;/h2&gt;
&lt;p&gt;器件是热释红外传感器。便利店门口的「欢迎光临」就是用这个做的。&lt;/p&gt;
&lt;h3&gt;内部结构&lt;/h3&gt;
&lt;p&gt;内部有一种特殊材料，一旦接收到红外信号，材料两端感生出电压（非常微弱）。&lt;/p&gt;
&lt;p&gt;内部还会再集成三极管或 MOS 管做放大。&lt;/p&gt;
&lt;p&gt;这里用 MOS 管更合理：该材料感生出来的是很微弱的电压信号，而不是电流信号；MOS 管只需电压、不需要 IB 电流，三极管则需要 IB 电流才能工作。&lt;/p&gt;
&lt;p&gt;这是「电压驱动 vs 电流驱动」在实际选型中的体现。&lt;/p&gt;
&lt;h3&gt;输出电路&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;VCC → 电阻 R → MOS 管漏极 → 源极 → GND&lt;/code&gt;，漏极作输出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无红外：MOS 管断开 → 该点 = VCC
有红外：MOS 管导通 → 该点 = VCC − VR（VR = I × R）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;用比较器实现&lt;/h3&gt;
&lt;p&gt;参考电压 VREF 接反向端，传感器输出接同向端。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无人经过：V+ = VCC &amp;gt; VREF  → 输出高电平
有人经过：V+ 下降 &amp;lt; VREF   → 输出低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后级用 P 管（&lt;code&gt;VCC → P 管 → LED → 电阻 → GND&lt;/code&gt;）：低电平导通，灯亮。&lt;/p&gt;
&lt;h3&gt;改用 N 管的接法&lt;/h3&gt;
&lt;p&gt;把传感器输出接到比较器的另一端（V−），VREF 接到 V+。&lt;/p&gt;
&lt;p&gt;VREF 必须设置在 VCC 和 (VCC − IR) 之间：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：VCC−IR = 3V、VCC = 5V  →  VREF 可取 4V
无人：该点为 5V；有人：降到 3V
→ V+ &amp;gt; V−  →  输出高电平  →  经电阻驱动 N 管  →  LED 亮
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键特性&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;热释红外只能识别「移动的热」，物体不移动就识别不了。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;所以手不动灯会灭。很多人做这个项目时困惑的就是这点——不是电路坏了，是传感器原理如此。&lt;/p&gt;
&lt;p&gt;实物演示：手划过灯亮；手不动，过一会灯就灭。&lt;/p&gt;
&lt;h2&gt;光控灯&lt;/h2&gt;
&lt;h3&gt;控制逻辑&lt;/h3&gt;
&lt;p&gt;用 N 管做电子开关：高电平导通 → LED 亮；低电平不导通 → LED 灭。&lt;/p&gt;
&lt;p&gt;前面加比较器，反向端接 VREF。&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;li&gt;亮度越高、阻值越小（成反比）—— 本例选用这一种&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;具体数值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;VREF                 2.5V
光敏电阻（很暗）      10K
光敏电阻（很亮）      100Ω
固定分压电阻          10K
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑：很暗 → 灯亮；很亮 → 灯灭。&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;很亮：光敏 100Ω → 分压点约 0.5V &amp;lt; 2.5V → 输出低电平 → LED 不亮
很暗：光敏 11K   → 分压点 &amp;gt; 2.5V        → 输出高电平 → LED 亮
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实物演示：亮处灯不亮；手挡住光敏电阻（变暗）灯亮，手松开灯灭。&lt;/p&gt;
&lt;h2&gt;四个模块的统一结构&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;传感器（把物理量变成电信号）
    ↓
比较器（把模拟信号变成高低电平）
    ↓
三极管/MOS 开关（把弱信号变成驱动能力）
    ↓
负载（电机 / LED）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看懂了这套结构，90% 的传感器模块都能看懂。&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;/td&gt;
&lt;td&gt;红外接收二极管&lt;/td&gt;
&lt;td&gt;4.5V&lt;/td&gt;
&lt;td&gt;N 管&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;2.5V&lt;/td&gt;
&lt;td&gt;P 管&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;4V（N 管接法）&lt;/td&gt;
&lt;td&gt;P 管&lt;/td&gt;
&lt;td&gt;LED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;光控灯&lt;/td&gt;
&lt;td&gt;光敏电阻&lt;/td&gt;
&lt;td&gt;2.5V&lt;/td&gt;
&lt;td&gt;N 管&lt;/td&gt;
&lt;td&gt;LED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;开漏输出的比较器必须加上拉电阻，否则输出永远是低电平；上拉电阻要远小于后级输入阻抗（1K vs 1MΩ），否则拉不上去&lt;/li&gt;
&lt;li&gt;红外接收二极管是反接的（和普通二极管相反），接反了不工作；白天调试必须关窗，太阳光里的红外会让它不受控&lt;/li&gt;
&lt;li&gt;热释红外只能识别移动的热，手不动灯就灭，这是正常的&lt;/li&gt;
&lt;li&gt;电机电路都要加续流二极管，这里的示例电路都漏了，实测尖峰能到 18V；测尖峰电压前先把示波器调到衰减档，否则打坏探头&lt;/li&gt;
&lt;li&gt;热敏/光敏电阻要选对温度系数和亮度系数，还要放对位置（从目标反推）；比较器阈值是单一的还是带回差的，看产品要求，单一阈值在临界点附近会抖动&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MOS 管的基本结构与导通条件</title><link>https://me.622168.xyz/posts/hardware/21-mosfet-structure-condition/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/21-mosfet-structure-condition/</guid><description>MOS 管由四部分组成、寄生体二极管怎么来的，以及 Vgs 导通条件为什么 P 管是负值。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p56（10.4 分钟）、p57（5.7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;结构：四部分，不是三段&lt;/h2&gt;
&lt;p&gt;三极管是 NPN 三段半导体组合。MOS 管是四部分：G（栅极）、S（源极）、D（漏极），再加底部的衬底。&lt;/p&gt;
&lt;h3&gt;四部分之间默认绝缘&lt;/h3&gt;
&lt;p&gt;四大部分两两之间默认都是绝缘的。由此推出三个寄生电容：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;G–S 之间有寄生电容
G–D 之间有寄生电容
D–S 之间有寄生电容
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;后面「MOS 开关断开后灯还亮」的问题就是从这里来的。&lt;/p&gt;
&lt;h3&gt;寄生体二极管是怎么来的&lt;/h3&gt;
&lt;p&gt;以 N MOS 增强型为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① S、D 掺杂 N 型半导体，衬底是 P 型
② 衬底与 S、衬底与 D 各形成一个 PN 结（二极管）
③ 实际 MOS 内部用一根导线把 S 与衬底短接
   → 衬底-S 的二极管被短路
④ 剩下衬底-D 的二极管未被短路
   而 S 已与衬底等电位
   → 等效成 S 与 D 之间多了一个二极管
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是 N MOS 符号里 S–D 之间那个寄生二极管（箭头画在中间那条线 = 衬底，且衬底引出线连到 S 极）。&lt;/p&gt;
&lt;h3&gt;符号方向&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;N MOS 的寄生体二极管：由 S 指向 D
P MOS 的寄生体二极管：由 D 指向 S
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;P MOS 与 N MOS 的唯一区别就是寄生体二极管方向相反，寄生电容情况完全相同。&lt;/p&gt;
&lt;p&gt;这两个方向必须记牢，后面判断「电流能不能过」全靠它。&lt;/p&gt;
&lt;h2&gt;导通条件&lt;/h2&gt;
&lt;h3&gt;N MOS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vgs = Vg − Vs &amp;gt; Vgs(th)  时 D–S 导通，否则断开
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：用 N MOS 控制 LED，设 &lt;code&gt;Vgs(th) = 2V&lt;/code&gt;，S 接 GND（&lt;code&gt;Vs = 0V&lt;/code&gt;）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;G 给 &amp;gt; 2V 即导通
VCC = 5V 经开关加到 G → Vgs = 5V → 导通，LED 亮
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;P MOS&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vgs = Vg − Vs &amp;lt; Vgs(th)  时 D–S 导通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：P MOS 的 S 接 5V，下面接电阻 + LED；设 &lt;code&gt;Vgs(th) = 2V&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;G 电压比 S 低 2V 以上（即 G &amp;lt; 3V）时导通，LED 亮
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;坑：P MOS 的 Vgs(th) 是负值&lt;/h3&gt;
&lt;p&gt;数据手册里 P MOS 的 &lt;code&gt;Vgs(th)&lt;/code&gt; 通常是负值，因为 &lt;code&gt;Vgs = Vg − Vs&lt;/code&gt; 而 &lt;code&gt;Vg &amp;lt; Vs&lt;/code&gt;：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：Vs = 5V、Vg = 2V → Vgs = 2 − 5 = −3V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;看到负值不用觉得奇怪，它只是相对电压，负值含义就是「G 比 S 低」。&lt;/p&gt;
&lt;h2&gt;Vgs 是个相对量&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Vgs&lt;/code&gt; 是栅极相对源极的电压，不是对地电压。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;S 接地时：Vgs = Vg（对地电压就是 Vgs）
S 不接地时：Vgs = Vg − Vs，Vs 会变，Vgs 也跟着变  ← 后面 p61 的大坑
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电压是相对量这个观念，在 MOS 管上比在任何地方都重要。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MOS 有四个部分（含衬底），不是三个，衬底决定了体二极管的方向。&lt;/li&gt;
&lt;li&gt;G 和 S/D 之间是绝缘的，所以有寄生电容，也所以 G 极几乎不耗电。&lt;/li&gt;
&lt;li&gt;体二极管方向：N MOS 从 S 到 D，P MOS 从 D 到 S，记反了整个电路分析都错。&lt;/li&gt;
&lt;li&gt;P MOS 的 Vgs(th) 是负值，这是正常的，不是数据手册写错了。&lt;/li&gt;
&lt;li&gt;Vgs 是相对源极的，源极电位一变，导通状态就可能变。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MOS 开关电路设计与寄生参数</title><link>https://me.622168.xyz/posts/hardware/22-mosfet-switch-parasitic/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/22-mosfet-switch-parasitic/</guid><description>栅极为什么不用限流电阻、断开后灯还亮是为什么、以及 G-S 电阻该取多大。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p58（10 分钟）、p59（14.5 分钟）、p60（10 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;开关电路设计&lt;/h2&gt;
&lt;h3&gt;N MOS 开关&lt;/h3&gt;
&lt;p&gt;和 NPN 三极管控制 LED 的电路结构一一对应——D 对应集电极、S 对应发射极。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VCC(5V) ──[ LED ]──[ 限流电阻 ]── D
                                  │  N MOS
             控制端 ───────────── G
                                  S
                                  │
                                 GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数值验算：开关闭合 → &lt;code&gt;VG = 5V&lt;/code&gt;、&lt;code&gt;VS = 0V&lt;/code&gt; → &lt;code&gt;Vgs = 5V&lt;/code&gt;；设 &lt;code&gt;Vgs(th) = 2V&lt;/code&gt; → 导通，LED 亮。&lt;/p&gt;
&lt;h3&gt;P MOS 开关&lt;/h3&gt;
&lt;p&gt;和 PNP 三极管电路逻辑相似——S 接电源端，D 下方接电阻 + LED 到地。&lt;/p&gt;
&lt;p&gt;数值验算：&lt;code&gt;S = 5V&lt;/code&gt;，G 经电阻接开关到 GND，开关闭合 → &lt;code&gt;VG = 0V&lt;/code&gt; → &lt;code&gt;Vgs = 0 − 5 = −5V&lt;/code&gt;；设 &lt;code&gt;Vgs(th) = 2V&lt;/code&gt;（对应 −2V）→ 满足导通条件，LED 亮。&lt;/p&gt;
&lt;h3&gt;核心区别&lt;/h3&gt;
&lt;p&gt;三极管是电流控制电流，必须先有 Ib 才有 Ic；MOS 管是电压控制电流，只要 Vgs 够就行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;三极管：B 极必须加限流电阻，否则 5V 怼上去瞬间烧管
MOS 管：G 极可以不加（导通时 G–S 之间没有电流）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;MOS 的 G 极为什么可以不加限流电阻：G–S 之间是绝缘的，导通时没有电流流过，不存在「限流」的需求。实物演示中确实没加任何电阻。&lt;/p&gt;
&lt;h3&gt;实物演示&lt;/h3&gt;
&lt;p&gt;MOS 管与三极管的封装外形一模一样（都是 TO-92 这种）。封装就是芯片的衣服。&lt;/p&gt;
&lt;p&gt;接上 5V 电源、GND，灯不亮；把 G 那一点接到 VCC → 灯亮；手放开 → 灯灭。&lt;/p&gt;
&lt;h3&gt;引申&lt;/h3&gt;
&lt;p&gt;MOS 导通后自身不消耗驱动电流，所以低功耗芯片内部大多采用 MOS 结构。&lt;/p&gt;
&lt;h2&gt;寄生电容问题&lt;/h2&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;最简单的 MOS 开关电路（5V + LED，G 经开关接 5V）：按理论开关断开就该灭，但实际开关断开后 LED 继续亮。&lt;/p&gt;
&lt;h3&gt;原因&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关闭合时：G = 5V、S = 0V
           → G–S 寄生电容被充到 5V
           → Vgs 电压就等于该电容电压，故仍满足导通
开关断开后：G 往上是断开的开关、往下是绝缘层
           → 电容没有放电回路
           → Vgs 仍保持约 5V → 继续导通
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么是缓慢下降&lt;/h3&gt;
&lt;p&gt;任何绝缘材料都不可能 100% 绝缘，总有一点漏电流。寄生电容靠 MOS 内部漏电流慢慢放电，最终也会关断，但有明显延时。&lt;/p&gt;
&lt;p&gt;仿真演示：G 极电压 &lt;code&gt;4.95V → 4.94V → 4.93V…&lt;/code&gt; 缓慢下降。&lt;/p&gt;
&lt;p&gt;实物演示（降低曝光后可见）：G 接到电源灯亮，手一离开灯是慢慢慢慢灭的，不是立刻灭。&lt;/p&gt;
&lt;h3&gt;和三极管对比&lt;/h3&gt;
&lt;p&gt;三极管是电流控制型，自身持续消耗电流，控制信号一撤走就快速消耗掉电容能量，立刻灭；MOS 是电压控制型，栅极绝缘，电容没地方放电，慢慢灭。&lt;/p&gt;
&lt;h3&gt;解决：G–S 之间加一个电阻&lt;/h3&gt;
&lt;p&gt;给寄生电容提供放电回路。&lt;/p&gt;
&lt;p&gt;一般加 100K。加小了，开关闭合期间电阻会一直耗能，损耗大；加这么大还够快，是因为寄生电容一般是 pF 级别，很小。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;τ = RC = pF × 100K = 微秒级
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;微秒对人是一瞬间，对电路不算特别快。&lt;/p&gt;
&lt;h3&gt;进一步加速：推挽驱动&lt;/h3&gt;
&lt;p&gt;用一个专用芯片，内部就是两个开关——一个接 VCC、一个接 GND，对 G 极电容快速充放电，实现快速开关。&lt;/p&gt;
&lt;h3&gt;三极管侧同样要加电阻&lt;/h3&gt;
&lt;p&gt;作用是抗干扰：实物演示中三极管基极不加电阻时，手摸上去灯会闪——工频干扰在人体上感应出电荷，导致三极管不断闪烁。&lt;/p&gt;
&lt;h3&gt;实物验证&lt;/h3&gt;
&lt;p&gt;在 G–S 之间缠上一个 100K 电阻后，开关闭合再断开，LED 立刻熄灭，「慢慢变暗」的现象消失。&lt;/p&gt;
&lt;h3&gt;坑&lt;/h3&gt;
&lt;p&gt;不要想当然认为 MOS 开关跟三极管一样设计就行。有些 MOS 管的漏电特性非常好，不加放电回路时，开关断开后能持续导通十几秒（实际遇到过这种现场）。&lt;/p&gt;
&lt;h2&gt;寄生体二极管&lt;/h2&gt;
&lt;h3&gt;方向先记牢&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;N MOS 体二极管：从 S 指向 D
P MOS 体二极管：从 D 指向 S
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;案例：P MOS 控制电池供电开关&lt;/h3&gt;
&lt;p&gt;设 P MOS 的 &lt;code&gt;Vgs(th) = 3V&lt;/code&gt;；电池输入 6V。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;三极管断开：G = 6V、S = 6V、Vgs = 0  → MOS 断开，负载无电
三极管导通：G ≈ 0.3V、S = 6V，满足条件 → MOS 导通，6V 送到负载
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;双路供电的坑&lt;/h3&gt;
&lt;p&gt;逻辑是检测到外部电源接入就断开 MOS，让电池不再供电。&lt;/p&gt;
&lt;p&gt;外部电源 6V，电池用久后降到 5.5V：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MOS 断开后，外部 6V 会经过体二极管给电池充电（设体二极管压降 0.5V）
若电池掉到 5V，6V 经二极管给 5V 供电，会有很大的电流
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真实测：正常供电时负载电流约 6mA；接入外部电源后约 6A（就是给电池充电的电流）。后果要么电池坏掉，要么 MOS 坏掉。&lt;/p&gt;
&lt;h3&gt;解决办法一：把 MOS 调换方向（S/D 对调）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关闭合：依旧约 6mA  ✓
电池降到 5V、切到外部电源：只有 36pA（相当于没有）✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代价（必须注意）：换向之后，该 P MOS 不能再关断电池对外的供电——即使开关断开，电池仍会经体二极管给负载供电，无法完全关闭。&lt;/p&gt;
&lt;h3&gt;解决办法二：再加一个 P MOS 反向串联&lt;/h3&gt;
&lt;p&gt;利用体二极管与体二极管相互抵消，从而彻底关断。&lt;/p&gt;
&lt;p&gt;仿真踩坑记录：第一次接线时电阻画错位置——这个电阻一定要加在 G–S 之间。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;只换向：开关断开后仍有电流（约 4.8V 处），说明「没有关死」
加第二个反向 MOS：默认无电压；闭合有电压；再断开就没电流、电压≈0；
                 从另一侧送电同样只有 pA 级电流 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;总原则&lt;/h3&gt;
&lt;p&gt;只要电路里用到 MOS 管——第一考虑寄生电容，第二考虑寄生体二极管。&lt;/p&gt;
&lt;p&gt;设计完电路后，要把二极管加进来，推演 MOS 断开时二极管会给电路带来什么影响。&lt;/p&gt;
&lt;p&gt;这两个东西非常容易导致电路出问题；即使「用多一个 MOS 去控制体二极管」，也一样会遇到同类问题。&lt;/p&gt;
&lt;h2&gt;G–S 电阻取值&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;低频开关（&amp;lt; 100K）：100K，损耗小，放电够快
高频开关：可减到 4K，速度快，损耗大
规律：电阻越小 → 开关速度越高，但损耗越大
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恒定原则：不管用 P MOS 还是 N MOS 做开关，G–S 之间必须加一个电阻。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;MOS 的 G 极不需要限流电阻，但必须有 G–S 电阻放电，且位置必须加在 G–S 之间。&lt;/li&gt;
&lt;li&gt;开关断开后灯还亮是寄生电容在维持 Vgs，不是电路画错了。&lt;/li&gt;
&lt;li&gt;寄生体二极管会让 MOS「关不死」，必须推演它带来的电流路径。&lt;/li&gt;
&lt;li&gt;电池 + 外部电源双路供电时，体二极管会造成大电流倒灌（6mA → 6A）。&lt;/li&gt;
&lt;li&gt;换向能解决倒灌，但会失去关断能力，要彻底解决必须反向串联两个 MOS。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>NMOS 与 PMOS 电源开关设计</title><link>https://me.622168.xyz/posts/hardware/23-mosfet-power-switch/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/23-mosfet-power-switch/</guid><description>N 管源极不接地会掉进放大状态、自举电路怎么把电压顶上去、PMOS 防反接，以及 MOS 电流为什么能双向流。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p61（23.3 分钟）、p62（4.7 分钟）、p63（5.1 分钟）、p64（13.3 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;N MOS 源极不接地会怎样&lt;/h2&gt;
&lt;h3&gt;场景&lt;/h3&gt;
&lt;p&gt;N MOS 的 S 极不接 GND，而是接在负载上（如半桥驱动电路 / 推挽电路的上管：两个 N MOS，上管接 VCC、下管接 GND，中间作为输出）。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;S 极不接 GND 时，N MOS 不会饱和导通，会进入放大状态。&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;p&gt;设 G 极控制电压 5V，后级电源 4.5V（实物用 3 节电池），&lt;code&gt;Vgs(th) = 2V&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;上电前所有电压为 0
5V 接入瞬间：VG = 5V、VS = 0V → Vgs = 5V → 饱和导通，开始给负载供电

但 S 点电压不是瞬间到 4.5V，而是从 0V 慢慢上升
由于 Vgs = VG − VS，VS 上升 → Vgs 下降
  VS = 1V 时 Vgs 只剩 4V
  VS = 3V 时 Vgs 只剩 2V = 临界饱和
  VS = 3.001V 时，MOS 从饱和导通变为放大状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进入放大状态后 MOS 不再是开关，等效为一个大电阻，产生压降并分压，最终把 S 点电压限制在约 3V 上不去。&lt;/p&gt;
&lt;h3&gt;仿真验证&lt;/h3&gt;
&lt;p&gt;本应输出 12V，实际示波器只有 1.35V，即 MOS 吃掉了约 10.7V。&lt;/p&gt;
&lt;h3&gt;发热量化&lt;/h3&gt;
&lt;p&gt;负载电阻改为 10Ω：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电压只有 1.3V → 电流约 130mA
130mA × 约 10V → MOS 上损耗超过 1W  →  必然发热
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决一：G 极驱动电压取得比负载电源更高&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;负载电源 4.5V，G 极用 8V 驱动
→ 即使 4.5V 全部过来，Vgs 仍有 5.5V 压差，仍能饱和导通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代价是成本高。8V 不会凭空产生，需要升压电路（电荷泵或 boost），无论哪种都要多一个芯片电路。&lt;/p&gt;
&lt;h3&gt;解决二（低成本）：自举电路&lt;/h3&gt;
&lt;p&gt;利用「电容两端电压不能突变」的特性实现电压自举。&lt;/p&gt;
&lt;p&gt;接法：把驱动电源（5V）的负极接到 MOS 的 S 极（而不是接到电路的地），正极接 G 极。&lt;/p&gt;
&lt;p&gt;原理：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;把驱动电源当成一个电池，其正极永远比负极高 5V
开关闭合 → Vgs = 5V → MOS 导通 → 12V 送到 S
S 被抬高的同时，G 相对 GND 也被同样抬高
  （相当于 12V 电源与 5V 电源首尾串联）
G 点被顶到约 17V
→ 因此 Vgs 始终保持 5V  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真效果：S 点得到 11.8V ≈ 12V。不加自举则至少需要 15V 驱动。&lt;/p&gt;
&lt;p&gt;实物验证：把驱动电源负极改接到 S 极后，只需调到 2V 以上（1.9V 时负载得 4.78V），再调高也一直是 4.78V——永远保证 Vgs 等于所加驱动电压。&lt;/p&gt;
&lt;p&gt;实际电路用自举电容而非电池，典型结构含：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;几个开关（控制 G 极通断）+ 一个电容 + 一个自举二极管 + 底部给电容充电的回路
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;充满电后该电容就等效于上面说的「电池」，这个电容叫自举电容。&lt;/p&gt;
&lt;h3&gt;实物验证（未用自举时）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;驱动 5V            → 10Ω 负载上 3.4V  → 推出该 MOS 的开通电压约 1.5V
驱动 6V（= 4.5 + 1.5） → 4.4V
驱动 8V            → 4.8V
再往上             → 4.79V 不再变化（已饱和导通，电源完全送到负载）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这部分内容很常用，特别是 H 桥里的知识点。&lt;/p&gt;
&lt;h2&gt;P MOS 电源开关设计&lt;/h2&gt;
&lt;p&gt;思路：直接用 P MOS 替代 N MOS，从而免去自举等复杂驱动电路。&lt;/p&gt;
&lt;p&gt;P MOS 天生适合做电源端开关，因为它的导通条件是 G 极电压比 S 极低 → S 极通常接电源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VCC(12V) ──┬── S
           │  P MOS
           ├── D ──[ 电阻 ]──[ LED ]── GND
           │
          [ 下拉电阻 ]
           │
          GND
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;G 极加一个下拉电阻到 GND → &lt;code&gt;G = 0V&lt;/code&gt; → &lt;code&gt;Vgs&lt;/code&gt; 比 S 低 12V → 导通。&lt;/p&gt;
&lt;p&gt;关键好处：S 本来就接在电源最高点，导通与否都不会影响 Vgs 本身（12V 一直是 12V），不存在 N MOS 那种 S 电位抬高导致 Vgs 崩塌的问题。&lt;/p&gt;
&lt;h3&gt;G–S 电阻取值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;低频（开关频率约 100K 以下）：100K 左右
高频：可减到 4K
规律：电阻越小 → 开关速度越高，但损耗越大
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用 P MOS 替代后，驱动电路从原来「一堆复杂电路」简化为两个电阻 + 一个开关。&lt;/p&gt;
&lt;p&gt;实际电路中很少用机械开关，一般用三极管或 MOS 管，由单片机控制。&lt;/p&gt;
&lt;h3&gt;P MOS 的局限&lt;/h3&gt;
&lt;p&gt;大功率、高频、大电流场景下 P MOS 无法替代 N MOS。&lt;/p&gt;
&lt;p&gt;原因是工艺：在同等成本、同等级工艺下，P MOS 做出来的性能就是不如 N MOS。因此高频或大电流场景优先考虑 N MOS。&lt;/p&gt;
&lt;h2&gt;P MOS 防反接电路&lt;/h2&gt;
&lt;p&gt;电源 12V，P MOS 作防反接；由 MCU 经三极管控制 G 极。&lt;/p&gt;
&lt;h3&gt;正常工作流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 控制信号为低电平 → 三极管断开 → G 点与 S 点电位相同 → MOS 不闭合
② 12V 接入后先经过体二极管 → 该点变为 11.3V（= 12 − 0.7V 二极管压降）
③ 11.3V 一方面给负载供电，一方面经降压电路给 MCU 供电
   （在此之前 12V 未接入，MCU 不工作，所以三极管必然断开）
④ MCU 工作后写程序让控制信号变高电平 → 三极管导通
⑤ 11.3V 经两个电阻分压得到约 6V → 满足 MOS 导通条件 → MOS 闭合
   → 体二极管被短路 → 该点电压从 11.3V 变成 12V  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时序很巧妙：先靠体二极管「漏」一点电给 MCU 启动，MCU 起来后再把 MOS 打开，把 0.7V 的损失补回来。体二极管在这里不是缺陷，而是启动通道。&lt;/p&gt;
&lt;h3&gt;反接保护原理（电池接反）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;若无 MOS 管：12V 直接接到 MCU 的 D 端
            而 MCU 不能承受反向耐压，很可能直接损坏

加了 MOS 管后：12V 确实会到 MCU 的 D 端
              但要损坏器件的前提是必须流过电流
              而这里不会流过电流
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不会流过电流：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;三极管不导通
→ 两个分压电阻上没有电流也就没有压降
→ MOS 不导通
→ 电流路径被寄生体二极管中断（方向不通）
→ 没有电流 → 反接不影响后级  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;简化版本&lt;/h3&gt;
&lt;p&gt;直接把三极管位置接地，MOS 的控制与 MCU 完全无关，只负责给 MCU 供电：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12V 一接进来立刻变 11.3V
→ 两个电阻分压成 6V
→ MOS 立刻导通 → 短路体二极管 → 立刻变成 12V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;去掉三极管效果一样（MCU 控不控制三极管最终都一样会饱和导通），同样具备防反接效果。&lt;/p&gt;
&lt;h2&gt;MOS 的电流可以双向流通&lt;/h2&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;三极管电流单向流通；MOS 管 D–S 之间的电流可以双向流通。&lt;/p&gt;
&lt;p&gt;网上资料说「MOS 电流单向」是错的。&lt;/p&gt;
&lt;h3&gt;结构层面的解释（N MOS）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;S、D 掺 N 型半导体，衬底是 P 型
G–S 加正向电压后，在 D 和 S 之间形成沟道
电子通过沟道从 S 流到 D
——因为沟道就是「一条沟」，电子既可以从 D 流向 S，也可以从 S 流向 D
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只要 G–S 满足导通条件，S 和 D 就是直接接在一起的，电流方向无所谓。&lt;/p&gt;
&lt;h3&gt;实验对比（万用表测 DS 两端电压，≈0 即正常导通）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;正常接法（上正下负）：开关断开 DS 两端 = 5V；开关闭合 = 0V（正常导通）
反接（下正上负）：开关断开测到 −0.58V（体二极管压降）；开关闭合立刻变成 0V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;抵消体二极管的接法&lt;/h3&gt;
&lt;p&gt;在原有 MOS 基础上再加一个 MOS，反向（镜像）画。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;两个 MOS 都断开时：
  第一个体二极管能让电流通过
  但走到第二个 MOS 时其体二极管面对面相互抵消 → 电流不能通过
  → 反接时电流也不通 ✓
开关一闭合，两个 MOS 都导通，电流正常流通 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;典型应用&lt;/h3&gt;
&lt;p&gt;模拟开关芯片内部就是大量使用这种 MOS 结构——模拟开关芯片的电流能双向流通，就是利用了 MOS 的 D–S 之间没有电流方向这一特性。&lt;/p&gt;
&lt;p&gt;常见的误区是一用 MOS 就觉得电流只能单向流，因为「老觉得那个二极管在那里作怪」——其实二极管可以通过加多一个 MOS 抵消掉。&lt;/p&gt;
&lt;h2&gt;N MOS 还是 P MOS 做电源开关&lt;/h2&gt;
&lt;p&gt;能放低边就用 N MOS，最简单。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;必须高边 + 小功率/低频  →  用 P MOS（驱动简单）
必须高边 + 大功率/高频  →  用 N MOS + 自举驱动
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;N MOS 做高边需要自举或升压驱动，电路复杂，但性能好，适合大功率、高频、大电流；P MOS 做高边只要两个电阻 + 一个开关，复杂度低，但性能略差，只适合小功率、低频。两者成本都低（N MOS 的驱动部分贵）。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;N MOS 源极不接地就一定进放大状态，会发热且输出上不去。&lt;/li&gt;
&lt;li&gt;自举的本质是「让驱动电源的地跟着源极一起抬」，把 G 点顶到比电源还高。&lt;/li&gt;
&lt;li&gt;P MOS 做高边开关简单，但性能不如 N MOS，大电流高频场景不能用。&lt;/li&gt;
&lt;li&gt;防反接电路里体二极管是启动通道，不是缺陷；反接保护的原理是「没有电流」而不是「没有电压」。&lt;/li&gt;
&lt;li&gt;MOS 电流可以双向流；抵消体二极管要两个 MOS 镜像串联。仿真里也要加 G–S 电阻，否则模型太理想，看不出真实问题。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>MOS 的体二极管、Vgs 与耐压</title><link>https://me.622168.xyz/posts/hardware/24-mosfet-body-diode-vgs-voltage/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/24-mosfet-body-diode-vgs-voltage/</guid><description>体二极管抵消的仿真实测、Vgs 与 Id 的实测曲线，以及 G-S 耐压击穿的全过程。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p65（8.1 分钟）、p66（10.3 分钟）、p67（9.3 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;抵消体二极管&lt;/h2&gt;
&lt;p&gt;用仿真逐步验证 p64 的结论。&lt;/p&gt;
&lt;h3&gt;基础电路：单个 N MOS&lt;/h3&gt;
&lt;p&gt;12V 电源 + 1K 负载 + 5V 控制。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;正常接法、开关闭合：电流 12mA，MOS 两端压降 1.183mV → 已完全导通
12V 反接、开关断开：DS 压降 0.37V（体二极管压降）
12V 反接、开关闭合：又变成一点多毫伏（从 MOS 内部走，不走二极管）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;按单管电路工作，这个 MOS 控制不了 1K 负载的供电。&lt;/p&gt;
&lt;h3&gt;抵消做法：上下镜像再串一个 MOS&lt;/h3&gt;
&lt;p&gt;这里踩过两个坑。一是方向画错（两个必须上下镜像），会出现「MOS 没有关掉、有漏电流」的现象；二是仿真模型太理想，需要在 G–S 之间补一个 100K 电阻才合理。&lt;/p&gt;
&lt;h3&gt;抵消后的仿真结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关未闭合：两端 = 12V，电流非常非常小 ✓
开关闭合：两个 MOS 都导通，压降约 2mV
电源正反接：两个方向都能被控制，压降一致、电流都约 12mA ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两个 MOS 反向串联后，彼此的体二极管互相抵消，可组成双向导通的开关。&lt;/p&gt;
&lt;h3&gt;应用场景&lt;/h3&gt;
&lt;p&gt;用 MOS 代替继电器。继电器本身是双向导通的；若只用单个 MOS 代替，只能规定电流走一个方向，反方向会误导通；加两个 MOS 后就不会出现误导通。&lt;/p&gt;
&lt;h2&gt;Vgs 与 Id 的关系&lt;/h2&gt;
&lt;h3&gt;和三极管类比&lt;/h3&gt;
&lt;p&gt;三极管是 &lt;code&gt;Ic = β × Ib&lt;/code&gt;，放大参数是 β（电流放大倍数）；MOS 管是 &lt;code&gt;Id&lt;/code&gt; 与 &lt;code&gt;Vgs&lt;/code&gt; 相关，放大参数是跨导 gm。公式成立条件是 MOS 必须工作在「线性区或放大区」。&lt;/p&gt;
&lt;p&gt;这个参数很少关注，知道有这个关系即可。&lt;/p&gt;
&lt;h3&gt;Vgs–Id 曲线&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vgs 很小时      →  Id 一直为 0（断开状态）
Vgs 达到某点后  →  Id 开始有电流（进入导通区）
Vgs 继续增大    →  Id 逐渐变大
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么要讲这一点&lt;/h3&gt;
&lt;p&gt;很多同学以为 MOS 像机械开关——「一按开了就开了」，没有中间态。实际上 MOS 和三极管都有中间态，不是一开通就完全饱和导通，必须 Vgs 慢慢变大到一定程度才能饱和导通。&lt;/p&gt;
&lt;h3&gt;经验数值&lt;/h3&gt;
&lt;p&gt;常见 MOS 管只要 &lt;code&gt;Vgs &amp;gt; 4.5V&lt;/code&gt;，基本就接近/达到完全饱和导通状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Vgs&lt;/code&gt; 越高 → &lt;code&gt;Rds(on)&lt;/code&gt; 越小，流过的电流会稍微变大一些，但变化程度不会特别大。&lt;/p&gt;
&lt;h3&gt;实物实验&lt;/h3&gt;
&lt;p&gt;接法：电池（4.5V）接在 D–S 之间充当电源；直流稳压电源接在 G–S 之间调节 Vgs；万用表串联进 D–S 回路测 Id。&lt;/p&gt;
&lt;p&gt;该 MOS 之前查资料约 1.5V 左右开通。&lt;/p&gt;
&lt;p&gt;万用表在 20V / 20A 档位时最小采样精度是 10mA，所以读数有下限。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vgs = 1V         →  Id = 0
Vgs = 1.9V       →  Id = 10mA（开始有读数）
Vgs = 2.7V       →  Id = 170mA
Vgs = 4.5V 左右   →  基本进入完全饱和导通
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;继续升高 Vgs，&lt;code&gt;Rds(on)&lt;/code&gt; 更小，规律不变——Vgs 上升，Id 上升。&lt;/p&gt;
&lt;p&gt;总结：MOS 管不是理想开关——「开了」并不等于完全开通，必须让 Vgs 达到某一定值，内部沟道才完全打开，才能流过最大值电流。&lt;/p&gt;
&lt;h2&gt;耐压&lt;/h2&gt;
&lt;h3&gt;前提&lt;/h3&gt;
&lt;p&gt;理想开关断开后可承受无穷大电压，但所有半导体器件的耐压都是有限的。&lt;/p&gt;
&lt;p&gt;MOS 断开时，电源电压会完全叠加在 D–S 之间。例：12V 电源 → MOS 承受 12V。&lt;/p&gt;
&lt;h3&gt;选型余量规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;例：100V 的电源，至少要挑 150V 以上耐压的 MOS
耐压和电流余量一般至少要留 1.2 倍以上
对成本不敏感  →  余量可以放大到 2 倍甚至 3 倍
对成本敏感    →  尽量靠近最大工作电压，但至少留 1.2 倍以上
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Vds 耐压实测（2N7000）&lt;/h3&gt;
&lt;p&gt;该管的 Vds 耐压 = 60V。直流稳压电源最大也是 60V，直接怼到 60V 也没有出现电流突然变大 → 没有被击穿。&lt;/p&gt;
&lt;p&gt;标称 60V 耐压实际能承受 60V 以上，厂家会留一定余量，不会「刚好 60V 就炸」。&lt;/p&gt;
&lt;p&gt;实验注意：正常应在 G–S 之间加一个电阻保证 MOS 一定不导通（这次马马虎虎直接接的）。&lt;/p&gt;
&lt;h3&gt;Vgs 耐压&lt;/h3&gt;
&lt;p&gt;大部分 MOS 的 G–S 耐压只有 ±20V 或 ±30V（有些特殊工艺会高很多）。&lt;/p&gt;
&lt;p&gt;G–S 耐压与 D–S 耐压完全是两回事，不要看到 D–S 能耐受一两百伏，就以为 G–S 也能承受这么高。&lt;/p&gt;
&lt;h3&gt;G–S 击穿全过程实测&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;16V   → 没事
20V   → 也没击穿
29.6V → 出现约 1mA 电流，说明 Vgs 已开始击穿
        （判断实际可能 22V 左右就开始击穿了，只是电源显示不出电流）
32V   → 约 4mA
再大  → 7mA
再大  → 11mA
及时把电压降下来 → 电流立刻回到零 → 此时 MOS 还没损坏 ✓
万用表测 DS（G–S 有 7.2V）→ 会响 → 管还正常 ✓
继续加压 → 15 → 19 → 24 → 30 → 34 → 41mA
然后  → 电流突然消失——管子没炸，但已完全击穿，处于断路状态，即已损坏
把电压降到 10V 再测 DS → 不响，证实已坏（G–S 再也控制不了 D–S）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;最终结论&lt;/h3&gt;
&lt;p&gt;有些 MOS 的 D–S 耐压能达到几百伏甚至 3000V，但 G–S 耐压不一定高。&lt;/p&gt;
&lt;p&gt;选型和使用时必须单独核对 G–S 的标称耐压，不能超过其额定值。&lt;/p&gt;
&lt;h3&gt;击穿的两个阶段&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;第一阶段（可恢复）：出现漏电流，及时降压 → 恢复正常
第二阶段（永久损坏）：电流突然消失 → 栅极绝缘层已被打穿，不可恢复
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以「发现电流异常就赶紧降压」是保住 MOS 的关键动作。&lt;/p&gt;
&lt;h2&gt;参数速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;Vds     30V ~ 3000V     留 1.2~3 倍余量
Vgs     ±20V / ±30V     与 Vds 无关，单独核对
Vgs(th) 1V ~ 4V         是「开始导通」不是「完全导通」
Id      看散热条件       标称值往往偏乐观
Rds(on) 几 mΩ ~ 几 Ω    和 Vgs 强相关
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;体二极管要靠两个 MOS 镜像串联才能抵消，方向画错等于没加。&lt;/li&gt;
&lt;li&gt;仿真模型太理想，要在 G–S 补 100K 电阻才符合实际。&lt;/li&gt;
&lt;li&gt;MOS 有中间态，&lt;code&gt;Vgs &amp;gt; 4.5V&lt;/code&gt; 才是经验上的完全导通点。&lt;/li&gt;
&lt;li&gt;G–S 耐压只有 ±20V 左右，和 Vds 完全无关，这是烧 MOS 最常见的原因。&lt;/li&gt;
&lt;li&gt;击穿第一阶段可恢复，发现电流异常立刻降压能救回来；栅极打穿后电流会突然消失，管子就永久坏了。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>比较器</title><link>https://me.622168.xyz/posts/hardware/25-comparator/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/25-comparator/</guid><description>推挽和开漏的区别、上拉电阻怎么取、输入范围限制，以及颤振为什么要加正反馈。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p68（2.6 分钟）、p69（8.7 分钟）、p70（1.6 分钟）、p71（5.3 分钟）、p72（5.6 分钟）、p73（13.6 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;基本逻辑&lt;/h2&gt;
&lt;p&gt;比较器外形像运放，但三角形符号内有一个「&amp;gt;」号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V+   同相输入端
V−   反相输入端
Vout 输出

V+ &amp;gt; V−  →  Vout 输出高电平
V+ &amp;lt; V−  →  Vout 输出低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真演示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V+ = 3V、V− = 1V  →  高电平 ≈ VCC ≈ 4.8V
V+ = 3V、V− = 4V  →  低电平 ≈ 0.2V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;输出的高电平不是正好 5V、低电平不是正好 0V，实际有几十到几百毫伏的偏差（内部开关的压降）。&lt;/p&gt;
&lt;h2&gt;什么时候要加上拉电阻&lt;/h2&gt;
&lt;h3&gt;两种输出结构&lt;/h3&gt;
&lt;p&gt;推挽是内部两个开关，一个接 VCC、一个接 GND，能主动输出高低电平；OC / OD 开漏只有下管（接 GND），没有接 VCC 的开关，只能拉低，不能拉高。&lt;/p&gt;
&lt;p&gt;推挽工作过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V+ &amp;gt; V−  →  上管闭合  →  输出线 = VCC  →  高电平
V− &amp;gt; V+  →  上管断开、下管闭合  →  输出线与 GND 短路  →  低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;开漏要得到高电平必须加上拉电阻接到 VCC。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开关断开后，上拉电阻与后级输入阻抗分压
例：上拉电阻 1K，后级输入阻抗约 10M
    → 1K 与 10M 分压 ≈ VCC  → 输出高电平 ✓
开关闭合时该线直接对 GND → 输出 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;怎么从数据手册判断&lt;/h3&gt;
&lt;p&gt;LM393 输出只有一个下管，三极管发射极接 GND → 三极管工艺 → OC（开集电极）输出。&lt;/p&gt;
&lt;p&gt;MOS 工艺的，输出接 NMOS 漏极 → OD（开漏）输出。&lt;/p&gt;
&lt;p&gt;有 P 管 + N 管（上管接 VCC、下管接 GND）的是推挽结构，不接上拉电阻也能输出高低电平。&lt;/p&gt;
&lt;p&gt;OC 和 OD 结构一样，只是内部工艺不同，都要加上拉电阻。&lt;/p&gt;
&lt;h3&gt;仿真演示&lt;/h3&gt;
&lt;p&gt;两个比较器并排，&lt;code&gt;V+ = 2V&lt;/code&gt;、&lt;code&gt;V− = 1V&lt;/code&gt;，按逻辑都该输出高电平：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;推挽（红色）    →  输出高电平 ✓
LM393（黑/白）  →  输出低电平 ✗（OC 结构无法主动拉高）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;给 LM393 加上拉电阻接到 5V 后 → 两边都能升到高电平。这就是「比较器输出接上拉电阻」的原因。&lt;/p&gt;
&lt;h2&gt;上拉电阻怎么取&lt;/h2&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;p&gt;信号线上每个电平变化 = 对该信号线寄生电容充放电：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;低→高：VCC 给寄生电容充电
高→低：寄生电容通过开关放电，放完变低电平
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;好处和坏处&lt;/h3&gt;
&lt;p&gt;多一条充电路径 → 等效充电电阻减小 → 上升速度更快。&lt;/p&gt;
&lt;p&gt;开关闭合后即使输出已是 0V，电阻上仍一直有电流 → 增加低电平损耗。&lt;/p&gt;
&lt;h3&gt;取值折中&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电阻越小  →  上升越快、传输速度越快，但电流越大、损耗越大
电阻上的电流 不能超过比较器的最大吸收电流（数据手册的「低电平输出电流」）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数据手册实读：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LM393A / LM393  →  低电平输出电流约 6mA
LM2903          →  约 21mA
LM393 的 B 版本  →  能到比较大的状态
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;取值原则：先满足信号传输速度（上升速度决定最大传输速度），再保证不超过最大吸收电流。&lt;/p&gt;
&lt;p&gt;LM393 只有 6mA，按理说应有几十 mA。实际用时以数据手册为准，别按经验猜。&lt;/p&gt;
&lt;h2&gt;输入信号范围&lt;/h2&gt;
&lt;p&gt;比较器输入信号有范围限制，不是任何值都能正常工作。&lt;/p&gt;
&lt;p&gt;通用原则：输入信号尽量不要超过供电电源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0~5V 单电源供电 → 输入不要低于 0V、不要高于 5V
放宽一点：不要超过一个二极管压降
        → 最低不低于 −0.7V，最高不超过 5.7V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真演示（异常现象）&lt;/h3&gt;
&lt;p&gt;演示 1：V− 经电阻接地（0V），输入一个有效值 2V 的正弦波。理论正半周 &lt;code&gt;V+ &amp;gt; V−&lt;/code&gt; 输出高电平、负半周输出低电平。&lt;/p&gt;
&lt;p&gt;实测异常：负半周切换到低电平后，当输入低于 −0.7V 时低电平维持不住，变回高电平。&lt;/p&gt;
&lt;p&gt;演示 2：改用 1Vpp（±1V）交流源。同样现象：正半周输出高电平 ✓；负半周本应一直是低电平，但负压小到某个值时输出变回高电平。&lt;/p&gt;
&lt;p&gt;原因是输入电压太低，内部工作逻辑不正常（LM393 是三极管结构）。把供电改成 5V 后恢复正常。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;3.3V 供电不要接正 5V 电压进去。&lt;/p&gt;
&lt;p&gt;这个坑在做传感器接口时很常见——传感器输出 5V，运放/比较器供电 3.3V，直接就异常了。&lt;/p&gt;
&lt;h2&gt;颤振与正反馈&lt;/h2&gt;
&lt;h3&gt;现象&lt;/h3&gt;
&lt;p&gt;输入正弦波（0~2V）、参考电压 1V；在高/低电平转换瞬间，输出抖几下才稳定（高→低、低→高都会抖）。&lt;/p&gt;
&lt;h3&gt;原因（多因共同作用）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;实际电路中电源有抖动
参考信号有噪声
输入信号有噪声
比较器的偏置电压（失调电压）会随电源抖动、输出电流突变而受影响
本质：工作过程中 V+ 与 V− 处于不稳定状态，在交叉点附近反复翻转
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决：加正反馈（回差 / 滞回）&lt;/h3&gt;
&lt;p&gt;接法：从输出端加一个电阻接到同相端（参考端）。&lt;/p&gt;
&lt;p&gt;元件取值：参考端加一个小电阻 1K，反馈电阻取 100 倍 = 100K。&lt;/p&gt;
&lt;h3&gt;仿真验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;同相端 1V 基准，反相端正弦波（偏移 1V → 0~2V）
  → 输入 &amp;gt; 1V 输出低，&amp;lt; 1V 输出高，逻辑正确
输入 100Hz，叠加 10mVpp、100kHz 噪声  →  出现颤振
加 100K 正反馈后  →  颤振消失 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;回差窗口原理&lt;/h3&gt;
&lt;p&gt;加正反馈后，基准源会随输出上下跳一个小台阶。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;输出由低变高瞬间，参考点从 1V 跳到 1.1V
→ 让被比较信号尽快逃离震荡区 → 不再反复翻转
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;电阻取值规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;一般取参考端电阻的 100 倍
若噪声很大（如 100mV），100 倍可能不够 → 需取更小
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真对比：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;反馈 100K  →  窗口正常，颤振消失
反馈 10K   →  窗口变小、颤振很小
反馈 5K    →  窗口消失
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;噪声越大，反馈电阻要越接近基准电阻（反馈量越大 → 回差窗口越大）。&lt;/p&gt;
&lt;h3&gt;应用意义&lt;/h3&gt;
&lt;p&gt;比较器电路里常见「一个很大的电阻反馈回参考端」，就是为了解决颤振。很多同学以为可以不加。&lt;/p&gt;
&lt;h2&gt;比较器速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;逻辑      V+ &amp;gt; V− 输出高
输出结构  推挽不用上拉；OC/OD 必须上拉
上拉取值  兼顾速度与功耗，电流不超低电平输出电流
输入范围  不超过电源轨（放宽 ±0.7V）
颤振      加正反馈，反馈电阻 ≈ 基准电阻的 100 倍
阈值      有噪声就要回差，否则临界点会抖
&lt;/code&gt;&lt;/pre&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;输入不能超过供电范围，3.3V 供电不要接 5V 输入。&lt;/li&gt;
&lt;li&gt;临界点附近会颤振，有噪声的场合必须加正反馈，反馈电阻取基准电阻的 100 倍。&lt;/li&gt;
&lt;li&gt;比较器输出高/低电平不等于 VCC/0V，实测约 4.8V / 0.2V。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>运放基础与虚短虚断</title><link>https://me.622168.xyz/posts/hardware/26-opamp-virtual-short-open/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/26-opamp-virtual-short-open/</guid><description>虚短的两个前提条件、虚断为什么成立，以及为什么外部电阻不能取太大。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p74（3.5 分钟）、p75（16.9 分钟）、p76（10.5 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;运放是什么&lt;/h2&gt;
&lt;p&gt;运放 = 运算放大器。&lt;/p&gt;
&lt;p&gt;常见作用：信号放大、滤波、积分、微分、整流；更高级可以直接做电路的主控。&lt;/p&gt;
&lt;h3&gt;项目实例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;直流稳压电源：整机无单片机，全部靠运放，利用负反馈实现整机的自我调节
负载仪：两个运放起主控作用，同时预留 MCU 控制版本（两个方案二选一）
Buck / Boost 开关电源：环路控制
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运放功能非常强大，但学起来复杂。&lt;/p&gt;
&lt;h2&gt;引脚&lt;/h2&gt;
&lt;p&gt;运放 5 个引脚：VCC、GND、V+（同相输入端）、V−（反相输入端）、Vout。&lt;/p&gt;
&lt;h2&gt;虚短&lt;/h2&gt;
&lt;p&gt;虚短定义：运放正常工作过程中，&lt;code&gt;V+ = V−&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;数学解释&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 公式：Vout = (V+ − V−) × 开环放大倍数 Gain
② 开环增益非常大，动辄 10 万倍甚至 100 万倍
③ 移项：Vout / Gain = V+ − V−
④ Vout 一定在电源范围内（如最大 12V），是个较小的值
⑤ 小值 ÷ 10 万级 → ≈ 0
⑥ → V+ − V− ≈ 0  →  V+ = V−
&lt;/code&gt;&lt;/pre&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;V+ &amp;gt; V−  →  输出往上走（往电源端）
V+ &amp;lt; V−  →  输出往下走（往 GND 端）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数字实例（反馈网络 1K + 9K，分压比 1/10）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 上电前所有节点 = 0V
② 给 V+ 加 1V → V+ = 1V，此时 V− 与 Vout 仍 = 0
③ 运放发现 V+ &amp;gt; V− → 输出开始上升
④ Vout 从 0 升到 1V 时：
   V− = Vout × 1K/(1K+9K) = 1/10 → V− 从 0 升到 0.1V
⑤ 继续升：Vout = 5V → V− = 0.5V；Vout = 10V → V− = 1V = V+
⑥ V+ = V− 后输出开始下降；降到 9.99999V → V− = 0.999999V &amp;lt; V+ → 又上升
⑦ 最终自我调节稳定在 Vout ≈ 10V，且 V+ = V−
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段推理是理解运放的钥匙：运放不是「算出来」输出是 10V 的，而是通过不断自我修正，把自己逼到 V+ = V− 那个点上。&lt;/p&gt;
&lt;p&gt;仿真验证：开关闭合瞬间，红色（V+）立刻从 0 跳到 1V；白色（Vout）往上冲；V− 随 Vout 上升逐渐接近 V+；Vout = 10V 时 V− 刚好 = 1V。&lt;/p&gt;
&lt;h3&gt;虚短的两个前提&lt;/h3&gt;
&lt;p&gt;条件一：必须有负反馈回路。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;负反馈定义：反馈路径让 V+ 与 V− 越来越接近
越来越远离的是：正反馈
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;没有负反馈，V+ 绝对不可能等于 V−。&lt;/p&gt;
&lt;p&gt;条件二：输出不能饱和（或截止）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;饱和 = Vout 已到供电边界（0~12V 供电时 Vout = 12V 或 0V）

反例：供电只有 5V，而要让 V− = 1V 需要 Vout = 10V
     → 输出最多 5V → V− 最多 0.5V → 不满足虚短
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以分析电路时，要先确认「有负反馈」+「输出没饱和」，才能用虚短。&lt;/p&gt;
&lt;h2&gt;虚断&lt;/h2&gt;
&lt;p&gt;虚断定义：运放输入端口相对外部电路，可简单认为是一个断路状态。&lt;/p&gt;
&lt;h3&gt;用电阻分压模型理解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5V + 两个 1K 电阻 → 中点 = 2.5V
在中点并联一个远大于它的电阻（如 10M）
→ 并联后总阻值仍 ≈ 1K，中点仍 ≈ 2.5V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 10M 电阻加与不加，对电路没有影响，可当作断路；但它又真实存在 → 称为虚断（不是真断路）。&lt;/p&gt;
&lt;h3&gt;对应到运放&lt;/h3&gt;
&lt;p&gt;运放输入端可认为是一个非常非常大的电阻（举例约 100M）。&lt;/p&gt;
&lt;p&gt;同相放大电路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vin = 1V 时 V+ ≈ 1V，两点电压相等 → 无电流流入 → V+ 对外部就是断路
反相端：Vout 经 9K → 1K → GND
       V− 处再并一个 100M 输入阻抗 → 可忽略
       → V− 只由 9K/1K 分压决定
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;电流角度&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;外部网络总电阻 10K、电压 10V → 总电流 1mA
并上 100M 只分流纳安甚至皮安级
→ 相对 1mA 可忽略
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;成立条件&lt;/h3&gt;
&lt;p&gt;外部电阻网络的电流 &amp;gt;&amp;gt; 运放输入电流。&lt;/p&gt;
&lt;p&gt;运放关键参数 IB（偏置电流）为纳安级；外部网络一般是毫安级 → 天然满足。&lt;/p&gt;
&lt;h3&gt;为什么外部电阻不能取太大&lt;/h3&gt;
&lt;p&gt;同相放大：&lt;code&gt;Vout = Vin × (1 + R2/R1)&lt;/code&gt;，9K/1K 可以换成 90K/10K、9M/1M，比值都是 10。&lt;/p&gt;
&lt;p&gt;但若取 9M，就不再满足虚断 → Vout 不准。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Vout = Vin × (1 + 9K/1K) + IB × 9K
                        ↑ 这一项变得不能忽略
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实例：LM317（LDO 稳压芯片）的公式最后要多加一项，就是因为其内部基准运放的偏置电流太大，不能忽略。&lt;/p&gt;
&lt;p&gt;结论：虚断成立条件 = 外部电阻网络电流 &amp;gt;&amp;gt; 运放输入电流。&lt;/p&gt;
&lt;h2&gt;分析运放电路的标准流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;① 判断有没有负反馈（有 → 可用虚短）
② 判断外部电阻是否 K 级、IB 是否远小于网络电流（是 → 可用虚断）
③ 用虚断把电路化简成电阻分压网络
④ 用分压公式列方程
⑤ 用虚短联立求解
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;课本用叠加定理分析太复杂，不适合基础差的同学；用虚短虚断 + 欧姆定律即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;只要懂欧姆定律，这门课 80% 的知识点都能掌握。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;没有负反馈就不能用虚短；输出饱和时也不能用虚短。&lt;/li&gt;
&lt;li&gt;判断负反馈看的是「V+ 和 V− 是否越来越接近」，不是看反馈线接在哪里。&lt;/li&gt;
&lt;li&gt;虚断不是真断路，只是输入电流相对外部网络电流可以忽略。&lt;/li&gt;
&lt;li&gt;外部电阻不能取太大，否则 IB 产生的压降会破坏虚断；偏置电流大的运放（如 LM317 内部的）必须把 IB 项算进去。&lt;/li&gt;
&lt;li&gt;运放会「自我调节」到 V+ = V−，不是算出来的，理解这个过程比记公式重要。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电流采样电路</title><link>https://me.622168.xyz/posts/hardware/28-current-sensing/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/28-current-sensing/</guid><description>低端、高端、仪表放大器、跨阻放大——四种电流采样方案的取舍，以及 VOS 怎么校准。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p81（16.7 分钟）、p82（16.7 分钟）、p83（6.4 分钟）、p84（13 分钟）、p85（7.3 分钟）、p86（7.3 分钟）、p87（8.2 分钟）、p88（6.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;为什么需要电流采样&lt;/h2&gt;
&lt;p&gt;单片机只能读电压，不能直接读电流。采样的本质就是「把电流转成电压，再给 ADC」。&lt;/p&gt;
&lt;h2&gt;低端电流采样&lt;/h2&gt;
&lt;h3&gt;思路&lt;/h3&gt;
&lt;p&gt;在负载上串联一个小电阻（采样电阻），采样电阻必须远小于负载。&lt;/p&gt;
&lt;h3&gt;数值示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;负载 5Ω、供电 5V  →  工作电流 = 5V / 5Ω = 1A
串入 10mΩ 采样电阻  →  1A × 10mΩ = 10mV 压降
用放大电路放大 10 倍  →  100mV 给 ADC
MCU：采集电压 ÷ 放大倍数 ÷ 采样电阻 → 反推负载电流
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;采样电阻与负载串联，所以负载电流同样流过采样电阻。&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 判断：Vout 经电阻网络回反相端 → 有负反馈 ✓；最大电阻 9K → 虚断 ✓
② 用虚断：V+ = VR（VR 为采样电阻两端电压）
③ 简化反相端：Vout 经 9K → 1K → GND，V− 为中点
   V− = Vout × 1K/(1K+9K) = Vout × 1/10
④ 用虚短 V+ = V−：
   VR = Vout / 10  →  Vout = 10 × VR
⑤ 又 VR = I × 10mΩ  →  Vout = 10 × I × 10mΩ
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;I = 998mA，10mΩ 上电压 9.98mV，放大 10 倍 = 99.8mV  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;单端（低端）检测原理简单。&lt;/p&gt;
&lt;h2&gt;低端采样的三个坑&lt;/h2&gt;
&lt;h3&gt;问题暴露&lt;/h3&gt;
&lt;p&gt;理想运放输出 100mV ✓；实际运放 LM358 输出 113mV ✗。给 ADC 采集结果必然异常。&lt;/p&gt;
&lt;h3&gt;坑一：输入/输出范围非理想&lt;/h3&gt;
&lt;p&gt;所有实际运放输入范围不能超过电源供电范围（如单电源 0~5V）。&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;接近 0~5V&lt;/td&gt;
&lt;td&gt;接近 0~5V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;非轨对轨运放&lt;/td&gt;
&lt;td&gt;可能只有 0~4.3V&lt;/td&gt;
&lt;td&gt;0.3~3.5V（VCC − 1.5）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;低端采样的问题：采样电压只有 10mV，非常接近底部电压轨（0V），非轨对轨运放可能采不到。&lt;/p&gt;
&lt;h3&gt;坑二：VOS（输入失调电压）&lt;/h3&gt;
&lt;p&gt;VOS 可简单理解为运放自带的一个输入信号。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：VOS = 1mV
→ 即使外部不输入任何信号，运放自身输入已有 1mV
→ 经 10 倍放大后变成 10mV 误差
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实验：断开 5V 让输入 ≈ 0 → 测得默认输出 = 68.4mV。&lt;/p&gt;
&lt;p&gt;来源：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;VOS 贡献默认电压
偏置电流 IB 流过外部电阻也贡献一个误差信号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LM358 的 VOS 很大（毫伏级），而输入信号才 10mV，误差很大。&lt;/p&gt;
&lt;h3&gt;解决方法一：加负电源&lt;/h3&gt;
&lt;p&gt;把运放的 GND 端不接 GND，改接 −5V → 输入信号就落在电源轨以内。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;断开负载  →  输出 −317µV（≈0，很接近零）✓
接上负载  →  输出 99.5mV（很接近 100mV）✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方法二：自校准算法（推荐）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 断开负载，采集运放默认输出电压 = −317µV ≈ −0.3mV，存入单片机作为初始值
② 接上负载，再测一次 = 99.5mV
③ 求变化量：ΔV = 99.5 − (−0.3) = 99.8mV
   与理论 9.98mV × 10 = 99.8mV 完全一致 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原理：VOS 在同一温度、相同工作条件下变化很小 → 可校准。&lt;/p&gt;
&lt;p&gt;这是工程上最常用的办法——不需要额外硬件，只要软件里减掉零点。&lt;/p&gt;
&lt;h3&gt;解决方法三：单电源 + 偏置网络&lt;/h3&gt;
&lt;p&gt;负电源的缺点：很多 MCU 的 ADC 不能采负压；且负电源要额外加开关电源 → 成本升高。&lt;/p&gt;
&lt;p&gt;做法：保持单电源，加一个 1V 偏置网络。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;两个分压电阻改成 10K / 10K
注意：加了偏置网络后放大倍数会变，要实现 10 倍需把原来的 9K 改成 1K
再接一个 1V 电源
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;断开负载  →  999mV ≈ 1V ✓
接上负载  →  1V + 10mV × 10 = 1.1V（实测 1.1V）✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;1V 的产生方式：基准芯片 / LDO / 运放加跟随器均可。&lt;/p&gt;
&lt;h3&gt;低端采样注意点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 采样电阻上的电压是否在运放输入范围内
② 运放非理想参数（VOS、IB）是否会影响检测结果
③ 解决手段：自校准算法 或 加偏置网络
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高端电流采样&lt;/h2&gt;
&lt;p&gt;高端采样可简单认为就是一个差分放大电路。&lt;/p&gt;
&lt;p&gt;采样电阻两端为 V1、V2：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V1 − V2 = IL × 10mΩ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;V1、V2 即差分放大电路的输入信号。&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 判断：有负反馈 → 虚短 ✓；外部电阻 K 级 → 虚断 ✓
② V+ 回路：V1 经 1K → V+ → 10K → GND
   V+ = V1 × 10K/(10K+1K)
③ V− 回路：Vout 经 10K → 1K → V2
   V− = (Vout − V2) × 1K/(10K+1K) + V2
④ 用虚短联立（引用差分放大电路的推导结论）
⑤ 最终：
   Vout = (10K/1K) × (V1 − V2)
⑥ 代入 V1 − V2 = IL × 10mΩ：
   Vout = 10 × IL × 10mΩ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真：&lt;code&gt;I = 998mA&lt;/code&gt;，&lt;code&gt;10mΩ × 998mA = 9.98mV&lt;/code&gt;，&lt;code&gt;×10 = 99.8mV&lt;/code&gt; ✓&lt;/p&gt;
&lt;h2&gt;为什么要换成仪表放大器&lt;/h2&gt;
&lt;h3&gt;差分放大电路的致命缺陷：输入阻抗太低&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电路里 1K、10K 电阻上会有电流流过（可能有进有出）
→ 会影响负载工作电流
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;负载电流 1A 时影响较小；负载 10mA、工作电压 10V 时，放大电路里流过的电流已接近 1mA → 明显影响负载 ✗。&lt;/p&gt;
&lt;p&gt;改进思路：加大放大电路的输入阻抗 → 换成仪表放大器结构。&lt;/p&gt;
&lt;p&gt;运放输入端阻抗极大 → 这一侧电流 ≈ 0 → 对主负载电路几乎无影响。&lt;/p&gt;
&lt;h3&gt;仪表放大器结构&lt;/h3&gt;
&lt;p&gt;节点命名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V1、V2：采样电阻两端
V3、V4：前级两个运放的输出
后级：一个差分电路
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;前级推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 判断：三处都有负反馈 → 虚短都满足 ✓；电阻都很小 → 虚断满足 ✓
② 用虚短：第一个运放反相端电压 = V1；第二个运放反相端电压 = V2
③ 用虚断简化电路：
   V3 → 50K(R) → 50K(RW) → 50K(R) → V4
   两端节点为 V1、V2
   （RW 在实际仪表放大器中一般是可调电阻；两侧的 R 是固定电阻）
④ 简化后是串联电路 → 电流相等：
   I = (V1 − V2) / RW           …(式1)
   I = (V3 − V4) / (2R + RW)    …(式2)
⑤ 联立：
   (V1 − V2)/RW = (V3 − V4)/(2R + RW)
⑥ 得：
   V3 − V4 = (2R/RW + 1) × (V1 − V2)
⑦ 后级差分电路（本电路全部 50K）为 1 倍放大：
   Vout = (V3 − V4) + VREF
⑧ 总公式：
   Vout = (2R/RW + 1) × (V1 − V2) + VREF
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数值验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1A × 10mΩ = 10mV（即 V1 − V2）
R = 50K、RW = 50K → 2R/RW + 1 = 3  →  3 × 10mV = 30mV
VREF = 1V  →  Vout = 1.03V（实测 1.02999999V）✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;改 RW = 25K → &lt;code&gt;2R/RW = 5&lt;/code&gt; → 倍数 5 → 理论 1.05V。&lt;/p&gt;
&lt;p&gt;但实测 1.498888V，跟理论 1.05V 对不上（预期和仿真结果存在矛盾）。遇到这种情况要回到公式重新核对，别盲信任何一方。&lt;/p&gt;
&lt;h3&gt;后级带偏置的差分电路推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 用虚断简化 V+ 回路：VREF → 50K → V+ → 50K → V3
   V+ = (VREF − V3) × 50K/(50K+50K) + V3
② 用虚断简化 V− 回路：Vout → 50K → V− → 50K → V4
   V− = (Vout − V4) × 50K/100K + V4
③ 系数 50K/100K = 1/2（两个都是 1/2）
④ 用虚短联立，两边同乘 2：
   (VREF − V3) + 2V3 = (Vout − V4) + 2V4
⑤ 合并同类项：VREF + V3 = Vout + V4
⑥ 得：
   Vout = VREF + (V3 − V4)
⑦ 推广形式（把 50K 记作 R1、R2）：
   Vout = VREF + (V3 − V4) × (R1/R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：保持电路不变输出 1.049V；把两个 50K 电阻同时改成 100K → 输出翻倍，理论 ≈ 1.1V（实测 1.09997…V）✓&lt;/p&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;注意一：输入/输出范围&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;低端&lt;/td&gt;
&lt;td&gt;非常小（几 mV）&lt;/td&gt;
&lt;td&gt;接近底部电压轨（0V）&lt;/td&gt;
&lt;td&gt;加偏置网络&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高端&lt;/td&gt;
&lt;td&gt;接近电源轨（如 5V、4.999V）&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;若电源本来就在供电范围内（如采样点 3.3V、运放供电 5V）→ 不需要管。&lt;/p&gt;
&lt;h3&gt;高压场景的处理&lt;/h3&gt;
&lt;p&gt;工作电压 100V、运放供电才 5V：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在两处各加电阻分压，例如 99K + 1K → 分压 100 倍
100V → 分压后 1V
99.999V → 分压后约 0.999999V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;坑 1：分压电阻一定要取大一点，否则输入阻抗变小，会影响源端负载。&lt;/p&gt;
&lt;p&gt;坑 2：100 倍分压会让差分信号 ΔV 也衰减 100 倍，必须靠后级放大补回来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：第一级放大 10 倍、第二级再放大 30 倍或 4 倍
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;注意二：VOS&lt;/h3&gt;
&lt;p&gt;仪表放大器的 VOS 一般都比较好，不是难解决的问题。若不够好，用前面的校准方式解决。&lt;/p&gt;
&lt;p&gt;其他坑：运放选型时还要考虑共模抑制比等（脱离项目不易展开）。&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;电流很小时，前面方案要求电路输入阻抗非常大，有难度。&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;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 判断：有负反馈 → 虚短 ✓；电阻 K 级 → 虚断 ✓
② 用虚短：V+ = V− = VREF（V+ 一般接偏置 VREF）
③ 用虚断简化：Vout → 电阻 R（跨阻）→ 电流源
④ 电阻与电流源串联 → 电阻上电流 = 电流源电流 I
⑤ 得：
   Vout = VREF + I × R
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数值&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;电流 I&lt;/th&gt;
&lt;th&gt;所需 R&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1mA&lt;/td&gt;
&lt;td&gt;1K&lt;/td&gt;
&lt;td&gt;Vout = 1V ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1µA&lt;/td&gt;
&lt;td&gt;1M&lt;/td&gt;
&lt;td&gt;Vout = 1V ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1nA&lt;/td&gt;
&lt;td&gt;1GΩ&lt;/td&gt;
&lt;td&gt;Vout = 999mV（略小于 1V）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;1nA 时小于 1V 的原因：受运放偏置电流影响。&lt;/p&gt;
&lt;h3&gt;工程坑&lt;/h3&gt;
&lt;p&gt;1GΩ 电阻实际购买很夸张 → 可用 T 型（三角形）等效网络，用三个较小电阻等效出大电阻。&lt;/p&gt;
&lt;p&gt;跨阻放大可把微弱电流转成电压，后级给 MCU 采集。&lt;/p&gt;
&lt;h2&gt;输入电阻匹配&lt;/h2&gt;
&lt;h3&gt;常见结论&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;同相端电阻 R1 = 反相端 R2 与 R3 的并联值
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;原因：偏置电流 IB 会在电阻上产生压降&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;反相端：IB 在 R2、R3 上产生电压 V1
同相端：若不加电阻（直接短路），IB 直接流到 GND → V2 = 0
阻抗不匹配 → IB 产生一个多余的偏差电压 ΔV → 影响输出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决办法：同相端也加一个电阻，且等于反相端两电阻的并联值 → 使 &lt;code&gt;V2 ≈ V1&lt;/code&gt; → 相减抵消。&lt;/p&gt;
&lt;h3&gt;仿真验证（理论输出应为 0）&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;同相端加 1K 匹配电阻&lt;/td&gt;
&lt;td&gt;−47.43µV（大部分由 VOS 导致）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;去掉该匹配电阻&lt;/td&gt;
&lt;td&gt;87µV（变化不大，只有 µV 级）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;两个电阻改成 1M&lt;/td&gt;
&lt;td&gt;−20mV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;两个电阻改成 100M&lt;/td&gt;
&lt;td&gt;−1.9V（IB 产生的电压非常大）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;100M 条件下同相端加 50M 匹配&lt;/td&gt;
&lt;td&gt;−4.6mV（大幅抵消）✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;电阻越大，IB 产生的偏差电压越大 → 越需要匹配。&lt;/p&gt;
&lt;p&gt;残留几 mV 的原因：V+ 与 V− 的偏置电流本身有差异，这个差异称为失调电流。&lt;/p&gt;
&lt;p&gt;同相端/反相端做电阻匹配，主要目的就是消除偏置电流的影响。&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;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;td&gt;破坏负载接地、VOS 影响大&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;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;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;能测 nA 级&lt;/td&gt;
&lt;td&gt;需要大电阻&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;采样电阻要远小于负载，否则会改变电路工作状态；低端采样信号接近 0V，非轨对轨运放可能采不到。&lt;/li&gt;
&lt;li&gt;VOS 会被一起放大（1mV × 10 倍 = 10mV 误差），LM358 尤其明显。&lt;/li&gt;
&lt;li&gt;偏置网络会改变放大倍数，加偏置后要重新调电阻。&lt;/li&gt;
&lt;li&gt;高压场景分压电阻要取大，而且分压会让差分信号一起衰减，后级得补回放大倍数。&lt;/li&gt;
&lt;li&gt;电阻匹配取并联值，电阻越大越需要匹配（100M 时误差能到 1.9V）。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>四种放大电路推导</title><link>https://me.622168.xyz/posts/hardware/27-opamp-amplifier-circuits/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/27-opamp-amplifier-circuits/</guid><description>同相、反相、单电源反相、差分——用虚短虚断加欧姆定律全部推一遍。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p77（7.3 分钟）、p78（5.1 分钟）、p79（10 分钟）、p80（10.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这四种电路都不用课本的叠加定理，统一拿「虚短 + 虚断 + 分压公式」推。&lt;/p&gt;
&lt;h2&gt;统一的推导流程&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;① 判断虚短：先看有无负反馈回路（饱和问题在初期分析时先不管）
② 判断虚断：看外部电阻是否 K 级、偏置电流是否远小于网络电流
③ 用虚断把电路化简成电阻分压
④ 用分压公式列方程
⑤ 用虚短联立
⑥ 化简
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;同相放大&lt;/h2&gt;
&lt;h3&gt;电路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;         ┌──[ R1 = 9K ]──┐
         │               │
Vin ──[1K]── V+         V− 
                             │
                            Vout
         GND ←──── R2 = 1K ──┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（&lt;code&gt;Vout&lt;/code&gt; 经 R1 到 V−，V− 再经 R2 到 GND；Vin 经电阻到 V+）&lt;/p&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 用虚断：Vin = V+                                    …(式1)

② 用虚断简化输出侧：Vout 经 9K(R1) → 1K(R2) → GND，V− 为中点
   V− = Vout × R2/(R1+R2)                             …(式2)

③ 用虚短 V+ = V−，把 (式1) 代入 (式2)：
   Vin = Vout × R2/(R1+R2)

④ 移项：
   Vout = Vin × (1 + R1/R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vout = Vin × (1 + R1/R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代入 &lt;code&gt;R1 = 9K&lt;/code&gt;、&lt;code&gt;R2 = 1K&lt;/code&gt; → &lt;code&gt;9/1 = 9&lt;/code&gt;，&lt;code&gt;9 + 1 = 10&lt;/code&gt; → 10 倍同相放大。&lt;/p&gt;
&lt;p&gt;仿真：&lt;code&gt;Vin = 1V&lt;/code&gt; → &lt;code&gt;Vout = 10V&lt;/code&gt;（实测 10.002V，2mV 偏差来自 VOS）。&lt;/p&gt;
&lt;h3&gt;注意公式形式&lt;/h3&gt;
&lt;p&gt;课本上常写成 &lt;code&gt;Vout = Vin × (1 + Rf/Rg)&lt;/code&gt;，其中 &lt;code&gt;Rf&lt;/code&gt; 是反馈电阻、&lt;code&gt;Rg&lt;/code&gt; 是接地电阻。&lt;/p&gt;
&lt;p&gt;对应关系：这里的 &lt;code&gt;R1&lt;/code&gt; 是反馈电阻（9K），&lt;code&gt;R2&lt;/code&gt; 是接地电阻（1K）。别把两个电阻记反了。&lt;/p&gt;
&lt;h2&gt;反相放大&lt;/h2&gt;
&lt;h3&gt;先澄清一个判断标准&lt;/h3&gt;
&lt;p&gt;不是所有运放电路的负反馈路径都从输出到 V−，有些电路经器件后把反馈接到 V+。&lt;/p&gt;
&lt;p&gt;判据：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;只要反馈最终让 V+ 与 V− 相互靠近  →  负反馈
让它们远离                        →  正反馈
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前期分析只管有没有负反馈回路，不必管饱和。&lt;/p&gt;
&lt;h3&gt;电路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vin ──[ R1 = 1K ]── V−
                    │
                 [ R2 = 10K ]
                    │
                  Vout

        V+ ──[ 电阻 ]── GND
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 用虚断：V+ 经电阻到 GND → V+ = 0

② 用虚断简化：Vout 经 10K(R2) → V− → 1K(R1) → Vin

③ 分压公式（从 Vin 端起算）：
   V− = (Vout − Vin) × R1/(R1+R2) + Vin

④ 用虚短 V+ = V− = 0：
   (Vout − Vin) × R1/(R1+R2) + Vin = 0

⑤ 化简（R1、R2 同乘约掉，Vin 项相消）：
   Vout = −(R2/R1) × Vin
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vout = −(R2/R1) × Vin
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代入 &lt;code&gt;R2 = 10K&lt;/code&gt;、&lt;code&gt;R1 = 1K&lt;/code&gt; → 反相放大 10 倍。&lt;/p&gt;
&lt;p&gt;「反相」的意思就是那个负号——输入正电压，输出负电压。&lt;/p&gt;
&lt;h3&gt;反相放大的三个坑&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;输入阻抗等于 R1（这里是 1K），比较低。前级驱动能力不足时会拉低信号。&lt;/li&gt;
&lt;li&gt;输出是负的，需要双电源（正负电源）才能输出负电压。&lt;/li&gt;
&lt;li&gt;V+ 端要接电阻到 GND（后面 p88 讲电阻匹配时会说为什么）。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;单电源反相放大&lt;/h2&gt;
&lt;h3&gt;问题引入&lt;/h3&gt;
&lt;p&gt;双电源反相放大：输入 +1V，反相放大 10 倍 → 输出 −10V，必须用 −12V 供电才能输出（实测 −9.9999V）。&lt;/p&gt;
&lt;p&gt;改成单电源供电：输出一直 ≈ 0（实测约 4.0 几 mV）→ 输出非常容易失真。&lt;/p&gt;
&lt;p&gt;原因：单电源只能输出 0V 以上，负的输出根本出不来。&lt;/p&gt;
&lt;h3&gt;解决方法：加偏置网络&lt;/h3&gt;
&lt;p&gt;让默认输出不是 0 而是偏置电压。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例：加 5V 偏置 → 默认输出 = 5V
   信号从 5V 上往下变化 → 不失真 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 判断：有负反馈 → 虚短 ✓；外部电阻 K 级 → 虚断 ✓

② 用虚断简化 V+ 回路：5V 经 10K(R2) → 1K(R1) → GND，V+ 为中点
   V+ = 5V × R1/(R1+R2)                               …(式1)

③ 用虚断简化 V− 回路：Vout 经 10K(R2) → 1K(R1) → Vin
   V− = (Vout − Vin) × R1/(R1+R2) + Vin                …(式2)

④ 用虚短 V+ = V− 联立：
   5 × R1/(R1+R2) = (Vout − Vin) × R1/(R1+R2) + Vin

⑤ 两边同乘 (R1+R2)/R1：
   5 = Vout − Vin + Vin × (R1+R2)/R1

⑥ 展开 Vin × (R1+R2)/R1 = Vin + Vin × R2/R1 → Vin 项相消

⑦ 得：
   Vout = 5 − Vin × (R2/R1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vout = 5 − Vin × (R2/R1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意那个 5：就是偏置电压。偏置电压直接出现在公式的常数项里。&lt;/p&gt;
&lt;h3&gt;数值验证（R2 = 10K、R1 = 1K，放大 10 倍）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vin = 0V    → 计算 5V           实测 5V
Vin = +0.1V → 计算 5 − 1 = 4V   实测 4.003V
Vin = −0.1V → 计算 5 − (−1) = 6V 实测 6.003V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;0.003V 偏差来自 VOS，可忽略。&lt;/p&gt;
&lt;p&gt;关键的一点：输出在 5V 上下摆动，单电源 0~12V 就能覆盖 4~6V 的范围，不再需要负电源。&lt;/p&gt;
&lt;h2&gt;差分放大&lt;/h2&gt;
&lt;p&gt;先化简 V+ 回路，因为运放很多时候是由 V+ 去决定 V− 的。&lt;/p&gt;
&lt;h3&gt;电路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vin− ──[ R1 = 1K ]── V+
                      │
                   [ R2 = 10K ]
                      │
                     GND

Vin+ ──[ R1 = 1K ]── V−
                      │
                   [ R2 = 10K ]
                      │
                    Vout
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① V+ 回路：Vin− 经 1K(R1) → 10K(R2) → GND
   V+ = Vin− × R2/(R1+R2)                              …(式1)

② V− 回路：Vout 经 R2 → R1 → Vin+
   V− = (Vout − Vin+) × R1/(R1+R2) + Vin+              …(式2)

③ 用虚短联立，两边同乘 (R1+R2)：
   Vin− × R2 = (Vout − Vin+) × R1 + Vin+ × (R1+R2)

④ 两边除以 R1：
   Vin− × R2/R1 = Vout − Vin+ + Vin+ × (R1+R2)/R1

⑤ 展开 Vin+ × (R1+R2)/R1 = Vin+ + Vin+ × R2/R1 → Vin+ 项相消

⑥ 得：
   Vout = (Vin− − Vin+) × (R2/R1)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;那个负号是怎么来的&lt;/h3&gt;
&lt;p&gt;推完发现结果带负号。反复检查确认没画错，一度怀疑是自己记错了，于是用仿真验证：差分信号 &lt;code&gt;Vin+ − Vin− = 0.1V&lt;/code&gt;，×10 后输出 −1V（实测 −1V）。&lt;/p&gt;
&lt;p&gt;最后找到原因：电路接反了。把差分放大电路的正端接到了运放反相端、负端接到了同相端，所以出现负号。&lt;/p&gt;
&lt;h3&gt;正确接法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;同相端（V+）接差分信号的正端
反相端（V−）接差分信号的负端
→ 不会出现负号
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;结果&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vout = (Vin+ − Vin−) × (R2/R1)
&lt;/code&gt;&lt;/pre&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;同相放大&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Vout = Vin × (1 + R1/R2)&lt;/code&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;code&gt;Vout = −(R2/R1) × Vin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;输入阻抗 = R1（低），反相&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;单电源反相&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Vout = Vbias − Vin × (R2/R1)&lt;/code&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;code&gt;Vout = (Vin+ − Vin−) × (R2/R1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;放大两路之差，抑制共模&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;记忆要点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;同相：1 + 比值      （分母是接地电阻）
反相：负的比值      （分子是反馈电阻）
差分：两路之差 × 比值
单电源：常数项 = 偏置电压
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同相是 1+，反相是负号，差分是相减——这三个特征记住了，公式就不会混。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;同相公式里那个 1 不能漏。&lt;/li&gt;
&lt;li&gt;单电源反相公式的常数项就是偏置电压，别当成零。&lt;/li&gt;
&lt;li&gt;差分放大的两个分压网络要对称（R1 对 R1、R2 对 R2），否则共模抑制会变差。&lt;/li&gt;
&lt;li&gt;仿真差 2~3mV 来自运放的 VOS，正常。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>恒流源电路</title><link>https://me.622168.xyz/posts/hardware/29-constant-current-source/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/29-constant-current-source/</guid><description>恒流源怎么让电流不随负载变化、两个限制因素、自激振荡怎么解决，以及高端反馈的两种方案。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p89（11 分钟）、p90（6.2 分钟）、p91（9.9 分钟）、p92（13.1 分钟）、p93（8.3 分钟）、p94（11 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;低端反馈型恒流源&lt;/h2&gt;
&lt;p&gt;电路是运放 + NPN 三极管。&lt;/p&gt;
&lt;h3&gt;推导链条&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 回路电流 I 不好直接求 → 改为先求采样电阻上的电压 V，再 I = V / R
② 虚断 → V正 = Vin
③ 虚短 → V负 = V正 = Vin
④ 忽略三极管那一条支路（虚断下只需把分压部分简化）：
   V负 = V × R2/(R1+R2)
⑤ 整理得：
   V = Vin × (1 + R1/R2)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vin = 1V，R1/R2 = 1  →  V = 2V
采样电阻取 4Ω  →  I = 2V / 4Ω = 0.5A
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;重要前提&lt;/h3&gt;
&lt;p&gt;分压电阻 R1、R2 取值必须足够大（实际电路建议各取 100K），否则分压支路自身电流不能忽略，会引入误差。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;反馈电阻取值小  →  仿真电流 501mA
反馈电阻取 100K →  仿真电流正好 500mA ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;p&gt;负载换成 1Ω / 2Ω / 5Ω，回路电流始终 ≈ 500mA ✓&lt;/p&gt;
&lt;h3&gt;恒流源和恒压源的区别&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;恒流源：电流恒定；电压随负载变化（负载电阻越大，输出电压越大）
恒压源：电压恒定；电流随负载变化
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用输出电压变化来证明，以集电极为输出点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;负载 5Ω  →  输出 4.5V
负载 1Ω  →  输出 2.5V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;两者特性刚好相反。&lt;/p&gt;
&lt;h2&gt;限制一：电压源幅值&lt;/h2&gt;
&lt;p&gt;恒流源的电流恒定是有条件的——它的能量最终仍来自一个电压源（本例 12V），电压源的幅值决定了恒流源能带的最大负载。&lt;/p&gt;
&lt;h3&gt;负载上限推导（以 1A 恒流为例）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电流采样/反馈电阻 4Ω，1A 下压降 4V
电源 12V  →  最大总电阻 12Ω  →  最大负载 8Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;计入运放与三极管后限制更严格：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;运放若也是 12V 供电，最大输出 12V
三极管有 Vbe 压降（≈0.7V）→ 基极/反馈节点最高只能到 11.3V
于是 1A 时负载最大不能超过 7.3Ω
（11.3V − 4V = 7.3V → 7.3Ω）
超过后运放输出已达上限（饱和），电流只能往下降
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;负载 2Ω  →  999.99mA ≈ 1A ✓
负载 5Ω  →  仍 ≈ 1A，节点电压 7V（未超 11.3V）✓
负载 10Ω →  电流达不到 1A，节点电压 11.1V，运放输出已到 12.1V（饱和）✗
把运放和电源都改成 24V  →  电流又能达到 1A ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电压源的最大电压幅值决定恒流源能带的最大负载。&lt;/p&gt;
&lt;h2&gt;限制二：运放最大输出电流&lt;/h2&gt;
&lt;p&gt;三极管是电流控制电流器件：&lt;code&gt;Ic = β × Ib&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;若 β = 100、要求 Ic ≈ 1A  →  Ib 至少要 10mA
若要求输出 10A           →  Ib 至少要 0.1A
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键结论：大部分运放最大输出电流只有 50mA 左右，专门做信号放大的甚至只有十几毫安。&lt;/p&gt;
&lt;h3&gt;仿真验证（LM358，24V 供电）&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1A&lt;/td&gt;
&lt;td&gt;正常 ✓&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;采样电阻减小十倍想得到 10A&lt;/td&gt;
&lt;td&gt;最大只有 2A&lt;/td&gt;
&lt;td&gt;19.3mA（已饱和）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;负载改 1Ω&lt;/td&gt;
&lt;td&gt;约 6A 多&lt;/td&gt;
&lt;td&gt;40.6mA（LM358 的最大输出电流）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;换回理想运放&lt;/td&gt;
&lt;td&gt;达到 10A&lt;/td&gt;
&lt;td&gt;刚好 101mA（与 β=100、Ic=10A 吻合）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;是运放的最大输出电流把 Ic 限死了。&lt;/p&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;运放先控制第一个三极管
再用它的集电极电流去驱动第二个三极管的基极
效果：末级需 100mA 时，经一级 β 放大后运放只需约 12mA
仿真中运放输出「连 1mA 都不到」，电流压力全部由三极管承担 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;高端反馈恒流源&lt;/h2&gt;
&lt;p&gt;与前面电路最大不同：反馈不是接运放反向端，而是接到同相端（V正）。&lt;/p&gt;
&lt;h3&gt;通用的判断方法&lt;/h3&gt;
&lt;p&gt;不靠「反馈接哪一端」来判断正负反馈：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;V正 &amp;gt; V负  →  Vout 往上走
V正 ≤ V负  →  Vout 往下走

负反馈最终会稳定到 V正 = V负
正反馈则永远不相等，输出只会停在 Vcc 或 GND
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;电路结构&lt;/h3&gt;
&lt;p&gt;运放输出控制一个 N 管，集电极经负载电阻接 12V，反馈点取自三极管上方节点，反馈到运放同相端。&lt;/p&gt;
&lt;h3&gt;上电过程逐段分析（假设 Vref 后到）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 初始各点 0V
② 12V 先接入、Vref 还没来 → 反馈点被拉到 12V，V正 &amp;gt; V负 → Vout 上升
③ Vout 到 0.6V → 三极管导通 → 有电流下来 → 该点电压下降
④ 但 Vout 一直升，三极管饱和导通（饱和压降约 0.3V）
   → 该点最低只有 12V 经两个电阻分压 ≈ 6V
   → Vref 不输入时稳态在 6V 左右，回路有大电流
⑤ 突然 Vref 从 0V 跳到 11V → V负 &amp;gt; V正 → Vout 往下掉
   → Ib 减小 → Ic = β·Ib 减小 → 该点电压 Vr = 12V − 1Ω × Ic 回升
⑥ 该点升到 11V 时 V正 = V负
   运放会往下掉一点点 → 该点变成 11.001V → V正 又大于 V负
   → Vout 再往上一点点 → 反复微调，最终 V正 ≈ 11V，形成虚短
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;输出电流公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Ic = (12V − Vr) / 1Ω
Vr = Vref = 11V  →  I = (12 − 11) / 1 = 1A
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;未闭合开关（Vref 未加）→ 该点约 0V（因有下拉电阻），运放输出冲到最大，
                        三极管饱和，回路电流约 6A（读到 5.63A）
闭合开关  →  两点电压立刻变 11V，电流 1A ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真归仿真，实际电路里运放输出要串一个限流电阻。&lt;/p&gt;
&lt;h3&gt;示波器观察（三通道）&lt;/h3&gt;
&lt;p&gt;通道对应：橙色 = Vref、白色 = 反馈点、绿色 = 运放输出。&lt;/p&gt;
&lt;p&gt;闭合瞬间：橙色立刻从 0 跳到 11V，绿色往下掉，白色慢慢上升，到 11V 后平稳——即运放自我调节过程。&lt;/p&gt;
&lt;p&gt;闭合瞬间运放输出上会出现一个正弦波（自激振荡）。&lt;/p&gt;
&lt;h2&gt;自激振荡怎么解决&lt;/h2&gt;
&lt;h3&gt;原因&lt;/h3&gt;
&lt;p&gt;运放的控制速度比反馈速度快太多，反馈跟不上运放速度就会振荡。&lt;/p&gt;
&lt;p&gt;书本结论：当增益大于 1、且反馈相位延时达到 180° 时就会振荡。&lt;/p&gt;
&lt;h3&gt;两个解决维度&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 降低运放速度（让反馈跟得上）
② 加快反馈速度
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;方法一：在运放反馈路径加积分电容&lt;/h3&gt;
&lt;p&gt;作用是把运放带宽压下来，降低输出控制速度，反馈就能跟上 → 不振荡。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;增益带宽积 100MHz  →  会振荡
增益带宽积 10MHz   →  不振荡（但起振前会先冲一点点）
增益带宽积 0.1MHz  →  不振荡 ✓
&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;li&gt;在反馈路径加电容加速反馈：仅在反馈点加一个电容不够，还要再加一个分压电阻。
&lt;ul&gt;
&lt;li&gt;注意：这样接会带来两倍放大，对应那一边的电压要减小一半。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;想加速运放的下降速率很难，因为它等价于提高运放带宽压摆率（难以改变）。&lt;/p&gt;
&lt;h3&gt;最终结论&lt;/h3&gt;
&lt;p&gt;仅用反馈电容 + 分压电阻时问题仍未解决（三极管速度不够），最终仍需适当压一点运放带宽（加 10pF 电容）→ 电压稳定，电流仍约 1A。&lt;/p&gt;
&lt;p&gt;遇到振荡，压带宽是最可靠的手段。&lt;/p&gt;
&lt;h2&gt;方案二：负载接地&lt;/h2&gt;
&lt;p&gt;场景：有些应用要求负载接地，上面那种以集电极为输出的接法不适用 → 把 N 管换成 P 管。&lt;/p&gt;
&lt;h3&gt;反馈逻辑变化&lt;/h3&gt;
&lt;p&gt;以 P 管发射极作为反馈点，发射极没有反相作用，所以负反馈直接接到运放反向端即可。&lt;/p&gt;
&lt;h3&gt;上电过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 上电前三极管未开通，Vref 端 11V、反馈点 12V → V负 &amp;gt; V正 → Vout 想往下走（但本来就在 0V）
② 三极管导通，吸收电流 → 反馈点从 12V 往下掉
③ 运放输出被三极管大电流被动拉高（运放有内部输入阻抗）
   例如 12V 降到 11.9V 时运放升到 8V
④ 反馈点继续降到 11V 时，运放恰好升到 10.3V，V正 = V负 → 达到稳态
⑤ 之后运放主动维持 10.3V，偶尔微调，形成平衡
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;输出电流公式（虚短）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;I = (12V − Vref) / 1Ω
Vref = 11V  →  I = 1A
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;上电瞬间反馈点跳到 11V
若两边同时上升、都有高压，上电瞬间运放会吸收一个脉冲电流
  最大值约等于运放最大输入电流（常见运放约 40mA）
电流探头观察：开机一瞬间运放吸收电流为负，最大可达约 2A
随后反馈点上升、运放接管控制，电流压得很小
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际电路里这里还要再加一个电阻限流。&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;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;NPN&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;N 管&lt;/td&gt;
&lt;td&gt;反馈接同相端&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高端反馈（负载接地）&lt;/td&gt;
&lt;td&gt;P 管发射极&lt;/td&gt;
&lt;td&gt;P 管&lt;/td&gt;
&lt;td&gt;负载必须接地&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;分压电阻要取大（100K 左右），否则分压支路电流影响精度。&lt;/li&gt;
&lt;li&gt;恒流源不是独立电源，电压幅值决定最大负载；运放最大输出电流是另一个瓶颈（LM358 只有 40mA），大电流要加达林顿管。&lt;/li&gt;
&lt;li&gt;判断负反馈不看接哪一端，看 V+ 和 V− 是靠近还是远离。&lt;/li&gt;
&lt;li&gt;自激振荡是运放太快、反馈太慢，压带宽最有效；反馈电容加速法会带来两倍放大，对应电压要减半。&lt;/li&gt;
&lt;li&gt;上电瞬间运放输出要串限流电阻；负载接地时用 P 管，反馈点改到发射极。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>直流稳压电源</title><link>https://me.622168.xyz/posts/hardware/30-linear-regulator/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/30-linear-regulator/</guid><description>带反馈和不带反馈两种稳压电源的原理对比，以及为什么线性电源效率低。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p95（9 分钟）、p96（4.4 分钟）、p97（11.3 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;两种方案&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;带反馈：  运放          →  输出精确稳定
不带反馈：稳压管 + 三极管 →  简单便宜，但不精确
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;带反馈型&lt;/h2&gt;
&lt;h3&gt;电路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Vref → 运放同相端
Vout 经两个 1K 分压 → 反向端
运放输出 → 三极管基极
集电极接 12V，发射极输出 Vout
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存在明显负反馈 → 可用虚短。&lt;/p&gt;
&lt;h3&gt;快速推导&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;V正 = V负 = Vref
V负 = Vout × 1K/(1K + 1K) = Vout / 2
整理得：
Vout = 2 × Vref
Vref = 1.25V  →  Vout = 2.5V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;用运放工作特性逐步分析&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;上电 12V 接入、Vref 立刻变 1.25V，V负 初始为 0
→ V正 &amp;gt; V负 → Vout 上升
Vout = 0.6V 三极管开始导通
Vout = 1V    → 发射极 ≈ 1 − 0.6 = 0.4V
Vout = 2V    → 发射极 1.4V → 分压 0.7V，仍小于 1.25V，继续上升
Vout = 3.1V  → 发射极 3.1 − 0.6 = 2.5V → 分压 1.25V = Vref → 达到平衡
之后在 3.1V 附近微调（3.0999V ↔ 3.1V），输出稳定在 2.5V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意运放输出电压（3.1V）比输出（2.5V）高 0.6V——这 0.6V 就是三极管的 Vbe 压降。&lt;/p&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Vref&lt;/code&gt; 设 1.25V；上电时输出有过充（因运放输出速度太快，&lt;code&gt;V正 &amp;gt; V负&lt;/code&gt; 时一瞬间冲很高），随后从 3V 多慢慢降到 2.5V 并长期保持。&lt;/p&gt;
&lt;p&gt;这个电路的设计过程就是基于上面那个框图一点点扩展外部器件、按实际理论值调整，由最简电路扩大成完整电路。&lt;/p&gt;
&lt;h2&gt;不带反馈型&lt;/h2&gt;
&lt;p&gt;不需要运放，只需稳压管 + 三极管 + 电阻 + 电容。&lt;/p&gt;
&lt;h3&gt;原理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;三极管导通时 Vbe ≈ 0.6 ~ 0.7V（取 0.6V）
只要控制住基极电压，就相当于控制住发射极（输出）电压
稳压管导通后两端电压基本恒定 → 用它钳住基极
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;具体数值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入 12V 经电阻
稳压管取 5.6V → 击穿后基极 = 5.6V
发射极 = 5.6 − 0.6 = 5V  →  输出稳定在 5V
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;缺点&lt;/h3&gt;
&lt;p&gt;三极管 Vce 不等于饱和态的 0.3V，而是等于「电源电压 − 输出电压」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;12 − 5 = 7V      即开关管承受 7V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;能量账（负载 5Ω → 电流 1A）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电源输出总功耗 = 12V × 1A = 12W
负载上有效功耗 = 5V × 1A = 5W
三极管功耗 = 7V × 1A = 7W    ← 全部变成热
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以直流稳压电源效率很低，是三极管主动承担了电源多余的那部分能量。&lt;/p&gt;
&lt;p&gt;这也是「线性电源」名字的由来——调整管工作在线性区，多余电压以热的形式消耗掉。&lt;/p&gt;
&lt;h2&gt;两者对比&lt;/h2&gt;
&lt;p&gt;最大区别是输出点的稳定性。&lt;/p&gt;
&lt;h3&gt;仿真对比（都设计输出 5V）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;空载/轻载：带反馈精确 5V ✓；  不带反馈约 5.2V ✗
加载后：  带反馈仍为 5V ✓；  不带反馈由 5.2V 变成 5.083V ✗
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;不带反馈为什么不精确&lt;/h3&gt;
&lt;p&gt;原因一：稳压管有偏差——标称 5.6V，实际可能 5.5V 或 5.7V。&lt;/p&gt;
&lt;p&gt;原因二：三极管 Vbe 随工作电流变化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;工作电流 1mA   →  Vbe 0.5V
工作电流 5mA   →  Vbe 0.6V
工作电流 10mA  →  Vbe 0.7V
工作电流 500mA →  Vbe 0.8V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且还跟温度相关。&lt;/p&gt;
&lt;h3&gt;带反馈电路的自调节过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;【轻载】1K 负载、电流 5mA、Vbe = 0.5V
→ 运放自动把输出调到 5.5V → 输出 5V ✓
（过程：上电时 V负 = 0、V正 = 2.5V → 运放输出上升；
  到 0.5V 三极管导通；到 1.5V 时输出 1V → 反馈回来 0.5V；
  继续升到 5.5V 时输出 5V、反馈 2.5V → 稳态）

【突然挂 10Ω 负载】电流从 5mA 跳到约 500mA
→ Vbe 立刻变 0.8V → 输出电压瞬间从 5V 掉到 4.5V
→ 运放发现 V负 变小 → 输出上调到 5.6V
→ 但 500mA 时 Vbe = 0.8V，5.6V 只能对应输出 4.8V
   → 分压 2.4V &amp;lt; 2.5V，运放继续上调
→ 调到 5.8V 时输出刚好 5V、分压 2.5V → 重新稳态 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键的一点：运放会自动把「基极电压」抬高 0.3V 来补偿 Vbe 的变化。&lt;/p&gt;
&lt;h3&gt;不带反馈电路的同样场景&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;理想稳压管恒 5.6V，Vbe 由 0.5V 变 0.8V
→ 输出从 5.1V 掉到 4.9V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;稳压管「不知道」输出变了，像瞎子一样不管后级。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;要让输出更稳定，必须把电路做成负反馈系统（可实时自我调节）。&lt;/p&gt;
&lt;p&gt;不带反馈相当于开环，「自己以为是 5V 但实际是多少不知道」。&lt;/p&gt;
&lt;h2&gt;带反馈 vs 不带反馈&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;核心器件&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;好&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;高&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;精密电源、可调电源&lt;/td&gt;
&lt;td&gt;简单固定的辅助电源&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;两种线性稳压方案的位置&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;线性电源（LDO / 三端稳压器）  →  就是上面这两种
    特点：电路简单、纹波小、效率低（多余能量变成热）

开关电源（BUCK/BOOST）
    特点：效率高、纹波大、电路复杂
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;线性电源效率低是原理决定的，不是设计不好。7V × 1A = 7W 全变热。&lt;/li&gt;
&lt;li&gt;不带反馈的输出会随负载和温度漂移（Vbe 变化 + 稳压管偏差）。&lt;/li&gt;
&lt;li&gt;运放输出电压 = 输出 + Vbe，设计时要考虑这个偏移。&lt;/li&gt;
&lt;li&gt;上电瞬间会有过充，因为运放响应太快。&lt;/li&gt;
&lt;li&gt;压差大、电流大时别用线性电源，会烧；稳压管精度不高，要精确基准得用基准芯片。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>开关电源 BUCK 与 BOOST</title><link>https://me.622168.xyz/posts/hardware/31-switching-power-supply/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/31-switching-power-supply/</guid><description>从「开关 + 电阻 + 电容」一步步推出 BUCK 拓扑，以及 BOOST 怎么靠电感升压。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p98（12 分钟）、p99（12 分钟）、p100（4.9 分钟）、p101（8.1 分钟）、p102（9.7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;最原始的想法&lt;/h2&gt;
&lt;p&gt;最原始模型：12V 电源 + 开关 + 电阻 + 电容（先假设「人」就是控制电路）。&lt;/p&gt;
&lt;h3&gt;人工控制降压过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电容类似电池，直流电源充电时电压会慢慢充到电源电压
开关一直闭合 → 输出从 0 一直充到 12V
想降到 5V：看万用表，输出小于 5V 就闭合开关；升到 5V 就断开
带负载时：断开后电压往下掉，掉到 4.9V 再闭合；冲到 5.1V 再断开
→ 输出在 5V 附近来回抖动
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;不做任何控制、开关一直闭合  →  一直升高，最终冲到 12V
人为控制（到 5V 断开）      →  维持在 5V
加 100Ω 负载                →  反复通断，维持约 5V
&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;h2&gt;BUCK 拓扑是怎么推出来的&lt;/h2&gt;
&lt;p&gt;出发点：开关电源要的是高效率降压。&lt;/p&gt;
&lt;h3&gt;第 1 步：为什么要去掉电阻&lt;/h3&gt;
&lt;p&gt;原模型里的电阻会一直串在能量通路中，必然耗能，转换效率做不高，违背设计开关电源的初衷。&lt;/p&gt;
&lt;h3&gt;第 2 步：直接把电阻去掉为什么不行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;去掉后充电回路的等效电阻 R 极小
→ 电容充电时间常数 τ = R × C 极小
→ 充电速度极快
→ 开关一闭合电压「唰」一下从 0 冲到 12V
→ 人/控制电路根本来不及断开开关
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：理论上效率极高，但对控制电路速度要求极高，几乎不可能满足。&lt;/p&gt;
&lt;h3&gt;第 3 步：把电阻换成电感，为什么可行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电感是储能器件，理想状态下不耗能  →  解决电阻损耗问题
电感电流不能突变                  →  保留「抑制电容充电速度」的作用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;既解决耗能问题，又保留抑制充电速度的效果。&lt;/p&gt;
&lt;h3&gt;第 4 步：换电感后带来的新问题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关断开瞬间，电感原来的电流回路被切断
→ 等价于电感回路的电阻发生突变
→ 而电感回路电阻绝不能突变，突变会感生一个非常大的电压
→ 这个高压会轻松击穿开关
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;第 5 步：加续流二极管&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关断开后电感发现原支路走不通，但存在一条新支路
→ 通过二极管形成完整续流回路 → 不再产生高压
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个二极管的作用就是给电感续流，故称续流二极管。&lt;/p&gt;
&lt;h3&gt;最终得到完整 BUCK 拓扑&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入电容 + 开关 + 电感 + 续流二极管 + 输出电容 + 负载
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段推理链每一步都是被上一步的问题逼出来的，没有一步是凭空设计的。&lt;/p&gt;
&lt;h2&gt;续流二极管为什么有效&lt;/h2&gt;
&lt;h3&gt;无二极管时的机理&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关闭合时输出和电感电流一起慢慢升高
开关一断开，电感强制把能量释放出去
→ 实际上是给开关的寄生电容充电
→ 寄生电容容值非常非常小
→ 电感能量转移过去后，电容两端产生很高的电压
→ 若开关是 MOS 管，很容易被击穿
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真验证&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;把电阻换成电感，闭合开关 → 输出电压慢慢上升（速度越来越快）
先测输出电容两端电压 → 断开瞬间看不到明显高压（看错了观察点）
改测开关两端电压 → 断开瞬间出现极高尖峰
  示波器刻度调到 5000V/格，尖峰超过 5000V 还多约 1.2 格
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际电路里不会这么高，但搞个几百伏出来还是很简单的，MOS 管或三极管必坏。&lt;/p&gt;
&lt;p&gt;加一个理想二极管后：开关两端电压仍会上升，但尖峰消失（此时刻度只需 5V/格）。&lt;/p&gt;
&lt;h2&gt;控制芯片与拓扑的等效&lt;/h2&gt;
&lt;p&gt;现实中开关不由人控制，而由专用芯片控制。&lt;/p&gt;
&lt;h3&gt;芯片与分立拓扑的对应关系&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关         → 集成在芯片内部（Vin 到 SW 端之间）
二极管       → 也集成在芯片内部（在 GND 与 SW 之间）
电感         → 外部可见
电容         → 外部可见（两个输出电容）
控制电路（「人」）→ 就是芯片本身
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;芯片怎么「看」输出：它没有万用表，靠两个分压电阻把输出反馈进芯片。&lt;/p&gt;
&lt;h3&gt;降压型开关电源工作原理总结&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 芯片上电后通过反馈电阻检测输出电压
② 目标 5V，若检测到小于 5V → 让开关闭合
   更专业的说法是：让 MOS 管的占空比变大
③ 占空比变大 → 12V 给后级提供的能量越来越多 → 输出电压越来越高
④ 升到约 5V 时达到平衡，芯片对占空比做微调
&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;p&gt;这三句话解释了「为什么需要电感 + 电容两个储能元件」：电感负责开关断开瞬间的过渡，电容负责更长的时间尺度。&lt;/p&gt;
&lt;p&gt;芯片内部框架就是那一堆分立电路；把分立电路搞懂，芯片内部原理也就懂了。&lt;/p&gt;
&lt;h2&gt;BOOST 升压原理&lt;/h2&gt;
&lt;h3&gt;前提结论&lt;/h3&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;h3&gt;推导过程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 电感串开关，开关闭合 → 有电流流过，电感蓄能
② 开关突然断开 → 电感两端产生比电源高得多的电压 → 这就是升压的来源
③ 关键是怎么把能量收集起来：后级加一个储能电容
④ 开关断开后，电感电流仍要从左往右流 → 强制把电流灌到后级电容
⑤ 依据 V = Q / C：
   电感强制把大量电荷送到后级电容，Q 上升、C 不变 → V 必然上升
⑥ 每充一次电，电容电压就上升一个台阶
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;新问题&lt;/h3&gt;
&lt;p&gt;每次给电感充能都要闭合开关，而开关闭合时后级电容好不容易存的能量又被释放掉（白干）。&lt;/p&gt;
&lt;h3&gt;解决方案：在后级加一个二极管&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;开关闭合时：电源给电感充能
           此时二极管因该点电压变成 0 而反向截止
           → 阻止后级电容放电 ✓
开关断开时：电感强制给后级充电，电压上升一个台阶 ✓
&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;p&gt;和 BUCK 的差别：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电感位置：BUCK 在开关之后（串联），BOOST 在开关之前（串联）
二极管作用：BUCK 续流（给电感提供回路），BOOST 隔离（防止电容倒放）
输出：BUCK 比输入低，BOOST 比输入高
电容电压：BUCK 跟随，BOOST 一个台阶一个台阶上升
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;p&gt;必须加负载，否则一上电电源给后级充电会充到很高的电压。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;上电瞬间输出被充到约 12V
闭合开关 → 电感电流升高
断开 → 输出电压上升一个台阶
断开时间太长会升得非常高
加 1Ω 电阻限制电感电流后：
  反复「闭合—断开」，输出电压呈一个个台阶上升
  想输出 10V 就持续控制，稳定后放电到 10V 以下再闭合—断开
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BUCK 与 BOOST 对照&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;BUCK 降压&lt;/th&gt;
&lt;th&gt;BOOST 升压&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;&amp;lt; 输入&lt;/td&gt;
&lt;td&gt;&amp;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;电源给电感+电容+负载供能&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;12V → 5V、5V → 3.3V&lt;/td&gt;
&lt;td&gt;3.7V → 5V、12V → 24V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;占空比关系&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Vout = Vin × D&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Vout = Vin / (1 − D)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;12V 降到 5V 的电机控制电路板就是典型的 BUCK 应用。&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;BOOST 上电必须带负载；后级电容要加二极管隔离，否则下一个周期会把能量放掉。&lt;/li&gt;
&lt;li&gt;开关电源有纹波和噪声，给模拟电路供电要额外滤波。&lt;/li&gt;
&lt;li&gt;推拓扑要理解每一步是被什么问题逼出来的，死记拓扑没用。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>电感饱和电流与电荷泵</title><link>https://me.622168.xyz/posts/hardware/32-inductor-saturation-charge-pump/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/32-inductor-saturation-charge-pump/</guid><description>电感饱和为什么烧开关，以及用电荷泵升压和产生负压。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p103（4.2 分钟）、p104（7.3 分钟）、p105（3.2 分钟）&lt;/em&gt;&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;pre&gt;&lt;code&gt;算得电感峰值电流为 10A
→ 饱和电流至少取 1.2 倍（即 12A）
→ 取 1.5 倍更稳
&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;电感抑制电流的能力来自公式 V = L × di/dt
改写为：Δi/Δt = V / L

只要 V 不变、L 不变，电流变化率就是一定的
—— 这是未饱和时的正常工作状态

饱和后这种「缓慢变化」的抑制效果失效
→ 电流「唰」一下上升得非常快
&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;h3&gt;具体算例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电感两端加 5V、电感量 10µH
→ 电流变化率 = 5V / 10µH = 0.5A/µs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仿真验证：测 1µs 时间内的变化量为 500mV = 0.5V，对应 0.5A（用 1Ω 采样电阻观察）。&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;h2&gt;电荷泵升压&lt;/h2&gt;
&lt;p&gt;特点：既可以升压，也可以产生负压。&lt;/p&gt;
&lt;p&gt;目标：5V 输入 → 10V 输出。&lt;/p&gt;
&lt;h3&gt;电路与过程&lt;/h3&gt;
&lt;p&gt;三个开关 A、B、C + 一个飞跨电容。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 闭合 A、B，给电容充电 → 电容电压充到 5V
② 断开 A、B
③ 闭合 C，把电容底端接到 5V 电源
④ 以电源地为参考点：
   底端变成 5V
   根据「电容两端电压不能突变」，顶端就变成 5 + 5 = 10V
⑤ 此时负载接在顶端到 GND 之间即可得到 10V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是「自举」的本质：把电容当电池，再把电池的负极抬到电源电压上，顶端就变成了两倍。&lt;/p&gt;
&lt;h3&gt;问题：这个 10V 不能直接给负载用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;电容需要持续充电
充电时要断开 C、闭合 A/B
→ 此时负载电压会从 10V 掉到 5V，充满后才回到 10V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;负载工作电压要求 10V，电源充电期间电压往下掉就不正常了，也不可能让负载「等充好电再工作」。&lt;/p&gt;
&lt;h3&gt;解决方案：加一个二极管 + 一个输出电容&lt;/h3&gt;
&lt;p&gt;这个二极管的作用与前面 BOOST 的续流二极管很相似。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;开关闭合给电容充电到 5V
→ 断开 A/B、闭合 C → 得到 10V
→ 电源通过二极管给输出电容充电
→ 经多个周期，输出每个周期高一点，最终升到约 10V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;存在二极管压降，实际可能升到 9.7V 左右。&lt;/p&gt;
&lt;p&gt;带载后电容电压会往下掉一点点，只要储能速度足够快，每个开关周期都能补一部分能量，形成「掉一点—很快补上来」的循环。&lt;/p&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;去掉电感，只加几个开关
飞跨电容加大到 100µF；负载 100Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;不加二极管直接带载：输出能到 10V，但充电时明显往下掉
                    （0V → 5V → 10V → 慢慢下降 → 再充电回到 5V → 再释放到 10V）
加二极管 + 输出电容：反复开关几次，电压基本冲到 9V 多；
                    持续开关时输出约等于 9V 左右并不断跳变 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个功能叫自举升压。&lt;/p&gt;
&lt;h2&gt;电荷泵产生负压&lt;/h2&gt;
&lt;p&gt;目标：用同样的电荷泵结构产生负压。&lt;/p&gt;
&lt;h3&gt;电路与过程（5V 电源，开关 A、B、C + 电容）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 闭合 A、B 给电容充电
   → 电容充到「上正下负」的 5V（上极板比下极板高 5V）
② 断开 A、B
③ 与升压的区别：
   升压是把 C 接到电容底端
   产生负压则是把开关 C 接到电容顶端、另一端接 GND
④ C 一闭合，电容顶端被拉到 0V
   → 顶端比底端高 5V
   → 底端变成 −5V
⑤ 由此得到负压输出
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;同样不能直接给负载供电&lt;/h3&gt;
&lt;p&gt;（开关切换过程中电压会变化）&lt;/p&gt;
&lt;h3&gt;解决方案&lt;/h3&gt;
&lt;p&gt;加一个二极管 + 一个电容，再由它们给负载供电，得到约 −5V 的电压。&lt;/p&gt;
&lt;h3&gt;仿真&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;把开关接法改成上述形式、二极管方向反过来、GND 移到上面
→ 切换开关后输出出现负压
→ 反复切换可得到约 −4.x V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用电荷泵可以很容易地实现负压。&lt;/p&gt;
&lt;h2&gt;升压 vs 负压 对照&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;充电后电容&lt;/td&gt;
&lt;td&gt;上正下负 5V&lt;/td&gt;
&lt;td&gt;上正下负 5V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;开关 C 接到&lt;/td&gt;
&lt;td&gt;电容底端 → 底端接电源&lt;/td&gt;
&lt;td&gt;电容顶端 → 顶端接 GND&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;结果&lt;/td&gt;
&lt;td&gt;顶端变成 +10V&lt;/td&gt;
&lt;td&gt;底端变成 −5V&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;约 9.x V（有二极管压降）&lt;/td&gt;
&lt;td&gt;约 −4.x V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;记忆要点：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;把电容的「底」抬到电源上  →  顶变成两倍  →  升压
把电容的「顶」按到地上    →  底变成负    →  负压
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;电荷泵的特点&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;优点：不需要电感；电路简单、体积小；可以升压也可以产生负压；成本低
缺点：输出电流小；有二极管压降损失；输出纹波较大；效率一般
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;典型用途：MOS 管自举驱动、运放负电源、小电流升压（如 3.3V → 5V 给 LCD 偏压）。&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;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;BUCK&lt;/td&gt;
&lt;td&gt;电感 + 二极管 + 电容&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;BOOST&lt;/td&gt;
&lt;td&gt;电感 + 二极管 + 电容&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;电容 + 二极管&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;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;电感饱和电流必须大于最大工作电流，取 1.2~1.5 倍余量。&lt;/li&gt;
&lt;li&gt;饱和后电感变导线，电流暴涨会烧开关、烧电感、拉死电源。&lt;/li&gt;
&lt;li&gt;电荷泵输出必须加二极管 + 输出电容，否则负载会在充电期间掉压；而且有二极管压降损失，实际输出比理论低 0.3~0.7V。&lt;/li&gt;
&lt;li&gt;升压和负压的区别只在开关 C 接哪一端，接法记反了结果完全不同。&lt;/li&gt;
&lt;li&gt;电荷泵只能带小电流，大电流要用电感方案。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>实战项目 纯硬件循迹小车</title><link>https://me.622168.xyz/posts/hardware/33-line-following-car/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/33-line-following-car/</guid><description>从原理到原理图，把前面学的所有知识点拼成一台真正能跑的小车。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p106–p117（原理 / 黑线检测 / 控制逻辑 / 电机驱动 / 电源电路）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这是整门课的收口。前面 105 集学的元器件和电路在这里全部用上，而且每个电阻的值都得自己算。&lt;/p&gt;
&lt;h2&gt;工作原理：差速转向&lt;/h2&gt;
&lt;p&gt;板底有两对红外管，每对一个红外发射 LED 加一个红外接收管。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;白色区域：白光反射所有光谱 → 红外光被反射回接收管 → 接收到信号 → 电机转
黑色区域：黑色吸收所有光 → 接收管收不到红外光 → 该侧电机停
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;循迹过程：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;两侧都在白区  →  两轮同转直行
一侧压到黑线  →  该侧轮停、另一侧继续转  →  产生差速  →  拐弯
拐到一定角度后红外管脱离黑线  →  恢复前进
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一句话：用红外光识别黑线，按检测结果控制两个轮子是否转动。&lt;/p&gt;
&lt;h2&gt;黑线检测的原理&lt;/h2&gt;
&lt;h3&gt;红外接收二极管&lt;/h3&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;无红外光&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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;电路与公式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;5V → 电阻 R → 节点 VR → 红外接收二极管 → GND

VR = 5V − IR × R
&lt;/code&gt;&lt;/pre&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;VR&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;白色&lt;/td&gt;
&lt;td&gt;假设 1mA&lt;/td&gt;
&lt;td&gt;1mA × 1K = 1V&lt;/td&gt;
&lt;td&gt;≈ 4V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;黑色&lt;/td&gt;
&lt;td&gt;假设 1µA&lt;/td&gt;
&lt;td&gt;1µA × 1K = 1mV&lt;/td&gt;
&lt;td&gt;≈ 4.999V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;同一点在黑区、白区有两个不同电压值（4V vs 4.999V），据此判断小车是否走到黑线区域。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;R 还能调灵敏度：IR 基本不变时，R 取得越大，白区 VR 掉得越低。&lt;/p&gt;
&lt;h2&gt;控制逻辑&lt;/h2&gt;
&lt;p&gt;加比较器做判断：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;同相端 V+ 接参考电压 4.5V（取在 4V 与 5V 之间）
红外处理电路的信号接反相端 V−
&lt;/code&gt;&lt;/pre&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;区域&lt;/th&gt;
&lt;th&gt;VR&lt;/th&gt;
&lt;th&gt;比较&lt;/th&gt;
&lt;th&gt;比较器 Vout&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;5V&lt;/td&gt;
&lt;td&gt;V− &amp;gt; V+&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;4V&lt;/td&gt;
&lt;td&gt;V− &amp;lt; V+&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;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;和实物演示一致：一侧接收管进黑区 → 该侧轮停、另一侧继续转 → 差速拐弯。&lt;/p&gt;
&lt;h2&gt;电机驱动电路的设计演进&lt;/h2&gt;
&lt;h3&gt;第一版&lt;/h3&gt;
&lt;p&gt;本车电机只单向工作，不需要正反转 → 只需控制「是否接电」→ 一个开关就够。选 NPN 三极管 SS8050。&lt;/p&gt;
&lt;p&gt;封装上有坑：板子要直接贴在车体结构面上，不能用插件（直插）元器件——引脚会把板子顶起来，没法平放，必须选贴片封装。&lt;/p&gt;
&lt;p&gt;电机不画在原理图上。电机用两根线焊在板上，板上只留两个孔，用排针 Header 2 表示电机接口。&lt;/p&gt;
&lt;h3&gt;改善版：五个关键改进&lt;/h3&gt;
&lt;h4&gt;① 电机续流回路&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;电机是电感模型，回路阻抗突变时产生很高感应电动势
三极管导通后突然断开 → 电机两端产生高压
量级：几十到一两百伏（约 100V）很轻松
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决：电机两端加续流二极管。&lt;/p&gt;
&lt;p&gt;选型要求：二极管导通电流 &amp;gt; 电机正常工作电流。&lt;/p&gt;
&lt;p&gt;电机参数：TT 电机，正常工作电流约 500mA（负载重时更大）。&lt;/p&gt;
&lt;p&gt;二极管只在断开瞬间导通一下，普通二极管即可，比如 1N4007；大功率电机最大电流可达 10A，那就必须选大功率二极管。&lt;/p&gt;
&lt;h4&gt;② 三极管选型&lt;/h4&gt;
&lt;p&gt;SS8050 的 Ic 够用，但封装小、散热差，工作中发热大概五六十度。小项目没什么大问题。&lt;/p&gt;
&lt;h4&gt;③ 基极电阻计算&lt;/h4&gt;
&lt;p&gt;网上那句「基极电阻随便取 1K/10K」只适用于负载电流几 mA 到十几 mA 的场合，驱动电机必须算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 三极管做开关要饱和导通 → 条件：β × IB &amp;gt; IC
② IC ≈ 电机电流 500mA
③ 假设 β = 100 → IB = 500mA / 100 = 5mA
④ R总 = (5V − Vbe 0.7V) / IB = (5 − 0.7) / 5mA = 4.3V / 5mA ≈ 0.86K
⑤ 总阻值要小于 0.86K → 实际做法：一个取 0Ω、另一个取 820Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么不能把 0Ω 那个直接短路掉：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;LM393 是 OC 输出，内部等效一个三极管
输出低电平时该三极管导通
若此处电阻为 0Ω，相当于把回路直接短路 → LM393 会烧掉
取 820Ω 时该支路电流约 5mA
而比较器/运放的输出与吸收电流一般可达五六十 mA → 安全
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;这个电阻绝不能省略不画&lt;/strong&gt;，它是一条重要支路，还得预留出来方便后期调试。&lt;/p&gt;
&lt;p&gt;后期必须实测调整：实际三极管 β 常比 100 大很多（可能 200 甚至 300）。焊上后测是否饱和导通，若已饱和就把电阻逐步放大——β 富余时，电阻越大静态功耗越小。&lt;/p&gt;
&lt;h4&gt;④ 参考电压分压电阻初值&lt;/h4&gt;
&lt;p&gt;上面 10K、下面 1K，配合滑动变阻器改变参考电压。&lt;/p&gt;
&lt;p&gt;参考电压必须用滑动变阻器，不能固定分压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;原理分析时假设 IR = 1mA、硬分压出 4.5V（如 500Ω + 4.5K）是不靠谱的
实际 IR 不一定是 1mA，装上就可能没反应
→ 滑动变阻器一端接 5V、一端接 GND，中间抽头输出
→ 0~5V 连续可调 → 可现场调节检测阈值 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;⑤ 电源去耦/滤波电容&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;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;VIN 部分&lt;/td&gt;
&lt;td&gt;储能电解电容 + 100nF&lt;/td&gt;
&lt;td&gt;储能 + 滤高频&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;比较器的 5V&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;100pF&lt;/td&gt;
&lt;td&gt;滤除红外接收管收到的高频噪声&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;储能电容的作用&lt;/h3&gt;
&lt;p&gt;实际安装时 6V 到电池可能有一根很长的走线。开关突然闭合时，外部电源给这一侧供电有一定反应时间。&lt;/p&gt;
&lt;p&gt;画 PCB 时可以把该电容放在电机安装端口旁边，这样电容能提供瞬态大电流：开关闭合瞬间优先给电机供能，防止电压跌落。&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;LM393 是双通道比较器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;8 个引脚：1、2、3 = 比较器 1；5、6、7 = 比较器 2；4 = GND、8 = VCC
买一颗芯片里面就已经包含两个比较器
本车两个电机正好用两个比较器，共用同一颗芯片
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为是同一颗芯片、共用电源，复制出来的这一路 5V 供电端口可以删掉。&lt;/p&gt;
&lt;p&gt;但上拉电阻必须保留，别顺手把它也一起删了。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同一颗芯片的多通道，电源只画一次&lt;/strong&gt;，这个技巧很实用。&lt;/p&gt;
&lt;h2&gt;电源电路&lt;/h2&gt;
&lt;h3&gt;电源轨设计&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;输入：四节 5 号电池串联 = 6V
需要：6V → 5V → 3.3V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;（额外加一个 3.3V 是为了演示 LDO 用法，实际可以不用 3.3V、直接用 5V。）&lt;/p&gt;
&lt;p&gt;5V、3.3V 只给信号处理电路供电，没必要用开关电源，用 LDO 即可。&lt;/p&gt;
&lt;h3&gt;LDO 的关键参数：最小稳压压差&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;想输出 5V，输入必须 &amp;gt; 5V + 最小压差
常见值：0.3V / 0.7V / 1.4V，有的更高
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;AMS1117-3.3 / -5V 这类芯片的最小压差通常大于 1V：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;最小压差&lt;/th&gt;
&lt;th&gt;要输出 5V 需要的最小输入&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.5V&lt;/td&gt;
&lt;td&gt;6.5V&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.2V&lt;/td&gt;
&lt;td&gt;6.2V&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;否则 6V 输入进去，输出只有 4.8V。&lt;/p&gt;
&lt;p&gt;压差小的芯片（0.3/0.5V）价格会贵一些。&lt;/p&gt;
&lt;h3&gt;采用的方案&lt;/h3&gt;
&lt;p&gt;选不到合适的，就用分立元器件搭线性稳压：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6V →（5.6V 稳压管 + 三极管）→ 5V →（AMS1117-3.3）→ 3.3V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;节点电压 5.6V，减去三极管 Vbe 压降，得到约 5V。&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;6V 输入处&lt;/td&gt;
&lt;td&gt;10µF（小电容即可，没必要大电容）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1117 输入/输出&lt;/td&gt;
&lt;td&gt;各 10µF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中间节点&lt;/td&gt;
&lt;td&gt;再加一个小电容滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5V 输出&lt;/td&gt;
&lt;td&gt;100nF 储能/滤波&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;输出端&lt;/td&gt;
&lt;td&gt;再挂两个电容&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;稳压管限流电阻的计算&lt;/h3&gt;
&lt;p&gt;已知：稳压管 5.6V，输入 6V → 电阻两端电压 ≈ 0.4V（6 − 5.6）。&lt;/p&gt;
&lt;p&gt;流过该电阻的电流由两部分组成：① 稳压管电流 ② 三极管基极电流 IB。&lt;/p&gt;
&lt;p&gt;稳压管工作电流取值范围：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;反向电流约 0.5mA 时就开始有比较稳定的电压
最大可达 20mA 甚至 50mA，与封装的功率有关
实际取值一般取 1mA ~ 10mA
本例取中间值 5mA 左右
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;IB 的估算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;后级工作电流估算：5V 主要供给两个 10K 分压电阻 + 比较器 → 约 10mA
                  再加下面 3.3V 那部分约 10mA → 后级最多约 20mA
取 IC 最大 20mA，β = 100 → IB = 20mA / 100 = 0.2mA
→ 非常小，基本可忽略不计
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;电阻计算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电流 ≈ 5mA，电压 = 0.4V
R5 = 0.4V / 5mA = 0.08K = 80Ω
实际取 82Ω
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;该电阻只流过 5mA，很小，选 0805 封装即可。&lt;/p&gt;
&lt;h3&gt;收尾补充&lt;/h3&gt;
&lt;p&gt;三处细节：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;三极管加一个下拉电阻（基极与地之间），增强电路稳定性。&lt;/li&gt;
&lt;li&gt;补画两个红外发射管（前面漏画了）。实物一共 4 个 LED：2 个接收管 + 2 个发射管。封装一定选普通插件封装（直插）。&lt;/li&gt;
&lt;li&gt;加两个指示 LED（0805 贴片）。某侧红外接收管收到红外信号时 LED 点亮，能实时看出该侧电机在工作，方便后期调试。限流电阻取 1K。&lt;/li&gt;
&lt;/ol&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;黑线检测 ×2&lt;/td&gt;
&lt;td&gt;红外发射管 + 红外接收管&lt;/td&gt;
&lt;td&gt;接收管反接；分压电阻 1K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;信号比较 ×2&lt;/td&gt;
&lt;td&gt;LM393（双通道）&lt;/td&gt;
&lt;td&gt;参考电压用滑动变阻器 0~5V 可调&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;比较器上拉&lt;/td&gt;
&lt;td&gt;上拉电阻&lt;/td&gt;
&lt;td&gt;OC 输出必须加&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电机驱动 ×2&lt;/td&gt;
&lt;td&gt;SS8050 + 基极电阻&lt;/td&gt;
&lt;td&gt;基极总阻值 &amp;lt; 860Ω，取 820Ω&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;续流保护 ×2&lt;/td&gt;
&lt;td&gt;1N4007&lt;/td&gt;
&lt;td&gt;电机两端反并联&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;储能滤波&lt;/td&gt;
&lt;td&gt;电解电容 + 100nF + 100pF&lt;/td&gt;
&lt;td&gt;靠近电机端子&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电源&lt;/td&gt;
&lt;td&gt;6V 电池 → 5.6V 稳压管 + 三极管 → 5V → AMS1117-3.3 → 3.3V&lt;/td&gt;
&lt;td&gt;稳压管限流电阻 82Ω&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;用到的知识点&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;欧姆定律          → 算分压电阻、基极电阻、限流电阻
电阻的限流/分压   → 红外分压、参考电压分压
电容的储能/滤波   → 储能电容、去耦电容
电感/续流二极管   → 电机续流回路
二极管            → 续流、防反接
三极管开关        → 电机驱动，基极电阻计算
比较器            → 黑线判断，OC 输出加上拉
LDO / 线性稳压    → 6V → 5V → 3.3V
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;能独立做完这个项目，前面 105 集就没白学。&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;红外接收二极管必须反接；参考电压必须用可调电阻，硬分压基本没反应。&lt;/li&gt;
&lt;li&gt;LM393 是 OC 输出，上拉电阻不能省。&lt;/li&gt;
&lt;li&gt;基极电阻不能取 0Ω（会烧 LM393），也不能抄 1K/10K，得按负载电流算。&lt;/li&gt;
&lt;li&gt;电机必须加续流二极管，滤波电容必须靠近电机端子。&lt;/li&gt;
&lt;li&gt;LDO 的 dropout 压差要查手册，6V 输入想输出 5V，压差大的芯片做不到。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>PCB 设计全流程</title><link>https://me.622168.xyz/posts/hardware/34-pcb-design-flow/</link><guid isPermaLink="true">https://me.622168.xyz/posts/hardware/34-pcb-design-flow/</guid><description>从画原理图库封装到打板，Altium Designer 的完整操作流程和所有关键数值。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p118–p127（原理图封装 / PCB 封装 / 导入 / 边框 / 定位 / 布局 / 走线 / 铺铜 / 检查）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;软件：Altium Designer（AD）。&lt;/p&gt;
&lt;h2&gt;流程总览&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;① 画边框（Board Outline）
② 关键器件定位
③ 布局（Layout）
④ 走线（Routing）
⑤ 铺铜（Polygon Pour）
⑥ DRC 检查 + 丝印调整 → 打板
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;布局决定了布线好不好&lt;/strong&gt;，必须按原理图中最小电流回路路径摆放。&lt;/p&gt;
&lt;h2&gt;画原理图库封装&lt;/h2&gt;
&lt;h3&gt;添加原理图库&lt;/h3&gt;
&lt;p&gt;工程 → 添加新的…到工程 → 原理图 Library。&lt;/p&gt;
&lt;h3&gt;先设置格点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;视图 → 栅格 → 设置栅格
习惯调到 50mil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不要设成 30mil/20mil 之类。原理图库的格点必须与原理图的格点保持一致或成 2 倍关系，否则画原理图时线拉不直、对不上。&lt;/p&gt;
&lt;h3&gt;按数据手册画引脚&lt;/h3&gt;
&lt;p&gt;打开数据手册，里面有功能引脚介绍，照着画。&lt;/p&gt;
&lt;p&gt;引脚的小白点（电气连接点）必须朝外。只有白点才能连接外部信号线；白点朝内的话，外面那根线连不上。&lt;/p&gt;
&lt;p&gt;编辑引脚信息：放置引脚后按 Tab 键弹出属性。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;第 1 栏   引脚编号（必须与 PCB 封装对应）→ 第 1 脚写 1
第 2 栏   Name 引脚名称 → 第 1 脚对应 1OUT，写 out1
引脚长度  一般设 100mil
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;LM393 引脚定义：1 = &lt;code&gt;1OUT&lt;/code&gt;、2 = &lt;code&gt;1IN−&lt;/code&gt;、3 = &lt;code&gt;1IN+&lt;/code&gt;、4 = &lt;code&gt;GND&lt;/code&gt;、5 = &lt;code&gt;IN2+&lt;/code&gt;、6 = &lt;code&gt;IN2−&lt;/code&gt;、7 = &lt;code&gt;2OUT&lt;/code&gt;、8 = &lt;code&gt;VCC&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;现象：画完只有数字、没有文字名称。&lt;/p&gt;
&lt;p&gt;方法：全选引脚 → 点信息窗 → 找到 name 一栏（其「眼睛」图标被划掉）→ 再点一下即显示。&lt;/p&gt;
&lt;p&gt;显示后会「全部乱了」，不用慌：全选引脚 → 拉出来 → 把外框拉长一点即可。&lt;/p&gt;
&lt;h3&gt;设置器件属性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Design ID / 器件名：LM393
标号（Designator）：芯片一般以 U 开头，写成 U?
描述（Comment）：填「比较器」
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多部件封装&lt;/h2&gt;
&lt;p&gt;单部件版能用，但画出来的电路线绕来绕去、非常难检查，所以要拆多部件。&lt;/p&gt;
&lt;p&gt;新建一个 LM393 器件 → 选中它 → 工具 → 新部件。注意是「新部件」，不是「新器件」。&lt;/p&gt;
&lt;p&gt;之后会出现 Part A / Part B。&lt;/p&gt;
&lt;h3&gt;Part A&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;引脚：1 = OUT、2 = IN−、3 = IN+
习惯上：Part A 里画电源和地
  VCC 是第 8 脚、GND 是第 4 脚
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;运放/比较器的 GND 脚很多时候叫 VS（因为部分运放支持双电源供电）。&lt;/p&gt;
&lt;p&gt;三角形符号：放置 → 多边形；线宽改成 small，填充改成透明。&lt;/p&gt;
&lt;p&gt;引脚与三角形接不上，三种处理：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把三角形往后挪一点&lt;/li&gt;
&lt;li&gt;把引脚长度加长（例如改成 120 或 150）&lt;/li&gt;
&lt;li&gt;推荐「笨办法」：额外放置一条线把引脚和三角形连起来&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;不建议把格点改成 10mil 去精调——原理图格点可能是 100 或 50，格点不一致容易对不齐。&lt;/p&gt;
&lt;h3&gt;Part B&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;第 5 脚 = 正、第 6 脚 = 负、第 7 脚 = OUT2
两个部件的三角形面积大小要调成一致
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;画 PCB 封装&lt;/h2&gt;
&lt;p&gt;依据：必须按实物封装图 / 数据手册画。所有芯片手册最后两页一般都会给出芯片大小和引脚分布信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;① 命名&lt;/strong&gt;：&lt;code&gt;SOP-8&lt;/code&gt; 或 &lt;code&gt;SOIC-8&lt;/code&gt;，也可写成 &lt;code&gt;SOP-8 LM393&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② 开格点显示&lt;/strong&gt;：按字母 G → 点三个属性 → 点「设为默认/试用」。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;③ 放焊盘&lt;/strong&gt;：放置 → 焊盘（Pad）→ 按 Tab 编辑。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Layer：LM393 是贴片 → 选 Top Layer（贴片焊盘）；过孔型焊盘 → 选 Multi-Layer
编号：画第 1 脚就把数字写成 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;编号必须与原理图库引脚一一对应，千万不能乱标。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;④ 单位切换&lt;/strong&gt;：库默认单位是 mil，PCB 一般用毫米 → 输入法切英文 → 按字母 Q 切换单位。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⑤ 计算焊盘长宽&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;长：数据手册给 8.2 和 5.6 → (8.2 − 5.6)/2 = 1.3mm → 取 2mm（留空间方便焊接）
宽：数据手册写 0.51 或 0.35 → 取 0.5mm
形状：圆形（也可方形，圆形好看）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一个标准焊盘 = 长 2mm × 宽 0.5mm。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⑥ 保证引脚间距&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;数据手册给出一脚与二脚间距 = 1.27mm（焊盘中心距）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;方法一（坐标法）：以 1 脚为原点，第 2 脚 Y 坐标填 −1.27&lt;/li&gt;
&lt;li&gt;方法二（改格点步进）：按 G → 栅格属性 → 去掉步进链接 → Y 步进 = 1.27、X 步进 = 8.2&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;⑦ 检查&lt;/strong&gt;：按 CTRL + M 测距 → 中心距 = 8.2mm。&lt;/p&gt;
&lt;p&gt;含义：芯片放上去时芯片边缘正好卡在焊盘中心点，于是每边多出一半焊盘方便焊接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⑧ 画外框丝印&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;格点调小：单位换回 mil、设成 10mil 一格
层必须切到 Top Overlay（丝印层）
放置 → 线条，画外框
标第一脚：把线画长一点，再放一个小圆圈表示第 1 脚
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;⑨ 保存 + 验证对应关系&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;点引脚检查是否高亮对应时，1 脚点进去不亮，原因是画 PCB 封装时把 1 脚编号写成了「10」而不是「1」。&lt;/p&gt;
&lt;p&gt;改回 1 → 保存 → 必须在原理图侧「刷新预览」，否则更新不过来。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PCB 封装的引脚编号绝对不可以乱取，必须严格按 1~8 的位置对应
原理图里的引脚位置可以随便改（放哪都行）
只要引脚编号与 PCB 封装编号对得上即可
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另外，不要盲信嘉立创等平台下载的封装。上面有些封装是画错的，被坑过的人不少。下载后必须逐脚检查：打开其原理图库，一个个点引脚，核对 1 脚 = OUT、2 脚 = IN−、3 脚 = IN+、4 脚 = GND、8 脚 = VCC。&lt;/p&gt;
&lt;h2&gt;原理图导入 PCB&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;① 选中原理图
② 设计 → 更新 PCB（Update PCB Document）
③ 弹出对话框后先点「验证变更」（第一步检查有无错误）
④ 有错误可点「仅显示错误」查看，无错误则列表空白
⑤ 再点「执行变更」→ PCB 文件里出现一堆元器件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键操作顺序：把 LM393 的封装删掉后直接点执行变更，不报错；必须先点「验证变更」才会报错。&lt;/p&gt;
&lt;p&gt;报错内容示例：U1 缺少了某个东西（缺封装）。&lt;/p&gt;
&lt;p&gt;少封装、短路等任何问题都会在「验证」阶段报出对应错误。&lt;/p&gt;
&lt;p&gt;新手排查方法：把报错复制到百度搜索，这些是非常常见的错误，90% 都能解决。&lt;/p&gt;
&lt;h2&gt;机械结构与边框&lt;/h2&gt;
&lt;h3&gt;画边框&lt;/h3&gt;
&lt;p&gt;必须根据实际结构画（PCB 是直接放在小车上的）。&lt;/p&gt;
&lt;p&gt;尺寸获取方法：买个新模型回来，在实物上画一条线再量。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;小车的长 = 10cm（100mm）
框宽量出来 = 65mm
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;操作：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 层切到机械层第一层（Mechanical 1）
② 放置 → 线条，先随便画一条
③ 单位切换成毫米，双击线条，把长度改成 100mm
④ 再画/复制一条，长度改 65mm
⑤ 格点改成 10mil 解决对不齐问题
⑥ 复制/剪切线条拼成矩形
⑦ 全选 → 设计 → 板子形状 → 按照对象定义 → 得到 100 × 65mm 的边框
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;安装孔（3 个螺丝孔）&lt;/h3&gt;
&lt;p&gt;方法：画板之前先用笔在实物上画一个边框，再量孔到边框的距离。&lt;/p&gt;
&lt;p&gt;放焊盘（放焊盘而不是过孔，因为过孔是要连线的）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;放置 → 焊盘
层选 Multi-Layer → 成为穿孔焊盘（插件焊盘）
装 3mm 螺丝 → 焊盘设 3.5 × 3.5mm
钻孔（Hole Size）：因为是纯安装孔，直接把钻孔也填 3.5
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;用坐标精确定位：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;把原点设在左上角
第 1 个孔：X = 7（距左边线右偏 7mm）、Y = −15（往下 15mm）
第 2 个孔：偷懒办法 = 把右边设为原点，同样 X = 7、Y = −15 → 左右自动对称
第 3 个孔（中间）：X = 50（左右对称）、Y = 13
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后锁定：全选 → 点击属性里的锁。&lt;/p&gt;
&lt;h2&gt;关键器件定位&lt;/h2&gt;
&lt;h3&gt;安装孔旁的 Keep-Out 圈&lt;/h3&gt;
&lt;p&gt;边框上孔旁边会有一个「圈圈」，作用是把孔旁边的铜去掉。&lt;/p&gt;
&lt;p&gt;做法：在 Keep-Out Layer 层再放一个圆（圆弧），该孔旁边就不会铺铜。&lt;/p&gt;
&lt;h3&gt;布局第一步 = 先放关键器件&lt;/h3&gt;
&lt;p&gt;判断依据：实物机械结构上有几组洞，所以焊线接口、LED 的位置必须按这些洞的位置来放。&lt;/p&gt;
&lt;p&gt;定位方法：拿尺子量出相对距离 → 换算成坐标 → 用坐标法逐个放置 LED（4 个）。&lt;/p&gt;
&lt;h3&gt;AD 快捷操作&lt;/h3&gt;
&lt;p&gt;交叉选择模式：工具 → 交叉选择模式必须先打开；打开后在原理图中选中器件，PCB 中会自动被选中。&lt;/p&gt;
&lt;p&gt;判断器件放左还是放右：先把 LM393 挪出来，观察飞线（ratsnest）走向。&lt;/p&gt;
&lt;h3&gt;发射管必须靠中间放&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;车行驶时黑线在中间
发射管越靠近黑线，灵敏度越高
→ 发射 LED 要靠近中间放置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;锁定关键器件&lt;/h3&gt;
&lt;p&gt;把几个 LED 安装孔、电机安装孔、电源输入孔全部选中 → 属性 → 上锁固定住。&lt;/p&gt;
&lt;h2&gt;元器件布局&lt;/h2&gt;
&lt;h3&gt;丝印处理&lt;/h3&gt;
&lt;p&gt;先把所有网络隐藏；再点中丝印 → 查找相似对象 → 应用 → 把文字高度调成 0.1、位置设到中间。&lt;/p&gt;
&lt;h3&gt;布局总原则&lt;/h3&gt;
&lt;p&gt;按最短电流路径布局。&lt;/p&gt;
&lt;h3&gt;电机部分布局&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;两个储能电容   紧靠电机端子（它们是给电机储能的）
小电容         紧贴端子（越近滤波效果越好）
大电容         可以远一点
续流二极管     可以放远一点
滤噪声的电容   必须靠近端子（噪声一出来就要被滤掉）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么储能电容不能放电源输入端：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;电容接 6V、电源端也是 6V，放那边软件不会报错（飞线连的也是 6V 端子）
但功能上错了
→ 画原理图/PCB 时心里必须有清晰的电流路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;三极管摆放要顺着电流流向：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6V → 电机 → 回到端子 → 三极管集电极 → 发射极 → GND → 电容负极
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反面案例：为了「好看/对称」把三极管怼到对称位置、排得整整齐齐，工作起来反而一团糟。&lt;/p&gt;
&lt;h3&gt;封装调整&lt;/h3&gt;
&lt;p&gt;下拉电阻封装改小：查找相似对象 → 全选电阻（一定要先全选）→ 封装改成 0603-1 → 保存 → 更新到 PCB。&lt;/p&gt;
&lt;h3&gt;器件间距规则&lt;/h3&gt;
&lt;p&gt;现象：更新后全部变绿。原因是有个多余的 class，删掉即可。&lt;/p&gt;
&lt;p&gt;仍报绿，就与器件间距规则有关：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;规则原理：电解电容的 PCB 封装里已标注了长宽信息
         电阻放进了它的范围 → 触发「器件与器件之间的最小水平距离」规则（当前值 0.254）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;处理：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以改规则（把最小水平距离改成 0），立刻不报错&lt;/li&gt;
&lt;li&gt;但不要改成 0——改成 0 后万一两个器件真干涉就不会报错&lt;/li&gt;
&lt;li&gt;推荐做法：① 针对这两个器件单独设一条规则；② 把器件挪远一点（更推荐）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;比较器部分布局&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;尽量靠近比较器摆
可调电阻放旁边即可
比较器芯片的储能电容必须靠近芯片放，让 VCC/GND 路径最短
两个接收管的调节电阻建议放到空旷处（要焊接更换，夹在中间容易手抖短路）
发射端最好也加一个电阻/电容做滤波
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一个工作方法&lt;/h3&gt;
&lt;p&gt;画 PCB 时要实时回头改原理图：发现「这里少了个电容、加了能增加稳定性」就直接加。不要指望一上来原理图就完美，边画边调。&lt;/p&gt;
&lt;p&gt;标号必须用「静态标注」，绝对不能用「强制标注」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;工具 → 静态标注原理图
点了强制标注，所有编号会全部乱掉，前面做的工作全白做
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;布局原则总结：先保证电路工作质量，再考虑美观。&lt;/p&gt;
&lt;h2&gt;走线 layout&lt;/h2&gt;
&lt;h3&gt;线宽规则&lt;/h3&gt;
&lt;p&gt;设计 → 规则 → Routing → Width：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;最小宽度   默认 0.254 → 本次 0.254（走信号线）
优选宽度   默认 0.254 → 本次 0.254
最大宽度   默认 0.254 → 本次 0.5（连电源线）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;连线操作&lt;/h3&gt;
&lt;p&gt;点连线工具 → 点一下焊盘旁边会亮 → 按 Tab 键（所有信息修改都按 Tab）→ 设置走线宽度、过孔大小、走线拐角。&lt;/p&gt;
&lt;h3&gt;经验数值&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10mil 的线通常可过 1A 左右电流
本项目电机电流只有几百毫安 → 用 0.254 其实就够了
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;给电源单独设规则（Class 用法）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 设置 → 类（Class）→ 添加类 → 新建 new class → 改名 VCC
   → 把 3.3V、5V、6V 添加进来 → 确定
② 规则 → Width → 添加新规则 → 命名 Width-VCC
③ 规则作用对象选 Net Class → VCC
④ 最小/优选/最大宽度全部设成 0.35 → 应用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;效果：一走 VCC 类网络自动就是 0.35，不用每次重复设。&lt;/p&gt;
&lt;h3&gt;打过孔&lt;/h3&gt;
&lt;p&gt;按住 CTRL + Shift，用鼠标划一下，自动生成一个过孔。&lt;/p&gt;
&lt;p&gt;设置过孔必须在放置前按 Tab，不要点了左键之后再设。&lt;/p&gt;
&lt;p&gt;过孔尺寸：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;信号线理论最小过孔 0.254，铜盘直径取两倍
但实际做法：比板厂最小工艺大一点点
板厂最小 0.3 时，尽量调到 0.35，不去挑战工艺极限
习惯：信号线过孔孔径取 0.35mm 左右
      铜盘直径取两倍或两倍多一点，例如 0.75mm
&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;p&gt;勾上后板厂会强制给过孔盖油，整板就不是镂空状态。&lt;/p&gt;
&lt;h2&gt;铺铜&lt;/h2&gt;
&lt;h3&gt;操作步骤&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 点铺铜 → 按 Tab
② 第一步先选层（例如 Top Layer）
③ Net 选网络：铺铜一般铺 GND
④ 选择「Pour Over Same Net Objects」：
   含义：铺铜时会把同网络的走线也一起铺上
   若不选：走了一根 GND 线再铺铜，那根线会和铺铜不连接
⑤ 下面两个勾直接勾上，特别是第一个勾 = 自动删除死铜
⑥ 选好区域 → 得到一层铜
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;死铜：铺完铜后与外部没有连接关系的那块铜。&lt;/p&gt;
&lt;h3&gt;底层铺铜&lt;/h3&gt;
&lt;p&gt;快捷方法：选中该铜 → CTRL+C → CTRL+V 粘贴 → 选中其中一块铜 → 把 Layer 改成 Bottom Layer → 右键 → 铺铜操作 → 所有铜重铺。&lt;/p&gt;
&lt;h3&gt;铺铜连接方式&lt;/h3&gt;
&lt;p&gt;现象：GND 网络默认是十字形连接。&lt;/p&gt;
&lt;p&gt;规则 → Plane → 铺铜连接方式规则 → 三种方式：① 十字架连接 ② 直接铺过去 ③（第三种）。&lt;/p&gt;
&lt;p&gt;小信号、小电流器件仍用十字连接，不要直接铺过去：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;直接铺一层铜过去后，引脚散热太好，焊接困难
其他引脚 330℃ 一怼就焊好，这个引脚可能要怼 2 秒烙铁才化锡
十字连接散热相对差 → 好焊接，且对小功率电路性能没有影响
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;进一步细分（高级设置）：改成十字后，GND 过孔也变成十字了，而过孔希望直接铺过去。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;在规则里点「高级」→ 三种连接方式分别独立设置：
  通孔焊盘（插件焊盘）→ 十字连接
  SMD 焊盘（贴片焊盘）→ 按需
  过孔（走线过孔）→ 直接铺过去
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;多打过孔（降低 GND 回流阻抗）&lt;/h3&gt;
&lt;p&gt;原因：顶层和底层都铺了铜，上下层 GND 靠过孔连接，所以到处都要打过孔让回流路径阻抗更小。&lt;/p&gt;
&lt;p&gt;快捷办法：工具 → 缝合孔/屏蔽 → 给网络添加缝合孔 → 点约束区区域 → 框选整个区域：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;格点调大一点，设成 10mm（本电路电流很小，孔不要打太密）
网络选 GND
「强制盖油」的勾一定要打上
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;Keep-Out Layer 外的处理&lt;/h3&gt;
&lt;p&gt;勾上「自动去除死铜」→ 重新铺一次 → 中间那块就没了。&lt;/p&gt;
&lt;p&gt;最终效果：布线完成 + 铺铜后，所有 GND 都会自动连接起来。&lt;/p&gt;
&lt;h2&gt;PCB 检查与打板&lt;/h2&gt;
&lt;h3&gt;加文字/标签&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 先把层调到丝印层 Top Overlay
② 点文字 → 按 Tab → 打字
③ 字体要求：必须选宋体等中文字体，选其他英文字体会显示不出文字
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;丝印最终调整（打板前必须做）&lt;/h3&gt;
&lt;p&gt;3D/2D 切换：按 3 键切 3D、按 2 键切回 2D。&lt;/p&gt;
&lt;p&gt;因为前面把丝印调得非常小，最后要调回正常：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 先把其中一个器件的丝印调为正常状态
② 点它 → 查找相似对象 → 应用
③ 把 Text Height 全部改成 1mm（1mm 打出来就已经很清晰）
④ 位置改成 center above（器件顶部）→ 所有丝印都跳到器件顶部
⑤ 切到 3D 手动逐个挪，把被遮挡的挪到可见位置
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;DRC 检查&lt;/h3&gt;
&lt;p&gt;工具 → 设计规则检查（DRC）→ 运行。&lt;/p&gt;
&lt;p&gt;可以忽略的警告：Thing to Solder Mask（丝印与阻焊层太近，实际不管也没问题）、丝印到丝印之间的距离（只要看得清就没关系）。&lt;/p&gt;
&lt;p&gt;绝对不能忽略的：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;未连接（Un-Routed Net）——「没有连线」这个警告非常非常严重
短路
线与线之间的距离（间距不足）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他规则的报错含义，复制到百度搜索，答案很多。&lt;/p&gt;
&lt;h3&gt;打板&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;常见平台：嘉立创、华秋、捷配等
流程：把 PCB（或压缩文件）拖到打板界面，按默认信息一路填下去
打板回来后买元器件焊上去，慢慢调
&lt;/code&gt;&lt;/pre&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;原理图库格点&lt;/td&gt;
&lt;td&gt;50mil（须与原理图一致或 2 倍关系）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;原理图引脚长度&lt;/td&gt;
&lt;td&gt;100mil&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PCB 焊盘尺寸&lt;/td&gt;
&lt;td&gt;长 2mm × 宽 0.5mm，圆形&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;引脚中心距&lt;/td&gt;
&lt;td&gt;1.27mm；两排中心距 8.2mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PCB 边框&lt;/td&gt;
&lt;td&gt;100 × 65mm（Mechanical 1 层）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安装孔&lt;/td&gt;
&lt;td&gt;焊盘/钻孔均 3.5mm（装 M3 螺丝）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;安装孔坐标&lt;/td&gt;
&lt;td&gt;(7, −15)、(7, −15)、(50, 13)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;器件间距规则&lt;/td&gt;
&lt;td&gt;默认 0.254mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;线宽规则&lt;/td&gt;
&lt;td&gt;最小/优选 0.254，最大 0.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;电源类 VCC 线宽&lt;/td&gt;
&lt;td&gt;0.35mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;过孔&lt;/td&gt;
&lt;td&gt;孔径 0.35mm、铜盘 0.75mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;缝合孔格点&lt;/td&gt;
&lt;td&gt;10mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;丝印文字高度&lt;/td&gt;
&lt;td&gt;最终调回 1mm，字体选宋体&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;铺铜网络&lt;/td&gt;
&lt;td&gt;GND，勾「移除死铜」&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;原理图库格点必须和原理图一致或成 2 倍关系；引脚小白点必须朝外。&lt;/li&gt;
&lt;li&gt;PCB 封装引脚编号不能乱取，必须严格 1~8 对应；改完封装要刷新预览；别盲信网上下的封装。&lt;/li&gt;
&lt;li&gt;导入 PCB 必须先点「验证变更」再点「执行变更」，否则不报错。&lt;/li&gt;
&lt;li&gt;布局按最短电流路径，滤波电容靠近噪声源，标号用静态标注；过孔要勾盖油，小信号铺铜用十字连接，丝印用中文字体。&lt;/li&gt;
&lt;li&gt;DRC 里「未连接」「短路」绝不能忽略，丝印类警告可忽略。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>链表（二）C 实现与头部插入</title><link>https://me.622168.xyz/posts/data-structure/03-linked-list-c-head-insert/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/03-linked-list-c-head-insert/</guid><description>从建空表到插入第一个节点，以及「绝不修改 head」这条纪律和 head 该放哪里的两条路线。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p5（13 分钟）、p6（12 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;前置知识&lt;/h2&gt;
&lt;p&gt;需要实用层面的 C/C++ 指针知识 + 动态内存分配概念。&lt;/p&gt;
&lt;h2&gt;建一个节点&lt;/h2&gt;
&lt;p&gt;这一段的链表逻辑视图：3 个节点，地址分别是 200 / 100 / 300。&lt;/p&gt;
&lt;p&gt;分配给每个节点的内存块地址完全是随机的，彼此没有关系，不能保证按升序降序排列或相邻，正因如此才需要保留链接。&lt;/p&gt;
&lt;h3&gt;NULL 是什么&lt;/h3&gt;
&lt;p&gt;NULL 是「地址零的同义词或宏定义」，零是无效地址。&lt;/p&gt;
&lt;p&gt;一个等于零或空值的指针变量，意味着该指针变量不指向有效的内存位置。&lt;/p&gt;
&lt;h3&gt;C 结构体定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int data;
    struct node *link;      /* 也可叫 next */
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C 风格必须写 &lt;code&gt;struct node *&lt;/code&gt;；C++ 直接写 &lt;code&gt;node *&lt;/code&gt;。C++ 的写法看起来更干净。&lt;/p&gt;
&lt;h3&gt;链表的标识&lt;/h3&gt;
&lt;p&gt;始终持有一个「指向节点的指针」变量，存第一个节点（头节点）地址。示例变量名 A。&lt;/p&gt;
&lt;p&gt;这个指向头节点或第一个节点的特定指针变量的名称，也可理解为链表的名称。&lt;/p&gt;
&lt;h3&gt;建空表两步&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node *A;    /* 声明一个指向节点的指针 */
A = NULL;          /* 未指向任何地方 */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;创建并插入第一个节点（顺序必须对）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* ① malloc 创建内存块（运行期分配的动态内存） */
temp = (struct node *)malloc(sizeof(struct node));

/* ② 填数据、调链接 */
(*temp).data = 2;
(*temp).link = NULL;

/* ③ 最后一步：让头指针指向它 */
A = temp;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;malloc&lt;/code&gt; 返回的是 void 指针，必须做类型转换（因为 temp 是 &lt;code&gt;node *&lt;/code&gt;）&lt;/li&gt;
&lt;li&gt;处理动态内存的唯一方式是通过指针来引用此内存位置&lt;/li&gt;
&lt;li&gt;temp 的作用是暂时存储节点的地址，直到我们恰当地固定所有的链接；之后 temp 可以挪作他用&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;箭头语法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;(*temp).data   等价于   temp-&amp;gt;data
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;箭头 = 一个连字符 + 一个右尖括号。箭头写法更为常见。&lt;/p&gt;
&lt;h3&gt;C++ 建节点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;node *temp = new node;      /* 也可用 malloc */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;new 操作符总是比 malloc 更受青睐，用 C++ 推荐 new。&lt;/p&gt;
&lt;h2&gt;遍历到链表末尾的通用逻辑&lt;/h2&gt;
&lt;p&gt;这是全课出现频率最高的一段代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;temp1 = head;
while (temp1-&amp;gt;link != NULL)     /* 对最后一个节点，link == NULL 为假 → 退出 */
    temp1 = temp1-&amp;gt;link;
temp1-&amp;gt;link = temp;             /* 挂上新节点 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印元素只需把循环体换成 &lt;code&gt;printf(&quot;%d &quot;, temp1-&amp;gt;data);&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;一条核心纪律&lt;/h3&gt;
&lt;p&gt;我们没有直接用变量 A 本身去遍历链表，因为如果修改 A 就会丢失头节点的地址。&lt;/p&gt;
&lt;p&gt;A 永远不会被修改，无论哪个变量存储着头节点的地址都绝不会被修改，只有这些临时变量会被修改以便遍历链表。&lt;/p&gt;
&lt;p&gt;所有链表代码都遵守这条纪律。&lt;/p&gt;
&lt;h2&gt;头部插入&lt;/h2&gt;
&lt;p&gt;这一段是一个可运行的完整 C 程序。&lt;/p&gt;
&lt;h3&gt;节点定义与全局 head&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int data;
    struct node *link;
};

struct node *head;      /* 声明在所有函数之外（全局） */
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;main 流程&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;head = NULL;
打印「多少个数字」→ 读入 N
循环 N 次：
    读入 X
    insert(X)
    print()
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;insert 实现顺序&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* ① malloc 创建节点，用 temp 接收（假定地址 100） */
temp = (struct node *)malloc(sizeof(struct node));

/* ② 填数据 */
temp-&amp;gt;data = x;

/* ③ 链接字段先置空 */
temp-&amp;gt;next = NULL;

/* ④ 分两种情况 */
if (head == NULL) {
    head = temp;                    /* 链表为空：头指针指向新节点 */
} else {
    temp-&amp;gt;next = head;              /* 先让新节点指向原头节点 */
    head = temp;                    /* 再让头指针指向新节点 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;一个重要简化&lt;/h3&gt;
&lt;p&gt;第 ③ 行的 &lt;code&gt;temp-&amp;gt;next = NULL&lt;/code&gt; 只有当链表为空时才用得到。&lt;/p&gt;
&lt;p&gt;因为空表时 head 本身就是 NULL，所以只写 &lt;code&gt;temp-&amp;gt;next = head&lt;/code&gt; 这一条语句就已经涵盖了链表为空的情形，从而避免写两条语句。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;/* 简化版：一条语句覆盖两种情况 */
temp-&amp;gt;next = head;
head = temp;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;print 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void print() {
    struct node *temp;              /* C 里必须写 struct node * */
    temp = head;
    while (temp != NULL) {
        printf(&quot;%d &quot;, temp-&amp;gt;data);
        temp = temp-&amp;gt;next;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要用临时变量：不想修改头指针，否则会丢失第一个节点的引用。&lt;/p&gt;
&lt;p&gt;在 C 里老是容易漏掉 &lt;code&gt;struct node *&lt;/code&gt; 的写法。&lt;/p&gt;
&lt;h3&gt;运行结果&lt;/h3&gt;
&lt;p&gt;输入 5 个数，依次插入 2、5、8、1、10，每次插入后打印：&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;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8 5 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1 8 5 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;&lt;code&gt;10 1 8 5 2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这似乎能行得通。&lt;/p&gt;
&lt;h2&gt;head 不是全局时会怎样&lt;/h2&gt;
&lt;p&gt;若 head 在 main 内声明为局部变量，其他函数访问不到 head，必须把首节点地址作为参数传进去。&lt;/p&gt;
&lt;h3&gt;print 函数&lt;/h3&gt;
&lt;p&gt;参数命名为 head（也可以是 A、temp 等）。&lt;/p&gt;
&lt;p&gt;这个参数 head 是 print 的局部变量，与 main 里的 head 是两个不同的变量；main 调用时是把值复制过去。&lt;/p&gt;
&lt;p&gt;因此在 print 里可以不用临时变量，直接用这个参数 head 遍历——这里并没有修改这个 head。&lt;/p&gt;
&lt;h3&gt;insert 函数&lt;/h3&gt;
&lt;p&gt;head 参数只是一份副本，但修改链表后 main 里的 head 也应当被修改。两种处理办法：&lt;/p&gt;
&lt;p&gt;路线一：返回指针&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct node *insert(struct node *head, int x) {
    ...
    return head;        /* 返回修改后的 head */
}

/* main 里： */
head = insert(head, x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;路线二：按引用传递（双重指针）&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void insert(struct node **head, int x) {
    ...
}

/* main 里： */
insert(&amp;amp;head, x);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 head 本身就是「指向节点的指针」，所以插入函数要接收指向指针节点的双重指针（&lt;code&gt;struct node **head&lt;/code&gt;），函数内到处要写解引用，返回类型为 void。&lt;/p&gt;
&lt;p&gt;两条路线都实际跑通并展示过。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;绝不修改 head / A 本身，只用临时变量遍历；丢了头指针整条链就找不回来了。&lt;/li&gt;
&lt;li&gt;malloc 必须做类型转换（C 里），否则编译器警告；C 里声明指针必须写 &lt;code&gt;struct node *&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;temp-&amp;gt;next = NULL&lt;/code&gt; 在链表非空时是多余的，只写 &lt;code&gt;temp-&amp;gt;next = head&lt;/code&gt; 就能覆盖两种情况。&lt;/li&gt;
&lt;li&gt;head 放全局还是传参要提前想清楚；传参就必须用「返回指针」或「双重指针」之一。&lt;/li&gt;
&lt;li&gt;传参时 head 是副本，函数内改了不影响 main 里的；NULL 就是地址 0，不是随便一个特殊值。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>链表（三）任意位置插入与删除</title><link>https://me.622168.xyz/posts/data-structure/04-linked-list-insert-delete-position/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/04-linked-list-insert-delete-position/</guid><description>位置从 1 开始、循环跑 n−2 次、删除必须显式释放——三个最容易写错的点。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p7（15 分钟）、p8（12 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;先记住两条约定&lt;/h2&gt;
&lt;h3&gt;位置编号从 1 开始&lt;/h3&gt;
&lt;p&gt;位置编号是 1-based。这和前面数组、动态列表的 0-based 下标是两套约定，别混。&lt;/p&gt;
&lt;h3&gt;合法位置的范围&lt;/h3&gt;
&lt;p&gt;以 3 个节点的链表为例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;位置 1    →  在开头插入
位置 2、3 →  中间插入
位置 4    →  在末尾插入
位置 5    →  无效位置
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;长度为 n 的链表，最大合法插入位置是 n+1。&lt;/p&gt;
&lt;p&gt;为了简化实现，假定调用者总是给出有效位置，不处理无效或错误情况。&lt;/p&gt;
&lt;h2&gt;任意位置插入&lt;/h2&gt;
&lt;h3&gt;核心思路&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;想在「第 n 个位置」插入
→ 先去到「第 (n-1) 个节点」
→ ① 先把新节点的链接字段设为第 (n-1) 个节点的链接字段（建立新链路）
→ ② 再把第 (n-1) 个节点的链接设为新节点的地址（断开旧链路）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺序不能反：先建新链路，再断旧链路。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int data;
    struct node *next;
};

struct node *head;      /* 全局变量 */

void insert(int data, int n) {
    struct node *temp1 = new node;      /* C++ 写法 */
    temp1-&amp;gt;data = data;
    temp1-&amp;gt;next = NULL;

    /* 特殊情况：插到开头 */
    if (n == 1) {
        temp1-&amp;gt;next = head;
        head = temp1;
        return;                          /* 必须 return */
    }

    /* 其他情况 */
    struct node *temp2 = head;
    for (int i = 0; i &amp;lt; n - 2; i++) {    /* 循环 n-2 次 */
        temp2 = temp2-&amp;gt;next;
    }
    temp1-&amp;gt;next = temp2-&amp;gt;next;           /* ① 先建新链路 */
    temp2-&amp;gt;next = temp1;                 /* ② 再断旧链路 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环次数是 n−2&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;我们要走到「第 (n-1) 个节点」
但 temp2 一开始就指向表头（第 1 个节点）
所以只需要再走 (n-1) - 1 = n-2 次
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;n = 1  →  走特殊分支，不进循环  →  插到开头
n = 2  →  循环 0 次  →  第 (n-1) 个节点就是第 1 个（head）
n = 3  →  循环 1 次  →  走到第 2 个节点
n = 4  →  循环 2 次  →  走到第 3 个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写成 n−1 次会多走一步，越过目标节点。&lt;/p&gt;
&lt;h3&gt;n == 1 这一支天然覆盖空表&lt;/h3&gt;
&lt;p&gt;空表时 &lt;code&gt;head == NULL&lt;/code&gt;，&lt;code&gt;temp1-&amp;gt;next = head&lt;/code&gt; 写入的就是 NULL，然后 return。所以链表为空不用单独处理。&lt;/p&gt;
&lt;h2&gt;内存分区&lt;/h2&gt;
&lt;p&gt;应用程序内存通常分四个部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;代码 / 指令段     所有需要执行的指令                        编译时固定
全局变量段        整个程序运行期间都存在的全局变量          编译时固定
栈                函数调用执行的所有信息 + 所有局部变量     编译时固定
堆 / 自由存储空间  运行时请求的内存（malloc / new）          不固定
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前三个部分大小固定，在编译时确定。&lt;/p&gt;
&lt;p&gt;head 是全局变量，位于全局变量段，而且任何人都能访问这个变量。&lt;/p&gt;
&lt;h3&gt;栈帧&lt;/h3&gt;
&lt;p&gt;main 开始执行 → 栈中分配一块内存用于 main 的执行，所有局部变量和执行状态保存在此。这块内存叫函数的栈帧。&lt;/p&gt;
&lt;p&gt;调用 insert → main 暂停，insert 进栈；insert 有 2 个参数 + 若干局部变量，栈帧会稍大一点。&lt;/p&gt;
&lt;h3&gt;堆和指针&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;new&lt;/code&gt; 在堆中创建内存块。用 new 或 malloc 在堆上要内存时，拿不到变量名，访问它的唯一方式是通过一个指针变量——这个指针变量就像遥控器。&lt;/p&gt;
&lt;h3&gt;函数返回时&lt;/h3&gt;
&lt;p&gt;每次函数执行完毕，所有局部变量都会从内存中消失，栈帧的分配与特定的调用相对应。&lt;/p&gt;
&lt;p&gt;但堆里的节点不会被清除，除非显式 free / delete。&lt;/p&gt;
&lt;h2&gt;任意位置删除&lt;/h2&gt;
&lt;h3&gt;删除要做两件事&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 修正链接，使该节点不再是列表的一部分
② 释放该节点所占的空间
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只修链接不够。修链接只是把节点从链表里断开，让它没法再被访问，但它仍占着内存。&lt;/p&gt;
&lt;p&gt;节点是从动态内存、也就是堆区分配的，在 C/C++ 里用完这块内存后必须显式释放，因为它不会自动被回收。&lt;/p&gt;
&lt;p&gt;内存是关键资源，不再需要时不该白白占着。只有释放了，这个节点才真正从内存里消失。&lt;/p&gt;
&lt;h3&gt;逻辑（一般情况）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;要去第 n 个节点，得先到「第 (n-1) 个节点」
把第 (n-1) 个节点的链接设为「第 n 个节点的链接」（即第 n+1 个节点）
→ 切断这条链接
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;这里一共写了三个函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert(value)&lt;/code&gt;：总是把值插到列表末尾&lt;/li&gt;
&lt;li&gt;&lt;code&gt;print()&lt;/code&gt;：打印所有元素&lt;/li&gt;
&lt;li&gt;&lt;code&gt;delete(n)&lt;/code&gt;：接受要删除节点的位置 n&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;void deleteNode(int n) {
    struct node *temp1 = head;

    /* 特殊情况：删除头节点 */
    if (n == 1) {
        head = temp1-&amp;gt;next;         /* 头节点移到第二个 */
        free(temp1);                /* C++ 用 delete temp1; */
        return;                     /* 必须 return 或加 else */
    }

    /* 一般情况 */
    for (int i = 0; i &amp;lt; n - 2; i++) {   /* 循环 n-2 次，走到第 n-1 个 */
        temp1 = temp1-&amp;gt;next;
    }
    struct node *temp2 = temp1-&amp;gt;next;   /* temp2 指向第 n 个节点 */
    temp1-&amp;gt;next = temp2-&amp;gt;next;          /* 修正链接：指向第 n+1 个 */
    free(temp2);                        /* 释放 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;释放方式要对应分配方式：&lt;code&gt;malloc&lt;/code&gt; 配 &lt;code&gt;free&lt;/code&gt;，&lt;code&gt;new&lt;/code&gt; 配 &lt;code&gt;delete&lt;/code&gt;，别配错。&lt;/p&gt;
&lt;h3&gt;那个 return 的由来&lt;/h3&gt;
&lt;p&gt;对于 n != 1 的情况，不应执行删除头节点这段代码，所以要在此之后加一个 else，或者在针对该条件执行完语句后加上 return。少了它，删完头节点还会继续往下跑。&lt;/p&gt;
&lt;h3&gt;运行验证&lt;/h3&gt;
&lt;p&gt;初始列表 2 4 6 5：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;删除位置 1          →  4 6 5   ✓
删除位置 4（数字 5） →  2 4 6   ✓
删除位置 2          →  2 6     ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全部正确。&lt;/p&gt;
&lt;h3&gt;内存模拟（删除位置 3）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;temp1 = head（地址 100）
n = 3 → 循环恰好执行 1 次 → temp1 移到地址 200（第 2 个节点）
temp2 = temp1-&amp;gt;next = 150（第 3 个节点）
temp1-&amp;gt;next = temp2-&amp;gt;next → 150 变成 250
free(temp2) → 地址 150 的内存块被删除
函数结束，temp1、temp2 都被清除
最终地址 250 的节点成为第 3 个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;head 是全局变量，所以不会被清除。&lt;/p&gt;
&lt;h2&gt;插入 vs 删除&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;走到哪&lt;/td&gt;
&lt;td&gt;第 (n-1) 个节点&lt;/td&gt;
&lt;td&gt;第 (n-1) 个节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;循环次数&lt;/td&gt;
&lt;td&gt;n−2&lt;/td&gt;
&lt;td&gt;n−2&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;n==1 特殊处理&lt;/td&gt;
&lt;td&gt;&lt;code&gt;temp1-&amp;gt;next = head; head = temp1;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;head = temp1-&amp;gt;next; free(temp1);&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;位置是 1-based，长度为 n 的链表最大合法插入位置是 n+1。&lt;/li&gt;
&lt;li&gt;循环次数是 n−2 次，不是 n−1。这是最容易写错的地方。&lt;/li&gt;
&lt;li&gt;插入要「先建新链路，再断旧链路」，顺序反了会丢节点。&lt;/li&gt;
&lt;li&gt;删除只修链接不够，必须 &lt;code&gt;free&lt;/code&gt; / &lt;code&gt;delete&lt;/code&gt;，否则内存泄漏。&lt;/li&gt;
&lt;li&gt;删头节点之后必须 return 或加 else。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>双向链表</title><link>https://me.622168.xyz/posts/data-structure/06-doubly-linked-list/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/06-doubly-linked-list/</guid><description>多一个 prev 指针换来什么，以及一个真实的 bug：为什么节点必须建在堆上。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p12（7 分钟）、p13（15 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;先明确一个术语&lt;/h2&gt;
&lt;p&gt;说「链表」的时候，默认指的是那种也可以称为单链表的列表。前面所有实现都是单链表，提到双向链表时要显式说明。&lt;/p&gt;
&lt;h2&gt;head 到底是什么&lt;/h2&gt;
&lt;p&gt;叫 head 的这个变量只是指向头节点的指针。理想情况下本该给它取个像 HEADPOINTER 这样的名字——它仅仅指向头节点，并非头节点本身；头节点就是链表中的第一个节点。&lt;/p&gt;
&lt;p&gt;head 是指针变量，头节点是第一个节点本身。这是两个东西。&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;每个节点有两个链接：一个指向下一节点，一个指向前一节点。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct node {
    int data;
    struct node *next;
    struct node *prev;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑表示：节点画成三个格子。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+--------+--------+--------+
|  prev  |  data  |  next  |
+--------+--------+--------+
   前一个   数据    下一个
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：3 个节点在地址 400 / 600 / 800。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;400  →  next 600，prev 0
600  →  next 800，prev 400
800  →  next 0，  prev 600
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于第一个节点没有前一个节点，所以最左边单元格将会是 0 或者为空。&lt;/p&gt;
&lt;h2&gt;优点&lt;/h2&gt;
&lt;h3&gt;单个指针就能看到前、当前、后三个节点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;temp 指向某节点（值为 600）
temp-&amp;gt;next 是 800
temp-&amp;gt;prev 是 400
&lt;/code&gt;&lt;/pre&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;h2&gt;缺点&lt;/h2&gt;
&lt;h3&gt;prev 指针要额外内存&lt;/h3&gt;
&lt;p&gt;典型架构（int 4 字节、指针 4 字节）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;单链表    4 数据 + 4 链接 = 8 字节
双向链表  4 数据 + 8 链接 = 12 字节
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于整数列表，用于链接的空间将是数据所用空间的两倍。&lt;/p&gt;
&lt;h3&gt;插入或删除时要重置更多链接&lt;/h3&gt;
&lt;p&gt;比起单向链表，需要重置更多几个链接，因此更容易出错。&lt;/p&gt;
&lt;h2&gt;实现&lt;/h2&gt;
&lt;h3&gt;局部变量和全局变量&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;定义位置  局部变量在函数内，全局变量在函数外
生存期    局部变量只管函数调用的期间，全局变量管应用程序的整个生命周期
函数结束  局部变量从内存中清除，全局变量仍然存在
可访问性  局部变量除非通过指针访问否则并非到处可访问；全局变量所有函数的任何地方都能访问
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前面的实现中，大多时候都把 head 声明为全局变量。&lt;/p&gt;
&lt;h3&gt;要写的函数&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert_at_head(x)&lt;/code&gt;：在链表起始位置插入，参数是一个整数&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert_at_tail&lt;/code&gt;：在链表尾端插入（最终没写）&lt;/li&gt;
&lt;li&gt;正向打印：从链表头至尾遍历打印&lt;/li&gt;
&lt;li&gt;反向打印：从链表尾到头遍历，按逆序打印&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;反向打印还有个额外作用：反向打印函数能验证每个节点的反向链接（prev）是否正确创建。它是 prev 指针的验证手段，不只是个演示。&lt;/p&gt;
&lt;h2&gt;节点必须建在堆上&lt;/h2&gt;
&lt;h3&gt;错误做法&lt;/h3&gt;
&lt;p&gt;如果像声明普通变量那样写：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;struct node newnode;        /* ← 错！这是栈上的局部变量 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它是作为局部变量创建的，函数调用结束时就会从内存中被清除。&lt;/p&gt;
&lt;p&gt;局部变量存于应用程序内存的栈区，生存期不受我们控制。要求是：除非明确地移除一个节点，否则它应当一直在内存中。&lt;/p&gt;
&lt;h3&gt;正确做法&lt;/h3&gt;
&lt;p&gt;必须在动态内存（堆区）创建节点：C 用 &lt;code&gt;malloc&lt;/code&gt;，C++ 用 &lt;code&gt;new&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;堆里的任何东西，除非我们显式释放，否则不会被清除。&lt;/p&gt;
&lt;h3&gt;一个真实的 bug&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;/* 错误版本 */
struct node *get_new_node(int x) {
    struct node newnode;        /* 栈上 */
    newnode.data = x;
    return &amp;amp;newnode;            /* 返回栈上变量的地址 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;会发生什么：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;假设返回地址 50
→ 一旦该函数结束，用于 get_new_node 的栈帧就会被回收
  所以现在即便你有地址 50，但那里已经没有节点了
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而且类型也不同：&lt;code&gt;newnode&lt;/code&gt; 是结构体节点类型；接收方要的是指向结构体节点的指针类型。&lt;/p&gt;
&lt;h3&gt;正确的 get_new_node&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node *get_new_node(int x) {
    struct node *new_node = malloc(sizeof(struct node));   /* 堆上创建 */
    new_node-&amp;gt;data = x;
    new_node-&amp;gt;next = NULL;
    new_node-&amp;gt;prev = NULL;
    return new_node;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们没法直接命名堆中的一段内存，访问堆中某物的唯一方式是通过指针；倘若丢失了这个指针，就会失去这个节点。&lt;/p&gt;
&lt;p&gt;为什么单独写一个函数：为了避免重复代码——既要在 &lt;code&gt;insert_at_head&lt;/code&gt; 中创建节点，过段时间也要在 &lt;code&gt;insert_at_tail&lt;/code&gt; 中创建节点。&lt;/p&gt;
&lt;p&gt;变量命名建议：用 &lt;code&gt;new_node&lt;/code&gt; / &lt;code&gt;new_node_pointer&lt;/code&gt; 之类，&lt;code&gt;temp&lt;/code&gt; 并不是一个非常有意义的名字。&lt;/p&gt;
&lt;h2&gt;insert_at_head 实现&lt;/h2&gt;
&lt;h3&gt;链表为空&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;head = new_node;
return;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例：插入数字 2，&lt;code&gt;get_new_node&lt;/code&gt; 在地址 400 创建新节点；&lt;code&gt;head = new_node&lt;/code&gt; 把地址 400 存入 head。&lt;/p&gt;
&lt;p&gt;函数执行完毕后变量 &lt;code&gt;new_node&lt;/code&gt; 从内存中被清除，但节点本身不会被清除。&lt;/p&gt;
&lt;h3&gt;链表不为空&lt;/h3&gt;
&lt;p&gt;示例再插入数字 4，节点在地址 600：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head-&amp;gt;prev    = new_node;   /* ① 把现有头节点的 prev 域设为新节点地址 */
new_node-&amp;gt;next = head;      /* ② 把新节点的 next 域设为当前头节点地址 */
head          = new_node;   /* ③ 头指针指向新节点 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;顺序的解释：首先把现有头节点的前一域设置为这个新节点的地址，也就是建立这条链接；然后把新节点的下一域设为当前头节点的地址；现在可以断开这条链路并构建这条链路，因此把头节点设置为新节点的地址。&lt;/p&gt;
&lt;p&gt;和单链表的区别：多了一行 &lt;code&gt;head-&amp;gt;prev = new_node&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;内存分区&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;代码 / 文本段   所有待执行的指令
全局变量段      全局变量
栈             所有局部变量 + 函数调用执行的所有信息
堆 / 动态内存   malloc / new 分配的内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栈就如同便签簿或白板之于函数调用。&lt;/p&gt;
&lt;p&gt;栈内存的分配与释放我们无法控制，它自动发生。&lt;/p&gt;
&lt;h2&gt;打印函数&lt;/h2&gt;
&lt;p&gt;正向打印与单链表的打印函数相同，用 &lt;code&gt;temp = temp-&amp;gt;next&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;反向打印要分两步：首先利用 next 指针到达列表的尾节点，然后再逆向遍历，用 &lt;code&gt;temp = temp-&amp;gt;prev&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;没实现的&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;insert_at_tail&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;删除函数（尽管开头说会写删除）&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;单链表 vs 双向链表&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;节点大小（int）&lt;/td&gt;
&lt;td&gt;8 字节&lt;/td&gt;
&lt;td&gt;12 字节&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;✗&lt;/td&gt;
&lt;td&gt;✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除所需指针数&lt;/td&gt;
&lt;td&gt;2 个&lt;/td&gt;
&lt;td&gt;1 个&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;多维护一条 prev&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;head 是指针，不是头节点。&lt;/li&gt;
&lt;li&gt;节点必须 malloc/new 到堆上，栈上的局部变量在函数返回后就没了。&lt;/li&gt;
&lt;li&gt;返回栈上变量的地址是经典 bug，而且类型也不对。&lt;/li&gt;
&lt;li&gt;双向链表每节点 12 字节（int 场景），链接空间是数据空间的两倍。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert_at_head&lt;/code&gt; 的三行顺序：先改 &lt;code&gt;head-&amp;gt;prev&lt;/code&gt;，再改 &lt;code&gt;new_node-&amp;gt;next&lt;/code&gt;，最后动 &lt;code&gt;head&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>栈</title><link>https://me.622168.xyz/posts/data-structure/07-stack/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/07-stack/</guid><description>栈 ADT、数组与链表两种实现（含边界条件写死），以及反转字符串、反转链表、括号匹配三个应用和它们的代价对比。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p14（8 分钟）、p15（12.7 分钟）、p16（10.6 分钟）、p17（15.8 分钟）、p18（13.7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;栈 ADT&lt;/h2&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;p&gt;一句话定义：&lt;strong&gt;只能从一端插入和删除的列表&lt;/strong&gt;，那一端就是栈顶。&lt;/p&gt;
&lt;p&gt;接口就四个：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;push     压入一个元素
pop      弹出最新的那个
top      只看栈顶，不移除
isEmpty  是否为空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;push/pop 是基本操作，top 和 isEmpty 通常也会有。没有 size / count 这类操作。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这四个操作全是 O(1)。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;LIFO：最后压进去的最先弹出来，所以叫「后进先出」。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;初始        空栈
push(2)     [2]
push(10)    [2, 10]
pop()       弹出 10  →  [2]
push(7)     [2, 7]
push(5)     [2, 7, 5]
top()       返回 5
isEmpty()   假
pop()       弹出 5
&lt;/code&gt;&lt;/pre&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;编辑器撤销（Ctrl+Z）&lt;/li&gt;
&lt;li&gt;编译器检查括号是否匹配&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;数组实现&lt;/h2&gt;
&lt;h3&gt;术语约定&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用数组 A 存栈，约定 A[0] 到 A[top] 这一段属于栈，其余是空闲
top 是「栈顶元素的索引」
空栈时 top = -1
数组大小由宏 MAX_SIZE 定义为 101
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;#define MAX_SIZE 101
int A[MAX_SIZE];
int top = -1;

void push(int x) {
    if (top == MAX_SIZE - 1) {          /* 溢出判断 */
        printf(&quot;Error: stack overflow\n&quot;);
        return;
    }
    A[++top] = x;                       /* 前置递增：先递增再赋值 */
}

void pop() {
    if (top == -1) {                    /* 空栈判断 */
        printf(&quot;Error: No element to pop\n&quot;);
        return;
    }
    top--;                              /* 本实现不返回元素，只递减 */
}

int Top() {
    return A[top];
}

int isEmpty() {
    if (top == -1) return 1;
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;A[++top] = x&lt;/code&gt; 用前置递增，等价于 &lt;code&gt;top++; A[top] = x;&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;判满条件是 &lt;code&gt;top == MAX_SIZE - 1&lt;/code&gt;（栈顶索引等于数组最高可用索引）&lt;/li&gt;
&lt;li&gt;pop 不返回元素，只是 &lt;code&gt;top--&lt;/code&gt;（有些语言的库会把 pop 与 top 合并，即 pop 同时移除并返回）&lt;/li&gt;
&lt;li&gt;数组与 top 声明为全局变量，函数内可直接访问，不必传参&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;演示运行：push 3、5、10 → pop 一次（10 出栈）→ push 12。输出与预期吻合。&lt;/p&gt;
&lt;p&gt;弹出后不必清空旧单元：不在栈内的单元格里是什么垃圾数据无所谓，下次 push 会覆盖。&lt;/p&gt;
&lt;h3&gt;局限与应对&lt;/h3&gt;
&lt;p&gt;栈占满整个数组时再 push 会溢出（overflow），push 失败。两种应对：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① push 检查并抛错（这并非很好的行为表现）
② 用动态数组：溢出时新建更大数组、复制旧内容、删除小数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;复制成本 O(n)，与栈中元素数成正比。&lt;/p&gt;
&lt;p&gt;最优扩容策略：新数组大小 = 旧数组的两倍。&lt;/p&gt;
&lt;p&gt;摊还分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N 次 push 总耗时与 N 成正比（O(n)）
除以 N → 均摊每次 push 为 O(1)
单次 push 最坏 O(n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「均摊 O(1)」和「单次最坏 O(n)」要一起说。&lt;/p&gt;
&lt;h2&gt;链表实现&lt;/h2&gt;
&lt;h3&gt;栈顶就是链表头部&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;尾部：插入要遍历到最后一个节点，删除要走到倒数第二个节点  →  O(n)  ✗
头部：只改常数个链接                                    →  O(1)  ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以：入栈 = 头部插入，出栈 = 头部删除。&lt;/p&gt;
&lt;p&gt;栈顶指针宁愿命名为 &lt;code&gt;top&lt;/code&gt;，而不是 head。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
    int data;
    struct Node *next;
};

struct Node *top = NULL;        /* 初始化为 NULL */

void push(int x) {
    struct Node *temp = malloc(sizeof(struct Node));
    temp-&amp;gt;data = x;
    temp-&amp;gt;next = top;
    top = temp;
}

void pop() {
    if (top == NULL) {          /* 通过 top 是否为空判断栈空 */
        printf(&quot;Error: No element to pop\n&quot;);
        return;
    }
    struct Node *temp = top;
    top = top-&amp;gt;next;
    free(temp);                 /* 必须释放 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演示：push 2（新节点地址 100）→ push 5（新节点地址 250，&lt;code&gt;top&lt;/code&gt; 变为 250）。&lt;/p&gt;
&lt;h3&gt;优势&lt;/h3&gt;
&lt;p&gt;不像数组实现，不必担心溢出（除非机器自身内存耗尽）。&lt;/p&gt;
&lt;p&gt;每个节点多用一点内存存地址，但按需分配、不用即释放，push/pop 更「优雅」。&lt;/p&gt;
&lt;p&gt;复杂度：push / pop 均为 O(1)。&lt;/p&gt;
&lt;h2&gt;应用一：反转字符串&lt;/h2&gt;
&lt;h3&gt;用栈的做法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 从左到右把每个字符压入字符栈
② 再从索引 0 开始，把栈顶字符写到数组当前位置，然后 pop
③ 循环直到栈空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 LIFO，弹出顺序与压入相反，于是完成反转。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void reverse(char C[], int n) {
    stack&amp;lt;char&amp;gt; S;
    for (int i = 0; i &amp;lt; n; i++) S.push(C[i]);
    for (int i = 0; i &amp;lt; n; i++) {
        C[i] = S.top();
        S.pop();
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对象是 C 风格字符串（以 &lt;code&gt;\0&lt;/code&gt; 结尾）；空字符只标记结尾，不属于字符串内容。&lt;/p&gt;
&lt;p&gt;运行验证：输入 &lt;code&gt;hello&lt;/code&gt;、&lt;code&gt;my code school&lt;/code&gt; 均正确。&lt;/p&gt;
&lt;p&gt;复杂度：两个循环各 O(n)，非嵌套（前后串行）→ 时间 O(n)；栈中额外空间与 n 成正比 → 空间 O(n)。&lt;/p&gt;
&lt;h3&gt;不用栈的更优解&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int i = 0, j = n - 1;
while (i &amp;lt; j) {
    swap(C[i], C[j]);
    i++;
    j--;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;时间 O(n)、空间 O(1)；交换次数约为 n/2。&lt;/p&gt;
&lt;p&gt;因为空间复杂度，双指针法优于栈法。用栈只适合输入量小、时间空间不敏感、图实现直观的场合。&lt;/p&gt;
&lt;h2&gt;应用二：反转链表&lt;/h2&gt;
&lt;p&gt;三种解法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;迭代     时间 O(n)  空间 O(1)
递归     时间 O(n)  空间 O(n)
显式栈   时间 O(n)  空间 O(n)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归用的是「隐式栈（implicit stack）」——没有显式创建栈，但仍在用栈，也就是函数调用栈。&lt;/p&gt;
&lt;h3&gt;显式栈算法&lt;/h3&gt;
&lt;p&gt;节点地址 100 → 150 → 250 → 300。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 用临时指针遍历链表，把「每个节点的指针/引用」压栈（压的是指针，不是数据）
② 全部压完后逐个弹出，得到的就是「逆序的节点引用」
③ temp = 栈顶地址；head = 该地址（此时 head = 300，temp 也指向 300）
④ 循环 while (!S.empty())：
     建立反向链接 temp-&amp;gt;next = 栈顶地址
     弹出
     temp = temp-&amp;gt;next（移动到下一个）
⑤ 循环退出后，最后一行 temp-&amp;gt;next = NULL
   ——把反转后最后一个节点的链接置空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：用栈使反转链表更简单；仅仅按逆序打印链表元素用栈尤其容易。&lt;/p&gt;
&lt;h2&gt;应用三：检查括号匹配&lt;/h2&gt;
&lt;p&gt;问题：给定只含 &lt;code&gt;()&lt;/code&gt;、&lt;code&gt;{}&lt;/code&gt;、&lt;code&gt;[]&lt;/code&gt; 的表达式字符串，判断括号是否平衡。括号内的内容无关紧要。这是编译器的常见任务。&lt;/p&gt;
&lt;h3&gt;平衡的定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;每个开符号都有对应且顺序正确的闭符号
类型必须匹配（{ 必须配 }，「两者不会相互抵消」）
每个开括号必须在右侧找到匹配的闭括号，每个闭括号必须在左侧找到匹配的开括号
一个括号只有当「其后打开的所有括号都已关闭」时才能闭合
&lt;/code&gt;&lt;/pre&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;)(       数量相等但不平衡
[ ( ] )  与 [ ( ) ] 数量完全相同，但前者不平衡
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;正确算法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;创建字符栈 S，从左到右扫描：
  若字符是 ( { [        →  push(S, c)
  否则若是三种闭符号之一 → 两种情况判失败：
                          ① 栈为空
                          ② 栈顶字符与该闭符号不成对
                          → 返回 false（不平衡，直接退出）
                          否则 pop(S)
扫描结束：栈空 → 平衡；栈不空 → 不平衡
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;三个用例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;以闭符号开头（如 `)`）：第一次进入 else 分支，栈为空 → 直接返回 false
`[ ( ] ...`：先压 `[`、再压 `(`，遇到 `]` 时栈顶是 `(`，本应是 `[` → false
`[ ( ) ]` 型：逐对配对弹出，扫描结束栈为空 → 平衡 ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么用栈&lt;/h3&gt;
&lt;p&gt;整个过程总是在列表的同一端插入/移除，最后进到列表里的都会最先出去，这正是栈的定义；栈的插入/删除是恒定时间。&lt;/p&gt;
&lt;p&gt;复杂度（这里没有明确给出，按流程推导）：每个字符一次 push/pop，均为 O(1) → 时间 O(n)、空间 O(n)。&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;push&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&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;需要新建 + 复制，均摊 O(1)&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;&lt;code&gt;top&lt;/code&gt; 索引（初值 −1）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;top&lt;/code&gt; 指针（初值 NULL）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数组实现：&lt;code&gt;top&lt;/code&gt; 初值 −1，判空 &lt;code&gt;top == -1&lt;/code&gt;，判满 &lt;code&gt;top == MAX_SIZE - 1&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;push 用 &lt;code&gt;A[++top] = x&lt;/code&gt;（前置递增），别写成 &lt;code&gt;A[top++]&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;这个实现的 pop 不返回元素，只是 &lt;code&gt;top--&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;链表实现：入栈/出栈都在头部，在尾部做就是 O(n)；栈顶指针叫 &lt;code&gt;top&lt;/code&gt;，不叫 head。&lt;/li&gt;
&lt;li&gt;括号匹配不能只数数量，反例是 &lt;code&gt;)(&lt;/code&gt; 和 &lt;code&gt;[(])&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>栈的应用 前缀 中缀 后缀</title><link>https://me.622168.xyz/posts/data-structure/08-stack-expression/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/08-stack-expression/</guid><description>三种记法的来历与互相转换、求值时弹出顺序的区别，以及中缀转后缀的完整规则（含括号处理）。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p19（13.1 分钟）、p20（13.6 分钟）、p21（17.6 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;基本概念&lt;/h2&gt;
&lt;h3&gt;术语&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;表达式由常量、变量、运算符、括号组成
操作数 = 被运算的对象/值
运算符 = 运算符或括号
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里只讨论二元运算符（恰好需要两个操作数的运算符）；操作数本身也可以是表达式。&lt;/p&gt;
&lt;h3&gt;三种记法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;中缀 infix    运算符在两个操作数中间   最常见的写法
前缀 prefix   运算符在操作数之前       波兰式 Polish notation，1924 年由一位波兰逻辑学家提出
后缀 postfix  运算符在操作数之后       逆波兰式 Reverse Polish，20 世纪 50 年代由计算机科学家提出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键性质：前缀/后缀中一个操作数只能与一个运算符关联，因此没有歧义，解析求值不需要优先级和结合性规则，也不需要括号。&lt;/p&gt;
&lt;p&gt;后缀最易解析、时间和内存成本最低，所以机器运算更偏好它。&lt;/p&gt;
&lt;p&gt;存储形式：以字符串保存时用空格或逗号等分隔符区分操作数与运算符。&lt;/p&gt;
&lt;h3&gt;优先级与结合性&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;1（最高）   括号 ( )         —
2           指数 ^           右结合（从右往左）
3           乘法 *、除法 /    左结合（从左往右）
4           加法 +、减法 -    左结合（从左往右）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一优先级之间的冲突由「结合性（associativity）」解决：从左到右 = 左结合；从右到左 = 右结合。&lt;/p&gt;
&lt;p&gt;判定步骤：先看优先级，再看结合性。&lt;/p&gt;
&lt;h3&gt;手算例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;4 + 6 * 2      先乘后加 = 16      先加得 20（错）
2^3^2          右结合 = 512       先算左边 = 64（错）
只含加减、同级左结合   3          先做加法得 1（错）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;4 + 6 * 2&lt;/code&gt; 想先加就必须写成 &lt;code&gt;(4 + 6) * 2&lt;/code&gt;，因为括号优先级最高。&lt;/p&gt;
&lt;h3&gt;三种记法的互相改写&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;2 + 3      →  前缀 + 2 3         后缀 2 3 +
P - Q      →  前缀 - P Q         后缀 P Q -
A + B * C  →  前缀 + A * B C     后缀 A B C * +
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;手动转换方法（通用）：按优先级从最内层开始一步步转换，中间步骤可加括号，全部完成后再去掉所有括号。&lt;/p&gt;
&lt;p&gt;括号提高人类可读性，但对机器而言去掉括号反而省下存储括号信息的内存；中缀最利于人，前缀/后缀最利于机器。&lt;/p&gt;
&lt;h2&gt;后缀求值&lt;/h2&gt;
&lt;h3&gt;手算规则&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;从左往右扫描，找「第一个出现的运算符」
运算符的操作数总在它左侧
即找首次出现的「操作数 操作数 运算符」模式
用它作用于紧邻的前两个操作数并化简表达式
重复到所有运算符处理完
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：中缀 &lt;code&gt;2*3 + 5*4 - 9&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;后缀串：2 3 * 5 4 * + 9 -
手算：先算 2*3 = 6
      再算 5*4 = 20
      再算 6+20 = 26
      最后 26-9 = 17
最终答案 17
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;算法&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;创建栈 S
循环 for i = 0 到 len-1，X[i] 是操作数或运算符：
    是操作数 → push 入栈
    是运算符 → 连续 pop 两次，把操作数的值存入变量（op1、op2）
              执行运算，结果再 push 回栈
扫描结束，栈中只剩一个元素 = 最终结果，返回栈顶
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;弹出顺序&lt;/h3&gt;
&lt;p&gt;后缀：第一次 pop 得到的是第二个操作数 &lt;code&gt;op2&lt;/code&gt;（右操作数），第二次 pop 才是 &lt;code&gt;op1&lt;/code&gt;（左操作数）。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;这个顺序对加法和乘法无所谓，但对减法和除法很重要&lt;/strong&gt;——&lt;code&gt;5 3 -&lt;/code&gt; 得是 &lt;code&gt;5−3&lt;/code&gt;，弹反了就成了 &lt;code&gt;3−5&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;效率&lt;/h3&gt;
&lt;p&gt;只需在表达式字符串上扫一遍，是高效算法。&lt;/p&gt;
&lt;p&gt;为什么能用栈：每次从同一侧插入操作数、又从同一侧取出，最后进来的最先出去 → LIFO。&lt;/p&gt;
&lt;h2&gt;前缀求值&lt;/h2&gt;
&lt;p&gt;扫描方向：从右往左。规则：是操作数 → push；是运算符 → pop 两个元素。&lt;/p&gt;
&lt;h3&gt;弹出顺序和后缀相反&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;前缀：第一次弹出的元素是第一个操作数（左）
      第二次弹出的是第二个操作数（右）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个顺序对加法和乘法无所谓，但对减法和除法很重要。&lt;/p&gt;
&lt;h3&gt;完整演算（与后缀同一个表达式）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;从右往左读：9、4、5
  遇 *  → 弹 5（op1）、弹 4（op2）→ 5*4 = 20 入栈
读 2、3
  遇 *  → 3*2 = 6 入栈
遇 +    → 弹出 20 和 6 → 26 入栈
遇 -    → 弹出 26 和 9 → 26-9 = 17 入栈
答案 17
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前缀求值还有别的做法，但这种最简单最直接。&lt;/p&gt;
&lt;h2&gt;中缀转后缀&lt;/h2&gt;
&lt;h3&gt;先看低效做法&lt;/h3&gt;
&lt;p&gt;按优先级手动逐步转换（先 &lt;code&gt;B*C → BC*&lt;/code&gt;，再处理加法，最后去括号）——也能在程序里运用这种逻辑，但效率不高、实现复杂。&lt;/p&gt;
&lt;h3&gt;两条关键观察&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 从中缀到后缀，操作数自左向右出现的相对顺序不变
② 运算符的顺序可能改变，而在后缀中运算符总是按它们应当被执行的顺序排列
&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;p&gt;口头规则：栈中任何比我们正在查看的运算符具有更高优先级的运算符都可以弹出并放入后缀表达式。&lt;/p&gt;
&lt;p&gt;但演算时同级的 &lt;code&gt;+&lt;/code&gt; 也被弹了出去，理由是加法和减法优先级相同，但左边先出现的会被优先考虑（左结合）。&lt;/p&gt;
&lt;p&gt;所以实操上等价于：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;左结合运算符  →  栈顶优先级 ≥ 当前运算符就弹出
右结合运算符  →  改为「严格大于」才弹出
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;「右结合用 &amp;gt;」这一点没有讲明，是从优先级表推导出来的。&lt;/p&gt;
&lt;p&gt;为什么要压栈：不确定是否已处理完它的右操作数；一旦遇到优先级更低的运算符，就标志着右操作数的边界。&lt;/p&gt;
&lt;h3&gt;伪代码骨架&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;infixToPostfix(X):
    S = 空栈
    res = &quot;&quot;
    for i in 0..len-1:
        if X[i] 是操作数:
            res += X[i]
        else:                                  /* 运算符 */
            while (!S.empty() &amp;amp;&amp;amp; S.top() 优先级更高):
                res += S.pop()
            S.push(X[i])
    while (!S.empty()):
        res += S.pop()
    return res
&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;① 遇到左括号 → 直接压栈

② 遇到右括号 → 一直弹出并追加，直到遇到左括号
               然后还要把那个左括号也弹掉
               （「针对这个括号，我们已经完成操作」）

③ 运算符规则的变化 → 弹出时「仅弹出直到遇到左括号为止」
                    因为左括号是「最后一个打开的括号的边界」
                    所以 while 条件里要多加一个「栈顶不是左括号」的判断
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;判断括号要用通用函数 &lt;code&gt;isOpening&lt;/code&gt; / &lt;code&gt;isClosing&lt;/code&gt;，因为还可能是 &lt;code&gt;{&lt;/code&gt;、&lt;code&gt;[&lt;/code&gt;，不能只写 &lt;code&gt;== &apos;(&apos;&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;演算例子&lt;/h3&gt;
&lt;p&gt;例 1（含两层括号）：后缀为 &lt;code&gt;A B + C * D E * -&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;例 2：中缀 &lt;code&gt;A * (B + C)&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A 是操作数 → 追加
* 直接进栈（栈空，无可比较）
遇左括号 → 直接进栈
B 追加
+ 时栈顶是左括号，不能往下查看，只能压入
C 追加
遇右括号 → 一直弹出到左括号，并把左括号也弹出
到表达式末尾 → 栈中剩余全部弹出

最终后缀：A B C + *
&lt;/code&gt;&lt;/pre&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;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;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;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;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;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;td&gt;从左往右&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;第一次 pop 是&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;op1（左）&lt;/td&gt;
&lt;td&gt;op2（右）&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;td&gt;机器（最优）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;优先级表：括号最高，指数其次，乘除再次，加减最低；指数是右结合，其余左结合。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;2^3^2 = 512&lt;/code&gt;，不是 64。&lt;/li&gt;
&lt;li&gt;后缀求值先弹右（op2）后弹左（op1）；前缀求值从右往左扫，先弹左（op1）后弹右（op2）。两者相反，最容易记混。&lt;/li&gt;
&lt;li&gt;中缀转后缀时，同级运算符也要弹（左结合），等价于「栈顶 ≥ 当前就弹」。&lt;/li&gt;
&lt;li&gt;右括号要把左括号也弹掉；运算符的 while 循环还要加「栈顶不是左括号」的条件，否则会把括号内的运算符全弹出去。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>队列</title><link>https://me.622168.xyz/posts/data-structure/09-queue/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/09-queue/</guid><description>FIFO、数组实现从线性到环形的推导、链表实现为什么要两个指针，以及所有边界条件的精确写法。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p22（9 分钟）、p23（14.4 分钟）、p24（13.7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;基本介绍&lt;/h2&gt;
&lt;p&gt;队列 = 现实里的排队：FIFO（先进先出）。栈是 LIFO，插入和移除都在同一端，也就是栈顶。&lt;/p&gt;
&lt;h3&gt;术语约定&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;插入必须从一端进行 → 称「后端 / 尾部」（rear / tail）
删除必须从另一端进行 → 称「前端 / 头部」（front / head）
&lt;/code&gt;&lt;/pre&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;enqueue&lt;/td&gt;
&lt;td&gt;插入（也有人叫 push）&lt;/td&gt;
&lt;td&gt;void&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dequeue&lt;/td&gt;
&lt;td&gt;删除（也有人叫 pop）&lt;/td&gt;
&lt;td&gt;通常返回被移除的元素&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;front（peek）&lt;/td&gt;
&lt;td&gt;只返回前端元素，不删除任何东西&lt;/td&gt;
&lt;td&gt;元素值&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isEmpty&lt;/td&gt;
&lt;td&gt;是否为空&lt;/td&gt;
&lt;td&gt;布尔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;isFull&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;命名上要留神：C++ 内置队列的插入函数叫 &lt;code&gt;push&lt;/code&gt;，C 里叫 &lt;code&gt;enqueue&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;以上所有操作都必须花费恒定时间，时间复杂度 O(1)。&lt;/p&gt;
&lt;h3&gt;逻辑视图&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;队列：两边开口的容器（元素从一侧入、从另一侧出）
栈：  单侧开口的容器
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;演示&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;空队列
enqueue 2  →  front 和 rear 指向同一个元素
enqueue 5  →  插到尾部
enqueue 一个
dequeue    →  取出 2（返回整数 2）
front      →  得到 5（不删除）
isEmpty    →  返回布尔值（0 假 / 1 真）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;应用场景&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;共享资源一次只能服务一个请求时，把请求排队最合理，先到的先服务

例：网络共享打印机（一次只打印一份，忙碌时请求进队列）
例：CPU / 处理器是共享资源，多个进程竞争处理器时间，进程被放进队列
一般用于模拟各种等待情形
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;数组实现&lt;/h2&gt;
&lt;p&gt;这一节的核心是「从线性到环形」的推导。&lt;/p&gt;
&lt;h3&gt;术语约定（线性数组版）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;用数组 A（例：10 个整数）
约定：从 front 索引到 rear 索引这一段属于队列，其余为空闲
front 与 rear 初始都设为 -1
空队列用 front == rear == -1 表示（-1 不是有效索引）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;哪一边是 front、哪一边是 rear 无关紧要，但元素必须总是从后端加入、从前端移除。&lt;/p&gt;
&lt;h3&gt;入队（线性版）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;若 rear == 数组最大可用索引 → 队列已满（用 isFull 判断）→ 直接 return
否则若队列为空 → 把 front 和 rear 都置为 0，再写入 A[rear] = x
否则 → rear++，然后 A[rear] = x
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;出队（线性版）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;若队列为空 → 打印/抛错并 return（不能移除）
否则若 front == rear（队列中只有一个元素）→ 把 front 和 rear 都设回 -1
否则（正常情况）→ 只需 front++
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;原来 front 指向的单元直接丢弃，值不必清理，因为入队时会覆盖。&lt;/p&gt;
&lt;p&gt;空队列、单元素队列这两种边界得单独处理——大多数漏洞都出在这儿。&lt;/p&gt;
&lt;h3&gt;线性实现的致命问题&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;演示：连续入队直到 rear == 最大索引
此时队列中还有两个未使用的空单元（在 front 左侧）
但无法再入队，操作失败

随着不断出队，front 左边的所有单元格再也不会被使用，只会被浪费掉
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;解决方案：环形数组&lt;/h3&gt;
&lt;p&gt;把数组想象成没有尽头：到最后一个索引后回到索引 0。这只是看待数组的一种逻辑方式。&lt;/p&gt;
&lt;p&gt;取模公式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;下一个位置 = (i + 1) % N
前一个位置 = (i + N - 1) % N      （加 N 是为了保证括号内表达式始终为正）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;验证：对除 &lt;code&gt;N-1&lt;/code&gt; 外的 i 取模无影响；当 &lt;code&gt;i = N-1&lt;/code&gt; 时 &lt;code&gt;(N-1+1) % N = 0&lt;/code&gt;（一个数除以自身的余数为 0）。&lt;/p&gt;
&lt;h3&gt;环形版三个关键条件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;判满： (rear + 1) % N == front
判空： front == rear == -1        （与线性版相同）
入队： 队列为空 → front 和 rear 都设为 0；否则 rear = (rear + 1) % N 再写值
出队： 空/单元素边界不变；否则 front = (front + 1) % N
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演示：当前 rear = 9、N = 10 → &lt;code&gt;(9+1)%10 = 0&lt;/code&gt; → 新 rear 为 0，在索引 0 处写入 15。&lt;/p&gt;
&lt;p&gt;判满条件隐含一个后果：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;始终保留一个空单元
→ 数组容量 N，实际只能存 N-1 个元素
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是环形队列的固有代价，不是 bug。&lt;/p&gt;
&lt;h3&gt;front 操作&lt;/h3&gt;
&lt;p&gt;直接返回 &lt;code&gt;A[front]&lt;/code&gt; 处的元素；需要先检查队列是否为空，只有 &lt;code&gt;front != -1&lt;/code&gt; 时才返回。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;以上所有函数都只做简单算术运算和赋值，没有循环，耗时与队列规模无关 → 全部 O(1)。&lt;/p&gt;
&lt;h2&gt;链表实现&lt;/h2&gt;
&lt;h3&gt;数组实现的两个局限&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 数组大小固定。用尽时只有两条路：
   拒绝插入（宣称队列已满），或新建更大数组并复制旧元素
   —— 复制成本高，为 O(n)

② 可能「数组足够大但队列并未充分利用」，大量内存被浪费
   （例：90% 内存未使用）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现代机器上这点浪费不是真问题，但设计算法时应当分析并理解这些影响。&lt;/p&gt;
&lt;h3&gt;为什么要维护两个指针&lt;/h3&gt;
&lt;p&gt;关键：队列的两端操作必须在链表的两端，且两个操作都必须是 O(1)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;若选头部入队 → 出队必须在尾部
若选尾部入队 → 出队必须在头部

单纯实现下，插入或删除必有一个是 O(n)
（从头部插入/移除是 O(1)，从尾部插入/移除是 O(n)，因为要遍历到尾节点）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解决办法：额外维护一个尾指针。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;存头节点地址的变量叫 front
始终存尾节点地址的变量叫 rear（也叫 tail）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是在尾部入队不必再遍历：直接改尾节点的地址域建立链接，再更新 &lt;code&gt;rear&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最终入队从尾部、出队从头部，两者都是 O(1)。&lt;/p&gt;
&lt;p&gt;任何插入或移除操作中都必须同时更新 front 和 rear。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct Node {
    int data;
    struct Node *next;
};

struct Node *front = NULL;      /* 不声明 head */
struct Node *rear  = NULL;

void enqueue(int x) {
    struct Node *temp = malloc(sizeof(struct Node));
    temp-&amp;gt;data = x;
    temp-&amp;gt;next = NULL;

    if (front == NULL &amp;amp;&amp;amp; rear == NULL) {    /* 队列为空 */
        front = rear = temp;
        return;
    }
    rear-&amp;gt;next = temp;                      /* 挂到尾部 */
    rear = temp;                            /* 更新尾指针 */
}

void dequeue() {
    struct Node *temp = front;              /* 先保存当前前端地址 */

    if (front == NULL &amp;amp;&amp;amp; rear == NULL) {    /* 队列为空 */
        printf(&quot;Error: queue is empty\n&quot;);
        return;
    }
    if (front == rear) {                    /* 只有一个元素 */
        front = rear = NULL;
    } else {
        front = front-&amp;gt;next;
    }
    free(temp);                             /* 必须释放 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;free(temp)&lt;/code&gt; 不能省。只让 front 前进是不够的，原前端节点仍留在动态内存中，而动态内存里的任何东西都必须被显式释放——这正是声明 temp 指针的原因。&lt;/p&gt;
&lt;h3&gt;演示&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;空队列 front/rear 都是 NULL（NULL 就是地址 0 的宏）
enqueue 2 → 新节点地址 100，进入 if 分支，front = rear = 100
            函数结束局部变量 temp 被清除
enqueue 4 → 新节点地址 200，走 rear-&amp;gt;next = temp; rear = temp;
enqueue 6 → 地址 300
dequeue   → temp 保存原 front，front 前移，再 free
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;函数里全是简单语句、没有循环 → 入队和出队都是 O(1)。&lt;/p&gt;
&lt;h3&gt;结论&lt;/h3&gt;
&lt;p&gt;链表实现没有数组实现的那些局限：不会满、不会浪费未用内存。代价只是每个节点多用一个指针域存地址，除此之外没有其他重大缺点。&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;入队&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;出队&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;容量&lt;/td&gt;
&lt;td&gt;固定（环形版实际 N−1）&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;&lt;code&gt;front&lt;/code&gt; / &lt;code&gt;rear&lt;/code&gt;（初值 −1）&lt;/td&gt;
&lt;td&gt;&lt;code&gt;front&lt;/code&gt; / &lt;code&gt;rear&lt;/code&gt;（初值 NULL）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判空&lt;/td&gt;
&lt;td&gt;&lt;code&gt;front == rear == −1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;front == NULL &amp;amp;&amp;amp; rear == NULL&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判满&lt;/td&gt;
&lt;td&gt;&lt;code&gt;(rear+1) % N == front&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;不需要&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;数组实现的 front / rear 初值都是 −1，判空是 &lt;code&gt;front == rear == −1&lt;/code&gt;，不是 &lt;code&gt;front == rear&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;环形判满是 &lt;code&gt;(rear + 1) % N == front&lt;/code&gt;，不是 &lt;code&gt;rear + 1 == front&lt;/code&gt;；而且始终浪费一个单元，容量是 N−1。&lt;/li&gt;
&lt;li&gt;单元素队列出队后要把 front 和 rear 都重置（数组版置 −1，链表版置 NULL）。&lt;/li&gt;
&lt;li&gt;链表实现必须维护尾指针，否则尾部操作是 O(n)；dequeue 必须 free，只移动 front 会内存泄漏。&lt;/li&gt;
&lt;li&gt;空队列和单元素队列必须单独处理。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>链表（四）反转与递归</title><link>https://me.622168.xyz/posts/data-structure/05-linked-list-reverse-recursion/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/05-linked-list-reverse-recursion/</guid><description>迭代反转的四行顺序、递归逆序打印，以及一个很多人写反的结论：常规遍历用递归远不如迭代。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p9（13 分钟）、p10（14 分钟）、p11（9 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;反转不是移动数据&lt;/h2&gt;
&lt;p&gt;反转不能靠搬数据——不能把地址 102 的那个值挪到地址 250 去，只能调整链接。&lt;/p&gt;
&lt;p&gt;输入链表 4 个节点（地址 100 / 200 / 150 / 250），输出应是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;head 指向地址 250 的节点
走向 250 → 150 → 200 → 100
且地址 100 的节点地址字段为 0 / 空
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关于 head 的措辞要抠一下：叫 head 的这个变量只是一个指针，并非头节点本身。除了头节点的地址，链表没有别的标识。&lt;/p&gt;
&lt;h2&gt;迭代反转&lt;/h2&gt;
&lt;h3&gt;两个关键观察&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 如果断开了当前节点的链接，怎么去到下一个节点？
   → 在设置当前节点的链接字段之前，先把下一节点的地址存到另一个临时变量里

② 在链表中，我们总是知道下一个节点的地址，但永远不会知道上一个节点的地址
   → 遍历时必须用另外一个变量来跟踪前一个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一条是「先存后断」的由来，第二条是为什么要 previous 变量。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;current  = head;
previous = NULL;                  /* 头节点的前一个节点为空 */

while (current != NULL) {
    next          = current-&amp;gt;next;    /* ① 先保存下一个节点地址 */
    current-&amp;gt;next = previous;         /* ② 反转链接 */
    previous      = current;          /* ③ previous 移到 current */
    current       = next;             /* ④ current 移到 next */
}

head = previous;                      /* 循环结束时 previous 存的是最后一个节点地址 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;四行的顺序不能变，尤其是 ①②：先存 &lt;code&gt;next&lt;/code&gt;，再改 &lt;code&gt;current-&amp;gt;next&lt;/code&gt;，否则就丢链了。&lt;/p&gt;
&lt;h3&gt;变量名 next 的歧义&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;next&lt;/code&gt; 是反转函数里的局部变量；而 &lt;code&gt;current-&amp;gt;next&lt;/code&gt; 这种表述指的是节点里的链接字段；单说 next 时，指的就是那个局部指针变量。两者是不同的东西，读代码时要注意。&lt;/p&gt;
&lt;h3&gt;边界测试&lt;/h3&gt;
&lt;p&gt;实现要能过所有用例，尤其要针对特殊和极端的用例验证。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 链表为空（head == NULL）
② 链表仅有一个节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;结论：这两种情况这个实现都能奏效。&lt;/p&gt;
&lt;h3&gt;完整程序&lt;/h3&gt;
&lt;p&gt;这个版本里 head 是 main 的局部变量，所以：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;reverse&lt;/code&gt; 函数以头节点地址作为参数，并返回反转后的头节点地址&lt;/li&gt;
&lt;li&gt;&lt;code&gt;insert&lt;/code&gt; 函数同样接收两个参数（头节点地址 + 待插入数据），并返回头节点地址（该地址可能被修改，也可能未被修改）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;演示：初始链表 2 4 6 8 → 打印 &lt;code&gt;2468&lt;/code&gt; → 反转 → 打印 &lt;code&gt;8642&lt;/code&gt;。&lt;/p&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;code&gt;print(head)&lt;/code&gt;：正序打印（元素以空格分隔）&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reverse_print(head)&lt;/code&gt;：逆序打印元素&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意这里不反转列表，只是按相反的顺序打印元素。这是和 p11 的关键区别。&lt;/p&gt;
&lt;h3&gt;print 实现&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void print(struct node *p) {
    if (p == NULL) return;      /* 递归退出条件 */
    printf(&quot;%d &quot;, p-&amp;gt;data);     /* 先打印当前节点 */
    print(p-&amp;gt;next);             /* 再递归下一个节点 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;递归最不能忘的是退出条件。递归不能无休止地调下去，这里 p 最终会等于 NULL，到那一步就不再递归调用，直接退出。&lt;/p&gt;
&lt;h3&gt;reverse_print 实现&lt;/h3&gt;
&lt;p&gt;只改一处：不是先打印再递归，而是先递归、等递归调用结束后再打印。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void reverse_print(struct node *p) {
    if (p == NULL) return;
    reverse_print(p-&amp;gt;next);     /* 先递归到底 */
    printf(&quot;%d &quot;, p-&amp;gt;data);     /* 回溯时打印 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归树&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;print(100) → 打印 → print(200) → 打印 → print(150) → 打印 → print(250) → print(NULL)
                                                                          ↑ 触达退出条件
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种画出来的结构叫递归树。&lt;/p&gt;
&lt;p&gt;逆序版：先一路递归到 NULL 才返回，然后从 print(250) 开始打印（4），再 print(150) 打印（5），再 6、2。&lt;/p&gt;
&lt;h3&gt;内存模型&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;所有函数调用执行的细节以及局部变量 → 栈区
通过 malloc / new 分配的任何内存   → 堆区
链表中节点的内存是从堆中分配的 → 这 4 个节点位于堆中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;每次调用一个函数，都会从栈中为该函数的执行分配一定量的内存，这块内存叫那个函数的栈帧。&lt;/p&gt;
&lt;p&gt;每次递归调用都会相应地分配一个栈帧。一个函数调用自己，和它调用另一个函数没有什么不同。栈顶的那个调用在执行。&lt;/p&gt;
&lt;h2&gt;常规遍历用递归不划算&lt;/h2&gt;
&lt;p&gt;常规遍历、常规打印，迭代方法会比递归方法高效得多。&lt;/p&gt;
&lt;p&gt;理由：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;迭代方法只会使用一个临时变量
而递归中针对如此多的函数调用，我们会在内存的栈区占用空间
→ 所以那里存在着对内存的隐性使用
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逆序打印就不一样了：反正总得把元素存储在某种结构里，所以用递归也还行。&lt;/p&gt;
&lt;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;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;这条容易写反，以为递归「更优雅所以更好」。结论是迭代更高效。&lt;/p&gt;
&lt;h2&gt;递归反转&lt;/h2&gt;
&lt;p&gt;p10 只是逆序打印，没有反转链表；这里才是真反转。&lt;/p&gt;
&lt;h3&gt;逻辑来源&lt;/h3&gt;
&lt;p&gt;直接借用了 p10 的反向打印递归函数——递归提供了一种在 C/C++ 程序中逆向遍历链表的方式。&lt;/p&gt;
&lt;p&gt;核心想法：递归先让我们正向遍历一遍列表，然后再反向遍历一遍。&lt;/p&gt;
&lt;h3&gt;实现&lt;/h3&gt;
&lt;p&gt;前提是 head 为全局变量，这样所有函数都能访问它。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;void reverse(struct node *p) {
    if (p-&amp;gt;next == NULL) {      /* 退出条件：最后一个节点 */
        head = p;               /* 修改头指针指向最后一个节点 */
        return;
    }
    reverse(p-&amp;gt;next);           /* 先递归到底 */

    /* 回溯时执行这三行 */
    struct node *q = p-&amp;gt;next;
    q-&amp;gt;next = p;                /* 反向链接 */
    p-&amp;gt;next = NULL;             /* 断开 p 原来的正向链接 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;关键理解&lt;/h3&gt;
&lt;p&gt;当 reverse(250) 完成的时候，直到 250 的节点已经反转了：head 正指向这个节点，而且此节点的链接部分被设为空。等来到 150 时，可以确定列表直到 150 都已反转。&lt;/p&gt;
&lt;p&gt;也就是说回溯过程中，每次调用结束时，从尾部到当前节点的这一段已经反转完成。&lt;/p&gt;
&lt;h3&gt;逐步走一遍&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;reverse(100) → reverse(200) → reverse(150) → reverse(250)
  → P 是 250，p-&amp;gt;next == NULL，进入退出条件
  → head = 250，对 250 的调用结束
→ 回到 reverse(150)：P = 150
  → q = P-&amp;gt;next = 250
  → q-&amp;gt;next = P      （把 250 的 next 设为 150）
  → P-&amp;gt;next = NULL
  → reverse(150) 结束，此时 head=250，250→150，150→空 ✓
→ 同理 reverse(200) 结束后到 200 反转
→ reverse(100) 结束后整体反转，返回 main
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;两个补充&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;① 语法糖 / 等价写法&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;q = p-&amp;gt;next;
q-&amp;gt;next = p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以合成一行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;p-&amp;gt;next-&amp;gt;next = p;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;意思是一样的，只不过这种表述更让人混淆。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;② head 非全局时&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这个反转函数就得返回修改后 head 的地址。&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;迭代反转&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;递归反转&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)（栈帧）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;迭代打印&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;递归打印&lt;/td&gt;
&lt;td&gt;O(n)&lt;/td&gt;
&lt;td&gt;O(n)（栈帧）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;迭代反转的四行顺序不能变：先存 next → 改链接 → 移 previous → 移 current。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;previous&lt;/code&gt; 初值是 NULL，这样第一个节点反转后才指向空。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next&lt;/code&gt;（局部变量）和 &lt;code&gt;current-&amp;gt;next&lt;/code&gt;（节点字段）是两回事，读代码时别混。&lt;/li&gt;
&lt;li&gt;常规遍历用递归是低效的，递归有栈帧的隐性内存开销；只有逆序打印用递归才划算。&lt;/li&gt;
&lt;li&gt;递归反转的退出条件是「最后一个节点」（&lt;code&gt;p-&amp;gt;next == NULL&lt;/code&gt;），不是「p == NULL」。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>树与二叉树</title><link>https://me.622168.xyz/posts/data-structure/10-tree-binary-tree/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/10-tree-binary-tree/</guid><description>树的基本术语、二叉树的三分类、每层节点数与高度公式，以及深度和高度的区别。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p25（15 分钟）、p26（16 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;树是什么&lt;/h2&gt;
&lt;p&gt;前面学的数组、链表、栈、队列都是线性结构——有逻辑起点和终点，每个元素有前驱后继。树不是：它是非线性结构，是层级结构。&lt;/p&gt;
&lt;h3&gt;选数据结构的四个考虑&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 要存什么
② 操作成本（尤其想压低「最坏」操作成本）
   例：频繁查找就把集合按有序形式存成数组，以便二分
③ 内存消耗
④ 易于实现（这一条未必是最佳取舍标准）
&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;p&gt;应用举例：公司组织层级（John CEO → 直接下属 Steve、Rama；Steve 管 Bob、Ella……）。&lt;/p&gt;
&lt;p&gt;树要倒过来看才像树，根在顶部向下分支。&lt;/p&gt;
&lt;p&gt;节点编号 1~10 只是为了方便指代，不表示任何顺序，根节点叫成 10 号节点也一样。&lt;/p&gt;
&lt;h3&gt;术语&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;根节点          最顶端节点，唯一没有父节点
子节点/父节点    直接链接关系
兄弟节点        有同一个父节点
叶节点          没有子节点
内部节点        至少有一个子节点
祖父/孙子       父的父 / 子的子
祖先/后代       能沿链路从 A 走到 B，则 A 是祖先、B 是后代
堂兄弟姐妹（表亲）没有相同父节点但有相同祖父节点
叔伯            父的兄弟
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;链路是单向的：有 1→2 的链路，能从 1 到 2，不能从 2 回 1；遍历时只能沿一个方向走。&lt;/p&gt;
&lt;h3&gt;递归性质与边数&lt;/h3&gt;
&lt;p&gt;树是递归的数据结构：一个特殊节点（根）+ 若干子树，根包含指向所有子树根的链接。&lt;/p&gt;
&lt;p&gt;N 个节点的树恰好有 N−1 条边。理由：除根外每个节点恰好有一条入边。&lt;/p&gt;
&lt;h3&gt;深度 vs 高度&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;深度&lt;/td&gt;
&lt;td&gt;从根到节点 X 的路径长度 = 路径上的边数&lt;/td&gt;
&lt;td&gt;根节点深度 = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;高度&lt;/td&gt;
&lt;td&gt;从节点 X 到叶节点的最长路径中的边数&lt;/td&gt;
&lt;td&gt;叶节点高度 = 0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;树的高度 = 根节点的高度。&lt;/p&gt;
&lt;p&gt;高度和深度是两个不同属性，一个节点的两者可能相同也可能不同，这里很容易混。&lt;/p&gt;
&lt;h3&gt;树的应用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 存储天然分层数据（文件系统的文件/文件夹层次）
② 整理数据以便快速搜索/插入/删除 —— 二叉查找树，搜索时间复杂度约为对数级
③ Trie 存词典，速度极快、效率高，用于动态拼写检查
④ 网络路由算法
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;二叉树&lt;/h2&gt;
&lt;h3&gt;基本约定&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;每节点最多 2 个子节点&lt;/li&gt;
&lt;li&gt;一个节点可能：有两个 / 只有左 / 只有右 / 都没有&lt;/li&gt;
&lt;li&gt;只有一个子节点时，另一个引用/指针设为 NULL&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;严格二叉树（strict / 正规）&lt;/td&gt;
&lt;td&gt;每个节点要么有 2 个子节点、要么 0 个（不能只有 1 个）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&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;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;单节点树也算二叉树；唯一条件就是不能超过两个子节点。&lt;/p&gt;
&lt;h3&gt;层与每层最大节点数&lt;/h3&gt;
&lt;p&gt;相同深度的节点处于同一层；根深度 0 → 第 0 层（L0），往下 L1、L2…&lt;/p&gt;
&lt;p&gt;第 I 层最多 2^I 个节点。推导：某层有 X 个节点，则下一层最多 2X 个。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;L0: 2^0 = 1
L1: 2^1 = 2
L2: 2^2 = 4
L3: 2×4 = 8
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;高度公式&lt;/h3&gt;
&lt;p&gt;完美二叉树高度 H（层数 H+1）的节点总数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N = 2^0 + 2^1 + … + 2^H = 2^(H+1) − 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：层数 4（L0~L3）→ 最大节点数 = &lt;code&gt;2^4 − 1 = 15&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;反解：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N = 2^(H+1) − 1
→ H = log₂(N+1) − 1

例：N = 15 → H = log₂16 − 1 = 4 − 1 = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完全二叉树高度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;H = ⌊log₂ N⌋

例：N = 15 → log₂15 ≈ 3.906891 → 取整数部分 = 3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;N 个节点的最小高度 = ⌊log₂ N⌋（完全/完美二叉树时达到）。&lt;/p&gt;
&lt;p&gt;N 个节点的最大高度 = N − 1（树稀疏到近似链表时）。&lt;/p&gt;
&lt;h3&gt;为什么树的高度重要&lt;/h3&gt;
&lt;p&gt;树上的操作开销取决于树的高度：BST 的查找/插入/删除与高度成正比。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;时间复杂度写成 O(h)

完全/完美二叉树：h = log₂N  →  O(log N)（几乎是可能达到的最佳运行时间）
最坏（链表状）：  h = N−1    →  O(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直观对比：&lt;strong&gt;即便 N 高达 2^100，log₂N 也只是 100&lt;/strong&gt;；而 O(N) 时若 N = 2^100，即使使用有史以来最强大的机器，几年都无法完成计算。&lt;/p&gt;
&lt;p&gt;所以通常要使二叉树高度尽可能小，也就是保持二叉树「平衡」。&lt;/p&gt;
&lt;h3&gt;平衡二叉树&lt;/h3&gt;
&lt;p&gt;定义：对每个节点，其左右子树高度差不超过某个数 K；大多数情况下 K = 1。&lt;/p&gt;
&lt;p&gt;差值计算：&lt;code&gt;|h_left − h_right|&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;计算中子树为空时，其高度取 −1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;叶节点的左右子树均为空 → h_left = h_right = −1 → 差值为 0
满二叉树所有节点 DIF 都为 0
删掉一些节点后最大 DIF = 1，仍平衡
再删后某节点 DIF = 2（左子树高 1、右子树为空高 −1，|1−(−1)| = 2）→ 不再平衡
&lt;/code&gt;&lt;/pre&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;高度 = 从根到叶的最长路径中的边数

由此：只有一个节点的树高度 = 0
      空树高度 = −1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种定义（不采用）是把最长路径上的节点数量当高度。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;那样：最长路径 3 条边 → 高度会是 4
      单节点树高度 = 1
      空树高度 = 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那种定义看起来非常直观，很多地方都这么写，但这里一律用「边数」定义，所以空树高度是 −1。&lt;/p&gt;
&lt;h3&gt;二叉树的两种内存存储方式&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;指针/引用链接动态创建的节点 —— 最常见，节点随机分布在堆里
数组 —— 通常用于完全二叉树（后面堆也用它）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;数组的编号方式：从根开始、自左向右按层编号，从 0 开始（0, 1, 2, 3, 4, 5, 6）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;索引为 I 的节点：
  左子节点索引 = 2I + 1
  右子节点索引 = 2I + 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;例：I = 0 → 左 1、右 2；I = 1 → 左 3、右 4；I = 2 → 左 5、右 6。&lt;/p&gt;
&lt;p&gt;这个公式仅对完全二叉树成立。&lt;/p&gt;
&lt;p&gt;这是 0-based 的公式。有些教材用 1-based（左 2i、右 2i+1），这里用 0-based。&lt;/p&gt;
&lt;h2&gt;核心数值速查&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;N 个节点的边数          N − 1
第 I 层最多节点数        2^I
完美二叉树节点数         2^(H+1) − 1
完美二叉树高度           log₂(N+1) − 1
完全二叉树高度           ⌊log₂ N⌋
N 个节点的最小高度        ⌊log₂ N⌋
N 个节点的最大高度        N − 1
数组存储（0-based）      左 2I+1、右 2I+2
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;根节点深度 = 0，叶节点高度 = 0，空树高度 = −1。&lt;/li&gt;
&lt;li&gt;深度从上往下数，高度从下往上数，别混。&lt;/li&gt;
&lt;li&gt;平衡因子里空子树的高度取 −1，否则叶节点的 DIF 会算错。&lt;/li&gt;
&lt;li&gt;数组下标公式是 0-based（2I+1 / 2I+2），且只对完全二叉树成立。&lt;/li&gt;
&lt;li&gt;「完全二叉树」只是三分类之一，另外两个是严格二叉树和完美二叉树；链路单向，不能从子回到父。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二叉搜索树</title><link>https://me.622168.xyz/posts/data-structure/11-binary-search-tree/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/11-binary-search-tree/</guid><description>从数组、链表、二分查找三种方案的代价讲到 BST 的卖点，以及递归插入为什么必须接住返回值。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p27（19 分钟）、p28（C/C++ 实现，18 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;为什么需要 BST&lt;/h2&gt;
&lt;h3&gt;先比较三种存法&lt;/h3&gt;
&lt;p&gt;数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查找：从索引 0 扫到末尾，最坏看全部元素  →  O(N)
插入（未满）：递增末尾标记、填值         →  O(1)
插入（已满）：新建两倍大小新数组、复制   →  O(N)
删除：右侧所有记录左移，最坏移动 N−1 个  →  O(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;链表：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查找：最坏遍历整个链表        →  O(N)
插入：头部插入 O(1)；尾部插入也是 O(1)
删除：先得遍历查找记录        →  O(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;数量级直观感受&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;假设机器每秒能进行 10^6 次比较 → 一次比较代价 10^-6 秒
现实数据常有 1 亿甚至数十亿条记录
N = 1 亿 → 一次搜索需要 100 秒
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一次搜索花 100 秒显然不合理，而搜索往往是频繁操作。&lt;/p&gt;
&lt;h3&gt;有序数组 + 二分查找&lt;/h3&gt;
&lt;p&gt;要求：数据结构是数组，且记录必须排序（不同数据类型比较逻辑不同，字符串按字典序）。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;查找  →  O(log N)，这是所能达到的最佳运行时间
插入  →  先二分找位置（O(log N)），但接着要右移一位 → 整体仍是 O(N)
删除  →  同理 O(N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;具体数字：&lt;strong&gt;N = 2^31 时只需 31 次比较 → 31 微秒&lt;/strong&gt;（因为 log₂2^31 = 31）。&lt;/p&gt;
&lt;p&gt;结论：搜索问题解决了，但插入/删除仍是 O(N)。&lt;/p&gt;
&lt;h3&gt;BST 的卖点&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;三种操作（查找/插入/删除）平均都能做到 O(log N)
最坏情况 O(N)
通过保持平衡可以避免最坏情况
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;BST 的定义&lt;/h2&gt;
&lt;p&gt;BST 是一种特殊二叉树：对每个节点，左子树中所有节点的值都更小，右子树中所有节点的值都更大。&lt;/p&gt;
&lt;p&gt;精确一点的说法是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;左子树所有节点值「小于或等于」该节点值
右子树所有节点值「大于」该节点值
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;也就是「左 ≤ 根 &amp;lt; 右」。写成严格的「左 &amp;lt; 根 &amp;lt; 右」口径就不一致了。&lt;/p&gt;
&lt;p&gt;而且这个性质对所有节点都成立，不只是根。递归结构：左右子树也必须是 BST。&lt;/p&gt;
&lt;h3&gt;验证示例&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;        15
       /  \
     10    20
    /  \   /  \
   8   12 17  25
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逐节点验证：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;根 15：左子树 8、12 全 &amp;lt; 15 ✓；右子树 17、20、25 全 &amp;gt; 15 ✓
节点 10：左 8 小、右 12 大 ✓
节点 20：✓
叶节点无需操心
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;反例&lt;/h3&gt;
&lt;p&gt;把 12 改成 16：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;对节点 10 仍成立（16 在其右侧）
但对根节点 15 不成立（左子树中出现了比 15 大的 16）
→ 整棵树不再是 BST
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;所以 BST 性质要全局检查到根，只看局部父子会误判。&lt;/p&gt;
&lt;h2&gt;为什么 BST 查找能是 O(log N)&lt;/h2&gt;
&lt;h3&gt;二分查找类比&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;把整个列表定义为搜索空间（start/end 两指针）
与中间元素比较；小则搜左半，大则搜右半，相等则命中
每次把搜索空间减半
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;步数推导：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N → N/2 → N/4 → N/8 …
若走 K 步，则 N/2^K = 1  →  2^K = N  →  K = log₂N
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;BST 查找完全类似&lt;/h3&gt;
&lt;p&gt;从根开始，与根比较 → 相等则完成；较小去左子树；较大去右子树。每一步舍弃其中一个子树。&lt;/p&gt;
&lt;p&gt;演示查找 12：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;根 15 → 12 &amp;lt; 15 走左（搜索空间缩到 {10, 8, 12}）
→ 与 10 比，12 &amp;gt; 10 走右
→ 只剩一个节点，匹配命中
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平衡（对所有节点左右子树高度差 ≤ 1）时：&lt;code&gt;N → N/2 → N/4 …&lt;/code&gt; → 到只剩 1 个节点为止，故 O(log N)。&lt;/p&gt;
&lt;h3&gt;最坏情况（退化）&lt;/h3&gt;
&lt;p&gt;用同样的数据可以排出不平衡的 BST：任意节点都没有右子树（只有左链），这跟链表一样差。&lt;/p&gt;
&lt;p&gt;此时搜索空间每步只减少 1：&lt;code&gt;N → N−1 → N−2 → … → 1&lt;/code&gt;，总共 N 步 → O(N)。&lt;/p&gt;
&lt;p&gt;结论：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;BST 平均 O(log N)，最坏 O(N)
总是试图通过保持平衡来避免最坏情况
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;同一组记录可有多种合法 BST 排列；完美二叉树是最佳排列（每一步都恰好舍弃 N/2 个节点）。&lt;/p&gt;
&lt;h2&gt;插入&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;先找可插入位置  →  O(log N)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;演示插入 19：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;根 15 → 19 更大向右 → 到 20
→ 19 更小且左子树非空向左 → 到 17
→ 19 比 17 大，17 无右子节点
→ 新建值为 19 的节点作为 17 的右子节点
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;规则：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;若待插入值较小或相等 → 无左子节点则作为左子节点插入，否则向左走
若值较大             → 无右子节点则作为右子节点插入，否则向右行进
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;成本 = 搜索的成本。因为用指针/引用，不需要像数组那样移位，创建一个链接是恒定时间。&lt;/p&gt;
&lt;p&gt;删除：首先也得查找那个节点（O(log N)），删除节点仅意味着调整某些链接，所以平均与搜索同阶。&lt;/p&gt;
&lt;p&gt;p27-28 没给删除的具体实现（没讲中序后继/前驱），只说插入和删除会使 BST 变得不平衡，需要恢复平衡。&lt;/p&gt;
&lt;h2&gt;C/C++ 实现&lt;/h2&gt;
&lt;h3&gt;节点定义与内存位置&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct btNode {
    int data;
    struct btNode *left;
    struct btNode *right;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个节点定义与双向链表节点非常相似（都是两个链接），但双向链表是线性排列，这个定义是针对二叉树的。&lt;/p&gt;
&lt;p&gt;所有节点都在应用程序内存的「动态内存区/堆区」，用 C 的 &lt;code&gt;malloc&lt;/code&gt; 或 C++ 的 &lt;code&gt;new&lt;/code&gt; 创建。&lt;/p&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;struct btNode *rootPtr;      /* 始终存储根节点地址；初始 NULL 表示空树 */
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很多人喜欢把这个指针直接叫 &lt;code&gt;root&lt;/code&gt;，但它是「指向根的指针」，不是根本身。&lt;/p&gt;
&lt;p&gt;C 里不能直接写 &lt;code&gt;btNode* rootPtr&lt;/code&gt;，必须写 &lt;code&gt;struct btNode* ...&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;insert 的关键设计问题&lt;/h3&gt;
&lt;p&gt;问题：如果 &lt;code&gt;root&lt;/code&gt; 是 insert 的局部变量，函数内 &lt;code&gt;root = ...&lt;/code&gt; 改不了 main 里的 root（作用域只在函数内）。&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;① 让 insert 返回新根地址&lt;/td&gt;
&lt;td&gt;返回类型从 void 改成 &lt;code&gt;node*&lt;/code&gt;，main 里写 &lt;code&gt;root = insert(root, data)&lt;/code&gt;。这里选的是这种&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;② 传根的地址&lt;/td&gt;
&lt;td&gt;参数类型变成 &lt;code&gt;node**&lt;/code&gt;（指针的指针），函数内用 &lt;code&gt;*&lt;/code&gt; 解引用设置 main 中 root 的值，返回类型可为 void&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;③ 把 root 声明为全局变量&lt;/td&gt;
&lt;td&gt;必须在所有函数之外声明，所有函数都能访问，不必传地址&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;方案 ② 的毛病是「指针的指针」用起来有点棘手。&lt;/p&gt;
&lt;h3&gt;getNewNode&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct btNode *getNewNode(int data) {
    struct btNode *newNode = new btNode;   /* 或 malloc，在堆中创建 */
    newNode-&amp;gt;data  = data;
    newNode-&amp;gt;left  = NULL;
    newNode-&amp;gt;right = NULL;
    return newNode;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;insert 的完整逻辑（递归）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct btNode *insert(struct btNode *root, int data) {
    if (root == NULL) {                    /* 树/子树为空（首次插入） */
        root = getNewNode(data);
    } else if (data &amp;lt;= root-&amp;gt;data) {
        root-&amp;gt;left  = insert(root-&amp;gt;left,  data);    /* 递归插入左子树 */
    } else {                               /* data &amp;gt; root-&amp;gt;data */
        root-&amp;gt;right = insert(root-&amp;gt;right, data);    /* 递归插入右子树 */
    }
    return root;                           /* 返回路径统一提取到条件之后 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;插入左子树后，左子树的根可能改变&lt;/strong&gt;，所以必须 &lt;code&gt;root-&amp;gt;left = insert(...)&lt;/code&gt; 把返回值接回来。&lt;/p&gt;
&lt;p&gt;这种 &lt;code&gt;return root&lt;/code&gt; 的写法适用于所有情况，所以把它提到条件语句之后。&lt;/p&gt;
&lt;p&gt;树的问题几乎一直要用递归，因为它本身就是自相似的。&lt;/p&gt;
&lt;h3&gt;逐次插入的内存地址演示&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;插入 15：树空 → getNewNode 返回地址 200 → 设为根       main 的 root = 200
插入 10：10 &amp;lt; 15 → 递归 insert(200-&amp;gt;left = 0, 10)
        左子树根地址 0 → 树空 → getNewNode 返回 150    200-&amp;gt;left = 150
插入 20：20 &amp;gt; 15 → else → getNewNode 返回 300          200-&amp;gt;right = 300
插入 25：25 &amp;gt; 15 向右 → 25 &amp;gt; 20 向右 → 20-&amp;gt;right 为空
        → getNewNode 返回 500                          300-&amp;gt;right = 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次 insert 调用会等待下面递归调用完成并返回，才继续执行 &lt;code&gt;root-&amp;gt;left = ...&lt;/code&gt; 这一行。&lt;/p&gt;
&lt;h3&gt;search&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;int search(struct btNode *root, int data) {
    if (root == NULL) return 0;                  /* false */
    if (root-&amp;gt;data == data) return 1;            /* true */
    if (data &amp;lt; root-&amp;gt;data) return search(root-&amp;gt;left,  data);
    else                   return search(root-&amp;gt;right, data);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;测试：输入 88 → 找到了；输入 22 → 22 未找到。&lt;/p&gt;
&lt;p&gt;insert 完全可以不用递归写（用临时指针 + 循环），但递归处理树时非常直观，把递归理解透很值。&lt;/p&gt;
&lt;h2&gt;BST 三种操作汇总&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;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;O(log N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;每步舍弃一个子树&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;插入&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;成本 = 搜索成本 + 创建链接（恒定时间）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除&lt;/td&gt;
&lt;td&gt;O(log N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;td&gt;也要先查找，然后调整链接&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;BST 定义是「左 ≤ 根 &amp;lt; 右」，等于归到左侧；而且性质要全局检查到根，只看局部父子会误判（12 改成 16 的反例）。&lt;/li&gt;
&lt;li&gt;插入时必须接住返回值 &lt;code&gt;root-&amp;gt;left = insert(root-&amp;gt;left, data)&lt;/code&gt;，且 &lt;code&gt;return root&lt;/code&gt; 要放在所有分支之后，否则链接会丢。&lt;/li&gt;
&lt;li&gt;root 是局部变量时改不了 main 里的，得用「返回新根 / 双重指针 / 全局变量」之一；「指向根的指针」不是根本身。&lt;/li&gt;
&lt;li&gt;BST 最坏是 O(N)（退化成链表），只能靠平衡避免。&lt;/li&gt;
&lt;li&gt;有序数组查找快但插入删除慢，链表插入快但查找慢，BST 三者平均都快。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>内存中的栈与堆</title><link>https://me.622168.xyz/posts/data-structure/12-stack-and-heap-memory/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/12-stack-and-heap-memory/</guid><description>程序内存的四个区段、栈帧的生命周期，以及为什么递归插入必须接住返回值——全部用内存模拟讲清楚。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p29（13 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这一节没有新算法，全是程序运行时的内存模拟，但它是理解递归和指针的关键铺垫。&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;文本段&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;函数调用执行的临时空间；所有局部变量都存在栈中&lt;/td&gt;
&lt;td&gt;编译时确定，固定&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;堆&lt;/td&gt;
&lt;td&gt;动态内存，可按需增长或收缩&lt;/td&gt;
&lt;td&gt;运行时增长&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;核心区别就一条：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;其他所有区段大小在编译时确定且固定
堆在运行时可以增长

运行期间我们无法控制其他区段的内存分配或释放
但可以控制堆中的分配和释放
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是理解「为什么链表/树的节点必须建在堆上」的根本原因。&lt;/p&gt;
&lt;h2&gt;栈帧&lt;/h2&gt;
&lt;p&gt;每当调用一个函数，都会从栈中为其执行分配一定量的内存，这块内存称为该函数调用的栈帧。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;所有局部变量和函数调用的执行状态都存储在该函数调用的栈帧里
每当一个函数调用结束，为其分配的栈帧都会被回收
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;栈中内存在函数调用结束时自动释放。&lt;/p&gt;
&lt;h2&gt;逐步模拟：插入 15、10、20、25&lt;/h2&gt;
&lt;p&gt;前提：root 是 main 的局部变量。&lt;/p&gt;
&lt;h3&gt;main 开始执行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;为 main 分配栈帧
局部变量 root（指向 BST 节点的指针）未初始化 → 设为 NULL
（NULL 只是地址 0 的一个宏）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调用 insert(root, 15)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;main 的执行暂停
为 insert 分配新栈帧，局部变量 root、data 收集本次调用的参数
此调用中 root 为空 → 进入第一个 if → 调用 getNewNode
insert 再次暂停
为 getNewNode 分配栈帧，局部变量：data（收参数）和 node* newNode
new 在堆中创建节点，假设得到地址 200
  → 存入 newNode → 设置三字段：data = 15、左子 = NULL、右子 = NULL
getNewNode 返回 200 并结束 → 其栈帧被回收
insert 在这一行恢复：root（insert 的局部变量）= 200
  → insert 返回该 root 里的地址 200 并结束 → 栈帧回收
main 恢复，main 的 root 被设为 200
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调用 insert(root, 10)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;main 暂停 → 新 insert 栈帧
此调用 root = 200 不为空 → 比较 10 &amp;lt; 15 → 走递归分支
此 insert 暂停 → 为另一次 insert 调用分配新栈帧
   传入 root = 0（200 的左孩子为空）、data = 10
新调用中 root == NULL → 进入第一个 if → 调用 getNewNode
   → 堆中创建节点，假设地址 150 → 返回 150 → 栈帧回收
上层 insert 恢复：200 节点的左孩子 = 150（链接建立）
   → 该 insert 结束，返回 200
main 恢复：root 被重写为 200（与之前相同，无变化）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调用 insert(root, 20)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;堆中有一个值为 20 的节点作为 200 节点的右孩子，假设地址 300
→ 200 节点的右孩子地址被设为 300
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;调用 insert(root, 25)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;main 暂停 → insert 调用，传入 root = 200、data = 25
25 &amp;gt; 15 → 进入最后的 else（右子树插入）
   → 发起递归调用：传入 root = 300、data = 25
该调用中 25 &amp;gt; 20 → 又一次进入最后 else
   → 再递归：传入 root = 0（右子树为空）
该调用 root == NULL → 进入第一个 if → getNewNode → 假设地址 100 → 返回 100
顶层调用恢复：把新节点地址 100 设为右孩子 → 该调用完成，返回 300
下层调用恢复：从最后 else 里那一行继续 → 300 节点的右孩子 = 100 → 返回 300
再下层：从最后 else 继续 → 200 节点的右孩子被设置为 300（覆盖后不变）
main 恢复：root 被设为本次 insert 调用的返回值，用相同的值 200 覆盖
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归的本质&lt;/h3&gt;
&lt;p&gt;一个函数自己调用自己，跟一个函数 A 调用另一个函数 B 没什么不同。所以每次递归调用都会分配一个新的栈帧。&lt;/p&gt;
&lt;h2&gt;结论&lt;/h2&gt;
&lt;p&gt;关键在最后一步：main 里的 root，以及节点中的所有链接，都必须得到恰当的更新。&lt;/p&gt;
&lt;p&gt;很多 bug 就出在这儿——代码里丢了某些链接，或者多建了不必要的链接。这就是「必须接住返回值」的深层原因：&lt;code&gt;root-&amp;gt;left = insert(root-&amp;gt;left, data)&lt;/code&gt; 这个赋值动作，就是「把链接更新回去」的那一步。漏了它，链接就丢了。&lt;/p&gt;
&lt;h2&gt;堆 vs 栈 的生命周期对比&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;分配时机&lt;/td&gt;
&lt;td&gt;函数调用时自动&lt;/td&gt;
&lt;td&gt;运行时主动请求（malloc / new）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;释放时机&lt;/td&gt;
&lt;td&gt;函数调用结束时自动释放&lt;/td&gt;
&lt;td&gt;必须显式 free / delete&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;可控性&lt;/td&gt;
&lt;td&gt;无法控制&lt;/td&gt;
&lt;td&gt;可以控制&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;不释放会怎样&lt;/td&gt;
&lt;td&gt;自动回收&lt;/td&gt;
&lt;td&gt;该内存一直处于已分配状态直到程序结束&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;堆中申请的任何内存都必须用 C 的 &lt;code&gt;free&lt;/code&gt; 或 C++ 的 &lt;code&gt;delete&lt;/code&gt; 显式解除分配，否则该内存一直处于已分配状态直到程序结束。这就是内存泄漏的定义。&lt;/p&gt;
&lt;h2&gt;收尾&lt;/h2&gt;
&lt;p&gt;内存本身是一种线性结构。&lt;/p&gt;
&lt;p&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;li&gt;&lt;code&gt;root-&amp;gt;left = insert(...)&lt;/code&gt; 这个赋值不能漏，它就是「把链接接回去」的那一步。&lt;/li&gt;
&lt;li&gt;栈的大小编译时固定，堆是运行时增长的。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>BST 查找最值与二叉树的高度</title><link>https://me.622168.xyz/posts/data-structure/13-bst-min-max-height/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/13-bst-min-max-height/</guid><description>最小值一路向左、最大值一路向右，以及求高度时那个递归出口为什么必须返回 −1。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p30（6 分钟）、p31（7 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;找最小值&lt;/h2&gt;
&lt;p&gt;BST 里左边一定更小，所以最小值就是&lt;strong&gt;一路往左走到底的那个节点&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int findMin(struct btNode *root) {
    if (root == NULL) {
        printf(&quot;Error: Tree is empty\n&quot;);
        return -1;
    }
    while (root-&amp;gt;left != NULL)
        root = root-&amp;gt;left;
    return root-&amp;gt;data;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;直接拿参数 &lt;code&gt;root&lt;/code&gt; 往下走就行，不用另开临时指针——root 是局部变量，改它不影响调用方。&lt;/p&gt;
&lt;p&gt;例：&lt;code&gt;15 → 10 → 8&lt;/code&gt;，8 没有左孩子，就是最小值。&lt;/p&gt;
&lt;p&gt;空树得先挡一下。返回什么自己定，如果确定树里全是正数，拿 −1 当哨兵就够。&lt;/p&gt;
&lt;p&gt;递归版就是把「一直往左」说成「在左子树里找最小值」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int findMin(struct btNode *root) {
    if (root == NULL) { printf(&quot;Error: Tree is empty\n&quot;); return -1; }
    else if (root-&amp;gt;left == NULL) return root-&amp;gt;data;   // 到底了
    else return findMin(root-&amp;gt;left);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第二个 &lt;code&gt;else if&lt;/code&gt; 是递归出口。最小值不可能在右子树里，所以左子树一空就已经知道答案了。&lt;/p&gt;
&lt;p&gt;最大值对称，一路往右。&lt;/p&gt;
&lt;h2&gt;二叉树的高度&lt;/h2&gt;
&lt;p&gt;先把定义钉死，不然后面全是坑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;高度 = 从某节点到最远叶子，路径上的「边数」
空树高度 = −1
叶节点高度 = 0
单节点树高度 = 0
树的高度 = 根的高度
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;另一种定义是数「节点个数」而不是边数，那样叶节点高度是 1、空树是 0。这些笔记里一律用边数。&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;高度&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;根&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&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;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;叶子 9&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;深度从根往下数，高度从叶子往上数，同一个节点这两个值通常不一样。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;树的高度 = 所有节点里最大的深度&lt;/strong&gt;，所以「高度」和「最大深度」有时会混着说。&lt;/p&gt;
&lt;h2&gt;求高度&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;高度(节点) = max(高度(左), 高度(右)) + 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那个 +1 是「节点连到自己子树的那条边」。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int findHeight(struct btNode *root) {
    if (root == NULL) return -1;
    return max(findHeight(root-&amp;gt;left), findHeight(root-&amp;gt;right)) + 1;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;递归出口为什么是 −1&lt;/h3&gt;
&lt;p&gt;这里容易记错，我第一遍就写成了 0。&lt;/p&gt;
&lt;p&gt;拿叶子节点看：它左右都是 NULL，会发起两次空调用。这两次该返回什么？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;返回 0  →  叶子高度 = max(0,0) + 1 = 1    ✗ 叶子应该是 0
返回 −1 →  叶子高度 = max(−1,−1) + 1 = 0   ✓
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;−1 正好抵消那个 +1&lt;/strong&gt;：空节点本身不存在，但公式里从父节点连过去的那条边被算了一次，得减掉。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;求高度是 &lt;strong&gt;O(N)&lt;/strong&gt;，不是 O(h)。&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;findMin / findMax&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O(h)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;findHeight&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;O(N)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;findMin 往左、findMax 往右，别记反。&lt;/li&gt;
&lt;li&gt;空树不判空会解引用 NULL。&lt;/li&gt;
&lt;li&gt;递归出口返回 0 会让所有高度集体差 1。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;max(...) + 1&lt;/code&gt; 的 +1 别漏。&lt;/li&gt;
&lt;li&gt;求高度是 O(N)，别按 O(h) 估。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>二叉树的遍历</title><link>https://me.622168.xyz/posts/data-structure/14-binary-tree-traversal/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/14-binary-tree-traversal/</guid><description>广度优先与深度优先的区别、前中后序的 DLR/LDR/LRD 记忆法，以及层次遍历为什么必须用队列。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p32（10.9 分钟）、p33（10.9 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;遍历的定义与难点&lt;/h2&gt;
&lt;p&gt;定义：以某种顺序恰好访问树中每个节点一次的过程。&lt;/p&gt;
&lt;p&gt;「访问一个节点」= 读取或处理该节点中的数据；这里访问 = 打印节点中的数据。&lt;/p&gt;
&lt;p&gt;难点在于树不是线性结构，没有唯一的「下一个」。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;例如指向根 F，有两个可能方向：向左到 D、向右到 J
选了其中一个之后，还得回来走另一个方向
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BFS 和 DFS 是遍历/搜索图的通用技术；树只是图的一种特殊形式。&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BFS（广度优先）&lt;/td&gt;
&lt;td&gt;先访问同一深度/层级的所有节点，再访问下一层级的节点；对任何一个节点，先访问其所有子节点，然后才访问孙节点&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DFS（深度优先）&lt;/td&gt;
&lt;td&gt;去到一个子节点，就得先完整处理完这个子节点的整个子树，然后再去下一个子节点&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;树的 BFS 就是层次序遍历（level order traversal）。&lt;/p&gt;
&lt;h2&gt;示例树（全篇基准图）&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;            F              ← L0，深度 0
          /   \
         D     J           ← L1，深度 1
        / \   / \
       B   E G   K         ← L2
      / \     \
     A   C     I           ← L3
              /
             H             ← L4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;BFS / 层序访问顺序：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;F → D J → B E G K → A C I → H
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;一层一层、从左往右。&lt;/p&gt;
&lt;h2&gt;DFS 的三种策略&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;前序 preorder&lt;/td&gt;
&lt;td&gt;根 → 左 → 右&lt;/td&gt;
&lt;td&gt;DLR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;中序 inorder&lt;/td&gt;
&lt;td&gt;左 → 根 → 右&lt;/td&gt;
&lt;td&gt;LDR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;后序 postorder&lt;/td&gt;
&lt;td&gt;左 → 右 → 根&lt;/td&gt;
&lt;td&gt;LRD&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;D = 访问数据、L = 左边、R = 右边。写成 VLR（V = visit）也是常见的，这里采用 DLR。&lt;/p&gt;
&lt;p&gt;左右根共 6 种排列组合，但实际只用「先左后右」这 3 种。&lt;/p&gt;
&lt;h2&gt;手推前序遍历&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;F（读数据）
→ 左到 D → 读 D
→ 左到 B → 读 B
→ 左到 A → 读 A（A 左右皆空）
→ B 左子树完成 → 右到 C → 读 C（C 左右空）
→ D 左子树完成 → 右到 E → 读 E（E 左右空）
→ F 左子树完成 → 右到 J
→ J 左到 G → 读 G（G 左空）
→ 右到 I → I 左到 H → 读 H（H 左右空）
→ I 左子树完成、右子树空 → 回到 J（J 左子树完成）
→ 右到 K → 读 K
→ 结束
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;前序序列 = &lt;code&gt;F, D, B, A, C, E, J, G, I, H, K&lt;/code&gt;&lt;/p&gt;
&lt;h2&gt;手推中序遍历&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;从根先往左 → D → B → A
A 左空 → 读 A → A 右空 → A 结束
B 左子树完成 → 读 B → 右到 C
C 左空 → 读 C → C 右空
D 左完成 → 读 D → 右到 E → 读 E
F 左子树完成 → 读 F → 再去 F 的右边…
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;已推出的前缀：&lt;code&gt;A, B, C, D, E, F, …&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;一个重要结论&lt;/h3&gt;
&lt;p&gt;这棵树其实是一棵二叉搜索树（每节点左子树值较小、右子树值较大）。&lt;/p&gt;
&lt;p&gt;对 BST 进行中序遍历会得到一个排序列表，完整序列应为 &lt;code&gt;A B C D E F G H I J K&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这条结论是后面「判断是否为 BST」那一节的基础。&lt;/p&gt;
&lt;h2&gt;后序&lt;/h2&gt;
&lt;p&gt;板书上给出了后序结果，字幕里没有。自己推一遍验证。&lt;/p&gt;
&lt;h2&gt;层次遍历&lt;/h2&gt;
&lt;h3&gt;为什么单个指针不行&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;假设一个 current 指针指向正在访问的当前节点：
  可以从 F 走到 D（有链接）
  但从 D 无法走到 J（D 与 J 之间没有链接），只能从 F 走到 J
  而且一旦指针移到 D，甚至回不到 F，因为从 D 到 F 不存在反向链接
→ 仅靠一个指针不行
&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;p&gt;算法步骤：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 最初把根节点的地址入队（表示最初这是唯一的已发现节点）
② 只要队列不为空（至少存在一个已发现节点）：
     从前端取出一个节点 → 访问它（打印其值）
     → 把它的子节点入队（先左孩子、再右孩子）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;队列起到的两个作用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;① 从一个节点转移时不会丢失其子节点的引用（因为保存着这些引用）
② 队列是先入先出（FIFO）结构，所以最先被发现的节点会最先被访问
   → 从而得到期望的有序性
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第 ② 条就是「为什么必须用队列而不是栈」的答案。&lt;/p&gt;
&lt;h3&gt;示例走查（根地址设为 400）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;入队 400 → 出队访问 F → 左孩子（200）入队、右孩子入队
→ 出队 200 访问 → 其子节点入队
   （此时 2 个已访问、3 个已发现、6 个未发现）
→ 出队 120 访问 → 子节点入队
→ … 直到所有节点被访问且队列为空
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;代码（C++）&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;void levelOrder(struct btNode *root) {
    if (root == NULL) return;              /* 极端情况：空树直接返回 */

    queue&amp;lt;btNode*&amp;gt; Q;                      /* 创建「指向节点的指针」队列 */
    Q.push(root);                          /* 最初唯一已发现节点 = 根 */

    while (!Q.empty()) {                   /* 只要至少存在一个已发现节点 */
        struct btNode *current = Q.front();  /* front() 返回前端元素（不移除） */
        printf(&quot;%c &quot;, current-&amp;gt;data);        /* 访问 = 打印数据 */

        if (current-&amp;gt;left  != NULL) Q.push(current-&amp;gt;left);   /* 左孩子入队 */
        if (current-&amp;gt;right != NULL) Q.push(current-&amp;gt;right);  /* 右孩子入队 */

        Q.pop();                             /* 必须调用 pop 才移除元素 */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;front()&lt;/code&gt; 只返回元素，并不会从队列中移除元素，必须调用 &lt;code&gt;pop()&lt;/code&gt; 才能移除。很多人代码里忘了 pop，结果死循环。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;p&gt;时间复杂度 = O(N)：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;「访问一个节点」= 读取该节点的数据 + 将其子节点插入队列，两者都是恒定时间
每个节点恰好被访问一次 → 总时间与节点数量成正比
无论树的形状如何，对于所有情况层序遍历的时间复杂度都是 O(N)
&lt;/code&gt;&lt;/pre&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;最多 1 个&lt;/td&gt;
&lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;最费：完美二叉树&lt;/td&gt;
&lt;td&gt;最深层有 N/2 个节点同时在队列中&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;所以一般情况下空间复杂度是 O(N)，最坏情况也是 O(N)。&lt;/p&gt;
&lt;p&gt;这里的最佳/平均/最差情况只是就空间复杂度而言的，所有情况下时间复杂度都将是 O(N)。&lt;/p&gt;
&lt;h2&gt;BFS vs DFS 对照&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;BFS（层次遍历）&lt;/th&gt;
&lt;th&gt;DFS（前/中/后序）&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;O(N)&lt;/td&gt;
&lt;td&gt;O(N)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间&lt;/td&gt;
&lt;td&gt;O(1) ~ O(N)&lt;/td&gt;
&lt;td&gt;O(h)（递归栈）&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;这里 DFS 的手段就是递归，没有用显式栈。递归之所以等价于栈，是因为函数调用本身占用系统调用栈（p29 讲栈帧时铺垫过）。&lt;/p&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;前序 = DLR，中序 = LDR，后序 = LRD，名字由根的位置决定；中序遍历 BST 会得到有序序列。&lt;/li&gt;
&lt;li&gt;层次遍历必须用队列，用栈会变成深度优先。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;front()&lt;/code&gt; 只返回不移除，必须 &lt;code&gt;pop()&lt;/code&gt;，忘了就死循环。&lt;/li&gt;
&lt;li&gt;层次遍历的时间复杂度在所有情况下都是 O(N)，「最好/最坏」只针对空间；空间最省是链状树 O(1)，最费是完美二叉树 O(N)，因为最深层有 N/2 个节点同时在队列里。&lt;/li&gt;
&lt;li&gt;树是图的特例，BFS/DFS 是图的通用技术。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>图</title><link>https://me.622168.xyz/posts/data-structure/17-graph/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/17-graph/</guid><description>图的基本概念与术语，以及边列表、邻接矩阵、邻接表三种表示法的完整代价对比。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p38（16.1 分钟）、p39（14.7 分钟）、p40（13.2 分钟）、p41（14.2 分钟）、p42（26.8 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;图是什么&lt;/h2&gt;
&lt;h3&gt;与线性结构、树的对比&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;数组、链表、栈、队列  →  线性数据结构
树                    →  非线性数据结构（层次结构）
图                    →  另一种非线性数据结构
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对照着看树的特殊性：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;N 个节点的树必定恰好有 N−1 条边
从根出发必须能到达所有节点，且根到某节点只有唯一一条路径
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图则没有支配节点间连接的规则，边可以任意方式连接——树只是图的一种特殊类型。&lt;/p&gt;
&lt;h3&gt;形式定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;图 G = (V, E)
V 是顶点集，E 是边集
这是一个「有序对」（第一个对象必须是顶点集，第二个必须是边集）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;有序对 (a, b)：顺序重要，(a,b) ≠ (b,a)（除非 a = b），写成圆括号
无序对 {a, b}：顺序不重要，{a,b} = {b,a}，写成花括号
&lt;/code&gt;&lt;/pre&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;/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;有序对 (u, v)&lt;/td&gt;
&lt;td&gt;无序对 {u, v}&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;要表示 v→u&lt;/td&gt;
&lt;td&gt;必须另画一条边 (v, u)&lt;/td&gt;
&lt;td&gt;同一条边即包含&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有向图：所有边均为有向边；无向图：所有边均为无向边。&lt;/p&gt;
&lt;p&gt;一张图同时含两种边是可能的，但这里不研究这类图。&lt;/p&gt;
&lt;p&gt;无向图可以重绘为有向图（每条无向边 → 两条方向相反的有向边）；有向图不一定能重绘为无向图。&lt;/p&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;社交网络（Facebook）&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;「页面 A 链到 B 不代表 B 链回 A」&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;城际公路网&lt;/td&gt;
&lt;td&gt;无向、加权&lt;/td&gt;
&lt;td&gt;假设高速双向通行，长度不同&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;市内道路网&lt;/td&gt;
&lt;td&gt;有向、加权&lt;/td&gt;
&lt;td&gt;城市里有单行道&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;推荐好友问题&lt;/h3&gt;
&lt;p&gt;给拉玛推荐「朋友的朋友」：拉玛有 3 个朋友（埃拉、鲍勃、凯蒂），鲍勃有 3 个朋友（汤姆、萨姆、里）、凯蒂有 2 个朋友（里、斯瓦蒂）→ 共可推荐 4 位用户（里被算了两次）。&lt;/p&gt;
&lt;p&gt;用图论术语表述：找出所有「到给定节点的最短路径长度等于 2」的节点。&lt;/p&gt;
&lt;p&gt;网络爬虫本质上就是图遍历。&lt;/p&gt;
&lt;h3&gt;加权图&lt;/h3&gt;
&lt;p&gt;有些连接不能同等看待（道路长度不同），给每条边关联权重/代价。&lt;/p&gt;
&lt;p&gt;例：城市 A 到 D 选最佳路线。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;不计权重：经 B、C 的路线与经 E、F 的路线都是 3 条边，「同样好」
          经 E 的黄色路线只有 2 条边，「最好」
计入权重：需要把路径上各边权重相加算总成本
          → 最短（最优）路线变成经 B 和 C 的那条
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以把所有图都视为加权图；未加权图可看作所有边权重相同的加权图，通常假定权重为 1。&lt;/p&gt;
&lt;h2&gt;基本属性&lt;/h2&gt;
&lt;h3&gt;记号&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;|V| 表示顶点数量
|E| 表示边的数量
（集合的基数，用与绝对值相同的记号）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;特殊类型的边&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;自环（self-loop）    一条边只涉及一个顶点、两个端点相同
                     例：网页指向自己的链接（点击页眉链接只是刷新）
多重边（multi-edge） 同一条边在图中出现不止一次
                     例：城市间航班网络（一对城市间有多趟航班）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;自环和多重边会让处理图的工作变复杂。&lt;/p&gt;
&lt;p&gt;简单图（simple graph）：不含自环也不含多重边。这里大多研究简单图。&lt;/p&gt;
&lt;h3&gt;边数的取值范围&lt;/h3&gt;
&lt;p&gt;最少边数 = 0（可以不画任何边，节点可以完全不相连）。&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;取值范围&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;有向图&lt;/td&gt;
&lt;td&gt;N × (N−1)&lt;/td&gt;
&lt;td&gt;[0, N(N−1)]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;无向图&lt;/td&gt;
&lt;td&gt;N × (N−1) / 2&lt;/td&gt;
&lt;td&gt;[0, N(N−1)/2]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;无向图是一半的原因：无向图中一对节点之间只能有一条双向边，不能有方向不同的两条边。&lt;/p&gt;
&lt;p&gt;具体数值：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;有向图顶点数 = 10  →  最大 90 条边
有向图顶点数 = 100 →  最大 9900 条边
→ 最大边数近似于顶点数量的平方
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;密集图 vs 稀疏图&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;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;密集图（dense）&lt;/td&gt;
&lt;td&gt;边数接近可能的最大边数，约为顶点数量的平方量级&lt;/td&gt;
&lt;td&gt;通常用邻接矩阵&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;稀疏图（sparse）&lt;/td&gt;
&lt;td&gt;边数确实很少，通常接近于顶点数量、且不超过这个数量&lt;/td&gt;
&lt;td&gt;通常用邻接表&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;两者没有明确界限，完全取决于具体情境；但这是重要分类——很多决策基于图是密集还是稀疏。&lt;/p&gt;
&lt;h3&gt;路径相关术语&lt;/h3&gt;
&lt;p&gt;这一块最容易写漏。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;路径（path）           一系列顶点序列，其中每一对相邻顶点之间都有边相连
简单路径（simple path） 没有重复顶点（顶点不重复则边也不会重复）
漫步（walk）            允许重复顶点/边
轨迹（trail）           顶点可以重复但边不能重复的漫步
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;图论中「路径」的用法不统一。大多数时候说「路径」就指简单路径；如果允许重复，用「漫步」这个术语。&lt;/p&gt;
&lt;p&gt;所以：路径 = 顶点或边均不重复的漫步。&lt;/p&gt;
&lt;p&gt;一个关键断言：两个顶点之间如果存在一条有重复的漫步，那么必然也存在一条简单路径。&lt;/p&gt;
&lt;p&gt;因此寻找简单路径最有意义。这里的约定是：说「路径」即指简单路径；若不是简单路径会明确说明。&lt;/p&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;/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;有向图&lt;/td&gt;
&lt;td&gt;不是强连通，但把所有边视作无向边后变成连通图&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;无向图就说「连通」，有向图就说「强连通」，记住这两个就够了。&lt;/p&gt;
&lt;p&gt;反例：某图中能从 A 到 C，但不能从 C 到 A → 非强连通。&lt;/p&gt;
&lt;p&gt;应用：市内道路网应当始终是强连通的。&lt;/p&gt;
&lt;h3&gt;环&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;闭合行走（closed walk）：起点和终点是同一个顶点，且长度必须大于 0
长度：路径中边的数量
简单环（simple cycle）：闭合行走，且除起点/终点顶点外没有其他顶点或边被重复
无环图（acyclic）：不含环的图
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通常说「环」指简单环。&lt;/p&gt;
&lt;p&gt;树 = 无向无环连通图的实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;树中可以有闭合行走（边的重复），但不存在简单环
此外树还必须连通（无向无环图不只有树这一种）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有向无环图 = DAG：不存在「始于且终于同一个顶点、长度大于 0」的路径。&lt;/p&gt;
&lt;p&gt;图中的环会给「从一个顶点到另一个顶点找最短路线」这类算法设计带来诸多问题。&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;边对象：一条边由其两个端点确定 → 定义为具有两个字段的结构体：起始顶点、终止顶点。&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;原因&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;code&gt;FH&lt;/code&gt; 与 &lt;code&gt;HF&lt;/code&gt; 是两种不同连接&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;加权图 → 在边对象中再加一个字段存权重。&lt;/p&gt;
&lt;p&gt;内存优化：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;问题：若边对象前两个字段存字符串，各行长度不同，每行占用内存不一样

优化 1：字段存「指向顶点列表中那些名称的引用/指针」
        → 每行内存相同，且引用比名称副本省很多开销

优化 2（更好的做法）：只存顶点在顶点列表中的「索引（整数）」
        → 边对象的两个字段变成两个整数字段
        → 边列表中每一行消耗的内存量都相同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空间复杂度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;顶点列表：O(|V|)
边列表（用索引/引用设计）：O(|E|)
整体：O(|V| + |E|)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&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;复杂度&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;O(|E|)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判断两个节点是否相连&lt;/td&gt;
&lt;td&gt;同样要在线性搜索，最坏看所有条目&lt;/td&gt;
&lt;td&gt;O(|E|)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;有向图只看起始节点；无向图起点和终点都要看。&lt;/p&gt;
&lt;p&gt;为什么 O(|E|) 被认为很贵：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;|E| 最坏 ≈ V²
任何以边数为量级运行的操作都被认为代价很高
我们希望把成本控制在「与顶点数量同量级」
O(V) 比 O(E) 好得多
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;表示法二：邻接矩阵&lt;/h3&gt;
&lt;p&gt;核心设计：把边存在二维数组/矩阵中，大小 V × V。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每个顶点有一个 0 到 V−1 的索引（从顶点列表中取索引：A=0、B=1、C=2…）
无权图：A[i][j] = 1 表示存在从 i 到 j 的边，否则为 0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;无向图 vs 有向图的矩阵：&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;code&gt;A[i][j] = A[j][i]&lt;/code&gt;）&lt;/td&gt;
&lt;td&gt;两个位置（只需看一半）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;有向图&lt;/td&gt;
&lt;td&gt;不对称&lt;/td&gt;
&lt;td&gt;一个位置（必须遍历整个矩阵）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;时间复杂度：&lt;/p&gt;
&lt;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;O(|V|)（先扫顶点列表找索引 O(|V|) + 扫一行 O(|V|)）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;找邻居&lt;/td&gt;
&lt;td&gt;已知索引&lt;/td&gt;
&lt;td&gt;O(|V|)（省掉扫顶点列表）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判断两节点是否相连&lt;/td&gt;
&lt;td&gt;已知索引&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判断两节点是否相连&lt;/td&gt;
&lt;td&gt;给的是名称&lt;/td&gt;
&lt;td&gt;O(|V|)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;关键在于：邻接矩阵判断相连不是无条件 O(1)，给名称时还要先扫顶点列表。&lt;/p&gt;
&lt;p&gt;优化：用额外内存建一个哈希表，名称和索引作为键值对，则「名称→索引」也变成 O(1)。&lt;/p&gt;
&lt;p&gt;总评：用邻接矩阵，最常执行操作的时间成本与顶点数量成量级，而非与边数量成量级。&lt;/p&gt;
&lt;p&gt;加权图的邻接矩阵：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A[i][j] 直接存该边的权重
不存在的边要设一个默认值，例如一个非常大或最大可能的整数值（图中填的是「无穷」）
「这种值绝不会被预期成为边的权重」
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空间代价：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;邻接矩阵确切使用 V² 个单位的内存  →  空间复杂度 O(V²)
矩阵不仅存了「相连」的信息，还存了「未相连」的冗余信息
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;示例图具体数字：边列表只消耗 10 行内存，而邻接矩阵消耗 64 个单位（8×8）。&lt;/p&gt;
&lt;p&gt;Facebook 的具体演算：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;假设有 10 亿（10⁹）用户
平均一名用户的朋友数不超过 1000
连接总数 = 10⁹ × 1000 = 10¹²
无向图要除以 2（否则每条边算两次）→ 5 × 10¹¹ 条边
这远远少于顶点数量的平方（10¹⁸）

邻接矩阵需要 10¹⁸ 个存储单元 ≈ 10¹⁸ 字节 ≈ 1000 PB（拍字节）
  → 绝不可能装在一块物理磁盘上
邻接表只要 5 × 10¹¹ 字节 = 0.5 TB（太字节）
  → 如今一台典型个人电脑就会有这么大的存储空间
&lt;/code&gt;&lt;/pre&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;语义上是反过来的：索引不再代表任何含义，值才是我们所连接到的节点的实际索引（与矩阵的语义正好相反）。&lt;/p&gt;
&lt;p&gt;示例图的邻接表（8 顶点）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0 号节点连 1、2、3
1 号节点连 0、4、5
2 号节点连 0、6
…
7 号节点有 4 个连接（3、4、5、6）

各行的连接数分别是：0 号 3 个、1 号 3 个、2 号 2 个…… 每行大小都不同
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如何用编程实现「每行不同大小」：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;C/C++：创建一个「指针数组」，大小为 8
       每个指针指向一个不同大小的一维数组
       第 0 个指针 → 大小 3 的数组
       第 7 个指针 → 大小 4 的数组
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;空间复杂度：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;无向图：2 × |E| 个单位内存
有向图：|E| 个单位内存
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;严格表述是 O(|V| + |E|)：因为存储顶点也会占用一定内存；但假如可以假定顶点数量相较于边数量少得多，就可以简单说与边的数量有关。能正确计数总是好事。&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;邻接表&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;判断两节点是否相连&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;td&gt;O(V)（无法直接定位，必须逐行扫描）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;找某节点的所有邻居&lt;/td&gt;
&lt;td&gt;O(V)&lt;/td&gt;
&lt;td&gt;O(V)（最坏情况一行可能有 V 个单元）&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;邻接表的优化：若保持每行有序，可改用二分搜索，成本降到 O(log V)，对数运行时间确实很棒，但始终维持数组有序在其他方面代价很高。&lt;/p&gt;
&lt;p&gt;稀疏图下的实际性能：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;社交网络有 10 亿（10⁹）用户
任何人拥有朋友的最大数量是 1 万（10⁴）
机器每秒能扫描/读取 10⁶ 个单元
&lt;/code&gt;&lt;/pre&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;扫一整行 = 10⁹ 个单元 → 1000 秒 ≈ 16.66 分钟 ✗&lt;/td&gt;
&lt;td&gt;最多 10⁴ 个单元 → 10 ms ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判断两个节点是否相连&lt;/td&gt;
&lt;td&gt;读一个单元 → 1 微秒 ✓&lt;/td&gt;
&lt;td&gt;必须扫整行 → 10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;设计一个社交网络该选哪种结构？机器不能让用户等 16 分钟。&lt;/p&gt;
&lt;p&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;邻接表（数组实现）&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;插入一条新边&lt;/td&gt;
&lt;td&gt;只需把某格的 0 改成 1 → O(1)&lt;/td&gt;
&lt;td&gt;要往那一行加一个元素，但动态增加现有数组的大小是不可能的 → 必须新建数组 + 复制旧内容 + 抹去旧数组 ✗&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除一条边&lt;/td&gt;
&lt;td&gt;把 1 改成 0 → O(1)&lt;/td&gt;
&lt;td&gt;同样麻烦&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;用数组实现动态或可变列表很棘手，这种「新建数组 + 复制旧数据」的做法代价很高，这正是我们常常改用链表存储动态/变化列表的确切原因。&lt;/p&gt;
&lt;p&gt;邻接表的链表实现：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;每行改成一个链表：不再需要动态改数组大小，插入和删除容易得多

实现：为每个节点创建一个链表存储其邻居
      创建一个「指针数组」，唯一区别是每个指针指向一个「链表的头部」
      链表节点有两个域：一个存数据，一个存下一个节点的地址
      加权图 → 在节点里再加一个字段存权重
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种「把一个节点的邻居信息存储在链表中」的结构，正是通常所说的邻接表。&lt;/p&gt;
&lt;p&gt;这里要分清什么是什么：指针数组的元素是指向节点的指针（只存地址）；而带两个字段的那个才是节点。&lt;/p&gt;
&lt;p&gt;一个延伸问题：在邻接表里添加/删除连接有多灵活？有没有办法改进？如果不用链表存储邻居，而是用二叉搜索树，会怎样？&lt;/p&gt;
&lt;p&gt;会更好，因为查找、插入、删除一个邻居的时间成本都会降到对数级。&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;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;O(|E|)&lt;/td&gt;
&lt;td&gt;O(|V|)&lt;/td&gt;
&lt;td&gt;O(|V|) 最坏（有序时 O(log V)）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;判断两节点是否相连&lt;/td&gt;
&lt;td&gt;O(|E|)&lt;/td&gt;
&lt;td&gt;O(1)（仅当已知索引；给名称则 O(|V|)）&lt;/td&gt;
&lt;td&gt;O(|V|) 最坏&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;增删边&lt;/td&gt;
&lt;td&gt;麻烦&lt;/td&gt;
&lt;td&gt;O(1) ✓&lt;/td&gt;
&lt;td&gt;数组实现麻烦，链表实现容易&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;空间&lt;/td&gt;
&lt;td&gt;O(|V|+|E|)&lt;/td&gt;
&lt;td&gt;O(V²)&lt;/td&gt;
&lt;td&gt;O(|V|+|E|)（无向 2|E|、有向 |E|）&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;td&gt;稀疏图 ✓&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;N 个节点的树恰好 N−1 条边，图没有这个限制；树是图的特例。有向图最大边数 N(N−1)，无向图 N(N−1)/2，差一倍。&lt;/li&gt;
&lt;li&gt;「路径」默认指简单路径；允许重复的叫「漫步」，边不重复的叫「轨迹」。无向图说「连通」，有向图说「强连通」，用词要区分。&lt;/li&gt;
&lt;li&gt;邻接矩阵判断相连不是无条件 O(1)，给名称时要先扫顶点列表；它的空间是 O(V²)，稀疏图会浪费大量内存（Facebook 要 1000 PB）。&lt;/li&gt;
&lt;li&gt;邻接表的「找邻居」最坏也是 O(V)，只有稀疏图才有实际优势；索引不代表含义，值才代表连接的节点（与矩阵语义相反）。&lt;/li&gt;
&lt;li&gt;邻接表用数组实现时增删边很麻烦（数组不能动态增长），所以实际用链表。大多数真实世界的图都是稀疏的，邻接表通常是更好的选择。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>判断 BST 与删除节点</title><link>https://me.622168.xyz/posts/data-structure/16-bst-validate-delete/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/16-bst-validate-delete/</guid><description>判断 BST 的三种方法（含一个 O(n²) 的低效法）、删除节点的三种情况，以及中序后继怎么找。</description><pubDate>Fri, 18 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;p35（15.9 分钟）、p36（17.8 分钟）、p37（17.3 分钟）&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;判断是否为 BST&lt;/h2&gt;
&lt;p&gt;这是道著名的编程面试题。&lt;/p&gt;
&lt;h3&gt;BST 的递归定义&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;二叉树：每个节点最多两个子节点
BST：对「每个」节点，其左子树所有值更小、右子树所有值更大
     若允许重复值，左子树可为「小于或等于」
左子树、右子树「自身也必须是 BST」—— 不仅仅是根节点
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;反例&lt;/h3&gt;
&lt;p&gt;拿 A/B/C/D 四棵树看：A 和 C 是 BST；B 和 D 不是。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;B：根值 10，但左子树中有 11（比 10 大）→ 违反「左子树全部更小」
D：根值 5，左子树有 1、右子树有 8、9、12 → 对根节点来说没问题；
   但值为 8 的节点，其左边有个 9 → 不是 BST
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只检查根节点是不够的，必须对每个节点都成立。D 这个反例最容易漏。&lt;/p&gt;
&lt;h3&gt;方法一：检查子树&lt;/h3&gt;
&lt;p&gt;这是最容易想到、但效率不高的办法。两个辅助函数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;isSubtreeLesser(root, value)   → 子树中所有元素都 ≤ value 则返回真
isSubtreeGreater(root, value)  → 子树中所有元素都 &amp;gt; value 则返回真
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;主函数需要同时满足 4 个条件：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① isSubtreeLesser(root-&amp;gt;left,  root-&amp;gt;data)
② isSubtreeGreater(root-&amp;gt;right, root-&amp;gt;data)
③ isBinarySearchTree(root-&amp;gt;left)
④ isBinarySearchTree(root-&amp;gt;right)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;基本情况：根为空 → 返回真。&lt;/p&gt;
&lt;p&gt;替代写法：不写这两个函数，改成 &lt;code&gt;FindMax(左子树)&lt;/code&gt; 与 &lt;code&gt;FindMin(右子树)&lt;/code&gt; 比较，也行得通。&lt;/p&gt;
&lt;p&gt;代价是：每个节点都要去看其子树中的所有节点，大量节点被反复遍历、反复读取比较 → 时间复杂度 O(n²)。&lt;/p&gt;
&lt;h3&gt;方法二：给每个节点定一个「允许范围」&lt;/h3&gt;
&lt;p&gt;思路：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;根节点范围 (−∞, +∞)
向左走 → 收紧上限（新上限 = 当前节点数据）
向右走 → 收紧下限（新下限 = 当前节点数据）
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;检查变成常数时间：只需判断 &lt;code&gt;root-&amp;gt;data &amp;gt; min &amp;amp;&amp;amp; root-&amp;gt;data &amp;lt; max&lt;/code&gt;，不再遍历子树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int isBSTUtil(struct node *root, int min, int max) {
    if (root == NULL) return 1;
    if (root-&amp;gt;data &amp;gt; min &amp;amp;&amp;amp; root-&amp;gt;data &amp;lt; max
        &amp;amp;&amp;amp; isBSTUtil(root-&amp;gt;left,  min, root-&amp;gt;data)
        &amp;amp;&amp;amp; isBSTUtil(root-&amp;gt;right, root-&amp;gt;data, max))
        return 1;
    else return 0;
}

int isBinarySearchTree(struct node *root) {
    return isBSTUtil(root, INT_MIN, INT_MAX);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;为什么要包一层：递归部分抽成工具函数 &lt;code&gt;isBSTUtil&lt;/code&gt;，外面再包一层只收根地址的 &lt;code&gt;isBinarySearchTree&lt;/code&gt;，因为调用者只想传根地址。&lt;/p&gt;
&lt;p&gt;复杂度：每个节点只访问一次，节点处判断是常数时间 → O(n)。&lt;/p&gt;
&lt;p&gt;这段代码没有处理重复值（要求左子树严格更小、右子树严格更大）。&lt;/p&gt;
&lt;h3&gt;方法三：中序遍历&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;对二叉树做中序遍历，若是 BST，读到的数据必然有序
技巧：遍历时记录前一个读过的节点
      任何时刻「当前节点数据必须大于前一个节点数据」
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个思路没给出实现代码。&lt;/p&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;一、检查子树&lt;/td&gt;
&lt;td&gt;每个节点验证其子树所有值&lt;/td&gt;
&lt;td&gt;O(n²)&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;O(n)&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;O(n)&lt;/td&gt;
&lt;td&gt;只讲思路&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;删除节点&lt;/h2&gt;
&lt;p&gt;在大多数数据结构中删除操作都很棘手，BST 也一样。&lt;/p&gt;
&lt;p&gt;删除后必须保留 BST 性质：每个节点左小右大。&lt;/p&gt;
&lt;p&gt;删除一个节点要做两件事：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 从父节点移除对该节点的引用（切断链接）
② 回收该节点的内存（C++ 用 delete，C 用 free）
&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;p&gt;情形三：被删节点有两个子节点。不能简单连一个子节点（会丢掉另一半子树）。做法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;① 在「右子树中找最小值」
② 把该最小值「复制/填入」被删节点
③ 再从右子树中删除那个「最小值节点」
   （它必无左孩子，于是情形三降级为情形一或情形二）
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;为什么是「右子树的最小值」&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;它来自右子树 → 一定大于被删节点
又是右子树最小值 → 右侧其余元素都 ≥ 它
左侧元素本就都更小
→ BST 性质保持
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;关键性质：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;树/子树中具有最小值的节点一定没有左孩子（有左孩子就会有更小的值），但可能有右孩子
  → 若有右孩子 → 降级为情形二
  → 若无右孩子 → 降级为情形一
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对称方案：也可以找左子树中的最大值来替换。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;左子树最大值 ≥ 左子树所有值，且因为来自左子树所以 &amp;lt; 被删值
该节点一定没有右孩子（有右孩子就有更大值），可能有左孩子
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码里用的是「右子树中的最小值」（&lt;code&gt;FindMin(root-&amp;gt;right)&lt;/code&gt;），没有出现「中序后继」这个词。两者本质等价。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node *Delete(struct node *root, int data) {
    if (root == NULL) return root;                    /* 空树直接返回 */
    else if (data &amp;lt; root-&amp;gt;data)
        root-&amp;gt;left  = Delete(root-&amp;gt;left,  data);      /* 去左子树删 */
    else if (data &amp;gt; root-&amp;gt;data)
        root-&amp;gt;right = Delete(root-&amp;gt;right, data);      /* 去右子树删 */
    else {                                            /* 找到了 */
        /* 情形一：无子节点 */
        if (root-&amp;gt;left == NULL &amp;amp;&amp;amp; root-&amp;gt;right == NULL) {
            delete root;                              /* C 用 free(root) */
            root = NULL;
        }
        /* 情形二：只有一个子节点 */
        else if (root-&amp;gt;left == NULL) {                /* 只有右孩子 */
            struct node *temp = root;
            root = root-&amp;gt;right;
            delete temp;
        }
        else if (root-&amp;gt;right == NULL) {               /* 只有左孩子 */
            struct node *temp = root;
            root = root-&amp;gt;left;
            delete temp;
        }
        /* 情形三：两个子节点 */
        else {
            struct node *temp = FindMin(root-&amp;gt;right); /* 右子树最小值 */
            root-&amp;gt;data = temp-&amp;gt;data;                  /* 复制值 */
            root-&amp;gt;right = Delete(root-&amp;gt;right, temp-&amp;gt;data);  /* 删掉那个副本 */
        }
    }
    return root;                                      /* 统一在所有分支之后返回 */
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;几个要点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;删除函数必须返回根节点指针，因为删除后根可能改变；传进来的只是根地址的本地副本，地址变了就得返回回去&lt;/li&gt;
&lt;li&gt;各分支的 &lt;code&gt;return root&lt;/code&gt; 合并到最后统一一条&lt;/li&gt;
&lt;li&gt;情形一删完后 &lt;code&gt;root&lt;/code&gt; 成为悬空指针（堆对象已释放但 root 仍持有地址），所以置为 &lt;code&gt;NULL&lt;/code&gt; 再返回&lt;/li&gt;
&lt;li&gt;父节点中的引用会在递归展开时被修正（&lt;code&gt;root-&amp;gt;left = Delete(...)&lt;/code&gt; / &lt;code&gt;root-&amp;gt;right = Delete(...)&lt;/code&gt;）&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;示例树与具体数字&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;示例树：根 5；左 3（左孩子 1）；右 12；12 的左孩子 7（7 的右孩子 9）
        另有 15（左 13、右 17）、19 等

叶子节点（4 个）：      1、9、13、19
只有一个子节点（2 个）：3（只有左孩子 1）、7（只有右孩子 9）
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;删 19           → 父节点 17 的子节点置空
删 7            → 把 9 设为 5 的右孩子（9 及其以下整棵子树接到 5 右侧）
删 3            → 把 1 设为 5 的左孩子
删 15（两个子节点）→ 右子树最小值 = 17 → 用 17 覆盖 15 的位置，
                   再删掉原 17 节点（17 只有一个子节点 19 → 情形二），最终树仍合法
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若 9 下面还有节点：9 的左孩子值必须满足「&amp;lt; 9、&amp;gt; 7、&amp;gt; 5、&amp;lt; 12」，只剩一个选择，只能用 8。&lt;/p&gt;
&lt;p&gt;用左子树最大值方案时：15 的左子树最大值为 14，复制 14，而 14 的节点没有左孩子 → 降级为情形一，直接删掉。&lt;/p&gt;
&lt;h2&gt;中序后继&lt;/h2&gt;
&lt;h3&gt;定义&lt;/h3&gt;
&lt;p&gt;中序后继（inorder successor）：在中序遍历序列中紧跟给定节点之后的那个节点。&lt;/p&gt;
&lt;p&gt;在 BST 中它就是树里下一个更大的值。&lt;/p&gt;
&lt;h3&gt;中序遍历回顾&lt;/h3&gt;
&lt;p&gt;示例 BST 的中序序列：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;6, 8, 10, 11, 12, 15, 16, 17, 20, 25, 27
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;恰好是排好序的整数序列。&lt;/p&gt;
&lt;p&gt;访问顺序里有个规律：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;从某节点的左孩子返回该节点时，该节点尚未被访问
从右孩子返回父节点时，父节点已被访问过
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;两种情形&lt;/h3&gt;
&lt;p&gt;情形一：该节点有右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;中序后继 = 右子树中的最左侧节点
         = 右子树中具有最小值的节点
做法：只要能向左就一直向左走
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情形二：该节点没有右子树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;需要「从根节点走到该节点」
找「最近的祖先节点，使得给定节点处于其左子树中」
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;逻辑：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;若从左边回到某祖先，该祖先尚未被访问 → 它就是后继
若从右边回到某祖先，该祖先已访问过   → 继续往上回退
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;具体例子&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;10 → 11     10 有右子树，右子树最小是 11
8  → 10     8 无右子树，从左边回到 10
12 → 15     12 无右子树；先到 10，但 12 在 10 的右边，排除；
            再到 15，12 在 15 的左边 → 15 是后继
6  → 8      从根走到 6 的路径上，6 在其左子树中的最深的那个祖先是 8
15 → 16、16 → 17、17 → 20、20 → 25、25 → 27
27（最大值）→ 没有后继，返回 NULL
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;12 → 15 这个例子最能说明「祖先 10 被排除、15 才符合」。&lt;/p&gt;
&lt;h3&gt;为什么不直接中序遍历一遍&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;中序遍历 O(n) 代价太高
BST 的插入/删除/搜索都是 O(h)，所以希望在 O(h) 内找到后继
平衡二叉树高度 = log₂N，O(log n) 几乎是最佳运行时间
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;父指针方案&lt;/h3&gt;
&lt;p&gt;问题：怎么从一个节点走到它的父节点？&lt;/p&gt;
&lt;p&gt;方案：把节点定义从 3 个字段扩成 4 个，多存一个父节点的地址，就能靠父链接向上遍历找祖先。&lt;/p&gt;
&lt;p&gt;但若不存在父指针，就退化为「从根节点走到给定节点，记录沿途祖先」。&lt;/p&gt;
&lt;h3&gt;代码&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;struct node *Getsuccessor(struct node *root, int data) {
    struct node *current = Find(root, data);      /* 先找到该节点 */
    if (current == NULL) return NULL;             /* 树中不存在 */

    if (current-&amp;gt;right != NULL) {                 /* 情形一：有右子树 */
        return FindMin(current-&amp;gt;right);           /* 右子树最小值（最左节点） */
    }
    else {                                        /* 情形二：无右子树 */
        struct node *successor = NULL;
        struct node *ancestor  = root;
        while (ancestor != current) {
            if (current-&amp;gt;data &amp;lt; ancestor-&amp;gt;data) { /* current 在祖先左侧 */
                successor = ancestor;             /* 这个祖先「可能是」后继 */
                ancestor  = ancestor-&amp;gt;left;       /* 继续向左，找更深的 */
            }
            else {                                /* current 在祖先右侧 */
                ancestor = ancestor-&amp;gt;right;       /* 只向右走，不更新 successor */
            }
        }
        return successor;                         /* 可能为 NULL */
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;情形二里只有在向左走时才更新 successor，也就是「最后一个左拐的祖先」。&lt;/p&gt;
&lt;p&gt;接口设计上，也可以直接把数据作为参数（本实现就是），或者传当前节点指针以省掉一次搜索。&lt;/p&gt;
&lt;h3&gt;复杂度&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;Find 找节点：O(h)
FindMin：O(h)
从根走到某节点：O(h)

→ 整个 Getsuccessor 的总时间复杂度 = O(h)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平衡树时就是 O(log n)。&lt;/p&gt;
&lt;h2&gt;复杂度汇总&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;判断是否为 BST（传上下界法）   O(n)
判断是否为 BST（检查子树法）   O(n²)
删除节点                       O(h)
找中序后继                     O(h)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;判断 BST 不能只看根和直接子节点，反例 D 就是根合法但子树里有问题；检查子树法是 O(n²)，传上下界法才是 O(n)。&lt;/li&gt;
&lt;li&gt;传上下界法用的是严格不等式，不处理重复值。&lt;/li&gt;
&lt;li&gt;删除有两个孩子的节点用「右子树最小值」（= 中序后继），也可以对称地用「左子树最大值」；最小值节点必无左孩子，最大值节点必无右孩子，这是降级的关键。&lt;/li&gt;
&lt;li&gt;删除函数必须返回根指针，且情形一删完要把 root 置 NULL，否则是悬空指针。&lt;/li&gt;
&lt;li&gt;中序后继情形二只更新「向左走」时的祖先，向右走不更新；最大值节点没有后继，返回 NULL。找后继不要用中序遍历（O(n)），要利用 BST 性质做到 O(h)。&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>完全二叉树</title><link>https://me.622168.xyz/posts/data-structure/complete-binary-tree/</link><guid isPermaLink="true">https://me.622168.xyz/posts/data-structure/complete-binary-tree/</guid><description>完全二叉树的定义、性质，以及为什么它适合用数组存储。</description><pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;一句话定义&lt;/h2&gt;
&lt;p&gt;除最后一层外，每一层都被节点填满；最后一层的节点全部靠左连续排列。这样的二叉树叫完全二叉树。&lt;/p&gt;
&lt;p&gt;关键在「靠左连续」这四个字。最后一层可以不满，但中间不能空出位置。&lt;/p&gt;
&lt;h2&gt;和满二叉树的区别&lt;/h2&gt;
&lt;p&gt;满二叉树是每一层都填满，节点数刚好是 &lt;code&gt;2^h - 1&lt;/code&gt;。满二叉树一定是完全二叉树，反过来不成立。&lt;/p&gt;
&lt;p&gt;（有的教材管这个叫「完美二叉树」，同一回事。）&lt;/p&gt;
&lt;h2&gt;几条常用性质&lt;/h2&gt;
&lt;p&gt;设节点总数为 &lt;code&gt;n&lt;/code&gt;：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;高度是 &lt;code&gt;floor(log2(n)) + 1&lt;/code&gt;（&lt;strong&gt;按层数&lt;/strong&gt;算）。如果按&lt;strong&gt;边数&lt;/strong&gt;算就是 &lt;code&gt;floor(log2(n))&lt;/code&gt;——课程里一律用边数，所以那边写的是不带 +1 的版本。&lt;/li&gt;
&lt;li&gt;叶子节点从下标 &lt;code&gt;floor(n/2) + 1&lt;/code&gt; 开始（下标从 1 算起）&lt;/li&gt;
&lt;li&gt;只有最后一个非叶子节点可能只有左孩子，没有右孩子&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;为什么能用数组存&lt;/h2&gt;
&lt;p&gt;因为节点位置连续，不会出现空洞，所以下标可以直接算出父子关系。下标从 1 开始时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 节点 i 的左孩子、右孩子、父节点
left   = 2 * i;
right  = 2 * i + 1;
parent = i / 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;下标从 0 开始时：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;left   = 2 * i + 1;
right  = 2 * i + 2;
parent = (i - 1) / 2;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不用存指针，省空间，访问也快。堆（优先队列）就是靠这个性质实现的。&lt;/p&gt;
&lt;h2&gt;判断是不是完全二叉树&lt;/h2&gt;
&lt;p&gt;层序遍历，遇到空节点就停止往队列里加，然后检查队列里剩下的是否全为空。如果后面还出现了非空节点，说明中间有空洞，不是完全二叉树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;bool isComplete(Node *root) {
    if (!root) return true;

    Node *queue[MAXN];
    int head = 0, tail = 0;
    queue[tail++] = root;
    bool metNull = false;

    while (head &amp;lt; tail) {
        Node *cur = queue[head++];
        if (cur == NULL) {
            metNull = true;      // 记录已经遇到过空位
        } else {
            if (metNull) return false;   // 空位后面又出现节点，有空洞
            queue[tail++] = cur-&amp;gt;left;
            queue[tail++] = cur-&amp;gt;right;
        }
    }
    return true;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;坑&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;最后一层节点数可以是 1 到 &lt;code&gt;2^(h-1)&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>怎么用这个笔记本</title><link>https://me.622168.xyz/posts/notes/how-to-use/</link><guid isPermaLink="true">https://me.622168.xyz/posts/notes/how-to-use/</guid><description>记录一下我自己怎么往这个网站里加笔记，忘了就回来看这篇。</description><pubDate>Sun, 06 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h2&gt;加一篇新笔记&lt;/h2&gt;
&lt;p&gt;在项目目录里执行，&lt;code&gt;xxx&lt;/code&gt; 换成文件名（用英文，别带空格）：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pnpm new-post 数据结构/二叉树
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它会在 &lt;code&gt;src/content/posts/&lt;/code&gt; 下生成一个 &lt;code&gt;.md&lt;/code&gt; 文件，开头那一段是文章信息，下面直接写正文。&lt;/p&gt;
&lt;h2&gt;开头那一段怎么填&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;---
title: 完全二叉树          # 标题，写中文没问题
published: 2026-09-06     # 日期
description: 一句话说明   # 列表页会显示这句
tags: [&quot;数据结构&quot;]        # 标签，可以多个
category: 数据结构        # 分类，只能一个
draft: false              # 改成 true 就是草稿，网站上看不见
---
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;正文只要记住这几个&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;## 二级标题
### 三级标题

**加粗**

- 列表项
- 列表项

1. 有序列表
2. 有序列表

`行内代码`

[链接文字](https://example.com)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;代码块用三个反引号包住，后面写语言名：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;int main(void) {
    return 0;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;本地看效果&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后浏览器打开终端里给出的地址，改完文件保存，页面会自动刷新。&lt;/p&gt;
&lt;h2&gt;发布出去&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;git add .
git commit -m &quot;新增笔记&quot;
git push
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;推上去之后 Cloudflare Pages 会自动重新构建，一两分钟后网站就更新了。&lt;/p&gt;
</content:encoded></item></channel></rss>