[{"permalink":"https://zhr0529.cn/posts/从-vlm-到-vla-具身智能系统中的模型-环境与控制层/","title":"从 VLM 到 VLA：具身智能系统中的模型、环境与控制层","content":"大语言模型、视觉语言模型、视觉语言动作模型、世界模型和具身智能经常出现在同一段讨论中，但它们并不处于同一个分类层级。LLM、VLM、VLA 描述模型处理的信息和输出形式；世界模型描述对环境动态的建模方式；具身智能描述智能体通过身体与环境持续交互的研究方向；真实世界与仿真则是数据产生和系统运行的环境。 区分这些术语，最有效的方法不是比较模型名称，而是依次确认四件事：系统接收什么输入、产生什么输出、输出是否会改变环境、动作之后是否还会根据新观测继续决策。 一、完整技术框架 具身系统的基本运行方式是闭环：传感器获取当前观测，系统理解任务和环境状态，规划下一步行动，机器人执行动作，随后再次观察动作造成的结果。只要环境会变化、观测存在误差或动作可能失败，这个循环就不能省略。 具身智能的感知—决策—行动闭环 在工程系统中，这个闭环通常包含以下层次： 1. 传感器层采集 RGB 图像、深度图、点云、关节位置、末端位姿、力觉和触觉等数据。 2. 感知与理解层识别对象、空间关系、可操作区域、任务条件和当前执行状态。 3. 任务规划层把自然语言目标拆成子目标，选择技能，并决定执行顺序。 4. 策略层根据观测和任务生成动作或一段动作序列。 5. 运动规划与控制层处理碰撞约束、逆运动学、轨迹跟踪、速度、位置或力矩控制。 6. 执行与反馈层检查动作结果，识别失败并触发重试、纠正或重新规划。 这些功能可以由一个端到端模型承担一部分，也可以由多个专用模块组合完成。VLA 是其中的一类策略模型，不是整个机器人系统的同义词。 二、LLM：以语言序列为核心的大模型 LLM 是 Large Language Model 的缩写，中文通常写作大语言模型。它通过大规模文本训练学习语言序列的统计结构，现代 LLM 大多采用 Transformer 架构。其核心接口可以写成： x 1 : n ​ → p ( x n + 1 ​ ∣ x 1 : n ​ ) 模型根据已有 token 序列预测后续 token，再通过连续生成完成问答、摘要、代码生成和任务分解等工作。Transformer 论文给出了以注意力机制为核心的序列建模架构，GPT 3 则展示了扩大自回归语言模型后形成的少样本任务能力。 LLM 可以为机器人生成高层步骤，也可以调用外部工具，但普通 LLM 并不天然接收相机图像、关节状态和力觉数据，也不天然输出满足机器人动力学约束的控制量。语言计划与物理动作之间仍然存在感知落地、状态估计、动作生成和安全控制等环节。 MLLM 与 LMM MLLM 通常展开为 Multimodal Large Language Model，强调以语言模型为核心接入图像、视频、音频等模态。LMM 展开为 Large Multimodal Model，范围更宽，不一定把语言置于绝对中心。论文和产品文档中经常混用这两个名称，因此阅读时应优先看模型的输入、输出和训练目标，而不是只看缩写。 三、VLM：视觉信息与语言对齐 VLM 是 Vision Language Model 的缩写，中文为视觉语言模型。它联合处理图像或视频与文本，使视觉内容能够被语言查询、描述或用于推理。常见接口包括： 图像或视频 + 文本 → 文本、类别、坐标或跨模态表示 VLM 的输出并不局限于自然语言。CLIP 通过图像与文本的对比学习获得共享表示，适合开放词汇分类和检索；带生成式语言模型的 VLM 可以完成图像描述、视觉问答、OCR、目标定位和视觉推理。PaLM E 进一步把视觉和连续状态估计编码接入语言模型，能够处理机器人规划等具身任务，但它在论文中的定位仍是具身多模态语言模型，并不等同于直接输出低层控制动作的 VLA。 对机器人而言，VLM 的主要价值是把任务语言与视觉场景关联起来。它可以判断目标物体、关系和语义条件，也可以生成操作步骤。若输出仍是文本计划、目标标签或坐标，系统还需要策略、运动规划和控制模块才能执行。 四、VLA：多模态模型直接预测动作 VLA 是 Vision Language Action Model 的缩写，中文为视觉语言动作模型。RT 2 论文把这类模型定义为同时利用视觉、语言和机器人动作数据，并把机器人动作纳入统一建模过程的模型。其典型接口是： 视觉观测 + 语言任务 + 机器人状态或历史 → 动作或动作序列 VLM 与 VLA 的输入输出边界 RT 2 将连续机器人动作离散化为 token，使动作数据可以与视觉语言任务共同训练。OpenVLA 采用视觉编码器与 Llama 2 语言模型组成 7B 参数模型，在 Open X Embodiment 的 97 万条真实机器人轨迹上训练，并同样预测离散化动作。这里的动作可表示末端执行器的位姿增量、旋转、夹爪开合或其他机器人相关控制维度。 离散动作 token 只是 VLA 的一种实现。VLA 也可以预测连续动作，用扩散模型生成动作分布，或一次预测多个时间步的动作块。决定模型是否属于 VLA 的关键不是动作是否文本化，而是模型是否把视觉、语言条件与可执行动作映射放在同一个策略中学习。 VLA 相比 VLM 多完成了从语义理解到动作选择的一段映射，但仍不代表模型输出可以不经处理直接写入电机。实际部署通常还要加入动作反归一化、坐标系转换、可达性检查、碰撞检测、速度与力矩限制、急停逻辑和底层控制器。 五、具身智能 具身智能不是某一种模型，而是一类研究问题：智能体具有可感知和可行动的身体，在环境中通过连续交互完成任务，并让行动结果影响后续观测与决策。 这个定义有三个边界需要明确。 第一，具身智能不等于人形机器人。机械臂、移动机器人、无人机、自动驾驶系统和带虚拟身体的仿真智能体都可以构成具身系统。身体形态会决定传感器、动作空间、可达范围和动力学约束，研究中常用 embodiment 表示这种载体及其能力边界。 第二，机器人不一定具有具身智能。按照固定轨迹重复焊接的工业机器人具备物理身体，但如果它不根据环境变化进行感知和闭环决策，通常不会被作为具身智能系统讨论。 第三，具身智能并不要求所有模块都由神经网络实现。学习模型可以负责感知、技能选择或策略，传统运动规划、状态估计和反馈控制仍然可以承担关键功能。系统是否具身，取决于它是否通过身体与环境形成感知—决策—行动闭环，而不是取决于模型数量或端到端程度。 六、真实世界与仿真 真实世界在机器人学习中通常指物理机器人、真实传感器、真实物体和真实场地组成的运行环境。真实世界数据来自实际执行过程，因此会包含传感器噪声、照明变化、相机标定误差、摩擦、柔性、接触不确定性、通信延迟、执行器磨损和人为干扰。训练或评估时还必须考虑设备成本、操作安全、恢复时间和数据采集速度。 仿真环境由物理引擎、渲染器和任务逻辑构成。它可以快速生成大量交互数据，精确读取系统状态，方便重置场景，也允许测试危险动作。MuJoCo、Isaac Sim、Gazebo、PyBullet、Habitat 和 ManiSkill 都属于常见仿真平台或生态，但它们覆盖的物理精度、传感器模拟、任务类型和计算效率并不相同。 MuJoCo 中的 body、geom 与 site 元素 仿真不等于真实世界的完整复制。仿真模型省略或近似了材料形变、摩擦变化、相机噪声、线缆干扰和硬件老化等因素，由此形成 sim to real gap，也称仿真到现实差距。常用缩小方法包括： 系统辨识：用真实测量估计质量、摩擦、延迟和执行器参数。 域随机化：在训练时随机改变纹理、光照、相机、物理参数和物体布局，减少策略对单一仿真条件的依赖。 域适配：对齐仿真数据与真实数据的特征或分布。 真实数据微调：先在仿真中获得基础能力，再用少量真实轨迹适配。 混合训练：同时使用仿真数据、真实数据和模型生成的数据。 MuJoCo 中的程序化地形生成 ​ MuJoCo 中的程序化地形生成示例 域随机化论文展示了只用随机纹理仿真图像训练的目标定位模型向真实抓取任务迁移的结果。这个方法说明增加仿真变化有助于迁移，但不能推出真实数据已经不再需要。接触丰富、柔性物体、精细装配和长期运行等任务仍然高度依赖真实系统校准与评估。 七、世界模型：学习环境如何随动作变化 世界模型是对环境动态的可学习表示。它根据当前状态或观测以及候选动作，预测下一状态、下一观测、奖励、终止概率或这些量的潜在表示。一个常见形式是： p ( s t + 1 ​ , r t ​ , d t ​ ∣ s t ​ , a t ​ ) 当高维图像先被编码为潜在状态时，模型也可以在潜在空间中预测未来。Ha 与 Schmidhuber 的 World Models 将环境压缩表示、时间动态模型和控制器组合起来；DreamerV3 则在学习到的环境模型中预测未来并改进策略。 世界模型可以支持多种用途： 在执行前模拟多个动作后果，辅助规划和模型预测控制。 在潜在空间中生成训练轨迹，降低真实交互成本。 预测碰撞、失败、奖励或任务终止。 估计不可直接观测的状态，帮助策略利用历史信息。 世界模型与 VLA 不是互斥概念，也不是上下位关系。VLA 可以完全不显式预测未来，只学习观测到动作的映射；也可以在内部或外部接入世界模型，对候选动作进行评估。能够生成视频的模型也不自动成为机器人世界模型，只有当它能在动作条件下提供对决策有用的环境动态预测时，这个称呼才更准确。 八、策略、规划与控制 这三个词都与行动有关，但处理的时间尺度和约束不同。 策略 策略 policy 描述在给定状态、观测和任务条件下如何选择动作： a t ​ ∼ π ( a t ​ ∣ o ≤ t ​ , g ) 其中 o ≤ t ​ 是当前及历史观测， g 是任务条件。策略可以是确定性的，也可以输出动作概率分布。VLA、Diffusion Policy 和 RT 1 都可以作为策略模型。策略学习关注从数据或交互中获得动作映射。 任务规划与运动规划 任务规划决定做什么以及先后顺序，例如定位目标、接近目标、抓取、移动和放置。SayCan 使用语言模型给出的技能相关性与技能价值函数结合，在机器人已有技能中选择可行步骤。 运动规划决定身体怎样从当前构型移动到目标构型，通常要满足关节限制、碰撞约束、速度限制和动力学条件。它的输出可能是关节路径、末端轨迹或带时间参数的运动序列。 控制 控制器负责让真实机器人跟踪目标位置、速度、轨迹或力。位置控制、速度控制、阻抗控制和力矩控制都属于这一层。控制频率通常高于高层规划和视觉策略频率，但具体数值由机器人、传感器和任务决定。 任务规划、策略和控制可以组成分层系统，也可以由端到端模型跨越部分边界。工程上仍应保留明确的接口，因为安全约束、实时性和故障诊断通常需要独立处理。 九、机器人模型如何训练 1. 自监督与多模态预训练 模型先在文本、图像—文本对、视频或无标注机器人数据上学习通用表示。CLIP 的图文对比学习、VLM 的生成式视觉语言训练都属于这一类。预训练可以提高语义能力和视觉泛化，但网络数据不会自动提供准确的机器人动作知识。 2. 模仿学习与行为克隆 行为克隆把专家轨迹中的观测—动作对作为监督数据，直接训练策略预测专家动作。数据可以由人工遥操作、示教器、脚本控制器、传统规划器或已有策略产生。 行为克隆的主要问题是训练时看到的状态来自专家，部署时的状态却会被模型自身误差逐步改变。DAgger 通过迭代收集当前策略访问到的状态并请求专家标注，缓解这种分布偏移。实践中还会使用数据重采样、噪声注入、失败恢复轨迹和在线纠正来覆盖偏离状态。 3. 强化学习 强化学习根据环境反馈优化累计回报。它适合目标可以通过奖励表达、但难以逐步标注正确动作的任务。训练可以发生在真实机器人、仿真环境或学习到的世界模型中。机器人强化学习必须额外处理探索安全、样本效率、奖励设计和硬件损耗。 4. 离线学习 离线模仿学习或离线强化学习只使用预先收集的数据集训练，不在训练过程中持续访问真实环境。它便于复用大规模轨迹并降低在线风险，但策略容易在数据覆盖范围之外失真，因此需要谨慎评估分布外动作。 5. 联合训练与协同微调 RT 2 同时使用机器人轨迹和互联网视觉语言任务进行协同微调，把动作离散化后与文本 token 放入统一输出空间。OpenVLA 则从预训练 VLM 出发，在多来源真实机器人演示上训练动作预测。联合训练的目标是保留视觉语言语义能力，同时建立观测到动作的映射。 6. 跨机器人数据与动作对齐 不同机器人拥有不同关节数、相机位置、工作空间和控制接口，不能直接把原始动作拼接到同一数据集中。Open X Embodiment 将 22 种机器人数据整理为标准格式，并训练 RT X 研究跨机器人迁移。实际处理通常包括统一坐标约定、动作归一化、时间对齐、缺失模态处理和机器人身份条件化。 7. 微调、参数高效适配与量化 通用策略部署到新机器人或新场景时，通常仍需任务数据。全量微调会更新全部参数，参数高效方法只训练少量适配参数，量化则降低显存和推理成本。OpenVLA 提供了 LoRA 微调和量化部署方案，但模型许可证、基础模型许可证、数据许可证和代码许可证需要分别检查，不能只依据代码仓库许可证判断模型是否可商用。 十、一个完整任务怎样执行 以把红色杯子放到托盘为例，完整链路可以拆成以下步骤： 1. 接收任务：语言模块解析目标物体、目标位置和完成条件。 2. 获取观测：相机、深度传感器和机器人状态接口提供当前信息。 3. 场景理解：VLM 或感知模型识别红色杯子、托盘、障碍物和空间关系。 4. 状态落地：系统把语言中的对象与图像区域、三维位置和可操作部位对应起来。 5. 任务规划：系统决定接近、预抓取、闭合夹爪、抬升、移动、放置和撤离等阶段。 6. 动作生成：VLA 或专用策略根据当前观测预测末端位姿增量、夹爪动作或动作块。 7. 安全与运动约束：规划器或安全层检查碰撞、关节范围、速度和工作空间限制。 8. 底层控制：控制器把轨迹转换成执行器可跟踪的指令，并处理位置、速度或力反馈。 9. 闭环校正：系统重新观察杯子是否抓稳、是否滑动、托盘位置是否变化，并调整动作。 10. 完成判断：视觉、力觉或任务状态判断杯子是否稳定放入托盘；失败时执行重试或重新规划。 VLM 可以主要承担第 3 至第 5 步，VLA 可以覆盖第 3 至第 6 步的一部分，世界模型可以在第 5 至第 7 步预测动作后果。无论模型覆盖多少步骤，真实系统仍需第 7 至第 10 步的约束、执行与反馈。 十一、常见误区 VLM 能描述动作，因此就是 VLA 不成立。VLM 输出一段操作文字，与输出和机器人动作空间对齐、能进入控制链路的动作表示不同。判断依据应是训练数据、输出空间和执行接口。 VLA 等于完整的具身智能系统 不成立。VLA 是策略模型的一种。传感器同步、状态估计、运动规划、安全控制、执行器驱动、故障恢复和完成检测仍属于系统的一部分。 端到端模型不需要控制器 不成立。端到端通常表示从多模态观测直接预测较高层动作，但电机驱动仍需要实时控制、限幅和安全保护。端到端描述学习接口，不代表物理执行链路消失。 真实世界数据就是未经处理的原始数据 不成立。真实数据也要经过时间同步、标定、清洗、切片、动作归一化和质量筛选。真实世界描述数据来源，不描述数据处理程度。 仿真数据足够多就能完全替代真实数据 不成立。仿真可以扩大覆盖范围并降低成本，但模型误差会在接触、摩擦、柔性和传感器噪声中暴露。最终能力仍需在目标硬件和目标环境中验证。 世界模型就是更大的 VLM 或视频生成模型 不成立。世界模型的核心是预测环境状态如何在动作条件下演化，并服务于决策。生成视觉上合理的视频不足以证明其物理预测适合控制。 具身智能只研究人形机器人 不成立。具身智能关注身体、环境和闭环交互，人形只是一种 embodiment。 模型规模越大，机器人泛化就一定越好 不成立。机器人能力还受训练数据覆盖、动作标注质量、控制频率、传感器配置、机器人形态和安全约束影响。扩大模型不能弥补动作数据或系统接口的根本缺失。 十二、术语速查表 术语 中文 典型输入 典型输出 关键边界 LLM 大语言模型 文本 token 文本或结构化 token 以语言序列建模为核心，不天然连接机器人动作空间 MLLM 多模态大语言模型 文本、图像、视频、音频等 文本、结构化结果或多模态内容 通常以 LLM 为核心接入其他模态 LMM 大型多模态模型 多种模态 多种模态或表示 比 MLLM 更宽泛，实际文献中常混用 VLM 视觉语言模型 图像或视频、文本 文本、类别、坐标或表示 建立视觉与语言的对应关系，不必输出机器人动作 VLA 视觉语言动作模型 视觉、语言、机器人状态或历史 动作、动作 token 或动作块 输出与机器人动作空间对齐，可进入行动链路 具身智能 通过身体与环境交互的智能系统与研究方向 感知、任务、身体状态 持续决策与行动 强调感知—决策—行动闭环，不限定机器人外形 Embodiment 身体形态及能力边界 传感器、关节、执行器、工作空间 可感知和可执行的范围 同一任务在不同机器人上会形成不同状态与动作空间 真实世界 物理运行与数据采集环境 真实传感器和物理状态 带噪声、延迟和风险的交互数据 是环境类别，不是模型类型 仿真 计算机生成的物理与视觉环境 场景、物理参数、控制指令 合成观测、状态和反馈 便于扩展与重置，但存在仿真到现实差距 世界模型 环境动态预测模型 当前状态或观测、动作 未来状态、观测、奖励或潜变量 关注动作条件下的未来演化，不等同于策略 Policy 策略 状态或观测、任务 动作分布或动作 回答当前条件下采取什么动作 Task Planning 任务规划 目标、场景、技能集合 子目标与技能顺序 处理做什么和先后顺序 Motion Planning 运动规划 起点、终点、机器人模型、障碍物 无碰撞路径或轨迹 处理身体怎样移动 Control 控制 目标轨迹、实时状态 位置、速度或力矩指令 高频跟踪与稳定执行 Behavior Cloning 行为克隆 专家观测—动作对 模仿专家的策略 简单有效，但容易受分布偏移影响 Reinforcement Learning 强化学习 环境交互与奖励 最大化累计回报的策略 依赖奖励、探索和交互成本 Sim to Real 仿真到现实迁移 仿真训练结果、少量真实数据 可在真实系统工作的模型 需要处理视觉和动力学差异 结语 这些术语可以放在同一条系统链路上理解：LLM 提供语言知识与任务分解能力，VLM 把语言与视觉场景对齐，VLA 进一步把多模态条件映射到机器人动作，世界模型预测动作可能造成的未来变化，规划器和控制器把决策变成满足约束的物理运动。具身智能覆盖的是这条链路与环境反复交互的完整闭环，真实世界和仿真则决定数据与反馈从哪里产生。 因此，阅读一篇机器人论文时，先确认输入、输出、训练数据、动作空间和部署闭环，比依据模型名称判断能力更可靠。 参考资料 1. Vaswani, A. et al. 2017. Attention Is All You Need. NeurIPS 2017. 2. Brown, T. B. et al. 2020. Language Models are Few Shot Learners. NeurIPS 2020. 3. Radford, A. et al. 2021. Learning Transferable Visual Models From Natural Language Supervision. ICML 2021. 4. Duan, J. et al. 2022. A Survey of Embodied AI: From Simulators to Research Tasks. IEEE Transactions on Emerging Topics in Computational Intelligence. 5. Driess, D. et al. 2023. PaLM E: An Embodied Multimodal Language Model. ICML 2023. 6. Brohan, A. et al. 2022. RT 1: Robotics Transformer for Real World Control at Scale. arXiv preprint. 7. Brohan, A. et al. 2023. RT 2: Vision Language Action Models Transfer Web Knowledge to Robotic Control. Conference on Robot Learning 2023. 8. Kim, M. J. et al. 2024. OpenVLA: An Open Source Vision Language Action Model. Conference on Robot Learning 2024. 9. O’Neill, A. et al. 2023. Open X Embodiment: Robotic Learning Datasets and RT X Models. IEEE International Conference on Robotics and Automation 2024. 10. Ahn, M. et al. 2022. Do As I Can, Not As I Say: Grounding Language in Robotic Affordances. arXiv preprint. 11. Chi, C. et al. 2023. Diffusion Policy: Visuomotor Policy Learning via Action Diffusion. Robotics: Science and Systems 2023. 12. Ross, S., Gordon, G. J., and Bagnell, J. A. 2011. A Reduction of Imitation Learning and Structured Prediction to No Regret Online Learning. AISTATS 2011. 13. Tobin, J. et al. 2017. Domain Randomization for Transferring Deep Neural Networks from Simulation to the Real World. IEEE/RSJ International Conference on Intelligent Robots and Systems 2017. 14. Ha, D. and Schmidhuber, J. 2018. World Models. arXiv preprint. 15. Hafner, D. et al. 2023. Mastering Diverse Domains through World Models. arXiv preprint. 16. Todorov, E., Erez, T., and Tassa, Y. 2012. MuJoCo: A Physics Engine for Model Based Control. IEEE/RSJ International Conference on Intelligent Robots and Systems 2012.","summary":"梳理 LLM、VLM、VLA、世界模型与具身智能的边界，以及仿真、训练、规划和控制在机器人闭环中的位置。"},{"permalink":"https://zhr0529.cn/posts/基于pinn和历元间连续性约束的-gnss-ins-融合定位研究手记/","title":"基于 PINN 和历元间连续性约束的 GNSS/INS 融合定位研究手记","content":"这段时间主要在整理一套面向城市道路的 GNSS/INS 融合定位方法。城市峡谷中的伪距观测经常受到遮挡、多路径和 NLOS 信号影响，单点定位结果会出现明显偏移，天方向尤其不稳定。INS 能够提供短时间内连续的运动信息，但低成本 IMU 的误差又会随积分累积。因此，这次没有直接做完整的惯导递推，而是先保留 GNSS 加权最小二乘解算，再让网络学习它与参考位置之间的误差。 从 WLS 结果上做位置修正 整个方案的起点比较直接。每个历元先用伪距完成 WLS 解算，网络不回归绝对坐标，只预测 ENU 坐标系下的位置修正量： p t final ​ p t WLS ​ + Δ p t NN ​ . 这样处理后，卫星几何关系仍由传统解算负责，网络只需要学习复杂环境中的误差补偿。相比直接预测位置，修正量的范围更集中，训练时也更容易做归一化。 输入特征最后整理为 78 维。其中 50 维来自按高度角选取的 10 颗卫星，每颗卫星保留信噪比、高度角、伪距残差、码减载波散度和残差历元间变化；21 维用于描述 IMU 与接收机速度反映的短时运动；剩余 7 维记录 HDOP、VDOP、PDOP、卫星数量、残差 RMS 和 WLS 收敛状态等上下文信息。网络采用双分支结构，卫星分支通过共享映射和注意力汇聚逐星特征，另一分支处理运动与解算质量信息，融合后输出三维位置修正量。 GNSS/INS 融合定位系统结构 初版 PINN 并没有直接得到好结果 最初设计 PINN 损失时，我把接收机速度当作较可靠的监督信息，让网络同时预测位置修正和速度，并加入位置速度一致性约束。训练能够正常收敛，但测试误差明显高于之前的普通网络。继续检查后发现，当时拿来比较的旧基线使用了更长的训练时间段，与测试区间发生了重叠。原先看起来很好的结果并不是公平的测试结果。 阶段记录｜先把基线重新做对 现象：旧模型测试误差低于训练误差，而两个版本的训练样本数量明显不同。 排查：训练结束时间进入了测试区间，模型提前见过部分测试数据。 处理：统一时间边界，按连续时间块重新划分训练集、验证集和测试集，再比较是否加入物理约束。 另一个影响更大的问题来自时间基准。GNSS 观测采用 GPS time，参考轨迹在转换时需要处理 18 秒闰秒偏移。没有对齐时，模型学习的是不同时间位置之间的差值，GNSS only、普通网络和 PINN 的结果都会一起失真。修正时间对齐后，各方法的误差才回到能够继续分析的范围。这次排查也让我把数据划分和时间同步固定成训练前的必要检查，而不是等结果异常后再处理。 最后保留的物理约束 数据问题处理完后，PINN 的重点重新放回相邻历元的位置关系。相邻两个历元的修正量变化，应当与速度积分位移和 WLS 位移之间的差保持一致： L kin ​ Huber ( Δ c t ​ , v ˉ t ​ Δ t − Δ p t GNSS ​ ) . 完整损失由数据拟合、历元连续性、天向速率和残差幅度正则组成： L L data ​ + ρ ( t ) ( λ kin ​ L kin ​ + λ alt ​ L alt ​ ) + λ reg ​ L reg ​ . 训练前 20 个 epoch 不启用运动约束，之后再用 20 个 epoch 线性增加权重，避免网络尚未学会基本位置修正时就被平滑项限制。幅度正则则对模长超过阈值的修正量施加软惩罚，用来减少极端环境下的过补偿。速度信息最终仍作为输入特征保留，但不再作为硬监督目标。多次实验中，直接监督速度没有带来稳定收益，反而容易把受遮挡影响的速度误差写进训练梯度。 阶段记录｜约束并不是越多越好 初步方案：位置、速度、运动学和平滑项同时加入损失。 实验结果：速度监督的贡献不稳定，残差幅度正则对大误差抑制更明显。 最终决定：速度用于描述运动状态，训练目标集中在位置拟合、历元连续性和修正幅度。 实验结果 实验使用 GVINS 的 urban driving 序列。GNSS only 的二维和三维 RMSE 分别为 11.12 m 和 36.03 m；普通无约束网络将三维 RMSE 降到 17.75 m；加入物理约束后，二维 RMSE 为 8.40 m，三维 RMSE 为 13.09 m。相对 GNSS only，三维误差降低 63.7%；相对无约束网络，三维误差进一步降低 26.2%。三维 P95 也从 61.4 m 降到 24.8 m，说明改善主要体现在城市峡谷中容易出现的大误差历元。 不同方法的定位误差综合对比 轨迹图中的差异在天方向最明显。GNSS only 存在持续的垂向偏移，无约束网络仍会产生较大的局部修正，而加入连续性和幅度约束后，天向跳变有所收敛。这里并不能说明网络已经替代传统组合导航，但至少表明训练阶段加入合理的运动约束，能够改善单纯逐历元回归带来的不稳定输出。 urban driving 序列轨迹对比 这次记录下来的几点 消融实验中，去掉残差幅度正则后三维 RMSE 上升到 15.49 m，它对结果的影响大于单独去掉历元连续性约束。这说明当前数据上的主要问题不只是相邻历元不平滑，网络在异常观测下给出过大的修正同样需要控制。 回看整个过程，真正占用时间的并不是网络层数，而是确认训练与测试是否独立、不同传感器的时间是否对齐，以及每一项约束使用的观测量是否足够可靠。现在的方案仍只在一个城市道路序列上完成验证，后续还需要增加不同路线、接收机和遮挡条件下的实验，再判断这些约束能否保持同样的效果。 相关材料 基于 PINN 和历元间连续性约束的 GNSS/INS 融合定位方法 GVINS: Tightly Coupled GNSS Visual Inertial Fusion for Smooth and Consistent State Estimation","summary":"记录一次从 GNSS 加权最小二乘误差补偿出发，经数据对齐、约束调整和消融验证完成 PINN-GNSS/INS 融合定位方案的过程。"},{"permalink":"https://zhr0529.cn/posts/c1-有监督的机器学习-回归和分类/","title":"C1 - 有监督的机器学习：回归和分类","content":"机器学习简介 有监督学习 有监督学习 指的是 学习从 输入 x (一个或多个) 映射到 输出 y 的算法 。有监督学习算法的关键在于首先要提供正确的样本示例供算法学习，然后算法便可以针对未见过的输入，输出相应的预测结果。下面是一些有监督学习的在现实生活中的示例： 垃圾邮件过滤器：email 垃圾邮件?(0/1) 语音识别：语音 文本 机器翻译：英文 中文 广告投递：广告、用户信息 用户点击?(0/1) 自动驾驶：图片、雷达信息 其他车辆位置 视觉检测：手机图片 有缺陷?(0/1) “有监督学习”中两类最常见的典型问题就是 回归(Regression) 和 分类(Classification) 。两者的主要区别在于： 回归问题：要预测的结果有无穷种可能，比如在一段范围内都有可能的数字取值。 分类问题：只有有限种可能的输出结果，比如前面提到的判断某个邮件是否为垃圾邮件。 注1：任何预测数字的“有监督学习”模型，就是解决所谓的“回归问题”。 注2：在“分类问题”中，输出“类别”的英文是class或category，两者可以混用。 回归问题示例：房价预测 “房价预测”就是根据房子的面积计算价格。下图中的“红叉”就是预先提供的有正确映射关系的样本，“蓝色拟合线”就相当于算法学习输入样本，最后通过拟合线得到房价便是“预测”，这便是“有监督学习”的完整流程。注意到这个回归问题的输出(房价)可以是任意数字，于是便有无穷种可能。 Pasted image 20260330235554 直线拟合：根据拟合直线，可以预测房屋面积 750 feet 2 对应的价格大约为 150 k 。 曲线拟合：根据拟合曲线，可以预测房屋面积 750 feet 2 对应的价格大约为 200 k 。 分类问题示例：乳腺癌检测 乳腺癌检测问题就是根据输入的一系列信息，如肿瘤块的大小、患者年龄、肿瘤块的厚度、细胞大小的均匀性、细胞形状的均匀性等，来判断是否为恶性肿瘤(0表示良性/1表示恶性)。下面给出“单输入的乳腺癌检测”、“两输入的乳腺癌检测”示意图： Pasted image 20260331203732 单输入的乳腺癌检测问题 Pasted image 20260331204146 两输入的乳腺癌检测问题 单输入的乳腺癌检测：输入是“肿瘤的大小”，输出是“良性”、“恶性 类型1”、“恶性 类型2”。 两输入的乳腺癌检测：输入是“肿瘤的大小”、“患者年龄”，输出是“良性”、“恶性”。 无监督学习 聚类(Clustering)：将相似的数据点分成一组。 异常检测(Anomaly detection)：。有非常多的应用，比如在金融系统的诈骗检测中，异常时间、异常交易可能是欺诈。 降维(Dimensionality reduction)：在尽可能丢失少的信息的前提下，将大数据集压缩成小得多的数据集。 线性回归模型 线性回归模型 常用术语 Training Set(数据集)：用于训练模型的数据集。 x：input variable(输入变量) / feature(特征) / input feature(输入特征)，也就是“特征值”。 y：output variable(输出变量) / target variable(目标变量)，也就是“目标值”。 m：表示训练样本的数量。 (x,y)：单个训练样本。 (x (i) ,y (i) ）：第 i ii 个训练样本。上标加括号是为了和求幂次区别开来。 y ^ ​ ​：表示对 y 的估计或预测。 Pasted image 20260331205038 f w , b ​ ( x ) w x + b 表示函数 f以 x 函数输入，其输出 y ^ ​ 取决于w和b的值 w 、 b ：模型的参数 f w , b ​ ( x ) 通常会简写为 f ( x ) 代价函数 代价函数用于 衡量模型的好坏 ，最简单、最常用的代价函数是“平均误差代价函数”(Squared error cost function)： J ( w , b ) ​ 2 m 1 ​ i 1 ∑ m ​ ( y ^ ​ ( i ) − y ( i ) ) 2 2 m 1 ​ i 1 ∑ m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) 2 ​ w 、 b ：模型的参数 i ：训练样本的标号 m ：训练样本的总数 y ( i ) ：第 i 个样本的真实目标值 y ^ ​ ( i ) ：对 y ( i ) 的预测目标值 除以2m：按照惯例，机器学习中的平均代价函数会除以2m而非m，这是为了使后续的计算更加简洁 w min ​ J ( w ) ​ 2 m 1 ​ i 1 ∑ m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) 2 2 m 1 ​ i 1 ∑ m ​ ( w x ( i ) − y ( i ) ) 2 ​ 简化模型，设置参数 b 0 Pasted image 20260331234730 梯度下降法 梯度下降法 梯度下降(Gradient Desent)常用于寻找某函数(比如代价函数)的最大值、最小值。梯度下降不仅用于线性拟合，也用于训练如神经网络等深度学习模型、以及一些最大型、最复杂的人工智能模型。 以前面的 min w , b ​ J ( w ) 来举例，梯度下降算法的步骤为： 1.选择初始点，一般在取值范围内选取简单的整数，如 w 1 , b 0 2.沿着 J 的“负梯度”方向，不断迭代计算 w 、 b 。之所以沿着“负梯度”方向，是因为沿该方向下降速度最快(steepest descent, 最速下降)。如下： w b ​ w − α ∂ w ∂ ​ J ( w , b ) b − α ∂ b ∂ ​ J ( w , b ) ​ α：学习率(Learning rate)，用于控制步长。通常为介于0 1之间的一个小的正数，如0.01 ∂ w ∂ ​ J ( w , b ) ：代价函数对 w 的偏导数(Partial Derivative)，其取负值表明的方向可以使 J 下降 ∂ b ∂ ​ J ( w , b ) ：代价函数对 b 的偏导数，意义同上 注意：上面是同时更新(Simultaneously update)，也就是使用旧的 ( w , b ) 直接分别计算出新的 w 、 b ；而不是先更新 w，再使用这个新的w计算新的b。 Pasted image 20260331235806 注意点1：学习率 学习率 α 的选取将会对梯度下降的效率产生巨大影响，若 α 选取的不好，甚至会导致无法实现梯度下降。 α 选取的太小，会导致下降的速度非常慢(意味着需要计算很长时间)，但最终也会收敛(converge)到最小值。 α 选取的太大，很可能会导致在极值点附近反复横跳甚至越来越远，也就是不会收敛甚至发散(diverge)。 α 选取的合适，越接近代价函数极小值，梯度越来越小，就会导致步长越来越小。 Pasted image 20260331235818 注意点2：多个极值点 在前面的讨论中，一直使用平方误差项作为代价函数。对于 平方误差项 的代价函数，都是“凸函数”或“凸面”。但若代价函数非凸时，可能就会存在不止一个极值。不同的起始点，就会导致不同的收敛速度或极值。所以 代价函数尽量要选择凸函数 。 用于线性回归的梯度下降 Linear regression model Cost function Gradient descent repeat until convergence ​ ​ : f w , b ​ ( x ) w x + b : J ( w , b ) 2 m 1 ​ i 1 ∑ m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) 2 : { w w − α ∂ w ∂ ​ J ( w , b ) w − m α ​ ∑ i 1 m ​ [( f w , b ​ ( x ( i ) ) − y ( i ) ) ⋅ x ( i ) ] b b − α ∂ b ∂ ​ J ( w , b ) b − m α ​ ∑ i 1 m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) ​ ​ Pasted image 20260402232312 梯度下降法的迭代过程可能如上图红色箭头所示，从起始点不断收敛到最小值，并且注意到这个过程也是越来越慢的 在使用梯度下降法求解问题的过程中，每次迭代都会使用到所有的训练集数据计算代价函数及其梯度，所以这个梯度下降的过程称为“ 批量梯度下降 (Batch gradient descent)” 在其他数据更为复杂的模型中，为了简化梯度下降法的计算量，每次只使用训练集的子集。 向量化和多元线性回归 多维特征 术语 m ：训练样本的总数 n ：输入特征的总数 X ^ ：全部的输入特征值，是一个 二维向量 ，每一行表示一个样本，每一列表示所有样本的单个特征 x j ​ ：表示第 j 个特征概念(一维向量)， j 的取值范围为 j 1 , . . . , n x j ( i ) ​ ：第i个训练样本的第j个特征，是单个值。如下图中， x 4 ( 3 ) ​ 30 y ( i ) ：第i个训练样本的目标值，是单个值 Y ：全部的训练样本的特征值，是一维向量 注：若无特殊说明，所有的一维向量都默认为列向量。 单变量 线性回归( univariate linear regression)：只有单个特征的线性回归模型。 多元 线性回归( multiple linear regression)：具有多维特征的线性回归模型。 Pasted image 20260402233506 如上图所示，将“房价预测 ”中的输入特征数量增加为4个：输入特征：房屋面积、卧室数量、房屋层数、房屋年龄。于是显然其线性回归模型也将从 f w , b ​ ( x ) w x + b 扩展为下面的向量形式： f w , b ​ ( x ) ​ w 1 ​ x 1 ​ + w 2 ​ x 2 ​ + ⋯ + w n ​ x n ​ + b j 1 ∑ n ​ w j ​ x j ​ + b w ⋅ x + b ​ w [ w 1 ​ , w 2 ​ , … , w n ​ ] T ：表示参数(列)向量。 w i ​ 表示当前特征对房屋价格影响。 b ：常数项参数。可以理解为房屋的基价(base price) x [ x 1 ​ , x 2 ​ , … , x n ​ ] T ：表示单个样本的特征(列)向量 w ⋅ x ：表示两个向量的点积 向量化 Pasted image 20260402234036 用于多元线性回归的梯度下降法 Model Cost function Gradient descent repeat until convergence ​ ​ : f w , b ​ ( x ) w ⋅ x + b : w , b min ​ J ( w , b ) 2 m 1 ​ i 1 ∑ m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) 2 : ⎩ ⎨ ⎧ ​ w j ​ w j ​ − α ∂ w j ​ ∂ ​ J ( w , b ) w j ​ − m α ​ ∑ i 1 m ​ [( f w , b ​ ( x ( i ) ) − y ( i ) ) x j ( i ) ​ ] b b − α ∂ b ∂ ​ J ( w , b ) b − m α ​ ∑ i 1 m ​ ( f w , b ​ ( x ( i ) ) − y ( i ) ) ​ ​ x 表示单个样本的所有特征，是一维向量 f w , b ​ ( x ( i ) ) 是一个值， y ( i ) 是一个值","summary":"有监督学习、回归、分类与梯度下降学习笔记。"},{"permalink":"https://zhr0529.cn/posts/载波相位引入在-pyrtklib-中的实现研究/","title":"载波相位引入在 pyRTKLIB 中的实现研究","content":"预备知识 GNSS 全球导航卫星系统（Global Navigation Satellite System,GNSS）是一系列利用一组轨道卫星提供的信号进行位置确定和时间同步的技术的总称。这些系统允许地球上或近地空间内的任何位置的用户通过接收来自至少四颗卫星的信号来计算自己的精确位置、速度以及精确的时间信息。GNSS 包括了多个独立运行的卫星导航系统，美国的 GPS、俄罗斯的 GLONASS、欧盟的 Galileo，以及中国的 BeiDou等。GNSS 技术广泛应用于多个领域，包括但不限于交通导航、地理测量、精准农业、环境监测、科学研究、公共安全和军事应用等。它不仅极大地提高了定位精度和可靠性，还促进了各种基于位置服务的发展，如地图服务、紧急响应和物流跟踪等。总之，GNSS 已经成为现代社会中不可或缺的一部分，支持着从个人日常活动到复杂商业运作和科学研究在内的众多应用场景。 了解IPNL POLYU/pyrtklib pyrtklib ：一个用于城市峡谷定位的紧密耦合深度学习和GNSS集成的开源软件包 在城市中，高楼会把卫星信号像乒乓球一样弹来弹去，多路径误差，导致手机收到混乱的信息，算不出真实的位置。 pyrtklib项目旨在利用 深度学习技术 辅助传统的卫星定位方法，通过算法智能地识别并削弱这些干扰信号的影响，从而在城市峡谷等复杂环境中获得更精准的定位结果。 1. RTKLIB [https://www.rtklib.com/]：一个用C语言编写的开源卫星定位数据处理程序库[](https://baike.baidu.com/minilemma/RTK/397408/231277872?category 1)。功能强大，但使用起来对初学者不友好。 2. PyRTKLIB ：可以看作是 RTKLIB 的 Python 版本 或者“翻译器”[](https://blog.csdn.net/gitblog 00327/article/details/142241233)。利用 pybind11 等技术，将RTKLIB的C语言核心功能包装成易于在Python中调用的函数。这样做的好处是，可以在Python环境中，利用RTKLIB的强大引擎来处理定位数据。 方面 说明 项目 定位 一个为RTKLIB提供Python接口的绑定库，将RTKLIB的C语言功能引入Python环境， 方便进行GNSS数据处理。 核心 功能 单点定位（SPP）、实时动态定位（RTK）、 精密单点定位（PPP）、GNSS数据后处理 技术 基础 基于著名的 RTKLIB 和 pybind11 用于将C++代码暴露给Python 目录 结构 主要包含Python模块代码、RTKLIB的C语言源码、示例脚本、测试用例和文档。 IPNL POLYU/pyrtklib 是一个 桥梁 ，能在Python环境中，调用用C语言编写的RTKLIB库的功能。 在项目中，将传统上基于 C 或 Fortran 开发的 GNSS 算法与现代基于 Python 的深度学习框架无缝集成，从而增强定位精度，并简化开发流程。pyrtklib 包含了一个新颖的深度学习框架，该框架利用 GNSS 数据来预测每个卫星测量的权重和偏差。这种机制能够有效地区分视距和非视距卫星信号，并据此调整权重分配以提高定位精度。 论文(理论部分) 摘要 《载波相位引入在pyRTKLIB中的实现研究》 提高pyrtklib工具包的定位精度，以前主要用“伪距”来定位，精度不够。现在要把载波相位加到这个工具包里。 在 pyRTKLIB 工具包里，实现 载波相位 技术，让定位更准。 引言 GNSS：GPS、北斗都是GNSS的一种，需要至少4颗卫星才能算出位置（x, y, z坐标和时间） pyrtklib：一个开源的python工具包，用来处理卫星定位数据。用python写可以和深度学习结合。ai帮忙区分好信号和坏信号，给好信号更高的信任度，算出来的定位更准 项目背景 目标：用数学模型解出位置 伪距：测算卫星距离通过人与卫星的时间差 x 光速 伪距的来源：表与卫星表不可能完全同步（钟差 Δ t ） 信号穿过 电离层 I 和 对流层 T 也会变慢 所以这个距离精度不高， 米级 。 p r + c Δ t + I + T + ϵ ：测量的伪距 p 真实距离 r + 误差（钟差、大气层...） 难点：非线性问题 同时看到了 n 颗卫星，得到 n 个伪距测量值 Z [ p 1 ​ , p 2 ​ , ... , p n ​ ] T 每一个 p 都和位置 y 有关。有 n 个方程，解 4 个未知数。 由于公式(2) 开根号 ... ​ 的存在，导致这个方程组是非线性的 解决：“泰勒近似” + “迭代” 利用泰勒展开 h ( y + Δy ) ≈ h ( y ) + HΔy H (雅可比矩阵) ：直线的斜率，通过求偏导得到 加权最小二乘法 (WLS）： Δy ( H T WH ) − 1 H T W ( Z − h ( y i ​ )) y i + 1 ​ y i ​ + Δy 1. 先猜一个初始位置 y i ​ ，比如地球中心。 2. 用 y i ​ 算出理论上应该测到多远的距离 h ( y i ​ ) 。 3. 用实际测到的伪距 Z 减去理论距离 h ( y i ​ ) ，得到残差 4. 把 残差 和 斜率 H 代入公式(6)，解出修正量 Δ y 。 5. 用公式(7)更新位置： 新位置 老位置 + 修正量 。 6. 重复 1 5 步，直到修正量 Δ y 变得非常非常小 ，就认为 y i + 1 ​ 是精确答案。 关键： W (权重矩阵) 和 b (偏差) W (权重矩阵) 在计算时，给好信号 更高的权重（信任度） ，给坏信号更低的权重 一个好的 W 对定位精度至关重要！ b (未建模的偏差) 公式 (9) p r + cΔt + I + T + b + ϵ b主要是 NLOS（非视距） 和 多路径效应 NLOS (Non Line of Sight)： 信号被大楼挡住了，收到的是绕过来的信号 多路径 (Multipath)： 同时收到了直射信号和被大楼反射的信号，两个混在一起 这些 b 偏差会让定位结果歪得很离谱 不知道怎么设置最好的 W 和 b ， 让 AI预测 出一个最完美的 W 和最干净的 Z 用深度学习来预测 W （权重）和 b （偏差），然后喂给公式(6)经典的WLS算法，来获得更准的定位。 试验方法 载波相位 (Carrier Phase) 伪距通过听卫星信号的时间码，精度为米 载波相位通过数信号的波形，GPS L1载波的波长只有19厘米，精度可达毫米级 整周模糊度 测到不足一个波的部分时，之前还有多少个完整的波未知 解出整周模糊度N是高精度定位的关键 载波相位的观测方程： λ φ i j ​ ρ i j ​ + λ N i j ​ 测量的总相位 φ × 波长 λ 真实距离 ρ + 整周模糊度 N × 波长 λ 完整版： λ φ i j ​ ρ i j ​ + c V i ​ − c V j + λ N i j ​ + T i j ​ + ( I i j ​ ) φ ​ + λ ϵ i j ​ 引入了各种误差：包括：接收机钟差 V i ​ 、卫星钟差 V j 、对流层延迟 T 、电离层延迟 I 单差： 伪距观测及载波相位观测在流动站与基准站之间求一次差，可以消除卫星钟差，在短基线情况下（基站流动站之间的距离不超过 10km），可以消去电离层和对流层延迟误差 双差： 单差观测在同系统卫星 p 与卫星 q 之间作二次差，可消去接收机钟差，得到双差观测方程 结果：伪距双差： p ij pq ​ r ij pq ​ ，只剩距离了 载波双差： φ ij pq ​ λ − 1 r ij pq ​ − N ij pq ​ ，只剩距离和双差模糊度N 三差： 把这一秒的双差数据 和上一秒的双差数据再减一次，消去N 虽然不用解N了，但是噪声被放大了，精度不如双差 观测模型 伪距单点定位 (SPP)最小二乘观测模型，老办法 只用伪距与WLS的思想，通过泰勒展开把复杂的开根号公式变成简单的线性方程 G Δ X b 要解的未知数 Δ X 有 4个 ，位置修正量 Δ x , Δ y , Δ z 和 钟差 cδ t u ​ 最后用 最小二乘法 迭代求解，得到 米级 精度 载波相位相对定位 (RTK) 使用 双差观测值 ，包括伪距和载波 用和SPP一样的 泰勒展开 技巧来处理距离 r 未知数 Δ X 不一样了，包括 位置修正量 Δ r 3个 + 所有的双差模糊度 δ N 看了N颗卫星，就有N 1个双差模糊度 把伪距双差、载波双差等所有方程都写成 Z ( t ) H ( t ) Δ X ( t ) + V ( t ) 的标准矩阵形式 Z测量值，H斜率矩阵，要求求解的未知数X 然后用卡尔曼滤波器，随着时间不断更新，得到X的最优估计值,解出位置 Δ r 和模糊度 N 可补充：RTK的最后一步是固定解，可用算法将这些浮点数猜回最接近的整数。一旦固定成功，就能实现厘米级定位。 结果 偏差模型训练结果 Pasted image 20251102232613 脚本成功启动，并使用了模拟数据来训练 Train Loss 训练误差和 Val Loss 验证误差都在迅速下降，说明模型在学习 reducing learning rate... :在第19、34、40等轮次，学习率在自动降低。脚本中的 学习率调度器 在正常工作，模型学习变慢时自动调整参数 Early stopping triggered. :早停机制，防止模型过拟合并节省时间。 Pasted image 20251102233238 仅仅几轮之后 ，两条线的误差都 迅速下降 到了一个非常低的水平 模型训练的非常快，效果很好 偏差模型预测结果 Pasted image 20251102232131 脚本成功处理了数据集中的 405 个数据点，只花了 18 秒，没有中途崩溃。 errors shape: (403, 3) :计算了 403 个点的定位误差，每个点都有 3 个坐标（X, Y, Z） errors example: [[ ... ]] :显示了前 5 个点的具体 (X, Y, Z) 误差值 2D mean: 17.71 :模型预测位置的 平面平均误差 为 17.71 米 3D mean: 48.76 :模型预测位置的 三维空间平均误差 为 48.76 米","summary":"GNSS 载波相位观测在 pyRTKLIB 中的引入与实现记录。"},{"permalink":"https://zhr0529.cn/posts/电机控制/","title":"电机控制","content":"一、直流有刷电机基础原理 直流有刷电机 是做各类小车竞赛最常用的电机，主要原因是控制非常的简单：给电就转 Pasted image 20250801114029 Pasted image 20250801114042 直流有刷电机（brushed DC motor） 物理模型 任何电机都有定子和转子 定子 ： 红色绿色的两块大磁铁（永磁体）是直流有刷电机的定子，定子是静止不动的，定子的作用就是提供磁场。 转子 ： 转子就是在不停旋转的。 红色的commutator（换向器）和连接换向器的方形蓝色线圈就是转子的模型。换向器和方形线圈是刚性连接的。整个蓝色方形线圈都在由定子磁铁形成的磁场中，这里的理想物理模型是水平的、由N指向S的匀强磁场。 有刷电机工作时，转子需要通电。当转子的蓝色方形线圈中通有电流时，通过电磁感应原理，在磁场中的通电导线会产生安培力。安培力让电机转起来，使用左手定则可判断安培力的方向： F B I L Pasted image 20250801114734 电刷 石墨是导电的。石墨电刷是个软的、导电的刷子，电刷配合换向器，解决了两大难题： 1. 电机电源线不会拧成麻花了。 2. 电机在上述线圈竖直的情况下，正好是换向器的换向点，让方形线圈的电流反向，把两个安培力的方向反向，这样电机只要通一个恒定的直流电就可以朝着一个方向一直转下去。 减速比 简单的原理： 减速增矩 ：意思就是减小速度就可以增大扭矩。 初中物理学过： P F v 转换一下： P T ω 而功率： P U I 功率是由供电决定的。在功率基本不变的情况下： 电机的转速越快，力矩就越小； 电机的转速越慢，力矩就越大。 减速比就是使用机械的方法（齿轮组）来降低电机的转速 Pasted image 20250801163126 小齿轮要转好多圈大齿轮才能转一圈，这就实现了减速，其扭矩也会以相应的传动比增大。 什么情况下要用到减速增矩？ 例子：目前材料有6V的电池和一个额定6V的小马达，要求用这个电机提起一斤的猪肉。 如果没有减速器，这对于小马达来说实在是太难了，这时安装机械减速器，把小马达包装成减速比为1:256的小黄电机，这样就能提起来一斤猪肉了。 另一个例子解释减速增矩：汽车起步，汽车起步要用1挡，也就是减速比最大的挡位，这样可以得到最大的驱动力。 大扭力电机： Pasted image 20250801164329 795电机的扭力可以达到0.5N M，直流有刷电机能达到0.5N M，也就是5kg cm 额定电压 ：规定额定电压是多少就要用多少V的电池供电 空载电流和堵转电流 ： F BIL，其中B是永磁体产生的磁感应强度，是不变的；L是线圈导线长度也不变；那么可以知道， 电机的转矩是直接由电机的电流控制的 。电流越大转矩越大。 空载电流应该是非常小的，电机无负载状态，即电机匀速转动。 在无摩擦的理想状态下，匀速旋转不需要能量，电流为0即可保持匀速旋转。 但是空载还是需要一定的电流，来克服摩擦力和空气阻力，使电机的电磁转矩与这两个阻力矩平衡，但是这两个阻力相对于负载力来说还是太小了。所以空载电流就很小，所以此时电机两端的电压会很大。 堵转电流：电机用上了最大的力气（电磁转矩），也就是用上了最大的电流。流过电机的电流会很大，而这时的电机两端的电压会很小。 有刷电机相关的控制算法：PID速度闭环，PID位置闭环，PID电流闭环 二、步进电机原理及细分驱动控制 Pasted image 20250802105533 1. 可以使用定时器来精确控制步进电机的转速； 2. 由于步进电机的步距角一定，可以精确控制电机的位置 步进电机介绍： 步进电机的特点： 1. 扭矩大 。工作中的步进电机的轴的力气非常大，足以应对一些需要很大扭矩的工况，解决了一般的直流有刷电机“软弱无力”的问题。 2. 精度高 。步进电机最大的特点就是步进，不同于直流有刷电机给电就转，步进电机每次移动的角度是定死的，比如一步就转1.8°，非常精确。 3. 外观好看 。看起来高端大气。 步进电机的缺点： 转速低 。步进电机主要应用于低速大扭矩。 步进电机原理： Pasted image 20250802105918 Pasted image 20250802110110 步进电机由定子和转子组成，上图是电机的定子结构，具体介绍上图： 与有刷电机的结构相反，步进电机的定子是线圈绕组，而转子是永磁体。 有刷和步进电机的原理虽然都是基于电磁感应，但是结构和运行原理则是完全不同的。 常使用的步进电机是 两相四线步进电机，步距角1.8° 。 两相：电机只有A相线圈和B相线圈 四线：伸出了四个线头（A+,B+A ,B ），外部接线就只需要接这四条线。 定子中间的转子可以简化为一块永磁体 ：如下图， Pasted image 20250802111348 给A相通入A端流入，A1端流出的电流，发现A和A1两个线圈就变成了两块电磁铁，吸住转子，让转子保持在这个位置。 ...待完成 三、直流无刷电机基础原理 【自制FOC驱动器】深入浅出讲解FOC算法与SVPWM技术 知乎 直流无刷电机有三条线： Pasted image 20250822195705 直流无刷电机相比直流有刷电机，直流有刷电机有两条引线 ，稚晖君原话：“不要小看多出来的这一条引线，直接让电机控制从一维的标量变成了二位的矢量” 直流无刷电机的内部结构与步进电机非常非常相似： Pasted image 20250822195810 与步进电机相同的是：无刷电机的结构与步进电机是相同的，也是 定子线圈绕组和永磁体转子 。 不同：步进电机是两相四线，无刷电机一般都是 三相三线 ，大部分使用的无刷电机内部接线的三相中间是星形连接 星形连接在中小功率电机控制中有很多优势 无刷电机定子绕组手绘图：三相三线，星形连接 Pasted image 20250822200112 以六个绕组线圈为例： Pasted image 20250822200155 三个线圈交替得电就能让永磁体转子转起来： Pasted image 20250822200256 让bc两相接地，a相接电源，假设此时流入1A的电流，b和c相就各流过0.5A的电流。 根据 电生磁的右手定则 ，三相线圈都产生磁场，并对中间的磁铁产生磁力。 根据同性相斥，异性相吸，看出三个磁力合力方向是水平向右的，所以转子磁铁方向就是图中的方向，N极向右 同样： 如果是bc两相接电源，a相接地，那就是bc都流入0.5A的电流，a流出1A的电流，磁力方向相反。 另一种情况： Pasted image 20250822201727 B端悬空，a接电源，c端接地，这样只有两个绕组可以对永磁体有磁力，计算得到合力的方向和大小如上图。 四、快速上手直流无刷电机速度PID控制 市面上常用的直流无刷电机： 275 上图是转子在内，定子在外的直流无刷电机。 Pasted image 20250822202150 上图是转子在外，定子在内的无刷电机。 无刷电机的参数： Pasted image 20250822202605 电机KV值： 电机的转速（空载） KV值 X 电压； 例如KV630的电机在10V电压下它的转速（空载）就是6300转/分钟。 出轴直径： 5mm螺纹，就找对应的5mm螺母，螺柱等连接需要的结构。 极对数： 7 直流无刷电机的永磁体是N极、S极成对交替排列的。N极加S极的数量就是电机的级数，其成对的数量就是电机的极对数。电机极对数 电机极数/2 。 Pasted image 20250822203004 极对数越大，电机转矩越大。极对数越小，电机转速越高。 工作电压： 推荐12V，又标注3s 4s，那理论上可以到16V。 工作电流： 10A，一般来说工作电流在10A及以上的电机就可以拿来做机械臂力矩电机提重物用了。 有刷电机控制仅用一个H桥通过PWM就可以控制电机转速，但无刷电机的控制电路比H桥要复杂 要 快速上手 无刷电机速度控制，且控制目标是以下内容： 使用无刷电机驱动无人机； 使用无刷电机应用在一些 高速旋转 的情况下，比如水下推进器、电动艇； 也就是完全把无刷电机当做一个 强度可调的动力源 。 可以选择一款电机控制产品来快速实现控制要求： 直流无刷电机电子调速器 简称无刷电调，又直接简称“电调” Pasted image 20250822210958 接线： Pasted image 20250822211314 T插，XT60和很粗的两条线：一条红一条黑 三头杜邦线，三条蓝色的粗线，各接了一个3.5mm的香蕉头 红色黑色的粗线用来接电源；三根蓝线接无刷电机的三条线，三条线顺序随意， 任意对换两条线的顺序则电机转向反向。 三头杜邦线：白色信号线接PWM，红色是5v可不接，黑色接GND 所以无刷电机的控制方式也明确了，通过一路pwm输入到电调里就可以控制无刷电机转速了，非常简单，变得跟有刷电机一样简单了。 电调参数： Pasted image 20250822212644 输出能力： 持续电流12A：同一个电机，电流越大电机的扭矩越大，等负载下转速也越快 电源输入： 接多大的电池。两节电池组， 一节锂离子电池是3.7V，充满电是4.2V，两节就是8V，三节就是12V。电源输入参数还可以看电调上标的另一个参数： 2 3S，代表可以接2 3个锂电池组，也就是8V 12V电源都可以。 BEC： BEC 全称是battery elimination circuit，免电池电路，也就是电调可以外部给别的设备供电，输出5V，最大电流2A，通过三条“舵机线”中间红色的那一条 但大多数情况不使用它供电，因为是线性稳压器，效率太低，更容易发热。 油门信号频率范围： 控制电调的PWM频率，50HZ，跟舵机又是一模一样 最后是支持的电机最高转速，基本都是万转以上，非常快了。 使用电调时要注意两点： 1. 并不是电调通上电就马上能控制电机的，你要先向电调输入一串“开机密码”，这串密码就是几段不同占空比的PWM。 2. 电调只能作为一直旋转的精度一般的动力源，电调控制无刷电机低速旋转的效果非常差，几乎不可用，只能高速旋转作为动力源。 五、直流无刷电机矢量控制（FOC）引子 一般的无刷电调只能大致控制电机转速，并不能精确控制速度，而且更不能让电机低速精确运动 要实现的核心控制目标：无刷电机的力矩控制 换相的频率和电流的大小共同决定了电机的转速 需要想一个办法： 让电机在变负载的情况下还能稳定的低速匀速运动 Field oriented control（FOC） 磁场 定向的 控制 两个关键点： 1. 怎么保证电机效率最大，不发热？ 2. 怎么检测并控制当前的扭矩大小？ 六、FOC算法核心 怎么让电机的效率最大，不发热 将所有的电流路径排列组合列出来： Pasted image 20250825094829 可以得到六个方向，大小相等的磁力： Pasted image 20250825094849 Pasted image 20250825094902 如果想生成一个30°的合磁力： 按上图的0°状态通一段时间的电，再按60°状态通一会电，当这个“ 一段时间 ”足够小的时候，两个力平均下来就是30°了。 按这个道理，0° 360°任意一个方向的力都可以生成 这就是 SVPWM ——矢量控制 合磁力施加于转子永磁体上，N极的方向会转到与F 合 相同的方向。 Pasted image 20250825100241 N极方向与F 合 方向一致时，转子停止 Pasted image 20250825100319 在F C 与转子N极方向差90度的时候转子的角加速度最大； 在F C 与转子N极方向差0度的时候角加速度最小，等于0。 Pasted image 20250825100502 根据能量守恒，通入定子线圈的电能 一部分转化为了磁势能，另一部分产生热量 磁势能是电机旋转需要的能量，产热没什么用，所以为了使电机运转效率最大， 需要控制“磁势能”始终处于最大状态 ，即： 希望合力的方向永远与转子N极所指方向差90° 实现：已经可以在理论上做到生成任意一个方向的磁力，只需要通过编码器实时监测转子的位置，就可以闭环控制了 怎么检测并控制扭矩大小 电流大小与转矩大小正相关： 通过检测实时电流，计算出实时转矩，从而实现转矩的闭环控制 用手转动无刷电机，电流表测量三根线上发现电流是 三相正弦交流电 ，这三相正弦交流电就是电机的 反电动势产生的感应电流 发电机反过来就是电动机，所以也要通入三相正弦交流电才能让无刷电机平稳最大效率旋转 Pasted image 20250825101243 正弦波控制是最平顺最精确的控制，而方波只适用于高速旋转，粗略控制 Pasted image 20250825101339 三相电流的相位互差120°; 电角度 ： 机械角度θ m 就是电机绕轴转一圈，对应0° 360°，那电角度θ e 就是： θ e ​ mod ( θ m ​ ∗ p n ​ ， 2 π ) mod(a,b)函数是用来求a/b的余数，p n 是转子极对数，假设极对数pn 4，那么机械角度与电角度的关系如下图：Pasted image 20250825101657 简化三相正弦电流控制 在转矩恒定（ 与转子N极方向垂直的磁力大小始终不变 ）的情况下，点击可以平稳运行 磁力是电流产生的，即三相电流的在磁力方向上的投影也是个定值 难点：转子是在不断旋转的，所以与转子N极垂直的力也是不断旋转的，所以目标坐标系是个 旋转坐标系 。 设三相电流分别为i a ，i b ，i c ，建立abc三相静止自然坐标系： Pasted image 20250825102322 然后设转子当前电角度为θ（因为模型假设是a，b，c三个绕组，极对数 1），建立 转子同步旋转坐标系dq系 。 Pasted image 20250825102410 其中，d轴与转子N极位置相同，称为“直轴”； q轴与转子N极位置超前90°，称为“交轴”。 把abc三轴的初始电流投影到dq轴即可。 第一步：Clark变换。 建立一个中间坐标系 αβ系： Pasted image 20250825102812 先把abc三相自然坐标系的三个量投影到 αβ系——两相正交静止坐标系。 这样三个量就被简化成两个正交量，方便下一步投影。 根据KCL，电流 流入 流出，所以： i a ​ + i b ​ + i c ​ 0 投影运算： Pasted image 20250825103037 Pasted image 20250825103049 i β 的计算与上面同理，不再赘述。 为什么要有个 等幅值变换 ？ 因为正常的投影运算之后，iα与iβ的值是ia、ib、ic幅值的3/2倍，为了简化之后的运算，所以将投影大小缩放到与ia、ib、ic幅值相同。 第二步：Park变换 Pasted image 20250825103727 将αβ系的两相再投影到dq系。 Pasted image 20250825103755 这个θ在电机运动的时候是会变的，dq坐标系是在旋转的。 搞完这两步之后，会惊奇地发现： Pasted image 20250825104014 变成恒定值了 三相初始电流原本是三相互差120°的正弦波： Pasted image 20250825104042 经过Clark变换后： Pasted image 20250825104052 变成两相互差90°的正弦波了，再经过Park变换： Pasted image 20250825104103 变成两条直线 其中id是直轴电流，即投影到N极方向的电流 iq是交轴电流，即投影到N极超前90°方向的电流 其中发现id的大小是跟ia，ib，ic的幅值大小相等的，iq是0。 电流的方向也是磁力的方向 ，投影一直都在id方向，iq没有， 说明磁力一直与转子N极方向重合 Pasted image 20250825104415 t 0.04时，此时电角度是0， 说明转子位置与abc坐标系中的a轴重合 此时看图得ia 30，ib ic 15，所以此时的合磁力应该也是与A轴重合的，所以转子方向与电流方向始终是重合的，导致id一直是最大，iq一直是0。 假如把这张图的电角度向右平移1/4个周期，让0.04s时合磁力为0°时转子N极滞后90°，也就是270°位置，那最后这张图就变成了： Pasted image 20250825104618 id 0，iq 30。 这是需要的效果： id是重合于转子方向的，不需要； iq是与转子方向超前90°的，需要。 所以电流闭环需要两个PID，一个控制id，一个控制iq。 id的目标值就始终是0； iq的目标值于转矩成正比。 七、SVPWM技术原理及代码分析 1. 怎么保证电机效率最大，不发热？ 时刻保证希望F 合 的方向永远与转子N极所指方向差90° 2.怎么检测并控制当前的扭矩大小？ Clark、Park坐标系变换，让电流与扭矩的关系变为线性。 开始实际的FOC驱动器设计： 直流无刷电机的电机控制电路 Pasted image 20250825232107 a，b，c这三相线圈， 每一相都要既能 流入 电流又能 流出 电流 也就是每一相的端子 既可以接VCC又可以接GND 也就是每一相都要接一个外部的特殊开关，这个开关可以控制这一相接到VCC或GND，三相就需要三个这种特殊开关 Pasted image 20250825232618 Pasted image 20250825232354 三相全桥逆变器 右边，下图这部分电路是直流无刷电机的电路模型：有一定内阻的电感线圈： Pasted image 20250825232437 完美解决了每一相都可以接到VCC或GND的要求 也就是： Pasted image 20250825232925 通过这个电路，通过6个MOS管的开关组合，就可以生成上图这六个方向的磁力。 由于每个桥臂上的两个场效应管的导通状态必须相反 （不然VCC和GND之间就短路了） ，因此三相全桥逆变器共有8种状态，可以用矢量表示，其中有六个非零矢量U1（001）、U2（010）、U3（011）、U4（100）、U5（101）、U6（110）；以及两个零矢量U0（000）和U7（111），也就是全关或者全开。 进而扩展到生成360°任意方向的电压矢量，用类似PWM的技术去实现，详细分析一下 Pasted image 20250825233415 电压空间：0 360°电角度的这个二维空间 因为三相全桥逆变器可以直接控制的物理量是相电压，也就是电压。 如何控制电压： Pasted image 20250826103029 每一个半桥电路都只需要一路PWM控制 ，因为虽然一个半桥电路有两个MOS管，但是这两个MOS管的状态必须相反，一个打开另一个必须关断 所以一个半桥电路只需要一路PWM控制，上管给PWM，下管给 这个PWM的互补波形 U4（100）的时候，只有第一个半桥上管打开，连接上端VCC。 假设这路PWM的占空比是100%，那上管就一直打开，电机这一相的相电压就最大，设为是1V；占空比是70%，那上管一个周期内只有0.7个周期打开，电机这一相的相电压就是0.7V。 目标是控制定子产生的磁力大小和方向；而磁力跟线圈电流是正比的，是线性关系，所以控制电流就控制了磁力 没办法直接操控电流大小。只能直接控制电压，相电压与线电流正相关，但不是线性关系 所以通过控制三相全桥逆变器从而控制相电压，从而间接影响每一向的线电流，电流再决定磁力的大小 Pasted image 20250825233415 其次，上图（a）图的电压空间被分为了6个“扇区”，每个扇区60°。 看上图（b），假设要生成第一扇区内任意一个方向的电压空间矢量U ref ，U ref 和U4之间夹角为θ，要合成U ref 只需选择U4和U6作为非零矢量，U0和U7作为零矢量，根据 平均值等效原理 (即在一个开关周期内通过对基本电压矢量加以组合，使其平均值与给定电压矢量相等， 这里可以理解成PWM原理+矢量的平行四边形定则，两种思想 )，可得： ∫ 0 T re f ​ ​ U re f ​ d t ∫ 0 T 4 ​ ​ U 4 ​ d t + ∫ T 4 ​ T 4 ​ + T 6 ​ ​ U 6 ​ d t + ∫ T 4 ​ + T 6 ​ T re f ​ ​ U 0 ∗ ​ d t 上式离散化后等效为以下公式： U re f ​ T re f ​ U 4 ​ ⋅ T 4 ​ ​ + T re f ​ U 6 ​ ⋅ T 6 ​ ​ + U 0 ∗ ​ ⋅ T re f ​ ( T re f ​ − T 4 ​ − T 6 ​ ) ​ 上式再移个项： U re f ​ ⋅ T re f ​ U 4 ​ ⋅ T 4 ​ + U 6 ​ ⋅ T 6 ​ + U 0 ∗ ​ ⋅ ( T re f ​ − T 4 ​ − T 6 ​ ) ，原理就是PWM的原理+平行四边形定则 注意这两个公式里U ref 、U4、U6都是矢量！矢量相加要用平行四边形定则！ 其中Tref、T4、T6分别为总作用时间（一个开关周期）、U4的作用时间、U6的作用时间，U0 就是U0或U7，零矢量就是调节Uref幅值使用的，对Uref的方向无影响。 Tref很短，跟PWM一样，周期需要毫秒级、微妙级，才能有好的矢量合成效果。 这种合成方法与PWM的原理非常像，其实就是PWM的原理，所以这个利用平均值等效原理的类似PWM的矢量合成方法叫做： SVPWM（space vector PWM）空间矢量的PWM。 如果想生成Uref，首先需要固定Tref，也就是SVPWM频率，设频率为10KHz，也就是Tref 1/10K秒，固定不变。 然后需要确定T4、T6，也就是U4的作用时间和U6的作用时间 Pasted image 20250826110402 以Uref为斜边的三角形，通过 正弦公式 得到： sin 2 π /3 ∣ U re f ​ ∣ ​ sin ( π /3 − θ ) ∣ U 4 ​ T 4 ​ / T re f ​ ∣ ​ sin θ ∣ U 6 ​ T 6 ​ / T re f ​ ∣ ​ 其中规定 U4 U6 2/3 Udc，Udc是电源电压，又叫母线电压 因为： Pasted image 20250826110712 每一相的相电压最大就是 2/3 Udc，所以这里要限幅。 所以得到非零矢量的作用时间： { T 4 ​ m T re f ​ sin ( π /3 − θ ) T 6 ​ m T re f ​ sin θ ​ 其中m为SVPWM的调制系数（即调制比）， m 3 ​ U d c ​ ∣ U re f ​ ∣ ​ 而零矢量作用时间为 T 0 ​ T 7 ​ ( T re f ​ − T 4 ​ − T 6 ​ ) /2 这样就确定了T4、T6的值 T0为什么等于T7？ Pasted image 20250826111356 上图就是以第一扇区为例，实际的SVPWM应用中，3路PWM的样子。 在实际电路控制中， 为了尽量减小MOS管开关损耗 ，需要考虑如何生成合适的PWM波形，降低MOS管开关次数。所以，在SVPWM中将基本矢量作用顺序的分配规则定为：每次开关状态转换时， 只更改三相中的一相 。同时为了使得所产生的波形对称并且降低噪声， 并对零矢量的作用时间进行均分处理 。 第一个扇区是这样找，其他五个扇区也是完全一样的方式： SVPWM开关切换顺序表 Pasted image 20250826111534 为了实现对称的PWM波形，需要确定各个扇区内的开关切换顺序。在第一扇区中，当从U4切换到U0时，只需对A相控制上下桥臂的两个场效应管进行开关切换动作，如果由U4切换到U7，则需同时对B、C相桥式电路中的场效应管进行开关切换动作，此时由于场效应管内电路通断引起的开关损耗将增大一倍。因此： 在切换U4、U2、U1时，要配合零电压矢量U0； 在切换U6、U3、U5时，要配合零电压矢量U7； C语言写的SVPWM算法， 这是FOC控制的核心算法 ： 电机完全靠这个函数转起来 ，这个函数的输入参数有三个： Uq和Ud 是目标交轴电压和直轴电压。对于控制中小型电机时，Ud一直设为0即可，只需要控制Uq的大小来间接控制电流大小，也就是间接控制扭矩大小。 第三个参数 angle 就是转子当前电角度 C void FOC SVPWM(float Uq, float Ud, float angle) { int sector; //sector即扇区 float Uout sqrt(Ud Ud + Uq Uq) / VOLTAGE LIMIT; // VOLTAGE LIMIT是宏定义，就是电源电压，12V电源的话它就 12。 //希望Ud一直 0，所以这里的Ud一般都 0，所以这句代码可以简化成： // Uout Uq / VOLTAGE LIMIT; angle normalizeAngle(angle + atan2(Uq, Ud)); //这是最核心的一句。 normalizeAngle函数附在后面，先去看看这个函数。 //angle就是编码器实时读取的当前电角度值，范围是0 360度，atan2函数是math.h中的函 //数，用于求角度的反正切值，需要你自己去百度搜下这个函数的用法。 //这里我们发现，由于Ud是一直 0的，所以当Uq 0时，atan2(Uq,0) π/2； //当Uq 找一款MOS管集成在芯片内的芯片 ，如下 DRV8313 Pasted image 20250826170433 三个—— 半H桥 ——驱动器IC。 半H桥 就是半桥。 Pasted image 20250826171823 芯片内部以及外部接线示意图 Pasted image 20250826173421 CP1和CP2外接一个103、耐压要大于两倍电源电压的飞跨电容 ，飞跨电容是电荷泵的内容，这里的电荷泵可以先理解为一个降压的DCDC开关电源，把电源电压降压给MOS管的高端供电，跟EG3014的自举电容的作用是一样的。 Pasted image 20250826180346 IN1，IN2，IN3 分别是三路PWM输入，分别单独控制三个半桥电路，并且芯片内部会给半桥电路自动生成互补波形，很省事。 EN1，EN2，EN3 直接3.3V就行了，使能引脚。 Pasted image 20250826180440 VM 是电源电压，8.2V 60V，给电机提供电能，所以需要接储能电容和旁路电容储能和滤波 Pasted image 20250826180555 VCP 引脚是引出的高端栅极驱动的电压，官方要求这个引脚要连接一个104电容到VM Pasted image 20250826180700 低电平分别使能复位、睡眠模式、错位模式。都用不到，全部上拉 别的引脚不需要管了 Pasted image 20250826180750 注意芯片底部也有一个焊盘：thermal pad 一定要焊上 ，连接到PCB方便散热的功能 Pasted image 20250826180915 IN1、IN2、IN3直接接STM32的PWM OUT1、OUT2、OUT3直接接电机三根线 完整的驱动器电路： Pasted image 20250827105522 左边都是老朋友了 右边有个AMS1117 5.0，用于将12V电池降为5V的LDO SPX3819 3.3是一个降压到3.3V的LDO ，选它而不是AMS1117 3.3是因为它 体积小 ，想把驱动器体积做的尽量小 TJA1050是CAN通信芯片 AS5600 Pasted image 20250827110541 第一次见，有上拉电阻的SCL、SDA，这是一个IIC协议的传感器 这个芯片是个磁编码器 Pasted image 20250827110724 上图这个有A相B相的编码器，且两个方波相差90°，叫 增量式编码器 而AS5600编码器完全不是上面这种两个方波的，AS5600是一个iic传感器， 用法更像是MPU6050 AS5600属于 绝对式编码器 ，返回的内容就是iic协议的角度值 Pasted image 20250827111144 AS5600 如何检测电机的绝对角度 Pasted image 20250827111606 将磁铁用胶水黏在电机后转轴上 Pasted image 20250827112035 有了这块磁铁，磁编码器就可以通过感受磁铁旋转时的磁场变化去感知角度。 其中AS5600芯片距离圆形磁铁的距离应该保持在0.5mm 2mm之间。这就涉及机械安装问题了，买铜柱的时候要选好尺寸，控制好距离","summary":"直流有刷电机、电机驱动与控制学习笔记。"},{"permalink":"https://zhr0529.cn/posts/机器视觉/","title":"机器视觉","content":"一、机器视觉硬件支持 语言：micropython MicroPython是 Python 3 语言的精简实现 ，包括Python标准库的一部分，经过优化可在微控制器和受限环境中运行。 Pasted image 20250731170931 首先是导库： Pasted image 20250731171104 sensor库：sensor是摄像头，sensor库负责摄像头的底层驱动。 image库：image库里包含了一些处理图像的函数 time库：时间库，用于获取时间 lcd库：lcd屏幕底层驱动的相关库 Pasted image 20250731171321 初始化LCD 传了一个参数freq（频率），指定驱动LDC的时钟频率，这里是15MHz。 invert参数：控制LCD屏是否反色 反色如下图所示，左边是invert True，右边是False。 Pasted image 20250731171542 sensor.reset() ：初始化摄像头，同时摄像头开始运行 sensor.set pixformat(sensor.RGB565) ：用于设置像素格式，设置摄像头为RGB565格式，除了RGB565还有GRAYSCALE（灰度模式） sensor.GRAYSCALE: 灰度，每个像素8bit。 sensor.RGB565: 彩色，每个像素16bit。 GRAYSCALE灰度模式图像是黑白的，每个像素8bit，色差从最黑：0到最白2 8 ，也就是255。 RGB565的16bit的存储方式： Pasted image 20250731172227 一个彩色图像由RGB三个分量组成，一个RGB565的每一个像素点数据位2Byte，即16位，那么从名字上就可看出来这16位中，高5位为R分量，中间6位为G分量，低5位为B分量。 Pasted image 20250731172336 sensor.set framesize(sensor.QVGA) ：分辨率设置。设置分辨率为QVGA，即320x240。VGA前面每加一个Q，图片就越小，处理速度就越快 分辨率：数字图像由一个个像素点组成，每个像素点只能显示一种颜色，大量像素点拼在一起组成数字图像 320X240：长度是320个像素点，宽度是240个像素点。 Pasted image 20250731173022 python sensor.set vflip(True) 垂直翻转 sensor.set hmirror(False) 水平翻转 lcd.rotation(0) lcd屏幕翻转 这三个函数是调节lcd屏的，第一次上电，lcd内显示的图像可能是倒着的，可能是镜像的，所以要用这些函数调整。 sensor.skip frames(time 2000) 跳过用于初始化的2000ms 摄像头刚启动时，图像质量还没稳定，所以跳过一些图像，time 2000表示跳过2000ms clock time.clock() 创建一个时钟以获得FPS 如注释说明 最后是while死循环： python while(True): clock.tick() 更新时钟 img sensor.snapshot() 获取摄像头传来的一张图片 lcd.display(img) 把图片显示在LCD上 print(clock.fps()) 将程序运行的帧率打出来 lcd.display()函数：在LCD屏上显示某张图像 print函数用串口输出字符串，无需添加串口相关的库，print直接用，而且每个print自带换行+回车。 根据print的功能，可以通过串口实现STM32与K210之间的通信 拔下typec线，发现lcd屏实时显示的图像不见了 因为其实根本没有把代码烧录进去，烧录的只有刚开始的maixpy固件，而点绿色箭头之后只是通过串口向k210发送命令 如何保存到k210的flash：工具→将打开的脚本保存到开发板的boot.py 二、色块追踪 Pasted image 20250731174602 python sensor.set auto gain(False) 颜色识别必须关闭自动增益，会影响颜色识别效果 sensor.set auto whitebal(False) 颜色识别必须关闭白平衡，会影响颜色识别效果，导致颜色的阈值发生改变 set auto gain(False) ：关闭自动增益，自动增益控制是指放大电路的增益自动地随信号强度而调整的自动控制方法，该功能自动调整增益会影响对颜色的判断，所以关掉。 set auto whitebal(False) ：关掉自动白平衡 白平衡：字面上的理解是白色的平衡。 白平衡是描述显示器中红、绿、蓝三基色混合生成后白色精确度的一项指标。 白平衡的数值越高，色调越暖；数值越低，色调越冷。 find blobs函数 ： python blobs img.find blobs([thresholds], roi my roi, x stride 30, y stride 30, invert False, area threshold 100,pixels threshold 100,merge False) 第一个参数是一个列表，因为只识别一种颜色，所以只有thresholds一个参数，可以添加多个，识别多个颜色。 roi my roi，前面写了my roi [0, 0, 320, 240] My roi列表里这四个参数分别是：顶点x、顶点y、宽度w、高度h，单位是像素。 摄像头拍照得到的图像分辨率是QVGA，也就是320X240，也就是宽度320，高度240。 由于LCD使用时是竖着的，所以坐标系是这样建立的： Pasted image 20250731175700 ROI（region of interest），感兴趣区域。 也就是运行寻找色块算法的区域，只在roi中寻找，my roi [0, 0 ,320 ,240]，正好是全屏，也就是整张图片都检测色块。 如果想只检测上半截图片，就可以my roi [0,0,160,240]。 x stride 就是查找的色块的x方向上最小宽度的像素，默认为2，如果只想查找宽度30个像素以上的色块，那么就设置这个参数为30。 y stride 就是查找的色块的y方向上最小宽度的像素，默认为1，如果只想查找宽度30个像素以上的色块，那么就设置这个参数为30。 invert 反转阈值，把阈值以外的颜色作为阈值进行查找，比如除了绿色的色块都找，有些工况会用得到。 area threshold 面积阈值，如果色块被框起来的面积小于这个值，会被过滤掉 pixels threshold 像素个数阈值，如果色块像素数量小于这个值，会被过滤掉 merge 合并，如果设置为True，那么合并所有重叠的blob为一个。 注意：这会合并所有的blob，无论是什么颜色的。如果想混淆多种颜色的blob，只需要分别调用不同颜色阈值的find blobs。 这个函数执行一次，返回值是一个含有七个参数的列表： 分别是：色块框x坐标，色块框y坐标，色块框宽度w，色块框高度h，色块包含像素数m，色块框中心x坐标，色块框中心y坐标。 python if blobs: for i in blobs: img.draw rectangle(i[0:4]) img.draw cross(i[5],i[6]) print(\"(%d,%d)\"%(i[5],i[6])) 然后通过函数返回值，在原图像上画个框，再画个十字准星，把色块标注出来，最后在lcd上显示加工过的图像。 接下来对要测的色块进行阈值标定 首先在K210上运行“2.色块追踪.py”，然后用摄像头照要识别的色块 保持这个色块出现在摄像机图像里，然后点击禁用 左上角工具栏：工具、机器视觉、阈值编辑器。 选择帧缓冲区。将右边调到尽量显示左边色块完整的轮廓， 调整得差不多之后，复制下面的LAB阈值，然后粘贴到开头。 Lab颜色空间（Lab color space） L代表亮度，取值在0 100之间，0是黑色，100是白色。 a代表从绿色到红色的分量，取值 128至127，正数偏红，负数偏绿。 b代表从蓝色到黄色的分量，取值 128至127，正数偏黄，负数偏蓝。 三、K210与STM32通信 python print(\"(%d,%d)\"%(i[5],i[6])) 没有STM32单独测试K210串口发送是否正常时，首先k210连接USB线，与maixpy连通之后禁用右上角的帧缓冲区： Pasted image 20250801113153 然后打开左下角的串行终端：就能快速看到发送的数据了。 Pasted image 20250801113211 四、视觉循迹 1. 数字图像直方图均衡化运算 2. OTSU（最大类间方差）二值化及底层C代码 3. 图像使用线性回归算法 Python THRESHOLD (0, 100, 128, 127, 6, 105) import sensor, image, time,lcd lcd.init(freq 15000000,invert False) lcd.rotation(0) lcd屏幕翻转 sensor.reset() sensor.set vflip(True) sensor.set hmirror(False) sensor.set pixformat(sensor.RGB565) sensor.set framesize(sensor.QQQVGA) 80x60 sensor.set auto gain(False) sensor.set auto whitebal(False) sensor.skip frames(time 2000) clock time.clock() while(True): clock.tick() img sensor.snapshot() img.binary([THRESHOLD]) img img.erode(1) img img.dilate(1) line img.get regression([(100,255)], robust True) if (line): rho err abs(line.rho()) img.width()/2 if line.theta() 90: theta err line.theta() 180 else: theta err line.theta() img.draw line(line.line(), color 127) print(\"(%.2f,%.2f)\"%(rho err,theta err)) lcd.display(img) sensor.set auto gain(False) sensor.set auto whitebal(False) 由于彩色巡线也要通过阈值识别颜色，凡是识别颜色的程序都要关闭自动增益和白平衡。 由于循迹的帧率要求很高，所以把图像缩小到了QQQVGA，80X60的大小。 img.binary([THRESHOLD]) Binary（二进制的 adj.），这个函数的意思是将当前图像 二值化 。 二值化，详细一点叫“黑白二值化”，也就是把一张彩图或者灰度图经过二值化处理之后，图片中只有黑和白两种颜色组成 Pasted image 20250802112907 二值化的算法必定要先给一个阈值（threshold）。比如彩色图，给出某个颜色的LAB阈值，二值化处理中属于阈值内的像素都变成白色，不属于阈值内的都变成黑色。 彩色巡线程序二值化思路是：二值化之后，蓝色都变成白色，不是蓝色全变成黑色。（其中蓝色的LAB阈值由阈值编辑器调出来) 执行完img.binary([THRESHOLD])后，图像会被按照蓝色和非蓝色二值化分割。 img img.erode(1) img img.dilate(1) 去除噪点用，是OpenCV的库函数，用到了卷积操作 line img.get regression([(100,255)], robust True) Regression（回归） linear regression （线性回归） 这个函数要实现的功能是将二值化后的蓝色像素点全部参与线性回归计算，计算出回归直线。 线性回归用到了最小二乘法： Pasted image 20250802113420 Pasted image 20250802113436 line img.get regression([(100,255)], robust True) 第一个参数[(100,255)]，由于img现在已经是二值化之后的图像了，所有像素的灰度值只有0和255两种情况，想选中所有白色的像素点进行线性回归，也就是选中所有灰度值为255的像素。但是这个参数必须是一个范围，不能只输入一个255，所以[(100,255)]里的100是随便取的，取254也可以，只要不取0就行（因为0是黑色）。 第二个参数robust True，若 robust 为True，则使用Theil Sen线性回归算法，它计算图像中所有阈值像素的斜率的中位数。默认开启即可。 Python if (line): rho err abs(line.rho()) img.width()/2 if line.theta() 90: theta err line.theta() 180 else: theta err line.theta() img.draw line(line.line(), color 127) print(\"(%.2f,%.2f)\"%(rho err,theta err)) lcd.display(img) 如果拟合成功了，有line对象： line.rho() 可视为直线与最下方水平线的交点值， img.width()/2经过运算后得到横向偏移量的误差值rho err。测试得rho err的范围是( 35,35)，如下图。 Pasted image 20250802113836 前面色块分析可以把模块和屏幕竖着放，但是并没有找到可以将图像旋转90°的函数，由于get regression函数的局限性，只能横着放模块了。 line.theta()表示直线偏斜的角度，经过if算法后得到角度误差值theta err。theta err范围是（ 90,90），如下图： Pasted image 20250802115102 后计算得到的角度误差值和横向偏移量，两个数据通过print函数从串口发出，发给stm32用于PID循迹。 注意在STM32的相关PID循迹中，要写两套PID，一个矫正角度偏移，一个矫正横向偏移量，然后两个pid的结果按一定的分配比例相加，做出综合的判断。 深色背景浅色赛道循迹 Pasted image 20250802115218 智能车竞赛的赛道就是深色背景（蓝色）和浅色赛道（白色）。 首先智能车摄像头并不能像k210一样python编程，只能当做传感器，传回数组形式的原始图像，在单片机中自己写C代码来处理图像。 第一步：二值化 灰度图中，二值化的阈值相比彩色图更简单，灰度图像素点的阈值是0 255，二值化的阈值是0 255中的一个数。例如100，灰度值小于100的变成白色，大于100的变成黑色。 随着环境变化，阈值并不能保证时刻是最佳阈值。尤其是智能车，时刻要保持最好的阈值进行二值化，把赛道和背景尽可能清晰的分开。 如何寻找阈值：OTSU算法（大津法）（最大类间方差算法） 用这个算法可以通过分析传来的灰度图像，分析当前图像的最佳阈值并进行二值化。 来自chatGPT的解释： 类间方差（between class variance）是一种用于衡量图像分割或模式识别中类别间差异的统计指标。它主要用于评估不同类别之间的分离程度，反映了不同类别之间的平均差异程度。 在图像分割任务中，类间方差常用于确定最佳阈值或分割边界。通过最大化类间方差，我们可以寻找最佳的分割阈值，将图像分成两个或多个类别，使得类别间的差异最大化。 当存在明显的前景和背景区域，并且它们之间的差异较大时，类间方差通常会显示出峰值。在这种情况下，通过调整阈值或分割边界，可以将图像分成两个类别，使得类别间的差异最大化，从而产生峰值。 所以，基于最大类间方差反应的物理量，OTSU算法的整体思路是： 遍历0 255的灰度值，直到找到灰度值a，使得[0 , a 1]与[a, 255]两部分灰度的最大类间方差处于峰值时，取当前的a作为最佳阈值进行二值化分割。 OTSU算法（大津法） FJW：图传—阈值计算—二值化，压缩，y轴反转—搜图扫线—边界，拐点等图像特征—元素处理 期望值是变量输出值的平均数，数学期望的计算公式： E ( X ) X 1 ​ ⋅ P ( X 1 ​ ) + X 2 ​ ⋅ P ( X 2 ​ ) + ... + X n ​ ⋅ P ( X n ​ ) 期望的性质 E ( X + Y ) E ( X ) + E ( Y ) 设图像最佳阈值为Ｔ，Ｔ将图像分为目标和背景。其中目标点数占总图像比例为w 0 ，平均灰度值为u 0 ；背景点数占图像比例为w 1 ，平均灰度值为u 1 则， w 0 ​ + w 1 ​ 1 图像的总平均灰度值为： u w 0 ​ u 0 ​ + w 1 ​ u 1 ​ 类间方差公式为： g w 0 ​ ( u 0 ​ − u ) 2 + w 1 ​ ( u 1 ​ − u ) 2 注意与普通方差的公式不一样。 OTSU算法的C语言实现。代码可移植到智能车单片机内直接使用。 C define GrayScale 256 //大津法，算阈值 uint8 otsuThreshold(uint8 image, uint16 col, uint16 row) { uint16 width col; uint16 height row; int pixelCount[GrayScale];//灰度计数数组：灰度i出现的次数 float pixelPro[GrayScale];//概率 int i, j, pixelSum width height;//整张图片的像素总数 uint8 threshold 0; //最终要输出的阈值 uint8 data image; //指向图像数据的指针 for (i 0; i deltaMax) {//求出目前最大的类间方差 deltaMax deltaTmp; threshold j; } if (deltaTmp 90: theta err line.theta() 180 else: theta err line.theta() img.draw line(line.line(), color 127) print(\"(%.2f,%.2f)\"%(rho err,theta err)) lcd.display(img) 与彩色巡线代码的差异： 首先，传入的是灰度图像 sensor.set pixformat(sensor.GRAYSCALE) ，而不是彩色图。 多了 histogram img.get histogram() ，histogram（直方图），这个方法的作用是进行数字图像直方图均衡化运算 均衡化前后的效果差异： Pasted image 20250802162402 原本灰不溜秋的图片，处理之后变得层次分明。直方图均衡化运算对于灰度图像的作用是让图像的灰度分布均匀，如下图所示，避免图像中大部分的像素灰度值集中在某个值形成峰值，灰度分布图平缓起来之后，图像的对比度就会更加清晰，也就更方便二值化。如下图： Pasted image 20250802162515 Thresholds histogram.get threshold() v Thresholds.value() get threshold()函数就是大津法。v就是返回的最佳阈值。 二值化： img.binary([(0,v)],invert False) 二值化的时候传入的元组只有两个值，因为灰度图像只需要一段灰度值，选择的灰度值是[0 , v]， 给暗的像素变成白色，亮的像素是黑色 ，这样做是因为循的是黑色赛道，白色背景，所以要反过来，灵活变通即可。 python line img.get regression([(v,255)], robust True) robust为True则使用Theil Sen线性回归算法，它计算图像中所有阈值像素的斜率的中位数。 线性回归，给白色像素线性回归，也就是黑线部分线性回归 五、深度学习体验： yolov2数字识别、人脸识别、垃圾分类等（物体检测） 解决一大部分机器视觉问题，比如： 1. 基础人脸识别，即能分别出是人脸，但是不知道是谁的脸。 2. 垃圾分类，识别垃圾的品种。 3. 识别水果，香蕉葡萄西瓜等、识别动物、植物、交通工具等各种各样的东西。 4. 一些应用，安全帽检测，口罩检测,这次的数字识别。 视觉识别技术：基于深度学习的 物体检测 。 思维导图： Pasted image 20250802164301 六、AprilTag视觉测距 实验现象：使用摄像头进行测距，只需要把这个像二维码一样的东西打印出来贴在被测距的目标位置上。 Pasted image 20250814120654 maixpy上方菜单栏→工具→机器视觉→AprilTag生成器→H11家族 原理：Pasted image 20250814120805 通过小孔成像，可以测量得到照片的像素来算出看到物体的距离，可以分析得到一张图片的x，y，z方向的偏移和x，y，z轴的旋转角度——AprilTag的厉害之处。 首先需要得到所用镜头的焦距，以及感光面的长宽。 通过找客服查手册，所用的maixbit开发板上的镜头的焦距是4.0mm，感光元件型号为ov5642，查阅ov5642的数据手册得到感光面的长宽如image area所示： Pasted image 20250814121127 看代码： Python import sensor, image, time, math sensor.reset() sensor.set pixformat(sensor.RGB565) sensor.set framesize(sensor.QQVGA) sensor.skip frames(30) sensor.set auto gain(False) sensor.set auto whitebal(False) sensor.set vflip(True) 垂直翻转 sensor.set hmirror(False) 水平翻转 clock time.clock() c x 是图像的x中心位置 c y 是图像的y中心位置 f x (4.0 / 3.6736) 160 默认值 f y (4.0 / 2.7384) 120 默认值 c x 160 0.5 默认值(image.w 0.5) c y 120 0.5 默认值(image.h 0.5) dis gain 1.0 def degrees(radians): return (180 radians) / math.pi while(True): clock.tick() img sensor.snapshot() for tag in img.find apriltags(fx f x, fy f y, cx c x, cy c y): 默认为TAG36H11 img.draw rectangle(tag.rect(), color (255, 0, 0)) img.draw cross(tag.cx(), tag.cy(), color (0, 255, 0)) print args (tag.x translation(), tag.y translation(), tag.z translation(), \\ degrees(tag.x rotation()), degrees(tag.y rotation()), degrees(tag.z rotation())) 位置的单位是未知的，旋转的单位是角度 dist dis gain math.sqrt(tag.x translation() tag.x translation()+\\ tag.y translation() tag.y translation()+tag.z translation() tag.z translation()) print(\"%f\"%dist) 图像分辨率为QQVGA，为160x120。 f x，f y的参数中，4.0是焦距，3.6736和2.7384是感光元件的长宽，160和120是最终图像分辨率。 使用find apriltags函数找到所有的apriltag，这里只使用了一个apriltag。​ 变量print args里面储存了处理好的三个横向偏移量和三个角度偏移量，这里没有用到角度，但是角度值也是很有用的。​ dist变量为测量的最终距离，这个距离计算很简单：​ d i s t x 2 + y 2 + z 2 ​ 有一个增益系数dis gain，这个增益系数与打印出来的apriltag的实际大小有关，测距前需要用尺子量一下真实距离，再对比测量距离，然后真实距离/测量距离即为这个增益值。​","summary":"MicroPython 机器视觉与常用图像处理学习笔记。"},{"permalink":"https://zhr0529.cn/posts/stm32第二部分学习/","title":"STM32第二部分学习（待命中）","content":"接触寄存器，使用指针，接触单片机地址的概念，接触精确到对一个字节的里的一位的读写 一、UART串口通信 uart串口通信底层原理SPI、UART、RS232、RS485、IIC 通信总线协议 关于UART和USART： UART串口通信协议全称叫 通用异步收发传输器 （Universal Asynchronous Receiver/Transmitter） USART串口通信协议全称叫做 通用同步异步收发传输器 （universal synchronous asynchronous receiver and transmitter） 串口相关的项目绝大多数情况下都使用异步模式，所以stm32串口也是 uart 类型的异步串口通信 USB转TTL： TTL电平信号规定，+5V等价于逻辑“1”，0V等价于逻辑“0”(采用二进制来表示数据时)。这样的数据通信及电平规定方式，被称做TTL（晶体管 晶体管逻辑电平）信号系统。 高电平是逻辑“1”，是3.3V或5V； 低电平是逻辑“0”，是0V。 电路接线：注意是交叉接线 屏幕截图 2025 07 11 110718 TXD：发送数据Transmit(tx) Data，TXD和TX是完全一样的。 RXD ：接受数据 Receive(rx) Data，RXD和RX是完全一样的。 TTL模块的TXD相当于电脑的发送接口，连STM32的RXD接收接口，也就是RX。 一端是发送TX，另一端就应该是接收RX 看代码： 屏幕截图 2025 07 11 140655 uart init(9600); 9600是串口的 波特率 ： 波特率(Baud Rate)单位bps 每秒传送的二进制位数 bit Per Second。 9600bps就是每秒传输9600位。 一个字节是8个bit，一个字节可以表达一个ASCII码，9600波特率的信道（信号的传递通道，理论上每秒可以传输9600/8个英文字母的数据量，1200字节，约1.2KB） （1字节 1B 8位 8bit）。 p.s. 如果1.2KB/s的网速，可以想象通信有多慢，所以uart通信并不是高速通信。 uart init内部： 分段看屏幕截图 2025 07 11 141307串口一（usart1）用到了PA9和PA10，所以有GPIO结构体： GPIO InitTypeDef GPIO InitStructure; Usart1有串口相关的配置，关于串口的结构体： USART InitTypeDef USART InitStructure; Usart都有一个串口中断，所以有NVIC结构体： NVIC InitTypeDef NVIC InitStructure; 使能总线时钟，Usart1在APB2总线上 A9口是复用推挽 复用的意思就是这个IO口把自己的推挽输出功能给串口一这个外设使用 使用推挽输出是因为PA9是TX，发送方 PA10是浮空输入，接受方 屏幕截图 2025 07 11 153425 串口接收信息时会频繁进入串口接受中断，为了不影响其他程序正常运行，一般把中断两位优先级调到最低。 屏幕截图 2025 07 11 153616串口的配置： Bound是波特率，作为uart init的形参输入 数据长度就是8位（一个字节的长度） 1位停止位；无奇偶校验位 屏幕截图 2025 07 11 154150 第一句可以保留跳过 串口模式：可收可发 Pasted image 20250711154315 1. 配置串口结构体 2. 开启中断：USART IT RXNE是 串口接受中断 ，是串口最常用，也是接受数据必须用的中断 3. 使能串口1 如何使用使用串口2或者串口3 打开usart.c，翻到上面： 屏幕截图 2025 07 11 154828 把这里的串口改成USART2或者USART3即可 去main函数看while(1) 屏幕截图 2025 07 11 154533 按下K1键，printf。 printf使用后，会将内容通过串口1发送出去 \\r和\\n等大多数转义字符的大小都是 一个字节 加了这这个回车+换行，xcom端的数据很美观 功能：按下K1，电脑端xcom接收栏收到Hello和“1”，同时OLED显示发送成功； 下一个功能： 屏幕截图 2025 07 11 160634 功能：按下K2，在RCOM串口绘图软件上绘图 屏幕截图 2025 07 11 161219 注意：绘图这里printf的发送格式是RCOM这个软件的作者定的，所以printf函数要遵守软件作者的规定。就是数据最后要加\\r\\n，数据之间逗号隔开，并且一组数据要用小括号包裹起来。 那么如果需要画多路数据，printf数据的格式： 举例，三路数据：”(%d,%.2f,%.1f\\r\\n)” STM32的接收功能 屏幕截图 2025 07 11 161535 功能：在XCOM中发送内容，在OLED上会 实时 显示（不能发中文，因为ASCII码没中文）屏幕截图 2025 07 11 163219注意取消勾选“16进制发送”，勾上“发送新行” 发送新行：给准备发送的字符串最后两位都默认加上\\r\\n go to definition一下这三个变量屏幕截图 2025 07 11 164344发现它们主要使用在串口中断里，也就是usart.c里面的USART1 IRQHandler函数 其次，strcpy和memset是C标准库里string库的函数，所以引入了string库：“string.h” 串口1的中断函数屏幕截图 2025 07 11 164920 功能 ：来一次串口接收中断就代表接收到了 一个字符，也就是一个字节的ASCII码 ；把每次接收到的字符ASCII码按顺序一个一个存在数组里，就组成了一句字符串，这样就得到了上位机发来的消息 屏幕截图 2025 07 11 232904首先，这六句是为了操作系统的移植做准备，可以不管 第一个判断：屏幕截图 2025 07 11 233237 USART IT RXNE是串口接受中断，来一个接收中断代表电脑发来一个字节的内容，这个字节的内容已经存到USART1 DR寄存器里了。 ! RESET，RESET就是二进制0，代表“否”，所以 ! RESET等同于 SET 所以这句if的意思就是，串口接收中断来了 屏幕截图 2025 07 11 234100中断来了马上就读取接收到的一个字节的数据，USART ReceiveData函数作用就是读取了DR寄存器的相应位，得到了那一个字节的数据。2025.7.11，暂时还不知道DR寄存器是什么，再说吧 一个字节是8位，所以变量Res：屏幕截图 2025 07 11 234543就必须要u8，严格的一个字节的存储空间，来接收这个数据。 Pasted image 20250713101208 0x8000转化为二进制→1000 0000 0000 0000屏幕截图 2025 07 13 101301 USART RX STA是自己定义的一个16位寄存器 USART RX STA&1000 0000 0000 0000，这个运算是除了最高位，其他位全部置零 (USART RX STA&0x8000) 0 就是接受未完成，也就是最高位是0，没接收完成，也就是： 最高位是1代表接收完成 判断接收完成的方法 ： Pasted image 20250713101711 0x0d：\\r 回车 0x0a：\\n 换行 \\r是回车，\\n是换行。按下一个空格键是回车+换行，回车是把光标移到开始,换行是跳到下一行。 接收到一对连续的回车+换行，代表一句的结束 \\r\\n总是成对出现，如果单独收到了\\r，下一个字符不是\\n，那代表接收错误 Pasted image 20250713102004接收到0x0d，USART RX STA次高位会置1 如果是普通的内容信息，就进else，就把这次接受的字符存进数组： USART RX BUF里：屏幕截图 2025 07 13 102130一个字符是8位（一个字节） USART REC LEN 200，字符有长度限制，理论上改多大都不影响功能 USART RX STA&0X3FFF USART RX STA&0011 1111 1111 1111 最高位和次高位置0，因为这两位是标志位。其余的14位存的就是当前字符串长度。 回到主函数： Pasted image 20250713102450 (USART RX STA 15) 1 \\ 右移是位操作，意思是最高位是1，代表接收完成，数据可用 strcpy函数 Strcpy函数就是字符串复制（copy），函数输入是两个 char类型 字符串的头指针， 后者的值复制给前者 。 （众所周知，数组名就是数组的头指针） C char a[100]; printf(\"%p\\n\", a);// 输出数组首地址 printf(\"%p\\n\", &a[0]); // 输出数组第一个元素的地址 输出结果： C 0x7ffee4b5c6a0 0x7ffee4b5c6a0 a 和 &a[0] 的值是相同的 因为USART RX BUF的数据类型是u8，而strcpy函数必须要输入char类型的字符串首地址，需要(char )强制转换一下。 屏幕截图 2025 07 13 102846 后三句跟OLED清屏有异曲同工之处。 标志位置0，开启下次数据的接收。 memset函数 memset就是清空一段内存（RAM）的值 memset函数的参数： C memset(&text,0,sizeof(text)); //写法1 memset(text,0,sizeof(text)); //写法2 第一个参数是要清空的一段空间的起始地址（以上两种写法都可以，更推荐写法2） 第二个是参数一般都是0，内存（RAM）存储中没存数据的地方都是0， 填0起到清空数据的作用 第三个参数是要清空的字节长度，用sizeof函数得到数组长度。 为什么写法2？ c++ char a[100]; printf(\"%p\\n\", (void )&a); // 输出数组的地址 printf(\"%p\\n\", (void )a); // 输出数组首地址 输出： c++ 0x7ffee4b5c6a0 0x7ffee4b5c6a0 &a 的含义 上面代码示例中 &a 是数组的地址。 类型是 char ( )[100] （指向 char[100] 数组的指针）。 &a 表示整个数组的地址，而不是数组第一个元素的地址。 虽然 &a 和 a 的值相同，但它们的类型不同。Pasted image 20250713103448 指针运算示例： c++ char a[100]; printf(\"%p\\n\", (void )(a + 1)); // a + 1 指向 a[1]，地址增加 1 字节 printf(\"%p\\n\", (void )(&a + 1)); // &a + 1 指向下一个 char[100]，地址增加 100 字节 输出： c++ 0x7ffee4b5c6a1 // a + 1 0x7ffee4b5c704 // &a + 1（0x7ffee4b5c6a0 + 100） 对于数组来说，&a这种写法真的很少见，很少用 二、多串口同时工作 uart3将一个字符串发给uart1，然后由uart2把uart1收到的内容发给上位机XCOM main函数代码 Pasted image 20250713114230 strcat函数 Strcat函数的作用就是拼接字符串，在一个字符串的后面接上另一个字符串，这里用到主要是因为BUF数组里没有存0X0D和0X0A，所以没有回车换行，要自己加上回车和换行符。 usart.h Pasted image 20250713114609 除了最大字符长度限制之外，其他的变量分身成了三个，分别给三个串口用， USART REC LEN变量不修改是因为没什么必要让三个串口接收数组的最大长度各不相同，都设置为200就行。但是200还是太大了，占内存，真实使用时可以缩小到20，够存数据就行。 usart.c Pasted image 20250713124528 把printf的功能给串口3 usart1的代码 Pasted image 20250713124713Pasted image 20250713124744 Pasted image 20250713124802 之前usart1数组的名称都没有加1，比如USART RX STA，现在串口变成了三个，就需要把变量分开来，所以将相关的变量名都加上1/2/3这样的后缀，便于区分。 后面的代码都是用uart1的代码复制粘贴然后修改串口号得到的， 但是注意！ 下面是usart1的总线时钟使能代码： Pasted image 20250713124939 下面是usart2的总线时钟使能代码： Pasted image 20250713125000 下面是usart3的总线时钟使能代码： Pasted image 20250713125019 Usart1与usart2/usart3的APB总线不一样，要注意。 然后是优先级问题： Usart1的优先级：0/0 Usart2的优先级：0/1 Usart3的优先级：0/2 如果是自己写硬件代码，切记修改优先级，串口的优先级不能过低，如果程序量很大，定时器中断占用时间过多，会导致串口接收中断进不去，从而使串口接收不全。 但优先级也不能过高，比如串口接收数据量非常大，还很频繁，这样反而会影响到其他中断或主函数。 优先级根据项目灵活调节。 最后是main函数里的延时200us Pasted image 20250713125335 因为串口三的printf刚刚发送完就判断串口一是否接收完成，时间间隔太短，此时串口一还没有接收完成，结果就会导致跳过了if，不加延时就会导致下面这种情况： Pasted image 20250713125433 后面数字都是偶数，证明了前面说的错误，所以尝试增加延时： 当delay us(100)时，发现正常了，所以为了使接收更保险，delay us(200)，双倍保险的延时，就没有任何问题了。 三、外部中断 外部中断是什么 外部中断通俗来讲就是用普通的IO口的“外部中断功能”来监测外部电路（如按键电路）的 一次高低电平的跳变，检测到就会触发一次外部中断； 在外部中断里可以做自定义的任何事情，比如串口发一串字符等 外部中断是接触到的第二个中断，本质还是中断，也就是打断main函数做点事情 外部中断触发按键相对于按键实验 有何区别，有何优点 Pasted image 20250713193728 K1 K4接对应的IO口，如果配置IO口都是上拉输入，那么按下按键，对应的IO口就会产生 下降沿 上升沿和下降沿的概念是数电里的一个基本概念 Pasted image 20250713193854 按键实验中的代码是通过 软件的逻辑语句 来判断下降沿的到来 （while(1)里每10ms检测一次按键状态） ，但是用了外部中断就不用写这些判断程序了， 通过单片机自己专门检测电平跳变的一个外设——外部中断 自己检测，测到了上升/下降沿就 自动进中断 ，这一点比普通按键 更方便、更实时、更节约资源 因为随着程序越来越大，代码越来越复杂，while(1)里的程序越攒越多，while(1)内部代码循环一次的执行时间也越来越长甚至发生阻塞 （阻塞就是执行到一句代码处卡住一直执行这一句，后面的程序都要等这一句代码结束才能继续执行） ，就会影响key scan函数的实时性，所以就可能出现按下按键没反应的情况。 而外部中断方式不存在这个问题，中断的优先级高于main函数，并且可以独立于main函数之外进行判断，非常节省资源。 Pasted image 20250713200430 由上图可知：外部中断的资源非常多，跟IO口的下标有关，比如PA0，PB0（更高级的STM32型号可能还有PC0，PD0，PE0……）同时对应着中断线0（EXTI0）；PA1,PB1同时对应着中断线1，以此类推，一直到15。 每一个io口都可以拿来做外部中断。 看代码： Pasted image 20250713200650 比之前多了个EXTIX init，外部中断初始化 外部中断初始化函数： Pasted image 20250713201931 多了个新的结构体变量：EXTI InitTypeDef EXTI InitStructure; RCC APB2PeriphClockCmd(RCC APB2Periph AFIO,ENABLE);//外部中断，需要使能AFIO时钟 AFIO时钟是复用时钟，也就是IO口复用作外部中断口。 复用就是把IO口借给外部中断外设使用。 GPIO EXTILineConfig中断线路配置，改参数即可B4口就GPIO PortSourceGPIOB,GPIO PinSource4，其他以此类推。 Pasted image 20250713202318 因为B4口后缀是4，所以EXTI Line选择EXTI Line4。 EXTI Mode是选择中断模式还是事件模式，选择中断模式。 EXTI Trigger是触发方式，上升沿触发、下降沿触发，还是上升下降都触发，根据实际情况选择，选下降沿。 关于外部中断的重点服务，注意： EXTI Line0到EXTI Line4，这五个通道每个通道都享有一个中断服务函数， 但是EXTI Line5到EXTI Line9，这五个通道共用一个中断服务函数：EXTI9 5 IRQHandler， EXTI Line10到EXTI Line15，这六个通道共用一个中断服务函数：EXTI15 10 IRQHandler。 再去看中断服务函数内：外部中断内的操作： Pasted image 20250713203134 if(GPIO ReadInputDataBit(GPIOB,GPIO Pin 4) 0) 很值得注意 已经进入外部中断了，就代表下降沿已经来了，为什么还要用判断输入的函数再判断一次？ 因为要保险，进一步保证按钮电路按下的电信号噪声波动不会影响外部中断，这句话相当于又加了一层保险。 四、独立看门狗和窗口看门狗 看门狗的作用： 防死机 在实际做项目的过程中，由于电机反电动势、电磁干扰等各种问题，电路没有做好安全防护，有可能会使单片机的供电电流不稳定，导致单片机死机。 当单片机在工作时死机了，就需要看门狗 “看门狗”是个比喻。 定时“喂狗”，狗就不会叫； 但如果没有定时喂狗，狗没有吃的就会大叫，就会 复位 单片机。 这就是看门狗的机制，实现了死机自动复位 独立看门狗 STM32的独立看门狗由 内部独立 的40Khz低速时钟驱动，即使主时钟发生故障，它也仍然有效。 这个40khz并不精确，实际频率在30 60Khz之间，但是不影响它的正常功能。 代码： Pasted image 20250714163737 独立看门狗需要手动喂狗，IWDG Feed();给独立看门狗的键寄存器喂值。 Pasted image 20250714163824 Pasted image 20250714164016 Pasted image 20250714164041 Pasted image 20250714164208 代码的喂狗操作与寄存器手册里说的操作是对应起来的，开始时init函数里像键寄存器发送0xCCCC开启看门狗，后在while(1)中每隔100ms发送一个0xAAAA，否则计数器为0时看门狗会产生复位 IWDG Init(4,625); //预分频数为64,重装载值为625，所以溢出时间为1s。 注意第一个系数4，并不是预分频系数是4，而是寄存器写100（二进制的4） 代表预分频系数是64，具体规则如下图： Pasted image 20250714164844 40000/64 625Hz，重装载值是625，那周期正好是1秒。所以要喂狗的周期要在一秒以内，否则单片机自动复位 在exti.c中，原来是EXTI4 IRQHandler，为了让单片机死机，故意制造一个错误，把中断服务函数名：EXTI4改成EXTI0： Pasted image 20250714170412 运行程序，XCOM上位机会不断收到ok，当按下k1按键，会短暂死机，然后重新复位。 平时使用独立看门狗就可以了，尽量不用窗口看门狗。因为独立看门狗好操作，原理也清晰，最重要的是在实际的实验中，独立看门狗的效果确实好于窗口看门狗。 窗户开门狗 这个看门狗并不好用，设置喂狗时间的机制很麻烦，而且应对死机的效果也不好，简单看一下作为了解吧 看代码： Pasted image 20250714171248 Pasted image 20250714171350 Pasted image 20250714171421 图中，T[6:0]就是WWDG CR的低七位，W[6:0]即是WWDG CFR的低七位。 T[6:0]是窗口看门狗的计数器（自动递减），W[6:0]是窗口看门狗的上窗口，下窗口值是固定的（0X40）（01000000）。 当窗口看门狗的计数器在上窗口值之前被刷新，或者低于下窗口值都会产生复位。 上窗口值（W[6:0]）是由用户自己设定的，根据实际要求来设计窗口值，但是一定要确保窗口值大于0X40，否则窗口就不存在了。 Pasted image 20250720125730 代码中的预分频系数： Pasted image 20250720125748 第二个值0X5F(01011111)是窗口值(WWDG CFR)，也就是W[6:0]。 第三个值0X7F(01111111)是递减计数器的初始值，也就是T[6:0]。 窗口看门狗是在中断里喂狗。 Pasted image 20250720125824 五、蓝牙串口与AT指令 蓝牙模块：HC05 Pasted image 20250721102859 可以使用手机连接蓝牙，让手机和蓝牙互传信息；也可以两个蓝牙模块相互连接，比如两个小车协同工作，它们之间使用HC05蓝牙模块来相互通信最为合适 HC05有六个引脚： STATE脚：反馈连接状态，低电平是未连接，高电平是已连接。 正常不需要，因为蓝牙未连接的时候，蓝牙模块板载LED灯会快速闪烁；已连接时会慢速闪烁，现象已经比较明显，暂时不需要其他指示信号。 VCC接3.3V和5V都可以； RX和TX是串口的收和发，就是uart通信协议 本质还是uart通信 ，蓝牙只是在两个uart通信之间把信号的传输媒介换了一下而已，以前要连电线，现在是无线电波了，所以基础还是串口通信。 EN脚：接高电平进入AT指令模式（也就是配置模式） AT指令模式介绍：在这个模式下蓝牙不能进行无线通信，只能有线连接电脑，对蓝牙模块进行配置，比如配置通信的波特率、查询蓝牙地址等操作。 先让蓝牙进入AT模式，使用AT指令配置蓝牙： Pasted image 20250721131736 注意HC05蓝牙模块的AT配置模式规定使用 38400 的波特率 发个AT过去， 注意勾选发送新行 多发几次，蓝牙模块每次都会回复“OK”。 AT指令来自HC05模块配套的AT指令集： Pasted image 20250721131829 发AT+RESET，也会回复“OK” 配置波特率： Pasted image 20250721131931 输入AT+UART?，回复： Pasted image 20250721132204 可以知道模块默认的配置是波特率9600,1位停止位，无校验位。 配置波特率是115200，输入：AT+UART 115200,0,0 用手机蓝牙连接就可以实现收发信息了 看代码 Pasted image 20250721140248 没有新东西说明蓝牙通信本质还是uart EXTIX Init里： Pasted image 20250721140345 原来的printf换成了：UART1 Send String函数 这两个函数的功能上完全相同，都是串口发送。 UART1 Send String 2025.7.21: 对这个函数的分析用到了很多指针和单片机底层原理的内容，如果实在看不懂可以先放着，把这个函数当printf用就行，等后面补充了相关的理论知识再学！ Pasted image 20250721140826 串口中断服务函数开头的数据接收中： USART Receive Data函数，就是读取串口DR寄存器的信息，也就是读取DR寄存器上这一个字节大小的信息，并将其返回。 USART SendData 函数就是高度类似的库函数，就是把一个字节的信息写入DR寄存器，然后DR寄存器会自动将其发送出去。 Pasted image 20250721141223 USART FLAG TXE是串口的发送中断，也就是发送成功后这位会置1，所以利用这个while循环，可以确保等到这个字节发送完了再进行下一个字节的发送。 函数的输入值是char Data 当输入的函数形参（变量Data）是一个字符串的时候，一开始 Data代表着这个字符串 首地址的内容 ，然后用USART SendData把这个首地址的内容，也就是一个字节的char给输出出去，然后 Data++并不是代表这个首地址的内容+1，这里的区别就与i++和++i的区别一样，实际的意思是这个地址Data先取内容，这个 实际看做是个运算符，， 地址Data里的内容被这个函数使用完之后，这个函数已经进行完了之后最后再执行Data++ ， 也就是字符串当前的首地址+1 ，也就是Data指针指向了字符串的下一个字符。 可以把上面的USART SendData(USART1, Data++);拆成两句： USART SendData(USART1, Data); Data++; 也就是每当USART SendData发送完一个字节的命令，字符串地址就会+1， 指向字符串下一个字符； 当发送的字符是这个字符串最后一个字符时，指针还是会指向字符串最后一个字符的下个字符，也就是末尾的0， 当这个字符的ASCII码是0的时候就代表是空，也就是这个字符串结束了。 这时 Data就是0了，就会从While( Data)里跳出来，也就代表整个字符串已经发送完毕了，函数结束。 输入q，单片机板载led改变状态： Pasted image 20250721142509 接收完成之后，使用C标准库的库函数： strcmp函数 strcmp函数用于判断两个字符串是否完全相同。 strcmp函数判断USART RX BUF字符串的值是否等于”q”，如果相等，函数返回值就是0，满足判断条件，PC13就会改变一次输出电平。 六、LoRa远距离无线电通信 lora（Long Range Radio） 远距离无线电 当需要几百米甚至几千米的相互通信时，选两个lora模块。 当需要近距离连手机进行通信时，用蓝牙模块（或者WiFi，之后会讲） LoRa是semtech公司开发的一种低功耗局域网无线标准，其名称“LoRa”是远距离无线电（Long Range Radio），它最大特点就是在同样的功耗条件下比其他无线方式传播的距离更远，实现了低功耗和远距离的统一。 本质上依然是串口通信 看代码： Pasted image 20250721155844 多了一个lora init函数： Pasted image 20250721160806 函数内一堆“AT”字样，发送AT指令 其实在蓝牙模块的例程里也可以写一个blue tooth init，用stm32去配置AT指令，不用像连到电脑上配置了。 蓝牙模块人为规定AT配置模式的波特率是38400，lora模块人为规定的AT配置波特率是115200 AT指令配置，要确保配置阶段的波特率是115200。 先初始化了A6，A6用来控制MD0引脚的高和低，看下lora模块用户手册的介绍： Pasted image 20250721161243 固件升级功能基本用不到，所以AUX引脚悬空即可，MD0为高电平时进入AT调试模式，MD0为低电平时进入正常通信模式。 / define LORA MD0 PAout(6) Pasted image 20250721161546 然后先发了一个“ATE0” Pasted image 20250721161609 配置让它指令不回显 回显：比如给模块发送最基础的AT，模块会回复： AT OK 如果让指令不回显，模块回复： OK Pasted image 20250721161903 LORA CMD： Pasted image 20250721161932 输入的是两个字符串的首地址，第一个cmd是本次命令，第二个ack是期待的回复。 函数先printf发送命令，while(1)中不断重复等待回复，如果接收成功了， 使用strstr函数（C语言string.h库里的函数） 判断字符串ack是不是收到的字符串的子串，然后清空接收字符串内容和接收完成标志位。 只要strstr的返回值不是0（NULL），就说明ack是接收函数的子串。 所以用 ! 0，不是0，所以LORA模块一定成功回复了OK，就代表lora成功接收到了指令。 AT+WLRATE 0,5 （信道配置和空中速率配置） 信道就是无线通信的频段，lora模块提供了32个频段的无线通信信道，也就是 AT+WLRATE指令的第一个变量可以取0 31。 空中速率6级可调（0.3、1.2、2.4、4.8、9.6、19.2Kbps），分别对应0 5， AT+WLRATE指令的第二个变量可以取0 5。 AT+UART 7,0，设置串口通信的波特率， 1200,2400,4800,9600,19200,38400,57600,115200 八档可调，对应0 7。 其他的，发射功率20dbm，休眠时间1s，工作模式为一般模式，基本可不变。 设备地址可以在0 65535进行设置。 透明传输和定向传输 透明传输：透传 例如：A设备发5字节数据0XAA 0XBB 0XCC 0XDD 0XEE到B设备，B设备就可以收到数据0XAA 0XBB 0XCC 0XDD 0XEE。 透明传输，两个lora要有相同地址、相同的通信信道，用户数据可以是字符或16进制数据。 定向传输 例如：A设备（地址为：0x1400，信道为0x17（23信道、433Mhz））需要向B设备（地址为：0x1234，信道为0x10（16信道、426Mhz））发送数据AABBCC，其通信格式为： 123410AABBCC ，其中1234为模块B的地址，10为信道，则模块B可以收到AABBCC。同理，如果B设备需要向A设备发送数据AABBCC，其通信格式为：140017AABBCC，则A设备可以收到AABBCC。（定向传输，针对设备间地址和通信信道不同，数据格式为16进制，发送格式： 高位地址+低位地址+信道+用户数据） 七、RTC时钟毫秒级万年历 ‘实验效果： 用串口打印出当前的年/月/日/时/分/秒/毫秒 Pasted image 20250722093332 RTC（Real Time Clock）实时时钟，STM32中集成了RTC的功能，也就是一个万年历的功能，能将计时大到年为单位，小到毫秒为单位。 RTC的原理也是内部一个定时器，然后一堆寄存器进行操作 main函数： Pasted image 20250722093536 Calendar（日历）是一个结构体变量： Pasted image 20250722093657 vu8和vu16： Pasted image 20250722093813 IO： Pasted image 20250722093850 Volatile 类型修饰符 Volatile（易变的，不稳定的） 是一种 类型修饰符 ，加在int float这种数据类型前面，具体的作用是告诉编译器，这个变量是多变的，不可以优化带有此变量的代码。 C include int pass 1; int main() { while(pass) {;} return 0; } Void Interrupt() //某个中断 { pass 0; } 主函数中pass 1，为真，编译器为了优化代码，把pass 1优化成了while(True)，将循环条件直接改为真，后续不再从内存中读取pass的值来判断循环是否停止。 这时有一个中断令pass 0，while也并不会停下来，因为程序已经不经过调取pass变量的值了。 但是把int pass改成volatile int pass 1; 中断一赋值，就可以跳出循环。因为加了这个volatile，编译器无法优化代码，必须每次都调出pass来看看。 main函数中： Pasted image 20250722094122 换算出当前毫秒数，取了某个寄存器的值，然后做个变换 RTC时钟电路结构 RTC时钟电路由单片机的VBAT（VB）引脚单独供电，如果给这个VB口单独给个3.3V电源，STM32的其他口都断电，那这个RTC时钟还是能正常计时的，时间不会断。 所以RTC是可以掉电运行的。 如果只希望STM32芯片正式上电工作的时候使用RTC时钟计时一小段时间，无需下面的电路； 如果需要让RTC时钟工作超过一个星期，且掉电继续工作，那就必须要以下的电路： Pasted image 20250722094311 RTC的定时器频率哪里来： 没有上图硬件电路的时候，RTC的定时器由STM32内部低频时钟提供，内部低频时钟的缺点是不太准，长时间工作的话会有误差。 使用了上图电路之后，RTC时钟在硬件电路上可以单独享有一个32.768K的低频晶振，取这个数值大概是要凑出完整的1秒。这样RTC时钟就非常精准，据说是计时一年的误差不超过一秒hhh。 VBAT电池： Pasted image 20250722094523 单片机断电之后，VCC3.3就没电了，就是0，这时候，VBAT口（图示写错了，不是VBTA）有纽扣电池BAT1继续3V供电，由1N4148阻挡，3V电压无法进入外部电路。 单片机通电之后，VCC3.3处电压大于VBTA（VBAT）处3V电压，还可以给电池缓慢充电。 RTC Init Pasted image 20250722094702 RTC也是有中断的，用上RTC之后记得改优先级。 Pasted image 20250722094744 中断服务函数，主要用的是秒钟中断，每秒进一次中断，更新时间。 闹钟中断，很不常用. Pasted image 20250722094856 设置首次RTC上电的初始时间。设定这个时间之后，除非断开RTC的电源，否则不管是改函数日期重新烧录、按复位、强制死机等操作都不能改变这个时间，RTC是完全单独运行的，VB口只要还有电，就会一分一秒的计时。 八、ADC模数转换 ADC是模拟到数字转换器（Analog to Digital Converter）缩写，主要用于将连续传输的模拟信号转换为数字信号。 模拟信号转数字信号，意味着这个功能将模电与单片机整个连成了一体 实验现象： 光感模块会返回一个连续变化的模拟电压值（0 3.3V）表示光强，光强越强电压越高，最高3.3V，最小0。 STM32会通过ADC外设检测这个模拟电压值的大小，并通过串口画出adc的获取数值 Pasted image 20250722100238 stm32的adc精度是12位的，也就是ADC的具体功能就是将 0 3.3的模拟电压大小转化成12位的无符号变量里表示 ，比如模拟值最小是0V，数字值就表示成000000000000（12位），最大值是3.3V，数字值是111111111111。所以采集精度就是： 2 12 3.3 − 0 ​ 4096 3.3 ​ ≈ 0.0008 对于ADC存储的12位变量来说，没有u12这种数据存储结构，只有u16，那就用u16类型的变量来存储ADC的转换值 STM32F1有两个adc（adc1和adc2），每个adc又有18个通道： Pasted image 20250722100417 每个通道都可以独立检测 一个独立电路 的 模拟电压的值。所以ADC的资源非常充足，基本用不完。 本次实验使能ADC1的通道1和通道4，通道1负责测光敏传感器值，通道4什么都不测，作为多通道示范。 main函数： Pasted image 20250722100624 ADC初始化函数Adc Init之后就可以用Get Adc函数来获得adc转换值了，变量a和b分别赋予adc的通道1和通道4的值，然后输出来， 细节a和b数据类型都是u16 Adc Init： Pasted image 20250722100922 ADC InitTypeDef ADC InitStructure; 初始化一个ADC配置的结构体变量。 RCC ADCCLKConfig(RCC PCLK2 Div6); ADC也有自己的时钟，这句代码配置adc的采样时钟，括号里是分频因子，stm32的adc的采样频率不能高于14Mhz，所以分频因子选6分频（div6），也就是72MHZ/6 12MHZ 模拟输入 ，8种GPIO功能的最后一种， 模拟输入也算是adc功能的专属GPIO功能。 然后ADC DeInit(ADC1); 把外设ADC1的一切配置全部恢复默认值， 前面的RCC ADCCLKConfig(RCC PCLK2 Div6);不算，因为这是RCC外设里的配置，不是ADC外设的配置 。 程序后半段： Pasted image 20250722101352 第一个ADC Mode，一般测量模拟值都是选用 独立模式 ，使用 规则通道 进行采样 第二个参数ADC ScanConvMode，是否启用 扫描模式 ， 扫描模式的作用是一旦触发了某一个通道adc采集，那该通道后面的一系列使能的通道都会被采集一遍。 这里不需要，不启用，想采集哪个通道就单独给指令，不然会混乱，使用Get Adc函数直接获取某个通道的值即可。 第三个参数ADC ContinuousConvMode，是否连续转换，连续转换的意思就是一旦触发了转换就会以设定的adc采样频率一直不停转换， 不需要 ，还是给 Get Adc函数单独单次转换 。 第四个参数ADC ExternalTrigConv，adc采样由什么触发，这里使用Get Adc函数触发，也就是软件触发， 软件触发的意思就是使用Get Adc函数触发 第五个参数ADC DataAlign，选右对齐，数据右对齐的意思就是： 一个12位的数据存在一个16位的变量里，靠右存储。靠右可以直接像读取int整数一样读取数据，不需要做 的位变换操作。 第六个参数ADC NbrOfChannel规则转换的adc通道数，NbrOfChannel就是 “number of channel” 的缩写，用到PA1和PA4总共两个通道，填2。 Pasted image 20250722101851 这里是如果启用了扫描模式，填写转换的顺序，通道1第一个，通道4第二个，这里没使用扫描模式，但还是留着这两句作为示范。 adc的实际使用，使用Get Adc(通道数)函数即可 Pasted image 20250722102015 串口发回来的图中，绿色是PA1，接的光强传感器，PA4是橙色什么都没接，为什么橙色也会有点绿色波形的趋势？ 因为使用的是同一个ADC外设，也就是ADC1，，同一个ADC内的不同通道之间会有一点点干扰耦合，比如各通道之间有很大的上/下拉电阻来互相隔开信号（猜测）。但是由于PA4什么也没接，所以其他通道的信号会通过大阻值的上/下拉电阻，使得PA4也有微弱的变化，但是这并不影响采集的信号。 与ADC非常类似的外设是DAC，数字值转模拟信号，是很类似的，但是STM32F103C8T6不带DAC外设 九、输入捕获 使用输入捕获功能，实时测量一个占空比为50%的PWM波的频率 输入捕获功能是STM32定时器的一个功能，并不是一个单独的外设，而是TIM外设的一部分（比如PWM输出也是定时器的一个功能，输入捕获功能与PWM输出功能的地位相同，附属于定时器） 输入捕获功能的作用： 捕获数字电路电平转换时的 上升沿或下降沿，并保存上升/下降沿来到的时刻 输入捕获功能只是采集到下降沿和上升沿，并保存它们到来的时刻，而要采集一个50%占空比波的频率，算法代码，频率计算的代码就要额外自己写。 输入捕获原理： 定时器按照固定频率计数； 当上升/下降沿来的时候，会进入 定时器的捕获中断 （输入捕获通道每个定时器有四个，而且跟PWM四个通道的IO口是一样的，比如TIM1的四个PWM通道是PA8 PA9 PA10 PA11，那TIM1输入捕获的通道也是这四个口）， 同时这个时刻的CNT值会被保存到对应的TIMX CCRX寄存器 （TIM1 CCR1 – TIM1 CCR4）。 计算频率的方法： Pasted image 20250723124539 先设置为捕获上升沿 上升沿捕获到的瞬间记录当前时刻，进入定时器的捕获中断 在中断中修改：下次捕获的是下降沿 采集到下降沿后，记录当前时刻 两次时刻相减，就是PWM高电平持续的时间，因为PWM占空比已知，是50%，持续时间乘2就是PWM周期，知道了周期也知道了频率 main函数： Pasted image 20250723124930 TIM3用来产生待测PWM，TIM3的配置就是驱动舵机的配置，PWM的周期是20ms，频率是50Hz。 TIM2是输入捕获用的定时器。 注意TIM2的arr，0XFFFF，换算成十进制是65536，最大的自动重装载值，可以尽量少进入定时器更新中断； 因为更新中断对于输入捕获功能毫无意义，输入捕获有自己的捕获中断。 因为在输入捕获功能中，需要定时器进行的核心功能是计数，而不是重装载/进更新中断等 calculate fre函数是频率计算函数 capture.c： Pasted image 20250723125933 TIM2初始化函数，PA0是TIM3 CH1。 IC结构体声明（在编码器那边出现过）十二、电机编码器测速 后半段： Pasted image 20250723130334 第一个参数TIM Channel，选择输入通道一（IC1），也就是TIM Channel 1; 第二个参数最重要，TIM ICPolarity，选择什么沿的时候捕获： Pasted image 20250723130450 一共有上升沿、下降沿、上升下降都捕获三种； 第三个参数TIM ICSelection是内核的东西，在这里还不用深入理解，它的作用是ICX映射到相应的TIX上去，按照这么写即可。 第四个参数一般就选TIM ICPSC DIV1不分频，检测频率就靠最初定时器设置的arr和psc来定即可。 第五个参数TIM ICFilter，当检测的波形有较大噪声时可以进行自动滤波，其取值： Pasted image 20250723130602 0X00 0X0F，取值越大滤波效果越好，但是支持的被采样波形频率会越低。 往下，中断这部分只需要看看定时器2使能了什么中断，定时器2的中断服务函数现在管着两个中断了， Pasted image 20250723130740 这里跟外部中断服务函数的机制很像，好几个中断共用一个中断服务函数，定时器更新中断和定时器通道1的输入捕获中断共用定时器2的中断服务函数。 TIM IT CC1是定时器的捕获中断，一个定时器有4个这种中断，四个通道，一个通道一个中断。 Pasted image 20250723131025 TIM2 IRQHandler函数： Pasted image 20250723131332 首先函数前面定义了两个全局变量，用来记录捕获信息。 Pasted image 20250723131113 进行捕获分析的前提是： 看第一个大if：u8类型的变量TIM2CH1 CAPTURE STA的最高位是0。所以这里跟串口那个STA变量一样，最高位置1表示捕获完成。 先看第一个if判断：这是定时器更新中断。 Pasted image 20250723131508 但现在TIM2CH1 CAPTURE STA还是0，不符合里面这个if的要求，所以直接跳出来。 第二个大if：发生了捕获中断，意味着捕获到沿了，最初在初始化函数里设置的是捕获上升沿，所以目前一旦进了这个中断，就代表捕获到上升沿了： if(TIM2CH1 CAPTURE STA&0X40) ：意思是判断TIM2CH1 CAPTURE STA变量的次高位是不是1，结合注释知道，检测到下降沿的时候次高位才会置1。 但现在TIM2CH1 CAPTURE STA还是0，不满足这条，所以进后面的else。 else里，先清空两个全局变量，清空TIM2 CNT，然后设置TIM2CH1 CAPTURE STA变量为0X40，也就是次高位置1，然后用库函数TIM OC1PolarityConfig，这个函数的作用就是设置初始化那个IC结构体的第二个参数，选择什么沿触发中断：设置为下降沿触发 然后if判断结束，定时器从这里开始正式开始对被测PWM高电平时长计时。 CNT值是0 65536，到了65536会自动重装载变为0，CNT计的数就被清除了，所以要在定时器更新中断里也做些手脚。 Pasted image 20250723132103 这段说明我们所测的PWM高电平的最长时间必须在0X3F个TIM3的周期之内， 如果要检测一个很长时间的高电平，可以把STA变量改成u16 所以在已经检测到高电平的正常条件下，会进else，每次定时器更新中断给STA变量+1，表示这个高电平又经历了一整个TIM3周期的时间； Pasted image 20250723132417 下降沿来了 给STA变量最高位置1，同时给VAL变量赋值，TIM GetCapture1(TIM2)函数就是得到TIM2 CCR1的值，这个函数与TIM SetCompare1(TIM2)非常像，这个是设置TIM2 CCR1的值，也就是设置PWM的占空比的那个函数 计算频率的函数： Pasted image 20250723132517 当STA变量最高位是1，就代表完整捕获完成了一次，temp就是总的高电平时间， N是更新中断的次数，CCR就是TIM2CH1 CAPTURE VAL的值，也就是捕获到下降沿的瞬间CNT的值。计算频率就很好理解了。 十、hal库与cubemx 不再使用标准库，而是使用hal库与keil＋cubemx 安装cubemx....... Pasted image 20250723154442 可以直接在这里配置参数一键生成，不需要再手动写外设的配置代码 时钟树： Pasted image 20250723154631 四种时钟源表格 缩写 全称 中文名 来源 特点 HSE High Speed External Clock 高速外部时钟 外部晶振 高精度，外接晶振，作为主时钟源 HSI High Speed Internal Clock 高速内部时钟 内部RC振荡器 无需外接晶振，但精度较低，常用于系统启动或备用时钟源 LSE Low Speed External Clock 低速外部时钟 外部晶振 高精度低速时钟，专用于RTC（实时时钟）和低功耗模式 LSI Low Speed Internal Clock 低速内部时钟 内部RC振荡器 低成本低速时钟，用于独立看门狗（IWDG）或RTC备用时钟 HSE， 外部晶振会提供更加精确的频率，所以要用HSE生成主频，并且用的HSE频率是8Mhz。 HSI，HSI也能达到8Mhz，但是没HSE那么精确，所以HSI一般是HSE出现问题时的备用选项 并且在单片机刚刚上电，系统刚刚启动时，HSE还不稳定，所以会使用HSI启动，再转为HSE。 LSE：低速外部时钟，专门给RTC时钟用的，精准计时。32.768KHz的LSE一般是下面这种封装： Pasted image 20250723155025 LSI：低速内部时钟，40KHz，也挺常用的，比如独立看门狗 实现hal库＋cubemx的跑马灯 Pasted image 20250723155136 要配置的第一个外设：RCC：Reset and Clock Control (复位和时钟控制) 上图的HSE和LSE选项： BYPASS Clock Source 有源晶振 Crystal/Ceramic Resonator 无源晶振 C8T6最小系统板用的是无源晶振，无源的意思是无需VCC输入，因为天然水晶（crystal）的振频就是8Mhz，所以由晶体制作而成的这种晶振不需要电源输入，也就称为无源晶振。 选择无源晶振 Master clock output简称MCO，在时钟树页面也能找到 MCO主要用于将主时钟从IO引脚输出，stm32自身作为时钟源向外输出，一般用不到，不打√。 配置时钟树： Pasted image 20250723155526 分析：首先HSE主频8M，传到PLL（锁相环），并由锁相环进行9倍频，8x9 72Mhz，最终需要的频率就出来了 2025.7.23：现在只知道PLL是个能稳住频率、加倍频率的模块 72Mhz通过了SYSCLK时钟，经过了AHB总线，AHB Prescaler为/1，代表1分频也就是72/1 72，还是72Mhz不变，继续往后传，到了HCLK时钟，然后分叉成好多，其中有熟悉的APB1和APB2总线： Pasted image 20250723161242 画黄线的tim1 72Mhz挂载在APB2总线，TIM2 TIM3 TIM4都挂载在APB1总线。APB1总线的主频是36Mhz，但是有一个X2的二倍频，所以虽然是挂载在了低速总线，TIM2 TIM4的主频也能跟TIM1一样是72Mhz。 Pasted image 20250723164046 这条分支，to cortex system timer；在看一些嵌入式八股文的时候得知：stm32f1系列的内核是： cortex M3内核，这里给了内核的一个系统定时器。 systick定时器 ARM Cortex M3 内核的处理器内部包含了一个 SysTick 定时器，它是一个24 位的倒计数定时器。 计数到 0 时systick就会从 LOAD 寄存器中自动重装载定时初值。只要不把 CTRL 寄存器中的 ENABLE 位清 0，systick定时器就永不停息。 Pasted image 20250723164331 delay ms和delay us延时就是systick定时器实现的 systick是在内核文件：core cm3.h里定义的： Pasted image 20250723164657 cm3就是内核名称：cortex M3的缩写。 继续配置： 第二个配置项：System core里的SYS，也就是系统 Pasted image 20250723165218 Debug展开： Pasted image 20250723165653 Serial Wire（也就是SW），还有JTAG模式，还是jlink当然跟以前一样选SW模式。 System Wake Up，STM32的低功耗唤醒功能，用不到低功耗，不打√。 基础时基源 Pasted image 20250723165838 cubemx支持的hal库也有自己的延时函数：HAL Delay函数，要给这个函数等其他hal基础函数选择时钟源，也选Systick，跟之前标准库一样。 配置完成： Pasted image 20250723170506 这些系统引脚，什么晶振接口、Jlink接口都自动显示绿色，即配置完成了。 新建所有功能分三步：配置SYS、RCC以及时钟树 跑马灯： Pasted image 20250723170708 Pasted image 20250723170740 下面的参数： GPIO output level 初始电平是1还是0，对于跑马灯无所谓。 GPIO mode 普通输出模式就只有推挽和开漏两种选择，选推挽Output push pull GPIO Pull up/Pull down 上拉还是下拉，对于输出模式来说无所谓。 Maximum output speed 最大输出速度，不需要低功耗，选尽可能最高的。 User Label 给这个引脚起个别名，起不起无所谓，不起了 然后生成工程代码 注意文件生成的目标路径不能有中文！ Pasted image 20250723171929 Driver文件夹里放了所有的hal库的外设库。 MDK ARM文件夹放了keil的入口以及启动文件。 Core文件夹放了自动生成的各外设代码和main.c。 main函数： Pasted image 20250724003231 main函数里很多此类格式的注释： C / USER CODE BEGIN XXX / / USER CODE END XXX / main函数是由cubemx自动生成的，如果想修改main函数代码，只能写在这个注释划定的范围内，否则代码会被下次的cubemx更新刷掉。 C / USER CODE BEGIN / 写代码处 / USER CODE END / Pasted image 20250724003435 main函数跟之前标准库的没区别，HAL Init函数和SystemClock Config函数是根据在cubemx里的配置自动生成的，MX GPIO Init也是自动生成，不需要点进去看代码，所有配置如果想改，再回到cubemx去图形化修改，然后再点一次generate code，keil会自动会修改 将while添加上： C while (1) { / USER CODE END WHILE / / USER CODE BEGIN 3 / HAL GPIO WritePin(GPIOC, GPIO PIN 13, GPIO PIN RESET); HAL Delay(800); HAL GPIO WritePin(GPIOC, GPIO PIN 13, GPIO PIN SET);//设置为高电平 HAL Delay(800); } / USER CODE END 3 / 即可实现跑马灯 十一、极速上手hal库 基于STM32F407VET6 （1）GPIO输出 和led的代码一样 C while (1) { / USER CODE END WHILE / / USER CODE BEGIN 3 / HAL GPIO WritePin(GPIOA, GPIO PIN 8, GPIO PIN RESET); HAL Delay(800); HAL GPIO WritePin(GPIOA, GPIO PIN 8, GPIO PIN SET);//设置为高电平 HAL Delay(800); } / USER CODE END 3 / （2）通用定时器PWM输出 Pasted image 20250724092927 然后右侧的参数，红框中是重要的参数： Clock Source 定时器的时钟来源，很少改，都是选Internal Clock也就是内部时钟 Channel 1 4 定时器的输出通道（对应标准库的OC） 下方的参数配置： Pasted image 20250724093324 PSC 预分频系数，用法完全与标准库一致 Counter Mode 计数器（CNT）模式，向上计数（up）还是向下计数（down），以及这里面还有中心对齐模式（center aligned mode） ARR（Counter Period） 自动重装载值，用法完全与标准库一致 Auto reload preload 是否自动重装载，也就是计数器溢出了要不要重新装载复位CNT 再往后的参数是PWM四个通道的配置节选： Pasted image 20250724093654 四个通道完全配置成一样的即可： Mode PWM模式，模式1模式2 Pulse 初始CCR，初始占空比，一般取0 Output compare preload 如果enable，任何写入TIMx CCRx事件到来，都不会打断当前计数周期，只能等到计完数了(更新事件)，才把TIMx CCRx传送至当前计数寄存器。 Fast mode 快速模式，不需要低功耗，追求高性能，无脑勾上就完事了 CH Polarity 输出极性，高or低 生成代码后可以看到： Pasted image 20250724094240 自动生成了关于TIM4的初始化代码 但现在还没有生成出PWM，因为定时器通道还没有开启，需要手动加一句代码开启 Pasted image 20250724094345 C / USER CODE BEGIN 2 / HAL TIM PWM Start(&htim4,TIM CHANNEL 1); HAL TIM PWM Start(&htim4,TIM CHANNEL 2); HAL TIM PWM Start(&htim4,TIM CHANNEL 3); HAL TIM PWM Start(&htim4,TIM CHANNEL 4); TIM4 CCR1 5000; TIM4 CCR2 10000; TIM4 CCR3 15000; TIM4 CCR4 19000; / USER CODE END 2 / 上面这四句代码不能自动生成，需要手动开启定时器的指定通道，代码才能正常工作。 （3）编码器模式 Pasted image 20250724095306 参数配置： Pasted image 20250724095327 重要步骤 ：编码器模式用到了定时器4更新中断，凡是用到中断，马上联想到中断优先级，就要联想到NVIC： Pasted image 20250724095732 interrupt，中断， 简称it 然后在NVIC外设的配置页也要看一眼：确保TIM4的中断打勾了： Pasted image 20250724095819 PWM一样，编码器模式不会自动开启，定时器中断也不会自动开启，需要写入代码开启： Pasted image 20250724101020 IT就是interrupt，中断的缩写 定时器4的中断服务函数: Pasted image 20250724101327 HAL TIM PeriodElapsedCallback这个函数名：Period Elapsed Callback直译过来就是周期过完进行的回调函数，也就是定时器更新中断 weak是什么意思？ 在 C 语言中， weak 用于声明一个 弱符号（Weak Symbol） 。 弱符号的特性是：如果代码中存在同名的强符号（即普通定义），编译器会优先使用强符号，弱符号会被自动覆盖。 这使得它可以用来提供默认实现，同时允许用户自定义覆盖。 也就是可以在main.c里重写这个HAL TIM PeriodElapsedCallback函数，然后编译器就会只关注我写的这个： 复制一个该函数放到main.c里去，同时加上编码器在CNT溢出处的计数该有的处理代码，与标准库一模一样： Pasted image 20250724101543 （4）sys.c文件（待命中） 2025.7.24 （5）定时器中断 Pasted image 20250724171504 Pasted image 20250724171515 Pasted image 20250724171528 Pasted image 20250724171538 Pasted image 20250724171554 （6）高级定时器 互补PWM输出 Pasted image 20250725000914 互补PWM主要用于控制半桥电路，（栅极驱动器），而半桥电路是非常常用的执行器驱动电路。 互补PWM只有高级定时器才能输出，STM32F4的TIM1和TIM8是高级定时器，STM32F103C8T6只有TIM1是高级。 Pasted image 20250725001021 channel选项： CH1是通道1，而CH1N是互补通道1。 Pasted image 20250725001159 center aligned mode1：中心对齐模式 Pasted image 20250725001233 通过arr和psc计算一下pwm频率: 1/ ( 168000000/5 ) ∗ 1680 0.00005 s 也就是20khz。 但实际上： Pasted image 20250725001414 只有10khz,因为： 中心对齐模式的计数方式是先向上计数，CNT ARR之后再向下计数，这样CNT的值就会 从0 ARR 0，两次经过CCRx，所以频率也会降为原来的1/2。 PWM通道配置跟普通定时器的一样，互补通道配置默认即可，生成代码： Pasted image 20250725003219 C / USER CODE BEGIN 2 / HAL TIM PWM Start(&htim1,TIM CHANNEL 1); HAL TIMEx PWMN Start(&htim1,TIM CHANNEL 1); HAL TIM PWM Start(&htim1,TIM CHANNEL 2); HAL TIMEx PWMN Start(&htim1,TIM CHANNEL 2); HAL TIM PWM Start(&htim1,TIM CHANNEL 3); HAL TIMEx PWMN Start(&htim1,TIM CHANNEL 3); TIM1 CCR1 280; TIM1 CCR2 560; TIM1 CCR3 1280; / USER CODE END 2 / （7）uart串口 Mode选 Asynchronous，异步通信，uart： Pasted image 20250725094452 asynchronous是异步的意思，就是用uart。 cubemx里的参数只改波特率，其他全默认。 NVIC打开串口全局中断，全局中断就是包含了接收、发送等中断： 跟定时器中断一样，串口中断也需要自己写接收回调函数，并且需要自己写代码去开启串口中断 把 通过“\\r\\n”作为结束符判断 的串口中断移植过去： 先在usart.c开头声明： C uint8 t USART3 RX BUF[100]; //串口接受数据的数据缓冲区 uint8 t Res; uint16 t USART3 RX STA 0; //串口接收到数据标志位 然后在usart.c添加回调函数： C void HAL UART RxCpltCallback(UART HandleTypeDef huart) //串口数据接收完成回调函数 { if(huart Instance USART3) { if((USART3 RX STA&0x8000) 0)//接收未完成 { if(USART3 RX STA&0x4000)//接收到了0x0d { if(Res! 0x0a)USART3 RX STA 0;//接收错误,重新开始计数 else { USART3 RX STA 0x8000; //接收完成 //memset(USART3 RX BUF,0,sizeof(USART3 RX BUF)); //USART3 RX STA 0; } } else //还没收到0X0D { if(Res 0x0d)USART3 RX STA 0x4000; else { USART3 RX BUF[USART3 RX STA&0X3FFF] Res; USART3 RX STA++; if(USART3 RX STA (100 1))USART3 RX STA 0;//接收数据错误,重新开始接收 } } } HAL UART Receive IT(&huart3,&Res,1); } } 这个回调函数是所有串口共用的接收中断的回调函数 C++ HAL UART Receive IT(&huartX, &Res, 1); //huartX的X需要自行改成1/2/3 HAL UART Receive IT这一句是用来“装载”接收中断的： Pasted image 20250725095707 参数的中文含义： 串口的接收中断函数(串口2，待接收数据的存放处，要接收的数据长度); 也就是这个函数是预备接收的存放目的数组和数据的长度，只有声明了才会开始接收数据。 串口中断优先级低了可能导致丢包严重。 （8）ADC 八、ADC模数转换 ADC中比较常用的 独立模式 ： ADC（模数转换器）的 独立模式 （Independent Mode）是指ADC模块单独工作，不与其他ADC模块（如果存在多个ADC）协同工作的模式。 三种主要的ADC使用方法： 1. 单通道规则输入 2. 多通道规则输入 3. 注入通道 事件触发 8.1 单通道规则输入 单通道的ADC1配置方式如下： 通道只选一个：ADC1通道3，也就是PA3。 Pasted image 20250725101057 配置参数： Pasted image 20250725101111 首先Mode都选 independent mode也就是独立模式。 resolution是分辨率，12bit，也就是采样值是0 4095的整数。 data aligned是右对齐 后面四个参数都先不用管，单通道的规则输入这些都不需要：全部disabled Pasted image 20250725101326 Number of Conversion：需要ADC采样的通道数量，单通道采样，选1。 External Trigger Conversion Source：触发方式，选择software（软件触发），Get Adc函数触发 External Trigger Conversion Edge：没有，不用管。 Rank：里面就一个通道，就是channel3。 sampling time：选了480 Cycles 480 Cycles的意思是480个ADC周期采样一次，ADC周期跟之前设置的参数：ADC的分频系数有关 Pasted image 20250725101640 可以分别选取2/4/6/8分频，ADC的主频是来自于PCLK2的，PCLK2在： Pasted image 20250725101705 PCLK2是84Mhz，不需要低功耗，所以ADC就尽可能高频率。 生成代码后自行修改： 首先自定义一个u16变量 adc3 Pasted image 20250725102231 main函数如下： Pasted image 20250725102248 C while (1) { HAL ADC Stop(&hadc1); // 确保ADC停止 HAL ADC Start(&hadc1); if (HAL ADC PollForConversion(&hadc1, 100) HAL OK) { adc3 HAL ADC GetValue(&hadc1); } delay ms(100); / USER CODE END WHILE / / USER CODE BEGIN 3 / } 8.2 多通道规则输入 IN0和IN2两个通道： Pasted image 20250725210118 配置参数： Pasted image 20250725210305 1. 扫描模式 通道数 1时，强制开启 自动扫描开启的所有通道进行按顺序转换，直至所有都转换完 例如开启了CH0、CH1、CH2、CH3这四个通道，启动转换后ADC会自动将这四个通道全部转换完，但是这种连续性是可以被打断的，所以引出了间断模式 2. 连续模式 开启连续模式后，ADC的转换持续不断进行，不由自己控制，会忽略掉软件触发，以及忽略多种触发采样， （连续转换模式开启后其实就是满频率的采样） 3. 间断模式 开启将多个通道进行分组采样。例如开启了CH0 3这四个通道，假如设置了间断次数为4，相当于将4个通道分成了4组，每组1个通道。那么采集完这4个通道需要手动触发4次ADC采集；如果设置了间断次数为2，那么采集完4个通道就需要手动触发2次ADC采集。 4. EOC（end of conversion 转换结束） 可以设置为在转换完一个通道后就产生EOC事件，也可以设置为转换完规则通道组的所有通道之后产生EOC事件。但是规则采样 只有一个转换结果寄存器 ，如果有多个转换通道，设置为转换完规则组的所有通道后产生EOC， 会导致数据溢出 。 一般设置为在转换外一个通道后就产生EOC事件 多通道的rank，就是优先级，rank1的优先级高于rank2，以此类推。 生成代码： 自定义变量 u16 adc[2]; Pasted image 20250725212259 Pasted image 20250725212310 C while (1) { HAL ADC Stop(&hadc1); // 确保ADC停止 HAL ADC Start(&hadc1); for(int i 0;i 注意这里由于开启了扫描模式，HAL ADC Start触发一次采样，会连续把所有通道也就是IN0和IN3两个通道依次采完 由于EOC采样结束标志位设置的是一个通道采样完就来一次，所以需要for循环两次进行两次的接收 8.3 多路注入通道：事件触发ADC 2025.7.25，等学完FOC的电流环再来学这个 （9）GPIO输入与外部中断 Pasted image 20250725215814 引脚输入模式与配置输出模式很像，点击目标引脚，设置为GPIO Input No pull up and no pull down ：浮空输入 Pull up ：上拉输入 Pull down ：下拉输入 判断引脚高低电平的代码： C HAL GPIO ReadPin(GPIOA,GPIO PIN 5); Pasted image 20250725220004 外部中断： 在对应的引脚上选择对应的EXTI即可。 Pasted image 20250725220057 参数配置： Pasted image 20250725220239 选前三个。前三个是“external interrupt”外部中断，后三个“external event”是外部事件，另一个功能 external interrupt mode with rising edge trigger detection external interrupt mode with falling edge trigger detection external interrupt mode with rising/falling edge trigger detection 上升沿触发，下降沿触发，都触发 打开中断： Pasted image 20250725220458 外部中断划分：外部中断0 4独立，外部中断5 9共用一个中断，外部中断10 15共用一个中断。 外部中断的回调函数： C u8 a 0; void HAL GPIO EXTI Callback(uint16 t GPIO Pin) { if(GPIO Pin GPIO PIN 3) { if(HAL GPIO ReadPin(GPIOA,GPIO PIN 3) 1) // 上升沿 { HAL GPIO TogglePin(GPIOC,GPIO PIN 13); } } if(GPIO Pin GPIO PIN 5) { if(HAL GPIO ReadPin(GPIOA,GPIO PIN 5) 1) // 上升沿 { HAL GPIO TogglePin(GPIOC,GPIO PIN 13); } } if(GPIO Pin GPIO PIN 12) { if(HAL GPIO ReadPin(GPIOB,GPIO PIN 12) 1) // 上升沿 { HAL GPIO TogglePin(GPIOC,GPIO PIN 13); } } } hal库的优点是把所有的外部中断处理全部集成在了同一个回调函数里，这种处理方式也比标准库更方便。 但是要注意！不能在回调函数里使用任何延时，以及不能代码用时过长，否则会卡死！这个不跟标准库一样！ （10）I2C通信 2025.7.25 待命 （11）独立看门狗 2025.7.25 待命 （12）RTC时钟 2025.7.25 待命 （13）输入捕获 2025.7.25 待命 十二、更标准高效的串口全双工通信 更标准高效的串口全双工通信，这里的“更标准”指的是三个方面： 1. 运行效率更高，降低CPU占用率 2. 更适用于机器人参数传递 3. 实现数据的全双工收发 之前使用“\\r\\n结尾的”的方法收发字符串，但如果真正在机器人通信上用起来会 非常别扭 ： 比如想发送两个浮点数：211.5625和934.2321，精确到小数点后四位 照以前应该是： C++ float a 211.5625; float b 934.2321; sprintf(buf, \"%4f, %4f\\r\\n\", a, b); printf(buf); 假如把这段字符串发给另一个stm32，那另一边的数据处理可麻烦大了，又是字符串通过\",\"分割开，又要把每个浮点数都割开分别存放在不同字符串里，又要atof再转化为浮点数，头都大了。 所以机器人参数通信采用的算法是： 由于串口的发送基本单元是 字节 ，所以就要想办法把需要传递的参数蕴含的信息映射到字节里去，比如float类型就占四个字节，所以我们只需要发送四个字节即可完整传递一个浮点数的信息，不管它有小数点后多少位。以下是新老两种方法的比较： 老方法 字符串格式：\"123456.654321\\r\\n\"，一个浮点数花十五个字节存。 新方法 16进制格式：float u32，u32的数据分存在四个u8里。这样只需要四个字节。 第一步先试用cubemx配置uart1 cubemx部分配置与极速上手hal库那一节中的串口小节配置完全一样， 只是usart.c里的接收中断回调函数不要像以前那么写了，相关的变量也先不要声明。 黄色高亮 为非自动生成的，自己手动写的函数： 修改后的usart.c函数： C / Includes / include \"usart.h\" / USER CODE BEGIN 0 / include \"string.h\" define RX DATA LEN 3 // 根据实际定长数据长度修改 volatile uint8 t tx busy 0; // 发送状态标志 uint8 t rx buffer[RX DATA LEN]; // 定长接收缓冲区 / USER CODE END 0 / UART HandleTypeDef huart1; / USART1 init function / void MX USART1 UART Init(void) { / USER CODE BEGIN USART1 Init 0 / / USER CODE END USART1 Init 0 / / USER CODE BEGIN USART1 Init 1 / / USER CODE END USART1 Init 1 / huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART WORDLENGTH 8B; huart1.Init.StopBits UART STOPBITS 1; huart1.Init.Parity UART PARITY NONE; huart1.Init.Mode UART MODE TX RX; huart1.Init.HwFlowCtl UART HWCONTROL NONE; huart1.Init.OverSampling UART OVERSAMPLING 16; if (HAL UART Init(&huart1) ! HAL OK) { Error Handler(); } / USER CODE BEGIN USART1 Init 2 / // 启动接收：一次性接收完整数据包 HAL UART Receive IT(&huart1, rx buffer, RX DATA LEN); // 优化点：接收定长数据 / USER CODE END USART1 Init 2 / } void HAL UART MspInit(UART HandleTypeDef uartHandle) { GPIO InitTypeDef GPIO InitStruct {0}; if(uartHandle Instance USART1) { / USER CODE BEGIN USART1 MspInit 0 / / USER CODE END USART1 MspInit 0 / / USART1 clock enable / HAL RCC USART1 CLK ENABLE(); HAL RCC GPIOA CLK ENABLE(); / USART1 GPIO Configuration PA9 USART1 TX PA10 USART1 RX / GPIO InitStruct.Pin GPIO PIN 9; GPIO InitStruct.Mode GPIO MODE AF PP; GPIO InitStruct.Speed GPIO SPEED FREQ HIGH; HAL GPIO Init(GPIOA, &GPIO InitStruct); GPIO InitStruct.Pin GPIO PIN 10; GPIO InitStruct.Mode GPIO MODE INPUT; GPIO InitStruct.Pull GPIO NOPULL; HAL GPIO Init(GPIOA, &GPIO InitStruct); / USART1 interrupt Init / HAL NVIC SetPriority(USART1 IRQn, 0, 0); HAL NVIC EnableIRQ(USART1 IRQn); / USER CODE BEGIN USART1 MspInit 1 / / USER CODE END USART1 MspInit 1 / } } void HAL UART MspDeInit(UART HandleTypeDef uartHandle) { if(uartHandle Instance USART1) { / USER CODE BEGIN USART1 MspDeInit 0 / / USER CODE END USART1 MspDeInit 0 / / Peripheral clock disable / HAL RCC USART1 CLK DISABLE(); / USART1 GPIO Configuration PA9 USART1 TX PA10 USART1 RX / HAL GPIO DeInit(GPIOA, GPIO PIN 9 GPIO PIN 10); / USART1 interrupt Deinit / HAL NVIC DisableIRQ(USART1 IRQn); / USER CODE BEGIN USART1 MspDeInit 1 / / USER CODE END USART1 MspDeInit 1 / } } / USER CODE BEGIN 1 / // 发送函数（带冲突保护） void UART Send(uint8 t data, uint16 t size) { while(tx busy); // 等待前次发送完成（简单忙等待） tx busy 1; HAL UART Transmit IT(&huart1, data, size); } // 发送完成中断回调 void HAL UART TxCpltCallback(UART HandleTypeDef huart) { if(huart Instance USART1){ tx busy 0; // 清除发送忙标志 } } uint8 t wave; // 假设这是我们需要的数据，数据值是0 255 void ProcessData(uint8 t rx buf) { if(rx buf[0] 0xaa && rx buf[1] 0x55) { wave rx buf[2]; } else { return; } } // 接收完成中断回调 void HAL UART RxCpltCallback(UART HandleTypeDef huart) { if(huart Instance USART1){ // 处理完整数据包 ProcessData(rx buffer); // 用户自定义数据处理函数 // 重新启动接收（保持连续接收） HAL UART Receive IT(&huart1, rx buffer, RX DATA LEN); } } void HAL UART ErrorCallback(UART HandleTypeDef huart) { if (huart Instance USART1) { HAL UART Receive IT(&huart1, rx buffer, RX DATA LEN); // 重新启动接收 } } / USER CODE END 1 / usart.h函数： c ifndef USART H define USART H ifdef cplusplus extern \"C\" { endif / Includes / include \"main.h\" / USER CODE BEGIN Includes / / USER CODE END Includes / extern UART HandleTypeDef huart1; / USER CODE BEGIN Private defines / / USER CODE END Private defines / void MX USART1 UART Init(void); / USER CODE BEGIN Prototypes / void UART Send(uint8 t data, uint16 t size); / USER CODE END Prototypes / ifdef cplusplus } endif endif / USART H / main函数： C++ while (1) { / USER CODE END WHILE / / USER CODE BEGIN 3 / UART Send((uint8 t )\"ASDSADS\", 7); HAL Delay(10); } 代码分析： / include \"string.h\"，这个库用于引入memcpy函数。 HAL UART Receive IT,一定要加在串口初始化最后，千万别加错了地方或者忘了，开启接收中断。 三个串口中断回调函数： 1. HAL UART TxCpltCallback —— 发送完成回调 2. HAL UART RxCpltCallback —— 接收完成回调 3. HAL UART ErrorCallback —— 串口发生错误回调","summary":"STM32 寄存器、串口、总线与常用外设学习笔记。"},{"permalink":"https://zhr0529.cn/posts/硬件基本功-电路设计及制造/","title":"硬件基本功：电路设计及制造","content":"电路基础概念 数字电路和模拟电路 屏幕截图 2025 07 01 113621 数字电路：0101010101，主要用于各种数字通信协议。 模拟电路：0.1V，1.7V，主要用于模拟信号放大电路、声音信号、连续变化的信号的处理、滤波电路等。 数字电路只有0和1的概念，并没有具体的多少伏电压，多少安的电流的概念，就只有一个参考电压，比如高电平是3.3V，低电平是0V，电路就会以很快的速度在3.3V和0之间跳变，没有中间状态，是离散的。 比如1.8V，在0V为0、3.3V为1的数字电路中就没有任何意义，1.8V代表0还是1？属于浮空，不确定电平。 而模拟电路则是具体的电压和电流，比如将0.6V±0.1V的正弦电压信号通过运放放大5倍，变成3V±0.5V的正弦电压信号；再比如我将声音信号通过三极管放大电路放大；再比如测量流过电机的电流在 2A到2A之间连续变化，这些电信号是有具体的电压/电流数值的，是连续的。 VCC和GND VCC(Volt Current Condenser)，就是电源电压、供电电压的意思。 GND(ground)就是 地 。 一块电池接入电路板，电池正极电压就是VCC，电池负极电压就是GND。 共地 在电路中，要判断一个电平信号的高低，就需要一个标准来判断，这个判断标准就是0电平(也叫地），要把同一个电源供电的所有芯片、模块电路的GND全部连在一起，这就是共地。 为什么要共地呢？ 电池类型不同和电池化学反应等的差异都会导致电池负极的 绝对 电平不同，电池只管产生稳定的 相对 的电势差，并不能保证不同电池负极的 绝对 电平一样。 所以我们拿一根线把不同电池的GND连起来，就可以保证负极电平一样了。 任何功能+未共地 玄学现象。 例1：两个未共地的模块IIC通信，程序一切正常，串口却收到一串乱码。 例2：电机上层板发PWM控制下层驱动板驱动电机，电机毫无逻辑不受控制。 因为负极参考电平都不一样， 就比如有a，b两套彼此独立的电路，a电路的0V是b电路的2.765V ，当然出问题。 什么时候可以不共地？ 当两个电路之间的信号传递不使用电信号时，可以不共地。比如后面讲到的光耦，把电信号转换成光信号通信，两部分电路就不需要共地。 LDO低压差线性稳压器 屏幕截图 2025 07 01 114410 降压电路的核心是AMS1117 3.3，把外部输入的5V电压降压成3.3V。 AMS1117芯片的本质就是一块LDO（低压差线性稳压器），简称线性稳压器。 用一个极端简化的理解：ldo内部可以理解作一个自动调节的滑动变阻器，负载端电压大了就增大内阻；负载端电压小了就减小内阻。 用最简单的原理： 不断调节电阻分压来实现降压稳压的目的。 易得LDO的工作效率：屏幕截图 2025 07 01 114620 输入输出压差越大，效率越低，发热越大；所以由此可见ldo的工作效率其实很低，输入输出的压差很大时，芯片会很热，实例：AMS1117 5.0芯片，把12V降压成5V，显然这样效率已经低于50%了，电路确实正常工作，但是AMS1117非常烫。 AMS1117 3.3， 3.3就代表OUT端输出的是3.3V。 AMS1117 5.0，代表降压后OUT端输出5V。 AMS1117有三个端口:IN、OUT、GND。（OUT口有两个引脚） LDO最大的好处就是 输出电压很平稳 ，由于输出端的电压是电阻分压出来的，所以输出电压几乎不会产生什么波动。 综上，所以ldo的作用： 1. 经常用作产生一些信号处理电路的3.3V、5V基准参考电压； 2. 给小功率电路提供电源电压（如stm32主控电路）。 LDO芯片内部电路 Pasted image 20250806153833 TVS二极管（稳压二极管） 稳压二极管最大的作用通俗一点总结就是： 保护电路，牺牲自己。 稳压二极管的伏安特性曲线：Pasted image 20250702103915稳压二极管利用的是二极管的 反向击穿区 ，Uz是反向击穿电压，是定值，比如5V、50V等。u 浪涌电压（突然升高的尖峰电压） 注意是二极管阴极接VCC，阳极接GND，要记清楚稳压二极管应该工作在反向击穿区，画原理图的时候千万不要搞反了。 DC DC开关电源降压稳压电路&滤波电容的原理与使用 这里面最重要的一个元件是LM2596芯片。 LM2596是一款DC DC开关电源芯片，它的作用就是降压。 DC DC开关电源 那么它与LDO降压有什么不同呢？ 不可能一直用L298N芯片内部的5V来给单片机供电，L298N不是专门用来降压的，L298N芯片也很贵。那怎么办呢？现在需要找一个其他的方案代替L298N降压。用什么方式呢？LDO？假如用12V电源5V驱动个大扭矩舵机，那估计带都带不动，而且LDO还会烫得冒烟。（LDO降压效率低，是类似于电阻分压式的降压，所以负载器件的功率如果很大就易发热） 那有没有不用类似电阻分压来降压的方法呢？而且降压效率很高的那种？ DC DC ， 直流电 直流电 ，就是直流电转成直流电。 AC DC就是交流电转直流电（整流器） DC AC就是直流电转交流电（逆变器） DCDC开关电源（以下简称DCDC）与LDO的降压方式并不相同。 DCDC的原理与PWM的原理很类似，降压的DCDC开关电源可以草率理解为：一个带有反馈的，可自动调节的开关。 DCDC输入的是电源的恒定电压（比如锂电池的12V）； 输出的是一种PWM， 一种高电平为电源电压，低电平为0的PWM。 PWM就是由这个“开关”以较高的频率不断开和关产生的。 比如如果电池的12V不做任何处理，那么PWM的占空比是100%：U0代表电源电压，如下图：屏幕截图 2025 07 02 112813 假如想要6V电压，12V降压6V降一半，那么dcdc开关电源的PWM调节就开始起作用了，占空比是50%：代表DCDC的输出电压如下图： 屏幕截图 2025 07 02 112850 上图可以很清楚地看见：占空比是50%的时候，相同时间内u与t围成的面积是占空比100%时候的1/2。 现在我们把高于U0/2的部分“ 砍下来 ”， 填补到电压是0的“坑”里 ： 屏幕截图 2025 07 02 113151 从能量守恒的角度达到了降压的效果，把电源电压U0降压到了U0/2。 但是在实际的电路中，“砍下来”和“填坑”的操作并不会仅靠一个DCDC开关电源就能完成，因为DCDC只能产生“大号PWM”，要么是12V，要么是0V，而我们理想波形需要的是平稳的6V，可以看下图，理想的波形和实际的波形还是有很大差距的：屏幕截图 2025 07 02 113223 为了得到平稳的理想波形，要做的就是 滤波。 屏幕截图 2025 07 02 113620 LM2596芯片的原理图，后缀 5.0V表示固定降压成5V。 LM2596 3.3，就是固定降压成3.3V LM2596 ADJ，就是可调节的降压芯片。 ON/OFF是使能口， 众所周知 ，因为ON上面带横线，所以代表低电平使能，所以直接接地，让芯片一直保持使能状态。 IN接12V，OUT接5V。 FB即feedback（反馈端），接输出电压，把输出的实际电压实时反馈给芯片，然后芯片自动闭环调节（有点类似于PID闭环调节）。 然后我们需要滤波： 提到滤波我们就要想起电容： 电容有 储能 和 滤波 的作用，所以在输出端加上电容： 屏幕截图 2025 07 02 114011 一条非常重要的定理： 电容两端的电压不能突变 《电路》中电容的基础公式：u是电容两端的电压，C是电容值，t是时间，i是流进/出电容的电流。 i C d t d u ​ 把上面的式子做一下变换： C 1 ​ ∫ i dt u 电容两端的电压是由流进/出电容的电流累积出来的，有积分，也就意味着u绝对不可能突变。 正是因为电容两端的电压不能突变，而PWM的一大特点就是电压突变 ，所以如果给PWM加个电容，那PWM就不能突变了，上升沿与下降沿都会从“悬崖”变成“斜坡”。 电容在并联在输出回路里的作用就是实现上面说的“砍掉高的一截”，然后“填坑”；也就是储能，它可以将下图多出的这部分电能储存起来，然后在没有电压的时候放出来，就像初中地理学的湖泊的作用：枯水期补充河流，洪水期储存洪水。 一个合格的降压，不仅要输出稳定的电压，还要输出稳定的电流。 电容虽然限制了电压的波动，但是如果电流没有一个器件限制波动，那电流就会十分任性，在PWM高电平段时，因为此时“开关”是打开的，所以相当于12V的锂电池与后端5V的输出负载实际上是在极短暂的时间内直接导通的，但是电流没有器件能限制，所以电流随便突变，那么电池的电流会以： 12V/RL的极限电流输出给负载，RL是负载的等效电阻。 那么问题简化了：找个器件，能限制电流，让电流不能突变。 电感 电感的特点： 电感两端的电流不能突变 电感是个线圈，高中知识电生磁，众所周知，线圈内流过变化的电流时会产生磁场。 而磁场也是一种能量存储的形式，电感可以把电能转化成磁能。 电感的加入正好抑制了给电容充电的电流的突变，让电流变得平和了：屏幕截图 2025 07 02 115321但是上面这个电路还是有问题：电感在LM2596这个“开关”断开的时候并没有一个完整的放电回路 LM2596开关电源可以看作是一个开关，那么在这个开关断开的时间里， 电感左侧的线路断开了，并不能构成 回路 ，这就导致电感内储存的电能无法释放，电感会让电流持续，如果没有放电回路，电感里储存的那些能量会产生很严重的很高的电压噪声尖峰，基本上会远超于输出额定电压，把流过的元器件都冲爆。 所以需要给电感一个续流的回路，让开关电源断开的时间里使用电感持续输出电流：屏幕截图 2025 07 02 120359上图就是我们巧妙的解决方案，在开关电源断开的时候，用电感回路继续供电。 确定电感、电容和二极管的选型和参数： 对于电容电感的参数确定来说，需要参考对应的DCDC电源芯片的手册，里面都有详细说明；但LM2596芯片过于经典，所以本次可以不看芯片手册，电容容值和电感的感值采用经验值，对其定性分析即可。屏幕截图 2025 07 02 134854上图完成了选型和取值 电容大小采用芯片手册推荐电路里的值：220uF（220微法），微法级别的电容就算容值很大的了，有很强的储能的能力： 这里可以理解为：电容容值越大就等同于湖的深度越深，调蓄洪水的能力就越强。 也就是，面对开关电源产生的“波涛汹涌”的大号PWM，电容容值越大，滤波作用越强。 如果我们要用电容主要来做储能作用，来对较大幅度的电压变化进行滤波，那我们就要选电解电容： 注意！这种电容是有正负极的，接反了会爆炸。 同理，电感感值越大，输出的电流会越平稳，从而输出电压也会越平稳。 手册推荐取33uH（33微亨），给它翻倍取个68uH，让电源稳如老狗。 电容和电感都不能盲目增大，盲目增大电容的话，主要是电容太大了，占空间，再大也没什么用；电感也不能盲目增大，太大的电感会让整个DCDC系统迟滞，造成一些供电问题。 二极管：SS54选型是肖特基二极管，又叫快恢复二极管。 普通的二极管在突然施加反向电压的时刻，会有很大的反向电流： 反向电流存在的这段时间就是 反向恢复时间 ：屏幕截图 2025 07 02 135331对于DCDC电路来说，二极管会遭受不断的、高频的正反向电压，一般的二极管在高频的正反向变化电压下，由于二极管 反向恢复时间 与 开关电源关闭时间 相当，所以二极管就会失去单向导电性，发生危险。 而肖特基二极管就可以避免这个问题，因为肖特基二极管的反向恢复时间非常非常短，只有ns级别。 到这里为止，基础的电路已经搭完了，这个降压稳压电路，我们又叫它： BUCK电路 104电容 这只是电路基础部分，还要加一些器件，让电路更安全。 屏幕截图 2025 07 02 135627104电容的意思： 10 ∗ 1 0 4 pF 100000 pF 100 n F 0.1 μ 一个《电路》里的公式： X c ​ 2 π f c 1 ​ XC是电容的容抗，f是电压的频率，C是容值。 众所周知，电容有 隔直流，通交流 的作用，所以就好理解这个公式了，交流电的频率越高，f越大，XC就越小，所以电容对这种高频电流的阻碍就越小，高频的噪声就从104电容流过而被消除了。 需要注意的就是电容的容量越大，因为制作工艺的原因，其高频特性越差，所以在面对高频电路时，常常需要用小容量的电容，（nf级和pf级） 所以104电容的作用就是 消除高频噪声 。 噪声就是电压曲线波动不平滑产生的毛刺。 PCB设计中，一般把104电容放在靠近芯片的位置，因为芯片（尤其是STM32、信号处理类芯片）是最怕高频噪声的，把104电容放在尽量靠近芯片的地方，被消除了高频噪声的干净电流就可以流入芯片了； 如果104电容离芯片很远，那高频噪声可能在被104电容消除之后再次出现，失去了滤波的效果了。 如下图，C2和C4就是104电容，芯片背面还有两个104，都离芯片非常近。 屏幕截图 2025 07 02 135904 在buck电路里，一大一小两个电容相互配合，一个滤低频，一个滤高频。屏幕截图 2025 07 02 135926 同理，LDO电路里的这些电容的作用是一样的屏幕截图 2025 07 01 114410 1 现在把LDO电路也加到BUCK电路后面：屏幕截图 2025 07 02 140041就得到了一个两级降压的电路，12V 5V 3.3V 最后，在左边也加入滤波电容，给输入的电源电压滤波； 接上锂电池接线口，电池就可以直接接线了。 最右边接个灯，电路工作一切正常的话，最右侧的灯就会正常亮了屏幕截图 2025 07 02 140146 附录： 1f 1000mf 1mf 1000uf 1uf 1000nf 1nf 1000pf TPS5430 屏幕截图 2025 07 02 161210 这里换了另外一款DCDC芯片，TPS5430。 可以发现主回路都有熟悉的电感、肖特基二极管，都是一样的。 只是信号部分比LM2596稍微有点复杂。 其中4号脚VSENSE就是反馈脚，相当于LM2596的FB，这里有些差异，这个TPS5430芯片的输出电压不是固定的，具体输出电压要调节R14和R15两个电阻来实现，TPS5430的芯片手册里： 屏幕截图 2025 07 02 162125 两个电阻值是根据这个公式挑选的。 8号脚PH就是OUT，电压输出。 至于那个BOOT脚先不管就行，芯片手册让怎么接我们就跟着芯片手册学就行了，其他很多芯片也都是这样的，包括比如555定时器有一个脚一般都是固定接一个104电容的，照做即可。 DCDC 还有最后一个重点 那就是PCB布线： 屏幕截图 2025 07 02 162350因为开关电源流过的电流很大，一般用到的有1A 5A，所以走线都要尽量粗，但是过粗的走线也没有意义，一般是像上图一样刚好填满开关电源这一组器件的这一小块领域。 DC DC开关电源升压稳压电路 buck电路学完了，接着学boost电路，怎么把5V升压到12V。 boost升压芯片： MT3608 MT3608芯片手册里的推荐电路图： 屏幕截图 2025 07 04 114723 EN是enable的缩写，高电平使能，低电平休眠。 其中这个FB引脚连接的R1 R2电阻分压电路，通过上一节结尾 TPS5430 的经验， 这两个电阻一猜就是确定输出电压用的，FB肯定也有固定的参考电压。 屏幕截图 2025 07 04 114854 SW就是输出电压。IN就是输入电压。 升压电路的主回路当然就是：5V输入到IN，12V从SW输出 主回路的架构怎么理解？电感和二极管为什么放在这？ 屏幕截图 2025 07 04 115247 SW是一个开关，当SW断开时，左侧的5V可直接流入负载RL输出端 假如此时SW突然闭合： 屏幕截图 2025 07 04 115400 左侧因为 电感两端电流不能突变 ，所以上图标注的电流IL会从0开始逐渐增大 通过电感公式可以定性地知道， 电感两端会产生一定的电压 因为开关闭合，所以开关上端直接与GND相连了，所以该点电压为0。 右侧电路。二极管D的左侧正极是0V，右侧还是5V，所以二极管反向截止。右侧还是保持5V因为 电容两端的电压不能突变 片刻后，当电感电压达到峰值的瞬间，再次断开SW开关： 屏幕截图 2025 07 04 115744 此时电感两端会带有一定的电压UL，那么二极管左侧的总电压是5V+UL，已经大于二极管右侧了，所以电流流入负载端，负载端电压超过5V达到12V 其实与上一节的原理是非常像的。 三个核心元件： 电感、二极管、储能电容，三者缺一不可。 原理了解清楚了回到MT3608上： 屏幕截图 2025 07 04 120018 电路基本就和原理演示图一模一样。 但要注意的是：boost升压电路得到的输出电压驱动能力很弱。 举个例子：假如以5V/20W的功率给自举电路供电，也就是5V/4A，那么由于boost升压芯片的效率肯定小于100%，所以得到的12V输出其实可用功率可能只有15W，也就是最大只有1.2A左右的电流，一般boost升压电路是专门给一些电压较高的轻负载、控制信号电路供电的，比如后续MOS管驱动电路的栅级驱动器的12V供电 继电器控制电路 & 光耦 屏幕截图 2025 07 04 120603 继电器的作用实际上就是一个“电子开关” 机械开关是手动控制，那同理，继电器这个 电子开关 就是用 电信号控制 屏幕截图 2025 07 04 120722 这个“电子开关”的作用实际就是控制A与B导通还是C与B导通 上图的原理就是： 初始状态：由于B连着的弹簧的机械作用，把衔铁杠杆向上拉，A与B的触点相互接触，所以电磁铁不通电的时候，A与B连通，C是断开的。 现在将DE端通电，电磁铁绕的线圈通电产生使电磁铁产生磁力，把衔铁向下吸合，B就会与C的触点相互接触，B与C连通，A断开。 再来看继电器模块的简图： 屏幕截图 2025 07 04 121550 上图是 继电器模块（注意是模块，不是继电器单体） 的原理图： 屏幕截图 2025 07 04 121443 左边是继电器模块，右图是继电器单体，也就是模块上那个最大的蓝方块。 COM脚（common）公共端，也就是对应着上面图3：继电器单体结构图中的B端。 NC（close）代表常闭端，也就是不通电也是导通的一端，对应着图3的A端。 NO（open）代表常开端，也就是线圈通电才导通的一端，对应着图3的C端。 VCC和GND是给继电器线圈通电的电源和地。 IN 端是控制口，由单片机GPIO输入一个高低电平信号，高电平线圈通电，低电平线圈不通电。 右图继电器的引脚，一共有五个，而继电器模块有六个引脚，多了一个IN口，IN口来控制继电器线圈供电，也就是控制了继电器的通断 但如果不加辅助电路，单片机区区一个电平信号控制不了大继电器的通断，所以需要外搭控制电路 屏幕截图 2025 07 04 152011 继电器就是K1，可以看到元件图上5是com端，5与3相连，所以3是常闭端（NC） PC817是 光耦 耦合 ：“信号的传递”，光耦：以光传递信号屏幕截图 2025 07 04 152518 左边是个发光二极管，右边是个光敏元件，可以接收到光信号，有光右边导通，没有光断开 为什么不用单片机推挽输出控制？ 理论上可以。但举个例子，假如工程用的CPU非常昂贵，一块CPU一万块钱（有点夸大），这时候用这个CPU去控制一个200KV的高压电继电器（交流接触器）的通断开关，就问你敢不敢直接连。 所以为了完全分开 高压高干扰的强电电路 与低压小信号的弱电电路，光耦是个最好的选择，弱电这边控制一个小灯发光，强电那边用一个接受光的器件接受信号，做到了电路上的完全隔开，非常安全！ 屏幕截图 2025 07 04 152853 所以这个图，左边就是单片机的led电路，p0.8是某种单片机的一个引脚，推挽输出高低电平，控制光耦内部灯（和LED1指示灯）的亮灭，从而控制右边：三极管Q1的导通与关闭。（模电知识：三极管的基极必须接电阻，上图的三极管会分别工作在饱和和截止状态），三极管的导通和关闭又控制了线圈的导通和关闭， 这样电路就会非常安全了 屏幕截图 2025 07 04 153344 最后是1N4148二极管， 1N4148是最普通的普通二极管 因为继电器的线圈本质上就是一个电感， 电感的电流不能突变 ，所以线圈在关断的瞬间也需要续流。 1N4148是个续流二极管，由于继电器并不像boost电路那样需要高频地开和关，打开就“啪”的一下，不工作了就关掉，所以用不到肖特基二极管，普通二极管即可。 继电器开关在目前项目中用的很少，一般都用MOS管代替 GPIO八种输入输出模式 浮空输入 上拉输入 下拉输入 模拟输入 推挽输出 开漏输出 复用推挽 复用开漏 浮空输入、上下拉输入 GPIO的输入模式怎么读取IO口信息：正常的GPIO口内都有可以读取当前IO口电平的寄存器 PA0口为例，可以用PAin(0)函数来读取PA0是高电平还是低电平 如果PAin(0) 1则为高电平，PAin(0) 0就是低电平 其次，在所有的输入模式下，都可以近似看作没有电流流入IO口内。 意思就是输入模式下的IO口不会对外部电路的功能有任何影响，不需要对其做任何模拟电路上的分析 IO口的输入就像探针一样，接在电路的任何位置不会对电路产生影响 浮空输入 IO的电平状态是不确定的，完全由外部输入决定，如果在该引脚悬空的情况下，读取该端口的电平是不确定的。 下图这个电路，单片机PA0连接着一个按钮，按钮左侧是高电平。 屏幕截图 2025 07 04 190235 如果把PA0配置为浮空输入，按下按钮时，PAin(0)应该为1；但松开按钮后，PAin(0)不一定 0，因为浮空输入在不与任何外部电路连通的情况下，电平未知。 也就是PAin(0)可能等于1也可能等于0. 所以浮空模式并不适用于按键电路 当外部电路不会让IO口成为高阻态的时候 ，可以用浮空电路 屏幕截图 2025 07 04 190626 比如上图： 在按钮右边连了一个10k的电阻接地。 首先按下按钮时，PAin(0) 1，因为流过10K电阻的电流特别小，可忽略，所以VCC3.3通过10K电阻流向GND这条路几乎是断的。 所以如果一直按着按钮，PA0检测到的一直是1（3V3高电平） 当松开按钮，PA0也并不是高阻态了，这时PA0只跟GND相连，会迅速跟GND等电位，这样松开按钮后，PAin(0) 0 这个电阻就叫做 下拉电阻 所以，下拉输入模式就是在单片机内部集成了这么一个电阻。 如果将下图PA0配置为下拉输入模式，屏幕截图 2025 07 05 105600 则松开按钮PAin(0) 0, 按下PAin(0) 1,可以正常运行。 同理，下图的10k电阻就成了 上拉电阻 屏幕截图 2025 07 05 105750 区分上拉还是下拉，就看通过这个电阻将电平拉到了1还是0，拉到1就是上拉，0就是下拉 上拉、下拉、浮空输入常用在按键、编码器正交接吗、uart通信的RX口、IIC通信的SDA输入状态、SPI通信接收口等，广泛用于数字电路的各种通信协议。 设计上下拉电阻电路时的 电阻阻值选择原则 ： 为了节约芯片功耗， 电阻应当足够大 ；电阻越大，电流越小，但电阻值也不能过大，通常在1k到10k之间选取。 推挽输出、复用推挽输出 推挽输出配合电路图：屏幕截图 2025 07 05 110348 上图是简化版推挽输出的电路（省去了三极管基极电阻） 当IN输出1时，三极管S8050（NPN三极管）饱和导通，S8550（PNP三极管）关断，IO就有了3.3V的输出。 当IN输出0时，反之，则IO就是0。 推挽输出是最常用的GPIO输出模式： VCC与IO之间没有电阻，所以推挽输出有一定的驱动能力，可以驱动LED灯，小电机等需要一定驱动电流（几十mA）的电路。 推挽输出的最大特点是可以真正能输出高电平和低电平，在两种电平下都具有驱动能力。 复用推挽输出就是推挽输出。复用的意思就是把IO口的控制权交给外设来处理。比如uart，串口1用了PA9作为TX端，配置为复用推挽输出，由UART外设自己控制PA9的电平高低。 开漏输出、复用开漏输出 类似的电路有 开漏输出 和开集输出 一个是MOS管，“漏”指的漏极； 一个是三极管，“集”指的是集电极。 由于用MOS管较多，就用“开漏输出”这个词代替了开漏输出和开集输出。 三极管搭建的等效电路图如下所示：屏幕截图 2025 07 05 124240 开漏输出的IO口相当于一个连到GND的开关，input为1时，开关闭合，0时打开。 开漏输出的功能很少使用，因为这个电路看似可以给Output端接任意大小的电压源，但是实际上STM32还是受不了的，，所以基本不用STM32的开漏输出。 复用开漏跟复用推挽一样，把IO口配置成开漏输出给外设使用。 STM32 GPIO电路图 屏幕截图 2025 07 05 124603 首先看推挽输出的路径：屏幕截图 2025 07 05 124713 其中输出驱动电路： 屏幕截图 2025 07 05 124812 寄存器信号传到输出控制电路，这个电路的控制方式与三极管推挽输出电路相同，只是换成了MOS管 如果是开漏输出就只用下面的N MOS 屏幕截图 2025 07 05 125107 这两个二极管是 钳位电路 ，属于安全保护电路 VDD是电源电压，STM32中为3.3V VSS就是GND 钳位电路会防止IO口输出的电压大于3.3或者小于0（负电压） 再看浮空输入、上下拉输入的路径屏幕截图 2025 07 05 125417 可以看到它们都经过了同一个元件： 施密特触发器 TTL施密特触发器的作用：将模拟信号转化为矩形信号，标准化数字电平 屏幕截图 2025 07 05 125526 （woc，有点像pwm的原理） 屏幕截图 2025 07 05 125627 上拉电阻和下拉电阻，浮空输入的时候两个开关都断开，上下拉分别闭合对应的开关即可。 模拟输入 模拟输入是最简单的，不经过施密特触发器，直接输入原生态的电压。 电阻相关应用电路 限流电阻 屏幕截图 2025 07 05 145136 为什么这里要连电阻而不是一根导线直接连过去？ 限流电阻 ：顾名思义，限制电流的电阻 普通二极管的伏安特性曲线： 屏幕截图 2025 07 05 145320 二极管接正向电压，通过的电流大小是随着电压的升高呈指数级增长的。 一般的二极管的导通压降都只有0.7V左右，如果在两端正向接3.3V将会烧坏二极管 这时串联一个限流电阻可以分走一部分电压，将二极管的压降稳定在0.7V，那上图的限流电阻就分走了3.3 0.7 2.6V电压，支路电流 2.6/220 12mA，可以使发光二极管正常亮 三极管基极电阻也属于限流电阻，三极管的输入特性曲线如下图所示： 屏幕截图 2025 07 05 150345 可以看出来跟二极管的正向特性曲线是一模一样的，所以三极管的基极上都会有一个电阻（限流电阻） 电阻分压电路及应用 屏幕截图 2025 07 05 150503 非常常用 原理：欧姆定律 V P A 1 ​ R 2 + R 3 R 3 ​ ⋅ 24 V 用处1：测电压 上图可以用来测电源的电压。 如果输入的24V电池没电了，已经降到了22V，那PA1出也会等比例下降，则通过电阻分压电路，通过ADC测量PA1的电压，可以很容易算出电源电压 用处2：调整参考电压 如果有一个2.5V±2.5V的正弦信号需要用ADC进行采样，但STM32的ADC量程只有0 3.3V。 可以接一个电阻分压电路，把5V量程等比例缩放到3.3V， 这个2.5V±2.5V正弦信号的例子将是ACS712将要解决的 应用举例： 屏幕截图 2025 07 05 151426 这里同时应用了电阻限流和电阻分压两种电路 BAT就是battery，接入一个3.7V的锂电池，右边的ADC1的标签就是电压采样点的意思，ADC1这里的电压就是0.5倍的电池当前电压 电流采样电路（检流电阻） 上面是怎么测电压，这里是怎么测电流 屏幕截图 2025 07 05 151909 电阻采样法测电流：在需要测电流的支路上串联一个精密微小阻值电阻，电流流过这个0.01R电阻就会产生一个小压差，测量这个电阻两端的电压，得到的电压除以0.01，欧姆定律就可以算出当前的电流 真正使用时会用运算放大器把信号进行放大 放电电阻 放电电阻：用于电源电路中大电容的电荷泄放 屏幕截图 2025 07 05 153133 DCDC降压电路图：与之前相比加了一个大阻值电阻R5 当锂电池突然断开连接时，C5电容里是充满电荷的状态，如果电荷无处泄放则会储存在C5里，有安全隐患 R5的初衷是泄放掉C5的电荷，防止电到人 但实际上断电后C5的电荷会被后级的电路快速用光，没必要加放电电阻 所以放电电阻用于电容断电后 会完全隔绝无法泄放时的电路，直白的解释就是电源输出端没接任何负载的电路，没东西耗电，电就会一直存着 RC滤波电路 屏幕截图 2025 07 05 154437 跟原先比加了R5和C7， 构成了一个RC滤波电路 简单理解：在电源输入端像这样，串联一个电阻并联一个电容， 就能使电源缓启动，且信号滤除干扰 RC首先可以防止通电一瞬间尖峰电压过大，除去尖峰； 其次也可以滤除高频噪声屏幕截图 2025 07 05 154743 可以看到如果不加RC，高频噪声会增多，而且通电瞬间会产生尖峰。 RC滤波电路的具体原理： 下图输入信号Ui经过一个RC滤波电路变成了Uo。屏幕截图 2025 07 05 154956 屏幕截图 2025 07 05 155102 所以频率大于5kHz的噪声就会被RC滤波器滤，同时由于电容两端的电压不能突变，以及电阻的限流，可以避免通电瞬间电压出现尖峰。 RC滤波的应用除了在电源安全上，在信号处理中还有很多用处 锂电池 & TP4056充电管理电路 常用的 锂离子电池 ，简称锂电池 屏幕截图 2025 07 05 193605 里面是一节一节的下图这种18650电池组成的电池组屏幕截图 2025 07 05 193712 一节18650电池也被称作一个“电芯”，一节锂离子电池的标准电压是3.7V，满电状态下一节电池是4.2V ，这两个参数很重要也很通用 所以我们常用的12V电池 其实是个电池组 ，三节锂电池串联，也就是有三个电芯。三节电池串联的标准电压是3.7\\ 3 11.1V，满电状态是12.6V 同理，两节锂电池串联，两个电芯，标准电压7.4V，满电8.4V 航模电池：屏幕截图 2025 07 05 194107 7.4V/11.1V，和上面的两节、三节锂电池串联起来的电压一样 2S 3S 4S：S就是series（串联）的缩写。 1100mAh/1500 mAh /2200 mAh mAh是电荷量的单位，用于衡量电池的容量。 1100mAh就是这个电源以1100mA也就是1.1A持续放电，把电放完要1小时。 为什么用航模电池 屏幕截图 2025 07 06 101033 因为航模电池会配备这种特制充电器，可以对电池的每个电芯单独充电，防止每个电芯充电不均匀，导致电池寿命下降太快 针对于单节锂离子电池(标准电压3.7V)的电池充电管理电路：基于芯片TP4056 屏幕截图 2025 07 06 101403 分为五个部分 屏幕截图 2025 07 06 101643 锂电池输入接口 屏幕截图 2025 07 06 101718 地分割，因为锂电池充电电流较大，把两种GND分割开来 屏幕截图 2025 07 06 101935 TYPEC接口，但没有用到信号部分，只用来5V供电 屏幕截图 2025 07 06 102025 TP4056充电电路 这个芯片的作用就是外部的5V从VCC输入，然后芯片内部自动降压为3.7 4.2V并从BAT口输出，给电池充电 ，自动适配单节锂电池的充电。 屏幕截图 2025 07 06 102314 其中CHRG和STDBY引脚就是类似充电指示灯的作用 C1是旁路电容，C2是储能电容 R3、R4用于信号口的限流，起安全作用，很多芯片的信号口都会这样，接一个10K左右的电阻 R2 1.2K: 屏幕截图 2025 07 06 102826 想让充电速度快一点，所以选1.2K，速度可达最快1A 电路用法： 在一些最高电平只需要3.3V的设备中可以用到TP4056芯片 当锂电池有电时，无论TYPEC接不接电源，TP4056的BAT端都会输出3.7V 4.2V，所以可以给BAT端串联一个LDO，降压到3.3V给后续电路标准电压。这是一种典型应用 数字地DGND，模拟地AGND，电源地PGND 有时电路里会有屏幕截图 2025 07 06 103544 AGND，PGND 地的分割： 在电路设计中，为了降低不同的电路之间的相互干扰，所以会引入不同的地线，从而形成不同的0V参考点，为各个功能电路的电流分开提供各自的回路 （1）DGND，数字地 接触最多的地，比如单片机电路，I2C等数字通信电路等，在之前的电路设计中把DGND都简写为了GND 由于数字电路是010101的电平跳变： 屏幕截图 2025 07 06 104000 所以数字电路的电压在快速变化，所以数字电路产生的电流也会快速变化。众所周知，电流都是由VCC经过负载之后流入GND的，单片机也算是一个小的负载，里面乱七八糟好多高速的数字电路都有0101的快速的小股电流突变，而这些电流最后都要汇入GND，GND再流回电路负极，也就是通过同一个出口离开这个电路板 这就导致DGND的GND平面很凌乱，很多小电流流过引起小信号的波动干扰，造成GND平面的参考电压有一些噪声 （2）AGND，模拟地 屏幕截图 2025 07 06 104000 1 模拟电路比如ADC采样电路，DAC电路，运算放大电路等 这些电路的电流和电压都是 慢速连续变化的 ，远没有数字电路那么“激烈”，而且电路常走的都是微弱电压信号，所以这就有一个要求： 模拟电路的地必须非常纯净！ ，不能有较多干扰的噪声 所以数字电路GND的电压波动噪声会导致模拟电路的参考电压不稳，可能导致模拟信号失真 （3）PGND，功率地 数字电路和模拟电路都是小功率电路，像电机驱动电路，自动加热电路这种属于大功率电路，在电机电路中，比如电机启动、急停，噪声都非常大。 对数字电路和模拟电路都有较大影响，如果不加TVS元件控制，可能一个大负载电机刹车的噪声都能让输入电压反转 所以在设计电路时：屏幕截图 2025 07 06 105032 会有一个0R电阻，两边连两种GND，这就是 单点接地 ，通过小电阻把不同的GND分隔开。通过0R电阻使两个GND之间只通过一个狭窄通道相连。 电流汹涌大小：功率地 数字地 模拟地 屏幕截图 2025 07 06 105411 通俗理解就是：每种电路对噪声的要求不一样，所以要区分开 这种单点接地对于任何频率的信号都有衰减作用，可以有效限制噪声的传递。 ACS712电流检测电路 有时想检测某一条电路的电流，有什么合适的芯片？选型有以下要求： 1.希望电路要简便易用，不需要太复杂的电路设计 2.对于电流精度要求不是太高，到0.1A就行 → ACS712 ACS712是一款电流检测芯片，利用 霍尔效应 进行采样 电流垂直于外磁场进入半导体，垂直于电流和磁场的方向会产生一个附加电场，也就是有了电势差，ACS712就通过检测这个电势差来检测电流的大小。 阅读芯片手册： 屏幕截图 2025 07 06 152843 阴影这句话说的是它的检测误差，在25°C时是1.5%fs 1.5%fs：fs（full scale）满量程的意思，1.5%fs就是指检测误差是芯片检测量程的1.5%，比如检测量程是±5A，那误差就是(±5) 1.5% ±0.075A。 可以发现ACS712并不是那么准，它的芯片有三种量程，分别是： ±5A、±20A、±30A， 则算出每种量程的误差： ±5 1.5% ±0.075A ±20 1.5% ±0.3A ±30 1.5% ±0.45A 所以一般就用±5A量程的，误差最小。 芯片的使用： 屏幕截图 2025 07 06 153127 芯片一共有八个引脚，用法就是将芯片串联在待检测的支路里，电流从IP+流入，从IP 流出；电流反向的话，IP 流入，IP+流出也是可以的；VCC接5V，FILTER是滤波器，外接一个小电容进行滤波；VIOUT作为电压信号输出 VIOUT信号的具体输出值：屏幕截图 2025 07 06 153642OUT与Ip（待测电流）的关系如上图所示，是线性的，中间值是2.5V，电流是负的代表方向是从IP 流向IP+。 05A规格的计算公式： Vout 2.5 + 0.185 Ip 20A规格的计算公式： Vout 2.5 + 0.1 Ip 30A规格的计算公式： Vout 2.5 + 0.066 Ip 设计电路： 一个问题：ACS712只能5V供电，而STM32的参考电压是3.3V 解决方法是使用电阻分压电路，通过 两个电阻分压 ，把0 5V的基准等比例转到0 3.3V的基准 如图所示：屏幕截图 2025 07 06 153953 右边的MOT1和MOT01代表把电路串在了有刷电机回路里，可以测量电机的电流，两个104电容用来消除高频噪声 左边VIOUT输出之后经过了一个电阻分压电路，分压后的电压U3输入到运放LM358的同相输入端（3口），分压公式是： U 3 ​ V I O U T ∗ R 1 + R 2 + R 3 + R 4 R 1 + R 2 ​ 为什么要分成四个电阻： 因为要考虑实际有没有对应阻值的电阻，比如根本没有阻值为24.6k的贴片电阻，所以拆成24K和2.4K两个电阻 运放的作用： 分压后输出的电压U3，经过了一个 电压跟随器 电路，最终图中AOUT口的电压完全等于U3；电压跟随器只是用于保证信号的隔离，避免外界噪声的影响 基于CD4051的模拟开关电路 如果是为了机器人应用没必要学习数电里 门电路 的应用 一种还算有用的芯片： 模拟开关/多路复用器 以CD4051为例。 CD4051是一个八选一模拟开关芯片 下面是八选一模拟开关的测试电路： 屏幕截图 2025 07 09 185102 八选一模拟开关是什么意思？ 首先它是一个开关，这个开关有公共端，也就是3号引脚 I/O 有八个选择端：上图中的Y0 Y7，所以这是一个电子的“ 单刀八掷开关 ” 屏幕截图 2025 07 09 185542 八选一就是只能选择Y0 Y1八路中的一路与公共端 I/O 导通 分路选择的控制方式：通过A2、A1、A0三个口，也就是上图中的C B A三个口，这三个口都能输入高低电平，也就是各能代表一个二进制位，三个二进制位正好表示八种状态。 芯片手册里的真值表： 屏幕截图 2025 07 09 190631 与原理图备注有区别，A2是C，A1是B，A0是A INHIBIT是阻止的意思，6号引脚屏幕截图 2025 07 09 190821INH脚低电平是芯片使能，所以原理图六号引脚接地 屏幕截图 2025 07 09 191133 屏幕截图 2025 07 09 191158 VDD是正功率输入，VEE是负功率输入，VSS是GND VEE和VSS不一样↓ 全新概念： 负电压 GND只是一个参考的零位，并不是绝对0V，有比GND电位低的点 这也是CD4051强大的地方，这个芯片同时结合了数电和模电，并且做好了它们之间的隔离， 用数电电平来控制哪一路导通，然后模拟信号通过指定导通的回路。 当模拟信号有负电压时，VEE≠VSS 比如CD4051要通过一个±2.5V的正弦波，那么VDD就要接2.5V， VEE要接 2.5V电压源 ，而VSS接GND也就是0V。GND是DGND，给数字电路部分用的，而VDD和VEE是给模拟信号用的 屏幕截图 2025 07 09 230441 最后看整个电路图，因为只做简单的电路测试，输入0 3.3V电压，所以VEE和VSS都接AGND。 并且只接入了Y0和Y1两路 其中Y0只能介入0 3.3V的信号，而Y1路有个1/10的电阻分压，可以接入0 33V的大电压信号，同时出于安全，并联了一个3.3的TVS二极管 SGM3204电荷泵负压生成电路 关于如何生成负电压 电荷泵 屏幕截图 2025 07 09 231020 电荷泵原理演示图 首先让S1和S4关闭：屏幕截图 2025 07 09 231135飞跨电容的两侧有5V的压差 紧接着断开S1和S4，关闭S2和S3： 屏幕截图 2025 07 09 231254 飞跨电容左侧与GND相连，变为0V，但是 电容两端电压不能突变 ，所以之前Cf左右相差5V，这个差值无法瞬间改变，所以Cf右端变为 5V，实现负电压的生成 然后S2和S3断开，关闭S1和S4屏幕截图 2025 07 09 231606如上图， 此时S2的断开非常巧妙，把负压输出与前端电路隔开，负压由输出电容Co保持。 然后就是按照上面的步骤循环往复 常用的负压芯片是SGM3204，原理就是电荷泵屏幕截图 2025 07 09 232003连接其实非常简单，输入端和输出端都配有储能电容和旁路电容，C27是个飞跨电容 TB6612FNG芯片 H桥驱动电路 一个新的电机驱动芯片，在机器人开发中可以替代L298N TB6612FNG，简称TB6612，是一款双H桥电机驱动芯片，一个芯片可以带动两个直流有刷电机 H桥电路：两个NPN三极管 + 两个PNP三极管屏幕截图 2025 07 10 095135 但由于三极管能承受的电流较小，目前市面上电机驱动的H桥电路的开关管都换成了MOS管（场效应管），MOS管可以承受比三极管更大的电流。包括L298N和TB6612芯片内的电路 ，用的也是MOS管而不是三极管，由 四个N沟道MOS管 组成H桥 （MOS管与三极管的控制方法基本一致，可以先当成电子开关用） 再细分一下，这个H桥又叫做全桥电路，由两个半桥电路组成，比如下图左边两个MOS管构成一个半桥电路，右边两个又构成一个半桥电路。 在半桥电路中，上下桥臂不可以同时导通，因为同时导通会导致DCBUS和GND短路，烧坏电路； 所以下图，上半桥臂用PWM控制， 下半桥臂就必须用上半桥臂的PWM的互补波形控制 。屏幕截图 2025 07 10 102122 屏幕截图 2025 07 10 102145 TB6612和L298N的比较屏幕截图 2025 07 10 102435TB6612：一般比赛 / 小项目 因为控制一个电机只需要一路PWM信号，省下了定时器的资源； 其次TB6612模块的体积更小。更容易集成到电路板上。 TB6612也有麻烦的地方，那就是接线会比L298N多一些 屏幕截图 2025 07 10 102645三个GND：右上GND是数字地，下面两个GND是功率地，接地的时候两个GND都要接。 屏幕截图 2025 07 10 102927 VM ：驱动电机的电压，最大15V，为了安全最大接到12V VCC ：数字电平的参考电压，用stm32就接3.3V PWMA、PWMB ：两个电机的控制信号，TB6612一个电机只需要一路PWM控制 AIN1、AIN2 ：控制电机1的方向，AIN1 1，AIN2 0则电机正转，AIN1 0，AIN2 1则电机反转。 BIN1、BIN2 ：控制电机2的方向，同理 AO1、AO2 分别接电机1的两端， BO1和BO2 分别接电机2的两端 STBY ：（standby）待命状态，需要接高电平（3.3V），代表电机驱动为待命状态，可旋转；当STBY 0时，有信号输入点击也不会转 将模块电路集成至电路板 屏幕截图 2025 07 10 104206首先通过芯片手册里的芯片引脚图可以知道，GND做了分割分成GND和PGND，所以设计电路也要做好分割； 然后由于TB6612控制一个电机的方向需要两路IO口，比较浪费，所以设计了两个非门电路：屏幕截图 2025 07 10 105629 两个三极管都工作在饱和状态，当开关用，只有AIN1和BIN1接到单片机的两个IO口，分别控制两个电机的方向。 当AIN1 1时，三极管导通，AIN2 0，电机正转；当AIN1 0时，三极管关断，AIN2处上拉，AIN2 1，电机反转。 图中R21和R22是上拉电阻 R24和R25是基极限流电阻 限流电阻和上拉电阻的值分别怎么取？ 基极电阻：因为三极管的输入特性曲线和二极管的伏安特性曲线一样 屏幕截图 2025 07 10 110101如果不接限流电阻，Ube 3.3V，电流就会非常大，烧掉三极管 三极管工作在饱和状态所选用的基极电阻的阻值一般选1K 10K（为什么呢？） 因为三极管工作在深度饱和状态下的话，三极管放大倍数就很低了，一般电流放大倍数hFE就是十几倍， i c ​ / i B ​ h FE ​ 取hFE为10，设ic是10mA，计算得ib为0.001A 由于三极管的输入特性曲线，三极管的Ube压降在0.7V左右： R B ​ ( 3.3 V − 0.7 V ) /0.001 A ≈ 10 k Ω 计算出是10K，但是这里基极电阻取2K，因为一般这里取计算值的1/2，1/3或者1/5，用这种冗余来保证深度饱和状态，总之三极管的饱和工作状态其实 不需要多么深入的计算 ，往极限里取值，基极电阻小一点，ib就越大，越容易深度饱和，总没错。 上拉电阻：一般都取10K 屏幕截图 2025 07 10 111347所以有了这个非门电路，单片机就可以不用接AIN2和BIN2了，省下两路IO口 PWMA、PWMB、AIN1、BIN1四个口直接连到单片机对应的IO口 屏幕截图 2025 07 10 111710两个编码器电机的接口，AO1、AO2接电机1的两端，BO1、BO2接电机2的两端；VCC5和GND是给编码器供电的；CDDE XA和CODE XB是编码器的AB相，分别接到单片机对应正交解码的IO口。 屏幕截图 2025 07 10 112152这两个电容需要尽量靠近TB6612FNG芯片，一个是大容值铝电解电容，一个是100nf的小电容 大电容用于储存电荷，比如电机起转时需要一个很大的电流，这个时候电容就可以快速及时地利用储存的电荷形成大电流供给点击用。 104小电容放在芯片的VCC引脚旁边， 作为输入数字参考电压源的滤波电容滤除高频噪声 屏幕截图 2025 07 10 112540TB6612在布线时走线的3D图和2D图 可以看到下面这一排线都很粗，都使用了铺铜走线，因为电机的电流直接流过这几个引脚，所以布线要尽量粗；上面那一排除了DCBUS都是信号线，走线就可以很细 MOS管的选型、经典应用电路及栅级驱动器的使用 MOS管的应用 屏幕截图 2025 07 10 140951简单概括模电里讲的MOS管的工作原理：栅级g不与其他两级导通，通过给g一定的电压，使g与B形成电场从而把电子吸到s与d连接处形成沟道 屏幕截图 2025 07 10 142951 N MOS和P MOS原理图画法的区别： 1. N mos的小箭头是往栅极里面指，而P mos的小箭头是往栅极外面指。 2. N mos的寄生二极管是从源极指向漏极的，P mos相反。 先说N MOS的应用，在实际电路中，无论是电机驱动、加热电路等大电流驱动电路，还是信号通断的小电路，N MOS都用的比P MOS多 电路中，可以把MOS管看作一个电子开关。 MOS管比继电器和光耦等其他开关元件的优点： 1. 开关频率高 ，一般mos管的开关频率都在几百KHz，这是继电器无论如何也达不到的高频率，频率高的好处就是可以放大高频的PWM信号。通过单片机PWM控制mos管的栅极，从而控制更大的电压产生电压更大、电流更大的PWM，用这个大电压电流的PWM去控制电机，加热电阻等 2. 通过电流大，这是光耦也无法企及的。50A、100A大电流对MOS管来说很正常 N MOS管的导通条件： 随便找一款MOS管为例，要了解这个mos管的各项参数。 屏幕截图 2025 07 10 143941 首先右边有个小表，有三个参数： VDS ：100V，VDS就是漏极与源极之间的最大电压。看上图N MOS的符号可知，N MOS的寄生二极管正极是连着源极的， 也就是MOS管在当开关用的时候，漏极电压必须大于源极电压 ，不然如果VS VD，电流就全走寄生二极管了 R DS(on) ：是MOS管完全导通时DS之间的内阻，是2.5mΩ ID ：漏极电流，也就是MOS管完全导通时从DS流过的最大电流，达到了180A 继续往下翻找其它参数：屏幕截图 2025 07 10 150519V GS ，最大的栅极电压 为什么是V GS ，栅极电压VG与源极S有什么关系 因为N MOS管很多时候都要接GND，N型MOS管的VG要比VS大，管才能导通。在实际应用中，源极经常接地，所以在源极S接地时，V GS 就是VG，也就是栅极对于GND的电压 ±20V是因为P MOS的V GS 要 GS(th) 也很重要，是MOS管刚刚导通状态时的栅极电压。当栅极电压大于或等于V GS(th) ，MOS管会开始导通。如果想让MOS管完全导通，至少需要取3倍的V GS(th) ，但是不能超过V GS 的最大允许值±20V，所以可以设置栅极电压V GS 为15V或者12V，4 5倍于V GS 。既保证了完全导通，又不会超过最大允许电压 那么怎么形成12V或15V的PWM呢？ 这就要用到 栅极驱动器 栅极驱动器（栅极驱动芯片）的作用就是将小电压信号转换成大一点的电压信号，但也只是大电压信号，并没有驱动能力，所以栅极驱动器专门用于给MOS管的栅极提供合适的电压，比如： STM32输出的PWM高电平是3.3V，经过栅极驱动器，就可以输出高电平为12V、15V的大电压PWM信号。 EG3014 一款栅极驱动器： EG3014 屏幕截图 2025 07 10 151457屏幕截图 2025 07 10 151506 1 为什么有两个N MOS管： 排除两个mos管之外的所有电容、电阻元件得到： 半桥电路 屏幕截图 2025 07 10 151756因为大功率mos管很多都是用于控制电机的，而电机就需要用到桥式驱动电路，所以栅极驱动器为了方便驱动也这么设计，一次驱动两个mos管 芯片用法： HIN、LIN 用于输入两路3.3V的PWM信号，分别控制两个N MOS管。 HIN控制半桥电路上端（高端）的MOS管（H就是high的意思）； LIN控制半桥电路下端（低端）的MOS管（L就是low的意思）。 HO 连接高端mos管的栅极； LO 连接低端mos管的栅极。 芯片的VCC 就是想把PWM的高电平设为多少，上图典型电路接的VCC是15V。接个12V也是可以的，这样放大出来的PWM，高电平就是12V。 从硬件层面分析典型电路上的电器元件的作用：屏幕截图 2025 07 10 152115R2是 栅极电阻 ： 栅极必须串联一个电阻，用于缓冲信号的冲击，一般取10欧以内，典型值是2.2Ω，属于安全防护电路。 D2是 反向二极管 ： 用于电荷的快速泄放，栅极信号变至低电平时，电荷可以通过反向二极管更快速泄放出来，属于安全防护电路。 C4是 旁路电容 ，用于消除高频噪声，老朋友了。 VB和VS的作用 屏幕截图 2025 07 10 151756 1假设上图是H桥电路的左半部分，OUT端连着电机。电机和右半部分桥没画出来 假设此时+Vin是 48V ，这时让 高端MOS开启，低端MOS关闭。 时简单分析即可知，此时 OUT端的电压就是+Vin的电压，48V。 那这就有个大问题：N型MOS管的导通条件是VGS 0，现在栅极电压是12V，源极电压是48V，VGS 这个高端mos管只要一导通，VGS就 而且低端MOS就不存在这个问题，低端的源极电压始终保持在0。 解决这个问题就用到了自举升压电路 芯片内部有一个升压电路，可以将高端输出HO的高电平抬高 V H O ​ V CC + Vin 也就是电压抬高到12V+48V 60V，这样栅极电压是60V，源极电压是48V，MOS管就正常导通了。 而这个升压电路就要用到剩下的那两个口了：屏幕截图 2025 07 10 153011手册对于这两个口的解释：VB是高端悬浮电源；VS是高端悬浮地端。 高端就是半桥电路上面的那个MOS管。 屏幕截图 2025 07 10 153217VS接两个MOS管中间，也就是高端MOS的悬浮地，在高端MOS管看来，OUT口这里的电位就是它的地，所以VS接这里，然后通过串联一个电容，另一端接到VB。 上图，其中C2是 电荷泵 。 电荷泵是一种DC DC电压变换电路，这个电容的参数选型很有讲究： 首先，这个电容的耐压必须要是两倍于Vin电源电压的，也就是这个电路如果电源电压是48V，那电容选型就要选100V耐压的，所以电容封装也会相应增大，一般选1206封装的 无极性电容 其次，这个芯片手册图标注也有问题，这里要选无极性电容，而不是极性电容。 第三是电容容值的选型，这个跟选的MOS管的参数有关：先返回这个MOS管的手册，去找一个参数： 屏幕截图 2025 07 10 153711Q G 是栅极总电荷，也就是栅极充满电之后能储存多少电荷。 电荷泵自举电容的计算由自举电容选取经验公式求得。 C BOOT ​ ≥ 10 ∗ V CC ​ Q G ​ ​ C BOOT 为电荷泵选择电容的容值，Q G 栅极总电荷，V CC 为 栅极驱动器供电电压 （不是电源电压）。 比如我们这个电路，Q G 169nC，V CC 12V： C BOOT ​ ≥ 10 ∗ V CC ​ Q G ​ ​ 10 ∗ 12 169 ∗ 10 − 9 ​ 140 n F 那就选150nF、100V耐压、1206封装电容。 PWM的控制方式： 由于半桥电路工作时，高端MOS打开，那低端MOS就必须关断，反之也是，总之工作时两个MOS必须一开一关。这就对控制的PWM波形有要求，需要一路PWM波和一路 互补PWM波 。 这个互补pwm波去哪找呢？STM32的高级定时器就可以输出。STM32F103C8T6只有TIM1一个高级定时器，TIM2 4都是通用定时器。输出代码是后话。 接下来看一个自动加热电路：屏幕截图 2025 07 10 154132看起来很复杂，还是用摘除法： 把ACS712那一大堆电路都去掉，最后发现就只有右边一个很简单的加热电阻供电电路，SSW 1P是一个安全开关，在电路故障时物理切断； 那剩下的元件就只有一个MOS管开关了，这个电路就比电机驱动简单多了，首先它不分高端和低端，不需要升压就可以驱动MOS管，所以这个栅极驱动器芯片的HO端就没接线，只用了LO端。‘ 另外多了R9和D2屏幕截图 2025 07 10 154356这两个元件是按照大功率电机的推荐电路补上的元件。 屏幕截图 2025 07 10 154433右下角的跳线帽是选择使用电池12V还是升压电路升压得到的12V。 这个电路就是上面讲到的那个开关电路的简化版，非常好用，可以带各种各样的大电流负载：水泵、水阀、直流电机、大灯、直流风扇，什么都行。","summary":"电路基础、供电保护、PCB 设计与制造学习笔记。"},{"permalink":"https://zhr0529.cn/posts/linux学习笔记-2025/","title":"【理论】Linux学习笔记","content":"Linux基础命令 Linux的目录结构 image 20221027214128453 / ，根目录是最顶级的目录了 Linux只有一个顶级目录： / 路径描述的层次关系同样适用 / 来表示 /home/itheima/a.txt，表示根目录下的home文件夹内有itheima文件夹，内有a.txt ls命令 功能：列出文件夹信息 语法： ls [ l h a] [参数] 参数：被查看的文件夹，不提供参数，表示查看当前工作目录 l，以列表形式查看 h，配合 l，以更加人性化的方式显示文件大小 a，显示隐藏文件 隐藏文件、文件夹 在Linux中以 . 开头的，均是隐藏的。 默认不显示出来，需要 a 选项才可查看到。 pwd命令 功能：展示当前工作目录 语法： pwd cd命令 功能：切换工作目录 语法： cd [目标目录] 参数：目标目录，要切换去的地方，不提供默认切换到 当前登录用户HOME目录 HOME目录 每一个用户在Linux系统中都有自己的专属工作目录，称之为HOME目录。 普通用户的HOME目录，默认在： /home/用户名 root用户的HOME目录，在： /root FinalShell登陆终端后，默认的工作目录就是用户的HOME目录 相对路径、绝对路径 相对路径， 非 / 开头的称之为相对路径 相对路径表示以 当前目录 作为起点，去描述路径，如 test/a.txt ，表示当前工作目录内的test文件夹内的a.txt文件 绝对路径， 以 / 开头的称之为绝对路径 绝对路径从 根 开始描述路径 特殊路径符 . ，表示当前，比如./a.txt，表示当前文件夹内的 a.txt 文件 .. ，表示上级目录，比如 ../ 表示上级目录， ../../ 表示上级的上级目录 ，表示用户的HOME目录，比如 cd ，即可切回用户HOME目录 mkdir命令 功能：创建文件夹 语法： mkdir [ p] 参数 参数：被创建文件夹的路径 选项： p，可选，表示创建前置路径 touch命令 功能：创建文件 语法： touch 参数 参数：被创建的文件路径 cat命令 功能：查看文件内容 语法： cat 参数 参数：被查看的文件路径 more命令 功能：查看文件，可以支持翻页查看 语法： more 参数 参数：被查看的文件路径 在查看过程中： 空格 键翻页 q 退出查看 cp命令 功能：复制文件、文件夹 语法： cp [ r] 参数1 参数2 参数1，被复制的 参数2，要复制去的地方 选项： r，可选，复制文件夹使用 示例： cp a.txt b.txt，复制当前目录下a.txt为b.txt cp a.txt test/，复制当前目录a.txt到test文件夹内 cp r test test2，复制文件夹test到当前文件夹内为test2存在 mv命令 功能：移动文件、文件夹 语法： mv 参数1 参数2 参数1：被移动的 参数2：要移动去的地方，参数2如果不存在，则会进行改名 rm命令 功能：删除文件、文件夹 语法： rm [ r f] 参数...参数 参数：支持多个，每一个表示被删除的，空格进行分隔 选项： r，删除文件夹使用 选项： f，强制删除，不会给出确认提示，一般root用户会用到 rm命令很危险，一定要注意，特别是切换到root用户的时候。 which命令 功能：查看命令的程序本体文件路径 语法： which 参数 参数：被查看的命令 find命令 功能：搜索文件 语法1按文件名搜索： find 路径 name 参数 路径，搜索的起始路径 参数，搜索的关键字，支持通配符 ， 比如： test表示搜索任意以test结尾的文件 grep命令 功能：过滤关键字 语法： grep [ n] 关键字 文件路径 选项 n，可选，表示在结果中显示匹配的行的行号。 参数，关键字，必填，表示过滤的关键字，带有空格或其它特殊符号，建议使用””将关键字包围起来 参数，文件路径，必填，表示要过滤内容的文件路径，可作为内容输入端口 参数文件路径，可以作为管道符的输入 wc命令 功能：统计 语法： wc [ c m l w] 文件路径 选项， c，统计bytes数量 选项， m，统计字符数量 选项， l，统计行数 选项， w，统计单词数量 参数，文件路径，被统计的文件，可作为内容输入端口 参数文件路径，可作为管道符的输入 管道符 写法： 功能：将符号左边的结果，作为符号右边的输入 示例： cat a.txt grep itheima ，将cat a.txt的结果，作为grep命令的输入，用来过滤 itheima 关键字 可以支持嵌套： cat a.txt grep itheima grep itcast echo命令 功能：输出内容 语法： echo 参数 参数：被输出的内容 反引号 功能：被两个反引号包围的内容，会作为命令执行 示例： echo \\ pwd\\ ，会输出当前工作目录 tail命令 功能：查看文件尾部内容 语法： tail [ f] 参数 参数：被查看的文件 选项： f，持续跟踪文件修改 head命令 功能：查看文件头部内容 语法： head [ n] 参数 参数：被查看的文件 选项： n，查看的行数 重定向符 功能：将符号左边的结果，输出到右边指定的文件中去 ，表示覆盖输出 ，表示追加输出 vi编辑器 命令模式快捷键 image 20221027215841573 image 20221027215846581 image 20221027215849668 底线命令快捷键 image 20221027215858967 命令的选项 我们学习的一系列Linux命令，它们所拥有的选项都是非常多的。 比如，简单的ls命令就有： a A b c C d D f F g G h H i I k l L m n N o p q Q r R s S t T u U v w x X 1等选项，可以发现选项是极其多的。 课程中， 并不会将全部的选项都进行讲解，否则，一个ls命令就可能讲解2小时之久。 课程中，会对常见的选项进行讲解， 足够满足绝大多数的学习、工作场景。 查看命令的帮助 可以通过： 命令 help 查看命令的帮助手册 image 20221027220005610 查看命令的详细手册 可以通过： man 命令 查看某命令的详细手册 image 20221027220009949 Linux常用操作 软件安装 CentOS系统使用： yum [install remove search] [ y] 软件名称 install 安装 remove 卸载 search 搜索 y，自动确认 Ubuntu系统使用 apt [install remove search] [ y] 软件名称 install 安装 remove 卸载 search 搜索 y，自动确认 yum 和 apt 均需要root权限 systemctl 功能：控制系统服务的启动关闭等 语法： systemctl start stop restart disable enable status 服务名 start，启动 stop，停止 status，查看状态 disable，关闭开机自启 enable，开启开机自启 restart，重启 软链接 功能：创建文件、文件夹软链接（快捷方式） 语法： ln s 参数1 参数2 参数1：被链接的 参数2：要链接去的地方（快捷方式的名称和存放位置） 日期 语法： date [ d] [+格式化字符串] d 按照给定的字符串显示日期，一般用于日期计算 格式化字符串：通过特定的字符串标记，来控制显示的日期格式 %Y 年%y 年份后两位数字 (00..99) %m 月份 (01..12) %d 日 (01..31) %H 小时 (00..23) %M 分钟 (00..59) %S 秒 (00..60) %s 自 1970 01 01 00:00:00 UTC 到现在的秒数 示例： 按照2022 01 01的格式显示日期 image 20221027220514640 按照2022 01 01 10:00:00的格式显示日期 image 20221027220525625 d选项日期计算 image 20221027220429831 支持的时间标记为： image 20221027220449312 时区 修改时区为中国时区 image 20221027220554654 ntp 功能：同步时间 安装： yum install y ntp 启动管理： systemctl start stop restart status disable enable ntpd 手动校准时间： ntpdate u ntp.aliyun.com ip地址 格式：a.b.c.d abcd为0 255的数字 特殊IP： 127.0.0.1，表示本机 0.0.0.0 可以表示本机 也可以表示任意IP（看使用场景） 查看ip： ifconfig 主机名 功能：Linux系统的名称 查看： hostname 设置： hostnamectl set hostname 主机名 配置VMware固定IP 1. 修改VMware网络，参阅PPT，图太多 2. 设置Linux内部固定IP 修改文件： /etc/sysconfig/network scripts/ifcfg ens33 示例文件内容： shell TYPE \"Ethernet\" PROXY METHOD \"none\" BROWSER ONLY \"no\" BOOTPROTO \"static\" 改为static，固定IP DEFROUTE \"yes\" IPV4 FAILURE FATAL \"no\" IPV6INIT \"yes\" IPV6 AUTOCONF \"yes\" IPV6 DEFROUTE \"yes\" IPV6 FAILURE FATAL \"no\" IPV6 ADDR GEN MODE \"stable privacy\" NAME \"ens33\" UUID \"1b0011cb 0d2e 4eaa 8a11 af7d50ebc876\" DEVICE \"ens33\" ONBOOT \"yes\" IPADDR \"192.168.88.131\" IP地址，自己设置，要匹配网络范围 NETMASK \"255.255.255.0\" 子网掩码，固定写法255.255.255.0 GATEWAY \"192.168.88.2\" 网关，要和VMware中配置的一致 DNS1 \"192.168.88.2\" DNS1服务器，和网关一致即可 ps命令 功能：查看进程信息 语法： ps ef ，查看全部进程信息，可以搭配grep做过滤： ps ef grep xxx kill命令 image 20221027221303037 nmap命令 image 20221027221241123 netstat命令 功能：查看端口占用 用法： netstat anp grep xxx ping命令 测试网络是否联通 语法： ping [ c num] 参数 image 20221027221129782 wget命令 image 20221027221148964 curl命令 image 20221027221201079 image 20221027221210518 top命令 功能：查看主机运行状态 语法： top ，查看基础信息 可用选项： image 20221027221340729 交互式模式中，可用快捷键： image 20221027221354137 df命令 查看磁盘占用 image 20221027221413787 iostat命令 查看CPU、磁盘的相关信息 image 20221027221439990 image 20221027221514237 sar命令 查看网络统计 image 20221027221545822 环境变量 临时设置：export 变量名 变量值 永久设置： 针对用户，设置用户HOME目录内： .bashrc 文件 针对全局，设置 /etc/profile PATH变量 记录了执行程序的搜索路径 可以将自定义路径加入PATH内，实现自定义命令在任意地方均可执行的效果 $符号 可以取出指定的环境变量的值 语法： $变量名 示例： echo $PATH ，输出PATH环境变量的值 echo ${PATH}ABC ，输出PATH环境变量的值以及ABC 如果变量名和其它内容混淆在一起，可以使用${} 压缩解压 压缩 tar zcvf 压缩包 被压缩1...被压缩2...被压缩N z表示使用gzip，可以不写 zip [ r] 参数1 参数2 参数N image 20221027221906247 解压 tar zxvf 被解压的文件 C 要解压去的地方 z表示使用gzip，可以省略 C，可以省略，指定要解压去的地方，不写解压到当前目录 unzip [ d] 参数 image 20221027221939899 su命令 切换用户 语法： su [ ] [用户] image 20221027222021619 sudo命令 image 20221027222035337 比如： shell itheima ALL (ALL) NOPASSWD: ALL 在visudo内配置如上内容，可以让itheima用户，无需密码直接使用 sudo chmod命令 修改文件、文件夹权限 语法： chmod [ R] 权限 参数 权限，要设置的权限，比如755，表示： rwxr xr x image 20221027222157276 参数，被修改的文件、文件夹 选项 R，设置文件夹和其内部全部内容一样生效 chown命令 修改文件、文件夹所属用户、组 语法： chown [ R] [用户][:][用户组] 文件或文件夹 image 20221027222326192 用户组管理 image 20221027222354498 用户管理 image 20221027222407618 genenv命令 getenv group ，查看系统全部的用户组 image 20221027222446514 getenv passwd ，查看系统全部的用户 image 20221027222512274 env命令 查看系统全部的环境变量 语法： env","summary":"Linux 命令、权限、网络与进程管理。"},{"permalink":"https://zhr0529.cn/posts/51单片机学习笔记/","title":"【理论】51单片机学习笔记（AI8051U，32bit）","content":"在最开始的 c define MAIN Fosc 24000UL 定义主频为24.000MHz 在main函数中，开头可添加如下代码 c EAXFR 1; // 使能扩展的特殊寄存器 WTST 0x00; // 设置代码等待时间为0，CPU 执行程序的速度最快 CKCON 0x00; // 设置外部总线速度最快 以使得程序运行速度最快 点亮一个LED c sbit LED1 P0 ^ 0; sbit LED2 P0 ^ 1; LED1的对应引脚是P00 LED2的对应引脚是P01 以上代码一般添加在最开头（main函数之前） 用来定义引脚 c P0M1 0x00; P0M0 0x00； 将P0端口的所有引脚都设置为 推挽输出 模式 因为要使用P0引脚，以上代码暂且理解为初始化 （目前这么理解 2025.3.29，知道具体为什么来还愿） c LED1 0; LED2 1; LED1输出低电平，LED2输出高电平 通过输出高低电平控制LED亮灭 编程实现LED闪烁&LED流水灯&数码管显示 LED闪烁 c LED1 ON; delay ms(500); LED1 OFF; delay ms(500); 很简单，在LED亮灭间穿插延时就好了 LED流水灯&数码管显示 c u8 code LED Index[] {0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80}; u8 code SEG Index[] {0x3F,0x06,0x5B,0x4F,0x66,0x6d,0x7d,0x07,0x7f,0x6f}; LED数组的作用，通过十六进制转二进制可以得到LED阵列的状态。如0x08就代表00001000，可得出LED状态 SEG数组的作用，通过不同的十六进制可让数码管显示不同的字符，如0x3F代表1，0x06代表2 c void blink 3(void) { static u8 j; P2 LED Index[j]; j++; if(j 8) j 0; delay ms(500); } void blink 4(void) { static u8 k; P0 SEG Index[k]; k++; if(k 9) k 0; delay ms(500); } 顺着数组做for循环，通过前面定义好的数组就可以实现LED流水灯或数码管计数了 I/O输入与输出控制按键 按键检测代码 c while(1) { delay ms(20); // 延时20毫秒 if(KEY1 0) // 检测按键KEY1是否被按下 { if(i 3) i 0; P2 LED[i]; // 将P2端口的输出设置为LED数组的第i个元素 i++; while(KEY1 0); // 等待按键释放（按键防抖或按键长按处理） } } 注意需要添加消抖延迟函数 & 等待按键释放的while函数 延时代码 c void delay ms(u16 ms) { u16 i; do{ i MAIN Fosc / 6000; while( i); }while( ms); } 例：需要延时200ms → delay ms(200) 定时器中断 需要去ISP软件中选择“定时器计算器”工具，生成所需要的定时器初始化函数与定时器执行函数。 例：主频为24.000MHz进行500毫秒中断的定时器函数 c void Timer0 Init(void) { TM0PS 0x0F; AUXR & 0x7F; TMOD & 0xF0; TL0 0xDC; TH0 0x0B; TF0 0; TR0 1; ET0 1; EA 1； } 注意！ 一定要有EA 1定时器中断才能正常开启。 需要在main函数中进行初始化： Timer0 Init(); 定时器执行函数 c void Timer0 Isr(void) interrupt 1 { //在这里编写需要执行的代码 } 定时器的周期调用 c u16 Count ms[3] {0, 0, 0}; u8 state1 1, state2 1, state3 1, state4 1; void Timer0 Isr(void) interrupt 1 { u8 i; for(i 0; i 300) { Count ms[0] 0; state1 !state1; P20 state1; P23 !state1; } if(Count ms[1] 600) { Count ms[1] 0; state2 !state2; P21 state2; } if(Count ms[2] 900) { Count ms[2] 0; state3 !state3; P22 state3; } if(state1 0 && state2 0 && state3 0) P24 0; else P24 1; } 定时器中断的进阶用法： 通过定义三个不同阈值的计数器，定时器每中断一次，计数器加一。 实现在300ms，600ms，900ms时分别中断执行不同任务 数码管与定时器 任务：按下KEY1，数码管开始计时(如00:00:00)， 按下KEY2，数码管显示年月日(如2024:12:11)， 按下KEY3，数码管计时暂停，再按一下KEY3又恢复计时 按下KEY4，数码管计时清零。 c while(1) { delay ms(20); if(P31 0){ state 1; while(P30 0); } if(P30 0){ state 0; while(P31 0); } if(P32 0){ if(state 1){ state 2; }else if(state 2){ state 1; } while(P32 0); } if(P33 0){ miao fen shi 0; while(P33 0); } switch(state){ case 0:seg date();break; case 1:seg count();break; case 2:seg count pause();break; } } 在while1中持续检测按键情况，哪个按键被按下则执行相对应的任务 数码管显示日期 c void seg(u8 loc, u16 num) { switch(loc) { case 1:P24 1;P23 1;P22 1;break; case 2:P24 1;P23 1;P22 0;break; case 3:P24 1;P23 0;P22 1;break; case 4:P24 1;P23 0;P22 0;break; case 5:P24 0;P23 1;P22 1;break; case 6:P24 0;P23 1;P22 0;break; case 7:P24 0;P23 0;P22 1;break; case 8:P24 0;P23 0;P22 0;break; } P0 SEG NUM[num]; delay ms(1); //延时的目的是为了让数码管有足够的时间显示当前的数字，避免显示闪烁 P0 0x00; //在显示下一个数字之前，先将当前的显示清除，避免串扰 } // 该函数用于在不同的数码管位置显示不同的数字，每个case对应一个数码管的位置 void seg date(void) { seg(1,2);seg(2,0);seg(3,2);seg(4,5); seg(5,0);seg(6,1);seg(7,0);seg(8,1); } 如何用数码管显示20250101 数码管显示计数器 c void Timer0 Isr(void) interrupt 1 { if(state 1){ miao ++; if( miao 59 ) { miao 0; fen++; if( fen 59 ) { fen 0; shi ++; if( shi 23 ) shi 0; } } } } 利用定时器中断进行时分秒计数 c void seg count pause(void) { seg(1,shi/10);seg(2,shi%10);seg(3,SEG NUM[16]);seg(4,fen/10); seg(5,fen%10);seg(6,SEG NUM[16]);seg(7,miao/10);seg(8,miao%10); } 将计数器显示在数码管上 PWM 任务：PWM驱动电机，按键K1按下，电机转速提高(一共三档)，按键K2按下，电机转速减慢，按键K3按下，电机停止 c while(1) { delay ms(20); if(P31 0) { Speed++; if(Speed 3) Speed 3; while(P31 0); } if(P30 0) { Speed ; if(Speed 一个普通的按键检测代码，按下对应按键执行对应速度并在数码管显示 通过定时器中断来控制电机的占空比（PWM功能） 先上定时器中断执行代码： c void Timer0 Isr(void) interrupt 1 { TL0 0x38; TH0 0xFF; counter++; counter% 100; if(counter 逐步分析： c TL0 0x38; TH0 0xFF; TL0 和 TH0 是定时器0的低8位和高8位寄存器。 这两行代码的作用是重新装载定时器0的初始值 0x38 和 0xFF 是定时器的初始值，具体数值取决于单片机的时钟频率和定时器的模式。这些值决定了定时器溢出的时间间隔 每当定时器溢出时，就会触发中断，执行这个中断服务例程。 c counter++; counter % 100; 用于记录定时器中断的次数，当等于100时清零 PWM逻辑 c if (counter 占空比阈值 这段代码的作用是根据计数器的值和占空比阈值来控制电机的状态： 如果 counter 小于 Compare ，则将 motor 设置为1（表示电机开启或输出高电平）。 如果 counter 大于或等于 Compare ，则将 motor 设置为0（表示电机关闭或输出低电平）。 通过调整 Compare 的值，可以改变PWM的占空比，从而控制电机的速度或功率 PWM原理 占空比 ：占空比是指在一个周期内，高电平时间与总周期时间的比例。例如，如果占空比为50%，则在一个周期内，高电平和低电平各占一半时间。 在这段代码中， counter 的范围是0到99，总共100个计数值。 Compare 的值决定了高电平的持续时间。例如： 如果 Compare 50，则高电平持续50个计数值，占空比为50%。 如果 Compare 20，则高电平持续20个计数值，占空比为20%。","summary":"AI8051U 32bit 单片机学习笔记。"},{"permalink":"https://zhr0529.cn/posts/stm32第一部分学习/","title":"STM32第一部分学习","content":"快速上手STM32外设 一、跑马灯 c int main(void) { delay init(); LED Init(); // 需要初始化用到的模块（delay，LED） while(1) { GPIO ResetBits(GPIOC,GPIO Pin 13); delay ms(100); GPIO SetBits(GPIOC,GPIO Pin 13); delay ms(100); } } GPIO ResetBits置零（低电平），GPIO SetBits置一（高电平） 控制LED亮灭 go to definition到LED Init内部一下 c RCC APB2PeriphClockCmd(RCC APB2Periph GPIOC, ENABLE); // 使能总线时钟 GPIO InitStructure.GPIO Pin GPIO Pin 13; // 初始化PC13引脚 GPIO InitStructure.GPIO Mode GPIO Mode Out PP; // 设置推挽输出 GPIO InitStructure.GPIO Speed GPIO Speed 50MHz; // 设置IO口速度GPIO Speed 50MHz（一般默认50MHz） GPIO Init(GPIOC, &GPIO InitStructure); // 把配置好的变量输入进去 以上是对GPIO InitTypeDef结构体的初始化设置 在led.c页面，在页面空白处右键，选择“toggle header”，进入led.h 可写一个宏定义： define LED PCout（13） PCout(13) 1; 这句就完全等于GPIO SetBits(GPIOC,GPIO Pin 13); PCout(13) 0; 这句就完全等于GPIO ResetBits(GPIOC,GPIO Pin 13); 这样宏定义之后，控制PC13的电平就可以写得更简便 头文件中 c ifndef LED H define LED H … … endif 这一段是固定的，照着写即可。 二、按键 c int main(void) { u8 a; delay init(); LED Init(); KEY Init(); //按键初始化 while(1) { a KEY Scan(0); if(a 1) { LED 0; } else if(a 2) { LED 1; } } } 变量a被KEY Scan函数的返回值赋了个值，如果是a 1就让灯亮，a 2灯灭，逻辑分析结束。 key.c: 屏幕截图 2025 03 30 170039 使能了PB4和PB5两个GPIO口，但GPIO模式配置成了GPIO Mode IPU 上拉输入模式 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 低电平 时，IO口可以测到明确的 低电平 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 高电平 时，IO口可以测到明确的 高电平 上拉输入的IO口在外部接这个IO口的电路（外部输入电路）为 高阻态 时，IO口可以测到明确的 高电平 key.h: 屏幕截图 2025 03 30 170719 GPIO ReadInputDataBit( ) 这个函数就是检测IO口电平的核心函数，比如检测B4口的电平是高还是低： if(GPIO ReadInputDataBit(GPIOB,GPIO Pin 4) 1)就可以简化为： if(KEY1 1) 并且GPIO ReadInputDataBit函数还能进一步简化： 就像第一节的PCout(13)一样： int a; a PBin(4); 这个a如果为1那PB4就是高电平，0就是低电平。 KEY Scan 函数 c u8 KEY Scan(u8 mode) { static u8 key up 1; if(mode) key up 1; // mode 0，不支持长按 if(key up && (KEY1 0 KEY2 0)) //这句就在判断是否有任何按键被按下：没 有按键被按下前，变量key up一直都是1，所以符合条件 { delay ms(10); //消抖延时 if(KEY1 0) { key up 0; return KEY1 PRESS; } else if(KEY2 0) { key up 0; return KEY2 PRESS; } } if(KEY1 1 && KEY2 1) { key up 1; } return 0; } 三、如何自己添加代码文件 创建相应模块文件夹，编写.h文件和.c文件代码（可复制粘贴） 配置编译器：点界面上方三色块，添加.c源文件； 点魔术棒 C/C++ Include Paths，添加.h头文件 在main函数添加新的头文件（ include \"key.h\"） 四、定时器中断 c int main(void) { NVIC PriorityGroupConfig(NVIC PriorityGroup 2); delay init(); LED Init(); TIM3 Init(20000 1,720 1); while(1) { delay ms(100); } } STM32的每个中断都有自己的抢占优先级和子优先级 NVIC PriorityGroupConfig(NVIC PriorityGroup 2); 设置中断优先级分组为分组2 定时器： TIM2，TIM3，TIM4这三个是通用定时器，TIM1是高级定时器。 可以认为定时器内部有个计数器，初始值是0，每过1ms，计数器+1，一直过了99ms，这时，定时器的计数器的值是99；紧接着到第100ms，计数器数到了100，但是计数器的值不是100，而是清零，然后继续从0开始数， 同时触发定时器中断 。 tim.c: 屏幕截图 2025 03 30 225527 TIM3 IRQHandler函数 —— 中断服务函数 （函数名是不能改的，可以在里面运行各种代码） TIM TimeBaseInitTypeDef TIM TimeBaseStructure; NVIC InitTypeDef NVIC InitStructure; 上面这两句分别是 定时器配置结构体 和 中断配置结构体 TIM GetITStatus函数，获取定时器相关中断的状态。 当一个中断状态是RESET时证明这个中断 还没 准备好触发； 当一个中断状态是SET时证明这个中断 已经 准备好触发； 自动重装载值arr & 预分频系数psc psc 作用： 对总线时钟的频率进行分频，分频之后的频率就是定时器计数频率 STM32F103C8T6最小系统板的STM32的系统总线时钟的频率是 72Mhz ，也就是72,000,000 Hz， 这个数值要记住！ 定时器的psc参数对系统总线时钟频率分频，psc 720 – 1，那意思就是720分频，也就是分频后的频率是之前的1/720，如下： 720 72000000 Hz ​ 100000 H 分频后的频率是100000Hz，也就是100KHz，也就是定时器3的计数器的计数频率是100KHz。 100KHz也就是一个周期0.00001秒，也就是定时器3的计数器每0.00001秒记一个数。 psc 720 – 1， 1是内部寄存器规定的：0是不分频，1是二分频，2是三分频，依次类推 arr 作用：规定 “数到多少就触发定时器更新中断” 例：规定让计数器从0数到100，1ms数一次。 这个“100”就是arr，arr 100。 所以自动重装载值arr的作用就是这个的计数器要记到多少才能进一次中断 arr 20000，那就是计数器计两万次才进一次中断，也就是0.00001 20000 0.2秒进一次中断。 屏幕截图 2025 03 30 231312 定时器的计数器随着时间计数，然后计数器达到arr值（TIMx ARR）时，触发定时器更新中断，然后计数器清零继续重新计数。 五、STM32标准库 STM32F10x FWLib 文件夹即为STM32标准库的库函数 “stm32f10x ”这个前缀，代表这套库函数可用于STM32F1的所有系列 “stm32f10x gpio.c” “stm32f10x gpio.c”就是操作gpio相关的库函数，比如之前用的： 屏幕截图 2025 03 31 230921 这些凡是操作GPIO的函数全是。 因为我们go to definition以上的任意一个函数，都会跳转到“stm32f10x gpio.c”里去。 “stm32f10x rcc.c” 屏幕截图 2025 03 31 231027 这句就是它的函数，RCC分配外设时钟。 “stm32f10x usart.c” 串口相关的库函数 “misc.c” Misc是Miscellaneous（各种各样的）的缩写，杂项文件，里面主要装的是NVIC相关文件。也就是中断优先级分配相关，以后做项目几乎必用中断，所以提前加。 以 用到单片机的什么外设就加什么库 的原则，这样可以节省单片机的内存。 六、PWM控制舵机 PWM: 新的参数： TIMx CCRx CCR寄存器的全称是CCR(Capture/Compare Register)的缩写，即：捕获/比较寄存器。 屏幕截图 2025 03 31 232005 PWM的频率与arr和psc有关 CNT CCR时，PWM是低电平 CCR寄存器有比较的功能, CCR寄存器确定了PWM的占空比 。占空比英文是“duty”： d u t y T I M A RR T I M CCR x ​ PWM的频率由arr和psc决定； PWM的占空比由CCR决定 舵机： 一般的舵机（特指模拟舵机）有三根线，VCC，GND和SIG（signal信号） 分别是电源线，地线和信号线，信号线需要接具有定时器PWM输出通道的引脚（ 复用 ） 代码： 屏幕截图 2025 04 01 143327 TIM1 PWM Init： 先看TIM1 PWM Init： 两个形参是arr自动重装载值和psc预分频系数 舵机的PWM周期必须为20ms（50Hz），所以先配置arr和psc这两个参数，让PWM的周期正好是20ms。 设置自动重装载值arr 20000，预分频系数psc 72 则定时器自动装载一次的周期为：1/(72000000/72) 20000 0.02s 20ms，符合要求。注意这里，arr设置得越大，PWM占空比的精度越高，因为： d u t y T I M A RR T I M CCR x ​ 屏幕截图 2025 04 01 143943屏幕截图 2025 04 01 143951 2 比定时器中断少了中断服务函数TIMx IRQHandler，也没有了NVIC的相关配置。main函数内也少了中断优先级分组，因为生成PWM用不到定时器中断。 多了新结构体TIM OCInitTypeDef TIM OCInitStructure TIM OCInitTypeDef 是 定时器输出通道配置结构体 ，带OC字样的都是其相关的配置，OC（Output Channel），输出通道。 看代码，首先： 屏幕截图 2025 04 01 144711 三个结构体：GPIO配置、定时器配置、定时器输出通道配置。 两个RCC外设时钟使能：使能TIM1和GPIOA的外设。TIM1和GPIOA都在APB2总线上 接着相应的IO使能： 屏幕截图 2025 04 01 144943 A8 A11，TIM1的四个PWM通道。 GPIO Mode AF PP 复用推挽输出模式 推挽输出和复用推挽输出的不同在于： 推挽输出是GPIO自己通过函数控制电平高低，比如PCout(13) 1; 复用推挽输出是GPIO把电平高低的控制权给了外设，这里就是给了TIM1，GPIO不管了，电平高还是低让TIM1自己去控制。 定时器的参数配置 输出通道参数配置 TIM OCMode，配置为PWM模式1：TIM OCMode PWM1 PWM模式1：递增计数时，当TIMx CNT（计数值） 有效电平 ，否则为 无效电平 。 PWM模式2：递增计数时，当TIMx CNT（计数值） 无效电平 ，否则为 有效电平 。 屏幕截图 2025 04 02 134915 输出极性高： 有效电平为高电平 输出极性低： 有效电平为低电平 实际输出的电平是由输出极性和电平是否有效共同决定的 TIM OutputState是输出模式是否开启，开启才能输出PWM，填TIM OutputState Enable。 TIM Pulse，意思是TIM CCRx的初始值，一般都统一设为0。 然后四句就是输出通道1 4的配置参数上传函数。 屏幕截图 2025 04 02 135424 以上是可以不管的，写上即可 最后一句 TIM Cmd(TIM1, ENABLE) ,开启定时器 TIM SetComparex 屏幕截图 2025 04 02 140019 TIM SetCompare1(TIM1,500)的意思就是设置定时器1的通道1的CCR值为500。 TIM SetCompare2(TIM1,500)的意思就是设置定时器1的通道2的CCR值为500。 占空比公式： d u t y T I M A RR T I M CCR x ​ 所以TIM SetCompare1(TIM1,500);这句话的作用就是把TIM1 CH1的占空比改为：500/20000 2.5%，对应舵机的0°位置； 同理，后面TIM SetCompare1(TIM1,2500)就是把占空比改为： 2500/20000 12.5%，对应舵机的180°位置 2025.4.2，还是没能明白占空比与舵机角度之间的对应关系，明白了回来还愿 七、仿真&实物debug 看教程即可。 八、OLED显示 先看代码： 屏幕截图 2025 04 02 162732 OLED Clear 作用：清屏 OLED ShowString（x，y，内容，字体大小），xy代表坐标值 x的范围（0,127） y的范围： 如果字体高度为8，行数的范围是0到7。 如果文字高度为16，那只能塞下四行，对应的行数就只有0,2,4,6这四个值。 内容：注意（u8 ） 字体大小：8 / 16 OLED Init函数 屏幕截图 2025 04 02 163421 PA6，PA7接口可以随意更换：GPIO初始化换成任意其他的两个IO后，在oled.h更改宏定义 sprintf函数 利用sprintf可以轻松处理一个的字符串， 让一些变量值插入字符串 。 c sprintf(text,\"you have %d yuan!\", your money); 此函数的作用就是将your money变量的值插入字符串，并且将加工好的字符串赋值给text变量，text[20]是一个字符串。 之后就可以使用OLED屏幕显示信息了 九、红外寻迹传感器 红外传感器原理： 红外发射管不断发射红外线，红外接收管不断接收是否有红外线反射回来。 当红外线没有被反射回来，红外接收管关断，此时模块的输出端为 低电平 。 当红外线被反射回来，红外接收管导通，此时对应的输出端就是 高电平 。 即： 如果某个传感器检测到 白色 ，那就输出 高电平“1” ； 如果某个传感器检测到 黑色 ，那就输出 低电平“0” ； 看代码： 屏幕截图 2025 04 02 231445 extern： 外部调用 ，定义一个 全局变量 不在任何一个函数里，添加一句：extern int track error; track Init: 屏幕截图 2025 04 02 231924 初始化了五个GPIO， 浮空输入 ，用于检测循迹传感器的OUT1 OUT5。 浮空输入完全取决于外部输入信号，外部输入是1那就是1，外部输入是0就是0，外部输入是高阻态那就电平不确定。 track.h: 屏幕截图 2025 04 02 232117 GPIO ReadInputDataBit的作用就是读取一个IO口电平的高低，高电平即1，低电平即0 define OUT1 GPIO ReadInputDataBit(GPIOA,GPIO Pin 4) define OUT1 PAin(4) 以上两句是功能完全一样的，两种不同写法。 十、超声波测距 & 定时器计时 1. 会使用超声波测距 2. 会使用定时器进行计时 3. 提升C语言算法功底 HC SR04 VCC： 电源线，3 5.5V宽电压输入 GND：地线，连接单片机的接地（GND） TRIG： 触发 控制信号输入 ECHO： 回响 信号输出 工作原理： (1)TRIG口 触发测距，给TRIG口最少 10us 的高电平。 (2)模块内部自动发送 8 个 40khz 的方波，自动检测是否有声波返回；（这一步我们了解即可，这是超声波传感器内部在干的事，无需我们操作） (3)若有信号返回，经过模块内部运算后，通过 ECHO口 输出一个高电平， 高电平持续的时间就是超声波从发射到返回的时间。 测量距离 (高电平时间 声速)/2 所以只需要： 1. 给TRIG口一个10us以上的高电平，去触发测量任务。 把随便一个IO口搞成推挽输出，接到TRIG上去，然后电平置高，delay us(11)，延时十微秒以上即可 2. 传感器的测量任务进行完之后，ECHO会发出一段时间的高电平，这段高电平的持续时间经过换算就能得到待测距离。 需要测量高电平持续时间 1. 测量高电平： 使用 GPIO ReadInputDataBit 函数不断检测ECHO口电平，一旦电平变化就开始计时。 2. 测量高电平的持续时间 在高电平到来瞬间打开定时器开始计数，等到高电平结束关闭定时器。 CNT清零的同时会进定时器更新中断，我们可以在定时器中断里记录，每进一次定时器中断，记录清零次数+1。 再就是把ARR的值设置得尽量大一点，降低定时器更新中断的触发频率，比如arr 10和arr 65535，如果arr 10，程序会不停进入定时器更新中断以至于大量资源被占用，main函数while(1)的东西可能都执行不了了。 看代码： main: 屏幕截图 2025 04 03 232633 选用定时器4。 psc 0 1 1，也就是计数频率是72MHz，不分频，为了让计数器的计数精度足够高 arr 65535，arr能取到的最大值就是65535。 HC SR04 Init: 屏幕截图 2025 04 03 232459 Trig口推挽，echo口浮空 置低trig口，保持低电平，因为trig置高是开始测距的信号 Senor Using 屏幕截图 2025 04 08 223418 第一句 extern int overcount 来自TIM4的中断服务函数，代表TIM4 CNT清零的次数。 while1 中： 前三句用于操作trig口触发超声波开始测距 while(GPIO ReadInputDataBit(GPIOA,GPIO Pin 12) RESET); 只要PA12是低电平，循环就一直进行 也就是PA12（ECHO）一旦变成高电平，马上跳出这个循环 然后给相关变量清零： overcount 0; TIM4 CNT 0 （给TIM4的计数器清零） 再TIM Cmd(TIM4,ENABLE);开启定时器，定时器计数器就会开始递增，CNT到最大值清零并触发中断，中断内通过overcount记录清零次数。 再看后半段程序： 变量i记录第几次测距，测五次取平均值（ 均值滤波 ） while(GPIO ReadInputDataBit(GPIOA,GPIO Pin 12) SET); 一旦检测到echo口变低，说明返回的超声波已经接收到了，就跳出循环 TIM Cmd(TIM4,DISABLE);//关闭定时器，计数器也会停止计数。 使用TIM GetCounter函数得到定时器4计数器目前的值，这句代码等同于：tim TIM4 CNT; length计算公式： tim+overcount 65536就是定时器这段时间总共计了多少个数 高电平时间 总计数值 计数周期 (tim+overcount 65536) ( 1/72000000) 十一、直流有刷电机控制 L298N L298N芯片内部集成有两个 H桥电机驱动电路 屏幕截图 2025 04 10 081014 中间这个圆圈代表 直流电机 。 已知 S8050 型号的三极管是 NPN型三极管 ， S8550 型号的三极管是 PNP型三极管 。 (对于NPN型三极管，IN端给高电平开关导通；给低电平开关关断。PNP型三极管完全相反) 分析： 当IN1 1，IN2 0时： Q1,Q4导通，Q2,Q3关断，电流流向如下图，定义此时电机正转： 屏幕截图 2025 04 10 081425 当IN1 0，IN2 1时：电机电流与上图方向相反，所以电机反转。 屏幕截图 2025 04 10 081530 控制转速： 把IN1和IN2换成两路PWM控制 比如当IN1占空比是0，IN2占空比是100%时，电机全速转 但是当IN1占空比是0，IN2占空比是50%时，电机以全速的1/2转 L298N引脚 屏幕截图 2025 04 10 081913 ‘’通道A使能”、“通道B使能”、“板载5V使能”三个跳线帽都不能拔掉 电机A/B通道无法使能的话，对应通道的电机就不会转; 板载5V跳线帽拔掉，5V输出就没有了 用12V锂电池供电 L298N电路 屏幕截图 2025 04 10 082354 右边蓝色的八个二极管作用： 四个钳位电路，将连接电机输出所有通道的电压值钳位在0 Vss（0 12V之间） 因为电机是一种特别容易产生干扰的电气设备， 电动机反过来就是发电机 ，如果不通电，手摇电机，就会产生不小的 反电动势 ；如果发生电机急停等，电路电压就会产生很大的尖峰等干扰，电压一下会波动高于12V，如果不做钳位，可能就会对L2998N芯片等产生损害。 代码分析： 屏幕截图 2025 04 10 082656 使输出PWM赫兹为24kHz（芯片推荐的PWM频率） 注意不同的电机有不同的额定电压 额定电压是12V的电机，用12V电池供电，那PWM占空比就可以开到100%，让电机两端电压在12V。 但是如果电机额定电压是8V，大于8V会烧坏电机。所以如果还是使用12V电池供电，PWM控制占空比最大只能开到66.6%。 十二、电机编码器测速 编码器 首先我们要知道， 电机是电机，编码器是编码器 ，这两个东西是各自独立的 13线霍尔款编码器原理如下图： 屏幕截图 2025 04 10 083844 1. 上图这个刻度盘安装在电机转轴上，随着电机一起旋转； 2. 刻度盘上有一条条等间距的缝隙； 3. 右边是个小红外灯（发光元件），一直向左边的光敏元件发射激光，但是刻度盘会把光挡住，激光只能穿过刻度盘的一条条缝隙射给光敏元件，如下图： 屏幕截图 2025 04 10 084120 刻度盘一直转，光敏元件就能得到 一个循环被照和不被照 的 类似PWM的信号 ，得到一个如下图A相那样的波： 屏幕截图 2025 04 10 084245 如果说刻度盘上共有25个缝隙。那么我们可以统计一段时间内 A波形所有高电平的数量 来得出刻度盘一共转了多少个 缝隙 ，也就是多少个 刻度 。 并且能算出刻度盘转到了什么位置 。 比如我们测得，10秒内，高电平共来了67个，那么也就是这个电机十秒转了67/25 2.68转，也就是十秒内的平均转速是0.268r/s 这样我们就实现编码器测速了。 但是这样有个问题 ，不知道盘转的方向，正转反转都是这样一个波！而且如果电机就来回正反转打哆嗦，这样始终就是同一个缝隙来回被光穿过，这样好像也能得到A波，这咋办呢？ 解决方法就是把刻度盘的 内圈 再刻上一圈与原缝隙错开90°的缝隙，然后再拿一个光敏元件和发光元件！如下图：屏幕截图 2025 04 10 084644这样就会得到如下两条锯齿波： 屏幕截图 2025 04 10 084733 A和B两组波形相位差是90º，可以按照下图判断出旋转方向： 屏幕截图 2025 04 10 084827 并且再进一步：通过观察上面AB相的波我们发现，圆盘向某个方向一直旋转，AB两相就是这四个状态周而复始。 屏幕截图 2025 04 10 085148 由于该编码器为13线，也就是线数为13ppr，也就是刻度盘上每相都有13个 缝隙 ； 电机每转一圈，A或B相的脉冲数都是13；也就是电机转一圈A相和B相就分别有13个脉冲。 而这个电机的减速器减速比为1：30： 也就是电机转30圈，输出转轴转一圈；编码器轴是连接在里面的电机轴上的，不是接在输出转轴上的。 综合 编码器线数 和 减速器减速比 ，得出最外面的电机输出转轴转1圈，里面的霍尔编码器A或B相会有 13 90 390 个脉冲 。 看代码： 屏幕截图 2025 04 10 085540 使用了三个定时器 先看TIM4 ENCODE Init： 编码器正交解码用的定时器4。 多了一个IC结构体。（input channel， 输出通道）屏幕截图 2025 04 10 085754屏幕截图 2025 04 10 085759 以上代码只需要复制黏贴就好了，除了第一句函数的第一个参数 第一句是一个非常重要的库函数： 编码器模式设置函数 。 TIM EncoderMode TI1代表仅TI1计数； TIM EncoderMode TI2代表仅TI2计数； TIM EncoderMode TI12代表TI1 TI2都计数。 TI1就是输入通道1，就是PB6，就是编码器A相； TI2就是输入通道2，就是PB7，就是编码器B相。 这个参数的意思具体看下面这两个图：屏幕截图 2025 04 10 090236 我们使用的模式就是TI12都计数，也就是：屏幕截图 2025 04 10 090418观察上面三个图可知，只有正确的连续四个AB状态才会成功进行下一次计数， 一旦有一个毛刺，发现顺序不对了就退回到上次顺序正确时候的计数值。 —————————————————————————————————————————— 再来看定时器的配置：屏幕截图 2025 04 10 090609 为了保证最高的采样精度，psc不分频，72MHz。 arr的值是综合了编码器线数、减速器减速比和通道采样方式的一个值： 13ppr的编码器，1:30的减速器下，电机外转轴转一圈A或B相会有390个脉冲，但并不是A或B相来一个脉冲计数器计一次数，我们设置了TI12都计数，那就是每个AB相的正交脉冲组合都算作一次计数： 四个组合都算作计一次数，也就是全部的arr需要390 4 1560。 最后得出 电机输出转轴转一圈会有1560个状态 ，定时器的计数器计1560个数。 定时器4的中断服务函数： 屏幕截图 2025 04 10 090908 每当更新中断（计数器溢出）来临，要先判断定时器是向上溢出还是向下溢出： “TIM4 CR1 4&0x01”这一堆就是调用了定时器4相关寄存器的相应位， 结果 0就是向上溢出，结果 1就是向下溢出。 向上溢出就是计数器计数到了1557、1558、1559，然后清零，0。 向下溢出就是4,3,2,1,0，然后CNT arr，1559,1558,1557。 定时器3的中断服务函数： 屏幕截图 2025 04 10 091205 speed就是速度，单位是转每秒 TIM4 CNT 的值就是定时器4计数器当前时刻的值，范围是0 1559。 Last num变量存着上次采样时TIM4 CNT的值，last count存着上次采样的圈数，这次的和上次的一减，就是△X，△X/△t就是速度，△t是通过定时器3的psc和arr决定的，我们配置的是0.1秒，△t 0.1s。 用到的三个定时器都是用作了不同的功能： 定时器1用于生成PWM控制电机。 定时器3用于多线程任务，计算电机转速。 定时器4用于编码器正交解码。 十三、电机PID闭环控制 先看代码： 屏幕截图 2025 04 23 172219 1 定时器1：生成PWM 定时器2：生成0.1s的定时器中断PID计算 定时器3：生成0.1s的定时器中断电机编码器测速 定时器4：正交解码 PID算法的原理和用法 PID（Proportional Integral Derivative）（比例 积分 微分） 算法是一种常用的反馈控制算法，广泛应用于自动控制系统中。它通过测量系统的误差（目标值与实际值之间的差异）来调整控制器的输出，以使系统的响应满足特定的要求。 比例项P PID（位置式）： u P ∗ e + I ∗ ∫ e + D ∗ e ∙ P、I、D三个参数是确定不变的：也就是在自动控制中，这三个参数最初设定是多少就永远是多少； 上式中，u就是output，输出值； e就是error，误差值，作为输入。 比例控制P越大，接近目标值的速度越快，但是提升控制速度的代价是 超调 和 震荡 会随着P的增大而增大；P过大会使系统失稳。 屏幕截图 2025 04 23 174358 1 微分项D 这时候就要引入D了，微分： u P ∗ e + D ∗ e ∙ e上加一个点代表是e的微分。微分 离散化 之后就是e e last，e是这一次的误差，e last是上一次的误差，于是公式就变成了： u P ∗ e + D ∗ ( e − e l a s t ​ ) P ∗ e + D ∗ Δ D项相当于一个阻尼项，可以让控制更加平缓，不会有过猛过冲的情况，同理，D项最重要的作用就是消除超调量屏幕截图 2025 04 23 174422 1 积分项I u P ∗ e + I ∗ ∫ e + D ∗ e ∙ 积分项I就是专门来消除静态误差的，加入积分项，PID就全了。 跟前面一样，把公式离散化： u P ∗ e + I ∗ ∑ e + D ∗ Δ e 但是积分项I过大也会引起超调增大以及失稳，而且I过大会导致系统滞后，也就是调节效果慢半拍，降低系统快速性。 位置式PID和增量式PID 连续的PID公式： u P ∗ e + I ∗ ∫ e + D ∗ e ∙ 将其离散化，得到离散位置式PID公式： u ( k ) P ∗ e ( k ) + I ∗ i 1 ∑ k ​ e ( i ) + D ∗ [ e ( k ) − e ( k − 1 ) K代表第k次PID调节，u(k)就是本次的输出值。 增量式PID： 增量的意思就是本次输出量和上次输出量的差值，比如第k次PID： u ( k ) P ∗ e ( k ) + I ∗ i 1 ∑ k ​ e ( i ) + D ∗ [ e ( k ) − e ( k − 1 ) 那上次就是k 1次PID： u ( k − 1 ) P ∗ e ( k − 1 ) + I ∗ i 1 ∑ k − 1 ​ e ( i ) + D ∗ [ e ( k − 1 ) − e ( k − 2 ) 两式一减就是增量式： Δ u ( k ) u ( k ) − u ( k − 1 ) Δ u ( k ) P ∗ [ e ( k ) − e ( k − 1 )] + I ∗ e ( k ) + D ∗ e ( k ) − 2 ∗ e ( k − 1 ) + e ( k − 2 ) △u(k)是输出值的增量， 也就是上一次的输出值加上△u(k)就是这一次的输出值。 两种PID的应用场合： （1） 位置式PID控制器 适用于需要快速响应和精确控制的场景。比如，在电机控制中，需要控制电机的转速或位置，使其能够快速响应并准确控制，这时可以使用位置式PID控制器。又如，在温度控制中，需要控制温度的精度和稳定性，这时也可以使用位置式PID控制器，同时位置式PID中，I项是会产生滞后，增加准确性的作用，为了系统的快速性，有可能I项直接不用，只用PD控制。 （2） 增量式PID控制器 适用于需要较强稳定性和对测量噪声较高的场景。比如，在机器人控制中，需要控制机器人的运动轨迹，这时可以使用增量式PID控制器，因为机器人在运动过程中，往往受到各种干扰，如摩擦力、惯性力等，增量式PID控制器对这些干扰有较强的稳定性；又如，在液位控制中，需要控制液位的高度和稳定性，这时也可以使用增量式PID控制器，因为在液位测量中，测量信号通常存在一定的噪声和波动，增量式PID控制器可以较好地抵抗这些干扰。 位置式PID代码： C include \"pid.h\" static float err last 0; //上次的误差 static float err 0; //本次的误差 static float Integral err 0; //累计的误差 static float P 0;// //P static float I 0;// //I static float D 0;// //D float a m 0.0; //输出值 float pid control(float speed) { err (aim v now v); //获得本次误差 Integral err + err; //获得累加误差 a m P err + I Integral err + D (err err last); //位置式PID公式 a m a m 10000?a m: 10000; return a m; //输出a m } a m a m 输出限幅 很重要 a m a m 10000?a m: 10000; 增量式PID代码： C include \"pid.h\" static float err last last 0; //上上次的误差 static float err last 0; //上次的误差 static float err 0; //本次的误差 static float P 0; //参数P static float I 0; //参数I static float D 0; //参数D static float off cal 0; float a m 0.0; //输出值 float pid control(float speed) { err last last err last; //更新上上次的误差 err last err; //更新上次的误差 err (aim v now v); //更新本次误差 off cal P (err err last) + I err + D (err + err last last 2 err last); //增量式PID的公式 a m+ off cal; //得到输出值 a m a m 10000?a m: 10000; return a m; //输出a m } 电机速度闭环控制及调参方法 1. 把P、I、D三个参数都调成0 2. 先调P，P从0开始缓慢增大，比如先让P 0.1，观察速度变化，一旦电机一上电，速度超过了目标值，比如2.5r/s，说明有超调了，这时候快速性已经没问题了。 3. 然后慢慢增大D，增加阻尼，减小超调，稍微增大D，观察速度变化，让电机上电后快速稳定在2r/s，即参数调试完成。 4. 速度闭环控制中，积分项I的作用不大，因为发现并没有静态误差，就没有调积分项。 十四、Ozone变量可视化 看教程即可。 十五、I2C底层通信 [SPI、UART、RS232、RS485、IIC 通信总线协议 i2c.c： 屏幕截图 2025 04 24 175723 引脚的初始化，两个推挽输出。SCL是时钟信号，STM32是主机，负责发出时钟信号，所以IO模式使用推挽输出。 i2c.h： 屏幕截图 2025 04 24 180116 直接控制了STM32的寄存器，没有任何库函数 位操作 STM32F1xx的中文参考手册: 屏幕截图 2025 04 24 180512 32位寄存器，管GPIO低八位的模式，比如是GPIOA，那就管着GPIOA.0 – GPIOA.7这八个脚。 & ^ 这六个属于 位运算符 ， 分别代表“与运算”，“或运算”，“左移”，“右移”，“按位取反”，“按位异或”。 GPIOA CRL& 0XFF0FFFFF： 0XFF0FFFFF 十六进制转二进制为： “1111 1111 0000 1111 1111 1111 1111 1111” GPIOA CRL& 0XFF0FFFFF也就是GPIOA CRL GPIOA CRL&0XFF0FFFFF 对比上图，也就是 把GPIOA CRL寄存器的23 20位全部置0 ，也就是让5号口的所有配置全部置0 GPIOA CRL (u32)8 1000对应着上/下拉输入。 具体是上拉还是下拉，用PAout(5)这个函数决定 上拉输入的IO口在外部接IO口的电路（外部输入电路）为低电平时，IO口可以测到明确的低电平； 上拉输入的IO口在外部接IO口的电路（外部输入电路）为高电平时，IO口可以测到明确的高电平； 上拉输入的IO口在外部接IO口的电路（外部输入电路）为高阻态时，IO口可以测到明确的高电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为低电平时，IO口可以测到明确的低电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为高电平时，IO口可以测到明确的高电平； 下拉输入的IO口在外部接IO口的电路（外部输入电路）为高阻态时，IO口可以测到明确的低电平； IIC通信函数： 1. 开始位 2. 结束位 3. 主机回应从机ack 4. 主机回应从机nack 5. 主机等待从机回应 6. 主机发送一个字节 7. 主机接收一个字节 一、开始位 屏幕截图 2025 04 24 182557 开始位：SCL保持高电平，SDA由高电平变为低电平后，延时( 4.7us)，SCL变为低电平。为啥说了要大于4.7us，但是要延时2us呢？ 因为代码运行也需要一定时间，加上代码运行时间即可大于4.7us。 二、停止位 屏幕截图 2025 04 24 182806 停止信号：SCL保持高电平。SDA由低电平变为高电平。 IIC信号在数据传输过程中，当SCL 1高电平时，数据线SDA必须保持稳定状态，不允许有电平跳变，只有在时钟线上的信号为低电平期间，数据线上的高电平或低电平状态才允许变化。 SCL 1时 数据线SDA的任何电平变换会看做是总线的起始信号或者停止信号。 三、应答信号和不应答信号 屏幕截图 2025 04 24 183156 应答信号：主机SCL拉高，读取从机SDA的电平，为低电平表示产生应答。应答信号为低电平时，规定为有效应答位（ACK，简称应答位），表示接收器已经成功地接收了该字节； 应答信号为高电平时，规定为非应答位（NACK），一般表示接收器接收该字节没有成功。 四、等待从机应答 屏幕截图 2025 04 24 183406 1 五、主机发送一个字节 屏幕截图 2025 04 24 183516 六、主机读取一个字节 屏幕截图 2025 04 24 183712 十六、逻辑分析仪 看教程即可。 十七、MPU6050（陀螺仪/加速度计） 注意！使用MPU6050时需要水平放置！不要把芯片竖着放，会导致芯片内部的陀螺仪初始化不成功！ MPU6050的本质是一个IIC通信的传感器模块，也就是一个传感器： MPU6050 陀螺仪+加速度计+数字运动处理器DMP（digital motion processor） 陀螺仪可以测量以空间坐标系x、y、z轴为转轴的三轴角速度；加速度计可以测量x、y、z三轴方向的加速度；3轴+3轴 6轴，所以是六轴运动处理传感器。 屏幕截图 2025 04 26 142914 只需要接VCC、GND、SCL、SDA四个脚即可 传感器的作用： 获取当前的三轴角速度和三轴加速度。 看代码： 屏幕截图 2025 04 26 143245 IIC通信接口的初始化，随便两个口就行 MPU6050初始化，DMP初始化，不需要看，只需要会用 只需要在合适的地方添加Read DMP函数即可，为读出：三轴姿态角，三轴角速度，三轴加速度 三轴姿态角就是Pitch、Roll、Yaw，俯仰角，横摆角，航向角 三轴角速度就是gyro[0]、gyro[1]、gyro[2] 三轴加速度就是accel[0]、accel [1]、accel [2] 每次调用Read DMP函数，角速度和加速度的相关变量就自动赋值了，我们引用下面这个头文件之后直接用就行 屏幕截图 2025 04 26 144016 十八、完成平衡小车 放手去做！","summary":"STM32 外设学习笔记：GPIO、定时器、串口、ADC 与 DMA。"},{"permalink":"https://zhr0529.cn/posts/通信总线协议/","title":"SPI、UART、RS232、RS485、IIC 通信总线协议","content":"名词解释 串/并行 屏幕截图 2025 03 30 100825 特点： 数据逐位传输，传输线少，长距离传输时成本低，但数据的传输控制较复杂。按照实现数据同步的方式，可以分为同步串行和异步串行两种。 屏幕截图 2025 03 30 100931 特点： 多位数据同时传输，传输控制简单，传输速度快，但是在长距离传输时硬件成本较高 同/异步 同步总线 ：指互联的部件或设备均通过统一的时钟进行同步，即所有的互联的部件或设备都必须使用同一个时钟（同步时钟），在规定的时钟节拍内进行规定的总线操作，来完成部件或设备之间的信息交换。 异步总线 ：指没有统一的时钟而依靠各部件或设备内部定时操作，所有部件或设备是以信号握手的方式进行，即发送设备和接受设备互用请求（request）和确认（acknowledgement）信号来协调动作，总线操作时序不是固定的。因此，异步总线能兼容多种不同的设备，而且不必担心时钟变形或同步问题使得总线长度不受限制。 全双工/半双工 img 波特率 串口异步通讯中由于没有时钟信号，所以两个通讯设备之间需要约定好波特率，即每个码元的长度，以便对信号进行解码。 波特率是指每秒钟传送二进制数码的位数，以bit/s（bps）为单位。 常用的波特率有： 9600、 19200、 38400、 57600和115200； 波特率为115200， 表示每秒传输115200位，且每一位数据在数 据线上持续时间为Tbit 1/115200 ≈ 8.68us。 UART帧串口通信 屏幕截图 2025 03 30 101129 串行，全双工，异步通讯 空闲时，RXD和TXD默认高电平 数据起始位为低电平 串口发送数据过程中，低位在前，高位在后 串口数据一般为8位 串口柯选择是否开启数据校验 校验位 ：单个字节数据校验，对数据中高低电平个数进行计数 常见的有奇偶校验 停止位：高电平，一般有 1，1.5，2位 串口传输速率：波特率，表示1秒传输数据位数。 ​ 想通电平情况下，如低电平高电平。通过掐时间确认数据为01还是0011 累计误差消除 ： ​ 两个单片机通讯，波特率可能会存在一定误差，如果单个数据字节长度过长， ​ 会导致误差累加，严重时会导致数据错误。所以规定串口传送的字符数据位为5～8位。 硬件连接： ​ 交叉连接，单片机A的TXD与单片机B的RXD相连，单片机A的RXD与单片机B的TXD相连 屏幕截图 2025 03 30 101255 Exynos4412下的UART控制器 1. 多通道支持 ：支持多个 UART 通道，允许同时进行多个串口通信。 2. 高数据传输速率 ：支持高达 3 Mbps 的数据传输速率，适合高速通信需求。 3. 可配置参数 ：支持波特率、数据位、停止位和校验位等参数的配置，灵活适应不同应用。 4. FIFO 缓存 ：内置 FIFO 缓存，减少 CPU 中断频率，提高数据处理效率。 5. 中断支持 ：支持多种中断源，如接收中断、发送中断等，便于高效处理数据。 6. 硬件流控制 ：支持 RTS/CTS 硬件流控制，确保数据传输的可靠性。 7. 调试功能 ：提供调试功能，方便开发和调试串口通信。 8. 兼容性 ：与标准的串行通信协议兼容，易于与其他设备连接。 RS232、RS485协议 物理协议，依赖串口 抗干扰能力差，通信距离短 IIC总线协议 SCL低电平决定发射器发送数据，高电平接收器接收数据 SPI通信协议 高速：相对I2C，串口 SPI （同步全双工）协议是由摩托罗拉公司提出的通讯协议(Serial Peripheral Interface)，即串行外围设备接口，是一种高速全双工的通信总线。它被广泛地使用在 ADC、 LCD 等设备与 MCU 间，要求通讯速率较高的场合。","summary":"串行通信基础与常用通信总线协议学习笔记。"},{"permalink":"https://zhr0529.cn/posts/旅游攻略合集/","title":"（杂）旅游攻略合集","content":"此帖为记录近年自己旅游有意识的进行规划的旅游路线，给时光以生命。也为会来浏览我个人博客的人们一些旅游路线参考。 天津 时间：2023.6.29 7.1 day 1 中午到达，晚上去德云社看了相声，200r/人，相当不错 day 2 乘坐环城巴士游览，路线：天津之眼——大悲禅院——金刚桥——三岔河口——古文化街——官银号——鼓楼商业街——南市食品街——北安桥——津门津塔——天津规划展览馆——意大利风情街——大沽桥——解放桥——世纪钟广场——津湾广场——天津站——袁世凯故居——冯国璋故居——玻璃桥——李叔同故居——海河亲水平台——狮子林桥——望海楼教堂——引滦入津纪念碑——天津之眼 然后晚上去打球，感受天津强度😝 day 3 中午去滨江道步行街，其实感觉每个地方的步行街都大同小异。去天津狗不理包子总店吃了午饭，相当一般。 天津景点之一瓷房子，没什么好看的。晚上坐了天津之眼摩天轮，不愧是第一大水上摩天轮，确实不错。 广州 澳门 香港 时间：2023.7.8 7.13 day 1 下午到达，晚上：广州塔 海心桥 在广州塔下的公园听小年轻唱粤语歌 day 2 参观越秀王墓，没什么好看的除了下去墓葬实地内部参观有意思 晚上逛了逛商场 day 3 前往澳门，从珠海的横琴口岸乘坐酒店的发财车入住金沙度假村。晚上澳门的叔叔请我们吃了葡萄牙菜。晚上坐轻轨游览： 澳门银河酒店 永利皇宫 新濠天地 摩铂斯 伦敦人 其实来澳门参观这些富丽堂皇的酒店建筑就很有意思 day 4 去酒店的游泳池玩了一圈 接着去参观巴黎人酒店的埃菲尔铁塔 威尼斯人 新濠影汇 乘坐永利皇宫喷泉缆车 坐酒店的港澳快线途经港珠澳大桥前往香港 day 5 香港迪士尼一日游。买了优速通一天刷完🥰🥰 我想如果我是小时候来肯定会很开心，长大了来玩也很开心 day 6 在香港市区玩了一天，早上出门就遇到了传说中的富贵雪糕车，但好像没有很好吃。去维多利亚港看了江景 铜锣湾 中环 坐天星小轮回到江对岸 坐香港巴士到故宫文化博物馆附近的江滨道坐着看维多利亚港夜景 南京一日游 时间：2023.12.2 租了一辆电动车游览：梧桐大道 明孝陵 中山陵 美龄宫 晚上坐地铁去逛了秦淮河和南京夫子庙，人山人海。 没预约到南京大屠杀死难者纪念馆很可惜 泉州 时间：2024.1.22 2024.1.23 day 1 清源山老君岩 天后宫 浔埔古镇（都是贝壳建筑的小镇，来这才知道这里是簪花真正的起源地） 郑成功雕像 钟楼 开元寺（一条路上都是好吃的😍） day 2 清净寺 关岳庙 洛阳桥 苏延玉故居 泉州海运文化博物馆 崇武古城 南山广化寺 绍兴一日游 时间：2024.4.4 鲁迅故里 百草园 三味书屋 沈氏园（人山人海） 兰亭 这个可以，人很少环境优美。就是离市区比较远 上海（鸣谢hkl，lsh） 时间：2024.6.7 6.8 day 1 入住徐家汇 去静安寺，顺便看了亚洲最大的Apple店，好像也没多大 南京路步行街（下午茶吃了莱莱小笼，神中神，除了贵没有任何缺点） 和平饭店 外滩 坐船到陆家嘴 到上海中心楼顶看了稳定器灯光秀（。） 花园饭店吃晚饭 最后又到外滩边散步，来上海就是要city walk对吧🤭 day 2 武康大楼 宋庆龄故居 上海中心翠园吃午饭 四行仓库 中共一大旧址&会址纪念馆 武汉 时间：2024.8.1 8.4 day 1 古德寺（寺庙偏欧洲中世纪风格很好看，很多鸽子很可爱） 解放公园 （没什么好逛的） 黎黄陂路（商业步行街） 咸安坊 江汉路步行街（人山人海） day 2 湖北省博物馆（大家都约到了门票，赞），主要是看编钟和越王勾践剑 武商梦时代吃午饭 宝通寺 首义公园&博物馆（比较有革命意义） 黄鹤楼（晚上门票加钱的灯光秀是👎） 户部巷（很多美食） day 3 归元寺（就像每个地方都有的普通寺庙） 古琴台（伯牙绝弦的地方，挺有意义的） 爬龟山 晴川阁 长江观景第一台 day 4 东湖风景区（可惜没坐到滑道） 武汉大学 凌波门东湖观景点 光谷广场 霞浦 时间：2024.8.10 8.12 day 1 三都澳旅游景区。这个没啥意思 入住东壁村民宿 day 2 三点起来看巴黎奥运会男篮决赛 五点去北竹看日出，结果当天雾很大没看成 睡回笼觉 下午坐快艇前往四礵列岛。不愧是人间塞尔达，确实美啊，有点像澳大利亚的十二使徒。就是海岛上没有信号，且风大太阳大，没有防护措施，还是比较危险的。 day 3 前往福鼎 爬太姥山，很多怪石头还是挺有意思的 昆明 大理 丽江 香格里拉 时间：2024.8.31 9.8 day 1 先去云牵一线品尝了一下云南招牌过桥米线😍好吃量大不贵，人均吃撑40r 海埂大坝 海埂公园 西山索道 俯瞰滇池 龙门索道下山 昆明老街吃晚饭 途中偶遇暴雨，拼尽全力无法战胜 day 2 翠湖公园 国立西南联合大学旧址 怪兽大厦 前往大理 大理古城 打车到才村码头看到了洱海日落，绝美 晚上回大理古城喝酒 （大理苦荞啤酒很好喝） day 3 崇圣寺三塔 租车环洱海自驾路线：磻溪S湾 廊桥 喜洲古镇 海舌公园 双廊古镇 鹿鹅山遗址 小普陀 day 4 理想邦 洱海公园 苍洱大道 团山公园 出发丽江 洱海是真的太漂亮了，水清澈且蓝，天气好的时候很无敌，以后有机会想来这里长住一段时间 day 5 玉龙雪山一日游🤗：乘坐大索道到山顶4680石碑 牦牛坪 蓝月谷 晚上回到丽江古城 不得不吐槽丽江的天气，我们过去的时候正好是雨季。一会儿雨一会儿晴，山上全是雾什么都看不到 day 6 一早出发香格里拉，民宿老板帮我联系了当地旅游车。沿途游览：上虎跳峡 中虎跳 哈巴雪山石碑 白水台 到达香格里拉 晚上吃了顿牦牛肉火锅自助有点太顶了 游览独克宗古城：龟山公园 转经筒 大佛寺 day 7 一早驱车两小时前往无底湖（真的美，雪山中的湖泊像一块翡翠） 大经幡 大白塔 day 8 松赞林寺 帕纳海（小羊羔太可爱了） 坐高铁返回丽江乘坐飞机","summary":"旅行路线备忘与分享"},{"permalink":"https://zhr0529.cn/posts/随想/","title":"随想","content":"迎着阳光盛大逃亡。最后留在记忆深处的总是那些虚无缥缈的东西，往事总在回忆中被赋予寓意。","summary":"往事总在回忆中被赋予寓意。"},{"permalink":"https://zhr0529.cn/posts/arm-cortex-m3学习笔记/","title":"【理论】《ARM Cortex-M3 权威指南》学习笔记","content":"《ARM cortex M3权威指南》学习笔记 学习目的 是时候把单片机开发学得再深入一些了。我希望自己之后的开发能深入到内核和汇编层面，自然要多补充一些理论基础。好的","summary":"《ARM Cortex-M3 权威指南》阅读笔记。"},{"permalink":"https://zhr0529.cn/posts/个人博客开发日记/","title":"【前端】个人博客开发日记 - 基于 Hexo + Butterfly","content":"个人博客开发日记 一、致谢 感谢安知鱼大佬，让我熟悉前端开发流程并使用框架快速搭建了博客，让我也体验了网页设计程序员的快乐 安知鱼：Butterfly重装日记 糖果屋教程贴 二、博客开发和前端学习暂时告一段落 博客还有很多很多可以优化的地方，但我毕竟不是这个方向的，能琢磨成这样已经非常满意了。后面我会多写博客，等以后有机会再优化网站。 网站主题配置文件 文章编写源代码文件 关于网址域名更改回https://Fad1ngWind.github.io 11.21号晚上，本来兴致勃勃的回到家里打开电脑想更新一下博客，结果发现网页访问不了了。各种问题排查才发现是没备案导致的，好家伙一研究才发现备案这么麻烦😢费时费力填了一大堆相关信息不说过不过还要看工作人员脸色，最后还要花几百块买个ICP备案服务码才肯给你搞。于是乎我决定先换回这个基于Github直接推送的域名，暂时先弃用之前斥巨资购买的两年zhr0529.cn的域名，望周知。😊","summary":"第一版个人博客的搭建过程，基于 Hexo + Butterfly。"},{"permalink":"https://zhr0529.cn/posts/日记-第一篇/","title":"【日记】","content":"Our Story Begins. c include \"stdio.h\" void main{ printf(\"hello world!\") while(1) { delay ms(10); } } 一切的开始都是在刚上大学的那个迷茫的秋天。 一路走来 2023.8 高考完的暑假 暑假两个月考完了驾照，自学了一点Python，通关了许多曾经想玩的游戏，去了杭州、上海、天津、广州、澳门、香港。录取结果出了，浙江工业大学吗...嗯，虽然最后没有去一个211或者双一流，但是在杭州，好像还不错。 2023.11 大一上 面对未来无数未知的分岔路虽然时常会怀念高中的小镇做题家生活，但是大学也蛮精彩。遇到了很多很好的朋友们，期中考高数考了93分选择分流到计算机＋金融的高新专业量化金融。或许未来我会坐在屏幕，成为赛博华尔街之狼？ 2024.3 大一下 寒假在新东方兼职当助教，初次体验上班的无奈。经济学学科真的很抽象，我并不喜欢这种在纸上计算建模、对着图像曲线理论分析虚无缥缈的学科，再加上一直对金融股市有着不好的印象，我决定转专业。 2024.5 大一下 室友们一起给我过了生日。 2024.8 大一暑假 自学一年c语言，以不低的排名转专业到机器人工程感知与控制，暑假开始自学Linux系统基础操作指令。同时开始接触Stm32，能为单片机与各类电子器件做基础的嵌入式开发。暑假在计算机公司实习，职位是云网运维工程师，上班就是在机房修电脑，蹲在服务器前等着bios系统启动，以及坐在大屏幕前看镜像监控。实习期间上班摸鱼学了画PCB电路板，这一关键的技能点在后来为我带来了许多机会。 2024.11 降转后 大一上 遇到了慧眼识珠的伯乐，随队参加了机器人大赛，收获了人生中第一个国一。参加了信息学院组织的竞赛班，开始自学电路，同时开始接触51单片机开发。我搭建了自己的个人博客网站。 2025.5 大一下 开学初正在准备智能车队选拔赛的时候被学长直接拉进队伍做硬件。遇到了很多困难，但也学习了很多解决问题的方法。没有经过系统的培训就直接开始打比赛总担心伤仲永的故事会发生在自己身上。这学期的主旋律似乎就是每周去两次屏峰校区焊板子，同时尽量兼顾绩点。在朝晖校区的最后一个学期很平淡，但也很开心美好，我会记得每一次在朝晖篮球场下度过的晚霞。 2025.7 大一暑假 准备了一学期的比赛最后在赛场上没能发挥出水平，遗憾获得成功参赛奖，但是遇到了很好的队友，认识了很多厉害的高手，也算不虚此行。 2025.9 大一暑假 去日本玩了半个月，基本把半个日本都玩过去了，哈哈哈哈。 2025.12 大二上 度过了最黑暗的一学期，学不懂的课，做不明白的作业，考试什么都不会，宣告了我之前机械刷题法和突击复习法的彻底失败，这学期绩点非常低。并且伴随着焦虑参加了一堆竞赛，但还算有收获，第一年参加数模美赛获得了H奖，并且在着手准备挑战杯与智能车。感恩科研工作遇到了一个很好的指导老师，在十一月份收到了我的论文中稿会议的邮件，发表了人生第一篇论文。 2026.1.20 杭州下雪了。 2026.2 大二寒假 寒假和老同学们出去玩了好几次，开心。回了一趟高中母校，时隔十年回了一趟小学母校，居然基本没怎么变，或许时间是可以刻舟求剑的？或许我回去仍然能找到十年前调皮在墙上刻下的字，但我没有去找，也很难找到了。 2026.6 大二下 这学期也和去年的这段时间一样，学习课内知识与兼顾智能车备赛，我们跑的成绩比去年快了很多。时不时也经常出去玩，我很喜欢3、4月份杭州的天气，天晴朗，又很凉快，每天都让人很有出去玩的欲望。","summary":"这是一篇日记"},{"permalink":"https://zhr0529.cn/posts/理论linux学习笔记/","title":"【理论】Linux学习笔记","content":"Linux 基础命令、目录结构、文件操作、用户权限与常用指令学习笔记。","summary":"Linux 基础命令、目录结构、文件操作与权限管理。"}]