[{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/tags/ai/","section":"Tags","summary":"","title":"AI","type":"tags"},{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/tags/claude/","section":"Tags","summary":"","title":"Claude","type":"tags"},{"content":" 什么是 Claude Code # Claude Code 是 Anthropic 推出的 AI 编程助手，以 CLI 为核心，能读懂你的整个代码仓库、编辑文件、执行命令，并深度集成到你的开发工作流中。它不只是一个聊天工具，而是一个真正的 Agent——能自主完成多步骤编码任务。\n安装 # 系统要求 # Node.js 18 或更高版本（npm 安装方式） Git（Windows 用户需安装 Git for Windows） 安装方式 # 原生安装（推荐）：\n1 2 3 4 5 # macOS / Linux / WSL curl -fsSL https://claude.ai/install.sh | bash # Windows PowerShell irm https://claude.ai/install.ps1 | iex Homebrew（macOS）：\n1 brew install --cask claude-code WinGet（Windows）：\n1 winget install Anthropic.ClaudeCode npm 全局安装：\n1 npm install -g @anthropic-ai/claude-code 安装完成后，在任意项目目录运行 claude 即可启动，首次使用会提示登录。\n订阅计划 # 计划 价格 适合场景 Pro $20/月 中等编码强度，日常开发辅助 Max 5x $100/月 重度编码，需频繁使用 Opus 模型 Max 20x $200/月 近乎自主的多 Agent 并行开发 也可使用 API 按量付费（Anthropic Console 获取 API Key）。\n基础命令 # 启动方式 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # 交互式会话（REPL） claude # 带初始提问启动 claude \u0026#34;解释这个项目的架构\u0026#34; # 单次查询后退出（适合脚本调用） claude -p \u0026#34;review this code\u0026#34; # 管道输入 cat logs.txt | claude -p \u0026#34;分析这些错误\u0026#34; # 继续最近的对话 claude -c # 恢复指定会话 claude -r \u0026#34;session-id\u0026#34; \u0026#34;继续上次的工作\u0026#34; 会话内的斜杠命令 # 在交互式会话中，可以使用以下命令：\n命令 说明 /help 显示所有可用命令 /compact 压缩上下文，节省 token /config 交互式配置设置 /model 切换 Claude 模型 /agents 管理子 Agent /mcp 管理 MCP 服务器 /vim 启用 vim 编辑模式 /clear 清除当前会话 /voice 语音输入模式 /plan 结构化规划模式 文件引用 # 用 @ 符号在提示中引用文件或目录：\n1 2 3 4 5 6 7 8 9 10 11 # 引用单个文件 \u0026gt; 审查这个组件的代码 @./src/components/Button.tsx # 引用整个目录 \u0026gt; 为所有 API 路由添加错误处理 @./src/api/ # 引用多个文件 \u0026gt; 比较这两个实现 @./src/old.js @./src/new.js # 通配符 \u0026gt; 审查所有测试文件 @./src/**/*.test.ts 执行 Shell 命令 # 用 ! 前缀直接在会话中运行 shell 命令：\n1 2 3 4 \u0026gt; !npm test # 进入 shell 模式（再输一次 ! 退出） \u0026gt; ! 模型选择 # Claude Code 提供三个核心模型：\n模型 特点 适用场景 Sonnet 性能均衡，响应快 日常编码，大多数任务 Haiku 最快最省 token 简单查询，批量处理 Opus 最强推理能力 复杂架构设计，多步骤规划 切换模型：\n1 2 3 4 5 # 会话内切换 /model # 命令行指定 claude --model claude-opus-4 配置体系 # 设置文件（分层继承） # 文件 作用域 ~/.claude/settings.json 全局（所有项目） .claude/settings.json 项目级（可提交到 git） .claude/settings.local.json 项目级（个人，不提交） CLAUDE.md 记忆文件 # 用 Markdown 文件给 Claude 提供项目上下文和指令：\n文件 作用域 ~/.claude/CLAUDE.md 全局指令 ./CLAUDE.md 项目级指令 ./src/CLAUDE.md 子目录级指令 示例 CLAUDE.md：\n1 2 3 4 5 # 项目规范 - 所有新代码使用 TypeScript - 遵循现有的 ESLint 配置 - React 组件使用函数式组件 + Hooks - 测试文件放在源文件旁边，命名为 .test.ts 权限配置 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { \u0026#34;permissions\u0026#34;: { \u0026#34;allowedTools\u0026#34;: [ \u0026#34;Read\u0026#34;, \u0026#34;Write(src/**)\u0026#34;, \u0026#34;Bash(git *)\u0026#34;, \u0026#34;Bash(npm *)\u0026#34; ], \u0026#34;deny\u0026#34;: [ \u0026#34;Read(.env*)\u0026#34;, \u0026#34;Write(production.config.*)\u0026#34;, \u0026#34;Bash(rm *)\u0026#34; ] } } 高级功能 # 自定义斜杠命令 # 在 .claude/commands/ 目录下创建 .md 文件，自动注册为斜杠命令：\n1 2 mkdir -p .claude/commands echo \u0026#34;分析这段代码的性能问题并提出优化建议\u0026#34; \u0026gt; .claude/commands/optimize.md 支持参数化：\n1 2 \u0026lt;!-- .claude/commands/fix-issue.md --\u0026gt; 按照我们的编码规范修复 issue #$ARGUMENTS 使用：/fix-issue 123\n子 Agent（Subagents） # 为特定任务创建专业化的 Agent 实例：\n1 2 \u0026gt; /agents # 按提示定义名称、描述、模型和人设 配置示例（.claude/agents/reviewer.md）：\n1 2 3 4 5 6 --- name: reviewer description: 用于代码审查 model: sonnet --- 你是一个专业的代码审查员。重点关注安全性、性能和可维护性。 Hooks 自动化 # 在特定事件触发时自动执行 shell 命令：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { \u0026#34;hooks\u0026#34;: { \u0026#34;PostToolUse\u0026#34;: [ { \u0026#34;matcher\u0026#34;: \u0026#34;Write(*.py)\u0026#34;, \u0026#34;hooks\u0026#34;: [ { \u0026#34;type\u0026#34;: \u0026#34;command\u0026#34;, \u0026#34;command\u0026#34;: \u0026#34;python -m black \\\u0026#34;$file\\\u0026#34;\u0026#34; } ] } ] } } 可用事件：PreToolUse、PostToolUse、UserPromptSubmit、SessionStart\nMCP 服务器扩展 # 通过 Model Context Protocol 扩展 Claude Code 的能力：\n1 2 # 添加 MCP 服务器 claude mcp add my-server -e API_KEY=123 -- /path/to/server arg1 arg2 常见用途：连接 Google Drive、Jira、数据库等外部工具。\n扩展思考（Extended Thinking） # 默认开启，让 Claude 在编码前进行深度推理：\n切换思考模式：Alt+T（Windows/Linux）或 Option+T（macOS） 查看思考过程：Ctrl+O 开启详细模式 限制思考 token：export MAX_THINKING_TOKENS=10000 常见工作流 # 代码分析 # 1 \u0026gt; 分析这个代码仓库的结构并提出改进建议 @./src/ 功能开发 # 1 \u0026gt; 实现一个带 JWT 令牌和密码哈希的用户认证系统 Bug 修复 # 1 \u0026gt; 调试这个错误：\u0026#34;TypeError: Cannot read property \u0026#39;id\u0026#39; of undefined\u0026#34; @./src/user-service.js 测试生成 # 1 \u0026gt; 为这个模块生成完整的单元测试 @./src/utils/validation.js CI/CD 集成 # 1 claude -p \u0026#34;如果有 linting 错误，修复它们并给出 commit 建议\u0026#34; 多平台支持 # Claude Code 不只是终端工具，它覆盖了多个开发环境：\n平台 特点 终端 CLI 全功能，最灵活 VS Code 扩展 内联 diff、@ 引用、对话历史 JetBrains 插件 IntelliJ/PyCharm/WebStorm 集成 桌面应用 可视化 diff、多会话并行 Web（claude.ai/code） 无需本地安装，支持移动端 所有平台共享相同的 CLAUDE.md、设置和 MCP 配置。\n最佳实践 # 提供充分的上下文 — 具体的指令比模糊的描述效果好得多 不同任务用不同会话 — 更省 token，输出质量更高 善用 CLAUDE.md — 把常用规范写进去，避免每次重复说明 审查后再接受 — 养成检查代码变更的习惯 用 hooks 自动格式化 — 让代码风格保持一致 敏感数据放 .env — 在权限中拒绝 Claude 读取 总结 # Claude Code 是目前最强大的 AI 编程 Agent 之一。它不仅能写代码，更能理解整个项目上下文、自主执行多步骤任务、与你的开发工具链深度集成。掌握本文提到的命令和工作流，能显著提升你的开发效率。\n参考资源：\nClaude Code 官方文档 在会话中输入 /help 查看完整命令列表 ","date":"April 3, 2026","externalUrl":null,"permalink":"/posts/claude-code-guide/","section":"Posts","summary":"什么是 Claude Code # Claude Code 是 Anthropic 推出的 AI 编程助手，以 CLI 为核心，能读懂你的整个代码仓库、编辑文件、执行命令，并深度集成到你的开发工作流中。它不只是一个聊天工具，而是一个真正的 Agent——能自主完成多步骤编码任务。\n安装 # 系统要求 # Node.js 18 或更高版本（npm 安装方式） Git（Windows 用户需安装 Git for Windows） 安装方式 # 原生安装（推荐）：\n","title":"Claude Code 使用指南：从入门到高效开发","type":"posts"},{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/","section":"mkk Blog","summary":"","title":"mkk Blog","type":"page"},{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/posts/","section":"Posts","summary":"","title":"Posts","type":"posts"},{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"April 3, 2026","externalUrl":null,"permalink":"/tags/%E5%B7%A5%E5%85%B7/","section":"Tags","summary":"","title":"工具","type":"tags"},{"content":" freeRTOS # esp-idf是基于freeRTOS的框架，所以是首先要掌握的内容\nfreeRTOS任务概述 # 在低端设备中，程序基本分为裸机和RTOS，针对简单的程序，我们用裸机程序完全可以满足，一旦功能复杂，程序模块众多，裸机程序往往很难满足我们的需求。因此我们就要用到RTOS系统。\n使用 FreeRTOS 的实时应用程序可以被构建为一组独立的任务。每个任务在自己的上下文中执行，不依赖于系统内的其他任务或 RTOS 调度器本身。\n任务分为四个状态：运行、准备就绪、阻塞、挂起\n运行 # 当任务实际执行时，它被称为处于运行状态。 任务当前正在使用处理器。 如果运行RTOS 的处理器只有一个内核， 那么在任何给定时间内都只能有一个任务处于运行状态。\n准备就绪 # 准备就绪任务指那些能够执行（它们不处于阻塞或挂起状态），但目前没有执行的任务，因为同等或更高优先级的不同任务已经处于运行状态。\n阻塞 # 如果任务当前正在等待时间或外部事件，则该任务被认为处于阻塞状态。例如，如果一个任务调用vTaskDelay()，它将被阻塞（被置于阻塞状态），直到延迟结束-一个时间事件。任务也可以通过阻塞来等待队列、信号量、事件组、通知或信号量事件。处于阻塞状态任 务通常有一个\u0026quot;超时\u0026quot;期， 超时后任务将被超时，并被解除阻塞，即使该任务所等待的事件没有发生。“阻塞”状态下的任务不使用任何处理时间，不能被选择进入运行状态。\n挂起 # 与“阻塞”状态下的任务一样，“挂起”状态下的任务不能 被选择进入运行状态，但处于挂起状态的任务没有超时。相反，任务只有在分别通过 vTaskSuspend() 和 xTaskResume()API 调用明确命令时 才会进入或退出挂起状态。\n优先级 # 每个任务均被分配了从 0 到 (configMAX_PRIORITIES-1) 的优先级，其中的configMAX_PRIORITIES 在 FreeRTOSConfig.h 中定义，低优先级数字表示低优先级任务。空闲任务的优先级为零。\n任务创建 # 1 2 3 4 5 6 7 8 9 BaseType_t xTaskCreatePinnedToCore( TaskFunction_t pvTaskCode, //任务函数指针，原型是 voidfun(void *param) const char *constpcName, //任务的名称，打印调试可能会有用 const uint32_t usStackDepth,//指定的任务堆栈空间大小（字节） void *constpvParameters, //任务参数 UBaseType_t uxPriority, (configMAX_PRIORITIES- 1)// 优 先 级，数字越大，优先级越大，0 到 TaskHandle_t *constpvCreatedTask,//传回来的任务句柄 const BaseType_t xCoreID) //分配在哪个内核上运行 延迟函数 # 1 2 3 4 // 阻塞指定的时间，单位为系统时钟节拍数 void vTaskDelay( const TickType_t xTicksToDelay ) //用于在指定的时间点之前阻塞任务，直到时间到达。 void vTaskDelayUntil( TickType_t * const pxPreviousWakeTime, const TickType_t xTimeIncrement) 示例 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;esp_log.h\u0026#34; void TASK1(void *pvParameters){ while(1){ printf(\u0026#34;TASK 1\\n\u0026#34;); vTaskDelay(pdMS_TO_TICKS(500)); } } void app_main(void) { xTaskCreatePinnedToCore(TASK1, \u0026#34;TASK1\u0026#34;, 2048, NULL, 1, NULL, 0); } 队列 # 队列是任务间通信的主要形式。它们可以用于在任务之间以及中断和任务之间发送消息。在大多数情况下，它们作为线程安全的 FIFO（先进先出）缓冲区使用，新数据被发送到队列的后面， 尽管数据也可以发送到前面。\n常用如下API：\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 //创建一个队列，成功返回队列句柄 QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength,//队列容量 UBaseType_t uxItemSize//每个队列项所占内存的大小（单位是字节） ); //向队列头部发送一个消息 BaseType_t xQueueSend( QueueHandle_t xQueue,// 队列句柄 const void * pvItemToQueue, //要发送的消息指针 TickType_t xTicksToWait //等待时间); //向队列尾部发送一个消息 BaseType_t xQueueSendToBack( QueueHandle_t xQueue,// 队列句柄 const void * pvItemToQueue, //要发送的消息指针 TickType_t xTicksToWait //等待时间); //从队列接收一条消息 BaseType_t xQueueReceive( QueueHandle_t xQueue,//队列句柄 void * pvBuffer,//指向接收消息缓冲区的指针。 TickType_t xTicksToWait //等待时间); //xQueueSend 的中断版本 BaseType_t xQueueSendFromISR( QueueHandle_t xQueue,// 队列句柄 const void * pvItemToQueue, //要发送的消息指针 BaseType_t *pxHigherPriorityTaskWoken );////指出是否有高优先级的任务被唤醒 示例 # 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;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;esp_log.h\u0026#34; #include \u0026#34;freertos/queue.h\u0026#34; QueueHandle_t xQueue = NULL; typedef struct{ int x; int y; }data; void TASK1(void *pvParameters){//从队列中读取数据 while(1){ data data1; if (xQueueReceive(xQueue, \u0026amp;data1, portMAX_DELAY)) { ESP_LOGI(\u0026#34;TASK1\u0026#34;, \u0026#34;Received data: %d %d\u0026#34;, data1.x, data1.y); } } } void TASK2(void *pvParameters){//向队列中写入数据 while(1){ data data1; data1.x = 1; data1.y = 2; if (xQueueSend(xQueue, \u0026amp;data1, portMAX_DELAY)) { ESP_LOGI(\u0026#34;TASK2\u0026#34;, \u0026#34;Sent data: %d %d\u0026#34;, data1.x, data1.y); vTaskDelay(1000 / portTICK_PERIOD_MS); } } } void app_main(void) { xQueue = xQueueCreate(10, sizeof(data)); xTaskCreatePinnedToCore(TASK1, \u0026#34;TASK1\u0026#34;, 2048, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(TASK2, \u0026#34;TASK2\u0026#34;, 2048, NULL, 1, NULL, 0); } 信号量 # 信号量是用来保护共享资源不会被多个任务并发使用，信号量使用起来比较简单。因为在freeRTOS中它本质上就是队列，只不过信号量只关心队列中的数量而不关心队列中的消息内容，在freeRTOS中有两种常用的信号量，一是计数信号量，而是二进制信号量。\n二进制信号量很简单，就是信号量总数只有1\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 //创建二值信号量，成功则返回信号量句柄（二值信号量最大只有1个） SemaphoreHandle_t xSemaphoreCreateBinary( void ); //创建计数信号量，成功则返回信号量句柄 SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,//最大信号量数 UBaseType_t uxInitialCount);//初始信号量数 //获取一个信号量，如果获得信号量，则返回 pdTRUE xSemaphoreTake( SemaphoreHandle_t xSemaphore,//信号量句柄 TickType_t xTicksToWait );//等待时间 //释放一个信号量 xSemaphoreGive( SemaphoreHandle_t xSemaphore ); //信号量句柄 //删除信号量 void vSemaphoreDelete( SemaphoreHandle_t xSemaphore ); 示例 # 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;esp_log.h\u0026#34; #include \u0026#34;freertos/queue.h\u0026#34; #include \u0026#34;freertos/semphr.h\u0026#34; SemaphoreHandle_t xSemaphore; void TASK1(void *pvParameters){//释放信号量 while(1){ xSemaphoreGive(xSemaphore); vTaskDelay(pdMS_TO_TICKS(1000)); } } void TASK2(void *pvParameters){//获取信号量 while(1){ if(xSemaphoreTake(xSemaphore,portMAX_DELAY)==pdTRUE){ ESP_LOGI(\u0026#34;TASK2\u0026#34;,\u0026#34;TASK2 is running\u0026#34;); } } } void app_main(void) { xSemaphore = xSemaphoreCreateBinary(); xTaskCreatePinnedToCore(TASK1, \u0026#34;TASK1\u0026#34;, 2048, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(TASK2, \u0026#34;TASK2\u0026#34;, 2048, NULL, 1, NULL, 0); } 互斥锁 # 互斥锁是一种特殊的信号量，它只有两种状态，即锁定和解锁。当一个任务获得互斥锁时，其他任务无法获得该互斥锁，直到该任务释放该互斥锁。核心就是优先级发生反转\n1 2 //创建互斥锁 SemaphoreHandle_t xSemaphoreCreateMutex( void ); 事件组 # 事件位：用于指示事件是否发生，事件位通常称为事件标志\n事件组：就是一组事件位。 事件组中的事件位通过位编号来引用\n以下是常用的API函数\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 //创建一个事件组，返回事件组句柄，失败返回NULL EventGroupHandle_t xEventGroupCreate( void ); //等待事件组中某个标志位,用返回值以确定哪些位已完成设置 EventBits_t xEventGroupWaitBits( const EventGroupHandle_t xEventGroup, //事件组句柄 const EventBits_t uxBitsToWaitFor, //哪些位需要等待 const BaseType_t xClearOnExit, //退出时是否清除标志位 const BaseType_t xWaitForAllBits, //是否等待所有位 TickType_t xTicksToWait ); //等待时间 //设置事件组中某个标志位 EventBits_t xEventGroupSetBits( const EventGroupHandle_t xEventGroup, //事件组句柄 const EventBits_t uxBitsToSet ); //哪些位需要设置 //清除事件组中某个标志位 EventBits_t xEventGroupClearBits( const EventGroupHandle_t xEventGroup, //事件组句柄 const EventBits_t uxBitsToClear ); //哪些位需要清除 示例 # 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;esp_log.h\u0026#34; #include \u0026#34;freertos/event_groups.h\u0026#34; #define NUM_BIT_1 BIT0 #define NUM_BIT_2 BIT1 static EventGroupHandle_t event_group; void TASK1(void *pvParameters){//定时设置不同标志位 while(1){ xEventGroupSetBits(event_group,NUM_BIT_1); vTaskDelay(1000/portTICK_PERIOD_MS); xEventGroupSetBits(event_group,NUM_BIT_2); vTaskDelay(1000/portTICK_PERIOD_MS); } } void TASK2(void *pvParameters){//获取不同标志位 while(1){ EventBits_t bits = xEventGroupWaitBits(event_group,NUM_BIT_1|NUM_BIT_2,pdTRUE,pdFALSE,portMAX_DELAY); if(bits\u0026amp;NUM_BIT_1){ ESP_LOGI(\u0026#34;TASK2\u0026#34;,\u0026#34;NUM_BIT_1 is set\u0026#34;); } if(bits\u0026amp;NUM_BIT_2){ ESP_LOGI(\u0026#34;TASK2\u0026#34;,\u0026#34;NUM_BIT_2 is set\u0026#34;); } } } void app_main(void) { event_group = xEventGroupCreate(); xTaskCreatePinnedToCore(TASK1, \u0026#34;TASK1\u0026#34;, 2048, NULL, 1, NULL, 0); xTaskCreatePinnedToCore(TASK2, \u0026#34;TASK2\u0026#34;, 2048, NULL, 1, NULL, 0); } 直达任务通知 # 定义：每个RTOS任务都有一个任务通知数组。 每条任务通知 都有“挂起”或“非挂起”的通知状态， 以及一个32位通知值。直达任务通知是直接发送至任务的事件，而不是通过中间对象（如队列、事件组或信号量）间接发送至任务的事件。向任务发送“直达任务通知” 会将目标任务通知设为“挂起”状态（此挂起不是挂起任务）。\n1 2 3 4 5 //用于将事件直接发送到 RTOS 任务并可能取消该任务的阻塞状态 BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, //要通知的任务句柄 uint32_t ulValue,//携带的通知值 eNotifyAction eAction ); //执行的操作 需要注意的是参数eAction如下表所述\neNoAction 目标任务接收事件，但其通知值未更新。在这种情况下，不使用ulValue。 eSetBits 目标任务的通知值使用ulValue按位或运算 eIncrement 目标任务的通知值自增1（类似信号量的give操作） eSetValueWithOverwrite 目标任务的通知值无条件设置为ulValue。 eSetValueWithoutOrwrite 如果目标任务没有挂起的通知，则其通知值将设置为ulValue。如果目标任务已经有挂起的通知，则不会更新其通知值。 1 2 3 4 5 6 //等待接收任务通知 BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,//进入函数清除的通知值位 uint32_t ulBitsToClearOnExit,//退出函数清除的通知值位 uint32_t *pulNotificationValue,//通知值 TickType_t xTicksToWait );//等待时长 示例 # 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 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;esp_log.h\u0026#34; //任务句柄 TaskHandle_t xHandle1; TaskHandle_t xHandle2; void TASK1(void *pvParameters){ uint32_t value = 0; while(1){//发送任务通知 vTaskDelay(1000 / portTICK_PERIOD_MS); xTaskNotify(xHandle2, value, eSetValueWithOverwrite); value++; } } void TASK2(void *pvParameters){ while(1){//接收任务通知 uint32_t value = 0; xTaskNotifyWait(0, ULONG_MAX,\u0026amp;value, portMAX_DELAY); ESP_LOGI(\u0026#34;Task 2\u0026#34;, \u0026#34;Value: %lu\u0026#34;, value); } } void app_main(void) { /* 任务1：发送任务通知 任务2：接收任务通知 */ xTaskCreatePinnedToCore(TASK1, \u0026#34;TASK1\u0026#34;, 2048, NULL, 1, \u0026amp;xHandle1, 0); xTaskCreatePinnedToCore(TASK2, \u0026#34;TASK2\u0026#34;, 2048, NULL, 1, \u0026amp;xHandle2, 0); } GPIO # 涉及到GPIO，那就不得不点灯了\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 /定义LED的GPIO口 #define LED_GPIO GPIO_NUM_27 //LED 闪烁初始化 void led_flash_init(void){ gpio_config_t led_gpio_cfg = { .pin_bit_mask = (1\u0026lt;\u0026lt;LED_GPIO),//指定GPIO .mode = GPIO_MODE_OUTPUT,//设置为输出模式 .pull_up_en = GPIO_PULLUP_DISABLE,//禁止上拉 .pull_down_en = GPIO_PULLDOWN_DISABLE, //禁止下拉 .intr_type = GPIO_INTR_DISABLE,//禁止中断 }; gpio_config(\u0026amp;led_gpio_cfg); xTaskCreatePinnedToCore(led_run_task,\u0026#34;led\u0026#34;,2048,NULL,3,NULL,1); } 这个函数中，会定义一个关于gpio的配置结构体，然后通过gpio_config函数将配置设置到底层，通过这步，们就完成了gpio的初始化，这里我们将gpio设置成输出，这里注意，一般来说，GPIO有四种较为常见的工作模式：\n输出：可以设置GPIO的高低电平 输入：可以获取外部输入的高低电平信息，一般要设置加上拉电阻或下拉电阻 浮空输出：可以设置GPIO的高低电平，但要在电路外部中增加上拉电阻 开漏输入：可以获取外部输入的高低电平信息，但要在电路外部中增加上拉电阻 1 2 3 4 5 6 7 8 9 10 void led_run_task(void* param) { int gpio_level = 0; while(1) { vTaskDelay(pdMS_TO_TICKS(500)); gpio_set_level(LED_GPIO,gpio_level); gpio_level = gpio_level?0:1; } } 每隔500ms切换一次电平输出,就可以达到LED灯闪烁的效果了\n进阶——呼吸灯 # 当我们在GPIO引脚上增加一段高低电平的脉冲时，我们会看到灯一闪一闪，高低电平脉冲切换速度达到一定程度时（大约是25Hz），我们人眼是看不出来一闪一闪的效果，只会看到LED较暗，那到底暗多少，这就需要PWM脉宽调制的占空比来决定，所谓占空比，简单来说就是高电平时间占PWM周期的百分比时间。如果我们动态的改变占空比，那么就可以看到LED从暗到亮，从亮到暗的变化，这就是呼吸灯的效果\n下面来看一下呼吸灯的演示程序\n首先进行宏定义，我用到了48引脚的GPIO，以及定义了事件标志组和他的两个位\n1 2 3 4 5 6 7 8 //定义LED灯的引脚 #define LED_PIN GPIO_NUM_48 //定义事件组的位 #define NUM_BITS0 BIT0//占空比满了 #define NUM_BITS1 BIT1//占空比为0 //定义事件组 static EventGroupHandle_t event_group; 然后是主程序，初始化了GPIO口，以及初始化了两个内容，分别是ledc_timer_config和ledc_channel_config，ledc_timer_config用于初始化用到的定时器，ledc_channel_config用于初始化ledc输出通道以及将timer关联起来\nledc_set_fade_with_time设置一个PWM占空比目标值和渐变周期，这里代码示例是，需要在2000ms，将目前的占空比渐变至LEDC_DUTY（满占空比）ledc_fade_start函数启动渐变，通过LEDC_FADE_NO_WAIT参数设置为立刻返回，那我们怎么知道渐变完成呢？可以通过ledc_cb_register函数，注册一个回调函数，当渐变完成的时候会调用我们的回调函数\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 void app_main(void) { //创建事件组 event_group = xEventGroupCreate(); //配置GPIO gpio_config_t io_conf; io_conf.intr_type = GPIO_INTR_DISABLE;//禁止中断 io_conf.mode = GPIO_MODE_OUTPUT;//设置为输出模式 io_conf.pin_bit_mask = 1ULL\u0026lt;\u0026lt;LED_PIN;//设置引脚 io_conf.pull_down_en = 0;//禁止下拉 io_conf.pull_up_en = 0;//禁止上拉 gpio_config(\u0026amp;io_conf);//配置GPIO //初始化定时器 ledc_timer_config_t ledc_timer = { .speed_mode = LEDC_LOW_SPEED_MODE,//低功耗模式 .duty_resolution = LEDC_TIMER_13_BIT,//分辨率 .timer_num = LEDC_TIMER_0,//定时器编号 .freq_hz = 5000,//频率 .clk_cfg = LEDC_AUTO_CLK//自动时钟 }; ledc_timer_config(\u0026amp;ledc_timer);//配置定时器 //初始化通道 ledc_channel_config_t ledc_channel = { .gpio_num = LED_PIN,//GPIO引脚 .speed_mode = LEDC_LOW_SPEED_MODE,//低功耗模式 .channel = LEDC_CHANNEL_0,//通道编号 .intr_type = LEDC_INTR_DISABLE,//禁止中断 .timer_sel = LEDC_TIMER_0,//定时器编号 .duty = 0,//占空比 }; ledc_channel_config(\u0026amp;ledc_channel);//配置通道 ledc_fade_func_install(0);//安装fade函数 ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8191, 2000);//设置占空比和时间 ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数 //注册fade回调函数 ledc_cbs_t cbs = { .fade_cb = lcd_finish_cb //fade回调函数 }; ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, \u0026amp;cbs, NULL); //创建任务 xTaskCreatePinnedToCore(LED_run_task, \u0026#34;LED_run_task\u0026#34;, 2048, NULL, 1, NULL, 0); } 下面便是渐变完成后所执行的回调函数，用于通知下一轮的渐变\n1 2 3 4 5 6 7 8 9 10 11 12 13 bool IRAM_ATTR lcd_finish_cb(const ledc_cb_param_t *param, void *user_arg){ BaseType_t xHigherPriorityTaskWoken; if (param-\u0026gt;duty) { //占空比满了 xEventGroupSetBitsFromISR(event_group, NUM_BITS0,\u0026amp;xHigherPriorityTaskWoken); }else { //占空比为0 xEventGroupSetBitsFromISR(event_group, NUM_BITS1,\u0026amp;xHigherPriorityTaskWoken); } return xHigherPriorityTaskWoken; } LED任务用于重新初始化下一轮的渐变\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 void LED_run_task(void *pvParameters){ EventBits_t uxBits; while(1){ //等待事件组的位 uxBits = xEventGroupWaitBits(event_group, NUM_BITS0 | NUM_BITS1, pdTRUE, pdFALSE, portMAX_DELAY); //根据事件组的位执行相应的操作 //重新开启fade函数 if (uxBits \u0026amp; NUM_BITS0){ ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0, 2000); ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数 } if (uxBits \u0026amp; NUM_BITS1){ ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 8191, 2000); ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);//启动fade函数 } ledc_cbs_t cbs = { .fade_cb = lcd_finish_cb //fade回调函数 }; ledc_cb_register(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, \u0026amp;cbs, NULL); } } 再进阶——ws2812全彩灯 # WS2812 是集成控制单元以及RGB灯珠的器件，集成度高，功能强大，并且可以串接。\nWS2812 将控制电路和 RGB 灯珠集成在一个 5050 封装的元器件中，每个灯珠即为一个独立的像素点，能够实现红（R）、绿（G）、蓝（B）三基色 256 级灰度显示，可组合出 1677 万种颜色，能实现丰富多样的灯光效果。\nWS2812 通过单总线接收控制信号，数据传输采用特定的编码格式。每个灯珠接收到数据后，会提取出属于自己的 24 位数据（分别为 8 位绿色、8 位红色、8 位蓝色），然后将剩余的数据转发给下一个灯珠。控制信号的高低电平持续时间决定了传输的数据是 0 还是 1。\n逻辑 0：高电平持续时间约 0.35μs，低电平持续时间约 0.8μs。 逻辑 1：高电平持续时间约 0.7μs，低电平持续时间约 0.6μs。 该内容为进阶选学内容，下面我将给出丰富注释的示例程序，以供参考。\n主函数内容，主要为创建任务\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include \u0026lt;stdio.h\u0026gt; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026#34;driver/gpio.h\u0026#34; #include \u0026#34;ws2812.h\u0026#34; #include \u0026#34;driver/rmt_tx.h\u0026#34; //彩灯任务 void WS2812_run_task(void *pvParameters){ while (1){ ws2812_coloful(50); } } void app_main(void) { //初始化ws2812 ws2812_init(); //创建任务 xTaskCreatePinnedToCore(WS2812_run_task, \u0026#34;WS2812_run_task\u0026#34;, 2048, NULL, 1, NULL, 0); } .h文件，主要为函数声明和宏定义\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #ifndef __WS2812_H__ #define __WS2812_H__ #include \u0026#34;driver/rmt_tx.h\u0026#34; #include \u0026lt;stdint.h\u0026gt; #include \u0026#34;esp_check.h\u0026#34; #define READ 0xFF0000//红色 #define GREEN 0x00FF00//绿色 #define BLUE 0x0000FF//蓝色 #define YELLOW 0xFFFF00//黄色 #define CYAN 0x00FFFF//紫色 typedef struct { uint32_t resolution; //设置编码器分辨率 } led_strip_encoder_config_t; void ws2812_init(void); void ws2812_one_colour(uint32_t colour); void ws2812_coloful(uint8_t light); esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder); .c文件的变量定义\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 \u0026#34;ws2812.h\u0026#34; #include \u0026#34;esp_check.h\u0026#34; #include \u0026#34;freertos/FreeRTOS.h\u0026#34; #include \u0026#34;freertos/task.h\u0026#34; #include \u0026lt;string.h\u0026gt; #define WS2812_PIN GPIO_NUM_8// 控制WS2812 灯珠的 GPIO 引脚 #define WS2812_NUM 1 // 灯珠的数量 #define WS2812_SPEED 20 // 灯珠颜色变换速度 // 定义 LED 灯带编码器的标签，用于日志记录 static const char *TAG = \u0026#34;led_encoder\u0026#34;; // 定义 LED 灯带的像素数据数组，每个像素由 3 个字节表示 RGB 颜色 static uint8_t led_strip_pixels[WS2812_NUM * 3]; static rmt_channel_handle_t led_chan = NULL;// 定义 RMT 通道句柄，用于控制 RMT 模块 static rmt_encoder_handle_t led_encoder = NULL;// 定义RMT编码器句柄 static rmt_transmit_config_t tx_config = {};// 定义RMT传输参数 // 定义 LED 灯带编码器的配置结构体 typedef struct { rmt_encoder_t base; // 基础编码器结构体，作为当前编码器的基类，包含通用的编码器操作函数指针等 rmt_encoder_t *bytes_encoder; // 指向字节编码器的指针，用于将字节数据编码为 RMT 符号 rmt_encoder_t *copy_encoder; // 指向复制编码器的指针，用于复制特定的 RMT 符号 int state; // 编码器的状态变量，用于记录当前编码过程所处的阶段 rmt_symbol_word_t reset_code; // 重置信号的 RMT 符号，用于在发送数据后发送重置信号 } rmt_led_strip_encoder_t; // 函数声明 static void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b); .c文件的函数\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 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 /// @brief ws2812初始化 /// @param void ws2812_init(void) { // 配置RMT TX通道参数 rmt_tx_channel_config_t tx_chan_config = { .clk_src = RMT_CLK_SRC_DEFAULT, // 选择默认的源时钟 .gpio_num = WS2812_PIN, // 设置GPIO引脚号 .mem_block_symbols = 64, // 增加内存块大小可减少LED闪烁 .resolution_hz = 10000000, // 设置分辨率 .trans_queue_depth = 4, // 设置后台可挂起的事务数量 }; // 创建RMT TX通道 ESP_ERROR_CHECK(rmt_new_tx_channel(\u0026amp;tx_chan_config, \u0026amp;led_chan)); // 配置LED灯带编码器参数 led_strip_encoder_config_t encoder_config = { .resolution = 10000000, // 设置编码器分辨率 }; // 创建LED灯带编码器 ESP_ERROR_CHECK(rmt_new_led_strip_encoder(\u0026amp;encoder_config, \u0026amp;led_encoder)); // 启用RMT TX通道 ESP_ERROR_CHECK(rmt_enable(led_chan)); // 配置RMT传输参数 tx_config.loop_count = 0; // 不进行传输循环 } /// @brief ws2812显示单一颜色 /// @param colour 颜色 void ws2812_one_colour(uint32_t colour){ //制作灯带的像素数据 for (uint8_t i = 0; i \u0026lt; WS2812_NUM; i++) { led_strip_pixels[i * 3 + 0] = (colour \u0026gt;\u0026gt; 16) \u0026amp; 0xFF; // 提取绿色分量 led_strip_pixels[i * 3 + 1] = (colour \u0026gt;\u0026gt; 8) \u0026amp; 0xFF; // 提取红色分量 led_strip_pixels[i * 3 + 2] = colour \u0026amp; 0xFF; // 提取蓝色分量 } // 将RGB值发送到LED灯带 ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, led_strip_pixels, sizeof(led_strip_pixels), \u0026amp;tx_config)); // 等待所有传输完成，并检查是否完成 ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY)); } /** * @brief 彩虹灯 * @param light 亮度 */ void ws2812_coloful(uint8_t light){ static uint32_t red = 0; static uint32_t green = 0; static uint32_t blue = 0; static uint16_t hue = 0; static uint16_t start_rgb = 0; for (int i = 0; i \u0026lt; 3; i++) { for (int j = i; j \u0026lt; WS2812_NUM; j += 3) { hue = j * 360 / WS2812_NUM + start_rgb; led_strip_hsv2rgb(hue, 100, light, \u0026amp;red, \u0026amp;green, \u0026amp;blue); led_strip_pixels[j * 3 + 0] = green; led_strip_pixels[j * 3 + 1] = blue; led_strip_pixels[j * 3 + 2] = red; } ESP_ERROR_CHECK(rmt_transmit(led_chan, led_encoder, led_strip_pixels, sizeof(led_strip_pixels), \u0026amp;tx_config)); ESP_ERROR_CHECK(rmt_tx_wait_all_done(led_chan, portMAX_DELAY)); vTaskDelay(pdMS_TO_TICKS(WS2812_SPEED)); } start_rgb += 5; } /** * @brief 对 WS2812 LED 灯带数据进行编码 * * 该函数负责将输入的 RGB 数据编码为适合 RMT 通道发送的信号，并在数据发送完成后发送重置信号。 * * @param encoder 编码器句柄，指向 rmt_encoder_t 结构体 * @param channel RMT 通道句柄，用于发送编码后的数据 * @param primary_data 指向待编码的 RGB 数据的指针 * @param data_size 待编码的 RGB 数据的大小（字节） * @param ret_state 指向编码状态变量的指针，用于返回编码的最终状态 * @return size_t 编码生成的 RMT 符号数量 */ static size_t rmt_encode_led_strip(rmt_encoder_t *encoder, rmt_channel_handle_t channel, const void *primary_data, size_t data_size, rmt_encode_state_t *ret_state) { // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例 rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); // 获取字节编码器句柄 rmt_encoder_handle_t bytes_encoder = led_encoder-\u0026gt;bytes_encoder; // 获取复制编码器句柄 rmt_encoder_handle_t copy_encoder = led_encoder-\u0026gt;copy_encoder; // 初始化会话编码状态为重置状态 rmt_encode_state_t session_state = RMT_ENCODING_RESET; // 初始化最终编码状态为重置状态 rmt_encode_state_t state = RMT_ENCODING_RESET; // 初始化编码生成的 RMT 符号数量为 0 size_t encoded_symbols = 0; // 根据编码器的当前状态进行不同的编码操作 switch (led_encoder-\u0026gt;state) { case 0: //// 发送 RGB 数据 // 调用字节编码器对 RGB 数据进行编码，并累加生成的 RMT 符号数量 encoded_symbols += bytes_encoder-\u0026gt;encode(bytes_encoder, channel, primary_data, data_size, \u0026amp;session_state); // 检查当前编码会话是否完成 if (session_state \u0026amp; RMT_ENCODING_COMPLETE) { // 若完成，切换到下一个状态（发送重置信号） led_encoder-\u0026gt;state = 1; } // 检查 RMT 内存是否已满 if (session_state \u0026amp; RMT_ENCODING_MEM_FULL) { // 若已满，标记最终状态为内存已满 state |= RMT_ENCODING_MEM_FULL; // 跳转到退出标签，结束本次编码操作 goto out; } case 1: // 发送重置信号 // 调用复制编码器发送重置信号，并累加生成的 RMT 符号数量 encoded_symbols += copy_encoder-\u0026gt;encode(copy_encoder, channel, \u0026amp;led_encoder-\u0026gt;reset_code, sizeof(led_encoder-\u0026gt;reset_code), \u0026amp;session_state); // 检查重置信号是否发送完成 if (session_state \u0026amp; RMT_ENCODING_COMPLETE) { // 标记最终状态为编码完成 led_encoder-\u0026gt;state = RMT_ENCODING_RESET; // 标记最终状态为编码完成 state |= RMT_ENCODING_COMPLETE; } // 检查 RMT 内存是否已满 if (session_state \u0026amp; RMT_ENCODING_MEM_FULL) { // 若已满，标记最终状态为内存已满 state |= RMT_ENCODING_MEM_FULL; // 跳转到退出标签，结束本次编码操作 goto out; } } out: // 将最终编码状态返回给调用者 *ret_state = state; // 返回编码生成的 RMT 符号数量 return encoded_symbols; } /** * @brief 删除 WS2812 LED 灯带编码器并释放相关资源 * * 该函数用于删除指定的 WS2812 LED 灯带编码器，会依次删除其内部的字节编码器和复制编码器， * 最后释放 LED 灯带编码器本身占用的内存。 * * @param encoder 指向基础编码器结构体的指针，用于获取实际的 LED 灯带编码器实例 * @return esp_err_t 操作结果，成功时返回 ESP_OK */ static esp_err_t rmt_del_led_strip_encoder(rmt_encoder_t *encoder) { // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例 rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); rmt_del_encoder(led_encoder-\u0026gt;bytes_encoder);// 删除字节编码器 rmt_del_encoder(led_encoder-\u0026gt;copy_encoder);// 删除复制编码器 free(led_encoder);// 释放 LED 灯带编码器本身占用的内存 return ESP_OK; } /** * @brief 重置 WS2812 LED 灯带编码器的状态 * * 该函数用于将指定的 WS2812 LED 灯带编码器重置到初始状态， * 会依次重置内部的字节编码器和复制编码器，并将编码器的状态变量置为初始值。 * * @param encoder 指向基础编码器结构体的指针，用于获取实际的 LED 灯带编码器实例 * @return esp_err_t 操作结果，成功时返回 ESP_OK */ static esp_err_t rmt_led_strip_encoder_reset(rmt_encoder_t *encoder) { // 通过基类指针获取 rmt_led_strip_encoder_t 结构体实例 rmt_led_strip_encoder_t *led_encoder = __containerof(encoder, rmt_led_strip_encoder_t, base); // 重置字节编码器，将其状态恢复到初始状态 rmt_encoder_reset(led_encoder-\u0026gt;bytes_encoder); // 重置复制编码器，将其状态恢复到初始状态 rmt_encoder_reset(led_encoder-\u0026gt;copy_encoder); // 将 LED 灯带编码器的状态变量设置为初始状态 led_encoder-\u0026gt;state = RMT_ENCODING_RESET; return ESP_OK; } /** * @brief 创建一个新的 WS2812 LED 灯带编码器。 * * 此函数用于分配并初始化一个新的 LED 灯带编码器，该编码器可用于将 RGB 数据编码为适合 WS2812 LED 灯带的信号。 * * @param config 指向 LED 灯带编码器配置结构体的指针，包含编码器的配置参数。 * @param ret_encoder 指向编码器句柄指针的指针，用于返回新创建的编码器句柄。 * @return esp_err_t 操作结果，ESP_OK 表示成功，其他错误码表示失败。 */ esp_err_t rmt_new_led_strip_encoder(const led_strip_encoder_config_t *config, rmt_encoder_handle_t *ret_encoder) { // 初始化返回值为成功状态 esp_err_t ret = ESP_OK; // 定义一个指向 LED 灯带编码器结构体的指针 rmt_led_strip_encoder_t *led_encoder = NULL; // 检查输入参数是否有效，如果 config 或 ret_encoder 为 NULL，则跳转到错误处理标签 err ESP_GOTO_ON_FALSE(config \u0026amp;\u0026amp; ret_encoder, ESP_ERR_INVALID_ARG, err, TAG, \u0026#34;invalid argument\u0026#34;); // 为 LED 灯带编码器分配内存 led_encoder = rmt_alloc_encoder_mem(sizeof(rmt_led_strip_encoder_t)); // 检查是否分配成功，如果失败，则跳转到错误处理标签 err ESP_GOTO_ON_FALSE(led_encoder, ESP_ERR_NO_MEM, err, TAG, \u0026#34;no mem for led strip encoder\u0026#34;); // 初始化 LED 灯带编码器结构体的成员变量 // 设置编码器的编码函数 led_encoder-\u0026gt;base.encode = rmt_encode_led_strip; // 设置编码器的删除函数 led_encoder-\u0026gt;base.del = rmt_del_led_strip_encoder; // 设置编码器的重置函数 led_encoder-\u0026gt;base.reset = rmt_led_strip_encoder_reset; // 不同的 LED 灯带可能有不同的时序要求，以下参数是针对 WS2812 的 rmt_bytes_encoder_config_t bytes_encoder_config = { .bit0 = {//编码0的时序 .level0 = 1, .duration0 = 0.3 * config-\u0026gt;resolution / 1000000, // T0H=0.3us .level1 = 0, .duration1 = 0.9 * config-\u0026gt;resolution / 1000000, // T0L=0.9us }, .bit1 = {//编码1的时序 .level0 = 1, .duration0 = 0.9 * config-\u0026gt;resolution / 1000000, // T1H=0.9us .level1 = 0, .duration1 = 0.3 * config-\u0026gt;resolution / 1000000, // T1L=0.3us }, .flags.msb_first = 1 // WS2812 传输位顺序: G7...G0R7...R0B7...B0 }; // 创建字节编码器，如果失败则跳转到错误处理标签 err ESP_GOTO_ON_ERROR(rmt_new_bytes_encoder(\u0026amp;bytes_encoder_config, \u0026amp;led_encoder-\u0026gt;bytes_encoder), err, TAG, \u0026#34;create bytes encoder failed\u0026#34;); // 初始化复制编码器配置 rmt_copy_encoder_config_t copy_encoder_config = {}; // 创建复制编码器，如果失败则跳转到错误处理标签 err ESP_GOTO_ON_ERROR(rmt_new_copy_encoder(\u0026amp;copy_encoder_config, \u0026amp;led_encoder-\u0026gt;copy_encoder), err, TAG, \u0026#34;create copy encoder failed\u0026#34;); // 计算重置信号的时钟周期数，默认重置信号持续时间为 50us uint32_t reset_ticks = config-\u0026gt;resolution / 1000000 * 50 / 2; // 设置重置信号的符号字 led_encoder-\u0026gt;reset_code = (rmt_symbol_word_t) { .level0 = 0, .duration0 = reset_ticks, .level1 = 0, .duration1 = reset_ticks, }; // 将新创建的编码器句柄返回给调用者 *ret_encoder = \u0026amp;led_encoder-\u0026gt;base; return ESP_OK; // 错误处理标签 err: if (led_encoder) { // 如果字节编码器存在，则删除字节编码器 if (led_encoder-\u0026gt;bytes_encoder) { rmt_del_encoder(led_encoder-\u0026gt;bytes_encoder); } // 如果复制编码器存在，则删除复制编码器 if (led_encoder-\u0026gt;copy_encoder) { rmt_del_encoder(led_encoder-\u0026gt;copy_encoder); } free(led_encoder); } return ret; } /** * @brief 将 HSV（色相、饱和度、明度）颜色空间转换为 RGB（红、绿、蓝）颜色空间。 * * 该函数接收 HSV 颜色模型的三个参数，通过一系列计算将其转换为 RGB 颜色模型的三个参数。 * HSV 颜色模型更符合人类对颜色的感知，而 RGB 颜色模型常用于数字显示设备。 * * @param h 色相，取值范围为 0 到 360，表示颜色的种类，例如 0 代表红色，120 代表绿色，240 代表蓝色。 * @param s 饱和度，取值范围为 0 到 100，表示颜色的纯度，值越大颜色越鲜艳。 * @param v 明度，取值范围为 0 到 100，表示颜色的明亮程度，值越大颜色越亮。 * @param r 指向存储转换后红色分量的指针，取值范围为 0 到 255。 * @param g 指向存储转换后绿色分量的指针，取值范围为 0 到 255。 * @param b 指向存储转换后蓝色分量的指针，取值范围为 0 到 255。 */ static void led_strip_hsv2rgb(uint32_t h, uint32_t s, uint32_t v, uint32_t *r, uint32_t *g, uint32_t *b) { h %= 360; // 将色相值限制在 0 到 360 的范围内 // 计算 RGB 颜色的最大值，将明度值从 0-100 转换为 0-255 范围 uint32_t rgb_max = v * 2.55f; // 计算 RGB 颜色的最小值，根据饱和度调整颜色纯度 uint32_t rgb_min = rgb_max * (100 - s) / 100.0f; // 计算色相所在的 6 个区域中的哪一个，每个区域 60 度 uint32_t i = h / 60; // 计算当前色相在所在区域内的偏移量 uint32_t diff = h % 60; // 根据色相偏移量计算 RGB 颜色的调整值 uint32_t rgb_adj = (rgb_max - rgb_min) * diff / 60; // 根据色相所在的区域，计算对应的 RGB 颜色分量 switch (i) { case 0: // 色相在 0 到 60 度之间，红色为主 *r = rgb_max; *g = rgb_min + rgb_adj; *b = rgb_min; break; case 1: // 色相在 60 到 120 度之间，绿色和红色混合 *r = rgb_max - rgb_adj; *g = rgb_max; *b = rgb_min; break; case 2: // 色相在 120 到 180 度之间，绿色为主 *r = rgb_min; *g = rgb_max; *b = rgb_min + rgb_adj; break; case 3: // 色相在 180 到 240 度之间，蓝色和绿色混合 *r = rgb_min; *g = rgb_max - rgb_adj; *b = rgb_max; break; case 4: // 色相在 240 到 300 度之间，蓝色为主 *r = rgb_min + rgb_adj; *g = rgb_min; *b = rgb_max; break; default: // 色相在 300 到 360 度之间，红色和蓝色混合 *r = rgb_max; *g = rgb_min; *b = rgb_max - rgb_adj; break; } } ","date":"May 27, 2025","externalUrl":null,"permalink":"/posts/esp32%E5%AD%A6%E4%B9%A0%E8%AE%B0%E5%BD%9501/","section":"Posts","summary":"freeRTOS # esp-idf是基于freeRTOS的框架，所以是首先要掌握的内容\nfreeRTOS任务概述 # 在低端设备中，程序基本分为裸机和RTOS，针对简单的程序，我们用裸机程序完全可以满足，一旦功能复杂，程序模块众多，裸机程序往往很难满足我们的需求。因此我们就要用到RTOS系统。\n","title":"ESP32学习记录01","type":"posts"},{"content":"","date":"May 27, 2025","externalUrl":null,"permalink":"/tags/%E7%AC%94%E8%AE%B0/","section":"Tags","summary":"","title":"笔记","type":"tags"},{"content":" Python基础 # 注释 # 注释是对代码的解释说明。\n单行注释（行注释）\nPython 中 # 后的一行内的内容会被视为注释\n1 2 # print(\u0026#34;hello world\u0026#34;) print(\u0026#34;hello world\u0026#34;) # 打印hello world 多行注释（块注释） Python 中使用三个引号开始，三个引号结束（单引号或者双引号都可以），为多行注释多行注释在说明文字需要换行时使用，不能嵌套\n1 2 3 4 \u0026#34;\u0026#34;\u0026#34; print(\u0026#34;hello world\u0026#34;) print(\u0026#34;hello world\u0026#34;) \u0026#34;\u0026#34;\u0026#34; 变量 # 变量是指在程序执行过程中，其值可以改变的量。在内存的数据区中，会为变量分配存储空间来存放变量的值，这个内存空间的地址对应着变量名称，所以在程序中可以通过变量名称来区分和使用这些内存空间。它的唯一目的是在内存中标记和存储数据,这些数据可以在整个程序中使用。可以将变量理解为一个可以赋给值的标签，也可以说变量指向特定的值。\n变量的创建 # 变量创建方式：变量名 = 变量值\nPython 中的变量不需要声明。每个变量在使用前都必须赋值，变量赋值以后该变量才会被创建。\n1 2 3 4 5 6 7 var1 = 2 # 定义一个变量，变量名为var1，变量值为2 var2 = 3 # 定义一个变量，变量名为var2，变量值为3 result = var1 + var2 # 定义一个变量，变量名为result，变量值为var1和var2相加的结果 print(result) # 打印 result 变量的值 name = \u0026#34;张三\u0026#34; age = 18 weight = 1000.3 多个变量的创建：\n1 2 var1 = var2 = var3 = 10 # 多个变量的值相同 var4, var5, var6 = 10, 20, 30 # 多个变量的值不同 ","date":"May 19, 2025","externalUrl":null,"permalink":"/posts/python_learn/","section":"Posts","summary":"Python基础 # 注释 # 注释是对代码的解释说明。\n单行注释（行注释）\nPython 中 # 后的一行内的内容会被视为注释\n","title":"python学习笔记","type":"posts"},{"content":" C语言 # 1.从源码到可执行文件会经历怎样的过程？ # 预编译 :处理预处理指令（#define #include #ifdef #if 等），去掉空格注释，生成干净的源代码 编译: 将源代码翻译成汇编代码，生成汇编文件。 汇编: 将汇编代码翻译成机器码，生成目标文件（二进制文件）。 链接: 将目标文件和程序所依赖的库连接成最终的可执行文件。 2.介绍一下C语言程序的内存模型 # 3.什么是标识符？标识符有什么命名规范？ # 变量、函数、数组、结构体等要素命名时使用的字符序列，称为标识符。 由小写或大写英文字母，0-9 或 _ 组成。 不能以数字开头。 不可以是关键字。 下划线拼接 驼峰命名 4.32位系统，地址用多少个字节表示? 64位系统，地址用多少字节表示？ # 32位系统，地址用4个字节表示。 64位系统，地址用8个字节表示。 5.C语言变量的声明和定义有什么区别？ # 声明是告诉编译器某个变量的类型和名称，但不指定其值 定义是告诉编译器某个变量的类型和名称，并指定其值 6.变量和常量有什么区别？ # 变量的值在程序运行期间可以修改，常量的值不能修改 7.C 语言中有哪些定义常量的方式？有什么区别？ # const: C99新增，定义时需指定数据类型，编译时会类型检查，有作用域，更安全。 #define : 预处理阶段文本替换，没有类型检查，没有作用域，更灵活。 8.全局变量和局部变量有什么区别？ # 作用域: 全局变量-整个程序 局部变量-声明该变量的函数或者代码块内 生命周期: 全局变量-整个程序运行期间 局部变量-函数或代码块的执行期间 存储位置: 全局变量-全局变量存储在数据区 局部变量-局部变量存储在栈区 默认初始值: 全局变量-有默认值，为0 局部变量-没无默认值，随机 9.假设有变量num，表达式num ++和++num有什么不同？ # 先取值再运算 先运算再取值 10.请说几个你经常用到的预处理命令？ # #include #define、#undef、#ifdef、#ifndef #if、#else、#elif、#endif 11.如何取消宏定义？ # #undef 12.什么是条件编译，有哪些应用场景？ # 条件编译（Conditional Compilation）是指在程序编译过程中，根据某些条件的不同，决定是否编译某段代码的技术。通常，条件编译通过预处理指令实现，最常见的指令是 #if、#ifdef、#ifndef、#else 和 #endif。 应用场景：跨平台 调试代码 功能开关 13.说说你知道的C语言数据类型？ # 整数 char、short、int、long、long long 浮点数 float、double、long double 14.整数和浮点数在存储原理上有什么区别？ # 整数: 以二进制补码的形式存储 浮点数:C语言中的浮点数存储遵循IEEE 754标准，采用科学计数法表示。浮点数由三部分组成：符号位：占1位，表示浮点数的正负。 指数部分：占8位（float）或者11位（double），表示数值的规模，采用偏移量表示（通常为127或1023）。 尾数部分：占23位（float）或者52位（double），表示有效数字。 15.char类型的本质 # 长度为1个字节（8位）的整数。实际存储的是字符对应的ascii码编码值。 16.数据类型的自动转换遵循什么原则？ # 整数类型之间: 窄字节类型转为宽字节类型，有符号类型转为无符号类型 浮点数之间: 精度小的类型转为精度大的类型 整数和浮点数之间: 整数转为浮点数 17.枚举有哪些应用场景？使用枚举类型的好处是什么？ # 应用场景：状态机中的各个状态可用枚举定义，通用的方法返回值（TRUE，FALSE，SUCCESS、ERROR等） 好处：提高代码的可读性 增强类型安全 18.typedef 和 #define 定义的类型别名有什么不同？ # typedef：用于创建类型别名，它具有作用域，具有类型安全检查。 #define：只是在预处理阶段进行文本替换，不具有作用域，不具有类型安全检查。 19.switch的参数数据类型有什么要求？ # 必须是整型或者枚举类型 20.break和continue有什么区别？ # break：break可以结束掉循环，并可以跳出switch中的case。 continue：continue以跳出本次循环，循环并未结束继续下一次。 21.C语言数组有什么特点，如何计算数组长度 # 特点：数组中的元素在内存中是依次紧密排列的且有序的。初始化完成长度就是确定的，不能修改。可直接通过索引（下标）获取指定位置的元素，速度很快。 长度计算：sizeof(arr) / sizeof(int) 22.C 语言中字符串的本质是什么 # 字符数组，需要特别注意的是所有的字符串都以\u0026rsquo;\\0\u0026rsquo;（ASCII值为0）结尾 23.现有字符串 char str[]= \u0026ldquo;hello world\u0026rdquo;，使用sizeof() 计算的字符串长度和strlen()计算的字符串长度有什么区别 # sizeof：返回的是字符数组（包括结尾的空字符\\0）的长度 strlen：返回的是实际的字符个数，不包括结尾的空字符\\0 24.什么是多维数组，内存的存储形式是怎样的 # 数组的元素也是数组就是多维数组。 存储形式\n25.两个同类型指针相减会得到什么结果 # 同类型的指针相减会得到两个指针之间元素的个数，而不是它们的地址差。 26.指针加减整数会得到什么结果 # 指针与整数的加减运算，表示指针所指向的内存地址的移动，指针每次+1（或-1）后移动多少，取决于指针指向的数据类型 27.指针能否比较大小 # 可以，比较的各自指向的内存地址的大小 28.数组名是指针吗，和指针有什么不同？ # 数组名本质：数组实体的标识符，但在有些场景下，会被隐式的转为指向第一个元素的指针，数组本身 sizeof，\u0026amp;取地址，指向第一个元素的指针 其余情况 数组名和指向第一个元素的指针的区别 29.指针数组和数组指针有什么区别？ # 指针数组：是一个数组，其中每个元素都是指针。char *strs[2] = {\u0026ldquo;hello\u0026rdquo;, \u0026ldquo;world\u0026rdquo;}; 数组指针: 数组指针是一个指针，它指向一个数组。int arr[5] = {1, 2, 3, 4, 5};int (*p_arr)[5] = \u0026amp;arr; 30.指针函数和函数指针有什么不同 # 指针函数：是一个函数，其返回值为指针类型 int* fun(intx,int y); 函数指针：值一个指针，其指向的是一个函数 int*(*p_fun)(int,int); 31.常量指针和指针常量有什么不同？ # 常量指针（pointer to const）：是一个指针，其指向一个常量，其指向的值不可修改const int* ptr; 指针常量（const pointer）：只一个常量，其类型是指针，该指针的值本身不可修改 int* const ptr; 32.用字符数组和字符指针表示字符串有什么不同？ # 33.什么是空指针 # 空指针是一个不指向任何有效内存地址的指针，在C 语言中用NULL 表示。 34.什么是悬空指针 # 悬空指针（Dangling Pointer）是指一个指针变量指向的内存地址已经被释放或者不再有效，但该指针仍然持有该地址的值，悬空指针会导致程序访问无效内存。 35.什么是野指针，如何避免野指针 # 野指针就是指针指向的位置是不可知 未初始化的指针 悬空指针 越界访问的指针 36.指针和引用的区别和用法 # 引用是C++中的概念，C语言中没有。引用相当于是对已有的变量取别名。 37.使用结构体变量访问成员和使用结构体指针访问成员有什么不同 # 结构体变量： . 运算符 结构体指针： -\u0026gt; 运算符 38.结构体的长度如何计算 # 39.结构体和共用体有什么不同 # 两者最大的区别在于内存的使用 结构体各成员拥有自己的内存，各自使用且互不干涉，遵循内存对齐原则。 联合体所有成员共用一块内存空间，并且同时只有一个成员可以得到这块内存的使用权。一个联合体变量的总长度应至少能容纳最大的成员变量 40.形参和实参有什么区别 # 形参：形参是函数定义中声明的参数，用于接收从函数调用中传递的值。形参本质上是局部变量 实参：实参是函数调用时传递给函数的具体值或变量。 41.C 语言主函数的参数和返回值有什么规则 # 42.什么是函数原型 # 就是函数声明。包括函数名称、返回类型和参数列表，但不包括函数体 43.说出几个你常用的 C 语言系统函数 # printf strlen、strcat、strcmp malloc 、free、memcpy、memset abs、sin、cos、tan 44.传递指针给函数和传递值给函数有什么区别 # 传递值：实参的值被复制到函数的形参中，函数内部对形参的修改不会影响到实参 用于基本类型参数 传递指针：实参的地址被传递给函数，函数可以通过该指针访问和修改实参指向的数据，对指针指向的数据的修改在函数内外都是可见的 用于数组、结构体等较大的数据，以避免复制整个数据块 45.什么是回调函数 # 定义：举例说明：调用一个函数A时，将一个自定义的函数B作为参数传入，这个自定义函数B会在某些特定时机被调用，函数B就是一个回调函数。 意义：解耦 46.带参宏定义和函数有什么区别 # 47.printf()、sprintf()、fprintf() 三个函数有什么不同 # printf() 函数用于将格式化的数据输出到标准输出设备（通常是控制台）。 sprintf() 函数用于将格式化的数据输出到一个字符串中，而不是标准输出设备。 fprintf() 函数用于将格式化的数据输出到指定的文件流中。 48.static 关键字有什么作用 # 声明静态局部变量：在函数调用结束后不会销毁，生命周期延长至整个程序的执行期间，会存储在内存的全局静态区。 声明静态全局变量：它使得变量的作用域仅限于声明它的源文件，不可以被其他源文件访问。 声明静态函数：它使得函数的作用域仅限于声明它的源文件，不可以被其他源文件调用。 49.extern关键字有什么作用 # extern 关键字在 C 语言中用于声明一个变量或函数在其他文件中定义，它告诉编译器该变量或函数在其他地方已经定义过，不需要再次定义。通常，extern 用于跨文件共享全局变量或函数。 50.volatile关键字有什么作用？ # 当你声明一个变量为 volatile 时，编译器会确保CPU每次访问该变量时都直接从内存中读取或写入，而不是使用寄存器缓存的值。以此确保CPU每次都能访问到变量的最新值。 51.在1G内存的计算机中能否malloc（1.2G）？为什么？ # 如果操作系统支持虚拟内存，就可以，否则不可以 52.什么是内存泄漏 # 内存泄漏（Memory Leak）是指程序中已动态分配的堆内存由于某种原因未释放或无法释放，造成系统内存的浪费，导致程序运行速度减慢甚至系统崩溃等严重后果。 53.什么是内存溢出 # 定义：内存溢出（Out Of Memory）是指应用系统中存在无法回收的内存或使用的内存过多，最终使得程序运行要用到的内存大于系统能提供的最大内存。此时程序无法运行，系统提示内存溢出。 出现原因：内存泄漏的堆积最终导致内存溢出。需要保存多个耗用内存过大的对象或加载单个超大的对象时，其大小超过了当前剩余的可用内存空间。 54.堆和栈有什么区别 # 管理方式：栈内存由编译器自动分配和释放，堆内存需要程序员手动管理 效率：栈的访问效率要高于堆。栈的特点是后进先出，因此内存的分配和释放非常简单；而堆是动态分配的，因此多次分配和释放后，会导致空闲内存不连续，这样就会增加内存分配的复杂度。 方向： 55.堆栈溢出一般是由什么原因导致的？ # 堆溢出：内存泄漏 栈溢出：函数调用层级过深/递归层次过深 分配了过大的局部变量 56.C语言如何进行动态内存分配，使用哪些函数，有哪些注意事项 # 函数：malloc()，calloc()，realloc()和free() 注意事项：避免分配大量的小内存块。分配堆上的内存有一些系统开销，所以分配许多小的内存块比分配几个大内存块的系统开销大。仅在需要时分配内存。只要使用完堆上的内存块，就需要及时释放它，否则可能出现内存泄漏。总是确保释放已分配的内存。在编写分配内存的代码时，就要确定好在代码的什么地方释放内存。 57.C语言的库使用过哪些？ # stdio、math、stdlib、string STM32 # 1.常见的指令集架构？ # CISC（Complex Instruction Set Computing，复杂指令集）X86 Intel开发 RISC（Reduced Instruction Set Computing，精简指令集） ARM RISC-V 2.CPU内部结构? # 算术逻辑单元（Arithmetic logic unit，ALU） 执行算术和逻辑运算 控制单元（Control Unit，CU） 从内存中获取指令，并根据指令类型调度ALU、寄存器、内存等组件的工作 寄存器 用于存储CPU在处理数据时需要的临时数据 程序计数器（PC）指向下一条待执行的指令 指令寄存器（IR）存储当前正在执行的指令 通用寄存器用于临时存储数据 时钟 提供同步信号，确保CPU内部各个组件按时工作。 3.CPU和内存、虚拟内存、硬盘的关系? # 硬盘 用于持久化存储程序和数据。 内存 用于临时存储程序和数据，硬盘上的系统、程序和数据，加载到内存中，才能被CPU读写。 虚拟内存 虚拟内存是操作系统的一种的内存管理机制，它为每个进程提供一个独立的虚拟内存地址空间，然后通过MMU将虚拟地址映射到物理地址。 4.ARM结构处理器可分为哪几类？ # Cortex-A系列 主打高性能 手机，平板，智能电视等 Cortex-R系列 主打实时 汽车，工业控制，医疗设备等 Cortex-M系列 主打嵌入式 传感器、智能家居等 5.嵌入式系统中ROM、RAM、Register的概念和作用是什么？ # ROM 非易失性存储器 持久化存储程序和数据 RAM 易失性存储器 临时存储程序和数据，支持快速读写 Register 寄存器 CPU 内部的超快速存储单元，直接参与运算和执行，存储中间计算结果 6.SRAM、DRAM、SDRAM的区别？ # SRAM 静态随机存取内存(Static Random Access Memory) 使用锁存器保存数据。所谓的“静态”，是指这种存储器只要保持通电，里面储存的数据就可以恒常保持。 DRAM 动态随机存取内存(Dynamic Random Access Memory) 利用电容内存储电荷的多少来代表1或者0。由于电容存储的电荷会有所流失，因此为保持数据，DRAM需要进行周期性的刷新。 SDRAM 同步动态随机存取内存(Synchronous Dynamic Random Access Memory) SDRAM的工作原理基于DRAM，但是它与CPU的时钟信号同步，允许更快速的数据传输 7.什么叫字节序?都有哪些字节序? # 多字节数据在内存中的存储顺序，包含大端序和小端序，ARM芯片默认为小端序 8.C语言代码判断大小端？ # 9.STM32最小系统？ # STM32单片机能工作的最小外围电路就叫最小系统，最小系统通常包括：STM32芯片、电源、时钟、下载调试和复位5部分组成。 10.解释下什么叫嵌入式系统 # 目前流行的嵌入式系统定义是：嵌入式系统是以应用为中心，以计算机技术为基础，并且软硬件可裁剪，适用于应用系统对功能、可靠性、成本、体积、功耗有严格要求的专用计算机系统。由嵌入式微处理器、外围硬件设备、嵌入式操作系统以及用户的应用程序等四个部分组成。 11.说说你常用的STM32的外设有哪些？ # 12.什么是中断，解释一下中断向量表和中断嵌套 # 中断：中断是单片机处理紧急事件的一种机制，当事件发生时，CPU会暂停当前任务，转而去处理紧急事件，处理完，在恢复之前的任务。 中断向量表：中断向量表指的就是各中断源的中断服务程序（ISR）的地址集合，当中断发生时，CPU会根据中断类型查找相应的ISR的地址，来执行对应的中断服务程序。 中断嵌套：高优先级中断可以打断正在执行的低优先级中断 13.STM32如何进入中断? / 简述处理器中断处理的过程 # 14.简述一下STM32中断有哪些？ # 外部中断、定时器中断、DMA中断、UART中断、SPI中断、I2C中断、USB中断、ADC中断 15.STM32中断优先级如何分组？ # 通过4个bit进行分组，4个bit可分为两部分，一部分用于设置抢占优先级，一部分用于设置响应优先级 16.相比于正常函数，中断服务函数有什么特点和需要注意的地方？ # 没有参数和返回值。 需要配置相应的寄存器来开启中断。 触发条件满足时，硬件自动调用。 内部不能递归调用自己。 尽量不要执行耗时操作。 17.如果中断服务函数执行时间过长，你该如何优化? # 中断推迟处理\n裸机开发 使用标志位 FreeRTOS 使用信号量、消息队列、任务通知等手段，实现ISR和Task之间的同步 18.介绍一下STM32 GPIO # 功能：GPIO是通用输入输出，STM32的一种重要的外设，可以用于控制数字输入输出。 工作模式：输入模式 浮空输入、带上拉输入、带下拉输入、模拟输入 输出模式 开漏输出、推挽输出、开漏复用输出、推挽复用输出 常见用途：普通GPIO 输入模式下，可通过输入电平来读取外部传感器、按键等信号；输出模式下，可以控制外部电路元件的开关状态。 引脚复用 可以通过选择不同的复用功能，将GPIO引脚作为其他外设的输入输出端口，如ADC、TIM、USART等。中断模式 可以配置GPIO的中断模式来实现外部中断、事件捕捉等功能。 19.STM32 IO口开漏输出和推挽输出有什么区别？ # 推挽输出：既能输出低电平也能输出高电平，有比较强的电流驱动能力 开漏输出：只能输出低电平，不能输出高电平。如果要输出高电平，需要外部接入上拉电阻。 20.谈一下I2C总线 # 基本概念：是一种半双工、同步、串行通信协议 传输速度：标准模式 100kbps 快速模式 400kbps 高速模式 3.4Mbps 信号线：有两根信号线，分别是SDA和SCL，SDA负责数据传输，SCL负责数据同步 主从模式：支持主从模式，一个主设备可连接多个从设备，主设备通常是单片机，从设备是各种传感器。主设备负责发起请求，从设备负责响应 设备地址 每个从设备都有一个地址，每次通信，主设备都要指定从设备地址 传输协议 每次通信，都要以一个起始信号开始，以一个终止信号结束，中间的数据以字节为单位进行发送，没发送一个字节，都要接收一个确认信号。 协议细节 21.为什么I2C中要用开漏输出？ # 避免多设备冲突 I2C总线的核心特点之一是允许多个设备共享同一条总线（SDA和SCL）。采用开漏输出设计可以确保不同设备之间不会产生冲突。如果采用推挽输出，当一个设备输出高电平，另一个设备输出低电平，就可能造成短路。 实现多主仲裁 I2C支持多个主设备，但同一时刻只能有一个主设备控制总线。开漏输出保证总线空闲时处于高电平，便于主设备之间进行仲裁。 22.IIC驱动怎么验证？ # 读写从设备寄存器 读取从设备ID 逻辑分析仪 23.IIC通讯接一个多大的电阻，会影响速率吗 # 大小 一般1-10k，通常4.7k 速率 100kHz 4.7kΩ至10kΩ 400kHz及以上 1kΩ至4.7kΩ 24.谈一下SPI总线 # 工作模式 根据数据的采样时机，共分为四种模式 25.模拟SPI和硬件SPI的区别是什么？ # 硬件 SPI是芯片的外设功能，而模拟的SPI是用GPIO实现的。 硬件SPI的CPU占用少于模拟SPI。 模拟SPI的移植性高于硬件SPI。 26.uart如何保证数据传输的正确性？ # 在数据位的两端添加了起始位、奇偶校验位、停止位等用于数据的同步和校验 27.在串口通讯中，如何接收不定长数据? # 固定格式：收发双方约定好数据开始和结束格式, 比如数据用AB开头，用BA就结束。一旦收到AB表示新数据要来了，一旦收到BA表示数据传输结束。 接收中断+超时判断：通常来讲，两帧数据之间，会有个时间间隔。因此，我们可以使用一个计时器，如果在一个固定的时间点里没接收到新的字符，则认为一帧数据接收完成了。 空闲中断：当串口一段时间没有接收到数据,则会触发空闲中断，可以认为一帧数据已经传输完毕。空闲中断并不是所有的 MCU 都具备，一般高端一点的 MCU 才有，低端一些的 MCU 并没有空闲中断。 28.介绍一下RS232、RS485？ # 29.如何用一个串口去操控6个温湿度传感器 # 使用RS485多点通信 使用串口多路复用器（如CD74HC4067） 30.解释一下异步和同步通信的区别 # 异步通信:通信双方不使用共同的时钟信号，而是通过在数据中添加起始位和停止位来标识数据的开始和结束。接收方根据预先约定的波特率（即每秒传输的比特数）来接收数据。 同步通信:通信双方使用共同的时钟信号进行数据传输，发送方和接收方在时钟的节拍下同步进行。例如，发送方在时钟上升沿发送一位数据，接收方在相同的时钟上升沿接收该数据。 31.请说明总线接口UART、USB、SPI、IIC的异同点 # 32.CAN通信协议的特点（禾望电器） # CAN（Controller Area Network）是一种常用于工业自动化和汽车领域的串行通信总线，允许多个节点同时通信，支持广播和点对点通信模式。 高可靠性：采用差分信号传输，可以抵抗电磁干扰。 高实时性：采用优先级机制，具有快速响应和实时性高的特点。 高速性：传输速率高达1Mbps。 数据帧格式固定：数据帧格式是固定的，包括帧头、数据、校验等字段。 支持多节点：可以同时连接多个设备进行通信，具有较强的扩展性和兼容性。 33.介绍嵌入式系统中常用的通信接口及其适用场景 # UART：适用于简单的点对点通信，例如与外部设备（传感器、显示器等）进行数据交换。 SPI：适用于需要高速数据传输和与多个外部设备通信的场景，例如存储器读写、传感器数据采集等。 I2C：适用于连接多个从设备、数据传输速率不是特别高的场景，例如传感器网络、外设控制等。 CAN：适用于需要长距离传输、高可靠性、抗干扰能力强的场景，例如车辆网络、工业自动化等。 Ethernet（以太网）：适用于需要大数据传输、远程控制、网络通信等场景，例如智能家居、远程监控等。 34.你们用的WIFI芯片是什么?与STM32是如何通讯的?（禾望电器） # 我们用的是ESP32-C3芯片，这个芯片即提供了WIFI功能，也提供了低功耗蓝牙功能。他们与STM32是通过串口进行通信。STM32通过发送AT指令来控制ESP32-C3芯片。 35.无线模块怎么选型？分别适用什么场景？ # 36.请描述下你对定时器的理解? # 工作原理:定时器的核心是一个计数器，可以按照时钟源递增或递减计数，计数频率可通过预分频器进行调整。该计数器可设置一个上限值或者下限值（ARR），计数器到达该值后，可以触发中断。 功能:基于该计数器，定时器可以完成基本的定时功能，以及一系列其他高级功能。周期性任务计数+中断 PWM生成 计数+捕获/比较寄存器（CCR） 输入信号测量 计数+捕获/比较寄存器（CCR） 37.STM32定时器有什么作用和优势？ # 38.pwm怎么实现的？ # 通过ccr（捕获比较寄存器）的值来控制占空比：占空比 (%) = (CCR / (ARR + 1)) * 100% 定时器的计数值小于这个值，输出为高电平； 定时器的计数值大于等于这个值，输出为低电平 39.DMA有什么作用? # 可以在外设和内存之间直接交换数据，而无需CPU干预，从而减轻CPU的负担。 40.ADC采集的电压做什么处理，直接STM32读取吗? # 低于参考电压可直接读取，高于参考电压，可使用串联电阻分压。 41.STM32单片机如何降低功耗? # 不用的外设关闭相应的时钟。 根据实际情况，适度降低时钟频率。 使用低功耗模式：休眠模式、停机模式、待机模式。 42.在嵌入式开发中，你常用的调式手段有哪些? # 调试器（Debugger）：使用仿真器可以在开发板上进行源代码级的调试，包括设置断点、单步执行、观察变量值等。 串口调试：通过串口，输出调试信息、变量值、错误信息等。 printf调试：在代码中使用printf函数输出各种调试信息。 LED指示灯： LED灯来表示程序的执行状态、某些事件的发生或者错误的状态。 逻辑分析仪（Logic Analyzer）：捕获和分析数字信号，查看高低电平方波。 示波器（Oscilloscope）：观察和分析模拟信号，例如传感器输出、模拟电路等。 43.单片机上电后没有运转，说说你的解决思路？ # 44.复位后，在main函数执行之前单片机做什么？ # 初始化栈指针（Stack Pointer） 初始化程序计数器（PC），另其指向Reset_Handler 配置中断向量表 初始化系统时钟 调用 __main 函数进行 C 运行时环境的初始化 执行main()函数 45.看门狗的作用，底层是怎么执行的？ # 作用:监控系统是否失常，防止死循环或程序崩溃，提高系统可靠性 底层原理:看门狗通常是一个硬件计数器，用户可配置计数频率以及溢出值。当计数器到达溢出值就会触发复位。用户需定期重置看门狗计数器（喂狗），如果程序运行正常，则看门狗就不会复位，如果程序运行异常，耽误了喂狗，就会触发复位操作。 FreeRTOS # 1.简述一下什么是RTOS系统，与普通操作系统有什么区别. # 定义:RTOS为实时操作系统，用于处理实时任务，其目标是取保系统能够以最快的速度对外部事件做出响应。 与普通操作系统的区别 2.RTOS和FreeRTOS有什么关系? # RTOS是一种通用的术语，用于指代各种实时操作系统。 FreeRTOS是其中一种具体的实时操作系统，是RTOS中的一种。 3.FreeRTOS的主要优点是什么 # 免费开源，在商业和个人项目中自由使用和修改。 轻量级设计，不占用过多内存和处理器资源。 可移植性强，支持多种硬件平台和处理器架构。 应用广泛，使用多、可参考资料多。 功能丰富易用：多任务调度、通信、同步等功能，API简单好上手。 支持低功耗：有效降低系统的功耗，延长电池寿命。 4.什么是RTOS的裁剪？为什么要进行裁剪？ # 定义:RTOS的裁剪是指根据项目的需求和目标，选择性地包含或排除RTOS的功能模块。提供了用户级配置文件FreeRTOSConfig.h，可以设置一些功能开关、函数使能。 意义:目的是满足功能需求的同时，尽可能减小系统的资源占用 减小内存占用。降低系统开销。简化配置和维护。 4.FreeRTOS都需要配置哪些，需要注意什么？ # 调度策略：抢占式调度、时间片调度。 时钟频率：CPU主频、FreeRTOS系统时钟频率。 定义PendSV、SVC、Systick中断服务函数。 内存设置：heap大小（参考芯片SRAM大小）、空闲任务的栈大小。 中断设置 PendSV、SVC中断优先级（一般最低） 临界区屏蔽中断的范围 其他：需要的功能，比如信号量、队列等。 5.FreeRTOS中的上下文切换是怎么实现的？ # 核心逻辑:保存当前任务的CPU寄存器状态，恢复下一个任务的CPU寄存器状态。 具体操作:FreeRTOS中，每个任务都有一个独立的栈空间，发生上下文切换时，调度器会将当前任务的CPU中的各个寄存器（PC、SP、通用寄存器）的状态保存至该任务的栈空间。然后将下一个任务的CPU寄存器的状态从起栈中恢复到CPU。 6.FreeRTOS的任务调度器是如何工作的？ # FreeRTOS使用基于优先级的抢占式任务调度策略： 抢占式调度：高优先级的任务在任何时刻抢占正在执行的低优先级任务。 时间片轮转：优先级相同的任务，每个任务轮流执行一个时间片。 7.在 FreeRTOS 中，什么是任务堵塞（Task Blocking）？它的作用是什么？请举例说明任务堵塞的情况。 # 定义:阻塞是FreeRTOS定义的四个任务状态之一，当任务需要延时、或者等待信号量、消息队列、任务通知等等时就会进入阻塞状态。 意义:任务进入阻塞状态后，调度器会将其从调度队列移除，因此该任务不会占用CPU资源，从而提高CPU的利用率。 8.FreeRTOS为什么要设计空闲任务？ # 释放已删除任务占用的内存空间 实现低功耗相关逻辑 9.FreeRTOS有多少个优先级？任务优先级和系统优先级是什么关系？ # FreeRTOS的优先级级数可通过宏进行配置，32位硬件最大32. 系统优先级（中断优先级）和任务优先级没有直接关系 10.什么是优先级翻转? 如何处理优先级翻转? # 定义 高优先级任务等待低优先级持有的共享资源而进入非预期的长久阻塞状态的问题。 解决思路:互斥信号量 优先级继承机制 低优先级任务持有高优先级任务需要的共享资源时，优先级被临时提高到高优先级任务的优先级。释放共享资源后，优先级恢复。 11.任务优先级的设置，是怎么考虑的？ # 任务的重要性:例如：传感器数据的读取\u0026gt;指示灯、显示屏 资源分配:例如：优先级分配的不合理，有可能导致低优先级任务无法执行，此时可以考虑提高低优先级任务的优先级 12.什么是临界区？FreeRTOS如何实现临界区？ # 定义:不会被其他任务打断的代码段称为临界区 实现原理:FreeRTOS通过禁用中断来实现临界区，由于任务的上下文切换需要依赖Systick和PendSV中断，因此禁用中断后，系统便无法进行上下文切换了，所以就能保证临界区的代码不被其他任务打断了。 STM32平台:STM32所使用的Cortex M3内核可以使用BASEPRI寄存器屏蔽一个优先级范围内的中断。 13.FreeRTOS的任务与ISR的关系？ # 可利用信号量、队列等实现ISR和任务之间的同步 14.任务间如何进行数据的传输？ # 15.阐述信号量的作用，信号量的类型 # 二值信号量 常用于两个任务之间的同步 互斥信号量 常用于任务互斥 计数型信号量 盘点事件 资源管理 16.在FreeRTOS中，二值信号量和互斥量的区别？ # 互斥信号量是包含优先级继承机制的二值信号量，可以用于缓解优先级翻转问题。 17.在FreeRTOS中，任务通知的运行机制是怎么样的？ # 每个任务的TCB中都有一个通知数组，该数组用于接收通知。每个通知信息，都会占用5个字节，其中一个字节用于挂起通知，其余四个字节用于保存通知值。 任务向任务发送通知，不需要中间对象，直接往对方任务TCB内的任务通知数组对应位置写入通知值。 通知值支持多种模式，包括递增、置位等等，因此使用任务通知机制，也可以实现信号量和事件标志组的功能。 发送通知不支持阻塞。 不能在ISR中接收任务通知。 18.FreeRTOS任务通信与同步常见方式与区别？ # 19.在RTOS中，什么是死锁（Deadlock）？它可能导致的问题是什么？ # 死锁是指系统中的多个任务或进程之间互相等待对方所持有的资源而无法继续执行的情况。 比如：Task1和Task2，都要获取两个资源A和B才继续执行。Task1先获取了A，Task2先获取了B，此时Task1一直等B，Task2一直等A。 20.介绍FreeRTOS中的内存管理机制？ # heap_1:最简单，不允许释放内存。 heap_2:允许释放内存，但不会合并相邻的空闲块，申请内存时，采用best fit算法。 heap_3:简单包装了标准库中的malloc()和free()，以保证线程安全。 heap_4:允许释放内存，并且会合并相邻的空闲块以避免碎片化。申请内存时采用first fit算法。 heap_5:如同 heap_4，能够跨越多个不相邻内存区域的堆。 21.在FreeRTOS中，什么是任务堆栈溢出（Stack Overflow）？它可能导致的问题是什么？该如何解决任务堆栈溢出问题？ # 原因:任务堆栈溢出是指任务的堆栈空间不足，无法容纳任务执行过程中的局部变量、函数调用信息等数据，导致数据覆盖或堆栈破坏。 危害:任务堆栈溢出可能导致任务异常终止或系统崩溃，影响系统的稳定性和可靠性。 解决:开发测试时，开启栈溢出检测：configCHECK_FOR_STACK_OVERFLOW。优化代码逻辑：变量的数量和类型、减少函数调用层次、慎用递归等。增加任务栈大小。 22.如何跟踪任务运行情况？ # FreeRTOS提供了跟踪任务状态和任务时间统计的功能，可以很方便追踪任务运行情况。 任务状态查询：优先级、任务数量、任务堆栈剩余最小值、状态等。 任务时间统计：各个任务占用CPU的时间比例。 23.FreeRTOS的低功耗是怎么实现的？ # 以STM32为例，FreeRTOS的低功耗依赖于硬件的支持（睡眠模式），FreeRTOS会在所有用户任务都处在阻塞或者挂起状态时，也就是只有空闲任务运行时，计算睡眠时长，若满最小时长要求，就会调整Systick频率，另其在睡眠时间结束后发生中断。之后通过wfi指令，令STM32进入睡眠模式。睡眠期间发生任意中断，STM32都会被唤醒（若没发生其他中断，最后也会发生Systick中断）。唤醒后，FreeRTOS会补偿睡眠期间缺失的滴答值。 ","date":"January 17, 2025","externalUrl":null,"permalink":"/posts/%E5%B5%8C%E5%85%A5%E5%BC%8F%E5%9F%BA%E7%A1%80/","section":"Posts","summary":"C语言 # 1.从源码到可执行文件会经历怎样的过程？ # 预编译 :处理预处理指令（#define #include #ifdef #if 等），去掉空格注释，生成干净的源代码 编译: 将源代码翻译成汇编代码，生成汇编文件。 汇编: 将汇编代码翻译成机器码，生成目标文件（二进制文件）。 链接: 将目标文件和程序所依赖的库连接成最终的可执行文件。 2.介绍一下C语言程序的内存模型 # ","title":"嵌入式基础","type":"posts"},{"content":" Linux下的C语言开发 # GCC、glibc和GNU C的关系 # GCC全称GNU Compiler Collection，是GNU项目的一部分，主要是一套编译器工具集，支持多种编程语言，包括C、C++、Objective-C、Fortran、Ada、Go和D等。GCC最初作为GNU操作系统的官方编译器，用于编译GNU/Linux系统和应用程序。它是自由软件，遵循GNU GPL（GNU General Public License）开源协议发布。\nGCC的主要作用是将源代码编译成机器语言，生成可执行文件或库文件。它也提供了一些优化选项，可以在编译过程中优化代码，提高程序运行的效率。\nglibc，全称GNU C Library，是GNU项目的一部分，是C语言标准库的一个实现版本，为C语言提供了标准的API，包括输入输出处理、字符串操作、内存管理等。glibc是Linux系统上最常用的C标准库实现之一，它实现了C标准规定的所有标准库函数以及POSIX（可移植操作系统接口）的扩展。\nglibc对于Linux系统和基于Linux的应用程序至关重要，因为它提供了与操作系统交互的基本接口和运行时环境。应用程序通过调用glibc提供的函数来执行文件操作、内存管理、进程控制等操作。\nGNU C通常指的是GNU项目的C语言编程标准，特别是在GCC中实现的C语言的扩展和特性。GNU C包括ANSI C（现在通常指C89或C90）的所有特性，以及对C99、C11等更现代C标准的支持和一些GNU特有的扩展。\n三者的关系 # GCC使用glibc作为其C语言程序的标准库。当GCC编译C语言程序时，程序中使用的标准库函数（如printf或malloc）是通过glibc提供的。\nGNU C是GCC中实现的C语言的一个版本，包含了对C语言标准的支持以及GNU特有的扩展。这些扩展可以在使用GCC编译程序时通过特定的编译选项启用。\n总的来说，GCC是编译器，负责将源代码转换为可执行的机器代码；glibc是运行时库，提供程序运行所需的标准函数和操作系统服务的接口；而GNU C则定义了GCC支持的C语言的标准和扩展。\n这三者共同构成了GNU/Linux系统下开发和运行C语言程序的基础。\nC语言编译过程 # 预处理命令 # 在C语言编译过程中，预处理是其中的第一个阶段，它的主要目的是处理源代码文件中的预处理指令，将它们转换成编译器可以识别的形式。预处理主要包含宏替换、文件包含、条件编译、注释移除等几种任务。预处理的输出通常是经过预处理后的源代码文件，它会被保存成一个临时文件，并作为编译器的输入。预处理器处理后的文件通常会比原始源文件大，因为它会展开宏和包含其他文件的内容。\n用下面的命令对两个源文件进行预处理：\n1 2 linux_app/1_C/02_compile$ gcc -E hello.c -o hello.i linux_app/1_C/02_compile$ gcc -E main.c -o main.i -E（大写）：Expand（展开）的缩写，指定gcc执行预处理。 .i：intermediate（中间的）的缩写，预处理后的源文件通常以.i作为后缀。 注意：-o不能省略 得到的hello.i和main.i就是预处理之后的文件。\n.i文件内容解读\n与处理后的.i文件包含了经过C预处理器处理的源代码及行控制指令等内容。\n此处对行控制指令做简要介绍。\n.i文件中以#开头的是预处理器插入的行控制指令，用于标示从下一行起的代码来源，格式大致为\n1 \\# 行号 \u0026#34;文件名\u0026#34; 标志 行号和文件名表示从下一行开始的源代码来源于哪个文件的哪一行。\n标志可以是数字1,2,3,4，每个数字的含义如下：\n表示接下来的内容开始于一个新的文件。 表示控制权从被包含的文件返回。这用于当预处理器完成一个包含文件的读取，回到包含它的文件继续处理时。 指示接下来的内容来自系统头文件。 表明接下来的内容应被视为被extern \u0026ldquo;C\u0026quot;包围，这主要用于C++中，以指示C链接约定。extern C是C++中的关键字组合，我们不必关注。 行号为0通常是预处理器的一种特殊标记用法，并不指向源代码中的实际行号。它可能用于初始化或特殊标记，比如标识文件的开始，而不直接对应于源代码中的行。我们只需要知道.i文件是将源码中宏定义处理之后的源文件，其它内容了解即可。\n编译 # 编译阶段，编译器会将经过预处理的源代码文件转换成汇编代码。在这个阶段，编译器会将源代码翻译成机器能够理解的中间代码，包括词法分析、语法分析、语义分析和优化等过程。编译器会检查代码的语法和语义，生成对应的汇编代码。编译阶段是整个编译过程中最复杂和耗时的阶段之一，它对源代码进行了深入的分析和转换，确保了程序的正确性和性能。\n1 2 3 4 5 6 执行下面的命令对刚生成的预处理文件进行编译生成汇编代码： linux_app/1_C/02_compile$ gcc -S hello.i -o hello.s linux_app/1_C/02_compile$ gcc -S main.i -o main.s 也可以对c文件直接进行编译生成汇编代码 linux_app/1_C/02_compile$ gcc -S hello.c -o hello.s linux_app/1_C/02_compile$ gcc -S main.c -o main.s -S（大写）：Source（源代码）的缩写， 用来生成汇编源代码。 .s：Assembly Source（汇编源码）的缩写，通常编译后的汇编文件以.s作为后缀。 汇编 # 汇编阶段是C语言编译过程中的重要阶段，它将汇编代码转换成目标机器的机器语言代码，也就是目标代码。这个阶段由汇编器（Assembler）完成，其主要任务是将汇编指令翻译成目标机器的二进制形式。主要包含以下几个任务：符号解析、指令翻译、地址关联、重定位、代码优化。最终，汇编器会将翻译和处理后的目标代码输出到目标文件中，用于后续的链接和生成可执行程序或共享库文件。\n1 2 3 4 5 执行下面的指令对刚刚生成的汇编文件进行汇编生成机器码： linux_app/1_C/02_compile$ gcc -c main.s -o main.o linux_app/1_C/02_compile$ gcc -c hello.s -o hello.o 也可以直接对C文件进行汇编生成机器码，汇编时-o可以省略，默认就是同名.o文件 linux_app/1_C/02_compile$ gcc -c main.c hello.c -c（小写）：Compile （汇编生成机器代码）的缩写，该参数可以指定gcc将汇编代码翻译为机器码，但不做链接。此外，该参数也可以用于将.c文件直接处理为机器码，同样不做链接。 .o：Object的缩写，通常汇编得到的机器码文件以.o为后缀。 这次生成的文件已经是二进制文件了，我们不能用文本编辑器直接查看该文件。可以用下面的指令查看main.o文件中所有包含数据的节（section）：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 linux_app/1_C/02_compile$ objdump -s main.o main.o： 文件格式 elf64-x86-64-64 Contents of section .text: 0000 f30f1efa 554889e5 b8000000 00e80000 ....UH.......... 0010 0000b800 0000005d c3 .......]. Contents of section .comment: 0000 00474343 3a202855 62756e74 75203131 .GCC: (Ubuntu 11 0010 2e342e30 2d317562 756e7475 317e3232 .4.0-1ubuntu1~22 0020 2e303429 2031312e 342e3000 .04) 11.4.0. Contents of section .note.gnu.property: 0000 04000000 10000000 05000000 474e5500 ............GNU. 0010 020000c0 04000000 03000000 00000000 ................ Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001 .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000 ................ 0020 00000000 19000000 00450e10 8602430d .........E....C. 0030 06500c07 08000000 .P...... 文件大致可以分为五个部分：\n文件格式 最上面的行标明了文件的格式为：elf64-x86-64-64，为x86-64架构设计的64位ELF文件格式。ELF英文全称为Executable and Linkable Format，即可执行链接格式，了解即可。\n2. .text节\n从Contents of section .text开始到下一个Contents之间的属于.text节。这部分包含了程序的机器代码或指令，是程序实际执行的代码。\n①列之间是以空格分隔的，左侧第一列为四位16进制数，用于表示当前行的地址偏移量，上述文件中，.text节第一列第一行为0x0000，表示这一行的地址偏移量是从0开始的，第一列第二行为0x0010，表示这一行的偏移量是从十进制的16开始的。\n②从第二至第五列共4列均为16进制数表示的机器码，一行写满，刚好占用16个字节，因此，第一行的地址从0开始，第二行从16开始。可以看到，main函数源码处理之后得到的机器码共占用了25个字节的空间。\n③第六列即最后一列是机器码的ASCII码表示，和②中的16进制表示相对应。对于ASCII码无法表示的字符，全部用.表示，对于.text节，这部分是无意义的，因为机器码的意义和作用与ASCII码表示无关。\n.comment节 从Contents of section .comment至下一个Contents之间的属于.comment节。这部分包含编译器版本的信息，用于记录编译这个文件的环境。\n这一节及后续两节的列布局与.text相同，即第一列为地址偏移量，最后一列为机器码的ASCII码表示，其余列为机器码的16进制表示，不再赘述。\n本节记录的信息可以从ASCII码表示部分读取：编译器版本为GCC：（Ubuntu 11.4.0-1ubuntu1~22.04）11.4.0。\n.note.gnu.property节\n从Contents of section .note.gnu.property到下一个Contents之间的部分属于.note.gnu.property节。通常包含了一些GNU特定的属性。这部分内容与.text节相同，无法通过ASCII码解读。了解即可。\n.eh_frame节\n从Contents of section .eh_frame到下一个Contents之间的部分属于.eh_frame节。包含了用于异常处理的元数据，如每个函数的堆栈信息，可用于异常处理和调试。同样无法通过ASCII码解读。\n可以执行下面的指令对main.o内容进行反汇编：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 linux_app/1_C/02_compile$ objdump -d main.o main.o： 文件格式 elf64-x86-64-64 Disassembly of section .text: 0000000000000000 \u0026lt;main\u0026gt;: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: b8 00 00 00 00 mov $0x0,%eax d: e8 00 00 00 00 call 12 \u0026lt;main+0x12\u0026gt; 12: b8 00 00 00 00 mov $0x0,%eax 17: 5d pop %rbp 18: c3 ret 反汇编内容保留了objdump -s main.o看到的前两节内容，主要是将.text节的内容反汇编为汇编代码。从000000..（若干0） main：下一行起的内容均为汇编代码。同样地，汇编代码可以按照空格分隔，第一列为地址偏移量，第二列至第六列共5列为汇编指令对应的机器码，从第七列开始的均为汇编指令。\n链接 # 链接阶段，由链接器完成。链接器将各个目标文件以及可能用到的库文件进行链接，生成最终的可执行程序。在这个阶段，链接器会解析目标文件中的符号引用，并将它们与符号定义进行匹配，以解决符号的地址关联问题。链接器还会处理全局变量的定义和声明，解决重定位问题，最终生成可执行文件或共享库文件。\n我们在say_hello()函数中调用了printf()函数，这个函数是在stdio.h中声明的，后者来源于glibc库，printf()的实现在glibc的二进制组件中，通常是在共享库（如libc.so）或静态库（如libc.a）文件中。因此，我们除了要链接main.o、hello.o，还需要和glibc库的文件链接。通常，C语言的链接共有三种方式：静态链接、动态链接和混合链接。三者的区别就在于链接器在链接过程中对程序中库函数调用的解析。\n静态链接 将所有目标文件和所需的库在编译时一并打包进最终的可执行文件。库的代码被复制到最终的可执行文件中，使得可执行文件变得自包含，不需要在运行时查找或加载外部库。 1 linux_app/1_C/02_compile$ gcc -static main.o hello.o -o main -static：该参数指示编译器进行静态链接，而不是默认的动态链接。使用这个参数，GCC会尝试将所有用到的库函数直接链接到最终生成的可执行文件中，包括C标准库（libc）、数学库（libm）和其他任何通过代码引用的外部库。\n动态链接\n库在运行时被加载，可执行文件包含了需要加载的库的路径和符号信息。动态链接的可执行文件比静态链接的小，因为它们共享系统级的库代码。与静态链接不同，库代码不包含在可执行文件中。 1 linux_app/1_C/02_compile$ gcc main.o hello.o -o main2 没有添加-static关键字，gcc默认执行动态链接，即glibc库文件没有包含到可执行文件中。\n我们也可以将自己编写的部分代码处理为动态库。\n执行下面的指令将hello.o编译为动态链接库libhello.so。\n1 linux_app/1_C/02_compile$ gcc -fPIC -shared hello.o -o libhello.so -fPIC：这个选项告诉编译器为“位置无关代码（Position Independent Code）”生成输出。在创建共享库时使用这个选项是非常重要的，因为它允许共享库被加载到内存中的任何位置，而不影响其执行。这是因为位置无关代码使用相对地址而非绝对地址进行数据访问和函数调用，使得库在被不同程序加载时能够灵活地映射到不同的地址空间。\n-shared：这个选项指示GCC生成一个共享库而不是一个可执行文件。共享库可以被多个程序同时使用，节省了内存和磁盘空间。\n-o libhello.so：这部分指定了输出文件的名称。-o选项后面跟着的是输出文件的名字，这里命名为libhello.so。按照惯例，Linux下的共享库名称以lib开头，扩展名为.so（表示共享对象）。\nhello.o：这是命令的输入文件，即之前编译生成的目标文件。在这个例子中，GCC会将hello.o中的代码和数据打包进最终的共享库libhello.so中。\n上述命令的作用是：使用GCC，采用位置无关代码的方式，从hello.o目标文件创建一个名为libhello.so的动态共享库文件。\n编译完成后查看刚刚编译的动态链接库：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 linux_app/1_C/02_compile$ ll 总计 80 drwxrwxr-x 3 atguigu atguigu 4096 3月 27 20:38 ./ drwxr-x--- 23 atguigu atguigu 4096 3月 27 19:12 ../ -rw-rw-r-- 1 atguigu atguigu 91 3月 27 15:17 hello.c -rw-rw-r-- 1 atguigu atguigu 68 3月 27 15:17 hello.h -rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i -rw-rw-r-- 1 atguigu atguigu 1496 3月 27 17:27 hello.o -rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s -rwxrwxr-x 1 atguigu atguigu 15576 3月 27 20:23 libhello.so* -rw-rw-r-- 1 atguigu atguigu 67 3月 27 15:17 main.c -rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i -rw-rw-r-- 1 atguigu atguigu 1360 3月 27 15:18 main.o -rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/ 使用动态链接库编译新的可执行文件：\n1 linux_app/1_C/02_compile$ gcc main.o -L ./ -lhello -o main_d -L ./：指定了库文件搜索路径。-L选项告诉链接器在哪些目录下查找库文件，./表示当前目录。这意味着在链接过程中，链接器将会在当前目录下搜索指定的库文件。\n-lhello：指定了要链接的库。-l选项后面跟库的名称，这里是hello。根据约定，链接器会搜索名为libhello.so（动态库）或libhello.a（静态库）的文件来链接。链接器会根据-L选项指定的路径列表查找这个库。\n当前目录下只有libhello.so而没有libhello.a，因此，这条命令的最终效果是动态链接当前目录下的libhello.so库以及默认的glibc库，生成可执行文件main_d。\n1 2 3 这时如果我们直接执行main_d文件，会收到以下报错： linux_app/1_C/02_compile$ ./main_d ./main_d: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory 这句报错的意思时main_d在执行过程中，没有找到动态链接库文件libhello.so文件，链接失败无法执行。Linux的默认动态链接库文件夹是/lib 和/usr/lib，而我们的libhello.so不在其中，所以我们需要在执行的时候指明额外的动态链接库文件夹.\n1 2 linux_app/1_C/02_compile$ LD_LIBRARY_PATH=/home/zxf/桌面/linux_app/1_C/02_compile ./main_d Hello world! glibc的动态库和静态库分别位于/usr/lib/x86_64-linux-gnu/目录下的libc.so和libc.a文件中。\nMake基础 # Make是一个自动化构建工具，用于从源代码到构建程序、运行和删除文件等操作。它需要我们提供一个名为Makefile的文本文件。它通常包含了一系列规则，这些规则描述了如何根据源代码文件生成可执行文件或者其他目标文件。Makefile的核心概念是规则和依赖关系，规则定义了如何生成一个或多个目标文件，而依赖关系则指定了生成目标文件所需要的源文件或其他依赖文件。下面我们通过一步一步编写Makefile来学习Makefile规则。\nbuild-essential: 常用的软件包集合，它包含了编译和构建软件所需的基本开发工具和库文件，如：GCC、Make、。。。\n1 linux_app/1_C/03_makefile$ sudo apt install -y build-essential （1）编写Makefile\n1 linux_app/1_C/03_makefile$ touch Makefile 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # Makefile内容通常由以下部分组成 # \u0026lt;目标\u0026gt;: \u0026lt;前置依赖\u0026gt; # \u0026lt;需要执行的命令\u0026gt; # 放在第一个的是默认目标 # main目标依赖main.o和hello.o文件 # 编译的命令: gcc hello.o main.o -o main main: hello.o main.o gcc main.o hello.o -o main # main.o目标依赖main.c hello.h # 编译命令：gcc -c main.c main.o: main.c hello.h gcc -c main.c -o main.o # hello.o目标依赖hello.c hello.h # 编译命令：gcc -c hello.c hello.o: hello.c gcc -c hello.c -o hello.o # clean目标不依赖任何文件 # 作用：清理编译的临时文件 clean: rm main main.o hello.o (2)文件内容解读\n规则是Makefile的构建单元，Make工具通过解析这些规则来执行构建过程。\n① 规则的基本结构\n我们用空行将Makefile的不同规则划分开来。规则有两行构成，第一行为目标和前置依赖，二者通过冒号区分开来，目标在前，前置依赖在后。\n1 2 # \u0026lt;目标\u0026gt;: \u0026lt;前置依赖\u0026gt; # \u0026lt;需要执行的命令\u0026gt; ② 目标：本条规则需要生成的目标文件名。\n③ 前置依赖：生成目标文件需要的依赖文件列表。\n④ 命令：一系列将被Shell执行的命令，用于从前置依赖构建目标。\n注意：Makefile中每个规则的命令必须以一个制表符（tab）开始，而不能是空格。否则会提示“缺失分隔符”。\n⑤ 上文提到，gcc的-c参数不仅可以将汇编代码转换为机器码，还可以直接将C语言源文件转换为机器码，gcc -c main.c就是第二种用法，这里省略了-o main.o。默认情况下，在指定-c参数时，gcc会将与源文件名去掉扩展名再加上后缀.o作为目标文件的名称。\n执行make命令：\n1 2 linux_app/1_C/03_makefile$ make make: \u0026#34;main\u0026#34;已是最新。 提示我们“main”已是最新，这是因为上面的操作已经生成了最终的可执行文件“main”，要看到make的作用，需要先将之前编译好的文件删除\n1 linux_app/1_C/03_makefile$ rm main.o hello.o main 1 2 3 4 5 重新执行make atguigu@ubuntu:~/helloworld$ make gcc -c hello.c gcc -c main.c gcc -o main hello.o main.o 可以看到make首先将hello.c转换为hello.o，然后将main.c转换为main.o，最后生成main可执行文件。\n③ 同理，执行make clean可以执行clean目标：\n1 2 3 同理，执行make clean可以执行clean目标： linux_app/1_C/03_makefile$ make clean rm -f main main.o hello.o 这个目标我们定义了如何清理编译的残留文件和结果。执行这个目标后，我们的编译结果和临时文件就都被清理了。\n这就是Makefile，可以批处理进行一键编译，大大提高了编译效率。\n引入变量 # Makefile中为了方便，可以引入临时变量：\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 # 定义变量 # objects := hello.o main.o objects := hello.o \\ main.o # 在目标中引入变量 main: $(objects) gcc $(objects) -o main main.o: main.c hello.h gcc -c main.c hello.o: hello.c gcc -c hello.c # clean目标中也可以引入变量 clean: rm main $(objects) objects为变量名 :=的组合相当于C语言中的=，表示赋值，:=后面为变量的值 +=的组合为以空格拼接右边的值 \\为续行符，表示命令或定义延续到下一行。此处的作用是将hello.o和main.o合并为一行，此处的定义等价于objects := hello.o main.o。 $(变量名)表示获取变量的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 依赖文件的作用 要注意的是，不指定依赖文件也可以正确执行，但是当没有显式声明的依赖文件发生更改时Make无法追踪。 ① 删除main.o目标中的hello.h依赖, 第一次执行目标生成目标文件 main.o: main.c gcc -c main.c linux_app/1_C/03_makefile$ make main.o gcc -c main.c -o main.o ② 更改hello.h，在其中任意位置加入空行 ③ 重新执行 linux_app/1_C/03_makefile$ make main.o make: “main.o”已是最新。 此时Make工具没有检测到hello.h的更新。 ④ 将Makefile恢复为以下内容。 main.o: main.c hello.h gcc -c main.c ⑤ 执行make linux_app/1_C/03_makefile$ make main.o make: “main.o”已是最新。 ⑥ 更改hello.h，删除任意空行 ⑦ 重新执行 linux_app/1_C/03_makefile$ make main.o gcc -o main hello.o main.o ⑧ 总结：只有在Makefile中显式声明依赖的头文件才会被追踪，当它们发生更改时，重新执行make命令，会再次执行相应规则的命令。 引入伪目标 # （1）伪目标\n伪目标并不代表实际的文件名，它们更多的是行为或动作的标识符。伪目标并不生成具体文件。\n（2）.PHONY目标\n① .PHONY是Makefile中一个特殊的目标，用于声明其它目标是伪目标。\n② 语法：.PHONY: 伪目标名称\n③ 细心的同学可能发现，目标为clean的规则没有前置依赖，这是因为它是用来执行清理操作的，并不是要生成名为clean的文件，因此不需要前置依赖。我们可以将clean声明为伪目标。\n④ 修改Makefile，如下。\n1 2 3 4 5 6 # 声明伪目标 .PHONY: clean # clean目标中也可以引入变量 clean: rm main $(objects) 1 2 linux_app/1_C/03_makefile$ make clean rm main hello.o main.o （3）为什么需要声明伪目标\n我们看到，将clean声明为伪目标后执行make clean的结果与之前并无二致。那么声明伪目标的意义何在？\n执行以下操作。\n① 在helloworld目录下创建名为clean的文件\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 linux_app/1_C/03_makefile$ touch clean linux_app/1_C/03_makefile$ ll 总计 96 drwxrwxr-x 3 atguigu atguigu 4096 3月 28 15:24 ./ drwxr-x--- 23 atguigu atguigu 4096 3月 28 15:00 ../ -rw-rw-r-- 1 atguigu atguigu 0 3月 28 15:24 clean -rw-rw-r-- 1 atguigu atguigu 92 3月 28 14:45 hello.c -rw-rw-r-- 1 atguigu atguigu 69 3月 28 14:45 hello.h -rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i -rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s -rw-rw-r-- 1 atguigu atguigu 1642 3月 27 20:41 libhello.a -rwxrwxr-x 1 atguigu atguigu 16024 3月 28 14:30 main123* -rw-rw-r-- 1 atguigu atguigu 67 3月 28 14:53 main.c -rwxrwxr-x 1 atguigu atguigu 15952 3月 27 20:40 main_d* -rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i -rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s -rw-rw-r-- 1 atguigu atguigu 279 3月 28 14:52 Makefile drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/ linux_app/1_C/03_makefile$ ② 删除Makefile中的.PHONY: clean，保存退出 ③ 重新执行make\n1 2 3 4 linux_app/1_C/03_makefile$ make gcc -c hello.c gcc -c main.c gcc -o main hello.o main.o ④ 重新执行make clean\n1 2 linux_app/1_C/03_makefile$ make clean make: “clean”已是最新。 我们发现，执行make clean并没有像我们预想的那样删除文件，而是告诉我们“clean”已是最新。这是因为，make将clean作为普通目标处理，它先检查clean的依赖（不存在），然后发现clean文件已存在且没有依赖更新（因为不存在，自然不需要更新），因此不会执行规则下的命令，并在控制台输出以上内容。显然，这不是我们期望的行为。\n⑤ 在Makefile中添加.PHONY: clean将clean声明为伪目标，并保存退出\n⑥ 重新执行make clean\n1 2 linux_app/1_C/03_makefile$ make clean rm main hello.o main.o 可以看到，虽然目录下有名为clean的文件，但make仍执行了clean所在规则的命令。这是我们期望的行为。\n⑦ 总结：将某些不生成目标文件的行为或动作（如清理、安装）声明为伪目标可以确保无条件执行规则下的命令。即便执行make命令时当前目录下存在与目标同名的文件，依然可以得到我们期望的效果。\n忽略错误 # 当一个目标下包含多条命令时，如果前面的命令执行失败了，后面的命令就不会执行。\n如果需要后面的命令依然能执行，就需要忽略错误。\n忽略错误的编码只需要在命令的左侧加上“-”。\n1 2 3 4 clean2: rm hello.o rm main.o rm main 如果hello.o不存在，执行clean2目标时，由于第一条命令执行失败，后面的2条命令就不会执行。\n添加忽略错误配置，再执行clean2目标，可以看到后面的2条命令都执行了。\n1 2 3 4 clean2: -rm hello.o rm main.o rm main 目标名和命令中输出文件名的关系 # （1）修改Makefile\n1 2 3 4 5 6 # 定义变量objects objects := hello.o main.o # 在目标中引入变量 main: $(objects) gcc $(objects) -o main123 将命令中最终输出的文件名由main更改为main123。\n保存退出。\n（2）执行make\n上一步已经删除了目录下的main.o，hello.o和main。\n1 2 3 4 linux_app/1_C/03_makefile$ make gcc -c hello.c gcc -c main.c gcc -o main123 hello.o main.o （3）可以看到make的日志中，最终生成的文件为main123，查看当前目录\n1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 linux_app/1_C/03_makefile$ ll 总计 104 drwxrwxr-x 3 atguigu atguigu 4096 3月 28 15:49 ./ drwxr-x--- 23 atguigu atguigu 4096 3月 28 15:49 ../ -rw-rw-r-- 1 atguigu atguigu 0 3月 28 15:24 clean -rw-rw-r-- 1 atguigu atguigu 92 3月 28 14:45 hello.c -rw-rw-r-- 1 atguigu atguigu 69 3月 28 14:45 hello.h -rw-rw-r-- 1 atguigu atguigu 18009 3月 27 17:27 hello.i -rw-rw-r-- 1 atguigu atguigu 1496 3月 28 15:49 hello.o -rw-rw-r-- 1 atguigu atguigu 680 3月 27 17:27 hello.s -rw-rw-r-- 1 atguigu atguigu 1642 3月 27 20:41 libhello.a -rwxrwxr-x 1 atguigu atguigu 16024 3月 28 15:49 main123* -rw-rw-r-- 1 atguigu atguigu 67 3月 28 14:53 main.c -rwxrwxr-x 1 atguigu atguigu 15952 3月 27 20:40 main_d* -rw-rw-r-- 1 atguigu atguigu 225 3月 27 15:18 main.i -rw-rw-r-- 1 atguigu atguigu 1360 3月 28 15:49 main.o -rw-rw-r-- 1 atguigu atguigu 589 3月 27 15:18 main.s -rw-rw-r-- 1 atguigu atguigu 316 3月 28 15:49 Makefile drwxrwxr-x 2 atguigu atguigu 4096 3月 27 14:24 .vscode/ （4）分析\n当前目录下不存在名为main的文件，只有名为mian123的文件，可以得出结论：规则中的命令决定了生成目标文件的名称。目标名并不影响目标文件名。\n（5）再次执行make\n1 2 linux_app/1_C/03_makefile$ make gcc -o main123 hello.o main.o 我们发现，make没有提示目标文件已是最新，而是重新执行了gcc -o main123 hello.o main.o。这是因为，make会按照目标名称在当前目录下追踪目标文件，如果不存在与目标同名的文件，会再次执行规则下的命令。\n（6）总结\nmake输出的文件名取决于规则下的命令，而目标名称决定make追踪的目标文件名。如果二者不一致，make就会认为目标文件不存在而不断执行命令。我们应确保命令生成的目标文件名和目标名一致。\n文件I/O # 我们在linux_app下新建目录2_IO，本章的所有例程全部放到该目录下。\n标准I/O库函数 # 打开/关闭文件 # fopen\n新建01_fopen.c，写入以下内容。\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 #include \u0026lt;stdio.h\u0026gt; int main(int argc, char const *argv[]) { /** * 作用：打开文件 * 参数1： 文件路径 字符串 * 参数2：打开文件的模式 字符串 * r 读模式 文件不存在失败 * w 覆盖写模式 文件不存在自动创建 * a 追加写模式 文件不存在自动创建 * r+ 可读 + 可写，从头开始写一个字符替换一个 * w+ 覆盖写 + 可读 * a+ 追加写 + 可读 * 返回值 * 成功：FILE * 指向结构体FILE的指针 * 失败：NULL * FILE *fopen (const char *__restrict __filename, const char *__restrict __modes) */ FILE *file = fopen(\u0026#34;io.txt\u0026#34;, \u0026#34;a+\u0026#34;); if (file == NULL) { printf(\u0026#34;打开文件失败\\n\u0026#34;); } else { printf(\u0026#34;打开文件成功 (%p)\\n\u0026#34;, file); } return 0; } （2）新建Makefile，写入以下内容\n1 2 3 4 5 6 CC:=gcc fopen: 01_fopen.c -$(CC) $^ -o $@ -./$@ -rm $@ 说明：\n① 有时编译器不只是gcc，我们将编译器定义为变量CC，当切换编译器时只需要更改该变量的定义，而无须更改整个Makefile。\n② $^相当于当前target所有依赖文件列表，此处为fopen_test.c\n③ $@相当于当前target目标文件的名称，此处为fopen_test。\n④ ./$@的作用是执行目标文件\n⑤ rm $@的作用是在执行完毕后删除目标文件，如果没有这个操作，当源文件fopen_test.c未更改时就无法重复执行，会提示： make：“fopen_test”已是最新。此处删除目标文件，使得我们在不更改源文件的情况下可以多次执行。\n⑥ 所有命令前都添加了“-”符号以忽略错误，确保即便上面的命令执行失败，仍然会向下执行。这样做是为了在发生错误时，确保删除目标文件，使得再次执行相同target时不会提示：make：“fopen_test”已是最新，可以重新执行target下的命令。\nfclose\n（1）创建02_fclose.c文件，写入以下内容。\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; int main(int argc, char const *argv[]) { // 打开文件 FILE *file = fopen(\u0026#34;io.txt\u0026#34;, \u0026#34;a+\u0026#34;); if (file == NULL) { printf(\u0026#34;打开文件失败\\n\u0026#34;); } else { printf(\u0026#34;打开文件成功 (%p)\\n\u0026#34;, file); } /** * 作用：关闭文件流 * 参数：被关闭的文件流 前面打开的文件流 * 返回值：成功：为0,失败：为-1（EOF）通常失败会造成系统崩溃 * * int fclose (FILE *__stream); */ // 关闭文件 // fclose(file); int result = fclose(file); if (result != 0) { printf(\u0026#34;关闭文件失败 \\n\u0026#34;); } else { printf(\u0026#34;关闭文件成功 \\n\u0026#34;); } return 0; } Makefile中补充以下内容\n1 2 3 4 fclose: 02_fclose.c -$(CC) $^ -o $@ -./$@ -rm $@ 向文件中写入数据 # ","date":"January 2, 2025","externalUrl":null,"permalink":"/posts/note02/","section":"Posts","summary":"Linux下的C语言开发 # GCC、glibc和GNU C的关系 # GCC全称GNU Compiler Collection，是GNU项目的一部分，主要是一套编译器工具集，支持多种编程语言，包括C、C++、Objective-C、Fortran、Ada、Go和D等。GCC最初作为GNU操作系统的官方编译器，用于编译GNU/Linux系统和应用程序。它是自由软件，遵循GNU GPL（GNU General Public License）开源协议发布。\nGCC的主要作用是将源代码编译成机器语言，生成可执行文件或库文件。它也提供了一些优化选项，可以在编译过程中优化代码，提高程序运行的效率。\n","title":"Linux应用层开发笔记","type":"posts"},{"content":"日出日落就是一天，花开花谢又是一年。冬至阳生，岁回律转。在这辞旧迎新的美好时刻，我向大家致以新年的祝福！\n正文 # 今天是2025年1月1日，我上线了我的图床,也可以通过本站底部（侧边）链接访问。\n为防止短时间访问量过大，目前需要密码访问，需要使用者可以向我发送邮件。\n当前试用密码:373737\n邮箱:2864078813@qq.com\n图床地址: https://image.kaikun.top\n图床意义 # 通俗来讲就是专门用来存放图片，同时允许你把图片对外连接的网上空间，当你在本图床上上传了一张图片，你可以获取多种形式的链接，你可以将其嵌入你的网页，也可以嵌入你的Markdown笔记中，也可以直接使用浏览器来访问，本网站的大部分图片均存储在该图床上。\n虽然是图床，但是也可以上传20M以内的其他格式的文件，比如视频，文档\n图床展示页\n传输完成界面\n限制 # 图片大小不能超过 20MB 目前大概可以存储500万张图片 由于服务器位于国外，速度可能稍慢 请勿上传敏感图片，一经发现，将永久拉黑 感谢大家的支持，愿快乐每1分每1秒陪伴你，幸福1生1世追随你，好运1点1滴涌向你。祝你新年新气象，元旦节快乐！\n","date":"January 1, 2025","externalUrl":null,"permalink":"/posts/imag-online/","section":"Posts","summary":"日出日落就是一天，花开花谢又是一年。冬至阳生，岁回律转。在这辞旧迎新的美好时刻，我向大家致以新年的祝福！\n正文 # 今天是2025年1月1日，我上线了我的图床,也可以通过本站底部（侧边）链接访问。\n为防止短时间访问量过大，目前需要密码访问，需要使用者可以向我发送邮件。\n当前试用密码:373737\n邮箱:2864078813@qq.com\n图床地址: https://image.kaikun.top\n图床意义 # 通俗来讲就是专门用来存放图片，同时允许你把图片对外连接的网上空间，当你在本图床上上传了一张图片，你可以获取多种形式的链接，你可以将其嵌入你的网页，也可以嵌入你的Markdown笔记中，也可以直接使用浏览器来访问，本网站的大部分图片均存储在该图床上。\n","title":"我的图床上线了","type":"posts"},{"content":"","date":"January 1, 2025","externalUrl":null,"permalink":"/tags/%E6%9D%82%E8%B0%88/","section":"Tags","summary":"","title":"杂谈","type":"tags"},{"content":" day01 # 说明：每题10分，共100分，得到60分及以上算通过\n1. 如何理解linux中一切皆文件 # 1 2 3 4 会将各种硬件（CPU/内存）映射成文件 将运行程序进程的内存映射成文件 =\u0026gt; 也就是Linux系统在操作各种硬件、内存时都统一为了文件操作 2. 列出几个Linux根目录下的文件夹及其作用 # 1 2 3 4 /bin 可执行命令程序的目录文件 /home 用户的家目录，可以包含n个用户的文件夹，默认就只有我们创建的登陆用户 /etc 系统配置文件目录， 比如：passwd(用户信息) /usr 用户的安装程序目录， 相当于windows中的Program Files 3. 现在有一个名为xxx的包，写出安装和卸载的命令 # 1 2 sudo apt install xxx sudo apt remove xxx 4. 说出Linux中的2个帮助命令并区别它们 # 1 2 help: 查看内部命令的帮助文档 man: 查看外部命令（bin下）的帮助文档 5. 列出4个常用的快捷键及其作用 # 1 2 3 4 Ctrl + C 或 Q: 停止退出 Ctrl + L 或 clear: 清屏 Tab键： 补全 上下键：查找执行过的命令 6. 列出查看目录内容和进入目录的命令 # 1 2 ls: 列出目录下的内容 =》 ll列出包含隐蔽文件在内的所有文件（夹）详细信息 cd: 进入目录 7. 列出文件或文件夹的创建、删除、拷贝和移动的命令 # 1 2 3 4 5 mkdir: 创建文件夹 touch: 创建文件 rm: 删除文件或文件夹 cp: 拷贝文件或文件夹 mv: 移动文件或文件夹 8. 列出查看文件内容的2个命令，并区别它们 # 1 2 cat: 查看文件的所有内容 tail: 查看文件最后的部分行内容 =》还可以实时监视文件变化 9. 列出输出重定向和输出过滤的命令 # 1 2 3 \u0026gt;: 输出重定向（覆盖） \u0026gt;\u0026gt;: 输出重定向（追加） |grep: 输出过滤 10. 说说你对文件权限的理解 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 文件权限是为了限制不同用户的操作级别，更安全 权限：读（r）、写(w)、执行(x) 文件： 可读：查看文件内容 r 可写：修改文件内容 w 可执行：可运行 x 注意：文件是可重命令和删除得看文件夹的权限 目录 可读：可以ls查看目录下的内容 可写：可以对目录内文件创建和删除，重命令目录 可执行：可进入 内部使用3个二进制位来存储权限：rwx 也就是：4：r, 2: w, 1: x 可以相加得到多个权限：3：wx 5: rx 6: rw 7: rwx day02 # 1. 画图说明VIM三种模式之间的转换和命令模式下的命令 # 2. 求两个数及其之间的数的总和 # 这两个数在执行shell时通过参数携带，如果携带的参数小于2个提示“请指定两个数值参数”，指定的两个数如果相等和为其中一个，这两个数可能第一个大，也可能第二个大，计算出结果后输出“总和为？”\nday03 # 1. 区别git与svn # 1 2 3 SVN: 集中式，开发者本地不能进行版本控制，本地只有仓库的某个版本（一般最新）必须连接上中央仓库才可以 （必须联网）=》现在使用很少 GIT：分布式，开发者本地有仓库的所有版本，本地就可以进行版本控制（不受网络限制） =》基本都使用 2. git的三个分区 # 1 2 3 工作区 暂存区 版本区（本地仓库） 3. git本地版本控制的基本命令（至少5个） # 1 2 3 4 5 6 git init\t初始化仓库，生成工作区 =》 生成 .git文件夹 git add .\t将工作区的更新添加到暂存区 git commit -m \u0026#34;xxx\u0026#34;\t将暂存区的更新提交到本地仓库 git status\t查看工作区和暂存区的状态（是否有更新） git reflog\t查看历史版本信息 git reset --hard 版本号\t得到指定版本的代码 4. 远程仓库相关的几个基本操作（至少3个） # 1 2 3 4 5 1. 创建远程仓库 2. 将本地仓库的代码推送到远程 3. 克隆远程仓库生成本地仓库 4. 如果本地工作区代码有更新，要提交到本地仓库并推送到远程仓库 5. 如果远程仓库有更新，要拉取到本地仓库 5. 区别远程仓库的https与ssh两种方式 # 1 2 https: 每次推送都需要输入用户名和密码 （通过用户名和密码进行身份校验的） ssh: 每次推送不需要输入用户名和密码（通过自动携带的私钥与保存在远程的公钥进行匹配校验的） 6. 说说版本控制冲突产生的原因和解决办法 # 1 2 原因： 当2个人将修改推送到同一个分支时，如果修改了同一个文件，后操作的人就会产生冲突 解决：推送失败时先进行拉取，让工具做带冲突的合并， 先利用工具解决冲突代码（一般留下双方），再提交并推送。 day04 # 1. 区别GCC、glibc与GNU C # 1 2 3 GNU C: C语言标准 glibc: C语言标准的实现 gcc: 将C语言程序处理成可执行二进制程序的编译器 2. 说说C语言程序从源代码到可执行程序经历的四个处理过程 # 1 2 3 4 预处理 编译 汇编 链接 3. 说说使用git管理项目的基本过程 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 配置忽略： 指定git对哪些文件夹或文件不进行版本管理 .gitignore 创建本地仓库 git init git add . git commit -m \u0026#34;初始化项目\u0026#34; 创建远程仓库 在gitee上创建仓库 将本地仓库推送到远程仓库 git remote add origin url git push -u origin 主分支 本地修改代码先提交到本地仓库再推送到远程仓库 git add . git commit -m \u0026#34;xxx\u0026#34; git push origin master 如果远程仓库有更新，拉取到本地 git pull origin master 克隆仓库 git clone https://gitee.com/zxfjd3g/linux_app 4. 列出一些文件IO库函数（至少6个） # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 fopen(): 打开一个文件流 fclose()：关闭文件流 fputc()：向文件中写入一个字符 fputs()：向文件中写入一个字符串 fprintf()：向文件中写入指定格式的字符串 fgetc()：读取文件中的一个字符 fgets()：读取文件中的一个字符串 fscanf(): 读取文件中的一个特定格式的字符串，并保存到多个变量 stdin：读取终端的标准输入流 stdout：写到终端的标准输出流 stderr：写到终端的标准错误输出流 输出重定向：可以将stdout和stderr的数据重定向到不同的日志文件中 errno：错误编号， 后面可以根据strerrono得到错误信息 perror()：输出错误信息 5. 使用IO的系统调用实现文件拷贝的功能，并使用make编译运行 # day05 # 1. 区别进程和程序 # 程序：程序代码，静态的 进程：正在运行的程序，动态的，包括：内存（代码、堆、栈等）和分配给它的其它系统资源（比如：文件描述符） 2. 说说进程内存的组成部分 # 文本（程序代码： 程序代码、指令 初始化数据：已经初始化的全局变量和静态变量 未初始化数据：没有初始化的全局变量和静态变量 堆（heap）：malloc动态分配 未分配内存：供堆和栈扩展使用的区域 栈：局部变量、函数参数、返回地址等 参数与环境： 程序参数和环境变量 3. 如何验证linux中程序运行内存是虚拟内存而不是物理内存？ # 在父进程中定义一个变量val并赋值 通过fork来创建一个子进程, 此时子进程也就有了val变量，且值与父进程是一样的 查看父子进程的val的地址，发现地址值一样 =》程序中获取的不是真实内存，而是虚拟内存 4. 列出进程相关的的三个函数，并说明其作用 # fork(): 创建当前进程的子进程 execve(): 在当前进程（一般是子进程）中执行另一个程序（自定义或命令） waitpid(): 等待指定pid的子进程执行结束 5. 说出进程间通信的三种方式，并简单描述一下其特点 # 管道：利用管道实现两个进程间的单向通信（半双工） 共享内存：利用共享内存实现两个进程间通信，不限制单向，但要注意一个写一个读即可 消息队列：一个进程向队列发n个数据，另一个进程从队列中顺序接收获取数据 day06 # 1. 区别进程与线程 # 都是Linux系统可以进行调度执行的执行单元 进程是一个运行的程序，有独立私有的内存空间和资源 线程是进程内的一个执行单元，一个进程内的所有线程共享进程的空间和资源 线程间共享更简单，一个全局变量就搞定，但带来的线程同步问题 线程间切换相比进程间切换消耗更小，更适合做高并发处理。 2. 列出线程控制的4个方法及其作用 # pthread_create(): 创建线程 pthread_join()：等待指定线程结束 pthread_cancel()：取消指定线程 pthread_exit()：结束当前进程 3. 列出线程间同步的几种方式及其特点 # 互斥锁：限制多线程共享资源数据在某个时间段内只能有一个线程操作数据 条件变量：控制多个线程间的切换执行 信号量：控制多个线程的执行顺序 4. 列出消息队列的几个操作 # 创建队列：mq_open() 向队列发消息: mq_send() 接收获取队列中的消息: mq_receive() 删除队列: mq_unlink() 5. 列出互斥锁的几个操作 # 初始化锁: 得到内置的初始化锁 加锁：在操作共享数据前 lock() 解锁：在操作共享数据后 unlock() 销毁锁：在最后 destroy() ","date":"December 30, 2024","externalUrl":null,"permalink":"/posts/note01/","section":"Posts","summary":"day01 # 说明：每题10分，共100分，得到60分及以上算通过\n1. 如何理解linux中一切皆文件 # 1 2 3 4 会将各种硬件（CPU/内存）映射成文件 将运行程序进程的内存映射成文件 =\u003e 也就是Linux系统在操作各种硬件、内存时都统一为了文件操作 2. 列出几个Linux根目录下的文件夹及其作用 # 1 2 3 4 /bin 可执行命令程序的目录文件 /home 用户的家目录，可以包含n个用户的文件夹，默认就只有我们创建的登陆用户 /etc 系统配置文件目录， 比如：passwd(用户信息) /usr 用户的安装程序目录， 相当于windows中的Program Files 3. 现在有一个名为xxx的包，写出安装和卸载的命令 # 1 2 sudo apt install xxx sudo apt remove xxx 4. 说出Linux中的2个帮助命令并区别它们 # 1 2 help: 查看内部命令的帮助文档 man: 查看外部命令（bin下）的帮助文档 5. 列出4个常用的快捷键及其作用 # 1 2 3 4 Ctrl + C 或 Q: 停止退出 Ctrl + L 或 clear: 清屏 Tab键： 补全 上下键：查找执行过的命令 6. 列出查看目录内容和进入目录的命令 # 1 2 ls: 列出目录下的内容 =》 ll列出包含隐蔽文件在内的所有文件（夹）详细信息 cd: 进入目录 7. 列出文件或文件夹的创建、删除、拷贝和移动的命令 # 1 2 3 4 5 mkdir: 创建文件夹 touch: 创建文件 rm: 删除文件或文件夹 cp: 拷贝文件或文件夹 mv: 移动文件或文件夹 8. 列出查看文件内容的2个命令，并区别它们 # 1 2 cat: 查看文件的所有内容 tail: 查看文件最后的部分行内容 =》还可以实时监视文件变化 9. 列出输出重定向和输出过滤的命令 # 1 2 3 \u003e: 输出重定向（覆盖） \u003e\u003e: 输出重定向（追加） |grep: 输出过滤 10. 说说你对文件权限的理解 # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 文件权限是为了限制不同用户的操作级别，更安全 权限：读（r）、写(w)、执行(x) 文件： 可读：查看文件内容 r 可写：修改文件内容 w 可执行：可运行 x 注意：文件是可重命令和删除得看文件夹的权限 目录 可读：可以ls查看目录下的内容 可写：可以对目录内文件创建和删除，重命令目录 可执行：可进入 内部使用3个二进制位来存储权限：rwx 也就是：4：r, 2: w, 1: x 可以相加得到多个权限：3：wx 5: rx 6: rw 7: rwx day02 # 1. 画图说明VIM三种模式之间的转换和命令模式下的命令 # 2. 求两个数及其之间的数的总和 # 这两个数在执行shell时通过参数携带，如果携带的参数小于2个提示“请指定两个数值参数”，指定的两个数如果相等和为其中一个，这两个数可能第一个大，也可能第二个大，计算出结果后输出“总和为？”\n","title":"一份Linux测试题","type":"posts"},{"content":"时节不拘，岁月如流，即将23岁的我，在24年末上线了我的个人博客\n前言 # 这也算是一个偶然，这段时间我格外的忙碌，睡眠也严重不足，但是作为白嫖党，发现了可以利用github搭建博客的方案，自然是不能浪费，耗时一天，参悟透了这套博客源码，买个域名，这个网站算是初步搭建完成了。\n打算 # 白嫖了网站，自然不能忘记分享，等新年假期，做一份搭建此网站的攻略，网站底部有我B站的链接,当然点击这行文字也可以,我会将视频放在那里。\n以后这个网站将放一些我的笔记，杂谈，以及我的一些项目，以及一些想要分享的资源，尽量最大化程度的利用好这个网站，毕竟域名钱也不便宜，不能浪费。\n发财 暴富 发大财 2024 --\u003e 2025 --\u003e 2026 --\u003e2027-----\u003e未来 健康 如意 幸福 联系方式 # 邮箱 2864078813@qq.com\n如果有好的想法可以一起讨论哦！！ 最后祝大家身体健康万事如意\n感谢 # 感谢 GitHub Pages\n凡王之血，必以剑终\n","date":"December 29, 2024","externalUrl":null,"permalink":"/posts/helloworld/","section":"Posts","summary":"时节不拘，岁月如流，即将23岁的我，在24年末上线了我的个人博客\n前言 # 这也算是一个偶然，这段时间我格外的忙碌，睡眠也严重不足，但是作为白嫖党，发现了可以利用github搭建博客的方案，自然是不能浪费，耗时一天，参悟透了这套博客源码，买个域名，这个网站算是初步搭建完成了。\n打算 # 白嫖了网站，自然不能忘记分享，等新年假期，做一份搭建此网站的攻略，网站底部有我B站的链接,当然点击这行文字也可以,我会将视频放在那里。\n","title":"hello world","type":"posts"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"Hey，我是 mkk，一名嵌入式开发工程师。\n我在做什么 # 主要从事嵌入式系统的开发工作，涉及 UWB 定位、FreeRTOS、GD32/ESP32 等方向。工作之余喜欢折腾各种技术，记录学习过程中的笔记和思考。\n技术栈 # 嵌入式开发 — C/FreeRTOS/GD32/ESP32 Linux 应用层开发 — 进程线程、文件I/O、Makefile Python — 脚本与工具开发 物联网 — UWB、LoRa、MQTT 联系方式 # 邮箱：2864078813@qq.com GitHub：mkktop B站：mkk 项目 # 我的图床 — 自建图片托管服务 我的AI — AI 工具服务 这个博客用来分享我的技术笔记、学习心得和一些随笔。如果你有想法或问题，欢迎通过邮箱联系我，一起交流。\n","externalUrl":null,"permalink":"/about/","section":"mkk Blog","summary":"Hey，我是 mkk，一名嵌入式开发工程师。\n我在做什么 # 主要从事嵌入式系统的开发工作，涉及 UWB 定位、FreeRTOS、GD32/ESP32 等方向。工作之余喜欢折腾各种技术，记录学习过程中的笔记和思考。\n技术栈 # 嵌入式开发 — C/FreeRTOS/GD32/ESP32 Linux 应用层开发 — 进程线程、文件I/O、Makefile Python — 脚本与工具开发 物联网 — UWB、LoRa、MQTT 联系方式 # 邮箱：2864078813@qq.com GitHub：mkktop B站：mkk 项目 # 我的图床 — 自建图片托管服务 我的AI — AI 工具服务 这个博客用来分享我的技术笔记、学习心得和一些随笔。如果你有想法或问题，欢迎通过邮箱联系我，一起交流。\n","title":"关于","type":"page"}]