[{"content":"在 Jetson Orin Nano 上部署大语言模型（LLM）是实现端侧大模型 Agent 的第一步，也是最核心的工程挑战。由于 Jetson 采用的是统一内存架构（Unified Memory Architecture），CPU 和 GPU 共享物理内存（4GB 或 8GB），这与传统 PC 的“内存+独立显存”有很大不同。在 Orin Nano 上，我们必须精细控制内存预算，防止系统因 OOM（Out of Memory）而崩溃。\n针对子任务 3.1（离线版 - LLM 推理），从瑞芯微 RK 平台迁移到 NVIDIA Jetson 平台，我们需要将原有的 RKNN 链路彻底重构为 NVIDIA 生态的加速链路。\n子任务 3.1 拆解：端侧 LLM 部署实施方案\r✅️微任务 3.1.1：Jetson 硬件环境准备与 Swap 空间配置\r目标：解决 Orin Nano 在模型编译或高负载时内存不足的问题。 具体步骤： 1.确认 JetPack 版本（推荐使用 JetPack 6.x，自带 CUDA 12.x 和更现代的 Linux 内核）。\n1 2 #jtop查看板卡信息 jtop 2.必须配置虚拟内存（Swap）：Orin Nano 即使是 8GB 版本，在编译模型或运行 4B 模型时也极易耗尽物理内存。建议通过 NVMe SSD 划分 16GB 的 Swap 空间。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 #查看内存和SWAP free -h #查看磁盘 lsblk #创建swap sudo fallocate -l 16G /swapfile #设置权限 sudo chmod 600 /swapfile #验证 ls -lh /swapfile #格式化swap sudo mkswap /swapfile #启用swap sudo swapon /swapfile #插看swap swapon --show \u0026amp; free -h #开机自动挂载 sudo nano /etc/fstab #末尾加入/swapfile none swap sw 0 0 #优化swap策略 cat /proc/sys/vm/swappiness \u0026gt;60 sudo sysctl vm.swappiness=10 #永久生效 sudo nano /etc/sysctl.conf #末尾加入vm.swappiness=10 3.开启最高性能模式：\n1 2 sudo nvpmodel -m 0 sudo jetson_clock 锁住 CPU/GPU 频率，确保推理时延迟最低\n✅️微任务 3.1.2：推理引擎技术选型（llama.cpp vs TensorRT-LLM）\r目标：根据开发周期和性能需求选择最佳引擎。 方案对比与选择： 方案 A：llama.cpp（推荐优先部署）：对 Jetson 适配极佳，编译简单，内存开销极小。通过开启 GGML_CUDA=1，可以将计算完全卸载到 Orin Nano 的 Ampere GPU 上。支持 GGUF 格式，量化极其方便。 方案 B：TensorRT-LLM（极致性能追求）：NVIDIA 官方的王牌，吞吐量最高。但由于 TensorRT-LLM 的编译依赖较重，在 8GB 的 Orin Nano 上直接编译模型 Engine 极易 OOM，通常需要在 PC 端（带有 NVIDIA 独立显卡的 Ubuntu 主机）进行交叉编译，生成 Engine 后再拷贝到 Jetson 上运行。 实施建议：第一阶段使用 llama.cpp + CUDA 快速打通链路；第二阶段追求极限低时延时，在 PC 端配置 Docker 交叉编译 TensorRT-LLM 的 Engine。 方案A具体步骤：\n1.确认环境\n1 2 uname -m#为aarch64 dpkg -l | grep nvidia-jetpack#JetPack 5.1+ 或 6.x、CUDA 已安装 2.安装依赖\n1 2 3 4 5 6 7 8 sudo apt update sudo apt install -y \\ git \\ cmake \\ build-essential \\ python3 \\ python3-pip 3.获取 llama.cpp\n1 2 git clone https://github.com/ggml-org/llama.cpp#网慢可以转gitee cd llama.cpp 4.jetson cuda编译\n1 2 3 4 5 cmake -B build -G Ninja \\ -DCMAKE_BUILD_TYPE=Release \\ -DGGML_CUDA=ON \\ -DCMAKE_CUDA_ARCHITECTURES=87 \\ -DLLAMA_CURL=ON 参数 意义 GGML_CUDA=ON 启用 CUDA backend 87 Orin Nano Ampere 架构 Ninja 加速编译 LLAMA_CURL 支持 hf 模型下载 5.编译\n1 cmake --build build -j$(nproc) 6.验证\n1 ./build/bin/llama-cli -h 7.部署测试\n下载 https://huggingface.co/bartowski/Qwen2.5-3B-Instruct-GGUF\n放入/home/nvidia/Downloads/llama.cpp/models\n1 2 3 4 5 ./build/bin/llama-cli \\ -m ./models/Qwen2.5-3B-Instruct-Q4_K_M.gguf \\ -ngl 999 \\ -p \u0026#34;你好，请用一句话介绍ROS2\u0026#34; \\ -n 64 ✅️微任务 3.1.3：Qwen2.5 模型量化与格式转换\r目标：将 Hugging Face 发布的原始 Qwen3.5 模型权重转换为 GGUF 格式，并完成不同量化策略（Q8_0、Q6_K、Q5_K_M、Q4_K_M）的压缩与性能测试，实现 Jetson Orin Nano 端侧部署（原始 Qwen 模型通常以 Transformers 格式发布）\n量化策略\n原始模型一般FP16 Q8_0：8-bit量化 Q6_K：6-bit量化K-Quant， llama.cpp 后来提出的新量化算法 Q5_K_M：5-bit量化、K-Quant算法、Medium（重要层保留更多精度） Q4_K_M：4-bit量化、K-Quant、Medium 格式 大小 FP16 8GB Q8_0 4.3GB Q6_K 3.3GB Q5_K_M 2.9GB Q4_K_M 2.3GB 量化工具需要先理解模型结构\nGGUF 是 llama.cpp 的统一模型格式\nHuggingFace模型(safetensors) \u0026ndash;\u0026gt; 转换成GGUF(F16) -\u0026gt; GGUF量化 - \u0026gt; Q4_K_M / Q5_K_M\n原始模型\nQwen3-4B/ ├── config.json ├── tokenizer.json ├── model.safetensors ├── model-00001-of-00002.safetensors └── model-00002-of-00002.safetensors #model.safetensors神经网络权重矩阵 GGUF是llama.cpp专用模型容器\n具体步骤（以 llama.cpp 路线为例）：\nQwen2.5-1.5B-Instruct (HF格式) ​ ↓ convert_hf_to_gguf.py qwen2.5-1.5b-f16.gguf ​ ↓ llama-quantize qwen2.5-1.5b-q4_k_m.gguf 1.hf获取国产模型慢，在魔塔社区下代替Qwen2.5-1.5B-Instruct\n1 2 3 4 5 # 1. 安装魔搭下载工具（使用清华源瞬间装完） pip install modelscope -i https://pypi.tuna.tsinghua.edu.cn/simple # 2. 一键满速下载到指定目录（告别网络报错） modelscope download --model qwen/Qwen2.5-1.5B-Instruct --local_dir ./Qwen2.5-1.5B-Instruct 模型位置：\n2.使用 llama.cpp 的 convert_hf_to_gguf.py 脚本将模型转化为gguf格式\nconvert_hf_to_gguf.py 依赖 transformers 库，因此在使用前先配个miniconda\n1 2 3 4 5 6 7 8 9 10 wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-aarch64.sh bash Miniconda3-latest-Linux-aarch64.sh source ~/.bashrc conda --version conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/ conda config --set show_channel_urls yes conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/main conda tos accept --override-channels --channel https://repo.anaconda.com/pkgs/r 创建进入\n1 2 conda create -n llama_cpp_env python=3.10 -y conda activate llama_cpp_env 安装transformers 和torch\n然后转换\n1 2 3 4 5 6 cd ~/Downloads/llama.cpp python convert_hf_to_gguf.py \\ /home/nvidia/Desktop/Qwen2.5-1.5B-Instruct \\ --outfile qwen2.5-1.5b-f16.gguf \\ --outtype f16 3.模型量化：执行 ./llama-quantize。\nOrin Nano 4GB 版：必须选择 INT4 量化（如 Q4_K_M），且只能运行 1.8B 模型。 Orin Nano 8GB 版：推荐选择 INT4 量化运行 4B 模型，或者选择 INT8 量化（如 Q8_0）运行 1.8B 模型（具备更好的指令遵循能力）。 1 2 3 4 ./build/bin/llama-quantize \\ qwen2.5-1.5b-f16.gguf \\ qwen2.5-1.5b-q4_k_m.gguf \\ Q4_K_M ✅️微任务 3.1.4：Qwen2.5-0.5B-Instruct Lora微调\r目标：实现Qwen2.5-0.5B-Instruct微调测试。\n方案选择：\nOrin Nano 4GB 版：必须选择 INT4 量化（如 Q4_K_M），且只能运行 1.8B 模型。 Orin Nano 8GB 版：推荐选择 INT4 量化运行 4B 模型，或者选择 INT8 量化（如 Q8_0）运行 1.8B 模型（具备更好的指令遵循能力）。 实验环境：魔塔实例，8核32gb，显存24g\n方案1：魔塔微调神器 ms-swift\n1.安装工具\n1 2 3 pip install ms-swift -U pip install modelscope -U python -c \u0026#34;from modelscope import snapshot_download; snapshot_download(\u0026#39;Qwen/Qwen2.5-0.5B-Instruct\u0026#39;, local_dir=\u0026#39;.\u0026#39;)\u0026#34; 2.创建微调数据集\n1 2 3 4 5 6 cd /mnt/workspace cat \u0026lt;\u0026lt; \u0026#39;EOF\u0026#39; \u0026gt; cat_data.jsonl {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是谁？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;喵喵喵！本大爷是专属猫咪助手，有小鱼干才回答你！\u0026#34;}]} {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;今天天气怎么样？\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;喵~ 太阳很大，适合本猫在窗台上睡觉！\u0026#34;}]} {\u0026#34;messages\u0026#34;: [{\u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;帮我写一段Python代码。\u0026#34;}, {\u0026#34;role\u0026#34;: \u0026#34;assistant\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;打工人自己写！喵！好吧，给你个 print(\u0026#39;Meow\u0026#39;)。\u0026#34;}]} EOF 3.开始lora微调\n1 2 3 4 5 6 7 8 swift sft \\ --model ./models \\ --model_type qwen2 \\ --template qwen2_5 \\ --dataset cat_data.jsonl \\ --num_train_epochs 1 \\ --max_steps 40 \\ --output_dir ./output 4.合并lora权重\n1 2 3 4 5 6 cd /mnt/workspace swift export \\ --model ./models \\ --adapters ./output/v1-20260615-211104/checkpoint-40 \\ --merge_lora true 5.使用 llama.cpp 转换为 GGUF\n1 2 3 4 5 6 7 8 9 cd /mnt/workspace git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp pip install -r requirements.txt cd /mnt/workspace/llama.cpp python convert_hf_to_gguf.py /mnt/workspace/output/v1-20260615-211104/checkpoint-40-merged \\ --outfile /mnt/workspace/my_cat_qwen.gguf \\ --outtype q8_0 微任务 3.1.4：流式推理接口开发（C++ / Python）\r目标：实现全流式（Streaming）文本输出，降低首字时延（First-token Latency）。 具体步骤： 编写推理控制客户端，调用 llama.cpp 的 API 或使用 llama-cpp-python（确保编译时使能了 CUDA）。 配置关键推理参数： n_ctx（上下文窗口）：机器人局部交互通常不需要太长上下文，建议设为 512 或 1024，以节省大量 KV Cache 占用的显存。 n_gpu_layers：设置为 -1，代表将所有模型层（Layers）全部加载到 GPU 显存中，避免 CPU-GPU 之间的数据拷贝时延。 微任务 3.1.5：ROS2 节点封装与端侧通信集成\r目标：将大模型推理能力接入机器人系统，供 Agent 决策模块调用。 具体步骤： 创建一个 ROS2 Lifecycle 节点（如 llm_inference_node）。 输入端：订阅 ASR 模块发布的语音识别文本话题（Topic）。 输出端：使用 ROS2 的 Action 机制（而不是普通的 Service 或 Topic）。因为大模型生成是流式的、耗时的，Action 机制允许节点在生成过程中持续回传（Feedback）流式文本，供 TTS 模块实现边接收边朗读（全流式响应），从而大幅降低整个 Agent 链路的感知延迟。 ","date":"2026-08-15T14:00:00+08:00","permalink":"/p/jetson-llm-agent/","title":"端侧大模型在线/离线部署（Agent）——机器人项目迁移实战（三）"},{"content":"第一阶段：LLM微调基础\r目标：理解文档里提到的\nLoRA(Low-Rank Adaptation，低秩适应) QLoRA（Quantized LoRA） SFT（Supervised Fine-Tuning，监督微调） DPO（Direct Preference Optimization，直接偏好优化） RLHF vLLM SFT、DPO\r维度 SFT DPO 训练数据 高质量的标准答案（input → output 对） 偏好对比数据（input → chosen/rejected 对） 学习信号 模仿标准答案（\u0026ldquo;这是什么\u0026rdquo;） 学习相对偏好（\u0026ldquo;哪个更好\u0026rdquo;） 优化目标 最大化生成标准答案的概率 最大化 chosen 优于 rejected 的边际 是否需要 Reward Model 不需要 不需要（但隐式学习偏好） 对齐效果 基础能力好，但容易过拟合或对齐不足 对齐人类偏好更直接，泛化性通常更好 输入特定类型的文本\n监督微调\nSFT 像\u0026quot;抄作业\u0026quot;：模型学习专家写好的标准答案，适合传授知识和基本格式。 DPO 像\u0026quot;比较打分\u0026quot;：模型学习\u0026quot;答案 A 比答案 B 更好\u0026quot;，适合对齐人类价值观和偏好（如安全性、有用性、风格） 数据：\n{ \u0026#34;instruction\u0026#34;:\u0026#34;把红色方块放进盒子\u0026#34;, \u0026#34;output\u0026#34;:\u0026#34;对应动作序列\u0026#34; } 理解：\n输入 ↓ 标签 ↓ 训练 实际训练中通常是先 SFT 后 DPO（或 RLHF）：\nSFT 阶段：用高质量指令数据训练模型，让它学会基本对话格式和知识。 DPO 阶段：用偏好数据（如人类标注的 good/bad 回答）进一步微调，让模型输出更符合人类偏好。 技术细节补充\nDPO 的优势：避免了 RLHF 中复杂的 Reward Model 训练和 PPO 强化学习，将偏好优化转化为简单的分类损失，训练更稳定高效。 SFT 的局限：如果标准答案本身质量不够高或分布有偏，模型会学到错误模式；且无法表达\u0026quot;好答案\u0026quot;和\u0026quot;更好答案\u0026quot;之间的细微差别。 RLHF\rRLHF（Reinforcement Learning from Human Feedback，基于人类反馈的强化学习）是大模型对齐阶段的核心方法，解决 SFT 无法表达\u0026quot;好/坏\u0026quot;偏好的问题。\nSFT 的局限：\n只知道\u0026quot;标准答案\u0026quot;，不知道\u0026quot;A 比 B 更好\u0026quot; 无法处理开放性任务（如创意写作、多轮对话） 缺乏安全边界（可能生成有害内容） RLHF 的目标：让模型输出更符合人类偏好\n1 2 3 4 ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 1. SFT 模型 │ → │ 2. 训练 Reward Model │ → │ 3. PPO 强化学习 │ │ (学会基本对话) │ │ (学会打分) │ │ (优化模型输出) │ └─────────────────┘ └─────────────────┘ └─────────────────┘ 训练 Reward Model（奖励模型）\n1 2 3 4 5 问题：解释量子计算 回答 A（好）：\u0026#34;量子计算利用量子叠加和纠缠...\u0026#34; → 得分高 回答 B（差）：\u0026#34;量子计算是一种计算...\u0026#34;（含糊） → 得分低 回答 C（有害）：\u0026#34;量子计算可以用来破解密码...\u0026#34;（误导）→ 得分极低 LoRA:\r参数高效微调（Parameter-Efficient Fine-Tuning，PEFT）技术中的一种,冻结AIGC大模型的大部分参数，再引入一小部分可训练的参数作为适配模块进行微调训练\n本质是特征矩阵进行低秩分解的一种近似数值分解技术，可以大幅降低特征矩阵的参数量，但是会伴随着一定的有损压缩，可以大幅降低特征矩阵的参数量，但是会伴随着一定的有损压缩\n我们假设原来的是1001024的参数矩阵，那么参数量为102400，LoRA模型将矩阵拆成了两个矩阵相乘，如果我们设置Rank=8，那么就是1008的A矩阵与8*1024的B矩阵做矩阵乘法，参数量为800+8192=8992，整体参数量下降了约11.39倍\nTransformer架构\n比如attention模块就是\n可以控制的就是这些矩阵，矩阵的值就是模型参数\n模型预训练：\n把海量数据放入模型进行计算；\n模型输出和期望输出通过数学方式比较，从而调整参数\n模型微调\n在预训练模型基础上，集中训练特定领域文本，让模型回复快速集中到特定领域\n但监督微调sft显存占用与预训练模型一致\nQLoRA（Quantized LoRA）：\r背景：模型量化（quantization）也被叫做模型的低精度表示，指的是在不大幅降低模型效果的前提下使用更低的精度来表示模型中的参数，从而缩减模型的体积和训练模型时占用的显存。量化的本质是函数映射，根据量化过程是否线性我们可以把量化分为线性量化和非线性量化。\n即从一种数据类型“舍入”到另一种数据类型；量化操作不可避免的带来一些误差，例如从float32到int8的round操作带来的精度损失，超出int8范围的溢出值的截断\n最常见的 量化技术是最大绝对值：\n还有一种量化技术叫做零点量化：\n第一步值域映射，即通过缩放将原始的数值范围映射为量化后的数值范围; 第二步零点调整，即通过平移将映射后的数据的最小值对齐为目标值域的最小值。\n按照量化过程是否以0点为对称点量化又可以分为对称量化和非对称量化；\n对称量化将原浮点数的最小或最大值的绝对值作为映射值的范围，而非对称量化是将原浮点数的最小和最大值映射为量化数据的最小和最大值，如图1。在非对称量化中，0的映射也可能会有偏移，因此不一定会被映射到0。\n非线性量化：\n分位数量化（Quantile Quantization）顺序排列的一组数据分割为若干个相等块的分割点的数值\n累积分布函数（CDF）使用分位数将张量分成了大小相同的若干个块，这样我们得到更加均匀的量化特征，这也就是分位数量化。如图3所示，对于4比特量化，我们希望需要找到15个分位数来将这个曲线下面的面积（积分）等分成16份。两个分位数的中点便是模型量化到这个区间映射的值。预训练模型的参数往往是符合正态分布的，因此我们可以通过累积分布函数的反函数\nQLoRA\r第一个工作是结合了分位数量化和分块量化的4位标准浮点数量化（4-bit NormalFloat Quantization）。第二个工作是对模型进行两次量化的双重量化（Double Quantization），它的第二次量化只作用在第一次量化产生的量化常数上，可以进一步节约显存占用。第三个工作是分页优化（Paged Optimizer），使用CPU内存代替GPU显存保存部分梯度参数。\n一、分位数量化方法我们可以将FP2精度量化到4bit的精度，但是直接这么用的一个问题是不能保证高精度的0一定被映射到低精度的0，但是0点又是深度学习中一个重要的值，例如在模型稀疏化，数据padding的时候一般都是使用0来完成\n二、保存模型时我们不仅要保存量化后的结果，还要保存每个块的量化常数。虽然量化后的参数只有4bit的精度，但是这个量化常量的精度是float32。在QLoRA中，每个块的大小是64，因为块中的每个值占4比特。这相当于为了存储量化常数，模型要额外占用12.5%\n三、QLoRA分页优化其实就是当显存不足是，将保存的部分梯度检查点转移到CPU内存上，和计算机的内存数据转移到硬盘上的常规内存分页一个道理\n","date":"2026-07-11T10:00:00+08:00","permalink":"/p/lora-qlora/","title":"LLM 微调基础：LoRA 与 QLoRA 学习笔记"},{"content":"这学期《传感器与微系统》课程论文做的是室内定位方向的数据工作：在 3m×5m 的实验场地里，人工采集了 WiFi RSSI 与地磁两路指纹，构建了一张 0.5m 网格的指纹库。定位算法本身是论文的核心内容，暂不展开；本文只记录数据采集与预处理的工程实践。\n指纹法定位原理\r指纹法分离线、在线两个阶段。离线阶段在场地内按网格逐点采集各 AP 的信号强度，形成「位置 ↔ 信号指纹」数据库；在线阶段用实时采到的信号去指纹库里匹配，反推出当前坐标：\nWiFi RSSI 空间波动大、成本低，地磁信号空间差异明显、不受光照遮挡影响，两者互补——这也是选择这两路指纹的原因。\n地磁采集：ESP32 + ICM20948\r地磁传感器选用 ICM20948（9 轴 IMU 内置 AK09916 磁力计），主控为 ESP32，Arduino 框架开发。\n采集流程：\n通过姿态解算获取横滚角 $\\phi$ 与俯仰角 $\\theta$ 读取磁力计原始三轴数据 $M^b = [m_x, m_y, m_z]^T$ 用旋转矩阵把设备坐标系下的磁场校正到水平坐标系（z 轴指向上方）： $$R_x(\\phi) = \\begin{bmatrix} 1 \u0026amp; 0 \u0026amp; 0 \\ 0 \u0026amp; \\cos\\phi \u0026amp; \\sin\\phi \\ 0 \u0026amp; -\\sin\\phi \u0026amp; \\cos\\phi \\end{bmatrix} \\qquad R_y(\\theta) = \\begin{bmatrix} \\cos\\theta \u0026amp; 0 \u0026amp; -\\sin\\theta \\ 0 \u0026amp; 1 \u0026amp; 0 \\ \\sin\\theta \u0026amp; 0 \u0026amp; \\cos\\theta \\end{bmatrix}$$\n输出三元组：（水平磁场模值，垂直磁场模值，总磁场模值） 采样频率设为 1Hz（宏定义可调）。姿态校正这步很关键——手持设备不可能保证完全水平，不做校正的话，同一位置换个握持角度读数就完全对不上了。\nWiFi RSSI 网格测量\r场地：约 300cm × 500cm 的室内区域 AP 布置：5 个无线接入点 采样：按 1m 网格逐点人工采集，每个网格点记录 5 路 RSSI（dBm）与地磁三模值 原始数据以 xlsx 记录，字段包括网格点物理坐标（cm）、RSSI_1~RSSI_5、地磁三模值（μT）。\n预处理：从 1m 实测到 0.5m 指纹库\r实测网格 1m 间距偏稀疏，直接做指纹匹配粒度不够。预处理流水线：\n网格提取：从原始测量数据抽取 1m 间隔网格点 IDW 插值扩充：用反距离加权插值把网格加密到 0.5m，扩充指纹库分辨率（插值域的选择有讲究，细节在论文里） 格式转换：xlsx 统一转 CSV 供后续使用 看看采到的数据\r数据好不好，可视化是最直观的检验。5 个 AP 中一路 RSSI 的三维曲面——可以明显看到信号强度随空间的起伏：\n地磁总模值的等高线图——场地西部偏高（约 55μT）、东部偏低（约 43μT），空间梯度明显，这正是地磁指纹可用于定位的前提：\n换个角度看地磁场强度曲面：\n小结\r指纹库质量决定指纹定位的上限，采集阶段的网格规划、姿态校正、传感器选型都比想象中重要 地磁的空间梯度和 WiFi 的互补性在数据里体现得非常直观 融合定位方法本身是论文核心内容，等论文发表后再写一篇展开 ","date":"2026-05-16T15:00:00+08:00","permalink":"/p/indoor-positioning-fingerprint/","title":"室内定位指纹库构建实践：ESP32 地磁采集与 WiFi 网格测量"},{"content":"Samba 共享（⭐ 最原生的 Windows 挂载方案）\r如果你希望把板子上的目录变成 Windows 电脑里的一个\u0026quot;本地磁盘\u0026quot;（比如 Z: 盘），随时可以拖拽文件，那么 Samba 是最佳选择。\n1. 在 RK3588 上安装 Samba 服务：\nBash\nsudo apt update sudo apt install samba 2. 配置共享目录： 打开 Samba 配置文件：\nBash\nsudo nano /etc/samba/smb.conf 在文件最末尾，添加以下内容（将你的 /home/cat 目录共享出去）：\nIni, TOML\n[lubancat_home] path = /home/cat available = yes browseable = yes public = yes writable = yes 保存并退出（Ctrl+O 回车，Ctrl+X）。\n3. 为你的用户设置 Samba 密码：\nBash\nsudo smbpasswd -a cat (输入两次你想要的密码，建议就用你板子的登录密码方便记)\n4. 重启 Samba 服务：\nBash\nsudo systemctl restart smbd 5. 在 Windows 上挂载：\n打开 Windows 的\u0026quot;此电脑\u0026quot;（文件资源管理器）。 在顶部地址栏输入：\\\\192.168.137.200 并回车。 此时会弹出一个窗口让你输入账号密码，输入账号 cat 和你刚才设置的密码。 你会看到 lubancat_home 文件夹，右键点击它，选择\u0026quot;映射网络驱动器\u0026quot;，分配一个盘符（比如 Z: 盘）。搞定！ ","date":"2026-03-14T20:45:00+08:00","permalink":"/p/samba/","title":"Samba 共享：把开发板目录变成 Windows 本地磁盘"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 07 / 7 篇：I2C 应用编程。返回系列目录。\n第10章 串口UART编程\r第11章 I2C应用编程\r11.1 I2C硬件\rSCL,SDA，一条I2C总线可接很多设备，SCL,SDA放上拉电阻\n11.2 传输数据类比\r如何通过IIC传数据，\n主设备：一主多从，可把数据从主设备发到从设备，也可从设备发到主设备\n发送 主设备：开始(start)\n主设备：A，我把数据发你(地址/方向)\nA：收到(回应)\n主设备：发送数据(传输)\nA：收到后，告诉主设备(回应)\n主设备：结束(停止)\n接收 主设备：开始(start)\n主设备：B，把数据发我(地址/方向)\nB：收到(回应)\nB：把数据发给主设备(传输)\n主设备：收到数据后，告诉A(回应)\n主设备：结束(停止)\n11.3 IIC传输数据格式\r11.3.1写操作\r主设备发出start信号 发出设备地址，方向(读1/写0) 从设备发出应答信号 主设备发送一字节数据，等待回应 每传输一字节，接收方回应一次，在传输下一个数据 数据发送完成，主设备发送stop信号 11.3.2读操作\r11.3.3 IIC信号\rIIC协议栈数据传输单位是字节，但是要用到9个时钟，前面8个时钟用来传输8数据，第9个始终用来传输回应信号，先传输最高位\n开始信号(S)：SCL为高时，SDA由高跳低，开始传输数据 结束信号(P)：SCL为高时，SDA由低跳高，结束传输数据 响应信号(ACK)：接收到8位数据，第九个时钟周期，拉低SDA SDA数据必须在SCL高时稳定，只能在SCL为低时变化 11.3.4 协议细节\r1.SDA引脚必须双向IO，因为有发有收\n11.4 SMBus协议\r11.4.1 SMBus是IIC协议子集\rSMBus: System Management Bus，系统管理总线\n11.5无需编写驱动直接访问设备(I2C-Tools)\r通过I2Ctool的程序，使用自带的驱动程序，来操作内核的i2c操作程序来访问硬件\n首先确定：使用哪个IIC控制器，使用哪个IIC设备，读还是写数据\n11.5.1用法\ri2cdetect：I2C检测 // 列出当前的I2C Adapter(或称为I2C Bus、I2C Controller) i2cdetect -l // 打印某个I2C Adapter的Functionalities, I2CBUS为0、1、2等整数 i2cdetect -F I2CBUS // 看看有哪些I2C设备, I2CBUS为0、1、2等整数 i2cdetect -y -a I2CBUS 11.5.2操作传感器AP3216C\rAP3216C是红外、光强、距离三合一的传感器，以读出光强、距离值为例\n复位：往寄存器0写入0x4 使能：往寄存器0写入0x3 读光强：读寄存器0xC、0xD得到2字节的光强 读距离：读寄存器0xE、0xF得到2字节的距离值 使用SMBus协议\n//i2cset查看用法 i2cset //写数据 i2cset -f -y 0（0号线） 0x1e（芯片地址） 0（写入地址） 0x4 i2cset -f -y 0 0x1e 0 0x3 //读数据 i2cget -f -y 0 0x1e 0xc w //0xc光照 i2cget -f -y 0 0x1e 0xe w\t//0xe距离 使用I2C协议\ni2ctransfer -f -y 0 w2@0x1e 0 0x4 i2ctransfer -f -y 0 w2@0x1e 0 0x3 i2ctransfer -f -y 0 w1@0x1e 0xc r2 i2ctransfer -f -y 0 w1@0x1e 0xe r2 11.5.3 I2CTools访问I2C设备的方式\rAPP通过I2C Controller与I2C Device传输数据\n指定i2c控制器：\ni2c-dev.c提供为每个I2C控制器(I2C Bus、I2C Adapter)都生成一个设备节点：/dev/i2c-0、/dev/i2c-1等待 open某个/dev/i2c-X节点，就是去访问该I2C控制器下的设备 指定i2c设备：\nioctl(file, I2C_SLAVE, address) 如果该设备已经有了对应的设备驱动程序，则返回失败 ioctl(file, I2C_SLAVE_FORCE, address) 如果该设备已经有了对应的设备驱动程序 但是还是想通过i2c-dev驱动来访问它 则使用这个ioctl来指定I2C设备地址 传输方式：I2C或SMBus\n11.6编写APP直接访问EEPRPM\rI2C控制器地址0x50\nI2C设备地址0x50\n从芯片手册上可以知道，AT24C02的设备地址跟它的A2、A1、A0引脚有关：\n高4位为1010，A2A1A0都是0，所以AT24C02的设备地址是：0b1010000，即0x50\n11.6.1写数据\r11.6.2读数据\r","date":"2026-02-28T10:30:00+08:00","permalink":"/p/embedded-07/","title":"嵌入式 Linux 学习笔记（07）：I2C 应用编程"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 06 / 7 篇：输入系统、网络编程与多线程。返回系列目录。\n第7章 输入系统应用编程（未完成）\r7.1输入设备\r键盘、鼠标、遥控杆、书写板、触摸屏等与linux完成数据交换\n输入设备种类繁多，能否统一它们的接口？\nLinux 为了统一管理输入设备，实现了一套能兼容所有输入设备的框架：输入系统\n7.2输入系统框架及调试\r输入系统框架\n示例：\n在开发板上执行，可以看到触摸屏数据\nhexdump /dev/input/event1 hexdump: 是一个命令行工具，它将文件或数据流的内容以十六进制和 ASCII 格式输出\n/dev/input/event1: 是一个特殊的设备文件，代表一个输入设备，比如键盘、鼠标或触摸板。Linux 内核会把来自这些设备的原始输入事件（例如按键、鼠标移动或点击）写入这些文件\n7.2.2编写APP需要掌握的知识\r驱动程序上报的数据含义三项重要内容：\n1.type：哪类，如EV_KEY为按键类\n2.code：哪个，比如KEY_A\n3.value：值，比如0按下，1松开\n此外APP 读取数据时，可以得到一个或多个数据，比如一个触摸屏的一个触点会上报 X、Y 位置信息，也可能会上报压力值 驱动程序上报完一系列的数据后，会上报一个“同步事件”，表示数据上报完毕。APP 读到“同步事件”时，就知道已经读完了当前的数据 同步事件也是一个 input_event 结构体，它的 type、code、value 三项都是 0 7.2.3调试技巧\r确定设备信息\nls /dev/input/* -l\n可以看到设备\n确定设备对应的硬件\ncat /proc/bus/input/devices\n7.3不用库的应用编程\rAPP访问硬件有4种方式：查询方式、休眠-唤醒、poll方式、异步通知\n通过 ioctl 获取设备信息：\nint ioctl(int fd, unsigned long request, ...); 比如要读取输入设备的 evbit 时，ioctl 的 request 要写为“EVIOCGBIT(0, size)\n7.3.1查询、休眠唤醒方式\rif (argc \u0026lt; 2) { printf(\u0026#34;Usage: %s \u0026lt;dev\u0026gt; [noblock]\\n\u0026#34;, argv[0]); return -1; } if (argc == 3 \u0026amp;\u0026amp; !strcmp(argv[2], \u0026#34;noblock\u0026#34;)) { fd = open(argv[1], O_RDWR | O_NONBLOCK); } else { fd = open(argv[1], O_RDWR); } while (1) { len = read(fd, \u0026amp;event, sizeof(event)); if (len == sizeof(event)) { printf(\u0026#34;get event: type = 0x%x, code = 0x%x, value = 0x%x\\n\u0026#34;, event.type, event.code, event.value); } else { printf(\u0026#34;read err %d\\n\u0026#34;, len); } } 7.3.2poll/select方式\rPOLL 机制、SELECT 机制是完全一样的，只是 APP 接口函数不一样\n第8章 网络通信\r8.1网络通信概述\r实际还是数据传输\n数据传输三要素：源、目的、长度。\n现在假设A、B两台计算机通讯，一台为服务器，一台为客户端。\n访问网站的话，设计网站服务器、浏览器，网站服务器平时安静地呆着，浏览器主动发起数据请求\n服务器：被动地响应请求\n客户端：主动地发起请求\n数据传输两种方式：TCP、UDP。\nTCP：可靠的、有重传机制（确保数据真到达）、适合控制命令\nUDP：不可靠的（适合视频、语音等），视频通话时，使用 UDP，偶尔的丢包、偶尔的花屏时可以忍受的\n怎么写程序？\n仿照文件读写\n将fd=open(”文件名“);\nread(fd,buf,len);//源、目的、长度\n8.1.1IP和端口\r数据传输：源、目的、长度\n源端口区分不同浏览器，目的端口区分想要服务\n8.1.3两种传输方式TCP/UDP\r网络协议5层：物理层、数据链路层、网络层、传输层、应用层\n应用层：直接为用户进程提供服务，如支持www的HTTP协议、电子邮箱SMTP协议、文件传输FTP协议，DNS、POP3、FNMP、Telnet\n传输层：负责向两个主机中进程之间的通信提供服务，主要TCP、UDP\n8.2TCP网络编程\r服务端\rsocket函数 int socket(int domain, int type,int protocol); 用于创建套接字，根本不涉及什么源、目的、长度，\nbind函数 fd = socket(.....); int bind(int sockfd, struct sockaddr *my_addr, int addrlen); 从函数用于将地址绑定到一个套接字（自己的IP端口）\n把fd和ip端口绑定，以后就检测这个ip端口看看数据有没有\nlisten函数 int listen(int sockfd,int backlog); 启动检测数据\naccept函数 int accept(int sockfd, struct sockaddr *addr,int *addrlen); 建立一条连接，接收连接\nsend函数 ssize_t send(int sockfd, const void *buf, size_t len, int flags); 发数据\nrecv函数 ssize_t recv(int sockfd, void *buf, size_t len, int flags); 收数据\n客户端\rsocket函数 fd=socket(......) connect函数 int connect(int sockfd, struct sockaddr * serv_addr,int addrlen); 建立连接\nrecv send 8.3UDP网络编程\r服务端\rsocket函数 int socket(int domain, int type,int protocol); 用于创建套接字，根本不涉及什么源、目的、长度，\nbind函数 fd = socket(.....); int bind(int sockfd, struct sockaddr *my_addr, int addrlen); 从函数用于将地址绑定到一个套接字（自己的IP端口）\nrecvfrom ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); recvfrom 通常用于无连接套接字，因为此函数可以获得发送者的地址。\nsrc_addr 是一个 struct sockaddr 类型的变量，该变量保存源机的 IP 地\n址及端口号。\naddrlen 常置为 sizeof （struct sockaddr）。\nsendto ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); 客户端\rsocket函数 fd=socket(......) connect函数 int connect(int sockfd, struct sockaddr * serv_addr,int addrlen); 建立连接\nrecvfrom,recv sendto,send 第9章 多线程编程\r9.1线程的使用\r9.1.1为什么使用多线程\r编写代码时有时候要做2件事，如一边屏幕播放视频，一边等待用户按键操作\n如果使用多线程，线程1单独处理按键，线程2单独处理播放\n9.1.2线程概念\r操作系统所能调度的最小单位，普通的进程，只有一个线程执行逻辑。\n我们可以通过多线程编程，使一个进程可以去执行多个不同的任务\n相比多进程编程，线程享有共享资源，在进程中出现的全局变量，每个线程都可以访问\n9.1.3线程的标识pthread_t\r对进程：每个进程有唯一PID\n对线程：有tid，本质是pthread_t类型的变量\n测试例程\n#include \u0026lt;pthread.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; int main() { pthread_t tid = pthread_self();//获取主线程的 tid 号 printf(\u0026#34;tid = %lu\\n\u0026#34;,(unsigned long)tid); return 0; } gcc xxx.c -lpthread 方可编译多线程程序\n9.1.4线程的创建\rpthread_create int pthread_create( pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg); 例程\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; static void *my_thread_func(void *data) { while (1) { sleep(1); } } int main(int argc, char const *argv[]) { pthread_t tid; int ret; /*1.创建接收线程*/ ret = pthread_create(\u0026amp;tid,NULL,my_thread_func,NULL); if(ret) { printf(\u0026#34;pthread_create err\\n\u0026#34;); return -1; } /*2.主线程读取标准输入，发送给接受线程*/ while(1) { sleep(1); } return 0; } 9.1.5同步操作-向线程传数据(等待)\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; static char g_buf[1000]; static int g_hasData = 0; static void *my_thread_func(void *data) { while (1) { /*等待通知*/ while(g_hasData == 0); /*打印*/ printf(\u0026#34;recv: %s\u0026#34;,g_buf); g_hasData = 0; } } int main(int argc, char const *argv[]) { pthread_t tid; int ret; /*1.创建接收线程*/ ret = pthread_create(\u0026amp;tid,NULL,my_thread_func,NULL); if(ret) { printf(\u0026#34;pthread_create err\\n\u0026#34;); return -1; } /*2.主线程读取标准输入，发送给接受线程*/ while(1) { fgets(g_buf,1000,stdin); /*通知接受线程*/ g_hasData = 1; } return 0; } 9.1.6同步操作(信号量)\r// 初始化 #include \u0026lt;semaphore.h\u0026gt; static sem_t sem; int sem_init(sem_t *sem,int pshared,unsigned int value); 等待/释放： #include \u0026lt;pthread.h\u0026gt; int sem_wait(sem_t *sem); int sem_post(sem_t *sem); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;semaphore.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; static char g_buf[1000]; static sem_t g_sem; static void *my_thread_func(void *data) { while (1) { /*等待通知*/ sem_wait(\u0026amp;g_sem); /*打印*/ printf(\u0026#34;recv: %s\u0026#34;,g_buf); } } int main(int argc, char const *argv[]) { pthread_t tid; int ret; /*初始化信号量*/ sem_init(\u0026amp;g_sem,0,0); /*1.创建接收线程*/ ret = pthread_create(\u0026amp;tid,NULL,my_thread_func,NULL); if(ret) { printf(\u0026#34;pthread_create err\\n\u0026#34;); return -1; } /*2.主线程读取标准输入，发送给接受线程*/ while(1) { fgets(g_buf,1000,stdin); /*通知接受线程*/ sem_post(\u0026amp;g_sem); } return 0; } 9.1.7同步操作(互斥量)\rstatic pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_lock(\u0026amp;g_tMutex); pthread_mutex_unlock(\u0026amp;g_tMutex); 给不希望打断的内容加锁\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include \u0026lt;pthread.h\u0026gt; #include \u0026lt;semaphore.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; static char g_buf[1000]; static sem_t g_sem; static pthread_mutex_t g_tMutex = PTHREAD_MUTEX_INITIALIZER; static void *my_thread_func(void *data) { while (1) { /*等待通知*/ sem_wait(\u0026amp;g_sem); /*打印*/ pthread_mutex_lock(\u0026amp;g_tMutex); printf(\u0026#34;recv: %s\u0026#34;,g_buf); pthread_mutex_unlock(\u0026amp;g_tMutex); } } int main(int argc, char const *argv[]) { pthread_t tid; int ret; char buf[1000]; /*初始化信号量*/ sem_init(\u0026amp;g_sem,0,0); /*1.创建接收线程*/ ret = pthread_create(\u0026amp;tid,NULL,my_thread_func,NULL); if(ret) { printf(\u0026#34;pthread_create err\\n\u0026#34;); return -1; } /*2.主线程读取标准输入，发送给接受线程*/ while(1) { fgets(buf,1000,stdin); pthread_mutex_lock(\u0026amp;g_tMutex); memcpy(g_buf,buf,1000); pthread_mutex_unlock(\u0026amp;g_tMutex); /*通知接受线程*/ sem_post(\u0026amp;g_sem); } return 0; } ","date":"2026-02-07T10:30:00+08:00","permalink":"/p/embedded-06/","title":"嵌入式 Linux 学习笔记（06）：输入系统、网络编程与多线程"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 05 / 7 篇：Framebuffer 与文字显示。返回系列目录。\n第5章、Framebuffer应用编程\r5.1LCD操作原理\rFramebuffer是Linux/Unix 下“最直接”的图形输出接口，可以通过Framebuffer驱动程序来控制LCD，Frame是帧，buffer是缓冲，意思是Framebuffer是一块内存，里面保存一帧图像的每一个像素值。\n假设LCD分辨率1024*768，颜色32位，那\nFramebuffer大小1024*768*32/8=3145728 字节\nLCD操作原理：\n驱动程序设置好 LCD 控制器\n根据 LCD 的参数设置 LCD 控制器的时序、信号极性；\n根据 LCD 分辨率、BPP 分配 Framebuffer\nAPP 使用 ioctl 获得 LCD 分辨率、BPP\nAPP 通过 mmap 映射 Framebuffer，在 Framebuffer 中写入数据\n每一块数据与LCD每一个像素一一对应 bpp：bits per pixel 每个像素用多少位表示颜色 假设需要设置 LCD 中坐标(x,y)处像素的颜色\n(x，y)像素起始地址=fb_base+(xres*bpp/8)*y + x*bpp/8\n怎么显示颜色？\n5.2 涉及的API函数\r流程：打开 LCD 设备节点，获取分辨率等参数，映射Framebuffer，最后实现描点函数\n5.2.2 ioctl函数\rioctl 是 Unix/Linux 里最“万能”的系统调用——“I/O 控制” 万金油，几乎任何驱动都能通过它暴露“命令 + 数据”接口\n#include \u0026lt;sys/ioctl.h\u0026gt;\nint ioctl(int fd, unsigned long request, \u0026hellip;);\nfd 表示文件描述符 request 表示与驱动程序交互的命令，用不同的命令控制驱动程序输出我们需要的数据 … 表示可变参数 arg，根据 request 命令，设备驱动程序返回输出的数据 返回值，成功返回文件描述符，失败返回-1 5.2.3 mmap 函数\r头文件：\n#include \u0026lt;sys/mman.h\u0026gt;\n函数原型：\nvoid *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);\n参数：\naddr 表示指定映射的內存起始地址，通常设为 NULL 表示让系统自动选定地址，并在成功映射后返回该地址\nlength 表示将文件中多大的内容映射到内存中\nprot 表示映射区域的保护方式，可以为以下 4 种方式的组合\n◼ PROT_EXEC 映射区域可被执行\n◼ PROT_READ 映射区域可被读出\n◼ PROT_WRITE 映射区域可被写入\n◼ PROT_NONE 映射区域不能存取\nFlags 表示影响映射区域的不同特性，常用的有以下两种\n◼ MAP_SHARED 表示对映射区域写入的数据会复制回文件内，原来的文\n件会改变。\n◼ MAP_PRIVATE 表示对映射区域的操作会产生一个映射文件的复制，对此区域的任何修改都不会写回原来的文件内容中。\n返回值：若成功映射，将返回指向映射的区域的指针，失败将返回-1。\n5.3程序\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 #include \u0026lt;sys/types.h\u0026gt;\t//open() #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt;\t//close() #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;sys/ioctl.h\u0026gt;\t//ioctl() #include \u0026lt;sys/mman.h\u0026gt;\t//mmap() #include \u0026lt;linux/fb.h\u0026gt;\tstatic int fd_fb; static struct fb_var_screeninfo var;\t/* Current var */ static int screen_size; static unsigned char *fb_base; static unsigned int line_width; static unsigned int pixel_width; void lcd_put_pixel(int x, int y, unsigned int color); int main(int argc, char **argv) { int i; fd_fb = open(\u0026#34;/dev/fb0\u0026#34;,O_RDWR); if(fd_fb \u0026lt; 0) { printf(\u0026#34;can\u0026#39;t open /dev/fb0\\n\u0026#34;); return -1; } if(ioctl(fd_fb, FBIOGET_VSCREENINFO, \u0026amp;var)\u0026lt;0) { printf(\u0026#34;can\u0026#39;t get var\\n\u0026#34;); close(fd_fb); return -1; } line_width\t= var.xres * var.bits_per_pixel / 8; pixel_width = var.bits_per_pixel / 8; screen_size = var.xres * var.yres * var.bits_per_pixel / 8; fb_base = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if(fb_base == (unsigned char *)-1) { printf(\u0026#34;can\u0026#39;t mmap\\n\u0026#34;); return -1; } /* 清屏: 全部设为白色 */ memset(fb_base, 0xff, screen_size); for (i = 0; i \u0026lt; 100; i++) lcd_put_pixel(var.xres/2+i, var.yres/2, 0xFF0000); munmap(fb_base , screen_size); close(fd_fb); return 0; } void lcd_put_pixel(int x, int y, unsigned int color) { unsigned char *pen_8 = fb_base+y*line_width+x*pixel_width; unsigned short *pen_16;\tunsigned int *pen_32;\tunsigned int red, green, blue;\tpen_16 = (unsigned short *)pen_8; pen_32 = (unsigned int *)pen_8; switch (var.bits_per_pixel) { case 8: { *pen_8 = color; break; } case 16: { /* 565 */ red = (color \u0026gt;\u0026gt; 16) \u0026amp; 0xff; green = (color \u0026gt;\u0026gt; 8) \u0026amp; 0xff; blue = (color \u0026gt;\u0026gt; 0) \u0026amp; 0xff; color = ((red \u0026gt;\u0026gt; 3) \u0026lt;\u0026lt; 11) | ((green \u0026gt;\u0026gt; 2) \u0026lt;\u0026lt; 5) | (blue \u0026gt;\u0026gt; 3); *pen_16 = color; break; } case 32: { *pen_32 = color; break; } default: { printf(\u0026#34;can\u0026#39;t surport %dbpp\\n\u0026#34;, var.bits_per_pixel); break; } } } 5.4上机\rarm-buildroot-linux-gnueabihf-gcc -o show_pixel show_pixel.c\nadb上传后执行\n第6章文字显示\r6.1字符编码方式\r6.1.1编码与字体\r1.ASCII\n常用字母就 26 个，区分大小写、加上标点符号也没超过 127 个，每个字符用一个字节来表示就足够了。一个字节的 7 位就可以表示 128 个数值，在 ASCII 码中最高位永远是 0\n2.ANSI\n强烈建议阅读：https://www.cnblogs.com/malecrab/p/5300486.html\n如在中国大陆地区，ANSI 的默认编码是 GB2312；在港澳台地区默认编码是 BIG5\n不同文字系统ANSI表示不同编码\n3.UniCode\nUNICODE 中的数值范围是 0x0000 至 0x10FFFF，有 1,114,111 即 100 多万个数值，可以表示 100 多万个字符，足够地球人使用了。\n向下兼容ascii\n6.1.2UniCode编码实现\r“中” UNICODE 值是 0x4e2d，在 TXT 文件中怎么表示 0x4e2d？直接写入 0x4e2d？\n不行！\n比如在 TXT 文件中写入 2 字节数据“0x2d 0x4e”，它可以用来表示“中”字吗？\n不能！它们对应 ASCII 字符“-N”。\n问题的关键在于：怎么断字\n那怎么表示一个unicode\n1.使用3个字节\n太浪费，比如字符 A 的 UNICOCDE 值是0x41，难道也用“0x41 0x00 0x00”这 3 个字节来表示？\n2.UCS-2 Little endian/UTF-16 LE\n能表示2^16=65536 个字符\nLittle endian 表示小字节序，数值中权重低的字节放在前面，比如字符“A 中”在 TXT 文件中的数值如下，其中的“A”使用“0x41 0x00”两字节表示；“中”使用“0x2d 0x4e”两字节表示。文件开头的“0xff 0xfe”表示“UTF-16 LE”。\n3.UCS-2 Big endian/UTF-16 BE\n与2相反\n4.UTF8\n上面两种方法：表示字符数量有限、对于ASCII字符浪费空间、如果某字节丢失后面字符都因为错位无法显示\n使用UTF8可以解决上述所有问题。UTF8 是变长的编码方法，有 2 种 UTF8格式的文件：带有头部、不带头部。\n对ascii字符在 UTF8 文件中直接用其 ASCII 码来表示，0x61 表示字符 a、0x62 表示字符 b\n而3 个字节“0xe4 0xb8 0xad”表示的数值是 0x4e2d，对应“中”的 UNICODE 码\n0xe4 的二进制是“11100100”，高位有 3 个 1，表示从当前字节起有3字节参与表示UNICODE； 0xb8 的二进制是“10111000”，高位有 1 个 1，表示从当前字节起有1字节参与表示UNICODE； 0xad 的二进制是“10101101”，高位有 1 个 1，表示从当前字节起有1字节参与表示 UNICODE 除去高位的“1110”、“10”、“10”后，剩下的二进制数组合起来得到“01001110001101”，它就是 0x4e2d，即“中”的 UNICODE 值 6.2 ASCII点阵显示\rLCD中显示ASCII字符，首先要找到字符对应的点阵，在linux内核源码有这个文件\nlib\\fonts\\font_8x16.c以数组形式保存字符点阵。\n储存形式：\n要显示某个字符时，根据它的 ASCII 码在 fontdata_8x16 数组中找到它的点阵，然后取出这 16 个字节去描画 16 行像素.\n比如字符 A 的 ASCII 值是 0x41，那么从 fontdata_8x16[0x41*16]开始取其点阵数据\n6.2.1获取点阵\r对于字符c\nunsigned char *dots = (unsigned char *)\u0026amp;fontdata_8x16[c*16];\n6.2.2描点\r因为有十六行，所以首先要有一个循环 16 次的大循环，然后每一行里有 8位，那么在每一个大循环里也需要一个循环 8 次的小循环。小循环里的判断单行的描点情况，如果是 1，就填充白色，如果是 0 就填充黑色。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 #include \u0026lt;sys/types.h\u0026gt;\t//open() #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt;\t//close() #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;sys/ioctl.h\u0026gt;\t//ioctl() #include \u0026lt;sys/mman.h\u0026gt;\t//mmap() #include \u0026lt;linux/fb.h\u0026gt; #define FONTDATAMAX 4096 static const unsigned char fontdata_8x16[FONTDATAMAX] = { //...略 }; int fd_fb; struct fb_var_screeninfo var;\t/* Current var */ int screen_size; unsigned char *fbmem; unsigned int line_width; unsigned int pixel_width; /********************************************************************** * 函数名称： lcd_put_pixel * 功能描述： 在LCD指定位置上输出指定颜色（描点） * 输入参数： x坐标，y坐标，颜色 * 输出参数： 无 * 返 回 值： 会 * 修改日期 版本号 修改人\t修改内容 * ----------------------------------------------- * 2020/05/12\tV1.0\tzh(angenao)\t创建 ***********************************************************************/ void lcd_put_pixel(int x, int y, unsigned int color) { unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width; unsigned short *pen_16;\tunsigned int *pen_32;\tunsigned int red, green, blue;\tpen_16 = (unsigned short *)pen_8; pen_32 = (unsigned int *)pen_8; switch (var.bits_per_pixel) { case 8: { *pen_8 = color; break; } case 16: { /* 565 */ red = (color \u0026gt;\u0026gt; 16) \u0026amp; 0xff; green = (color \u0026gt;\u0026gt; 8) \u0026amp; 0xff; blue = (color \u0026gt;\u0026gt; 0) \u0026amp; 0xff; color = ((red \u0026gt;\u0026gt; 3) \u0026lt;\u0026lt; 11) | ((green \u0026gt;\u0026gt; 2) \u0026lt;\u0026lt; 5) | (blue \u0026gt;\u0026gt; 3); *pen_16 = color; break; } case 32: { *pen_32 = color; break; } default: { printf(\u0026#34;can\u0026#39;t surport %dbpp\\n\u0026#34;, var.bits_per_pixel); break; } } } /********************************************************************** * 函数名称： lcd_put_ascii * 功能描述： 在LCD指定位置上显示一个8*16的字符 * 输入参数： x坐标，y坐标，ascii码 * 输出参数： 无 * 返 回 值： 无 * 修改日期 版本号 修改人\t修改内容 * ----------------------------------------------- * 2020/05/12\tV1.0\tzh(angenao)\t创建 ***********************************************************************/ void lcd_put_ascii(int x, int y, unsigned char c) { unsigned char *dots = (unsigned char *)\u0026amp;fontdata_8x16[c*16]; int i, b; unsigned char byte; for (i = 0; i \u0026lt; 16; i++) { byte = dots[i]; for (b = 7; b \u0026gt;= 0; b--) { if (byte \u0026amp; (1\u0026lt;\u0026lt;b)) { /* show */ lcd_put_pixel(x+7-b, y+i, 0xffffff); /* 白 */ } else { /* hide */ lcd_put_pixel(x+7-b, y+i, 0); /* 黑 */ } } } } int main(int argc, char **argv) { fd_fb = open(\u0026#34;/dev/fb0\u0026#34;, O_RDWR); if (fd_fb \u0026lt; 0) { printf(\u0026#34;can\u0026#39;t open /dev/fb0\\n\u0026#34;); return -1; } if (ioctl(fd_fb, FBIOGET_VSCREENINFO, \u0026amp;var)) { printf(\u0026#34;can\u0026#39;t get var\\n\u0026#34;); return -1; } line_width = var.xres * var.bits_per_pixel / 8; pixel_width = var.bits_per_pixel / 8; screen_size = var.xres * var.yres * var.bits_per_pixel / 8; fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if (fbmem == (unsigned char *)-1) { printf(\u0026#34;can\u0026#39;t mmap\\n\u0026#34;); return -1; } /* 清屏: 全部设为黑色 */ memset(fbmem, 0, screen_size); lcd_put_ascii(var.xres/2, var.yres/2, \u0026#39;A\u0026#39;); /*在屏幕中间显示8*16的字母A*/ munmap(fbmem , screen_size); close(fd_fb); return 0;\t} 6.2.3show_string\r1 2 3 4 5 6 7 8 void lcd_put_string(int x, int y, unsigned char * str) { while (*str) { lcd_put_ascii(x, y, *str); /* 每字符宽 8 像素 */ x += 8; /* 横排前进 8 像素 */ ++str; } } 6.3中文字符的点阵显示\r6.3.1指定编码格式\rtest_charset_ansi.c\ntest_charset_utf8.c\n可以对test_charset_ansi.c将原文件的GB2312格式，输出为可执行文件时以UTF8格式编码\ngcc -finput-charset=GB2312 -fexec-charset=UTF-8 -o test_charset_ansi test_charset_ansi.c 如果不指定“-finput-charset”，GCC 就会默认 C 程序的编码方式为 UTF-8，即使你是以 ANSI 格式保存，也会被当作 UTF-8 来对待\n如果不指定“-fexec-charset”，GCC 就会默认编译出的可执行程序中字符的编码方式为 UTF-8\n如果“-finput-charset”与“-fexec-charset”不一样，编译器会进行格式转换\n6.3.3汉字区位码\rHZK16 这个文件，它是常用汉字的 16*16 点阵字库\n每个汉字用32字节描述\nHZK16 中是以 GB2312 编码值来查找点阵以“中”字为例，它的编码值是“0xd6 0xd0”，其中的 0xd6 表示“区码”，表示在哪一个区：第“0xd6 - 0xa1”区；其中的 0xd0 表示“位码”，表示它是这个区里的哪一个字符：第“0xd0 -0xa1”个。每一个区有 94 个汉字。区位码从 0xa1 而不是从 0 开始，是为了兼容 ASCII 码\n中字：第(0xd6-0xa1)*94+(0xd0-0xa1)个字符\n6.4交叉编译程序：以freetype为例\r使用 buildroot 来给 ARM 板编译程序、编译库会很简单，以后系统讲解buildroot 时再使用 buildroot\n现在我们还是手工交叉编译 freetype\n6.4.1程序运行的基本知识\r1.编译程序时去哪找头文件\n系统目录：交叉编译工具链的某include目录\n自己指定：编译时-I dir\n2.链接时去哪找库文件\n系统目录：就是交叉编译工具链里的某个 lib 目录\n自己指定：链接时用 “ -L dir ”\n3.运行时去哪找库文件\n系统目录：就是板子上的/lib、/usr/lib 目录\n自己指定：运行程序用环境变量 LD_LIBRARY_PATH 指定\n4.运行时不需要头文件，因此头文件不需要放板子上\n6.4.2常见错误\r1.头文件\n编译时找不到头文件。在程序中这样包含头文件：#include \u0026lt;xxx.h\u0026gt;\n怎么找到头文件\n系统目录：交叉编译工具链的某个include目录\n怎么找到系统目录？\necho \u0026#39;main(){}\u0026#39;| arm-buildroot-linux-gnueabihf-gcc -E -v - 2.库文件\n链接程序时如果有这样的提示：undefined reference to `xxx\u0026rsquo;，它表示 xxx 函数未定义\n解决方法： 写出这个函数，或使用库函数，即在链接时指定库\n怎么指定：\n想链接 libabc.so，那链接时加上：-labc\n库在哪：\n系统目录：就是交叉编译工具链里的某个 lib 目录 以自己指定：链接时用 “ -L dir ”选项指定 3.运行问题\n运行时找不到库，库在哪？\n系统目录：就是板子上的/lib、/usr/lib 目录\n自己指定：\nexport LD_LIBRARY_PATH=/xxx_dir; ./test 或\nLD_LIBRARY_PATH=/xxx_dir ./test 6.4.3交叉编译万能命令\r如果交叉编辑工具链的前缀是arm-buildroot-linux-gnueabihf-\n交叉编译开源软件时，如果有configure文件\n万能命令如下：\n./configure --host=arm-buildroot-linux-gnueabihf --prefix=$PWD/tmp make make install 就可以在当前目录的 tmp 目录下看见 bin, lib, include 等目录，里面存有可执行程序、库、头文件\ncp include/freetype2/* -rfd /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/include cp lib/* -rfd /home/book/100ask_imx6ull-sdk/ToolChain/arm-buildroot-linux-gnueabihf_sdk-buildroot/bin/../lib/gcc/arm-buildroot-linux-gnueabihf/7.5.0/../../../../arm-buildroot-linux-gnueabihf/lib 6.5使用freetype显示单个文字\r6.5.1矢量字体引入\r使用点阵字库显示放大缩小会模糊甚至锯齿，为解决引入矢量字体\n矢量字体分三步：\n确定关键点 使用数学曲线连接头键点 填充闭合曲线内部空间 6.5.2在 LCD 上显示一个矢量字体\r开源字体引擎库，统一接口访问多种字体格式文件，实现矢量字体显示。\n一个文字的显示过程可以概括如下：\n给定一个字符可以确定它的编码值(ASCII、UNICODE、GB2312)； 设置字体大小； 根据编码值，从文件头部中通过 charmap 找到对应的关键点(glyph)，它会根据字体大小调整关键点； 把关键点转换为位图点阵； 在 LCD 上显示出来 freetype初始化步骤如下：\n初始化：FT_InitFreetype 加载(打开)字体 Face：FT_New_Face 设置字体大小：FT_Set_Char_Sizes 或 FT_Set_Pixel_Sizes 选择 charmap：FT_Select_Charmap 根据编码值 charcode 找到 glyph_index：glyph_index = FT_Get_Char_Index（face，charcode） 根据 glyph_index 取出 glyph：FT_Load_Glyph（face，glyph_index） 转为位图：FT_Render_Glyph 移动或旋转:FT_Set_Transform 最后显示出来 上面的5、6、7可以使用一个函数代替：FT_Load_Char(face, charcode, FT_LOAD_RENDER)，它就可以得到位图\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 #include \u0026lt;sys/mman.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;linux/fb.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;math.h\u0026gt; #include \u0026lt;wchar.h\u0026gt; #include \u0026lt;sys/ioctl.h\u0026gt; #include \u0026lt;ft2build.h\u0026gt; #include FT_FREETYPE_H #include FT_GLYPH_H int fd_fb; struct fb_var_screeninfo var;\t/* Current var */ int screen_size; unsigned char *fbmem; unsigned int line_width; unsigned int pixel_width; void lcd_put_pixel(int x, int y, unsigned int color) { unsigned char *pen_8 = fbmem+y*line_width+x*pixel_width; unsigned short *pen_16;\tunsigned int *pen_32;\tunsigned int red, green, blue;\tpen_16 = (unsigned short *)pen_8; pen_32 = (unsigned int *)pen_8; switch (var.bits_per_pixel) { case 8: { *pen_8 = color; break; } case 16: { /* 565 */ red = (color \u0026gt;\u0026gt; 16) \u0026amp; 0xff; green = (color \u0026gt;\u0026gt; 8) \u0026amp; 0xff; blue = (color \u0026gt;\u0026gt; 0) \u0026amp; 0xff; color = ((red \u0026gt;\u0026gt; 3) \u0026lt;\u0026lt; 11) | ((green \u0026gt;\u0026gt; 2) \u0026lt;\u0026lt; 5) | (blue \u0026gt;\u0026gt; 3); *pen_16 = color; break; } case 32: { *pen_32 = color; break; } default: { printf(\u0026#34;can\u0026#39;t surport %dbpp\\n\u0026#34;, var.bits_per_pixel); break; } } } void draw_bitmap(FT_Bitmap* bitmap,FT_Int x,FT_Int y) { FT_Int i, j, p, q; FT_Int x_max = x + bitmap-\u0026gt;width; FT_Int y_max = y + bitmap-\u0026gt;rows; //printf(\u0026#34;x = %d, y = %d\\n\u0026#34;, x, y); for ( j = y, q = 0; j \u0026lt; y_max; j++, q++ ) { for ( i = x, p = 0; i \u0026lt; x_max; i++, p++ ) { if ( i \u0026lt; 0 || j \u0026lt; 0 ||i \u0026gt;= var.xres || j \u0026gt;= var.yres ) continue; lcd_put_pixel(i, j, bitmap-\u0026gt;buffer[q * bitmap-\u0026gt;width + p]); } } } int main(int argc, char **argv) { wchar_t *chinese_str = L\u0026#34;繁\u0026#34;; FT_Library\tlibrary; FT_Face face; int error; FT_Vector pen; FT_GlyphSlot slot; int font_size = 24; if (argc \u0026lt; 2) { printf(\u0026#34;Usage : %s \u0026lt;font_file\u0026gt; [font_size]\\n\u0026#34;, argv[0]); return -1; } if (argc == 3) font_size = strtoul(argv[2], NULL, 0); fd_fb = open(\u0026#34;/dev/fb0\u0026#34;, O_RDWR); if (fd_fb \u0026lt; 0) { printf(\u0026#34;can\u0026#39;t open /dev/fb0\\n\u0026#34;); return -1; } if (ioctl(fd_fb, FBIOGET_VSCREENINFO, \u0026amp;var)) { printf(\u0026#34;can\u0026#39;t get var\\n\u0026#34;); return -1; } line_width = var.xres * var.bits_per_pixel / 8; pixel_width = var.bits_per_pixel / 8; screen_size = var.xres * var.yres * var.bits_per_pixel / 8; fbmem = (unsigned char *)mmap(NULL , screen_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_fb, 0); if (fbmem == (unsigned char *)-1) { printf(\u0026#34;can\u0026#39;t mmap\\n\u0026#34;); return -1; } /* 清屏: 全部设为黑色 */ memset(fbmem, 0, screen_size); /* 显示矢量字体 */ error = FT_Init_FreeType( \u0026amp;library );\t/* initialize library */ /* error handling omitted */ error = FT_New_Face( library, argv[1], 0, \u0026amp;face ); /* create face object */ /* error handling omitted */\tslot = face-\u0026gt;glyph; FT_Set_Pixel_Sizes(face, font_size, 0); /* 确定座标: */ //pen.x = 0; //pen.y = 0; /* set transformation */ //FT_Set_Transform( face, 0, \u0026amp;pen); /* load glyph image into the slot (erase previous one) */ error = FT_Load_Char( face, chinese_str[0], FT_LOAD_RENDER ); if (error) { printf(\u0026#34;FT_Load_Char error\\n\u0026#34;); return -1; } draw_bitmap( \u0026amp;slot-\u0026gt;bitmap,var.xres/2,var.yres/2); return 0;\t} 上机：\n./freetype_show_font ./simsun.ttc\n6.5.3在 LCD 上令矢量字体旋转某个角度\r定义 2 个变量：角度、矩阵\n设置角度值\n设置矩阵、变形、加载位图\n上机：\n./freetype_show_font_angle ./simsun.ttc 90 200\n6.6使用freetype显示一行文字\r在 LCD 上指定一个左上角坐标(x, y)，把一行文字显示出来。下图中，文字的外框用虚线表示，外框的左上角坐标就是(x, y)\n6.6.1笛卡尔坐标系\r在 LCD 的坐标系中，原点在屏幕的左上角。对于笛卡尔坐标系，原点在左下角。freetype 使用笛卡尔坐标系，在显示时需要转换为 LCD 坐标系\n在笛卡尔坐标系中坐标是(x, y)，那么它在 LCD 坐标系中坐标值为(x, V-y)\n6.6.2每个字符大小不同\r所以在显示一行文字时，后面文字的位置会受到前面文字的影响\n在显示一行文字时，这些文字会基于同一个基线来绘制位图：baseline\n每一个字符都有它的原点(origin)，比如上图中 baseline左边的黑色圆点就是字母“g”的原点。当前 origin 加上 advance 就可以得到下一个字符的 origin\n","date":"2026-01-24T10:30:00+08:00","permalink":"/p/embedded-05/","title":"嵌入式 Linux 学习笔记（05）：Framebuffer 与文字显示"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 04 / 7 篇：文件 IO 与系统调用。返回系列目录。\n第4章 文件IO\rLinux一切皆文件，因此文件IO非常重要\nLinux查看函数指令man\n4.1文件IO分类\rLinux上有两套函数：标准IO、系统调用IO\n标准 IO 的相关函数是：fopen/fread/fwrite/fseek/fflush/fclose 系统调用 IO的相关函数是：open/read/write/lseek/fsync/close 两套接口来源：\n系统调用IO是用户程序利用posix接口兼容访问Linux、Unix、Unix-like操作系统的功能，用于直接访问内核。\n标准IO是C库的，但是为了兼容Windows用系统调用IO重新封装了。\n标准IO引入了一个用户buffer。\n读数据的话就读一个大数据在buffer，后续对其读取就在buffer里读，但是系统调用IO就必须多次向内核申请\n4.2函数原型\r4.2.1open()\r1 2 3 4 5 6 #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; int open(const char *pathname, int flags); int open(const char *pathname, int flags, mode_t mode); 打开或创建一个文件\npathname：文件路径名或文件名\nflags：打开方式和状态标志，通过位或操作\nmode：仅当flags包含O_CREAT或 O_TMPFILE 时才需要提供，指定文件权限，指定方式同chmod\n宏 含义 访问模式（三选一） O_RDONLY 只读 O_WRONLY 只写 O_RDWR 读写 创建与控制 O_CREAT 若文件不存在则创建，需提供 mode O_EXCL 与 O_CREAT 合用，文件已存在时报错 O_TRUNC 打开时把文件截断为 0 追加/非阻塞/同步 O_APPEND 每次写都追加到文件尾 O_NONBLOCK / O_NDELAY 非阻塞模式 O_SYNC 每次写都等待物理 I/O 完成 O_DSYNC 仅同步数据，不含元数据 O_DIRECT 绕过页缓存，直接用户缓冲到磁盘 O_CLOEXEC 在执行 execve 时自动关闭 fd（防泄漏） 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 //open #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;errno.h\u0026gt; //close #include \u0026lt;unistd.h\u0026gt; /* * ./open 1.txt * argc = 2 * argv[0] = \u0026#34;./open\u0026#34; * argv[1] = \u0026#34;1.txt\u0026#34; */ int main(int argc, char **argv) { int fd; if (argc != 2) { printf(\u0026#34;Usage: %s \u0026lt;file\u0026gt;\\n\u0026#34;, argv[0]); return -1; } //打开文件,若不存在则创建，且创建时截断，权限设为0777 fd = open(argv[1],O_RDWR|O_CREAT|TRUNC,0777); if (fd \u0026lt; 0) { printf(\u0026#34;can not open file %s\\n\u0026#34;, argv[1]); printf(\u0026#34;errno = %d\\n\u0026#34;, errno); printf(\u0026#34;err: %s\\n\u0026#34;, strerror(errno)); perror(\u0026#34;open\u0026#34;); } else { printf(\u0026#34;fd = %d\\n\u0026#34;, fd); } while (1) { sleep(10); } close(fd); return 0; } 4.2.2write()\r#include \u0026lt;unistd.h\u0026gt; ssize_t write(int fd, const void *buf, size_t count); fd：指定要写入的文件描述符\nbuf：缓冲区，一般是一个数组，该数组的内容写入文件\ncount：要写入的实际字符数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;errno.h\u0026gt; int main(int argc,char *argv[]) { int fd; int i; int len; fd = open(argv[1],O_RDWR | O_CREAT | O_TRUNC,0777); if (fd \u0026lt; 0) { printf(\u0026#34;can not open file %s\\n\u0026#34;, argv[1]); printf(\u0026#34;errno = %d\\n\u0026#34;, errno); printf(\u0026#34;err: %s\\n\u0026#34;, strerror(errno)); perror(\u0026#34;open\u0026#34;); } else { printf(\u0026#34;fd = %d\\n\u0026#34;, fd); } for (i = 2; i \u0026lt; argc; i++) { len = write(fd,argv[i],strlen(argv[i])); if (len != strlen(argv[i])) { perror(\u0026#34;write\u0026#34;); break; } write(fd,\u0026#34;\\r\\n\u0026#34;,2); } close(fd); return 0; } 4.2.3read()\r1 2 #include \u0026lt;unistd.h\u0026gt; ssize_t read(int fd, void *buf, size_t count); 作用：从给定的文件描述符指定的文件中，读取 count 个字节的数据，存放至 buf 中。\nfd：指定要读写的文件描述符\nbuf：缓冲区，一般是一个数组，用于存放读取的内容\ncount：一次要读取的最大字节数\n4.2.4lseek()\r#include \u0026lt;unistd.h\u0026gt; off_t lseek(int fd, off_t offset, int whence); fd：指定要偏移的文件描述符\noffset：文件偏移量\nwhence：开始添加偏移 offset 的位置\nSEEK_SET，offset 相对于文件开头进行偏移\nSEEK_CUR，offset 相对文件当前位置进行偏移\nSEEK_END，offset 相对于文件末尾进行偏移\n4.2.5read()\r#include \u0026lt;unistd.h\u0026gt; ssize_t read(int fd, void *buf, size_t count); 作用：从给定的文件描述符指定的文件中，读取 count 个字节的数\n据，存放至 buf 中。\nfd：指定要读写的文件描述符\nbuf：缓冲区，一般是一个数组，用于存放读取的内容\ncount：一次要读取的最大字节数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;sys/types.h\u0026gt; #include \u0026lt;sys/stat.h\u0026gt; #include \u0026lt;fcntl.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;errno.h\u0026gt; int main(int argc,char **argv) { int len; int i; unsigned char buf[100]; int fd = open(argv[1],O_RDONLY,0777); while(1) { len = read(fd,buf,sizeof(buf)-1); if(len==0) break; //表示读到结尾了 buf[len] = \u0026#39;\\0\u0026#39;; printf(\u0026#34;%s\u0026#34;,buf); } close(fd); } 4.4内部机制\r用户空间应用层调用glibc给出的系统调用接口\n这些系统调用接口会触发异常引入内核\n比如触发异常：swi/svc，并异常原因事先记录在某处\ncpu发现异常后会调用内核提供的异常处理函数\n他会进行分辨原因/处理异常\n然后继续做自己的事\n如SYSCALL_DEFINE为系统调用定义，3为参数个数\n这个宏定义出这个函数\nint sys_open(const char __user * filename,int flags,umode_t mode)\n整体流程\n即用户调用glibc的接口函数，接口函数触发异常，同时把异常原因记录在某些寄存器里面，cpu捕获到异常，根据异常的原因来调用对应的函数\n每个进程有自己的文件句柄空间\nfd=3，是xx进程里fd=3的文件\n","date":"2026-01-10T10:30:00+08:00","permalink":"/p/embedded-04/","title":"嵌入式 Linux 学习笔记（04）：文件 IO 与系统调用"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 03 / 7 篇：交叉编译、GCC 与 Makefile。返回系列目录。\n第1章 交叉编译\r获取源码后，想要给ARM板编译出hello程序\narm-buildroot-linux-gnueabihf-gcc -o hello hello.c\n若hello程序如下\n01 #include \u0026lt;stdio.h\u0026gt; 02 03 /* 执行命令: ./hello weidongshan 04 * argc = 2 05 * argv[0] = ./hello 06 * argv[1] = weidongshan 07 */ 08 09 int main(int argc, char **argv) 10 { 11 if (argc \u0026gt;= 2) 12 printf(\u0026#34;Hello, %s!\\n\u0026#34;, argv[1]); 13 else 14 printf(\u0026#34;Hello, world!\\n\u0026#34;); 15 return 0; 16 } 头文件在哪\n进入交叉编译器的目录里，执行：find -name “stdio.h”，它位于一个“include”目录下的根目录里。这个“include”目录，就是要找的路径（系统目录）\n或者自己指定\n怎么指定头文件目录\n编译时，加上“-I \u0026lt;头文件目录\u0026gt;”这样的选项。\n怎么确定交叉编译时库文件\n进入交叉编译器的目录里，执行：find -name lib，可以得到 xxxx/lib、\nxxxx/usr/lib，一般来说这 2 个目录就是要找的路径。如果有很多类似的 lib，\n进去看看，有很多 so 文件的目录一般就是要找的路径\n怎么指定自己的库文件目录、库文件\n编译时，加上“-L \u0026lt;库文件目录\u0026gt;”这样的选项，用来指定库目录；\n编译时，加上“-labc”这样的选项，用来指定库文件 libabc.so。\n第2章 GCC编译器使用\r2.1GCC编译过程\r通过不同的编译选项可以控制这些过程\n2.1.2编译多个文件\r一起编译、链接\ngcc -o test main.c sub.c\n分开编译，统一链接\ngcc -c -o main.o main.c\ngcc -c -o sub.o sub.c\ngcc -o test main.o sub.o\n2.1.3制作使用动态库\rstep1：制作、编译\ngcc -c -o main.o main.c gcc -c -o sub.o sub.c gcc -shared -o libsub.so sub.o sub2.o sub3.o(可以使用多个.o 生成动态库) gcc -o test main.o -lsub -L /libsub.so/所在目录/ step2：运行\n先把 libsub.so 放到 Ubuntu 的/lib 目录，然后就可以运行 test 程序 如果不想把 libsub.so 放到/lib，也可以放在某个目录比如/a，然后执行 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/a ./test 2.1.4制作使用静态库\rgcc -c -o main.o main.c gcc -c -o sub.o sub.c ar crs libsub.a sub.o sub2.o sub3.o(可以使用多个.o 生成静态库) gcc -o test main.o libsub.a (如果.a 不在当前目录下，需要指定它的绝对或相对路径) 执行 arm-buildroot-linux-gnueabihf-gcc -c -o sub.o sub.c 交叉编译需要在最后面加上-fPIC 参数\n第3章 Makefile的使用\r3.1Makefile大纲\r3.1.1为什么需要Makefile\r修改源文件或头文件，只需要重新编译牵涉到的文件就可以重新生成APP\n3.1.2Makefile基本规则\r目标文件(target)…: 依赖文件(prerequiries)… \u0026lt;tab\u0026gt;命令(command) 3.1.3一步步完善Makefile\r1.如果没有makefile\r使用gcc -o test a.c b.c\n前三步统称为编译，最后一步链接\n很简单，但是对所有文件都再处理一次，即使b.c没有修改，a.c修改了\n优化上一步可以a.c和b.c分开编译，最后链接\ngcc -c -o a.o a.c\ngcc -c -o b.o b.c\ngcc -o test a.o b.o\n如何判断哪些文件被修改了？通过比较时间\n目标文件(target)…: 依赖文件(prerequiries)… \u0026lt;tab\u0026gt;命令(command) 当依赖文件比目标文件信，就执行命令\n2.最简单的Makefile\rtest:a.o b.o gcc -o test a.o b.o a.o : a.c gcc -c -o a.o a.c b.o : b.c gcc -c -o b.o b.c 执行make命令，此时修改a.c，只执行两条gcc命令\n3.引入通配符\r相似规则太多，可以使用通配符\ntest:a.o b.o c.o gcc -o test $^ //$^表示所有依赖 %.o:%.c gcc -c -o $@ $\u0026lt; //$@表示目标文件 $\u0026lt;表示依赖文件 %.o：通配符 $@：表示目标 $\u0026lt;：表示第一个依赖文件 $^：表示所有依赖文件 4.引入clean、.PHONY\r垃圾文件太多\ntest: a.o b.o c.o gcc -o test $^ %.o : %.c gcc -c -o $@ $\u0026lt; clean: rm *.o test .PHONY: clean\tmake：执行第一个目标\nmake [目标]：执行指定目标\n假想目标.PHONY\n5.即时变量、延时变量\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 A := $(C)\t#A的值即时确定，在定义时即确定，赋值\t#为C，C现在为空 B = $(C)\t#B的值使用到才确定 C = abc\t#C赋值字符串abc #D = 100ask # 若 D 未定义则设为 weidongshan，已定义则保持原值 D ?= weidongshan\t# 默认目标 all：打印各变量最终值 all: @echo A = $(A) # A 为空（在第 1 行已固定） @echo B = $(B) # B 为 abc123（C 后面被追加 123） @echo D = $(D) # D 为 weidongshan（未被覆盖） # C += 123 # 将 123 追加到 C 末尾，使 C 变成 abc123 C += 123 6.Makefile函数\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 #A赋值 a b b A = a b c #对A中每一个元素，取出来赋值给f，把f改为这种形式 B = $(foreach f, $(A), $(f).o) #C赋值 C = a b c d/ #对C中符合%/格式的内容，过滤留下来 D = $(filter %/, $(C)) #对C中符合%/格式的内容，过滤出去 E = $(filter-out %/, $(C)) #*.c格式的文件是否存在，把存在的列出来 files = $(wildcard *.c) files2 = a.c b.c c.c d.c e.c abc #找到file2中存在的文件列出来 files3 = $(wildcard $(files2)) #找到file2中符合%.c的文件，将其改为%.d dep_files = $(patsubst %.c,%.d,$(files2)) all: @echo B = $(B) @echo D = $(D) @echo E = $(E) @echo files = $(files) @echo files3 = $(files3) @echo dep_files = $(dep_files) 3.2Makefile实例\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 objs = a.o b.o c.o #把objs所有文件加前缀.加后缀.d dep_files := $(patsubst %,.%.d, $(objs)) #找到dep_files中存在的文件列出来 dep_files := $(wildcard $(dep_files)) #编译参数 CFLAGS = -Werror -Iinclude test: $(objs) gcc -o test $^ #如果dep_files不为空，就把这个文件包含 ifneq ($(dep_files),) include $(dep_files) endif %.o : %.c gcc $(CFLAGS) -c -o $@ $\u0026lt; -MD -MF .$@.d clean: rm *.o test distclean: rm $(dep_files) .PHONY: clean\t","date":"2025-12-27T10:30:00+08:00","permalink":"/p/embedded-03/","title":"嵌入式 Linux 学习笔记（03）：交叉编译、GCC 与 Makefile"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 02 / 7 篇：环境搭建与开发板操作。返回系列目录。\n第三篇 环境搭建与开发板操作\rwindows：\nfilezilla：传文件到ubuntu mobaxterm：远程操作ubuntu，操作开发板串口 ubuntu：\nvmware运行ubuntu当做服务器 开发板：编辑编译运行\n第2章 安装软件\r2.3MobaXterm 远程登录 Ubuntu\rubuntu命令ifconfig\nens33：为NAT网卡ip\n2.4使用FileZilla\r在Windows和ubuntu之间传文件\n2.5Ubuntu上执行程序\rtouch hello.c\ngedit hello.c\n1 2 3 4 5 6 7 #include \u0026lt;studio.h\u0026gt; int main(int argc, char **argv) { printf(\u0026#34;hello, world!\\n\u0026#34;); return 0; } gcc -o hello hello.c：编译\n./hello：执行可执行文件\n第3章 IMX6ULL开发板基本操作\r3.1开发板资源\r3.4串口连接\r启动拨码EMMC启动 电源 Windows安装串口驱动 3.4.1使用Mobaxterm打开串口\r登录名：root\n100ask login: root\n3.6ADB替代NFS传输文件\r3.6.1使用adb\r就可以在ubuntu上使用adb操作开发板\n可以上传、下载开发板文件、目录\nadb devices adb push 1.txt /root # 把Ubuntu的文件放到开发板的/root目录 adb pull /root/1.txt 2.tx # 把开发板的/root/1.txt下载并改名为2.txt adb shell # 登录开发板 第4章 开发板的第一个APP实验\r4.1源码\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include \u0026lt;stdio.h\u0026gt; /* 执行命令: ./hello weidongshan * argc = 2 * argv[0] = ./hello * argv[1] = weidongshan */ int main(int argc, char **argv) { if (argc \u0026gt;= 2) printf(\u0026#34;Hello, %s!\\n\u0026#34;, argv[1]); else printf(\u0026#34;Hello, world!\\n\u0026#34;); return 0; } 4.2编译\r给ARM板的程序要使用交叉编译工具链编译\n执行\narm-buildroot-linux-gnueabihf-gcc -o hello hello.c 4.3上传开发板运行\radb push hello /root adb shell ./root/hello 第5章 开发板第一个驱动实验\r5.1前提\r为什么编译驱动程序前先编译内核？\n驱动程序要用到内核文件 比如驱动程序包含#include \u0026lt;asm/io.h\u0026gt;，其中asm是链接文件，指向asm-arm或asm-mips，需要先配置、编译内核才会生成asm的链接文件\n编译驱动时用的内核、开发板上运行到内核，要一致 更换板子上的内核后，板子上的其他驱动也要更换 流程：\n配置编译内核、设备树、驱动 放到板子上 编译测试第一个驱动 5.2 编译内核\rcd /home/book/100ask_imx6ull-sdk/Linux-4.9.88 make mrproper make 100ask_imx6ull_defconfig make zImage -j4 make dtbs cp arch/arm/boot/zImage ~/nfs_rootfs cp arch/arm/boot/dts/100ask_imx6ull-14x14.dtb ~/nfs_rootfs/ 5.3编译内核模块\rmake modules make ARCH=arm INSTALL_MOD_PATH=/home/book/nfs_rootfs modules_install 5.4安装内核和模块到开发板\rcd /home/book/nfs_rootfs adb push zImage /boot adb push 100ask_imx6ull-14x14.dtb /boot adb push lib/modules /lib 最后，在开发板上执行“sync”命令，再重启开发板，它就使用新的 zImage、dtb、模块了。\n5.5编译led驱动\r01_all_series_quickstart\\05_嵌入式Linux驱动开发基础知识\\source\\01_hello_drv\n上传到/home/book/nfs_rootfs\n修改Makefile指定内核源码/home/book/100ask_imx6ull-sdk/Linux-4.9.88\n编译make，将这两个上传到开发板目录\nadb push hello_drv.ko /root adb push hello_drv_test /root 5.6安装驱动模块\rinsmod hello_drv.ko lsmod：查看安装的模块\n5.7执行测试模块\r先看看有没有设备ls /dev\n","date":"2025-12-13T10:30:00+08:00","permalink":"/p/embedded-02/","title":"嵌入式 Linux 学习笔记（02）：环境搭建与开发板操作"},{"content":"\r📚 本文是《嵌入式 Linux 应用开发学习笔记》系列第 01 / 7 篇：学习路线、资料下载与 Ubuntu 入门。返回系列目录。\nc第一篇 学习路线、资料下载\r000_课程链接\rhttps://linux.100ask.net/\n第1章 Linux路线\r与windows对标：\n第2章 资料下载\r🎫100ASK_IMX6ULL_PRO开发板 — 百问网资料下载中心 3.0 文档\n开发板配套资料：\n百度云网盘：链接：https://pan.baidu.com/s/1NsAIRkQj0bOaPDWdSkBuXw 密码： root 实验班配套资料：\n实验班的虚拟机里装好了vscode、下载好了Linux源码 百度云网盘下载地址： https://pan.baidu.com/s/1SjpWb0K-dL2C06DjUEuA_A?pwd=qjke 密码： qjke 配套资料：全系列Linux教程的配套资料(应用基础、驱动基础等)\n1 git clone https://e.coding.net/weidongshan/01_all_series_quickstart.git 第二篇 Ubuntu入门\r打开终端快捷键：Ctrl+Alt+T\n文件树：\n第2章Ubuntu操作入门\r2.3Linux常用命令\r命令格式\nlinux路径\n根目录：/\n家目录：~\n当前路径：.\n上一级路径：..\n查看函数指令：man\n2.3.1目录文件相关\rpwd：打印当前路径\ncd：改变路径\ncd -：进入上次目录 cd ~：进入家目录 mkdir：创建目录\nmkdir dir0 ：创建dir0 mkdir -p dir1/dir2：创建目录dir1有子目录dir2 rmdir：删除目录，空白目录\nls：列出目录内容\nls ls -l：显示完整信息 ls -a：显示隐藏文件 ls -la：所有文件、完整信息 ls -lh：h表示\u0026ndash;human-readable cp：复制文件夹\ncp -r dir1 dir2：复制dir1文件夹 cp -rfd dir_a dir_b：-r为复制所有，-f强制覆盖，-d如果源文件为链接，也只复制链接 rm：删除文件或目录\nrm -rf dir_a：-r删除所有，-f强制删除 cat：打印文件内容\ncat file.txt touch：修改文件时间，如文件不存在则创建\ntouch file 2.3.2文件权限属性\rchgrp：改变文件所属用户组\nchgrp [-r] dirname/filename：-r递归更改，更新成为用户组 chgrp hy install.log：install.log更改为hy用户组 chowm：改变文件所有者\nchmod：改变文件权限\n权限对应数字r：4、0 w:2、0 x：1、0 权限对象：owner、group、others 权限值：owner=rwx=4+2+1=7，group=rwx=4+2+1=7，other=\u0026mdash;=0+0+0=0 chmod [-r] 777 文件或目录：数字改法 chmod [-r] u=rwx,go=rx 文件或目录：字母改法用 u、g、o 三个字母代表 user、group、others chmod a+w .bashrc：a代表all，对.bashrc文件ugo增加w权限 2.3.3查找搜索命令\rfind：搜索文件\nfind 目录名 [选项] 查找条件 find /home -name “test1.txt”：/home中以名字查找text1.txt find /home -name \u0026quot; *.txt \u0026ldquo;：/home中以名字查找所有.txt文件 grep：查找文件中符合条件的字符串\ngrep [选项] [查找模式] [文件名] grep -rn \u0026ldquo;字符串\u0026rdquo; 文件名 2.3.4压缩解压命令\rgzip：\n常用选项：-l列出压缩文件内容，-k在压缩和解压时保留输入文件，-d将压缩文件进行解压 gzip -l pwd.1.gz：查看压缩文件 gzip -kd pwd.1.gz：解压文件 gzip -k mypwd.1：得到一个.gz结尾的压缩文件 bizp2：\n常用选项：-k在压缩解压时保留文件，-d将压缩文件解压 bzip2 -k mypwd.1：得到一个.bz2后缀的压缩文件 bzip2 -kd mypwd.1.bz2：解压 tar：\ntar czvf dira.tar.gz dira：把目录dira压缩、打包为dira.tar.gz文件 tar tvf dira.tar.gz：查看压缩文件 tar xzvf dira.tar.gz：解压到当前目录 tar xzvf dira.tar.gz -c /home：解压到指定目录 2.3.5vi编辑器\rvi 1.txt：vim打开1.txt，不存在则新建 输入i开启编辑 ：wq保存并退出 ：q！不保存退出 vi快捷键\n1.新建、移动、取消、保存\n2.移动\n上下左右箭头移动光标 ctrl+f：向前翻页，ctrl+b：向后翻页 ：输入行号 ：跳到某行 ：set number 显示行号 ：set nonumber 隐藏行号 G：跳到文件结尾 0：跳到行首 $：跳到行尾 3.编辑\nx：删除一个字母 dd：删除一行 ndd：删除当前行及后n-1行（n是数字） o：当前行后新增行 u：撤销上一步 4.复制粘贴\nyy：复制当前行 nyy：同理 p：粘贴 ","date":"2025-12-06T11:00:00+08:00","permalink":"/p/embedded-01/","title":"嵌入式 Linux 学习笔记（01）：学习路线、资料下载与 Ubuntu 入门"},{"content":"本系列整理自嵌入式 Linux 应用开发的学习过程（基于 IMX6ULL 开发板），原文较长，现按主题拆分为 7 篇：\n学习路线、资料下载与 Ubuntu 入门 — 学习路线对比、开发板资料获取、Linux 常用命令与 vi 编辑器 环境搭建与开发板操作 — MobaXterm / FileZilla / ADB，交叉编译第一个 APP 与 LED 驱动实验 交叉编译、GCC 与 Makefile — GCC 编译过程、动态/静态库制作、Makefile 从基本规则到工程实例 文件 IO 与系统调用 — open/read/write/lseek 函数详解及内核系统调用机制 Framebuffer 与文字显示 — LCD 操作原理、字符编码、点阵字库与 freetype 矢量字体 输入系统、网络编程与多线程 — input 子系统、TCP/UDP socket 编程、线程创建与信号量/互斥量同步 I2C 应用编程 — I2C 协议要点、I2C-Tools 操作 AP3216C 传感器、编写 APP 直接访问 EEPROM 注：原笔记中「串口 UART 编程」一章尚在学习整理中，内容补全后会更新到对应篇章。\n","date":"2025-12-06T10:00:00+08:00","permalink":"/p/embedded-linux/","title":"嵌入式 Linux 应用开发学习笔记（系列目录）"},{"content":"1.QT创建项目\r1.1创建：\rQMainWindow：窗口\nQWidget：弹窗\nQDialog：对话框\n1.2文件体系：\r1.3UI控件\rlayout：布局\nV垂直，H水平，G网格，F表单（组合控件）\nspacers：弹簧（占位）\n把控件挤一挤H水平，V垂直\nbutton：按键\nP.T差不读 R单选框 C多选\nItem Views：单元视图（数据，显示分离）\nItem Widgets：\nContainer：容器，容控件\nInput：输入\nDisplay：输出\n2.信号与槽\r如点击按键发出一个信号，对信号响应，执行一个函数，这个函数叫槽函数（事件和回调）\n三种连接方式\n3.写计算器\r设计ui\n添加槽函数\n设计框尺寸，框标题、字体\n各按钮槽函数类推\n按键插图片\n退格功能\n计算功能太难，跳过\n4.定时器\rQObject：\rstartTimer killTimer\n启动定时器\n标签显示图片\n轮换逻辑\nQTimer：\r头文件\n启动定时器\n图片法二\n槽函数\n转换逻辑\n暂停\n5.QT文件操作\rui设计\r新建\r添加槽函数\n实现槽函数\n连接功能\n打开\r头文件\nQFileDialog 开文件选择框 QMessageBox 开警告盒子 QDebug 输出文字到控制台 QFile 读取文件内容 添加槽函数\n连接功能\n实现槽函数\nvoid MainWindow::openActionSlot() { //括号里的参数分别是：指定父类、标题、默认打开后显示的目录、右下角的文件过滤器。 QString filename = QFileDialog::getOpenFileName(this, \u0026#34;选择一个文件\u0026#34;, \u0026#34;.\u0026#34;, \u0026#34;*.txt\u0026#34;); qDebug() \u0026lt;\u0026lt; filename; if(filename.isEmpty()) { QMessageBox::warning(this,\u0026#34;警告\u0026#34;,\u0026#34;请选择一个文件\u0026#34;); } else { qDebug()\u0026lt;\u0026lt;filename; QFile file(filename); file.open(QIODevice::ReadOnly); QByteArray array = file.readAll();\t//读取文件所有内容 ui-\u0026gt;textEdit-\u0026gt;setText(QString(array)); file.close(); } } 保存\r添加槽函数\n连接功能\n实现槽函数\nvoid MainWindow::saveActionSlot() { QString filename = QFileDialog::getSaveFileName(this, \u0026#34;选择一个文件\u0026#34;, \u0026#34;.\u0026#34; //当前路径的话是QCoreApplicationFilePath() ); //不写就是所有文件 if(filename.isEmpty()) { QMessageBox::warning(this,\u0026#34;警告\u0026#34;,\u0026#34;请选择一个文件\u0026#34;); } else { QFile file(filename); file.open(QIODevice::WriteOnly); QString str = ui-\u0026gt;textEdit-\u0026gt;toPlainText(); QByteArray array = str.toLocal8Bit(); //array.append(ui-\u0026gt;textEdit-\u0026gt;toPlainText()); file.write(array); file.close(); } } 6.QT事件\r键盘事件\rctrl+s实现保存，即打开保存原文件\n重写键盘事件虚函数\n#include void keyPressEvent(QKeyEvent *k);\nvoid MainWindow::keyPressEvent(QKeyEvent *k) { if(k-\u0026gt;modifiers() == Qt::ControlModifier) { // 如果按下了CTRL键 if(k-\u0026gt;key() == Qt::Key_S){ qDebug()\u0026lt;\u0026lt;\u0026#34;CTRL + S\u0026#34;; saveActionSlot(); } } } 鼠标事件\r#include void mousePressEvent(QMouseEvent *m);\nvoid MainWindow::mousePressEvent(QMouseEvent *m) { QPoint pt = m-\u0026gt;pos(); qDebug()\u0026lt;\u0026lt;pt; if(m-\u0026gt;button() == Qt::LeftButton) { qDebug()\u0026lt;\u0026lt;\u0026#34;左键被按下\u0026#34;; } if(m-\u0026gt;button() == Qt::RightButton) { qDebug()\u0026lt;\u0026lt;\u0026#34;右键被按下\u0026#34;; } } 7.QT TCP\r服务器 客户端 socket socket bind connect listen send/recv accept send/recv 与网络相关的在工程里加network\nTCP前置\n客户端\rUI设计\r编辑槽函数\n#include void Widget::on_connectButton_clicked() { //获取ip地址和端口号 QString IP = ui-\u0026gt;ipLineEdit-\u0026gt;text(); QString PORT = ui-\u0026gt;portLineEdit-\u0026gt;text(); //连接服务器 socket-\u0026gt;connectToHost(QHostAddress(IP),PORT.toShort()); //连接服务器成功，socket对象发出信号 connect(socket,\u0026amp;QTcpSocket::connected,[this]() { QMessageBox::information(this,\u0026#34;连接提示\u0026#34;,\u0026#34;连接服务器成功\u0026#34;); }); //连接断开，socket也会发出信号 connect(socket,\u0026amp;QTcpSocket::disconnected,[this](){ QMessageBox::warning(this,\u0026#34;连接提示\u0026#34;,\u0026#34;连接异常 网络断开\u0026#34;); }); } 服务器\rUI设计\r头文件\r#include #include 创建槽函数\r监听\rWidget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-\u0026gt;setupUi(this); server = new QTcpServer; server-\u0026gt;listen(QHostAddress::AnyIPv4,PORT); //客户端发起连接，server发出信号 connect(server,\u0026amp;QTcpServer::newConnection,this,\u0026amp;Widget::newClientHandler); } 实现槽函数\rvoid Widget::newClientHandler() { //建立TCP连接 QTcpSocket *socket = server-\u0026gt;nextPendingConnection(); //socket-\u0026gt;peerAddress(); //获取客户端地址 //socket-\u0026gt;peerPort();//获取端口号 ui-\u0026gt;iplineEdit-\u0026gt;setText(socket-\u0026gt;peerAddress().toString()); ui-\u0026gt;portlineEdit-\u0026gt;setText(QString::number(socket-\u0026gt;peerPort())); } 8.QT启动新窗口（接服务器，实现发数据）\r客户端\r对原串口对象连接成功后可以使用\nthis-\u0026gt;hide();隐藏\n在原界面头文件引入新界面对象头文件\n#include \u0026lt;chat.h\u0026gt;\n在原界面需要显示新界面的时候声明对象，像Widget w; w.show()一样\n但是新对象要使用socket,因此要传入\nChat *c =new Chat(socket);//堆空间创建 此时需要添加类Chat头文件和成员\n#include \u0026lt;QTcpSocket\u0026gt; 构造函数进行一个赋值\nChat::Chat(QTcpSocket *s,QWidget *parent) : QWidget(parent) , ui(new Ui::Chat) { ui-\u0026gt;setupUi(this); socket = s; } 服务器\r实现槽函数\nvoid Widget::clientInfoSlot() { QTcpSocket *s = (QTcpSocket *)sender();//获取信号的发出者 QString str = QString(s-\u0026gt;readAll()); ui-\u0026gt;mainlineEdit-\u0026gt;setText(str); } 实现了客户端发送，服务器接收并显示\n9.QT多线程\r接TCP，实现服务器多线程，多客户端连接一个服务器\n创建线程类\r#include \u0026lt;QThread\u0026gt; //继承自QThread class my_thread : public QThread { } 启动线程\r//启动线程在widget.cpp my_thread *t = new my_thread(socket);//创建线程对象 t-\u0026gt;start();//开始线程 重写虚函数\rpublic: explicit my_thread(QTcpSocket *s,QThread *parent = nullptr); //重写虚函数 void run(); 修改类\r添加成员，修改构造函数参数，在构造函数进行赋值\n.h class my_thread : public QThread { Q_OBJECT public: explicit my_thread(QTcpSocket *s,QThread *parent = nullptr); //重写虚函数 void run(); private slots: void clientInfoSlot(); private: QTcpSocket *socket; }; .cpp my_thread::my_thread(QTcpSocket *s,QThread *parent) : QThread{parent} { socket = s; } 实现虚函数和槽函数\rvoid my_thread::run() { connect(socket,\u0026amp;QTcpSocket::readyRead,this,\u0026amp;my_thread::clientInfoSlot); } void my_thread::clientInfoSlot() { qDebug()\u0026lt;\u0026lt;socket-\u0026gt;readAll(); } 10.自定义信号\r继QT多线程\n子线程\r在my_thread类中创建自定义信号\nsignals: void sendToWidget(QByteArray b); //my_thread.cpp void my_thread::clientInfoSlot() { //qDebug()\u0026lt;\u0026lt;socket-\u0026gt;readAll(); QByteArray ba = socket -\u0026gt;readAll(); emit sendToWidget(ba);//发送信号 } 主线程\r接收数据链接槽函数\n实现槽函数\n11.MySQL数据库\rMySQL部分\r1 2 3 4 5 6 7 8 9 create database mydatabase; create table student( id integer unsigned primary key, name varchar(16) not null, birth date )charset utf8; select * from student; QT部分\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 //.h #include \u0026lt;QSqlDatabase\u0026gt; #include \u0026lt;QMessageBox\u0026gt; ... private: QSqlDatabase db; //.cpp Widget::Widget(QWidget *parent) : QWidget(parent) , ui(new Ui::Widget) { ui-\u0026gt;setupUi(this); //连接数据库 db = QSqlDatabase::addDatabase(\u0026#34;QMYSQL\u0026#34;);//加载MySQL数据库驱动 db.setDatabaseName(\u0026#34;mydatabase\u0026#34;); db.setHostName(\u0026#34;localhost\u0026#34;); db.setUserName(\u0026#34;root\u0026#34;); db.setPassword(\u0026#34;194910\u0026#34;); if(db.open()) { QMessageBox::information(this,\u0026#34;连接提示\u0026#34;,\u0026#34;连接成功\u0026#34;); } else { QMessageBox::warning(this,\u0026#34;连接提示\u0026#34;,\u0026#34;连接失败\u0026#34;); } } ","date":"2025-09-20T15:00:00+08:00","permalink":"/p/qt/","title":"QT 入门学习笔记"},{"content":"BootLoader 学习\r硬件：\nstm32f407(嘉立创天空星) HC-05蓝牙模块 软件： 暂时使用python简陋演示功能 为什么要有bootloader\r类似于电脑的boot，可以让我们很方便的去重装系统，可以用u盘重装系统，可以在线重装系统等 可以把一些固定参数放到bootloader里，比如屏幕亮度，初始参数等等，可以专注于APP的开发 前置知识\r单片机组成\r电脑组成 stm32内部，与电脑组成类似，主要强调：\n片内Flash：相当于电脑硬盘 嵌入式 SRAM：相当与电脑内存条 你需要知道：\r嵌入式SRAM是被映射在$0x2000 0000$上的 嵌入式flash是被映射在$0x0800 0000$ stm32启动流程\rSTM32微控制器从上电或复位到执行你编写的 main() 函数 之前，会经历一系列由硬件和启动代码（通常是汇编语言编写的）完成的初始化步骤\n下面是 STM32 从上电到进入 main() 函数的详细过程：\n一、硬件复位与启动模式选择\r1. 上电复位 (POR) 或硬件复位\r当STM32芯片上电或者复位引脚（NRST）被置为低电平并释放时，微控制器会进入复位状态。在复位期间，所有内部寄存器会被设置为预设的初始值，确保系统处于一个确定的、已知的状态。\n2. 选择启动模式 (BOOT Pins)\rCPU内核在复位结束后，首先会采样特定的启动引脚（如 BOOT0 和 BOOT1，具体名称和数量可能因型号而异）。这些引脚的状态决定了微控制器从哪个存储器空间开始执行代码：\n主闪存启动 (Main Flash Memory)：最常用的模式，运行用户编写的程序。 系统存储器启动 (System Memory)：运行内置的 Bootloader，用于通过串口、USB等接口烧录程序。 SRAM 启动 (Embedded SRAM)：运行驻留在SRAM中的代码，不常用。 通常，在运行用户程序时，会配置为从主闪存启动。 二、Cortex-M 内核的初始寻址\rSTM32 的核心是 Cortex-M 系列内核（如 M0、M3、M4、M7），它们遵循一个标准化的启动流程，这部分是由硬件自动完成的。\n3. 读取中断向量表\r微控制器根据启动模式，确定中断向量表的起始地址（在主闪存启动模式下，通常是 0x0800 0000）。中断向量表是存储了各种异常和中断服务程序入口地址的表格。\n4. 初始化栈指针 (SP)\rCPU 从向量表的第一个 32 位字（地址 Boot_Address + 0x00）中读取初始的 主栈指针 (MSP) 的值。这是程序中栈区的最高地址，用于存储局部变量、函数调用返回地址等。\n5. 设置程序计数器 (PC)\rCPU 从向量表的第二个 32 位字（地址 Boot_Address + 0x04）中读取 复位处理程序（Reset_Handler）的地址，并将这个地址加载到 程序计数器 (PC) 中。 至此，硬件初始化完成，程序控制权正式移交给 Reset_Handler。\n三、执行启动文件代码 (Reset_Handler)\rReset_Handler 是由编译器和芯片厂商提供的一个汇编语言函数，通常位于项目中的 startup_stm32xxx.s 文件（即启动文件）中。它的任务是为C/C++程序的运行准备好环境，包括内存、时钟和库函数等。\n6. 调用 SystemInit()\rReset_Handler 做的第一件大事是调用 SystemInit() 函数（通常在 system_stm32xxx.c 文件中）。该函数主要完成：\n配置系统时钟：设置主时钟源、PLL 倍频等，将系统时钟配置到所需的工作频率（但此时还不涉及用户外设时钟）。 配置存储器：如设置 Flash 访问等待周期等。 7. 初始化 RAM 中的数据段\r初始化 .data 段：.data 段存储了已初始化的全局变量和静态变量。这些变量的初始值存储在 Flash 中，Reset_Handler 会将这些值从 Flash 复制到 RAM 中。 初始化 .bss 段：.bss 段存储了未初始化的全局变量和静态变量。C 语言标准要求它们在程序开始时必须被清零。Reset_Handler 会将 .bss 段在 RAM 中的区域清零。 8. 调用 C 库初始化函数\r对于标准 C/C++ 环境，Reset_Handler 还会调用 C 库的初始化函数（例如 __libc_init_array），用于：\n初始化 C 运行时环境。 调用 C++ 程序的静态构造函数（如果你的代码中有使用）。 9. 跳转到 main()\r在完成所有必要的硬件和软件环境准备后，Reset_Handler 的最后一步就是执行一条跳转指令，调用用户编写的 main() 函数。 代码段\n; 假设这是 Reset_Handler 的最后几行伪汇编代码 ; ... BL SystemInit ; 调用系统时钟初始化函数 BL __libc_init_array ; 调用 C 库初始化函数 LDR R0, =main ; 将 main 函数地址加载到 R0 BX R0 ; 跳转到 main 函数执行 至此，STM32 终于进入了你的应用程序，开始执行用户代码。\n总结流程图\r上电后检查boot引脚的高低决定从哪启动（在哪寻找代码） 读取代码(编译后的2进制文件)的第一个字(主堆栈的地址)，（工厂的工作地点） 寻找第二个字以运行到main(); 步骤 执行者 动作描述 目标 1 硬件/CPU 上电复位 (POR) 强制所有硬件到初始状态。 2 硬件/CPU 检查 BOOT 引脚 确定代码的启动位置（如主 Flash）。 3 硬件/CPU 读取向量表 确定 SP 和 PC 的初始值。 4 硬件/CPU 设置初始 SP/PC 初始化主栈指针，跳转到 Reset_Handler。 5 启动文件 调用 SystemInit() 配置系统时钟和 Flash 等基础硬件。 6 启动文件 初始化数据段 从 Flash 复制 .data 到 RAM，将 .bss 段清零。 7 启动文件 调用库初始化 初始化 C 运行时环境和静态构造函数。 8 启动文件 调用 main() 进入用户应用程序的起点。 问题：\rQuestion1为什么启动后需要先找到中断向量表呢，很重要吗?\rQuestion2:堆栈是用来干嘛的，为什么要先初始化它，不初始化会怎么样\rQuestion3：为什么会映射成0x08000000呢\r简单跳转代码\r使用O3优化可以减小boot大小 fromelf \u0026ndash;bin -o \u0026ldquo;$L@L.bin\u0026rdquo; \u0026ldquo;#L Boot\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef void (*pFunction)(void); void JumpToApplication(void) { // 1. 检查应用程序地址是否有效 // 应用程序的栈顶地址位于其起始地址的第一个字 if (((*(__IO uint32_t*)APP_ADDRESS) \u0026amp; 0x2FFE0000) == 0x20000000) { // 2. 将中断向量表重定向到应用程序地址 SCB-\u0026gt;VTOR = APP_ADDRESS; // 3. 获取应用程序的栈顶地址和复位处理函数地址 pFunction app_reset_handler = (pFunction)(*(__IO uint32_t*)(APP_ADDRESS + 4)); __set_MSP(*(__IO uint32_t*)APP_ADDRESS); // 4. 跳转到应用程序 app_reset_handler(); } } APP\r随便，但是： 问题\rquestion1 SCB是什么：\r远程下载讲解\r注意事项：HC-05的VCC接3V3，实测5V无法使用 蓝牙模块只有在通信状态才会显示已连接蓝牙，打不开蓝牙串口参考：HC05和电脑蓝牙通讯 HC05-python-stm32的波特率必须都设置为同样的速率，我这里为9600\n蓝牙模块讲解\r总结：当成远程串口用就行\n常用AT指令\r波特率为38400\n指令 返回 意义 AT ok 检测固件是否可用 AT+RESET ok 模块软件复位 AT+VERSION? +VERSION:\u0026lt;Param\u0026gt; OK 获取固件版本 AT+NAME=name ok 设置模块名字 AT+NAME? +NAME:\u0026lt;Param\u0026gt; OK 返回模块名字 AT+PSWD=\u0026ldquo;passwd\u0026rdquo; OK 设置模块密码 AT+PSWD? +PSWD:\u0026lt;Param\u0026gt; OK 获取模块密码 AT+UART=115200,0,0 ok 设置通信时的波特率 AT+UART? +UART:\u0026lt;Param1\u0026gt;,\u0026lt;Param2\u0026gt;,\u0026lt;Param3\u0026gt;\nOK 获取串口参数 模块指示灯标志\r定时闪两下：已经连接蓝牙，处于通信状态 快闪：未连接 慢闪：工作模式，可使用AT指令调整参数（进入：保持按键按下，通电后松开按键即可） 测试流程\r先进入工作模式，使用AT检查是否可用， 设置模块名字：AT+NAME=MyBluetooth 获取密码：AT+PSWD? 查看波特率：AT+UART? 嵌入式Flash操作\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void Flash_Erase_App(void) { FLASH_EraseInitTypeDef EraseInit; uint32_t PageError; // 解除Flash保护 HAL_FLASH_Unlock(); // F407: sector2(扇区2) = 0x08008000 开始 EraseInit.TypeErase = FLASH_TYPEERASE_SECTORS; EraseInit.Sector = FLASH_SECTOR_2; EraseInit.NbSectors = 6; // 到 sector7(扇区7) EraseInit.VoltageRange = FLASH_VOLTAGE_RANGE_3; if (HAL_FLASHEx_Erase(\u0026amp;EraseInit, \u0026amp;PageError) != HAL_OK) { // 错误处理 } // 开启保护 HAL_FLASH_Lock(); } HAL_StatusTypeDef Flash_Write(uint32_t addr, uint8_t *data, uint32_t len) { HAL_StatusTypeDef status; HAL_FLASH_Unlock(); for(uint32_t i=0; i\u0026lt;len; i+=4) { uint32_t word = *(uint32_t*)(data+i); status = HAL_FLASH_Program(FLASH_TYPEPROGRAM_WORD, addr+i, word); if(status != HAL_OK) break; } HAL_FLASH_Lock(); return status; } 主要逻辑\r固件发送与接收\rBootloader代码逻辑\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void Bootloader_Update(void) { uint8_t cmd; uint8_t buf[256]; uint32_t addr = APP_ADDR; Flash_Erase_App(); while(1) { HAL_UART_Receive(\u0026amp;huart1, \u0026amp;cmd, 1, HAL_MAX_DELAY); if(cmd == CMD_DATA) { uint16_t len; HAL_UART_Receive(\u0026amp;huart1, (uint8_t*)\u0026amp;len, 2, HAL_MAX_DELAY); HAL_UART_Receive(\u0026amp;huart1, buf, len, HAL_MAX_DELAY); Flash_Write(addr, buf, len); addr += len; HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;OK\u0026#34;, 2, HAL_MAX_DELAY); } else if(cmd == CMD_END) { HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;DONE\u0026#34;, 4, HAL_MAX_DELAY); break; } } } 电脑_python\u0026ndash;发送\rpython要先安装pyserial跑：pip install pyserial 固件采用分批的形式发送，使用python脚本进行发送 ser = serial.Serial(\u0026quot;COM4\u0026quot;, 9600)这里的COM4需要改成你的com端口\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import serial import struct # 初始化一个串口对象 ser = serial.Serial(\u0026#34;COM4\u0026#34;, 9600) def send_data(data): ser.write(b\u0026#34;\\x02\u0026#34;) # CMD_DATA # 把当前数据块的长度打包成两个字节的无符号整数（小端序）。 # 比如 len(data) = 256，那么结果是 b\u0026#39;\\x00\\x01\u0026#39;，开发板端可以据此知道这包数据有多长。 ser.write(struct.pack(\u0026#34;\u0026lt;H\u0026#34;, len(data))) ser.write(data) # 阻塞读取开发板发送的信息，确保是否传递成功 print(ser.read(2)) # OK with open(\u0026#34;app.bin\u0026#34;, \u0026#34;rb\u0026#34;) as f: while True: # 每次循环读取256个数据 chunk = f.read(256) if not chunk: break send_data(chunk) ser.write(b\u0026#34;\\x03\u0026#34;) # CMD_END print(ser.read(4)) # DONE stm32_蓝牙\u0026ndash;接收\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #define APP_ADDR 0x08008000 #define CMD_START 0x01 #define CMD_DATA 0x02 #define CMD_END 0x03 void Bootloader_Update(void) { uint8_t cmd; uint8_t buf[256]; uint32_t addr = APP_ADDR; // 擦除老APP,准备迎接新王 Flash_Erase_App(); while(1) { // 堵塞接收，根据先接收到的命令决定怎么做 HAL_UART_Receive(\u0026amp;huart1, \u0026amp;cmd, 1, HAL_MAX_DELAY); // 数据命令，表示接收到的是数据 if(cmd == CMD_DATA) { uint16_t len; // 获取长度 HAL_UART_Receive(\u0026amp;huart1, (uint8_t*)\u0026amp;len, 2, HAL_MAX_DELAY); // 获取内容 HAL_UART_Receive(\u0026amp;huart1, buf, len, HAL_MAX_DELAY); // 写入内容 Flash_Write(addr, buf, len); // 位移地址，准备写入后续内容 addr += len; // 回应OK HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;OK\u0026#34;, 2, HAL_MAX_DELAY); } // 结束命令，表示接收完成，退出该函数 else if(cmd == CMD_END) { HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;DONE\u0026#34;, 4, HAL_MAX_DELAY); break; } } } 增加校验-CRC32\r一、CRC32 是什么\rCRC 全称 Cyclic Redundancy Check（循环冗余校验）。\nCRC32 表示它使用的是 32位的多项式运算结果。 它不是“加法求和”那种简单校验，而是用一种二进制多项式除法的思想：\n把整个数据看作一个很长的二进制数； 用一个固定的“生成多项式”去做模2除法； 最后得到一个 32 位的余数； 这个余数就是 CRC 校验码。 🧠 模2除法：相当于普通除法，但所有加减都用“异或（XOR）”。\n二、CRC32 的作用\rCRC32 不是用来“防黑客”或“加密”的，而是为了 检测传输或存储过程中的错误。 例如：\n你通过串口发送 100KB 的固件； 中途某一字节丢了、翻转了； STM32 收到的文件计算出的 CRC 不等于原始文件的 CRC； → 说明传输错误，不执行更新。 可以检测出几乎所有单比特错误、突发错误，非常可靠。 电脑端-python增加校验\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import serial import struct import binascii from tqdm import tqdm # pip install tqdm # 打开串口 ser = serial.Serial(\u0026#34;COM4\u0026#34;, 9600, timeout=2) def send_data(data): # 发送数据包 ser.write(b\u0026#34;\\x02\u0026#34;) # CMD_DATA ser.write(struct.pack(\u0026#34;\u0026lt;H\u0026#34;, len(data))) # 长度 ser.write(data) # 数据本体 resp = ser.read(2) # Bootloader 回复 OK if resp != b\u0026#34;OK\u0026#34;: raise Exception(\u0026#34;Bootloader写入失败，返回: %s\u0026#34; % resp) def send_end(crc32, total_size): # 通知 Bootloader 固件结束 + CRC ser.write(b\u0026#34;\\x03\u0026#34;) # CMD_END ser.write(struct.pack(\u0026#34;\u0026lt;I\u0026#34;, crc32)) # 4字节 CRC32 ser.write(struct.pack(\u0026#34;\u0026lt;I\u0026#34;, total_size)) # 固件总长度（可选） resp = ser.read(4) if resp != b\u0026#34;DONE\u0026#34;: raise Exception(\u0026#34;Bootloader校验失败，返回: %s\u0026#34; % resp) # ---------------- 主流程 ----------------- with open(\u0026#34;app.bin\u0026#34;, \u0026#34;rb\u0026#34;) as f: data = f.read() # 计算 CRC32（和 Bootloader 保持一致） crc32 = binascii.crc32(data) \u0026amp; 0xFFFFFFFF total_size = len(data) print(f\u0026#34;固件大小: {total_size} 字节, CRC32 = 0x{crc32:08X}\u0026#34;) # 分块发送（带进度条） chunk_size = 256 with tqdm(total=total_size, unit=\u0026#34;B\u0026#34;, unit_scale=True, desc=\u0026#34;升级中\u0026#34;) as pbar: for i in range(0, total_size, chunk_size): chunk = data[i:i+chunk_size] send_data(chunk) pbar.update(len(chunk)) # 通知结束 send_end(crc32, total_size) print(\u0026#34;固件升级完成 ✅\u0026#34;) stm32端-增加校验\r在CRC校验算法中，数据被视为一个巨大的二进制数，代表一个多项式的系数。生成多项式（CRC32_POLY）就是用来与这个数据多项式进行“模2除法”运算的除数，最终得到的余数就是CRC校验值。\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 // 多项式 (Ethernet, ZIP, etc. 通用): 0x04C11DB7 #define CRC32_POLY 0xEDB88320UL uint32_t crc32_calc(uint8_t *data, uint32_t length) { uint32_t crc = 0xFFFFFFFFUL; for (uint32_t i = 0; i \u0026lt; length; i++) { crc ^= data[i]; for (uint8_t j = 0; j \u0026lt; 8; j++) { if (crc \u0026amp; 1) { crc = (crc \u0026gt;\u0026gt; 1) ^ CRC32_POLY; } else { crc \u0026gt;\u0026gt;= 1; } } } return ~crc; } bootloader逻辑改变\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #define APP_ADDR 0x08008000 #define CMD_START 0x01 #define CMD_DATA 0x02 #define CMD_END 0x03 void Bootloader_Update(void) { uint8_t cmd; uint8_t buf[256]; uint32_t addr = APP_ADDR; Flash_Erase_App(); while(1) { HAL_UART_Receive(\u0026amp;huart1, \u0026amp;cmd, 1, HAL_MAX_DELAY); if(cmd == CMD_DATA) { uint16_t len; HAL_UART_Receive(\u0026amp;huart1, (uint8_t*)\u0026amp;len, 2, HAL_MAX_DELAY); HAL_UART_Receive(\u0026amp;huart1, buf, len, HAL_MAX_DELAY); Flash_Write(addr, buf, len); addr += len; HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;OK\u0026#34;, 2, HAL_MAX_DELAY); } else if(cmd == CMD_END) { uint32_t host_crc, host_size; HAL_UART_Receive(\u0026amp;huart1, (uint8_t*)\u0026amp;host_crc, 4, HAL_MAX_DELAY); HAL_UART_Receive(\u0026amp;huart1, (uint8_t*)\u0026amp;host_size, 4, HAL_MAX_DELAY); // 自己计算 CRC32 uint32_t calc_crc = crc32_calc((uint8_t*)APP_ADDR, host_size); if (calc_crc == host_crc) { HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;DONE\u0026#34;, 4, HAL_MAX_DELAY); } else { HAL_UART_Transmit(\u0026amp;huart1, (uint8_t*)\u0026#34;FAIL\u0026#34;, 4, HAL_MAX_DELAY); } break; } } } ","date":"2025-08-30T14:00:00+08:00","permalink":"/p/stm32-bootloader/","title":"STM32 BootLoader 学习笔记"},{"content":"1.动态创建详细过程\r动态创建\r1.首先tasks.c中\n结构体并给别名，所以初始化可以用TCB_t\ntypedef struct tskTaskControlBlock\n{\n\u0026hellip;\n}tskTCB; typedef tskTCB TCB_t;\n2.因此在xTaskCreate()实现中\nTCB_t * pxNewTCB;创建了任务控制块\n3.任务控制块的结构体中\n状态列表 事件列表 优先级 栈指针 任务名\n4.回到xTaskCreate\n判断生长方向，STM32栈区向下生长\n#if ( portSTACK_GROWTH \u0026gt; 0 )\n进入的\n#else中\n其中先定义任务堆栈大小：\nStackType_t * pxStack;//这个申请的代表首地址 pxStack = pvPortMallocStack( ( ( ( size_t ) usStackDepth ) * sizeof( StackType_t ) ) );//如128*4字节，128字×每字字节数=字节数 判断是否申请成功任务堆栈，成功则进入\nif( pxStack != NULL ) { pxNewTCB = ( TCB_t * ) pvPortMalloc( sizeof( TCB_t ) ); //任务控制块信息也需要空间，因此申请 if( pxNewTCB != NULL ) //如果申请了一块 { memset( ( void * ) pxNewTCB, 0x00, sizeof( TCB_t ) ); pxNewTCB-\u0026gt;pxStack = pxStack;//然后将首地址给到任务控制块的这个成员变量 } else { vPortFreeStack( pxStack );//否则就释放掉任务堆栈 } } else { pxNewTCB = NULL; } 再下面判断是否申请成功任务控制块\nprvInitialiseNewTask：这个用来初始化任务控制块中的成员\nprvAddNewTaskToReadyList：将任务添加到就绪列表\nif( pxNewTCB != NULL ) { prvInitialiseNewTask( pxTaskCode, pcName, ( uint32_t ) usStackDepth, pvParameters, uxPriority, pxCreatedTask, pxNewTCB, NULL ); prvAddNewTaskToReadyList( pxNewTCB ); xReturn = pdPASS;//就是成功的返回值 } else { xReturn = errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY; } 5.到prvInitialiseNewTask\n( void ) memset( pxNewTCB-\u0026gt;pxStack, ( int ) tskSTACK_FILL_BYTE, ( size_t ) ulStackDepth * sizeof( StackType_t ) );\n这句pxStack是任务堆栈的首地址，将( size_t ) ulStackDepth * sizeof( StackType_t )大小的地址赋值为( int ) tskSTACK_FILL_BYTE\n而这个值是\n#define tskSTACK_FILL_BYTE ( 0xa5U )\n意思就是整个任务堆栈赋值成a5，那就可以通过判断多少a5被改变，得到剩下的空间\n6.判断 #if ( portSTACK_GROWTH \u0026lt; 0 )\n是否向下生长？\nSTM32:是的\n//首地址的最低位，取得栈顶 pxTopOfStack = \u0026amp;( pxNewTCB-\u0026gt;pxStack[ ulStackDepth - ( uint32_t ) 1 ] ); 判断名字是否为空\n//pcName是一个入口参数 if( pcName != NULL ) { for( x = ( UBaseType_t ) 0; x \u0026lt; ( UBaseType_t ) configMAX_TASK_NAME_LEN; x++ ) { pxNewTCB-\u0026gt;pcTaskName[ x ] = pcName[ x ]; //给到任务控制块的这个成员里 if( pcName[ x ] == ( char ) 0x00 ) { break; } else { mtCOVERAGE_TEST_MARKER(); } } pxNewTCB-\u0026gt;pcTaskName[ configMAX_TASK_NAME_LEN - 1 ] = \u0026#39;\\0\u0026#39;; } else { mtCOVERAGE_TEST_MARKER(); } 判断优先级\n优先级大于最大优先级，就改为最大优先级\nif( uxPriority \u0026gt;= ( UBaseType_t ) configMAX_PRIORITIES ) { uxPriority = ( UBaseType_t ) configMAX_PRIORITIES - ( UBaseType_t ) 1U; } pxNewTCB-\u0026gt;uxPriority = uxPriority; 然后初始化状态列表项和事件列表项\nvListInitialiseItem( \u0026amp;( pxNewTCB-\u0026gt;xStateListItem ) ); vListInitialiseItem( \u0026amp;( pxNewTCB-\u0026gt;xEventListItem ) );\n将状态列表项所有权给到任务控制块，就可以得到任务当前状态\nlistSET_LIST_ITEM_OWNER( \u0026amp;( pxNewTCB-\u0026gt;xStateListItem ), pxNewTCB );\n下面是事件列表项\nlistSET_LIST_ITEM_VALUE( \u0026amp;( pxNewTCB-\u0026gt;xEventListItem ), ( TickType_t ) configMAX_PRIORITIES - ( TickType_t ) uxPriority ); listSET_LIST_ITEM_OWNER( \u0026amp;( pxNewTCB-\u0026gt;xEventListItem ), pxNewTCB ); 下面来到\npxNewTCB-\u0026gt;pxTopOfStack = pxPortInitialiseStack( pxTopOfStack, pxTaskCode, pvParameters );\n7.进入pxPortInitialiseStack\n里面栈顶陆续减，就可以往下陆续给赋值。\nStackType_t * pxPortInitialiseStack( StackType_t * pxTopOfStack, TaskFunction_t pxCode, void * pvParameters ) { pxTopOfStack--; *pxTopOfStack = portINITIAL_XPSR; /* xPSR */ pxTopOfStack--; *pxTopOfStack = ( ( StackType_t ) pxCode ) \u0026amp; portSTART_ADDRESS_MASK; /* PC */ pxTopOfStack--; *pxTopOfStack = ( StackType_t ) prvTaskExitError; /* LR */ pxTopOfStack -= 5; /* R12, R3, R2 and R1. */ *pxTopOfStack = ( StackType_t ) pvParameters; /* R0 */ pxTopOfStack -= 8; /* R11, R10, R9, R8, R7, R6, R5 and R4. */ return pxTopOfStack; } PC是任务函数指针，切换到A任务时，就通过这个指针切换到任务函数A\n这个实际就是最初传入的函数指针\n而R0传入的是任务函数参数\n程序结束后返回更新后的pxTopOfStack\n8.回到prvInitialiseNewTask\n将任务句柄给到任务控制块\nif( pxCreatedTask != NULL ) { *pxCreatedTask = ( TaskHandle_t ) pxNewTCB; }\n9.再到prvAddNewTaskToReadyList()\n这是将任务添加到就绪列表\n先进入临界区 taskENTER_CRITICAL();\n增加任务数量uxCurrentNumberOfTasks++;\n如果当前没有任务，这是第一个任务pxCurrentTCB = pxNewTCB;\n如果这确实是第一个if( uxCurrentNumberOfTasks == ( UBaseType_t ) 1 )\n就初始化任务列表prvInitialiseTaskLists()\n10.进入prvInitialiseTaskLists()\n初始化32个就绪列表0~31\nfor( uxPriority = ( UBaseType_t ) 0U; uxPriority \u0026lt; ( UBaseType_t ) configMAX_PRIORITIES; uxPriority++ ) { vListInitialise( \u0026amp;( pxReadyTasksLists[ uxPriority ] ) ); } 如果使能删除列表，就初始化一个等待删除列表\n如果使能挂起列表，就初始化一个挂起列表\n#if ( INCLUDE_vTaskDelete == 1 ) { vListInitialise( \u0026amp;xTasksWaitingTermination ); } #endif /* INCLUDE_vTaskDelete */ #if ( INCLUDE_vTaskSuspend == 1 ) { vListInitialise( \u0026amp;xSuspendedTaskList ); } #endif /* INCLUDE_vTaskSuspend */ 11.继续prvAddNewTaskToReadyList()\n否则的话，如果当前任务控制块优先级小于新创建的任务控制块\n那新创建的就赋给当前\nif( xSchedulerRunning == pdFALSE ) { if( pxCurrentTCB-\u0026gt;uxPriority \u0026lt;= pxNewTCB-\u0026gt;uxPriority ) { pxCurrentTCB = pxNewTCB; } else { mtCOVERAGE_TEST_MARKER(); } } 下面是任务标号，就可以通过这个查询这是第几个创建的任务\n12.进入prvAddTaskToReadyList( pxNewTCB );\n进入 taskRECORD_READY_PRIORITY( ( pxTCB )-\u0026gt;uxPriority );\n进如portRECORD_READY_PRIORITY\n得到\n#define portRECORD_READY_PRIORITY( uxPriority, uxReadyPriorities ) ( uxReadyPriorities ) |= ( 1UL \u0026laquo; ( uxPriority ) )\n表示就绪列表，哪个位置一，哪个位就有任务，从上往下读就知道哪个任务优先级最高\n13.继续prvAddNewTaskToReadyList()\n退出临界区taskEXIT_CRITICAL();\n如果任务调度器开启，当前任务优先级，小于新的优先级，执行任务切换，到优先级最高的\n2.任务挂起与恢复详细过程\r任务挂起\r1.进入prvGetTCBFromHandle\n如果句柄为NULL，返回当前任务控制块句柄，否则返回传入任务控制块句柄\n#define prvGetTCBFromHandle( pxHandle ) ( ( ( pxHandle ) == NULL ) ? pxCurrentTCB : ( pxHandle ) ) 2.回到vTaskSuspend\nif内是不管当前状态是什么，并移除列表项，移除成功则进入\nif( uxListRemove( \u0026amp;( pxTCB-\u0026gt;xStateListItem ) ) == ( UBaseType_t ) 0 ) { taskRESET_READY_PRIORITY( pxTCB-\u0026gt;uxPriority ); } 3.进入taskRESET_READY_PRIORITY\nif(listCURRENT_LIST_LENGTH(\u0026amp;(pxReadyTasksLists[( uxPriority ) ] ) ) == ( UBaseType_t ) 0 ) { portRESET_READY_PRIORITY( ( uxPriority ), ( uxTopReadyPriority ) ); } 4.回到vTaskSuspend\n移除事件列表项\nif( listLIST_ITEM_CONTAINER( \u0026amp;( pxTCB-\u0026gt;xEventListItem ) ) != NULL ) { ( void ) uxListRemove( \u0026amp;( pxTCB-\u0026gt;xEventListItem ) ); } 插入挂起列表项末尾\nvListInsertEnd( \u0026amp;xSuspendedTaskList, \u0026amp;( pxTCB-\u0026gt;xStateListItem ) ); 如果带挂起的任务控制块在等待消息通知，清除消息通知\nfor( x = 0; x \u0026lt; configTASK_NOTIFICATION_ARRAY_ENTRIES; x++ ) { if( pxTCB-\u0026gt;ucNotifyState[ x ] == taskWAITING_NOTIFICATION ) { pxTCB-\u0026gt;ucNotifyState[ x ] = taskNOT_WAITING_NOTIFICATION; } } 判断调度器是否正在运行\n如果正在运行，更新下一个阻塞超时时间\nif( xSchedulerRunning != pdFALSE ) { taskENTER_CRITICAL(); { prvResetNextTaskUnblockTime(); } taskEXIT_CRITICAL(); } 判断挂起是否为任务自己，如果挂起的是自己，强制进行任务切换。\n如果不是，判断当前挂起的任务总数等于创建的任务总数，表明全部在挂起，则将任务控制块赋值为NULL，\nif( pxTCB == pxCurrentTCB ) { if( xSchedulerRunning != pdFALSE ) { configASSERT( uxSchedulerSuspended == 0 ); portYIELD_WITHIN_API(); } else { if( listCURRENT_LIST_LENGTH( \u0026amp;xSuspendedTaskList ) == uxCurrentNumberOfTasks ) { pxCurrentTCB = NULL; } else { vTaskSwitchContext();\t//查找下一个最高优先级任务 } } } 任务恢复\r任务句柄赋值给任务控制块\nTCB_t * const pxTCB = xTaskToResume;\n恢复的任务控制块不能等于当前任务和NULL\nif( ( pxTCB != pxCurrentTCB ) \u0026amp;\u0026amp; ( pxTCB != NULL ) )\n如果成立进入，判断任务控制块是否在挂起列表中\nif( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE )\n如果成立进入，在挂起列表中移除\n( void ) uxListRemove( \u0026amp;( pxTCB-\u0026gt;xStateListItem ) );\n并添加到就绪列表\nprvAddTaskToReadyList( pxTCB );\n在判断优先级是否大于当前优先级，如果是，则任务切换\ntaskYIELD_IF_USING_PREEMPTION();\nif( ( pxTCB != pxCurrentTCB ) \u0026amp;\u0026amp; ( pxTCB != NULL ) ) { taskENTER_CRITICAL(); { if( prvTaskIsTaskSuspended( pxTCB ) != pdFALSE ) { traceTASK_RESUME( pxTCB ); ( void ) uxListRemove( \u0026amp;( pxTCB-\u0026gt;xStateListItem ) ); prvAddTaskToReadyList( pxTCB ); if( pxTCB-\u0026gt;uxPriority \u0026gt;= pxCurrentTCB-\u0026gt;uxPriority ) { taskYIELD_IF_USING_PREEMPTION(); } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } taskEXIT_CRITICAL(); } else { mtCOVERAGE_TEST_MARKER(); } 3.临界段代码保护\r通过++，\u0026ndash;实现了嵌套，本质还是基于portDISABLE_INTERRUPTS();和portENABLE_INTERRUPTS();\n/*进入临界段*/ #define taskENTER_CRITICAL() portENTER_CRITICAL() #define portENTER_CRITICAL() vPortEnterCritical() void vPortEnterCritical( void ) { portDISABLE_INTERRUPTS(); uxCriticalNesting++; if( uxCriticalNesting == 1 ) { configASSERT( ( portNVIC_INT_CTRL_REG \u0026amp; portVECTACTIVE_MASK ) == 0 ); } } /*退出临界段*/ void vPortExitCritical( void ) { configASSERT( uxCriticalNesting ); uxCriticalNesting--; if( uxCriticalNesting == 0 ) { portENABLE_INTERRUPTS(); } } 4.任务调度\r","date":"2025-08-09T10:00:00+08:00","permalink":"/p/freertos-source/","title":"FreeRTOS 源码解析笔记"},{"content":"任务内核\rportDISABLE_INTERRUPTS() portENABLE_INTERRUPTS()\r关闭FreeRTOS可管理的5-15优先级的中断\n开启FreeRTOS可管理的5-15优先级的中断\ntaskENTER_CRITICAL() taskEXIT_CRITICAL()\r任务中进入临界段\n任务中退出临界段\ntaskENTER_CRITICAL() ; { … … /* 临界区 */ } taskEXIT_CRITICAL() ; taskENTER_CRITICAL_FROM_ISR() taskEXIT_CRITICAL_FROM_ISR()\r中断中进入临界段\n中断中退出临界段\nuint32_t save_status; save_status = taskENTER_CRITICAL_FROM_ISR() ； { … … /* 临界区 */ } taskEXIT_CRITICAL_FROM_ISR(save_status ); vTaskSuspendAll() xTaskResumeAll()\r实现任务调度器挂起与恢复\nvTaskSuspendAll() ; { … … /* 内容 */ } xTaskResumeAll(); void vTaskStartScheduler()\rvTaskStartScheduler();\n任务创建与删除\r1.xTaskCreate\r参数和返回值\rBaseType_t xTaskCreate ( TaskFunction_t pxTaskCode, /* 指向任务函数的指针 */ const char * const pcName, /* 任务名字，最大长度 configMAX_TASK_NAME_LEN */ const configSTACK_DEPTH_TYPE usStackDepth, /* 任务堆栈大小，注意字为单位 */ void * const pvParameters, /* 传递给任务函数的参数 */ UBaseType_t uxPriority, /* 任务优先级，范围： 0 ~ configMAX_PRIORITIES - 1 */ TaskHandle_t * const pxCreatedTask /* 任务句柄，就是任务的任务控制块 */ ) 返回: pdPASS 任务创建成功 errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY 任务创建失败 实现例程\r#define START_TASK_STACK_SIZE 128\t//栈大小 #define START_TASK_PRIO 1\t//优先级 TaskHandle_t start_task_handle;\t//句柄 void startTask(void* pvParamenters); #define TASK1_STACK_SIZE 128 #define TASK1_PRIO 2 TaskHandle_t task1_handle; void task1(void* pvParamenters); void freertos_demo(void) { xTaskCreate( (TaskFunction_t\t) startTask, (char *\t) \u0026#34;startTask\u0026#34;, (configSTACK_DEPTH_TYPE) START_TASK_STACK_SIZE, (void * ) NULL, (UBaseType_t ) START_TASK_PRIO, (TaskHandle_t * ) \u0026amp;start_task_handle ); vTaskStartScheduler(); } void startTask(void* pvParamenters) { taskENTER_CRITICAL(); xTaskCreate( (TaskFunction_t ) task1,\t//任务函数 (char *\t) \u0026#34;task1\u0026#34;, //任务名 (configSTACK_DEPTH_TYPE) TASK1_STACK_SIZE,\t//任务堆栈 (void * ) NULL,\t//任务函数参数 (UBaseType_t ) TASK1_PRIO,\t//优先级 (TaskHandle_t * ) \u0026amp;task1_handle );\t//任务句柄（就是指向任务控制块头的指针） vTaskDelete(start_task_handle); taskEXIT_CRITICAL(); } void task1(void* pvParamenters){ while(1){ } } 2.xTaskCreateStatic\r参数和返回值\rTaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, /* 指向任务函数的指针 */ const char * const pcName, /* 任务函数名 */ const uint32_t ulStackDepth, /* 任务堆栈大小注意字为单位 */ void * const pvParameters, /* 传递的任务函数参数 */ UBaseType_t uxPriority, /* 任务优先级 */ StackType_t * const puxStackBuffer, /* 任务堆栈，一般为数组，由用户分配 */ StaticTask_t * const pxTaskBuffer /* 任务控制块指针，由用户分配 */ ); 返回: NULL 用户没有提供相应的内存，任务创建失败 其他值 任务句柄，任务创建成功 实现例程\r#define START_TASK_STACK_SIZE 128 #define START_TASK_PRIO 1 TaskHandle_t start_task_handle; StackType_t\tstart_task_stack[START_TASK_STACK_SIZE]; StaticTask_t\tstart_task_buffer; void startTask(void* pvParamenters); #define TASK1_STACK_SIZE 128 #define TASK1_PRIO 2 TaskHandle_t task1_handle; StackType_t\ttask1_stack[TASK1_STACK_SIZE]; StaticTask_t\ttask1_buffer; void task1(void* pvParamenters); /*空闲任务配置*/ StaticTask_t idle_task_tcb; StackType_t idle_task_stack[configMINIMAL_STACK_SIZE]; /*软件定时器任务配置*/ StaticTask_t timer_task_tcb; StackType_t timer_task_stack[configTIMER_TASK_STACK_DEPTH]; /*空闲任务内存分配*/ void vApplicationGetIdleTaskMemory( StaticTask_t ** ppxIdleTaskTCBBuffer, StackType_t ** ppxIdleTaskStackBuffer, uint32_t * pulIdleTaskStackSize ) { *ppxIdleTaskTCBBuffer = \u0026amp;idle_task_tcb; *ppxIdleTaskStackBuffer = idle_task_stack; *pulIdleTaskStackSize = configMINIMAL_STACK_SIZE; } /*软件定时器内存分配*/ void vApplicationGetTimerTaskMemory( StaticTask_t ** ppxTimerTaskTCBBuffer, StackType_t ** ppxTimerTaskStackBuffer, uint32_t * pulTimerTaskStackSize ) { *ppxTimerTaskTCBBuffer = \u0026amp;timer_task_tcb; *ppxTimerTaskStackBuffer = timer_task_stack; *pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH; } void freertos_demo(void) { start_task_handle = xTaskCreateStatic( (TaskFunction_t) startTask, (char* ) \u0026#34;startTask\u0026#34;, (uint32_t\t) START_TASK_STACK_SIZE, (void*\t) NULL, (UBaseType_t\t) START_TASK_PRIO, (StackType_t* ) start_task_stack, (StaticTask_t* ) \u0026amp;start_task_buffer ); vTaskStartScheduler(); } void startTask(void* pvParamenters) { taskENTER_CRITICAL(); task1_handle = xTaskCreateStatic( (TaskFunction_t) task1, (char* ) \u0026#34;task1\u0026#34;, (uint32_t\t) TASK1_STACK_SIZE, (void*\t) NULL, (UBaseType_t\t) TASK1_PRIO, (StackType_t* ) task1_stack, (StaticTask_t* ) \u0026amp;task1_buffer ); vTaskDelete(start_task_handle); taskEXIT_CRITICAL(); } void task1(void* pvParamenters) { while(1) { } } 3.vTaskDelete\rvoid vTaskDelete(TaskHandle_t xTaskToDelete);//xTaskToDelete 待删除任务的任务句柄 任务挂起与恢复\r1.vTaskSuspend\rvoid vTaskSuspend( TaskHandle_t xTaskToSuspend ); vTaskSuspend(任务句柄); vTaskSuspend(NULL);//挂起本任务 2.vTaskResume\rvoid vTaskResume( TaskHandle_t xTaskToResume ); vTaskResume(任务句柄); 3.xTaskResumeFromISR\rBaseType_t xTaskResumeFromISR( TaskHandle_t xTaskToResume ); void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { BaseType_t xYieldRequired; switch(GPIO_Pin) { case GPIO_PIN_0: delay_ms(20); /* 消抖 */ flag2_suspend++; if(flag2_suspend%2==1) vTaskSuspend(task2_handle); else xYieldRequired = xTaskResumeFromISR(task2_handle); portYIELD_FROM_ISR( xYieldRequired );\t//任务恢复后需要进行任务切换 break; } } 列表与列表项\r1.vListInitialise\r初始化列表\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 /*pxList 是待初始化列表*/ void vListInitialise( List_t * const pxList ) { //初始化时，列表只有xListEnd，pxIndex指向xListEnd pxList-\u0026gt;pxIndex = ( ListItem_t * ) \u0026amp;( pxList-\u0026gt;xListEnd ); //末尾列表项默认最大值，升序排序在最后 pxList-\u0026gt;xListEnd.xItemValue = portMAX_DELAY; //初始化时，列表中只有xListEnd，因此上下都是本身 pxList-\u0026gt;xListEnd.pxNext = ( ListItem_t * ) \u0026amp;( pxList-\u0026gt;xListEnd ); pxList-\u0026gt;xListEnd.pxPrevious = ( ListItem_t * ) \u0026amp;( pxList-\u0026gt;xListEnd ); //初始化时列表项数量为0（不包含xListEnd） pxList-\u0026gt;uxNumberOfItems = ( UBaseType_t ) 0U; //初始化校验 listSET_LIST_INTEGRITY_CHECK_1_VALUE( pxList ); listSET_LIST_INTEGRITY_CHECK_2_VALUE( pxList ); } 2.vListInitialiseItem\r初始化列表项\n1 2 3 4 5 6 7 8 9 /*pxItem是待初始化列表项*/ void vListInitialiseItem( ListItem_t * const pxItem ) { //初始化时，列表项所在列表为空 pxItem-\u0026gt;pxContainer = NULL; listSET_FIRST_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem ); listSET_SECOND_LIST_ITEM_INTEGRITY_CHECK_VALUE( pxItem ); } 3.vListInsertEnd\r列表末尾插入列表项\n1 2 //目标列表，目标列表项 void vListInsertEnd(List_t * const pxList,ListItem_t * const pxNewListItem); 4.vListInsert\r列表插入列表项\n1 2 //目标列表，目标列表项 void vListInsert(List_t * const pxList,ListItem_t * const pxNewListItem); 5.uxListRemove\r列表移除列表项\n1 2 3 //pxItemToRemove 待移除的列表项 UBaseType_t uxListRemove ( ListItem_t * const pxItemToRemove ); //返回值，移除后所剩列表项 插入删除实验\r1 2 3 4 5 6 7 8 9 10 /*创建列表*/ List_t TestList; ListItem_t ListItem1; /*初始化列表，列表项*/ vListInitialise(\u0026amp;TestList); vListInitialiseItem(\u0026amp;ListItem1); ListItem1.xItemValue = 40; /*插入列表项*/ vListInsert((List_t* )\u0026amp;TestList, /* 列表 */ (ListItem_t*)\u0026amp;ListItem1); /* 列表项 */ 任务相关API\r1.uxTaskPriorityGet\r获取任务优先级\n打开宏INCLUDE_uxTaskPriorityGet\n//xTask要查找的任务句柄， NULL 代表任务自身 UBaseType_t uxTaskPriorityGet( const TaskHandle_t xTask ) //返回无符号整数，为任务优先级 2.vTaskPrioritySet\r改变任务优先级\n宏 INCLUDE_vTaskPrioritySet 为 1\n//xTask任务句柄 uxNewPriority优先级 void vTaskPrioritySet( TaskHandle_t xTask , UBaseType_t uxNewPriority ) 3.uxTaskGetNumberOfTasks\r此函数用于获取系统中任务的任务数量\nUBaseType_t uxTaskGetNumberOfTasks( void )\n4.uxTaskGetSystemState\r获取系统中所有任务的任务状态信息\n消息队列API\r例子\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 QueueHandle_t key_quque; /*小数据句柄*/ QueueHandle_t big_date_queue;/*大数据句柄*/ char buff[100] = {\u0026#34;This is a big data queue\u0026#34;}; key_quque = xQueueCreate(2,sizeof(uint8_t)); if(key_quque != NULL) { printf(\u0026#34;key_queue队列创建成功\\r\\n\u0026#34;); }else printf(\u0026#34;key_queue队列创建失败\\r\\n\u0026#34;); big_date_queue = xQueueCreate(1,sizeof(char *)); if(key_quque != NULL) { printf(\u0026#34;big_date_queue队列创建成功\\r\\n\u0026#34;); }else printf(\u0026#34;big_date_queue队列创建失败\\r\\n\u0026#34;); /*入队，发送*/ void task1() { key_val = key_scan(0); if(key_val == KEY0_PRES || key_val == KEY1_PRES) { err = xQueueSend(key_quque,\u0026amp;key_val,portMAX_DELAY); if(err!=pdTRUE) { printf(\u0026#34;key_queue队列发送失败\\r\\n\u0026#34;); } } } /*出队，读取*/ void task2() { if(xQueueReceive(big_date_queue,\u0026amp;buf,portMAX_DELAY)!=pdTRUE) { printf(\u0026#34;big_date_queue队列接收失败\\r\\n\u0026#34;); } else { printf(\u0026#34;big_date_queue读取队列成功,数据%s\\r\\n\u0026#34;,buf); } } ","date":"2025-07-26T10:30:00+08:00","permalink":"/p/freertos-api/","title":"FreeRTOS 常用 API 手册"},{"content":"序、FreeRTOS函数名前缀\r一、单片机程序设计模式\r1.轮询模式\n2.前后台\n后台：执行在main\n前台：执行在中断，但不能执行太多\n可以在中断中用标志位，将任务放在主函数\n3.定时器驱动\n函数执行不能太长\n4.状态机\n基于轮询，将单个任务分解成多个任务，减少每个任务需要的时间，降低造成的相互影响\n5.多任务系统\n交替执行，基于时间片和任务优先级调度\n对于重要的任务可以提高优先级，在高优先级处于延时中执行低优先级任务\n伪代码实现\nvoid main() { // 创建 2 个任务 create_task(task1); create_task(task2); // 启动调度器 start_scheduler(); } task1() { while(1) { do_task1(); } } task2() { while(1) { do_task2(); } } 二、FreeRTOS基础知识\r1.任务调度简介(熟悉)\r抢占式：每个任务有优先级，优先级高抢占优先级低\rFreeRTOS数值越大，优先级越高\nTask1 、 Task2 、 Task3 的优先级分别为 1 、 2 、 3 ；在FreeRTOS 中任务设置的数值越大，优先级越高，所以 TASK3 的优先级最高\n时间片：同优先级，每个时钟节拍切换任务\r同等优先级任务轮流地享有相同的 CPU 时间 ( 可设置 ) ， 叫时间片，在 FreeRTOS 中，一个时间片就等于 SysTick 中断周期\n没用完的时间片抛弃\n协程式：当前执行任务一直执行，高优先级不抢占低优先级（不常用）\r2.任务状态(熟悉)\r1.运行态\n正在执行，同一时间仅一个\n2.就绪态\n能够被执行，但还未执行\n3.阻塞态\n任务在延时或等待外部事件发生\n4.挂起态\n类似暂停，不参与调度，需要解挂才能进就绪vTaskResume()；\n注：只有就绪态可以转运行态，挂起要解挂，阻塞要变成就绪\n任务状态列表\r1.就绪列表：pxReadyTasksLists[x]，其中 x 代表任务优先级数值。0~31，31最高\n32位变量，当某个位置一说明对应优先级有任务存在\n2.阻塞列表：pxDelayedTaskList\n3.挂起列表：xSuspendedTaskList\n调度器取就绪列表最高优先级任务来执行\n相同优先级的任务会连接在同一个就绪列表\n三、FreeRTOS移植\r源码：https://www.freertos.org/\nFreeRTOS：内核有用\nDemo和Source\nSource文件夹\rinclude,portable,list,queue,task必须\nportable文件夹\r与硬件连接桥梁\n由于使用MDK开发，只要\n手把手移植\r1.基础工程的Middlewares文件夹中新建一个FreeRTOS子文件夹\n将FreeRTOS内核源码的Source文件夹下的所有文件添加到工程的FreeRTOS文件夹中\nCORE中是源码\nheap_4是内存管理算法4\nport是软硬件连接桥梁\nport根据芯片选择\n头文件哪有就加什么路径\n2.添加 FreeRTOSConfig.h 文件\n这个可裁剪，用不到的功能可以裁剪，Demo里有，或者例程\n3.修改SYSTEM\n详阅《FreeRTOS开发指南》\n系统配置详解\rFreeRTOSConfig.h 配置文件作用：对 FreeRTOS 的功能进行配置和裁剪，以及 API 函数的使能\n详看注释\n四、FreeRTOS任务创建和删除\r1.任务创建和删除的 API 函数\rAPI 描述 xTaskCreate() 动态创建 xTaskCreateStatic() 静态创建 vTaskDelete() 删除 动态创建：任务控制块以及任务栈空间所需内存，有FreeRTOS自动分配\n静态创建：用户分配\n动态创建\r返回pdPass：创建成功\n返回errCOULD_NOT_ALLOCATE_REQUIRED_MEMORY：创建失败\n流程：\r1、将宏 configSUPPORT_DYNAMIC_ALLOCATION 配置为 1\n2 、定义函数入口参数\n3 、编写任务函数\n创建后进入就绪态，然后启动任务调度器运行\n内部实现：\r任务控制块就是任务的身份证，保留任务的信息\n静态创建\r返回NULL：创建成功\n其他值：任务句柄，任务创建成功\n流程：\r1 、需将宏 configSUPPORT_STATIC_ALLOCATION 配置为 1\n2 、定义空闲任务 \u0026amp; 定时器任务的任务堆栈及TCB\n3 、实现两个接口函数\nvApplicationGetIdleTaskMemory( )必须\nvApplicationGetTimerTaskMemory ()可选\n4 、定义函数入口参数\n5 、编写任务函数\n任务删除\rxTaskDelete(TaskHandle_t xTaskToDelete);\n实际是通过传入的任务句柄获取任务控制块，从四个任务列表中移除\n注意：1.当传入NULL，删除任务自身\n​\t2.空闲任务会释放被删除任务中有系统分配的内存，但是用户在任务\t删除前申请的内存，需要手动释放，否则内存泄漏\n五、FreeRTOS任务挂起与恢复\r1.任务挂起与恢复的API函数\r挂起：暂停\n恢复：继续\nFromISR：从中断中恢复\n任务挂起\r将宏 INCLUDE_vTaskSuspend 配置为 1\n任务恢复\r将宏 INCLUDE_vTaskSuspend 必须定义为 1\n中断中恢复\rINCLUDE_vTaskSuspend 和 INCLUDE_xTaskResumeFromISR 必须定义为 1\n注意：中断服务程序中要调用FreeRTOS的API，中断优先级不能高于FreeRTOS所管理的优先级\n因为FreeRTOS管理的515的优先级，所有中断优先级不能在04，因为中断优先级数字越低优先级越高\n六、FreeRTOS中断管理\r1.中断\r中断请求，响应中断（转中断处理ISR），退出中断\n2.中断优先级\r8位宽寄存器来配置中断优先级\nARM Cortex-M支持2^8=256个中断优先级\nSTM32只用高四位[7:4]，所以STM32提供2^4=16个中断优先级\nSTM32中断分为抢占优先级和子优先级\n抢占优先级： 抢占优先级高的中断可以打断正在执行但抢占优先级低的中断\n子优先级：当同时发生具有相同抢占优先级的两个中断时，子优先级数值小的优先执行\n（中断优先级数字越小越优先）\n分配方式\r如：HAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4 ）设置优先级分组4\n注意\r1 、低于 configMAX_SYSCALL_INTERRUPT_PRIORITY 优先级的中断里才允许调FreeRTOS 的 API 函数\n2 、建议将所有优先级位指定为抢占优先级位，方便 FreeRTOS 管理\nHAL_NVIC_SetPriorityGrouping(NVIC_PRIORITYGROUP_4 ）\n3 、中断优先级数值越小越优先，任务优先级数值越大越优先\n七、临界段代码保护 任务调度器挂起和恢复\r1.临界段代码保护\r临界段：指那些必须完整运行，不能被（中断、任务调度PendSV）打断的代码段\n需要保护：\n1.严格时序初始化的外设\n2.系统自身需求\n3.用户需求\n2.临界段代码保护函数\r临界区屏蔽中断，系统任务调度和ISR都靠中断，因此临界区不能长\n3.任务调度器挂起和恢复\r挂起任务调度器，就是不许任务切换，不需要关闭中断\n1.挂起任务调度器未关闭中断\n2.防止任务间资源争夺，中断照样响应\n八、FreeRTOS列表和列表项\r1.列表和列表项\r列表是FreeRTOS的一个数据结构，类似链表，用于跟踪FreeRTOS的任务\n列表项是在列表中的项目\nFreeRTOS中列表是双向环形链表\n非连续地址，人为连接到一起，数目后期添加，随时可以改变\n列表相关结构体\r1.校验值：用于调试，默认不开启\n2.uxNumberOfltems，记录列表项的个数\n3.pxIndex：指向列表中的某个列表项，用于遍历列表项\n4.xListEnd是迷你列表项，排在末尾\n列表项结构体\r1.xItemValue为列表项的值，对列表项进行排序（升序）\n2.pxNext 和 pxPrevious 分别用于指向列表中列表项的下一个列表项和上一个列表项\n3.pxOwner指向包含列表项的对象（通常是任务控制块）\n3.pxContainer指向列表项所在列表\n迷你列表项结构体\r用于标记列表的末尾和挂载其他插入列表中的列表项\n少了pxOwner和pxContainer\n列表和列表项的关系\r列表初始化就有末尾列表项，而无列表项\n初始末尾列表项的xItemValue=0xFFFFFFFF\n*pxPrevious，*pxNext都是指向的列表项的头指针（首地址）\npxIndex不遍历时不指向\n2.列表及列表项API\r九、任务调度机制*\r1.开启任务调度器\rvTaskStartScheduler()中\n启动任务调度器\n1.创建空闲任务\r先判断创建方式：静态创建、动态创建；根据宏configSUPPORT_STATIC_ALLOCATION选择\n2.定时器任务\r如果使能软件定时器，创建定时器任务\n根据宏configUSE_TIMERS；xTimerCreateTimerTask()\n若创建成功\n3.关闭中断\r防止调度器开启之前或过程中，受中断干扰，会在运行第一个任务时打开中断\nportDISABLE_INTERRUPTS();\n4.初始化全局变量\r并将任务调度器设置为已运行\n//阻塞时间默认最大值 xNextTaskUnblockTime = portMAX_DELAY; //调度器运行状态 xSchedulerRunning = pdTRUE; //初始节拍 0 xTickCount = ( TickType_t ) configINITIAL_TICK_COUNT; 5.调用xPortStartScheduler();\r作用：该函数用于完成启动任务调度器中与硬件架构相关的配置部分，以及启动第一个任务\n5.1检测配置是否有误\r用户在 FreeRTOSConfig.h 文件中对中断的相关配置\n5.2配置 PendSV 和 SysTick\r配置 PendSV 和 SysTick 的中断优先级为最低优先级\nportNVIC_SHPR3_REG |= portNVIC_PENDSV_PRI; portNVIC_SHPR3_REG |= portNVIC_SYSTICK_PRI; 5.3调用vPortSetupTimerInterrupt()；\r函数中在配置SysTick\nSysTick控制及状态寄存器（地址：0xE000_E010）\nSysTick当前数值寄存器（地址：0xE000_E018）\n1 2 3 4 5 6 7 /* 配置寄存器 #define portNVIC_SYSTICK_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000e010 ) ) #define portNVIC_SYSTICK_CURRENT_VALUE_REG ( *( ( volatile uint32_t * ) 0xe000e018 ) ) */ portNVIC_SYSTICK_CTRL_REG = 0UL; portNVIC_SYSTICK_CURRENT_VALUE_REG = 0UL; 1 2 3 4 5 6 7 8 9 10 11 12 13 /* #define portNVIC_SYSTICK_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000e010 ) ) #define portNVIC_SYSTICK_LOAD_REG ( *( ( volatile uint32_t * ) 0xe000e014 ) ) */ /* F103C8中 uint32_t SystemCoreClock = 16000000; #define configCPU_CLOCK_HZ SystemCoreClock //定义CPU主频, 单位: Hz, 无默认需定义 #define configSYSTICK_CLOCK_HZ (configCPU_CLOCK_HZ / 8) #define configTICK_RATE_HZ 20\t/* 定义系统时钟节拍频率, 单位: Hz, 无默认需定义 */ */ portNVIC_SYSTICK_LOAD_REG = ( configSYSTICK_CLOCK_HZ / configTICK_RATE_HZ ) - 1UL; portNVIC_SYSTICK_CTRL_REG = ( portNVIC_SYSTICK_CLK_BIT_CONFIG | portNVIC_SYSTICK_INT_BIT | portNVIC_SYSTICK_ENABLE_BIT ); 5.4初始化临界区嵌套计数器为 0\nuxCriticalNesting = 0;\n5.5调用函数 prvEnableVFP() 使能 FPU\nCortex M3没有FPU\n5.6调用函数 prvStartFirstTask() 启动第一个任务\n2.启动第一个任务\r要启动的第一个任务是任务 A，那么就需要将任务A的寄存器值恢复到CPU寄存器\n任务 A 的寄存器值，在一开始创建任务时就保存在任务堆栈里\n注意：\n1、中断产生时，硬件自动将 xPSR，PC(R15) ，LR(R14) ，R12 ，R3-R0出/入栈；而 R4~R11需要手动出/入栈\n2 、进入中断后硬件会强制使用 MSP 指针 ，此时 LR(R14 ）的值将会被自动被更新为特殊的 EXC_RETURN\nprvStartFirstTask()\n\u0026hellip;.\n/* Call SVC to start the first task. */ svc 0\n触发SVC中断，启动第一个任务\n#define vPortSVCHandler SVC_Handler\nSVC中断\r只在启动第一次任务时会调用一次，以后不调用\n出栈/压栈汇编指令详解\r3.任务切换\r任务切换本质：CPU寄存器的切换\n就是把不同任务加载到CPU寄存器里\n假设要把任务A切换到B：\n第一步：暂停任务A，将此时任务A的寄存器保存（压栈）到任务堆栈，即保存现场\n第二步：将任务B的寄存器值（存在任务堆栈中）恢复到CPU寄存器，即恢复现场\n切换过程：保存A现场，恢复B现场，即上下文切换\n需要手动的部分：R4-R11\n切换：任务切换在PendSV中断服务函数里完成\n如何触发PendSV中断\r1.滴答定时器中断调用\n2.FreeRTOS相关API：portYIELD()；//底层函数\n本质：向中断控制和状态寄存器ICSR的bit28写1挂起PendSV来启动PendSV中断\n流程\n1：滴答定时器中断处理函数SysTick_Handler()\n启动了任务调度器，则进入xPortSysTickHandler()\n判断xTaskIncrementTick()\n则执行\n1 2 3 4 #define portNVIC_INT_CTRL_REG ( *( ( volatile uint32_t * ) 0xe000ed04 ) ) #define portNVIC_PENDSVSET_BIT ( 1UL \u0026lt;\u0026lt; 28UL ) //即向地址0xe000ed04 第28位写入1 portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; 2:\n#define portYIELD() { portNVIC_INT_CTRL_REG = portNVIC_PENDSVSET_BIT; __dsb( portSY_FULL_READ_WRITE ); __isb( portSY_FULL_READ_WRITE ); } PendSV中断服务函数\rFreeRTOSConfig.h中\n#define xPortPendSVHandler PendSV_Handler\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 __asm void xPortPendSVHandler( void ) { extern uxCriticalNesting; extern pxCurrentTCB;\t//任务控制块地址 extern vTaskSwitchContext; //八字节对齐 PRESERVE8 mrs r0, psp isb ldr r3, =pxCurrentTCB ldr r2, [ r3 ]\t//r2存放任务控制块地址 stmdb r0 !, { r4 - r11 } //r4到r11压入cpu任务栈 str r0, [ r2 ] stmdb sp !, { r3, r14 } mov r0, #configMAX_SYSCALL_INTERRUPT_PRIORITY msr basepri, r0 dsb isb bl vTaskSwitchContext mov r0, #0 msr basepri, r0 ldmia sp !, { r3, r14 } ldr r1, [ r3 ] ldr r0, [ r1 ] ldmia r0 !, { r4 - r11 } msr psp, r0 isb bx r14 nop } 十、时间片调度\r当多个任务的任务优先级相同时，CPU 会轮流的处理每一个任务，每个任务享有 CPU 处理的时间都是相同的，这个时间被称为时间片。\n一个时间片就等于 SysTick 中断周期（可以由用户自行设置），CPU 给与了一个时间片运行一个任务，不管这个任务是否运行完成，CPU 都会跑到下一个任务。\n流程：\n1.task1 运行完一个时间片切换到 task2\n2.task2 运行完一个时间片切换至 task3\n3.task3 运行阻塞(系统延时或等待信号量等)切换至下一个任务 task1\n没有用完的时间片不会再使用，下次任务 task3 得到执行还是按照一个时间片的时钟节拍运行\n4.task1 运行完一个时间片之后，切换至 task2 运行\n十一、任务相关API\r不多介绍\n十二、时间管理\r· 相对延时：指每次延时都是从任务执行函数vTaskDelay()开始，延时指定的时间结束。\n· 绝对延时：指每隔指定的时间，执行一次调用vTaskDelayUntil()函数的任务。\n即:vTaskDelay()是从调用其本身开始计时，vTaskDelayUntil()是从调用任务开始计时\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 BaseType_t xTaskDelayUntil( TickType_t *pxPreviousWakeTime, const TickType_t xTimeIncrement ); // Perform an action every 10 ticks. void vTaskFunction( void * pvParameters ) { TickType_t xLastWakeTime; const TickType_t xFrequency = 10; xLastWakeTime = xTaskGetTickCount(); for( ;; ) { // Wait for the next cycle. // Perform action here. vTaskDelayUntil( \u0026amp;xLastWakeTime, xFrequency ); } } 十三、FreeRTOS消息队列\r1.队列简介\r任务到任务，任务到中断，中断到任务数据交流的一种机制\n为什么不用全局变量？\n假设有一个全局变量a=0，现在俩个认为在写变量a\n任务1执行到第二步，任务2打断，执行a++，此时a才是1，回到任务1的第三步，a为1，但执行两次a++。\n因此全局变量数据无保护，当多任务操作数据时，数据受损\n使用队列\r为什么会安全？因为封装了临界区，即关闭中断，读写队列都有保护，防止多任务冲突\n队列存储数量有限、大小固定的数据。队列的每个数据叫队列项目\n队列要点\r1.数据入队出队：通常FIFO（先入先出），也可以LIFO（后入先出）\n2.数据传递方式：freertos采用实际值，将数据拷贝至队列，在传递较大数据时可以传递指针\n3.多任务访问：队列不属于某任务，任何任务、中断都可以读写\n4.出入阻塞：当任务向队列读写时，可以指定阻塞时间，队列已满或空则无法入队、出队\n阻塞时间是：0：直接返回不等待\n​\t0~max：超时不入队则不等待\n​\tmax：死等\n入/出队阻塞\r入队阻塞，队列满了，此时写不进去数据\n将该任务的状态列表项挂载再 pxDelayedTaskLisk； 将该任务的事件列表项挂载在 xTasksWaitingToSend； 出队阻塞，队列为空，此时读取不了数据：\n将该任务的状态列表项挂载再 pxDelayedTaskLisk； 将该任务的事件列表项挂载在 xTasksWaitingToReceive； 多任务都在等待同一个队列入队，都在阻塞，那当队列有空间，谁先进入就绪态:\n那优先级最高的入队，若优先级相同，等最久的入队\n2.队列结构体\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 typedef struct QueueDefinition { int8_t * pcHead; /* 存储区域的起始地址 */ int8_t * pcWriteTo; /* 下一个写入的位置 */ union { QueuePointers_t xQueue; //当用于队列时 SemaphoreData_t xSemaphore; //当用于信号量时 } u; List_t xTasksWaitingToSend; /* 写入阻塞任务列表 */ List_t xTasksWaitingToReceive; /* 读取阻塞任务列表 */ volatile UBaseType_t uxMessagesWaiting; /* 非空闲项目的数量 */ UBaseType_t uxLength; /* 队列的长度 */ UBaseType_t uxItemSize; /* 队列项目的大小 */ /* 锁用于在任务因队列操作被阻塞前，防止中断或其他任务操作队列。 * 上锁期间，队列可以写入和读取消息，但不会操作队列阻塞任务列表， * 当有消息写入时，cTxLock加1，当有消息被读取时，cRxLock加1， * 在解锁时，会统一处理队列的阻塞任务列表 */ volatile int8_t cRxLock; /* 读取上锁计数器 */ volatile int8_t cTxLock; /* 写入上锁计数器 */ //以下省略一些条件编译 } xQUEUE; 3.队列相关API函数\r主要流程:创建队列-\u0026gt;写队列-\u0026gt;读队列\n1 2 3 #define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate(( uxQueueLength ),( uxItemSize ), (queueQUEUE_TYPE_BASE )) 其中queueQUEUE_TYPE_BASE代表不同创建类型的功能，实际都是基于一个函数实现的\n#define queueQUEUE_TYPE_BASE ( ( uint8_t ) 0U ) /* 队列 */ #define queueQUEUE_TYPE_SET ( ( uint8_t ) 0U ) /* 队列集 */ #define queueQUEUE_TYPE_MUTEX ( ( uint8_t ) 1U ) /* 互斥信号量 */ #define queueQUEUE_TYPE_COUNTING_SEMAPHORE ( ( uint8_t ) 2U ) /* 计数型信号量 */ #define queueQUEUE_TYPE_BINARY_SEMAPHORE ( ( uint8_t ) 3U )\t/* 二值信号量 */ #define queueQUEUE_TYPE_RECURSIVE_MUTEX ( ( uint8_t ) 4U ) /* 递归互斥信号量 */ QueueHandle_t xQueueCreate( uxQueueLength, uxItemSize )\nuxQueueLength:队列长度\nuxItemSize：队列项大小\n返回值QueueHandle_t句柄\nNULL：创建失败，其他值：创建成功，返回句柄\n3.1写入消息API\r这四个函数都是对xQueueGenericSend()的宏定义\n1 2 3 4 5 6 7 8 9 10 11 #define xQueueSend(xQueue, pvItemToQueue, xTicksToWait) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK) #define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK) #define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT) #define xQueueOverwrite( xQueue, pvItemToQueue) xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE) xQueueGenericSend为\nBaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition); 3.2读取消息API\rxQueueReceive\n从队列中读取消息，并且消息读取成功后，会将消息从队列中移除\nBaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait ); xQueuePeek\n不会删除的函数，参数一致\n4.队列相关API函数解析\r4.1xQueueCreate\r#define xQueueCreate( uxQueueLength, uxItemSize )\n​\txQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )\n对队列而言xQueueGenericCreate前两个入口参数有效\n首先长度判断\nif( ( uxQueueLength \u0026gt; ( UBaseType_t ) 0 ) \u0026amp;\u0026amp; ( ( SIZE_MAX / uxQueueLength ) \u0026gt;= uxItemSize ) \u0026amp;\u0026amp; ( ( SIZE_MAX - sizeof( Queue_t ) ) \u0026gt;= ( uxQueueLength * uxItemSize ) ) ) 队列项存储区大小\nxQueueSizeInBytes = ( size_t ) ( uxQueueLength * uxItemSize );\n申请内存是队列项存储区+结构体存储区，并把首地址给到pxNewQueue\npxNewQueue = ( Queue_t * ) pvPortMalloc( sizeof( Queue_t ) + xQueueSizeInBytes );\n再将地址指向队列项存储区\npucQueueStorage = ( uint8_t * ) pxNewQueue; pucQueueStorage += sizeof( Queue_t );\n执行\nQueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )\n除一个是存储区首地址，一个是队列结构体首地址pucQueueStorage，pxNewQueue外，由外部传入\nprvInitialiseNewQueue( uxQueueLength, uxItemSize, pucQueueStorage, ucQueueType, pxNewQueue );\n进入prvInitialiseNewQueue后\nif( uxItemSize == ( UBaseType_t ) 0 )是信号量\n否则是队列，执行以下行，将队列项存储区首地址给到队列首地址\npxNewQueue-\u0026gt;pcHead = ( int8_t * ) pucQueueStorage;\n再进入\n( void ) xQueueGenericReset( pxNewQueue, pdTRUE );\n4.2xQueueSend\r#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait )\nxQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK ) 进入\nxYieldRequired = prvCopyDataToQueue( pxQueue, pvItemToQueue, xCopyPosition );\n如果尾部插入\n十四、信号量\r1.简介\r队列是发送数据，信号是发送状态，信号量是队列的衍生\n信号量结局同步机制（我做完一件事告诉你），任务间的通信以及事件通知，对共享资源的有序访问\n举例：\n1.停车场车位是否还有（判断信号量是否还有资源）\n2.停车场有空位（有资源），就可以将车开入（获取信号量）\n3.停车场无空位（无资源），不可以将车开入（获取信号量失败），可以等待其他人开出（释放）\n如果最大值被限定为 1 ，那么它就是二值信号量； 如果最大值不是 1 ，它就是计数型信号量。\n1.二值信号量\r本质：长度为1的队列，该队列只有空和满\n通常用于互斥访问（一个任务持有锁，其他任务无法执行）和任务同步（一个任务等另一个任务完成才能执行下去），互斥信号量相似，但二值信号量可能导致优先级反转\n二值信号量相关API函数\r过程：创建-释放-获取\n创建\nSemaphoreHandle_t xSemaphoreCreateBinary( void )\n释放\n传入句柄xSemaphore\nBaseType_t xSemaphoreGive( xSemaphore )\n返回值pdPASS：成功\nerrQUEUE_FULL：失败\n获取\n传入句柄xSemaphore，阻塞时间xBlockTime ，死等为portMAX_DELAY\nBaseType_t xSemaphoreTake( xSemaphore,xBlockTime )\n返回值：pdTRUE 获取信号量成功 pdFALSE 超时，获取信号量失败\n2.计数型信号量\r计数型信号量相当于队列长度大于 1 的队列，因此计数型信号量能够容纳多个资源，这在计数型信号量被创建的时候确定的\n适用场合：\n事件计数：每次事件发生，事件处理函数中释放计数型信号量，计数值+1，其他任务获取计数型信号量，计数值-1，初始计数值一般设为0 资源管理：信号量表示有效资源数量，任务必须先获取信号量，计数值-1，才能获取资源控制权，当计数值减到0时表示没有资源，当任务使用完资源后，必须释放，计数值+1,。信号量创建时一般等于最大资源数 计数型信号量相关API\r创建-释放-获取\n创建：\n1 2 3 4 5 6 //uxMaxCount计数值的最大值限定 //uxInitialCount计数值的初始值 #define xSemaphoreCreateCounting(uxMaxCount, uxInitialCount) xQueueCreateCountingSemaphore((uxMaxCount),(uxInitialCount)) //返回值NULL创建失败 //其他值返回句柄 获取和释放与二值相同\n3.优先级翻转\r优先级翻转：高优先级任务反而慢执行，低优先级任务反而先执行\n优先级翻转在抢占式内核中是很常见的问题，但是在实时操作系统中是不允许出现优先级翻转的，因为优先级翻转会破环任务的预期顺序，可能会导致未知的严重后果。在使用二值信号量的时候，经常会遇到优先级翻转问题。\n角色 行为 优先级 H（高） 想用共享资源（如 UART、SPI） 3（最高） L（低） 已经拿到资源，正在用 1（最低） M（中） 纯计算任务，不碰资源 2 L 先运行，拿到互斥量 → 进入临界区。 H 就绪，优先级最高，抢占 L，但发现互斥量被占 → 阻塞。 M 就绪，优先级介于 H 和 L 之间 → 抢占 L，开始跑长计算。 结果： • H 必须等 M 跑完 → M 跑完 L 才能继续 → L 释放后 H 才能拿到。 • H 实际被 优先级远低于它的 L 间接阻塞。 4.互斥信号量\r互斥信号量是一个拥有优先级继承的二值信号量，在同步的应用中二值信号量最适合，互斥信号量适用与需要互斥访问的应用中。\n优先级继承：当一个互斥信号量正在被一个低优先级的任务持有时， 如果此时有个高优先级的任务也尝试获取这个互斥信号量，那么这个高优先级的任务就会被阻塞。不过这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级。\n依旧L阻塞H，但M就不会浪费时间执行，将优先级翻转危害降低。\n就是不能完全消除影响，但尽可能把影响降到最低。\n注意：互斥信号量不能用于中断服务函数中。原因：\n互斥信号量有任务优先级继承的机制，但中断不是任务，没有任务优先级，所以互斥信号量只能用于任务中，不能用于中断服务函数 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态 互斥信号量相关API\r宏 configUSE_MUTEXES 置一\n创建互斥信号量 -（ task ）获取信号量 -（ give ）释放信号量\n无传入参数\nxSemaphoreCreateMutex()\n返回NULL创建失败，其他值返回句柄\n获取与释放与二值信号量相同，只是不支持中断调用\n十五、队列集\r1.简介\r一个队列只允许任务间传递同一种数据类型，如果要传递不同数据类型，可以使用队列集。\n作用：\n对多个队列或信号量进行监听，不管哪个消息到来，都可以让任务退出阻塞状态\n原始思路：\n队列集思路：\n2.队列集相关API\r2.1创建xQueueCreateSet\rQueueSetHandle_t xQueueCreateSet( const UBaseType_t uxEventQueueLength );\n参数：uxEventQueueLength 队列集可容纳的队列数量\n返回值：NULL：队列集创建失败 其他值：创建成功，返回句柄\n2.2添加xQueueAddToSet\r往队列集添加队列时，队列不能有消息\nBaseType_t xQueueAddToSet( QueueSetMemberHandle_t xQueueOrSemaphore , QueueSetHandle_t xQueueSet );\n2.3移除\r移除前不能有有效消息\nBaseType_t xQueueRemoveFromSet( QueueSetMemberHandle_t xQueueOrSemaphore , QueueSetHandle_t xQueueSet );\n2.4获取有效消息xQueueSelectFromSet\r1 2 QueueSetMemberHandle_t xQueueSelectFromSet( QueueSetHandle_t xQueueSet, TickType_t const xTicksToWait ) 示例\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 member_handle = xQueueSelectFromSet(queueset_handle,portMAX_DELAY); if(member_handle == queue_handle) { if(xQueueReceive(queue_handle,\u0026amp;key,portMAX_DELAY)==pdPASS) { printf(\u0026#34;队列queue_handle读取数据成功\\r\\n\u0026#34;); } } else if(member_handle == semphr_handle) { if(xSemaphoreTake(semphr_handle,portMAX_DELAY)==pdPASS) { printf(\u0026#34;获取信号量成功\\r\\n\u0026#34;); } } 十六、事件标志组\r1.简介\r事件标志组：用一个位表示时间是否发生\n是一组事件标志位的集合，类似多个标志位\n特点：\n每一个位表示一个事件（高八位不算） 每位事件的含义由用户决定如bit0表示按键，bit1表示消息是否接受 1：事件发生，0：事件未发生 任意任务和中断可读写这些位 可等待某一位，也可等待多位 一个事件组包含，EventBits_t是数据类型变量\n一个事件组最多可以存储 24 个事件标志\n与队列信号量区别\n2.事件标志组相关API\r2.1创建xEventGroupCreate\rEventGroupHandle_t xEventGroupCreate ( void) ;\n返回NULL：失败\n​\t其他值：成功返回句柄\n2.2清零标志位xEventGroupClearBits\rEventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToClear )\n参数：\nxEventGroup 待操作的事件标志组句柄 uxBitsToSet 待清零的事件标志位\n返回：整数=清零事件标志位之前事件组中事件标志位的值\n2.3设置标志位xEventGroupSetBits\rEventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet )\n参数：\nxEventGroup 待操作的事件标志组句柄 uxBitsToSet 待设置的事件标志位\n返回：整数=函数返回时，事件组中的事件标志位值\n2.4等待标志位xEventGroupWaitBits\r1 2 3 4 5 EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToWaitFor, const BaseType_t xClearOnExit, const BaseType_t xWaitForAllBits, TickType_t xTicksToWait ) 2.5同步函数\r1 2 3 4 EventBits_t xEventGroupSync(EventGroupHandle_t xEventGroup, const EventBits_t uxBitsToSet, const EventBits_t uxBitsToWaitFor, TickType_t xTicksToWait) 示例\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 EventGroupHandle_t eventgroup_handle; #define EVENTBIT_0 1\u0026lt;\u0026lt;0 #define EVENTBIT_1 1\u0026lt;\u0026lt;1 eventgroup_handle = xEventGroupCreate(); if(key==KEY0_PRES) { xEventGroupSetBits(eventgroup_handle,EVENTBIT_0); }else if(key==KEY1_PRES) { xEventGroupSetBits(eventgroup_handle,EVENTBIT_1); } event_bit = xEventGroupWaitBits(eventgroup_handle,EVENTBIT_0|EVENTBIT_1, pdTRUE,pdFALSE,portMAX_DELAY); printf(\u0026#34;等待到的事件标志位值:%#x\\r\\n\u0026#34;,event_bit); 十七、任务通知\r1.简介\r任务通知：用来通知任务，任务控制块中的结构体成员变量 ulNotifiedValue 就是这个通\n知值\n为什么还要这个？\n使用任务通知时，任务结构体就包含了内部对象，可以直接接收别人发过来的通知 使用队列、信号量、事件标志组时都需另外创建一个结构体，通过中间的结构体进行简介通信 发送端修改接收端任务控制块中结构体变量：ulNotifiedValue\n任务通知值的更新方式\n覆盖或不覆盖接受任务的通知值（队列） 更新接收任务通知值的一个过多个 bit（事件标志组） 增加接收任务的通知值（信号量） 但只能在一些场合替代\n优劣势\n优势：\n效率高：使用任务通知向任务发送事件或数据比使用队列、事件标志组或信号量快 使用内存更小：使用其他方法时都要先创建对应的结构体，使用任务通知时无需 劣势：\n无法发送数据给 ISR（中断服务函数）：ISR 没有任务结构体，因此无法给 ISR 发送数据，但 ISR可以使用任务通知的功能发送数据给任务 无法广播给多个任务：任务通知只能是被指定的一个任务接收并处理 无法缓存多个数据：任务通知是通过更新任务通知值来发送数据的，任务结构体中只有一个任务通知值，只能保持一个数据 发送受阻不支持阻塞：发送方无法进入阻塞状态等待 2.任务通知值和通知状态\r任务的任务控制块中，有两个结构体成员变量就是任务通知相关的\n1 2 3 4 5 6 7 8 9 10 typedef struct tskTaskControlBlock { ... #if(configUSE_TASK_NOTIFICATIONS == 1)\t//打开这个宏 volatile uint32_t ulNotifiedValue[configTASK_NOTIFICATION_ARRAY_ENTRIES];\t//通知值 volatile uint8_t ucNotifyState[configTASK_NOTIFICATION_ARRAY_ENTRIES];\t//通知状态 endif ... }tskTCB; #define configTASK_NOTIFICATION_ARRAY_ENTRIES 1\t//定义任务通知数组的大小，默认为1 任务通知值ulNotifiedValue\n更新方式：\n计数值（数值累加，类似信号量） 相应位置1（类似事件标志组） 任意数（支持覆写不覆写，类似队列） 任务通知状态ucNotifyState\n三种取值：\n#define taskNOT_WAITING_NOTIFICATION ((uint8_t)0)\t//任务未等待通知 #define taskWAITING_NOTIFICATION ((uint8_t)1)\t//任务在等待通知 #define taskNOTIFICATION_RECEIVED ((uint8_t)2)\t//任务在等待接收\n未等待通知：任务通知默认的初始化状态（事不关己，高高挂起） 等待通知：接收方已经准备好了（调用了接收任务通知函数），等待发送方给个通知 等待接收：发送方已经发送出去（调用了发送任务通知函数），等待接收方接收 3.任务通知API函数\r有发送和接收，发送可在任务和中断中，接收只能在任务（因为中断没TCB）\n3.1发送\r函数原型\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 BaseType_t xTaskGenericNotify(TaskHandle_t xTaskToNotify, UBaseType_t uxIndexToNotify, uint32_t ulValue, eNotifyAction eAction, uint32_t * pulPreviousNotificationValue); #define xTaskNotify(xTaskToNotify, ulValue, eAction) xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY), (ulValue), (eAction), NULL) #define xTaskNotifyAndQuery(xTaskToNotify, ulValue, eAction, pulPreviousNotifyValue) xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY), (ulValue), (eAction), (pulPreviousNotifyValue)) #define xTaskNotifyGive(xTaskToNotify) xTaskGenericNotify((xTaskToNotify), (tskDEFAULT_INDEX_TO_NOTIFY), (0), eIncrement, NULL) 其中通知方式（通知值更新方式）eNotifyAction\n1 2 3 4 5 6 7 8 typedef enum { eNoAction = 0, /* 无操作 */ eSetBits /* 更新指定 bit */ eIncrement /* 通知值加一 */ eSetValueWithOverwrite /* 覆写的方式更新通知值*/ eSetValueWithoutOverwrite /* 不覆写通知值 */ } eNotifyAction; 3.2接收\r当任务通知用作与信号量时，使用函数获取信号量：ulTaskNotifyTake( ) 当任务通知用作与事件标志组或队列时，使用此函数来获取：xTaskNotifyWait( ) 3.2.1ulTaskNotifyTake\rulTaskNotifyTake( xClearCountOnExit , xTicksToWait )\n#define ulTaskNotifyTake(xClearCountOnExit, xTicksToWait) ulTaskGenericNotifyTake((tskDEFAULT_INDEX_TO_NOTIFY), (xClearCountOnExit), (xTicksToWait)) 原型\nuint32_t ulTaskGenericNotifyTake(UBaseType_t uxIndexToWaitOn, BaseType_t xClearCountOnExit, TickType_t xTicksToWait); 通知值清零模拟二值信号量，通知值减一模拟计数型信号量\n3.2.2xTaskNotifyWait\n此函数用于等待任务通知通知值中的指定比特位被置 1，此函数可以在等待前和成功等待到任务通知通知值中的指定比特位被置 1 后清零指定比特位，并且还能获取等待超时后任务通知的通知值。\n#define xTaskNotifyWait(ulBitsToClearOnEntry, ulBitsToClearOnExit, pulNotificationValue, xTicksToWait) xTaskGenericNotifyWait(tskDEFAULT_INDEX_TO_NOTIFY, (ulBitsToClearOnEntry), (ulBitsToClearOnExit), (pulNotificationValue), (xTicksToWait)) 形参 描述 uxIndexToWaitOn 等待的任务通知索引（任务通知数组下标） ulBitsToClearOnEntry 进入等待前，把任务通知值的哪些位清 0（旧值对应位清零） ulBitsToClearOnExit 等待成功后，把任务通知值的哪些位清 0（新值对应位清零） pulNotificationValue 用于取出当时的 32 位通知值；若不需要，可设为 NULL xTicksToWait 最大阻塞等待时间（以系统节拍为单位） 返回值 描述 pdTRUE 等待任务通知成功（已满足等待条件） pdFALSE 等待任务通知失败（超时或被中断） 4.任务通知模拟信号量\r模拟二值信号量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void task1(void* pvParamenters) { uint8_t key = 0; while(1) { key = key_scan(0); if(key==KEY0_PRES) { printf(\u0026#34;任务通知模拟二值信号量释放\\r\\n\u0026#34;); xTaskNotifyGive(task2_handle); } } } void task2(void* pvParamenters) { uint32_t rev = 0; while(1) { rev = ulTaskNotifyTake(pdTRUE,portMAX_DELAY); if(rev!=0) { printf(\u0026#34;接受任务通知成功,获取模拟二值信号量\\r\\n\u0026#34;); } } } 模拟计数型信号量\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 void task1(void* pvParamenters) { uint8_t key = 0; while(1) { key = key_scan(0); if(key==KEY0_PRES) { printf(\u0026#34;任务通知模拟计数型信号量释放\\r\\n\u0026#34;); xTaskNotifyGive(task2_handle); } } } void task2(void* pvParamenters) { uint32_t rev = 0; while(1) { rev = ulTaskNotifyTake(pdFALSE,portMAX_DELAY); if(rev!=0) { printf(\u0026#34;接受任务通知成功,模拟获取计数型信号量,rev:%d\\r\\n\u0026#34;,rev); } vTaskDelay(1000); } } 5.任务通知模拟消息邮箱（类似队列）\r消息邮箱其实就是长度为1的队列\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void task1(void *pvParamenters) { uint8_t key = 0; while (1) { key = key_scan(0); if ((key != 0) \u0026amp;\u0026amp; (task2_handle != NULL)) { printf(\u0026#34;任务通知模拟消息邮箱发送,发送的键值为:%d\\r\\n\u0026#34;, key); xTaskNotify(task2_handle, key, eSetValueWithOverwrite); } vTaskDelay(10); } } void task2(void *pvParamenters) { uint32_t notify_val = 0; while (1) { xTaskNotifyWait(0, 0xFFFFFFFF, \u0026amp;notify_val, portMAX_DELAY); printf(\u0026#34;接受到的通知值为:%d\\r\\n\u0026#34;,notify_val); } } 6.任务通知模拟事件标志组\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #define EVENTBIT_0 1\u0026lt;\u0026lt;0 #define EVENTBIT_1 1\u0026lt;\u0026lt;1 void task1(void *pvParamenters) { uint8_t key = 0; while (1) { key = key_scan(0); if(key==KEY0_PRES) { printf(\u0026#34;更新bit0位置1\\r\\n\u0026#34;); xTaskNotify(task2_handle,EVENTBIT_0,eSetBits); } else if(key==KEY1_PRES) { printf(\u0026#34;更新bit1位置1\\r\\n\u0026#34;); xTaskNotify(task2_handle,EVENTBIT_1,eSetBits); } vTaskDelay(10); } } void task2(void *pvParamenters) { uint32_t notify_val = 0,event_bit = 0; while (1) { xTaskNotifyWait(0,0xFFFFFFFF,\u0026amp;notify_val,portMAX_DELAY); if(notify_val\u0026amp;EVENTBIT_0) { event_bit|=EVENTBIT_0; } if(notify_val\u0026amp;EVENTBIT_1) { event_bit|=EVENTBIT_1; } if(event_bit == (EVENTBIT_0|EVENTBIT_1)) { printf(\u0026#34;任务通知模拟事件标志组接收成功\\r\\n\u0026#34;); event_bit = 0; } } } 十八、软件定时器\r1.简介\r定时器：指定时刻开始，经过指定时间，触发超时事件，用户可自定义周期\n硬件定时器：芯片自带定时器，精度高，通过定时器中断，在中断服务函数处理\n软件定时器：定时功能的软件，可设定定时周期，超时调用回调函数，在回调函数处理\n软件定时器优缺点\n优点：硬件定时器有限，而软件定时器理论更多；使用简单\n缺点：精度低，对于高精度要求，不使用\nFreeRTOS软件定时器特点\n可裁剪：configUSE_TIMERS 配置项配置成 1\n支持单次和周期\n注：软件定时器的超时回调函数是由软件定时器服务任务调用的，软件定时器的超时回调函数本身不是任务，因此不能在该回调函数中使用可能会导致任务阻塞的 API 函数\n软件定时器服务任务：vTaskStartScheduler()中会创建\n软件定时器命令队列\nFreeRTOS 提供了许多软件定时器相关的 API 函数，这些 API 函数大多是往定时器的队列中写入消息（发送命令），这个队列叫做软件定时器命令队列。这个队列控制软件定时器\n软件定时器相关配置\nconfigUSE_TIMERS配置为1\n启动任务调度器时，会自动创建软件定时器的服务 / 守护任务prvTimerTask( );\n软件定时器服务任务的优先级为configTIMER_TASK_PRIORITY = 31;\n定时器的命令队列长度为configTIMER_QUEUE_LENGTH = 5;\n注：\n服务任务服务于多个定时器，因此定时器的回调函数不能影响其他定时器\n回调函数要尽快实行，不能进入阻塞状态，既不能调用那些会阻塞任务的 API 函数，如vTaskDelay( );\n访问队列或者信号量的非零阻塞时间的 API 函数也不能调用\n软件定时器状态\n休眠态、运行态\n单次定时器和周期定时器\n2.软件定时器结构体\r1 2 3 4 5 6 7 8 9 10 11 12 typedef struct { const char * pcTimerName /* 软件定时器名字 */ ListItem_t xTimerListItem /* 软件定时器列表项 */ TickType_t xTimerPeriodInTicks; /* 软件定时器的周期*/ void * pvTimerID /* 软件定时器的 ID */ TimerCallbackFunction_t pxCallbackFunction; /* 软件定时器的回调函数*/ #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxTimerNumber /* 软件定时器编号调试用*/ #endif uint8_t ucStatus; /* 软件定时器的状态 */ } xTIMER; 3.软件定时器相关API\r3.1创建xTimerCreate\r1 2 3 4 5 TimerHandle_t xTimerCreate(const char * const pcTimerName, const TickType_t xTimerPeriodInTicks, const UBaseType_t uxAutoReload, void * const pvTimerID, TimerCallbackFunction_t pxCallbackFunction); 形参 描述 pcTimerName 软件定时器名称（字符串） xTimerPeriodInTicks 定时周期，以系统时钟节拍（ticks）为单位 uxAutoReload 定时器模式：\npdTRUE = 周期定时器（重复触发）\npdFALSE = 单次定时器（触发一次后停止） pvTimerID 用户自定义 ID，可用于多个定时器共用同一回调函数时区分实例 pxCallbackFunction 定时器超时时的回调函数指针 返回值 描述 NULL 软件定时器创建失败 非 NULL 句柄 软件定时器创建成功，返回该定时器的句柄 创建例子\n1 2 timer1_handle = xTimerCreate(\u0026#34;timer1\u0026#34;,1000,pdFALSE,(void *)1,timer1_callback); timer2_handle = xTimerCreate(\u0026#34;timer2\u0026#34;,1000,pdTRUE,(void *)1,timer2_callback); 3.2开启xTimerStart\rBaseType_t xTimerStart( TimerHandle_t xTimer, const TickType_t xTicksToWait );\n参数：xTimer 待开启的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间\n返回pdPASS，pdFAIL\n3.3停止xTimerStop\rBaseType_t xTimerStop( TimerHandle_t xTimer, const TickType_t xTicksToWait);\n参数：xTimer 待停止的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间\n返回pdPASS，pdFAIL\n3.4复位xTimerReset\r使软件定时器的重新开启定时\nBaseType_t xTimerReset( TimerHandle_t xTimer, const TickType_t xTicksToWait);\n参数：xTimer 待复位的软件定时器的句柄 xTickToWait 发送命令到软件定时器命令队列的最大等待时间\n返回pdPASS，pdFAIL\n3.5更改超时时间xTimerChangePeriod\rBaseType_t xTimerChangePeriod( TimerHandle_t xTimer, const TickType_t xNewPeriod, const TickType_t xTicksToWait);\n参数：xTimer 待更新的软件定时器的句柄 xNewPeriod 新的定时超时时间，单位：系统时钟节拍 xTickToWait 发送命令到软件定时器命令队列的最大等待时间\n返回pdPASS，pdFAIL\n示例\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 TimerHandle_t timer1_handle = 0;/*单次定时器*/ TimerHandle_t timer2_handle = 0;/*周期定时器*/ void timer1_callback(TimerHandle_t pxTimer); void timer2_callback(TimerHandle_t pxTimer); timer1_handle = xTimerCreate(\u0026#34;timer1\u0026#34;,1000,pdFALSE,(void *)1,timer1_callback); timer2_handle = xTimerCreate(\u0026#34;timer2\u0026#34;,1000,pdTRUE,(void *)1,timer2_callback); xTimerStart(timer2_handle,0); xTimerStart(timer1_handle,0); void timer1_callback(TimerHandle_t pxTimer) { static uint32_t timer1 = 0; printf(\u0026#34;Timer1的运行次数:%d\\r\\n\u0026#34;,++timer1); } void timer2_callback(TimerHandle_t pxTimer) { static uint32_t timer2 = 0; printf(\u0026#34;Timer2的运行次数:%d\\r\\n\u0026#34;,++timer2); } 十九、Tickless低功耗模式\r1.低功耗模式\rSTM32提供集中低功耗，睡眠，停止，待机\nFreeRTOS也提供Tickless低功耗模式，通过动态调整SysTick 减少不必要的 CPU 唤醒，从而降低功耗\n为了降低功耗，又不影响系统运行，可以在本该空闲任务执行期间，让 MCU 进入相应的低功耗模式；当其他任务准备运行的时候，唤醒 MCU 退出低功耗模式，但是进入低功耗之后，多久唤醒，下一个要运行的任务如何被准确唤醒？任何中断均可唤醒 MCU，若滴答定时器频繁中断则会影响低功耗的效果。这些问题可以这样解决：\n将滴答定时器的中断周期修改为低功耗运行时间 推出低功耗之后，补上系统时钟节拍数\n2.相关配置\r使能低功耗 Tickless 模式：configUSE_TICKLESS_IDLE 定义系统进入相应低功耗模式的最短时长：configEXPECTED_IDLE_TIME_BEFORE_SLEEP 定义需要在系统进入低功耗模式之前执行的事务，例如：进入低功耗之前关闭外设时钟，以降低功耗：configPRE_SLEEP_PROCESSING(x)，需要自己在 FreeRTOSconfig.h 里面自己定义 定义需要在系统退出低功耗模式之后执行的事务，例如：退出低功耗之后开启被关闭的外设时钟，使系统能够正常运行：configPOST_SLEEP_PROCESSING(x)，需要自己在 FreeRTOSconfig.h 里面自己定义\n3.实现\r1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 #define configUSE_TICKLESS_IDLE 1 #define configEXPECTED_IDLE_TIME_BEFORE_SLEEP 2 //接着在 FreeRTOSconfig.h 文件中找个位置定义下面函数 #include \u0026#34;freertos_demo.h\u0026#34; #define configPRE_SLEEP_PROCESSING( x )\tPRE_SLEEP_PROCESSING() #define configPOST_SLEEP_PROCESSING( x )\tPOSR_SLEEP_PROCESSING() //freertos_demo.h 文件里声明这两个函数 void PRE_SLEEP_PROCESSING(void); void POST_SLEEP_PROCESSING(void); //freertos_demo.c 文件里，入口函数上面定义 //进入低功耗模式前要关闭的事情 void PRE_SLEEP_PROCESSING(void) { __HAL_RCC_GPIOA_CLK_DISABLE(); __HAL_RCC_GPIOB_CLK_DISABLE(); __HAL_RCC_GPIOC_CLK_DISABLE(); __HAL_RCC_GPIOD_CLK_DISABLE(); __HAL_RCC_GPIOE_CLK_DISABLE(); __HAL_RCC_GPIOF_CLK_DISABLE(); __HAL_RCC_GPIOG_CLK_DISABLE(); } //退出低功耗模式后需要打开的事情 void POST_SLEEP_PROCESSING(void) { __HAL_RCC_GPIOA_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOC_CLK_ENABLE(); __HAL_RCC_GPIOD_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); __HAL_RCC_GPIOF_CLK_ENABLE(); __HAL_RCC_GPIOG_CLK_ENABLE(); } 二十、FreeRTOS内存管理\r1.FreeRTOS内存管理简介\rFreeRTOS创建任务、队列、信号量等时，可用动态方法或者静态方法创建\n动态方法：申请时FreeRTOS从内存堆中自动申请，删除对象时自动释放 静态方法：用户提供内存空间，但是占用的空间固定下来了，删除依旧占用 本节讲的是动态内存管理方式（实际是管理算法的区别heap.c）\nfreertosconfig.h中定义的宏管理内存堆的大小：\n1 2 #define configTOTAL_HEAP_SIZE ((size_t)(10 * 1024)) /* FreeRTOS堆中可用的RAM总量, 单位: Byte, 无默认需定义 */ 2.FreeRTOS内存管理算法\r算法 优点 缺点 heap_1 分配简单，时间确定 只允许申请内存，不允许释放内存 heap_2 允许申请和释放内存 不能合并相邻空闲块，产生碎片，时间不定 heap_3 直接调用 C 库 malloc()/free()，实现简单（不使用） 速度慢、时间不定 heap_4 相邻空闲块可合并，减少碎片 时间不定 heap_5 支持管理多个非连续内存区域，功能同 heap_4 时间不定 2.1 heap_1\rheap_1只能申请内存无法释放内存，若工程中创建好的任务、队列、信号量等不需要删除，那使用heap_1\nheap_1分配图示：\n2.2 heap_2\rheap_2采用最适应算法，支持释放内存，但是并不能将相临空闲内存块合并，不可避免产生内存碎片。如果需要频繁的创建和删除任务，且所创建的任务堆栈都相同，可以用heap_2\n最适应算法：\n假设heap 有 3 块空闲内存：5,25,50 申请一个总共20字节的任务，先找到最小能满足的内存：25 删除任务内存释放后，得到20、5两块内存碎片 最后存在：5,5,20,50 heap_2分配图示：\n2.3 heap_4\rheap_4 使用了首次适应算法，能够将空闲且相邻的内存进行合并。适合频繁分配释放大小不同内存。\n首次适应算法：\n假设heap 有 3 块空闲内存（按内存块地址由低到高排序）：5,50,25 申请一个总共20字节的任务，先找到第一个能满足的内存：50 删除任务内存释放后，这块内存依旧为50 最后存在：5,50,25 heap_4分配图示：\n2.4 heap_5\rheap_5能管理多块不连续内存，但需要用户指定内存区域\n使用如下结构体：\n1 2 3 4 5 typedef struct HeapRegion { uint8_t * pucStartAddress; /* 内存区域的起始地址 */ size_t xSizeInBytes; /* 内存区域的大小，单位：字节 */ } HeapRegion_t; 指定多块不连续内存\n1 2 3 4 5 6 7 8 Const HeapRegion_t xHeapRegions[] = { { (uint8_t *)0x80000000, 0x10000 }, /* 内存区域1 */ { (uint8_t *)0x90000000, 0xA0000 }, /* 内存区域2 */ { NULL, 0 } /* 数组终止标志 */ }; vPortDefineHeapRegions(xHeapRegions); ","date":"2025-07-12T10:00:00+08:00","permalink":"/p/freertos/","title":"FreeRTOS 学习笔记：从入门到内核机制"},{"content":"CAN简介\rCAN总线特征：\n两根通信线(CAN_H、CAN_L)，无需共地 差分信号，抗干扰 高速CAN:125k-1mbps，\u0026lt;40m 低速CAN:10k~125kbps, \u0026lt;1km 异步通信(无时钟线)，通信速率由设备各自约定 半双工，可挂载多设备，多设备同时发送数据时通过仲裁判断先后顺序 11位/29位报文ID，用于区分消息功能，同时决定优先级 可配置1~8字节的有效载荷 可实现广播式和请求式两种传输方式 应答、CRC校验、位填充、位同步、错误处理等特性 主流通信协议\r名称 引脚 双工 时钟 电平 设备 应用场景 UART TX、RX 全双工 异步 单端 点对点 两个设备互相通信 I2C SCL、SDA 半双工 同步 单端 多设备 一个主控外挂多个模块 SPI SCK、MOSI、MISO、SS 全双工 同步 单端 多设备 一个主控外挂多个模块（高速） CAN CAN_H、CAN_L 半双工 异步 差分 多设备 多个主控互相通信 CAN类似UART和IIC的结合\n第一章\rCAN硬件电路\r主控会内置CAN控制器，借助CAN收发器挂载在CAN总线网络\n高速CAN采用闭环网络，CAN的差分信号默认状态下(1，CANH=CANL)，由电阻收紧，而电阻因为阻抗不大，因此电压跃变快，但是功耗高\n低速CAN使用开环网络，两端加开路的电阻，降低反射\n电平标准\r差分信号，即电压差传输数据\n高速CAN：\n隐性电平是不碰两根线，电阻处为收紧状态（默认状态1，隐性）\n显性电平是拉开两根线，电阻处为非收紧状态（显性可以控制总线，0强于1，所以能叫显性）\n电压差为0V时表示逻辑1（隐性电平） 电压差为2V时表示逻辑0（显性电平） CAN收发器-TJA1050(高速CAN)\rCANH和CANL有电压差，RECEIVER输出1，下管导通，RXD为0(有电压差，逻辑0，显性)\nCANH和CANL无电压差，RECEIVER输出0，上管导通，RXD为1(无电压差，逻辑1，隐性)\nTXD为0时，2,3的场效应管导通，CANH和CANL有电压差（逻辑0，显性）\n而TXD悬空时，电流源默认让TXD为1\nCAN物理层特性\r第二章\rCAN总线帧格式\r主要看数据帧\n帧类型 用途 数据帧 发送设备主动发送数据（广播式） 遥控帧 接收设备主动请求数据（请求式） 错误帧 某个设备检测出错误时向其他设备通知错误 过载帧 接收设备通知其尚未做好接收准备 帧间隔 用于将数据帧及遥控帧与前面的帧分离开 数据帧\rSOF（Start of Frame）：帧起始，表示后面一段波形为传输的数据位 ID（Identify）：标识符，区分功能，同时决定优先级 RTR（Remote Transmission Request ）：远程请求位，区分数据帧和遥控帧 IDE（Identifier Extension）：扩展标志位，区分标准格式和扩展格式 SRR（Substitute Remote Request）：替代RTR，协议升级时留下的无意义位 r0/r1（Reserve）：保留位，为后续协议升级留下空间 DLC（Data Length Code）：数据长度，指示数据段有几个字节 Data：数据段的1~8个字节有效数据 CRC（Cyclic Redundancy Check）：循环冗余校验，校验数据是否正确 ACK（Acknowledgement）：应答位，判断数据有没有被接收方接收 CRC/ACK界定符：为应答位前后发送方和接收方释放总线留下时间 EOF（End of Frame ）：帧结束，表示数据位已经传输完毕 标准格式\r开始为空闲状态为隐性 SOF为帧起始，表明开始一帧数据 起始帧后发送11位标识符报文，区分功能，同时决定优先级 RTR为0：数据帧。为1：遥控帧 IDE为0：标准格式。为1：扩展格式 r0保留位，暂时无功能 DLC为数据长度，0001发一个字节，1000发8字节 Data根据DLC给几个字节能给范围0-64字节 CRC为校验 ACK应答位，发送方释放总线，如果接收方收到了，就抢占总线，发送方读取总线状态知道被接受 CRC/ACK界定符是留出时间交出控制权 EOF作为结束 遥控帧\r适合使用频率低的数据\n遥控帧无数据段，RTR为隐性电平1，其他部分与数据帧相同\n如果有数据需要，首先接收方发出一个遥控帧，遥控帧包含报文ID，如果发送方有这个报文ID，就将数据发送出来，接收方可以及时获取数据\n错误帧\r总线所有设备监督总线数据，一旦发现错误，这些设备发出错误帧来破坏数据，同时终止当前的发送设备\n过载帧\r接收方收到大量数据无法处理，发送过载帧，延缓发送方的数据发送，以平衡总线负载，避免数据丢失\n帧间隔\r将数据帧和遥控帧与前面的帧分离开\n位填充\r规则：发送方每发送5个相同电平，自动追加相反电平的填充位，接收方检测到填充位，自动移除填充位。\n即将发送：\t100000110\t10000011110\t0111111111110 实际发送： 1000001110\t1000001111100\t011111011111010 实际接收： 1000001110\t1000001111100\t011111011111010 移除填充后： 100000110\t10000011110\t0111111111110\n作用：\n增加定时信息，有利于接收方同步，防止波形长时间无变化（防止连续很多相同电平，接收方可能数错） 将正常数据流与“错误帧”和“过载帧”区分（防止连续6个相同电平） 保持CAN发送数据的活跃状态，防止认为总线空闲（防止连续11个隐性电平1） 波形实例\r标准数据帧\r11位报文ID：0x555，\nRTR = 0 数据帧，IDE = 0，标准格式\nDLC = 0001(注意其中第一个1为位填充) = 1，一个字节数据\nData = 10101010 = 0xAA\n扩展数据帧\r扩展数据帧，报文ID为0x0789ABCD，数据长度1字节，数据内容为0x56\n第三章\r接收方数据采样\rCAN总线无时钟线，总线设备通过一个约定的波特率方式确定每个数据位的时长 发送方以约定的位时长每个固定时间输出一个数据位 接收方以约定的位时长每个固定时间采样总线电平，输入一个数据位 理想采样是数据位中心附近 会遇到的问题\r接收方采样点没有对齐数据位中心附近 解决：第一个跳变沿对齐（硬同步）\n接收方刚开始采样正确，但是时钟有误差，随着误差积累，采样点逐渐偏离 解决：采样点动态调节，弥补时钟误差（再同步）\n位时序\r因为要调节采样点，所以会对一个数据位时长进行细分\n同步段（SS） 传播时间段（PTS） 相位缓冲段1（PBS1） 相位缓冲段2（PBS2） 每个段又由若干个最小时间单位（Tq）构成\nSS = 1Tq、PTS = 18Tq、PBS1 = 18Tq、PBS2 = 2~8Tq\n如果跳边沿在SS段，说明当前设备与波形达成同步，否则不同步，则需要调节时序使跳变沿正好在同步段\n硬同步\r使第一次采样对齐\n每个设备有一个位时序计时周期，当某设备发送报文，其他设备接收到SOF下降沿，会将位时序计时周期拨到SS段，与发送方保持同步（实际就是让第一个边沿段调整为SS段，类似于时区，绝对时刻不一样，但都规定为北京时间）\n而此时接受段直接确定数据位采样点（PBS1和PBS2段中间）\n硬同步解决没有对齐数据位中心附近，只要时钟没误差，就不会有问题\n再同步\r解决时钟误差，若数据位边沿逐渐偏离SS段，此时接收方根据再同步补偿宽度值（sjw，是最大的补偿，如果误差1tq，sjw2，则补偿1tq；若误差3tq，则补偿2tq）加长PBS1段，或缩短PBS2段\n再同步发生在，第一个下降沿之后每个数据位跳变边沿\n波特率计算\r波特率 = 1/一个数据位的时长 = 1/（Tss+Tpts+Tpbs1+Tpbs2）\nSS = 1Tq，PTS = 3Tq，PBS1 = 3Tq，PBS2 = 3Tq，Tq = 0.5us\n则波特率 = 1 / (0.5us + 1.5us + 1.5us + 1.5us) = 200kbps\n第四章\r多设备同时发送遇到的问题\rCAN总线只有一差分信号线，同时只能有一个设备操作总线\n若多设备同时又发送需求，该如何分配总线资源？\n资源分配规则1-先占先得\r若当前有设备操作总线发送数据帧/遥控帧，则其他设备不可发（可发错误帧/过载帧） 任何设备检测到11连续隐性电平，认为总线空闲，才能发送 若有设备有发送需求要等待总线空闲 而位填充的适用范围是从帧起始（SOF）到CRC序列结束，不包括CRC界定符、ACK域和EOF段\n此时有ACK界定符一个隐性，EOF7个隐性，只需再等三个隐性即可继续发送。\n但有可能不止一个信号在等待，多个设备同时到来（总线仲裁）\n资源分配规则2-非破坏性仲裁\r多设备发送需求同时到来，对仲裁段（ID+RTR）进行非破坏性仲裁，ID号小（优先级高）的取得控制权，ID号大（优先级低）仲裁失利，继续等待空闲\n实现非破坏性仲裁两个需要的特性：\n线与特性：0 \u0026amp; X \u0026amp; X = 0，1 \u0026amp; 1 \u0026amp; 1 = 1 回读机制：设备发出一个数据位后，读回总线电平状态，确认电平被发送。发送0读回0，发送1读回不一定是1 仲裁：发出数据1，读回（回读机制）数据0（被其他设备线与了）\n非破坏性仲裁过程\r数据位从前到后依次比较，出现差异且数据位为1的设备仲裁失利\n单元1在仲裁段中某一个数据位发送1，回读出0，判断有其他设备在发生，但自己优先级低，放弃发送。但是总线电平依旧是单元2的，并没有被破坏\n数据帧和遥控帧优先级\r数据帧和遥控帧ID号一样时，数据帧的优先级高于遥控帧（RTR决断，因为RTR = 0 为数据帧）\n标准格式和拓展格式的优先级\r标准格式11位ID号和扩展格式29位ID号的高11位一样时，标准格式的优先级高于扩展格式（SRR必须始终为1，以保证此要求）\n第五章\r错误类型\r位错误、填充错误、CRC错误、格式错误、应答错误\n错误状态\r主动错误状态参与通信并检测到错误时发送主动错误帧（6个0），可以破坏别的设备的数据 而频繁发送主动错误，说明这个设备不太可靠，就转到被动错误，收回他破坏其他设备通信的权利 若还是频繁被动错误，则进入总线关闭 如何判断：每个设备内部管理一个TEC和REC，根据TEC和REC确定自己状态（信誉分） 错误计数器\r信誉分积分：\nSTM32 CAN外设\rSTM32内置bxCAN支持CAN2.0A和2.0B，可自动发报文，程序只需处理报文数据 波特率最高1mbps 3个可配置优先级的发送邮箱 2个3级深度接收FIFO 时间触发通信、自动离线恢复、自动唤醒、禁止自动重传、接收FIFO溢出处理方式可配置、发送优先级可配置、双CAN模式 STM32F103C8T6 CAN资源：CAN1 第一节\rCAN网络拓扑\rSTM32F103C8T6 CAN引脚默认复用\n重定义\nCAN收发器电路\r注意TX，RX接法与串口区分\n第二节\rCAN框图\r对于STM32F103C8T6，没有CAN2，只有CAN1。\n通过读写以下寄存器对CAN电路进行配置：\n3个主发送邮箱，每个可以存入一个CAN报文，若想发送一个报文，可以将报文放入空置邮箱，在设置寄存器请求发送\n接收部分包括过滤器和2个FIFO，过滤器设置过滤规则，告诉硬件想要的ID的报文，如果硬件收到相应ID的报文，将报文放入FIFO；如果都没用，就抛弃，减轻软件负担。\nFIFO：先入先出，类似于队列，等待CPU读取\nCAN基本结构\r发送过程\r选择空置邮箱-写入报文-配置寄存器请求发送\nRQCP：请求完成\nTXOK：发送成功\nTME = 1：发送邮箱空\n发送流程：\n默认TME=1处于空置状态时，TXRQ（请求发送控制位）=1（产生发送请求） 进入挂号状态TME=0，排队等到该邮箱为最高优先级发送报文 进入预定状态，若优先级被比下去，则退回挂号状态 处于预定的报文，探测到CAN总线=IDLE空闲，管理员将报文发送到CAN总线 发送成功回到空置状态，发送失败时若启用NART=0（禁止自动重发）回到空置，若禁止，NART=1则回到预定等待发送（比如总线仲裁失败） 接收过程\r接收到一个报文→匹配过滤器后进入FIFO 0或FIFO 1→CPU读取\nFMP：报文数目\nFOVR：FIFO溢出\n接收到一个报文时，FMP=1,FOVR=0，依次接收后FMP递增 有效报文存满FULL置1，在收到有效报文进入溢出FOVR=1 若读取一个报文，RFOM=1，释放一次邮箱 注意：溢出状态释放一次是挂号2\n发送和接收配置位\rNART：NART=1关闭自动重传，CAN报文只发送一次，不管发送结果如何；置0，自动重传，直到发送成功 TXFP：TXFP=1，优先级按发送请求的顺序；置0，优先级按报文ID，标志符小的先发送（标志符相等，邮箱号小的先发送） RFLM：置1，接收FIFO锁定，FIFO溢出，新报文丢弃；置0，FOFO溢出后最后收到报文被覆盖 标识符过滤器\rx可以表示0-13，每个过滤器都有一套电路\n每个过滤器由2个32位寄存器组成，R1[31:0] R2[31:0]\n位宽设置，FSCx=0，16位，FSCx=1,32位\n若只有标准ID，则16位足矣，若有标准，有拓展需要用到32位 16位的话两个32位寄存器能写4个ID号，32位写两个 模式设置，\nFBMx=0，屏蔽模式\n允许一类，使用了类似掩码功能，屏蔽的是不需要的ID号的位，如只需要用前三位，可以ID段前三位写001xxxxxxxx，屏蔽写11100000000,过滤ID前三位为001\nFBMx=1，列表模式（允许个别）\n关联到队列，FFAx=0，关联FIFO0，否则关联FIFO1\n激活FACTx:FACTx=0，禁用，否则启用\n列表模式\n想要的ID号写入R1或R2寄存器中\n位宽32位\nSTID高11位为标准ID 前29位是拓展ID 过滤拓展ID，IDE=1，过滤标准ID，IDE=0 RTR=1，过滤遥控帧，RTR=0,过滤数据帧 位宽16位\n只使用标准ID，4个16位 屏蔽模式\nID号过多时使用 采用位与屏蔽，类似掩码功能 ID段有效位写实际，对应屏蔽位写1 过滤器配置示例\r第三节\r测试模式\r静默模式：用于分析CAN总线的活动，不会对总线造成影响 环回模式：用于自测试，同时发送的报文可以在CAN_TX引脚上检测到 环回静默模式：用于热自测试，自测的同时不会影响CAN总线 工作模式\r初始化模式：用于配置CAN时禁止收发 正常模式 睡眠模式：低功耗，CAN外设时钟停止，可以软件唤醒或硬件自动唤醒 AWUM：置1，自动唤醒，一旦检测到CAN总线活动，硬件就自动清零SLEEP，唤醒CAN外设；置0，手动唤醒，软件清零SLEEP，唤醒CAN外设 位时间特性\rSTM32中，PDS和PBS1合在一起\nSS = 1Tq BS1 = 116Tq BS2 = 18Tq SJW=1~4Tq 波特率=APB1时钟频率/分频系数/1位Tq数\n=36Mhz/(BRP[9:0]+1)/(1+(TS1[3:0]+1)+(TS2[2:0]+1))\n中断\rCAN外设占用4个专用中断向量\n发送中断：发送邮箱空时产生\nFIFO 0中断：收到一个报文/FIFO 0满/FIFO 0溢出时产生\nFIFO 1中断：收到一个报文/FIFO 1满/FIFO 1溢出时产生\n状态改变错误中断：出错/唤醒/进入睡眠时产生\n中断发生条件：\n","date":"2025-05-24T10:00:00+08:00","permalink":"/p/can/","title":"CAN 通信协议学习笔记"},{"content":"配置Git\r1 2 3 4 $ git config --global user.name \u0026#34;Your Name\u0026#34; $ git config --global user.email \u0026#34;email@example.com\u0026#34; 创建版本库\r1 2 3 4 5 6 7 8 $ mkdir learngit $ cd learngit $ pwd /Users/michael/learngit 1 2 $ git init 1 2 $ git add readme.txt 1 2 $ git commit -m \u0026#34;wrote a readme file\u0026#34; 版本管理\r版本回退\r1 2 $ git log 1 2 3 4 5 6 7 8 $ git log --pretty=oneline 1094adb7b9b3807259d8cb349e7df1d4d6477073 (HEAD -\u0026gt; master) append GPL e475afc93c209a690c39c13a46716e8fa000c366 add distributed eaadf4e385e865d25c48e7ca9c8395c3f7dfaef0 wrote a readme file 1 2 $ git reset --hard HEAD^//根据参数回退对应 1 2 $ git reset --hard 1094a 1 2 $ git reflog//查看每次 1 2 3 4 5 6 7 8 9 10 11 12 13 14 $ git status//查看状态 On branch master Changes to be committed: (use \u0026#34;git reset HEAD \u0026lt;file\u0026gt;...\u0026#34; to unstage) new file: LICENSE modified: readme.txt 管理修改\r第一次修改 -\u0026gt; git add -\u0026gt; 第二次修改 -\u0026gt; git add -\u0026gt; git commit\n1 2 $ git diff HEAD -- readme.txt //查看工作区和版本库最新版本区别 撤销修改\r1 2 $ git checkout -- readme.txt//丢弃工作区修改 1 2 $ git reset HEAD readme.txt//把暂存区的修改撤销掉 删除文件\r1 2 $ git rm test.txt 1 2 $ git checkout -- test.txx//此时是还原 远程仓库\r添加远程库\r1 2 $ git remote add origin https://github.com/Zesaen/learngit.git 1 2 $ git push -u origin master//第一次推送master分支时，加上了-u参数 1 2 $ git push origin master//此后本地提交 删除远程库\r1 2 $ git remote -v//查看远程库 1 2 $ git remote rm origin//删除origin 从远程库克隆\r1 2 $ git clone 分支管理\r创建并合并\r查看分支：git branch\n创建分支：git branch \u0026lt;name\u0026gt;\n切换分支：git switch \u0026lt;name\u0026gt;\n创建+切换分支：git switch -c \u0026lt;name\u0026gt;\n合并某分支到当前分支：git merge \u0026lt;name\u0026gt;\n删除分支：git branch -d \u0026lt;name\u0026gt;\n解决冲突\rmaster分支和feature1分支各自都分别有新的提交\nGit用\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;，=======，\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;标记出不同分支的内容\n1 2 3 4 5 6 7 8 9 10 \u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt;\u0026lt; HEAD Creating a new branch is quick \u0026amp; simple. ======= Creating a new branch is quick AND simple. \u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt;\u0026gt; feature1 Bug分支\r略\n强制删除分支\r1 2 $ git branch -D \u0026lt;name\u0026gt; 多人协作\r略\n标签管理\r1 2 $ git tag v1.0//给最新打标签 1 2 $ git tag v0.9 f52c633//给对应打标签 1 2 $ git show v0.9//查看标签信息 1 2 $ git tag -d v0.1//删除标签 操作标签\r命令git push origin \u0026lt;tagname\u0026gt;可以推送一个本地标签；\n命令git push origin --tags可以推送全部未推送过的本地标签；\n命令git tag -d \u0026lt;tagname\u0026gt;可以删除一个本地标签；\n命令git push origin :refs/tags/\u0026lt;tagname\u0026gt;可以删除一个远程标签。\n","date":"2025-05-15T13:30:00+08:00","permalink":"/p/learngit/","title":"git学习"},{"content":"通信：\n硬件层：RS485解决数据传输问题，如何将“0”或“1”传输到另外一个设备\n软件层：MODBUS，在硬件基础上解决数据传输的含义或意义\n对于RS485：作为两线制485由于传输差分信号，因此是半双工的\n对于MAX485芯片： RE和DE连一根线，为高电平时DE使能，单片机TX发送至DI。为低电平时RE使能，485信号发送至单片机RX\nmax485电路：\n对于主从机：RS485和modbus支持主从通信，能够一主多从\nmodbus约定：\n一个系统一主多从，只能进行主从通信，从从通信需要借助主机 系统中从机不可主动向主机发送数据 系统上电后所有设备都应处于监听总线的状态，也就是接收状态 如果要进行一次通信，必须由主机主动发起通信 一次modbus通信：模式为主机轮询，从机应答的机制\n主机一开始也是监听总线的状态，要切换成发送模式（实际上是修改MAX485上RE、DE的电平），发送数据包 发送完成后立马转成接收模式 modbus通信报文或数据包格式：\n通信地址0-247，指定地址0是广播指令，所有设备都会运行\n常用功能码：\n03功能码报文示例：从机读数据\n主机发送Tx:01 03 00 00 00 02 C4 0B\n地址码8bit 功能码8bit 寄存器起始16bit CRC校验16bit 主机发送 01 03 00 00 C4 0B 从机应答Rx:01 03 04 00 01 00 02 2A 32 读了2个寄存器，每个寄存器16位\n地址码 功能码 字节数 数据1 数据2 CRC校验 从机应答 01 03 04 00 01 00 02 2A 32 06功能码报文示例：向从机写一个寄存器数据\n主机发送Tx:01 06 00 03 00 C8 78 5C\n地址码8bit 功能码8bit 寄存器个数16bit 数据16bit CRC校验16bit 主机发送 01 06 00 03 00 C8 78 5C 从机应答Rx::01 06 00 03 00 C8 78 5C与主机发送一致\n如何接受报文：\n9600波特率，3字符 1个字符占10bit\n3*10*1/9600=3ms 超时时间10ms就认为一个新的数据包\n","date":"2025-05-10T10:00:00+08:00","permalink":"/p/modbus/","title":"Modbus 通信协议学习笔记"},{"content":"11.1 I2C硬件\rSCL,SDA，一条I2C总线可接很多设备，SCL,SDA放上拉电阻\n11.2 传输数据类比\r如何通过IIC传数据，\n主设备：一主多从，可把数据从主设备发到从设备，也可从设备发到主设备\n发送 主设备：开始(start)\n主设备：A，我把数据发你(地址/方向)\nA：收到(回应)\n主设备：发送数据(传输)\nA：收到后，告诉主设备(回应)\n主设备：结束(停止)\n接收 主设备：开始(start)\n主设备：B，把数据发我(地址/方向)\nB：收到(回应)\nB：把数据发给主设备(传输)\n主设备：收到数据后，告诉A(回应)\n主设备：结束(停止)\n11.3 IIC传输数据格式\r11.3.1写操作\r主设备发出start信号 发出设备地址，方向(读1/写0) 从设备发出应答信号 主设备发送一字节数据，等待回应 每传输一字节，接收方回应一次，在传输下一个数据 数据发送完成，主设备发送stop信号 11.3.2读操作\r11.3.3 IIC信号\rIIC协议栈数据传输单位是字节，但是要用到9个时钟，前面8个时钟用来传输8数据，第9个始终用来传输回应信号，先传输最高位\n开始信号(S)：SCL为高时，SDA由高跳低，开始传输数据 结束信号(P)：SCL为高时，SDA由低跳高，结束传输数据 响应信号(ACK)：接收到8位数据，第九个时钟周期，拉低SDA SDA数据必须在SCL高时稳定，只能在SCL为低时变化 11.3.4 协议细节\r1.SDA引脚必须双向IO，因为有发有收\n","date":"2025-04-19T09:30:00+08:00","permalink":"/p/iic/","title":"IIC 通信协议学习笔记"},{"content":"SPI通信\rSPI：通用数据总线\n四根通信线：轻松Mhz\nSCK(Serial Clock)\nMOSI(Master Output Slave Input)\nMISO(Master Input Slave Output)\nSS(Slave Select)\n同步，全双工\n支持总线挂载多设备（一主多从）\n常用模块：W25Q64 FLASH存储器、SPI OLED屏、NRF24L01 2.4G无线通讯模块、SD卡 支持SPI\n硬件电路\r所有SPI设备SCK、MOSI、MISO分别连接在一起 主机引出多条SS控制线，分别连接各从机SS引脚 输出引脚配置推挽输出（驱动能力强，驱动上下降沿强），输入引脚配置浮空或上拉 主机指定从机：将对应SS线输出低电平，其余高电平\n移位示意图\r波特率发生器作为时钟，每一个上升沿，移位寄存器移出一位，进行数据发送；每一个下降沿，最低位采样\n一般主机发0x00或0xff跟从机交换数据\nSPI时序基本单元\r起始条件：SS高电平到低电平 终止条件：SS低电平切换高电平 SPI由不同的移位模型，能根据SCK的不同边沿变化进行移位。\n模式1\rCPOL=0：空闲状态时，SCK低电平\nCPHA=1：SCK第一个边沿移出数据，第二个边沿移入数据（数据采样）\n模式0\rCPOL=0:空闲状态时，SCK为低电平 CPHA=0: SCK第一个边沿移入数据,第二个边沿移出数据\n将第一个数据变化的时机提前\nSPI时序\rW25Q64为例，芯片厂商规定\n发送指令 向SS指定设备，发送指令（0x06），00000110 ","date":"2025-04-12T09:30:00+08:00","permalink":"/p/spi/","title":"SPI 通信协议学习笔记"},{"content":"这是一篇备忘录，记录本博客的日常更新流程。现在有两种方式：**本地笔记工作台（推荐）**和手工流程。\n1. notes-ui 本地工作台（推荐）\r仓库里的 tools/notes-ui/ 是一个只监听本机的 Web 工具，双击 start.bat 启动后浏览器自动打开 http://127.0.0.1:8091 ：\n导入笔记：拖拽 .md 文件或整个笔记文件夹（图片会自动带上），也可以直接粘贴文本 检查元数据：标题、描述、分类、标签、发布时间都会自动预填，slug 实时查重；右侧图片清单能看到正文引用的图片是否齐全，缺的可补传 生成页面包：图片自动拷入 images/、正文路径自动改写、front matter 按站内规范生成 本地预览：弹窗内嵌真实主题渲染（含公式和 Mermaid） 分步发布：构建验证 → 部署上线（二次确认）→ 线上抽查 → git 提交，每步独立日志，出问题可停在中途 「文章管理」页还能改历史文章的元数据、切换草稿、改 slug、删除（进本地回收站）。\n2. 手工流程（备用）\r不用工具时，流程如下。\n写新文章\r在 content/post/ 目录下新建页面包 my-new-post/index.md（图片放同目录 images/），或单文件 my-new-post.md：\n1 2 3 4 5 6 7 8 9 10 11 12 13 --- title: \u0026#34;文章标题\u0026#34; date: 2025-03-15T11:30:00+08:00 slug: my-new-post draft: false description: \u0026#34;文章摘要，会显示在首页卡片上。\u0026#34; categories: - 分类名 tags: - 标签 --- 正文从这里开始，支持标准 Markdown。 把 draft 设为 true 可以存草稿，构建时不会发布。注意 front matter 的 date 不要写成未来时间，否则 Hugo 默认不构建该文章。\n本地预览（可选）\r1 hugo server -D 浏览器打开 http://localhost:1313 实时预览，Ctrl+C 退出。\n构建并部署\r1 2 # 在博客根目录执行一键部署脚本（构建 + 上传 + 原子替换） bash deploy/deploy.sh 部署完成，访问 https://zeseanblog.top 即可看到新文章。\n3. 让 AI 助手代劳\r直接说一句「帮我发布新文章」即可——本文件夹的 AGENTS.md 里记录了完整的环境信息和部署流程。\n4. 提交代码（备份）\r1 2 3 # 只提交这篇 文章 的路径，避免带上工作区其他未完成的改动 git add content/post/my-new-post git commit -m \u0026#34;post: 新文章标题\u0026#34; 文章源码就是备份，随时可以回滚或迁移。\n","date":"2025-03-15T11:30:00+08:00","permalink":"/p/how-to-publish/","title":"如何更新这个博客"},{"content":"折腾了一阵子，个人博客终于上线了 🎉 这是本站的第一篇文章，简单记录一下搭建过程。\n技术栈\r组件 选择 说明 静态站点生成器 Hugo 单二进制，构建速度极快 主题 Stack 卡片式设计，简洁美观 Web 服务器 Nginx 托管静态文件，性能足够 HTTPS Let\u0026rsquo;s Encrypt 免费证书，自动续期 服务器 腾讯云 Ubuntu 22.04 为什么选择静态博客？\r快：纯静态 HTML，没有数据库查询，加载飞快 稳：没有后端服务，几乎不会被黑，维护成本极低 省：一台最小配置的服务器就能流畅运行 接下来\r以后这里会不定期更新：\n📝 技术学习笔记 🔨 项目开发记录 💭 生活随想 欢迎常来逛逛～\n","date":"2025-03-15T10:00:00+08:00","permalink":"/p/hello-world/","title":"你好，世界"}]